/*
**  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>.
**
**  nntp.c: Network News Transfer Protocol (NNTP) client library
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <ctype.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/time.h>

#include "nntp.h"

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#if defined(HAVE_DMALLOC_H) && defined(DMALLOC)
#include "dmalloc.h"
#endif

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

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

/* maximum NNTP protocol line length */
#define NNTP_LINE_MAXLEN 1024

typedef struct {
    int   rl_cnt;
    char *rl_bufptr;
    char  rl_buf[NNTP_LINE_MAXLEN];
} nntp_readline_t;

struct nntp_st {
    nntp_io_t       io;
    nntp_readline_t rl; 
    struct timeval  tv;
    int             kludgeinn441dup;
    char           *lastresp;
};

ssize_t nntp_fd_read(void *_ctx, void *buf, size_t buflen)
{
    nntp_fd_t *ctx = (nntp_fd_t *)_ctx;
    return read(ctx->fd, buf, buflen);
}

ssize_t nntp_fd_write(void *_ctx, const void *buf, size_t buflen)
{
    nntp_fd_t *ctx = (nntp_fd_t *)_ctx;
    return write(ctx->fd, buf, buflen);
}

nntp_t *nntp_create(nntp_io_t *io)
{
    nntp_t *nntp;

    if ((nntp = (nntp_t *)malloc(sizeof(nntp_t))) == NULL) 
        return NULL;

    if (io == NULL) 
        return NULL;
    nntp->io.ctx    = io->ctx;
    nntp->io.read   = io->read;
    nntp->io.write  = io->write;
    nntp->rl.rl_cnt = 0;
    nntp->rl.rl_bufptr = NULL;
    nntp->rl.rl_buf[0] = NUL;
    nntp->kludgeinn441dup = FALSE;
    nntp->lastresp = NULL;

    return nntp;
}

nntp_rc_t nntp_init(nntp_t *nntp)
{
    nntp_rc_t rc;
    char line[NNTP_LINE_MAXLEN];
  
    /* RFC0977 2.4.3. General Responses
     * In general, 1xx codes may be ignored or displayed as desired;  code 200
     * or 201 is sent upon initial connection to the NNTP server depending
     * upon posting permission; code 400 will be sent when the NNTP server
     * discontinues service (by operator request, for example); and 5xx codes
     * indicate that the command could not be performed for some unusual
     * reason.
     *
     * 1xx text
     * 200 server ready - posting allowed
     * 201 server ready - no posting allowed
     * 400 service discontinued
     * 500 command not recognized
     * 501 command syntax error
     * 502 access restriction or permission denied
     * 503 program fault - command not performed
     */

    do {
        if ((rc = nntp_readline(nntp, line, sizeof(line))) != NNTP_OK) {
            return rc;
            }
    } while (line[0] == '1');

    /* prepare for the INN kludge using 441 returns for other things but
     * "Duplicate".  Typical welcome string is "200 host InterNetNews NNRP
     * server INN 2.3.2 ready (posting ok)."
     */
    if (strncmp(line, "200", 3) == 0 && strstr(line, " INN ") != NULL) {
        nntp->kludgeinn441dup = TRUE;
    }
    else {
        nntp->kludgeinn441dup = FALSE;
    }

    if (strncmp(line, "200", 3) == 0)
        return NNTP_OK;

    return NNTP_ERR_INIT;
}

void nntp_destroy(nntp_t *nntp)
{
    if (nntp != NULL) {
        if (nntp->lastresp != NULL)
            free(nntp->lastresp);
        free(nntp);
    }
    return;
}

char *nntp_lastresp(nntp_t *nntp)
{
    if (nntp == NULL)
        return "";
    if(nntp->lastresp == NULL)
        return "";
    return nntp->lastresp;
}

nntp_rc_t nntp_readline(nntp_t *nntp, char *buf, size_t buflen)
{
    /* read a line (characters until NL) from input stream */
    size_t n;
    char c;
    nntp_readline_t *rl = &nntp->rl;

    if (nntp == NULL)
        return NNTP_ERR_ARG;
    for (n = 0; n < buflen-1;) {

        /* fetch one character (but read more) */
        if (rl->rl_cnt <= 0) {
            do {
                rl->rl_cnt = nntp->io.read(nntp->io.ctx, rl->rl_buf, NNTP_LINE_MAXLEN);
            } while (rl->rl_cnt == -1 && errno == EINTR);
            if (rl->rl_cnt == -1)
                return NNTP_ERR_SYSTEM;
            if (rl->rl_cnt == 0)
                return NNTP_EOF;
            rl->rl_bufptr = rl->rl_buf;
        }

        /* act on fetched character */
        rl->rl_cnt--;
        c = *rl->rl_bufptr++;
        if (c == '\r')
            continue;       /* skip copying CR */
        if (c == '\n')
            break;          /* end of line */
        buf[n++] = c;       /* output char into given buffer */

    }
    buf[n] = NUL;          /* string termination */
    if (n == (buflen-1)) 
        return NNTP_ERR_OVERFLOW;
    return NNTP_OK;
}

