summaryrefslogtreecommitdiff
path: root/src/mailman/app/docs/plugins.rst
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/app/docs/plugins.rst')
-rw-r--r--src/mailman/app/docs/plugins.rst210
1 files changed, 210 insertions, 0 deletions
diff --git a/src/mailman/app/docs/plugins.rst b/src/mailman/app/docs/plugins.rst
new file mode 100644
index 000000000..4430179ae
--- /dev/null
+++ b/src/mailman/app/docs/plugins.rst
@@ -0,0 +1,210 @@
+=======
+Plugins
+=======
+
+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
+ >>> from mailman.config import config
+ >>> 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
+ ...
+ ... @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
+ ...
+ ... def rest_object(self):
+ ... pass
+ ... """, file=fp)
+
+Running the hooks
+-----------------
+
+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:
+ ... print("""\
+ ... [meta]
+ ... extends: test.cfg
+ ...
+ ... [plugin.hook_example]
+ ... class: hooks.ExamplePlugin
+ ... enable: yes
+ ... """, file=fp)
+
+The hooks are run in the second and third steps of initialization. However,
+we can't run those initialization steps in process, so call a command line
+script that will produce no output to force the hooks to run.
+::
+
+ >>> import subprocess
+ >>> from mailman.testing.layers import ConfigLayer
+ >>> def call(cfg_path, python_path):
+ ... exe = os.path.join(os.path.dirname(sys.executable), 'mailman')
+ ... env = os.environ.copy()
+ ... env.update(
+ ... MAILMAN_CONFIG_FILE=cfg_path,
+ ... PYTHONPATH=python_path,
+ ... )
+ ... test_cfg = os.environ.get('MAILMAN_EXTRA_TESTING_CFG')
+ ... if test_cfg is not None:
+ ... env['MAILMAN_EXTRA_TESTING_CFG'] = test_cfg
+ ... proc = subprocess.Popen(
+ ... [exe, 'lists', '--domain', 'ignore', '-q'],
+ ... cwd=ConfigLayer.root_directory, env=env,
+ ... universal_newlines=True,
+ ... stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ ... stdout, stderr = proc.communicate()
+ ... assert proc.returncode == 0, stderr
+ ... print(stdout)
+ ... print(stderr)
+
+ >>> call(config_path, config_directory)
+ pre-hook: 1
+ post-hook: 2
+ <BLANKLINE>
+ <BLANKLINE>
+
+ >>> os.remove(config_path)
+
+Deprecated hooks
+----------------
+
+The old-style `pre_hook` and `post_hook` callables are deprecated and are no
+longer called upon startup.
+::
+
+ >>> deprecated_hook_path = os.path.join(config_directory, 'deprecated_hooks.py')
+ >>> with open(deprecated_hook_path, 'w') as fp:
+ ... print("""\
+ ... def do_something():
+ ... print("does something")
+ ...
+ ... def do_something_else():
+ ... print("does something else")
+ ...
+ ... """, file=fp)
+
+ >>> deprecated_config_path = os.path.join(config_directory, 'deprecated.cfg')
+ >>> with open(deprecated_config_path, 'w') as fp:
+ ... print("""\
+ ... [meta]
+ ... extends: test.cfg
+ ...
+ ... [mailman]
+ ... pre_hook: deprecated_hooks.do_something
+ ... post_hook: deprecated_hooks.do_something_else
+ ... """, file=fp)
+
+ >>> call(deprecated_config_path, config_directory)
+ does something
+ does something else
+ <BLANKLINE>
+ ... UserWarning: The pre_hook configuration value has been replaced by the plugins infrastructure. ...
+ ... UserWarning: The post_hook configuration value has been replaced by the plugins infrastructure. ...
+ <BLANKLINE>
+
+ >>> os.remove(deprecated_config_path)