/*
 * main_tlswrapper_smtp.c - SMTP front-end with optional STARTTLS and greylist
 *
 * Implements the SMTP-speaking wrapper process used in front of a child
 * program. The wrapper proxies SMTP commands and replies, tracks the current
 * mail transaction for logging, optionally consults a greylist policy server,
 * and can advertise and switch to STARTTLS when a TLS control channel is
 * available on file descriptor 5.
 *
 * The process can drop privileges, enter the runtime jail, and run the child
 * service under a separate user before the SMTP session starts.
 */

#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <signal.h>
#include <unistd.h>
#include "str.h"
#include "randombytes.h"
#include "log.h"
#include "iptostr.h"
#include "connectioninfo.h"
#include "jail.h"
#include "writeall.h"
#include "buffer.h"
#include "stralloc.h"
#include "open.h"
#include "e.h"
#include "tls.h"
#include "fd.h"
#include "resolvehost.h"
#include "hostport.h"
#include "conn.h"
#include "case.h"
#include "timeoutwrite.h"
#include "timeoutread.h"
#include "parsenum.h"
#include "alloc.h"
#include "main.h"

/* clang-format off */

static const char *jailaccount = 0;
static const char *jaildir = EMPTYDIR;
static const char *user = 0;

static int flagverbose = 1;
static int flagstarttls = 0;
static int flaggreyfailclosed = 0;

static int fromchild[2] = {-1, -1};
static int tochild[2] = {-1, -1};
static pid_t child = -1;
static int status;

static unsigned char localip[16] = {0};
static unsigned char localport[2] = {0};
static unsigned char remoteip[16] = {0};
static unsigned char remoteport[2] = {0};
static char remoteipstr[IPTOSTR_LEN] = {0};

/*
 * cleanup - overwrite connection state and temporary stack storage
 *
 * Randomizes sensitive process state before exit so connection metadata does
 * not remain in memory longer than necessary.
 */
static void cleanup(void) {
    randombytes(localip, sizeof localip);
    randombytes(localport, sizeof localport);
    randombytes(remoteip, sizeof remoteip);
    randombytes(remoteport, sizeof remoteport);
    alloc_freeall();
    {
        unsigned char stack[4096];
        randombytes(stack, sizeof stack);
    }
}

/*
 * die - terminate the wrapper and reap the child when needed
 *
 * @x: process exit status
 *
 * Cleans up local state, optionally waits for the SMTP child, and mirrors the
 * child's exit status on clean shutdown.
 */
static void __attribute__((noreturn)) die(int x) {

    int r;

    /* cleanup */
    cleanup();
    if (child == -1) _exit(x);
    if (x != 0) _exit(x);

    /* wait for child */
    close(fromchild[0]);
    close(tochild[1]);
    do {
        r = waitpid(child, &status, 0);
    } while (r == -1 && errno == EINTR);
    errno = 0;
    if (!WIFEXITED(status)) {
        log_f2("child process killed by signal ", log_num(WTERMSIG(status)));
        _exit(111);
    }
    log_d2("child exited with status ", log_num(WEXITSTATUS(status)));
    _exit(WEXITSTATUS(status));
}

#define die_jail()                                                             \
    do {                                                                       \
        log_f1("unable to create jail");                                       \
        die(111);                                                              \
    } while (0)
#define die_pipe()                                                             \
    do {                                                                       \
        log_f1("unable to create pipe");                                       \
        die(111);                                                              \
    } while (0)
#define die_fork()                                                             \
    do {                                                                       \
        log_f1("unable to fork");                                              \
        die(111);                                                              \
    } while (0)
#define die_dup()                                                              \
    do {                                                                       \
        log_f1("unable to dup");                                               \
        die(111);                                                              \
    } while (0)
#define die_nomem()                                                            \
    do {                                                                       \
        log_f1("unable to allocate memory");                                   \
        die(111);                                                              \
    } while (0)
#define die_droppriv(x)                                                        \
    do {                                                                       \
        log_f3("unable to drop privileges to '", (x), "'");                   \
        die(111);                                                              \
    } while (0)

/*
 * usage - print command usage and exit
 */
static void __attribute__((noreturn)) usage(void) {
    log_u1("tlswrapper-smtp [options] child");
    die(100);
}

static const char *timeoutstr = "600";
static const char *crwtimeoutstr = "15";
static long long timeout;
static long long crwtimeout;