nntp_rc_t nntp_writeline(nntp_t *nntp, char *buf)
{
    char tmp[NNTP_LINE_MAXLEN];

    if (nntp == NULL)
        return NNTP_ERR_ARG;
    strncpy(tmp, buf, NNTP_LINE_MAXLEN-3);
    strcat(tmp, "\r\n");
    if (nntp->io.write(nntp->io.ctx, tmp, strlen(tmp)) < 0)
        return NNTP_ERR_SYSTEM;
    return NNTP_OK;
}

nntp_rc_t nntp_post(nntp_t *nntp, msg_t *msg)
{
    nntp_rc_t rc = NNTP_OK;
    char line[NNTP_LINE_MAXLEN];

    /*  RFC2980
     *   
     *  2.3 MODE READER
     *  MODE READER is used by the client to indicate to the server that it is
     *  a news reading client.  Some implementations make use of this
     *  information to reconfigure themselves for better performance in
     *  responding to news reader commands.  This command can be contrasted
     *  with the SLAVE command in RFC0977, which was not widely implemented.
     *  MODE READER was first available in INN.
     *  
     *  2.3.1 Responses
     *  200 Hello, you can post
     *  201 Hello, you can't post
     *  
     *  Research:
     *  
     *  < 200 dev16 InterNetNews server INN 2.3.2 ready
     *  > POST
     *  < 500 "post" not implemented; try "help".
     *  > MODE READER
     *  < 200 dev16 InterNetNews NNRP server INN 2.3.2 ready (posting ok).
     *  > POST
     *  < 340 Ok, recommended ID <...>
     *  
     *  In other words, INN *requires* the use of "MODE READER". This has been
     *  verified when INN is configured for expecting both news readers and
     *  feeders from a given source address. When INN is configured to expect
     *  readers only for a given source address the use of "MODE READER" is
     *  optional.
     */
    *line = NUL;
    strcat(line, "MODE READER");
    if ((rc = nntp_writeline(nntp, line)) != NNTP_OK)
        return rc;
    if ((rc = nntp_readline(nntp, line, sizeof(line))) != NNTP_OK)
        return rc;

    /*  A 200 response means posting ok, 201 means posting not allowed. We
     *  don't care about 5xx errors, they simply mean the server doesn't know
     *  about the RFC2980 "MODE READER" command. But any other response is not
     *  expected and we treat this as an error.
     */
    if (strncmp(line, "201", 3) == 0)
        CU(NNTP_ERR_DELIVERY);
    if (   strncmp(line, "200", 3) != 0
        && strncmp(line, "5"  , 1) != 0
          )
        CU(NNTP_ERR_DELIVERY);

    /*  check if this server already knows that artice
     *  > STAT <message-id>
     *  < 223 yes, article already known
     *  < 430 no, i don't know the article, yet
     */
    *line = NUL;
    strcat(line, "STAT ");
    strcat(line, msg->cpMsgid);
    if ((rc = nntp_writeline(nntp, line)) != NNTP_OK)
        return rc;
    if ((rc = nntp_readline(nntp, line, sizeof(line))) != NNTP_OK)
        return rc;
    if (strncmp(line, "223", 3) == 0)
        return NNTP_OK;
    if (strncmp(line, "430", 3) != 0)
        CU(NNTP_ERR_DELIVERY);

    /*  post the article
     *  > POST
     *  < 340 gimme that thing
     *  > From: ...
     *  > Subject: ...
     *  > Newsgroups: ...
     *  > Message-ID: <...>
     *  > [additional headers]
     *  > 
     *  > [body with dots escaped]
     *  > .
     *  < 240 ok, thank you
     *  < 441 duplicate (ok for us)
     *  
     *  Research:
     *  
     *  < 200 dev16 InterNetNews server INN 2.3.2 ready
     *  > POST
     *  [...]
     *  240 Article posted <...>
     *  441 435 Duplicate
     *  441 437 Too old
     *  441 Duplicate "Newsgroups" header
     *  441 Required "Subject" header is missing
     *  441 Obsolete "Received" header
     *  441 Line # too long
     *  
     *  In other words, INN uses 441 for other status messages as well.
     */
    *line = NUL;
    strcat(line, "POST");
    if ((rc = nntp_writeline(nntp, line)) != NNTP_OK)
        return rc;
    if ((rc = nntp_readline(nntp, line, sizeof(line))) != NNTP_OK)
        return rc;
    if (strncmp(line, "340", 3) != 0)
        CU(NNTP_ERR_DELIVERY);

    do {
        rc = nntp->io.write(nntp->io.ctx, msg->cpMsg, strlen(msg->cpMsg));
    } while (rc == -1 && errno == EINTR);
    if (rc == -1)
        return NNTP_ERR_SYSTEM;

    if ((rc = nntp_readline(nntp, line, sizeof(line))) != NNTP_OK)
        return rc;

    if (strncmp(line, "240", 3) == 0)
        return NNTP_OK;

    if (nntp->kludgeinn441dup) {
        if (strncmp(line, "441 435", 7) == 0)
            return NNTP_OK;
    }
    else {
        if (strncmp(line, "441", 3) == 0)
            return NNTP_OK;
    }

    CU(NNTP_ERR_DELIVERY);

    CUS:
    nntp->lastresp = strdup(line);
    return rc;
}

