Merge branch 'mainline' into letorbi/gmime-3
This commit is contained in:
commit
77d44d41a3
41 changed files with 1260 additions and 1193 deletions
|
|
@ -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
|
||||
|
|
|
|||
8
po/ca.po
8
po/ca.po
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
92
src/client/components/components-search-bar.vala
Normal file
92
src/client/components/components-search-bar.vala
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
632
src/engine/app/app-search-folder.vala
Normal file
632
src/engine/app/app-search-folder.vala
Normal 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
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>?
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue