#include <unistd.h>

#include "base64.h"
#include "buffer.h"
#include "byte.h"
#include "case.h"
#include "cdbread.h"
#include "close.h"
#include "constmap.h"
#include "dnsresolv.h"
#include "env.h"
#include "error.h"
#include "exit.h"
#include "fd.h"
#include "fmt.h"
#include "ip.h"
#include "open.h"
#include "scan.h"
#include "sig.h"
#include "str.h"
#include "stralloc.h"
#include "timeout.h"
#include "wait.h"

#include "commands.h"
#include "control.h"
#include "dns.h"
#include "ipme.h"
#include "mfrules.h"
#include "now.h"
#include "qmail.h"
#include "rcpthosts.h"
#include "received.h"
#include "recipients.h"
#include "smtpdlog.h"
#include "spf.h"
#include "tls_start.h"
#include "wildmat.h"

#ifdef USE_CONFIG
  #include "fehsqm-config.h"
#else
  #include "auto_qmail.h"
#endif

/**
 * @file qmail-smtpd.c -- authenticating ESMTP/ESMTPS server
 * @brief requires sslserver or tcpserver
 */

#define PAM111421
#define AUTHSLEEP  5
#define PORT_SMTPS "465"

#define MIMETYPE_LEN  9
#define LOADER_LEN    5
#define BASE64MESSAGE "content-transfer-encoding: base64"
#define FDIN          0
#define FDOUT         1
#define FDLOG         2
#define FDAUTH        3

#define BUFFER_SIZE 1024
#define MAXHOPS     100
unsigned long databytes = 0;
long timeout = 1200;

static int modssl_info(void);

ssize_t safewrite(int fd, char *buf, int len)
{
  int r;
  r = timeoutwrite(timeout, fd, buf, len);
  if (r <= 0) _exit(1);
  return r;
}

char outbuf[BUFFER_SIZE / 2];
buffer bo = BUFFER_INIT(safewrite, FDOUT, outbuf, sizeof(outbuf));

void flush()
{
  buffer_flush(&bo);
}  // this triggers writing to STDIO

static ssize_t saferead(int fd, char *buf, int len)
{
  int r;
  flush();
  r = timeoutread(timeout, fd, buf, len);
  if (r == -1)
    if (errno == ETIMEDOUT) die_alarm();
  if (r <= 0) die_read();
  return r;
}

char inbuf[BUFFER_SIZE];
buffer bi = BUFFER_INIT(saferead, FDIN, inbuf, sizeof(inbuf));

char logbuf[256];
buffer bl = BUFFER_INIT(write, FDLOG, logbuf, sizeof(logbuf));

void out(char *s)
{
  buffer_puts(&bo, s);
}

stralloc sa = {0};
ipalloc ia = {0};

int bhelocheck(void);

/* this file is too long -------------------------------------- DNS helper    */

static int dnsq(char *arg, char type)
{
  unsigned int random;
  int at;
  int r = -2;
  int len;

  len = str_len(arg);
  if (len < 1) return r;

  if (arg[len - 1] == ' ') len--; /* trailing blank */
  if (len < 1) return r;

  at = byte_rchr(arg, len, '@');
  if (at < len) {
    if (!stralloc_copyb(&sa, arg + at + 1, len - at - 1)) die_nomem();
  } else if (!stralloc_copyb(&sa, arg, len)) {
    die_nomem();
  }

  random = now() + (getpid() << 16);

  switch (type) { /* Common for IPv4 and IPv6 */
    case 'A': r = dns_ip(&ia, &sa); break;
    case 'M': r = dns_mxip(&ia, &sa, random); break;
  }

  switch (r) {
    case DNS_ERR: out("451 DNS temporary failure (#4.3.0)\r\n"); return -1;
    case DNS_MEM: die_nomem();
    default:
      if (ia.len) return 0;
  }

  return 1;
}

/* this file is too long -------------------------------------- Greeting      */

static stralloc greeting = {0};

static void smtp_greet(char *code)
{
  buffer_puts(&bo, code);
  buffer_put(&bo, greeting.s, greeting.len);
}

static void smtp_help()
{
  out("214 s/qmail home page: https://www.fehcom.de/sqmail.html\r\n");
}

static void smtp_quit()
{
  smtp_greet("221 ");
  out("\r\n");
  flush();
  _exit(0);
}

char *remoteip;
char *remotehost;
char *remoteinfo;
char *local;
char *localport;
char *relayclient;
int flagutf8 = 0;

stralloc protocol = {0};
stralloc helohost = {0};
char *fakehelo; /* pointer into helohost, or 0 */
stralloc tlsinfo = {0};

char *helocheck;
int flagbadhelo;
int flagdnshelo;
int seenhelo = 0;

char *badmailcond;
char *badhelocond;

static void dohelo(char *helo)
{
  if (!stralloc_copys(&helohost, helo)) die_nomem();
  if (!stralloc_0(&helohost)) die_nomem();
  fakehelo = case_diffs(remotehost, helohost.s) ? helohost.s : 0;
  if (helocheck) {
    if (str_len(helocheck) == 1) {
      switch (*helocheck) {
        case '=':
          flagbadhelo = bhelocheck();
          if (fakehelo) {
            flagdnshelo = 1;
            badhelocond = "=";
          }
          break;
        case 'A':
          flagbadhelo = bhelocheck();
          if (flagbadhelo == 0) {
            flagdnshelo = dnsq(helohost.s, 'A');
            badhelocond = "A";
          }
          break;
        case 'M':
          flagbadhelo = bhelocheck();
          if (flagbadhelo == 0) {
            flagdnshelo = dnsq(helohost.s, 'M');
            badhelocond = "M";
          }
          break;
        case '.':
          flagbadhelo = bhelocheck();
          if (!str_len(helo)) flagbadhelo = -2;
          break;
        case '!':
          if (!str_len(helo)) flagbadhelo = -2;
          break;
      }
    } else {
      flagbadhelo = bhelocheck();
    }
    if (flagbadhelo == -3) flagbadhelo = 0;
  }
  if (!env_put("HELOHOST", helohost.s)) die_nomem();
}

int liphostok = 0;
stralloc liphost = {0};

int bmfok = 0;
stralloc bmf = {0};
struct constmap mapbmf;

int brtok = 0;
stralloc brt = {0};
struct constmap mapbrt;

int badhelook = 0;
stralloc badhelo = {0};
struct constmap mapbhlo;

static struct cdb cdbm;
static struct cdb cdbl;

static int fdbmt;

/*
  -2: found white
  -1: found in cdb;
   1: white;
   2: cdb;
   3: white+cdb;
   4: !relay+white;
   6: !relay+white+cdb;
*/
int flagmimetype = 0;

char *badmimeinit;

static int fdblt;
int flagloadertype = 0; /* 1: cdb; 2: !relay+cdb; -1: found in cdb */
char *badloaderinit;

static int fdmav;
int flagmav = 0;
int localmf = 0; /* 1: domainpart->rcpthosts; 2: ->mailformrules; 3: ->remoteinfo; 4: ->DN(Email) */
char *localmfcheck;

char *mfdnscheck;
char *qhpsi;
char *base64;

int maxrcptcount = 0;
int flagerrcpts = 0;
int flagnotorious = 0;

int tarpitcount = 0;
int tarpitdelay = 0;

int greylist = 0;
stralloc pgbind = {0};

char *auth;

/*
  -1: Cert
   0: none
   1: login/plain
   2: cram
   3: login/plain/cram
  11: must_login/plain
  12: must_2
  13: must_3
*/
int smtpauth = 0;

int seenauth = 0; /* 1:ESMTPA 2:~CLIENTDN */
stralloc authmethod = {0};

/*
  -1: TLS
   0: none
   1: STARTTLS
   2: require_STARTTLS
   3: relay_if_CLIENTDN
   4: require_+_relay_if_CLIENTDN
*/
int starttls = 0;

int seentls = 0; /* 1:~STARTTLS 2:~TLS 3:~CLIENTDN */
char *ucspitls = 0;
char *tlsversion;
char *cipher;
char *cipherperm;
char *cipherused;
char *clientdn;
char *clientcn;
char *dnemail;

stralloc mailto = {0};
stralloc deliverto = {0};
char *delivermailto;
stralloc rblinfo = {0};
char *rblsmtpd;

int flagspf = 0;
static stralloc spfbounce = {0};

