[geary/wip/714104-refine-account-dialog] Enable config file versioning



commit 123f51dbb2349417c6804fad2ae0144022f6934a
Author: Michael Gratton <mike vee net>
Date:   Fri Dec 7 10:12:02 2018 +1100

    Enable config file versioning
    
    This (way too large patch) enables versioning in config files, and
    provides a mechanism by which to load older versions from a newer
    version of Geary. It also properly introduces a new v1 config format
    that adds several groups to geary.ini to make it easier to read and to
    distinguish between incoming/outgoig services rather than IMAP/SMTP.
    
    To do this, a few things that should have happened in seperate patches
    were also done:
    
     * Make AccountInformation's imap and smtp properties mutable (they
     aren't stateful any more anyway), make ServiceInformation non-abstract
     again and remove the subclasses (to get config versioning happening
     without an explosion of a classes, it all has to be handled from the
     AccountManager anyway), and some other misc things.

 po/POTFILES.in                                     |    2 -
 src/client/accounts/accounts-editor-add-pane.vala  |   26 +-
 src/client/accounts/accounts-editor-list-pane.vala |    2 +-
 .../accounts/accounts-editor-servers-pane.vala     |    4 +-
 src/client/accounts/accounts-manager.vala          | 1093 ++++++++++++++------
 src/client/accounts/add-edit-page.vala             |   29 +-
 src/client/accounts/goa-service-information.vala   |  105 --
 src/client/accounts/local-service-information.vala |   84 --
 src/client/application/goa-mediator.vala           |  105 +-
 src/client/meson.build                             |    2 -
 src/engine/api/geary-account-information.vala      |  140 +--
 src/engine/api/geary-engine.vala                   |   32 +-
 src/engine/api/geary-service-information.vala      |   51 +-
 src/engine/util/util-config-file.vala              |   38 +-
 src/engine/util/util-object.vala                   |    4 +-
 test/client/accounts/accounts-manager-test.vala    |  126 ++-
 .../engine/api/geary-account-information-test.vala |   10 +-
 test/engine/api/geary-engine-test.vala             |    9 +-
 .../engine/api/geary-service-information-mock.vala |   29 -
 test/engine/app/app-conversation-monitor-test.vala |    3 +-
 .../engine/imap-engine/account-processor-test.vala |    3 +-
 test/engine/util-config-file-test.vala             |    7 -
 test/meson.build                                   |    2 -
 23 files changed, 1181 insertions(+), 725 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 0fddc2a9..d3f2263c 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -24,8 +24,6 @@ src/client/accounts/accounts-editor-row.vala
 src/client/accounts/accounts-editor-servers-pane.vala
 src/client/accounts/accounts-manager.vala
 src/client/accounts/add-edit-page.vala
-src/client/accounts/goa-service-information.vala
-src/client/accounts/local-service-information.vala
 src/client/accounts/login-dialog.vala
 src/client/application/application-avatar-store.vala
 src/client/application/autostart-manager.vala
