======= 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 >>> 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 ... 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. ... >>> os.remove(deprecated_config_path)