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
|
=======
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)
>>> fp.close()
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():
... 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
post-hook: 2
<BLANKLINE>
>>> os.remove(config_path)
|