static void setup()
{
  char *x;
  unsigned long u;
  int r;
  flagip6 = 1;  // GCC 10 implicit int declarition if global var

  if (control_init() == -1) die_control();
  if (control_rldef(&greeting, "control/smtpgreeting", 1, (char *)0) != 1) die_control();
  liphostok = control_rldef(&liphost, "control/localiphost", 1, (char *)0);
  if (liphostok == -1) die_control();
  if (control_readint(&timeout, "control/timeoutsmtpd") == -1) die_control();
  if (timeout <= 0) timeout = 1;

  if (rcpthosts_init() == -1) die_control();
  if (recipients_init() == -1) die_control();

  bmfok = control_readfile(&bmf, "control/badmailfrom", 0);
  if (bmfok == -1) die_control();
  if (bmfok)
    if (!constmap_init(&mapbmf, bmf.s, bmf.len, 0)) die_nomem();

  brtok = control_readfile(&brt, "control/badrcptto", 0);
  if (brtok == -1) die_control();
  if (brtok)
    if (!constmap_init(&mapbrt, brt.s, brt.len, 0)) die_nomem();

  if (control_readint(&databytes, "control/databytes") == -1) die_control();
  x = env_get("DATABYTES");
  if (x) {
    scan_ulong(x, &u);
    databytes = u;
  }
  if (!(databytes + 1)) --databytes;

  if (!stralloc_copys(&protocol, "ESMTP")) die_nomem(); /* RFC 3848 */
  remoteip = env_get("TCP6REMOTEIP");                   /* compactified IPv6 */
  if (!remoteip) remoteip = env_get("TCPREMOTEIP");     /* allow other tcpserver/sslserver */
  if (remoteip && (str_chr(remoteip, ':') < str_len(remoteip))) {
    if (byte_equal(remoteip, 7, V4MAPPREFIX)) {
      remoteip = remoteip + 7;
      flagip6 = 0;
    }
  } else {
    flagip6 = 0;
  }
  if (!remoteip) {
    remoteip = "unknown";
    flagip6 = -1;
  }
  local = env_get("TCP6LOCALHOST");
  if (!local) local = env_get("TCPLOCALHOST");
  if (!local) local = env_get("TCP6LOCALIP");
  if (!local) local = env_get("TCPLOCALIP");
  if (!local) local = "unknown";
  localport = env_get("TCP6LOCALPORT");
  if (!localport) localport = env_get("TCPLOCALPORT");
  if (!localport) localport = "0";
  remotehost = env_get("TCP6REMOTEHOST");
  if (!remotehost) remotehost = env_get("TCPREMOTEHOST");
  if (!remotehost) remotehost = "unknown";
  remoteinfo = env_get("TCP6REMOTEINFO");
  if (!remoteinfo) remoteinfo = env_get("TCPREMOTEINFO");
  relayclient = env_get("RELAYCLIENT");

  if (!case_diffs(localport, PORT_SMTPS)) {
    if (!modssl_info()) die_starttls();
    starttls = -1;
  }

  mfdnscheck = env_get("MFDNSCHECK");
  x = env_get("MAXRECIPIENTS");
  if (x) {
    scan_ulong(x, &u);
    maxrcptcount = u;
  }
  if (!(maxrcptcount + 1)) --maxrcptcount;

  helocheck = env_get("HELOCHECK");
  if (helocheck) {
    badhelook = control_readfile(&badhelo, "control/badhelo", 0);
    if (badhelook == -1) die_control();
    if (badhelook)
      if (!constmap_init(&mapbhlo, badhelo.s, badhelo.len, 0)) die_nomem();
  }

  x = env_get("TARPITCOUNT");
  if (x) {
    scan_ulong(x, &u);
    tarpitcount = u;
  }
  x = env_get("TARPITDELAY");
  if (x) {
    scan_ulong(x, &u);
    tarpitdelay = u;
  }

  x = env_get("POSTGREY");  // RFC 6647
  if (x) {
    if (case_diffs(x, "-")) {
      greylist = 1;
      if (!stralloc_copys(&pgbind, x)) die_nomem();
      if (!stralloc_append(&pgbind, "")) die_nomem();
    }
  }

  localmfcheck = env_get("LOCALMFCHECK");
  if (localmfcheck) {
    localmf = 1;
    if (str_len(localmfcheck) == 1 && *localmfcheck == '!') {
      localmf = 2;
      fdmav = open_read("control/mailfromrules.cdb");
      if (fdmav == -1) localmf = 1;
    }
    if (str_len(localmfcheck) == 1 && *localmfcheck == '=') {
      localmf = 3;
    }
    if (str_len(localmfcheck) == 1 && *localmfcheck == '?') {
      localmf = 4;
    }
  }

  badmimeinit = env_get("BADMIMETYPE");
  if (badmimeinit) {
    fdbmt = open_read("control/badmimetypes.cdb");
    if (str_len(badmimeinit) == 1) {
      if (*badmimeinit == '-')
        flagmimetype = 0;
      else {
        if (*badmimeinit == '!') flagmimetype = 1;
        if (*badmimeinit == '+') flagmimetype = 4;
      }
    }
    if (fdbmt != -1) flagmimetype = flagmimetype + 2;
  }

  badloaderinit = env_get("BADLOADERTYPE");
  if (badloaderinit) {
    if (str_len(badloaderinit) == 1) {
      if (*badloaderinit == '-')
        flagloadertype = 0;
      else {
        flagloadertype = 1;
        if (*badloaderinit == '+') flagloadertype = 2;
        fdblt = open_read("control/badloadertypes.cdb");
        if (fdblt == -1) flagloadertype = 0;
      }
    }
  }

  base64 = env_get("BASE64");
  qhpsi = env_get("QHPSI");
  auth = env_get("SMTPAUTH");
  if (auth) {
    smtpauth = 1;
    if (!case_diffs(auth, "-")) smtpauth = 0;
    if (!case_diffs(auth, "!")) smtpauth = 11;
    if (case_starts(auth, "cram")) smtpauth = 2;
    if (case_starts(auth, "+cram")) smtpauth = 3;
    if (case_starts(auth, "!cram")) smtpauth = 12;
    if (case_starts(auth, "!+cram")) smtpauth = 13;
  }

  if (!seentls) {
    ucspitls = env_get("UCSPITLS");
    if (ucspitls) {
      starttls = 1;
      if (!case_diffs(ucspitls, "-")) starttls = 0;
      if (!case_diffs(ucspitls, "!")) starttls = 2;
      if (!case_diffs(ucspitls, "?")) starttls = 3;
      if (!case_diffs(ucspitls, "@")) starttls = 4;
    }
  }

  delivermailto = env_get("DELIVERTO");
  if (delivermailto) {
    if (!stralloc_cats(&mailto, delivermailto)) die_nomem();
    if (!stralloc_cats(&mailto, " ")) die_nomem();
  }

  rblsmtpd = env_get("RBLSMTPD");
  if (rblsmtpd) {
    if (!stralloc_cats(&rblinfo, rblsmtpd)) die_nomem();
    if (!stralloc_0(&rblinfo)) die_nomem();
  }

  x = env_get("SPF");
  if (x) {
    scan_ulong(x, &u);
    flagspf = u;
  }
  if (flagspf < 0 || flagspf > 6) die_control();
  if (flagspf) {
    r = control_readline(&spflocalrules, "control/spflocalrules");
    if (r == -1) die_control();
    if (!stralloc_0(&spflocalrules)) die_nomem();
    if (control_rldef(&spfexplain, "control/spfexplain", 0, SPF_DEFEXP) == -1) die_control();
    if (!stralloc_0(&spfexplain)) die_nomem();
  }

  x = env_get("UTF8");
  if (x) flagutf8 = 1;

  if (!stralloc_copys(&helohost, "")) die_nomem();  // helohost is initially empty
  if (!stralloc_0(&helohost)) die_nomem();
  fakehelo = 0;
}

static void auth_info(char *method)
{
  if (!env_put("AUTHPROTOCOL", method)) die_nomem();
  if (!env_put("AUTHUSER", remoteinfo)) die_nomem();
  if (!env_unset("TCPREMOTEINFO")) die_read();
  if (!env_put("TCPREMOTEINFO", remoteinfo)) die_nomem();
  if (!env_unset("TCP6REMOTEINFO")) die_read();
  if (!env_put("TCP6REMOTEINFO", remoteinfo)) die_nomem();

  if (!stralloc_append(&protocol, "A")) die_nomem();
}

