/* ========================================================================== */
/*! \file
 * \brief Article filtering
 *
 * Copyright (c) 2012-2020 by the developers. See the LICENSE file for details.
 *
 * If nothing else is specified, function 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 <string.h>

#include "conf.h"
#include "core.h"
#include "encoding.h"
#include "filter.h"
#include "fileutils.h"
#include "main.h"
#include "xdg.h"


/* ========================================================================== */
/*! \defgroup FILTER FILTER: Regular expressions and scoring
 *
 * Location of scorefile: \c $XDG_CONFIG_HOME/$CFG_NAME/scorefile
 *
 * Any line starting with \c # is treated as a comment (not parsed and ignored).
 * All other lines are parsed as rules with 4 colon-separated fields:
 * - Field 1: Group wildmat (the rule takes effect for matching groups only)
 * - Field 2: Type (indicating the target element and matching method)
 * - Field 3: Score (signed integer value)
 * - Field 4: String
 * No whitespace is allowed between the fields and separators.
 *
 * \note
 * Because \c : (colon) is used as field separator, it is not allowed to use
 * it in wildmats.
 *
 * Rules with unknown type are ignored.
 *
 * \attention
 * It is required that 'SSIZE_MAX' is at least 'INT_MAX' (must be checked by
 * build system).
 */
/*! @{ */


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

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

/*! \brief Permissions for score file */
#define FILTER_PERM  (posix_mode_t) (POSIX_S_IRUSR | POSIX_S_IWUSR)

/*! \name Score limits
 *
 * Type must be \c int .
 * Minimum and maximum values are \c INT_MIN and \c INT_MAX .
 */
/*! @{ */
#define FILTER_SCORE_MAX  INT_MAX
#define FILTER_SCORE_MIN  INT_MIN
/*! @} */


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

/* Data types of score entries
 *
 * The IDs must start with value 0 (unknown type) and must be contiguous
 * Most used types should be defined first for better performance
 */
enum filter_rule_type
{
   SCORE_TYPE_UNKNOWN     = 0,
   /* ----------------------------------------------------------------------- */
   SCORE_TYPE_FROM        = 1,  /* Literal string vs. 'From' */
   SCORE_TYPE_FROM_ERE    = 2,  /* Extended regular expression vs. 'From' */
   SCORE_TYPE_SUBJECT     = 3,  /* Literal string vs. 'Subject' */
   SCORE_TYPE_SUBJECT_ERE = 4,  /* Extended regular expression vs. 'Subject' */
   SCORE_TYPE_MSGID_ERE   = 5,  /* Extended regular expr. vs. 'Message-ID' */
   SCORE_TYPE_GROUP       = 6,  /* Literal string vs. element of 'Newsgroups' */
   /* ----------------------------------------------------------------------- */
   SCORE_END_OF_LIST      = 7
};

#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
/* Wildmat linked pattern list element */
struct filter_wm
{
   int  negate;
   enum filter_cs  cs;
   posix_regex_t*  ere;
   struct filter_wm*  next;
};
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */

/* Scoring rule */
struct filter
{
   const char*  group_wildmat;
   enum filter_rule_type  type;
   int  value;
   const char*  string;
   int  found;
   struct filter*  next;
   enum filter_cs  cs;
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
   struct filter_wm*  wm;  /* Object for 'group_wildmat' */
   posix_regex_t*  ere;  /* Extended regular expression for 'string' */
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */
};


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

/* Rule names for first field of score file entries
 *
 * \attention
 * The data type 'enum filter_rule_type' must be suitable as index!
 */
static const char*  filter_type_name[] =
{
   "unknown",
   /* ----------------------------------------------------------------------- */
   "from",
   "from_ere",
   "subject",
   "subject_ere",
   "msgid_ere",
   "group",
   /* ----------------------------------------------------------------------- */
   "eol"
};

static enum filter_cs  filter_locale = FILTER_CS_ASCII;
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
static enum filter_cs  testgroup_cs;
static posix_regex_t*  testgroup_ere = NULL;
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */
static size_t  score_len_max = 1000;
static struct filter*  scores = NULL;
static const char  scorefile_name[] = "scorefile";


#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB


/* ========================================================================== */
/* Print error message if system failed to compile a regular expression
 *
 * \param[in] code  Error code
 * \param[in] ere   Pointer to compiled ERE
 *
 * The value \e code must be nonzero and the last error code returned from
 * \c regcomp() for \e ere according to the POSIX standard:
 * <br>
 * http://pubs.opengroup.org/onlinepubs/9699919799/functions/regcomp.html
 *
 * \attention
 * The definition "last error code returned" is incomplete (may mean the last
 * call from a thread or the last call from the whole process)
 * => Always use the filter module from the same, single thread.
 *
 * \note
 * The error message is always formatted using the POSIX locale.
 */

static void  filter_print_ere_error(int  code, posix_regex_t*  ere)
{
   const char*  mod_name = MAIN_ERR_PREFIX;
   size_t  mod_len = strlen(mod_name);
   size_t  len;
   char*  buf = NULL;

   if(!code)
   {
      PRINT_ERROR("Can't process invalid error code");
   }
   else
   {
#  if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI
      /* Don't use NLS for error messages on stderr */
      posix_setlocale(POSIX_LC_MESSAGES, "POSIX");
#  endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI */
      len = posix_regerror(code, ere, buf, 0);
      if(!len || POSIX_SIZE_MAX - mod_len < len)
      {
         PRINT_ERROR("Error message has invalid size");
      }
      else
      {
         buf = (char*) posix_malloc(mod_len + len);
         if(NULL == buf)
         {
            PRINT_ERROR("Cannot allocate memory for error message");
         }
         else
         {
            memcpy(buf, mod_name, mod_len);
            posix_regerror(code, ere, &buf[mod_len], len);
            print_error(buf);
            posix_free((void*) buf);
         }
      }
#  if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI
      posix_setlocale(POSIX_LC_MESSAGES, "");
#  endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI */
   }
}


/* ========================================================================== */
/* Compile ERE
 *
 * \param[out] cs      Pointer to codeset of character classification locale
 * \param[out] ere     Pointer to compiled ERE
 * \param[in]  string  Raw ERE pattern
 *
 * \note
 * On success, the caller is responsible for freeing the ressources allocated
 * for the object pointed to by \e ere .
 */

