/* ========================================================================== */
/*! \file
 * \brief Network News Transfer Protocol
 *
 * Copyright (c) 2012-2024 by the developers. See the LICENSE file for details.
 *
 * If nothing else is specified, functions return zero to indicate success
 * and a negative value to indicate an error.
 */


/* ========================================================================== */
/* Include headers */

#include "posix.h"  /* Include this first because of feature test macros */

#include <ctype.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "conf.h"
#include "config.h"
#include "compression.h"
#include "core.h"
#include "encoding.h"
#include "inet.h"
#include "log.h"
#include "main.h"
#include "nntp.h"
#include "tls.h"


/* ========================================================================== */
/*! \defgroup NNTP NNTP: Network News Transfer Protocol
 *
 * This transport subsystem should behave RFC 1951, RFC 977, RFC 2980, RFC 3977,
 * RFC 4643 and RFC 6048 conformant.
 *
 * Secure NNTPS connections can be requested if TLS support is available.
 *
 * The AUTHINFO USER/PASS extension from RFC 4643 is supported if TLS support is
 * available. The AUTHINFO SASL extension from RFC 4643 is not supported.
 *
 * Compression support is used according to RFC 8054.
 * Compression is used if advertised by the server (with COMPRESS capability),
 * the required local libraries (e.g. zlib for DEFLATE algorithm) are available
 * and compression isn't disabled by configuration (CONF_COMPRESSION parameter).
 *
 * \note
 * Authentication is always done before enabling compression.
 *
 * RFC 3977 allows non-ASCII encoded group names, but recommend not to use them.
 * They are supported nevertheless by this transport driver
 *
 * \attention
 * RFC 5536 conformant messages can't use Unicode group names.
 * RFC 5536 mandates that the whole header must be ASCII encoded and forbids
 * MIME encoded words as defined in RFC 2047 for the group identifiers inside
 * the "Newsgroups" and "Followup-To" header fields.
 *
 * \note
 * Unicode identifiers are ambigous by definition and can't simply be used as
 * received without an additional normalization step.
 * As long as there is no "canonical" normalization algorithm defined for the
 * Usenet, it is in general a good idea to not use Unicode for identifiers at
 * all.
 */
/*! @{ */


/* ========================================================================== */
/* Constants */

/*! \brief Message prefix for NNTP module */
#define MAIN_ERR_PREFIX  "NNTP: "

/*! \brief Maximum number of simultaneous NNTP connections */
#define NNTP_HANDLEMAX  1U

/*! \brief Maximum line length (for command/response, not payload)
 *
 * Must be at least 512 characters.
 * <br>
 * RFC 977 : Maximum command line length is 512 characters
 * <br>
 * RFC 3977: Maximum length of first line of response is 512 characters
 */
#define NNTP_LINELENGTHMAX  (size_t) 512

/*! \brief NNTP V2 maximum command argument length (according to RFC 3977) */
#define NNTP_ARGLENGTHMAX  (size_t) 497

/*! \name NNTP capabilities
 *
 * The flags can be bitwise ORed together.
 */
/*! @{ */
#define NNTP_CAPA_MODE_READER         0x0001U
#define NNTP_CAPA_READER              0x0002U
#define NNTP_CAPA_LIST                0x0004U
#define NNTP_CAPA_LIST_MOTD           0x0008U
#define NNTP_CAPA_LIST_DISTRIB_PATS   0x0010U
#define NNTP_CAPA_LIST_SUBSCRIPTIONS  0x0020U
#define NNTP_CAPA_OVER                0x0040U
#define NNTP_CAPA_POST                0x0080U
#define NNTP_CAPA_AUTHINFO_USER       0x0100U
#define NNTP_CAPA_COMPRESS            0x0200U
#define NNTP_CAPA_MAXARTNUM           0x0400U
/*! @} */

/*! \name Algorithms for NNTP compression extension
 *
 * The flags can be bitwise ORed together.
 */
/*! @{ */
#define NNTP_COMPRESS_DEFLATE  0x0001U
/*! @} */


/* ========================================================================== */
/* Data types */

/* NNTP commands */
enum  nntp_cmd
{
   NNTP_CMD_INIT,                  /* Dummy for reply to initial connect */
   NNTP_CMD_QUIT,
   NNTP_CMD_CAPABILITIES,
   NNTP_CMD_MODE_READER,
   NNTP_CMD_MAXARTNUM,
   NNTP_CMD_COMPRESS,
   NNTP_CMD_AUTHINFO_USER,
   NNTP_CMD_AUTHINFO_PASS,
   NNTP_CMD_LIST,                  /* Equal to LIST ACTIVE in NNTP V2 */
   NNTP_CMD_LIST_NEWSGROUPS,
   NNTP_CMD_LIST_MOTD,
   NNTP_CMD_LIST_DISTRIB_PATS,
   NNTP_CMD_LIST_SUBSCRIPTIONS,
   NNTP_CMD_LIST_OVERVIEW_FMT,
   NNTP_CMD_GROUP,
   NNTP_CMD_OVER,
   NNTP_CMD_ARTICLE_BY_MID,
   NNTP_CMD_ARTICLE,
   NNTP_CMD_HEAD,
   NNTP_CMD_BODY,
   NNTP_CMD_POST
};

/* NNTP server handle */
struct nntp_server
{
   unsigned int  connected;       /* Connection established flag */
   int  sd;                       /* Socket descriptor */
   FILE*  lfs;                    /* Logfile stream */
   unsigned int  version;         /* Protocol version */
   unsigned int  capabilities;    /* Capability flags */
   size_t  over_newsgroups;       /* Index of Newsgroups header field */
   unsigned int  compress_algs;   /* Compression algorithms */
   int  compress_active;          /* Compression layer active flag */
   void*  compress_stream;        /* Pointer to compressed data stream object */
   int  auth;                     /* Authentication algorithm */
   nntp_anum_t maxartnum;         /* Maximum article number from server */
   char*  distrib_pats;           /* Distribution patterns */
   size_t  distrib_pats_len;      /* Length of distribution patterns */
#if CFG_USE_TLS
   void*  eco;                    /* Encrypted connection object */
   const char*  user;             /* User account */
   const char*  passwd;           /* Password for user account */
#  if CFG_USE_OPENSSL_API_1_1 && !CFG_USE_OPENSSL_API_3
   void*  peekbuf;                /* Peek buffer for OpenSSL 1.1.x */
   size_t  peekbuf_len;           /* Length of peek buffer for OpenSSL 1.1.x */
#  endif  /* CFG_USE_OPENSSL_API_1_1  && !CFG_USE_OPENSSL_API_3 */
#endif  /* CFG_USE_TLS */
};

/* NNTP server response */
struct nntp_response
{
   unsigned int  status;          /* Complete status code */
   unsigned char  code1;          /* Status code 1. digit */
   unsigned char  code2;          /* Status code 2. digit */
   unsigned char  code3;          /* Status code 3. digit */
   char*  msg;                    /* First line of response */
   char*  content;                /* Content of response */
   size_t  lines;                 /* Number of lines in 'content' */
   size_t  bufsize;               /* Buffer size for 'content' */
};


/* ========================================================================== */
/* Variables */

/* Server state array */
static struct nntp_server  server[NNTP_HANDLEMAX];


/* ========================================================================== */
/* Forward declarations */

static int  exec_auth(int);
static int  refresh_capabilities(int);
static int  refresh_overview_format(int);


/* ========================================================================== */
/* Watermark parser
 *
 * Max. 20 digits are supported for MAXARTNUM extension.
 * RFC 3977 allows max. 16 digits.
 *
 * This function must correctly process leading zeros.
 */

static int  parse_number(int  c, char  wm[16], int*  wm_len, nntp_anum_t*  n)
{
   int  res = 1;  /* A result of 1 means "feed more data" */

   if(0x20 == c && *wm_len)
   {
      if(enc_convert_ascii_to_anum((core_anum_t*) n, wm, *wm_len))
      {
         PRINT_ERROR("Invalid water mark field");
         res = -1;
      }
      else  { res = 0; }
   }
   else
   {
      /* Verify that next character is a digit */
      if(!(0x30 <= c && 0x39 >= c))
      {
         PRINT_ERROR("Invalid character in water mark field");
         res = -1;
      }
      /* Check length limit */
      else if(16 <= *wm_len)
      {
         PRINT_ERROR("Water mark field too long");
         res = -1;
      }
      else  { wm[(*wm_len)++] = (char) c; }
   }

   return(res);
}


/* ========================================================================== */
/* Initialize server handle object */

static int  get_handle(int*  handle)
{
   int  res = -1;
   unsigned int  i;

   /* Search for unused handle */
   for(i = 0; i < NNTP_HANDLEMAX; ++i)
   {
      if(server[i].connected)  continue;

      /* Found => Init handle */
      server[i].sd = -1;
      server[i].lfs = NULL;
      server[i].version = 0;  /* Unknown */
      server[i].capabilities = 0;
      server[i].over_newsgroups = 0;  /* Not supported */
      server[i].compress_algs = 0;
      server[i].compress_active = 0;
      server[i].compress_stream = NULL;
      server[i].auth = 0;
      server[i].maxartnum = 0;
      server[i].distrib_pats = NULL;
      server[i].distrib_pats_len = 0;
#if CFG_USE_TLS
      server[i].eco = NULL;
      server[i].user = NULL;
      server[i].passwd = NULL;
#  if CFG_USE_OPENSSL_API_1_1 && !CFG_USE_OPENSSL_API_3
      server[i].peekbuf = NULL;
      server[i].peekbuf_len = 0;
#  endif  /* CFG_USE_OPENSSL_API_1_1  && !CFG_USE_OPENSSL_API_3 */
#endif  /* CFG_USE_TLS */

      *handle = (int) i;
      res = 0;
      break;
   }

   return(res);
}


/* ========================================================================== */
/* Flush TX direction of transport layer */

static int  transport_flush(int  handle, int  terminate)
{
   int  res = 0;

#if !CFG_CMPR_DISABLE
   if(server[handle].compress_active)
   {
      if(!terminate)  { res = cmpr_flush(server[handle].compress_stream); }
      else  { res = cmpr_terminate(server[handle].compress_stream); }
   }
#endif  /* !CFG_CMPR_DISABLE */

   return(res);
}


/* ========================================================================== */
/* Send data to server
 *
 * \attention
 * The parameter \e flags must always be zero.
 */

static api_posix_ssize_t  transport_send(int  handle, const void*  buf,
                                         size_t  len, int  flags)
{
   /* No flags are supported */
   if(flags)
   {
      PRINT_ERROR("transport_send(): Called with invalid flags");
      return(-1);
   }
#if CFG_USE_TLS
   if(NULL != server[handle].eco)
   {
      return(tls_send(server[handle].eco, buf, len));
   }
#endif  /* CFG_USE_TLS */
   return(api_posix_send(server[handle].sd, buf, len, flags));
}


/* ========================================================================== */
/* Send data with optional compression */

static api_posix_ssize_t  srv_send(int  handle, const void*  buf, size_t  len)
{
#if !CFG_CMPR_DISABLE
   if(server[handle].compress_active)
   {
      return(cmpr_send(server[handle].compress_stream, buf, len));
   }
   else
#endif  /* !CFG_CMPR_DISABLE */
   {
      return(transport_send(handle, buf, len, 0));
   }
}


/* ========================================================================== */
/* Receive data from server
 *
 * \attention
 * For the parameter \e flags , only \c API_POSIX_MSG_PEEK is supported.
 */

static api_posix_ssize_t  transport_recv(int  handle, void*  buf, size_t  len,
                                         int  flags)
{
   api_posix_ssize_t  res = -1;
#if CFG_USE_TLS
   int  peek = 0;
#  if CFG_USE_OPENSSL_API_1_1 && !CFG_USE_OPENSSL_API_3
   char*  p;
#  endif  /* CFG_USE_OPENSSL_API_1_1 && !CFG_USE_OPENSSL_API_3 */
#endif  /* CFG_USE_TLS */

   /* Check data length */
   if(API_POSIX_SSIZE_MAX < len)
   {
      PRINT_ERROR("transport_recv(): Called with invalid data length");
      return(-1);
   }

   /* Only the 'MSG_PEEK' flag is supported */
   if(~(API_POSIX_MSG_PEEK) & flags)
   {
      PRINT_ERROR("transport_recv(): Called with invalid flags");
      return(-1);
   }

#if CFG_USE_TLS
   if(NULL != server[handle].eco)
   {
      if(API_POSIX_MSG_PEEK & flags)  { peek = 1; }
#  if CFG_USE_OPENSSL_API_1_1 && !CFG_USE_OPENSSL_API_3
      /*
       * In OpenSSL 1.1.0 the peek flag handling no longer work via the
       * function 'SSL_peek()'. OpenSSL spins instead of processing the
       * (available) data from the socket.
       *
       * This is a workaround that uses it's own buffer inside the server
       * handle. It is ugly to do this here, but every handle requires its
       * own buffer that is associated with the connection object 'eco'.
       *
       * The workaround is no longer needed for OpenSSL API 3.
       */
      if(!peek && !server[handle].peekbuf_len)
      {
         res = tls_recv(server[handle].eco, buf, len, 0);
      }
      else
      {
         /* Check for peek request and empty peek buffer */
         if(peek && !server[handle].peekbuf_len)
         {
            /* Resize peek buffer */
            p = (char*) api_posix_realloc(server[handle].peekbuf, len);
            if(NULL != p)
            {
               server[handle].peekbuf = p;
               res = tls_recv(server[handle].eco,
                              server[handle].peekbuf, len, 0);
               if(0 < res)
               {
                  server[handle].peekbuf_len = (size_t) res;
                  res = -1;  /* Result is set below after the data was copied */
               }
            }
         }
         /* Return data from peek buffer first */
         if(server[handle].peekbuf_len)
         {
            if(server[handle].peekbuf_len < len)
            {
               /* Reduce request to size of buffered data */
               len = server[handle].peekbuf_len;
            }
            memcpy(buf, server[handle].peekbuf, len);
            res = (api_posix_ssize_t) len;
            /* Remove data from peek buffer in read mode */
            if(!peek)
            {
               p = (char*) server[handle].peekbuf;
               memmove(server[handle].peekbuf, (void*) &p[len],
                       server[handle].peekbuf_len - len);
               server[handle].peekbuf_len -= len;
            }
         }
      }
#  else  /* CFG_USE_OPENSSL_API_1_1 && !CFG_USE_OPENSSL_API_3 */
      /* OpenSSL 1.0.x and 3.x.x can handle the peek flag */
      res = tls_recv(server[handle].eco, buf, len, peek);
#  endif  /* CFG_USE_OPENSSL_API_1_1 && !CFG_USE_OPENSSL_API_3 */
   }
   else
#endif  /* CFG_USE_TLS */
   {
      res = api_posix_recv(server[handle].sd, buf, len, flags);
   }

   return(res);
}


/* ========================================================================== */
/* Receive data from server */

static api_posix_ssize_t  srv_recv(int  handle, void*  buf, size_t  len,
                                   int  flags)
{
#if !CFG_CMPR_DISABLE
   if(server[handle].compress_active)
   {
      return(cmpr_recv(server[handle].compress_stream, buf, len, flags));
   }
   else
#endif  /* !CFG_CMPR_DISABLE */
   {
      return(transport_recv(handle, buf, len, flags));
   }
}


/* ========================================================================== */
/* Create response */

static int  create_response(struct nntp_response**  r)
{
   int  res = 0;

   *r = (struct nntp_response*) api_posix_malloc(sizeof(struct nntp_response));
   if(NULL == *r)
   {
      PRINT_ERROR("Out of memory while allocating response");
      res = -1;
   }
   else
   {
      /* Init optional fields */
      (*r)->msg = NULL;
      (*r)->content = NULL;
      (*r)->lines = 0;
      (*r)->bufsize = 0;
   }

   return(res);
}


/* ========================================================================== */
/* Destroy response */

static void  destroy_response(struct nntp_response**  r)
{
   if(NULL != *r)
   {
      api_posix_free((void*) (*r)->msg);
      api_posix_free((void*) (*r)->content);
      api_posix_free((void*) *r);
   }
}


/* ========================================================================== */
/* Check whether status code indicates a multiline response
 *
 * RFC 3977 specifies:
 * Exceptions are the commands GROUP and LISTGROUP.
 * In all other cases, the client MUST only use the status indicator itself to
 * determine the nature of the response.
 */

static int  multiline(unsigned int  status, enum nntp_cmd  command)
{
   /* List with status codes that correspond to multiline responses */
   static unsigned int  code[12] =
   {
      100U, 101U, 211U, 215U, 220U, 221U, 222U, 224U, 225U, 230U, 231U,
      0U  /* A zero must terminate the list! */
   };
   int  res = 0;
   unsigned int  i = 0;

   /* Check whether status code indicates a multiline response */
   while(code[i])  { if(status == code[i++])  { res = 1; } }

   /* Exception for GROUP command (that use a 211 single line response) */
   if(211U == status && NNTP_CMD_GROUP == command)  { res = 0; }

   return(res);
}


/* ========================================================================== */
/* Receive data block of multiline response */

static int  recv_multiline_data_block(int  handle,
                                      struct nntp_response*  response)
{
   int  res = 0;
   api_posix_ssize_t  rv;
   char*  resp = NULL;
   char*  p;
   size_t  len = 512;
   size_t  ri = 0;
   size_t  pi;
   size_t  i;
   int  eor = 0;  /* End of record (data block) */
   size_t  lines = 0;

   /*
    * Note:
    * The line lengths inside the data block may be arbitrary.
    * We allocate 512Byte at the beginning and expontially increase the size
    * if required.
    */
   resp = (char*) api_posix_malloc(len);
   if(NULL == resp)
   {
      PRINT_ERROR("Out of memory while receiving response");
      res = -1;
   }

   /* Receive data */
   while(!res && !eor)
   {
      while(!res && !eor && len > ri)
      {
         /* Peek into next chunk of input */
         pi = ri;
         do  { rv = srv_recv(handle, &resp[ri], len - ri, API_POSIX_MSG_PEEK); }
         while((api_posix_ssize_t) -1 == rv
                && API_POSIX_EINTR == api_posix_errno);
         if((api_posix_ssize_t) -1 == rv)
         {
            PRINT_ERROR("Reading data failed");
            res = -2;
            break;
         }
         pi += (size_t) rv;
         /* 'pi' now points behind the last character */
         /* Check for EOR */
         i = ri;
         while(pi > i)
         {
            /* Check for EOL */
            if('\n' == resp[i])
            {
               ++lines;
               if(2 == i)
               {
                  if('\r' == resp[i - 1] && '.' == resp[i - 2])
                  {
                     /* Empty data block */
                     eor = 1;
                     break;
                  }
               }
               else if(4 <= i)
               {
                  if('\r' == resp[i - 1] && '.' == resp[i - 2]
                     && '\n' == resp[i - 3] && '\r' == resp[i - 4])
                  {
                     eor = 1;
                     break;
                  }
               }
            }
            ++i;
         }
         if(eor)
         {
            ++i;
            --lines;
         }
         /* 'i' now points behind the last character */
         /* Read next chunk of input (up to but not including index 'i') */
         while(i > ri)
         {
            do  { rv = srv_recv(handle, &resp[ri], i - ri, 0); }
            while((api_posix_ssize_t) -1 == rv
                  && API_POSIX_EINTR == api_posix_errno);
            if((api_posix_ssize_t) -1 == rv)
            {
               PRINT_ERROR("Reading data failed");
               res = -2;
               break;
            }
            ri += (size_t) rv;
         }
         /* 'ri' now points behind the last character */
      }
      /* Allocate more memory if required */
      if(!res && !eor)
      {
         len *= 2;
         p = (char*) api_posix_realloc(resp, len);
         if(NULL == p)
         {
            PRINT_ERROR("Out of memory while receiving response");
            res = -1;
         }
         else  { resp = p; }
      }
   }
   if(!res && !eor)  { res = -1; }
   if(res)  { api_posix_free((void*) resp); }
   else
   {
      /* Replace ".\r\n" termination with zero termination */
      len = ri - 3;
      resp[len] = 0;
      /* Remove dot stuffing */
      if(len)
      {
         if('.' == resp[0] && '.' == resp[1])
         {
            memmove(&resp[0], &resp[1], --len + (size_t) 1);
         }
         for(i = 2; i < len; ++i)
         {
            if('.' == resp[i] && '.' == resp[i - 1] && (char) 10 == resp[i - 2])
            {
               memmove(&resp[i - 1], &resp[i], len-- - i + (size_t) 1);
            }
         }
      }
      if(NULL != response->content)
      {
         api_posix_free((void*) response->content);
      }
      response->content = resp;
      response->lines = lines;
      response->bufsize = len + 1;  /* For the terminating NUL */
      log_add(server[handle].lfs, response->content);
   }

   return(res);
}


/* ========================================================================== */
/* Receive response */

static int  recv_reply(int  handle, enum nntp_cmd  command,
                       struct nntp_response*  response)
{
   int  res = 0;
   api_posix_ssize_t  rv;
   char  resp[NNTP_LINELENGTHMAX + (size_t) 1];
   size_t  len = NNTP_LINELENGTHMAX;
   size_t  li = 0;
   size_t  pi;
   size_t  i;
   int  eol = 0;

   /* Receive first line of response */
   while(!res && !eol && len > li)
   {
      /* Peek into next chunk of input */
      pi = li;
      do  { rv = srv_recv(handle, &resp[li], len - li, API_POSIX_MSG_PEEK); }
      while((api_posix_ssize_t) -1 == rv && API_POSIX_EINTR == api_posix_errno);
      /* Treat zero (connection was closed by peer) as error */
      if((api_posix_ssize_t) 0 >= rv)
      {
         PRINT_ERROR("Reading data failed");
         res = -2;
         break;
      }
      pi += (size_t) rv;
      /* Check for EOL */
      i = li;
      while(pi > i)
      {
         if('\n' == resp[i++])
         {
            eol = 1;
            break;
         }
      }
      /* Read next chunk of input (up to but not including index 'i') */
      while(i > li)
      {
         do  { rv = srv_recv(handle, &resp[li], i - li, 0); }
         while((api_posix_ssize_t) -1 == rv
               && API_POSIX_EINTR == api_posix_errno);
         if((api_posix_ssize_t) -1 == rv)
         {
            PRINT_ERROR("Reading data failed");
            res = -2;
            break;
         }
         li += (size_t) rv;
      }
   }
   if(!res && !eol)  { res = -1; }
   if(!res)
   {
      /* Store first line of response */
      resp[li] = 0;
      resp[NNTP_LINELENGTHMAX] = 0;
      for(i = 0; i < 3; ++i)
      {
         if((unsigned char) 9 >= (unsigned char) resp[i])
         {
            PRINT_ERROR("Protocol error: Invalid response code");
            log_add(server[handle].lfs, "[<= Invalid response]\n");
            res = -1;
         }
      }
      if(!res)
      {
         response->code1 = (unsigned char) (resp[0] - (char) 48);
         response->code2 = (unsigned char) (resp[1] - (char) 48);
         response->code3 = (unsigned char) (resp[2] - (char) 48);
         if(NULL != response->msg)  { api_posix_free((void*) response->msg); }
         response->msg = (char*) api_posix_malloc(strlen(resp) + (size_t) 1);
         if(NULL == response->msg)
         {
            PRINT_ERROR("Out of memory while receiving response");
            res = -1;
         }
         else  { strcpy(response->msg, resp); }
         response->content = NULL;
         response->lines = 0;
         log_add(server[handle].lfs, "[<=] ");
         log_add(server[handle].lfs, response->msg);
      }
   }

   /* Check for success */
   if(!res)
   {
      response->status = (unsigned int) response->code3;
      response->status += (unsigned int) response->code2 * 10U;
      response->status += (unsigned int) response->code1 * 100U;
      /* Check whether the response is multiline */
      if(multiline(response->status, command))
      {
         log_add(server[handle].lfs, "[<= Expect multiline data block]\n");
         res = recv_multiline_data_block(handle, response);
      }
   }

   return(res);
}


/* ========================================================================== */
/* Send multiline data block */

static int  send_multiline_data_block(int  handle, const char*  data)
{
   static const char  eob[] = ".\r\n";
   int  res = 0;
   size_t  len;
   size_t  i;
   api_posix_ssize_t  rv;
   char*  buf = NULL;
   size_t  bi;
   char  c;

   if(NULL == data)  { res = -1; }
   else
   {
      log_add(server[handle].lfs, "[=> Send multiline data block]\n");
      /*
       * Verify data and add dot stuffing
       * Calculate 3 additional bytes for potentially missing EOL mark and NUL.
       * Allocating a buffer twice as large is always sufficient.
       * Eliminate single CR or LF characters (not parts of EOL mark).
       * Ensure that (non empty) data block ends with CRLF.
       */
      buf = (char*) api_posix_malloc((strlen(data) + (size_t) 3) * (size_t) 2);
      if(NULL == buf)
      {
         PRINT_ERROR("Memory allocation failed for dot stuffing");
         data = "";
      }
      else
      {
         i = 0;  bi = 0;
         /* Assignment in truth expression is intended */
         while((c = data[i++]))
         {
            if( (13 == (int) c && 10 != (int) data[i])
               || (10 == (int) c && 13 != (int) data[i - (size_t) 2] && i) )
            {
               continue;
            }
            if('.' == c && (size_t) 3 <= i)
            {
               if( (10 == (int) data[i - (size_t) 2])
                  && (13 == (int) data[i - (size_t) 3]) )
               {
                  /* Add additional dot */
                  buf[bi++] = '.';
               }
            }
            buf[bi++] = c;
         }
         /* Check for CR+LF at end of (non empty) buffer */
         if(bi)
         {
            if( (size_t) 2 > bi
               || 10 != (int) buf[bi - (size_t) 1]
               || 13 != (int) buf[bi - (size_t) 2] )
            {
               /* Add missing EOL mark */
               buf[bi++] = (char) 13;
               buf[bi++] = (char) 10;
            }
         }
         /* Terminate new data */
         buf[bi++] = 0;
         data = buf;
      }
      /* Send data */
      log_add(server[handle].lfs, data);
      len = strlen(data);
      i = 0;
      while(i < len)
      {
         do  { rv = srv_send(handle, &data[i], len - i); }
         while((api_posix_ssize_t) -1 == rv
               && API_POSIX_EINTR == api_posix_errno);
         if((api_posix_ssize_t) -1 == rv)
         {
            PRINT_ERROR("Writing data failed");
            res = -2;
            break;
         }
         i += (size_t) rv;
      }
      api_posix_free((void*) buf);
      /* Send EOB mark */
      log_add(server[handle].lfs, eob);
      len = strlen(eob);
      i = 0;
      while(i < len)
      {
         do  { rv = srv_send(handle, &eob[i], len - i); }
         while((api_posix_ssize_t)-1 == rv
               && API_POSIX_EINTR == api_posix_errno);
         if((api_posix_ssize_t) -1 == rv)
         {
            PRINT_ERROR("Writing data failed");
            res = -2;
            break;
         }
         i += (size_t) rv;
      }
      /* Flush TX direction of transport layer */
      if(!res)
      {
         res = transport_flush(handle, 0);
         if(0 > res)  { res = -2; }
      }
   }

   return(res);
}


/* ========================================================================== */
/* Execute command on NNTP server
 *
 * This function must be reentrant.
 *
 * \return
 * - 0 on success
 * - -1 on error
 * - -2 if the connection to the server is broken
 * - -3 if authentication requested by server failed
 */

static int  exec_cmd(int  handle, enum nntp_cmd  command,
                     struct nntp_response*  response, ...)
{
   va_list  ap;                    /* Object for argument list handling */
   int  res = 0;
   char  cmd[NNTP_LINELENGTHMAX + (size_t) 1];
   api_posix_ssize_t  rv;
   size_t  len;
   size_t  i;
   const char*  s;
   nntp_anum_t  article_id;
   char  article_asc[17];
   int  auth = 0;  /* Commands used for authentication must set this flag */
   int  retry = 1;  /* Commands that should never retried can clear this flag */
   int  post = 0;  /* The POST command must set this flag */
   const char*  data = NULL;

   /* Be prepared for retry after potential authentication */
   do
   {
      if(0 > retry)
      {
         PRINT_ERROR("Bug in command retry state machine");
         res = -1;
         break;
      }

      /* Prepare command */
      va_start(ap, response);
      switch(command)
      {
         case NNTP_CMD_QUIT:
         {
            retry = 0;
            strcpy(cmd, "QUIT\r\n");
            break;
         }
         case NNTP_CMD_CAPABILITIES:
         {
            retry = 0;
            strcpy(cmd, "CAPABILITIES\r\n");
            break;
         }
         case NNTP_CMD_MODE_READER:
         {
            retry = 0;
            strcpy(cmd, "MODE READER\r\n");
            break;
         }
         case NNTP_CMD_MAXARTNUM:
         {
            strcpy(cmd, "MAXARTNUM ");
            s = va_arg(ap, const char*);
            len = NNTP_LINELENGTHMAX - strlen(cmd) - (size_t) 2;
            strncat(cmd, s, len);
            strcat(cmd, "\r\n");
            break;
         }
         case NNTP_CMD_COMPRESS:
         {
            strcpy(cmd, "COMPRESS ");
            s = va_arg(ap, const char*);
            len = NNTP_LINELENGTHMAX - strlen(cmd) - (size_t) 2;
            strncat(cmd, s, len);
            strcat(cmd, "\r\n");
            break;
         }
         case NNTP_CMD_LIST:
         {
            /* Check whether server has LIST capability */
            if(!(server[handle].capabilities & NNTP_CAPA_LIST))
            {
               PRINT_ERROR("Server has no LIST capability");
               res = -1;
            }
            else
            {
               /* Yes, use ACTIVE argument if supported */
               if(1U < server[handle].version)
               {
                  strcpy(cmd, "LIST ACTIVE\r\n");
               }
               else  { strcpy(cmd, "LIST\r\n"); }
            }
            break;
         }
         case NNTP_CMD_LIST_NEWSGROUPS:
         {
            /* Check whether server has LIST capability */
            if(!(server[handle].capabilities & NNTP_CAPA_LIST))
            {
               PRINT_ERROR("Server has no LIST capability");
               res = -1;
            }
            else  { strcpy(cmd, "LIST NEWSGROUPS\r\n"); }
            break;
         }
         case NNTP_CMD_LIST_MOTD:
         {
            /* Check whether server has LIST MOTD capability */
            if(!(server[handle].capabilities & NNTP_CAPA_LIST_MOTD))
            {
               PRINT_ERROR("Server has no LIST MOTD capability");
               res = -1;
            }
            else  { strcpy(cmd, "LIST MOTD\r\n"); }
            break;
         }
         case NNTP_CMD_LIST_DISTRIB_PATS:
         {
            /* Check whether server has LIST DISTRIB.PATS capability */
            if(!(server[handle].capabilities & NNTP_CAPA_LIST_DISTRIB_PATS))
            {
               PRINT_ERROR("Server has no LIST DISTRIB.PATS capability");
               res = -1;
            }
            else  { strcpy(cmd, "LIST DISTRIB.PATS\r\n"); }
            break;
         }
         case NNTP_CMD_LIST_SUBSCRIPTIONS:
         {
#if 0  /* Disable NNTP V2 capability check, should work with NNTP V1 too */
            /* Check whether server has LIST SUBSCRIPTIONS capability */
            if(!(server[handle].capabilities & NNTP_CAPA_LIST_SUBSCRIPTIONS))
            {
               PRINT_ERROR("Server has no LIST SUBSCRIPTIONS capability");
               res = -1;
            }
            else
#endif
            { strcpy(cmd, "LIST SUBSCRIPTIONS\r\n"); }
            break;
         }
         case NNTP_CMD_LIST_OVERVIEW_FMT:
         {
            /* Check whether server has OVER capability */
            if(!(server[handle].capabilities & NNTP_CAPA_OVER))
            {
               PRINT_ERROR("Server has no OVER capability");
               res = -1;
            }
            else  { strcpy(cmd, "LIST OVERVIEW.FMT\r\n"); }
            break;
         }
         case NNTP_CMD_GROUP:
         {
            strcpy(cmd, "GROUP ");
            s = va_arg(ap, const char*);
            len = strlen(s);
            if(NNTP_LINELENGTHMAX - strlen(cmd) - (size_t) 2 < len)
            {
               PRINT_ERROR("Value of NNTP_LINELENGTHMAX too small");
               res = -1;
               break;
            }
            strncat(cmd, s, len);
            strcat(cmd, "\r\n");
            break;
         }
         case NNTP_CMD_OVER:
         {
            /* Check whether server has OVER capability */
            if(!(server[handle].capabilities & NNTP_CAPA_OVER))
            {
               PRINT_ERROR("Server has no OVER capability");
               res = -1;
            }
            else
            {
               strcpy(cmd, "OVER ");
               article_id = *va_arg(ap, nntp_anum_t*);
               if(0 > enc_convert_anum_to_ascii(article_asc, &len,
                                                (core_anum_t) article_id))
               {
                  res = -1;
                  break;
               }
               if(NNTP_LINELENGTHMAX - strlen(cmd) - (size_t) 2
                  < len + (size_t) 1)
               {
                  PRINT_ERROR("Value of NNTP_LINELENGTHMAX too small");
                  res = -1;
                  break;
               }
               strncat(cmd, article_asc, len);
               strcat(cmd, "-");
               article_id = *va_arg(ap, nntp_anum_t*);
               if(0 > enc_convert_anum_to_ascii(article_asc, &len,
                                                (core_anum_t) article_id))
               {
                  res = -1;
                  break;
               }
               if(NNTP_LINELENGTHMAX - strlen(cmd) - (size_t) 2 < len)
               {
                  PRINT_ERROR("Value of NNTP_LINELENGTHMAX too small");
                  res = -1;
                  break;
               }
               strncat(cmd, article_asc, len);
               strcat(cmd, "\r\n");
            }
            break;
         }
         case NNTP_CMD_ARTICLE_BY_MID:
         {
            strcpy(cmd, "ARTICLE ");
            s = va_arg(ap, const char*);
            len = strlen(s);
            if(NNTP_LINELENGTHMAX - strlen(cmd) - (size_t) 2 < len)
            {
               PRINT_ERROR("Value of NNTP_LINELENGTHMAX too small");
               res = -1;
               break;
            }
            strncat(cmd, s, len);
            strcat(cmd, "\r\n");
            break;
         }
         case NNTP_CMD_ARTICLE:
         case NNTP_CMD_HEAD:
         case NNTP_CMD_BODY:
         {
            article_id = *va_arg(ap, nntp_anum_t*);
            if(0 > enc_convert_anum_to_ascii(article_asc, &len,
                                             (core_anum_t) article_id))
            {
               res = -1;
               break;
            }
            if(NNTP_CMD_HEAD == command)  { strcpy(cmd, "HEAD "); }
            else if(NNTP_CMD_BODY == command)  { strcpy(cmd, "BODY "); }
            else  { strcpy(cmd, "ARTICLE "); }
            if(NNTP_LINELENGTHMAX - strlen(cmd) - (size_t) 2 < len)
            {
               PRINT_ERROR("Value of NNTP_LINELENGTHMAX too small");
               res = -1;
               break;
            }
            strncat(cmd, article_asc, len);
            strcat(cmd, "\r\n");
            break;
         }
         case NNTP_CMD_POST:
         {
            /* Check whether server has POST capability */
            if(!(server[handle].capabilities & NNTP_CAPA_POST))
            {
               PRINT_ERROR("Server has no POST capability");
               res = -1;
            }
            else
            {
               strcpy(cmd, "POST\r\n");
               data = va_arg(ap, const char*);
               post = 1;
            }
            break;
         }
#if CFG_USE_TLS
         case NNTP_CMD_AUTHINFO_USER:
         {
            auth = 1;
            strcpy(cmd, "AUTHINFO USER ");
            len = strlen(server[handle].user);
            if(NNTP_LINELENGTHMAX - strlen(cmd) - (size_t) 2 < len)
            {
               PRINT_ERROR("Value of NNTP_LINELENGTHMAX too small");
               res = -1;
               break;
            }
            strncat(cmd, server[handle].user, len);
            strcat(cmd, "\r\n");
            break;
         }
         case NNTP_CMD_AUTHINFO_PASS:
         {
            auth = 1;
            strcpy(cmd, "AUTHINFO PASS ");
            len = strlen(server[handle].passwd);
            if(NNTP_LINELENGTHMAX - strlen(cmd) - (size_t) 2 < len)
            {
               PRINT_ERROR("Value of NNTP_LINELENGTHMAX too small");
               res = -1;
               break;
            }
            strncat(cmd, server[handle].passwd, len);
            strcat(cmd, "\r\n");
            break;
         }
#endif  /* CFG_USE_TLS */
         default:
         {
            PRINT_ERROR("Request to execute unknown command");
            res = -1;
            break;
         }
      }

      /* Send command */
      if(!res)
      {
         /* Avoid writing password to logfile */
         if(NNTP_CMD_AUTHINFO_PASS == command)
         {
            log_add(server[handle].lfs, "[=>] AUTHINFO PASS ********\n");
            /* printf("[Command] AUTHINFO PASS ********\n"); */
         }
         else
         {
            log_add(server[handle].lfs, "[=>] ");
            log_add(server[handle].lfs, cmd);
            /* printf("[Command] %s", cmd); */
         }
         len = strlen(cmd);
         i = 0;
         while(i < len)
         {
            do  { rv = srv_send(handle, &cmd[i], len - i); }
            while((api_posix_ssize_t) -1 == rv
                  && API_POSIX_EINTR == api_posix_errno);
            if((api_posix_ssize_t) -1 == rv)
            {
               PRINT_ERROR("Writing data failed");
               res = -2;
               break;
            }
            i += (size_t) rv;
         }
         /* Flush TX direction of transport layer */
         if(!res)
         {
            if(NNTP_CMD_QUIT == command)
            {
               /* Indicate termination to transport layer */
               res = transport_flush(handle, 1);
            }
            else
            {
               res = transport_flush(handle, 0);
            }
            if(0 > res)
            {
               PRINT_ERROR("Failed to flush transport layer");
               res = -2;
            }
         }
      }
      va_end(ap);

      /* Receive reply */
      if(!res)  { res = recv_reply(handle, command, response); }

      /*
       * Check whether authentication is required here while the last command
       * and its parameters are still available on the stack for retry.
       */
      if(res || auth)  { break; }
      else if(480U != response->status)  { retry = 0; }
      else
      {
         /* Server requests authentication */
         if(exec_auth(handle))
         {
            /* Authentication failed */
            log_add(server[handle].lfs, "[Authentication failed]\n");
            res = -3;
         }
         else  { res = refresh_capabilities(handle); }
      }
   }
   while(!res && retry--);

