// 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);
}