/*
**  Copyright (c) 2001 The OSSP Project <http://www.ossp.org/>
**  Copyright (c) 2001 Cable & Wireless Deutschland <http://www.cw.com/de/>
**
**  This file is part of OSSP lmtp2nntp, an LMTP speaking local
**  mailer which forwards mails as Usenet news articles via NNTP.
**  It can be found at http://www.ossp.org/pkg/lmtp2nntp/.
**
**  This program is free software; you can redistribute it and/or
**  modify it under the terms of the GNU General Public  License
**  as published by the Free Software Foundation; either version
**  2.0 of the License, or (at your option) any later version.
**
**  This program is distributed in the hope that it will be useful,
**  but WITHOUT ANY WARRANTY; without even the implied warranty of
**  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
**  General Public License for more details.
**
**  You should have received a copy of the GNU General Public License
**  along with this file; if not, write to the Free Software
**  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
**  USA, or contact the OSSP project <ossp@ossp.org>.
**
**  lmtp2nntp.c: LMTP to NNTP main procedure
*/

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <sys/utsname.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <signal.h>
#include <pwd.h>

/* third party */
#include "str.h"
#include "argz.h"
#include "shpat_match.h"
#include "l2.h"
#include "daemon.h"

/* library version check (compile-time) */
#define  L2_VERSION_HEX_REQ 0x001200
#define  L2_VERSION_STR_REQ "0.1.0"
#define STR_VERSION_HEX_REQ 0x009206
#define STR_VERSION_STR_REQ "0.9.6"
#ifdef L2_VERSION_HEX
#if L2_VERSION_HEX < L2_VERSION_HEX_REQ
#error "require a newer version of OSSP L2"
#endif
#endif
#ifdef STR_VERSION_HEX
#if STR_VERSION_HEX < STR_VERSION_HEX_REQ
#error "require a newer version of OSSP Str"
#endif
#endif

/* own headers */
#include "lmtp2nntp.h"
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#if defined(HAVE_DMALLOC_H) && defined(DMALLOC)
#include "dmalloc.h"
#endif
#include "lmtp.h"
#include "nntp.h"
#include "sa.h"
#include "msg.h"
#define _VERSION_C_AS_HEADER_
#include "version.c"
#undef  _VERSION_C_AS_HEADER_

#ifndef FALSE
#define FALSE (1 != 1)
#endif
#ifndef TRUE
#define TRUE (!FALSE)
#endif

#ifndef NUL
#define NUL '\0'
#endif

#define ERR_EXECUTION 1
#define ERR_DELIVERY -2

#define STDSTRLEN 512
#define MAXNEWSSERVICES 16
#define MAXACLS 32

static lmtp_rc_t lmtp_cb_lhlo(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx);
static lmtp_rc_t lmtp_cb_mail(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx);
static lmtp_rc_t lmtp_cb_rcpt(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx);
static lmtp_rc_t lmtp_cb_data(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx);
static lmtp_rc_t lmtp_cb_noop(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx);
static lmtp_rc_t lmtp_cb_rset(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx);
static lmtp_rc_t lmtp_cb_quit(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *ctx);

static int helo_rfc0821domain(char *msg, char **domain);
static int helo_rfc1035domain(char *msg, char **domain);

struct session {
    int     lhlo_seen;
    char   *lhlo_domain;
};

static void catchsignal(int sig, ...);
static void initsession(struct session *session);
static void resetsession(struct session *session);
int groupmatch(char *, size_t, char *);

struct ns {
    char           *h;    /* host */
    char           *p;    /* port */
    sa_addr_t      *saa;  /* socket address abstraction */
    sa_t           *sa;   /* socket abstraction */
    nntp_t         *nntp;
    nntp_rc_t       rc;
    l2_stream_t    *l2;
};

struct acl {
    char      *acl;
    int        not;
    sa_addr_t *saa;
    size_t     prefixlen;
};

typedef struct {
    l2_context_t    ctx;
    char           *progname;
    char           *option_logfile;
    int             option_groupmode;
    int             option_operationmode;
    char           *option_operationmodefakestatus;
    char           *option_operationmodefakedsn;
    int             option_maxmessagesize;
    char           *azHeaderValuePairs;
    size_t          asHeaderValuePairs;
    int             option_timeout_lmtp_accept;
    int             option_timeout_lmtp_read;
    int             option_timeout_lmtp_write;
    int             option_timeout_nntp_connect;
    int             option_timeout_nntp_read;
    int             option_timeout_nntp_write;
    char           *option_mailfrom;
    char           *option_restrictheader;
    unsigned int    option_levelmask;
    char           *option_pidfile;
    int             option_killflag;
    uid_t           option_uid;
    int             option_daemon;
    int             option_aclc;
    struct acl      option_acl[MAXACLS];
    int             option_veryverbose;
    int             option_childsmax;
    int             active_childs;
    l2_stream_t    *l2;
    sa_addr_t      *saaAltio;
    sa_t           *saAltio;
    char           *cpBindh;
    char           *cpBindp;
    sa_addr_t      *saaBind;
    sa_t           *saBind;
    sa_addr_t      *saaIO;
    sa_t           *saIO;
    int             fdIOi;
    int             fdIOo;
    int             nsc;
    struct ns       ns[MAXNEWSSERVICES];
    char           *azGroupargs;
    size_t          asGroupargs;
    struct          session session;
    msg_t          *msg;
    struct utsname  uname;
} lmtp2nntp_t;

static void lmtp_gfs_ns(struct ns *);
static void lmtp_gfs_lhlo(lmtp2nntp_t *);
static void lmtp_gfs_rset(lmtp2nntp_t *);
static void lmtp_gfs_quit(lmtp2nntp_t *);

enum {
    GROUPMODE_ARG,
    GROUPMODE_ENVELOPE,
    GROUPMODE_HEADER
};

enum {
    OPERATIONMODE_FAKE,
    OPERATIONMODE_POST,
    OPERATIONMODE_FEED
};

/*
 * print usage information
 */
static void usage(char *command)
{
    /*  use
     *  perl <lmtp2nntp.c -e 'while (<>) { if(m/\/\*POD (.*) .*\*\//) { $_=$1; s/.<(.*?)>/$1/g ; print "\"$_ \"\n" };}'
     *  to pull the USAGE string out of this source
     */
    fprintf(stderr, 
            "USAGE: %s "
            "[-C childsmax] "
            "[-D] "
            "[-K] "
            "[-P pidfile] "
            "[-V] "
            "[-a addr/mask[,addr/mask[,...]] "
            "[-b addr[:port]|-|path[:perms]] "
            "[-c addr[:port]] "
            "[-d addr[:port][,addr[:port], ...]] "
            "[-g groupmode] "
            "[-h header:value] "
            "[-l level[:logfile]] "
            "[-m mailfrom] "
            "[-n nodename] "
            "[-o operationmode] "
            "[-r restrictheader] "
            "[-s size] "
            "[-t name=sec[,name=sec[,...]] "
            "[-u uid] "
            "[-v] "
            "newsgroup [newsgroup ...] "
            "\n",
            command);
    return;
}

static ssize_t hook_lmtp_read(void *_ctx, void *buf, size_t nbytes)
{
    lmtp2nntp_t *ctx = (lmtp2nntp_t *)_ctx;
    ssize_t rc;
    size_t n;
    sa_rc_t rv;

    if (ctx->saIO != NULL) {
        if ((rv = sa_read(ctx->saIO, buf, nbytes, &n)) != SA_OK)
            rc = -1;
        else
            rc = (ssize_t)n;
    }
    else
        rc = read(ctx->fdIOi, buf, nbytes);
    if (rc == -1)
        log0(ctx, TRACE, "LMTP read error: %m");
    else
        log3(ctx, TRACE, "LMTP %5d << \"%{text}D\"", rc, buf, rc);
    log1(ctx, DEBUG, "hook_lmtp_read() return, rc=%d", rc);
    return rc;
}

static ssize_t hook_lmtp_write(void *_ctx, const void *buf, size_t nbytes)
{
    lmtp2nntp_t *ctx = (lmtp2nntp_t *)_ctx;
    ssize_t rc;
    size_t n;
    sa_rc_t rv;

    log3(ctx, TRACE, "LMTP %5d >> \"%{text}D\"", nbytes, buf, nbytes);
    if (ctx->saIO != NULL) {
        if ((rv = sa_write(ctx->saIO, buf, nbytes, &n)) != SA_OK)
            rc = -1;
        else
            rc = (ssize_t)n;
    }
    else
        rc = write(ctx->fdIOo, buf, nbytes);
    if (rc == -1)
        log0(ctx, TRACE, "LMTP write error: %m");
    return rc;
}

static ssize_t hook_nntp_read(void *_ctx, void *buf, size_t nbytes)
{
    struct ns *ctx = (struct ns *)_ctx;
    ssize_t rc;
    size_t n;
    sa_rc_t rv;

    if ((rv = sa_read(ctx->sa, buf, nbytes, &n)) != SA_OK)
        rc = -1;
    else
        rc = (ssize_t)n;
    if (rc == -1)
        log0(ctx, TRACE, "NNTP read error: %m");
    else
        log3(ctx, TRACE, "NNTP %5d << \"%{text}D\"", rc, buf, rc);
    return rc;
}

static ssize_t hook_nntp_write(void *_ctx, const void *buf, size_t nbytes)
{
    struct ns *ctx = (struct ns *)_ctx;
    ssize_t rc;
    size_t n;
    sa_rc_t rv;

    log3(ctx, TRACE, "NNTP %5d >> \"%{text}D\"", nbytes, buf, nbytes);
    if ((rv = sa_write(ctx->sa, buf, nbytes, &n)) != SA_OK)
        rc = -1;
    else
        rc = (ssize_t)n;
    if (rc == -1)
        log0(ctx, TRACE, "NNTP write error: %m");
    return rc;
}

static l2_result_t 
formatter_prefix(l2_context_t *_ctx, const char id, const char *param,
          char *bufptr, size_t bufsize, size_t *buflen, va_list *ap)
{
    lmtp2nntp_t *ctx = (lmtp2nntp_t *)_ctx->vp;
            
    if ((ctx->msg != NULL) && (ctx->msg->cpFid != NULL)) {
        sprintf(bufptr, "%s: ", ctx->msg->cpFid);
        *buflen = strlen(bufptr);
    }
    else
        *buflen = 0;
    return L2_OK;
}

static l2_result_t 
formatter_errno(l2_context_t *_ctx, const char id, const char *param,
          char *bufptr, size_t bufsize, size_t *buflen, va_list *ap)
{
    sprintf(bufptr, "(%d) %s", errno, strerror(errno));
    *buflen = strlen(bufptr);
    return L2_OK;
}

static void catchsignal(int sig, ...)
{
    va_list ap;
    static lmtp2nntp_t *ctx = NULL;
    pid_t pid;

    if(sig == 0) {
        va_start(ap, sig);
        if ((ctx = va_arg(ap, lmtp2nntp_t *)) == NULL)
            exit(ERR_EXECUTION);
        log0(ctx, TRACE, "catching and logging signals now");
        va_end(ap);
        return;
    }
    if (ctx != NULL) {
        switch (sig) {
            case SIGCHLD:
                log1(ctx, NOTICE, "caught signal %d - wait for child", sig);
                pid = wait(NULL);
                ctx->active_childs--;
                log2(ctx, NOTICE, "caught signal %d - child [%ld] terminated", sig, (long)pid);
                return;
            case SIGUSR1:
                log1(ctx, NOTICE, "caught signal %d - flush logging stream", sig);
                l2_stream_flush(ctx->l2);
                return;
            case SIGHUP:
            case SIGINT:
            case SIGQUIT:
                log1(ctx, NOTICE, "caught signal %d - exit - no more logging", sig);
                break;
            default:
                log1(ctx, PANIC, "CAUGHT SIGNAL %d - EXIT - NO MORE LOGGING", sig);
        }
        l2_stream_destroy(ctx->l2);
    }
    exit(ERR_EXECUTION);
}