static int modssl_info()
{
  tlsversion = env_get("SSL_PROTOCOL");
  if (!tlsversion) return 0;

  cipher = env_get("SSL_CIPHER");
  if (!cipher) cipher = "unknown";
  cipherperm = env_get("SSL_CIPHER_ALGKEYSIZE");
  if (!cipherperm) cipherperm = "unknown";
  cipherused = env_get("SSL_CIPHER_USEKEYSIZE");
  if (!cipherused) cipherused = "unknown";
  clientdn = env_get("SSL_CLIENT_S_DN");
  if (!clientdn) {
    clientdn = "none";
  } else {
    seentls = 3;
    seenauth = 2;
    smtpauth = -1;
    relayclient = "";
  }

  if (!stralloc_copys(&tlsinfo, tlsversion)) die_nomem();
  if (!stralloc_cats(&tlsinfo, ": ")) die_nomem();
  if (!stralloc_cats(&tlsinfo, cipher)) die_nomem();
  if (!stralloc_cats(&tlsinfo, " [")) die_nomem();
  if (!stralloc_cats(&tlsinfo, cipherused)) die_nomem();
  if (!stralloc_cats(&tlsinfo, "/")) die_nomem();
  if (!stralloc_cats(&tlsinfo, cipherperm)) die_nomem();
  if (!stralloc_cats(&tlsinfo, "] \n")) die_nomem();
  if (!stralloc_cats(&tlsinfo, "  DN=")) die_nomem();
  if (!stralloc_cats(&tlsinfo, clientdn)) die_nomem();
  if (!stralloc_0(&tlsinfo)) die_nomem();

  if (!stralloc_append(&protocol, "S")) die_nomem();

  if (seentls == 3 && starttls == 4) {
    clientcn = env_get("SSL_CLIENT_S_DN_CN");
    remoteinfo = clientcn ? clientcn : clientdn;
    dnemail = env_get("SSL_CLIENT_S_DN_Email");
    if (!dnemail) dnemail = "unknown";
    if (!stralloc_cats(&authmethod, "cert")) die_nomem();
    auth_info(authmethod.s);
  }
  return 1;
}

/* this file is too long --------------------------------- Address checks */

stralloc addr = {0}; /* will be 0-terminated, if addrparse returns 1 */
stralloc eddr = {0}; /* extended address; used for smart address recognition */
stralloc rddr = {0}; /* test anti-spoofing host name */
stralloc mailfrom = {0};
stralloc rcptto = {0};
stralloc user = {0};
stralloc fuser = {0};
stralloc mfparms = {0};

int seenmail = 0;
int flagaddr; /* defined if seenmail */
int flagrcpt;
int flagdnsmf = 0;
int flagsize;
int rcptcount = 0;

static int addrparse(char *arg)
{
  int i;
  char ch;
  char terminator;
  struct ip4_address ip4s;
  struct ip6_address ip6s;
  int flagesc;
  int flagquoted;

  terminator = '>';
  i = str_chr(arg, '<');
  if (arg[i])
    arg += i + 1;
  else
    return 0;

  /* strip source route */
  if (*arg == '@')
    while (*arg)
      if (*arg++ == ':') break;

  if (!stralloc_copys(&addr, "")) die_nomem();
  flagesc = 0;
  flagquoted = 0;
  for (i = 0; (ch = arg[i]); ++i) { /* copy arg to addr, stripping quotes */
    if (flagesc) {
      if (!stralloc_append(&addr, &ch)) die_nomem();
      flagesc = 0;
    } else {
      if (!flagquoted && (ch == terminator)) break;
      switch (ch) {
        case '\\': flagesc = 1; break;
        case '"':  flagquoted = !flagquoted; break;
        default:
          if (!stralloc_append(&addr, &ch)) die_nomem();
      }
    }
  }
  /* could check for termination failure here, but why bother? */
  if (!stralloc_append(&addr, "")) die_nomem();

  if (liphostok) {
    i = byte_rchr(addr.s, addr.len, '@');
    if (i < addr.len) /* if not, partner should go read rfc 821 */
      if (addr.s[i + 1] == '[') {
        if (byte_rchr(addr.s + i + 2, addr.len - i - 2, ':') < str_len(addr.s)) { /* @[IPv6::] */
          if (!addr.s[i + 1 + ip6_scanbracket(addr.s + i + 1, (char *)&ip6s.d)])
            if (ipme_is6(&ip6s)) {
              addr.len = i + 1;
              if (!stralloc_cat(&addr, &liphost)) die_nomem();
            }
        } else { /* @[IPv4] */
          if (!addr.s[i + 1 + ip4_scanbracket(addr.s + i + 1, (char *)&ip4s.d)])
            if (ipme_is4(&ip4s)) {
              addr.len = i + 1;
              if (!stralloc_cat(&addr, &liphost)) die_nomem();
            }
        }
      }
    if (!stralloc_0(&addr)) die_nomem();
  }

  if (addr.len > 900) return 0;
  return 1;
}

int bhelocheck()
{
  int i;
  int j;
  int k = 0;
  char subvalue;

  if (badhelook && helohost.len > 1) { /* helohost! */
    if (!stralloc_copyb(&eddr, helohost.s, helohost.len - 1)) die_nomem();
    if (!stralloc_append(&eddr, "!")) die_nomem();
    if (!stralloc_0(&eddr)) die_nomem();
    if (constmap(&mapbhlo, eddr.s, eddr.len - 1)) return -3;

    if (constmap(&mapbhlo, helohost.s, helohost.len - 1)) return -1;

    i = 0;
    for (j = 0; j < badhelo.len; ++j)
      if (!badhelo.s[j]) {
        subvalue = badhelo.s[i] != '!';
        if (!subvalue) i++;
        if ((k != subvalue) && wildmat(helohost.s, badhelo.s + i)) k = subvalue;
        i = j + 1;
      }
    return k;
  }
  return 0;
}

static int bmfcheck()
{
  int i = 0;
  int j = 0;
  int k = 0;
  int at = 0;
  int dlen;
  int rlen;
  char subvalue;

  if (bmfok && mailfrom.len > 1) {
    rlen = str_len(remotehost);
    at = byte_rchr(mailfrom.s, mailfrom.len, '@');

    /* '?' enhanced address to skip all other tests including MFDNSCHECK */

    if (!stralloc_copys(&eddr, "?")) die_nomem();
    if (!stralloc_cat(&eddr, &mailfrom)) die_nomem();
    case_lowerb(eddr.s, eddr.len);
    if (constmap(&mapbmf, eddr.s, eddr.len - 1)) return -110;

    /* '+' extended address for none-RELAYCLIENTS */

    if (at && !relayclient) {
      if (!stralloc_copyb(&eddr, mailfrom.s, mailfrom.len - 1)) die_nomem();
      if (!stralloc_append(&eddr, "+")) die_nomem();
      if (!stralloc_0(&eddr)) die_nomem();
      case_lowerb(eddr.s, eddr.len);
      if (constmap(&mapbmf, eddr.s + at, eddr.len - at - 1)) return -5;
    }

    /* '-' extended address from UNKNOWN */

    if (at && !case_diffs(remotehost, "unknown")) {
      if (!stralloc_copyb(&eddr, mailfrom.s, mailfrom.len - 1)) die_nomem();
      if (!stralloc_append(&eddr, "-")) die_nomem();
      if (!stralloc_0(&eddr)) die_nomem();
      case_lowerb(eddr.s, eddr.len);
      if (constmap(&mapbmf, eddr.s + at, eddr.len - at - 1)) return -4;
    }

    /* '=' extended address for WELLKNOWN senders */

    else if (at && rlen >= mailfrom.len - at - 1)
    {
      dlen = mailfrom.len - at - 2;
      if (!stralloc_copyb(&eddr, mailfrom.s, mailfrom.len - 1)) die_nomem();
      if (!stralloc_append(&eddr, "=")) die_nomem();
      if (!stralloc_0(&eddr)) die_nomem();
      case_lowerb(eddr.s, eddr.len);
      if (str_diffn(remotehost + rlen - dlen, eddr.s + at + 1, dlen))
        if (constmap(&mapbmf, eddr.s + at, eddr.len - at - 1)) return -3;

      /* '~' extended address for MISMATCHED Domains */

      if (case_diffrs(remotehost, mailfrom.s + at + 1)) {
        j = 0;
        do {
          if (!stralloc_copys(&eddr, "~")) die_nomem();
          if (!stralloc_cats(&eddr, remotehost + j)) die_nomem();
          if (!stralloc_0(&eddr)) die_nomem();
          if (constmap(&mapbmf, eddr.s, eddr.len - 1)) return -2;
          j = byte_chr(remotehost + j, rlen - j, '.') + j + 1;
        } while (j > 0 && rlen - j > 0);
      }
    }

    /* Standard */

    if (constmap(&mapbmf, mailfrom.s, mailfrom.len - 1)) return -1;
    if (at && at < mailfrom.len)
      if (constmap(&mapbmf, mailfrom.s + at, mailfrom.len - at - 1)) return -1;

    /* Wildmating */

    i = k = 0;
    for (j = 0; j < bmf.len; ++j) {
      if (!bmf.s[j]) {
        subvalue = bmf.s[i] != '!';
        if (!subvalue) i++;
        if ((k != subvalue) && wildmat(mailfrom.s, bmf.s + i)) k = subvalue;
        i = j + 1;
      }
    }
    return k;
  }

  return 0;
}

