/*
 * lmtp2nntp.c
 *
 * The lmtp2nntp program reads mail as a LMTP server and posts it to one or
 * more newsgroups using NNTP. It delivers the message immediately or fails.
 *
 * The OSSP Project, Cable & Wireless Deutschland GmbH
 * Thomas Lotterer, <thomas.lotterer@cw.com>
 *
 */

#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <sys/utsname.h>

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

/* own headers */
#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"
#include "trace.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 MESSAGE_MAXLEN 8*1024*1024
#define STDSTRLEN 128
#define MAXNEWSSERVICES 3

extern void lmtp_debug_dumplmtp(lmtp_t *lmtp);

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 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_t *sa;
    int s;          /* socket */
    nntp_t *nntp;
    nntp_rc_t rc;
};

typedef struct {
    int             option_verbose;
    int             option_tracing;
    int             option_groupmode;
    int             option_deliverymode;
    char           *option_deliverymodefakestatus;
    char           *option_deliverymodefakedsn;
    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_lhlo(lmtp2nntp_t *ctx);
static void lmtp_gfs_rset(lmtp2nntp_t *ctx);
static void lmtp_gfs_quit(lmtp2nntp_t *ctx);

enum {
    GROUPMODE_ARG,
    GROUPMODE_ENVELOPE,
    GROUPMODE_HEADER
};

enum {
    DELIVERYMODE_FAKE,
    DELIVERYMODE_POST,
    DELIVERYMODE_FEED
};

/*
 * print usage information
 */
static void usage(char *command)
{
    /*  use
     *  perl <lmtp2nntp.c -e 'while (<>) { if(m/\/\*POD (.*) .*\*\//) { $_=$1; s/.<(.*?)>/$1/g ; print "$_ " };}'
     *  to pull the USAGE string out of this source
     */
    fprintf(stderr, 
            "USAGE: %s "
            "lmtp2nntp [-d deliverymode] [-g groupmode] [-h host[:port]] "
            "[-t tracefile] [-v] [-V] newsgroup [newsgroup ...]"
            "\n",
            command);
    return;
}

int main(int argc, char **argv)
{
    int          rc = 0;
    lmtp_t      *lmtp;
    lmtp_io_t    lmtp_io;
    lmtp2nntp_t *ctx;
    int          i;             /* general purpose scratch int, index ... */
    char        *cp;            /* general purpose character pointer */
    char        *progname;
    char        *azHosts;
    size_t       asHosts;
    char        *cpHost;
    char        *cpPort;
    sa_t        *sa;

    progname = argv[0];

    /* create application context */
    if ((ctx = (lmtp2nntp_t *)malloc(sizeof(lmtp2nntp_t))) == NULL)
        exit(ERR_EXECUTION);
    ctx->option_verbose = FALSE;
    ctx->option_tracing = FALSE;
    ctx->option_groupmode = GROUPMODE_ARG;
    ctx->option_deliverymode = DELIVERYMODE_FAKE;
    ctx->option_deliverymodefakestatus = "553";   /* Requested action not taken: mailbox name not allowed */
    ctx->option_deliverymodefakedsn    = "5.7.1"; /* Delivery not authorized, message refused */
    ctx->nsc = 0;
    for (i=0; i < MAXNEWSSERVICES; i++) {
        ctx->ns[i].h = NULL;
        ctx->ns[i].p = NULL;
        ctx->ns[i].sa = NULL;
        ctx->ns[i].s = -1;
        ctx->ns[i].nntp = NULL;
        ctx->ns[i].rc = LMTP_ERR_UNKNOWN;
    }
    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", progname, strerror(errno));
        exit(ERR_EXECUTION);
    }

#if 1
    {
        char buf[1000];
        int bufused = 0;
        int tracefile;

        for (i=0; i<argc; i++)
            bufused+=sprintf(buf+bufused, "[%d]=\"%s\"\n", i, argv[i]);
        if ((tracefile = open("/tmp/t", O_CREAT|O_WRONLY|O_APPEND, 0664)) != -1) {
            write(tracefile, buf, bufused);
            close(tracefile);
        }
    }
#endif

    /*POD B<lmtp2nntp> */

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

    /* read in the arguments */
    while ((i = getopt(argc, argv, "d:g:h:t:vV")) != -1) {
        switch (i) {
            case 'd': /*POD [B<-d> I<deliverymode>] */
                if      (strcasecmp(optarg, "post") == 0)
                    ctx->option_deliverymode = DELIVERYMODE_POST;
                else if (strcasecmp(optarg, "feed") == 0)
                    ctx->option_deliverymode = DELIVERYMODE_FEED;
                else {
                    if (strlen(optarg) != 9) {
                        fprintf(stderr, "%s:Error: Invalid format or length \"%s\" to option -d\n", progname, optarg);
                        exit(ERR_EXECUTION);
                    }

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

                    optarg[3] = NUL;
                    ctx->option_deliverymodefakestatus = &optarg[0];
                    ctx->option_deliverymodefakedsn    = &optarg[4];

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

                    if (   (strlen(ctx->option_deliverymodefakedsn) != 5)
                        || !isdigit((int)ctx->option_deliverymodefakedsn[0])
                        || (ctx->option_deliverymodefakedsn[1] != '.')
                        || !isdigit((int)ctx->option_deliverymodefakedsn[2])
                        || (ctx->option_deliverymodefakedsn[3] != '.')
                        || !isdigit((int)ctx->option_deliverymodefakedsn[4])
                        || (ctx->option_deliverymodefakedsn[0] != ctx->option_deliverymodefakestatus[0])) {
                            fprintf(stderr, "%s:Error: Invalid dsn in format \"%s\" to option -d\n", progname, optarg);
                            exit(ERR_EXECUTION);
                        }
                    }
                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", progname, optarg);
                    exit(ERR_EXECUTION);
                }
                break;
            case 'h': /*POD [B<-h> I<host>[I<:port>]] */
                if (argz_create_sep(optarg, ',', &azHosts, &asHosts) != 0)
                    exit(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 -h\n", progname, ctx->nsc);
                        exit(ERR_EXECUTION);
                    }

                    /* parse host[:port] string into host and port */
                    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 ((sa = sa_create(SA_IP, "tcp",
                                        ctx->ns[ctx->nsc].h,
                                        ctx->ns[ctx->nsc].p)) == NULL) {
                        fprintf(stderr, "%s:Error: creating TCP socket address failed for \"%s:%s\": %s\n", 
                                progname, 
                                ctx->ns[ctx->nsc].h, 
                                ctx->ns[ctx->nsc].p, 
                                strerror(errno));
                        exit(ERR_EXECUTION);
                    }
                    ctx->ns[ctx->nsc].sa = sa;
                    if ((ctx->ns[ctx->nsc].s =
                         socket(sa->sa_buf->sa_family, SOCK_STREAM, sa->sa_proto)) == -1) {
                        fprintf(stderr, "%s:Error: Creating TCP socket failed for \"%s:%s\": %s\n", 
                                progname, 
                                ctx->ns[ctx->nsc].h, 
                                ctx->ns[ctx->nsc].p, 
                                strerror(errno));
                        exit(ERR_EXECUTION);
                    }
                    ctx->ns[ctx->nsc].nntp = NULL;
                    ctx->nsc++;
                }
                free(azHosts);
                break;
            case 't': /*POD [B<-t> I<tracefile>] */
                ctx->option_tracing = TRUE;
                trace_read (-1, optarg, 0);
                trace_write(-1, optarg, 0);
                break;
            case 'v': /*POD [B<-v>] (verbose)*/
                ctx->option_verbose = TRUE;
                break;
            case 'V': /*POD [B<-V>] (version)*/
                fprintf(stdout, "%s\n", lmtp2nntp_version.v_gnu);
                exit(0);
                break;
            case '?':
            default:
                usage(progname);
                exit(ERR_EXECUTION);
        }
    }
    /*POD I<newsgroup> [I<newsgroup> ...] */
    for (i = optind; i < argc; i++) {
        argz_add(&ctx->azGroupargs, &ctx->asGroupargs, argv[i]);
    }

    /* initialize LMTP context */
    lmtp_io.select = NULL;
    lmtp_io.read   = trace_read;
    lmtp_io.write  = trace_write;
    if ((lmtp = lmtp_create(STDIN_FILENO, STDOUT_FILENO, ctx->option_tracing ? &lmtp_io : NULL)) == NULL) {
        fprintf(stderr, "%s:Error: Unable to initialize LMTP library\n", progname);
        exit(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);
    
    /* loop for LMTP protocol */
    lmtp_loop(lmtp);

    /* graceful shutdown */
    lmtp_gfs_quit(ctx);
    lmtp_gfs_lhlo(ctx);
    lmtp_destroy(lmtp);
    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;
    lmtp_res_t res;
    nntp_rc_t rc;
    char str[STDSTRLEN];
    int bOk;
    int i;
    nntp_io_t nntp_io;

    nntp_io.select = NULL;
    nntp_io.read   = trace_read;
    nntp_io.write  = trace_write;

    /*  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
     */
    if (ctx->session.lhlo_seen) {
        res.statuscode = "503";
        res.dsncode    = "5.0.0";
        res.statusmsg  = "Duplicate LHLO.";
        lmtp_response(lmtp, &res);
        return 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
     */
    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.";
        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.5   System incorrectly configured
     */
    if (ctx->nsc == 0) {
        res.statuscode = "451";
        res.dsncode    = "4.3.5";
        res.statusmsg  = "No valid NNTP services configured.";
        lmtp_response(lmtp, &res);
        return LMTP_OK;
    }

    i = 0;
    do {
        bOk = TRUE;
        if (connect(ctx->ns[i].s, ctx->ns[i].sa->sa_buf, ctx->ns[i].sa->sa_len) < 0) {
            bOk = FALSE;
        }
        if (bOk && ((ctx->ns[i].nntp = nntp_create(ctx->ns[i].s, ctx->ns[i].s, 
                                                   ctx->option_tracing ? &nntp_io : NULL)) == NULL)) {
            bOk = FALSE;
        }
        if (bOk && ((rc = nntp_init(ctx->ns[i].nntp)) != NNTP_OK)) {
            bOk = FALSE;
        }
        if (bOk)
            i++;
        else {
            if (i < --ctx->nsc) {
                memcpy(&ctx->ns[i], &ctx->ns[i+1], (ctx->nsc - i ) * sizeof(struct ns));
            }
        }
    } while (i < ctx->nsc);

    /*  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
     */
    if (ctx->nsc == 0) {
        res.statuscode = "421";
        res.dsncode    = "4.4.1";
        res.statusmsg  = "All attempts connecting to NNTP services failed.";
        lmtp_response(lmtp, &res);
        return 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;
    lmtp_response(lmtp, &res);
    return LMTP_OK;
}

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

    for (i = 0; i < ctx->nsc; i++) {
        if (ctx->ns[i].nntp != NULL)
            nntp_destroy(ctx->ns[i].nntp);
        if (ctx->ns[i].s != -1)
            close(ctx->ns[i].s);
        if (ctx->ns[i].sa != NULL)
            sa_destroy(ctx->ns[i].sa);
        if (ctx->ns[i].p != NULL)
            free(ctx->ns[i].p);
        if (ctx->ns[i].h != NULL)
            free(ctx->ns[i].h);
    }
}

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_res_t res;

    /*  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
     */
    if (!ctx->session.lhlo_seen) {
        res.statuscode = "553";
        res.dsncode    = "5.1.8";
        res.statusmsg  = "friendly people say LHLO to open a transmission channel.";
        lmtp_response(lmtp, &res);
        return 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
     */
    if (ctx->msg != NULL) {
        res.statuscode = "503";
        res.dsncode    = "5.5.0";
        res.statusmsg  = "Sender already specified.";
        lmtp_response(lmtp, &res);
        return 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 
     */
    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;
    }

    /*  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
     */
    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.";
        lmtp_response(lmtp, &res);
        msg_destroy(ctx->msg);
        ctx->msg = NULL;
        return 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
     */
    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.";
        lmtp_response(lmtp, &res);
        msg_destroy(ctx->msg);
        ctx->msg = NULL;
        return 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;
}

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

    /*  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
     */
    if ((ctx->msg == NULL) || (ctx->msg->mail_from == NULL)) {
        res.statuscode = "503";
        res.dsncode    = "5.5.0";
        res.statusmsg  = "specify sender with MAIL first.";
        lmtp_response(lmtp, &res);
        return 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
     */
    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.";
        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.1.1   Bad destination mailbox address
     */
    if ((cp == NULL) || (strlen(cp) == 0)) {
        res.statuscode = "550";
        res.dsncode    = "5.1.1";
        res.statusmsg  = "nul Recipient/ Group.";
        lmtp_response(lmtp, &res);
        return 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
     */
    if (ctx->option_groupmode == GROUPMODE_ENVELOPE) {
        if (str_parse(cp, "m/^<(.+)?@[^@]+>$/i", &group) <= 0) {
            res.statuscode = "550";
            res.dsncode    = "5.1.1";
            res.statusmsg  = "Recipient did not transform into group.";
            lmtp_response(lmtp, &res);
            return LMTP_OK;
        }
        if (!groupmatch(ctx->azGroupargs, ctx->asGroupargs, group)) {
            res.statuscode = "550";
            res.dsncode    = "5.7.2";
            res.statusmsg  = "unmatched Group.";
            lmtp_response(lmtp, &res);
            return LMTP_OK;
        }
        argz_add(&ctx->msg->azEnvgroups, &ctx->msg->asEnvgroups, group);
    }
    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.";
    lmtp_response(lmtp, &res);
    return LMTP_OK;
}

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)
{
    lmtp_res_t res;
    lmtp_rc_t rc = LMTP_OK;
    lmtp2nntp_t *ctx = (lmtp2nntp_t *)_ctx;
    char   *azErr;
    size_t  asErr;
    char errorstring[STDSTRLEN];
    char *rcpt;
    int i;
    int bSuccess;
    char *cp;

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

    rc = lmtp_readmsg(lmtp, &ctx->msg->cpMsg, MESSAGE_MAXLEN);

    /*  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.3.4   Message too big for system
     */
    if (rc == LMTP_ERR_OVERFLOW) {
        str_format(errorstring, sizeof(errorstring), "Overflow reading message: %s", lmtp_error(rc));
        res.statuscode = "552";
        res.dsncode    = "5.3.4";
        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
     */
    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
     */
    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
     */
    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   554 Transaction failed
     *  RFC1893 2. Status Codes                         5.X.X   Permanent Failure
     *  RFC1893 3.5 Network and Routing Status          X.6.5   Conversion Failed
     */
    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;
    }

    bSuccess = NNTP_EOF; /* assume a hard error for the worst case */
    for (i = 0; i < ctx->nsc; i++) {
        switch (ctx->option_deliverymode) {
            case DELIVERYMODE_FAKE:
                break;
            case DELIVERYMODE_POST:
                ctx->ns[i].rc = nntp_post(ctx->ns[i].nntp, ctx->msg);
                break;
            case DELIVERYMODE_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_TIMEOUT)
                || (ctx->ns[i].rc == NNTP_ERR_SYSTEM)
                || (ctx->ns[i].rc == NNTP_DEFER)
                  )
              )
            bSuccess = NNTP_DEFER;
    }

    /*  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_deliverymode == DELIVERYMODE_FAKE) {
                    res.statuscode = ctx->option_deliverymodefakestatus;
                    res.dsncode    = ctx->option_deliverymodefakedsn;
                    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", ctx->ns[i].h, ctx->ns[i].p, nntp_error(ctx->ns[i].rc));
                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)
{
    lmtp_res_t res;
    lmtp_rc_t rc = 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.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;

    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)
{
    /* 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;

    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)
{
    /* graceful shutdown */
    lmtp_gfs_rset(ctx);
    resetsession(&ctx->session);
}
