[geary/mjog/user-plugins: 18/26] Convert plugins to use isolated context model



commit ee4bd117ee1152f594861471d8157a64a4baae14
Author: Michael Gratton <mike vee net>
Date:   Tue Mar 10 13:08:40 2020 +1100

    Convert plugins to use isolated context model
    
    Convert the plugin implementation to use a model where each plugin
    has its own context object instances and has limited/no access to the
    client's and engine's objects.

 po/POTFILES.in                                     |   1 +
 src/client/application/application-controller.vala | 137 +---
 .../application-folder-store-factory.vala          | 282 +++++++++
 .../application/application-main-window.vala       |  13 +-
 .../application-notification-context.vala          | 688 +++++++++++++++------
 .../application/application-plugin-manager.vala    |  54 +-
 src/client/meson.build                             |   1 +
 .../desktop-notifications.vala                     | 261 +++++---
 .../plugin/messaging-menu/messaging-menu.vala      | 105 ++--
 .../notification-badge/notification-badge.vala     |  64 +-
 src/client/plugin/plugin-notification.vala         |   9 +-
 11 files changed, 1130 insertions(+), 485 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 1a1db9c1..7f3178e5 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -23,6 +23,7 @@ src/client/application/application-configuration.vala
 src/client/application/application-contact-store.vala
 src/client/application/application-contact.vala
 src/client/application/application-controller.vala
+src/client/application/application-folder-store-factory.vala
 src/client/application/application-main-window.vala
 src/client/application/application-notification-context.vala
 src/client/application/application-plugin-manager.vala