#define SMTP_MAX_LINE 4096
#define SMTP_MAX_MAILFROM 2048
#define SMTP_MAX_RCPTTO 2048
#define SMTP_MAX_RCPTS 256
#define SMTP_MAX_RCPTTODATA 65536

/*
 * timeout_parse - parse and validate a timeout value in seconds
 *
 * @x: decimal timeout string
 *
 * Returns the parsed timeout. Invalid values terminate the process with a
 * usage error.
 */
static long long timeout_parse(const char *x) {
    long long ret;
    if (!parsenum(&ret, 1, 86400, x)) {
        log_f3("unable to parse timeout from the string '", x, "', timeout must be a number in the range <1,86400>");
        die(100);
    }
    return ret;
}

/*
 * signalhandler - abort on session timeout or fatal signal delivery
 *
 * @signum: delivered signal number, unused
 */
static void signalhandler(int signum) {
    (void) signum;
    /* die(111); */
    _exit(111);

}

/*
 * _write - write with the configured client read/write timeout
 *
 * @fd: destination file descriptor
 * @xv: source buffer
 * @xlen: number of bytes to write
 *
 * Returns the number of bytes written. Failures terminate the session.
 */
static long long _write(int fd, void *xv, long long xlen) {
    long long w = timeoutwrite(crwtimeout, fd, xv, xlen);
    if (w <= 0) {
        log_d1("write failed");
        die(111);
    }
    return w;
}

/*
 * _read - read with the configured client read/write timeout
 *
 * @fd: source file descriptor
 * @xv: destination buffer
 * @xlen: number of bytes to read
 *
 * Returns the number of bytes read. Failures terminate the session.
 */
static long long _read(int fd, void *xv, long long xlen) {

    long long r = timeoutread(crwtimeout, fd, xv, xlen);
    if (r <= 0) {
        log_d1("read failed");
        die(111);
    }
    return r;
}

static char inbuf[8192];
static buffer ssin = buffer_INIT(_read, 0, inbuf, sizeof inbuf);
static char outbuf[128];
static buffer ssout = buffer_INIT(_write, 1, outbuf, sizeof outbuf);
static char outbuf5[32];
static buffer ssout5 = buffer_INIT(_write, 5, outbuf5, sizeof outbuf5);
static char cinbuf[128];
static buffer sscin;
static char coutbuf[8192];
static buffer sscout;

static stralloc line = {0};
static stralloc cline = {0};

static stralloc mailfrom = {0};
static stralloc rcptto = {0};
static stralloc rcpttodata = {0};
static unsigned int rcptcount = 0;

/*
 * _catlogid - append the current log id in bracket form
 *
 * @sa: destination string accumulator
 *
 * Returns 1 on success and 0 on allocation failure.
 */
static int _catlogid(stralloc *sa) {

    if (log_get_id()) {
        if (!stralloc_cats(sa, " [")) return 0;
        if (!stralloc_cats(sa, log_get_id())) return 0;
        if (!stralloc_cats(sa, "]")) return 0;
    }
    return 1;
}

/*
 * stralloc_reset0 - reset a string accumulator to an empty string
 *
 * @sa: string accumulator to reset
 */
static void stralloc_reset0(stralloc *sa) {
    if (!stralloc_copys(sa, "")) die_nomem();
    if (!stralloc_0(sa)) die_nomem();
    --sa->len;
}

/*
 * smtp_reset_transaction - clear per-message SMTP transaction state
 *
 * Resets MAIL FROM, RCPT TO aggregation, and recipient counters after RSET,
 * QUIT, or completion of DATA.
 */
static void smtp_reset_transaction(void) {
    stralloc_reset0(&mailfrom);
    stralloc_reset0(&rcptto);
    stralloc_reset0(&rcpttodata);
    rcptcount = 0;
}

/*
 * smtp_line_append - append one byte while enforcing a line length limit
 *
 * @sa: destination string accumulator
 * @ch: byte to append
 * @what: human-readable item name for error logging
 */
static void smtp_line_append(stralloc *sa, char ch, const char *what) {
    if (sa->len >= SMTP_MAX_LINE) {
        log_d2(what, " exceeds configured limit");
        die(111);
    }
    if (!stralloc_append(sa, &ch)) die_nomem();
}


static stralloc greylistresp = {0};
static const char *greylistmsg = 0;
static char *greylisthostport = 0;
static char greylisthost[256];
static unsigned char greylistport[2];
static unsigned char greylistip[16];
static long long greylistiplen;
static int greylistfd = -1;
static char greylistbuf[256];
static buffer gssin;

