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
This commit is contained in:
Charles Lindsay 2014-01-29 18:18:31 -08:00
parent 0dcf353ac2
commit cce04b814f
22 changed files with 374 additions and 96 deletions

View file

@ -277,6 +277,7 @@ src/engine/memory/memory-byte-buffer.vala
src/engine/memory/memory-empty-buffer.vala src/engine/memory/memory-empty-buffer.vala
src/engine/memory/memory-file-buffer.vala src/engine/memory/memory-file-buffer.vala
src/engine/memory/memory-growable-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-string-buffer.vala
src/engine/memory/memory-unowned-byte-array-buffer.vala src/engine/memory/memory-unowned-byte-array-buffer.vala
src/engine/memory/memory-unowned-bytes-buffer.vala src/engine/memory/memory-unowned-bytes-buffer.vala

7
sql/version-017.sql Normal file
View file

@ -0,0 +1,7 @@
--
-- 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;

View file

@ -218,6 +218,7 @@ engine/memory/memory-byte-buffer.vala
engine/memory/memory-empty-buffer.vala engine/memory/memory-empty-buffer.vala
engine/memory/memory-file-buffer.vala engine/memory/memory-file-buffer.vala
engine/memory/memory-growable-buffer.vala engine/memory/memory-growable-buffer.vala
engine/memory/memory-offset-buffer.vala
engine/memory/memory-string-buffer.vala engine/memory/memory-string-buffer.vala
engine/memory/memory-unowned-byte-array-buffer.vala engine/memory/memory-unowned-byte-array-buffer.vala
engine/memory/memory-unowned-bytes-buffer.vala engine/memory/memory-unowned-bytes-buffer.vala

View file