int main(int argc, char **argv)
{
    int           rc;
    lmtp_t       *lmtp = NULL;
    lmtp_io_t     lmtp_io;
    lmtp2nntp_t  *ctx = NULL;
    int           bOk;
    int           i;             /* general purpose scratch int, index ... */
    char         *cp;            /* general purpose character pointer */
    char         *azHosts;
    size_t        asHosts;
    char         *azTimeout;
    size_t        asTimeout;
    char         *azACL;
    size_t        asACL;
    char         *cpHost;
    char         *cpPort;
    l2_channel_t *chPrefix;
    l2_channel_t *chBuf;
    l2_channel_t *chFile;
    pid_t         pid;
    FILE         *fd;
    char         *cpName;
    char         *cpValue;
    int           nValue;
    char         *cpAddr;
    char         *cpPrefixLen;
    struct passwd *sPasswd;
    char         *cpHeadername;
    char         *cpHeadervalue;

    /* drop effective uid/gid priviledges */
    seteuid(getuid());
    setegid(getgid());

    /* library version check (run-time) */
    if (l2_version.v_hex < L2_VERSION_HEX_REQ) {
        fprintf(stderr, "require OSSP L2 >= %s, found %s\n", L2_VERSION_STR_REQ, L2_VERSION_STR);
        CU(ERR_EXECUTION);
    }
    if (str_version.v_hex < STR_VERSION_HEX_REQ) {
        fprintf(stderr, "require OSSP Str >= %s, found %s\n", STR_VERSION_STR_REQ, STR_VERSION_STR);
        CU(ERR_EXECUTION);
    }

    /* create application context */
    if ((ctx = (lmtp2nntp_t *)malloc(sizeof(lmtp2nntp_t))) == NULL)
        CU(ERR_EXECUTION);
    ctx->ctx.vp = ctx;
    ctx->progname = strdup(argv[0]);
    ctx->option_logfile = NULL;
    ctx->option_groupmode = GROUPMODE_ARG;
    ctx->option_operationmode = OPERATIONMODE_FAKE;
    ctx->option_operationmodefakestatus = "553";   /* Requested action not taken: mailbox name not allowed */
    ctx->option_operationmodefakedsn    = "5.7.1"; /* Delivery not authorized, message refused */
    ctx->option_maxmessagesize = 8 * 1024 * 1024;
    ctx->azHeaderValuePairs = NULL;
    ctx->asHeaderValuePairs = 0;
    ctx->option_timeout_lmtp_accept = 0;
    ctx->option_timeout_lmtp_read = 10;
    ctx->option_timeout_lmtp_write = 10;
    ctx->option_timeout_nntp_connect = 360;
    ctx->option_timeout_nntp_read = 60;
    ctx->option_timeout_nntp_write = 60;
    ctx->option_mailfrom = NULL;
    ctx->option_restrictheader = NULL;
    ctx->option_levelmask = L2_LEVEL_NONE;
    ctx->option_pidfile = NULL;
    ctx->option_killflag = FALSE;
    ctx->option_uid = getuid();
    ctx->option_daemon = FALSE;
    ctx->option_veryverbose = FALSE;
    ctx->option_childsmax = 10;
    ctx->active_childs = 0;
    ctx->l2 = NULL;
    ctx->saaAltio = NULL;
    ctx->saAltio = NULL;
    ctx->cpBindh = NULL;
    ctx->cpBindp = NULL;
    ctx->saaBind = NULL;
    ctx->saBind = NULL;
    ctx->nsc = 0;
    for (i=0; i < MAXNEWSSERVICES; i++) {
        ctx->ns[i].h = NULL;
        ctx->ns[i].p = NULL;
        ctx->ns[i].saa = NULL;
        ctx->ns[i].sa = NULL;
        ctx->ns[i].nntp = NULL;
        ctx->ns[i].rc = LMTP_ERR_UNKNOWN;
        ctx->ns[i].l2 = NULL;
    }
    ctx->option_aclc = 0;
    for (i = 0; i < MAXACLS; i++) {
        ctx->option_acl[i].acl = NULL;
        ctx->option_acl[i].not = FALSE;
        ctx->option_acl[i].saa = NULL;
        ctx->option_acl[i].prefixlen = 0;
    }
    ctx->azGroupargs = NULL;
    ctx->asGroupargs = 0;
    initsession(&ctx->session);
    ctx->msg = NULL;
    if (uname(&ctx->uname) == -1) {
        fprintf(stderr, "%s:Error: uname failed \"%s\"\n", ctx->progname, strerror(errno));
        CU(ERR_EXECUTION);
    }

    /*POD B<lmtp2nntp> */

    /*  use
     *  perl <lmtp2nntp.c -e 'while (<>) { if(m/\/\*POD (.*) .*\*\//) { $_=$1; print "$_\n" };}'
     *  to pull the POD SYNOPSIS header directly out of this source
     */

    /* read in the arguments */
    while ((i = getopt(argc, argv, "C:DKP:Va:b:c:d:g:h:l:m:n:o:r:s:t:u:v")) != -1) {
        switch (i) {
            case 'C': /*POD [B<-C> I<childsmax>] */
                ctx->option_childsmax = atoi(optarg);
                if (ctx->option_childsmax <= 0) {
                    fprintf(stderr, "%s:Error: Invalid number (%d) to option -C\n", ctx->progname, ctx->option_childsmax);
                    CU(ERR_EXECUTION);
                }
                break;
            case 'D': /*POD [B<-D>] */
                ctx->option_daemon = TRUE;
                break;
            case 'K': /*POD [B<-K>] */
                ctx->option_killflag = TRUE;
                break;
            case 'P': /*POD [B<-P> I<pidfile>] */
                ctx->option_pidfile = strdup(optarg);
                break;
            case 'V': /*POD [B<-V>] */
                ctx->option_veryverbose = TRUE;
                break;
            case 'a': /*POD [B<-a> I<addr>/I<mask>[,I<addr>/I<mask>[,...]] */
                if (argz_create_sep(optarg, ',', &azACL, &asACL) != 0)
                    CU(ERR_EXECUTION);
                cp = NULL;
                while ((cp = argz_next(azACL, asACL, cp)) != NULL) {
                    if (ctx->option_aclc >= MAXACLS) {
                        fprintf(stderr, "%s:Error: Too many ACL (%d) using option -a\n", ctx->progname, ctx->option_aclc);
                        CU(ERR_EXECUTION);
                    }
                    ctx->option_acl[ctx->option_aclc].acl = strdup(cp);
                    if (cp[0] == '!') {
                        ctx->option_acl[ctx->option_aclc].not = TRUE;
                        cpAddr = strdup(cp+1);
                    }
                    else {
                        cpAddr = strdup(cp);
                    }
                    if ((cpPrefixLen = strrchr(cpAddr, '/')) != NULL)
                        *cpPrefixLen++ = NUL;
                    else
                        cpPrefixLen = "-1";
                    ctx->option_acl[ctx->option_aclc].prefixlen = atoi(cpPrefixLen);
                    if ((rc = sa_addr_create(&ctx->option_acl[ctx->option_aclc].saa)) != SA_OK) {
                        fprintf(stderr, "%s:Error: Creating address failed for -a option (%d)\n", 
                                ctx->progname, rc);
                    }
                    if ((rc = sa_addr_u2a(ctx->option_acl[ctx->option_aclc].saa, "inet://%s:0", cpAddr)) != SA_OK) {
                        fprintf(stderr, "%s:Error: Parsing host address failed for \"%s:0\" (%d)\n", 
                                ctx->progname, cpAddr, rc);
                        CU(ERR_EXECUTION);
                    }
                    ctx->option_aclc++;
                    free(cpAddr);
                }
                free(azACL);
                break;
            case 'b': /*POD [B<-b> I<addr>[I<:port>]|C<->|I<path>[:perms]] */
                if (strcmp(optarg, "-") != 0) {
                    if ((rc = sa_create(&ctx->saAltio)) != SA_OK) {
                        fprintf(stderr, "%s:Error: Creating TCP socket failed for \"%s\": %s\n", 
                                ctx->progname, optarg, strerror(errno));
                        CU(ERR_EXECUTION);
                    }
                    if ((rc = sa_addr_create(&ctx->saaAltio)) != SA_OK) {
                        fprintf(stderr, "%s:Error: Creating address failed for -b option (%d)\n", 
                                ctx->progname, rc);
                    }
                    if (optarg[0] == '/') {
                        char *cpPath;
                        char *cpPerm;
                        int nPerm;
                        int n;

                        cpPath = strdup(optarg);
                        cpPerm = NULL;
                        nPerm  = -1;
                        if ((cpPerm = strrchr(cpPath, ':')) != NULL) {
                            *cpPerm++ = '\0';
                            nPerm = 0;
                            for (i = 0; i < 4 && cpPerm[i] != '\0'; i++) {
                                if (!isdigit((int)cpPerm[i])) {
                                    nPerm = -1;
                                    break;
                                }
                                n = cpPerm[i] - '0';
                                if (n > 7) {
                                    nPerm = -1;
                                    break;
                                }
                                nPerm = ((nPerm << 3) | n);
                            }
                            if (nPerm == -1 || cpPerm[i] != '\0') {
                                fprintf(stderr, "%s:Error: Invalid permissions \"%s\"\n", ctx->progname, cpPerm);
                                CU(ERR_EXECUTION);
                            }
                        }
                        if ((rc = sa_addr_u2a(ctx->saaAltio, "unix:%s", cpPath)) != SA_OK) {
                            fprintf(stderr, "%s:Error: Parsing alternate IO guessing UNIX domain socket failed for \"%s\" (%d)\n", 
                                    ctx->progname, cpPath, rc);
                            CU(ERR_EXECUTION);
                        }
                        if ((rc = sa_bind(ctx->saAltio, ctx->saaAltio)) != SA_OK) {
                            fprintf(stderr, "%s:Error: Bind failed for \"%s\": %s\n", 
                                    ctx->progname, cpPath, strerror(errno));
                            CU(ERR_EXECUTION);
                        }
                        if (nPerm != -1) {
                            if (chmod(cpPath, nPerm) == -1) {
                                fprintf(stderr, "%s:Error: chmod failed for \"%s\": %s\n", ctx->progname, cpPath, strerror(errno));
                                CU(ERR_EXECUTION);
                            }
                        }
                        if (getuid() == 0 && getuid() != ctx->option_uid) {
                            if (chown(cpPath, ctx->option_uid, -1) == -1) {
                                fprintf(stderr, "%s:Error: chown failed for \"%s\": %s\n", ctx->progname, cpPath, strerror(errno));
                                CU(ERR_EXECUTION);
                            }
                        }
                        free(cpPath);
                    }
                    else {
                        if ((rc = sa_addr_u2a(ctx->saaAltio, "inet://%s", optarg)) != SA_OK) {
                            fprintf(stderr, "%s:Error: Parsing alternate IO guessing INET socket failed for \"%s\" (%d)\n", 
                                    ctx->progname, optarg, rc);
                            CU(ERR_EXECUTION);
                        }
                        if ((rc = sa_bind(ctx->saAltio, ctx->saaAltio)) != SA_OK) {
                            fprintf(stderr, "%s:Error: Bind failed for \"%s\": %s\n", 
                                    ctx->progname, optarg, strerror(errno));
                            CU(ERR_EXECUTION);
                        }
                    }
                    if ((rc = sa_listen(ctx->saAltio, -1)) != SA_OK) {
                        fprintf(stderr, "%s:Error: Listen to failed for \"%s\": %s\n", 
                                ctx->progname, optarg, strerror(errno));
                        CU(ERR_EXECUTION);
                    }
                }
                break;
            case 'c': /*POD [B<-c> I<addr>[I<:port>]] */
                ctx->cpBindh = strdup(optarg);
                if ((ctx->cpBindp = strrchr(ctx->cpBindh, ':')) != NULL) {
                    *ctx->cpBindp++ = NUL;
                    ctx->cpBindp = strdup(ctx->cpBindp);
                }
                else 
                    ctx->cpBindp = strdup("0");
                if ((rc = sa_addr_create(&ctx->saaBind)) != SA_OK) {
                    fprintf(stderr, "%s:Error: Creating address failed for -c option (%d)\n", 
                            ctx->progname, rc);
                }
                if ((rc = sa_addr_u2a(ctx->saaBind, "inet://%s:%s", ctx->cpBindh, ctx->cpBindp)) != SA_OK) {
                    fprintf(stderr, "%s:Error: Parsing bind address failed for \"%s:%s\" (%d)\n", 
                            ctx->progname, ctx->cpBindh, ctx->cpBindp, rc);
                    CU(ERR_EXECUTION);
                }
                break;
            case 'd': /*POD [B<-d> I<addr>[I<:port>][,I<addr>[I<:port>], ...]] */
                if (argz_create_sep(optarg, ',', &azHosts, &asHosts) != 0)
                    CU(ERR_EXECUTION);
                cp = NULL;
                while ((cp = argz_next(azHosts, asHosts, cp)) != NULL) {
                    if (ctx->nsc >= MAXNEWSSERVICES) {
                        fprintf(stderr, "%s:Error: Too many services (%d) using option -d\n", ctx->progname, ctx->nsc);
                        CU(ERR_EXECUTION);
                    }
                    cpHost = strdup(cp);
                    if ((cpPort = strrchr(cpHost, ':')) != NULL) {
                        *cpPort++ = NUL;
                        cpPort = strdup(cpPort);
                    }
                    else 
                        cpPort = strdup("nntp");
                    ctx->ns[ctx->nsc].h = cpHost;
                    ctx->ns[ctx->nsc].p = cpPort;
                    if ((rc = sa_addr_create(&ctx->ns[ctx->nsc].saa)) != SA_OK) {
                        fprintf(stderr, "%s:Error: Creating address failed for -d option (%d)\n", 
                            ctx->progname, rc);
                    }
                    if ((rc = sa_addr_u2a(ctx->ns[ctx->nsc].saa, "inet://%s:%s", 
                                     ctx->ns[ctx->nsc].h, ctx->ns[ctx->nsc].p)) != SA_OK) {
                        fprintf(stderr, "%s:Error: Parsing host address failed for \"%s:%s\" (%d)\n", 
                                ctx->progname, ctx->ns[ctx->nsc].h, ctx->ns[ctx->nsc].p, rc);
                        CU(ERR_EXECUTION);
                    }
                    if ((rc = sa_create(&ctx->ns[ctx->nsc].sa)) != SA_OK) {
                        fprintf(stderr, "%s:Error: Creating TCP socket failed for \"%s:%s\": %s\n", 
                                ctx->progname, ctx->ns[ctx->nsc].h, ctx->ns[ctx->nsc].p, strerror(errno));
                        CU(ERR_EXECUTION);
                    }
                    ctx->ns[ctx->nsc].nntp = NULL;
                    ctx->nsc++;
                }
                free(azHosts);
                break;
            case 'g': /*POD [B<-g> I<groupmode>] */
                if      (strcasecmp(optarg, "arg") == 0)
                    ctx->option_groupmode = GROUPMODE_ARG;
                else if (strcasecmp(optarg, "envelope") == 0)
                    ctx->option_groupmode = GROUPMODE_ENVELOPE;
                else if (strcasecmp(optarg, "header") == 0)
                    ctx->option_groupmode = GROUPMODE_HEADER;
                else {
                    fprintf(stderr, "%s:Error: Invalid mode \"%s\" to option -g\n", ctx->progname, optarg);
                    CU(ERR_EXECUTION);
                }
                break;
            case 'h': /*POD [B<-h> I<header>:<value>] */
                cpHeadername = strdup(optarg);
                if ((cp = strchr(cpHeadername, ':')) == NULL) {
                    free(cpHeadername);
                    fprintf(stderr, "%s:Error: header \"%s\" for -h option not terminated with colon\n", 
                            ctx->progname, cpHeadername);
                    CU(ERR_EXECUTION);
                }
                cp++;
                if (*cp == NUL) {
                    free(cpHeadername);
                    fprintf(stderr, "%s:Error: header \"%s\" for -h option has no value\n", 
                            ctx->progname, cpHeadername);
                    CU(ERR_EXECUTION);
                }
                cpHeadervalue = strdup(cp);
                *cp = NUL;
                argz_add(&ctx->azHeaderValuePairs, &ctx->asHeaderValuePairs, cpHeadername);
                argz_add(&ctx->azHeaderValuePairs, &ctx->asHeaderValuePairs, cpHeadervalue);
                free(cpHeadervalue);
                free(cpHeadername);
                break;
            case 'l': /*POD [B<-l> I<level>[:I<logfile>]] */
                if ((cp = strrchr(optarg, ':')) != NULL) {
                    *cp++ = NUL;
                    if (*cp == NUL) {
                        fprintf(stderr, "%s:Error: empty logfile to option -l\n", ctx->progname);
                        CU(ERR_EXECUTION);
                    }
                    else
                        ctx->option_logfile = strdup(cp);
                }
                else
                    ctx->option_logfile = strdup("logfile");

                if (l2_util_s2l(optarg, strlen(optarg), ',', &ctx->option_levelmask) != L2_OK) {
                    fprintf(stderr, "%s:Error: invalid level \"%s\" to option -l\n", ctx->progname, optarg);
                    CU(ERR_EXECUTION);
                }
                ctx->option_levelmask = L2_LEVEL_UPTO(ctx->option_levelmask);
                break;
            case 'm': /*POD [B<-m> I<mailfrom>] */
                ctx->option_mailfrom = strdup(optarg);
                /* protect ourselfs from the substitution of backreferences.
                 * Missing varargs would cause segfaults.  Rewrite capturing
                 * brackets to clustering syntax. Use poor man's s///g
                 * simulator as current str library doesn't support global
                 * substitution */
                while (str_parse(ctx->option_mailfrom, "s/(.*?)\\((?!\\?:)(.*)/$1(?:$2/", &cp) > 0) {
                    free(ctx->option_mailfrom);
                    ctx->option_mailfrom = cp;
                }
                if (str_parse("<>", ctx->option_mailfrom) == -1) {
                    fprintf(stderr, "%s:Error: illegal regex \"%s\" to option -m.\n", ctx->progname, ctx->option_mailfrom);
                    CU(ERR_EXECUTION);
                }
                break;
            case 'n': /*POD [B<-n> I<nodename>] */
                if (strlen(optarg) > sizeof(ctx->uname.nodename)-1) {
                        fprintf(stderr, "%s:Error: nodename \"%s\" to long to option -n.\n", ctx->progname, optarg);
                        CU(ERR_EXECUTION);
                }
                strcpy(ctx->uname.nodename, optarg);
                break;
            case 'o': /*POD [B<-o> I<operationmode>] */
                if      (strcasecmp(optarg, "post") == 0)
                    ctx->option_operationmode = OPERATIONMODE_POST;
                else if (strcasecmp(optarg, "feed") == 0)
                    ctx->option_operationmode = OPERATIONMODE_FEED;
                else {
                    if (strlen(optarg) != 9) {
                        fprintf(stderr, "%s:Error: Invalid format or length \"%s\" to option -o\n", ctx->progname, optarg);
                        CU(ERR_EXECUTION);
                    }

                    if (optarg[3] != '/') {
                        fprintf(stderr, "%s:Error: Invalid format or missing slash \"%s\" to option -o\n", ctx->progname, optarg);
                        CU(ERR_EXECUTION);
                    }

                    optarg[3] = NUL;
                    ctx->option_operationmodefakestatus = &optarg[0];
                    ctx->option_operationmodefakedsn    = &optarg[4];

                    if (   strlen(ctx->option_operationmodefakestatus) != 3
                        || !isdigit((int)ctx->option_operationmodefakestatus[0])
                        || !isdigit((int)ctx->option_operationmodefakestatus[1])
                        || !isdigit((int)ctx->option_operationmodefakestatus[2])) {
                            fprintf(stderr, "%s:Error: Invalid status in format \"%s\" to option -o\n", ctx->progname, optarg);
                            CU(ERR_EXECUTION);
                        }

                    if (   (strlen(ctx->option_operationmodefakedsn) != 5)
                        || !isdigit((int)ctx->option_operationmodefakedsn[0])
                        || (ctx->option_operationmodefakedsn[1] != '.')
                        || !isdigit((int)ctx->option_operationmodefakedsn[2])
                        || (ctx->option_operationmodefakedsn[3] != '.')
                        || !isdigit((int)ctx->option_operationmodefakedsn[4])
                        || (ctx->option_operationmodefakedsn[0] != ctx->option_operationmodefakestatus[0])) {
                            fprintf(stderr, "%s:Error: Invalid dsn in format \"%s\" to option -o\n", ctx->progname, optarg);
                            CU(ERR_EXECUTION);
                        }
                    }
                break;
            case 'r': /*POD [B<-r> I<restrictheader>] */
                ctx->option_restrictheader = strdup(optarg);
                /* protect ourselfs from the substitution of backreferences.
                 * Missing varargs would cause segfaults.  Rewrite capturing
                 * brackets to clustering syntax. Use poor man's s///g
                 * simulator as current str library doesn't support global
                 * substitution */
                while (str_parse(ctx->option_restrictheader, "s/(.*?)\\((?!\\?:)(.*)/$1(?:$2/", &cp) > 0) {
                    free(ctx->option_restrictheader);
                    ctx->option_restrictheader = cp;
                }
                if (str_parse("<>", ctx->option_restrictheader) == -1) {
                    fprintf(stderr, "%s:Error: illegal regex \"%s\" to option -r.\n", ctx->progname, ctx->option_restrictheader);
                    CU(ERR_EXECUTION);
                }
                break;
            case 's': /*POD [B<-s> I<size>] */
                ctx->option_maxmessagesize = atoi(optarg);
                if(ctx->option_maxmessagesize < 64) {
                    fprintf(stderr, "%s:Error: maximum message size is unacceptable small.\n", ctx->progname);
                    CU(ERR_EXECUTION);
                }
                break;
            case 't': /*POD [B<-t> I<name>=I<sec>[,I<name>=I<sec>[,...]] */
                if (argz_create_sep(optarg, ',', &azTimeout, &asTimeout) != 0)
                    CU(ERR_EXECUTION);
                cp = NULL;
                while ((cp = argz_next(azTimeout, asTimeout, cp)) != NULL) {
                    cpName = strdup(cp);
                    if ((cpValue = strrchr(cpName, '=')) == NULL) {
                        fprintf(stderr, "%s:Error: comma-seperated argument %s to option -t have to be name=value.\n", ctx->progname, cp);
                        CU(ERR_EXECUTION);
                    }
                    *cpValue++ = NUL;
                    nValue = atoi(cpValue);
                    if (nValue < 0) {
                        fprintf(stderr, "%s:Error: timeout %s=%d to option -t must be a positive integer.\n", ctx->progname, cpName, nValue);
                        CU(ERR_EXECUTION);
                    }
                    if (strcmp(cpName, "lmtp") == 0) {
                        ctx->option_timeout_lmtp_accept = nValue;
                        ctx->option_timeout_lmtp_read = nValue;
                        ctx->option_timeout_lmtp_write = nValue;
                    }
                    else if (strcmp(cpName, "lmtp:accept") == 0)
                        ctx->option_timeout_lmtp_accept = nValue;
                    else if (strcmp(cpName, "lmtp:read") == 0)
                        ctx->option_timeout_lmtp_read = nValue;
                    else if (strcmp(cpName, "lmtp:write") == 0)
                        ctx->option_timeout_lmtp_write = nValue;
                    else if (strcmp(cpName, "nntp") == 0) {
                        ctx->option_timeout_nntp_connect = nValue;
                        ctx->option_timeout_nntp_read = nValue;
                        ctx->option_timeout_nntp_write = nValue;
                    }
                    else if (strcmp(cpName, "nntp:connect") == 0)
                        ctx->option_timeout_nntp_connect = nValue;
                    else if (strcmp(cpName, "nntp:read") == 0)
                        ctx->option_timeout_nntp_read = nValue;
                    else if (strcmp(cpName, "nntp:write") == 0)
                        ctx->option_timeout_nntp_write = nValue;
                    else {
                        fprintf(stderr, "%s:Error: unknown timeout %s to option -t.\n", ctx->progname, cpName);
                        CU(ERR_EXECUTION);
                    }
                    free(cpName);
                }
                free(azTimeout);
                break;
            case 'u': /*POD [B<-u> I<uid>] */
                if (isdigit((int)optarg[0])) {
                    if ((sPasswd = getpwuid((uid_t)atoi(optarg))) == NULL) {
                        fprintf(stderr, "%s:Error: uid \"%s\" not found for -u option.\n", ctx->progname, optarg);
                        CU(ERR_EXECUTION);
                    }
                }
                else {
                    if ((sPasswd = getpwnam(optarg)) == NULL) {
                        fprintf(stderr, "%s:Error: loginname \"%s\" not found for -u option.\n", ctx->progname, optarg);
                        CU(ERR_EXECUTION);
                    }
                }
                ctx->option_uid = sPasswd->pw_uid;
                break;
            case 'v': /*POD [B<-v>] (version)*/
                fprintf(stdout, "%s\n", lmtp2nntp_version.v_gnu);
                CU(0);
                break;
            case '?':
            default:
                usage(ctx->progname);
                CU(ERR_EXECUTION);
        }
    }
    /*POD I<newsgroup> [I<newsgroup> ...] */
    for (i = optind; i < argc; i++) {
        argz_add(&ctx->azGroupargs, &ctx->asGroupargs, argv[i]);
    }

    /* if no positive ACL exists (option -a) add a wildcard match-all for IPv4 and IPv6 */
    bOk = FALSE;
    for (i = 0; i < ctx->option_aclc; i++) {
        if (!ctx->option_acl[i].not) {
            bOk = TRUE;
            break;
        }
    }
    if (!bOk) {
        if (ctx->option_aclc >= MAXACLS) {
            fprintf(stderr, "%s:Error: Too many ACL (%d) using option -a (no space for additional fake IPv4 ACL)\n", ctx->progname, ctx->option_aclc);
            CU(ERR_EXECUTION);
        }
        ctx->option_acl[ctx->option_aclc].acl = "0.0.0.0";
        ctx->option_acl[ctx->option_aclc].not = FALSE;
        ctx->option_acl[ctx->option_aclc].prefixlen = 0;
        if ((rc = sa_addr_create(&ctx->option_acl[ctx->option_aclc].saa)) != SA_OK) {
            fprintf(stderr, "%s:Error: Creating fake address failed for -a option (%d)\n", 
                    ctx->progname, rc);
        }
        if ((rc = sa_addr_u2a(ctx->option_acl[ctx->option_aclc].saa, "inet://%s:0", ctx->option_acl[ctx->option_aclc].acl)) != SA_OK) {
            fprintf(stderr, "%s:Error: Parsing host address failed for \"%s:0\" (%s)\n", 
                    ctx->progname, ctx->option_acl[ctx->option_aclc].acl,
                    sa_error(rc));
            CU(ERR_EXECUTION);
        }
        ctx->option_aclc++;
    }
    if (!bOk) {
        if (ctx->option_aclc >= MAXACLS) {
            fprintf(stderr, "%s:Error: Too many ACL (%d) using option -a (no space for additional fake IPv6 ACL)\n", ctx->progname, ctx->option_aclc);
            CU(ERR_EXECUTION);
        }
        ctx->option_acl[ctx->option_aclc].acl = "[::]";
        ctx->option_acl[ctx->option_aclc].not = FALSE;
        ctx->option_acl[ctx->option_aclc].prefixlen = 0;
        if ((rc = sa_addr_create(&ctx->option_acl[ctx->option_aclc].saa)) != SA_OK) {
            fprintf(stderr, "%s:Error: Creating fake address failed for -a option (%d)\n", 
                    ctx->progname, rc);
        }
        if ((rc = sa_addr_u2a(ctx->option_acl[ctx->option_aclc].saa, "inet://%s:0", ctx->option_acl[ctx->option_aclc].acl)) != SA_OK) {
            fprintf(stderr, "%s:Error: Parsing host address failed for \"%s:0\" (%s)\n", 
                    ctx->progname, ctx->option_acl[ctx->option_aclc].acl,
                    sa_error(rc));
            CU(ERR_EXECUTION);
        }
        ctx->option_aclc++;
    }

    if (getuid() != ctx->option_uid) {
        if (setuid(ctx->option_uid) == -1) {
            fprintf(stderr, "%s:Error: Setting UID to %d failed: %s\n", 
                    ctx->progname, ctx->option_uid, strerror(errno));
            CU(ERR_EXECUTION);
        }
    }

    if ((ctx->l2 = l2_stream_create()) == NULL) {
        fprintf(stderr, "%s:Error: logging failed to create stream\n", ctx->progname);
        CU(ERR_EXECUTION);
    }
    if (l2_stream_levels(ctx->l2, L2_LEVEL_UPTO(L2_LEVEL_DEBUG), L2_LEVEL_NONE) != L2_OK) {
        fprintf(stderr, "%s:Error: logging failed to set global logging level\n", ctx->progname);
        CU(ERR_EXECUTION);
    }

    if (l2_stream_formatter(ctx->l2, 'P', formatter_prefix, &ctx->ctx) != L2_OK) {
        fprintf(stderr, "%s:Error: logging failed to register prefix formatter\n", ctx->progname);
        CU(ERR_EXECUTION);
    }
    if (l2_stream_formatter(ctx->l2, 'D', l2_util_fmt_dump, NULL) != L2_OK) {
        fprintf(stderr, "%s:Error: logging failed to register dump formatter\n", ctx->progname);
        CU(ERR_EXECUTION);
    }
    if (l2_stream_formatter(ctx->l2, 'S', l2_util_fmt_string, NULL) != L2_OK) {
        fprintf(stderr, "%s:Error: logging failed to register string formatter\n", ctx->progname);
        CU(ERR_EXECUTION);
    }
    if (l2_stream_formatter(ctx->l2, 'm', formatter_errno, NULL) != L2_OK) {
        fprintf(stderr, "%s:Error: logging failed to register errno formatter\n", ctx->progname);
        CU(ERR_EXECUTION);
    }

    if((ctx->option_levelmask != L2_LEVEL_NONE) && (ctx->option_logfile != NULL)) {
        if ((chPrefix  = l2_channel_create(&l2_handler_prefix)) == NULL) {
            fprintf(stderr, "%s:Error: logging failed to create prefix channel\n", ctx->progname);
            CU(ERR_EXECUTION);
        }
        if (l2_channel_configure(chPrefix, "prefix,timezone", "%b %d %H:%M:%S <%L> lmtp2nntp[%P]: ", "local") != L2_OK) {
            fprintf(stderr, "%s:Error: logging failed to configure prefix channel\n", ctx->progname);
            CU(ERR_EXECUTION);
        }

        if (!ctx->option_veryverbose) {
            if ((chBuf  = l2_channel_create(&l2_handler_buffer)) == NULL) {
                fprintf(stderr, "%s:Error: logging failed to create buffer channel\n", ctx->progname);
                CU(ERR_EXECUTION);
            }
            if (l2_channel_configure(chBuf, "size", 65536) != L2_OK) {
                fprintf(stderr, "%s:Error: logging failed to configure buffer channel\n", ctx->progname);
                CU(ERR_EXECUTION);
            }
        }
        else
            chBuf = NULL;

        if ((chFile = l2_channel_create(&l2_handler_file)) == NULL) {
            fprintf(stderr, "%s:Error: logging failed to create file channel\n", ctx->progname);
            CU(ERR_EXECUTION);
        }
        if (l2_channel_configure(chFile, "path,append,perm", ctx->option_logfile, TRUE, 0644) != L2_OK) {
            fprintf(stderr, "%s:Error: logging failed to configure file channel\n", ctx->progname);
            CU(ERR_EXECUTION);
        }

        if (!ctx->option_veryverbose) {
            if (l2_channel_stack(chFile, chBuf) != L2_OK) {
                fprintf(stderr, "%s:Error: logging failed to stack buffer channel on top of file channel\n", ctx->progname);
                CU(ERR_EXECUTION);
            }
            if (l2_channel_stack(chBuf, chPrefix) != L2_OK) {
                fprintf(stderr, "%s:Error: logging failed to stack prefix channel on top of buffer channel\n", ctx->progname);
                CU(ERR_EXECUTION);
            }
        }
        else {
            if (l2_channel_stack(chFile, chPrefix) != L2_OK) {
                fprintf(stderr, "%s:Error: logging failed to stack prefix channel on top of buffer channel\n", ctx->progname);
                CU(ERR_EXECUTION);
            }
        }

        if (l2_channel_open(chPrefix) != L2_OK) {
            fprintf(stderr, "%s:Error: logging failed to open buffer channel\n", ctx->progname);
            CU(ERR_EXECUTION);
        }

        if (l2_stream_channel(ctx->l2, chPrefix, ctx->option_levelmask, L2_LEVEL_NONE) != L2_OK) {
            fprintf(stderr, "%s:Error: logging failed to attach channel into stream\n", ctx->progname);
            CU(ERR_EXECUTION);
        }
    }

    if (log1(ctx, NOTICE, "startup, version %s", lmtp2nntp_version.v_gnu) != L2_OK) {
        fprintf(stderr, "%s:Error: logging failed to log startup message to stream\n", ctx->progname);
        CU(ERR_EXECUTION);
    }
    /* from this point on logging is up and running and fprintf(stderr, ...)
     * should not be used in the remainder of the code
     */

    if (ctx->option_veryverbose)
        log0(ctx, NOTICE, "logging very verbose (unbuffered)");

    if ((ctx->option_pidfile != NULL) && ctx->option_killflag) {
        if ((fd = fopen(ctx->option_pidfile, "r")) == NULL)
            log1(ctx, ERROR, "cannot open pidfile \"%s\" for reading %m", ctx->option_pidfile);
        else {
            if (fscanf(fd, "%d\n", &pid) != 1) {
                fclose(fd);
                log1(ctx, ERROR, "cannot extract pid from pidfile \"%s\"", ctx->option_pidfile);
            }
            else {
                fclose(fd);
                log1(ctx, TRACE, "going to kill pid[%d]", pid);
                if (kill(pid, SIGHUP) == -1)
                    log1(ctx, ERROR, "killing pid[%d] failed %m", pid);
                if (unlink(ctx->option_pidfile) == -1)
                    log1(ctx, ERROR, "unlinking pidfile \"%s\" failed %m", ctx->option_pidfile);
            }
        }
        CU(0);
    }

    catchsignal(0, ctx);
    signal(SIGCHLD, (void(*)())catchsignal);
    signal(SIGHUP,  (void(*)())catchsignal);
    signal(SIGINT,  (void(*)())catchsignal);
    signal(SIGQUIT, (void(*)())catchsignal);
    signal(SIGILL,  (void(*)())catchsignal);
    signal(SIGBUS,  (void(*)())catchsignal);
    signal(SIGSEGV, (void(*)())catchsignal);
    signal(SIGSYS,  (void(*)())catchsignal);
    signal(SIGTERM, (void(*)())catchsignal);
    signal(SIGUSR1, (void(*)())catchsignal);
    signal(SIGUSR2,            SIG_IGN    );

    /* loop for LMTP protocol with support for alternate io through daemon */
    if (ctx->saAltio == NULL) {
        /* initialize LMTP context */
        ctx->fdIOi = STDIN_FILENO;
        ctx->fdIOo = STDOUT_FILENO;
        lmtp_io.ctx    = ctx;
        lmtp_io.read   = hook_lmtp_read;
        lmtp_io.write  = hook_lmtp_write;
        if ((lmtp = lmtp_create(&lmtp_io)) == NULL) {
            log0(ctx, ERROR, "Unable to initialize LMTP library\n");
            CU(ERR_EXECUTION);
        }
        /*  RFC0821, 4.5.1. MINIMUM IMPLEMENTATION
         *  In order to make SMTP workable, the following minimum implementation
         *  is required for all receivers: [...]
         *  RFC0821, 4.1.2. COMMAND SYNTAX
         *  
         *  Verb Parameter
         *  ----+-------------------------------
         *  HELO <SP> <domain> <CRLF>
         *  MAIL <SP> FROM:<reverse-path> <CRLF>
         *  RCPT <SP> TO:<forward-path> <CRLF>
         *  DATA <CRLF>
         *  RSET <CRLF>
         *  NOOP <CRLF>
         *  QUIT <CRLF>
         */
        lmtp_register(lmtp, "LHLO", lmtp_cb_lhlo, ctx, NULL, NULL); 
        lmtp_register(lmtp, "MAIL", lmtp_cb_mail, ctx, NULL, NULL);
        lmtp_register(lmtp, "RCPT", lmtp_cb_rcpt, ctx, NULL, NULL);
        lmtp_register(lmtp, "DATA", lmtp_cb_data, ctx, NULL, NULL);
        lmtp_register(lmtp, "RSET", lmtp_cb_rset, ctx, NULL, NULL);
        lmtp_register(lmtp, "NOOP", lmtp_cb_noop, ctx, NULL, NULL);
        lmtp_register(lmtp, "QUIT", lmtp_cb_quit, ctx, NULL, NULL);
        lmtp_loop(lmtp);
        lmtp_gfs_quit(ctx);
        lmtp_gfs_lhlo(ctx);
        lmtp_destroy(lmtp);
    } else {
        pid = getpid();
        if (ctx->option_daemon) {
            daemonize();
            log1(ctx, NOTICE, "daemonized, previous pid[%d]", pid);
        }
        if (ctx->option_pidfile != NULL) {
            if ((fd = fopen(ctx->option_pidfile, "w+")) == NULL)
                log1(ctx, ERROR, "cannot open pidfile \"%s\" for writing %m", ctx->option_pidfile);
            else {
                fprintf(fd, "%d\n", getpid());
                fclose(fd);
            }
        }

        sa_timeout(ctx->saAltio, SA_TIMEOUT_ALL,    0, 0);
        sa_timeout(ctx->saAltio, SA_TIMEOUT_ACCEPT, ctx->option_timeout_lmtp_accept, 0);
        sa_timeout(ctx->saAltio, SA_TIMEOUT_READ,   ctx->option_timeout_lmtp_read,   0);
        sa_timeout(ctx->saAltio, SA_TIMEOUT_WRITE,  ctx->option_timeout_lmtp_write,  0);
        while (1) {
            while (ctx->active_childs >= ctx->option_childsmax) {
                log1(ctx, ERROR, "maximum number of childs (%d) reached - waiting (1s)", ctx->option_childsmax);
                sleep(1);
            }

            if ((rc = sa_accept(ctx->saAltio, &ctx->saaIO, &ctx->saIO)) != SA_OK) {
                if (rc == SA_ERR_SYS)
                    log3(ctx, ERROR, "accept failed: %s: (%d) %s", sa_error(rc), errno, strerror(errno));
                else
                    log1(ctx, ERROR, "accept failed: %s", sa_error(rc));
                sleep(10);
                continue;
            }

            /* Access Control List */
            bOk = FALSE;
            /* check positive matches */
            for (i = 0; i < ctx->option_aclc; i++) {
                char *cpA1;
                char *cpA2;
                if (ctx->option_acl[i].not)
                    continue;
                sa_addr_a2u(ctx->option_acl[i].saa, &cpA1);
                sa_addr_a2u(ctx->saaIO, &cpA2);
                if (sa_addr_match(ctx->saaIO, ctx->option_acl[i].saa, ctx->option_acl[i].prefixlen) == SA_OK) {
                    log4(ctx, TRACE, "positive/inclusive ACL \"%s\" (%s/%d) matches %s: YES (stop comparison)", ctx->option_acl[i].acl, cpA1, ctx->option_acl[i].prefixlen, cpA2);
                    bOk = TRUE;
                    break;
                }
                else
                    log4(ctx, TRACE, "positive/inclusive ACL \"%s\" (%s/%d) matches %s: NO", ctx->option_acl[i].acl, cpA1, ctx->option_acl[i].prefixlen, cpA2);
                free(cpA1);
                free(cpA2);
            }
            /* check negative matches */
            for (i = 0; i < ctx->option_aclc; i++) {
                char *cpA1;
                char *cpA2;
                if (!ctx->option_acl[i].not)
                    continue;
                sa_addr_a2u(ctx->option_acl[i].saa, &cpA1);
                sa_addr_a2u(ctx->saaIO, &cpA2);
                if (sa_addr_match(ctx->saaIO, ctx->option_acl[i].saa, ctx->option_acl[i].prefixlen) == SA_OK) {
                    log4(ctx, TRACE, "negative/exclusive ACL \"%s\" (not %s/%d) matches %s: YES (stop comparison)", ctx->option_acl[i].acl, cpA1, ctx->option_acl[i].prefixlen, cpA2);
                    bOk = FALSE;
                    break;
                }
                else {
                    log4(ctx, TRACE, "negative/exclusive ACL \"%s\" (not %s/%d) matches %s: NO", ctx->option_acl[i].acl, cpA1, ctx->option_acl[i].prefixlen, cpA2);
                }
            }
            if (bOk) {
                char *cpA;
                sa_addr_a2u(ctx->saaIO, &cpA);
                log1(ctx, TRACE, "connection from %s accepted due to ACL", cpA); 
                free(cpA);
            }
            else {
                char *cpA;
                sa_addr_a2u(ctx->saaIO, &cpA);
                log1(ctx, ERROR, "connection from %s refused due to ACL", cpA); 
                free(cpA);
                sa_destroy(ctx->saIO);
                sa_addr_destroy(ctx->saaIO);
                continue;
            }

            /* logging buffer must be empty before fork otherwise content is
             * duplicated and written twice on next flush */
            l2_stream_flush(ctx->l2);
            pid = fork();
            if (pid == -1) {
                log0(ctx, ERROR, "daemon cannot spawn child %m");
                continue;
            }
            if (pid != 0) {
                log1(ctx, INFO, "daemon forked process, new child pid[%d]", pid);
                ctx->active_childs++;
                sa_destroy(ctx->saIO);
                sa_addr_destroy(ctx->saaIO);
                continue;
            }
            log1(ctx, NOTICE, "startup new child process, parent pid[%d]", getppid());

            /* child must close listening socket */
            sa_destroy(ctx->saAltio);
            ctx->saAltio = NULL; /* prevent cleanup from free'ing this again */
            
            /* initialize LMTP context */
            lmtp_io.ctx    = ctx;
            lmtp_io.read   = hook_lmtp_read;
            lmtp_io.write  = hook_lmtp_write;
            if ((lmtp = lmtp_create(&lmtp_io)) == NULL) {
                log0(ctx, ERROR, "Unable to initialize LMTP library\n");
                CU(ERR_EXECUTION);
            }
            /*  RFC0821, 4.5.1. MINIMUM IMPLEMENTATION
             *  In order to make SMTP workable, the following minimum implementation
             *  is required for all receivers: [...]
             *  RFC0821, 4.1.2. COMMAND SYNTAX
             *  
             *  Verb Parameter
             *  ----+-------------------------------
             *  HELO <SP> <domain> <CRLF>
             *  MAIL <SP> FROM:<reverse-path> <CRLF>
             *  RCPT <SP> TO:<forward-path> <CRLF>
             *  DATA <CRLF>
             *  RSET <CRLF>
             *  NOOP <CRLF>
             *  QUIT <CRLF>
             */
            lmtp_register(lmtp, "LHLO", lmtp_cb_lhlo, ctx, NULL, NULL); 
            lmtp_register(lmtp, "MAIL", lmtp_cb_mail, ctx, NULL, NULL);
            lmtp_register(lmtp, "RCPT", lmtp_cb_rcpt, ctx, NULL, NULL);
            lmtp_register(lmtp, "DATA", lmtp_cb_data, ctx, NULL, NULL);
            lmtp_register(lmtp, "RSET", lmtp_cb_rset, ctx, NULL, NULL);
            lmtp_register(lmtp, "NOOP", lmtp_cb_noop, ctx, NULL, NULL);
            lmtp_register(lmtp, "QUIT", lmtp_cb_quit, ctx, NULL, NULL);
            lmtp_loop(lmtp);
            lmtp_gfs_quit(ctx);
            lmtp_gfs_lhlo(ctx);
            lmtp_destroy(lmtp);
            CU(0);
        }
    }
    CU(0);

    /* graceful shutdown */
    CUS:
    log0(ctx, NOTICE, "graceful shutdown shortly before exit - no more logging");
    l2_stream_destroy(ctx->l2);
    if (ctx->saAltio)
        sa_destroy(ctx->saAltio);
    if (ctx->saaAltio)
        sa_addr_destroy(ctx->saaAltio);
    if (ctx->option_restrictheader != NULL)
        free(ctx->option_restrictheader);
    if (ctx->azHeaderValuePairs != NULL)
        free(ctx->azHeaderValuePairs);
    if (ctx->option_pidfile != NULL)
        free(ctx->option_pidfile);
    if (ctx->option_logfile != NULL)
        free(ctx->option_logfile);
    if (ctx->progname != NULL)
        free(ctx->progname);
    if (ctx->azGroupargs != NULL)
        free(ctx->azGroupargs);
    if (ctx != NULL)
        free(ctx);
    str_parse(NULL, NULL);

    return rc;
}