static int  filter_compile_ere(enum filter_cs*  cs, posix_regex_t**  ere,
                               const char*  string)
{
   int  res = 0;
   int  rv;
   enum enc_mime_cs  charset = ENC_CS_UNKNOWN;
   const char*  pat;
   const char*  p = NULL;

   *cs = filter_locale;
   if(FILTER_CS_ISO8859_1 == filter_locale)
   {
      /* Convert string to ISO 8859-1 */
      p = enc_convert_to_8bit(&charset, string, NULL);
      if(NULL == p)  { res = -1; }
      else if(ENC_CS_ISO8859_1 != charset)  { res = -1; }
   }
   else if(FILTER_CS_UTF_8 != filter_locale)
   {
      /* Treat unsupported codeset as ASCII */
      *cs = FILTER_CS_ASCII;
      res = enc_ascii_check(string);
   }
   if(res)
   {
      /* String cannot be process without UTF-8 locale */
      PRINT_ERROR("ERE cannot be used with current locale");
   }
   else
   {
      /* Allocate memory */
      *ere = (posix_regex_t*)  posix_malloc(sizeof(posix_regex_t));
      if(NULL == *ere)
      {
         PRINT_ERROR("Cannot allocate memory for regular expression");
         res = -1;
      }
      else
      {
         /* Compile regular expression if required */
         pat = string;
         if(FILTER_CS_ISO8859_1 == filter_locale)  { pat = p; }
         rv = posix_regcomp(*ere, pat, POSIX_REG_EXTENDED | POSIX_REG_NOSUB);
         if(rv)
         {
            PRINT_ERROR("Compiling regular expression failed");
            filter_print_ere_error(rv, *ere);
            posix_free((void*) *ere);
            res = -1;
         }
         else if(main_debug)
         {
            printf("%s: %sCompiling regular expression\n",
                   CFG_NAME, MAIN_ERR_PREFIX);
         }
      }
   }
   /* Release memory for ISO 8859-1 string */
   if(NULL != p && string != p)  { enc_free((void*) p); }

   return(res);
}


#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */


/* ========================================================================== */
/* Score rule destructor
 *
 * \param[in,out] rule  Object created by \ref filter_score_rule_contructor()
 */

static void  filter_score_rule_destructor(struct filter**  rule)
{
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
   struct filter_wm*  p;
   struct filter_wm*  q;
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */

   if(NULL != rule && NULL != *rule)
   {
      posix_free((void*) (*rule)->group_wildmat);
      posix_free((void*) (*rule)->string);
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
      /* Destroy wildmat linked pattern list */
      p = (*rule)->wm;
      while(NULL != p)
      {
         q = p->next;
         posix_regfree(p->ere);
         posix_free((void*) p->ere);
         posix_free((void*) p);
         p = q;
      }
      /* Destroy regular expression object for string ERE */
      if(NULL != (*rule)->ere)
      {
         posix_regfree((*rule)->ere);
         posix_free((void*) (*rule)->ere);
      }
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */
      posix_free((void*) *rule);
      *rule = NULL;
   }
}


/* ========================================================================== */
/* Score rule constructor
 *
 * \param[out] new_rule       Pointer to new rule
 * \param[in]  group_wildmat  Limit rule scope to groups matching this wildmat
 * \param[in]  type           Type of new rule
 * \param[in]  score          Score if rule matches
 * \param[in]  string         Literal string or regular expression
 * \param[in]  dcre           Don't compile regular expressions if nonzero
 *
 * The parameter \e dcre should only be nonzero for exporting the rule data
 * back to the scorefile (for comparing the raw regular expression strings).
 *
 * \attention
 * The parameters \e group_wildmat and \e string must point to a memory block
 * allocated with the function \ref posix_malloc() and will become part of the
 * created object.
 * On error the caller stay responsible to free the memory for \e wildmat and
 * \e string .
 *
 * \note
 * On success, the caller is responsible for freeing the ressources allocated
 * for the object \e rule (use \ref filter_score_rule_destructor() function).
 *
 * On error \c NULL is written to \e new_rule .
 */

static int  filter_score_rule_constructor(struct filter**  new_rule,
                                          const char*  group_wildmat,
                                          enum filter_rule_type  type,
                                          int  score, const char*  string,
                                          int  dcre)
{
   int   res = 0;
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
   int  rv;
   struct enc_wm_pattern*  wma;
   struct filter_wm*  pat;
   int  i;
   struct filter_wm*  last = NULL;
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */

   /* Create new rule */
   *new_rule = (struct filter*) posix_malloc(sizeof(struct filter));
   if(NULL == *new_rule)  { res = -1; }
   else
   {
      (*new_rule)->group_wildmat = group_wildmat;
      (*new_rule)->type = type;
      (*new_rule)->value = score;
      (*new_rule)->string = string;
      (*new_rule)->found = 0;
      (*new_rule)->next = NULL;
      (*new_rule)->cs = FILTER_CS_UTF_8;
   }
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
   if(!res)
   {
      (*new_rule)->wm = NULL;
      (*new_rule)->ere = NULL;

      /* Compile regular expressions for wildmat patterns */
      if(!dcre)
      {
         if(strcmp("*", (*new_rule)->group_wildmat))
         {
            rv = enc_create_wildmat(&wma, group_wildmat);
            if(0 < rv)
            {
               /* Process array backwards to get rightmost pattern first */
               for(i = rv; i; --i)
               {
                  pat = (struct filter_wm*)
                        posix_malloc(sizeof(struct filter_wm));
                  if(NULL == pat)  { res = -1;  break; }
                  else
                  {
                     pat->negate = wma[i - 1].negate;
                     pat->cs = FILTER_CS_UTF_8;
                     res = filter_compile_ere(&pat->cs, &pat->ere,
                                              wma[i - 1].ere);
                     pat->next = NULL;
                     /* Link list */
                     if(NULL == last)  { (*new_rule)->wm = pat; }
                     else  { last->next = pat; }
                     last = pat;
                  }
                  if(res)  { break; }
               }
               enc_destroy_wildmat(&wma, rv);
            }
         }
      }

      /* Compile regular expression for string */
      if(!res && !dcre)
      {
         switch((*new_rule)->type)
         {
            case SCORE_TYPE_FROM_ERE:
            case SCORE_TYPE_SUBJECT_ERE:
            case SCORE_TYPE_MSGID_ERE:
            {
               res = filter_compile_ere(&(*new_rule)->cs, &(*new_rule)->ere,
                                        string);
               break;
            }
            default:
            {
               /* Rule do not use a regular expression */
               break;
            }
         }
      }

      /* Check for error */
      if(res)
      {
         PRINT_ERROR("Creating score rule failed");
         /* Mask strings to prevent double free */
         (*new_rule)->group_wildmat = NULL;
         (*new_rule)->string = NULL;
         filter_score_rule_destructor(new_rule);
      }
   }
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */

   return(res);
}


