/* This is a simple SMTP server which implements the minimal set of commands required to accept mail. All it does is accept mail and store it in a file. It should accept mail from any MTA although as of 0.1.1 it has not yet been tested against any except sendmail and postfix. This is half of a simple store-and-forward server, suitable for backup MX handling, and could also be used as a spamtrap/honeypot, or as a front end to some custom project such as a spam filter. The code is written in fairly basic C and should be easy to modify for custom tasks. (SMTP was never meant to be a complex protocol, and it's entirely reasonable to code in it directly without the overhead of packages such as libsmtp) This isn't fancy or complex code; it took an afternoon to write and debug. I had looked for similar code on the net but all the ones I found were too complex, too hard to install, or were written in obscure languages. Invoke it by adding it to inetd.conf. It requires no privileges except write access to the spool directory. See README.txt for more information. Graham Toal <gtoal@gtoal.com> 10 Aug 2005 You'll find that this version of the source has some conditional code in it that if enabled would cause it to only accept mail for a specific domain and certain users. I did that to my own copy to cut down the overhead of rejecting spam on my main server, as my domain happened to have several thousand dead accounts on it due to having previously been in use as an ISP. I'm leaving the code in the source as an example of what you can do with this program. Obviously you would never want to use that modification verbatim, without first changing the domain and the users... */ #define VERSION "0.1.2" // History: // 0.0.0a Internal development 20050808 // 0.1.0 First public release 20050810 // 0.1.1 Restructured for easier maintenance // 0.1.2 Fixed some error codes // TO DO: There is still some code in here left over from the // smtpfilter project, relating to timeouts in the SMTP session. // I need to review it to confirm that it is relevant in this // context. #include "common.c" static int from_client, to_client; /* streams */ int main (int argc, char **argv) { FILE *spoolfile; FILE *controlfile; char *spoolname, *tmpname, *controlname; static char comm[4], code[4]; /* command, and return code */ char connecting_host[MAX_STRING + 1], helo_host[MAX_STRING + 1]; char domain[MAX_STRING + 1], username[MAX_STRING + 1]; int accept = FALSE, i, c, rc; #define DRAIN() \ for (;;) {\ c = get (from_client, ReadTimeout);\ if (c == '\r')\ continue;\ if (c == EOF)\ debug_exit (3);\ if (c == '\n')\ break;\ } // if you don't want debugging logs, remove the line below. logfile = fopen(tmpnam(NULL), "w"); starttime = time (NULL); // these may be meaningless without putting connection into raw mode? setvbuf (stdin, NULL, _IONBF, BUFSIZ); setvbuf (stdout, NULL, _IONBF, BUFSIZ); // I could hard-wire these numbers (ie '0') but this is probably safer... from_client = fileno(stdin); // fd of stdin to_client = fileno(stdout); // fd of stdout tmpname = spoolname = controlname = NULL; spoolfile = controlfile = NULL; connecting_host[0] = helo_host[0] = username[0] = domain[0] = '\0'; // Initialisation. { // Determine IP of caller... struct sockaddr_storage name; int namelen = sizeof (name); struct in_addr addr; strcpy (connecting_host, "0.0.0.0"); /* Default is an invalid address */ // test a friendly address by telneting to localhost // test a hostile address by running this code from the command-line memset (&name, 0, sizeof (name)); if (getpeername (from_client, (struct sockaddr *) &name, &namelen) >= 0) { addr = ((struct sockaddr_in *) &name)->sin_addr; // Had some portability issues with the IPV6 code. Removed it. // if (name.ss_family == AF_INET) { (void) inet_ntop (AF_INET, &addr, connecting_host, MAX_STRING); // } } connecting_host[MAX_STRING - 1] = '\0'; /* just in case */ } if (setjmp (RDTimeout) != 0) { // Dead-man's switch. This should suicide an smtpfilter process which // has run away with the CPU. Set it long enough that it doesn't // interfere with real connections, and enable/disable it only around // code areas where we may be seeing the problem. debug_exit (0); } accept = FALSE; // if even one recipient is OK, we'll take the mail. (void) signal (SIGALRM, dtimer); (void) alarm ((unsigned) DeadMansTimeout); put (to_client, "220 localhost SMTP Backupmx\n"); (void) alarm ((unsigned) 0); // Cancel the alarm call for (;;) { // Loop on each command from sender i = 0; // We use a non-standard timeout while waiting for a command. // We use the regular timeout in all other places (at the moment) (void) signal (SIGALRM, dtimer); (void) alarm ((unsigned) DeadMansTimeout); c = get (from_client, CmdTimeout); for (;;) { if (!((c == '\r') || (c == ' ') || (c == '\t'))) { if (c == EOF) debug_exit (4); if (isalpha (c) && isupper (c)) c = tolower (c); // for strncmp later comm[i++] = c; if (c == '\n') break; if (i == 4) break; } // Once we've started reading a command, we make the timeout even // shorter c = get (from_client, ShortTimeout); } if (c == '\n') { // we don't support any 1, 2, or 3 letter commands put (to_client, "500 5.5.1 Command unrecognized: \""); write (to_client, comm, i - 1); if (logfile != NULL) {write (fileno(logfile), comm, i - 1); fflush(logfile);} put (to_client, "\"\n"); } else if (strncmp (comm, "data", 4) == 0) { int mailsize = 0; int state; DRAIN(); if (controlfile == NULL || spoolfile == NULL) { put (to_client, "503 5.0.0 Need MAIL command\n"); } else if (!accept) { put (to_client, "503 5.0.0 Need RCPT (recipient)\n"); } else { put (to_client, "354 Enter mail, end with \".\" on a line by itself\n"); // fprintf (spoolfile, "From MAILER-DAEMON %s", ctime (&starttime) // /* ctime includes \n */); fprintf (spoolfile, "Received: from %s ([%s]) by %s", helo_host, connecting_host, "backupmx"); // how can we get our own name? fprintf (spoolfile, " for %s@%s; %s", username, domain, ctime (&starttime)); /* Small state machine to track the sending of mail body. Try to avoid writing the final "." to the output file... */ #define STARTLINE 1 #define DOTSTART 2 #define DOTTED 3 #define INLINE 4 state = STARTLINE; { int Timeout = ReadTimeout; for (;;) { int lastc = c; do { c = get (from_client, Timeout); } while (c == '\r'); if (c == EOF) { fflush (spoolfile); fclose (spoolfile); remove (spoolname); // and other cleanup needed debug_exit (5); // broken connection on input => drop // it! } if ((c == '.') && (state == STARTLINE)) state = DOTSTART; else if ((state == DOTSTART) && (c == '\n')) state = DOTTED; else if (c == '\n') state = STARTLINE; else if (c == '\r') /* do nothing */; else state = INLINE; if (state == DOTTED) break; if (state != DOTSTART) { // Is this still needed, now that we have no back-end // code such as spamassassin which might take a long time // to respond? mailsize += 1; if ((mailsize & 0xfffff) == 0xfffff) (void) alarm ((unsigned) DeadMansTimeout); // ... tickle the alarm, some mails were taking more // than 5 minutes to arrive. fputc (c, spoolfile); } } } // fprintf (spoolfile, "\n"); // TEMP: make it unix mbox format for now fflush (spoolfile); fclose (spoolfile); spoolfile = NULL; fflush(controlfile); fclose(controlfile); controlfile = NULL; (void) alarm ((unsigned) 0); // Cancel the alarm call (void) signal (SIGALRM, dtimer); // reset before invoking spamprobe (void) alarm ((unsigned) DeadMansTimeout); put (to_client, "250 2.0.0 %s Message accepted for delivery\n", spoolname); // AT THIS POINT WE RENAME THE .tmp FILE to .job // When the forwarder sees a .job file, it is safe to execute the job { int i; char *jobname; if (controlname == NULL) { fprintf(stderr, "Assertion failure\n"); debug_exit (12); } jobname = strdup(controlname); i = strlen(jobname); if ((i > 3) && (strcmp(&jobname[i-4], EXT) == 0)) { jobname[i-3] = 'j'; jobname[i-2] = 'o'; jobname[i-1] = 'b'; } rename(controlname, jobname); } } } else if (strncmp (comm, "rcpt", 4) == 0) { int state; char *domainp, *usernamep; domainp = domain; usernamep = username; *domainp = '\0'; *usernamep = '\0'; i = 0; if (controlfile == NULL || spoolfile == NULL) { DRAIN(); put (to_client, "503 5.0.0 Need MAIL command\n"); } else { // Small state machine to extract username@domain from "rcpt to" // command. this came from code which was extracting this info // from a stream and did not care about extra fields, so it is // very lax in what it accepts. #define PRE 1 #define GRAB_USERNAME 2 #define GRAB_DOMAIN 3 #define DONE 4 state = PRE; for (;;) { c = get (from_client, ReadTimeout); if (c == '\r') continue; if (c == EOF) debug_exit (6); if (c == '\n') break; if ((state == GRAB_USERNAME) || (state == GRAB_DOMAIN)) { // canonicalise case in username and domain if (isalpha (c) && isupper (c)) c = tolower (c); } if ((state == PRE) && (c == '<')) { state = GRAB_USERNAME; i = 0; } else if ((state == GRAB_USERNAME) && (c == '>')) { state = DONE; strcpy (domain, ""); // DEFAULT_DOMAIN domainp = domain + strlen (domain); // [TO DO!] #define for local default domain above } else if ((state == GRAB_DOMAIN) && (c == '>')) { state = DONE; } else if ((state == GRAB_USERNAME) && (c == '@')) { state = GRAB_DOMAIN; i = 0; } else if ((state == GRAB_USERNAME) && (c != ' ')) { // truncate if username too long (might be x500 address :-( // // ) if (i < (MAX_STRING - 1)) { *usernamep++ = c; i += 1; } } else if ((state == GRAB_DOMAIN) && (c != ' ')) { // truncate if domain too long - it's probably a hack // attempt if (i < (MAX_STRING - 1)) { *domainp++ = c; i += 1; } } } #undef PRE #undef GRAB_USERNAME #undef GRAB_DOMAIN #undef DONE *domainp = '\0'; *usernamep = '\0'; // SAVE USERNAME@DOMAIN (or just USERNAME) to control file. fprintf(controlfile, "rcpt to:<%s@%s>\n", username, domain); #ifdef LOCAL_VTCOM_HACK if (((strcasecmp(domain, "vt.com") == 0) && /* Only the listed 4 users at this domain */ ((strcasecmp(username, "gtoal") == 0) || (strcasecmp(username, "bobf") == 0) || (strcasecmp(username, "susanf") == 0) || (strcasecmp(username, "postmaster") == 0)) ) || (strcasecmp(domain, "gtoal.com") == 0) /* Any user at this domain */ || (strcasecmp(domain, "feldtman.com") == 0) /* or this one */ ) { put (to_client, "250 2.1.5 <%s@%s>... Recipient ok\n", username, domain); accept = TRUE; } else { // 550 5.1.1 <jednfhjfh@vt.com>... User unknown put (to_client, "550 5.1.1 <%s@%s>... User unknown\n", username, domain); } #else /* DEFAULT BEHAVIOUR: */ put (to_client, "250 2.1.5 <%s@%s>... Recipient ok\n", username, domain); accept = TRUE; #endif } } else if (strncmp (comm, "mail", 4) == 0) { int state; char *domainp, *usernamep; if ((controlfile != NULL) && (spoolfile != NULL)) { DRAIN(); put (to_client, "503 5.5.0 Sender already specified\n"); // } else if (...) { // polite people say helo first } else { accept = FALSE; // becomes TRUE when we get a valid RCPT TO // belt & braces: if (controlfile != NULL) fclose(controlfile);controlfile = NULL; if (spoolfile != NULL) fclose(spoolfile);spoolfile = NULL; tmpname = tmpnam (NULL); if (spoolname != NULL) free(spoolname); spoolname = malloc(strlen(tmpname)+strlen(SPOOLDIR)+1); sprintf(spoolname, "%s%s", SPOOLDIR, tmpname); if (controlname != NULL) free(controlname); controlname = malloc(strlen(spoolname)+strlen(EXT)+1); sprintf(controlname, "%s%s", spoolname, EXT); spoolfile = fopen (spoolname, "w"); /* 4.X.X Persistent Transient Failure A persistent transient failure is one in which the message as sent is valid, but some temporary event prevents the successful sending of the message. Sending in the future may be successful. X.2.X Mailbox Status Mailbox status indicates that something having to do with the mailbox has cause this DSN. Mailbox issues are assumed to be under the general control of the recipient. 3.3 Mailbox Status X.2.0 Other or undefined mailbox status The mailbox exists, but something about the destination mailbox has caused the sending of this DSN. X.2.1 Mailbox disabled, not accepting messages The mailbox exists, but is not accepting messages. This may be a permanent error if the mailbox will never be re-enabled or a transient error if the mailbox is only temporarily disabled. X.2.2 Mailbox full The mailbox is full because the user has exceeded a per-mailbox administrative quota or physical capacity. The general semantics implies that the recipient can delete messages to make more space available. This code should be used as a persistent transient failure. X.2.3 Message length exceeds administrative limit A per-mailbox administrative message length limit has been exceeded. This status code should be used when the per-mailbox message length limit is less than the general system limit. This code should be used as a permanent failure. X.2.4 Mailing list expansion problem The mailbox is a mailing list address and the mailing list was unable to be expanded. This code may represent a permanent failure or a persistent transient failure. */ if (spoolfile == NULL) { DRAIN(); put (to_client, "450 4.2.0 Requested mail action not taken: mailbox unavailable - cannot open %s - %s\n", spoolname, strerror(errno)); debug_exit (7); // more graceful failover needed? } controlfile = fopen (controlname, "w"); if (controlfile == NULL) { DRAIN(); put (to_client, "450 4.2.0 Requested mail action not taken: mailbox unavailable - cannot open %s - %s\n", controlname, strerror(errno)); debug_exit (8); // more graceful failover needed? } domainp = domain; usernamep = username; *domainp = '\0'; *usernamep = '\0'; i = 0; // Small state machine to extract username@domain from "mail from" // command. #define PRE 1 #define GRAB_USERNAME 2 #define GRAB_DOMAIN 3 #define DONE 4 state = PRE; for (;;) { c = get (from_client, ReadTimeout); if (c == '\r') continue; if (c == EOF) { put (to_client, "451 4.3.0 Requested action aborted: error in processing - unexpected end of file\n"); debug_exit (9); } if (c == '\n') break; if ((state == GRAB_USERNAME) || (state == GRAB_DOMAIN)) { // canonicalise case in username and domain if (isalpha (c) && isupper (c)) c = tolower (c); } if ((state == PRE) && (c == '<')) { state = GRAB_USERNAME; i = 0; } else if ((state == GRAB_USERNAME) && (c == '>')) { state = DONE; strcpy (domain, ""); // DEFAULT_DOMAIN domainp = domain + strlen (domain); // [TO DO!] #define for local default domain above } else if ((state == GRAB_DOMAIN) && (c == '>')) { state = DONE; } else if ((state == GRAB_USERNAME) && (c == '@')) { state = GRAB_DOMAIN; i = 0; } else if ((state == GRAB_USERNAME) && (c != ' ')) { // truncate if username too long (might be x500 address :-( // // ) if (i < (MAX_STRING - 1)) { *usernamep++ = c; i += 1; } } else if ((state == GRAB_DOMAIN) && (c != ' ')) { // truncate if domain too long - it's probably a hack // attempt if (i < (MAX_STRING - 1)) { *domainp++ = c; i += 1; } } } #undef PRE #undef GRAB_USERNAME #undef GRAB_DOMAIN #undef DONE *domainp = '\0'; *usernamep = '\0'; // SAVE USERNAME@DOMAIN (or just USERNAME) to control file. if (*helo_host != '\0') { fprintf(controlfile, "helo %s\n", helo_host); } else if (*connecting_host != '\0') { fprintf(controlfile, "helo %s\n", connecting_host); } fprintf(controlfile, "mail from:<%s@%s>\n", username, domain); put (to_client, "250 2.1.0 <%s@%s>... Sender ok\n", username, domain); } } else if (strncmp (comm, "rset", 4) == 0) { DRAIN(); *domain = '\0'; *username = '\0'; *helo_host = '\0'; accept = FALSE; if (controlfile != NULL) fclose(controlfile); controlfile = NULL; if (spoolfile != NULL) fclose(spoolfile); spoolfile = NULL; put (to_client, "250 2.0.0 Reset state\n"); } else if (strncmp (comm, "noop", 4) == 0) { DRAIN(); put (to_client, "250 2.0.0 OK\n"); } else if (strncmp (comm, "help", 4) == 0) { DRAIN(); put (to_client, "214-2.0.0 This is backupmx/store version "); put (to_client, VERSION); put (to_client, "\n"); put (to_client, "214-2.0.0 Commands available:\n"); put (to_client, "214-2.0.0 HELO MAIL RCPT DATA\n"); put (to_client, "214-2.0.0 RSET NOOP QUIT HELP\n"); put (to_client, "214 2.0.0 End of HELP info\n"); } else if (strncmp (comm, "helo", 4) == 0) { int i; char *s = helo_host; if (*helo_host != '\0') { DRAIN(); put (to_client, "220 2.5.0 HELO/EHLO command already issued.\n"); } else { *helo_host = '\0'; for (;;) { c = get (from_client, ReadTimeout); if (c == EOF) debug_exit (10); if ((c == '\r') || (c == ' ') || (c == '\t') || c == '\0') continue; if (c == '\n') break; if ((s-helo_host) + 1 < MAX_STRING) { *s++ = c; *s = '\0'; } } put (to_client, "250 localhost Hello %s[%s], pleased to meet you\n", helo_host, connecting_host); } } else if (strncmp (comm, "quit", 4) == 0) { DRAIN(); put (to_client, "221 2.0.0 localhost closing connection\n"); debug_exit (0); } else { DRAIN(); put (to_client, "500 5.5.1 Command unrecognized: \""); write (to_client, comm, i); // i still has length (4) if (logfile != NULL) {write (fileno(logfile), comm, i); fflush(logfile);} put (to_client, "\"\n"); } } (void) alarm ((unsigned) 0); // Cancel the alarm call debug_exit (0); return (1); // Shouldn't get here }