static void resetsession(struct session *session)
{
    if (session->lhlo_domain != NULL)
        free(session->lhlo_domain);
    initsession(session);
    return;
}

static void initsession(struct session *session)
{
    session->lhlo_seen = FALSE;
    session->lhlo_domain = NULL;
    return;
}

static lmtp_rc_t lmtp_cb_lhlo(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *_ctx)
{
    /*  
     *  RFC0821 [excerpt] 4.1. SMTP COMMANDS
     *  4.1.1.  COMMAND SEMANTICS, HELO
     *  This command and an OK reply to it confirm that both the sender-SMTP
     *  and the receiver-SMTP are in the initial state, that is, there is no
     *  transaction in progress and all state tables and buffers are cleared.
     * 
     *  The first command in a session must be the HELO command.  The HELO
     *  command may be used later in a session as well.  If the HELO command
     *  argument is not acceptable a 501 failure reply must be returned and
     *  the receiver-SMTP must stay in the same state.
     *
     *  If the transaction beginning command argument is not acceptable a 501
     *  failure reply must be returned and the receiver-SMTP must stay in the
     *  same state.  If the commands in a transaction are out of order a 503
     *  failure reply must be returned and the receiver-SMTP must stay in the
     *  same state.
     *
     *  HELO <SP> <domain> <CRLF>
     */
    lmtp2nntp_t *ctx = (lmtp2nntp_t *)_ctx;
    nntp_rc_t    rc;
    lmtp_res_t   res;
    char         str[STDSTRLEN];
    int          bOk;
    int          i;
    nntp_io_t    nntp_io;

    log1(ctx, INFO, "LMTP service executing LHLO command < %s", req->msg);

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   503 Bad sequence of commands
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.0.0   Other undefined Status
     */
    log0(ctx, TRACE, "checking for duplicate LHLO");
    if (ctx->session.lhlo_seen) {
        res.statuscode = "503";
        res.dsncode    = "5.0.0";
        res.statusmsg  = "Duplicate LHLO.";
        CU(LMTP_OK);
    }

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   501 Syntax error in parameters or arguments
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.0.0   Other undefined Status
     */
    log0(ctx, TRACE, "checking domain to match either RFC0821 or RFC1035 syntax");
    if (! (   helo_rfc0821domain(req->msg, &ctx->session.lhlo_domain) > 0
           || helo_rfc1035domain(req->msg, &ctx->session.lhlo_domain) > 0)) {
        res.statuscode = "501";
        res.dsncode    = "5.0.0";
        res.statusmsg  = "Please identify yourself. Domain must match RFC0821/RFC1035.";
        CU(LMTP_OK);
    }

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   451 Requested action aborted: local error in processing
     *  RFC1893 2. Status Codes                         4.X.X   Persistent Transient Failure
     *  RFC1893 3.5 Network and Routing Status          X.3.5   System incorrectly configured
     */
    if (ctx->option_operationmode != OPERATIONMODE_FAKE) {
        log0(ctx, TRACE, "check if at least one NNTP service was successfully configured");
        if (ctx->nsc == 0) {
            res.statuscode = "451";
            res.dsncode    = "4.3.5";
            res.statusmsg  = "No valid NNTP services configured.";
            CU(LMTP_OK);
        }
    }

    log0(ctx, TRACE, "try to establish a session to any configured NNTP services");
    if (ctx->option_operationmode == OPERATIONMODE_FAKE)
        log0(ctx, NOTICE, "NNTP running in fake mode, network connections will be executed but result is ignored");
    i = 0;
    do {
        log1(ctx, DEBUG, "trying ns[%d]", i);
        bOk = TRUE;
        log2(ctx, TRACE, "try %s:%s", ctx->ns[i].h, ctx->ns[i].p);

        ctx->ns[i].l2 = ctx->l2;

        if (bOk && (ctx->saaBind != NULL)) {
            log2(ctx, DEBUG, "bind local socket to %s:%s", ctx->cpBindh, ctx->cpBindp);
            if (sa_bind(ctx->ns[i].sa, ctx->saaBind) != SA_OK) {
                bOk = FALSE;
                log2(ctx, ERROR, "binding NNTP client to local address %s:%s failed, %m", ctx->cpBindh, ctx->cpBindp);
            }
        }

        sa_timeout(ctx->ns[i].sa, SA_TIMEOUT_ALL, 0, 0);
        sa_timeout(ctx->ns[i].sa, SA_TIMEOUT_CONNECT, ctx->option_timeout_nntp_connect, 0);
        sa_timeout(ctx->ns[i].sa, SA_TIMEOUT_READ,    ctx->option_timeout_nntp_read,    0);
        sa_timeout(ctx->ns[i].sa, SA_TIMEOUT_WRITE,   ctx->option_timeout_nntp_read,    0);

        if (bOk) {
            log0(ctx, DEBUG, "connect");
            if (sa_connect(ctx->ns[i].sa, ctx->ns[i].saa) != SA_OK) {
                bOk = FALSE;
                log2(ctx, WARNING, "connect to %s:%s failed, %m",
                     ctx->ns[i].h, ctx->ns[i].p);
            }
        }

        if (bOk) {
            log0(ctx, DEBUG, "nntp_create");
            nntp_io.ctx    = &ctx->ns[i];
            nntp_io.read   = hook_nntp_read;
            nntp_io.write  = hook_nntp_write;
            if ((ctx->ns[i].nntp = nntp_create(&nntp_io)) == NULL) {
                bOk = FALSE;
                log0(ctx, ERROR, "creation of NNTP context failed");
            }
        }

        if (bOk) {
            log0(ctx, DEBUG, "nntp_init");
            if ((rc = nntp_init(ctx->ns[i].nntp)) != NNTP_OK) {
                bOk = FALSE;
                log2(ctx, ERROR, "initialization of NNTP context failed, (%d) %s", rc, nntp_error(rc));
            }
        }

        if (bOk) {
            log2(ctx, INFO, "NNTP session to %s:%s successfully established", ctx->ns[i].h, ctx->ns[i].p);
            i++;
        }
        else {
            log2(ctx, WARNING, "NNTP session establishment to %s:%s failed", ctx->ns[i].h, ctx->ns[i].p);
            log1(ctx, DEBUG, "removing ns[%d] from list", i);
            lmtp_gfs_ns(&ctx->ns[i]);
            if (i < --ctx->nsc) {
                memcpy(&ctx->ns[i], &ctx->ns[i+1], (ctx->nsc - i ) * sizeof(struct ns));
            }
        }
    } while (i < ctx->nsc);

    if (ctx->option_operationmode == OPERATIONMODE_FAKE)
        log1(ctx, NOTICE, "NNTP running in fake mode, network connections successfully established=%d but ignored", ctx->nsc);
    else
    {
        /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   421 <domain> Service not available
         *  RFC1893 2. Status Codes                         4.X.X   Persistent Transient Failure
         *  RFC1893 3.5 Network and Routing Status          X.4.1   No answer from host
         */
        log0(ctx, DEBUG, "check if at least one NNTP session successfully established");
        if (ctx->nsc == 0) {
            log0(ctx, ERROR, "no NNTP session established");
            res.statuscode = "421";
            res.dsncode    = "4.4.1";
            res.statusmsg  = "No NNTP session established.";
            CU(LMTP_OK);
        }
    }
        
    ctx->session.lhlo_seen = TRUE;

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   250 Requested mail action okay, completed
     */
    str_format(str, sizeof(str),
               "%s Hello %s, pleased to meet you.\n" /* RFC2821 4.1.1.1 */
               "ENHANCEDSTATUSCODES\n"               /* RFC2034 */
               "DSN\n"                               /* RFC1894 */
               "PIPELINING\n"                        /* RFC1854 */
               "8BITMIME",                           /* RFC1652 */
               ctx->uname.nodename,
               ctx->session.lhlo_domain);
    res.statuscode = "250";
    res.dsncode    = NULL; /* DSN not used for greeting */
    res.statusmsg  = str;
    CU(LMTP_OK);

    CUS:
    lmtp_response(lmtp, &res);
    return rc;
}