@ -50,6 +50,11 @@ public class AddEditPage : Gtk.Box {
set { check_remember_password.active = value; } 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 { public string smtp_username {
get { return entry_smtp_username.text; } get { return entry_smtp_username.text; }
set { entry_smtp_username.text = value; } set { entry_smtp_username.text = value; }
@ -139,6 +144,7 @@ public class AddEditPage : Gtk.Box {
private Gtk.Entry entry_nickname; private Gtk.Entry entry_nickname;
private Gtk.ComboBoxText combo_service; private Gtk.ComboBoxText combo_service;
private Gtk.CheckButton check_remember_password; private Gtk.CheckButton check_remember_password;
private Gtk.CheckButton check_save_sent_mail;
private Gtk.Alignment other_info; private Gtk.Alignment other_info;
@ -198,6 +204,7 @@ public class AddEditPage : Gtk.Box {
label_password = (Gtk.Label) builder.get_object("label: password"); label_password = (Gtk.Label) builder.get_object("label: password");
entry_password = (Gtk.Entry) builder.get_object("entry: password"); entry_password = (Gtk.Entry) builder.get_object("entry: password");
check_remember_password = (Gtk.CheckButton) builder.get_object("check: remember_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"); 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_real_name.changed.connect(on_changed);
entry_nickname.changed.connect(on_changed); entry_nickname.changed.connect(on_changed);
check_remember_password.toggled.connect(on_changed); check_remember_password.toggled.connect(on_changed);
check_save_sent_mail.toggled.connect(on_changed);
combo_service.changed.connect(on_changed); combo_service.changed.connect(on_changed);
entry_imap_host.changed.connect(on_changed); entry_imap_host.changed.connect(on_changed);
entry_imap_port.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.user : null,
info.smtp_credentials != null ? info.smtp_credentials.pass : null, info.smtp_credentials != null ? info.smtp_credentials.pass : null,
info.service_provider, info.service_provider,
info.save_sent_mail,
info.allow_save_sent_mail(),
info.default_imap_server_host, info.default_imap_server_host,
info.default_imap_server_port, info.default_imap_server_port,
info.default_imap_server_ssl, info.default_imap_server_ssl,
@ -304,6 +314,8 @@ public class AddEditPage : Gtk.Box {
string? initial_smtp_username = null, string? initial_smtp_username = null,
string? initial_smtp_password = null, string? initial_smtp_password = null,
int initial_service_provider = Geary.ServiceProvider.GMAIL, 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, string? initial_default_imap_host = null,
uint16 initial_default_imap_port = Geary.Imap.ClientConnection.DEFAULT_PORT_SSL, uint16 initial_default_imap_port = Geary.Imap.ClientConnection.DEFAULT_PORT_SSL,
bool initial_default_imap_ssl = true, bool initial_default_imap_ssl = true,
@ -322,6 +334,8 @@ public class AddEditPage : Gtk.Box {
email_address = initial_email ?? ""; email_address = initial_email ?? "";
password = initial_imap_password != null ? initial_imap_password : ""; password = initial_imap_password != null ? initial_imap_password : "";
remember_password = initial_remember_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); set_service_provider((Geary.ServiceProvider) initial_service_provider);
combo_imap_encryption.active = Encryption.NONE; // Must be default; set to real value below. combo_imap_encryption.active = Encryption.NONE; // Must be default; set to real value below.
combo_smtp_encryption.active = Encryption.NONE; combo_smtp_encryption.active = Encryption.NONE;
@ -538,6 +552,7 @@ public class AddEditPage : Gtk.Box {
account_information.imap_remember_password = remember_password; account_information.imap_remember_password = remember_password;
account_information.smtp_remember_password = remember_password; account_information.smtp_remember_password = remember_password;
account_information.service_provider = get_service_provider(); 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_host = imap_host;
account_information.default_imap_server_port = imap_port; account_information.default_imap_server_port = imap_port;
account_information.default_imap_server_ssl = imap_ssl; account_information.default_imap_server_ssl = imap_ssl;
@ -573,6 +588,7 @@ public class AddEditPage : Gtk.Box {
welcome_box.visible = mode == PageMode.WELCOME; welcome_box.visible = mode == PageMode.WELCOME;
entry_nickname.visible = label_nickname.visible = mode != PageMode.WELCOME; entry_nickname.visible = label_nickname.visible = mode != PageMode.WELCOME;
storage_container.visible = mode == PageMode.EDIT; storage_container.visible = mode == PageMode.EDIT;
check_save_sent_mail.visible = mode != PageMode.WELCOME;
if (get_service_provider() == Geary.ServiceProvider.OTHER) { if (get_service_provider() == Geary.ServiceProvider.OTHER) {
// Display all options for custom providers. // Display all options for custom providers.

View file

@ -653,7 +653,11 @@ public class GearyController : Geary.BaseObject {
break; break;
case Geary.Account.Problem.EMAIL_DELIVERY_FAILURE: 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; break;
default: 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; bool activate_message = false;
try { try {
// Due to a timing hole where it's possible to delete a message // 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 (activate_message) {
if (!main_window.status_bar.is_message_active(StatusBar.Message.OUTBOX_SEND_FAILURE)) if (!main_window.status_bar.is_message_active(message))
main_window.status_bar.activate_message(StatusBar.Message.OUTBOX_SEND_FAILURE); main_window.status_bar.activate_message(message);
libnotify.set_error_notification(_("Error sending email"), switch (message) {
_("Geary encountered an error sending an email. If the problem persists, please manually delete the email from your Outbox folder.")); 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) { private void on_account_email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
if (folder.special_folder_type == Geary.SpecialFolderType.OUTBOX) { 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_SEND_FAILURE);
main_window.status_bar.deactivate_message(StatusBar.Message.OUTBOX_SAVE_SENT_MAIL_FAILED);
libnotify.clear_error_notification(); libnotify.clear_error_notification();
} }
} }

View file

@ -16,7 +16,8 @@
public class StatusBar : Gtk.Statusbar { public class StatusBar : Gtk.Statusbar {
public enum Message { public enum Message {
OUTBOX_SENDING, OUTBOX_SENDING,
OUTBOX_SEND_FAILURE; OUTBOX_SEND_FAILURE,
OUTBOX_SAVE_SENT_MAIL_FAILED;
internal string get_text() { internal string get_text() {
switch (this) { switch (this) {
@ -26,6 +27,10 @@ public class StatusBar : Gtk.Statusbar {
case Message.OUTBOX_SEND_FAILURE: case Message.OUTBOX_SEND_FAILURE:
/// Displayed in the space-limited status bar when a message fails to be sent due to error. /// Displayed in the space-limited status bar when a message fails to be sent due to error.
return _("Error sending email"); 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: default:
assert_not_reached(); assert_not_reached();
} }
@ -37,6 +42,8 @@ public class StatusBar : Gtk.Statusbar {
return Context.OUTBOX; return Context.OUTBOX;
case Message.OUTBOX_SEND_FAILURE: case Message.OUTBOX_SEND_FAILURE:
return Context.OUTBOX; return Context.OUTBOX;
case Message.OUTBOX_SAVE_SENT_MAIL_FAILED:
return Context.OUTBOX;
default: default:
assert_not_reached(); assert_not_reached();
} }

View file

@ -850,7 +850,7 @@ public class ComposerWindow : Gtk.Window {
// only save HTML drafts to avoid resetting the DOM (which happens when converting the // only save HTML drafts to avoid resetting the DOM (which happens when converting the
// HTML to flowed text) // HTML to flowed text)
draft_id = yield drafts_folder.create_email_async(new Geary.RFC822.Message.from_composed_email( 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; draft_save_label.label = DRAFT_SAVED_TEXT;
} catch (Error e) { } catch (Error e) {

View file

@ -593,6 +593,7 @@ public class ConversationViewer : Gtk.Box {
// <div id="$MESSAGE_ID" class="email"> // <div id="$MESSAGE_ID" class="email">
// <div class="geary_spacer"></div> // <div class="geary_spacer"></div>
// <div class="email_container"> // <div class="email_container">
// <div class="email_warning"></div>
// <div class="button_bar"> // <div class="button_bar">
// <div class="starred button"><img class="icon" /></div> // <div class="starred button"><img class="icon" /></div>
// <div class="unstarred 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) { } catch (Error e) {
warning("Failed to set classes on .email: %s", e.message); 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, private static void on_context_menu(WebKit.DOM.Element clicked_element, WebKit.DOM.Event event,

View file

@ -100,12 +100,8 @@ public abstract class Geary.AbstractAccount : BaseObject, Geary.Account {
Cancellable? cancellable = null) throws Error; Cancellable? cancellable = null) throws Error;
public virtual Geary.Folder? get_special_folder(Geary.SpecialFolderType special) throws Error { public virtual Geary.Folder? get_special_folder(Geary.SpecialFolderType special) throws Error {
foreach (Folder folder in list_folders()) { return Geary.traverse<Geary.Folder>(list_folders())
if (folder.special_folder_type == special) .first_matching(f => f.special_folder_type == special);
return folder;
}
return null;
} }
public abstract async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = null) public abstract async void send_email_async(Geary.ComposedEmail composed, Cancellable? cancellable = null)

View file

@ -26,6 +26,7 @@ public class Geary.AccountInformation : BaseObject {
private const string SMTP_SSL = "smtp_ssl"; private const string SMTP_SSL = "smtp_ssl";
private const string SMTP_STARTTLS = "smtp_starttls"; private const string SMTP_STARTTLS = "smtp_starttls";
private const string SMTP_NOAUTH = "smtp_noauth"; private const string SMTP_NOAUTH = "smtp_noauth";
private const string SAVE_SENT_MAIL_KEY = "save_sent_mail";
// //
// "Retired" keys // "Retired" keys
@ -52,6 +53,20 @@ public class Geary.AccountInformation : BaseObject {
public Geary.ServiceProvider service_provider { get; set; } public Geary.ServiceProvider service_provider { get; set; }
public int prefetch_period_days { 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. // Order for display purposes.
public int ordinal { get; set; } 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 Geary.Credentials? smtp_credentials { get; set; default = new Geary.Credentials(null, null); }
public bool smtp_remember_password { get; set; default = true; } 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.) // Used to create temporary AccountInformation objects. (Note that these cannot be saved.)
public AccountInformation.temp_copy(AccountInformation copy) { public AccountInformation.temp_copy(AccountInformation copy) {
copy_from(copy); copy_from(copy);
@ -100,6 +117,7 @@ public class Geary.AccountInformation : BaseObject {
SERVICE_PROVIDER_KEY, Geary.ServiceProvider.GMAIL.to_string())); SERVICE_PROVIDER_KEY, Geary.ServiceProvider.GMAIL.to_string()));
prefetch_period_days = get_int_value(key_file, GROUP, PREFETCH_PERIOD_DAYS_KEY, prefetch_period_days = get_int_value(key_file, GROUP, PREFETCH_PERIOD_DAYS_KEY,
DEFAULT_PREFETCH_PERIOD_DAYS); 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++); ordinal = get_int_value(key_file, GROUP, ORDINAL_KEY, default_ordinal++);
if (ordinal >= default_ordinal) if (ordinal >= default_ordinal)
@ -134,6 +152,7 @@ public class Geary.AccountInformation : BaseObject {
email = from.email; email = from.email;
service_provider = from.service_provider; service_provider = from.service_provider;
prefetch_period_days = from.prefetch_period_days; prefetch_period_days = from.prefetch_period_days;
save_sent_mail = from.save_sent_mail;
ordinal = from.ordinal; ordinal = from.ordinal;
default_imap_server_host = from.default_imap_server_host; default_imap_server_host = from.default_imap_server_host;
default_imap_server_port = from.default_imap_server_port; 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; 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 * Fetch the passwords for the given services. For each service, if the
* password is unset, use get_passwords_async() first; if the password is * 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_value(GROUP, SMTP_USERNAME_KEY, smtp_credentials.user);
key_file.set_boolean(GROUP, SMTP_REMEMBER_PASSWORD_KEY, smtp_remember_password); 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_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) { if (service_provider == ServiceProvider.OTHER) {
key_file.set_value(GROUP, IMAP_HOST, default_imap_server_host); key_file.set_value(GROUP, IMAP_HOST, default_imap_server_host);

View file

@ -12,6 +12,7 @@ public interface Geary.Account : BaseObject {
NETWORK_UNAVAILABLE, NETWORK_UNAVAILABLE,
DATABASE_FAILURE, DATABASE_FAILURE,
EMAIL_DELIVERY_FAILURE, EMAIL_DELIVERY_FAILURE,
SAVE_SENT_MAIL_FAILED,
} }
public abstract Geary.AccountInformation information { get; protected set; } public abstract Geary.AccountInformation information { get; protected set; }

View file

@ -31,6 +31,13 @@ public class Geary.EmailFlags : Geary.NamedFlags {
return new NamedFlag("DRAFT"); 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() { public EmailFlags() {
} }
@ -50,5 +57,9 @@ public class Geary.EmailFlags : Geary.NamedFlags {
public inline bool is_draft() { public inline bool is_draft() {
return contains(DRAFT); return contains(DRAFT);
} }
public inline bool is_outbox_sent() {
return contains(OUTBOX_SENT);
}
} }

View file

@ -17,10 +17,11 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
public int64 id; public int64 id;
public int position; public int position;
public int64 ordering; public int64 ordering;
public bool sent;
public Memory.Buffer? message; public Memory.Buffer? message;
public SmtpOutboxEmailIdentifier outbox_id; 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) { SmtpOutboxFolderRoot root) {
assert(position >= 1); assert(position >= 1);
@ -28,6 +29,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
this.position = position; this.position = position;
this.ordering = ordering; this.ordering = ordering;
this.message = message; this.message = message;
this.sent = sent;
outbox_id = new SmtpOutboxEmailIdentifier(id, ordering); outbox_id = new SmtpOutboxEmailIdentifier(id, ordering);
} }
@ -139,14 +141,18 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
try { try {
Gee.ArrayList<OutboxRow> list = new Gee.ArrayList<OutboxRow>(); Gee.ArrayList<OutboxRow> list = new Gee.ArrayList<OutboxRow>();
yield db.exec_transaction_async(Db.TransactionType.RO, (cx, cancellable) => { yield db.exec_transaction_async(Db.TransactionType.RO, (cx, cancellable) => {
Db.Statement stmt = cx.prepare( Db.Statement stmt = cx.prepare("""
"SELECT id, ordering, message FROM SmtpOutboxTable ORDER BY ordering"); SELECT id, ordering, message
FROM SmtpOutboxTable
WHERE sent = 0
ORDER BY ordering
""");
Db.Result results = stmt.exec(cancellable); Db.Result results = stmt.exec(cancellable);
int position = 1; int position = 1;
while (!results.finished) { while (!results.finished) {
list.add(new OutboxRow(results.rowid_at(0), position++, results.int64_at(1), 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); results.next(cancellable);
} }
@ -172,9 +178,9 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
try { try {
row = yield outbox_queue.recv_async(); row = yield outbox_queue.recv_async();
// Ignore messages that have since been deleted. // Ignore messages that have since been sent.
if (!yield ordering_exists_async(row.ordering, null)) { if (!yield is_unsent_async(row.ordering, null)) {
debug("Dropping deleted outbox message %s", row.outbox_id.to_string()); debug("Dropping sent outbox message %s", row.outbox_id.to_string());
continue; continue;
} }
} catch (Error wait_err) { } catch (Error wait_err) {
@ -232,7 +238,7 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
CredentialsMediator.ServiceFlag.SMTP, true)) CredentialsMediator.ServiceFlag.SMTP, true))
report = false; report = false;
} catch (Error e) { } catch (Error e) {
debug("Error prompting for IMAP password: %s", e.message); debug("Error prompting for SMTP password: %s", e.message);
} }
if (report) if (report)
@ -251,20 +257,40 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
continue; 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 // Remove from database ... can't use remove_email_async() because this runs even if
// the outbox is closed as a Geary.Folder. // the outbox is closed as a Geary.Folder.
try { try {
debug("Outbox postman: Removing \"%s\" (ID:%s) from database", message_subject(message), debug("Outbox postman: Deleting row %s", row.outbox_id.to_string());
row.outbox_id.to_string());
Gee.ArrayList<SmtpOutboxEmailIdentifier> list = new Gee.ArrayList<SmtpOutboxEmailIdentifier>(); Gee.ArrayList<SmtpOutboxEmailIdentifier> list = new Gee.ArrayList<SmtpOutboxEmailIdentifier>();
list.add(row.outbox_id); list.add(row.outbox_id);
yield internal_remove_email_async(list, null); yield internal_remove_email_async(list, null);
} catch (Error rm_err) { } catch (Error e) {
debug("Outbox postman: Unable to remove row from database: %s", rm_err.message); 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"); 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); 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); email_count = do_get_email_count(cx, cancellable);
return Db.TransactionOutcome.COMMIT; return Db.TransactionOutcome.COMMIT;
@ -366,15 +392,23 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
Db.Statement stmt; Db.Statement stmt;
if (initial_id != null) { if (initial_id != null) {
stmt = cx.prepare( stmt = cx.prepare("""
"SELECT id, ordering, message FROM SmtpOutboxTable WHERE ordering >= ? " SELECT id, ordering, message, sent
+ "ORDER BY ordering %s LIMIT ?".printf(dir)); FROM SmtpOutboxTable
WHERE ordering >= ?
ORDER BY ordering %s
LIMIT ?
""".printf(dir));
stmt.bind_int64(0, stmt.bind_int64(0,
flags.is_including_id() ? initial_id.ordering : initial_id.ordering + 1); flags.is_including_id() ? initial_id.ordering : initial_id.ordering + 1);
stmt.bind_int(1, count); stmt.bind_int(1, count);
} else { } else {
stmt = cx.prepare( stmt = cx.prepare("""
"SELECT id, ordering, message FROM SmtpOutboxTable ORDER BY ordering %s LIMIT ?".printf(dir)); SELECT id, ordering, message, sent
FROM SmtpOutboxTable
ORDER BY ordering %s
LIMIT ?
""".printf(dir));
stmt.bind_int(0, count); 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, 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; position += flags.is_newest_to_oldest() ? -1 : 1;
assert(position >= 1); assert(position >= 1);
} while (results.next()); } while (results.next());
@ -480,6 +514,23 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
return row_to_email(row); return row_to_email(row);
} }
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, public virtual async void remove_email_async(Gee.List<Geary.EmailIdentifier> email_ids,
Cancellable? cancellable = null) throws Error { Cancellable? cancellable = null) throws Error {
check_open(); check_open();
@ -538,7 +589,10 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
Geary.Email email = message.get_email(row.outbox_id); Geary.Email email = message.get_email(row.outbox_id);
// TODO: Determine message's total size (header + body) to store in Properties. // 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_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; return email;
} }
@ -580,11 +634,40 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
email_sent(rfc822); 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; bool exists = false;
yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => { yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
Db.Statement stmt = cx.prepare( 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); stmt.bind_int64(0, ordering);
exists = !stmt.exec(cancellable).finished; 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) private OutboxRow? do_fetch_row_by_ordering(Db.Connection cx, int64 ordering, Cancellable? cancellable)
throws Error { throws Error {
Db.Statement stmt = cx.prepare( Db.Statement stmt = cx.prepare("""
"SELECT id, message FROM SmtpOutboxTable WHERE ordering=?"); SELECT id, message, sent
FROM SmtpOutboxTable
WHERE ordering=?
""");
stmt.bind_int64(0, ordering); stmt.bind_int64(0, ordering);
Db.Result results = stmt.exec(cancellable); Db.Result results = stmt.exec(cancellable);
@ -654,7 +740,16 @@ private class Geary.SmtpOutboxFolder : Geary.AbstractLocalFolder, Geary.FolderSu
if (position < 1) if (position < 1)
return null; 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) private bool do_remove_email(Db.Connection cx, SmtpOutboxEmailIdentifier id, Cancellable? cancellable)

View file

@ -528,7 +528,10 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.AbstractAccount {
Cancellable? cancellable = null) throws Error { Cancellable? cancellable = null) throws Error {
check_open(); 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 // don't use create_email_async() as that requires the folder be open to use
yield local.outbox.enqueue_email_async(rfc822, cancellable); yield local.outbox.enqueue_email_async(rfc822, cancellable);

View file

@ -5,14 +5,18 @@
*/ */
// Sent Mail generally is the same as other mail folders, but it doesn't support key features, // 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 // 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, public GenericSentMailFolder(GenericAccount account, Imap.Account remote, ImapDB.Account local,
ImapDB.Folder local_folder, SpecialFolderType special_folder_type) { ImapDB.Folder local_folder, SpecialFolderType special_folder_type) {
base (account, remote, local, local_folder, 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);
}
}

View file

@ -887,6 +887,8 @@ private class Geary.Imap.Folder : BaseObject {
if (flags != null) { if (flags != null) {
Imap.EmailFlags imap_flags = Imap.EmailFlags.from_api_email_flags(flags); Imap.EmailFlags imap_flags = Imap.EmailFlags.from_api_email_flags(flags);
msg_flags = imap_flags.message_flags; msg_flags = imap_flags.message_flags;
} else {
msg_flags = new MessageFlags(new Geary.Collection.SingleItem<MessageFlag>(MessageFlag.SEEN));
} }
InternalDate? internaldate = null; InternalDate? internaldate = null;

View file

@ -0,0 +1,47 @@
/* 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 = new Bytes.from_bytes(buffer.get_bytes(), offset, buffer.size - offset);
return bytes;
}
/**
* {@inheritDoc}
*/
public unowned uint8[] to_unowned_uint8_array() {
return get_bytes().get_data();
}
}

View file

@ -17,9 +17,9 @@ public class Geary.RFC822.Message : BaseObject {
private const string HEADER_IN_REPLY_TO = "In-Reply-To"; private const string HEADER_IN_REPLY_TO = "In-Reply-To";
private const string HEADER_REFERENCES = "References"; private const string HEADER_REFERENCES = "References";
private const string HEADER_MAILER = "X-Mailer"; private const string HEADER_MAILER = "X-Mailer";
private const string HEADER_BCC = "Bcc";
// Internal note: If a field is added here, it *must* be set in Message.from_parts(), // Internal note: If a field is added here, it *must* be set in stock_from_gmime().
// Message.without_bcc(), and stock_from_gmime().
public RFC822.MailboxAddress? sender { get; private set; default = null; } public RFC822.MailboxAddress? sender { get; private set; default = null; }
public RFC822.MailboxAddresses? from { get; private set; default = null; } public RFC822.MailboxAddresses? from { get; private set; default = null; }
public RFC822.MailboxAddresses? to { get; private set; default = null; } public RFC822.MailboxAddresses? to { get; private set; default = null; }
@ -33,6 +33,13 @@ public class Geary.RFC822.Message : BaseObject {
private GMime.Message message; private GMime.Message message;
// Since GMime.Message does a bad job of separating the headers and body (GMime.Message.get_body()
// returns the full message, headers and all), we keep a buffer around that points to the body
// part from the source. This is only needed by get_email(). Unfortunately, we can't always
// set these easily, so sometimes get_email() won't work.
private Memory.Buffer? body_buffer = null;
private size_t? body_offset = null;
public Message(Full full) throws RFC822Error { public Message(Full full) throws RFC822Error {
GMime.Parser parser = new GMime.Parser.with_stream(Utils.create_stream_mem(full.buffer)); GMime.Parser parser = new GMime.Parser.with_stream(Utils.create_stream_mem(full.buffer));
@ -40,6 +47,10 @@ public class Geary.RFC822.Message : BaseObject {
if (message == null) if (message == null)
throw new RFC822Error.INVALID("Unable to parse RFC 822 message"); throw new RFC822Error.INVALID("Unable to parse RFC 822 message");
// See the declaration of these fields for why we do this.
body_buffer = full.buffer;
body_offset = (size_t) parser.get_headers_end();
stock_from_gmime(); stock_from_gmime();
} }
@ -70,10 +81,13 @@ public class Geary.RFC822.Message : BaseObject {
if (message == null) if (message == null)
throw new RFC822Error.INVALID("Unable to parse RFC 822 message"); throw new RFC822Error.INVALID("Unable to parse RFC 822 message");
body_buffer = body.buffer;
body_offset = 0;
stock_from_gmime(); stock_from_gmime();
} }
public Message.from_composed_email(Geary.ComposedEmail email) { public Message.from_composed_email(Geary.ComposedEmail email, string? message_id) {
message = new GMime.Message(true); message = new GMime.Message(true);
// Required headers // Required headers
@ -84,6 +98,8 @@ public class Geary.RFC822.Message : BaseObject {
message.set_sender(sender.to_rfc822_string()); message.set_sender(sender.to_rfc822_string());
message.set_date((time_t) email.date.to_unix(), message.set_date((time_t) email.date.to_unix(),
(int) (email.date.get_utc_offset() / TimeSpan.HOUR)); (int) (email.date.get_utc_offset() / TimeSpan.HOUR));
if (message_id != null)
message.set_message_id(message_id);
// Optional headers // Optional headers
if (email.to != null) { if (email.to != null) {
@ -182,51 +198,21 @@ public class Geary.RFC822.Message : BaseObject {
// Makes a copy of the given message without the BCC fields. This is used for sending the email // 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. // without sending the BCC headers to all recipients.
public Message.without_bcc(Message email) { public Message.without_bcc(Message email) {
message = new GMime.Message(true); // GMime doesn't make it easy to get a copy of the body of a message. It's easy to
// make a new message and add in all the headers, but calling set_mime_part() with
// Required headers. // the existing one's get_mime_part() result yields a double Content-Type header in
sender = email.sender; // the *original* message. Clearly the objects aren't meant to be used like that.
message.set_sender(email.message.get_sender()); // Barring any better way to clone a message, which I couldn't find by looking at
// the docs, we just dump out the old message to a buffer and read it back in to
date = email.date; // create the new object. Kinda sucks, but our hands are tied.
message.set_date_as_string(email.date.to_string()); try {
this.from_buffer (email.message_to_memory_buffer(false, false));
// Optional headers. } catch (Error e) {
if (email.to != null) { error("Error creating a memory buffer from a message: %s", e.message);
to = email.to;
foreach (RFC822.MailboxAddress mailbox in email.to)
message.add_recipient(GMime.RecipientType.TO, mailbox.name, mailbox.address);
} }
if (email.cc != null) { message.remove_header(HEADER_BCC);
cc = email.cc; bcc = null;
foreach (RFC822.MailboxAddress mailbox in email.cc)
message.add_recipient(GMime.RecipientType.CC, mailbox.name, mailbox.address);
}
if (email.in_reply_to != null) {
in_reply_to = email.in_reply_to;
message.set_header(HEADER_IN_REPLY_TO, email.in_reply_to.value);
}
if (email.references != null) {
references = email.references;
message.set_header(HEADER_REFERENCES, email.references.to_rfc822_string());
}
if (email.subject != null) {
subject = email.subject;
message.set_subject(email.subject.value);
}
// User-Agent
if (!Geary.String.is_empty(email.mailer)) {
mailer = email.mailer;
message.set_header(HEADER_MAILER, email.mailer);
}
// Setup body depending on what MIME components were filled out.
message.set_mime_part(email.message.get_mime_part());
} }
private GMime.Object? coalesce_parts(Gee.List<GMime.Object> parts, string subtype) { private GMime.Object? coalesce_parts(Gee.List<GMime.Object> parts, string subtype) {
@ -271,7 +257,16 @@ public class Geary.RFC822.Message : BaseObject {
return part; return part;
} }
/**
* Construct a Geary.Email from a Message. NOTE: this requires you to have created
* the Message in such a way that its body_buffer and body_offset fields will be filled
* out. See the various constructors for details. (Otherwise, we don't have a way
* to get the body part directly, because of GMime's shortcomings.)
*/
public Geary.Email get_email(Geary.EmailIdentifier id) throws Error { public Geary.Email get_email(Geary.EmailIdentifier id) throws Error {
assert(body_buffer != null);
assert(body_offset != null);
Geary.Email email = new Geary.Email(id); Geary.Email email = new Geary.Email(id);
email.set_message_header(new Geary.RFC822.Header(new Geary.Memory.StringBuffer( email.set_message_header(new Geary.RFC822.Header(new Geary.Memory.StringBuffer(
@ -281,8 +276,8 @@ public class Geary.RFC822.Message : BaseObject {
email.set_receivers(to, cc, bcc); email.set_receivers(to, cc, bcc);
email.set_full_references(null, in_reply_to, references); email.set_full_references(null, in_reply_to, references);
email.set_message_subject(subject); email.set_message_subject(subject);
email.set_message_body(new Geary.RFC822.Text(new Geary.Memory.StringBuffer( email.set_message_body(new Geary.RFC822.Text(new Geary.Memory.OffsetBuffer(
message.get_body().to_string()))); body_buffer, body_offset)));
email.set_message_preview(new Geary.RFC822.PreviewText.from_string(get_preview())); email.set_message_preview(new Geary.RFC822.PreviewText.from_string(get_preview()));
return email; return email;

View file

@ -29,7 +29,7 @@ async void main_async() throws Error {
composed_email.body_text = contents; composed_email.body_text = contents;
} }
Geary.RFC822.Message msg = new Geary.RFC822.Message.from_composed_email(composed_email); Geary.RFC822.Message msg = new Geary.RFC822.Message.from_composed_email(composed_email, null);
stdout.printf("\n\n%s\n\n", msg.to_string()); stdout.printf("\n\n%s\n\n", msg.to_string());
yield session.send_email_async(msg.sender, msg); yield session.send_email_async(msg.sender, msg);

View file

@ -125,6 +125,14 @@ hr {
-webkit-transition: border-color 3s ease; -webkit-transition: border-color 3s ease;
-webkit-transition: box-shadow 3s ease; -webkit-transition: box-shadow 3s ease;
} }
.email .email_warning {
display: none;
padding: 1em;
background-color: #fcc;
text-align: center;
}
.email_box { .email_box {
box-sizing: border-box; box-sizing: border-box;
-webkit-box-sizing: border-box; -webkit-box-sizing: border-box;
@ -256,6 +264,7 @@ body:not(.nohide) .email.hide .header_container .avatar {
body:not(.nohide) .email.hide .email { body:not(.nohide) .email.hide .email {
display: none; display: none;
} }
.email:not(.hide) .email_warning.show,
body:not(.nohide) .email.hide .header_container .preview { body:not(.nohide) .email.hide .header_container .preview {
display: block; display: block;
} }

View file

@ -7,6 +7,7 @@
<div class="compressed_note"><span><span></div> <div class="compressed_note"><span><span></div>
<div class="geary_spacer"></div> <div class="geary_spacer"></div>
<div class="email_container"> <div class="email_container">
<div class="email_warning"></div>
<div class="header_container"> <div class="header_container">
<img src="" class="avatar" /> <img src="" class="avatar" />
<div class="button_bar"> <div class="button_bar">

View file

@ -261,6 +261,27 @@
<child> <child>
<placeholder/> <placeholder/>
</child> </child>
<child>
<placeholder/>
</child>
<child>
<object class="GtkCheckButton" id="check: save_sent_mail">
<property name="label" translatable="yes">_Save sent mail</property>
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="receives_default">False</property>
<property name="use_underline">True</property>
<property name="xalign">0</property>
<property name="active">True</property>
<property name="draw_indicator">True</property>
</object>
<packing>
<property name="left_attach">1</property>
<property name="top_attach">6</property>
<property name="width">1</property>
<property name="height">1</property>
</packing>
</child>
</object> </object>
<packing> <packing>
<property name="expand">False</property> <property name="expand">False</property>