[geary] Better special folder detection/creation
- From: Charles Lindsay <clindsay src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [geary] Better special folder detection/creation
- Date: Tue, 11 Feb 2014 23:29:38 +0000 (UTC)
commit 4552757994309ab975945b4d50c59963582022d4
Author: Charles Lindsay <chaz yorba org>
Date: Tue Feb 11 15:24:01 2014 -0800
Better special folder detection/creation
This looks for some translatable common names for special folders like
Sent Mail, Drafts, Spam and Trash, instead of only relying on the
server's special-use or xlist extensions. If the server doesn't report
special-use/xlist, we look for common folder names, creating them on the
server if necessary, so we always have folders necessary for tasks like
saving drafts or sent mail.
Closes: bgo #713492
po/POTFILES.in | 7 +-
src/CMakeLists.txt | 7 +-
src/client/application/geary-controller.vala | 64 +-
src/client/composer/composer-window.vala | 25 +-
src/engine/abstract/geary-abstract-account.vala | 3 +
src/engine/api/geary-account-information.vala | 108 ++
src/engine/api/geary-account.vala | 9 +
src/engine/api/geary-folder-path.vala | 56 +-
src/engine/imap-db/outbox/smtp-outbox-folder.vala | 6 +-
.../gmail/imap-engine-gmail-account.vala | 16 +-
.../gmail/imap-engine-gmail-folder.vala | 2 +-
.../gmail/imap-engine-gmail-search-folder.vala | 2 +-
.../imap-engine/imap-engine-email-prefetcher.vala | 4 +-
.../imap-engine/imap-engine-generic-account.vala | 172 +++-
.../imap-engine-generic-all-mail-folder.vala | 18 -
.../imap-engine-generic-drafts-folder.vala | 28 -
.../imap-engine/imap-engine-generic-folder.vala | 1287 +-------------------
.../imap-engine-generic-sent-mail-folder.vala | 22 -
.../imap-engine-generic-trash-folder.vala | 23 -
.../imap-engine/imap-engine-minimal-folder.vala | 1295 ++++++++++++++++++++
.../imap-engine/imap-engine-replay-queue.vala | 4 +-
.../other/imap-engine-other-account.vala | 19 +-
.../other/imap-engine-other-folder.vala | 7 +-
.../outlook/imap-engine-outlook-account.vala | 23 +-
.../outlook/imap-engine-outlook-drafts-folder.vala | 19 +
.../outlook/imap-engine-outlook-folder.vala | 7 +-
.../imap-engine-abstract-list-email.vala | 8 +-
.../replay-ops/imap-engine-copy-email.vala | 4 +-
.../replay-ops/imap-engine-create-email.vala | 4 +-
.../replay-ops/imap-engine-fetch-email.vala | 4 +-
.../replay-ops/imap-engine-list-email-by-id.vala | 2 +-
.../imap-engine-list-email-by-sparse-id.vala | 2 +-
.../replay-ops/imap-engine-mark-email.vala | 4 +-
.../replay-ops/imap-engine-move-email.vala | 4 +-
.../replay-ops/imap-engine-remove-email.vala | 4 +-
.../replay-ops/imap-engine-replay-append.vala | 4 +-
.../replay-ops/imap-engine-replay-disconnect.vala | 4 +-
.../replay-ops/imap-engine-replay-removal.vala | 4 +-
.../imap-engine-server-search-email.vala | 2 +-
.../yahoo/imap-engine-yahoo-account.vala | 21 +-
.../yahoo/imap-engine-yahoo-folder.vala | 7 +-
src/engine/imap/api/imap-account.vala | 12 +
src/engine/imap/command/imap-create-command.vala | 23 +
src/engine/util/util-iterable.vala | 7 +
44 files changed, 1765 insertions(+), 1588 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index a4ce70c..361fce6 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -154,6 +154,7 @@ src/engine/imap/command/imap-close-command.vala
src/engine/imap/command/imap-command.vala
src/engine/imap/command/imap-compress-command.vala
src/engine/imap/command/imap-copy-command.vala
+src/engine/imap/command/imap-create-command.vala
src/engine/imap/command/imap-examine-command.vala
src/engine/imap/command/imap-expunge-command.vala
src/engine/imap/command/imap-fetch-command.vala
@@ -195,11 +196,8 @@ src/engine/imap-engine/imap-engine-contact-store.vala
src/engine/imap-engine/imap-engine-email-flag-watcher.vala
src/engine/imap-engine/imap-engine-email-prefetcher.vala
src/engine/imap-engine/imap-engine-generic-account.vala
-src/engine/imap-engine/imap-engine-generic-all-mail-folder.vala
-src/engine/imap-engine/imap-engine-generic-drafts-folder.vala
src/engine/imap-engine/imap-engine-generic-folder.vala
-src/engine/imap-engine/imap-engine-generic-sent-mail-folder.vala
-src/engine/imap-engine/imap-engine-generic-trash-folder.vala
+src/engine/imap-engine/imap-engine-minimal-folder.vala
src/engine/imap-engine/imap-engine-replay-operation.vala
src/engine/imap-engine/imap-engine-replay-queue.vala
src/engine/imap-engine/imap-engine-send-replay-operation.vala
@@ -208,6 +206,7 @@ src/engine/imap-engine/other/imap-engine-other-account.vala
src/engine/imap-engine/other/imap-engine-other-folder.vala
src/engine/imap-engine/outlook/imap-engine-outlook-account.vala
src/engine/imap-engine/outlook/imap-engine-outlook-folder.vala
+src/engine/imap-engine/outlook/imap-engine-outlook-drafts-folder.vala
src/engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala
src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
src/engine/imap-engine/replay-ops/imap-engine-create-email.vala
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 3eed7a6..b992514 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -94,6 +94,7 @@ engine/imap/command/imap-close-command.vala
engine/imap/command/imap-command.vala
engine/imap/command/imap-compress-command.vala
engine/imap/command/imap-copy-command.vala
+engine/imap/command/imap-create-command.vala
engine/imap/command/imap-examine-command.vala
engine/imap/command/imap-expunge-command.vala
engine/imap/command/imap-fetch-command.vala
@@ -182,11 +183,8 @@ engine/imap-engine/imap-engine-contact-store.vala
engine/imap-engine/imap-engine-email-flag-watcher.vala
engine/imap-engine/imap-engine-email-prefetcher.vala
engine/imap-engine/imap-engine-generic-account.vala
-engine/imap-engine/imap-engine-generic-all-mail-folder.vala
-engine/imap-engine/imap-engine-generic-drafts-folder.vala
engine/imap-engine/imap-engine-generic-folder.vala
-engine/imap-engine/imap-engine-generic-sent-mail-folder.vala
-engine/imap-engine/imap-engine-generic-trash-folder.vala
+engine/imap-engine/imap-engine-minimal-folder.vala
engine/imap-engine/imap-engine-replay-operation.vala
engine/imap-engine/imap-engine-replay-queue.vala
engine/imap-engine/imap-engine-send-replay-operation.vala
@@ -197,6 +195,7 @@ engine/imap-engine/other/imap-engine-other-account.vala
engine/imap-engine/other/imap-engine-other-folder.vala
engine/imap-engine/outlook/imap-engine-outlook-account.vala
engine/imap-engine/outlook/imap-engine-outlook-folder.vala
+engine/imap-engine/outlook/imap-engine-outlook-drafts-folder.vala
engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala
engine/imap-engine/replay-ops/imap-engine-copy-email.vala
engine/imap-engine/replay-ops/imap-engine-create-email.vala
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index fb09d59..a198bcf 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -1426,15 +1426,7 @@ public class GearyController : Geary.BaseObject {
actions.get_action(ACTION_MARK_AS_STARRED).set_visible(unstarred_selected);
actions.get_action(ACTION_MARK_AS_UNSTARRED).set_visible(starred_selected);
- Geary.Folder? spam_folder = null;
- try {
- spam_folder = current_account.get_special_folder(Geary.SpecialFolderType.SPAM);
- } catch (Error e) {
- debug("Could not locate special spam folder: %s", e.message);
- }
-
- if (spam_folder != null &&
- current_folder.special_folder_type != Geary.SpecialFolderType.DRAFTS &&
+ if (current_folder.special_folder_type != Geary.SpecialFolderType.DRAFTS &&
current_folder.special_folder_type != Geary.SpecialFolderType.OUTBOX) {
if (current_folder.special_folder_type == Geary.SpecialFolderType.SPAM) {
// We're in the spam folder.
@@ -1446,7 +1438,7 @@ public class GearyController : Geary.BaseObject {
actions.get_action(ACTION_MARK_AS_SPAM).label = MARK_AS_SPAM_LABEL;
}
} else {
- // No Spam folder, or we're in Drafts/Outbox, so gray-out the option.
+ // We're in Drafts/Outbox, so gray-out the option.
actions.get_action(ACTION_MARK_AS_SPAM).sensitive = false;
actions.get_action(ACTION_MARK_AS_SPAM).label = MARK_AS_SPAM_LABEL;
}
@@ -1531,12 +1523,13 @@ public class GearyController : Geary.BaseObject {
mark_email(get_selected_email_ids(false), null, flags);
}
- private void on_mark_as_spam() {
+ private async void mark_as_spam_async(Cancellable? cancellable) {
Geary.Folder? destination_folder = null;
if (current_folder.special_folder_type != Geary.SpecialFolderType.SPAM) {
// Move to spam folder.
try {
- destination_folder = current_account.get_special_folder(Geary.SpecialFolderType.SPAM);
+ destination_folder = yield current_account.get_required_special_folder_async(
+ Geary.SpecialFolderType.SPAM, cancellable);
} catch (Error e) {
debug("Error getting spam folder: %s", e.message);
}
@@ -1553,6 +1546,10 @@ public class GearyController : Geary.BaseObject {
on_move_conversation(destination_folder);
}
+ private void on_mark_as_spam() {
+ mark_as_spam_async.begin(null);
+ }
+
private void copy_email(Gee.Collection<Geary.EmailIdentifier> ids,
Geary.FolderPath destination) {
if (ids.size > 0) {
@@ -1902,26 +1899,10 @@ public class GearyController : Geary.BaseObject {
on_archive_or_delete_selection_finished);
}
- private bool current_folder_supports_trash(out Geary.FolderSupport.Move? move = null,
- out Geary.FolderPath? trash_path = null) {
- try {
- if (current_folder != null && current_folder.special_folder_type != Geary.SpecialFolderType.TRASH
- && !current_folder.properties.is_local_only && current_account != null) {
- Geary.FolderSupport.Move? supports_move = current_folder as Geary.FolderSupport.Move;
- Geary.Folder? trash_folder =
current_account.get_special_folder(Geary.SpecialFolderType.TRASH);
- if (supports_move != null && trash_folder != null) {
- move = supports_move;
- trash_path = trash_folder.path;
- return true;
- }
- }
- } catch (Error e) {
- debug("Error finding trash folder: %s", e.message);
- }
-
- move = null;
- trash_path = null;
- return false;
+ private bool current_folder_supports_trash() {
+ return (current_folder != null && current_folder.special_folder_type != Geary.SpecialFolderType.TRASH
+ && !current_folder.properties.is_local_only && current_account != null
+ && (current_folder as Geary.FolderSupport.Move) != null);
}
public bool confirm_delete(int num_messages) {
@@ -1963,13 +1944,18 @@ public class GearyController : Geary.BaseObject {
if (trash) {
debug("Trashing selected messages");
- Geary.FolderPath? trash_path;
- Geary.FolderSupport.Move? supports_move;
- if (!current_folder_supports_trash(out supports_move, out trash_path))
- debug("Folder %s doesn't support move or account %s doesn't have a trash folder",
- current_folder.to_string(), current_account.to_string());
- else
- yield supports_move.move_email_async(ids, trash_path, cancellable);
+ if (current_folder_supports_trash()) {
+ Geary.FolderPath trash_path = (yield current_account.get_required_special_folder_async(
+ Geary.SpecialFolderType.TRASH, cancellable)).path;
+ Geary.FolderSupport.Move? supports_move = current_folder as Geary.FolderSupport.Move;
+ if (supports_move != null) {
+ yield supports_move.move_email_async(ids, trash_path, cancellable);
+ return;
+ }
+ }
+
+ debug("Folder %s doesn't support move or account %s doesn't have a trash folder",
+ current_folder.to_string(), current_account.to_string());
return;
}
diff --git a/src/client/composer/composer-window.vala b/src/client/composer/composer-window.vala
index 85937f2..880da63 100644
--- a/src/client/composer/composer-window.vala
+++ b/src/client/composer/composer-window.vala
@@ -343,13 +343,8 @@ public class ComposerWindow : Gtk.Window {
debug("Error getting message body: %s", error.message);
}
- try {
- Geary.Folder? draft_folder =
account.get_special_folder(Geary.SpecialFolderType.DRAFTS);
- if (draft_folder != null && is_referred_draft)
- draft_id = referred.id;
- } catch (Error e) {
- debug("Error looking up special folder: %s", e.message);
- }
+ if (is_referred_draft)
+ draft_id = referred.id;
add_attachments(referred.attachments);
break;
@@ -461,7 +456,7 @@ public class ComposerWindow : Gtk.Window {
// If there's only one account, open the drafts folder. If there's more than one account,
// the drafts folder will be opened by on_from_changed().
if (!from_multiple.visible)
- open_drafts_folder.begin(cancellable_drafts);
+ open_drafts_folder_async.begin(cancellable_drafts);
}
public ComposerWindow.from_mailto(Geary.Account account, string mailto) {
@@ -801,11 +796,11 @@ public class ComposerWindow : Gtk.Window {
}
// Returns the drafts folder for the current From account.
- private async void open_drafts_folder(Cancellable cancellable) throws Error {
- yield close_drafts_folder(cancellable);
+ private async void open_drafts_folder_async(Cancellable cancellable) throws Error {
+ yield close_drafts_folder_async(cancellable);
- Geary.FolderSupport.Create? folder = account.get_special_folder(Geary.SpecialFolderType.DRAFTS)
- as Geary.FolderSupport.Create;
+ Geary.FolderSupport.Create? folder = (yield account.get_required_special_folder_async(
+ Geary.SpecialFolderType.DRAFTS, cancellable)) as Geary.FolderSupport.Create;
if (folder == null)
return; // No drafts folder.
@@ -815,7 +810,7 @@ public class ComposerWindow : Gtk.Window {
drafts_folder = folder;
}
- private async void close_drafts_folder(Cancellable? cancellable = null) throws Error {
+ private async void close_drafts_folder_async(Cancellable? cancellable = null) throws Error {
if (drafts_folder == null)
return;
@@ -1683,7 +1678,7 @@ public class ComposerWindow : Gtk.Window {
from = new_account_info.get_from().to_rfc822_string();
set_entry_completions();
- open_drafts_folder.begin(cancellable_drafts);
+ open_drafts_folder_async.begin(cancellable_drafts);
}
} catch (Error e) {
debug("Error updating account in Composer: %s", e.message);
@@ -1705,7 +1700,7 @@ public class ComposerWindow : Gtk.Window {
}
public override void destroy() {
- close_drafts_folder.begin();
+ close_drafts_folder_async.begin();
}
}
diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala
index 29db690..7c194f8 100644
--- a/src/engine/abstract/geary-abstract-account.vala
+++ b/src/engine/abstract/geary-abstract-account.vala
@@ -104,6 +104,9 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account {
.first_matching(f => f.special_folder_type == special);
}
+ public abstract async Geary.Folder get_required_special_folder_async(Geary.SpecialFolderType special,
+ Cancellable? cancellable) throws Error;
+
public abstract async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable =
null)
throws Error;
diff --git a/src/engine/api/geary-account-information.vala b/src/engine/api/geary-account-information.vala
index 297799f..6955ed1 100644
--- a/src/engine/api/geary-account-information.vala
+++ b/src/engine/api/geary-account-information.vala
@@ -27,6 +27,10 @@ public class Geary.AccountInformation : BaseObject {
private const string SMTP_STARTTLS = "smtp_starttls";
private const string SMTP_NOAUTH = "smtp_noauth";
private const string SAVE_SENT_MAIL_KEY = "save_sent_mail";
+ private const string DRAFTS_FOLDER_KEY = "drafts_folder";
+ private const string SENT_MAIL_FOLDER_KEY = "sent_mail_folder";
+ private const string SPAM_FOLDER_KEY = "spam_folder";
+ private const string TRASH_FOLDER_KEY = "trash_folder";
//
// "Retired" keys
@@ -80,6 +84,11 @@ public class Geary.AccountInformation : BaseObject {
public bool default_smtp_server_ssl { get; set; }
public bool default_smtp_server_starttls { get; set; }
public bool default_smtp_server_noauth { get; set; }
+
+ public Geary.FolderPath? drafts_folder_path { get; set; default = null; }
+ public Geary.FolderPath? sent_mail_folder_path { get; set; default = null; }
+ public Geary.FolderPath? spam_folder_path { get; set; default = null; }
+ public Geary.FolderPath? trash_folder_path { get; set; default = null; }
public Geary.Credentials imap_credentials { get; set; default = new Geary.Credentials(null, null); }
public bool imap_remember_password { get; set; default = true; }
@@ -142,6 +151,15 @@ public class Geary.AccountInformation : BaseObject {
smtp_credentials = null;
}
}
+
+ drafts_folder_path = build_folder_path(get_string_list_value(
+ key_file, GROUP, DRAFTS_FOLDER_KEY));
+ sent_mail_folder_path = build_folder_path(get_string_list_value(
+ key_file, GROUP, SENT_MAIL_FOLDER_KEY));
+ spam_folder_path = build_folder_path(get_string_list_value(
+ key_file, GROUP, SPAM_FOLDER_KEY));
+ trash_folder_path = build_folder_path(get_string_list_value(
+ key_file, GROUP, TRASH_FOLDER_KEY));
}
}
@@ -167,6 +185,10 @@ public class Geary.AccountInformation : BaseObject {
imap_remember_password = from.imap_remember_password;
smtp_credentials = from.smtp_credentials;
smtp_remember_password = from.smtp_remember_password;
+ drafts_folder_path = from.drafts_folder_path;
+ sent_mail_folder_path = from.sent_mail_folder_path;
+ spam_folder_path = from.spam_folder_path;
+ trash_folder_path = from.trash_folder_path;
}
/**
@@ -181,6 +203,61 @@ public class Geary.AccountInformation : BaseObject {
}
/**
+ * Gets the path used when Geary has found or created a special folder for
+ * this account. This will be null if Geary has always been told about the
+ * special folders by the server, and hasn't had to go looking for them.
+ * Only the DRAFTS, SENT, SPAM, and TRASH special folder types are valid to
+ * pass to this function.
+ */
+ public Geary.FolderPath? get_special_folder_path(Geary.SpecialFolderType special) {
+ switch (special) {
+ case Geary.SpecialFolderType.DRAFTS:
+ return drafts_folder_path;
+
+ case Geary.SpecialFolderType.SENT:
+ return sent_mail_folder_path;
+
+ case Geary.SpecialFolderType.SPAM:
+ return spam_folder_path;
+
+ case Geary.SpecialFolderType.TRASH:
+ return trash_folder_path;
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ /**
+ * Sets the path Geary will look for or create a special folder. This is
+ * only obeyed if the server doesn't tell Geary which folders are special.
+ * Only the DRAFTS, SENT, SPAM, and TRASH special folder types are valid to
+ * pass to this function.
+ */
+ public void set_special_folder_path(Geary.SpecialFolderType special, Geary.FolderPath? path) {
+ switch (special) {
+ case Geary.SpecialFolderType.DRAFTS:
+ drafts_folder_path = path;
+ break;
+
+ case Geary.SpecialFolderType.SENT:
+ sent_mail_folder_path = path;
+ break;
+
+ case Geary.SpecialFolderType.SPAM:
+ spam_folder_path = path;
+ break;
+
+ case Geary.SpecialFolderType.TRASH:
+ trash_folder_path = path;
+ break;
+
+ default:
+ assert_not_reached();
+ }
+ }
+
+ /**
* Fetch the passwords for the given services. For each service, if the
* password is unset, use get_passwords_async() first; if the password is
* set or it's not in the key store, use prompt_passwords_async(). Return
@@ -406,6 +483,16 @@ public class Geary.AccountInformation : BaseObject {
}
}
+ private Geary.FolderPath? build_folder_path(Gee.List<string>? parts) {
+ if (parts == null || parts.size == 0)
+ return null;
+
+ Geary.FolderPath path = new Imap.FolderRoot(parts[0], null);
+ for (int i = 1; i < parts.size; i++)
+ path = path.get_child(parts.get(i));
+ return path;
+ }
+
private string get_string_value(KeyFile key_file, string group, string key, string def = "") {
try {
return key_file.get_value(group, key);
@@ -416,6 +503,18 @@ public class Geary.AccountInformation : BaseObject {
return def;
}
+ private Gee.List<string> get_string_list_value(KeyFile key_file, string group, string key) {
+ try {
+ string[] list = key_file.get_string_list(group, key);
+ if (list.length > 0)
+ return new Gee.ArrayList<string>.wrap(list);
+ } catch(KeyFileError err) {
+ // Ignore.
+ }
+
+ return new Gee.ArrayList<string>();
+ }
+
private bool get_bool_value(KeyFile key_file, string group, string key, bool def = false) {
try {
return key_file.get_boolean(group, key);
@@ -490,6 +589,15 @@ public class Geary.AccountInformation : BaseObject {
key_file.set_boolean(GROUP, SMTP_NOAUTH, default_smtp_server_noauth);
}
+ key_file.set_string_list(GROUP, DRAFTS_FOLDER_KEY, (drafts_folder_path != null
+ ? drafts_folder_path.as_list().to_array() : new string[] {}));
+ key_file.set_string_list(GROUP, SENT_MAIL_FOLDER_KEY, (sent_mail_folder_path != null
+ ? sent_mail_folder_path.as_list().to_array() : new string[] {}));
+ key_file.set_string_list(GROUP, SPAM_FOLDER_KEY, (spam_folder_path != null
+ ? spam_folder_path.as_list().to_array() : new string[] {}));
+ key_file.set_string_list(GROUP, TRASH_FOLDER_KEY, (trash_folder_path != null
+ ? trash_folder_path.as_list().to_array() : new string[] {}));
+
string data = key_file.to_data();
string new_etag;
diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala
index 989039b..a144566 100644
--- a/src/engine/api/geary-account.vala
+++ b/src/engine/api/geary-account.vala
@@ -280,6 +280,15 @@ public interface Geary.Account : BaseObject {
public abstract Geary.Folder? get_special_folder(Geary.SpecialFolderType special) throws Error;
/**
+ * Returns the Folder object with the given special folder type. The folder will be
+ * created on the server if it doesn't already exist. An error will be thrown if the
+ * folder doesn't exist and can't be created. The only valid special folder types that
+ * can be required are: DRAFTS, SENT, SPAM, and TRASH.
+ */
+ public abstract async Geary.Folder get_required_special_folder_async(Geary.SpecialFolderType special,
+ Cancellable? cancellable = null) throws Error;
+
+ /**
* Submits a ComposedEmail for delivery. Messages may be scheduled for later delivery or immediately
* sent. Subscribe to the "email-sent" signal to be notified of delivery. Note that that signal
* does not return the ComposedEmail object but an RFC822-formatted object. Allowing for the
diff --git a/src/engine/api/geary-folder-path.vala b/src/engine/api/geary-folder-path.vala
index 41f02a2..886f589 100644
--- a/src/engine/api/geary-folder-path.vala
+++ b/src/engine/api/geary-folder-path.vala
@@ -192,8 +192,7 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
public string? get_fullpath(string? use_separator) {
string? separator = use_separator ?? get_root().default_separator;
- // no separator, no fullpath
- if (separator == null)
+ if (separator == null && !is_root())
return null;
// use cached copy if the stars align
@@ -221,22 +220,7 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
return case_sensitive ? str_hash(basename) : str_hash(basename.down());
}
- /**
- * { inheritDoc}
- *
- * Comparisons for Geary.FolderPath is defined as (a) empty paths are less-than non-empty paths
- * and (b) each element is compared to the corresponding path element of the other FolderPath
- * following collation rules for casefolded (case-insensitive) compared, and (c) shorter paths
- * are less-than longer paths, assuming the path elements are equal up to the shorter path's
- * length.
- *
- * Note that the { link FolderRoot.default_separator} has no bearing on comparisons, although
- * { link FolderPath.case_sensitive} does.
- *
- * Returns -1 if this path is lexiographically before the other, 1 if its after, and 0 if they
- * are equal.
- */
- public int compare_to(Geary.FolderPath other) {
+ private int compare_internal(Geary.FolderPath other, bool allow_case_sensitive, bool normalize) {
if (this == other)
return 0;
@@ -251,8 +235,13 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
string this_element = this_list[ctr];
string other_element = other_list[ctr];
- // if either case-sensitive, then comparison is CS
- if (!get_folder_at(ctr).case_sensitive && !other.get_folder_at(ctr).case_sensitive) {
+ if (normalize) {
+ this_element = this_element.normalize();
+ other_element = other_element.normalize();
+ }
+ if (!allow_case_sensitive
+ // if either case-sensitive, then comparison is CS
+ || (!get_folder_at(ctr).case_sensitive && !other.get_folder_at(ctr).case_sensitive)) {
this_element = this_element.casefold();
other_element = other_element.casefold();
}
@@ -268,6 +257,33 @@ public class Geary.FolderPath : BaseObject, Gee.Hashable<Geary.FolderPath>,
}
/**
+ * Does a Unicode-normalized, case insensitive match. Useful for getting a rough idea if
+ * a folder matches a name, but shouldn't be used to determine strict equality.
+ */
+ public int compare_normalized_ci(Geary.FolderPath other) {
+ return compare_internal(other, false, true);
+ }
+
+ /**
+ * { inheritDoc}
+ *
+ * Comparisons for Geary.FolderPath is defined as (a) empty paths are less-than non-empty paths
+ * and (b) each element is compared to the corresponding path element of the other FolderPath
+ * following collation rules for casefolded (case-insensitive) compared, and (c) shorter paths
+ * are less-than longer paths, assuming the path elements are equal up to the shorter path's
+ * length.
+ *
+ * Note that the { link FolderRoot.default_separator} has no bearing on comparisons, although
+ * { link FolderPath.case_sensitive} does.
+ *
+ * Returns -1 if this path is lexiographically before the other, 1 if its after, and 0 if they
+ * are equal.
+ */
+ public int compare_to(Geary.FolderPath other) {
+ return compare_internal(other, true, false);
+ }
+
+ /**
* { inheritDoc}
*
* As with { link compare_to}, the { link FolderRoot.default_separator} has no bearing on the
diff --git a/src/engine/imap-db/outbox/smtp-outbox-folder.vala
b/src/engine/imap-db/outbox/smtp-outbox-folder.vala
index 2dc8ea0..f791242 100644
--- a/src/engine/imap-db/outbox/smtp-outbox-folder.vala
+++ b/src/engine/imap-db/outbox/smtp-outbox-folder.vala
@@ -636,10 +636,10 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
private async void save_sent_mail_async(Geary.RFC822.Message rfc822, Cancellable? cancellable)
throws Error {
- Geary.Folder? sent_mail = _account.get_special_folder(Geary.SpecialFolderType.SENT);
- Geary.FolderSupport.Create? create = sent_mail as Geary.FolderSupport.Create;
+ Geary.FolderSupport.Create? create = (yield _account.get_required_special_folder_async(
+ Geary.SpecialFolderType.SENT, cancellable)) as Geary.FolderSupport.Create;
if (create == null)
- throw new EngineError.NOT_FOUND("Save sent mail enabled, but no sent mail folder");
+ throw new EngineError.NOT_FOUND("Save sent mail enabled, but no writable sent mail folder");
bool open = false;
try {
diff --git a/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala
b/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala
index 66f2569..cf0c3d0 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-account.vala
@@ -71,7 +71,7 @@ private class Geary.ImapEngine.GmailAccount : Geary.ImapEngine.GenericAccount {
}
}
- protected override GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
+ protected override MinimalFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
ImapDB.Account local_account, ImapDB.Folder local_folder) {
// although Gmail supports XLIST, this will be called on startup if the XLIST properties
// for the folders hasn't been retrieved yet. Once they've been retrieved and stored in
@@ -81,19 +81,7 @@ private class Geary.ImapEngine.GmailAccount : Geary.ImapEngine.GenericAccount {
switch (special_folder_type) {
case SpecialFolderType.ALL_MAIL:
- return new GenericAllMailFolder(this, remote_account, local_account, local_folder,
- special_folder_type);
-
- case SpecialFolderType.SENT:
- return new GenericSentMailFolder(this, remote_account, local_account, local_folder,
- special_folder_type);
-
- case SpecialFolderType.TRASH:
- return new GenericTrashFolder(this, remote_account, local_account, local_folder,
- special_folder_type);
-
- case SpecialFolderType.DRAFTS:
- return new GenericDraftsFolder(this, remote_account, local_account, local_folder,
+ return new MinimalFolder(this, remote_account, local_account, local_folder,
special_folder_type);
default:
diff --git a/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
b/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
index f6525bd..3151c3b 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-folder.vala
@@ -4,7 +4,7 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
-private class Geary.ImapEngine.GmailFolder : GenericFolder, FolderSupport.Archive {
+private class Geary.ImapEngine.GmailFolder : MinimalFolder, FolderSupport.Archive {
public GmailFolder(GmailAccount account, Imap.Account remote, ImapDB.Account local,
ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
base (account, remote, local, local_folder, special_folder_type);
diff --git a/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
b/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
index f5c0328..18aecee 100644
--- a/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
+++ b/src/engine/imap-engine/gmail/imap-engine-gmail-search-folder.vala
@@ -21,7 +21,7 @@ public class Geary.ImapEngine.GmailSearchFolder : Geary.SearchFolder {
Cancellable? cancellable = null) throws Error {
Geary.Folder? trash_folder = null;
try {
- trash_folder = account.get_special_folder(Geary.SpecialFolderType.TRASH);
+ 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);
}
diff --git a/src/engine/imap-engine/imap-engine-email-prefetcher.vala
b/src/engine/imap-engine/imap-engine-email-prefetcher.vala
index 9f574f4..619e102 100644
--- a/src/engine/imap-engine/imap-engine-email-prefetcher.vala
+++ b/src/engine/imap-engine/imap-engine-email-prefetcher.vala
@@ -23,7 +23,7 @@ private class Geary.ImapEngine.EmailPrefetcher : Object {
public Nonblocking.CountingSemaphore active_sem { get; private set;
default = new Nonblocking.CountingSemaphore(null); }
- private unowned ImapEngine.GenericFolder folder;
+ private unowned ImapEngine.MinimalFolder folder;
private int start_delay_sec;
private Nonblocking.Mutex mutex = new Nonblocking.Mutex();
private Gee.TreeSet<Geary.Email> prefetch_emails = new Gee.TreeSet<Geary.Email>(
@@ -31,7 +31,7 @@ private class Geary.ImapEngine.EmailPrefetcher : Object {
private uint schedule_id = 0;
private Cancellable cancellable = new Cancellable();
- public EmailPrefetcher(ImapEngine.GenericFolder folder, int start_delay_sec = PREFETCH_DELAY_SEC) {
+ public EmailPrefetcher(ImapEngine.MinimalFolder folder, int start_delay_sec = PREFETCH_DELAY_SEC) {
assert(start_delay_sec > 0);
this.folder = folder;
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 668dd04..87741b6 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -13,8 +13,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
private Imap.Account remote;
private ImapDB.Account local;
private bool open = false;
- private Gee.HashMap<FolderPath, GenericFolder> folder_map = new Gee.HashMap<
- FolderPath, GenericFolder>();
+ 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 uint refresh_folder_timeout_id = 0;
private bool in_refresh_enumerate = false;
@@ -194,12 +194,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
message("%s: Rebuild complete", to_string());
}
- // Subclasses should implement this to return their flavor of a GenericFolder with the
+ // Subclasses should implement this to return their flavor of a MinimalFolder with the
// appropriate interfaces attached. The returned folder should have its SpecialFolderType
// set using either the properties from the local folder or its path.
//
// This won't be called to build the Outbox or search folder, but for all others (including Inbox) it
will.
- protected abstract GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
+ protected abstract MinimalFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
ImapDB.Account local_account, ImapDB.Folder local_folder);
// Subclasses with specific SearchFolder implementations should override
@@ -208,15 +208,15 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
return new SearchFolder(this);
}
- private GenericFolder build_folder(ImapDB.Folder local_folder) {
+ private MinimalFolder build_folder(ImapDB.Folder local_folder) {
return Geary.Collection.get_first(build_folders(
Geary.iterate<ImapDB.Folder>(local_folder).to_array_list()));
}
- private Gee.Collection<GenericFolder> build_folders(Gee.Collection<ImapDB.Folder> local_folders) {
+ private Gee.Collection<MinimalFolder> build_folders(Gee.Collection<ImapDB.Folder> local_folders) {
Gee.ArrayList<ImapDB.Folder> folders_to_build = new Gee.ArrayList<ImapDB.Folder>();
- Gee.ArrayList<GenericFolder> built_folders = new Gee.ArrayList<GenericFolder>();
- Gee.ArrayList<GenericFolder> return_folders = new Gee.ArrayList<GenericFolder>();
+ Gee.ArrayList<MinimalFolder> built_folders = new Gee.ArrayList<MinimalFolder>();
+ Gee.ArrayList<MinimalFolder> return_folders = new Gee.ArrayList<MinimalFolder>();
foreach(ImapDB.Folder local_folder in local_folders) {
if (folder_map.has_key(local_folder.get_path()))
@@ -226,7 +226,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
}
foreach(ImapDB.Folder folder_to_build in folders_to_build) {
- GenericFolder folder = new_folder(folder_to_build.get_path(), remote, local, folder_to_build);
+ MinimalFolder folder = new_folder(folder_to_build.get_path(), remote, local, folder_to_build);
folder_map.set(folder.path, folder);
built_folders.add(folder);
return_folders.add(folder);
@@ -425,26 +425,156 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
yield local.clone_folder_async(remote_folder, cancellable);
}
- // Fetch the local account's version of the folder for the GenericFolder
+ // Fetch the local account's version of the folder for the MinimalFolder
return build_folder((ImapDB.Folder) yield local.fetch_folder_async(path, cancellable));
}
+ private Gee.HashMap<Geary.SpecialFolderType, Gee.ArrayList<string>> get_mailbox_search_names() {
+ Gee.HashMap<Geary.SpecialFolderType, string> mailbox_search_names
+ = new Gee.HashMap<Geary.SpecialFolderType, string>();
+ mailbox_search_names.set(Geary.SpecialFolderType.DRAFTS,
+ // List of folder names to match for Drafts, separated by |. Please add localized common
+ // names for the Drafts folder, leaving in the English names as well. The first in the list
+ // will be the default, so please add the most common localized name to the front.
+ _("Drafts | Draft"));
+ mailbox_search_names.set(Geary.SpecialFolderType.SENT,
+ // List of folder names to match for Sent Mail, separated by |. Please add localized common
+ // names for the Sent Mail folder, leaving in the English names as well. The first in the list
+ // will be the default, so please add the most common localized name to the front.
+ _("Sent | Sent Mail | Sent Email | Sent E-Mail"));
+ mailbox_search_names.set(Geary.SpecialFolderType.SPAM,
+ // List of folder names to match for Spam, separated by |. Please add localized common
+ // names for the Spam folder, leaving in the English names as well. The first in the list
+ // will be the default, so please add the most common localized name to the front.
+ _("Junk | Spam | Junk Mail | Junk Email | Junk E-Mail | Bulk Mail | Bulk Email | Bulk E-Mail"));
+ mailbox_search_names.set(Geary.SpecialFolderType.TRASH,
+ // List of folder names to match for Trash, separated by |. Please add localized common
+ // names for the Trash folder, leaving in the English names as well. The first in the list
+ // will be the default, so please add the most common localized name to the front.
+ _("Trash | Rubbish | Rubbish Bin"));
+
+ Gee.HashMap<Geary.SpecialFolderType, Gee.ArrayList<string>> compiled
+ = new Gee.HashMap<Geary.SpecialFolderType, Gee.ArrayList<string>>();
+
+ foreach (Geary.SpecialFolderType t in mailbox_search_names.keys) {
+ compiled.set(t, Geary.iterate_array<string>(mailbox_search_names.get(t).split("|"))
+ .map<string>(n => n.strip()).to_array_list());
+ }
+
+ return compiled;
+ }
+
+ private async Geary.Folder ensure_special_folder_async(Geary.SpecialFolderType special,
+ Cancellable? cancellable) throws Error {
+ Geary.Folder? folder = get_special_folder(special);
+ if (folder != null)
+ return folder;
+
+ MinimalFolder? minimal_folder = null;
+ Geary.FolderPath? path = information.get_special_folder_path(special);
+ if (path != null) {
+ debug("Previously used %s for special folder %s", path.to_string(), special.to_string());
+ } else {
+ // This is the first time we're turning a non-special folder into a special one.
+ // After we do this, we'll record which one we picked in the account info.
+
+ Gee.ArrayList<string> search_names = get_mailbox_search_names().get(special);
+ foreach (string search_name in search_names) {
+ Geary.FolderPath search_path = new Imap.FolderRoot(search_name, null);
+ foreach (Geary.FolderPath test_path in folder_map.keys) {
+ if (test_path.compare_normalized_ci(search_path) == 0) {
+ path = search_path;
+ break;
+ }
+ }
+ if (path != null)
+ break;
+ }
+ if (path == null) {
+ foreach (string search_name in search_names) {
+ Geary.FolderPath search_path = new Imap.FolderRoot(
+ Imap.MailboxSpecifier.CANONICAL_INBOX_NAME, null).get_child(search_name);
+ foreach (Geary.FolderPath test_path in folder_map.keys) {
+ if (test_path.compare_normalized_ci(search_path) == 0) {
+ path = search_path;
+ break;
+ }
+ }
+ if (path != null)
+ break;
+ }
+ }
+
+ if (path == null)
+ path = new Imap.FolderRoot(search_names[0], null);
+
+ information.set_special_folder_path(special, path);
+ yield information.store_async(cancellable);
+ }
+
+ if (path in folder_map.keys) {
+ debug("Promoting %s to special folder %s", path.to_string(), special.to_string());
+
+ minimal_folder = folder_map.get(path);
+ } else {
+ debug("Creating %s to use as special folder %s", path.to_string(), special.to_string());
+
+ // TODO: ignore error due to already existing.
+ yield remote.create_folder_async(path, cancellable);
+ minimal_folder = (MinimalFolder) yield fetch_folder_async(path, cancellable);
+ }
+
+ minimal_folder.set_special_folder_type(special);
+ return minimal_folder;
+ }
+
+ public override async Geary.Folder get_required_special_folder_async(Geary.SpecialFolderType special,
+ Cancellable? cancellable) throws Error {
+ switch (special) {
+ case Geary.SpecialFolderType.DRAFTS:
+ case Geary.SpecialFolderType.SENT:
+ case Geary.SpecialFolderType.SPAM:
+ case Geary.SpecialFolderType.TRASH:
+ break;
+
+ default:
+ throw new EngineError.BAD_PARAMETERS(
+ "Invalid special folder type %s passed to get_required_special_folder_async",
+ special.to_string());
+ }
+
+ check_open();
+
+ return yield ensure_special_folder_async(special, cancellable);
+ }
+
+ private async void ensure_special_folders_async(Cancellable? cancellable) throws Error {
+ Geary.SpecialFolderType[] required = {
+ Geary.SpecialFolderType.DRAFTS,
+ Geary.SpecialFolderType.SENT,
+ Geary.SpecialFolderType.SPAM,
+ Geary.SpecialFolderType.TRASH,
+ };
+ foreach (Geary.SpecialFolderType special in required)
+ yield ensure_special_folder_async(special, cancellable);
+ }
+
private async void update_folders_async(Gee.Map<FolderPath, Geary.Folder> existing_folders,
Gee.Map<FolderPath, Imap.Folder> remote_folders, Cancellable? cancellable) {
// update all remote folders properties in the local store and active in the system
Gee.HashSet<Geary.FolderPath> altered_paths = new Gee.HashSet<Geary.FolderPath>();
foreach (Imap.Folder remote_folder in remote_folders.values) {
- GenericFolder? generic_folder = existing_folders.get(remote_folder.path)
- as GenericFolder;
- if (generic_folder == null)
+ MinimalFolder? minimal_folder = existing_folders.get(remote_folder.path)
+ as MinimalFolder;
+ if (minimal_folder == null)
continue;
// only worry about alterations if the remote is openable
if (remote_folder.properties.is_openable.is_possible()) {
- ImapDB.Folder local_folder = generic_folder.local_folder;
+ ImapDB.Folder local_folder = minimal_folder.local_folder;
if (remote_folder.properties.have_contents_changed(local_folder.get_properties(),
- generic_folder.to_string())) {
+ minimal_folder.to_string())) {
altered_paths.add(remote_folder.path);
}
}
@@ -462,8 +592,8 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
// (but only promote, not demote, since getting the special folder type via its
// properties relies on the optional XLIST extension)
// use this iteration to add discovered properties to map
- if (generic_folder.special_folder_type == SpecialFolderType.NONE)
-
generic_folder.set_special_folder_type(remote_folder.properties.attrs.get_special_folder_type());
+ if (minimal_folder.special_folder_type == SpecialFolderType.NONE)
+
minimal_folder.set_special_folder_type(remote_folder.properties.attrs.get_special_folder_type());
}
// If path in remote but not local, need to add it
@@ -500,7 +630,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
debug("Unable to fetch local folder after cloning: %s", convert_err.message);
}
}
- Gee.Collection<Geary.Folder> engine_added = new Gee.ArrayList<Geary.Folder>();
+ Gee.Collection<MinimalFolder> engine_added = new Gee.ArrayList<Geary.Folder>();
engine_added.add_all(build_folders(folders_to_build));
// TODO: Remove local folders no longer available remotely.
@@ -523,6 +653,12 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
if (altered.size > 0)
notify_folders_contents_altered(altered);
}
+
+ try {
+ yield ensure_special_folders_async(cancellable);
+ } catch (Error e) {
+ warning("Unable to ensure special folders: %s", e.message);
+ }
}
public override async void send_email_async(Geary.ComposedEmail composed,
diff --git a/src/engine/imap-engine/imap-engine-generic-folder.vala
b/src/engine/imap-engine/imap-engine-generic-folder.vala
index ade5748..b09f034 100644
--- a/src/engine/imap-engine/imap-engine-generic-folder.vala
+++ b/src/engine/imap-engine/imap-engine-generic-folder.vala
@@ -4,1291 +4,22 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
-private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.FolderSupport.Copy,
- Geary.FolderSupport.Mark, Geary.FolderSupport.Move {
- private const int FORCE_OPEN_REMOTE_TIMEOUT_SEC = 10;
- private const int DEFAULT_REESTABLISH_DELAY_MSEC = 10;
- private const int MAX_REESTABLISH_DELAY_MSEC = 1000;
-
- public override Account account { get { return _account; } }
-
- public override FolderProperties properties { get { return _properties; } }
-
- public override FolderPath path {
- get {
- return local_folder.get_path();
- }
- }
-
- private SpecialFolderType _special_folder_type;
- public override SpecialFolderType special_folder_type {
- get {
- return _special_folder_type;
- }
- }
-
- internal ImapDB.Folder local_folder { get; protected set; }
- internal Imap.Folder? remote_folder { get; protected set; default = null; }
- internal EmailPrefetcher email_prefetcher { get; private set; }
- internal EmailFlagWatcher email_flag_watcher;
-
- private weak GenericAccount _account;
- private Geary.AggregatedFolderProperties _properties = new Geary.AggregatedFolderProperties(
- false, false);
- private Imap.Account remote;
- private ImapDB.Account local;
- private Folder.OpenFlags open_flags = OpenFlags.NONE;
- private int open_count = 0;
- private bool remote_opened = false;
- private Nonblocking.ReportingSemaphore<bool>? remote_semaphore = null;
- private ReplayQueue? replay_queue = null;
- private int remote_count = -1;
- private uint open_remote_timer_id = 0;
- private int reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
-
+private class Geary.ImapEngine.GenericFolder : MinimalFolder, Geary.FolderSupport.Remove,
+ Geary.FolderSupport.Create {
public GenericFolder(GenericAccount account, Imap.Account remote, ImapDB.Account local,
ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
- _account = account;
- this.remote = remote;
- this.local = local;
- this.local_folder = local_folder;
- _special_folder_type = special_folder_type;
- _properties.add(local_folder.get_properties());
-
- email_flag_watcher = new EmailFlagWatcher(this);
- email_flag_watcher.email_flags_changed.connect(on_email_flags_changed);
-
- email_prefetcher = new EmailPrefetcher(this);
-
- local_folder.email_complete.connect(on_email_complete);
- }
-
- ~EngineFolder() {
- if (open_count > 0)
- warning("Folder %s destroyed without closing", to_string());
-
- local_folder.email_complete.disconnect(on_email_complete);
- }
-
- public void set_special_folder_type(SpecialFolderType new_type) {
- SpecialFolderType old_type = _special_folder_type;
- _special_folder_type = new_type;
- if(old_type != new_type)
- notify_special_folder_type_changed(old_type, new_type);
- }
-
- public override Geary.Folder.OpenState get_open_state() {
- if (open_count == 0)
- return Geary.Folder.OpenState.CLOSED;
-
- return (remote_folder != null) ? Geary.Folder.OpenState.BOTH : Geary.Folder.OpenState.LOCAL;
- }
-
- // Returns the synchronized remote count (-1 if not opened) and the last seen remote count (stored
- // locally, -1 if not available)
- //
- // Return value is the remote_count, unless the remote is unopened, in which case it's the
- // last_seen_remote_count (which may be -1).
- //
- // remote_count, last_seen_remote_count, and returned value do not reflect any notion of
- // messages marked for removal
- internal int get_remote_counts(out int remote_count, out int last_seen_remote_count) {
- remote_count = this.remote_count;
- last_seen_remote_count = local_folder.get_properties().select_examine_messages;
- if (last_seen_remote_count < 0)
- last_seen_remote_count = local_folder.get_properties().status_messages;
-
- return (remote_count >= 0) ? remote_count : last_seen_remote_count;
- }
-
- private async bool normalize_folders(Geary.Imap.Folder remote_folder, Geary.Folder.OpenFlags open_flags,
- Cancellable? cancellable) throws Error {
- debug("%s: Begin normalizing remote and local folders", to_string());
-
- Geary.Imap.FolderProperties local_properties = local_folder.get_properties();
- Geary.Imap.FolderProperties remote_properties = remote_folder.properties;
-
- // and both must have their next UID's (it's possible they don't if it's a non-selectable
- // folder)
- if (local_properties.uid_next == null || local_properties.uid_validity == null) {
- debug("%s: Unable to verify UIDs: missing local UIDNEXT (%s) and/or UIDVALIDITY (%s)",
- to_string(), (local_properties.uid_next == null).to_string(),
- (local_properties.uid_validity == null).to_string());
-
- return false;
- }
-
- if (remote_properties.uid_next == null || remote_properties.uid_validity == null) {
- debug("%s: Unable to verify UIDs: missing remote UIDNEXT (%s) and/or UIDVALIDITY (%s)",
- to_string(), (remote_properties.uid_next == null).to_string(),
- (remote_properties.uid_validity == null).to_string());
-
- return false;
- }
-
- // If UIDVALIDITY changes, all email in the folder must be removed as the UIDs are now
- // invalid ... we merely detach the emails (leaving their contents behind) so duplicate
- // detection can fix them up. But once all UIDs are removed, it's much like the next
- // if case where no earliest UID available, so simply exit.
- //
- // see http://tools.ietf.org/html/rfc3501#section-2.3.1.1
- if (local_properties.uid_validity.value != remote_properties.uid_validity.value) {
- debug("%s: UID validity changed, detaching all email: %s -> %s", to_string(),
- local_properties.uid_validity.value.to_string(),
- remote_properties.uid_validity.value.to_string());
-
- yield local_folder.detach_all_emails_async(cancellable);
-
- return true;
- }
-
- // fetch email from earliest email to last to (a) remove any deletions and (b) update
- // any flags that may have changed
- ImapDB.EmailIdentifier? local_earliest_id = yield local_folder.get_earliest_id_async(cancellable);
- ImapDB.EmailIdentifier? local_latest_id = yield local_folder.get_latest_id_async(cancellable);
-
- // verify still open; this is required throughout after each yield, as a close_async() can
- // come in ay any time since this does not run in the context of open_async()
- check_open("normalize_folders (local earliest/latest UID)");
-
- // if no earliest UID, that means no messages in local store, so nothing to update
- if (local_earliest_id == null || local_latest_id == null) {
- debug("%s: local store empty, nothing to normalize", to_string());
-
- return true;
- }
-
- assert(local_earliest_id.has_uid());
- assert(local_latest_id.has_uid());
-
- // if any messages are still marked for removal from last time, that means the EXPUNGE
- // never arrived from the server, in which case the folder is "dirty" and needs a full
- // normalization
- Gee.Set<ImapDB.EmailIdentifier>? already_marked_ids = yield local_folder.get_marked_ids_async(
- cancellable);
-
- // however, there may be enqueue ReplayOperations waiting to remove messages on the server
- // that marked some or all of those messages
- Gee.HashSet<ImapDB.EmailIdentifier> to_be_removed = new Gee.HashSet<ImapDB.EmailIdentifier>();
- replay_queue.get_ids_to_be_remote_removed(to_be_removed);
-
- // don't consider those already marked as "already marked" if they were not leftover from
- // the last open of this folder
- if (already_marked_ids != null)
- already_marked_ids.remove_all(to_be_removed);
-
- bool is_dirty = (already_marked_ids != null && already_marked_ids.size > 0);
-
- if (is_dirty)
- debug("%s: %d remove markers found, folder is dirty", to_string(), already_marked_ids.size);
-
- // if UIDNEXT has changed, that indicates messages have been appended (and possibly removed)
- int64 uidnext_diff = remote_properties.uid_next.value - local_properties.uid_next.value;
-
- int local_message_count = (local_properties.select_examine_messages >= 0)
- ? local_properties.select_examine_messages : 0;
- int remote_message_count = (remote_properties.select_examine_messages >= 0)
- ? remote_properties.select_examine_messages : 0;
-
- // if UIDNEXT is the same as last time AND the total count of email is the same, then
- // nothing has been added or removed
- if (!is_dirty && uidnext_diff == 0 && local_message_count == remote_message_count) {
- debug("%s: No messages added/removed since last opened, normalization completed", to_string());
-
- return true;
- }
-
- // a full normalize works from the highest possible UID on the remote and work down to the lowest
UID on
- // the local; this covers all messages appended since last seen as well as any removed
- Imap.UID last_uid = remote_properties.uid_next.previous(true);
-
- // if the difference in UIDNEXT values equals the difference in message count, then only
- // an append could have happened, so only pull in the new messages ... note that this is not
foolproof,
- // as UIDs are not guaranteed to increase by 1; however, this is a standard implementation practice,
- // so it's worth looking for
- //
- // (Also, this cannot fail; if this situation exists, then it cannot by definition indicate another
- // situation, esp. messages being removed.)
- Imap.UID first_uid;
- if (!is_dirty && uidnext_diff == (remote_message_count - local_message_count)) {
- first_uid = local_latest_id.uid.next(true);
-
- debug("%s: Messages only appended (local/remote UIDNEXT=%s/%s total=%d/%d diff=%s), gathering
mail UIDs %s:%s",
- to_string(), local_properties.uid_next.to_string(), remote_properties.uid_next.to_string(),
- local_properties.select_examine_messages, remote_properties.select_examine_messages,
uidnext_diff.to_string(),
- first_uid.to_string(), last_uid.to_string());
- } else {
- first_uid = local_earliest_id.uid;
-
- debug("%s: Messages appended/removed (local/remote UIDNEXT=%s/%s total=%d/%d diff=%s), gathering
mail UIDs %s:%s",
- to_string(), local_properties.uid_next.to_string(), remote_properties.uid_next.to_string(),
- local_properties.select_examine_messages, remote_properties.select_examine_messages,
uidnext_diff.to_string(),
- first_uid.to_string(), last_uid.to_string());
- }
-
- // get all the UIDs in said range from the local store, sorted; convert to non-null
- // for ease of use later
- Gee.Set<Imap.UID>? local_uids = yield local_folder.list_uids_by_range_async(
- first_uid, last_uid, true, cancellable);
- if (local_uids == null)
- local_uids = new Gee.HashSet<Imap.UID>();
-
- check_open("normalize_folders (list local)");
-
- // Do the same on the remote ... make non-null for ease of use later
- Gee.Set<Imap.UID>? remote_uids = yield remote_folder.list_uids_async(
- new Imap.MessageSet.uid_range(first_uid, last_uid), cancellable);
- if (remote_uids == null)
- remote_uids = new Gee.HashSet<Imap.UID>();
-
- check_open("normalize_folders (list remote)");
-
- debug("%s: Loaded local (%d) and remote (%d) UIDs, normalizing...", to_string(),
- local_uids.size, remote_uids.size);
-
- Gee.HashSet<Imap.UID> removed_uids = new Gee.HashSet<Imap.UID>();
- Gee.HashSet<Imap.UID> appended_uids = new Gee.HashSet<Imap.UID>();
- Gee.HashSet<Imap.UID> inserted_uids = new Gee.HashSet<Imap.UID>();
-
- // Because the number of UIDs being processed can be immense in large folders, process
- // in a background thread
- yield Nonblocking.Concurrent.global.schedule_async(() => {
- // walk local UIDs looking for UIDs no longer on remote, removing those that are available
- // make the next pass that much shorter
- foreach (Imap.UID local_uid in local_uids) {
- // if in local but not remote, consider removed from remote
- if (!remote_uids.remove(local_uid))
- removed_uids.add(local_uid);
- }
-
- // everything remaining in remote has been added since folder last seen ... whether they're
- // discovered (inserted) or appended depends on the highest local UID
- foreach (Imap.UID remote_uid in remote_uids) {
- if (remote_uid.compare_to(local_latest_id.uid) > 0)
- appended_uids.add(remote_uid);
- else
- inserted_uids.add(remote_uid);
- }
-
- // the UIDs marked for removal are going to be re-inserted into the vector once they're
- // cleared, so add them here as well
- if (already_marked_ids != null) {
- foreach (ImapDB.EmailIdentifier id in already_marked_ids) {
- assert(id.has_uid());
-
- if (!appended_uids.contains(id.uid))
- inserted_uids.add(id.uid);
- }
- }
- }, cancellable);
-
- debug("%s: changes since last seen: removed=%d appended=%d inserted=%d", to_string(),
- removed_uids.size, appended_uids.size, inserted_uids.size);
-
- // fetch from the server the local store's required flags for all appended/inserted messages
- // (which is simply equal to all remaining remote UIDs)
- Gee.List<Geary.Email>? to_create = null;
- if (remote_uids.size > 0) {
- // for new messages, get the local store's required fields (which provide duplicate
- // detection)
- to_create = yield remote_folder.list_email_async(
- new Imap.MessageSet.uid_sparse(remote_uids.to_array()), ImapDB.Folder.REQUIRED_FIELDS,
- cancellable);
- }
-
- check_open("normalize_folders (list remote appended/inserted required fields)");
-
- // store new messages and add IDs to the appended/discovered EmailIdentifier buckets
- Gee.Set<ImapDB.EmailIdentifier> appended_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
- Gee.Set<ImapDB.EmailIdentifier> locally_appended_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
- Gee.Set<ImapDB.EmailIdentifier> inserted_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
- Gee.Set<ImapDB.EmailIdentifier> locally_inserted_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
- if (to_create != null && to_create.size > 0) {
- Gee.Map<Email, bool>? created_or_merged = yield local_folder.create_or_merge_email_async(
- to_create, cancellable);
- assert(created_or_merged != null);
-
- // it's possible a large number of messages have come in, so process them in the
- // background
- yield Nonblocking.Concurrent.global.schedule_async(() => {
- foreach (Email email in created_or_merged.keys) {
- ImapDB.EmailIdentifier id = (ImapDB.EmailIdentifier) email.id;
- bool created = created_or_merged.get(email);
-
- // report all appended email, but separate out email never seen before (created)
- // as locally-appended
- if (appended_uids.contains(id.uid)) {
- appended_ids.add(id);
-
- if (created)
- locally_appended_ids.add(id);
- } else if (inserted_uids.contains(id.uid)) {
- inserted_ids.add(id);
-
- if (created)
- locally_inserted_ids.add(id);
- }
- }
- }, cancellable);
-
- debug("%s: Finished creating/merging %d emails", to_string(), created_or_merged.size);
- }
-
- check_open("normalize_folders (created/merged appended/inserted emails)");
-
- // Convert removed UIDs into EmailIdentifiers and detach immediately
- Gee.Set<ImapDB.EmailIdentifier>? removed_ids = null;
- if (removed_uids.size > 0) {
- removed_ids = yield local_folder.get_ids_async(removed_uids,
- ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable);
- if (removed_ids != null && removed_ids.size > 0) {
- yield local_folder.detach_multiple_emails_async(removed_ids, cancellable);
- }
- }
-
- check_open("normalize_folders (removed emails)");
-
- // remove any extant remove markers, as everything is accounted for now, except for those
- // waiting to be removed in the queue
- yield local_folder.clear_remove_markers_async(to_be_removed, cancellable);
-
- check_open("normalize_folders (clear remove markers)");
-
- //
- // now normalized
- // notify subscribers of changes
- //
-
- Folder.CountChangeReason count_change_reason = Folder.CountChangeReason.NONE;
-
- if (removed_ids != null && removed_ids.size > 0) {
- // there may be operations pending on the remote queue for these removed emails; notify
- // operations that the email has shuffled off this mortal coil
- replay_queue.notify_remote_removed_ids(removed_ids);
-
- // notify subscribers about emails that have been removed
- debug("%s: Notifying of %d removed emails since last opened", to_string(), removed_ids.size);
- notify_email_removed(removed_ids);
-
- count_change_reason |= Folder.CountChangeReason.REMOVED;
- }
-
- // notify inserted (new email located somewhere inside the local vector)
- if (inserted_ids.size > 0) {
- debug("%s: Notifying of %d inserted emails since last opened", to_string(), inserted_ids.size);
- notify_email_inserted(inserted_ids);
-
- count_change_reason |= Folder.CountChangeReason.INSERTED;
- }
-
- // notify inserted (new email located somewhere inside the local vector that had to be
- // created, i.e. no portion was stored locally)
- if (locally_inserted_ids.size > 0) {
- debug("%s: Notifying of %d locally inserted emails since last opened", to_string(),
- locally_inserted_ids.size);
- notify_email_locally_inserted(locally_inserted_ids);
-
- count_change_reason |= Folder.CountChangeReason.INSERTED;
- }
-
- // notify appended (new email added since the folder was last opened)
- if (appended_ids.size > 0) {
- debug("%s: Notifying of %d appended emails since last opened", to_string(), appended_ids.size);
- notify_email_appended(appended_ids);
-
- count_change_reason |= Folder.CountChangeReason.APPENDED;
- }
-
- // notify locally appended (new email never seen before added since the folder was last
- // opened)
- if (locally_appended_ids.size > 0) {
- debug("%s: Notifying of %d locally appended emails since last opened", to_string(),
- locally_appended_ids.size);
- notify_email_locally_appended(locally_appended_ids);
-
- count_change_reason |= Folder.CountChangeReason.APPENDED;
- }
-
- if (count_change_reason != Folder.CountChangeReason.NONE) {
- debug("%s: Notifying of %Xh count change reason (%d remote messages)", to_string(),
- count_change_reason, remote_message_count);
- notify_email_count_changed(remote_message_count, count_change_reason);
- }
-
- debug("%s: Completed normalize_folder", to_string());
-
- return true;
- }
-
- public override async void wait_for_open_async(Cancellable? cancellable = null) throws Error {
- if (open_count == 0 || remote_semaphore == null)
- throw new EngineError.OPEN_REQUIRED("wait_for_open_async() can only be called after
open_async()");
-
- // if remote has not yet been opened, do it now ... this bool can go true only once after
- // an open_async, it's reset at close time
- if (!remote_opened) {
- debug("wait_for_open_async %s: opening remote on demand...", to_string());
-
- remote_opened = true;
- open_remote_async.begin(open_flags, null);
- }
-
- if (!yield remote_semaphore.wait_for_result_async(cancellable))
- throw new EngineError.ALREADY_CLOSED("%s failed to open", to_string());
- }
-
- public override async bool open_async(Geary.Folder.OpenFlags open_flags, Cancellable? cancellable = null)
- throws Error {
- if (open_count++ > 0) {
- // even if opened or opening, respect the NO_DELAY flag
- if (open_flags.is_all_set(OpenFlags.NO_DELAY)) {
- cancel_remote_open_timer();
- wait_for_open_async.begin();
- }
-
- debug("Not opening %s: already open (open_count=%d)", to_string(), open_count);
-
- return false;
- }
-
- this.open_flags = open_flags;
-
- open_internal(open_flags, cancellable);
-
- return true;
- }
-
- private void open_internal(Folder.OpenFlags open_flags, Cancellable? cancellable) {
- remote_semaphore = new Geary.Nonblocking.ReportingSemaphore<bool>(false);
-
- // start the replay queue
- replay_queue = new ReplayQueue(this);
-
- // Unless NO_DELAY is set, do NOT open the remote side here; wait for the ReplayQueue to
- // require a remote connection or wait_for_open_async() to be called ... this allows for
- // fast local-only operations to occur, local-only either because (a) the folder has all
- // the information required (for a list or fetch operation), or (b) the operation was de
- // facto local-only. In particular, EmailStore will open and close lots of folders,
- // causing a lot of connection setup and teardown
- //
- // However, want to eventually open, otherwise if there's no user interaction (i.e. a
- // second account Inbox they don't manipulate), no remote connection will ever be made,
- // meaning that folder normalization never happens and unsolicited notifications never
- // arrive
- if (open_flags.is_all_set(OpenFlags.NO_DELAY))
- wait_for_open_async.begin();
- else
- start_remote_open_timer();
+ base (account, remote, local, local_folder, special_folder_type);
}
- private void start_remote_open_timer() {
- if (open_remote_timer_id != 0)
- Source.remove(open_remote_timer_id);
-
- open_remote_timer_id = Timeout.add_seconds(FORCE_OPEN_REMOTE_TIMEOUT_SEC, on_open_remote_timeout);
- }
-
- private void cancel_remote_open_timer() {
- if (open_remote_timer_id == 0)
- return;
-
- Source.remove(open_remote_timer_id);
- open_remote_timer_id = 0;
- }
-
- private bool on_open_remote_timeout() {
- open_remote_timer_id = 0;
-
- // remote was not forced open due to caller, so open now
- wait_for_open_async.begin();
-
- return false;
- }
-
- private async void open_remote_async(Geary.Folder.OpenFlags open_flags, Cancellable? cancellable) {
- cancel_remote_open_timer();
-
- // watch for folder closing before this call got a chance to execute
- if (open_count == 0)
- return;
-
- try {
- debug("Fetching information for remote folder %s", to_string());
- Imap.Folder folder = yield remote.fetch_folder_async(local_folder.get_path(),
- cancellable);
-
- debug("Opening remote folder %s", folder.to_string());
- yield folder.open_async(cancellable);
-
- // allow subclasses to examine the opened folder and resolve any vital
- // inconsistencies
- if (yield normalize_folders(folder, open_flags, cancellable)) {
- // update flags, properties, etc.
- yield local.update_folder_select_examine_async(folder, cancellable);
-
- // signals
- folder.appended.connect(on_remote_appended);
- folder.removed.connect(on_remote_removed);
- folder.disconnected.connect(on_remote_disconnected);
-
- // state
- remote_count = folder.properties.email_total;
-
- // all set; bless the remote folder as opened
- remote_folder = folder;
- } else {
- debug("Unable to prepare remote folder %s: normalize_folders() failed", to_string());
- notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, null);
-
- // schedule immediate close
- close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, false,
- cancellable);
-
- return;
- }
- } catch (Error open_err) {
- debug("Unable to open or prepare remote folder %s: %s", to_string(), open_err.message);
- notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, open_err);
-
- // schedule immediate close and force reestablishment
- close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_ERROR, true,
- cancellable);
-
- return;
- }
-
- // open success, reset reestablishment delay
- reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
-
- int count;
- try {
- count = (remote_folder != null)
- ? remote_count
- : yield local_folder.get_email_count_async(ImapDB.Folder.ListFlags.NONE, cancellable);
- } catch (Error count_err) {
- debug("Unable to fetch count from local folder: %s", count_err.message);
-
- count = 0;
- }
-
- // notify any threads of execution waiting for the remote folder to open that the result
- // of that operation is ready
- try {
- remote_semaphore.notify_result(remote_folder != null, null);
- } catch (Error notify_err) {
- debug("Unable to fire semaphore notifying remote folder ready/not ready: %s",
- notify_err.message);
-
- // do this now rather than wait for close_internal_async() to execute to ensure that
- // any replay operations already queued don't attempt to run
- clear_remote_folder();
-
- notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, notify_err);
-
- // schedule immediate close
- close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, false,
- cancellable);
-
- return;
- }
-
- _properties.add(remote_folder.properties);
-
- // notify any subscribers with similar information
- notify_opened(
- (remote_folder != null) ? Geary.Folder.OpenState.BOTH : Geary.Folder.OpenState.LOCAL,
- count);
- }
-
- public override async void close_async(Cancellable? cancellable = null) throws Error {
- if (open_count == 0 || --open_count > 0)
- return;
-
- if (remote_folder != null)
- _properties.remove(remote_folder.properties);
-
- yield close_internal_async(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, false,
- cancellable);
- }
-
- // NOTE: This bypasses open_count and forces the Folder closed.
- internal async void close_internal_async(Folder.CloseReason local_reason, Folder.CloseReason
remote_reason,
- bool force_reestablish, Cancellable? cancellable) {
- cancel_remote_open_timer();
-
- // only flushing pending ReplayOperations if this is a "clean" close, not forced due to
- // error
- bool flush_pending = !remote_reason.is_error();
-
- // If closing due to error, notify all operations waiting for the remote that it's not
- // coming available ... this wakes up any ReplayOperation blocking on wait_for_open_async(),
- // necessary in order to finish ReplayQueue.close_async (i.e. to prevent deadlock); this
- // is necessary because it's possible for this method to be called before the remote_folder
- // has even had a chance to open.
- //
- // Note that we don't want to do this for a clean close, because we want to flush out
- // pending operations first
- Imap.Folder? closing_remote_folder = null;
- if (!flush_pending)
- closing_remote_folder = clear_remote_folder();
-
- // Close the replay queues; if a "clean" close, flush pending operations so everything
- // gets a chance to run; if forced close, drop everything outstanding
- try {
- if (replay_queue != null) {
- debug("Closing replay queue for %s... (flush_pending=%s)", to_string(),
- flush_pending.to_string());
- yield replay_queue.close_async(flush_pending);
- debug("Closed replay queue for %s", to_string());
- }
- } catch (Error replay_queue_err) {
- debug("Error closing %s replay queue: %s", to_string(), replay_queue_err.message);
- }
-
- replay_queue = null;
-
- // if a "clean" close, now go ahead and close the folder
- if (flush_pending)
- closing_remote_folder = clear_remote_folder();
-
- if (closing_remote_folder != null || force_reestablish) {
- // to avoid keeping the caller waiting while the remote end closes (i.e. drops the
- // connection or performs an IMAP CLOSE operation), close it in the background and
- // reestablish connection there, if necessary
- //
- // TODO: Problem with this is that we cannot effectively signal or report a close error,
- // because by the time this operation completes the folder is considered closed. That
- // may not be important to most callers, however.
- //
- // It also means the reference to the Folder must be maintained until completely
- // closed. Also not a problem, as GenericAccount does that internally. However, this
- // might be an issue if GenericAccount removes this folder due to a user command or
- // detection on the server, so this background op keeps a reference to the Folder
- close_remote_folder_async.begin(this, closing_remote_folder, remote_reason,
- force_reestablish);
- }
-
- remote_opened = false;
-
- // if remote reason is an error, then close_remote_folder_async() will be performing
- // reestablishment, so go no further
- if ((remote_reason.is_error() && closing_remote_folder != null) || force_reestablish)
- return;
-
- // forced closed one way or another, so reset state
- open_count = 0;
- reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
-
- // use remote_reason even if remote_folder was null; it could be that the error occurred
- // while opening and remote_folder was yet unassigned ... also, need to call this every
- // time, even if remote was not fully opened, as some callers rely on order of signals
- notify_closed(remote_reason);
-
- // see above note for why this must be called every time
- notify_closed(local_reason);
-
- notify_closed(CloseReason.FOLDER_CLOSED);
-
- debug("Folder %s closed", to_string());
- }
-
- // Returns the remote_folder, if it was set
- private Imap.Folder? clear_remote_folder() {
- if (remote_folder != null) {
- // disconnect signals before ripping out reference
- remote_folder.appended.disconnect(on_remote_appended);
- remote_folder.removed.disconnect(on_remote_removed);
- remote_folder.disconnected.disconnect(on_remote_disconnected);
- }
-
- Imap.Folder? old_remote_folder = remote_folder;
- remote_folder = null;
- remote_count = -1;
-
- remote_semaphore.reset();
- try {
- remote_semaphore.notify_result(false, null);
- } catch (Error err) {
- debug("Error attempting to notify that remote folder %s is now closed: %s", to_string(),
- err.message);
- }
-
- return old_remote_folder;
- }
-
- // See note in close_async() for why this method is static and uses an owned ref
- private static async void close_remote_folder_async(owned GenericFolder folder,
- owned Imap.Folder? remote_folder, Folder.CloseReason remote_reason, bool force_reestablish) {
- // force the remote closed; if due to a remote disconnect and plan on reopening, *still*
- // need to do this
- try {
- if (remote_folder != null)
- yield remote_folder.close_async(null);
- } catch (Error err) {
- debug("Unable to close remote %s: %s", remote_folder.to_string(), err.message);
-
- // fallthrough
- }
-
- // reestablish connection (which requires renormalizing the remote with the local) if
- // close was in error
- if (remote_reason.is_error() || force_reestablish) {
- debug("Reestablishing broken connection to %s in %dms", folder.to_string(),
- folder.reestablish_delay_msec);
- Timeout.add(folder.reestablish_delay_msec, () => {
- folder.open_internal(OpenFlags.NO_DELAY, null);
-
- return false;
- });
-
- folder.reestablish_delay_msec = (folder.reestablish_delay_msec * 2).clamp(
- DEFAULT_REESTABLISH_DELAY_MSEC, MAX_REESTABLISH_DELAY_MSEC);
- }
- }
-
- public override async void find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
- out Geary.EmailIdentifier? low, out Geary.EmailIdentifier? high,
+ public async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
Cancellable? cancellable = null) throws Error {
- low = null;
- high = null;
-
- Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? map
- = yield account.get_containing_folders_async(ids, cancellable);
-
- if (map != null) {
- Gee.ArrayList<Geary.EmailIdentifier> in_folder = new Gee.ArrayList<Geary.EmailIdentifier>();
- foreach (Geary.EmailIdentifier id in map.get_keys()) {
- if (path in map.get(id))
- in_folder.add(id);
- }
-
- if (in_folder.size > 0) {
- Gee.SortedSet<Geary.EmailIdentifier> sorted = Geary.EmailIdentifier.sort(in_folder);
-
- low = sorted.first();
- high = sorted.last();
- }
- }
- }
-
- private void on_email_complete(Gee.Collection<Geary.EmailIdentifier> email_ids) {
- notify_email_locally_complete(email_ids);
- }
-
- private void on_remote_appended(int reported_remote_count) {
- debug("%s on_remote_appended: remote_count=%d reported_remote_count=%d", to_string(), remote_count,
- reported_remote_count);
-
- if (reported_remote_count < 0)
- return;
-
- // from the new remote total and the old remote total, glean the SequenceNumbers of the
- // new email(s)
- Gee.List<Imap.SequenceNumber> positions = new Gee.ArrayList<Imap.SequenceNumber>();
- for (int pos = remote_count + 1; pos <= reported_remote_count; pos++)
- positions.add(new Imap.SequenceNumber(pos));
-
- // store the remote count NOW, as further appended messages could arrive before the
- // ReplayAppend executes
- remote_count = reported_remote_count;
-
- if (positions.size > 0)
- replay_queue.schedule_server_notification(new ReplayAppend(this, reported_remote_count,
positions));
- }
-
- // Need to prefetch at least an EmailIdentifier (and duplicate detection fields) to create a
- // normalized placeholder in the local database of the message, so all positions are
- // properly relative to the end of the message list; once this is done, notify user of new
- // messages. If duplicates, create_email_async() will fall through to an updated merge,
- // which is exactly what we want.
- //
- // This MUST only be called from ReplayAppend.
- internal async void do_replay_appended_messages(int reported_remote_count,
- Gee.List<Imap.SequenceNumber> remote_positions) {
- StringBuilder positions_builder = new StringBuilder("( ");
- foreach (Imap.SequenceNumber remote_position in remote_positions)
- positions_builder.append_printf("%s ", remote_position.to_string());
- positions_builder.append(")");
-
- debug("%s do_replay_appended_message: current remote_count=%d reported_remote_count=%d
remote_positions=%s",
- to_string(), remote_count, reported_remote_count, positions_builder.str);
-
- if (remote_positions.size == 0)
- return;
-
- Gee.HashSet<Geary.EmailIdentifier> created = new Gee.HashSet<Geary.EmailIdentifier>();
- Gee.HashSet<Geary.EmailIdentifier> appended = new Gee.HashSet<Geary.EmailIdentifier>();
- try {
- Imap.MessageSet msg_set = new Imap.MessageSet.sparse(remote_positions.to_array());
- Gee.List<Geary.Email>? list = yield remote_folder.list_email_async(msg_set,
- ImapDB.Folder.REQUIRED_FIELDS, null);
- if (list != null && list.size > 0) {
- debug("%s do_replay_appended_message: %d new messages in %s", to_string(),
- list.size, msg_set.to_string());
-
- // need to report both if it was created (not known before) and appended (which
- // could mean created or simply a known email associated with this folder)
- Gee.Map<Geary.Email, bool> created_or_merged =
- yield local_folder.create_or_merge_email_async(list, null);
- foreach (Geary.Email email in created_or_merged.keys) {
- // true means created
- if (created_or_merged.get(email)) {
- debug("%s do_replay_appended_message: appended email ID %s added",
- to_string(), email.id.to_string());
-
- created.add(email.id);
- } else {
- debug("%s do_replay_appended_message: appended email ID %s associated",
- to_string(), email.id.to_string());
- }
-
- appended.add(email.id);
- }
- } else {
- debug("%s do_replay_appended_message: no new messages in %s", to_string(),
- msg_set.to_string());
- }
- } catch (Error err) {
- debug("%s do_replay_appended_message: Unable to process: %s",
- to_string(), err.message);
- }
-
- // store the reported count, *not* the current count (which is updated outside the of
- // the queue) to ensure that updates happen serially and reflect committed local changes
- try {
- yield local_folder.update_remote_selected_message_count(reported_remote_count, null);
- } catch (Error err) {
- debug("%s do_replay_appended_message: Unable to save appended remote count %d: %s",
- to_string(), reported_remote_count, err.message);
- }
-
- if (appended.size > 0)
- notify_email_appended(appended);
-
- if (created.size > 0)
- notify_email_locally_appended(created);
-
- notify_email_count_changed(reported_remote_count, CountChangeReason.APPENDED);
-
- debug("%s do_replay_appended_message: completed, current remote_count=%d reported_remote_count=%d",
- to_string(), remote_count, reported_remote_count);
- }
-
- private void on_remote_removed(Imap.SequenceNumber position, int reported_remote_count) {
- debug("%s on_remote_removed: remote_count=%d position=%s reported_remote_count=%d", to_string(),
- remote_count, position.to_string(), reported_remote_count);
-
- if (reported_remote_count < 0)
- return;
-
- // notify of removal to all pending replay operations
- replay_queue.notify_remote_removed_position(position);
-
- // update remote count NOW, as further appended and removed messages can arrive before
- // ReplayRemoval executes
- //
- // something to note at this point: the ExpungeEmail operation marks messages as removed,
- // then signals they're removed and reports an adjusted count in its replay_local_async().
- // remote_count is *not* updated, which is why it's safe to do that here without worry.
- // similarly, signals are only fired here if marked, so the same EmailIdentifier isn't
- // reported twice
- remote_count = reported_remote_count;
-
- replay_queue.schedule_server_notification(new ReplayRemoval(this, reported_remote_count, position));
- }
-
- // This MUST only be called from ReplayRemoval.
- internal async void do_replay_removed_message(int reported_remote_count, Imap.SequenceNumber
remote_position) {
- debug("%s do_replay_removed_message: current remote_count=%d remote_position=%d
reported_remote_count=%d",
- to_string(), remote_count, remote_position.value, reported_remote_count);
-
- if (!remote_position.is_valid()) {
- debug("%s do_replay_removed_message: ignoring, invalid remote position or count",
- to_string());
-
- return;
- }
-
- int local_count = -1;
- int local_position = -1;
-
- ImapDB.EmailIdentifier? owned_id = null;
- try {
- // need total count, including those marked for removal, to accurately calculate position
- // from server's point of view, not client's
- local_count = yield local_folder.get_email_count_async(
- ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, null);
- local_position = remote_position.value - (reported_remote_count + 1 - local_count);
-
- // zero or negative means the message exists beyond the local vector's range, so
- // nothing to do there
- if (local_position > 0) {
- debug("%s do_replay_removed_message: local_count=%d local_position=%d", to_string(),
- local_count, local_position);
-
- owned_id = yield local_folder.get_id_at_async(local_position, null);
- } else {
- debug("%s do_replay_removed_message: message not stored locally (local_count=%d
local_position=%d)",
- to_string(), local_count, local_position);
- }
- } catch (Error err) {
- debug("%s do_replay_removed_message: unable to determine ID of removed message %s: %s",
- to_string(), remote_position.to_string(), err.message);
- }
-
- bool marked = false;
- if (owned_id != null) {
- debug("%s do_replay_removed_message: detaching from local store Email ID %s", to_string(),
- owned_id.to_string());
- try {
- // Reflect change in the local store and notify subscribers
- yield local_folder.detach_single_email_async(owned_id, out marked, null);
- } catch (Error err) {
- debug("%s do_replay_removed_message: unable to remove message #%s: %s", to_string(),
- remote_position.to_string(), err.message);
- }
-
- // Notify queued replay operations that the email has been removed (by EmailIdentifier)
- replay_queue.notify_remote_removed_ids(
- Geary.iterate<ImapDB.EmailIdentifier>(owned_id).to_array_list());
- } else {
- debug("%s do_replay_removed_message: remote_position=%d unknown in local store "
- + "(reported_remote_count=%d local_position=%d local_count=%d)",
- to_string(), remote_position.value, reported_remote_count, local_position, local_count);
- }
-
- // for debugging
- int new_local_count = -1;
- try {
- new_local_count = yield local_folder.get_email_count_async(
- ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, null);
- } catch (Error err) {
- debug("%s do_replay_removed_message: error fetching new local count: %s", to_string(),
- err.message);
- }
-
- // as with on_remote_appended(), only update in local store inside a queue operation, to
- // ensure serial commits
- try {
- yield local_folder.update_remote_selected_message_count(reported_remote_count, null);
- } catch (Error err) {
- debug("%s do_replay_removed_message: unable to save removed remote count: %s", to_string(),
- err.message);
- }
-
- // notify of change
- if (!marked && owned_id != null)
- notify_email_removed(Geary.iterate<Geary.EmailIdentifier>(owned_id).to_array_list());
-
- if (!marked)
- notify_email_count_changed(reported_remote_count, CountChangeReason.REMOVED);
-
- debug("%s do_replay_remove_message: completed, current remote_count=%d "
- + "(reported_remote_count=%d local_count=%d starting local_count=%d remote_position=%d
local_position=%d marked=%s)",
- to_string(), remote_count, reported_remote_count, new_local_count, local_count,
remote_position.value,
- local_position, marked.to_string());
- }
-
- private void on_remote_disconnected(Imap.ClientSession.DisconnectReason reason) {
- debug("on_remote_disconnected: reason=%s", reason.to_string());
-
- replay_queue.schedule(new ReplayDisconnect(this, reason));
- }
-
- //
- // list_email_by_id variants
- //
-
- public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier? initial_id,
- int count, Geary.Email.Field required_fields, Folder.ListFlags flags,
- Cancellable? cancellable = null) throws Error {
- Gee.List<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
- yield do_list_email_by_id_async("list_email_by_id_async", initial_id, count, required_fields,
- flags, accumulator, null, cancellable);
-
- return !accumulator.is_empty ? accumulator : null;
- }
-
- public override void lazy_list_email_by_id(Geary.EmailIdentifier? initial_id, int count,
- Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
- Cancellable? cancellable = null) {
- do_lazy_list_email_by_id_async.begin(initial_id, count, required_fields, flags, cb, cancellable);
- }
-
- private async void do_lazy_list_email_by_id_async(Geary.EmailIdentifier? initial_id, int count,
- Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable?
cancellable) {
- try {
- yield do_list_email_by_id_async("lazy_list_email_by_id", initial_id, count, required_fields,
- flags, null, cb, cancellable);
- } catch (Error err) {
- cb(null, err);
- }
- }
-
- private async void do_list_email_by_id_async(string method, Geary.EmailIdentifier? initial_id,
- int count, Geary.Email.Field required_fields, Folder.ListFlags flags,
- Gee.List<Geary.Email>? accumulator, EmailCallback? cb, Cancellable? cancellable) throws Error {
- check_open(method);
- check_flags(method, flags);
- if (initial_id != null)
- check_id(method, initial_id);
-
- if (count == 0) {
- // signal finished
- if (cb != null)
- cb(null, null);
-
- return;
- }
-
- // Schedule list operation and wait for completion.
- ListEmailByID op = new ListEmailByID(this, (ImapDB.EmailIdentifier) initial_id, count,
- required_fields, flags, accumulator, cb, cancellable);
- replay_queue.schedule(op);
-
- yield op.wait_for_ready_async(cancellable);
- }
-
- //
- // list_email_by_sparse_id variants
- //
-
- public async override Gee.List<Geary.Email>? list_email_by_sparse_id_async(
- Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields, Folder.ListFlags flags,
- Cancellable? cancellable = null) throws Error {
- Gee.ArrayList<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
- yield do_list_email_by_sparse_id_async("list_email_by_sparse_id_async", ids, required_fields,
- flags, accumulator, null, cancellable);
-
- return (accumulator.size > 0) ? accumulator : null;
- }
-
- public override void lazy_list_email_by_sparse_id(Gee.Collection<Geary.EmailIdentifier> ids,
- Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable?
cancellable = null) {
- do_lazy_list_email_by_sparse_id_async.begin(ids, required_fields, flags, cb, cancellable);
- }
-
- private async void do_lazy_list_email_by_sparse_id_async(Gee.Collection<Geary.EmailIdentifier> ids,
- Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable?
cancellable) {
- try {
- yield do_list_email_by_sparse_id_async("lazy_list_email_by_sparse_id", ids, required_fields,
- flags, null, cb, cancellable);
- } catch (Error err) {
- cb(null, err);
- }
- }
-
- private async void do_list_email_by_sparse_id_async(string method,
- Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields, Folder.ListFlags flags,
- Gee.List<Geary.Email>? accumulator, EmailCallback? cb, Cancellable? cancellable = null) throws Error
{
- check_open(method);
- check_flags(method, flags);
- check_ids(method, ids);
-
- if (ids.size == 0) {
- // signal finished
- if (cb != null)
- cb(null, null);
-
- return;
- }
-
- // Schedule list operation and wait for completion.
- // TODO: Break up requests to avoid hogging the queue
- ListEmailBySparseID op = new ListEmailBySparseID(this, (Gee.Collection<ImapDB.EmailIdentifier>) ids,
- required_fields, flags, accumulator, cb, cancellable);
- replay_queue.schedule(op);
-
- yield op.wait_for_ready_async(cancellable);
- }
-
- 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 {
- check_open("fetch_email_async");
- check_flags("fetch_email_async", flags);
- check_id("fetch_email_async", id);
-
- FetchEmail op = new FetchEmail(this, (ImapDB.EmailIdentifier) id, required_fields, flags,
- cancellable);
- replay_queue.schedule(op);
-
- yield op.wait_for_ready_async(cancellable);
-
- if (op.email == null) {
- throw new EngineError.NOT_FOUND("Email %s not found in %s", id.to_string(), to_string());
- } else if (!op.email.fields.fulfills(required_fields)) {
- throw new EngineError.INCOMPLETE_MESSAGE("Email %s in %s does not fulfill required fields %Xh
(has %Xh)",
- id.to_string(), to_string(), required_fields, op.email.fields);
- }
-
- return op.email;
- }
-
- // Helper function for child classes dealing with the delete/archive question. This method will
- // mark the message as deleted and expunge it.
- protected async void expunge_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
- Cancellable? cancellable = null) throws Error {
- check_open("expunge_email_async");
- check_ids("expunge_email_async", email_ids);
-
- RemoveEmail remove = new RemoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) email_ids,
- cancellable);
- replay_queue.schedule(remove);
-
- yield remove.wait_for_ready_async(cancellable);
- }
-
- private void check_open(string method) throws EngineError {
- if (open_count == 0)
- throw new EngineError.OPEN_REQUIRED("%s failed: folder %s is not open", method, to_string());
- }
-
- private void check_flags(string method, Folder.ListFlags flags) throws EngineError {
- if (flags.is_all_set(Folder.ListFlags.LOCAL_ONLY) &&
flags.is_all_set(Folder.ListFlags.FORCE_UPDATE)) {
- throw new EngineError.BAD_PARAMETERS("%s %s failed: LOCAL_ONLY and FORCE_UPDATE are mutually
exclusive",
- to_string(), method);
- }
- }
-
- private void check_id(string method, EmailIdentifier id) throws EngineError {
- if (!(id is ImapDB.EmailIdentifier))
- throw new EngineError.BAD_PARAMETERS("Email ID %s is not IMAP Email ID", id.to_string());
- }
-
- private void check_ids(string method, Gee.Collection<EmailIdentifier> ids) throws EngineError {
- foreach (EmailIdentifier id in ids)
- check_id(method, id);
- }
-
- public virtual async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
- Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
- Cancellable? cancellable = null) throws Error {
- check_open("mark_email_async");
-
- MarkEmail mark = new MarkEmail(this, to_mark, flags_to_add, flags_to_remove, cancellable);
- replay_queue.schedule(mark);
- yield mark.wait_for_ready_async(cancellable);
- }
-
- public virtual async void copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
- Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
- check_open("copy_email_async");
- check_ids("copy_email_async", to_copy);
-
- CopyEmail copy = new CopyEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_copy, destination);
- replay_queue.schedule(copy);
- yield copy.wait_for_ready_async(cancellable);
- }
-
- public virtual async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
- Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
- check_open("move_email_async");
- check_ids("move_email_async", to_move);
-
- MoveEmail move = new MoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_move, destination);
- replay_queue.schedule(move);
- yield move.wait_for_ready_async(cancellable);
- }
-
- private void on_email_flags_changed(Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> changed) {
- notify_email_flags_changed(changed);
- }
-
- // TODO: A proper public search mechanism; note that this always round-trips to the remote,
- // doesn't go through the replay queue, and doesn't deal with messages marked for deletion
- internal async Geary.EmailIdentifier? find_earliest_email_async(DateTime datetime,
- Geary.EmailIdentifier? before_id, Cancellable? cancellable) throws Error {
- check_open("find_earliest_email_async");
- if (before_id != null)
- check_id("find_earliest_email_async", before_id);
-
- Imap.SearchCriteria criteria = new Imap.SearchCriteria();
- criteria.is_(Imap.SearchCriterion.since_internaldate(new
Imap.InternalDate.from_date_time(datetime)));
-
- // if before_id available, only search for messages before it
- if (before_id != null) {
- Imap.UID? before_uid = yield local_folder.get_uid_async((ImapDB.EmailIdentifier) before_id,
- ImapDB.Folder.ListFlags.NONE, cancellable);
- if (before_uid == null) {
- throw new EngineError.NOT_FOUND("before_id %s not found in %s", before_id.to_string(),
- to_string());
- }
-
- criteria.and(Imap.SearchCriterion.message_set(
- new Imap.MessageSet.uid_range(new Imap.UID(Imap.UID.MIN), before_uid.previous(true))));
- }
-
- Gee.List<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
- ServerSearchEmail op = new ServerSearchEmail(this, criteria, Geary.Email.Field.NONE,
- accumulator, cancellable);
-
- // need to check again due to the yield in the above conditional block
- check_open("find_earliest_email_async.schedule operation");
-
- replay_queue.schedule(op);
-
- if (!yield op.wait_for_ready_async(cancellable))
- return null;
-
- // find earliest ID; because all Email comes from Folder, UID should always be present
- ImapDB.EmailIdentifier? earliest_id = null;
- foreach (Geary.Email email in accumulator) {
- ImapDB.EmailIdentifier email_id = (ImapDB.EmailIdentifier) email.id;
-
- if (earliest_id == null || email_id.uid.compare_to(earliest_id.uid) < 0)
- earliest_id = email_id;
- }
-
- return earliest_id;
+ yield expunge_email_async(email_ids, cancellable);
}
- internal async Geary.EmailIdentifier create_email_async(RFC822.Message rfc822, Geary.EmailFlags? flags,
- DateTime? date_received, Geary.EmailIdentifier? id, Cancellable? cancellable = null) throws Error {
- check_open("create_email_async");
- if (id != null)
- check_id("create_email_async", id);
-
- Error? cancel_error = null;
- Geary.EmailIdentifier? ret = null;
- try {
- CreateEmail create = new CreateEmail(this, rfc822, flags, date_received, cancellable);
- replay_queue.schedule(create);
- yield create.wait_for_ready_async(cancellable);
-
- ret = create.created_id;
- } catch (Error e) {
- if (e is IOError.CANCELLED)
- cancel_error = e;
- else
- throw e;
- }
-
- Geary.FolderSupport.Remove? remove_folder = this as Geary.FolderSupport.Remove;
-
- // Remove old message.
- if (id != null && remove_folder != null)
- yield remove_folder.remove_single_email_async(id, null);
-
- // If the user cancelled the operation, throw the error here.
- if (cancel_error != null)
- throw cancel_error;
-
- // If the caller cancelled during the remove operation, delete the newly created message to
- // safely back out.
- if (cancellable != null && cancellable.is_cancelled() && ret != null && remove_folder != null)
- yield remove_folder.remove_single_email_async(ret, null);
-
- return ret;
+ public new async Geary.EmailIdentifier? create_email_async(
+ RFC822.Message rfc822, Geary.EmailFlags? flags, DateTime? date_received,
+ Geary.EmailIdentifier? id, Cancellable? cancellable = null) throws Error {
+ return yield base.create_email_async(rfc822, flags, date_received, id, cancellable);
}
}
diff --git a/src/engine/imap-engine/imap-engine-minimal-folder.vala
b/src/engine/imap-engine/imap-engine-minimal-folder.vala
new file mode 100644
index 0000000..f1e6297
--- /dev/null
+++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala
@@ -0,0 +1,1295 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * 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.ImapEngine.MinimalFolder : Geary.AbstractFolder, Geary.FolderSupport.Copy,
+ Geary.FolderSupport.Mark, Geary.FolderSupport.Move {
+ private const int FORCE_OPEN_REMOTE_TIMEOUT_SEC = 10;
+ private const int DEFAULT_REESTABLISH_DELAY_MSEC = 10;
+ private const int MAX_REESTABLISH_DELAY_MSEC = 1000;
+
+ public override Account account { get { return _account; } }
+
+ public override FolderProperties properties { get { return _properties; } }
+
+ public override FolderPath path {
+ get {
+ return local_folder.get_path();
+ }
+ }
+
+ private SpecialFolderType _special_folder_type;
+ public override SpecialFolderType special_folder_type {
+ get {
+ return _special_folder_type;
+ }
+ }
+
+ internal ImapDB.Folder local_folder { get; protected set; }
+ internal Imap.Folder? remote_folder { get; protected set; default = null; }
+ internal EmailPrefetcher email_prefetcher { get; private set; }
+ internal EmailFlagWatcher email_flag_watcher;
+
+ private weak GenericAccount _account;
+ private Geary.AggregatedFolderProperties _properties = new Geary.AggregatedFolderProperties(
+ false, false);
+ private Imap.Account remote;
+ private ImapDB.Account local;
+ private Folder.OpenFlags open_flags = OpenFlags.NONE;
+ private int open_count = 0;
+ private bool remote_opened = false;
+ private Nonblocking.ReportingSemaphore<bool>? remote_semaphore = null;
+ private ReplayQueue? replay_queue = null;
+ private int remote_count = -1;
+ private uint open_remote_timer_id = 0;
+ private int reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
+
+ public MinimalFolder(GenericAccount account, Imap.Account remote, ImapDB.Account local,
+ ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
+ _account = account;
+ this.remote = remote;
+ this.local = local;
+ this.local_folder = local_folder;
+ _special_folder_type = special_folder_type;
+ _properties.add(local_folder.get_properties());
+
+ email_flag_watcher = new EmailFlagWatcher(this);
+ email_flag_watcher.email_flags_changed.connect(on_email_flags_changed);
+
+ email_prefetcher = new EmailPrefetcher(this);
+
+ local_folder.email_complete.connect(on_email_complete);
+ }
+
+ ~EngineFolder() {
+ if (open_count > 0)
+ warning("Folder %s destroyed without closing", to_string());
+
+ local_folder.email_complete.disconnect(on_email_complete);
+ }
+
+ public void set_special_folder_type(SpecialFolderType new_type) {
+ SpecialFolderType old_type = _special_folder_type;
+ _special_folder_type = new_type;
+ if(old_type != new_type)
+ notify_special_folder_type_changed(old_type, new_type);
+ }
+
+ public override Geary.Folder.OpenState get_open_state() {
+ if (open_count == 0)
+ return Geary.Folder.OpenState.CLOSED;
+
+ return (remote_folder != null) ? Geary.Folder.OpenState.BOTH : Geary.Folder.OpenState.LOCAL;
+ }
+
+ // Returns the synchronized remote count (-1 if not opened) and the last seen remote count (stored
+ // locally, -1 if not available)
+ //
+ // Return value is the remote_count, unless the remote is unopened, in which case it's the
+ // last_seen_remote_count (which may be -1).
+ //
+ // remote_count, last_seen_remote_count, and returned value do not reflect any notion of
+ // messages marked for removal
+ internal int get_remote_counts(out int remote_count, out int last_seen_remote_count) {
+ remote_count = this.remote_count;
+ last_seen_remote_count = local_folder.get_properties().select_examine_messages;
+ if (last_seen_remote_count < 0)
+ last_seen_remote_count = local_folder.get_properties().status_messages;
+
+ return (remote_count >= 0) ? remote_count : last_seen_remote_count;
+ }
+
+ private async bool normalize_folders(Geary.Imap.Folder remote_folder, Geary.Folder.OpenFlags open_flags,
+ Cancellable? cancellable) throws Error {
+ debug("%s: Begin normalizing remote and local folders", to_string());
+
+ Geary.Imap.FolderProperties local_properties = local_folder.get_properties();
+ Geary.Imap.FolderProperties remote_properties = remote_folder.properties;
+
+ // and both must have their next UID's (it's possible they don't if it's a non-selectable
+ // folder)
+ if (local_properties.uid_next == null || local_properties.uid_validity == null) {
+ debug("%s: Unable to verify UIDs: missing local UIDNEXT (%s) and/or UIDVALIDITY (%s)",
+ to_string(), (local_properties.uid_next == null).to_string(),
+ (local_properties.uid_validity == null).to_string());
+
+ return false;
+ }
+
+ if (remote_properties.uid_next == null || remote_properties.uid_validity == null) {
+ debug("%s: Unable to verify UIDs: missing remote UIDNEXT (%s) and/or UIDVALIDITY (%s)",
+ to_string(), (remote_properties.uid_next == null).to_string(),
+ (remote_properties.uid_validity == null).to_string());
+
+ return false;
+ }
+
+ // If UIDVALIDITY changes, all email in the folder must be removed as the UIDs are now
+ // invalid ... we merely detach the emails (leaving their contents behind) so duplicate
+ // detection can fix them up. But once all UIDs are removed, it's much like the next
+ // if case where no earliest UID available, so simply exit.
+ //
+ // see http://tools.ietf.org/html/rfc3501#section-2.3.1.1
+ if (local_properties.uid_validity.value != remote_properties.uid_validity.value) {
+ debug("%s: UID validity changed, detaching all email: %s -> %s", to_string(),
+ local_properties.uid_validity.value.to_string(),
+ remote_properties.uid_validity.value.to_string());
+
+ yield local_folder.detach_all_emails_async(cancellable);
+
+ return true;
+ }
+
+ // fetch email from earliest email to last to (a) remove any deletions and (b) update
+ // any flags that may have changed
+ ImapDB.EmailIdentifier? local_earliest_id = yield local_folder.get_earliest_id_async(cancellable);
+ ImapDB.EmailIdentifier? local_latest_id = yield local_folder.get_latest_id_async(cancellable);
+
+ // verify still open; this is required throughout after each yield, as a close_async() can
+ // come in ay any time since this does not run in the context of open_async()
+ check_open("normalize_folders (local earliest/latest UID)");
+
+ // if no earliest UID, that means no messages in local store, so nothing to update
+ if (local_earliest_id == null || local_latest_id == null) {
+ debug("%s: local store empty, nothing to normalize", to_string());
+
+ return true;
+ }
+
+ assert(local_earliest_id.has_uid());
+ assert(local_latest_id.has_uid());
+
+ // if any messages are still marked for removal from last time, that means the EXPUNGE
+ // never arrived from the server, in which case the folder is "dirty" and needs a full
+ // normalization
+ Gee.Set<ImapDB.EmailIdentifier>? already_marked_ids = yield local_folder.get_marked_ids_async(
+ cancellable);
+
+ // however, there may be enqueue ReplayOperations waiting to remove messages on the server
+ // that marked some or all of those messages
+ Gee.HashSet<ImapDB.EmailIdentifier> to_be_removed = new Gee.HashSet<ImapDB.EmailIdentifier>();
+ replay_queue.get_ids_to_be_remote_removed(to_be_removed);
+
+ // don't consider those already marked as "already marked" if they were not leftover from
+ // the last open of this folder
+ if (already_marked_ids != null)
+ already_marked_ids.remove_all(to_be_removed);
+
+ bool is_dirty = (already_marked_ids != null && already_marked_ids.size > 0);
+
+ if (is_dirty)
+ debug("%s: %d remove markers found, folder is dirty", to_string(), already_marked_ids.size);
+
+ // if UIDNEXT has changed, that indicates messages have been appended (and possibly removed)
+ int64 uidnext_diff = remote_properties.uid_next.value - local_properties.uid_next.value;
+
+ int local_message_count = (local_properties.select_examine_messages >= 0)
+ ? local_properties.select_examine_messages : 0;
+ int remote_message_count = (remote_properties.select_examine_messages >= 0)
+ ? remote_properties.select_examine_messages : 0;
+
+ // if UIDNEXT is the same as last time AND the total count of email is the same, then
+ // nothing has been added or removed
+ if (!is_dirty && uidnext_diff == 0 && local_message_count == remote_message_count) {
+ debug("%s: No messages added/removed since last opened, normalization completed", to_string());
+
+ return true;
+ }
+
+ // a full normalize works from the highest possible UID on the remote and work down to the lowest
UID on
+ // the local; this covers all messages appended since last seen as well as any removed
+ Imap.UID last_uid = remote_properties.uid_next.previous(true);
+
+ // if the difference in UIDNEXT values equals the difference in message count, then only
+ // an append could have happened, so only pull in the new messages ... note that this is not
foolproof,
+ // as UIDs are not guaranteed to increase by 1; however, this is a standard implementation practice,
+ // so it's worth looking for
+ //
+ // (Also, this cannot fail; if this situation exists, then it cannot by definition indicate another
+ // situation, esp. messages being removed.)
+ Imap.UID first_uid;
+ if (!is_dirty && uidnext_diff == (remote_message_count - local_message_count)) {
+ first_uid = local_latest_id.uid.next(true);
+
+ debug("%s: Messages only appended (local/remote UIDNEXT=%s/%s total=%d/%d diff=%s), gathering
mail UIDs %s:%s",
+ to_string(), local_properties.uid_next.to_string(), remote_properties.uid_next.to_string(),
+ local_properties.select_examine_messages, remote_properties.select_examine_messages,
uidnext_diff.to_string(),
+ first_uid.to_string(), last_uid.to_string());
+ } else {
+ first_uid = local_earliest_id.uid;
+
+ debug("%s: Messages appended/removed (local/remote UIDNEXT=%s/%s total=%d/%d diff=%s), gathering
mail UIDs %s:%s",
+ to_string(), local_properties.uid_next.to_string(), remote_properties.uid_next.to_string(),
+ local_properties.select_examine_messages, remote_properties.select_examine_messages,
uidnext_diff.to_string(),
+ first_uid.to_string(), last_uid.to_string());
+ }
+
+ // get all the UIDs in said range from the local store, sorted; convert to non-null
+ // for ease of use later
+ Gee.Set<Imap.UID>? local_uids = yield local_folder.list_uids_by_range_async(
+ first_uid, last_uid, true, cancellable);
+ if (local_uids == null)
+ local_uids = new Gee.HashSet<Imap.UID>();
+
+ check_open("normalize_folders (list local)");
+
+ // Do the same on the remote ... make non-null for ease of use later
+ Gee.Set<Imap.UID>? remote_uids = yield remote_folder.list_uids_async(
+ new Imap.MessageSet.uid_range(first_uid, last_uid), cancellable);
+ if (remote_uids == null)
+ remote_uids = new Gee.HashSet<Imap.UID>();
+
+ check_open("normalize_folders (list remote)");
+
+ debug("%s: Loaded local (%d) and remote (%d) UIDs, normalizing...", to_string(),
+ local_uids.size, remote_uids.size);
+
+ Gee.HashSet<Imap.UID> removed_uids = new Gee.HashSet<Imap.UID>();
+ Gee.HashSet<Imap.UID> appended_uids = new Gee.HashSet<Imap.UID>();
+ Gee.HashSet<Imap.UID> inserted_uids = new Gee.HashSet<Imap.UID>();
+
+ // Because the number of UIDs being processed can be immense in large folders, process
+ // in a background thread
+ yield Nonblocking.Concurrent.global.schedule_async(() => {
+ // walk local UIDs looking for UIDs no longer on remote, removing those that are available
+ // make the next pass that much shorter
+ foreach (Imap.UID local_uid in local_uids) {
+ // if in local but not remote, consider removed from remote
+ if (!remote_uids.remove(local_uid))
+ removed_uids.add(local_uid);
+ }
+
+ // everything remaining in remote has been added since folder last seen ... whether they're
+ // discovered (inserted) or appended depends on the highest local UID
+ foreach (Imap.UID remote_uid in remote_uids) {
+ if (remote_uid.compare_to(local_latest_id.uid) > 0)
+ appended_uids.add(remote_uid);
+ else
+ inserted_uids.add(remote_uid);
+ }
+
+ // the UIDs marked for removal are going to be re-inserted into the vector once they're
+ // cleared, so add them here as well
+ if (already_marked_ids != null) {
+ foreach (ImapDB.EmailIdentifier id in already_marked_ids) {
+ assert(id.has_uid());
+
+ if (!appended_uids.contains(id.uid))
+ inserted_uids.add(id.uid);
+ }
+ }
+ }, cancellable);
+
+ debug("%s: changes since last seen: removed=%d appended=%d inserted=%d", to_string(),
+ removed_uids.size, appended_uids.size, inserted_uids.size);
+
+ // fetch from the server the local store's required flags for all appended/inserted messages
+ // (which is simply equal to all remaining remote UIDs)
+ Gee.List<Geary.Email>? to_create = null;
+ if (remote_uids.size > 0) {
+ // for new messages, get the local store's required fields (which provide duplicate
+ // detection)
+ to_create = yield remote_folder.list_email_async(
+ new Imap.MessageSet.uid_sparse(remote_uids.to_array()), ImapDB.Folder.REQUIRED_FIELDS,
+ cancellable);
+ }
+
+ check_open("normalize_folders (list remote appended/inserted required fields)");
+
+ // store new messages and add IDs to the appended/discovered EmailIdentifier buckets
+ Gee.Set<ImapDB.EmailIdentifier> appended_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
+ Gee.Set<ImapDB.EmailIdentifier> locally_appended_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
+ Gee.Set<ImapDB.EmailIdentifier> inserted_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
+ Gee.Set<ImapDB.EmailIdentifier> locally_inserted_ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
+ if (to_create != null && to_create.size > 0) {
+ Gee.Map<Email, bool>? created_or_merged = yield local_folder.create_or_merge_email_async(
+ to_create, cancellable);
+ assert(created_or_merged != null);
+
+ // it's possible a large number of messages have come in, so process them in the
+ // background
+ yield Nonblocking.Concurrent.global.schedule_async(() => {
+ foreach (Email email in created_or_merged.keys) {
+ ImapDB.EmailIdentifier id = (ImapDB.EmailIdentifier) email.id;
+ bool created = created_or_merged.get(email);
+
+ // report all appended email, but separate out email never seen before (created)
+ // as locally-appended
+ if (appended_uids.contains(id.uid)) {
+ appended_ids.add(id);
+
+ if (created)
+ locally_appended_ids.add(id);
+ } else if (inserted_uids.contains(id.uid)) {
+ inserted_ids.add(id);
+
+ if (created)
+ locally_inserted_ids.add(id);
+ }
+ }
+ }, cancellable);
+
+ debug("%s: Finished creating/merging %d emails", to_string(), created_or_merged.size);
+ }
+
+ check_open("normalize_folders (created/merged appended/inserted emails)");
+
+ // Convert removed UIDs into EmailIdentifiers and detach immediately
+ Gee.Set<ImapDB.EmailIdentifier>? removed_ids = null;
+ if (removed_uids.size > 0) {
+ removed_ids = yield local_folder.get_ids_async(removed_uids,
+ ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, cancellable);
+ if (removed_ids != null && removed_ids.size > 0) {
+ yield local_folder.detach_multiple_emails_async(removed_ids, cancellable);
+ }
+ }
+
+ check_open("normalize_folders (removed emails)");
+
+ // remove any extant remove markers, as everything is accounted for now, except for those
+ // waiting to be removed in the queue
+ yield local_folder.clear_remove_markers_async(to_be_removed, cancellable);
+
+ check_open("normalize_folders (clear remove markers)");
+
+ //
+ // now normalized
+ // notify subscribers of changes
+ //
+
+ Folder.CountChangeReason count_change_reason = Folder.CountChangeReason.NONE;
+
+ if (removed_ids != null && removed_ids.size > 0) {
+ // there may be operations pending on the remote queue for these removed emails; notify
+ // operations that the email has shuffled off this mortal coil
+ replay_queue.notify_remote_removed_ids(removed_ids);
+
+ // notify subscribers about emails that have been removed
+ debug("%s: Notifying of %d removed emails since last opened", to_string(), removed_ids.size);
+ notify_email_removed(removed_ids);
+
+ count_change_reason |= Folder.CountChangeReason.REMOVED;
+ }
+
+ // notify inserted (new email located somewhere inside the local vector)
+ if (inserted_ids.size > 0) {
+ debug("%s: Notifying of %d inserted emails since last opened", to_string(), inserted_ids.size);
+ notify_email_inserted(inserted_ids);
+
+ count_change_reason |= Folder.CountChangeReason.INSERTED;
+ }
+
+ // notify inserted (new email located somewhere inside the local vector that had to be
+ // created, i.e. no portion was stored locally)
+ if (locally_inserted_ids.size > 0) {
+ debug("%s: Notifying of %d locally inserted emails since last opened", to_string(),
+ locally_inserted_ids.size);
+ notify_email_locally_inserted(locally_inserted_ids);
+
+ count_change_reason |= Folder.CountChangeReason.INSERTED;
+ }
+
+ // notify appended (new email added since the folder was last opened)
+ if (appended_ids.size > 0) {
+ debug("%s: Notifying of %d appended emails since last opened", to_string(), appended_ids.size);
+ notify_email_appended(appended_ids);
+
+ count_change_reason |= Folder.CountChangeReason.APPENDED;
+ }
+
+ // notify locally appended (new email never seen before added since the folder was last
+ // opened)
+ if (locally_appended_ids.size > 0) {
+ debug("%s: Notifying of %d locally appended emails since last opened", to_string(),
+ locally_appended_ids.size);
+ notify_email_locally_appended(locally_appended_ids);
+
+ count_change_reason |= Folder.CountChangeReason.APPENDED;
+ }
+
+ if (count_change_reason != Folder.CountChangeReason.NONE) {
+ debug("%s: Notifying of %Xh count change reason (%d remote messages)", to_string(),
+ count_change_reason, remote_message_count);
+ notify_email_count_changed(remote_message_count, count_change_reason);
+ }
+
+ debug("%s: Completed normalize_folder", to_string());
+
+ return true;
+ }
+
+ public override async void wait_for_open_async(Cancellable? cancellable = null) throws Error {
+ if (open_count == 0 || remote_semaphore == null)
+ throw new EngineError.OPEN_REQUIRED("wait_for_open_async() can only be called after
open_async()");
+
+ // if remote has not yet been opened, do it now ... this bool can go true only once after
+ // an open_async, it's reset at close time
+ if (!remote_opened) {
+ debug("wait_for_open_async %s: opening remote on demand...", to_string());
+
+ remote_opened = true;
+ open_remote_async.begin(open_flags, null);
+ }
+
+ if (!yield remote_semaphore.wait_for_result_async(cancellable))
+ throw new EngineError.ALREADY_CLOSED("%s failed to open", to_string());
+ }
+
+ public override async bool open_async(Geary.Folder.OpenFlags open_flags, Cancellable? cancellable = null)
+ throws Error {
+ if (open_count++ > 0) {
+ // even if opened or opening, respect the NO_DELAY flag
+ if (open_flags.is_all_set(OpenFlags.NO_DELAY)) {
+ cancel_remote_open_timer();
+ wait_for_open_async.begin();
+ }
+
+ debug("Not opening %s: already open (open_count=%d)", to_string(), open_count);
+
+ return false;
+ }
+
+ this.open_flags = open_flags;
+
+ open_internal(open_flags, cancellable);
+
+ return true;
+ }
+
+ private void open_internal(Folder.OpenFlags open_flags, Cancellable? cancellable) {
+ remote_semaphore = new Geary.Nonblocking.ReportingSemaphore<bool>(false);
+
+ // start the replay queue
+ replay_queue = new ReplayQueue(this);
+
+ // Unless NO_DELAY is set, do NOT open the remote side here; wait for the ReplayQueue to
+ // require a remote connection or wait_for_open_async() to be called ... this allows for
+ // fast local-only operations to occur, local-only either because (a) the folder has all
+ // the information required (for a list or fetch operation), or (b) the operation was de
+ // facto local-only. In particular, EmailStore will open and close lots of folders,
+ // causing a lot of connection setup and teardown
+ //
+ // However, want to eventually open, otherwise if there's no user interaction (i.e. a
+ // second account Inbox they don't manipulate), no remote connection will ever be made,
+ // meaning that folder normalization never happens and unsolicited notifications never
+ // arrive
+ if (open_flags.is_all_set(OpenFlags.NO_DELAY))
+ wait_for_open_async.begin();
+ else
+ start_remote_open_timer();
+ }
+
+ private void start_remote_open_timer() {
+ if (open_remote_timer_id != 0)
+ Source.remove(open_remote_timer_id);
+
+ open_remote_timer_id = Timeout.add_seconds(FORCE_OPEN_REMOTE_TIMEOUT_SEC, on_open_remote_timeout);
+ }
+
+ private void cancel_remote_open_timer() {
+ if (open_remote_timer_id == 0)
+ return;
+
+ Source.remove(open_remote_timer_id);
+ open_remote_timer_id = 0;
+ }
+
+ private bool on_open_remote_timeout() {
+ open_remote_timer_id = 0;
+
+ // remote was not forced open due to caller, so open now
+ wait_for_open_async.begin();
+
+ return false;
+ }
+
+ private async void open_remote_async(Geary.Folder.OpenFlags open_flags, Cancellable? cancellable) {
+ cancel_remote_open_timer();
+
+ // watch for folder closing before this call got a chance to execute
+ if (open_count == 0)
+ return;
+
+ try {
+ debug("Fetching information for remote folder %s", to_string());
+ Imap.Folder folder = yield remote.fetch_folder_async(local_folder.get_path(),
+ cancellable);
+
+ debug("Opening remote folder %s", folder.to_string());
+ yield folder.open_async(cancellable);
+
+ // allow subclasses to examine the opened folder and resolve any vital
+ // inconsistencies
+ if (yield normalize_folders(folder, open_flags, cancellable)) {
+ // update flags, properties, etc.
+ yield local.update_folder_select_examine_async(folder, cancellable);
+
+ // signals
+ folder.appended.connect(on_remote_appended);
+ folder.removed.connect(on_remote_removed);
+ folder.disconnected.connect(on_remote_disconnected);
+
+ // state
+ remote_count = folder.properties.email_total;
+
+ // all set; bless the remote folder as opened
+ remote_folder = folder;
+ } else {
+ debug("Unable to prepare remote folder %s: normalize_folders() failed", to_string());
+ notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, null);
+
+ // schedule immediate close
+ close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, false,
+ cancellable);
+
+ return;
+ }
+ } catch (Error open_err) {
+ debug("Unable to open or prepare remote folder %s: %s", to_string(), open_err.message);
+ notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, open_err);
+
+ // schedule immediate close and force reestablishment
+ close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_ERROR, true,
+ cancellable);
+
+ return;
+ }
+
+ // open success, reset reestablishment delay
+ reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
+
+ int count;
+ try {
+ count = (remote_folder != null)
+ ? remote_count
+ : yield local_folder.get_email_count_async(ImapDB.Folder.ListFlags.NONE, cancellable);
+ } catch (Error count_err) {
+ debug("Unable to fetch count from local folder: %s", count_err.message);
+
+ count = 0;
+ }
+
+ // notify any threads of execution waiting for the remote folder to open that the result
+ // of that operation is ready
+ try {
+ remote_semaphore.notify_result(remote_folder != null, null);
+ } catch (Error notify_err) {
+ debug("Unable to fire semaphore notifying remote folder ready/not ready: %s",
+ notify_err.message);
+
+ // do this now rather than wait for close_internal_async() to execute to ensure that
+ // any replay operations already queued don't attempt to run
+ clear_remote_folder();
+
+ notify_open_failed(Geary.Folder.OpenFailed.REMOTE_FAILED, notify_err);
+
+ // schedule immediate close
+ close_internal_async.begin(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, false,
+ cancellable);
+
+ return;
+ }
+
+ _properties.add(remote_folder.properties);
+
+ // notify any subscribers with similar information
+ notify_opened(
+ (remote_folder != null) ? Geary.Folder.OpenState.BOTH : Geary.Folder.OpenState.LOCAL,
+ count);
+ }
+
+ public override async void close_async(Cancellable? cancellable = null) throws Error {
+ if (open_count == 0 || --open_count > 0)
+ return;
+
+ if (remote_folder != null)
+ _properties.remove(remote_folder.properties);
+
+ yield close_internal_async(CloseReason.LOCAL_CLOSE, CloseReason.REMOTE_CLOSE, false,
+ cancellable);
+ }
+
+ // NOTE: This bypasses open_count and forces the Folder closed.
+ internal async void close_internal_async(Folder.CloseReason local_reason, Folder.CloseReason
remote_reason,
+ bool force_reestablish, Cancellable? cancellable) {
+ cancel_remote_open_timer();
+
+ // only flushing pending ReplayOperations if this is a "clean" close, not forced due to
+ // error
+ bool flush_pending = !remote_reason.is_error();
+
+ // If closing due to error, notify all operations waiting for the remote that it's not
+ // coming available ... this wakes up any ReplayOperation blocking on wait_for_open_async(),
+ // necessary in order to finish ReplayQueue.close_async (i.e. to prevent deadlock); this
+ // is necessary because it's possible for this method to be called before the remote_folder
+ // has even had a chance to open.
+ //
+ // Note that we don't want to do this for a clean close, because we want to flush out
+ // pending operations first
+ Imap.Folder? closing_remote_folder = null;
+ if (!flush_pending)
+ closing_remote_folder = clear_remote_folder();
+
+ // Close the replay queues; if a "clean" close, flush pending operations so everything
+ // gets a chance to run; if forced close, drop everything outstanding
+ try {
+ if (replay_queue != null) {
+ debug("Closing replay queue for %s... (flush_pending=%s)", to_string(),
+ flush_pending.to_string());
+ yield replay_queue.close_async(flush_pending);
+ debug("Closed replay queue for %s", to_string());
+ }
+ } catch (Error replay_queue_err) {
+ debug("Error closing %s replay queue: %s", to_string(), replay_queue_err.message);
+ }
+
+ replay_queue = null;
+
+ // if a "clean" close, now go ahead and close the folder
+ if (flush_pending)
+ closing_remote_folder = clear_remote_folder();
+
+ if (closing_remote_folder != null || force_reestablish) {
+ // to avoid keeping the caller waiting while the remote end closes (i.e. drops the
+ // connection or performs an IMAP CLOSE operation), close it in the background and
+ // reestablish connection there, if necessary
+ //
+ // TODO: Problem with this is that we cannot effectively signal or report a close error,
+ // because by the time this operation completes the folder is considered closed. That
+ // may not be important to most callers, however.
+ //
+ // It also means the reference to the Folder must be maintained until completely
+ // closed. Also not a problem, as GenericAccount does that internally. However, this
+ // might be an issue if GenericAccount removes this folder due to a user command or
+ // detection on the server, so this background op keeps a reference to the Folder
+ close_remote_folder_async.begin(this, closing_remote_folder, remote_reason,
+ force_reestablish);
+ }
+
+ remote_opened = false;
+
+ // if remote reason is an error, then close_remote_folder_async() will be performing
+ // reestablishment, so go no further
+ if ((remote_reason.is_error() && closing_remote_folder != null) || force_reestablish)
+ return;
+
+ // forced closed one way or another, so reset state
+ open_count = 0;
+ reestablish_delay_msec = DEFAULT_REESTABLISH_DELAY_MSEC;
+
+ // use remote_reason even if remote_folder was null; it could be that the error occurred
+ // while opening and remote_folder was yet unassigned ... also, need to call this every
+ // time, even if remote was not fully opened, as some callers rely on order of signals
+ notify_closed(remote_reason);
+
+ // see above note for why this must be called every time
+ notify_closed(local_reason);
+
+ notify_closed(CloseReason.FOLDER_CLOSED);
+
+ debug("Folder %s closed", to_string());
+ }
+
+ // Returns the remote_folder, if it was set
+ private Imap.Folder? clear_remote_folder() {
+ if (remote_folder != null) {
+ // disconnect signals before ripping out reference
+ remote_folder.appended.disconnect(on_remote_appended);
+ remote_folder.removed.disconnect(on_remote_removed);
+ remote_folder.disconnected.disconnect(on_remote_disconnected);
+ }
+
+ Imap.Folder? old_remote_folder = remote_folder;
+ remote_folder = null;
+ remote_count = -1;
+
+ remote_semaphore.reset();
+ try {
+ remote_semaphore.notify_result(false, null);
+ } catch (Error err) {
+ debug("Error attempting to notify that remote folder %s is now closed: %s", to_string(),
+ err.message);
+ }
+
+ return old_remote_folder;
+ }
+
+ // See note in close_async() for why this method is static and uses an owned ref
+ private static async void close_remote_folder_async(owned MinimalFolder folder,
+ owned Imap.Folder remote_folder, Folder.CloseReason remote_reason, bool force_reestablish) {
+ // force the remote closed; if due to a remote disconnect and plan on reopening, *still*
+ // need to do this
+ try {
+ if (remote_folder != null)
+ yield remote_folder.close_async(null);
+ } catch (Error err) {
+ debug("Unable to close remote %s: %s", remote_folder.to_string(), err.message);
+
+ // fallthrough
+ }
+
+ // reestablish connection (which requires renormalizing the remote with the local) if
+ // close was in error
+ if (remote_reason.is_error() || force_reestablish) {
+ debug("Reestablishing broken connection to %s in %dms", folder.to_string(),
+ folder.reestablish_delay_msec);
+ Timeout.add(folder.reestablish_delay_msec, () => {
+ folder.open_internal(OpenFlags.NO_DELAY, null);
+
+ return false;
+ });
+
+ folder.reestablish_delay_msec = (folder.reestablish_delay_msec * 2).clamp(
+ DEFAULT_REESTABLISH_DELAY_MSEC, MAX_REESTABLISH_DELAY_MSEC);
+ }
+ }
+
+ public override async void find_boundaries_async(Gee.Collection<Geary.EmailIdentifier> ids,
+ out Geary.EmailIdentifier? low, out Geary.EmailIdentifier? high,
+ Cancellable? cancellable = null) throws Error {
+ low = null;
+ high = null;
+
+ Gee.MultiMap<Geary.EmailIdentifier, Geary.FolderPath>? map
+ = yield account.get_containing_folders_async(ids, cancellable);
+
+ if (map != null) {
+ Gee.ArrayList<Geary.EmailIdentifier> in_folder = new Gee.ArrayList<Geary.EmailIdentifier>();
+ foreach (Geary.EmailIdentifier id in map.get_keys()) {
+ if (path in map.get(id))
+ in_folder.add(id);
+ }
+
+ if (in_folder.size > 0) {
+ Gee.SortedSet<Geary.EmailIdentifier> sorted = Geary.EmailIdentifier.sort(in_folder);
+
+ low = sorted.first();
+ high = sorted.last();
+ }
+ }
+ }
+
+ private void on_email_complete(Gee.Collection<Geary.EmailIdentifier> email_ids) {
+ notify_email_locally_complete(email_ids);
+ }
+
+ private void on_remote_appended(int reported_remote_count) {
+ debug("%s on_remote_appended: remote_count=%d reported_remote_count=%d", to_string(), remote_count,
+ reported_remote_count);
+
+ if (reported_remote_count < 0)
+ return;
+
+ // from the new remote total and the old remote total, glean the SequenceNumbers of the
+ // new email(s)
+ Gee.List<Imap.SequenceNumber> positions = new Gee.ArrayList<Imap.SequenceNumber>();
+ for (int pos = remote_count + 1; pos <= reported_remote_count; pos++)
+ positions.add(new Imap.SequenceNumber(pos));
+
+ // store the remote count NOW, as further appended messages could arrive before the
+ // ReplayAppend executes
+ remote_count = reported_remote_count;
+
+ if (positions.size > 0)
+ replay_queue.schedule_server_notification(new ReplayAppend(this, reported_remote_count,
positions));
+ }
+
+ // Need to prefetch at least an EmailIdentifier (and duplicate detection fields) to create a
+ // normalized placeholder in the local database of the message, so all positions are
+ // properly relative to the end of the message list; once this is done, notify user of new
+ // messages. If duplicates, create_email_async() will fall through to an updated merge,
+ // which is exactly what we want.
+ //
+ // This MUST only be called from ReplayAppend.
+ internal async void do_replay_appended_messages(int reported_remote_count,
+ Gee.List<Imap.SequenceNumber> remote_positions) {
+ StringBuilder positions_builder = new StringBuilder("( ");
+ foreach (Imap.SequenceNumber remote_position in remote_positions)
+ positions_builder.append_printf("%s ", remote_position.to_string());
+ positions_builder.append(")");
+
+ debug("%s do_replay_appended_message: current remote_count=%d reported_remote_count=%d
remote_positions=%s",
+ to_string(), remote_count, reported_remote_count, positions_builder.str);
+
+ if (remote_positions.size == 0)
+ return;
+
+ Gee.HashSet<Geary.EmailIdentifier> created = new Gee.HashSet<Geary.EmailIdentifier>();
+ Gee.HashSet<Geary.EmailIdentifier> appended = new Gee.HashSet<Geary.EmailIdentifier>();
+ try {
+ Imap.MessageSet msg_set = new Imap.MessageSet.sparse(remote_positions.to_array());
+ Gee.List<Geary.Email>? list = yield remote_folder.list_email_async(msg_set,
+ ImapDB.Folder.REQUIRED_FIELDS, null);
+ if (list != null && list.size > 0) {
+ debug("%s do_replay_appended_message: %d new messages in %s", to_string(),
+ list.size, msg_set.to_string());
+
+ // need to report both if it was created (not known before) and appended (which
+ // could mean created or simply a known email associated with this folder)
+ Gee.Map<Geary.Email, bool> created_or_merged =
+ yield local_folder.create_or_merge_email_async(list, null);
+ foreach (Geary.Email email in created_or_merged.keys) {
+ // true means created
+ if (created_or_merged.get(email)) {
+ debug("%s do_replay_appended_message: appended email ID %s added",
+ to_string(), email.id.to_string());
+
+ created.add(email.id);
+ } else {
+ debug("%s do_replay_appended_message: appended email ID %s associated",
+ to_string(), email.id.to_string());
+ }
+
+ appended.add(email.id);
+ }
+ } else {
+ debug("%s do_replay_appended_message: no new messages in %s", to_string(),
+ msg_set.to_string());
+ }
+ } catch (Error err) {
+ debug("%s do_replay_appended_message: Unable to process: %s",
+ to_string(), err.message);
+ }
+
+ // store the reported count, *not* the current count (which is updated outside the of
+ // the queue) to ensure that updates happen serially and reflect committed local changes
+ try {
+ yield local_folder.update_remote_selected_message_count(reported_remote_count, null);
+ } catch (Error err) {
+ debug("%s do_replay_appended_message: Unable to save appended remote count %d: %s",
+ to_string(), reported_remote_count, err.message);
+ }
+
+ if (appended.size > 0)
+ notify_email_appended(appended);
+
+ if (created.size > 0)
+ notify_email_locally_appended(created);
+
+ notify_email_count_changed(reported_remote_count, CountChangeReason.APPENDED);
+
+ debug("%s do_replay_appended_message: completed, current remote_count=%d reported_remote_count=%d",
+ to_string(), remote_count, reported_remote_count);
+ }
+
+ private void on_remote_removed(Imap.SequenceNumber position, int reported_remote_count) {
+ debug("%s on_remote_removed: remote_count=%d position=%s reported_remote_count=%d", to_string(),
+ remote_count, position.to_string(), reported_remote_count);
+
+ if (reported_remote_count < 0)
+ return;
+
+ // notify of removal to all pending replay operations
+ replay_queue.notify_remote_removed_position(position);
+
+ // update remote count NOW, as further appended and removed messages can arrive before
+ // ReplayRemoval executes
+ //
+ // something to note at this point: the ExpungeEmail operation marks messages as removed,
+ // then signals they're removed and reports an adjusted count in its replay_local_async().
+ // remote_count is *not* updated, which is why it's safe to do that here without worry.
+ // similarly, signals are only fired here if marked, so the same EmailIdentifier isn't
+ // reported twice
+ remote_count = reported_remote_count;
+
+ replay_queue.schedule_server_notification(new ReplayRemoval(this, reported_remote_count, position));
+ }
+
+ // This MUST only be called from ReplayRemoval.
+ internal async void do_replay_removed_message(int reported_remote_count, Imap.SequenceNumber
remote_position) {
+ debug("%s do_replay_removed_message: current remote_count=%d remote_position=%d
reported_remote_count=%d",
+ to_string(), remote_count, remote_position.value, reported_remote_count);
+
+ if (!remote_position.is_valid()) {
+ debug("%s do_replay_removed_message: ignoring, invalid remote position or count",
+ to_string());
+
+ return;
+ }
+
+ int local_count = -1;
+ int local_position = -1;
+
+ ImapDB.EmailIdentifier? owned_id = null;
+ try {
+ // need total count, including those marked for removal, to accurately calculate position
+ // from server's point of view, not client's
+ local_count = yield local_folder.get_email_count_async(
+ ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, null);
+ local_position = remote_position.value - (reported_remote_count + 1 - local_count);
+
+ // zero or negative means the message exists beyond the local vector's range, so
+ // nothing to do there
+ if (local_position > 0) {
+ debug("%s do_replay_removed_message: local_count=%d local_position=%d", to_string(),
+ local_count, local_position);
+
+ owned_id = yield local_folder.get_id_at_async(local_position, null);
+ } else {
+ debug("%s do_replay_removed_message: message not stored locally (local_count=%d
local_position=%d)",
+ to_string(), local_count, local_position);
+ }
+ } catch (Error err) {
+ debug("%s do_replay_removed_message: unable to determine ID of removed message %s: %s",
+ to_string(), remote_position.to_string(), err.message);
+ }
+
+ bool marked = false;
+ if (owned_id != null) {
+ debug("%s do_replay_removed_message: detaching from local store Email ID %s", to_string(),
+ owned_id.to_string());
+ try {
+ // Reflect change in the local store and notify subscribers
+ yield local_folder.detach_single_email_async(owned_id, out marked, null);
+ } catch (Error err) {
+ debug("%s do_replay_removed_message: unable to remove message #%s: %s", to_string(),
+ remote_position.to_string(), err.message);
+ }
+
+ // Notify queued replay operations that the email has been removed (by EmailIdentifier)
+ replay_queue.notify_remote_removed_ids(
+ Geary.iterate<ImapDB.EmailIdentifier>(owned_id).to_array_list());
+ } else {
+ debug("%s do_replay_removed_message: remote_position=%d unknown in local store "
+ + "(reported_remote_count=%d local_position=%d local_count=%d)",
+ to_string(), remote_position.value, reported_remote_count, local_position, local_count);
+ }
+
+ // for debugging
+ int new_local_count = -1;
+ try {
+ new_local_count = yield local_folder.get_email_count_async(
+ ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, null);
+ } catch (Error err) {
+ debug("%s do_replay_removed_message: error fetching new local count: %s", to_string(),
+ err.message);
+ }
+
+ // as with on_remote_appended(), only update in local store inside a queue operation, to
+ // ensure serial commits
+ try {
+ yield local_folder.update_remote_selected_message_count(reported_remote_count, null);
+ } catch (Error err) {
+ debug("%s do_replay_removed_message: unable to save removed remote count: %s", to_string(),
+ err.message);
+ }
+
+ // notify of change
+ if (!marked && owned_id != null)
+ notify_email_removed(Geary.iterate<Geary.EmailIdentifier>(owned_id).to_array_list());
+
+ if (!marked)
+ notify_email_count_changed(reported_remote_count, CountChangeReason.REMOVED);
+
+ debug("%s do_replay_remove_message: completed, current remote_count=%d "
+ + "(reported_remote_count=%d local_count=%d starting local_count=%d remote_position=%d
local_position=%d marked=%s)",
+ to_string(), remote_count, reported_remote_count, new_local_count, local_count,
remote_position.value,
+ local_position, marked.to_string());
+ }
+
+ private void on_remote_disconnected(Imap.ClientSession.DisconnectReason reason) {
+ debug("on_remote_disconnected: reason=%s", reason.to_string());
+
+ replay_queue.schedule(new ReplayDisconnect(this, reason));
+ }
+
+ //
+ // list_email_by_id variants
+ //
+
+ public override async Gee.List<Geary.Email>? list_email_by_id_async(Geary.EmailIdentifier? initial_id,
+ int count, Geary.Email.Field required_fields, Folder.ListFlags flags,
+ Cancellable? cancellable = null) throws Error {
+ Gee.List<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
+ yield do_list_email_by_id_async("list_email_by_id_async", initial_id, count, required_fields,
+ flags, accumulator, null, cancellable);
+
+ return !accumulator.is_empty ? accumulator : null;
+ }
+
+ public override void lazy_list_email_by_id(Geary.EmailIdentifier? initial_id, int count,
+ Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb,
+ Cancellable? cancellable = null) {
+ do_lazy_list_email_by_id_async.begin(initial_id, count, required_fields, flags, cb, cancellable);
+ }
+
+ private async void do_lazy_list_email_by_id_async(Geary.EmailIdentifier? initial_id, int count,
+ Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable?
cancellable) {
+ try {
+ yield do_list_email_by_id_async("lazy_list_email_by_id", initial_id, count, required_fields,
+ flags, null, cb, cancellable);
+ } catch (Error err) {
+ cb(null, err);
+ }
+ }
+
+ private async void do_list_email_by_id_async(string method, Geary.EmailIdentifier? initial_id,
+ int count, Geary.Email.Field required_fields, Folder.ListFlags flags,
+ Gee.List<Geary.Email>? accumulator, EmailCallback? cb, Cancellable? cancellable) throws Error {
+ check_open(method);
+ check_flags(method, flags);
+ if (initial_id != null)
+ check_id(method, initial_id);
+
+ if (count == 0) {
+ // signal finished
+ if (cb != null)
+ cb(null, null);
+
+ return;
+ }
+
+ // Schedule list operation and wait for completion.
+ ListEmailByID op = new ListEmailByID(this, (ImapDB.EmailIdentifier) initial_id, count,
+ required_fields, flags, accumulator, cb, cancellable);
+ replay_queue.schedule(op);
+
+ yield op.wait_for_ready_async(cancellable);
+ }
+
+ //
+ // list_email_by_sparse_id variants
+ //
+
+ public async override Gee.List<Geary.Email>? list_email_by_sparse_id_async(
+ Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields, Folder.ListFlags flags,
+ Cancellable? cancellable = null) throws Error {
+ Gee.ArrayList<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
+ yield do_list_email_by_sparse_id_async("list_email_by_sparse_id_async", ids, required_fields,
+ flags, accumulator, null, cancellable);
+
+ return (accumulator.size > 0) ? accumulator : null;
+ }
+
+ public override void lazy_list_email_by_sparse_id(Gee.Collection<Geary.EmailIdentifier> ids,
+ Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable?
cancellable = null) {
+ do_lazy_list_email_by_sparse_id_async.begin(ids, required_fields, flags, cb, cancellable);
+ }
+
+ private async void do_lazy_list_email_by_sparse_id_async(Gee.Collection<Geary.EmailIdentifier> ids,
+ Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, Cancellable?
cancellable) {
+ try {
+ yield do_list_email_by_sparse_id_async("lazy_list_email_by_sparse_id", ids, required_fields,
+ flags, null, cb, cancellable);
+ } catch (Error err) {
+ cb(null, err);
+ }
+ }
+
+ private async void do_list_email_by_sparse_id_async(string method,
+ Gee.Collection<Geary.EmailIdentifier> ids, Geary.Email.Field required_fields, Folder.ListFlags flags,
+ Gee.List<Geary.Email>? accumulator, EmailCallback? cb, Cancellable? cancellable = null) throws Error
{
+ check_open(method);
+ check_flags(method, flags);
+ check_ids(method, ids);
+
+ if (ids.size == 0) {
+ // signal finished
+ if (cb != null)
+ cb(null, null);
+
+ return;
+ }
+
+ // Schedule list operation and wait for completion.
+ // TODO: Break up requests to avoid hogging the queue
+ ListEmailBySparseID op = new ListEmailBySparseID(this, (Gee.Collection<ImapDB.EmailIdentifier>) ids,
+ required_fields, flags, accumulator, cb, cancellable);
+ replay_queue.schedule(op);
+
+ yield op.wait_for_ready_async(cancellable);
+ }
+
+ 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 {
+ check_open("fetch_email_async");
+ check_flags("fetch_email_async", flags);
+ check_id("fetch_email_async", id);
+
+ FetchEmail op = new FetchEmail(this, (ImapDB.EmailIdentifier) id, required_fields, flags,
+ cancellable);
+ replay_queue.schedule(op);
+
+ yield op.wait_for_ready_async(cancellable);
+
+ if (op.email == null) {
+ throw new EngineError.NOT_FOUND("Email %s not found in %s", id.to_string(), to_string());
+ } else if (!op.email.fields.fulfills(required_fields)) {
+ throw new EngineError.INCOMPLETE_MESSAGE("Email %s in %s does not fulfill required fields %Xh
(has %Xh)",
+ id.to_string(), to_string(), required_fields, op.email.fields);
+ }
+
+ return op.email;
+ }
+
+ // Helper function for child classes dealing with the delete/archive question. This method will
+ // mark the message as deleted and expunge it.
+ protected async void expunge_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
+ Cancellable? cancellable = null) throws Error {
+ check_open("expunge_email_async");
+ check_ids("expunge_email_async", email_ids);
+
+ RemoveEmail remove = new RemoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) email_ids,
+ cancellable);
+ replay_queue.schedule(remove);
+
+ yield remove.wait_for_ready_async(cancellable);
+ }
+
+ private void check_open(string method) throws EngineError {
+ if (open_count == 0)
+ throw new EngineError.OPEN_REQUIRED("%s failed: folder %s is not open", method, to_string());
+ }
+
+ private void check_flags(string method, Folder.ListFlags flags) throws EngineError {
+ if (flags.is_all_set(Folder.ListFlags.LOCAL_ONLY) &&
flags.is_all_set(Folder.ListFlags.FORCE_UPDATE)) {
+ throw new EngineError.BAD_PARAMETERS("%s %s failed: LOCAL_ONLY and FORCE_UPDATE are mutually
exclusive",
+ to_string(), method);
+ }
+ }
+
+ private void check_id(string method, EmailIdentifier id) throws EngineError {
+ if (!(id is ImapDB.EmailIdentifier))
+ throw new EngineError.BAD_PARAMETERS("Email ID %s is not IMAP Email ID", id.to_string());
+ }
+
+ private void check_ids(string method, Gee.Collection<EmailIdentifier> ids) throws EngineError {
+ foreach (EmailIdentifier id in ids)
+ check_id(method, id);
+ }
+
+ public virtual async void mark_email_async(Gee.List<Geary.EmailIdentifier> to_mark,
+ Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
+ Cancellable? cancellable = null) throws Error {
+ check_open("mark_email_async");
+
+ MarkEmail mark = new MarkEmail(this, to_mark, flags_to_add, flags_to_remove, cancellable);
+ replay_queue.schedule(mark);
+ yield mark.wait_for_ready_async(cancellable);
+ }
+
+ public virtual async void copy_email_async(Gee.List<Geary.EmailIdentifier> to_copy,
+ Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
+ check_open("copy_email_async");
+ check_ids("copy_email_async", to_copy);
+
+ CopyEmail copy = new CopyEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_copy, destination);
+ replay_queue.schedule(copy);
+ yield copy.wait_for_ready_async(cancellable);
+ }
+
+ public virtual async void move_email_async(Gee.List<Geary.EmailIdentifier> to_move,
+ Geary.FolderPath destination, Cancellable? cancellable = null) throws Error {
+ check_open("move_email_async");
+ check_ids("move_email_async", to_move);
+
+ MoveEmail move = new MoveEmail(this, (Gee.List<ImapDB.EmailIdentifier>) to_move, destination);
+ replay_queue.schedule(move);
+ yield move.wait_for_ready_async(cancellable);
+ }
+
+ private void on_email_flags_changed(Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> changed) {
+ notify_email_flags_changed(changed);
+ }
+
+ // TODO: A proper public search mechanism; note that this always round-trips to the remote,
+ // doesn't go through the replay queue, and doesn't deal with messages marked for deletion
+ internal async Geary.EmailIdentifier? find_earliest_email_async(DateTime datetime,
+ Geary.EmailIdentifier? before_id, Cancellable? cancellable) throws Error {
+ check_open("find_earliest_email_async");
+ if (before_id != null)
+ check_id("find_earliest_email_async", before_id);
+
+ Imap.SearchCriteria criteria = new Imap.SearchCriteria();
+ criteria.is_(Imap.SearchCriterion.since_internaldate(new
Imap.InternalDate.from_date_time(datetime)));
+
+ // if before_id available, only search for messages before it
+ if (before_id != null) {
+ Imap.UID? before_uid = yield local_folder.get_uid_async((ImapDB.EmailIdentifier) before_id,
+ ImapDB.Folder.ListFlags.NONE, cancellable);
+ if (before_uid == null) {
+ throw new EngineError.NOT_FOUND("before_id %s not found in %s", before_id.to_string(),
+ to_string());
+ }
+
+ criteria.and(Imap.SearchCriterion.message_set(
+ new Imap.MessageSet.uid_range(new Imap.UID(Imap.UID.MIN), before_uid.previous(true))));
+ }
+
+ Gee.List<Geary.Email> accumulator = new Gee.ArrayList<Geary.Email>();
+ ServerSearchEmail op = new ServerSearchEmail(this, criteria, Geary.Email.Field.NONE,
+ accumulator, cancellable);
+
+ // need to check again due to the yield in the above conditional block
+ check_open("find_earliest_email_async.schedule operation");
+
+ replay_queue.schedule(op);
+
+ if (!yield op.wait_for_ready_async(cancellable))
+ return null;
+
+ // find earliest ID; because all Email comes from Folder, UID should always be present
+ ImapDB.EmailIdentifier? earliest_id = null;
+ foreach (Geary.Email email in accumulator) {
+ ImapDB.EmailIdentifier email_id = (ImapDB.EmailIdentifier) email.id;
+
+ if (earliest_id == null || email_id.uid.compare_to(earliest_id.uid) < 0)
+ earliest_id = email_id;
+ }
+
+ return earliest_id;
+ }
+
+ protected async Geary.EmailIdentifier? create_email_async(
+ RFC822.Message rfc822, Geary.EmailFlags? flags, DateTime? date_received,
+ Geary.EmailIdentifier? id, Cancellable? cancellable = null) throws Error {
+ check_open("create_email_async");
+ if (id != null)
+ check_id("create_email_async", id);
+
+ Error? cancel_error = null;
+ Geary.EmailIdentifier? ret = null;
+ try {
+ CreateEmail create = new CreateEmail(this, rfc822, flags, date_received, cancellable);
+ replay_queue.schedule(create);
+ yield create.wait_for_ready_async(cancellable);
+
+ ret = create.created_id;
+ } catch (Error e) {
+ if (e is IOError.CANCELLED)
+ cancel_error = e;
+ else
+ throw e;
+ }
+
+ Geary.FolderSupport.Remove? remove_folder = this as Geary.FolderSupport.Remove;
+
+ // Remove old message.
+ if (id != null && remove_folder != null)
+ yield remove_folder.remove_single_email_async(id, null);
+
+ // If the user cancelled the operation, throw the error here.
+ if (cancel_error != null)
+ throw cancel_error;
+
+ // If the caller cancelled during the remove operation, delete the newly created message to
+ // safely back out.
+ if (cancellable != null && cancellable.is_cancelled() && ret != null && remove_folder != null)
+ yield remove_folder.remove_single_email_async(ret, null);
+
+ return ret;
+ }
+}
+
diff --git a/src/engine/imap-engine/imap-engine-replay-queue.vala
b/src/engine/imap-engine/imap-engine-replay-queue.vala
index 55524ed..9b78bd1 100644
--- a/src/engine/imap-engine/imap-engine-replay-queue.vala
+++ b/src/engine/imap-engine/imap-engine-replay-queue.vala
@@ -49,7 +49,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
return remote_queue.size;
} }
- private weak GenericFolder owner;
+ private weak MinimalFolder owner;
private Nonblocking.Mailbox<ReplayOperation> local_queue = new Nonblocking.Mailbox<ReplayOperation>();
private Nonblocking.Mailbox<ReplayOperation> remote_queue = new Nonblocking.Mailbox<ReplayOperation>();
private ReplayOperation? local_op_active = null;
@@ -124,7 +124,7 @@ private class Geary.ImapEngine.ReplayQueue : Geary.BaseObject {
* each ReplayOperation waiting to perform a remote operation, cancelling it if the remote
* folder is not ready.
*/
- public ReplayQueue(GenericFolder owner) {
+ public ReplayQueue(MinimalFolder owner) {
this.owner = owner;
// fire off background queue processors
diff --git a/src/engine/imap-engine/other/imap-engine-other-account.vala
b/src/engine/imap-engine/other/imap-engine-other-account.vala
index 1a4dda2..3832e1b 100644
--- a/src/engine/imap-engine/other/imap-engine-other-account.vala
+++ b/src/engine/imap-engine/other/imap-engine-other-account.vala
@@ -10,7 +10,7 @@ private class Geary.ImapEngine.OtherAccount : Geary.ImapEngine.GenericAccount {
base (name, account_information, false, remote, local);
}
- protected override GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
+ protected override MinimalFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
ImapDB.Account local_account, ImapDB.Folder local_folder) {
SpecialFolderType type;
if (Imap.MailboxSpecifier.folder_path_is_inbox(path))
@@ -18,22 +18,7 @@ private class Geary.ImapEngine.OtherAccount : Geary.ImapEngine.GenericAccount {
else
type = local_folder.get_properties().attrs.get_special_folder_type();
- switch (type) {
- case SpecialFolderType.SENT:
- return new GenericSentMailFolder(this, remote_account, local_account, local_folder,
- type);
-
- case SpecialFolderType.TRASH:
- return new GenericTrashFolder(this, remote_account, local_account, local_folder,
- type);
-
- case SpecialFolderType.DRAFTS:
- return new GenericDraftsFolder(this, remote_account, local_account, local_folder,
- type);
-
- default:
- return new OtherFolder(this, remote_account, local_account, local_folder, type);
- }
+ return new OtherFolder(this, remote_account, local_account, local_folder, type);
}
}
diff --git a/src/engine/imap-engine/other/imap-engine-other-folder.vala
b/src/engine/imap-engine/other/imap-engine-other-folder.vala
index e234590..3728faf 100644
--- a/src/engine/imap-engine/other/imap-engine-other-folder.vala
+++ b/src/engine/imap-engine/other/imap-engine-other-folder.vala
@@ -4,15 +4,10 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
-private class Geary.ImapEngine.OtherFolder : GenericFolder, Geary.FolderSupport.Remove {
+private class Geary.ImapEngine.OtherFolder : GenericFolder {
public OtherFolder(OtherAccount account, Imap.Account remote, ImapDB.Account local,
ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
base (account, remote, local, local_folder, special_folder_type);
}
-
- public async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
- Cancellable? cancellable = null) throws Error {
- yield expunge_email_async(email_ids, cancellable);
- }
}
diff --git a/src/engine/imap-engine/outlook/imap-engine-outlook-account.vala
b/src/engine/imap-engine/outlook/imap-engine-outlook-account.vala
index 8c08d17..c40d0ea 100644
--- a/src/engine/imap-engine/outlook/imap-engine-outlook-account.vala
+++ b/src/engine/imap-engine/outlook/imap-engine-outlook-account.vala
@@ -36,7 +36,7 @@ private class Geary.ImapEngine.OutlookAccount : Geary.ImapEngine.GenericAccount
base (name, account_information, false, remote, local);
}
- protected override GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
+ protected override MinimalFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
ImapDB.Account local_account, ImapDB.Folder local_folder) {
// use the Folder's attributes to determine if it's a special folder type, unless it's
// INBOX; that's determined by name
@@ -46,23 +46,10 @@ private class Geary.ImapEngine.OutlookAccount : Geary.ImapEngine.GenericAccount
else
special_folder_type = local_folder.get_properties().attrs.get_special_folder_type();
- // generate properly-interfaced Folder depending on the special type
- // Proper Drafts support depends on Outlook.com supporting UIDPLUS or us devising another
- // mechanism to associate new messages with drafts-in-progress; see
- // http://redmine.yorba.org/issues/7495
- switch (special_folder_type) {
- case SpecialFolderType.SENT:
- return new GenericSentMailFolder(this, remote_account, local_account, local_folder,
- special_folder_type);
-
- case SpecialFolderType.TRASH:
- return new GenericTrashFolder(this, remote_account, local_account, local_folder,
- special_folder_type);
-
- default:
- return new OutlookFolder(this, remote_account, local_account, local_folder,
- special_folder_type);
- }
+ if (special_folder_type == Geary.SpecialFolderType.DRAFTS)
+ return new OutlookDraftsFolder(this, remote_account, local_account, local_folder,
special_folder_type);
+
+ return new OutlookFolder(this, remote_account, local_account, local_folder, special_folder_type);
}
}
diff --git a/src/engine/imap-engine/outlook/imap-engine-outlook-drafts-folder.vala
b/src/engine/imap-engine/outlook/imap-engine-outlook-drafts-folder.vala
new file mode 100644
index 0000000..605808d
--- /dev/null
+++ b/src/engine/imap-engine/outlook/imap-engine-outlook-drafts-folder.vala
@@ -0,0 +1,19 @@
+/* Copyright 2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * Since Outlook doesn't support UIDPLUS, we can't delete old drafts before
+ * saving a new one. Instead of allowing their drafts folder to fill up with
+ * countless revisions of every message, we simply don't expose the
+ * Geary.FolderSupport.Create interface from the drafts folder, so nothing gets
+ * saved at all.
+ */
+private class Geary.ImapEngine.OutlookDraftsFolder : MinimalFolder {
+ public OutlookDraftsFolder(OutlookAccount account, Imap.Account remote, ImapDB.Account local,
+ ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
+ base (account, remote, local, local_folder, special_folder_type);
+ }
+}
diff --git a/src/engine/imap-engine/outlook/imap-engine-outlook-folder.vala
b/src/engine/imap-engine/outlook/imap-engine-outlook-folder.vala
index 8c9a2bc..b65e3bc 100644
--- a/src/engine/imap-engine/outlook/imap-engine-outlook-folder.vala
+++ b/src/engine/imap-engine/outlook/imap-engine-outlook-folder.vala
@@ -4,15 +4,10 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
-private class Geary.ImapEngine.OutlookFolder : GenericFolder, Geary.FolderSupport.Remove {
+private class Geary.ImapEngine.OutlookFolder : GenericFolder {
public OutlookFolder(OutlookAccount account, Imap.Account remote, ImapDB.Account local,
ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
base (account, remote, local, local_folder, special_folder_type);
}
-
- public async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
- Cancellable? cancellable = null) throws Error {
- yield expunge_email_async(email_ids, cancellable);
- }
}
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala
index 57da967..ebe90c0 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-abstract-list-email.vala
@@ -7,7 +7,7 @@
private abstract class Geary.ImapEngine.AbstractListEmail : Geary.ImapEngine.SendReplayOperation {
private class RemoteBatchOperation : Nonblocking.BatchOperation {
// IN
- public GenericFolder owner;
+ public MinimalFolder owner;
public Imap.MessageSet msg_set;
public Geary.Email.Field unfulfilled_fields;
public Geary.Email.Field required_fields;
@@ -15,7 +15,7 @@ private abstract class Geary.ImapEngine.AbstractListEmail : Geary.ImapEngine.Sen
// OUT
public Gee.Set<Geary.EmailIdentifier> created_ids = new Gee.HashSet<Geary.EmailIdentifier>();
- public RemoteBatchOperation(GenericFolder owner, Imap.MessageSet msg_set,
+ public RemoteBatchOperation(MinimalFolder owner, Imap.MessageSet msg_set,
Geary.Email.Field unfulfilled_fields, Geary.Email.Field required_fields) {
this.owner = owner;
this.msg_set = msg_set;
@@ -53,7 +53,7 @@ private abstract class Geary.ImapEngine.AbstractListEmail : Geary.ImapEngine.Sen
}
}
- protected GenericFolder owner;
+ protected MinimalFolder owner;
protected Geary.Email.Field required_fields;
protected Gee.List<Geary.Email>? accumulator = null;
protected weak EmailCallback? cb;
@@ -62,7 +62,7 @@ private abstract class Geary.ImapEngine.AbstractListEmail : Geary.ImapEngine.Sen
private Gee.HashMap<Imap.UID, Geary.Email.Field> unfulfilled = new Gee.HashMap<Imap.UID,
Geary.Email.Field>();
- public AbstractListEmail(string name, GenericFolder owner, Geary.Email.Field required_fields,
+ public AbstractListEmail(string name, MinimalFolder owner, Geary.Email.Field required_fields,
Folder.ListFlags flags, Gee.List<Geary.Email>? accumulator, EmailCallback? cb,
Cancellable? cancellable) {
base(name);
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
index 422d0af..9fec2ae 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-copy-email.vala
@@ -5,12 +5,12 @@
*/
private class Geary.ImapEngine.CopyEmail : Geary.ImapEngine.SendReplayOperation {
- private GenericFolder engine;
+ private MinimalFolder engine;
private Gee.HashSet<ImapDB.EmailIdentifier> to_copy = new Gee.HashSet<ImapDB.EmailIdentifier>();
private Geary.FolderPath destination;
private Cancellable? cancellable;
- public CopyEmail(GenericFolder engine, Gee.List<ImapDB.EmailIdentifier> to_copy,
+ public CopyEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_copy,
Geary.FolderPath destination, Cancellable? cancellable = null) {
base("CopyEmail");
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala
index 4477df2..b27170b 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-create-email.vala
@@ -7,13 +7,13 @@
private class Geary.ImapEngine.CreateEmail : Geary.ImapEngine.SendReplayOperation {
public Geary.EmailIdentifier? created_id { get; private set; default = null; }
- private GenericFolder engine;
+ private MinimalFolder engine;
private RFC822.Message rfc822;
private Geary.EmailFlags? flags;
private DateTime? date_received;
private Cancellable? cancellable;
- public CreateEmail(GenericFolder engine, RFC822.Message rfc822, Geary.EmailFlags? flags,
+ public CreateEmail(MinimalFolder engine, RFC822.Message rfc822, Geary.EmailFlags? flags,
DateTime? date_received, Cancellable? cancellable) {
base.only_remote("CreateEmail");
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-fetch-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-fetch-email.vala
index 5a92736..a1c6f78 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-fetch-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-fetch-email.vala
@@ -7,7 +7,7 @@
private class Geary.ImapEngine.FetchEmail : Geary.ImapEngine.SendReplayOperation {
public Email? email = null;
- private GenericFolder engine;
+ private MinimalFolder engine;
private ImapDB.EmailIdentifier id;
private Email.Field required_fields;
private Email.Field remaining_fields;
@@ -16,7 +16,7 @@ private class Geary.ImapEngine.FetchEmail : Geary.ImapEngine.SendReplayOperation
private Imap.UID? uid = null;
private bool remote_removed = false;
- public FetchEmail(GenericFolder engine, ImapDB.EmailIdentifier id, Email.Field required_fields,
+ public FetchEmail(MinimalFolder engine, ImapDB.EmailIdentifier id, Email.Field required_fields,
Folder.ListFlags flags, Cancellable? cancellable) {
base ("FetchEmail");
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala
b/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala
index adc7b03..bd3f3d1 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-id.vala
@@ -10,7 +10,7 @@ private class Geary.ImapEngine.ListEmailByID : Geary.ImapEngine.AbstractListEmai
private int fulfilled_count = 0;
private Imap.UID? initial_uid = null;
- public ListEmailByID(GenericFolder owner, ImapDB.EmailIdentifier? initial_id, int count,
+ public ListEmailByID(MinimalFolder owner, ImapDB.EmailIdentifier? initial_id, int count,
Geary.Email.Field required_fields, Folder.ListFlags flags, Gee.List<Geary.Email>? accumulator,
EmailCallback? cb, Cancellable? cancellable) {
base ("ListEmailByID", owner, required_fields, flags, accumulator, cb, cancellable);
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-sparse-id.vala
b/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-sparse-id.vala
index 02534a1..c7e42d4 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-sparse-id.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-list-email-by-sparse-id.vala
@@ -7,7 +7,7 @@
private class Geary.ImapEngine.ListEmailBySparseID : Geary.ImapEngine.AbstractListEmail {
private Gee.HashSet<ImapDB.EmailIdentifier> ids = new Gee.HashSet<ImapDB.EmailIdentifier>();
- public ListEmailBySparseID(GenericFolder owner, Gee.Collection<ImapDB.EmailIdentifier> ids,
+ public ListEmailBySparseID(MinimalFolder owner, Gee.Collection<ImapDB.EmailIdentifier> ids,
Geary.Email.Field required_fields, Folder.ListFlags flags, Gee.List<Geary.Email>? accumulator,
EmailCallback cb, Cancellable? cancellable) {
base ("ListEmailBySparseID", owner, required_fields, flags, accumulator, cb, cancellable);
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-mark-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-mark-email.vala
index e2a3fe3..e40f3fa 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-mark-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-mark-email.vala
@@ -5,14 +5,14 @@
*/
private class Geary.ImapEngine.MarkEmail : Geary.ImapEngine.SendReplayOperation {
- private GenericFolder engine;
+ private MinimalFolder engine;
private Gee.List<Geary.EmailIdentifier> to_mark = new Gee.ArrayList<Geary.EmailIdentifier>();
private Geary.EmailFlags? flags_to_add;
private Geary.EmailFlags? flags_to_remove;
private Gee.Map<ImapDB.EmailIdentifier, Geary.EmailFlags>? original_flags = null;
private Cancellable? cancellable;
- public MarkEmail(GenericFolder engine, Gee.List<Geary.EmailIdentifier> to_mark,
+ public MarkEmail(MinimalFolder engine, Gee.List<Geary.EmailIdentifier> to_mark,
Geary.EmailFlags? flags_to_add, Geary.EmailFlags? flags_to_remove,
Cancellable? cancellable = null) {
base("MarkEmail");
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
index c3f7c62..40157d3 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email.vala
@@ -5,14 +5,14 @@
*/
private class Geary.ImapEngine.MoveEmail : Geary.ImapEngine.SendReplayOperation {
- private GenericFolder engine;
+ private MinimalFolder engine;
private Gee.List<ImapDB.EmailIdentifier> to_move = new Gee.ArrayList<ImapDB.EmailIdentifier>();
private Geary.FolderPath destination;
private Cancellable? cancellable;
private Gee.Set<ImapDB.EmailIdentifier>? moved_ids = null;
private int original_count = 0;
- public MoveEmail(GenericFolder engine, Gee.List<ImapDB.EmailIdentifier> to_move,
+ public MoveEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_move,
Geary.FolderPath destination, Cancellable? cancellable = null) {
base("MoveEmail");
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
index f26f85c..b2a5ab3 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
@@ -5,13 +5,13 @@
*/
private class Geary.ImapEngine.RemoveEmail : Geary.ImapEngine.SendReplayOperation {
- private GenericFolder engine;
+ private MinimalFolder engine;
private Gee.List<ImapDB.EmailIdentifier> to_remove = new Gee.ArrayList<ImapDB.EmailIdentifier>();
private Cancellable? cancellable;
private Gee.Set<ImapDB.EmailIdentifier>? removed_ids = null;
private int original_count = 0;
- public RemoveEmail(GenericFolder engine, Gee.List<ImapDB.EmailIdentifier> to_remove,
+ public RemoveEmail(MinimalFolder engine, Gee.List<ImapDB.EmailIdentifier> to_remove,
Cancellable? cancellable = null) {
base("RemoveEmail");
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-replay-append.vala
b/src/engine/imap-engine/replay-ops/imap-engine-replay-append.vala
index 874847d..6d561be 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-replay-append.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-replay-append.vala
@@ -5,11 +5,11 @@
*/
private class Geary.ImapEngine.ReplayAppend : Geary.ImapEngine.ReplayOperation {
- public GenericFolder owner;
+ public MinimalFolder owner;
public int remote_count;
public Gee.List<Imap.SequenceNumber> positions;
- public ReplayAppend(GenericFolder owner, int remote_count, Gee.List<Imap.SequenceNumber> positions) {
+ public ReplayAppend(MinimalFolder owner, int remote_count, Gee.List<Imap.SequenceNumber> positions) {
base ("Append", Scope.REMOTE_ONLY);
this.owner = owner;
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-replay-disconnect.vala
b/src/engine/imap-engine/replay-ops/imap-engine-replay-disconnect.vala
index f4590c0..9ac2663 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-replay-disconnect.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-replay-disconnect.vala
@@ -5,10 +5,10 @@
*/
private class Geary.ImapEngine.ReplayDisconnect : Geary.ImapEngine.ReplayOperation {
- public GenericFolder owner;
+ public MinimalFolder owner;
public Imap.ClientSession.DisconnectReason reason;
- public ReplayDisconnect(GenericFolder owner, Imap.ClientSession.DisconnectReason reason) {
+ public ReplayDisconnect(MinimalFolder owner, Imap.ClientSession.DisconnectReason reason) {
base ("Disconnect", Scope.LOCAL_ONLY);
this.owner = owner;
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-replay-removal.vala
b/src/engine/imap-engine/replay-ops/imap-engine-replay-removal.vala
index 0690bb5..4195168 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-replay-removal.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-replay-removal.vala
@@ -5,11 +5,11 @@
*/
private class Geary.ImapEngine.ReplayRemoval : Geary.ImapEngine.ReplayOperation {
- public GenericFolder owner;
+ public MinimalFolder owner;
public int remote_count;
public Imap.SequenceNumber position;
- public ReplayRemoval(GenericFolder owner, int remote_count, Imap.SequenceNumber position) {
+ public ReplayRemoval(MinimalFolder owner, int remote_count, Imap.SequenceNumber position) {
base ("Removal", Scope.LOCAL_ONLY);
this.owner = owner;
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-server-search-email.vala
b/src/engine/imap-engine/replay-ops/imap-engine-server-search-email.vala
index d5b7c8f..5a96e0c 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-server-search-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-server-search-email.vala
@@ -13,7 +13,7 @@
private class Geary.ImapEngine.ServerSearchEmail : Geary.ImapEngine.AbstractListEmail {
private Imap.SearchCriteria criteria;
- public ServerSearchEmail(GenericFolder owner, Imap.SearchCriteria criteria, Geary.Email.Field
required_fields,
+ public ServerSearchEmail(MinimalFolder owner, Imap.SearchCriteria criteria, Geary.Email.Field
required_fields,
Gee.List<Geary.Email>? accumulator, Cancellable? cancellable) {
// OLDEST_TO_NEWEST used for vector expansion, if necessary
base ("ServerSearchEmail", owner, required_fields, Geary.Folder.ListFlags.OLDEST_TO_NEWEST,
diff --git a/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala
b/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala
index 4a91945..68958e5 100644
--- a/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala
+++ b/src/engine/imap-engine/yahoo/imap-engine-yahoo-account.vala
@@ -48,27 +48,12 @@ private class Geary.ImapEngine.YahooAccount : Geary.ImapEngine.GenericAccount {
}
}
- protected override GenericFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
+ protected override MinimalFolder new_folder(Geary.FolderPath path, Imap.Account remote_account,
ImapDB.Account local_account, ImapDB.Folder local_folder) {
SpecialFolderType special_folder_type = special_map.has_key(path) ? special_map.get(path)
: Geary.SpecialFolderType.NONE;
- switch (special_folder_type) {
- case SpecialFolderType.SENT:
- return new GenericSentMailFolder(this, remote_account, local_account, local_folder,
- special_folder_type);
-
- case SpecialFolderType.TRASH:
- return new GenericTrashFolder(this, remote_account, local_account, local_folder,
- special_folder_type);
-
- case SpecialFolderType.DRAFTS:
- return new GenericDraftsFolder(this, remote_account, local_account, local_folder,
- special_folder_type);
-
- default:
- return new YahooFolder(this, remote_account, local_account, local_folder,
- special_folder_type);
- }
+ return new YahooFolder(this, remote_account, local_account, local_folder,
+ special_folder_type);
}
}
diff --git a/src/engine/imap-engine/yahoo/imap-engine-yahoo-folder.vala
b/src/engine/imap-engine/yahoo/imap-engine-yahoo-folder.vala
index 50d9469..3c97019 100644
--- a/src/engine/imap-engine/yahoo/imap-engine-yahoo-folder.vala
+++ b/src/engine/imap-engine/yahoo/imap-engine-yahoo-folder.vala
@@ -4,15 +4,10 @@
* (version 2.1 or later). See the COPYING file in this distribution.
*/
-private class Geary.ImapEngine.YahooFolder : GenericFolder, Geary.FolderSupport.Remove {
+private class Geary.ImapEngine.YahooFolder : GenericFolder {
public YahooFolder(YahooAccount account, Imap.Account remote, ImapDB.Account local,
ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
base (account, remote, local, local_folder, special_folder_type);
}
-
- public async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
- Cancellable? cancellable = null) throws Error {
- yield expunge_email_async(email_ids, cancellable);
- }
}
diff --git a/src/engine/imap/api/imap-account.vala b/src/engine/imap/api/imap-account.vala
index 7e46c04..090e582 100644
--- a/src/engine/imap/api/imap-account.vala
+++ b/src/engine/imap/api/imap-account.vala
@@ -152,6 +152,18 @@ private class Geary.Imap.Account : BaseObject {
return path_to_mailbox.has_key(path);
}
+ public async void create_folder_async(FolderPath path, Cancellable? cancellable) throws Error {
+ check_open();
+
+ StatusResponse response = yield send_command_async(new CreateCommand(
+ new MailboxSpecifier.from_folder_path(path, null)), null, null, cancellable);
+
+ if (response.status != Status.OK) {
+ throw new ImapError.SERVER_ERROR("Server reports error creating path %s: %s", path.to_string(),
+ response.to_string());
+ }
+ }
+
public async Imap.Folder fetch_folder_async(FolderPath path, Cancellable? cancellable)
throws Error {
check_open();
diff --git a/src/engine/imap/command/imap-create-command.vala
b/src/engine/imap/command/imap-create-command.vala
new file mode 100644
index 0000000..83ff2be
--- /dev/null
+++ b/src/engine/imap/command/imap-create-command.vala
@@ -0,0 +1,23 @@
+/* Copyright 2011-2014 Yorba Foundation
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later). See the COPYING file in this distribution.
+ */
+
+/**
+ * See [[http://tools.ietf.org/html/rfc3501#section-6.3.3]]
+ */
+
+public class Geary.Imap.CreateCommand : Command {
+ public const string NAME = "create";
+
+ public MailboxSpecifier mailbox { get; private set; }
+
+ public CreateCommand(MailboxSpecifier mailbox) {
+ base (NAME);
+
+ this.mailbox = mailbox;
+
+ add(mailbox.to_parameter());
+ }
+}
diff --git a/src/engine/util/util-iterable.vala b/src/engine/util/util-iterable.vala
index 10ad20a..bccabb3 100644
--- a/src/engine/util/util-iterable.vala
+++ b/src/engine/util/util-iterable.vala
@@ -27,6 +27,13 @@ namespace Geary {
return Geary.traverse<G>(list);
}
+
+ /**
+ * Take an array of items and return a Geary.Iterable for convenience.
+ */
+ public Geary.Iterable<G> iterate_array<G>(G[] a) {
+ return Geary.traverse<G>(new Gee.ArrayList<G>.wrap(a));
+ }
}
/**
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]