static void lmtp_gfs_ns(struct ns *ns)
{
    if (ns->nntp != NULL) {
        nntp_destroy(ns->nntp);
        ns->nntp = NULL;
    }
    if (ns->sa != NULL) {
        sa_destroy(ns->sa);
        ns->sa = NULL;
    }
    if (ns->saa != NULL) {
        sa_addr_destroy(ns->saa);
        ns->saa = NULL;
    }
    if (ns->p != NULL) {
        free(ns->p);
        ns->p = NULL;
    }
    if (ns->h != NULL) {
        free(ns->h);
        ns->h = NULL;
    }
}

static void lmtp_gfs_lhlo(lmtp2nntp_t *ctx)
{
    int i;

    log0(ctx, TRACE, "LMTP service LHLO command - graceful shutdown");

    for (i = 0; i < ctx->nsc; i++)
            lmtp_gfs_ns(&ctx->ns[i]);

    if (ctx->option_mailfrom != NULL)
        free(ctx->option_mailfrom);
    if (ctx->cpBindh != NULL)
        free(ctx->cpBindh);
    if (ctx->cpBindp != NULL)
        free(ctx->cpBindp);
    if (ctx->saBind != NULL)
        sa_destroy(ctx->saBind);
    if (ctx->saaBind != NULL)
        sa_addr_destroy(ctx->saaBind);
}

