Files
mailnag/Mailnag/common/plugins.py
Patrick Ulbrich 3a9bd9f34b Use proper enums
2019-11-04 20:11:38 +01:00

278 lines
6.9 KiB
Python

# Copyright 2013 - 2019 Patrick Ulbrich <zulu99@gmx.net>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
import os
import imp
import inspect
import logging
from enum import Enum
from Mailnag.common.config import cfg_folder
from Mailnag.common.dist_cfg import LIB_DIR
PLUGIN_LIB_PATH = os.path.join(LIB_DIR, 'plugins')
PLUGIN_USER_PATH = os.path.join(cfg_folder, 'plugins')
PLUGIN_PATHS = [ PLUGIN_LIB_PATH, PLUGIN_USER_PATH ]
#
# All known hook types.
#
class HookTypes(Enum):
# func signature:
# IN: List of loaded accounts
# OUT: None
ACCOUNTS_LOADED = 'accounts-loaded'
# func signature:
# IN: None
# OUT: None
MAIL_CHECK = 'mail-check'
# func signature:
# IN: new mails, all mails
# OUT: None
MAILS_ADDED = 'mails-added'
# func signature:
# IN: remaining mails
# OUT: None
MAILS_REMOVED = 'mails-removed'
# func signature:
# IN: all mails
# OUT: filtered mails
FILTER_MAILS = 'filter-mails'
#
# Registry class for plugin hooks.
#
# Registered hook functions must not block the mailnag daemon.
# Hook functions with an execution time > 1s should be
# implemented non-blocking (i. e. asynchronously).
class HookRegistry:
def __init__(self):
self._hooks = {
HookTypes.ACCOUNTS_LOADED : [],
HookTypes.MAIL_CHECK : [],
HookTypes.MAILS_ADDED : [],
HookTypes.MAILS_REMOVED : [],
HookTypes.FILTER_MAILS : []
}
# Priority should be an integer value fom 0 (very high) to 100 (very low)
# Plugin hooks will be called in order from high to low priority.
def register_hook_func(self, hooktype, func, priority = 100):
self._hooks[hooktype].append( (priority, func) )
def unregister_hook_func(self, hooktype, func):
pairs = self._hooks[hooktype]
pair = next(pa for pa in pairs if (pa[1] == func))
pairs.remove(pair)
def get_hook_funcs(self, hooktype):
pairs_by_prio = sorted(self._hooks[hooktype], key = lambda p: p[0])
funcs = [ f for p, f in pairs_by_prio ]
return funcs
# Abstract base class for a MailnagController instance
# passed to plugins.
class MailnagController:
# Returns a HookRegistry object.
def get_hooks(self): pass
# Shuts down the Mailnag process.
# May throw an InvalidOperationException.
def shutdown(self): pass
# Enforces a manual mail check.
# May throw an InvalidOperationException.
def check_for_mails(self): pass
# Marks the mail with specified mail_id as read.
# May throw an InvalidOperationException.
def mark_mail_as_read(self, mail_id): pass
#
# Mailnag Plugin base class
#
class Plugin:
def __init__(self):
# Plugins shouldn't do anything in the constructor.
# They are expected to start living if they are actually
# enabled (i.e. in the enable() method).
# Plugin data isn't enabled yet and call to methods like
# get_mailnag_controller() or get_config().
pass
#
# Abstract methods,
# to be overriden by derived plugin types.
#
def enable(self):
# Plugins are expected to
# register all hooks here.
raise NotImplementedError
def disable(self):
# Plugins are expected to
# unregister all hooks here,
# free all allocated resources,
# and terminate threads (if any).
raise NotImplementedError
def get_manifest(self):
# Plugins are expected to
# return a tuple of the following form:
# (name, description, version, author, mandatory).
raise NotImplementedError
def get_default_config(self):
# Plugins are expected to return a
# dictionary with default values.
raise NotImplementedError
def has_config_ui(self):
# Plugins are expected to return True if
# they provide a configuration widget,
# otherwise they must return False.
raise NotImplementedError
def get_config_ui(self):
# Plugins are expected to
# return a GTK widget here.
# Return None if the plugin
# does not need a config widget.
raise NotImplementedError
def load_ui_from_config(self, config_ui):
# Plugins are expected to
# load their config values (get_config())
# in the widget returned by get_config_ui().
raise NotImplementedError
def save_ui_to_config(self, config_ui):
# Plugins are expected to
# save the config values of the widget
# returned by get_config_ui() to their config
# (get_config()).
raise NotImplementedError
#
# Public methods
#
def init(self, modname, cfg, mailnag_controller):
config = {}
# try to load plugin config
if cfg.has_section(modname):
for name, value in cfg.items(modname):
config[name] = value
# sync with default config
default_config = self.get_default_config()
for k, v in default_config.items():
if k not in config:
config[k] = v
self._modname = modname
self._config = config
self._mailnag_controller = mailnag_controller
def get_name(self):
name = self.get_manifest()[0]
return name
def get_modname(self):
return self._modname
def get_config(self):
return self._config
#
# Protected methods
#
def get_mailnag_controller(self):
return self._mailnag_controller
#
# Static methods
#
# Note : Plugin instances do not own
# a reference to MailnagController object
# when instantiated in *config mode*.
@staticmethod
def load_plugins(cfg, mailnag_controller = None, filter_names = None):
plugins = []
plugin_types = Plugin._load_plugin_types()
for modname, t in plugin_types:
try:
if (filter_names == None) or (modname in filter_names):
p = t()
p.init(modname, cfg, mailnag_controller)
plugins.append(p)
except:
logging.exception("Failed to instantiate plugin '%s'" % modname)
return plugins
@staticmethod
def _load_plugin_types():
plugin_types = []
for path in PLUGIN_PATHS:
if not os.path.exists(path):
continue
for f in os.listdir(path):
mod = None
modname, ext = os.path.splitext(f)
try:
if ext.lower() == '.py':
if not os.path.exists(os.path.join(path, modname + '.pyc')):
mod = imp.load_source(modname, os.path.join(path, f))
elif ext.lower() == '.pyc':
mod = imp.load_compiled(modname, os.path.join(path, f))
if mod != None:
for t in dir(mod):
t = getattr(mod, t)
if inspect.isclass(t) and \
(inspect.getmodule(t) == mod) and \
issubclass(t, Plugin):
plugin_types.append((modname, t))
except:
logging.exception("Error while opening plugin file '%s'" % f)
return plugin_types