Commit bc8a77bc authored by Eric Gregory's avatar Eric Gregory

Closes #3769 Initial save to drafts functionality

parent 14b9343d
......@@ -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,8 +735,36 @@ 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() {
......@@ -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() {
......
......@@ -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");
......
......@@ -109,6 +109,30 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account {
public abstract async Gee.Collection<string>? get_search_matches_async(
Gee.Collection<Geary.EmailIdentifier> 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;
}
......
......@@ -275,6 +275,30 @@ public interface Geary.Account : BaseObject {
public abstract async Gee.Collection<string>? get_search_matches_async(
Gee.Collection<Geary.EmailIdentifier> 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.
*/
......
......@@ -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
}
......@@ -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;
}
......@@ -11,7 +11,7 @@
*/
public class Geary.NamedFlag : BaseObject, Gee.Hashable<Geary.NamedFlag> {
private string name;
public string name { get; private set; }
public NamedFlag(string name) {
this.name = name;
......
......@@ -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<Geary.Email>? list_email_by_id_async(
......
......@@ -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);
}
......
......@@ -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<MailboxInformation>? list_results, Gee.List<StatusData>? status_results,
Cancellable? cancellable) throws Error {
......
......@@ -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<MessageFlag> msg_flags = new Gee.ArrayList<MessageFlag>();
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<NamedFlag> added) {
foreach (NamedFlag flag in added) {
if (flag.equal_to(UNREAD))
......
......@@ -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);
}
......
......@@ -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");
......
......@@ -142,6 +142,11 @@
<object class="GtkAction" id="close"/>
<accelerator key="w" modifiers="GDK_CONTROL_MASK"/>
</child>
<child>
<object class="GtkAction" id="save">
<property name="label" translatable="yes">Sa_ve Draft</property>
</object>
</child>
</object>
<object class="GtkArrow" id="menu arrow">
<property name="visible">True</property>
......@@ -554,10 +559,8 @@
</child>
<child>
<object class="GtkToolItem" id="filler">
<property name="use_action_appearance">False</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="use_action_appearance">False</property>
<child>
<placeholder/>
</child>
......@@ -568,8 +571,8 @@
</child>
<child>
<object class="GtkToggleToolButton" id="menu button">
<property name="related_action">menu</property>
<property name="use_action_appearance">False</property>
<property name="related_action">menu</property>
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="tooltip_text" translatable="yes" comments="Various options for formatting text">Formatting Menu</property>
......@@ -665,10 +668,9 @@
<child>
<object class="GtkButton" id="add_pending_attachments">
<property name="label" translatable="yes">_Include Original Attachments</property>
<property name="visible">False</property>
<property name="no_show_all">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
<property name="no_show_all">True</property>
<property name="halign">start</property>
<property name="valign">center</property>
<property name="use_underline">True</property>
......@@ -679,6 +681,19 @@
<property name="position">1</property>
</packing>
</child>
<child>
<object class="GtkButton" id="save_draft">
<property name="related_action">save</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">True</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">2</property>
</packing>
</child>
<child>
<object class="GtkButton" id="Discard">
<property name="label">gtk-discard</property>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment