/* ========================================================================== */
/*! \file
 * \brief Newsreader core (for handling RFC 5536 conformant messages)
 *
 * Copyright (c) 2012-2024 by the developers. See the LICENSE file for details.
 *
 * All newsreader functions (but nothing generic like file handling) that are
 * not related to the transport or the user interface (UI) should be implemented
 * here.
 *
 * 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 <stdlib.h>
#include <string.h>
#include <time.h>

#include "conf.h"
#include "config.h"
#include "core.h"
#include "database.h"
#include "digest.h"
#include "encoding.h"
#include "extutils.h"
#include "fileutils.h"
#include "filter.h"
#include "group.h"
#include "hmac.h"
#include "log.h"
#include "main.h"
#include "secure.h"
#include "timing.h"
#include "ui.h"
#include "xdg.h"


/* ========================================================================== */
/*! \defgroup CORE CORE: Newsreader base functionality
 *
 * The core use a separate thread for the transport subsystem calls, otherwise
 * it would block the UI while waiting for data.
 *
 * A nexus binds the core to a transport subsystem (like NNTP or UUCP).
 *
 * Current limitations:
 * - Only one server nexus is supported at a time
 * - Only NNTP transport is supported
 * - There is no command queue
 * - No Unicode support at protocol level (see below)
 *
 * At least the following things must be (re)implemented for Unicode support at
 * protocol level:
 * - The NNTP transport driver already supports Unicode group names.
 *   This support currently only implements encoding checks. There is no
 *   normalization or equality matching.
 * - The article header cache database currently is based on the POSIX portable
 *   filename character set. For Unicode group names the database must be
 *   modified or replaced.
 * - Unicode allows different legal encodings and/or composition variants for
 *   the same string. This makes all protocol entities that are encoded this way
 *   ambiguous by definition. This is not acceptable because we internally use
 *   protocol entities like group names as identifiers. This means that any
 *   incoming identifier must be normalized before internal use to be non-
 *   ambiguous. Read this document for details about normalization:
 *   http://www.unicode.org/reports/tr15/ .
 *   Normalization to NFC can be provided by the ENCODING module.
 * - For outgoing names there is no normalization defined in NNTP yet.
 *   Most likely this will become the incoming format from that server in the
 *   future. This means we need to store two representations of names (one
 *   internal form and an external form that is server dependent).
 * - The '~/.newsrc' file is ASCII encoded. This can't be changed without
 *   breaking compatibility with other newsreaders. A separate database for
 *   Unicode group states must be implemented while the ASCII group states
 *   should stay in the old place for backward compatibility.
 * - RFC 5536 conformant article headers are not allowed to contain Unicode,
 *   MIME encoded words are not allowed in the "Newsgroups" header field too
 *   (see chapter 3.1.4). We need a new parser that is conformant to a successor
 *   of RFC 5536.
 * - The handling for LIST DISTRIB.PATS in the CORE module must be rewritten
 *   to check for a UTF-8 based locale and convert the data otherwise (for the
 *   wildmat matching via POSIX extended regular expressions).
 *
 * \attention
 * It is required that 'PID_MAX' is not larger than 'LONG_MAX' (must be checked
 * by build system).
 */
/*! @{ */


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

enum core_nexus_state
{
   CORE_NEXUS_CLOSED,
   CORE_NEXUS_ESTABLISHED
};

enum core_command
{
   CORE_CMD_INVALID,
   CORE_CMD_GET_MOTD,
   CORE_CMD_GET_DISTRIB_PATS,
   CORE_CMD_GET_SUBSCRIPTIONS,
   CORE_CMD_RESET_GROUPSTATES,
   CORE_CMD_GET_GROUPLIST,
   CORE_CMD_GET_GROUPLABELS,
   CORE_CMD_GET_GROUPINFO,
   CORE_CMD_SET_GROUP,
   CORE_CMD_GET_OVERVIEW,
   CORE_CMD_GET_ARTICLE_BY_MID,
   CORE_CMD_GET_ARTICLE,
   CORE_CMD_GET_ARTICLE_HEADER,
   CORE_CMD_GET_ARTICLE_BODY,
   CORE_CMD_POST_ARTICLE,
   CORE_TERMINATE_NEXUS
};

enum core_header_type
{
   CORE_HT_UNSTRUCT,
   CORE_HT_STRUCT
};

enum core_header_id
{
   /* End of header marker */
   CORE_HDR_EOH,
   /* IDs for mandatory header fields according to RFC 5536 */
   CORE_HDR_MSGID,                 /* "Message-ID" */
   CORE_HDR_GROUPS,                /* "Newsgroups" */
   CORE_HDR_FROM,                  /* "From" */
   CORE_HDR_SUBJECT,               /* "Subject" */
   CORE_HDR_DATE,                  /* "Date" */
   /* Alyways keep optional headers behind this entry */
   CORE_HDR_OPT,
   /* IDs for optional header fields according to RFC 5536 */
   CORE_HDR_SUPERS,                /* "Supersedes" */
   CORE_HDR_FUP2,                  /* "Followup-To" */
   CORE_HDR_REPLY2,                /* "Reply-To" */
   CORE_HDR_UAGENT,                /* "User-Agent" */
   CORE_HDR_ORG,                   /* "Organization" */
   CORE_HDR_REFS,                  /* "References" */
   CORE_HDR_DIST,                  /* "Distribution" */
   /* IDs for optional header fields according to RFC 2047 */
   CORE_HDR_MIME,                  /* "MIME-Version" */
   CORE_HDR_CT,                    /* "Content-Type" */
   CORE_HDR_CTE,                   /* "Content-Transfer-Encoding" */
   CORE_HDR_CD,                    /* "Content-Disposition" */
   /* Nonstandard header fields */
   CORE_HDR_X_NEWSR,               /* "X-Newsreader" */
   CORE_HDR_X_MAILER,              /* "X-Mailer" */
   CORE_HDR_X_PAGENT,              /* "X-Posting-Agent" */
   /* Obsolete header fields */
   CORE_HDR_LINES                  /* "Lines" */
};

struct core_headerfield
{
   enum core_header_id  id;
   enum core_header_type  type;
   const char*  content;
};

struct core_nexus
{
   enum  core_nexus_state  nntp_state;
   const char*  nntp_server;
   int  nntp_handle;
   const char*  nntp_current_group;
};

struct distrib_pats
{
   const char*  wildmat;
   size_t  weight;
   const char*  dist;
};


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

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

/*! \brief Number of retries for nexus operations
 *
 * Should be at least 1, otherwise the core module can't automatically recover
 * after a nexus loss (e.g. disconnect from server).
 */
#define CORE_NEXUS_RETRIES  1U

/*! \brief Sufficient for any RFC 5536 conformant header line
 *
 * The real limit defined in RFC 5536 is 998 characters.
 */
#define CORE_HEADER_LINE_LENGTH  (size_t) 1024

/*! \name Control flags for header parser (for internal use only)
 *
 * The flags can be bitwise ORed together.
 */
/*! @{ */
#define CORE_CFLAG_COMMENT  0x01U
#define CORE_CFLAG_QSTRING  0x02U
#define CORE_CFLAG_EWORD    0x04U
/*! @} */

/*! File containing secret for SHA2-based Cancel-Locks/-Keys
 *
 * \note The minimum value for NAME_MAX defined by POSIX is 14 characters.
 */
#define CORE_CL_SECRET_FILE  ".cancelsecret"

/*! \brief Do not parse for content in "User-Agent" header field
 *
 * Set this to nonzero if you want to see the comments in the GUI.
 *
 * \attention
 * The contents of comments can contain quoted-pair and encoded-word tokens.
 * The header parser will not decode them and the data may not be readable
 * for humans (e.g. Base64 encoding in encoded-word). Therefore this option
 * is disabled by default.
 */
#define CORE_UAGENT_RAW  0


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

/*! \brief Global data object (shared by all threads) */
struct core_data  data;

static posix_pthread_t  pt;
static int  pt_valid = 0;
static posix_pthread_t  ui_pt;
static posix_pthread_mutex_t  pt_mutex = POSIX_PTHREAD_MUTEX_INITIALIZER;
static posix_pthread_cond_t  pt_cond = POSIX_PTHREAD_COND_INITIALIZER;
static struct core_hierarchy_element*  h = NULL;
static struct core_nexus*  n = NULL;
static enum core_command  command = CORE_CMD_INVALID;


/* ========================================================================== */
/* Check whether character is part of quoted-string and/or comment
 *
 * \param[in] s      String
 * \param[in] p      Pointer to character inside string \e s
 * \param[in] flags  Control flags
 *
 * Only the conditions marked with flags are reported.
 *
 * \return
 * - 0 if character is not part of specified tokens
 * - Positive value if \e p points inside one of specified tokens
 */

static int  check_iqscf(const char*  s, const char*  p, unsigned int  flags)
{
   int  res = 0;
   size_t  i = 0;
   int  escape = 0;
   int  lwbs = 0;  /* Last character was backslash flag */
   int  lweq = 0;  /* Last character was equal sign flag*/
   int  lwqm = 0;  /* Last character was question mark flag*/
   int  iew = 0;  /* Inside encoded-word flag */
   int  iqs = 0;  /* Inside quoted-string flag */
   unsigned int  cmt = 0;  /* Comment nesting depth */

   while(s[i])
   {
      /* Check for start of encoded-word */
      if(!iew && lweq && '?' == s[i])  { iew = 1; }
      /* Check for quoted-pair */
      if(iqs || cmt)
      {
         if(lwbs)  { escape = 1; }  else  { escape = 0; }
      }
      if(0x5C == s[i] && !escape)  { lwbs = 1; }  else  { lwbs = 0; }
      /* Check for start of comment (can be nested) */
      if(!iew && !iqs && '(' == s[i])
      {
         if(!escape)
         {
            if(POSIX_UINT_MAX == cmt)
            {
               /* Comment nesting depth overflow */
               PRINT_ERROR("Header parser: Too many nested comments");
            }
            else  { ++cmt; }
         }
      }
      /* Check for start of quoted string */
      if(!iew && !cmt && '"' == s[i])
      {
         if(!escape)
         {
            if(!iqs)  { iqs = 1; }  else  { iqs = 2; }
         }
      }
      /* Check for match */
      if(p == &s[i])
      {
         if(CORE_CFLAG_COMMENT & flags && cmt)
         {
            res |= (int) CORE_CFLAG_COMMENT;
         }
         if(CORE_CFLAG_QSTRING & flags && iqs)
         {
            res |= (int) CORE_CFLAG_QSTRING;
         }
         if(CORE_CFLAG_EWORD & flags && iew)
         {
            res |= (int) CORE_CFLAG_EWORD;
         }
         break;
      }
#if 0
      /* For debugging */
      if(!i)
      {
         printf("I");
         if(CORE_CFLAG_EWORD & flags)  { printf("W"); }
         else  { printf(" "); }
         if(CORE_CFLAG_QSTRING & flags)  { printf("S"); }
         else  { printf(" "); }
         if(CORE_CFLAG_COMMENT & flags)  { printf("C"); }
         else  { printf(" "); }
         printf(": %s\n", s);
         printf("      ");
      }
#  if 0
      /* Mark quoted-pair escaping */
      if(escape)  { printf("E"); }  else
#  endif
      if(iqs)  { printf("S"); }
      else if(cmt)
      {
         if(9 >= cmt)  { printf("%u", cmt); }  else  { printf("0"); }
      }
      else  { printf(" "); }
#endif
      /* Check for end of comment */
      if(!iew && !iqs && ')' == s[i])
      {
         if(!escape)
         {
            if(cmt)  { --cmt; }
            else
            {
               /* Opening parenthesis missing */
               PRINT_ERROR("Header parser: Syntax error in comment");
            }
         }
      }
      /* Check for end of quoted-string */
      if(!iew && !cmt && '"' == s[i])
      {
         if(!escape && 1 < iqs)  { iqs = !iqs; }
      }
      /* Check for end of encoded-word */
      if(iew && lwqm && '=' == s[i])  { iew = 0; }
      if('=' == s[i])  { lweq = 1; }  else  { lweq = 0; }
      if('?' == s[i])  { lwqm = 1; }  else  { lwqm = 0; }
      /* Next character */
      ++i;
   }
#if 0
   /* For debugging */
   printf("^ (res: %d)\n", res);
#endif

   return(res);
}


/* ========================================================================== */
/* Check whether character is part of comment
 *
 * \param[in] s      String
 * \param[in] p      Pointer to character inside string \e s
 *
 * \return
 * - 0 if character pointed to by \e p is not part of comment
 * - Negative value if \e p points inside comment or is not found
 */

static int  check_ic(const char*  s, const char*  p)
{
   return(check_iqscf(s, p, CORE_CFLAG_COMMENT));
}


/* ========================================================================== */
/* Check whether character is part of quoted-string or comment
 *
 * \param[in] s      String
 * \param[in] p      Pointer to character inside string \e s
 *
 * \return
 * - 0 if character pointed to by \e p is syntactically used
 * - Negative value if \e p points inside quoted-string, comment or is not found
 */

static int  check_iqsc(const char*  s, const char*  p)
{
   return(check_iqscf(s, p, CORE_CFLAG_QSTRING | CORE_CFLAG_COMMENT));
}


/* ========================================================================== */
/* Convert "From" header field from RFC 850 to RFC 5536 format
 *
 * According to RFC 850 the following rule is applied:
 * - A name in parenthesis that follows the address is not a comment. This is no
 *   longer allowed by RFC 5536 and today defined as regular comment
 *   => We accept the RFC 850 format for backward compatibility and convert it
 *   to RFC 5536 format so that the header parser can process it.
 *
 * This function converts:
 *    foo@bar.com (Full name)
 * to:
 *    Full name <foo@bar.com>
 *
 * \param[in] from  Unfolded body of header field "From"
 *
 * \note
 * An SP between address and comment is required in the input data.
 *
 * \attention
 * Only the first mailbox of a list is converted, the others are stripped.
 *
 * \attention
 * On success, the old memory block pointed to by \e from is 'free()'d by this
 * function!
 *
 * This function never fail.
 *
 * \return
 * - \e from or converted string
 */

static char*  convert_from_rfc850_to_rfc5536(char*  from)
{
   char*  res = from;
   size_t  len = strlen(from);
   char*  name;
   char*  cp;
   char*  comma = NULL;
   char*  addr;
   size_t  i;
   char*  p;
   char*  tmp = NULL;
   int  invalid;

   /* Check whether single comment is present and no angle-addr */
   name = strchr(from, (int) '(');
   cp = strchr(from, (int) ')');
   if(cp)  { comma = strchr(&cp[1], (int) ','); }
   if(NULL != name && cp > name)
   {
      /* Ignore comments or angle-addr of other mailboxes */
      if( (!comma && NULL == strchr(&name[1], (int) '(')
           && NULL == strchr(from, (int) '<'))
          ||
          (comma && (NULL == strchr(&name[1], (int) '(')
                     || strchr(&name[1], (int) '(') > comma)
                    && (NULL == strchr(from, (int) '<')
                     || strchr(from, (int) '<') > comma)) )
      {
         /* Allocate temporary buffer (1 additional byte for NUL termination) */
         tmp = (char*) posix_malloc(len * (size_t) 2 + (size_t) 1);
         if(NULL != tmp)
         {
            i = (size_t) (name - from);
            if(i)
            {
               strncpy(tmp, from, len + (size_t) 1);  tmp[len] = 0;
               if(comma)  { tmp[(size_t) (comma - from)] = 0; }
               /* Extract address */
               if(' ' == tmp[--i])
               {
                  tmp[i] = 0;
                  addr = tmp;
                  /* Check address */
                  if(NULL != strchr(addr, (int) '@'))
                  {
                     /* Looks good => Extract name */
                     i += (size_t) 2;
                     name = &tmp[i];
                     p = strchr(name, (int) ')');
                     if(NULL != p)
                     {
                        p[0] = 0;
                        /* Check name */
                        invalid = enc_ascii_check_printable(name);
                        if(invalid)
                        {
                           PRINT_ERROR("Header parser: Control characters"
                                       " in RFC 850 full name not supported");
                        }
                        else
                        {
                           if(NULL != strpbrk(name, "()<>,:;"))
                           {
                              PRINT_ERROR("Header parser: Invalid RFC 850"
                                          " full name in parenthesis");
                              invalid = 1;
                           }
                           if(!invalid)
                           {
                              /*
                               * Create quoted-pair encoding for '"'characters
                               * that can't be represented literally in a
                               * quoted-string (RFC 850 allow '"' characters in
                               * full name)
                               * Note: We have always allocated enough memory
                               * for the temporary buffer after name!
                               */
                              if(NULL != strchr(name, (int) '"'))
                              {
                                 i = 0;
                                 while(name[i])
                                 {
                                    if('"' == name[i])
                                    {
                                       /* Verify whether already quoted-pair */
                                       if( !(i && 0x5C
                                           == (int) name[i - (size_t) 1]) )
                                       {
                                          len = strlen(&name[i]);
                                          memmove((void*) (&name[i] + 1),
                                                  (void*) &name[i], ++len);
                                          name[i++] = 0x5C;
                                       }
                                    }
                                    ++i;
                                 }
                              }
                              /* Check for single trailing backslash */
                              p = strrchr(name, 0x5C);
                              if(NULL != p && !p[1])
                              {
                                 if( (p != name && 0x5C != (int) *(p - 1))
                                     || p == name )
                                 {
                                    PRINT_ERROR("Header parser: Invalid"
                                                " quoted-pair in RFC 850"
                                                " full name accepted/ignored");
                                    *p = 0;
                                 }
                              }
                              /* Success => Allocate new memory block */
                              len = 1;  /* NUL termination */
                              len += strlen(name);
                              len += strlen(addr);
                              len += (size_t) 2;  /* Double quotes */
                              len += (size_t) 1;  /* SP separator */
                              len += (size_t) 2;  /* Angle brackets */
                              res = (char*) posix_malloc(len);
                              if (NULL == res)  { res = from; }
                              else
                              {
                                 if(!name[0])
                                 {
                                    /* Omit the separator if name is empty */
                                    posix_snprintf(res, len, "<%s>", addr);
                                 }
                                 else
                                 {
                                    /* Represent the name as quoted string */
                                    posix_snprintf(res, len, "\"%s\" <%s>",
                                                   name, addr);
                                 }
                                 if(comma)
                                 {
                                    PRINT_ERROR("Header parser: Only"
                                                " first mailbox of mailbox-list"
                                                " processed");
                                 }
#if 0
                                 /* For debugging */
                                 printf("RFC 850 : %s\n", from);
                                 printf("RFC 5536: %s\n", res);
#endif
                                 posix_free((void*) from);
                              }
                           }
                        }
                     }
                  }
               }
            }
         }
      }
   }
   posix_free((void*) tmp);

   return(res);
}


/* ========================================================================== */
/* Article header parser */
/*
 * Parses the header 'h' and create an array 'e' that contains structures with
 * the ID, the type and the extracted bodies of header fields.
 * For structured header fields, comments are removed and runs of white space
 * are replaced by a single space.
 * If the header contains a field multiple times, all of them will be added to
 * the result array with the same ID.
 *
 * Note: What we call a "header field" is called a "header line" by RFC 3977.
 *
 * On success, the caller is responsible to free the memory allocated for the
 * array 'e' and all of the body content where its elements point to.
 */
/*! \todo Better article header parser.
 * The quick&dirty top down header parser try to behave RFC 5536 conformant.
 * Because the syntax is very complicated, some special cases that are seldom
 * used in real world are not handled correctly.
 * To improve this, we should consider to use a state machine generated by yacc
 * for the official grammar as parser or at least a lexical analyzer generated
 * by lex for the official tokens to split them correctly.
 */
/*
 * According to RFC 5322, the header fields are accepted in arbitrary order.
 *
 * According to RFC 822 the following rules are applied:
 * - All header field names must be treated case-insensitive => We do so.
 *
 * According to RFC 5536 the following rules are applied:
 * - At least 1 space must follow the colon at the end of the header field name
 *   It is allowed to accept header fields without spaces => We do so.
 * - Header fields with empty body are not allowed
 *   => We ignore them.
 * - Header fields longer than 998 characters must be folded
 *   It is allowed to accept them unfolded anyhow
 *   => We do so up to CORE_HEADER_LINE_LENGTH.
 * - Header fields must contain only printable ASCII characters
 *   This is mandatory => All header fields that use anything else are ignored.
 * - Some header fields are not allowed to contain comments:
 *   Control, Distribution, Followup-To, Lines, Newsgroups, Path, Supersedes,
 *   Xref, Message-ID, Lines
 *   => If there are comments in such a header field we use, the header field is
 *   accepted and the comments stay in place (are treated as part of the body).
 * - The header field "Lines" is marked as obsolete and it is recommended to
 *   ignore it
 *   => We use it nevertheless because otherwise it is not possible to process
 *   the number of lines information from the overview data
 */

