/************************************************************************ * procmail - The autonomous mail processor * * * * It has been designed to be able to be run suid root and (in * * case your mail spool area is *not* world writable) sgid * * mail (or daemon), without creating security holes. * * * * Seems to be perfect. * * * * Copyright (c) 1990-1999, S.R. van den Berg, The Netherlands * * Copyright (c) 1999-2001, Philip Guenther, The United States * * of America * * #include "../README" * ************************************************************************/ #ifdef RCS static /*const*/char rcsid[]= "$Id: procmail.c,v 1.183 2001/08/31 04:57:36 guenther Exp $"; #endif #include "../patchlevel.h" #include "procmail.h" #include "acommon.h" #include "sublib.h" #include "robust.h" #include "shell.h" #include "misc.h" #include "memblk.h" #include "pipes.h" #include "common.h" #include "cstdio.h" #include "exopen.h" #include "mcommon.h" #include "goodies.h" #include "locking.h" #include "mailfold.h" #include "lastdirsep.h" #include "authenticate.h" #include "lmtp.h" #include "foldinfo.h" #include "variables.h" #include "comsat.h" #include "from.h" static const char*const nullp,exflags[]=RECFLAGS,drcfile[]="Rcfile:", pmusage[]=PM_USAGE,*etcrc=ETCRC,misrecpt[]="Missing recipient\n", extrns[]="Extraneous ",ignrd[]=" ignored\n",pardir[]=chPARDIR, defspath[]=DEFSPATH,defmaildir[]=DEFmaildir; char*buf,*buf2,*loclock; const char shell[]="SHELL",lockfile[]="LOCKFILE",newline[]="\n",binsh[]=BinSh, unexpeof[]="Unexpected EOL\n",*const*gargv,*const*restargv= &nullp,*sgetcp, pmrc[]=PROCMAILRC,*rcfile,dirsep[]=DIRSEP,devnull[]=DevNull,empty[]="", lgname[]="LOGNAME",executing[]="Executing",oquote[]=" \"",cquote[]="\"\n", procmailn[]="procmail",whilstwfor[]=" whilst waiting for ",home[]="HOME", host[]="HOST",*defdeflock=empty,*argv0=empty,curdir[]={chCURDIR,'\0'}, slogstr[]="%s \"%s\"",conflicting[]="Conflicting ",orgmail[]="ORGMAIL", insufprivs[]="Insufficient privileges\n",defpath[]=DEFPATH, exceededlb[]="Exceeded LINEBUF\n",errwwriting[]="Error while writing to", Version[]=VERSION; int retval=EX_CANTCREAT,retvl2=EXIT_SUCCESS,sh,pwait,rc= -1, privileged=priv_START,lexitcode=EXIT_SUCCESS,ignwerr,crestarg,savstdout, berkeley,mailfilter,erestrict,Deliverymode,ifdepth; /* depth of outermost */ struct dyna_array ifstack; size_t linebuf=mx(DEFlinebuf,1024/*STRLEN(systm_mbox)<<1*/); volatile int nextexit,lcking; /* if termination is imminent */ pid_t thepid; long filled,lastscore; /* the length of the mail, and the last score */ memblk themail; /* the mail */ char*thebody; /* the body of the message */ uid_t uid; gid_t gid,sgid; static auth_identity*savepass(spass,uid)auth_identity*const spass; const uid_t uid; { const auth_identity*tpass; if(auth_filledid(spass)&&auth_whatuid(spass)==uid) goto ret; if(tpass=auth_finduid(uid,0)) /* save by copying */ { auth_copyid(spass,tpass); ret: return spass; } return (auth_identity*)0; } #define rct_ABSOLUTE 0 /* rctypes for tryopen() */ #define rct_CURRENT 1 #define rct_DEFAULT 2 #define rcs_DELIVERED 1 /* rc exit codes for mainloop() */ #define rcs_EOF 2 #define rcs_HOST 3 static void usage P((void)); static int tryopen P((const int delay_setid,const int rctype,const int dowarning)), mainloop P((void)); int main(argc,argv)int argc;const char*const argv[]; { register char*chp,*chp2; #if 0 /* enable this if you want to trace procmail */ kill(getpid(),SIGSTOP);/*raise(SIGSTOP);*/ #endif newid(); ;{ int presenviron,override;char*fromwhom=0; const char*idhint=0;gid_t egid=getegid(); presenviron=Deliverymode=mailfilter=override=0; Openlog(procmailn,LOG_PID,LOG_MAIL); /* for the syslogd */ if(argc) /* sanity check, any argument at all? */ { Deliverymode=!!strncmp(lastdirsep(argv0=argv[0]),procmailn, STRLEN(procmailn)); for(argc=0;(chp2=(char*)argv[++argc])&&*chp2=='-';) for(;;) /* processing options */ { switch(*++chp2) { case VERSIONOPT: usage(); return EXIT_SUCCESS; case HELPOPT1:case HELPOPT2:elog(pmusage);elog(PM_HELP); elog(PM_QREFERENCE); return EX_USAGE; case PRESERVOPT:presenviron=1; continue; case MAILFILTOPT:mailfilter=1; continue; case OVERRIDEOPT:override=1; continue; case BERKELEYOPT:case ALTBERKELEYOPT:berkeley=1; continue; case TEMPFAILOPT:retval=EX_TEMPFAIL; continue; case FROMWHOPT:case ALTFROMWHOPT: if(*++chp2) fromwhom=chp2; else if(chp2=(char*)argv[argc+1]) argc++,fromwhom=chp2; else nlog("Missing name\n"); break; case ARGUMENTOPT: { static struct dyna_array newargv; if(*++chp2) goto setarg; else if(chp2=(char*)argv[argc+1]) { argc++; setarg: app_valp(newargv,(const char*)chp2); restargv=&(acc_valp(newargv,0)); crestarg++; } else nlog("Missing argument\n"); break; } case DELIVEROPT: if(!*(chp= ++chp2)&&!(chp=(char*)argv[++argc])) { nlog(misrecpt); break; } else { Deliverymode=1; goto last_option; } case LMTPOPT: #ifdef LMTP Deliverymode=2; goto last_option; #else nlog("LMTP support not enabled in this binary\n"); return EX_USAGE; #endif case '-': if(!*++chp2) { argc++; goto last_option; } default:nlog("Unrecognised options:");logqnl(chp2); elog(pmusage);elog("Processing continued\n"); case '\0':; } break; } } if(Deliverymode==1&&!(chp=chp2)) nlog(misrecpt),Deliverymode=0; last_option: switch(Deliverymode) { case 0: idhint=getenv(lgname); if(mailfilter&&crestarg) { crestarg=0; /* -m will supersede -a */ conflopt: nlog(conflicting);elog("options\n");elog(pmusage); } break; #ifdef LMTP case 2: if(fromwhom) { fromwhom=0; /* -z disables -f, */ goto confldopt; /* -p and -m */ } #endif case 1: if(presenviron||mailfilter) confldopt: { presenviron=mailfilter=0; /* -d disables -p and -m */ goto conflopt; } break; default: /* this cannot happen */ abort(); } cleanupenv(presenviron); ;{ auth_identity*pass,*passinvk;auth_identity*spassinvk; uid_t euid=geteuid(); uid=getuid();gid=getgid(); spassinvk=auth_newid();passinvk=savepass(spassinvk,uid); /* are we */ checkprivFrom_(euid,passinvk?auth_username(passinvk):0,override); doumask(INIT_UMASK); /* allowed to set the From_ line? */ while((savstdout=rdup(STDOUT))<=STDERR) { rclose(savstdout); /* move stdout out of the way */ if(0>(savstdout=opena(devnull))) goto nodevnull; syslog(LOG_ALERT,"Descriptor %d was not open\n",savstdout); } fclose(stdout);rclose(STDOUT); /* just to make sure */ if(0>opena(devnull)) nodevnull: { writeerr(devnull);syslog(LOG_EMERG,slogstr,errwwriting,devnull); return EX_OSFILE; /* couldn't open /dev/null */ } #ifdef console opnlog(console); #endif setbuf(stdin,(char*)0);allocbuffers(linebuf,1); #ifdef SIGXCPU signal(SIGXCPU,SIG_IGN);signal(SIGXFSZ,SIG_IGN); #endif #ifdef SIGLOST signal(SIGLOST,SIG_IGN); #endif #if DEFverbose verboff();verbon(); #else verbon();verboff(); #endif #ifdef SIGCHLD signal(SIGCHLD,SIG_DFL); #endif signal(SIGPIPE,SIG_IGN); setcomsat(empty); /* turn on biff by default */ ultstr(0,(unsigned long)uid,buf);filled=0; if(!passinvk||!(chp2=(char*)auth_username(passinvk))) chp2=buf; #ifdef LMTP if(Deliverymode==2) { auth_identity**rcpts,**lastrcpt,**currcpt; currcpt=rcpts=lmtp(&lastrcpt,chp2); if(currcpt+1!=lastrcpt) /* if there's more than one recipient */ lockblock(&themail); /* then no one can write to the block */ else /* otherwise the only recipient */ private(1); /* gets the original */ while(currcptbopen(buf)) /* try opening the rcfile */ { if(dowarning) rerr: readerr(buf); return 0; } if(!delay_setid&&privileged) /* if we're not supposed to delay */ { closerc(); /* and we haven't changed yet, then close it, */ setids(); /* transmogrify to prevent peeking, */ if(0>bopen(buf)) /* and try again */ goto rerr; /* they couldn't read it, so it was bogus */ } #ifndef NOfstat if(fstat(rc,&stbuf)) /* the right way */ #else if(stat(buf,&stbuf)) /* the best we can */ #endif { static const char susprcf[]="Suspicious rcfile"; suspicious_rc: closerc();nlog(susprcf);logqnl(buf); syslog(LOG_ALERT,slogstr,susprcf,buf); goto rerr; } if(delay_setid) /* change now if we haven't already */ setids(); if(rctype==rct_CURRENT) /* opened rcfile in the current directory? */ { if(!didchd) setmaildir(curdir); } else /* * OK, so now we have opened an absolute rcfile, but for security * reasons we only accept it if it is owned by the recipient or * root and is not world writable, and the directory it is in is * not world writable or has the sticky bit set. If this is the * default rcfile then we also outlaw group writability. */ { register char*chp=lastdirsep(buf),c; c= *chp; if(((stbuf.st_uid!=uid&&stbuf.st_uid!=ROOT_uid|| /* check uid, */ (stbuf.st_mode&S_IWOTH)|| /* writable by others, */ rctype==rct_DEFAULT&& /* if the default then also check */ (stbuf.st_mode&S_IWGRP)&& /* for writable by group, */ (NO_CHECK_stgid||stbuf.st_gid!=gid) )&&strcmp(devnull,buf)|| /* /dev/null is a special case, */ (*chp='\0',stat(buf,&stbuf))|| /* check the directory, */ #ifndef CAN_chown /* sticky and can't chown */ !(stbuf.st_mode&S_ISVTX)&& /* means we don't care if */ #endif /* it's group or world writable */ ((stbuf.st_mode&(S_IWOTH|S_IXOTH))==(S_IWOTH|S_IXOTH)|| rctype==rct_DEFAULT&& (stbuf.st_mode&(S_IWGRP|S_IXGRP))==(S_IWGRP|S_IXGRP)&& (NO_CHECK_stgid||stbuf.st_gid!=gid)))) { *chp=c; goto suspicious_rc; } *chp=c; } yell(drcfile,buf); /* * Chdir now if we haven't already */ if(!didchd) /* have we done this already? */ { const char*chp; if(buildpath(maildir,defmaildir,(char*)0)) exit(EX_OSERR); /* something was wrong: give up the ghost */ if(chdir(chp=buf)) { chderr(buf); /* no, well, then try an initial chdir */ chp=tgetenv(home); if(!strcmp(chp,buf)||chdir(chp)) chderr(chp),chp=curdir; /* that didn't work, use "." */ } setmaildir(chp); } return 1; /* we're good to go */ } static int mainloop P((void)) { int lastsucc,lastcond,prevcond,i,skiprc;register char*chp,*tolock; lastsucc=lastcond=prevcond=skiprc=0; tolock=0; do { unlock(&loclock); /* unlock any local lockfile */ goto commint; do { skipline(); commint:do skipspace(); /* skip whitespace */ while(testB('\n')); } while(testB('#')); /* no comment :-) */ if(testB(':')) /* check for a recipe */ { int locknext,succeed;char*startchar;long tobesent; static char flags[maxindex(exflags)]; do { int nrcond; if(readparse(buf,getb,0,skiprc)) return rcs_EOF; /* give up on this one */ ;{ char*temp; /* so that chp isn't forced */ nrcond=strtol(buf,&temp,10);chp=temp; /* into memory */ } if(chp==buf) /* no number parsed */ nrcond= -1; if(tolock) /* clear temporary buffer for lockfile name */ free(tolock); for(i=maxindex(flags);i;i--) /* clear the flags */ flags[i]=0; for(tolock=0,locknext=0;;) { chp=skpspace(chp); switch(i= *chp++) { default: ;{ char*flg; if(!(flg=strchr(exflags,i))) /* a valid flag? */ { chp--; break; } flags[flg-exflags]=1; /* set the flag */ } case '\0': if(chp!=Tmnate) /* if not the real end, skip */ continue; break; case ':':locknext=1; /* yep, local lockfile specified */ if(*chp||++chp!=Tmnate) tolock=tstrdup(chp),chp=strchr(chp,'\0')+1; } concatenate(chp);skipped(chp); /* display leftovers */ break; } /* parse & test the conditions */ i=conditions(flags,prevcond,lastsucc,lastcond,skiprc!=0,nrcond); if(!skiprc) { if(!flags[ALSO_NEXT_RECIPE]&&!flags[ALSO_N_IF_SUCC]) lastcond=i==1; /* save the outcome for posterity */ if(!prevcond||!flags[ELSE_DO]) prevcond=i==1; /* ditto for `else if' like constructs */ } } while(i==2); /* missing in action, reiterate */ startchar=themail.p;tobesent=filled; if(flags[PASS_HEAD]) /* body, header or both? */ { if(!flags[PASS_BODY]) tobesent=thebody-themail.p; } else if(flags[PASS_BODY]) tobesent-=(startchar=thebody)-themail.p; Stdout=0;succeed=sh=0; pwait=flags[WAIT_EXIT]|flags[WAIT_EXIT_QUIET]<<1; ignwerr=flags[IGNORE_WRITERR];skipspace(); if(i) zombiecollect(),concon('\n'); progrm: if(testB('!')) /* forward the mail */ { if(!i) skiprc|=1; if(strlcpy(buf,sendmail,linebuf)>=linebuf) goto fail; chp=strchr(buf,'\0'); if(*flagsendmail) { char*q;int got=0; if(!(q=simplesplit(chp+1,flagsendmail,buf+linebuf-1,&got))) goto fail; *(chp=q)='\0'; } if(readparse(chp+1,getb,0,skiprc)) goto fail; if(i) { if(startchar==themail.p) { startchar[filled]='\0'; /* just in case */ startchar=(char*)skipFrom_(startchar,&tobesent); } /* leave off leading From_ -- it confuses some mailers */ goto forward; } skiprc&=~1; } else if(testB('|')) /* pipe the mail */ { chp=buf2; if(getlline(chp,buf2+linebuf)) /* get the command to start */ goto commint; if(i) { metaparse(buf2); if(!sh&&buf+1==Tmnate) /* just a pipe symbol? */ { *buf='|';*(char*)(Tmnate++)='\0'; /* fake it */ goto tostdout; } forward: if(locknext) { if(!tolock) /* an explicit lockfile specified already */ { *buf2='\0'; /* find the implicit lockfile ('>>name') */ for(chp=buf;i= *chp++;) if(i=='>'&&*chp=='>') { chp=skpspace(chp+1); tmemmove(buf2,chp,i=strcspn(chp,EOFName)); buf2[i]='\0'; if(sh) /* expand any environment variables */ { chp=tstrdup(buf);sgetcp=buf2; if(readparse(buf,sgetc,0,0)) { *buf2='\0'; goto nolock; } strcpy(buf2,buf);strcpy(buf,chp);free(chp); } break; } if(!*buf2) nolock: { nlog("Couldn't determine implicit lockfile from"); logqnl(buf); } } lcllock(tolock,buf2); if(!pwait) /* try and protect the user from his */ pwait=2; /* blissful ignorance :-) */ } rawnonl=flags[RAW_NONL]; if(flags[CONTINUE]&&(flags[FILTER]||Stdout)) nlog(extrns),elog("copy-flag"),elog(ignrd); inittmout(buf); if(flags[FILTER]) { if(startchar==themail.p&&tobesent!=filled) /* if only 'h' */ { if(!pipthrough(buf,startchar,tobesent)) readmail(1,tobesent),succeed=!pipw; } else if(!pipthrough(buf,startchar,tobesent)) { filled=startchar-themail.p; readmail(0,tobesent); succeed=!pipw; } } else if(Stdout) /* capturing stdout again? */ succeed=!pipthrough(buf,startchar,tobesent); else if(!pipin(buf,startchar,tobesent,1)) /* regular program */ { succeed=1; if(flags[CONTINUE]) goto logsetlsucc; else goto frmailed; } goto setlsucc; } } else if(testB(EOF)) nlog("Incomplete recipe\n"); else /* dump the mail into a mailbox file or directory */ { int ofiltflag;char*end=buf+linebuf-4; /* reserve some room */ if(ofiltflag=flags[FILTER]) flags[FILTER]=0,nlog(extrns),elog("filter-flag"),elog(ignrd); if(chp=gobenv(buf,end)) /* can it be an environment name? */ { if(chp==end) { getlline(buf,buf+linebuf); goto fail; } if(skipspace()) chp++; /* keep pace with argument breaks */ if(testB('=')) /* is it really an assignment? */ { int c; *chp++='=';*chp='\0'; if(skipspace()) chp++; ungetb(c=getb()); switch(c) { case '!':case '|': /* ok, it's a pipe */ if(i) Stdout = tstrdup(buf); goto progrm; } } } /* find the end, start of a nesting recipe? */ else if((chp=strchr(buf,'\0'))==buf&& testB('{')&& (*chp++='{',*chp='\0',testB(' ')|| /* } } */ testB('\t')|| testB('\n'))) { if(locknext&&!flags[CONTINUE]) nlog(extrns),elog("locallockfile"),elog(ignrd); if(flags[PASS_BODY]) nlog(extrns),elog("deliver-body flag"),elog(ignrd); if(flags[PASS_HEAD]) nlog(extrns),elog("deliver-head flag"),elog(ignrd); if(flags[IGNORE_WRITERR]) nlog(extrns),elog("ignore-write-error flag"),elog(ignrd); if(flags[RAW_NONL]) nlog(extrns),elog("raw-mode flag"),elog(ignrd); if(!i) /* no match? */ skiprc+=2; /* increase the skipping level */ else { app_vali(ifstack,prevcond); /* push prevcond */ app_vali(ifstack,lastcond); /* push lastcond */ if(locknext) { lcllock(tolock,""); if(!pwait) /* try and protect the user from his */ pwait=2; /* blissful ignorance :-) */ } succeed=1; if(flags[CONTINUE]) { yell("Forking",procmailn); private(0); /* can't share anymore */ inittmout(procmailn);onguard(); if(!(pidchild=sfork())) /* clone yourself */ { if(loclock) /* lockfiles are not inherited */ free(loclock),loclock=0; if(globlock) free(globlock),globlock=0; /* clear up the */ newid();offguard();duprcs(); /* identity crisis */ } else { offguard(); if(forkerr(pidchild,procmailn)) succeed=0; /* tsk, tsk, no cloning today */ else { int excode; /* wait for our significant other? */ if(pwait&& (excode=waitfor(pidchild))!=EXIT_SUCCESS) { if(!(pwait&2)||verbose) /* do we report it? */ progerr(procmailn,excode,pwait&2); succeed=0; } pidchild=0;skiprc+=2; /* skip over the braces */ ifstack.filled-=2; /* retract the stack */ } } } goto setlsucc; /* definitely no logabstract */ } continue; } if(!i) /* no match? */ skiprc|=1; /* temporarily disable subprograms */ if(readparse(chp,getb,0,skiprc)) fail: { succeed=0; goto setlsucc; } if(i) { if(ofiltflag) /* protect who use bogus filter-flags */ startchar=themail.p,tobesent=filled; /* whole message */ tostdout: rawnonl=flags[RAW_NONL]; if(locknext) /* write to a file or directory */ lcllock(tolock,buf); inittmout(buf); /* to break messed-up kernel locks */ if(writefolder(buf,strchr(buf,'\0')+1,startchar,tobesent, ignwerr,0)&& (succeed=1,!flags[CONTINUE])) frmailed: { if(ifstack.vals) free(ifstack.vals); return rcs_DELIVERED; } logsetlsucc: if(succeed&&flags[CONTINUE]&&lgabstract==2) logabstract(tgetenv(lastfolder)); setlsucc: rawnonl=0;lastsucc=succeed;lasttell= -1; /* for comsat */ resettmout(); /* clear any pending timer */ } skiprc&=~1; /* reenable subprograms */ } } /* { */ else if(testB('}')) /* end block */ { if(skiprc>1) /* just skipping */ skiprc-=2; /* decrease level */ else if(ifstack.filled>ifdepth) /* restore lastcond from stack */ { lastcond=acc_vali(ifstack,--ifstack.filled); prevcond=acc_vali(ifstack,--ifstack.filled); /* prevcond */ } /* as well */ else nlog("Closing brace unexpected\n"); /* stack empty */ } else /* then it must be an assignment */ { char*end=buf+linebuf; if(!(chp=gobenv(buf,end))) { if(!*buf) /* skip a word first */ getbl(buf,end); /* then a line */ skipped(buf); /* display leftovers */ continue; } if(chp==end) /* overflow => give up */ break; skipspace(); if(testB('=')) /* removal or assignment? */ { *chp='='; if(readparse(++chp,getb,1,skiprc)) continue; } else *++chp='\0'; /* throw in a second terminator */ if(!skiprc) { const char*p; p=sputenv(buf); chp[-1]='\0'; asenv(p); } } if(rc<0) /* abnormal exit from the rcfile? */ return rcs_HOST; } while(!testB(EOF)||(skiprc=0,poprc())); return rcs_EOF; }