static int brtcheck()
{
  int i;
  int j = 0;
  int k = 0;
  char subvalue;

  if (brtok) {
    if (constmap(&mapbrt, addr.s, addr.len - 1)) return -2;

    int at = byte_rchr(addr.s, addr.len, '@');
    if (at < addr.len)
      if (constmap(&mapbrt, addr.s + at, addr.len - at - j)) return -1;

    /* '#' enhanced address to consider invalid rcptto addresses for none-relayclients */

    if (!relayclient) {
      if (!stralloc_copys(&eddr, "+")) die_nomem();
      if (!stralloc_cat(&eddr, &addr)) die_nomem();
      if (!stralloc_0(&eddr)) die_nomem();
      case_lowerb(eddr.s, eddr.len);
      if (constmap(&mapbmf, eddr.s, eddr.len - 1)) return 110;
    }

    i = 0;
    for (j = 0; j < brt.len; ++j) {
      if (!brt.s[j]) {
        subvalue = brt.s[i] != '!';
        if (!subvalue) i++;
        if ((k != subvalue) && wildmat(addr.s, brt.s + i)) k = subvalue;
        i = j + 1;
      }
    }
    return k;
  }
  return 0;
}

static int addrallowed(char *arg)
{
  int r;
  r = rcpthosts(arg, str_len(arg));
  if (r == -1) die_control();
  return r;
}

static int rcptallowed()
{
  int r;
  r = recipients(addr.s, str_len(addr.s));
#ifdef PAM111421
  if (r == 111) die_recipients();
#endif
  if (r == -3) die_recipients();
  if (r == -2) die_nomem();
  if (r == -1) die_control();
  return r;
}

static int localaddr(char *mf)
{
  int at;
  int mflen;

  mflen = str_len(mf);
  if (mflen < 1) return 0;

  if (localmf == 4) {
    if (!case_diffs(dnemail, mf)) return 2;
    return -4;
  }
  if (localmf == 3) {
    if (!case_diffs(remoteinfo, mf)) return 2;
    return -3;
  } else if (localmf == 2) {
    return mfrules(fdmav, remoteip, remotehost, remoteinfo, mf);
  } else {
    if (str_len(localmfcheck) > 1) {
      case_lowerb(localmfcheck, str_len(localmfcheck));
      at = byte_rchr(mf, mflen, '@');
      if (at < mflen)
        if (!str_diffn(localmfcheck, mf + at + 1, mflen - at - 1)) return 2;
    }
    if (addrallowed(mf)) return 3;
    return -2;
  }
}

static int spf_check(int flag6)
{
  int r;

  if (mailfrom.len <= 1) {
    flagspf = 0;
    return 0;
  }

  DNS_INIT
  r = spf_query(remoteip, helohost.s, mailfrom.s, local, flag6);
  if (r == SPF_NOMEM) die_nomem();
  if (!stralloc_0(&spfinfo)) die_nomem();

  switch (r) {
    case SPF_ME:
    case SPF_OK:
      if (!env_put("SPFRESULT", "pass")) die_nomem();
      flagspf = 10;
      break;
    case SPF_LOOP:
    case SPF_ERROR:
    case SPF_SYNTAX:
    case SPF_EXHAUST:
      if (!env_put("SPFRESULT", "error")) die_nomem();
      if (flagspf < 2) {
        flagspf = 0;
        break;
      }
      out("451 SPF lookup failure (#4.3.0)\r\n");
      return -1;
    case SPF_NONE:
      if (!env_put("SPFRESULT", "none")) die_nomem();
      flagspf = 0;
      break;
    case SPF_UNKNOWN:
      if (!env_put("SPFRESULT", "unknown")) die_nomem();
      if (flagspf < 6)
        break;
      else
        return 4;
    case SPF_NEUTRAL:
      if (!env_put("SPFRESULT", "neutral")) die_nomem();
      if (flagspf < 5)
        break;
      else
        return 3;
    case SPF_SOFTFAIL:
      if (!env_put("SPFRESULT", "softfail")) die_nomem();
      if (flagspf < 4)
        break;
      else
        return 2;
    case SPF_FAIL:
      if (!env_put("SPFRESULT", "fail")) die_nomem();
      if (flagspf < 3) break;
      if (!spf_parse(&spfbounce, spfexpmsg.s, expdomain.s)) die_nomem();
      return 1;
  }

  return 0;
}

/* this file is too long --------------------------------- MF parser   */

static int mailfrom_size(char *arg)
{
  unsigned long r;
  unsigned long sizebytes = 0;

  scan_ulong(arg, &r);
  sizebytes = r;
  if (databytes)
    if (sizebytes > databytes) return 1;
  return 0;
}

static void mailfrom_auth(char *arg, int len)
{
  if (!stralloc_copys(&fuser, "")) die_nomem();
  if (case_starts(arg, "<>")) {
    if (!stralloc_cats(&fuser, "unknown")) die_nomem();
  } else {
    while (len) {
      if (*arg == '+') {
        if (case_starts(arg, "+3D")) {
          arg = arg + 2;
          len = len - 2;
          if (!stralloc_cats(&fuser, "=")) die_nomem();
        }
        if (case_starts(arg, "+2B")) {
          arg = arg + 2;
          len = len - 2;
          if (!stralloc_cats(&fuser, "+")) die_nomem();
        }
      } else {
        if (!stralloc_catb(&fuser, arg, 1)) die_nomem();
      }
      arg++;
      len--;
    }
  }

  if (!stralloc_0(&fuser)) die_nomem();
  if (!remoteinfo) {
    remoteinfo = fuser.s;
    if (!env_unset("TCPREMOTEINFO")) die_read();
    if (!env_put("TCPREMOTEINFO", remoteinfo)) die_nomem();
    if (!env_unset("TCP6REMOTEINFO")) die_read();
    if (!env_put("TCP6REMOTEINFO", remoteinfo)) die_nomem();
  }
}

static void mailfrom_parms(char *arg)
{
  int len;

  if ((len = str_len(arg))) {
    if (!stralloc_copys(&mfparms, "")) die_nomem();
    while (len) {
      arg++;
      len--;
      if (*arg == ' ' || *arg == '\0') {
        if (flagutf8)
          if (case_starts(mfparms.s, "SMTPUTF8")) flagutf8 = 2;
        if (case_starts(mfparms.s, "SIZE="))
          if (mailfrom_size(mfparms.s + 5)) {
            flagsize = 1;
            return;
          }
        if (case_starts(mfparms.s, "AUTH=")) mailfrom_auth(mfparms.s + 5, mfparms.len - 5);
        if (!stralloc_copys(&mfparms, "")) die_nomem();
      } else if (!stralloc_catb(&mfparms, arg, 1)) {
        die_nomem();
      }
    }
  }
}

/* this file is too long --------------------------------- SMTP dialog */

static void smtp_helo(char *arg)
{
  smtp_greet("250 ");
  out("\r\n");
  flush();
  seenmail = 0;
  rcptcount = 0;
  seenhelo++;
  dohelo(arg);
}