static int helo_rfc0821domain(char *msg, char **domain)
{
    int rc;

    rc = str_parse(msg, 
            "^.+ ("
    /*
     ##
     ##  The mega Perl regular expression below is generated
     ##  with the following Perl program. This is only possible
     ##  because the given grammar is Chomsky-3 (right or left
     ##  linear grammar, but noth both).
     ##
    
     # BNF grammar for <domain> according to RFC0821:
     # <snum>        ::= one, two, or three digits representing a decimal integer value in the range 0 through 255
     # <a>           ::= any one of the 52 alphabetic characters A through Z in upper case and a through z in lower case
     # <d>           ::= any one of the ten digits 0 through 9
     # <let-dig-hyp> ::= <a> | <d> | "-"
     # <let-dig>     ::= <a> | <d>
     # <ldh-str>     ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
     # <dotnum>      ::= <snum> "." <snum> "." <snum> "." <snum>
     # <number>      ::= <d> | <d> <number>
     # <name>        ::= <a> <ldh-str> <let-dig>
     # <element>     ::= <name> | "#" <number> | "[" <dotnum> "]"
     # <domain>      ::= <element> | <element> "." <domain>
     #
     # corresponding Perl regular expression ($domain)
     $snum        = "(?:[0-9]|[0-9]{2}|[0-1][0-9]{2}|2[0-4][0-9]|25[0-5])";
     $d           = "[0-9]";
     $a           = "[A-Za-z]";
     $let_dig_hyp = "(?:$a|$d|-)";
     $let_dig     = "(?:$a|$d)";
     $ldh_str     = "${let_dig_hyp}+";
     $dotnum      = "$snum\\.$snum\\.$snum\\.$snum";
     $number      = "$d+";
     $name        = "$a$ldh_str$let_dig";
     $element     = "(?:$name|#$number|\\[$dotnum\\])";
     $domain      = "(?:$element\.)*$element";
     #
     # translate into C string block suitable for passing to the Perl
     # Compatible Regular Expressions (PCRE) based string library Str.
     my $cregex = $domain;
     $cregex .= "\n";
     $cregex =~ s|\\|\\\\|sg;
     $cregex =~ s|(.{17})|$1\n|sg;
     $cregex =~ s|([^\n]+)\n|"$1"\n|sg;
     $cregex =~ s|\n\n|\n|sg;
     print "$cregex";
     */

    "(?:(?:[A-Za-z](?:[A-Za-z]|[0-9]|-)+(?:[A-Za-z]|[0-9])|#[0-9]+|\\[(?:[0"
    "-9]|[0-9]{2}|[0-1][0-9]{2}|2[0-4][0-9]|25[0-5])\\.(?:[0-9]|[0-9]{2}|[0"
    "-1][0-9]{2}|2[0-4][0-9]|25[0-5])\\.(?:[0-9]|[0-9]{2}|[0-1][0-9]{2}|2[0"
    "-4][0-9]|25[0-5])\\.(?:[0-9]|[0-9]{2}|[0-1][0-9]{2}|2[0-4][0-9]|25[0-5"
    "])\\]).)*(?:[A-Za-z](?:[A-Za-z]|[0-9]|-)+(?:[A-Za-z]|[0-9])|#[0-9]+|\\"
    "[(?:[0-9]|[0-9]{2}|[0-1][0-9]{2}|2[0-4][0-9]|25[0-5])\\.(?:[0-9]|[0-9]"
    "{2}|[0-1][0-9]{2}|2[0-4][0-9]|25[0-5])\\.(?:[0-9]|[0-9]{2}|[0-1][0-9]{"
    "2}|2[0-4][0-9]|25[0-5])\\.(?:[0-9]|[0-9]{2}|[0-1][0-9]{2}|2[0-4][0-9]|"
    "25[0-5])\\])"

    ")$", domain);
    return rc;
}

