/*
 *  nntp.c: NNTP library (implementation)
 *
 *  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 NNTP speaking local
 *  mailer which forwards mails as Usenet news articles via NNTP.
 *  It can be found at http://www.ossp.com/pkg/lmtp2nntp/.
 *
 *  Permission to use, copy, modify, and distribute this software for
 *  any purpose with or without fee is hereby granted, provided that
 *  the above copyright notice and this permission notice appear in all
 *  copies.
 *
 *  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED
 *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 *  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 *  IN NO EVENT SHALL THE AUTHORS AND COPYRIGHT HOLDERS AND THEIR
 *  CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
 *  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 *  OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
 *  OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
 *  SUCH DAMAGE.
 */

#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"
#include "str.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 {
    int             rfd;
    int             wfd;
    nntp_io_t       io;
    nntp_readline_t rl; 
    struct timeval  tv;
    int             kludgeinn441dup;
};

nntp_t *nntp_create(int rfd, int wfd, nntp_io_t *io)
{
    nntp_t *nntp;

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

    if (io == NULL) {
        nntp->io.select = select;
        nntp->io.read   = read;
        nntp->io.write  = write;
    } else {
        nntp->io.select = io->select ? io->select : select;
        nntp->io.read   = io->read   ? io->read   : read;
        nntp->io.write  = io->write  ? io->write  : write;
    }

    nntp->rfd = rfd;
    nntp->wfd = wfd;
    nntp->rl.rl_cnt = 0;
    nntp->rl.rl_bufptr = NULL;
    nntp->rl.rl_buf[0] = NUL;
    nntp_timeout(nntp, 3);
    nntp->kludgeinn441dup = FALSE;

    return nntp;
}

nntp_rc_t nntp_timeout(nntp_t *nntp, long timeout)
{
    nntp->tv.tv_sec = timeout;
    nntp->tv.tv_usec = 0;
    return NNTP_OK;
}

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)
        free(nntp);
    return;
}

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;
    struct timeval tv;
    fd_set fds;
    int rc;

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

        /* fetch one character (but read more) */
        if (rl->rl_cnt <= 0) {
            FD_ZERO(&fds);
            FD_SET(nntp->rfd, &fds);
            tv.tv_sec  = nntp->tv.tv_sec;
            tv.tv_usec = nntp->tv.tv_usec;
            rc = nntp->io.select(nntp->rfd + 1, &fds, NULL, NULL, &tv);
            if (rc == 0)
                return NNTP_TIMEOUT;
            else if (rc == -1)
                return NNTP_ERR_SYSTEM;
            do {
                rl->rl_cnt = nntp->io.read(nntp->rfd, 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->wfd, 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)
        return NNTP_ERR_POST;
    if (   strncmp(line, "200", 3) != 0
        && strncmp(line, "5"  , 1) != 0
          )
        return NNTP_ERR_POST;

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

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

    do {
        rc = nntp->io.write(nntp->wfd, 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;
    }

    return NNTP_ERR_POST;

#if 0
            /* check if this server accepts at least one of the newsgroups
            > GROUP
            < 211 yes, group exists
            < 411 no, group doesn't exist
            */

#endif
}

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))
        return NNTP_ERR_POST;

    if (strncmp(line, "335", 3) != 0)
        return NNTP_ERR_POST;

    do {
        rc = nntp->io.write(nntp->wfd, 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;

    return NNTP_ERR_POST;
}

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_TIMEOUT     ) str = "NNTP: timeout";
    else if (rc == NNTP_DEFER       ) str = "NNTP: transmission deferred";
    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_POST    ) str = "NNTP: cannot post message";
    else if (rc == NNTP_ERR_INIT    ) str = "NNTP: initialization failed";
    else if (rc == NNTP_ERR_UNKNOWN ) str = "NNTP: unknown error";
    return str;
}