static int  header_parser(struct core_headerfield**  e, const char*  h)
{
   const char*  hfields[] =
   {
      "MESSAGE-ID", "NEWSGROUPS", "FROM",
      "SUBJECT", "DATE", "SUPERSEDES",
      "FOLLOWUP-TO", "REPLY-TO", "USER-AGENT",
      "ORGANIZATION", "REFERENCES", "DISTRIBUTION",
      "MIME-VERSION", "CONTENT-TYPE", "CONTENT-TRANSFER-ENCODING",
      "CONTENT-DISPOSITION",
      "X-NEWSREADER", "X-MAILER", "X-POSTING-AGENT",
      "LINES", NULL
   };
   /* The order of IDs must match the order of the strings above */
   enum core_header_id  hfieldids[] =
   {
      CORE_HDR_MSGID, CORE_HDR_GROUPS, CORE_HDR_FROM,
      CORE_HDR_SUBJECT, CORE_HDR_DATE, CORE_HDR_SUPERS,
      CORE_HDR_FUP2, CORE_HDR_REPLY2, CORE_HDR_UAGENT,
      CORE_HDR_ORG,  CORE_HDR_REFS, CORE_HDR_DIST,
      CORE_HDR_MIME, CORE_HDR_CT, CORE_HDR_CTE,
      CORE_HDR_CD,
      CORE_HDR_X_NEWSR, CORE_HDR_X_MAILER, CORE_HDR_X_PAGENT,
      CORE_HDR_LINES, CORE_HDR_EOH
   };
   enum core_header_type  hfieldtypes[] =
   {
      CORE_HT_STRUCT, CORE_HT_STRUCT, CORE_HT_STRUCT,
      CORE_HT_UNSTRUCT, CORE_HT_STRUCT, CORE_HT_STRUCT,
      CORE_HT_STRUCT, CORE_HT_STRUCT, CORE_HT_STRUCT,
      CORE_HT_UNSTRUCT, CORE_HT_STRUCT, CORE_HT_STRUCT,
      CORE_HT_STRUCT, CORE_HT_STRUCT, CORE_HT_STRUCT,
      CORE_HT_STRUCT,
      CORE_HT_STRUCT, CORE_HT_STRUCT, CORE_HT_STRUCT,
      CORE_HT_STRUCT, CORE_HT_UNSTRUCT
   };
   int  res = 0;
   size_t  asize = 16;  /* Initial size of result array */
   size_t  ai = 0;
   size_t  i = 0;
   size_t  ii;
   const char*  target;
   char*  buf = NULL;
   char*  p;
   char*  q;
   size_t  buflen = CORE_HEADER_LINE_LENGTH;
   size_t  len;
   int  used;  /* Flag indicating header field is known and used */
   int  qstring;
   int  skip;
   int  resync;
   struct core_headerfield  hf;
   char*  gstart;
   char*  gend;
   char*  comment;
   struct core_headerfield*  tmp;
   int  inside_qs;

   /* Allocate memory for result array */
   *e = (struct core_headerfield*)
        posix_malloc(asize * sizeof(struct core_headerfield));
   if (NULL == *e)  { res = -1; }

   /* Allocate memory for header line */
   if(!res)
   {
      buf = (char*) posix_malloc(buflen);
      if (NULL == buf)  { res = -1; }
   }

   /* Parser */
   while(!res && h[i])
   {
      /* Extract header field name and convert it to upper case */
      resync = 0;
      target = strchr(&h[i], (int) ':');
      if(target <= &h[i])  { resync = 1; }  /* NULL is lower than any pointer */
      else
      {
         len = (size_t) (target - &h[i]);
         if(buflen <= len)  { resync = 1; }  /* We need one additional byte */
         else
         {
            for(ii = 0; ii < len; ++ii)
            {
               buf[ii] = (char) toupper((int) h[i + ii]);
            }
            buf[len] = 0;
            i += len;
         }
      }

      /* Check whether we use this header field */
      if(!resync)
      {
         used = 0;
         ii = 0;
         while(NULL != hfields[ii])
         {
            if(!strcmp(hfields[ii], buf))  { used = 1;  break; }
            ++ii;
         }
         if(!used)
         {
            /* We don't use this header field => Ignore */
            resync = 1;
         }
         else
         {
            /* Used header field found => Store ID and type */
            hf.id = hfieldids[ii];
            hf.type = hfieldtypes[ii];
         }
      }

      /* Extract header field body if required */
      if(!resync)
      {
         /* Skip potential leading SPs */
         if(CORE_HT_STRUCT == hf.type)
         {
            /* Remove all spaces */
            while(' ' == h[++i]);
         }
         else
         {
            /* Remove only first space (even if multiple are present) */
            if(' ' == h[++i])  { ++i; };
         }
         /* Process field body */
         buf[0] = 0;
         ii = 0;
         do
         {
            /* Search for 0x0D = CR (end of body or folding point) */
            target = strchr(&h[i], 0x0D);
            if(target <= &h[i])  { resync = 1;  break; }
            else
            {
               /* Check whether buffer size must be increased */
               len = (size_t) (target - &h[i]);
               while(buflen <= ii + len)  /* We need one additional byte */
               {
                  p = (char*) posix_realloc(buf, buflen *= (size_t) 2);
                  if(NULL == p)  { res = -1;  break; }  else  { buf = p; }
               }
               if(-1 == res)  { break; }
               /* Copy next chunk of body to buffer */
               memcpy((void*) &buf[ii], (void*) &h[i], len);
               buf[ii += len] = 0;
               i += len;
            }
            /* Verify correct line termination */
            if((const char) 0x0A != target[1])
            {
               PRINT_ERROR("Header parser: Invalid CR in field body");
               resync = 1;
               break;
            }
            /* Check for folded header field */
            if((const char) 0x09 == target[2] || (const char) 0x20 == target[2])
            {
               /* Yes => Unfold next line */
               i += 2;
               if(CORE_HT_STRUCT == hf.type)
               {
                  /*
                   * According to RFC 5322, runs of FWS, comment or CFWS are
                   * semantically interpreted as single SP.
                   * Note: This rule only applies to structured header fields!
                   */
                  buf[ii++] = 0x20;
                  while((const char) 0x09 == h[i] || (const char) 0x20 == h[i])
                  {
                     ++i;
                  }
               }
            }
            else { break; }
         }
         while(!resync);
         if(res)  { break; }
      }

      /* Verify header field body, remove comments and process quoted strings */
      if(!resync)
      {
         /* Check for empty header field body */
         if(!buf[0])  { resync = 1; }
         else
         {
            if(CORE_HDR_DIST == hf.id)
            {
               /* Check and reformat distribution header field */
               enc_ascii_convert_distribution(buf);
            }
            else
            {
               /* Check whether header field body is printable ASCII */
               if(0 > enc_ascii_check_printable(buf))
               {
                  /* No => Repair */
                  PRINT_ERROR("Header parser: "
                              "Invalid characters replaced");
                  enc_ascii_convert_to_printable(buf);
               }
            }
            /* Check for structured header field */
            if(CORE_HT_STRUCT == hf.type)
            {
               /* Remove comments */
               switch(hf.id)
               {
#if CORE_UAGENT_RAW
                  case CORE_HDR_UAGENT:
                  {
                     break;
                  }
#endif  /* CORE_UAGENT_RAW */
                  case CORE_HDR_MSGID:
                  case CORE_HDR_GROUPS:
                  case CORE_HDR_FUP2:
                  case CORE_HDR_SUPERS:
                  case CORE_HDR_DIST:
                  case CORE_HDR_LINES:
                  {
                     /* No comments allowed in these header fields */
                     break;
                  }
                  case CORE_HDR_REPLY2:
                  case CORE_HDR_FROM:
                  {
                     /*
                      * RFC 850 allows full name in comment as exception!
                      * => Convert RFC 850 address to RFC 5536 format or the
                      *    will be lost otherwise.
                      */
                     buf = convert_from_rfc850_to_rfc5536(buf);
                     /* No break here is intended! */
                  }
                  default:
                  {
                     ii = 0;
                     comment = NULL;
                     while(buf[ii])
                     {
                        /* Check for start of comment */
                        if('(' == buf[ii] && NULL == comment)
                        {
                           if(check_ic(buf, &buf[ii]))  { comment = &buf[ii]; }
                        }
                        /* Check for end of comment */
                        else if(NULL != comment && ')' == buf[ii])
                        {
                           if(!check_ic(buf, &buf[ii + (size_t) 1]))
                           {
                              /* Skip comment */
                              len = strlen(&buf[++ii]);
                              memmove((void*) comment, (void*) &buf[ii], ++len);
                              ii = (size_t) (comment - buf);
                              comment = NULL;
                              continue;
                           }
                        }
                        ++ii;
                     }
                  }
               }
               /* Treat runs of WSP semantically as single space */
               ii = 0;
               while(buf[ii])
               {
                  /* Replace HT with SP */
                  if((char) 0x09 == buf[ii])  { buf[ii] = ' '; }
                  if(ii && ' ' == buf[ii])
                  {
                     if(' ' == buf[ii - (size_t) 1])
                     {
                        p = &buf[ii++];
                        if( !( (size_t) 2 <= ii
                            && 0x5C == (int) buf[ii - (size_t) 2]
                            && check_iqsc(buf, p) ) )
                        {
                           len = strlen(&buf[ii]);
                           memmove((void*) p, (void*) &buf[ii], ++len);
                           --ii;
                        }
                     }
                     else  { ++ii; }
                  }
                  else  { ++ii; }
               }
               /* Remove potential leading SP */
               if(' ' == *buf)
               {
                  len = strlen(&buf[1]);
                  memmove((void*) buf, (void*) &buf[1], ++len);
               }
               /* Special handling for "Content-Type" field */
               if(CORE_HDR_CT == hf.id)
               {
                  inside_qs = 0;
                  ii = 0;
                  while(buf[++ii])
                  {
                     /* Remove whitespace before semicolon, slash, equal sign */
                     if('"' == buf[ii])
                     {
                        /*
                         * Parameter values are allowed to contain whitespace and
                         * '=' inside quoted-string => Don't touch this whitespace.
                         */
                         if(!inside_qs)  { inside_qs = 1; }
                         else
                         {
                            /* Check for quoted-pair */
                            if((char) 0x5C != buf[ii - 1])  { inside_qs = 0; }
                         }
                     }
                     if(!inside_qs &&
                        (';' == buf[ii] || '=' == buf[ii] || '/' == buf[ii]))
                     {
                        if(' ' == buf[ii - 1])
                        {
                           len = strlen(&buf[ii]);
                           memmove((void*) &buf[ii - 1], (void*) &buf[ii],
                                   ++len);
                          --ii;
                        }
                     }
                     /* Remove whitespace after slash and equal sign */
                     if(!inside_qs && ('=' == buf[ii] || '/' == buf[ii]))
                     {
                        if(' ' == buf[ii + 1])
                        {
                           len = strlen(&buf[ii + 2]);
                           memmove((void*) &buf[ii + 1], (void*) &buf[ii + 2],
                                   ++len);
                        }
                     }
                  }
               }
               /* Special handling for "Content-Transfer-Encoding" field */
               else if(CORE_HDR_CTE == hf.id)
               {
                  /* Strip all content after first occurence of whitespace */
                  ii = 0;
                  while(buf[ii++])
                  {
                     if(' ' == buf[ii])
                     {
                        PRINT_ERROR("Header parser: Garbage at end of"
                                    " Content-Transfer-Encoding field ignored");
                        buf[ii] = 0;
                     }
                  }
               }
               /* Special handling for "From" and "Reply-To" fields */
               else if(CORE_HDR_FROM == hf.id || CORE_HDR_REPLY2 == hf.id)
               {
                  /* address-list is not supported, extract first group */
                  if(CORE_HDR_REPLY2 == hf.id)
                  {
                     q = buf;
                     do  { gstart = strchr(q, (int) ':');  q = gstart + 1; }
                     while(NULL != gstart && check_iqsc(buf, gstart));
                     if(NULL != gstart)
                     {
                        q = gstart;
                        do  { gend = strchr(q, (int) ';');  q = gend + 1; }
                        while(NULL != gend && check_iqsc(buf, gend));
                        if(gstart < gend)
                        {
                           /* Check whether group is first address */
                           q = buf;
                           do  { p = strchr(q, (int) ',');  q = p + 1; }
                           while(NULL != p && check_iqsc(buf, p));
                           if(NULL == p || p > gstart)
                           {
                              /* Yes => Extract group-list */
                              PRINT_ERROR("Header parser: Only first"
                                          " address of address-list processed");
                              *gend = 0;
                              while(++gstart < gend)
                              {
                                 /* Skip leading whitespace */
                                 if(' ' != *gstart && 0x09 != (int) *gstart)
                                 {
                                    break;
                                 }
                              }
                              len = (size_t) (gend - gstart);
                              memmove((void*) buf, (void*) gstart, ++len);
                           }
                        }
                     }
                  }
                  /* mailbox-list is not supported, extract first mailbox */
                  q = buf;
                  do  { p = strchr(q, (int) ',');  q = p + 1; }
                  while(NULL != p && check_iqsc(buf, p));
                  if(NULL != p)
                  {
                     PRINT_ERROR("Header parser: Only first mailbox of"
                                 " mailbox-list processed");
                     *p = 0;
                  }
                  /* Remove whitespace from addr-spec */
                  p = strrchr(buf, (int) '<');  if(NULL == p)  { p = buf; }
                  ii = 0;
                  while(buf[ii])
                  {
                     if(&buf[ii] < p)  { ++ii;  continue; }
                     if(ii && ' ' == buf[ii])
                     {
                        /* Check for "after < or @" and "before > or @" */
                        if( '<' == buf[ii - (size_t) 1]
                            || '@' == buf[ii - (size_t) 1]
                            || '>' == buf[ii + (size_t) 1]
                            || '@' == buf[ii + (size_t) 1] )
                        {
                           p = &buf[ii];
                           len = strlen(&buf[++ii]);
                           memmove((void*) p, (void*) &buf[ii], ++len);
                        }
                        else  { ++ii; }
                     }
                     else  { ++ii; }
                  }
               }
#if !CORE_UAGENT_RAW
               /* Special handling for "User-Agent" field */
               else if(CORE_HDR_UAGENT == hf.id)
               {
                  /* Remove whitespace between product and product-version */
                  ii = 0;
                  while(buf[ii])
                  {
                     if(ii && ' ' == buf[ii])
                     {
                        /* Check for "after or before /" */
                        if( '/' == buf[ii - (size_t) 1]
                           || '/' == buf[ii + (size_t) 1] )
                        {
                           p = &buf[ii];
                           len = strlen(&buf[++ii]);
                           memmove((void*) p, (void*) &buf[ii], ++len);
                        }
                        else  { ++ii; }
                     }
                     else  { ++ii; }
                  }
               }
#endif  /* CORE_UAGENT_RAW */
#if CORE_UAGENT_RAW
               if(CORE_HDR_UAGENT != hf.id)
               {
#endif  /* CORE_UAGENT_RAW */
                  /* Process quoted strings (potential quoted pairs inside) */
                  ii = 0;
                  qstring = 0;
                  skip = 0;
                  while(buf[ii])
                  {
                     if('"' == buf[ii])  { qstring = !qstring;  skip = 1; }
                     if(qstring && (char) 0x5C == buf[ii])  { skip = 2; }
                     if(skip)
                     {
                        p = &buf[ii];
                        len = strlen(&buf[++ii]);
                        memmove((void*) p, (void*) &buf[ii], ++len);
                        /* Check current position again if not quoted pair */
                        if(1 == skip)  { --ii; }
                        skip = 0;
                     }
                     else  { ++ii; }
                  }
#if CORE_UAGENT_RAW
               }
#endif  /* CORE_UAGENT_RAW */
            }
            /* Processing of header field completed => Store body */
            hf.content = buf;
         }
      }

      /* Add header field to result array */
      if(!resync)
      {
         /* Allocate more memory for result buffer if required */
         if(ai >= asize - (size_t) 2)
         {
            tmp = (struct core_headerfield*)
                  posix_realloc(*e, (asize *= (size_t) 2)
                                    * sizeof(struct core_headerfield));
            if (NULL == tmp)  { res = -1;  break; }  else  { *e = tmp; }
         }
         (*e)[ai].id = hf.id;
         (*e)[ai++].content = hf.content;
         /* Allocate new buffer for next header field */
         buflen = CORE_HEADER_LINE_LENGTH;
         buf = (char*) posix_malloc(buflen);
         if (NULL == buf)  { res = -1;  break; }
      }

      /* Resync to start of next header field */
      do
      {
         /* Search for 0x0D = CR (end of body or folding point) */
         target = strchr(&h[i], 0x0D);
         if(NULL == target)
         {
            PRINT_ERROR("Header parser: Body separator missing");
            res = -1;
            break;
         }
         else  { i += (size_t) (++target - &h[i]);}
         /* Search for 0x0A = LF */
         if((const char) 0x0A == h[i]) { ++i; }
         /* Check for end of header */
         if((const char) 0x0D == h[i])
         {
            if((const char) 0x0A == h[i + 1])
            {
               /* End of header detected */
               /* printf("EOH detected\n"); */
               res = 1;
               break;
            }
         }
      }
      while(h[i] && (' ' == h[i] || (const char) 0x09 == h[i]));
   }
   /* Terminate result array */
   if(*e)  { (*e)[ai].id = CORE_HDR_EOH; }

   /* Clean up */
   posix_free((void*) buf);
   if(0 > res)
   {
      if(*e)
      {
         ai = 0;
         while(CORE_HDR_EOH != (*e)[ai].id)
         {
            posix_free((void*) (*e)[ai++].content);
         }
         posix_free((void*) *e);
      }
   }
   else  { res = 0; }

#if 0
   /* For debugging */
   if(!res && *e)
   {
      printf("\n");
      for(i = 0; i < 80; ++i)  { printf("-"); }  printf("\n");
      ai = 0;
      while(CORE_HDR_EOH != (*e)[ai].id)
      {
         i = 0;
         do
         {
            if(hfieldids[i] == (*e)[ai].id)  { break; }
         }
         while(hfieldids[i++]);
         printf("%s: %s\n", hfields[i], (*e)[ai].content);
         ++ai;
      }
      for(i = 0; i < 80; ++i)  { printf("-"); } printf("\n");
      printf("\n");
   }
#endif

   return(res);
}


/* ========================================================================== */
/* Extract groups from 'Newsgroups' header field */
/*
 * The return value is 'NULL' on error or a pointer to an array of strings
 * otherwise. Because the parameter 'body' is allowed to have arbitrary size,
 * this array has arbitrary size too. The array is terminated with a 'NULL'
 * pointer. The caller is responsible to free the memory for the array.
 */

static const char**  extract_groups(const char*  body)
{
   const char**  res = NULL;
   size_t  i = 0;
   int  p_flag = 0;  /* Indicates processing of group is in progress */
   size_t  ns = 0;
   size_t  ne;
   char*  group = NULL;
   size_t  len;
   int  err = 0;
   size_t  asize = 1;  /* Current size of group array (in elements) */
   size_t  bsize = 0;  /* Current size of memory block (in octets) */
   const char**  p;

   /* Note: The header parser has already unfolded the body */
   do
   {
      /* Check for EOB or white space */
      if(!body[i] || ',' == body[i] || ' ' == body[i]
         || (const char) 0x09 == body[i])
      {
         ne = i;
         if(p_flag)
         {
            /* Extract group */
            if (ne < ns)  { continue; }
            else  { len = ne - ns; }
            group = (char*) posix_malloc(len + (size_t) 1);
            if(NULL == group)  { err = 1;  break; }
            memcpy(group, &body[ns], len);
            group[len] = 0;
            /* printf("Group: %s\n", group); */
            /* Add pointer to array */
            if(POSIX_SIZE_MAX == asize)  { err = 1;  break; }
            len = (asize + (size_t) 1) * sizeof(const char*);
            if(len >= bsize)
            {
               /* Allocate more memory in exponentially increasing chunks */
               /* Initial buffer size must be sufficient for two pointers */
               if(!bsize)  { bsize = sizeof(const char*); }
               p = posix_realloc((void*) res, bsize *= (size_t) 2);
               if(NULL == p)  { err = 1;  break; }
               res = p;
            }
            res[asize - (size_t) 1] = (const char*) group;
            res[asize++] = NULL;
         }
         p_flag = 0;
         continue;
      }
      else  { if(!p_flag)  { ns = i;  p_flag = 1; } }
   }
   while(body[i++]);

   /* Release memory if not successful */
   if(err)
   {
      posix_free((void*) group);
      if(res)
      {
         for(i = 0; i < asize; ++i)  { posix_free((void*) res[i]); }
         posix_free((void*) res);
         res = NULL;
      }
   }

   return(res);
}


/* ========================================================================== */
/* Extract Message-IDs from 'References' header field */
/*
 * RFC 5536 forbids that Message-IDs are longer than 250 octets.
 *
 * The return value is 'NULL' on error or a pointer to an array of strings
 * otherwise. Because the parameter 'body' is allowed to have arbitrary size,
 * this array has arbitrary size too. The array is terminated with a 'NULL'
 * pointer. The caller is responsible to free the memory for the array.
 */

static const char**  extract_refs(const char*  body)
{
   const char**  res = NULL;
   size_t  i = 0;
   int  p_flag = 0;  /* Indicates processing of msg-id is in progress */
   size_t  ns = 0;  /* Start index */
   size_t  ne;  /* End index (points behind the last character) */
   char*  msgid;
   size_t  len;
   int  err = 0;
   size_t  asize = 1;  /* Current size of reference array (in elements) */
   size_t  bsize = 0;  /* Current size of memory block (in octets) */
   const char**  p;
   int  cfws_warn = 1;

   /*
    * Note:
    * The header parser has already unfolded the body and replaced CFWS with SP
    */
   /* printf("\n%s\n", body); */
   do
   {
      /* Check for CFWS or end of body */
      /* The check for '<' is used for error tolerance if CFWS is missing */
      if(!body[i] || ' ' == body[i] || '<' == body[i])
      {
         if(p_flag)
         {
            p_flag = 0;
            ne = i;
            if(!ne || ns >= ne)
            {
               PRINT_ERROR("Header parser: "
                           "Invalid index in References (Bug)");
               continue;
            }
            else
            {
               len = ne - ns;
               if((size_t) 2 > len || (size_t) 250 < len)
               {
                  PRINT_ERROR("Header parser: "
                              "Invalid length of MID in References");
                  continue;
               }
               if('>' != body[ne - (size_t) 1])  { continue; }
            }
            /* Extract Message-ID */
            if(cfws_warn && '<' == body[i])
            {
               PRINT_ERROR("Header parser: CFWS missing in References");
               cfws_warn = 0;
            }
            msgid = (char*) posix_malloc(len + (size_t) 1);
            if(NULL == msgid)  { err = 1;  break; }
            memcpy(msgid, &body[ns], len);
            msgid[len] = 0;
            /* printf("Ref: %s\n", msgid); */
            /* Add pointer to array */
            if(POSIX_SIZE_MAX == asize)  { break; }
            len = (asize + (size_t) 1) * sizeof(const char*);
            if(len >= bsize)
            {
               /* Allocate more memory in exponentially increasing chunks */
               /* Initial buffer size must be sufficient for two pointers */
               if(!bsize)  { bsize = sizeof(const char*); }
               p = posix_realloc((void*) res, bsize *= (size_t) 2);
               if(NULL == p)  { break; }
               res = p;
            }
            res[asize - (size_t) 1] = (const char*) msgid;
            res[asize++] = NULL;
         }
      }
      /* Check for start of next Message-ID */
      if(!p_flag)
      {
         if('<' == body[i])  { ns = i;  p_flag = 1; }
      }
   }
   while(body[i++]);

   /* Release memory if not successful */
   if(res && err)
   {
      for(i = 0; i < asize; ++i)  { posix_free((void*) res[i]); }
      posix_free((void*) res);
      res = NULL;
   }

   return(res);
}


/* ========================================================================== */
/* Destroy article header object */

static void  header_object_destructor(struct core_article_header**  ahp)
{
   size_t  i;

   if(NULL != *ahp)
   {
      /* Delete header object content */
      posix_free((void*) (*ahp)->msgid);
      if(NULL != (*ahp)->groups)
      {
         i = 0;
         while(NULL != (*ahp)->groups[i])
         {
            posix_free((void*) (*ahp)->groups[i++]);
         }
         posix_free((void*) (*ahp)->groups);
      }
      posix_free((void*) (*ahp)->from);
      posix_free((void*) (*ahp)->subject);
      /* Note: Date field is part of the object structure */
      posix_free((void*) (*ahp)->supers);
      posix_free((void*) (*ahp)->fup2);
      posix_free((void*) (*ahp)->reply2);
      posix_free((void*) (*ahp)->uagent);
      posix_free((void*) (*ahp)->org);
      if(NULL != (*ahp)->refs)
      {
         i = 0;
         while(NULL != (*ahp)->refs[i])
         {
            posix_free((void*) (*ahp)->refs[i++]);
         }
         posix_free((void*) (*ahp)->refs);
      }
      posix_free((void*) (*ahp)->dist);
      posix_free((void*) (*ahp)->mime_v);
      posix_free((void*) (*ahp)->mime_ct);
      posix_free((void*) (*ahp)->mime_cte);
      posix_free((void*) (*ahp)->mime_cd);
      posix_free((void*) (*ahp)->x_newsr);
      posix_free((void*) (*ahp)->x_mailer);
      posix_free((void*) (*ahp)->x_pagent);
      /* Note: Lines field is part of the object structure */

      /* Delete header object structure */
      posix_free((void*) *ahp);
      *ahp = NULL;
   }
}


