/*
 * $Id: smtpsend,v 1.35 2002/12/07 22:22:02 joseph Exp $
 *
 */

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include "memleak.h"
#include <string.h>
#include <time.h>

#include "sys/errno.h"

#include "swis.h"

#include "wimplib.h"


#include "defines.h"


#include "base64.h"
#include "bufsock.h"
#include "config.h"
#include "err.h"
#include "errlist.h"
#include "ftrunc.h"
#include "log.h"
#include "msgs.h"
#include "smtpsend.h"
#include "status.h"
#include "stopquit.h"
#include "time822.h"
#include "treport.h"
#include "xconnect.h"
#include "leakcheck.h"
#include "ctype.h"

#ifdef CRAM_MD5
# include "md5.h"
#endif


#define smtp_BUFSIZE 4096

typedef struct
{
  bufsock          bsock;
  xsock            s;
  status_handle    status;
  stopquit_t       stopped;
  char            *server;
  bool             support_auth_plain;
  bool             support_auth_login;
  bool             support_auth_cram_md5;
}
smtp_info;

static smtp_info smtp_statinfo;

typedef struct
{
  unsigned int load_addr;
  unsigned int exec_addr;
  int          size;
  int          attributes;
  int          objtype;
  char         name[MAX_LEAF_LEN];
}
osgbpb_dir_info;

typedef enum
{
  smtp_Success,
  smtp_Abort,	/* Must stop send altogether */
  smtp_Fail,	/* Failed on this message, left in queue but OK to send more */
  smtp_Moved	/* As fail but message has been moved to badmarker or bounced */
}
smtp_result;

static const char text_dir[]     = TASK_MQUEUE_TEXT;
static const char work_dir[]     = TASK_MQUEUE_WORK;
static const char bad_text_dir[] = TASK_BADMARK_TEXT;
static const char bad_work_dir[] = TASK_BADMARK_WORK;


static void smtp_stop_handler(stopquit_t action, void *han)
{
  smtp_info *info = (smtp_info *) han;
  info->stopped = action;
  xsyslog_logmessage(log_NAME, msgs_lookup("Stopped"), log_MiscInfo);
}


/*
 * Enumerate(?) contents of a directory. To start, pass index as 0, then
 * pass value returned from last call as index in next call.
 * If a file is found, index!=-1. If index==-1, all files have been found
 *
 * Don't do anything stupid like calling this across wimp polls :)
 *
 */

static int enum_dir(const char *dirname, char *buf, int bufsize, int index, int *readout)
{
  int read, nextin;

  do
  {
    if ( _swix(OS_GBPB, _INR(0,6) | _OUTR(3,4), 9,
         dirname, buf, 1, index, bufsize, 0, &read, &nextin) )
    {
      *readout = 0;
      return -1;
    }
  }
  while (read==0 && nextin !=-1);

  *readout = read;
  return nextin;
}


static bool check_work()
{
  char buffer[MAX_LEAF_LEN];
  int read;
  
  enum_dir(work_dir, buffer, sizeof(buffer), 0, &read);
  if (read > 0)
    return true;
  
  return false;
}

static int my_strncasecmp(const char *s, const char *t, int n)
{
  int a,b;

  if (n==0)
    return 0;

  for ( ; (a=tolower(*s)) == (b=tolower(*t)); s++,t++ )
    if ( (*s==0) || (--n==0) )
      return 0;

  return a - b;
}

static int count_text(int *size)
{
  osgbpb_dir_info buffer;
  int index;
  int count;

  if (size)
    *size = 0;
  
  for (count = 0, index = 0; index != -1; )
  {
    int read;

    if (E(_swix(OS_GBPB, _INR(0,6)|_OUTR(3,4), 10,
    	text_dir, &buffer, 1, index, sizeof(buffer), "*", &read, &index)))
      break;
    if (read)
    {
      ++count;
      if (size)
        *size += buffer.size;
    }
  }
  return count;
}


static int smtp_findnext( const char *dir, char *last, int lastsize )
{
  char tmp[MAX_LEAF_LEN];
  char found[ sizeof tmp ];
  int index = 0;
  int read;

  /* loop through all files in dir, finding a file greater than the file we've just fetched
   * and less than the ones we've found so far. */

  *found = 0;

  do
  {
    index = enum_dir( dir, tmp, sizeof tmp, index, &read );
    if (read != 0)
    {
      if ( strcmp( last, tmp ) < 0 && ( strcmp( found, tmp ) > 0 || *found == 0 ) )
      {
        strcpy( found, tmp );
      }
    }
  }
  while (index != -1);

  if ( *found )
  {
    *last = 0;
    strncat( last, found, lastsize - 1 );
    return 0;
  }

  return -1;
}


static void do_delete (const char *pathname)
{
  char msg[MAX_PATH_LEN];
  _kernel_oserror *e = _swix(OS_File, _INR(0,1), 6, pathname);
  if (e)
  {
    snprintf (msg, sizeof (msg), "Can't delete %s: %s", pathname, e->errmess);
    treport (msg, log_OSError);
  }
}


static void delete_queued_message(const char *leaf)
{
  char pathname[MAX_PATH_LEN];

  snprintf (pathname, sizeof (pathname), "%s.%s", work_dir, leaf);
  do_delete (pathname);

  snprintf (pathname, sizeof (pathname), "%s.%s", text_dir, leaf);
  do_delete (pathname);
}