diff --git a/src/client/application/application-controller.vala 
b/src/client/application/application-controller.vala
index 47143389..75ccb0cf 100644
--- a/src/client/application/application-controller.vala
+++ b/src/client/application/application-controller.vala
@@ -79,8 +79,6 @@ internal class Application.Controller : Geary.BaseObject {
     private Gee.Map<Geary.AccountInformation,AccountContext> accounts =
         new Gee.HashMap<Geary.AccountInformation,AccountContext>();
 
-    private NotificationContext notifications;
-
     // Cancelled if the controller is closed
     private GLib.Cancellable controller_open;
 
@@ -165,13 +163,7 @@ internal class Application.Controller : Geary.BaseObject {
 
         }
 
-        this.notifications = new NotificationContext(
-            this.avatars,
-            this.get_contact_store_for_account,
-            this.should_notify_new_messages
-        );
-
-        this.plugins = new PluginManager(this.application, this.notifications);
+        this.plugins = new PluginManager(this.application);
 
         // Migrate configuration if necessary.
         Migrate.xdg_config_dir(this.application.get_user_data_directory(),
@@ -270,7 +262,7 @@ internal class Application.Controller : Geary.BaseObject {
         try {
             yield composer_barrier.wait_async();
         } catch (GLib.Error err) {
-            debug("Error waiting at composer barrier: %s", err.message);
+            warning("Error waiting at composer barrier: %s", err.message);
         }
 
         // Now that all composers are closed, we can shut down the
@@ -298,11 +290,10 @@ internal class Application.Controller : Geary.BaseObject {
         try {
             yield window_barrier.wait_async();
         } catch (GLib.Error err) {
-            debug("Error waiting at window barrier: %s", err.message);
+            warning("Error waiting at window barrier: %s", err.message);
         }
 
         // Release general resources now there's no more UI
-        this.notifications.clear_folders();
         try {
             this.plugins.close();
         } catch (GLib.Error err) {
@@ -331,10 +322,10 @@ internal class Application.Controller : Geary.BaseObject {
         try {
             yield account_barrier.wait_async();
         } catch (GLib.Error err) {
-            debug("Error waiting at account barrier: %s", err.message);
+            warning("Error waiting at account barrier: %s", err.message);
         }
 
-        debug("Closed Application.Controller");
+        info("Closed Application.Controller");
     }
 
     /**
@@ -1254,33 +1245,6 @@ internal class Application.Controller : Geary.BaseObject {
         return retry;
     }
 
-    private bool is_inbox_descendant(Geary.Folder target) {
-        bool is_descendent = false;
-
-        Geary.Account account = target.account;
-        Geary.Folder? inbox = account.get_special_folder(Geary.SpecialFolderType.INBOX);
-
-        if (inbox != null) {
-            is_descendent = inbox.path.is_descendant(target.path);
-        }
-        return is_descendent;
-    }
-
-    private void on_special_folder_type_changed(Geary.Folder folder,
-                                                Geary.SpecialFolderType old_type,
-                                                Geary.SpecialFolderType new_type) {
-        // Update notifications
-        this.notifications.remove_folder(folder);
-        if (folder.special_folder_type == Geary.SpecialFolderType.INBOX ||
-            (folder.special_folder_type == Geary.SpecialFolderType.NONE &&
-             is_inbox_descendant(folder))) {
-            Geary.AccountInformation info = folder.account.information;
-            this.notifications.add_folder(
-                folder, this.accounts.get(info).cancellable
-            );
-        }
-    }
-
     private void on_folders_available_unavailable(
         Geary.Account account,
         Gee.BidirSortedSet<Geary.Folder>? available,
@@ -1292,33 +1256,13 @@ internal class Application.Controller : Geary.BaseObject {
                 if (!Controller.should_add_folder(available, folder)) {
                     continue;
                 }
-                folder.special_folder_type_changed.connect(
-                    on_special_folder_type_changed
-                );
 
                 GLib.Cancellable cancellable = context.cancellable;
-                switch (folder.special_folder_type) {
-                case Geary.SpecialFolderType.INBOX:
+                if (folder.special_folder_type == INBOX) {
                     if (context.inbox == null) {
                         context.inbox = folder;
                     }
                     folder.open_async.begin(NO_DELAY, cancellable);
-
-                    // Always notify for new messages in the Inbox
-                    this.notifications.add_folder(
-                        folder, cancellable
-                    );
-                    break;
-
-                case Geary.SpecialFolderType.NONE:
-                    // Only notify for new messages in non-special
-                    // descendants of the Inbox
-                    if (is_inbox_descendant(folder)) {
-                        this.notifications.add_folder(
-                            folder, cancellable
-                        );
-                    }
-                    break;
                 }
             }
         }
@@ -1329,23 +1273,9 @@ internal class Application.Controller : Geary.BaseObject {
             bool has_prev = unavailable_iterator.last();
             while (has_prev) {
                 Geary.Folder folder = unavailable_iterator.get();
-                folder.special_folder_type_changed.disconnect(
-                    on_special_folder_type_changed
-                );
 
-                switch (folder.special_folder_type) {
-                case Geary.SpecialFolderType.INBOX:
+                if (folder.special_folder_type == INBOX) {
                     context.inbox = null;
-                    this.notifications.remove_folder(folder);
-                    break;
-
-                case Geary.SpecialFolderType.NONE:
-                    // Only notify for new messages in non-special
-                    // descendants of the Inbox
-                    if (is_inbox_descendant(folder)) {
-                        this.notifications.remove_folder(folder);
-                    }
-                    break;
                 }
 
                 has_prev = unavailable_iterator.previous();
@@ -1356,48 +1286,15 @@ internal class Application.Controller : Geary.BaseObject {
         }
     }
 
-    private bool should_notify_new_messages(Geary.Folder folder) {
-        // Don't show notifications if the top of the folder's
-        // conversations is visible. That is, if there is a main
-        // window, it's focused, the folder is selected, and the
-        // conversation list is at the top.
-        MainWindow? window = this.application.last_active_main_window;
-        return (
-            window == null ||
-            !window.has_toplevel_focus ||
-            window.selected_folder != folder ||
-            window.conversation_list_view.vadjustment.value > 0.0
-        );
-    }
-
-    // Clears messages if conditions are true: anything in should_notify_new_messages() is
-    // false and the supplied visible messages are visible in the conversation list view
-    public void clear_new_messages(string caller,
-                                   Gee.Set<Geary.App.Conversation>? supplied) {
-        MainWindow? window = this.application.last_active_main_window;
-        Geary.Folder? selected = (
-            (window != null) ? window.selected_folder : null
-        );
-        NotificationContext notifications = this.notifications;
-        if (selected != null && (
-                !notifications.get_folders().contains(selected) ||
-                should_notify_new_messages(selected))) {
-
-            Gee.Set<Geary.App.Conversation> visible =
-                supplied ?? window.conversation_list_view.get_visible_conversations();
-
-            foreach (Geary.App.Conversation conversation in visible) {
-                try {
-                    if (notifications.are_any_new_messages(selected,
-                                                           conversation.get_email_ids())) {
-                        debug("Clearing new messages: %s", caller);
-                        notifications.clear_new_messages(selected);
-                        break;
-                    }
-                } catch (Geary.EngineError.NOT_FOUND err) {
-                    // all good
-                }
-            }
+    /** Clears new message counts in notification plugin contexts. */
+    public void clear_new_messages(Geary.Folder source,
+                                   Gee.Set<Geary.App.Conversation> visible) {
+        foreach (MainWindow window in this.application.get_main_windows()) {
+            window.folder_list.set_has_new(source, false);
+        }
+        foreach (NotificationContext context in
+                 this.plugins.get_notification_contexts()) {
+            context.clear_new_messages(source, visible);
         }
     }
 
@@ -1575,7 +1472,7 @@ internal class Application.Controller : Geary.BaseObject {
 
         AccountContext? context = this.accounts.get(service.account);
         if (context != null) {
-            this.notifications.email_sent(context.account, sent);
+            //this.notifications.email_sent(context.account, sent);
         }
     }
 
diff --git a/src/client/application/application-folder-store-factory.vala 
b/src/client/application/application-folder-store-factory.vala
new file mode 100644
index 00000000..84384e3a
--- /dev/null
+++ b/src/client/application/application-folder-store-factory.vala
@@ -0,0 +1,282 @@
+/*
+ * Copyright © 2020 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.
+ */
+
+/**
+ * A factory for constructing plugin folder stores and folder objects.
+ *
+ * This class provides a common implementation that shares folder
+ * objects between different plugin context instances.
+ */
+internal class Application.FolderStoreFactory : Geary.BaseObject {
+
+
+    private class FolderStoreImpl : Geary.BaseObject, Plugin.FolderStore {
+
+
+        private Gee.Map<Geary.Folder,FolderImpl> folders;
+
+
+        public FolderStoreImpl(Gee.Map<Geary.Folder,FolderImpl> folders) {
+            this.folders = folders;
+        }
+
+        /** Returns a read-only set of all known folders. */
+        public Gee.Collection<Plugin.Folder> get_folders() {
+            return this.folders.values.read_only_view;
+        }
+
+        internal void destroy() {
+            this.folders = Gee.Map.empty();
+        }
+
+    }
+
+
+    private class AccountImpl : Geary.BaseObject, Plugin.Account {
+
+
+        public string display_name {
+            get { return this.backing.display_name; }
+        }
+
+
+        private Geary.AccountInformation backing;
+
+
+        public AccountImpl(Geary.AccountInformation backing) {
+            this.backing = backing;
+        }
+
+    }
+
+
+    private class FolderImpl : Geary.BaseObject, Plugin.Folder {
+
+
+        // These constants are used to determine the persistent id of
+        // the folder. Changing these may break plugins.
+        private const string ID_FORMAT = "%s:%s";
+        private const string ID_PATH_SEP = ">";
+
+
+        public string persistent_id {
+            get { return this._persistent_id; }
+        }
+        private string _persistent_id;
+
+        public string display_name {
+            get { return this._display_name; }
+        }
+        private string _display_name;
+
+        public Geary.SpecialFolderType folder_type {
+            get { return this.backing.special_folder_type; }
+        }
+
+        public Plugin.Account? account {
+            get { return this._account; }
+        }
+        private AccountImpl? _account;
+
+        // The underlying engine folder being represented.
+        internal Geary.Folder backing { get; private set; }
+
+
+        public FolderImpl(Geary.Folder backing, AccountImpl? account) {
+            this.backing = backing;
+            this._account = account;
+            this._persistent_id = ID_FORMAT.printf(
+                backing.account.information.id,
+                string.join(ID_PATH_SEP, backing.path.as_array())
+            );
+            folder_type_changed();
+        }
+
+        public GLib.Variant to_variant() {
+            return new GLib.Variant.tuple({
+                    this.backing.account.information.id,
+                        new GLib.Variant.variant(this.backing.path.to_variant())
+            });
+        }
+
+        internal void folder_type_changed() {
+            notify_property("folder-type");
+            this._display_name = this.backing.get_display_name();
+            notify_property("display-name");
+        }
+
+    }
+
+
+    private Geary.Engine engine;
+
+    private Gee.Map<Geary.AccountInformation,AccountImpl> accounts =
+        new Gee.HashMap<Geary.AccountInformation,AccountImpl>();
+    private Gee.Map<Geary.Folder,FolderImpl> folders =
+        new Gee.HashMap<Geary.Folder,FolderImpl>();
+    private Gee.Set<FolderStoreImpl> stores =
+        new Gee.HashSet<FolderStoreImpl>();
+
+
+    /**
+     * Constructs a new factory instance.
+     */
+    public FolderStoreFactory(Geary.Engine engine) throws GLib.Error {
+        this.engine = engine;
+        this.engine.account_available.connect(on_account_available);
+        this.engine.account_unavailable.connect(on_account_unavailable);
+        foreach (Geary.Account account in this.engine.get_accounts()) {
+            add_account(account.information);
+        }
+    }
+
+    /** Clearing all state of the store. */
+    public void destroy() throws GLib.Error {
+        foreach (FolderStoreImpl store in this.stores) {
+            store.destroy();
+        }
+        this.stores.clear();
+
+        this.engine.account_available.disconnect(on_account_available);
+        this.engine.account_unavailable.disconnect(on_account_unavailable);
+        foreach (Geary.Account account in this.engine.get_accounts()) {
+            remove_account(account.information);
+        }
+        this.folders.clear();
+    }
+
+    /** Constructs a new folder store for use by plugin contexts. */
+    public Plugin.FolderStore new_folder_store() {
+        var store = new FolderStoreImpl(this.folders);
+        this.stores.add(store);
+        return store;
+    }
+
+    /** Destroys a folder store once is no longer required. */
+    public void destroy_folder_store(Plugin.FolderStore plugin) {
+        FolderStoreImpl? impl = plugin as FolderStoreImpl;
+        if (impl != null) {
+            impl.destroy();
+            this.stores.remove(impl);
+        }
+    }
+
+    /** Returns the plugin folder for the given engine folder. */
+    public Plugin.Folder? get_plugin_folder(Geary.Folder engine) {
+        return this.folders.get(engine);
+    }
+
+    /** Returns the engine folder for the given plugin folder. */
+    public Geary.Folder? get_engine_folder(Plugin.Folder plugin) {
+        FolderImpl? impl = plugin as FolderImpl;
+        return (impl != null) ? impl.backing : null;
+    }
+
+    private void add_account(Geary.AccountInformation added) {
+        try {
+            this.accounts.set(added, new AccountImpl(added));
+            Geary.Account account = this.engine.get_account(added);
+            account.folders_available_unavailable.connect(
+                on_folders_available_unavailable
+            );
+            account.folders_special_type.connect(
+                on_folders_type_changed
+            );
+            add_folders(account.list_folders());
+        } catch (GLib.Error err) {
+            warning(
+                "Failed to add account %s to folder store: %s",
+                added.id, err.message
+            );
+        }
+    }
+
+    private void remove_account(Geary.AccountInformation removed) {
+        try {
+            Geary.Account account = this.engine.get_account(removed);
+            account.folders_available_unavailable.disconnect(
+                on_folders_available_unavailable
+            );
+            account.folders_special_type.disconnect(
+                on_folders_type_changed
+            );
+            remove_folders(account.list_folders());
+            this.accounts.unset(removed);
+        } catch (GLib.Error err) {
+            warning(
+                "Error removing account %s from folder store: %s",
+                removed.id, err.message
+            );
+        }
+    }
+
+    private void add_folders(Gee.Collection<Geary.Folder> to_add) {
+        foreach (Geary.Folder folder in to_add) {
+            this.folders.set(
+                folder,
+                new FolderImpl(
+                    folder, this.accounts.get(folder.account.information)
+                )
+            );
+        }
+        foreach (FolderStoreImpl store in this.stores) {
+            store.folders_available(to_plugin_folders(to_add));
+        }
+    }
+
+    private void remove_folders(Gee.Collection<Geary.Folder> to_remove) {
+        foreach (Geary.Folder folder in to_remove) {
+            this.folders.unset(folder);
+        }
+        foreach (FolderStoreImpl store in this.stores) {
+            store.folders_unavailable(to_plugin_folders(to_remove));
+        }
+    }
+
+    private Gee.Collection<FolderImpl> to_plugin_folders(
+        Gee.Collection<Geary.Folder> folders
+    ) {
+        return Geary.traverse(
+            folders
+        ).map<FolderImpl>(
+            (f) => this.folders.get(f)
+        ).to_linked_list().read_only_view;
+    }
+
+    private void on_account_available(Geary.AccountInformation to_add) {
+        add_account(to_add);
+    }
+
+    private void on_account_unavailable(Geary.AccountInformation to_remove) {
+        remove_account(to_remove);
+    }
+
+    private void on_folders_available_unavailable(
+        Geary.Account account,
+        Gee.BidirSortedSet<Geary.Folder>? available,
+        Gee.BidirSortedSet<Geary.Folder>? unavailable
+    ) {
+        if (available != null && !available.is_empty) {
+            add_folders(available);
+        }
+        if (unavailable != null && !unavailable.is_empty) {
+            remove_folders(available);
+        }
+    }
+
+    private void on_folders_type_changed(Geary.Account account,
+                                         Gee.Collection<Geary.Folder> changed) {
+        var folders = to_plugin_folders(changed);
+        foreach (FolderImpl folder in folders) {
+            folder.folder_type_changed();
+        }
+        foreach (FolderStoreImpl store in this.stores) {
+            store.folders_type_changed(folders);
+        }
+    }
+
+}
diff --git a/src/client/application/application-main-window.vala 
b/src/client/application/application-main-window.vala
index e88d23fc..31d8ba8b 100644
--- a/src/client/application/application-main-window.vala
+++ b/src/client/application/application-main-window.vala
@@ -741,8 +741,6 @@ public class Application.MainWindow :
                 );
 
                 yield open_conversation_monitor(this.conversations, cancellable);
-                this.controller.clear_new_messages(GLib.Log.METHOD, null);
-
                 this.controller.process_pending_composers();
             }
         }
@@ -2082,7 +2080,12 @@ public class Application.MainWindow :
     // this signal does not necessarily indicate that the application
     // previously didn't have focus and now it does
     private void on_has_toplevel_focus() {
-        this.controller.clear_new_messages(GLib.Log.METHOD, null);
+        if (this.selected_folder != null) {
+            this.controller.clear_new_messages(
+                this.selected_folder,
+                this.conversation_list_view.get_visible_conversations()
+            );
+        }
     }
 
     private void on_folder_selected(Geary.Folder? folder) {
@@ -2098,7 +2101,9 @@ public class Application.MainWindow :
     }
 
     private void on_visible_conversations_changed(Gee.Set<Geary.App.Conversation> visible) {
-        this.controller.clear_new_messages(GLib.Log.METHOD, visible);
+        if (this.selected_folder != null) {
+            this.controller.clear_new_messages(this.selected_folder, visible);
+        }
     }
 
     private void on_conversation_activated(Geary.App.Conversation activated) {
diff --git a/src/client/application/application-notification-context.vala 
b/src/client/application/application-notification-context.vala
index faa38d68..2152243e 100644
--- a/src/client/application/application-notification-context.vala
+++ b/src/client/application/application-notification-context.vala
@@ -1,9 +1,9 @@
 /*
- * Copyright 2016 Software Freedom Conservancy Inc.
- * Copyright 2019 Michael Gratton <mike vee net>
+ * Copyright © 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2019-2020 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.
+ * (version 2.1 or later). See the COPYING file in this distribution.
  */
 
 /**
@@ -14,285 +14,597 @@
  * the plugins will be passed an instance of this class as the
  * `context` property.
  *
- * Plugins can connect to the "notify::count", the {@link
- * new_messages_arrived} or the {@link new_messages_retired} signals
- * and update their state as these change.
+ * Plugins should register folders they wish to monitor by calling
+ * {@link start_monitoring_folder}. The context will then start
+ * keeping track of email being delivered to the folder and being seen
+ * in a main window updating {@link total_new_messages} and emitting
+ * the {@link new_messages_arrived} and {@link new_messages_retired}
+ * signals as appropriate.
  *
  * @see Plugin.NotificationPlugin
  */
 public class Application.NotificationContext : Geary.BaseObject {
 
 
-    /** Monitor hook for obtaining a contact store for an account. */
-    internal delegate Application.ContactStore? GetContactStore(
-        Geary.Account account
-    );
+    private const Geary.Email.Field REQUIRED_FIELDS  = FLAGS;
+
+
+    private class ApplicationImpl : Geary.BaseObject, Plugin.Application {
+
+
+        private Client backing;
+        private FolderStoreFactory folders;
+
+
+        public ApplicationImpl(Client backing,
+                               FolderStoreFactory folders) {
+            this.backing = backing;
+            this.folders = folders;
+        }
+
+        public override void show_folder(Plugin.Folder folder) {
+            Geary.Folder? target = this.folders.get_engine_folder(folder);
+            if (target != null) {
+                this.backing.show_folder.begin(target);
+            }
+        }
+
+    }
+
+
+    private class EmailStoreImpl : Geary.BaseObject, Plugin.EmailStore {
+
+
+        private class EmailImpl : Geary.BaseObject, Plugin.Email {
+
+
+            public Plugin.EmailIdentifier identifier {
+                get {
+                    if (this._id == null) {
+                        this._id = new IdImpl(this.backing.id, this.account);
+                    }
+                    return this._id;
+                }
+            }
+            private IdImpl? _id = null;
+
+            public string subject {
+                get { return this._subject; }
+            }
+            string _subject;
+
+            internal Geary.Email backing;
+            // Remove this when EmailIdentifier is updated to include
+            // the account
+            internal Geary.AccountInformation account { get; private set; }
+
+
+            public EmailImpl(Geary.Email backing,
+                             Geary.AccountInformation account) {
+                this.backing = backing;
+                this.account = account;
+                Geary.RFC822.Subject? subject = this.backing.subject;
+                this._subject = subject != null ? subject.to_string() : "";
+            }
+
+            public Geary.RFC822.MailboxAddress? get_primary_originator() {
+                return Util.Email.get_primary_originator(this.backing);
+            }
+
+        }
+
+
+        private class IdImpl : Geary.BaseObject,
+            Gee.Hashable<Plugin.EmailIdentifier>, Plugin.EmailIdentifier {
 
-    /** Monitor hook to determine if a folder should be notified about. */
-    internal delegate bool ShouldNotifyNewMessages(Geary.Folder folder);
+
+            internal Geary.EmailIdentifier backing { get; private set; }
+            // Remove this when EmailIdentifier is updated to include
+            // the account
+            internal Geary.AccountInformation account { get; private set; }
+
+
+            public IdImpl(Geary.EmailIdentifier backing,
+                          Geary.AccountInformation account) {
+                this.backing = backing;
+                this.account = account;
+            }
+
+            public GLib.Variant to_variant() {
+                return this.backing.to_variant();
+            }
+
+            public bool equal_to(Plugin.EmailIdentifier other) {
+                if (this == other) {
+                    return true;
+                }
+                IdImpl? impl = other as IdImpl;
+                return (
+                    impl != null &&
+                    this.backing.equal_to(impl.backing) &&
+                    this.account.equal_to(impl.account)
+                );
+            }
+
+            public uint hash() {
+                return this.backing.hash();
+            }
+
+        }
+
+
+        private Client backing;
+
+
+        public EmailStoreImpl(Client backing) {
+            this.backing = backing;
+        }
+
+        public async Gee.Collection<Plugin.Email> get_email(
+            Gee.Collection<Plugin.EmailIdentifier> plugin_ids,
+            GLib.Cancellable? cancellable
+        ) throws GLib.Error {
+            var emails = new Gee.HashSet<Plugin.Email>();
+
+            // The email could theoretically come from any account, so
+            // group them by account up front. The common case will be
+            // only a single account, so optimise for that a bit.
+
+            var accounts = new Gee.HashMap<
+                Geary.AccountInformation,
+                    Gee.Set<Geary.EmailIdentifier>
+            >();
+            Geary.AccountInformation? current_account = null;
+            Gee.Set<Geary.EmailIdentifier>? engine_ids = null;
+            foreach (Plugin.EmailIdentifier plugin_id in plugin_ids) {
+                IdImpl? id_impl = plugin_id as IdImpl;
+                if (id_impl != null) {
+                    if (id_impl.account != current_account) {
+                        current_account = id_impl.account;
+                        engine_ids = accounts.get(current_account);
+                        if (engine_ids == null) {
+                            engine_ids = new Gee.HashSet<Geary.EmailIdentifier>();
+                            accounts.set(current_account, engine_ids);
+                        }
+                    }
+                    engine_ids.add(id_impl.backing);
+                }
+            }
+
+            foreach (var account in accounts.keys) {
+                AccountContext context =
+                    this.backing.controller.get_context_for_account(account);
+                Gee.Collection<Geary.Email> batch =
+                    yield context.emails.list_email_by_sparse_id_async(
+                        accounts.get(account),
+                        ENVELOPE,
+                        NONE,
+                        context.cancellable
+                    );
+                if (batch != null) {
+                    foreach (var email in batch) {
+                        emails.add(new EmailImpl(email, account));
+                    }
+                }
+            }
+
+            return emails;
+        }
+
+        internal Gee.Collection<Plugin.EmailIdentifier> get_plugin_ids(
+            Gee.Collection<Geary.EmailIdentifier> engine_ids,
+            Geary.AccountInformation account
+        ) {
+            var plugin_ids = new Gee.HashSet<Plugin.EmailIdentifier>();
+            foreach (var id in engine_ids) {
+                plugin_ids.add(new IdImpl(id, account));
+            }
+            return plugin_ids;
+        }
+
+    }
+
+
+    private class ContactStoreImpl : Geary.BaseObject, Plugin.ContactStore {
+
+
+        private Application.ContactStore backing;
+
+
+        public ContactStoreImpl(Application.ContactStore backing) {
+            this.backing = backing;
+        }
+
+        public async Gee.Collection<Contact> search(string query,
+                                                    uint min_importance,
+                                                    uint limit,
+                                                    GLib.Cancellable? cancellable
+        ) throws GLib.Error {
+            return yield this.backing.search(
+                query, min_importance, limit, cancellable
+            );
+        }
+
+        public async Contact load(Geary.RFC822.MailboxAddress mailbox,
+                                  GLib.Cancellable? cancellable
+        ) throws GLib.Error {
+            return yield this.backing.load(mailbox, cancellable);
+        }
+
+    }
 
 
     private class MonitorInformation : Geary.BaseObject {
+
         public Geary.Folder folder;
         public GLib.Cancellable? cancellable = null;
-        public int count = 0;
-        public Gee.HashSet<Geary.EmailIdentifier> new_ids
-            = new Gee.HashSet<Geary.EmailIdentifier>();
+        public Gee.Set<Geary.EmailIdentifier> recent_ids =
+            new Gee.HashSet<Geary.EmailIdentifier>();
 
-        public MonitorInformation(Geary.Folder folder, GLib.Cancellable? cancellable) {
+        public MonitorInformation(Geary.Folder folder,
+                                  GLib.Cancellable? cancellable) {
             this.folder = folder;
             this.cancellable = cancellable;
         }
     }
 
-    /** Current total new message count across all accounts and folders. */
-    public int total_new_messages { get; private set; default = 0; }
-
     /**
-     * Folder containing the recent new message received, if any.
+     * Returns the plugin application object.
      *
-     * @see last_new_message
+     * No special permissions are required to use access this.
      */
-    public Geary.Folder? last_new_message_folder {
-        get; private set; default = null;
+    public Plugin.Application plugin_application {
+        get; private set;
     }
 
     /**
-     * Most recent new message received, if any.
+     * Current total new message count for all monitored folders.
      *
-     * @see last_new_message_folder
+     * This is the sum of the the counts returned by {@link
+     * get_new_message_count} for all folders that are being monitored
+     * after a call to {@link start_monitoring_folder}.
      */
-    public Geary.Email? last_new_message {
-        get; private set; default = null;
-    }
-
-    /** Returns a store to lookup avatars for notifications. */
-    public Application.AvatarStore avatars { get; private set; }
-
-
-    private Geary.Email.Field required_fields { get; private set; default = FLAGS; }
-
-    private Gee.Map<Geary.Folder, MonitorInformation> folder_information =
-        new Gee.HashMap<Geary.Folder, MonitorInformation>();
+    public int total_new_messages { get; private set; default = 0; }
 
-    private unowned GetContactStore contact_store_delegate;
-    private unowned ShouldNotifyNewMessages notify_delegate;
+    private Gee.Map<Geary.Folder,MonitorInformation> folder_information =
+        new Gee.HashMap<Geary.Folder,MonitorInformation>();
 
+    private unowned Client application;
+    private FolderStoreFactory folders_factory;
+    private Plugin.FolderStore folders;
+    private EmailStoreImpl email;
+    private PluginManager.PluginFlags flags;
 
-    /** Emitted when a new folder will be monitored. */
-    public signal void folder_added(Geary.Folder folder);
 
-    /** Emitted when a folder should no longer be monitored. */
-    public signal void folder_removed(Geary.Folder folder);
+    /**
+     * Emitted when new messages have been downloaded.
+     *
+     * This will only be emitted for folders that are being monitored
+     * by calling {@link start_monitoring_folder}.
+     */
+    public signal void new_messages_arrived(
+        Plugin.Folder parent,
+        int total,
+        Gee.Collection<Plugin.EmailIdentifier> added
+    );
 
-    /** Emitted when new messages have been downloaded. */
-    public signal void new_messages_arrived(Geary.Folder parent, int total, int added);
+    /**
+     * Emitted when a folder has been cleared of new messages.
+     *
+     * This will only be emitted for folders that are being monitored
+     * after a call to {@link start_monitoring_folder}.
+     */
+    public signal void new_messages_retired(Plugin.Folder parent, int total);
 
-    /** Emitted when a folder has been cleared of new messages. */
-    public signal void new_messages_retired(Geary.Folder parent, int total);
 
-    /** Emitted when an email has been sent. */
-    public signal void email_sent(Geary.Account account,
-                                  Geary.RFC822.Message sent);
+    /** Constructs a new context instance. */
+    internal NotificationContext(Client application,
+                                 FolderStoreFactory folders_factory,
+                                 PluginManager.PluginFlags flags) {
+        this.application = application;
+        this.folders_factory = folders_factory;
+        this.folders = folders_factory.new_folder_store();
+        this.email = new EmailStoreImpl(application);
+        this.flags = flags;
+
+        this.plugin_application = new ApplicationImpl(
+            application, folders_factory
+        );
+    }
 
+    /**
+     * Returns a store to lookup folders for notifications.
+     *
+     * This method may prompt for permission before returning.
+     *
+     * @throws Geary.EngineError.PERMISSIONS if permission to access
+     * this resource was not given
+     */
+    public async Plugin.FolderStore get_folders()
+        throws Geary.EngineError.PERMISSIONS {
+        return this.folders;
+    }
 
-    /** Constructs a new context instance. */
-    internal NotificationContext(AvatarStore avatars,
-                                 GetContactStore contact_store_delegate,
-                                 ShouldNotifyNewMessages notify_delegate) {
-        this.avatars = avatars;
-        this.contact_store_delegate = contact_store_delegate;
-        this.notify_delegate = notify_delegate;
+    /**
+     * Returns a store to lookup email for notifications.
+     *
+     * This method may prompt for permission before returning.
+     *
+     * @throws Geary.EngineError.PERMISSIONS if permission to access
+     * this resource was not given
+     */
+    public async Plugin.EmailStore get_email()
+        throws Geary.EngineError.PERMISSIONS {
+        return this.email;
     }
 
-    /** Determines if notifications should be made for a specific folder. */
-    public bool should_notify_new_messages(Geary.Folder folder) {
-        return this.notify_delegate(folder);
+    /**
+     * Returns a store to lookup contacts for notifications.
+     *
+     * This method may prompt for permission before returning.
+     *
+     * @throws Geary.EngineError.NOT_FOUND if the given account does
+     * not exist
+     * @throws Geary.EngineError.PERMISSIONS if permission to access
+     * this resource was not given
+     */
+    public async Plugin.ContactStore get_contacts_for_folder(Plugin.Folder source)
+        throws Geary.EngineError.NOT_FOUND,
+            Geary.EngineError.PERMISSIONS {
+        Geary.Folder? folder = this.folders_factory.get_engine_folder(source);
+        AccountContext? context = null;
+        if (folder != null) {
+            context = this.application.controller.get_context_for_account(
+                folder.account.information
+            );
+        }
+        if (context == null) {
+            throw new Geary.EngineError.NOT_FOUND(
+                "No account for folder: %s", source.display_name
+            );
+        }
+        return new ContactStoreImpl(context.contacts);
     }
 
-    /** Returns a contact store to lookup contacts for notifications. */
-    public Application.ContactStore? get_contact_store(Geary.Account account) {
-        return this.contact_store_delegate(account);
+    /**
+     * Returns the client's application object.
+     *
+     * Only plugins that are trusted by the client will be provided
+     * access to the application instance.
+     *
+     * @throws Geary.EngineError.PERMISSIONS if permission to access
+     * this resource was not given
+     */
+    public Client get_client_application()
+        throws Geary.EngineError.PERMISSIONS {
+        if (!(PluginManager.PluginFlags.TRUSTED in this.flags)) {
+            throw new Geary.EngineError.PERMISSIONS("Plugin is not trusted");
+        }
+        return this.application;
     }
 
-    /** Returns a read-only set the context's monitored folders. */
-    public Gee.Collection<Geary.Folder> get_folders() {
-        return this.folder_information.keys.read_only_view;
+    /**
+     * Determines if notifications should be made for a specific folder.
+     *
+     * Notification plugins should call this to first before
+     * displaying a "new mail" notification for mail in a specific
+     * folder. It will return true for any monitored folder that is
+     * not currently visible in the currently focused main window, if
+     * any.
+     */
+    public bool should_notify_new_messages(Plugin.Folder target) {
+        // Don't show notifications if the top of a monitored folder's
+        // conversations are visible. That is, if there is a main
+        // window, it's focused, the folder is selected, and the
+        // conversation list is at the top.
+        Geary.Folder? folder = this.folders_factory.get_engine_folder(target);
+        MainWindow? window = this.application.last_active_main_window;
+        return (
+            folder != null &&
+            this.folder_information.has_key(folder) && (
+                window == null ||
+                !window.has_toplevel_focus ||
+                window.selected_folder != folder ||
+                window.conversation_list_view.vadjustment.value > 0.0
+            )
+        );
     }
 
-    /** Returns the new message count for a specific folder. */
-    public int get_new_message_count(Geary.Folder folder)
+    /**
+     * Returns the new message count for a specific folder.
+     *
+     * The context must have already been requested to monitor the
+     * folder by a call to {@link start_monitoring_folder}.
+     */
+    public int get_new_message_count(Plugin.Folder target)
         throws Geary.EngineError.NOT_FOUND {
-        MonitorInformation? info = folder_information.get(folder);
+        Geary.Folder? folder = this.folders_factory.get_engine_folder(target);
+        MonitorInformation? info = null;
+        if (folder != null) {
+            info = folder_information.get(folder);
+        }
         if (info == null) {
             throw new Geary.EngineError.NOT_FOUND(
                 "No such folder: %s", folder.path.to_string()
             );
         }
-        return info.count;
+        return info.recent_ids.size;
     }
 
-    /** Adds fields for loaded email required by a plugin. */
-    public void add_required_fields(Geary.Email.Field fields) {
-        this.required_fields |= fields;
-    }
-
-    /** Removes fields for loaded email no longer required by a plugin. */
-    public void remove_required_fields(Geary.Email.Field fields) {
-        this.required_fields ^= fields;
-    }
-
-    internal void add_folder(Geary.Folder folder, GLib.Cancellable? cancellable) {
-        if (!this.folder_information.has_key(folder)) {
+    /**
+     * Starts monitoring a folder for new messages.
+     *
+     * Notification plugins should call this to start the context
+     * recording new messages for a specific folder.
+     */
+    public void start_monitoring_folder(Plugin.Folder target) {
+        Geary.Folder? folder = this.folders_factory.get_engine_folder(target);
+        AccountContext? context =
+            this.application.controller.get_context_for_account(
+                folder.account.information
+            );
+        if (folder != null &&
+            context != null &&
+            !this.folder_information.has_key(folder)) {
             folder.email_locally_appended.connect(on_email_locally_appended);
             folder.email_flags_changed.connect(on_email_flags_changed);
             folder.email_removed.connect(on_email_removed);
 
             this.folder_information.set(
-                folder, new MonitorInformation(folder, cancellable)
+                folder, new MonitorInformation(folder, context.cancellable)
             );
-
-            folder_added(folder);
         }
     }
 
-    internal void remove_folder(Geary.Folder folder) {
-        if (folder_information.has_key(folder)) {
-            folder.email_locally_appended.disconnect(on_email_locally_appended);
-            folder.email_flags_changed.disconnect(on_email_flags_changed);
-            folder.email_removed.disconnect(on_email_removed);
-
-            this.total_new_messages -= this.folder_information.get(folder).count;
-
-            this.folder_information.unset(folder);
-
-            folder_removed(folder);
+    /** Stops monitoring a folder for new messages. */
+    public void stop_monitoring_folder(Plugin.Folder target) {
+        Geary.Folder? folder = this.folders_factory.get_engine_folder(target);
+        if (folder != null) {
+            remove_folder(folder);
         }
     }
 
-    internal void clear_folders() {
+    /** Determines if a folder is curently being monitored. */
+    public bool is_monitoring_folder(Plugin.Folder target) {
+        return this.folder_information.has_key(
+            this.folders_factory.get_engine_folder(target)
+        );
+    }
+
+    internal void destroy() {
+        this.folders_factory.destroy_folder_store(this.folders);
         // Get an array so the loop does not blow up when removing values.
         foreach (Geary.Folder monitored in this.folder_information.keys.to_array()) {
             remove_folder(monitored);
         }
     }
 
-    internal bool are_any_new_messages(Geary.Folder folder,
-                                     Gee.Collection<Geary.EmailIdentifier> ids)
-        throws Geary.EngineError.NOT_FOUND {
-        MonitorInformation? info = folder_information.get(folder);
-        if (info == null) {
-            throw new Geary.EngineError.NOT_FOUND(
-                "No such folder: %s", folder.path.to_string()
-            );
+    internal void clear_new_messages(Geary.Folder location,
+                                     Gee.Set<Geary.App.Conversation>? visible) {
+        MonitorInformation? info = this.folder_information.get(location);
+        if (info != null) {
+            foreach (Geary.App.Conversation conversation in visible) {
+                if (Geary.traverse(
+                        conversation.get_email_ids()
+                    ).any((id) => info.recent_ids.contains(id))) {
+                    Gee.Set<Geary.EmailIdentifier> old_ids = info.recent_ids;
+                    info.recent_ids = new Gee.HashSet<Geary.EmailIdentifier>();
+                    update_count(info, false, old_ids);
+                    break;
+                }
+            }
         }
-        return Geary.traverse(ids).any((id) => info.new_ids.contains(id));
     }
 
-    internal void clear_new_messages(Geary.Folder folder)
-        throws Geary.EngineError.NOT_FOUND {
-        MonitorInformation? info = folder_information.get(folder);
-        if (info == null) {
-            throw new Geary.EngineError.NOT_FOUND(
-                "No such folder: %s", folder.path.to_string()
-            );
+    private void new_messages(MonitorInformation info,
+                              Gee.Collection<Geary.Email> emails) {
+        Gee.Collection<Geary.EmailIdentifier> added =
+            new Gee.HashSet<Geary.EmailIdentifier>();
+        foreach (Geary.Email email in emails) {
+            if (email.email_flags.is_unread() &&
+                info.recent_ids.add(email.id)) {
+                added.add(email.id);
+            }
+        }
+        if (added.size > 0) {
+            update_count(info, true, added);
         }
-
-        info.new_ids.clear();
-        last_new_message_folder = null;
-        last_new_message = null;
-
-        update_count(info, false, 0);
-    }
-
-    private void on_email_locally_appended(Geary.Folder folder,
-        Gee.Collection<Geary.EmailIdentifier> email_ids) {
-        do_process_new_email.begin(folder, email_ids);
-    }
-
-    private void on_email_flags_changed(Geary.Folder folder,
-        Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> ids) {
-        retire_new_messages(folder, ids.keys);
-    }
-
-    private void on_email_removed(Geary.Folder folder, Gee.Collection<Geary.EmailIdentifier> ids) {
-        retire_new_messages(folder, ids);
     }
 
-    private async void do_process_new_email(Geary.Folder folder,
-        Gee.Collection<Geary.EmailIdentifier> email_ids) {
+    private void retire_new_messages(Geary.Folder folder,
+        Gee.Collection<Geary.EmailIdentifier> email_ids
+    ) {
         MonitorInformation info = folder_information.get(folder);
-
-        try {
-            Gee.List<Geary.Email>? list = yield folder.list_email_by_sparse_id_async(email_ids,
-                required_fields, Geary.Folder.ListFlags.NONE, info.cancellable);
-            if (list == null || list.size == 0) {
-                debug("Warning: %d new emails, but none could be listed", email_ids.size);
-
-                return;
+        Gee.Collection<Geary.EmailIdentifier> removed =
+            new Gee.HashSet<Geary.EmailIdentifier>();
+        foreach (Geary.EmailIdentifier email_id in email_ids) {
+            if (info.recent_ids.remove(email_id)) {
+                removed.add(email_id);
             }
+        }
 
-            new_messages(info, list);
-
-            debug("do_process_new_email: %d messages listed, %d unread in folder %s",
-                list.size, info.count, folder.to_string());
-        } catch (Error err) {
-            debug("Unable to notify of new email: %s", err.message);
+        if (removed.size > 0) {
+            update_count(info, false, removed);
         }
     }
 
-    private void new_messages(MonitorInformation info, Gee.Collection<Geary.Email> emails) {
-        int appended_count = 0;
-        foreach (Geary.Email email in emails) {
-            if (!email.fields.fulfills(required_fields)) {
-                debug("Warning: new message %s (%Xh) does not fulfill NewMessagesMonitor required fields of 
%Xh",
-                    email.id.to_string(), email.fields, required_fields);
-            }
-
-            if (info.new_ids.contains(email.id))
-                continue;
+    private void update_count(MonitorInformation info,
+                              bool arrived,
+                              Gee.Collection<Geary.EmailIdentifier> delta) {
+        Plugin.Folder folder =
+            this.folders_factory.get_plugin_folder(info.folder);
+        if (arrived) {
+            this.total_new_messages += delta.size;
+            new_messages_arrived(
+                folder,
+                info.recent_ids.size,
+                this.email.get_plugin_ids(delta, info.folder.account.information)
+            );
+        } else {
+            this.total_new_messages -= delta.size;
+            new_messages_retired(
+                folder, info.recent_ids.size
+            );
+        }
+    }
 
-            if (!email.email_flags.is_unread())
-                continue;
+    private void remove_folder(Geary.Folder target) {
+        MonitorInformation? info = this.folder_information.get(target);
+        if (info != null) {
+            target.email_locally_appended.disconnect(on_email_locally_appended);
+            target.email_flags_changed.disconnect(on_email_flags_changed);
+            target.email_removed.disconnect(on_email_removed);
 
-            last_new_message_folder = info.folder;
-            last_new_message = email;
+            this.total_new_messages -= info.recent_ids.size;
 
-            info.new_ids.add(email.id);
-            appended_count++;
+            this.folder_information.unset(target);
         }
 
-        update_count(info, true, appended_count);
     }
 
-    private void retire_new_messages(Geary.Folder folder,
-        Gee.Collection<Geary.EmailIdentifier> email_ids) {
-        MonitorInformation info = folder_information.get(folder);
-
-        int removed_count = 0;
-        foreach (Geary.EmailIdentifier email_id in email_ids) {
-            if (last_new_message != null && last_new_message.id.equal_to(email_id)) {
-                last_new_message_folder = null;
-                last_new_message = null;
+    private async void do_process_new_email(
+        Geary.Folder folder,
+        Gee.Collection<Geary.EmailIdentifier> email_ids
+    ) {
+        MonitorInformation info = this.folder_information.get(folder);
+        if (info != null) {
+            Gee.List<Geary.Email>? list = null;
+            try {
+                list = yield folder.list_email_by_sparse_id_async(
+                    email_ids,
+                    REQUIRED_FIELDS,
+                    NONE,
+                    info.cancellable
+                );
+            } catch (GLib.Error err) {
+                warning(
+                    "Unable to list new email for notification: %s", err.message
+                );
+            }
+            if (list != null && !list.is_empty) {
+                new_messages(info, list);
+            } else {
+                warning(
+                    "%d new emails, but none could be listed for notification",
+                    email_ids.size
+                );
             }
-
-            if (info.new_ids.remove(email_id))
-                removed_count++;
         }
-
-        update_count(info, false, removed_count);
     }
 
-    private void update_count(MonitorInformation info, bool arrived, int delta) {
-        int new_size = info.new_ids.size;
+    private void on_email_locally_appended(Geary.Folder folder,
+        Gee.Collection<Geary.EmailIdentifier> email_ids) {
+        do_process_new_email.begin(folder, email_ids);
+    }
 
-        total_new_messages += new_size - info.count;
-        info.count = new_size;
+    private void on_email_flags_changed(Geary.Folder folder,
+        Gee.Map<Geary.EmailIdentifier, Geary.EmailFlags> ids) {
+        retire_new_messages(folder, ids.keys);
+    }
 
-        if (arrived)
-            new_messages_arrived(info.folder, info.count, delta);
-        else
-            new_messages_retired(info.folder, info.count);
+    private void on_email_removed(Geary.Folder folder,
+                                  Gee.Collection<Geary.EmailIdentifier> ids) {
+        retire_new_messages(folder, ids);
     }
 
 }
diff --git a/src/client/application/application-plugin-manager.vala 
b/src/client/application/application-plugin-manager.vala
index e052d0c0..bdae6aba 100644
--- a/src/client/application/application-plugin-manager.vala
+++ b/src/client/application/application-plugin-manager.vala
@@ -19,33 +19,48 @@ public class Application.PluginManager : GLib.Object {
         "notification-badge"
     };
 
+    /** Flags assigned to a plugin by the manager. */
+    [Flags]
+    public enum PluginFlags {
+        /** If set, the plugin is in the set of trusted plugins. */
+        TRUSTED;
+    }
+
+
     private Client application;
-    private Peas.Engine engine;
+    private Peas.Engine plugins;
     private bool is_shutdown = false;
     private string trusted_path;
 
+    private FolderStoreFactory folders_factory;
+
     private Peas.ExtensionSet notification_extensions;
-    private NotificationContext notifications;
+    private Gee.Set<NotificationContext> notification_contexts =
+        new Gee.HashSet<NotificationContext>();
 
 
-    public PluginManager(Client application,
-                         NotificationContext notifications) {
+    public PluginManager(Client application) throws GLib.Error {
         this.application = application;
-        this.engine = Peas.Engine.get_default();
+        this.plugins = Peas.Engine.get_default();
+        this.folders_factory = new FolderStoreFactory(application.engine);
 
         this.trusted_path = application.get_app_plugins_dir().get_path();
         this.plugins.add_search_path(trusted_path, null);
 
-        this.notifications = notifications;
         this.notification_extensions = new Peas.ExtensionSet(
-            this.engine,
-            typeof(Plugin.Notification),
-            "application", this.application,
-            "context", this.notifications
+            this.plugins,
+            typeof(Plugin.Notification)
         );
         this.notification_extensions.extension_added.connect((info, extension) => {
                 Plugin.Notification? plugin = extension as Plugin.Notification;
                 if (plugin != null) {
+                    var context = new NotificationContext(
+                        this.application,
+                        this.folders_factory,
+                        to_plugin_flags(info)
+                    );
+                    this.notification_contexts.add(context);
+                    plugin.notifications = context;
                     plugin.activate();
                 }
             });
@@ -54,19 +69,22 @@ public class Application.PluginManager : GLib.Object {
                 if (plugin != null) {
                     plugin.deactivate(this.is_shutdown);
                 }
+                var context = plugin.notifications;
+                context.destroy();
+                this.notification_contexts.remove(context);
             });
 
         string[] optional_names = application.config.get_optional_plugins();
-        foreach (Peas.PluginInfo info in this.engine.get_plugin_list()) {
+        foreach (Peas.PluginInfo info in this.plugins.get_plugin_list()) {
             string name = info.get_module_name();
             try {
                 if (info.is_available()) {
                     if (is_trusted(info)) {
                         debug("Loading trusted plugin: %s", name);
-                        this.engine.load_plugin(info);
+                        this.plugins.load_plugin(info);
                     } else if (name in optional_names) {
                         debug("Loading optional plugin: %s", name);
-                        this.engine.load_plugin(info);
+                        this.plugins.load_plugin(info);
                     }
                 }
             } catch (GLib.Error err) {
@@ -82,9 +100,13 @@ public class Application.PluginManager : GLib.Object {
         );
     }
 
+    public inline PluginFlags to_plugin_flags(Peas.PluginInfo plugin) {
+        return is_trusted(plugin) ? PluginFlags.TRUSTED : 0;
+    }
+
     public Gee.Collection<Peas.PluginInfo> get_optional_plugins() {
         var plugins = new Gee.LinkedList<Peas.PluginInfo>();
-        foreach (Peas.PluginInfo plugin in this.engine.get_plugin_list()) {
+        foreach (Peas.PluginInfo plugin in this.plugins.get_plugin_list()) {
             try {
                 plugin.is_available();
                 if (!is_trusted(plugin)) {
@@ -145,4 +167,8 @@ public class Application.PluginManager : GLib.Object {
         this.folders_factory.destroy();
     }
 
+    internal Gee.Collection<NotificationContext> get_notification_contexts() {
+        return this.notification_contexts.read_only_view;
+    }
+
 }
diff --git a/src/client/meson.build b/src/client/meson.build
index 847c8583..da3e81e5 100644
--- a/src/client/meson.build
+++ b/src/client/meson.build
@@ -9,6 +9,7 @@ geary_client_vala_sources = files(
   'application/application-contact-store.vala',
   'application/application-contact.vala',
   'application/application-controller.vala',
+  'application/application-folder-store-factory.vala',
   'application/application-main-window.vala',
   'application/application-notification-context.vala',
   'application/application-plugin-manager.vala',
diff --git a/src/client/plugin/desktop-notifications/desktop-notifications.vala 
b/src/client/plugin/desktop-notifications/desktop-notifications.vala
index 7b6c9822..95c0c9bc 100644
--- a/src/client/plugin/desktop-notifications/desktop-notifications.vala
+++ b/src/client/plugin/desktop-notifications/desktop-notifications.vala
@@ -1,6 +1,6 @@
 /*
- * Copyright 2016 Software Freedom Conservancy Inc.
- * Copyright 2019 Michael Gratton <mike vee net>.
+ * Copyright © 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2019-2020 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.
@@ -21,33 +21,40 @@ public void peas_register_types(TypeModule module) {
 public class Plugin.DesktopNotifications : Geary.BaseObject, Notification {
 
 
-    public const Geary.Email.Field REQUIRED_FIELDS =
-        Geary.Email.Field.ORIGINATORS | Geary.Email.Field.SUBJECT;
+    private const Geary.SpecialFolderType[] MONITORED_TYPES = {
+        INBOX, NONE
+    };
 
-    public Application.Client application {
-        get; construct set;
-    }
-
-    public Application.NotificationContext context {
-        get; construct set;
+    public global::Application.NotificationContext notifications {
+        get; set;
     }
 
     private const string ARRIVED_ID = "email-arrived";
 
+    private global::Application.Client? application = null;
+    private EmailStore? email = null;
     private GLib.Notification? arrived_notification = null;
     private GLib.Cancellable? cancellable = null;
 
 
     public override void activate() {
-        this.context.add_required_fields(REQUIRED_FIELDS);
-        this.context.new_messages_arrived.connect(on_new_messages_arrived);
+        try {
+            this.application = this.notifications.get_client_application();
+        } catch (GLib.Error error) {
+            warning(
+                "Failed obtain application instance: %s",
+                error.message
+            );
+        }
+
+        this.notifications.new_messages_arrived.connect(on_new_messages_arrived);
         this.cancellable = new GLib.Cancellable();
+
+        this.connect_signals.begin();
     }
 
     public override void deactivate(bool is_shutdown) {
         this.cancellable.cancel();
-        this.context.new_messages_arrived.disconnect(on_new_messages_arrived);
-        this.context.remove_required_fields(REQUIRED_FIELDS);
 
         // Keep existing notifications if shutting down since they are
         // persistent, but revoke if the plugin is being disabled.
@@ -56,96 +63,116 @@ public class Plugin.DesktopNotifications : Geary.BaseObject, Notification {
         }
     }
 
+    private async void connect_signals() {
+        try {
+            this.email = yield this.notifications.get_email();
+        } catch (GLib.Error error) {
+            warning(
+                "Unable to get folders for plugin: %s",
+                error.message
+            );
+        }
+
+        try {
+            FolderStore folders = yield this.notifications.get_folders();
+            folders.folders_available.connect(
+                (folders) => check_folders(folders)
+            );
+            folders.folders_unavailable.connect(
+                (folders) => check_folders(folders)
+            );
+            folders.folders_type_changed.connect(
+                (folders) => check_folders(folders)
+            );
+            check_folders(folders.get_folders());
+        } catch (GLib.Error error) {
+            warning(
+                "Unable to get folders for plugin: %s",
+                error.message
+            );
+        }
+    }
+
     private void clear_arrived_notification() {
         this.application.withdraw_notification(ARRIVED_ID);
         this.arrived_notification = null;
     }
 
-    private void notify_new_mail(Geary.Folder folder, int added) {
-        string body = ngettext(
-            /// Notification body text for new email when no other
-            /// new messages are already awaiting.
-            "%d new message", "%d new messages", added
-        ).printf(added);
+    private async void notify_specific_message(Folder folder,
+                                               int total,
+                                               Email email
+    ) throws GLib.Error {
+        string title = to_notitication_title(folder.account, total);
+        Geary.RFC822.MailboxAddress? originator = email.get_primary_originator();
+        if (originator != null) {
+            ContactStore contacts =
+                yield this.notifications.get_contacts_for_folder(folder);
+            global::Application.Contact? contact = yield contacts.load(
+                originator, this.cancellable
+            );
+            title = (
+                contact.is_trusted
+                ? contact.display_name
+                : originator.to_short_display()
+            );
+        }
 
-        int total = 0;
-        try {
-            total = this.context.get_new_message_count(folder);
-        } catch (Geary.EngineError err) {
-            // All good
+        string body = email.subject;
+        if (total > 1) {
+            body = ngettext(
+                /// Notification body when a message as been received
+                /// and other unread messages have not been
+                /// seen. First string substitution is the message
+                /// subject, second is the number of unseen messages,
+                /// third is the name of the email account.
+                "%s\n(%d other new message for %s)",
+                "%s\n(%d other new messages for %s)",
+                total - 1
+            ).printf(
+                body,
+                total - 1,
+                folder.account.display_name
+            );
         }
 
+        issue_arrived_notification(title, body, folder, email.identifier);
+    }
+
+    private void notify_general(Folder folder, int total, int added) {
+        string title = to_notitication_title(folder.account, total);
+        string body = ngettext(
+            /// Notification body when multiple messages have been
+            /// received at the same time and other unseen messages
+            /// exist. String substitution is the number of new
+            /// messages that have arrived.
+            "%d new message", "%d new messages", added
+        ).printf(added);
         if (total > added) {
             body = ngettext(
-                /// Notification body text for new email when
-                /// other new messages have already been notified
-                /// about
+                /// Notification body when multiple messages have been
+                /// received at the same time and some unseen messages
+                /// already exist. String substitution is the message
+                /// above with the number of new messages that have
+                /// arrived, number substitution is the total number
+                /// of unseen messages.
                 "%s, %d new message total", "%s, %d new messages total",
                 total
             ).printf(body, total);
         }
 
-        issue_arrived_notification(
-            folder.account.information.display_name, body, folder, null
-        );
-    }
-
-    private async void notify_one_message(Geary.Folder folder,
-                                          Geary.Email email,
-                                          GLib.Cancellable? cancellable)
-        throws GLib.Error {
-        Geary.RFC822.MailboxAddress? originator =
-            Util.Email.get_primary_originator(email);
-        if (originator != null) {
-            Application.ContactStore contacts =
-                this.context.get_contact_store(folder.account);
-            Application.Contact contact = yield contacts.load(
-                originator, cancellable
-            );
-
-            int count = 1;
-            try {
-                count = this.context.get_new_message_count(folder);
-            } catch (Geary.EngineError.NOT_FOUND err) {
-                // All good
-            }
-
-            string body = "";
-            if (count <= 1) {
-                body = Util.Email.strip_subject_prefixes(email);
-            } else {
-                body = ngettext(
-                    "%s\n(%d other new message for %s)",
-                    "%s\n(%d other new messages for %s)", count - 1).printf(
-                        Util.Email.strip_subject_prefixes(email),
-                        count - 1,
-                        folder.account.information.display_name
-                    );
-            }
-
-            issue_arrived_notification(
-                contact.is_trusted
-                ? contact.display_name : originator.to_short_display(),
-                body,
-                folder,
-                email.id
-            );
-        } else {
-            notify_new_mail(folder, 1);
-        }
+        issue_arrived_notification(title, body, folder, null);
     }
 
     private void issue_arrived_notification(string summary,
                                             string body,
-                                            Geary.Folder folder,
-                                            Geary.EmailIdentifier? id) {
+                                            Folder folder,
+                                            EmailIdentifier? id) {
         // only one outstanding notification at a time
         clear_arrived_notification();
 
         string? action = null;
         GLib.Variant[] target_param = new GLib.Variant[] {
-            folder.account.information.id,
-            new GLib.Variant.variant(folder.path.to_variant())
+            new GLib.Variant.variant(folder.to_variant())
         };
 
         if (id == null) {
@@ -172,11 +199,13 @@ public class Plugin.DesktopNotifications : Geary.BaseObject, Notification {
         GLib.Notification notification = new GLib.Notification(summary);
         notification.set_body(body);
         notification.set_icon(
-            new GLib.ThemedIcon("%s-symbolic".printf(Application.Client.APP_ID))
+            new GLib.ThemedIcon(
+                "%s-symbolic".printf(global::Application.Client.APP_ID)
+            )
         );
 
-        /* We do not show notification action under Unity */
-
+        // Do not show notification actions under Unity, it's
+        // notifications daemon doesn't support them.
         if (this.application.config.desktop_environment == UNITY) {
             this.application.send_notification(id, notification);
             return notification;
@@ -192,22 +221,60 @@ public class Plugin.DesktopNotifications : Geary.BaseObject, Notification {
         }
     }
 
-    private void on_new_messages_arrived(Geary.Folder folder,
-                                         int total,
-                                         int added) {
-        if (this.context.should_notify_new_messages(folder)) {
-            if (added == 1 &&
-                this.context.last_new_message_folder != null &&
-                this.context.last_new_message != null) {
-                this.notify_one_message.begin(
-                    this.context.last_new_message_folder,
-                    this.context.last_new_message,
-                    this.cancellable
-                );
-            } else if (added > 0) {
-                notify_new_mail(folder, added);
+    private async void handle_new_messages(Folder folder,
+                                           int total,
+                                           Gee.Collection<EmailIdentifier> added) {
+        if (this.notifications.should_notify_new_messages(folder)) {
+            // notify about a specific message if it's the only one
+            // present and it can be loaded, otherwise notify
+            // generally
+            bool notified = false;
+            if (this.email != null &&
+                added.size == 1) {
+                try {
+                    Email? message = Geary.Collection.first(
+                        yield this.email.get_email(added, this.cancellable)
+                    );
+                    if (message != null) {
+                        yield notify_specific_message(folder, total, message);
+                        notified = true;
+                    } else {
+                        warning("Could not load email for notification");
+                    }
+                } catch (GLib.Error error) {
+                    warning("Error loading email for notification: %s", error.message);
+                }
+            }
+
+            if (!notified) {
+                notify_general(folder, total, added.size);
             }
         }
     }
 
+    private void check_folders(Gee.Collection<Folder> folders) {
+        foreach (Folder folder in folders) {
+            if (folder.folder_type in MONITORED_TYPES) {
+                this.notifications.start_monitoring_folder(folder);
+            } else {
+                this.notifications.stop_monitoring_folder(folder);
+            }
+        }
+    }
+
+    private inline string to_notitication_title(Account account, int count) {
+        return ngettext(
+            /// Notification title when new messages have been
+            /// received. String substitution is the name of the email
+            /// account.
+            "New message for %s", "New messages for %s", count
+        ).printf(account.display_name);
+    }
+
+    private void on_new_messages_arrived(Folder folder,
+                                         int total,
+                                         Gee.Collection<EmailIdentifier> added) {
+        this.handle_new_messages.begin(folder, total, added);
+    }
+
 }
diff --git a/src/client/plugin/messaging-menu/messaging-menu.vala 
b/src/client/plugin/messaging-menu/messaging-menu.vala
index 6fd3b69f..a97840cd 100644
--- a/src/client/plugin/messaging-menu/messaging-menu.vala
+++ b/src/client/plugin/messaging-menu/messaging-menu.vala
@@ -1,6 +1,6 @@
 /*
- * Copyright 2016 Software Freedom Conservancy Inc.
- * Copyright 2019 Michael Gratton <mike vee net>.
+ * Copyright © 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2019-2020 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.
@@ -19,66 +19,56 @@ public void peas_register_types(TypeModule module) {
 public class Plugin.MessagingMenu : Geary.BaseObject, Notification {
 
 
-    public Application.Client application {
-        get; construct set;
+    public global::Application.NotificationContext notifications {
+        get; set;
     }
 
-    public Application.NotificationContext context {
-        get; construct set;
-    }
 
     private global::MessagingMenu.App? app = null;
+    private FolderStore? folders = null;
 
 
     public override void activate() {
         this.app = new global::MessagingMenu.App(
-            "%s.desktop".printf(Application.Client.APP_ID)
+            "%s.desktop".printf(global::Application.Client.APP_ID)
         );
         this.app.register();
         this.app.activate_source.connect(on_activate_source);
 
-        this.context.folder_removed.connect(on_folder_removed);
-        this.context.new_messages_arrived.connect(on_new_messages_changed);
-        this.context.new_messages_retired.connect(on_new_messages_changed);
+        this.notifications.new_messages_arrived.connect(on_new_messages_changed);
+        this.notifications.new_messages_retired.connect(on_new_messages_changed);
+        this.connect_folders.begin();
     }
 
     public override void deactivate(bool is_shutdown) {
-        this.context.folder_removed.disconnect(on_folder_removed);
-        this.context.new_messages_arrived.disconnect(on_new_messages_changed);
-        this.context.new_messages_retired.disconnect(on_new_messages_changed);
-
         this.app.activate_source.disconnect(on_activate_source);
         this.app.unregister();
         this.app = null;
     }
 
-    private string get_source_id(Geary.Folder folder) {
-        return "new-messages-id-%s-%s".printf(folder.account.information.id, folder.path.to_string());
-    }
-
-    private void on_activate_source(string source_id) {
-        foreach (Geary.Folder folder in this.context.get_folders()) {
-            if (source_id == get_source_id(folder)) {
-                this.application.show_folder.begin(folder);
-                break;
-            }
+    private async void connect_folders() {
+        try {
+            this.folders = yield this.notifications.get_folders();
+            folders.folders_available.connect(
+                (folders) => check_folders(folders)
+            );
+            folders.folders_unavailable.connect(
+                (folders) => check_folders(folders)
+            );
+            folders.folders_type_changed.connect(
+                (folders) => check_folders(folders)
+            );
+            check_folders(folders.get_folders());
+        } catch (GLib.Error error) {
+            warning(
+                "Unable to get folders for plugin: %s",
+                error.message
+            );
         }
     }
 
-    private void on_new_messages_changed(Geary.Folder folder, int count) {
-        if (count > 0) {
-            show_new_messages_count(folder, count);
-        } else {
-            remove_new_messages_count(folder);
-        }
-    }
-
-    private void on_folder_removed(Geary.Folder folder) {
-        remove_new_messages_count(folder);
-    }
-
-    private void show_new_messages_count(Geary.Folder folder, int count) {
-        if (this.context.should_notify_new_messages(folder)) {
+    private void show_new_messages_count(Folder folder, int count) {
+        if (this.notifications.should_notify_new_messages(folder)) {
             string source_id = get_source_id(folder);
 
             if (this.app.has_source(source_id)) {
@@ -87,7 +77,7 @@ public class Plugin.MessagingMenu : Geary.BaseObject, Notification {
                 this.app.append_source_with_count(
                     source_id,
                     null,
-                    _("%s — New Messages").printf(folder.account.information.display_name),
+                    _("%s — New Messages").printf(folder.display_name),
                     count);
             }
 
@@ -95,7 +85,7 @@ public class Plugin.MessagingMenu : Geary.BaseObject, Notification {
         }
     }
 
-    private void remove_new_messages_count(Geary.Folder folder) {
+    private void remove_new_messages_count(Folder folder) {
         string source_id = get_source_id(folder);
         if (this.app.has_source(source_id)) {
             this.app.remove_attention(source_id);
@@ -103,4 +93,37 @@ public class Plugin.MessagingMenu : Geary.BaseObject, Notification {
         }
     }
 
+    private string get_source_id(Folder folder) {
+        return "geary%s".printf(folder.to_variant().print(false));
+    }
+
+    private void on_activate_source(string source_id) {
+        if (this.folders != null) {
+            foreach (Folder folder in this.folders.get_folders()) {
+                if (source_id == get_source_id(folder)) {
+                    this.notifications.plugin_application.show_folder(folder);
+                    break;
+                }
+            }
+        }
+    }
+
+    private void on_new_messages_changed(Folder folder, int count) {
+        if (count > 0) {
+            show_new_messages_count(folder, count);
+        } else {
+            remove_new_messages_count(folder);
+        }
+    }
+
+    private void check_folders(Gee.Collection<Folder> folders) {
+        foreach (Folder folder in folders) {
+            if (folder.folder_type == INBOX) {
+                this.notifications.start_monitoring_folder(folder);
+            } else if (this.notifications.is_monitoring_folder(folder)) {
+                this.notifications.stop_monitoring_folder(folder);
+            }
+        }
+    }
+
 }
diff --git a/src/client/plugin/notification-badge/notification-badge.vala 
b/src/client/plugin/notification-badge/notification-badge.vala
index b3b343a7..661ad662 100644
--- a/src/client/plugin/notification-badge/notification-badge.vala
+++ b/src/client/plugin/notification-badge/notification-badge.vala
@@ -1,6 +1,6 @@
 /*
- * Copyright 2016 Software Freedom Conservancy Inc.
- * Copyright 2019 Michael Gratton <mike vee net>.
+ * Copyright © 2016 Software Freedom Conservancy Inc.
+ * Copyright © 2019-2020 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.
@@ -19,21 +19,22 @@ public void peas_register_types(TypeModule module) {
 public class Plugin.NotificationBadge : Geary.BaseObject, Notification {
 
 
-    public Application.Client application {
-        get; construct set;
-    }
+    private const Geary.SpecialFolderType[] MONITORED_TYPES = {
+        INBOX, NONE
+    };
 
-    public Application.NotificationContext context {
-        get; construct set;
+    public global::Application.NotificationContext notifications {
+        get; set;
     }
 
     private UnityLauncherEntry? entry = null;
 
 
     public override void activate() {
-        var connection = this.application.get_dbus_connection();
-        var path = this.application.get_dbus_object_path();
         try {
+            var application = this.notifications.get_client_application();
+            var connection = application.get_dbus_connection();
+            var path = application.get_dbus_object_path();
             if (connection == null || path == null) {
                 throw new GLib.IOError.NOT_CONNECTED(
                     "Application does not have a DBus connection or path"
@@ -42,7 +43,7 @@ public class Plugin.NotificationBadge : Geary.BaseObject, Notification {
             this.entry = new UnityLauncherEntry(
                 connection,
                 path + "/plugin/notificationbadge",
-                Application.Client.APP_ID + ".desktop"
+                global::Application.Client.APP_ID + ".desktop"
             );
         } catch (GLib.Error error) {
             warning(
@@ -51,18 +52,53 @@ public class Plugin.NotificationBadge : Geary.BaseObject, Notification {
             );
         }
 
-        this.context.notify["total-new-messages"].connect(on_total_changed);
-        update_count();
+        connect_folders.begin();
     }
 
     public override void deactivate(bool is_shutdown) {
-        this.context.notify["total-new-messages"].disconnect(on_total_changed);
+        this.notifications.notify["total-new-messages"].disconnect(
+            on_total_changed
+        );
         this.entry = null;
     }
 
+    public async void connect_folders() {
+        try {
+            FolderStore folders = yield this.notifications.get_folders();
+            folders.folders_available.connect(
+                (folders) => check_folders(folders)
+            );
+            folders.folders_unavailable.connect(
+                (folders) => check_folders(folders)
+            );
+            folders.folders_type_changed.connect(
+                (folders) => check_folders(folders)
+            );
+            check_folders(folders.get_folders());
+        } catch (GLib.Error error) {
+            warning(
+                "Unable to get folders for plugin: %s",
+                error.message
+            );
+        }
+
+        this.notifications.notify["total-new-messages"].connect(on_total_changed);
+        update_count();
+    }
+
+    private void check_folders(Gee.Collection<Folder> folders) {
+        foreach (Folder folder in folders) {
+            if (folder.folder_type in MONITORED_TYPES) {
+                this.notifications.start_monitoring_folder(folder);
+            } else {
+                this.notifications.stop_monitoring_folder(folder);
+            }
+        }
+    }
+
     private void update_count() {
         if (this.entry != null) {
-            int count = this.context.total_new_messages;
+            int count = this.notifications.total_new_messages;
             if (count > 0) {
                 this.entry.set_count(count);
             } else {
diff --git a/src/client/plugin/plugin-notification.vala b/src/client/plugin/plugin-notification.vala
index fd409644..b00549f9 100644
--- a/src/client/plugin/plugin-notification.vala
+++ b/src/client/plugin/plugin-notification.vala
@@ -10,14 +10,9 @@
  */
 public interface Plugin.Notification : Geary.BaseObject {
 
-    /** The application instance containing the plugin. */
-    public abstract Application.Client application {
-        get; construct set;
-    }
-
     /** Context object for notifications. */
-    public abstract Application.NotificationContext context {
-        get; construct set;
+    public abstract global::Application.NotificationContext notifications {
+        get; set;
     }
 
     /* Invoked to activate the plugin, after loading. */


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