   /* Check whether server have requested article to post */
   if(!res && post && 3U == response->code1 && 4U == response->code2)
   {
      /* Yes => Send article as multiline data block */
      res = send_multiline_data_block(handle, data);

      /* Receive reply */
      if(!res)  { res = recv_reply(handle, command, response); }
   }

   return(res);
}


/* ========================================================================== */
/* Execute authentication on NNTP server */

static int  exec_auth(int  handle)
{
   int  res = -1;
   int  rv;
   struct nntp_response*  response = NULL;

   /* Allocate memory for response */
   rv = create_response(&response);
   if(rv)  { PRINT_ERROR("Cannot allocate memory for response"); }
   else
   {
      /* Authenticate against server */
      switch(server[handle].auth)
      {
         case -1:
         {
            PRINT_ERROR("Error: Authentication requested more than once");
            break;
         }
         case 0:
         {
            PRINT_ERROR("Authentication required but disabled");
            break;
         }
         case 1:
         {
            /* Check whether server has AUTHINFO USER capability */
            if(!(server[handle].capabilities & NNTP_CAPA_AUTHINFO_USER))
            {
               PRINT_ERROR("Server has no AUTHINFO USER capability");
            }
            else
            {
#if CFG_USE_TLS
               /* RFC 2980 / RFC 4643 conformant AUTHINFO USER authentication */
               rv = exec_cmd(handle, NNTP_CMD_AUTHINFO_USER, response);
               if(!rv && 381U == response->status)
               {
                  rv = exec_cmd(handle, NNTP_CMD_AUTHINFO_PASS, response);
                  if(!rv && 281U == response->status)
                  {
                     /* Don't accept further authentication requests */
                     server[handle].auth = -1;
                     res = 0;
                  }
               }
               if(res)
               {
                  PRINT_ERROR("Authentication was not accepted");
               }
#else  /* CFG_USE_TLS */
              PRINT_ERROR("No TLS support (required for authentication)");
#endif  /* CFG_USE_TLS */
            }
            break;
         }
         default:
         {
            PRINT_ERROR("Authentication algorithm not supported");
            break;
         }
      }
   }
   /* Release memory for response */
   destroy_response(&response);

   return(res);
}


/* ========================================================================== */
/* Enable compression on NNTP server
 *
 * \return
 * - 0 on success
 * - 1 if negotiation of the mandatory DEFLATE algorithm failed
 * - -1 on error
 */

#if !CFG_CMPR_DISABLE
static int  exec_compress(int  handle)
{
   int  res = -1;
   int  rv;
   struct nntp_response*  response = NULL;
   const char*  alg = NULL;

   /* Allocate memory for response */
   rv = create_response(&response);
   if(rv)  { PRINT_ERROR("Cannot allocate memory for response"); }
   else
   {
      /* Check whether server has COMPRESS capability */
      if(!(server[handle].capabilities & NNTP_CAPA_COMPRESS))
      {
         PRINT_ERROR("Server has no COMPRESS capability (bug)");
      }
      else
      {
         /* Select compression algorithm */
         if(server[handle].compress_algs & NNTP_COMPRESS_DEFLATE)
         {
#  if CFG_USE_ZLIB
            /* DEFLATE according to RFC 1951 */
            alg = "DEFLATE";
#  endif  /* CFG_USE_ZLIB */
         }
         if(NULL == alg)
         {
            PRINT_ERROR("No matching compression algorithm available");
            res = 1;
         }
         else
         {
            /* Enable compression */
            rv = exec_cmd(handle, NNTP_CMD_COMPRESS, response, alg);
            if(!rv && 206U == response->status)
            {
               /* NNTP negotiation for COMPRESS extension successful */
               log_add(server[handle].lfs,
                       "[Using COMPRESS (RFC 8054) extension]\n");
               res = 0;
            }
         }
      }
   }
   /* Release memory for response */
   destroy_response(&response);

   /* Create compressed data stream object */
   if(!res)
   {
      server[handle].compress_stream = cmpr_stream_constructor(CMPR_ALG_DEFLATE,
                                                               handle,
                                                               transport_send,
                                                               transport_recv);
      if(NULL == server[handle].compress_stream)  { res = -1; }
      else  { server[handle].compress_active = 1; }
   }

   /* Check for error */
   if(res)  { PRINT_ERROR("Enabling compression failed"); }

   return(res);
}
#endif  /* !CFG_CMPR_DISABLE */


/* ========================================================================== */
/* Get current capabilities from server */

static int  refresh_capabilities(int  handle)
{
   int  res = -1;
   struct nntp_response*  response = NULL;
   int  rv;
   unsigned int  v[2];
   unsigned int  ver;
   size_t  i, j;
   size_t  len;
   char*  p;
   unsigned int  version_old = server[handle].version;

   /* Allocate memory for response */
   res = create_response(&response);
   if(!res)
   {
      /*
       * Since NNTP version 2 the CAPABILITIES command is mandatory
       * If the current value is 1, this means that the CAPABILITIES command is
       * not supported and we do not send it again.
       */
      if(1U == version_old)  { res = 0; }
      else
      {
         res = exec_cmd(handle, NNTP_CMD_CAPABILITIES, response);
         if(!res)
         {
            /* Parse capabilities */
            if(101U != response->status)
            {
               /* NNTP V1 */
               log_add(server[handle].lfs,
                       "[Switch to NNTP V1 (RFC 977) mode]\n");
               server[handle].version = 1;
               /* Expect all required capabilities to be present */
               server[handle].capabilities = NNTP_CAPA_READER | NNTP_CAPA_LIST
                                             | NNTP_CAPA_POST
                                             | NNTP_CAPA_AUTHINFO_USER;
            }
            else
            {
               /* Capabilities must be parsed case insensitive */
               len = strlen(response->content);
               for(i = 0; i < len; ++i)
               {
                  response->content[i] = (char)
                                         toupper((int) response->content[i]);
               }
               /*
                * Extract protocol version from first line
                * Be prepared for something like "2 3".
                */
               rv = sscanf(response->content, "VERSION %u %u", &v[0], &v[1]);
               if(1 > rv)
               {
                  PRINT_ERROR("Protocol error: No version information");
               }
               else
               {
                  /* Select highest supported protocol version */
                  ver = 1;
                  for(i = 0; i < (size_t) rv; ++i)
                  {
                     if(v[i] > ver)  { ver = v[i]; }
                     if(2U == ver)  { break; }
                  }
                  if(2U == ver)
                  {
                     if(2U != version_old)
                     {
                        log_add(server[handle].lfs,
                                "[Switch to NNTP V2 (RFC 3977) mode]\n");
                     }
                     server[handle].version = 2U;
                  }
                  else
                  {
                     if(1U != ver)
                     {
                        log_add(server[handle].lfs,
                                "[Advertised protocol not supported]\n");
                     }
                     if(1U != version_old)
                     {
                        log_add(server[handle].lfs,
                                "[Switch to NNTP V1 (RFC 977) mode]\n");
                     }
                     server[handle].version = 1U;
                     /* Expect all required capabilities to be present */
                     server[handle].capabilities = NNTP_CAPA_READER
                                                   | NNTP_CAPA_LIST
                                                   | NNTP_CAPA_POST
                                                   | NNTP_CAPA_AUTHINFO_USER;
                  }

                  /* Parse capabilities */
                  if(2U <= ver)
                  {
                     server[handle].capabilities = 0;
                     if(NULL != strstr(response->content, "\nMODE-READER"))
                     {
                        server[handle].capabilities |= NNTP_CAPA_MODE_READER;
                     }
                     if(NULL != strstr(response->content, "\nREADER"))
                     {
                        server[handle].capabilities |= NNTP_CAPA_READER;
                     }
                     p = strstr(response->content, "\nMAXARTNUM");
                     if(NULL != p)
                     {
                        i = 0; while(p[++i])
                        {
                           if(!enc_ascii_check_digit(&p[i]))
                           {
                              for (j = i, len = 0; i + (size_t) 20 > j; ++j)
                              {
                                 if(!enc_ascii_check_digit(&p[j]))  { ++len; }
                              }
                              rv = enc_convert_ascii_to_anum(
                                 &server[handle].maxartnum, &p[i], len);
                              if (-2 == rv)
                              {
                                 /* Too large => Clamp to our limit */
                                 server[handle].maxartnum = NNTP_ANUM_T_MAX;
                                 rv = 0;
                              }
                              else if (!rv
                                  && (nntp_anum_t) 2147483647UL
                                     >= server[handle].maxartnum)
                              {
                                 /* Too small => Do not accept */
                                 server[handle].maxartnum = 0;
                                 rv = -1;
                              }
                              if(!rv)
                              {
                                 server[handle].capabilities
                                     |= NNTP_CAPA_MAXARTNUM;
                              }
                              break;
                           }
                        }
                     }
                     p = strstr(response->content, "\nLIST");
                     if(NULL != p)
                     {
                        server[handle].capabilities |= NNTP_CAPA_LIST;
                        i = 0; while(p[++i])
                        {
                           if('\n' == p[i])  { p[i] = 0;  break; }
                        }
                        if(NULL != strstr(p, "MOTD"))
                        {
                           server[handle].capabilities |= NNTP_CAPA_LIST_MOTD;
                        }
                        if(NULL != strstr(p, "DISTRIB.PATS"))
                        {
                           if(config[CONF_DIST_SUGG].val.i)
                           {
                              server[handle].capabilities
                                 |= NNTP_CAPA_LIST_DISTRIB_PATS;
                           }
                        }
                        if(NULL != strstr(p, "SUBSCRIPTIONS"))
                        {
                           server[handle].capabilities
                              |= NNTP_CAPA_LIST_SUBSCRIPTIONS;
                        }
                        p[i] = '\n';  /* Remove temp. NUL termination again */
                     }
                     if(NULL != strstr(response->content, "\nOVER"))
                     {
                        if(!config[CONF_NO_OVER].val.i)
                        {
                           server[handle].capabilities |= NNTP_CAPA_OVER;
                        }
                     }
                     if(NULL != strstr(response->content, "\nPOST"))
                     {
                        server[handle].capabilities |= NNTP_CAPA_POST;
                     }
                     p = strstr(response->content, "\nAUTHINFO");
                     if(NULL != p)
                     {
                        i = 0; while(p[++i])
                        {
                           if('\n' == p[i])  { p[i] = 0;  break; }
                        }
                        if(NULL != strstr(p, "USER"))
                        {
                           server[handle].capabilities
                              |= NNTP_CAPA_AUTHINFO_USER;
                        }
                        p[i] = '\n';  /* Remove temp. NUL termination again */
                     }
                     p = strstr(response->content, "\nCOMPRESS");
                     if(NULL != p)
                     {
                        i = 0; while(p[++i])
                        {
                           if('\n' == p[i])  { p[i] = 0;  break; }
                        }
                        if(NULL != strstr(p, "DEFLATE"))
                        {
                           server[handle].capabilities |= NNTP_CAPA_COMPRESS;
                           server[handle].compress_algs
                              |= NNTP_COMPRESS_DEFLATE;
                        }
                        p[i] = '\n';  /* Remove temp. NUL termination again */
                        if(!(server[handle].capabilities & NNTP_CAPA_COMPRESS))
                        {
                           PRINT_ERROR("Advertised compression "
                                       "algorithms not supported");
                        }
                     }
                  }
               }
            }
         }
      }
   }

   /* Release memory for response */
   destroy_response(&response);

   return(res);
}


