1 /* GNU Mailutils -- a suite of utilities for electronic mail
2    Copyright (C) 2006-2021 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 3, 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, see
16    <http://www.gnu.org/licenses/>. */
17 
18 /* Moderator robot for Mailman-driven mail archives.
19    Mailman moderation request is a MIME message consisting of the three parts:
20 
21    1  text/plain      Introduction for the human reader.
22    2  message/rfc822  Original submission.
23    3  message/rfc822  Mailman control message.
24 
25    Replying to part 3 (keeping the subject intact) instructs Mailman to
26    discard the original submission.
27    Replying to part 3 while adding an `Approved:' header with the list
28    password in it approves the submission.
29 
30    Syntax:
31 
32      moderator [:keep]
33                [:address <address: string>]
34 	       [:program <sieve-program: 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 a Sieve
45    source file to be used on the message. If :program tag is given, its
46    argument supplies a Sieve program to be used on this message. Only
47    one of :program or :source may be specified. Supplying them both, or
48    supplying several instances of the same keyword, is an error. The
49    behavior of the action in this case is undefined.
50 
51    If neither :program nor :source is supplied, moderator will create
52    a copy of the existing Sieve machine and use it on the message.
53 
54    The action checks the message structure: it will bail out if the message
55    does not have exactly 3 MIME parts, or if parts 2 and 3 are not of
56    message/rfc822. It is the responsibility of the caller to make sure
57    the message is actually a valid Mailman moderation request, for example:
58 
59    if allof(header :is "Sender" "mailman-bounces@gnu.org",
60             header :is "X-List-Administrivia" "yes")
61      {
62         moderator :source "~/.sieve/mailman.sv";
63      }
64 
65 */
66 
67 #ifdef HAVE_CONFIG_H
68 # include <config.h>
69 #endif
70 
71 #include <unistd.h>
72 #include <mailutils/sieve.h>
73 #include <stdlib.h>
74 
75 static int
moderator_filter_message(mu_sieve_machine_t mach,mu_message_t msg,int * pdiscard)76 moderator_filter_message (mu_sieve_machine_t mach,
77 			  mu_message_t msg, int *pdiscard)
78 {
79   int rc;
80   mu_sieve_machine_t newmach;
81   mu_attribute_t attr;
82   char *arg;
83 
84   if (mu_sieve_get_tag (mach, "source", SVT_STRING, &arg))
85     {
86       rc = mu_sieve_machine_clone (mach, &newmach);
87       if (rc)
88 	{
89 	  mu_sieve_error (mach, _("cannot initialize sieve machine: %s"),
90 			  mu_strerror (rc));
91 	  return 1;
92 	}
93       /* FIXME: This should be configurable:
94 	   moderator :inherit
95 	   moderator :debug 2
96 	   ...
97       */
98 
99       rc = mu_sieve_compile (newmach, arg);
100       if (rc)
101 	mu_sieve_error (mach, _("cannot compile source `%s'"), arg);
102     }
103   else if (mu_sieve_get_tag (mach, "program", SVT_STRING, &arg))
104     {
105       struct mu_locus_range locrange = MU_LOCUS_RANGE_INITIALIZER;
106 
107       rc = mu_sieve_machine_clone (mach, &newmach);
108       if (rc)
109 	{
110 	  mu_sieve_error (mach, _("cannot initialize sieve machine: %s"),
111 			  mu_strerror (rc));
112 	  return 1;
113 	}
114       mu_sieve_get_locus (mach, &locrange);
115       rc = mu_sieve_compile_text (newmach,
116 				  arg, strlen (arg),
117 				  &locrange.beg);
118       if (rc)
119 	mu_sieve_error (mach, _("cannot compile subprogram"));
120     }
121   else
122     rc = mu_sieve_machine_dup (mach, &newmach);
123 
124   if (rc)
125     return rc;
126 
127   mu_message_get_attribute (msg, &attr);
128   mu_attribute_unset_deleted (attr);
129 
130   rc = mu_sieve_message (newmach, msg);
131 
132   if (rc)
133     mu_sieve_error (newmach, _("failed to run inferior sieve machine"));
134   else
135     *pdiscard = mu_attribute_is_deleted (attr);
136 
137   mu_sieve_machine_destroy (&newmach);
138 
139   return rc;
140 }
141 
142 /* Copy the value of the header field FROM from the email header FROM_HDR to
143    the header field TO in the header TO_HDR, replacing the field, if it
144    exists. */
145 static int
copy_header(mu_sieve_machine_t mach,mu_header_t to_hdr,char * to,mu_header_t from_hdr,char * from)146 copy_header (mu_sieve_machine_t mach,
147 	     mu_header_t to_hdr, char *to, mu_header_t from_hdr, char *from)
148 {
149   int rc;
150   const char *value = NULL;
151 
152   if ((rc = mu_header_sget_value (from_hdr, from, &value)))
153     {
154       mu_sieve_error (mach, _("cannot get `%s:' header: %s"),
155 		      from, mu_strerror (rc));
156       return rc;
157     }
158   rc = mu_header_set_value (to_hdr, to, value, 1);
159   return rc;
160 }
161 
162 
163 static int
moderator_discard_message(mu_sieve_machine_t mach,mu_message_t request,const char * from)164 moderator_discard_message (mu_sieve_machine_t mach, mu_message_t request,
165 			   const char *from)
166 {
167   int rc;
168   mu_message_t reply;
169   mu_header_t repl_hdr, req_hdr;
170   mu_mailer_t mailer;
171 
172   rc = mu_message_create (&reply, NULL);
173   if (rc)
174     return rc;
175   rc = mu_message_get_header (reply, &repl_hdr);
176   if (rc)
177     {
178       mu_message_destroy (&reply, NULL);
179       return rc;
180     }
181 
182   rc = mu_message_get_header (request, &req_hdr);
183   if (rc)
184     {
185       mu_message_destroy (&reply, NULL);
186       return rc;
187     }
188 
189   if (copy_header (mach, repl_hdr, MU_HEADER_TO, req_hdr, MU_HEADER_FROM)
190       || copy_header (mach,
191 		      repl_hdr, MU_HEADER_SUBJECT, req_hdr, MU_HEADER_SUBJECT))
192     {
193       mu_message_destroy (&reply, NULL);
194       return rc;
195     }
196 
197   if (from)
198     mu_header_set_value (repl_hdr, MU_HEADER_FROM, from, 1);
199 
200   mailer = mu_sieve_get_mailer (mach);
201   rc = mu_mailer_open (mailer, 0);
202   if (rc)
203     mu_sieve_error (mach, _("cannot open mailer: %s"),
204 		    mu_strerror (rc));
205   else
206     {
207       rc = mu_mailer_send_message (mailer, reply, NULL, NULL);
208       mu_mailer_close (mailer);
209 
210       if (rc)
211 	mu_sieve_error (mach, _("cannot send message: %s"),
212 			mu_strerror (rc));
213     }
214   mu_message_destroy (&reply, NULL);
215   return rc;
216 }
217 
218 int
moderator_message_get_part(mu_sieve_machine_t mach,mu_message_t msg,size_t index,mu_message_t * pmsg)219 moderator_message_get_part (mu_sieve_machine_t mach,
220 			    mu_message_t msg, size_t index, mu_message_t *pmsg)
221 {
222   int rc;
223   mu_message_t tmp;
224   mu_header_t hdr = NULL;
225   const char *value;
226 
227   if ((rc = mu_message_get_part (msg, index, &tmp)))
228     {
229       mu_sieve_error (mach, _("cannot get message part #%lu: %s"),
230 		      (unsigned long) index, mu_strerror (rc));
231       return 1;
232     }
233 
234   mu_message_get_header (tmp, &hdr);
235   if (mu_header_sget_value (hdr, MU_HEADER_CONTENT_TYPE, &value) == 0
236       && memcmp (value, "message/rfc822", 14) == 0)
237     {
238       mu_stream_t str;
239       mu_body_t body;
240 
241       mu_message_get_body (tmp, &body);
242       mu_body_get_streamref (body, &str);
243 
244       rc = mu_stream_to_message (str, pmsg);
245       mu_stream_destroy (&str);
246       if (rc)
247 	{
248 	  mu_sieve_error (mach,
249 			  _("cannot convert MIME part stream to message: %s"),
250 			  mu_strerror (rc));
251 	  return 1;
252 	}
253     }
254   else if (value)
255     {
256       mu_sieve_error (mach,
257 		      _("expected message type message/rfc822, but found %s"),
258 		      value);
259       return 1;
260     }
261   else
262     {
263       mu_sieve_error (mach, _("no Content-Type header found"));
264       return 1;
265     }
266   return 0;
267 }
268 
269 static int
moderator_action(mu_sieve_machine_t mach)270 moderator_action (mu_sieve_machine_t mach)
271 {
272   mu_message_t msg, orig;
273   int rc;
274   size_t nparts = 0;
275   int discard = 0;
276   int ismime;
277 
278   msg = mu_sieve_get_message (mach);
279   mu_message_is_multipart (msg, &ismime);
280 
281   if (!ismime)
282     {
283       mu_sieve_error (mach, _("message is not multipart"));
284       mu_sieve_abort (mach);
285     }
286 
287   rc = mu_message_get_num_parts (msg, &nparts);
288   if (rc)
289     {
290       mu_sieve_error (mach, "mu_message_get_num_parts: %s", mu_strerror (rc));
291       mu_sieve_abort (mach);
292     }
293 
294   if (nparts != 3) /* Mailman moderation requests have three parts */
295     {
296       mu_sieve_error (mach, _("expected 3 parts, but found %lu"),
297 		      (unsigned long) nparts);
298       mu_sieve_abort (mach);
299     }
300 
301   if ((rc = moderator_message_get_part (mach, msg, 2, &orig)))
302     mu_sieve_abort (mach);
303 
304   rc = moderator_filter_message (mach, orig, &discard);
305   mu_message_unref (orig);
306   if (rc)
307     mu_sieve_abort (mach);
308 
309   if (discard && !mu_sieve_is_dry_run (mach))
310     {
311       mu_message_t request;
312       char *from = NULL;
313 
314       if ((rc = moderator_message_get_part (mach, msg, 3, &request)))
315 	{
316 	  mu_sieve_error (mach, _("cannot get message part #3: %s"),
317 			  mu_strerror (rc));
318 	  mu_sieve_abort (mach);
319 	}
320 
321       mu_sieve_get_tag (mach, "address", SVT_STRING, &from);
322 
323       if (moderator_discard_message (mach, request, from))
324 	discard = 0;
325       else
326 	{
327 	  if (!mu_sieve_get_tag (mach, "keep", SVT_VOID, NULL))
328 	    {
329 	      mu_attribute_t attr = 0;
330 
331 	      if (mu_message_get_attribute (msg, &attr) == 0)
332 		mu_attribute_set_deleted (attr);
333 	    }
334 	  else
335 	    discard = 0;
336 	}
337       mu_message_unref (request);
338     }
339 
340   mu_sieve_log_action (mach, "MODERATOR",
341 		       discard ? _("discarding message") :
342 		       _("keeping message"));
343   return 0;
344 }
345 
346 
347 /* Initialization */
348 
349 /* Required arguments: */
350 static mu_sieve_data_type moderator_req_args[] = {
351   SVT_VOID
352 };
353 
354 /* Tagged arguments: */
355 static mu_sieve_tag_def_t moderator_tags[] = {
356   { "keep", SVT_VOID },
357   { "address", SVT_STRING },
358   { "source", SVT_STRING },
359   { "program", SVT_STRING },
360   { NULL }
361 };
362 
363 static mu_sieve_tag_group_t moderator_tag_groups[] = {
364   { moderator_tags, NULL },
365   { NULL }
366 };
367 
368 
369 /* Initialization function. */
370 int
SIEVE_EXPORT(moderator,init)371 SIEVE_EXPORT(moderator,init) (mu_sieve_machine_t mach)
372 {
373   mu_sieve_register_action (mach, "moderator", moderator_action,
374 			    moderator_req_args,
375 			    moderator_tag_groups, 1);
376   return 0;
377 }
378 
379