fix invalid cast runtime warning
[claws.git] / src / advsearch.c
1 /*
2  * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 2012-2014 the Claws Mail team
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program. If not, see <http://www.gnu.org/licenses/>.
17  * 
18  */
19
20 #ifdef HAVE_CONFIG_H
21 # include "config.h"
22 # include "claws-features.h"
23 #endif
24
25 #include "advsearch.h"
26
27 #include <glib.h>
28 #include <ctype.h>
29
30 #include "matcher.h"
31 #include "matcher_parser.h"
32 #include "utils.h"
33 #include "prefs_common.h"
34 #include "timing.h"
35
36 struct _AdvancedSearch {
37         struct {
38                 AdvancedSearchType       type;
39                 gchar                   *matchstring;
40         } request;
41
42         MatcherList                     *predicate;
43         gboolean                         is_fast;
44         gboolean                         search_aborted;
45
46         struct {
47                 gboolean (*cb)(gpointer data, guint at, guint matched, guint total);
48                 gpointer data;
49         } on_progress_cb;
50         struct {
51                 void (*cb)(gpointer data);
52                 gpointer data;
53         } on_error_cb;
54 };
55
56 void advsearch_set_on_progress_cb(AdvancedSearch *search, gboolean (*cb)(gpointer, guint, guint, guint), gpointer data)
57 {
58         search->on_progress_cb.cb = cb;
59         search->on_progress_cb.data = data;
60 }
61
62 void advsearch_set_on_error_cb(AdvancedSearch* search, void (*cb)(gpointer data), gpointer data)
63 {
64         search->on_error_cb.cb = cb;
65         search->on_error_cb.data = data;
66 }
67
68 static void prepare_matcher(AdvancedSearch *search);
69 static gboolean search_impl(MsgInfoList **messages, AdvancedSearch* search,
70                             FolderItem* folderItem, gboolean recursive);
71
72 // --------------------------
73
74 AdvancedSearch* advsearch_new()
75 {
76         AdvancedSearch *result;
77
78         result = g_new0(AdvancedSearch, 1);
79
80         return result;
81 }
82
83 void advsearch_free(AdvancedSearch *search)
84 {
85         if (search->predicate != NULL)
86                 matcherlist_free(search->predicate);
87
88         g_free(search->request.matchstring);
89         g_free(search);
90 }
91
92 void advsearch_set(AdvancedSearch *search, AdvancedSearchType type, const gchar *matchstring)
93 {
94         cm_return_if_fail(search != NULL);
95
96         search->request.type = type;
97
98         g_free(search->request.matchstring);
99         search->request.matchstring = g_strdup(matchstring);
100
101         prepare_matcher(search);
102 }
103
104 gboolean advsearch_is_fast(AdvancedSearch *search)
105 {
106         cm_return_val_if_fail(search != NULL, FALSE);
107
108         return search->is_fast;
109 }
110
111 gboolean advsearch_has_proper_predicate(AdvancedSearch *search)
112 {
113         cm_return_val_if_fail(search != NULL, FALSE);
114
115         return search->predicate != NULL;
116 }
117
118 gboolean advsearch_search_msgs_in_folders(AdvancedSearch* search, MsgInfoList **messages,
119                                           FolderItem* folderItem, gboolean recursive)
120 {
121         if (search == NULL || search->predicate == NULL)
122                 return FALSE;
123
124         search->search_aborted = FALSE;
125         return search_impl(messages, search, folderItem, recursive);
126 }
127
128 void advsearch_abort(AdvancedSearch *search)
129 {
130         search->search_aborted = TRUE;
131 }
132
133 gchar *advsearch_expand_search_string(const gchar *search_string)
134 {
135         int i = 0;
136         gchar term_char, save_char;
137         gchar *cmd_start, *cmd_end;
138         GString *matcherstr;
139         gchar *returnstr = NULL;
140         gchar *copy_str;
141         gboolean casesens, dontmatch, regex;
142         /* list of allowed pattern abbreviations */
143         struct {
144                 gchar           *abbreviated;   /* abbreviation */
145                 gchar           *command;       /* actual matcher command */
146                 gint            numparams;      /* number of params for cmd */
147                 gboolean        qualifier;      /* do we append stringmatch operations */
148                 gboolean        quotes;         /* do we need quotes */
149         }
150         cmds[] = {
151                 { "a",  "all",                          0,      FALSE,  FALSE },
152                 { "ag", "age_greater",                  1,      FALSE,  FALSE },
153                 { "al", "age_lower",                    1,      FALSE,  FALSE },
154                 { "agh","age_greater_hours",            1,      FALSE,  FALSE },
155                 { "alh","age_lower_hours",              1,      FALSE,  FALSE },
156                 { "b",  "body_part",                    1,      TRUE,   TRUE  },
157                 { "B",  "message",                      1,      TRUE,   TRUE  },
158                 { "c",  "cc",                           1,      TRUE,   TRUE  },
159                 { "C",  "to_or_cc",                     1,      TRUE,   TRUE  },
160                 { "D",  "deleted",                      0,      FALSE,  FALSE },
161                 { "da", "date_after",                   1,      FALSE,  TRUE  },
162                 { "db", "date_before",                  1,      FALSE,  TRUE  },
163                 { "e",  "header \"Sender\"",            1,      TRUE,   TRUE  },
164                 { "E",  "execute",                      1,      FALSE,  TRUE  },
165                 { "f",  "from",                         1,      TRUE,   TRUE  },
166                 { "F",  "forwarded",                    0,      FALSE,  FALSE },
167                 { "h",  "headers_part",                 1,      TRUE,   TRUE  },
168                 { "H",  "headers_cont",                 1,      TRUE,   TRUE  },
169                 { "ha", "has_attachments",              0,      FALSE,  FALSE },
170                 { "i",  "messageid",                    1,      TRUE,   TRUE  },
171                 { "I",  "inreplyto",                    1,      TRUE,   TRUE  },
172                 { "k",  "colorlabel",                   1,      FALSE,  FALSE },
173                 { "L",  "locked",                       0,      FALSE,  FALSE },
174                 { "n",  "newsgroups",                   1,      TRUE,   TRUE  },
175                 { "N",  "new",                          0,      FALSE,  FALSE },
176                 { "O",  "~new",                         0,      FALSE,  FALSE },
177                 { "r",  "replied",                      0,      FALSE,  FALSE },
178                 { "R",  "~unread",                      0,      FALSE,  FALSE },
179                 { "s",  "subject",                      1,      TRUE,   TRUE  },
180                 { "se", "score_equal",                  1,      FALSE,  FALSE },
181                 { "sg", "score_greater",                1,      FALSE,  FALSE },
182                 { "sl", "score_lower",                  1,      FALSE,  FALSE },
183                 { "Se", "size_equal",                   1,      FALSE,  FALSE },
184                 { "Sg", "size_greater",                 1,      FALSE,  FALSE },
185                 { "Ss", "size_smaller",                 1,      FALSE,  FALSE },
186                 { "t",  "to",                           1,      TRUE,   TRUE  },
187                 { "tg", "tag",                          1,      TRUE,   TRUE  },
188                 { "T",  "marked",                       0,      FALSE,  FALSE },
189                 { "U",  "unread",                       0,      FALSE,  FALSE },
190                 { "x",  "references",                   1,      TRUE,   TRUE  },
191                 { "X",  "test",                         1,      FALSE,  FALSE },
192                 { "y",  "header \"X-Label\"",           1,      TRUE,   TRUE  },
193                 { "&",  "&",                            0,      FALSE,  FALSE },
194                 { "|",  "|",                            0,      FALSE,  FALSE },
195                 { "p",  "partial",                      0,      FALSE,  FALSE },
196                 { NULL, NULL,                           0,      FALSE,  FALSE }
197         };
198
199         if (search_string == NULL)
200                 return NULL;
201
202         copy_str = g_strdup(search_string);
203
204         matcherstr = g_string_sized_new(16);
205         cmd_start = copy_str;
206         while (cmd_start && *cmd_start) {
207                 /* skip all white spaces */
208                 while (*cmd_start && isspace((guchar)*cmd_start))
209                         cmd_start++;
210                 cmd_end = cmd_start;
211
212                 /* extract a command */
213                 while (*cmd_end && !isspace((guchar)*cmd_end))
214                         cmd_end++;
215
216                 /* save character */
217                 save_char = *cmd_end;
218                 *cmd_end = '\0';
219
220                 dontmatch = FALSE;
221                 casesens = FALSE;
222                 regex = FALSE;
223
224                 /* ~ and ! mean logical NOT */
225                 if (*cmd_start == '~' || *cmd_start == '!')
226                 {
227                         dontmatch = TRUE;
228                         cmd_start++;
229                 }
230                 /* % means case sensitive match */
231                 if (*cmd_start == '%')
232                 {
233                         casesens = TRUE;
234                         cmd_start++;
235                 }
236                 /* # means regex match */
237                 if (*cmd_start == '#') {
238                         regex = TRUE;
239                         cmd_start++;
240                 }
241
242                 /* find matching abbreviation */
243                 for (i = 0; cmds[i].command; i++) {
244                         if (!strcmp(cmd_start, cmds[i].abbreviated)) {
245                                 /* restore character */
246                                 *cmd_end = save_char;
247
248                                 /* copy command */
249                                 if (matcherstr->len > 0) {
250                                         g_string_append(matcherstr, " ");
251                                 }
252                                 if (dontmatch)
253                                         g_string_append(matcherstr, "~");
254                                 g_string_append(matcherstr, cmds[i].command);
255                                 g_string_append(matcherstr, " ");
256
257                                 /* stop if no params required */
258                                 if (cmds[i].numparams == 0)
259                                         break;
260
261                                 /* extract a parameter, allow quotes */
262                                 while (*cmd_end && isspace((guchar)*cmd_end))
263                                         cmd_end++;
264
265                                 cmd_start = cmd_end;
266                                 if (*cmd_start == '"') {
267                                         term_char = '"';
268                                         cmd_end++;
269                                 }
270                                 else
271                                         term_char = ' ';
272
273                                 /* extract actual parameter */
274                                 while ((*cmd_end) && (*cmd_end != term_char))
275                                         cmd_end++;
276
277                                 if (*cmd_end == '"')
278                                         cmd_end++;
279
280                                 save_char = *cmd_end;
281                                 *cmd_end = '\0';
282
283                                 if (cmds[i].qualifier) {
284                                         if (casesens)
285                                                 g_string_append(matcherstr, regex ? "regexp " : "match ");
286                                         else
287                                                 g_string_append(matcherstr, regex ? "regexpcase " : "matchcase ");
288                                 }
289
290                                 /* do we need to add quotes ? */
291                                 if (cmds[i].quotes && term_char != '"')
292                                         g_string_append(matcherstr, "\"");
293
294                                 /* copy actual parameter */
295                                 g_string_append(matcherstr, cmd_start);
296
297                                 /* do we need to add quotes ? */
298                                 if (cmds[i].quotes && term_char != '"')
299                                         g_string_append(matcherstr, "\"");
300
301                                 /* restore original character */
302                                 *cmd_end = save_char;
303
304                                 break;
305                         }
306                 }
307
308                 if (*cmd_end)
309                         cmd_end++;
310                 cmd_start = cmd_end;
311         }
312
313         g_free(copy_str);
314
315         /* return search string if no match is found to allow
316            all available filtering expressions in advanced search */
317         if (matcherstr->len > 0) {
318                 returnstr = g_string_free(matcherstr, FALSE);
319         } else {
320                 returnstr = g_strdup(search_string);
321                 g_string_free(matcherstr, TRUE);
322         }
323         return returnstr;
324 }
325
326 static void prepare_matcher_extended(AdvancedSearch *search)
327 {
328         gchar *newstr = advsearch_expand_search_string(search->request.matchstring);
329
330         if (newstr && newstr[0] != '\0') {
331                 search->predicate = matcher_parser_get_cond(newstr, &search->is_fast);
332                 g_free(newstr);
333         }
334 }
335
336 #define debug_matcher_list(prefix, list)                                        \
337 do {                                                                            \
338         gchar *str = list ? matcherlist_to_string(list) : g_strdup("(NULL)");   \
339                                                                                 \
340         debug_print("%s: %s\n", prefix, str);                                   \
341                                                                                 \
342         g_free(str);                                                            \
343 } while(0)
344
345 static void prepare_matcher_tag(AdvancedSearch *search)
346 {
347         gchar **words = search->request.matchstring 
348                         ? g_strsplit(search->request.matchstring, " ", -1)
349                         : NULL;
350         gint i = 0;
351
352         if (search->predicate == NULL) {
353                 search->predicate = g_new0(MatcherList, 1);
354                 search->predicate->bool_and = FALSE;
355                 search->is_fast = TRUE;
356         }
357
358         while (words && words[i] && *words[i]) {
359                 MatcherProp *matcher;
360
361                 g_strstrip(words[i]);
362
363                 matcher = matcherprop_new(MATCHCRITERIA_TAG, NULL,
364                                           MATCHTYPE_MATCHCASE, words[i], 0);
365
366                 search->predicate->matchers = g_slist_prepend(search->predicate->matchers, matcher);
367
368                 i++;
369         }
370         g_strfreev(words);
371 }
372
373 static void prepare_matcher_header(AdvancedSearch *search, gint match_header)
374 {
375         MatcherProp *matcher;
376
377         if (search->predicate == NULL) {
378                 search->predicate = g_new0(MatcherList, 1);
379                 search->predicate->bool_and = FALSE;
380                 search->is_fast = TRUE;
381         }
382
383         matcher = matcherprop_new(match_header, NULL, MATCHTYPE_MATCHCASE,
384                         search->request.matchstring, 0);
385
386         search->predicate->matchers = g_slist_prepend(search->predicate->matchers, matcher);
387 }
388
389 static void prepare_matcher_mixed(AdvancedSearch *search)
390 {
391         prepare_matcher_tag(search);
392         debug_matcher_list("tag matcher list", search->predicate);
393
394         /* we want an OR search */
395         if (search->predicate)
396                 search->predicate->bool_and = FALSE;
397
398         prepare_matcher_header(search, MATCHCRITERIA_SUBJECT);
399         debug_matcher_list("tag + subject matcher list", search->predicate);
400         prepare_matcher_header(search, MATCHCRITERIA_FROM);
401         debug_matcher_list("tag + subject + from matcher list", search->predicate);
402         prepare_matcher_header(search, MATCHCRITERIA_TO);
403         debug_matcher_list("tag + subject + from + to matcher list", search->predicate);
404         prepare_matcher_header(search, MATCHCRITERIA_CC);
405         debug_matcher_list("tag + subject + from + to + cc matcher list", search->predicate);
406 }
407
408 static void prepare_matcher(AdvancedSearch *search)
409 {
410         const gchar *search_string;
411
412         cm_return_if_fail(search != NULL);
413
414         if (search->predicate) {
415                 matcherlist_free(search->predicate);
416                 search->predicate = NULL;
417         }
418
419         search_string = search->request.matchstring;
420
421         if (search_string == NULL || search_string[0] == '\0')
422                 return;
423
424         switch (search->request.type) {
425                 case ADVANCED_SEARCH_SUBJECT:
426                         prepare_matcher_header(search, MATCHCRITERIA_SUBJECT);
427                         debug_matcher_list("subject search", search->predicate);
428                         break;
429
430                 case ADVANCED_SEARCH_FROM:
431                         prepare_matcher_header(search, MATCHCRITERIA_FROM);
432                         debug_matcher_list("from search", search->predicate);
433                         break;
434
435                 case ADVANCED_SEARCH_TO:
436                         prepare_matcher_header(search, MATCHCRITERIA_TO);
437                         debug_matcher_list("to search", search->predicate);
438                         break;
439
440                 case ADVANCED_SEARCH_TAG:
441                         prepare_matcher_tag(search);
442                         debug_matcher_list("tag search", search->predicate);
443                         break;
444
445                 case ADVANCED_SEARCH_MIXED:
446                         prepare_matcher_mixed(search);
447                         debug_matcher_list("mixed search", search->predicate);
448                         break;
449
450                 case ADVANCED_SEARCH_EXTENDED:
451                         prepare_matcher_extended(search);
452                         debug_matcher_list("extended search", search->predicate);
453                         break;
454
455                 default:
456                         debug_print("unknown search type (%d)\n", search->request.type);
457                         break;
458         }
459 }
460
461 static gboolean search_progress_notify_cb(gpointer data, gboolean on_server, guint at,
462                 guint matched, guint total)
463 {
464         AdvancedSearch *search = (AdvancedSearch*) data;
465
466         if (search->search_aborted)
467                 return FALSE;
468
469         if (on_server || search->on_progress_cb.cb == NULL)
470                 return TRUE;
471
472         return search->on_progress_cb.cb(search->on_progress_cb.data, at, matched, total);
473 }
474
475 static gboolean search_filter_folder(MsgNumberList **msgnums, AdvancedSearch *search,
476                                           FolderItem *folderItem, gboolean onServer)
477 {
478         gint matched;
479         gboolean tried_server = onServer;
480
481         matched = folder_item_search_msgs(folderItem->folder,
482                 folderItem,
483                 msgnums,
484                 &onServer,
485                 search->predicate,
486                 search_progress_notify_cb,
487                 search);
488
489         if (matched < 0) {
490                 if (search->on_error_cb.cb != NULL)
491                         search->on_error_cb.cb(search->on_error_cb.data);
492                 return FALSE;
493         }
494
495         if (folderItem->folder->klass->supports_server_search && tried_server && !onServer) {
496                 return search_filter_folder(msgnums, search, folderItem, onServer);
497         } else {
498                 return TRUE;
499         }
500 }
501
502 static gboolean search_impl(MsgInfoList **messages, AdvancedSearch* search,
503                             FolderItem* folderItem, gboolean recursive)
504 {
505         if (recursive) {
506                 START_TIMING("recursive");
507                 if (!search_impl(messages, search, folderItem, FALSE)) {
508                         END_TIMING();
509                         return FALSE;
510                 }
511                 if (folderItem->node->children != NULL && !search->search_aborted) {
512                         GNode *node;
513                         for (node = folderItem->node->children; node != NULL; node = node->next) {
514                                 FolderItem *cur = FOLDER_ITEM(node->data);
515                                 debug_print("in: %s\n", cur->path);
516                                 if (!search_impl(messages, search, cur, TRUE)) {
517                                         END_TIMING();
518                                         return FALSE;
519                                 }
520                         }
521                 }
522                 END_TIMING();
523         } else if (!folderItem->no_select) {
524                 MsgNumberList *msgnums = NULL;
525                 MsgNumberList *cur;
526                 MsgInfoList *msgs = NULL;
527                 gboolean can_search_on_server = folderItem->folder->klass->supports_server_search;
528                 START_TIMING("folder");
529                 if (!search_filter_folder(&msgnums, search, folderItem,
530                                           can_search_on_server)) {
531                         g_slist_free(msgnums);
532                         END_TIMING();
533                         return FALSE;
534                 }
535
536                 for (cur = msgnums; cur != NULL; cur = cur->next) {
537                         MsgInfo *msg = folder_item_get_msginfo(folderItem, GPOINTER_TO_UINT(cur->data));
538
539                         msgs = g_slist_prepend(msgs, msg);
540                 }
541
542                 while (msgs != NULL) {
543                         MsgInfoList *front = msgs;
544
545                         msgs = msgs->next;
546
547                         front->next = *messages;
548                         *messages = front;
549                 }
550
551                 g_slist_free(msgnums);
552                 END_TIMING();
553         }
554
555         return TRUE;
556 }