nntp_rc_t nntp_feed(nntp_t *nntp, msg_t *msg)
{
    nntp_rc_t rc = NNTP_OK;
    char line[NNTP_LINE_MAXLEN];

    /*  RFC0977 3.4. The IHAVE command, 3.4.2. Responses
     *  < 235 article transferred ok
     *  < 335 send article to be transferred.  End with <CR-LF>.<CR-LF>
     *  < 435 article not wanted - do not send it
     *  < 436 transfer failed - try again later
     *  < 437 article rejected - do not try again
     *  
     *  Research:
     *  < 200 dev16 InterNetNews server INN 2.3.2 ready
     *  > IHAVE <messageid>
     *  < 335 [no text; this number means positive response]
     *  < 435 Duplicate
     *  < 437 Missing "Path" header
     *  < 437 Unwanted newsgroup "quux"
     *  < 480 Transfer permission denied
     */
    *line = NUL;
    strcat(line, "IHAVE ");
    strcat(line, msg->cpMsgid);
    if ((rc = nntp_writeline(nntp, line)) != NNTP_OK)
        return rc;

    if ((rc = nntp_readline(nntp, line, sizeof(line))) != NNTP_OK)
        return rc;

    if (strncmp(line, "435", 3) == 0)
        return NNTP_OK;

    if (strncmp(line, "436", 3) == 0)
        return NNTP_DEFER;

    if (   (strncmp(line, "437", 3) == 0)
        || (strncmp(line, "480", 3) == 0))
        CU(NNTP_ERR_DELIVERY);

    if (strncmp(line, "335", 3) != 0)
        CU(NNTP_ERR_DELIVERY);

    do {
        rc = nntp->io.write(nntp->io.ctx, msg->cpMsg, strlen(msg->cpMsg));
    } while (rc == -1 && errno == EINTR);
    if (rc == -1)
        return NNTP_ERR_SYSTEM;

    if ((rc = nntp_readline(nntp, line, sizeof(line))) != NNTP_OK)
        return rc;

    if (strncmp(line, "235", 3) == 0)
        return NNTP_OK;

    if (strncmp(line, "436", 3) == 0)
        return NNTP_DEFER;

    CU(NNTP_ERR_DELIVERY);

    CUS:
    nntp->lastresp = strdup(line);
    return rc;
}

char *nntp_error(nntp_rc_t rc)
{
    char *str;
                                      str = "NNTP: errorcode has no description";
    if      (rc == NNTP_OK          ) str = "NNTP: no error";
    else if (rc == NNTP_EOF         ) str = "NNTP: end of file";
    else if (rc == NNTP_DEFER       ) str = "NNTP: transmission deferred";
    else if (rc == NNTP_FAKE        ) str = "NNTP: fake status not real";
    else if (rc == NNTP_ERR_SYSTEM  ) str = "NNTP: see errno";
    else if (rc == NNTP_ERR_ARG     ) str = "NNTP: invalid argument";
    else if (rc == NNTP_ERR_OVERFLOW) str = "NNTP: buffer overflow";
    else if (rc == NNTP_ERR_DELIVERY) str = "NNTP: cannot deliver message";
    else if (rc == NNTP_ERR_INIT    ) str = "NNTP: initialization failed";
    else if (rc == NNTP_ERR_UNKNOWN ) str = "NNTP: unknown error";
    return str;
}

