#include <stdio.h> #include <ctype.h> #include <signal.h> #include <sysexits.h> #include "string.h" #include "smtp.h" #include "mail.h" #include <sys/stat.h> /* * names of the limit files */ #define CON ".smtpscheds" #define NCON ".nsmtpscheds" #define OLD 1*3600L /* old C file -> try less often */ #define VOLD 6*3600L /* very old C file -> try much less often */ #define OLDW 1*3600L /* wait time between tries at old files */ #define VOLDW 4*3600L /* wait time between tries at very old files */ extern char *UPASROOT; int warn = -1; int removedays = -1; int cleanup; int verbose; int testmode; int Xonly; int Conly; string *replyaddr; string *dest; int mypid; char **getcmd(); #define MAXDEST 50 /* # destinations remembered */ #define MAXTPERD 5*60 /* total time allowed before skipping dests */ struct { string *dest; time_t time; } destlist[MAXDEST]; /* time consumed by unsuccessful tries at dest */ int ndest = 0; int debug; /* * actions to take when locking */ #define BLOCK 0 /* wait for 5 minutes if the directory is locked */ #define SKIP 1 /* skip the directory if it is locked */ #define IGNORE 2 /* don't lock the directory or care if it is */ /* * If called with arguments, those arguments are spool directories. Descend * each one processing the control files in it (C.* and X.*). * * If called without arguments, descend all spool directories. * * -s #scheds specifies a maximum number of concurrent smtpscheds. * -w #days causes users to be warned if their mail files are older * than #days days * -r #days causes mail older than #days days to be returned to sender * -c cleanup empty directories * -D debug * -L level logging level */ usage() { fprintf(stderr, "smtpsched [-cvtDL] [-w #days] [-r #days] [-s #scheds] [dir]\n"); exit(1); } main(ac, av) int ac; char *av[]; { DIR *dirp; Direct *dp; int max=0; int c; extern int optind; extern char *optarg; umask(2); mypid = getpid(); /* * avoid annoying distractions */ signal(SIGPIPE, SIG_IGN); signal(SIGHUP, SIG_IGN); Openlog("smtpsched", LOG_PID, LOG_SMTPSCHED); setlogmask(LOG_UPTO(LOG_INFO)); while ((c = getopt(ac, av, "XCDcvtr:w:s:L:")) != EOF) switch (c) { case 'X': Xonly++; break; case 'C': Conly++; break; case 't': testmode++; break; case 'v': verbose++; break; case 'c': cleanup++; break; case 'r': removedays = atoi(optarg); break; case 's': max = atoi(optarg); break; case 'w': warn = atoi(optarg); break; case 'L': setloglevel(optarg); break; case 'D': debug++; break; default: usage(); } /* * go to top spool directory */ if(chdir(SMTPQROOT)<0){ Syslog(LOG_ALERT, "can't chdir to %s\n", SMTPQROOT); exit(1); } /* * if there are too many running exit */ if(max && toomany(max)<0) exit(0); /* * If specific directories, do just them. Keep running the directory till there * is no change. */ if(optind!=ac){ for(; optind<ac; optind++) while(dodir(av[optind], SKIP) && !warn && !removedays) ; return 0; } /* * walk through all directories in top directory. the lock is * non-blocking (if neither r nor w options specified) to let * different instances of smtpsched skip over each other. */ dirp = opendir("."); if(dirp==NULL){ Syslog(LOG_ALERT, "couldn't read %s\n", SMTPQROOT); exit(1); } while(dp = readdir(dirp)){ if(strcmp(dp->d_name, ".")!=0 && strcmp(dp->d_name, "..")!=0) dodir(dp->d_name, (warn>=0 || removedays>=0) ? IGNORE : SKIP); } closedir(dirp); return 0; } /* * do both directions in a directory */ dodir(dname, action) char *dname; { int i, err; static string *ds; struct stat buf; if ((err=stat(dname, &buf)) < 0) return 0; if (!(buf.st_mode & S_IFDIR)) return 0; ds = s_reset(ds); s_append(ds, dname); if (debug) fprintf(stderr, "Checking %s\n", dname); i = dodirdir(s_to_c(ds), action, "X."); i += dodirdir(s_to_c(ds), action, "C."); return i; } /* * walk through all entries in this directory. process any * not starting with '.'. lock the directory before proceeding. */ dodirdir(dname, action, direction) char *dname; char *direction; { DIR *dirp; Direct *dp; int i; int changed=0; int ents=0, files=0; static string *ls; extern int errno; ls = s_reset(ls); s_append(ls, direction); s_append(ls, dname); /* * lock the directory. the lock is in the parent directory. */ switch(action){ case BLOCK: for(i=0; i<3; i++){ if(lock(s_to_c(ls))==0) break; if (debug) Syslog(LOG_DEBUG, "pausing for lock"); sleep(5); } if(i==3) return changed; break; case SKIP: if(lock(s_to_c(ls))<0){ Syslog(LOG_DEBUG, "couldn't lock %s\n", dname); return changed; } break; case IGNORE: break; } /* * descend into the directory. if it isn't a directory, * this will fail. */ if(chdir(dname)<0){ if(action != IGNORE) unlock(s_to_c(ls)); return changed; } /* * walk through the entries */ dirp = opendir("."); if(dirp==NULL){ Syslog(LOG_INFO, "couldn't read directory %s\n", dname); if(chdir(SMTPQROOT)<0){ Syslog(LOG_ALERT, "can't chdir back to SMTPQROOT\n"); exit(1); } if(action != IGNORE) unlock(s_to_c(ls)); return changed; } while(dp = readdir(dirp)){ if(strcmp(dp->d_name, ".")==0 || strcmp(dp->d_name, "..")==0) continue; files++; if (cleanup) continue; if (dp->d_name[0] == *direction) { switch(dofile(dname, dp->d_name)){ case 0: /* file removed */ changed = 1; break; case 1: /* file left alone */ ents += 1; break; } } } closedir(dirp); /* * go back up. symbolic links could be painful!!!! */ if(chdir(SMTPQROOT)<0){ Syslog(LOG_ALERT, "Can't chdir back to SMTPQROOT\n"); exit(1); } /* * cleanup empty directories */ if(cleanup && files==0){ Syslog(LOG_DEBUG, "%s empty\n", dname); if(rmdir(dname)<0) Syslog(LOG_ALERT, "can't unlink: %d\n", errno); } if(action != IGNORE) unlock(s_to_c(ls)); return changed; } /* * process a spool control file. control file names start with C. or * X. all error goes into an error file. * * return 0 if file removed, 1 otherwise. */ dofile(dname, name) char *dname; char *name; { int rv; int fd, ofd; char *ef; extern char *fileoftype(); struct stat sb; time_t now, Edate, Cdate; rv = -1; /* * if the file is inconsistent, remove it */ if(cleanup && inconsistent(name)){ Syslog(LOG_NOTICE, "%s/%s inconsistent\n", dname, name); unlink(name); return 0; } /* * if this is not a control file, ignore it */ if(name[1]!='.' || (name[0]!='C' && name[0]!='X')) return 1; /* * if file is too old, warn user and remove it. if checking age, * don't run the control file. */ if(warn>=0 || removedays>=0) { if(checkage(name)==0) { Syslog(LOG_NOTICE, "%s/%s too old\n", dname, name); doremove(name); return 0; } return 1; } /* * don't run control file when cleaning up */ if(cleanup) return 1; /* * Backoff scheme: don't try old C files very often */ ef = fileoftype('E', name); if (name[0]=='C') { now = time((time_t *)0); Cdate = now; Edate = now-VOLDW-1; if (stat(name, &sb)==0) Cdate = sb.st_ctime; if (stat(ef, &sb)==0) Edate = sb.st_mtime; if (now-Cdate>VOLD && now-Edate<VOLDW || now-Cdate>OLD && now-Edate<OLDW) { if (verbose) Syslog(LOG_DEBUG, "ignore %s/%s: not time yet\n", dname, name); if (debug==0) return 1; } } /* * in test mode, just return */ if (testmode) { Syslog(LOG_DEBUG, "would process %s/%s\n", dname, name); return 1; } /* * redirect output to an error file */ ofd = dup(2); close(2); fd = open(ef, 1); if(fd<0) fd = creat(ef, 0666); if(fd>=0){ lseek(fd, 0l, 2); /* * process the file */ if(name[0]=='C') { rv = dosmtp(dname, name); } else if(name[0]=='X') { rv = dormail(dname, name); } /* * get old error file back */ close(2); (void) dup(ofd); close(ofd); } /* * if processing was successful, remove the spool files */ if(rv==0) { doremove(name); return 0; } return 1; } /* * remove the control file, data file, and error file */ doremove(ctl) char *ctl; { fflush(stdout); unlink(fileoftype('E', ctl)); unlink(ctl); unlink(fileoftype('D', ctl)); } /* * run rmail. rmail takes care of its own errors, so if rmail fails, * just don't remove the files. */ dormail(dname, ctl) char *dname; char *ctl; { char **av; int rc; /* * fork off the command */ if ((av = getcmd(ctl, "/bin/rmail")) == NULL) { Syslog(LOG_WARNING, "Could not get rmail params for %s", ctl); return -1; } if ((rc = docmd(ctl, av)) == 0){ Syslog(LOG_DEBUG, "success"); return 0; } else { Syslog(LOG_DEBUG, "failed, rc = %d", rc); return -1; } } /* * run smtp. if an error occurs, determine its importance and send * a error mail message if it is fatal. */ dosmtp(dname, ctl) char *dname; char *ctl; { static string *cmd; int status, i; char **av; time_t t0, t1; /* * fork off the command */ cmd = s_reset(cmd); s_append(cmd, UPASROOT); s_append(cmd, "smtp"); av = getcmd(ctl, s_to_c(cmd)); if (av==NULL) { Syslog(LOG_WARNING, "Could not get smtp params for %s", ctl); return -1; } /* * Check whether unsuccessful attempts at this destination * have taken too much time. If so, pass over the file. */ for (i=0; i<ndest; i++) { if (strcmp(s_to_c(dest), s_to_c(destlist[i].dest))==0) { if (destlist[i].time > MAXTPERD) { Syslog(LOG_DEBUG, "passed %s (%d sec)\n", s_to_c(dest), destlist[i].time); fprintf(stderr, "can't contact destination\n"); return -1; } break; } } if (i==ndest) { if (ndest<MAXDEST) ndest++; else i = 0; /* loses storage */ /* * the following s_copy died on a malformed `C' file. The * contents of these files should be checked more carefully. */ destlist[i].dest = s_copy(s_to_c(dest)); destlist[i].time = 0; } time(&t0); switch(status=docmd(ctl, av)){ case 0: /* it worked */ Syslog(LOG_DEBUG, "success\n"); destlist[i].time = 0; return 0; case EX_UNAVAILABLE: /* service unavailable */ case EX_NOPERM: /* permission denied */ case EX_NOUSER: /* rejected by the other side */ case EX_NOHOST: /* host name unknown */ case EX_DATAERR: /* data format error */ case EX_USAGE: /* command line usage error */ Syslog(LOG_INFO, "failed with status %d\n", status); returnmail(ctl, 1); /*permanant failure*/ destlist[i].time = 0; return 0; case EX_CANTCREAT: /* can't create (user) output file */ case EX_IOERR: /* input/output error */ case EX_OSERR: /* system error (e.g., can't fork) */ case EX_OSFILE: /* critical OS file missing */ case EX_SOFTWARE: /* internal software error */ case EX_NOINPUT: /* cannot open input */ case EX_PROTOCOL: /* remote error in protocol */ /* gauss is having flakey datakit errors that confuse the * SMTP protocol. EX_PROTOCOL is a temporary error for gauss-ches*/ case EX_TEMPFAIL: /* temp failure; user is invited to retry */ Syslog(LOG_INFO, "temp fail with status %d\n", status); time(&t1); /*temporary failure*/ destlist[i].time += t1-t0; return -1; default: /* possibly a temporary problem */ Syslog(LOG_WARNING, "unknown fail with status %d\n", status); time(&t1); destlist[i].time += t1-t0; return -1; } } /* * open a control file and parse the first line. It contains * the reply address and the destination (for returning the mail). * * It leaves the control file open and returns the fp. */ FILE * parseline1(ctl) char *ctl; { FILE *fp; static string *line; fp = fopen(ctl, "r"); if(fp==NULL) return NULL; /* * get reply address and destination */ line = s_reset(line); if(s_read_line(fp, line)==NULL){ fprintf(stderr, "smtpsched: error reading ctl file %s: %s\n", ctl, s_to_c(line)); fclose(fp); return NULL; } replyaddr = s_parse(s_restart(line), s_reset(replyaddr)); if(replyaddr==NULL){ fprintf(stderr, "smtpsched: error reading ctl file replyaddr %s\n", ctl); fclose(fp); return NULL; } dest = s_parse(line, s_reset(dest)); if(dest==NULL){ fprintf(stderr, "smtpsched: error reading ctl file dest %s\n", ctl); fclose(fp); return NULL; } return fp; } /* * Read control file to get arguments for command. Leave dest and replyaddr * available. The control file has two lines. The first is reply address * and recipients. the second is the arguments for the command. */ char ** getcmd(ctl, cmd) char *ctl; char *cmd; { static string *args; FILE *fp; static char *av[1024]; int ac=0; char *cp; fp = parseline1(ctl); if (fp==NULL) return NULL; /* * make command line */ av[ac++] = cmd; args = s_reset(args); if(s_read_line(fp, args)==NULL){ fprintf(stderr, "smtpsched: error reading ctl file %s\n", ctl); fclose(fp); return NULL; } fclose(fp); cp = s_to_c(args); cp[strlen(cp) - 1] = '\0'; /*zap the newline*/ Syslog(LOG_INFO, "%s <%s", cmd, ctl); for(cp = s_to_c(args); *cp && ac<1023;){ av[ac++] = cp++; while(*cp && !isspace(*cp)) cp++; while(isspace(*cp)) *cp++ = 0; } av[ac] = 0; return av; } /* * execute a command, put standard error in the error file. */ docmd(ctl, av) char *ctl; char **av; { int fd; int pid, status; int n; /* * fork off the command */ switch(pid = fork()){ case -1: return -1; case 0: /* * make data file standard input */ close(0); fd = open(fileoftype('D', ctl), 0); if(fd<0){ perror("smtpsched: error reading data file:\n"); exit(1); } /* * make error file standard output */ close(1); fd = dup(2); /* * start the command */ execvp(av[0], av); exit(-2); default: /* * wait for the command to terminate */ while((n = wait(&status))>=0) if(n == pid) break; if(status&0xff) return -2; else return (status>>8)&0xff; } } /* * see if the number of consumers has been exceeded. if not, add this process * to the list. * * returns 0 if there were the number was not exceeded, -1 otherwise */ toomany(max) int max; { FILE *ifp=NULL; FILE *ofp=NULL; int cur=0; int pid; /* * lock consumers file */ if(lock(CON)<0) return -1; /* * open old and new consumer files */ ofp = fopen(NCON, "w"); if(ofp==NULL){ fprintf(stderr, "can't open %s\n", NCON); goto error; } ifp = fopen(CON, "r"); if(ifp!=NULL){ /* * see how many consumers are still around */ while(fscanf(ifp, "%d", &pid)==1){ if(kill(pid, 0) == 0){ cur++; if(fprintf(ofp, "%d\n", pid)<0){ fprintf(stderr, "error writing %s\n", NCON); goto error; } } } if(cur >= max) goto error; } /* * add us to the group of consumers */ if(fprintf(ofp, "%d\n", getpid())<0){ fprintf(stderr, "error writing %s\n", NCON); goto error; } if(ifp!=NULL) fclose(ifp); if(fclose(ofp)==EOF) goto error; unlink(CON); if(link(NCON, CON)<0) fprintf(stderr, "can't link %s to %s file\n", CON, NCON); unlink(NCON); unlock(CON); return 0; error: /* * too many consumers or we can't make a new consumer file */ if(ifp!=NULL) fclose(ifp); if(ofp!=NULL) fclose(ofp); unlink(NCON); unlock(CON); return -1; } /* * return true if the file is inconsistent. The following are inconsistent: * - a control file without a datafile * - an error file without a datafile * - a day old data file without a control file * - a limit file of any kind */ inconsistent(file) char *file; { struct stat s; int days; /* * switch on file type */ switch(file[0]){ case 'C': case 'X': /* * if no data file, control file is inconsistent */ if(stat(fileoftype('D', file), &s)<0) return 1; break; case 'E': /* * if no control file, error file is inconsistent */ if(stat(fileoftype('X', file), &s)<0 && stat(fileoftype('C', file), &s)<0) return 1; /* * if no data file, error file is inconsistent */ if(stat(fileoftype('D', file), &s)<0) return 1; break; case 'D': /* * look for a control file */ if(stat(fileoftype('X', file), &s)==0 || stat(fileoftype('C', file), &s)==0) break; /* * no control file, data file inconsistent if >=1 day old */ if(stat(file, &s)<0) return 0; days = (time((long *)0) - s.st_ctime)/(24*60*60); if(days>0) return 1; break; default: break; } return 0; } /* * check the age of a file. if it is greater than warn or remove, tell the * sender. return 0 if the file is to be removed, -1 otherwise. */ checkage(ctl) char *ctl; { struct stat s; int days; char buf[256]; FILE *fp; /* * get the file's age */ if(stat(ctl, &s)<0) return -1; days = (time((long *)0) - s.st_ctime)/(24*60*60); /* * check for removal */ if(removedays>=0 && days>=removedays){ fp = parseline1(ctl); if(fp==NULL) return -1; fclose(fp); Syslog(LOG_INFO,"returning mail to %s orig to %s after %d days", s_to_c(replyaddr), s_to_c(dest), days); return returnmail(ctl, 1); } /* * check for warning */ if(warn>=0 && days>=warn){ fp = parseline1(ctl); if(fp==NULL) return -1; fclose(fp); Syslog(LOG_INFO, "warning %s about %s after %d days", s_to_c(replyaddr), s_to_c(dest), days); returnmail(ctl, 0); } return -1; } /* * return a piece of mail with a reason for the return */ returnmail(ctl, warn) char *ctl; { int pid, status; string *cmd; int pfd[2]; char buf[132]; int fd, n; int reads; FILE *fp; FILE *ifp; long now; char asct[27]; if(pipe(pfd)<0) return -1; switch(pid=fork()){ case -1: close(pfd[0]); close(pfd[1]); return -1; case 0: /* * start up the mailer to take the refusal message */ close(0); dup(pfd[0]); close(pfd[1]); execl("/bin/rmail", "/bin/rmail", s_to_c(replyaddr), 0); exit(1); default: /* * pipe the refusal message to the mailer */ close(pfd[0]); fp = fdopen(pfd[1], "w"); if(fp==NULL) { close(pfd[1]); break; } /* * the From line */ now = time((long *)0); strcpy(asct, ctime(&now)); asct[24] = 0; fprintf(fp, "From postmaster %s remote from \n", asct); /* * the refusal message */ if(warn) { fprintf(fp, "Subject: smtp mail failed\n\n"); fprintf(fp, "Your mail to %s is undeliverable.\n", s_to_c(dest)); } else { fprintf(fp, "Subject: smtp mail warning\n\n"); fprintf(fp, "Your mail to %s is not yet delivered.\n", s_to_c(dest)); fprintf(fp, "Delivery attempts continue.\n"); } /* * then diagnosis of error */ fprintf(fp, "---------- diagnosis ----------\n"); ifp = fopen(fileoftype('E', ctl), "r"); if(ifp!=NULL){ for(reads=0; reads<20; reads++) { if(fgets(buf, sizeof(buf), ifp)==NULL) break; fputs(buf, fp); } fclose(ifp); } /* * finally the message itself */ fprintf(fp, "---------- unsent mail ----------\n"); ifp = fopen(fileoftype('D', ctl), "r"); if(ifp!=NULL){ for(reads=0; reads<50; reads++) { if(fgets(buf, sizeof(buf), ifp)==NULL) break; fputs(buf, fp); } fclose(ifp); } fclose(fp); /* * wait for the warning to finish */ while((n = wait(&status))>=0) if(n == pid) break; return status ? -1 : 0; } close(pfd[1]); return -1; }