diff options
| author | J08nY | 2017-06-01 15:46:48 +0200 |
|---|---|---|
| committer | J08nY | 2017-08-07 17:39:07 +0200 |
| commit | 0d4f53b51892866b5cc85ace229b23f4b9bac896 (patch) | |
| tree | 5394af7e3d9e8895babf93cf4af19480d14f53dd | |
| parent | 38a86adcdb78c1944c26a5ab8deddff619b33bcf (diff) | |
| download | mailman-0d4f53b51892866b5cc85ace229b23f4b9bac896.tar.gz mailman-0d4f53b51892866b5cc85ace229b23f4b9bac896.tar.zst mailman-0d4f53b51892866b5cc85ace229b23f4b9bac896.zip | |
| -rw-r--r-- | src/mailman/app/docs/plugins.rst | 174 | ||||
| -rw-r--r-- | src/mailman/commands/docs/conf.rst | 1 | ||||
| -rw-r--r-- | src/mailman/config/config.py | 1 | ||||
| -rw-r--r-- | src/mailman/config/schema.cfg | 12 | ||||
| -rw-r--r-- | src/mailman/core/initialize.py | 29 | ||||
| -rw-r--r-- | src/mailman/core/plugins.py | 42 | ||||
| -rw-r--r-- | src/mailman/interfaces/plugin.py | 32 | ||||
| -rw-r--r-- | src/mailman/rest/docs/systemconf.rst | 2 | ||||
| -rw-r--r-- | src/mailman/rest/tests/test_systemconf.py | 4 | ||||
| -rw-r--r-- | src/mailman/testing/plugin.py | 36 | ||||
| -rw-r--r-- | src/mailman/testing/testing.cfg | 4 |
11 files changed, 250 insertions, 87 deletions
diff --git a/src/mailman/app/docs/plugins.rst b/src/mailman/app/docs/plugins.rst index 07d0ec33c..42038b654 100644 --- a/src/mailman/app/docs/plugins.rst +++ b/src/mailman/app/docs/plugins.rst @@ -1,10 +1,10 @@ -===== -Hooks -===== +======= +Plugins +======= -Mailman defines two initialization hooks, one which is run early in the -initialization process and the other run late in the initialization process. -Hooks name an importable callable so it must be accessible on ``sys.path``. +Mailman defines a plugin as a package accessible on ``sys.path`` that provides +components Mailman will use. Such add handlers, rules, chains, etc... +First we create an example plugin that provides one additional rule: :: >>> import os, sys @@ -12,27 +12,115 @@ Hooks name an importable callable so it must be accessible on ``sys.path``. >>> config_directory = os.path.dirname(config.filename) >>> sys.path.insert(0, config_directory) + >>> example_plugin_path = os.path.join(config_directory, 'example_plugin') + >>> rules_path = os.path.join(example_plugin_path, 'rules') + >>> os.makedirs(rules_path) + >>> open(os.path.join(example_plugin_path, '__init__.py'), 'a').close() + >>> open(os.path.join(rules_path, '__init__.py'), 'a').close() + >>> rule_path = os.path.join(rules_path, 'example_rule.py') + >>> with open(rule_path, 'w') as fp: + ... print("""\ + ... from mailman.interfaces.rules import IRule + ... from public import public + ... from zope.interface import implementer + ... + ... @public + ... @implementer(IRule) + ... class ExampleRule: + ... + ... name = 'example' + ... description = 'Example rule to show pluggable components.' + ... record = True + ... + ... def check(self, mlist, msg, msgdata): + ... return msg.original_size > 1024 + ... """, file=fp) + >>> fp.close() + +Then enable the example plugin in config. +:: + + >>> example_config = """\ + ... [plugin.example] + ... path: example_plugin + ... enable: yes + ... """ + >>> config.push('example_cfg', example_config) + +Now the `example` rule can be seen as an ``IRule`` component and will be used +when any chain uses a link called `example`. +:: + + >>> from mailman.interfaces.rules import IRule + >>> from mailman.utilities.plugins import find_pluggable_components + >>> rules = sorted([rule.name + ... for rule in find_pluggable_components('rules', IRule)]) + >>> for rule in rules: + ... print(rule) + administrivia + any + approved + banned-address + dmarc-mitigation + emergency + example + implicit-dest + loop + max-recipients + max-size + member-moderation + news-moderation + no-senders + no-subject + nonmember-moderation + suspicious-header + truth + + + >>> config.pop('example_cfg') + >>> from shutil import rmtree + >>> rmtree(example_plugin_path) + + +Hooks +===== + +Plugins can also add initialization hooks, which will be run during the +initialization process. By creating a class implementing the IPlugin interface. +One which is run early in the process and the other run late in the +initialization process. +:: + >>> hook_path = os.path.join(config_directory, 'hooks.py') >>> with open(hook_path, 'w') as fp: ... print("""\ + ... from mailman.interfaces.plugin import IPlugin + ... from public import public + ... from zope.interface import implementer + ... ... counter = 1 - ... def pre_hook(): - ... global counter - ... print('pre-hook:', counter) - ... counter += 1 ... - ... def post_hook(): - ... global counter - ... print('post-hook:', counter) - ... counter += 1 + ... @public + ... @implementer(IPlugin) + ... class ExamplePlugin: + ... + ... def pre_hook(self): + ... global counter + ... print('pre-hook:', counter) + ... counter += 1 + ... + ... def post_hook(self): + ... global counter + ... print('post-hook:', counter) + ... counter += 1 ... """, file=fp) >>> fp.close() +Running the hooks +----------------- -Pre-hook -======== - -We can set the pre-hook in the configuration file. +We can set the plugin class in the config file. +:: >>> config_path = os.path.join(config_directory, 'hooks.cfg') >>> with open(config_path, 'w') as fp: @@ -40,8 +128,9 @@ We can set the pre-hook in the configuration file. ... [meta] ... extends: test.cfg ... - ... [mailman] - ... pre_hook: hooks.pre_hook + ... [plugin.hook_example] + ... class: hooks.ExamplePlugin + ... enable: yes ... """, file=fp) The hooks are run in the second and third steps of initialization. However, @@ -72,50 +161,7 @@ script that will produce no output to force the hooks to run. >>> call() pre-hook: 1 + post-hook: 2 <BLANKLINE> >>> os.remove(config_path) - - -Post-hook -========= - -We can set the post-hook in the configuration file. -:: - - >>> with open(config_path, 'w') as fp: - ... print("""\ - ... [meta] - ... extends: test.cfg - ... - ... [mailman] - ... post_hook: hooks.post_hook - ... """, file=fp) - - >>> call() - post-hook: 1 - <BLANKLINE> - - >>> os.remove(config_path) - - -Running both hooks -================== - -We can set the pre- and post-hooks in the configuration file. -:: - - >>> with open(config_path, 'w') as fp: - ... print("""\ - ... [meta] - ... extends: test.cfg - ... - ... [mailman] - ... pre_hook: hooks.pre_hook - ... post_hook: hooks.post_hook - ... """, file=fp) - - >>> call() - pre-hook: 1 - post-hook: 2 - <BLANKLINE> diff --git a/src/mailman/commands/docs/conf.rst b/src/mailman/commands/docs/conf.rst index 6c7309773..1a6a4679d 100644 --- a/src/mailman/commands/docs/conf.rst +++ b/src/mailman/commands/docs/conf.rst @@ -50,7 +50,6 @@ key, along with the names of the corresponding sections. [logging.smtp] path: smtp.log [logging.subscribe] path: mailman.log [logging.vette] path: mailman.log - [plugin.master] path: If you specify both a section and a key, you will get the corresponding value. diff --git a/src/mailman/config/config.py b/src/mailman/config/config.py index 2ba657905..405b689ef 100644 --- a/src/mailman/config/config.py +++ b/src/mailman/config/config.py @@ -76,6 +76,7 @@ class Configuration: self.handlers = {} self.pipelines = {} self.commands = {} + self.plugins = {} self.password_context = None self.db = None diff --git a/src/mailman/config/schema.cfg b/src/mailman/config/schema.cfg index b708c6ceb..0e7936943 100644 --- a/src/mailman/config/schema.cfg +++ b/src/mailman/config/schema.cfg @@ -54,14 +54,6 @@ pending_request_life: 3d # How long should files be saved before they are evicted from the cache? cache_life: 7d -# A callable to run with no arguments early in the initialization process. -# This runs before database initialization. -pre_hook: - -# A callable to run with no arguments late in the initialization process. -# This runs after adapters are initialized. -post_hook: - # Which paths.* file system layout to use. layout: here @@ -93,9 +85,13 @@ listname_chars: [-_.0-9a-z] # - styles for IStyle path: +# The full import path for the class implementing IPlugin. +class: + # Whether to enable this plugin or not. enable: no + [shell] # `mailman shell` (also `withlist`) gives you an interactive prompt that you # can use to interact with an initialized and configured Mailman system. Use diff --git a/src/mailman/core/initialize.py b/src/mailman/core/initialize.py index dc67e9a68..18e874552 100644 --- a/src/mailman/core/initialize.py +++ b/src/mailman/core/initialize.py @@ -26,11 +26,11 @@ by the command line arguments. import os import sys +import warnings import mailman.config.config import mailman.core.logging from mailman.interfaces.database import IDatabaseFactory -from mailman.utilities.modules import call_name from pkg_resources import resource_string as resource_bytes from public import public from zope.component import getUtility @@ -133,7 +133,7 @@ def initialize_2(debug=False, propagate_logs=None, testing=False): * Database * Logging - * Pre-hook + * Plugin's pre_hook * Rules * Chains * Pipelines @@ -146,10 +146,17 @@ def initialize_2(debug=False, propagate_logs=None, testing=False): """ # Create the queue and log directories if they don't already exist. mailman.core.logging.initialize(propagate_logs) - # Run the pre-hook if there is one. + # Initialize plugins + from mailman.core.plugins import initialize as initialize_plugins + initialize_plugins() + # Run the plugin pre_hooks config = mailman.config.config - if config.mailman.pre_hook: - call_name(config.mailman.pre_hook) + if "pre_hook" in config.mailman: # pragma: no cover + warnings.warn( + "The pre_hook configuration value has been replaced by the " + "plugins infrastructure.", DeprecationWarning) + for plugin in config.plugins.values(): + plugin.pre_hook() # Instantiate the database class, ensure that it's of the right type, and # initialize it. Then stash the object on our configuration object. utility_name = ('testing' if testing else 'production') @@ -171,12 +178,16 @@ def initialize_2(debug=False, propagate_logs=None, testing=False): def initialize_3(): """Third initialization step. - * Post-hook + * Plugin's post_hook """ - # Run the post-hook if there is one. + # Run the plugin post_hooks config = mailman.config.config - if config.mailman.post_hook: - call_name(config.mailman.post_hook) + if "post_hook" in config.mailman: # pragma: no cover + warnings.warn( + "The post_hook configuration value has been replaced by the " + "plugins infrastructure.", DeprecationWarning) + for plugin in config.plugins.values(): + plugin.post_hook() @public diff --git a/src/mailman/core/plugins.py b/src/mailman/core/plugins.py new file mode 100644 index 000000000..565c590b5 --- /dev/null +++ b/src/mailman/core/plugins.py @@ -0,0 +1,42 @@ +# Copyright (C) 2008-2017 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Various plugin helpers.""" + +from lazr.config import as_boolean +from mailman.config import config +from mailman.interfaces.plugin import IPlugin +from mailman.utilities.modules import find_name +from public import public +from zope.interface.verify import verifyObject + + +@public +def initialize(): + """Initialize all enabled plugins.""" + for plugin_config in config.plugin_configs: + plugin_class_path = plugin_config['class'] + if as_boolean(plugin_config.enable) and plugin_class_path: + plugin_class = find_name(plugin_class_path) + plugin = plugin_class() + verifyObject(IPlugin, plugin) + name = plugin_config.name.split('.')[-1] + plugin.name = name + assert plugin.name not in config.plugins, ( + 'Duplicate plugin "{}" found in {}'.format( + plugin.name, plugin_class)) + config.plugins[plugin.name] = plugin diff --git a/src/mailman/interfaces/plugin.py b/src/mailman/interfaces/plugin.py new file mode 100644 index 000000000..9e226824f --- /dev/null +++ b/src/mailman/interfaces/plugin.py @@ -0,0 +1,32 @@ +# Copyright (C) 2007-2017 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Interfaces for plugins.""" + +from public import public +from zope.interface import Interface + + +@public +class IPlugin(Interface): + """A plugin providing components and hooks.""" + + def pre_hook(): + """A plugin hook called in the first initialization step. Before DB.""" + + def post_hook(): + """A plugin hook called in the second initialization step. After DB.""" diff --git a/src/mailman/rest/docs/systemconf.rst b/src/mailman/rest/docs/systemconf.rst index a65b81ab8..f5c9b691a 100644 --- a/src/mailman/rest/docs/systemconf.rst +++ b/src/mailman/rest/docs/systemconf.rst @@ -24,8 +24,6 @@ You can also get all the values for a particular section, such as the listname_chars: [-_.0-9a-z] noreply_address: noreply pending_request_life: 3d - post_hook: - pre_hook: self_link: http://localhost:9001/3.0/system/configuration/mailman sender_headers: from from_ reply-to sender site_owner: noreply@example.com diff --git a/src/mailman/rest/tests/test_systemconf.py b/src/mailman/rest/tests/test_systemconf.py index 5acc38adb..42802068c 100644 --- a/src/mailman/rest/tests/test_systemconf.py +++ b/src/mailman/rest/tests/test_systemconf.py @@ -46,8 +46,6 @@ class TestSystemConfiguration(unittest.TestCase): listname_chars='[-_.0-9a-z]', noreply_address='noreply', pending_request_life='3d', - post_hook='', - pre_hook='', self_link='http://localhost:9001/3.0/system/configuration/mailman', sender_headers='from from_ reply-to sender', site_owner='noreply@example.com', @@ -182,7 +180,7 @@ class TestSystemConfiguration(unittest.TestCase): 'paths.here', 'paths.local', 'paths.testing', - 'plugin.master', + 'plugin.example', 'runner.archive', 'runner.bad', 'runner.bounces', diff --git a/src/mailman/testing/plugin.py b/src/mailman/testing/plugin.py new file mode 100644 index 000000000..75667844a --- /dev/null +++ b/src/mailman/testing/plugin.py @@ -0,0 +1,36 @@ +# Copyright (C) 2009-2017 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman 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 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman 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 +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Example plugin with hooks for testing purposes.""" + +from mailman.interfaces.plugin import IPlugin +from public import public +from zope.interface import implementer + + +@public +@implementer(IPlugin) +class ExamplePlugin: + + def __init__(self): + pass + + def pre_hook(self): + pass + + def post_hook(self): + pass diff --git a/src/mailman/testing/testing.cfg b/src/mailman/testing/testing.cfg index 94b4c07b9..9e17015ef 100644 --- a/src/mailman/testing/testing.cfg +++ b/src/mailman/testing/testing.cfg @@ -28,6 +28,10 @@ incoming: mailman.testing.mta.FakeMTA [passwords] configuration: python:mailman.testing.passlib +[plugin.example] +enable: yes +class: mailman.testing.plugin.ExamplePlugin + [webservice] port: 9001 |
