summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/mailman/app/docs/plugins.rst173
-rw-r--r--src/mailman/commands/docs/conf.rst1
-rw-r--r--src/mailman/config/config.py1
-rw-r--r--src/mailman/config/schema.cfg12
-rw-r--r--src/mailman/core/initialize.py29
-rw-r--r--src/mailman/core/plugins.py42
-rw-r--r--src/mailman/interfaces/plugin.py32
-rw-r--r--src/mailman/rest/docs/systemconf.rst2
-rw-r--r--src/mailman/rest/tests/test_systemconf.py4
-rw-r--r--src/mailman/testing/plugin.py36
-rw-r--r--src/mailman/testing/testing.cfg4
11 files changed, 249 insertions, 87 deletions
diff --git a/src/mailman/app/docs/plugins.rst b/src/mailman/app/docs/plugins.rst
index ba9bb249e..bff079544 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,114 @@ 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-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 +127,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,
@@ -69,50 +157,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 6273c3b1c..f465aab53 100644
--- a/src/mailman/commands/docs/conf.rst
+++ b/src/mailman/commands/docs/conf.rst
@@ -58,7 +58,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 a41d86169..4ae6bfd65 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
def _clear(self):
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