[geary] Add option to save sent mail



commit cce04b814f572014c7b8d9ec16f701b8e2f0fe82
Author: Charles Lindsay <chaz yorba org>
Date:   Wed Jan 29 18:18:31 2014 -0800

    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

 po/POTFILES.in                                     |    1 +
 sql/version-017.sql                                |    7 +
 src/CMakeLists.txt                                 |    1 +
 src/client/accounts/add-edit-page.vala             |   16 ++
 src/client/application/geary-controller.vala       |   29 +++-
 src/client/components/status-bar.vala              |    9 +-
 src/client/composer/composer-window.vala           |    2 +-
 .../conversation-viewer/conversation-viewer.vala   |   13 ++
 src/engine/abstract/geary-abstract-account.vala    |    8 +-
 src/engine/api/geary-account-information.vala      |   31 ++++
 src/engine/api/geary-account.vala                  |    1 +
 src/engine/api/geary-email-flags.vala              |   11 ++
 src/engine/imap-db/outbox/smtp-outbox-folder.vala  |  153 ++++++++++++++++----
 .../imap-engine/imap-engine-generic-account.vala   |    5 +-
 .../imap-engine-generic-sent-mail-folder.vala      |   10 +-
 src/engine/imap/api/imap-folder.vala               |    2 +
 src/engine/memory/memory-offset-buffer.vala        |   47 ++++++
 src/engine/rfc822/rfc822-message.vala              |   91 ++++++------
 src/mailer/main.vala                               |    2 +-
 theming/message-viewer.css                         |    9 ++
 theming/message-viewer.html                        |    1 +
 ui/login.glade                                     |   21 +++
 22 files changed, 374 insertions(+), 96 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 6279475..521a3d5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -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
diff --git a/sql/version-017.sql b/sql/version-017.sql
new file mode 100644
index 0000000..f1cb4d3
--- /dev/null
+++ b/sql/version-017.sql
@@ -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;
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 4c3a45a..4a04d92 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -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
diff --git a/src/client/accounts/add-edit-page.vala b/src/client/accounts/add-edit-page.vala
index d8f5830..edcb7b5 100644
--- a/src/client/accounts/add-edit-page.vala
+++ b/src/client/accounts/add-edit-page.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.
diff --git a/src/client/application/geary-controller.vala b/src/client/application/geary-controller.vala
index 6218f56..6b61dea 100644
--- a/src/client/application/geary-controller.vala
+++ b/src/client/application/geary-controller.vala
@@ -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();
         }
     }
diff --git a/src/client/components/status-bar.vala b/src/client/components/status-bar.vala
index 95c877b..6a7bf58 100644
--- a/src/client/components/status-bar.vala
+++ b/src/client/components/status-bar.vala
@@ -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();
             }