static smtp_result cant_process(const char *leaf, FILE *wfp, FILE *tfp,
	const char *tag)
{
  _kernel_oserror *e;
  char from[MAX_PATH_LEN], to[MAX_PATH_LEN];
  int result;

  if (wfp)
    fclose(wfp);
  if (tfp)
    fclose(tfp);
  treport(msgs_lookup1(tag, leaf), log_OSError);
  snprintf(from, sizeof (from), "%s.%s", work_dir, leaf);
  snprintf(to, sizeof (to), "%s.%s", bad_work_dir, leaf);
  
  result = 0;
  
  if (!file_exists(from))
  {
    xsyslogf(log_NAME,log_OSError,"File %s doesn't exist",from);
    result = 1;
  }
  
  if ( (e = file_rename(from, to)) == NULL)
  {
    result = 1;
  }
  else
  {
    xsyslogf(log_NAME,log_OSError,"Rename from %s to %s failed [%s]",from, to, e->errmess);
  }
  
  if (result)
  {
    /* moved work file ok, now move text file */
    snprintf(from, sizeof (from), "%s.%s", text_dir, leaf);
    snprintf(to, sizeof (to), "%s.%s", bad_text_dir, leaf);
    result = 0;
    if (file_exists(from))
    {
      result = 1;
    }
    else
    {
      xsyslogf(log_NAME,log_OSError,"File %s doesn't exist",from);
    }    
    if (result && (e = file_rename(from, to)) != NULL)
    {
      xsyslogf(log_NAME,log_OSError,"Rename from %s to %s failed [%s]",from, to, e->errmess);
      snprintf(from, sizeof (from), "%s.%s", bad_work_dir, leaf);
      snprintf(to, sizeof (to), "%s.%s", work_dir, leaf);
      if ((e = file_rename(from, to)) != NULL)
      {
        xsyslogf(log_NAME,log_OSError,"Rename from %s to %s failed [%s]",from, to, e->errmess);
      }
    }
    else
    {
      wimp_start_task("Filer_OpenDir "TASK_BADMARK, 0);
      return smtp_Moved;
    }
  }
  treport(msgs_lookup1("BadMark", leaf), log_OSError);
  return smtp_Fail;
}

/* Returns reply code, or -1 if the read failed.
 * @param b Pointer for last line of response to be returned in. Valid until
 *          next socket read operation. */
static int smtp_get_reply(smtp_info *sinfo, char const **b)
{
  bool wasline;
  bool multi = false;
  int result = 0;
  const char *line;

  do {
    if ((line = bufsock_read_line(sinfo->bsock, &wasline, NULL)) != 0)
    {
      multi = (line[3] == '-');
      result = atoi(line);
    }
    else
      return -1;
    xsyslog_logmessage(log_NAME, line, log_ServerResponse);
  } while (multi);
  if (b)
    *b = line;
  
  return result;
}

static bool smtp_wait_quitrep(smtp_info *sinfo)
{
  const char *buffer;
  int code = smtp_get_reply(sinfo, &buffer);

  if (code == -1)
  {
    char str[32];
    sprintf(str, "%d", errno);
    xsyslog_logmessage(log_NAME,
      msgs_lookup3("NoRep", "QUIT", errlist_str(errno), str),
      log_SocketError);
    return false;
  }
  else if (code / 100 == 2)
  {
    xsyslog_logmessage(log_NAME, msgs_lookup("GoodQuit"), log_MiscInfo);
    return true;
  }
  else
  {
    xsyslogf(log_NAME, log_ServerError, "%s : %s",
    	msgs_lookup1("BadRep", "QUIT"), buffer);
  }
  return false;
}

/* Sends RSET to server */
static void rset(smtp_info *sinfo)
{
  const char *b;

  if (!xsock_xwrite(sinfo->s, "RSET"))
    return;

  if (smtp_get_reply(sinfo, &b) / 100 != 2)
  {
    treport(msgs_lookup1("BadRep", "RSET"), log_ServerError);
    xsyslog_logmessage(log_NAME, b, log_ServerError);
  }
}

static void smtp_show_status(status_handle status, int bytes, bool force)
{
  static int lt;
  int tn;

  _swix(OS_ReadMonotonicTime, _OUT(0), &tn);
  if (tn - lt > 25 || force)
  {
    status_bytes(status, bytes);
    lt = tn;
  }
}

static void spool_across(FILE *from, FILE *to, char *buffer, int buflen)
{
  while (!feof(from))
  {
    if (fgets(buffer, buflen, from))
      fputs(buffer, to);
  }
}

static bool bounce_to(const char *btaddr, FILE *btfp, FILE *txfp,
	char *buffer, int buflen, const char *server, const char *to, const char *servmsg)
{
  time_t t;
  int line;
  const char *domain;
  long int start,end;

  t = time(0);
  start = ftell( btfp );

  fprintf(btfp, HASHBANG_RMAIL_N "From: %s\n", 0, msgs_lookup("MDaemon"));
  fprintf(btfp, "To: %s\n", btaddr);
  fprintf(btfp, "Subject: %s\n", msgs_lookup("BounceSubj"));
  fputs("Date: ", btfp);
  time822_fprint(btfp, t);
  domain = getenv("Inet$LocalDomain");
  strcpy(buffer, domain ? domain : "localhost");
  fprintf(btfp, "\nMessage-Id: <%08x.bounce@%s.%s>\n\n",
  	(int) t, getenv("Inet$HostName"), buffer);
  for (line = 0; ;)
  {
    _swix(MessageTrans_EnumerateTokens, _INR(0,4)|_OUT(2)|_OUT(4),
    	&msgs_descriptor, "BounceLine??", buffer, buflen, line, &domain, &line);
    if (!domain)
      break;
    fprintf(btfp, "%s\n", msgs_lookup(buffer));
  }
  fprintf(btfp, "\nServer: %s\nAttempting to send message to address: %s\nError : %s\n\n", server, to, servmsg);
  for (line = 0; ;)
  {
    _swix(MessageTrans_EnumerateTokens, _INR(0,4)|_OUT(2)|_OUT(4),
    	&msgs_descriptor, "CopyLine??", buffer, buflen, line, &domain, &line);
    if (!domain)
      break;
    fprintf(btfp, "%s\n", msgs_lookup(buffer));
  }
  putc('\n', btfp);
  spool_across(txfp, btfp, buffer, buflen);

  end = ftell( btfp );
  fseek( btfp, start, SEEK_SET );
  fprintf( btfp, HASHBANG_RMAIL_N, end - start - LENGTH_HASHBANG_RMAIL_N );

  return (!ferror(btfp) && !ferror(txfp));
}