/* ========================================================================== */
/* Create article header object */
/*
 * If the same header field is found by the parser multiple times, the first
 * one is used and all others are ignored.
 */

/* Allocates an empty string */
#define CORE_GET_EMPTY_STRING(p) \
{ \
   p = (char*) posix_malloc(1); \
   if(NULL == p)  { res = -1; } \
   else  { ((char*) p)[0] = 0; } \
}
/* \attention Adjust the buffer size if you change the error message! */
#define CORE_GET_ERROR_STRING(p) \
{ \
   p = (char*) posix_malloc(34); \
   if(NULL == p)  { res = -1; } \
   else  { strcpy((char*) p, "[Missing or invalid header field]"); } \
}
static int  header_object_constructor(struct core_article_header**  ahp,
                                      const char*  h)
{
   int  res;
   struct core_headerfield*  e = NULL;
   size_t  i = 0;
   int  rv;
   const char*  mime_s;
   int  mime = 0;

   res = header_parser(&e, h);
   if(!res)
   {
      /* Allocate memory for header fields */
      *ahp = (struct core_article_header*)
             posix_malloc(sizeof(struct core_article_header));
      if(NULL == *ahp)  { res = -1; }
      else
      {
         /* Init header fields */
         /* Mandatory header fields */
         (*ahp)->msgid = NULL;
         (*ahp)->groups = NULL;
         (*ahp)->from = NULL;
         (*ahp)->subject = NULL;
         (*ahp)->date = 0;
         /* Optional header fields */
         (*ahp)->supers = NULL;
         (*ahp)->fup2 = NULL;
         (*ahp)->reply2 = NULL;
         (*ahp)->uagent = NULL;
         (*ahp)->org = NULL;
         (*ahp)->refs = NULL;
         (*ahp)->dist = NULL;
         (*ahp)->mime_v = NULL;
         (*ahp)->mime_ct = NULL;
         (*ahp)->mime_cte = NULL;
         (*ahp)->mime_cd = NULL;
         (*ahp)->x_newsr = NULL;
         (*ahp)->x_mailer = NULL;
         (*ahp)->x_pagent = NULL;
         /* Obsolete header fields */
         (*ahp)->lines = 0;

         /* Insert all fields that the parser have found */
         while(CORE_HDR_EOH != e[i].id)
         {
            switch(e[i].id)
            {
               case CORE_HDR_MSGID:
               {
                  if(!(*ahp)->msgid)
                  {
                     (*ahp)->msgid = e[i].content;
                     e[i].content = NULL;
                  }
                  break;
               }
               case CORE_HDR_GROUPS:
               {
                  if(!(*ahp)->groups)
                  {
                     (*ahp)->groups = extract_groups(e[i].content);
                     /* New memory was allocated, don't preserve 'content' */
                  }
                  break;
               }
               case CORE_HDR_FROM:
               {
                  if(!(*ahp)->from)
                  {
                     rv = enc_mime_word_decode(&mime_s, e[i].content);
                     if(!rv)  { mime = 1; }
                     else  { mime_s = e[i].content;  e[i].content = NULL; }
                     (*ahp)->from = mime_s;
                  }
                  break;
               }
               case CORE_HDR_SUBJECT:
               {
                  if(!(*ahp)->subject)
                  {
                     rv = enc_mime_word_decode(&mime_s, e[i].content);
                     if(!rv)  { mime = 1; }
                     else  { mime_s = e[i].content;  e[i].content = NULL; }
                     (*ahp)->subject = mime_s;
                  }
                  break;
               }
               case CORE_HDR_DATE:
               {
                  if(!(*ahp)->date)
                  {
                     (*ahp)->date = enc_timestamp_decode(e[i].content);
                  }
                  break;
               }
               case CORE_HDR_SUPERS:
               {
                  if(!(*ahp)->supers)
                  {
                     (*ahp)->supers = e[i].content;
                     e[i].content = NULL;
                  }
                  break;
               }
               case CORE_HDR_FUP2:
               {
                  if(!(*ahp)->fup2)
                  {
                     (*ahp)->fup2 = e[i].content;
                     e[i].content = NULL;
                  }
                  break;
               }
               case CORE_HDR_REPLY2:
               {
                  if(!(*ahp)->reply2)
                  {
                     rv = enc_mime_word_decode(&mime_s, e[i].content);
                     if(!rv)  { mime = 1; }
                     else  { mime_s = e[i].content;  e[i].content = NULL; }
                     (*ahp)->reply2 = mime_s;
                  }
                  break;
               }
               case CORE_HDR_UAGENT:
               {
                  if(!(*ahp)->uagent)
                  {
                     rv = enc_mime_word_decode(&mime_s, e[i].content);
                     if(!rv)  { mime = 1; }
                     else  { mime_s = e[i].content;  e[i].content = NULL; }
                     (*ahp)->uagent = mime_s;
                  }
                  break;
               }
               case CORE_HDR_ORG:
               {
                  if(!(*ahp)->org)
                  {
                     rv = enc_mime_word_decode(&mime_s, e[i].content);
                     if(!rv)  { mime = 1; }
                     else  { mime_s = e[i].content;  e[i].content = NULL; }
                     (*ahp)->org = mime_s;
                  }
                  break;
               }
               case CORE_HDR_REFS:
               {
                  if(!(*ahp)->refs)
                  {
                     (*ahp)->refs = extract_refs(e[i].content);
                     /* New memory was allocated, don't preserve 'content' */
                  }
                  break;
               }
               case CORE_HDR_DIST:
               {
                  if(!(*ahp)->dist)
                  {
                     (*ahp)->dist = e[i].content;
                     e[i].content = NULL;
                  }
                  break;
               }
               case CORE_HDR_MIME:
               {
                  if(!(*ahp)->mime_v)
                  {
                     (*ahp)->mime_v = e[i].content;
                     e[i].content = NULL;
                  }
                  break;
               }
               case CORE_HDR_CT:
               {
                  if(!(*ahp)->mime_ct)
                  {
                     rv = enc_mime_para_decode(&mime_s, e[i].content, 1);
                     if(!rv)  { mime = 1; }
                     else  { mime_s = e[i].content;  e[i].content = NULL; }
                     (*ahp)->mime_ct = mime_s;
                  }

                  break;
               }
               case CORE_HDR_CTE:
               {
                  if(!(*ahp)->mime_cte)
                  {
                     (*ahp)->mime_cte = e[i].content;
                     e[i].content = NULL;
                  }
                  break;
               }
               case CORE_HDR_CD:
               {
                  if(!(*ahp)->mime_cd)
                  {
                     rv = enc_mime_para_decode(&mime_s, e[i].content, 0);
                     if(rv)  { mime_s = e[i].content;  e[i].content = NULL; }
                     (*ahp)->mime_cd = mime_s;
                  }
                  break;
               }
               case CORE_HDR_X_NEWSR:
               {
                  if(!(*ahp)->x_newsr)
                  {
                     rv = enc_mime_word_decode(&mime_s, e[i].content);
                     if(!rv)  { mime = 1; }
                     else  { mime_s = e[i].content;  e[i].content = NULL; }
                     (*ahp)->x_newsr = mime_s;
                  }
                  break;
               }
               case CORE_HDR_X_MAILER:
               {
                  if(!(*ahp)->x_mailer)
                  {
                     rv = enc_mime_word_decode(&mime_s, e[i].content);
                     if(!rv)  { mime = 1; }
                     else  { mime_s = e[i].content;  e[i].content = NULL; }
                     (*ahp)->x_mailer = mime_s;
                  }
                  break;
               }
               case CORE_HDR_X_PAGENT:
               {
                  if(!(*ahp)->x_pagent)
                  {
                     rv = enc_mime_word_decode(&mime_s, e[i].content);
                     if(!rv)  { mime = 1; }
                     else  { mime_s = e[i].content;  e[i].content = NULL; }
                     (*ahp)->x_pagent = mime_s;
                  }
                  break;
               }
               case CORE_HDR_LINES:
               {
                  if(!(*ahp)->lines)
                  {
                     (*ahp)->lines = enc_lines_decode(e[i].content);
                  }
                  break;
               }
               default:
               {
                  PRINT_ERROR("Invalid header field ID");
                  break;
               }
            }
            ++i;
         }
      }

      /* Delete remaining parser results */
      i = 0;
      while(CORE_HDR_EOH != e[i].id)
      {
         posix_free((void*) e[i++].content);
      }
      posix_free((void*) e);

      /* Ensure that all mandatory header fields contain valid stings */
      if(!res)
      {
         if(NULL == (*ahp)->msgid)  { CORE_GET_EMPTY_STRING((*ahp)->msgid); }
         if(NULL == (*ahp)->groups)
         {
            (*ahp)->groups = (const char**)
                             posix_malloc(sizeof(const char*) * (size_t) 2);
            if(NULL != (*ahp)->groups)
            {
               CORE_GET_EMPTY_STRING((*ahp)->groups[0]);
               (*ahp)->groups[1] = NULL;
            }
         }
         if(NULL == (*ahp)->from)  { CORE_GET_ERROR_STRING((*ahp)->from); }
         if(NULL == (*ahp)->subject)
         {
            CORE_GET_ERROR_STRING((*ahp)->subject);
         }
      }

      /* Destroy unfinished object on error */
      if(res)  { header_object_destructor(ahp); }
   }

   /* Check for MIME format without MIME declaration */
   if(!res && mime)
   {
      if(!(*ahp)->mime_v)
      {
         PRINT_ERROR("Header parser: "
                     "MIME-Version field missing, but MIME is used");
      }
   }

   return(res);
}


/* ========================================================================== */
/* Create new hierarchy element object */

static int  hierarchy_element_constructor(struct core_hierarchy_element**  he,
                                          core_anum_t  num, unsigned int  flags)
{
   int  res = 0;

   /* Allocate hierarchy element structure */
   *he = (struct core_hierarchy_element*)
         posix_malloc(sizeof(struct core_hierarchy_element));
   if(NULL == *he)  { res = -1; }
   else
   {
      /* Init all fields of structure (all pointers to 'NULL') */
      (*he)->anum = num;           /* Article number 0 is reserved */
      (*he)->flags = flags;
      (*he)->header = NULL;
      (*he)->parent = NULL;
      (*he)->children = 0;
      (*he)->child = NULL;
   }

   return(res);
}


/* ========================================================================== */
/* Destroy hierarchy element object */

static void  hierarchy_element_destructor(struct core_hierarchy_element**  he)
{
   if(NULL != *he)
   {
      /* Destroy header object */
      header_object_destructor(&(*he)->header);
      /* Delete child array */
      posix_free((void*) (*he)->child);
      /* Delete hierarchy element structure */
      posix_free((void*) *he);
      *he = NULL;
   }
}


/* ========================================================================== */
/* Initialize new (sub)hierarchy */
/*
 * Deletes the current hierarchy and return with only the root node.
 *
 * For deep hierarchies this algorithm is slow, but memory consumption is very
 * low (no recursion).
 */

static int  hierarchy_init(struct core_hierarchy_element**  root)
{
   int  res = 0;
   struct core_hierarchy_element**  current;

   if(!root)  { res = -1; }
   else
   {
      /* Delete old article hierarchy */
      while(*root)
      {
         /* Process one leaf node per loop */
         current = root;
         while((*current)->children)
         {
            /* Select last child in array */
            current = &(*current)->child[(*current)->children - (size_t) 1];
         }
         if((*current)->parent)  { (*current)->parent->children--; }
         hierarchy_element_destructor(current);
      }
      /* Create new root node */
      res = hierarchy_element_constructor(root, 0, 0);
   }

   return(res);
}


/* ========================================================================== */
/* Add article to (sub)hierarchy */

static struct core_hierarchy_element*
hierarchy_find_article(const char*  msgid, struct core_hierarchy_element*  root)
{
   struct core_hierarchy_element*  res = root;
   struct core_hierarchy_element*  tmp;
   size_t  i;

   /* Check children of root node */
   for(i = 0; i < root->children; ++i)
   {
      if(!strcmp(msgid, root->child[i]->header->msgid))
      {
         res = root->child[i];
         break;
      }
      else
      {
         /* Recursively search for parent article if parent was not found */
         tmp = hierarchy_find_article(msgid, root->child[i]);
         if(tmp != root->child[i])
         {
            res = tmp;
            break;
         }
      }
   }

   return(res);
}

static int  hierarchy_sort_children(const struct core_hierarchy_element**  a,
                                    const struct core_hierarchy_element**  b)
{
   int  res = 0;
   core_time_t  a_date = (*a)->header->date;
   core_time_t  b_date = (*b)->header->date;

   if(!config[CONF_INV_ORDER].val.i)
   {
      /* Normal order */
      if(a_date < b_date)  { res = -1; }
      if(a_date > b_date)  { res = 1; }
   }
   else
   {
      /* Inverted order */
      if(a_date < b_date)  { res = 1; }
      if(a_date > b_date)  { res = -1; }
   }

   return(res);
}

static int  hierarchy_add(struct core_hierarchy_element**  root,
                          core_anum_t  num, unsigned int  flags,
                          const char*  header)
{
   int  res = 0;
   struct core_hierarchy_element*  he_new = NULL;
   struct core_hierarchy_element**  he_tmp = NULL;
   struct core_hierarchy_element*  he_parent = *root;
   struct core_hierarchy_element*  he_super = NULL;
   size_t  size;
   size_t  i = 0;
   const char  supers_subject[] = "[Superseded]";
   char*  tmp;
   int  rv;
   char  an[17];
   size_t  an_len;

   /* Fail if root node doesn't exist */
   if(NULL == *root)  { res = -1; }

   /* Create new article node */
   if(!res)
   {
      res = hierarchy_element_constructor(&he_new, num, flags);
      /* Process header */
      if(!res)
      {
         if(main_debug)
         {
            rv = enc_convert_anum_to_ascii(an, &an_len, num);
            if(!rv)
            {
               fprintf(stderr, "%s: %sAdd article %s to hierarchy ...\n",
                       CFG_NAME, MAIN_ERR_PREFIX, an);
            }
         }
         res = header_object_constructor(&he_new->header, header);
      }
      /* Check for error */
      if(0 > res)  { posix_free((void*) he_new); }
   }

   /* Check for supersede */
   if(!res)
   {
      if(NULL != he_new->header->supers)
      {
         /* Search for superseded article */
         he_super = hierarchy_find_article(he_new->header->supers, *root);
         if(he_super != *root)
         {
            /* Replace subject of superseded article */
            size = strlen(supers_subject);
            tmp = (char*) posix_realloc((void*) he_super->header->subject,
                                        ++size);
            if(NULL == tmp)
            {
               PRINT_ERROR("Memory allocation for subject field failed");
            }
            else
            {
               strcpy(tmp, supers_subject);
               he_super->header->subject = tmp;
            }
         }
      }
   }

   /* Search for parent article, otherwise add to root node */
   if(!res)
   {
      /* Find parent article if the new article has references */
      if(NULL != he_new->header->refs)
      {
         /* Search for last reference first */
         while(NULL != he_new->header->refs[i])  { ++i; };
         while(i)
         {
            he_parent = hierarchy_find_article(he_new->header->refs[--i],
                                               *root);
            if(he_parent != *root)  { break; }
         }
      }
      /* Increase size of parents child array */
      size = (he_parent->children + (size_t) 1)
              * sizeof(struct core_hierarchy_element*);
      he_tmp = (struct core_hierarchy_element**)
               posix_realloc(he_parent->child, size);
      if(NULL == he_tmp)  { res = -1; }
      /* Insert new node */
      if(res)  { hierarchy_element_destructor(&he_new); }
      else
      {
         he_parent->child = he_tmp;
         he_new->parent = he_parent;
         he_parent->child[he_parent->children++] = he_new;
         /* Sort array of children */
         qsort((void*) &he_parent->child[0], he_parent->children,
               sizeof(struct core_hierarchy_element*),
               (int (*)(const void*, const void*)) hierarchy_sort_children);
      }
   }

   return(res);
}


/* ========================================================================== */
/* Update element in (sub)hierarchy */

static struct core_hierarchy_element*
hierarchy_find_element(core_anum_t  num, struct core_hierarchy_element*  root)
{
   struct core_hierarchy_element*  res = NULL;
   struct core_hierarchy_element*  tmp;
   size_t  i;

   /* Check children of root node */
   for(i = 0; i < root->children; ++i)
   {
      if(num == root->child[i]->anum)
      {
         res = root->child[i];
         break;
      }
      else
      {
         /* Recursively search for parent article if parent was not found */
         tmp = hierarchy_find_element(num, root->child[i]);
         if(tmp)
         {
            res = tmp;
            break;
         }
      }
   }

   return(res);
}


static int  hierarchy_update(struct core_hierarchy_element**  root,
                             core_anum_t  num, const char*  header)
{
   int  res = 0;
   struct core_hierarchy_element*  he = NULL;
   struct core_article_header*  hdr = NULL;

   /* Fail if root node doesn't exist */
   if(NULL == *root)  { res = -1; }

   /* Search for hierarchy element to replace */
   if(!res)
   {
      he = hierarchy_find_element(num, *root);
      if(NULL == he)  { res = -1; }
   }

   /* Process header */
   if(!res)  { res = header_object_constructor(&hdr, header); }

   /* Replace header object of hierarchy element */
   if(!res)
   {
      header_object_destructor(&he->header);
      he->header = hdr;
   }

   return(res);
}


/* ========================================================================== */
/* Allocate and initialize nexus object */