/*
 * _gwrite - write to the greylist service with timeout handling
 *
 * @fd: greylist socket
 * @xv: source buffer
 * @xlen: number of bytes to write
 *
 * In fail-closed mode, errors terminate the process. Otherwise they are
 * logged and returned to the caller.
 */
static long long _gwrite(int fd, void *xv, long long xlen) {
    long long w = timeoutwrite(crwtimeout, fd, xv, xlen);
    if (w <= 0) {
        if (flaggreyfailclosed) {
            log_f3("write to '", greylisthostport, "' failed");
            die(111);
        }
        else {
            log_w3("write to '", greylisthostport, "' failed");
        }
    }
    return w;
}

/*
 * _gread - read from the greylist service with timeout handling
 *
 * @fd: greylist socket
 * @xv: destination buffer
 * @xlen: number of bytes to read
 *
 * In fail-closed mode, errors terminate the process. Otherwise they are
 * logged and returned to the caller.
 */
static long long _gread(int fd, void *xv, long long xlen) {

    long long r = timeoutread(crwtimeout, fd, xv, xlen);
    if (r <= 0) {
        if (flaggreyfailclosed) {
            log_f3("read from '", greylisthostport, "' failed");
            die(111);
        }
        else {
            log_w3("read from '", greylisthostport, "' failed");
        }
    }
    return r;
}

/*
 * greylist - query the policy service for the current sender and recipient
 *
 * Sends a postfix-style access policy request to the configured greylist
 * server and returns a static SMTP reply string when the recipient should be
 * deferred or rejected. A null result means the command should continue to
 * the child SMTP server.
 */
static const char *greylist(void) {

    char ch;

    if (!stralloc_copys(&greylistresp, "")) die_nomem();

    buffer_init(&gssin, _gwrite, greylistfd, greylistbuf, sizeof greylistbuf);

    if (buffer_puts(&gssin, "request=smtpd_access_policy\n") == -1) return 0;
    if (buffer_puts(&gssin, "client_address=") == -1) return 0;
    if (buffer_puts(&gssin, remoteipstr) == -1) return 0;
    if (buffer_puts(&gssin, "\nsender=") == -1) return 0;
    if (buffer_puts(&gssin, mailfrom.s) == -1) return 0;
    if (buffer_puts(&gssin, "\nrecipient=") == -1) return 0;
    if (buffer_puts(&gssin, rcptto.s) == -1) return 0;
    if (buffer_puts(&gssin, "\n\n") == -1) return 0;
    if (buffer_flush(&gssin)== -1) return 0;

    buffer_init(&gssin, _gread, greylistfd, greylistbuf, sizeof greylistbuf);

    do {
        if (buffer_get(&gssin, &ch, 1) != 1) return 0;
        if (!stralloc_append(&greylistresp, &ch)) die_nomem();
    } while (ch != '\n');
    if (!stralloc_0(&greylistresp)) die_nomem();
    log_t2("greylist: ", greylistresp.s);
    if (greylistresp.len >= 12) {
        if (!case_diffb(greylistresp.s, 12, "action=dunno")) {
            return 0;
        }
    }
    if (greylistresp.len >= 13) {
        if (!case_diffb(greylistresp.s, 13, "action=reject")) {
            return "553 bad reputation (#5.7.1)";
        }
    }
    return "450 greylisted (#4.3.0)";
}

/*
 * smtpline - read one SMTP reply from the child process
 *
 * @append: optional extra reply line appended to successful EHLO replies
 * @addlogid: non-zero to append the current log id to the emitted reply
 *
 * Reads one SMTP reply, including multiline replies, into cline and returns
 * the numeric reply code.
 */