/* this is used to open a users /incoming/ mail file, so we can write
 * bounce messages to it.
 */
static FILE *smtp_openmailtext(char *username)
{
  FILE *btfp;
  char buffer[MAX_PATH_LEN];
  
  snprintf(buffer, sizeof (buffer), TASK_MAIL_TEXT".%s", username);
  if ((btfp = fopen(buffer, "r+")) == 0)
  {
    /* file doesn't exist? Try creating it... */
    btfp = fopen(buffer, "w");
    if ( btfp )
      fclose( btfp );
    btfp = fopen(buffer, "r+");
  }
  if (!btfp)
  {
    xsyslogf(log_NAME, log_OSError, "Failed write to file %s in smtp_openmailtext", buffer);
    return NULL;
  }
  
  if (fseek(btfp, 0, SEEK_END))
  {
    xsyslogf(log_NAME, log_OSError, "Failed seek to end of file %s in smtp_openmailtext", buffer);
    fclose(btfp);
    return NULL;
  }
  
  return btfp;
}

static smtp_result local_bounce(const char *leaf, const char *to, const char *server, const char *servmsg, int delete)
{
  char btaddr[MAX_ADDR_LEN];
  char *buffer;
  char *at;
  bool bounced;
  long int pos;
  FILE *btfp = 0, *wkfp = 0, *txfp = 0;

  buffer = malloc(smtp_BUFSIZE);
  if (!buffer)
  {
    xsyslogf(log_NAME, log_OSError, "Memory allocation failed in local_bounce");
    return cant_process(leaf, 0, 0, "NoBounce");
  }

  /* Find who tried to send the mail */
  snprintf(buffer, smtp_BUFSIZE, "%s.%s", work_dir, leaf);
  if ((wkfp = fopen(buffer, "r")) == 0)
  {
    xsyslogf(log_NAME, log_OSError, "Failed to open work file %s in local_bounce", buffer);
    free(buffer);
    return cant_process(leaf, 0, 0, "NoBounce");
  }
  fgets(buffer, smtp_BUFSIZE, wkfp);
  fscanf(wkfp, ADDR_SCANF "\n", btaddr);
  if (ferror(wkfp))
  {
    xsyslogf(log_NAME, log_OSError, "Failed - ferror set in local_bounce", buffer);
    free(buffer);
    fclose(wkfp);
    return cant_process(leaf, wkfp, 0, "NoBounce");
  }
  at = strchr(btaddr, '@');
  if (at)
    *at = 0;
  fclose(wkfp);

  snprintf(buffer, smtp_BUFSIZE, "%s.%s", text_dir, leaf);
  if ((txfp = fopen(buffer, "r")) == 0)
  {
    xsyslogf(log_NAME, log_OSError, "Failed to open text file %s in local_bounce", buffer);
    free(buffer);
    return cant_process(leaf, 0, 0, "NoBounce");
  }

  btfp = smtp_openmailtext(btaddr);
  if (!btfp)
  {
    xsyslogf(log_NAME, log_OSError, "Failed to open mailbox %s for bounce message, trying postmaster", btaddr);
    btfp = smtp_openmailtext("postmaster");
  }
    
  if (!btfp)
  {
    xsyslogf(log_NAME, log_OSError, "Failed to find a mailbox to bounce to in local_bounce");;
    if (btfp)
      fclose(btfp);
    free(buffer);
    return cant_process(leaf, 0, txfp, "NoBounce");
  }
  if (at)
    *at = '@';

  pos = ftell(btfp);
  bounced = bounce_to(btaddr, btfp, txfp, buffer, smtp_BUFSIZE, server, to, servmsg);

  fclose(btfp);
  fclose(txfp);

  if (bounced)
  {
    free(buffer);
    if ( delete )
    {
      delete_queued_message (leaf);
    }
    return smtp_Moved;
  }
  snprintf(buffer, smtp_BUFSIZE, TASK_MAIL_TEXT".%s", at);
  if (pos)
    file_truncate(buffer, pos);
  else
    remove(buffer);
  free(buffer);
  xsyslogf(log_NAME, log_OSError, "Failed - bounce_to failed in local_bounce [%p]");
  return cant_process(leaf, 0, 0, "NoBounce");
}


