2 * Claws Mail -- a GTK based, lightweight, and fast e-mail client
3 * Copyright (C) 2012-2014 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/>.
22 # include "claws-features.h"
25 #include "advsearch.h"
31 #include "matcher_parser.h"
33 #include "prefs_common.h"
36 struct _AdvancedSearch
{
38 AdvancedSearchType type
;
42 MatcherList
*predicate
;
44 gboolean search_aborted
;
47 gboolean (*cb
)(gpointer data
, guint at
, guint matched
, guint total
);
51 void (*cb
)(gpointer data
);
56 void advsearch_set_on_progress_cb(AdvancedSearch
*search
, gboolean (*cb
)(gpointer
, guint
, guint
, guint
), gpointer data
)
58 search
->on_progress_cb
.cb
= cb
;
59 search
->on_progress_cb
.data
= data
;
62 void advsearch_set_on_error_cb(AdvancedSearch
* search
, void (*cb
)(gpointer data
), gpointer data
)
64 search
->on_error_cb
.cb
= cb
;
65 search
->on_error_cb
.data
= data
;
68 static void prepare_matcher(AdvancedSearch
*search
);
69 static gboolean
search_impl(MsgInfoList
**messages
, AdvancedSearch
* search
,
70 FolderItem
* folderItem
, gboolean recursive
);
72 // --------------------------
74 AdvancedSearch
* advsearch_new()
76 AdvancedSearch
*result
;
78 result
= g_new0(AdvancedSearch
, 1);
83 void advsearch_free(AdvancedSearch
*search
)
85 if (search
->predicate
!= NULL
)
86 matcherlist_free(search
->predicate
);
88 g_free(search
->request
.matchstring
);
92 void advsearch_set(AdvancedSearch
*search
, AdvancedSearchType type
, const gchar
*matchstring
)
94 cm_return_if_fail(search
!= NULL
);
96 search
->request
.type
= type
;
98 g_free(search
->request
.matchstring
);
99 search
->request
.matchstring
= g_strdup(matchstring
);
101 prepare_matcher(search
);
104 gboolean
advsearch_is_fast(AdvancedSearch
*search
)
106 cm_return_val_if_fail(search
!= NULL
, FALSE
);
108 return search
->is_fast
;
111 gboolean
advsearch_has_proper_predicate(AdvancedSearch
*search
)
113 cm_return_val_if_fail(search
!= NULL
, FALSE
);
115 return search
->predicate
!= NULL
;
118 gboolean
advsearch_search_msgs_in_folders(AdvancedSearch
* search
, MsgInfoList
**messages
,
119 FolderItem
* folderItem
, gboolean recursive
)
121 if (search
== NULL
|| search
->predicate
== NULL
)
124 search
->search_aborted
= FALSE
;
125 return search_impl(messages
, search
, folderItem
, recursive
);
128 void advsearch_abort(AdvancedSearch
*search
)
130 search
->search_aborted
= TRUE
;
133 gchar
*advsearch_expand_search_string(const gchar
*search_string
)
136 gchar term_char
, save_char
;
137 gchar
*cmd_start
, *cmd_end
;
139 gchar
*returnstr
= NULL
;
141 gboolean casesens
, dontmatch
, regex
;
142 /* list of allowed pattern abbreviations */
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 */
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
}
199 if (search_string
== NULL
)
202 copy_str
= g_strdup(search_string
);
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
))
212 /* extract a command */
213 while (*cmd_end
&& !isspace((guchar
)*cmd_end
))
217 save_char
= *cmd_end
;
224 /* ~ and ! mean logical NOT */
225 if (*cmd_start
== '~' || *cmd_start
== '!')
230 /* % means case sensitive match */
231 if (*cmd_start
== '%')
236 /* # means regex match */
237 if (*cmd_start
== '#') {
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
;
249 if (matcherstr
->len
> 0) {
250 g_string_append(matcherstr
, " ");
253 g_string_append(matcherstr
, "~");
254 g_string_append(matcherstr
, cmds
[i
].command
);
255 g_string_append(matcherstr
, " ");
257 /* stop if no params required */
258 if (cmds
[i
].numparams
== 0)
261 /* extract a parameter, allow quotes */
262 while (*cmd_end
&& isspace((guchar
)*cmd_end
))
266 if (*cmd_start
== '"') {
273 /* extract actual parameter */
274 while ((*cmd_end
) && (*cmd_end
!= term_char
))
280 save_char
= *cmd_end
;
283 if (cmds
[i
].qualifier
) {
285 g_string_append(matcherstr
, regex
? "regexp " : "match ");
287 g_string_append(matcherstr
, regex
? "regexpcase " : "matchcase ");
290 /* do we need to add quotes ? */
291 if (cmds
[i
].quotes
&& term_char
!= '"')
292 g_string_append(matcherstr
, "\"");
294 /* copy actual parameter */
295 g_string_append(matcherstr
, cmd_start
);
297 /* do we need to add quotes ? */
298 if (cmds
[i
].quotes
&& term_char
!= '"')
299 g_string_append(matcherstr
, "\"");
301 /* restore original character */
302 *cmd_end
= save_char
;
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
);
320 returnstr
= g_strdup(search_string
);
321 g_string_free(matcherstr
, TRUE
);
326 static void prepare_matcher_extended(AdvancedSearch
*search
)
328 gchar
*newstr
= advsearch_expand_search_string(search
->request
.matchstring
);
330 if (newstr
&& newstr
[0] != '\0') {
331 search
->predicate
= matcher_parser_get_cond(newstr
, &search
->is_fast
);
336 #define debug_matcher_list(prefix, list) \
338 gchar *str = list ? matcherlist_to_string(list) : g_strdup("(NULL)"); \
340 debug_print("%s: %s\n", prefix, str); \
345 static void prepare_matcher_tag(AdvancedSearch
*search
)
347 gchar
**words
= search
->request
.matchstring
348 ? g_strsplit(search
->request
.matchstring
, " ", -1)
352 if (search
->predicate
== NULL
) {
353 search
->predicate
= g_new0(MatcherList
, 1);
354 search
->predicate
->bool_and
= FALSE
;
355 search
->is_fast
= TRUE
;
358 while (words
&& words
[i
] && *words
[i
]) {
359 MatcherProp
*matcher
;
361 g_strstrip(words
[i
]);
363 matcher
= matcherprop_new(MATCHCRITERIA_TAG
, NULL
,
364 MATCHTYPE_MATCHCASE
, words
[i
], 0);
366 search
->predicate
->matchers
= g_slist_prepend(search
->predicate
->matchers
, matcher
);
373 static void prepare_matcher_header(AdvancedSearch
*search
, gint match_header
)
375 MatcherProp
*matcher
;
377 if (search
->predicate
== NULL
) {
378 search
->predicate
= g_new0(MatcherList
, 1);
379 search
->predicate
->bool_and
= FALSE
;
380 search
->is_fast
= TRUE
;
383 matcher
= matcherprop_new(match_header
, NULL
, MATCHTYPE_MATCHCASE
,
384 search
->request
.matchstring
, 0);
386 search
->predicate
->matchers
= g_slist_prepend(search
->predicate
->matchers
, matcher
);
389 static void prepare_matcher_mixed(AdvancedSearch
*search
)
391 prepare_matcher_tag(search
);
392 debug_matcher_list("tag matcher list", search
->predicate
);
394 /* we want an OR search */
395 if (search
->predicate
)
396 search
->predicate
->bool_and
= FALSE
;
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
);
408 static void prepare_matcher(AdvancedSearch
*search
)
410 const gchar
*search_string
;
412 cm_return_if_fail(search
!= NULL
);
414 if (search
->predicate
) {
415 matcherlist_free(search
->predicate
);
416 search
->predicate
= NULL
;
419 search_string
= search
->request
.matchstring
;
421 if (search_string
== NULL
|| search_string
[0] == '\0')
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
);
430 case ADVANCED_SEARCH_FROM
:
431 prepare_matcher_header(search
, MATCHCRITERIA_FROM
);
432 debug_matcher_list("from search", search
->predicate
);
435 case ADVANCED_SEARCH_TO
:
436 prepare_matcher_header(search
, MATCHCRITERIA_TO
);
437 debug_matcher_list("to search", search
->predicate
);
440 case ADVANCED_SEARCH_TAG
:
441 prepare_matcher_tag(search
);
442 debug_matcher_list("tag search", search
->predicate
);
445 case ADVANCED_SEARCH_MIXED
:
446 prepare_matcher_mixed(search
);
447 debug_matcher_list("mixed search", search
->predicate
);
450 case ADVANCED_SEARCH_EXTENDED
:
451 prepare_matcher_extended(search
);
452 debug_matcher_list("extended search", search
->predicate
);
456 debug_print("unknown search type (%d)\n", search
->request
.type
);
461 static gboolean
search_progress_notify_cb(gpointer data
, gboolean on_server
, guint at
,
462 guint matched
, guint total
)
464 AdvancedSearch
*search
= (AdvancedSearch
*) data
;
466 if (search
->search_aborted
)
469 if (on_server
|| search
->on_progress_cb
.cb
== NULL
)
472 return search
->on_progress_cb
.cb(search
->on_progress_cb
.data
, at
, matched
, total
);
475 static gboolean
search_filter_folder(MsgNumberList
**msgnums
, AdvancedSearch
*search
,
476 FolderItem
*folderItem
, gboolean onServer
)
479 gboolean tried_server
= onServer
;
481 matched
= folder_item_search_msgs(folderItem
->folder
,
486 search_progress_notify_cb
,
490 if (search
->on_error_cb
.cb
!= NULL
)
491 search
->on_error_cb
.cb(search
->on_error_cb
.data
);
495 if (folderItem
->folder
->klass
->supports_server_search
&& tried_server
&& !onServer
) {
496 return search_filter_folder(msgnums
, search
, folderItem
, onServer
);
502 static gboolean
search_impl(MsgInfoList
**messages
, AdvancedSearch
* search
,
503 FolderItem
* folderItem
, gboolean recursive
)
506 START_TIMING("recursive");
507 if (!search_impl(messages
, search
, folderItem
, FALSE
)) {
511 if (folderItem
->node
->children
!= NULL
&& !search
->search_aborted
) {
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
)) {
523 } else if (!folderItem
->no_select
) {
524 MsgNumberList
*msgnums
= NULL
;
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
);
536 for (cur
= msgnums
; cur
!= NULL
; cur
= cur
->next
) {
537 MsgInfo
*msg
= folder_item_get_msginfo(folderItem
, GPOINTER_TO_UINT(cur
->data
));
539 msgs
= g_slist_prepend(msgs
, msg
);
542 while (msgs
!= NULL
) {
543 MsgInfoList
*front
= msgs
;
547 front
->next
= *messages
;
551 g_slist_free(msgnums
);