static long long smtpline(const char *append, int addlogid) {

    unsigned char ch;
    unsigned long long code;

    if (!stralloc_copys(&cline, "")) die_nomem();

    buffer_GETC(&sscin, (char *)&ch); code = ch - '0';
    smtp_line_append(&cline, ch, "child SMTP reply");
    buffer_GETC(&sscin, (char *)&ch); code = code * 10 + (ch - '0');
    smtp_line_append(&cline, ch, "child SMTP reply");
    buffer_GETC(&sscin, (char *)&ch); code = code * 10 + (ch - '0');
    smtp_line_append(&cline, ch, "child SMTP reply");

    for (;;) {
        buffer_GETC(&sscin, (char *)&ch);
        if (append && code == 250 && ch == ' ') {
            smtp_line_append(&cline, '-', "child SMTP reply");
        }
        else {
            smtp_line_append(&cline, ch, "child SMTP reply");
        }
        if (ch != '-') break;
        while (ch != '\n') {
            buffer_GETC(&sscin, (char *)&ch);
            smtp_line_append(&cline, ch, "child SMTP reply");
        }
        buffer_GETC(&sscin, (char *)&ch);
        smtp_line_append(&cline, ch, "child SMTP reply");
        buffer_GETC(&sscin, (char *)&ch);
        smtp_line_append(&cline, ch, "child SMTP reply");
        buffer_GETC(&sscin, (char *)&ch);
        smtp_line_append(&cline, ch, "child SMTP reply");
    }
    while (ch != '\n') {
        buffer_GETC(&sscin, (char *)&ch);
        smtp_line_append(&cline, ch, "child SMTP reply");
    }
    if (append && code == 250) {
        if (cline.len + (long long) str_len(append) > SMTP_MAX_LINE) {
            log_d1("child SMTP reply exceeds configured limit");
            die(111);
        }
        if (!stralloc_cats(&cline, append)) die_nomem();
    }
    if (!stralloc_0(&cline)) die_nomem();
    --cline.len;
    log_t3("child line: '", cline.s, "'");

    /* add logid */
    if (addlogid && log_get_id()) {
        if (cline.len > 0) if (cline.s[cline.len - 1] == '\n') --cline.len;
        if (cline.len > 0) if (cline.s[cline.len - 1] == '\r') --cline.len;
        if (!_catlogid(&cline)) die_nomem();
        if (!stralloc_cats(&cline, "\r\n")) die_nomem();
        if (!stralloc_0(&cline)) die_nomem();
        --cline.len;
        log_t3("child line with logid: '", cline.s, "'");
    }
    return code;
}
/*
 * readline - read one SMTP command line from the client
 *
 * Normalizes NUL bytes to LF, enforces the configured line limit, and stores
 * the result in line terminated with CRLF.
 */
static void readline(void) {

    if (!stralloc_copys(&line, "")) die_nomem();

    for (;;) {
      char ch;
      buffer_GETC(&ssin, &ch);
      if (ch == '\n') break;
      if (!ch) ch = '\n';
      smtp_line_append(&line, ch, "SMTP command line");
    }
    if (line.len > 0) if (line.s[line.len - 1] == '\r') --line.len;
    if (!stralloc_cats(&line, "\r\n")) die_nomem();
    if (!stralloc_0(&line)) die_nomem();
    --line.len;
    log_t3("line: '", line.s, "'");
}

struct commands {
    const char *verb;
    void (*action)(void);
};

/*
 * commands - dispatch SMTP commands in a loop
 *
 * @c: command table terminated by a null verb entry
 */
static void commands(struct commands *c) {

    long long i, len;

    for (;;) {
        readline();

        for (len = 0; len < line.len; ++len) {
            if (line.s[len] == ' ') break;
            if (line.s[len] == '\r') break;
            if (line.s[len] == '\n') break;
        }

        for (i = 0; c[i].verb; ++i) {
            if (!case_diffb(line.s, len, c[i].verb)) break;
        }
        c[i].action();
    }
}

/*
 * smtp_greet - forward the child's initial greeting to the client
 */
static void smtp_greet(void) {

    smtpline(0, 1);
    buffer_putsflush(&ssout, cline.s);
}

/*
 * copy - proxy one client command to the child and one reply back
 *
 * @logid: non-zero to append the current log id to the returned reply
 *
 * Returns the child's numeric SMTP status code.
 */
static long long copy(int logid) {

    long long code;

    buffer_putsflush(&sscout, line.s);
    code = smtpline(0, logid);
    buffer_putsflush(&ssout, cline.s);
    log_d3(line.s, ": ", cline.s);
    return code;
}

/*
 * smtp_default - pass unhandled commands through unchanged
 */
static void smtp_default(void) {
    copy(0);
}

/*
 * smtp_quit - proxy QUIT and terminate the wrapper cleanly
 */
static void smtp_quit(void) {
    copy(1);
    die(0);
}

/*
 * smtp_data - proxy DATA and log the final transaction result
 *
 * Streams the message body to the child until the terminating dot line and
 * resets the tracked transaction state after the final reply.
 */