static smtp_result send_data( smtp_info *sinfo, const char *leaf, FILE *tfp, int size )
{
  char buffer[smtp_BUFSIZE];
  char *ptr;
  char *read;
  int space;
  bool wasline = true;
  int header = 1;
  int useragent = 0;
  int subject = 0;
  int keepspace = 2 + strlen(TASK_USERAGENT) + 1; /* user agent + ' ' */

  do
  {
    ptr = buffer;
    space = sizeof buffer;

    /* read from file - leaving two spare characters - one for \r, and another for a potential extra '.' */
    /* (fgets() saves one for the NULL terminator itself */
    while ( space > 256 && (read = fgets( ptr, space - keepspace, tfp), read) )
    {
      int len = strlen( ptr );

      smtp_show_status( sinfo->status, size += len, false );

      if ( wasline && *ptr == '.' )
      {
        /* need to add a . - move buffer forward */
        memmove( ptr + 1, ptr, len + 1 );
        *ptr = '.';
        len += 1;
      }

      if (wasline && header)
      {
        if (!my_strncasecmp( ptr, "User-Agent:", 11 ) && !useragent)
        {
          if (strstr(ptr, TASK_USERAGENT) == NULL)
          {
            /* only add if not already present */
            strcpy( ptr + len - 1, " " TASK_USERAGENT "\n" );
            useragent = 1;
            len = strlen( ptr );
          }
        }
        else if (!my_strncasecmp( ptr, "Subject:", 8 ) && !subject)
        {
          char x = 0, *p = ptr + 8;
          while (isspace(*p))
            p++;
          if (strlen(p) > 60)
          {
            x=p[60];
            p[60] = 0;
          }
          xsyslogf(log_NAME, log_MiscInfo, "Message subject: %s", p);
          subject = 1;
          if (x)
          {
            p[60] = x;
          }
        }
        else if (*ptr == '\n')
        {
          header = 0; /* end of header */
          if (!useragent)
          {
            /* Useragent header not already present - add one */
            strcpy( ptr + len - 1, "User-Agent: " TASK_USERAGENT "\r\n\n" );
            len = strlen( ptr );
          }
          if (!subject)
          {
            /* Didn't find a subject header - log something */
            xsyslogf(log_NAME, log_MiscInfo, "Message subject: [no subject]");
          }
        }
      }

      if ( ptr[len - 1] == '\n' )
      {
        wasline = true;
        ptr[len - 1] = '\r';
        ptr[len] = '\n';
        ptr[len + 1] = 0;
        len++;
      }
      else
        wasline = false;

      ptr += len;
      space -= len;
    }

    if ( ptr != buffer && !xsock_ywritelen(sinfo->s, buffer, sizeof(buffer) - space) )
    {
      fclose(tfp);
      return smtp_Abort;
    }
  }
  while ( read );

  smtp_show_status(sinfo->status, size, true);

  if (ferror(tfp))
  {
    rset(sinfo);
    return cant_process(leaf, 0, tfp, "NoText");
  }
  fclose(tfp);

  if (!xsock_xwrite(sinfo->s, wasline ? "." : "\r\n."))
    return smtp_Abort;

  return smtp_Success;
}


static void chomp (char *buf)
{
  int l = strlen (buf);
  if (l && buf[l - 1] == '\n')
    buf[l - 1] = 0;
}


static smtp_result send_envelope( smtp_info *sinfo, const char *leaf, int *size, FILE **text )
{
  char buffer[smtp_BUFSIZE];
  int objtype;
  FILE *tfp = NULL, *wfp = NULL;
  char *ptr;
  int res;
  const char *b;
  int rcpt_to;

  snprintf(buffer, sizeof (buffer), "%s.%s", text_dir, leaf);
  if (E(_swix(OS_File, _INR(0,1)|_OUT(0)|_OUT(4), 5, buffer, &objtype, size))
  	|| objtype != 1)
    return cant_process(leaf, 0, 0, "NoText");

  smtp_show_status(sinfo->status, 0, true);
  status_bytes_total(sinfo->status, *size);

  *size = 0;

  snprintf(buffer, sizeof (buffer), "%s.%s", work_dir, leaf);
  if ((wfp = fopen(buffer, "r")) == 0)
    return cant_process(leaf, 0, 0, "NoWork");

  snprintf(buffer, sizeof (buffer), "%s.%s", text_dir, leaf);
  if ((tfp = fopen(buffer, "r")) == 0)
  {
    rset(sinfo);
    return cant_process(leaf, wfp, tfp, "NoText2");
  }

  if (!fgets(buffer, smtp_BUFSIZE, wfp))
    return cant_process(leaf, wfp, tfp, "NoWork2");
/*xsyslogf(log_NAME, log_DebugInfo, "Gateway is \'%s\'", buffer);*/

  if (!fgets(buffer, smtp_BUFSIZE, wfp))
    return cant_process(leaf, wfp, tfp, "NoWork3");

  buffer[MAX_ADDR_LEN - 1] = 0; /* truncate to max allowed length */
  chomp (buffer); /* remove trailing LF */
  xsyslogf(log_NAME, log_MiscInfo, "Sending message %s:", leaf);
  xsyslogf(log_NAME, log_MiscInfo, "From:            %s", buffer);
  sprintf(buffer + MAX_ADDR_LEN, "%s ->", buffer);
  status_show_misc(sinfo->status, buffer + MAX_ADDR_LEN);

  if ( ptr = getenv( TASK_LOGNAME"$EnvelopeFrom" ), ptr )
    snprintf(buffer + MAX_ADDR_LEN, sizeof (buffer) - MAX_ADDR_LEN,
	     "MAIL FROM:<%s>", ptr);
  else
    snprintf(buffer + MAX_ADDR_LEN, sizeof (buffer) - MAX_ADDR_LEN,
	     "MAIL FROM:<%s>", buffer);

  if (!xsock_xwrite(sinfo->s, buffer + MAX_ADDR_LEN))
  {
    fclose(wfp);
    fclose(tfp);
    return smtp_Abort;
  }

  res = smtp_get_reply(sinfo, &b);
  if (res < 0 || res / 100 >= 4)
  {
    fclose(wfp);
    fclose(tfp);
    treport(msgs_lookup1("BadRep", "MAIL"),
    	res < 0 ? log_SocketError : log_ServerError);
    if (res >= 0)
    {
      xsyslog_logmessage(log_NAME, b, log_ServerError);
      if (res / 100 == 5)
        return local_bounce(leaf, "(at MAIL FROM: stage)", sinfo->server, b, 1);
      else
        return smtp_Fail;
    }
    else
      return smtp_Abort;
  }

  if ( !fgets(buffer, smtp_BUFSIZE, wfp) )
  {
    smtp_result res;
    treport(msgs_lookup1("BadRcpt", leaf), log_ServerError);
    fclose(tfp);
    fclose(wfp);
    res = local_bounce(leaf, sinfo->server, "<no recipients>", "<no recipients>", 1);
    rset(sinfo);
    return res;
  }

  rcpt_to = 0;

  do
  {
    chomp (buffer); /* remove trailing LF */
    sprintf(buffer + MAX_ADDR_LEN, "-> %s", buffer);
    status_show_misc(sinfo->status, buffer + MAX_ADDR_LEN);
    sprintf(buffer + MAX_ADDR_LEN, "RCPT TO:<%s>", buffer);

    if (rcpt_to < 5)
    {
      xsyslogf(log_NAME, log_MiscInfo, "Recipient:       %s", buffer);
    }
    else if (rcpt_to == 5)
    {
      xsyslogf(log_NAME, log_MiscInfo, "Recipient:       (Multiple recipients, further logging suppressed)", buffer);
      rcpt_to++; /* to avoid logging twice if this RCPT TO fails */
    }

    if (!xsock_xwrite(sinfo->s, buffer + MAX_ADDR_LEN))
    {
      fclose(wfp);
      fclose(tfp);
      return smtp_Abort;
    }
    res = smtp_get_reply(sinfo, &b);
    if (res / 100 == 4)
    {
      /* We don't want to bounce or otherwise dequeue mail if there is a
       * temporary server problem.
       */
      fclose(wfp);
      fclose(tfp);
      treport(msgs_lookup("TempFail"), log_ServerError);
      rset(sinfo);
      return smtp_Fail;
    }
    if (res / 100 >= 5)
    {
      char *errmsg;

      xsyslogf(log_NAME, log_ServerError, "%s : %s", msgs_lookup1("BadRep", "RCPT"), b);

      errmsg = malloc(strlen(b) + 1);
      if (errmsg)
        strcpy(errmsg, b);

      /* unfortunately if local_bounce fails here and calls cant_process, cant_process
       * which fail as the work file is still open (and besides we don't want to
       * get rid of the workfile). Hopefully local_bounce isn't going to fail much,
       * though. It might be nice to close the workfile after reading the last line,
       * so it at least works if the email was only destined for one person.
       */
      res = local_bounce(leaf, buffer, sinfo->server, errmsg ? errmsg : "?", 0);
      free(errmsg);
      if ( res != smtp_Moved )
      {
        /* bounce failed */
        fclose( wfp );
        fclose( tfp );
        return smtp_Abort;
      }
      /* sent user bounce message, process other rcpt's */
    }
    else if (res < 0)
    {
      rset(sinfo);
      fclose(wfp);
      fclose(tfp);
      return smtp_Abort;
    }
    else
      rcpt_to++;
  }
  while ( fgets(buffer, smtp_BUFSIZE, wfp) );

  if (ferror(wfp))
  {
    rset(sinfo);
    return cant_process(leaf, wfp, tfp, "NoWork");
  }

  fclose(wfp);

  if (rcpt_to == 0)
  {
    char *errmsg;
    /* no valid recipients - errors already sent to user, just drop message */
    errmsg = malloc(strlen(b) + 1);
    if (errmsg)
      strcpy(errmsg, b);
    treport(msgs_lookup1("BadRcpt", leaf), log_ServerError);
    fclose(tfp);
    delete_queued_message (leaf);
    free( errmsg );
    rset( sinfo );
    return smtp_Fail;
  }

  *text = tfp;

  return smtp_Success;
}


