diff --git a/po/POTFILES.in b/po/POTFILES.in index 7f3178e5..10fdcaf5 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -84,20 +84,23 @@ src/client/folder-list/folder-list-inboxes-branch.vala src/client/folder-list/folder-list-search-branch.vala src/client/folder-list/folder-list-special-grouping.vala src/client/folder-list/folder-list-tree.vala +src/client/plugin/plugin-account.vala +src/client/plugin/plugin-application.vala +src/client/plugin/plugin-contact-store.vala +src/client/plugin/plugin-email-store.vala +src/client/plugin/plugin-email.vala +src/client/plugin/plugin-error.vala +src/client/plugin/plugin-folder-store.vala +src/client/plugin/plugin-folder.vala +src/client/plugin/plugin-notification-etension.vala +src/client/plugin/plugin-plugin-base.vala +src/client/plugin/plugin-trusted-etension.vala src/client/plugin/desktop-notifications/desktop-notifications.plugin.in src/client/plugin/desktop-notifications/desktop-notifications.vala src/client/plugin/messaging-menu/messaging-menu.plugin.in src/client/plugin/messaging-menu/messaging-menu.vala src/client/plugin/notification-badge/notification-badge.plugin.in src/client/plugin/notification-badge/notification-badge.vala -src/client/plugin/plugin-account.vala -src/client/plugin/plugin-application.vala -src/client/plugin/plugin-contact-store.vala -src/client/plugin/plugin-email-store.vala -src/client/plugin/plugin-email.vala -src/client/plugin/plugin-folder-store.vala -src/client/plugin/plugin-folder.vala -src/client/plugin/plugin-notification.vala src/client/sidebar/sidebar-branch.vala src/client/sidebar/sidebar-common.vala src/client/sidebar/sidebar-count-cell-renderer.vala diff --git a/src/client/application/application-notification-context.vala b/src/client/application/application-notification-context.vala index 2152243e..3c4c018a 100644 --- a/src/client/application/application-notification-context.vala +++ b/src/client/application/application-notification-context.vala @@ -7,51 +7,15 @@ */ /** - * Provides a context for notification plugins. - * - * The context provides an interface for notification plugins to - * interface with the Geary client application. Plugins that implement - * the plugins will be passed an instance of this class as the - * `context` property. - * - * 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 + * Implementation of the notification extension context. */ -public class Application.NotificationContext : Geary.BaseObject { +internal class Application.NotificationContext : + Geary.BaseObject, Plugin.NotificationContext { 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 { @@ -248,23 +212,8 @@ public class Application.NotificationContext : Geary.BaseObject { } } - /** - * Returns the plugin application object. - * - * No special permissions are required to use access this. - */ - public Plugin.Application plugin_application { - get; private set; - } - - /** - * Current total new message count for all monitored folders. - * - * 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 int total_new_messages { get; private set; default = 0; } + public int total_new_messages { get { return this._total_new_messages; } } + public int _total_new_messages = 0; private Gee.Map folder_information = new Gee.HashMap(); @@ -273,84 +222,28 @@ public class Application.NotificationContext : Geary.BaseObject { private FolderStoreFactory folders_factory; private Plugin.FolderStore folders; private EmailStoreImpl email; - private PluginManager.PluginFlags flags; - /** - * 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 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); - - - /** Constructs a new context instance. */ internal NotificationContext(Client application, - FolderStoreFactory folders_factory, - PluginManager.PluginFlags flags) { + FolderStoreFactory folders_factory) { 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; - } - - /** - * 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 { + throws Plugin.Error.PERMISSION_DENIED { return this.email; } - /** - * 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.FolderStore get_folders() + throws Plugin.Error.PERMISSION_DENIED { + return this.folders; + } + public async Plugin.ContactStore get_contacts_for_folder(Plugin.Folder source) - throws Geary.EngineError.NOT_FOUND, - Geary.EngineError.PERMISSIONS { + throws Plugin.Error.NOT_FOUND, Plugin.Error.PERMISSION_DENIED { Geary.Folder? folder = this.folders_factory.get_engine_folder(source); AccountContext? context = null; if (folder != null) { @@ -359,30 +252,13 @@ public class Application.NotificationContext : Geary.BaseObject { ); } if (context == null) { - throw new Geary.EngineError.NOT_FOUND( + throw new Plugin.Error.NOT_FOUND( "No account for folder: %s", source.display_name ); } return new ContactStoreImpl(context.contacts); } - /** - * 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; - } - /** * Determines if notifications should be made for a specific folder. * @@ -417,14 +293,14 @@ public class Application.NotificationContext : Geary.BaseObject { * folder by a call to {@link start_monitoring_folder}. */ public int get_new_message_count(Plugin.Folder target) - throws Geary.EngineError.NOT_FOUND { + throws Plugin.Error.NOT_FOUND { 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( + throw new Plugin.Error.NOT_FOUND( "No such folder: %s", folder.path.to_string() ); } @@ -534,18 +410,19 @@ public class Application.NotificationContext : Geary.BaseObject { Plugin.Folder folder = this.folders_factory.get_plugin_folder(info.folder); if (arrived) { - this.total_new_messages += delta.size; + 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; + this._total_new_messages -= delta.size; new_messages_retired( folder, info.recent_ids.size ); } + notify_property("total-new-messages"); } private void remove_folder(Geary.Folder target) { @@ -555,7 +432,10 @@ public class Application.NotificationContext : Geary.BaseObject { target.email_flags_changed.disconnect(on_email_flags_changed); target.email_removed.disconnect(on_email_removed); - this.total_new_messages -= info.recent_ids.size; + if (!info.recent_ids.is_empty) { + this._total_new_messages -= info.recent_ids.size; + notify_property("total-new-messages"); + } this.folder_information.unset(target); } diff --git a/src/client/application/application-plugin-manager.vala b/src/client/application/application-plugin-manager.vala index 3f3bcba7..0299727c 100644 --- a/src/client/application/application-plugin-manager.vala +++ b/src/client/application/application-plugin-manager.vala @@ -11,18 +11,34 @@ public class Application.PluginManager : GLib.Object { - // Plugins that will be loaded automatically and trusted with - // access to the application if they have been installed - private const string[] TRUSTED_MODULES = { + // Plugins that will be loaded automatically when the client + // application stats up + private const string[] AUTOLOAD_MODULES = { "desktop-notifications", "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 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); + } + } + } @@ -33,9 +49,10 @@ public class Application.PluginManager : GLib.Object { private FolderStoreFactory folders_factory; - private Peas.ExtensionSet notification_extensions; - private Gee.Set notification_contexts = - new Gee.HashSet(); + private Gee.Map plugin_set = + new Gee.HashMap(); + private Gee.Map notification_contexts = + new Gee.HashMap(); public PluginManager(Client application) throws GLib.Error { @@ -46,40 +63,16 @@ public class Application.PluginManager : GLib.Object { this.trusted_path = application.get_app_plugins_dir().get_path(); this.plugins.add_search_path(trusted_path, null); - this.notification_extensions = new Peas.ExtensionSet( - 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(); - } - }); - this.notification_extensions.extension_removed.connect((info, extension) => { - Plugin.Notification? plugin = extension as Plugin.Notification; - if (plugin != null) { - plugin.deactivate(this.is_shutdown); - } - var context = plugin.notifications; - context.destroy(); - this.notification_contexts.remove(context); - }); + this.plugins.load_plugin.connect_after(on_load_plugin); + this.plugins.unload_plugin.connect(on_unload_plugin); string[] optional_names = application.config.get_optional_plugins(); 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); + if (is_autoload(info)) { + debug("Loading autoload plugin: %s", name); this.plugins.load_plugin(info); } else if (name in optional_names) { debug("Loading optional plugin: %s", name); @@ -92,15 +85,9 @@ public class Application.PluginManager : GLib.Object { } } - public inline bool is_trusted(Peas.PluginInfo plugin) { - return ( - plugin.get_module_name() in TRUSTED_MODULES && - plugin.get_module_dir().has_prefix(trusted_path) - ); - } - - public inline PluginFlags to_plugin_flags(Peas.PluginInfo plugin) { - return is_trusted(plugin) ? PluginFlags.TRUSTED : 0; + /** Returns the engine folder for the given plugin folder, if any. */ + public Geary.Folder? get_engine_folder(Plugin.Folder plugin) { + return this.folders_factory.get_engine_folder(plugin); } public Gee.Collection get_optional_plugins() { @@ -108,7 +95,7 @@ public class Application.PluginManager : GLib.Object { foreach (Peas.PluginInfo plugin in this.plugins.get_plugin_list()) { try { plugin.is_available(); - if (!is_trusted(plugin)) { + if (!is_autoload(plugin)) { plugins.add(plugin); } } catch (GLib.Error err) { @@ -125,7 +112,7 @@ public class Application.PluginManager : GLib.Object { bool loaded = false; if (plugin.is_available() && !plugin.is_loaded() && - !is_trusted(plugin)) { + !is_autoload(plugin)) { this.plugins.load_plugin(plugin); loaded = true; string name = plugin.get_module_name(); @@ -143,7 +130,7 @@ public class Application.PluginManager : GLib.Object { bool unloaded = false; if (plugin.is_available() && plugin.is_loaded() && - !is_trusted(plugin)) { + !is_autoload(plugin)) { this.plugins.unload_plugin(plugin); unloaded = true; string name = plugin.get_module_name(); @@ -163,11 +150,75 @@ public class Application.PluginManager : GLib.Object { internal void close() throws GLib.Error { this.is_shutdown = true; this.plugins.set_loaded_plugins(null); + this.plugins.garbage_collect(); this.folders_factory.destroy(); } + internal inline bool is_autoload(Peas.PluginInfo info) { + return info.get_module_name() in AUTOLOAD_MODULES; + } + internal Gee.Collection get_notification_contexts() { - return this.notification_contexts.read_only_view; + return this.notification_contexts.values.read_only_view; + } + + private void on_load_plugin(Peas.PluginInfo info) { + var plugin = this.plugins.create_extension( + info, + typeof(Plugin.PluginBase), + "plugin_application", + new ApplicationImpl(this.application, this.folders_factory) + ) as Plugin.PluginBase; + if (plugin != null) { + bool do_activate = true; + var trusted = plugin as Plugin.TrustedExtension; + if (trusted != null) { + if (info.get_module_dir().has_prefix(this.trusted_path)) { + trusted.client_application = this.application; + trusted.client_plugins = this; + } else { + do_activate = false; + this.plugins.unload_plugin(info); + } + } + + var notification = plugin as Plugin.NotificationExtension; + if (notification != null) { + var context = new NotificationContext( + this.application, + this.folders_factory + ); + this.notification_contexts.set(info, context); + notification.notifications = context; + } + + if (do_activate) { + this.plugin_set.set(info, plugin); + plugin.activate(); + } + } else { + warning( + "Could not construct BasePlugin from %s", info.get_module_name() + ); + } + } + + private void on_unload_plugin(Peas.PluginInfo info) { + var plugin = this.plugin_set.get(info); + if (plugin != null) { + plugin.deactivate(this.is_shutdown); + + var notification = plugin as Plugin.NotificationExtension; + if (notification != null) { + var context = this.notification_contexts.get(info); + if (context != null) { + this.notification_contexts.unset(info); + context.destroy(); + } + } + + this.plugin_set.unset(info); + } } } diff --git a/src/client/meson.build b/src/client/meson.build index da3e81e5..d6dd9a4a 100644 --- a/src/client/meson.build +++ b/src/client/meson.build @@ -1,4 +1,5 @@ # Geary client + geary_client_vala_sources = files( 'application/application-attachment-manager.vala', 'application/application-avatar-store.vala', @@ -97,9 +98,12 @@ geary_client_vala_sources = files( 'plugin/plugin-contact-store.vala', 'plugin/plugin-email-store.vala', 'plugin/plugin-email.vala', + 'plugin/plugin-error.vala', 'plugin/plugin-folder-store.vala', 'plugin/plugin-folder.vala', - 'plugin/plugin-notification.vala', + 'plugin/plugin-notification-extension.vala', + 'plugin/plugin-plugin-base.vala', + 'plugin/plugin-trusted-extension.vala', 'sidebar/sidebar-branch.vala', 'sidebar/sidebar-common.vala', diff --git a/src/client/plugin/desktop-notifications/desktop-notifications.vala b/src/client/plugin/desktop-notifications/desktop-notifications.vala index 95c0c9bc..24769fc8 100644 --- a/src/client/plugin/desktop-notifications/desktop-notifications.vala +++ b/src/client/plugin/desktop-notifications/desktop-notifications.vala @@ -10,7 +10,7 @@ public void peas_register_types(TypeModule module) { Peas.ObjectModule obj = module as Peas.ObjectModule; obj.register_extension_type( - typeof(Plugin.Notification), + typeof(Plugin.PluginBase), typeof(Plugin.DesktopNotifications) ); } @@ -18,35 +18,34 @@ public void peas_register_types(TypeModule module) { /** * Manages standard desktop application notifications. */ -public class Plugin.DesktopNotifications : Geary.BaseObject, Notification { +public class Plugin.DesktopNotifications : + PluginBase, NotificationExtension, TrustedExtension { private const Geary.SpecialFolderType[] MONITORED_TYPES = { INBOX, NONE }; - public global::Application.NotificationContext notifications { - get; set; + public NotificationContext notifications { + get; set construct; + } + + public global::Application.Client client_application { + get; set construct; + } + + public global::Application.PluginManager client_plugins { + get; set construct; } 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() { - 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(); @@ -94,7 +93,7 @@ public class Plugin.DesktopNotifications : Geary.BaseObject, Notification { } private void clear_arrived_notification() { - this.application.withdraw_notification(ARRIVED_ID); + this.client_application.withdraw_notification(ARRIVED_ID); this.arrived_notification = null; } @@ -206,8 +205,8 @@ public class Plugin.DesktopNotifications : Geary.BaseObject, Notification { // 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); + if (this.client_application.config.desktop_environment == UNITY) { + this.client_application.send_notification(id, notification); return notification; } else { if (action != null) { @@ -216,7 +215,7 @@ public class Plugin.DesktopNotifications : Geary.BaseObject, Notification { ); } - this.application.send_notification(id, notification); + this.client_application.send_notification(id, notification); return notification; } } diff --git a/src/client/plugin/messaging-menu/messaging-menu.vala b/src/client/plugin/messaging-menu/messaging-menu.vala index a97840cd..29f65fdd 100644 --- a/src/client/plugin/messaging-menu/messaging-menu.vala +++ b/src/client/plugin/messaging-menu/messaging-menu.vala @@ -10,17 +10,17 @@ public void peas_register_types(TypeModule module) { Peas.ObjectModule obj = module as Peas.ObjectModule; obj.register_extension_type( - typeof(Plugin.Notification), + typeof(Plugin.PluginBase), typeof(Plugin.MessagingMenu) ); } /** Updates the Unity messaging menu when new mail arrives. */ -public class Plugin.MessagingMenu : Geary.BaseObject, Notification { +public class Plugin.MessagingMenu : PluginBase, NotificationExtension { - public global::Application.NotificationContext notifications { - get; set; + public NotificationContext notifications { + get; set construct; } @@ -101,7 +101,7 @@ public class Plugin.MessagingMenu : Geary.BaseObject, Notification { 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); + this.plugin_application.show_folder(folder); break; } } diff --git a/src/client/plugin/notification-badge/notification-badge.vala b/src/client/plugin/notification-badge/notification-badge.vala index 661ad662..58438f5c 100644 --- a/src/client/plugin/notification-badge/notification-badge.vala +++ b/src/client/plugin/notification-badge/notification-badge.vala @@ -10,21 +10,30 @@ public void peas_register_types(TypeModule module) { Peas.ObjectModule obj = module as Peas.ObjectModule; obj.register_extension_type( - typeof(Plugin.Notification), + typeof(Plugin.PluginBase), typeof(Plugin.NotificationBadge) ); } /** Updates Unity application badge with total new message count. */ -public class Plugin.NotificationBadge : Geary.BaseObject, Notification { +public class Plugin.NotificationBadge : + PluginBase, NotificationExtension, TrustedExtension { private const Geary.SpecialFolderType[] MONITORED_TYPES = { INBOX, NONE }; - public global::Application.NotificationContext notifications { - get; set; + public NotificationContext notifications { + get; set construct; + } + + public global::Application.Client client_application { + get; set construct; + } + + public global::Application.PluginManager client_plugins { + get; set construct; } private UnityLauncherEntry? entry = null; @@ -32,9 +41,8 @@ public class Plugin.NotificationBadge : Geary.BaseObject, Notification { public override void activate() { try { - var application = this.notifications.get_client_application(); - var connection = application.get_dbus_connection(); - var path = application.get_dbus_object_path(); + var connection = this.client_application.get_dbus_connection(); + var path = this.client_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" diff --git a/src/client/plugin/plugin-error.vala b/src/client/plugin/plugin-error.vala new file mode 100644 index 00000000..67fae4b9 --- /dev/null +++ b/src/client/plugin/plugin-error.vala @@ -0,0 +1,27 @@ +/* + * Copyright © 2020 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * The base class for objects implementing a client plugin. + * + * To implement a new plugin, have it derive from this type and + * implement any additional extension interfaces (such as {@link + * NotificationExtension}) as required. + */ + +/** + * Errors when plugins request resources from their contexts. + */ +public errordomain Plugin.Error { + + /** Raised when access to a requested resource was denied. */ + PERMISSION_DENIED, + + /** Raised when a requested resource was not found. */ + NOT_FOUND; + +} diff --git a/src/client/plugin/plugin-notification-extension.vala b/src/client/plugin/plugin-notification-extension.vala new file mode 100644 index 00000000..230e98c7 --- /dev/null +++ b/src/client/plugin/plugin-notification-extension.vala @@ -0,0 +1,148 @@ +/* + * Copyright © 2019-2020 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * A plugin extension point for notifying of mail sending or arriving. + */ +public interface Plugin.NotificationExtension : PluginBase { + + /** + * Context object for notifications. + * + * This will be set during (or just after) plugin construction, + * before {@link PluginBase.activate} is called. + */ + public abstract NotificationContext notifications { + get; set construct; + } + +} + + +// XXX this should be an inner interface of NotificationExtension, but +// GNOME/vala#918 prevents that. + +/** + * Provides a context for notification plugins. + * + * The context provides an interface for notification plugins to + * interface with the Geary client application. Plugins that implement + * the plugins will be passed an instance of this class as the + * `context` property. + * + * 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.NotificationExtension.notifications + */ +public interface Plugin.NotificationContext : Geary.BaseObject { + + + /** + * Current total new message count for all monitored folders. + * + * 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 abstract int total_new_messages { get; default = 0; } + + /** + * 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 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); + + + /** + * Returns a store to lookup email for notifications. + * + * This method may prompt for permission before returning. + * + * @throws Error.PERMISSIONS if permission to access + * this resource was not given + */ + public abstract async Plugin.EmailStore get_email() + throws Error.PERMISSION_DENIED; + + /** + * Returns a store to lookup folders for notifications. + * + * This method may prompt for permission before returning. + * + * @throws Error.PERMISSIONS if permission to access + * this resource was not given + */ + public abstract async Plugin.FolderStore get_folders() + throws Error.PERMISSION_DENIED; + + /** + * Returns a store to lookup contacts for notifications. + * + * This method may prompt for permission before returning. + * + * @throws Error.NOT_FOUND if the given account does + * not exist + * @throws Error.PERMISSIONS if permission to access + * this resource was not given + */ + public abstract async Plugin.ContactStore get_contacts_for_folder(Plugin.Folder source) + throws Error.NOT_FOUND, Error.PERMISSION_DENIED; + + /** + * 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 abstract bool should_notify_new_messages(Plugin.Folder target); + + /** + * 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 abstract int get_new_message_count(Plugin.Folder target) + throws Error.NOT_FOUND; + + /** + * Starts monitoring a folder for new messages. + * + * Notification plugins should call this to start the context + * recording new messages for a specific folder. + */ + public abstract void start_monitoring_folder(Plugin.Folder target); + + /** Stops monitoring a folder for new messages. */ + public abstract void stop_monitoring_folder(Plugin.Folder target); + + /** Determines if a folder is curently being monitored. */ + public abstract bool is_monitoring_folder(Plugin.Folder target); + +} diff --git a/src/client/plugin/plugin-notification.vala b/src/client/plugin/plugin-notification.vala deleted file mode 100644 index b00549f9..00000000 --- a/src/client/plugin/plugin-notification.vala +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2019 Michael Gratton - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ - -/** - * A plugin extension point for notifying of mail sending or arriving. - */ -public interface Plugin.Notification : Geary.BaseObject { - - /** Context object for notifications. */ - public abstract global::Application.NotificationContext notifications { - get; set; - } - - /* Invoked to activate the plugin, after loading. */ - public abstract void activate(); - - /* Invoked to deactivate the plugin, prior to unloading */ - public abstract void deactivate(bool is_shutdown); - -} diff --git a/src/client/plugin/plugin-plugin-base.vala b/src/client/plugin/plugin-plugin-base.vala new file mode 100644 index 00000000..c7f0c5b2 --- /dev/null +++ b/src/client/plugin/plugin-plugin-base.vala @@ -0,0 +1,36 @@ +/* + * Copyright © 2020 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * The base class for objects implementing a client plugin. + * + * To implement a new plugin, have it derive from this type and + * implement any additional extension interfaces (such as {@link + * NotificationExtension}) as required. + */ +public abstract class Plugin.PluginBase : Geary.BaseObject { + + /** + * Returns an object for interacting with the client application. + * + * No special permissions are required to use access this + * resource. + * + * This will be set during (or just after) plugin construction, + * before {@link PluginBase.activate} is called. + */ + public Plugin.Application plugin_application { + get; construct; + } + + /** Invoked to activate the plugin, after loading. */ + public abstract void activate(); + + /** Invoked to deactivate the plugin, prior to unloading */ + public abstract void deactivate(bool is_shutdown); + +} diff --git a/src/client/plugin/plugin-trusted-extension.vala b/src/client/plugin/plugin-trusted-extension.vala new file mode 100644 index 00000000..fc0d9bd3 --- /dev/null +++ b/src/client/plugin/plugin-trusted-extension.vala @@ -0,0 +1,40 @@ +/* + * Copyright © 2020 Michael Gratton + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +/** + * A plugin extension point for trusted plugins. + * + * In-tree plugins may implement this interface if they require access + * to the client application's internal machinery. + * + * Since the client application and engine objects have no API + * stability guarantee, Geary will refuse to load out-of-tree plugins + * that implement this extension point. + */ +public interface Plugin.TrustedExtension : PluginBase { + + /** + * Client application object. + * + * This will be set during (or just after) plugin construction, + * before {@link PluginBase.activate} is called. + */ + public abstract global::Application.Client client_application { + get; set construct; + } + + /** + * Client plugin manager object. + * + * This will be set during (or just after) plugin construction, + * before {@link PluginBase.activate} is called. + */ + public abstract global::Application.PluginManager client_plugins { + get; set construct; + } + +}