diff --git a/src/client/accounts/accounts-editor-add-pane.vala 
b/src/client/accounts/accounts-editor-add-pane.vala
index bb85ab25..e8771d05 100644
--- a/src/client/accounts/accounts-editor-add-pane.vala
+++ b/src/client/accounts/accounts-editor-add-pane.vala
@@ -159,16 +159,18 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane {
         string message = "";
         Gtk.Widget? to_focus = null;
 
-        Geary.ServiceInformation imap = new_imap_service();
-        Geary.ServiceInformation smtp = new_smtp_service();
-
         Geary.AccountInformation account =
-            this.accounts.new_orphan_account(this.provider, imap, smtp);
+            this.accounts.new_orphan_account(
+                this.provider,
+                new Geary.RFC822.MailboxAddress(
+                    this.real_name.value.text.strip(),
+                    this.email.value.text.strip()
+                )
+            );
+
+        account.imap = new_imap_service();
+        account.imap = new_smtp_service();
 
-        account.append_sender(new Geary.RFC822.MailboxAddress(
-            this.real_name.value.text.strip(),
-            this.email.value.text.strip()
-        ));
         account.nickname = account.primary_mailbox.address;
 
         if (this.provider == Geary.ServiceProvider.OTHER) {
@@ -275,8 +277,8 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane {
         }
     }
 
-    private LocalServiceInformation new_imap_service() {
-        LocalServiceInformation service =
+    private Geary.ServiceInformation new_imap_service() {
+        Geary.ServiceInformation service =
            this.accounts.new_libsecret_service(Geary.Protocol.IMAP);
 
         if (this.provider == Geary.ServiceProvider.OTHER) {
@@ -309,8 +311,8 @@ internal class Accounts.EditorAddPane : Gtk.Grid, EditorPane {
         return service;
     }
 
-    private LocalServiceInformation new_smtp_service() {
-        LocalServiceInformation service =
+    private Geary.ServiceInformation new_smtp_service() {
+        Geary.ServiceInformation service =
            this.accounts.new_libsecret_service(Geary.Protocol.SMTP);
 
         if (this.provider == Geary.ServiceProvider.OTHER) {
diff --git a/src/client/accounts/accounts-editor-list-pane.vala 
b/src/client/accounts/accounts-editor-list-pane.vala
index aa49936d..71f1958b 100644
--- a/src/client/accounts/accounts-editor-list-pane.vala
+++ b/src/client/accounts/accounts-editor-list-pane.vala
@@ -454,7 +454,7 @@ private class Accounts.AddServiceProviderRow : EditorRow<EditorListPane> {
                 bool add_local = false;
                 try {
                     pane.accounts.add_goa_account.end(res);
-                } catch (Error.INVALID err) {
+                } catch (GLib.IOError.NOT_SUPPORTED err) {
                     // Not a supported type, so don't bother logging the error
                     add_local = true;
                 } catch (GLib.Error err) {
diff --git a/src/client/accounts/accounts-editor-servers-pane.vala 
b/src/client/accounts/accounts-editor-servers-pane.vala
index edd610ab..3cf8db9d 100644
--- a/src/client/accounts/accounts-editor-servers-pane.vala
+++ b/src/client/accounts/accounts-editor-servers-pane.vala
@@ -61,8 +61,8 @@ internal class Accounts.EditorServersPane : Gtk.Grid, EditorPane, AccountPane {
         this.editor = editor;
         this.account = account;
         this.engine = ((GearyApplication) editor.application).engine;
-        this.imap_mutable = account.imap.temp_copy();
-        this.smtp_mutable = account.smtp.temp_copy();
+        this.imap_mutable = new Geary.ServiceInformation.copy(account.imap);
+        this.smtp_mutable = new Geary.ServiceInformation.copy(account.smtp);
 
         this.pane_content.set_focus_vadjustment(this.pane_adjustment);
 
diff --git a/src/client/accounts/accounts-manager.vala b/src/client/accounts/accounts-manager.vala
index 836085bd..63fec935 100644
--- a/src/client/accounts/accounts-manager.vala
+++ b/src/client/accounts/accounts-manager.vala
@@ -47,10 +47,49 @@ public enum Accounts.CredentialsProvider {
     }
 }
 
-public errordomain Accounts.Error {
-    INVALID,
-    LOCAL_REMOVED,
-    GOA_REMOVED;
+
+/** Objects that can be used load/save account configuration. */
+public interface Accounts.AccountConfig : GLib.Object {
+
+    /** Loads a supported account from a config file. */
+    public abstract Geary.AccountInformation
+        load(Geary.ConfigFile config,
+             string id,
+             Geary.ServiceProvider? default_provider,
+             string? default_name)
+        throws ConfigError, GLib.KeyFileError;
+
+    /** Saves an account to a config file. */
+    public abstract void save(Geary.AccountInformation account,
+                              Geary.ConfigFile config);
+
+}
+
+
+/** Objects that can be used load/save service configuration. */
+public interface Accounts.ServiceConfig : GLib.Object {
+
+    /** Loads a service from a config file. */
+    public abstract Geary.ServiceInformation
+        load(Geary.ConfigFile config,
+             Geary.AccountInformation account,
+             Geary.Protocol protocol,
+             Geary.CredentialsMediator mediator)
+        throws ConfigError, GLib.KeyFileError;
+
+    /** Saves a service to a config file. */
+    public abstract void save(Geary.AccountInformation account,
+                              Geary.ServiceInformation service,
+                              Geary.ConfigFile config);
+
+}
+
+public errordomain Accounts.ConfigError {
+    IO,
+    SYNTAX,
+    UNSUPPORTED_VERSION,
+    UNAVAILABLE,
+    REMOVED;
 }
 
 
@@ -61,7 +100,8 @@ public errordomain Accounts.Error {
  * removing accounts and their persisted data (configuration,
  * databases, caches, authentication tokens). The manager supports
  * both locally-specified accounts (i.e. those created by the user in
- * the app) and from SSO systems such as GOA.
+ * the app) and from SSO systems such as GOA via the Accounts.Provider
+ * interface.
  *
  * Newly loaded and newly created accounts are first added to the
  * manager with a particular status (enabled, disabled, etc). Accounts
@@ -74,30 +114,13 @@ public class Accounts.Manager : GLib.Object {
     private const string LOCAL_ID_FORMAT = "account_%02u";
     private const string GOA_ID_PREFIX = "goa_";
 
-    private const string ACCOUNT_CONFIG_GROUP = "AccountInformation";
-    private const string ACCOUNT_MANAGER_GROUP = "AccountManager";
-    private const string IMAP_CONFIG_GROUP = "IMAP";
-    private const string SMTP_CONFIG_GROUP = "SMTP";
+    private const int CONFIG_VERSION = 1;
 
-    private const string ALTERNATE_EMAILS_KEY = "alternate_emails";
-    private const string ARCHIVE_FOLDER_KEY = "archive_folder";
-    private const string CREDENTIALS_METHOD_KEY = "credentials_method";
-    private const string CREDENTIALS_PROVIDER_KEY = "credentials_provider";
-    private const string DRAFTS_FOLDER_KEY = "drafts_folder";
-    private const string EMAIL_SIGNATURE_KEY = "email_signature";
-    private const string NICKNAME_KEY = "nickname";
-    private const string ORDINAL_KEY = "ordinal";
-    private const string PREFETCH_PERIOD_DAYS_KEY = "prefetch_period_days";
-    private const string PRIMARY_EMAIL_KEY = "primary_email";
-    private const string REMOVED_KEY = "removed";
-    private const string REAL_NAME_KEY = "real_name";
-    private const string SAVE_DRAFTS_KEY = "save_drafts";
-    private const string SAVE_SENT_MAIL_KEY = "save_sent_mail";
-    private const string SENT_MAIL_FOLDER_KEY = "sent_mail_folder";
-    private const string SERVICE_PROVIDER_KEY = "service_provider";
-    private const string SPAM_FOLDER_KEY = "spam_folder";
-    private const string TRASH_FOLDER_KEY = "trash_folder";
-    private const string USE_EMAIL_SIGNATURE_KEY = "use_email_signature";
+    private const string GROUP_METADATA = "Metadata";
+
+    private const string METADATA_STATUS = "status";
+    private const string METADATA_VERSION = "version";
+    private const string METADATA_GOA = "goa_id";
 
 
     /**
@@ -110,8 +133,24 @@ public class Accounts.Manager : GLib.Object {
         /** The account was disabled by the user. */
         DISABLED,
 
-        /** The account is unavailable to be used, by may come back. */
-        UNAVAILABLE;
+        /** The account is unavailable to be used, but may come back. */
+        UNAVAILABLE,
+
+        /** The account has been removed and is scheduled for deletion. */
+        REMOVED;
+
+        public static Status for_value(string value)
+        throws Geary.EngineError {
+            return Geary.ObjectUtils.from_enum_nick<Status>(
+                typeof(Status), value.ascii_down()
+            );
+        }
+
+        public string to_value() {
+            return Geary.ObjectUtils.to_enum_nick<Status>(
+                typeof(Status), this
+            );
+        }
     }
 
 
@@ -241,8 +280,7 @@ public class Accounts.Manager : GLib.Object {
      */
     public Geary.AccountInformation
         new_orphan_account(Geary.ServiceProvider provider,
-                           Geary.ServiceInformation imap,
-                           Geary.ServiceInformation smtp) {
+                           Geary.RFC822.MailboxAddress primary_mailbox) {
         string? last_account = this.accounts.keys.fold<string?>((next, last) => {
                 string? result = last;
                 if (next.has_prefix(LOCAL_ID_PREFIX)) {
@@ -257,11 +295,11 @@ public class Accounts.Manager : GLib.Object {
         }
         string id = LOCAL_ID_FORMAT.printf(next_id);
 
-        return new Geary.AccountInformation(id, provider, imap, smtp);
+        return new Geary.AccountInformation(id, provider, primary_mailbox);
     }
 
-    public LocalServiceInformation new_libsecret_service(Geary.Protocol service) {
-        return new LocalServiceInformation(service, libsecret);
+    public Geary.ServiceInformation new_libsecret_service(Geary.Protocol service) {
+        return new Geary.ServiceInformation(service, libsecret);
     }
 
     /**
@@ -432,7 +470,7 @@ public class Accounts.Manager : GLib.Object {
      * Determines if an account is a GOA account or not.
      */
     public bool is_goa_account(Geary.AccountInformation account) {
-        return (account.imap is GoaServiceInformation);
+        return (account.imap.mediator is GoaMediator);
     }
 
     /**
@@ -454,7 +492,7 @@ public class Accounts.Manager : GLib.Object {
             break;
 
         default:
-            throw new Error.INVALID("Not supported for GOA");
+            throw new GLib.IOError.NOT_SUPPORTED("Not supported for GOA");
         }
     }
 
@@ -467,14 +505,12 @@ public class Accounts.Manager : GLib.Object {
     public async void show_goa_account(Geary.AccountInformation account,
                                        GLib.Cancellable? cancellable)
         throws GLib.Error {
-        GoaServiceInformation? goa_service =
-           account.imap as GoaServiceInformation;
-        if (goa_service == null) {
-            throw new Error.INVALID("Not a GOA Account");
+        if (!is_goa_account(account)) {
+            throw new GLib.IOError.NOT_SUPPORTED("Not a GOA Account");
         }
 
         yield open_goa_settings(
-            goa_service.account.account.id, null, cancellable
+            to_goa_id(account.id), null, cancellable
         );
     }
 
@@ -486,228 +522,182 @@ public class Accounts.Manager : GLib.Object {
      */
     private async Geary.AccountInformation
         load_account(string id, GLib.Cancellable? cancellable)
-        throws GLib.Error {
+        throws ConfigError {
         GLib.File config_dir = this.user_config_dir.get_child(id);
         GLib.File data_dir = this.user_data_dir.get_child(id);
 
-        Geary.ConfigFile config_file = new Geary.ConfigFile(
+        Geary.ConfigFile config = new Geary.ConfigFile(
             config_dir.get_child(Geary.AccountInformation.SETTINGS_FILENAME)
         );
 
-        yield config_file.load(cancellable);
-
-        Geary.ConfigFile.Group config = config_file.get_group(ACCOUNT_CONFIG_GROUP);
-        CredentialsProvider provider = CredentialsProvider.from_string(
-            config.get_string(
-                CREDENTIALS_PROVIDER_KEY,
-                CredentialsProvider.LIBSECRET.to_string()
-            )
-        );
+        try {
+            yield config.load(cancellable);
+        } catch (GLib.KeyFileError err) {
+            throw new ConfigError.SYNTAX(err.message);
+        } catch (GLib.Error err) {
+            throw new ConfigError.IO(err.message);
+        }
 
-        string primary_email = config.get_string(PRIMARY_EMAIL_KEY);
+        Geary.ConfigFile.Group metadata_config =
+            config.get_group(GROUP_METADATA);
+        int version = metadata_config.get_int(METADATA_VERSION, 0);
+        Status status = Status.ENABLED;
+        try {
+            status = Status.for_value(
+                metadata_config.get_string(
+                    METADATA_STATUS, status.to_value()
+                ));
+        } catch (Geary.EngineError err) {
+            throw new ConfigError.SYNTAX("%s: Invalid status value", id);
+        }
 
-        Geary.AccountInformation? info = null;
-        switch (provider) {
-        case CredentialsProvider.LIBSECRET:
-            info = new_libsecret_account(id, config, primary_email);
-            break;
+        string? goa_id = metadata_config.get_string(METADATA_GOA, null);
+        bool is_goa = (goa_id != null);
+        GoaMediator? goa_mediator = null;
+        Geary.ServiceProvider? default_provider = null;
+        Geary.CredentialsMediator mediator = this.libsecret;
 
-        case CredentialsProvider.GOA:
-            if (this.goa_service != null) {
-                Goa.Object? object = this.goa_service.lookup_by_id(to_goa_id(id));
-                if (object != null) {
-                    info = new_goa_account(id, object);
-                    GoaMediator mediator = (GoaMediator) info.imap.mediator;
-                    try {
-                        yield mediator.update(info, cancellable);
-                    } catch (GLib.Error err) {
-                        report_problem(
-                            new Geary.ProblemReport(
-                                Geary.ProblemType.GENERIC_ERROR,
-                                err
-                            ));
-                    }
-                } else {
-                    // Could not find the GOA object for this account,
-                    // but have a working GOA connection, so it must
-                    // have been removed. Not much else that we can do
-                    // except remove it.
-                    throw new Error.GOA_REMOVED("GOA account not found");
-                }
+        if (is_goa) {
+            if (this.goa_service == null) {
+                throw new ConfigError.UNAVAILABLE("GOA service not available");
             }
 
-            if (info == null) {
-                // We have a GOA account, but either GOA is
-                // unavailable or the account has changed. Keep it
-                // around in case GOA comes back.
-                throw new Error.INVALID("GOA not available");
+            Goa.Object? goa_handle = this.goa_service.lookup_by_id(goa_id);
+            if (goa_handle != null) {
+                mediator = goa_mediator = new GoaMediator(goa_handle);
+                default_provider = goa_mediator.get_service_provider();
+            } else {
+                // The GOA account has gone away, so there's nothing
+                // we can do except to remove it locally as well
+                info(
+                    "%s: GOA account %s has been removed, removing local data",
+                    id, goa_id
+                );
+                status = Status.REMOVED;
+                // Use the default mediator since we can't create a
+                // GOA equiv, but set a dummy default provider so we
+                // don't get an error loading the config
+                default_provider = Geary.ServiceProvider.OTHER;
             }
-            break;
         }
 
-        info.set_account_directories(config_dir, data_dir);
-
-        info.ordinal = config.get_int(ORDINAL_KEY, info.ordinal);
-        if (info.ordinal >= Geary.AccountInformation.next_ordinal)
-            Geary.AccountInformation.next_ordinal = info.ordinal + 1;
-
-        info.append_sender(new Geary.RFC822.MailboxAddress(
-            config.get_string(REAL_NAME_KEY), primary_email
-        ));
+        AccountConfig? accounts = null;
+        ServiceConfig? services = null;
+        switch (version) {
+        case 0:
+            accounts = new AccountConfigLegacy();
+            services = new ServiceConfigLegacy();
+            break;
 
-        info.nickname = config.get_string(NICKNAME_KEY);
+        case 1:
+            accounts = new AccountConfigV1(is_goa);
+            services = new ServiceConfigV1();
+            break;
 
-        // Store alternate emails in a list of case-insensitive strings
-        Gee.List<string> alt_email_list = config.get_string_list(
-            ALTERNATE_EMAILS_KEY
-        );
-        if (alt_email_list.size != 0) {
-            foreach (string alt_email in alt_email_list) {
-                Geary.RFC822.MailboxAddresses mailboxes = new 
Geary.RFC822.MailboxAddresses.from_rfc822_string(alt_email);
-                foreach (Geary.RFC822.MailboxAddress mailbox in mailboxes.get_all())
-                info.append_sender(mailbox);
-            }
+        default:
+            throw new ConfigError.UNSUPPORTED_VERSION(
+                "Unsupported config version: %d", version
+            );
         }
 
-        info.prefetch_period_days = config.get_int(
-            PREFETCH_PERIOD_DAYS_KEY, info.prefetch_period_days
-        );
-        info.save_sent_mail = config.get_bool(
-            SAVE_SENT_MAIL_KEY, info.save_sent_mail
-        );
-        info.use_email_signature = config.get_bool(
-            USE_EMAIL_SIGNATURE_KEY, info.use_email_signature
-        );
-        info.email_signature = config.get_escaped_string(
-            EMAIL_SIGNATURE_KEY, info.email_signature
-        );
+        Geary.AccountInformation? account = null;
+        try {
+            account = accounts.load(
+                config,
+                id,
+                default_provider,
+                get_account_name()
+            );
+            account.set_account_directories(config_dir, data_dir);
+        } catch (GLib.KeyFileError err) {
+            throw new ConfigError.SYNTAX(err.message);
+        }
 
-        info.drafts_folder_path = Geary.AccountInformation.build_folder_path(
-            config.get_string_list(DRAFTS_FOLDER_KEY)
-        );
-        info.sent_mail_folder_path = Geary.AccountInformation.build_folder_path(
-            config.get_string_list(SENT_MAIL_FOLDER_KEY)
-        );
-        info.spam_folder_path = Geary.AccountInformation.build_folder_path(
-            config.get_string_list(SPAM_FOLDER_KEY)
-        );
-        info.trash_folder_path = Geary.AccountInformation.build_folder_path(
-            config.get_string_list(TRASH_FOLDER_KEY)
-        );
-        info.archive_folder_path = Geary.AccountInformation.build_folder_path(
-            config.get_string_list(ARCHIVE_FOLDER_KEY)
-        );
+        // If the account has been marked as removed, now that we have
+        // an account object and its dirs have been set up we can add
+        // it to the removed list and just bail out.
+        if (status == Status.REMOVED) {
+            this.removed.add(account);
+            throw new ConfigError.REMOVED("Account marked for removal");
+        }
 
-        info.save_drafts = config.get_bool(SAVE_DRAFTS_KEY, true);
+        if (!is_goa) {
+            try {
+                account.imap = services.load(
+                    config, account, Geary.Protocol.IMAP, mediator
+                );
+                account.smtp = services.load(
+                    config, account, Geary.Protocol.SMTP, mediator
+                );
+            } catch (GLib.KeyFileError err) {
+                throw new ConfigError.SYNTAX(err.message);
+            }
+        } else {
+            account.service_label = goa_mediator.get_service_label();
+            account.imap = new Geary.ServiceInformation(Geary.Protocol.IMAP, mediator);
+            account.smtp = new Geary.ServiceInformation(Geary.Protocol.SMTP, mediator);
 
-        // If the account has been removed, add it to the removed list
-        // and bail out
-        Geary.ConfigFile.Group manager_config =
-            config_file.get_group(ACCOUNT_MANAGER_GROUP);
-        if (manager_config.exists &&
-            manager_config.get_bool(REMOVED_KEY, false)) {
-            this.removed.add(info);
-            throw new Error.LOCAL_REMOVED("Account marked for removal");
+            try {
+                // This updates the service configs as well
+                yield goa_mediator.update(account, cancellable);
+            } catch (GLib.Error err) {
+                // If we get an error here, there might have been a
+                // problem loading the GOA data, or the OAuth token
+                // has expired. XXX Need to distinguish between the
+                // two.
+                set_available(account, false);
+                throw new ConfigError.UNAVAILABLE(err.message);
+            }
         }
 
-        return info;
+        return account;
     }
 
-    private async void save_account_locked(Geary.AccountInformation info,
+    private async void save_account_locked(Geary.AccountInformation account,
                                            GLib.Cancellable? cancellable)
         throws GLib.Error {
-        File? file = info.settings_file;
+        File? file = account.settings_file;
         if (file == null) {
-            throw new Error.INVALID(
-                "Account information does not have a settings file"
+            throw new GLib.IOError.NOT_SUPPORTED(
+                "Account %s does not have a settings file", account.id
             );
         }
 
-        Geary.ConfigFile config_file = new Geary.ConfigFile(file);
+        Geary.ConfigFile config = new Geary.ConfigFile(file);
 
         // Load the file first so we maintain old settings
         try {
-            yield config_file.load(cancellable);
+            yield config.load(cancellable);
         } catch (GLib.Error err) {
             // Oh well, just create a new one when saving
             debug("Could not load existing config file: %s", err.message);
         }
 
-        // If the account has been removed, set it as such. Otherwise
-        // ensure it is not set as such.
-        Geary.ConfigFile.Group manager_config =
-            config_file.get_group(ACCOUNT_MANAGER_GROUP);
-        if (this.removed.contains(info)) {
-            manager_config.set_bool(REMOVED_KEY, true);
-        } else if (manager_config.exists) {
-            manager_config.remove();
-        }
-
-        Geary.ConfigFile.Group config = config_file.get_group(ACCOUNT_CONFIG_GROUP);
-        if (info.imap is LocalServiceInformation) {
-            config.set_string(
-                CREDENTIALS_PROVIDER_KEY,
-                CredentialsProvider.LIBSECRET.to_string()
-            );
-            config.set_string(
-                CREDENTIALS_METHOD_KEY,
-                info.imap.credentials.supported_method.to_string()
-            );
-
-            if (info.service_provider == Geary.ServiceProvider.OTHER) {
-                Geary.ConfigFile.Group imap_config = config_file.get_group(
-                    IMAP_CONFIG_GROUP
-                );
-                ((LocalServiceInformation) info.imap).save_settings(imap_config);
+        Geary.ConfigFile.Group metadata_config =
+            config.get_group(GROUP_METADATA);
+        metadata_config.set_int(
+            METADATA_VERSION, CONFIG_VERSION
+        );
+        metadata_config.set_string(
+            METADATA_STATUS, get_status(account).to_value()
+        );
 
-                Geary.ConfigFile.Group smtp_config = config_file.get_group(
-                    SMTP_CONFIG_GROUP
-                );
-                ((LocalServiceInformation) info.smtp).save_settings(smtp_config);
-            }
-        } else if (info.imap is GoaServiceInformation) {
-            config.set_string(
-                CREDENTIALS_PROVIDER_KEY, CredentialsProvider.GOA.to_string()
-            );
+        bool is_goa = is_goa_account(account);
+        if (is_goa) {
+            metadata_config.set_string(METADATA_GOA, to_goa_id(account.id));
         }
 
-        config.set_string(REAL_NAME_KEY, info.primary_mailbox.name);
-        config.set_string(PRIMARY_EMAIL_KEY, info.primary_mailbox.address);
-        config.set_string(NICKNAME_KEY, info.nickname);
-        config.set_string(SERVICE_PROVIDER_KEY, info.service_provider.to_value());
-        config.set_int(ORDINAL_KEY, info.ordinal);
-        config.set_int(PREFETCH_PERIOD_DAYS_KEY, info.prefetch_period_days);
-        config.set_bool(SAVE_SENT_MAIL_KEY, info.save_sent_mail);
-        config.set_bool(USE_EMAIL_SIGNATURE_KEY, info.use_email_signature);
-        config.set_escaped_string(EMAIL_SIGNATURE_KEY, info.email_signature);
-        if (info.has_sender_aliases) {
-            Gee.List<Geary.RFC822.MailboxAddress> alts = info.sender_mailboxes;
-            alts.remove_at(0);
-
-            Gee.List<string> values = new Gee.LinkedList<string>();
-            foreach (Geary.RFC822.MailboxAddress alt in alts) {
-                values.add(alt.to_rfc822_string());
-            }
+        AccountConfig accounts = new AccountConfigV1(is_goa);
+        accounts.save(account, config);
 
-            config.set_string_list(ALTERNATE_EMAILS_KEY, values);
+        if (!is_goa) {
+            ServiceConfig services = new ServiceConfigV1();
+            services.save(account, account.imap, config);
+            services.save(account, account.smtp, config);
         }
 
-        Gee.LinkedList<string> empty = new Gee.LinkedList<string>();
-        config.set_string_list(DRAFTS_FOLDER_KEY, (info.drafts_folder_path != null
-            ? info.drafts_folder_path.as_list() : empty));
-        config.set_string_list(SENT_MAIL_FOLDER_KEY, (info.sent_mail_folder_path != null
-            ? info.sent_mail_folder_path.as_list() : empty));
-        config.set_string_list(SPAM_FOLDER_KEY, (info.spam_folder_path != null
-            ? info.spam_folder_path.as_list() : empty));
-        config.set_string_list(TRASH_FOLDER_KEY, (info.trash_folder_path != null
-            ? info.trash_folder_path.as_list() : empty));
-        config.set_string_list(ARCHIVE_FOLDER_KEY, (info.archive_folder_path != null
-            ? info.archive_folder_path.as_list() : empty));
-
-        config.set_bool(SAVE_DRAFTS_KEY, info.save_drafts);
-
         debug("Writing config to: %s", file.get_path());
-        yield config_file.save(cancellable);
+        yield config.save(cancellable);
     }
 
     private async void delete_account(Geary.AccountInformation info,
@@ -811,99 +801,26 @@ public class Accounts.Manager : GLib.Object {
             : id;
     }
 
-    private Geary.AccountInformation
-        new_libsecret_account(string id,
-                              Geary.ConfigFile.Group config,
-                              string fallback_login)
-        throws GLib.Error {
-
-        Geary.ServiceProvider provider = Geary.ServiceProvider.for_value(
-            config.get_string(SERVICE_PROVIDER_KEY,
-                              Geary.ServiceProvider.GMAIL.to_string())
-        );
-        Geary.Credentials.Method method = Geary.Credentials.Method.from_string(
-            config.get_string(CREDENTIALS_METHOD_KEY,
-                              Geary.Credentials.Method.PASSWORD.to_string())
-        );
-
-        Geary.ConfigFile.Group imap_config =
-        config.file.get_group(IMAP_CONFIG_GROUP);
-        LocalServiceInformation imap = new_libsecret_service(
-            Geary.Protocol.IMAP
-        );
-        imap_config.set_fallback(config.name, "imap_");
-        imap.load_credentials(imap_config, method, fallback_login);
-
-        Geary.ConfigFile.Group smtp_config =
-        config.file.get_group(SMTP_CONFIG_GROUP);
-        LocalServiceInformation smtp = new_libsecret_service(
-            Geary.Protocol.SMTP
-        );
-        smtp_config.set_fallback(config.name, "smtp_");
-        smtp.load_credentials(smtp_config, method, fallback_login);
-
-        // Generic IMAP accounts must load their settings from their
-        // config, GMail and others have it hard-coded hence don't
-        // need to load it.
-        if (provider == Geary.ServiceProvider.OTHER) {
-            imap.load_settings(imap_config);
-            smtp.load_settings(smtp_config);
-        } else {
-            provider.setup_service(imap);
-            provider.setup_service(smtp);
-        }
-
-        return new Geary.AccountInformation(
-            id, provider, imap, smtp
-        );
-    }
-
-    private Geary.AccountInformation new_goa_account(string id,
-                                                     Goa.Object account) {
-        GoaMediator mediator = new GoaMediator(account);
-
-        Geary.ServiceProvider provider = Geary.ServiceProvider.OTHER;
-        switch (account.get_account().provider_type) {
-        case "google":
-            provider = Geary.ServiceProvider.GMAIL;
-            break;
-
-        case "windows_live":
-            provider = Geary.ServiceProvider.OUTLOOK;
-            break;
-        }
-
-        Geary.AccountInformation info = new Geary.AccountInformation(
-            id,
-            provider,
-            new GoaServiceInformation(Geary.Protocol.IMAP, mediator, account),
-            new GoaServiceInformation(Geary.Protocol.SMTP, mediator, account)
-        );
-        info.service_label = account.get_account().provider_name;
-
-        return info;
-    }
-
     private async void create_goa_account(Goa.Object account,
                                           GLib.Cancellable? cancellable) {
-        Geary.AccountInformation info = new_goa_account(
-            to_geary_id(account), account
-        );
-
-        GoaMediator mediator = (GoaMediator) info.imap.mediator;
         // Goa.Account.mail_disabled doesn't seem to reflect if we get
         // get a valid mail object here, so just rely on that instead.
         Goa.Mail? mail = account.get_mail();
         if (mail != null) {
-            info.ordinal = Geary.AccountInformation.next_ordinal++;
-
             string? name = mail.name;
             if (Geary.String.is_empty_or_whitespace(name)) {
                 name = get_account_name();
             }
-            info.append_sender(new Geary.RFC822.MailboxAddress(
-                name, mail.email_address
-            ));
+
+            GoaMediator mediator = new GoaMediator(account);
+            Geary.AccountInformation info = new Geary.AccountInformation(
+                to_geary_id(account),
+                mediator.get_service_provider(),
+                new Geary.RFC822.MailboxAddress(name, mail.email_address)
+            );
+
+            info.ordinal = Geary.AccountInformation.next_ordinal++;
+            info.service_label = mediator.get_service_label();
             info.nickname = account.get_account().presentation_identity;
 
             try {
@@ -1037,3 +954,577 @@ public class Accounts.Manager : GLib.Object {
     }
 
 }
+
+/**
+ * Manages persistence for version 1 config files.
+ */
+public class Accounts.AccountConfigV1 : AccountConfig, GLib.Object {
+
+
+    private const string GROUP_ACCOUNT = "Account";
+    private const string GROUP_FOLDERS = "Folders";
+
+    private const string ACCOUNT_LABEL = "label";
+    private const string ACCOUNT_ORDINAL = "ordinal";
+    private const string ACCOUNT_PREFETCH = "prefetch_days";
+    private const string ACCOUNT_PROVIDER = "service_provider";
+    private const string ACCOUNT_SAVE_DRAFTS = "save_drafts";
+    private const string ACCOUNT_SAVE_SENT = "save_sent";
+    private const string ACCOUNT_SENDERS = "sender_mailboxes";
+    private const string ACCOUNT_SIG = "signature";
+    private const string ACCOUNT_USE_SIG = "use_signature";
+
+    private const string FOLDER_ARCHIVE = "archive_folder";
+    private const string FOLDER_DRAFTS = "drafts_folder";
+    private const string FOLDER_SENT = "sent_mail_folder";
+    private const string FOLDER_SPAM = "spam_folder";
+    private const string FOLDER_TRASH = "trash_folder";
+
+
+    private bool is_managed;
+
+    public AccountConfigV1(bool is_managed) {
+        this.is_managed = is_managed;
+    }
+
+    public  Geary.AccountInformation load(Geary.ConfigFile config,
+                                          string id,
+                                          Geary.ServiceProvider? default_provider,
+                                          string? default_name)
+        throws ConfigError, GLib.KeyFileError {
+        Geary.ConfigFile.Group account_config =
+            config.get_group(GROUP_ACCOUNT);
+
+        Gee.List<Geary.RFC822.MailboxAddress> senders =
+            new Gee.LinkedList<Geary.RFC822.MailboxAddress>();
+        foreach (string sender in
+                 account_config.get_required_string_list(ACCOUNT_SENDERS)) {
+            try {
+                senders.add(
+                    new Geary.RFC822.MailboxAddress.from_rfc822_string(sender)
+                );
+            } catch (Geary.RFC822Error err) {
+                throw new ConfigError.SYNTAX(
+                    "%s: Invalid sender address: %s", id, sender
+                );
+            }
+        }
+
+        if (senders.is_empty) {
+            throw new ConfigError.SYNTAX("%s: No sender addresses found", id);
+        }
+
+        Geary.ServiceProvider? provider = null;
+        try {
+            provider = default_provider ??
+                Geary.ServiceProvider.for_value(
+                    account_config.get_required_string(ACCOUNT_PROVIDER)
+                );
+        } catch (Geary.EngineError err) {
+            throw new ConfigError.SYNTAX("%s: No/bad service provider", id);
+        }
+
+        Geary.AccountInformation account = new Geary.AccountInformation(
+            id, provider, senders.remove_at(0)
+        );
+
+        account.ordinal = account_config.get_int(
+            ACCOUNT_ORDINAL, Geary.AccountInformation.next_ordinal++
+        );
+        account.nickname = account_config.get_string(
+            ACCOUNT_LABEL, account.nickname
+        );
+        account.prefetch_period_days = account_config.get_int(
+            ACCOUNT_PREFETCH, account.prefetch_period_days
+        );
+        account.save_drafts = account_config.get_bool(
+            ACCOUNT_SAVE_DRAFTS, account.save_drafts
+        );
+        account.save_sent_mail = account_config.get_bool(
+            ACCOUNT_SAVE_SENT, account.save_sent_mail
+        );
+        account.use_email_signature = account_config.get_bool(
+            ACCOUNT_USE_SIG, account.use_email_signature
+        );
+        account.email_signature = account_config.get_string(
+            ACCOUNT_SIG, account.email_signature
+        );
+        foreach (Geary.RFC822.MailboxAddress sender in senders) {
+            account.append_sender(sender);
+        }
+
+        Geary.ConfigFile.Group folder_config =
+            config.get_group(GROUP_FOLDERS);
+        account.archive_folder_path = load_folder(folder_config, FOLDER_ARCHIVE);
+        account.drafts_folder_path = load_folder(folder_config, FOLDER_DRAFTS);
+        account.sent_mail_folder_path = load_folder(folder_config, FOLDER_SENT);
+        account.spam_folder_path = load_folder(folder_config, FOLDER_SPAM);
+        account.trash_folder_path = load_folder(folder_config, FOLDER_TRASH);
+
+        return account;
+    }
+
+    /** Saves an account to a config file. */
+    public void save(Geary.AccountInformation account,
+                     Geary.ConfigFile config) {
+        Geary.ConfigFile.Group account_config =
+            config.get_group(GROUP_ACCOUNT);
+        account_config.set_int(ACCOUNT_ORDINAL, account.ordinal);
+        account_config.set_string(ACCOUNT_LABEL, account.nickname);
+        account_config.set_int(ACCOUNT_PREFETCH, account.prefetch_period_days);
+        account_config.set_bool(ACCOUNT_SAVE_DRAFTS, account.save_drafts);
+        account_config.set_bool(ACCOUNT_SAVE_SENT, account.save_sent_mail);
+        account_config.set_bool(ACCOUNT_USE_SIG, account.use_email_signature);
+        account_config.set_string(ACCOUNT_SIG, account.email_signature);
+        account_config.set_string_list(
+            ACCOUNT_SENDERS,
+            Geary.traverse(account.sender_mailboxes)
+            .map<string>((sender) => sender.to_rfc822_string())
+            .to_array_list()
+        );
+
+        if (!is_managed) {
+            account_config.set_string(
+                ACCOUNT_PROVIDER, account.service_provider.to_value()
+            );
+        }
+
+        Geary.ConfigFile.Group folder_config =
+            config.get_group(GROUP_FOLDERS);
+        save_folder(folder_config, FOLDER_ARCHIVE, account.archive_folder_path);
+        save_folder(folder_config, FOLDER_DRAFTS, account.drafts_folder_path);
+        save_folder(folder_config, FOLDER_SENT, account.sent_mail_folder_path);
+        save_folder(folder_config, FOLDER_SPAM, account.spam_folder_path);
+        save_folder(folder_config, FOLDER_TRASH, account.trash_folder_path);
+    }
+
+    private void save_folder(Geary.ConfigFile.Group config,
+                             string key,
+                             Geary.FolderPath? path) {
+        if (path != null) {
+            config.set_string_list(key, path.as_list());
+        }
+    }
+
+    private Geary.FolderPath? load_folder(Geary.ConfigFile.Group config,
+                                          string key) {
+        Geary.FolderPath? path = null;
+        Gee.List<string> parts = config.get_string_list(key);
+        if (!parts.is_empty) {
+            path = Geary.AccountInformation.build_folder_path(parts);
+        }
+        return path;
+    }
+
+}
+
+
+/**
+ * Manages persistence for un-versioned account configuration.
+ */
+public class Accounts.AccountConfigLegacy : AccountConfig, GLib.Object {
+
+    internal const string GROUP = "AccountInformation";
+
+    private const string ALTERNATE_EMAILS_KEY = "alternate_emails";
+    private const string ARCHIVE_FOLDER_KEY = "archive_folder";
+    private const string CREDENTIALS_METHOD_KEY = "credentials_method";
+    private const string CREDENTIALS_PROVIDER_KEY = "credentials_provider";
+    private const string DRAFTS_FOLDER_KEY = "drafts_folder";
+    private const string EMAIL_SIGNATURE_KEY = "email_signature";
+    private const string NICKNAME_KEY = "nickname";
+    private const string ORDINAL_KEY = "ordinal";
+    private const string PREFETCH_PERIOD_DAYS_KEY = "prefetch_period_days";
+    private const string PRIMARY_EMAIL_KEY = "primary_email";
+    private const string REAL_NAME_KEY = "real_name";
+    private const string SAVE_DRAFTS_KEY = "save_drafts";
+    private const string SAVE_SENT_MAIL_KEY = "save_sent_mail";
+    private const string SENT_MAIL_FOLDER_KEY = "sent_mail_folder";
+    private const string SERVICE_PROVIDER_KEY = "service_provider";
+    private const string SPAM_FOLDER_KEY = "spam_folder";
+    private const string TRASH_FOLDER_KEY = "trash_folder";
+    private const string USE_EMAIL_SIGNATURE_KEY = "use_email_signature";
+
+
+    public Geary.AccountInformation
+        load(Geary.ConfigFile config_file,
+             string id,
+             Geary.ServiceProvider? default_provider,
+             string? default_name)
+        throws ConfigError, GLib.KeyFileError {
+        Geary.ConfigFile.Group config = config_file.get_group(GROUP);
+
+        string primary_email = config.get_required_string(PRIMARY_EMAIL_KEY);
+        string real_name = config.get_string(REAL_NAME_KEY, default_name);
+
+        Geary.ServiceProvider? provider = null;
+        try {
+            provider = default_provider ??
+                Geary.ServiceProvider.for_value(
+                    config.get_required_string(SERVICE_PROVIDER_KEY)
+                );
+        } catch (Geary.EngineError err) {
+            throw new ConfigError.SYNTAX("%s: No/bad service provider", id);
+        }
+
+        Geary.AccountInformation info = new Geary.AccountInformation(
+            id, provider,
+            new Geary.RFC822.MailboxAddress(real_name, primary_email)
+        );
+
+        info.ordinal = config.get_int(ORDINAL_KEY, info.ordinal);
+        if (info.ordinal >= Geary.AccountInformation.next_ordinal) {
+            Geary.AccountInformation.next_ordinal = info.ordinal + 1;
+        }
+
+        info.append_sender(new Geary.RFC822.MailboxAddress(
+            config.get_string(REAL_NAME_KEY), primary_email
+        ));
+
+        info.nickname = config.get_string(NICKNAME_KEY);
+
+        // Store alternate emails in a list of case-insensitive strings
+        Gee.List<string> alt_email_list = config.get_string_list(
+            ALTERNATE_EMAILS_KEY
+        );
+        foreach (string alt_email in alt_email_list) {
+            Geary.RFC822.MailboxAddresses mailboxes =
+                new Geary.RFC822.MailboxAddresses.from_rfc822_string(alt_email);
+            foreach (Geary.RFC822.MailboxAddress mailbox in mailboxes.get_all()) {
+                info.append_sender(mailbox);
+            }
+        }
+
+        info.prefetch_period_days = config.get_int(
+            PREFETCH_PERIOD_DAYS_KEY, info.prefetch_period_days
+        );
+        info.save_sent_mail = config.get_bool(
+            SAVE_SENT_MAIL_KEY, info.save_sent_mail
+        );
+        info.use_email_signature = config.get_bool(
+            USE_EMAIL_SIGNATURE_KEY, info.use_email_signature
+        );
+        info.email_signature = config.get_string(
+            EMAIL_SIGNATURE_KEY, info.email_signature
+        );
+
+        info.drafts_folder_path = Geary.AccountInformation.build_folder_path(
+            config.get_string_list(DRAFTS_FOLDER_KEY)
+        );
+        info.sent_mail_folder_path = Geary.AccountInformation.build_folder_path(
+            config.get_string_list(SENT_MAIL_FOLDER_KEY)
+        );
+        info.spam_folder_path = Geary.AccountInformation.build_folder_path(
+            config.get_string_list(SPAM_FOLDER_KEY)
+        );
+        info.trash_folder_path = Geary.AccountInformation.build_folder_path(
+            config.get_string_list(TRASH_FOLDER_KEY)
+        );
+        info.archive_folder_path = Geary.AccountInformation.build_folder_path(
+            config.get_string_list(ARCHIVE_FOLDER_KEY)
+        );
+
+        info.save_drafts = config.get_bool(SAVE_DRAFTS_KEY, true);
+
+        return info;
+    }
+
+    public void save(Geary.AccountInformation info,
+                     Geary.ConfigFile config_file) {
+
+        Geary.ConfigFile.Group config = config_file.get_group(GROUP);
+
+        config.set_string(REAL_NAME_KEY, info.primary_mailbox.name ?? "");
+        config.set_string(PRIMARY_EMAIL_KEY, info.primary_mailbox.address);
+        config.set_string(NICKNAME_KEY, info.nickname);
+        config.set_string(SERVICE_PROVIDER_KEY, info.service_provider.to_value());
+        config.set_int(ORDINAL_KEY, info.ordinal);
+        config.set_int(PREFETCH_PERIOD_DAYS_KEY, info.prefetch_period_days);
+        config.set_bool(SAVE_SENT_MAIL_KEY, info.save_sent_mail);
+        config.set_bool(USE_EMAIL_SIGNATURE_KEY, info.use_email_signature);
+        config.set_string(EMAIL_SIGNATURE_KEY, info.email_signature);
+        if (info.has_sender_aliases) {
+            Gee.List<Geary.RFC822.MailboxAddress> alts = info.sender_mailboxes;
+            // Don't include the primary in the list
+            alts.remove_at(0);
+
+            config.set_string_list(
+                ALTERNATE_EMAILS_KEY,
+                Geary.traverse(alts)
+                .map<string>((alt) => alt.to_rfc822_string())
+                .to_array_list()
+            );
+        }
+
+        Gee.LinkedList<string> empty = new Gee.LinkedList<string>();
+        config.set_string_list(DRAFTS_FOLDER_KEY, (info.drafts_folder_path != null
+            ? info.drafts_folder_path.as_list() : empty));
+        config.set_string_list(SENT_MAIL_FOLDER_KEY, (info.sent_mail_folder_path != null
+            ? info.sent_mail_folder_path.as_list() : empty));
+        config.set_string_list(SPAM_FOLDER_KEY, (info.spam_folder_path != null
+            ? info.spam_folder_path.as_list() : empty));
+        config.set_string_list(TRASH_FOLDER_KEY, (info.trash_folder_path != null
+            ? info.trash_folder_path.as_list() : empty));
+        config.set_string_list(ARCHIVE_FOLDER_KEY, (info.archive_folder_path != null
+            ? info.archive_folder_path.as_list() : empty));
+
+        config.set_bool(SAVE_DRAFTS_KEY, info.save_drafts);
+    }
+
+}
+
+
+/**
+ * Manages persistence for version 1 service configuration.
+ */
+public class Accounts.ServiceConfigV1 : ServiceConfig, GLib.Object {
+
+    private const string GROUP_INCOMING = "Incoming";
+    private const string GROUP_OUTGOING = "Outgoing";
+
+    private const string CREDENTIALS = "credentials";
+    private const string HOST = "host";
+    private const string LOGIN = "login";
+    private const string PORT = "port";
+    private const string REMEMBER_PASSWORD = "remember_password";
+    private const string SECURITY = "transport_security";
+
+
+    /** Loads a supported service from a config file. */
+    public Geary.ServiceInformation load(Geary.ConfigFile config,
+                                         Geary.AccountInformation account,
+                                         Geary.Protocol protocol,
+                                         Geary.CredentialsMediator mediator)
+        throws ConfigError, GLib.KeyFileError {
+        Geary.ConfigFile.Group service_config = config.get_group(
+            protocol == IMAP ? GROUP_INCOMING : GROUP_OUTGOING
+        );
+
+        Geary.ServiceInformation service = new Geary.ServiceInformation(
+            protocol, mediator
+        );
+
+        string? login = service_config.get_string(LOGIN, null);
+        if (login != null) {
+            service.credentials = new Geary.Credentials(
+                Credentials.PASSWORD, login
+            );
+        }
+        service.remember_password = service_config.get_bool(
+            REMEMBER_PASSWORD, service.remember_password
+        );
+
+
+        if (account.service_provider == Geary.ServiceProvider.OTHER) {
+            service.host = service_config.get_required_string(HOST);
+            service.port = (uint16) service_config.get_int(PORT, service.port);
+
+            try {
+                service.transport_security =
+                Geary.TlsNegotiationMethod.for_value(
+                    service_config.get_string(SECURITY, null)
+                );
+            } catch (GLib.Error err) {
+                // Oh well
+                debug("%s: No/invalid transport security config for %s",
+                      account.id, protocol.to_value());
+            }
+
+            if (service.protocol == Geary.Protocol.SMTP) {
+                try {
+                    service.smtp_credentials_source =
+                    Geary.SmtpCredentials.for_value(
+                        service_config.get_required_string(CREDENTIALS)
+                    );
+                } catch (Geary.EngineError err) {
+                    debug("%s: No/invalid SMTP auth config", account.id);
+                }
+            }
+
+            if (service.port == 0) {
+                service.port = service.get_default_port();
+            }
+        }
+
+        return service;
+    }
+
+    /** Saves an service to a config file. */
+    public void save(Geary.AccountInformation account,
+                     Geary.ServiceInformation service,
+                     Geary.ConfigFile config) {
+        Geary.ConfigFile.Group service_config = config.get_group(
+            service.protocol == IMAP ? GROUP_INCOMING : GROUP_OUTGOING
+        );
+
+        if (service.credentials != null) {
+            service_config.set_string(LOGIN, service.credentials.user);
+        }
+        service_config.set_bool(REMEMBER_PASSWORD, service.remember_password);
+
+        if (account.service_provider == Geary.ServiceProvider.OTHER) {
+            service_config.set_string(HOST, service.host);
+            service_config.set_int(PORT, service.port);
+            service_config.set_string(
+                SECURITY, service.transport_security.to_value()
+            );
+
+            if (service.protocol == Geary.Protocol.SMTP) {
+                service_config.set_string(
+                    CREDENTIALS, service.smtp_credentials_source.to_value()
+                );
+            }
+        }
+    }
+
+}
+
+
+/**
+ * Manages persistence for un-versioned service configuration.
+ */
+public class Accounts.ServiceConfigLegacy : ServiceConfig, GLib.Object {
+
+
+    private const string HOST = "host";
+    private const string PORT = "port";
+    private const string REMEMBER_PASSWORD = "remember_password";
+    private const string SSL = "ssl";
+    private const string STARTTLS = "starttls";
+    private const string USERNAME = "username";
+
+    private const string SMTP_NOAUTH = "smtp_noauth";
+    private const string SMTP_USE_IMAP_CREDENTIALS = "smtp_use_imap_credentials";
+
+
+    /** Loads a supported service from a config file. */
+    public Geary.ServiceInformation load(Geary.ConfigFile config,
+                                         Geary.AccountInformation account,
+                                         Geary.Protocol protocol,
+                                         Geary.CredentialsMediator mediator)
+        throws ConfigError, GLib.KeyFileError {
+        Geary.ServiceInformation service = new Geary.ServiceInformation(
+            protocol, mediator
+        );
+
+        Geary.ConfigFile.Group service_config =
+            config.get_group(AccountConfigLegacy.GROUP);
+
+        string prefix = service.protocol.to_value() + "_";
+
+        string? login = service_config.get_string(
+            prefix + USERNAME, account.primary_mailbox.address
+        );
+        if (login != null) {
+            service.credentials = new Geary.Credentials(
+                Credentials.PASSWORD, login
+            );
+        }
+        service.remember_password = service_config.get_bool(
+            prefix + REMEMBER_PASSWORD, service.remember_password
+        );
+
+        if (account.service_provider == Geary.ServiceProvider.OTHER) {
+            service.host = service_config.get_string(prefix + HOST, service.host);
+            service.port = (uint16) service_config.get_int(
+                prefix + PORT, service.port
+            );
+
+            bool use_tls = service_config.get_bool(
+                prefix + SSL, protocol == Geary.Protocol.IMAP
+            );
+            bool use_starttls = service_config.get_bool(
+                prefix + STARTTLS, true
+            );
+            if (use_tls) {
+                service.transport_security = Geary.TlsNegotiationMethod.TRANSPORT;
+            } else if (use_starttls) {
+                service.transport_security = Geary.TlsNegotiationMethod.START_TLS;
+            } else {
+                service.transport_security = Geary.TlsNegotiationMethod.NONE;
+            }
+
+            if (service.protocol == Geary.Protocol.SMTP) {
+                bool use_imap = service_config.get_bool(
+                    SMTP_USE_IMAP_CREDENTIALS, service.credentials != null
+                );
+                bool no_auth = service_config.get_bool(
+                    SMTP_NOAUTH, false
+                );
+                if (use_imap) {
+                    service.smtp_credentials_source =
+                        Geary.SmtpCredentials.IMAP;
+                } else if (!no_auth) {
+                    service.smtp_credentials_source =
+                        Geary.SmtpCredentials.CUSTOM;
+                } else {
+                    service.smtp_credentials_source =
+                        Geary.SmtpCredentials.NONE;
+                }
+            }
+        }
+
+        return service;
+    }
+
+    /** Saves an service to a config file. */
+    public void save(Geary.AccountInformation account,
+                     Geary.ServiceInformation service,
+                     Geary.ConfigFile config) {
+        Geary.ConfigFile.Group service_config =
+            config.get_group(AccountConfigLegacy.GROUP);
+
+        string prefix = service.protocol.to_value() + "_";
+
+        if (service.credentials != null) {
+            service_config.set_string(
+                prefix + USERNAME, service.credentials.user
+            );
+        }
+        service_config.set_bool(
+            prefix + REMEMBER_PASSWORD, service.remember_password
+        );
+
+        if (account.service_provider == Geary.ServiceProvider.OTHER) {
+            service_config.set_string(prefix + HOST, service.host);
+            service_config.set_int(prefix + PORT, service.port);
+
+            switch (service.transport_security) {
+            case NONE:
+                service_config.set_bool(prefix + SSL, false);
+                service_config.set_bool(prefix + STARTTLS, false);
+                break;
+
+            case START_TLS:
+                service_config.set_bool(prefix + SSL, false);
+                service_config.set_bool(prefix + STARTTLS, true);
+                break;
+
+            case TRANSPORT:
+                service_config.set_bool(prefix + SSL, true);
+                service_config.set_bool(prefix + STARTTLS, false);
+                break;
+            }
+
+            if (service.protocol == Geary.Protocol.SMTP) {
+                switch (service.smtp_credentials_source) {
+                case NONE:
+                    service_config.set_bool(SMTP_USE_IMAP_CREDENTIALS, false);
+                    service_config.set_bool(SMTP_NOAUTH, true);
+                    break;
+
+                case IMAP:
+                    service_config.set_bool(SMTP_USE_IMAP_CREDENTIALS, true);
+                    service_config.set_bool(SMTP_NOAUTH, false);
+                    break;
+
+                case CUSTOM:
+                    service_config.set_bool(SMTP_USE_IMAP_CREDENTIALS, false);
+                    service_config.set_bool(SMTP_NOAUTH, false);
+                    break;
+                }
+            }
+        }
+    }
+
+}
diff --git a/src/client/accounts/add-edit-page.vala b/src/client/accounts/add-edit-page.vala
index a022c9fe..790871ab 100644
--- a/src/client/accounts/add-edit-page.vala
+++ b/src/client/accounts/add-edit-page.vala
@@ -690,20 +690,23 @@ public class AddEditPage : Gtk.Box {
             }
         }
 
+        Geary.ServiceInformation? imap = null;
+        Geary.ServiceInformation? smtp = null;
         if (info == null) {
             // New account
-            Geary.ServiceInformation imap =
-                this.application.controller.account_manager.new_libsecret_service(
-                    Geary.Protocol.IMAP
-                );
-            Geary.ServiceInformation smtp =
-                this.application.controller.account_manager.new_libsecret_service(
-                    Geary.Protocol.SMTP
-                );
+            imap = this.application.controller.account_manager.new_libsecret_service(
+                Geary.Protocol.IMAP
+            );
+            smtp = this.application.controller.account_manager.new_libsecret_service(
+                Geary.Protocol.SMTP
+            );
 
             try {
                 info = this.application.controller.account_manager.new_orphan_account(
-                    this.get_service_provider(), imap, smtp
+                    this.get_service_provider(),
+                    new Geary.RFC822.MailboxAddress(
+                        this.real_name.strip(), this.email_address.strip()
+                    )
                 );
             } catch (Error err) {
                 debug("Unable to create account %s for %s: %s",
@@ -712,13 +715,10 @@ public class AddEditPage : Gtk.Box {
         } else {
             // Existing account: create a copy so we don't mess up the
             // original.
-            info = new Geary.AccountInformation.temp_copy(info);
+            //info = new Geary.AccountInformation.temp_copy(info);
         }
 
         if (info != null) {
-            //info.primary_mailbox = new Geary.RFC822.MailboxAddress(
-            //    this.real_name.strip(), this.email_address.strip()
-            //);
             info.nickname = this.nickname.strip();
             info.imap.credentials = imap_credentials;
             info.smtp.credentials = smtp_credentials;
@@ -740,6 +740,9 @@ public class AddEditPage : Gtk.Box {
             info.use_email_signature = this.use_email_signature;
             info.email_signature = this.email_signature;
 
+            info.imap = imap;
+            info.smtp = smtp;
+
             on_changed();
         }
 
diff --git a/src/client/application/goa-mediator.vala b/src/client/application/goa-mediator.vala
index b8c55357..79be20b1 100644
--- a/src/client/application/goa-mediator.vala
+++ b/src/client/application/goa-mediator.vala
@@ -43,6 +43,24 @@ public class GoaMediator : Geary.CredentialsMediator, Object {
         this.account = account;
     }
 
+    public Geary.ServiceProvider get_service_provider() {
+        Geary.ServiceProvider provider = Geary.ServiceProvider.OTHER;
+        switch (this.account.get_account().provider_type) {
+        case "google":
+            provider = Geary.ServiceProvider.GMAIL;
+            break;
+
+        case "windows_live":
+            provider = Geary.ServiceProvider.OUTLOOK;
+            break;
+        }
+        return provider;
+    }
+
+    public string get_service_label() {
+        return this.account.get_account().provider_name;
+    }
+
     public async void update(Geary.AccountInformation geary_account,
                              GLib.Cancellable? cancellable)
         throws GLib.Error {
@@ -54,11 +72,8 @@ public class GoaMediator : Geary.CredentialsMediator, Object {
         this.oauth2 = this.account.get_oauth2_based();
         this.password = this.account.get_password_based();
 
-        GoaServiceInformation imap = (GoaServiceInformation) geary_account.imap;
-        imap.update();
-
-        GoaServiceInformation smtp = (GoaServiceInformation) geary_account.smtp;
-        smtp.update();
+        update_imap_config(geary_account.imap);
+        update_smtp_config(geary_account.smtp);
     }
 
     public virtual async bool load_token(Geary.AccountInformation account,
@@ -118,4 +133,84 @@ public class GoaMediator : Geary.CredentialsMediator, Object {
         return this.is_valid;
     }
 
+    private void update_imap_config(Geary.ServiceInformation service) {
+        Goa.Mail? mail = this.account.get_mail();
+        if (mail != null) {
+            parse_host_name(service, mail.imap_host);
+
+            if (mail.imap_use_ssl) {
+                service.transport_security = Geary.TlsNegotiationMethod.TRANSPORT;
+            } else if (mail.imap_use_tls) {
+                service.transport_security = Geary.TlsNegotiationMethod.START_TLS;
+            } else {
+                service.transport_security = Geary.TlsNegotiationMethod.NONE;
+            }
+
+            service.credentials = new Geary.Credentials(
+                this.method, mail.imap_user_name
+            );
+
+            if (service.port == 0) {
+                service.port = service.get_default_port();
+            }
+        }
+    }
+
+    private void update_smtp_config(Geary.ServiceInformation service) {
+        Goa.Mail? mail = this.account.get_mail();
+        if (mail != null) {
+            parse_host_name(service, mail.smtp_host);
+
+            if (mail.imap_use_ssl) {
+                service.transport_security = Geary.TlsNegotiationMethod.TRANSPORT;
+            } else if (mail.imap_use_tls) {
+                service.transport_security = Geary.TlsNegotiationMethod.START_TLS;
+            } else {
+                service.transport_security = Geary.TlsNegotiationMethod.NONE;
+            }
+
+            if (mail.smtp_use_auth) {
+                service.smtp_credentials_source = Geary.SmtpCredentials.CUSTOM;
+            } else {
+                service.smtp_credentials_source = Geary.SmtpCredentials.NONE;
+            }
+
+            if (mail.smtp_use_auth) {
+                service.credentials = new Geary.Credentials(
+                    this.method, mail.smtp_user_name
+                );
+            }
+
+            if (service.port == 0) {
+                service.port = service.get_default_port();
+            }
+        }
+    }
+
+    private void parse_host_name(Geary.ServiceInformation service,
+                                 string host_name) {
+        // Fall back to trying to use the host name as-is.
+        // At least the user can see it in the settings if
+        // they look.
+        service.host = host_name;
+        service.port = 0;
+
+        try {
+            GLib.NetworkAddress address = GLib.NetworkAddress.parse(
+                host_name, service.port
+            );
+
+            service.host = address.hostname;
+            service.port = (uint16) address.port;
+        } catch (GLib.Error err) {
+            warning(
+                "GOA account \"%s\" %s hostname \"%s\": %",
+                this.account.get_account().id,
+                service.protocol.to_value(),
+                host_name,
+                err.message
+            );
+        }
+    }
+
 }
diff --git a/src/client/meson.build b/src/client/meson.build
index 5fae0786..1e23be0c 100644
--- a/src/client/meson.build
+++ b/src/client/meson.build
@@ -28,8 +28,6 @@ geary_client_vala_sources = files(
   'accounts/accounts-editor-servers-pane.vala',
   'accounts/accounts-manager.vala',
   'accounts/add-edit-page.vala',
-  'accounts/goa-service-information.vala',
-  'accounts/local-service-information.vala',
   'accounts/login-dialog.vala',
 
   'components/client-web-view.vala',
diff --git a/src/engine/api/geary-account-information.vala b/src/engine/api/geary-account-information.vala
index 793ad2dd..a0c924e4 100644
--- a/src/engine/api/geary-account-information.vala
+++ b/src/engine/api/geary-account-information.vala
@@ -55,10 +55,6 @@ public class Geary.AccountInformation : BaseObject {
      */
     public File? data_dir { get; private set; default = null; }
 
-    //
-    // IMPORTANT: When adding new properties, be sure to add them to the copy method.
-    //
-
     /**
      * A unique, immutable, machine-readable identifier for this account.
      *
@@ -72,10 +68,31 @@ public class Geary.AccountInformation : BaseObject {
     /** Specifies the email provider for this account. */
     public Geary.ServiceProvider service_provider { get; private set; }
 
-    /** A human-readable label describing the email service provider. */
+    /**
+     * A human-readable label describing the email service provider.
+     *
+     * Known providers such as Gmail will have a label specified by
+     * clients, but other accounts can only really be identified by
+     * their server names. This attempts to extract a 'nice' value for
+     * label based on the service's host names.
+     */
     public string service_label {
-        get; public set;
+        owned get {
+            string? value = this._service_label;
+            if (value == null) {
+                string[] host_parts = this.imap.host.split(".");
+                if (host_parts.length > 1) {
+                    host_parts = host_parts[1:host_parts.length];
+                }
+                // don't stash this in _service_label since we want it
+                // updated if the service host names change
+                value = string.joinv(".", host_parts);
+            }
+            return value;
+        }
+        set { this._service_label = value; }
     }
+    private string? _service_label = null;
 
     /**
      * A unique human-readable display name for this account.
@@ -148,10 +165,17 @@ public class Geary.AccountInformation : BaseObject {
         get; set; default = AccountInformation.next_ordinal++;
     }
 
-    /* Information related to the account's server-side authentication
-     * and configuration. */
-    public ServiceInformation imap { get; private set; }
-    public ServiceInformation smtp { get; private set; }
+    /* Incoming email service configuration. */
+    public ServiceInformation imap {
+        get; set;
+        default = new ServiceInformation(Protocol.IMAP, null);
+    }
+
+    /* Outgoing email service configuration. */
+    public ServiceInformation smtp {
+        get; set;
+        default = new ServiceInformation(Protocol.SMTP, null);
+    }
 
     /** A lock that can be used to ensure saving is serialised. */
     public Nonblocking.Mutex write_lock {
@@ -200,59 +224,10 @@ public class Geary.AccountInformation : BaseObject {
      */
     public AccountInformation(string id,
                               ServiceProvider provider,
-                              ServiceInformation imap,
-                              ServiceInformation smtp) {
+                              RFC822.MailboxAddress primary_mailbox) {
         this.id = id;
         this.service_provider = provider;
-        this.imap = imap;
-        this.smtp = smtp;
-
-        // Known providers such as Gmail will have a label specified
-        // by clients, but other accounts can only really be
-        // identified by their server names. Try to extract a 'nice'
-        // value for label based on service host names.
-        string imap_host = imap.host;
-        string[] host_parts = imap_host.split(".");
-        if (host_parts.length > 1) {
-            host_parts = host_parts[1:host_parts.length];
-        }
-        this.service_label = string.joinv(".", host_parts);
-    }
-
-    /**
-     * Creates a copy of an instance.
-     */
-    public AccountInformation.temp_copy(AccountInformation from) {
-        this(
-            from.id,
-            from.service_provider,
-            from.imap.temp_copy(),
-            from.smtp.temp_copy()
-        );
-        copy_from(from);
-        this.is_copy = true;
-    }
-
-
-    /** Copies all properties from an instance into this one. */
-    public void copy_from(AccountInformation from) {
-        this.id = from.id;
-        this.nickname = from.nickname;
-        this.mailboxes.clear();
-        this.mailboxes.add_all(from.sender_mailboxes);
-        this.prefetch_period_days = from.prefetch_period_days;
-        this.save_sent_mail = from.save_sent_mail;
-        this.ordinal = from.ordinal;
-        this.imap.copy_from(from.imap);
-        this.smtp.copy_from(from.smtp);
-        this.drafts_folder_path = from.drafts_folder_path;
-        this.sent_mail_folder_path = from.sent_mail_folder_path;
-        this.spam_folder_path = from.spam_folder_path;
-        this.trash_folder_path = from.trash_folder_path;
-        this.archive_folder_path = from.archive_folder_path;
-        this.save_drafts = from.save_drafts;
-        this.use_email_signature = from.use_email_signature;
-        this.email_signature = from.email_signature;
+        append_sender(primary_mailbox);
     }
 
     /** Sets the location of the account's storage directories. */
@@ -405,10 +380,13 @@ public class Geary.AccountInformation : BaseObject {
      */
     public Credentials? get_smtp_credentials() {
         Credentials? smtp = null;
-        if (!this.smtp.smtp_noauth) {
-            smtp = this.smtp.smtp_use_imap_credentials
-                ? this.imap.credentials
-                : this.smtp.credentials;
+        switch (this.smtp.smtp_credentials_source) {
+        case IMAP:
+            smtp = this.imap.credentials;
+            break;
+        case CUSTOM:
+            smtp = this.smtp.credentials;
+            break;
         }
         return smtp;
     }
@@ -497,4 +475,36 @@ public class Geary.AccountInformation : BaseObject {
         return path;
     }
 
+    public bool equal_to(AccountInformation other) {
+        return (
+            this == other || (
+                this.id == other.id &&
+                this.ordinal == other.ordinal &&
+                this.service_provider == other.service_provider &&
+                this.service_label == other.service_label &&
+                this.nickname == other.nickname &&
+                this.primary_mailbox.equal_to(other.primary_mailbox) &&
+                this.has_sender_aliases == other.has_sender_aliases &&
+                this.sender_mailboxes.size == other.sender_mailboxes.size &&
+                traverse(this.sender_mailboxes).all(
+                    addr => other.sender_mailboxes.contains(addr)
+                ) &&
+                this.prefetch_period_days == other.prefetch_period_days &&
+                this.save_sent_mail == other.save_sent_mail &&
+                this.imap.equal_to(other.imap) &&
+                this.smtp.equal_to(other.smtp) &&
+                this.use_email_signature == other.use_email_signature &&
+                this.email_signature == other.email_signature &&
+                this.save_drafts == other.save_drafts &&
+                this.drafts_folder_path == other.drafts_folder_path &&
+                this.sent_mail_folder_path == other.sent_mail_folder_path &&
+                this.spam_folder_path == other.spam_folder_path &&
+                this.trash_folder_path == other.trash_folder_path &&
+                this.archive_folder_path == other.archive_folder_path &&
+                this.config_dir == other.config_dir &&
+                this.data_dir == other.data_dir
+            )
+        );
+    }
+
 }
diff --git a/src/engine/api/geary-engine.vala b/src/engine/api/geary-engine.vala
index 2617370f..f345b3e7 100644
--- a/src/engine/api/geary-engine.vala
+++ b/src/engine/api/geary-engine.vala
@@ -437,7 +437,7 @@ public class Geary.Engine : BaseObject {
      * Changes the service configuration for an account.
      *
      * This updates an account's service configuration with the given
-     * configuration, by copying it over the account's existing
+     * configuration, by replacing the account's existing
      * configuration for that service. The corresponding {@link
      * Account.incoming} or {@link Account.outgoing} client service
      * will also be updated so that the new configuration will start
@@ -456,31 +456,31 @@ public class Geary.Engine : BaseObject {
             );
         }
 
-        ServiceInformation? existing = null;
         ClientService? service = null;
+        bool was_updated = false;
         switch (updated.protocol) {
         case Protocol.IMAP:
-            existing = account.imap;
+            if (!account.imap.equal_to(updated)) {
+                was_updated = true;
+                account.imap = updated;
+            }
             service = impl.incoming;
             break;
         case Protocol.SMTP:
-            existing = account.smtp;
+            if (!account.smtp.equal_to(updated)) {
+                was_updated = true;
+                account.smtp = updated;
+            }
             service = impl.outgoing;
             break;
         }
 
-        bool was_updated = false;
-        if (service != null) {
-            if (!existing.equal_to(updated)) {
-                existing.copy_from(updated);
-                was_updated = true;
-
-                Endpoint endpoint = get_shared_endpoint(
-                    account.service_provider, existing
-                );
-                impl.set_endpoint(service, endpoint);
-                account.information_changed();
-            }
+        if (was_updated) {
+            Endpoint endpoint = get_shared_endpoint(
+                account.service_provider, updated
+            );
+            impl.set_endpoint(service, endpoint);
+            account.information_changed();
         }
 
         return was_updated;
diff --git a/src/engine/api/geary-service-information.vala b/src/engine/api/geary-service-information.vala
index c7cdd221..dece1e0e 100644
--- a/src/engine/api/geary-service-information.vala
+++ b/src/engine/api/geary-service-information.vala
@@ -110,12 +110,9 @@ public enum Geary.SmtpCredentials {
 
 
 /**
- * This class encloses all the information used when connecting with the server,
- * how to authenticate with it and which credentials to use. Derived classes
- * implement specific ways of doing that. For now, the only known implementation
- * resides in Geary.LocalServiceInformation.
+ * Encapsulates configuration information for a network service.
  */
-public abstract class Geary.ServiceInformation : GLib.Object {
+public class Geary.ServiceInformation : GLib.Object {
 
 
     /** Specifies if this service is for IMAP or SMTP. */
@@ -125,7 +122,7 @@ public abstract class Geary.ServiceInformation : GLib.Object {
     public string host { get; set; default = ""; }
 
     /** The server's port. */
-    public uint16 port { get; set; }
+    public uint16 port { get; set; default = 0; }
 
     /** The transport security method to use */
     public TlsNegotiationMethod transport_security {
@@ -169,7 +166,7 @@ public abstract class Geary.ServiceInformation : GLib.Object {
      * The credentials mediator used with this service.
      *
      * It is responsible for fetching and storing the credentials if
-     * applicable.
+     * as needed.
      */
     public CredentialsMediator mediator { get; private set; }
 
@@ -227,14 +224,32 @@ public abstract class Geary.ServiceInformation : GLib.Object {
     public bool smtp_use_imap_credentials { get; set; default = true; }
 
 
-    protected ServiceInformation(Protocol proto, CredentialsMediator mediator) {
+    /**
+     * Constructs a new configuration for a specific service.
+     */
+    public ServiceInformation(Protocol proto, CredentialsMediator mediator) {
         this.protocol = proto;
         this.mediator = mediator;
     }
 
+    /**
+     * Constructs a copy of the given service configuration.
+     */
+    public ServiceInformation.copy(ServiceInformation other) {
+        this(other.protocol, other.mediator);
+        this.host = other.host;
+        this.port = other.port;
+        this.use_starttls = other.use_starttls;
+        this.use_ssl = other.use_ssl;
+        this.credentials = (
+            other.credentials != null ? other.credentials.copy() : null
+        );
+        this.mediator = other.mediator;
+        this.remember_password = other.remember_password;
+        this.smtp_noauth = other.smtp_noauth;
+        this.smtp_use_imap_credentials = other.smtp_use_imap_credentials;
+    }
 
-    /** Returns a temporary copy of this service for editing. */
-    public abstract ServiceInformation temp_copy();
 
     /**
      * Returns the default port for this service type and settings.
@@ -273,7 +288,7 @@ public abstract class Geary.ServiceInformation : GLib.Object {
              this.port == other.port &&
              this.use_starttls == other.use_starttls &&
              this.use_ssl == other.use_ssl &&
-             (this.credentials == null && other.credentials != null ||
+             (this.credentials == null && other.credentials == null ||
               this.credentials != null && this.credentials.equal_to(other.credentials)) &&
              this.mediator == other.mediator &&
              this.remember_password == other.remember_password &&
@@ -282,18 +297,4 @@ public abstract class Geary.ServiceInformation : GLib.Object {
         );
     }
 
-    public void copy_from(Geary.ServiceInformation from) {
-        this.host = from.host;
-        this.port = from.port;
-        this.use_starttls = from.use_starttls;
-        this.use_ssl = from.use_ssl;
-        this.credentials = (
-            from.credentials != null ? from.credentials.copy() : null
-        );
-        this.mediator = from.mediator;
-        this.remember_password = from.remember_password;
-        this.smtp_noauth = from.smtp_noauth;
-        this.smtp_use_imap_credentials = from.smtp_use_imap_credentials;
-    }
-
 }
diff --git a/src/engine/util/util-config-file.vala b/src/engine/util/util-config-file.vala
index f6c57c20..92eac5c3 100644
--- a/src/engine/util/util-config-file.vala
+++ b/src/engine/util/util-config-file.vala
@@ -77,11 +77,11 @@ public class Geary.ConfigFile {
             }
         }
 
-        public string get_string(string key, string def = "") {
-            string ret = def;
+        public string? get_string(string key, string? def = null) {
+            string? ret = def;
             foreach (GroupLookup lookup in this.lookups) {
                 try {
-                    ret = this.backing.get_value(
+                    ret = this.backing.get_string(
                         lookup.group, lookup.prefix + key
                     );
                     break;
@@ -92,12 +92,10 @@ public class Geary.ConfigFile {
             return ret;
         }
 
-        public void set_string(string key, string value) {
-            this.backing.set_value(this.name, key, value);
-        }
-
-        public string get_escaped_string(string key, string def = "") {
-            string ret = def;
+        public string get_required_string(string key)
+            throws GLib.KeyFileError {
+            string? ret = null;
+            GLib.KeyFileError? key_err = null;
             foreach (GroupLookup lookup in this.lookups) {
                 try {
                     ret = this.backing.get_string(
@@ -105,13 +103,21 @@ public class Geary.ConfigFile {
                     );
                     break;
                 } catch (GLib.KeyFileError err) {
+                    if (key_err == null) {
+                        key_err = err;
+                    }
                     // continue
                 }
             }
+
+            if (key_err != null) {
+                throw key_err;
+            }
+
             return ret;
         }
 
-        public void set_escaped_string(string key, string value) {
+        public void set_string(string key, string value) {
             this.backing.set_string(this.name, key, value);
         }
 
@@ -126,6 +132,12 @@ public class Geary.ConfigFile {
             return new Gee.ArrayList<string>();
         }
 
+        public Gee.List<string> get_required_string_list(string key)
+            throws GLib.KeyFileError {
+            string[] list = this.backing.get_string_list(this.name, key);
+            return Geary.Collection.array_list_wrap<string>(list);
+        }
+
         public void set_string_list(string key, Gee.List<string> value) {
             this.backing.set_string_list(this.name, key, value.to_array());
         }
@@ -177,12 +189,12 @@ public class Geary.ConfigFile {
         }
 
         /** Removes a key from this group. */
-        public void remove_key(string name) throws GLib.Error {
+        public void remove_key(string name) throws GLib.KeyFileError {
             this.backing.remove_key(this.name, name);
         }
 
         /** Removes this group from the config file. */
-        public void remove() throws GLib.Error {
+        public void remove() throws GLib.KeyFileError {
             this.backing.remove_group(this.name);
         }
 
@@ -208,7 +220,7 @@ public class Geary.ConfigFile {
      * accessed from it before doing so. Use {@link Group.exists} to
      * determine if the group has previously been created.
      */
-    public Group get_group(string name) throws GLib.Error {
+    public Group get_group(string name) {
         return new Group(this, name, this.backing);
     }
 
diff --git a/src/engine/util/util-object.vala b/src/engine/util/util-object.vala
index e47aa5a3..c45b9abd 100644
--- a/src/engine/util/util-object.vala
+++ b/src/engine/util/util-object.vala
@@ -43,13 +43,13 @@ public void unmirror_properties(Gee.List<Binding> bindings) {
 }
 
 /** Convenience method for getting an enum value's nick name. */
-internal string to_enum_nick<E>(GLib.Type type, E value) {
+public string to_enum_nick<E>(GLib.Type type, E value) {
     GLib.EnumClass enum_type = (GLib.EnumClass) type.class_ref();
     return enum_type.get_value((int) value).value_nick;
 }
 
 /** Convenience method for getting an enum value's from its nick name. */
-internal E from_enum_nick<E>(GLib.Type type, string nick) throws EngineError {
+public E from_enum_nick<E>(GLib.Type type, string nick) throws EngineError {
     GLib.EnumClass enum_type = (GLib.EnumClass) type.class_ref();
     unowned GLib.EnumValue? e_value = enum_type.get_value_by_nick(nick);
     if (e_value == null) {
diff --git a/test/client/accounts/accounts-manager-test.vala b/test/client/accounts/accounts-manager-test.vala
index c27d0b69..ffcf6cb9 100644
--- a/test/client/accounts/accounts-manager-test.vala
+++ b/test/client/accounts/accounts-manager-test.vala
@@ -8,7 +8,11 @@
 class Accounts.ManagerTest : TestCase {
 
 
+    private const string TEST_ID = "test";
+
     private Manager? test = null;
+    private Geary.AccountInformation? account = null;
+    private Geary.ServiceInformation? service = null;
     private File? tmp = null;
 
 
@@ -17,6 +21,10 @@ class Accounts.ManagerTest : TestCase {
         add_test("create_account", create_account);
         add_test("create_orphan_account", create_orphan_account);
         add_test("create_orphan_account_with_legacy", create_orphan_account_with_legacy);
+        add_test("account_config_v1", account_config_v1);
+        add_test("account_config_legacy", account_config_legacy);
+        add_test("service_config_v1", service_config_v1);
+        add_test("service_config_legacy", service_config_legacy);
     }
 
     public override void set_up() throws GLib.Error {
@@ -34,21 +42,24 @@ class Accounts.ManagerTest : TestCase {
         data.make_directory();
 
         this.test = new Manager(new GearyApplication(), config, data);
+
+        this.account = new Geary.AccountInformation(
+            TEST_ID,
+            Geary.ServiceProvider.OTHER,
+            new Geary.RFC822.MailboxAddress(null, "test1 example com")
+        );
+
+        this.service = new Geary.ServiceInformation(Geary.Protocol.SMTP, null);
     }
 
        public override void tear_down() throws GLib.Error {
         this.test = null;
+        this.account = null;
+        this.service = null;
         @delete(this.tmp);
        }
 
     public void create_account() throws GLib.Error {
-        const string ID = "test";
-        Geary.AccountInformation account = new Geary.AccountInformation(
-            ID,
-            Geary.ServiceProvider.OTHER,
-            new Geary.MockServiceInformation(),
-            new Geary.MockServiceInformation()
-        );
         bool was_added = false;
         bool was_enabled = false;
 
@@ -64,7 +75,7 @@ class Accounts.ManagerTest : TestCase {
         this.test.create_account.end(async_result());
 
         assert_int(1, this.test.size, "Account manager size");
-        assert_equal(account, this.test.get_account(ID), "Is not contained");
+        assert_equal(account, this.test.get_account(TEST_ID), "Is not contained");
         assert_true(was_added, "Was not added");
         assert_true(was_enabled, "Was not enabled");
     }
@@ -72,8 +83,7 @@ class Accounts.ManagerTest : TestCase {
     public void create_orphan_account() throws GLib.Error {
         Geary.AccountInformation account1 = this.test.new_orphan_account(
             Geary.ServiceProvider.OTHER,
-            new Geary.MockServiceInformation(),
-            new Geary.MockServiceInformation()
+            new Geary.RFC822.MailboxAddress(null, "test1 example com")
         );
         assert(account1.id == "account_01");
 
@@ -85,21 +95,12 @@ class Accounts.ManagerTest : TestCase {
 
         Geary.AccountInformation account2 = this.test.new_orphan_account(
             Geary.ServiceProvider.OTHER,
-            new Geary.MockServiceInformation(),
-            new Geary.MockServiceInformation()
+            new Geary.RFC822.MailboxAddress(null, "test1 example com")
         );
         assert(account2.id == "account_02");
     }
 
     public void create_orphan_account_with_legacy() throws GLib.Error {
-        const string ID = "test";
-        Geary.AccountInformation account = new Geary.AccountInformation(
-            ID,
-            Geary.ServiceProvider.OTHER,
-            new Geary.MockServiceInformation(),
-            new Geary.MockServiceInformation()
-        );
-
         this.test.create_account.begin(
             account, new GLib.Cancellable(),
              (obj, res) => { async_complete(res); }
@@ -108,8 +109,7 @@ class Accounts.ManagerTest : TestCase {
 
         Geary.AccountInformation account1 = this.test.new_orphan_account(
             Geary.ServiceProvider.OTHER,
-            new Geary.MockServiceInformation(),
-            new Geary.MockServiceInformation()
+            new Geary.RFC822.MailboxAddress(null, "test1 example com")
         );
         assert(account1.id == "account_01");
 
@@ -121,12 +121,90 @@ class Accounts.ManagerTest : TestCase {
 
         Geary.AccountInformation account2 = this.test.new_orphan_account(
             Geary.ServiceProvider.OTHER,
-            new Geary.MockServiceInformation(),
-            new Geary.MockServiceInformation()
+            new Geary.RFC822.MailboxAddress(null, "test1 example com")
         );
         assert(account2.id == "account_02");
     }
 
+    public void account_config_v1() throws GLib.Error {
+        this.account.email_signature = "blarg";
+        this.account.nickname = "test-name";
+        this.account.ordinal = 100;
+        this.account.prefetch_period_days = 42;
+        this.account.save_drafts = false;
+        this.account.save_sent_mail = false;
+        this.account.use_email_signature = false;
+        Accounts.AccountConfigV1 config = new Accounts.AccountConfigV1(false);
+
+        Geary.ConfigFile file =
+            new Geary.ConfigFile(this.tmp.get_child("config"));
+
+        config.save(this.account, file);
+        Geary.AccountInformation copy = config.load(file, TEST_ID, null, null);
+
+        assert_true(this.account.equal_to(copy));
+    }
+
+    public void account_config_legacy() throws GLib.Error {
+        this.account.email_signature = "blarg";
+        this.account.nickname = "test-name";
+        this.account.ordinal = 100;
+        this.account.prefetch_period_days = 42;
+        this.account.save_drafts = false;
+        this.account.save_sent_mail = false;
+        this.account.use_email_signature = false;
+        Accounts.AccountConfigLegacy config =
+            new Accounts.AccountConfigLegacy();
+
+        Geary.ConfigFile file =
+            new Geary.ConfigFile(this.tmp.get_child("config"));
+
+        config.save(this.account, file);
+        Geary.AccountInformation copy = config.load(file, TEST_ID, null, null);
+
+        assert_true(this.account.equal_to(copy));
+    }
+
+    public void service_config_v1() throws GLib.Error {
+        this.service.host = "blarg";
+        this.service.port = 1234;
+        this.service.transport_security = Geary.TlsNegotiationMethod.NONE;
+        this.service.smtp_credentials_source = Geary.SmtpCredentials.CUSTOM;
+        this.service.credentials = new Geary.Credentials(
+            Geary.Credentials.Method.PASSWORD, "testerson"
+        );
+        Accounts.ServiceConfigV1 config = new Accounts.ServiceConfigV1();
+        Geary.ConfigFile file =
+            new Geary.ConfigFile(this.tmp.get_child("config"));
+
+        config.save(this.account, this.service, file);
+        Geary.ServiceInformation copy = config.load(
+            file, this.account, this.service.protocol, null
+        );
+
+        assert_true(this.service.equal_to(copy));
+    }
+
+    public void service_config_legacy() throws GLib.Error {
+        this.service.host = "blarg";
+        this.service.port = 1234;
+        this.service.transport_security = Geary.TlsNegotiationMethod.NONE;
+        this.service.smtp_credentials_source = Geary.SmtpCredentials.CUSTOM;
+        this.service.credentials = new Geary.Credentials(
+            Geary.Credentials.Method.PASSWORD, "testerson"
+        );
+        Accounts.ServiceConfigLegacy config = new Accounts.ServiceConfigLegacy();
+        Geary.ConfigFile file =
+            new Geary.ConfigFile(this.tmp.get_child("config"));
+
+        config.save(this.account, this.service, file);
+        Geary.ServiceInformation copy = config.load(
+            file, this.account, this.service.protocol, null
+        );
+
+        assert_true(this.service.equal_to(copy));
+    }
+
     private void delete(File parent) throws GLib.Error {
         FileInfo info = parent.query_info(
             "standard::*",
diff --git a/test/engine/api/geary-account-information-test.vala 
b/test/engine/api/geary-account-information-test.vala
index 42e97879..08d51b17 100644
--- a/test/engine/api/geary-account-information-test.vala
+++ b/test/engine/api/geary-account-information-test.vala
@@ -17,19 +17,19 @@ class Geary.AccountInformationTest : TestCase {
         AccountInformation test = new AccountInformation(
             "test",
             ServiceProvider.OTHER,
-            new MockServiceInformation(),
-            new MockServiceInformation()
+            new RFC822.MailboxAddress(null, "test1 example com")
         );
 
-        test.append_sender(new RFC822.MailboxAddress(null, "test1 example com"));
+        assert_true(test.primary_mailbox.equal_to(
+                        new RFC822.MailboxAddress(null, "test1 example com")));
         assert_false(test.has_sender_aliases);
 
         test.append_sender(new RFC822.MailboxAddress(null, "test2 example com"));
+        assert_true(test.has_sender_aliases);
+
         test.append_sender(new RFC822.MailboxAddress(null, "test3 example com"));
         assert_true(test.has_sender_aliases);
 
-        assert_true(test.primary_mailbox.equal_to(
-                        new RFC822.MailboxAddress(null, "test1 example com")));
         assert_true(
             test.has_sender_mailbox(new RFC822.MailboxAddress(null, "test1 example com")),
             "Primary address not found"
diff --git a/test/engine/api/geary-engine-test.vala b/test/engine/api/geary-engine-test.vala
index 8a6f4bbd..99206fd4 100644
--- a/test/engine/api/geary-engine-test.vala
+++ b/test/engine/api/geary-engine-test.vala
@@ -66,8 +66,7 @@ class Geary.EngineTest : TestCase {
         AccountInformation info = new AccountInformation(
             "test",
             ServiceProvider.OTHER,
-            new MockServiceInformation(),
-            new MockServiceInformation()
+            new RFC822.MailboxAddress(null, "test1 example com")
         );
         assert_false(this.engine.has_account(info.id));
 
@@ -86,8 +85,7 @@ class Geary.EngineTest : TestCase {
         AccountInformation info = new AccountInformation(
             "test",
             ServiceProvider.OTHER,
-            new MockServiceInformation(),
-            new MockServiceInformation()
+            new RFC822.MailboxAddress(null, "test1 example com")
         );
         this.engine.add_account(info);
         assert_true(this.engine.has_account(info.id));
@@ -103,8 +101,7 @@ class Geary.EngineTest : TestCase {
         AccountInformation info = new AccountInformation(
             "test",
             ServiceProvider.OTHER,
-            new MockServiceInformation(),
-            new MockServiceInformation()
+            new RFC822.MailboxAddress(null, "test1 example com")
         );
         assert_false(this.engine.has_account(info.id));
 
diff --git a/test/engine/app/app-conversation-monitor-test.vala 
b/test/engine/app/app-conversation-monitor-test.vala
index f99037fb..f3715170 100644
--- a/test/engine/app/app-conversation-monitor-test.vala
+++ b/test/engine/app/app-conversation-monitor-test.vala
@@ -31,8 +31,7 @@ class Geary.App.ConversationMonitorTest : TestCase {
         this.account_info = new AccountInformation(
             "account_01",
             ServiceProvider.OTHER,
-            new MockServiceInformation(),
-            new MockServiceInformation()
+            new RFC822.MailboxAddress(null, "test1 example com")
         );
         this.account = new MockAccount(this.account_info);
         this.base_folder = new MockFolder(
diff --git a/test/engine/imap-engine/account-processor-test.vala 
b/test/engine/imap-engine/account-processor-test.vala
index 8c6f649c..e80865eb 100644
--- a/test/engine/imap-engine/account-processor-test.vala
+++ b/test/engine/imap-engine/account-processor-test.vala
@@ -72,8 +72,7 @@ public class Geary.ImapEngine.AccountProcessorTest : TestCase {
         this.info = new Geary.AccountInformation(
             "test-info",
             ServiceProvider.OTHER,
-            new MockServiceInformation(),
-            new MockServiceInformation()
+            new RFC822.MailboxAddress(null, "test1 example com")
         );
         this.account = new Geary.MockAccount(this.info);
 
diff --git a/test/engine/util-config-file-test.vala b/test/engine/util-config-file-test.vala
index ed79841f..7cf3e849 100644
--- a/test/engine/util-config-file-test.vala
+++ b/test/engine/util-config-file-test.vala
@@ -19,7 +19,6 @@ class Geary.ConfigFileTest : TestCase {
         base("Geary.ConfigFileTest");
         add_test("test_string", test_string);
         add_test("test_string_fallback", test_string_fallback);
-        add_test("test_escaped_string", test_escaped_string);
         add_test("test_string_list", test_string_list);
         add_test("test_string_list", test_string_list);
         add_test("test_bool", test_bool);
@@ -55,12 +54,6 @@ class Geary.ConfigFileTest : TestCase {
         assert_string("a string", this.test_group.get_string(TEST_KEY));
     }
 
-    public void test_escaped_string() throws Error {
-        this.test_group.set_escaped_string(TEST_KEY, "a\nstring");
-        assert_string("a\nstring", this.test_group.get_escaped_string(TEST_KEY));
-        assert_string("=default", this.test_group.get_escaped_string(TEST_KEY_MISSING, "=default"));
-    }
-
     public void test_string_list() throws Error {
         this.test_group.set_string_list(
             TEST_KEY, new Gee.ArrayList<string>.wrap({ "a", "b"})
diff --git a/test/meson.build b/test/meson.build
index dda43e35..26e588ba 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -19,7 +19,6 @@ geary_test_engine_sources = [
   'engine/api/geary-email-properties-mock.vala',
   'engine/api/geary-folder-mock.vala',
   'engine/api/geary-folder-path-mock.vala',
-  'engine/api/geary-service-information-mock.vala',
 
   'engine/api/geary-account-information-test.vala',
   'engine/api/geary-attachment-test.vala',
@@ -68,7 +67,6 @@ geary_test_client_sources = [
   # geary-engine_internal.vapi, which leads to duplicate symbols when
   # linking
   'engine/api/geary-credentials-mediator-mock.vala',
-  'engine/api/geary-service-information-mock.vala',
 
   'client/accounts/accounts-manager-test.vala',
   'client/application/geary-configuration-test.vala',


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