static smtp_result process_leaf(smtp_info *sinfo, const char *leaf)
{
  FILE *tfp = 0;
  int size;
  int res;
  smtp_result result;
  const char *b;

  result = send_envelope( sinfo, leaf, &size, &tfp );
  if ( result != smtp_Success )
    return result;

  if (!xsock_xwrite(sinfo->s, "DATA"))
  {
    fclose(tfp);
    return smtp_Abort;
  }
  res = smtp_get_reply(sinfo, &b);
  if (res / 100 >= 4 || res < 0)
  {
    fclose(tfp);
    treport(msgs_lookup1("BadRep", "DATA"),
    	res < 0 ? log_SocketError : log_ServerError);
    if (res >= 0)
    {
      xsyslog_logmessage(log_NAME, b, log_ServerError);
      rset(sinfo);
      return smtp_Fail;
    }
    else
      return smtp_Abort;
  }

  result = send_data( sinfo, leaf, tfp, size );

  if ( result != smtp_Success )
    return result;

  res = smtp_get_reply(sinfo, &b);
  if (res / 100 >= 4 || res < 0)
  {
    treport(msgs_lookup1("BadBody", leaf),
    	res < 0 ? log_SocketError : log_ServerError);
    if (res >= 0)
      xsyslog_logmessage(log_NAME, b, log_ServerError);
    return res < 0 ? smtp_Abort : smtp_Fail;
  }

  delete_queued_message (leaf);

  return smtp_Success;
}


static void smtp_stop(smtp_info *sinfo, bool abrt, bool dstrystatus)
{
  if (sinfo->bsock)
  {
    if (xsock_xwrite(sinfo->s, "QUIT") && !abrt)
      smtp_wait_quitrep(sinfo);
    bufsock_free(sinfo->bsock);
    sinfo->bsock = 0;
  }
  if (sinfo->s != -1)
  {
    xsock_close(sinfo->s);
    sinfo->s = -1;
  }
  if (sinfo->status && dstrystatus)
  {
    status_free(sinfo->status);
    sinfo->status = 0;
  }
  /*free(sinfo);*/
}


#ifdef FEATURE_WORKFILE_GATEWAY
/* Read the gateway for a given email - which means reading the
 * first line of the work file. */
static int read_gateway(const char *leaf, char *server, int serversize)
{
  char buffer[smtp_BUFSIZE];
  FILE *wfp;
  
  snprintf(buffer, smtp_BUFSIZE, "%s.%s", work_dir, leaf);
  if ((wfp = fopen(buffer, "r")) == 0)
    return false;
  
  if (!fgets(server, serversize, wfp) || server[0]=='\0')
  {
    fclose(wfp);
    return false;
  }
  
  /* remove trailing \n */
  server[strlen(server)-1] = 0;
  fclose(wfp);

  return true;
}
#endif


#ifndef FEATURE_WORKFILE_GATEWAY
/* like config_lookup, but allows the entry in the choices file to be a 
 * system variable that is interpretted. Because of this, the caller must
 * free the result when finished with it.
 */
