diff --git a/src/client/composer/composer-window.vala b/src/client/composer/composer-window.vala index 486f64da..5bb83e09 100644 --- a/src/client/composer/composer-window.vala +++ b/src/client/composer/composer-window.vala @@ -167,6 +167,11 @@ public class ComposerWindow : Gtk.Window { private bool is_attachment_overlay_visible = false; private Gee.List? pending_attachments = null; + private Geary.FolderSupport.Create? drafts_folder = null; + private Geary.EmailIdentifier? draft_id = null; + private Cancellable cancellable_drafts = new Cancellable(); + private string default_save_label = ""; + private WebKit.WebView editor; // We need to keep a reference to the edit-fixer in composer-window, so it doesn't get // garbage-collected. @@ -431,6 +436,10 @@ public class ComposerWindow : Gtk.Window { chain.append(attachments_box); chain.append(button_area); box.set_focus_chain(chain); + + actions.get_action(ACTION_SAVE).sensitive = false; + default_save_label = actions.get_action(ACTION_SAVE).label; + open_drafts_folder.begin(cancellable_drafts); // Open drafts folder for initial account. } public ComposerWindow.from_mailto(Geary.Account account, string mailto) { @@ -741,30 +750,57 @@ public class ComposerWindow : Gtk.Window { } // Returns the drafts folder for the current From account. - private Geary.Folder? get_drafts_folder() { - Geary.Folder? drafts_folder = null; - try { - drafts_folder = account.get_special_folder(Geary.SpecialFolderType.DRAFTS); - } catch (Error e) { - debug("Error getting drafts folder: %s", e.message); + private async void open_drafts_folder(Cancellable cancellable) throws Error { + if (drafts_folder != null) { + // Close existing folder. + yield drafts_folder.close_async(cancellable); + drafts_folder = null; + draft_id = null; } - return drafts_folder; + actions.get_action(ACTION_SAVE).sensitive = false; + + Geary.FolderSupport.Create? folder = account.get_special_folder(Geary.SpecialFolderType.DRAFTS) + as Geary.FolderSupport.Create; + + if (folder == null) + return; // No drafts folder. + + yield folder.open_async(Geary.Folder.OpenFlags.FAST_OPEN, cancellable); + + // Only show Save button if we have a drafts folder to write to. + actions.get_action(ACTION_SAVE).sensitive = true; + + drafts_folder = folder; } // Save to the draft folder, if available. // Note that drafts are NOT "linkified." private void on_save() { - Geary.Folder? drafts_folder = get_drafts_folder(); + save_async.begin(on_save_completed); + } + + private async void save_async() { if (drafts_folder == null) { - stdout.printf("No drafts folder available for this account.\n"); + warning("No drafts folder available for this account."); return; } - account.create_email_async.begin(drafts_folder.path, - new Geary.RFC822.Message.from_composed_email(get_composed_email()), - new Geary.EmailFlags(), null, null); + actions.get_action(ACTION_SAVE).sensitive = false; + actions.get_action(ACTION_SAVE).set_label(_("Saving...")); + + try { + draft_id = yield drafts_folder.create_email_async(new Geary.RFC822.Message.from_composed_email( + get_composed_email()), new Geary.EmailFlags(), null, draft_id, null); + } catch (Error e) { + warning("Error saving draft: %s", e.message); + } + } + + private void on_save_completed() { + actions.get_action(ACTION_SAVE).sensitive = true; + actions.get_action(ACTION_SAVE).set_label(default_save_label); } private void on_add_attachment_button_clicked() { @@ -1500,6 +1536,8 @@ public class ComposerWindow : Gtk.Window { if (compose_type != ComposeType.NEW_MESSAGE) return; + actions.get_action(ACTION_SAVE).sensitive = false; + // Since we've set the combo box ID to the email addresses, we can // fetch that and use it to grab the account from the engine. string? id = from_multiple.get_active_id(); @@ -1512,14 +1550,13 @@ public class ComposerWindow : Gtk.Window { account = Geary.Engine.instance.get_account_instance(new_account_info); from = new_account_info.get_from().to_rfc822_string(); set_entry_completions(); + + open_drafts_folder.begin(cancellable_drafts); } } catch (Error e) { debug("Error updating account in Composer: %s", e.message); } } - - // Only show Save button if we have a drafts folder to write to. - actions.get_action(ACTION_SAVE).visible = get_drafts_folder() != null; } private void set_entry_completions() { diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala index a5b20ff7..3e0f5b0e 100644 --- a/src/engine/abstract/geary-abstract-account.vala +++ b/src/engine/abstract/geary-abstract-account.vala @@ -109,30 +109,6 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account { public abstract async Gee.Collection? get_search_matches_async( Gee.Collection ids, Cancellable? cancellable = null) throws Error; - public virtual async void create_email_async(Geary.FolderPath path, Geary.RFC822.Message rfc822, - Geary.EmailFlags? flags, DateTime? date_received, Cancellable? cancellable = null) throws Error { - Folder folder = yield fetch_folder_async(path, cancellable); - - FolderSupport.Create? supports_create = folder as FolderSupport.Create; - if (supports_create == null) - throw new EngineError.UNSUPPORTED("Folder %s does not support create", path.to_string()); - - yield supports_create.open_async(Folder.OpenFlags.NONE, cancellable); - - // don't leave folder open if create fails - Error? create_err = null; - try { - yield supports_create.create_email_async(rfc822, flags, date_received, cancellable); - } catch (Error err) { - create_err = err; - } - - yield supports_create.close_async(cancellable); - - if (create_err != null) - throw create_err; - } - public virtual string to_string() { return name; } diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala index af23681d..60c3531c 100644 --- a/src/engine/api/geary-account.vala +++ b/src/engine/api/geary-account.vala @@ -275,30 +275,6 @@ public interface Geary.Account : BaseObject { public abstract async Gee.Collection? get_search_matches_async( Gee.Collection ids, Cancellable? cancellable = null) throws Error; - /** - * Creates (appends) the message to the specified {@link Folder}. - * - * Some implementations may locate the Folder for the caller, check that it supports - * {@link FolderSupport.Create}, then call its create method. In that case, the usual - * {@link EngineError} exceptions apply, except for the requirement that the Folder be open. - * (@link Account} will open the Folder for the caller.) - * - * For other Folders, the Account may be able to bypass opening the folder and save it directly. - * - * The optional {@link EmailFlags} allows for those flags to be set when saved. Some Folders - * may ignore those flags (i.e. Outbox) if not applicable. - * - * The optional DateTime allows for the message's "date received" time to be set when saved. - * Like EmailFlags, this is optional if not applicable. - * - * Both values can be retrieved later via {@link EmailProperties}. - * - * @throws EngineError.NOT_FOUND If {@link FolderPath} could not be resolved. - * @throws EngineError.UNSUPPORTED If the Folder does not support FolderSupport.Create. - */ - public abstract async void create_email_async(Geary.FolderPath path, Geary.RFC822.Message rfc822, - Geary.EmailFlags? flags, DateTime? date_received, Cancellable? cancellable = null) throws Error; - /** * Used only for debugging. Should not be used for user-visible strings. */ diff --git a/src/engine/api/geary-folder-supports-create.vala b/src/engine/api/geary-folder-supports-create.vala index 95896600..6e0c4cd2 100644 --- a/src/engine/api/geary-folder-supports-create.vala +++ b/src/engine/api/geary-folder-supports-create.vala @@ -15,11 +15,20 @@ */ public interface Geary.FolderSupport.Create : Geary.Folder { /** - * Creates a message in the folder. If the message already exists in the {@link Geary.Folder}, - * it will be merged (that is, fields in the message not already present will be added). + * Creates (appends) the message to this folder. * * The Folder must be opened prior to attempting this operation. + * + * The optional {@link EmailFlags} allows for those flags to be set when saved. Some Folders + * may ignore those flags (i.e. Outbox) if not applicable. + * + * The optional DateTime allows for the message's "date received" time to be set when saved. + * Like EmailFlags, this is optional if not applicable. + * + * If an id is passed, this will replace the existing message by deleting it after the new + * message is created. The new message's ID is returned. */ - public abstract async void create_email_async(Geary.RFC822.Message rfc822, EmailFlags? flags, - DateTime? date_received, Cancellable? cancellable = null) throws Error; + public abstract async Geary.EmailIdentifier? create_email_async(Geary.RFC822.Message rfc822, EmailFlags? flags, + DateTime? date_received, Geary.EmailIdentifier? id = null, Cancellable? cancellable = null) throws Error; } + diff --git a/src/engine/imap-db/outbox/smtp-outbox-folder.vala b/src/engine/imap-db/outbox/smtp-outbox-folder.vala index eca6d5a0..05ace57c 100644 --- a/src/engine/imap-db/outbox/smtp-outbox-folder.vala +++ b/src/engine/imap-db/outbox/smtp-outbox-folder.vala @@ -274,11 +274,11 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu return row.outbox_id; } - public virtual async void create_email_async(Geary.RFC822.Message rfc822, EmailFlags? flags, - DateTime? date_received, Cancellable? cancellable = null) throws Error { + public virtual async Geary.EmailIdentifier? create_email_async(Geary.RFC822.Message rfc822, EmailFlags? flags, + DateTime? date_received, Geary.EmailIdentifier? id = null, Cancellable? cancellable = null) throws Error { check_open(); - yield enqueue_email_async(rfc822, cancellable); + return yield enqueue_email_async(rfc822, cancellable); } public override async Gee.List? list_email_by_id_async( diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala b/src/engine/imap-engine/imap-engine-generic-account.vala index fea2b5ff..98625d7c 100644 --- a/src/engine/imap-engine/imap-engine-generic-account.vala +++ b/src/engine/imap-engine/imap-engine-generic-account.vala @@ -536,20 +536,6 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount { return yield local.get_search_matches_async(previous_prepared_search_query, ids, cancellable); } - public override async void create_email_async(FolderPath path, RFC822.Message rfc822, Geary.EmailFlags? flags, - DateTime? date_received, Cancellable? cancellable = null) throws Error { - // local folders go through normal paths - Folder? folder = local_only.get(path); - if (folder != null) { - yield base.create_email_async(path, rfc822, flags, date_received, cancellable); - - return; - } - - // use IMAP APPEND command on remote folders, which doesn't require opening a folder - yield remote.create_email_async(path, rfc822, flags, date_received, cancellable); - } - private void on_login_failed(Geary.Credentials? credentials) { do_login_failed_async.begin(credentials); } diff --git a/src/engine/imap-engine/imap-engine-generic-drafts-folder.vala b/src/engine/imap-engine/imap-engine-generic-drafts-folder.vala index 4eb2e91d..26141313 100644 --- a/src/engine/imap-engine/imap-engine-generic-drafts-folder.vala +++ b/src/engine/imap-engine/imap-engine-generic-drafts-folder.vala @@ -8,7 +8,8 @@ // // Service-specific accounts can use this or subclass it for further customization -private class Geary.ImapEngine.GenericDraftsFolder : GenericFolder, Geary.FolderSupport.Remove { +private class Geary.ImapEngine.GenericDraftsFolder : GenericFolder, Geary.FolderSupport.Remove, + Geary.FolderSupport.Create { public GenericDraftsFolder(GenericAccount account, Imap.Account remote, ImapDB.Account local, ImapDB.Folder local_folder, SpecialFolderType special_folder_type) { base (account, remote, local, local_folder, special_folder_type); @@ -18,5 +19,10 @@ private class Geary.ImapEngine.GenericDraftsFolder : GenericFolder, Geary.Folder Cancellable? cancellable = null) throws Error { yield expunge_email_async(email_ids, cancellable); } + + public new async Geary.EmailIdentifier? create_email_async(RFC822.Message message, Geary.EmailFlags? flags, + DateTime? date_received, Geary.EmailIdentifier? id, Cancellable? cancellable) throws Error { + return yield base.create_email_async(message, flags, date_received, id, cancellable); + } } diff --git a/src/engine/imap-engine/imap-engine-generic-folder.vala b/src/engine/imap-engine/imap-engine-generic-folder.vala index cbbb593c..9e8128a4 100644 --- a/src/engine/imap-engine/imap-engine-generic-folder.vala +++ b/src/engine/imap-engine/imap-engine-generic-folder.vala @@ -1107,5 +1107,27 @@ private class Geary.ImapEngine.GenericFolder : Geary.AbstractFolder, Geary.Folde // now the EmailIdentifier should be valid, but use the one generated by the list operation return list[0].id; } + + internal async Geary.EmailIdentifier create_email_async(RFC822.Message rfc822, Geary.EmailFlags? flags, + DateTime? date_received, Geary.EmailIdentifier? id, Cancellable? cancellable = null) throws Error { + yield wait_for_open_async(cancellable); + check_open("create_email_async"); + + if (id != null) + check_id("create_email_async", id); + + // use IMAP APPEND command on remote folders, which doesn't require opening a folder + Geary.EmailIdentifier ret = yield remote_folder.create_email_async(rfc822, flags, + date_received, cancellable); + + // Remove old message. + if (id != null && this is Geary.FolderSupport.Remove) { + Gee.List id_list = new Gee.ArrayList(); + id_list.add(id); + yield expunge_email_async(id_list, cancellable); + } + + return ret; + } } diff --git a/src/engine/imap/api/imap-account.vala b/src/engine/imap/api/imap-account.vala index d44849bb..6b6a76d9 100644 --- a/src/engine/imap/api/imap-account.vala +++ b/src/engine/imap/api/imap-account.vala @@ -341,30 +341,6 @@ private class Geary.Imap.Account : BaseObject { return (list_results.size > 0) ? list_results : null; } - public async void create_email_async(Geary.FolderPath path, RFC822.Message message, - Geary.EmailFlags? flags, DateTime? date_received, Cancellable? cancellable) throws Error { - check_open(); - - Geary.FolderPath? processed = normalize_inbox(path); - if (processed == null) - throw new ImapError.INVALID("Invalid path %s", path.to_string()); - - MessageFlags? msg_flags = null; - if (flags != null) { - Imap.EmailFlags imap_flags = Imap.EmailFlags.from_api_email_flags(flags); - msg_flags = imap_flags.message_flags; - } - - InternalDate? internaldate = null; - if (date_received != null) - internaldate = new InternalDate.from_date_time(date_received); - - AppendCommand cmd = new AppendCommand(new MailboxSpecifier.from_folder_path(processed, null), - msg_flags, internaldate, message.get_network_buffer(false)); - - yield send_command_async(cmd, null, null, cancellable); - } - private async StatusResponse send_command_async(Command cmd, Gee.List? list_results, Gee.List? status_results, Cancellable? cancellable) throws Error { diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala index bcdf8fb3..330f64fd 100644 --- a/src/engine/imap/api/imap-folder.vala +++ b/src/engine/imap/api/imap-folder.vala @@ -261,13 +261,12 @@ private class Geary.Imap.Folder : BaseObject { } // All commands must executed inside the cmd_mutex; returns FETCH or STORE results - private async void exec_commands_async(Gee.Collection cmds, + private async Gee.Map? exec_commands_async(Gee.Collection cmds, out Gee.HashMap? fetched, out Gee.TreeSet? search_results, Cancellable? cancellable) throws Error { int token = yield cmd_mutex.claim_async(cancellable); - - // execute commands with mutex locked Gee.Map? responses = null; + // execute commands with mutex locked Error? err = null; try { responses = yield session.send_multiple_commands_async(cmds, cancellable); @@ -300,6 +299,8 @@ private class Geary.Imap.Folder : BaseObject { assert(responses != null); foreach (Command cmd in responses.keys) throw_on_failed_status(responses.get(cmd), cmd); + + return responses; } private void throw_on_failed_status(StatusResponse response, Command cmd) throws Error { @@ -522,7 +523,7 @@ private class Geary.Imap.Folder : BaseObject { new MailboxSpecifier.from_folder_path(destination, null)); Gee.Collection cmds = new Collection.SingleItem(cmd); - yield exec_commands_async(cmds, null, null, cancellable); + yield exec_commands_async(cmds, null, null, cancellable); } // TODO: Support MOVE extension @@ -835,6 +836,39 @@ private class Geary.Imap.Folder : BaseObject { return email; } + internal async Geary.EmailIdentifier? create_email_async(RFC822.Message message, Geary.EmailFlags? flags, + DateTime? date_received, Cancellable? cancellable) throws Error { + check_open(); + + MessageFlags? msg_flags = null; + if (flags != null) { + Imap.EmailFlags imap_flags = Imap.EmailFlags.from_api_email_flags(flags); + msg_flags = imap_flags.message_flags; + } + + InternalDate? internaldate = null; + if (date_received != null) + internaldate = new InternalDate.from_date_time(date_received); + + AppendCommand cmd = new AppendCommand(new MailboxSpecifier.from_folder_path(path, null), + msg_flags, internaldate, message.get_network_buffer(false)); + + Gee.Map responses = yield exec_commands_async( + new Collection.SingleItem(cmd), null, null, cancellable); + + // Grab the response and parse out the UID, if available. + StatusResponse response = responses.get(cmd); + if (response.status == Status.OK && response.response_code != null && + response.response_code.get_response_code_type().is_value("appenduid")) { + UID new_id = new UID(response.response_code.get_as_string(2).as_int()); + + return new Geary.Imap.EmailIdentifier(new_id, path); + } + + // We didn't get a UID back from the server. + return null; + } + private bool required_but_not_set(Geary.Email.Field check, Geary.Email.Field users_fields, Geary.Email email) { return users_fields.require(check) ? !email.fields.is_all_set(check) : false; } diff --git a/src/engine/imap/response/imap-response-code-type.vala b/src/engine/imap/response/imap-response-code-type.vala index 5d94eee5..0cf1facb 100644 --- a/src/engine/imap/response/imap-response-code-type.vala +++ b/src/engine/imap/response/imap-response-code-type.vala @@ -17,6 +17,7 @@ public class Geary.Imap.ResponseCodeType : BaseObject, Gee.Hashable { public const string ALERT = "alert"; public const string ALREADYEXISTS = "alreadyexists"; + public const string APPENDUID = "appenduid"; public const string AUTHENTICATIONFAILED = "authenticationfailed"; public const string AUTHORIZATIONFAILED = "authorizationfailed"; public const string BADCHARSET = "badcharset"; diff --git a/ui/composer.glade b/ui/composer.glade index 2fd0727f..5dec6edb 100644 --- a/ui/composer.glade +++ b/ui/composer.glade @@ -145,6 +145,7 @@ Sa_ve Draft + False @@ -682,7 +683,7 @@ - + save True True diff --git a/ui/composer_accelerators.ui b/ui/composer_accelerators.ui index eaa3022b..4bb8ce0f 100644 --- a/ui/composer_accelerators.ui +++ b/ui/composer_accelerators.ui @@ -28,4 +28,6 @@ + +