Commit ca5f00a6 ca5f00a649a32305efce927bf4b6c70ec0fdc1c8 by Sergey Poznyakoff

Automatic moderator for Mailman-driven mailing lists.

1 parent 78a9fc65
1 /* GNU Mailutils -- a suite of utilities for electronic mail
2 Copyright (C) 2006 Free Software Foundation, Inc.
3
4 GNU Mailutils is free software; you can redistribute it and/or modify
5 it under the terms of the GNU Lesser General Public License as published by
6 the Free Software Foundation; either version 2, or (at your option)
7 any later version.
8
9 GNU Mailutils is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU Lesser General Public License for more details.
13
14 You should have received a copy of the GNU Lesser General Public
15 License along with GNU Mailutils; if not, write to the Free
16 Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
17 Boston, MA 02110-1301 USA */
18
19 /* Moderator robot for Mailman-driven mail archives.
20 Mailman moderation request is a MIME message consisting of the three parts:
21
22 1 text/plain Introduction for the human reader.
23 2 message/rfc822 Original submission.
24 3 message/rfc822 Mailman control message.
25
26 Replying to part 3 (keeping the subject intact) instructs Mailman to
27 discard the original submission.
28 Replying to part 3 while adding an `Approved:' header with the list
29 password in it approves the submission.
30
31 Syntax:
32
33 moderator [:keep]
34 [:address <address: string>]
35 [:source <sieve-file: string>]
36
37 The moderator action spawns an inferior Sieve machine and filters the
38 original submission (part 2) through it. If the inferior machine marks
39 the message as deleted, the action replies to the control message,
40 thereby causing the submission to be discarded. The From: address of the
41 reply can be modified using :address tag. After discarding the message,
42 moderator marks it as deleted, unless it is given :keep tag.
43
44 If :source tag is given, its argument sieve-file specifies the Sieve
45 source file to be used on the message. Otherwise, moderator will create
46 a copy of the existing Sieve machine.
47
48 The action checks the message structure: it will bail out if the message
49 does not have exactly 3 MIME parts, or if parts 2 and 3 are not of
50 message/rfc822. It is the responsibility of the caller to make sure
51 the message is actually a valid Mailman moderation request, for example:
52
53 if allof(header :is "Sender" "mailman-bounces@gnu.org",
54 header :is "X-List-Administrivia" "yes")
55 {
56 moderator "~/.sieve/mailman.sv";
57 }
58
59 */
60
61 #ifdef HAVE_CONFIG_H
62 # include <config.h>
63 #endif
64
65 #include <unistd.h>
66 #include <mailutils/libsieve.h>
67 #include <stdlib.h>
68
69 static int
70 moderator_filter_message (mu_sieve_machine_t mach, mu_list_t tags,
71 mu_message_t msg, int *pdiscard)
72 {
73 int rc;
74 mu_sieve_machine_t newmach;
75 mu_attribute_t attr;
76 mu_sieve_value_t *arg;
77
78 if (mu_sieve_tag_lookup (tags, "source", &arg))
79 {
80 rc = mu_sieve_machine_init (&newmach, NULL);
81 if (rc)
82 {
83 mu_sieve_error (mach, _("Cannot initialize sieve machine: %s"),
84 mu_strerror (rc));
85 return 1;
86 }
87 /* FIXME: This should be configurable:
88 moderator :inherit
89 moderator :debug 2
90 ...
91 */
92 mu_sieve_machine_inherit_report (newmach, mach);
93
94 rc = mu_sieve_compile (newmach, arg->v.string);
95 if (rc)
96 mu_sieve_error (mach, _("cannot compile source `%s'"), arg->v.string);
97 }
98 else
99 rc = mu_sieve_machine_dup (mach, &newmach);
100
101 if (rc)
102 return rc;
103
104 mu_message_get_attribute (msg, &attr);
105 mu_attribute_unset_deleted (attr);
106
107 rc = mu_sieve_message (newmach, msg);
108
109 if (rc)
110 mu_sieve_error (newmach, _("failed to run inferior sieve machine"));
111 else
112 *pdiscard = mu_attribute_is_deleted (attr);
113
114 mu_sieve_machine_destroy (&newmach);
115
116 return rc;
117 }
118
119 static int
120 copy_header (mu_sieve_machine_t mach,
121 mu_header_t to_hdr, char *to, mu_header_t from_hdr, char *from)
122 {
123 int rc;
124 char *value = NULL;
125 if ((rc = mu_header_aget_value (from_hdr, from, &value)))
126 {
127 mu_sieve_error (mach, _("cannot get `%s:' header: %s"),
128 from, mu_strerror (rc));
129 return rc;
130 }
131 rc = mu_header_set_value (to_hdr, to, value, 0);
132 free (value);
133 return rc;
134 }
135
136
137 static int
138 moderator_discard_message (mu_sieve_machine_t mach, mu_message_t request,
139 const char *from)
140 {
141 int rc;
142 mu_message_t reply;
143 mu_header_t repl_hdr, req_hdr;
144 mu_mailer_t mailer;
145
146 rc = mu_message_create (&reply, NULL);
147 if (rc)
148 return rc;
149 rc = mu_message_get_header (reply, &repl_hdr);
150 if (rc)
151 {
152 mu_message_destroy (&reply, NULL);
153 return rc;
154 }
155
156 rc = mu_message_get_header (request, &req_hdr);
157 if (rc)
158 {
159 mu_message_destroy (&reply, NULL);
160 return rc;
161 }
162
163 if (copy_header (mach, repl_hdr, MU_HEADER_TO, req_hdr, MU_HEADER_FROM)
164 || copy_header (mach,
165 repl_hdr, MU_HEADER_SUBJECT, req_hdr, MU_HEADER_SUBJECT))
166 {
167 mu_message_destroy (&reply, NULL);
168 return rc;
169 }
170
171 if (from)
172 mu_header_set_value (repl_hdr, MU_HEADER_FROM, from, 0);
173
174 mailer = mu_sieve_get_mailer (mach);
175 rc = mu_mailer_open (mailer, 0);
176 if (rc)
177 mu_sieve_error (mach, _("cannot open mailer: %s"),
178 mu_strerror (rc));
179 else
180 {
181 rc = mu_mailer_send_message (mailer, reply, NULL, NULL);
182 mu_mailer_close (mailer);
183
184 if (rc)
185 mu_sieve_error (mach, _("cannot send message: %s"),
186 mu_strerror (rc));
187 }
188 mu_message_destroy (&reply, NULL);
189 return rc;
190 }
191
192 int
193 moderator_message_get_part (mu_sieve_machine_t mach,
194 mu_message_t msg, size_t index, mu_message_t *pmsg)
195 {
196 int rc;
197 mu_message_t tmp;
198 mu_header_t hdr = NULL;
199 char *value;
200
201 if ((rc = mu_message_get_part (msg, index, &tmp)))
202 {
203 mu_sieve_error (mach, _("cannot get message part #%lu: %s"),
204 (unsigned long) index, mu_strerror (rc));
205 return 1;
206 }
207
208 mu_message_get_header (tmp, &hdr);
209 if (mu_header_aget_value (hdr, MU_HEADER_CONTENT_TYPE, &value) == 0
210 && memcmp (value, "message/rfc822", 14) == 0)
211 {
212 mu_stream_t str;
213 mu_body_t body;
214
215 free (value);
216 mu_message_get_body (tmp, &body);
217 mu_body_get_stream (body, &str);
218
219 rc = mu_stream_to_message (str, pmsg);
220 if (rc)
221 {
222 mu_sieve_error (mach,
223 _("cannot convert MIME part stream to message: %s"),
224 mu_strerror (rc));
225 return 1;
226 }
227 }
228 else if (value)
229 {
230 mu_sieve_error (mach,
231 _("expected message type message/rfc822, but found %s"),
232 value);
233 free (value);
234 return 1;
235 }
236 else
237 {
238 mu_sieve_error (mach, _("No Content-Type header found"));
239 return 1;
240 }
241 return 0;
242 }
243
244 static int
245 moderator_action (mu_sieve_machine_t mach, mu_list_t args, mu_list_t tags)
246 {
247 mu_message_t msg, orig;
248 int rc;
249 size_t nparts = 0;
250 int discard = 0;
251 int ismime;
252
253 if (mu_sieve_get_debug_level (mach) & MU_SIEVE_DEBUG_TRACE)
254 {
255 mu_sieve_locus_t locus;
256 mu_sieve_get_locus (mach, &locus);
257 mu_sieve_debug (mach, "%s:%lu: moderator_test %lu\n",
258 locus.source_file,
259 (u_long) locus.source_line,
260 (u_long) mu_sieve_get_message_num (mach));
261 }
262
263 msg = mu_sieve_get_message (mach);
264 mu_message_is_multipart (msg, &ismime);
265
266 if (!ismime)
267 {
268 mu_sieve_error (mach, _("message is not multipart"));
269 mu_sieve_abort (mach);
270 }
271
272 mu_message_get_num_parts (msg, &nparts);
273
274 if (nparts != 3) /* Mailman moderation requests have three parts */
275 {
276 mu_sieve_error (mach, _("expected 3 parts, but found %lu"),
277 (unsigned long) nparts);
278 mu_sieve_abort (mach);
279 }
280
281 if ((rc = moderator_message_get_part (mach, msg, 2, &orig)))
282 mu_sieve_abort (mach);
283
284 rc = moderator_filter_message (mach, tags, orig, &discard);
285 mu_message_unref (orig);
286 if (rc)
287 mu_sieve_abort (mach);
288
289 if (discard && !mu_sieve_is_dry_run (mach))
290 {
291 mu_message_t request;
292 char *from = NULL;
293 mu_sieve_value_t *arg;
294
295 if ((rc = moderator_message_get_part (mach, msg, 3, &request)))
296 {
297 mu_sieve_error (mach, _("cannot get message part #3: %s"),
298 mu_strerror (rc));
299 mu_sieve_abort (mach);
300 }
301
302 if (mu_sieve_tag_lookup (tags, "address", &arg))
303 from = arg->v.string;
304
305 if (moderator_discard_message (mach, request, from))
306 discard = 0;
307 else
308 {
309 if (!mu_sieve_tag_lookup (tags, "keep", NULL))
310 sieve_mark_deleted (msg, 1);
311 else
312 discard = 0;
313 }
314 mu_message_unref (request);
315 }
316
317 mu_sieve_log_action (mach, "MODERATOR",
318 discard ? _("discarding message") :
319 _("keeping message"));
320 return 0;
321 }
322
323
324 /* Initialization */
325
326 /* Required arguments: */
327 static mu_sieve_data_type moderator_req_args[] = {
328 SVT_VOID
329 };
330
331 /* Tagged arguments: */
332 static mu_sieve_tag_def_t moderator_tags[] = {
333 { "keep", SVT_VOID },
334 { "address", SVT_STRING },
335 { "source", SVT_STRING },
336 { NULL }
337 };
338
339 static mu_sieve_tag_group_t moderator_tag_groups[] = {
340 { moderator_tags, NULL },
341 { NULL }
342 };
343
344
345 /* Initialization function. */
346 int
347 SIEVE_EXPORT(moderator,init) (mu_sieve_machine_t mach)
348 {
349 return mu_sieve_register_action (mach, "moderator", moderator_action,
350 moderator_req_args,
351 moderator_tag_groups, 1);
352 }
353