/* ========================================================================== */
/* Calculate and clamp new score
 *
 * \param[in] val  Old value
 * \param[in] val  Difference to old value that should be aplied
 *
 * \return
 * - Updated score
 */

static int  filter_score_add(int  val, int  diff)
{
   /* Check for increase */
   if(0 < diff)
   {
      if(0 < val)
      {
         /* Clamp to upper limit */
         if(FILTER_SCORE_MAX - val < diff)  { val = FILTER_SCORE_MAX; }
         else  { val += diff; }
      }
      else  { val += diff; }
   }

   /* Check for decrease */
   if(0 > diff)
   {
      if(0 > val)
      {
         /* Clamp to lower limit */
         if(FILTER_SCORE_MIN - val > diff)  { val = FILTER_SCORE_MIN; }
         else  { val += diff; }
      }
      else  { val += diff; }
   }

   return(val);
}


/* ========================================================================== */
/* Add score rule
 *
 * \param[in] new_rule  Rule to add
 */

static void  filter_add_score_rule(struct filter*  new_rule)
{
   struct filter*  last_rule = scores;

   if(NULL == last_rule)  { scores = new_rule; }
   else
   {
      /* Append rule to end of list */
      while(NULL != last_rule->next)  { last_rule = last_rule->next; }
      last_rule->next = new_rule;
   }
}


/* ========================================================================== */
/* Delete scoring rules */

static void  filter_delete_score_rules(void)
{
   struct filter*  rule = scores;
   struct filter*  next_rule;

   while(NULL != rule)
   {
      next_rule = rule->next;
      filter_score_rule_destructor(&rule);
      rule = next_rule;
   }
   scores = NULL;
}


/* ========================================================================== */
/* Decode score rule from score file
 *
 * \param[out] rule  Pointer to decoded rule object
 * \param[in]  line  Line from score file
 * \param[in]  len   Line buffer size
 * \param[in]  dcre  Dont't compile potential regular expressions if nonzero
 *
 * On error, \c NULL is written to \e rule .
 *
 * \note
 * On success, the caller is responsible for freeing the ressources allocated
 * for the object \e rule (use \ref filter_score_rule_destructor() function).
 */

static int  filter_decode_rule(struct filter**  rule,
                               const char*  line, size_t  len, int  dcre)
{
   int  res = -1;
   const char*  p;
   char*  q;
   enum filter_rule_type  type;
   size_t  start = 0;
   int  score = 0;
   char*  group_wildmat = NULL;
   size_t  wm_len;
   char*  string = NULL;
   int  rv;
   int  error = 0;

   /* Init rule pointer to defined value */
   *rule = NULL;

   /* Since version 1.0 all rules have a wildmat as first field */
   p = strchr(line, (int) ':');
   if(NULL == p)
   {
      PRINT_ERROR("Malformed rule in score file");
   }
   else
   {
      error = 1;
      wm_len = (size_t) (p - line);
      q = (char*) posix_realloc((void*) group_wildmat, wm_len + (size_t) 1);
      if(NULL == q)
      {
         PRINT_ERROR("Cannot allocate memory for wildmat");
      }
      else
      {
         group_wildmat = q;
         strncpy(group_wildmat, line, wm_len);
         group_wildmat[wm_len] = 0;
         line += wm_len + (size_t) 1;
         error = 0;
      }
   }

   /* Get type */
   type = SCORE_TYPE_UNKNOWN;
   if(!error)
   {
      while(SCORE_END_OF_LIST != ++type)
      {
         p = filter_type_name[type];
         start = strlen(p);
         if(start < len && !strncmp(line, p, start))
         {
            /* Verify that not only the first part has matched */
            if(':' == line[start])  { break; }
         }
      }
      if(SCORE_END_OF_LIST == type)  { type = SCORE_TYPE_UNKNOWN; }
      else if(main_debug)
      {
         printf("%s: %sScore rule type: %s\n", CFG_NAME, MAIN_ERR_PREFIX, p);
      }
   }

   /* Extract string from data field */
   if(!error)
   {
      string = (char*) posix_malloc((size_t) len);
      if(NULL == string)
      {
         PRINT_ERROR("Cannot allocate memory for score rule parser");
      }
      else
      {
         /* Decode data */
         switch(type)
         {
            case SCORE_TYPE_FROM:
            case SCORE_TYPE_FROM_ERE:
            case SCORE_TYPE_SUBJECT:
            case SCORE_TYPE_SUBJECT_ERE:
            case SCORE_TYPE_MSGID_ERE:
            case SCORE_TYPE_GROUP:
            {
               rv = sscanf(&line[start], ":%d:%[^\n]", &score, string);
               if(2 != rv)
               {
                  PRINT_ERROR("Invalid rule in score file");
               }
               else
               {
                  /* Success */
                  res = 0;
               }
               break;
            }
            default:
            {
               PRINT_ERROR("Unknown rule type in score file");
               break;
            }
         }
      }
   }

   /* Create score rule object */
   if(!res)
   {
#if 0
      /* For debugging */
      printf("=============================\n");
      printf("   Groups: %s\n", group_wildmat);
      printf("   Type  : %s\n", filter_type_name[type]);
      printf("   Score : %d\n", score);
      printf("   String: %s\n", string);
#endif
      res = filter_score_rule_constructor(rule, group_wildmat,
                                          type, score, string, dcre);
   }

   /* Release memory on error */
   if(res)
   {
      posix_free((void*) group_wildmat);
      posix_free((void*) string);
   }

   /*
    * For code review:
    * The allocated memory blocks becomes part of the new score rule object!
    */

   return(res);
}


