From 6963063fd5c112858daab32d432086279454d202 Mon Sep 17 00:00:00 2001 From: Eric Gregory Date: Mon, 11 Jun 2012 12:03:57 -0700 Subject: [PATCH] Send outgoing messages via Outbox folder: Closes #4569 Squashed commit of many patches that merged Eric's outbox patch as well as additional changes to upgrade the database rather than require it be wiped and some refactoring suggested by the Outbox implementation. Also updated Outbox to be fully atomic via Transactions. --- sql/Version-003.sql | 13 + src/CMakeLists.txt | 5 + src/client/geary-application.vala | 26 +- src/client/geary-controller.vala | 25 +- src/client/ui/folder-list.vala | 7 +- src/client/ui/message-list-cell-renderer.vala | 7 +- src/engine/api/geary-account-information.vala | 15 +- src/engine/api/geary-account.vala | 37 ++- .../api/geary-conversation-monitor.vala | 7 +- src/engine/api/geary-email-identifier.vala | 22 +- src/engine/api/geary-email.vala | 14 + src/engine/api/geary-folder.vala | 2 +- src/engine/api/geary-logging.vala | 3 +- src/engine/api/geary-special-folder.vala | 3 +- src/engine/imap/api/imap-account.vala | 14 +- .../imap/api/imap-email-identifier.vala | 7 +- src/engine/impl/geary-abstract-account.vala | 12 +- src/engine/impl/geary-abstract-folder.vala | 66 +++- .../impl/geary-generic-imap-account.vala | 63 +++- .../impl/geary-generic-imap-folder.vala | 5 +- src/engine/impl/geary-gmail-account.vala | 13 +- src/engine/impl/geary-yahoo-account.vala | 6 +- .../outbox/smtp-outbox-email-identifier.vala | 23 ++ .../outbox/smtp-outbox-email-properties.vala | 15 + .../impl/outbox/smtp-outbox-folder.vala | 307 ++++++++++++++++++ src/engine/rfc822/rfc822-message-data.vala | 4 + src/engine/rfc822/rfc822-message.vala | 45 ++- .../sqlite/abstract/sqlite-database.vala | 13 +- .../sqlite/abstract/sqlite-transaction.vala | 27 +- src/engine/sqlite/api/sqlite-account.vala | 65 +++- .../sqlite/email/sqlite-mail-database.vala | 11 +- .../email/sqlite-message-location-table.vala | 2 +- .../sqlite/imap/sqlite-imap-database.vala | 2 +- .../sqlite/smtp/sqlite-smtp-outbox-row.vala | 38 +++ .../sqlite/smtp/sqlite-smtp-outbox-table.vala | 196 +++++++++++ src/engine/util/util-string.vala | 15 + 36 files changed, 1035 insertions(+), 100 deletions(-) create mode 100644 sql/Version-003.sql create mode 100644 src/engine/impl/outbox/smtp-outbox-email-identifier.vala create mode 100644 src/engine/impl/outbox/smtp-outbox-email-properties.vala create mode 100644 src/engine/impl/outbox/smtp-outbox-folder.vala create mode 100644 src/engine/sqlite/smtp/sqlite-smtp-outbox-row.vala create mode 100644 src/engine/sqlite/smtp/sqlite-smtp-outbox-table.vala diff --git a/sql/Version-003.sql b/sql/Version-003.sql new file mode 100644 index 00000000..aa65929d --- /dev/null +++ b/sql/Version-003.sql @@ -0,0 +1,13 @@ + +-- +-- SmtpOutboxTable +-- + +CREATE TABLE SmtpOutboxTable ( + id INTEGER PRIMARY KEY, + ordering INTEGER, + message TEXT +); + +CREATE INDEX SmtpOutboxOrderingIndex ON SmtpOutboxTable(ordering ASC); + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 31bf8413..34b4fcc4 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -96,6 +96,9 @@ engine/impl/geary-receive-replay-operations.vala engine/impl/geary-replay-operation.vala engine/impl/geary-replay-queue.vala engine/impl/geary-send-replay-operations.vala +engine/impl/outbox/smtp-outbox-folder.vala +engine/impl/outbox/smtp-outbox-email-identifier.vala +engine/impl/outbox/smtp-outbox-email-properties.vala engine/nonblocking/nonblocking-abstract-semaphore.vala engine/nonblocking/nonblocking-batch.vala @@ -146,6 +149,8 @@ engine/sqlite/imap/sqlite-imap-folder-properties-row.vala engine/sqlite/imap/sqlite-imap-folder-properties-table.vala engine/sqlite/imap/sqlite-imap-message-properties-row.vala engine/sqlite/imap/sqlite-imap-message-properties-table.vala +engine/sqlite/smtp/sqlite-smtp-outbox-row.vala +engine/sqlite/smtp/sqlite-smtp-outbox-table.vala engine/state/state-machine-descriptor.vala engine/state/state-machine.vala diff --git a/src/client/geary-application.vala b/src/client/geary-application.vala index ef067440..115b89d8 100644 --- a/src/client/geary-application.vala +++ b/src/client/geary-application.vala @@ -86,6 +86,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc., static bool log_replay_queue = false; static bool log_conversations = false; static bool log_periodic = false; + static bool log_transactions = false; static bool version = false; const OptionEntry[] options = { { "debug", 0, 0, OptionArg.NONE, ref log_debug, N_("Output debugging information"), null }, @@ -94,6 +95,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc., { "log-replay-queue", 0, 0, OptionArg.NONE, ref log_replay_queue, N_("Output replay queue log"), null }, { "log-serializer", 0, 0, OptionArg.NONE, ref log_serializer, N_("Output serializer log"), null }, { "log-periodic", 0, 0, OptionArg.NONE, ref log_periodic, N_("Output periodic activity"), null }, + { "log-transactions", 0, 0, OptionArg.NONE, ref log_transactions, N_("Output database transactions"), null }, { "version", 'V', 0, OptionArg.NONE, ref version, N_("Display program version"), null }, { null } }; @@ -132,6 +134,9 @@ along with Geary; if not, write to the Free Software Foundation, Inc., if (log_periodic) Geary.Logging.enable_flags(Geary.Logging.Flag.PERIODIC); + if (log_transactions) + Geary.Logging.enable_flags(Geary.Logging.Flag.TRANSACTIONS); + if (log_debug) Geary.Logging.log_to(stdout); @@ -177,7 +182,7 @@ along with Geary; if not, write to the Free Software Foundation, Inc., if (this.account != null) this.account.report_problem.connect(on_report_problem); - controller.connect_account(this.account); + controller.connect_account_async.begin(this.account, null); } private void initialize_account() { @@ -228,7 +233,12 @@ along with Geary; if not, write to the Free Software Foundation, Inc., if (success) { account_information.store_async.begin(cancellable); - set_account(account_information.get_account()); + try { + set_account(account_information.get_account()); + } catch (Error err) { + // TODO: Handle more gracefully + error("Unable to retrieve email account: %s", err.message); + } } else { Geary.AccountInformation new_account_information = request_account_information(account_information); @@ -265,7 +275,13 @@ along with Geary; if not, write to the Free Software Foundation, Inc., account_information.store_async.begin(cancellable); account_information.credentials.pass = password; - set_account(account_information.get_account()); + + try { + set_account(account_information.get_account()); + } catch (Error err) { + // TODO: Handle more gracefull + error("Unable to retrieve email account: %s", err.message); + } } private string? get_username() { @@ -374,7 +390,9 @@ along with Geary; if not, write to the Free Software Foundation, Inc., public override bool exiting(bool panicked) { if (controller.main_window != null) controller.main_window.destroy(); - + + controller.disconnect_account_async.begin(null); + return true; } diff --git a/src/client/geary-controller.vala b/src/client/geary-controller.vala index 3a068efb..97bcf34c 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -120,7 +120,7 @@ public class GearyController { } ~GearyController() { - connect_account(null); + assert(account == null); } private void add_accelerator(string accelerator, string action) { @@ -242,7 +242,7 @@ public class GearyController { return entries; } - public void connect_account(Geary.EngineAccount? new_account) { + public async void connect_account_async(Geary.EngineAccount? new_account, Cancellable? cancellable) { if (account == new_account) return; @@ -261,12 +261,29 @@ public class GearyController { main_window.title = GearyApplication.NAME; main_window.folder_list.remove_all_branches(); + + try { + yield account.close_async(cancellable); + } catch (Error close_err) { + debug("Unable to close account %s: %s", account.to_string(), close_err.message); + } } account = new_account; // Connect the new account, if any. if (account != null) { + try { + yield account.open_async(cancellable); + } catch (Error open_err) { + // TODO: Better error reporting to user + debug("Unable to open account %s: %s", account.to_string(), open_err.message); + + account = null; + + GearyApplication.instance.panic(); + } + account.folders_added_removed.connect(on_folders_added_removed); // Personality-specific setup. @@ -285,6 +302,10 @@ public class GearyController { } } + public async void disconnect_account_async(Cancellable? cancellable) throws Error { + yield connect_account_async(null, cancellable); + } + private bool is_viewed_conversation(Geary.Conversation? conversation) { return conversation != null && selected_conversations.length > 0 && selected_conversations[0] == conversation; diff --git a/src/client/ui/folder-list.vala b/src/client/ui/folder-list.vala index f1e2c698..646d3750 100644 --- a/src/client/ui/folder-list.vala +++ b/src/client/ui/folder-list.vala @@ -87,10 +87,13 @@ public class FolderList : Sidebar.Tree { case Geary.SpecialFolderType.SPAM: return new ThemedIcon("mail-mark-junk"); - + case Geary.SpecialFolderType.TRASH: return new ThemedIcon("user-trash"); - + + case Geary.SpecialFolderType.OUTBOX: + return new ThemedIcon("mail-outbox"); + default: assert_not_reached(); } diff --git a/src/client/ui/message-list-cell-renderer.vala b/src/client/ui/message-list-cell-renderer.vala index f5c726f1..fda7d1e9 100644 --- a/src/client/ui/message-list-cell-renderer.vala +++ b/src/client/ui/message-list-cell-renderer.vala @@ -42,9 +42,10 @@ public class FormattedMessageData : Object { public FormattedMessageData.from_email(Geary.Email email, int num_emails, bool unread, bool flagged, Geary.Folder folder) { assert(email.fields.fulfills(MessageListStore.REQUIRED_FIELDS)); - + string who = ""; - if (folder.get_special_folder_type() == Geary.SpecialFolderType.SENT && + if ((folder.get_special_folder_type() == Geary.SpecialFolderType.SENT || + folder.get_special_folder_type() == Geary.SpecialFolderType.OUTBOX) && email.to != null && email.to.size > 0) { who = email.to[0].get_short_address(); } else if (email.from != null && email.from.size > 0) { @@ -147,7 +148,7 @@ public class FormattedMessageData : Object { cell_area.y + LINE_SPACING); ctx.paint(); } - + // Unread indicator. if (is_unread) { Gdk.cairo_set_source_pixbuf(ctx, IconFactory.instance.unread, cell_area.x + LINE_SPACING, diff --git a/src/engine/api/geary-account-information.vala b/src/engine/api/geary-account-information.vala index a8f77b2a..921b4039 100644 --- a/src/engine/api/geary-account-information.vala +++ b/src/engine/api/geary-account-information.vala @@ -109,22 +109,20 @@ public class Geary.AccountInformation : Object { return false; } - public Geary.EngineAccount get_account() { - File? user_data_dir = Geary.Engine.user_data_dir; - File? resource_dir = Geary.Engine.resource_dir; + public Geary.EngineAccount get_account() throws Error { Geary.Sqlite.Account sqlite_account = - new Geary.Sqlite.Account(credentials, user_data_dir, resource_dir); + new Geary.Sqlite.Account(credentials.user); switch (service_provider) { case ServiceProvider.GMAIL: return new GmailAccount("Gmail account %s".printf(credentials.to_string()), - credentials.user, this, user_data_dir, new Geary.Imap.Account( + credentials.user, this, Engine.user_data_dir, new Geary.Imap.Account( GmailAccount.IMAP_ENDPOINT, GmailAccount.SMTP_ENDPOINT, credentials, this), sqlite_account); case ServiceProvider.YAHOO: return new YahooAccount("Yahoo account %s".printf(credentials.to_string()), - credentials.user, this, user_data_dir, new Geary.Imap.Account( + credentials.user, this, Engine.user_data_dir, new Geary.Imap.Account( YahooAccount.IMAP_ENDPOINT, YahooAccount.SMTP_ENDPOINT, credentials, this), sqlite_account); @@ -142,11 +140,12 @@ public class Geary.AccountInformation : Object { smtp_flags, Smtp.ClientConnection.DEFAULT_TIMEOUT_SEC); return new OtherAccount("Other account %s".printf(credentials.to_string()), - credentials.user, this, user_data_dir, new Geary.Imap.Account(imap_endpoint, + credentials.user, this, Engine.user_data_dir, new Geary.Imap.Account(imap_endpoint, smtp_endpoint, credentials, this), sqlite_account); default: - assert_not_reached(); + throw new EngineError.NOT_FOUND("Service provider of type %s not known", + service_provider.to_string()); } } diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala index 6ca4d277..f3ced01f 100644 --- a/src/engine/api/geary-account.vala +++ b/src/engine/api/geary-account.vala @@ -12,29 +12,47 @@ public interface Geary.Account : Object { DATABASE_FAILURE } + public signal void opened(); + + public signal void closed(); + public signal void report_problem(Geary.Account.Problem problem, Geary.Credentials? credentials, Error? err); public signal void folders_added_removed(Gee.Collection? added, Gee.Collection? removed); + /** + * Signal notification method for subclasses to use. + */ + protected abstract void notify_opened(); + + /** + * Signal notification method for subclasses to use. + */ + protected abstract void notify_closed(); + + /** + * Signal notification method for subclasses to use. + */ protected abstract void notify_report_problem(Geary.Account.Problem problem, Geary.Credentials? credentials, Error? err); + /** + * Signal notification method for subclasses to use. + */ protected abstract void notify_folders_added_removed(Gee.Collection? added, Gee.Collection? removed); /** - * This method returns which Geary.Email.Field fields must be available in a Geary.Email to - * write (or save or store) the message to the backing medium. Different implementations will - * have different requirements, which must be reconciled. * - * In this case, Geary.Email.Field.NONE means "any". - * - * If a write operation is attempted on an email that does not have all these fields fulfilled, - * an EngineError.INCOMPLETE_MESSAGE will be thrown. */ - public abstract Geary.Email.Field get_required_fields_for_writing(); + public abstract async void open_async(Cancellable? cancellable = null) throws Error; + + /** + * + */ + public abstract async void close_async(Cancellable? cancellable = null) throws Error; /** * Lists all the folders found under the parent path unless it's null, in which case it lists @@ -60,6 +78,9 @@ public interface Geary.Account : Object { public abstract async Geary.Folder fetch_folder_async(Geary.FolderPath path, Cancellable? cancellable = null) throws Error; + /** + * Used only for debugging. Should not be used for user-visible strings. + */ public abstract string to_string(); } diff --git a/src/engine/api/geary-conversation-monitor.vala b/src/engine/api/geary-conversation-monitor.vala index 0e72d7a4..084228f4 100644 --- a/src/engine/api/geary-conversation-monitor.vala +++ b/src/engine/api/geary-conversation-monitor.vala @@ -104,7 +104,9 @@ public class Geary.ConversationMonitor : Object { id_ascending.remove(email); id_descending.remove(email); - message_ids.remove_all(email.get_ancestors()); + Gee.Set? ancestors = email.get_ancestors(); + if (ancestors != null) + message_ids.remove_all(ancestors); } private static int compare_date_ascending(Email a, Email b) { @@ -491,6 +493,9 @@ public class Geary.ConversationMonitor : Object { } private void process_email(Gee.List emails) { + Logging.debug(Logging.Flag.CONVERSATIONS, "[%s] ConversationMonitor::process_email: %d emails", + folder.to_string(), emails.size); + Gee.HashSet new_conversations = new Gee.HashSet(); Gee.MultiMap appended_conversations = new Gee.HashMultiMap< Conversation, Geary.Email>(); diff --git a/src/engine/api/geary-email-identifier.vala b/src/engine/api/geary-email-identifier.vala index 880078f8..409aee1c 100644 --- a/src/engine/api/geary-email-identifier.vala +++ b/src/engine/api/geary-email-identifier.vala @@ -17,15 +17,27 @@ * fields in the same Folder that match the email's position within it. The ordering field may * or may not be the actual unique identifier; in IMAP, for example, it is, while in other systems * it may not be. + * + * TODO: + * EmailIdentifier currently does not verify it is in the same Folder as the other EmailIdentifier + * passed to equals() and compare(). This may be added in the future. */ public abstract class Geary.EmailIdentifier : Object, Geary.Equalable, Geary.Comparable, Geary.Hashable { - public abstract int64 ordering { get; protected set; } + public int64 ordering { get; protected set; } + protected EmailIdentifier(int64 ordering) { + this.ordering = ordering; + } + + public virtual uint to_hash() { + return Geary.Hashable.int64_hash(ordering); + } + + // Virtual default implementation not provided because base class *must* verify that the + // Equalable is of its own type. public abstract bool equals(Geary.Equalable other); - public abstract uint to_hash(); - public virtual int compare(Geary.Comparable o) { Geary.EmailIdentifier? other = o as Geary.EmailIdentifier; if (other == null) @@ -43,6 +55,8 @@ public abstract class Geary.EmailIdentifier : Object, Geary.Equalable, Geary.Com return 0; } - public abstract string to_string(); + public virtual string to_string() { + return ordering.to_string(); + } } diff --git a/src/engine/api/geary-email.vala b/src/engine/api/geary-email.vala index 3d48f402..d1ae7c7c 100644 --- a/src/engine/api/geary-email.vala +++ b/src/engine/api/geary-email.vala @@ -64,6 +64,20 @@ public class Geary.Email : Object { public inline bool require(Field required_fields) { return is_all_set(required_fields); } + + public string to_list_string() { + StringBuilder builder = new StringBuilder(); + foreach (Field f in all()) { + if (is_all_set(f)) { + if (!String.is_empty(builder.str)) + builder.append(", "); + + builder.append(f.to_string()); + } + } + + return builder.str; + } } /** diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala index 5e3ff3ce..f31e6f8d 100644 --- a/src/engine/api/geary-folder.vala +++ b/src/engine/api/geary-folder.vala @@ -261,7 +261,7 @@ public interface Geary.Folder : Object { * * The Folder must be opened prior to attempting this operation. */ - public abstract async bool create_email_async(Geary.Email email, Cancellable? cancellable = null) + public abstract async bool create_email_async(Geary.RFC822.Message rfc822, Cancellable? cancellable = null) throws Error; /** diff --git a/src/engine/api/geary-logging.vala b/src/engine/api/geary-logging.vala index 56b2b1d5..16e87e00 100755 --- a/src/engine/api/geary-logging.vala +++ b/src/engine/api/geary-logging.vala @@ -13,7 +13,8 @@ public enum Flag { SERIALIZER, REPLAY, CONVERSATIONS, - PERIODIC; + PERIODIC, + TRANSACTIONS; public inline bool is_all_set(Flag flags) { return (flags & this) == flags; diff --git a/src/engine/api/geary-special-folder.vala b/src/engine/api/geary-special-folder.vala index 78c98b6a..f30b9894 100644 --- a/src/engine/api/geary-special-folder.vala +++ b/src/engine/api/geary-special-folder.vala @@ -11,7 +11,8 @@ public enum Geary.SpecialFolderType { FLAGGED, ALL_MAIL, SPAM, - TRASH + TRASH, + OUTBOX } public class Geary.SpecialFolder : Object { diff --git a/src/engine/imap/api/imap-account.vala b/src/engine/imap/api/imap-account.vala index db8324fa..41188db2 100644 --- a/src/engine/imap/api/imap-account.vala +++ b/src/engine/imap/api/imap-account.vala @@ -45,6 +45,16 @@ private class Geary.Imap.Account : Object { smtp = new Geary.Smtp.ClientSession(smtp_endpoint); } + public async void open_async(Cancellable? cancellable) throws Error { + // Nothing to do -- ClientSessionManager deals with maintaining connections + // TODO: Start ClientSessionManager here, not in ctor + } + + public async void close_async(Cancellable? cancellable) throws Error { + // Nothing to do -- ClientSessionManager deals with maintaining connections + // TODO: Stop ClientSessionManager here + } + public async Gee.Collection list_folders_async(Geary.FolderPath? parent, Cancellable? cancellable = null) throws Error { Geary.FolderPath? processed = process_path(parent, null, @@ -175,10 +185,8 @@ private class Geary.Imap.Account : Object { return parent; } - public async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = null) + public async void send_email_async(Geary.RFC822.Message rfc822, Cancellable? cancellable = null) throws Error { - Geary.RFC822.Message rfc822 = new Geary.RFC822.Message.from_composed_email(composed); - yield smtp.login_async(cred, cancellable); try { yield smtp.send_email_async(rfc822, cancellable); diff --git a/src/engine/imap/api/imap-email-identifier.vala b/src/engine/imap/api/imap-email-identifier.vala index 29507f5a..4bce4c1a 100644 --- a/src/engine/imap/api/imap-email-identifier.vala +++ b/src/engine/imap/api/imap-email-identifier.vala @@ -5,13 +5,12 @@ */ private class Geary.Imap.EmailIdentifier : Geary.EmailIdentifier { - public override int64 ordering { get; protected set; } - public Imap.UID uid { get; private set; } public EmailIdentifier(Imap.UID uid) { + base (uid.value); + this.uid = uid; - ordering = uid.value; } public override uint to_hash() { @@ -26,7 +25,7 @@ private class Geary.Imap.EmailIdentifier : Geary.EmailIdentifier { if (this == other) return true; - return uid.value == other.uid.value; + return ordering == other.ordering; } public override string to_string() { diff --git a/src/engine/impl/geary-abstract-account.vala b/src/engine/impl/geary-abstract-account.vala index 0270fb22..d7105e8f 100644 --- a/src/engine/impl/geary-abstract-account.vala +++ b/src/engine/impl/geary-abstract-account.vala @@ -21,7 +21,17 @@ public abstract class Geary.AbstractAccount : Object, Geary.Account { folders_added_removed(added, removed); } - public abstract Geary.Email.Field get_required_fields_for_writing(); + protected virtual void notify_opened() { + opened(); + } + + protected virtual void notify_closed() { + closed(); + } + + public abstract async void open_async(Cancellable? cancellable = null) throws Error; + + public abstract async void close_async(Cancellable? cancellable = null) throws Error; public abstract async Gee.Collection list_folders_async(Geary.FolderPath? parent, Cancellable? cancellable = null) throws Error; diff --git a/src/engine/impl/geary-abstract-folder.vala b/src/engine/impl/geary-abstract-folder.vala index 85d0cebd..0ec086e1 100644 --- a/src/engine/impl/geary-abstract-folder.vala +++ b/src/engine/impl/geary-abstract-folder.vala @@ -57,31 +57,81 @@ public abstract class Geary.AbstractFolder : Object, Geary.Folder { public abstract async int get_email_count_async(Cancellable? cancellable = null) throws Error; - public abstract async bool create_email_async(Geary.Email email, Cancellable? cancellable = null) - throws Error; + public abstract async bool create_email_async(Geary.RFC822.Message rfc822, + Cancellable? cancellable = null) throws Error; public abstract async Gee.List? list_email_async(int low, int count, Geary.Email.Field required_fields, Folder.ListFlags flags, Cancellable? cancellable = null) throws Error; - public abstract void lazy_list_email(int low, int count, Geary.Email.Field required_fields, - Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null); + public virtual void lazy_list_email(int low, int count, Geary.Email.Field required_fields, + Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null) { + do_lazy_list_email_async.begin(low, count, required_fields, flags, cb, cancellable); + } + private async void do_lazy_list_email_async(int low, int count, Geary.Email.Field required_fields, + Folder.ListFlags flags, EmailCallback cb, Cancellable? cancellable = null) { + try { + Gee.List? list = yield list_email_async(low, count, required_fields, flags, + cancellable); + if (list != null && list.size > 0) + cb(list, null); + + cb(null, null); + } catch (Error err) { + cb(null, err); + } + } + public abstract async Gee.List? list_email_by_id_async(Geary.EmailIdentifier initial_id, int count, Geary.Email.Field required_fields, Folder.ListFlags flags, Cancellable? cancellable = null) throws Error; - public abstract void lazy_list_email_by_id(Geary.EmailIdentifier initial_id, int count, + public virtual 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); + 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 { + Gee.List? list = yield list_email_by_id_async(initial_id, count, + required_fields, flags, cancellable); + if (list != null && list.size > 0) + cb(list, null); + + cb(null, null); + } catch (Error err) { + cb(null, err); + } + } public abstract async Gee.List? list_email_by_sparse_id_async( Gee.Collection ids, Geary.Email.Field required_fields, Folder.ListFlags flags, Cancellable? cancellable = null) throws Error; - public abstract void lazy_list_email_by_sparse_id(Gee.Collection ids, + public virtual void lazy_list_email_by_sparse_id(Gee.Collection ids, Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, - Cancellable? cancellable = null); + 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 ids, + Geary.Email.Field required_fields, Folder.ListFlags flags, EmailCallback cb, + Cancellable? cancellable) { + try { + Gee.List? list = yield list_email_by_sparse_id_async(ids, + required_fields, flags, cancellable); + if (list != null && list.size > 0) + cb(list, null); + + cb(null, null); + } catch (Error err) { + cb(null, err); + } + } public abstract async Gee.Map? list_local_email_fields_async( Gee.Collection ids, Cancellable? cancellable = null) throws Error; diff --git a/src/engine/impl/geary-generic-imap-account.vala b/src/engine/impl/geary-generic-imap-account.vala index 71b080a1..8aac7ba1 100644 --- a/src/engine/impl/geary-generic-imap-account.vala +++ b/src/engine/impl/geary-generic-imap-account.vala @@ -9,6 +9,7 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { private Sqlite.Account local; private Gee.HashMap properties_map = new Gee.HashMap< FolderPath, Imap.FolderProperties>(Hashable.hash_func, Equalable.equal_func); + private SmtpOutboxFolder? outbox = null; public GenericImapAccount(string name, string username, AccountInformation? account_info, File user_data_dir, Imap.Account remote, Sqlite.Account local) { @@ -24,10 +25,52 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { return properties_map.get(path); } - public override Geary.Email.Field get_required_fields_for_writing() { - // Return the more restrictive of the two, which is the NetworkAccount's. - // TODO: This could be determined at runtime rather than fixed in stone here. - return Geary.Email.Field.HEADER | Geary.Email.Field.BODY; + public override async void open_async(Cancellable? cancellable = null) throws Error { + yield local.open_async(get_account_information().credentials, Engine.user_data_dir, Engine.resource_dir, + cancellable); + + // need to back out local.open_async() if remote fails + try { + yield remote.open_async(cancellable); + } catch (Error err) { + // back out + try { + yield local.close_async(cancellable); + } catch (Error close_err) { + // ignored + } + + throw err; + } + + outbox = new SmtpOutboxFolder(remote, local.get_outbox()); + + notify_opened(); + } + + public override async void close_async(Cancellable? cancellable = null) throws Error { + // attempt to close both regardless of errors + Error? local_err = null; + try { + yield local.close_async(cancellable); + } catch (Error lclose_err) { + local_err = lclose_err; + } + + Error? remote_err = null; + try { + yield remote.close_async(cancellable); + } catch (Error rclose_err) { + remote_err = rclose_err; + } + + outbox = null; + + if (local_err != null) + throw local_err; + + if (remote_err != null) + throw remote_err; } public override async Gee.Collection list_folders_async(Geary.FolderPath? parent, @@ -50,6 +93,7 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { } background_update_folders.begin(parent, engine_list, cancellable); + engine_list.add(outbox); return engine_list; } @@ -64,6 +108,10 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { public override async Geary.Folder fetch_folder_async(Geary.FolderPath path, Cancellable? cancellable = null) throws Error { + + if (path.equals(outbox.get_path())) + return outbox; + Sqlite.Folder? local_folder = null; try { local_folder = (Sqlite.Folder) yield local.fetch_folder_async(path, cancellable); @@ -179,9 +227,10 @@ private abstract class Geary.GenericImapAccount : Geary.EngineAccount { return false; } - public override async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = null) - throws Error { - yield remote.send_email_async(composed, cancellable); + public override async void send_email_async(Geary.ComposedEmail composed, + Cancellable? cancellable = null) throws Error { + Geary.RFC822.Message rfc822 = new Geary.RFC822.Message.from_composed_email(composed); + yield outbox.create_email_async(rfc822, cancellable); } private void on_login_failed(Geary.Credentials? credentials) { diff --git a/src/engine/impl/geary-generic-imap-folder.vala b/src/engine/impl/geary-generic-imap-folder.vala index 7331ae8e..0ce699d4 100644 --- a/src/engine/impl/geary-generic-imap-folder.vala +++ b/src/engine/impl/geary-generic-imap-folder.vala @@ -94,9 +94,8 @@ private class Geary.GenericImapFolder : Geary.AbstractFolder { return Geary.Folder.OpenState.OPENING; } - public override async bool create_email_async(Geary.Email email, Cancellable? cancellable) throws Error { - check_open("create_email_async"); - + public override async bool create_email_async(Geary.RFC822.Message rfc822, Cancellable? + cancellable) throws Error { throw new EngineError.READONLY("Engine currently read-only"); } diff --git a/src/engine/impl/geary-gmail-account.vala b/src/engine/impl/geary-gmail-account.vala index 5a1f1e5f..abdfcdf5 100644 --- a/src/engine/impl/geary-gmail-account.vala +++ b/src/engine/impl/geary-gmail-account.vala @@ -47,10 +47,13 @@ private class Geary.GmailAccount : Geary.GenericImapAccount { private static void initialize_personality() { Geary.FolderPath gmail_root = new Geary.FolderRoot(GMAIL_FOLDER, Imap.Account.ASSUMED_SEPARATOR, true); + Geary.FolderRoot inbox_folder = new Geary.FolderRoot(Imap.Account.INBOX_NAME, + Imap.Account.ASSUMED_SEPARATOR, false); + Geary.FolderRoot outbox_folder = new SmtpOutboxFolderRoot(); special_folder_map = new SpecialFolderMap(); special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.INBOX, _("Inbox"), - new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR, false), 0)); + inbox_folder, 0)); special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.DRAFTS, _("Drafts"), gmail_root.get_child("Drafts"), 1)); special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.SENT, _("Sent Mail"), @@ -61,13 +64,15 @@ private class Geary.GmailAccount : Geary.GenericImapAccount { gmail_root.get_child("All Mail"), 4)); special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.SPAM, _("Spam"), gmail_root.get_child("Spam"), 5)); + special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.OUTBOX, + _("Outbox"), outbox_folder, 6)); special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.TRASH, _("Trash"), - gmail_root.get_child("Trash"), 6)); + gmail_root.get_child("Trash"), 7)); ignored_paths = new Gee.HashSet(Hashable.hash_func, Equalable.equal_func); ignored_paths.add(gmail_root); - ignored_paths.add(new Geary.FolderRoot(Imap.Account.INBOX_NAME, Imap.Account.ASSUMED_SEPARATOR, - true)); + ignored_paths.add(inbox_folder); + ignored_paths.add(outbox_folder); } public override string get_user_folders_label() { diff --git a/src/engine/impl/geary-yahoo-account.vala b/src/engine/impl/geary-yahoo-account.vala index 6db9a417..589686ca 100644 --- a/src/engine/impl/geary-yahoo-account.vala +++ b/src/engine/impl/geary-yahoo-account.vala @@ -53,6 +53,7 @@ private class Geary.YahooAccount : Geary.GenericImapAccount { FolderRoot spam_folder = new Geary.FolderRoot("Bulk Mail", Imap.Account.ASSUMED_SEPARATOR, false); FolderRoot trash_folder = new Geary.FolderRoot("Trash", Imap.Account.ASSUMED_SEPARATOR, false); + FolderRoot outbox_folder = new SmtpOutboxFolderRoot(); special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.INBOX, _("Inbox"), inbox_folder, 0)); @@ -62,14 +63,17 @@ private class Geary.YahooAccount : Geary.GenericImapAccount { sent_folder, 2)); special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.SPAM, _("Spam"), spam_folder, 3)); + special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.OUTBOX, + _("Outbox"), outbox_folder, 4)); special_folder_map.set_folder(new SpecialFolder(Geary.SpecialFolderType.TRASH, _("Trash"), - trash_folder, 4)); + trash_folder, 5)); ignored_paths = new Gee.HashSet(Hashable.hash_func, Equalable.equal_func); ignored_paths.add(inbox_folder); ignored_paths.add(drafts_folder); ignored_paths.add(sent_folder); ignored_paths.add(spam_folder); + ignored_paths.add(outbox_folder); ignored_paths.add(trash_folder); } diff --git a/src/engine/impl/outbox/smtp-outbox-email-identifier.vala b/src/engine/impl/outbox/smtp-outbox-email-identifier.vala new file mode 100644 index 00000000..95414292 --- /dev/null +++ b/src/engine/impl/outbox/smtp-outbox-email-identifier.vala @@ -0,0 +1,23 @@ +/* Copyright 2011-2012 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.OutboxEmailIdentifier : Geary.EmailIdentifier { + public OutboxEmailIdentifier(int64 ordering) { + base (ordering); + } + + public override bool equals(Geary.Equalable o) { + EmailIdentifier? other = o as EmailIdentifier; + if (other == null) + return false; + + if (this == other) + return true; + + return ordering == other.ordering; + } +} + diff --git a/src/engine/impl/outbox/smtp-outbox-email-properties.vala b/src/engine/impl/outbox/smtp-outbox-email-properties.vala new file mode 100644 index 00000000..c50d8aa5 --- /dev/null +++ b/src/engine/impl/outbox/smtp-outbox-email-properties.vala @@ -0,0 +1,15 @@ +/* Copyright 2011-2012 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.OutboxEmailProperties : Geary.EmailProperties { + public OutboxEmailProperties() { + } + + public override string to_string() { + return "OutboxProperties"; + } +} + diff --git a/src/engine/impl/outbox/smtp-outbox-folder.vala b/src/engine/impl/outbox/smtp-outbox-folder.vala new file mode 100644 index 00000000..77ae6f09 --- /dev/null +++ b/src/engine/impl/outbox/smtp-outbox-folder.vala @@ -0,0 +1,307 @@ +/* Copyright 2011-2012 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.SmtpOutboxFolderRoot : Geary.FolderRoot { + public const string MAGIC_BASENAME = "$GearyOutbox$"; + + public SmtpOutboxFolderRoot() { + base(MAGIC_BASENAME, null, false); + } +} + +// Special type of folder that runs an asynchronous send queue. Messages are +// saved to the database, then queued up for sending. +private class Geary.SmtpOutboxFolder : Geary.AbstractFolder { + private static FolderRoot? path = null; + + private Geary.Sqlite.SmtpOutboxTable local_folder; + private Imap.Account remote; + private Sqlite.Database db; + private bool opened = false; + private NonblockingMailbox outbox_queue = + new NonblockingMailbox(); + + public SmtpOutboxFolder(Imap.Account remote, Geary.Sqlite.SmtpOutboxTable table) { + this.remote = remote; + this.local_folder = table; + db = table.gdb; + + do_postman_async.begin(); + } + + private string message_subject(RFC822.Message message) { + return (message.subject != null && !String.is_empty(message.subject.to_string())) + ? message.subject.to_string() : "(no subject)"; + } + + // TODO: Use Cancellable to shut down outbox processor when closing account + private async void do_postman_async() { + debug("Starting outbox postman"); + + // Fill the send queue with existing mail (if any) + try { + Gee.List? row_list = yield local_folder.list_email_async( + null, new OutboxEmailIdentifier(-1), -1, null); + if (row_list != null && row_list.size > 0) { + debug("Priming outbox postman with %d stored messages", row_list.size); + foreach (Geary.Sqlite.SmtpOutboxRow row in row_list) + outbox_queue.send(row); + } + } catch (Error prime_err) { + warning("Error priming outbox: %s", prime_err.message); + } + + // Start the send queue. + for (;;) { + // yield until a message is ready + Geary.Sqlite.SmtpOutboxRow row; + try { + row = yield outbox_queue.recv_async(); + } catch (Error wait_err) { + debug("Outbox postman queue error: %s", wait_err.message); + + break; + } + + // Convert row into RFC822 message suitable for sending or framing + RFC822.Message message; + try { + message = new RFC822.Message.from_string(row.message); + } catch (RFC822Error msg_err) { + // TODO: This needs to be reported to the user + debug("Outbox postman message error: %s", msg_err.message); + + continue; + } + + // Send the message, but only remove from database once sent + try { + debug("Outbox postman: Sending \"%s\" (ID:%s)...", message_subject(message), row.to_string()); + yield remote.send_email_async(message); + } catch (Error send_err) { + debug("Outbox postman send error, retrying: %s", send_err.message); + + try { + outbox_queue.send(row); + } catch (Error send_err) { + debug("Outbox postman: Unable to re-send row to outbox, dropping on floor: %s", send_err.message); + } + + continue; + } + + // Remove from database + try { + debug("Outbox postman: Removing \"%s\" (ID:%s) from database", message_subject(message), + row.to_string()); + yield remove_single_email_async(new OutboxEmailIdentifier(row.ordering)); + } catch (Error rm_err) { + debug("Outbox postman: Unable to remove row from database: %s", rm_err.message); + } + } + + debug("Exiting outbox postman"); + } + + public override Geary.FolderPath get_path() { + if (path == null) + path = new SmtpOutboxFolderRoot(); + + return path; + } + + public override Geary.Trillian has_children() { + return Geary.Trillian.FALSE; + } + + public override Geary.SpecialFolderType? get_special_folder_type() { + return Geary.SpecialFolderType.OUTBOX; + } + + public override Geary.Folder.OpenState get_open_state() { + return opened ? Geary.Folder.OpenState.LOCAL : Geary.Folder.OpenState.CLOSED; + } + + public override async void open_async(bool readonly, Cancellable? cancellable = null) + throws Error { + if (opened) + throw new EngineError.ALREADY_OPEN("Folder %s already open", to_string()); + + opened = true; + notify_opened(Geary.Folder.OpenState.LOCAL, yield get_email_count_async(cancellable)); + } + + public override async void close_async(Cancellable? cancellable = null) throws Error { + opened = false; + notify_closed(Geary.Folder.CloseReason.LOCAL_CLOSE); + notify_closed(Geary.Folder.CloseReason.FOLDER_CLOSED); + } + + public override async int get_email_count_async(Cancellable? cancellable = null) throws Error { + return yield internal_get_email_count_async(null, cancellable); + } + + private async int internal_get_email_count_async(Sqlite.Transaction? transaction, Cancellable? cancellable) + throws Error { + return yield local_folder.get_email_count_async(transaction, cancellable); + } + + public override async bool create_email_async(Geary.RFC822.Message rfc822, + Cancellable? cancellable = null) throws Error { + Sqlite.Transaction transaction = yield db.begin_transaction_async("Outbox.create_email_async", + cancellable); + + Geary.Sqlite.SmtpOutboxRow row = yield local_folder.create_async(transaction, + rfc822.get_body_rfc822_buffer().to_string(), cancellable); + + int count = yield internal_get_email_count_async(transaction, cancellable); + + // signal message added before adding for delivery + Gee.List list = new Gee.ArrayList(); + list.add(new OutboxEmailIdentifier(row.ordering)); + notify_email_appended(list); + notify_email_count_changed(count, CountChangeReason.ADDED); + + // immediately add to outbox queue for delivery + outbox_queue.send(row); + + return true; + } + + public override async Gee.List? list_email_async(int low, int count, + Geary.Email.Field required_fields, Geary.Folder.ListFlags flags, Cancellable? cancellable = null) + throws Error { + return yield list_email_by_id_async(new OutboxEmailIdentifier(low), count, + required_fields, flags, cancellable); + } + + public override async Gee.List? list_email_by_id_async( + Geary.EmailIdentifier initial_id, int count, Geary.Email.Field required_fields, + Geary.Folder.ListFlags flags, Cancellable? cancellable = null) throws Error { + + OutboxEmailIdentifier? id = initial_id as OutboxEmailIdentifier; + assert(id != null); + + Sqlite.Transaction transaction = yield db.begin_transaction_async("Outbox.list_email_by_id_async", + cancellable); + + Gee.List? row_list = yield local_folder.list_email_async( + transaction, id, count, cancellable); + if (row_list == null || row_list.size == 0) + return null; + + Gee.List list = new Gee.ArrayList(); + foreach (Geary.Sqlite.SmtpOutboxRow row in row_list) { + int position = yield row.get_position_async(transaction, cancellable); + list.add(outbox_email_for_row(row, position)); + } + + return list; + } + + public override async Gee.List? list_email_by_sparse_id_async( + Gee.Collection _ids, Geary.Email.Field required_fields, + Geary.Folder.ListFlags flags, Cancellable? cancellable = null) throws Error { + Gee.List ids = new Gee.ArrayList(); + foreach (Geary.EmailIdentifier id in _ids) { + assert(id is OutboxEmailIdentifier); + ids.add((OutboxEmailIdentifier) id); + } + + Sqlite.Transaction transaction = yield db.begin_transaction_async("Outbox.list_email_by_sparse_id_async", + cancellable); + + Gee.List? row_list = yield local_folder. + list_email_by_sparse_id_async(transaction, ids, cancellable); + if (row_list == null || row_list.size == 0) + return null; + + Gee.List list = new Gee.ArrayList(); + foreach (Geary.Sqlite.SmtpOutboxRow row in row_list) { + int position = yield row.get_position_async(transaction, cancellable); + list.add(outbox_email_for_row(row, position)); + } + + return list; + } + + public override async Gee.Map? + list_local_email_fields_async(Gee.Collection ids, + Cancellable? cancellable = null) throws Error { + // Not implemented. + return null; + } + + 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 { + OutboxEmailIdentifier? id = _id as OutboxEmailIdentifier; + assert(id != null); + + Sqlite.Transaction transaction = yield db.begin_transaction_async("Outbox.fetch_email_async", + cancellable); + + Geary.Sqlite.SmtpOutboxRow? row = yield local_folder.fetch_email_async(transaction, id); + if (row == null) + throw new EngineError.NOT_FOUND("No message with ID %lld found in database", row.ordering); + + int position = yield row.get_position_async(transaction, cancellable); + + return outbox_email_for_row(row, position); + } + + public override async void remove_email_async(Gee.List email_ids, + Cancellable? cancellable = null) throws Error { + foreach (Geary.EmailIdentifier id in email_ids) + remove_single_email_async(id, cancellable); + } + + public override async void remove_single_email_async(Geary.EmailIdentifier _id, + Cancellable? cancellable = null) throws Error { + OutboxEmailIdentifier? id = _id as OutboxEmailIdentifier; + assert(id != null); + + Sqlite.Transaction transaction = yield db.begin_transaction_async("Outbox.remove_single_email_async", + cancellable); + + yield local_folder.remove_single_email_async(transaction, id, cancellable); + + int count = yield internal_get_email_count_async(transaction, cancellable); + + Gee.ArrayList list = new Gee.ArrayList(); + list.add(id); + notify_email_removed(list); + notify_email_count_changed(count, CountChangeReason.REMOVED); + } + + public override async void mark_email_async( + Gee.List to_mark, Geary.EmailFlags? flags_to_add, + Geary.EmailFlags? flags_to_remove, Cancellable? cancellable = null) throws Error { + // Not implemented. + } + + // Utility for getting an email object back from an outbox row. + private Geary.Email outbox_email_for_row(Geary.Sqlite.SmtpOutboxRow row, int position) throws Error { + RFC822.Message message = new RFC822.Message.from_string(row.message); + + Geary.Email email = message.get_email(position, new OutboxEmailIdentifier(row.ordering)); + email.set_email_properties(new OutboxEmailProperties()); + email.set_flags(new Geary.EmailFlags()); + + return email; + } + + public override async void copy_email_async(Gee.List to_copy, + Geary.FolderPath destination, Cancellable? cancellable = null) throws Error { + // Not implemented. + } + + public override async void move_email_async(Gee.List to_move, + Geary.FolderPath destination, Cancellable? cancellable = null) throws Error { + // Not implemented. + } +} + diff --git a/src/engine/rfc822/rfc822-message-data.vala b/src/engine/rfc822/rfc822-message-data.vala index 3d425d11..b1bbf181 100644 --- a/src/engine/rfc822/rfc822-message-data.vala +++ b/src/engine/rfc822/rfc822-message-data.vala @@ -277,5 +277,9 @@ public class Geary.RFC822.PreviewText : Geary.RFC822.Text { base (buffer); } + + public PreviewText.from_string(string preview) { + base (new Geary.Memory.StringBuffer(preview)); + } } diff --git a/src/engine/rfc822/rfc822-message.vala b/src/engine/rfc822/rfc822-message.vala index 5c431d03..b55ff89f 100644 --- a/src/engine/rfc822/rfc822-message.vala +++ b/src/engine/rfc822/rfc822-message.vala @@ -30,6 +30,10 @@ public class Geary.RFC822.Message : Object { stock_from_gmime(); } + public Message.from_string(string full_email) throws RFC822Error { + this(new Geary.RFC822.Full(new Geary.Memory.StringBuffer(full_email))); + } + public Message.from_parts(Header header, Text body) throws RFC822Error { GMime.StreamCat stream_cat = new GMime.StreamCat(); stream_cat.add_source(new GMime.StreamMem.with_buffer(header.buffer.get_array())); @@ -129,7 +133,7 @@ public class Geary.RFC822.Message : Object { message.set_mime_part(body_html); } } - + // Makes a copy of the given message without the BCC fields. This is used for sending the email // without sending the BCC headers to all recipients. public Message.without_bcc(Message email) { @@ -176,7 +180,44 @@ public class Geary.RFC822.Message : Object { // Setup body depending on what MIME components were filled out. message.set_mime_part(email.message.get_mime_part()); } - + + public Geary.Email get_email(int position, Geary.EmailIdentifier id) throws Error { + Geary.Email email = new Geary.Email(position, id); + + email.set_message_header(new Geary.RFC822.Header(new Geary.Memory.StringBuffer( + message.get_headers()))); + email.set_send_date(new Geary.RFC822.Date(message.get_date_as_string())); + email.set_originators(from, new Geary.RFC822.MailboxAddresses.single(sender), null); + email.set_receivers(to, cc, bcc); + email.set_full_references(null, in_reply_to, references); + email.set_message_subject(subject); + email.set_message_body(new Geary.RFC822.Text(new Geary.Memory.StringBuffer( + message.get_body().to_string()))); + email.set_message_preview(new Geary.RFC822.PreviewText.from_string( + preview_from_email(email))); + + return email; + } + + // Takes an e-mail object with a body and generates a preview. If there is no body + // or the body is the empty string, the empty string will be returned. + // + // Note that this is intended for outgoing messages, and as such we rely on the text + // section existing. + private string preview_from_email(Geary.Email email) { + try { + return Geary.String.safe_byte_substring(email.get_message(). + get_first_mime_part_of_content_type("text/plain").to_string(). + chug(), Geary.Email.MAX_PREVIEW_BYTES); + } catch (Error e) { + debug("Could not generate outbox preview: %s", e.message); + + // fall through + } + + return ""; + } + private void stock_from_gmime() { from = new RFC822.MailboxAddresses.from_rfc822_string(message.get_sender()); if (from.size == 0) { diff --git a/src/engine/sqlite/abstract/sqlite-database.vala b/src/engine/sqlite/abstract/sqlite-database.vala index 58ed69b2..9037fa0d 100644 --- a/src/engine/sqlite/abstract/sqlite-database.vala +++ b/src/engine/sqlite/abstract/sqlite-database.vala @@ -60,25 +60,24 @@ public abstract class Geary.Sqlite.Database { File upgrade_script; while ((upgrade_script = get_upgrade_script(++db_version)).query_exists()) { pre_upgrade(db_version); - + try { - debug("Upgrading to %d", db_version); - string upgrade_contents; - FileUtils.get_contents(upgrade_script.get_path(), out upgrade_contents); - db.run(upgrade_contents); + debug("Upgrading database to to version %d at %s", db_version, upgrade_script.get_path()); + + db.run_script(upgrade_script.get_path()); db.run("PRAGMA user_version = %d;".printf(db_version)); } catch (Error e) { // TODO Add rollback of changes here when switching away from SQLHeavy. warning("Error upgrading database: %s", e.message); throw e; } - + post_upgrade(db_version); } } private File get_upgrade_script(int version) { - return File.new_for_path("%s/Version-%03d.sql".printf(schema_dir.get_path(), version)); + return schema_dir.get_child("Version-%03d.sql".printf(version)); } } diff --git a/src/engine/sqlite/abstract/sqlite-transaction.vala b/src/engine/sqlite/abstract/sqlite-transaction.vala index 1b85572a..778afda9 100644 --- a/src/engine/sqlite/abstract/sqlite-transaction.vala +++ b/src/engine/sqlite/abstract/sqlite-transaction.vala @@ -7,6 +7,7 @@ public class Geary.Sqlite.Transaction { private static NonblockingMutex? transaction_lock = null; private static int next_id = 0; + private static string? held_by = null; public bool is_locked { get { return claim_stub != NonblockingMutex.INVALID_TOKEN; @@ -41,13 +42,12 @@ public class Geary.Sqlite.Transaction { public async void begin_async(Cancellable? cancellable = null) throws Error { assert(!is_locked); -#if TRACE_TRANSACTIONS - debug("[%s] claiming lock", to_string()); -#endif + + Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] claiming lock held by %s", to_string(), + !String.is_empty(held_by) ? held_by : "(no one)"); claim_stub = yield transaction_lock.claim_async(cancellable); -#if TRACE_TRANSACTIONS - debug("[%s] lock claimed", to_string()); -#endif + held_by = name; + Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] lock claimed", to_string()); } private void resolve(bool commit, Cancellable? cancellable) throws Error { @@ -60,13 +60,11 @@ public class Geary.Sqlite.Transaction { if (commit) is_commit_required = false; -#if TRACE_TRANSACTIONS - debug("[%s] releasing lock", to_string()); -#endif + Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] releasing lock held by %s", to_string(), + !String.is_empty(held_by) ? held_by : "(no one)"); transaction_lock.release(ref claim_stub); -#if TRACE_TRANSACTIONS - debug("[%s] released lock", to_string()); -#endif + held_by = null; + Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] released lock", to_string()); } public SQLHeavy.Query prepare(string sql) throws Error { @@ -87,9 +85,8 @@ public class Geary.Sqlite.Transaction { } public void set_commit_required() { -#if TRACE_TRANSACTIONS - debug("[%s] commit required", to_string()); -#endif + Logging.debug(Logging.Flag.TRANSACTIONS, "[%s] commit required", to_string()); + is_commit_required = true; } diff --git a/src/engine/sqlite/api/sqlite-account.vala b/src/engine/sqlite/api/sqlite-account.vala index 047c9aee..34977162 100644 --- a/src/engine/sqlite/api/sqlite-account.vala +++ b/src/engine/sqlite/api/sqlite-account.vala @@ -16,32 +16,65 @@ private class Geary.Sqlite.Account : Object { } private string name; - private ImapDatabase db; - private FolderTable folder_table; - private ImapFolderPropertiesTable folder_properties_table; - private MessageTable message_table; + private ImapDatabase? db = null; + private FolderTable? folder_table = null; + private ImapFolderPropertiesTable? folder_properties_table = null; + private MessageTable? message_table = null; + private SmtpOutboxTable? outbox_table = null; private Gee.HashMap folder_refs = new Gee.HashMap(Hashable.hash_func, Equalable.equal_func); - public Account(Geary.Credentials cred, File user_data_dir, File resource_dir) { - name = "SQLite account for %s".printf(cred.to_string()); + public Account(string username) { + name = "SQLite account for %s".printf(username); + } + + private void check_open() throws Error { + if (db == null) + throw new EngineError.OPEN_REQUIRED("Database not open"); + } + + public async void open_async(Geary.Credentials cred, File user_data_dir, File resource_dir, + Cancellable? cancellable) throws Error { + if (db != null) + throw new EngineError.ALREADY_OPEN("IMAP database already open"); try { db = new ImapDatabase(cred.user, user_data_dir, resource_dir); db.pre_upgrade.connect(on_pre_upgrade); db.post_upgrade.connect(on_post_upgrade); + db.upgrade(); } catch (Error err) { - error("Unable to open database: %s", err.message); + warning("Unable to open database: %s", err.message); + + // close database before exiting + db = null; + + throw err; } folder_table = db.get_folder_table(); folder_properties_table = db.get_imap_folder_properties_table(); message_table = db.get_message_table(); + outbox_table = db.get_smtp_outbox_table(); + } + + public async void close_async(Cancellable? cancellable) throws Error { + if (db == null) + return; + + folder_table = null; + folder_properties_table = null; + message_table = null; + outbox_table = null; + + db = null; } private async int64 fetch_id_async(Transaction? transaction, Geary.FolderPath path, Cancellable? cancellable = null) throws Error { + check_open(); + FolderRow? row = yield folder_table.fetch_descend_async(transaction, path.as_list(), cancellable); if (row == null) @@ -52,12 +85,16 @@ private class Geary.Sqlite.Account : Object { private async int64 fetch_parent_id_async(Transaction? transaction, Geary.FolderPath path, Cancellable? cancellable = null) throws Error { + check_open(); + return path.is_root() ? Row.INVALID_ID : yield fetch_id_async(transaction, path.get_parent(), cancellable); } public async void clone_folder_async(Geary.Imap.Folder imap_folder, Cancellable? cancellable = null) throws Error { + check_open(); + Geary.Imap.FolderProperties? imap_folder_properties = imap_folder.get_properties(); // properties *must* be available to perform a clone @@ -80,6 +117,8 @@ private class Geary.Sqlite.Account : Object { public async void update_folder_async(Geary.Imap.Folder imap_folder, Cancellable? cancellable = null) throws Error { + check_open(); + Geary.Imap.FolderProperties? imap_folder_properties = (Geary.Imap.FolderProperties?) imap_folder.get_properties(); @@ -111,6 +150,8 @@ private class Geary.Sqlite.Account : Object { public async Gee.Collection list_folders_async(Geary.FolderPath? parent, Cancellable? cancellable = null) throws Error { + check_open(); + Transaction transaction = yield db.begin_transaction_async("Account.list_folders_async", cancellable); @@ -149,6 +190,8 @@ private class Geary.Sqlite.Account : Object { public async bool folder_exists_async(Geary.FolderPath path, Cancellable? cancellable = null) throws Error { + check_open(); + try { int64 id = yield fetch_id_async(null, path, cancellable); @@ -163,6 +206,8 @@ private class Geary.Sqlite.Account : Object { public async Geary.Sqlite.Folder fetch_folder_async(Geary.FolderPath path, Cancellable? cancellable = null) throws Error { + check_open(); + // check references table first Geary.Sqlite.Folder? folder = get_sqlite_folder(path); if (folder != null) @@ -191,8 +236,14 @@ private class Geary.Sqlite.Account : Object { return (folder_ref != null) ? (Geary.Sqlite.Folder) folder_ref.get_reference() : null; } + public SmtpOutboxTable get_outbox() { + return outbox_table; + } + private Geary.Sqlite.Folder create_sqlite_folder(FolderRow row, Imap.FolderProperties? properties, Geary.FolderPath path) throws Error { + check_open(); + // create folder Geary.Sqlite.Folder folder = new Geary.Sqlite.Folder(db, row, properties, path); diff --git a/src/engine/sqlite/email/sqlite-mail-database.vala b/src/engine/sqlite/email/sqlite-mail-database.vala index 8a2a197c..32f19365 100644 --- a/src/engine/sqlite/email/sqlite-mail-database.vala +++ b/src/engine/sqlite/email/sqlite-mail-database.vala @@ -4,7 +4,7 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database { +private class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database { public const string FILENAME = "geary.db"; public MailDatabase(string user, File user_data_dir, File resource_dir) throws Error { @@ -48,5 +48,14 @@ public class Geary.Sqlite.MailDatabase : Geary.Sqlite.Database { ? attachment_table : (MessageAttachmentTable) add_table(new MessageAttachmentTable(this, heavy_table)); } + + public Geary.Sqlite.SmtpOutboxTable get_smtp_outbox_table() { + SQLHeavy.Table heavy_table; + SmtpOutboxTable? outbox_table = get_table("OutboxTable", out heavy_table) as SmtpOutboxTable; + + return (outbox_table != null) + ? outbox_table + : (SmtpOutboxTable) add_table(new SmtpOutboxTable(this, heavy_table)); + } } diff --git a/src/engine/sqlite/email/sqlite-message-location-table.vala b/src/engine/sqlite/email/sqlite-message-location-table.vala index f355a70e..4457689d 100644 --- a/src/engine/sqlite/email/sqlite-message-location-table.vala +++ b/src/engine/sqlite/email/sqlite-message-location-table.vala @@ -14,7 +14,7 @@ public class Geary.Sqlite.MessageLocationTable : Geary.Sqlite.Table { REMOVE_MARKER } - public MessageLocationTable(Geary.Sqlite.Database db, SQLHeavy.Table table) { + internal MessageLocationTable(Geary.Sqlite.Database db, SQLHeavy.Table table) { base (db, table); } diff --git a/src/engine/sqlite/imap/sqlite-imap-database.vala b/src/engine/sqlite/imap/sqlite-imap-database.vala index 3aa1add2..f237c02a 100644 --- a/src/engine/sqlite/imap/sqlite-imap-database.vala +++ b/src/engine/sqlite/imap/sqlite-imap-database.vala @@ -4,7 +4,7 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ -public class Geary.Sqlite.ImapDatabase : Geary.Sqlite.MailDatabase { +private class Geary.Sqlite.ImapDatabase : Geary.Sqlite.MailDatabase { public ImapDatabase(string user, File user_data_dir, File resource_dir) throws Error { base (user, user_data_dir, resource_dir); } diff --git a/src/engine/sqlite/smtp/sqlite-smtp-outbox-row.vala b/src/engine/sqlite/smtp/sqlite-smtp-outbox-row.vala new file mode 100644 index 00000000..0981fd00 --- /dev/null +++ b/src/engine/sqlite/smtp/sqlite-smtp-outbox-row.vala @@ -0,0 +1,38 @@ +/* Copyright 2011-2012 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.Sqlite.SmtpOutboxRow : Geary.Sqlite.Row { + public int64 id { get; set; default = INVALID_ID; } + public int64 ordering { get; set; } + public string? message { get; set; } + + private int position; + + public SmtpOutboxRow(SmtpOutboxTable table, int64 id, int64 ordering, string message, int position) { + base (table); + + this.id = id; + this.ordering = ordering; + this.message = message; + this.position = position; + } + + public async int get_position_async(Transaction? transaction, Cancellable? cancellable) + throws Error { + if (position >= 1) + return position; + + position = yield ((SmtpOutboxTable) table).fetch_position_async(transaction, ordering, + cancellable); + + return (position >= 1) ? position : -1; + } + + public string to_string() { + return "%lld".printf(ordering); + } +} + diff --git a/src/engine/sqlite/smtp/sqlite-smtp-outbox-table.vala b/src/engine/sqlite/smtp/sqlite-smtp-outbox-table.vala new file mode 100644 index 00000000..403ef597 --- /dev/null +++ b/src/engine/sqlite/smtp/sqlite-smtp-outbox-table.vala @@ -0,0 +1,196 @@ +/* Copyright 2011-2012 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.Sqlite.SmtpOutboxTable : Geary.Sqlite.Table { + public SmtpOutboxTable(Geary.Sqlite.Database gdb, SQLHeavy.Table table) { + base(gdb, table); + } + + public async Geary.Sqlite.SmtpOutboxRow create_async(Transaction? transaction, + string message, Cancellable? cancellable) throws Error { + Transaction locked = yield obtain_lock_async(transaction, "SmtpOutboxTable.create_async", + cancellable); + + SQLHeavy.Query query = locked.prepare( + "INSERT INTO SmtpOutboxTable " + + "(message, ordering)" + + "VALUES (?, (SELECT COALESCE(MAX(ordering), 0) + 1 FROM SmtpOutboxTable))"); + query.bind_string(0, message); + + int64 id = yield query.execute_insert_async(cancellable); + locked.set_commit_required(); + + yield release_lock_async(null, locked, cancellable); + + SmtpOutboxRow? row = yield fetch_email_by_row_id_async(transaction, id); + if (row == null) + throw new EngineError.NOT_FOUND("Unable to locate created row %lld", id); + + return row; + } + + public async int get_email_count_async(Transaction? transaction, Cancellable? cancellable) + throws Error { + Transaction locked = yield obtain_lock_async(transaction, + "SmtpOutboxTable.get_email_count_async", cancellable); + + SQLHeavy.Query query = locked.prepare( + "SELECT COUNT(*) FROM SmtpOutboxTable"); + + SQLHeavy.QueryResult results = yield query.execute_async(); + check_cancel(cancellable, "get_email_count_for_folder_async"); + + return (!results.finished) ? results.fetch_int(0) : 0; + } + + public async Gee.List? list_email_async(Transaction? transaction, + OutboxEmailIdentifier initial_id, int count, Cancellable? cancellable = null) throws Error { + int64 low = initial_id.ordering; + assert(low >= 1 || low == -1); + assert(count >= 0 || count == -1); + + Transaction locked = yield obtain_lock_async(transaction, "SmtpOutboxTable.list_email_async", + cancellable); + + SQLHeavy.Query query = locked.prepare( + "SELECT id, ordering, message FROM SmtpOutboxTable " + + "ORDER BY ordering %s %s".printf(count != -1 ? "LIMIT ?" : "", + low != -1 ? "OFFSET ?" : "")); + + int bind = 0; + if (count != -1) + query.bind_int(bind++, count); + + if (low != -1) + query.bind_int64(bind++, low - 1); + + SQLHeavy.QueryResult results = yield query.execute_async(); + check_cancel(cancellable, "list_email_async"); + + if (results.finished) + return null; + + Gee.List list = new Gee.ArrayList(); + do { + list.add(new SmtpOutboxRow(this, results.fetch_int64(0), results.fetch_int64(1), + results.fetch_string(2), -1)); + + yield results.next_async(); + + check_cancel(cancellable, "list_email_async"); + } while (!results.finished); + + return list; + } + + public async Gee.List? list_email_by_sparse_id_async( + Transaction? transaction, Gee.Collection ids, + Cancellable? cancellable = null) throws Error { + + Gee.List list = new Gee.ArrayList(); + + foreach (OutboxEmailIdentifier id in ids) { + Geary.Sqlite.SmtpOutboxRow? row = yield fetch_email_internal_async(transaction, + id, cancellable); + if (row != null) + list.add(row); + } + + return list.size > 0 ? list : null; + } + + public async int fetch_position_async(Transaction? transaction, int64 ordering, + Cancellable? cancellable) throws Error { + Transaction locked = yield obtain_lock_async(transaction, "SmtpOutboxTable.fetch_position_async", + cancellable); + + SQLHeavy.Query query = locked.prepare( + "SELECT ordering FROM SmtpOutboxTable ORDER BY ordering"); + + SQLHeavy.QueryResult results = yield query.execute_async(); + check_cancel(cancellable, "fetch_position_async"); + + int position = 1; + while (!results.finished) { + if (results.fetch_int64(0) == ordering) + return position; + + yield results.next_async(); + + check_cancel(cancellable, "fetch_position_async"); + + position++; + } + + // not found + return -1; + } + + // Fetch an email given an outbox ID. + public async Geary.Sqlite.SmtpOutboxRow? fetch_email_async(Transaction? transaction, + OutboxEmailIdentifier id, Cancellable? cancellable = null) throws Error { + return yield fetch_email_internal_async(transaction, id, cancellable); + } + + private async Geary.Sqlite.SmtpOutboxRow? fetch_email_internal_async(Transaction? transaction, + OutboxEmailIdentifier id, Cancellable? cancellable = null) throws Error { + Transaction locked = yield obtain_lock_async(transaction, + "SmtpOutboxTable.fetch_email_internal_async", cancellable); + + SQLHeavy.Query query = locked.prepare( + "SELECT id, ordering, message FROM SmtpOutboxTable " + + "WHERE ordering = ?"); + query.bind_int64(0, id.ordering); + + SQLHeavy.QueryResult results = yield query.execute_async(); + check_cancel(cancellable, "fetch_email_internal_async"); + + if (results.finished) + return null; + + SmtpOutboxRow? ret = new SmtpOutboxRow(this, results.fetch_int64(0), results.fetch_int64(1), + results.fetch_string(2), -1); + + check_cancel(cancellable, "fetch_email_internal_async"); + return ret; + } + + // Fetch an email given a database row ID. + private async Geary.Sqlite.SmtpOutboxRow? fetch_email_by_row_id_async(Transaction? transaction, + int64 id, Cancellable? cancellable = null) throws Error { + Transaction locked = yield obtain_lock_async(transaction, "SmtpOutboxTable.fetch_email_by_row_id_async", + cancellable); + + SQLHeavy.Query query = locked.prepare( + "SELECT id, ordering, message FROM SmtpOutboxTable WHERE id = ?"); + query.bind_int64(0, id); + + SQLHeavy.QueryResult results = yield query.execute_async(); + check_cancel(cancellable, "fetch_email_by_row_id_async"); + + if (results.finished) + return null; + + SmtpOutboxRow? ret = new SmtpOutboxRow(this, results.fetch_int64(0), results.fetch_int64(1), + results.fetch_string(2), -1); + + check_cancel(cancellable, "fetch_email_by_row_id_async"); + return ret; + } + + public async void remove_single_email_async(Transaction? transaction, OutboxEmailIdentifier id, + Cancellable? cancellable = null) throws Error { + Transaction locked = yield obtain_lock_async(transaction, "SmtpOutboxTable.remove_email_async", + cancellable); + + SQLHeavy.Query query = locked.prepare( + "DELETE FROM SmtpOutboxTable WHERE ordering=?"); + query.bind_int64(0, id.ordering); + + yield query.execute_async(); + } +} + diff --git a/src/engine/util/util-string.vala b/src/engine/util/util-string.vala index c0397ac8..f3556e9f 100644 --- a/src/engine/util/util-string.vala +++ b/src/engine/util/util-string.vala @@ -4,6 +4,10 @@ * (version 2.1 or later). See the COPYING file in this distribution. */ +// GLib's character-based substring function. +[CCode (cname = "g_utf8_substring")] +extern string glib_substring(string str, long start_pos, long end_pos); + namespace Geary.String { public inline bool is_null_or_whitespace(string? str) { @@ -89,5 +93,16 @@ public string reduce_whitespace(string _s) { return s; } +// Slices a string to, at most, max_length number of bytes (NOT including the null.) +// Due to the nature of UTF-8, it may be a few bytes shorter than the maximum. +// +// If the string is less than max_length bytes, it will be return unchanged. +public string safe_byte_substring(string s, ssize_t max_length) { + if (s.length < max_length) + return s; + + return glib_substring(s, 0, s.char_count(max_length)); +} + }