1 #include <sys/types.h>
2 #include <sys/stat.h>
3 #include <unistd.h>
4 #include "error.h"
5 #include "case.h"
6 #include "stralloc.h"
7 #include "str.h"
8 #include "env.h"
9 #include "sender.h"
10 #include "error.h"
11 #include "sig.h"
12 #include "wait.h"
13 #include "strerr.h"
14 #include "byte.h"
15 #include "getln.h"
16 #include "qmail.h"
17 #include "substdio.h"
18 #include "subfd.h"
19 #include "readwrite.h"
20 #include "seek.h"
21 #include "quote.h"
22 #include "datetime.h"
23 #include "now.h"
24 #include "fmt.h"
25 #include "getconfopt.h"
26 #include "cookie.h"
27 #include "messages.h"
28 #include "copy.h"
29 #include "hdr.h"
30 #include "mime.h"
31 #include "open.h"
32 #include "lock.h"
33 #include "die.h"
34 #include "idx.h"
35 #include "wrap.h"
36 #include "config.h"
37 #include "auto_version.h"
38
39 const char FATAL[] = "ezmlm-moderate: fatal: ";
40 const char INFO[] = "ezmlm-moderate: info: ";
41 const char USAGE[] =
42 "ezmlm-moderate: usage: ezmlm-moderate [-cCmMrRvV] [-t replyto] dir [/path/ezmlm-send]";
43
44 static const char *replyto = (char *) 0;
45 static int flagmime = MOD_MIME; /* default is message as attachment */
46
47 static stralloc sendopt = {0};
48
49 static struct option options[] = {
50 OPT_COPY_FLAG(sendopt,'c'),
51 OPT_COPY_FLAG(sendopt,'C'),
52 OPT_COPY_FLAG(sendopt,'r'),
53 OPT_COPY_FLAG(sendopt,'R'),
54 OPT_FLAG(flagmime,'m',1,0),
55 OPT_FLAG(flagmime,'M',0,0),
56 OPT_CSTR(replyto,'t',0),
57 OPT_CSTR(replyto,'T',0),
58 OPT_END
59 };
60
61 static stralloc to = {0};
62
63 char boundary[COOKIE];
64 stralloc line = {0};
65 stralloc qline = {0};
66 stralloc quoted = {0};
67 static stralloc fnmsg = {0};
68
69 struct qmail qq;
70
code_qput(const char * s,unsigned int n)71 static void code_qput(const char *s,unsigned int n)
72 {
73 if (!flagcd)
74 qmail_put(&qq,s,n);
75 else {
76 if (flagcd == 'B')
77 encodeB(s,n,&qline,0);
78 else
79 encodeQ(s,n,&qline);
80 qmail_put(&qq,qline.s,qline.len);
81 }
82 }
83
checkfile(const char * fn)84 static int checkfile(const char *fn)
85 /* looks for DIR/mod/{pending|rejected|accept}/fn.*/
86 /* Returns: */
87 /* 1 found in pending */
88 /* 0 not found */
89 /* -1 found in accepted */
90 /* -2 found in rejected */
91 /* Handles errors. */
92 /* ALSO: if found, fnmsg contains the o-terminated*/
93 /* file name. */
94 {
95 struct stat st;
96
97 if (!stralloc_copys(&fnmsg,"mod/pending/")) die_nomem();
98 if (!stralloc_cats(&fnmsg,fn)) die_nomem();
99 if (!stralloc_0(&fnmsg)) die_nomem();
100 if (stat(fnmsg.s,&st) == -1) {
101 if (errno != error_noent)
102 strerr_die2sys(111,FATAL,MSG1(ERR_STAT,fnmsg.s));
103 } else
104 return 1;
105
106 if (!stralloc_copys(&fnmsg,"mod/accepted/")) die_nomem();
107 if (!stralloc_cats(&fnmsg,fn)) die_nomem();
108 if (!stralloc_0(&fnmsg)) die_nomem();
109 if (stat(fnmsg.s,&st) == -1) {
110 if (errno != error_noent)
111 strerr_die2sys(111,FATAL,MSG1(ERR_STAT,fnmsg.s));
112 } else
113 return -1;
114
115 if (!stralloc_copys(&fnmsg,"mod/rejected/")) die_nomem();
116 if (!stralloc_cats(&fnmsg,fn)) die_nomem();
117 if (!stralloc_0(&fnmsg)) die_nomem();
118 if (stat(fnmsg.s,&st) == -1) {
119 if (errno != error_noent)
120 strerr_die2sys(111,FATAL,MSG1(ERR_STAT,fnmsg.s));
121 } else
122 return -2;
123 return 0;
124 }
125
maketo(void)126 static void maketo(void)
127 /* expects line to be a return-path line. If it is and the format is valid */
128 /* to is set to to the sender. Otherwise, to is left untouched. Assuming */
129 /* to is empty to start with, it will remain empty if no sender is found. */
130 {
131 unsigned int x, y;
132
133 if (case_startb(line.s,line.len,"return-path:")) {
134 x = 12 + byte_chr(line.s + 12,line.len-12,'<');
135 if (x != line.len) {
136 y = byte_rchr(line.s + x,line.len-x,'>');
137 if (y + x != line.len) {
138 if (!stralloc_copyb(&to,line.s+x+1,y-1)) die_nomem();
139 if (!stralloc_0(&to)) die_nomem();
140 } /* no return path-> no addressee. A NUL in the sender */
141 } /* is no worse than a faked sender, so no problem */
142 }
143 }
144
main(int argc,char ** argv)145 int main(int argc,char **argv)
146 {
147 const char *sender;
148 const char *def;
149 char *local;
150 const char *action;
151 int flaginheader;
152 int flaggoodfield;
153 int flagdone;
154 int fd;
155 int match;
156 const char *err;
157 char encin = '\0';
158 unsigned int start,confnum;
159 unsigned int pos,i;
160 int child;
161 int opt;
162 char *cp,*cpnext,*cplast,*cpafter;
163 char strnum[FMT_ULONG];
164 char hash[COOKIE];
165 substdio sstext;
166 char textbuf[1024];
167 datetime_sec when;
168 stralloc text = {0};
169 stralloc fnbase = {0};
170 stralloc fnnew = {0};
171 const char *dir;
172
173 (void) umask(022);
174 sig_pipeignore();
175 when = now();
176
177 if (!stralloc_copys(&sendopt,"-")) die_nomem();
178 opt = getconfopt(argc,argv,options,1,&dir);
179
180 sender = get_sender();
181 if (!sender) die_sender();
182 local = env_get("LOCAL");
183 if (!local) strerr_die2x(100,FATAL,MSG(ERR_NOLOCAL));
184 def = env_get("DEFAULT");
185 if (!def) strerr_die2x(100,FATAL,MSG(ERR_NODEFAULT));
186
187 if (!*sender)
188 strerr_die2x(100,FATAL,MSG(ERR_BOUNCE));
189 if (!sender[str_chr(sender,'@')])
190 strerr_die2x(100,FATAL,MSG(ERR_ANONYMOUS));
191 if (str_equal(sender,"#@[]"))
192 strerr_die2x(100,FATAL,MSG(ERR_BOUNCE));
193
194 /* local should be >= def, but who knows ... */
195 cp = local + str_len(local) - str_len(def) - 2;
196 if (cp < local) die_badformat();
197 action = local + byte_rchr(local,cp - local,'-');
198 if (action == cp) die_badformat();
199 action++;
200
201 if (!action[0]) die_badformat();
202 if (!str_start(action,ACTION_ACCEPT) && !str_start(action,ACTION_REJECT))
203 die_badformat();
204 start = str_chr(action,'-');
205 if (!action[start]) die_badformat();
206 confnum = 1 + start + str_chr(action + start + 1,'.');
207 if (!action[confnum]) die_badformat();
208 confnum += 1 + str_chr(action + confnum + 1,'.');
209 if (!action[confnum]) die_badformat();
210 if (!stralloc_copyb(&fnbase,action+start+1,confnum-start-1)) die_nomem();
211 if (!stralloc_0(&fnbase)) die_nomem();
212 cookie(hash,key.s,key.len,fnbase.s,"","a");
213 if (byte_diff(hash,COOKIE,action+confnum+1))
214 die_badformat();
215
216 lockfile("mod/lock");
217
218 switch(checkfile(fnbase.s)) {
219 case 0:
220 strerr_die2x(100,FATAL,MSG(ERR_MOD_TIMEOUT));
221 case -1: /* only error if new request != action taken */
222 if (str_start(action,ACTION_ACCEPT))
223 strerr_die2x(0,INFO,MSG(ERR_MOD_ACCEPTED));
224 else
225 strerr_die2x(100,FATAL,MSG(ERR_MOD_ACCEPTED));
226 case -2:
227 if (str_start(action,ACTION_REJECT))
228 strerr_die2x(0,INFO,MSG(ERR_MOD_REJECTED));
229 else
230 strerr_die2x(100,FATAL,MSG(ERR_MOD_REJECTED));
231 default:
232 break;
233 }
234 /* Here, we have an existing filename in fnbase with the complete path */
235 /* from the current dir in fnmsg. */
236
237 if (str_start(action,ACTION_REJECT)) {
238
239 if (qmail_open(&qq) == -1)
240 strerr_die2sys(111,FATAL,MSG(ERR_QMAIL_QUEUE));
241
242
243 /* Build recipient from msg return-path */
244 fd = open_read(fnmsg.s);
245 if (fd == -1) {
246 if (errno != error_noent)
247 strerr_die2sys(111,FATAL,MSG1(ERR_OPEN,fnmsg.s));
248 else
249 strerr_die2x(100,FATAL,MSG(ERR_MOD_TIMEOUT));
250 }
251 substdio_fdbuf(&sstext,read,fd,textbuf,sizeof(textbuf));
252
253 if (getln(&sstext,&line,&match,'\n') == -1 || !match)
254 strerr_die2sys(111,FATAL,MSG(ERR_READ_INPUT));
255 maketo(); /* extract SENDER from return-path */
256 /* Build message */
257 hdr_add2s("Mailing-List: ",MSG(TXT_MAILING_LIST));
258 if (listid.len > 0)
259 hdr_add2("List-ID: ",listid.s,listid.len);
260 hdr_datemsgid(when);
261 hdr_from("-owner");
262 if (replyto)
263 hdr_add2s("Reply-To: ",replyto);
264 hdr_add2s("To: ",to.s);
265 hdr_subject(MSG(SUB_RETURNED_POST));
266
267 if (flagmime) {
268 hdr_mime(CTYPE_MULTIPART);
269 hdr_boundary(0);
270 hdr_ctype(CTYPE_TEXT);
271 hdr_transferenc();
272 }
273 copy(&qq,"text/top",flagcd);
274 copy(&qq,"text/mod-reject",flagcd);
275
276 flaginheader = 1;
277 if (!stralloc_copys(&text,"")) die_nomem();
278 if (!stralloc_ready(&text,1024)) die_nomem();
279 for (;;) { /* copy moderator's rejection comment */
280 if (getln(subfdin,&line,&match,'\n') == -1)
281 strerr_die2sys(111,FATAL,MSG(ERR_READ_INPUT));
282 if (!match) break;
283 if (flaginheader) {
284 if (case_startb(line.s,line.len,"Content-Transfer-Encoding:")) {
285 pos = 26;
286 while (line.s[pos] == ' ' || line.s[pos] == '\t') ++pos;
287 if (case_startb(line.s+pos,line.len-pos,"base64"))
288 encin = 'B';
289 else if (case_startb(line.s+pos,line.len-pos,"quoted-printable"))
290 encin = 'Q';
291 }
292 if (line.len == 1)
293 flaginheader = 0;
294 } else
295 if (!stralloc_cat(&text,&line)) die_nomem();
296 } /* got body */
297 if (encin) {
298 if (encin == 'B')
299 decodeB(text.s,text.len,&line);
300 else
301 decodeQ(text.s,text.len,&line);
302 if (!stralloc_copy(&text,&line)) die_nomem();
303 }
304 cp = text.s;
305 cpafter = text.s + text.len;
306 if (!stralloc_copys(&line,"\n>>>>> -------------------- >>>>>\n"))
307 die_nomem();
308 flaggoodfield = 0;
309 flagdone = 0;
310 while ((cpnext = cp + byte_chr(cp,cpafter-cp,'\n')) != cpafter) {
311 i = byte_chr(cp,cpnext-cp,'%');
312 if (i <= 5 && cpnext-cp-i >= 3) {
313 /* max 5 "quote characters" and space for %%% */
314 if (cp[i+1] == '%' && cp[i+2] == '%') {
315 if (!flaggoodfield) { /* Start tag */
316 if (!stralloc_copyb("ed,cp,i)) die_nomem(); /* quote chars*/
317 flaggoodfield = 1;
318 cp = cpnext + 1;
319 continue;
320 } else { /* end tag */
321 if (flagdone) /* 0 no comment lines, 1 comment line */
322 flagdone = 2; /* 2 at least 1 comment line & end tag */
323 break;
324 }
325 }
326 }
327 if (flaggoodfield) {
328 cplast = cpnext - 1;
329 if (*cplast == '\r') /* CRLF -> '\n' for base64 encoding */
330 *cplast = '\n';
331 else
332 ++cplast;
333 /* NUL is now ok, so the test for it was removed */
334 flagdone = 1;
335 i = cplast - cp + 1;
336 if (quoted.len && quoted.len <= i &&
337 !str_diffn(cp,quoted.s,quoted.len)) { /* quote chars */
338 if (!stralloc_catb(&line,cp+quoted.len,i-quoted.len)) die_nomem();
339 } else
340 if (!stralloc_catb(&line,cp,i)) die_nomem(); /* no quote chars */
341 }
342 cp = cpnext + 1;
343 }
344 if (flagdone == 2) {
345 if (!stralloc_cats(&line,"<<<<< -------------------- <<<<<\n")) die_nomem();
346 code_qput(line.s,line.len);
347 }
348 if (flagcd == 'B') {
349 encodeB("",0,&line,2);
350 qmail_put(&qq,line.s,line.len);
351 }
352 if (flagmime) {
353 hdr_boundary(0);
354 hdr_ctype(CTYPE_MESSAGE);
355 }
356 qmail_puts(&qq,"\n");
357 if (seek_begin(fd) == -1)
358 strerr_die2sys(111,FATAL,MSG1(ERR_SEEK,fnmsg.s));
359
360 substdio_fdbuf(&sstext,read,fd,textbuf,sizeof(textbuf));
361 if (qmail_copy(&qq,&sstext,-1) != 0)
362 strerr_die2sys(111,FATAL,MSG1(ERR_READ,fnmsg.s));
363 close(fd);
364
365 if (flagmime)
366 hdr_boundary(1);
367
368 if (!stralloc_copy(&line,&outlocal)) die_nomem();
369 if (!stralloc_cats(&line,"-return-@")) die_nomem();
370 if (!stralloc_cat(&line,&outhost)) die_nomem();
371 if (!stralloc_0(&line)) die_nomem();
372 qmail_from(&qq,line.s);
373 if (to.len)
374 qmail_to(&qq,to.s);
375
376 if (!stralloc_copys(&fnnew,"mod/rejected/")) die_nomem();
377 if (!stralloc_cats(&fnnew,fnbase.s)) die_nomem();
378 if (!stralloc_0(&fnnew)) die_nomem();
379
380 /* this is strictly to track what happended to a message to give informative */
381 /* messages to the 2nd-nth moderator that acts on the same message. Since */
382 /* this isn't vital we ignore errors. Also, it is no big ideal if unlinking */
383 /* the old file fails. In the worst case it gets acted on again. If we issue */
384 /* a temp error the reject will be redone, which is slightly worse. */
385
386 if (*(err = qmail_close(&qq)) == '\0') {
387 fd = open_trunc(fnnew.s);
388 if (fd != -1)
389 close(fd);
390 unlink(fnmsg.s);
391 strnum[fmt_ulong(strnum,qmail_qp(&qq))] = 0;
392 strerr_die2x(0,"ezmlm-moderate: info: qp ",strnum);
393 } else
394 strerr_die4x(111,FATAL,MSG(ERR_TMP_QMAIL_QUEUE),": ",err + 1);
395
396 } else if (str_start(action,ACTION_ACCEPT)) {
397 fd = open_read(fnmsg.s);
398 if (fd == -1) {
399 if (errno !=error_noent)
400 strerr_die2sys(111,FATAL,MSG1(ERR_OPEN,fnmsg.s));
401 else /* shouldn't happen since we've got lock */
402 strerr_die3x(100,FATAL,fnmsg.s,MSG(ERR_MOD_TIMEOUT));
403 }
404
405 substdio_fdbuf(&sstext,read,fd,textbuf,sizeof(textbuf));
406 /* read "Return-Path:" line */
407 if (getln(&sstext,&line,&match,'\n') == -1 || !match)
408 strerr_die2sys(111,FATAL,MSG(ERR_READ_INPUT));
409 maketo(); /* extract SENDER to "to" */
410 env_put2("SENDER",to.s); /* set SENDER */
411 if (seek_begin(fd) == -1) /* rewind, since we read an entire buffer */
412 strerr_die2sys(111,FATAL,MSG1(ERR_SEEK,fnmsg.s));
413
414 if ((child = wrap_fork()) == 0) {
415 close(0);
416 dup(fd); /* make fnmsg.s stdin */
417 if (argc > opt + 1)
418 wrap_execvp((const char **)argv + opt);
419 else if (argc > opt)
420 wrap_execsh(argv[opt]);
421 else
422 wrap_execbin("/ezmlm-send", &sendopt, dir);
423 }
424 /* parent */
425 close(fd);
426 wrap_exitcode(child);
427 if (!stralloc_copys(&fnnew,"mod/accepted/")) die_nomem();
428
429 if (!stralloc_cats(&fnnew,fnbase.s)) die_nomem();
430 if (!stralloc_0(&fnnew)) die_nomem();
431 /* ignore errors */
432 fd = open_trunc(fnnew.s);
433 if (fd != -1)
434 close(fd);
435 unlink(fnmsg.s);
436 }
437 _exit(0);
438 }
439