[geary/mjog/invert-folder-class-hierarchy: 25/72] Geary.Folder: Make FolderProperties a remote-only concept




commit c1f91f06738e07c9c3b92bcae84af665453cc969
Author: Michael Gratton <mike vee net>
Date:   Sun Feb 14 10:32:56 2021 +1100

    Geary.Folder: Make FolderProperties a remote-only concept
    
    Remove the `properties` property from `Geary.Folder`, replacing it with
    just `email_total` and `email_unread` properties. Move the
    `FolderProperties` class into `RemoteFolder` and convert it into an
    interface.
    
    Remove the `email_count_changed` signal since API clients can just
    listen to notify signals for the two new properties, ensure
    implementations of folder are update and notifying of changes to them
    correctly.

 po/POTFILES.in                                     |  2 -
 src/client/application/application-controller.vala | 17 +++--
 .../application/application-main-window.vala       | 12 +--
 src/client/components/folder-popover.vala          | 21 +++---
 .../folder-list/folder-list-folder-entry.vala      | 22 +++---
 .../folder-list/folder-list-search-branch.vala     | 10 +--
 .../plugin/mail-merge/mail-merge-folder.vala       | 44 ++++-------
 src/engine/api/geary-folder-properties.vala        | 88 ----------------------
 src/engine/api/geary-folder-supports-create.vala   |  2 +-
 src/engine/api/geary-folder.vala                   | 28 ++-----
 src/engine/api/geary-remote-folder.vala            | 59 ++++++++++++++-
 src/engine/app/app-conversation-monitor.vala       |  2 +-
 src/engine/app/app-draft-manager.vala              |  3 +-
 src/engine/app/app-search-folder.vala              | 47 ++++--------
 .../app-fill-window-operation.vala                 |  4 +-
 src/engine/imap-db/imap-db-folder.vala             | 68 ++++++++++++-----
 .../imap-engine-account-synchronizer.vala          | 17 ++---
 .../imap-engine/imap-engine-generic-account.vala   |  5 +-
 .../imap-engine/imap-engine-minimal-folder.vala    | 68 +++++++++++------
 .../replay-ops/imap-engine-empty-folder.vala       | 25 ++----
 .../replay-ops/imap-engine-move-email-commit.vala  | 16 ++--
 .../replay-ops/imap-engine-move-email-prepare.vala | 18 +----
 .../replay-ops/imap-engine-move-email-revoke.vala  | 13 +---
 .../replay-ops/imap-engine-remove-email.vala       | 34 ++++-----
 .../replay-ops/imap-engine-replay-append.vala      |  4 +-
 .../replay-ops/imap-engine-replay-removal.vala     | 15 +---
 src/engine/imap/api/imap-folder-properties.vala    | 41 +++++++---
 src/engine/meson.build                             |  2 -
 src/engine/outbox/outbox-folder-properties.vala    | 22 ------
 src/engine/outbox/outbox-folder.vala               | 88 ++++++++++------------
 test/engine/app/app-conversation-monitor-test.vala |  2 -
 test/engine/app/app-conversation-set-test.vala     |  1 -
 test/engine/app/app-conversation-test.vala         |  1 -
 test/meson.build                                   |  1 -
 test/mock/mock-account.vala                        |  4 +-
 test/mock/mock-folder-properties.vala              | 24 ------
 test/mock/mock-folder.vala                         | 17 +++--
 test/mock/mock-remote-folder.vala                  | 46 +++++++++--
 38 files changed, 405 insertions(+), 488 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 0aa7b8e1a..af6b5824b 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -166,7 +166,6 @@ src/engine/api/geary-email.vala
 src/engine/api/geary-endpoint.vala
 src/engine/api/geary-engine-error.vala
 src/engine/api/geary-engine.vala
-src/engine/api/geary-folder-properties.vala
 src/engine/api/geary-folder-supports-archive.vala
 src/engine/api/geary-folder-supports-copy.vala
 src/engine/api/geary-folder-supports-create.vala
@@ -385,7 +384,6 @@ src/engine/nonblocking/nonblocking-reporting-semaphore.vala
 src/engine/nonblocking/nonblocking-variants.vala
 src/engine/outbox/outbox-email-identifier.vala
 src/engine/outbox/outbox-email-properties.vala
-src/engine/outbox/outbox-folder-properties.vala
 src/engine/outbox/outbox-folder.vala
 src/engine/rfc822/rfc822-error.vala
 src/engine/rfc822/rfc822-gmime-filter-blockquotes.vala
diff --git a/src/client/application/application-controller.vala 
b/src/client/application/application-controller.vala
index ccb8cf2b1..8797c3710 100644
--- a/src/client/application/application-controller.vala
+++ b/src/client/application/application-controller.vala
@@ -26,8 +26,8 @@ internal class Application.Controller :
         return (
             target != null &&
             target.used_as != TRASH &&
-            !target.properties.is_local_only &&
-            (target as Geary.FolderSupport.Move) != null
+            target is Geary.RemoteFolder &&
+            target is Geary.FolderSupport.Move
         );
     }
 