/* ========================================================================== */
/* Get current overview format from server */

static int  refresh_overview_format(int  handle)
{
   int  res = -1;
   struct nntp_response*  response = NULL;
   size_t  i;
   size_t  len;
   size_t  field;  /* Index in the sense of OVER (0 is the article number) */
   char*  bol;  /* Beginning of line */
   const char*  string;
   int error = 0;
   size_t  newsgroups_available = 0;

   /* Return success if not available */
   if(!(NNTP_CAPA_OVER & server[handle].capabilities))  { res = 0; }
   else
   {
      /* Allocate memory for response */
      res = create_response(&response);
      if(!res)
      {
         res = exec_cmd(handle, NNTP_CMD_LIST_OVERVIEW_FMT, response);
         if(!res)
         {
            if(215U != response->status)
            {
               log_add(server[handle].lfs,
                       "[Reading overview format failed]\n");
               res = -1;
            }
            else
            {
               /* Overview format must be parsed case insensitive */
               len = strlen(response->content);
               for(i = 0; i < len; ++i)
               {
                  response->content[i] = (char)
                                         toupper((int) response->content[i]);
               }
               /* Check mandatory fields of overview format and their order */
               bol = response->content;
               field = 1;
               while (1)
               {
                  switch(field)
                  {
                     case 0:
                     {
                        error = 1;
                        break;
                     }
                     case 1:
                     {
                        string = "SUBJECT:";
                        if(strncmp(bol, string, strlen(string)))  { error = 1; }
                        break;
                     }
                     case 2:
                     {
                        string = "FROM:";
                        if(strncmp(bol, string, strlen(string)))  { error = 1; }
                        break;
                     }
                     case 3:
                     {
                        string = "DATE:";
                        if(strncmp(bol, string, strlen(string)))  { error = 1; }
                        break;
                     }
                     case 4:
                     {
                        string = "MESSAGE-ID:";
                        if(strncmp(bol, string, strlen(string)))  { error = 1; }
                        break;
                     }
                     case 5:
                     {
                        string = "REFERENCES:";
                        if(strncmp(bol, string, strlen(string)))  { error = 1; }
                        break;
                     }
                     case 6:
                     {
                        string = ":BYTES";
                        if(strncmp(bol, string, strlen(string)))
                        {
                           string = "BYTES:";
                           if(strncmp(bol, string, strlen(string)))
                           {
                              error = 1;
                           }
                        }
                        break;
                     }
                     case 7:
                     {
                        string = ":LINES";
                        if(strncmp(bol, string, strlen(string)))
                        {
                           string = "LINES:";
                           if(strncmp(bol, string, strlen(string)))
                           {
                              error = 1;
                           }
                        }
                        break;
                     }
                     default:
                     {
                        string = "NEWSGROUPS:FULL";
                        if(!strncmp(bol, string, strlen(string)))
                        {
                           /* Newsgroups header field detected */
                           log_add(server[handle].lfs,
                                   "[Newsgroups header field "
                                   "available from overview]\n");
                           newsgroups_available = field;
                        }
                        break;
                     }
                  }
                  if(error)
                  {
                     res = -1;
                     break;
                  }

                  /* Skip to start of next line */
                  bol = strchr(bol, (int) '\n');
                  if (NULL == bol)
                  {
                     /* There are 7 mandatory fields after article number */
                     if(7 > field)  { res = -1; }
                     break;
                  }
                  else
                  {
                     ++bol;
                     ++field;
                  }
               }
            }
         }
      }
      /* Release memory for response */
      destroy_response(&response);
   }

   /* Store index of Newsgroups header field in overview */
   if (!res)
   {
      server[handle].over_newsgroups = newsgroups_available;
   }

   return(res);
}


/* ========================================================================== */
/* Ensure that NNTP server is in READER mode */

static int  switch_mode_reader(int  handle)
{
   int  res = 0;
   struct nntp_response*  response = NULL;

   if(NNTP_CAPA_MODE_READER & server[handle].capabilities)
   {
      /* Allocate memory for response */
      res = create_response(&response);
      if(!res)
      {
         res = exec_cmd(handle, NNTP_CMD_MODE_READER, response);
         if (!res)
         {
            /* Refresh capabilities of server */
            res = refresh_capabilities(handle);
            if(!(NNTP_CAPA_READER & server[handle].capabilities))
            {
               /* Switch to reading mode failed */
               PRINT_ERROR("Protocol error: No READER capability "
                           "after (advertised) mode switch");
               res = -1;
            }
         }
      }
      /* Release memory for response */
      destroy_response(&response);
   }

   return(res);
}


/* ========================================================================== */
/* Negotiate maximum supported article number (MAXARTNUM extension) */

static int  negotiate_maxartnum(int  handle)
{
   int  res = 0;
   struct nntp_response*  response = NULL;
   nntp_anum_t  maxartnum = NNTP_ANUM_T_MAX;
   char  argument[NNTP_ARGLENGTHMAX + (size_t) 1];
   int rv;

   if(NNTP_CAPA_MAXARTNUM & server[handle].capabilities)
   {
      /* Create string with our maximum supported article number */
#if ULONG_MAX < NNTP_ANUM_T_MAX
      PRINT_ERROR("Maximum article number does not fit in largest data type");
      res = -1;
#else
      if (maxartnum > server[handle].maxartnum)
      {
         maxartnum = server[handle].maxartnum;
      }
      rv = api_posix_snprintf(argument, NNTP_ARGLENGTHMAX + (size_t) 1,
                              "%lu", (unsigned long int) maxartnum);
      if (0 > rv || NNTP_ARGLENGTHMAX < (size_t) rv)
      {
         PRINT_ERROR("Maximum article number string generation failed");
         res = -1;
      }
#endif
      if (!res)
      {
         /* Allocate memory for response */
         res = create_response(&response);
         if(!res)
         {
            res = exec_cmd(handle, NNTP_CMD_MAXARTNUM, response, argument);
            if (!res)
            {
               if(2U != response->code1)
               {
                  /* Command was not successful */
                  PRINT_ERROR("Maximum article number negotiation failed");
                  res = -1;
               }
               else
               {
                  /* Refresh capabilities of server */
                  res = refresh_capabilities(handle);
                  if(NNTP_CAPA_MAXARTNUM & server[handle].capabilities)
                  {
                     /* Switch to reading mode failed */
                     PRINT_ERROR("Protocol error: MAXARTNUM capability still "
                                 "advertised after negotiation");
                     res = -1;
                  }
               }
            }
         }
         /* Release memory for response */
         destroy_response(&response);
      }
   }

   return(res);
}


/* ========================================================================== */
/* Get distribution patterns */