/* ========================================================================== */
/* Encode score rule for score file
 *
 * \param[in,out] line  Pointer to line for score file
 * \param[in,out] len   Pointer to line buffer size
 * \param[in]     rule  Rule to encode
 *
 * \attention
 * The pointer \e line must be \c NULL or point to a dynamically allocated
 * buffer.
 */

static int  filter_encode_rule(char**  line, size_t*  len, struct filter*  rule)
{
   const char*  frt = NULL;  /* Filter Rule Type */
   int  res = -1;
   int  rv;
   char*  p = NULL;
   size_t  l = 0;

   /* Create new score rule */
   switch(rule->type)
   {
      case SCORE_TYPE_FROM:
      case SCORE_TYPE_FROM_ERE:
      case SCORE_TYPE_SUBJECT:
      case SCORE_TYPE_SUBJECT_ERE:
      case SCORE_TYPE_MSGID_ERE:
      case SCORE_TYPE_GROUP:
      {
         frt = filter_type_name[rule->type];
         l += strlen(rule->group_wildmat);
         l += (size_t) 1;     /* Field separator */
         l += strlen(frt);     /* Type ID */
         l += (size_t) 1;     /* Field separator */
         l += score_len_max;  /* Score value */
         l += (size_t) 1;     /* Field separator */
         l += strlen(rule->string);
         l += (size_t) 1;     /* LF line termination */
         l += (size_t) 1;     /* NUL termination */
         p = (char*) posix_malloc(l);
         if(NULL == p)
         {
            PRINT_ERROR("Cannot allocate memory for score rule");
         }
         else
         {
            /* Since version 1.0 all rules use a wildmat */
            rv = posix_snprintf(p, l, "%s:%s:%d:%s\n", rule->group_wildmat,
                                frt, rule->value, rule->string);
            if(0 > rv || (size_t) rv >= l)
            {
               PRINT_ERROR("Encoding score rule failed");
               posix_free((void*) p);
            }
            else
            {
               /* Success => Replace line buffer */
               posix_free((void*) *line);
               *line = p;
               *len = l;
               res = 0;
            }
         }
         break;
      }
      default:
      {
         PRINT_ERROR("Encoding unknown rule type failed");
         break;
      }
   }

   return(res);
}


/* ========================================================================== */
/* Check whether score rule match
 *
 * \param[in] line  Line from score file
 * \param[in] len   Line buffer size
 * \param[in] rule  Rule to match
 */

static int  filter_check_rule(char*  line, size_t  len, struct filter*  rule)
{
   int  res = -1;
   struct filter*  current_rule;

   /* Decode line */
   if('#' != line[0])
   {
      /* Set 'dcre' flag, we only want to compare the strings of the rule */
      if(!filter_decode_rule(&current_rule, line, len, 1))
      {
         /* Check whether score has changed */
         if(current_rule->type == rule->type)
         {
            if(!strcmp(current_rule->string, rule->string))
            {
               /* Match detected */
               res = 0;
            }
         }
         /* Destroy current rule object */
         filter_score_rule_destructor(&current_rule);
      }
   }

   return(res);
}


/* ========================================================================== */
/* Export score rules
 *
 * \param[in] fs      Stream corresponding to old configuration
 * \param[in] fs_tmp  Stream corresponding to new configuration
 *
 * The current data in memory is merged with the data from \e fs and written
 * to \e fs_tmp .
 */

static int  filter_export_score_rules(FILE*  fs, FILE*  fs_tmp)
{
   int  res = -1;
   char*  line = NULL;
   size_t  len = 0;
   posix_ssize_t  readlen;
   int  rv;
   struct filter*  rule;

   if(main_debug)
   {
      printf("%s: %sStore scoring rules\n", CFG_NAME, MAIN_ERR_PREFIX);
   }

   while(1)
   {
      /* Read line */
      readlen = posix_getline(&line, &len, fs);
      if(-1 == readlen)
      {
         if(POSIX_ENOMEM == posix_errno)
         {
            PRINT_ERROR("Cannot assign memory for score file parser");
         }
         else
         {
            /* Check for error */
            if(ferror(fs))
            {
               PRINT_ERROR("Parse error in score file");
            }
            /* Check for EOF */
            else if(feof(fs))
            {
               res = 0;
            }
         }
      }
      if(0 >= readlen)  { break; }
      else
      {
         /* Update data */
         rule = scores;
         while(NULL != rule)
         {
            if(!rule->found && !filter_check_rule(line, len, rule))
            {
               /* Match => Update */
               rule->found = 1;
               filter_encode_rule(&line, &len, rule);
               break;
            }
            rule = rule->next;
         }

         /* Write line to new config file */
         rv = fprintf(fs_tmp, "%s", line);
         if(0 > rv)  { break; }
      }
   }

   /* Add missing entries to end of config file */
   if(!res)
   {
      rule = scores;
      while(NULL != rule)
      {
         if(!rule->found)
         {
            rv = filter_encode_rule(&line, &len, rule);
            if(rv)  { res = -1;  break; }
            else
            {
               /* Write new rule */
               rv = fprintf(fs_tmp, "%s", line);
               if(0 > rv)
               {
                  res = -1;
                  break;
               }
            }
         }
         rule = rule->next;
      }
   }

   /* Release memory for line buffer */
   posix_free((void*) line);

   return(res);
}


/* ========================================================================== */
/* Get scorefile pathname
 *
 * This function must be thread safe.
 * The caller is responsible to free the memory for the buffer on success.
 */

static int  filter_get_pathname(const char**  pathname, const char*  filename)
{
   int  res = -1;
   int  rv;

   *pathname = xdg_get_confdir(CFG_NAME);
   if(NULL != *pathname)
   {
      rv = fu_create_path(*pathname, (posix_mode_t) POSIX_S_IRWXU);
      if(0 == rv)
      {
         /* Store scorefile pathname */
         rv = xdg_append_to_path(pathname, filename);
         if(0 == rv)
         {
            res = 0;
         }
      }
   }

   /* Free memory on error */
   if(0 != res)
   {
      PRINT_ERROR("Cannot create score file pathname");
      posix_free((void*) *pathname);
      *pathname = NULL;
   }

   return(res);
}