@@ -35,10 +35,13 @@ internal class Application.Controller :
     private static bool should_add_folder(Gee.Collection<Geary.Folder>? all,
                                           Geary.Folder folder) {
         // if folder is openable, add it
-        if (folder.properties.is_openable != Geary.Trillian.FALSE)
-            return true;
-        else if (folder.properties.has_children == Geary.Trillian.FALSE)
-            return false;
+        var remote = folder as Geary.RemoteFolder;
+        if (remote != null) {
+            if (remote.remote_properties.is_openable != Geary.Trillian.FALSE)
+                return true;
+            else if (remote.remote_properties.has_children == Geary.Trillian.FALSE)
+                return false;
+        }
 
         // if folder contains children, we must ensure that there is
         // at least one of the same type
@@ -844,7 +847,7 @@ internal class Application.Controller :
     public async void delete_conversations(Geary.FolderSupport.Remove target,
                                            Gee.Collection<Geary.App.Conversation> conversations)
         throws GLib.Error {
-        var messages = target.properties.is_virtual
+        var messages = target.used_as == SEARCH
             ? to_all_email_ids(conversations)
             : to_in_folder_email_ids(conversations);
         yield delete_messages(target, conversations, messages);
diff --git a/src/client/application/application-main-window.vala 
b/src/client/application/application-main-window.vala
index 5c8af4b32..4abf94604 100644
--- a/src/client/application/application-main-window.vala
+++ b/src/client/application/application-main-window.vala
@@ -719,7 +719,8 @@ public class Application.MainWindow :
                     this.selected_folder, true
                 );
 
-                this.selected_folder.properties.notify.disconnect(update_headerbar);
+                this.selected_folder.notify["email-total"].disconnect(update_headerbar);
+                this.selected_folder.notify["email-unread"].disconnect(update_headerbar);
                 this.selected_folder = null;
             }
             if (this.conversations != null) {
@@ -773,7 +774,8 @@ public class Application.MainWindow :
             // loading conversations.
 
             if (to_select != null) {
-                to_select.properties.notify.connect(update_headerbar);
+                to_select.notify["email-total"].connect(update_headerbar);
+                to_select.notify["email-unread"].connect(update_headerbar);
 
                 this.conversations = new Geary.App.ConversationMonitor(
                     to_select,
@@ -1709,11 +1711,11 @@ public class Application.MainWindow :
             switch (this.selected_folder.used_as) {
             case DRAFTS:
             case OUTBOX:
-                count = this.selected_folder.properties.email_total;
+                count = this.selected_folder.email_total;
                 break;
 
             default:
-                count = this.selected_folder.properties.email_unread;
+                count = this.selected_folder.email_unread;
                 break;
             }
 
@@ -1862,7 +1864,7 @@ public class Application.MainWindow :
                     focus = this.conversation_list_view;
                 } else {
                     if (this.conversation_actions.selected_conversations == 1 &&
-                        this.selected_folder.properties.email_total > 0) {
+                        this.selected_folder.email_total > 0) {
                         main_leaflet.navigate(Hdy.NavigationDirection.FORWARD);
                         focus = this.conversation_viewer.visible_child;
                     }
diff --git a/src/client/components/folder-popover.vala b/src/client/components/folder-popover.vala
index faba1df2b..60c9c0577 100644
--- a/src/client/components/folder-popover.vala
+++ b/src/client/components/folder-popover.vala
@@ -29,18 +29,15 @@ public class FolderPopover : Gtk.Popover {
     }
 
     public void add_folder(Geary.Folder folder) {
-        // don't allow multiples and don't allow folders that can't be opened (that means they
-        // support almost no operations and have no content)
-        if (has_folder(folder) || folder.properties.is_openable.is_impossible())
-            return;
-
-        // also don't allow local-only or virtual folders, which also have a limited set of
-        // operations
-        if (folder.properties.is_local_only || folder.properties.is_virtual)
-            return;
-
-        list_box.add(build_row(folder));
-        list_box.invalidate_sort();
+        // Only include remote-backed folders that can be opened
+        if (!has_folder(folder)) {
+            var remote = folder as Geary.RemoteFolder;
+            if (remote != null &&
+                !remote.remote_properties.is_openable.is_impossible()) {
+                list_box.add(build_row(folder));
+                list_box.invalidate_sort();
+            }
+        }
     }
 
     public void enable_disable_folder(Geary.Folder folder, bool sensitive) {
diff --git a/src/client/folder-list/folder-list-folder-entry.vala 
b/src/client/folder-list/folder-list-folder-entry.vala
index ae78c1ffc..ec20de041 100644
--- a/src/client/folder-list/folder-list-folder-entry.vala
+++ b/src/client/folder-list/folder-list-folder-entry.vala
@@ -20,14 +20,14 @@ public class FolderList.FolderEntry :
         this.context = context;
         this.context.notify.connect(on_context_changed);
         this.has_new = false;
-        
this.folder.properties.notify[Geary.FolderProperties.PROP_NAME_EMAIL_TOTAL].connect(on_counts_changed);
-        
this.folder.properties.notify[Geary.FolderProperties.PROP_NAME_EMAIL_UNREAD].connect(on_counts_changed);
+        this.folder.notify["email-total"].connect(on_counts_changed);
+        this.folder.notify["email-unread"].connect(on_counts_changed);
     }
 
     ~FolderEntry() {
         this.context.notify.disconnect(on_context_changed);
-        
this.folder.properties.notify[Geary.FolderProperties.PROP_NAME_EMAIL_TOTAL].disconnect(on_counts_changed);
-        
this.folder.properties.notify[Geary.FolderProperties.PROP_NAME_EMAIL_UNREAD].disconnect(on_counts_changed);
+        this.folder.notify["email-total"].disconnect(on_counts_changed);
+        this.folder.notify["email-unread"].disconnect(on_counts_changed);
     }
 
     public override string get_sidebar_name() {
@@ -39,18 +39,18 @@ public class FolderList.FolderEntry :
         // messages in a folder. String substitution is the actual
         // number.
         string total_msg = ngettext(
-            "%d message", "%d messages", folder.properties.email_total
-        ).printf(folder.properties.email_total);
+            "%d message", "%d messages", folder.email_total
+        ).printf(folder.email_total);
 
-        if (folder.properties.email_unread == 0)
+        if (folder.email_unread == 0)
             return total_msg;
 
         // Translators: Label displaying number of unread email
         // messages in a folder. String substitution is the actual
         // number.
         string unread_msg = ngettext(
-            "%d unread", "%d unread", folder.properties.email_unread
-        ).printf(folder.properties.email_unread);
+            "%d unread", "%d unread", folder.email_unread
+        ).printf(folder.email_unread);
 
         // Translators: This string represents the divider between two
         // messages: "n messages" and "n unread", shown in the folder
@@ -99,10 +99,10 @@ public class FolderList.FolderEntry :
     public override int get_count() {
         switch (this.context.displayed_count) {
         case TOTAL:
-            return folder.properties.email_total;
+            return folder.email_total;
 
         case UNREAD:
-            return folder.properties.email_unread;
+            return folder.email_unread;
 
         default:
             return 0;
diff --git a/src/client/folder-list/folder-list-search-branch.vala 
b/src/client/folder-list/folder-list-search-branch.vala
index f3c27ab44..e1234ad0b 100644
--- a/src/client/folder-list/folder-list-search-branch.vala
+++ b/src/client/folder-list/folder-list-search-branch.vala
@@ -35,17 +35,13 @@ public class FolderList.SearchEntry : FolderList.AbstractFolderEntry {
 
         this.engine.account_available.connect(on_accounts_changed);
         this.engine.account_unavailable.connect(on_accounts_changed);
-        folder.properties.notify[
-            Geary.FolderProperties.PROP_NAME_EMAIL_TOTAL
-        ].connect(on_email_total_changed);
+        folder.notify["email-total"].connect(on_email_total_changed);
     }
 
     ~SearchEntry() {
         this.engine.account_available.disconnect(on_accounts_changed);
         this.engine.account_unavailable.disconnect(on_accounts_changed);
-        folder.properties.notify[
-            Geary.FolderProperties.PROP_NAME_EMAIL_TOTAL
-        ].disconnect(on_email_total_changed);
+        folder.notify["email-total"].disconnect(on_email_total_changed);
     }
 
     public override string get_sidebar_name() {
@@ -55,7 +51,7 @@ public class FolderList.SearchEntry : FolderList.AbstractFolderEntry {
     }
 
     public override string? get_sidebar_tooltip() {
-        int total = folder.properties.email_total;
+        int total = folder.email_total;
         return ngettext("%d result", "%d results", total).printf(total);
     }
 
diff --git a/src/client/plugin/mail-merge/mail-merge-folder.vala 
b/src/client/plugin/mail-merge/mail-merge-folder.vala
index 4be39dae4..44bb07fc6 100644
--- a/src/client/plugin/mail-merge/mail-merge-folder.vala
+++ b/src/client/plugin/mail-merge/mail-merge-folder.vala
@@ -75,41 +75,28 @@ public class MailMerge.Folder : Geary.BaseObject,
     }
 
 
-    private class FolderProperties : Geary.FolderProperties {
-
-        public FolderProperties() {
-            base(
-                0, 0,
-                Geary.Trillian.FALSE, Geary.Trillian.FALSE, Geary.Trillian.TRUE,
-                true, false, false
-            );
-        }
-
-        public void set_total(int total) {
-            this.email_total = total;
-        }
-
-    }
-
-
     /** {@inheritDoc} */
     public override Geary.Account account {
         get { return this._account; }
     }
     private Geary.Account _account;
 
-    /** {@inheritDoc} */
-    public override Geary.FolderProperties properties {
-        get { return _properties; }
-    }
-    private FolderProperties _properties = new FolderProperties();
-
     /** {@inheritDoc} */
     public override Geary.Folder.Path path {
         get { return _path; }
     }
     private Geary.Folder.Path _path;
 
+    /** {@inheritDoc} */
+    public int email_total {
+        get { return this.ids.size; }
+    }
+
+    /** {@inheritDoc} */
+    public int email_unread {
+        get { return 0; }
+    }
+
     /** {@inheritDoc} */
     public override Geary.Folder.SpecialUse used_as {
         get { return this._used_as; }
@@ -123,10 +110,7 @@ public class MailMerge.Folder : Geary.BaseObject,
     public string data_display_name { get; private set; }
 
     /** The number of email that have been sent. */
-    public uint email_sent { get; private set; default = 0; }
-
-    /** The number of email in total. */
-    public uint email_total { get; private set; default = 0; }
+    public int email_sent { get; private set; default = 0; }
 
     /** Specifies if the merged mail is currently being sent. */
     public bool is_sending { get; private set; default = false; }
@@ -343,9 +327,8 @@ public class MailMerge.Folder : Geary.BaseObject,
                 this.ids.add(id);
                 this.composed.set(id, composed);
                 this.email.set(id, email);
-                this._properties.set_total((int) next_id);
-                this.email_total = (uint) next_id;
 
+                notify_property("email-total");
                 email_inserted(Geary.Collection.single(id));
                 record = yield this.data.read_record();
             }
@@ -385,7 +368,8 @@ public class MailMerge.Folder : Geary.BaseObject,
                     this.ids.remove_at(last);
                     this.email.unset(id);
                     this.composed.unset(id);
-                    this._properties.set_total(last);
+
+                    notify_property("email-total");
                     email_removed(Geary.Collection.single(id));
 
                     // Rate limit to ~30/minute for now
diff --git a/src/engine/api/geary-folder-supports-create.vala 
b/src/engine/api/geary-folder-supports-create.vala
index 664e9f6c0..d947dfda3 100644
--- a/src/engine/api/geary-folder-supports-create.vala
+++ b/src/engine/api/geary-folder-supports-create.vala
@@ -30,7 +30,7 @@ public interface Geary.FolderSupport.Create : Folder {
      * time to be set when saved.  Like EmailFlags, this is optional
      * if not applicable.
      *
-     * @see FolderProperties.create_never_returns_id
+     * @see RemoteFolder.RemoteProperties.create_never_returns_id
      */
     public abstract async EmailIdentifier?
         create_email_async(RFC822.Message rfc822,
diff --git a/src/engine/api/geary-folder.vala b/src/engine/api/geary-folder.vala
index a070264dd..4f65eb990 100644
--- a/src/engine/api/geary-folder.vala
+++ b/src/engine/api/geary-folder.vala
@@ -486,14 +486,6 @@ public interface Geary.Folder : GLib.Object, Logging.Source {
 
     }
 
-    [Flags]
-    public enum CountChangeReason {
-        NONE = 0,
-        APPENDED,
-        INSERTED,
-        REMOVED
-    }
-
     /**
      * Flags modifying how email is retrieved.
      */
@@ -553,12 +545,15 @@ public interface Geary.Folder : GLib.Object, Logging.Source {
     /** The account that owns this folder. */
     public abstract Geary.Account account { get; }
 
-    /** Current properties for this folder. */
-    public abstract Geary.FolderProperties properties { get; }
-
     /** The path to this folder in the account's folder hierarchy. */
     public abstract Path path { get; }
 
+    /** The total number of email messages in this folder. */
+    public abstract int email_total { get; }
+
+    /** The number of unread email messages in this folder. */
+    public abstract int email_unread { get; }
+
     /**
      * Determines the special use of this folder.
      *
@@ -647,17 +642,6 @@ public interface Geary.Folder : GLib.Object, Logging.Source {
         this.account.email_flags_changed_in_folder(map, this);
     }
 
-    /**
-     * Fired when the total count of email in a folder has changed in any way.
-     *
-     * Note that this signal will fire after {@link email_appended},
-     * and {@link email_removed} (although see the note at
-     * email_removed).
-     */
-    public virtual signal void email_count_changed(
-        int new_count, CountChangeReason reason
-    );
-
     /**
      * Fired when the folder's special use has changed.
      *
diff --git a/src/engine/api/geary-remote-folder.vala b/src/engine/api/geary-remote-folder.vala
index f7290d0fa..1d5497579 100644
--- a/src/engine/api/geary-remote-folder.vala
+++ b/src/engine/api/geary-remote-folder.vala
@@ -1,6 +1,6 @@
 /*
  * Copyright © 2016 Software Freedom Conservancy Inc.
- * Copyright © 2018-2020 Michael Gratton <mike vee net>
+ * Copyright © 2018-2021 Michael Gratton <mike vee net>
  *
  * This software is licensed under the GNU Lesser General Public License
  * (version 2.1 or later). See the COPYING file in this distribution.
@@ -37,6 +37,63 @@
 public interface Geary.RemoteFolder : Folder {
 
 
+    /** A collection of known properties about a remote mailbox. */
+    public interface RemoteProperties : GLib.Object {
+
+
+        /**
+         * The total count of email in the remote mailbox.
+         */
+        public abstract int email_total { get; protected set; }
+
+        /**
+         * The total count of unread email in the remote mailbox.
+         */
+        public abstract int email_unread { get; protected set; }
+
+        /**
+         * Indicates whether the remote mailbox has children.
+         *
+         * Note that if {@link has_children} == {@link
+         * Trillian.TRUE}, it implies {@link supports_children} ==
+         * {@link Trillian.TRUE}.
+         */
+        public abstract Trillian has_children { get; protected set; }
+
+        /**
+         * Indicates whether the remote mailbox can have children.
+         *
+         * This does ''not'' mean creating a sub-folder is guaranteed
+         * to succeed.
+         */
+        public abstract Trillian supports_children { get; protected set; }
+
+        /**
+         * Indicates whether the remote mailbox can be opened.
+         *
+         * Mailboxes that cannot be opened exist as steps in the
+         * mailbox name-space only - they do not contain email.
+         */
+        public abstract Trillian is_openable { get; protected set; }
+
+        /**
+         * Indicates whether the remote mailbox reports ids for created email.
+         *
+         * True if a folder supporting {@link FolderSupport.Create}
+         * will not to return a {@link EmailIdentifier} when a call to
+         * {@link FolderSupport.Create.create_email_async} succeeds.
+         *
+         * This is for IMAP servers that don't support UIDPLUS. Most
+         * servers support UIDPLUS, so this will usually be false.
+         */
+        public abstract bool create_never_returns_id { get; protected set; }
+
+    }
+
+
+    /** Last known remote properties for this folder. */
+    public abstract RemoteProperties remote_properties { get; }
+
     /**
      * Indicates if the folder is checking for remote changes to email.
      *
diff --git a/src/engine/app/app-conversation-monitor.vala b/src/engine/app/app-conversation-monitor.vala
index dc54f1e38..876b75cbd 100644
--- a/src/engine/app/app-conversation-monitor.vala
+++ b/src/engine/app/app-conversation-monitor.vala
@@ -102,7 +102,7 @@ public class Geary.App.ConversationMonitor : BaseObject, Logging.Source {
     public bool can_load_more {
         get {
             return (
-                this.base_folder.properties.email_total >
+                this.base_folder.email_total >
                 this.folder_window_size
             ) && !this.fill_complete;
         }
diff --git a/src/engine/app/app-draft-manager.vala b/src/engine/app/app-draft-manager.vala
index 65ebc4d9f..2a3b0a0c4 100644
--- a/src/engine/app/app-draft-manager.vala
+++ b/src/engine/app/app-draft-manager.vala
@@ -217,7 +217,8 @@ public class Geary.App.DraftManager : BaseObject {
 
         // if drafts folder doesn't return the identifier of newly created emails, then this object
         // can't do it's work ... wait until open to check for this, to be absolutely sure
-        if (drafts_folder.properties.create_never_returns_id) {
+        if (drafts_folder is RemoteFolder &&
+            ((RemoteFolder) drafts_folder).remote_properties.create_never_returns_id) {
             throw new EngineError.UNSUPPORTED(
                 "%s: Drafts folder %s does not return created mail ID",
                 to_string(), drafts_folder.to_string()
diff --git a/src/engine/app/app-search-folder.vala b/src/engine/app/app-search-folder.vala
index 4e9b65dfa..98a686cda 100644
--- a/src/engine/app/app-search-folder.vala
+++ b/src/engine/app/app-search-folder.vala
@@ -33,20 +33,6 @@ public class Geary.App.SearchFolder : BaseObject,
     };
 
 
-    private class FolderPropertiesImpl : FolderProperties {
-
-
-        public FolderPropertiesImpl(int total, int unread) {
-            base(total, unread, Trillian.FALSE, Trillian.FALSE, Trillian.TRUE, true, true, false);
-        }
-
-        public void set_total(int total) {
-            this.email_total = total;
-        }
-
-    }
-
-
     // Represents an entry in the folder. Does not implement
     // Gee.Comparable since that would require extending GLib.Object
     // and hence make them very heavyweight.
@@ -83,18 +69,22 @@ public class Geary.App.SearchFolder : BaseObject,
     }
     private weak Account _account;
 
-    /** {@inheritDoc} */
-    public FolderProperties properties {
-        get { return _properties; }
-    }
-    private FolderPropertiesImpl _properties;
-
     /** {@inheritDoc} */
     public Folder.Path path {
         get { return _path; }
     }
     private Folder.Path? _path = null;
 
+    /** {@inheritDoc} */
+    public int email_total {
+        get { return this.entries.size; }
+    }
+
+    /** {@inheritDoc} */
+    public int email_unread {
+        get { return 0; }
+    }
+
     /**
      * {@inheritDoc}
      *
@@ -129,7 +119,6 @@ public class Geary.App.SearchFolder : BaseObject,
 
     public SearchFolder(Account account, Folder.Root root) {
         this._account = account;
-        this._properties = new FolderPropertiesImpl(0, 0);
         this._path = root.get_child(MAGIC_BASENAME, Trillian.TRUE);
 
         account.folders_available_unavailable.connect(on_folders_available_unavailable);
@@ -187,8 +176,8 @@ public class Geary.App.SearchFolder : BaseObject,
         this.entries = new_entry_set();
         this.ids = new_id_map();
 
+        notify_property("email-total");
         email_removed(old_ids.keys);
-        email_count_changed(0, REMOVED);
     }
 
     /**
@@ -598,29 +587,25 @@ public class Geary.App.SearchFolder : BaseObject,
             this.entries = entries;
             this.ids = ids;
 
-            this._properties.set_total(entries.size);
+            if (!added.is_empty && !removed.is_empty) {
+                notify_property("email-total");
+            }
 
             // Note that we probably shouldn't be firing these signals from inside
             // our mutex lock.  Keep an eye on it, and if there's ever a case where
             // it might cause problems, it shouldn't be too hard to move the
             // firings outside.
 
-            Folder.CountChangeReason reason = CountChangeReason.NONE;
-            if (removed.size > 0) {
+            if (!removed.is_empty) {
                 email_removed(removed);
-                reason |= Folder.CountChangeReason.REMOVED;
             }
-            if (added.size > 0) {
+            if (!added.is_empty) {
                 // TODO: we'd like to be able to use APPENDED here
                 // when applicable, but because of the potential to
                 // append a thousand results at once and the
                 // ConversationMonitor's inability to handle that
                 // gracefully (#7464), we always use INSERTED for now.
                 email_inserted(added);
-                reason |= Folder.CountChangeReason.INSERTED;
-            }
-            if (reason != CountChangeReason.NONE) {
-                email_count_changed(this.entries.size, reason);
             }
             debug("Processing done, entries/ids: %d/%d", entries.size, ids.size);
         } else {
diff --git a/src/engine/app/conversation-monitor/app-fill-window-operation.vala 
b/src/engine/app/conversation-monitor/app-fill-window-operation.vala
index 86685f5f5..53bc0fa96 100644
--- a/src/engine/app/conversation-monitor/app-fill-window-operation.vala
+++ b/src/engine/app/conversation-monitor/app-fill-window-operation.vala
@@ -52,7 +52,7 @@ private class Geary.App.FillWindowOperation : ConversationOperation {
             "Filled %d of %d locally, window: %d, total: %d",
             loaded, num_to_load,
             this.monitor.conversations.size,
-            this.monitor.base_folder.properties.email_total
+            this.monitor.base_folder.email_total
         );
 
         var remote = this.monitor.base_folder as RemoteFolder;
@@ -83,7 +83,7 @@ private class Geary.App.FillWindowOperation : ConversationOperation {
                 "Filled %d of %d from the remote, window: %d, total: %d",
                 loaded, num_to_load,
                 this.monitor.conversations.size,
-                this.monitor.base_folder.properties.email_total
+                this.monitor.base_folder.email_total
             );
 
         }
diff --git a/src/engine/imap-db/imap-db-folder.vala b/src/engine/imap-db/imap-db-folder.vala
index 695f3a70b..eeed32d86 100644
--- a/src/engine/imap-db/imap-db-folder.vala
+++ b/src/engine/imap-db/imap-db-folder.vala
@@ -129,13 +129,56 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
         this.properties = properties;
     }
 
-    public async int get_email_count_async(ListFlags flags, Cancellable? cancellable) throws Error {
+    public async int get_email_count_async(ListFlags flags,
+                                           GLib.Cancellable? cancellable)
+        throws GLib.Error {
         int count = 0;
-        yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
-            count = do_get_email_count(cx, flags, cancellable);
+        yield db.exec_transaction_async(
+            RO,
+            (cx) => {
+                Db.Statement stmt = cx.prepare(
+                    "SELECT COUNT(*) FROM MessageLocationTable WHERE folder_id=?");
+                stmt.bind_rowid(0, folder_id);
 
-            return Db.TransactionOutcome.SUCCESS;
-        }, cancellable);
+                Db.Result results = stmt.exec(cancellable);
+                if (!results.finished) {
+                    count = results.int_at(0);
+                    if (!flags.include_marked_for_remove()) {
+                        count -= do_get_marked_removed_count(cx, cancellable);
+                    }
+                }
+                return SUCCESS;
+            },
+            cancellable
+        );
+
+        return count;
+    }
+
+    public async int get_email_unread_async(GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        int count = 0;
+        yield db.exec_transaction_async(
+            RO,
+            (cx) => {
+                Db.Statement stmt = cx.prepare(
+                    """
+                    SELECT COUNT(*) FROM MessageLocationTable
+                    WHERE folder_id = ?
+                      AND remove_marker <> ?
+                      AND message_id IN (SELECT rowid FROM MessageSearchTable WHERE MessageSearchTable MATCH 
'{flags} : UNREAD')
+                    """
+                );
+                stmt.bind_rowid(0, folder_id);
+                stmt.bind_bool(1, true);
+                Db.Result results = stmt.exec(cancellable);
+                if (!results.finished) {
+                    count = results.int_at(0);
+                }
+                return SUCCESS;
+            },
+            cancellable
+        );
 
         return count;
     }
@@ -1389,21 +1432,6 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
     // These should only be called from within a TransactionMethod.
     //
 
-    private int do_get_email_count(Db.Connection cx, ListFlags flags, Cancellable? cancellable)
-        throws Error {
-        Db.Statement stmt = cx.prepare(
-            "SELECT COUNT(*) FROM MessageLocationTable WHERE folder_id=?");
-        stmt.bind_rowid(0, folder_id);
-
-        Db.Result results = stmt.exec(cancellable);
-        if (results.finished)
-            return 0;
-
-        int marked = !flags.include_marked_for_remove() ? do_get_marked_removed_count(cx, cancellable) : 0;
-
-        return Numeric.int_floor(results.int_at(0) - marked, 0);
-    }
-
     private int do_get_marked_removed_count(Db.Connection cx, Cancellable? cancellable) throws Error {
         Db.Statement stmt = cx.prepare(
             "SELECT COUNT(*) FROM MessageLocationTable WHERE folder_id=? AND remove_marker <> ?");
diff --git a/src/engine/imap-engine/imap-engine-account-synchronizer.vala 
b/src/engine/imap-engine/imap-engine-account-synchronizer.vala
index d0861546e..8ed96fd32 100644
--- a/src/engine/imap-engine/imap-engine-account-synchronizer.vala
+++ b/src/engine/imap-engine/imap-engine-account-synchronizer.vala
@@ -83,18 +83,16 @@ private class Geary.ImapEngine.AccountSynchronizer :
 
         foreach (Folder folder in folders) {
             // Only sync folders that:
-            // 1. Can actually be opened (i.e. are selectable)
-            // 2. Are remote backed
+            // 1. Are remote backed
+            // 2. Can actually be opened (i.e. are selectable)
             //
-            // All this implies the folder must be a MinimalFolder and
+            // This implies the folder must be a MinimalFolder and
             // we do require that for syncing at the moment anyway,
             // but keep the tests in for that one glorious day where
             // we can just use a generic folder.
-            MinimalFolder? imap_folder = folder as MinimalFolder;
+            var imap_folder = folder as MinimalFolder;
             if (imap_folder != null &&
-                folder.properties.is_openable.is_possible() &&
-                !folder.properties.is_local_only &&
-                !folder.properties.is_virtual) {
+                imap_folder.remote_properties.is_openable.is_possible()) {
 
                 AccountOperation? op = null;
                 switch (reason) {
@@ -258,7 +256,8 @@ private class Geary.ImapEngine.FullFolderSync : RefreshFolderSync {
     protected override async void sync_folder(GLib.DateTime max_epoch,
                                               GLib.Cancellable cancellable)
         throws GLib.Error {
-        ImapDB.Folder local_folder = ((MinimalFolder) this.folder).local_folder;
+        var imap_folder = (MinimalFolder) this.folder;
+        var local_folder = imap_folder.local_folder;
 
         // Detach older emails outside the prefetch window
         if (this.account.information.prefetch_period_days >= 0) {
@@ -313,7 +312,7 @@ private class Geary.ImapEngine.FullFolderSync : RefreshFolderSync {
 
             debug("Fetching to: %s", next_epoch.to_string());
 
-            if (local_count < this.folder.properties.email_total &&
+            if (local_count < imap_folder.remote_properties.email_total &&
                 next_epoch.compare(max_epoch) >= 0) {
                 if (next_epoch.compare(this.sync_max_epoch) > 0) {
                     current_oldest = yield expand_vector(
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 80aed7181..13bbaec3f 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -93,7 +93,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
         this.imap = imap;
 
         smtp.outbox = new Outbox.Folder(this, local_folder_root, local.db);
-        smtp.report_problem.connect(notify_report_problem);
+        smtp.report_problem.connect(this.notify_report_problem);
         smtp.set_logging_parent(this);
         this.smtp = smtp;
 
@@ -1171,8 +1171,9 @@ internal class Geary.ImapEngine.StartServices : AccountOperation {
         throws GLib.Error {
         yield this.account.incoming.start(cancellable);
 
-        this.account.register_local_folder(this.outbox);
+        yield this.outbox.load(cancellable);
         yield this.account.outgoing.start(cancellable);
+        this.account.register_local_folder(this.outbox);
     }
 
 }
diff --git a/src/engine/imap-engine/imap-engine-minimal-folder.vala 
b/src/engine/imap-engine/imap-engine-minimal-folder.vala
index 61bc7a72a..67be17934 100644
--- a/src/engine/imap-engine/imap-engine-minimal-folder.vala
+++ b/src/engine/imap-engine/imap-engine-minimal-folder.vala
@@ -43,26 +43,33 @@ private class Geary.ImapEngine.MinimalFolder : BaseObject,
     private weak GenericAccount _account;
 
     /** {@inheritDoc} */
-    public FolderProperties properties {
-        get { return this._properties; }
+    public Folder.Path path {
+        get { return this.local_folder.path; }
     }
-    private FolderProperties _properties;
 
     /** {@inheritDoc} */
-    public Folder.Path path {
-        get {
-            return local_folder.path;
-        }
+    public int email_total {
+        get { return this._email_total; }
     }
+    private int _email_total = 0;
+
+    /** {@inheritDoc} */
+    public int email_unread {
+        get { return this._email_unread; }
+    }
+    private int _email_unread = 0;
 
     /** {@inheritDoc} */
     public Folder.SpecialUse used_as {
-        get {
-            return this._used_as;
-        }
+        get { return this._used_as; }
     }
     private Folder.SpecialUse _used_as;
 
+    /** {@inheritDoc} */
+    public override RemoteProperties remote_properties {
+        get { return this.local_folder.properties; }
+    }
+
     /** {@inheritDoc} */
     public bool is_monitoring {
         get { return this._is_monitoring; }
@@ -125,7 +132,6 @@ private class Geary.ImapEngine.MinimalFolder : BaseObject,
         this.local_folder.email_complete.connect(on_email_complete);
 
         this._used_as = use;
-        this._properties = local_folder.properties;
 
         this.replay_queue = new ReplayQueue(this);
         this.replay_queue.remotely_executed.connect(this.on_remote_status_check);
@@ -146,6 +152,7 @@ private class Geary.ImapEngine.MinimalFolder : BaseObject,
         this.closed_semaphore.blind_notify();
     }
 
+    /** {@inheritDoc} */
     public void set_used_as_custom(bool enabled)
         throws EngineError.UNSUPPORTED {
         if (enabled) {
@@ -270,6 +277,30 @@ private class Geary.ImapEngine.MinimalFolder : BaseObject,
         return this.remote_session;
     }
 
+    /**
+     * Updates the email total and unread counts for the folder.
+     */
+    internal async void update_email_counts(GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        int existing_total = this._email_total;
+        int new_total = yield this.local_folder.get_email_count_async(
+            NONE, cancellable
+        );
+        if (existing_total != new_total) {
+            this._email_total = new_total;
+            notify_property("email-total");
+        }
+
+        int existing_unread = this._email_unread;
+        int new_unread = yield this.local_folder.get_email_unread_async(
+            cancellable
+        );
+        if (existing_unread != new_unread) {
+            this._email_unread = new_unread;
+            notify_property("email-unread");
+        }
+    }
+
     /**
      * Sets the special use for this folder.
      *
@@ -679,12 +710,11 @@ private class Geary.ImapEngine.MinimalFolder : BaseObject,
             return;
         }
 
-
         /*
          * Step 5: Notify subscribers of what has happened.
          */
 
-        Folder.CountChangeReason count_change_reason = Folder.CountChangeReason.NONE;
+        yield update_email_counts(cancellable);
 
         if (removed_ids != null && removed_ids.size > 0) {
             // there may be operations pending on the remote queue for these removed emails; notify
@@ -695,8 +725,6 @@ private class Geary.ImapEngine.MinimalFolder : BaseObject,
             debug("Notifying of %d removed emails since last opened",
                   removed_ids.size);
             email_removed(removed_ids);
-
-            count_change_reason |= Folder.CountChangeReason.REMOVED;
         }
 
         // notify created (new email located somewhere inside the
@@ -713,7 +741,6 @@ private class Geary.ImapEngine.MinimalFolder : BaseObject,
             debug("Notifying of %d inserted emails since last opened",
                   inserted_ids.size);
             email_inserted(inserted_ids);
-            count_change_reason |= Folder.CountChangeReason.INSERTED;
         }
 
         // notify appended (new email added since the folder was last opened)
@@ -721,13 +748,6 @@ private class Geary.ImapEngine.MinimalFolder : BaseObject,
             debug("Notifying of %d appended emails since last opened",
                   appended_ids.size);
             email_appended(appended_ids);
-            count_change_reason |= Folder.CountChangeReason.APPENDED;
-        }
-
-        if (count_change_reason != Folder.CountChangeReason.NONE) {
-            debug("Notifying of %Xh count change reason (%d remote messages)",
-                  count_change_reason, remote_message_count);
-            email_count_changed(remote_message_count, count_change_reason);
         }
 
         debug("Completed normalize_folder");
@@ -739,12 +759,12 @@ private class Geary.ImapEngine.MinimalFolder : BaseObject,
             Geary.Email.Field.NONE, ImapDB.Folder.ListFlags.NONE, cancellable);
 
         yield local_folder.detach_all_emails_async(cancellable);
+        yield update_email_counts(cancellable);
 
         if (all != null && all.size > 0) {
             Gee.List<EmailIdentifier> ids =
                 traverse<Email>(all).map<EmailIdentifier>((email) => email.id).to_array_list();
             email_removed(ids);
-            email_count_changed(0, Folder.CountChangeReason.REMOVED);
         }
     }
 
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-empty-folder.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-empty-folder.vala
index 11b0bc8ed..da2c0a634 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-empty-folder.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-empty-folder.vala
@@ -13,7 +13,6 @@ private class Geary.ImapEngine.EmptyFolder : Geary.ImapEngine.SendReplayOperatio
     private MinimalFolder engine;
     private Cancellable? cancellable;
     private Gee.Set<ImapDB.EmailIdentifier>? removed_ids = null;
-    private int original_count = 0;
 
     public EmptyFolder(MinimalFolder engine, Cancellable? cancellable) {
         base("EmptyFolder", OnError.RETRY);
@@ -23,25 +22,12 @@ private class Geary.ImapEngine.EmptyFolder : Geary.ImapEngine.SendReplayOperatio
     }
 
     public override async ReplayOperation.Status replay_local_async() throws Error {
-        this.original_count = this.engine.properties.email_total;
-        // because this value is only used for reporting count changes, offer best-possible service
-        if (this.original_count < 0)
-            this.original_count = 0;
-
         // mark everything in the folder as removed
         removed_ids = yield engine.local_folder.mark_removed_async(null, true, cancellable);
-
-        // if local folder is not empty, report all as being removed
-        if (removed_ids != null) {
-            if (removed_ids.size > 0)
-                engine.email_removed(removed_ids);
-
-            int new_count = Numeric.int_floor(original_count - removed_ids.size, 0);
-            if (new_count != original_count) {
-                engine.email_count_changed(new_count, REMOVED);
-            }
+        if (removed_ids != null && !removed_ids.is_empty) {
+            yield this.engine.update_email_counts(cancellable);
+            engine.email_removed(removed_ids);
         }
-
         return ReplayOperation.Status.CONTINUE;
     }
 
@@ -61,14 +47,13 @@ private class Geary.ImapEngine.EmptyFolder : Geary.ImapEngine.SendReplayOperatio
     public override async void backout_local_async() throws Error {
         if (removed_ids != null && removed_ids.size > 0) {
             yield engine.local_folder.mark_removed_async(removed_ids, false, cancellable);
+            yield this.engine.update_email_counts(cancellable);
             engine.email_inserted(removed_ids);
         }
-
-        engine.email_count_changed(original_count, INSERTED);
     }
 
     public override string describe_state() {
         return "removed_ids.size=%d".printf((removed_ids != null) ? removed_ids.size : 0);
     }
-}
 
+}
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
index 2db597dfc..e94512ed9 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email-commit.vala
@@ -92,17 +92,13 @@ private class Geary.ImapEngine.MoveEmailCommit : Geary.ImapEngine.SendReplayOper
     }
 
     public override async void backout_local_async() throws Error {
-        if (to_move.size == 0)
-            return;
-
-        yield engine.local_folder.mark_removed_async(to_move, false, cancellable);
-
-        int count = this.engine.properties.email_total;
-        if (count < 0) {
-            count = 0;
+        if (!this.to_move.is_empty) {
+            yield this.engine.local_folder.mark_removed_async(
+                this.to_move, false, this.cancellable
+            );
+            yield this.engine.update_email_counts(this.cancellable);
+            this.engine.email_inserted(this.to_move);
         }
-        engine.email_inserted(to_move);
-        engine.email_count_changed(count + to_move.size, INSERTED);
     }
 
     public override string describe_state() {
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email-prepare.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-move-email-prepare.vala
index 71930606d..a1a00c749 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-move-email-prepare.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email-prepare.vala
@@ -38,21 +38,11 @@ private class Geary.ImapEngine.MoveEmailPrepare : Geary.ImapEngine.SendReplayOpe
         if (to_move.size <= 0)
             return ReplayOperation.Status.COMPLETED;
 
-        int count = this.engine.properties.email_total;
-        // as this value is only used for reporting, offer best-possible service
-        if (count < 0)
-            count = to_move.size;
-
         prepared_for_move = yield engine.local_folder.mark_removed_async(to_move, true, cancellable);
-        if (prepared_for_move == null || prepared_for_move.size == 0)
-            return ReplayOperation.Status.COMPLETED;
-
-        engine.email_removed(prepared_for_move);
-        engine.email_count_changed(
-            Numeric.int_floor(count - prepared_for_move.size, 0),
-            REMOVED
-        );
-
+        if (prepared_for_move != null && !prepared_for_move.is_empty) {
+            yield this.engine.update_email_counts(this.cancellable);
+            this.engine.email_removed(prepared_for_move);
+        }
         return COMPLETED;
     }
 
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-move-email-revoke.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-move-email-revoke.vala
index df66828e7..73a89420b 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-move-email-revoke.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-move-email-revoke.vala
@@ -33,17 +33,10 @@ private class Geary.ImapEngine.MoveEmailRevoke : Geary.ImapEngine.SendReplayOper
 
         Gee.Set<ImapDB.EmailIdentifier>? revoked = yield engine.local_folder.mark_removed_async(
             to_revoke, false, cancellable);
-        if (revoked == null || revoked.size == 0)
-            return ReplayOperation.Status.COMPLETED;
-
-        int count = this.engine.properties.email_total;
-        if (count < 0) {
-            count = 0;
+        if (revoked != null && !revoked.is_empty) {
+            yield this.engine.update_email_counts(this.cancellable);
+            this.engine.email_inserted(revoked);
         }
-
-        engine.email_inserted(revoked);
-        engine.email_count_changed(count + revoked.size, INSERTED);
-
         return COMPLETED;
     }
 
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
index d20c76b48..26abbade4 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-remove-email.vala
@@ -5,11 +5,13 @@
  */
 
 private class Geary.ImapEngine.RemoveEmail : Geary.ImapEngine.SendReplayOperation {
+
+
     private MinimalFolder engine;
     private Gee.List<ImapDB.EmailIdentifier> to_remove = new Gee.ArrayList<ImapDB.EmailIdentifier>();
     private Cancellable? cancellable;
     private Gee.Set<ImapDB.EmailIdentifier>? removed_ids = null;
-    private int original_count = 0;
+
 
     public RemoveEmail(MinimalFolder engine,
                        Gee.Collection<ImapDB.EmailIdentifier> to_remove,
@@ -32,22 +34,11 @@ private class Geary.ImapEngine.RemoveEmail : Geary.ImapEngine.SendReplayOperatio
         if (this.to_remove.size <= 0)
             return ReplayOperation.Status.COMPLETED;
 
-        this.original_count = this.engine.properties.email_total;
-        // because this value is only used for reporting count changes, offer best-possible service
-        if (this.original_count < 0)
-            this.original_count = this.to_remove.size;
-
         removed_ids = yield engine.local_folder.mark_removed_async(to_remove, true, cancellable);
-        if (removed_ids == null || removed_ids.size == 0)
-            return ReplayOperation.Status.COMPLETED;
-
-        engine.email_removed(removed_ids);
-
-        engine.email_count_changed(
-            Numeric.int_floor(original_count - removed_ids.size, 0),
-            REMOVED
-        );
-
+        if (removed_ids != null && !removed_ids.is_empty) {
+            yield this.engine.update_email_counts(this.cancellable);
+            this.engine.email_removed(removed_ids);
+        }
         return CONTINUE;
     }
 
@@ -70,11 +61,13 @@ private class Geary.ImapEngine.RemoveEmail : Geary.ImapEngine.SendReplayOperatio
     }
 
     public override async void backout_local_async() throws Error {
-        if (removed_ids != null && removed_ids.size > 0) {
-            yield engine.local_folder.mark_removed_async(removed_ids, false, cancellable);
-            engine.email_inserted(removed_ids);
+        if (this.removed_ids != null && !this.removed_ids.is_empty) {
+            yield this.engine.local_folder.mark_removed_async(
+                this.removed_ids, false, this.cancellable
+            );
+            yield this.engine.update_email_counts(this.cancellable);
+            this.engine.email_inserted(this.removed_ids);
         }
-        engine.email_count_changed(original_count, INSERTED);
     }
 
     public override string describe_state() {
@@ -82,4 +75,3 @@ private class Geary.ImapEngine.RemoveEmail : Geary.ImapEngine.SendReplayOperatio
             (removed_ids != null) ? removed_ids.size : 0);
     }
 }
-
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-replay-append.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-replay-append.vala
index f79c91056..0d3676a96 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-replay-append.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-replay-append.vala
@@ -112,6 +112,8 @@ private class Geary.ImapEngine.ReplayAppend : Geary.ImapEngine.ReplayOperation {
             }
         }
 
+        yield this.owner.update_email_counts(this.cancellable);
+
         // store the reported count, *not* the current count (which is updated outside the of
         // the queue) to ensure that updates happen serially and reflect committed local changes
         yield this.owner.local_folder.update_remote_selected_message_count(
@@ -127,8 +129,6 @@ private class Geary.ImapEngine.ReplayAppend : Geary.ImapEngine.ReplayOperation {
             this.owner.email_appended(created);
         }
 
-        this.owner.email_count_changed(this.remote_count, Folder.CountChangeReason.APPENDED);
-
         debug("%s do_replay_appended_message: completed, this.remote_count=%d",
               to_string(), this.remote_count);
     }
diff --git a/src/engine/imap-engine/replay-ops/imap-engine-replay-removal.vala 
b/src/engine/imap-engine/replay-ops/imap-engine-replay-removal.vala
index 3f5efca6d..c15fc4202 100644
--- a/src/engine/imap-engine/replay-ops/imap-engine-replay-removal.vala
+++ b/src/engine/imap-engine/replay-ops/imap-engine-replay-removal.vala
@@ -105,13 +105,10 @@ private class Geary.ImapEngine.ReplayRemoval : Geary.ImapEngine.ReplayOperation
                 to_string(), this.position.value, this.remote_count, local_position, local_count);
         }
 
-        // for debugging
-        int new_local_count = -1;
         try {
-            new_local_count = yield this.owner.local_folder.get_email_count_async(
-                ImapDB.Folder.ListFlags.INCLUDE_MARKED_FOR_REMOVE, null);
+            yield this.owner.update_email_counts(null);
         } catch (Error err) {
-            debug("%s do_replay_removed_message: error fetching new local count: %s", to_string(),
+            debug("%s do_replay_removed_message: unable to update remote count: %s", to_string(),
                 err.message);
         }
 
@@ -134,16 +131,10 @@ private class Geary.ImapEngine.ReplayRemoval : Geary.ImapEngine.ReplayOperation
                 this.owner.marked_email_removed(removed);
         }
 
-        if (!marked) {
-            this.owner.email_count_changed(
-                this.remote_count, Folder.CountChangeReason.REMOVED
-            );
-        }
-
         debug("%s ReplayRemoval: completed, "
             + "(this.remote_count=%d local_count=%d starting local_count=%d this.position=%lld 
local_position=%lld marked=%s)",
               this.owner.to_string(),
-              this.remote_count, new_local_count, local_count,
+              this.remote_count, this.owner.email_total, local_count,
               this.position.value, local_position, marked.to_string());
     }
 
