summaryrefslogtreecommitdiff
path: root/src/mailman/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/mailman/app')
-rw-r--r--src/mailman/app/commands.py4
-rw-r--r--src/mailman/app/docs/hooks.rst121
-rw-r--r--src/mailman/app/docs/plugins.rst210
3 files changed, 212 insertions, 123 deletions
diff --git a/src/mailman/app/commands.py b/src/mailman/app/commands.py
index 7cf3bc4c5..ca257ffda 100644
--- a/src/mailman/app/commands.py
+++ b/src/mailman/app/commands.py
@@ -19,11 +19,11 @@
from mailman.config import config
from mailman.interfaces.command import IEmailCommand
-from mailman.utilities.modules import add_components
+from mailman.utilities.plugins import add_pluggable_components
from public import public
@public
def initialize():
"""Initialize the email commands."""
- add_components('mailman.commands', IEmailCommand, config.commands)
+ add_pluggable_components('commands', IEmailCommand, config.commands)
diff --git a/src/mailman/app/docs/hooks.rst b/src/mailman/app/docs/hooks.rst
deleted file mode 100644
index 07d0ec33c..000000000
--- a/src/mailman/app/docs/hooks.rst
+++ /dev/null
@@ -1,121 +0,0 @@
-=====
-Hooks
-=====
-
-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``.
-::
-
- >>> import os, sys
- >>> from mailman.config import config
- >>> config_directory = os.path.dirname(config.filename)
- >>> sys.path.insert(0, config_directory)
-
- >>> hook_path = os.path.join(config_directory, 'hooks.py')
- >>> with open(hook_path, 'w') as fp:
- ... print("""\
- ... counter = 1
- ... def pre_hook():
- ... global counter
- ... print('pre-hook:', counter)
- ... counter += 1
- ...
- ... def post_hook():
- ... global counter
- ... print('post-hook:', counter)
- ... counter += 1
- ... """, file=fp)
- >>> fp.close()
-
-
-Pre-hook
-========
-
-We can set the pre-hook in the configuration file.
-
- >>> config_path = os.path.join(config_directory, 'hooks.cfg')
- >>> with open(config_path, 'w') as fp:
- ... print("""\
- ... [meta]
- ... extends: test.cfg
- ...
- ... [mailman]
- ... pre_hook: hooks.pre_hook
- ... """, 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():
- ... exe = os.path.join(os.path.dirname(sys.executable), 'mailman')
- ... env = os.environ.copy()
- ... env.update(
- ... MAILMAN_CONFIG_FILE=config_path,
- ... PYTHONPATH=config_directory,
- ... )
- ... 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)
-
- >>> call()
- pre-hook: 1
- <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/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)