"Fossies" - the Fresh Open Source Software Archive

Member "tin-2.6.2/src/filter.c" (9 Dec 2022, 60003 Bytes) of package /linux/misc/tin-2.6.2.tar.xz:


As a special service "Fossies" has tried to format the requested source page into HTML format using (guessed) C and C++ source code syntax highlighting (style: standard) with prefixed line numbers and code folding option. Alternatively you can here view or download the uninterpreted source code file. For more information about "filter.c" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 2.6.1_vs_2.6.2.

A hint: This file contains one or more very long lines, so maybe it is better readable using the pure text view mode that shows the contents as wrapped lines within the browser window.


    1 /*
    2  *  Project   : tin - a Usenet reader
    3  *  Module    : filter.c
    4  *  Author    : I. Lea
    5  *  Created   : 1992-12-28
    6  *  Updated   : 2022-08-29
    7  *  Notes     : Filter articles. Kill & auto selection are supported.
    8  *
    9  * Copyright (c) 1991-2023 Iain Lea <iain@bricbrac.de>
   10  * All rights reserved.
   11  *
   12  * Redistribution and use in source and binary forms, with or without
   13  * modification, are permitted provided that the following conditions
   14  * are met:
   15  *
   16  * 1. Redistributions of source code must retain the above copyright notice,
   17  *    this list of conditions and the following disclaimer.
   18  *
   19  * 2. Redistributions in binary form must reproduce the above copyright
   20  *    notice, this list of conditions and the following disclaimer in the
   21  *    documentation and/or other materials provided with the distribution.
   22  *
   23  * 3. Neither the name of the copyright holder nor the names of its
   24  *    contributors may be used to endorse or promote products derived from
   25  *    this software without specific prior written permission.
   26  *
   27  * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
   28  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
   29  * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   30  * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
   31  * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
   32  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
   33  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   34  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
   35  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
   36  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
   37  * POSSIBILITY OF SUCH DAMAGE.
   38  */
   39 
   40 
   41 #ifndef TIN_H
   42 #   include "tin.h"
   43 #endif /* !TIN_H */
   44 #ifndef VERSION_H
   45 #   include "version.h"
   46 #endif /* !VERSION_H */
   47 #ifndef TCURSES_H
   48 #   include "tcurses.h"
   49 #endif /* !TCURSES_H */
   50 
   51 
   52 #define IS_READ(i)  (arts[i].status == ART_READ)
   53 #define IS_KILLED(i)    (arts[i].killed)
   54 #define IS_KILLED_UNREAD(i) (arts[i].killed == ART_KILLED_UNREAD)
   55 #define IS_SELECTED(i)  (arts[i].selected)
   56 
   57 /*
   58  * SET_FILTER in group grp, current article arts[i], with rule ptr[j]
   59  *
   60  * filtering is now done this way:
   61  * a. set score for all articles and rules
   62  * b. check each article if the score is above or below the limit
   63  *
   64  * SET_FILTER is now somewhat shorter, as the real filtering is done
   65  * at the end of filter_articles()
   66  */
   67 
   68 #define SET_FILTER(grp, i, j)   \
   69     if (ptr[j].score > 0) { \
   70         arts[i].score = (SCORE_MAX - ptr[j].score >= arts[i].score) ? \
   71         (arts[i].score + ptr[j].score) : SCORE_MAX ; \
   72     } else { \
   73         arts[i].score = (-SCORE_MAX - ptr[j].score <= arts[i].score) ? \
   74         (arts[i].score + ptr[j].score) : -SCORE_MAX ; }
   75 
   76 /*
   77  * These will probably go away when filtering is rewritten
   78  * Easier access to hashed msgids. Note that in REFS(), y must be free()d
   79  * msgid is mandatory in an article and cannot be NULL
   80  */
   81 #define MSGID(x)            (x->refptr ? x->refptr->txt : "")
   82 #define REFS(x,y)           ((y = get_references(x->refptr ? x->refptr->parent : NULL)) ? y : "")
   83 
   84 /*
   85  * global filter array
   86  */
   87 struct t_filters glob_filter = { 0, 0, (struct t_filter *) 0 };
   88 
   89 
   90 /*
   91  * Global filter file offset
   92  */
   93 int filter_file_offset;
   94 
   95 
   96 /*
   97  * Local prototypes
   98  */
   99 static int get_choice(int x, const char *help, const char *prompt, char *list[], int list_size);
  100 static int set_filter_scope(struct t_group *group);
  101 static struct t_filter_comment *add_filter_comment(struct t_filter_comment *ptr, char *text);
  102 static struct t_filter_comment *free_filter_comment(struct t_filter_comment *ptr);
  103 static struct t_filter_comment *copy_filter_comment(struct t_filter_comment *from, struct t_filter_comment *to);
  104 static t_bool add_filter_rule(struct t_group *group, struct t_article *art, struct t_filter_rule *rule, t_bool quick_filter_rule);
  105 static int test_match(const char *string, char *regex, t_bool nocase, t_bool use_regex, struct regex_cache *cache);
  106 static void expand_filter_array(struct t_filters *ptr);
  107 static void fmt_filter_menu_prompt(char *dest, size_t dest_len, const char *fmt_str, int len, const char *text);
  108 static void free_filter_item(struct t_filter *ptr);
  109 static void print_filter_menu(void);
  110 static void set_filter(struct t_filter *ptr);
  111 static void write_filter_array(FILE *fp, struct t_filters *ptr);
  112 #if 0 /* currently unused */
  113     static FILE *open_xhdr_fp(char *header, long min, long max);
  114 #endif /* 0 */
  115 
  116 
  117 /*
  118  * Add one more entry to the filter-comment-list.
  119  * If ptr == NULL the list will be created.
  120  */
  121 static struct t_filter_comment *
  122 add_filter_comment(
  123     struct t_filter_comment *ptr,
  124     char *text)
  125 {
  126     if (ptr == NULL) {
  127         ptr = my_malloc(sizeof(struct t_filter_comment));
  128         ptr->text = my_strdup(text);
  129         ptr->next = (struct t_filter_comment *) 0;
  130     } else
  131         ptr->next = add_filter_comment(ptr->next, text);
  132 
  133     return ptr;
  134 }
  135 
  136 
  137 /*
  138  * Free all entries in a filter-comment-list.
  139  * Set ptr to NULL and return it.
  140  */
  141 static struct t_filter_comment *
  142 free_filter_comment(
  143     struct t_filter_comment *ptr)
  144 {
  145     struct t_filter_comment *tmp, *next;
  146 
  147     tmp = ptr;
  148     while (tmp != NULL) {
  149         next = tmp->next;
  150         free(tmp->text);
  151         free(tmp);
  152         tmp = next;
  153     }
  154 
  155     return tmp;
  156 }
  157 
  158 
  159 /*
  160  * Copy the filter-comment-list 'from' into the list 'to'.
  161  */
  162 static struct t_filter_comment *
  163 copy_filter_comment(
  164     struct t_filter_comment *from,
  165     struct t_filter_comment *to)
  166 {
  167     if (from != NULL) {
  168         to = my_malloc(sizeof(struct t_filter_comment));
  169         to->text = my_strdup(from->text);
  170         to->next = copy_filter_comment(from->next, NULL);
  171     }
  172 
  173     return to;
  174 }
  175 
  176 
  177 static void
  178 expand_filter_array(
  179     struct t_filters *ptr)
  180 {
  181     int num;
  182     size_t block;
  183 
  184     num = ++ptr->max;
  185 
  186     block = (size_t) num * sizeof(struct t_filter);
  187 
  188     if (num == 1)   /* allocate */
  189         ptr->filter = my_malloc(block);
  190     else    /* reallocate */
  191         ptr->filter = my_realloc(ptr->filter, block);
  192 }
  193 
  194 
  195 /*
  196  * Looks for a matching filter hit (wildmat or pcre regex) in the supplied string
  197  * If the cache is not yet initialised, compile and optimise the regex
  198  * Returns 1 if we hit the rule
  199  * Returns 0 if we had no match
  200  * In case of error prints an error message and returns -1
  201  */
  202 static int
  203 test_match(
  204     const char *string,
  205     char *regex,
  206     t_bool nocase,
  207     t_bool use_regex,
  208     struct regex_cache *cache)
  209 {
  210     int error;
  211 
  212     if (!use_regex) {
  213         if (wildmat(string, regex, nocase))
  214             return 1;
  215     } else {
  216         if (!cache->re)
  217             compile_regex(regex, cache, (nocase ? REGEX_CASELESS : 0));
  218         if (cache->re) {
  219             error = match_regex_ex(string, (int) strlen(string), 0, 0, cache);
  220             if (error >= 0)
  221                 return 1;
  222             else if (error != REGEX_ERROR_NOMATCH) { /* also exclude BADUTF8 ? */
  223                 error_message(2, _(txt_pcre_error_num), error);
  224 #ifdef DEBUG
  225                 if (debug & DEBUG_FILTER) {
  226                     debug_print_file("FILTER", _(txt_pcre_error_num), error);
  227                     debug_print_file("FILTER", "\t regex: %s", regex);
  228                     debug_print_file("FILTER", "\tstring: %s", string);
  229                 }
  230 #endif /* DEBUG */
  231                 return -1;
  232             }
  233         }
  234     }
  235     return 0;
  236 }
  237 
  238 
  239 /*
  240  * set_filter() initialises a struct t_filter with default values
  241  */
  242 static void
  243 set_filter(
  244     struct t_filter *ptr)
  245 {
  246     if (ptr != NULL) {
  247         ptr->comment = (struct t_filter_comment *) 0;
  248         ptr->scope = NULL;
  249         ptr->inscope = TRUE;
  250         ptr->icase = FALSE;
  251         ptr->fullref = FILTER_MSGID;
  252         ptr->subj = NULL;
  253         ptr->from = NULL;
  254         ptr->msgid = NULL;
  255         ptr->lines_cmp = FILTER_LINES_NO;
  256         ptr->lines_num = 0;
  257         ptr->gnksa_cmp = FILTER_LINES_NO;
  258         ptr->gnksa_num = 0;
  259         ptr->score = 0;
  260         ptr->xref = NULL;
  261         ptr->path = NULL;
  262         ptr->time = (time_t) 0;
  263         ptr->next = (struct t_filter *) 0;
  264     }
  265 }
  266 
  267 
  268 /*
  269  * free_filter_item() frees all filter data (char *)
  270  */
  271 static void
  272 free_filter_item(
  273     struct t_filter *ptr)
  274 {
  275     ptr->comment = free_filter_comment(ptr->comment);
  276     FreeAndNull(ptr->scope);
  277     FreeAndNull(ptr->subj);
  278     FreeAndNull(ptr->from);
  279     FreeAndNull(ptr->msgid);
  280     FreeAndNull(ptr->xref);
  281     FreeAndNull(ptr->path);
  282 }
  283 
  284 
  285 /*
  286  * free_filter_array() frees t_filter structs t_filters contains pointers to
  287  */
  288 void
  289 free_filter_array(
  290     struct t_filters *ptr)
  291 {
  292     int i;
  293 
  294     if (ptr != NULL) {
  295         for (i = 0; i < ptr->num; i++)
  296             free_filter_item(ptr->filter + i);
  297 
  298         FreeAndNull(ptr->filter);
  299         ptr->num = 0;
  300         ptr->max = 0;
  301     }
  302 }
  303 
  304 
  305 /*
  306  * read ~/.tin/filter file contents into filter array
  307  */
  308 t_bool
  309 read_filter_file(
  310     const char *file)
  311 {
  312     FILE *fp;
  313     char buf[HEADER_LEN];
  314     char scope[HEADER_LEN];
  315     char comment_line[LEN]; /* one line of comment */
  316     char subj[HEADER_LEN];
  317     char from[HEADER_LEN];
  318     char msgid[HEADER_LEN];
  319     char buffer[HEADER_LEN];
  320     char gnksa[HEADER_LEN];
  321     char xref[HEADER_LEN];
  322     char path[HEADER_LEN];
  323     char scbuf[PATH_LEN];
  324     int i = 0;
  325     int icase = 0;
  326     int score = 0;
  327     long secs = 0L;
  328     struct t_filter_comment *comment = NULL;
  329     struct t_filter *ptr = NULL;
  330     t_bool need_write = FALSE;
  331     t_bool no_version_line = TRUE;
  332     t_bool expired_time = FALSE;
  333     time_t current_secs = (time_t) 0;
  334     static t_bool first_read = TRUE;
  335     struct t_version *upgrade = NULL;
  336 
  337     if ((fp = fopen(file, "r")) == NULL)
  338         return FALSE;
  339 
  340     if (!batch_mode || verbose)
  341         wait_message(0, _(txt_reading_filter_file));
  342 
  343     (void) time(&current_secs);
  344 
  345     /*
  346      * Reset all filter arrays if doing a reread of the active file
  347      */
  348     if (!first_read)
  349         free_filter_array(&glob_filter);
  350 
  351     filter_file_offset = 1;
  352     scope[0] = '\0';
  353     while (fgets(buf, (int) sizeof(buf), fp) != NULL) {
  354         if (*buf == '\n')
  355             continue;
  356         if (*buf == '#') {
  357             if (scope[0] == '\0')
  358                 filter_file_offset++;
  359             if (upgrade == NULL && first_read && match_string(buf, "# Filter file V", NULL, 0)) {
  360                 first_read = FALSE;
  361                 no_version_line = FALSE;
  362                 upgrade = check_upgrade(buf, "# Filter file V", FILTER_VERSION);
  363                 if (upgrade->state != RC_IGNORE)
  364                     upgrade_prompt_quit(upgrade, file); /* FILTER_FILE */ /* TODO: do something (more) useful here */
  365             }
  366             continue;
  367         }
  368 
  369         switch (my_tolower((unsigned char) buf[0])) {
  370             case 'c':
  371                 if (match_integer(buf + 1, "ase=", &icase, 1)) {
  372                     if (ptr && !expired_time)
  373                         ptr[i].icase = (unsigned) icase;
  374 
  375                     break;
  376                 }
  377                 if (match_string(buf + 1, "omment=", comment_line, sizeof(comment_line))) {
  378                     str_trim(comment_line);
  379                     comment = add_filter_comment(comment, comment_line);
  380                 }
  381                 break;
  382 
  383             case 'f':
  384                 if (match_string(buf + 1, "rom=", from, sizeof(from))) {
  385                     if (ptr && !expired_time) {
  386                         if (tinrc.wildcard && ptr[i].from != NULL) {
  387                             /* merge with already read value */
  388                             ptr[i].from = my_realloc(ptr[i].from, strlen(ptr[i].from) + strlen(from) + 2);
  389                             strcat(ptr[i].from, "|");
  390                             strcat(ptr[i].from, from);
  391                         } else {
  392                             FreeIfNeeded(ptr[i].from);
  393                             ptr[i].from = my_strdup(from);
  394                         }
  395                     }
  396                 }
  397                 break;
  398 
  399             case 'g':
  400                 if (match_string(buf + 1, "roup=", scope, sizeof(scope))) {
  401                     str_trim(scope);
  402 #ifdef DEBUG
  403                     if (debug & DEBUG_FILTER)
  404                         debug_print_file("FILTER", "\nnum=[%d] group=[%s]", glob_filter.num, scope);
  405 #endif /* DEBUG */
  406                     if (glob_filter.num >= glob_filter.max)
  407                         expand_filter_array(&glob_filter);
  408 
  409                     ptr = glob_filter.filter;
  410                     i = glob_filter.num++;
  411                     set_filter(&ptr[i]);
  412                     expired_time = FALSE;
  413                     ptr[i].scope = my_strdup(scope);
  414                     if (comment != NULL) {
  415                         ptr[i].comment = copy_filter_comment(comment, ptr[i].comment);
  416                         comment = free_filter_comment(comment);
  417                     }
  418                     subj[0] = '\0';
  419                     from[0] = '\0';
  420                     msgid[0] = '\0';
  421                     buffer[0] = '\0';
  422                     xref[0] = '\0';
  423                     path[0] = '\0';
  424                     icase = 0;
  425                     secs = 0L;
  426                     break;
  427                 }
  428                 if (match_string(buf + 1, "nksa=", gnksa, sizeof(gnksa))) {
  429                     if (ptr && !expired_time) {
  430                         if (gnksa[0] == '<') {
  431                             ptr[i].gnksa_cmp = FILTER_LINES_LT;
  432                             ptr[i].gnksa_num = atoi(&gnksa[1]);
  433                         } else if (gnksa[0] == '>') {
  434                             ptr[i].gnksa_cmp = FILTER_LINES_GT;
  435                             ptr[i].gnksa_num = atoi(&gnksa[1]);
  436                         } else {
  437                             ptr[i].gnksa_cmp = FILTER_LINES_EQ;
  438                             ptr[i].gnksa_num = atoi(gnksa);
  439                         }
  440                     }
  441                 }
  442                 break;
  443 
  444             case 'l':
  445                 if (match_string(buf + 1, "ines=", buffer, sizeof(buffer))) {
  446                     if (ptr && !expired_time) {
  447                         if (buffer[0] == '<') {
  448                             ptr[i].lines_cmp = FILTER_LINES_LT;
  449                             ptr[i].lines_num = atoi(&buffer[1]);
  450                         } else if (buffer[0] == '>') {
  451                             ptr[i].lines_cmp = FILTER_LINES_GT;
  452                             ptr[i].lines_num = atoi(&buffer[1]);
  453                         } else {
  454                             ptr[i].lines_cmp = FILTER_LINES_EQ;
  455                             ptr[i].lines_num = atoi(buffer);
  456                         }
  457                     }
  458                 }
  459                 break;
  460 
  461             case 'm':
  462                 if (match_string(buf + 1, "sgid=", msgid, sizeof(msgid))) {
  463                     if (ptr && !expired_time) {
  464                         if (tinrc.wildcard && ptr[i].msgid != NULL && ptr[i].fullref == FILTER_MSGID) {
  465                             /* merge with already read value */
  466                             ptr[i].msgid = my_realloc(ptr[i].msgid, strlen(ptr[i].msgid) + strlen(msgid) + 2);
  467                             strcat(ptr[i].msgid, "|");
  468                             strcat(ptr[i].msgid, msgid);
  469                         } else {
  470                             FreeIfNeeded(ptr[i].msgid);
  471                             ptr[i].msgid = my_strdup(msgid);
  472                             ptr[i].fullref = FILTER_MSGID;
  473                         }
  474                     }
  475                     break;
  476                 }
  477                 if (match_string(buf + 1, "sgid_last=", msgid, sizeof(msgid))) {
  478                     if (ptr && !expired_time) {
  479                         if (tinrc.wildcard && ptr[i].msgid != NULL && ptr[i].fullref == FILTER_MSGID_LAST) {
  480                             /* merge with already read value */
  481                             ptr[i].msgid = my_realloc(ptr[i].msgid, strlen(ptr[i].msgid) + strlen(msgid) + 2);
  482                             strcat(ptr[i].msgid, "|");
  483                             strcat(ptr[i].msgid, msgid);
  484                         } else {
  485                             FreeIfNeeded(ptr[i].msgid);
  486                             ptr[i].msgid = my_strdup(msgid);
  487                             ptr[i].fullref = FILTER_MSGID_LAST;
  488                         }
  489                     }
  490                     break;
  491                 }
  492                 if (match_string(buf + 1, "sgid_only=", msgid, sizeof(msgid))) {
  493                     if (ptr && !expired_time) {
  494                         if (tinrc.wildcard && ptr[i].msgid != NULL && ptr[i].fullref == FILTER_MSGID_ONLY) {
  495                             /* merge with already read value */
  496                             ptr[i].msgid = my_realloc(ptr[i].msgid, strlen(ptr[i].msgid) + strlen(msgid) + 2);
  497                             strcat(ptr[i].msgid, "|");
  498                             strcat(ptr[i].msgid, msgid);
  499                         } else {
  500                             FreeIfNeeded(ptr[i].msgid);
  501                             ptr[i].msgid = my_strdup(msgid);
  502                             ptr[i].fullref = FILTER_MSGID_ONLY;
  503                         }
  504                     }
  505                 }
  506                 break;
  507 
  508             case 'p':
  509                 if (match_string(buf + 1, "ath=", path, sizeof(path))) {
  510                     str_trim(path);
  511                     if (ptr && !expired_time) {
  512                         if (tinrc.wildcard && ptr[i].path != NULL) {
  513                             /* merge with already read value */
  514                             ptr[i].path = my_realloc(ptr[i].path, strlen(ptr[i].path) + strlen(path) + 2);
  515                             strcat(ptr[i].path, "|");
  516                             strcat(ptr[i].path, path);
  517                         } else {
  518                             FreeIfNeeded(ptr[i].path);
  519                             ptr[i].path = my_strdup(path);
  520                         }
  521                     }
  522                 }
  523                 break;
  524 
  525             case 'r':
  526                 if (match_string(buf + 1, "efs_only=", msgid, sizeof(msgid))) {
  527                     if (ptr && !expired_time) {
  528                         if (tinrc.wildcard && ptr[i].msgid != NULL && ptr[i].fullref == FILTER_REFS_ONLY) {
  529                             /* merge with already read value */
  530                             ptr[i].msgid = my_realloc(ptr[i].msgid, strlen(ptr[i].msgid) + strlen(msgid) + 2);
  531                             strcat(ptr[i].msgid, "|");
  532                             strcat(ptr[i].msgid, msgid);
  533                         } else {
  534                             FreeIfNeeded(ptr[i].msgid);
  535                             ptr[i].msgid = my_strdup(msgid);
  536                             ptr[i].fullref = FILTER_REFS_ONLY;
  537                         }
  538                     }
  539                 }
  540                 break;
  541 
  542             case 's':
  543                 if (match_string(buf + 1, "ubj=", subj, sizeof(subj))) {
  544                     if (ptr && !expired_time) {
  545                         if (tinrc.wildcard && ptr[i].subj != NULL) {
  546                             /* merge with already read value */
  547                             ptr[i].subj = my_realloc(ptr[i].subj, strlen(ptr[i].subj) + strlen(subj) + 2);
  548                             strcat(ptr[i].subj, "|");
  549                             strcat(ptr[i].subj, subj);
  550                         } else {
  551                             FreeIfNeeded(ptr[i].subj);
  552                             ptr[i].subj = my_strdup(subj);
  553                         }
  554 #ifdef DEBUG
  555                         if (debug & DEBUG_FILTER)
  556                             debug_print_file("FILTER", "buf=[%s]  Gsubj=[%s]", ptr[i].subj, glob_filter.filter[i].subj);
  557 #endif /* DEBUG */
  558                     }
  559                     break;
  560                 }
  561 
  562                 /*
  563                  * read score for rule
  564                  */
  565                 if (match_string(buf + 1, "core=", scbuf, PATH_LEN)) {
  566                     score = atoi(scbuf);
  567 #ifdef DEBUG
  568                     if (debug & DEBUG_FILTER)
  569                         debug_print_file("FILTER", "score=[%d]", score);
  570 #endif /* DEBUG */
  571                     if (ptr && !expired_time) {
  572                         if (score > SCORE_MAX)
  573                             score = SCORE_MAX;
  574                         else {
  575                             if (score < -SCORE_MAX)
  576                                 score = -SCORE_MAX;
  577                             else {
  578                                 if (!score) {
  579                                     if (!strncasecmp(scbuf, "kill", 4))
  580                                         score = tinrc.score_kill;
  581                                     else {
  582                                         if (!strncasecmp(scbuf, "hot", 3))
  583                                             score = tinrc.score_select;
  584                                     }
  585                                 }
  586                             }
  587                         }
  588                         ptr[i].score = score;
  589                     }
  590                 }
  591                 break;
  592 
  593             case 't':
  594                 if (match_long(buf + 1, "ime=", &secs)) {
  595                     if (ptr && !expired_time) {
  596                         ptr[i].time = (time_t) secs;
  597                         /* rule expired? */
  598                         if (secs && current_secs > (time_t) secs) {
  599 #ifdef DEBUG
  600                             if (debug & DEBUG_FILTER)
  601                                 debug_print_file("FILTER", "EXPIRED  secs=[%lu]  current_secs=[%lu]", (unsigned long int) secs, (unsigned long int) current_secs);
  602 #endif /* DEBUG */
  603                             glob_filter.num--;
  604                             expired_time = TRUE;
  605                             need_write = TRUE;
  606                         }
  607                     }
  608                 }
  609                 break;
  610 
  611             case 'x':
  612                 /*
  613                  * TODO: format has changed in FILTER_VERSION 1.0.0,
  614                  *       should we comment out older xref rules like below?
  615                  */
  616                 if (ptr && match_string(buf + 1, "ref=", xref, sizeof(xref))) {
  617                     str_trim(xref);
  618                     if (!expired_time) {
  619                         if (tinrc.wildcard && ptr[i].xref != NULL) {
  620                             /* merge with already read value */
  621                             ptr[i].xref = my_realloc(ptr[i].xref, strlen(ptr[i].xref) + strlen(xref) + 2);
  622                             strcat(ptr[i].xref, "|");
  623                             strcat(ptr[i].xref, xref);
  624                         } else {
  625                             FreeIfNeeded(ptr[i].xref);
  626                             ptr[i].xref = my_strdup(xref);
  627                         }
  628                     }
  629                     break;
  630                 }
  631                 if (ptr && ((upgrade && upgrade->state == RC_UPGRADE) || no_version_line)) {
  632                     char foo[HEADER_LEN];
  633 
  634                     if (match_string(buf + 1, "ref_max=", foo, LEN - 1)) {
  635                         /* TODO: give better explanation. */
  636                         snprintf(foo, HEADER_LEN, "%s%s", _(txt_removed_rule), str_trim(buf));
  637                         ptr[i].comment = add_filter_comment(ptr[i].comment, foo);
  638                         need_write = TRUE;
  639                         break;
  640                     }
  641                     if (match_string(buf + 1, "ref_score=", foo, LEN - 1)) {
  642                         /* TODO: give better explanation. */
  643                         snprintf(foo, HEADER_LEN, "%s%s", _(txt_removed_rule), str_trim(buf));
  644                         ptr[i].comment = add_filter_comment(ptr[i].comment, foo);
  645                         need_write = TRUE;
  646                     }
  647                 }
  648                 break;
  649 
  650             default:
  651                 break;
  652         }
  653     }
  654 
  655     if (comment) /* stray comment without scope */
  656         (void) free_filter_comment(comment);
  657 
  658     fclose(fp);
  659 
  660     if (!upgrade && need_write) {
  661         upgrade = my_malloc(sizeof(struct t_version));
  662         upgrade->state = RC_UPGRADE;
  663         upgrade->file_version = -1;
  664         upgrade_prompt_quit(upgrade, file); /* TODO: do something (more) useful here */
  665     }
  666 
  667     if (need_write || (upgrade && upgrade->state == RC_UPGRADE))
  668         write_filter_file(file);
  669 
  670     if (!cmd_line && !batch_mode)
  671         clear_message();
  672 
  673     FreeAndNull(upgrade);
  674     return TRUE;
  675 }
  676 
  677 
  678 /*
  679  * write filter strings to ~/.tin/filter
  680  */
  681 void
  682 write_filter_file(
  683     const char *filename)
  684 {
  685     FILE *fp;
  686     char *file_tmp;
  687     int i;
  688     long fpos;
  689 
  690     if (no_write)
  691         return;
  692 
  693     /* generate tmp-filename */
  694     file_tmp = get_tmpfilename(filename);
  695 
  696     if (!backup_file(filename, file_tmp)) {
  697         error_message(2, _(txt_filesystem_full_backup), filename);
  698         free(file_tmp);
  699         return;
  700     }
  701 
  702     if ((fp = fopen(filename, "w+")) == NULL) {
  703         free(file_tmp);
  704         return;
  705     }
  706 
  707     /* TODO: -> lang.c */
  708     fprintf(fp, "# Filter file V%s for the TIN newsreader\n#\n", FILTER_VERSION);
  709     fprintf(fp, "%s", _(txt_filter_file));
  710 
  711     fflush(fp);
  712 
  713     /* determine the file offset */
  714     if (!batch_mode) {
  715         if ((fpos = ftell(fp)) <= 0) {
  716             clearerr(fp);
  717             fclose(fp);
  718             rename_file(file_tmp, filename);
  719             free(file_tmp);
  720             error_message(2, _(txt_filesystem_full), filename);
  721             return;
  722         }
  723         rewind(fp);
  724         filter_file_offset = 1;
  725         while ((i = fgetc(fp)) != EOF) {
  726             if (i == '\n')
  727                 filter_file_offset++;
  728         }
  729         if (fseek(fp, fpos, SEEK_SET)) {
  730             clearerr(fp);
  731             fclose(fp);
  732             rename_file(file_tmp, filename);
  733             free(file_tmp);
  734             error_message(2, _(txt_filesystem_full), filename);
  735             return;
  736         }
  737     }
  738 
  739     /*
  740      * Save global filters
  741      */
  742     write_filter_array(fp, &glob_filter);
  743 
  744     if ((i = ferror(fp)) || fclose(fp)) {
  745         error_message(2, _(txt_filesystem_full), filename);
  746         rename_file(file_tmp, filename);
  747         if (i) {
  748             clearerr(fp);
  749             fclose(fp);
  750         }
  751     } else
  752         unlink(file_tmp);
  753 
  754     free(file_tmp);
  755 }
  756 
  757 
  758 static void
  759 write_filter_array(
  760     FILE *fp,
  761     struct t_filters *ptr)
  762 {
  763     int i;
  764     struct t_filter_comment *comment;
  765     time_t theTime = time(NULL);
  766 
  767     if (ptr == NULL)
  768         return;
  769 
  770     for (i = 0; i < ptr->num; i++) {
  771 #ifdef DEBUG
  772         if (debug & DEBUG_FILTER)
  773             debug_print_file("FILTER", "WRITE i=[%d] subj=[%s] from=[%s]\n", i, BlankIfNull(ptr->filter[i].subj), BlankIfNull(ptr->filter[i].from));
  774 #endif /* DEBUG */
  775 
  776         if (ptr->filter[i].time && theTime > ptr->filter[i].time)
  777             continue;
  778 #ifdef DEBUG
  779         if (debug & DEBUG_FILTER)
  780             debug_print_file("FILTER", "Scope=[%s]" cCRLF, (ptr->filter[i].scope != NULL ? ptr->filter[i].scope : "*"));
  781 #endif /* DEBUG */
  782 
  783         fprintf(fp, "\n");      /* makes filter file more readable */
  784 
  785         /* comments appear always first, if there are any... */
  786         if (ptr->filter[i].comment != NULL) {
  787             /*
  788              * Save the start of the list, in case write_filter_array is
  789              * called multiple times. Otherwise the list would get lost.
  790              */
  791             comment = ptr->filter[i].comment;
  792             while (ptr->filter[i].comment != NULL) {
  793                 fprintf(fp, "comment=%s\n", ptr->filter[i].comment->text);
  794                 ptr->filter[i].comment = ptr->filter[i].comment->next;
  795             }
  796             ptr->filter[i].comment = comment;
  797         }
  798 
  799         fprintf(fp, "group=%s\n", (ptr->filter[i].scope != NULL ? ptr->filter[i].scope : "*"));
  800 
  801         fprintf(fp, "case=%u\n", ptr->filter[i].icase);
  802 
  803         if (ptr->filter[i].score == tinrc.score_kill)
  804             fprintf(fp, "score=kill\n");
  805         else if (ptr->filter[i].score == tinrc.score_select)
  806             fprintf(fp, "score=hot\n");
  807         else
  808             fprintf(fp, "score=%d\n", ptr->filter[i].score);
  809 
  810         if (ptr->filter[i].subj != NULL)
  811             fprintf(fp, "subj=%s\n", ptr->filter[i].subj);
  812 
  813         if (ptr->filter[i].from != NULL)
  814             fprintf(fp, "from=%s\n", ptr->filter[i].from);
  815 
  816         if (ptr->filter[i].msgid != NULL) {
  817             switch (ptr->filter[i].fullref) {
  818                 case FILTER_MSGID:
  819                     fprintf(fp, "msgid=%s\n", ptr->filter[i].msgid);
  820                     break;
  821 
  822                 case FILTER_MSGID_LAST:
  823                     fprintf(fp, "msgid_last=%s\n", ptr->filter[i].msgid);
  824                     break;
  825 
  826                 case FILTER_MSGID_ONLY:
  827                     fprintf(fp, "msgid_only=%s\n", ptr->filter[i].msgid);
  828                     break;
  829 
  830                 case FILTER_REFS_ONLY:
  831                     fprintf(fp, "refs_only=%s\n", ptr->filter[i].msgid);
  832                     break;
  833 
  834                 default:
  835                     break;
  836             }
  837         }
  838 
  839         if (ptr->filter[i].lines_cmp != FILTER_LINES_NO) {
  840             switch (ptr->filter[i].lines_cmp) {
  841                 case FILTER_LINES_EQ:
  842                     fprintf(fp, "lines=%d\n", ptr->filter[i].lines_num);
  843                     break;
  844 
  845                 case FILTER_LINES_LT:
  846                     fprintf(fp, "lines=<%d\n", ptr->filter[i].lines_num);
  847                     break;
  848 
  849                 case FILTER_LINES_GT:
  850                     fprintf(fp, "lines=>%d\n", ptr->filter[i].lines_num);
  851                     break;
  852 
  853                 default:
  854                     break;
  855             }
  856         }
  857 
  858         if (ptr->filter[i].gnksa_cmp != FILTER_LINES_NO) {
  859             switch (ptr->filter[i].gnksa_cmp) {
  860                 case FILTER_LINES_EQ:
  861                     fprintf(fp, "gnksa=%d\n", ptr->filter[i].gnksa_num);
  862                     break;
  863 
  864                 case FILTER_LINES_LT:
  865                     fprintf(fp, "gnksa=<%d\n", ptr->filter[i].gnksa_num);
  866                     break;
  867 
  868                 case FILTER_LINES_GT:
  869                     fprintf(fp, "gnksa=>%d\n", ptr->filter[i].gnksa_num);
  870                     break;
  871 
  872                 default:
  873                     break;
  874             }
  875         }
  876 
  877         if (ptr->filter[i].xref != NULL)
  878             fprintf(fp, "xref=%s\n", ptr->filter[i].xref);
  879 
  880         if (ptr->filter[i].path != NULL)
  881             fprintf(fp, "path=%s\n", ptr->filter[i].path);
  882 
  883         if (ptr->filter[i].time) {
  884             char timestring[25];
  885             if (my_strftime(timestring, sizeof(timestring) - 1, "%Y-%m-%d %H:%M:%S UTC", gmtime(&(ptr->filter[i].time))))
  886                 fprintf(fp, "time=%lu (%s)\n", (unsigned long int) ptr->filter[i].time, timestring);
  887         }
  888     }
  889     fflush(fp);
  890 }
  891 
  892 
  893 /*
  894  * Interactive filter menu
  895  */
  896 static int
  897 get_choice(
  898     int x,
  899     const char *help,
  900     const char *prompt,
  901     char *list[],
  902     int list_size)
  903 {
  904     int ch, y, i = 0;
  905 
  906     if (help)
  907         show_menu_help(help);
  908 
  909     if (list == NULL || list_size < 1)
  910         return -1;
  911 
  912     y = strwidth(prompt);
  913 
  914     do {
  915         MoveCursor(x, y);
  916         my_fputs(list[i], stdout);
  917         my_flush();
  918         CleartoEOLN();
  919         ch = ReadCh();
  920         switch (ch) {
  921             case ' ':
  922                 i++;
  923                 i %= list_size;
  924                 break;
  925 
  926             case ESC:   /* (ESC) common arrow keys */
  927 #ifdef HAVE_KEY_PREFIX
  928             case KEY_PREFIX:
  929 #endif /* HAVE_KEY_PREFIX */
  930                 switch (get_arrow_key(ch)) {
  931                     case KEYMAP_UP:
  932                         i--;
  933                         if (i < 0)
  934                             i = list_size - 1;
  935                         ch = ' ';   /* don't exit the while loop yet */
  936                         break;
  937 
  938                     case KEYMAP_DOWN:
  939                         i++;
  940                         i %= list_size;
  941                         ch = ' ';   /* don't exit the while loop yet */
  942                         break;
  943 
  944                     default:
  945                         break;
  946                 }
  947                 break;
  948 
  949             default:
  950                 break;
  951         }
  952     } while (ch != '\n' && ch != '\r' && ch != iKeyAbort); /* TODO: replace hard coded keynames */
  953 
  954     if (ch == iKeyAbort)
  955         return -1;
  956 
  957     return i;
  958 }
  959 
  960 
  961 static const char *ptr_filter_comment;
  962 static const char *ptr_filter_lines;
  963 static const char *ptr_filter_menu;
  964 static const char *ptr_filter_scope;
  965 static const char *ptr_filter_text;
  966 static const char *ptr_filter_time;
  967 static const char *ptr_filter_groupname;
  968 static char text_subj[PATH_LEN];
  969 static char text_from[PATH_LEN];
  970 static char text_msgid[PATH_LEN];
  971 static char text_score[PATH_LEN];
  972 
  973 
  974 static void
  975 print_filter_menu(
  976     void)
  977 {
  978     ClearScreen();
  979 
  980     center_line(0, TRUE, ptr_filter_menu);
  981 
  982     MoveCursor(INDEX_TOP, 0);
  983     my_printf("%s%s%s", ptr_filter_comment, cCRLF, cCRLF);
  984     my_printf("%s%s", ptr_filter_text, cCRLF);
  985     my_printf("%s%s%s", _(txt_filter_text_type), cCRLF, cCRLF);
  986     my_printf("%s%s", text_subj, cCRLF);
  987     my_printf("%s%s", text_from, cCRLF);
  988     my_printf("%s%s%s", text_msgid, cCRLF, cCRLF);
  989     my_printf("%s%s", ptr_filter_lines, cCRLF);
  990     my_printf("%s%s", text_score, cCRLF);
  991     my_printf("%s%s%s", ptr_filter_time, cCRLF, cCRLF);
  992     my_printf("%s%s", ptr_filter_scope, ptr_filter_groupname);
  993     my_flush();
  994 }
  995 
  996 
  997 #if defined(SIGWINCH) || defined(SIGTSTP)
  998 void
  999 refresh_filter_menu(
 1000     void)
 1001 {
 1002     print_filter_menu();
 1003 
 1004     /*
 1005      * TODO:
 1006      * - refresh already entered and accepted information (follow control
 1007      *   flow in filter_menu below)
 1008      * - refresh help line
 1009      * - set cursor into current input field
 1010      * - refresh already entered data or selected item in current input field
 1011      *   (not everywhere possible yet -- must change getline.c for refreshing
 1012      *    string input)
 1013      */
 1014 }
 1015 #endif /* SIGWINCH || SIGTSTP */
 1016 
 1017 
 1018 /*
 1019  * a help function for filter_menu
 1020  * formats a menu option in a multibyte-safe way
 1021  *
 1022  * this function in closely tight to the way how the filter menu is build
 1023  */
 1024 static void
 1025 fmt_filter_menu_prompt(
 1026     char *dest,     /* where to store the resulting string */
 1027     size_t dest_len,    /* size of dest */
 1028     const char *fmt_str,    /* format string */
 1029     int len,        /* maximal len of the include string */
 1030     const char *text)   /* the include string */
 1031 {
 1032     char *buf;
 1033 #if defined(MULTIBYTE_ABLE) && !defined(NO_LOCALE)
 1034     wchar_t *wbuf, *wbuf2;
 1035 
 1036     if ((wbuf = char2wchar_t(text)) != NULL) {
 1037         wbuf2 = wcspart(wbuf, len, TRUE);
 1038         if ((buf = wchar_t2char(wbuf2)) == NULL) {
 1039             /* conversion failed, truncate original string */
 1040             buf = my_malloc(len + 1);
 1041             snprintf(buf, (size_t) (len + 1), "%-*.*s", len, len, text);
 1042         }
 1043 
 1044         free(wbuf);
 1045         free(wbuf2);
 1046     } else
 1047 #endif /* MULTIBYTE_ABLE && !NO_LOCALE */
 1048     {
 1049         buf = my_malloc(len + 1);
 1050         snprintf(buf, (size_t) (len + 1), "%-*.*s", len, len, text);
 1051     }
 1052     snprintf(dest, dest_len, fmt_str, buf);
 1053     free(buf);
 1054 }
 1055 
 1056 
 1057 /*
 1058  * Interactive filter menu so that the user can dynamically enter parameters.
 1059  * Can be configured for kill or auto-selection screens.
 1060  */
 1061 t_bool
 1062 filter_menu(
 1063     t_function type,
 1064     struct t_group *group,
 1065     struct t_article *art)
 1066 {
 1067     const char *ptr_filter_from;
 1068     const char *ptr_filter_msgid;
 1069     const char *ptr_filter_subj;
 1070     const char *ptr_filter_help_scope;
 1071     const char *ptr_filter_quit_edit_save;
 1072     char *ptr;
 1073     char **list;
 1074     char comment_line[LEN];
 1075     char buf[LEN];
 1076     char keyedit[MAXKEYLEN], keyquit[MAXKEYLEN], keysave[MAXKEYLEN];
 1077     char text_time[PATH_LEN];
 1078     char double_time[PATH_LEN];
 1079     char quat_time[PATH_LEN];
 1080     int i, len, clen, flen;
 1081     struct t_filter_rule rule;
 1082     t_bool proceed;
 1083     t_bool ret;
 1084     t_function func, default_func = FILTER_SAVE;
 1085 
 1086     signal_context = cFilter;
 1087 
 1088     rule.comment = (struct t_filter_comment *) 0;
 1089     rule.text[0] = '\0';
 1090     rule.scope[0] = '\0';
 1091     rule.counter = 0;
 1092     rule.lines_cmp = FILTER_LINES_NO;
 1093     rule.lines_num = 0;
 1094     rule.from_ok = FALSE;
 1095     rule.lines_ok = FALSE;
 1096     rule.msgid_ok = FALSE;
 1097     rule.fullref = FILTER_MSGID;
 1098     rule.subj_ok = FALSE;
 1099     rule.icase = FALSE;
 1100     rule.score = 0;
 1101     rule.expire_time = FALSE;
 1102     rule.check_string = FALSE;
 1103 
 1104     comment_line[0] = '\0';
 1105 
 1106     /*
 1107      * setup correct text for user selected menu
 1108      */
 1109     PrintFuncKey(keyedit, FILTER_EDIT, filter_keys);
 1110     PrintFuncKey(keyquit, GLOBAL_QUIT, filter_keys);
 1111     PrintFuncKey(keysave, FILTER_SAVE, filter_keys);
 1112 
 1113     if (type == GLOBAL_MENU_FILTER_KILL) {
 1114         ptr_filter_from = _(txt_kill_from);
 1115         ptr_filter_lines = _(txt_kill_lines);
 1116         ptr_filter_menu = _(txt_kill_menu);
 1117         ptr_filter_msgid = _(txt_kill_msgid);
 1118         ptr_filter_scope = _(txt_kill_scope);
 1119         ptr_filter_subj = _(txt_kill_subj);
 1120         ptr_filter_text = _(txt_kill_text);
 1121         ptr_filter_time = _(txt_kill_time);
 1122         ptr_filter_help_scope = _(txt_help_kill_scope);
 1123         ptr_filter_quit_edit_save = _(txt_quit_edit_save_kill);
 1124     } else {    /* type == GLOBAL_MENU_FILTER_SELECT */
 1125         ptr_filter_from = _(txt_select_from);
 1126         ptr_filter_lines = _(txt_select_lines);
 1127         ptr_filter_menu = _(txt_select_menu);
 1128         ptr_filter_msgid = _(txt_select_msgid);
 1129         ptr_filter_scope = _(txt_select_scope);
 1130         ptr_filter_subj = _(txt_select_subj);
 1131         ptr_filter_text = _(txt_select_text);
 1132         ptr_filter_time = _(txt_select_time);
 1133         ptr_filter_help_scope = _(txt_help_select_scope);
 1134         ptr_filter_quit_edit_save = _(txt_quit_edit_save_select);
 1135     }
 1136 
 1137     ptr_filter_comment = _(txt_filter_comment);
 1138     ptr_filter_groupname = group->name;
 1139 
 1140     clen = strwidth(_(txt_no));
 1141     clen = MAX(clen, strwidth(_(txt_yes)));
 1142     clen = MAX(clen, strwidth(_(txt_full)));
 1143     clen = MAX(clen, strwidth(_(txt_last)));
 1144     clen = MAX(clen, strwidth(_(txt_only)));
 1145 
 1146     flen = strwidth(ptr_filter_subj) - 2;
 1147     flen = MAX(flen, strwidth(ptr_filter_from) - 2);
 1148     flen = MAX(flen, strwidth(ptr_filter_msgid) - 2);
 1149 
 1150     len = cCOLS - flen - clen - 1 + 4;
 1151 
 1152     snprintf(text_time, sizeof(text_time), _(txt_time_default_days), tinrc.filter_days);
 1153     fmt_filter_menu_prompt(text_subj, sizeof(text_subj), ptr_filter_subj, len, art->subject);
 1154     snprintf(text_score, sizeof(text_score), _(txt_filter_score), (type == GLOBAL_MENU_FILTER_KILL ? -tinrc.score_kill : tinrc.score_select));
 1155     fmt_filter_menu_prompt(text_from, sizeof(text_from), ptr_filter_from, len, art->from);
 1156     fmt_filter_menu_prompt(text_msgid, sizeof(text_msgid), ptr_filter_msgid, len - 4, MSGID(art));
 1157 
 1158     print_filter_menu();
 1159 
 1160     /*
 1161      * None, one or multiple lines of comment.
 1162      * Continue until an empty line is entered.
 1163      * The empty line is ignored.
 1164      */
 1165     show_menu_help(_(txt_help_filter_comment));
 1166     while ((proceed = prompt_menu_string(INDEX_TOP, ptr_filter_comment, comment_line)) && comment_line[0] != '\0') {
 1167         rule.comment = add_filter_comment(rule.comment, comment_line);
 1168         comment_line[0] = '\0';
 1169     }
 1170     if (!proceed) {
 1171         rule.comment = free_filter_comment(rule.comment);
 1172         return FALSE;
 1173     }
 1174 
 1175     /*
 1176      * Text which might be used to filter on subj, from or msgid
 1177      */
 1178     show_menu_help(_(txt_help_filter_text));
 1179     if (!prompt_menu_string(INDEX_TOP + 2, ptr_filter_text, rule.text)) {
 1180         rule.comment = free_filter_comment(rule.comment);
 1181         return FALSE;
 1182     }
 1183 
 1184     if (*rule.text) {
 1185         list = my_malloc(sizeof(char *) * 8);
 1186         list[0] = (char *) _(txt_subj_line_only_case);
 1187         list[1] = (char *) _(txt_subj_line_only);
 1188         list[2] = (char *) _(txt_from_line_only_case);
 1189         list[3] = (char *) _(txt_from_line_only);
 1190         list[4] = (char *) _(txt_msgid_refs_line);
 1191         list[5] = (char *) _(txt_msgid_line_last);
 1192         list[6] = (char *) _(txt_msgid_line_only);
 1193         list[7] = (char *) _(txt_refs_line_only);
 1194 
 1195         i = get_choice(INDEX_TOP + 3, _(txt_help_filter_text_type), _(txt_filter_text_type), list, 8);
 1196         free(list);
 1197 
 1198         if (i == -1) {
 1199             rule.comment = free_filter_comment(rule.comment);
 1200             return FALSE;
 1201         }
 1202 
 1203         rule.counter = i;
 1204         switch (i) {
 1205             case FILTER_SUBJ_CASE_IGNORE:
 1206             case FILTER_FROM_CASE_IGNORE:
 1207                 rule.icase = TRUE;
 1208                 break;
 1209 
 1210             case FILTER_SUBJ_CASE_SENSITIVE:
 1211             case FILTER_FROM_CASE_SENSITIVE:
 1212             case FILTER_MSGID:
 1213             case FILTER_MSGID_LAST:
 1214             case FILTER_MSGID_ONLY:
 1215             case FILTER_REFS_ONLY:
 1216                 break;
 1217 
 1218             default: /* should not happen */
 1219                 /* CONSTANTCONDITION */
 1220                 assert(0 != 0);
 1221                 break;
 1222         }
 1223     }
 1224 
 1225     if (!*rule.text) {
 1226         rule.check_string = TRUE;
 1227         /*
 1228          * Subject:
 1229          */
 1230         list = my_malloc(sizeof(char *) * 2);
 1231         list[0] = (char *) _(txt_yes);
 1232         list[1] = (char *) _(txt_no);
 1233         i = get_choice(INDEX_TOP + 5, _(txt_help_filter_subj), text_subj, list, 2);
 1234         free(list);
 1235 
 1236         if (i == -1) {
 1237             rule.comment = free_filter_comment(rule.comment);
 1238             return FALSE;
 1239         } else
 1240             rule.subj_ok = (i == 0);
 1241 
 1242         /*
 1243          * From:
 1244          */
 1245         list = my_malloc(sizeof(char *) * 2);
 1246         if (rule.subj_ok) {
 1247             list[0] = (char *) _(txt_no);
 1248             list[1] = (char *) _(txt_yes);
 1249         } else {
 1250             list[0] = (char *) _(txt_yes);
 1251             list[1] = (char *) _(txt_no);
 1252         }
 1253         i = get_choice(INDEX_TOP + 6, _(txt_help_filter_from), text_from, list, 2);
 1254         free(list);
 1255 
 1256         if (i == -1) {
 1257             rule.comment = free_filter_comment(rule.comment);
 1258             return FALSE;
 1259         } else
 1260             rule.from_ok = rule.subj_ok ? (i != 0) : (i == 0);
 1261 
 1262         /*
 1263          * Message-ID:
 1264          */
 1265         list = my_malloc(sizeof(char *) * 4);
 1266         if (rule.subj_ok || rule.from_ok) {
 1267             list[0] = (char *) _(txt_no);
 1268             list[1] = (char *) _(txt_full);
 1269             list[2] = (char *) _(txt_last);
 1270             list[3] = (char *) _(txt_only);
 1271         } else {
 1272             list[0] = (char *) _(txt_full);
 1273             list[1] = (char *) _(txt_last);
 1274             list[2] = (char *) _(txt_only);
 1275             list[3] = (char *) _(txt_no);
 1276         }
 1277         i = get_choice(INDEX_TOP + 7, _(txt_help_filter_msgid), text_msgid, list, 4);
 1278         free(list);
 1279 
 1280         if (i == -1) {
 1281             rule.comment = free_filter_comment(rule.comment);
 1282             return FALSE;
 1283         } else {
 1284             switch ((rule.subj_ok || rule.from_ok) ? i : i + 1) {
 1285                 case 0:
 1286                 case 4:
 1287                     rule.msgid_ok = FALSE;
 1288                     rule.fullref = FILTER_MSGID;
 1289                     break;
 1290 
 1291                 case 1:
 1292                     rule.msgid_ok = TRUE;
 1293                     rule.fullref = FILTER_MSGID;
 1294                     break;
 1295 
 1296                 case 2:
 1297                     rule.msgid_ok = TRUE;
 1298                     rule.fullref = FILTER_MSGID_LAST;
 1299                     break;
 1300 
 1301                 case 3:
 1302                     rule.msgid_ok = TRUE;
 1303                     rule.fullref = FILTER_MSGID_ONLY;
 1304                     break;
 1305 
 1306                 default: /* should not happen */
 1307                     /* CONSTANTCONDITION */
 1308                     assert(0 != 0);
 1309                     break;
 1310             }
 1311         }
 1312 
 1313     }
 1314 
 1315     /*
 1316      * Lines:
 1317      */
 1318     show_menu_help(_(txt_help_filter_lines));
 1319 
 1320     buf[0] = '\0';
 1321 
 1322     if (!prompt_menu_string(INDEX_TOP + 9, ptr_filter_lines, buf)) {
 1323         rule.comment = free_filter_comment(rule.comment);
 1324         return FALSE;
 1325     }
 1326 
 1327     /*
 1328      * Get the < > sign if any for the lines rule
 1329      */
 1330     ptr = buf;
 1331     while (*ptr == ' ')
 1332         ptr++;
 1333 
 1334     if (*ptr == '>') {
 1335         rule.lines_cmp = FILTER_LINES_GT;
 1336         ptr++;
 1337     } else if (*ptr == '<') {
 1338         rule.lines_cmp = FILTER_LINES_LT;
 1339         ptr++;
 1340     } else if (*ptr == '=') {
 1341         rule.lines_cmp = FILTER_LINES_EQ;
 1342         ptr++;
 1343     }
 1344 
 1345     if (*ptr)
 1346         rule.lines_num = abs(atoi(ptr));
 1347 
 1348     if (rule.lines_num && rule.lines_cmp == FILTER_LINES_NO)
 1349         rule.lines_cmp = FILTER_LINES_EQ;
 1350 
 1351     if (rule.lines_cmp != FILTER_LINES_NO && rule.lines_num)
 1352         rule.lines_ok = TRUE;
 1353 
 1354     /*
 1355      * Scoring value
 1356      */
 1357     snprintf(buf, sizeof(buf), _(txt_filter_score_help), SCORE_MAX);
 1358     show_menu_help(buf);
 1359 
 1360     buf[0] = '\0';
 1361     if (!prompt_menu_string(INDEX_TOP + 10, text_score, buf)) {
 1362         rule.comment = free_filter_comment(rule.comment);
 1363         return FALSE;
 1364     }
 1365 
 1366     /* check if a score has been entered */
 1367     if (buf[0] != '\0')
 1368         /* use entered score */
 1369         rule.score = atoi(buf);
 1370     else {
 1371         /* use default score */
 1372         if (type == GLOBAL_MENU_FILTER_KILL)
 1373             rule.score = tinrc.score_kill;
 1374         else /* type == GLOBAL_MENU_FILTER_SELECT */
 1375             rule.score = tinrc.score_select;
 1376     }
 1377 
 1378     if (!rule.score) { /* ignore 0 scores */
 1379         rule.comment = free_filter_comment(rule.comment);
 1380         return FALSE;
 1381     }
 1382 
 1383     /*
 1384      * assure we are in range
 1385      */
 1386     if (rule.score < 0)
 1387         rule.score = abs(rule.score);
 1388     if (rule.score > SCORE_MAX)
 1389         rule.score = SCORE_MAX;
 1390 
 1391     /* get the right sign for the score */
 1392     if (type == GLOBAL_MENU_FILTER_KILL)
 1393         rule.score = -rule.score;
 1394 
 1395     /*
 1396      * Expire time
 1397      */
 1398     snprintf(double_time, sizeof(double_time), "2x %s", text_time);
 1399     snprintf(quat_time, sizeof(quat_time), "4x %s", text_time);
 1400     list = my_malloc(sizeof(char *) * 4);
 1401     list[0] = (char *) _(txt_unlimited_time);
 1402     list[1] = text_time;
 1403     list[2] = double_time;
 1404     list[3] = quat_time;
 1405     i = get_choice(INDEX_TOP + 11, _(txt_help_filter_time), ptr_filter_time, list, 4);
 1406     free(list);
 1407 
 1408     if (i == -1) {
 1409         rule.comment = free_filter_comment(rule.comment);
 1410         return FALSE;
 1411     }
 1412 
 1413     rule.expire_time = i;
 1414 
 1415     /*
 1416      * Scope
 1417      */
 1418     if (*rule.text || rule.subj_ok || rule.from_ok || rule.msgid_ok || rule.lines_ok) {
 1419         int j = 0;
 1420 
 1421         list = my_malloc(sizeof(char *) * 2); /* at least 2 scopes */
 1422         list[j++] = my_strdup(group->name);
 1423         list[j] = my_strdup(list[j - 1]);
 1424         while ((ptr = strrchr(list[j], '.')) != NULL) {
 1425             *(++ptr) = '*';
 1426             *(++ptr) = '\0';
 1427             j++;
 1428             list = my_realloc(list, sizeof(char *) * (size_t) (j + 1)); /* one element more */
 1429             list[j] = my_strdup(list[j - 1]);
 1430             list[j][strlen(list[j]) - 2] = '\0';
 1431         }
 1432         free(list[j]); /* this copy isn't needed anymore */
 1433         list[j] = (char *) _(txt_all_groups);
 1434 
 1435         if ((i = get_choice(INDEX_TOP + 13, ptr_filter_help_scope, ptr_filter_scope, list, j + 1)) > 0)
 1436             my_strncpy(rule.scope, i == j ? "*" : list[i], sizeof(rule.scope) - 1);
 1437 
 1438         for (j--; j >= 0; j--)
 1439             free(list[j]);
 1440         free(list);
 1441 
 1442         if (i == -1) {
 1443             rule.comment = free_filter_comment(rule.comment);
 1444             return FALSE;
 1445         }
 1446     } else {
 1447         rule.comment = free_filter_comment(rule.comment);
 1448         return FALSE;
 1449     }
 1450 
 1451     forever {
 1452         func = prompt_slk_response(default_func, filter_keys,
 1453                 ptr_filter_quit_edit_save, keyquit, keyedit, keysave);
 1454         switch (func) {
 1455 
 1456         case FILTER_EDIT:
 1457             add_filter_rule(group, art, &rule, FALSE); /* save the rule */
 1458             rule.comment = free_filter_comment(rule.comment);
 1459             if (!invoke_editor(filter_file, filter_file_offset, NULL))
 1460                 return FALSE;
 1461             unfilter_articles(group);
 1462             (void) read_filter_file(filter_file);
 1463             return TRUE;
 1464             /* keep lint quiet: */
 1465             /* FALLTHROUGH */
 1466 
 1467         case GLOBAL_QUIT:
 1468         case GLOBAL_ABORT:
 1469             rule.comment = free_filter_comment(rule.comment);
 1470             return FALSE;
 1471             /* keep lint quiet: */
 1472             /* FALLTHROUGH */
 1473 
 1474         case FILTER_SAVE:
 1475             /*
 1476              * Add the filter rule and save it to the filter file
 1477              */
 1478             ret = add_filter_rule(group, art, &rule, FALSE);
 1479             rule.comment = free_filter_comment(rule.comment);
 1480             return ret;
 1481             /* keep lint quiet: */
 1482             /* FALLTHROUGH */
 1483 
 1484         default:
 1485             break;
 1486         }
 1487     }
 1488     /* NOTREACHED */
 1489     return FALSE;
 1490 }
 1491 
 1492 
 1493 /*
 1494  * Quick command to add an auto-select / kill filter to specified groups filter
 1495  */
 1496 t_bool
 1497 quick_filter(
 1498     t_function type,
 1499     struct t_group *group,
 1500     struct t_article *art)
 1501 {
 1502     char *scope;
 1503     char txt[LEN];
 1504     int header, expire, icase;
 1505     struct t_filter_rule rule;
 1506     t_bool ret;
 1507 
 1508     if (type == GLOBAL_QUICK_FILTER_KILL) {
 1509         header = group->attribute->quick_kill_header;
 1510         expire = group->attribute->quick_kill_expire;
 1511         /* ON=case sensitive, OFF=ignore case -> invert */
 1512         icase = bool_not(group->attribute->quick_kill_case);
 1513         scope = group->attribute->quick_kill_scope;
 1514     } else {    /* type == GLOBAL_QUICK_FILTER_SELECT */
 1515         header = group->attribute->quick_select_header;
 1516         expire = group->attribute->quick_select_expire;
 1517         /* ON=case sensitive, OFF=ignore case -> invert */
 1518         icase = bool_not(group->attribute->quick_select_case);
 1519         scope = group->attribute->quick_select_scope;
 1520     }
 1521 
 1522 #ifdef DEBUG
 1523     if (debug & DEBUG_FILTER)
 1524         error_message(2, "%s header=[%d] scope=[%s] expire=[%s] case=[%d]", (type == GLOBAL_QUICK_FILTER_KILL) ? "KILL" : "SELECT", header, BlankIfNull(scope), txt_onoff[expire != FALSE ? 1 : 0], icase);
 1525 #endif /* DEBUG */
 1526 
 1527     /*
 1528      * Setup rules
 1529      */
 1530     if (strlen(BlankIfNull(scope)) > (sizeof(rule.scope) - 1))
 1531         return FALSE;
 1532     my_strncpy(rule.scope, BlankIfNull(scope), sizeof(rule.scope) - 1);
 1533     rule.counter = 0;
 1534     rule.lines_cmp = FILTER_LINES_NO;
 1535     rule.lines_num = 0;
 1536     rule.lines_ok = (header == FILTER_LINES);
 1537     rule.msgid_ok = (header == FILTER_MSGID) || (header == FILTER_MSGID_LAST);
 1538     rule.fullref = header; /* value is directly used to select correct filter type */
 1539     rule.from_ok = (header == FILTER_FROM_CASE_SENSITIVE || header == FILTER_FROM_CASE_IGNORE);
 1540     rule.subj_ok = (header == FILTER_SUBJ_CASE_SENSITIVE || header == FILTER_SUBJ_CASE_IGNORE);
 1541 
 1542     /* create an auto-comment. */
 1543     if (type == GLOBAL_QUICK_FILTER_KILL)
 1544         snprintf(txt, sizeof(txt), "%s%s%c%s%s%s", _(txt_filter_rule_created), "'", ']', "' (", _(txt_help_article_quick_kill), ").");
 1545     else
 1546         snprintf(txt, sizeof(txt), "%s%s%c%s%s%s", _(txt_filter_rule_created), "'", '[', "' (", _(txt_help_article_quick_select), ").");
 1547     rule.comment = add_filter_comment(NULL, txt);
 1548 
 1549     rule.text[0] = '\0';
 1550     rule.icase = icase;
 1551     rule.expire_time = expire;
 1552     rule.check_string = TRUE;
 1553     rule.score = (type == GLOBAL_QUICK_FILTER_KILL) ? tinrc.score_kill : tinrc.score_select;
 1554 
 1555     ret = add_filter_rule(group, art, &rule, TRUE);
 1556     rule.comment = free_filter_comment(rule.comment);
 1557     return ret;
 1558 }
 1559 
 1560 
 1561 /*
 1562  * Quick command to add an auto-select filter to the article that user
 1563  * has just posted. Selects on Subject: line with limited expire time.
 1564  * Don't process if GROUP_TYPE_MAIL || GROUP_TYPE_SAVE
 1565  */
 1566 t_bool
 1567 quick_filter_select_posted_art(
 1568     struct t_group *group,
 1569     const char *subj,
 1570     const char *a_message_id)   /* return value is always ignored */
 1571 {
 1572     t_bool filtered = FALSE;
 1573     char txt[LEN];
 1574 
 1575     if (group->type == GROUP_TYPE_NEWS) {
 1576         struct t_article art;
 1577         struct t_filter_rule rule;
 1578 
 1579 #ifdef __cplusplus /* keep C++ quiet */
 1580         rule.scope[0] = '\0';
 1581 #endif /* __cplusplus */
 1582 
 1583         if (strlen(group->name) > (sizeof(rule.scope) - 1)) /* groupname to long? */
 1584             return FALSE;
 1585 
 1586         /*
 1587          * Setup rules
 1588          */
 1589         rule.counter = 0;
 1590         rule.lines_cmp = FILTER_LINES_NO;
 1591         rule.lines_num = 0;
 1592         rule.from_ok = FALSE;
 1593         rule.lines_ok = FALSE;
 1594         rule.msgid_ok = FALSE;
 1595         rule.fullref = FILTER_MSGID;
 1596         rule.subj_ok = TRUE;
 1597         rule.text[0] = '\0';
 1598         rule.icase = FALSE;
 1599         rule.expire_time = TRUE;
 1600         rule.check_string = TRUE;
 1601         rule.score = tinrc.score_select;
 1602 
 1603         strcpy(rule.scope, group->name);
 1604 
 1605         /* create an auto-comment. */
 1606         snprintf(txt, sizeof(txt), "%s%s", _(txt_filter_rule_created), "add_posted_to_filter=ON.");
 1607         rule.comment = add_filter_comment(NULL, txt);
 1608 
 1609         /*
 1610          * Setup dummy article with posted articles subject
 1611          * xor Message-ID
 1612          */
 1613         set_article(&art);
 1614         if (*a_message_id) {
 1615             /* initialize art->refptr */
 1616             struct {
 1617                 struct t_msgid *next;
 1618                 struct t_msgid *parent;
 1619                 struct t_msgid *sibling;
 1620                 struct t_msgid *child;
 1621                 int article;
 1622                 char txt[HEADER_LEN];
 1623             } refptr_dummyart;
 1624 
 1625             rule.subj_ok = FALSE;
 1626             rule.msgid_ok = TRUE;
 1627             refptr_dummyart.next = (struct t_msgid *) 0;
 1628             refptr_dummyart.parent = (struct t_msgid *) 0;
 1629             refptr_dummyart.sibling = (struct t_msgid *) 0;
 1630             refptr_dummyart.child = (struct t_msgid *) 0;
 1631             refptr_dummyart.article = ART_NORMAL;
 1632             my_strncpy(refptr_dummyart.txt, a_message_id, HEADER_LEN);
 1633             /* Hack */
 1634             art.refptr = (struct t_msgid *) &refptr_dummyart;
 1635 
 1636             filtered = add_filter_rule(group, &art, &rule, FALSE);
 1637         } else {
 1638             art.subject = my_strdup(subj);
 1639             filtered = add_filter_rule(group, &art, &rule, FALSE);
 1640             FreeIfNeeded(art.subject);
 1641         }
 1642         rule.comment = free_filter_comment(rule.comment);
 1643     }
 1644     return filtered;
 1645 }
 1646 
 1647 
 1648 /*
 1649  * API to add filter rule to the local or global filter array
 1650  */
 1651 static t_bool
 1652 add_filter_rule(
 1653     struct t_group *group,
 1654     struct t_article *art,
 1655     struct t_filter_rule *rule,
 1656     t_bool quick_filter_rule)
 1657 {
 1658     char acbuf[PATH_LEN];
 1659     char sbuf[(sizeof(acbuf) / 2)]; /* half as big as acbuf so quote_wild(sbuf) fits into acbuf */
 1660     int i = glob_filter.num;
 1661     t_bool filtered = FALSE;
 1662     time_t current_time;
 1663     struct t_filter *ptr;
 1664 
 1665     if (glob_filter.num >= glob_filter.max)
 1666         expand_filter_array(&glob_filter);
 1667 
 1668     ptr = glob_filter.filter;
 1669 
 1670     ptr[i].inscope = TRUE;
 1671     ptr[i].icase = FALSE;
 1672     ptr[i].fullref = FILTER_MSGID;
 1673     ptr[i].comment = (struct t_filter_comment *) 0;
 1674     ptr[i].scope = NULL;
 1675     ptr[i].subj = NULL;
 1676     ptr[i].from = NULL;
 1677     ptr[i].msgid = NULL;
 1678     ptr[i].lines_cmp = (char) rule->lines_cmp;
 1679     ptr[i].lines_num = rule->lines_num;
 1680     ptr[i].gnksa_cmp = FILTER_LINES_NO;
 1681     ptr[i].gnksa_num = 0;
 1682     ptr[i].score = rule->score;
 1683     ptr[i].xref = NULL;
 1684     ptr[i].path = NULL;
 1685 
 1686     if (rule->comment != NULL)
 1687         ptr[i].comment = copy_filter_comment(rule->comment, ptr[i].comment);
 1688 
 1689     if (rule->scope[0] == '\0') /* replace empty scope with current group name */
 1690         ptr[i].scope = my_strdup(group->name);
 1691     else {
 1692         if ((rule->scope[0] != '*') && (rule->scope[1] != '\0')) /* copy non-global scope */
 1693             ptr[i].scope = my_strdup(rule->scope);
 1694     }
 1695 
 1696     (void) time(&current_time);
 1697     switch (rule->expire_time) {
 1698         case 1:
 1699             ptr[i].time = current_time + (time_t) (tinrc.filter_days * DAY);
 1700             break;
 1701 
 1702         case 2:
 1703             ptr[i].time = current_time + (time_t) (tinrc.filter_days * DAY * 2);
 1704             break;
 1705 
 1706         case 3:
 1707             ptr[i].time = current_time + (time_t) (tinrc.filter_days * DAY * 4);
 1708             break;
 1709 
 1710         default:
 1711             ptr[i].time = (time_t) 0;
 1712             break;
 1713     }
 1714 
 1715     ptr[i].icase = rule->icase;
 1716     if (*rule->text) {
 1717         snprintf(acbuf, sizeof(acbuf), REGEX_FMT, quote_wild_whitespace(rule->text));
 1718 
 1719         switch (rule->counter) {
 1720             case FILTER_SUBJ_CASE_IGNORE:
 1721             case FILTER_SUBJ_CASE_SENSITIVE:
 1722                 ptr[i].subj = my_strdup(acbuf);
 1723                 break;
 1724 
 1725             case FILTER_FROM_CASE_IGNORE:
 1726             case FILTER_FROM_CASE_SENSITIVE:
 1727                 ptr[i].from = my_strdup(acbuf);
 1728                 break;
 1729 
 1730             case FILTER_MSGID:
 1731             case FILTER_MSGID_LAST:
 1732             case FILTER_MSGID_ONLY:
 1733             case FILTER_REFS_ONLY:
 1734                 ptr[i].msgid = my_strdup(acbuf);
 1735                 ptr[i].fullref = rule->counter;
 1736                 break;
 1737 
 1738             default: /* should not happen */
 1739                 /* CONSTANTCONDITION */
 1740                 assert(0 != 0);
 1741                 break;
 1742         }
 1743         filtered = TRUE;
 1744         glob_filter.num++;
 1745     } else {
 1746         /*
 1747          * STRCPY() truncates subject/from/message-id so it fits
 1748          * into acbuf even after quote_wild()
 1749          */
 1750         if (rule->subj_ok) {
 1751             STRCPY(sbuf, art->subject);
 1752             snprintf(acbuf, sizeof(acbuf), REGEX_FMT, (rule->check_string ? quote_wild(sbuf) : sbuf));
 1753             ptr[i].subj = my_strdup(acbuf);
 1754         }
 1755         if (rule->from_ok) {
 1756             STRCPY(sbuf, art->from);
 1757             snprintf(acbuf, sizeof(acbuf), REGEX_FMT, quote_wild(sbuf));
 1758             ptr[i].from = my_strdup(acbuf);
 1759         }
 1760         /*
 1761          * message-ids should be quoted
 1762          */
 1763         if (rule->msgid_ok) {
 1764             /*
 1765              * If threading by references is set, and a quick kill is applied
 1766              * (in group level), it is applied with the data of the root
 1767              * article of the thread built by tin.
 1768              * In case of threading by references, if tin's root is not the
 1769              * *real* root of thread (which is the first entry in references
 1770              * field) any applying of filtering for MSGID (or MSGID_LAST)
 1771              * doesn't work, because the filter is applied with the data of
 1772              * tin's root article which doesn't cover the other articles in
 1773              * the thread.
 1774              * So the thread remains open (in group level). To overcome this,
 1775              * the first msgid from references field is taken in this case.
 1776              */
 1777             if (quick_filter_rule && group->attribute->thread_articles == THREAD_REFS &&
 1778                 (group->attribute->quick_kill_header == FILTER_MSGID ||
 1779                  group->attribute->quick_kill_header == FILTER_REFS_ONLY) &&
 1780                  art->refptr->parent != NULL)
 1781             {
 1782                 /* not real parent, take first references entry as MID */
 1783                 struct t_msgid *xptr;
 1784 
 1785                 for (xptr = art->refptr->parent; xptr->parent != NULL; xptr = xptr->parent)
 1786                     ;
 1787                 STRCPY(sbuf, xptr->txt);
 1788             } else {
 1789                 STRCPY(sbuf, MSGID(art));
 1790             }
 1791             snprintf(acbuf, sizeof(acbuf), REGEX_FMT, quote_wild(sbuf));
 1792             ptr[i].msgid = my_strdup(acbuf);
 1793             ptr[i].fullref = rule->fullref;
 1794         }
 1795         if (rule->subj_ok || rule->from_ok || rule->msgid_ok || rule->lines_ok) {
 1796             filtered = TRUE;
 1797             glob_filter.num++;
 1798         }
 1799     }
 1800 
 1801     if (filtered) {
 1802 #ifdef DEBUG
 1803         if (debug & DEBUG_FILTER)
 1804             wait_message(2, "inscope=[%s] scope=[%s] case=[%d] subj=[%s] from=[%s] msgid=[%s] fullref=[%u] line=[%d %d] time=[%lu]", bool_unparse(ptr[i].inscope), BlankIfNull(rule->scope), ptr[i].icase, BlankIfNull(ptr[i].subj), BlankIfNull(ptr[i].from), BlankIfNull(ptr[i].msgid), ptr[i].fullref, ptr[i].lines_cmp, ptr[i].lines_num, (unsigned long int) ptr[i].time);
 1805 #endif /* DEBUG */
 1806         write_filter_file(filter_file);
 1807     }
 1808     return filtered;
 1809 }
 1810 
 1811 
 1812 /*
 1813  * We assume that any articles which are tagged as killed are also
 1814  * tagged as being read BECAUSE they were killed. We retag them as
 1815  * being unread if they were unread before killing (ART_KILLED_UNREAD).
 1816  * Selected articles will be un"select"ed.
 1817  */
 1818 void
 1819 unfilter_articles(
 1820     struct t_group *group)
 1821 {
 1822     int i;
 1823 
 1824     for_each_art(i) {
 1825         arts[i].score = 0;
 1826         if (IS_KILLED(i)) {
 1827             if (IS_KILLED_UNREAD(i))
 1828                 art_mark(group, &arts[i], ART_UNREAD);
 1829             arts[i].killed = ART_NOTKILLED;
 1830         }
 1831         if (IS_SELECTED(i))
 1832             arts[i].selected = FALSE;
 1833     }
 1834 }
 1835 
 1836 
 1837 /*
 1838  * Filter any articles in specified group.
 1839  * Apply global filter rules followed by group filter rules.
 1840  * In global rules check if scope field set to determine if
 1841  * filter applies to current group.
 1842  */
 1843 t_bool
 1844 filter_articles(
 1845     struct t_group *group)
 1846 {
 1847     char buf[LEN];
 1848     int num, inscope;
 1849     int i, j;
 1850     struct t_filter *ptr;
 1851     struct regex_cache *regex_cache_subj = NULL;
 1852     struct regex_cache *regex_cache_from = NULL;
 1853     struct regex_cache *regex_cache_msgid = NULL;
 1854     struct regex_cache *regex_cache_xref = NULL;
 1855     struct regex_cache *regex_cache_path = NULL;
 1856     t_bool filtered = FALSE;
 1857     t_bool error = FALSE;
 1858     t_bool use_regex = tinrc.wildcard;
 1859 
 1860     /*
 1861      * check if there are any global filter rules
 1862      */
 1863     if (group->glob_filter->num == 0)
 1864         return filtered;
 1865 
 1866     /*
 1867      * Apply global filter rules first if there are any entries
 1868      */
 1869     /*
 1870      * Check if any scope rules are active for this group
 1871      * ie. group=comp.os.linux.help  scope=comp.os.linux.*
 1872      */
 1873     inscope = set_filter_scope(group);
 1874     if (!cmd_line && !batch_mode)
 1875         wait_message(0, _(txt_filter_global_rules), inscope, group->glob_filter->num);
 1876     num = group->glob_filter->num;
 1877     ptr = group->glob_filter->filter;
 1878 
 1879     /*
 1880      * set up cache tables for all types of filter rules
 1881      * (only for regexp matching)
 1882      */
 1883     if (use_regex) {
 1884         size_t msiz;
 1885 
 1886         msiz = sizeof(struct regex_cache) * (size_t) num;
 1887         regex_cache_subj = my_malloc(msiz);
 1888         regex_cache_from = my_malloc(msiz);
 1889         regex_cache_msgid = my_malloc(msiz);
 1890         regex_cache_xref = my_malloc(msiz);
 1891         regex_cache_path = my_malloc(msiz);
 1892         for (j = 0; j < num; j++) {
 1893             regex_cache_init(&regex_cache_subj[j]);
 1894             regex_cache_init(&regex_cache_from[j]);
 1895             regex_cache_init(&regex_cache_msgid[j]);
 1896             regex_cache_init(&regex_cache_xref[j]);
 1897             regex_cache_init(&regex_cache_path[j]);
 1898         }
 1899     }
 1900 
 1901     /*
 1902      * loop through all arts applying global & local filtering rules
 1903      */
 1904     for (i = 0; i < top_art && !error; i++) {
 1905 #ifdef HAVE_SELECT
 1906         if (wait_for_input()) /* allow abort */
 1907             break; /* to free the mem */
 1908 #endif /* HAVE_SELECT */
 1909 
 1910         arts[i].score = 0;
 1911 
 1912         if (tinrc.kill_level == KILL_UNREAD && IS_READ(i)) /* skip only when the article is read */
 1913             continue;
 1914 
 1915         for (j = 0; j < num && !error; j++) {
 1916             if (ptr[j].inscope) {
 1917                 /*
 1918                  * Filter on Subject: line
 1919                  */
 1920                 if (ptr[j].subj != NULL) {
 1921                     switch (test_match(arts[i].subject, ptr[j].subj, ptr[j].icase, use_regex, &regex_cache_subj[j])) {
 1922                         case 1:
 1923                             SET_FILTER(group, i, j);
 1924                             break;
 1925 
 1926                         case -1:
 1927                             error = TRUE;
 1928                             break;
 1929 
 1930                         default:
 1931                             break;
 1932                     }
 1933                 }
 1934 
 1935                 /*
 1936                  * Filter on From: line
 1937                  */
 1938                 if (ptr[j].from != NULL) {
 1939                     if (arts[i].name != NULL)
 1940                         snprintf(buf, sizeof(buf), "%s (%s)", arts[i].from, arts[i].name);
 1941                     else
 1942                         STRCPY(buf, arts[i].from);
 1943 
 1944                     switch (test_match(buf, ptr[j].from, ptr[j].icase, use_regex, &regex_cache_from[j])) {
 1945                         case 1:
 1946                             SET_FILTER(group, i, j);
 1947                             break;
 1948 
 1949                         case -1:
 1950                             error = TRUE;
 1951                             break;
 1952 
 1953                         default:
 1954                             break;
 1955                     }
 1956                 }
 1957 
 1958                 /*
 1959                  * Filter on Message-ID: line
 1960                  * Apply to Message-ID: & References: lines or
 1961                  * Message-ID: & last entry from References: line
 1962                  * Case is important here
 1963                  */
 1964                 if (ptr[j].msgid != NULL) {
 1965                     char *refs = NULL;
 1966                     const char *myrefs = NULL;
 1967                     const char *mymsgid = NULL;
 1968                     int x;
 1969                     struct t_article *art = &arts[i];
 1970                     /*
 1971                      * TODO: nice idea del'd; better apply one rule on all
 1972                      *       fitting articles, so we can switch to an appropriate
 1973                      *       algorithm for each kind of rule, including the
 1974                      *       deleted one.
 1975                      */
 1976 
 1977                     /* myrefs does not need to be freed */
 1978 
 1979                     /* use full references header or just the last entry? */
 1980                     switch (ptr[j].fullref) {
 1981                         case FILTER_MSGID:
 1982                             myrefs = REFS(art, refs);
 1983                             mymsgid = MSGID(art);
 1984                             break;
 1985 
 1986                         case FILTER_MSGID_LAST:
 1987                             myrefs = art->refptr ? (art->refptr->parent ? art->refptr->parent->txt : "") : "";
 1988                             mymsgid = MSGID(art);
 1989                             break;
 1990 
 1991                         case FILTER_MSGID_ONLY:
 1992                             myrefs = "";
 1993                             mymsgid = MSGID(art);
 1994                             break;
 1995 
 1996                         case FILTER_REFS_ONLY:
 1997                             myrefs = REFS(art, refs);
 1998                             mymsgid = "";
 1999                             break;
 2000 
 2001                         default: /* should not happen */
 2002                             /* CONSTANTCONDITION */
 2003                             assert(0 != 0);
 2004                             break;
 2005                     }
 2006 
 2007                     if ((x = test_match(myrefs, ptr[j].msgid, FALSE, use_regex, &regex_cache_msgid[j])) == 0) /* no match */
 2008                         x = test_match(mymsgid, ptr[j].msgid, FALSE, use_regex, &regex_cache_msgid[j]);
 2009 
 2010                     switch (x) {
 2011                         case 1:
 2012                             SET_FILTER(group, i, j);
 2013 #ifdef DEBUG
 2014                             if (debug & DEBUG_FILTER)
 2015                                 debug_print_file("FILTER", "Group[%s] Rule[%d][%s] ArtMSGID[%s] Artnum[%d]", group->name, j, ptr[j].msgid, mymsgid, i);
 2016 #endif /* DEBUG */
 2017                             break;
 2018 
 2019                         case -1:
 2020                             error = TRUE;
 2021                             break;
 2022 
 2023                         default:
 2024                             break;
 2025                     }
 2026                     FreeIfNeeded(refs);
 2027                 }
 2028                 /*
 2029                  * Filter on Lines: line
 2030                  */
 2031                 if ((ptr[j].lines_cmp != FILTER_LINES_NO) && (arts[i].line_count >= 0)) {
 2032                     switch (ptr[j].lines_cmp) {
 2033                         case FILTER_LINES_EQ:
 2034                             if (arts[i].line_count == ptr[j].lines_num) {
 2035                                 SET_FILTER(group, i, j);
 2036                             }
 2037                             break;
 2038 
 2039                         case FILTER_LINES_LT:
 2040                             if (arts[i].line_count < ptr[j].lines_num) {
 2041                                 SET_FILTER(group, i, j);
 2042                             }
 2043                             break;
 2044 
 2045                         case FILTER_LINES_GT:
 2046                             if (arts[i].line_count > ptr[j].lines_num) {
 2047                                 SET_FILTER(group, i, j);
 2048                             }
 2049                             break;
 2050 
 2051                         default:
 2052                             break;
 2053                     }
 2054                 }
 2055 
 2056                 /*
 2057                  * Filter on GNKSA code
 2058                  */
 2059                 if ((ptr[j].gnksa_cmp != FILTER_LINES_NO) && (arts[i].gnksa_code >= 0)) {
 2060                     switch (ptr[j].gnksa_cmp) {
 2061                         case FILTER_LINES_EQ:
 2062                             if (arts[i].gnksa_code == ptr[j].gnksa_num) {
 2063                                 SET_FILTER(group, i, j);
 2064                             }
 2065                             break;
 2066 
 2067                         case FILTER_LINES_LT:
 2068                             if (arts[i].gnksa_code < ptr[j].gnksa_num) {
 2069                                 SET_FILTER(group, i, j);
 2070                             }
 2071                             break;
 2072 
 2073                         case FILTER_LINES_GT:
 2074                             if (arts[i].gnksa_code > ptr[j].gnksa_num) {
 2075                                 SET_FILTER(group, i, j);
 2076                             }
 2077                             break;
 2078 
 2079                         default:
 2080                             break;
 2081                     }
 2082                 }
 2083 
 2084                 /*
 2085                  * Filter on Xref: lines
 2086                  *
 2087                  * "news.foo.bar foo.bar:666 bar.bar:999"
 2088                  * is turned into the Newsgroups like string
 2089                  * foo.bar,bar.bar
 2090                  */
 2091                 if (arts[i].xref && *arts[i].xref) {
 2092                     if (ptr[j].score && ptr[j].xref != NULL) {
 2093                         char *s, *e, *k;
 2094                         t_bool skip = FALSE;
 2095 
 2096                         s = arts[i].xref;
 2097                         if (strchr(s, ' ') || strchr(s, '\t')) {
 2098                             while (*s && !isspace((int) *s))    /* skip server name */
 2099                                 s++;
 2100                             while (*s && isspace((int) *s))
 2101                                 s++;
 2102                         }
 2103 #ifdef DEBUG
 2104                         else { /* server name missing in overview, i.e. colobus 2.1 */
 2105                             if (debug & DEBUG_FILTER) { /* TODO: lang.c, _()? */
 2106                                 debug_print_file("FILTER", "Malformed overview entry: servername missing.");
 2107                                 debug_print_file("FILTER", "\t Xref: %s", arts[i].xref);
 2108                             }
 2109                         }
 2110 #endif /* DEBUG */
 2111                         if (*s) {
 2112                             /* reformat */
 2113                             k = e = my_malloc(strlen(s) + 1);
 2114                             while (*s) {
 2115                                 if (*s == ':') {
 2116                                     *e++ = ',';
 2117                                     skip = TRUE;
 2118                                 }
 2119                                 if (*s != ':' && !isspace((int) *s) && !skip)
 2120                                     *e++ = *s;
 2121                                 if (isspace((int) *s))
 2122                                     skip = FALSE;
 2123                                 s++;
 2124                             }
 2125                             *--e = '\0';
 2126                         } else {
 2127 #ifdef DEBUG
 2128                             if (debug & DEBUG_FILTER) /* TODO: lang.c, _()? */
 2129                                 debug_print_file("FILTER", "Skipping Xref filter");
 2130 #endif /* DEBUG */
 2131                             error = TRUE;
 2132                             break;
 2133                         }
 2134 
 2135                         switch (test_match(k, ptr[j].xref, ptr[j].icase, use_regex, &regex_cache_xref[j])) {
 2136                             case 1:
 2137                                 SET_FILTER(group, i, j);
 2138                                 break;
 2139 
 2140                             case -1:
 2141                                 error = TRUE;
 2142                                 break;
 2143 
 2144                             default:
 2145                                 break;
 2146                         }
 2147                         free(k);
 2148                     }
 2149                 }
 2150 
 2151                 /*
 2152                  * Filter on Path: lines
 2153                  */
 2154                 if (arts[i].path && *arts[i].path) {
 2155                     if (ptr[j].path != NULL) {
 2156                         switch (test_match(arts[i].path, ptr[j].path, ptr[j].icase, use_regex, &regex_cache_path[j])) {
 2157                             case 1:
 2158                                 SET_FILTER(group, i, j);
 2159                                 break;
 2160 
 2161                             case -1:
 2162                                 error = TRUE;
 2163                                 break;
 2164 
 2165                             default:
 2166                                 break;
 2167                         }
 2168                     }
 2169                 }
 2170             }
 2171         }
 2172         /*
 2173          * if (i % (MODULO_COUNT_NUM * 20) == 0)
 2174          *    show_progress("Filter", i, top_art);
 2175          */
 2176     }
 2177 
 2178     /*
 2179      * throw away the contents of all regex_caches
 2180      */
 2181     if (use_regex) {
 2182         for (j = 0; j < num; j++) {
 2183             regex_cache_destroy(&regex_cache_subj[j]);
 2184             regex_cache_destroy(&regex_cache_from[j]);
 2185             regex_cache_destroy(&regex_cache_msgid[j]);
 2186             regex_cache_destroy(&regex_cache_xref[j]);
 2187             regex_cache_destroy(&regex_cache_path[j]);
 2188         }
 2189         free(regex_cache_subj);
 2190         free(regex_cache_from);
 2191         free(regex_cache_msgid);
 2192         free(regex_cache_xref);
 2193         free(regex_cache_path);
 2194     }
 2195 
 2196     /*
 2197      * now entering the main filter loop:
 2198      * all articles have scored, so do kill & select
 2199      */
 2200     if (!error) {
 2201         for_each_art(i) {
 2202             if (arts[i].score <= tinrc.score_limit_kill) {
 2203                 if (arts[i].status == ART_UNREAD)
 2204                     arts[i].killed = ART_KILLED_UNREAD;
 2205                 else
 2206                     arts[i].killed = ART_KILLED;
 2207                 filtered = TRUE;
 2208                 art_mark(group, &arts[i], ART_READ);
 2209                 if (group->attribute->show_only_unread_arts)
 2210                     arts[i].keep_in_base = FALSE;
 2211             } else if (arts[i].score >= tinrc.score_limit_select) {
 2212                 arts[i].selected = TRUE;
 2213             }
 2214         }
 2215     }
 2216 
 2217     if (!cmd_line && !batch_mode)
 2218         clear_message();
 2219 
 2220     return filtered;
 2221 }
 2222 
 2223 
 2224 /*
 2225  * Check if we have to filter on Path: for the given group
 2226  */
 2227 t_bool
 2228 filter_on_path(
 2229     struct t_group *group)
 2230 {
 2231     int i;
 2232     struct t_filter *flt;
 2233     t_bool ret = FALSE;
 2234 
 2235     if (group->glob_filter->num == 0)
 2236         return ret;
 2237 
 2238     if (set_filter_scope(group)) {
 2239         flt = group->glob_filter->filter;
 2240         for (i = 0; i < group->glob_filter->num; i++) {
 2241             if (flt[i].inscope && flt[i].path) {
 2242                 ret = TRUE;
 2243                 break;
 2244             }
 2245         }
 2246     }
 2247     return ret;
 2248 }
 2249 
 2250 
 2251 static int
 2252 set_filter_scope(
 2253     struct t_group *group)
 2254 {
 2255     int i, num, inscope;
 2256     struct t_filter *ptr, *prev;
 2257 
 2258     inscope = num = group->glob_filter->num;
 2259     prev = ptr = group->glob_filter->filter;
 2260 
 2261     for (i = 0; i < num; i++) {
 2262         ptr[i].inscope = TRUE;
 2263         ptr[i].next = (struct t_filter *) 0;
 2264         if (ptr[i].scope != NULL) {
 2265             if (!match_group_list(group->name, ptr[i].scope)) {
 2266                 ptr[i].inscope = FALSE;
 2267                 inscope--;
 2268             }
 2269         }
 2270         if (i != 0 && ptr[i].inscope)
 2271             prev = prev->next = &ptr[i];
 2272     }
 2273     return inscope;
 2274 }
 2275 
 2276 
 2277 /*
 2278  * This will come in useful for filtering on non-overview hdr fields.
 2279  * See also art.c:get_path_header().
 2280  */
 2281 #if 0
 2282 static FILE *
 2283 open_xhdr_fp(
 2284     char *header,
 2285     long min,
 2286     long max)
 2287 {
 2288 #   ifdef NNTP_ABLE
 2289     if (read_news_via_nntp && !read_saved_news && nntp_caps.hdr_cmd) {
 2290         char buf[NNTP_STRLEN];
 2291 
 2292         snprintf(buf, sizeof(buf), "%s %s %ld-%ld", nntp_caps.hdr_cmd, header, min, max);
 2293         return (nntp_command(buf, OK_HEAD, NULL, 0));
 2294     } else
 2295 #   endif /* NNTP_ABLE */
 2296         return (FILE *) 0;      /* Some trick implementation for local spool... */
 2297 }
 2298 #endif /* 0 */