diff --git a/src/engine/imap/api/imap-folder-properties.vala b/src/engine/imap/api/imap-folder-properties.vala
index 54b21ce3a..6d83eaf9e 100644
--- a/src/engine/imap/api/imap-folder-properties.vala
+++ b/src/engine/imap/api/imap-folder-properties.vala
@@ -38,20 +38,42 @@
  * is considered more authoritative than STATUS.
  */
 
-public class Geary.Imap.FolderProperties : Geary.FolderProperties {
+public class Geary.Imap.FolderProperties : BaseObject, RemoteFolder.RemoteProperties {
+
+    /** {@inheritDoc} */
+    public int email_total { get; protected set; }
+
+    /** {@inheritDoc} */
+    public int email_unread { get; protected set; }
+
+    /** {@inheritDoc} */
+    public Trillian has_children { get; protected set; }
+
+    /** {@inheritDoc} */
+    public Trillian supports_children { get; protected set; }
+
+    /** {@inheritDoc} */
+    public Trillian is_openable { get; protected set; }
+
+    /** {@inheritDoc} */
+    public bool create_never_returns_id { get; protected set; }
+
     /**
      * -1 if the Folder was not opened via SELECT or EXAMINE.  Updated as EXISTS server data
      * arrives.
      */
     public int select_examine_messages { get; private set; }
+
     /**
      * -1 if the FolderProperties were not obtained or updated via a STATUS command
      */
     public int status_messages { get; private set; }
+
     /**
      * -1 if the FolderProperties were not obtained or updated via a STATUS command
      */
     public int unseen { get; private set; }
+
     public int recent { get; internal set; }
     public UIDValidity? uid_validity { get; internal set; }
     public UID? uid_next { get; internal set; }
@@ -134,12 +156,12 @@ public class Geary.Imap.FolderProperties : Geary.FolderProperties {
 
         Trillian is_openable = Trillian.from_boolean(!attrs.is_no_select);
 
-        base(email_total, email_unread,
-             has_children, supports_children, is_openable,
-             false, // not local
-             false, // not virtual
-             !supports_uid);
-
+        this.email_total = email_total;
+        this.email_unread = email_unread;
+        this.has_children = has_children;
+        this.supports_children = supports_children;
+        this.is_openable = is_openable;
+        this.create_never_returns_id = create_never_returns_id;
         this.attrs = attrs;
     }
 
@@ -197,8 +219,9 @@ public class Geary.Imap.FolderProperties : Geary.FolderProperties {
     /**
      * Update an existing {@link FolderProperties} with fresh {@link StatusData}.
      *
-     * This will force the {@link Geary.FolderProperties.email_total} property to match the
-     * {@link status_messages} value.
+     * This will force the {@link
+     * Geary.RemoteFolder.RemoteProperties.email_total} property to
+     * match the {@link status_messages} value.
      */
     public void update_status(StatusData status) {
         set_status_message_count(status.messages, true);
diff --git a/src/engine/meson.build b/src/engine/meson.build
index 77f6ec9d3..c7dc61ea8 100644
--- a/src/engine/meson.build
+++ b/src/engine/meson.build
@@ -20,7 +20,6 @@ engine_vala_sources = files(
   'api/geary-engine-error.vala',
   'api/geary-engine.vala',
   'api/geary-folder.vala',
-  'api/geary-folder-properties.vala',
   'api/geary-folder-supports-archive.vala',
   'api/geary-folder-supports-copy.vala',
   'api/geary-folder-supports-create.vala',
@@ -254,7 +253,6 @@ engine_vala_sources = files(
   'outbox/outbox-email-identifier.vala',
   'outbox/outbox-email-properties.vala',
   'outbox/outbox-folder.vala',
-  'outbox/outbox-folder-properties.vala',
 
   'rfc822/rfc822.vala',
   'rfc822/rfc822-error.vala',
diff --git a/src/engine/outbox/outbox-folder.vala b/src/engine/outbox/outbox-folder.vala
index ce3348522..78a90c720 100644
--- a/src/engine/outbox/outbox-folder.vala
+++ b/src/engine/outbox/outbox-folder.vala
@@ -44,11 +44,8 @@ public class Geary.Outbox.Folder : BaseObject,
 
 
     /** {@inheritDoc} */
-    public override Account account { get { return this._account; } }
-
-    /** {@inheritDoc} */
-    public override Geary.FolderProperties properties {
-        get { return _properties; }
+    public override Account account {
+        get { return this._account; }
     }
 
     /**
@@ -58,12 +55,21 @@ public class Geary.Outbox.Folder : BaseObject,
      * with the name given by {@link MAGIC_BASENAME}.
      */
     public override Geary.Folder.Path path {
-        get {
-            return _path;
-        }
+        get { return this._path; }
     }
     private Geary.Folder.Path _path;
 
+    /** {@inheritDoc} */
+    public int email_total {
+        get { return this._email_total; }
+    }
+    private int _email_total = 0;
+
+    /** {@inheritDoc} */
+    public int email_unread {
+        get { return 0; }
+    }
+
     /**
      * Returns the type of this folder.
      *
@@ -82,11 +88,12 @@ public class Geary.Outbox.Folder : BaseObject,
 
     private weak Account _account;
     private Db.Database db = null;
-    private FolderProperties _properties = new FolderProperties(0, 0);
     private int64 next_ordering = 0;
 
 
-    internal Folder(Account account, Geary.Folder.Root root, Db.Database db) {
+    internal Folder(Account account,
+                    Geary.Folder.Root root,
+                    Db.Database db) {
         this._account = account;
         this._path = root.get_child(MAGIC_BASENAME, Trillian.TRUE);
         this.db = db;
@@ -98,7 +105,6 @@ public class Geary.Outbox.Folder : BaseObject,
                            GLib.DateTime? date_received,
                            GLib.Cancellable? cancellable = null)
         throws GLib.Error {
-        int email_count = 0;
         OutboxRow? row = null;
         yield db.exec_transaction_async(Db.TransactionType.WR, (cx) => {
             int64 ordering = do_get_next_ordering(cx, cancellable);
@@ -113,20 +119,16 @@ public class Geary.Outbox.Folder : BaseObject,
             int position = do_get_position_by_ordering(cx, ordering, cancellable);
 
             row = new OutboxRow(new_id, position, ordering, false, null);
-            email_count = do_get_email_count(cx, cancellable);
 
             return Db.TransactionOutcome.COMMIT;
         }, cancellable);
 
-        // update properties
-        _properties.set_total(yield get_email_count_async(cancellable));
-
-        Gee.List<EmailIdentifier> list = new Gee.ArrayList<EmailIdentifier>();
-        list.add(row.outbox_id);
+        this._email_total++;
+        notify_property("email-total");
 
+        var list = Geary.Collection.single(row.outbox_id);
         this.account.email_added(list, this);
         email_inserted(list);
-        email_count_changed(email_count, CountChangeReason.APPENDED);
 
         return row.outbox_id;
     }
@@ -159,8 +161,7 @@ public class Geary.Outbox.Folder : BaseObject,
         remove_email_async(Gee.Collection<Geary.EmailIdentifier> email_ids,
                            GLib.Cancellable? cancellable = null)
         throws GLib.Error {
-        Gee.List<Geary.EmailIdentifier> removed = new Gee.ArrayList<Geary.EmailIdentifier>();
-        int final_count = 0;
+        var removed = new Gee.ArrayList<Geary.EmailIdentifier>();
         yield db.exec_transaction_async(Db.TransactionType.WR, (cx) => {
             foreach (Geary.EmailIdentifier id in email_ids) {
                 // ignore anything not belonging to the outbox, but also don't report it as removed
@@ -174,20 +175,17 @@ public class Geary.Outbox.Folder : BaseObject,
                 // never reuse an ordering value while Geary is running.
                 do_get_next_ordering(cx, cancellable);
 
-                if (do_remove_email(cx, outbox_id, cancellable))
+                if (do_remove_email(cx, outbox_id, cancellable)) {
                     removed.add(outbox_id);
+                }
             }
-
-            final_count = do_get_email_count(cx, cancellable);
-
             return Db.TransactionOutcome.COMMIT;
         }, cancellable);
 
-        if (removed.size >= 0) {
-            _properties.set_total(final_count);
-
+        if (!removed.is_empty) {
+            this._email_total -= removed.size;
+            notify_property("email-total");
             email_removed(removed);
-            email_count_changed(final_count, REMOVED);
         }
     }
 
@@ -363,6 +361,21 @@ public class Geary.Outbox.Folder : BaseObject,
         return new Logging.State(this, this.path.to_string());
     }
 
+    /** Initialises outbox state from the database */
+    internal async void load(GLib.Cancellable cancellable) throws GLib.Error {
+        yield this.db.exec_transaction_async(
+            RO, (cx) => {
+                Db.Statement stmt = cx.prepare("SELECT COUNT(*) FROM SmtpOutboxTable");
+                Db.Result results = stmt.exec(cancellable);
+                if (!results.finished) {
+                    this._email_total = results.int_at(0);
+                }
+                return DONE;
+            },
+            cancellable
+        );
+    }
+
     // Utility for getting an email object back from an outbox row.
     private Geary.Email row_to_email(OutboxRow row) throws Error {
         Geary.Email? email = null;
@@ -388,17 +401,6 @@ public class Geary.Outbox.Folder : BaseObject,
         return email;
     }
 
-    private async int get_email_count_async(Cancellable? cancellable) throws Error {
-        int count = 0;
-        yield db.exec_transaction_async(Db.TransactionType.RO, (cx) => {
-            count = do_get_email_count(cx, cancellable);
-
-            return Db.TransactionOutcome.DONE;
-        }, cancellable);
-
-        return count;
-    }
-
     //
     // Transaction helper methods
     //
@@ -419,14 +421,6 @@ public class Geary.Outbox.Folder : BaseObject,
         }
     }
 
-    private int do_get_email_count(Db.Connection cx, Cancellable? cancellable) throws Error {
-        Db.Statement stmt = cx.prepare("SELECT COUNT(*) FROM SmtpOutboxTable");
-
-        Db.Result results = stmt.exec(cancellable);
-
-        return (!results.finished) ? results.int_at(0) : 0;
-    }
-
     private int do_get_position_by_ordering(Db.Connection cx, int64 ordering, Cancellable? cancellable)
         throws Error {
         Db.Statement stmt = cx.prepare(
diff --git a/test/engine/app/app-conversation-monitor-test.vala 
b/test/engine/app/app-conversation-monitor-test.vala
index bd8c8911d..36be8abc5 100644
--- a/test/engine/app/app-conversation-monitor-test.vala
+++ b/test/engine/app/app-conversation-monitor-test.vala
@@ -59,7 +59,6 @@ class Geary.App.ConversationMonitorTest : TestCase {
         );
         this.other_folder = new Mock.Folder(
             this.account,
-            null,
             this.folder_root.get_child("other"),
             NONE,
             null
@@ -167,7 +166,6 @@ class Geary.App.ConversationMonitorTest : TestCase {
         throws GLib.Error {
         var test_article = new Mock.Folder(
             this.account,
-            null,
             this.folder_root.get_child("base"),
             NONE,
             null
diff --git a/test/engine/app/app-conversation-set-test.vala b/test/engine/app/app-conversation-set-test.vala
index ef84c81d3..a00daf2aa 100644
--- a/test/engine/app/app-conversation-set-test.vala
+++ b/test/engine/app/app-conversation-set-test.vala
@@ -30,7 +30,6 @@ class Geary.App.ConversationSetTest : TestCase {
     public override void set_up() {
         this.folder_root = new Folder.Root("#test", false);
         this.base_folder = new Mock.Folder(
-            null,
             null,
             this.folder_root.get_child("test"),
             NONE,
diff --git a/test/engine/app/app-conversation-test.vala b/test/engine/app/app-conversation-test.vala
index 194f11c65..a1fff997b 100644
--- a/test/engine/app/app-conversation-test.vala
+++ b/test/engine/app/app-conversation-test.vala
@@ -30,7 +30,6 @@ class Geary.App.ConversationTest : TestCase {
     public override void set_up() {
         this.folder_root = new Folder.Root("#test", false);
         this.base_folder = new Mock.Folder(
-            null,
             null,
             this.folder_root.get_child("test"),
             NONE,
diff --git a/test/meson.build b/test/meson.build
index de8adf0b9..973727996 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -13,7 +13,6 @@ libmock_sources = [
   'mock/mock-email-identifier.vala',
   'mock/mock-email-properties.vala',
   'mock/mock-folder.vala',
-  'mock/mock-folder-properties.vala',
   'mock/mock-remote-folder.vala',
   'mock/mock-search-query.vala',
 ]
diff --git a/test/mock/mock-account.vala b/test/mock/mock-account.vala
index 192508922..e1f9d72a9 100644
--- a/test/mock/mock-account.vala
+++ b/test/mock/mock-account.vala
@@ -84,7 +84,7 @@ public class Mock.Account : Geary.Account,
         return object_call<Folder>(
             "create_personal_folder",
             { box_arg(name), box_arg(use), cancellable },
-            new Folder(null, null, null, use, null)
+            new Folder(null, null, use, null)
         );
     }
 
@@ -141,7 +141,7 @@ public class Mock.Account : Geary.Account,
         } catch (Geary.EngineError.NOT_FOUND err) {
             throw err;
         } catch (GLib.Error err) {
-            return new Folder(null, null, null, NONE, null);
+            return new Folder(null, null, NONE, null);
         }
     }
 
diff --git a/test/mock/mock-folder.vala b/test/mock/mock-folder.vala
index ee274b24f..43737e1b5 100644
--- a/test/mock/mock-folder.vala
+++ b/test/mock/mock-folder.vala
@@ -16,14 +16,18 @@ public class Mock.Folder : GLib.Object,
         get { return this._account; }
     }
 
-    public override Geary.FolderProperties properties {
-        get { return this._properties; }
-    }
-
     public override Geary.Folder.Path path {
         get { return this._path; }
     }
 
+    public override int email_total {
+        get { return this._email_total; }
+    }
+
+    public override int email_unread {
+        get { return this._email_unread; }
+    }
+
     public override Geary.Folder.SpecialUse used_as {
         get { return this._used_as; }
     }
@@ -38,19 +42,18 @@ public class Mock.Folder : GLib.Object,
 
 
     private Geary.Account _account;
-    private Geary.FolderProperties _properties;
     private Geary.Folder.Path _path;
+    private int _email_total = 0;
+    private int _email_unread = 0;
     private Geary.Folder.SpecialUse _used_as;
     private Geary.ProgressMonitor _opening_monitor;
 
 
     public Folder(Geary.Account? account,
-                  Geary.FolderProperties? properties,
                   Geary.Folder.Path? path,
                   Geary.Folder.SpecialUse used_as,
                   Geary.ProgressMonitor? monitor) {
         this._account = account;
-        this._properties = properties ?? new FolderPoperties();
         this._path = path;
         this._used_as = used_as;
         this._opening_monitor = monitor;
diff --git a/test/mock/mock-remote-folder.vala b/test/mock/mock-remote-folder.vala
index 8b9b1fc92..9255eccf6 100644
--- a/test/mock/mock-remote-folder.vala
+++ b/test/mock/mock-remote-folder.vala
@@ -13,18 +13,52 @@ public class Mock.RemoteFolder : GLib.Object,
     ValaUnit.MockObject {
 
 
+    public class RemoteProperties : GLib.Object,
+        Geary.RemoteFolder.RemoteProperties {
+
+
+        public int email_total { get; protected set; default = 0; }
+
+        public int email_unread { get; protected set; default = 0; }
+
+        public Geary.Trillian has_children {
+            get; protected set; default = Geary.Trillian.UNKNOWN;
+        }
+
+        public Geary.Trillian supports_children {
+            get; protected set; default = Geary.Trillian.UNKNOWN;
+        }
+
+        public Geary.Trillian is_openable {
+            get; protected set; default = Geary.Trillian.UNKNOWN;
+        }
+
+        public bool create_never_returns_id {
+            get; protected set; default = false;
+        }
+
+    }
+
     public Geary.Account account {
         get { return this._account; }
     }
 
-    public Geary.FolderProperties properties {
-        get { return this._properties; }
+    public Geary.RemoteFolder.RemoteProperties remote_properties {
+        get { return this._remote_properties; }
     }
 
     public Geary.Folder.Path path {
         get { return this._path; }
     }
 
+    public override int email_total {
+        get { return this._email_total; }
+    }
+
+    public override int email_unread {
+        get { return this._email_unread; }
+    }
+
     public Geary.Folder.SpecialUse used_as {
         get { return this._used_as; }
     }
@@ -49,21 +83,23 @@ public class Mock.RemoteFolder : GLib.Object,
 
 
     private Geary.Account _account;
-    private Geary.FolderProperties _properties;
+    private Geary.RemoteFolder.RemoteProperties _remote_properties;
     private Geary.Folder.Path _path;
+    private int _email_total = 0;
+    private int _email_unread = 0;
     private Geary.Folder.SpecialUse _used_as;
     private Geary.ProgressMonitor _opening_monitor;
 
 
     public RemoteFolder(Geary.Account? account,
-                        Geary.FolderProperties? properties,
+                        Geary.RemoteFolder.RemoteProperties? remote_properties,
                         Geary.Folder.Path? path,
                         Geary.Folder.SpecialUse used_as,
                         Geary.ProgressMonitor? monitor,
                         bool is_monitoring,
                         bool is_fully_expanded) {
         this._account = account;
-        this._properties = properties ?? new FolderPoperties();
+        this._remote_properties = remote_properties ?? new RemoteProperties();
         this._path = path;
         this._used_as = used_as;
         this._opening_monitor = monitor;


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