summaryrefslogtreecommitdiff
path: root/src/mailman/app/docs/plugins.rst
blob: 4430179ae479586d6bff494eaec24a999279dd82 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
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)