diff --git a/src/client/composer/composer-window.vala b/src/client/composer/composer-window.vala
index f1532a6..0f8f5b0 100644
--- a/src/client/composer/composer-window.vala
+++ b/src/client/composer/composer-window.vala
@@ -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) {
diff --git a/src/client/conversation-viewer/conversation-viewer.vala 
b/src/client/conversation-viewer/conversation-viewer.vala
index c1ea5f9..94375a9 100644
--- a/src/client/conversation-viewer/conversation-viewer.vala
+++ b/src/client/conversation-viewer/conversation-viewer.vala
@@ -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,
diff --git a/src/engine/abstract/geary-abstract-account.vala b/src/engine/abstract/geary-abstract-account.vala
index 920a268..1104300 100644
--- a/src/engine/abstract/geary-abstract-account.vala
+++ b/src/engine/abstract/geary-abstract-account.vala
@@ -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)
diff --git a/src/engine/api/geary-account-information.vala b/src/engine/api/geary-account-information.vala
index d12135f..78957ef 100644
--- a/src/engine/api/geary-account-information.vala
+++ b/src/engine/api/geary-account-information.vala
@@ -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;
@@ -151,6 +170,17 @@ public class Geary.AccountInformation : BaseObject {
     }
     
     /**
+     * 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
      * set or it's not in the key store, use prompt_passwords_async().  Return
@@ -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);
diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala
index 3c83947..89902c0 100644
--- a/src/engine/api/geary-account.vala
+++ b/src/engine/api/geary-account.vala
@@ -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; }
diff --git a/src/engine/api/geary-email-flags.vala b/src/engine/api/geary-email-flags.vala
index 2f019d3..b7ff54b 100644
--- a/src/engine/api/geary-email-flags.vala
+++ b/src/engine/api/geary-email-flags.vala
@@ -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);
+    }
 }
 
diff --git a/src/engine/imap-db/outbox/smtp-outbox-folder.vala 
b/src/engine/imap-db/outbox/smtp-outbox-folder.vala
index e196c53..2eca021 100644
--- a/src/engine/imap-db/outbox/smtp-outbox-folder.vala
+++ b/src/engine/imap-db/outbox/smtp-outbox-folder.vala
@@ -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)
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 87684bd..17c6393 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -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);
diff --git a/src/engine/imap-engine/imap-engine-generic-sent-mail-folder.vala 
b/src/engine/imap-engine/imap-engine-generic-sent-mail-folder.vala
index 5068284..98efac3 100644
--- a/src/engine/imap-engine/imap-engine-generic-sent-mail-folder.vala
+++ b/src/engine/imap-engine/imap-engine-generic-sent-mail-folder.vala
@@ -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);
+    }
 }
-
diff --git a/src/engine/imap/api/imap-folder.vala b/src/engine/imap/api/imap-folder.vala
index a38dc75..66edbcd 100644
--- a/src/engine/imap/api/imap-folder.vala
+++ b/src/engine/imap/api/imap-folder.vala
@@ -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;
diff --git a/src/engine/memory/memory-offset-buffer.vala b/src/engine/memory/memory-offset-buffer.vala
new file mode 100644
index 0000000..cd7ea78
--- /dev/null
+++ b/src/engine/memory/memory-offset-buffer.vala
@@ -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();
+    }
+}
diff --git a/src/engine/rfc822/rfc822-message.vala b/src/engine/rfc822/rfc822-message.vala
index 47b816b..6849cea 100644
--- a/src/engine/rfc822/rfc822-message.vala
+++ b/src/engine/rfc822/rfc822-message.vala
@@ -17,9 +17,9 @@ public class Geary.RFC822.Message : BaseObject {
     private const string HEADER_IN_REPLY_TO = "In-Reply-To";
     private const string HEADER_REFERENCES = "References";
     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(),
-    // Message.without_bcc(), and stock_from_gmime().
+    // Internal note: If a field is added here, it *must* be set in stock_from_gmime().
     public RFC822.MailboxAddress? sender { get; private set; default = null; }
     public RFC822.MailboxAddresses? from { 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;
     
+    // 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 {
         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)
             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();
     }
     
@@ -70,10 +81,13 @@ public class Geary.RFC822.Message : BaseObject {
         if (message == null)
             throw new RFC822Error.INVALID("Unable to parse RFC 822 message");
         
+        body_buffer = body.buffer;
+        body_offset = 0;
+        
         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);
         
         // Required headers
@@ -84,6 +98,8 @@ public class Geary.RFC822.Message : BaseObject {
         message.set_sender(sender.to_rfc822_string());
         message.set_date((time_t) email.date.to_unix(),
             (int) (email.date.get_utc_offset() / TimeSpan.HOUR));
+        if (message_id != null)
+            message.set_message_id(message_id);
         
         // Optional headers
         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
     // without sending the BCC headers to all recipients.
     public Message.without_bcc(Message email) {
-        message = new GMime.Message(true);
-        
-        // Required headers.
-        sender = email.sender;
-        message.set_sender(email.message.get_sender());
-        
-        date = email.date;
-        message.set_date_as_string(email.date.to_string());
-        
-        // Optional headers.
-        if (email.to != null) {
-            to = email.to;
-            foreach (RFC822.MailboxAddress mailbox in email.to)
-                message.add_recipient(GMime.RecipientType.TO, mailbox.name, mailbox.address);
-        }
-
-        if (email.cc != null) {
-            cc = email.cc;
-            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);
+        // 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
+        // the existing one's get_mime_part() result yields a double Content-Type header in
+        // the *original* message.  Clearly the objects aren't meant to be used like that.
+        // 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
+        // create the new object.  Kinda sucks, but our hands are tied.
+        try {
+            this.from_buffer (email.message_to_memory_buffer(false, false));
+        } catch (Error e) {
+            error("Error creating a memory buffer from a message: %s", e.message);
         }
         
-        // Setup body depending on what MIME components were filled out.
-        message.set_mime_part(email.message.get_mime_part());
+        message.remove_header(HEADER_BCC);
+        bcc = null;
     }
     
     private GMime.Object? coalesce_parts(Gee.List<GMime.Object> parts, string subtype) {
@@ -271,7 +257,16 @@ public class Geary.RFC822.Message : BaseObject {
         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 {
+        assert(body_buffer != null);
+        assert(body_offset != null);
+        
         Geary.Email email = new Geary.Email(id);
         
         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_full_references(null, in_reply_to, references);
         email.set_message_subject(subject);
-        email.set_message_body(new Geary.RFC822.Text(new Geary.Memory.StringBuffer(
-            message.get_body().to_string())));
+        email.set_message_body(new Geary.RFC822.Text(new Geary.Memory.OffsetBuffer(
+            body_buffer, body_offset)));
         email.set_message_preview(new Geary.RFC822.PreviewText.from_string(get_preview()));
         
         return email;
diff --git a/src/mailer/main.vala b/src/mailer/main.vala
index 0f1cc92..c53eaca 100644
--- a/src/mailer/main.vala
+++ b/src/mailer/main.vala
@@ -29,7 +29,7 @@ async void main_async() throws Error {
             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());
         
         yield session.send_email_async(msg.sender, msg);
diff --git a/theming/message-viewer.css b/theming/message-viewer.css
index 223af81..38ab886 100644
--- a/theming/message-viewer.css
+++ b/theming/message-viewer.css
@@ -125,6 +125,14 @@ hr {
     -webkit-transition: border-color 3s ease;
     -webkit-transition: box-shadow 3s ease;
 }
+
+.email .email_warning {
+    display: none;
+    padding: 1em;
+    background-color: #fcc;
+    text-align: center;
+}
+
 .email_box {
     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 {
         display: none;
     }
+    .email:not(.hide) .email_warning.show,
     body:not(.nohide) .email.hide .header_container .preview {
         display: block;
     }
diff --git a/theming/message-viewer.html b/theming/message-viewer.html
index 46c2c15..7a22310 100644
--- a/theming/message-viewer.html
+++ b/theming/message-viewer.html
@@ -7,6 +7,7 @@
     <div class="compressed_note"><span><span></div>
     <div class="geary_spacer"></div>
     <div class="email_container">
+        <div class="email_warning"></div>
         <div class="header_container">
             <img src="" class="avatar" />
             <div class="button_bar">
diff --git a/ui/login.glade b/ui/login.glade
index bd89d8f..9a62f67 100644
--- a/ui/login.glade
+++ b/ui/login.glade
@@ -261,6 +261,27 @@
         <child>
           <placeholder/>
         </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>
       <packing>
         <property name="expand">False</property>


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]