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(&quoted,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