static void smtp_ehlo(char *arg)
{
  char size[FMT_ULONG];

  smtp_greet("250-");
  out("\r\n");
  out("250-PIPELINING\r\n250-8BITMIME\r\n");
  if (flagutf8) out("250-SMTPUTF8\r\n");
  if (starttls > 0 && !seentls) out("250-STARTTLS\r\n");

  switch (smtpauth) {
    case 1:
    case 11: out("250-AUTH LOGIN PLAIN\r\n"); break;
    case 2:
    case 12: out("250-AUTH CRAM-MD5\r\n"); break;
    case 3:
    case 13: out("250-AUTH LOGIN PLAIN CRAM-MD5\r\n"); break;
  }

  size[fmt_ulong(size, (unsigned long)databytes)] = 0;
  out("250 SIZE ");
  out(size);
  out("\r\n");

  seenhelo++;
  seenmail = 0;
  rcptcount = 0;
  dohelo(arg);
}

static void smtp_rset(char *x)
{
  (void)x;

  seenmail = 0;
  rcptcount = 0; /* RFC 5321: seenauth + seentls stay */

  if (!stralloc_copys(&mailfrom, "")) die_nomem();
  if (!stralloc_copys(&rcptto, "")) die_nomem();
  out("250 flushed\r\n");
}

static void smtp_starttls()
{
  if (starttls == 0) err_starttls();

  out("220 Ready to start TLS (#5.7.0)\r\n");
  flush();

  if (!starttls_init()) die_starttls();
  buffer_init(&bi, saferead, FDIN, inbuf, sizeof(inbuf));
  seentls = 2;

  if (!starttls_info()) die_starttls();
  if (!modssl_info()) die_starttls();

  /* reset SMTP state */

  seenhelo = 0;
  seenmail = 0;
  rcptcount = 0;
  if (!stralloc_copys(&addr, "")) die_nomem();
  if (!stralloc_copys(&helohost, "")) die_nomem();
  if (!stralloc_copys(&mailfrom, "")) die_nomem();
  if (!stralloc_copys(&rcptto, "")) die_nomem();
  if (seenauth == 1) seenauth = 0; /* Otherwise Auth by client Cert */
}

static void smtp_mail(char *arg)
{
  if (flagutf8)
    if (!stralloc_cats(&protocol, "UTF8")) die_nomem();
  if (!stralloc_0(&protocol)) die_nomem();

  if ((starttls > 1) && !seentls) {
    err_tlsreq("Reject::TLS::missing", protocol.s, remoteip, remotehost, helohost.s);
    return;
  }
  if (smtpauth > 10 && !seenauth) {
    err_authreq("Reject::AUTH::missing", protocol.s, remoteip, remotehost, helohost.s);
    return;
  }
  if (!addrparse(arg)) {
    err_syntax();
    return;
  }

  flagsize = 0;
  rcptcount = 0;
  mailfrom_parms(arg);
  seenmail++;
  if (relayclient && localmf) {
    flagmav = localaddr(addr.s);
    if (flagmav > 0)
      if (!stralloc_append(&protocol, "M")) die_nomem();
  }
  if (!stralloc_copys(&rcptto, "")) die_nomem();
  if (!stralloc_copys(&mailfrom, addr.s)) die_nomem();
  if (!stralloc_0(&mailfrom)) die_nomem();
  if (!env_put("MAILFROM", mailfrom.s)) die_nomem();

  flagaddr = bmfcheck();
  if (flagaddr != -110)
    if (mfdnscheck) flagdnsmf = dnsq(mailfrom.s, 'M');

  out("250 ok\r\n");
}

/* this file is too long --------------------------------- Greylisting */

static int postgrey_scanner()
{
  int child;
  int wstat;

  char *postgrey_scannerarg[] = {
      "bin/qmail-postgrey", pgbind.s, mailfrom.s, addr.s, remoteip, remotehost, 0};

  switch (child = fork()) {
    case -1: return err_forkgl();
    case 0:  execv(*postgrey_scannerarg, postgrey_scannerarg); _exit(1);
  }

  wait_pid(&wstat, child);
  if (wait_crashed(wstat)) return err_postgl();

  switch (wait_exitcode(wstat)) {
    case 10: return 1;
    default: return 0;
  }
}

static void smtp_rcpt(char *arg)
{
  char *rcptok = 0;
  if (!seenmail) {
    err_wantmail();
    return;
  }
  if (!addrparse(arg)) {
    err_syntax();
    return;
  }
  rcptcount++;

  /* this file is too long --------------------------------- Split Horizon envelope checks */

  if (!relayclient) {
    if (!seenhelo && helocheck) /* Helo rejects */
      if (str_len(helocheck) == 1) {
        err_helo(
            "Reject::SNDR::Bad_Helo",
            protocol.s,
            remoteip,
            remotehost,
            helohost.s,
            mailfrom.s,
            addr.s,
            "0");
        return;
      }
    if (flagbadhelo) {
      switch (flagbadhelo) {
        case -2: badhelocond = "!"; break;
        case -1: badhelocond = "."; break;
        default: badhelocond = "*"; break;
      }
      err_helo(
          "Reject::SNDR::Bad_Helo",
          protocol.s,
          remoteip,
          remotehost,
          helohost.s,
          mailfrom.s,
          addr.s,
          badhelocond);
      return;
    }
    if (flagdnshelo > 0) {
      err_helo(
          "Reject::SNDR::DNS_Helo",
          protocol.s,
          remoteip,
          remotehost,
          helohost.s,
          mailfrom.s,
          addr.s,
          badhelocond);
      return;
    }

    if (flagdnsmf > 0) { /* Mail from rejects */
      err_mfdns(
          "Reject::ORIG::DNS_MF", protocol.s, remoteip, remotehost, helohost.s, mailfrom.s, addr.s);
      return;
    }

    if (!addrallowed(addr.s)) { /* Relaying rejects */
      err_nogateway(
          "Reject::SNDR::Invalid_Relay",
          protocol.s,
          remoteip,
          remotehost,
          helohost.s,
          mailfrom.s,
          addr.s);
      return;
    }

    if (greylist && (postgrey_scanner() == 1)) { /* Greylisting */
      postgrey(
          "Deferred::SNDR::Grey_Listed",
          protocol.s,
          remoteip,
          remotehost,
          helohost.s,
          mailfrom.s,
          addr.s);
      return;
    }

    if (tarpitcount && flagerrcpts >= tarpitcount) { /* Tarpitting et al. */
      if (tarpitdelay == 999) flagnotorious++;
      err_rcpts(
          "Reject::RCPT::Toomany_Rcptto",
          protocol.s,
          remoteip,
          remotehost,
          helohost.s,
          mailfrom.s,
          addr.s);
      return;
    }
    if (tarpitcount && rcptcount >= tarpitcount)
      if (tarpitdelay > 0 && tarpitdelay < 999) sleep(tarpitdelay);

    flagrcpt = rcptallowed(); /* Rcpt to rejects */
    if (!flagrcpt) {
      err_recipient(
          "Reject::RCPT::Failed_Rcptto",
          protocol.s,
          remoteip,
          remotehost,
          helohost.s,
          mailfrom.s,
          addr.s);
      flagerrcpts++;
      return;
    }

    if (flagspf) /* SPF rejects */
      if (spf_check(flagip6) > 0) {
        if (!stralloc_0(&spfbounce)) die_nomem();  // spfbounce is 0-terminated in any case
        err_spf(
            "Reject::SPF::Fail",
            protocol.s,
            remoteip,
            remotehost,
            helohost.s,
            mailfrom.s,
            addr.s,
            spfbounce.s);
        return;
      }
  }

  /* this file is too long --------------------------------- Local checks */

  else
  {
    if (flagmimetype == 4 || flagmimetype == 6) flagmimetype = 0;
    if (flagloadertype == 2) flagloadertype = 0;

    if (flagmav < 0) {
      err_mav(
          "Reject::ORIG::Invalid_Mailfrom",
          protocol.s,
          remoteip,
          remotehost,
          helohost.s,
          mailfrom.s,
          addr.s);
      return;
    }
    --addr.len;
    if (!stralloc_cats(&addr, relayclient)) die_nomem();
    if (!stralloc_0(&addr)) die_nomem();
  }

  /* this file is too long --------------------------------- Common checks */

  if (flagmimetype == 2 || flagmimetype == 3 || flagmimetype == 6) cdb_init(&cdbm, fdbmt);
  if (flagloadertype == 1) cdb_init(&cdbl, fdblt);

  if (flagaddr && flagaddr != -110) {
    switch (flagaddr) {
      case -1: badmailcond = "@"; break;
      case -2: badmailcond = "~"; break;
      case -3: badmailcond = "="; break;
      case -4: badmailcond = "-"; break;
      case -5: badmailcond = "+"; break;
      default: badmailcond = "*"; break;
    }
    err_bmf(
        "Reject::ORIG::Bad_Mailfrom",
        protocol.s,
        remoteip,
        remotehost,
        helohost.s,
        mailfrom.s,
        addr.s,
        badmailcond);
    return;
  }

  flagrcpt = brtcheck();
  if (flagrcpt == 110) {
    err_brt(
        "Reject::RCPT::Invalid_Rcptto",
        protocol.s,
        remoteip,
        remotehost,
        helohost.s,
        mailfrom.s,
        addr.s);
    return;
  } else if (flagrcpt > 0) {
    err_brt(
        "Reject::RCPT::Bad_Rcptto", protocol.s, remoteip, remotehost, helohost.s, mailfrom.s, addr.s);
    return;
  }

  if (flagsize) {
    err_size(
        "Reject::DATA::Invalid_Size", protocol.s, remoteip, remotehost, helohost.s, mailfrom.s, addr.s);
    return;
  }

  if (maxrcptcount && rcptcount > maxrcptcount) {
    err_rcpts(
        "Reject::RCPT::Toomany_Rcptto",
        protocol.s,
        remoteip,
        remotehost,
        helohost.s,
        mailfrom.s,
        addr.s);
    return;
  }

  /* this file is too long --------------------------------- Checks done; mailfrom/recipient accepted */

  if (!stralloc_cats(&rcptto, "T")) die_nomem();
  if (!stralloc_cats(&rcptto, addr.s)) die_nomem();
  if (!stralloc_0(&rcptto)) die_nomem();

  if (!stralloc_cats(&mailto, addr.s)) die_nomem();
  if (!stralloc_cats(&mailto, " ")) die_nomem();
  if (!stralloc_copys(&deliverto, mailto.s)) die_nomem();
  if (!stralloc_0(&deliverto)) die_nomem();
  if (!env_put("RCPTTO", deliverto.s)) die_nomem();

  /* this file is too long --------------------------------- Additional logging */

  switch (flagrcpt) {
    case 1:  rcptok = "Recipients_Cdb"; break;
    case 2:  rcptok = "Recipients_Pam"; break;
    case 3:  rcptok = "Recipients_Users"; break;
    case 4:  rcptok = "Recipients_Wild"; break;
    default: rcptok = "Rcpthosts_Rcptto"; break;
  }
  if (seenauth)
    smtp_loga(
        "Accept::AUTH::",
        protocol.s,
        remoteip,
        remotehost,
        helohost.s,
        mailfrom.s,
        addr.s,
        remoteinfo,
        authmethod.s);
  else if (flagmav > 0)
    smtp_logg(
        "Accept::ORIG::Local_Sender", protocol.s, remoteip, remotehost, helohost.s, mailfrom.s, addr.s);
  else if (relayclient)
    smtp_logg(
        "Accept::SNDR::Relay_Client", protocol.s, remoteip, remotehost, helohost.s, mailfrom.s, addr.s);
  else if (flagspf == 10)
    smtp_logr("Accept::SPF::", rcptok, protocol.s, remoteip, remotehost, helohost.s, mailfrom.s, addr.s);
  else
    smtp_logr(
        "Accept::RCPT::", rcptok, protocol.s, remoteip, remotehost, helohost.s, mailfrom.s, addr.s);

  out("250 ok\r\n");
}

