X-Git-Url: https://git.distorted.org.uk/~mdw/ezmlm/blobdiff_plain/5b62e993b0af39700031c2875d7f6654e6a02850..f8beb284087c279acfb30506f5bb32baa4949b44:/ezmlm-moderate.c diff --git a/ezmlm-moderate.c b/ezmlm-moderate.c new file mode 100644 index 0000000..363d207 --- /dev/null +++ b/ezmlm-moderate.c @@ -0,0 +1,611 @@ +/*$Id: ezmlm-moderate.c,v 1.42 1999/10/09 16:49:56 lindberg Exp $*/ +/*$Name: ezmlm-idx-040 $*/ + +#include +#include +#include "error.h" +#include "case.h" +#include "stralloc.h" +#include "str.h" +#include "env.h" +#include "error.h" +#include "sig.h" +#include "fork.h" +#include "wait.h" +#include "slurp.h" +#include "getconf.h" +#include "strerr.h" +#include "byte.h" +#include "getln.h" +#include "qmail.h" +#include "substdio.h" +#include "readwrite.h" +#include "seek.h" +#include "quote.h" +#include "datetime.h" +#include "now.h" +#include "date822fmt.h" +#include "fmt.h" +#include "sgetopt.h" +#include "auto_bin.h" +#include "cookie.h" +#include "errtxt.h" +#include "copy.h" +#include "idx.h" + +int flagmime = MOD_MIME; /* default is message as attachment */ +char flagcd = '\0'; /* default: do not use transfer encoding */ + +#define FATAL "ezmlm-moderate: fatal: " +#define INFO "ezmlm-moderate: info: " + +void die_usage() { strerr_die1x(100, + "ezmlm-moderate: usage: ezmlm-moderate [-cCmMrRvV] [-t replyto] " + "dir [/path/ezmlm-send]"); } + +void die_nomem() { strerr_die2x(111,FATAL,ERR_NOMEM); } + +void die_badformat() { strerr_die2x(100,FATAL,ERR_BAD_REQUEST); } + +void die_badaddr() { + strerr_die2x(100,FATAL,ERR_BAD_ADDRESS); +} + +stralloc outhost = {0}; +stralloc inlocal = {0}; +stralloc outlocal = {0}; +stralloc key = {0}; +stralloc mydtline = {0}; +stralloc mailinglist = {0}; +stralloc accept = {0}; +stralloc reject = {0}; +stralloc to = {0}; +stralloc send = {0}; +stralloc sendopt = {0}; +stralloc comment = {0}; +stralloc charset = {0}; +datetime_sec when; +struct datetime dt; + +char strnum[FMT_ULONG]; +char date[DATE822FMT]; +char hash[COOKIE]; +char boundary[COOKIE]; +stralloc line = {0}; +stralloc qline = {0}; +stralloc text = {0}; +stralloc quoted = {0}; +stralloc fnbase = {0}; +stralloc fnmsg = {0}; +stralloc fnnew = {0}; +stralloc fnsub = {0}; +char subbuf[256]; +substdio sssub; + +char *dir; + +struct stat st; + +struct qmail qq; + +void code_qput(s,n) +char *s; +unsigned int n; +{ + if (!flagcd) + qmail_put(&qq,s,n); + else { + if (flagcd == 'B') + encodeB(s,n,&qline,0,FATAL); + else + encodeQ(s,n,&qline,FATAL); + qmail_put(&qq,qline.s,qline.len); + } +} + +void transferenc() +{ + if (flagcd) { + qmail_puts(&qq,"\nContent-Transfer-Encoding: "); + if (flagcd == 'Q') + qmail_puts(&qq,"Quoted-printable\n\n"); + else + qmail_puts(&qq,"base64\n\n"); + } else + qmail_puts(&qq,"\n\n"); +} + +int checkfile(fn) +char *fn; +/* looks for DIR/mod/{pending|rejected|accept}/fn.*/ +/* Returns: */ +/* 1 found in pending */ +/* 0 not found */ +/* -1 found in accepted */ +/* -2 found in rejected */ +/* Handles errors. */ +/* ALSO: if found, fnmsg contains the o-terminated*/ +/* file name. */ +{ + + if (!stralloc_copys(&fnmsg,"mod/pending/")) die_nomem(); + if (!stralloc_cats(&fnmsg,fn)) die_nomem(); + if (!stralloc_0(&fnmsg)) die_nomem(); + if (stat(fnmsg.s,&st) == -1) { + if (errno != error_noent) + strerr_die6sys(111,FATAL,ERR_STAT,dir,"/",fnmsg.s,": "); + } else + return 1; + + if (!stralloc_copys(&fnmsg,"mod/accepted/")) die_nomem(); + if (!stralloc_cats(&fnmsg,fn)) die_nomem(); + if (!stralloc_0(&fnmsg)) die_nomem(); + if (stat(fnmsg.s,&st) == -1) { + if (errno != error_noent) + strerr_die6sys(111,FATAL,ERR_STAT,dir,"/",fnmsg.s,": "); + } else + return -1; + + if (!stralloc_copys(&fnmsg,"mod/rejected/")) die_nomem(); + if (!stralloc_cats(&fnmsg,fn)) die_nomem(); + if (!stralloc_0(&fnmsg)) die_nomem(); + if (stat(fnmsg.s,&st) == -1) { + if (errno != error_noent) + strerr_die6sys(111,FATAL,ERR_STAT,dir,"/",fnmsg.s,": "); + } else + return -2; + return 0; +} + +int qqwrite(fd,buf,len) int fd; char *buf; unsigned int len; +{ + qmail_put(&qq,buf,len); + return len; +} +char qqbuf[1]; +substdio ssqq = SUBSTDIO_FDBUF(qqwrite,-1,qqbuf,sizeof(qqbuf)); + +char inbuf[512]; +substdio ssin = SUBSTDIO_FDBUF(read,0,inbuf,sizeof(inbuf)); + +substdio sstext; +char textbuf[1024]; + +void maketo() +/* expects line to be a return-path line. If it is and the format is valid */ +/* to is set to to the sender. Otherwise, to is left untouched. Assuming */ +/* to is empty to start with, it will remain empty if no sender is found. */ +{ + unsigned int x, y; + + if (case_startb(line.s,line.len,"return-path:")) { + x = 12 + byte_chr(line.s + 12,line.len-12,'<'); + if (x != line.len) { + y = byte_rchr(line.s + x,line.len-x,'>'); + if (y + x != line.len) { + if (!stralloc_copyb(&to,line.s+x+1,y-1)) die_nomem(); + if (!stralloc_0(&to)) die_nomem(); + } /* no return path-> no addressee. A NUL in the sender */ + } /* is no worse than a faked sender, so no problem */ + } +} + +void main(argc,argv) +int argc; +char **argv; +{ + char *sender; + char *def; + char *local; + char *action; + int flaginheader; + int flagcomment; + int flaggoodfield; + int flagdone; + int fd, fdlock; + int match; + char *err; + char encin = '\0'; + char szchar[2] = "-"; + char *replyto = (char *) 0; + unsigned int start,confnum; + unsigned int pos,i; + int child; + int opt; + char *sendargs[4]; + char *cp,*cpnext,*cpfirst,*cplast,*cpafter; + int wstat; + + (void) umask(022); + sig_pipeignore(); + when = now(); + + if (!stralloc_copys(&sendopt," -")) die_nomem(); + while ((opt = getopt(argc,argv,"cCmMrRt:T:vV")) != opteof) + switch(opt) { /* pass on ezmlm-send options */ + case 'c': /* ezmlm-send flags */ + case 'C': + case 'r': + case 'R': + szchar[0] = (char) opt & 0xff; + if (!stralloc_append(&sendopt,szchar)) die_nomem(); + break; + case 'm': flagmime = 1; break; + case 'M': flagmime = 0; break; + case 't': + case 'T': if (optarg) replyto = optarg; break; + case 'v': + case 'V': strerr_die2x(0,"ezmlm-moderate version: ",EZIDX_VERSION); + default: + die_usage(); + } + + dir = argv[optind++]; + if (!dir) die_usage(); + + sender = env_get("SENDER"); + if (!sender) strerr_die2x(100,FATAL,ERR_NOSENDER); + local = env_get("LOCAL"); + if (!local) strerr_die2x(100,FATAL,ERR_NOLOCAL); + def = env_get("DEFAULT"); + + if (!*sender) + strerr_die2x(100,FATAL,ERR_BOUNCE); + if (!sender[str_chr(sender,'@')]) + strerr_die2x(100,FATAL,ERR_ANONYMOUS); + if (str_equal(sender,"#@[]")) + strerr_die2x(100,FATAL,ERR_BOUNCE); + + if (chdir(dir) == -1) + strerr_die4sys(111,FATAL,ERR_SWITCH,dir,": "); + + switch(slurp("key",&key,32)) { + case -1: + strerr_die4sys(111,FATAL,ERR_READ,dir,"/key: "); + case 0: + strerr_die4x(100,FATAL,dir,"/key",ERR_NOEXIST); + } + getconf_line(&mailinglist,"mailinglist",1,FATAL,dir); + getconf_line(&outhost,"outhost",1,FATAL,dir); + getconf_line(&outlocal,"outlocal",1,FATAL,dir); + set_cpoutlocal(&outlocal); /* for copy() */ + set_cpouthost(&outhost); /* for copy() */ + + if (def) { /* qmail>=1.02 */ + /* local should be >= def, but who knows ... */ + cp = local + str_len(local) - str_len(def) - 2; + if (cp < local) die_badformat(); + action = local + byte_rchr(local,cp - local,'-'); + if (action == cp) die_badformat(); + action++; + } else { /* older versions of qmail */ + getconf_line(&inlocal,"inlocal",1,FATAL,dir); + if (inlocal.len > str_len(local)) die_badaddr(); + if (case_diffb(inlocal.s,inlocal.len,local)) die_badaddr(); + action = local + inlocal.len; + if (*(action++) != '-') die_badaddr(); + } + + if (!action[0]) die_badformat(); + if (!str_start(action,ACTION_ACCEPT) && !str_start(action,ACTION_REJECT)) + die_badformat(); + start = str_chr(action,'-'); + if (!action[start]) die_badformat(); + confnum = 1 + start + str_chr(action + start + 1,'.'); + if (!action[confnum]) die_badformat(); + confnum += 1 + str_chr(action + confnum + 1,'.'); + if (!action[confnum]) die_badformat(); + if (!stralloc_copyb(&fnbase,action+start+1,confnum-start-1)) die_nomem(); + if (!stralloc_0(&fnbase)) die_nomem(); + cookie(hash,key.s,key.len,fnbase.s,"","a"); + if (byte_diff(hash,COOKIE,action+confnum+1)) + die_badformat(); + + fdlock = open_append("mod/lock"); + if (fdlock == -1) + strerr_die4sys(111,FATAL,ERR_OPEN,dir,"/mod/lock: "); + if (lock_ex(fdlock) == -1) + strerr_die4sys(111,FATAL,ERR_OBTAIN,dir,"/mod/lock: "); + + switch(checkfile(fnbase.s)) { + case 0: + strerr_die2x(100,FATAL,ERR_MOD_TIMEOUT); + case -1: /* only error if new request != action taken */ + if (str_start(action,ACTION_ACCEPT)) + strerr_die2x(0,INFO,ERR_MOD_ACCEPTED); + else + strerr_die2x(100,FATAL,ERR_MOD_ACCEPTED); + case -2: + if (str_start(action,ACTION_REJECT)) + strerr_die2x(0,INFO,ERR_MOD_REJECTED); + else + strerr_die2x(100,FATAL,ERR_MOD_REJECTED); + default: + break; + } +/* Here, we have an existing filename in fnbase with the complete path */ +/* from the current dir in fnmsg. */ + + if (str_start(action,ACTION_REJECT)) { + + if (qmail_open(&qq, (stralloc *) 0) == -1) + strerr_die2sys(111,FATAL,ERR_QMAIL_QUEUE); + + + /* Build recipient from msg return-path */ + fd = open_read(fnmsg.s); + if (fd == -1) { + if (errno != error_noent) + strerr_die4sys(111,FATAL,ERR_OPEN,fnmsg.s,": "); + else + strerr_die2x(100,FATAL,ERR_MOD_TIMEOUT); + } + substdio_fdbuf(&sstext,read,fd,textbuf,sizeof(textbuf)); + + if (getln(&sstext,&line,&match,'\n') == -1 || !match) + strerr_die2sys(111,FATAL,ERR_READ_INPUT); + maketo(); /* extract SENDER from return-path */ + /* Build message */ + qmail_puts(&qq,"Mailing-List: "); + qmail_put(&qq,mailinglist.s,mailinglist.len); + if(getconf_line(&line,"listid",0,FATAL,dir)) { + qmail_puts(&qq,"\nList-ID: "); + qmail_put(&qq,line.s,line.len); + } + qmail_puts(&qq,"\nDate: "); + datetime_tai(&dt,when); + qmail_put(&qq,date,date822fmt(date,&dt)); + qmail_puts(&qq,"Message-ID: <"); + if (!stralloc_copyb(&line,strnum,fmt_ulong(strnum,(unsigned long) when))) + die_nomem(); + if (!stralloc_append(&line,".")) die_nomem(); + if (!stralloc_catb(&line,strnum, + fmt_ulong(strnum,(unsigned long) getpid()))) die_nomem(); + if (!stralloc_cats(&line,".ezmlm@")) die_nomem(); + if (!stralloc_cat(&line,&outhost)) die_nomem(); + if (!stralloc_0(&line)) die_nomem(); + qmail_puts(&qq,line.s); + /* "unique" MIME boundary as hash of messageid */ + cookie(boundary,"",0,"",line.s,""); + qmail_puts(&qq,">\nFrom: "); + if (!quote("ed,&outlocal)) die_nomem(); + qmail_put(&qq,quoted.s,quoted.len); + qmail_puts(&qq,"-owner@"); + qmail_put(&qq,outhost.s,outhost.len); + if (replyto) { + qmail_puts(&qq,"\nReply-To: "); + qmail_puts(&qq,replyto); + } + qmail_puts(&qq, "\nTo: "); + qmail_puts(&qq,to.s); + qmail_puts(&qq,"\nSubject: "); + qmail_puts(&qq,TXT_RETURNED_POST); + qmail_put(&qq,quoted.s,quoted.len); + qmail_puts(&qq,"@"); + qmail_put(&qq,outhost.s,outhost.len); + + if (flagmime) { + if (getconf_line(&charset,"charset",0,FATAL,dir)) { + if (charset.len >= 2 && charset.s[charset.len - 2] == ':') { + if (charset.s[charset.len - 1] == 'B' || + charset.s[charset.len - 1] == 'Q') { + flagcd = charset.s[charset.len - 1]; + charset.s[charset.len - 2] = '\0'; + } + } + } else + if (!stralloc_copys(&charset,TXT_DEF_CHARSET)) die_nomem(); + if (!stralloc_0(&charset)) die_nomem(); + qmail_puts(&qq,"\nMIME-Version: 1.0\n"); + qmail_puts(&qq,"Content-Type: multipart/mixed;\n\tboundary="); + qmail_put(&qq,boundary,COOKIE); + qmail_puts(&qq,"\n\n--"); + qmail_put(&qq,boundary,COOKIE); + qmail_puts(&qq,"\nContent-Type: text/plain; charset="); + qmail_puts(&qq,charset.s); + transferenc(); + } + copy(&qq,"text/top",flagcd,FATAL); + copy(&qq,"text/mod-reject",flagcd,FATAL); + + flagcomment = 0; + flaginheader = 1; + if (!stralloc_copys(&text,"")) die_nomem(); + if (!stralloc_ready(&text,1024)) die_nomem(); + for (;;) { /* copy moderator's rejection comment */ + if (getln(&ssin,&line,&match,'\n') == -1) + strerr_die2sys(111,FATAL,ERR_READ_INPUT); + if (!match) break; + if (flaginheader) { + if (case_startb(line.s,line.len,"Content-Transfer-Encoding:")) { + pos = 26; + while (line.s[pos] == ' ' || line.s[pos] == '\t') ++pos; + if (case_startb(line.s+pos,line.len-pos,"base64")) + encin = 'B'; + else if (case_startb(line.s+pos,line.len-pos,"quoted-printable")) + encin = 'Q'; + } + if (line.len == 1) + flaginheader = 0; + } else + if (!stralloc_cat(&text,&line)) die_nomem(); + } /* got body */ + if (encin) { + if (encin == 'B') + decodeB(text.s,text.len,&line,FATAL); + else + decodeQ(text.s,text.len,&line,FATAL); + if (!stralloc_copy(&text,&line)) die_nomem(); + } + cp = text.s; + cpafter = text.s + text.len; + if (!stralloc_copys(&line,"\n>>>>> -------------------- >>>>>\n")) + die_nomem(); + flaggoodfield = 0; + flagdone = 0; + while ((cpnext = cp + byte_chr(cp,cpafter-cp,'\n')) != cpafter) { + i = byte_chr(cp,cpnext-cp,'%'); + if (i <= 5 && cpnext-cp >= 8) { + /* max 5 "quote characters" and space for %%% */ + if (cp[i+1] == '%' && cp[i+2] == '%') { + if (!flaggoodfield) { /* Start tag */ + if (!stralloc_copyb("ed,cp,i)) die_nomem(); /* quote chars*/ + flaggoodfield = 1; + cp = cpnext + 1; + cpfirst = cp; + continue; + } else { /* end tag */ + if (flagdone) /* 0 no comment lines, 1 comment line */ + flagdone = 2; /* 2 at least 1 comment line & end tag */ + break; + } + } + } + if (flaggoodfield) { + cplast = cpnext - 1; + if (*cplast == '\r') /* CRLF -> '\n' for base64 encoding */ + *cplast = '\n'; + else + ++cplast; + /* NUL is now ok, so the test for it was removed */ + flagdone = 1; + i = cplast - cp + 1; + if (quoted.len && quoted.len <= i && + !str_diffn(cp,quoted.s,quoted.len)) { /* quote chars */ + if (!stralloc_catb(&line,cp+quoted.len,i-quoted.len)) die_nomem(); + } else + if (!stralloc_catb(&line,cp,i)) die_nomem(); /* no quote chars */ + } + cp = cpnext + 1; + } + if (flagdone == 2) { + if (!stralloc_cats(&line,"<<<<< -------------------- <<<<<\n")) die_nomem(); + code_qput(line.s,line.len); + } + if (flagcd == 'B') { + encodeB("",0,&line,2,FATAL); + qmail_put(&qq,line.s,line.len); + } + if (flagmime) { + qmail_puts(&qq,"\n--"); + qmail_put(&qq,boundary,COOKIE); + qmail_puts(&qq,"\nContent-Type: message/rfc822\n\n"); + } else + qmail_puts(&qq,"\n"); + if (seek_begin(fd) == -1) + strerr_die4sys(111,FATAL,ERR_SEEK,fnmsg.s,": "); + + substdio_fdbuf(&sstext,read,fd,textbuf,sizeof(textbuf)); + if (substdio_copy(&ssqq,&sstext) != 0) + strerr_die4sys(111,FATAL,ERR_READ,fnmsg.s,": "); + close(fd); + + if (flagmime) { + qmail_puts(&qq,"\n--"); + qmail_put(&qq,boundary,COOKIE); + qmail_puts(&qq,"--\n"); + } + + if (!stralloc_copy(&line,&outlocal)) die_nomem(); + if (!stralloc_cats(&line,"-return-@")) die_nomem(); + if (!stralloc_cat(&line,&outhost)) die_nomem(); + if (!stralloc_0(&line)) die_nomem(); + qmail_from(&qq,line.s); + if (to.len) + qmail_to(&qq,to.s); + + if (!stralloc_copys(&fnnew,"mod/rejected/")) die_nomem(); + if (!stralloc_cats(&fnnew,fnbase.s)) die_nomem(); + if (!stralloc_0(&fnnew)) die_nomem(); + +/* this is strictly to track what happended to a message to give informative */ +/* messages to the 2nd-nth moderator that acts on the same message. Since */ +/* this isn't vital we ignore errors. Also, it is no big ideal if unlinking */ +/* the old file fails. In the worst case it gets acted on again. If we issue */ +/* a temp error the reject will be redone, which is slightly worse. */ + + if (*(err = qmail_close(&qq)) == '\0') { + fd = open_trunc(fnnew.s); + if (fd != -1) + close(fd); + unlink(fnmsg.s); + strnum[fmt_ulong(strnum,qmail_qp(&qq))] = 0; + strerr_die2x(0,"ezmlm-moderate: info: qp ",strnum); + } else + strerr_die3x(111,FATAL,ERR_TMP_QMAIL_QUEUE,err + 1); + + } else if (str_start(action,ACTION_ACCEPT)) { + fd = open_read(fnmsg.s); + if (fd == -1) + if (errno !=error_noent) + strerr_die4sys(111,FATAL,ERR_OPEN,fnmsg.s,": "); + else /* shouldn't happen since we've got lock */ + strerr_die3x(100,FATAL,fnmsg.s,ERR_MOD_TIMEOUT); + + substdio_fdbuf(&sstext,read,fd,textbuf,sizeof(textbuf)); + /* read "Return-Path:" line */ + if (getln(&sstext,&line,&match,'\n') == -1 || !match) + strerr_die2sys(111,FATAL,ERR_READ_INPUT); + maketo(); /* extract SENDER to "to" */ + env_put2("SENDER",to.s); /* set SENDER */ + if (seek_begin(fd) == -1) /* rewind, since we read an entire buffer */ + strerr_die4sys(111,FATAL,ERR_SEEK,fnmsg.s,": "); + +/* ##### NO REASON TO USE SH HERE ##### */ + sendargs[0] = "/bin/sh"; + sendargs[1] = "-c"; + if (argc > optind) { + sendargs[2] = argv[optind]; + } else { + if (!stralloc_copys(&send,auto_bin)) die_nomem(); + if (!stralloc_cats(&send,"/ezmlm-send")) die_nomem(); + if (sendopt.len > 2) + if (!stralloc_cat(&send,&sendopt)) die_nomem(); + if (!stralloc_cats(&send," '")) die_nomem(); + if (!stralloc_cats(&send,dir)) die_nomem(); + if (!stralloc_cats(&send,"'")) die_nomem(); + if (!stralloc_0(&send)) die_nomem(); + sendargs[2] = send.s; + } + sendargs[3] = 0; + + switch(child = fork()) { + case -1: + strerr_die2sys(111,FATAL,ERR_FORK); + case 0: /* child */ + close(0); + dup(fd); /* make fnmsg.s stdin */ + execv(*sendargs,sendargs); + if (errno == error_txtbsy || errno == error_nomem || + errno == error_io) + strerr_die5sys(111,FATAL,ERR_EXECUTE,"/bin/sh -c ",sendargs[2],": "); + else + strerr_die5sys(100,FATAL,ERR_EXECUTE,"/bin/sh -c ",sendargs[2],": "); + } + /* parent */ + wait_pid(&wstat,child); + close(fd); + if (wait_crashed(wstat)) + strerr_die3x(111,FATAL,sendargs[2],ERR_CHILD_CRASHED); + switch(wait_exitcode(wstat)) { + case 100: + strerr_die2x(100,FATAL,"Fatal error from child"); + case 111: + strerr_die2x(111,FATAL,"Temporary error from child"); + case 0: + break; + default: + strerr_die2x(111,FATAL,"Unknown temporary error from child"); + } + if (!stralloc_copys(&fnnew,"mod/accepted/")) die_nomem(); + + if (!stralloc_cats(&fnnew,fnbase.s)) die_nomem(); + if (!stralloc_0(&fnnew)) die_nomem(); +/* ignore errors */ + fd = open_trunc(fnnew.s); + if (fd != -1) + close(fd); + unlink(fnmsg.s); + _exit(0); + } +}