From bc8a77bcd1db502477e1b174602d8b9836726884 Mon Sep 17 00:00:00 2001 From: Eric Gregory Date: Thu, 18 Jul 2013 12:56:54 -0700 Subject: [PATCH] Closes #3769 Initial save to drafts functionality --- src/client/composer/composer-window.vala | 39 +++++++++++++++++-- src/client/geary-controller.vala | 6 --- .../abstract/geary-abstract-account.vala | 24 ++++++++++++ src/engine/api/geary-account.vala | 24 ++++++++++++ src/engine/api/geary-engine-error.vala | 4 +- .../api/geary-folder-supports-create.vala | 9 +---- src/engine/api/geary-named-flag.vala | 2 +- .../imap-db/outbox/smtp-outbox-folder.vala | 8 ++-- .../imap-engine-generic-account.vala | 14 +++++++ src/engine/imap/api/imap-account.vala | 24 ++++++++++++ src/engine/imap/api/imap-email-flags.vala | 20 ++++++++++ src/engine/rfc822/rfc822-message.vala | 4 +- src/engine/smtp/smtp-client-session.vala | 2 +- ui/composer.glade | 25 +++++++++--- 14 files changed, 172 insertions(+), 33 deletions(-) diff --git a/src/client/composer/composer-window.vala b/src/client/composer/composer-window.vala index 649f0451..486f64da 100644 --- a/src/client/composer/composer-window.vala +++ b/src/client/composer/composer-window.vala @@ -38,6 +38,7 @@ public class ComposerWindow : Gtk.Window { private const string ACTION_INSERT_LINK = "insertlink"; private const string ACTION_COMPOSE_AS_HTML = "compose as html"; private const string ACTION_CLOSE = "close"; + private const string ACTION_SAVE = "save"; private const string URI_LIST_MIME_TYPE = "text/uri-list"; private const string FILE_URI_PREFIX = "file://"; @@ -84,9 +85,6 @@ public class ComposerWindow : Gtk.Window { /// A list of keywords, separated by pipe ("|") characters, that suggest an attachment public const string ATTACHMENT_KEYWORDS_LOCALIZED = _("attach|enclosed|enclosing|cover letter"); - // Signal sent when the "Send" button is clicked. - public signal void send(ComposerWindow composer); - public Geary.Account account { get; private set; } public string from { get; set; } @@ -279,6 +277,8 @@ public class ComposerWindow : Gtk.Window { actions.get_action(ACTION_CLOSE).activate.connect(on_close); + actions.get_action(ACTION_SAVE).activate.connect(on_save); + ui = new Gtk.UIManager(); ui.insert_action_group(actions, 0); add_accel_group(ui.get_accel_group()); @@ -735,10 +735,38 @@ public class ComposerWindow : Gtk.Window { private void on_send() { if (should_send()) { linkify_document(editor.get_dom_document()); - send(this); + account.send_email_async.begin(get_composed_email()); + destroy(); } } + // 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); + } + + return drafts_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(); + if (drafts_folder == null) { + stdout.printf("No drafts folder available for this account.\n"); + + return; + } + + account.create_email_async.begin(drafts_folder.path, + new Geary.RFC822.Message.from_composed_email(get_composed_email()), + new Geary.EmailFlags(), null, null); + } + private void on_add_attachment_button_clicked() { AttachmentDialog dialog = null; do { @@ -1489,6 +1517,9 @@ public class ComposerWindow : Gtk.Window { 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/client/geary-controller.vala b/src/client/geary-controller.vala index eb361711..dd1856a2 100644 --- a/src/client/geary-controller.vala +++ b/src/client/geary-controller.vala @@ -1415,7 +1415,6 @@ public class GearyController { else window = new ComposerWindow(current_account, compose_type, referred); window.set_position(Gtk.WindowPosition.CENTER); - window.send.connect(on_send); // We want to keep track of the open composer windows, so we can allow the user to cancel // an exit without losing their data. @@ -1537,11 +1536,6 @@ public class GearyController { private void on_zoom_normal() { main_window.conversation_viewer.web_view.zoom_level = 1.0f; } - - private void on_send(ComposerWindow composer_window) { - composer_window.account.send_email_async.begin(composer_window.get_composed_email()); - composer_window.destroy(); - } private void on_sent(Geary.RFC822.Message rfc822) { NotificationBubble.play_sound("message-sent-email"); diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala index 3e0f5b0e..a5b20ff7 100644 --- a/src/engine/abstract/geary-abstract-account.vala +++ b/src/engine/abstract/geary-abstract-account.vala @@ -109,6 +109,30 @@ 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 60c3531c..af23681d 100644 --- a/src/engine/api/geary-account.vala +++ b/src/engine/api/geary-account.vala @@ -275,6 +275,30 @@ 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-engine-error.vala b/src/engine/api/geary-engine-error.vala index 9c1cff6d..25ab0123 100644 --- a/src/engine/api/geary-engine-error.vala +++ b/src/engine/api/geary-engine-error.vala @@ -9,12 +9,12 @@ public errordomain Geary.EngineError { ALREADY_OPEN, ALREADY_EXISTS, NOT_FOUND, - READONLY, BAD_PARAMETERS, BAD_RESPONSE, INCOMPLETE_MESSAGE, SERVER_UNAVAILABLE, ALREADY_CLOSED, - CLOSE_REQUIRED + CLOSE_REQUIRED, + UNSUPPORTED } diff --git a/src/engine/api/geary-folder-supports-create.vala b/src/engine/api/geary-folder-supports-create.vala index 995bbdc0..95896600 100644 --- a/src/engine/api/geary-folder-supports-create.vala +++ b/src/engine/api/geary-folder-supports-create.vala @@ -14,17 +14,12 @@ * without user interaction at some point in the future. */ public interface Geary.FolderSupport.Create : Geary.Folder { - public enum Result { - CREATED, - MERGED - } - /** * 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). * * The Folder must be opened prior to attempting this operation. */ - public abstract async Result create_email_async(Geary.RFC822.Message rfc822, Cancellable? cancellable = null) - throws Error; + public abstract async void create_email_async(Geary.RFC822.Message rfc822, EmailFlags? flags, + DateTime? date_received, Cancellable? cancellable = null) throws Error; } diff --git a/src/engine/api/geary-named-flag.vala b/src/engine/api/geary-named-flag.vala index 13534c81..21b73dcc 100644 --- a/src/engine/api/geary-named-flag.vala +++ b/src/engine/api/geary-named-flag.vala @@ -11,7 +11,7 @@ */ public class Geary.NamedFlag : BaseObject, Gee.Hashable { - private string name; + public string name { get; private set; } public NamedFlag(string name) { this.name = name; diff --git a/src/engine/imap-db/outbox/smtp-outbox-folder.vala b/src/engine/imap-db/outbox/smtp-outbox-folder.vala index 6b4b102a..eca6d5a0 100644 --- a/src/engine/imap-db/outbox/smtp-outbox-folder.vala +++ b/src/engine/imap-db/outbox/smtp-outbox-folder.vala @@ -231,7 +231,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu Db.Statement stmt = cx.prepare( "INSERT INTO SmtpOutboxTable (message, ordering)" + "VALUES (?, (SELECT COALESCE(MAX(ordering), 0) + 1 FROM SmtpOutboxTable))"); - stmt.bind_string_buffer(0, rfc822.get_body_rfc822_buffer_smtp(false)); + stmt.bind_string_buffer(0, rfc822.get_network_buffer(false)); int64 id = stmt.exec_insert(cancellable); @@ -274,13 +274,11 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu return row.outbox_id; } - public virtual async Geary.FolderSupport.Create.Result create_email_async(Geary.RFC822.Message rfc822, - Cancellable? cancellable = null) throws Error { + public virtual async void create_email_async(Geary.RFC822.Message rfc822, EmailFlags? flags, + DateTime? date_received, Cancellable? cancellable = null) throws Error { check_open(); yield enqueue_email_async(rfc822, cancellable); - - return FolderSupport.Create.Result.CREATED; } 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 98625d7c..fea2b5ff 100644 --- a/src/engine/imap-engine/imap-engine-generic-account.vala +++ b/src/engine/imap-engine/imap-engine-generic-account.vala @@ -536,6 +536,20 @@ 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/api/imap-account.vala b/src/engine/imap/api/imap-account.vala index 6b6a76d9..d44849bb 100644 --- a/src/engine/imap/api/imap-account.vala +++ b/src/engine/imap/api/imap-account.vala @@ -341,6 +341,30 @@ 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-email-flags.vala b/src/engine/imap/api/imap-email-flags.vala index 4e996bde..8c0f8801 100644 --- a/src/engine/imap/api/imap-email-flags.vala +++ b/src/engine/imap/api/imap-email-flags.vala @@ -20,6 +20,26 @@ public class Geary.Imap.EmailFlags : Geary.EmailFlags { add(LOAD_REMOTE_IMAGES); } + /** + * Converts a generic {@link Geary.EmailFlags} to IMAP's internal representation of them. + * + * If the Geary.EmailFlags cannot be cast to IMAP's version, they're created. + */ + public static Imap.EmailFlags from_api_email_flags(Geary.EmailFlags api_flags) { + Imap.EmailFlags? imap_flags = api_flags as Imap.EmailFlags; + if (imap_flags != null) + return imap_flags; + + Gee.ArrayList msg_flags = new Gee.ArrayList(); + foreach (Geary.NamedFlag named_flag in api_flags.get_all()) + msg_flags.add(new MessageFlag(named_flag.name)); + + if (!api_flags.is_unread()) + msg_flags.add(MessageFlag.SEEN); + + return new Imap.EmailFlags(new MessageFlags(msg_flags)); + } + protected override void notify_added(Gee.Collection added) { foreach (NamedFlag flag in added) { if (flag.equal_to(UNREAD)) diff --git a/src/engine/rfc822/rfc822-message.vala b/src/engine/rfc822/rfc822-message.vala index 07d8e3f8..8ab50c44 100644 --- a/src/engine/rfc822/rfc822-message.vala +++ b/src/engine/rfc822/rfc822-message.vala @@ -404,7 +404,7 @@ public class Geary.RFC822.Message : BaseObject { * Returns the {@link Message} as a {@link Memory.Buffer} suitable for in-memory use (i.e. * with native linefeed characters). */ - public Memory.Buffer get_body_rfc822_buffer_native() throws RFC822Error { + public Memory.Buffer get_native_buffer() throws RFC822Error { return message_to_memory_buffer(false, false); } @@ -415,7 +415,7 @@ public class Geary.RFC822.Message : BaseObject { * The buffer can also be dot-stuffed if required. See * [[http://tools.ietf.org/html/rfc2821#section-4.5.2]] */ - public Memory.Buffer get_body_rfc822_buffer_smtp(bool dotstuffed) throws RFC822Error { + public Memory.Buffer get_network_buffer(bool dotstuffed) throws RFC822Error { return message_to_memory_buffer(true, dotstuffed); } diff --git a/src/engine/smtp/smtp-client-session.vala b/src/engine/smtp/smtp-client-session.vala index 9b661205..c4144a62 100644 --- a/src/engine/smtp/smtp-client-session.vala +++ b/src/engine/smtp/smtp-client-session.vala @@ -173,7 +173,7 @@ public class Geary.Smtp.ClientSession { // DATA Geary.RFC822.Message email_copy = new Geary.RFC822.Message.without_bcc(email); - response = yield cx.send_data_async(email_copy.get_body_rfc822_buffer_smtp(true), true, + response = yield cx.send_data_async(email_copy.get_network_buffer(true), true, cancellable); if (!response.code.is_success_completed()) response.throw_error("Unable to send message"); diff --git a/ui/composer.glade b/ui/composer.glade index d3411a13..2fd0727f 100644 --- a/ui/composer.glade +++ b/ui/composer.glade @@ -142,6 +142,11 @@ + + + Sa_ve Draft + + True @@ -554,10 +559,8 @@ - False True False - False @@ -568,8 +571,8 @@ - menu False + menu True False Formatting Menu @@ -665,10 +668,9 @@ _Include Original Attachments - False - True True True + True start center True @@ -679,6 +681,19 @@ 1 + + + save + True + True + True + + + False + True + 2 + + gtk-discard