static char *smtp_getconfig(const char *token)
{
  int len;
  const char *end;
  char *server;
  const char *serv1;
  
  assert(token != NULL);
  
  serv1 = config_lookup(token);
  if ( serv1 == NULL )
  {
    return NULL;
  }
  while (isspace(*serv1))
    serv1++;
  end = serv1 + strlen(serv1);
  while (--end > serv1 && isspace(*end)) /**/;
  len = end - serv1 + 1;

  if ( serv1[0] == '<' && *end == '>' )
  {
    char serv[256];
    len -= 2;
    if (len >= sizeof (serv))
      len = sizeof (serv) - 1; /* hmm, long variable name? */
    /* copy, getting run of < and > */
    memcpy(serv, serv1 + 1, len);
    serv[len] = 0;
    serv1 = getenv(serv); /* skip < */
    if ( !serv1 )
      return NULL;
    len = strlen(serv1);
  }

  if (!len)
    return NULL;

  server = malloc(len + 1);

  if (!server)
  {
    E(msgs_nomem());
    return NULL;
  }

  memcpy(server, serv1, len);
  server[len] = 0;

  return server;
}
#endif



static void smtp_processehloline(smtp_info *sinfo, const char *line)
{
  const char *keyword;
  const char *keywordend;
  
  assert(sinfo != NULL);
  assert(line  != NULL);
  
  keyword = line + 4;
  while (isspace(*keyword))
    keyword++;
  keywordend = keyword;
  while (*keywordend != 0 && !isspace(*keywordend))
    keywordend++;
  xsyslogf(log_NAME, log_ServerResponse, "esmtp keyword: %.*s",
           keywordend - keyword, keyword);
  if (keywordend - keyword == 4 &&
      my_strncasecmp(keyword, "AUTH", 4) == 0)
  {
    const char *start;
    const char *ptr = keywordend;
    
    while (*ptr != 0)
    {
      while (isspace(*ptr))
        ptr++;
      start = ptr;
      while (*ptr != 0 && !isspace(*ptr))
        ptr++;
      xsyslogf(log_NAME, log_ServerResponse, "auth keyword: %.*s",
               ptr - start, start);
      if (ptr - start == 5 &&
          my_strncasecmp(start, "LOGIN", ptr - start) == 0)
      {
        sinfo->support_auth_login = true;
      }
      else if (ptr - start == 5 &&
          my_strncasecmp(start, "PLAIN", ptr - start) == 0)
      {
        sinfo->support_auth_plain = true;
      }
      else if (ptr - start == 8 &&
          my_strncasecmp(start, "CRAM-MD5", ptr - start) == 0)
      {
        sinfo->support_auth_cram_md5 = true;
      }
    }
  }
  /* FIXME: add 8bit clean support! */
}



static bool smtp_authlogin(smtp_info *sinfo,
                           const char *auth_username,
                           const char *auth_password)
{
  char auth_usernameb64[320];
  char auth_passwordb64[320];
  int code;
  const char *buffer;
  
  /* base64-encode the username */
  if (b64_ntop((const unsigned char *) auth_username, strlen(auth_username),
               auth_usernameb64, sizeof(auth_usernameb64)) == -1)
  {
    treport(msgs_lookup1("TooLong", "Username"), log_ServerError);
    return false;
  }

  /* base64-encode the password */
  if (b64_ntop((const unsigned char *) auth_password, strlen(auth_password),
               auth_passwordb64, sizeof(auth_passwordb64)) == -1)
  {
    treport(msgs_lookup1("TooLong", "Password"), log_ServerError);
    return false;
  }
  
  /* Now talk to the server */
  if (!xsock_xwrite(sinfo->s, "AUTH LOGIN"))
    return false;
  code = smtp_get_reply(sinfo, &buffer);

  if (code != 334)
  {
    treport(msgs_lookup("NoAuthSupport"), log_ServerError);
    return false;
  }

  if (!xsock_xwrite(sinfo->s, auth_usernameb64))
    return false;
  code = smtp_get_reply(sinfo, &buffer);

  if ((code / 100) == 5)
  {
    treport(msgs_lookup("InvalidAuthData"), log_ServerError);
    return false;
  }

  if (code != 334)
  {
    treport(msgs_lookup("NoSMTPAuthReply"), log_ServerError);
    return false;
  }

  if (!xsock_xwrite(sinfo->s, auth_passwordb64))
    return false;
  code = smtp_get_reply(sinfo, &buffer);

  if (code != 235)
  {
    treport(msgs_lookup("SMTPAuthFail"), log_ServerError);
    return false;
  }
  
  return true;
}



static bool smtp_authplain(smtp_info *sinfo,
                           const char *username,
                           const char *password)
{
  int         code;
  char        cmd[1024];
  char       *cmdptr;
  char        token[512];
  char       *tokenptr;
  const char *buffer;
  
  assert(username != NULL);
  assert(password != NULL);
  /* build up 'AUTH PLAIN \0user\0PASS' */

  strcpy(cmd, "AUTH PLAIN ");
  cmdptr = cmd + strlen("AUTH PLAIN ");
  
  tokenptr = token;
  /* add first empty string */
  *tokenptr++ = '\0';
  /* append, ignoring already used space, and leaving room for our NUL
   * and another NUL */
  tokenptr[0] = '\0';
  strncat(tokenptr, username, sizeof(token) - (tokenptr-token) - 2);
  tokenptr += strlen(tokenptr) + 1;
  
  tokenptr[0] = '\0';
  strncat(tokenptr, password, sizeof(token) - (tokenptr-token) - 1);
  tokenptr += strlen(tokenptr);

  if (tokenptr == token + sizeof(token) || 
      b64_ntop((const unsigned char *) token, tokenptr-token,
               cmdptr, sizeof(cmd) - (cmdptr-cmd)) == -1)
  {
    treport(msgs_lookup1("TooLong", "Username/password"), log_ServerError);
    return false;
  }
  
  if (!xsock_xwrite(sinfo->s, cmd))
    return false;
  
  code = smtp_get_reply(sinfo, &buffer);

  if (code != 235)
  {
    treport(msgs_lookup("SMTPAuthFail"), log_ServerError);
    xsyslogf(log_NAME, log_ServerError, "%s : %s",
            msgs_lookup1("BadRep", "AUTH PLAIN"), buffer);
    return false;
  }
  
  return true;
}



