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


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

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

#include <ctype.h>
#include <stddef.h>
#include <string.h>

#include <libjpiconv-0/iconv.h>
#include <libssiconv-0/iconv.h>

#include "conf.h"
#include "encoding.h"
#include "fileutils.h"
#include "main.h"


/* ========================================================================== */
/*! \defgroup ENCODING ENC: Codeset and header field handling
 *
 * The functions in this group should be conformant to the following standards:
 * ANSI X3.4,
 * ISO 2022, ISO 8601, ISO 8859, ISO 10646,
 * RFC 1468, RFC 2045, RFC 2046, RFC 2047, RFC 2049, RFC 2152, RFC 2183,
 * RFC 2231, RFC 2646, RFC 3629, RFC 3676, RFC 5198, RFC 5536, RFC 6657,
 * POSIX.1-1996,
 * Unicode 14.0.0
 *
 * \todo
 * We don't use \c iconv() because on old operating systems there may be no
 * Unicode support. And even on such old machines we don't want an external
 * dependency from GNU iconv.
 * <br>
 * There should be an option to use the systems \c iconv() on request.
 */
/*! @{ */


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

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

/* Define this to nonzero to enable Unicode NFC normalization debugging */
#define ENC_UC_NORM_DEBUG  0

/*! \brief Maximum length of MIME parameter attribute tokens */
#define ENC_MIME_PARA_LENGTH_MAX  (size_t) 127

/*! \brief MIME word encoder folding behaviour
 *
 * If this is defined to nonzero, all lines of RFC 2047 conformant header fields
 * that contain MIME encoded words are folded before 76 characters. Otherwise
 * all lines that contain no encoded-words are not folded before 998 characters.
 *
 * RFC 2047 is ambigous regarding this rule:
 * <br>
 * https://tools.ietf.org/html/rfc2047#section-2
 * <br>
 * The default value 1 is safe in any case. Please read section 2, paragraph 5
 * carefully before redefining this to 0!
 */
#define ENC_MIME_HEADER_FOLD_ASCII_LINES  1


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

/* ISO 2022 decoder states */
enum iso2022_state
{
   ISO2022_ASCII,
   ISO2022_ISO646,
   ISO2022_JIS_X_0208
};

/* Unicode hangul syllable type */
enum uc_hs_type
{
   UC_HST_NONE,
   UC_HST_L,
   UC_HST_V,
   UC_HST_T,
   UC_HST_LV,
   UC_HST_LVT
};

/* Unicode canonical decomposition (and character combining class) */
struct uc_cdc
{
   long int  cp;          /* Codepoint */
   unsigned char  ccc;    /* Canonical combining class */
   long int  dc1;         /* Decomposition mapping (recursive part 1) */
   long int  dc2;         /* Decomposition mapping (non-recursive part 2) */
};

/* Unicode hangul syllable type ranges */
struct uc_hst
{
   long int  first;       /* First codepoint of range */
   long int  last;        /* Last codepoint of range */
   enum uc_hs_type  hst;  /* Hangul syllable type */
};

/* Unicode NFC quick check codepoint ranges indicating normalization required */
struct uc_qc_nfc
{
   long int  first;       /* First codepoint of range */
   long int  last;        /* Last codepoint of range */
};

/* Unicode full composition exclusion codepoint ranges */
struct uc_fce
{
   long int  first;       /* First codepoint of range */
   long int  last;        /* Last codepoint of range */
};

/* Unicode default case folding mapping */
struct uc_cf
{
   long int  cp;          /* Codepoint */
   long int  first;       /* First codepoint of range */
   long int  second;      /* Second codepoint of range */
   long int  third;       /* Third codepoint of range */
};

/* IOS2022-JP to Unicode codepoint mapping */
struct iso2022_jp
{
   long int  jis;         /* JIS X 0208 codepoint */
   long int  uc;          /* Unicode codepoint */
};

/* MIME parameter (for RFC 2231 decoder) */
struct mime_parameter
{
   int  valid;
   char  attribute[ENC_MIME_PARA_LENGTH_MAX + 1];
   size_t  attribute_len;
   unsigned int  section;
   char  charset[ENC_MIME_PARA_LENGTH_MAX + 1];
   const char*  value_start;
   const char*  value_end;
};


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


/* Unicode codepoint inserted for rejected control characters */
#define ENC_RC  0xFFFDL  /* U+FFFD */

/*
 * Size of Unicode decomposition buffer
 * Minimum size: 8
 */
#define ENC_UC_DECOMPOSITION_BUFSIZE  (size_t) 16

/* Maximum size of header line */
#define ENC_HDR_BUFSIZE  (size_t) 998

/* Buffer size for "Format" parameter of MIME "Content-Type" header field */
#define ENC_FMT_BUFLEN  (size_t) 7

/* Unicode canonical decomposition data */
#include "../uc_cdc.c"

/* Unicode hangul syllable data */
#include "../uc_hst.c"

/* Unicode NFC quick check data */
#include "../uc_qc_nfc.c"

/* Unicode full composition exclusion data */
#include "../uc_fce.c"

/* Unicode default case folding data */
#include "../uc_cf.c"

/* MIME base64 encoding table */
static const char  enc_base64[64] =
{
   'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
   'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
   'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
   'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
   'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
   'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
   'w', 'x', 'y', 'z', '0', '1', '2', '3',
   '4', '5', '6', '7', '8', '9', '+', '/'
};


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

/*! Ignored value that was assigned to silence compiler warning */
static volatile int  ign;


/* ========================================================================== */
/* Decode hexadecimal nibble from ASCII to integer
 *
 * \param[in] nibble  ASCII encoded haxadecimal nibble to decode
 *
 * \return
 * - Integer value of \e nibble
 * - Negative value on error
 */

static int  enc_hex_decode_nibble(char  nibble)
{
   int  res = -1;
   int  n = nibble;

   if(0x30 <= n && 0x39 >= n)  { res = n - 0x30; }
   else if(0x41 <= n && 0x46 >= n)  { res = n - 0x41 + 10; }
   else if(0x61 <= n && 0x66 >= n)  { res = n - 0x61 + 10; }
   else  { PRINT_ERROR("Can't decode invalid hexadecimal nibble"); }
   /* printf("Hex nibble %c => %d\n", nibble, res); */

   return(res);
}


/* ========================================================================== */
/* Convert from supported 8 bit character sets to UTF-8
 *
 * \param[in] charset  8 bit character set used for string \e s
 * \param[in] s        String to convert
 *
 * \return
 * - Pointer to result (if not equal to \e s , a new memory block was allocated)
 * - NULL on error
 */

static const char*  enc_8bit_convert_to_utf8(enum enc_mime_cs  charset,
                                             const char*  s)
{
   const char*  res = NULL;
   size_t  inlen = 0;
   unsigned char us_ascii = 1;

   /*
    * Check whether data contains only of US-ASCII characters
    * (all supported character sets are US-ASCII extensions)
    */
   while (s[inlen])
   {
      if ((const unsigned char) 0x80 & (const unsigned char) s[inlen])
      {
         us_ascii = 0;
      }
      ++inlen;
   }
   if (us_ascii)
   {
       res = s;
   }
   else
   {
      char*  inbuf = posix_malloc(inlen + (size_t) 1);  /* +1 for NUL */

      if (NULL != inbuf)
      {
         size_t  outlen = inlen * (size_t) 4;  /* 4 octets per CP in UTF-8 */
         size_t  len = outlen;
         const char*  cs_name = NULL;
         char*  outbuf = posix_malloc(outlen + (size_t) 1);  /* +1 for NUL */

         if (NULL != outbuf)
         {
            switch(charset)
            {
               case ENC_CS_ASCII:         { cs_name = "US-ASCII";      break; }
               case ENC_CS_ISO8859_1:     { cs_name = "ISO-8859-1";    break; }
               case ENC_CS_ISO8859_2:     { cs_name = "ISO-8859-2";    break; }
               case ENC_CS_ISO8859_3:     { cs_name = "ISO-8859-3";    break; }
               case ENC_CS_ISO8859_4:     { cs_name = "ISO-8859-4";    break; }
               case ENC_CS_ISO8859_5:     { cs_name = "ISO-8859-5";    break; }
               case ENC_CS_ISO8859_6:     { cs_name = "ISO-8859-6";    break; }
               case ENC_CS_ISO8859_7:     { cs_name = "ISO-8859-7";    break; }
               case ENC_CS_ISO8859_8:     { cs_name = "ISO-8859-8";    break; }
               case ENC_CS_ISO8859_9:     { cs_name = "ISO-8859-9";    break; }
               case ENC_CS_ISO8859_10:    { cs_name = "ISO-8859-10";   break; }
               case ENC_CS_ISO8859_11:    { cs_name = "ISO-8859-11";   break; }
               case ENC_CS_ISO8859_13:    { cs_name = "ISO-8859-13";   break; }
               case ENC_CS_ISO8859_14:    { cs_name = "ISO-8859-14";   break; }
               case ENC_CS_ISO8859_15:    { cs_name = "ISO-8859-15";   break; }
               case ENC_CS_ISO8859_16:    { cs_name = "ISO-8859-16";   break; }
               case ENC_CS_MACINTOSH:     { cs_name = "Macintosh";     break; }
               case ENC_CS_KOI8R:         { cs_name = "KOI8-R";        break; }
               case ENC_CS_KOI8U:         { cs_name = "KOI8-U";        break; }
               case ENC_CS_WINDOWS_1250:  { cs_name = "Windows-1250";  break; }
               case ENC_CS_WINDOWS_1251:  { cs_name = "Windows-1251";  break; }
               case ENC_CS_WINDOWS_1252:  { cs_name = "Windows-1252";  break; }
               case ENC_CS_WINDOWS_1253:  { cs_name = "Windows-1253";  break; }
               case ENC_CS_WINDOWS_1254:  { cs_name = "Windows-1254";  break; }
               case ENC_CS_WINDOWS_1255:  { cs_name = "Windows-1255";  break; }
               case ENC_CS_WINDOWS_1256:  { cs_name = "Windows-1256";  break; }
               case ENC_CS_WINDOWS_1257:  { cs_name = "Windows-1257";  break; }
               case ENC_CS_WINDOWS_1258:  { cs_name = "Windows-1258";  break; }
               case ENC_CS_IBM437:        { cs_name = "IBM437";        break; }
               case ENC_CS_IBM775:        { cs_name = "IBM775";        break; }
               case ENC_CS_IBM850:        { cs_name = "IBM850";        break; }
               case ENC_CS_IBM852:        { cs_name = "IBM852";        break; }
               case ENC_CS_IBM858:        { cs_name = "IBM00858";      break; }
               default:                   {                            break; }
            }
            if (NULL != cs_name)
            {
               size_t rv = (size_t) -1;

               memcpy(inbuf, s, inlen + (size_t) 1);
               rv = ssic0_iconvstr("UTF-8", cs_name,
                                   inbuf, &inlen, outbuf, &outlen,
                                   SSIC0_ICONV_REPLACE_INVALID);
               if ((size_t) -1 == rv || (size_t) 0 != inlen)
               {
                  /* Failed */
                  PRINT_ERROR("Conversion from 8-bit codepage to UTF-8 failed");
                  posix_free((void*) outbuf);
               }
               else
               {
                  /* Success => Shrink output buffer to size of result */
                  len -= outlen;
                  outbuf[len] = 0;
                  res = posix_realloc((void*) outbuf, len + (size_t) 1);
                  if (NULL == res)  { posix_free((void*) outbuf); }
               }
            }
         }
         posix_free((void*) inbuf);
      }
   }

   return res;
}


/* ========================================================================== */
/*! \brief Verify CESU-8 or UTF-8 encoding
 *
 * \param[in] s    String to verify
 * \param[in] utf  Reject surrogate codepoints if nonzero.
 *
 * \note CESU-8 is defined in Unicode Technical Report #26.
 *
 * \attention
 * Read chapter 10 of RFC 3629 for UTF-8 security considerations.
 *
 * According to RFC 3629 the following rules are applied:
 * - Character code points beyond 0x10FFFF are invalid => We reject them.
 * - Only the shortest possible code sequence is allowed => We verify this.
 * - Surrogate character code points are invalid for UTF-8 => We reject them.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

static int  enc_uc_check_cesu8(const char*  s, unsigned char  utf)
{
   int  res = 0;
   size_t  i = 0;
   int  c;
   int  multibyte = 0;
   size_t  len = 0;
   size_t  remaining = 0;
   unsigned long int  mbc = 0;

   /* Assignment in truth expression is intended */
   while((c = (int) s[i++]))
   {
      /* Verify singlebyte character */
      if(!multibyte)
      {
         if(!(0 <= c && 127 >= c))  { multibyte = 1; }
      }
      /* Verify multibyte character */
      if(multibyte)
      {
         if(!remaining)
         {
            if((c & 0xE0) == 0xC0)  { len = 2; }
            else if((c & 0xF0) == 0xE0)  { len = 3; }
            else if((c & 0xF8) == 0xF0)  { len = 4; }
            else
            {
               PRINT_ERROR("Invalid start of code sequence in UTF-8 data");
               res = -1;
               break;
            }
            switch(len)
            {
               case 2:  mbc |= (unsigned long int) (c & 0x1F) << 6;  break;
               case 3:  mbc |= (unsigned long int) (c & 0x0F) << 12;  break;
               case 4:  mbc |= (unsigned long int) (c & 0x07) << 18;  break;
            }
            remaining = len - (size_t) 1;
         }
         else
         {
            if((c & 0xC0) != 0x80)
            {
               PRINT_ERROR("Invalid continuation character in UTF-8 sequence");
               res = -1;
               break;
            }
            else
            {
               --remaining;
               mbc |= (unsigned long int) (c & 0x3F) << remaining * (size_t) 6;
            }
            if(!remaining)
            {
               /* Verify character code */
               switch(len)
               {
                  case 2:
                  {
                     if(0x000080UL > mbc)
                     {
                        PRINT_ERROR("Invalid UTF-8 2-byte code sequence");
                        res = -1;
                     }
                     break;
                  }
                  case 3:
                  {
                     if(0x000800UL > mbc
                        || (utf && 0x00D800UL <= mbc && 0x00DFFFUL >= mbc))
                     {
                        PRINT_ERROR("Invalid UTF-8 3-byte code sequence");
                        res = -1;
                     }
                     break;
                  }
                  case 4:
                  {
                     if(0x010000UL > mbc || 0x10FFFFUL < mbc)
                     {
                        PRINT_ERROR("Invalid UTF-8 4-byte code sequence");
                        res = -1;
                     }
                     break;
                  }
                  default:
                  {
                     PRINT_ERROR("Bug in UTF-8 verify state machine");
                     res = -1;
                     break;
                  }
               }
               if(res)  { break; }
               /* Code sequence completely checked => Reset state machine */
               multibyte = 0;
               remaining = 0;
               mbc = 0;
            }
         }
      }
   }
   /* Check for incomplete multibyte code sequence at end of string */
   if(multibyte)  { res = -1; }

   return(res);
}


/* ========================================================================== */
/* Decode next Unicode codepoint from UTF-8 string
 *
 * \param[in]     s  UTF-8 string to decode
 * \param[in,out] i  Pointer to current index in string
 *
 * \attention
 * The string \e s MUST be already checked for valid UTF-8 encoding before
 * calling this function!
 *
 * On success, the index of the next codepoint is written to the location
 * pointed to by \e i .
 *
 * \return
 * - Next Unicode codepoint on success
 * - -1 on error
 */