static int helo_rfc1035domain(char *msg, char **domain)
{
    int rc;

    rc = str_parse(msg, 
            "^.+ ("
    /*
     ##
     ##  The mega Perl regular expression below is generated
     ##  with the following Perl program. This is only possible
     ##  because the given grammar is Chomsky-3 (right or left
     ##  linear grammar, but noth both).
     ##

     # BNF grammar for <domain> according to RFC1035:
     # <letter>      ::= any one of the 52 alphabetic characters A through Z in upper case and a through z in lower case
     # <digit>       ::= any one of the ten digits 0 through 9
     # <let-dig>     ::= <letter> | <digit>
     # <let-dig-hyp> ::= <let-dig> | "-"
     # <ldh-str>     ::= <let-dig-hyp> | <let-dig-hyp> <ldh-str>
     # <label>       ::= <letter> [ [ <ldh-str> ] <let-dig> ]
     # <subdomain>   ::= <label> | <subdomain> "." <label>
     # <domain>      ::= <subdomain> | " "
     #
     # corresponding Perl regular expression ($domain)
     $letter      = "[A-Za-z]";
     $digit       = "[0-9]";
     $let_dig     = "(?:$letter|$digit)";
     $let_dig_hyp = "(?:$let_dig|-)";
     $ldh_str     = "${let_dig_hyp}+";
     $label       = "(?:$letter(?:(?:$ldh_str)?$let_dig)?)";
     $subdomain   = "(?:$label\.)*$label";
     $domain      = "(?:$subdomain| )";
     #
     # translate into C string block suitable for passing to the Perl
     # Compatible Regular Expressions (PCRE) based string library Str.
     my $cregex = $domain;
     $cregex .= "\n";
     $cregex =~ s|\\|\\\\|sg;
     $cregex =~ s|(.{17})|$1\n|sg;
     $cregex =~ s|([^\n]+)\n|"$1"\n|sg;
     $cregex =~ s|\n\n|\n|sg;
     print "$cregex";
     */

    "(?:(?:(?:[A-Za-z](?:(?:(?:(?:[A-Za-z]|[0-9])|-)+)?(?:[A-Za-z]|[0-9]))?"
    ").)*(?:[A-Za-z](?:(?:(?:(?:[A-Za-z]|[0-9])|-)+)?(?:[A-Za-z]|[0-9]))?)|"
    " )"

    ")$", domain);
    return rc;
}

static lmtp_rc_t lmtp_cb_mail(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *_ctx)
{
    lmtp2nntp_t *ctx = (lmtp2nntp_t *)_ctx;
    lmtp_rc_t    rc;
    lmtp_res_t   res;

    log1(ctx, INFO, "LMTP service executing MAIL command < %s", req->msg);

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   553 Requested action not taken: mailbox name not allowed
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.1.8   Bad sender's system address
     */
    log0(ctx, TRACE, "checking for previous LHLO");
    if (!ctx->session.lhlo_seen) {
        res.statuscode = "553";
        res.dsncode    = "5.1.8";
        res.statusmsg  = "friendly people say LHLO to open a transmission channel.";
        CU(LMTP_OK);
    }

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   503 Bad sequence of commands
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.5.0   Other or undefined protocol status
     */
    log0(ctx, TRACE, "checking for previous MAIL");
    if (ctx->msg != NULL) {
        res.statuscode = "503";
        res.dsncode    = "5.5.0";
        res.statusmsg  = "Sender already specified.";
        CU(LMTP_OK);
    }

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   452 Requested action not taken: insufficient system storage
     *  RFC1893 2. Status Codes                         4.X.X   Persistent Transient Failure
     *  RFC1893 3.5 Network and Routing Status          X.3.1   Mail system full 
     */
    log0(ctx, TRACE, "msg_create");
    if ((ctx->msg = msg_create()) == NULL) {
        res.statuscode = "452";
        res.dsncode    = "4.3.1";
        res.statusmsg  = "Internal error - memory.";
        CU(LMTP_ERR_MEM);
    }
    ctx->msg->l2 = ctx->l2;

    /*  RFC1652 2. Framework for the 8bit MIME Transport Extension
     *  (4)  one optional parameter using the keyword BODY is added to the MAIL
     *  FROM command.  The value associated with this parameter is a keyword
     *  indicating whether a 7bit message [...] or a MIME message [...] is
     *  being sent. The syntax of the value is as follows, using the ABNF
     *  notation [...]
     *  
     *  body-value ::= "7BIT" / "8BITMIME"
     *  
     *  "MAIL From:<foo@bar>"
     *  "MAIL From:<foo@bar> BODY=8BITMIME"
     *  "MAIL From:<foo@bar> BODY=7BIT"
     *  
     *  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   553 Requested action not taken: mailbox name not allowed
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.1.7   Bad sender's mailbox address syntax
     */
    log0(ctx, TRACE, "checking if sender address is a domain name");
    if (str_parse(req->msg, "m/^MAIL From:\\s*<(?:.+@.+)>/i") <= 0) {
        res.statuscode = "553";
        res.dsncode    = "5.1.7";
        res.statusmsg  = "Domain name required for sender address.";
        CU(LMTP_OK);
    }
    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   501 Syntax error in parameters or arguments
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.5.4   Invalid command arguments
     */
    log0(ctx, TRACE, "checking BODY keyword");
    if (str_parse(req->msg, "m/^MAIL From:\\s*<(.+@.+)>"
                            "(?:\\s+BODY=(?:7BIT|8BITMIME)\\s*)?$/i", 
                            &ctx->msg->mail_from) <= 0) {
        res.statuscode = "501";
        res.dsncode    = "5.5.4";
        res.statusmsg  = "Unknown parameter for keyword BODY.";
        CU(LMTP_OK);
    }
    
    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   550 Requested action not taken: mailbox unavailable
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.7.1   Delivery not authorized, message refused
     */
    log0(ctx, TRACE, "checking if sender is allowed");
    if (ctx->option_mailfrom != NULL) {
        log2(ctx, TRACE, "\"%s\" matching against \"%s\"", ctx->msg->mail_from, ctx->option_mailfrom);
        if (str_parse(ctx->msg->mail_from, ctx->option_mailfrom) <= 0) {
            res.statuscode = "550";
            res.dsncode    = "5.7.1";
            res.statusmsg  = "Delivery not authorized, message refused.";
            CU(LMTP_OK);
        }
    }

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   250 Requested mail action okay, completed
     *  RFC1893 2. Status Codes                         2.X.X   Success
     *  RFC1893 3.5 Network and Routing Status          X.1.0   Other address status
     */
    res.statuscode = "250";
    res.dsncode    = "2.1.0";
    res.statusmsg  = "Sender ok.";
    lmtp_response(lmtp, &res);
    return LMTP_OK;

    CUS:
    lmtp_response(lmtp, &res);
    if (ctx->msg != NULL) {
        msg_destroy(ctx->msg);
        ctx->msg = NULL;
    }
    return rc;
}

