/* Reply-o-Matic - Automatic message replying system Copyright (C) 2006 - Rodrigo Barbosa This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ // #define DEBUG 1 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "rom.h" /* Globals */ char usedflags[256]; int pfd[2], dfd[2], cpfd[2], cdfd[2]; /* Paranoid mode */ paranoid_t paranoid; /* Rate limiting */ char ratefile[256]; unsigned long int replyrate = 0; char needrateopen = 0; FILE *ratefd = NULL; char ratefn[256]; mode_t oldmask; /**/ /* Ignores */ char igfile[256]; FILE *igfd = NULL; char ighomefile[256]; char dochroot = 0, didchroot = 0; FILE *logfd = NULL; #define addflag(newflag) strncat(usedflags,newflag,255) /* Taken from mutt */ #define SKIPWS(x) while(isspace(*x))x++ #define NONULL(x) x?x:"" /**/ /* We need this so we can be sure the children will exit correctly */ void diedie ( unsigned int exsig ) { if ( cpfd[1] > 2 ) write(cpfd[1],"X",1); if ( cdfd[1] > 2 ) write(cdfd[1],"X",1); if ( igfd ) fclose(igfd); #ifdef DEBUG if ( logfd ) fclose(logfd); #endif umask(oldmask); closelog(); exit(exsig); } void p_log(char *fmt, ...) { va_list vaa; char logmessage[2049]; time_t tdata; va_start(vaa,fmt); vsprintf(logmessage,fmt,vaa); syslog(LOG_INFO,"%s",logmessage); tdata=time(NULL); #ifdef DEBUG if ( logfd ) fprintf(logfd,"%s [%d]: %s\n",ctime(&tdata),getpid(),logmessage); #endif va_end(vaa); } void p_err(char *fmt, ...) { va_list vaa; char logmessage[2049]; time_t tdata; va_start(vaa,fmt); vsprintf(logmessage,fmt,vaa); syslog(LOG_ERR,"%s",logmessage); tdata=time(NULL); #ifdef DEBUG if ( logfd ) fprintf(logfd,"%s [%d]: %s\n",ctime(&tdata),getpid(),logmessage); #endif va_end(vaa); } /* Functions taken from mutt, and adapted where (and when) needed */ int ascii_isupper (int c) { return (c >= 'A') && (c <= 'Z'); } int ascii_islower (int c) { return (c >= 'a') && (c <= 'z'); } int ascii_toupper (int c) { if (ascii_islower (c)) return c & ~32; return c; } int ascii_tolower (int c) { if (ascii_isupper (c)) return c | 32; return c; } int ascii_strcasecmp (const char *a, const char *b) { int i; if (a == b) return 0; if (a == NULL && b) return -1; if (b == NULL && a) return 1; for (; *a || *b; a++, b++) { if ((i = ascii_tolower (*a) - ascii_tolower (*b))) return i; } return 0; } int mutt_strcasecmp(const char *a, const char *b) { return strcasecmp(NONULL(a), NONULL(b)); } size_t mutt_strlen(const char *a) { return a ? strlen (a) : 0; } char * mutt_substrcpy (char *dest, const char *beg, const char *end, size_t destlen) { size_t len; len = end - beg; if (len > destlen - 1) len = destlen - 1; memcpy (dest, beg, len); dest[len] = 0; return dest; } /**/ /* Mostly copied from mutt */ void getmimetype (const char *path, char *mtype, size_t mtlen) { FILE *f; char *p, *q, *ct; char buf[2049]; char subtype[256], xtype[256]; int count; int szf, sze, cur_sze; int type; *subtype = '\0'; *xtype = '\0'; type = 0; cur_sze = 0; szf = mutt_strlen (path); for (count = 0 ; count < 3 ; count++) { /* * can't use strtok() because we use it in an inner loop below, so use * a switch statement here instead. */ switch (count) { case 0: snprintf (buf, sizeof (buf), ROMDIR"/mime.types"); break; case 1: strncpy (buf, "/etc/mime.types", sizeof(buf)); break; case 2: strncpy (buf, "/mime.types", sizeof(buf)); break; default: goto bye; /* shouldn't happen */ } if ((f = fopen (buf, "r")) != NULL) { while (fgets (buf, sizeof (buf) - 1, f) != NULL) { /* weed out any comments */ if ((p = (char *) strchr (buf, '#'))) *p = 0; /* remove any leading space. */ ct = buf; SKIPWS (ct); /* position on the next field in this line */ if ((p = (char *) strpbrk (ct, " \t")) == NULL) continue; *p++ = 0; SKIPWS (p); /* cycle through the file extensions */ while ((p = (char *) strtok (p, " \t\n"))) { sze = mutt_strlen (p); if ((sze > cur_sze) && (szf >= sze) && (mutt_strcasecmp (path + szf - sze, p) == 0 || ascii_strcasecmp (path + szf - sze, p) == 0) && (szf == sze || path[szf - sze - 1] == '.')) { /* get the content-type */ if ((p = (char *) strchr (ct, '/')) == NULL) { /* malformed line, just skip it. */ break; } *p++ = 0; for (q = p; *q && !isspace (*q); q++) ; mutt_substrcpy (subtype, p, q, sizeof (subtype)); strncpy (xtype, ct, sizeof (xtype)); cur_sze = sze; } p = NULL; } } fclose (f); } } bye: if (*xtype != '\0') { snprintf(mtype,mtlen,"%s/%s",xtype,subtype); } } /* Used to create the MIME delimiter */ int getrandom ( int num, char *randout ) { int rfd; struct stat rstatbuf; char rchar; int rcount = 0; rfd=open(RANDOM_DEV,O_RDONLY); if ( !rfd ) { p_err("Unable to open %s.",RANDOM_DEV); close(rfd); return(0); } fstat(rfd,&rstatbuf); if ( ! S_ISCHR(rstatbuf.st_mode) ) { p_err("%s is no a character device. No random value will be gathered",RANDOM_DEV); close(rfd); return(0); } randout[num]=0; while ( rcount < num ) { if ( read(rfd,&rchar,1) < 1 ) { randout[rcount+1]=0; close(rfd); return(rcount+1); } if ( isalnum(rchar) || ( rchar == '+' ) || ( rchar == '_' ) ) { randout[rcount]=rchar; rcount++; } } close(rfd); return(rcount); } /* My strcasecmp function */ int rom_strcasecmp ( char * str1, char * str2 ) { if ( strlen(str1) > strlen(str2) ) return(1); if ( strlen(str2) > strlen(str1) ) return(-1); return(strcasecmp(str1,str2)); } void help ( void ) { printf( \ "Reply-o-Matic v%s\n" \ "Copyright 2006 - Rodrigo Barbosa ] [ -b ]\n" \ " [ -s || -S ] [ -r ] [ -v ]\n" \ " [ -c ] [ -d ]\n" \ " [ -a ] [ -t ]\n" \ " [ -C [] [ -u ] [ -g ]\n"\ " [ -U ] [ -G ] [ -R []\n" \ "\n" \ "Parameters:\n" \ "\n" \ " -h[n] If n is 0, nothing in included on the reply\n" \ " If n is 1, the headers of the original message are included\n" \ " inlined on the reply message (default)\n" \ " If n is 2, the reply message goes as a mime multipart message\n" \ " with the full original message attached\n" \ "\n" \ " -f Set the From: field of the reply message\n" \ " -b A file that contains the text to be included on\n" \ " the reply message (MUST BE A REGULAR FILE!)\n" \ " -s Set the Subject: of the reply message\n" \ " -S Set the Subject line as a reply (RE: ) of the original one\n" \ " (NOTE: -S taked precedence over -s. When both are set, the string in\n" \ " -s is only used if there is not subject line on the original\n" \ " message\n" \ " -r Set the Reply-To: field of the reply message\n" \ " -v Show this page\n" \ " -c Send a copy (blind carbon copy) the reply message\n" \ " to this address too\n" \ " -d Deliver the original message to this e-mail address\n" \ " (useful when you want an autoreply, but still need to\n" \ " get the original message)\n" \ " -a Attach a file on the reply. The file will be send as a \n" \ " MIME attachment, trying to guess the correct mimetype by\n" \ " the file extension. See manpage for more details.\n" \ " -t Specify the Content-Type of the attachment. No effect if\n" \ " used without the -a parameter\n" \ " -C Make ROM run in a chrooted environment. Default chroot dir\n" \ " is /etc/rom/\n" \ " -u -g Make ROM drop it's privileges to the given uid and gid,\n" \ " instead of using it's defaults of 65534\n" \ " -U -G Make ROM run the MTA/MDA with the given uid and gid,\n" \ " instead of not dropping privileges to do it\n"\ " NOTICE: -u, -g, -U and -G will only take effect if the calling user\n" \ " is root (real uid = 0). Otherwhise, ROM will drop it's\n" \ " privileges to the uid and gid of the calling user\n" \ " -R Number of hours to wait before replying to the same\n" \ " originator address. The given value or 1 hour, is -R\n" \ " is used without a value. (Default: don't wait)\n" \ "\n"\ " SEE THE MANPAGE FOR MORE DETAILS\n"\ "\n"\ "This program is distributed in the hope that it will be useful,\n" \ "but WITHOUT ANY WARRANTY; without even the implied warranty of\n" \ "MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n" \ "GNU General Public License for more details.\n" \ "\n" \ , rom_version); diedie(-1); } char *getbody ( char *fn ) { FILE *fd; static char *outbody; struct stat statbuf; fd=fopen ( (const char *) fn, "r" ); if ( fd <= 0 ) { printf("unable to open %s\n",fn); help(); } fstat(fileno(fd),&statbuf); if ( !S_ISREG(statbuf.st_mode) ) { p_err("Bodyfile must be a regular file\n"); help(); } outbody=calloc(statbuf.st_size + 1,1); fread(outbody,1,statbuf.st_size,fd); fclose(fd); return (outbody); } void raw_to_base64 (unsigned char *, const unsigned char *, size_t, size_t); /* Read the attachment file, and return it's content */ unsigned char *getattach ( char *fn ) { int fd; static unsigned char *outbody; unsigned char *out64; struct stat statbuf; size_t olen; unsigned filesize; fd=open ( (const char *) fn, O_RDONLY ); if ( fd <= 0 ) { p_err("Unable to open attachment\n"); return(NULL); } fstat(fd,&statbuf); if ( !S_ISREG(statbuf.st_mode) ) { p_err("Attachments must be regular files\n"); return(NULL); } filesize=statbuf.st_size; outbody=calloc(filesize + 1,1); read(fd,outbody,filesize); close(fd); out64=calloc((filesize * 4) + 1,1); olen=filesize * 4; raw_to_base64(out64,outbody,filesize,olen); #ifdef DEBUG p_log("getattach data: %u %u %u %u\n",strlen(out64),statbuf.st_size,filesize,olen); #endif return(out64); } /* Header unfolding (RFC2822) */ int unfold ( char *data, char *newdata, unsigned int datalen ) { unsigned long int i; unsigned long int ndi = 0; memset(newdata,0,datalen); for(i=0;i' ) ) break; if ( isblank(inaddr[i]) ) continue; outaddr[strlen(outaddr)]=inaddr[i]; } return(outaddr); } void split_user_data ( char *uname, char *uaddr, char *data ) { int i,field=0; uname[0]=uaddr[0]=0; for(i=0;i 2 ) { write(fileno(ratefd),ratedata,strlen(ratedata)); if ( ratedata[strlen(ratedata)-1] != '\n' ) write(fileno(ratefd),"\n",1); } if ( found ) fprintf(ratefd,"%s %lu",userdata, (unsigned long) time(NULL)); lock.l_type=F_UNLCK; fcntl(fileno(ratefd),F_SETLKW,&lock); fclose(ratefd); return(found); } int main ( int argc, char **argv ) { char instr[2049]; int i,fni,fdi; char *fname = NULL; char *fdata = NULL; int isfield; char *tmpbuf; const char *optstr = "f:h::b:s:r:vd:c:Sa:t:C::u:g:U:G:R::"; char c; char includeorig = 1; char rom_from[2049] = default_from; char rom_subject[2049] = default_subject; char dosubject = 0; char *rom_body = NULL; char isbody = 0; char boundary[256]; char doattach = 0; char attachtype[256]; char attachpn[2049]; char dotype = 0; char *attachment = NULL; char *attachfn = NULL; int cpid, dpid = 0; char from[2049],sender[2049],replyto[2049],dest[2049],inreply[2049],replyaddr[2049],forwardto[2049],ccto[2049],subject[2049],precedence[2049]; int romuid = 65534, romgid = 65534; int mailuid = 0, mailgid = 0; int rread = 0; /* Needed for ignorefile copying */ struct passwd *pwent; struct stat cstatbuf; char cinfn[256]; char coutfn[256]; /**/ /* Rates */ char etcratefile[256]; /* Needed for chroot */ char chrootdir[256] = ROMDIR; char dogetbody = 0; char bodyfile[256]; /**/ /* Incoming message buffer */ char *header = NULL, *uheader = NULL, *body = NULL, *hdr = NULL; unsigned long int headerlen = 0, uheaderlen = 0, bodylen = 0; /**/ /* Open connection to syslogd */ openlog("reply-o-matic",LOG_PID,LOG_MAIL); #ifdef DEBUG logfd=fopen("/tmp/rom.log","a"); #endif /* Clearing buffers */ memset(from,0,2049); memset(sender,0,2049); memset(replyto,0,2049); memset(inreply,0,2049); memset(replyaddr,0,2049); memset(forwardto,0,2049); memset(ccto,0,2049); memset(dest,0,2049); memset(attachpn,0,2049); memset(subject,0,2049); memset(precedence,0,2049); memset(usedflags,0,256); memset(boundary,0,256); memset(attachtype,0,256); memset(bodyfile,0,256); memset(ratefile,0,256); memset(etcratefile,0,256); memset(igfile,0,256); memset(cinfn,0,256); memset(coutfn,0,256); memset(ratefn,0,256); /* Getting paranoid */ oldmask=umask(077); getparanoid(); dochroot=paranoid.chroot ? 1 : 0; if ( dochroot ) addflag("C"); /* Need to define a boundary for mime multipart */ if ( getrandom(15,boundary) < 15 ) { snprintf(boundary,255,"=_ReplyOMatic_Default_Boundary_QAPLWSOKEDIJRFUHTYG"); } else { tmpbuf=calloc(255,1); snprintf(tmpbuf,255,"=_ReplyOMatic_%s_Boundary",boundary); strncpy(boundary,tmpbuf,255); free(tmpbuf); } if ( argc > 1 ) while ( (c = getopt(argc,argv,optstr)) ) { if ( c != -1 ) switch ( c ) { case 'h' : if ( optarg ) { if ( strlen(optarg) > 1 ) help(); switch ( optarg[0] ) { case '0': includeorig = atoi(optarg); addflag("h0"); break; case '1': includeorig = atoi(optarg); addflag("h1"); break; case '2': includeorig = atoi(optarg); addflag("h2"); break; default: help(); } } else { includeorig=1; addflag("h1"); } break; case 'f' : strncpy(rom_from,optarg,2048); addflag("f"); break; case 'S' : dosubject |= 2; addflag("S"); break; case 's' : strncpy(rom_subject,optarg,2048); dosubject |= 1; addflag("s"); break; case 'b' : dogetbody++; strncpy(bodyfile,optarg,255); addflag("b"); break; case 'r' : strncpy(replyaddr,optarg,2048); addflag("r"); break; case 'c' : strncpy(ccto,optarg,2048); addflag("c"); break; case 'd' : strncpy(forwardto,optarg,2048); if (!valid_emailaddress(forwardto)) { p_err("-d provided with an invalid e-mail address\n"); exit(0); } addflag("d"); break; case 'v' : help(); break; case 't' : if ( dotype ) { p_err("Only one mimetype can be specified. Only the first will take effect."); break; } strncpy(attachtype,optarg,255); addflag("t"); dotype++; break; case 'a' : if ( paranoid.noattach ) break; if ( attachment ) { p_err("Only one attachment can be specified. Only the first will take effect."); break; } doattach++; strncpy(attachpn,optarg,2048); addflag("a"); break; case 'u' : if ( paranoid.lockuid ) break; romuid = atoi (optarg); if ( romuid == 0 ) romuid = 65534; break; case 'U' : if ( paranoid.lockuid ) break; mailuid = atoi (optarg); break; case 'g' : if ( paranoid.lockgid ) break; romgid = atoi (optarg); if ( romgid == 0 ) romgid = 65534; break; case 'G' : if ( paranoid.lockgid ) break; mailgid = atoi (optarg); break; case 'C' : if ( paranoid.chroot ) break; if ( geteuid() != 0 ) { p_err("Need to be root to use chroot()\nRunning without chroot()\n"); break; } if ( optarg ) { strncpy(chrootdir,optarg,255); } dochroot=1; addflag("C"); break; case 'R' : if ( !replyrate ) { if ( optarg ) replyrate=atol(optarg); else replyrate=REPLYRATE; addflag("R"); } break; default: help(); } else break; } pipe(pfd); pipe(cpfd); switch ( cpid=fork() ) { case 0: close(0); close(pfd[1]); dup2(pfd[0],0); if ( getuid() == 0 ) { if ( mailgid ) setgid(mailgid); if ( mailuid ) setuid(mailuid); } else { setgid(getgid()); setuid(getuid()); } /* Stupid, but effective, why to prevent errors from * the MTA * It will only start when we tell it to. */ fcntl(cpfd[0],F_SETFL, O_NONBLOCK); tmpbuf=calloc(2,1); while ( read(cpfd[0],tmpbuf,1) < 1 ) { sleep(1); } if ( tmpbuf[0] == 'X' ) { exit(0); } free(tmpbuf); execl("/usr/sbin/sendmail","/usr/sbin/sendmail","-bm","-t",NULL); printf("Exec error on child: %d\n",errno); break; case -1: diedie(2); break; default: break; } if ( forwardto[0] != 0 ) { pipe(dfd); pipe(cdfd); switch ( dpid=fork() ) { case 0: close(0); close(dfd[1]); dup2(dfd[0],0); if ( getuid() == 0 ) { if ( mailgid ) setgid(mailgid); if ( mailuid ) setuid(mailuid); } else { setgid(getgid()); setuid(getuid()); } /* Stupid, but effective, why to prevent errors from * the MTA * It will only start when we tell it to. */ fcntl(cdfd[0],F_SETFL, O_NONBLOCK); tmpbuf=calloc(2,1); while ( (rread = read(cdfd[0],tmpbuf,1)) < 1 ) { if ( (rread < 0) && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK) ) sleep(1); else exit(0); } if ( tmpbuf[0] == 'X' ) { exit(0); } free(tmpbuf); execl("/usr/sbin/sendmail","/usr/sbin/sendmail","-bm",forwardto,NULL); printf("Exec error on child: %d\n",errno); break; case -1: diedie(2); break; default: break; } } pwent=getpwuid(getuid()); /* Define ratefile basename */ sprintf(ratefile,".uid-%u",getuid()); /* Try opening it on the users homedir */ snprintf(ratefn,255,"%s/.rom_rates",pwent->pw_dir); snprintf(etcratefile,255,"%s/.rates/%s",ROMDIR,ratefile); if (!stat(ratefn,&cstatbuf)) { /* Only accept if it's a regular file */ if ( S_ISREG(cstatbuf.st_mode) ) { ratefd=fopen(ratefn,"a+"); rewind(ratefd); needrateopen=1; } } if ( !ratefd && stat(etcratefile,&cstatbuf) ) { ratefd=fopen(ratefn,"w"); needrateopen=2; if ( geteuid() == 0 ) fchown(fileno(ratefd),getuid(),getgid()); } /* Let's copy the rates file from the user homedir, if it exists */ sprintf(igfile,".uid-%u",getuid()); snprintf(cinfn,255,"%s/.rom_ignores",pwent->pw_dir); /* Opening the .rom_ignores file on homedir */ if (!stat(cinfn,&cstatbuf)) { /* Only accept if it's a regular file */ if ( S_ISREG(cstatbuf.st_mode) ) { igfd=fopen(cinfn,"r"); } } /**/ if ( dochroot ) if ( chroot(chrootdir) == 0 ) { chdir("/"); didchroot=1; } /* Dropping privileges is root */ if ( geteuid() == 0 ) { if ( getuid() == 0 ) { setgid(romgid); setuid(romuid); } else { setgid(getgid()); setuid(getuid()); } } header=calloc(1,1); body=calloc(1,1); while (!feof(stdin)) { memset(instr,0,2049); fgets(instr,2048,stdin); if ( feof(stdin) ) break; if ( strlen(instr) == 1 ) { isbody=1; } else { if ( ( strlen(instr) == 2 ) && ( instr[0] == '\r' ) && ( instr[1] == '\n' ) ) isbody=1; } if (!isbody) { header=realloc(header,headerlen+strlen(instr)+1); headerlen += strlen(instr); strcat(header,instr); header[headerlen]=0; } else { body=realloc(body,bodylen+strlen(instr)+1); bodylen += strlen(instr); strcat(body,instr); body[bodylen]=0; } } uheader=calloc(headerlen+1,1); uheaderlen=unfold(header,uheader,headerlen); hdr=uheader; while ( (unsigned long int) hdr < ( (unsigned long int) uheader + uheaderlen ) ) { unsigned int idx = 0; memset(instr,0,2049); while ( 1 ) { if ( (unsigned long int) hdr > ( (unsigned long int) uheader + uheaderlen ) ) break; instr[idx++]=hdr[0]; if ( hdr[0] == '\n' ) { hdr++; break; } hdr++; } isfield=1; fname=calloc(strlen(instr) + 2,1); fdata=calloc(strlen(instr) + 2,1); fni=fdi=0; for (i=0;i