struct qmail qqt;
unsigned long bytestooverflow = 0;

stralloc line = {0};
stralloc base64types = {0};
stralloc badmimetype = {0};
stralloc badloadertype = {0};

unsigned int nolines = 0;
unsigned int flagb64 = 0;  /* lineno with BASE64MESSAGE */
unsigned int flagbase = 0; /* lineno with actual base64 content */
unsigned int flagblank = 0;

static void queue_put(char *ch)
{
  int i;

  if (flagmimetype > 0 || flagloadertype > 0) {
    if (line.len <= BUFFER_SIZE)
      if (!stralloc_catb(&line, ch, 1)) die_nomem(); /* Reassamble chars to line; prepend with 'L' */

    if (*ch == '\n') {
      nolines++;
      if (line.len == 2) {
        flagblank = nolines;
        flagbase = 0;
      }

      if (*(line.s + 1) == 'C' || *(line.s + 1) == 'c')
        if (case_startb(line.s + 1, line.len - 2, BASE64MESSAGE)) flagb64 = nolines;
      if (flagb64 && nolines == flagblank + 1 && line.len > MIMETYPE_LEN + 2) flagbase = nolines;
      if (*(line.s + 1) == '-') {
        flagb64 = 0;
        flagbase = 0;
      }

      if (flagmimetype > 0 && flagbase == nolines) { /* badmimetype */
        if (!stralloc_catb(&base64types, line.s + 1, MIMETYPE_LEN)) die_nomem();
        if (!stralloc_0(&base64types)) die_nomem();

        if (flagmimetype == 2 || flagmimetype == 3 || flagmimetype == 6) {
          if (cdb_find(&cdbm, line.s + 1, MIMETYPE_LEN)) {
            cdb_free(&cdbm);
            close(fdbmt);
            if (!stralloc_copyb(&badmimetype, line.s + 1, MIMETYPE_LEN)) die_nomem();
            if (!stralloc_0(&badmimetype)) die_nomem();
            if (!stralloc_cats(&rcptto, "M")) die_nomem();
            if (!stralloc_0(&rcptto)) die_nomem();
            qmail_fail(&qqt);
            flagmimetype = -1;
          }
        }
      }

      if (flagbase && line.len > LOADER_LEN + 2) {
        if (flagloadertype >= 1 || flagmimetype >= 1) {
          for (i = 0; i < line.len - LOADER_LEN; ++i) {
            if (flagloadertype == 1 && *(line.s + i) == *badloaderinit) { /* badloadertype */
              if (cdb_find(&cdbl, line.s + i, LOADER_LEN)) {
                cdb_free(&cdbl);
                close(fdbmt);
                if (!stralloc_copyb(&badloadertype, line.s + i, LOADER_LEN)) die_nomem();
                if (!stralloc_0(&badloadertype)) die_nomem();
                if (!stralloc_cats(&rcptto, "L")) die_nomem();
                if (!stralloc_0(&rcptto)) die_nomem();
                qmail_fail(&qqt);
                flagloadertype = -1;
              }
            }
            if (flagmimetype == 1 || flagmimetype == 3 || flagmimetype == 4) {
              if (*(line.s + i) == ' ' || *(line.s + i) == '\t') { /* white spaces */
                if (!stralloc_copyb(&badmimetype, line.s + i - 2, MIMETYPE_LEN)) die_nomem();
                if (!stralloc_0(&badmimetype)) die_nomem();
                if (!stralloc_cats(&rcptto, "M")) die_nomem();
                if (!stralloc_0(&rcptto)) die_nomem();
                qmail_fail(&qqt);
                flagmimetype = -2;
              }
            }
          }
        }
      }
      line.len = 0;
      if (!stralloc_copys(&line, "L")) die_nomem();
    }
  }

  if (bytestooverflow)
    if (!--bytestooverflow) qmail_fail(&qqt);
  qmail_put(&qqt, ch, 1);
}

