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