/* ========================================================================== */
/* Check whether a group in the list matches a wildmat
 *
 * Returns success (zero) if one of the groups in \e grouplist matches
 * \e wildmat .
 */

static int  filter_group_check(struct filter*  rule, const char**  grouplist)
{
   int  res = -1;
   size_t  i;
   const char*  group;
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
   struct filter_wm*  plp = rule->wm;
   int  rv;
   const char*  string;
   const char*  p = NULL;
   enum enc_mime_cs  charset = ENC_CS_UNKNOWN;
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */

   i = 0;
   /* Assignment in truth expression is intended! */
   while(NULL != (group = grouplist[i++]))
   {
      /* "Match all" must always work */
      if(!strcmp("*", rule->group_wildmat))  { res = 0;  break; }
      /* Check for literal match (usable for all locales) */
      if(!strcmp(rule->group_wildmat, group))  { res = 0;  break; }
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
      /* Check with regular expressions created from group wildmat */
      while(NULL != plp)
      {
         rv = 0;
         string = group;
         if(FILTER_CS_ASCII == plp->cs)
         {
            rv = enc_ascii_check(string);
         }
         else if(FILTER_CS_ISO8859_1 == plp->cs)
         {
            /* Try to convert data to ISO 8859-1 */
            p = enc_convert_to_8bit(&charset, string, NULL);
            if(NULL == p)  { rv = -1; }
            else if(ENC_CS_ISO8859_1 != charset)  { rv = -1; }
            else  { string = p; }
         }
         if(!rv && !posix_regexec(plp->ere, string, 0, NULL, 0))  { res = 0; }
         /* Release memory for ISO 8859-1 string */
         if(NULL != p && group != p)  { enc_free((void*) p); }
         /* Check for ERE match */
         if(!res)
         {
            /* printf("ERE of wildmat pattern matched\n"); */
            if(plp->negate)  { res = -1; }
            break;
         }
         /* Next pattern in list */
         plp = plp->next;
      }
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Initialize filter module
 *
 * \param[in] utf8  Flag indicating that the locale use UTF-8 encoding
 *
 * \attention
 * Remember that the locale must use either UTF-8 or ISO 8859-1 codeset or be
 * the POSIX locale.
 *
 * Step1 (only if \c CONF_SCORERC is configured):
 * - Rename current \c scorefile to \c scorefile.old
 * - Copy pathname configured with \c CONF_SCORERC to \c scorefile
 *
 * Step 2:
 * - Open and lock scorefile
 * - Load rules from scorefile to memory
 *
 * \return
 * - 0 on success
 * - Negative value on error
 */

int  filter_init(int  utf8)
{
   int  res = -1;
   const char*  scorerc = config[CONF_SCORERC].val.s;
   const char*  scorepathname = NULL;
   char*  oldscorepathname = NULL;
   int  rv;
   struct_posix_stat  state;
   int  fd = -1;
   FILE*  fs = NULL;
   char*  data = NULL;
   size_t  len;
   char*  line = NULL;
   posix_ssize_t  readlen;
   struct filter*  rule;
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI
   const char*  loc_ctype;
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI */

#if !(CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB)
   PRINT_ERROR("Regular expression support disabled by configuration");
#endif  /* !(CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB) */

   /* Set locale and check codeset */
   filter_locale = FILTER_CS_ASCII;
#if !(CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI)
#  if CFG_USE_CLB
   PRINT_ERROR("Cannot set locale due to configuration");
#  endif  /* CFG_USE_CLB */
   printf("%s: %sCooked character classification codeset: "
          "US-ASCII\n", CFG_NAME, MAIN_ERR_PREFIX);
#else  /* !(CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI) */
   loc_ctype = posix_setlocale(POSIX_LC_CTYPE, "");
   if(NULL == loc_ctype)
   {
      PRINT_ERROR("Setting locale for category 'LC_CTYPE' failed");
      return(-1);
   }
   else
   {
      printf("%s: %sCharacter classification locale: %s\n",
             CFG_NAME, MAIN_ERR_PREFIX, loc_ctype);
      if(utf8)
      {
         printf("%s: %sCooked character classification codeset: "
                "UTF-8\n", CFG_NAME, MAIN_ERR_PREFIX);
         filter_locale = FILTER_CS_UTF_8;
      }
      else
      {
#  if CFG_USE_XSI
         loc_ctype = posix_nl_langinfo(CODESET);
#  endif  /* CFG_USE_XSI */
         /* Check whether fallback to ISO 8859-1 is possible */
         if( NULL != strstr(loc_ctype, "8859-1")
             || NULL != strstr(loc_ctype, "8859_1")
             || NULL != strstr(loc_ctype, "88591") )
         {
            /* Verify that it is not something like "8859-15" */
            if('1' == loc_ctype[strlen(loc_ctype) - (size_t) 1])
            {
               printf("%s: %sCooked character classification codeset: "
                      "ISO-8859-1\n", CFG_NAME, MAIN_ERR_PREFIX);
               filter_locale = FILTER_CS_ISO8859_1;
            }
            else
            {
               PRINT_ERROR("Codeset of locale not supported");
               PRINT_ERROR("Supported codesets: US-ASCII, ISO-8859-1, UTF-8");
               PRINT_ERROR("(Use \"locale -a\" to find a locale)");
               return(-1);
            }
         }
         else
         {
            if( !strcmp(loc_ctype, "POSIX")
               || !strcmp(loc_ctype, "C")
               || NULL != strstr(loc_ctype, "ASCII")
               || NULL != strstr(loc_ctype, "X3.4") )
            {
               printf("%s: %sCooked character classification codeset: "
                      "US-ASCII\n", CFG_NAME, MAIN_ERR_PREFIX);
            }
            else
            {
               PRINT_ERROR("Codeset of locale not supported");
               PRINT_ERROR("Supported codesets: US-ASCII, ISO-8859-1, UTF-8");
               PRINT_ERROR("(Use \"locale -a\" to find a locale)");
               return(-1);
            }
         }
      }
   }
#endif  /* !(CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI) */

   /*
    * Calculate maximum length of score value strings
    *
    * Note: Use of snprintf() must no longer be SUS Version 2 compatible.
    * C99/POSIX.1-2001/SUSv3 semantics are now provided by the POSIX module.
    */
   rv = posix_snprintf(NULL, 0, "%d", FILTER_SCORE_MAX);
   if(0 <= rv)
   {
      score_len_max = (size_t) rv;
      rv = posix_snprintf(NULL, 0, "%d", FILTER_SCORE_MIN);
      if(0 <= rv)
      {
         if((size_t) rv > score_len_max)  { score_len_max = (size_t) rv; }
         res = 0;
      }
   }
   if(res)
   {
      PRINT_ERROR("Calculation of maximum score string length failed");
      return(res);
   }

   /* Step 1 */
   if(strlen(scorerc))
   {
      if(main_debug)
      {
         printf("%s: %sImport external scorerc: %s\n",
                CFG_NAME, MAIN_ERR_PREFIX, scorerc);
      }
      rv = posix_stat(scorerc, &state);
      if(rv)  { PRINT_ERROR("Cannot stat scorerc file"); }
      else if(POSIX_S_ISREG(state.st_mode))
      {
         rv = filter_get_pathname(&scorepathname, scorefile_name);
         if(!rv)
         {
            /* Read scorerc file */
            rv = fu_open_file(scorerc, &fd, POSIX_O_RDWR, (posix_mode_t) 0);
            if(!rv)
            {
               rv = fu_lock_file(fd);
               if(!rv)
               {
                  rv = fu_read_whole_file(fd, &data, &len);
               }
               fu_close_file(&fd, NULL);
               if(!rv)
               {
                  oldscorepathname =
                     posix_malloc(strlen(scorepathname) + (size_t) 5);
                  if(NULL == oldscorepathname)
                  {
                     PRINT_ERROR("Cannot allocate memory for pathname");
                  }
                  else
                  {
                     strcpy(oldscorepathname, scorepathname);
                     strcat(oldscorepathname, ".old");
                     rv = posix_rename(scorepathname, oldscorepathname);
                     if(rv)
                     {
                        PRINT_ERROR("Renaming score file failed");
                     }
                     rv = fu_open_file(scorepathname, &fd,
                                       POSIX_O_WRONLY | POSIX_O_CREAT,
                                       FILTER_PERM);
                     if(!rv)
                     {
                        rv = fu_lock_file(fd);
                        if(!rv)
                        {
                           len = strlen(data);
                           rv = fu_write_to_filedesc(fd, data, len);
                        }
                        fu_close_file(&fd, NULL);
                     }
                  }
               }
            }
         }
      }
      if(rv)
      {
         PRINT_ERROR("Importing scorerc failed, using local scorefile");
      }
   }
   /* Release memory */
   posix_free((void*) data);
   posix_free((void*) oldscorepathname);
   posix_free((void*) scorepathname);
   scorepathname = NULL;

   /* Step 2 */
   rv = filter_get_pathname(&scorepathname, scorefile_name);
   if(!rv)
   {
      rv = posix_stat(scorepathname, &state);
      if(rv)  { PRINT_ERROR("Cannot stat score file"); }
      else if(POSIX_S_ISREG(state.st_mode))
      {
         rv = fu_open_file(scorepathname, &fd, POSIX_O_RDWR, (posix_mode_t) 0);
         if(!rv)
         {
            rv = fu_lock_file(fd);
            if(!rv)
            {
               rv = fu_assign_stream(fd, &fs, "r");
               if(!rv)
               {
                  /* Load scoring rules */
                  if(main_debug)
                  {
                     printf("%s: %sLoad scoring rules from: %s\n",
                            CFG_NAME, MAIN_ERR_PREFIX, scorepathname);
                  }
                  while(1)
                  {
                     /* Read line */
                     readlen = posix_getline(&line, &len, fs);
                     if(-1 == readlen)
                     {
                        if(POSIX_ENOMEM == posix_errno)
                        {
                           PRINT_ERROR("Cannot allocate memory for score "
                                       "file parser");
                        }
                        else
                        {
                           /* Check for error */
                           if(ferror(fs))
                           {
                              PRINT_ERROR("Parse error in score file");
                           }
                        }
                     }
                     if(0 >= readlen)  { break; }
                     else
                     {
                        /* Extract data */
                        if('#' == line[0])  { continue; }
                        rv = filter_decode_rule(&rule, line, (size_t) readlen,
                                                0);
                        if(!rv)  { filter_add_score_rule(rule); }
                     }
                  }
                  posix_free((void*) line);
                  rv = 0;
               }
            }
         }
         fu_close_file(&fd, &fs);
      }
      if(rv)  { PRINT_ERROR("Importing rules from score file failed"); }
   }
   /* Release memory */
   posix_free((void*) scorepathname);

#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
   /* Initialize test group checking facility */
   if(!strlen(config[CONF_TESTGRP_ERE].val.s))
   {
      PRINT_ERROR("No ERE for test group matching found in config file");
      rv = -1;
   }
   else
   {
      /* Compile testgroup ERE */
      printf("%s: %sEnabling test group checking facility\n",
             CFG_NAME, MAIN_ERR_PREFIX);
      rv = filter_compile_ere(&testgroup_cs, &testgroup_ere,
                              config[CONF_TESTGRP_ERE].val.s);
   }
   if(rv)  { testgroup_ere = NULL; }
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */

   return(res);
}


/* ========================================================================== */
/*! \brief Shutdown filter module
 *
 * Step 1:
 * - Open and lock scorefile
 * - Save scoring rules in memory to score file
 * - Delete scoring rules from memory
 *
 * Step 2 (only if \c CONF_SCORERC is configured):
 * - Copy \c scorefile to the location configured with \c CONF_SCORERC
 */

void  filter_exit(void)
{
   const char*  scorerc = config[CONF_SCORERC].val.s;
   const char*  scorepathname = NULL;
   char*  tmppathname = NULL;
   int  rv;
   int  fd = -1;
   FILE*  fs = NULL;
   int  fd_tmp = -1;
   FILE*  fs_tmp = NULL;
   char*  data = NULL;
   size_t  len;
   struct_posix_stat  state;
   char*  p;

   /* Step 1 */
   rv = filter_get_pathname(&scorepathname, scorefile_name);
   if(!rv)
   {
      /* Create scorefile if it does not exist */
      rv = posix_stat(scorepathname, &state);
      if(rv && POSIX_ENOENT == posix_errno)
      {
         fu_open_file(scorepathname, &fd_tmp,
                      POSIX_O_WRONLY | POSIX_O_CREAT, FILTER_PERM);
         fu_close_file(&fd_tmp, NULL);
      }
      /* Open scorefile */
      rv = posix_stat(scorepathname, &state);
      if(rv)  { PRINT_ERROR("Cannot stat score file"); }
      else if(POSIX_S_ISREG(state.st_mode))
      {
         rv = fu_open_file(scorepathname, &fd, POSIX_O_RDWR, (posix_mode_t) 0);
         if(!rv)
         {
            rv = fu_lock_file(fd);
            if(!rv)
            {
               rv = fu_assign_stream(fd, &fs, "r");
               if(!rv)
               {
                  /* Open temporary file */
                  tmppathname = posix_malloc(strlen(scorepathname)
                                             + (size_t) 5);
                  if(NULL == tmppathname)
                  {
                     PRINT_ERROR("Cannot allocate memory for pathname");
                  }
                  else
                  {
                     strcpy(tmppathname, scorepathname);
                     strcat(tmppathname, ".new");
                     rv = fu_open_file(tmppathname, &fd_tmp, POSIX_O_WRONLY
                                       | POSIX_O_CREAT | POSIX_O_TRUNC,
                                       FILTER_PERM);
                     /*
                      * Because we have the lock for the score file, it is
                      * allowed to assume that no other instance of the program
                      * currently use the temporary filename.
                      */
                     if(!rv)
                     {
                        rv = fu_assign_stream(fd_tmp, &fs_tmp, "w");
                        if(!rv)
                        {
                           rv = filter_export_score_rules(fs, fs_tmp);
                        }
                        /* Flush stream of temporary file*/
                        if (!rv)  { rv = fu_sync(fd_tmp, fs_tmp); }
                        /* Rename temporary file to score file */
                        if(!rv)
                        {
                           rv = posix_rename(tmppathname, scorepathname);
                        }
                        if(rv)
                        {
                           if(tmppathname)
                           {
                              (void) fu_unlink_file(tmppathname);
                           }
                        }
                     }
                     fu_close_file(&fd_tmp, &fs_tmp);
                  }
               }
            }
         }
         fu_close_file(&fd, &fs);
      }
      if(rv)  { PRINT_ERROR("Exporting rules to score file failed"); }
   }
   filter_delete_score_rules();

   /* Step 2 */
   if(!rv && strlen(scorerc))
   {
      /* Read scorefile */
      rv = fu_open_file(scorepathname, &fd, POSIX_O_RDWR,
                        (posix_mode_t) 0);
      if(!rv)
      {
         rv = fu_lock_file(fd);
         if(!rv)
         {
            rv = fu_read_whole_file(fd, &data, &len);
         }
         fu_close_file(&fd, NULL);
         if(!rv)
         {
            /* Write scorerc file */
            if(main_debug)
            {
               printf("%s: %sExport to external scorerc: %s\n",
                      CFG_NAME, MAIN_ERR_PREFIX, scorerc);
            }
            p = posix_realloc(tmppathname, strlen(scorerc) + (size_t) 5);
            if(NULL == p)
            {
               PRINT_ERROR("Cannot allocate memory for pathname");
            }
            else
            {
               tmppathname = p;
               strcpy(tmppathname, scorerc);
               strcat(tmppathname, ".new");
               rv = fu_open_file(tmppathname, &fd,
                                 POSIX_O_WRONLY | POSIX_O_CREAT, FILTER_PERM);
               if(!rv)
               {
                  rv = fu_lock_file(fd);
                  if(!rv)
                  {
                     len = strlen(data);
                     rv = fu_write_to_filedesc(fd, data, len);
                     if(rv)  { rv = fu_sync(fd, NULL); }
                     if(rv)
                     {
                        PRINT_ERROR("Writing data to scorerc file failed");
                     }
                     else
                     {
                        rv = posix_rename(tmppathname, scorerc);
                        if(rv)
                        {
                           PRINT_ERROR("Renaming new scorerc file failed");
                        }
                     }
                  }
                  fu_close_file(&fd, NULL);
               }
            }
         }
      }
      if(rv)
      {
         PRINT_ERROR("Exporting score file data to scorerc failed");
      }
   }

   /* Release memory */
   posix_free((void*) data);
   posix_free((void*) tmppathname);
   posix_free((void*) scorepathname);

#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
   /* Destroy testgroup ERE */
   if(NULL != testgroup_ere)
   {
      /* Destroy regular expression object */
      posix_regfree(testgroup_ere);
      posix_free((void*) testgroup_ere);
      testgroup_ere = NULL;
   }
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */

   /* Clear locale configuration */
   filter_locale = FILTER_CS_ASCII;
}


/* ========================================================================== */
/*! \brief Check for test group
 *
 * \param[in] group  Single newsgroup name (not list)
 *
 * The test group ERE from the configuration is used for matching.
 *
 * \return
 * - 1 if \e group is a test group
 * - 0 otherwise
 */

int  filter_check_testgroup(const char*  group)
{
   int  res = 0;
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
   int  rv = 0;
   const char*  string;
   const char*  p = NULL;
   enum enc_mime_cs  charset = ENC_CS_UNKNOWN;

   /* Check whether testgroup ERE really was compiled */
   if(NULL == testgroup_ere)
   {
      PRINT_ERROR("Test group check failed (ERE not compiled)");
   }
   else
   {
      string = group;
      if(FILTER_CS_ASCII == testgroup_cs)
      {
         rv = enc_ascii_check(string);
      }
      else if(FILTER_CS_ISO8859_1 == testgroup_cs)
      {
         /* Try to convert data to ISO 8859-1 */
         p = enc_convert_to_8bit(&charset, string, NULL);
         if(NULL == p)  { rv = -1; }
         else if(ENC_CS_ISO8859_1 != charset) { rv = -1; }
         else  { string = p; }
      }
      if(rv)
      {
         PRINT_ERROR("Test group name cannot be checked with current locale");
      }
      else if(!posix_regexec(testgroup_ere, string, 0, NULL, 0))
      {
         printf("%s: %sTest group detected\n", CFG_NAME, MAIN_ERR_PREFIX);
         res = 1;
      }
   }

   /* Release memory for ISO 8859-1 string */
   if(NULL != p && group != p)  { enc_free((void*) p); }
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */

   return(res);
}


/* ========================================================================== */
/*! \brief Check for own article
 *
 * \param[in] he  Pointer to article hierarchy element
 *
 * The identity configuration is used as reference for matching.
 *
 * \return
 * - 1 if \e he corresponds to own article
 * - 0 otherwise
 */

int  filter_match_own(const struct core_hierarchy_element*  he)
{
   int  res = 0;

   if(NULL != he->header)
   {
      /*
       * Note 1:
       * The element 'from' (corresponding to the mandatory header field "From")
       * is never 'NULL'. If the header field is missing in the article, the
       * constructor for the hierarchy element in the CORE module inserts a
       * valid empty string.
       *
       * Note 2:
       * Configurations elements of string type are never 'NULL'. If no value
       * is found in the configfile, the FILTER module inserts valid empty
       * strings.
       */

      /*
       * This is the simple default rule
       * It matches to the identity configuration of the user.
       */
      if(he->header->from[0] && config[CONF_FROM].val.s[0])
      {
         if(!strcmp(he->header->from, config[CONF_FROM].val.s)
            || !strcmp(he->header->from, config[CONF_REPLYTO].val.s))
         {
            res = 1;
         }
      }
      /* Hook in more sophisticated custom code here if desired */
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Check for reply to own article
 *
 * \param[in] he  Pointer to article hierarchy element
 *
 * \return
 * - 1 if \e he corresponds to a reply to an own article
 * - 0 otherwise
 */

int  filter_match_reply_to_own(const struct core_hierarchy_element*  he)
{
   int  res = 0;
   const char*  last_ref = "";
   size_t  i;

   if(NULL != he->parent)
   {
      res = filter_match_own(he->parent);
      if(res)
      {
         /* Verify that there are no missing articles in between */
         if(NULL != he->header->refs)
         {
            i = 0;
            while(NULL != he->header->refs[i])
            {
               last_ref = he->header->refs[i++];
            }
         }
         if(strcmp(last_ref, he->parent->header->msgid))
         {
            /* There are missing article(s) in between */
            res = 0;
         }
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Get article score
 *
 * \param[in] he  Pointer to article hierarchy element
 *
 * \return
 * - Score of article
 * - 0 if no score is defined for article corresponding to \e he .
 */

int  filter_get_score(const struct core_hierarchy_element*  he)
{
   int  res = 0;
   struct filter*  rule = scores;
   const char*  data;
   size_t  i;
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
   int  rv;
   const char*  string;
   const char*  p = NULL;
   enum enc_mime_cs  charset = ENC_CS_UNKNOWN;
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */

   if(NULL != he->header)
   {
      while(NULL != rule)
      {
         /* Check whether group matches wildmat */
         if(!filter_group_check(rule, he->header->groups))
         {
            /* Yes => Check rule type */
            data = NULL;
            switch(rule->type)
            {
               /* ----------------------------------------------------------- */
               /* Literal matching */
               case SCORE_TYPE_FROM:
               {
                  if(NULL == data)  { data = he->header->from; }
                  /* No break here is intended! */
               }
               case SCORE_TYPE_SUBJECT:
               {
                  if(NULL == data)  { data = he->header->subject; }
                  if(!strcmp(rule->string, data))
                  {
                     res = filter_score_add(res, rule->value);
                  }
                  break;
               }
               /* ----------------------------------------------------------- */
               /* Literal matching against field element */
               case SCORE_TYPE_GROUP:
               {
                  i = 0;
                  /* Assignment in truth expression is intended! */
                  while(NULL != (data = he->header->groups[i++]))
                  {
                     if(!strcmp(rule->string, data))
                     {
                        res = filter_score_add(res, rule->value);
                     }
                  }
                  break;
               }
               /* ----------------------------------------------------------- */
               /* Extended regular expression matching */
               case SCORE_TYPE_FROM_ERE:
               {
                  if(NULL == data)  { data = he->header->from; }
                  /* No break here is intended! */
               }
               case SCORE_TYPE_SUBJECT_ERE:
               {
                  if(NULL == data)  { data = he->header->subject; }
                  /* No break here is intended! */
               }
               case SCORE_TYPE_MSGID_ERE:
               {
                  if(NULL == data)  { data = he->header->msgid; }
#if CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB
                  if(NULL == rule->ere)
                  {
                     /*
                      * If this happens, the 'dcre' parameter of the score rule
                      * constructor was not used correctly.
                      */
                     PRINT_ERROR("Regular expression not compiled (bug)");
                  }
                  else
                  {
                     rv = 0;
                     string = data;
                     if(FILTER_CS_ASCII == rule->cs)
                     {
                        rv = enc_ascii_check(string);
                     }
                     else if(FILTER_CS_ISO8859_1 == rule->cs)
                     {
                        /* Try to convert data to ISO 8859-1 */
                        p = enc_convert_to_8bit(&charset, string, NULL);
                        if(NULL == p)  { rv = -1; }
                        else if(ENC_CS_ISO8859_1 != charset)  { rv = -1; }
                        else  { string = p; }
                     }
                     if(!rv && !posix_regexec(rule->ere, string, 0, NULL, 0))
                     {
                        res = filter_score_add(res, rule->value);
                     }
                     /* Release memory for ISO 8859-1 string */
                     if(NULL != p && data != p)  { enc_free((void*) p); }
                  }
#endif  /* CFG_USE_POSIX_API >= 200112 || CFG_USE_XSI || CFG_USE_CLB */
                  break;
               }
               /* ----------------------------------------------------------- */
               default:
               {
                  PRINT_ERROR("Unknown type of score rule (bug)");
                  break;
               }
            }
         }
         rule = rule->next;
      }
   }

   return(res);
}


/* ========================================================================== */
/*! \brief Get codeset of locale category \c LC_CTYPE
 *
 * \return
 * - Codeset ID of locale category \c LC_CTYPE
 */

enum filter_cs  filter_get_locale_ctype(void)
{
   return(filter_locale);
}


/*! @} */

/* EOF */