static lmtp_rc_t lmtp_cb_rcpt(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *_ctx)
{
    lmtp_res_t   res;
    lmtp_rc_t    rc;
    lmtp2nntp_t *ctx = (lmtp2nntp_t *)_ctx;
    char        *cp;
    char        *group;

    log1(ctx, INFO, "LMTP service executing RCPT command < %s", req->msg);

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   503 Bad sequence of commands
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.5.0   Other or undefined protocol status
     */
    log0(ctx, TRACE, "checking for previous MAIL");
    if ((ctx->msg == NULL) || (ctx->msg->mail_from == NULL)) {
        res.statuscode = "503";
        res.dsncode    = "5.5.0";
        res.statusmsg  = "specify sender with MAIL first.";
        CU(LMTP_OK);
    }

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   501 Syntax error in parameters or arguments
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.5.2   Syntax error
     */
    log0(ctx, TRACE, "checking parameter syntax");
    if (str_parse(req->msg, "m/^RCPT To:\\s*(.+)$/i", &cp) <= 0) {
        res.statuscode = "501";
        res.dsncode    = "5.5.2";
        res.statusmsg  = "Syntax error in parameters.";
        CU(LMTP_OK);
    }
    
    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   550 Requested action not taken: mailbox unavailable
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.1.1   Bad destination mailbox address
     */
    log0(ctx, TRACE, "checking for empty parameter");
    if ((cp == NULL) || (strlen(cp) == 0)) {
        res.statuscode = "550";
        res.dsncode    = "5.1.1";
        res.statusmsg  = "empty Recipient/ Group.";
        CU(LMTP_OK);
    }

    /* in GROUPMODE = ARG|HEADER recipient must be acknowledged and stored to
     * give proper pipelining responses.  in GROUPMODE = ENVELOPE recipient is
     * transformed into a group and matched against groupfilter. Only valid
     * groups are stored to give proper pipelining responses.
     *
     *  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   550 Requested action not taken: mailbox unavailable
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.1.1   Bad destination mailbox address
     *                                                  X.7.2   Mailing list expansion prohibited
     */
    log1(ctx, DEBUG, "ctx->option_groupmode=%d", ctx->option_groupmode);
    if (ctx->option_groupmode == GROUPMODE_ENVELOPE) {
        log0(ctx, TRACE, "groupmode=envelope; transform recipient into group");
        if (str_parse(cp, "m/^<(.+)?@[^@]+>$/i", &group) <= 0) {
            res.statuscode = "550";
            res.dsncode    = "5.1.1";
            res.statusmsg  = "Recipient did not transform into group.";
            CU(LMTP_OK);
        }
        log1(ctx, TRACE, "groupmode=envelope; match group %s", group);
        if (!groupmatch(ctx->azGroupargs, ctx->asGroupargs, group)) {
            res.statuscode = "550";
            res.dsncode    = "5.7.2";
            res.statusmsg  = "unmatched Group.";
            CU(LMTP_OK);
        }
        log1(ctx, TRACE, "memorize group %s", group);
        argz_add(&ctx->msg->azEnvgroups, &ctx->msg->asEnvgroups, group);
    }
    log1(ctx, TRACE, "memorize recipient %s", cp);
    argz_add(&ctx->msg->azRcpt, &ctx->msg->asRcpt, cp);

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   250 Requested mail action okay, completed
     *  RFC1893 2. Status Codes                         2.X.X   Success
     *  RFC1893 3.5 Network and Routing Status          X.1.5   Destination address valid
     */
    res.statuscode = "250";
    res.dsncode    = "2.1.5";
    res.statusmsg  = ctx->option_groupmode == GROUPMODE_ENVELOPE ? "Group accepted." : "Recipient accepted.";
    CU(LMTP_OK);

    CUS:
    lmtp_response(lmtp, &res);
    return rc;
}

int groupmatch(char *azPattern, size_t asPattern, char *cpGroup)
{
    int bGroupmatch;
    char *cpGroupmatch;

    bGroupmatch = FALSE;
    cpGroupmatch = NULL;
    while ((cpGroupmatch = argz_next(azPattern, asPattern, cpGroupmatch)) != NULL) {
        if (shpat_match(cpGroupmatch, cpGroup, 0) == 0)
            bGroupmatch = TRUE;
    }
    return bGroupmatch;
}