static void smtp_data(void) {

    long long code;

    buffer_putsflush(&sscout, line.s);
    code = smtpline(0, 1);
    buffer_putsflush(&ssout, cline.s);
    if (code != 354) {
        log_d3(line.s, ": ", cline.s);
        log_w6("F=", mailfrom.s, " T=", rcpttodata.s, ": ", cline.s);
        return;
    }

    for (;;) {
        readline();
        buffer_puts(&sscout, line.s);
        if (line.len > 0) if (line.s[line.len - 1] == '\n') --line.len;
        if (line.len > 0) if (line.s[line.len - 1] == '\r') --line.len;
        if ((line.len == 1) && line.s[0] == '.') break;
    }
    buffer_flush(&sscout);
    code = smtpline(0, 1);
    buffer_putsflush(&ssout, cline.s);
    log_d2("DATA: ", cline.s);
    if (code != 250) {
        log_w6("F=", mailfrom.s, " T=", rcpttodata.s, ": ", cline.s);
    }
    else {
        log_i6("F=", mailfrom.s, " T=", rcpttodata.s, ": ", cline.s);
    }
    smtp_reset_transaction();
}

/*
 * smtp_mail - process MAIL FROM and start a new transaction context
 *
 * Extracts the envelope sender for logging and greylist queries before the
 * command is proxied to the child.
 */
static void smtp_mail(void) {

    long long code, i;

    smtp_reset_transaction();
    if (line.len >= 10) {
        if (!case_diffb(line.s, 10, "mail from:")) {
            if (!stralloc_copyb(&mailfrom, line.s + 10, line.len - 10)) die_nomem();
            if (mailfrom.s[mailfrom.len - 1] == '\n') --mailfrom.len;
            if (mailfrom.s[mailfrom.len - 1] == '\r') --mailfrom.len;
            for (i = 0; i < mailfrom.len; ++i) {
                if (mailfrom.s[i] == ' ') mailfrom.len = i;
            }
            if (mailfrom.len > SMTP_MAX_MAILFROM) {
                log_d1("MAIL FROM exceeds configured limit");
                die(111);
            }
            if (!stralloc_0(&mailfrom)) die_nomem();
        }
    }

    code = copy(1);
    if (code != 250) {
        log_w4("F=", mailfrom.s, ": ", cline.s);
    }
}

/*
 * smtp_rcpt - process RCPT TO with optional greylist enforcement
 *
 * Tracks recipients for logging, enforces per-transaction limits, and may
 * short-circuit the command with a greylist reply before contacting the child.
 */
static void smtp_rcpt(void) {

    long long code, i;

    if (line.len >= 8) {
        if (!case_diffb(line.s, 8, "rcpt to:")) {
            if (!stralloc_copyb(&rcptto, line.s + 8, line.len - 8)) die_nomem();
            if (rcptto.s[rcptto.len - 1] == '\n') --rcptto.len;
            if (rcptto.s[rcptto.len - 1] == '\r') --rcptto.len;
            for (i = 0; i < rcptto.len; ++i) {
                if (rcptto.s[i] == ' ') {
                    rcptto.len = i;
                    break;
                }
            }
            if (rcptto.len > SMTP_MAX_RCPTTO) {
                log_d1("RCPT TO exceeds configured limit");
                die(111);
            }
            if (rcptcount >= SMTP_MAX_RCPTS) {
                log_d1("too many recipients in a single transaction");
                die(111);
            }
            if (rcpttodata.len + rcptto.len + 1 > SMTP_MAX_RCPTTODATA) {
                log_d1("recipient list exceeds configured limit");
                die(111);
            }
            if (!stralloc_cat(&rcpttodata, &rcptto)) die_nomem();
            if (!stralloc_cats(&rcpttodata, ",")) die_nomem();
            if (!stralloc_0(&rcptto)) die_nomem();
            if (!stralloc_0(&rcpttodata)) die_nomem();
            --rcpttodata.len;
            ++rcptcount;
        }
    }

    if (mailfrom.len && rcptto.len && greylistfd != -1) {
        greylistmsg = greylist();
        if (greylistmsg) {
            if (!stralloc_copys(&cline, greylistmsg)) die_nomem();
            if (!_catlogid(&cline)) die_nomem();
            if (!stralloc_cats(&cline, "\r\n")) die_nomem();
            if (!stralloc_0(&cline)) die_nomem();
            buffer_putsflush(&ssout, cline.s);
            log_d3(line.s, ": ", cline.s);
            log_w6("F=", mailfrom.s, " T=", rcptto.s, ": ", cline.s);
            return;
        }
    }

    code = copy(1);
    if (code != 250) {
        log_w6("F=", mailfrom.s, " T=", rcptto.s, ": ", cline.s);
    }
}

