/*

   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
}