static long int  enc_uc_decode_utf8(const char*  s, size_t*  i)
{
   long int  res = -1L;
   int  c;
   int  multibyte = 0;
   size_t  len = 0;
   size_t  remaining = 0;
   unsigned long int  mbc = 0;
   int  error = 0;

   /* Assignment in truth expression is intended */
   while((c = (int) s[(*i)++]))
   {
      /* Check for singlebyte codepoint */
      if(!multibyte)
      {
         if(0 <= c && 127 >= c)  { res = (long int) c;  break; }
         else  { multibyte = 1; }
      }
      /* Decode multibyte codepoint */
      if(multibyte)
      {
         if(!remaining)
         {
            if((c & 0xE0) == 0xC0)  { len = 2; }
            else if((c & 0xF0) == 0xE0)  { len = 3; }
            else if((c & 0xF8) == 0xF0)  { len = 4; }
            switch(len)
            {
               case 2:  mbc |= (unsigned long int) (c & 0x1F) << 6;  break;
               case 3:  mbc |= (unsigned long int) (c & 0x0F) << 12;  break;
               case 4:  mbc |= (unsigned long int) (c & 0x07) << 18;  break;
               default:
               {
                  PRINT_ERROR("UTF-8 decoder called with invalid data");
                  error = 1;
                  break;
               }
            }
            if(error)  { res = -1L;  break; }
            remaining = len - (size_t) 1;
         }
         else
         {
            --remaining;
            mbc |= (unsigned long int) (c & 0x3F) << remaining * (size_t) 6;
            if(!remaining)
            {
               /* Codepoint decoding complete */
               res = (long int) mbc;
               break;
            }
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Encode Unicode codepoints to UTF-8
 *
 * \param[out]     buf   Encoded UTF-8 string
 * \param[in,out]  i     Current index in \e buf
 * \param[in]      dbuf  Codepoint buffer
 * \param[in,out]  di    Number of codepoints in \e dbuf
 *
 * \attention
 * The target buffer \e buf must be large enough for the encoded data. This must
 * be ensured by the caller using worst case calculations.
 *
 * On success, the start index of the next codepoint is written to the location
 * pointed to by \e i and zero is written to the location pointed to by \e di .
 */

/* 'static' removed because test program must call this function */
void  enc_uc_encode_utf8(char*  buf, size_t*  i, long int*  dbuf, size_t*  di)
{
   size_t  ii;
   int  inval = 0;
   unsigned char  prefix;
   unsigned char  data;

   for(ii = 0; ii < *di; ++ii)
   {
      if (0L > dbuf[ii]) { inval = 1; }
      else if(0x00007FL >= dbuf[ii])  { buf[(*i)++] = (char) dbuf[ii]; }
      else if(0x0007FFL >= dbuf[ii])
      {
         data = (unsigned char) ((dbuf[ii] >> 6) & 0x1FL);
         prefix = 0xC0;
         buf[(*i)++] = (char) (prefix | data);
         data = (unsigned char) (dbuf[ii] & 0x3FL);
         prefix = 0x80;
         buf[(*i)++] = (char) (prefix | data);
      }
      else if(0x00FFFFL >= dbuf[ii])
      {
         data = (unsigned char) ((dbuf[ii] >> 12) & 0x0FL);
         prefix = 0xE0;
         buf[(*i)++] = (char) (prefix | data);
         data = (unsigned char) ((dbuf[ii] >> 6) & 0x3FL);
         prefix = 0x80;
         buf[(*i)++] = (char) (prefix | data);
         data = (unsigned char) (dbuf[ii] & 0x3FL);
         prefix = 0x80;
         buf[(*i)++] = (char) (prefix | data);
      }
      else if(0x10FFFFL >= dbuf[ii])
      {
         data = (unsigned char) ((dbuf[ii] >> 18) & 0x07L);
         prefix = 0xF0;
         buf[(*i)++] = (char) (prefix | data);
         data = (unsigned char) ((dbuf[ii] >> 12) & 0x3FL);
         prefix = 0x80;
         buf[(*i)++] = (char) (prefix | data);
         data = (unsigned char) ((dbuf[ii] >> 6) & 0x3FL);
         prefix = 0x80;
         buf[(*i)++] = (char) (prefix | data);
         data = (unsigned char) (dbuf[ii] & 0x3FL);
         prefix = 0x80;
         buf[(*i)++] = (char) (prefix | data);
      }
      else  { inval = 1; }
      if(inval)
      {
         PRINT_ERROR("Unicode UTF-8 encoder: Invalid codepoint detected");
         buf[(*i)++] = '?';
         inval = 0;
      }
   }
   *di = 0;
}


/* ========================================================================== */
/* Convert from UTF-16BE to UTF-8
 *
 * \param[in] mb   Multibyte input data to convert (in UTF-16BE format)
 * \param[in] len  Length in bytes
 *
 * If \e len is an odd number, the last byte of input data \e mb is ignored.
 *
 * \attention
 * The result string is only guaranteed to have valid UTF-8 encoding. It
 * nevertheless may be semantically invalid and is not normalized!
 *
 * \return
 * - Pointer to result string in UTF-8 format (a new memory block was allocated)
 * - NULL on error
 */

static const char*  enc_uc_convert_utf16be_to_utf8(const char*  mb, size_t  len)
{
   const char*  res = NULL;
   char*  buf = NULL;
   size_t  bi = 0;
   size_t  i;
   long int  ucp;  /* Unicode codepoint */
   int  surrogate = 0;  /* Flag indicating that trail surrogate is expected */
   long int  lead_surrogate = 0;
   size_t  di;  /* Number of codepoints for UTF-8 encoder */

   /* Ensure even byte count */
   if((size_t) 1 & len)  { --len; }
   if(len)
   {
      /* Allocate buffer */
      buf = (char*) posix_malloc(len * (size_t) 2 + (size_t) 1);
      if(NULL != buf)
      {
         /* Process 16 bit words */
         for(i = 0; i < len; i += 2)
         {
            /* Decode UTF-16 using big endian byte order */
            ucp = (long int) (unsigned char) mb[i] << 8
                  | (long int) (unsigned char) mb[i + (size_t) 1];
            /* Decode surrogate pair and extract codepoint */
            if(0x00D800L <= ucp && 0x00DFFFL >= ucp)
            {
               if(!surrogate
                  && (0x00D800L <= ucp && 0x00DBFFL >= ucp))
               {
                  lead_surrogate = ucp - 0x00D800L;
                  surrogate = 1;
                  continue;
               }
               else if(surrogate
                       && (0x00DC00L <= ucp && 0x00DFFFL >= ucp))
               {
                  ucp = (lead_surrogate << 10L) | (ucp - 0x00DC00L);
                  ucp += 0x010000L;
                  surrogate = 0;
               }
               else
               {
                  PRINT_ERROR("Missing UTF-16 lead surrogate");
                  ucp = (long int) (unsigned char) '?';
                  surrogate = 0;
               }
            }
            if(surrogate && !(0x00DC00L <= ucp && 0x00DFFFL >= ucp))
            {
               PRINT_ERROR("Invalid UTF-16 trail surrogate");
               ucp = (long int) (unsigned char) '?';
            }
            surrogate = 0;
            /* Encode codepoint as UTF-8 into target buffer */
            di = 1;
            enc_uc_encode_utf8(buf, &bi, &ucp, &di);
         }
      }
   }

   /* Check for error */
   if(!bi)
   {
      PRINT_ERROR("Unicode UTF-16 to UTF-8 converter failed");
      if(NULL != buf)  { posix_free((void*) buf); }
   }
   else
   {
      /* Terminate result string on success */
      buf[bi] = 0;
      res = buf;
   }

   return(res);
}


/* ========================================================================== */
/* Check whether Unicode codepoint is an unwanted control character
 *
 * \param[in] ucp   Unicode codepoint to check
 *
 * RFC 5198 forbids C1 control characters => We reject them.
 *
 * Unicode variation selectors and control characters for bidirectional text
 * should be accepted. The Cocoa backend of FLTK (for Apple macOS) is able to
 * process them.
 *
 * \return
 * - 0 if nothing was found
 * - -1 if unwanted control characters are present
 */

static int  enc_uc_check_control(long int  ucp)
{
   int  res = 0;

   /* Check for ASCII C0 control characters plus DEL */
   if(0x1FL >= ucp || 0x7FL == ucp)
   {
      /* Accept only HT, LF and CR (required for canonical format) */
      if(0x09L != ucp && 0x0AL != ucp && 0x0DL != ucp)  { res = -1; }
   }
   /* Reject ISO 8859 C1 control characters */
   else if(0x80L <= ucp && 0x9FL >= ucp)  { res = -1; }
   /* Reject Unicode INTERLINEAR ANNOTATION special characters */
   else if(0xFFF9L <= ucp && 0xFFFBL >= ucp)  { res = -1; }
   /* Reject Unicode LINE SEPARATOR and PARAGRAPH SEPARATOR */
   else if(0x2028L <= ucp && 0x2029L >= ucp)  { res = -1; }
   /* Reject Unicode LANGUAGE TAG */
   else if( 0xE0001L == ucp)  { res = -1; }
#if 0
   /*
    * Reject LANGUAGE TAG associated range
    *
    * Note:
    * This range was deprecated since Unicode 5.1, but was reintroduced for
    * another purpose in Unicode 9.0 => No longer reject it.
    */
   else if(0xE0020L <= ucp && 0xE007FL >= ucp)  { res = -1; }
#endif

   return(res);
}


/* ========================================================================== */
/* Lookup canonical decomposition and combining class of Unicode codepoint
 *
 * \param[in]  ucp   Unicode codepoint to lookup
 * \param[out] res   Result
 */

static void  enc_uc_lookup_cdc(long int  ucp, struct uc_cdc*  res)
{
   size_t  i = 0;

   res->cp = ucp;
   res->ccc = 0;
   res->dc1 = -1L;
   res->dc2 = -1L;
   /* ASCII codepoints are always starters without canonical decomposition */
   if(128L <= ucp)
   {
      /* Lookup codepoint in Unicode database */
      while(-1L != uc_cdc_table[i].cp)
      {
         if(ucp == uc_cdc_table[i].cp)
         {
            /* Found nonstarter or canonical decomposable */
            res->ccc = uc_cdc_table[i].ccc;
            res->dc1 = uc_cdc_table[i].dc1;
            res->dc2 = uc_cdc_table[i].dc2;
            break;
         }
         ++i;
      }
   }
   /* Use codepoint as decomposition if no canonical decomposition was found */
   if(-1L == res->dc1)  { res->dc1 = ucp;  res->dc2 = -1L; }
}


/* ========================================================================== */
/* Lookup canonical composition of codepoint pair
 *
 * \param[in]  starter  Unicode codepoint of starter
 * \param[in]  cm       Unicode codepoint of combination mark (or other starter)
 *
 * \note
 * If \e starter is not a codepoint with canoncial combining class zero, the
 * function returns error.
 *
 * \return
 * - Codepoint of composition character on success
 * - -1 on error
 */

static long int  enc_uc_lookup_cc(long int  starter, long int  cm)
{
   long int  res = -1;
   struct uc_cdc  cdc;  /* Canonical decomposition data */
   size_t  i;
   size_t  ii;
   long int  first;
   long int  last;

   /* Only used for hangul algorithmic canonical composition */
   const long int  SBase = 0xAC00L;
   const long int  LBase = 0x1100L;
   const long int  VBase = 0x1161L;
   const long int  TBase = 0x11A7L;
   const long int  NCount = 588L;
   const long int  LCount = 19L;
   const long int  VCount = 21L;
   const long int  TCount = 28L;
   int  jamo = 0;  /* Number of jamo in syllable */
   enum uc_hs_type  hst;
   enum uc_hs_type  hst2;
   long int  LIndex;
   long int  VIndex;
   long int  TIndex;

   /* Check whether starter really is a starter */
   enc_uc_lookup_cdc(starter, &cdc);
   if(!cdc.ccc)
   {
      /* Yes => Lookup decomposition in Unicode database */
      i = 0;
      while(-1L != uc_cdc_table[i].cp)
      {
         if(uc_cdc_table[i].dc1 == starter && uc_cdc_table[i].dc2 == cm)
         {
            /* Found composition */
            res = uc_cdc_table[i].cp;
            /* Check for composition exception */
            ii = 0;
            while(-1L != uc_fce_table[ii].first)
            {
               first = uc_fce_table[ii].first;
               last = uc_fce_table[ii].last;
               if(first <= res && last >= res)
               {
                  /* Composition exception found */
#if ENC_UC_NORM_DEBUG
                  printf("   Canonical composition exception\n");
#endif  /* ENC_UC_NORM_DEBUG */
                  res = -1;
                  break;
               }
               ++ii;
            }
            break;
         }
         ++i;
      }

      /*
       * On error, check whether algorithmic composition is possible using
       * Unicode hangul syllable type database
       */
      enc_uc_lookup_cdc(cm, &cdc);
      if(!cdc.ccc)
      {
         i = 0;
         while(-1L == res && -1L != uc_hst_table[i].first)
         {
            first = uc_hst_table[i].first;
            last = uc_hst_table[i].last;
            hst = uc_hst_table[i].hst;
            if(first <= starter && last >= starter)
            {
               if(UC_HST_L == hst || UC_HST_LV == hst)
               {
                  /* Starter is a hangul L-type consonant or LV-type syllable */
                  ii = 0;
                  while(-1L != uc_hst_table[ii].first)
                  {
                     first = uc_hst_table[ii].first;
                     last = uc_hst_table[ii].last;
                     hst2 = uc_hst_table[ii].hst;
                     if(first <= cm && last >= cm)
                     {
                        if(UC_HST_L == hst && UC_HST_V == hst2)
                        {
                           if(LBase <= starter && VBase <= cm)
                           {
                              LIndex = starter - LBase;
                              if(LIndex < LCount)
                              {
                                 VIndex = cm - VBase;
                                 if(VIndex < VCount)
                                 {
#if ENC_UC_NORM_DEBUG
                                    printf("   Canonical composition"
                                           " for hangul LV-syllable found\n");
                                    printf("Hangul LIndex: %ld\n", LIndex);
                                    printf("Hangul VIndex: %ld\n", VIndex);
#endif  /* ENC_UC_NORM_DEBUG */
                                    jamo = 2;
                                    res = SBase
                                          + LIndex * NCount + VIndex * TCount;
                                 }
                              }
                           }
                        }
                        else if(UC_HST_LV == hst && UC_HST_T == hst2)
                        {
                           if(TBase <= cm)
                           {
                              TIndex = cm - TBase;
                              if(TIndex < TCount)
                              {
#if ENC_UC_NORM_DEBUG
                                 printf("   Canonical composition"
                                        " for hangul LVT-syllable found\n");
                                 printf("Hangul TIndex: %ld\n", TIndex);
#endif  /* ENC_UC_NORM_DEBUG */
                                 jamo = 3;
                                 res = starter + TIndex;
                              }
                           }
                        }
                        if(jamo)
                        {
                           if(-1L == res)
                           {
                              PRINT_ERROR("Unicode algorithmic composition"
                                          " for hangul syllable failed");
                           }
#if 1
                           /* Optional: Check hangul syllable type of result */
                           else
                           {
                              ii = 0;
                              while(-1L != uc_hst_table[ii].first)
                              {
                                 first = uc_hst_table[ii].first;
                                 last = uc_hst_table[ii].last;
                                 hst = uc_hst_table[ii].hst;
                                 if(first <= res && last >= res)
                                 {
                                    if(2 == jamo && UC_HST_LV != hst)
                                    {
                                       /* Result should be a LV-syllable! */
                                       res = -1L;
                                    }
                                    if(3 == jamo && UC_HST_LVT != hst)
                                    {
                                       /* Result should be a LVT-syllable! */
                                       res = -1L;
                                    }
                                    break;
                                 }
                                 ++ii;
                              }
                              if(-1L == res)
                              {
                                 PRINT_ERROR("Invalid Unicode hangul syllable"
                                             " detected (Bug)");
                              }
                           }
#endif
                           break;
                        }
                     }
                     ++ii;
                  }
               }
            }
            ++i;
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/* Lookup mapping for default case folding
 *
 * \param[in]  ucp  Unicode codepoint
 * \param[in]  cfm  Case folded mapping (up to 3 codepoints)
 *
 * If the case fold mapping is smaller than 3 codepoints, -1 is written to the
 * unused fields.
 *
 * \note
 * If \e ucp has no mapping for default case folding, \e ucp itself is
 * returned in the first field.
 */

static void  enc_uc_lookup_cf(long int  ucp, long int  mapping[3])
{
   size_t  i = 0;
   int  found = 0;

   /* Lookup codepoint in Unicode database */
   while(-1L != uc_cf_table[i].cp)
   {
      if(ucp == uc_cf_table[i].cp)
      {
         /* Found mapping for Unicode default case folding */
         mapping[0] = uc_cf_table[i].first;
         mapping[1] = uc_cf_table[i].second;
         mapping[2] = uc_cf_table[i].third;
         found = 1;
         break;
      }
      ++i;
   }
   if(!found)
   {
      mapping[0] = ucp;
      mapping[1] = -1L;
      mapping[2] = -1L;
   }
}


/* ========================================================================== */
/* Get number of Unicode glyphs from UTF-8 string
 *
 * \param[in] s    UTF-8 string to decode
 * \param[in] end  Check glyph count up to (but not including) this index
 *
 * To check all glyphs of a string, set \e end to zero.
 *
 * \attention
 * The string \e s MUST be already checked for valid UTF-8 encoding before
 * calling this function!
 *
 * \note
 * Soft hyphens (SHY) are not counted, except directly before \e end .
 * <br>
 * For this function the glyph count is defined as "number of starters".
 * For complex scripts this may not match the display width.
 *
 * \return
 * - Glyph count
 */

static size_t  enc_uc_get_glyph_count(const char*  s, size_t  end)
{
   size_t  res = 0;
   size_t  i = 0;
   long int  ucp;
   struct uc_cdc  cdc;  /* Canonical decomposition data */

   while(1)
   {
      ucp = enc_uc_decode_utf8(s, &i);
      if(-1L == ucp)  { break; }
      else
      {
         /* Do not count SHY characters, except at the end */
         if (!((0x00ADL == ucp) && (end && i < end)))
         {
            /* Check whether codepoint is a starter */
            enc_uc_lookup_cdc(ucp, &cdc);
            if(!cdc.ccc)  { ++res; }
         }
      }
      if(end && i >= end)  { break; }
   }

   return(res);
}


/* ========================================================================== */
/* Quick check for Unicode NFC normalization
 *
 * This function verify that:
 * - all codepoints are allowed in NFC
 * - the canonical ordering of combining marks is correct
 *
 * \param[in] s  UTF-8 string to check
 *
 * \attention
 * The string \e s MUST be already checked for valid UTF-8 encoding before
 * calling this function!
 */

static int  enc_uc_check_nfc(const char*  s)
{
   int  res = 0;
   size_t  i = 0;
   long int  ucp = 0;
   long int  first;
   long int  last;
   size_t  ii;
   struct uc_cdc  cdc;  /* Canonical decomposition data */
   unsigned char  ccc_last = 0;

   while(1)
   {
      ucp = enc_uc_decode_utf8(s, &i);
      if(-1L == ucp)  { break; }
      /* Quick check for ASCII */
      if(128L <= ucp)
      {
         /* Lookup codepoint in Unicode database */
         ii = 0;
         while(-1L != uc_qc_nfc_table[ii].first)
         {
            first = uc_qc_nfc_table[ii].first;
            last = uc_qc_nfc_table[ii].last;
            if(first <= ucp && last >= ucp)
            {
               /* Codepoint is (maybe) not allowed in NFC */
               res = -1;
               break;
            }
            ++ii;
         }
         if(res)  { break; }
         /* Check ordering of combining marks */
         enc_uc_lookup_cdc(ucp, &cdc);
         if(cdc.ccc && (cdc.ccc < ccc_last))  { res = -1;  break; }
         ccc_last = cdc.ccc;
      }
      else  { ccc_last = 0; }
   }

#if ENC_UC_NORM_DEBUG
   if(res)
   {
      printf("Maybe not NFC: %s (len: %u)\n  ", s, (unsigned int) strlen(s));
      i = 0;  while(s[i])
      {
         printf(" 0x%02X", (unsigned int) (unsigned char) s[i++]);
      }
      printf("\n");
   }
#endif  /* ENC_UC_NORM_DEBUG */

   return(res);
}


/* ========================================================================== */
/* Unicode canonical decomposition engine
 *
 * This function is reentrant and calls itself for recursive decomposition.
 *
 * \attention
 * The decomposition buffer \e dbuf must have the fixed size
 * \ref ENC_UC_DECOMPOSITION_BUFSIZE bytes.
 *
 * \param[in]      ucp   Unicode codepoint
 * \param[in,out]  dbuf  Decomposition buffer
 * \param[in]      di    Pointer to index in decomposition buffer
 */

static int  enc_uc_engine_decompose(long int  ucp, long int*  dbuf, size_t*  di)
{
   int  res = 0;
   struct uc_cdc  cdc;  /* Canonical decomposition data */

   /* Ensure that there is space for 2 codepoints in decomposition buffer */
   if(ENC_UC_DECOMPOSITION_BUFSIZE - (size_t) 2 <= *di)
   {
      /* Decomposition buffer not large enough */
      PRINT_ERROR("Unicode canonical decomposition engine failed");
      dbuf[0] = (long int) (unsigned char) '[';
      dbuf[1] = (long int) (unsigned char) 'E';
      dbuf[2] = (long int) (unsigned char) 'r';
      dbuf[3] = (long int) (unsigned char) 'r';
      dbuf[4] = (long int) (unsigned char) 'o';
      dbuf[5] = (long int) (unsigned char) 'r';
      dbuf[6] = (long int) (unsigned char) ']';
      *di = 7;
      res = -1;
   }
   else
   {
      /* Recursively decompose */
      enc_uc_lookup_cdc(ucp, &cdc);
      if(cdc.dc1 != ucp)  { res = enc_uc_engine_decompose(cdc.dc1, dbuf, di); }
      else  { dbuf[(*di)++] = cdc.dc1; }
      if(-1L != cdc.dc2)  { dbuf[(*di)++] = cdc.dc2; }
   }

   return(res);
}


/* ========================================================================== */
/* Unicode canonical ordering engine
 *
 * All bursts of codepoints with nonzero canonical combining class are
 * stable sorted in ascending order.
 *
 * \param[in,out]  dbuf  Decomposition buffer
 * \param[in]      di    Number of codepoints in decomposition buffer
 */

static void  enc_uc_engine_reorder(long int*  dbuf, size_t  di)
{
   size_t  i, ii, iii, iiii;
   struct uc_cdc  cdc;  /* Canonical decomposition data */
   size_t  len;
   long int  tmp;
   unsigned char  ccc1;
   unsigned char  ccc2;

   for(i = 0; i < di; ++i)
   {
      enc_uc_lookup_cdc(dbuf[i], &cdc);
      /* Starters (Ccc = 0) always stay in place */
      if(cdc.ccc)
      {
#if ENC_UC_NORM_DEBUG
         printf("   Nonstarter: U+%04lX (ccc=%u)\n",
                dbuf[i], (unsigned int)  cdc.ccc);
#endif  /* ENC_UC_NORM_DEBUG */
         ii = i;
         while(++ii < di)
         {
            enc_uc_lookup_cdc(dbuf[ii], &cdc);
            if(!cdc.ccc)  { break; }
         }
         len = ii - i;
         /* Sort burst of nonstarter codepoints (ccc != 0) to canonical order */
#if ENC_UC_NORM_DEBUG
         printf("   Sort burst: len=%u\n", (unsigned int)  len);
#endif  /* ENC_UC_NORM_DEBUG */
         for(iii = i; iii < i + len; ++iii)
         {
            /*
             * Bubble sort from end of buffer
             * This is very inefficient because the ccc lookup data is not
             * buffered. For european languages there are seldom more than
             * two or three codepoints combined => Keep it simple.
             */
            for(iiii = i + len - (size_t) 1; iiii > iii; --iiii)
            {
               enc_uc_lookup_cdc(dbuf[iiii - (size_t) 1], &cdc);
               ccc1 = cdc.ccc;
               enc_uc_lookup_cdc(dbuf[iiii], &cdc);
               ccc2 = cdc.ccc;
               if(ccc2 < ccc1)
               {
                  tmp = dbuf[iiii - (size_t) 1];
                  dbuf[iiii - (size_t) 1] = dbuf[iiii];
                  dbuf[iiii] = tmp;
               }
            }
         }
      }
#if ENC_UC_NORM_DEBUG
      else  { printf("   Starter   : U+%04lX\n", dbuf[i]); }
#endif  /* ENC_UC_NORM_DEBUG */
   }
}


/* ========================================================================== */
/* Unicode canonical composition engine
 *
 * \param[in,out]  dbuf  Codepoint buffer
 * \param[in]      di    Pointer to number of codepoints in buffer
 */

static void  enc_uc_engine_compose(long int*  dbuf, size_t*  di)
{
   size_t  i = 0;
   size_t  ii;
   long int  ucp;  /* Unicode codepoint */
   struct uc_cdc  cdc;  /* Canonical decomposition data */
   unsigned char  ccc;
   int  skip;

   while(++i < *di)
   {
      /* Check whether codepoint i can be canonically composed with starter */
#if ENC_UC_NORM_DEBUG
      printf("   ---\n");
      printf("   Starter at beginning : U+%04lX\n", dbuf[0]);
      printf("   Codepoint in question: U+%04lX\n", dbuf[i]);
#endif  /* ENC_UC_NORM_DEBUG */
      ucp = enc_uc_lookup_cc(dbuf[0], dbuf[i]);
      if(-1L != ucp)
      {
         /* Yes => Get canonical combining class */
         enc_uc_lookup_cdc(dbuf[i], &cdc);
         ccc = cdc.ccc;
#if ENC_UC_NORM_DEBUG
         printf("   Codepoint has ccc    : %u\n", (unsigned int) ccc);
         printf("   Canonical composition: U+%04lX\n", ucp);
#endif  /* ENC_UC_NORM_DEBUG */
         /* Search for other codepoints with same canonical combining class */
         skip = 0;
         for(ii = 1; ii < i; ++ii)
         {
            enc_uc_lookup_cdc(dbuf[ii], &cdc);
            if(cdc.ccc >= ccc)
            {
               /* Found => Preserve canonical ordering => Don't compose */
#if ENC_UC_NORM_DEBUG
               printf("   => Don't compose\n");
#endif  /* ENC_UC_NORM_DEBUG */
               skip = 1;
               break;
            }
         }
         if(skip)  { continue; }
         /* Not found => Compose */
#if ENC_UC_NORM_DEBUG
         printf("   => Compose\n");
#endif  /* ENC_UC_NORM_DEBUG */
         for(ii = i; ii < *di - (size_t) 1; ++ii)
         {
            dbuf[ii] = dbuf[ii + (size_t) 1];
         }
         dbuf[--*di] = -1L;
         dbuf[0] = ucp;
         /* Rewind index for now missing codepoint */
         --i;
      }
#if ENC_UC_NORM_DEBUG
      printf("   ---\n");
#endif  /* ENC_UC_NORM_DEBUG */
   }
}


/* ========================================================================== */
/* Unicode normalization engine (shared part)
 *
 * \param[in]  s    Valid Unicode string with arbitrary or no normalization
 * \param[out] l    Pointer to length of result
 * \param[in]  nfc  Normalization form (NFC if nonzero, otherwise NFD)
 *
 * \attention
 * The string \e s MUST be already checked for valid UTF-8 encoding before
 * calling this function!
 *
 * \return
 * - Pointer to processed Unicode data
 *   A new memory block was allocated
 * - NULL on error (Original memory block for \e s is still allocated)
 */

static const char*  enc_uc_engine_n(const char*  s, size_t*  l, int  nfc)
{
   char*  res = NULL;
   size_t  rlen = 0;
   size_t  ri = 0;
   char*  p;
   size_t  i = 0;
   size_t  last;
   long int  ucp;  /* Unicode codepoint */
   struct uc_cdc  cdc;  /* Canonical decomposition data */
   long int  dbuf[ENC_UC_DECOMPOSITION_BUFSIZE];
   size_t  di = 0;
   int  error = 0;  /* Error flag => Skip to next starter */

   while(1)
   {
      /* Allocate memory in exponentially increasing chunks */
      if(rlen - ri <= (size_t) 4 * ENC_UC_DECOMPOSITION_BUFSIZE)
      {
         /*
          * Ensure there is space in the result buffer for at least 4 times the
          * decompositon buffer size. Reason: Every Unicode codepoint can
          * consume up to 4 bytes after encoded to UTF-8 in worst case.
          */
         if(!rlen)  { rlen = (size_t) 4 * ENC_UC_DECOMPOSITION_BUFSIZE; }
         rlen *= (size_t) 2;
         p = posix_realloc((void*) res, rlen);
         if(NULL == p)  { posix_free((void*) res);  res = NULL;  break; }
         else  { res = p; }
      }
      /* Check whether next codepoint is a starter (ccc = 0) */
      last = i;
      ucp = enc_uc_decode_utf8(s, &i);
      if(-1L == ucp)  { break; }
      enc_uc_lookup_cdc(ucp, &cdc);
      if(!cdc.ccc)
      {
         /* Yes => Check for buffered sequence */
         if(di)
         {
            /* Present => Push last codepoint back and flush buffer first */
            i = last;
            enc_uc_engine_reorder(dbuf, di);
            if(nfc)  { enc_uc_engine_compose(dbuf, &di); }
            enc_uc_encode_utf8(res, &ri, dbuf, &di);
            error = 0;
            continue;
         }
      }
      /* Recursive canonical decomposition */
      if(!error)  { error = enc_uc_engine_decompose(ucp, dbuf, &di); }
   }
   /* Flush buffer */
   if(di)
   {
      enc_uc_engine_reorder(dbuf, di);
      if(nfc)  { enc_uc_engine_compose(dbuf, &di); }
      enc_uc_encode_utf8(res, &ri, dbuf, &di);
   }

   /* Terminate result string */
   if(NULL != res)
   {
      res[ri] = 0;
      *l = ri;
   }

   return(res);
}


/* ========================================================================== */
/* Unicode NFD normalization engine
 *
 * \param[in]  s  Valid Unicode string with arbitrary or no normalization
 * \param[out] l  Pointer to length of result
 *
 * \attention
 * The string \e s MUST be already checked for valid UTF-8 encoding before
 * calling this function!
 *
 * \return
 * - Pointer to decomposed Unicode data (UTF-8 encoded with NFD normalization)
 *   A new memory block was allocated
 * - NULL on error (Original memory block for \e s is still allocated)
 */

static const char*  enc_uc_engine_nfd(const char*  s, size_t*  l)
{
   return(enc_uc_engine_n(s, l, 0));
}


/* ========================================================================== */
/* Unicode NFC normalization engine part 1
 *
 * \param[in]  s  Valid Unicode string with arbitrary or no normalization
 * \param[out] l  Pointer to length of result
 *
 * \attention
 * The string \e s MUST be already checked for valid UTF-8 encoding before
 * calling this function!
 *
 * Part 1 does all the work but cannot compose starters, therefore the result
 * may contain starter pairs with canonical composition and must be
 * postprocessed by part 2 of the engine.
 *
 * \return
 * - Pointer to processed data (input for part 2 of the engine)
 *   A new memory block was allocated
 * - NULL on error (Original memory block for \e s is still allocated)
 */

static const char*  enc_uc_engine_nfc_part1(const char*  s, size_t*  l)
{
   return(enc_uc_engine_n(s, l, 1));
}


/* ========================================================================== */
/* Unicode NFC normalization engine part 2
 *
 * \param[in]  s     Unicode string processed by part 1 of the engine
 * \param[in]  l     Length of \e s
 * \param[out] flag  Flag indicating modified data
 *
 * Part 2 is for canonical composition of codepoint pairs that are both
 * starters. This includes algorithmic canonical composition for hangul
 * syllables.
 *
 * \return
 * - Pointer to precomposed Unicode data (UTF-8 encoded with NFC normalization)
 *   A new memory block was allocated
 * - NULL on error (Undefined data written to \e flag)
 */

static const char*  enc_uc_engine_nfc_part2(const char*  s, size_t  l,
                                            int*  flag)
{
   char*  res = NULL;
   size_t  ri = 0;
   size_t  i = 0;
   long int  ucp;  /* Unicode codepoint */
   struct uc_cdc  cdc;  /* Canonical decomposition data */
   long int  dbuf[2];
   size_t  di = 0;

   *flag = 0;
#if ENC_UC_NORM_DEBUG
   printf("   *** Part 2 ***\n");
#endif  /* ENC_UC_NORM_DEBUG */
   res = posix_malloc(++l);
   if(NULL != res)
   {
      while(1)
      {
         /* Append next codepoint to buffer */
         ucp = enc_uc_decode_utf8(s, &i);
         if(-1L == ucp)  { break; }
         dbuf[di++] = ucp;
         /* Check whether codepoint is a starter (ccc = 0) */
         enc_uc_lookup_cdc(ucp, &cdc);
         if(cdc.ccc)
         {
            /* No => Flush buffer */
            enc_uc_encode_utf8(res, &ri, dbuf, &di);
         }
         else
         {
            /* Yes => Check for canonical composition of starter pair */
            if((size_t) 2 == di)
            {
               enc_uc_engine_compose(dbuf, &di);
               /* Flush first starter if there was no canonical composition */
               if((size_t) 2 == di)
               {
                  di = 1;
                  enc_uc_encode_utf8(res, &ri, dbuf, &di);
                  dbuf[0] = ucp;
                  di = 1;
               }
               else
               {
                  /* Canonical composition found for starter pair */
                  *flag = 1;
               }
            }
         }
      }
   }
   /* Flush buffer */
   if(di)  { enc_uc_encode_utf8(res, &ri, dbuf, &di); }
   /* Terminate result string */
   if(NULL != res)  { res[ri] = 0; }

#if ENC_UC_NORM_DEBUG
   if(NULL != res)
   {
      printf("Now NFC: %s (len: %u)\n  ", res, (unsigned int) strlen(res));
      i = 0;  while(res[i])
      {
         printf(" 0x%02X", (unsigned int) (unsigned char) res[i++]);
      }
      printf("\n\n");
   }
#endif  /* ENC_UC_NORM_DEBUG */

   return(res);
}


/* ========================================================================== */
/* Strip defective combining character sequences (at the beginning of string)
 *
 * \param[in] s  UTF-8 string to process
 *
 * \attention
 * The string \e s MUST be already checked for valid UTF-8 encoding before
 * calling this function!
 *
 * This function strips defective combining character sequences at the
 * beginning so that the result becomes semantically valid when standing
 * alone.
 *
 * \return
 * - Pointer to processed Unicode data
 *   If the result is not equal to \e s , a new memory block was allocated
 * - NULL on error
 */

static const char*  enc_uc_strip_dccs(const char*  s)
{
   const char*  res = NULL;
   int  skip = 0;  /* Garbage at the beginning of string must be skipped */
   long int  ucp;  /* Unicode codepoint */
   struct uc_cdc  cdc;  /* Canonical decomposition data */
   size_t  i = 0;
   size_t  last;
   size_t  len;

   while(1)
   {
      last = i;
      ucp = enc_uc_decode_utf8(s, &i);
      if(-1L == ucp)  { break; }
      enc_uc_lookup_cdc(ucp, &cdc);
      if(!cdc.ccc)  { break; }
      else
      {
         /* The Unicode data tries to compose something with void */
         if(!skip)  { PRINT_ERROR("Semantic error in Unicode string"); }
         skip = 1;
      }
   }
   i = last;
   if(skip)
   {
      len = strlen(&s[i]);
      res = (const char*) posix_malloc(++len);
      if(NULL != res)  { memcpy((void*) res, &s[i], len); }
   }
   else  { res = s; }

   return(res);
}


/* ========================================================================== */
/* Normalize UTF-8 string to NFD
 *
 * Documentation about Unicode normalization:
 * <br>
 * http://www.unicode.org/reports/tr15/
 *
 * \param[in] s  UTF-8 string to normalize
 *
 * \attention
 * The string \e s MUST be already checked for valid UTF-8 encoding before
 * calling this function!
 *
 * \note
 * An Unicode string with valid transformation encoding can still be
 * semantically nonsense. If it starts with a codepoint that is not a "starter"
 * in terms of the standard, the resulting string starts with a "defective
 * combining character sequence" - but may still make sense if concatenated
 * with other data in front of it.
 * This function always strips defective combining character sequences at the
 * beginning so that the result becomes semantically valid even when standing
 * alone.
 *
 * \return
 * - Pointer to decomposed Unicode data (UTF-8 encoded with NFD normalization)
 *   If the result is not equal to \e s , a new memory block was allocated
 * - NULL on error
 */

static const char*  enc_uc_normalize_to_nfd(const char*  s)
{
   const char*  res = NULL;
   const char*  tgt;
   size_t  l = 0;

   /* Strip all nonstarters at the beginning */
   res = enc_uc_strip_dccs(s);
   if(NULL != res)
   {
      /* Normalize string to NFD */
      tgt = enc_uc_engine_nfd(res, &l);
      if(res != s)  { posix_free((void*) res); }
      res = tgt;
   }

   /* Check for error */
   if(NULL == res)  { PRINT_ERROR("Unicode NFD normalization failed"); }

   return(res);
}


/* ========================================================================== */
/* Normalize UTF-8 string to NFC
 *
 * Documentation about Unicode normalization:
 * <br>
 * http://www.unicode.org/reports/tr15/
 *
 * \param[in] s  UTF-8 string to normalize
 *
 * RFC 5198 recommends NFC for use in general Internet text messages
 * => We do so.
 *
 * \attention
 * The string \e s MUST be already checked for valid UTF-8 encoding before
 * calling this function!
 *
 * \note
 * An Unicode string with valid transformation encoding can still be
 * semantically nonsense. If it starts with a codepoint that is not a "starter"
 * in terms of the standard, the resulting string starts with a "defective
 * combining character sequence" - but may still make sense if concatenated
 * with other data in front of it.
 * This function always strips defective combining character sequences at the
 * beginning so that the result becomes semantically valid even when standing
 * alone.
 *
 * \return
 * - Pointer to precomposed Unicode data (UTF-8 encoded with NFC normalization)
 *   If the result is not equal to \e s , a new memory block was allocated
 * - NULL on error
 */

static const char*  enc_uc_normalize_to_nfc(const char*  s)
{
   const char*  res = NULL;
   const char*  tgt;
   size_t  l = 0;
   int  flag;

   /* Strip all nonstarters at the beginning */
   res = enc_uc_strip_dccs(s);

   /* Quick check whether the string is already in NFC */
   if(NULL != res && enc_uc_check_nfc(res))
   {
      /* No => Normalize string to NFC */
      tgt = enc_uc_engine_nfc_part1(res, &l);
      if(res != s)  { posix_free((void*) res); }
      res = tgt;
      if(NULL != res)
      {
         /* Fixme: This is ugly and very slow => Should be single pass */
         tgt = enc_uc_engine_nfc_part2(res, l, &flag);
         posix_free((void*) res);
         res = tgt;
         if(NULL != res && flag)
         {
            /* Part 1 must be repeated if starters were composed */
            tgt = enc_uc_engine_nfc_part1(res, &l);
            posix_free((void*) res);
            res = tgt;
         }
      }
   }

   /* Check for error */
   if(NULL == res)  { PRINT_ERROR("Unicode NFC normalization failed"); }

   return(res);
}


/* ========================================================================== */
/* Convert from ISO-2022-JP encoding to UTF-8
 *
 * \param[in] s  String to convert
 *
 * \return
 * - Pointer to result (if not equal to \e s , a new memory block was allocated)
 * - NULL on error
 */

static const char*  enc_iso2022jp_convert_to_utf8(const char*  s)
{
   const char*  res = NULL;
   size_t  inlen = 0;
   unsigned char us_ascii = 1;

   /*
    * Check whether data contains no switch from US-ASCII character set
    * (ISO-2022-JP is an US-ASCII extension)
    */
   while (s[inlen])
   {
      if ( ((const unsigned char) 0x80 &  (const unsigned char) s[inlen]) ||
           ((const unsigned char) 0x1B == (const unsigned char) s[inlen]) )
      {
         us_ascii = 0;
      }
      ++inlen;
   }
   if (us_ascii)
   {
       res = s;
   }
   else
   {
      char*  inbuf = posix_malloc(inlen + (size_t) 1);  /* +1 for NUL */

      if (NULL != inbuf)
      {
         size_t  outlen = inlen * (size_t) 4;  /* 4 octets per CP in UTF-8 */
         size_t  len = outlen;
         char*  outbuf = posix_malloc(outlen + (size_t) 1);  /* +1 for NUL */

         if (NULL != outbuf)
         {
            size_t rv = (size_t) -1;

            memcpy(inbuf, s, inlen + (size_t) 1);
            rv = jpic0_iconvstr("UTF-8", "ISO-2022-JP",
                                inbuf, &inlen, outbuf, &outlen,
                                JPIC0_ICONV_REPLACE_INVALID);
            if ((size_t) -1 == rv || (size_t) 0 != inlen)
            {
               /* Failed */
               PRINT_ERROR("Conversion from ISO-2022-JP to UTF-8 failed");
               posix_free((void*) outbuf);
            }
            else
            {
               /* Success => Shrink output buffer to size of result */
               len -= outlen;
               outbuf[len] = 0;
               res = posix_realloc((void*) outbuf, len + (size_t) 1);
               if (NULL == res)  { posix_free((void*) outbuf); }
            }
         }
         posix_free((void*) inbuf);
      }
   }

   return res;
}


/* ========================================================================== */
/* Decode IANA character set description
 *
 * \param[in] s    Description to decode
 * \param[in] len  Length of string \e s
 *
 * This function checks whether the description \e s represents a supported IANA
 * character set name and return the corresponding ID for it.
 * According to RFC 2047 the character set is treated case-insensitive.
 *
 * \result
 * - MIME character set ID (from \ref enc_mime_cs )
 * - \c ENC_CS_UNKNOWN on error
 */

static enum enc_mime_cs  enc_mime_get_charset(const char*  s, size_t  len)
{
   enum enc_mime_cs  res = ENC_CS_UNKNOWN;
   char  buf[ENC_CS_BUFLEN];
   size_t  i;
   const char  not_supported[] = "MIME: Unsupported character set: ";
   char*  p;
   size_t  l;
   int  isoinv = 0;
   int  macinv = 0;
   int  ibminv = 0;

   if(ENC_CS_BUFLEN <= len)
   {
      /* If you get this error, the value of 'ENC_CS_BUFLEN' is too small */
      PRINT_ERROR("MIME: Name of character set too long");
   }
   else
   {
      /* Convert description to upper case */
      for(i = 0; i < len; ++i)
      {
         buf[i] = (char) toupper((int) s[i]);
      }
      buf[len] = 0;
      /* Check for all known character sets */
      if(!strcmp(buf, "US-ASCII"))  { res = ENC_CS_ASCII; }
      else if(!strcmp(buf, "UTF-8"))  { res = ENC_CS_UTF_8; }
      else if(!strcmp(buf, "CESU-8"))  { res = ENC_CS_CESU_8; }
      else if(!strcmp(buf, "UTF-7"))  { res = ENC_CS_UTF_7; }
      else if(!strcmp(buf, "ISO-8859-1"))  { res = ENC_CS_ISO8859_1; }
      else if(!strcmp(buf, "ISO-8859-2"))  { res = ENC_CS_ISO8859_2; }
      else if(!strcmp(buf, "ISO-8859-3"))  { res = ENC_CS_ISO8859_3; }
      else if(!strcmp(buf, "ISO-8859-4"))  { res = ENC_CS_ISO8859_4; }
      else if(!strcmp(buf, "ISO-8859-5"))  { res = ENC_CS_ISO8859_5; }
      else if(!strcmp(buf, "ISO-8859-6"))  { res = ENC_CS_ISO8859_6; }
      else if(!strcmp(buf, "ISO-8859-7"))  { res = ENC_CS_ISO8859_7; }
      else if(!strcmp(buf, "ISO-8859-8"))  { res = ENC_CS_ISO8859_8; }
      else if(!strcmp(buf, "ISO-8859-9"))  { res = ENC_CS_ISO8859_9; }
      else if(!strcmp(buf, "ISO-8859-10"))  { res = ENC_CS_ISO8859_10; }
      else if(!strcmp(buf, "TIS-620"))  { res = ENC_CS_ISO8859_11; }
      /* Note: The proposed draft for ISO 8859-12 was released as ISO 8859-14 */
      else if(!strcmp(buf, "ISO-8859-13"))  { res = ENC_CS_ISO8859_13; }
      else if(!strcmp(buf, "ISO-8859-14"))  { res = ENC_CS_ISO8859_14; }
      else if(!strcmp(buf, "ISO-8859-15"))  { res = ENC_CS_ISO8859_15; }
      else if(!strcmp(buf, "ISO-8859-16"))  { res = ENC_CS_ISO8859_16; }
      else if(!strcmp(buf, "WINDOWS-1250"))  { res = ENC_CS_WINDOWS_1250; }
      else if(!strcmp(buf, "WINDOWS-1251"))  { res = ENC_CS_WINDOWS_1251; }
      else if(!strcmp(buf, "WINDOWS-1252"))  { res = ENC_CS_WINDOWS_1252; }
      else if(!strcmp(buf, "WINDOWS-1253"))  { res = ENC_CS_WINDOWS_1253; }
      else if(!strcmp(buf, "WINDOWS-1254"))  { res = ENC_CS_WINDOWS_1254; }
      else if(!strcmp(buf, "WINDOWS-1255"))  { res = ENC_CS_WINDOWS_1255; }
      else if(!strcmp(buf, "WINDOWS-1256"))  { res = ENC_CS_WINDOWS_1256; }
      else if(!strcmp(buf, "WINDOWS-1257"))  { res = ENC_CS_WINDOWS_1257; }
      else if(!strcmp(buf, "WINDOWS-1258"))  { res = ENC_CS_WINDOWS_1258; }
      else if(!strcmp(buf, "KOI8-R"))  { res = ENC_CS_KOI8R; }
      else if(!strcmp(buf, "KOI8-U"))  { res = ENC_CS_KOI8U; }
      else if(!strcmp(buf, "MACINTOSH"))  { res = ENC_CS_MACINTOSH; }
      else if(!strcmp(buf, "IBM437"))  { res = ENC_CS_IBM437; }
      else if(!strcmp(buf, "IBM775"))  { res = ENC_CS_IBM775; }
      else if(!strcmp(buf, "IBM850"))  { res = ENC_CS_IBM850; }
      else if(!strcmp(buf, "IBM852"))  { res = ENC_CS_IBM852; }
      else if(!strcmp(buf, "IBM00858"))  { res = ENC_CS_IBM858; }
      else if(!strcmp(buf, "ISO-2022-JP"))  { res = ENC_CS_ISO2022_JP; }

      /* Check for official IANA aliases */
      /* US-ASCII */
      else if(!strcmp(buf, "ANSI_X3.4-1968"))  { res = ENC_CS_ASCII; }
      else if(!strcmp(buf, "ANSI_X3.4-1986"))  { res = ENC_CS_ASCII; }
      else if(!strcmp(buf, "ISO-IR-6"))  { res = ENC_CS_ASCII; }
      else if(!strcmp(buf, "ISO_646.IRV:1991"))  { res = ENC_CS_ASCII; }
      else if(!strcmp(buf, "ISO646-US"))  { res = ENC_CS_ASCII; }
      else if(!strcmp(buf, "IBM367"))  { res = ENC_CS_ASCII; }
      else if(!strcmp(buf, "CP367"))  { res = ENC_CS_ASCII; }
      else if(!strcmp(buf, "CSASCII"))  { res = ENC_CS_ASCII; }
      else if(!strcmp(buf, "US"))  { res = ENC_CS_ASCII; }
      /* UTF-8 */
      else if(!strcmp(buf, "CSUTF-8"))  { res = ENC_CS_UTF_8; }
      /* CESU-8 */
      else if(!strcmp(buf, "CSCESU8"))  { res = ENC_CS_CESU_8; }
      else if(!strcmp(buf, "CSCESU-8"))  { res = ENC_CS_CESU_8; }
      /* UTF-7 */
      else if(!strcmp(buf, "CSUTF-7"))  { res = ENC_CS_UTF_7; }

      /* Check for official IANA aliases of ISO 8859 parts */
      /* ISO 8859-1 */
      else if(!strcmp(buf, "ISO_8859-1:1987"))  { res = ENC_CS_ISO8859_1; }
      else if(!strcmp(buf, "ISO_8859-1"))  { res = ENC_CS_ISO8859_1; }
      else if(!strcmp(buf, "ISO-IR-100"))  { res = ENC_CS_ISO8859_1; }
      else if(!strcmp(buf, "IBM819"))  { res = ENC_CS_ISO8859_1; }
      else if(!strcmp(buf, "CP819"))  { res = ENC_CS_ISO8859_1; }
      else if(!strcmp(buf, "CSISOLATIN1"))  { res = ENC_CS_ISO8859_1; }
      else if(!strcmp(buf, "LATIN1"))  { res = ENC_CS_ISO8859_1; }
      else if(!strcmp(buf, "L1"))  { res = ENC_CS_ISO8859_1; }
      /* ISO 8859-2 */
      else if(!strcmp(buf, "ISO_8859-2:1987"))  { res = ENC_CS_ISO8859_2; }
      else if(!strcmp(buf, "ISO_8859-2"))  { res = ENC_CS_ISO8859_2; }
      else if(!strcmp(buf, "ISO-IR-101"))  { res = ENC_CS_ISO8859_2; }
      else if(!strcmp(buf, "CSISOLATIN2"))  { res = ENC_CS_ISO8859_2; }
      else if(!strcmp(buf, "LATIN2"))  { res = ENC_CS_ISO8859_2; }
      else if(!strcmp(buf, "L2"))  { res = ENC_CS_ISO8859_2; }
      /* ISO 8859-3 */
      else if(!strcmp(buf, "ISO_8859-3:1988"))  { res = ENC_CS_ISO8859_3; }
      else if(!strcmp(buf, "ISO_8859-3"))  { res = ENC_CS_ISO8859_3; }
      else if(!strcmp(buf, "ISO-IR-109"))  { res = ENC_CS_ISO8859_3; }
      else if(!strcmp(buf, "CSISOLATIN3"))  { res = ENC_CS_ISO8859_3; }
      else if(!strcmp(buf, "LATIN3"))  { res = ENC_CS_ISO8859_3; }
      else if(!strcmp(buf, "L3"))  { res = ENC_CS_ISO8859_3; }
      /* ISO 8859-4 */
      else if(!strcmp(buf, "ISO_8859-4:1988"))  { res = ENC_CS_ISO8859_4; }
      else if(!strcmp(buf, "ISO_8859-4"))  { res = ENC_CS_ISO8859_4; }
      else if(!strcmp(buf, "ISO-IR-110"))  { res = ENC_CS_ISO8859_4; }
      else if(!strcmp(buf, "CSISOLATIN4"))  { res = ENC_CS_ISO8859_4; }
      else if(!strcmp(buf, "LATIN4"))  { res = ENC_CS_ISO8859_4; }
      else if(!strcmp(buf, "L4"))  { res = ENC_CS_ISO8859_4; }
      /* ISO 8859-5 */
      else if(!strcmp(buf, "ISO_8859-5:1988"))  { res = ENC_CS_ISO8859_5; }
      else if(!strcmp(buf, "ISO_8859-5"))  { res = ENC_CS_ISO8859_5; }
      else if(!strcmp(buf, "ISO-IR-144"))  { res = ENC_CS_ISO8859_5; }
      else if(!strcmp(buf, "CSISOLATINCYRILLIC"))  { res = ENC_CS_ISO8859_5; }
      else if(!strcmp(buf, "CYRILLIC"))  { res = ENC_CS_ISO8859_5; }
      /* ISO 8859-6 */
      else if(!strcmp(buf, "ISO_8859-6:1987"))  { res = ENC_CS_ISO8859_6; }
      else if(!strcmp(buf, "ISO_8859-6"))  { res = ENC_CS_ISO8859_6; }
      else if(!strcmp(buf, "ISO-IR-127"))  { res = ENC_CS_ISO8859_6; }
      else if(!strcmp(buf, "ECMA-114"))  { res = ENC_CS_ISO8859_6; }
      else if(!strcmp(buf, "ASMO-708"))  { res = ENC_CS_ISO8859_6; }
      else if(!strcmp(buf, "CSISOLATINARABIC"))  { res = ENC_CS_ISO8859_6; }
      else if(!strcmp(buf, "ARABIC"))  { res = ENC_CS_ISO8859_6; }
      /* ISO 8859-7 */
      else if(!strcmp(buf, "ISO_8859-7:1987"))  { res = ENC_CS_ISO8859_7; }
      else if(!strcmp(buf, "ISO_8859-7"))  { res = ENC_CS_ISO8859_7; }
      else if(!strcmp(buf, "ISO-IR-126"))  { res = ENC_CS_ISO8859_7; }
      else if(!strcmp(buf, "ECMA-118"))  { res = ENC_CS_ISO8859_7; }
      else if(!strcmp(buf, "ELOT_928"))  { res = ENC_CS_ISO8859_7; }
      else if(!strcmp(buf, "CSISOLATINGREEK"))  { res = ENC_CS_ISO8859_7; }
      else if(!strcmp(buf, "GREEK8"))  { res = ENC_CS_ISO8859_7; }
      else if(!strcmp(buf, "GREEK"))  { res = ENC_CS_ISO8859_7; }
      /* ISO 8859-8 */
      else if(!strcmp(buf, "ISO_8859-8:1988"))  { res = ENC_CS_ISO8859_8; }
      else if(!strcmp(buf, "ISO_8859-8"))  { res = ENC_CS_ISO8859_8; }
      else if(!strcmp(buf, "CSISOLATIN8"))  { res = ENC_CS_ISO8859_8; }
      else if(!strcmp(buf, "LATIN8"))  { res = ENC_CS_ISO8859_8; }
      else if(!strcmp(buf, "L8"))  { res = ENC_CS_ISO8859_8; }
      else if(!strcmp(buf, "ISO-IR-138"))  { res = ENC_CS_ISO8859_8; }
      else if(!strcmp(buf, "HEBREW"))  { res = ENC_CS_ISO8859_8; }
      else if(!strcmp(buf, "CSISOLATINHEBREW"))  { res = ENC_CS_ISO8859_8; }
      /* ISO 8859-9 */
      else if(!strcmp(buf, "ISO_8859-9:1989"))  { res = ENC_CS_ISO8859_9; }
      else if(!strcmp(buf, "ISO_8859-9"))  { res = ENC_CS_ISO8859_9; }
      else if(!strcmp(buf, "ISO-IR-148"))  { res = ENC_CS_ISO8859_9; }
      else if(!strcmp(buf, "CSISOLATIN5"))  { res = ENC_CS_ISO8859_9; }
      else if(!strcmp(buf, "LATIN5"))  { res = ENC_CS_ISO8859_9; }
      else if(!strcmp(buf, "L5"))  { res = ENC_CS_ISO8859_9; }
      /* ISO 8859-10 */
      else if(!strcmp(buf, "ISO_8859-10:1992"))  { res = ENC_CS_ISO8859_10; }
      else if(!strcmp(buf, "ISO-IR-157"))  { res = ENC_CS_ISO8859_10; }
      else if(!strcmp(buf, "CSISOLATIN6"))  { res = ENC_CS_ISO8859_10; }
      else if(!strcmp(buf, "LATIN6"))  { res = ENC_CS_ISO8859_10; }
      else if(!strcmp(buf, "L6"))  { res = ENC_CS_ISO8859_10; }
      /* ISO 8859-11 */
      else if(!strcmp(buf, "ISO_8859-11"))  { res = ENC_CS_ISO8859_11; }
      else if(!strcmp(buf, "CSTIS620"))  { res = ENC_CS_ISO8859_11; }
      /* ISO 8859-13 */
      else if(!strcmp(buf, "CSISO885913"))  { res = ENC_CS_ISO8859_13; }
      /* ISO 8859-14 */
      else if(!strcmp(buf, "ISO_8859-14:1998"))  { res = ENC_CS_ISO8859_14; }
      else if(!strcmp(buf, "ISO_8859-14"))  { res = ENC_CS_ISO8859_14; }
      else if(!strcmp(buf, "ISO-IR-199"))  { res = ENC_CS_ISO8859_14; }
      else if(!strcmp(buf, "CSISO885914"))  { res = ENC_CS_ISO8859_14; }
      else if(!strcmp(buf, "ISO-CELTIC"))  { res = ENC_CS_ISO8859_14; }
      else if(!strcmp(buf, "LATIN8"))  { res = ENC_CS_ISO8859_14; }
      else if(!strcmp(buf, "L8"))  { res = ENC_CS_ISO8859_14; }
      /* ISO 8859-15 */
      else if(!strcmp(buf, "ISO_8859-15"))  { res = ENC_CS_ISO8859_15; }
      else if(!strcmp(buf, "CSISO885915"))  { res = ENC_CS_ISO8859_15; }
      else if(!strcmp(buf, "LATIN9"))  { res = ENC_CS_ISO8859_15; }
      /* ISO 8859-16 */
      else if(!strcmp(buf, "ISO_8859-16:2001"))  { res = ENC_CS_ISO8859_16; }
      else if(!strcmp(buf, "ISO_8859-16"))  { res = ENC_CS_ISO8859_16; }
      else if(!strcmp(buf, "ISO-IR-226"))  { res = ENC_CS_ISO8859_16; }
      else if(!strcmp(buf, "CSISO885916"))  { res = ENC_CS_ISO8859_16; }
      else if(!strcmp(buf, "LATIN10"))  { res = ENC_CS_ISO8859_16; }
      else if(!strcmp(buf, "L10"))  { res = ENC_CS_ISO8859_16; }

      /* Check for official IANA aliases of Windows codepages */
      /* Windows-1250 */
      else if(!strcmp(buf, "CSWINDOWS1250"))  { res = ENC_CS_WINDOWS_1250; }
      /* Windows-1251 */
      else if(!strcmp(buf, "CSWINDOWS1251"))  { res = ENC_CS_WINDOWS_1251; }
      /* Windows-1252 */
      else if(!strcmp(buf, "CSWINDOWS1252"))  { res = ENC_CS_WINDOWS_1252; }
      /* Windows-1253 */
      else if(!strcmp(buf, "CSWINDOWS1253"))  { res = ENC_CS_WINDOWS_1253; }
      /* Windows-1254 */
      else if(!strcmp(buf, "CSWINDOWS1254"))  { res = ENC_CS_WINDOWS_1254; }
      /* Windows-1255 */
      else if(!strcmp(buf, "CSWINDOWS1255"))  { res = ENC_CS_WINDOWS_1255; }
      /* Windows-1256 */
      else if(!strcmp(buf, "CSWINDOWS1256"))  { res = ENC_CS_WINDOWS_1256; }
      /* Windows-1257 */
      else if(!strcmp(buf, "CSWINDOWS1257"))  { res = ENC_CS_WINDOWS_1257; }
      /* Windows-1258 */
      else if(!strcmp(buf, "CSWINDOWS1258"))  { res = ENC_CS_WINDOWS_1258; }

      /* Check for official IANA aliases of KOI8 codepages */
      else if(!strcmp(buf, "CSKOI8R"))  { res = ENC_CS_KOI8R; }
      else if(!strcmp(buf, "CSKOI8U"))  { res = ENC_CS_KOI8U; }

      /* Check for official IANA aliases of Macintosh */
      else if(!strcmp(buf, "CSMACINTOSH"))  { res = ENC_CS_MACINTOSH; }
      else if(!strcmp(buf, "MAC"))  { res = ENC_CS_MACINTOSH; }

      /* Check for official IANA aliases of IBM codepages */
      /* IBM437 */
      else if(!strcmp(buf, "CSPC8CODEPAGE437"))  { res = ENC_CS_IBM437; }
      else if(!strcmp(buf, "CP437"))  { res = ENC_CS_IBM437; }
      else if(!strcmp(buf, "437"))  { res = ENC_CS_IBM437; }
      /* IBM775 */
      else if(!strcmp(buf, "CSPC775BALTIC"))  { res = ENC_CS_IBM775; }
      else if(!strcmp(buf, "CP775"))  { res = ENC_CS_IBM775; }
      /* IBM850 */
      else if(!strcmp(buf, "CSPC850MULTILINGUAL"))  { res = ENC_CS_IBM850; }
      else if(!strcmp(buf, "CP850"))  { res = ENC_CS_IBM850; }
      else if(!strcmp(buf, "850"))  { res = ENC_CS_IBM850; }
      /* IBM852 */
      else if(!strcmp(buf, "CSPCP852"))  { res = ENC_CS_IBM852; }
      else if(!strcmp(buf, "CP852"))  { res = ENC_CS_IBM852; }
      else if(!strcmp(buf, "852"))  { res = ENC_CS_IBM852; }
      /* IBM00858 */
      else if(!strcmp(buf, "PC-MULTILINGUAL-850+EURO"))
           { res = ENC_CS_IBM858; }
      else if(!strcmp(buf, "CSIBM00858"))  { res = ENC_CS_IBM858; }
      else if(!strcmp(buf, "CCSID00858"))  { res = ENC_CS_IBM858; }
      else if(!strcmp(buf, "CP00858"))  { res = ENC_CS_IBM858; }
      /* ISO 2022-JP */
      else if(!strcmp(buf, "CSISO2022JP"))  { res = ENC_CS_ISO2022_JP; }

      /* -------------------------------------------------------------------- */
      /* To be more tolerant: Check again for invalid ISO 8859 declarations */
      else if(!strcmp(buf, "ISO8859-1"))
           { isoinv = 1;  res = ENC_CS_ISO8859_1; }
      else if(!strcmp(buf, "ISO8859-2"))
           { isoinv = 1;  res = ENC_CS_ISO8859_2; }
      else if(!strcmp(buf, "ISO8859-3"))
           { isoinv = 1;  res = ENC_CS_ISO8859_3; }
      else if(!strcmp(buf, "ISO8859-4"))
           { isoinv = 1;  res = ENC_CS_ISO8859_4; }
      else if(!strcmp(buf, "ISO8859-5"))
           { isoinv = 1;  res = ENC_CS_ISO8859_5; }
      else if(!strcmp(buf, "ISO8859-6"))
           { isoinv = 1;  res = ENC_CS_ISO8859_6; }
      else if(!strcmp(buf, "ISO8859-7"))
           { isoinv = 1;  res = ENC_CS_ISO8859_7; }
      else if(!strcmp(buf, "ISO8859-8"))
           { isoinv = 1;  res = ENC_CS_ISO8859_8; }
      else if(!strcmp(buf, "ISO8859-9"))
           { isoinv = 1;  res = ENC_CS_ISO8859_9; }
      else if(!strcmp(buf, "ISO8859-10"))
           { isoinv = 1;  res = ENC_CS_ISO8859_10; }
      else if(!strcmp(buf, "ISO-8859-11"))
           { isoinv = 1;  res = ENC_CS_ISO8859_11; }
      else if(!strcmp(buf, "ISO8859-11"))
           { isoinv = 1;  res = ENC_CS_ISO8859_11; }
      else if(!strcmp(buf, "ISO8859-13"))
          { isoinv = 1;  res = ENC_CS_ISO8859_13; }
      else if(!strcmp(buf, "ISO8859-14"))
           { isoinv = 1;  res = ENC_CS_ISO8859_14; }
      else if(!strcmp(buf, "ISO8859-15"))
           { isoinv = 1;  res = ENC_CS_ISO8859_15; }
      else if(!strcmp(buf, "ISO8859-16"))
           { isoinv = 1;  res = ENC_CS_ISO8859_16; }

      /* To be more tolerant: Check again for invalid MACINTOSH declarations */
      else if(!strcmp(buf, "MAC"))  { macinv = 1;  res = ENC_CS_MACINTOSH; }
      else if(!strcmp(buf, "MACROMAN"))
           { macinv = 1;  res = ENC_CS_MACINTOSH; }
      else if(!strcmp(buf, "X-MAC-ROMAN"))
           { macinv = 1;  res = ENC_CS_MACINTOSH; }

      /* To be more tolerant: Check again for invalid IBM declarations */
      else if(!strcmp(buf, "CP-437"))  { ibminv = 1;  res = ENC_CS_IBM437; }
      else if(!strcmp(buf, "IBM858"))  { ibminv = 1;  res = ENC_CS_IBM858; }
      else if(!strcmp(buf, "CP858"))  { ibminv = 1;  res = ENC_CS_IBM858; }
      else if(!strcmp(buf, "CP1250"))
           { ibminv = 1;  res = ENC_CS_WINDOWS_1250; }
      else if(!strcmp(buf, "CP1251"))
           { ibminv = 1;  res = ENC_CS_WINDOWS_1251; }
      else if(!strcmp(buf, "CP1252"))
           { ibminv = 1;  res = ENC_CS_WINDOWS_1252; }
      else if(!strcmp(buf, "CP1253"))
           { ibminv = 1;  res = ENC_CS_WINDOWS_1253; }
      else if(!strcmp(buf, "CP1254"))
           { ibminv = 1;  res = ENC_CS_WINDOWS_1254; }
      else if(!strcmp(buf, "CP1255"))
           { ibminv = 1;  res = ENC_CS_WINDOWS_1255; }
      else if(!strcmp(buf, "CP1256"))
           { ibminv = 1;  res = ENC_CS_WINDOWS_1256; }
      else if(!strcmp(buf, "CP1257"))
           { ibminv = 1;  res = ENC_CS_WINDOWS_1257; }
      else if(!strcmp(buf, "CP1258"))
           { ibminv = 1;  res = ENC_CS_WINDOWS_1258; }

      /* To be more tolerant: Check again for invalid UTF declarations */
      else if(!strcmp(buf, "UTF7"))
      {
         PRINT_ERROR("MIME: Invalid character set UTF7 accepted as UTF-7");
         res = ENC_CS_UTF_7;
      }
      else if(!strcmp(buf, "UTF8"))
      {
         PRINT_ERROR("MIME: Invalid character set UTF8 accepted as UTF-8");
         res = ENC_CS_UTF_8;
      }
      /* -------------------------------------------------------------------- */

      /* Check whether character set is supported */
      if(ENC_CS_UNKNOWN == res)
      {
         l = strlen(MAIN_ERR_PREFIX) + strlen(not_supported) + len;
         p = (char*) posix_malloc(++l);
         if(NULL != p)
         {
            /* No => Character set not supported */
            strcpy(p, MAIN_ERR_PREFIX);
            strcat(p, not_supported);
            strncat(p, buf, len);
            print_error(p);
            posix_free((void*) p);
         }
         /* Special check for ISO 8859-x character sets that aren't supported */
         buf[8] = 0;
         if(!strcmp(buf, "ISO-8859"))  { res = ENC_CS_ISO8859_X; }
         /* To be more tolerant: Check again for invalid ISO 8859 declaration */
         if(!strcmp(buf, "ISO8859"))  { isoinv = 1;  res = ENC_CS_ISO8859_X; }
      }
      if(isoinv)
      {
         PRINT_ERROR("MIME: Invalid ISO 8859 character set accepted");
      }
      else if(macinv)
      {
         PRINT_ERROR("MIME: Invalid Macintosh character set accepted");
      }
      else if(ibminv)
      {
         PRINT_ERROR("MIME: Invalid IBM codepage accepted");
      }
   }

   return(res);
}


/* ========================================================================== */
/* Decode MIME quoted printable data
 *
 * \param[in]  start  Pointer to start of data
 * \param[in]  end    Pointer to end of data
 * \param[in]  ec     Flag to switch between normal and encoded-word syntax
 * \param[out] dlen   Pointer to length of decoded data
 *
 * \e end must never be smaller than \e start and must point to the location
 * after the last character.
 *
 * \attention
 * If \e ec is true, the syntax is switched to that for encoded-words according
 * to RFC 2047.
 *
 * According to RFC 2045 the following rules are applied:
 * - Whitespace at end of lines must be ignored => We do so.
 * - A robust decoder may exclude invalid input data and continue
 *   => We do so by decoding all invalid characters as '?'.
 * - The hexadecimal representation of a character must use upper case letters
 *   A decoder is allowed to accept lower case letters too => We do so.
 * - If an invalid sequence follows a '=' character, it is allowed to accept
 *   this data as plain ASCII => We do so.
 * - Lines are not allowed to be longer than 76 characters, but it is allowed
 *   that a decoder accept lines of arbitrary length => We do so.
 *
 * \note
 * A NUL termination is always appended to the decoded data (but not calculated
 * for \e dlen ). This means that if the decoded data is text, the result buffer
 * can be directly used as C string.
 *
 * \result
 * - Pointer to new memory block containing the decoded data
 * - NULL on error (Value at location \e dlen is not valid)
 */

static char*  enc_mime_decode_qp(const char*  start, const char*  end,
                                 int  ec, size_t*  dlen)
{
   char*  res = NULL;
   size_t  len;
   size_t  bi = 0;
   size_t  i;
   char*  src = NULL;
   size_t  ws = POSIX_SIZE_MAX;
   char*  tmp = NULL;
   char*  p;
   char  current;
   unsigned char  c;
   int  state = 0;
   char  nibble_high = 0;
   int  v;
   int  invalid;

   /* Delete whitespace at end of lines */
   len = (size_t) (end - start);
   p = posix_malloc(len + (size_t) 1);
   if(NULL == p)  { return(NULL); }
   else
   {
      src = p;
      for(i = 0; i < len; ++i)
      {
         /* Check for EOL */
         if((char) 0x0A == start[i] && i)
         {
            if((char) 0x0D == start[i - (size_t) 1] && POSIX_SIZE_MAX != ws)
            {
               /* Seek back to remove whitespace */
               bi = ws;
               src[bi++] = 0x0D;
            }
         }
         /* Check for whitespace */
         if((char) 0x09 == start[i] || (char) 0x20 == start[i])
         {
            if(POSIX_SIZE_MAX == ws)  { ws = bi; }
         }
         else if((char) 0x0D != start[i])  { ws = POSIX_SIZE_MAX; }
         src[bi++] = start[i];
      }
      /* Terminate string in source buffer */
      src[bi] = 0;
      /* Reassign start and end pointers */
      start = src;
      end = &src[bi];
   }

   /* Decode data */
   len = 0;
   bi = 0;
   for(i = 0; i < (size_t) (end - start); ++i)
   {
      /* Allocate more memory in exponentially increasing chunks */
      /* Attention: An invalid QP sequence stays undecoded and 3 octets long! */
      if(bi + (size_t) 4 >= len)  /* We need (3 + NUL) additional bytes */
      {
         if(!len)  { len = 64; }
         p = posix_realloc((void*) tmp, len *= (size_t) 2);
         if(NULL == p)
         {
            posix_free((void*) tmp);
            tmp = NULL;
            break;
         }
         else  { tmp = p; }
      }
      /* Parse current character */
      current = start[i];
      /* Only printable ASCII characters, SPACE, HT, LF and CR are allowed */
      invalid = 0;
      v = (int) current;
      if(!((9 <= v && 10 >= v) || 13 == v || (32 <= v && 126 >= v)))
      {
         invalid = 1;
      }
      /* SPACE and HT are not allowed in encoded-words */
      if(ec && !invalid && (9 == v || 32 == v))  { invalid = 1; }
      if(invalid)
      {
         /* Invalid character detected */
         PRINT_ERROR("MIME: Decoding invalid quoted printable data");
         current = '?';
      }
      /* Equal sign sequence decoder state machine */
      c = 0;
      if(!state && '=' == current)  { ++state; }
      switch(state)
      {
         case 1:
         {
            /* Skip equal sign */
            ++state;
            break;
         }
         case 2:
         {
            /* Store CR of soft line break or high nibble of encoded octet */
            nibble_high = current;
            ++state;
            break;
         }
         case 3:
         {
            /* SPACE and HT at end of line must be ignored */
            if( !ec &&
                ((char) 0x09 == nibble_high || (char) 0x20 == nibble_high) )
            {
               if((char) 0x09 == current || (char) 0x20 == current)  { break; }
               else if((char) 0x0D == current)
               {
                  nibble_high = current;
                  break;
               }
            }
            ++state;
            /* No break here is intended! */
         }
         case 4:
         {
            state = 0;
            /* Check for soft line break */
            if(!ec && (char) 0x0D == nibble_high && (char) 0x0A == current)
            {
               /* printf("Soft line break\n"); */
               break;
            }
            /* Decode octet */
            invalid = 0;
            v = enc_hex_decode_nibble(nibble_high);
            if(0 > v)  { invalid = 1; }
            else
            {
               c = (unsigned char) (v * 16);
               v = enc_hex_decode_nibble(current);
               if(0 > v)  { invalid = 1; }
               else  { c += (unsigned char) v; }
            }
            if(invalid)
            {
               /* Invalid encoding => Accept data as ASCII */
               PRINT_ERROR("MIME: Invalid quoted printable encoded data");
               tmp[bi++] = '=';
               tmp[bi++] = nibble_high;
               c = (unsigned char) current;
            }
            break;
         }
         default:
         {
            /* Decode underscore to space */
            if(ec && '_' == current)  { c = (unsigned char) 0x20; }
            else  { c = (unsigned char) current; }
            break;
         }
      }
      if(c)  { tmp[bi++] = (char) c; }
   }

   /* Terminate decoded data (for use as C string) */
   if(NULL == tmp)  { res = NULL; }
   else
   {
      tmp[bi] = 0;
      /* Report length without the NUL termination */
      *dlen = bi;
      res = tmp;
   }
   posix_free((void*) src);

   return(res);
}


/* ========================================================================== */
/* Convert MIME quoted printable encoded text to Unicode (UTF-8 NFC)
 *
 * \param[in] charset  Character set of data
 * \param[in] start    Pointer to start of data
 * \param[in] end      Pointer to end of data
 * \param[in] ec       Flag to switch between normal and encoded-word syntax
 *
 * \e end must never be smaller than \e start and must point to the location
 * after the last character.
 *
 * \attention
 * If \e ec is true, the syntax is switched to that for encoded-words according
 * to RFC 2047.
 *
 * \result
 * - Pointer to new memory block containing the decoded data
 * - NULL on error
 */

static const char*  enc_mime_decode_q(enum enc_mime_cs  charset,
                                      const char*  start, const char*  end,
                                      int  ec)
{
   const char*  res = NULL;
   size_t  len;
   char*  tmp = NULL;

   tmp = enc_mime_decode_qp(start, end, ec, &len);
   if(NULL != tmp)
   {
      /* Convert result to Unicode and normalize to NFC */
      res = enc_convert_to_utf8_nfc(charset, tmp);
      if(tmp != res)  { posix_free((void*) tmp); }
   }

   return(res);
}


/* ========================================================================== */
/* Decode MIME base64 data
 *
 * \param[in]  start  Pointer to start of data
 * \param[in]  end    Pointer to end of data
 * \param[in]  np     Flag to request no padding
 * \param[out] dlen   Pointer to length of decoded data
 *
 * \e end must never be smaller than \e start and must point to the location
 * after the last character.
 *
 * If \e np is true, the syntax is switched to that for base64 inside of UTF-7
 * according to RFC 2152.
 *
 * \note
 * A NUL termination is always appended to the decoded data (but not calculated
 * for \e dlen ). This means that if the decoded data is text, the result buffer
 * can be directly used as C string.
 *
 * \result
 * - Pointer to new memory block containing the decoded data
 * - NULL on error (Value at location \e dlen is not valid)
 */

static char*  enc_mime_decode_base64(const char*  start, const char*  end,
                                     int  np, size_t*  dlen)
{
   char*  res = NULL;
   size_t  len = 0;
   size_t  bi = 0;
   size_t  i;  /* Index in input data */
   char*  tmp = NULL;
   char*  p;
   int  valid = 0;
   int  state = 0;
   unsigned long int  ii = 0;  /* Index in base64 alphabet */
   unsigned long int  input = 0;
   int  padding = 0;
   int  abort = 0;

   for(i = 0; i < (size_t) (end - start); ++i)
   {
      /* Allocate more memory in exponentially increasing chunks */
      if(bi + (size_t) 4 >= len)  /* We need (3 + NUL) additional bytes */
      {
         if(!len)  { len = 64; }
         p = posix_realloc((void*) tmp, len *= (size_t) 2);
         if(NULL == p)
         {
            PRINT_ERROR("MIME: Out of memory while decoding base64 data");
            abort = 1;
            break;
         }
         else  { tmp = p; }
      }
      valid = 0;
      /* Check for padding */
      if('=' == start[i])
      {
         ii = 0;
         if(0 == state || 1 == state)
         {
            /* Invalid padding character detected => Abort */
            PRINT_ERROR("MIME: Invalid padding in base64 encoded data");
            abort = 1;
            break;
         }
         else  { valid = 1; }
      }
      /* Check for valid encoding */
      else
      {
         if(' ' == start[i] || 0x09 == start[i]
            || 0x0A == start[i] || 0x0D == start[i])
         {
            /* Accept and ignore whitespace and line breaks */
            continue;
         }
         for(ii = 0; ii < 64UL; ++ii)
         {
            if(enc_base64[ii] == start[i])
            {
               valid = 1;
               break;
            }
         }
      }
      if(!valid)
      {
         /* Invalid character detected => Abort */
         PRINT_ERROR("MIME: Invalid character in base64 encoded data");
         abort = 1;
         break;
      }
      /* Parse current character */
      switch(state)
      {
         case 0:
         {
            /* Get first input character */
            input = ii << 18;
            padding = 0;
            ++state;
            break;
         }
         case 1:
         {
            /* Get second input character */
            input |= ii << 12;
            ++state;
            break;
         }
         case 2:
         {
            /* Get third input character */
            if('=' == start[i])  { padding = 2; }
            else  { input |= ii << 6; }
            ++state;
            break;
         }
         case 3:
         {
            /* Get fourth input character */
            if('=' == start[i])
            {
               if(!padding)  { padding = 1; }
            }
            else if(padding)
            {
               PRINT_ERROR("MIME: Invalid padding in base64 encoded data");
               abort = 1;
               break;
            }
            else  { input |= ii; }
            /* Decode 3 output octet from 4 input characters */
            tmp[bi++] = (char) (unsigned char) ((input >> 16) & 0xFFUL);
            if (2 > padding)
            {
               tmp[bi++] = (char) (unsigned char) ((input >> 8) & 0xFFUL);
               if (1 > padding)
               {
                  tmp[bi++] = (char) (unsigned char) (input & 0xFFUL);
               }
            }
            state = 0;
            break;
         }
         default:
         {
            PRINT_ERROR("MIME: base64 state machine failed (Bug)");
            abort = 1;
            break;
         }
      }
      if(abort)  { break; }
      if(padding && 0 == state)  { break; }
   }
   if(abort)
   {
      posix_free((void*) tmp);
      tmp = NULL;
   }

   /* Check for unpadded format */
   if(!abort && np && state)
   {
      switch(state)
      {
         case 2:
         {
            /* One character left */
            if(input & 0x000FFFUL)  { abort = 1; }
            else
            {
               tmp[bi++] = (char) (unsigned char) ((input >> 16) & 0xFFUL);
            }
            break;
         }
         case 3:
         {
            /* Two characters left */
            if(input & 0x00003FUL)  { abort = 1; }
            else
            {
               tmp[bi++] = (char) (unsigned char) ((input >> 16) & 0xFFUL);
               tmp[bi++] = (char) (unsigned char) ((input >> 8) & 0xFFUL);
            }
            break;
         }
         default:
         {
            abort = 1;
            break;
         }
      }
      if(abort)
      {
         PRINT_ERROR("MIME: Invalid end of unpadded base64 data");
      }
   }

   /* Terminate decoded data (for use as C string) */
   if(NULL == tmp)  { res = NULL; }
   else
   {
      tmp[bi] = 0;
      /* Report length without the NUL termination */
      *dlen = bi;
#if 0
      /* For debugging */
      printf("Length of base64 decoded data: %u\n", (unsigned int) *dlen);
#endif
      res = tmp;
   }

   return(res);
}


/* ========================================================================== */
/* Convert MIME base64 encoded text to Unicode (UTF-8)
 *
 * \param[in] charset  Character set of data
 * \param[in] start    Pointer to start of data
 * \param[in] end      Pointer to end of data
 * \param[in] np       Flag to request no padding
 *
 * \e end must never be smaller than \e start and must point to the location
 * after the last character.
 *
 * \attention
 * If \e np is true, the syntax is switched to that for base64 inside of UTF-7
 * according to RFC 2152. The resulting Unicode data is not normalized because
 * an UTF-7 shift sequence may start with a non-starter codepoint and represent
 * a "Defective combining character sequence" if processed without context.
 * The UTF-7 converter must normalize the data together with the context.
 *
 * \result
 * - Pointer to new memory block containing the decoded data
 * - NULL on error
 */

static const char*  enc_mime_decode_b(enum enc_mime_cs  charset,
                                      const char*  start, const char*  end,
                                      int  np)
{
   const char*  res = NULL;
   size_t  len = 0;
   char*  tmp = NULL;
   const char*  tmp2 = NULL;

   tmp = enc_mime_decode_base64(start, end, np, &len);
   if(NULL != tmp)
   {
      tmp2 = tmp;
      /* Convert UTF-16BE with separate function because data may contain NUL */
      if(ENC_CS_UTF_16BE == charset)
      {
         tmp2 = enc_uc_convert_utf16be_to_utf8(tmp, len);
         if(NULL == tmp2)
         {
            /* Error => Return empty string */
            tmp[0] = 0;
            tmp2 = tmp;
         }
         else  { posix_free((void*) tmp); }
         /* Do not normalize to NFC yet (maybe context is required) */
         res = tmp2;
      }
      else
      {
         /* Convert result to Unicode and normalize to NFC */
         res = enc_convert_to_utf8_nfc(charset, tmp2);
      }
      if(tmp2 != res)  { posix_free((void*) tmp2); }
   }

   return(res);
}


/* ========================================================================== */
/* Convert from CESU-8 to UTF-8 NFC
 *
 * \param[in] s  String to convert
 *
 * \todo Data with invalid UTF-8 sequences ist rejected. Should be repaired.
 *
 * \return
 * - Pointer to result (if not equal to \e s , a new memory block was allocated)
 * - NULL on error
 */

static const char*  enc_uc_convert_cesu8_to_utf8(const char*  s)
{
   const char*  res = NULL;
   char*  buf;
   size_t  len = strlen(s);
   size_t  i = 0;
   size_t  bi = 0;
   int  rv;
   long int  mbc;
   long int  hs = 0;  /* Buffer for high surrogate */
   long int  ucp;
   size_t  num;

   rv = enc_uc_check_cesu8(s, 0);
   if(0 == rv)
   {
      /*
       * Allocate the result buffer for same size as the original data.
       * This is always sufficient.
       */
      buf = (char*) posix_malloc(len + (size_t) 1);
      if(NULL != buf)
      {
         res = buf;
         while(len > i)
         {
            /* Decode multibyte character */
            mbc = enc_uc_decode_utf8(s, &i);
            if(-1L == mbc)
            {
               free((void*) buf);
               res = NULL;
               break;
            }
            /* Check for surrogate pair */
            if(0x00D800L <= mbc && 0x00DFFFL >= mbc)
            {
               if(0x00DBFFL >= mbc)
               {
                  /* High surrogate */
                  if(hs)
                  {
                     PRINT_ERROR("Invalid high surrogate in CESU-8 data");
                     /* Replacement character has same size in UTF-8 */
                     ucp = 0x00FFFDL;
                  }
                  else
                  {
                     hs = mbc;
                     continue;
                  }
               }
               else
               {
                  /* Low surrogate */
                  if(!hs)
                  {
                     PRINT_ERROR("Invalid low surrogate in CESU-8 data");
                     /* Replacement character has same size in UTF-8 */
                     ucp = 0x00FFFDL;
                  }
                  else
                  {
                     /* Calculate Unicode codepoint */
                     ucp = (hs & 0x0003FFL) << 10;
                     ucp |= (mbc & 0x0003FFL);
                     hs = 0;
                     ucp += 0x010000L;  /* Add size of BMP */
                  }
               }
            }
            else  { ucp = mbc; }
            num = 1;
            enc_uc_encode_utf8(buf, &bi, &ucp, &num);
         }
         if(NULL != res)
         {
            /* Terminate result string */
            buf[bi++] = 0;
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/* Convert from UTF-7 to UTF-8
 *
 * \param[in] s  String to convert
 *
 * \return
 * - Pointer to result (if not equal to \e s , a new memory block was allocated)
 * - NULL on error
 */

static const char*  enc_uc_convert_utf7_to_utf8(const char*  s)
{
   const char*  res = NULL;
   size_t  i = 0;
   int  shift = 0;  /* State of decoder */
   char*  buf;
   size_t  bi = 0;
   size_t  len = strlen(s);
   int  c;
   size_t  ii;
   const char*  start = NULL;
   const char*  end = NULL;
   const char*  utf8;
   size_t  utf8_len;
   int  eod = 0;  /* Flag indicating end of (input) data */

   /*
    * Allocate the result buffer twice the size as the original data.
    * This is always sufficient.
    *
    * The same buffer size may not be sufficient even if the base64 decoding
    * always reduce the size of the data. The reason is the UTF-16BE
    * representation of the UTF-7 data that we want to convert to UTF-8.
    * The UTF-8 representation of Unicode can be larger than UTF-16!
    * This is the case for the codepoints that require 3 bytes in UTF-8
    * and therefore an enlargement factor of 1.5 must be calculated.
    */
   buf = (char*) posix_malloc(++len * (size_t) 2);
   if(NULL != buf)
   {
      while(s[i])
      {
         /* Check current state */
         if(!shift)
         {
            /* Check for shift */
            if('+' == s[i])
            {
               if('-' == s[i + (size_t) 1])
               {
                  /* Literal '+' detected */
                  ++i;
                  buf[bi++] = '+';
               }
               else
               {
                  start = &s[i + (size_t) 1];
                  shift = 1;
               }
            }
            else
            {
               c = (int) s[i];
               /* Check for control characters (Rule 3) */
               if(0x09 == c || 0x0A == c || 0x0D == c|| 0x20 == c)
               {
                  buf[bi++] = s[i];
               }
               /* Check for character set D (Rule 1) */
               else if( (0x30 <= c && 0x39 >= c)
                   || (0x41 <= c && 0x5A >= c) || (0x61 <= c && 0x7A >= c)
                   || (0x27 <= c && 0x29 >= c) || (0x2C <= c && 0x2F >= c)
                   || 0x3A == c || 0x3F == c )
               {
                  buf[bi++] = s[i];
               }
               /* Check for character set O (Rule 1) */
               else if( (0x21 <= c && 0x26 >= c) || 0x2A == c
                   || (0x3B <= c && 0x3E >= c)
                   || 0x40 == c || 0x5B == c
                   || (0x5D <= c && 0x60 >= c) || (0x7B <= c && 0x7D >= c) )
               {
                  buf[bi++] = s[i];
               }
               else
               {
                  PRINT_ERROR("Invalid character in UTF-7 data ignored");
               }
            }
         }
         else
         {
            /* Check for character set B (Rule 2) */
            c = 0;
            for(ii = 0; ii < (size_t) 64; ++ii)
            {
               if(enc_base64[ii] == s[i])  { c = (int) s[i];  break; }
            }
            /* Treat end of data like a termination character */
            if(!s[i + (size_t) 1])  { eod = 1; }
            if(!c || eod)
            {
               /* Attention: eod must have lower precedence if both are true */
               if(!c)  { end = &s[i]; }
               else  { end = &s[i + (size_t) 1]; }
               /* Decode Unicode data as UTF-16BE */
               utf8 = enc_mime_decode_b(ENC_CS_UTF_16BE, start, end, 1);
               if(NULL == utf8)  { buf[bi++] = '?'; }
               else
               {
                  utf8_len = strlen(utf8);
                  for(ii = 0; ii < utf8_len; ++ii)  { buf[bi++] = utf8[ii]; }
                  posix_free((void*) utf8);
               }
               /* Preserve any terminating character except '-' */
               if(!c && '-' != s[i])  { --i; }
               /* Terminate shift state */
               shift = 0;
            }
         }
         ++i;
      }
      /* Terminate result string */
      buf[bi] = 0;
      res = buf;
   }

   /* Check for error */
   if(shift)
   {
      PRINT_ERROR("UTF-7 converter terminated in shift state (bug)");
   }

   return(res);
}


/* ========================================================================== */
/* Check for leap year in terms of gregorian calendar
 *
 * \return
 * - 0 if \e year is not a leap year
 * - Nonzero if \e year is a leap year
 */

static int  enc_check_leap_year(unsigned int  year)
{
   if(!(year % 400U) || (!(year % 4U) && (year % 100U)))  { return(1); }
   else  { return(0); }
}


/* ========================================================================== */
/* Encode date and time to POSIX timestamp (seconds since epoche)
 *
 * \param[out] pts      Pointer to seconds since epoche (as defined by POSIX.1)
 * \param[in]  year     Years
 * \param[in]  month    Months
 * \param[in]  day      Days
 * \param[in]  hour     Hours
 * \param[in]  minute   Minutes
 * \param[in]  seconds  Seconds
 * \param[in]  zone     Timezone correction in minutes (Zero for UTC)
 *
 * \attention
 * This function accepts no timestamps before the epoche (the Usenet has not
 * existed yet at that time).
 *
 * On error, zero is written to \e pts .
 */

static int  enc_encode_posix_timestamp(core_time_t*  pts, unsigned int  year,
                                       unsigned int  month, unsigned int  day,
                                       unsigned int  hour, unsigned int  minute,
                                       unsigned int  second, int  zone)
{
   static const unsigned int  dom[12] = { 31U, 29U, 31U, 30U, 31U, 30U,
                                          31U, 31U, 30U, 31U, 30U, 31U };
   int  res = -1;
   core_time_t  ts = 0;
   core_time_t  zone_seconds;
   unsigned int  i;

   /* Clamp year down to 1970 */
   if(1970U <= year)
   {
      /* Check for 'core_time_t' overflow (leave at least one year) */
      if(2104U < year)
      {
         PRINT_ERROR("Warning: core_time_t overflow while decoding timestamp");
         year = 2104U;
      }
      for(i = 1970U; i < year; ++i)
      {
         ts += (core_time_t) 365 * (core_time_t) 86400;
         /* Add an additional day for leap years */
         if(enc_check_leap_year(i))  { ts += (core_time_t) 86400; }
      }
      for(i = 0; i < month - 1U; ++i)
      {
         ts += (core_time_t) dom[i] * (core_time_t) 86400;
         /* Subtract one day if current year is not a leap year */
         if(1U == i && !enc_check_leap_year(year))
         {
            ts -= (core_time_t) 86400;
         }
      }
      ts += (core_time_t) (day - 1U) * (core_time_t) 86400;
      ts += (core_time_t) hour * (core_time_t) 3600;
      ts += (core_time_t) minute * (core_time_t) 60;
      ts += (core_time_t) second;
      if(0 > zone)
      {
         zone_seconds = (core_time_t) -zone * (core_time_t) 60;
         ts += zone_seconds;
         res = 0;
      }
      else
      {
         zone_seconds = (core_time_t) zone * (core_time_t) 60;
         if(ts >= zone_seconds)
         {
            ts -= zone_seconds;
            res = 0;
         }
      }
   }

   /* Store result */
   if(res)
   {
      PRINT_ERROR("Encoding POSIX timestamp failed");
      *pts = 0;
   }
   else  { *pts = ts; }

#if 0
   if(!res)
   {
      /* For debugging (not thread safe) */
      printf("Seconds  : %lu\n", (long int) ts);
      struct tm*  t;
      t = gmtime((posix_time_t*) &ts);
      printf("Conv. UTC: %04d-%02d-%02d %02d:%02d:%02d\n",
             t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
             t->tm_hour, t->tm_min, t->tm_sec);
   }
#endif

   return(res);
}


/* ========================================================================== */
/* Check RFC 5322 atom
 *
 * \param[in] s  Pointer to single character
 *
 * \return
 * - 0 if character pointed to by \e s is allowed
 * - Negative value on error
 */

static int  enc_check_atom(const char*  s)
{
   int  res = -1;
   int  c = (int) *s;

   /* Allow 'atext' */
   if(0x30 <= c && 0x39 >=c)  { res = 0; }
   else if(0x41 <= c && 0x5A >=c)  { res = 0; }
   else if(0x61 <= c && 0x7A >=c)  { res = 0; }
   else if((int) '!' == c)  { res = 0; }
   else if((int) '#' == c)  { res = 0; }
   else if((int) '$' == c)  { res = 0; }
   else if((int) '%' == c)  { res = 0; }
   else if((int) '&' == c)  { res = 0; }
   else if(0x27 == c)  { res = 0; }
   else if((int) '*' == c)  { res = 0; }
   else if((int) '+' == c)  { res = 0; }
   else if((int) '-' == c)  { res = 0; }
   else if((int) '/' == c)  { res = 0; }
   else if((int) '=' == c)  { res = 0; }
   else if((int) '?' == c)  { res = 0; }
   else if((int) '^' == c)  { res = 0; }
   else if((int) '_' == c)  { res = 0; }
   else if((int) '`' == c)  { res = 0; }
   else if((int) '{' == c)  { res = 0; }
   else if((int) '|' == c)  { res = 0; }
   else if((int) '}' == c)  { res = 0; }
   else if((int) '~' == c)  { res = 0; }

   return(res);
}


/* ========================================================================== */
/* Check RFC 5322 dot-atom
 *
 * \param[in] s  Pointer to single character
 *
 * \return
 * - 0 if character pointed to by \e s is allowed
 * - Negative value on error
 */

static int  enc_check_dotatom(const char*  s)
{
   int  res;

   /* Allow dot and atext */
   if('.' == *s)  { res = 0; }
   else  { res = enc_check_atom(s); }

   return(res);
}


/* ========================================================================== */
/* Encode words in display-name
 *
 * Words containing only 7 bit characters are encoded to atom or quoted-string.
 * Words containing 8 bit characters are preserved unchanged for MIME encoder.
 *
 * \param[in,out] s  Pointer to data buffer
 */

static void  enc_encode_dispname(char*  s)
{
   size_t  i = 0;
   char  buf[ENC_HDR_BUFSIZE + (size_t) 1];
   size_t  bi = 0;
   char  word[ENC_HDR_BUFSIZE + (size_t) 1];
   char*  w;
   size_t  word_len;
   int  last_word = 0;
   size_t  ii;
   int  atom;
   size_t  start;
   int  error;
   char  cbuf[2];

   while(s[i])
   {
      /* Extract next word */
      ii = i;  while(' ' == s[ii])  { ++ii; }
      w = strchr(&s[ii], (int) ' ');
      if(NULL == w)
      {
         word_len = strlen(&s[i]);
         last_word = 1;
      }
      else  { word_len = (size_t) (w - &s[i]); }
      if(ENC_HDR_BUFSIZE < word_len)  { word[0] = 0; }
      else
      {
         memcpy((void*) word, (void*) &s[i], word_len);
         word[word_len] = 0;
      }
      i += word_len;
      if(!last_word)  { ++i; }  /* Skip SP delimiter */

      /* Check word */
      atom = 1;
      ii = 0;
      while(word[ii])
      {
         if(0x80U <= (unsigned int) (unsigned char) word[ii])
         {
            atom = 1;
            break;
         }
         if(enc_check_atom(&word[ii]))  { atom = 0; }
         ++ii;
      }

      /* SP delimiter between words */
      if(bi)
      {
         if(ENC_HDR_BUFSIZE <= bi)  { break; }  else  { buf[bi++] = ' '; }
      }

      /* Copy data to buffer */
      if(atom)
      {
         if(ENC_HDR_BUFSIZE - bi < word_len)  { break; }
         else
         {
            memcpy((void*) &buf[bi], (void*) word, word_len);
            bi += word_len;
         }
      }
      else
      {
         /* Create quoted-string */
         start = bi;
         error = 0;
         /* Leading DQUOTE delimiter */
         if(ENC_HDR_BUFSIZE <= bi)  { error = 1; }  else  { buf[bi++] = '"'; }
         /* Process data */
         for(ii = 0; ii < word_len; ++ii)
         {
            /* Skip control characters */
            cbuf[0] = word[ii];  cbuf[1] = 0;
            if(enc_ascii_check_printable(cbuf))  { continue; }
            /* Check remaining buffer size */
            if(ENC_HDR_BUFSIZE - bi < (size_t) 2)  { error = 1;  break; }
            /* Check whether quoted pair is required */
            if('"' == word[ii] || 0x5C == (int) word[ii])  { buf[bi++] = 0x5C; }
            buf[bi++] = word[ii];
         }
         /* Trailing DQUOTE delimiter */
         if(ENC_HDR_BUFSIZE <= bi)  { error = 1; }  else  { buf[bi++] = '"'; }
         if(error)  { bi = start; }
      }
      if(last_word)  { break; }
   }
   /* Terminate buffer */
   buf[bi] = 0;
   /* Copy data back to callers buffer */
   strncpy(s, buf, ++bi);

   return;
}


/* ========================================================================== */
/* Decode MIME parameter percent encoding
 *
 * \param[in] buf  Pointer to data buffer
 * \param[in] cs   IANA name of character set
 *
 * This function decodes the percent encoding defined for MIME parameters by
 * RFC 2231. The IANA name of the character set for the resulting octet stream
 * must be specified by \e cs . The data is converted to Unicode in UTF-8
 * representation with NFC normalization.
 *
 * \return
 * - Pointer to decoded data (a new memory block was allocated)
 * - NULL on error
 */

static char*  enc_mime_decode_parameter(const char*  buf, const char*  cs)
{
   char*  res = NULL;
   char*  tmp = NULL;
   const char*  tmp2 = NULL;
   size_t  len;
   int  rv;
   enum enc_mime_cs  charset;

   if(NULL != buf)
   {
      /* Percent decoder */
      len = strlen(buf);
      tmp = (char*) posix_malloc(++len);
      if(NULL != tmp)
      {
         memcpy((void*) tmp, (void*) buf, len);
         if(enc_ascii_check_printable(tmp))
         {
            PRINT_ERROR("MIME: Nonprintable characters in parameter");
         }
         else
         {
            rv = enc_percent_decode(tmp, 1);
            if(0 > rv)
            {
               PRINT_ERROR("MIME: Percent encoding failed for parameter");
            }
            else
            {
               charset = enc_mime_get_charset(cs, strlen(cs));
               tmp2 = enc_convert_to_utf8_nfc(charset, tmp);
               if(NULL == tmp2)
               {
                  PRINT_ERROR("MIME: Parameter charset not supported");
               }
               else
               {
                  len = strlen(tmp2);
                  res = (char*) posix_malloc(++len);
                  if(NULL != res)
                  {
                     memcpy((void*) res, (void*) tmp2, len);
                  }
                  if(tmp != tmp2)  { posix_free((void*) tmp2); }
               }
            }
         }
      }
   }
   posix_free((void*) tmp);

   return(res);
}


/* ========================================================================== */
/*! \brief Create a "name-addr" construct according to RFC 5322
 *
 * This function is intended to create the "From" and "Reply-To" header fields.
 *
 * \param[in] data       Input data
 * \param[in] offset     Folding offset, e.g. \c sizeof("From: ")
 *
 * The input data must have the following format: \c name \c \<addr-spec\> .
 *
 * \attention
 * The \c addr-spec construct is not allowed to contain comments or quoted
 * strings. Both parts, \c name and \c \<addr-spec\> must fit on a single header
 * line of 998 characters. Note that \e offset adds to the length of \c name .
 *
 * \c name must be an Unicode identifier corresponding to \c addr-spec . If it
 * contains non-ASCII characters, it is converted to a valid \c display-name
 * token. The result will be folded according to RFC 2047.
 *
 * On success the caller is responsible to free the memory allocated for the
 * result.
 *
 * \return
 * - Pointer to encoded data (a new memory block was allocated)
 * - NULL on error
 */

const char*  enc_create_name_addr(const char*  data, size_t  offset)
{
   const char*  res = NULL;
   size_t  len = 4;  /* The space after name, the angle brackets and NUL */
   size_t  i;
   size_t  counter = 0;
   int  error = 0;
   char  c;
   char*  buf;
   int  rv;
   char  name[(size_t) 2 * ENC_HDR_BUFSIZE + (size_t) 1];
   char  addr_spec[ENC_HDR_BUFSIZE + (size_t) 1];

   /* Extract name and addr-spec parts from input data */
   if((size_t) 2 * ENC_HDR_BUFSIZE < strlen(data))  { error = 1; }
   else
   {
      strcpy(name, data);
      addr_spec[0] = 0;
      if(!strlen(name))  { error = 1; }
      else
      {
         i = 0;
         while(name[i])
         {
            if('<' == name[i])
            {
               if(NULL == strchr(&name[i + (size_t) 1], (int) '<'))
               {
                  if(!i)  { name[0] = 0; }
                  else  { name[i - (size_t) 1] = 0; }
                  if(ENC_HDR_BUFSIZE < strlen(&name[i]))  { error = 1; }
                  else
                  {
                     strcpy(addr_spec, &name[++i]);
                     i = strlen(addr_spec) - (size_t) 1;
                     if('>' != addr_spec[i])  { addr_spec[0] = 0; }
                     else  { addr_spec[i] = 0; }
                  }
                  break;
               }
            }
            ++i;
         }
      }
   }

   /* Prepare display-name */
   if(!error)
   {
      enc_encode_dispname(name);
      len += strlen(name);
   }

   /* Check addr-spec */
   if(!error)
   {
      len += strlen(addr_spec);
      error = enc_ascii_check(addr_spec);
      if(!error)
      {
         i = 0;
         do
         {
            c = addr_spec[i];  if(!c)  { break; }
            if('@' != c && enc_check_dotatom(&c))
            {
               /* Invalid dot-atom found */
               error = 1;
            }
            /* Handle "@" separator */
            else if('@' == c)
            {
               ++counter;
               if(!i || !addr_spec[i + (size_t) 1])
               {
                  /* Invalid separator found (at beginning or end) */
                  error = 1;
               }
               /* Verify that dot-atoms don't have dots at beginning or end */
               if('.' == addr_spec[i - (size_t) 1]
                  || '.' == addr_spec[i + (size_t) 1])
               {
                  /* Invalid dot-atom found */
                  error = 1;
               }
            }
            /* Verify that dot-atoms don't have dots at beginning or end */
            if(!error && '.' == c)
            {
               if(!i || !addr_spec[i + (size_t) 1])
               {
                  /* Invalid dot-atom found */
                  error = 1;
               }
            }
            ++i;
         }
         while(!error);
      }
      /* Final checks */
      if(! (error || (size_t) 1 != counter || (size_t) 5 > strlen(addr_spec)) )
      {
         /* Allocate buffer */
         buf = (char*) posix_malloc(len);
         if(NULL != buf)
         {
            /* Copy name and add trailing space (if not empty string) */
            if(name[0])
            {
               strcpy(buf, name);
               strcat(buf, " <");
            }
            else  { strcpy(buf, "<"); }
            /* Copy addr-spec between angle brackets */
            strcat(buf, addr_spec);
            strcat(buf, ">");
            /* MIME encoding */
            rv = enc_mime_word_encode(&res, buf, offset);
            if(0 >= rv)  { posix_free((void*) buf); }
            /* For positive return value, 'buf' was assigned to 'res'! */
         }
      }
   }

   /* Check for error */
   if(error)  { PRINT_ERROR("Creating name-addr construct failed"); }

   /* For code review: Do not 'free()' memory pointed to by 'buf' here! */

   return(res);
}

/* ========================================================================== */
/*! \brief Decode number of lines
 *
 * \param[in] lines  Number of lines
 *
 * \e lines must be a RFC 5536 conformant body of the (now obsolete) "Lines"
 * header field.
 *
 * \return
 * - Number of lines
 * - 0 on error
 */

unsigned long int  enc_lines_decode(const char*  lines)
{
   unsigned long int  res;

   if(1 != sscanf(lines, "%lu", &res))  { res = 0; }

   return(res);
}


/* ========================================================================== */
/*! \brief Convert number of lines to string
 *
 * \param[out] l     Pointer to result buffer (at least 11 characters large)
 * \param[in] l_raw  Number of lines
 *
 * \attention
 * The value of \e l_raw must be representable as decimal number with not more
 * than 10 digits. Otherwise the string \c "Error" is returned.
 */

void  enc_convert_lines_to_string(char*  l, unsigned long int  l_raw)
{
   int  rv;

   rv = posix_snprintf(l, 11, "%lu", l_raw);
   if(0 > rv || 11 <= rv)
   {
      l[0] = 'E';
      l[1] = 'r';
      l[2] = 'r';
      l[3] = 'o';
      l[4] = 'r';
      l[5] = 0;
   }
}


/* ========================================================================== */
/*! \brief Decode canonical timestamp to POSIX time (seconds since epoche)
 *
 * According to RFC 5322 all military timezones should be
 * treated as UTC because there was an error in RFC 822
 * => We do so and accept "Z" as valid because it means UTC
 *
 * \note
 * This function accepts no timestamps before the epoche (the Usenet has not
 * existed yet at that time).
 *
 * \param[in] timestamp  RFC 5536 conformant timestamp string
 *
 * \return
 * - Seconds since epoche (as defined by POSIX.1)
 * - 0 on error
 */

core_time_t  enc_timestamp_decode(const char*  timestamp)
{
   static const char*  months[12] = { "JAN", "FEB", "MAR", "APR", "MAY", "JUN",
                                     "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" };
   static const unsigned int  dom[12] = { 31U, 29U, 31U, 30U, 31U, 30U,
                                          31U, 31U, 30U, 31U, 30U, 31U };
   core_time_t  res = 0;
   int  error = 1;
   const char*  p;
   const char*  q;
   int  rv;
   char  m[4];
   char  z[6];
   unsigned int  zh = 0;
   unsigned int  zm = 0;
   unsigned int  i;
   unsigned int  year;
   unsigned int  month = 13U;
   unsigned int  day;
   unsigned int  hour = 0;
   unsigned int  minute = 0;
   unsigned int  second = 0;
   int  zone = 1;  /* Correction in minutes */
   int  pos = 0;

#if 0
   /* For debugging */
   printf("------------------------------------------------------------\n");
   printf("Timestamp: %s\n", timestamp);
#endif

   /* Skip optional day-of-week */
   p = strchr(timestamp, (int) ',');
   if(NULL == p)  { p = timestamp; }  else  { ++p; }

   /* Extract date */
   rv = sscanf(p, "%u %3c %u%n", &day, m, &year, &pos);
   if(3 != rv)  { PRINT_ERROR("Invalid date in timestamp"); }
   else
   {
      /* Check for obsolete year format as defined by RFC 5322 */
      if(1000U > year)
      {
         if(50U > year)  { year += 2000U; }  else  { year += 1900U; }
      }
      /* Decode month */
      m[0] = (char) toupper((int) m[0]);
      m[1] = (char) toupper((int) m[1]);
      m[2] = (char) toupper((int) m[2]);
      m[3] = 0;
      for(i = 0; i < 12; ++i)
      {
         if(!strcmp(months[i], m))  { month = i + 1U;  break; }
      }
      if(13U <= month)  { PRINT_ERROR("Invalid month in timestamp"); }
      else if(i < 12)
      {
         /* Check day */
         if(1U > day || dom[i] < day)  { month = 13U; }
         if(13U > month)
         {
            if(2U == month && 29U == day)
            {
               /* Check for leap year in terms of gregorian calendar */
               if(!enc_check_leap_year(year))  { month = 13U; }
            }
         }
         if(13U <= month)
         {
            PRINT_ERROR("Invalid day of month in timestamp");
         }
      }
   }

   /* Extract time if date was found */
   if(13U > month)
   {
      p += pos;
      rv = sscanf(p, "%u : %u%n", &hour, &minute, &pos);
      if(2 != rv)
      {
         PRINT_ERROR("Invalid time in timestamp");
      }
      else
      {
         p += pos;
         q = strchr(p, (int) ':');
         if(NULL != q)  { p = q; }
         rv = sscanf(p, ": %u%n", &second, &pos);
         if(1 == rv)  { p += pos; }
         rv = sscanf(p, "%5s", z);
         z[5] = 0;
         if(1 != rv)
         {
            PRINT_ERROR("Missing timezone in timestamp");
         }
         else
         {
            /* Check time (accept leap second according to RFC 5322) */
            if(23U < hour || 59U < minute || 60U < second)
            {
               PRINT_ERROR("Invalid time in timestamp");
            }
            else
            {
               /* Decode timezone */
               if('+' == z[0] || '-' == z[0])
               {
                  for(i = 1; i < 5; ++i)
                  {
                     if(enc_ascii_check_digit(&z[i]))  { zone = 0;  break; }
                  }
                  if(zone)
                  {
                     zh = ((unsigned int) z[1] - 0x30) * 10U;
                     zh += ((unsigned int) z[2] - 0x30);
                     zm = ((unsigned int) z[3] - 0x30) * 10U;
                     zm += ((unsigned int) z[4] - 0x30);
                     if(59U < zm)  { zone = 0; }
                  }
                  if(!zone)
                  {
                     PRINT_ERROR("Invalid timezone in timestamp");
                  }
                  else
                  {
                     zone = (int) (zh * 60U + zm);
                     if('-' == z[0])  { zone *= -1; }
                  }
               }
               else
               {
                  /* Check for obsolete timezone format */
                  if(!strcmp("GMT", z))  { zone = 0; }
                  else if(!strcmp("UT", z))  { zone = 0; }
                  else if(!strcmp("EDT", z))  { zone = -4 * 60; }
                  else if(!strcmp("EST", z))  { zone = -5 * 60; }
                  else if(!strcmp("CDT", z))  { zone = -5 * 60; }
                  else if(!strcmp("CST", z))  { zone = -6 * 60; }
                  else if(!strcmp("MDT", z))  { zone = -6 * 60; }
                  else if(!strcmp("MST", z))  { zone = -7 * 60; }
                  else if(!strcmp("PDT", z))  { zone = -7 * 60; }
                  else if(!strcmp("PST", z))  { zone = -8 * 60; }
                  else if(!strcmp("Z", z))  { zone = 0; }
                  else
                  {
                     zone = 0;
                     PRINT_ERROR("Decode unknown timezone in timestamp as UTC");
                  }
               }
#if 0
               /* For debugging */
               printf("Decoded  : %04u-%02u-%02u %02u:%02u:%02u %+d minutes\n",
                      year, month, day, hour, minute, second, zone);
#endif
               /* Decoding successful */
               error = 0;
            }
         }
      }
   }

   /* Calculate seconds since epoche */
   if(!error)
   {
      enc_encode_posix_timestamp(&res, year, month, day, hour, minute, second,
                                 zone);
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Convert POSIX timestamp to ISO 8601 conformant local date and time
 *
 * \param[out] isodate  Buffer for date string (at least 20 characters)
 * \param[in]  pts      Seconds since epoche (as defined by POSIX.1)
 *
 * ISO 8601 allows to omit the 'T' character between the date and time fields
 * if there is no risk of confusing a date and time of day representation.
 * This is the case here => We omit the 'T' for better human readability
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  enc_convert_posix_to_iso8601(char*  isodate, core_time_t  pts)
{
   int  res = -1;
   posix_time_t  ts;
   struct_posix_tm  t_data;
   struct_posix_tm*  t;

   /*
    * Check for potential 'time_t' overflow
    * Many historical 32 bit Unix systems use 'signed int' for 'time_t'.
    * We clamp the 'pts' to this lowest common denominator.
    */
   if((core_time_t) INT_MAX < pts)
   {
      /* Clamp time up to 2038-01-19T03:14:07Z if system uses 32 bit 'int' */
      PRINT_ERROR("Warning: time_t overflow while converting timestamp");
      ts = (core_time_t) INT_MAX;
   }
   else  { ts = (posix_time_t) pts; }

   /* Convert POSIX timestamp to ISO 8601 date */
   /*! \todo
    * Calling operating system for date conversion should be replaced until the
    * year 2038 (when 32 bit signed \c time_t implementations will overflow).
    */
   t = posix_localtime_r(&ts, &t_data);
   if(NULL != t)
   {
      /* Return value is intentionally ignred, use ign to silence compiler */
      ign = posix_snprintf(isodate, 20, "%04d-%02d-%02d %02d:%02d:%02d",
                           t->tm_year + 1900, t->tm_mon + 1, t->tm_mday,
                           t->tm_hour, t->tm_min, t->tm_sec);
      res = 0;
   }

   if(0 > res)  { PRINT_ERROR("Timestamp conversion failed"); }

   return(res);
}


/* ========================================================================== */
/*! \brief Get current UTC date in ISO 8601 conformant format
 *
 * \param[out] isodate  Buffer for date string (at least 21 characters)
 *
 * The date is written to \e isodate in \c YYYY-MM-DDTHH-MM-SSZ format.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  enc_get_iso8601_utc(char*  isodate)
{
   int  res = -1;
   posix_time_t  ts;
   struct_posix_tm  t_data;
   struct_posix_tm*  t;

   /*
    * Check for potential 'time_t' overflow
    * Many historical 32 bit Unix systems use 'signed int' for 'time_t'.
    * We clamp the 'pts' to this lowest common denominator.
    */
   posix_time(&ts);
   if((posix_time_t) 0 > ts)  { res = -1; }
   else
   {
      /* Convert POSIX timestamp to ISO 8601 date */
      /*! \todo
       * Calling operating system for date conversion should be replaced until the
       * year 2038 (when 32 bit signed \c time_t implementations will overflow).
       */
      t = posix_gmtime_r(&ts, &t_data);
      if(NULL != t)
      {
         /* Return value is intentionally ignred, use ign to silence compiler */
         ign = posix_snprintf(isodate, 21, "%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);
         res = 0;
      }
   }

   if(0 > res)  { PRINT_ERROR("ISO 8601 date request failed"); }

   return(res);
}


/* ========================================================================== */
/*! \brief Convert ISO 8601 conformant UTC date and time to POSIX timestamp
 *
 * \param[out] pts      Seconds since epoche (as defined by POSIX.1)
 * \param[in]  isodate  Buffer for date string (at least 20 characters)
 *
 * \attention
 * The parameter \e isodate must be in \c YYYY-MM-DDTHH-MM-SSZ format (UTC).
 *
 * \note
 * This function accepts no date input before the epoche.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  enc_convert_iso8601_to_posix(core_time_t*  pts, const char*  isodate)
{
   int  res = -1;
   int  rv;
   unsigned int  year;
   unsigned int  month;
   unsigned int  mday;
   unsigned int  hour;
   unsigned int  minute;
   unsigned int  second;

   /* Split ISO 8601 date */
   rv = sscanf(isodate, "%u-%u-%uT%u:%u:%uZ", &year, &month, &mday,
               &hour, &minute, &second);
   if(6 != rv)  { PRINT_ERROR("ISO 8601 timestamp has invalid format"); }
   else
   {
      if(1970U <= year && 9999U >= year
         && 1U <= month && 12U >= month
         && 1U <= mday && 31U >= mday
         && 23U >= hour && 59U >= minute && 59U >= second)  { res = 0; }
   }

   /* Calculate seconds since epoche */
   if(!res)
   {
      res = enc_encode_posix_timestamp(pts, year, month, mday,
                                       hour, minute, second, 0);
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Convert ISO 8601 conformant date to canonical timestamp
 *
 * \param[out]  ts       Pointer to canonical timestamp as defined by RFC 5322
 * \param[in]   isodate  ISO 8601 date string (exactly 10 characters)
 *
 * \attention
 * The parameter \e isodate must be in \c YYYY-MM-DD format (only date, time is
 * not supported).
 *
 * \note
 * On success, the caller is responsible to free the memory allocated for the
 * result string.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  enc_convert_iso8601_to_timestamp(const char**  ts, const char*  isodate)
{
   static const char*  months[12] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun",
                                     "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
   int  res = -1;
   int  rv;
   unsigned int  year;
   unsigned int  month;
   unsigned int  mday;
   char*  buf = NULL;
   size_t  len = 50;

   /* Split ISO 8601 date */
   rv = sscanf(isodate, "%u-%u-%u", &year, &month, &mday);
   if(3 != rv)  { PRINT_ERROR("ISO 8601 timestamp has invalid format"); }
   else
   {
      if(1900U <= year && 9999U >= year
         && 1U <= month && 12U >= month
         && 1U <= mday && 31U >= mday)  { res = 0; }
   }
   if(!res)
   {
      /* Allocate buffer for result */
      buf = (char*) posix_malloc(len);
      if(NULL == buf)  { res = -1; }
      else
      {
         posix_snprintf(buf, 50, "%u %s %04u %02d:%02d:%02d -0000",
                        mday, months[--month], year, 0, 0, 0);
         *ts = (const char*) buf;
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Convert article number from numerical format to ASCII
 *
 * \param[out] result  Pointer to result string buffer (Size: 17 bytes)
 * \param[out] len     Pointer to length of result string (Maximum value: 16)
 * \param[in]  wm      Article number (watermark) to convert
 *
 * RFC 3977 allows max. 16 digits.
 *
 * \note
 * The output is locale independent.
 *
 * \return
 * - 0 on success
 * - Negative value on error (\e result and \e len are not valid)
 */

int  enc_convert_anum_to_ascii(char  result[17], size_t*  len, core_anum_t  wm)
{
   int  res = -1;
   int  rv;

   /* C90 compilers are not required to support more than 32 bit data types */
   if (CORE_ANUM_T_MAX > ULONG_MAX)
   {
      PRINT_ERROR("Value of CORE_ANUM_T_MAX is too large");
   }
   else
   {
      rv = posix_snprintf(result, 17, "%lu", (unsigned long int) wm);
      if(rv > 0 && rv <= 16)
      {
         *len = (size_t) rv;
         res = 0;
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Convert number from ASCII to numerical format
 *
 * \param[out] result  Pointer to result
 * \param[in]  wm      Article number (watermark) string to convert
 * \param[in]  len     Length of string \e wm
 *
 * Max. 20 digits are supported, sufficient for 64-bit article numbers.
 * RFC 3977 allows max. 16 digits.
 *
 * This function correctly processes leading zeros and does not use standard
 * library functions with locale dependent behaviour.
 *
 * \note
 * \e wm needs no termination, the first \e len characters are used.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 * - -2 means larger than \ref NNTP_ANUM_T_MAX
 */

int  enc_convert_ascii_to_anum(core_anum_t*  result, const char*  wm,
                               int  len)
{
   int  res = -1;
   unsigned char  c;
   nntp_anum_t  pot = 1;  /* 10^0 (0th power of 10) */
   nntp_anum_t  d;
   nntp_anum_t  v = 0;

   /* Check length */
   if(0 < len && 20 >= len)
   {
      /* Process every digit as a power of ten */
      while(len)
      {
         /* Get character and check whether it is a digit */
         c = (unsigned char) wm[len-- - 1];
         if(enc_ascii_check_digit((char*) &c))  { len = -1;  break; }
         /* ASCII decode it to numerical digit */
         d = (nntp_anum_t) (unsigned char) (c - 0x30U);
         /* Calculate value of digit */
         d *= pot;
         /* Avoid overflow */
         if(NNTP_ANUM_T_MAX - v < d)  { res = -2;  break; }
         /* Add value of digit to result */
         v += d;
         /* Calculate next power of ten */
         pot *= 10;
      }
      /* Check whether processing was successful */
      if(!len)
      {
         *result = v;
         res = 0;
      }
   }

   if(0 > res)  { PRINT_ERROR("Article number conversion failed"); }

   return(res);
}


/* ========================================================================== */
/*! \brief Convert octet to hexadecimal (ASCII) format
 *
 * \param[out] result  Pointer to result
 * \param[in]  octet   Octet to convert
 *
 * Exactly 3 bytes are written to the buffer pointed to by \e result .
 * If \e octet is smaller than 16, a leading zero is created.
 * On error, the result "XX" is generated.
 * The \e result is always a zero terminated string.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */


int  enc_convert_octet_to_hex(char*  result, unsigned int  octet)
{
   int  res = -1;

   if(255U >= octet)
   {
      if(2 == posix_snprintf(result, 3, "%02X", octet))  { res = 0; }
   }

   /* Check for error */
   if(res)  { strcpy(result, "XX"); }

   return(res);
}


/* ========================================================================== */
/*! \brief Encode or decode data with ROT13 algorithm
 *
 * \param[in] data  Pointer to buffer with Data to encode/decode
 *
 * Any character that is not a latin ASCII character in the ranges A..Z and
 * a..z will stay unchanged.
 *
 * No memory is allocated. The operation is executed in the buffer pointed to
 * by \e data .
 */

void  enc_rot13(char*  data)
{
   size_t  i = 0;
   int  c;
   int  modified = 0;

   while(data[i])
   {
      c = (int) data[i];
      /* Check for capital letter */
      if(65 <= c && 90 >= c)
      {
         c += 13;
         if(90 < c)  { c = 65 - 1 + (c - 90); }
         modified = 1;
      }
      /* Check for small letter */
      else if(97 <= c && 122 >= c)
      {
         c += 13;
         if(122 < c)  { c = 97 - 1 + (c - 122); }
         modified = 1;
      }
      /* Change character */
      if(modified)  { data[i] = (char) c; }
      /* Process next character */
      modified = 0;
      ++i;
   }
}


/* ========================================================================== */
/*! \brief Encode binary data to base64
 *
 * \param[out] enc   Pointer to result (zero terminated string)
 * \param[in]  data  Data to encode
 * \param[in]  len   Data length
 *
 * If \e len is zero, \e data is not dereferenced and the result will be an
 * empty string.
 *
 * On error, nothing is written to \e enc .
 *
 * On success a pointer to the result buffer will be written to \e enc .
 * The caller is responsible to free the memory allocated for this buffer.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  enc_mime_encode_base64(const char**  enc, const char*  data, size_t  len)
{
   int  res = -1;
   size_t  enc_len = (size_t) 4 * len / (size_t) 3;
   char*  buf = NULL;
   size_t  i = 0;
   size_t  enc_i = 0;
   unsigned long int  word;
   unsigned char  b64[4];

   enc_len += (size_t) 5;  /* Last quartet and termination */
   buf = (char*) posix_malloc(enc_len);
   if(NULL != buf)
   {
      if(len)
      {
         while((size_t) 3 <= len)
         {
            /* Compose 24 bit input word */
            word = (unsigned long int) (unsigned char) data[i++] << 16;
            word |= (unsigned long int) (unsigned char) data[i++] << 8;
            word |= (unsigned long int) (unsigned char) data[i++];
            len -= (size_t) 3;
            /* Encode */
            b64[3] = (unsigned char) (word & 0x00003FUL);
            b64[2] = (unsigned char) ((word & 0x000FC0UL) >> 6);
            b64[1] = (unsigned char) ((word & 0x03F000UL) >> 12);
            b64[0] = (unsigned char) ((word & 0xFC0000UL) >> 18);
            buf[enc_i++] = enc_base64[b64[0]];
            buf[enc_i++] = enc_base64[b64[1]];
            buf[enc_i++] = enc_base64[b64[2]];
            buf[enc_i++] = enc_base64[b64[3]];
         }
         if(len)
         {
            /* End of data and padding */
            word = (unsigned long int) (unsigned char) data[i++] << 16;
            if((size_t) 1 < len)
            {
               word |= (unsigned long int) (unsigned char) data[i++] << 8;
            }
            /* Encode */
            b64[3] = (unsigned char) (word & 0x00003FUL);
            b64[2] = (unsigned char) ((word & 0x000FC0UL) >> 6);
            b64[1] = (unsigned char) ((word & 0x03F000UL) >> 12);
            b64[0] = (unsigned char) ((word & 0xFC0000UL) >> 18);
            buf[enc_i++] = enc_base64[b64[0]];
            buf[enc_i++] = enc_base64[b64[1]];
            if((size_t) 1 < len)  { buf[enc_i++] = enc_base64[b64[2]]; }
            else  { buf[enc_i++] = '='; }
            buf[enc_i++] = '=';
         }
      }
      buf[enc_i] = 0;
      *enc = buf;
      res = 0;
   }

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

   return(res);
}


/* ========================================================================== */
/*! \brief Extract addr-spec token from RFC 5322 mailbox
 *
 * \param[in] mailbox  RFC 5322 mailbox
 *
 * \attention
 * The checks are more restrictive than the formal specification of RFC 5322.
 * White space is not allowed inside the \c addr-spec token!
 *
 * \note
 * It is tolerated that \e mailbox contains an invalid \c name-addr token
 * because it is ignored anyway.
 *
 * On success a pointer to the result buffer is returned.
 * The caller is responsible to free the memory allocated for this buffer.
 *
 * \return
 * - Pointer to new memory block containing the \c addr-spec token
 * - NULL on error
 */

const char*  enc_extract_addr_spec(const char*  mailbox)
{
   char*  res = NULL;
   unsigned int  state;
   const char*  s;
   const char*  e;
   size_t  len;
   size_t  i, ii;

   if(NULL != mailbox)
   {
      len = strlen(mailbox);
      /* A valid addr-spec is at least 3 characters long */
      if((size_t) 3 <= len)
      {
         /* Default to assumption that whole mailbox is an 'addr-spec' token */
         s = mailbox;
         e = &mailbox[len];
         /*
          * To tolerate arbitrary garbage for the 'name-addr' token search
          * backward from the end for an 'angle-addr' token.
          */
         state = 0;
         for(i = len; i; --i)
         {
            ii = i - (size_t) 1;
            if(!state && '>' == mailbox[ii])
            {
               e = &mailbox[ii];
               ++state;
               continue;
            }
            if(1U == state && '@' == mailbox[ii])  { ++state;  continue; }
            if(2U == state && '<' == mailbox[ii])
            {
               /* 'angle-addr' token found at end of mailbox */
               ++state;
               s = &mailbox[ii];
               ++s;
               break;
            }
         }
         if((!state || 3U <= state) && e > s + 2)
         {
            /* Allocate new memory block and copy 'addr-spec' to it */
            len = (size_t) (e - s);
            res = (char*) posix_malloc(len + (size_t) 1);
            if(NULL != res)
            {
               memcpy((void*) res, (void*) s, len);  res[len] = 0;
               /* Check whether result is a valid 'addr-spec' token */
               i = 0;
               state = 0;
               while(res[i] && '@' != res[i])
               {
                  if(enc_check_dotatom(&res[i]))  { state = 1;  break; }
                  ++i;
               }
               if(!state)
               {
                  if(!i || '@' != res[i])  { state = 1; }
                  /* Skip '@' and verify that there is more data */
                  else  { if(!res[++i])  { state = 1; } }
               }
               if(!state)
               {
                  while(res[i])
                  {
                     /*
                      * Relaxed check for 'domain-literal'
                      * Check for printable ASCII only
                      */
                     if('[' == res[i])
                     {
                        if(enc_ascii_check_printable(&res[i]))  { state = 1; }
                        break;
                     }
                     /* Check 'dot-atom' */
                     if(enc_check_dotatom(&res[i]))
                     {
                        state = 1;
                        break;
                     }
                     ++i;
                  }
               }
               if(state)
               {
                  /* Invalid address format */
                  posix_free((void*) res);
                  res = NULL;
               }
            }
         }
      }
   }

   /* Check for error */
   if(NULL == res)  { PRINT_ERROR("Invalid e-mail address"); }

   return(res);
}


/* ========================================================================== */
/*! \brief Verify ASCII encoding
 *
 * \param[in] s  String to verify
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  enc_ascii_check(const char*  s)
{
   int  res = 0;
   size_t  i = 0;
   int  c;

   /* Assignment in the truth expression is intended */
   while((c = (int) s[i++]))
   {
      if(!(0 <= c && 127 >= c))  { res = -1; }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Check for ASCII alphabetic characters
 *
 * \param[in] s  Pointer to single character
 *
 * Locale independent check based on ASCII.
 *
 * \return
 * - 0 if \e s is an alphabetic character
 * - Negative value if \e s is not an alphabetic character
 */

int  enc_ascii_check_alpha(const char*  s)
{
   int  res = 0;
   int  c = (int) *s;

   if(!(65 <= c && 90 >= c) && !(97 <= c && 122 >= c))  { res = -1; }

   return(res);
}


/* ========================================================================== */
/*! \brief Check for ASCII digit characters
 *
 * \param[in] s  Pointer to single character
 *
 * Locale independent check based on ASCII.
 *
 * \return
 * - 0 if \e s is a digit character
 * - Negative value if \e s is not a digit character
 */

int  enc_ascii_check_digit(const char*  s)
{
   int  res = 0;
   int  c = (int) *s;

   if(!(48 <= c && 57 >= c))  { res = -1; }

   return(res);
}


/* ========================================================================== */
/*! \brief Check for printable ASCII characters
 *
 * \param[in] s  String to check
 *
 * HT (9) and SPACE (32, 0x20) inside \e s are treated as "printable" to make
 * this function suitable to check header field bodies according to RFC 5322.
 *
 * \note
 * The function \ref enc_ascii_convert_to_printable() can be used on error.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  enc_ascii_check_printable(const char*  s)
{
   int  res = 0;
   size_t  i = 0;
   int  c;

   /* Assignment in the truth expression is intended */
   while((c = (int) s[i++]))
   {
      if(!(9 == c || (32 <= c && 126 >= c)))  { res = -1; }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Convert to printable ASCII format
 *
 * \param[in] s  String to convert
 *
 * This function should be used to repair a string in-place after the function
 * \ref enc_ascii_check_printable() have reported an error.
 *
 * Every invalid byte is replaced with '?'.
 */

void  enc_ascii_convert_to_printable(char*  s)
{
   size_t  i = 0;
   int  c;

   while(s[i])
   {
      c = (int) s[i];
      if(!(9 == c || (32 <= c && 126 >= c)))  { s[i] = '?'; }
      ++i;
   }
}


/* ========================================================================== */
/*! \brief Convert body of distribution header field
 *
 * \param[in] s  String with unfolded body to convert
 *
 * This function process \e s in-place. The result will always be shorter or
 * same length as the original data.
 *
 * Every element of \c dist-list that contains invalid characters is removed.
 */

void  enc_ascii_convert_distribution(char*  s)
{
   size_t  i;
   size_t  len;
   int  c;
   size_t  start = 0;
   int  error = 0;
   char*  p;

   /* Remove whitespace */
   i = 0;
   while(s[i])
   {
      c = (int) s[i];
      if(9 == c || 32 == c)
      {
         len = strlen(&s[i + (size_t) 1]);
         /* Move including NUL termination */
         memmove((void*) &s[i], (void*) &s[i + (size_t) 1], ++len);
      }
      else  { ++i; }
   }

   /* Check content */
   i = 0;
   while(s[i])
   {
      /* Check for alphanumeric characters */
      if(enc_ascii_check_alpha(&s[i]) && enc_ascii_check_digit(&s[i]))
      {
         /* No => Check first character of 'dist-name' token */
         if(!start && !i)  { error = 1; }
         else if(start && start + (size_t) 1 == i)  { error = 1; }
         else
         {
            /* Not first => Check other characters of 'dist-name' token */
            if('+' != s[i] && '-' != s[i] && '_' != s[i] && ',' != s[i])
            {
               error = 1;
            }
         }
         /* Check for separator between entries */
         if(!error && ',' == s[i])
         {
            start = i;
            if(!s[i + (size_t) 1])  { error = 1; }
         }
      }
      /* Check for error */
      if(error)
      {
         PRINT_ERROR("Invalid entry in distribution list removed");
         p = strchr(&s[i + (size_t) 1], (int) ',');
         i = start;
         if(NULL == p)  { s[i] = 0; }
         else
         {
            /* Remove invalid entry */
            if(!start)  { p += 1; }  /* No separator before first entry */
            len = strlen(p);
            /* Move including NUL termination */
            memmove((void*) &s[start], (void*) p, ++len);
         }
         error = 0;
      }
      else  { ++i; }
   }
}


/* ========================================================================== */
/*! \brief Verify UTF-8 encoding
 *
 * \param[in] s  String to verify
 *
 * \attention
 * Read chapter 10 of RFC 3629 for UTF-8 security considerations.
 *
 * According to RFC 3629 the following rules are applied:
 * - Character code points beyond 0x10FFFF are invalid => We reject them.
 * - Only the shortest possible code sequence is allowed => We verify this.
 * - Surrogate character code points are invalid for UTF-8 => We reject them.
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  enc_uc_check_utf8(const char*  s)
{
   /* Enable additional check for surrogate-pairs */
   return enc_uc_check_cesu8(s, 1);
}


/* ========================================================================== */
/*! \brief Repair UTF-8 encoding
 *
 * \param[in] s  String to repair
 *
 * Invalid UTF-8 sequences and invalid codepoints are replaced with U+FFFD.
 *
 * \return
 * - Pointer to new memory block on success
 * - \c NULL on error
 */

const char*  enc_uc_repair_utf8(const char*  s)
{
   char*  res = (char*) posix_malloc(strlen(s) * (size_t) 3);
   const char  rc[3] = { (char) 0xEF, (char) 0xBF, (char) 0xBD };
   size_t  i = 0;
   size_t  ri = 0;
   int  c;
   int  multibyte = 0;
   size_t  len = 0;
   size_t  remaining = 0;
   unsigned long int  mbc = 0;
   int  error = 0;

   if(NULL != res)
   {
      /* Assignment in truth expression is intended */
      while((c = (int) s[i++]))
      {
         /* Resync after error */
         if(error)
         {
            if((c & 0xC0) == 0x80)  { continue; }
            else  { multibyte = 0; }
         }
         /* Verify singlebyte character */
         if(!multibyte)
         {
            if(!(0 <= c && 127 >= c))  { multibyte = 1; }
            else  { res[ri++] = (char) c; }
            /* Reset state machine */
            remaining = 0;
            mbc = 0;
            error = 0;
         }
         /* Verify multibyte character */
         if(multibyte)
         {
            if(!remaining)
            {
               if((c & 0xE0) == 0xC0)  { len = 2; }
               else if((c & 0xF0) == 0xE0)  { len = 3; }
               else if((c & 0xF8) == 0xF0)  { len = 4; }
               else
               {
                  /* Invalid start of code sequence in UTF-8 data */
                  res[ri++] = rc[0];  res[ri++] = rc[1];  res[ri++] = rc[2];
                  error = 1;
               }
               switch(len)
               {
                  case 2:  mbc |= (unsigned long int) (c & 0x1F) << 6;  break;
                  case 3:  mbc |= (unsigned long int) (c & 0x0F) << 12;  break;
                  case 4:  mbc |= (unsigned long int) (c & 0x07) << 18;  break;
                  default:  break;
               }
               remaining = len - (size_t) 1;
            }
            else
            {
               if((c & 0xC0) != 0x80)
               {
                  /* Invalid continuation character in UTF-8 sequence */
                  res[ri++] = rc[0];  res[ri++] = rc[1];  res[ri++] = rc[2];
                  if(0 <= c && 127 >= c)  { res[ri++] = (char) c; }
                  error = 1;
               }
               else
               {
                  --remaining;
                  mbc |= (unsigned long int) (c & 0x3F) << remaining * (size_t) 6;
               }
               if(!remaining && !error)
               {
                  /* Verify character code */
                  switch(len)
                  {
                     case 2:
                     {
                        if(0x000080UL > mbc)
                        {
                           /* Invalid UTF-8 2-byte code sequence */
                           res[ri++] = rc[0];
                           res[ri++] = rc[1];
                           res[ri++] = rc[2];
                           error = 1;
                        }
                        else
                        {
                           res[ri++] = s[i - (size_t) 2];
                           res[ri++] = s[i - (size_t) 1];
                        }
                        break;
                     }
                     case 3:
                     {
                        if(0x000800UL > mbc
                           || (0x00D800UL <= mbc && 0x00DFFFUL >= mbc))
                        {
                           /* Invalid UTF-8 3-byte code sequence */
                           res[ri++] = rc[0];
                           res[ri++] = rc[1];
                           res[ri++] = rc[2];
                           error = 1;
                        }
                        else
                        {
                           res[ri++] = s[i - (size_t) 3];
                           res[ri++] = s[i - (size_t) 2];
                           res[ri++] = s[i - (size_t) 1];
                        }
                        break;
                     }
                     case 4:
                     {
                        if(0x010000UL > mbc || 0x10FFFFUL < mbc)
                        {
                           /* Invalid UTF-8 4-byte code sequence */
                           res[ri++] = rc[0];
                           res[ri++] = rc[1];
                           res[ri++] = rc[2];
                           error = 1;
                        }
                        else
                        {
                           res[ri++] = s[i - (size_t) 4];
                           res[ri++] = s[i - (size_t) 3];
                           res[ri++] = s[i - (size_t) 2];
                           res[ri++] = s[i - (size_t) 1];
                        }
                        break;
                     }
                     default:
                     {
                        PRINT_ERROR("Bug in UTF-8 repair state machine");
                        posix_free((void*) res);
                        res = NULL;
                        break;
                     }
                  }
                  /* Code sequence complete */
                  multibyte = 0;
               }
            }
         }
      }
   }

   /* Check for error */
   if(NULL != res)
   {
      /* Terminate new string */
      res[ri] = 0;
      /* Verify again */
      if(enc_uc_check_utf8(res))
      {
         PRINT_ERROR("UTF-8 data still invalid after repair (bug)");
         posix_free((void*) res);
         res = NULL;
      }
      else  { PRINT_ERROR("UTF-8 data repaired"); }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Create wildmat pattern array
 *
 * \param[out] obj  Pointer to wildmat pattern array
 * \param[in]  wm   RFC 3977 conformant wildmat
 *
 * This function splits a RFC 3977 conformant \c wildmat into its elements of
 * type \c wildmat-pattern . Every \c wildmat-pattern is converted to a POSIX
 * extended regular expression and stored together with a negation flag (that
 * is set if the \c wildmat-pattern was preceded by an exclamation mark) in
 * the array \e obj .
 *
 * On success the caller is responsible to free the memoy allocated for the
 * resulting array with the function \e enc_destory_wildmat() .
 *
 * \attention
 * If the wildmat \e wm contains Unicode data, it must be normalized to NFC by
 * the caller.
 *
 * \return
 * - Number of patterns in the object on success
 * - Negative value on error (\c NULL was written to \e obj)
 */

int  enc_create_wildmat(struct enc_wm_pattern**  obj, const char*  wm)
{
   int  res = 0;
   size_t  len;
   size_t  i = 0;
   char*  buf = NULL;
   size_t  bi = 0;
   int  error = 0;
   int  negate = 0;
   int  store = 0;
   int  eod = 0;
   struct enc_wm_pattern*  p;
   size_t  obj_len = 0;

   *obj = NULL;

   /* Wildmat must have valid UTF-8 encoding */
   if(!enc_uc_check_utf8(wm))
   {
      /* Check for invalid characters (backslash and brackets) */
      if(NULL == strpbrk(wm, "\x5C[]"))
      {
         /* Extract wildmat-pattern elements */
         do
         {
            store = 0;
            negate = 0;
            /* Allocate ERE buffer for next pattern */
            len = strlen(&wm[i]);
            /* Required buffer size (see below): Triple + 2 + NUL */
            buf = (char*) posix_malloc(len * (size_t) 3 + (size_t) 3);
            if(NULL == buf)  { break; }
            else
            {
               bi = 0;
               buf[bi++] = '^';
               while(!store)
               {
                  /* Check for EOD */
                  if(!wm[i])
                  {
                     if((size_t) 1 < bi)  { store = 1; }
                     eod = 1;
                     break;
                  }
                  /* Check for (remaining) special character */
                  if(NULL != strchr(".()*+?{|^$", (int) wm[i]))
                  {
                     switch((int) wm[i])
                     {
                        /* Match arbitrary single UTF-8 codepoint (not octet) */
                        case (int) ')':
                        {
                           /* Replace with "[)]" (*3) */
                           buf[bi++] = '[';
                           buf[bi++] = ')';
                           buf[bi++] = ']';
                           break;
                        }
                        case (int) '*':
                        {
                           /* Replace with ".*" (*2) */
                           buf[bi++] = '.';
                           buf[bi++] = '*';
                           break;
                        }
                        case (int) '?':
                        {
                           /* Replace with dot (*1) */
                           buf[bi++] = '.';
                           break;
                        }
                        default:
                        {
                           /* Escape special character with backslash (*2) */
                           buf[bi++] = 0x5C;
                           buf[bi++] = wm[i];
                           break;
                        }
                     }
                  }
                  else
                  {
                     switch((int) wm[i])
                     {
                        case 0x09:
                        case 0x20:
                        {
                           /* Ignore whitespace */
                           break;
                        }
                        case (int) '!':
                        {
                           negate = 1;
                           break;
                        }
                        case (int) ',':
                        {
                           store = 1;
                           break;
                        }
                        default:
                        {
                           /* Ordinary character */
                           buf[bi++] = wm[i];
                           break;
                        }
                     }
                  }
                  ++i;
               }
               /* Store element into object */
               if(!store)  { posix_free((void*) buf); }
               else
               {
                  if(INT_MAX == res)  { error = 1; }
                  else
                  {
                     buf[bi++] = '$';
                     buf[bi] = 0;
                     /* printf("Pattern converted to ERE: %s\n", buf); */
                     obj_len += sizeof(struct enc_wm_pattern);
                     p = (struct enc_wm_pattern*) posix_realloc(*obj, obj_len);
                     if(NULL == p)  { error = 1; }
                     else
                     {
                        *obj = p;
                        (*obj)[res].negate = negate;
                        (*obj)[res].ere = buf;
                        ++res;
                     }
                  }
                  if(error)
                  {
                     posix_free((void*) buf);
                     break;
                  }
               }
            }
         }
         while(!eod);
      }
   }

   /* Check for error */
   if(error || !eod || 0 >= res)
   {
      PRINT_ERROR("Failed to convert RFC 3977 wildmat");
      enc_destroy_wildmat(obj, res);
      res = -1;
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Destroy wildmat pattern array
 *
 * \param[in,out] obj  Pointer to wildmat pattern array
 * \param[in]     num  Number of elements in array
 *
 * \c NULL is written to the location pointed to by \e obj after releasing the
 * memory allocated for the array.
 */

void  enc_destroy_wildmat(struct enc_wm_pattern**  obj, int  num)
{
   int  i;

   if(NULL != obj && NULL != *obj)
   {
      for(i = 0; i < num; ++i)
      {
         posix_free((void*) (*obj)[i].ere);
      }
      posix_free((void*) *obj);
      *obj = NULL;
   }
}


/* ========================================================================== */
/*! \brief Convert from canonical (RFC 822) to local (POSIX) form
 *
 * \param[in] s    String to convert
 * \param[in] rcr  Replace invalid CR control characters if nonzero
 * \param[in] rlf  Replace invalid LF control characters if nonzero
 *
 * According to RFC 822 and RFC 2049 this function accepts plain text article
 * content in canonical form and convert the CRLF line breaks to local (POSIX,
 * single LF) form.
 *
 * \attention
 * Single CR and LF control characters (not part of a CRLF sequence) are
 * forbidden in canonical format of text by RFC 2045 and RFC 2046.
 * Default behaviour is to preserve single CR and LF control characters.
 * The Unicode codepoint defined by \c ENC_RC can be inserted as replacement
 * for CR or/and LF by setting \e rcr or/and \e rlf respectively to a nonzero
 * value.
 *
 * On success the caller is responsible to free the allocated memory.
 *
 * \return
 * - Pointer to decoded data (a new memory block was allocated)
 * - NULL on error
 */

const char*  enc_convert_canonical_to_posix(const char*  s, int  rcr, int  rlf)
{
   const char*  res = NULL;
   size_t  i = 0;
   char*  buf = NULL;
   size_t  len = 0;
   size_t  bi = 0;
   char*  p;
   size_t  escr = 0;
   size_t  eslf = 0;
   long int  rc_ucp = ENC_RC;
   size_t  di;

   if(NULL != s)
   {
      /* Check for empty string and accept it */
      if(!s[0])
      {
         p = (char*) posix_malloc((size_t) 1);
         if(NULL != p)  { p[0] = 0;  res = p; }
      }
      else
      {
         while(s[i])
         {
            /*
             * Reserve space for one Unicode codepoint in UTF-8 transformation
             * format (4 octets in worst case).
             * At least 1 octet must stay available for NUL termination.
             */
            if(bi + (size_t) 4 + (size_t) 1 >= len)
            {
               /* Allocate more memory in exponentially increasing chunks */
               if(!len)  { len = 64; }
               p = (char*) posix_realloc((void*) buf, len *= (size_t) 2);
               if(NULL == p)  { posix_free((void*) buf);  buf = NULL;  break; }
               else  { buf = p; }
            }
            /* Check for end of line (CRLF) */
            if(bi && i && 0x0A == (int) s[i] && 0x0D == (int) s[i - (size_t) 1])
            {
               /* Yes => Replace the CR with LF and don't increment position */
               buf[bi - (size_t) 1] = 0x0A;
            }
            else if(i && 0x0A != (int) s[i] && 0x0D == (int) s[i - (size_t) 1])
            {
               /* Single CR character (not part of a CRLF sequence) detected */
               ++escr;
               if(rcr)
               {
                  --bi;
                  di = 1;
                  enc_uc_encode_utf8(buf, &bi, &rc_ucp, &di);
               }
               buf[bi++] = s[i];
            }
            else if(0x0A == (int) s[i])
            {
               /* Single LF character (not part of a CRLF sequence) detected */
               ++eslf;
               if(rlf)
               {
                  di = 1;
                  enc_uc_encode_utf8(buf, &bi, &rc_ucp, &di);
               }
               else
               {
                  buf[bi++] = s[i];
               }
            }
            else { buf[bi++] = s[i]; }
            ++i;
         }
         if(NULL != buf)
         {
            buf[bi] = 0;
            res = buf;
            /* Print stored errors */
            if(escr)
            {
               /* Print error message only once */
               PRINT_ERROR("Invalid CR control character(s) detected"
                           " while decoding canonical format");
            }
            if(eslf)
            {
               /* Print error message only once */
               PRINT_ERROR("Invalid LF control character(s) detected"
                           " while decoding canonical format");
            }
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Convert from local (POSIX) to canonical (RFC 822) form
 *
 * \param[in] s  String to convert
 *
 * 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 (CRLF) form.
 *
 * According to RFC 2045 and RFC 2046 single CR characters are deleted.
 *
 * On success the caller is responsible to free the allocated memory.
 *
 * \return
 * - Pointer to decoded data (a new memory block was allocated)
 * - NULL on error
 */

const char*  enc_convert_posix_to_canonical(const char*  s)
{
   const char*  res = NULL;
   size_t  i = 0;
   char*  buf = NULL;
   size_t  len = 0;
   size_t  bi = 0;
   char*  p;

   if(NULL != s)
   {
      /* Check for empty string and accept it */
      if(!s[0])
      {
         p = (char*) posix_malloc((size_t) 1);
         if(NULL != p)  { p[0] = 0;  res = p; }
      }
      else
      {
         while(s[i])
         {
            /* At least 3 octets must stay available for CR + LF + NUL */
            if(bi + (size_t) 4 >= len)
            {
               /* Allocate more memory in exponentially increasing chunks */
               if(!len)  { len = 64; }
               p = (char*) posix_realloc((void*) buf, len *= (size_t) 2);
               if(NULL == p)  { posix_free((void*) buf);  buf = NULL;  break; }
               else  { buf = p; }
            }
            /* Check for end of line (LF) */
            if(0x0A == (int) s[i])
            {
               /* Yes => Add a CR before the LF */
               buf[bi++] = 0x0D;
               buf[bi++] = 0x0A;
            }
            else if(0x0D == (int) s[i])
            {
               PRINT_ERROR("Invalid CR control character deleted"
                           " while converting to canonical format");
            }
            else { buf[bi++] = s[i]; }
            ++i;
         }
         if(NULL != buf)
         {
            /* Ensure that last line of nonempty result ends with CRLF */
            if(bi)
            {
               if(0x0A != (int) buf[bi - (size_t) 1])
               {
                  /* Append CR+LF */
                  buf[bi++] = 0x0D;
                  buf[bi++] = 0x0A;
               }
            }
            /* Add termination */
            buf[bi] = 0;
            res = buf;
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Convert string from supported character set to Unicode (UTF-8 NFC)
 *
 * \param[in] charset  Character set of string \e s
 * \param[in] s        String to convert
 *
 * According to RFC 2049 the following rules are applied:
 * - For all character sets from the ISO 8859 family that are not supported,
 *   at least the ASCII characters must be decoded correctly
 *   => We decode all non ASCII characters as "?" in this case.
 *
 * According to RFC 3629 the following rules are applied:
 * - If the input data is already UTF-8 is is not allowed to accept it
 *   unchecked. It is mandatory to check the validity of the encoding
 *   => We do so.
 *
 * \note
 * Some control characters that may cause problems are removed.
 *
 * \return
 * - Pointer to decoded Unicode data (UTF-8 encoded with NFC normalization)
 *   If the result is not equal to \e s , a new memory block was allocated
 * - NULL on error (Original memory block for \e s is still allocated)
 */

const char*  enc_convert_to_utf8_nfc(enum enc_mime_cs  charset, const char*  s)
{
   const char*  res = NULL;
   char*  p;
   const char*  tmp;
   size_t  len;
   size_t  i;
   size_t  ii;
   long int  ucp;
   long int  rc_ucp = ENC_RC;
   char  rc_utf8[5] = { 0 };
   int  cc_flag = 0;  /* Flag indicating unwanted control characters */
   size_t  di;

   switch(charset)
   {
      case ENC_CS_ISO8859_X:
      {
         PRINT_ERROR("Convert unsupported ISO 8859 character set as US-ASCII");
         /* No break here is intended */
      }
      case ENC_CS_ASCII:
      {
         len = strlen(s);
         p = (char*) posix_malloc(++len);
         if(NULL == p)  { break; }
         for(i = 0; i < len; ++i)
         {
            p[i] = s[i];
            if((unsigned char) 127 < (unsigned char) p[i])  { p[i] = '?'; }
         }
         res = p;
         break;
      }
      case ENC_CS_ISO8859_1:
      case ENC_CS_ISO8859_2:
      case ENC_CS_ISO8859_3:
      case ENC_CS_ISO8859_4:
      case ENC_CS_ISO8859_5:
      case ENC_CS_ISO8859_6:
      case ENC_CS_ISO8859_7:
      case ENC_CS_ISO8859_8:
      case ENC_CS_ISO8859_9:
      case ENC_CS_ISO8859_10:
      case ENC_CS_ISO8859_11:
      case ENC_CS_ISO8859_13:
      case ENC_CS_ISO8859_14:
      case ENC_CS_ISO8859_15:
      case ENC_CS_ISO8859_16:
      case ENC_CS_MACINTOSH:
      case ENC_CS_KOI8R:
      case ENC_CS_KOI8U:
      case ENC_CS_WINDOWS_1250:
      case ENC_CS_WINDOWS_1251:
      case ENC_CS_WINDOWS_1252:
      case ENC_CS_WINDOWS_1253:
      case ENC_CS_WINDOWS_1254:
      case ENC_CS_WINDOWS_1255:
      case ENC_CS_WINDOWS_1256:
      case ENC_CS_WINDOWS_1257:
      case ENC_CS_WINDOWS_1258:
      case ENC_CS_IBM437:
      case ENC_CS_IBM775:
      case ENC_CS_IBM850:
      case ENC_CS_IBM852:
      case ENC_CS_IBM858:
      {
         res = enc_8bit_convert_to_utf8(charset, s);
         break;
      }
      case ENC_CS_ISO2022_JP:
      {
         res = enc_iso2022jp_convert_to_utf8(s);
         break;
      }
      case ENC_CS_UTF_7:
      {
         res = enc_uc_convert_utf7_to_utf8(s);
         break;
      }
      case ENC_CS_CESU_8:
      {
         res = enc_uc_convert_cesu8_to_utf8(s);
         break;
      }
      case ENC_CS_UTF_8:
      {
         res = s;
         break;
      }
      default:
      {
         /* Not supported */
         res = NULL;
         break;
      }
   }

   /* Check encoding */
   if(NULL != res)
   {
      if(enc_uc_check_utf8(res))
      {
         /* Encoding is invalid */
         if(ENC_CS_UTF_8 != charset && ENC_CS_CESU_8 != charset
            && ENC_CS_UTF_7 != charset)
         {
            PRINT_ERROR("Invalid UTF-8 encoding detected");
         }
         /* Repair encoding */
         tmp = enc_uc_repair_utf8(res);
         if(res != tmp && res != s)  { posix_free((void*) res); }
         res = tmp;
      }
   }

   /* Normalize to NFC */
   if(NULL != res)
   {
      tmp = enc_uc_normalize_to_nfc(res);
      if(res != tmp && res != s)  { posix_free((void*) res); }
      res = tmp;
   }

   /* Remove unwanted control characters */
   if(NULL != res)
   {
      i = 0;
      while(1)
      {
         ucp = enc_uc_decode_utf8(res, &i);
         if(-1L == ucp)  { break; }
         if(enc_uc_check_control(ucp))  { cc_flag = 1;  break; }
      }
      if(cc_flag)
      {
         /* Unwanted control characters found */
         len = strlen(res);
         di = 1;
         i = 0;
         enc_uc_encode_utf8(rc_utf8, &i, &rc_ucp, &di);
         rc_utf8[i] = 0;
         len *= strlen(rc_utf8);
         p = (char*) posix_malloc(++len);
         if(NULL == p)
         {
            if(s != res)  { posix_free((void*) res); }
            res = NULL;
         }
         else
         {
            i = 0;  ii = 0;
            while(1)
            {
               ucp = enc_uc_decode_utf8(res, &i);
               if(-1L == ucp)  { break; }
               if(enc_uc_check_control(ucp))
               {
                  /* Replace them */
                  di = 1;
                  enc_uc_encode_utf8(p, &ii, &rc_ucp, &di);
               }
               else
               {
                  di = 1;
                  enc_uc_encode_utf8(p, &ii, &ucp, &di);
               }
            }
            p[ii] = 0;
            if(s != res)  { posix_free((void*) res); }
            res = p;
         }
         PRINT_ERROR("Unwanted control characters detected and replaced");
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Convert string from Unicode (UTF-8 NFC) to an 8bit character set
 *
 * \param[out] charset  Pointer to character set of result (or \c NULL)
 * \param[in]  s        Unicode string to convert in UTF-8 NFC format
 * \param[out] cs_iana  Pointer to IANA charset name of result (or \c NULL)
 *
 * \attention
 * Ensure that the string \e s is valid UTF-8 and normalized to NFC. Otherwise
 * this function will not work as expected.
 *
 * According to RFC 2046 the following rules are applied:
 * - In general, composition software should always use the "lowest common
 *   denominator" character set possible
 *   => We do so by preferring the widely supported ISO 8859-1 character set.
 *
 * \note
 * If this function supports more character sets in the future, ISO 8859-1 must
 * always stay the preferred one (because this is our fallback locale character
 * set to allow the use of POSIX regular expressions without Unicode support
 * from the system).
 *
 * If \c NULL is passed as parameter \e charset or \e cs_iana , this indicates
 * that the caller is not interested in this information. The corresponding
 * data is discarded in this case.
 *
 * \return
 * - Pointer to encoded data (the character set is written to \e charset)
 *   If the result is not equal to \e s , a new memory block was allocated
 * - NULL on error (Original memory block for \e s is still allocated)
 *   Nothing is written to \e charset and \e cs_iana in this case
 */

const char*  enc_convert_to_8bit(enum enc_mime_cs*  charset, const char*  s,
                                 const char**  cs_iana)
{
   const char*  res = NULL;
   size_t  i = 0;
   size_t  ii = 0;
   long int  ucp = 0;
   char*  p = NULL;
   size_t  len;
   int  error = 0;

   /*
    * Allocate target buffer with same size as source buffer.
    * This is always sufficient for every 8bit character set.
    */
   len = strlen(s);
   p = (char*) posix_malloc(++len);
   if(NULL != p)
   {
      while(1)
      {
         ucp = enc_uc_decode_utf8(s, &i);
         if(-1L == ucp)  { break; }
         /* ISO 8859-1 is mapped 1:1 into the Unicode codepoint space */
         if(256L <= ucp)  { error = 1;  break; }
         else  { p[ii++] = (char) (unsigned char) ucp; }
      }
      /* Check for error */
      if(error)  { posix_free((void*) p); }
      else
      {
         p[ii] = 0;
         res = p;
         if(NULL != charset)  { *charset = ENC_CS_ISO8859_1; }
         if(NULL != cs_iana)  { *cs_iana = "ISO-8859-1"; }
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Encode header field body using MIME \c encoded-word tokens
 *
 * This function use quoted-printable encoding.
 *
 * \param[out] r   Pointer to result string pointer
 * \param[in]  b   Header field body that contains potential Unicode data
 * \param[in]  pl  Length of header field prefix (Length limit: 25)
 *
 * The header field body \e b must be verified by the caller to be valid UTF-8
 * (this function will do the normalization to NFC).
 * The CRLF termination must be removed before calling this function.
 *
 * The length \e pl must include the header field name, the colon and any
 * potential white space not included in \e b .
 *
 * According to RFC 5536 the following rules are applied:
 * - A header field line is not allowed to be empty
 *   => The header field is never folded immediately after the name separator.
 * - Lines are not allowed to contain more than 1000 characters
 *   => We respect this by rejecting words that are longer than 998 characters.
 *
 * According to RFC 2047 the following rules are applied:
 * - White space between encoded-words is semantically ignored
 *   => A single space between encoded-words is included in the trailing word,
 *      additional LWSP characters are included into the leading word.
 * - A header line containing encoded-words must be no longer than 76 characters
 *   => We fold before this limit.
 * - If folding is required, each encoded-word must contain an integral number
 *   of characters and must be self-contained
 *   => We only split between Unicode combining character sequences when using
 *      UTF-8
 *      (between grapheme clusters would be better, but is not supported yet)
 * - If there is more than one character set that can represent the 8-bit
 *   content of an encoded-word, ISO 8859 should be preferred
 *   => We do so if the required ISO 8859 encoder is available
 *      (can be disabled with the \c force_unicode option in configfile).
 * - If encoded-word is not used because of 8-bit data, US-ASCII should be used
 *   => We do so
 *      (can be disabled with the \c force_unicode option in configfile).
 *
 * According to RFC 5198 the following rules are applied:
 * - It's recommended to use NFC normalization in general Internet text messages
 *   => We do so.
 *
 * On success, the address of the result buffer is written to the location
 * pointed to by \e r (this may be the same as \e b if there is nothing to do).
 * The caller is responsible to free the potentially allocated memory.
 * On error \c NULL is written to the location pointed to by \e r .
 *
 * \return
 * - 0 on success if a new memory block was allocated
 * - 1 on success if there was nothing to encode and no memory was allocated
 * - -1 on error
 */

int  enc_mime_word_encode(const char**  r, const char*  b, size_t  pl)
{
   static const char  error_msg[] = "[Error]";
   static const char  folding[] = "\n ";  /* Line break must be in POSIX form */
   int  res = 0;
   char*  rbuf = NULL;
   size_t  rbuf_len = 0;
   size_t  ri = 0;
   const char*  body = NULL;
   const char*  body_tmp = NULL;
   const char*  cs_iana = "UTF-8";
   enum enc_mime_cs  cs = ENC_CS_UTF_8;
   size_t  start = 0;
   size_t  end = 0;
   size_t  i = 0;
   size_t  ii;
   size_t  iii;
   int  enc_flag = 0;
   int  enc_last = 0;
   int  enc_split = 0;
   char  enc_word[1001];  /* sizeof(folding) + 998 + NUL */
   size_t  ei;
   size_t  word_len;
   unsigned int  dh, dl;
   char*  p;
   size_t  rem = 0;
   int  init = 1;  /* Flag indicating initial word */
   int  first = 1;  /* Flag indicating first line of header field */
   int  uc_split;  /* Flag indicating Unicode must be split here */
#if !ENC_MIME_HEADER_FOLD_ASCII_LINES
   int  no_ec = 1;  /* Flag indicating line contains no encoded-words */
#endif  /* ENC_MIME_HEADER_FOLD_ASCII_LINES */
   long int  ucp;  /* Unicode code point */
   struct uc_cdc  cdc;  /* Unicode canonical decomposition data */
   size_t  gcpsl;  /* Unicode combing character sequence length */
   int  eod;  /* End of data */

   /* Check parameters */
   if((size_t) 25 < pl)
   {
      PRINT_ERROR("MIME: Header field name too long");
      res = -1;
   }
   else
   {
      /* Calculate remaining bytes for folding */
      rem = (size_t) 76 - pl;
      /*
       * Check whether header field body contains only printable ASCII
       * and no "=?" or "?=" (to be more friendly) sequences
       */
      if(!enc_ascii_check_printable(b)
         && NULL == strstr(b, "=?") && NULL == strstr(b, "?="))
      {
         /* Nothing to do => Data can be used "as is" */
         res = 1;
      }
      else
      {
         /* Check Unicode */
         if(enc_uc_check_utf8(b))
         {
            /* Invalid Unicode */
            PRINT_ERROR("MIME: Encoding of header field failed");
            p = (char*) posix_malloc(strlen(error_msg) + (size_t) 1);
            if(NULL != p)  { strcpy(p, error_msg); }
            body_tmp = p;
         }
         else
         {
            /* Normalize Unicode */
            body_tmp = enc_uc_normalize_to_nfc(b);
         }
         if(NULL == body_tmp)  { res = -1; }
      }
   }

   /* Check for error */
   if(!res)
   {
      /* Check whether user has forced Unicode */
      if (config[CONF_FORCE_UNICODE].val.i)  { body = body_tmp; }
      else
      {
         /* Convert body to target character set */
         body = enc_convert_to_8bit(&cs, body_tmp, &cs_iana);
         if(NULL == body)  { body = body_tmp; }
         else
         {
            /* Check for 7bit data */
            if (0 == enc_ascii_check(body))
            {
               cs = ENC_CS_ASCII;
               cs_iana = "US-ASCII";
            }
         }
      }
      /* Split body into words using SP delimiter */
      do
      {
         end = i++;
         if(!body[i] || ' ' == body[i])
         {
            /* Check for 2*LWSP */
            if(body[i])
            {
               if(' ' == body[i + (size_t) 1]
                  || (char) 0x09 == body[i + (size_t) 1])
               {
                  continue;
               }
            }
            /* Check whether word needs encoding */
            enc_last = enc_flag;  enc_flag = 0;
            ei = 0;
            for(ii = start; ii <= end; ++ii)
            {
               enc_word[ei++] = body[ii];
               if(128U <= (unsigned int) body[ii])  { enc_flag = 1;  break; }
               if('=' == (unsigned int) body[ii])
               {
                  if((ii < end && '?' == body[ii + (size_t) 1])
                     || (ii > start && '?' == body[ii - (size_t) 1]))
                  {
                     enc_flag = 1;
                     break;
                  }
               }
            }
            if(enc_split)  { enc_flag = 1; }
            if(enc_flag)
            {
               /* Create MIME encoded word using quoted printable encoding */
#if !ENC_MIME_HEADER_FOLD_ASCII_LINES
               no_ec = 0;
#endif  /* ENC_MIME_HEADER_FOLD_ASCII_LINES */
               strcpy(enc_word, "=?");
               strcat(enc_word, cs_iana);
               strcat(enc_word, "?Q?");
               uc_split = 0;
               if(enc_last && !enc_split)
               {
                  /* The space between encoded words is not semantical */
                  strcat(enc_word, "_");
               }
               ei = strlen(enc_word);
               for(ii = start; ii <= end; ++ii)
               {
                  /* Check for start of UTF-8 sequence */
                  if(ENC_CS_UTF_8 == cs && 0x80 != ((int) body[ii] & 0xC0))
                  {
                     /* Search for next starter */
                     eod = 0;
                     iii = 0;
                     while(!uc_split)
                     {
                        /* Count bytes as "=XX", even if encoded "as is" */
                        gcpsl = iii * (size_t) 3;
                        /* Check for end of data */
                        if(!body[ii + iii])  { eod = 1; }
                        else
                        {
                           /* Decode UTF-8 sequence for codepoint */
                           if(!body[ii + iii])  { break; }
                           ucp = enc_uc_decode_utf8(&body[ii], &iii);
                           if(0L > ucp)
                           {
                              PRINT_ERROR("MIME: Decoding UCP failed");
                             break;
                           }
                           enc_uc_lookup_cdc(ucp, &cdc);
                           /* Check for starter */
                           if(!gcpsl)
                           {
                               /* If Starter => Skip */
                               if(!cdc.ccc)  { continue; }
                               /* Else abort */
                               else  { break; }
                           }
                        }
                        /*
                         * Check for next Unicode combining character sequence
                         * boundary
                         */
                        if(eod || !cdc.ccc)  /* Check eod first */
                        {
                           /* Combining character sequence boundary found */
                           /*
                            * Reserve space for encoded word prefix and suffix:
                            * "=?UTF-8?Q??="
                            * => 12 characters with the folding space
                            */
                           if((size_t) (75 - 12) < gcpsl)
                           {
                              /* Combining character sequence too long */
                              PRINT_ERROR("MIME: "
                                 "Combining character sequence too long");
                              /*
                               * Replace with '?' (U+FFFD is too large!)
                               * Maximum allowed length is one "=XX" triplet.
                               */
                              enc_word[ei++] = '=';
                              enc_word[ei++] = '3';
                              enc_word[ei++] = 'F';
                              ii += iii - (size_t) 1;
                              uc_split = 1;
                           }
                           /*
                            * Check for length limit
                            * Reserve 2 characters for closing "?="
                            * Special handling for first line with less space
                            */
                           else if(first && ((size_t) (rem - 2) - gcpsl < ei))
                           {
                              uc_split = 1;
                           }
                           else if((size_t) (75 - 2) - gcpsl < ei)
                           {
                              uc_split = 1;
                           }
                           break;
                        }
                     }
                  }
                  if(uc_split)  { /* Rewind current byte */ --ii; }
                  else
                  {
                     /* Check whether character can be encoded "as is" */
                     if( ('0' <= body[ii] && '9' >= body[ii])
                        || ('A' <= body[ii] && 'Z' >= body[ii])
                        || ('a' <= body[ii] && 'z' >= body[ii])
                        || '!' == body[ii] || '*' == body[ii] || '+' == body[ii]
                        || '-' == body[ii] || '/' == body[ii] )
                     {
                        /* Yes */
                        enc_word[ei++] = body[ii];
                     }
                     else
                     {
                        /* No => Encode with hexadecimal syntax */
                        enc_word[ei++] = '=';
                        dh = (unsigned int) (unsigned char) body[ii] / 16U;
                        if(10U > dh)  { enc_word[ei++] = (char) (48U + dh); }
                        else  { enc_word[ei++] = (char) (65U + dh - 10U); }
                        dl = (unsigned int) (unsigned char) body[ii] % 16U;
                        if(10U > dl)  { enc_word[ei++] = (char) (48U + dl); }
                        else  { enc_word[ei++] = (char) (65U + dl - 10U); }
                     }
                  }
                  /*
                   * Check for length limit
                   * Reserve 3 characters for next hexdecimal value
                   * Reserve 2 characters for closing "?="
                   */
                  if(uc_split || (size_t) (75 - 3 - 2) < ei)
                  {
                     /* Terminate normally if there are no more characters */
                     if(ii < end)
                     {
                        enc_split = 1;
                        /* Rewind index to process skipped data in next run */
                        i -= (end - ii);
                        --i;
                        break;
                     }
                  }
                  else  { enc_split = 0; }
               }
               /* End mark of encoded-word */
               enc_word[ei++] = '?';
               enc_word[ei++] = '=';
            }
            /* Terminate word */
            enc_word[ei] = 0;
            /* printf("Word: |%s|\n", enc_word); */
            /* One additional character for potential delimiting space */
            word_len = strlen(enc_word) + (size_t) 1;
            if((size_t) 998 < word_len)
            {
               PRINT_ERROR("MIME: Encoded-word too long");
               res = -1;
               break;
            }
            /* Fold header field if lines get too long otherwise */
            if(word_len && (word_len > rem)
#if !ENC_MIME_HEADER_FOLD_ASCII_LINES
               && !(no_ec && !enc_flag && (word_len < rem + (size_t) 922))
#endif  /* ENC_MIME_HEADER_FOLD_ASCII_LINES */
              )
            {
               /* Fold => This automatically creates SP delimiter */
               if(first)
               {
                  PRINT_ERROR("MIME: Encoded-word too long for first line");
                  res = -1;
                  break;
               }
               else if(word_len > rem)
               {
                  memmove((void*) &enc_word[strlen(folding)], (void*) enc_word,
                          word_len--);
                  /* Decrement because SP delimitier is part of folding mark */
                  memcpy((void*) enc_word, (void*) folding, strlen(folding));
                  word_len += strlen(folding);
                  rem = (size_t) 75;
               }
#if !ENC_MIME_HEADER_FOLD_ASCII_LINES
               /* Check whether last word was an encoded word */
               if(!enc_flag)  { no_ec = 1; }
#endif  /* ENC_MIME_HEADER_FOLD_ASCII_LINES */
            }
            else
            {
               /*
                * Prepend SP delimiter
                * Note that this delimiter is always syntactical, but not sematical
                * between two encoded words!
                */
               if(init)  { init = 0;  --word_len; }
               else
               {
                  memmove((void*) &enc_word[1], (void*) enc_word, word_len);
                  enc_word[0] = ' ';
               }
            }
            /* Allocate more memory in exponentially increasing chunks */
            /* Attention: Be prepared for large data (ASCII only lines) */
            while(ri + word_len >= rbuf_len)  /* One additional byte for NUL */
            {
               if(!rbuf_len)  { rbuf_len = 128; }
               p = posix_realloc((void*) rbuf, rbuf_len *= (size_t) 2);
               if(NULL == p) { res = -1;  break; }
               else  { rbuf = p; }
            }
            if(-1 == res)  { break; }
            /* Copy word to result buffer */
            memcpy((void*) &rbuf[ri], (void*) enc_word, word_len);
            ri += word_len;
            if(rem < word_len)  { rem = 0; }
            else  { rem -= word_len; }
            first = 0;
            /* Store new start index */
            start = i + (size_t) 1;
         }
      }
      while(body[i]);
   }
   if(body != body_tmp)  { posix_free((void*) body); }
   if(body_tmp != b)  { posix_free((void*) body_tmp); }
   /* Terminate result string */
   if(NULL != rbuf)  { rbuf[ri] = 0; }

   /* Check result */
   switch(res)
   {
      case 0:
      {
         *r = (const char*) rbuf;
         break;
      }
      case 1:
      {
         *r = b;
         break;
      }
      default:
      {
         posix_free((void*) rbuf);
         *r = NULL;
         break;
      }
   }
   /* if(0 <= res)  { printf("Result: %s\n", *r); } */

   return(res);
}


/* ========================================================================== */
/*! \brief Decode header field containing potential MIME \c encoded-word tokens
 *
 * \param[out] r  Pointer to result string pointer
 * \param[in]  b  Header field body that contains potential encoded-words
 *
 * The header field body \e b must be unfolded before calling this function.
 *
 * According to RFC 2047 the following rules are applied:
 * - An encoded-word is not allowed to be longer than 75 characters
 *   => We decode encoded-word of arbitrary length.
 * - An encoded-word not at the beginning can start after a 'linear-white-space'
 *   token => We resync the parser after every white space.
 * - Any amount of linear-space-white between 'encoded-word's must be ignored
 *   => We do so.
 * - The character set and encoding fields must be treated case-insensitive
 *   => We do so.
 * - All character sets from the ISO 8859 family that are not supported must be
 *   handled in a way that contained ASCII characters are decoded correctly
 *   => We do so.
 *
 * According to RFC 3629 the following rules are applied:
 * - If the content of an encoded word is UTF-8 encoded, it is is not allowed
 *   to accept it unchecked. It is mandatory to check the validity of the
 *   encoding => We do so.
 *
 * On success, the address of the result buffer is written to the location
 * pointed to by \e r (this may be the same as \e b if there is nothing to do).
 * The caller is responsible to free the potentially allocated memory.
 * On error \c NULL is written to the location pointed to by \e r .
 *
 * \return
 * - 0 on success if something was decoded and a new memory block was allocated
 * - 1 on success if there was nothing to decode and no memory was allocated
 * - -1 on error
 */

int  enc_mime_word_decode(const char**  r, const char*  b)
{
   int  res = 0;
   char*  rbuf = NULL;
   size_t  rbuf_len = 0;
   size_t  ri = 0;
   size_t  i = 0;
   const char*  target;
   char*  p;
   char*  p2;
   enum enc_mime_cs  charset;
   char  encoding;
   const char*  wbuf;
   const char*  nbuf;
   size_t  nbuf_len;
   int  word_flag = 0;
   size_t  word_trailing_space = 0;
   size_t  ii;
   int  ctrl = 0;  /* Indicates unwanted control characters to remove */
   size_t  len = 0;

   /* Fast special check for "no equal sign" */
   target = strchr(&b[i], (int) '=');
   if(NULL == target)
   {
      /* ... and no unwanted LF and CR control characters */
      target = strchr(&b[i], 0x0A);
      if(NULL == target)
      {
         target = strchr(&b[i], 0x0D);
         if(NULL == target)  { res = 1; }
      }
   }

   while(!res && b[i])
   {
      wbuf = NULL;
      /* Skip white space */
      nbuf_len = 0;
      while(b[i] &&
            (' ' == b[i + nbuf_len] || (const char) 0x09 == b[i + nbuf_len]))
      {
         ++nbuf_len;
      }
      if(!nbuf_len)
      {
         /* Check for encoded word */
         p = NULL;
         target = &b[i];
         if('=' == target[0])
         {
            if('?' == target[1])
            {
               /* Start delimiter detected */
               p = strchr(&target[2], (int) '?');
               if(NULL != p)
               {
                  /* Extract character set (ignore RF2231 language tokens) */
                  p2 = strchr(&target[2], (int) '*');
                  if(NULL == p2)  { p2 = p; }
                  else if(p < p2)  { p2 = p; }
                  charset = enc_mime_get_charset(&target[2],
                                                 (size_t) (p2 - &target[2]));
                  /* Extract encoding */
                  if(p[1])
                  {
                     encoding = (char) toupper((int) p[1]);
                     if('?' != p[2])
                     {
                        PRINT_ERROR("MIME: Syntax error in encoded-word");
                     }
                     else
                     {
                        /* Extract payload */
                        target = &p[3];
                        p = strchr(target, (int) '?');
                        if(NULL != p)
                        {
                           if('=' != p[1])
                           {
                              PRINT_ERROR("MIME: "
                                          "Too many fields in encoded-word");
                           }
                           else
                           {
                              /* End delimiter detected */
                              switch(encoding)
                              {
                                 case 'Q':
                                 {
                                    /* Use quoted printable decoder */
                                    wbuf = enc_mime_decode_q(charset,
                                                             target, p, 1);
                                    break;
                                 }
                                 case 'B':
                                 {
                                    /* Use base64 decoder */
                                    wbuf = enc_mime_decode_b(charset,
                                                             target, p, 0);
                                    break;
                                 }
                                 default:
                                 {
                                    PRINT_ERROR("MIME: Encoding not supported");
                                    break;
                                 }
                              }
                           }
                        }
                     }
                  }
               }
            }
         }
         if(NULL != wbuf)
         {
            /* Rewind white space between encoded words */
            if(word_flag)
            {
               while( ri && (' ' == rbuf[ri - (size_t) 1] ||
                             0x09 == (int) rbuf[ri - (size_t) 1]) )
               {
                  --ri;
               }
               ri += word_trailing_space;
            }
            /* Copy encoded word */
            word_flag = 1;
            nbuf = wbuf;
            nbuf_len = strlen(nbuf);
            i += (size_t) (&p[2] - &b[i]);
            /* Store number of trailing spaces */
            word_trailing_space = 0;
            if(nbuf_len)
            {
               ii = nbuf_len;
               while(ii--)
               {
                  if(' ' != nbuf[ii])  { break; }
                  else  { ++word_trailing_space; }
               }
            }
         }
         else
         {
            /* Copy as ASCII up to next white space */
            word_flag = 0;
            nbuf = &b[i];
            p = strchr(nbuf, (int) ' ');
            p2 = strchr(nbuf, 0x09);
            if(NULL != p2 && p2 < p)  { p = p2; }
            if(NULL == p)  { nbuf_len = strlen(nbuf); }
            else  { nbuf_len = (size_t) (p - nbuf); }
            i += nbuf_len;
         }
      }
      else
      {
         /* Copy white space */
         nbuf = &b[i];
         i += nbuf_len;
      }

      /* Allocate more memory in exponentially increasing chunks */
      while(ri + nbuf_len >= rbuf_len)  /* 1 additional byte for termination */
      {
         if(!rbuf_len)  { rbuf_len = 128; }
         if(POSIX_SIZE_MAX / (size_t) 2 < rbuf_len)  { res = -1;  break; }
         p = (char*) posix_realloc((void*) rbuf, rbuf_len *= (size_t) 2);
         if(NULL == p) { res = -1;  break; }
         else  { rbuf = p; }
      }

      /* Copy decoded word to result buffer */
      memcpy((void*) &rbuf[ri], (void*) nbuf, nbuf_len);
      ri += nbuf_len;
      if(NULL != wbuf)  { posix_free((void*) wbuf); }
   }
   /* Terminate result string */
   if(NULL != rbuf)  { len = ri;  rbuf[len] = 0; }

   /* Replace unwanted LF and CR control characters with U+FFFE */
   if(NULL != rbuf)
   {
      ri = 0;
      while(rbuf[ri])
      {
         if(0x0A == (int) rbuf[ri] || 0x0D == (int) rbuf[ri])
         {
            ctrl = 1;
            break;
         }
         ++ri;
      }
      if(ctrl)
      {
         if(POSIX_SIZE_MAX / (size_t) 3 <= len)  { res = -1; }
         else
         {
            /* Multiply string length by 3 and add 1 for NUL termination */
            rbuf_len = len * (size_t) 3 + (size_t) 1;
            p = (char*) posix_realloc((void*) rbuf, rbuf_len);
            if(NULL == p) { res = -1; }
            else
            {
               rbuf = p;
               /* Use U+FFFE (3 octets) as replacement character */
               ri = 0;
               do
               {
                  if(0x0A == (int) rbuf[ri] || 0x0D == (int) rbuf[ri])
                  {
                     memmove((void*) &rbuf[ri + (size_t) 2], (void*) &rbuf[ri],
                             rbuf_len - (ri + (size_t) 2));
                     rbuf[ri++] = 0xEF;
                     rbuf[ri++] = 0xBF;
                     rbuf[ri] = 0xBD;
                     }
               }
               while(rbuf[++ri]);
            }
            PRINT_ERROR("MIME: "
                        "Unwanted CR and/or LF detected in header field");
         }
      }
   }

   /* Check result */
   switch(res)
   {
      case 0:
      {
         *r = (const char*) rbuf;
         break;
      }
      case 1:
      {
         *r = b;
         break;
      }
      default:
      {
         posix_free((void*) rbuf);
         *r = NULL;
         break;
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Decode header field containing potential MIME parameters
 *
 * \param[out] r   Pointer to result string pointer
 * \param[in]  b   Prepared header field body that contains potential parameters
 * \param[in]  m   Operating mode (see description below)
 *
 * The parameter \e m enable special processing if set to a nonzero value.
 * \e m should be set to 1 for the \c Content-Type header field.
 *
 * \attention
 * This function must be called after unfolding the field body, with comments
 * stripped and after decoding of \c quoted-string tokens. Whitespace must
 * already be merged into the semantically equivalent single SP (and removed
 * completely before semicolons and around equal signs) by the caller.
 *
 * According to RFC 2231 the following rules are applied:
 * - Parameters can be split into multiple sections which can be listed in
 *   arbitrary order inside the header field body
 *   => We accept parameter sections in any order and merge them in ascending
 *      order.
 * - Parameter sections are allowed to contain literal content as well as
 *   \c quoted-string tokens. Mixing sections of both types is allowed
 *   => \c quoted-string tokens must already be decoded in \e b by the caller.
 * - Parameters can contain character set information
 *   => We accept content in any supported character set and decode it to
 *      Unicode NFC (non-US_ASCII octets of unsupported character sets are
 *      decoded to the underscore character).
 * - Parameter can contain language information => We accept and ignore it.
 *
 * According to RFC 3629 the following rules are applied:
 * - If the content of a parameter is UTF-8 encoded, it is is not allowed to
 *   accept it unchecked. It is mandatory to check the validity of the encoding
 *   => We do so.
 *
 * On success, the address of the result buffer is written to the location
 * pointed to by \e r (this may be the same as \e b if there is nothing to do).
 * The caller is responsible to free the potentially allocated memory.
 * On error \c NULL is written to the location pointed to by \e r .
 *
 * \return
 * - 0 on success if something was decoded and a new memory block was allocated
 * - 1 on success if there was nothing to decode and no memory was allocated
 * - -1 on error
 */

int  enc_mime_para_decode(const char**  r, const char*  b, int  m)
{
   int  res = -1;
   posix_locale_t  loc_ctype_posix = 0;
   int  finished = 0;
   struct mime_parameter**  parray = NULL;
   size_t  ppsize = sizeof(struct mime_parameter*);
   struct mime_parameter*  pdata;
   size_t  psize = sizeof(struct mime_parameter);
   const char*  first_end;  /* Semicolon after first element */
   const char*  p;  /* Start of parameter */
   const char*  p_cs;  /* Start of charset name */
   const char*  p_start;  /* Start of parameter value */
   const char*  p_end;  /* End of parameter */
   const char*  p_eq_sign;  /* Position of equal sign */
   const char*  p_asterisk;  /* Position of asterisk (or 'p_eq_sign') */
   size_t  alen;  /* Length of attribute token */
   size_t  clen;  /* Length of charset token */
   size_t  i = 0;
   size_t  ii = 0;
   int  rv;
   struct mime_parameter**  tmp;
   char*  tmp2;
   char*  tmp3;
   unsigned int  sec_num;
   char  ext_mark;  /* Flag indicating extended-parameter (with charset) */
   const char*  q;
   char*  rbuf = NULL;
   size_t  rbuf_len;
   size_t  ri = 0;
   size_t  len;
   size_t  len2;
   int  rewind;
   int  error = 0;
   char*  para_charset;  /* Pointer to charset declaration of first section */

   /* Create a locale object with LC_CTYPE == POSIX */
   loc_ctype_posix = posix_newlocale(POSIX_LC_CTYPE_MASK, "POSIX",
                                     (posix_locale_t) 0);
   if((posix_locale_t) 0 == loc_ctype_posix)
   {
      PRINT_ERROR("MIME: Cannot create locale object");
      return(res);
   }

   /* Nothing to do if there are no asterisks */
   if(NULL == strchr(b, (int) '*'))  { *r = b;  res = 1; }
   else
   {
#if 0
      /* For debugging */
      printf("---------------\n");
      printf("Header field body  : %s\n", b);
#endif
      /* Skip to end of content */
      first_end = strchr(b, (int) ';');
      if(NULL == first_end)  { *r = b;  res = 1; }
      else
      {
         /* Initialize parameter section array */
         parray = (struct mime_parameter**) posix_malloc(ppsize);
         if(NULL != parray)
         {
            parray[0] = NULL;
            /* Parse parameters */
            p_end = first_end;
            do
            {
               p = p_end + 1;
               sec_num = 0;
               ext_mark = ' ';
               clen = 0;
               /* Skip potential space after semicolon */
               if(' ' == *p)  { ++p; }
               /* Seach for end of parameter section content */
               p_end = strchr(p, (int) ';');
               if(NULL == p_end)
               {
                  p_end = p + strlen(p);
                  /* Strip potential trailing space */
                  if(' ' == *(p_end - 1))  { --p_end; }
                  finished = 1;
               }
               /* Search for end of parameter name */
               p_eq_sign = strchr(p, (int) '=');
               if(NULL == p_eq_sign)  { break; }
               if(p_end < p_eq_sign)  { break; }
               /* Search for end of attribute token (asterisk) */
               p_asterisk = strchr(p, (int) '*');
               if(NULL != p_asterisk && p_eq_sign > p_asterisk)
               {
                  /* Extract section number */
                  rv = sscanf(p_asterisk, " * %u", &sec_num);
                  if(1 != rv)
                  {
                     /* No section number specified */
                     sec_num = 0;
                     /* Check for extended-parameter */
                     sscanf(p_asterisk, " %c", &ext_mark);
                  }
                  else
                  {
                     /* Check for extended-parameter */
                     sscanf(p_asterisk, " * %*u %c", &ext_mark);
                  }
               }
               else  { p_asterisk = p_eq_sign; }
               alen = (size_t) (p_asterisk - p);
               if(alen && ' ' == p[alen - (size_t) 1])
               {
                  /* Strip potential trailing space */
                  --alen;
               }
               /* Check for parameter attribute length limit */
               if(ENC_MIME_PARA_LENGTH_MAX < alen)
               {
                  PRINT_ERROR("MIME: Parameter attribute too long");
                  continue;
               }
               /* Extract charset */
               p_start = p_eq_sign + 1;
               p_cs = p_start;
               if(!sec_num && '*' == ext_mark)
               {
                  q = strchr(p_start, 0x27);
                  if(NULL == q)
                  {
                     PRINT_ERROR("MIME: Parameter charset field missing");
                  }
                  else
                  {
                     clen = (size_t) (q - p_start);
                     if(ENC_MIME_PARA_LENGTH_MAX < clen)
                     {
                        PRINT_ERROR("MIME: Parameter charset too long");
                        clen = 0;
                     }
                     p_start = q + 1;
                     q = strchr(p_start, 0x27);
                     if(NULL == q)
                     {
                        PRINT_ERROR("MIME: Parameter language field missing");
                     }
                     else  { p_start = q + 1; }
                  }
               }
               /* Remove unknown parameters for "Content-Type" mode */
               if(1 == m)
               {
                  if(posix_strncasecmp_l(p, "Charset", alen, loc_ctype_posix)
                     && posix_strncasecmp_l(p, "Format", alen, loc_ctype_posix)
                     && posix_strncasecmp_l(p, "DelSp", alen, loc_ctype_posix)
                     && posix_strncasecmp_l(p, "InsLine", alen, loc_ctype_posix)
                     && posix_strncasecmp_l(p, "Boundary", alen, loc_ctype_posix)
                    )
                  {
                     /* Ignore all other parameters */
                     continue;
                  }
               }
               /* Increase size of array */
               tmp = (struct mime_parameter**)
                     posix_realloc(parray,
                                   ppsize += sizeof(struct mime_parameter*));
               if(NULL == tmp)
               {
                  PRINT_ERROR("MIME: Parameter memory allocation failed");
                  break;
               }
               parray = tmp;
               /* Construct parameter structure ... */
               pdata = (struct mime_parameter*) posix_malloc(psize);
               if(NULL == pdata)
               {
                  PRINT_ERROR("MIME: Parameter memory allocation failed");
                  break;
               }
               strncpy(pdata->attribute, p, alen);
               pdata->attribute[alen] = 0;
               pdata->attribute_len = alen;
               pdata->section = sec_num;
               strncpy(pdata->charset, p_cs, clen);
               pdata->charset[clen] = 0;
               pdata->value_start = p_start;
               pdata->value_end = p_end;
               pdata->valid = 1;
#if 0
               /* For debugging */
               printf("Index : %u / Section: %u (%s): ", (unsigned int) i,
                      sec_num, pdata->attribute);
               if(strlen(pdata->charset))
               {
                  printf("[Charset: %s] ", pdata->charset);
               }
               for(size_t  iii = 0; (size_t) (p_end - p_start) > iii; ++iii)
               {
                  printf("%c", pdata->value_start[iii]);
               }
               printf("\n");
#endif
               /* ... and append it to array */
               parray[i] = pdata;
               parray[++i] = NULL;
            }
            while(!finished);
            /* -------------------------------------------------------------- */
            /* Allocate new memory buffer for result */
            rbuf_len = (size_t) (first_end - b);
            /* 3 additional bytes for "; " separator and NUL termination */
            rbuf = (char*) posix_malloc(rbuf_len + (size_t) 3);
            if(NULL != rbuf)
            {
               /* Copy first element (including the semicolon) */
               strncpy(rbuf, b, rbuf_len);
               rbuf[rbuf_len] = 0;
               /* Strip SPs from first element for "Content-Type" mode */
               if(1 == m)
               {
                  /* Assignment in truth expression is intended */
                  while(NULL != (q = strchr(rbuf, (int) ' ')))
                  {
                     memmove((void*) q, (void*) (q + 1),
                             strlen(q + 1) + (size_t) 1);
                     if(rbuf_len)  { --rbuf_len; }
                  }
               }
               ri += rbuf_len;
               rbuf_len += (size_t) 3;
               /* Merge parameter sections */
               res = 0;
               i = 0;
               if(NULL == parray[i])
               {
                  PRINT_ERROR("MIME: Missing parameters");
               }
               else do
               {
                  /* Array contain at least 1 element */
                  if(!parray[i]->valid)  { continue; }
                  if(parray[i]->section)  { continue; }
                  /* Found initial section => Insert separator */
                  rbuf[ri++] = ';';  rbuf[ri++] = ' ';
                  /* Select initial section to force first match */
                  sec_num = 0;
                  rewind = 0;
                  ii = 0;
                  para_charset = NULL;
                  do
                  {
                     /* Search for next segment */
                     if(rewind)  { rewind = 0; ii = 0; }
                     if(!parray[ii]->valid)  { continue; }
                     if(sec_num != parray[ii]->section)  { continue; }
                     else if(!strcmp(parray[i]->attribute,
                                     parray[ii]->attribute))
                     {
                        /* Calculate length */
                        if(!sec_num)
                        {
                           /* One additional byte for NUL (and later '=') */
                           alen = parray[ii]->attribute_len + (size_t) 1;
                        }
                        else  { alen = 0; }
                        len = alen;
                        len += (size_t) (parray[ii]->value_end
                                         - parray[ii]->value_start);
                        /* Allocate memory in exponentially increasing chunks */
                        len += 3;  /* For "; " separator and NUL termination */
                        while(ri + len >= rbuf_len)
                        {
                           tmp2 = posix_realloc((void*) rbuf,
                                                rbuf_len *= (size_t) 2);
                           if(NULL == tmp2)
                           {
                              PRINT_ERROR("MIME: Memory allocation"
                                          " for result buffer failed");
                              error = 1;
                              continue;
                           }
                           else  { rbuf = tmp2; }
                        }
                        len -= (size_t) 3;
                        /* Append attribute to result buffer for section 0 */
                        if(!sec_num)
                        {
                           strncpy(&rbuf[ri], parray[ii]->attribute, alen);
                           rbuf[ri + alen - (size_t) 1] = '=';
                           ri += alen;
                           len -= alen;
                        }
                        /* Only first parameter section has a charset field */
                        if(!sec_num)  { para_charset = parray[ii]->charset; }
                        /* Append decoded value section to result buffer */
                        tmp3 = NULL;
                        if(NULL != para_charset)
                        {
                           /* Interpret zero length charset as "US-ASCII" */
                           if((size_t) 0 == strlen(para_charset))
                           {
                              para_charset="US-ASCII";
                           }
                           /* Decode charset of value section */
                           tmp2 = posix_malloc(len + (size_t) 1);
                           if(NULL != tmp2)
                           {
                              strncpy(tmp2, parray[ii]->value_start, len);
                              tmp2[len] = 0;
                              tmp3 = enc_mime_decode_parameter(tmp2,
                                                               para_charset);
                              if(NULL != tmp3)
                              {
                                 len2 = strlen(tmp3);
                                 if(len < len2)
                                 {
                                    PRINT_ERROR("MIME: Decoding error");
                                    posix_free((void*) tmp3);
                                    tmp3 = NULL;
                                 }
                                 else
                                 {
                                    strcpy(&rbuf[ri], tmp3);
                                    ri += len2;
                                 }
                              }
                              posix_free((void*) tmp2);
                           }
                        }
                        if(NULL == tmp3)
                        {
                           strncpy(&rbuf[ri], parray[ii]->value_start, len);
                           rbuf[ri + len] = 0;
                           ri += len;
                        }
                        posix_free((void*) tmp3);
                        parray[ii]->valid = 0;
                        /* Rewind index for next section */
                        rewind = 1;
                        ++sec_num;
                     }
                  }
                  while(!error && (NULL != parray[++ii] || rewind));
                  parray[i]->valid = 0;
               }
               while(!error && NULL != parray[++i]);
            }
            /* Destroy parameter section array */
            i = 0;
            while(NULL != parray[i])  { posix_free((void*) parray[i++]); }
            posix_free((void*) parray);
         }
      }
   }
   if(error)  { res = -1; }

   /* Destroy locale object */
   if((posix_locale_t) 0 != loc_ctype_posix)
   {
      posix_freelocale(loc_ctype_posix);
   }

   /* Check for error */
   if(0 > res)  { *r = NULL; }
   if(!res)
   {
#if 0
      /* For debugging (Attention: Terminal must use UTF-8 encoding!) */
      printf("Result: %s\n", rbuf);
      printf("---------------\n");
#endif
      *r = rbuf;
   }  else  { posix_free((void*) rbuf); }

   return(res);
}


/* ========================================================================== */
/*! \brief Decode MIME "Content-Type" header field
 *
 * \param[out]  ct       Pointer to result structure
 * \param[in]   hf_body  Header field body that contains the MIME content type
 * \param[out]  bo       Pointer to buffer for multipart boundary delimiter
 *
 * The header field body \e hf_body is decoded and content IDs are written to
 * the structure pointed to by \e ct .
 *
 * The buffer for the boundary string used in messages with content type
 * "multipart" must be allocated by the caller with a size of at least
 * \ref ENC_BO_BUFLEN and a pointer to the start of this buffer must be passed
 * as \e bo parameter. It is allowed to pass \c NULL for \e bo if the caller
 * is not interested in the boundary string.
 *
 * According to RFC 2045 the following rules are applied:
 * - If the content type is not present, "text/plain" and "US-ASCII" must be
 *   used as default => We do so.
 *
 * According to RFC 2046 the following rules are applied:
 * - The content type and subtype must be treated case insensitive => We do so.
 * - The parameter names must be treated case insensitive => We do so.
 * - The default character set must be assumed as "US-ASCII" if the "charset"
 *   parameter is missing for "text/plain" content type => We do so.
 *
 * According to RFC 3676 the following rules are applied:
 * - The values of parameters "Format" and "DelSp" must be treated case
 *   insensitive => We do so.
 * - The parameter "DelSp" should be ignored if content type is not "text/plain"
 *   with "format=flowed" => We do so.
 *
 * The experimental parameter "InsLine" set to "yes" adds an empty line
 * separator after every paragraph that end with an empty line.
 * This allows to declare single lines as paragraphs, e.g. for Smartphones,
 * without losing the separation to the following text (or creating double empty
 * line separation in compatibility view).
 *
 * \note
 * This function never fails, instead \c ENC_xxx_UNKNOWN IDs are returned.
 */

void  enc_mime_get_ct(struct enc_mime_ct*  ct, const char*  hf_body, char*  bo)
{
   char*  body = NULL;
   size_t  len;
   size_t  i;
   size_t  ii;
   char  fmt[ENC_FMT_BUFLEN];
   char  cs[ENC_CS_BUFLEN];
   size_t  bo_len, bo_len_valid;
   int  trailing_sp = 0;

   /* Initialize result */
   ct->type = ENC_CT_UNKNOWN;
   ct->subtype = ENC_CTS_UNKNOWN;
   ct->charset = ENC_CS_UNKNOWN;
   ct->flags = 0;

   /* Accept NULL pointer (treat as "field is not present") */
   if(NULL == hf_body)
   {
#if 0
      /* For debugging */
      printf("Content-Type: Not specified\n");
#endif
      ct->type = ENC_CT_TEXT;
      ct->subtype = ENC_CTS_PLAIN;
      ct->charset = ENC_CS_ASCII;
      return;
   }

   /* Allocate memory for case conversion */
   len = strlen(hf_body);
   body = (char*) posix_malloc(len + (size_t) 1);
   if(NULL != body)
   {
      /* Convert header field body to upper case */
      for(i = 0; i < len; ++i)  { body[i] = (char) toupper((int) hf_body[i]); }
      body[len] = 0;
#if 0
      /* For debugging */
      printf("Content-Type: %s\n", body);
#endif
      /* Check for content type "text" */
      if(!strncmp("TEXT", body, 4))
      {
         ct->type = ENC_CT_TEXT;
         ct->charset = ENC_CS_ASCII;
         if(!strncmp("TEXT/PLAIN", body, 10))
         {
            ct->subtype = ENC_CTS_PLAIN;
            /* Search for RFC 3676 "Format" parameter (case insensitive) */
            for(i = 0; i < len; ++i)
            {
               if(!strncmp("FORMAT", &body[i], 6))
               {
                  /* Extract parameter value */
                  ii = i + (size_t) 6;
                  while(body[ii])
                  {
                     if('=' != body[ii] && ' ' != body[ii])  { break; }
                     else  { ++ii; }
                  }
                  for(i = 0; i < ENC_FMT_BUFLEN; ++i)
                  {
                     if(!body[ii + i]
                        || ';' == body[ii + i] || ' ' == body[ii + i])
                     {
                        fmt[i] = 0;  break;
                     }
                     else  { fmt[i] = body[ii + i]; }
                  }
                  fmt[ENC_FMT_BUFLEN - (size_t) 1] = 0;
                  if(!strncmp("FLOWED", fmt, 6))
                  {
                     ct->flags |= ENC_CT_FLAG_FLOWED;
                  }
                  break;
               }
            }
            if(ct->flags & ENC_CT_FLAG_FLOWED)
            {
               /* Search for RFC 3676 "DelSp" parameter (case insensitive) */
               for(i = 0; i < len; ++i)
               {
                  if(!strncmp("DELSP", &body[i], 5))
                  {
                     /* Extract parameter value */
                     ii = i + (size_t) 5;
                     while(body[ii])
                     {
                        if('=' != body[ii] && ' ' != body[ii])  { break; }
                        else  { ++ii; }
                     }
                     for(i = 0; i < ENC_FMT_BUFLEN; ++i)
                     {
                        if(!body[ii + i]
                           || ';' == body[ii + i] || ' ' == body[ii + i])
                        {
                           fmt[i] = 0;  break;
                        }
                        else  { fmt[i] = body[ii + i]; }
                     }
                     fmt[ENC_FMT_BUFLEN - (size_t) 1] = 0;
                     if(!strncmp("YES", fmt, 3))
                     {
                        ct->flags |= ENC_CT_FLAG_DELSP;
                     }
                     break;
                  }
               }
               /* Search for "InsLine" parameter (case insensitive) */
               for(i = 0; i < len; ++i)
               {
                  if(!strncmp("INSLINE", &body[i], 7))
                  {
                     /* Extract parameter value */
                     ii = i + (size_t) 7;
                     while(body[ii])
                     {
                        if('=' != body[ii] && ' ' != body[ii])  { break; }
                        else  { ++ii; }
                     }
                     for(i = 0; i < ENC_FMT_BUFLEN; ++i)
                     {
                        if(!body[ii + i]
                           || ';' == body[ii + i] || ' ' == body[ii + i])
                        {
                           fmt[i] = 0;  break;
                        }
                        else  { fmt[i] = body[ii + i]; }
                     }
                     fmt[ENC_FMT_BUFLEN - (size_t) 1] = 0;
                     if(!strncmp("YES", fmt, 3))
                     {
                        ct->flags |= ENC_CT_FLAG_INSLINE;
                     }
                     break;
                  }
               }
            }
         }
         /* Search for "charset" parameter */
         for(i = 0; i < len; ++i)
         {
            if(!strncmp("CHARSET", &body[i], 7))
            {
               /* Extract parameter value */
               ii = i + (size_t) 7;
               while(body[ii])
               {
                  if('=' != body[ii] && ' ' != body[ii])  { break; }
                  else  { ++ii; }
               }
               for(i = 0; i < ENC_CS_BUFLEN; ++i)
               {
                  if(!body[ii + i]
                     || ';' == body[ii + i] || ' ' == body[ii + i])
                  {
                     cs[i] = 0;  break;
                  }
                  else  { cs[i] = body[ii + i]; }
               }
               cs[ENC_CS_BUFLEN - (size_t) 1] = 0;
               ct->charset = enc_mime_get_charset(cs, strlen(cs));
               break;
            }
         }
      }
      /* Check for content type "image" */
      else if(!strncmp("IMAGE", body, 5))
      {
         ct->type = ENC_CT_IMAGE;
      }
      /* Check for content type "audio" */
      else if(!strncmp("AUDIO", body, 5))
      {
         ct->type = ENC_CT_AUDIO;
      }
      /* Check for content type "video" */
      else if(!strncmp("VIDEO", body, 5))
      {
         ct->type = ENC_CT_VIDEO;
      }
      /* Check for content type "message" (only subtype "rfc822" supported) */
      else if(!strncmp("MESSAGE/RFC822", body, 14))
      {
         ct->type = ENC_CT_MESSAGE;
         ct->subtype = ENC_CTS_RFC822;
      }
      /* Check for content type "multipart", map unknown subtypes to "mixed" */
      else if(!strncmp("MULTIPART", body, 9))
      {
         ct->type = ENC_CT_MULTIPART;
         ct->subtype = ENC_CTS_MIXED;
         if(!strncmp("MULTIPART/ALTERNATIVE", body, 21))
         {
            ct->subtype = ENC_CTS_ALTERNATIVE;
         }
         else if(!strncmp("MULTIPART/DIGEST", body, 16))
         {
            ct->subtype = ENC_CTS_DIGEST;
         }
         bo[0] = 0;
         /* Search for "boundary" parameter */
         for(i = 0; i < len; ++i)
         {
            if(!strncmp("BOUNDARY", &body[i], 8))
            {
               /* Extract case sensitive parameter value */
               ii = i + (size_t) 8;
               if('=' != hf_body[ii++])
               {
                  PRINT_ERROR("MIME: "
                              "Missing multipart boundary parameter value");
               }
               else
               {
                  /* Start of boundary parameter value found */
                  for(i = 0; i < ENC_BO_BUFLEN; ++i)
                  {
                     if(!hf_body[ii + i] || ';' == hf_body[ii + i])
                     {
                        bo[i] = 0;  break;
                     }
                     else  { bo[i] = hf_body[ii + i]; }
                  }
                  bo[ENC_BO_BUFLEN - (size_t) 1] = 0;
                  /* Check boundary */
                  bo_len = strlen(bo);
                  bo_len_valid = strspn(bo,
                     "0123456789"
                     "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
                     "'()+_,-./:=?"
                     " ");
                  if (bo_len_valid != bo_len)
                  {
                     PRINT_ERROR("MIME: Invalid multipart boundary parameter");
                     if(bo_len_valid && ' ' != bo[bo_len_valid - (size_t) 1])
                     {
                        /* Strip invalid tail */
                        bo[bo_len_valid] = 0;
                        bo_len = bo_len_valid;
                     }
                     else
                     {
                        /* Boundary not usable, use "?" as dummy replacement */
                        bo[0] = '?';
                        bo_len = 1;
                        bo[bo_len] = 0;
                     }
                  }
                  while(bo_len)
                  {
                     if(' ' != bo[bo_len - (size_t) 1])  { break; }
                     /* Strip trailing SP */
                     trailing_sp = 1;
                     bo[--bo_len] = 0;
                  }
                  if(trailing_sp)
                  {
                     PRINT_ERROR("MIME: Stripped trailing whitespace "
                                 "from multipart boundary parameter");
                  }
               }
               break;
            }
         }
      }
   }
   posix_free((void*) body);
}


/* ========================================================================== */
/*! \brief Decode content transfer encoding description
 *
 * \param[in] hf_body  MIME content transfer encoding description string
 *
 * This function checks whether the string \e hf_body represents a supported
 * content transfer encoding and return the corresponding ID for it.
 * According to RFC 2047 the content transfer encoding is treated
 * case-insensitive.
 *
 * \note
 * It is allowed to call this function with \e hf_body set to \c NULL. This is
 * treated as an error and the return value will indicate an unknown transfer
 * encoding.
 *
 * \note
 * RFC 2049 requires that every non-7bit MIME content must be labeled with a
 * content transfer encoding header field of "8bit" or "binary".
 *
 * \result
 * - MIME content transfer encoding ID (from \ref enc_mime_cte )
 * - \c ENC_CTE_UNKNOWN on error
 */
/* If this header field is missing, we assume "binary" instead of "7bit".
 * Don't change this because it is required for handling unknown transfer
 * encodings!
 */

enum enc_mime_cte  enc_mime_get_cte(const char*  hf_body)
{
   enum enc_mime_cte  res = ENC_CTE_BIN;
   char  buf[ENC_CTE_BUFLEN];
   size_t  len;
   size_t  i;
   const char  not_supported[]
      = "ENC: MIME: Unsupported content transfer encoding: ";
   char*  p;
   size_t  l;

   /* Accept NULL pointer */
   if(NULL != hf_body)
   {
      res = ENC_CTE_UNKNOWN;
      len = strlen(hf_body);
      if(ENC_CTE_BUFLEN <= len)
      {
         /* If you get this error, the value of 'ENC_CTE_BUFLEN' is too small */
         PRINT_ERROR("MIME: Name of content transfer encoding too long");
      }
      else
      {
         /* Convert description to upper case */
         for(i = 0; i < len; ++i)
         {
            buf[i] = (char) toupper((int) hf_body[i]);
         }
         buf[len] = 0;
         /* Check for all known content transfer encodings */
         if(!strcmp(buf, "7BIT"))  { res = ENC_CTE_7BIT; }
         if(!strcmp(buf, "8BIT"))  { res = ENC_CTE_8BIT; }
         if(!strcmp(buf, "BINARY"))  { res = ENC_CTE_BIN; }
         if(!strcmp(buf, "QUOTED-PRINTABLE"))  { res = ENC_CTE_Q; }
         if(!strcmp(buf, "BASE64"))  { res = ENC_CTE_B; }
         /* To be more tolerant: Check again for invalid identity declaration */
         if(!strcmp(buf, "7-BIT"))
         {
            PRINT_ERROR("MIME: "
               "Invalid content transfer encoding 7-bit accepted as 7bit");
            res = ENC_CTE_7BIT;
         }
         if(!strcmp(buf, "8-BIT"))
         {
            PRINT_ERROR("MIME: "
               "Invalid content transfer encoding 8-bit accepted as 8bit");
            res = ENC_CTE_8BIT;
         }
         /* Check whether content transfer encoding is supported */
         if(ENC_CTE_UNKNOWN == res)
         {
            l = strlen(not_supported) + len;
            p = (char*) posix_malloc(++l);
            if(NULL != p)
            {
               strcpy(p, not_supported);
               strncat(p, buf, len);
               print_error(p);
               posix_free((void*) p);
            }
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Decode content disposition
 *
 * \param[in]  hf_body   Body of Content-Disposition header field
 * \param[out] type      Pointer to content disposition type ID
 * \param[out] filename  Pointer to filename
 *
 * The field body \e hf_body must be unfolded and preprocessed (parameters must
 * ne already decoded according to RFC 2231).
 * The value for the filename parameter must be already converted to UTF-8.
 *
 * If a filename parameter is present, a new memory block is allocated for
 * \e filename . Otherwise \c NULL is returned.
 */

void  enc_mime_get_cd(const char*  hf_body,
                      enum enc_mime_cd*  type, const char**  filename)
{
   posix_locale_t  loc_ctype_posix;
   char*  body = NULL;
   const char*  fn_para = "FILENAME=";
   const char*  p;
   const char*  q;
   size_t  len;
   char*  buf;
   size_t  i;

   /* Prepare values to return if an error occurs */
   *type = ENC_CD_UNKNOWN;
   *filename = NULL;

   /* Extract disposition type (case-insensitive) */
   loc_ctype_posix = posix_newlocale(POSIX_LC_CTYPE_MASK, "POSIX",
                                     (posix_locale_t) 0);
   if((posix_locale_t) 0 == loc_ctype_posix)
   {
      PRINT_ERROR("MIME: Cannot create locale object");
      return;
   }
   else
   {
      if(!posix_strncasecmp_l(hf_body, "inline", strlen("inline"),
                              loc_ctype_posix))
      {
         *type = ENC_CD_INLINE;
      }
      else if(!posix_strncasecmp_l(hf_body, "attachment", strlen("attachment"),
                                   loc_ctype_posix))
      {
         *type = ENC_CD_ATTACHMENT;
      }
      posix_freelocale(loc_ctype_posix);
   }

   /* Extract filename */
   len = strlen(hf_body);
   body = (char*) posix_malloc(len + (size_t) 1);
   if(NULL != body)
   {
      /* Convert header field body to upper case */
      for(i = 0; i < len; ++i)  { body[i] = (char) toupper((int) hf_body[i]); }
      body[len] = 0;
      /* Check for parameter "filename" */
      p = strstr(body, fn_para);
      if(NULL != p)
      {
         p += strlen(fn_para);
         q = strchr(p, ';');
         if(NULL != q)  { len = (size_t) (q - p); }
         else  { len = strlen(p); }
         /* Copy filename case-sensitive */
         buf = (char*) malloc(len + (size_t) 1);
         if(NULL != buf)
         {
            i = (size_t) (p - body);
            strncpy(buf, &hf_body[i], len);
            buf[len] = 0;
            /* Strip path, if present */
            p = strrchr(buf, '/');
            if(NULL != p)  { ++p;  memmove(buf, p, strlen(p) + (size_t) 1); }
            /*
             * Reject filename if it contains '~', '|' or '\' characters.
             * See RFC 2183 Section 5 "Security Considerations" for details
             */
            p = strpbrk(buf, "~|\x5C");
            if(NULL != p)
            {
               PRINT_ERROR("MIME: "
                           "Filename in Content-Disposition rejected");
            }
            else  { *filename = buf; }
         }
      }
   }
   posix_free((void*) body);
}


/* ========================================================================== */
/*! \brief Decode MIME content transfer encoding and save to file
 *
 * \param[in] pn      Pathname of file
 * \param[in] cte     MIME content transfer encoding
 * \param[in] entity  MIME entity body
 *
 * According to RFC 2049 all transfer encodings not defined in MIME 1.0 are
 * rejected.
 *
 * \return
 * - 0 on success
 * - -1 on error
 */

int enc_mime_save_to_file(const char*  pn, enum enc_mime_cte  cte,
                          const char*  entity)
{
   int  res = -1;
   size_t  len = strlen(entity);
   const char*  p = entity;
   const char*  buf = NULL;
   int  fd;
   int  rv;
   posix_mode_t  perm = POSIX_S_IRUSR | POSIX_S_IWUSR |
                        POSIX_S_IRGRP | POSIX_S_IWGRP |
                        POSIX_S_IROTH | POSIX_S_IWOTH;

   /* Decode transfer encoding */
   switch(cte)
   {
      case ENC_CTE_Q:
      {
         buf = enc_mime_decode_qp(entity, &entity[len], 0, &len);
         p = buf;
         break;
      }
      case ENC_CTE_B:
      {
         buf = enc_mime_decode_base64(entity, &entity[len], 0, &len);
         p = buf;
         break;
      }
      case ENC_CTE_7BIT:
      case ENC_CTE_8BIT:
      case ENC_CTE_BIN:
      {
         break;
      }
      default:
      {
         PRINT_ERROR("MIME: Content transfer encoding not supported");
         break;
      }
   }

   /* Save to file */
   if(NULL != p)
   {
      rv = fu_open_file(pn, &fd, POSIX_O_WRONLY | POSIX_O_CREAT | POSIX_O_TRUNC,
                        perm);
      if(rv)
      {
         PRINT_ERROR("MIME: Opening file failed");
      }
      else
      {
         rv = fu_write_to_filedesc(fd, p, len);
         if(rv)
         {
            PRINT_ERROR("MIME: Writing to file failed");
         }
         else  { res = 0; }
         fu_close_file(&fd, NULL);
      }
   }

   enc_free((void*) buf);

   return(res);
}


/* ========================================================================== */
/*! \brief Decode MIME text content to UTF-8 NFC
 *
 * \param[in] cte      MIME content transfer encoding
 * \param[in] charset  MIME character set
 * \param[in] s        MIME encoded data
 *
 * According to RFC 2049 all transfer encodings not defined in MIME 1.0 are
 * rejected.
 *
 * \return
 * - Pointer to decoded data.
 *   If the result is not equal to \e s , a new memory block was allocated
 * - NULL on error (Original memory block for \e s is still allocated)
 */

const char*  enc_mime_decode(enum enc_mime_cte  cte, enum enc_mime_cs  charset,
                             const char*  s)
{
   const char*  res = NULL;
   size_t  len = strlen(s);

   /* Decode transfer encoding and convert charset to Unicode */
   switch(cte)
   {
      case ENC_CTE_Q:
      {
         res = enc_mime_decode_q(charset, s, &s[len], 0);
         break;
      }
      case ENC_CTE_B:
      {
         res = enc_mime_decode_b(charset, s, &s[len], 0);
         break;
      }
      case ENC_CTE_7BIT:
      case ENC_CTE_8BIT:
      case ENC_CTE_BIN:
      {
         res = enc_convert_to_utf8_nfc(charset, s);
         break;
      }
      default:
      {
         PRINT_ERROR("MIME: Content transfer encoding not supported");
         break;
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Decode MIME "text/plain" content with "format=flowed" parameter
 *
 * \param[in] s        MIME encoded data in canonical form
 * \param[in] delsp    Delete spaces at EOL if nonzero
 * \param[in] insline  Add empty line separator after paragraphs if nonzero
 *
 * \attention
 * The encoding of the data referenced by \e s must be valid Unicode in UTF-8
 * representation. This must be checked by the caller before this function is
 * used.
 *
 * \return
 * - Pointer to decoded data
 *   (if the result is not equal to \e s , a new memory block was allocated)
 * - NULL on error (Original memory block for \e s is still allocated)
 */

const char*  enc_mime_flowed_decode(const char*  s, unsigned int  delsp,
                                    unsigned int  insline)
{
   const char*  quote_mark;
   int  error = 0;
   int  abort;
   int  check;
   char*  p;
   size_t  ii;
   /* Index in input buffer */
   size_t  i = 0;
   /* Target buffer */
   char*  buf = NULL;
   size_t  len = 0;
   size_t  bi = 0;
   int  insert_crlf = 0;
   /* Paragraph buffer */
   char*  para = NULL;
   size_t  plen = 0;
   size_t  pi = 0;
   int  pflowed;
   int  pell;  /* Empty last line */
   /* Line buffer */
   size_t  start;
   size_t  end;
   size_t  llen = 0;
   size_t  llimit;
   int  flowed;
   /* Quote depth */
   int  qdepth;
   size_t  qd;
   /* Index after last space (or SHY) suitable for line break */
   size_t  last_space;
   size_t  ustring_len;

   /* Set quote mark style according to config file */
   switch(config[CONF_QUOTESTYLE].val.i)
   {
      case 0:  { quote_mark = ">";  break; }
      case 1:  { quote_mark = "> ";  break; }
      default:
      {
         PRINT_ERROR("Quoting style configuration not supported");
         /* Use default from old versions that can't be configured */
         quote_mark = "> ";
         break;
      }
   }
   /* Process data */
   while(s[i])
   {
      /* Process next paragraph */
      pi = 0;
      pflowed = 0;
      pell = 0;
      qdepth = -1;
      do
      {
         /* Process next line */
         flowed = 0;
         /* Calculate quoting depth */
         qd = 0;
         while('>' == s[i])
         {
            if(POSIX_INT_MAX <= qd)  { break; }
            ++qd;
            ++i;
         }
         if(-1 == qdepth)  { qdepth = (int) qd; }
         else
         {
            if((int) qd != qdepth)
            {
               PRINT_ERROR("MIME: Invalid paragraph format"
                           " (format=flowed)");
               i -= qd;
               break;
            }
         }
         /* Remove space stuffing */
         if(' ' == s[i])  { ++i; }
         start = end = i;
         /* Search for EOL */
         while(s[i])
         {
            if(i && 0x0A == (int) s[i])
            {
               if(0x0D != (int) s[i - (size_t) 1])
               {
                  /* Canonical line termination must be CR+LF */
                  PRINT_ERROR("MIME: Invalid line termination"
                           " (format=flowed)");
                  end = i;
               }
               else  { end = i - (size_t) 1; }
               ++i;
               break;
            }
            /* Special handling for last line without CR+LF */
            if(!s[++i])  { end = i; }
         }
         llen = end - start;
         /* Check for flowed line */
         if(llen && ' ' == s[end - (size_t) 1])
         {
            /* Check for signature separator */
            if(!((size_t) 3 == llen
               && '-' == s[start] && '-' == s[start + (size_t) 1]))
            {
               flowed = 1;
               pflowed = 1;
               if(delsp)  { --llen;  --end; }
            }
         }
         /* Allocate memory in exponentially increasing chunks */
         while(pi + llen + (size_t) 1 >= plen)  /* At least 1 additional byte */
         {
            if(!plen)  { plen = 128; }
            p = (char*) posix_realloc((void*) para, plen *= (size_t) 2);
            if(NULL == p)
            {
               PRINT_ERROR("Memory allocation failed");
               error = 1;
               break;
            }
            else  { para = p; }
         }
         if(error)  { break; }
         /* Copy line to paragraph buffer */
         strncpy(&para[pi], &s[start], llen);
         pi += llen;
      }
      while(flowed);
      if(error)  { break; }
      para[pi] = 0;
      /* Set flag if paragraph ends with empty line */
      if(pflowed && !llen)  { pell = 1; };
      /* Copy fixed line or flowed paragraph to target buffer */
      pi = 0;
      do
      {
         llen = (size_t) qdepth * strlen(quote_mark);
         if(!pflowed)
         {
            start = 0;
            end = strlen(para);
            llen += end;
         }
         else
         {
            /* Rewrap flowed lines before 72 characters if possible */
            start = pi;
            last_space = 0;
            abort = 0;
            while(!abort)
            {
               check = 0;
               if(!para[pi])  { abort = 1; }
               else
               {
                  /* Check for SP */
                  if(' ' == para[pi])  { check = 1; }
                  /* Check for SHY (in UTF-8 encoding) */
                  else if(pi
                          && 0xADU == (unsigned int) (unsigned char) para[pi]
                          && 0xC2U == (unsigned int)
                                      (unsigned char) para[pi - (size_t) 1])
                  {
                     check = 1;
                  }
                  /* Check for ZWSP (in UTF-8 encoding) */
                  else if(1 < pi
                          && 0x8BU == (unsigned int) (unsigned char) para[pi]
                          && 0x80U == (unsigned int)
                                      (unsigned char) para[pi - (size_t) 1]
                          && 0xE2U == (unsigned int)
                                      (unsigned char) para[pi - (size_t) 2])
                  {
                     check = 1;
                  }
                  ++pi;
               }
               if(abort || check)
               {
                  /* Allow max. 78 characters for quoted content */
                  llimit = (size_t) 72;
                  if(1 == qdepth)  { llimit = (size_t) 74; }
                  else if(2 == qdepth)  { llimit = (size_t) 76; }
                  else if(3 <= qdepth)  { llimit = (size_t) 78; }
                  /* Use 20 characters as minimum content width */
                  if(llimit - (size_t) 20
                     <= (size_t) qdepth * strlen(quote_mark))
                  {
                     llimit = (size_t) 20;
                  }
                  else
                  {
                     llimit -= (size_t) qdepth * strlen(quote_mark);
                  }
                  /* Check for line length limit */
                  ustring_len = pi - start;
                  if(ustring_len)
                  {
                     /* Do not count trailing SP */
                     if(pi && ' ' == para[pi - (size_t) 1])  { --ustring_len; }
                  }
                  if(llimit < enc_uc_get_glyph_count(&para[start], ustring_len))
                  {
                     /* Check for second last line */
                     if(last_space)  { pi = last_space; }
                     /* Check for last line */
                     else if(abort)
                     {
                        pflowed = 0;
                        if(pell)  { insert_crlf = 1; }
                     }
                     break;
                  }
                  /* Check for end of paragraph */
                  if(abort)
                  {
                     pflowed = 0;
                     if(pell)  { insert_crlf = 1; }
                  }
                  else  { last_space = pi; }
               }
            }
            /* Skip trailing SP */
            if(start < pi && ' ' == para[pi - (size_t) 1])
            {
               end = pi - (size_t) 1;
            }
            else  { end = pi; }
            llen += end - start;
         }
         /* Two additional characters for CR+LF line termination */
         llen += (size_t) 2;
         /* InsLine parameter has precedence over configfile entry */
         if(!insline)
         {
            /* Reset request for empty line separator if not configured */
            if(!config[CONF_FLOWED_CRLF].val.i)  { insert_crlf = 0; }
         }
         /* Two additional characters for optional empty line after paragraph */
         if(insert_crlf)  { llen += (size_t) 2; }
         /* Allocate memory in exponentially increasing chunks */
         while(bi + llen + (size_t) 1 >= len)  /* At least 1 additional byte */
         {
            if(!len)  { len = 256; }
            p = (char*) posix_realloc((void*) buf, len *= (size_t) 2);
            if(NULL == p)
            {
               PRINT_ERROR("Memory allocation failed");
               error = 1;
               break;
            }
            else  { buf = p; }
         }
         if(error)  { break; }
         /* Copy quote marks */
         for(ii = 0; ii < (size_t) qdepth; ++ii)
         {
            strncpy(&buf[bi], quote_mark, strlen(quote_mark));
            bi += strlen(quote_mark);
         }
         /* Copy line */
         strncpy(&buf[bi], &para[start], end - start);
         bi += end - start;
         /* Copy line termination */
         buf[bi++] = (char) 0x0D;  buf[bi++] = (char) 0x0A;
      }
      while(pflowed);
      /* Insert optional empty line separator after paragraph */
      if(insert_crlf)
      {
         buf[bi++] = (char) 0x0D;  buf[bi++] = (char) 0x0A;
         insert_crlf = 0;
      }
      if(error)  { break; }
   }
   posix_free((void*) para);
   if(error)
   {
      PRINT_ERROR("MIME: Decoding of format=flowed content failed");
      posix_free((void*) buf);
      buf = NULL;
   }
   else if(NULL != buf)
   {
      /* Terminate string in target buffer */
      buf[bi] = 0;
   }

   return(buf);
}


/* ========================================================================== */
/*! \brief Extract MIME encapsulated message
 *
 * \param[in]  s    MIME encapsulated message
 * \param[in]  len  Length of encapsulated message
 * \param[out] mpe  MIME multipart entity locations
 *
 * On success a pointer to the result array is written to \e mpe . The caller
 * is responsible to free the memory allocated for this array.
 *
 * \return
 * - 1 on success
 * - 0 on error
 */

size_t  enc_mime_message(const char*  s, size_t  len,
                         struct enc_mime_mpe**  mpe)
{
   size_t  res = 0;
   struct enc_mime_mpe*  array;

   /* Allocate memory for array element */
   array = (struct enc_mime_mpe*) posix_malloc(sizeof(struct enc_mime_mpe));
   if(NULL == array)
   {
      PRINT_ERROR("Parsing encapsulated message aborted");
   }
   else
   {
      /* Store start index and length of entity */
      array[res].start = s;
      array[res++].len = len;
   }

   /* Check for success */
   *mpe = NULL;
   if(res)  { *mpe = array; }

   return(res);
}


/* ========================================================================== */
/*! \brief Parse MIME multipart content
 *
 * \param[in]  s    MIME encoded multipart data
 * \param[in]  b    MIME boundary delimiter
 * \param[out] mpe  MIME multipart entity locations
 *
 * On success a pointer to the result array is written to \e mpe . The caller
 * is responsible to free the memory allocated for this array.
 *
 * \return
 * - Nonzero number of entities in multipart data on success
 * - 0 on error
 */

size_t  enc_mime_multipart(const char*  s, const char*  b,
                           struct enc_mime_mpe**  mpe)
{
   size_t  res = 0;
   size_t  b_len;
   char  boundary[ENC_BO_BUFLEN] = "--";
   size_t  i = 0;
   int  preamble = 1;
   size_t  match;
   size_t  start = 0;
   size_t  end = 0;
   size_t  e_len;
   struct enc_mime_mpe*  array = NULL;
   struct enc_mime_mpe*  tmp;

   b_len = strlen(b);
   /* RFC 2046 limits the boundary delimiter length to 70 characters */
   if(!b_len || (size_t) 70 < b_len)
   {
      PRINT_ERROR("Invalid MIME multipart boundary delimiter");
   }
   else if ((size_t) 75 > ENC_BO_BUFLEN)
   {
      PRINT_ERROR("Value of ENC_BO_BUFLEN must be at least 75");
   }
   else
   {
      /* Add "--" prefix to boundary */
      strncpy(&boundary[2], b, 71);
      b_len += (size_t) 2;
      /* Parse content */
      while(s[i])
      {
         /*
          * Store potential end of entity
          * RFC 2046 specifies that last CRLF of an entity is part of the
          * following boundary delimiter.
          */
         if((size_t) 2 <= i)  { end = i - (size_t) 2; }
         /* Compare boundary with beginning of line */
         match = 0;
         if(!strncmp(&s[i], boundary, b_len))  { match = 1; }
         /* Skip to beginning of next line (this also consumes potential LWS) */
         while(1)
         {
            if(!s[i])  { break; }
            else if((char) 0x0D == s[i++])
            {
               if((char) 0x0A == s[i++])  { break; }
            }
         }
         /* Check for start of entity */
         if(match)
         {
            /* Ignore preamble */
            if(!preamble && end > start)
            {
               /* Allocate memory for new array element */
               e_len = end - start;
               tmp = (struct enc_mime_mpe*)
                     posix_realloc(array, (res + (size_t) 1)
                                          * sizeof(struct enc_mime_mpe));
               if(NULL == tmp)
               {
                  PRINT_ERROR("Parsing multipart message aborted");
                  break;
               }
               else
               {
                  array = tmp;
                  /* Store start index and length of entity */
                  array[res].start = &s[start];
                  array[res++].len = e_len;
               }
            }
            /* Prepare for next entity */
            start = i;
            preamble = 0;
         }
      }
   }

   /* Check for success */
   *mpe = NULL;
   if(res)  { *mpe = array; }

   return(res);
}


/* ========================================================================== */
/*! \brief Percent decoder
 *
 * \param[in] s      String to decode (URI or MIME parameter value)
 * \param[in] clean  Replace NUL and ';' with '_' if nonzero
 *
 * \note
 * The data is decoded in place because it can't be larger after the decoding
 * operation.
 *
 * If \e s is \c NULL no operation is performed and success is returned.
 *
 * \return
 * - Positive value on success (if data in \e s was decoded)
 * - 0 on success (if there was nothing to do)
 * - Negative value if percent encoding in \e s is invalid
 */

int  enc_percent_decode(char*  s, int  clean)
{
   int  res = 0;
   char*  p = s;
   char*  q;
   int  invalid;
   int  v;
   unsigned char  c = 0;
   size_t  len;

   while(NULL != p)
   {
      q = p;
      p = strchr(q, (int) '%');
      if(NULL != p)
      {
         /* Percent sign found => Decode character */
         res = 1;
         if((size_t) 3 > strlen(p))  { res = -1;  break; }
         invalid = 0;
         v = enc_hex_decode_nibble(p[1]);
         if(0 > v)  { invalid = 1; }
         else
         {
            c = (unsigned char) (v * 16);
            v = enc_hex_decode_nibble(p[2]);
            if(0 > v)  { invalid = 1; }
            else  { c += (unsigned char) v; }
         }
         /* Check for invalid data */
         if(invalid)  { res = -1;  break; }
         else
         {
            p[0] = (char) c;
            if(clean)
            {
               /* Replace NUL and ';' with '_' */
               if(!p[0] || ';' == p[0])  { p[0] = '_'; }
            }
            len = strlen(&p[3]);
            memmove((void*) &p[1], (void*) &p[3], ++len);
            ++p;
         }
      }
   }
   if(-1 == res)  { PRINT_ERROR("Percent decoding of URI failed"); }

   return(res);
}


/* ========================================================================== */
/*! \brief Percent encoding for URI content
 *
 * \param[in] s    URI body to encode
 * \param[in] sch  URI scheme
 *
 * Passing \c NULL for parameter \e s is allowed and treated as error.
 *
 * Generic URI syntax is defined in RFC 3986.
 * <br>
 * The scheme "ftp" is defined in RFC 1738.
 * <br>
 * The scheme "http" is defined in RFC 7230.
 * <br>
 * The scheme "mailto" is defined in RFC 6068.
 * <br>
 * The scheme "news" is defined in RFC 5538.
 *
 * The following characters are percent encoded:
 * - Space (not allowed for "mailto" and "news" schemes)
 * - The literal percent sign
 * - The list "gen-delims" defined in RFC 3986
 * - Anything not in the list "unreserved" for "http" and "ftp" schemes
 * - For the "mailto" scheme exactly one "commercial at" sign is required and
 *   treated literally
 * - For the "news" scheme a single "commercial at" sign is accepted literally
 *
 * \return
 * - Pointer to result on success.
 *   If the result is not equal to \e s , a new memory block was allocated
 * - NULL on error
 */

const char*  enc_uri_percent_encode(const char*  s, enum enc_uri_scheme  sch)
{
   const char*  res = NULL;
   const char*  gen_delims  =
      ":/?#[]@";      /* gen-delims */
   const char*  sub_delims  =
      "!$&'()*+,;=";  /* sub-delims */
   const char*  unreserved  =
      "abcdefghijklmnopqrstuvwxyz"  /* Small letters */
      "ABCDEFGHIJKLMNOPQRSTUVWXYZ"  /* Capital letters */
      "0123456789"    /* Digits */
      "-._~";         /* Hyphen, Period, Underscore and Tilde */
   int  process = 0;
   size_t  i = 0;
   char*  buf = NULL;
   size_t  bi = 0;
   int  error = 0;
   int  encode;
   size_t  commercial_at = 0;
   unsigned int  nibble;

   if(NULL != s)
   {
      /* Check whether only unreserved characters are present */
      while(s[i])
      {
        if(NULL == strchr(unreserved, (int) s[i]))  { process = 1;  break; }
        ++i;
      }
      if(!process)  { res = s; }
      else
      {
         /* Allocate new buffer (Triple size is always sufficient) */
         buf = (char*) posix_malloc(strlen(s) * (size_t) 3 + (size_t) 1);
         if(NULL != buf)
         {
            i = 0;
            while(s[i])
            {
               encode = 0;
               switch(sch)
               {
                  case ENC_URI_SCHEME_HTTP:
                  case ENC_URI_SCHEME_FTP:
                  {
                     /*
                      * Because we don't parse the URI syntax, it is not
                      * possible to decide whether a slash is allowed or not
                      * here => Always accept it.
                      */
                     if('/' == s[i])  { encode = 0; }
                     else if(NULL == strchr(unreserved, (int) s[i]))
                     {
                        encode = 1;
                     }
                     break;
                  }
                  case ENC_URI_SCHEME_MAILTO:
                  case ENC_URI_SCHEME_NEWS:
                  {
                     if(ENC_URI_SCHEME_NEWS == sch && '>' == s[i])
                     {
                        /* As defined by RFC 5536 Section 3.1.3 */
                        error = 1;
                     }
                     if(' ' == s[i])  { error = 1; }
                     else if('%' == s[i])  { encode = 1; }
                     else if('@' == s[i])
                     {
                        if(!commercial_at)
                        {
                           /* Accept zero or one "commercial at" signs */
                           ++commercial_at;
                        }
                        else  { error = 1; }
                     }
                     else if(NULL != strchr(gen_delims, (int) s[i]))
                     {
                        encode = 1;
                     }
                     else if(ENC_URI_SCHEME_MAILTO == sch
                             && NULL != strchr(sub_delims, (int) s[i]))
                     {
                        /* Some listed in RFC 6068 Section 2 => Encode all */
                        encode = 1;
                     }
                     break;
                  }
                  default:
                  {
                     PRINT_ERROR("Invalid URI scheme for percent encoding");
                     error = 1;
                     break;
                  }
               }
               if(error)  { break; }
               if(!encode)  { buf[bi++] = s[i]; }
               else
               {
                  /* Percent encoder */
                  buf[bi++] = '%';
                  /* High nibble */
                  nibble = ((unsigned int) s[i] & 0xF0U) >> 4;
                  if(10U > nibble)  { buf[bi] = 0x30; }
                  else  { buf[bi] = 0x41;  nibble -= 10U; }
                  buf[bi++] += (char) nibble;
                  /* Low nibble */
                  nibble = (unsigned int) s[i] & 0x0FU;
                  if(10 > nibble)  { buf[bi] = 0x30; }
                  else  { buf[bi] = 0x41;  nibble -= 10U; }
                  buf[bi++] += (char) nibble;
               }
               ++i;
            }
            /* Terminate result string */
            buf[bi] = 0;
            /* Check for error */
            if(!error)  { res = buf; }
         }
      }
   }
   if(NULL != res)
   {
      /* Ensure that one "@" is present in URI with scheme "mailto" */
      if(ENC_URI_SCHEME_MAILTO == sch && !commercial_at)
      {
         PRINT_ERROR("Missing \"@\" in URI with scheme \"mailto\"");
         error = 1;
      }
   }
   if(error)
   {
      PRINT_ERROR("Percent encoding of URI failed");
      posix_free((void*) buf);
      res = NULL;
   }

   return(res);
}


/* ========================================================================== */
/* Unicode search (case insensitive)
 *
 * This function uses the Unicode Default Case Folding algorithm.
 * This means it is based on the full case folding operations without the
 * context-dependent mappings sensitive to the casing context.
 *
 * https://www.unicode.org/versions/Unicode13.0.0/UnicodeStandard-13.0.pdf
 *
 * According to Unicode 13.0.0 Section 3.13 the following algorithm is required
 * for caseless matching of two strings:
 *
 *    NFD(toCasefold(NFD(X))) == NFD(toCasefold(NFD(Y)))
 *
 * \param[in]  s          String to search in
 * \param[in]  start_pos  Start index in \e s
 * \param[in]  search_s   Search string
 * \param[out] found_pos  Position in \e s where \e search_s was found
 * \param[out] found_len  Length of match in \e s
 *
 * \attention
 * The strings \e s and \e search_s must be normalized to NFC by the caller!
 *
 * \note
 * It is treated as error if \e start_pos points inside a combing character
 * sequence (to a codepoint with nonzero canonical combining class).
 *
 * \return
 * - 0 on success (\e found_pos and \e found_len are valid)
 * - -1 on error (nothing was written to \e found_pos and \e found_len )
 */

int  enc_uc_search(const char*  s, size_t  start_pos, const char*  search_s,
                   size_t*  found_pos, size_t*  found_len)
{
   int  res = -1;
   int  ok = 0, ok2 = 0, ok3 = 0, ok4 = 0, ok5 = 0;
   size_t  search_s_len;  /* Length of search string (in NFD) */
   size_t  s_len;  /* Length of target from start_pos (in NFD) */
   size_t  i, j;
   size_t  bi;  /* Index in case folding target buffer */
   long int  ucp;
   struct uc_cdc  ucp_attr;
   long int  mapping[3];
   size_t  di;
   size_t  match_pos = 0, match_len = 0;  /* With case folding */
   size_t  tmp_pos = 0, end_pos = 0;      /* Without case folding */
   const char*  s_nfd = NULL;
   const char*  search_s_nfd = NULL;
   const char*  s_cf = NULL;
   const char*  search_s_cf = NULL;
   char*  p;
   const char*  q;
   char  utf[4];
   size_t  inc;

   /*
    * Only non-ASCII codepoints (at least 2 bytes in UTF-8) will increase the
    * length. The maximum length of an UTF-8 sequence is 4 bytes.
    * => Worst case increase factor is 2 (per codepoint for UTF-8).
    * Maximum increase factor on codepoint basis is 3 according to
    * Unicode 13.0.0 standard.
    * => Worst case increase factor is 6 (2 * 3 for case folding with UTF-8).
    */
   const size_t  mem_factor = 6;

   /* Check input data and normalize to NFD from start position */
   if(NULL == s || enc_uc_check_utf8(s))  { goto error; }
   i = 0;  ucp = enc_uc_decode_utf8(s, &i);
   if(-1L == ucp)  { goto error; }
   enc_uc_lookup_cdc(ucp, &ucp_attr);
   if(ucp_attr.ccc)  { goto error; }
   s_nfd = enc_uc_normalize_to_nfd(&s[start_pos]);
   if(NULL == s_nfd)  { goto error; }

   /* Check search string and normalize it to NFD */
   if(NULL == search_s || enc_uc_check_utf8(search_s))  { goto error; }
   search_s_nfd = enc_uc_normalize_to_nfd(search_s);
   if(NULL == search_s_nfd)  { goto error; }

   /* Unicode case folding for search_s */
   search_s_len = strlen(search_s_nfd);
   if(!search_s_len)  { goto error; }
   if(search_s_len * mem_factor + (size_t) 1 < search_s_len)
   {
      /* Wraparound in memory size calculation */
      PRINT_ERROR("Memory allocation failed");
   }
   else
   {
      p = (char*) posix_malloc(search_s_len * mem_factor + (size_t) 1);
      if(NULL == p)  { PRINT_ERROR("Memory allocation failed"); }
      else
      {
         i = 0;
         bi = 0;
         while(1)
         {
            ucp = enc_uc_decode_utf8(search_s_nfd, &i);
            if(-1L == ucp)  { break; }
            else
            {
               enc_uc_lookup_cf(ucp, mapping);
               for(j = 0; (size_t) 3 > j; ++j)
               {
                  if(-1L == mapping[j])  { break; }
                  else
                  {
                     di = 1;
                     enc_uc_encode_utf8(p, &bi, &mapping[j], &di);
                  }
               }
            }
         }
         p[bi] = 0;
         /* Normalize target string to NFD again after case folding */
         q = enc_uc_normalize_to_nfd(p);
         if(NULL == q)  { enc_free((void*) p); }
         else
         {
            if(p == q)  { search_s_cf = p; }
            else
            {
               enc_free((void*) p);
               search_s_cf = q;
            }
            match_len = strlen(search_s_cf);
            ok = 1;
         }
      }
   }

   /* Unicode case folding for s */
   if(ok)
   {
      s_len = strlen(s_nfd);
      if(s_len * mem_factor + (size_t) 1 < s_len)
      {
         /* Wraparound in memory size calculation */
         PRINT_ERROR("Memory allocation failed");
      }
      else
      {
         p = (char*) posix_malloc(s_len * mem_factor + (size_t) 1);
         if(NULL == p)  { PRINT_ERROR("Memory allocation failed"); }
         else
         {
            i = 0;
            bi = 0;
            while(1)
            {
               ucp = enc_uc_decode_utf8(s_nfd, &i);
               if(-1L == ucp)  { break; }
               else
               {
                  enc_uc_lookup_cf(ucp, mapping);
                  for(j = 0; (size_t) 3 > j; ++j)
                  {
                     if(-1L == mapping[j])  { break; }
                     else
                     {
                        di = 1;
                        enc_uc_encode_utf8(p, &bi, &mapping[j], &di);
                     }
                  }
               }
            }
            p[bi] = 0;
            /* Normalize target string to NFD again after case folding */
            q = enc_uc_normalize_to_nfd(p);
            if(NULL == q)  { enc_free((void*) p); }
            else
            {
               if(strlen(p) != strlen(q))
               {
                  /*
                   * The result must have the same length (only reordering is
                   * allowed), otherwise the position calculation below will
                   * fail!
                   *
                   * For this implementation:
                   * It is assumed, that the NFD normalization after case
                   * folding of NFD data will not change the length in UTF-8.
                   * Fail gracefully if this assumption is wrong or become
                   * wrong with the database from a future Unicode version
                   * => Report no match and print a bug warning in this case.
                   */
                  PRINT_ERROR("Case folding failed, length changed (bug)");
               }
               else
               {
                  if(p == q)  { s_cf = p; }
                  else
                  {
                     enc_free((void*) p);
                     s_cf = q;
                  }
                  ok2 = 1;
               }
            }
         }
      }
   }

   /* Search with binary compare in case folded data */
   if(ok && ok2)
   {
      p = strstr(s_cf, search_s_cf);
      if(NULL != p)
      {
         match_pos = (size_t) (p - s_cf);
         ok3 = 1;
      }
   }

   /*
    * Unicode normalization and full case folding may have changed the length
    * of the data (in codepoints and in bytes for UTF-8).
    *
    * Therefore both positions of a match, start and end, may be different
    * compared to the original data. The corresponding positions for the
    * original data must be calculated.
    */
   if(ok3)
   {
      /* Calculate start and end offsets of match in (unfolded) NFD data */
      i = 0;
      bi = 0;
      while(1)
      {
         if(bi == match_pos)
         {
            tmp_pos = i;
            ok4 = 1;
         }
         ucp = enc_uc_decode_utf8(s_nfd, &i);
         if(-1L == ucp)  { break; }
         else
         {
            enc_uc_lookup_cf(ucp, mapping);
            for(j = 0; (size_t) 3 > j; ++j)
            {
               if(-1L == mapping[j])  { break; }
               else
               {
                  di = 1;
                  inc = 0;
                  enc_uc_encode_utf8(utf, &inc, &mapping[j], &di);
                  /* Result data is thrown away, only its length is used */
                  bi += inc;
               }
            }
         }
         if(ok4 && (bi == match_pos + match_len))
         {
            end_pos = i;
            ok5 = 1;
            break;
         }
      }

      /* Calculate start and end offsets of match in NFC data */
      if(ok5 && tmp_pos < end_pos)
      {
         p = (char*) posix_malloc(end_pos + (size_t) 1);
         if(NULL == p)  { PRINT_ERROR("Memory allocation failed"); }
         else
         {
            strncpy(p, s_nfd, end_pos);  p[end_pos] = 0;
            q = enc_uc_normalize_to_nfc(p);
            if(NULL != q)
            {
               j = strlen(q);
               if(p != q)  { posix_free((void*) q); }
               if(end_pos >= j)
               {
                  end_pos -= (end_pos - j);
                  /* Calculate start offset of match in original NFC data */
                  p[tmp_pos] = 0;
                  q = enc_uc_normalize_to_nfc(p);
                  if(NULL != q)
                  {
                     j = strlen(q);
                     if(p != q)  { posix_free((void*) q); }
                     if(tmp_pos >= j)
                     {
                        tmp_pos -= (tmp_pos - j);
                        *found_pos = start_pos + tmp_pos;
                        *found_len = end_pos - tmp_pos;
                        res = 0;
                     }
                  }
               }
            }
            posix_free((void*) p);
         }
      }
   }

   posix_free((void*) search_s_cf);
   posix_free((void*) s_cf);

error:
   if(search_s != search_s_nfd)  { posix_free((void*) search_s_nfd); }
   if(&s[start_pos] != s_nfd)  { posix_free((void*) s_nfd); }

   return(res);
}


/* ========================================================================== */
/*! \brief Free an object allocated by encoding module
 *
 * Use this function to release dynamic memory that was allocated by the
 * encoding module.
 *
 * \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  enc_free(void*  p)
{
   /*
    * Attention:
    * Parts of the CORE module (for historical reasons) are still using
    * \c posix_free() to release memory allocated by this module. Until the
    * separation is complete, the memory manager of this module cannot be
    * changed.
    */
   posix_free(p);
}


/*! @} */

/* EOF */
