Commit cce04b81 authored by Charles Lindsay's avatar Charles Lindsay

Add option to save sent mail

This adds the ability for Geary to push sent mail up to the account's
Sent Mail folder (if available).  There's an accompanying account option
that defaults to on (meaning: push sent mail).

The current implementation will leave messages in the Outbox (though
they won't be sent again) if they fail to be pushed to Sent Mail.  This
isn't the best solution, but it at least means you have a way of seeing
the problem and hopefully copying the data elsewhere manually if you
need to save it.

Note that Geary might not always recognize an account's Sent Mail
folder.  This is the case for any "Other" accounts that don't support
the "special use" or "xlist" IMAP extensions.  In this case, Geary will
either throw an error and leave messages in the Outbox, or erase the
message from the Outbox when it's sent, depending on the value of the
account's save sent mail option.  Better support for detecting the Sent
Mail folder in every case is coming soon.

Closes: bgo #713263
parent 0dcf353a
......@@ -277,6 +277,7 @@ src/engine/memory/memory-byte-buffer.vala
src/engine/memory/memory-empty-buffer.vala
src/engine/memory/memory-file-buffer.vala
src/engine/memory/memory-growable-buffer.vala
src/engine/memory/memory-offset-buffer.vala
src/engine/memory/memory-string-buffer.vala
src/engine/memory/memory-unowned-byte-array-buffer.vala
src/engine/memory/memory-unowned-bytes-buffer.vala
......
--
-- We're now keeping sent mail around after sending, so we can also push it up
-- to the Sent Mail folder. This column lets us keep track of the state of
-- messages in the outbox.
--
ALTER TABLE SmtpOutboxTable ADD COLUMN sent INTEGER DEFAULT 0;
......@@ -218,6 +218,7 @@ engine/memory/memory-byte-buffer.vala
engine/memory/memory-empty-buffer.vala
engine/memory/memory-file-buffer.vala
engine/memory/memory-growable-buffer.vala
engine/memory/memory-offset-buffer.vala
engine/memory/memory-string-buffer.vala
engine/memory/memory-unowned-byte-array-buffer.vala
engine/memory/memory-unowned-bytes-buffer.vala
......
......@@ -50,6 +50,11 @@ public class AddEditPage : Gtk.Box {
set { check_remember_password.active = value; }
}
public bool save_sent_mail {
get { return check_save_sent_mail.active; }
set { check_save_sent_mail.active = value; }
}
public string smtp_username {
get { return entry_smtp_username.text; }
set { entry_smtp_username.text = value; }
......@@ -139,6 +144,7 @@ public class AddEditPage : Gtk.Box {
private Gtk.Entry entry_nickname;
private Gtk.ComboBoxText combo_service;
private Gtk.CheckButton check_remember_password;
private Gtk.CheckButton check_save_sent_mail;
private Gtk.Alignment other_info;
......@@ -198,6 +204,7 @@ public class AddEditPage : Gtk.Box {
label_password = (Gtk.Label) builder.get_object("label: password");
entry_password = (Gtk.Entry) builder.get_object("entry: password");
check_remember_password = (Gtk.CheckButton) builder.get_object("check: remember_password");
check_save_sent_mail = (Gtk.CheckButton) builder.get_object("check: save_sent_mail");
label_error = (Gtk.Label) builder.get_object("label: error");
......@@ -242,6 +249,7 @@ public class AddEditPage : Gtk.Box {
entry_real_name.changed.connect(on_changed);
entry_nickname.changed.connect(on_changed);
check_remember_password.toggled.connect(on_changed);
check_save_sent_mail.toggled.connect(on_changed);
combo_service.changed.connect(on_changed);
entry_imap_host.changed.connect(on_changed);
entry_imap_port.changed.connect(on_changed);
......@@ -280,6 +288,8 @@ public class AddEditPage : Gtk.Box {
info.smtp_credentials != null ? info.smtp_credentials.user : null,
info.smtp_credentials != null ? info.smtp_credentials.pass : null,
info.service_provider,
info.save_sent_mail,
info.allow_save_sent_mail(),
info.default_imap_server_host,
info.default_imap_server_port,
info.default_imap_server_ssl,
......@@ -304,6 +314,8 @@ public class AddEditPage : Gtk.Box {
string? initial_smtp_username = null,
string? initial_smtp_password = null,
int initial_service_provider = Geary.ServiceProvider.GMAIL,
bool initial_save_sent_mail = true,
bool allow_save_sent_mail = true,
string? initial_default_imap_host = null,
uint16 initial_default_imap_port = Geary.Imap.ClientConnection.DEFAULT_PORT_SSL,
bool initial_default_imap_ssl = true,
......@@ -322,6 +334,8 @@ public class AddEditPage : Gtk.Box {
email_address = initial_email ?? "";
password = initial_imap_password != null ? initial_imap_password : "";
remember_password = initial_remember_password;
save_sent_mail = initial_save_sent_mail;
check_save_sent_mail.sensitive = allow_save_sent_mail;
set_service_provider((Geary.ServiceProvider) initial_service_provider);
combo_imap_encryption.active = Encryption.NONE; // Must be default; set to real value below.
combo_smtp_encryption.active = Encryption.NONE;
......@@ -538,6 +552,7 @@ public class AddEditPage : Gtk.Box {
account_information.imap_remember_password = remember_password;
account_information.smtp_remember_password = remember_password;
account_information.service_provider = get_service_provider();
account_information.save_sent_mail = save_sent_mail;
account_information.default_imap_server_host = imap_host;
account_information.default_imap_server_port = imap_port;
account_information.default_imap_server_ssl = imap_ssl;
......@@ -573,6 +588,7 @@ public class AddEditPage : Gtk.Box {
welcome_box.visible = mode == PageMode.WELCOME;
entry_nickname.visible = label_nickname.visible = mode != PageMode.WELCOME;
storage_container.visible = mode == PageMode.EDIT;
check_save_sent_mail.visible = mode != PageMode.WELCOME;
if (get_service_provider() == Geary.ServiceProvider.OTHER) {
// Display all options for custom providers.
......
......@@ -653,7 +653,11 @@ public class GearyController : Geary.BaseObject {
break;
case Geary.Account.Problem.EMAIL_DELIVERY_FAILURE:
handle_send_failure();
handle_outbox_failure(StatusBar.Message.OUTBOX_SEND_FAILURE);
break;
case Geary.Account.Problem.SAVE_SENT_MAIL_FAILED:
handle_outbox_failure(StatusBar.Message.OUTBOX_SAVE_SENT_MAIL_FAILED);
break;
default:
......@@ -661,7 +665,7 @@ public class GearyController : Geary.BaseObject {
}
}
private void handle_send_failure() {
private void handle_outbox_failure(StatusBar.Message message) {
bool activate_message = false;
try {
// Due to a timing hole where it's possible to delete a message
......@@ -685,16 +689,29 @@ public class GearyController : Geary.BaseObject {
}
if (activate_message) {
if (!main_window.status_bar.is_message_active(StatusBar.Message.OUTBOX_SEND_FAILURE))
main_window.status_bar.activate_message(StatusBar.Message.OUTBOX_SEND_FAILURE);
libnotify.set_error_notification(_("Error sending email"),
_("Geary encountered an error sending an email. If the problem persists, please manually delete the email from your Outbox folder."));
if (!main_window.status_bar.is_message_active(message))
main_window.status_bar.activate_message(message);
switch (message) {
case StatusBar.Message.OUTBOX_SEND_FAILURE:
libnotify.set_error_notification(_("Error sending email"),
_("Geary encountered an error sending an email. If the problem persists, please manually delete the email from your Outbox folder."));
break;
case StatusBar.Message.OUTBOX_SAVE_SENT_MAIL_FAILED:
libnotify.set_error_notification(_("Error saving sent mail"),
_("Geary encountered an error saving a sent message to Sent Mail. The message will stay in your Outbox folder until you delete it."));
break;
default:
assert_not_reached();
}
}
}
private void on_account_email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
if (folder.special_folder_type == Geary.SpecialFolderType.OUTBOX) {
main_window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SEND_FAILURE);
main_window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SAVE_SENT_MAIL_FAILED);
libnotify.clear_error_notification();
}
}
......
......@@ -16,7 +16,8 @@
public class StatusBar : Gtk.Statusbar {
public enum Message {
OUTBOX_SENDING,
OUTBOX_SEND_FAILURE;
OUTBOX_SEND_FAILURE,
OUTBOX_SAVE_SENT_MAIL_FAILED;
internal string get_text() {
switch (this) {
......@@ -26,6 +27,10 @@ public class StatusBar : Gtk.Statusbar {
case Message.OUTBOX_SEND_FAILURE:
/// Displayed in the space-limited status bar when a message fails to be sent due to error.
return _("Error sending email");
case Message.OUTBOX_SAVE_SENT_MAIL_FAILED:
// Displayed in the space-limited status bar when a message fails to be uploaded
// to Sent Mail after being sent.
return _("Error saving sent mail");
default:
assert_not_reached();
}
......@@ -37,6 +42,8 @@ public class StatusBar : Gtk.Statusbar {
return Context.OUTBOX;
case Message.OUTBOX_SEND_FAILURE:
return Context.OUTBOX;
case Message.OUTBOX_SAVE_SENT_MAIL_FAILED:
return Context.OUTBOX;
default:
assert_not_reached();
}
......
......@@ -850,7 +850,7 @@ public class ComposerWindow : Gtk.Window {
// only save HTML drafts to avoid resetting the DOM (which happens when converting the
// HTML to flowed text)
draft_id = yield drafts_folder.create_email_async(new Geary.RFC822.Message.from_composed_email(
get_composed_email(null, true)), flags, null, draft_id, cancellable);
get_composed_email(null, true), null), flags, null, draft_id, cancellable);
draft_save_label.label = DRAFT_SAVED_TEXT;
} catch (Error e) {
......
......@@ -593,6 +593,7 @@ public class ConversationViewer : Gtk.Box {
// <div id="$MESSAGE_ID" class="email">
// <div class="geary_spacer"></div>
// <div class="email_container">
// <div class="email_warning"></div>
// <div class="button_bar">
// <div class="starred button"><img class="icon" /></div>
// <div class="unstarred button"><img class="icon" /></div>
......@@ -968,6 +969,18 @@ public class ConversationViewer : Gtk.Box {
} catch (Error e) {
warning("Failed to set classes on .email: %s", e.message);
}
try {
WebKit.DOM.HTMLElement email_warning = Util.DOM.select(container, ".email_warning");
Util.DOM.toggle_class(email_warning.get_class_list(), "show", email.email_flags.is_outbox_sent());
if (email.email_flags.is_outbox_sent()) {
email_warning.set_inner_html(
_("This message was sent successfully, but could not be saved to %s.").printf(
Geary.SpecialFolderType.SENT.get_display_name()));
}
} catch (Error e) {
warning("Error showing outbox warning bar: %s", e.message);
}
}
private static void on_context_menu(WebKit.DOM.Element clicked_element, WebKit.DOM.Event event,
......
......@@ -100,12 +100,8 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account {
Cancellable? cancellable = null) throws Error;
public virtual Geary.Folder? get_special_folder(Geary.SpecialFolderType special) throws Error {
foreach (Folder folder in list_folders()) {
if (folder.special_folder_type == special)
return folder;
}
return null;
return Geary.traverse<Geary.Folder>(list_folders())
.first_matching(f => f.special_folder_type == special);
}
public abstract async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = null)
......
......@@ -26,6 +26,7 @@ public class Geary.AccountInformation : BaseObject {
private const string SMTP_SSL = "smtp_ssl";
private const string SMTP_STARTTLS = "smtp_starttls";
private const string SMTP_NOAUTH = "smtp_noauth";
private const string SAVE_SENT_MAIL_KEY = "save_sent_mail";
//
// "Retired" keys
......@@ -52,6 +53,20 @@ public class Geary.AccountInformation : BaseObject {
public Geary.ServiceProvider service_provider { get; set; }
public int prefetch_period_days { get; set; }
/**
* Whether the user has requested that sent mail be saved. Note that Geary
* will only actively push sent mail when this AND allow_save_sent_mail()
* are both true.
*/
public bool save_sent_mail {
// If we aren't allowed to save sent mail due to account type, we want
// to return true here on the assumption that the account will save
// sent mail for us, and thus the user can't disable sent mail from
// being saved.
get { return (allow_save_sent_mail() ? _save_sent_mail : true); }
set { _save_sent_mail = value; }
}
// Order for display purposes.
public int ordinal { get; set; }
......@@ -71,6 +86,8 @@ public class Geary.AccountInformation : BaseObject {
public Geary.Credentials? smtp_credentials { get; set; default = new Geary.Credentials(null, null); }
public bool smtp_remember_password { get; set; default = true; }
private bool _save_sent_mail = true;
// Used to create temporary AccountInformation objects. (Note that these cannot be saved.)
public AccountInformation.temp_copy(AccountInformation copy) {
copy_from(copy);
......@@ -100,6 +117,7 @@ public class Geary.AccountInformation : BaseObject {
SERVICE_PROVIDER_KEY, Geary.ServiceProvider.GMAIL.to_string()));
prefetch_period_days = get_int_value(key_file, GROUP, PREFETCH_PERIOD_DAYS_KEY,
DEFAULT_PREFETCH_PERIOD_DAYS);
save_sent_mail = get_bool_value(key_file, GROUP, SAVE_SENT_MAIL_KEY, true);
ordinal = get_int_value(key_file, GROUP, ORDINAL_KEY, default_ordinal++);
if (ordinal >= default_ordinal)
......@@ -134,6 +152,7 @@ public class Geary.AccountInformation : BaseObject {
email = from.email;
service_provider = from.service_provider;
prefetch_period_days = from.prefetch_period_days;
save_sent_mail = from.save_sent_mail;
ordinal = from.ordinal;
default_imap_server_host = from.default_imap_server_host;
default_imap_server_port = from.default_imap_server_port;
......@@ -150,6 +169,17 @@ public class Geary.AccountInformation : BaseObject {
smtp_remember_password = from.smtp_remember_password;
}
/**
* Return whether this account allows setting the save_sent_mail option.
* If not, save_sent_mail will always be true and setting it will be
* ignored.
*/
public bool allow_save_sent_mail() {
// We should never push mail to Gmail, since its servers automatically
// push sent mail to the sent mail folder.
return service_provider != ServiceProvider.GMAIL;
}
/**
* Fetch the passwords for the given services. For each service, if the
* password is unset, use get_passwords_async() first; if the password is
......@@ -445,6 +475,7 @@ public class Geary.AccountInformation : BaseObject {
key_file.set_value(GROUP, SMTP_USERNAME_KEY, smtp_credentials.user);
key_file.set_boolean(GROUP, SMTP_REMEMBER_PASSWORD_KEY, smtp_remember_password);
key_file.set_integer(GROUP, PREFETCH_PERIOD_DAYS_KEY, prefetch_period_days);
key_file.set_boolean(GROUP, SAVE_SENT_MAIL_KEY, save_sent_mail);
if (service_provider == ServiceProvider.OTHER) {
key_file.set_value(GROUP, IMAP_HOST, default_imap_server_host);
......
......@@ -12,6 +12,7 @@ public interface Geary.Account : BaseObject {
NETWORK_UNAVAILABLE,
DATABASE_FAILURE,
EMAIL_DELIVERY_FAILURE,
SAVE_SENT_MAIL_FAILED,
}
public abstract Geary.AccountInformation information { get; protected set; }
......
......@@ -31,6 +31,13 @@ public class Geary.EmailFlags : Geary.NamedFlags {
return new NamedFlag("DRAFT");
} }
/// Signifies a message in our outbox that has been sent but we're still
/// keeping around for other purposes, i.e. pushing up to Sent Mail.
public static NamedFlag OUTBOX_SENT { owned get {
// This shouldn't ever touch the wire, so make it invalid IMAP.
return new NamedFlag(" OUTBOX SENT ");
} }
public EmailFlags() {
}
......@@ -50,5 +57,9 @@ public class Geary.EmailFlags : Geary.NamedFlags {
public inline bool is_draft() {
return contains(DRAFT);
}
public inline bool is_outbox_sent() {
return contains(OUTBOX_SENT);
}
}
......@@ -17,10 +17,11 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
public int64 id;
public int position;
public int64 ordering;
public bool sent;
public Memory.Buffer? message;
public SmtpOutboxEmailIdentifier outbox_id;
public OutboxRow(int64 id, int position, int64 ordering, Memory.Buffer? message,
public OutboxRow(int64 id, int position, int64 ordering, bool sent, Memory.Buffer? message,
SmtpOutboxFolderRoot root) {
assert(position >= 1);
......@@ -28,6 +29,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
this.position = position;
this.ordering = ordering;
this.message = message;
this.sent = sent;
outbox_id = new SmtpOutboxEmailIdentifier(id, ordering);
}
......@@ -139,14 +141,18 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
try {
Gee.ArrayList<OutboxRow> list = new Gee.ArrayList<OutboxRow>();
yield db.exec_transaction_async(Db.TransactionType.RO, (cx, cancellable) => {
Db.Statement stmt = cx.prepare(
"SELECT id, ordering, message FROM SmtpOutboxTable ORDER BY ordering");
Db.Statement stmt = cx.prepare("""
SELECT id, ordering, message
FROM SmtpOutboxTable
WHERE sent = 0
ORDER BY ordering
""");
Db.Result results = stmt.exec(cancellable);
int position = 1;
while (!results.finished) {
list.add(new OutboxRow(results.rowid_at(0), position++, results.int64_at(1),
results.string_buffer_at(2), _path));
false, results.string_buffer_at(2), _path));
results.next(cancellable);
}
......@@ -172,9 +178,9 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
try {
row = yield outbox_queue.recv_async();
// Ignore messages that have since been deleted.
if (!yield ordering_exists_async(row.ordering, null)) {
debug("Dropping deleted outbox message %s", row.outbox_id.to_string());
// Ignore messages that have since been sent.
if (!yield is_unsent_async(row.ordering, null)) {
debug("Dropping sent outbox message %s", row.outbox_id.to_string());
continue;
}
} catch (Error wait_err) {
......@@ -232,7 +238,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
CredentialsMediator.ServiceFlag.SMTP, true))
report = false;
} catch (Error e) {
debug("Error prompting for IMAP password: %s", e.message);
debug("Error prompting for SMTP password: %s", e.message);
}
if (report)
......@@ -251,20 +257,40 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
continue;
}
// If we got this far the send was successful, so reset the send retry interval.
send_retry_seconds = MIN_SEND_RETRY_INTERVAL_SEC;
if (_account.information.allow_save_sent_mail() && _account.information.save_sent_mail) {
// First mark as sent, so if there's a problem pushing up to Sent Mail,
// we don't retry sending.
try {
debug("Outbox postman: Marking %s as sent", row.outbox_id.to_string());
yield mark_email_as_sent_async(row.outbox_id, null);
} catch (Error e) {
debug("Outbox postman: Unable to mark row as sent: %s", e.message);
}
try {
debug("Outbox postman: Saving %s to sent mail", row.outbox_id.to_string());
yield save_sent_mail_async(message, null);
} catch (Error e) {
debug("Outbox postman: Error saving sent mail: %s", e.message);
report_problem(Geary.Account.Problem.SAVE_SENT_MAIL_FAILED, e);
continue;
}
}
// Remove from database ... can't use remove_email_async() because this runs even if
// the outbox is closed as a Geary.Folder.
try {
debug("Outbox postman: Removing \"%s\" (ID:%s) from database", message_subject(message),
row.outbox_id.to_string());
debug("Outbox postman: Deleting row %s", row.outbox_id.to_string());
Gee.ArrayList<SmtpOutboxEmailIdentifier> list = new Gee.ArrayList<SmtpOutboxEmailIdentifier>();
list.add(row.outbox_id);
yield internal_remove_email_async(list, null);
} catch (Error rm_err) {
debug("Outbox postman: Unable to remove row from database: %s", rm_err.message);
} catch (Error e) {
debug("Outbox postman: Unable to delete row: %s", e.message);
}
// If we got this far the send was successful, so reset the send retry interval.
send_retry_seconds = MIN_SEND_RETRY_INTERVAL_SEC;
}
debug("Exiting outbox postman");
......@@ -314,7 +340,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
int position = do_get_position_by_ordering(cx, ordering, cancellable);
row = new OutboxRow(id, position, ordering, message, _path);
row = new OutboxRow(id, position, ordering, false, message, _path);
email_count = do_get_email_count(cx, cancellable);
return Db.TransactionOutcome.COMMIT;
......@@ -366,15 +392,23 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
Db.Statement stmt;
if (initial_id != null) {
stmt = cx.prepare(
"SELECT id, ordering, message FROM SmtpOutboxTable WHERE ordering >= ? "
+ "ORDER BY ordering %s LIMIT ?".printf(dir));
stmt = cx.prepare("""
SELECT id, ordering, message, sent
FROM SmtpOutboxTable
WHERE ordering >= ?
ORDER BY ordering %s
LIMIT ?
""".printf(dir));
stmt.bind_int64(0,
flags.is_including_id() ? initial_id.ordering : initial_id.ordering + 1);
stmt.bind_int(1, count);
} else {
stmt = cx.prepare(
"SELECT id, ordering, message FROM SmtpOutboxTable ORDER BY ordering %s LIMIT ?".printf(dir));
stmt = cx.prepare("""
SELECT id, ordering, message, sent
FROM SmtpOutboxTable
ORDER BY ordering %s
LIMIT ?
""".printf(dir));
stmt.bind_int(0, count);
}
......@@ -392,7 +426,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
}
list.add(row_to_email(new OutboxRow(results.rowid_at(0), position, ordering,
results.string_buffer_at(2), _path)));
results.bool_at(3), results.string_buffer_at(2), _path)));
position += flags.is_newest_to_oldest() ? -1 : 1;
assert(position >= 1);
} while (results.next());
......@@ -480,7 +514,24 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
return row_to_email(row);
}
public virtual async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
private async void mark_email_as_sent_async(SmtpOutboxEmailIdentifier outbox_id,
Cancellable? cancellable = null) throws Error {
yield db.exec_transaction_async(Db.TransactionType.WR, (cx) => {
do_mark_email_as_sent(cx, outbox_id, cancellable);
return Db.TransactionOutcome.COMMIT;
}, cancellable);
Geary.EmailFlags flags = new Geary.EmailFlags();
flags.add(Geary.EmailFlags.OUTBOX_SENT);
Gee.HashMap<Geary.EmailIdentifier, Geary.EmailFlags> changed_map
= new Gee.HashMap<Geary.EmailIdentifier, Geary.EmailFlags>();
changed_map.set(outbox_id, flags);
notify_email_flags_changed(changed_map);
}
public virtual async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
Cancellable? cancellable = null) throws Error {
check_open();
......@@ -538,7 +589,10 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
Geary.Email email = message.get_email(row.outbox_id);
// TODO: Determine message's total size (header + body) to store in Properties.
email.set_email_properties(new SmtpOutboxEmailProperties(new DateTime.now_local(), -1));
email.set_flags(new Geary.EmailFlags());
Geary.EmailFlags flags = new Geary.EmailFlags();
if (row.sent)
flags.add(Geary.EmailFlags.OUTBOX_SENT);
email.set_flags(flags);
return email;
}
......@@ -580,11 +634,40 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
email_sent(rfc822);
}
private async bool ordering_exists_async(int64 ordering, Cancellable? cancellable) throws Error {
private async void save_sent_mail_async(Geary.RFC822.Message rfc822, Cancellable? cancellable)
throws Error {
Geary.Folder? sent_mail = _account.get_special_folder(Geary.SpecialFolderType.SENT);
Geary.FolderSupport.Create? create = sent_mail as Geary.FolderSupport.Create;
if (create == null)
throw new EngineError.NOT_FOUND("Save sent mail enabled, but no sent mail folder");
bool open = false;
try {
yield create.open_async(Geary.Folder.OpenFlags.FAST_OPEN, cancellable);
open = true;
yield create.create_email_async(rfc822, null, null, null, cancellable);
yield create.close_async(cancellable);
open = false;
} catch (Error e) {
if (open) {
try {
yield create.close_async(cancellable);
open = false;
} catch (Error e) {
debug("Error closing folder %s: %s", create.to_string(), e.message);
}
}
throw e;
}
}
private async bool is_unsent_async(int64 ordering, Cancellable? cancellable) throws Error {
bool exists = false;
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
Db.Statement stmt = cx.prepare(
"SELECT 1 FROM SmtpOutboxTable WHERE ordering=?");
"SELECT 1 FROM SmtpOutboxTable WHERE ordering=? AND sent = 0");
stmt.bind_int64(0, ordering);
exists = !stmt.exec(cancellable).finished;
......@@ -642,8 +725,11 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
private OutboxRow? do_fetch_row_by_ordering(Db.Connection cx, int64 ordering, Cancellable? cancellable)
throws Error {
Db.Statement stmt = cx.prepare(
"SELECT id, message FROM SmtpOutboxTable WHERE ordering=?");
Db.Statement stmt = cx.prepare("""
SELECT id, message, sent
FROM SmtpOutboxTable
WHERE ordering=?
""");
stmt.bind_int64(0, ordering);
Db.Result results = stmt.exec(cancellable);
......@@ -654,7 +740,16 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
if (position < 1)
return null;
return new OutboxRow(results.rowid_at(0), position, ordering, results.string_buffer_at(1), _path);
return new OutboxRow(results.rowid_at(0), position, ordering, results.bool_at(2),
results.string_buffer_at(1), _path);
}
private void do_mark_email_as_sent(Db.Connection cx, SmtpOutboxEmailIdentifier id, Cancellable? cancellable)
throws Error {
Db.Statement stmt = cx.prepare("UPDATE SmtpOutboxTable SET sent = 1 WHERE ordering = ?");
stmt.bind_int64(0, id.ordering);
stmt.exec(cancellable);
}
private bool do_remove_email(Db.Connection cx, SmtpOutboxEmailIdentifier id, Cancellable? cancellable)
......
......@@ -528,7 +528,10 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
Cancellable? cancellable = null) throws Error {
check_open();
Geary.RFC822.Message rfc822 = new Geary.RFC822.Message.from_composed_email(composed);
// TODO: we should probably not use someone else's FQDN in something
// that's supposed to be globally unique...
Geary.RFC822.Message rfc822 = new Geary.RFC822.Message.from_composed_email(
composed, GMime.utils_generate_message_id(information.get_smtp_endpoint().host_specifier));
// don't use create_email_async() as that requires the folder be open to use
yield local.outbox.enqueue_email_async(rfc822, cancellable);
......
......@@ -5,14 +5,18 @@
*/
// Sent Mail generally is the same as other mail folders, but it doesn't support key features,
// like archiving (since sent messages are in the archive).
// like archiving (since sent messages are in the archive). Instead, it supports appending.
//
// Service-specific accounts can use this or subclass it for further customization
private class Geary.ImapEngine.GenericSentMailFolder : GenericFolder {
private class Geary.ImapEngine.GenericSentMailFolder : GenericFolder, Geary.FolderSupport.Create {
public GenericSentMailFolder(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);
}
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);
}
}
......@@ -887,6 +887,8 @@ private class Geary.Imap.Folder : BaseObject {
if (flags != null) {
Imap.EmailFlags imap_flags = Imap.EmailFlags.from_api_email_flags(flags);
msg_flags = imap_flags.message_flags;
} else {
msg_flags = new MessageFlags(new Geary.Collection.SingleItem<MessageFlag>(MessageFlag.SEEN));
}
InternalDate? internaldate = null;
......
/* Copyright 2011-2013 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.
*/
/**
* A buffer that's simply an offset into an existing buffer.
*/
public class Geary.Memory.OffsetBuffer : Geary.Memory.Buffer, Geary.Memory.UnownedBytesBuffer {
/**
* {@inheritDoc}
*/
public override size_t size { get { return buffer.size - offset; } }
/**
* {@inheritDoc}
*/
public override size_t allocated_size { get { return size; } }
private Geary.Memory.Buffer buffer;
private size_t offset;
private Bytes? bytes = null;
public OffsetBuffer(Geary.Memory.Buffer buffer, size_t offset) {
assert(offset < buffer.size);
this.buffer = buffer;
this.offset = offset;
}
/**
* {@inheritDoc}
*/
public override Bytes get_bytes() {
if (bytes == null)
bytes