Merge branch 'mainline' into letorbi/gmime-3

This commit is contained in:
Torben 2019-12-18 11:38:02 +01:00
commit 77d44d41a3
41 changed files with 1260 additions and 1193 deletions

View file

@ -37,6 +37,7 @@ src/client/components/components-in-app-notification.vala
src/client/components/components-inspector.vala
src/client/components/components-placeholder-pane.vala
src/client/components/components-preferences-window.vala
src/client/components/components-search-bar.vala
src/client/components/components-validator.vala
src/client/components/components-web-view.vala
src/client/components/count-badge.vala
@ -46,7 +47,6 @@ src/client/components/main-toolbar.vala
src/client/components/main-window-info-bar.vala
src/client/components/monitored-progress-bar.vala
src/client/components/monitored-spinner.vala
src/client/components/search-bar.vala
src/client/components/status-bar.vala
src/client/components/stock.vala
src/client/composer/composer-box.vala
@ -144,7 +144,6 @@ src/engine/api/geary-named-flags.vala
src/engine/api/geary-problem-report.vala
src/engine/api/geary-progress-monitor.vala
src/engine/api/geary-revokable.vala
src/engine/api/geary-search-folder.vala
src/engine/api/geary-search-query.vala
src/engine/api/geary-service-information.vala
src/engine/api/geary-service-provider.vala
@ -153,6 +152,7 @@ src/engine/app/app-conversation-monitor.vala
src/engine/app/app-conversation.vala
src/engine/app/app-draft-manager.vala
src/engine/app/app-email-store.vala
src/engine/app/app-search-folder.vala
src/engine/app/conversation-monitor/app-append-operation.vala
src/engine/app/conversation-monitor/app-conversation-operation-queue.vala
src/engine/app/conversation-monitor/app-conversation-operation.vala
@ -229,16 +229,11 @@ src/engine/imap-db/imap-db-email-identifier.vala
src/engine/imap-db/imap-db-folder.vala
src/engine/imap-db/imap-db-gc.vala
src/engine/imap-db/imap-db-message-row.vala
src/engine/imap-db/search/imap-db-search-email-identifier.vala
src/engine/imap-db/search/imap-db-search-folder-properties.vala
src/engine/imap-db/search/imap-db-search-folder.vala
src/engine/imap-db/search/imap-db-search-query.vala
src/engine/imap-db/search/imap-db-search-term.vala
src/engine/imap-db/imap-db-search-query.vala
src/engine/imap-engine/gmail/imap-engine-gmail-account.vala
src/engine/imap-engine/gmail/imap-engine-gmail-all-mail-folder.vala
src/engine/imap-engine/gmail/imap-engine-gmail-drafts-folder.vala
src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
src/engine/imap-engine/gmail/imap-engine-gmail-spam-trash-folder.vala
src/engine/imap-engine/imap-engine-account-operation.vala
src/engine/imap-engine/imap-engine-account-processor.vala

View file

@ -148,7 +148,7 @@ msgstr "La darrera amplada registrada de la finestra de l'aplicació."
#: desktop/org.gnome.Geary.gschema.xml:20
msgid "Height of window"
msgstr "Alçada de l'aplicació"
msgstr "Alçada de la finestra"
#: desktop/org.gnome.Geary.gschema.xml:21
msgid "The last recorded height of the application window."
@ -614,7 +614,7 @@ msgstr "Yahoo"
#. loaded but disabled by the user.
#: src/client/accounts/accounts-editor-list-pane.vala:371
msgid "This account has been disabled"
msgstr "Aquest compte ha estat deshabilitat"
msgstr "Aquest compte ha estat inhabilitat"
#. Translators: Tooltip for accounts that have been
#. loaded but because of some error are not able to be
@ -759,7 +759,7 @@ msgstr "Copyright 2016 Software Freedom Conservancy Inc."
#: src/client/application/geary-application.vala:25
msgid "Copyright 2016-2019 Geary Development Team."
msgstr "Copyright 2016-2019 Geary Development Team."
msgstr "Copyright 2016-2019 equip de desenvolupament del Geary."
#: src/client/application/geary-application.vala:27
msgid "Visit the Geary web site"
@ -1434,7 +1434,7 @@ msgstr ""
#. Keep, Discard or Cancel.
#: src/client/composer/composer-widget.vala:1130
msgid "Do you want to keep or discard this draft message?"
msgstr "Vols mantindre o descartar aquest esborrany de missatge?"
msgstr "Vols mantenir o descartar aquest esborrany de missatge?"
#. Translators: This dialog text is displayed to the
#. user when closing a composer where the options are

View file