static lmtp_rc_t lmtp_cb_data(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *_ctx)
{
    lmtp2nntp_t *ctx = (lmtp2nntp_t *)_ctx;
    lmtp_rc_t    rc = LMTP_OK;
    lmtp_res_t   res;
    char        *azErr;
    size_t       asErr;
    char         errorstring[STDSTRLEN];
    char        *rcpt;
    int          i;
    int          bSuccess;
    char        *cp;
    int          bOk;
    char        *cpRestrictheader;
    char        *cpRestrictvalue;

    log1(ctx, INFO, "LMTP service executing DATA command < %s", req->msg);

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   503 Bad sequence of commands
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.5.0   Other or undefined protocol status
     */
    log0(ctx, TRACE, "checking for previous RCPT");
    if ((ctx->msg == NULL) || (argz_count(ctx->msg->azRcpt, ctx->msg->asRcpt) == 0)) {
        res.statuscode = "503";
        res.dsncode    = "5.5.0";
        res.statusmsg  = "specify recipient with RCPT first.";
        lmtp_response(lmtp, &res);
        return LMTP_OK;
    }

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   354 Start mail input; end with <CRLF>.<CRLF>
     */
    log0(ctx, TRACE, "tell remote to send message now");
    res.statuscode = "354";
    res.dsncode    = NULL; /* DSN not used for data */
    res.statusmsg  = "Enter mail, end with \".\" on a line by itself";
    lmtp_response(lmtp, &res);

    log1(ctx, TRACE, "read message with maximum size to accept = %d", ctx->option_maxmessagesize);
    rc = lmtp_readmsg(lmtp, &ctx->msg->cpMsg, ctx->option_maxmessagesize);

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   552 Requested mail action aborted: exceeded storage allocation
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.2.3   Message length exceeds administrative limit.
     */
    log0(ctx, TRACE, "checking for excessive message size");
    if (rc == LMTP_ERR_OVERFLOW) {
        str_format(errorstring, sizeof(errorstring), "Message length exceeds administrative limit. %s", lmtp_error(rc));
        res.statuscode = "552";
        res.dsncode    = "5.2.3";
        res.statusmsg  = errorstring;
        rcpt = NULL;
        while ((rcpt = argz_next(ctx->msg->azRcpt, ctx->msg->asRcpt, rcpt)) != NULL) {
            lmtp_response(lmtp, &res);
        }
        return LMTP_OK;
    }

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   451 Requested action aborted: local error in processing
     *  RFC1893 2. Status Codes                         4.X.X   Persistent Transient Failure
     *  RFC1893 3.5 Network and Routing Status          X.3.2   System not accepting network messages
     */
    log0(ctx, TRACE, "checking for system error");
    if (rc == LMTP_ERR_SYSTEM) {
        str_format(errorstring, sizeof(errorstring), "System error reading message: %s", strerror(errno));
        res.statuscode = "451";
        res.dsncode    = "4.3.2";
        res.statusmsg  = errorstring;
        rcpt = NULL;
        while ((rcpt = argz_next(ctx->msg->azRcpt, ctx->msg->asRcpt, rcpt)) != NULL) {
            lmtp_response(lmtp, &res);
        }
        return LMTP_OK;
    }

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   451 Requested action aborted: local error in processing
     *  RFC1893 2. Status Codes                         4.X.X   Persistent Transient Failure
     *  RFC1893 3.5 Network and Routing Status          X.3.2   System not accepting network messages
     */
    log0(ctx, TRACE, "checking for other error");
    if(rc != LMTP_OK) {
        str_format(errorstring, sizeof(errorstring), "Unknown error reading message: %s", lmtp_error(rc));
        res.statuscode = "451";
        res.dsncode    = "4.3.2";
        res.statusmsg  = errorstring;
        rcpt = NULL;
        while ((rcpt = argz_next(ctx->msg->azRcpt, ctx->msg->asRcpt, rcpt)) != NULL) {
            lmtp_response(lmtp, &res);
        }
        return LMTP_OK;
    }

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   554 Transaction failed
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.6.5   Conversion Failed
     */
    log0(ctx, TRACE, "split message");
    if ((rc = msg_split(ctx->msg)) != MSG_OK) {
        str_format(errorstring, sizeof(errorstring), "Error splitting message: %s", msg_error(rc));
        res.statuscode = "554";
        res.dsncode    = "5.6.5";
        res.statusmsg  = errorstring;
        rcpt = NULL;
        while ((rcpt = argz_next(ctx->msg->azRcpt, ctx->msg->asRcpt, rcpt)) != NULL) {
            lmtp_response(lmtp, &res);
        }
        return LMTP_OK;
    }

    if      (ctx->option_groupmode == GROUPMODE_ENVELOPE) {
        if ((cp = malloc(ctx->msg->asEnvgroups + 1)) == NULL) {
            /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   452 Requested action not taken: insufficient system storage
             *  RFC1893 2. Status Codes                         4.X.X   Persistent Transient Failure
             *  RFC1893 3.5 Network and Routing Status          X.3.1   Mail system full 
             */
            if ((ctx->msg = msg_create()) == NULL) {
                res.statuscode = "452";
                res.dsncode    = "4.3.1";
                res.statusmsg  = "Internal error - memory.";
                lmtp_response(lmtp, &res);
                return LMTP_ERR_MEM;
            }
        }
        ctx->msg->azNewsgroups = memcpy(cp, ctx->msg->azEnvgroups, ctx->msg->asEnvgroups);
        ctx->msg->asNewsgroups =                                   ctx->msg->asEnvgroups;
    }
    else if (ctx->option_groupmode == GROUPMODE_ARG) {
        if ((cp = malloc(ctx->asGroupargs + 1)) == NULL) {
            /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   452 Requested action not taken: insufficient system storage
             *  RFC1893 2. Status Codes                         4.X.X   Persistent Transient Failure
             *  RFC1893 3.5 Network and Routing Status          X.3.1   Mail system full 
             */
            if ((ctx->msg = msg_create()) == NULL) {
                res.statuscode = "452";
                res.dsncode    = "4.3.1";
                res.statusmsg  = "Internal error - memory.";
                lmtp_response(lmtp, &res);
                return LMTP_ERR_MEM;
            }
        }
        ctx->msg->azNewsgroups = memcpy(cp, ctx->azGroupargs, ctx->asGroupargs);
        ctx->msg->asNewsgroups =                              ctx->asGroupargs;
    }
    else { /*                      == GROUPMODE_HEADER */
        cp = ctx->msg->azNewsgroups;
        while (cp != NULL) {
            if (!groupmatch(ctx->azGroupargs, ctx->asGroupargs, cp)) {
                if (argz_next(ctx->msg->azNewsgroups, ctx->msg->asNewsgroups, cp) == NULL) {
                    argz_delete(&ctx->msg->azNewsgroups, &ctx->msg->asNewsgroups, cp);
                    break;
                }
                else
                    argz_delete(&ctx->msg->azNewsgroups, &ctx->msg->asNewsgroups, cp);
            } else {
                cp = argz_next(ctx->msg->azNewsgroups, ctx->msg->asNewsgroups, cp);
            }
        }
        /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   550 Requested action not taken: mailbox unavailable
         *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
         *  RFC1893 3.5 Network and Routing Status          X.7.2   Mailing list expansion prohibited
         */
        if (ctx->msg->asNewsgroups == 0) {
            rcpt = NULL;
            while ((rcpt = argz_next(ctx->msg->azRcpt, ctx->msg->asRcpt, rcpt)) != NULL) {
                res.statuscode = "550";
                res.dsncode    = "5.7.2";
                res.statusmsg  = "Header did not match any valid group.";
                lmtp_response(lmtp, &res);
            }
            return LMTP_OK;
        }
    }

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   550 Requested action not taken: mailbox unavailable
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.7.1   Delivery not authorized, message refused
     */
    log0(ctx, TRACE, "checking if restricted header causes reject");
    if (ctx->option_restrictheader != NULL) {
        bOk = FALSE;
        cp = NULL;
        while ((cp = argz_next(ctx->msg->azHeaders, ctx->msg->asHeaders, cp)) != NULL) {
            cpRestrictheader = cp;
            if ((cp = argz_next(ctx->msg->azHeaders, ctx->msg->asHeaders, cp)) == NULL)
                break;
            cpRestrictvalue = cp;
            str_format(errorstring, sizeof(errorstring), "%s %s", cpRestrictheader, cpRestrictvalue);
            if (str_parse(errorstring, ctx->option_restrictheader) <= 0) {
                log2(ctx, TRACE, "\"%s\" matching against \"%s\" NO", errorstring, ctx->option_restrictheader);
            }
            else {
                log2(ctx, TRACE, "\"%s\" matching against \"%s\": YES", errorstring, ctx->option_restrictheader);
                bOk = TRUE;
                break;
            }
        }
        if (bOk) {
            log0(ctx, TRACE, "restricted header found");
            rcpt = NULL;
            while ((rcpt = argz_next(ctx->msg->azRcpt, ctx->msg->asRcpt, rcpt)) != NULL) {
                res.statuscode = "550";
                res.dsncode    = "5.7.1";
                res.statusmsg  = "Restricted header matched, message rejected.";
                lmtp_response(lmtp, &res);
            }
            return LMTP_OK;
        }
    }

    /* Optionally add command line specified Header/Value Pairs
     */
    log0(ctx, TRACE, "adding header/value pairs");
    if ((ctx->asHeaderValuePairs >= 1) && ((ctx->asHeaderValuePairs & 1) == 0)) {
        cp = NULL;
        while ((cp = argz_next(ctx->azHeaderValuePairs, ctx->asHeaderValuePairs, cp)) != NULL) {
            argz_add(&ctx->msg->azHeaders, &ctx->msg->asHeaders, cp);
        }
    }

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   554 Transaction failed
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.6.5   Conversion Failed
     */
    log0(ctx, TRACE, "join message");
    if ((rc = msg_join(ctx->msg)) != MSG_OK) {
        str_format(errorstring, sizeof(errorstring), "Error joining message: %s", msg_error(rc));
        res.statuscode = "554";
        res.dsncode    = "5.6.5";
        res.statusmsg  = errorstring;
        rcpt = NULL;
        while ((rcpt = argz_next(ctx->msg->azRcpt, ctx->msg->asRcpt, rcpt)) != NULL) {
            lmtp_response(lmtp, &res);
        }
        return LMTP_OK;
    }

    log0(ctx, TRACE, "deliver message");
    bSuccess = NNTP_ERR_DELIVERY; /* assume a hard error for the worst case */
    for (i = 0; i < ctx->nsc; i++) {
        switch (ctx->option_operationmode) {
            case OPERATIONMODE_FAKE:
                ctx->ns[i].rc = NNTP_FAKE;
                break;
            case OPERATIONMODE_POST:
                ctx->ns[i].rc = nntp_post(ctx->ns[i].nntp, ctx->msg);
                break;
            case OPERATIONMODE_FEED:
                ctx->ns[i].rc = nntp_feed(ctx->ns[i].nntp, ctx->msg);
                break;
        }
        if (ctx->ns[i].rc == NNTP_OK)
            bSuccess = NNTP_OK;
        if (   bSuccess != NNTP_OK
            && (
                   (ctx->ns[i].rc == NNTP_ERR_SYSTEM)
                || (ctx->ns[i].rc == NNTP_DEFER)
                  )
              )
            bSuccess = NNTP_DEFER;
    }

    if (ctx->option_operationmode == OPERATIONMODE_FAKE) {
        str_format(errorstring, sizeof(errorstring),
                   "NNTP running in fake mode, delivery of %s [%d bytes] %s but delivery status forced to",
                   ctx->msg->cpMsgid,
                   strlen(ctx->msg->cpMsg),
                   ((bSuccess == NNTP_OK)    ? "succeeded" :
                    (bSuccess == NNTP_DEFER) ? "deferred"  : "failed"));
        switch (ctx->option_operationmodefakestatus[0]) {
            case '5':
                bSuccess = NNTP_ERR_UNKNOWN;
                log2(ctx, NOTICE, "%s %s", errorstring, "failed");
                break;
            case '4':
                bSuccess = NNTP_DEFER;
                log2(ctx, NOTICE, "%s %s", errorstring, "deferred");
                break;
            default:
                bSuccess = NNTP_OK;
                log2(ctx, NOTICE, "%s %s", errorstring, "succeeded");
                break;
        }
    } else {
        str_format(errorstring, sizeof(errorstring), "%sdelivery of %s [%d bytes]", 
                   ((ctx->option_operationmode == OPERATIONMODE_POST) ? "post " :
                   (ctx->option_operationmode == OPERATIONMODE_FEED) ? "feed " : ""),
                   ctx->msg->cpMsgid,
                   strlen(ctx->msg->cpMsg));
        if (bSuccess == NNTP_OK)
            log2(ctx, NOTICE,  "%s %s", errorstring, "succeeded");
        else if(bSuccess == NNTP_DEFER)
            log2(ctx, WARNING, "%s %s", errorstring, "deferred");
        else
            log2(ctx, ERROR,   "%s %s", errorstring, "failed");
    }


    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   250 Requested mail action okay, completed
     *                                                  451 Requested action aborted: local error in processing
     *                                                  554 Transaction failed
     *  RFC1893 2. Status Codes                         2.X.X   Success
     *                                                  4.X.X   Persistent Transient Failure
     *                                                  5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.0.0   Other undefined Status
     *                                                  X.4.2   Bad connection
     */
    rcpt = NULL;
    while ((rcpt = argz_next(ctx->msg->azRcpt, ctx->msg->asRcpt, rcpt)) != NULL) {
        if (ctx->option_operationmode == OPERATIONMODE_FAKE) {
                    res.statuscode = ctx->option_operationmodefakestatus;
                    res.dsncode    = ctx->option_operationmodefakedsn;
                    str_format(errorstring, sizeof(errorstring),
                               "NNTP noop fake return for %s", rcpt);
        } else {
            switch (bSuccess) {
                case NNTP_OK:
                    str_format(errorstring, sizeof(errorstring),
                               "Message accepted for delivery to %s", rcpt);
                    res.statuscode = "250";
                    res.dsncode    = "2.0.0";
                    break;
                case NNTP_DEFER:
                    str_format(errorstring, sizeof(errorstring),
                               "Requested action aborted for %s, local error in processing.", rcpt);
                    res.statuscode = "451";
                    res.dsncode    = "4.4.2";
                    break;
                default:
                    str_format(errorstring, sizeof(errorstring),
                               "Error sending article for %s.", rcpt);
                    res.statuscode = "554";
                    res.dsncode    = "5.4.2";
                    break;
            }
        }
        azErr = NULL;
        asErr = 0;
        argz_add(&azErr, &asErr, errorstring);
        for (i = 0; i < ctx->nsc; i++) {
            if (ctx->ns[i].rc != NNTP_OK) {
                str_format(errorstring, sizeof(errorstring), 
                "%s:%s returned %s\n"
                "%s:%s lastresp \"%s\"", 
                ctx->ns[i].h, ctx->ns[i].p, nntp_error(ctx->ns[i].rc), 
                ctx->ns[i].h, ctx->ns[i].p, nntp_lastresp(ctx->ns[i].nntp));
                argz_add(&azErr, &asErr, errorstring);
            }
        }
        if (azErr != NULL) {
            argz_stringify(azErr, asErr, '\n');
            res.statusmsg  = azErr;
            lmtp_response(lmtp, &res);
            free(azErr);
            azErr = NULL;
            asErr = 0;
        }
        else {
            res.statusmsg  = errorstring;
            lmtp_response(lmtp, &res);
        }
    }

    msg_destroy(ctx->msg);
    ctx->msg = NULL;

    return LMTP_OK;
}

static lmtp_rc_t lmtp_cb_noop(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *_ctx)
{
    lmtp2nntp_t *ctx = (lmtp2nntp_t *)_ctx;
    lmtp_res_t res;
    lmtp_rc_t rc = LMTP_OK;

    log1(ctx, INFO, "LMTP service executing NOOP command < %s", req->msg);

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   250 Requested mail action okay, completed
     *  RFC1893 2. Status Codes                         2.X.X   Success
     *  RFC1893 3.5 Network and Routing Status          X.0.0   Other undefined Status
     */
    res.statuscode = "250";
    res.dsncode    = "2.0.0";
    res.statusmsg  = "OK. Nice talking to you.";
    lmtp_response(lmtp, &res);
    return rc;
}

static lmtp_rc_t lmtp_cb_rset(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *_ctx)
{
    lmtp2nntp_t *ctx = (lmtp2nntp_t *)_ctx;
    lmtp_res_t res;
    lmtp_rc_t rc = LMTP_OK;

    log1(ctx, INFO, "LMTP service executing RSET command < %s", req->msg);

    lmtp_gfs_rset(ctx);

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   250 Requested mail action okay, completed
     *  RFC1893 2. Status Codes                         2.X.X   Success
     *  RFC1893 3.5 Network and Routing Status          X.0.0   Other undefined Status
     */
    res.statuscode = "250";
    res.dsncode    = "2.0.0";
    res.statusmsg  = "Reset state.";
    lmtp_response(lmtp, &res);
    return rc;
}

static void lmtp_gfs_rset(lmtp2nntp_t *ctx)
{
    log0(ctx, TRACE, "LMTP service RSET command - graceful shutdown");

    if (ctx->msg != NULL) {
        msg_destroy(ctx->msg);
        ctx->msg = NULL;
    }
}

static lmtp_rc_t lmtp_cb_quit(lmtp_t *lmtp, lmtp_io_t *io, lmtp_req_t *req, void *_ctx)
{
    lmtp2nntp_t *ctx = (lmtp2nntp_t *)_ctx;
    lmtp_res_t res;
    lmtp_rc_t rc = LMTP_EOF;

    log1(ctx, INFO, "LMTP service executing QUIT command < %s", req->msg);

    lmtp_gfs_quit(ctx);

    /*  RFC0821 4.2.1. REPLY CODES BY FUNCTION GROUPS   221 <domain> Service closing transmission channel
     *  RFC1893 2. Status Codes                         2.X.X   Success
     *  RFC1893 3.5 Network and Routing Status          X.0.0   Other undefined Status
     */
    res.statuscode = "221";
    res.dsncode    = "2.0.0";
    res.statusmsg  = "LMTP Service closing transmission channel.";
    lmtp_response(lmtp, &res);
    return rc;
}

static void lmtp_gfs_quit(lmtp2nntp_t *ctx)
{
    log0(ctx, TRACE, "LMTP service QUIT command - graceful shutdown");

    lmtp_gfs_rset(ctx);
    resetsession(&ctx->session);
}