/*
 * smtp_ehlo - proxy EHLO and advertise STARTTLS when available
 */
static void smtp_ehlo(void) {

    struct stat st;

    buffer_putsflush(&sscout, line.s);
    if (flagstarttls && fstat(5, &st) == 0) {
        (void) smtpline("250 STARTTLS\r\n", 0);
    }
    else {
        (void) smtpline(0, 0);
    }
    errno = 0;
    buffer_putsflush(&ssout, cline.s);
    log_d3(line.s, ": ", cline.s);
}

/*
 * smtp_starttls - switch the client side to TLS when supported
 *
 * Sends the STARTTLS readiness reply on descriptor 5, closes the plaintext
 * control descriptor, and resets the child SMTP state with RSET.
 */
static void smtp_starttls(void) {

    struct stat st;

    if (!flagstarttls || fstat(5, &st) != 0) {
        if (!stralloc_copys(&cline, "502 unimplemented (#5.5.1)")) die_nomem();
        if (!_catlogid(&cline)) die_nomem();
        if (!stralloc_cats(&cline, "\r\n")) die_nomem();
        if (!stralloc_0(&cline)) die_nomem();
        buffer_putsflush(&ssout, cline.s);
        log_d3(line.s, ": ", cline.s);
        errno = 0;
        return;
    }

    if (!stralloc_copys(&cline, "220 ready to start TLS (#2.0.0)")) die_nomem();
    if (!_catlogid(&cline)) die_nomem();
    if (!stralloc_cats(&cline, "\r\n")) die_nomem();
    if (!stralloc_0(&cline)) die_nomem();

    buffer_putsflush(&ssout5, cline.s);
    close(5);
    log_d3(line.s, ": ", cline.s);

    buffer_putsflush(&sscout, "RSET\r\n");
    smtpline(0, 1);
    smtp_reset_transaction();
}

struct commands smtpcommands[] = {
  { "quit", smtp_quit }
, { "data", smtp_data }
, { "mail", smtp_mail }
, { "rcpt", smtp_rcpt }
, { "ehlo", smtp_ehlo }
, { "starttls", smtp_starttls }
, { 0, smtp_default }
} ;


/*
 * main_tlswrapper_smtp - run the SMTP wrapper front-end
 *
 * @argc: process argument count
 * @argv: process argument vector
 * @flagnojail: non-zero to skip privilege dropping and jailed helpers
 *
 * Parses command-line options, starts the SMTP child, optionally resolves and
 * connects to the greylist service, enters the jail, and then proxies the
 * SMTP session until QUIT or failure.
 */