#ifdef CRAM_MD5
void
hmac_md5 (const char *text, int textlen, const char *key, int keylen,
	  char *digest)
{
  md5_ctx context;
  char ipad[65], opad[65], tk[16];
  signed int i;

  /* The HMAC_MD5 transform looks like:
   *   MD5 (K EOR opad, MD5 (K EOR ipad, text))
   * where K is the key, ipad is '6' x 64, opad is '\' x 64, and text is the
   * data to be protected
   */

  /* If key is longer than 64 bytes, use its MD5 hash instead. */
  if (keylen > 64)
  {
    md5_buffer (key, keylen, tk);
    key = tk;
    keylen = 16;
  }

  /* First, store the key in the pads */
  memset (ipad, sizeof (ipad));
  memcpy (ipad, key, keylen);

  /* EOR key with ipad and opad values */
  for (i = 63; i >= 0; i++)
  {
    opad[i] = (ipad[i] ^= 0x36) ^ (0x36 ^ 0x5c);
    opad[i] ^= 0x5c;
  }

  /* Perform inner MD5 */
  md5_init_ctx (&context);	/* init context for first pass */
  md5_process_bytes (ipad, 64, &context);	/* start with inner pad */
  md5_process_bytes (text, textlen, &context);	/* add text of datagram */
  md5_finish_ctx (&context, digest);	/* finish first pass */

  /* Perform outer MD5 */
  md5_init_ctx (&context);	/* init context for second pass */
  md5_process_bytes (opad, 64, &context);	/* start with outer pad */
  md5_process_bytes (digest, 16, &context);	/* add first hash */
  md5_finish_ctx (&context, digest);	/* finish second pass */

  /* Done. */
}


static bool smtp_authcrammd5(smtp_info *sinfo,
                             const char *username,
                             const char *password)
{
  if (!xsock_xwrite(sinfo->s, "AUTH CRAM-MD5"))
    return false;
  code = smtp_get_reply(sinfo, &buffer);
  
  if (code != 334)
  {
    treport(msgs_lookup("NoAuthSupport"), log_ServerError);
    return false;
  }

  // need to get text given by server as well as response code
  // let's say this text is in the variable 'response'
  char response[64];
  char digest[64];
  char wibble[64];
  // b64 decode response (into username array, sorry)
  b64_pton(response,auth_usernameb64, sizeof(auth_usernameb64));

  // calculate digest using this response, password and stick it in 'digest'
  hmac_md5(auth_usernameb64, sizeof(auth_usernameb64),
	   password, strlen (password), digest);

  // create reply to server, consisting of username, space, digest
  snprintf(wibble, sizeof (wibble), "%s %s", username, digest);

  // b64 encode this reply
  b64_ntop((unsigned char *)wibble, strlen(wibble), auth_usernameb64, \
      sizeof(auth_usernameb64));

  // send it
  if (!xsock_xwrite(sinfo->s, auth_usernameb64))
    return false;
  code = smtp_get_reply(sinfo, &buffer);

  if (code != 235)
  {
    treport(msgs_lookup("SMTPAuthFail"), log_ServerError);
    return false;
  }
}
#endif /* CRAM_MD5 */
      


/* returns smtp_Success if it worked, smtp_Abort is something broke,
 * and smtp_Fail if the server appears not to support EHLO */
static smtp_result smtp_startesmtp(smtp_info *sinfo, const char *hostname)
{
  int          code;
  char         helo[80];
  bool         wasline;
  bool         firstline = true;
  bool         multi = false;
  const char  *line;
  char        *username;
  char        *password;
  smtp_result  result = smtp_Success;

  sprintf(helo, "EHLO %.*s", sizeof(helo) - sizeof("EHLO ") - 1, hostname);
  if (!xsock_xwrite(sinfo->s, helo))
    return smtp_Abort;

  do
  {
    if ((line = bufsock_read_line(sinfo->bsock, &wasline, NULL)) != 0)
    {
      multi = (line[3] == '-');
      code = atoi(line);
    }
    else
    {
      char str[32];
      sprintf(str, "%d", errno);
      xsyslog_logmessage(log_NAME,
        msgs_lookup3("NoRep", "EHLO", errlist_str(errno), str),
        log_SocketError);
      return smtp_Abort;
    }
    xsyslog_logmessage(log_NAME, line, log_ServerResponse);
    if (code == 250)
    {
      if (firstline)
      {
        /* the first name just contains the server's name & greeting */
        firstline = false;
      }
      else if (strlen(line) > 4)
      {
        /* otherwise, it's a line listing an extension, let's see if it's
         * one we recognise - format is keyword followed by space & params */
        smtp_processehloline(sinfo, line);
      }
    }
  }
  while (multi);
  
  if (code != 250)
  {
    xsyslogf(log_NAME, log_ServerError, "%s : %s",
      msgs_lookup1("BadRep", "EHLO"), line);
    
    return smtp_Fail;
  }
  
  xsyslogf(log_NAME, log_DebugInfo,
           "login = %d, plain = %d, cram-md5 = %d",
           sinfo->support_auth_login, sinfo->support_auth_plain,
           sinfo->support_auth_cram_md5);
  
  username = smtp_getconfig("SMTP.username");
  password = smtp_getconfig("SMTP.password");
  if (username == NULL ||
      password == NULL)
  {
    /* no password info */
  }
  else
  {
    bool result = true;
#ifdef CRAM_MD5
    if (sinfo->support_auth_cram_md5)
    {
      if (!smtp_authcrammd5(sinfo, username, password))
        result = smtp_Fail;
    }
    else
#endif /* CRAM_MD5 */
    if (sinfo->support_auth_plain)
    {
      /* try plain authentication (rfc 2595) */
      if (!smtp_authplain(sinfo, username, password))
        result = smtp_Fail;
    }
    else if (sinfo->support_auth_login)
    {
      /* try login authentication (no RFC). */
      if (!smtp_authlogin(sinfo, username, password))
        result = smtp_Fail;
    }
  }
  
  free(username);
  free(password);
  
  return result;
}