@ -878,6 +878,7 @@ internal class Application.Controller : Geary.BaseObject {
private async void open_account(Geary.Account account) {
AccountContext context = new AccountContext(
account,
new Geary.App.SearchFolder(account, account.local_folder_root),
new Geary.App.EmailStore(account),
new Application.ContactStore(account, this.folks)
);
@ -976,8 +977,10 @@ internal class Application.Controller : Geary.BaseObject {
account_unavailable(context, is_shutdown);
context.cancellable.cancel();
// Stop any background processes
context.search.clear();
context.contacts.close();
context.cancellable.cancel();
// Explicitly close the inbox since we explicitly open it
Geary.Folder? inbox = context.inbox;
@ -1770,6 +1773,9 @@ internal class Application.AccountContext : Geary.BaseObject {
/** The account's Inbox folder */
public Geary.Folder? inbox = null;
/** The account's search folder */
public Geary.App.SearchFolder search = null;
/** The account's email store */
public Geary.App.EmailStore emails { get; private set; }
@ -1818,9 +1824,11 @@ internal class Application.AccountContext : Geary.BaseObject {
public AccountContext(Geary.Account account,
Geary.App.SearchFolder search,
Geary.App.EmailStore emails,
Application.ContactStore contacts) {
this.account = account;
this.search = search;
this.emails = emails;
this.contacts = contacts;
}

View file

@ -282,7 +282,7 @@ public class Application.MainWindow :
// Widget descendants
public FolderList.Tree folder_list { get; private set; default = new FolderList.Tree(); }
public MainToolbar main_toolbar { get; private set; }
public SearchBar search_bar { get; private set; default = new SearchBar(); }
public SearchBar search_bar { get; private set; }
public ConversationListView conversation_list_view { get; private set; }
public ConversationViewer conversation_viewer { get; private set; }
public StatusBar status_bar { get; private set; default = new StatusBar(); }
@ -676,13 +676,14 @@ public class Application.MainWindow :
!this.folder_list.select_inbox(to_select.account))) {
this.folder_list.select_folder(to_select);
}
if (to_select.special_folder_type == SEARCH) {
this.previous_non_search_folder = to_select;
}
} else {
this.folder_list.deselect_folder();
}
if (!(to_select is Geary.SearchFolder)) {
this.previous_non_search_folder = to_select;
}
update_conversation_actions(NONE);
update_title();
this.main_toolbar.update_trash_button(
@ -821,9 +822,9 @@ public class Application.MainWindow :
/** Displays and focuses the search bar for the window. */
public void show_search_bar(string? text = null) {
this.search_bar.give_search_focus();
this.search_bar.grab_focus();
if (text != null) {
this.search_bar.set_search_text(text);
this.search_bar.entry.text = text;
}
}
@ -902,37 +903,46 @@ public class Application.MainWindow :
return closed;
}
public void show_search(string text, bool is_interactive) {
Geary.SearchFolder? search_folder = null;
if (this.selected_account != null) {
search_folder = this.selected_account.get_special_folder(
SEARCH
) as Geary.SearchFolder;
}
internal async void start_search(string query_text, bool is_interactive) {
var context = get_selected_account_context();
if (context != null) {
// Stop any search in progress
this.search_open.cancel();
var cancellable = this.search_open = new GLib.Cancellable();
var strategy = this.application.config.get_search_strategy();
try {
var query = yield context.account.new_search_query(
query_text,
strategy,
cancellable
);
this.folder_list.set_search(
this.application.engine, context.search
);
yield context.search.search(query, cancellable);
} catch (GLib.Error error) {
handle_error(context.account.information, error);
}
}
}
internal void stop_search(bool is_interactive) {
// Stop any search in progress
this.search_open.cancel();
var cancellable = this.search_open = new GLib.Cancellable();
this.search_open = new GLib.Cancellable();
if (Geary.String.is_empty_or_whitespace(text)) {
if (this.previous_non_search_folder != null &&
this.selected_folder is Geary.SearchFolder) {
this.select_folder.begin(
this.previous_non_search_folder, is_interactive
);
}
this.folder_list.remove_search();
if (search_folder != null) {
search_folder.clear();
}
} else if (search_folder != null) {
search_folder.search(
text, this.application.config.get_search_strategy(), cancellable
);
this.folder_list.set_search(
this.application.engine, search_folder
if (this.previous_non_search_folder != null &&
this.selected_folder.special_folder_type == SEARCH) {
this.select_folder.begin(
this.previous_non_search_folder, is_interactive
);
}
this.folder_list.remove_search();
foreach (var context in this.application.controller.get_account_contexts()) {
context.search.clear();
}
}
internal bool select_first_inbox(bool is_interactive) {
@ -972,6 +982,10 @@ public class Application.MainWindow :
Geary.Account.sort_by_path(to_add.account.list_folders())
);
add_folder(
((Geary.Smtp.ClientService) to_add.account.outgoing).outbox
);
this.accounts.add(to_add);
}
}
@ -991,15 +1005,14 @@ public class Application.MainWindow :
// that when the account is gone.
if (this.selected_folder != null &&
this.selected_folder.account == to_remove.account) {
Geary.SearchFolder? current_search = (
this.selected_folder as Geary.SearchFolder
bool is_account_search_active = (
this.selected_folder.special_folder_type == SEARCH
);
yield select_folder(to_select, false);
// Clear the account's search folder if it existed
if (current_search != null) {
this.search_bar.set_search_text("");
if (is_account_search_active) {
this.search_bar.entry.text = "";
this.search_bar.search_mode_enabled = false;
}
}
@ -1165,8 +1178,8 @@ public class Application.MainWindow :
this.notify["has-toplevel-focus"].connect(on_has_toplevel_focus);
// Search bar
this.search_bar = new SearchBar(this.application.engine);
this.search_bar.search_text_changed.connect(on_search);
this.search_bar.show();
this.search_bar_box.pack_start(this.search_bar, false, false, 0);
// Folder list
@ -1567,7 +1580,7 @@ public class Application.MainWindow :
if (!this.has_composer) {
if (this.conversations.size == 0) {
// Let the user know if there's no available conversations
if (this.selected_folder is Geary.SearchFolder) {
if (this.selected_folder.special_folder_type == SEARCH) {
this.conversation_viewer.show_empty_search();
} else {
this.conversation_viewer.show_empty_folder();
@ -2061,7 +2074,11 @@ public class Application.MainWindow :
}
private void on_search(string text) {
show_search(text, true);
if (Geary.String.is_empty_or_whitespace(text)) {
stop_search(true);
} else {
this.start_search.begin(text, true);
}
}
private void on_visible_conversations_changed(Gee.Set<Geary.App.Conversation> visible) {

View file

@ -0,0 +1,92 @@
/*
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class SearchBar : Hdy.SearchBar {
/// Translators: Search entry placeholder text
private const string DEFAULT_SEARCH_TEXT = _("Search");
public Gtk.SearchEntry entry {
get; private set; default = new Gtk.SearchEntry();
}
private Components.EntryUndo search_undo;
private Geary.Account? current_account = null;
private Geary.Engine engine;
public signal void search_text_changed(string search_text);
public SearchBar(Geary.Engine engine) {
this.engine = engine;
this.search_undo = new Components.EntryUndo(this.entry);
this.notify["search-mode-enabled"].connect(on_search_mode_changed);
/// Translators: Search entry tooltip
this.entry.tooltip_text = _("Search all mail in account for keywords");
this.entry.search_changed.connect(() => {
search_text_changed(this.entry.text);
});
this.entry.activate.connect(() => {
search_text_changed(this.entry.text);
});
this.entry.placeholder_text = DEFAULT_SEARCH_TEXT;
this.entry.has_focus = true;
var column = new Hdy.Column();
column.maximum_width = 450;
column.add(this.entry);
connect_entry(this.entry);
add(column);
show_all();
}
public override void grab_focus() {
set_search_mode(true);
this.entry.grab_focus();
}
public void set_account(Geary.Account? account) {
if (current_account != null) {
current_account.information.changed.disconnect(
on_information_changed
);
}
if (account != null) {
account.information.changed.connect(
on_information_changed
);
}
current_account = account;
on_information_changed(); // Set new account name.
}
private void on_information_changed() {
this.entry.placeholder_text = (
this.current_account == null || this.engine.accounts_count == 1
? DEFAULT_SEARCH_TEXT
/// Translators: Search entry placeholder, string
/// replacement is the name of an account
: _("Search %s account").printf(
this.current_account.information.display_name
)
);
}
private void on_search_mode_changed() {
if (!this.search_mode_enabled) {
this.search_undo.reset();
}
}
}

View file

@ -1,131 +0,0 @@
/* Copyright 2016 Software Freedom Conservancy Inc.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
public class SearchBar : Gtk.SearchBar {
private const string DEFAULT_SEARCH_TEXT = _("Search");
public string search_text { get { return search_entry.text; } }
public bool search_entry_has_focus { get { return search_entry.has_focus; } }
private Gtk.SearchEntry search_entry = new Gtk.SearchEntry();
private Components.EntryUndo search_undo;
private Geary.ProgressMonitor? search_upgrade_progress_monitor = null;
private MonitoredProgressBar search_upgrade_progress_bar = new MonitoredProgressBar();
private Geary.Account? current_account = null;
public signal void search_text_changed(string search_text);
public SearchBar() {
// Search entry.
search_entry.width_chars = 28;
search_entry.tooltip_text = _("Search all mail in account for keywords (Ctrl+S)");
search_entry.search_changed.connect(() => {
search_text_changed(search_entry.text);
});
search_entry.activate.connect(() => {
search_text_changed(search_entry.text);
});
search_entry.has_focus = true;
this.search_undo = new Components.EntryUndo(this.search_entry);
this.notify["search-mode-enabled"].connect(on_search_mode_changed);
// Search upgrade progress bar.
search_upgrade_progress_bar.show_text = true;
search_upgrade_progress_bar.visible = false;
search_upgrade_progress_bar.no_show_all = true;
add(search_upgrade_progress_bar);
add(search_entry);
set_search_placeholder_text(DEFAULT_SEARCH_TEXT);
}
public void set_search_text(string text) {
this.search_entry.text = text;
}
public void give_search_focus() {
set_search_mode(true);
search_entry.grab_focus();
}
public void set_search_placeholder_text(string placeholder) {
search_entry.placeholder_text = placeholder;
}
public void set_account(Geary.Account? account) {
on_search_upgrade_finished(); // Reset search box.
if (search_upgrade_progress_monitor != null) {
search_upgrade_progress_monitor.start.disconnect(on_search_upgrade_start);
search_upgrade_progress_monitor.finish.disconnect(on_search_upgrade_finished);
search_upgrade_progress_monitor = null;
}
if (current_account != null) {
current_account.information.changed.disconnect(
on_information_changed);
}
if (account != null) {
search_upgrade_progress_monitor = account.search_upgrade_monitor;
search_upgrade_progress_bar.set_progress_monitor(search_upgrade_progress_monitor);
search_upgrade_progress_monitor.start.connect(on_search_upgrade_start);
search_upgrade_progress_monitor.finish.connect(on_search_upgrade_finished);
if (search_upgrade_progress_monitor.is_in_progress)
on_search_upgrade_start(); // Remove search box, we're already in progress.
account.information.changed.connect(
on_information_changed);
search_upgrade_progress_bar.text =
_("Indexing %s account").printf(account.information.display_name);
}
current_account = account;
on_information_changed(); // Set new account name.
}
private void on_search_upgrade_start() {
// Set the progress bar's width to match the search entry's width.
int minimum_width = 0;
int natural_width = 0;
search_entry.get_preferred_width(out minimum_width, out natural_width);
search_upgrade_progress_bar.width_request = minimum_width;
search_entry.hide();
search_upgrade_progress_bar.show();
}
private void on_search_upgrade_finished() {
search_entry.show();
search_upgrade_progress_bar.hide();
}
private void on_information_changed() {
var main = get_toplevel() as Application.MainWindow;
if (main != null) {
set_search_placeholder_text(
current_account == null ||
main.application.engine.accounts_count == 1
? DEFAULT_SEARCH_TEXT :
_("Search %s account").printf(
current_account.information.display_name
)
);
}
}
private void on_search_mode_changed() {
if (!this.search_mode_enabled) {
this.search_undo.reset();
}
}
}

View file

@ -1275,7 +1275,9 @@ public class ConversationMessage : Gtk.Grid, Geary.BaseInterface {
WebKit.HitTestResult hit_test,
uint modifiers) {
this.body_container.set_tooltip_text(
hit_test.context_is_link() ? Util.Email.shorten_url(hit_test.get_link_uri()) : null
hit_test.context_is_link()
? Util.Gtk.shorten_url(hit_test.get_link_uri())
: null
);
this.body_container.trigger_tooltip_query();
}

View file

@ -296,10 +296,9 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
conversation.base_folder.account, null
);
if (query == null) {
Geary.SearchFolder? search_folder =
conversation.base_folder as Geary.SearchFolder;
var search_folder = conversation.base_folder as Geary.App.SearchFolder;
if (search_folder != null) {
query = search_folder.search_query;
query = search_folder.query;
}
}
@ -425,8 +424,11 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
// opening every message in the conversation as soon as
// the user presses a key
if (text.length >= 2) {
query = yield account.open_search(
text, this.config.get_search_strategy(), cancellable
var strategy = this.config.get_search_strategy();
query = yield account.new_search_query(
text,
strategy,
cancellable
);
}
}
@ -452,16 +454,16 @@ public class ConversationViewer : Gtk.Stack, Geary.BaseInterface {
} else {
// Find became disabled, re-show search terms if any
this.current_list.search.unmark_terms();
Geary.SearchFolder? search_folder = (
Geary.App.SearchFolder? search_folder = (
this.current_list.conversation.base_folder
as Geary.SearchFolder
as Geary.App.SearchFolder
);
this.conversation_find_undo.reset();
if (search_folder != null) {
Geary.SearchQuery? search_query = search_folder.search_query;
if (search_query != null) {
Geary.SearchQuery? query = search_folder.query;
if (query != null) {
this.current_list.search.highlight_matching_email.begin(
search_query,
query,
true
);
}

View file

@ -8,12 +8,12 @@
* This branch is a top-level container for a search entry.
*/
public class FolderList.SearchBranch : Sidebar.RootOnlyBranch {
public SearchBranch(Geary.SearchFolder folder, Geary.Engine engine) {
public SearchBranch(Geary.App.SearchFolder folder, Geary.Engine engine) {
base(new SearchEntry(folder, engine));
}
public Geary.SearchFolder get_search_folder() {
return (Geary.SearchFolder) ((SearchEntry) get_root()).folder;
public Geary.App.SearchFolder get_search_folder() {
return (Geary.App.SearchFolder) ((SearchEntry) get_root()).folder;
}
}
@ -22,7 +22,7 @@ public class FolderList.SearchEntry : FolderList.AbstractFolderEntry {
Geary.Engine engine;
private int account_count = 0;
public SearchEntry(Geary.SearchFolder folder,
public SearchEntry(Geary.App.SearchFolder folder,
Geary.Engine engine) {
base(folder);
this.engine = engine;

View file

@ -244,7 +244,7 @@ public class FolderList.Tree : Sidebar.Tree, Geary.BaseInterface {
}
public void set_search(Geary.Engine engine,
Geary.SearchFolder search_folder) {
Geary.App.SearchFolder search_folder) {
if (search_branch != null && has_branch(search_branch)) {
// We already have a search folder. If it's the same one, just
// select it. If it's a new search folder, remove the old one and

View file

@ -37,6 +37,7 @@ geary_client_vala_sources = files(
'components/components-inspector-system-view.vala',
'components/components-placeholder-pane.vala',
'components/components-preferences-window.vala',
'components/components-search-bar.vala',
'components/components-validator.vala',
'components/components-web-view.vala',
'components/count-badge.vala',
@ -46,7 +47,6 @@ geary_client_vala_sources = files(
'components/main-window-info-bar.vala',
'components/monitored-progress-bar.vala',
'components/monitored-spinner.vala',
'components/search-bar.vala',
'components/status-bar.vala',
'components/stock.vala',

View file

@ -281,13 +281,4 @@ namespace Util.Email {
return body_text;
}
private string shorten_url(string url) {
string new_url = "";
if (url.length < 90) {
new_url = url;
} else {
new_url = url.substring(0,40) + "..." + url.substring(-40);
}
return new_url;
}
}

View file

@ -209,4 +209,13 @@ namespace Util.Gtk {
return copy;
}
/** Returns a truncated form of a URL if it is too long for display. */
public string shorten_url(string url) {
string new_url = url;
if (url.length >= 90) {
new_url = url.substring(0,40) + "" + url.substring(-40);
}
return new_url;
}
}

View file

@ -131,8 +131,17 @@ public abstract class Geary.Account : BaseObject, Logging.Source {
*/
public Geary.ContactStore contact_store { get; protected set; }
/**
* The root path for all local folders.
*
* Any local folders create by the engine or clients must use this
* as the root for local folders.
*/
public FolderRoot local_folder_root {
get; private set; default = new Geary.FolderRoot("$geary-local", true);
}
public ProgressMonitor background_progress { get; protected set; }
public ProgressMonitor search_upgrade_monitor { get; protected set; }
public ProgressMonitor db_upgrade_monitor { get; protected set; }
public ProgressMonitor db_vacuum_monitor { get; protected set; }
@ -425,6 +434,22 @@ public abstract class Geary.Account : BaseObject, Logging.Source {
public abstract async Geary.Email local_fetch_email_async(Geary.EmailIdentifier email_id,
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error;
/**
* Return a collection of email with the given identifiers.
*
* The returned collection will be in the same order as the
* natural ordering of the given identifiers.
*
* Throws {@link EngineError.NOT_FOUND} if any email is not found
* and {@link EngineError.INCOMPLETE_MESSAGE} if the fields aren't
* available.
*/
public abstract async Gee.List<Email> list_local_email_async(
Gee.Collection<EmailIdentifier> ids,
Email.Field required_fields,
GLib.Cancellable? cancellable = null
) throws GLib.Error;
/**
* Create a new {@link SearchQuery} for this {@link Account}.
*
@ -436,13 +461,12 @@ public abstract class Geary.Account : BaseObject, Logging.Source {
* baked into the caller's code is up to the caller. CONSERVATIVE is designed to be a good
* default.
*
* The SearchQuery object can only be used with calls into this Account.
*
* Dropping the last reference to the SearchQuery will close it.
* The resulting object can only be used with calls into this
* account instance.
*/
public abstract async Geary.SearchQuery open_search(string query,
SearchQuery.Strategy strategy,
GLib.Cancellable? cancellable)
public abstract async SearchQuery new_search_query(string query,
SearchQuery.Strategy strategy,
GLib.Cancellable? cancellable)
throws GLib.Error;
/**

View file

@ -18,21 +18,18 @@
* they will be unique throughout the Geary engine.
*/
public abstract class Geary.EmailIdentifier : BaseObject, Gee.Hashable<Geary.EmailIdentifier> {
public abstract class Geary.EmailIdentifier :
BaseObject, Gee.Hashable<Geary.EmailIdentifier> {
/** Base variant type returned by {@link to_variant}. */
public const string BASE_VARIANT_TYPE = "(y??)";
public const string BASE_VARIANT_TYPE = "(yr)";
// Warning: only change this if you know what you are doing.
protected string unique;
protected EmailIdentifier(string unique) {
this.unique = unique;
}
/** {@inheritDoc} */
public abstract uint hash();
public virtual uint hash() {
return unique.hash();
}
/** {@inheritDoc} */
public abstract bool equal_to(EmailIdentifier other);
/**
* Returns a representation useful for serialisation.
@ -50,16 +47,7 @@ public abstract class Geary.EmailIdentifier : BaseObject, Gee.Hashable<Geary.Ema
/**
* Returns a representation useful for debugging.
*/
public virtual string to_string() {
return "[%s]".printf(unique.to_string());
}
public virtual bool equal_to(Geary.EmailIdentifier other) {
if (this == other)
return true;
return unique == other.unique;
}
public abstract string to_string();
/**
* A comparator for stabilizing sorts.
@ -71,7 +59,7 @@ public abstract class Geary.EmailIdentifier : BaseObject, Gee.Hashable<Geary.Ema
if (this == other)
return 0;
return strcmp(unique, other.unique);
return strcmp(to_string(), other.to_string());
}
/**

View file

@ -662,19 +662,6 @@ public abstract class Geary.Folder : BaseObject, Logging.Source {
Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields, ListFlags flags,
Cancellable? cancellable = null) throws Error;
/**
* Returns the locally available Geary.Email.Field fields for the specified emails. If a
* list or fetch operation occurs on the emails that specifies a field not returned here,
* the Engine will either have to go out to the remote server to get it, or (if
* ListFlags.LOCAL_ONLY is specified) not return it to the caller.
*
* If the EmailIdentifier is unknown locally, it will not be present in the returned Map.
*
* The Folder must be opened prior to attempting this operation.
*/
public abstract async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>? list_local_email_fields_async(
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error;
/**
* Returns a single email that fulfills the required_fields flag at the ordered position in
* the folder. If the email_id is invalid for the folder's contents, an EngineError.NOT_FOUND

View file

@ -1,81 +0,0 @@
/* Copyright 2016 Software Freedom Conservancy Inc.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Special local {@link Folder} used to query and display search results of {@link Email} from
* across the {@link Account}'s local storage.
*
* SearchFolder is merely specified to be a Folder, but implementations may add various
* {@link FolderSupport} interfaces. In particular {@link FolderSupport.Remove} should be supported,
* but again, is not required.
*
* SearchFolder is expected to produce {@link EmailIdentifier}s which can be accepted by other
* Folders within the Account (with the exception of the Outbox). Those Folders may need to
* translate those EmailIdentifiers to their own type for ordering reasons, but in general the
* expectation is that the results of SearchFolder can then be applied to operations on Email in
* other remote-backed folders.
*/
public abstract class Geary.SearchFolder : Geary.AbstractLocalFolder {
private weak Account _account;
public override Account account { get { return _account; } }
private FolderProperties _properties;
public override FolderProperties properties { get { return _properties; } }
private FolderPath? _path = null;
public override FolderPath path { get { return _path; } }
public override SpecialFolderType special_folder_type {
get {
return Geary.SpecialFolderType.SEARCH;
}
}
public Geary.SearchQuery? search_query { get; protected set; default = null; }
/**
* Fired when the search query has changed. This signal is fired *after* the search
* has completed.
*/
public signal void search_query_changed(Geary.SearchQuery? query);
protected SearchFolder(Account account, FolderProperties properties, FolderPath path) {
_account = account;
_properties = properties;
_path = path;
}
protected virtual void notify_search_query_changed(SearchQuery? query) {
search_query_changed(query);
}
/**
* Sets the keyword string for this search.
*
* This is a nonblocking call that initiates a background search which can be stopped with the
* supplied Cancellable.
*
* When the search is completed, {@link search_query_changed} will be fired. It's possible for
* the {@link search_query} property to change before completion.
*/
public abstract void search(string query, SearchQuery.Strategy strategy, Cancellable? cancellable = null);
/**
* Clears the search query and results.
*
* {@link search_query_changed} will be fired and {@link search_query} will be set to null.
*/
public abstract void clear();
/**
* Given a list of mail IDs, returns a set of casefolded words that match for the current
* search query.
*/
public abstract async Gee.Set<string>? get_search_matches_async(
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error;
}

View file

@ -1,20 +1,27 @@
/* Copyright 2016 Software Freedom Conservancy Inc.
/*
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2019 Michael Gratton <mike@vee.met>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* An object to hold state for various search subsystems that might need to
* parse the same text string different ways.
* Specifies an expression for searching email in a search folder.
*
* The only interaction the API user should have with this is creating new ones and then passing
* them to the search methods in the Engine.
* New instances can be constructed via {@link
* Account.new_search_query} and then passed to search methods on
* {@link Account} or {@link App.SearchFolder}.
*
* @see Geary.Account.open_search
* @see Account.new_search_query
* @see Account.local_search_async
* @see Account.get_search_matches_async
* @see App.SearchFolder.search
*/
public abstract class Geary.SearchQuery : BaseObject {
/**
* An advisory parameter regarding search quality, scope, and breadth.
*
@ -50,8 +57,14 @@ public abstract class Geary.SearchQuery : BaseObject {
HORIZON
}
/** The account that owns this query. */
public Account owner { get; private set; }
/**
* The original user search text.
* The original search text.
*
* This is used mostly for debugging.
*/
public string raw { get; private set; }
@ -60,7 +73,11 @@ public abstract class Geary.SearchQuery : BaseObject {
*/
public Strategy strategy { get; private set; }
protected SearchQuery(string raw, Strategy strategy) {
protected SearchQuery(Account owner,
string raw,
Strategy strategy) {
this.owner = owner;
this.raw = raw;
this.strategy = strategy;
}

View file

@ -0,0 +1,632 @@
/*
* Copyright 2016 Software Freedom Conservancy Inc.
* Copyright 2019 Michael Gratton <mike@vee.net>
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* A folder for executing and listing an account-wide email search.
*
* This uses the search methods on {@link Account} to implement the
* search, then collects search results and presents them via the
* folder interface.
*/
public class Geary.App.SearchFolder :
AbstractLocalFolder, FolderSupport.Remove {
/** Number of messages to include in the initial search. */
public const int MAX_RESULT_EMAILS = 1000;
/** The canonical name of the search folder. */
public const string MAGIC_BASENAME = "$GearyAccountSearchFolder$";
private const SpecialFolderType[] EXCLUDE_TYPES = {
SpecialFolderType.SPAM,
SpecialFolderType.TRASH,
SpecialFolderType.DRAFTS,
// Orphan emails (without a folder) are also excluded; see ct or.
};
private class FolderPropertiesImpl : FolderProperties {
public FolderPropertiesImpl(int total, int unread) {
base(total, unread, Trillian.FALSE, Trillian.FALSE, Trillian.TRUE, true, true, false);
}
public void set_total(int total) {
this.email_total = total;
}
}
// Represents an entry in the folder. Does not implement
// Gee.Comparable since that would require extending GLib.Object
// and hence make them very heavyweight.
private class EmailEntry {
public static int compare_to(EmailEntry a, EmailEntry b) {
int cmp = 0;
if (a != b && a.id != b.id && !a.id.equal_to(b.id)) {
cmp = a.received.compare(b.received);
if (cmp == 0) {
cmp = a.id.stable_sort_comparator(b.id);
}
}
return cmp;
}
public EmailIdentifier id;
public GLib.DateTime received;
public EmailEntry(EmailIdentifier id, GLib.DateTime received) {
this.id = id;
this.received = received;
}
}
/** {@inheritDoc} */
public override Account account {
get { return _account; }
}
private weak Account _account;
/** {@inheritDoc} */
public override FolderProperties properties {
get { return _properties; }
}
private FolderPropertiesImpl _properties;
/** {@inheritDoc} */
public override FolderPath path {
get { return _path; }
}
private FolderPath? _path = null;
/**
* {@inheritDoc}
*
* Always returns {@link SpecialFolderType.SEARCH}.
*/
public override SpecialFolderType special_folder_type {
get { return SpecialFolderType.SEARCH; }
}
/** The query being evaluated by this folder, if any. */
public SearchQuery? query { get; protected set; default = null; }
// Folders that should be excluded from search
private Gee.HashSet<FolderPath?> exclude_folders =
new Gee.HashSet<FolderPath?>();
// The email present in the folder, sorted
private Gee.TreeSet<EmailEntry> contents;
// Map of engine ids to search ids
private Gee.Map<EmailIdentifier,EmailEntry> id_map;
private Nonblocking.Mutex result_mutex = new Nonblocking.Mutex();
private GLib.Cancellable executing = new GLib.Cancellable();
public SearchFolder(Account account, FolderRoot root) {
this._account = account;
this._properties = new FolderPropertiesImpl(0, 0);
this._path = root.get_child(MAGIC_BASENAME, Trillian.TRUE);
account.folders_available_unavailable.connect(on_folders_available_unavailable);
account.folders_special_type.connect(on_folders_special_type);
account.email_locally_complete.connect(on_email_locally_complete);
account.email_removed.connect(on_account_email_removed);
new_contents();
// Always exclude emails that don't live anywhere from search
// results.
exclude_orphan_emails();
}
~SearchFolder() {
account.folders_available_unavailable.disconnect(on_folders_available_unavailable);
account.folders_special_type.disconnect(on_folders_special_type);
account.email_locally_complete.disconnect(on_email_locally_complete);
account.email_removed.disconnect(on_account_email_removed);
}
/**
* Executes the given query over the account's local email.
*
* Calling this will block until the search is complete.
*/
public async void search(SearchQuery query, GLib.Cancellable? cancellable)
throws GLib.Error {
int result_mutex_token = yield result_mutex.claim_async();
clear();
if (cancellable != null) {
GLib.Cancellable @internal = this.executing;
cancellable.cancelled.connect(() => { @internal.cancel(); });
}
this.query = query;
GLib.Error? error = null;
try {
yield do_search_async(null, null, this.executing);
} catch(Error e) {
error = e;
}
result_mutex.release(ref result_mutex_token);
if (error != null) {
throw error;
}
}
/**
* Cancels and clears the search query and results.
*
* The {@link query} property will be cleared.
*/
public void clear() {
this.executing.cancel();
this.executing = new GLib.Cancellable();
var old_ids = this.id_map;
new_contents();
notify_email_removed(old_ids.keys);
notify_email_count_changed(0, REMOVED);
this.query = null;
}
/**
* Returns a set of case-folded words matched by the current query.
*
* The set contains words from the given collection of email that
* match any of the non-negated text operators in {@link query}.
*/
public async Gee.Set<string>? get_search_matches_async(
Gee.Collection<EmailIdentifier> targets,
GLib.Cancellable? cancellable = null
) throws GLib.Error {
Gee.Set<string>? results = null;
if (this.query != null) {
results = yield account.get_search_matches_async(
this.query, check_ids(targets), cancellable
);
}
return results;
}
public override async Gee.List<Email>? list_email_by_id_async(
EmailIdentifier? initial_id,
int count,
Email.Field required_fields,
Folder.ListFlags flags,
Cancellable? cancellable = null
) throws GLib.Error {
int result_mutex_token = yield result_mutex.claim_async();
var engine_ids = new Gee.LinkedList<EmailIdentifier>();
if (Folder.ListFlags.OLDEST_TO_NEWEST in flags) {
EmailEntry? oldest = null;
if (!this.contents.is_empty) {
if (initial_id == null) {
oldest = this.contents.last();
} else {
oldest = this.id_map.get(initial_id);
if (oldest == null) {
throw new EngineError.NOT_FOUND(
"Initial id not found: %s", initial_id.to_string()
);
}
if (!(Folder.ListFlags.INCLUDING_ID in flags)) {
oldest = contents.higher(oldest);
}
}
}
if (oldest != null) {
var iter = (
this.contents.iterator_at(oldest) as
Gee.BidirIterator<EmailEntry>
);
engine_ids.add(oldest.id);
while (engine_ids.size < count && iter.previous()) {
engine_ids.add(iter.get().id);
}
}
} else {
// Newest to oldest
EmailEntry? newest = null;
if (!this.contents.is_empty) {
if (initial_id == null) {
newest = this.contents.first();
} else {
newest = this.id_map.get(initial_id);
if (newest == null) {
throw new EngineError.NOT_FOUND(
"Initial id not found: %s", initial_id.to_string()
);
}
if (!(Folder.ListFlags.INCLUDING_ID in flags)) {
newest = contents.lower(newest);
}
}
}
if (newest != null) {
var iter = (
this.contents.iterator_at(newest) as
Gee.BidirIterator<EmailEntry>
);
engine_ids.add(newest.id);
while (engine_ids.size < count && iter.next()) {
engine_ids.add(iter.get().id);
}
}
}
Gee.List<Email>? results = null;
GLib.Error? list_error = null;
if (!engine_ids.is_empty) {
try {
results = yield this.account.list_local_email_async(
engine_ids,
required_fields,
cancellable
);
} catch (GLib.Error error) {
list_error = error;
}
}
result_mutex.release(ref result_mutex_token);
if (list_error != null) {
throw list_error;
}
return results;
}
public override async Gee.List<Email>? list_email_by_sparse_id_async(
Gee.Collection<EmailIdentifier> list,
Email.Field required_fields,
Folder.ListFlags flags,
Cancellable? cancellable = null
) throws GLib.Error {
return yield this.account.list_local_email_async(
check_ids(list), required_fields, cancellable
);
}
public override async Email fetch_email_async(EmailIdentifier fetch,
Email.Field required_fields,
Folder.ListFlags flags,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
require_id(fetch);
return yield this.account.local_fetch_email_async(
fetch, required_fields, cancellable
);
}
public virtual async void remove_email_async(
Gee.Collection<EmailIdentifier> remove,
GLib.Cancellable? cancellable = null
) throws GLib.Error {
Gee.MultiMap<EmailIdentifier,FolderPath>? ids_to_folders =
yield account.get_containing_folders_async(
check_ids(remove), cancellable
);
if (ids_to_folders != null) {
Gee.MultiMap<FolderPath,EmailIdentifier> folders_to_ids =
Collection.reverse_multi_map<EmailIdentifier,FolderPath>(
ids_to_folders
);
foreach (FolderPath path in folders_to_ids.get_keys()) {
Folder folder = account.get_folder(path);
FolderSupport.Remove? removable = folder as FolderSupport.Remove;
if (removable != null) {
Gee.Collection<EmailIdentifier> ids = folders_to_ids.get(path);
debug("Search folder removing %d emails from %s", ids.size, folder.to_string());
bool open = false;
try {
yield folder.open_async(NONE, cancellable);
open = true;
yield removable.remove_email_async(ids, cancellable);
} finally {
if (open) {
try {
yield folder.close_async();
} catch (Error e) {
debug("Error closing folder %s: %s", folder.to_string(), e.message);
}
}
}
}
}
}
}
private void require_id(EmailIdentifier id)
throws EngineError.NOT_FOUND {
if (!this.id_map.has_key(id)) {
throw new EngineError.NOT_FOUND(
"Id not found: %s", id.to_string()
);
}
}
private Gee.List<EmailIdentifier> check_ids(
Gee.Collection<EmailIdentifier> to_check
) {
var available = new Gee.LinkedList<EmailIdentifier>();
var id_map = this.id_map;
var iter = to_check.iterator();
while (iter.next()) {
var id = iter.get();
if (id_map.has_key(id)) {
available.add(id);
}
}
return available;
}
// NOTE: you must call this ONLY after locking result_mutex_token.
// If both *_ids parameters are null, the results of this search are
// considered to be the full new set. If non-null, the results are
// considered to be a delta and are added or subtracted from the full set.
// add_ids are new ids to search for, remove_ids are ids in our result set
// and will be removed.
private async void do_search_async(Gee.Collection<EmailIdentifier>? add_ids,
Gee.Collection<EmailIdentifier>? remove_ids,
GLib.Cancellable? cancellable)
throws GLib.Error {
var id_map = this.id_map;
var contents = this.contents;
var added = new Gee.LinkedList<EmailIdentifier>();
var removed = new Gee.LinkedList<EmailIdentifier>();
if (remove_ids == null) {
// Adding email to the search, either searching all local
// email if to_add is null, or adding only a matching
// subset of the given in to_add
//
// TODO: don't limit this to MAX_RESULT_EMAILS. Instead,
// we could be smarter about only fetching the search
// results in list_email_async() etc., but this leads to
// some more complications when redoing the search.
Gee.Collection<EmailIdentifier>? id_results =
yield this.account.local_search_async(
this.query,
MAX_RESULT_EMAILS,
0,
this.exclude_folders,
add_ids, // If null, will search all local email
cancellable
);
if (id_results != null) {
// Fetch email to get the received date for
// correct ordering in the search folder
Gee.Collection<Email> email_results =
yield this.account.list_local_email_async(
id_results,
PROPERTIES,
cancellable
);
if (add_ids == null) {
// Not appending new email, so remove any not
// found in the results. Add to a set first to
// avoid O(N^2) lookup complexity.
var hashed_results = new Gee.HashSet<EmailIdentifier>();
hashed_results.add_all(id_results);
var existing = id_map.map_iterator();
while (existing.next()) {
if (!hashed_results.contains(existing.get_key())) {
var entry = existing.get_value();
existing.unset();
contents.remove(entry);
removed.add(entry.id);
}
}
}
foreach (var email in email_results) {
if (!id_map.has_key(email.id)) {
var entry = new EmailEntry(
email.id, email.properties.date_received
);
contents.add(entry);
id_map.set(email.id, entry);
added.add(email.id);
}
}
}
} else {
// Removing email, can just remove them directly
foreach (var id in remove_ids) {
EmailEntry entry;
if (id_map.unset(id, out entry)) {
contents.remove(entry);
removed.add(id);
}
}
}
this._properties.set_total(this.contents.size);
// Note that we probably shouldn't be firing these signals from inside
// our mutex lock. Keep an eye on it, and if there's ever a case where
// it might cause problems, it shouldn't be too hard to move the
// firings outside.
Folder.CountChangeReason reason = CountChangeReason.NONE;
if (added.size > 0) {
// TODO: we'd like to be able to use APPENDED here when applicable,
// but because of the potential to append a thousand results at
// once and the ConversationMonitor's inability to handle that
// gracefully (#7464), we always use INSERTED for now.
notify_email_inserted(added);
reason |= Folder.CountChangeReason.INSERTED;
}
if (removed.size > 0) {
notify_email_removed(removed);
reason |= Folder.CountChangeReason.REMOVED;
}
if (reason != CountChangeReason.NONE)
notify_email_count_changed(this.contents.size, reason);
}
private async void do_append(Folder folder,
Gee.Collection<EmailIdentifier> ids,
GLib.Cancellable? cancellable)
throws GLib.Error {
int result_mutex_token = yield result_mutex.claim_async();
GLib.Error? error = null;
try {
if (!this.exclude_folders.contains(folder.path)) {
yield do_search_async(ids, null, cancellable);
}
} catch (GLib.Error e) {
error = e;
}
result_mutex.release(ref result_mutex_token);
if (error != null)
throw error;
}
private async void do_remove(Folder folder,
Gee.Collection<EmailIdentifier> ids,
GLib.Cancellable? cancellable)
throws GLib.Error {
int result_mutex_token = yield result_mutex.claim_async();
GLib.Error? error = null;
try {
var id_map = this.id_map;
var relevant_ids = (
traverse(ids)
.filter(id => id_map.has_key(id))
.to_linked_list()
);
if (relevant_ids.size > 0) {
yield do_search_async(null, relevant_ids, cancellable);
}
} catch (GLib.Error e) {
error = e;
}
result_mutex.release(ref result_mutex_token);
if (error != null)
throw error;
}
private inline void new_contents() {
this.contents = new Gee.TreeSet<EmailEntry>(EmailEntry.compare_to);
this.id_map = new Gee.HashMap<EmailIdentifier,EmailEntry>();
}
private void include_folder(Folder folder) {
this.exclude_folders.remove(folder.path);
}
private void exclude_folder(Folder folder) {
this.exclude_folders.add(folder.path);
}
private void exclude_orphan_emails() {
this.exclude_folders.add(null);
}
private void on_folders_available_unavailable(
Gee.Collection<Folder>? available,
Gee.Collection<Folder>? unavailable
) {
if (available != null) {
// Exclude it from searching if it's got the right special type.
foreach(Folder folder in traverse<Folder>(available)
.filter(f => f.special_folder_type in EXCLUDE_TYPES))
exclude_folder(folder);
}
}
private void on_folders_special_type(Gee.Collection<Folder> folders) {
foreach (Folder folder in folders) {
if (folder.special_folder_type in EXCLUDE_TYPES) {
exclude_folder(folder);
} else {
include_folder(folder);
}
}
}
private void on_email_locally_complete(Folder folder,
Gee.Collection<EmailIdentifier> ids) {
if (this.query != null) {
this.do_append.begin(
folder, ids, null,
(obj, res) => {
try {
this.do_append.end(res);
} catch (GLib.Error error) {
this.account.report_problem(
new AccountProblemReport(
this.account.information, error
)
);
}
}
);
}
}
private void on_account_email_removed(Folder folder,
Gee.Collection<EmailIdentifier> ids) {
if (this.query != null) {
this.do_remove.begin(
folder, ids, null,
(obj, res) => {
try {
this.do_remove.end(res);
} catch (GLib.Error error) {
this.account.report_problem(
new AccountProblemReport(
this.account.information, error
)
);
}
}
);
}
}
}

View file

@ -37,13 +37,12 @@ private class Geary.ImapDB.Account : BaseObject {
get; private set; default = new Imap.FolderRoot("$geary-imap");
}
// Only available when the Account is opened
public IntervalProgressMonitor search_index_monitor { get; private set;
default = new IntervalProgressMonitor(ProgressType.SEARCH_INDEX, 0, 0); }
public SimpleProgressMonitor upgrade_monitor { get; private set; default = new SimpleProgressMonitor(
ProgressType.DB_UPGRADE); }
public SimpleProgressMonitor vacuum_monitor { get; private set; default = new SimpleProgressMonitor(
ProgressType.DB_VACUUM); }
public SimpleProgressMonitor upgrade_monitor {
get; private set; default = new SimpleProgressMonitor(DB_UPGRADE);
}
public SimpleProgressMonitor vacuum_monitor {
get; private set; default = new SimpleProgressMonitor(DB_VACUUM);
}
/** The backing database for the account. */
public ImapDB.Database db { get; private set; }
@ -575,7 +574,7 @@ private class Geary.ImapDB.Account : BaseObject {
foreach (string? field in query.get_fields()) {
debug(" - Field \"%s\" terms:", field);
foreach (SearchTerm? term in query.get_search_terms(field)) {
foreach (SearchQuery.Term? term in query.get_search_terms(field)) {
if (term != null) {
debug(" - \"%s\": %s, %s",
term.original,
@ -613,7 +612,7 @@ private class Geary.ImapDB.Account : BaseObject {
// <http://redmine.yorba.org/issues/7372>.
StringBuilder sql = new StringBuilder();
sql.append("""
SELECT id, internaldate_time_t
SELECT id
FROM MessageTable
INDEXED BY MessageTableInternalDateTimeTIndex
""");
@ -650,11 +649,7 @@ private class Geary.ImapDB.Account : BaseObject {
Db.Result result = stmt.exec(cancellable);
while (!result.finished) {
int64 message_id = result.int64_at(0);
int64 internaldate_time_t = result.int64_at(1);
DateTime? internaldate = (internaldate_time_t == -1
? null : new DateTime.from_unix_local(internaldate_time_t));
ImapDB.EmailIdentifier id = new ImapDB.SearchEmailIdentifier(message_id, internaldate);
var id = new ImapDB.EmailIdentifier(message_id, null);
matching_ids.add(id);
id_map.set(message_id, id);
@ -739,7 +734,7 @@ private class Geary.ImapDB.Account : BaseObject {
Gee.Set<string>? result = results.get(id);
if (result != null) {
foreach (string match in result) {
foreach (SearchTerm term in query.get_all_terms()) {
foreach (SearchQuery.Term term in query.get_all_terms()) {
// if prefix-matches parsed term, then don't strip
if (match.has_prefix(term.parsed)) {
good_match_found = true;
@ -804,6 +799,48 @@ private class Geary.ImapDB.Account : BaseObject {
return search_matches;
}
public async Gee.List<Email>? list_email(Gee.Collection<EmailIdentifier> ids,
Email.Field required_fields,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
check_open();
var results = new Gee.ArrayList<Email>();
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
foreach (var id in ids) {
// TODO: once we have a way of deleting messages, we won't be able
// to assume that a row id will point to the same email outside of
// transactions, because SQLite will reuse row ids.
Geary.Email.Field db_fields;
MessageRow row = Geary.ImapDB.Folder.do_fetch_message_row(
cx, id.message_id, required_fields, out db_fields, cancellable
);
if (!row.fields.fulfills(required_fields)) {
throw new EngineError.INCOMPLETE_MESSAGE(
"Message %s only fulfills %Xh fields (required: %Xh)",
id.to_string(), row.fields, required_fields
);
}
Email email = row.to_email(id);
Attachment.add_attachments(
cx,
this.db.attachments_path,
email,
id.message_id,
cancellable
);
results.add(email);
}
return Db.TransactionOutcome.DONE;
},
cancellable
);
return results;
}
public async Geary.Email fetch_email_async(ImapDB.EmailIdentifier email_id,
Geary.Email.Field required_fields, Cancellable? cancellable = null) throws Error {
check_open();
@ -880,9 +917,6 @@ private class Geary.ImapDB.Account : BaseObject {
debug("Error populating %s search table: %s", account_information.id, e.message);
}
if (search_index_monitor.is_in_progress)
search_index_monitor.notify_finish();
debug("%s: Done populating search table", account_information.id);
}
@ -976,13 +1010,6 @@ private class Geary.ImapDB.Account : BaseObject {
if (count > 0) {
debug("%s: Found %d/%d missing indexed messages, %d remaining...",
account_information.id, count, limit, total_unindexed);
if (!search_index_monitor.is_in_progress) {
search_index_monitor.set_interval(0, total_unindexed);
search_index_monitor.notify_start();
}
search_index_monitor.increment(count);
}
return (count < limit);

View file

@ -9,7 +9,7 @@
private class Geary.ImapDB.EmailIdentifier : Geary.EmailIdentifier {
private const string VARIANT_TYPE = "(yxx)";
private const string VARIANT_TYPE = "(y(xx))";
public int64 message_id { get; private set; }
@ -18,8 +18,6 @@ private class Geary.ImapDB.EmailIdentifier : Geary.EmailIdentifier {
public EmailIdentifier(int64 message_id, Imap.UID? uid) {
assert(message_id != Db.INVALID_ROWID);
base (message_id.to_string());
this.message_id = message_id;
this.uid = uid;
}
@ -27,8 +25,6 @@ private class Geary.ImapDB.EmailIdentifier : Geary.EmailIdentifier {
// Used when a new message comes off the wire and doesn't have a rowid associated with it (yet)
// Requires a UID in order to find or create such an association
public EmailIdentifier.no_message_id(Imap.UID uid) {
base (Db.INVALID_ROWID.to_string());
message_id = Db.INVALID_ROWID;
this.uid = uid;
}
@ -41,12 +37,13 @@ private class Geary.ImapDB.EmailIdentifier : Geary.EmailIdentifier {
"Invalid serialised id type: %s", serialised.get_type_string()
);
}
GLib.Variant inner = serialised.get_child_value(1);
Imap.UID? uid = null;
int64 uid_value = serialised.get_child_value(2).get_int64();
int64 uid_value = inner.get_child_value(1).get_int64();
if (uid_value >= 0) {
uid = new Imap.UID(uid_value);
}
this(serialised.get_child_value(1).get_int64(), uid);
this(inner.get_child_value(0).get_int64(), uid);
}
// Used to promote an id created with no_message_id to one that has a
@ -55,8 +52,6 @@ private class Geary.ImapDB.EmailIdentifier : Geary.EmailIdentifier {
// you not to be able to find them.
public void promote_with_message_id(int64 message_id) {
assert(this.message_id == Db.INVALID_ROWID);
unique = message_id.to_string();
this.message_id = message_id;
}
@ -64,6 +59,19 @@ private class Geary.ImapDB.EmailIdentifier : Geary.EmailIdentifier {
return (uid != null) && uid.is_valid();
}
/** {@inheritDoc} */
public override uint hash() {
return GLib.int64_hash(this.message_id);
}
/** {@inheritDoc} */
public override bool equal_to(Geary.EmailIdentifier other) {
return (
this.get_type() == other.get_type() &&
this.message_id == ((EmailIdentifier) other).message_id
);
}
public override int natural_sort_comparator(Geary.EmailIdentifier o) {
ImapDB.EmailIdentifier? other = o as ImapDB.EmailIdentifier;
if (other == null)
@ -84,13 +92,19 @@ private class Geary.ImapDB.EmailIdentifier : Geary.EmailIdentifier {
int64 uid_value = this.uid != null ? this.uid.value : -1;
return new GLib.Variant.tuple(new Variant[] {
new GLib.Variant.byte('i'),
new GLib.Variant.int64(this.message_id),
new GLib.Variant.int64(uid_value)
new GLib.Variant.tuple(new Variant[] {
new GLib.Variant.int64(this.message_id),
new GLib.Variant.int64(uid_value)
})
});
}
public override string to_string() {
return "[%s/%s]".printf(message_id.to_string(), (uid == null ? "null" : uid.to_string()));
return "%s(%lld,%s)".printf(
this.get_type().name(),
this.message_id,
this.uid != null ? this.uid.to_string() : "null"
);
}
public static Gee.Set<Imap.UID> to_uids(Gee.Collection<ImapDB.EmailIdentifier> ids) {

View file

@ -43,6 +43,63 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
private const string SEARCH_OP_VALUE_UNREAD = "unread";
/**
* Various associated state with a single term in a search query.
*/
internal class Term : GLib.Object {
/**
* The original tokenized search term with minimal other processing performed.
*
* For example, punctuation might be removed, but no casefolding has occurred.
*/
public string original { get; private set; }
/**
* The parsed tokenized search term.
*
* Casefolding and other normalizing text operations have been performed.
*/
public string parsed { get; private set; }
/**
* The stemmed search term.
*
* Only used if stemming is being done ''and'' the stem is different than the {@link parsed}
* term.
*/
public string? stemmed { get; private set; }
/**
* A list of terms ready for binding to an SQLite statement.
*
* This should include prefix operators and quotes (i.e. ["party"] or [party*]). These texts
* are guaranteed not to be null or empty strings.
*/
public Gee.List<string> sql { get; private set; default = new Gee.ArrayList<string>(); }
/**
* Returns true if the {@link parsed} term is exact-match only (i.e. starts with quotes) and
* there is no {@link stemmed} variant.
*/
public bool is_exact { get { return parsed.has_prefix("\"") && stemmed == null; } }
public Term(string original, string parsed, string? stemmed, string? sql_parsed, string? sql_stemmed) {
this.original = original;
this.parsed = parsed;
this.stemmed = stemmed;
// for now, only two variations: the parsed string and the stemmed; since stem is usually
// shorter (and will be first in the OR statement), include it first
if (!String.is_empty(sql_stemmed))
sql.add(sql_stemmed);
if (!String.is_empty(sql_parsed))
sql.add(sql_parsed);
}
}
// Maps of localised search operator names and values to their
// internal forms
private static Gee.HashMap<string, string> search_op_names =
@ -255,18 +312,19 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
// their search term values. Note that terms without an operator
// are stored with null as the key. Not using a MultiMap because
// we (might) need a guarantee of order.
private Gee.HashMap<string?, Gee.ArrayList<SearchTerm>> field_map
= new Gee.HashMap<string?, Gee.ArrayList<SearchTerm>>();
private Gee.HashMap<string?, Gee.ArrayList<Term>> field_map
= new Gee.HashMap<string?, Gee.ArrayList<Term>>();
// A list of all search terms, regardless of search op field name
private Gee.ArrayList<SearchTerm> all = new Gee.ArrayList<SearchTerm>();
private Gee.ArrayList<Term> all = new Gee.ArrayList<Term>();
public async SearchQuery(ImapDB.Account account,
public async SearchQuery(Geary.Account owner,
ImapDB.Account local,
string query,
Geary.SearchQuery.Strategy strategy,
GLib.Cancellable? cancellable) {
base(query, strategy);
this.account = account;
base(owner, query, strategy);
this.account = local;
switch (strategy) {
case Strategy.EXACT:
@ -305,11 +363,11 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
return field_map.keys;
}
public Gee.List<SearchTerm>? get_search_terms(string? field) {
public Gee.List<Term>? get_search_terms(string? field) {
return field_map.has_key(field) ? field_map.get(field) : null;
}
public Gee.List<SearchTerm>? get_all_terms() {
public Gee.List<Term>? get_all_terms() {
return all;
}
@ -329,7 +387,7 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
bool strip_results = true;
if (this.strategy == Geary.SearchQuery.Strategy.HORIZON)
strip_results = false;
else if (traverse<SearchTerm>(this.all).any(
else if (traverse<Term>(this.all).any(
term => term.stemmed == null || term.is_exact)) {
strip_results = false;
}
@ -341,8 +399,8 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
new Gee.HashMap<Geary.NamedFlag,bool>();
foreach (string? field in this.field_map.keys) {
if (field == SEARCH_OP_IS) {
Gee.List<SearchTerm>? terms = get_search_terms(field);
foreach (SearchTerm term in terms)
Gee.List<Term>? terms = get_search_terms(field);
foreach (Term term in terms)
if (term.parsed == SEARCH_OP_VALUE_READ)
conditions.set(new NamedFlag("UNREAD"), true);
else if (term.parsed == SEARCH_OP_VALUE_UNREAD)
@ -358,11 +416,11 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
internal Gee.HashMap<string, string> get_query_phrases() {
Gee.HashMap<string, string> phrases = new Gee.HashMap<string, string>();
foreach (string? field in field_map.keys) {
Gee.List<SearchTerm>? terms = get_search_terms(field);
Gee.List<Term>? terms = get_search_terms(field);
if (terms == null || terms.size == 0 || field == "is")
continue;
// Each SearchTerm is an AND but the SQL text within in are OR ... this allows for
// Each Term is an AND but the SQL text within in are OR ... this allows for
// each user term to be AND but the variants of each term are or. So, if terms are
// [party] and [eventful] and stems are [parti] and [event], the search would be:
//
@ -379,7 +437,7 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
//
// party* OR parti* eventful* OR event*
StringBuilder builder = new StringBuilder();
foreach (SearchTerm term in terms) {
foreach (Term term in terms) {
if (term.sql.size == 0)
continue;
@ -438,12 +496,12 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
--quotes;
}
SearchTerm? term;
Term? term;
if (in_quote) {
// HACK: this helps prevent a syntax error when the user types
// something like from:"somebody". If we ever properly support
// quotes after : we can get rid of this.
term = new SearchTerm(s, s, null, s.replace(":", " "), null);
term = new Term(s, s, null, s.replace(":", " "), null);
} else {
string original = s;
@ -479,7 +537,7 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
if (field == SEARCH_OP_IS) {
// s will have been de-translated
term = new SearchTerm(original, s, null, null, null);
term = new Term(original, s, null, null, null);
} else {
// SQL MATCH syntax for parsed term
string? sql_s = "%s*".printf(s);
@ -505,7 +563,7 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
if (String.contains_any_char(s, SEARCH_TERM_CONTINUATION_CHARS))
s = "\"%s\"".printf(s);
term = new SearchTerm(original, s, stemmed, sql_s, sql_stemmed);
term = new Term(original, s, stemmed, sql_s, sql_stemmed);
}
}
@ -514,7 +572,7 @@ private class Geary.ImapDB.SearchQuery : Geary.SearchQuery {
// Finally, add the term
if (!this.field_map.has_key(field)) {
this.field_map.set(field, new Gee.ArrayList<SearchTerm>());
this.field_map.set(field, new Gee.ArrayList<Term>());
}
this.field_map.get(field).add(term);
this.all.add(term);

View file

@ -1,75 +0,0 @@
/* Copyright 2016 Software Freedom Conservancy Inc.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private class Geary.ImapDB.SearchEmailIdentifier : ImapDB.EmailIdentifier,
Gee.Comparable<SearchEmailIdentifier> {
public DateTime? date_received { get; private set; }
public SearchEmailIdentifier(int64 message_id, DateTime? date_received) {
base(message_id, null);
this.date_received = date_received;
}
public static int compare_descending(SearchEmailIdentifier a, SearchEmailIdentifier b) {
return b.compare_to(a);
}
public static Gee.ArrayList<SearchEmailIdentifier> array_list_from_results(
Gee.Collection<Geary.EmailIdentifier>? results) {
Gee.ArrayList<SearchEmailIdentifier> r = new Gee.ArrayList<SearchEmailIdentifier>();
if (results != null) {
foreach (Geary.EmailIdentifier id in results) {
SearchEmailIdentifier? search_id = id as SearchEmailIdentifier;
assert(search_id != null);
r.add(search_id);
}
}
return r;
}
// Searches for a generic EmailIdentifier in a collection of SearchEmailIdentifiers.
public static SearchEmailIdentifier? collection_get_email_identifier(
Gee.Collection<SearchEmailIdentifier> collection, Geary.EmailIdentifier id) {
foreach (SearchEmailIdentifier search_id in collection) {
if (id.equal_to(search_id))
return search_id;
}
return null;
}
public override int natural_sort_comparator(Geary.EmailIdentifier o) {
ImapDB.SearchEmailIdentifier? other = o as ImapDB.SearchEmailIdentifier;
if (other == null)
return 1;
return compare_to(other);
}
public virtual int compare_to(SearchEmailIdentifier other) {
// if both have date received, compare on that, using stable sort if the same
if (date_received != null && other.date_received != null) {
int compare = date_received.compare(other.date_received);
return (compare != 0) ? compare : stable_sort_comparator(other);
}
// if neither have date received, fall back on stable sort
if (date_received == null && other.date_received == null)
return stable_sort_comparator(other);
// put identifiers with no date ahead of those with
return (date_received == null ? -1 : 1);
}
public override string to_string() {
return "[%s/null/%s]".printf(message_id.to_string(),
(date_received == null ? "null" : date_received.to_string()));
}
}

View file

@ -1,16 +0,0 @@
/* Copyright 2016 Software Freedom Conservancy Inc.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private class Geary.ImapDB.SearchFolderProperties : Geary.FolderProperties {
public SearchFolderProperties(int total, int unread) {
base(total, unread, Trillian.FALSE, Trillian.FALSE, Trillian.TRUE, true, true, false);
}
public void set_total(int total) {
this.email_total = total;
}
}

View file

@ -1,423 +0,0 @@
/* Copyright 2016 Software Freedom Conservancy Inc.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
private class Geary.ImapDB.SearchFolder : Geary.SearchFolder, Geary.FolderSupport.Remove {
/** Max number of emails that can ever be in the folder. */
public const int MAX_RESULT_EMAILS = 1000;
/** The canonical name of the search folder. */
public const string MAGIC_BASENAME = "$GearySearchFolder$";
private const Geary.SpecialFolderType[] EXCLUDE_TYPES = {
Geary.SpecialFolderType.SPAM,
Geary.SpecialFolderType.TRASH,
Geary.SpecialFolderType.DRAFTS,
// Orphan emails (without a folder) are also excluded; see ctor.
};
private Gee.HashSet<Geary.FolderPath?> exclude_folders = new Gee.HashSet<Geary.FolderPath?>();
private Gee.TreeSet<ImapDB.SearchEmailIdentifier> search_results;
private Geary.Nonblocking.Mutex result_mutex = new Geary.Nonblocking.Mutex();
public SearchFolder(Geary.Account account, FolderRoot root) {
base(
account,
new SearchFolderProperties(0, 0),
root.get_child(MAGIC_BASENAME, Trillian.TRUE)
);
account.folders_available_unavailable.connect(on_folders_available_unavailable);
account.folders_special_type.connect(on_folders_special_type);
account.email_locally_complete.connect(on_email_locally_complete);
account.email_removed.connect(on_account_email_removed);
clear_search_results();
// 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);
account.folders_special_type.disconnect(on_folders_special_type);
account.email_locally_complete.disconnect(on_email_locally_complete);
account.email_removed.disconnect(on_account_email_removed);
}
private async void append_new_email_async(Geary.SearchQuery query, Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
int result_mutex_token = yield result_mutex.claim_async();
Error? error = null;
try {
yield do_search_async(query, ids, null, cancellable);
} catch(Error e) {
error = e;
}
result_mutex.release(ref result_mutex_token);
if (error != null)
throw error;
}
private async void handle_removed_email_async(Geary.SearchQuery query, Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable) throws Error {
int result_mutex_token = yield result_mutex.claim_async();
Error? error = null;
try {
Gee.ArrayList<ImapDB.SearchEmailIdentifier> relevant_ids
= Geary.traverse<Geary.EmailIdentifier>(ids)
.map_nonnull<ImapDB.SearchEmailIdentifier>(
id => ImapDB.SearchEmailIdentifier.collection_get_email_identifier(search_results, id))
.to_array_list();
if (relevant_ids.size > 0)
yield do_search_async(query, null, relevant_ids, cancellable);
} catch(Error e) {
error = e;
}
result_mutex.release(ref result_mutex_token);
if (error != null)
throw error;
}
/**
* Clears the search query and results.
*/
public override void clear() {
Gee.Collection<ImapDB.SearchEmailIdentifier> local_results = search_results;
clear_search_results();
notify_email_removed(local_results);
notify_email_count_changed(0, Geary.Folder.CountChangeReason.REMOVED);
if (search_query != null) {
search_query = null;
notify_search_query_changed(null);
}
}
/**
* Sets the keyword string for this search.
*/
public override void search(string query, Geary.SearchQuery.Strategy strategy, Cancellable? cancellable = null) {
set_search_query_async.begin(query, strategy, cancellable, on_set_search_query_complete);
}
private void on_set_search_query_complete(Object? source, AsyncResult result) {
try {
set_search_query_async.end(result);
} catch(Error e) {
debug("Search error: %s", e.message);
}
}
private async void set_search_query_async(string query,
Geary.SearchQuery.Strategy strategy,
Cancellable? cancellable)
throws GLib.Error {
Geary.SearchQuery search_query = yield account.open_search(
query, strategy, cancellable
);
int result_mutex_token = yield result_mutex.claim_async();
Error? error = null;
try {
yield do_search_async(search_query, null, null, cancellable);
} catch(Error e) {
error = e;
}
result_mutex.release(ref result_mutex_token);
this.search_query = search_query;
notify_search_query_changed(search_query);
if (error != null)
throw error;
}
// NOTE: you must call this ONLY after locking result_mutex_token.
// If both *_ids parameters are null, the results of this search are
// considered to be the full new set. If non-null, the results are
// considered to be a delta and are added or subtracted from the full set.
// add_ids are new ids to search for, remove_ids are ids in our result set
// that will be removed if this search doesn't turn them up.
private async void do_search_async(Geary.SearchQuery query, Gee.Collection<Geary.EmailIdentifier>? add_ids,
Gee.Collection<ImapDB.SearchEmailIdentifier>? remove_ids, Cancellable? cancellable) throws Error {
// There are three cases here: 1) replace full result set, where the
// *_ids parameters are both null, 2) add to result set, where just
// remove_ids is null, and 3) remove from result set, where just
// add_ids is null. We can't add and remove at the same time.
assert(add_ids == null || remove_ids == null);
// TODO: don't limit this to MAX_RESULT_EMAILS. Instead, we could be
// smarter about only fetching the search results in list_email_async()
// etc., but this leads to some more complications when redoing the
// search.
Gee.ArrayList<ImapDB.SearchEmailIdentifier> results
= ImapDB.SearchEmailIdentifier.array_list_from_results(yield account.local_search_async(
query, MAX_RESULT_EMAILS, 0, exclude_folders, add_ids ?? remove_ids, cancellable));
Gee.List<ImapDB.SearchEmailIdentifier> added
= Gee.List.empty<ImapDB.SearchEmailIdentifier>();
Gee.List<ImapDB.SearchEmailIdentifier> removed
= Gee.List.empty<ImapDB.SearchEmailIdentifier>();
if (remove_ids == null) {
added = Geary.traverse<ImapDB.SearchEmailIdentifier>(results)
.filter(id => !(id in search_results))
.to_array_list();
}
if (add_ids == null) {
removed = Geary.traverse<ImapDB.SearchEmailIdentifier>(remove_ids ?? search_results)
.filter(id => !(id in results))
.to_array_list();
}
search_results.remove_all(removed);
search_results.add_all(added);
((ImapDB.SearchFolderProperties) properties).set_total(search_results.size);
// Note that we probably shouldn't be firing these signals from inside
// our mutex lock. Keep an eye on it, and if there's ever a case where
// it might cause problems, it shouldn't be too hard to move the
// firings outside.
Geary.Folder.CountChangeReason reason = CountChangeReason.NONE;
if (added.size > 0) {
// TODO: we'd like to be able to use APPENDED here when applicable,
// but because of the potential to append a thousand results at
// once and the ConversationMonitor's inability to handle that
// gracefully (#7464), we always use INSERTED for now.
notify_email_inserted(added);
reason |= Geary.Folder.CountChangeReason.INSERTED;
}
if (removed.size > 0) {
notify_email_removed(removed);
reason |= Geary.Folder.CountChangeReason.REMOVED;
}
if (reason != CountChangeReason.NONE)
notify_email_count_changed(search_results.size, reason);
}
public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier? initial_id,
int count, Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, Cancellable? cancellable = null)
throws Error {
if (count <= 0)
return null;
// TODO: as above, this is incomplete and inefficient.
int result_mutex_token = yield result_mutex.claim_async();
Geary.EmailIdentifier[] ids = new Geary.EmailIdentifier[search_results.size];
int initial_index = 0;
int i = 0;
foreach (ImapDB.SearchEmailIdentifier id in search_results) {
if (initial_id != null && id.equal_to(initial_id))
initial_index = i;
ids[i++] = id;
}
if (initial_id == null && flags.is_all_set(Geary.Folder.ListFlags.OLDEST_TO_NEWEST))
initial_index = ids.length - 1;
Gee.List<Geary.Email> results = new Gee.ArrayList<Geary.Email>();
Error? fetch_err = null;
if (initial_index >= 0) {
int increment = flags.is_oldest_to_newest() ? -1 : 1;
i = initial_index;
if (!flags.is_including_id() && initial_id != null)
i += increment;
int end = i + (count * increment);
for (; i >= 0 && i < search_results.size && i != end; i += increment) {
try {
results.add(yield fetch_email_async(ids[i], required_fields, flags, cancellable));
} catch (Error err) {
// Don't let missing or incomplete messages stop the list operation, which has
// different semantics from fetch
if (!(err is EngineError.NOT_FOUND) && !(err is EngineError.INCOMPLETE_MESSAGE)) {
fetch_err = err;
break;
}
}
}
}
result_mutex.release(ref result_mutex_token);
if (fetch_err != null)
throw fetch_err;
return (results.size == 0 ? null : results);
}
public override async Gee.List<Geary.Email>? list_email_by_sparse_id_async(
Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields,
Geary.Folder.ListFlags flags, Cancellable? cancellable = null) throws Error {
// TODO: Fetch emails in a batch.
Gee.List<Geary.Email> result = new Gee.ArrayList<Geary.Email>();
foreach(Geary.EmailIdentifier id in ids)
result.add(yield fetch_email_async(id, required_fields, flags, cancellable));
return (result.size == 0 ? null : result);
}
public override async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>? list_local_email_fields_async(
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
// TODO: This method is not currently called, but is required by the interface. Before completing
// this feature, it should either be implemented either here or in AbstractLocalFolder.
error("Search folder does not implement list_local_email_fields_async");
}
public override async Geary.Email fetch_email_async(Geary.EmailIdentifier id,
Geary.Email.Field required_fields, Geary.Folder.ListFlags flags,
Cancellable? cancellable = null) throws Error {
return yield account.local_fetch_email_async(id, required_fields, cancellable);
}
public virtual async void
remove_email_async(Gee.Collection<Geary.EmailIdentifier> email_ids,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? ids_to_folders
= yield account.get_containing_folders_async(email_ids, cancellable);
if (ids_to_folders == null)
return;
Gee.MultiMap<Geary.FolderPath, Geary.EmailIdentifier> folders_to_ids
= Geary.Collection.reverse_multi_map<Geary.EmailIdentifier, Geary.FolderPath>(ids_to_folders);
foreach (Geary.FolderPath path in folders_to_ids.get_keys()) {
Geary.Folder folder = account.get_folder(path);
Geary.FolderSupport.Remove? remove = folder as Geary.FolderSupport.Remove;
if (remove == null)
continue;
Gee.Collection<Geary.EmailIdentifier> ids = folders_to_ids.get(path);
assert(ids.size > 0);
debug("Search folder removing %d emails from %s", ids.size, folder.to_string());
bool open = false;
try {
yield folder.open_async(Geary.Folder.OpenFlags.NONE, cancellable);
open = true;
yield remove.remove_email_async(
Collection.copy(ids), cancellable
);
} finally {
if (open) {
try {
yield folder.close_async();
} catch (Error e) {
debug("Error closing folder %s: %s", folder.to_string(), e.message);
}
}
}
}
}
/**
* Given a list of mail IDs, returns a set of casefolded words that match for the current
* search query.
*/
public override async Gee.Set<string>? get_search_matches_async(
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
if (search_query == null)
return null;
return yield account.get_search_matches_async(search_query, ids, cancellable);
}
private void include_folder(Geary.Folder folder) {
this.exclude_folders.remove(folder.path);
}
private void exclude_folder(Geary.Folder folder) {
this.exclude_folders.add(folder.path);
}
private void exclude_orphan_emails() {
this.exclude_folders.add(null);
}
private void clear_search_results() {
search_results = new Gee.TreeSet<ImapDB.SearchEmailIdentifier>(
ImapDB.SearchEmailIdentifier.compare_descending);
}
private void on_folders_available_unavailable(Gee.Collection<Geary.Folder>? available,
Gee.Collection<Geary.Folder>? unavailable) {
if (available != null) {
// Exclude it from searching if it's got the right special type.
foreach(Geary.Folder folder in Geary.traverse<Geary.Folder>(available)
.filter(f => f.special_folder_type in EXCLUDE_TYPES))
exclude_folder(folder);
}
}
private void on_folders_special_type(Gee.Collection<Geary.Folder> folders) {
foreach (Geary.Folder folder in folders) {
if (folder.special_folder_type in EXCLUDE_TYPES) {
exclude_folder(folder);
} else {
include_folder(folder);
}
}
}
private void on_email_locally_complete(Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids) {
if (search_query != null) {
this.append_new_email_async.begin(
search_query, folder, ids, null, on_append_new_email_complete
);
}
}
private void on_append_new_email_complete(GLib.Object? source,
GLib.AsyncResult result) {
try {
this.append_new_email_async.end(result);
} catch (GLib.Error e) {
debug("Error appending new email to search results: %s", e.message);
}
}
private void on_account_email_removed(Geary.Folder folder,
Gee.Collection<Geary.EmailIdentifier> ids) {
if (search_query != null) {
this.handle_removed_email_async.begin(
search_query, folder, ids, null,
on_handle_removed_email_complete
);
}
}
private void on_handle_removed_email_complete(GLib.Object? source,
GLib.AsyncResult result) {
try {
this.handle_removed_email_async.end(result);
} catch (GLib.Error e) {
debug("Error removing removed email from search results: %s",
e.message);
}
}
}

View file

@ -1,62 +0,0 @@
/* Copyright 2016 Software Freedom Conservancy Inc.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Various associated state with a single term in a {@link ImapDB.SearchQuery}.
*/
private class Geary.ImapDB.SearchTerm : BaseObject {
/**
* The original tokenized search term with minimal other processing performed.
*
* For example, punctuation might be removed, but no casefolding has occurred.
*/
public string original { get; private set; }
/**
* The parsed tokenized search term.
*
* Casefolding and other normalizing text operations have been performed.
*/
public string parsed { get; private set; }
/**
* The stemmed search term.
*
* Only used if stemming is being done ''and'' the stem is different than the {@link parsed}
* term.
*/
public string? stemmed { get; private set; }
/**
* A list of terms ready for binding to an SQLite statement.
*
* This should include prefix operators and quotes (i.e. ["party"] or [party*]). These texts
* are guaranteed not to be null or empty strings.
*/
public Gee.List<string> sql { get; private set; default = new Gee.ArrayList<string>(); }
/**
* Returns true if the {@link parsed} term is exact-match only (i.e. starts with quotes) and
* there is no {@link stemmed} variant.
*/
public bool is_exact { get { return parsed.has_prefix("\"") && stemmed == null; } }
public SearchTerm(string original, string parsed, string? stemmed, string? sql_parsed, string? sql_stemmed) {
this.original = original;
this.parsed = parsed;
this.stemmed = stemmed;
// for now, only two variations: the parsed string and the stemmed; since stem is usually
// shorter (and will be first in the OR statement), include it first
if (!String.is_empty(sql_stemmed))
sql.add(sql_stemmed);
if (!String.is_empty(sql_parsed))
sql.add(sql_parsed);
}
}

View file

@ -78,8 +78,4 @@ private class Geary.ImapEngine.GmailAccount : Geary.ImapEngine.GenericAccount {
}
}
protected override SearchFolder new_search_folder() {
return new GmailSearchFolder(this, this.local_folder_root);
}
}

View file

@ -1,46 +0,0 @@
/*
* Copyright 2016 Software Freedom Conservancy Inc.
*
* This software is licensed under the GNU Lesser General Public License
* (version 2.1 or later). See the COPYING file in this distribution.
*/
/**
* Gmail-specific SearchFolder implementation.
*/
private class Geary.ImapEngine.GmailSearchFolder : ImapDB.SearchFolder {
private Geary.App.EmailStore email_store;
public GmailSearchFolder(Geary.Account account, FolderRoot root) {
base (account, root);
this.email_store = new Geary.App.EmailStore(account);
}
public override async void
remove_email_async(Gee.Collection<Geary.EmailIdentifier> email_ids,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
Geary.Folder? trash_folder = null;
try {
trash_folder = yield account.get_required_special_folder_async(
Geary.SpecialFolderType.TRASH, cancellable
);
} catch (Error e) {
debug("Error looking up trash folder in %s: %s",
account.to_string(), e.message);
}
if (trash_folder == null) {
debug("Can't remove email from search folder because no trash folder was found in %s",
account.to_string());
} else {
// Copying to trash from one folder is all that's required in Gmail
// to fully trash the message.
yield this.email_store.copy_email_async(
email_ids, trash_folder.path, cancellable
);
}
}
}

View file

@ -39,25 +39,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
/** Local database for the account. */
public ImapDB.Account local { get; private set; }
/**
* The root path for all local folders.
*
* No folder exists for this path, it merely exists to provide a
* common root for the paths of all local folders.
*/
protected FolderRoot local_folder_root = new Geary.FolderRoot(
"$geary-local", true
);
private bool open = false;
private Cancellable? open_cancellable = null;
private Nonblocking.Semaphore? remote_ready_lock = null;
private Geary.SearchFolder? search_folder { get; private set; default = null; }
private Gee.HashMap<FolderPath, MinimalFolder> folder_map = new Gee.HashMap<
FolderPath, MinimalFolder>();
private Gee.HashMap<FolderPath, Folder> local_only = new Gee.HashMap<FolderPath, Folder>();
private Gee.Map<FolderPath,MinimalFolder> folder_map =
new Gee.HashMap<FolderPath,MinimalFolder>();
private AccountProcessor? processor;
private AccountSynchronizer sync;
@ -107,7 +94,6 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
);
this.background_progress = new ReentrantProgressMonitor(ACTIVITY);
this.search_upgrade_monitor = local.search_index_monitor;
this.db_upgrade_monitor = local.upgrade_monitor;
this.db_vacuum_monitor = local.vacuum_monitor;
@ -149,16 +135,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
throw err;
}
// Create/load local folders
local_only.set(this.smtp.outbox.path, this.smtp.outbox);
this.search_folder = new_search_folder();
local_only.set(this.search_folder.path, this.search_folder);
this.open = true;
notify_opened();
notify_folders_available_unavailable(sort_by_path(local_only.values), null);
this.queue_operation(
new LoadFolders(this, this.local, get_supported_special_folders())
@ -170,21 +148,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
yield this.imap.start(cancellable);
this.queue_operation(new StartPostie(this));
// Kick off a background update of the search table, but since
// the database is getting hammered at startup, wait a bit
// before starting the update ... use the ordinal to stagger
// these being fired off (important for users with many
// accounts registered).
// Kick off a background update of the search table.
//
// This is an example of an operation for which we need an
// engine-wide operation queue, not just an account-wide
// queue.
const int POPULATE_DELAY_SEC = 5;
int account_sec = this.information.ordinal.clamp(0, 10);
Timeout.add_seconds(POPULATE_DELAY_SEC + account_sec, () => {
this.local.populate_search_table.begin(cancellable);
return false;
});
// XXX since this hammers the database, this is an example of
// an operation for which we need an engine-wide operation
// queue, not just an account-wide queue.
this.queue_operation(new PopulateSearchTable(this));
}
public override async void close_async(Cancellable? cancellable = null) throws Error {
@ -212,25 +181,16 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
// Close folders and ensure they do in fact close
Gee.BidirSortedSet<Folder> locals = sort_by_path(this.local_only.values);
Gee.BidirSortedSet<Folder> remotes = sort_by_path(this.folder_map.values);
this.local_only.clear();
this.folder_map.clear();
notify_folders_available_unavailable(null, locals);
notify_folders_available_unavailable(null, remotes);
foreach (Geary.Folder folder in locals) {
debug("Waiting for local to close: %s", folder.to_string());
yield folder.wait_for_close_async();
}
foreach (Geary.Folder folder in remotes) {
debug("Waiting for remote to close: %s", folder.to_string());
yield folder.wait_for_close_async();
}
// Close IMAP service manager
// Close IMAP service manager now that folders are closed
try {
yield this.imap.stop();
@ -241,7 +201,6 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
// Close local infrastructure
this.search_folder = null;
try {
yield local.close_async(cancellable);
} finally {
@ -439,23 +398,18 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
throws EngineError.NOT_FOUND {
Folder? folder = this.folder_map.get(path);
if (folder == null) {
folder = this.local_only.get(path);
if (folder == null) {
throw new EngineError.NOT_FOUND(
"Folder not found: %s", path.to_string()
);
}
throw new EngineError.NOT_FOUND(
"Folder not found: %s", path.to_string()
);
}
return folder;
}
/** {@inheritDoc} */
public override Gee.Collection<Folder> list_folders() {
Gee.HashSet<Folder> all_folders = new Gee.HashSet<Folder>();
all_folders.add_all(this.folder_map.values);
all_folders.add_all(this.local_only.values);
return all_folders;
var all = new Gee.HashSet<Folder>();
all.add_all(this.folder_map.values);
return all;
}
/** {@inheritDoc} */
@ -511,6 +465,16 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
return (Gee.Collection<ImapDB.EmailIdentifier>) ids;
}
/** {@inheritDoc} */
public override async SearchQuery new_search_query(string query,
SearchQuery.Strategy strategy,
GLib.Cancellable? cancellable)
throws GLib.Error {
return yield new ImapDB.SearchQuery(
this, local, query, strategy, cancellable
);
}
public override async Gee.MultiMap<Geary.Email, Geary.FolderPath?>? local_search_message_id_async(
Geary.RFC822.MessageID message_id, Geary.Email.Field requested_fields, bool partial_ok,
Gee.Collection<Geary.FolderPath?>? folder_blacklist, Geary.EmailFlags? flag_blacklist,
@ -524,11 +488,15 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
return yield local.fetch_email_async(check_id(email_id), required_fields, cancellable);
}
public override async Geary.SearchQuery open_search(string query,
SearchQuery.Strategy strategy,
GLib.Cancellable? cancellable)
throws GLib.Error {
return yield new ImapDB.SearchQuery(local, query, strategy, cancellable);
/** {@inheritDoc} */
public override async Gee.List<Email> list_local_email_async(
Gee.Collection<EmailIdentifier> ids,
Email.Field required_fields,
GLib.Cancellable? cancellable = null
) throws GLib.Error {
return yield local.list_email(
check_ids(ids), required_fields, cancellable
);
}
public override async Gee.Collection<Geary.EmailIdentifier>? local_search_async(Geary.SearchQuery query,
@ -786,16 +754,6 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
*/
protected abstract MinimalFolder new_folder(ImapDB.Folder local_folder);
/**
* Constructs a concrete search folder implementation.
*
* Subclasses with specific SearchFolder implementations should
* override this to return the correct subclass.
*/
protected virtual SearchFolder new_search_folder() {
return new ImapDB.SearchFolder(this, this.local_folder_root);
}
/** {@inheritDoc} */
protected override void
notify_folders_available_unavailable(Gee.BidirSortedSet<Folder>? available,
@ -854,7 +812,6 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
UpdateRemoteFolders op = new UpdateRemoteFolders(
this,
this.local_only.keys,
get_supported_special_folders()
);
op.completed.connect(() => {
@ -1121,6 +1078,26 @@ internal class Geary.ImapEngine.StartPostie : AccountOperation {
}
/**
* Account operation for populating the full-text-search table.
*/
internal class Geary.ImapEngine.PopulateSearchTable : AccountOperation {
internal PopulateSearchTable(GenericAccount account) {
base(account);
}
public override async void execute(GLib.Cancellable cancellable)
throws GLib.Error {
yield ((GenericAccount) this.account).local.populate_search_table(
cancellable
);
}
}
/**
* Account operation that updates folders from the remote.
*/
@ -1128,16 +1105,13 @@ internal class Geary.ImapEngine.UpdateRemoteFolders : AccountOperation {
private weak GenericAccount generic_account;
private Gee.Collection<FolderPath> local_folders;
private Geary.SpecialFolderType[] specials;
internal UpdateRemoteFolders(GenericAccount account,
Gee.Collection<FolderPath> local_folders,
Geary.SpecialFolderType[] specials) {
base(account);
this.generic_account = account;
this.local_folders = local_folders;
this.specials = specials;
}
@ -1262,10 +1236,10 @@ internal class Geary.ImapEngine.UpdateRemoteFolders : AccountOperation {
.filter(f => !existing_folders.has_key(f.path))
.to_array_list();
// If path in local but not remote (and isn't local-only, i.e. the Outbox), need to remove it
// Remove if path in local but not remote
Gee.ArrayList<Geary.Folder> to_remove
= Geary.traverse<Gee.Map.Entry<FolderPath,Geary.Folder>>(existing_folders)
.filter(e => !remote_folders.has_key(e.key) && !this.local_folders.contains(e.key))
.filter(e => !remote_folders.has_key(e.key))
.map<Geary.Folder>(e => (Geary.Folder) e.value)
.to_array_list();

View file

@ -1226,15 +1226,6 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
return !op.accumulator.is_empty ? op.accumulator : null;
}
public override async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>? list_local_email_fields_async(
Gee.Collection<Geary.EmailIdentifier> ids, Cancellable? cancellable = null) throws Error {
check_open("list_local_email_fields_async");
check_ids("list_local_email_fields_async", ids);
return yield local_folder.list_email_fields_by_id_async(
(Gee.Collection<Geary.ImapDB.EmailIdentifier>) ids, ImapDB.Folder.ListFlags.NONE, cancellable);
}
public override async Geary.Email fetch_email_async(Geary.EmailIdentifier id,
Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, Cancellable? cancellable = null)
throws Error {
@ -1267,9 +1258,9 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
RemoveEmail remove = new RemoveEmail(
this,
(Gee.List<ImapDB.EmailIdentifier>)
traverse(to_expunge).to_array_list(),
cancellable);
(Gee.Collection<ImapDB.EmailIdentifier>) to_expunge,
cancellable
);
replay_queue.schedule(remove);
yield remove.wait_for_ready_async(cancellable);
@ -1321,8 +1312,8 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
MarkEmail mark = new MarkEmail(
this,
(Gee.List<ImapDB.EmailIdentifier>)
traverse(to_mark).to_array_list(),
(Gee.Collection<ImapDB.EmailIdentifier>)
to_mark,
flags_to_add,
flags_to_remove,
cancellable
@ -1383,10 +1374,8 @@ private class Geary.ImapEngine.MinimalFolder : Geary.Folder, Geary.FolderSupport
return null;
MoveEmailPrepare prepare = new MoveEmailPrepare(
this,
(Gee.List<ImapDB.EmailIdentifier>)
traverse(to_move).to_array_list(),
cancellable);
this, (Gee.Collection<ImapDB.EmailIdentifier>) to_move, cancellable
);
replay_queue.schedule(prepare);
yield prepare.wait_for_ready_async(cancellable);

View file

@ -12,9 +12,11 @@ private class Geary.ImapEngine.MarkEmail : Geary.ImapEngine.SendReplayOperation
private Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags>? original_flags = null;
private Cancellable? cancellable;
public MarkEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_mark,
Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
Cancellable? cancellable = null) {
public MarkEmail(MinimalFolder engine,
Gee.Collection<ImapDB.EmailIdentifier> to_mark,
EmailFlags? flags_to_add,
EmailFlags? flags_to_remove,
GLib.Cancellable? cancellable = null) {
base("MarkEmail", OnError.RETRY);
this.engine = engine;

View file

@ -19,8 +19,9 @@ private class Geary.ImapEngine.MoveEmailPrepare : Geary.ImapEngine.SendReplayOpe
private Cancellable? cancellable;
private Gee.List<ImapDB.EmailIdentifier> to_move = new Gee.ArrayList<ImapDB.EmailIdentifier>();
public MoveEmailPrepare(MinimalFolder engine, Gee.Collection<ImapDB.EmailIdentifier> to_move,
Cancellable? cancellable) {
public MoveEmailPrepare(MinimalFolder engine,
Gee.Collection<ImapDB.EmailIdentifier> to_move,
GLib.Cancellable? cancellable) {
base.only_local("MoveEmailPrepare", OnError.RETRY);
this.engine = engine;

View file

@ -11,8 +11,9 @@ private class Geary.ImapEngine.RemoveEmail : Geary.ImapEngine.SendReplayOperatio
private Gee.Set<ImapDB.EmailIdentifier>? removed_ids = null;
private int original_count = 0;
public RemoveEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_remove,
Cancellable? cancellable = null) {
public RemoveEmail(MinimalFolder engine,
Gee.Collection<ImapDB.EmailIdentifier> to_remove,
GLib.Cancellable? cancellable = null) {
base("RemoveEmail", OnError.RETRY);
this.engine = engine;

View file

@ -37,7 +37,6 @@ geary_engine_vala_sources = files(
'api/geary-problem-report.vala',
'api/geary-progress-monitor.vala',
'api/geary-revokable.vala',
'api/geary-search-folder.vala',
'api/geary-search-query.vala',
'api/geary-service-information.vala',
'api/geary-service-provider.vala',
@ -47,6 +46,7 @@ geary_engine_vala_sources = files(
'app/app-conversation-monitor.vala',
'app/app-draft-manager.vala',
'app/app-email-store.vala',
'app/app-search-folder.vala',
'app/conversation-monitor/app-append-operation.vala',
'app/conversation-monitor/app-conversation-operation-queue.vala',
@ -178,11 +178,7 @@ geary_engine_vala_sources = files(
'imap-db/imap-db-folder.vala',
'imap-db/imap-db-gc.vala',
'imap-db/imap-db-message-row.vala',
'imap-db/search/imap-db-search-email-identifier.vala',
'imap-db/search/imap-db-search-folder.vala',
'imap-db/search/imap-db-search-folder-properties.vala',
'imap-db/search/imap-db-search-query.vala',
'imap-db/search/imap-db-search-term.vala',
'imap-db/imap-db-search-query.vala',
'imap-engine/imap-engine.vala',
'imap-engine/imap-engine-account-operation.vala',
@ -201,7 +197,6 @@ geary_engine_vala_sources = files(
'imap-engine/gmail/imap-engine-gmail-all-mail-folder.vala',
'imap-engine/gmail/imap-engine-gmail-drafts-folder.vala',
'imap-engine/gmail/imap-engine-gmail-folder.vala',
'imap-engine/gmail/imap-engine-gmail-search-folder.vala',
'imap-engine/gmail/imap-engine-gmail-spam-trash-folder.vala',
'imap-engine/other/imap-engine-other-account.vala',
'imap-engine/other/imap-engine-other-folder.vala',

View file

@ -9,14 +9,13 @@
private class Geary.Outbox.EmailIdentifier : Geary.EmailIdentifier {
private const string VARIANT_TYPE = "(yxx)";
private const string VARIANT_TYPE = "(y(xx))";
public int64 message_id { get; private set; }
public int64 ordering { get; private set; }
public EmailIdentifier(int64 message_id, int64 ordering) {
base("Outbox.EmailIdentifier:%s".printf(message_id.to_string()));
this.message_id = message_id;
this.ordering = ordering;
}
@ -28,9 +27,30 @@ private class Geary.Outbox.EmailIdentifier : Geary.EmailIdentifier {
"Invalid serialised id type: %s", serialised.get_type_string()
);
}
GLib.Variant mid = serialised.get_child_value(1);
GLib.Variant uid = serialised.get_child_value(2);
this(mid.get_int64(), uid.get_int64());
GLib.Variant inner = serialised.get_child_value(1);
GLib.Variant mid = inner.get_child_value(0);
GLib.Variant ord = inner.get_child_value(1);
this(mid.get_int64(), ord.get_int64());
}
/** {@inheritDoc} */
public override uint hash() {
return GLib.int64_hash(this.message_id);
}
/** {@inheritDoc} */
public override bool equal_to(Geary.EmailIdentifier other) {
return (
this.get_type() == other.get_type() &&
this.message_id == ((EmailIdentifier) other).message_id
);
}
/** {@inheritDoc} */
public override string to_string() {
return "%s(%lld,%lld)".printf(
this.get_type().name(), this.message_id, this.ordering
);
}
public override int natural_sort_comparator(Geary.EmailIdentifier o) {
@ -46,8 +66,10 @@ private class Geary.Outbox.EmailIdentifier : Geary.EmailIdentifier {
// inform GenericAccount that it's an SMTP id.
return new GLib.Variant.tuple(new Variant[] {
new GLib.Variant.byte('o'),
new GLib.Variant.int64(this.message_id),
new GLib.Variant.int64(this.ordering)
new GLib.Variant.tuple(new Variant[] {
new GLib.Variant.int64(this.message_id),
new GLib.Variant.int64(this.ordering)
})
});
}

View file

@ -325,37 +325,6 @@ public class Geary.Outbox.Folder :
return (list.size > 0) ? list : null;
}
public override async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>?
list_local_email_fields_async(Gee.Collection<Geary.EmailIdentifier> ids,
GLib.Cancellable? cancellable = null)
throws GLib.Error {
check_open();
Gee.Map<Geary.EmailIdentifier, Geary.Email.Field> map = new Gee.HashMap<
Geary.EmailIdentifier, Geary.Email.Field>();
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
Db.Statement stmt = cx.prepare(
"SELECT id FROM SmtpOutboxTable WHERE ordering=?");
foreach (Geary.EmailIdentifier id in ids) {
EmailIdentifier? outbox_id = id as EmailIdentifier;
if (outbox_id == null)
throw new EngineError.BAD_PARAMETERS("%s is not outbox EmailIdentifier", id.to_string());
stmt.reset(Db.ResetScope.CLEAR_BINDINGS);
stmt.bind_int64(0, outbox_id.ordering);
// merely checking for presence, all emails in outbox have same fields
Db.Result results = stmt.exec(cancellable);
if (!results.finished)
map.set(outbox_id, Geary.Email.Field.ALL);
}
return Db.TransactionOutcome.DONE;
}, cancellable);
return (map.size > 0) ? map : null;
}
public override async Email
fetch_email_async(Geary.EmailIdentifier id,
Geary.Email.Field required_fields,

View file

@ -10,8 +10,9 @@ public class Geary.MockAccount : Account, MockObject {
public class MockSearchQuery : SearchQuery {
internal MockSearchQuery() {
base("", SearchQuery.Strategy.EXACT);
internal MockSearchQuery(Account owner,
string raw) {
base(owner, raw, SearchQuery.Strategy.EXACT);
}
}
@ -203,6 +204,18 @@ public class Geary.MockAccount : Account, MockObject {
);
}
public override async Gee.List<Email> list_local_email_async(
Gee.Collection<EmailIdentifier> ids,
Email.Field required_fields,
GLib.Cancellable? cancellable = null
) throws GLib.Error {
return object_or_throw_call<Gee.List<Email>>(
"list_local_email_async",
{ids, box_arg(required_fields), cancellable},
new EngineError.NOT_FOUND("Mock call")
);
}
public override async Email local_fetch_email_async(EmailIdentifier email_id,
Email.Field required_fields,
Cancellable? cancellable = null)
@ -214,11 +227,11 @@ public class Geary.MockAccount : Account, MockObject {
);
}
public override async SearchQuery open_search(string query,
SearchQuery.Strategy strategy,
GLib.Cancellable? cancellable)
public override async SearchQuery new_search_query(string raw,
SearchQuery.Strategy strategy,
GLib.Cancellable? cancellable)
throws GLib.Error {
return new MockSearchQuery();
return new MockSearchQuery(this, raw);
}
public override async Gee.Collection<EmailIdentifier>?

View file

@ -12,17 +12,35 @@ public class Geary.MockEmailIdentifer : EmailIdentifier {
public MockEmailIdentifer(int id) {
base(id.to_string());
this.id = id;
}
public override uint hash() {
return GLib.int_hash(this.id);
}
public override bool equal_to(Geary.EmailIdentifier other) {
return (
this.get_type() == other.get_type() &&
this.id == ((MockEmailIdentifer) other).id
);
}
public override string to_string() {
return "%s(%d)".printf(
this.get_type().name(),
this.id
);
}
public override GLib.Variant to_variant() {
return new GLib.Variant.int32(id);
}
public override int natural_sort_comparator(Geary.EmailIdentifier other) {
MockEmailIdentifer? other_mock = other as MockEmailIdentifer;
return (other_mock == null) ? 1 : this.id - other_mock.id;
}
public override GLib.Variant to_variant() {
return new GLib.Variant.int32(id);
}
}

View file

@ -110,13 +110,6 @@ public class Geary.MockFolder : Folder, MockObject {
);
}
public override async Gee.Map<Geary.EmailIdentifier, Geary.Email.Field>?
list_local_email_fields_async(Gee.Collection<Geary.EmailIdentifier> ids,
Cancellable? cancellable = null)
throws Error {
throw new EngineError.UNSUPPORTED("Mock method");
}
public override async Geary.Email
fetch_email_async(Geary.EmailIdentifier email_id,
Geary.Email.Field required_fields,

View file

@ -26,6 +26,7 @@ class Geary.ImapDB.AccountTest : TestCase {
add_test("fetch_base_folder", fetch_base_folder);
add_test("fetch_child_folder", fetch_child_folder);
add_test("fetch_nonexistent_folder", fetch_nonexistent_folder);
add_test("list_local_email", list_local_email);
}
public override void set_up() throws GLib.Error {
@ -310,4 +311,60 @@ class Geary.ImapDB.AccountTest : TestCase {
}
}
public void list_local_email() throws GLib.Error {
Email.Field fixture_fields = Email.Field.RECEIVERS;
string fixture_to = "test1@example.com";
this.account.db.exec(
"INSERT INTO MessageTable (id, fields, to_field) " +
"VALUES (1, %d, '%s');".printf(fixture_fields, fixture_to)
);
this.account.db.exec(
"INSERT INTO MessageTable (id, fields, to_field) " +
"VALUES (2, %d, '%s');".printf(fixture_fields, fixture_to)
);
this.account.list_email.begin(
iterate_array<Geary.ImapDB.EmailIdentifier>({
new EmailIdentifier(1, null),
new EmailIdentifier(2, null)
}).to_linked_list(),
Email.Field.RECEIVERS,
null,
(obj, ret) => { async_complete(ret); }
);
Gee.List<Email> result = this.account.list_email.end(
async_result()
);
assert_int(2, result.size, "Not enough email listed");
assert_true(new EmailIdentifier(1, null).equal_to(result[0].id));
assert_true(new EmailIdentifier(2, null).equal_to(result[1].id));
this.account.list_email.begin(
Collection.single(new EmailIdentifier(3, null)),
Email.Field.RECEIVERS,
null,
(obj, ret) => { async_complete(ret); }
);
try {
this.account.list_email.end(async_result());
assert_not_reached();
} catch (EngineError.NOT_FOUND error) {
// All good
}
this.account.list_email.begin(
Collection.single(new EmailIdentifier(1, null)),
Email.Field.BODY,
null,
(obj, ret) => { async_complete(ret); }
);
try {
this.account.list_email.end(async_result());
assert_not_reached();
} catch (EngineError.INCOMPLETE_MESSAGE error) {
// All good
}
}
}

View file

@ -86,10 +86,18 @@ public class Geary.ImapEngine.GenericAccountTest : TestCase {
);
assert_non_null(
test_article.to_email_identifier(new GLib.Variant("(yxx)", 'i', 1, 2))
test_article.to_email_identifier(
new GLib.Variant(
"(yr)", 'i', new GLib.Variant("(xx)", 1, 2)
)
)
);
assert_non_null(
test_article.to_email_identifier(new GLib.Variant("(yxx)", 'o', 1, 2))
test_article.to_email_identifier(
new GLib.Variant(
"(yr)", 'o', new GLib.Variant("(xx)", 1, 2)
)
)
);
}