static int  nexus_constructor(struct core_nexus**  nexus, const char*  server)
{
   int  res = -1;
   char*  s;

   if(NULL == *nexus)
   {
      s = (char*) posix_malloc(strlen(server) + (size_t) 1);
      if(NULL != s)
      {
         strcpy(s, server);
         *nexus = (struct core_nexus*) posix_malloc(sizeof(struct core_nexus));
         if(NULL == *nexus)  { posix_free((void*) s); }
         else
         {
            (*nexus)->nntp_state = CORE_NEXUS_CLOSED;
            (*nexus)->nntp_server = s;
            (*nexus)->nntp_handle = -1;
            (*nexus)->nntp_current_group = NULL;
            res = 0;
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/* Destroy nexus object */

static void  nexus_destructor(struct core_nexus**  nexus)
{
   if(NULL != *nexus)
   {
      if(NULL != (*nexus)->nntp_server)
      {
         posix_free((void*) (*nexus)->nntp_server);
      }
      if(NULL != (*nexus)->nntp_current_group)
      {
         posix_free((void*) (*nexus)->nntp_current_group);
      }
      posix_free((void*) *nexus);
      *nexus = NULL;
   }
}


/* ========================================================================== */
/* Establish connection to news server */

static int  nexus_open(struct core_nexus**  nexus)
{
   int  res = 0;
   const char*  logpathname = log_get_logpathname();
   const char*  service = config[CONF_SERVICE].val.s;
   int  enc = config[CONF_ENC].val.i;
   int  auth = config[CONF_AUTH].val.i;
   int  immed = config[CONF_IMMEDAUTH].val.i;
   const char*  user = config[CONF_USER].val.s;
   const char*  pass = config[CONF_PASS].val.s;

   /* Enable protocol logfile for debug mode */
   if(main_debug)
   {
      printf("%s: %sProtocol logfile: %s\n",
             CFG_NAME, MAIN_ERR_PREFIX, logpathname);
   }
   else
   {
      /* Remove potential existing logfile */
      if(!fu_check_file(logpathname, NULL))
      {
         (void) fu_unlink_file(logpathname);
      }
      posix_free((void*) logpathname);
      logpathname = NULL;
   }

   /* Allocate new nexus if required */
   if(NULL == *nexus)
   {
      res = nexus_constructor(nexus, config[CONF_SERVER].val.s);
   }
   if(!res)
   {
      /* Connect to NNTP server */
      /* Check authentication algorithm */
      switch(auth)
      {
         case 0:
         {
            res = nntp_open(&(*nexus)->nntp_handle, (*nexus)->nntp_server,
                            service, logpathname, enc, auth);
            break;
         }
         case 1:
         {
            if(!config[CONF_PASS].val.s[0])
            {
               PRINT_ERROR("Authentication with empty password rejected");
               res = -1;
            }
            else
            {
               res = nntp_open(&(*nexus)->nntp_handle, (*nexus)->nntp_server,
                              service, logpathname, enc, auth,
                              immed, user, pass);
            }
            break;
         }
         default:
         {
            PRINT_ERROR("Authentication algorithm not supported");
            res = -1;
            break;
         }
      }
      if (0 > res)  { (*nexus)->nntp_state = CORE_NEXUS_CLOSED; }
      else  { (*nexus)->nntp_state = CORE_NEXUS_ESTABLISHED; }
   }

   posix_free((void*) logpathname);

   return(res);
}


/* ========================================================================== */
/* Close connection to news server
 *
 * \attention
 * This function is called by the core thread cleanup handler.
 */

static void  nexus_close(struct core_nexus**  nexus)
{
   if(NULL != *nexus)
   {
      /* Close connection to NNTP server */
      if(-1 != (*nexus)->nntp_handle)
      {
         nntp_close(&(*nexus)->nntp_handle, 0);
      }
      /* Destroy nexus */
      nexus_destructor(nexus);
   }

   return;
}


/* ========================================================================== */
/* Try to establish a nexus */

static int  nexus_handler(struct core_nexus**  nexus)
{
   int  res = 0;

   /* Check whether nexus exist and is in established state */
   if(NULL == *nexus)  { res = -1; }
   else if(CORE_NEXUS_ESTABLISHED != (*nexus)->nntp_state)  { res = -1; }
   /* Establish nexus if required */
   if(res)
   {
      res = nexus_open(nexus);
      if(!res)
      {
         /* Set current group if there is one */
         if(NULL != (*nexus)->nntp_current_group)
         {
            nntp_set_group((*nexus)->nntp_handle, (*nexus)->nntp_current_group,
                           NULL);
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/* Check connection to server
 *
 * Close nexus if transport subsystem report broken connection.
 * Check whether authentication failed.
 *
 * \return
 * - Nonzero to indicate abort request
 */

static int  check_connection(int  r)
{
   int  res = 0;

   if(-2 == r)
   {
      PRINT_ERROR("Lost nexus, trying to recover");
      if(NULL != n)
      {
         nntp_close(&n->nntp_handle, NNTP_CLOSE_NOQUIT);
         n->nntp_state = CORE_NEXUS_CLOSED;
      }
   }
   else if(-3 == r)  { res = -1; }

   return(res);
}


/* ========================================================================== */
/* Get message of the day */

static void  get_motd(void)
{
   int  res;
   size_t  len = 0;
   char*  motd = NULL;
   unsigned int  retries = CORE_NEXUS_RETRIES;

   do
   {
      res = nexus_handler(&n);
      if(!res)
      {
         res = nntp_get_motd(n->nntp_handle, &motd, &len);
         if(!res)
         {
            /* Store result */
            data.data = (void*) motd;
            data.size = len;
         }
      }
      if(check_connection(res))  { break; }
   }
   while(res && retries--);
   data.result = res;
}


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

static void  get_distrib_pats(void)
{
   int  res;
   size_t  len = 0;
   const char*  d_pats = NULL;
   unsigned int  retries = CORE_NEXUS_RETRIES;

   do
   {
      res = nexus_handler(&n);
      if(!res)
      {
         res = nntp_get_distrib_pats(n->nntp_handle, &d_pats, &len);
         if(!res)
         {
            /* Store result */
            data.data = (void*) d_pats;
            data.size = len;
         }
      }
      if(check_connection(res))  { break; }
   }
   while(res && retries--);
   data.result = res;
}


/* ========================================================================== */
/* Get subscriptions */

static void  get_subscriptions(void)
{
   int  res;
   size_t  len = 0;
   char*  subs = NULL;
   unsigned int  retries = CORE_NEXUS_RETRIES;

   do
   {
      res = nexus_handler(&n);
      if(!res)
      {
         res = nntp_get_subscriptions(n->nntp_handle, &subs, &len);
         if(!res)
         {
            /* Store result */
            data.data = (void*) subs;
            data.size = len;
         }
      }
      if(check_connection(res))  { break; }
   }
   while(res && retries--);
   data.result = res;
}


/* ========================================================================== */
/* Reset group states and article header cache */

static void  reset_group_states(void)
{
   int  res;
   int  rv;

   nexus_close(&n);
   res = db_clear();
   rv = group_reset_states();
   if(!res)  { res = rv; }

   data.result = res;
}


/* ========================================================================== */
/* Get list of available newsgroups */

static void  get_group_list(void)
{
   int  res;
   size_t  gc;
   struct nntp_groupdesc*  groups = NULL;
   unsigned int  retries = CORE_NEXUS_RETRIES;

   do
   {
      res = nexus_handler(&n);
      if(!res)
      {
         res = nntp_get_grouplist(n->nntp_handle, &gc, &groups);
         if(!res)
         {
            data.data = (void*) groups;
            data.size = gc;
         }
      }
      if(check_connection(res))  { break; }
   }
   while(0 > res && retries--);
   data.result = res;
}


/* ========================================================================== */
/* Get list of newsgroup labels */

static void  get_group_labels(void)
{
   int  res;
   size_t  gc;
   struct nntp_grouplabel*  labels = NULL;
   unsigned int  retries = CORE_NEXUS_RETRIES;

   do
   {
      res = nexus_handler(&n);
      if(!res)
      {
         res = nntp_get_group_labels(n->nntp_handle, &gc, &labels);
         if(!res)
         {
            data.data = (void*) labels;
            data.size = gc;
         }
      }
      if(check_connection(res))  { break; }
   }
   while(0 > res && retries--);
   data.result = res;
}


/* ========================================================================== */
/* Get information about multiple newsgroups */

static void  get_groupinfo(void)
{
   int  res = -1;
   size_t  gc;
   struct core_groupstate**  gs;
   struct nntp_groupdesc*  gd = NULL;
   struct nntp_groupdesc*  garray = NULL;
   const char**  gl;
   size_t  i;
   unsigned int  retries = CORE_NEXUS_RETRIES;

   /* Extract data about groups to query */
   gc = data.size;
   gs = (struct core_groupstate**) data.data;
   if(!gc)
   {
      /* Delete article header cache for all groups */
      db_update_groups(0, NULL);
      data.data = (void*) NULL;
      res = 0;
   }
   else
   {
      /* Delete article header cache for all groups that are not listed */
      gl = (const char**) posix_malloc(gc * sizeof(const char*));
      if(NULL != gl)
      {
         for(i = 0; i < gc; ++i)  { gl[i] = (*gs)[i].name; }
         db_update_groups(gc, gl);
         posix_free((void*) gl);
      }
      do
      {
         res = nexus_handler(&n);
         if(!res)
         {
            /* Allocate memory for information object */
            garray = (struct nntp_groupdesc*)
                      posix_malloc(gc * sizeof(struct nntp_groupdesc));
            if(NULL == garray)  { res = -1;  break; }
            /* Get group information */
            for(i = 0; i < gc; ++i)
            {
               /* printf("Query info for group: %s\n", (*gs)[i].name); */
               res = nntp_set_group(n->nntp_handle, (*gs)[i].name, &gd);
               if(0 > res)
               {
                  /* Check for lost nexus */
                  if(-2 == res)  { break; }
                  /* Check for failed authentication */
                  if(-3 == res)
                  {
                     /* Give up */
                     retries = 0;
                     break;
                  }
                  /* Group not available => Mark as empty */
                  gd = nntp_group_descriptor_constructor((*gs)[i].name);
                  if(NULL == gd)  { break; }
                  else  { res = 0; }
               }
               memcpy((void*) &garray[i], (void*) gd,
                      sizeof(struct nntp_groupdesc));
               /* Update descriptor (because we have copied the name) */
               memcpy((void*) &garray[i].name, (void*) &(*gs)[i].name,
                      sizeof(const char*));
               posix_free((void*) gd);
            }
         }
         if(0 > res)
         {
            posix_free((void*) garray);
            garray = NULL;
         }
         else  { data.data = (void*) garray; }
         if(check_connection(res))  { break; }
      }
      while(0 > res && retries--);
   }
   data.result = res;
}


/* ========================================================================== */
/* Set current group */

static void  set_group(void)
{
   int  res;
   struct nntp_groupdesc*  gd = NULL;
   char*  gn;
   size_t  len;
   unsigned int  retries = CORE_NEXUS_RETRIES;
   core_anum_t  socr;  /* Start of current article watermark range */

   do
   {
      res = nexus_handler(&n);
      if(!res)
      {
         res = nntp_set_group(n->nntp_handle, (const char*) data.data, &gd);
         if(!res)
         {
            /*
             * Store current group
             * This is necessary to be able to reestablish the state if the
             * connection to the server gets broken.
             */
            len = strlen(data.data);
            gn = (char*) posix_malloc(++len);
            if(NULL == gn)  { n->nntp_current_group = NULL; }
            else
            {
               strcpy(gn, data.data);
               if(NULL != n->nntp_current_group)
               {
                  posix_free((void*) n->nntp_current_group);
               }
               n->nntp_current_group = (const char*) gn;
            }
            /* Clamp range of local database for group to current range */
            socr = (core_anum_t) gd->lwm;
            if((core_anum_t) 1 < socr)
            {
               db_delete(gd->name, (core_anum_t) 1, --socr);
            }
            /* Prepare result */
            data.size = 1;
            data.data = (void*) gd;
         }
      }
      if(check_connection(res))  { break; }
   }
   while(res && retries--);
   data.result = res;
}


/* ========================================================================== */
/* Get article header overview */

static void  get_overview(void)
{
   int  res;
   struct core_range*  range = (struct core_range*) data.data;
   size_t  len = 0;
   char*  overview = NULL;
   unsigned int  retries = CORE_NEXUS_RETRIES;

   do
   {
      res = nexus_handler(&n);
      if(!res)
      {
         res = nntp_get_overview(n->nntp_handle, range->first, range->last,
                                 &overview, &len);
         if(!res)
         {
            /* Store result */
            data.data = (void*) overview;
            data.size = len;
         }
      }
      if(check_connection(res))  { break; }
   }
   while(res && retries--);
   data.result = res;
}


/* ========================================================================== */
/* Get complete article via Message-ID */

static void  get_article_by_mid(void)
{
   int  res;
   size_t  len = 0;
   char*  article = NULL;
   unsigned int  retries = CORE_NEXUS_RETRIES;

   do
   {
      res = nexus_handler(&n);
      if(!res)
      {
         res = nntp_get_article_by_mid(n->nntp_handle, (const char*) data.data,
                                &article, &len);
         if(!res)
         {
            /* Store result */
            data.data = (void*) article;
            data.size = len;
         }
      }
      if(check_connection(res))  { break; }
   }
   while(res && retries--);
   data.result = res;
}


/* ========================================================================== */
/* Get complete article */

static void  get_article(void)
{
   int  res;
   size_t  len = 0;
   char*  article = NULL;
   unsigned int  retries = CORE_NEXUS_RETRIES;

   do
   {
      res = nexus_handler(&n);
      if(!res)
      {
         res = nntp_get_article(n->nntp_handle, (const nntp_anum_t*) data.data,
                                &article, &len);
         if(!res)
         {
            /* Store result */
            data.data = (void*) article;
            data.size = len;
         }
      }
      if(check_connection(res))  { break; }
   }
   while(res && retries--);
   data.result = res;
}


/* ========================================================================== */
/* Get article header */

static void  get_article_header(void)
{
   int  res;
   size_t  len = 0;
   char*  header = NULL;
   core_anum_t*  anum = (core_anum_t*) data.data;
   unsigned int  retries = CORE_NEXUS_RETRIES;

   /* Check whether requested article header is in local database */
   res = db_read(n->nntp_current_group, *anum, &header, &len);
   if(!res)
   {
      /* Yes */
      data.data = (void*) header;
      data.size = len;
   }
   else
   {
      /* No => Fetch it from server */
      do
      {
         res = nexus_handler(&n);
         if(!res)
         {
            res = nntp_get_article_header(n->nntp_handle,
                                          (const nntp_anum_t*) anum,
                                          &header, &len);
            if(!res)
            {
               data.data = (void*) header;
               data.size = len;
               /*
                * Add header to local database
                * Note that 'len' is the buffer size, not the header length!
                */
               db_add(n->nntp_current_group, *anum, header, strlen(header));
            }
            else
            {
               /*
                * Can't retrieve article header
                * Return success anyhow because missing articles in the reported
                * range are allowed (the IDs of canceled articles are not
                * reassigned). The NULL pointer indicates that there is no such
                * article in this case.
                */
               data.data = NULL;
               data.size = 0;
               res = 0;
            }
         }
         if(check_connection(res))  { break; }
      }
      while(res && retries--);
   }
   data.result = res;
}


/* ========================================================================== */
/* Get article body */

static void  get_article_body(void)
{
   int  res;
   size_t  len = 0;
   char*  body = NULL;
   unsigned int  retries = CORE_NEXUS_RETRIES;

   do
   {
      res = nexus_handler(&n);
      if(!res)
      {
         res = nntp_get_article_body(n->nntp_handle,
                                     (const nntp_anum_t*) data.data,
                                     &body, &len);
         if(!res)
         {
            /* Store result */
            data.data = (void*) body;
            data.size = len;
         }
      }
      if(check_connection(res))  { break; }
   }
   while(res && retries--);
   data.result = res;
}


/* ========================================================================== */
/* Post article */

static void  post_article(void)
{
   int  res;
   unsigned int  retries = CORE_NEXUS_RETRIES;

   do
   {
      res = nexus_handler(&n);
      if(!res)
      {
         res = nntp_post_article(n->nntp_handle, (const char*) data.data);
      }
      if(check_connection(res))  { break; }
   }
   while(res && retries--);
   /* Release memory allocated for article by core */
   posix_free((void*) data.data);
   data.data = NULL;
   data.size = 0;
   data.result = res;
}


/* ========================================================================== */
/* Core thread cleanup handler */

static void  cleanup_handler(void*  arg)
{
   /*
    * This function must not execute any potentially blocking I/O system calls
    * or other things that may not terminate. Otherwise cancelling the core
    * thread may fail and the program will freeze while joining the core thread.
    */
   if(main_debug)  { PRINT_ERROR("Execute cleanup handler"); }
}


/* ========================================================================== */
/* Core thread entry point */

static void*  core_main(void*  arg)
{
   int  rv;

   /* Install cleanup handler */
   posix_pthread_cleanup_push(cleanup_handler, NULL);

   /* Condition handler */
   core_mutex_lock();
   while(1)
   {
      /* Wait for wakeup condition */
      rv = posix_pthread_cond_wait(&pt_cond, &pt_mutex);
      if(rv)
      {
         PRINT_ERROR("Waiting for condition failed");
         break;
      }

      /* Execute command */
      switch(command)
      {
         case CORE_CMD_GET_MOTD:
         {
            get_motd();
            break;
         }
         case CORE_CMD_GET_DISTRIB_PATS:
         {
            get_distrib_pats();
            break;
         }
         case CORE_CMD_GET_SUBSCRIPTIONS:
         {
            get_subscriptions();
            break;
         }
         case CORE_CMD_RESET_GROUPSTATES:
         {
            reset_group_states();
            break;
         }
         case CORE_CMD_GET_GROUPLIST:
         {
            get_group_list();
            break;
         }
         case CORE_CMD_GET_GROUPLABELS:
         {
            get_group_labels();
            break;
         }
         case CORE_CMD_GET_GROUPINFO:
         {
            get_groupinfo();
            break;
         }
         case CORE_CMD_SET_GROUP:
         {
            set_group();
            break;
         }
         case CORE_CMD_GET_OVERVIEW:
         {
            get_overview();
            break;
         }
         case CORE_CMD_GET_ARTICLE_BY_MID:
         {
            get_article_by_mid();
            break;
         }
         case CORE_CMD_GET_ARTICLE:
         {
            get_article();
            break;
         }
         case CORE_CMD_GET_ARTICLE_HEADER:
         {
            get_article_header();
            break;
         }
         case CORE_CMD_GET_ARTICLE_BODY:
         {
            get_article_body();
            break;
         }
         case CORE_CMD_POST_ARTICLE:
         {
            post_article();
            break;
         }
         /* This command is for internal use only */
         case CORE_TERMINATE_NEXUS:
         {
            nexus_close(&n);
            data.result = 0;
            break;
         }
         default:
         {
            PRINT_ERROR("Unknown command ignored");
            data.result = -1;
            break;
         }
      }
      /*
       * Note:
       * If 'data.result' indicates success, the UI must release the memory
       * allocated for 'data.data'.
       */
      command = CORE_CMD_INVALID;

      /* Wakeup UI thread to process the result */
      if(CORE_TERMINATE_NEXUS != command)  { ui_wakeup(data.cookie); }
   }
   core_mutex_unlock();

   /* Remove cleanup handler */
   posix_pthread_cleanup_pop(1);

   return(NULL);
}


/* ========================================================================== */
/* Wait for core thread command queue to become empty
 *
 * \param[in] checks  Number of checks
 * \param[in] to      Timeout in miliseconds
 */

static int  commands_in_queue(unsigned int  checks, unsigned int  to)
{
   int  res = -1;
   unsigned int  i;
   int  rv;

   /* Wait until nexus termination completes */
   for(i = 0; i < checks; ++i)
   {
      rv = time_msleep(to);
      if(rv)  { break; }
      /* Check whether command queue is empty */
      rv = posix_pthread_mutex_trylock(&pt_mutex);
      if(!rv)
      {
         if(CORE_CMD_INVALID == command)
         {
            /* Yes => Return success */
            res = 0;
         }
         core_mutex_unlock();
      }
      if(!res)  { break; }
   }

   return(res);
}


/* ========================================================================== */
/* Destroy distribution pattern data object
 *
 * \param[out] pats  Pointer to object
 */

static void  core_distrib_pats_destructor(struct distrib_pats***  pats)
{
   size_t  i = 0;

   if(NULL != pats && NULL != *pats)
   {
      while(NULL != (*pats)[i])
      {
         posix_free((void*) (*pats)[i]->wildmat);
         posix_free((void*) (*pats)[i]->dist);
         posix_free((void*) (*pats)[i++]);
      }
      posix_free((void*) *pats);
      *pats = NULL;
   }

   return;
}


/* ========================================================================== */
/* Parse distribution patterns and create data object
 *
 * \param[out] pats  Extracted patterns, weigths and distributions
 * \param[in]  data  Raw data to parse (must be a zero terminated string)
 *
 * The content of \e data is expected to be RFC 3977 conformant distribution
 * pattern information with lines in the following format:
 * <br>
 * weight:wildmat:distribution
 *
 * \note
 * If \e data is an empty list (this is allowed by RFC 3977), a valid empty
 * object is created and success is returned.
 *
 * On success a pointer to the result object is written to \e pats and the
 * caller is responsible to destroy the object with the function
 * \ref core_distrib_pats_destructor()
 */

static int  core_distrib_pats_constructor(struct distrib_pats***  pats,
                                          const char*  data)
{
   int  res = -1;
   size_t  sosp = sizeof(struct distrib_pats*);
   const char*  raw = core_convert_canonical_to_posix(data, 0, 0);
   size_t  i = 0;
   const char*  p;
   size_t  line_len;
   char*  line = NULL;
   int  rv;
   unsigned long int  weight;
   char*  wildmat = NULL;
   char*  dist = NULL;
   struct distrib_pats*  element = NULL;
   struct distrib_pats**  object = NULL;
   size_t  objects = 1;
   char*  tmp;
   struct distrib_pats*  tmp2;
   struct distrib_pats**  tmp3;

   if(NULL != raw)
   {
      /* Check for empty list */
      if(!raw[i])
      {
         tmp3 = (struct distrib_pats**) posix_malloc(sosp);
         if(NULL != tmp3)
         {
            object = tmp3;
            object[0] = NULL;
            res = 0;
         }
      }
      else
      {
         /* Parse data */
         while(raw[i])
         {
            /* Parse next line */
            p = strchr(&raw[i], 0x0A);
            if(NULL == p)
            {
               /* Garbage at end of data => Ignore and treat as EOD */
               if(NULL != object)  { res = 0; }
               break;
            }
            else
            {
               line_len = (size_t) (p - &raw[i]);
               if(line_len)
               {
                  tmp = (char*) posix_realloc(line, line_len + (size_t) 1);
                  if(NULL == tmp)  { break; }
                  else
                  {
                     line = tmp;
                     strncpy(line, &raw[i], line_len);
                     line[line_len] = 0;
                  }
                  tmp = (char*) posix_realloc(wildmat, line_len + (size_t) 1);
                  if(NULL == tmp)  { break; }  else  { wildmat = tmp; }
                  tmp = (char*) posix_realloc(dist, line_len + (size_t) 1);
                  if(NULL == tmp)  { break; }  else  { dist = tmp; }
                  rv = sscanf(line, "%lu:%[^][\\:\r\n]:%s", &weight, wildmat, dist);
                  if(3 != rv)
                  {
                     PRINT_ERROR("Invalid distribution pattern ignored");
                  }
                  else
                  {
                     /* Create new element for object */
                     tmp2 = (struct distrib_pats*)
                            posix_malloc(sizeof(struct distrib_pats));
                     if(NULL != tmp2)
                     {
                        element = tmp2;
                        element->weight = weight;
                        element->wildmat = wildmat;  wildmat = NULL;
                        element->dist = dist;  dist = NULL;
                        /* Add new element to object */
                        tmp3 = (struct distrib_pats**)
                               posix_realloc(object, ++objects * sosp);
                        if(NULL == tmp3)
                        {
                           posix_free((void*) element->wildmat);
                           posix_free((void*) element->dist);
                           posix_free((void*) element);
                           break;
                        }
                        else
                        {
                           object = tmp3;
                           object[objects - 2] = element;
                           object[objects - 1] = NULL;
                           element = NULL;
                        }
                     }
                  }
               }
               i += line_len;
            }
            ++i;  /* Skip Linefeed */
         }
         /* Check for EOD */
         if(res && !raw[i] && NULL != object)  { res = 0; }
      }
   }
   posix_free((void*) line);
   posix_free((void*) wildmat);
   posix_free((void*) dist);
   posix_free((void*) raw);

   /* Check for error */
   if(res)  { core_distrib_pats_destructor(&object); }
   else  { *pats = object; }

   return(res);
}


/* ========================================================================== */
/* Check line length of article
 *
 * \param[in] article  Article in canonical format (with CRLF line termination)
 *
 * Check for lines containing more than 998 octets.
 *
 * RFC 5322 (Mail) specifies a line length limit of 998 characters (excluding
 * the CRLF line termination). RFC 2045 (MIME) and RFC 5536 (Netnews) do not
 * relax this limit.
 *
 * A character is an US-ASCII codepoint in the sense of RFC 5322. This is
 * redefined by RFC 2045 to an octet and RFC 5536 is based on MIME.
 *
 * \return
 * - 0 on success
 * - -1 if lines with more than 998 octets were detected
 */

static int core_check_line_length(const char*  article)
{
   int  res = 0;
   size_t  i = 0;
   size_t  len;
   char*  p;

   while(1)
   {
      /* Search for CR */
      p = strchr(&article[i], 0x0D);
      if(NULL == p)  { break; }
      len = p - &article[i];
      /* Check for valid CRLF line break */
      if(0x0A != (int) article[i + len + (size_t) 1])
      {
         PRINT_ERROR("Invalid CR control character (not part of line break)");
         res = -1;
         break;
      }
      if (998 < len)
      {
         PRINT_ERROR("Article contains lines with more than 998 octets");
         res = -1;
         break;
      }
      i += len + (size_t) 1;  /* Skip after CR */
      if(article[i])  { ++i; }  /* Skip expected LF too */
   }

   /* Special handling for last line without CRLF */
   if(!res)
   {
      p = strchr(&article[i], 0x00);
      if(NULL == p)
      {
         PRINT_ERROR("End of string not found (bug)");
         res = -1;
      }
      else
      {
         len = p - &article[i];
         if (998 < len)
         {
            PRINT_ERROR("Article contains lines with more than 998 octets");
            res = -1;
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Extract groups from 'Newsgroups' header field (exported for UI)
 *
 * \param[in] body  Unfolded body of \c Newsgroups header field
 *
 * Because the parameter \e body is allowed to have arbitrary size, the result
 * has arbitrary size too. The result array is terminated with a \c NULL entry.
 *
 * \note
 * The caller is responsible to free the memory for the array.
 *
 * \return
 * - Pointer to array of strings
 * - NULL on error
 */

const char**  core_extract_groups(const char*  body)
{
   return(extract_groups(body));
}


/* ========================================================================== */
/*! \brief Get list of available newsgroups (exported for UI)
 *
 * \param[in] cookie  Callback cookie
 *
 * The core will fetch the list with all groups that are available on the
 * server (always the default one from the configuration \ref config ).
 * After the operation was successfully started, the function returns (with the
 * value 1). After the operation has completed, the core thread calls the
 * function \ref ui_wakeup() with \e cookie as parameter.
 *
 * The UI can extract the result of the operation from the \ref core_data
 * object field \ref core_data::result (0 on success, negative on error).
 * On success the field \ref core_data::data points to a buffer with the array
 * of group descriptors.
 * The field \ref core_data::size contains the number of groups in the array.
 * If the server is available but doesn't contain groups, success is
 * returned with a \c NULL pointer and zero size.
 *
 * The caller is responsible to free the memory allocated for the array and all
 * of its elements.
 *
 * \return
 * - 1 indicates that the operation is in progress
 * - Negative value on error
 */

int  core_get_group_list(unsigned int  cookie)
{
   int  res = -1;
   int  rv = -1;

   /* Queue command */
   core_mutex_lock();
   if(CORE_CMD_INVALID == command)
   {
      data.cookie = cookie;
      data.result = -1;
      command = CORE_CMD_GET_GROUPLIST;
      res = 1;

      /* Wake up core thread */
      rv = posix_pthread_cond_signal(&pt_cond);
      if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
   }
   else
   {
      PRINT_ERROR("Command queue overflow");
      res = -1;
   }
   core_mutex_unlock();

   /* Check for error */
   if(rv)  res = -1;

   return(res);
}


/* ========================================================================== */
/*! \brief Get list of newsgroup labels (exported for UI)
 *
 * \param[in] cookie  Callback cookie
 *
 * The core will fetch the list with newsgroup labels that are available on the
 * server (always the default one from the configuration \ref config ).
 * After the operation was successfully started, the function returns (with the
 * value 1). After the operation has completed, the core thread calls the
 * function \ref ui_wakeup() with \e cookie as parameter.
 *
 * The UI can extract the result of the operation from the \ref core_data
 * object field \ref core_data::result (0 on success, negative on error).
 * On success the field \ref core_data::data points to a buffer with the array
 * of group label structures.
 * The field \ref core_data::size contains the number of groups in the array.
 * If the server is available but doesn't contain groups, success is
 * returned with a \c NULL pointer and zero size.
 *
 * The caller is responsible to free the memory allocated for the array and all
 * of its elements.
 *
 * \return
 * - 1 indicates that the operation is in progress
 * - Negative value on error
 */

int  core_get_group_labels(unsigned int  cookie)
{
   int  res = -1;
   int  rv = -1;

   /* Queue command */
   core_mutex_lock();
   if(CORE_CMD_INVALID == command)
   {
      data.cookie = cookie;
      data.result = -1;
      command = CORE_CMD_GET_GROUPLABELS;
      res = 1;

      /* Wake up core thread */
      rv = posix_pthread_cond_signal(&pt_cond);
      if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
   }
   else
   {
      PRINT_ERROR("Command queue overflow");
      res = -1;
   }
   core_mutex_unlock();

   /* Check for error */
   if(rv)  res = -1;

   return(res);
}


/* ========================================================================== */
/*! \brief Alphabetically sort group list (exported for UI)
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  core_sort_group_list(void)
{
   return(group_sort_list());
}


/* ========================================================================== */
/*! \brief Store group subscription (exported for UI)
 *
 * \param[in] name  Group name to add
 *
 * \note
 * The group \e name must have US-ASCII encoding or otherwise articles would
 * be not RFC 5536 conformant.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  core_subscribe_group(const char*  name)
{
   int  res = -1;
   struct core_groupstate  gs;

   if(enc_ascii_check(name))
   {
      PRINT_ERROR("Subscription rejected for invalid group name");
   }
   else
   {
      gs.name = name;
      gs.info = NULL;
      res = group_add(&gs);
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Remove group from list (exported for UI)
 *
 * \param[in,out] num    Pointer to number of groups in \e list
 * \param[in,out] list   Pointer to array of group state structures
 * \param[in]     index  Index of group to unsubscribe in \e list
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  core_unsubscribe_group(size_t*  num,  struct core_groupstate**  list,
                            size_t*  index)
{
   int  res = 0;

   if(!*num || *index > *num - (size_t) 1)  { res = -1; }
   else
   {
      posix_free((void*) (*list)[*index].name);
      group_article_range_destructor(&(*list)[*index].info);
      if(*index < *num - (size_t) 1)
      {
         memmove((void*) &(*list)[*index],
                 (void*) &(*list)[*index + (size_t) 1],
                 sizeof(struct core_groupstate) * (*num - (size_t) 1 - *index));
      }
      --*num;
      if(*index)  { --*index; }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Reset states of subscribed groups (exported for UI)
 *
 * \param[in] cookie  Callback cookie
 *
 * Closes the current server nexus and delete all group states.
 * This function must be called after the server has been changed and
 * the mapping from Message-IDs to article numbers is no longer valid.
 * After the operation was successfully started, the function returns (with the
 * value 1). After the operation has completed, the core thread calls the
 * function \ref ui_wakeup() with \e cookie as parameter.
 *
 * The UI can extract the result of the operation from the \ref core_data
 * object field \ref core_data::result (0 on success, negative on error).
 *
 * \return
 * - 1 indicates that the operation is in progress
 * - Negative value on error
 */

int  core_reset_group_states(unsigned int  cookie)
{
   int  res = -1;
   int  rv = -1;

   /* Queue command */
   core_mutex_lock();
   if(CORE_CMD_INVALID == command)
   {
      data.cookie = cookie;
      data.result = -1;
      command = CORE_CMD_RESET_GROUPSTATES;
      res = 1;

      /* Wake up core thread */
      rv = posix_pthread_cond_signal(&pt_cond);
      if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
   }
   else
   {
      PRINT_ERROR("Command queue overflow");
      res = -1;
   }
   core_mutex_unlock();

   /* Check for error */
   if(rv)  res = -1;

   return(res);
}


/* ========================================================================== */
/*! \brief Store states of subscribed groups (exported for UI)
 *
 * \param[in] num   Pointer to number of groups
 * \param[in] list  Pointer to array of group information structures
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  core_export_group_states(size_t  num, struct core_groupstate*  list)
{
   return(group_set_list(num, list));
}


/* ========================================================================== */
/*! \brief Get states of subscribed groups (exported for UI)
 *
 * The states which articles where already read are taken from the groupfile.
 * The last viewed articles of the groups are preserved.
 * The \e index is updated to match the new group list.
 *
 * \param[in,out] num    Pointer to number of groups in \e list
 * \param[in,out] list   Pointer to array of group state structures
 * \param[in,out] index  Pointer to index of current group in \e list
 *
 * The caller is responsible to free the memory allocated for the information
 * object. The destructor function \ref core_destroy_subscribed_group_states()
 * should be used for this purpose.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  core_update_subscribed_group_states(size_t*  num,
                                         struct core_groupstate**  list,
                                         size_t*  index)
{
   int  res = -1;
   size_t  num_old = *num;
   struct core_groupstate*  list_old = *list;
   size_t  index_old = *index;
   size_t  i;
   size_t  ii;

   /* Get new group list */
   res = group_get_list(num, list);

   /*
    * The current group database doesn't store the current index and the last
    * viewed article ID, therefore these fields are now invalid.
    */

   /* Reassign the missing values */
   if(num && num_old && NULL != *list && NULL != list_old)
   {
      *index = 0;
      for(i = 0; i < *num; ++i)
      {
         /* Process last viewed articles */
         for(ii = 0; ii < num_old; ++ii)
         {
            if(!strcmp((*list)[i].name, list_old[ii].name))
            {
               (*list)[i].last_viewed = list_old[ii].last_viewed;
            }
         }
         /* Process current index */
         if(!strcmp((*list)[i].name, list_old[index_old].name))
         {
            *index = index_old;
         }
      }
   }

   /* Free memory allocated for old list */
   group_destroy_list(&num_old, &list_old);

   return(res);
}


/* ========================================================================== */
/*! \brief Destructor for subscribed group states (exported for UI)
 *
 * \param[in] num   Pointer to number of groups
 * \param[in] list  Pointer to array of group state structures
 *
 * This destructor is intended to destroy the object created by the function
 * \ref core_update_subscribed_group_states() .
 *
 * If \e list is \c NULL , the function do nothing.
 */

void  core_destroy_subscribed_group_states(size_t*  num,
                                           struct core_groupstate**  list)
{
   group_destroy_list(num, list);
}


/* ========================================================================== */
/*! \brief Get information about subscribed groups (exported for UI)
 *
 * \param[in] num     Pointer to number of groups in \e list
 * \param[in] list    Pointer to array of state structures
 * \param[in] cookie  Callback cookie
 *
 * \note
 * An empty list with \e num pointing to zero is allowed.
 * The parameter \e list should point to \c NULL in this case.
 *
 * The core will collect the information for the groups listed in \e list from
 * the server (always the default one from the configuration
 * \ref config ).
 * After the operation was successfully started, the function returns (with the
 * value 1). After the operation has completed, the core thread calls the
 * function \ref ui_wakeup() with \e cookie as parameter.
 *
 * The UI can extract the result of the operation from the \ref core_data
 * object field \ref core_data::result (0 on success, negative on error).
 * On success the field \ref core_data::data points to an array of group
 * descriptors with \e num entries (or is \c NULL for an empty group list).
 *
 * The caller is responsible to free the memory allocated for the information
 * object. The destructor function \ref core_destroy_subscribed_group_info()
 * should be used for this purpose.
 *
 * \attention
 * Always call this function with all subscribed groups in \e list because the
 * article header cache is deleted for all groups not found in \e list as a side
 * effect.
 *
 * \return
 * - 1 indicates that the operation is in progress
 * - Negative value on error
 */

int  core_get_subscribed_group_info(const size_t*  num,
                                    struct core_groupstate**  list,
                                    unsigned int  cookie)
{
   int  res = -1;
   int  rv = -1;

   /* Queue command */
   core_mutex_lock();
   if(CORE_CMD_INVALID == command)
   {
      data.cookie = cookie;
      data.result = -1;
      data.size = *num;
      data.data = (void*) list;
      command = CORE_CMD_GET_GROUPINFO;
      res = 1;

      /* Wake up core thread */
      rv = posix_pthread_cond_signal(&pt_cond);
      if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
   }
   else
   {
      PRINT_ERROR("Command queue overflow");
      res = -1;
   }
   core_mutex_unlock();

   /* Check for error */
   if(rv)  res = -1;

   return(res);
}


/* ========================================================================== */
/*! \brief Destructor for subscribed group information (exported for UI)
 *
 * \param[in,out] list  Pointer to array of group descriptors
 *
 * This destructor is intended to destroy the object created by the function
 * \ref core_get_subscribed_group_info() .
 *
 * If \e list is \c NULL , the function do nothing.
 */

void  core_destroy_subscribed_group_info(struct core_groupdesc**  list)
{
   posix_free((void*) *list);
   *list = NULL;
}


/* ========================================================================== */
/*! \brief Set current group (exported for UI)
 *
 * \param[in] name    Group name
 * \param[in] cookie  Callback cookie
 *
 * The core will configure the server (always the default one from the
 * configuration \ref config ) to the current group \e name .
 * After the operation was successfully started, the function returns (with the
 * value 1). After the operation has completed, the core thread calls the
 * function \ref ui_wakeup() with \e cookie as parameter.
 *
 * The UI can extract the result of the operation from the \ref core_data
 * object field \ref core_data::result (0 on success, negative on error).
 * On success the field \ref core_data::data points to the buffer with the
 * group descriptor. The field \ref core_data::size is set to one.
 *
 * The caller is responsible to free the memory allocated for the buffer.
 *
 * \return
 * - 1 indicates that the operation is in progress
 * - Negative value on error
 */

int  core_set_group(const char*  name, unsigned int  cookie)
{
   int  res = -1;
   int  rv = -1;

   /* Queue command */
   core_mutex_lock();
   if(CORE_CMD_INVALID == command)
   {
      data.cookie = cookie;
      data.result = -1;
      data.data = (void*) name;
      command = CORE_CMD_SET_GROUP;
      res = 1;

      /* Wake up core thread */
      rv = posix_pthread_cond_signal(&pt_cond);
      if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
   }
   else
   {
      PRINT_ERROR("Command queue overflow");
      res = -1;
   }
   core_mutex_unlock();

   /* Check for error */
   if(rv)  res = -1;

   return(res);
}


/* ========================================================================== */
/*! \brief Get message of the day (exported for UI)
 *
 * \param[in] cookie  Callback cookie
 *
 * The core will fetch the message of the day from a server (currently
 * always the default one from the configuration \ref config ).
 *
 * After the operation was successfully started, the function returns (with the
 * value 1). After the operation has completed, the core thread calls the
 * function \ref ui_wakeup() with \e cookie as parameter.
 *
 * The UI can extract the result of the operation from the \ref core_data
 * object field \ref core_data::result (0 on success, negative on error).
 * On success the field \ref core_data::data points to the buffer with the
 * message of the day. The field \ref core_data::size contains the buffer size.
 * If the server is available, but doesn't provide a message of the day,
 * success is returned with a \c NULL pointer and zero size.
 *
 * The caller is responsible to free the memory allocated for the buffer.
 *
 * \return
 * - 1 indicates that the operation is in progress
 * - Negative value on error or if not supported
 */

int  core_get_motd(unsigned int  cookie)
{
   int  res = -1;
   int  rv = -1;

   if(NULL != n)
   {
      /* Check whether the server has message of the day capability */
      core_mutex_lock();
      if(!nntp_get_capa_list_motd(n->nntp_handle))
      {
         PRINT_ERROR("Server doesn't provide message of the day");
      }
      else
      {
         /* Yes => Queue command */
         if(CORE_CMD_INVALID == command)
         {
            data.cookie = cookie;
            data.result = -1;
            data.data = NULL;
            command = CORE_CMD_GET_MOTD;
            res = 1;

            /* Wake up core thread */
            rv = posix_pthread_cond_signal(&pt_cond);
            if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
         }
         else
         {
            PRINT_ERROR("Command queue overflow");
            res = -1;
         }

         /* Check for error */
         if(rv)  res = -1;
      }
      core_mutex_unlock();
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Get distribution suggestions (exported for UI)
 *
 * \param[out] dist    Suggested distribution for \e groups
 * \param[in]  groups  Newsgroup list (\c NULL for empty list is allowed)
 *
 * The core will fetch distribution suggestions (if available) from a server
 * (currently always the default one from the configuration \ref config )
 * for \e groups .
 * The result is the suggested content for the \c Distribution header field.
 *
 * On success, the caller is responsible to free the memory allocated for the
 * buffer.
 *
 * \return
 * - Zero on success (Pointer to new memory buffer was written to \e dist)
 * - Negative value for no suggestion and on error
 */

int  core_get_distribution(const char**  dist, const char**  groups)
{
   int  res = -1;
   const char*  data;
   struct distrib_pats**  pats = NULL;
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
   int  num;
   char*  p;
   size_t  len;
   size_t  i = 0;
   size_t  ii;
   int  iii;
   char*  buf = NULL;
   size_t  buf_len = 0;
   int  rv;
   struct enc_wm_pattern*  wma = NULL;
   posix_regex_t  ere;
   size_t  weight;
   int  match = -1;
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */

   core_mutex_lock();
   if(NULL == groups)
   {
      PRINT_ERROR("Empty group list for distribution suggestions");
   }
   else if(!config[CONF_DIST_SUGG].val.i)
   {
      PRINT_ERROR("Distribution suggestions disabled by configuration");
   }
   else
   {
      /* Get distribution patterns */
#if 0
      /* For debugging */
      char  x[1024];
      posix_snprintf(x, (size_t) 1024, "%s",
                     "3:de.alt.test:de\r\n"
      );
      data = x;
      res = 0;
#else
      res = nntp_get_distrib_pats(n->nntp_handle, &data, NULL);
#endif
      if(1 == res)
      {
         PRINT_ERROR("Server doesn't provide distribution suggestions");
         res = -1;
      }
      /* Parse distribution patterns */
      if(!res)  { res = core_distrib_pats_constructor(&pats, data); }
   }
   core_mutex_unlock();

   /* Check groups against patterns */
   if(!res)
   {
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
      res = -1;
      while(NULL != groups[i])
      {
         /*
          * Currently missing for Unicode groupname support:
          * - Groupname must be checked to be valid UTF-8
          * - Groupname must be normalized to NFC
          * - OS locale must use UTF-8 or groupname must be converted to the
          *   character set of the locale
          */
         if(enc_ascii_check(groups[i]))
         {
            PRINT_ERROR("Unicode groupname ignored (not supported yet)");
            ++i;
            continue;
         }
         weight = 0;
         match = -1;
         ii = 0;
         while(NULL != pats[ii])
         {
            /*
             * Currently missing for Unicode wildmat support
             * - Wildmat must be checked to be valid UTF-8
             * - Wildmat must be normalized to NFC
             * - OS locale must use UTF-8 or wildmat must be converted to the
             *   character set of the locale
             */
            if(enc_ascii_check(pats[ii]->wildmat))
            {
               PRINT_ERROR("Unicode wildmat ignored (not supported yet)");
               num = -1;
            }
            else
            {
               /* Convert wildmat to array of POSIX EREs */
               num = enc_create_wildmat(&wma, pats[ii]->wildmat);
            }
            if(0 < num)
            {
               /* Process array backwards to get rightmost pattern first */
               for(iii = num; iii; --iii)
               {
                  /* Compile regular expression */
                  rv = posix_regcomp(&ere, wma[iii - 1].ere,
                                     POSIX_REG_EXTENDED | POSIX_REG_NOSUB);
                  if(rv)
                  {
                     PRINT_ERROR("Compiling regular expression failed");
                  }
                  else
                  {
                     /* Check whether group matches wildmat */
                     rv = posix_regexec(&ere, groups[i], 0, NULL, 0);
                     posix_regfree(&ere);
                     if(!rv)
                     {
                        /* Yes */
                        if(wma[iii - 1].negate)
                        {
                        }
                        else
                        {
                           /* Check weigth */
                           if(weight <= pats[ii]->weight)
                           {
                              /* Select this index as current best match */
                              weight = pats[ii]->weight;
                              match = (int) ii;
                           }
                        }
                        break;
                     }
                  }
               }
               enc_destroy_wildmat(&wma, num);
            }
            /* Continue with next pattern */
            ++ii;
         }
         if(match >= 0)
         {
            /* Append suggested distribution */
            len = strlen(pats[match]->dist);
            if(!buf_len)
            {
               buf = (char*) posix_malloc(++len);
               if(NULL != buf)
               {
                  buf_len = len;
                  strcpy(buf, pats[match]->dist);
                  *dist = buf;
                  res = 0;
               }
            }
            else
            {
               p = (char*) posix_realloc((void*) buf, ++len + buf_len);
               if(NULL != p)
               {
                  buf = p;
                  buf_len += len;
                  strcat(buf, ",");
                  strcat(buf, pats[match]->dist);
                  *dist = buf;
               }
            }
         }
         /* Continue with next group */
         ++i;
      }
#else  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */
      PRINT_ERROR("Distribution pattern matching requires regular "
                  "expression support");
      res = -1;
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */
   }

   /* Destroy distribution pattern object */
   core_distrib_pats_destructor(&pats);

#if 0
   /* For debugging */
   if(!res)  { printf("Suggested distribution: %s\n", *dist); }
#endif

   return(res);
}


/* ========================================================================== */
/*! \brief Get subscription proposals (exported for UI)
 *
 * \param[in] cookie  Callback cookie
 *
 * The core will fetch the subscription proposals from a server (currently
 * always the default one from the configuration \ref config ).
 *
 * \note
 * The data provided by this function is intended for initial population of
 * a newsrc file.
 *
 * After the operation was successfully started, the function returns (with the
 * value 1). After the operation has completed, the core thread calls the
 * function \ref ui_wakeup() with \e cookie as parameter.
 *
 * The UI can extract the result of the operation from the \ref core_data
 * object field \ref core_data::result (0 on success, negative on error).
 * On success the field \ref core_data::data points to the buffer with the
 * newsgroup list. The field \ref core_data::size contains the buffer size.
 * If the server is available, but doesn't provide a message of the day,
 * success is returned with a \c NULL pointer and zero size.
 *
 * The caller is responsible to free the memory allocated for the buffer.
 *
 * \return
 * - 1 indicates that the operation is in progress
 * - Negative value on error or if not supported
 */

int  core_get_subscription_proposals(unsigned int  cookie)
{
   int  res = -1;
   int  rv = -1;

   /* Queue command */
   if(CORE_CMD_INVALID == command)
   {
      data.cookie = cookie;
      data.result = -1;
      data.data = NULL;
      command = CORE_CMD_GET_SUBSCRIPTIONS;
      res = 1;

      /* Wake up core thread */
      rv = posix_pthread_cond_signal(&pt_cond);
      if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
   }
   else
   {
      PRINT_ERROR("Command queue overflow");
      res = -1;
   }
   core_mutex_unlock();

   /* Check for error */
   if(rv)  res = -1;

   return(res);
}


/* ========================================================================== */
/*! \brief Get article header overview (exported for UI)
 *
 * \param[in] range   Pointer to article number range
 * \param[in] cookie  Callback cookie
 *
 * The core will fetch the article headers of range \e range from a server
 * (currently always the default one from the configuration \ref config ).
 *
 * \attention
 * The \e range must be one contiguous range without holes. A list of ranges is
 * not allowed here.
 *
 * After the operation was successfully started, the function returns (with the
 * value 1). After the operation has completed, the core thread calls the
 * function \ref ui_wakeup() with \e cookie as parameter.
 *
 * The UI can extract the result of the operation from the \ref core_data
 * object field \ref core_data::result (0 on success, negative on error).
 * On success the field \ref core_data::data points to the buffer with the
 * article headers. The field \ref core_data::size contains the buffer size.
 * If the server is available but doesn't contain the requested
 * articles, success is returned with a \c NULL pointer and zero size.
 *
 * The caller is responsible to free the memory allocated for the buffer.
 *
 * \return
 * - 1 indicates that the operation is in progress
 * - Negative value on error or if not supported
 */

int  core_get_overview(struct core_range*  range, unsigned int  cookie)
{
   int  res = -1;
   int  rv = -1;

   if(NULL != n)
   {
      /* Check whether server has overview capability */
      core_mutex_lock();
      if(nntp_get_capa_over(n->nntp_handle))
      {
         /* Yes => Queue command */
         if(CORE_CMD_INVALID == command)
         {
            data.cookie = cookie;
            data.result = -1;
            data.data = (void*) range;
            command = CORE_CMD_GET_OVERVIEW;
            res = 1;

            /* Wake up core thread */
            rv = posix_pthread_cond_signal(&pt_cond);
            if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
         }
         else
         {
            PRINT_ERROR("Command queue overflow");
            res = -1;
         }

         /* Check for error */
         if(rv)  res = -1;
      }
      core_mutex_unlock();
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Get complete article by Message-ID (exported for UI)
 *
 * \param[in] mid     Message-ID (with angle brackets)
 * \param[in] cookie  Callback cookie
 *
 * The core will fetch the article with Message-ID \e mid from a server
 * (currently always the default one from the configuration \ref config ).
 * After the operation was successfully started, the function returns (with the
 * value 1). After the operation has completed, the core thread calls the
 * function \ref ui_wakeup() with \e cookie as parameter.
 *
 * \attention
 * \e mid must be valid until \ref ui_wakeup() is called.
 *
 * The UI can extract the result of the operation from the \ref core_data
 * object field \ref core_data::result (0 on success, negative on error).
 * On success the field \ref core_data::data points to the buffer with the
 * article. The field \ref core_data::size contains the buffer size.
 * If the server is available but doesn't contain the requested article,
 * success is returned with a \c NULL pointer and zero size.
 *
 * The caller is responsible to free the memory allocated for the buffer.
 *
 * \return
 * - 1 indicates that the operation is in progress
 * - Negative value on error
 */

int  core_get_article_by_mid(const char*  mid, unsigned int  cookie)
{
   int  res = -1;
   int  rv = -1;

   /* Queue command */
   core_mutex_lock();
   if(CORE_CMD_INVALID == command)
   {
      data.cookie = cookie;
      data.result = -1;
      data.data = (void*) mid;
      command = CORE_CMD_GET_ARTICLE_BY_MID;
      res = 1;

      /* Wake up core thread */
      rv = posix_pthread_cond_signal(&pt_cond);
      if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
   }
   else
   {
      PRINT_ERROR("Command queue overflow");
      res = -1;
   }
   core_mutex_unlock();

   /* Check for error */
   if(rv)  res = -1;

   return(res);
}


/* ========================================================================== */
/*! \brief Get complete article (exported for UI)
 *
 * \param[in] id      Article number
 * \param[in] cookie  Callback cookie
 *
 * The core will fetch the article with number \e id from a server (currently
 * always the default one from the configuration \ref config ).
 * After the operation was successfully started, the function returns (with the
 * value 1). After the operation has completed, the core thread calls the
 * function \ref ui_wakeup() with \e cookie as parameter.
 *
 * \attention
 * \e id must be valid until \ref ui_wakeup() is called.
 *
 * The UI can extract the result of the operation from the \ref core_data
 * object field \ref core_data::result (0 on success, negative on error).
 * On success the field \ref core_data::data points to the buffer with the
 * article. The field \ref core_data::size contains the buffer size.
 * If the server is available but doesn't contain the requested article,
 * success is returned with a \c NULL pointer and zero size.
 *
 * The caller is responsible to free the memory allocated for the buffer.
 *
 * \return
 * - 1 indicates that the operation is in progress
 * - Negative value on error
 */

int  core_get_article(const core_anum_t*  id, unsigned int  cookie)
{
   int  res = -1;
   int  rv = -1;

   /* Queue command */
   core_mutex_lock();
   if(CORE_CMD_INVALID == command)
   {
      data.cookie = cookie;
      data.result = -1;
      data.data = (void*) id;
      command = CORE_CMD_GET_ARTICLE;
      res = 1;

      /* Wake up core thread */
      rv = posix_pthread_cond_signal(&pt_cond);
      if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
   }
   else
   {
      PRINT_ERROR("Command queue overflow");
      res = -1;
   }
   core_mutex_unlock();

   /* Check for error */
   if(rv)  res = -1;

   return(res);
}


/* ========================================================================== */
/*! \brief Get article header (exported for UI)
 *
 * \param[in] id      Article number
 * \param[in] cookie  Callback cookie
 *
 * The core will fetch the article header with number \e id from a server
 * (currently always the default one from the configuration \ref config ).
 * After the operation was successfully started, the function returns (with the
 * value 1). After the operation has completed, the core thread calls the
 * function \ref ui_wakeup() with \e cookie as parameter.
 *
 * \attention
 * \e id must be valid until \ref ui_wakeup() is called.
 *
 * The UI can extract the result of the operation from the \ref core_data
 * object field \ref core_data::result (0 on success, negative on error).
 * On success the field \ref core_data::data points to the buffer with the
 * article header. The field \ref core_data::size contains the buffer size.
 * If the server is available but doesn't contain the requested article,
 * success is returned with a \c NULL pointer and zero size.
 *
 * The caller is responsible to free the memory allocated for the buffer.
 *
 * \return
 * - 1 indicates that the operation is in progress
 * - Negative value on error
 */

int  core_get_article_header(const core_anum_t*  id, unsigned int  cookie)
{
   int  res = -1;
   int  rv = -1;

   /* Queue command */
   core_mutex_lock();
   if(CORE_CMD_INVALID == command)
   {
      data.cookie = cookie;
      data.result = -1;
      data.data = (void*) id;
      command = CORE_CMD_GET_ARTICLE_HEADER;
      res = 1;

      /* Wake up core thread */
      rv = posix_pthread_cond_signal(&pt_cond);
      if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
   }
   else
   {
      PRINT_ERROR("Command queue overflow");
      res = -1;
   }
   core_mutex_unlock();

   /* Check for error */
   if(rv)  res = -1;

   return(res);
}


/* ========================================================================== */
/*! \brief Get article body (exported for UI)
 *
 * \param[in] id      Article number
 * \param[in] cookie  Callback cookie
 *
 * The core will fetch the article body with number \e id from a server
 * (currently always the default one from the configuration \ref config ).
 * After the operation was successfully started, the function returns (with the
 * value 1). After the operation has completed, the core thread calls the
 * function \ref ui_wakeup() with \e cookie as parameter.
 *
 * \attention
 * \e id must be valid until \ref ui_wakeup() is called.
 *
 * The UI can extract the result of the operation from the \ref core_data object
 * field \ref core_data::result (0 on success, negative on error).
 * On success the field \ref core_data::data points to the buffer with the
 * article body. The field \ref core_data::size contains the buffer size.
 * If the server is available but doesn't contain the requested article,
 * success is returned with a \c NULL pointer and zero size.
 *
 * The caller is responsible to free the memory allocated for the buffer.
 *
 * \return
 * - 1 indicates that the operation is in progress
 * - Negative value on error
 */

int  core_get_article_body(const core_anum_t*  id, unsigned int  cookie)
{
   int  res = -1;
   int  rv = -1;

   /* Queue command */
   core_mutex_lock();
   if(CORE_CMD_INVALID == command)
   {
      data.cookie = cookie;
      data.result = -1;
      data.data = (void*) id;
      command = CORE_CMD_GET_ARTICLE_BODY;
      res = 1;

      /* Wake up core thread */
      rv = posix_pthread_cond_signal(&pt_cond);
      if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
   }
   else
   {
      PRINT_ERROR("Command queue overflow");
      res = -1;
   }
   core_mutex_unlock();

   /* Check for error */
   if(rv)  res = -1;

   return(res);
}


/* ========================================================================== */
/*! \brief Post article (exported for UI)
 *
 * \param[in] article  Pointer to Unicode article in RFC 5536 canonical form
 * \param[in] cookie   Callback cookie
 *
 * This function will check whether the article contains non-ASCII characters.
 * If this is the case in the header, the article is rejected (this is
 * forbidden by RFC 5536).
 *
 * If the body contains non-ASCII characters, the body is converted to ISO 8859
 * if possible (this is recommended by RFC 2046, can be disabled with the
 * \c force_unicode option in configfile).
 * Two fields are appended to the header indicating a transfer encoding of
 * \c 8bit and a content type of \c text/plain. For the content type, a
 * parameter for the actual character set is appended. A second parameter is
 * appended that indicates \c fixed format (according to RFC 3676).
 *
 * If injection via external inews is configured, the result is never 1 and
 * success (or error) derived from the exit status is returned immediately.
 * The parameter \e cookie is not used in this case.
 *
 * Otherwise the core will post the article pointed to by \e article to the
 * server (currently always the default one from the configuration
 * \ref config ). After the operation was successfully started, the function
 * returns (with the value 1). After the operation has completed, the core
 * thread calls the function \ref ui_wakeup() with \e cookie as parameter.
 *
 * \note
 * The article is copied immediately, the data where \e article points to must
 * not be valid until the operation is complete.
 *
 * The UI can extract the result of the operation from the \ref core_data object
 * field \ref core_data::result (0 on success, negative on error).
 *
 * \return
 * - 0 indicates success (from injection delegation to external inews)
 * - 1 indicates that the operation is in progress
 * - Negative value on error
 */

int  core_post_article(const char*  article, unsigned int  cookie)
{
   int  res = 1;
   int  rv;
   const char*  a = NULL;
   const char*  header = NULL;
   const char*  body = NULL;
   char*  p = NULL;
   char*  q;
   const char*  r;
   size_t  len;
   const char  hl_te[] = "Content-Transfer-Encoding: 8bit\r\n";
   const char  hl_ct[] = "Content-Type: text/plain; charset=";
   const char  hl_ct_ff[] = "; format=fixed\r\n";
   const char*  cs_iana = "UTF-8";

   /* Check if there are non-ASCII characters used */
   if(!enc_ascii_check(article))
   {
      /* No => Send as is but allocate new memory block */
      len = strlen(article);
      p = (char*) posix_malloc(++len);
      if(NULL == p)
      {
         PRINT_ERROR("Memory allocation for article failed");
         res = -1;
      }
      else
      {
         strncpy(p, article, len);
         a = p;
      }
   }
   else
   {
      /* Yes => Check encoding and normalize Unicode to NFC */
      a = enc_convert_to_utf8_nfc(ENC_CS_UTF_8, article);
      if(NULL == a)
      {
         PRINT_ERROR("Article encoding check/normalization failed");
         res = -1;
      }
      else
      {
         /* Copy result into scope of local memory manager in any case */
         len = strlen(a);
         p = (char*) posix_malloc(++len);
         if(NULL == p)
         {
            PRINT_ERROR("Memory allocation for article failed");
            res = -1;
         }
         else  { strncpy(p, a, len); }
         if(a != article)  { enc_free((void*) a); }
         a = p;
      }

      /* Extract header (without the "empty line" separator) */
      if(0 <= res)
      {
         p = strstr(a, "\r\n\r\n");
         if(NULL == p)
         {
            PRINT_ERROR("Article to post contains no header separator");
            res = -1;
         }
         else
         {
            len = (size_t) (p - a) + (size_t) 2;
            q = (char*) posix_malloc(++len);
            if(NULL == q)
            {
               PRINT_ERROR("Memory allocation for article header failed");
               res = -1;
            }
            else
            {
               strncpy(q, a, --len);  q[len] = 0;
               header = q;
               /* Verify that header is ASCII encoded */
               if(enc_ascii_check(header))
               {
                  PRINT_ERROR("Article to post has invalid header encoding");
                  res = -1;
               }
            }
         }
      }

      /* Extract body */
      if(0 <= res)
      {
         len = strlen(&p[4]);
         q = (char*) posix_malloc(++len);
         if(NULL == q)
         {
            PRINT_ERROR("Memory allocation for article body failed");
            res = -1;
         }
         else
         {
            strncpy(q, &p[4], --len);  q[len] = 0;
            body = q;
            /* Check whether user has forced Unicode */
            if (!config[CONF_FORCE_UNICODE].val.i)
            {
               /* Convert body to target character set */
               r = enc_convert_to_8bit(NULL, body, &cs_iana);
               if(NULL != r)
               {
                  /* 'cs_iana' now contains the new character set */
                  if(r != body)
                  {
                     /* Copy result into scope of local memory manager */
                     len = strlen(r);
                     p = (char*) posix_malloc(++len);
                     if(NULL == p)
                     {
                        PRINT_ERROR("Memory allocation for article body "
                                    "failed");
                        res = -1;
                     }
                     else
                     {
                        strncpy(p, r, len);
                        enc_free((void*) r);
                        posix_free((void*) body);
                        body = p;
                     }
                  }
               }
            }
         }
      }

      /* Add MIME transfer encoding and content type header fields */
      if(0 <= res)
      {
         len = strlen(header) + strlen(hl_te);
         len += strlen(hl_ct) + strlen(cs_iana) + strlen(hl_ct_ff);
         p = (char*) posix_malloc(++len);
         if(NULL == p)
         {
            PRINT_ERROR("Memory allocation for MIME header field failed");
            res = -1;
         }
         else
         {
            strcpy(p, header);
            strcat(p, hl_te);
            strcat(p, hl_ct);  strcat(p, cs_iana);  strcat(p, hl_ct_ff);
            posix_free((void*) header);
            header = p;
         }
      }

      /* Recombine header and body */
      if(0 <= res)
      {
         len = 2;
         len += strlen(header);
         len += strlen(body);
         p = (char*) posix_malloc(++len);
         if(NULL == p)
         {
            PRINT_ERROR("Memory allocation for final article failed");
            res = -1;
         }
         else
         {
            strcpy(p, header);
            strcat(p, "\r\n");  /* Empty line separator */
            strcat(p, body);
            posix_free((void*) a);
            a = p;
         }
      }
   }

   /* Release memory for article parts */
   posix_free((void*) header);
   posix_free((void*) body);

   /* Check line length */
   rv = core_check_line_length(a);
   if(rv)  { res = -1; }

   /* Post article */
   if(1 == res)
   {
      /* Check whether external inews should be used */
      if(strlen(config[CONF_INEWS].val.s))
      {
         /* Yes => Delegate injection to external inews */
         res = ext_inews(a);
         if(res)  { res = -1; }
      }
      else
      {
         /* No => Queue POST command for core thread */
         core_mutex_lock();
         if(CORE_CMD_INVALID == command)
         {
            data.cookie = cookie;
            data.result = -1;
            /* Core thread will release the memory block allocated for a */
            data.data = (void*) a;
            command = CORE_CMD_POST_ARTICLE;

            /* Wake up core thread */
            rv = posix_pthread_cond_signal(&pt_cond);
            if(rv)
            {
               PRINT_ERROR("Waking up core thread failed");
               /* Remove command from queue */
               command = CORE_CMD_INVALID;
               res = -1;
            }
         }
         else
         {
            PRINT_ERROR("Command queue overflow");
            res = -1;
         }
         core_mutex_unlock();
      }
   }

   /* Check for error */
   if(0 >= res)  { posix_free((void*) a); }

   return(res);
}


/* ========================================================================== */
/*! \brief Check whether article was alread read (exported for UI)
 *
 * \param[in] group    Group in which article resides
 * \param[in] article  Hierarchy element assigned to article
 *
 * \return
 * - 0 if article was not read yet
 * - Positive value if article was already read
 */

int  core_check_already_read(struct core_groupstate*  group,
                             struct core_hierarchy_element*  article)
{
   int  res = 0;
   struct core_range*  cr = group->info;
   core_anum_t  a = article->anum;

   /* Search article in list of already read ranges */
   while(NULL != cr)
   {
      if(a >= cr->first && a <= cr->last)  { res = 1;  break; }
      cr = cr->next;
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Mark article as read (exported for UI)
 *
 * \param[in,out] group    Group in which article resides
 * \param[in]     article  Article number
 */

void  core_mark_as_read(struct core_groupstate*  group, core_anum_t  article)
{
   struct core_range*  cr = group->info;
   core_anum_t  a = article;
   int  new_range = 0;
   int  before_current = 0;
   int  rv;
   struct core_range*  nr;
   struct core_range  tmp;

   /* Article number zero is reserved */
   if(a)
   {
#if 0
      /* For debugging */
      printf("\nGroup: %s\n", group->name);
      while(NULL != cr)
      {
         printf("  %lu-%lu\n", cr->first, cr->last);
         cr = cr->next;
      }
      cr = group->info;
      printf("Mark as read: %lu\n", a);
#endif
      /* Add article to list of already read ranges */
      if(NULL == cr)  { new_range = 1; }
      else
      {
         while(1)
         {
            /* Check whether article is inside current range */
            if(a >= cr->first && a <= cr->last)  { break; }
            /* Check whether article is before current range */
            if(a < cr->first)
            {
               /* Yes => Check whether current range can be extended downward */
               if(cr->first - (core_anum_t) 1 == a)  { cr->first -= 1; }
               else  { new_range = 1;  before_current = 1; }
               break;
            }
            /* No => Check whether current range can be extended upward */
            if(cr->last + (core_anum_t) 1 == a)  { cr->last += 1;  break; }
            /* Check for last range */
            if(NULL == cr->next)  { new_range = 1;  break; }
            else  { cr = cr->next; }
         }
      }
      if(new_range)
      {
         /* Insert new article range in list */
         rv = group_article_range_constructor(&nr, a, a, NULL);
         if(!rv)
         {
            if(NULL == cr)  { group->info = cr = nr; }
            else
            {
               if(before_current)
               {
                  /* Swapping new and current range */
                  nr->next = cr->next;
                  memcpy((void*) &tmp, (void*) cr, sizeof(struct core_range));
                  memcpy((void*) cr, (void*) nr, sizeof(struct core_range));
                  memcpy((void*) nr, (void*) &tmp, sizeof(struct core_range));
               }
               /* Add behind current range */
               nr->next = cr->next;  cr->next = nr;
            }
         }
      }
      /* Merge adjacent ranges */
      cr = group->info;
      while(NULL != cr)
      {
         if(NULL != cr->next)
         {
            if(cr->last + (core_anum_t) 1 == cr->next->first)
            {
               cr->last = cr->next->last;
               nr = cr->next;
               cr->next = cr->next->next;
               posix_free((void*) nr);
            }
         }
         cr = cr->next;
      }
#if 0
      /* For debugging */
      cr = group->info;
      while(NULL != cr)
      {
         printf("  %lu-%lu\n", cr->first, cr->last);
         cr = cr->next;
      }
#endif
   }
}


/* ========================================================================== */
/*! \brief Mark article as unread (exported for UI)
 *
 * \param[in,out] group    Group in which article resides
 * \param[in]     article  Article number
 */

void  core_mark_as_unread(struct core_groupstate*  group, core_anum_t  article)
{
   struct core_range*  cr = group->info;
   core_anum_t  a = article;
   struct core_range*  lr = NULL;
   struct core_range*  nr;
   struct core_range*  tmp;
   int  rv;

   /* Article number zero is reserved */
   if(a)
   {
#if 0
      /* For debugging */
      printf("\nGroup: %s\n", group->name);
      while(NULL != cr)
      {
         printf("  %lu-%lu\n", cr->first, cr->last);
         cr = cr->next;
      }
      cr = group->info;
      printf("Mark as unread: %lu\n", a);
#endif
      /* Remove article from list of already read ranges */
      while(NULL != cr)
      {
         if(a >= cr->first && a <= cr->last)
         {
            /* Article found in current range */
            if(cr->first == cr->last)
            {
               /* Remove article range */
               tmp = cr;
               cr = cr->next;
               tmp->next = NULL;
               group_article_range_destructor(&tmp);
               if(NULL == lr)  { group->info = cr; }
               else  { lr->next = cr;  }
            }
            else if(a == cr->first)
            {
               /* Modify start of article range */
               cr->first += (core_anum_t) 1;
            }
            else if(a == cr->last)
            {
               /* Modify end of article range */
               cr->last -= (core_anum_t) 1;
            }
            else
            {
               /* Split article range */
               rv = group_article_range_constructor(&nr, a + (core_anum_t) 1,
                                                    cr->last, NULL);
               if(!rv)
               {
                  cr->last = a - (core_anum_t) 1;
                  nr->next = cr->next;
                  cr->next = nr;
               }
            }
            break;
         }
         else  { lr = cr;  cr = cr->next; }
      }
#if 0
      /* For debugging */
      cr = group->info;
      while(NULL != cr)
      {
         printf("  %lu-%lu\n", cr->first, cr->last);
         cr = cr->next;
      }
#endif
   }
}


/* ========================================================================== */
/*! \brief Convert from canonical (RFC 822) to local (POSIX) form
 *
 * According to RFC 822 and RFC 2049 this function accepts plain text article
 * content in canonical form and convert the CR+LF line breaks to local (POSIX,
 * single LF) form. Single CR and LF characters are preserved by default (this
 * can be overridden by \e rcr and \e rlf respectively).
 *
 * \param[in] s    String to convert
 * \param[in] rcr  Insert replacement character for single CR
 * \param[in] rlf  Insert replacement character for single LF
 *
 * \return
 * - Pointer to decoded data (a new memory block was allocated)
 * - NULL on error
 */

const char*  core_convert_canonical_to_posix(const char*  s, int  rcr, int  rlf)
{
   return(enc_convert_canonical_to_posix(s, rcr, rlf));
}


/* ========================================================================== */
/*! \brief Convert from local (POSIX) to canonical (RFC 822) form
 *
 * According to RFC 822 and RFC 2049 this function accepts plain text article
 * content in local (POSIX) form and convert the single LF line breaks to
 * canonical (CR+LF) form. Single CR characters are removed.
 *
 * \param[in] s  String to convert
 *
 * \return
 * - Pointer to decoded data (a new memory block was allocated)
 * - NULL on error
 */

const char*  core_convert_posix_to_canonical(const char*  s)
{
   return(enc_convert_posix_to_canonical(s));
}


/* ========================================================================== */
/*! \brief Manage article hierarchy in memory (exported for UI)
 *
 * \param[in,out] hier    Pointer to article hierarchy pointer
 * \param[in]     action  What should be done
 * \param[in]     anum    Article number
 *
 * To use the default article hierarchy, \e hier shall be set to \c NULL .
 *
 * The core provides a facility to create a hierarchy from articles that contain
 * the header field "References". The UI can use it as follows:
 *
 * First the hierarchy must be initialized with the \e action
 * \ref CORE_HIERARCHY_INIT (this automatically destroys the old hierarchy
 * if one exist). All other parameters are ignored.
 *
 * At any time after initialization, the root node of the current hierarchy
 * can be read with the \e action \ref CORE_HIERARCHY_GETROOT . \e anum must be
 * zero and as additional parameter the address of a buffer must be
 * supplied to which the root node is written on success. The data type must be
 * \ref core_hierarchy_element \c ** .
 *
 * After an article header was fetched from the server, it can be added to the
 * hierarchy with the action \ref CORE_HIERARCHY_ADD . \e anum should be set to
 * the number that the server has assigned to the article and, as additional
 * parameter, a pointer to the raw article header must be provided. The data
 * type must be \c const \c char \c * . The hierarchy manager will parse the
 * article header, check it and extract all relevant information from it. With
 * this information an article header object is created that is part of the
 * hierarchy element object together with the article number, the parent and
 * child article pointers.
 *
 * The action \ref CORE_HIERARCHY_ADD_OVER does the same, except that a flag is
 * stored into the new node that indicates the incomplete data. This action is
 * intended to add NNTP overview data.
 *
 * \attention
 * For superseding to work, articles must be added in the correct order.
 * In other words: The article that should be superseded must already be part of
 * the hierarchy. In general this can easily achieved by adding the articles by
 * number in ascending order.
 *
 * The resulting hierarchy is a tree object that the UI can display without
 * detailed knowledge of the article header format.
 *
 * In some cases the data in the hierarchy elements is incomplete, e.g. if the
 * hierarchy was created from NNTP overview data. It is possible to recreate
 * single hierarchy elements with additional data in place, e.g. after the
 * complete article was downloaded later. To update an existing hierarchy
 * element, the action must be set to \ref CORE_HIERARCHY_UPDATE . The
 * parameters are the same as for \ref CORE_HIERARCHY_ADD .
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  core_hierarchy_manager(struct core_hierarchy_element**  hier,
                            enum core_hierarchy_action  action,
                            core_anum_t  anum, ...)
{
   va_list  ap;                    /* Object for argument list handling */
   int  res = 0;

   va_start(ap, anum);

   if(NULL == hier)  { hier = &h; }

   switch(action)
   {
      case CORE_HIERARCHY_INIT:
      {
         res = hierarchy_init(hier);
         break;
      }
      case CORE_HIERARCHY_GETROOT:
      {
         if(NULL == *hier)  { res = -1; }
         else  { *va_arg(ap, struct core_hierarchy_element**) = *hier; }
         break;
      }
      case CORE_HIERARCHY_ADD:
      {
         res = hierarchy_add(hier, anum, 0U, va_arg(ap, const char*));
         break;
      }
      case CORE_HIERARCHY_ADD_OVER:
      {
         res = hierarchy_add(hier, anum, CORE_HE_FLAG_OVER,
               va_arg(ap, const char*));
         break;
      }
      case CORE_HIERARCHY_UPDATE:
      {
         res = hierarchy_update(hier, anum, va_arg(ap, const char*));
         break;
      }
      default:
      {
         PRINT_ERROR("Invalid article hierarchy action");
         break;
      }
   }

   va_end(ap);

   return(res);
}


/* ========================================================================== */
/*! \brief Create article hierarchy from header overview (exported for UI)
 *
 * Parser for RFC 3977 conformant header overview data as provided by the
 * function \ref core_get_overview() .
 *
 * \note
 * All articles in \e range that are missing in \e overview are marked as read.
 *
 * \param[in,out] group  Group in which articles reside
 * \param[in] range      Pointer to article number range
 * \param[in] overview   Pointer to article header overview data
 */

void  core_create_hierarchy_from_overview(struct core_groupstate*  group,
                                          struct core_range*  range,
                                          const char*  overview)
{
   static const char  mime_v_field[] = "MIME-Version: 1.0\r\n";
   static const char  newsgroups_field[] = "Newsgroups: ";
   /* Names of corresponding header fields for overview data fields */
   static const char*  field[] =
   {
      "", "Subject: ", "From: ", "Date: ", "Message-ID: ", "References: ",
      "", "Lines: "
   };
   size_t  i = 0;             /* Index in overview */
   size_t  len;
   char*  content;            /* Content of single field */
   char*  header = NULL;      /* Pseudo-header generated from overview data */
   size_t  hi;                /* Index in pseudo-header */
   size_t  hlen = 4096;
   int  rv;
   core_anum_t  ai = 0;
   core_anum_t  aic = range->first;
   const char*  p;
   char*  q;
   unsigned int  f;
   unsigned int  f_max = 7;  /* There are 7 mandatory fields */
   size_t  tmp;
   int  error = 0;

   /* Get index of optional Newsgroups header field in overview */
   core_mutex_lock();
   rv = nntp_get_over_newsgroups_index(n->nntp_handle, &tmp);
   core_mutex_unlock();
   if(!rv && (size_t) 7 < tmp && POSIX_UINT_MAX > tmp)
   {
      /* Overview does contain Newsgroups header field */
      f_max = tmp;
   }

   /* Allocate initial buffer for pseudo-header */
   header = (char*) posix_malloc(hlen);
   /* Allocate buffer for 'sscanf()' that is guaranteed large enough */
   len = strlen(overview);
   content = (char*) posix_malloc(++len);
   /* Process overview lines */
   while(NULL != header && NULL != content)
   {
      /* Prepend MIME version header field (for header parser) */
      strcpy(header, mime_v_field);
      hi = sizeof(mime_v_field) - (size_t) 1;  /* Subtract 1 for NUL */
      /* Insert current group for FILTER module (if not in overview) */
      if(7 == f_max)
      {
         rv = posix_snprintf(&header[hi], hlen - hi, "%s%s\r\n",
                             newsgroups_field, group->name);
         if(0 < rv)
         {
            if(hlen - hi > (size_t) rv)  { hi += (size_t) rv; }
         }
      }
      /* Extract article watermark */
      rv = sscanf(&overview[i], "%s", content);
      if(1 == rv)
      {
         rv = enc_convert_ascii_to_anum(&ai, content, (int) strlen(content));
         if(!rv && ai)
         {
            /* Mark missing articles as read */
            while(aic < ai)  { core_mark_as_read(group, aic++); }
            ++aic;
            /* Extract other fields */
            for(f = 1; f_max >= f; ++f)
            {
               /* Search for start of next field (start with index 1) */
               p = strchr(&overview[i], 0x09);
               if(NULL == p)
               {
                  /* Field missing in overview */
                  error = 1;
                  break;
               }
               i += (size_t) (p - &overview[i]) + (size_t) 1;
               /* Skip unused fields */
               if(6U == f || (7U < f && f_max != f))  { continue; }
               /* Extract next field (allowed to be empty!) */
               if(0x09 == (int) (unsigned char) overview[i])  { continue; }
               rv = sscanf(&overview[i], "%[^\t]", content);
               if(1 == rv)
               {
                  /*
                   * Allocate more memory if required
                   * (3 additional bytes for termination "\r\n\0")
                   */
                  len = strlen(field[f]) + strlen(content) + (size_t) 3;
                  while(hlen - hi < len)
                  {
                     q = (char*) posix_realloc(header, hlen *= (size_t) 2);
                     if(NULL == q)  { error = 1;  break; }
                     else  { header = q; }
                  }
                  /* Append new header field to pseudo-header */
                  if(7 >= f)
                  {
                     rv = posix_snprintf(&header[hi], hlen - hi, "%s%s\r\n",
                                         field[f], content);
                  }
                  else
                  {
                     /* Optional header fields must have a ":full" tag */
                     rv = posix_snprintf(&header[hi], hlen - hi, "%s\r\n",
                                         content);
                  }
                  if(0 < rv)
                  {
                     if(hlen - hi <= (size_t) rv)
                     {
                        PRINT_ERROR("Buffer overflow in header overview"
                                    " parser detected (Bug)");
                        error = 1;
                        break;

                     }
                     hi += (size_t) rv;
                  }
               }
            }
            if(!error)
            {
#if 0
               /* For debugging */
               printf("Pseudo-Header generated from overview:\n---\n%s---\n",
                      header);
#endif
               rv = core_hierarchy_manager(NULL, CORE_HIERARCHY_ADD_OVER,
                                           ai, header);
               if(0 > rv)  { error = 1; }
            }
            if(error)
            {
               PRINT_ERROR("Header overview parser failed");
               break;
            }
         }
      }
      /* Skip to next line */
      p = strchr(&overview[i], 0x0A);
      if(NULL == p)  { break; }  /* End of overview data */
      i += (size_t) (p - &overview[i]) + (size_t) 1;
   }

   /* Mark potential missing articles at end of range as read */
   while(range->last > ai)  { core_mark_as_read(group, ++ai); }

   /* Check for error and free memory */
   if(NULL == header || NULL == content)
   {
      PRINT_ERROR("Out of memory while processing header overview data");
   }
   posix_free((void*) header);
   posix_free((void*) content);
}


/* ========================================================================== */
/*! \brief Parse header of MIME multipart entity (exported for UI)
 *
 * \param[in]  entity  Pointer to beginning of MIME multipart entity
 * \param[in]  len     Length of MIME multipart entity
 * \param[out] e_h     Object with decoded header of MIME multipart entity
 * \param[out] e_len   Length of MIME multipart entity content
 *
 * On success the caller is responsible to free the memory allocated for the
 * decoded header object. Use the function \ref core_destroy_entity_header()
 * to do this.
 *
 * \return
 * - Pointer to beginning of MIME multipart entity content on success
 * - NULL on error
 */

const char*  core_entity_parser(const char*  entity, size_t  len,
                                struct core_article_header**  e_h,
                                size_t*  e_len)
{
   const char*  res = NULL;
   char*  h = NULL;
   size_t  h_len;
   size_t  sob = 0;  /* Start of body */
   size_t  i;
   int  rv;

   /* Split entity into header and body */
   for(i = 0; i < len; ++i)
   {
      if(i && (char) 0x0A == entity[i])
      {
         if((char) 0x0D == entity[i - (size_t) 1])
         {
            /* End of line found => Check for empty line */
            if((size_t) 1 == i)  { sob = ++i;  break; }
            else if((size_t) 3 <= i)
            {
               if((char) 0x0A == entity[i - (size_t) 2]
                  && (char) 0x0D == entity[i - (size_t) 3])
               {
                  sob = ++i;
                  break;
               }
            }
         }
      }
   }
   if((size_t) 2 <= sob)
   {
      *e_len = len - sob;
      /* Strip the empty line between header and body */
      h_len = sob - (size_t) 2;
      h = (char*) posix_malloc(h_len + (size_t) 1);
      if(NULL != h)
      {
         /* Copy header and store pointer to start of body */
         strncpy(h, entity, h_len);  h[h_len] = 0;
         res = &entity[sob];
      }
   }

   /* Parse header of entity */
   if(NULL != h)
   {
      rv = header_object_constructor(e_h, h);
      if(rv)  { res = NULL; }
   }
   posix_free((void*) h);

   return(res);
}


/* ========================================================================== */
/*! \brief Destructor for MIME multipart entity header object (exported for UI)
 *
 * \param[in,out] ehp  Pointer to decoded header object of MIME multipart entity
 */

void  core_destroy_entity_header(struct core_article_header**  ehp)
{
   header_object_destructor(ehp);
}


/* ========================================================================== */
/*! \brief Get home directory of user (exported for UI)
 *
 * \note
 * On success, the caller is responsible to free the memory allocated for the
 * result.
 *
 * \return
 * - Pointer to result on success
 * - \c NULL on error
 */

const char*  core_get_homedir(void)
{
   const char*  buf = NULL;

   if(!ts_getenv("HOME", &buf))
   {
      if(fu_check_path(buf))
      {
         posix_free((void*) buf);
         buf = NULL;
      }
   }

   return(buf);
}


/* ========================================================================== */
/*! \brief Suggest pathname to save something to a file (exported for UI)
 *
 * Intended as suggestion for "save as" file selection dialogs.
 *
 * This implementation uses the program name and the date to create the
 * filename. The users home directory is used as path.
 *
 * \note
 * On success, the caller is responsible to free the memory allocated for the
 * result.
 *
 * \return
 * - Pointer to result on success
 * - \c NULL on error
 */

const char*  core_suggest_pathname(void)
{
   char*  buf = NULL;
   const char*  path = core_get_homedir();
   const char*  name = CFG_NAME;
   char  date[17] = { 0 };
   posix_time_t  ts;
   struct_posix_tm  t_data;
   struct_posix_tm*  t = NULL;
   size_t  len = 1;                /* NUL termination */
   int  rv;

   if(NULL != path)
   {
      posix_time(&ts);
      t = posix_gmtime_r(&ts, &t_data);
      if(NULL != t)
      {
         /* Create ISO 8601 timestamp */
         rv = posix_snprintf(date, (size_t) 17, "%04d%02d%02dT%02d%02d%02dZ",
                             t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
                             t->tm_hour, t->tm_min, t->tm_sec);
         if(16 != rv)
         {
            PRINT_ERROR("Created date and time string is invalid (bug)");
         }
         else
         {
            len += strlen(path);   /* Path to home directory */
            len += 1;              /* Slash */
            len += strlen(name);   /* Program name */
            len += 1;              /* Separator */
            len += strlen(date);   /* Timestamp */
            buf = (char*) posix_malloc(len);
            if(NULL != buf)
            {
                strcpy(buf, path);
                strcat(buf, "/");
                strcat(buf, name);
                strcat(buf, "-");
                strcat(buf, date);
            }
         }
      }
   }

   return(buf);
}


/* ========================================================================== */
/*! \brief Get current date and time in RFC 5322 format (exported for UI)
 *
 * \param[in] force_utc  Force usage of UTC time with unknown timezone
 *
 * RFC 5322 recommends but not require to use the local time. If the
 * POSIX.1-2001 API is available and \e force_utc is set to zero, then local
 * time is used. Otherwise the returned date is UTC and marked with the unknown
 * timezone information \c -0000 defined by RFC 5322.
 *
 * If \c timestamp_comment option is enabled in configfile and \e force_utc is
 * set to a nonzero value, a comment with the abbreviation of the timezone is
 * appended (if the POSIX.1-2001 API or the X/Open XSI extension are provided
 * by the operating system).
 *
 * \note
 * On success, the caller is responsible to free the memory allocated for the
 * result.
 *
 * \return
 * - Pointer to result on success
 * - \c NULL on error
 */

const char*  core_get_datetime(int  force_utc)
{
   static const char*  months[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                     "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
   static const char*  weekday[7] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri",
                                      "Sat"};
   char*  buf = NULL;
   size_t  len = 1;                /* NUL termination */
   posix_time_t  ts;
   struct_posix_tm  t_data;
   struct_posix_tm*  t = NULL;
   char  t_zone[6] = { 0, 0, 0, 0, 0, 0 };
   int  error = 0;
   int  fallback = 1;
   int  rv;

   /* Check whether user has configured to hide local time */
   if(!config[CONF_TS_LTIME].val.i)  { force_utc = 1; }

   /* Calculate required buffer size */
   len += 31;                      /* RFC 5322 date */
   len += 8;                       /* Timezone: SP + '(' + 5 digits + ')' */
   /* Allocate buffer for result */
   buf = (char*) posix_malloc(len);
   if(NULL != buf)
   {
      posix_time(&ts);
#if CFG_USE_POSIX_API >= 200112
      if(!force_utc)
      {
         t = posix_localtime_r(&ts, &t_data);
         if(NULL != t)
         {
            /* Get timezone information ("%z" is not available with SUSv2) */
            if(posix_strftime(t_zone, 6, "%z", t))  { fallback = 0; }
         }
      }
#endif  /* CFG_USE_POSIX_API >= 200112 */
      if(fallback)
      {
         /* Fallback for backward compatibility: No timezone information */
         t = posix_gmtime_r(&ts, &t_data);
         if(NULL == t)  { error = 1; }
         else  { strcpy(t_zone, "-0000"); }
      }
      if(!error)
      {
         rv = posix_snprintf(buf, len, "%s, %d %s %04d %02d:%02d:%02d %s",
                             weekday[t->tm_wday],
                             t->tm_mday, months[t->tm_mon], t->tm_year + 1900,
                             t->tm_hour, t->tm_min, t->tm_sec, t_zone);
         if(!(30 <= rv && 31 >= rv))
         {
            PRINT_ERROR("Created date and time string is invalid (bug)");
            error = 1;
         }
      }

#if CFG_USE_XSI || CFG_USE_POSIX_API >= 200112
      if(config[CONF_TS_COMMENT].val.i)
      {
         /* Add comment with timezone name */
         if(!fallback)
         {
            /* Accept only names that are not longer than numerical format */
            if(!posix_strftime(t_zone, 6, "%Z", t))
            {
               PRINT_ERROR("Creating timezone name comment failed");
            }
            else
            {
               strcat(buf, " (");
               strcat(buf, t_zone);
               strcat(buf, ")");
            }
         }
      }
#endif  /* CFG_USE_XSI || CFG_USE_POSIX_API >= 200112 */
   }

   /* Check for error */
   if(error)
   {
      posix_free((void*) buf);
      buf = NULL;
   }

   return(buf);
}


/* ========================================================================== */
/*! \brief Get globally unique Message-ID (exported for UI)
 *
 * \param[in] fqdn  Fully qualified domain name without root domain
 *
 * \attention
 * \e fqdn must be specified without the (nameless) DNS root domain, this means
 * there must be no trailing dot! \e fqdn must match the \c dot-atom syntax
 * defined in RFC 5322 to be usable as \c id-right element of a Message-ID.
 *
 * According to RFC 5536 the following rules are applied:
 * - The Message-ID must not be more than 250 octets in length => We check this.
 * - The Message-ID must be globally unique for all time => We use algorithm A3.
 * - The \c id-right element should be a domain => We use FQDN w/o root domain.
 * - Message-IDs should be unpredictable => We use a random field.
 *
 * Description of Message-ID format created by algorithm A3:
 * <br>
 * \c date \c random_pid . \c A3 . \c program \@ \c fqdn
 *
 * The date field contains a modified base64 encoded POSIX timestamp (seconds
 * since epoche) with 48bits.
 *
 * The random and pid fields are combined into a modified base64 encoded value
 * with 48bits. The first 16bits are random, they are added to deal with real
 * world clocks that are often not synchronized with UTC and don't always run
 * monotonic. They also make the Message-ID unpredictable as recommended by
 * RFC 5536. The last 32bits are the 32 LSBs of the process identifier (PID).
 * The function \c getpid() is used for this purpose. Both parts are combined
 * to one value to make the length an integral multiple of octet triplets that
 * can be base64 encoded without padding.
 *
 * The modified base64 encoding uses the base64 alphabet with \c / (slash)
 * replaced by \c - (minus).
 *
 * The Ax field represents the algorithm used to create the message ID. If the
 * algorithm is modified in the future, the number x should be incremented.
 *
 * The program field contains the value of the variable \c CFG_NAME from the
 * configuration file.
 *
 * The fqdn field is taken from the parameter \e fqdn and is checked for valid
 * \c dot-atom syntax according to RFC 5322.
 *
 * \note
 * On success, the caller is responsible to free the memory allocated for the
 * result.
 *
 * \return
 * - Pointer to result on success
 * - \c NULL on error
 */

const char*  core_get_msgid(const char*  fqdn)
{
   const char datext[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
                         "0123456789" "!#$%&'*+-/=?^_`{|}~" ".";
   char*  buf = NULL;
   size_t  len = 0;
   posix_time_t  ts;
   core_time_t  ts_int;
   unsigned long int  r;
   unsigned long int  pid;
   int  rv;
   int  error = 0;
   unsigned char  secs[6];
   unsigned char  rpp[6];
   const char*  buf_date = NULL;
   const char*  buf_rpp = NULL;
   size_t  i;

   /* Check "fqdn" parameter */
   if(NULL == fqdn)  { error = 1; }
   else
   {
      len = strlen(fqdn);
      if(!len)  { error = 1; }
      /* Check for dot-atom syntax */
      else if( len != strspn(fqdn, datext)
               || '.' == fqdn[0]
               || '.' == fqdn[len - (size_t) 1] )
      {
         PRINT_ERROR("Invalid FQDN format (dot-atom syntax required)");
         error = 1;
      }
   }
   if(!error)
   {
      len = 1;  /* NUL termination */
      /* Calculate required buffer size */
      ++len;  /* Opening angle bracket */
      len += (size_t) 8;  /* Date */
      len += (size_t) 8;  /* Random+PID */
      ++len;  /* Dot */
      len += (size_t) 2;  /* Algorithm */
      ++len;  /* Dot */
      len += strlen(CFG_NAME);  /* Program name */
      ++len;  /* Commercial at */
      len += strlen(fqdn);  /* FQDN (without root domain) */
      ++len;  /* Closing angle bracket */
      /* Verify length of Message-ID */
      if((size_t) 251 < len)
      {
         PRINT_ERROR("Invalid message ID (FQDN too long)");
         error = 1;
      }
   }
   if(!error)
   {
      /* Prepare date field */
      posix_time(&ts);
      ts_int = (core_time_t) ts;  /* ts is allowed to be floating point */
      secs[0] = 0;
      secs[1] = 0;
      secs[2] = (unsigned char) (ts_int >> 24);
      secs[3] = (unsigned char) (ts_int >> 16);
      secs[4] = (unsigned char) (ts_int >> 8);
      secs[5] = (unsigned char) (ts_int & 0xFFU);
      rv = enc_mime_encode_base64(&buf_date, (char*) secs, 6);
      if(rv)  { error = 1; }
   }
   if(!error)
   {
      /* Prepare random+pid field */
      r = (unsigned long int) posix_random();
      rpp[0] = (unsigned char) (r >> 8);
      rpp[1] = (unsigned char) (r & 0xFFU);
      pid = (unsigned long int) posix_getpid();
      rpp[2] = (unsigned char) (pid >> 24);
      rpp[3] = (unsigned char) (pid >> 16);
      rpp[4] = (unsigned char) (pid >> 8);
      rpp[5] = (unsigned char) (pid & 0xFFU);
      rv = enc_mime_encode_base64(&buf_rpp, (char*) rpp, 6);
      if(rv)  { error = 1; }
   }
   if(!error)
   {
      /* Allocate buffer for result */
      buf = (char*) posix_malloc(len);
      if(NULL != buf)
      {
         /* Create message ID */
         strcpy(buf, "<");
         strcat(buf, buf_date);
         strcat(buf, buf_rpp);
         strcat(buf, ".A3." CFG_NAME "@");
         strcat(buf, fqdn);
         strcat(buf, ">");
      }
   }
   /* Release memory allocated by base64 encoder */
   enc_free((void*) buf_date);
   enc_free((void*) buf_rpp);
   /* Replace "/" with "-" in <id-left> */
   for(i = 0; len > i; ++i)
   {
      if('@' == buf[i])  { break; }
      if('/' == buf[i])  { buf[i] = '-'; }
   }

   if(main_debug && NULL == buf)
   {
      fprintf(stderr, "%s: %sFunction core_get_msgid() returned error\n",
              CFG_NAME, MAIN_ERR_PREFIX);
   }

   return(buf);
}


/* ========================================================================== */
/*! \brief Create Cancel-Key for Message-ID (exported for UI)
 *
 * \param[in] scheme  Algorithm to use for "scheme"
 * \param[in] msgid   Message-ID of article that should correspond to Cancel-Key
 *
 * This function creates a \c c-key element with with \e scheme and \e mid
 * according to RFC 8315.
 *
 * On success, the caller is responsible for releasing the memory allocated for
 * the result.
 *
 * \return
 * - Pointer to the buffer containing the created \c c-key element on success
 * - \c NULL on error
 */

const char*  core_get_cancel_key(unsigned int  scheme, const char*  msgid)
{
   char*  res = NULL;
   const char*  confdir = NULL;
   const char*  spn = NULL;  /* Secret file pathname */
   int  fd = -1;
   const char*  scheme_string = NULL;
   const char*  sec = NULL;  /* Pointer to deprecated secret in config array */
   char  secret[64];  /* Buffer for secret from separate file */
   size_t  mac_len = 0;
   unsigned char  mac[HMAC_SHA2_256_LEN];  /* Large enough for all schemes */
   const char*  mac_enc;
   size_t  len;
   int  rv;

   switch(scheme)
   {
      case CORE_CL_SHA1:
      {
         mac_len = HMAC_SHA1_160_LEN;
         scheme_string = "sha1:";
         break;
      }
      case CORE_CL_SHA256:
      {
         mac_len = HMAC_SHA2_256_LEN;
         scheme_string = "sha256:";
         break;
      }
      default:
      {
         PRINT_ERROR("Scheme requested for Cancel-Key not supported");
         break;
      }
   }

   if(NULL != scheme_string)
   {
      rv = -1;
      switch(scheme)
      {
         case CORE_CL_SHA1:
         {
            /* Use secret from configfile for backward compatibility */
            if (strlen(config[CONF_CANCELKEY].val.s))
            {
               sec = config[CONF_CANCELKEY].val.s;
            }
            if(NULL != sec)
            {
               rv = hmac_sha1_160(msgid, strlen(msgid), sec, strlen(sec), mac);
               /* Do not overwrite secret in config array */
            }
            break;
         }
         case CORE_CL_SHA256:
         {
            /* Load secret from separate file */
            confdir = xdg_get_confdir(CFG_NAME);
            if(NULL != confdir)
            {
               rv = fu_create_path(confdir, (posix_mode_t) POSIX_S_IRWXU);
               if(0 == rv)
               {
                  spn = confdir;
                  rv = xdg_append_to_path(&spn, CORE_CL_SECRET_FILE);
                  if(0 == rv)
                  {
                     if(main_debug)
                     {
                        fprintf(stderr, "%s: %sCL secret file: %s\n",
                                CFG_NAME, MAIN_ERR_PREFIX, spn);
                     }
                     /* Generate new secret, if file does not already exist */
                     secure_cl_secret(spn);
                     rv = fu_open_file(spn, &fd, POSIX_O_RDONLY, 0);
                     posix_free((void*) spn);  spn = NULL;
                     if(rv)
                     {
                        PRINT_ERROR("Opening CL secret file failed");
                     }
                     else
                     {
                        len = 64;
                        rv = fu_read_from_filedesc(fd, secret, &len);
                        fu_close_file(&fd, NULL);
                        if(rv)
                        {
                           PRINT_ERROR("No secret available for CL scheme "
                                       "SHA256");
                        }
                        else
                        {
                           if(main_debug)
                           {
                              fprintf(stderr, "%s: %sSize of CL secret: %u"
                                      " octets\n", CFG_NAME, MAIN_ERR_PREFIX,
                                      (unsigned int) len);
                           }
                           if((size_t) 32 > len)
                           {
                              PRINT_ERROR("Warning: Secret for CL scheme "
                                          "SHA256 too short");
                           }
                           rv = hmac_sha2_256(msgid, strlen(msgid),
                                              secret, len, mac);
                           /* Overwrite secret in memory */
                           secure_clear_memory(secret, len);
                        }
                     }
                  }
               }
            }
            break;
         }
         default:
         {
            PRINT_ERROR("Invalid Cancel-Key scheme (bug)");
            break;
         }
      }

      if(!rv)
      {
         rv = enc_mime_encode_base64(&mac_enc, (char*) mac, mac_len);
         if(!rv)
         {
            len = strlen(scheme_string);
            len += strlen(mac_enc);
            res = (char*) posix_malloc(++len);
            if(NULL != res)
            {
               strcpy(res, scheme_string);
               strcat(res, mac_enc);
            }
            enc_free((void*) mac_enc);
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Create Cancel-Lock for Message-ID (exported for UI)
 *
 * \param[in] scheme  Algorithm to use for "scheme"
 * \param[in] mid    Message-ID of article that should correspond to Cancel-Lock
 *
 * This function creates a \c c-lock element with with \e scheme and \e mid
 * according to RFC 8315.
 *
 * On success, the caller is responsible for releasing the memory allocated for
 * the result.
 *
 * \return
 * - Pointer to the buffer containing the created \c c-lock element on success
 * - \c NULL on error
 */

const char*  core_get_cancel_lock(unsigned int  scheme, const char*  mid)
{
   char*  res = NULL;
   const char*  scheme_string = NULL;
   const char*  ckey = NULL;
   const char*  key = NULL;
   size_t  md_len = 0;
   unsigned char  md[DIGEST_SHA2_256_LEN];  /* Large enough for all schemes */
   const char*  md_enc;
   size_t  len;
   int  rv;

   switch(scheme)
   {
      case CORE_CL_SHA1:
      {
         md_len = DIGEST_SHA1_160_LEN;
         scheme_string = "sha1:";
         break;
      }
      case CORE_CL_SHA256:
      {
         md_len = DIGEST_SHA2_256_LEN;
         scheme_string = "sha256:";
         break;
      }
      default:
      {
         PRINT_ERROR("Scheme requested for Cancel-Lock not supported");
         break;
      }
   }

   if(NULL != scheme_string)
   {
      ckey = core_get_cancel_key(scheme, mid);
      if(NULL != ckey)
      {
         len = strlen(scheme_string);
         /* Check and strip scheme */
         if(strlen(ckey) < len || strncmp(ckey, scheme_string, len))
         {
            PRINT_ERROR("Cancel-Key has unsupported scheme (bug)");
         }
         else  { key = &ckey[len]; }
         if(NULL != key)
         {
            rv = -1;
            switch(scheme)
            {
               case CORE_CL_SHA1:
               {
                  rv = digest_sha1_160(key, strlen(key), md);
                  break;
               }
               case CORE_CL_SHA256:
               {
                  rv = digest_sha2_256(key, strlen(key), md);
                  break;
               }
               default:
               {
                  PRINT_ERROR("Invalid Cancel-Lock scheme (bug)");
                  break;
               }
            }
            if(!rv)
            {
               rv = enc_mime_encode_base64(&md_enc, (char*) md, md_len);
               if(!rv)
               {
                  len += strlen(md_enc);
                  res = (char*) posix_malloc(++len);
                  if(NULL != res)
                  {
                     strcpy(res, scheme_string);
                     strcat(res, md_enc);
                  }
                  enc_free((void*) md_enc);
               }
            }
         }
      }
   }
   posix_free((void*) ckey);

   return(res);
}


/* ========================================================================== */
/*! \brief Get signature for outgoing messages (exported for UI)
 *
 * \param[out] warnings  Pointer to location for result warning flags
 *
 * Use the \c CORE_SIG_FLAG_xxx constants to decode \e warnings .
 *
 * \note
 * It is allowed to pass \c NULL for \e warnings if the caller is not interested
 * in this data.
 *
 * \note
 * On success, the caller is responsible to free the memory allocated for the
 * result.
 *
 * \return
 * - Pointer to result on success
 * - \c NULL on error (nothing was written to \e warnings )
 */

const char*  core_get_signature(unsigned int*  warnings)
{
   char*  res = NULL;
   const char*  homedir = NULL;
   const char  sigdir[] = "/";
   char*  sigpathname = NULL;
   const char*  sigfile = NULL;
   int  rv;
   int  fd;
   size_t  len;
   unsigned int  warn_flags = 0;
   size_t  i;
   size_t  lines = 0;

   /* Get home directory from environment */
   rv = ts_getenv("HOME", &homedir);
   /* Prepare configuration directory */
   if(!rv)
   {
      /* Check home directory path */
      rv = fu_check_path(homedir);
      /* Create signature file pathname */
      if(!rv)
      {
         /* Take signature file from configuration */
         if(strlen(config[CONF_SIGFILE].val.s))
         {
            sigfile = config[CONF_SIGFILE].val.s;
         }
         if(NULL != sigfile)
         {
            /* The additional byte is for the terminating NUL */
            sigpathname = (char*) posix_malloc(strlen(homedir) + strlen(sigdir)
                                               + strlen(sigfile) + (size_t) 1);
            if(NULL == sigpathname)
            {
               PRINT_ERROR(""
                           "Cannot allocate memory for config file pathname");
            }
            else
            {
               strcpy(sigpathname, homedir);
               strcat(sigpathname, sigdir);
               strcat(sigpathname, sigfile);
               /* printf("Signature file: %s\n", sigpathname); */
               /* Check whether file exist */
               rv = fu_check_file(sigpathname, NULL);
               if(!rv)
               {
                  /* Open signature file and copy content to memory buffer */
                  rv = fu_open_file(sigpathname, &fd, POSIX_O_RDONLY, 0);
                  if(!rv)
                  {
                     rv = fu_read_whole_file(fd, &res, &len);
                     if(rv)  { res = NULL; }
                     fu_close_file(&fd, NULL);
                  }
               }
            }
         }
      }
      posix_free((void*) sigpathname);
      posix_free((void*) homedir);
   }
   else  { PRINT_ERROR("Environment variable 'HOME' is not defined"); }

   /* Return warning flags on success */
   if(NULL != res)
   {
      /* Check for missing separator */
      if(strncmp(res, "-- \n", (size_t) 4))
      {
         warn_flags |= CORE_SIG_FLAG_SEPARATOR;
      }
      /* This warning means that the signature is not valid UTF-8 */
      if(enc_uc_check_utf8(res))  { warn_flags |= CORE_SIG_FLAG_INVALID; }
      /* This warning means that the signature is not US-ASCII */
      if(enc_ascii_check(res))  { warn_flags |= CORE_SIG_FLAG_NOTASCII; }
      /* This warning means that the signature contains more than 4 lines */
      len = strlen(res);
      if(len)  { lines = 1; }
      for(i = 0; i < len; ++i)
      {
         if(0x0A == res[i])
         {
            if(i + (size_t) 1 != len)
            {
               if(POSIX_SIZE_MAX > lines)  { ++lines; }
            }
         }
      }
      if(lines && !(warn_flags & CORE_SIG_FLAG_SEPARATOR))  { --lines; }
      if((size_t) 4 < lines)  { warn_flags |= CORE_SIG_FLAG_LENGTH; }
      *warnings = warn_flags;
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Get introduction line for citation (exported for UI)
 *
 * \param[in] ca   Name of cited author
 * \param[in] ngl  Newsgroup list of the cited article
 *
 * The result of this function is an introduction line string without linebreak
 * at the end. The format is taken from the configuration \ref config .
 *
 * \note
 * On success, the caller is responsible to free the memory allocated for the
 * result.
 *
 * \return
 * - Pointer to result on success
 * - \c NULL on error
 */

const char*  core_get_introduction(const char*  ca, const char*  ngl)
{
   char*  res = NULL;
   int  error = 0;
   const char*  cfg;
   char*  fmt = NULL;
   char*  p = NULL;
   const char*  c1 = NULL;
   const char*  c2 = NULL;
   int  len;

   if(NULL != ca)
   {
      res = (char*) posix_malloc((size_t) 998);
      if(NULL != res)
      {
         /* Check format from configuration */
         cfg = config[CONF_INTRO].val.s;
         fmt = (char*) posix_malloc(strlen(cfg) + (size_t) 1);
         if (NULL == fmt)  { error = 1; }
         else
         {
            strcpy(fmt, cfg);
            p = strchr(fmt, (int) '%');
            if(NULL != p)
            {
               /* Check first conversion */
               if('s' == p[1])  { c1 = ca; }
               else if('g' == p[1])  { p[1] = 's';  c1 = ngl; }
               else  { error = 2; }
               if(!error)
               {
                  p = strchr(&p[2], (int) '%');
                  if(NULL != p)
                  {
                     /* Check second conversion */
                     if('s' == p[1])  { c2 = ca; }
                     else if('g' == p[1])  { p[1] = 's';  c2 = ngl; }
                     else  { error = 2; }
                     if(!error)
                     {
                        if(NULL != strchr(&p[2], (int) '%'))
                        {
                           fprintf(stderr, "%s: %s"
                              "More than 2 conversions in introduction line "
                              "are not supported\n", CFG_NAME, MAIN_ERR_PREFIX);
                           error = 1;
                        }
                     }
                     if(2 == error)
                     {
                        fprintf(stderr, "%s: %s"
                           "Conversion type in introduction line not "
                           "supported\n", CFG_NAME, MAIN_ERR_PREFIX);
                     }
                  }
               }
            }
            if(!error)
            {
               /* Create introduction line */
               len = posix_snprintf(res, 998, fmt, c1, c2);
               if(0 >= len)  { error = 1; }
            }
            posix_free((void*) fmt);
         }
         /* Check for error */
         if(error)
         {
            posix_free((void*) res);
            res = NULL;
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Convert pathname to codeset of locale (exported for UI)
 *
 * \param[in] pathname  Pathname to convert (UTF-8 encoded)
 *
 * On success the caller is responsible to free the memory allocated for the
 * returned pathname.
 *
 * \todo
 * As target this function uses the codeset that was detected for the locale
 * category \c LC_CTYPE by the \c FILTER module.
 * The locale parsing code should be moved to the \c CORE module.
 *
 * \return
 * - Pointer to (potentially converted) pathname on success
 * - NULL on error
 */

const char*  core_convert_pathname_to_locale(const char*  pathname)
{
   char*  res;
   int  error = 0;
   size_t  len = strlen(pathname) + (size_t) 1;
   const char*  rv;
   const char*  rv2;
   enum enc_mime_cs  cs;

   /* Attention: The conversions must never increase the size of the string! */
   res = (char*) posix_malloc(len);
   if(NULL != res)
   {
      switch(filter_get_locale_ctype())
      {
         case FILTER_CS_UTF_8:
         {
            /* Check and normalize to NFC */
            rv = enc_convert_to_utf8_nfc(ENC_CS_UTF_8, pathname);
            if(NULL == rv)  { error = 1; }
            else
            {
               if(len <= strlen(rv))  { error = 1; }
               else  { strcpy(res, rv); }
               if(rv != pathname)  { enc_free((void*) rv); }
            }
            break;
         }
         case FILTER_CS_ISO8859_1:
         {
            /* Check and normalize to NFC */
            rv = enc_convert_to_utf8_nfc(ENC_CS_UTF_8, pathname);
            if(NULL == rv)  { error = 1; }
            else
            {
               /* Convert to ISO 8859-1 */
               rv2 = enc_convert_to_8bit(&cs, rv, NULL);
               if(NULL == rv2)  { error = 1; }
               else
               {
                  if(ENC_CS_ISO8859_1 != cs || len <= strlen(rv2))
                  {
                     error = 1;
                  }
                  else  { strcpy(res, rv2); }
                  if(rv2 != rv)  { enc_free((void*) rv2); }
               }
               if(rv != pathname)  { enc_free((void*) rv); }
            }
            break;
         }
         case FILTER_CS_ASCII:
         {
            if(enc_ascii_check(pathname))  { error = 1; }
            else  { strcpy(res, pathname); }
            break;
         }
         default:
         {
            PRINT_ERROR("Unknown codeset (bug)");
            error = 1;
            break;
         }
      }
   }

   /* Check for error */
   if(error)
   {
      PRINT_ERROR("Pathname conversion to codeset of locale failed");
      posix_free((void*) res);
      res = NULL;
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Check wheter file exists (exported for UI)
 *
 * \param[in] pathname  Pathname of file (UTF-8 encoded)
 *
 * \note
 * The encoding of the pathname is converted to the encoding of the locale.
 *
 * \return
 * - 0 on success (file exists)
 * - Negative value on error
 */

int  core_check_file_exist(const char*  pathname)
{
   int  res = -1;
   const char*  pn = core_convert_pathname_to_locale(pathname);

   if(NULL != pn)
   {
      res = fu_check_file(pn, NULL);
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Save string to text file (exported for UI)
 *
 * \param[in] pathname  Pathname of file (UTF-8 encoded)
 * \param[in] s         String to convert
 *
 * If the file \e pathname does not exist, it is created.
 *
 * \note
 * The encoding of the pathname is converted to the encoding of the locale.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  core_save_to_file(const char*  pathname, const char*  s)
{
   int  res = -1;
   const char*  pn;
   int  flags = POSIX_O_WRONLY | POSIX_O_CREAT | POSIX_O_TRUNC;
   posix_mode_t  perm = POSIX_S_IRUSR | POSIX_S_IWUSR |
                        POSIX_S_IRGRP | POSIX_S_IWGRP |
                        POSIX_S_IROTH | POSIX_S_IWOTH;
   int  fd = -1;
   FILE*  fs = NULL;

   pn = core_convert_pathname_to_locale(pathname);
   if(NULL != pn)
   {
      res = fu_open_file(pn, &fd, flags, perm);
      if(!res)
      {
         res = fu_assign_stream(fd, &fs, "w");
         if(!res)  { fprintf(fs, "%s", s); }
      }
      fu_close_file(&fd, &fs);
      core_free((void*) pn);
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Create temporary file (exported for UI)
 *
 * On success the caller is responsible to free the memory allocated for the
 * returned pathname.
 * Use the function \ref core_tmpfile_delete() to delete the file and free the
 * memory block allocated for the pathname.
 *
 * \note
 * This function uses the value of the environment variable \c $TMPDIR for the
 * directory. If not set, \c /tmp is used instead.
 *
 * \return
 * - Pointer to pathname of created file
 * - NULL on error
 */

const char*  core_tmpfile_create(void)
{
#define CORE_PID_MAXLEN  (size_t) 32  /* Lower Limit: 15 */
   char*  tmppathname = NULL;
   int  rv;
   int  error = 0;
   const char*  tmpdir = NULL;
   char*  pn = NULL;
   size_t  len_pn;
   long int  len_pn_max;
   size_t  len;
   long int  pid;
   char  pid_string[CORE_PID_MAXLEN];

   /* Use value of $TMPDIR if set */
   rv = ts_getenv("TMPDIR", &tmpdir);
   if(0 > rv)
   {
      /* Fallback to "/tmp" if $TMPDIR is not set */
      tmpdir = "/tmp";
   }
   len = strlen(tmpdir);
   pn = posix_malloc(++len);
   if(NULL != pn)  { strcpy(pn, tmpdir); }
   if(0 <= rv)  { posix_free((void*) tmpdir); }

   /* Check for valid directory */
   if(NULL != pn)
   {
      len_pn = strlen(pn);
      len_pn_max = posix_pathconf(pn, POSIX_PC_NAME_MAX);
      if(0L > len_pn_max)
      {
         /* Pathname length check failed, use minimum required by POSIX.1 */
         PRINT_ERROR("Temporary file pathname length check failed");
         len_pn_max = 14;
      }
      /* printf("len_pn_max: %u\n", (unsigned int) len_pn_max); */
      if(POSIX_LONG_MAX > len_pn_max)
      {
         ++len_pn_max;  /* For slash separator */
      }

      /* Try to use "CFGNAME_PID_XXXXXX" format for unique name */
      len = strlen(CFG_NAME);
      len += (size_t) 2;  /* For slash and underscore */
      pid = (long int) posix_getpid();
      rv = posix_snprintf(pid_string, CORE_PID_MAXLEN, "%ld", pid);
      if(0 > rv || CORE_PID_MAXLEN <= (size_t) rv)  { error = 1; }
      else
      {
         len += (size_t) rv;
         len += (size_t) 7;  /* For "_XXXXXX" */
         if((size_t) len_pn_max >= len)
         {
            len_pn += len;
            /* One additional byte for NUL termination */
            tmppathname = posix_realloc((void*) pn, ++len_pn);
            if(NULL == tmppathname)  { error = 1; }
            else
            {
               strcat(tmppathname, "/" CFG_NAME "_");
               strcat(tmppathname, pid_string);
               strcat(tmppathname, "_XXXXXX");
            }
         }
      }
      /* Check for error */
      if(error)
      {
         /* Name too long => Use truncated $CFG_NAME */
         PRINT_ERROR("Temporary file pathname too long"
                     " (truncated and no longer unique)");
         len_pn += 15;
         tmppathname = posix_realloc((void*) pn, ++len_pn);
         if(NULL == tmppathname)
         {
            posix_free((void*) pn);
         }
         else
         {
            strncat(tmppathname, "/" CFG_NAME, (size_t) 8);
            strcat(tmppathname, "_XXXXXX");
         }
      }
   }

   /* Create temporary file */
   if(NULL != tmppathname)
   {
      rv = posix_mkstemp(tmppathname);
      if(0 > rv)
      {
         PRINT_ERROR("Creating temporary file failed");
         posix_free((void*) tmppathname);
         tmppathname = NULL;
      }
   }

   /* For code review: The caller must 'free()' the memory for 'tmppathname'! */
   return(tmppathname);
}


/* ========================================================================== */
/*! \brief Delete temporary file (exported for UI)
 *
 * \param[in] pathname  Pathname of file to delete
 *
 * \attention
 * This function automatically free the memory allocated for \e pathname .
 * This function should only be used with pathnames that are created with the
 * function \ref core_tmpfile_create() .
 */

void  core_tmpfile_delete(const char*  pathname)
{
   if(NULL != pathname)
   {
      (void) fu_unlink_file(pathname);
      posix_free((void*) pathname);
   }
}


/* ========================================================================== */
/*! \brief Close nexus (exported for UI) */

void  core_disconnect(void)
{
   if(NULL != n)
   {
      nntp_close(&n->nntp_handle, 0);
      n->nntp_state = CORE_NEXUS_CLOSED;
   }
}


/* ========================================================================== */
/*! \brief  Lock mutex for data object (exported for UI)
 *
 * You need to own the mutex before every access to the global object \ref data
 * to synchronize the threads.
 *
 * \attention Unlock the mutex again as soon as possible after the access!
 */

void  core_mutex_lock(void)
{
   int  rv;

   rv = posix_pthread_mutex_lock(&pt_mutex);
   if(rv)  { PRINT_ERROR("Locking mutex failed"); }
}


/* ========================================================================== */
/*! \brief Unlock mutex for data object (exported for UI)
 *
 * Call this function after the access to the global object \ref data is
 * complete.
 */

void  core_mutex_unlock(void)
{
   int  rv;

   rv = posix_pthread_mutex_unlock(&pt_mutex);
   if(rv)  { PRINT_ERROR("Unlocking mutex failed"); }
}


/* ========================================================================== */
/*! \brief Check whether code is running in UI thread (exported for UI)
 *
 * \return
 * - 0 if not running in UI thread
 * - 1 if running in UI thread
 * - Negative value on error
 */

int  core_check_thread_ui(void)
{
   int  res = -1;
   int  rv;

   if(pt_valid)
   {
      rv = posix_pthread_equal(ui_pt, posix_pthread_self());
      if(rv)  { res = 1; }  else  { res = 0; }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Free an object allocated by core (exported for UI)
 *
 * Use this function to release dynamic memory that was allocated by the core.
 *
 * \param[in] p  Pointer to object
 *
 * Release the memory for the object pointed to by \e p.
 *
 * \note
 * The pointer \e p is allowed to be \c NULL and no operation is performed in
 * this case.
 */

void  core_free(void*  p)
{
   posix_free(p);
}


/* ========================================================================== */
/*! \brief Initialize core (exported for UI)
 *
 * Spawn a new thread for the core.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  core_init(void)
{
   int  res;
   posix_sigset_t  sigmask;
   posix_sigset_t  oldmask;

   /* Store ID of UI thread */
   ui_pt = posix_pthread_self();

   /* Mask exit signals so that the core thread inherits the new mask */
   posix_sigemptyset(&sigmask);
   posix_sigaddset(&sigmask, POSIX_SIGINT);
   posix_sigaddset(&sigmask, POSIX_SIGQUIT);
   posix_sigaddset(&sigmask, POSIX_SIGTERM);
   res = posix_pthread_sigmask(POSIX_SIG_BLOCK, &sigmask, &oldmask);
   if(res)  { PRINT_ERROR("Setting signal mask failed"); }
   else
   {
      /*
       * Seed RNG with our PID
       *
       * Note that this RNG is never used for cryptographic tasks! We can't
       * share the cryptographic RNG from the TLS module because it is optional.
       * But we need at least some weak random numbers for Message-ID
       * generation. They must be available even for minimal configuration
       * without TLS module and without XSI extension of operating system.
       */
      posix_srandom((unsigned int) posix_getpid());
      /* Spawn core thread */
      res = posix_pthread_create(&pt, NULL, core_main, NULL);
      if(res)  { PRINT_ERROR("Spawning thread failed"); }
      else
      {
         pt_valid = 1;
         /* Restore signal mask for UI thread */
         res = posix_pthread_sigmask(POSIX_SIG_SETMASK, &oldmask, NULL);
         if(res)
         {
            PRINT_ERROR("Restoring signal mask failed");
            core_exit();
         }
      }
   }
   if(res)  res = -1;

   return(res);
}


/* ========================================================================== */
/*! \brief Shutdown core (exported for UI)
 *
 * Cancel the core thread and destroy the article hierarchy.
 *
 * \note
 * It is allowed to call this function after \ref core_init() had failed before.
 */

void  core_exit(void)
{
   int  rv;

   /* Cancel core thread */
   if(pt_valid)
   {
      /* Wait for command queue to become empty (1 second timeout) */
      if(main_debug)  { PRINT_ERROR("Wait for command queue to drain"); }
      rv = commands_in_queue(10U, 100U);
      if(rv)  { PRINT_ERROR("Command queue drain timeout"); }
      else
      {
         /* Queue nexus termination command */
         core_mutex_lock();
         data.cookie = 0;
         data.result = -1;
         data.data = NULL;
         command = CORE_TERMINATE_NEXUS;
         rv = posix_pthread_cond_signal(&pt_cond);
         if(rv)  { PRINT_ERROR("Waking up core thread failed"); }
         core_mutex_unlock();

         /* Wait until nexus termination completes (1 second timeout) */
         rv = commands_in_queue(10U, 100U);
         if(rv)  { PRINT_ERROR("Nexus termination failed"); }
      }
      /* Cancel core thread */
      if(main_debug)  { PRINT_ERROR("Cancel core thread"); }
      rv = posix_pthread_cancel(pt);
      if(rv)  { PRINT_ERROR("Cancelling core thread failed"); }
      else
      {
         /* Join core thread */
         if(main_debug)  { PRINT_ERROR("Join core thread"); }
         rv = posix_pthread_join(pt, NULL);
         if(rv)  { PRINT_ERROR("Joining core thread failed"); }
      }
   }

   /* Destroy article hierarchy */
   core_hierarchy_manager(NULL, CORE_HIERARCHY_INIT, 0);
   hierarchy_element_destructor(&h);
}


/*! @} */

/* EOF */