int main_tlswrapper_smtp(int argc, char **argv, int flagnojail) {

    char *x;
    struct stat st;
    int greylistfds[2];

    signal(SIGPIPE, SIG_IGN);
    signal(SIGALRM, signalhandler);
    alarm(30);
    log_set_name("tlswrapper-smtp");
    log_set_id(0);


    (void) argc;
    if (!argv[0]) usage();
    for (;;) {
        if (!argv[1]) break;
        if (argv[1][0] != '-') break;
        x = *++argv;
        if (x[0] == '-' && x[1] == 0) break;
        if (x[0] == '-' && x[1] == '-' && x[2] == 0) break;
        while (*++x) {
            if (*x == 'q') { flagverbose = 0; log_set_level(flagverbose); continue; }
            if (*x == 'Q') { flagverbose = 1; log_set_level(flagverbose); continue; }
            if (*x == 'v') { log_set_level(++flagverbose); continue; }
            /* user */
            if (*x == 'u') {
                if (x[1]) { user = x + 1; break; }
                if (argv[1]) { user = *++argv; break; }
            }
            /* timeouts */
            if (*x == 'T') {
                if (x[1]) { crwtimeoutstr =  x + 1; break; }
                if (argv[1]) { crwtimeoutstr = *++argv; break; }
            }
            if (*x == 't') {
                if (x[1]) { timeoutstr =  x + 1; break; }
                if (argv[1]) { timeoutstr = *++argv; break; }
            }
            /* greylist */
            if (*x == 'g') {
                if (x[1]) { greylisthostport =  x + 1; break; }
                if (argv[1]) { greylisthostport = *++argv; break; }
            }
            if (*x == 'c') { flaggreyfailclosed = 1; continue; }
            if (*x == 'C') { flaggreyfailclosed = 0; continue; }
            /* jail */
            if (*x == 'J') {
                if (x[1]) { jaildir = (x + 1); break; }
                if (argv[1]) { jaildir = (*++argv); break; }
            }
            if (*x == 'j') {
                if (x[1]) { jailaccount = (x + 1); break; }
                if (argv[1]) { jailaccount = (*++argv); break; }
            }
            usage();
        }
    }
    if (!*++argv) usage();
    timeout = timeout_parse(timeoutstr);
    crwtimeout = timeout_parse(crwtimeoutstr);
    if (greylisthostport) {
        if (!hostport_parse(greylisthost, sizeof greylisthost, greylistport, greylisthostport)) {
            log_f3("unable to parse greylist host:port from the string: '", greylisthostport, "'");
            die(100);
        }
        log_t5("greylist address '", greylisthost, ":", log_port(greylistport), "'");
    }

    if (fstat(5, &st) == 0) {
        flagstarttls = 1;
    }

    /* run child process */
    if (open_pipe(tochild) == -1) die_pipe();
    if (open_pipe(fromchild) == -1) die_pipe();
    child = fork();
    switch (child) {
        case -1:
            die_fork();
            break;
        case 0:
            close(tochild[1]);
            close(0);
            if (dup(tochild[0]) != 0) die_dup();
            fd_blocking_enable(0);

            close(fromchild[0]);
            close(1);
            if (dup(fromchild[1]) != 1) die_dup();
            fd_blocking_enable(1);

            close(5);

            /* drop root */
            if (user) if (jail_droppriv(user) == -1) die_droppriv(user);

            signal(SIGPIPE, SIG_DFL);
            log_t3("running '", argv[0], "'");
            execvp(*argv, argv);
            log_f2("unable to run ", *argv);
            die(111);
    }
    close(fromchild[1]);
    close(tochild[0]);
    fd_blocking_enable(fromchild[0]);
    fd_blocking_enable(tochild[1]);
    buffer_init(&sscin, _read, fromchild[0], cinbuf, sizeof cinbuf);
    buffer_init(&sscout, _write, tochild[1], coutbuf, sizeof coutbuf);

    /* initialize randombytes */
    {
        char ch[1];
        randombytes(ch, sizeof ch);
    }

    if (greylisthostport) {
        if (!flagnojail) {
            if (!resolvehost_init()) {
                log_f1("unable to create jailed process for DNS resolver");
                die(111);
            }
        }
        if (!conn_init(1)) {
            log_f1("unable to initialize TCP connection");
            die(111);
        }
    }

    /* create jail */
    if (!flagnojail) {
        if (jail(jailaccount, jaildir, 1) == -1) die_jail();
    }

    if (greylisthostport) {
        if (flagnojail) {
            greylistiplen = resolvehost(greylistip, sizeof greylistip, greylisthost);
        }
        else {
            greylistiplen = resolvehost_do(greylistip, sizeof greylistip, greylisthost);
        }
        if (greylistiplen < 0) {
            log_f3("unable to resolve host '", greylisthost, "'");
            die(111);
        }
        if (greylistiplen == 0) {
            log_f3("unable to resolve host '", greylisthost, "': name not exist");
            die(111);
        }
        log_d4("resolvehost: ", greylisthost, ": ", log_ip(greylistip));
        if (!flagnojail) resolvehost_close();

        if (!conn(greylistfds, crwtimeout, greylistip, greylistiplen, greylistport)) {
            if (flaggreyfailclosed) {
                log_f2("unable to connect to ", greylisthostport);
                die(111);
            }
            else {
                log_w2("unable to connect to ", greylisthostport);
            }
        }
        else {
            greylistfd = greylistfds[0];
            close(greylistfds[1]);
        }
    }

    /* get connection info */
    (void) connectioninfo_get(localip, localport, remoteip, remoteport);
    log_set_ip(iptostr(remoteipstr, remoteip));

    /* initialize mail transaction state */
    smtp_reset_transaction();

    alarm(timeout);

    smtp_greet();

    commands(smtpcommands);
    die(111);
    return 111;
}

/* clang-format on */
