// See the companion program store.c for more documentation // The control-file format is very simple; it is simply a // list of SMTP commands to execute. Once the file is // done, the data is sent (taking care to escape single dots) // and terminated with a "." and "RSET", ready for the next // mail. All mails are sent in one connection where // possible. Any error causes this program to exit; the // higher level script which invokes this must take care // of retries. Putting this in cron to be invoked every // five minutes is probably safe. #define VERSION "0.1.0" // History: // 0.0.0a Internal development 20050811 // 0.1.0 First release 20050811 // 0.1.1 Fixed some error handling static int from_server, to_server; /* streams */ int ConnTimeout = 6; /* timeout in secs for initial connect */ #include "common.c" int verbose = 0, debug = 0; typedef int bool; #define incopy(a) *((struct in_addr *)a) #define NOT_DOTTED_QUAD ((u_long)-1) #define MAXHOSTNAME 256 /* maximum size of an hostname */ #define MAXADDRS 35 /* max address count from gethostnamadr.c */ char *MyHostName = NULL; /* my own fully qualified host name */ static jmp_buf Timeout; static void timer(int sig) { longjmp(Timeout, 1); } #include "connect.c" // Read a response from an SMTP server, including possible continuation lines // Return the numerical response as the function result (untested & unused so far) int smtp_read_reply(void) { unsigned int i, n100, n10, n1; char code[4]; for (;;) { int c; i = 0; for (;;) { c = get(from_server, ReadTimeout); if (c == EOF) break; if (c == '\n') break; if (c == '\r') {i = 0; continue;} if (i < 4) code[i++] = c; } if (c == EOF) break; if ((i == 4) && (code[3] == '-')) continue; // continuation line if (c == '\n') break; } n100 = code[0] - '0'; n10 = code[1] - '0'; n1 = code[2] - '0'; if ((n100 < 10) && (n10 < 10) && (n1 < 10)) { return(n100*100+n10*10+n1); } // actually if it gets here we're in big trouble... return(501); } /* This may be useful: 4.2.1. REPLY CODES BY FUNCTION GROUPS 500 Syntax error, command unrecognized [This may include errors such as command line too long] 501 Syntax error in parameters or arguments 502 Command not implemented 503 Bad sequence of commands 504 Command parameter not implemented 211 System status, or system help reply 214 Help message [Information on how to use the receiver or the meaning of a particular non-standard command; this reply is useful only to the human user] 220 <domain> Service ready 221 <domain> Service closing transmission channel 421 <domain> Service not available, closing transmission channel [This may be a reply to any command if the service knows it must shut down] 250 Requested mail action okay, completed 251 User not local; will forward to <forward-path> 450 Requested mail action not taken: mailbox unavailable [E.g., mailbox busy] 550 Requested action not taken: mailbox unavailable [E.g., mailbox not found, no access] 451 Requested action aborted: error in processing 551 User not local; please try <forward-path> 452 Requested action not taken: insufficient system storage 552 Requested mail action aborted: exceeded storage allocation 553 Requested action not taken: mailbox name not allowed [E.g., mailbox syntax incorrect] 354 Start mail input; end with <CRLF>.<CRLF> 554 Transaction failed */ //------------------------------------------------------------ // Open the given job file, send the email, followed by RSET // Assumes already talking to an SMTP server, and leaves // the connection open. // Renames the .job file to .bye on completion // TO DO: **OUGHT TO** rename .job to .run during the send so that // multiple instances of this code can run - must test // rc of rename in order to avoid duplication (cheaper test than // file locking) // Note that the mail body file is found by stripping the .job // extension from the given file name int forward(char *jobname) { FILE *jobfile, *datafile; char *s; int rc, sent_ok; fprintf(stderr, "Sending %s\n", jobname); { char *p, runname[MAX_STRING+1]; strcpy(runname, jobname); p = strrchr(runname, '.'); if (p == NULL) return(FALSE); strcpy(p, ".run"); rc = rename(jobname, runname); if (rc != 0) return(FALSE); // Returning TRUE would allow it to handle the next file // however it would give the impression that this one was // sent OK which is probably not wanted. strcpy(jobname, runname); } jobfile = fopen(jobname, "r"); if (jobfile == NULL) return(FALSE); s = strrchr(jobname, '.'); if (s == NULL) return(FALSE); *s = '\0'; datafile = fopen(jobname, "r"); if (datafile == NULL) return(FALSE); *s = '.'; for (;;) { int c; for (;;) { c = fgetc(jobfile); if (c == EOF) break; put(to_server, "%c", c); if (c == '\n') break; } if (c == EOF) break; // commands from script file: (void)smtp_read_reply(); // we'll allow these to fail if they do } // Don't tweak this gratuitously! /* The reason for the above is that you can get multiple RCPT TO command, some of which may be valid and some of which may be for non-existent users, in both real mail and in spam. We *could* exit if we get all failures for every rcpt command, but that requires quite a bit of logic, and the remote MTA will reject the DATA command if that happens anyway. So this way we may get a couple of extra failures, but as long as we check the status when we send the DATA, nothing calamitous should happen. */ fclose(jobfile); { #define STARTLINE 1 #define IN_LINE 2 int c, state = STARTLINE; put(to_server, "DATA\n"); rc = smtp_read_reply(); if ((rc / 100) != 3) { fclose(datafile); return(FALSE); } for (;;) { c = fgetc(datafile); if (c == EOF) break; if ((c == '.') && (state == STARTLINE)) put(to_server, "."); if (c == '\n') state = STARTLINE; else state = IN_LINE; put(to_server, "%c", c); } if (state == IN_LINE) put(to_server, "\n"); put(to_server, ".\n"); sent_ok = smtp_read_reply(); fclose(datafile); } // OOPS :-( I just spotted a bug that I haven't yet corrected - // smtp_read_reply() returns an SMTP status code, not true/false! // Need to modify the test below. 18 Oct 2005 10am. Check back tomorrow. if (!sent_ok) { return(FALSE); // reasons for rejection may include virus in data // so we don't want to requeue .job file } // If successful, rename .run to .bye and let controlling daemon clean up after us put(to_server, "RSET\n"); (void)smtp_read_reply(); { char *p, deadname[MAX_STRING+1]; strcpy(deadname, jobname); p = strrchr(deadname, '.'); if (p == NULL) return(FALSE); strcpy(p, ".bye"); rename(jobname, deadname); } return(TRUE); } // List files in spool directory, open connection to remote MTA, // send all files, then close connection. Exit on any errors. int main(int argc, char **argv) { FILE *files; char command[MAX_STRING+1], jobfile[MAX_STRING+1]; int rc; if (argc != 2) {fprintf(stderr, "usage: forward hostname\n"); exit(1);} // comment out the line below to remove debugging logfile = fopen(tmpnam(NULL), "w"); // Need to use find rather than "ls -1 *.job" because large directories // will exceed the shell's ability to expand wildcards. There are // cleaner ways than this to get a list of matching filenames, but // it works for now. May clean it up later. sprintf(command, FIND_COMMAND, SPOOLDIR); files = popen(command, "r"); {int c = fgetc(files); // Exit if no files queued. Allow higher-level to do the wait if (c == EOF) exit(2); // thus avoiding gratuitous smtp connection. ungetc(c, files); // (even a pipe ought to allow 1 char of pushback) } rc = makeconnection(argv[1], &to_server, &from_server); if (rc != 0) { // Very likely remote host is down, so exit. Calling script will retry. fprintf(stderr, "forward: Cannot connect to %s\n", argv[1]); exit(rc); } rc = smtp_read_reply(); // Read the SMTP welcome banner if ((rc / 100) != 2) { exit(rc); } for (;;) { // Loop over all files int c; char *p = jobfile; for (;;) { // read one filename c = fgetc(files); if (c == EOF) break; if (c == '\n') break; if (p-jobfile+2 < MAX_STRING) { // shouldn't exceed, but avoid buffer overflow in case *p++ = c; *p = '\0'; // doesn't fail gracefully but doesn't allow exploit either } } if (c == EOF) break; if (!forward(jobfile)) break; // send the mail to the next hop } pclose(files); put(to_server, "QUIT\n"); // close down cleanly. may already be closed. (void)smtp_read_reply(); // if it was closed we'll have a short timeout here // don't care if smtp error returned exit(0); return(1); }