static bool smtp_connect(smtp_info *sinfo, char *server)
{
  char helo[80];
  const char *hostname;
  const char *buffer;
  smtp_result ehloresult;
  int code;

  if ( !server )
    return false;

  if ((hostname = getenv("Inet$HostName")) == 0)
  {
    treport(msgs_lookup("NoHName"), log_OSError);
    return false;
  }

  /* connect and get greeting */
  if ((sinfo->s = xconnect_smtp(server, sinfo->status)) == -1)
  {
    treport(msgs_lookup1("SMTPConnect", server), log_SocketError);
    return false;
  }
  if ((sinfo->bsock = bufsock_create(sinfo->s)) == 0)
    return !E(msgs_nomem());
  code = smtp_get_reply(sinfo, &buffer);
  if (code == -1)
  {
    char str[32];
    sprintf(str, "%d", errno);
    xsyslog_logmessage(log_NAME,
      msgs_lookup2("NoLoginRep", errlist_str(errno), str),
      log_SocketError);
    return false;
  }
  else if (code / 100 != 2)
  {
    xsyslog_logmessage(log_NAME, msgs_lookup1("BadLogin", buffer),
    	log_ServerError);
    return false;
  }
  
  /* try to send an EHLO */
  ehloresult = smtp_startesmtp(sinfo, hostname);
  if (ehloresult == smtp_Success)
    return true;
  if (ehloresult == smtp_Abort)
    return false;
  assert(ehloresult == smtp_Fail);
  
  /* EHLO failed, send a HELO instead */
  sprintf(helo, "HELO %.*s", sizeof(helo) - sizeof("HELO ") - 1, hostname);
  if (!xsock_xwrite(sinfo->s, helo))
    return false;
  
  code = smtp_get_reply(sinfo, &buffer);
  if (code == -1)
  {
    char str[32];
    sprintf(str, "%d", errno);
    xsyslog_logmessage(log_NAME,
      msgs_lookup3("NoRep", "HELO", errlist_str(errno), str),
      log_SocketError);
    return false;
  }
  else if (code / 100 != 2)
  {
    xsyslogf(log_NAME, log_ServerError, "%s : %s",
    	msgs_lookup1("BadRep", "HELO"), buffer);
    return false;
  }
  
  /* we're now connected & have done a HELO */

  return true;
}

static bool smtp_exit_installed = false;

static void smtp_stop_static()
{
  smtp_stop(&smtp_statinfo, true, true);
}

bool smtp_send(int *sent)
{
  int size;
  smtp_info *sinfo = &smtp_statinfo;
  static const smtp_info empty_sinfo;
  stopquit_t stopped;
  char leaf[MAX_LEAF_LEN];
  char *static_server = NULL;
#ifdef FEATURE_WORKFILE_GATEWAY
  char server[128] = "";
  char lastserver[128] = "";
#endif

  *sent = 0;

  if (!check_work())
    return true;

  /* initialise all of smtp_info structure to sensible defaults */
  *sinfo = empty_sinfo;
  sinfo->s = -1;

#ifndef FEATURE_WORKFILE_GATEWAY
  static_server = smtp_getconfig("SMTP");
  if (static_server == NULL)
  {
    treport(msgs_lookup("NoSMTP"), log_OSError);
    return false;
  }
#endif

  sinfo->status = status_create();
  status_set_label(sinfo->status, msgs_lookup("Sending"));

#ifndef FEATURE_WORKFILE_GATEWAY
  if (!smtp_connect(sinfo, static_server))
  {
    smtp_stop(sinfo, true, true);
    free(static_server);
    return false;
  }
  sinfo->server = static_server;
#endif

  if (!smtp_exit_installed)
  {
    atexit(smtp_stop_static);
    smtp_exit_installed = true;
  }

  status_count_total(sinfo->status, count_text(&size)); /* number of msgs */
  status_total_total(sinfo->status, size); /* total size of msgs */

  sinfo->stopped = stopquit_Null;
  stopquit_registerhandler( smtp_stop_handler, sinfo );

  *leaf = 0;
  /* Size is actually now message index for status */
  for ( size = 1 ; /* no termination */; ++size )
  {
    if (smtp_findnext( work_dir, leaf, sizeof leaf ) == -1)
      break; /* no more files */

#ifdef FEATURE_WORKFILE_GATEWAY
    strcpy(lastserver, server);
    
    if (!read_gateway(leaf, server, sizeof server))
      /* failed to read gateway from this email - try next one */
      continue;
    
    if (strcmp(server,lastserver) != 0)
    {
      /* different server to last time */
      if (lastserver[0] != '\0')
      {
	/* disconnect from previous server */
	smtp_stop(sinfo, false, false);
      }
      
      if (!smtp_connect(sinfo, server))
	break;
      sinfo->server = server;
    }
#endif
  
    xsyslog_logmessage(log_NAME, msgs_lookup1("SendLeaf", leaf), log_DebugInfo);

    status_count(sinfo->status, size);

    if ( process_leaf(sinfo, leaf) == smtp_Abort )
      break;
    
    (*sent)++;
  }
  stopquit_deregister();
  stopped = sinfo->stopped;
  smtp_stop(sinfo, stopped, true);
  if (stopped)
    stopquit_act(stopped);
  if (static_server)
    free(static_server);
  return true;
}
