2 * Claws Mail -- a GTK based, lightweight, and fast e-mail client
3 * Copyright (C) 2012-2023 the Claws Mail team
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.
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.
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/>.
21 # include "claws-features.h"
24 #include "advsearch.h"
30 #include "matcher_parser.h"
32 #include "prefs_common.h"
35 struct _AdvancedSearch
{
37 AdvancedSearchType type
;
41 MatcherList
*predicate
;
43 gboolean search_aborted
;
46 gboolean (*cb
)(gpointer data
, guint at
, guint matched
, guint total
);
50 void (*cb
)(gpointer data
);
55 void advsearch_set_on_progress_cb(AdvancedSearch
*search
, gboolean (*cb
)(gpointer
, guint
, guint
, guint
), gpointer data
)
57 search
->on_progress_cb
.cb
= cb
;
58 search
->on_progress_cb
.data
= data
;
61 void advsearch_set_on_error_cb(AdvancedSearch
* search
, void (*cb
)(gpointer data
), gpointer data
)
63 search
->on_error_cb
.cb
= cb
;
64 search
->on_error_cb
.data
= data
;
67 static void prepare_matcher(AdvancedSearch
*search
);
68 static gboolean
search_impl(MsgInfoList
**messages
, AdvancedSearch
* search
,
69 FolderItem
* folderItem
, gboolean recursive
);
71 // --------------------------
73 AdvancedSearch
* advsearch_new()
75 AdvancedSearch
*result
;
77 result
= g_new0(AdvancedSearch
, 1);
82 void advsearch_free(AdvancedSearch
*search
)
84 if (search
->predicate
!= NULL
)
85 matcherlist_free(search
->predicate
);
87 g_free(search
->request
.matchstring
);
91 void advsearch_set(AdvancedSearch
*search
, AdvancedSearchType type
, const gchar
*matchstring
)
93 cm_return_if_fail(search
!= NULL
);
95 search
->request
.type
= type
;
97 g_free(search
->request
.matchstring
);
98 search
->request
.matchstring
= g_strdup(matchstring
);
100 prepare_matcher(search
);
103 gboolean
advsearch_is_fast(AdvancedSearch
*search
)
105 cm_return_val_if_fail(search
!= NULL
, FALSE
);
107 return search
->is_fast
;
110 gboolean
advsearch_has_proper_predicate(AdvancedSearch
*search
)
112 cm_return_val_if_fail(search
!= NULL
, FALSE
);
114 return search
->predicate
!= NULL
;
117 gboolean
advsearch_search_msgs_in_folders(AdvancedSearch
* search
, MsgInfoList
**messages
,
118 FolderItem
* folderItem
, gboolean recursive
)
120 if (search
== NULL
|| search
->predicate
== NULL
)
123 search
->search_aborted
= FALSE
;
124 return search_impl(messages
, search
, folderItem
, recursive
);
127 void advsearch_abort(AdvancedSearch
*search
)
129 search
->search_aborted
= TRUE
;
133 static void advsearch_extract_param(GString
*matcherstr
, gchar
**cmd_start_
, gchar
**cmd_end_
, gboolean quotes
, gboolean qualifier
, gboolean casesens
, gboolean regex
)
135 gchar
*cmd_start
, *cmd_end
;
136 gchar term_char
, save_char
;
138 cmd_start
= *cmd_start_
;
141 /* extract a parameter, allow quotes */
142 while (*cmd_end
&& isspace((guchar
)*cmd_end
))
146 if (*cmd_start
== '"') {
153 /* extract actual parameter */
154 while ((*cmd_end
) && (*cmd_end
!= term_char
))
160 save_char
= *cmd_end
;
165 g_string_append(matcherstr
, regex
? "regexp " : "match ");
167 g_string_append(matcherstr
, regex
? "regexpcase " : "matchcase ");
170 /* do we need to add quotes ? */
171 if (quotes
&& term_char
!= '"')
172 g_string_append(matcherstr
, "\"");
174 /* copy actual parameter */
175 g_string_append(matcherstr
, cmd_start
);
177 /* do we need to add quotes ? */
178 if (quotes
&& term_char
!= '"')
179 g_string_append(matcherstr
, "\"");
181 /* restore original character */
182 *cmd_end
= save_char
;
185 *cmd_start_
= cmd_start
;
189 gchar
*advsearch_expand_search_string(const gchar
*search_string
)
192 gchar
*cmd_start
, *cmd_end
;
195 gchar
*returnstr
= NULL
;
197 gboolean casesens
, dontmatch
, regex
;
198 /* list of allowed pattern abbreviations */
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 */
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
}
255 if (search_string
== NULL
)
258 copy_str
= g_strdup(search_string
);
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
))
268 /* extract a command */
269 while (*cmd_end
&& !isspace((guchar
)*cmd_end
))
273 save_char
= *cmd_end
;
280 /* ~ and ! mean logical NOT */
281 if (*cmd_start
== '~' || *cmd_start
== '!')
286 /* % means case sensitive match */
287 if (*cmd_start
== '%')
292 /* # means regex match */
293 if (*cmd_start
== '#') {
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
;
305 if (matcherstr
->len
> 0) {
306 g_string_append(matcherstr
, " ");
309 g_string_append(matcherstr
, "~");
310 g_string_append(matcherstr
, cmds
[i
].command
);
311 g_string_append(matcherstr
, " ");
313 /* stop if no params required */
314 if (cmds
[i
].numparams
== 0)
317 /* extract a first parameter before the final matched one */
318 if (cmds
[i
].numparams
== 2)
320 advsearch_extract_param(matcherstr
, &cmd_start
, &cmd_end
, cmds
[i
].quotes
, FALSE
, casesens
, regex
);
321 g_string_append(matcherstr
, " ");
323 advsearch_extract_param(matcherstr
, &cmd_start
, &cmd_end
, cmds
[i
].quotes
, cmds
[i
].qualifier
, casesens
, regex
);
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
);
340 returnstr
= g_strdup(search_string
);
341 g_string_free(matcherstr
, TRUE
);
346 static void prepare_matcher_extended(AdvancedSearch
*search
)
348 gchar
*newstr
= advsearch_expand_search_string(search
->request
.matchstring
);
350 if (newstr
&& newstr
[0] != '\0') {
351 search
->predicate
= matcher_parser_get_cond(newstr
, &search
->is_fast
);
356 #define debug_matcher_list(prefix, list) \
358 gchar *str = list ? matcherlist_to_string(list) : g_strdup("(NULL)"); \
360 debug_print("%s: %s\n", prefix, str); \
365 static void prepare_matcher_tag(AdvancedSearch
*search
)
367 gchar
**words
= search
->request
.matchstring
368 ? g_strsplit(search
->request
.matchstring
, " ", -1)
372 if (search
->predicate
== NULL
) {
373 search
->predicate
= g_new0(MatcherList
, 1);
374 search
->predicate
->bool_and
= FALSE
;
375 search
->is_fast
= TRUE
;
378 while (words
&& words
[i
] && *words
[i
]) {
379 MatcherProp
*matcher
;
381 g_strstrip(words
[i
]);
383 matcher
= matcherprop_new(MATCHCRITERIA_TAG
, NULL
,
384 MATCHTYPE_MATCHCASE
, words
[i
], 0);
386 search
->predicate
->matchers
= g_slist_prepend(search
->predicate
->matchers
, matcher
);
393 static void prepare_matcher_header(AdvancedSearch
*search
, gint match_header
)
395 MatcherProp
*matcher
;
397 if (search
->predicate
== NULL
) {
398 search
->predicate
= g_new0(MatcherList
, 1);
399 search
->predicate
->bool_and
= FALSE
;
400 search
->is_fast
= TRUE
;
403 matcher
= matcherprop_new(match_header
, NULL
, MATCHTYPE_MATCHCASE
,
404 search
->request
.matchstring
, 0);
406 search
->predicate
->matchers
= g_slist_prepend(search
->predicate
->matchers
, matcher
);
409 static void prepare_matcher_mixed(AdvancedSearch
*search
)
411 prepare_matcher_tag(search
);
412 debug_matcher_list("tag matcher list", search
->predicate
);
414 /* we want an OR search */
415 if (search
->predicate
)
416 search
->predicate
->bool_and
= FALSE
;
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
);
428 static void prepare_matcher(AdvancedSearch
*search
)
430 const gchar
*search_string
;
432 cm_return_if_fail(search
!= NULL
);
434 if (search
->predicate
) {
435 matcherlist_free(search
->predicate
);
436 search
->predicate
= NULL
;
439 search_string
= search
->request
.matchstring
;
441 if (search_string
== NULL
|| search_string
[0] == '\0')
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
);
450 case ADVANCED_SEARCH_FROM
:
451 prepare_matcher_header(search
, MATCHCRITERIA_FROM
);
452 debug_matcher_list("from search", search
->predicate
);
455 case ADVANCED_SEARCH_TO
:
456 prepare_matcher_header(search
, MATCHCRITERIA_TO
);
457 debug_matcher_list("to search", search
->predicate
);
460 case ADVANCED_SEARCH_TAG
:
461 prepare_matcher_tag(search
);
462 debug_matcher_list("tag search", search
->predicate
);
465 case ADVANCED_SEARCH_MIXED
:
466 prepare_matcher_mixed(search
);
467 debug_matcher_list("mixed search", search
->predicate
);
470 case ADVANCED_SEARCH_EXTENDED
:
471 prepare_matcher_extended(search
);
472 debug_matcher_list("extended search", search
->predicate
);
476 debug_print("unknown search type (%d)\n", search
->request
.type
);
481 static gboolean
search_progress_notify_cb(gpointer data
, gboolean on_server
, guint at
,
482 guint matched
, guint total
)
484 AdvancedSearch
*search
= (AdvancedSearch
*) data
;
486 if (search
->search_aborted
)
489 if (on_server
|| search
->on_progress_cb
.cb
== NULL
)
492 return search
->on_progress_cb
.cb(search
->on_progress_cb
.data
, at
, matched
, total
);
495 static gboolean
search_filter_folder(MsgNumberList
**msgnums
, AdvancedSearch
*search
,
496 FolderItem
*folderItem
, gboolean onServer
)
499 gboolean tried_server
= onServer
;
501 matched
= folder_item_search_msgs(folderItem
->folder
,
506 search_progress_notify_cb
,
510 if (search
->on_error_cb
.cb
!= NULL
)
511 search
->on_error_cb
.cb(search
->on_error_cb
.data
);
515 if (folderItem
->folder
->klass
->supports_server_search
&& tried_server
&& !onServer
) {
516 return search_filter_folder(msgnums
, search
, folderItem
, onServer
);
522 static gboolean
search_impl(MsgInfoList
**messages
, AdvancedSearch
* search
,
523 FolderItem
* folderItem
, gboolean recursive
)
526 START_TIMING("recursive");
527 if (!search_impl(messages
, search
, folderItem
, FALSE
)) {
531 if (folderItem
->node
->children
!= NULL
&& !search
->search_aborted
) {
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
)) {
543 } else if (!folderItem
->no_select
) {
544 MsgNumberList
*msgnums
= NULL
;
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
);
556 for (cur
= msgnums
; cur
!= NULL
; cur
= cur
->next
) {
557 MsgInfo
*msg
= folder_item_get_msginfo(folderItem
, GPOINTER_TO_UINT(cur
->data
));
559 msgs
= g_slist_prepend(msgs
, msg
);
562 while (msgs
!= NULL
) {
563 MsgInfoList
*front
= msgs
;
567 front
->next
= *messages
;
571 g_slist_free(msgnums
);