static void blast(int *hops)
{
  char ch;
  int state;
  int seencr;
  int flaginheader;
  int pos;        /* number of bytes since most recent \n, if fih */
  int flagmaybex; /* 1 if this line might match RECEIVED, if fih */
  int flagmaybey; /* 1 if this line might match \r\n, if fih */
  int flagmaybez; /* 1 if this line might match DELIVERED, if fih */

  state = 1;
  *hops = 0;
  flaginheader = 1;
  pos = 0;
  flagmaybex = flagmaybey = flagmaybez = 1;
  seencr = 0;
  for (;;) {
    buffer_get(&bi, &ch, 1);
    if (ch == '\n') {
      if (seencr == 0) {
        buffer_seek(&bi, -1);
        ch = '\r';
      }
    }
    if (ch == '\r')
      seencr = 1;
    else
      seencr = 0;
    if (flaginheader) {
      if (pos < 9) {
        if (ch != "delivered"[pos])
          if (ch != "DELIVERED"[pos]) flagmaybez = 0;
        if (flagmaybez)
          if (pos == 8) ++*hops;
        if (pos < 8)
          if (ch != "received"[pos])
            if (ch != "RECEIVED"[pos]) flagmaybex = 0;
        if (flagmaybex)
          if (pos == 7) ++*hops;
        if (pos < 2)
          if (ch != "\r\n"[pos]) flagmaybey = 0;
        if (flagmaybey)
          if (pos == 1) flaginheader = 0;
        ++pos;
      }
      if (ch == '\n') {
        pos = 0;
        flagmaybex = flagmaybey = flagmaybez = 1;
      }
    }
    switch (state) {
      case 0:
        if (ch == '\n') straynewline();
        if (ch == '\r') {
          state = 4;
          continue;
        }
        break;
      case 1: /* \r\n */
        if (ch == '\n') straynewline();
        if (ch == '.') {
          state = 2;
          continue;
        }
        if (ch == '\r') {
          state = 4;
          continue;
        }
        state = 0;
        break;
      case 2: /* \r\n + . */
        if (ch == '\n') straynewline();
        if (ch == '\r') {
          state = 3;
          continue;
        }
        state = 0;
        break;
      case 3: /* \r\n + .\r */
        if (ch == '\n') return;
        queue_put(".");
        queue_put("\r");
        if (ch == '\r') {
          state = 4;
          continue;
        }
        state = 0;
        break;
      case 4: /* + \r */
        if (ch == '\n') {
          state = 1;
          break;
        }
        if (ch != '\r') {
          queue_put("\r");
          state = 0;
        }
    }
    queue_put(&ch);
  }
}

char accept_buf[FMT_ULONG];

static void acceptmessage(unsigned long qp)
{
  datetime_sec when;
  when = now();
  out("250 ok ");
  accept_buf[fmt_ulong(accept_buf, (unsigned long)when)] = 0;
  out(accept_buf);
  out(" qp ");
  accept_buf[fmt_ulong(accept_buf, qp)] = 0;
  out(accept_buf);
  out("\r\n");
}

static void smtp_data()
{
  int hops;
  unsigned long qp;
  char *qqx;

  if (!seenmail) {
    err_wantmail();
    return;
  }
  if (!rcptto.len) {
    err_wantrcpt();
    return;
  }
  if (flagnotorious) {
    err_notorious();
  }
  seenmail = 0;
  if (databytes) bytestooverflow = databytes + 1;

  if (!stralloc_copys(&addr, "")) die_nomem();
  if (!stralloc_cats(&addr, rcptto.s + 1)) die_nomem();
  if (!stralloc_0(&addr)) die_nomem();

  if (qmail_open(&qqt) == -1) {
    err_qqt();
    return;
  }
  qp = qmail_qp(&qqt);

  out("354 go ahead\r\n");

  if (flagspf && !relayclient) spfheader(&qqt, spfinfo.s, local, remoteip, helohost.s, mailfrom.s);
  received(&qqt, protocol.s, local, remoteip, remotehost, remoteinfo, fakehelo, tlsinfo.s, rblinfo.s);
  blast(&hops);
  hops = (hops >= MAXHOPS);
  if (hops) qmail_fail(&qqt);
  if (base64 && base64types.len == 0) {
    if (!stralloc_cats(&rcptto, "Q")) die_nomem();
    if (!stralloc_0(&rcptto)) die_nomem();
  }
  qmail_from(&qqt, mailfrom.s);
  qmail_put(&qqt, rcptto.s, rcptto.len);

  qqx = qmail_close(&qqt);
  if (!*qqx) {
    acceptmessage(qp);
    return;
  }
  if (hops) {
    out("554 too many hops, this message is looping (#5.4.6)\r\n");
    return;
  }
  if (databytes)
    if (!bytestooverflow) {
      err_size(
          "Reject::DATA::Invalid_Size",
          protocol.s,
          remoteip,
          remotehost,
          helohost.s,
          mailfrom.s,
          addr.s);
      return;
    }
  if (flagmimetype < 0) {
    err_data(
        "Reject::DATA::Bad_MIME",
        protocol.s,
        remoteip,
        remotehost,
        helohost.s,
        mailfrom.s,
        addr.s,
        badmimetype.s);
    return;
  }
  if (flagloadertype < 0) {
    err_data(
        "Reject::DATA::Bad_Loader",
        protocol.s,
        remoteip,
        remotehost,
        helohost.s,
        mailfrom.s,
        addr.s,
        badloadertype.s);
    return;
  }
  if (*qqx == 'I') {
    err_data(
        "Reject::DKIM::Signature",
        protocol.s,
        remoteip,
        remotehost,
        helohost.s,
        mailfrom.s,
        addr.s,
        "fail");
    return;
  }
  if (*qqx == 'S') {
    err_data(
        "Reject::DATA::Spam_Message",
        protocol.s,
        remoteip,
        remotehost,
        helohost.s,
        mailfrom.s,
        addr.s,
        "spam");
    return;
  }
  if (*qqx == 'A') {
    err_data(
        "Reject::DATA::MIME_Attach",
        protocol.s,
        remoteip,
        remotehost,
        helohost.s,
        mailfrom.s,
        addr.s,
        "MIME");
    return;
  }
  if (*qqx == 'V') {
    if (qhpsi)
      err_data(
          "Reject::DATA::Virus_Infected",
          protocol.s,
          remoteip,
          remotehost,
          helohost.s,
          mailfrom.s,
          addr.s,
          qhpsi);
    else
      err_data(
          "Reject::DATA::Virus_Infected",
          protocol.s,
          remoteip,
          remotehost,
          helohost.s,
          mailfrom.s,
          addr.s,
          "AV scanner");
    return;
  }
  if (*qqx == 'D')
    out("554 ");
  else
    out("451 ");
  out(qqx + 1);
  out("\r\n");
}

/* this file is too long --------------------------------- SMTP Auth */

char unique[FMT_ULONG + FMT_ULONG + 3];
static stralloc authin = {0}; /* input from SMTP client */
static stralloc pass = {0};   /* plain passwd or digest */
static stralloc resp = {0};   /* b64 response */
static stralloc chal = {0};   /* CRAM-MD5 plain challenge */
static stralloc slop = {0};   /* CRAM-MD5 b64 challenge */

char **childargs;
char authbuf[512];
buffer ba = BUFFER_INIT(safewrite, FDAUTH, authbuf, sizeof(authbuf));

static int authgetl()
{
  int i;

  if (!stralloc_copys(&authin, "")) die_nomem();
  for (;;) {
    if (!stralloc_readyplus(&authin, 1)) die_nomem(); /* XXX */
    i = buffer_get(&bi, authin.s + authin.len, 1);
    if (i != 1) die_read();
    if (authin.s[authin.len] == '\n') break;
    ++authin.len;
  }

  if (authin.len > 0)
    if (authin.s[authin.len - 1] == '\r') --authin.len;
  authin.s[authin.len] = 0;
  if (*authin.s == '*' && *(authin.s + 1) == 0) return err_authabort();
  if (authin.len == 0) return err_authinput();
  return authin.len;
}

static int authenticate()
{
  int child;
  int wstat;
  int pi[2];

  if (!stralloc_0(&user)) die_nomem();
  if (!stralloc_0(&pass)) die_nomem();
  if (!stralloc_0(&chal)) die_nomem();
  if (!env_put("AUTHUSER", user.s)) die_nomem();

  if (pipe(pi) == -1) return err_pipe();
  switch (child = fork()) {
    case -1: return err_fork();
    case 0:
      close(pi[1]);
      if (fd_copy(FDAUTH, pi[0]) == -1) return err_pipe();
      sig_pipedefault();
      execvp(*childargs, childargs);
      _exit(1);
  }
  close(pi[0]);

  buffer_init(&ba, write, pi[1], authbuf, sizeof(authbuf));
  if (buffer_put(&ba, user.s, user.len) == -1) return err_write();
  if (buffer_put(&ba, pass.s, pass.len) == -1) return err_write();
  if (smtpauth == 2 || smtpauth == 3 || smtpauth == 12 || smtpauth == 13)
    if (buffer_put(&ba, chal.s, chal.len) == -1) return err_write();
  if (buffer_flush(&ba) == -1) return err_write();

  close(pi[1]);
  if (!stralloc_copys(&chal, "")) die_nomem();
  if (!stralloc_copys(&slop, "")) die_nomem();
  byte_zero(authbuf, sizeof(authbuf));
  if (wait_pid(&wstat, child) == -1) return err_child();
  if (wait_crashed(wstat)) return err_child();
  if (wait_exitcode(wstat)) {
    sleep(AUTHSLEEP);
    return 1;
  } /* no */
  return 0; /* yes */
}

