/* * Claws Mail -- a GTK+ based, lightweight, and fast e-mail client * Copyright (C) 2012-2014 the Claws Mail team * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * */ #ifdef HAVE_CONFIG_H # include "config.h" # include "claws-features.h" #endif #include "advsearch.h" #include #include #include "matcher.h" #include "matcher_parser.h" #include "utils.h" #include "prefs_common.h" #include "timing.h" struct _AdvancedSearch { struct { AdvancedSearchType type; gchar *matchstring; } request; MatcherList *predicate; gboolean is_fast; gboolean search_aborted; struct { gboolean (*cb)(gpointer data, guint at, guint matched, guint total); gpointer data; } on_progress_cb; struct { void (*cb)(gpointer data); gpointer data; } on_error_cb; }; void advsearch_set_on_progress_cb(AdvancedSearch *search, gboolean (*cb)(gpointer, guint, guint, guint), gpointer data) { search->on_progress_cb.cb = cb; search->on_progress_cb.data = data; } void advsearch_set_on_error_cb(AdvancedSearch* search, void (*cb)(gpointer data), gpointer data) { search->on_error_cb.cb = cb; search->on_error_cb.data = data; } static void prepare_matcher(AdvancedSearch *search); static gboolean search_impl(MsgInfoList **messages, AdvancedSearch* search, FolderItem* folderItem, gboolean recursive); // -------------------------- AdvancedSearch* advsearch_new() { AdvancedSearch *result; result = g_new0(AdvancedSearch, 1); return result; } void advsearch_free(AdvancedSearch *search) { if (search->predicate != NULL) matcherlist_free(search->predicate); g_free(search->request.matchstring); g_free(search); } void advsearch_set(AdvancedSearch *search, AdvancedSearchType type, const gchar *matchstring) { cm_return_if_fail(search != NULL); search->request.type = type; g_free(search->request.matchstring); search->request.matchstring = g_strdup(matchstring); prepare_matcher(search); } gboolean advsearch_is_fast(AdvancedSearch *search) { cm_return_val_if_fail(search != NULL, FALSE); return search->is_fast; } gboolean advsearch_has_proper_predicate(AdvancedSearch *search) { cm_return_val_if_fail(search != NULL, FALSE); return search->predicate != NULL; } gboolean advsearch_search_msgs_in_folders(AdvancedSearch* search, MsgInfoList **messages, FolderItem* folderItem, gboolean recursive) { if (search == NULL || search->predicate == NULL) return FALSE; search->search_aborted = FALSE; return search_impl(messages, search, folderItem, recursive); } void advsearch_abort(AdvancedSearch *search) { search->search_aborted = TRUE; } gchar *advsearch_expand_search_string(const gchar *search_string) { int i = 0; gchar term_char, save_char; gchar *cmd_start, *cmd_end; GString *matcherstr; gchar *returnstr = NULL; gchar *copy_str; gboolean casesens, dontmatch, regex; /* list of allowed pattern abbreviations */ struct { gchar *abbreviated; /* abbreviation */ gchar *command; /* actual matcher command */ gint numparams; /* number of params for cmd */ gboolean qualifier; /* do we append stringmatch operations */ gboolean quotes; /* do we need quotes */ } cmds[] = { { "a", "all", 0, FALSE, FALSE }, { "ag", "age_greater", 1, FALSE, FALSE }, { "al", "age_lower", 1, FALSE, FALSE }, { "agh","age_greater_hours", 1, FALSE, FALSE }, { "alh","age_lower_hours", 1, FALSE, FALSE }, { "b", "body_part", 1, TRUE, TRUE }, { "B", "message", 1, TRUE, TRUE }, { "c", "cc", 1, TRUE, TRUE }, { "C", "to_or_cc", 1, TRUE, TRUE }, { "D", "deleted", 0, FALSE, FALSE }, { "da", "date_after", 1, FALSE, TRUE }, { "db", "date_before", 1, FALSE, TRUE }, { "e", "header \"Sender\"", 1, TRUE, TRUE }, { "E", "execute", 1, FALSE, TRUE }, { "f", "from", 1, TRUE, TRUE }, { "F", "forwarded", 0, FALSE, FALSE }, { "h", "headers_part", 1, TRUE, TRUE }, { "H", "headers_cont", 1, TRUE, TRUE }, { "ha", "has_attachments", 0, FALSE, FALSE }, { "i", "messageid", 1, TRUE, TRUE }, { "I", "inreplyto", 1, TRUE, TRUE }, { "k", "colorlabel", 1, FALSE, FALSE }, { "L", "locked", 0, FALSE, FALSE }, { "n", "newsgroups", 1, TRUE, TRUE }, { "N", "new", 0, FALSE, FALSE }, { "O", "~new", 0, FALSE, FALSE }, { "r", "replied", 0, FALSE, FALSE }, { "R", "~unread", 0, FALSE, FALSE }, { "s", "subject", 1, TRUE, TRUE }, { "se", "score_equal", 1, FALSE, FALSE }, { "sg", "score_greater", 1, FALSE, FALSE }, { "sl", "score_lower", 1, FALSE, FALSE }, { "Se", "size_equal", 1, FALSE, FALSE }, { "Sg", "size_greater", 1, FALSE, FALSE }, { "Ss", "size_smaller", 1, FALSE, FALSE }, { "t", "to", 1, TRUE, TRUE }, { "tg", "tag", 1, TRUE, TRUE }, { "T", "marked", 0, FALSE, FALSE }, { "U", "unread", 0, FALSE, FALSE }, { "x", "references", 1, TRUE, TRUE }, { "X", "test", 1, FALSE, FALSE }, { "y", "header \"X-Label\"", 1, TRUE, TRUE }, { "&", "&", 0, FALSE, FALSE }, { "|", "|", 0, FALSE, FALSE }, { "p", "partial", 0, FALSE, FALSE }, { NULL, NULL, 0, FALSE, FALSE } }; if (search_string == NULL) return NULL; copy_str = g_strdup(search_string); matcherstr = g_string_sized_new(16); cmd_start = copy_str; while (cmd_start && *cmd_start) { /* skip all white spaces */ while (*cmd_start && isspace((guchar)*cmd_start)) cmd_start++; cmd_end = cmd_start; /* extract a command */ while (*cmd_end && !isspace((guchar)*cmd_end)) cmd_end++; /* save character */ save_char = *cmd_end; *cmd_end = '\0'; dontmatch = FALSE; casesens = FALSE; regex = FALSE; /* ~ and ! mean logical NOT */ if (*cmd_start == '~' || *cmd_start == '!') { dontmatch = TRUE; cmd_start++; } /* % means case sensitive match */ if (*cmd_start == '%') { casesens = TRUE; cmd_start++; } /* # means regex match */ if (*cmd_start == '#') { regex = TRUE; cmd_start++; } /* find matching abbreviation */ for (i = 0; cmds[i].command; i++) { if (!strcmp(cmd_start, cmds[i].abbreviated)) { /* restore character */ *cmd_end = save_char; /* copy command */ if (matcherstr->len > 0) { g_string_append(matcherstr, " "); } if (dontmatch) g_string_append(matcherstr, "~"); g_string_append(matcherstr, cmds[i].command); g_string_append(matcherstr, " "); /* stop if no params required */ if (cmds[i].numparams == 0) break; /* extract a parameter, allow quotes */ while (*cmd_end && isspace((guchar)*cmd_end)) cmd_end++; cmd_start = cmd_end; if (*cmd_start == '"') { term_char = '"'; cmd_end++; } else term_char = ' '; /* extract actual parameter */ while ((*cmd_end) && (*cmd_end != term_char)) cmd_end++; if (*cmd_end == '"') cmd_end++; save_char = *cmd_end; *cmd_end = '\0'; if (cmds[i].qualifier) { if (casesens) g_string_append(matcherstr, regex ? "regexp " : "match "); else g_string_append(matcherstr, regex ? "regexpcase " : "matchcase "); } /* do we need to add quotes ? */ if (cmds[i].quotes && term_char != '"') g_string_append(matcherstr, "\""); /* copy actual parameter */ g_string_append(matcherstr, cmd_start); /* do we need to add quotes ? */ if (cmds[i].quotes && term_char != '"') g_string_append(matcherstr, "\""); /* restore original character */ *cmd_end = save_char; break; } } if (*cmd_end) cmd_end++; cmd_start = cmd_end; } g_free(copy_str); /* return search string if no match is found to allow all available filtering expressions in advanced search */ if (matcherstr->len > 0) { returnstr = g_string_free(matcherstr, FALSE); } else { returnstr = g_strdup(search_string); g_string_free(matcherstr, TRUE); } return returnstr; } static void prepare_matcher_extended(AdvancedSearch *search) { gchar *newstr = advsearch_expand_search_string(search->request.matchstring); if (newstr && newstr[0] != '\0') { search->predicate = matcher_parser_get_cond(newstr, &search->is_fast); g_free(newstr); } } #define debug_matcher_list(prefix, list) \ do { \ gchar *str = list ? matcherlist_to_string(list) : g_strdup("(NULL)"); \ \ debug_print("%s: %s\n", prefix, str); \ \ g_free(str); \ } while(0) static void prepare_matcher_tag(AdvancedSearch *search) { gchar **words = search->request.matchstring ? g_strsplit(search->request.matchstring, " ", -1) : NULL; gint i = 0; if (search->predicate == NULL) { search->predicate = g_new0(MatcherList, 1); search->predicate->bool_and = FALSE; search->is_fast = TRUE; } while (words && words[i] && *words[i]) { MatcherProp *matcher; g_strstrip(words[i]); matcher = matcherprop_new(MATCHCRITERIA_TAG, NULL, MATCHTYPE_MATCHCASE, words[i], 0); search->predicate->matchers = g_slist_prepend(search->predicate->matchers, matcher); i++; } g_strfreev(words); } static void prepare_matcher_header(AdvancedSearch *search, gint match_header) { MatcherProp *matcher; if (search->predicate == NULL) { search->predicate = g_new0(MatcherList, 1); search->predicate->bool_and = FALSE; search->is_fast = TRUE; } matcher = matcherprop_new(match_header, NULL, MATCHTYPE_MATCHCASE, search->request.matchstring, 0); search->predicate->matchers = g_slist_prepend(search->predicate->matchers, matcher); } static void prepare_matcher_mixed(AdvancedSearch *search) { prepare_matcher_tag(search); debug_matcher_list("tag matcher list", search->predicate); /* we want an OR search */ if (search->predicate) search->predicate->bool_and = FALSE; prepare_matcher_header(search, MATCHCRITERIA_SUBJECT); debug_matcher_list("tag + subject matcher list", search->predicate); prepare_matcher_header(search, MATCHCRITERIA_FROM); debug_matcher_list("tag + subject + from matcher list", search->predicate); prepare_matcher_header(search, MATCHCRITERIA_TO); debug_matcher_list("tag + subject + from + to matcher list", search->predicate); prepare_matcher_header(search, MATCHCRITERIA_CC); debug_matcher_list("tag + subject + from + to + cc matcher list", search->predicate); } static void prepare_matcher(AdvancedSearch *search) { const gchar *search_string; cm_return_if_fail(search != NULL); if (search->predicate) { matcherlist_free(search->predicate); search->predicate = NULL; } search_string = search->request.matchstring; if (search_string == NULL || search_string[0] == '\0') return; switch (search->request.type) { case ADVANCED_SEARCH_SUBJECT: prepare_matcher_header(search, MATCHCRITERIA_SUBJECT); debug_matcher_list("subject search", search->predicate); break; case ADVANCED_SEARCH_FROM: prepare_matcher_header(search, MATCHCRITERIA_FROM); debug_matcher_list("from search", search->predicate); break; case ADVANCED_SEARCH_TO: prepare_matcher_header(search, MATCHCRITERIA_TO); debug_matcher_list("to search", search->predicate); break; case ADVANCED_SEARCH_TAG: prepare_matcher_tag(search); debug_matcher_list("tag search", search->predicate); break; case ADVANCED_SEARCH_MIXED: prepare_matcher_mixed(search); debug_matcher_list("mixed search", search->predicate); break; case ADVANCED_SEARCH_EXTENDED: prepare_matcher_extended(search); debug_matcher_list("extended search", search->predicate); break; default: debug_print("unknown search type (%d)\n", search->request.type); break; } } static gboolean search_progress_notify_cb(gpointer data, gboolean on_server, guint at, guint matched, guint total) { AdvancedSearch *search = (AdvancedSearch*) data; if (search->search_aborted) return FALSE; if (on_server || search->on_progress_cb.cb == NULL) return TRUE; return search->on_progress_cb.cb(search->on_progress_cb.data, at, matched, total); } static gboolean search_filter_folder(MsgNumberList **msgnums, AdvancedSearch *search, FolderItem *folderItem, gboolean onServer) { gint matched; gboolean tried_server = onServer; matched = folder_item_search_msgs(folderItem->folder, folderItem, msgnums, &onServer, search->predicate, search_progress_notify_cb, search); if (matched < 0) { if (search->on_error_cb.cb != NULL) search->on_error_cb.cb(search->on_error_cb.data); return FALSE; } if (folderItem->folder->klass->supports_server_search && tried_server && !onServer) { return search_filter_folder(msgnums, search, folderItem, onServer); } else { return TRUE; } } static gboolean search_impl(MsgInfoList **messages, AdvancedSearch* search, FolderItem* folderItem, gboolean recursive) { if (recursive) { START_TIMING("recursive"); if (!search_impl(messages, search, folderItem, FALSE)) { END_TIMING(); return FALSE; } if (folderItem->node->children != NULL && !search->search_aborted) { GNode *node; for (node = folderItem->node->children; node != NULL; node = node->next) { FolderItem *cur = FOLDER_ITEM(node->data); debug_print("in: %s\n", cur->path); if (!search_impl(messages, search, cur, TRUE)) { END_TIMING(); return FALSE; } } } END_TIMING(); } else if (!folderItem->no_select) { MsgNumberList *msgnums = NULL; MsgNumberList *cur; MsgInfoList *msgs = NULL; gboolean can_search_on_server = folderItem->folder->klass->supports_server_search; START_TIMING("folder"); if (!search_filter_folder(&msgnums, search, folderItem, can_search_on_server)) { g_slist_free(msgnums); END_TIMING(); return FALSE; } for (cur = msgnums; cur != NULL; cur = cur->next) { MsgInfo *msg = folder_item_get_msginfo(folderItem, GPOINTER_TO_UINT(cur->data)); msgs = g_slist_prepend(msgs, msg); } while (msgs != NULL) { MsgInfoList *front = msgs; msgs = msgs->next; front->next = *messages; *messages = front; } g_slist_free(msgnums); END_TIMING(); } return TRUE; }