diff --git a/src/engine/api/geary-search-folder.vala b/src/engine/api/geary-search-folder.vala index 4402a6fd..7c64fa38 100644 --- a/src/engine/api/geary-search-folder.vala +++ b/src/engine/api/geary-search-folder.vala @@ -35,9 +35,12 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { private weak Account _account; private SearchFolderProperties properties = new SearchFolderProperties(0, 0); - private Gee.HashSet exclude_folders = new Gee.HashSet(); - private Geary.SpecialFolderType[] exclude_types = { Geary.SpecialFolderType.SPAM, - Geary.SpecialFolderType.TRASH }; + private Gee.HashSet exclude_folders = new Gee.HashSet(); + private Geary.SpecialFolderType[] exclude_types = { + Geary.SpecialFolderType.SPAM, + Geary.SpecialFolderType.TRASH, + // Orphan emails (without a folder) are also excluded; see ctor. + }; private Gee.TreeSet search_results; private Geary.Nonblocking.Mutex result_mutex = new Geary.Nonblocking.Mutex(); @@ -49,12 +52,28 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { public SearchFolder(Account account) { _account = account; + account.folders_available_unavailable.connect(on_folders_available_unavailable); + clear_search_results(); - // TODO: The exclusion system needs to watch for changes, since the special folders are - // not always ready by the time this c'tor executes. - foreach(Geary.SpecialFolderType type in exclude_types) - exclude_special_folder(type); + // We always want to exclude emails that don't live anywhere from + // search results. + exclude_orphan_emails(); + } + + ~SearchFolder() { + account.folders_available_unavailable.disconnect(on_folders_available_unavailable);; + } + + private void on_folders_available_unavailable(Gee.Collection? available, + Gee.Collection? unavailable) { + if (available != null) { + foreach (Geary.Folder folder in available) { + // Exclude it from searching if it's got the right special type. + if (folder.get_special_folder_type() in exclude_types) + exclude_folder(folder); + } + } } /** @@ -217,16 +236,12 @@ public class Geary.SearchFolder : Geary.AbstractLocalFolder { return yield account.local_fetch_email_async(id, required_fields, cancellable); } - private void exclude_special_folder(Geary.SpecialFolderType type) { - Geary.Folder? folder = null; - try { - folder = account.get_special_folder(type); - } catch (Error e) { - debug("Could not get special folder: %s", e.message); - } - - if (folder != null) - exclude_folders.add(folder.get_path()); + private void exclude_folder(Geary.Folder folder) { + exclude_folders.add(folder.get_path()); + } + + private void exclude_orphan_emails() { + exclude_folders.add(null); } private uint email_id_hash(Geary.Email a) { diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala index e44018cf..a6c3701d 100644 --- a/src/engine/imap-db/imap-db-account.vala +++ b/src/engine/imap-db/imap-db-account.vala @@ -587,22 +587,41 @@ private class Geary.ImapDB.Account : BaseObject { return prepared_query.str.strip(); } + // Append each id in the collection to the StringBuilder, in a format + // suitable for use in an SQL statement IN (...) clause. + private void sql_append_ids(StringBuilder s, Gee.Collection ids) { + bool first = true; + foreach (int64? id in ids) { + assert(id != null); + + if (!first) + s.append(", "); + s.append(id.to_string()); + first = false; + } + } + public async Gee.Collection? search_async(string prepared_query, Geary.Email.Field requested_fields, bool partial_ok, int limit = 100, int offset = 0, Gee.Collection? folder_blacklist = null, Gee.Collection? search_ids = null, Cancellable? cancellable = null) throws Error { Gee.Collection search_results = new Gee.HashSet(); - // TODO: support blacklist, search_ids + // TODO: support search_ids yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { + string blacklisted_ids_sql = do_get_blacklisted_message_ids_sql( + folder_blacklist, cx, cancellable); + string sql = """ SELECT id FROM MessageSearchTable JOIN MessageTable USING (id) WHERE MessageSearchTable MATCH ? - ORDER BY internaldate_time_t DESC """; + if (blacklisted_ids_sql != "") + sql += " AND id NOT IN (%s)".printf(blacklisted_ids_sql); + sql += " ORDER BY internaldate_time_t DESC"; if (limit > 0) sql += " LIMIT ? OFFSET ?"; Db.Statement stmt = cx.prepare(sql); @@ -618,8 +637,11 @@ private class Geary.ImapDB.Account : BaseObject { MessageRow row = Geary.ImapDB.Folder.do_fetch_message_row( cx, id, requested_fields, cancellable); - if (partial_ok || row.fields.fulfills(requested_fields)) - search_results.add(row.to_email(-1, new Geary.ImapDB.EmailIdentifier(id))); + if (partial_ok || row.fields.fulfills(requested_fields)) { + Geary.Email email = row.to_email(-1, new Geary.ImapDB.EmailIdentifier(id)); + Geary.ImapDB.Folder.do_add_attachments(cx, email, id, cancellable); + search_results.add(email); + } result.next(cancellable); } @@ -877,6 +899,71 @@ private class Geary.ImapDB.Account : BaseObject { return do_fetch_folder_id(cx, path.get_parent(), create, out parent_id, cancellable); } + // Turn the collection of folder paths into actual folder ids. As a + // special case, if "folderless" or orphan emails are to be blacklisted, + // set the out bool to true. + private Gee.Collection do_get_blacklisted_folder_ids(Gee.Collection? folder_blacklist, + Db.Connection cx, out bool blacklist_folderless, Cancellable? cancellable) throws Error { + blacklist_folderless = false; + Gee.ArrayList ids = new Gee.ArrayList(); + + if (folder_blacklist != null) { + foreach (Geary.FolderPath? folder_path in folder_blacklist) { + if (folder_path == null) { + blacklist_folderless = true; + } else { + int64 id; + do_fetch_folder_id(cx, folder_path, true, out id, cancellable); + if (id != Db.INVALID_ROWID) + ids.add(id); + } + } + } + + return ids; + } + + // Return a parameterless SQL statement that selects any message ids that + // are in a blacklisted folder. This is used as a sub-select for the + // search query to omit results from blacklisted folders. + private string do_get_blacklisted_message_ids_sql(Gee.Collection? folder_blacklist, + Db.Connection cx, Cancellable? cancellable) throws Error { + bool blacklist_folderless; + Gee.Collection blacklisted_ids = do_get_blacklisted_folder_ids( + folder_blacklist, cx, out blacklist_folderless, cancellable); + + StringBuilder sql = new StringBuilder(); + if (blacklisted_ids.size > 0 || blacklist_folderless) { + if (blacklist_folderless) { + // We select out of the MessageTable and join on the location + // table so we can recognize emails that aren't in any folders. + // This is slightly more complicated than the case below, where + // we can just select directly out of the location table. + sql.append(""" + SELECT m.id + FROM MessageTable m + LEFT JOIN MessageLocationTable l ON l.message_id = m.id + WHERE folder_id IS NULL + """); + if (blacklisted_ids.size > 0) { + sql.append(" OR folder_id IN ("); + sql_append_ids(sql, blacklisted_ids); + sql.append(")"); + } + } else { + sql.append(""" + SELECT message_id + FROM MessageLocationTable + WHERE folder_id IN ( + """); + sql_append_ids(sql, blacklisted_ids); + sql.append(")"); + } + } + + return sql.str; + } + // For a message row id, return a set of all folders it's in, or null if // it's not in any folders. private Gee.Set? do_find_email_folders(Db.Connection cx, int64 message_id,