static int auth_login(char *arg)
{
  int r;
  if (smtpauth == 2 || smtpauth == 12) return 1; /* only login/plain */

  if (*arg) {
    if ((r = b64decode((unsigned char *)arg, str_len(arg), &user)) == 1) return err_authinput();
  } else {
    out("334 VXNlcm5hbWU6\r\n");
    flush(); /* Username: */
    if (authgetl() < 0) return -1;
    if ((r = b64decode((unsigned char *)authin.s, authin.len, &user)) == 1) return err_authinput();
  }
  if (r == -1) die_nomem();

  out("334 UGFzc3dvcmQ6\r\n");
  flush(); /* Password: */

  if (authgetl() < 0) return -1;
  if ((r = b64decode((unsigned char *)authin.s, authin.len, &pass)) == 1) return err_authinput();
  if (r == -1) die_nomem();

  if (!user.len || !pass.len) return err_authinput();
  return authenticate();
}

static int auth_plain(char *arg)
{
  int r, id = 0;
  if (smtpauth == 2 || smtpauth == 12) return 1; /* only login/plain */

  if (*arg) {
    if ((r = b64decode((unsigned char *)arg, str_len(arg), &resp)) == 1) return err_authinput();
  } else {
    out("334 \r\n");
    flush();
    if (authgetl() < 0) return -1;
    if ((r = b64decode((unsigned char *)authin.s, authin.len, &resp)) == 1) return err_authinput();
  }
  if (r == -1 || !stralloc_0(&resp)) die_nomem();
  while (resp.s[id]) id++; /* "authorize-id\0userid\0passwd\0" */

  if (resp.len > id + 1)
    if (!stralloc_copys(&user, resp.s + id + 1)) die_nomem();
  if (resp.len > id + user.len + 2)
    if (!stralloc_copys(&pass, resp.s + id + user.len + 2)) die_nomem();

  if (!user.len || !pass.len) return err_authinput();
  return authenticate();
}

static int auth_cram()
{
  int i, r;
  char *s;
  if (smtpauth == 1 || smtpauth == 11) return 1; /* no challenge if login/plain */

  s = unique; /* generate challenge */
  s += fmt_uint(s, getpid());
  *s++ = '.';
  s += fmt_ulong(s, (unsigned long)now());
  *s++ = '@';
  *s++ = 0;
  if (!stralloc_copys(&chal, "<")) die_nomem();
  if (!stralloc_cats(&chal, unique)) die_nomem();
  if (!stralloc_cats(&chal, local)) die_nomem();
  if (!stralloc_cats(&chal, ">")) die_nomem();
  if (b64encode(&chal, &slop) < 0) die_nomem();
  if (!stralloc_0(&slop)) die_nomem();

  out("334 "); /* "334 base64_challenge \r\n" */
  out(slop.s);
  out("\r\n");
  flush();

  if (authgetl() < 0) return -1; /* got response */
  if ((r = b64decode((unsigned char *)authin.s, authin.len, &resp)) == 1) return err_authinput();
  if (r == -1 || !stralloc_0(&resp)) die_nomem();

  i = str_rchr(resp.s, ' ');
  s = resp.s + i;
  while (*s == ' ') ++s;
  resp.s[i] = 0;
  if (!stralloc_copys(&user, resp.s)) die_nomem(); /* userid */
  if (!stralloc_copys(&pass, s)) die_nomem();      /* digest */

  if (!user.len || !pass.len) return err_authinput();
  return authenticate();
}

static int err_noauth_handler(char *s)
{
  (void)s;
  return err_noauth();
}

struct authcmd {
  char *text;
  int (*fun)(char *);
} authcmds[] = {
    {   "login",         auth_login},
    {   "plain",         auth_plain},
    {"cram-md5",          auth_cram},
    {         0, err_noauth_handler}
};

static void smtp_auth(char *arg)
{
  int i;
  char *cmd = arg;

  /* prevent users to expose userid + password over unencrypted connection */

  if ((starttls > 1) && !seentls) {
    if (!stralloc_append(&protocol, "A")) die_nomem();
    if (!stralloc_0(&protocol)) die_nomem();
    err_authsetup("Reject::TLS::required", protocol.s, remoteip, remotehost, helohost.s);
    return;
  }

  if ((starttls > 1) && !seenhelo) {
    if (!stralloc_append(&protocol, "A")) die_nomem();
    if (!stralloc_0(&protocol)) die_nomem();
    err_tlsreq("Reject::AUTH::invalid", protocol.s, remoteip, remotehost, helohost.s);
    return;
  }

  if (!smtpauth) {
    out("503 auth not available (#5.3.3)\r\n");
    flush();
    _exit(0);
  }
  if (smtpauth && !*childargs) {
    err_authsetup("Reject::AUTH::invalid", protocol.s, remoteip, remotehost, helohost.s);
    flush();
    _exit(1);
  }
  if (seenauth) {
    err_authd();
    return;
  }
  if (seenmail) {
    err_authmail();
    return;
  }

  if (!stralloc_copys(&user, "")) die_nomem();
  if (!stralloc_copys(&pass, "")) die_nomem();
  if (!stralloc_copys(&resp, "")) die_nomem();
  if (!stralloc_copys(&chal, "")) die_nomem(); /* only needed for CRAM-MD5 */

  i = str_chr(cmd, ' '); /* get AUTH type */
  arg = cmd + i;
  while (*arg == ' ') ++arg;
  cmd[i] = 0;

  for (i = 0; authcmds[i].text; ++i)
    if (case_equals(authcmds[i].text, cmd)) break;

  if (!authcmds[i].text) { /* invalid auth cmd */
    if (!stralloc_append(&protocol, "A")) die_nomem();
    if (!stralloc_0(&protocol)) die_nomem();
    err_authinvalid("Reject::AUTH::Method", protocol.s, remoteip, remotehost, helohost.s);
    return;
  }

  if (!stralloc_copys(&authmethod, authcmds[i].text)) die_nomem();
  if (!stralloc_0(&authmethod)) die_nomem();

  switch (authcmds[i].fun(arg)) {
    case 0:
      seenauth = 1;
      relayclient = "";
      remoteinfo = user.s;
      auth_info(authmethod.s);
      out("235 ok, go ahead (#2.0.0)\r\n");
      break;
    case 1:
      if (!stralloc_append(&protocol, "A")) die_nomem();
      if (!stralloc_0(&protocol)) die_nomem();
      err_authfail("Reject::AUTH::", protocol.s, remoteip, remotehost, helohost.s, user.s, authmethod.s);
      return;
  }
}

/* this file is too long --------------------------------- GO ON */

static void err_noop_handler(char *s)
{
  (void)s;
  err_noop();
}

static void err_vrfy_handler(char *s)
{
  (void)s;
  err_vrfy();
}

static void err_unimpl_handler(char *s)
{
  (void)s;
  err_unimpl();
}

struct commands smtpcommands[] = {
    {    "rcpt",          smtp_rcpt,     0},
    {    "mail",          smtp_mail,     0},
    {    "data",          smtp_data, flush},
    {    "auth",          smtp_auth, flush},
    {    "quit",          smtp_quit, flush},
    {    "helo",          smtp_helo, flush},
    {    "ehlo",          smtp_ehlo, flush},
    {    "rset",          smtp_rset, flush},
    {    "help",          smtp_help, flush},
    {    "noop",   err_noop_handler, flush},
    {    "vrfy",   err_vrfy_handler, flush},
    {"starttls",      smtp_starttls, flush},
    {         0, err_unimpl_handler, flush}
};

int main(int argc, char **argv)
{
  childargs = argv + 1;
  sig_pipeignore();
  if (chdir(auto_qmail) == -1) die_control();
  setup();
  smtpdlog_init();
  if (ipme_init() != 1) die_ipme();
  smtp_greet("220 ");
  out(" ESMTP\r\n");
  flush();
  if (commands(&bi, smtpcommands) == 0) die_read();
  die_nomem();

  return 0;
}