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"
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
;
132 gchar
*advsearch_expand_search_string(const gchar
*search_string
)
135 gchar term_char
, save_char
;
136 gchar
*cmd_start
, *cmd_end
;
138 gchar
*returnstr
= NULL
;
140 gboolean casesens
, dontmatch
, regex
;
141 /* list of allowed pattern abbreviations */
143 gchar
*abbreviated
; /* abbreviation */
144 gchar
*command
; /* actual matcher command */
145 gint numparams
; /* number of params for cmd */
146 gboolean qualifier
; /* do we append stringmatch operations */
147 gboolean quotes
; /* do we need quotes */
150 { "a", "all", 0, FALSE
, FALSE
},
151 { "ag", "age_greater", 1, FALSE
, FALSE
},
152 { "al", "age_lower", 1, FALSE
, FALSE
},
153 { "agh","age_greater_hours", 1, FALSE
, FALSE
},
154 { "alh","age_lower_hours", 1, FALSE
, FALSE
},
155 { "b", "body_part", 1, TRUE
, TRUE
},
156 { "B", "message", 1, TRUE
, TRUE
},
157 { "c", "cc", 1, TRUE
, TRUE
},
158 { "C", "to_or_cc", 1, TRUE
, TRUE
},
159 { "D", "deleted", 0, FALSE
, FALSE
},
160 { "e", "header \"Sender\"", 1, TRUE
, TRUE
},
161 { "E", "execute", 1, FALSE
, TRUE
},
162 { "f", "from", 1, TRUE
, TRUE
},
163 { "F", "forwarded", 0, FALSE
, FALSE
},
164 { "h", "headers_part", 1, TRUE
, TRUE
},
165 { "H", "headers_cont", 1, TRUE
, TRUE
},
166 { "ha", "has_attachments", 0, FALSE
, FALSE
},
167 { "i", "messageid", 1, TRUE
, TRUE
},
168 { "I", "inreplyto", 1, TRUE
, TRUE
},
169 { "k", "colorlabel", 1, FALSE
, FALSE
},
170 { "L", "locked", 0, FALSE
, FALSE
},
171 { "n", "newsgroups", 1, TRUE
, TRUE
},
172 { "N", "new", 0, FALSE
, FALSE
},
173 { "O", "~new", 0, FALSE
, FALSE
},
174 { "r", "replied", 0, FALSE
, FALSE
},
175 { "R", "~unread", 0, FALSE
, FALSE
},
176 { "s", "subject", 1, TRUE
, TRUE
},
177 { "se", "score_equal", 1, FALSE
, FALSE
},
178 { "sg", "score_greater", 1, FALSE
, FALSE
},
179 { "sl", "score_lower", 1, FALSE
, FALSE
},
180 { "Se", "size_equal", 1, FALSE
, FALSE
},
181 { "Sg", "size_greater", 1, FALSE
, FALSE
},
182 { "Ss", "size_smaller", 1, FALSE
, FALSE
},
183 { "t", "to", 1, TRUE
, TRUE
},
184 { "tg", "tag", 1, TRUE
, TRUE
},
185 { "T", "marked", 0, FALSE
, FALSE
},
186 { "U", "unread", 0, FALSE
, FALSE
},
187 { "x", "header \"References\"", 1, TRUE
, TRUE
},
188 { "X", "test", 1, FALSE
, FALSE
},
189 { "y", "header \"X-Label\"", 1, TRUE
, TRUE
},
190 { "&", "&", 0, FALSE
, FALSE
},
191 { "|", "|", 0, FALSE
, FALSE
},
192 { "p", "partial", 0, FALSE
, FALSE
},
193 { NULL
, NULL
, 0, FALSE
, FALSE
}
196 if (search_string
== NULL
)
199 copy_str
= g_strdup(search_string
);
201 matcherstr
= g_string_sized_new(16);
202 cmd_start
= copy_str
;
203 while (cmd_start
&& *cmd_start
) {
204 /* skip all white spaces */
205 while (*cmd_start
&& isspace((guchar
)*cmd_start
))
209 /* extract a command */
210 while (*cmd_end
&& !isspace((guchar
)*cmd_end
))
214 save_char
= *cmd_end
;
221 /* ~ and ! mean logical NOT */
222 if (*cmd_start
== '~' || *cmd_start
== '!')
227 /* % means case sensitive match */
228 if (*cmd_start
== '%')
233 /* # means regex match */
234 if (*cmd_start
== '#') {
239 /* find matching abbreviation */
240 for (i
= 0; cmds
[i
].command
; i
++) {
241 if (!strcmp(cmd_start
, cmds
[i
].abbreviated
)) {
242 /* restore character */
243 *cmd_end
= save_char
;
246 if (matcherstr
->len
> 0) {
247 g_string_append(matcherstr
, " ");
250 g_string_append(matcherstr
, "~");
251 g_string_append(matcherstr
, cmds
[i
].command
);
252 g_string_append(matcherstr
, " ");
254 /* stop if no params required */
255 if (cmds
[i
].numparams
== 0)
258 /* extract a parameter, allow quotes */
259 while (*cmd_end
&& isspace((guchar
)*cmd_end
))
263 if (*cmd_start
== '"') {
270 /* extract actual parameter */
271 while ((*cmd_end
) && (*cmd_end
!= term_char
))
277 save_char
= *cmd_end
;
280 if (cmds
[i
].qualifier
) {
282 g_string_append(matcherstr
, regex
? "regexp " : "match ");
284 g_string_append(matcherstr
, regex
? "regexpcase " : "matchcase ");
287 /* do we need to add quotes ? */
288 if (cmds
[i
].quotes
&& term_char
!= '"')
289 g_string_append(matcherstr
, "\"");
291 /* copy actual parameter */
292 g_string_append(matcherstr
, cmd_start
);
294 /* do we need to add quotes ? */
295 if (cmds
[i
].quotes
&& term_char
!= '"')
296 g_string_append(matcherstr
, "\"");
298 /* restore original character */
299 *cmd_end
= save_char
;
312 /* return search string if no match is found to allow
313 all available filtering expressions in advanced search */
314 if (matcherstr
->len
> 0) returnstr
= matcherstr
->str
;
315 else returnstr
= g_strdup(search_string
);
316 g_string_free(matcherstr
, FALSE
);
320 static void prepare_matcher_extended(AdvancedSearch
*search
)
322 gchar
*newstr
= advsearch_expand_search_string(search
->request
.matchstring
);
324 if (newstr
&& newstr
[0] != '\0') {
325 search
->predicate
= matcher_parser_get_cond(newstr
, &search
->is_fast
);
330 #define debug_matcher_list(prefix, list) \
332 gchar *str = list ? matcherlist_to_string(list) : g_strdup("(NULL)"); \
334 debug_print("%s: %s\n", prefix, str); \
339 static void prepare_matcher_tag(AdvancedSearch
*search
)
341 gchar
**words
= search
->request
.matchstring
342 ? g_strsplit(search
->request
.matchstring
, " ", -1)
346 if (search
->predicate
== NULL
) {
347 search
->predicate
= g_new0(MatcherList
, 1);
348 search
->predicate
->bool_and
= FALSE
;
349 search
->is_fast
= TRUE
;
352 while (words
&& words
[i
] && *words
[i
]) {
353 MatcherProp
*matcher
;
355 g_strstrip(words
[i
]);
357 matcher
= matcherprop_new(MATCHCRITERIA_TAG
, NULL
,
358 MATCHTYPE_MATCHCASE
, words
[i
], 0);
360 search
->predicate
->matchers
= g_slist_prepend(search
->predicate
->matchers
, matcher
);
367 static void prepare_matcher_header(AdvancedSearch
*search
, gint match_header
)
369 MatcherProp
*matcher
;
371 if (search
->predicate
== NULL
) {
372 search
->predicate
= g_new0(MatcherList
, 1);
373 search
->predicate
->bool_and
= FALSE
;
374 search
->is_fast
= TRUE
;
377 matcher
= matcherprop_new(match_header
, NULL
, MATCHTYPE_MATCHCASE
,
378 search
->request
.matchstring
, 0);
380 search
->predicate
->matchers
= g_slist_prepend(search
->predicate
->matchers
, matcher
);
383 static void prepare_matcher_mixed(AdvancedSearch
*search
)
385 prepare_matcher_tag(search
);
386 debug_matcher_list("tag matcher list", search
->predicate
);
388 /* we want an OR search */
389 if (search
->predicate
)
390 search
->predicate
->bool_and
= FALSE
;
392 prepare_matcher_header(search
, MATCHCRITERIA_SUBJECT
);
393 debug_matcher_list("tag + subject matcher list", search
->predicate
);
394 prepare_matcher_header(search
, MATCHCRITERIA_FROM
);
395 debug_matcher_list("tag + subject + from matcher list", search
->predicate
);
396 prepare_matcher_header(search
, MATCHCRITERIA_TO
);
397 debug_matcher_list("tag + subject + from + to matcher list", search
->predicate
);
398 prepare_matcher_header(search
, MATCHCRITERIA_CC
);
399 debug_matcher_list("tag + subject + from + to + cc matcher list", search
->predicate
);
402 static void prepare_matcher(AdvancedSearch
*search
)
404 const gchar
*search_string
;
406 cm_return_if_fail(search
!= NULL
);
408 if (search
->predicate
) {
409 matcherlist_free(search
->predicate
);
410 search
->predicate
= NULL
;
413 search_string
= search
->request
.matchstring
;
415 if (search_string
== NULL
|| search_string
[0] == '\0')
418 switch (search
->request
.type
) {
419 case ADVANCED_SEARCH_SUBJECT
:
420 prepare_matcher_header(search
, MATCHCRITERIA_SUBJECT
);
421 debug_matcher_list("subject search", search
->predicate
);
424 case ADVANCED_SEARCH_FROM
:
425 prepare_matcher_header(search
, MATCHCRITERIA_FROM
);
426 debug_matcher_list("from search", search
->predicate
);
429 case ADVANCED_SEARCH_TO
:
430 prepare_matcher_header(search
, MATCHCRITERIA_TO
);
431 debug_matcher_list("to search", search
->predicate
);
434 case ADVANCED_SEARCH_TAG
:
435 prepare_matcher_tag(search
);
436 debug_matcher_list("tag search", search
->predicate
);
439 case ADVANCED_SEARCH_MIXED
:
440 prepare_matcher_mixed(search
);
441 debug_matcher_list("mixed search", search
->predicate
);
444 case ADVANCED_SEARCH_EXTENDED
:
445 prepare_matcher_extended(search
);
446 debug_matcher_list("extended search", search
->predicate
);
450 debug_print("unknown search type (%d)\n", search
->request
.type
);
455 static gboolean
search_progress_notify_cb(gpointer data
, gboolean on_server
, guint at
,
456 guint matched
, guint total
)
458 AdvancedSearch
*search
= (AdvancedSearch
*) data
;
460 if (search
->search_aborted
)
463 if (on_server
|| search
->on_progress_cb
.cb
== NULL
)
466 return search
->on_progress_cb
.cb(search
->on_progress_cb
.data
, at
, matched
, total
);
469 static gboolean
search_filter_folder(MsgNumberList
**msgnums
, AdvancedSearch
*search
,
470 FolderItem
*folderItem
, gboolean onServer
)
473 gboolean tried_server
= onServer
;
475 matched
= folder_item_search_msgs(folderItem
->folder
,
480 search_progress_notify_cb
,
484 if (search
->on_error_cb
.cb
!= NULL
)
485 search
->on_error_cb
.cb(search
->on_error_cb
.data
);
489 if (folderItem
->folder
->klass
->supports_server_search
&& tried_server
&& !onServer
) {
490 return search_filter_folder(msgnums
, search
, folderItem
, onServer
);
496 static gboolean
search_impl(MsgInfoList
**messages
, AdvancedSearch
* search
,
497 FolderItem
* folderItem
, gboolean recursive
)
500 if (!search_impl(messages
, search
, folderItem
, FALSE
))
503 if (folderItem
->node
->children
!= NULL
&& !search
->search_aborted
) {
505 for (node
= folderItem
->node
->children
; node
!= NULL
; node
= node
->next
) {
506 FolderItem
*cur
= FOLDER_ITEM(node
->data
);
507 debug_print("in: %s\n", cur
->path
);
508 if (!search_impl(messages
, search
, cur
, TRUE
))
512 } else if (!folderItem
->no_select
) {
513 MsgNumberList
*msgnums
= NULL
;
515 MsgInfoList
*msgs
= NULL
;
516 gboolean can_search_on_server
= folderItem
->folder
->klass
->supports_server_search
;
518 if (!search_filter_folder(&msgnums
, search
, folderItem
,
519 can_search_on_server
)) {
520 g_slist_free(msgnums
);
524 for (cur
= msgnums
; cur
!= NULL
; cur
= cur
->next
) {
525 MsgInfo
*msg
= folder_item_get_msginfo(folderItem
, GPOINTER_TO_UINT(cur
->data
));
527 msgs
= g_slist_prepend(msgs
, msg
);
530 while (msgs
!= NULL
) {
531 MsgInfoList
*front
= msgs
;
535 front
->next
= *messages
;
539 g_slist_free(msgnums
);