static int  get_distrib_pats(int  handle)
{
   int  res = 0;
   struct nntp_response*  response = NULL;

   /* Destroy old data */
   server[handle].distrib_pats_len = 0;
   if(NULL != server[handle].distrib_pats)
   {
      api_posix_free((void*) server[handle].distrib_pats);
   }
   server[handle].distrib_pats = NULL;

   /* Return success if not available */
   if(NNTP_CAPA_LIST_DISTRIB_PATS & server[handle].capabilities)
   {
      /* Allocate memory for response */
      res = create_response(&response);

      /* Send LIST DISTRIB.PATS command */
      if(!res)
      {
         res = exec_cmd(handle, NNTP_CMD_LIST_DISTRIB_PATS, response);
      }
      if(!res)
      {
         if(2U != response->code1)
         {
            /* Command was not successful */
            res = -1;
            if(503U == response->status)
            {
               PRINT_ERROR("Server reported that no distribution patterns"
                           " are maintained");
               /* NULL pointer and nonzero length indicate "not maintained" */
               server[handle].distrib_pats_len = 1;
               res = 0;
            }
         }
         else
         {
            /* Command was successful */
            server[handle].distrib_pats_len = response->bufsize;
            server[handle].distrib_pats = response->content;
            /* Preserve content buffer */
            response->content = NULL;
         }
      }

      /* Release memory for response */
      destroy_response(&response);
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Open connection to NNTP server
 *
 * \param[out] handle      Pointer to server descriptor
 * \param[in]  servername  Server name or address string
 * \param[in]  service     Service name or port
 * \param[in]  logfile     Logfile name
 * \param[in]  enc         Encryption algorithm ID (for connection)
 * \param[in]  auth        Authentication algorithm ID (client against server)
 *
 * \e servername must be an IPv4 address in dotted decimal representation, an
 * IPv6 address in colon separated hexadecimal representation or a hostname that
 * can be resolved to an IPv4 address via DNS.
 *
 * \e service must be a valid service name like \c nntp or \c nntps or it must
 * be the target TCP port on which the NNTP or NNTPS service is listening. The
 * default value is 119 (or 563 for NNTP over TLS respectively).
 *
 * If \e logfile is \c NULL then log data is redirected to \c /dev/null .
 *
 * This function handles optional encryption for the connection to the server if
 * \e enc is nonzero. The following encryption algorithms are supported:
 * - 0: No encryption
 * - 1: RFC 2246 / RFC 4346 / RFC 5246 / RFC 8446 conformant TLS
 *      <br>This includes the authentication of the server against the client
 *      using a X509 certificate.
 * - 2: Same as 1 but additionally offer weak cipher suites to server
 *      <br>The list includes (at the end) the cipher suite RSA-RSA-RC4-MD5 that
 *      was defined as mandatory by RFC 4642 in the past.
 *      <br>Not recommended! Use only if algorithm 1 is not accepted by server.
 *
 * This function handles optional authentication of the client against the
 * server if \e auth is nonzero. The following authentication algorithms are
 * supported:
 * - 0: No authentication
 * - 1: RFC 4643 conformant AUTHINFO USER/PASS authentication
 *      <br>An encrypted connection is mandatory for this algorithm!
 *      <br>The first optional parameter is a flag of type \c int that indicates
 *      whether authentication should be done immediately.
 *      <br>There must be two additional string parameters of type \c const
 *      \c char* that specify username and password as plain text. Both strings
 *      are copied and the pointers are not required to stay valid after this
 *      function returns.
 *
 * \return
 * - 0 on success (a valid connection handle was written to \e handle )
 * - Negative value on error
 */

int  nntp_open(int*  handle, const char*  servername, const char*  service,
               const char*  logfile, int  enc, int  auth, ...)
{
   va_list  ap;                    /* Object for argument list handling */
   int  res = 0;
   int  af = API_POSIX_AF_UNSPEC;      /* Network address family */
   struct nntp_response*  response = NULL;
   char  sbuf[7];
   FILE*  fs;
#if CFG_USE_TLS || !CFG_CMPR_DISABLE
   int  rv;
#endif  /* CFG_USE_TLS || !CFG_CMPR_DISABLE */
#if CFG_USE_TLS
   int  weak = 0;  /* Offer weak/unsecure cipher suites to server */
   const char*  sni = NULL;  /* Hostname for "server_name" TLS extension */
   const char*  pv;  /* Protocol version string */
   const char*  cs;  /* Cipher suite string */
   const char*  kx;  /* Key exchange (not part of cipher suite in TLSv1.3) */
   void*  cert;  /* Pointer to certificate object (from server) */
   const char*  certs;  /* Certificate string (from server) */
   int  immed = 0;
   const char*  s1;
   char*  s2;
   size_t  len;
#endif  /* CFG_USE_TLS */

   *handle = -1;
   va_start(ap, auth);

   /* Sanity checks */
   if(0 > enc || 2 < enc)
   {
      PRINT_ERROR("Requested encryption algorithm not supported");
      res = -1;
   }
   else if(0 > auth || 1 < auth)
   {
      PRINT_ERROR("Requested authentication algorithm not supported");
      res = -1;
   }
#if !CFG_NNTP_AUTH_UNENCRYPTED
   else if(1 == auth && !enc)
   {
      PRINT_ERROR("Encryption required for selected authentication algorithm");
      res = -1;
   }
#endif

   /* Allocate memory for response */
   if(!res)  { res = create_response(&response); }

   /* Allocate connection object */
   if(!res)
   {
      res = get_handle(handle);
      if(!res)
      {
         server[*handle].auth = auth;
#if CFG_USE_TLS
         if(1 == auth)
         {
            /* Get immediate flag */
            immed = va_arg(ap, int);
            /* Copy user and password to server handle */
            s1 = va_arg(ap, const char*);
            len = strlen(s1);
            s2 = (char*) api_posix_malloc(++len);
            if(NULL == s2)  { res = -1; }
            else
            {
               memcpy(s2, s1, len);
               server[*handle].user = s2;
               s1 = va_arg(ap, const char*);
               len = strlen(s1);
               s2 = (char*) api_posix_malloc(++len);
               if(NULL == s2)  { res = -1; }
               else
               {
                  memcpy(s2, s1, len);
                  server[*handle].passwd = s2;
               }
            }
         }
#endif  /* CFG_USE_TLS */
      }
   }

   /* Open logfile */
   if(!res)
   {
      res = log_open_logfile(&fs, logfile);
      if(!res)  { server[*handle].lfs = fs; }
   }

   /* Connect to server */
   if(!res)
   {
      log_add(server[*handle].lfs, "[Using NNTP protocol driver]\n");
      log_add(server[*handle].lfs, "[=> Connect to ");
      log_add(server[*handle].lfs, servername);
      api_posix_snprintf(sbuf, 7, ":%s", service);
      log_add(server[*handle].lfs, sbuf);
      log_add(server[*handle].lfs, "]\n");
      res = inet_connect(&server[*handle].sd, &af, servername, service);
      /* Set RX and TX timeouts if supported by OS */
      inet_set_rx_timeout(server[*handle].sd, 15U);
      inet_set_tx_timeout(server[*handle].sd, 15U);
   }

   /* Establish encrypted connection */
   if(!res && enc)
   {
#if CFG_USE_TLS
      if(2 == enc)  { weak = 1; }
      sni = tls_sni(servername);
      res = tls_open(server[*handle].sd, &server[*handle].eco, weak, sni);
      if(res)
      {
         PRINT_ERROR("Failed to establish encryption layer");
      }
      else
      {
         log_add(server[*handle].lfs, "[Established encrypted connection");
         rv = tls_get_ciphersuite(&server[*handle].eco, &pv, &cs, &kx);
         if(!rv)
         {
            log_add(server[*handle].lfs, " using ");
            log_add(server[*handle].lfs, pv);
            log_add(server[*handle].lfs, " protocol with cipher suite ");
            log_add(server[*handle].lfs, cs);
         }
         log_add(server[*handle].lfs, "]\n");
      }
      if(!res)
      {
         /* Verify certificate received from server */
         rv = tls_cert_verify(&server[*handle].eco, &cert, servername, weak);
         if(0 <= rv)
         {
            log_add(server[*handle].lfs, "[<= Expect X509 certificate]\n");
            if(!tls_cert_get_string(cert, &certs))
            {
               log_add(server[*handle].lfs, certs);
               tls_free((void*) certs);
            }
         }
         if(rv)
         {
            log_add(server[*handle].lfs,
                    "[Server certificate verification failed]\n");
            if(-2 == rv)  { res = rv; }  else  { res = -1; }
         }
         else
         {
            log_add(server[*handle].lfs,
                    "[Server certificate verification successful]\n");
         }
      }
#else  /* CFG_USE_TLS */
      PRINT_ERROR("Compiled without TLS support");
      res = -1;
#endif  /* CFG_USE_TLS */
   }

   /* Receive reply */
   if(!res)  { res = recv_reply(*handle, NNTP_CMD_INIT, response); }

   /* Release memory for response */
   destroy_response(&response);

   /* Get capabilities of server */
   if(!res)  { res = refresh_capabilities(*handle); }

   /* Switch server to READER mode */
   if(!res)  { res = switch_mode_reader(*handle); }

   /* Check for error */
   if(res)
   {
      if(-2 == res)
      {
         /* TLS module has requested to close the connection */
         PRINT_ERROR("TLS module has requested to close the connection");
      }
      else  { PRINT_ERROR("Cannot connect to server"); }
      nntp_close(handle, NNTP_CLOSE_NOQUIT);
   }
   else
   {
      server[*handle].connected = 1;

#if CFG_USE_TLS
      /* Do authentication immediately on request */
      if(1 == auth && immed)
      {
         if(exec_auth(*handle))
         {
            /* Authentication failed */
            log_add(server[*handle].lfs, "[Authentication failed]\n");
            res = -3;
         }
         else  { res = refresh_capabilities(*handle); }
      }
#endif  /* CFG_USE_TLS */

      /* Negotiate maximum article number (continue after error) */
      if(!res)  { (void) negotiate_maxartnum(*handle); }

      /* Don't enable compression if deferred authentication is configured */
      if(!res && 0 >= server[*handle].auth)
      {
         /* Enable compression if supported by server */
         if(server[*handle].capabilities & NNTP_CAPA_COMPRESS)
         {
#if !CFG_CMPR_DISABLE
            /* Check that negotiation is not disabled by user */
            if(config[CONF_COMPRESSION].val.i)
            {
               rv = exec_compress(*handle);
               if(0 < rv)
               {
                  /* Compression algorithm negotiation failed */
                  log_add(server[*handle].lfs,
                          "[No matching compression algorithm available]\n");
               }
               if(rv)
               {
                  /* Enabling compression failed */
                  log_add(server[*handle].lfs, "[Enabling compression failed]\n");
               }
               else  { res = refresh_capabilities(*handle); }
               /* Ignore errors and continue without compression */
            }
            else
            {
               log_add(server[*handle].lfs,
                       "[Compression negotiation disabled by user]\n");
            }
#else  /* !CFG_CMPR_DISABLE */
            log_add(server[*handle].lfs,
                    "[Compression disabled by configuration]\n");
#endif  /* !CFG_CMPR_DISABLE */
         }
      }
   }

   /* Get overview format (only once with the current implementation) */
   if(!res)
   {
      res = refresh_overview_format(*handle);
      if(res)
      {
         PRINT_ERROR("Reading overview format failed of format invalid");
      }
   }

   /* Get distribution patterns */
   if(!res)
   {
      if(get_distrib_pats(*handle))
      {
         PRINT_ERROR("Reading distribution patterns failed");
      }
   }

   va_end(ap);

   return(res);
}


/* ========================================================================== */
/*! \brief Disconnect from NNTP server
 *
 * \param[in,out] handle  Pointer to server descriptor
 * \param[in]     flags   Control flags (use \c NNTP_CLOSE_xxx constants)
 */

void  nntp_close(int*  handle, unsigned int  flags)
{
   int  rv;
   struct nntp_response*  response = NULL;

   if(-1 != *handle)
   {
      /* Send QUIT command (if not disabled) */
      if(!(NNTP_CLOSE_NOQUIT & flags))
      {
         /* This will automatically exec a termination flush for compression */
         rv = create_response(&response);
         if(!rv)  { exec_cmd(*handle, NNTP_CMD_QUIT, response); }
         destroy_response(&response);
      }
#if !CFG_CMPR_DISABLE
      /* Shutdown compression layer */
      if(server[*handle].compress_active)
      {
         /* Destroy compressed data stream object */
         cmpr_stream_destructor(server[*handle].compress_stream);
         log_add(server[*handle].lfs, "[Compression layer terminated]\n");
      }
#endif  /* !CFG_CMPR_DISABLE */
#if CFG_USE_TLS
      /* Shutdown encryption layer */
      if(NULL != server[*handle].eco)
      {
         tls_close(&server[*handle].eco);
         log_add(server[*handle].lfs, "[Encryption layer terminated]\n");
      }
      api_posix_free((void*) server[*handle].user);
      api_posix_free((void*) server[*handle].passwd);
#  if CFG_USE_OPENSSL_API_1_1 && !CFG_USE_OPENSSL_API_3
      api_posix_free((void*) server[*handle].peekbuf);
#  endif  /* CFG_USE_OPENSSL_API_1_1 && !CFG_USE_OPENSSL_API_3 */
#endif  /* CFG_USE_TLS */
      /* Destroy distribution patterns */
      api_posix_free((void*) server[*handle].distrib_pats);
      /* Close logfile */
      if(NULL != server[*handle].lfs)
      {
         log_add(server[*handle].lfs, "[Close connection]\n\n");
         log_close_logfile(&server[*handle].lfs);
      }
      /* Terminate network connection */
      if(-1 != server[*handle].sd)  { inet_close(&server[*handle].sd); }
      server[*handle].connected = 0;
      *handle = -1;
   }

   return;
}


/* ========================================================================== */
/*! \brief Get message of the day capability of NNTP server
 *
 * \param[in] handle  Server descriptor
 *
 * This function will trigger no communication with the server. It simply
 * reports the currently buffered state for the server associated with
 * \e handle .
 *
 * \return
 * - \c LIST MOTD capability flag (nonzero if capability is available)
 */

int  nntp_get_capa_list_motd(int  handle)
{
   int  res = 0;

   if(-1 != handle)
   {
      if(server[handle].capabilities & NNTP_CAPA_LIST_MOTD)  { res = 1; }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Get message of the day
 *
 * This option is defined in RFC 6048.
 *
 * \param[in]  handle  Server descriptor
 * \param[out] data    Pointer to MOTD data buffer pointer
 * \param[out] len     Pointer to buffer size
 *
 * On success, the caller is responsible for releasing the memory allocated
 * for the array \e data .
 *
 * \return
 * - 0 on success
 * - -1 on error
 * - -2 if connection to server is broken
 * - -3 if authentication requested by server failed
 */

int  nntp_get_motd(int  handle, char**  data, size_t*  len)
{
   int  res;
   struct nntp_response*  response = NULL;

   /* Allocate memory for response */
   res = create_response(&response);

   /* Send LIST MOTD command */
   if(!res)  { res = exec_cmd(handle, NNTP_CMD_LIST_MOTD, response); }
   if(!res)
   {
      if(2U != response->code1)
      {
         if(503U == response->status)
         {
            PRINT_ERROR("Server reported that no MOTD is maintained");
         }
         res = -1;
      }
      else
      {
         /* Command wass successful */
         *len = response->bufsize;
         *data = response->content;
         /* Preserve content buffer */
         response->content = NULL;
      }
   }

   /* Release memory for response */
   destroy_response(&response);

   return(res);
}


/* ========================================================================== */
/*! \brief Get distribution patterns
 *
 * This option is defined in RFC 3977.
 *
 * \param[in]  handle  Server descriptor
 * \param[out] data    Pointer to pattern data buffer
 * \param[out] len     Pointer to buffer size
 *
 * \note
 * It is allowed to pass \c NULL for \e len if the caller is not interested in
 * this value.
 *
 * This function will trigger no communication with the server. On success it
 * returns a pointer to the currently buffered data for the server associated
 * with \e handle that was retrieved while opening the connection.
 *
 * \note
 * No extra memory is allocated for the caller, so there is nothing to free.
 *
 * \return
 * - 0 on success
 * - 1 if not available
 * - -1 on error
 */

int  nntp_get_distrib_pats(int  handle, const char**  data, size_t*  len)
{
   int  res = 1;

   *data = NULL;
   if(NULL != len)  { *len = 0; }
   if(server[handle].capabilities & NNTP_CAPA_LIST_DISTRIB_PATS)
   {
      res = -1;
      if(server[handle].distrib_pats_len)
      {
         /* NULL pointer with nonzero length indicates "not maintained */
         if(NULL == server[handle].distrib_pats)  { res = 1; }
         else
         {
            *data = server[handle].distrib_pats;
            if(NULL != len)  { *len = server[handle].distrib_pats_len; }
            res = 0;
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Get message of the day capability of NNTP server
 *
 * \param[in] handle  Server descriptor
 *
 * This function will trigger no communication with the server. It simply
 * reports the currently buffered state for the server associated with
 * \e handle .
 *
 * \return
 * - \c LIST SUBSCRIPTIONS capability flag (nonzero if capability is available)
 */

int  nntp_get_capa_list_subscriptions(int  handle)
{
   int  res = 0;

   if(-1 != handle)
   {
      if(server[handle].capabilities & NNTP_CAPA_LIST_SUBSCRIPTIONS)
      {
         res = 1;
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Get subscription proposals
 *
 * This option is defined in RFC 6048.
 *
 * \param[in]  handle  Server descriptor
 * \param[out] data    Pointer to subscriptions data buffer pointer
 * \param[out] len     Pointer to buffer size
 *
 * On success, the caller is responsible for releasing the memory allocated
 * for the array \e data .
 *
 * \return
 * - 0 on success
 * - -1 on error
 * - -2 if connection to server is broken
 * - -3 if authentication requested by server failed
 */

int  nntp_get_subscriptions(int  handle, char**  data, size_t*  len)
{
   int  res;
   struct nntp_response*  response = NULL;

   /* Allocate memory for response */
   res = create_response(&response);

   /* Send LIST SUBSCRIPTIONS command */
   if(!res)  { res = exec_cmd(handle, NNTP_CMD_LIST_SUBSCRIPTIONS, response); }
   if(!res)
   {
      if(2U != response->code1)
      {
         if(503U == response->status)
         {
            PRINT_ERROR("Server reported that no SUBSCRIPTIONS are maintained");
         }
         res = -1;
      }
      else
      {
         /* Command wass successful */
         *len = response->bufsize;
         *data = response->content;
         /* Preserve content buffer */
         response->content = NULL;
      }
   }

   /* Release memory for response */
   destroy_response(&response);

   return(res);
}


/* ========================================================================== */
/*! \brief Allocate and initialize a descriptor for group
 *
 * \param[in] name  Group name
 *
 * An empty group descriptor is created and \e name is copied behind this
 * descriptor so that the whole object can be destroyed as a single memory
 * block.
 *
 * On success, the caller is responsible to destroy the allocated object.
 *
 * \return
 * - Pointer to initialized group descriptor on success
 * - NULL on error
 */

struct nntp_groupdesc*  nntp_group_descriptor_constructor(const char*  name)
{
   struct nntp_groupdesc*  gd = NULL;
   size_t  l;
   size_t  ls;

   /* Allocate memory for group descriptor */
   l = sizeof(struct nntp_groupdesc);
   ls = strlen(name) + (size_t) 1;
   gd = (struct nntp_groupdesc*) api_posix_malloc(l + ls);
   if (NULL != gd)
   {
      /* Copy group name to descriptor */
      strcpy(&((char*) gd)[l], name);
      gd->name = &((char*) gd)[l];
      gd->eac = 0;
      gd->lwm = 0;
      gd->hwm = 0;
      gd->flags = 0;
   }

   return(gd);
}


/* ========================================================================== */
/*! \brief Get group list
 *
 * \param[in]  handle      Server descriptor
 * \param[out] groupcount  Pointer to number of groups
 * \param[out] p           Pointer to array of group descriptors
 *
 * On success, the caller is responsible for releasing the memory allocated
 * for the array \e p .
 *
 * \note
 * If \e groupcount is zero, \e p may be \c NULL .
 *
 * \return
 * - 0 on success
 * - -1 on error
 * - -2 if connection to server is broken
 * - -3 if authentication requested by server failed
 */

int  nntp_get_grouplist(int  handle, size_t*  groupcount,
                        struct nntp_groupdesc**  p)
{
   int  res;
   struct nntp_response*  response = NULL;
   char*  content = NULL;
   int  ascii;
   int  field;
   size_t  name;
   nntp_anum_t  n;
   nntp_anum_t  lwm;
   nntp_anum_t  hwm;
   int  pa;  /* Posting allowed flag */
   size_t  i = 0;
   int  invalid;
   int  c;
   char  wm[16];
   int  wm_len;
   int  wm_flag;
   size_t  gi = 0;
   int  rv;

   /* Init values returned to caller */
   *groupcount = 0;
   *p = NULL;

   /* Allocate memory for response */
   res = create_response(&response);

   /* Send LIST [ACTIVE] command */
   if(!res)
   {
      res = exec_cmd(handle, NNTP_CMD_LIST, response);
      /* Verify that list is not empty */
      if (!res && NULL == response->content)  { res = -1; }
   }

   /* Allocate memory for group descriptors and group name strings */
   if(!res)
   {
      /* printf("Lines: %u\n", (unsigned int) response->lines); */
      *p = (struct nntp_groupdesc*)
           api_posix_malloc(sizeof(struct nntp_groupdesc) * response->lines
                            + response->bufsize);
      if (NULL == *p)
      {
         PRINT_ERROR("Memory allocation for group list failed");
         res = -1;
      }
      else
      {
         /* Append content after group descriptor table */
         content = (void*) &(*p)[response->lines];
         memcpy((void*) content, (void*) response->content, response->bufsize);
      }
   }

   /* Release memory for response */
   destroy_response(&response);

   /* Parse response data */
   if(!res)
   {
      /*
       * The response data has the following format for every line:
       *
       * <group name> <high water mark> <low water mark> <status>
       *
       * An empty response is valid.
       * The fields are separated by one or more spaces.
       * The water mark numbers may contain leading zeros.
       *
       * A status 'n' is taken as "posting is not allowed", all other status
       * indications are taken as "posting allowed" (try at least).
       * Invalid lines are ignored and the processing is not aborted.
       */

      /* Response parser */
      while(content[i])
      {
         /* Parse next line */
         invalid = 1;
         field = 0;
         name = i;
         ascii = 1;
         wm_len = 0;
         wm_flag = 0;
         lwm = 0;
         hwm = 0;
         pa = 1;
         do
         {
            /* Get next character */
            c = (int) content[i];
            if(!c)  { break; }
            /* Field parser state machine */
            switch(field)
            {
               case 0:  /* Group name */
               {
                  /* Check for end of field */
                  if(0x20 == c)
                  {
                     /* Terminate group name string */
                     content[i] = 0;
                     /* Check whether group name is ASCII encoded */
                     if(enc_ascii_check_printable(&content[name]))
                     {
                        /* Treat nonprintable ASCII characters as Unicode */
                        ascii = 0;
                     }
                     /* Check whether name encoding is valid UTF-8 */
                     if(!ascii)
                     {
                        if(enc_uc_check_utf8(&content[name]))
                        {
                           /* Name field encoding is invalid */
                           PRINT_ERROR("Unicode group name invalid");
                           field = -1;
                           break;
                        }
                     }
                     ++field;
                  }
                  break;
               }
               case 1:  /* Potential additional spaces */
               {
                  if(0x20 == c)  { break; }
                  ++field;
                  /* No break here is intended */
               }
               /* FALLTHROUGH */
               case 2:  /* Water mark */
               {
                  rv = parse_number(c, wm, &wm_len, &n);
                  if(-1 == rv)  { field = -1;  break; }
                  if(!rv)
                  {
                     if(!wm_flag)
                     {
                        hwm = n;
                        wm_flag = 1;
                        wm_len = 0;
                        field = 1;
                     }
                     else
                     {
                        lwm = n;
                        ++field;
                     }
                  }
                  break;
               }
               case 3:  /* Potential additional spaces */
               {
                  if(0x20 == c)  { break; }
                  ++field;
                  /* No break here is intended */
               }
               /* FALLTHROUGH */
               case 4:  /* Status */
               {
                  /* Check whether posting into this group is allowed */
                  if(invalid)
                  {
                     if('n' == c)  pa = 0;
                     /* Mark line as valid */
                     invalid = 0;
                  }
                  break;
               }
               default:
               {
                  /* Error */
                  invalid = 1;
                  break;
               }
            }
         }
         while((char) 0x0A != content[i++]);

         /* Store data */
         if(invalid)
         {
            if (!gi && !content[i])
            {
               /* Empty list */
               PRINT_ERROR("Empty group list received");
            }
            else
            {
               /* Invalid group entry found */
               PRINT_ERROR("Unsupported entry in group list ignored");
            }
         }
         else
         {
#if 0
            /* For debugging only */
            printf("%lu: %s %lu %lu %d\n", (unsigned long int) gi,
                   &content[name], hwm, lwm, pa);
#endif
            /* Store group name */
            (*p)[gi].name = &content[name];
            /* Store watermarks */
            (*p)[gi].lwm = lwm;
            (*p)[gi].hwm = hwm;
            /* Store estimated article count */
            (*p)[gi].eac = 0;
            if(hwm > lwm)  { (*p)[gi].eac = hwm - lwm + (nntp_anum_t) 1; }
            /* Store group flags */
            (*p)[gi].flags = 0;
            if(ascii) { (*p)[gi].flags |= NNTP_GROUP_FLAG_ASCII; }
            if(pa) { (*p)[gi].flags |= NNTP_GROUP_FLAG_PA; }
            /* Switch to next group */
            ++gi;
         }
      }

      /* Store group count */
      *groupcount = gi;
   }

   /* Before returning error: Release memory for group descriptors */
   if(res)
   {
      api_posix_free(*p);
      *p = NULL;
      *groupcount = 0;
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Get additional group information
 *
 * \param[in]  handle  Server descriptor
 * \param[out] groupcount  Pointer to number of groups
 * \param[out] p           Pointer to array of group descriptors
 *
 * On success, the caller is responsible for releasing the memory allocated
 * for the array \e p .
 *
 * \note
 * If \e groupcount is zero, \e p may be \c NULL .
 *
 * \return
 * - 0 on success
 * - -1 on error
 * - -2 if connection to server is broken
 * - -3 if authentication requested by server failed
 */

int  nntp_get_group_labels(int  handle, size_t*  groupcount,
                           struct nntp_grouplabel**  p)
{
   int  res;
   struct nntp_response*  response = NULL;
   char*  content = NULL;
   int  field;
   size_t  name;
   size_t  label = 0;
   size_t  i = 0;
   int  invalid;
   int  c;
   size_t  gi = 0;

   /* Init values returned to caller */
   *groupcount = 0;
   *p = NULL;

   /* Allocate memory for response */
   res = create_response(&response);

   /* Send LIST NEWSGROUPS command */
   if(!res)  { res = exec_cmd(handle, NNTP_CMD_LIST_NEWSGROUPS, response); }

   /* Check response */
   if(!res)
   {
      if(2U != response->code1)
      {
         if(503U == response->status)
         {
            PRINT_ERROR("Server reported that descriptions are not maintained");
         }
         res = -1;
      }
   }
   /* Verify that list is not empty */
   if (!res && NULL == response->content)  { res = -1; }

   /* Allocate memory for group descriptors and group name strings */
   if(!res)
   {
      /* printf("Lines: %u\n", (unsigned int) response->lines); */
      *p = (struct nntp_grouplabel*)
           api_posix_malloc(sizeof(struct nntp_grouplabel)* response->lines
                                   + response->bufsize);
      if (NULL == *p)
      {
         PRINT_ERROR("Memory allocation for group information failed");
         res = -1;
      }
      else
      {
         /* Append content after group descriptor table */
         content = (void*) &(*p)[response->lines];
         memcpy((void*) content, (void*) response->content, response->bufsize);
      }
   }

   /* Release memory for response */
   destroy_response(&response);

   /* Parse response data */
   if(!res)
   {
      /*
       * The response data has the following format for every line:
       *
       * <group name> <group information>
       *
       * An empty response is valid.
       * The fields are separated by whitespace.
       */

      /* Response parser */
      while(content[i])
      {
         /* Parse next line */
         invalid = 1;
         field = 0;
         name = i;
         do
         {
            /* Get next character */
            c = (int) content[i];
            if(!c)  { break; }
            /* Field parser state machine */
            switch(field)
            {
               case 0:  /* Group name */
               {
                  /* Check for end of field */
                  if(0x20 == c || 0x09 == c)
                  {
                     /* Terminate group name string */
                     content[i] = 0;
                     ++field;
                  }
                  break;
               }
               case 1:  /* Potential additional spaces */
               {
                  if(0x20 == c || 0x09 == c)  { break; };
                  label = i;
                  ++field;
                  /* No break here is intended */
               }
               /* FALLTHROUGH */
               case 2:  /* Group description */
               {
                  /* Check for end of field */
                  if(0x0D == c)
                  {
                     /* Terminate group name string */
                     content[i] = 0;
                     /* Mark line as valid */
                     invalid = 0;
                  }
                  break;
               }
               default:
               {
                  /* Error */
                  invalid = 1;
                  break;
               }
            }
         }
         while((char) 0x0A != content[i++]);

         /* Store data */
         if(invalid)
         {
            /* Invalid group entry found */
            PRINT_ERROR("Unsupported entry in group label list ignored");
         }
         else
         {
#if 0
            /* For debugging only */
            printf("%lu: %s %s\n", (unsigned long int) gi,
                   &content[name], &content[label]);
#endif
            /* Store group name */
            (*p)[gi].name = &content[name];
            /* Store group information */
            (*p)[gi].label = &content[label];
            /* Switch to next group */
            ++gi;
         }
      }

      /* Store group count */
      *groupcount = gi;
   }

   /* Before returning error: Release memory for group descriptors */
   if(res)
   {
      api_posix_free(*p);
      *p = NULL;
      *groupcount = 0;
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Set current group
 *
 * \param[in]  handle  Server descriptor
 * \param[in]  name    Group name
 * \param[out] gd      Pointer to group descriptor
 *
 * On success and if \e gd is not \c NULL , a pointer to an initialized group
 * descriptor for the group specified by \e name is written to \e gd .
 * The caller is responsible for releasing the memory allocated for this group
 * descriptor.
 *
 * \return
 * - 0 on success
 * - -1 on error
 * - -2 if connection to server is broken
 * - -3 if authentication requested by server failed
 */


int  nntp_set_group(int  handle, const char*  name, struct nntp_groupdesc**  gd)
{
   int  res;
   struct nntp_response*  response = NULL;
   char  wm[16];
   int  wm_len = 0;
   int  wm_flag = 0;
   int  field = 0;
   char*  s;
   int  rv;
   nntp_anum_t  n;
   size_t  i = 0;
   int  c;
   int  abort = 0;
   unsigned int  flags = 0;

   if(NULL != gd)  { *gd = NULL; }

   /* Allocate memory for response */
   res = create_response(&response);

   /* Check encoding of group name */
   if(!res)
   {
      if(enc_ascii_check(name))
      {
         if(enc_uc_check_utf8(name))
         {
            /* Name encoding is invalid */
            PRINT_ERROR("Unicode group name invalid");
            res = -1;
         }
      }
      else  { flags |= NNTP_GROUP_FLAG_ASCII; }
   }

   /* Send GROUP command */
   if(!res)  { res = exec_cmd(handle, NNTP_CMD_GROUP, response, name); }

   /* Check response */
   if(!res)
   {
      if(2U != response->code1)
      {
         PRINT_ERROR("Group not available");
         res = -1;
      }
      else if(NULL != gd)
      {
         /* Allocate memory and initialize group descriptor */
         *gd = nntp_group_descriptor_constructor(name);
         if (NULL == *gd)  { res = -1; }
         else
         {
            /* Copy flags to descriptor */
            (*gd)->flags = flags;
            /* Parse article count and watermarks */
            s = &response->msg[4];
            while(s[i] && !abort)
            {
               c = (int) s[i];
               if(!c)  { break; }
               switch(field)
               {
                  case 0:  /* Potential additional spaces */
                  {
                     if(0x20 == c)  { break; }
                     ++field;
                     /* No break here is intended */
                  }
                  /* FALLTHROUGH */
                  case 1:  /* Number of articles */
                  {
                     rv = parse_number(c, wm, &wm_len, &n);
                     if(-1 == rv)  { field = -1;  break; }
                     if(!rv)
                     {
                        (*gd)->eac = n;
                        wm_len = 0;
                        ++field;
                     }
                     break;
                  }
                  case 2:  /* Potential additional spaces */
                  {
                     if(0x20 == c)  { break; }
                     ++field;
                     /* No break here is intended */
                  }
                  /* FALLTHROUGH */
                  case 3:  /* Water marks */
                  {
                     rv = parse_number(c, wm, &wm_len, &n);
                     if(-1 == rv)  { field = -1; break; }
                     if(!rv)
                     {
                        if(!wm_flag)
                        {
                           (*gd)->lwm = n;
                           wm_flag = 1;
                           wm_len = 0;
                           field = 2;
                        }
                        else
                        {
                           (*gd)->hwm = n;
                           if((*gd)->lwm > (*gd)->hwm)
                           {
                              if((*gd)->eac)
                              {
                                 PRINT_ERROR("Invalid watermarks");
                                 (*gd)->eac = 0;
                              }
                           }
                           /* Finished */
                           abort = 1;
                        }
                     }
                     break;
                  }
                  default:
                  {
                     /* Error */
                     res = -1;
                     abort = 1;
                     break;
                  }
               }
               ++i;
            }
         }
      }
   }

   /* Release memory for response */
   destroy_response(&response);

   /* Before returning error: Release memory for group descriptor */
   if(res && NULL != gd)  { api_posix_free(*gd);  *gd = NULL; }

   return(res);
}


/* ========================================================================== */
/*! \brief Get overview capability of NNTP server
 *
 * \param[in]   handle  Server descriptor
 *
 * This function will trigger no communication with the server. It simply
 * reports the currently buffered state for the server associated with
 * \e handle .
 *
 * \return
 * - \c OVER capability flag (nonzero if this capability is available)
 */

int  nntp_get_capa_over(int  handle)
{
   int  res = 0;

   if(-1 != handle)
   {
      if(server[handle].capabilities & NNTP_CAPA_OVER)  { res = 1; }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Get index of Newsgroups header field in overview
 *
 * \param[in]   handle  Server descriptor
 * \param[out]  index   Pointer to index
 *
 * This function will trigger no communication with the server. It simply
 * reports the currently buffered state for the server associated with
 * \e handle .
 *
 * The Newsgroups header field is optional for the overview. The index zero
 * is returned, if the overview does not contain the data.
 *
 * \return
 * - 0 on success (\e index is valid)
 * - -1 on error
 */

int  nntp_get_over_newsgroups_index(int  handle, size_t*  index)
{
   size_t  res = -1;

   if(-1 != handle)
   {
      *index = server[handle].over_newsgroups;
      res = 0;
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Get overview for article range
 *
 * \param[in]  handle  Server descriptor
 * \param[in]  first   First article identifier of range
 * \param[in]  last    Last article identifier of range
 * \param[out] data    Pointer to overview data buffer pointer
 * \param[out] len     Pointer to article buffer size
 *
 * On success, a pointer to the overview data buffer is written to \e data
 * and the caller is responsible to free the associated memory. The size of this
 * buffer is written to \e len .
 *
 * \return
 * - 0 on success
 * - -1 on error
 * - -2 if connection to server is broken
 * - -3 if authentication requested by server failed
 */

int  nntp_get_overview(int  handle, nntp_anum_t  first, nntp_anum_t  last,
                       char**  data, size_t*  len)
{
   int  res;
   struct nntp_response*  response = NULL;

   /* Allocate memory for response */
   res = create_response(&response);

   /* Send OVER command */
   if(!res)  { res = exec_cmd(handle, NNTP_CMD_OVER, response, &first, &last); }
   if(!res)
   {
      if(2U != response->code1)  { res = -1; }
      else
      {
         /* Command wass successful */
         *len = response->bufsize;
         *data = response->content;
         /* Preserve content buffer */
         response->content = NULL;
      }
   }

   /* Release memory for response */
   destroy_response(&response);

   return(res);
}


/* ========================================================================== */
/*! \brief Get complete article via Message-ID
 *
 * \param[in]  handle   Server descriptor
 * \param[in]  mid      Pointer to article Message-ID
 * \param[out] article  Pointer to article buffer pointer
 * \param[out] len      Pointer to article buffer size
 *
 * On success, a pointer to the article buffer is written to \e article
 * and the caller is responsible to free the associated memory. The size of this
 * buffer is written to \e len .
 *
 * \return
 * - 0 on success
 * - -1 on error
 * - -2 if connection to server is broken
 * - -3 if authentication requested by server failed
 */

int  nntp_get_article_by_mid(int  handle, const char*  mid,
                             char**  article, size_t*  len)
{
   int  res;
   struct nntp_response*  response = NULL;

   /* Allocate memory for response */
   res = create_response(&response);

   /* Send ARTICLE command */
   if(!res)  { res = exec_cmd(handle, NNTP_CMD_ARTICLE_BY_MID, response, mid); }
   if(!res)
   {
      if(2U != response->code1)  { res = -1; }
      else
      {
         /* Command wass successful */
         *len = response->bufsize;
         *article = response->content;
         /* Preserve content buffer */
         response->content = NULL;
      }
   }

   /* Release memory for response */
   destroy_response(&response);

   return(res);
}


/* ========================================================================== */
/*! \brief Get complete article
 *
 * \param[in]  handle   Server descriptor
 * \param[in]  id       Pointer to article identifier
 * \param[out] article  Pointer to article buffer pointer
 * \param[out] len      Pointer to article buffer size
 *
 * On success, a pointer to the article buffer is written to \e article
 * and the caller is responsible to free the associated memory. The size of this
 * buffer is written to \e len .
 *
 * \return
 * - 0 on success
 * - -1 on error
 * - -2 if connection to server is broken
 * - -3 if authentication requested by server failed
 */

int  nntp_get_article(int  handle, const nntp_anum_t*  id, char**  article,
                      size_t*  len)
{
   int  res;
   struct nntp_response*  response = NULL;

   /* Allocate memory for response */
   res = create_response(&response);

   /* Send ARTICLE command */
   if(!res)  { res = exec_cmd(handle, NNTP_CMD_ARTICLE, response, id); }
   if(!res)
   {
      if(2U != response->code1)  { res = -1; }
      else
      {
         /* Command wass successful */
         *len = response->bufsize;
         *article = response->content;
         /* Preserve content buffer */
         response->content = NULL;
      }
   }

   /* Release memory for response */
   destroy_response(&response);

   return(res);
}


/* ========================================================================== */
/*! \brief Get article header
 *
 * \param[in]  handle  Server descriptor
 * \param[in]  id      Pointer to article identifier
 * \param[out] header  Pointer to article header buffer pointer
 * \param[out] len     Pointer to article header buffer size
 *
 * On success, a pointer to the article header buffer is written to \e header
 * and the caller is responsible to free the associated memory. The size of this
 * buffer is written to \e len .
 *
 * \return
 * - 0 on success
 * - -1 on error
 * - -2 if connection to server is broken
 * - -3 if authentication requested by server failed
 */

int  nntp_get_article_header(int  handle, const nntp_anum_t*  id,
                             char**  header, size_t*  len)
{
   int  res;
   struct nntp_response*  response = NULL;

   /* Allocate memory for response */
   res = create_response(&response);

   /* Send HEAD command */
   if(!res)  { res = exec_cmd(handle, NNTP_CMD_HEAD, response, id); }
   if(!res)
   {
      if(2U != response->code1)  { res = -1; }
      else
      {
         /* Command wass successful */
         *len = response->bufsize;
         *header = response->content;
         /* Preserve content buffer */
         response->content = NULL;
      }
   }

   /* Release memory for response */
   destroy_response(&response);

   return(res);
}


/* ========================================================================== */
/*! \brief Get article body
 *
 * \param[in]  handle  Server descriptor
 * \param[in]  id      Pointer to article identifier
 * \param[out] body    Pointer to article body buffer pointer
 * \param[out] len     Pointer to article body buffer size
 *
 * On success, a pointer to the article body buffer is written to \e body
 * and the caller is responsible to free the associated memory. The size of this
 * buffer is written to \e len .
 *
 * \return
 * - 0 on success
 * - -1 on error
 * - -2 if connection to server is broken
 * - -3 if authentication requested by server failed
 */

int  nntp_get_article_body(int  handle, const nntp_anum_t*  id, char**  body,
                           size_t*  len)
{
   int  res;
   struct nntp_response*  response = NULL;

   /* Allocate memory for response */
   res = create_response(&response);

   /* Send BODY command */
   if(!res)  { res = exec_cmd(handle, NNTP_CMD_BODY, response, id); }
   if(!res)
   {
      if(2U != response->code1)  { res = -1; }
      else
      {
         /* Command wass successful */
         *len = response->bufsize;
         *body = response->content;
         /* Preserve content buffer */
         response->content = NULL;
      }
   }

   /* Release memory for response */
   destroy_response(&response);

   return(res);
}


/* ========================================================================== */
/*! \brief Post article
 *
 * \param[in] handle   Server descriptor
 * \param[in] article  Pointer to article buffer
 *
 * \e article must point to a RFC 5536 conformant article in canonical form.
 * The caller is responsible for the validity check of the article format.
 *
 * \return
 * - 0 on success
 * - -1 on error
 * - -2 if connection to server is broken
 * - -3 if authentication requested by server failed
 */

int  nntp_post_article(int  handle, const char*  article)
{
   int  res;
   struct nntp_response*  response = NULL;

   /* Allocate memory for response */
   res = create_response(&response);

   /* Send POST command */
   if(!res)  { res = exec_cmd(handle, NNTP_CMD_POST, response, article); }
   if(!res)
   {
      if(2U != response->code1)  { res = -1; }
   }

   /* Release memory for response */
   destroy_response(&response);

   return(res);
}


/*! @} */

/* EOF */
