"Fossies" - the Fresh Open Source Software Archive

Member "tin-2.4.2/src/art.c" (8 Dec 2017, 76555 Bytes) of package /linux/misc/tin-2.4.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 "art.c" see the Fossies "Dox" file reference documentation and the latest Fossies "Diffs" side-by-side code changes report: 2.4.1_vs_2.4.2.

    1 /*
    2  *  Project   : tin - a Usenet reader
    3  *  Module    : art.c
    4  *  Author    : I.Lea & R.Skrenta
    5  *  Created   : 1991-04-01
    6  *  Updated   : 2017-02-03
    7  *  Notes     :
    8  *
    9  * Copyright (c) 1991-2018 Iain Lea <iain@bricbrac.de>, Rich Skrenta <skrenta@pbm.com>
   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  * 1. Redistributions of source code must retain the above copyright
   16  *    notice, this list of conditions and the following disclaimer.
   17  * 2. Redistributions in binary form must reproduce the above copyright
   18  *    notice, this list of conditions and the following disclaimer in the
   19  *    documentation and/or other materials provided with the distribution.
   20  * 3. The name of the author may not be used to endorse or promote
   21  *    products derived from this software without specific prior written
   22  *    permission.
   23  *
   24  * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
   25  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
   26  * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
   27  * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
   28  * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
   29  * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
   30  * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
   31  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
   32  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
   33  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
   34  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
   35  */
   36 
   37 
   38 #ifndef TIN_H
   39 #   include "tin.h"
   40 #endif /* !TIN_H */
   41 #ifndef TCURSES_H
   42 #   include "tcurses.h"
   43 #endif /* !TCURSES_H */
   44 #ifndef NEWSRC_H
   45 #   include "newsrc.h"
   46 #endif /* !NEWSRC_H */
   47 
   48 #ifndef STPWATCH_H
   49 #   include "stpwatch.h"
   50 #endif /* !STPWATCH_H */
   51 
   52 /*
   53  * TODO: fixup to remove CURR_GROUP dependency in all sort funcs
   54  */
   55 #define SortBy(func)    tin_sort(arts, (size_t) top_art, sizeof(struct t_article), func);
   56 
   57 int top_art = 0;                /* # of articles in arts[] */
   58 
   59 /*
   60  * Local prototypes
   61  */
   62 static FILE *open_art_header(char *groupname, t_artnum art, t_artnum *next);
   63 static FILE *open_xover_fp(struct t_group *group, const char *mode, t_artnum min, t_artnum max, t_bool local);
   64 static char *find_nov_file(struct t_group *group, int mode);
   65 static char *print_date(time_t secs);
   66 static char *print_from(struct t_group *group, struct t_article *article, int charset);
   67 static int artnum_comp(t_comptype p1, t_comptype p2);
   68 static int base_comp(t_comptype p1, t_comptype p2);
   69 static int date_comp_asc(t_comptype p1, t_comptype p2);
   70 static int date_comp_desc(t_comptype p1, t_comptype p2);
   71 static int from_comp_asc(t_comptype p1, t_comptype p2);
   72 static int from_comp_desc(t_comptype p1, t_comptype p2);
   73 static int global_get_multiparts(int aindex, MultiPartInfo **malloc_and_setme_info);
   74 static int global_look_for_multipart_info(int aindex, MultiPartInfo *setme, char start, char stop, int *offset);
   75 static int last_date_comp_base_asc(t_comptype p1, t_comptype p2);
   76 static int last_date_comp_base_desc(t_comptype p1, t_comptype p2);
   77 static int lines_comp_asc(t_comptype p1, t_comptype p2);
   78 static int lines_comp_desc(t_comptype p1, t_comptype p2);
   79 static int read_art_headers(struct t_group *group, int total, t_artnum top);
   80 static int read_overview(struct t_group *group, t_artnum min, t_artnum max, t_artnum *top, t_bool local);
   81 static int score_comp_asc(t_comptype p1, t_comptype p2);
   82 static int score_comp_desc(t_comptype p1, t_comptype p2);
   83 static int score_comp_base(t_comptype p1, t_comptype p2);
   84 static int subj_comp_asc(t_comptype p1, t_comptype p2);
   85 static int subj_comp_desc(t_comptype p1, t_comptype p2);
   86 static int valid_artnum(t_artnum art);
   87 static t_artnum find_first_unread(struct t_group *group);
   88 static t_artnum setup_hard_base(struct t_group *group);
   89 static t_bool parse_headers(FILE *fp, struct t_article *h);
   90 static t_compfunc eval_sort_arts_func(unsigned int sort_art_type);
   91 static time_t get_last_posting_date(long n);
   92 static void sort_base(unsigned int sort_threads_type);
   93 static void thread_by_multipart(void);
   94 static void thread_by_percentage(struct t_group *group);
   95 static void thread_by_subject(void);
   96 static void write_overview(struct t_group *group);
   97 
   98 
   99 /*
  100  * Display a suitable 'entering group' message if screen needs redrawing
  101  * Allow for the non-printing %s, and the %-age counter
  102  */
  103 void
  104 show_art_msg(
  105     const char *group)
  106 {
  107 /* what if cCOLS < (strlen)+18? */
  108     wait_message(0, _(txt_group), cCOLS - strlen(_(txt_group)) + 2 - 3, group);
  109 }
  110 
  111 
  112 /*
  113  * Construct the pointers to the first (base) article in each thread.
  114  * If we are showing only unread, then point to the first unread. I have
  115  * no idea why this should be so, it causes problems elsewhere [which_response]
  116  */
  117 void
  118 find_base(
  119     struct t_group *group)
  120 {
  121     int i, j;
  122 
  123     grpmenu.max = 0;
  124 
  125 #ifdef DEBUG
  126     debug_print_arts();
  127 #endif /* DEBUG */
  128 
  129     for_each_art(i) {
  130         /*
  131          * .prev will be set on each article that is after the first article in
  132          * the thread. Invalid articles which have been expired will have
  133          * .thread set to ART_EXPIRED
  134          */
  135         if (arts[i].prev >= 0 || arts[i].thread == ART_EXPIRED || (arts[i].killed && tinrc.kill_level == KILL_NOTHREAD))
  136             continue;
  137 
  138         if (grpmenu.max >= max_base)
  139             expand_base();
  140 
  141         if (group->attribute->show_only_unread_arts) {
  142             if (arts[i].status != ART_READ || arts[i].keep_in_base)
  143                 base[grpmenu.max++] = i;
  144             else {
  145                 /* Find 1st unread art in thread */
  146                 for (j = i; j >= 0; j = arts[j].thread) {
  147                     if (arts[j].status != ART_READ || arts[j].keep_in_base) {
  148                         base[grpmenu.max++] = i;
  149                         break;
  150                     }
  151                 }
  152             }
  153         } else
  154             base[grpmenu.max++] = i;
  155     }
  156     /* sort base[] */
  157     if (group->attribute->sort_threads_type > SORT_THREADS_BY_NOTHING)
  158         sort_base(group->attribute->sort_threads_type);
  159 }
  160 
  161 
  162 /*
  163  * Longword comparison routine for the tin_sort()
  164  */
  165 static int
  166 base_comp(
  167     t_comptype p1,
  168     t_comptype p2)
  169 {
  170     const t_artnum *a = (const t_artnum *) p1;
  171     const t_artnum *b = (const t_artnum *) p2;
  172 
  173     if (*a < *b)
  174         return -1;
  175 
  176     if (*a > *b)
  177         return 1;
  178 
  179     return 0;
  180 }
  181 
  182 
  183 /*
  184  * via NNTP:
  185  *   Issue a LISTGROUP command
  186  *   Read the article numbers existing in the group into base[]
  187  *   If the LISTGROUP failed, issue a GROUP command. Use the results to
  188  *   create a less accurate version of base[]
  189  *   This data will already be sorted
  190  *
  191  * on local spool:
  192  *   Read the spool dir to populate base[] as above. Sort it.
  193  *
  194  * Grow the arts[] and bitmaps as needed.
  195  * NB: the output will be sorted on artnum
  196  *
  197  * grpmenu.max is one past top.
  198  * Returns total number of articles in group, or -1 on error
  199  */
  200 static t_artnum
  201 setup_hard_base(
  202     struct t_group *group)
  203 {
  204     t_artnum total = 0;
  205 
  206     grpmenu.max = 0;
  207 
  208     /*
  209      * If reading with NNTP, issue a LISTGROUP
  210      */
  211     if (read_news_via_nntp && !read_saved_news && group->type == GROUP_TYPE_NEWS) {
  212 #ifdef NNTP_ABLE
  213         char buf[NNTP_STRLEN];
  214         char line[NNTP_STRLEN];
  215         int getart_limit = (cmdline.args & CMDLINE_GETART_LIMIT) ? cmdline.getart_limit : tinrc.getart_limit;
  216         FILE *fp;
  217         t_artnum last, start, count = 0, j = 0;
  218         static t_bool skip_listgroup = FALSE;
  219 
  220         /*
  221          * Some nntp servers are broken and need an extra GROUP command
  222          * (reported by reorx@irc.pl). This affects (old?) versions of
  223          * nntpcache, leafnode and SurgeNews. Usually this should not be
  224          * needed.
  225          *
  226          * For getart_limit recheck lowwatermark as at leat giganews gives
  227          * very different results for LIST ACTIVE (3 year retention for all)
  228          * and GROUP (based on the clients contract).
  229          * Calculate range and prepare base[] not to loose unread arts.
  230          */
  231         if (nntp_caps.broken_listgroup || (!skip_listgroup && getart_limit && nntp_caps.type == CAPABILITIES && nntp_caps.reader)) {
  232             snprintf(buf, sizeof(buf), "GROUP %s", group->name);
  233             if (nntp_command(buf, OK_GROUP, line, sizeof(line)) == NULL)
  234                 return -1;
  235 
  236             if (sscanf(line, "%"T_ARTNUM_SFMT" %"T_ARTNUM_SFMT, &count, &start) != 2)
  237                 return -1;
  238 
  239             if (getart_limit > 0) {
  240                 j = group->xmax - getart_limit;
  241                 count = MAX(find_first_unread(group), start);
  242             }
  243             if (getart_limit < 0) {
  244                 j = getart_limit + find_first_unread(group);
  245                 count = group->xmin;
  246             }
  247             if (j < group->xmin)
  248                 j = group->xmin;
  249 
  250             for (; count < j; count++) {
  251                 if (grpmenu.max >= max_base)
  252                     expand_base();
  253                 base[grpmenu.max++] = count;
  254             }
  255         }
  256 
  257         /*
  258          * See if LISTGROUP works
  259          */
  260         if (!skip_listgroup && getart_limit != 0) { /* try to avoid traffic */
  261             if (nntp_caps.type == CAPABILITIES && nntp_caps.reader) {
  262                 /* RFC 3977 allows ranges in LISTGROUP */
  263                 if (getart_limit > 0)
  264                     snprintf(buf, sizeof(buf), "LISTGROUP %s %"T_ARTNUM_PFMT"-%"T_ARTNUM_PFMT"", group->name, j, group->xmax);
  265                 else /* getart_limit < 0; fetch till newsest art */
  266                     snprintf(buf, sizeof(buf), "LISTGROUP %s %"T_ARTNUM_PFMT"-", group->name, j);
  267 
  268             } else /* for RFC 977 just use GROUP */
  269                 skip_listgroup = TRUE;
  270 
  271         } else /* get all article numbers */
  272             snprintf(buf, sizeof(buf), "LISTGROUP %s", group->name);
  273 
  274         if (!skip_listgroup) {
  275             if ((fp = nntp_command(buf, OK_GROUP, NULL, 0)) != NULL) {
  276                 char *ptr;
  277 
  278 #   ifdef DEBUG
  279                 if (debug & DEBUG_NNTP)
  280                     debug_print_file("NNTP", "setup_hard_base(%s)", buf);
  281 #   endif /* DEBUG */
  282 
  283                 while ((ptr = tin_fgets(fp, FALSE)) != NULL) {
  284                     if (grpmenu.max >= max_base)
  285                         expand_base();
  286                     base[grpmenu.max++] = atoartnum(ptr);
  287                     total++;
  288                 }
  289 
  290                 if (tin_errno)
  291                     return -1;
  292             } else
  293                 skip_listgroup = TRUE;
  294         }
  295 
  296         if (skip_listgroup) { /* LISTGROUP was skipped or failed */
  297             /*
  298              * Handle the obscure case that the user aborted before the LISTGROUP
  299              * had a chance to respond
  300              */
  301             if (tin_errno)
  302                 return -1;
  303 
  304             snprintf(buf, sizeof(buf), "GROUP %s", group->name);
  305             if (nntp_command(buf, OK_GROUP, line, sizeof(line)) == NULL)
  306                 return -1;
  307 
  308             if (sscanf(line, "%"T_ARTNUM_SFMT" %"T_ARTNUM_SFMT" %"T_ARTNUM_SFMT, &count, &start, &last) != 3)
  309                 return -1;
  310 
  311 #   ifdef DEBUG
  312             if (debug & DEBUG_NNTP)
  313                 debug_print_file("NNTP", "setup_hard_base(%s)", buf);
  314 #   endif /* DEBUG */
  315             total = count;
  316             grpmenu.max = 0;
  317             if (getart_limit > 0) {
  318                 if ((j = find_first_unread(group)) > start) {
  319                     if (group->xmax > getart_limit) {
  320                         start = MIN(j, group->xmax - getart_limit);
  321                         total = getart_limit;
  322                     } else
  323                         start = j;
  324                 }
  325             }
  326             if (getart_limit < 0) {
  327                 if ((j = (getart_limit + find_first_unread(group))) > start)
  328                     start = j;
  329             }
  330             while (start <= last) {
  331                 if (grpmenu.max >= max_base)
  332                     expand_base();
  333                 base[grpmenu.max++] = start++;
  334             }
  335         }
  336 #endif /* NNTP_ABLE */
  337     /*
  338      * Reading off local spool, read the directory files
  339      */
  340     } else {
  341         DIR *d;
  342         DIR_BUF *e;
  343         char group_path[PATH_LEN];
  344         t_artnum art;
  345 
  346         make_base_group_path(group->spooldir, group->name, group_path, sizeof(group_path));
  347 
  348         if ((d = opendir(group_path)) != NULL) {
  349             while ((e = readdir(d)) != NULL) {
  350                 art = atoartnum(e->d_name);
  351                 if (art >= 1) {
  352                     total++;
  353                     if (grpmenu.max >= max_base)
  354                         expand_base();
  355                     base[grpmenu.max++] = art;
  356                 }
  357             }
  358             CLOSEDIR(d);
  359             tin_sort((char *) base, (size_t) grpmenu.max, sizeof(t_artnum), base_comp);
  360         } else {
  361             perror_message(_(txt_cannot_open), group_path);
  362 #if 0
  363             if (access(group_path, R_OK) != 0)
  364                 error_message(2, _(txt_not_exist));
  365 #endif /* 0 */
  366             return -1;
  367         }
  368     }
  369 
  370     if (grpmenu.max) {
  371         if (base[grpmenu.max - 1] > group->xmax)
  372             group->xmax = base[grpmenu.max - 1];
  373         expand_bitmap(group, base[0]);
  374     }
  375 
  376     return total;
  377 }
  378 
  379 
  380 /*
  381  * Main group indexing routine.
  382  *
  383  * Will read any existing index, create or incrementally update
  384  * the index by looking at the articles in the spool directory,
  385  * and attempt to write a new index if necessary.
  386  *
  387  * Returns FALSE if the user aborted the indexing, otherwise TRUE
  388  */
  389 t_bool
  390 index_group(
  391     struct t_group *group)
  392 {
  393     int i;
  394     int changed;                /* Count of articles whose overview has changed */
  395     int getart_limit;
  396     int respnum;
  397     int total;
  398     t_artnum last_read_article;
  399     t_artnum min, new_min, max;
  400     t_bool caching_xover;
  401     t_bool filtered;
  402 
  403     if (group == NULL)
  404         return TRUE;
  405 
  406     if (!batch_mode)
  407         show_art_msg(group->name);
  408 
  409     signal_context = cArt;          /* Set this only once curr_group is valid */
  410 
  411     hash_reclaim();
  412     free_art_array();
  413     free_msgids();
  414 
  415     BegStopWatch("setup_hard_base()");
  416 
  417     /*
  418      * Get list of valid article numbers
  419      */
  420     if (setup_hard_base(group) < 0)
  421         return FALSE;
  422 
  423     EndStopWatch();
  424     PrintStopWatch();
  425 
  426 #ifdef DEBUG
  427     if (debug & DEBUG_NEWSRC) {
  428         debug_print_comment("Before read_overview");
  429         debug_print_bitmap(group, NULL);
  430     }
  431 #endif /* DEBUG */
  432 
  433     min = grpmenu.max ? base[0] : group->xmin;
  434     max = grpmenu.max ? base[grpmenu.max - 1] : min - 1;
  435 
  436     getart_limit = (cmdline.args & CMDLINE_GETART_LIMIT) ? cmdline.getart_limit : tinrc.getart_limit;
  437 
  438     if (getart_limit > 0) {
  439         if (grpmenu.max && (grpmenu.max > getart_limit))
  440             min = base[grpmenu.max - getart_limit];
  441         else
  442             getart_limit = 0;
  443     } else if (getart_limit < 0) {
  444         t_artnum first_unread = find_first_unread(group);
  445 
  446         if (min - first_unread < getart_limit)
  447             min = first_unread + getart_limit;
  448         else
  449             getart_limit = 0;
  450     }
  451 
  452     /*
  453      * Quit now if no articles
  454      */
  455     if (max < 0)
  456         return FALSE;
  457 
  458     top_art = 0;
  459     last_read_article = T_ARTNUM_CONST(0);
  460 
  461     /*
  462      * Read in the existing overview data for min..max
  463      * This read has local=TRUE set if locally caching XOVER records to ensure
  464      * we pull in any private overview caches in preference to using using OVER
  465      *
  466      * When reading local spool, this will pull in the system wide overview
  467      * cache (if found) otherwise the private overview cache will be read
  468      */
  469     caching_xover = (tinrc.cache_overview_files && nntp_caps.over_cmd && group->type == GROUP_TYPE_NEWS);
  470     if ((changed = read_overview(group, min, max, &last_read_article, caching_xover)) == -1)
  471         return FALSE;   /* user aborted indexing */
  472 
  473     /*
  474      * Fill in the range last_read_article...max using XOVER
  475      * Only do this if the previous read_overview() was against private cache
  476      */
  477     if ((last_read_article < max) && caching_xover) {
  478         new_min = (last_read_article >= min) ? last_read_article + 1 : min;
  479 
  480         if ((i = read_overview(group, new_min, max, &last_read_article, FALSE)) == -1)
  481             return FALSE;   /* user aborted indexing */
  482         else
  483             changed += i;
  484     } else
  485         caching_xover = FALSE;
  486 
  487     /*
  488      * Mark as UNTHREADED all articles that have been verified as valid
  489      * Get num of new arts to index so the user will have an idea of index time
  490      */
  491     for (i = 0, total = 0; i < grpmenu.max; i++) {
  492         if ((respnum = valid_artnum(base[i])) >= 0) {
  493             arts[respnum].thread = ART_UNTHREADED;
  494             continue;
  495         }
  496         if (base[i] <= last_read_article)       /* It is vital this test be done last */
  497             continue;
  498         total++;
  499     }
  500 
  501     /*
  502      * Add any articles to arts[] that are new or were killed
  503      */
  504     if (total > 0) {
  505         new_min = (getart_limit != 0 && last_read_article < min) ? min - 1 : last_read_article;
  506 
  507         if ((i = read_art_headers(group, total, new_min)) == -1)
  508             return FALSE;       /* user aborted indexing */
  509         else
  510             changed += i;
  511     }
  512 
  513 #ifdef DEBUG
  514     if (debug & DEBUG_NEWSRC) {
  515         debug_print_comment("Before parse_unread_arts()");
  516         debug_print_bitmap(group, NULL);
  517     }
  518 #endif /* DEBUG */
  519     /*
  520      * Do this before calling art_mark(,, ART_READ) if you want
  521      * the unread count to be correct.
  522      */
  523     min = getart_limit > 0 ? min : T_ARTNUM_CONST(0);
  524     parse_unread_arts(group, min);
  525 #ifdef DEBUG
  526     if (debug & DEBUG_NEWSRC) {
  527         debug_print_comment("After parse_unread_arts()");
  528         debug_print_bitmap(group, NULL);
  529     }
  530 #endif /* DEBUG */
  531 
  532     /*
  533      * Stat all articles to see if any have expired
  534      */
  535     for_each_art(i) {
  536         if (arts[i].thread == ART_EXPIRED) {
  537             changed++;
  538 #ifdef DEBUG
  539             if (debug & DEBUG_NEWSRC)
  540                 debug_print_comment("art.c: index_group() purging...");
  541 #endif /* DEBUG */
  542             art_mark(group, &arts[i], ART_READ);
  543             if (group->attribute->show_only_unread_arts)
  544                 arts[i].keep_in_base = FALSE;
  545         }
  546     }
  547 
  548     /*
  549      * Only rewrite the index if it has changed
  550      * TODO review the exact logic behind "|| caching_xover"
  551      */
  552     if (changed || caching_xover)
  553         write_overview(group);
  554 
  555     /*
  556      * Create the reference tree. The msgid and ref ptrs will
  557      * be free()d now that the NovFile has been written.
  558      */
  559     build_references(group);
  560 
  561     /*
  562      * Needs access to the reference tree
  563      */
  564     filtered = filter_articles(group);
  565 
  566     BegStopWatch("make_threads()");
  567 
  568     /*
  569      * Thread the group
  570      */
  571     make_threads(group, FALSE);
  572 
  573     EndStopWatch();
  574     PrintStopWatch();
  575 
  576     if ((changed > 0 || filtered) && !batch_mode)
  577         clear_message();
  578 
  579     return TRUE;
  580 }
  581 
  582 
  583 /*
  584  * Returns number of first unread article
  585  */
  586 static t_artnum
  587 find_first_unread(
  588     struct t_group *group)
  589 {
  590     unsigned char *p;
  591     unsigned char *end = group->newsrc.xbitmap;
  592     t_artnum first = group->newsrc.xmin; /* initial value */
  593 
  594     if ((p = group->newsrc.xbitmap)) {
  595         end += group->newsrc.xbitlen / NBITS;
  596         for (; *p == '\0' && p < end; p++, first += NBITS)
  597             ;
  598     }
  599     return first;
  600 }
  601 
  602 
  603 /*
  604  * Open an article for reading just the header
  605  * 'next' is used/updated with the next article number
  606  * to optimise the number of 'HEAD' commands issued on
  607  * groups with holes.
  608  */
  609 static FILE *
  610 open_art_header(
  611     char *groupname,
  612     t_artnum art,
  613     t_artnum *next)
  614 {
  615     char buf[NNTP_STRLEN];
  616 #ifdef NNTP_ABLE
  617     FILE *fp;
  618     int i;
  619 
  620     if (read_news_via_nntp && CURR_GROUP.type == GROUP_TYPE_NEWS) {
  621         /*
  622          * Don't bother requesting if we have not got there yet.
  623          * This is a big win if the group has got holes in it (ie. if 000's
  624          * of articles have expired between active files min & max values).
  625          */
  626         if (art < *next)
  627             return NULL;
  628 
  629         snprintf(buf, sizeof(buf), "HEAD %"T_ARTNUM_PFMT, art);
  630         if ((fp = nntp_command(buf, OK_HEAD, NULL, 0)) != NULL)
  631             return fp;
  632 
  633         /*
  634          * TODO:
  635          * shall we stop on 5xx?, i.e JamNNTPd/2 1.3 responds with
  636          * "503 Access denied" instead of 480 but NEXT still works,
  637          * so tin loops over all articles without getting useful data
  638          */
  639 
  640         /*
  641          * HEAD failed, try to find NEXT
  642          * Should return "223 artno message-id more text...."
  643          */
  644         i = new_nntp_command("NEXT", OK_NOTEXT, buf, sizeof(buf));
  645         switch (i) {
  646             case OK_NOTEXT:
  647                 *next = atoartnum(buf);     /* Set next art number */
  648                 break;
  649 
  650 #   ifndef BROKEN_LISTGROUP
  651             /*
  652              * might happen if LISTGROUP doesn't select group, but
  653              * we are not -DBROKEN_LISTGROUP
  654              */
  655             case ERR_NCING:
  656                 nntp_caps.broken_listgroup = TRUE;
  657                 snprintf(buf, sizeof(buf), "GROUP %s", groupname);
  658                 if (nntp_command(buf, OK_GROUP, NULL, 0) == NULL)
  659                     return NULL;
  660                 snprintf(buf, sizeof(buf), "HEAD %"T_ARTNUM_PFMT, art);
  661                 if ((fp = nntp_command(buf, OK_HEAD, NULL, 0)) != NULL)
  662                     return fp;
  663                 if (nntp_command("NEXT", OK_NOTEXT, buf, sizeof(buf)))
  664                     *next = atoartnum(buf);
  665                 break;
  666 #   endif /* !BROKEN_LISTGROUP */
  667 
  668             default:
  669                 /*
  670                  * TODO: abort loop over all arts on ERR_NONEXT
  671                  */
  672 #   ifndef BROKEN_LISTGROUP
  673                 /*
  674                  * to avoid out of sync responses
  675                  * (listgroup seems to work, but didn't select new group,
  676                  *  so xover seems to work but returns old data)
  677                  * we set listgroup_broken = TRUE; once we saw a
  678                  * ERR_NOARTIG / ERR_NONEXT or the like - even if
  679                  * ERR_NOARTIG may occur on servers where listgroup
  680                  * isn't broken...
  681                  */
  682                 nntp_caps.broken_listgroup = TRUE;
  683 #   endif /* !BROKEN_LISTGROUP */
  684                 break;
  685         }
  686 
  687         return NULL;
  688     }
  689 #endif /* NNTP_ABLE */
  690 
  691     snprintf(buf, sizeof(buf), "%"T_ARTNUM_PFMT, art);
  692     return (fopen(buf, "r"));
  693 }
  694 
  695 
  696 /*
  697  * Called after XOVER/local/private overview databases have been loaded
  698  * Read and parse in headers for any arts not already found (usually
  699  * new articles that have not been indexed yet)
  700  * Any new articles that are added have ->thread set to ART_UNTHREADED
  701  * 'top' is the current highest artnum read
  702  *
  703  * Return values are:
  704  *   >0   Number of additional articles read in
  705  *    0   No additional (new) articles were found
  706  *   -1   user aborted during read
  707  */
  708 static int
  709 read_art_headers(
  710     struct t_group *group,
  711     int total,
  712     t_artnum top)
  713 {
  714     FILE *fp;
  715     char dir[PATH_LEN];
  716     char group_msg[LEN];
  717     int i;
  718     int modified = 0;
  719     t_artnum art;
  720     t_artnum head_next = -1; /* Reset the next article number index (for when HEAD fails) */
  721     t_bool res;
  722 
  723     /*
  724      * Change to groups spooldir to optimize fopen()'s on local articles
  725      * NB open_art_header() requires this
  726      */
  727     if (!read_news_via_nntp || group->type != GROUP_TYPE_NEWS) {
  728         char buf[PATH_LEN];
  729 
  730         get_cwd(dir);
  731         make_base_group_path(group->spooldir, group->name, buf, sizeof(buf));
  732         if (chdir(buf) != 0) {
  733 #ifdef DEBUG
  734             if (debug & DEBUG_MISC)
  735                 fprintf(stderr, "read_art_headers(chdir(%s))", strerror(errno));
  736 #endif /* DEBUG */
  737             return -1;
  738         }
  739     }
  740 
  741     snprintf(group_msg, sizeof(group_msg), _(txt_group), cCOLS - strlen(_(txt_group)) + 2 - 3, group->name);
  742 
  743     for (i = 0; i < grpmenu.max; i++) { /* for each article number */
  744         art = base[i];
  745 
  746         /*
  747          * Skip articles that are below the low water mark or are
  748          * already present
  749          */
  750         if (valid_artnum(art) >= 0)
  751             continue;
  752         if (art <= top)
  753             continue;
  754 
  755         /*
  756          * Try and open the article
  757          */
  758         if ((fp = open_art_header(group->name, art, &head_next)) == NULL)
  759             continue;
  760 
  761         /*
  762          * Add article to arts[]
  763          */
  764         if (top_art >= max_art)
  765             expand_art();
  766 
  767         set_article(&arts[top_art]);
  768         arts[top_art].artnum = art;
  769         arts[top_art].thread = ART_UNTHREADED;
  770 
  771         res = parse_headers(fp, &arts[top_art]);
  772 
  773         TIN_FCLOSE(fp);
  774         if (tin_errno) {
  775             modified = -1;
  776             break;
  777         }
  778 
  779         if (!res) {
  780 #ifdef DEBUG
  781             if (debug & DEBUG_NNTP) {
  782                 char buf[PATH_LEN];
  783 
  784                 snprintf(buf, sizeof(buf), "FAILED parse_headers(%"T_ARTNUM_PFMT")", art);
  785                 debug_print_file("NNTP", "read_art_headers() %s", buf);
  786             }
  787 #endif /* DEBUG */
  788             arts[top_art].artnum = T_ARTNUM_CONST(0);
  789             arts[top_art].date = (time_t) 0;
  790             FreeAndNull(arts[top_art].xref);
  791             FreeAndNull(arts[top_art].refs);
  792             FreeAndNull(arts[top_art].msgid);
  793             if (arts[top_art].archive) {
  794                 FreeAndNull(arts[top_art].archive->partnum);
  795                 FreeAndNull(arts[top_art].archive);
  796             }
  797             arts[top_art].tagged = 0;
  798             arts[top_art].thread = ART_EXPIRED;
  799             arts[top_art].prev = ART_NORMAL;
  800             arts[top_art].status = ART_UNREAD;
  801             arts[top_art].killed = ART_NOTKILLED;
  802             arts[top_art].selected = FALSE;
  803             continue;
  804         }
  805 
  806         top = arts[top_art].artnum; /* used if arts are killed */
  807         top_art++;
  808 
  809         if (++modified % MODULO_COUNT_NUM == 0)
  810             show_progress(group_msg, modified, total);
  811     }
  812 
  813     /*
  814      * Change back to previous dir before indexing started
  815      */
  816     if (!read_news_via_nntp || group->type != GROUP_TYPE_NEWS)
  817         chdir(dir);
  818 
  819     return modified;
  820 }
  821 
  822 
  823 /*
  824  * The algorithm is elegant, using the fact that identical Subject lines
  825  * are hashed to the same node in table[] (see hashstr.c)
  826  *
  827  * Mark i as being in j's thread list if
  828  * . The article is _not_ being ignored
  829  * . The article is not already threaded
  830  * . One of the following is true:
  831  *    1) The subject lines are the same
  832  *    2) Both are part of the same archive (name's match and arch bit set)
  833  * IMHO the tests for archive name are redundant and have been for years
  834  */
  835 static void
  836 thread_by_subject(
  837     void)
  838 {
  839     int i, j;
  840     struct t_hashnode *h;
  841 
  842     for_each_art(i) {
  843         if (IGNORE_ART_THREAD(i))
  844             continue;
  845 
  846         /*
  847          * Get the contents of the magic marker in the hashnode
  848          */
  849         h = (struct t_hashnode *) (arts[i].subject - sizeof(int) - sizeof(void *)); /* FIXME: cast increases required alignment of target type */
  850 
  851         j = h->aptr;
  852 
  853         if (j != -1 && j < i) {
  854 #if 1
  855             if (arts[i].prev == ART_NORMAL && (arts[i].subject == arts[j].subject))
  856 #else
  857             /* see also refs.c:collate_subjects() */
  858             if (arts[i].prev == ART_NORMAL && ((arts[i].subject == arts[j].subject) || (arts[i].archive && arts[j].archive && (arts[i].archive->name == arts[j].archive->name))))
  859 #endif /* 1 */
  860             {
  861                 arts[j].thread = i;
  862                 arts[i].prev = j;
  863             }
  864         }
  865 
  866         /*
  867          * Update the magic marker with the highest numbered mesg in
  868          * arts[] that has been used in this thread so far
  869          */
  870         h->aptr = i;
  871     }
  872 
  873 #if 0
  874     fprintf(stderr, "Subj dump\n");
  875     fprintf(stderr, "%3s %3s %3s %3s : %3s %3s\n", "#", "Par", "Sib", "Chd", "In", "Thd");
  876     for_each_art(i) {
  877         fprintf(stderr, "%3d %3d %3d %3d : %3d %3d : %.50s %s\n", i,
  878             (arts[i].refptr->parent) ? arts[i].refptr->parent->article : -2,
  879             (arts[i].refptr->sibling) ? arts[i].refptr->sibling->article : -2,
  880             (arts[i].refptr->child) ? arts[i].refptr->child->article : -2,
  881             arts[i].prev, arts[i].thread, arts[i].refptr->txt, arts[i].subject);
  882     }
  883 #endif /* 0 */
  884 }
  885 
  886 
  887 /*
  888  * This Threading algorithm threads articles into 'buckets' where each bucket
  889  * contains all the articles which match the root article's subject line to
  890  * the configured percentage. Eg, if the root article had the subject "asdf"
  891  * and the match percentage was configured to be 75% then any article would
  892  * match if its subject was no different in more than a single character.
  893  */
  894 static void
  895 thread_by_percentage(
  896     struct t_group *group)
  897 {
  898     int i, j, k;
  899     int root_num = 0; /* The index number of the root we are currently working on. */
  900     unsigned int unmatched; /* This is the number of characters that don't match between the two strings */
  901     unsigned int percentage = 100 - group->attribute->thread_perc;
  902     size_t slen;
  903 
  904     /* First we need to sort art[] to simplify and speed up the matching. */
  905     SortBy(subj_comp_asc);
  906 
  907     /*
  908      * Now we put all the articles which match enough into the thread. If
  909      * an article doesn't match enough we create a new thread and then add
  910      * to that and so on.
  911      */
  912     base[0] = 0;
  913     arts[0].prev = ART_NORMAL;
  914     for_each_art(i) {
  915         if (i == 0)
  916             continue;
  917 
  918         /* Check each character to see if it matched enough */
  919         k = 0;
  920         unmatched = 0;
  921         for (j = 0; arts[base[root_num]].subject[j] != '\0' && arts[i].subject[k] != '\0'; j++, k++) {
  922             if (arts[base[root_num]].subject[j] != arts[i].subject[k])
  923                 unmatched++;
  924         }
  925 
  926         /*
  927          * By getting here we have a number of unmatched characters
  928          * between the two strings. We also have the length of the
  929          * strings available to us easily.
  930          * All we need to do is see if the match is good enough, but
  931          * we count differences in the length of the strings against
  932          * them matching.
  933          */
  934         if (!(slen = strlen(arts[base[root_num]].subject)))
  935             slen++;
  936         unmatched += slen - strlen(arts[i].subject);
  937         if (unmatched * 100 / slen > percentage) {
  938             /*
  939              * If there is less greater than percentage% different start a
  940              * new thread.
  941              */
  942             base[++root_num] = i;
  943             arts[i].prev = ART_NORMAL;
  944             continue;
  945         } else {
  946             /*
  947              * The subject lines match enough to consider them part of a single
  948              * thread, so add the current article to the thread.
  949              */
  950             if (arts[base[root_num]].thread < 0)
  951                 arts[base[root_num]].thread = i;
  952             arts[i].prev = i - 1;
  953             arts[i - 1].thread = i;
  954             continue;
  955         }
  956     }
  957 }
  958 
  959 
  960 /*
  961  * This was brought over from tags.c, however this version doesn't not
  962  * opperate on base_index
  963  *
  964  * Parses a subject header of the type "multipart message subject (01/42)"
  965  * into a MultiPartInfo struct, or fails if the message subject isn't in the
  966  * right form.
  967  *
  968  * @return nonzero on success
  969  */
  970 int
  971 global_get_multipart_info(
  972     int aindex,
  973     MultiPartInfo *setme)
  974 {
  975     int i, j, offi, offj;
  976     MultiPartInfo setmei, setmej;
  977 
  978     i = global_look_for_multipart_info(aindex, &setmei, '[', ']', &offi);
  979     j = global_look_for_multipart_info(aindex, &setmej, '(', ')', &offj);
  980 
  981     /* Ok i hits first */
  982     if (offi > offj) {
  983         *setme = setmei;
  984         return i;
  985     }
  986 
  987     /* Its j or they are both the same (which must be zero!) so we don't care */
  988     *setme = setmej;
  989     return j;
  990 }
  991 
  992 
  993 /*
  994  * Again this was taken from tags.c, but works on global indices into arts
  995  * rather then on base_index.
  996  */
  997 static int
  998 global_look_for_multipart_info(
  999     int aindex,
 1000     MultiPartInfo* setme,
 1001     char start,
 1002     char stop,
 1003     int *offset)
 1004 {
 1005     char *subj;
 1006     char *pch;
 1007     MultiPartInfo tmp;
 1008 
 1009     *offset = 0;
 1010 
 1011     /* entry assertions */
 1012     assert(0 <= aindex && aindex < top_art && "invalid index");
 1013     assert(setme != NULL && "setme must not be NULL");
 1014 
 1015     /* parse the message */
 1016     subj = arts[aindex].subject;
 1017     pch = strrchr(subj, start);
 1018     if (!pch || !isdigit((int) pch[1]))
 1019         return 0;
 1020 
 1021     tmp.base_index = aindex; /* This could be confusing because we are actually storing the index into arts, not the base_index. */
 1022     tmp.subject_compare_len = pch - subj;
 1023     tmp.part_number = (int) strtol(pch + 1, &pch, 10);
 1024     if (*pch != '/' && *pch != '|')
 1025         return 0;
 1026 
 1027     if (!isdigit((int) pch[1]))
 1028         return 0;
 1029 
 1030     tmp.total = (int) strtol(pch + 1, &pch, 10);
 1031     if (*pch != stop)
 1032         return 0;
 1033 
 1034     tmp.subject = subj;
 1035     *setme = tmp;
 1036     *offset = pch - subj;
 1037     return 1;
 1038 }
 1039 
 1040 
 1041 /*
 1042  * Taken from tags.c but changed to use indices into arts[] instead of
 1043  * base_index. Changed so that even when we don't have all the parts we
 1044  * return a valid array.
 1045  *
 1046  * Tries to find all the parts to the multipart message pointed to by
 1047  * base_index.
 1048  *
 1049  * @return on success, the number of parts found. On failure, zero if not a
 1050  * multipart or the negative value of the first missing part.
 1051  * @param base_index index pointing to one of the messages in a multipart
 1052  * message.
 1053  * @param malloc_and_setme_info on success, set to a malloced array the
 1054  * parts found.
 1055  */
 1056 static int
 1057 global_get_multiparts(
 1058     int aindex,
 1059     MultiPartInfo **malloc_and_setme_info)
 1060 {
 1061     int i;
 1062     int part_index;
 1063     MultiPartInfo tmp, tmp2;
 1064     MultiPartInfo *info = NULL;
 1065 
 1066     /* entry assertions */
 1067     assert(0 <= aindex && aindex < top_art && "Invalid index");
 1068     assert(malloc_and_setme_info != NULL && "malloc_and_setme_info must not be NULL");
 1069 
 1070     /* make sure this is a multipart message... */
 1071     if (!global_get_multipart_info(aindex, &tmp) || tmp.total < 1)
 1072         return 0;
 1073 
 1074     /* make a temporary buffer to hold the multipart info... */
 1075     info = my_malloc(sizeof(MultiPartInfo) * tmp.total);
 1076 
 1077     /* zero out part-number for the repost check below */
 1078     for (i = 0; i < tmp.total; ++i) {
 1079         info[i].total = tmp.total; /* Added this for thread_by_multipart */
 1080         info[i].part_number = -1;
 1081     }
 1082 
 1083     /* try to find all the multiparts... */
 1084     for_each_art(i) {
 1085         if (strncmp(arts[i].subject, tmp.subject, tmp.subject_compare_len))
 1086             continue;
 1087 
 1088         if (!global_get_multipart_info(i, &tmp2))
 1089             continue;
 1090 
 1091         /* 'test (1/5)' is not the same as 'test (1/15)' */
 1092         if (tmp.total != tmp2.total)
 1093             continue;
 1094 
 1095         part_index = tmp2.part_number - 1;
 1096 
 1097         /*
 1098          * skip "blah (00/102)" or "blah (103/102)" subjects
 1099          */
 1100         if (part_index < 0 || part_index >= tmp.total)
 1101             continue;
 1102 
 1103         /* repost check: do we already have this part? */
 1104         if (info[part_index].part_number != -1) {
 1105             assert(info[part_index].part_number == tmp2.part_number && "bookkeeping error");
 1106             continue;
 1107         }
 1108 
 1109         /* we have a match, hooray! */
 1110         info[part_index] = tmp2;
 1111     }
 1112 
 1113     /* see if we got them all. */
 1114     for (i = 0; i < tmp.total; ++i) {
 1115         if (info[i].part_number != i + 1) {
 1116             *malloc_and_setme_info = info;
 1117             return -(i + 1); /* missing part #(i+1) */
 1118         }
 1119     }
 1120 
 1121     /* looks like a success .. */
 1122     *malloc_and_setme_info = info;
 1123     return tmp.total;
 1124 }
 1125 
 1126 
 1127 /*
 1128  *  The algorithm uses the tag multipart searches to thread articles together.
 1129  */
 1130 static void
 1131 thread_by_multipart(
 1132     void)
 1133 {
 1134     int i, j, threadNum;
 1135     MultiPartInfo *minfo = NULL;
 1136 
 1137     for_each_art(i) {
 1138         if (IGNORE_ART_THREAD(i) || arts[i].prev >= 0 || !global_get_multiparts(i, &minfo))
 1139             continue;
 1140 
 1141         threadNum = -1;
 1142         for (j = minfo[0].total - 1; j >= 0; j--) {
 1143             if (minfo[j].part_number != -1) {
 1144                 if (threadNum != -1) {
 1145                     arts[minfo[j].base_index].thread = threadNum;
 1146                     arts[threadNum].prev = minfo[j].base_index;
 1147                 }
 1148 
 1149                 threadNum = minfo[j].base_index;
 1150             }
 1151         }
 1152     }
 1153     free(minfo);
 1154 }
 1155 
 1156 
 1157 /*
 1158  * Go through the articles in arts[] and create threads. There are
 1159  * 5 strategies currently defined :
 1160  *
 1161  *  THREAD_NONE     No threading
 1162  *  THREAD_SUBJ     Threads are created using like Subject lines
 1163  *  THREAD_REFS     Threads are created using the References headers
 1164  *  THREAD_BOTH     Threads created using References and then Subject
 1165  *  THREAD_MULTI    Threads created using Subject to search for Multiparts
 1166  *  THREAD_PERC     Threads based upon a char for char match of greater than x%
 1167  *
 1168  * .thread and .prev are used to hold the threading information, see tin.h for
 1169  * more information
 1170  * Only process valid (unexpired) articles we haven't visited yet
 1171  * (ie arts[].thread == ART_UNTHREADED)
 1172  *
 1173  * The rethread parameter is used to force the deletion of existing threading
 1174  * information before threading which happens anyway expect when using
 1175  * THREAD_NONE (I don't immediately see how this is useful)
 1176  */
 1177 /* TODO: rewrite that user can easly combine different 'threading'
 1178  *       methods, i.e:
 1179  *       - thread_by_multipart() + collate_subjects()
 1180  */
 1181 void
 1182 make_threads(
 1183     struct t_group *group,
 1184     t_bool rethread)
 1185 {
 1186     if (!cmd_line && !batch_mode)
 1187         info_message((group->attribute->thread_articles == THREAD_NONE ? _(txt_unthreading_arts) : _(txt_threading_arts)));
 1188 
 1189 #ifdef DEBUG
 1190     if (debug & DEBUG_MISC)
 1191         error_message(2, "rethread=[%d]  thread_articles=[%d]  attr_thread_articles=[%d]",
 1192                 rethread, tinrc.thread_articles, group->attribute->thread_articles);
 1193 #endif /* DEBUG */
 1194 
 1195     /*
 1196      * Sort all the articles using the preferred method
 1197      * When find_base() is called, the bases are created ordered
 1198      * on arts[] and so the base messages under all threading systems
 1199      * will be sorted in this way.
 1200      */
 1201     sort_arts(group->attribute->sort_article_type);
 1202 
 1203     /*
 1204      * Reset all the ptrs to articles following the above sort
 1205      */
 1206     clear_art_ptrs();
 1207 
 1208     /*
 1209      * The threading pointers need to be reset if re-threading
 1210      * If using ref threading, revector the links back to the articles
 1211      */
 1212     if (rethread || group->attribute->thread_articles) {
 1213         int i;
 1214 
 1215         for_each_art(i) {
 1216             if (arts[i].thread >= 0)
 1217                 arts[i].thread = ART_UNTHREADED;
 1218 
 1219             arts[i].prev = ART_NORMAL;
 1220 
 1221             /* Should never happen if tree is built properly */
 1222             if (arts[i].refptr == NULL) {
 1223 #ifdef DEBUG
 1224                 if (debug & DEBUG_REFS) {
 1225                     my_fprintf(stderr, "\nError  : art->refptr is NULL\n");
 1226                     my_fprintf(stderr, "Artnum : %"T_ARTNUM_PFMT"\n", arts[i].artnum);
 1227                     my_fprintf(stderr, "Subject: %s\n", arts[i].subject);
 1228                     my_fprintf(stderr, "From   : %s\n", arts[i].from);
 1229                     assert(arts[i].refptr != NULL);
 1230                 } else
 1231 #endif /* DEBUG */
 1232                     continue;
 1233             }
 1234             arts[i].refptr->article = i;
 1235         }
 1236     }
 1237 
 1238     /*
 1239      * Do the right thing according to the threading strategy
 1240      */
 1241     switch (group->attribute->thread_articles) {
 1242         case THREAD_NONE:
 1243             break;
 1244 
 1245         case THREAD_SUBJ:
 1246             thread_by_subject();
 1247             break;
 1248 
 1249         case THREAD_REFS:
 1250             thread_by_reference();
 1251             break;
 1252 
 1253         case THREAD_BOTH:
 1254             thread_by_reference();
 1255             collate_subjects();
 1256             break;
 1257 
 1258         case THREAD_MULTI:
 1259             thread_by_multipart();
 1260             break;
 1261 
 1262         case THREAD_PERC:
 1263             thread_by_percentage(group);
 1264             break;
 1265 
 1266         default: /* not reached */
 1267             break;
 1268     }
 1269 
 1270     /*
 1271      * Rebuild base[]
 1272      */
 1273     find_base(group);
 1274 }
 1275 
 1276 
 1277 static t_compfunc
 1278 eval_sort_arts_func(
 1279     unsigned int sort_art_type)
 1280 {
 1281     switch (sort_art_type) {
 1282         case SORT_ARTICLES_BY_NOTHING:      /* don't sort at all */
 1283             return artnum_comp;
 1284 
 1285         case SORT_ARTICLES_BY_SUBJ_DESCEND:
 1286             return subj_comp_desc;
 1287 
 1288         case SORT_ARTICLES_BY_SUBJ_ASCEND:
 1289             return subj_comp_asc;
 1290 
 1291         case SORT_ARTICLES_BY_FROM_DESCEND:
 1292             return from_comp_desc;
 1293 
 1294         case SORT_ARTICLES_BY_FROM_ASCEND:
 1295             return from_comp_asc;
 1296 
 1297         case SORT_ARTICLES_BY_DATE_DESCEND:
 1298             return date_comp_desc;
 1299 
 1300         case SORT_ARTICLES_BY_DATE_ASCEND:
 1301             return date_comp_asc;
 1302 
 1303         case SORT_ARTICLES_BY_SCORE_DESCEND:
 1304             return score_comp_desc;
 1305 
 1306         case SORT_ARTICLES_BY_SCORE_ASCEND:
 1307             return score_comp_asc;
 1308 
 1309         case SORT_ARTICLES_BY_LINES_DESCEND:
 1310             return lines_comp_desc;
 1311 
 1312         case SORT_ARTICLES_BY_LINES_ASCEND:
 1313             return lines_comp_asc;
 1314 
 1315         default:
 1316             break;
 1317     }
 1318     return NULL;
 1319 }
 1320 
 1321 
 1322 void
 1323 sort_arts(
 1324     unsigned int sort_art_type)
 1325 {
 1326     t_compfunc comp_func = eval_sort_arts_func(sort_art_type);
 1327 
 1328     if (comp_func)
 1329         SortBy(comp_func);
 1330 }
 1331 
 1332 
 1333 static void
 1334 sort_base(
 1335     unsigned int sort_threads_type)
 1336 {
 1337     switch (sort_threads_type) {
 1338         case SORT_THREADS_BY_SCORE_DESCEND:
 1339         case SORT_THREADS_BY_SCORE_ASCEND:
 1340             tin_sort(base, (size_t) grpmenu.max, sizeof(t_artnum), score_comp_base);
 1341             break;
 1342 
 1343         case SORT_THREADS_BY_LAST_POSTING_DATE_DESCEND:
 1344             tin_sort(base, (size_t) grpmenu.max, sizeof(t_artnum), last_date_comp_base_desc);
 1345             break;
 1346 
 1347         case SORT_THREADS_BY_LAST_POSTING_DATE_ASCEND:
 1348             tin_sort(base, (size_t) grpmenu.max, sizeof(t_artnum), last_date_comp_base_asc);
 1349             break;
 1350     }
 1351 }
 1352 
 1353 
 1354 /*
 1355  * This is called to get header info for articles not already found in the
 1356  * overview files.
 1357  * Code reads (max_lineno) lines of article to catch headers like Archive-name:
 1358  * which are not normally included in XOVER or even the normal block of headers.
 1359  * How this is supposed to be useful when 99% of the time we'll have overview
 1360  * data I don't know...
 1361  * TODO: move Archive-name: parsing to article body parsing, remove the
 1362  * TODO: max_lineno nonsense and parse just the hdrs. Only parse if
 1363  * TODO: currgrp->auto_save is set, otherwise it is redundant info
 1364  */
 1365 static t_bool
 1366 parse_headers(
 1367     FILE *fp,
 1368     struct t_article *h)
 1369 {
 1370     char art_from_addr[HEADER_LEN];
 1371     char art_full_name[HEADER_LEN];
 1372     char *hdr, *ptr;
 1373     unsigned int lineno = 0;
 1374     unsigned int max_lineno = 25;
 1375     t_bool got_from, got_lines, got_received;
 1376 
 1377     got_from = got_lines = got_received = FALSE;
 1378 
 1379     while ((ptr = tin_fgets(fp, TRUE)) != NULL) {
 1380         /*
 1381          * Look for the end of information which tin wants to get.
 1382          * Applies when reading local spool and via NNTP.
 1383          */
 1384 
 1385         /*
 1386          * as Archive-name: is placed in the body it's safe to exit
 1387          * the loop if it was found.
 1388          */
 1389         if (lineno++ > max_lineno || h->archive)
 1390             break;
 1391 
 1392         unfold_header(ptr);
 1393         switch (toupper((unsigned char) *ptr)) {
 1394             case 'A':   /* Archive-name:  optional */
 1395                 /*
 1396                  * Archive-name: {name}/{part|patch}{number}
 1397                  * eg, acorn/faq/part01
 1398                  */
 1399                 if ((hdr = parse_header(ptr + 1, "rchive-name", FALSE, FALSE, FALSE))) {
 1400                     char *s;
 1401 
 1402                     if ((s = strrchr(hdr, '/')) != NULL) {
 1403                         struct t_archive *archptr = my_malloc(sizeof(struct t_archive));
 1404 
 1405                         if (STRNCASECMPEQ(s + 1, "part", 4)) {
 1406                             archptr->partnum = my_strdup(s + 5);
 1407                             archptr->ispart = TRUE;
 1408                         } else if (STRNCASECMPEQ(s + 1, "patch", 5)) {
 1409                             archptr->partnum = my_strdup(s + 6);
 1410                             archptr->ispart = FALSE;
 1411                         } else {        /* part or patch must be present */
 1412                             free(archptr);
 1413                             continue;
 1414                         }
 1415                         strtok(archptr->partnum, "\n");
 1416                         *s = '\0';
 1417                         archptr->name = hash_str(hdr);
 1418                         h->archive = archptr;
 1419                     }
 1420                 }
 1421                 break;
 1422 
 1423             case 'D':   /* Date:  mandatory */
 1424                 if (!h->date) {
 1425                     if ((hdr = parse_header(ptr + 1, "ate", FALSE, FALSE, FALSE)))
 1426                         h->date = parsedate(hdr, (struct _TIMEINFO *) 0);
 1427                 }
 1428                 break;
 1429 
 1430             case 'F':   /* From:  mandatory */
 1431                 if (!got_from) {
 1432                     if ((hdr = parse_header(ptr + 1, "rom", FALSE, FALSE, FALSE))) {
 1433                         h->gnksa_code = parse_from(hdr, art_from_addr, art_full_name);
 1434                         h->from = hash_str(buffer_to_ascii(art_from_addr));
 1435                         if (*art_full_name)
 1436                             h->name = hash_str(eat_tab(convert_to_printable(rfc1522_decode(art_full_name), FALSE)));
 1437                         got_from = TRUE;
 1438                     }
 1439                 }
 1440                 break;
 1441 
 1442             case 'L':   /* Lines:  optional */
 1443                 if (!got_lines) {
 1444                     if ((hdr = parse_header(ptr + 1, "ines", FALSE, FALSE, FALSE))) {
 1445                         h->line_count = atoi(hdr);
 1446                         got_lines = TRUE;
 1447                     }
 1448                 }
 1449                 break;
 1450 
 1451             case 'M':   /* Message-ID:  mandatory */
 1452                 if (!h->msgid) {
 1453                     if ((hdr = parse_header(ptr + 1, "essage-ID", FALSE, FALSE, FALSE)))
 1454                         h->msgid = my_strdup(hdr);
 1455                 }
 1456                 break;
 1457 
 1458             case 'R':   /* References:  optional */
 1459                 if (!h->refs) {
 1460                     if ((hdr = parse_header(ptr + 1, "eferences", FALSE, FALSE, FALSE)))
 1461                         h->refs = my_strdup(hdr);
 1462                 }
 1463 
 1464                 /* Received:  If found it's probably a mail article */
 1465                 if (!got_received) {
 1466                     if (parse_header(ptr + 1, "eceived", FALSE, FALSE, FALSE)) {
 1467                         max_lineno <<= 1;       /* double the max number of line to read for mails */
 1468                         got_received = TRUE;
 1469                     }
 1470                 }
 1471                 break;
 1472 
 1473             case 'S':   /* Subject:  mandatory */
 1474                 if (!h->subject) {
 1475                     if ((hdr = parse_header(ptr + 1, "ubject", FALSE, FALSE, FALSE)))
 1476                         h->subject = hash_str(eat_re(eat_tab(convert_to_printable(rfc1522_decode(hdr), FALSE)), FALSE));
 1477                 }
 1478                 break;
 1479 
 1480             case 'X':   /* Xref:  optional */
 1481                 if (!h->xref) {
 1482                     if ((hdr = parse_header(ptr + 1, "ref", FALSE, FALSE, FALSE)))
 1483                         h->xref = my_strdup(hdr);
 1484                 }
 1485                 break;
 1486 
 1487             default:
 1488                 break;
 1489         } /* switch */
 1490 
 1491     } /* while */
 1492 
 1493 #ifdef NNTP_ABLE
 1494     if (ptr)
 1495         drain_buffer(fp);
 1496 #endif /* NNTP_ABLE */
 1497 
 1498     if (tin_errno)
 1499         return FALSE;
 1500 
 1501     /*
 1502      * The son of RFC 1036 states that the following hdrs are mandatory. It
 1503      * also states that Subject, Newsgroups and Path are too. Ho hum.
 1504      *
 1505      * What about readinng mail from local spool via ~/.tin/active.mail,
 1506      * they might not have a Message-ID but got_received is very likely to
 1507      * be true.
 1508      */
 1509     if (got_from && h->date && h->msgid) {
 1510         if (!h->subject)
 1511             h->subject = hash_str("<No subject>");
 1512 
 1513 #ifdef DEBUG
 1514         debug_print_header(h);
 1515 #endif /* DEBUG */
 1516         return TRUE;
 1517     }
 1518 
 1519     return FALSE;
 1520 }
 1521 
 1522 
 1523 /*
 1524  * Read in an overview index file. Fields are separated by TAB.
 1525  * return the number of expired articles encountered or -1 if the user aborted
 1526  * the read
 1527  * 'top' is set to the highest artnum read
 1528  * If 'local' is set then always open local overview cache in preference to
 1529  * using NNTP XOVER
 1530  *
 1531  * Format (mandatory as far as line count [RFC2980]):
 1532  *  1. article number (ie. 183)                [mandatory]
 1533  *  2. Subject: line  (ie. Which newsreader?)  [mandatory]
 1534  *  3. From: line     (ie. iain@ecrc.de)       [mandatory]
 1535  *  4. Date: line     (rfc822 format)          [mandatory]
 1536  *  5. MessageID:     (ie. <123@ether.net>)    [mandatory]
 1537  *  6. References:    (ie. <message-id> ....)  [optional]
 1538  *  7. Byte count     (Skipped - not used)     [mandatory]
 1539  *  8. Line count     (ie. 23)                 [mandatory]
 1540  *  9. Xref: line     (ie. alt.test:389)       [optional]
 1541  */
 1542 static int
 1543 read_overview(
 1544     struct t_group *group,
 1545     t_artnum min,
 1546     t_artnum max,
 1547     t_artnum *top,
 1548     t_bool local)
 1549 {
 1550     FILE *fp;
 1551     char *ptr;
 1552     char *q;
 1553     char *buf;
 1554     char *group_msg;
 1555     char art_full_name[HEADER_LEN];
 1556     char art_from_addr[HEADER_LEN];
 1557     unsigned int count;
 1558     int expired = 0;
 1559     t_artnum artnum;
 1560     struct t_article *art;
 1561     size_t over_fields = 1;
 1562 
 1563     /*
 1564      * open the overview file (whether it be local or via nntp)
 1565      */
 1566     if ((fp = open_xover_fp(group, "r", min, max, local)) == NULL)
 1567         return expired;
 1568 
 1569     if (group->xmax > max)
 1570         group->xmax = max;
 1571 
 1572     group_msg = fmt_string(_(txt_group), cCOLS - strlen(_(txt_group)) + 2 - 3, group->name);
 1573 
 1574     /* get the number of fields per over-record as announced by LIST OVERVIEW.FMT */
 1575     if (ofmt) {
 1576         for (; ofmt[over_fields].name; over_fields++)
 1577             ;
 1578     }
 1579     if (!--over_fields) { /* e.g. nntp_caps.type == CAPABILITIES && !nntp_caps.list_overview_fmt -> assume defaults */
 1580         ofmt = my_realloc(ofmt, sizeof(struct t_overview_fmt) * (8 + 1));
 1581         ofmt[0].type = OVER_T_INT;
 1582         ofmt[0].name = my_strdup("Artnum:");
 1583         ofmt[1].type = OVER_T_STRING;
 1584         ofmt[1].name = my_strdup("Subject:");
 1585         ofmt[2].type = OVER_T_STRING;
 1586         ofmt[2].name = my_strdup("From:");
 1587         ofmt[3].type = OVER_T_STRING;
 1588         ofmt[3].name = my_strdup("Date:");
 1589         ofmt[4].type = OVER_T_STRING;
 1590         ofmt[4].name = my_strdup("Message-ID:");
 1591         ofmt[5].type = OVER_T_STRING;
 1592         ofmt[5].name = my_strdup("References:");
 1593         ofmt[6].type = OVER_T_INT;
 1594         ofmt[6].name = my_strdup("Bytes:");
 1595         ofmt[7].type = OVER_T_INT;
 1596         ofmt[7].name = my_strdup("Lines:");
 1597         ofmt[8].type = OVER_T_ERROR;
 1598         ofmt[8].name = NULL;
 1599         over_fields = 7;
 1600     }
 1601 
 1602     while ((buf = tin_fgets(fp, FALSE)) != NULL) {
 1603         if (need_resize) {
 1604             handle_resize((need_resize == cRedraw) ? TRUE : FALSE);
 1605             need_resize = cNo;
 1606         }
 1607 
 1608         /*
 1609          * Read artnum
 1610          */
 1611         if ((ptr = tin_strtok(buf, "\t")) == NULL)
 1612             continue;
 1613 
 1614         /*
 1615          * read the article number, guaranteed to be the first field
 1616          */
 1617         artnum = atoartnum(ptr);
 1618 
 1619         /*
 1620          * artnum field invalid/corrupt or is 1st line of local cached overview
 1621          * (group name)
 1622          */
 1623         if (artnum <= 0)
 1624             continue;
 1625 
 1626         /*
 1627          * skip artnums below the given minimum (getart_limit)
 1628          */
 1629         if (artnum < min)
 1630             continue;
 1631 
 1632         /*
 1633          * Check to make sure article in nov file has not expired in group
 1634          */
 1635         if (artnum < group->xmin) {
 1636             expired++;
 1637             continue;
 1638         }
 1639 
 1640         /*
 1641          * artnum in overview data higher than groups high mark
 1642          *
 1643          * TODO: - warn user about broken overviews?
 1644          *       - try to parse the Xref:-line to get the correct artnum
 1645          *       - see also parse_unread_arts()
 1646          */
 1647         if (artnum > group->xmax)
 1648             continue;
 1649 
 1650         if (top_art >= max_art)
 1651             expand_art();
 1652 
 1653         art = &arts[top_art];
 1654         set_article(art);
 1655         art->artnum = *top = artnum;
 1656 
 1657         /*
 1658          * Note: Fields after line count are not mandatory, use "LIST OVERVIEW.FMT"
 1659          *       to check for additions like we do with xref_supported
 1660          */
 1661         for (count = 1; (ptr = tin_strtok(NULL, "\t")) != NULL; count++) {
 1662             /* skip unexpected tailing fields */
 1663             if (count > over_fields) {
 1664 #ifdef DEBUG
 1665                 if (debug & DEBUG_NNTP)
 1666                     debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") Unexpected overview-field %d of %d: %s", nntp_caps.over_cmd, artnum, count, over_fields, ptr);
 1667 #endif /* DEBUG */
 1668 
 1669                 /* "common error" Xref:full in overview-data but not in OVERVIEW.FMT */
 1670                 if (count == over_fields + 1) {
 1671                     if (!strncasecmp(ptr, "Xref: ", 6)) {
 1672 #ifdef DEBUG
 1673                         if (debug & DEBUG_NNTP)
 1674                             debug_print_file("NNTP", "%s: found unexpected Xref: on semi std. position", nntp_caps.over_cmd);
 1675 #endif /* DEBUG */
 1676                         over_fields++;
 1677                         ofmt = my_realloc(ofmt, sizeof(struct t_overview_fmt) * (over_fields + 2)); /* + 2 = artnum and end-marker */
 1678                         ofmt[over_fields].type = OVER_T_FSTRING;
 1679                         ofmt[over_fields].name = my_strdup("Xref:");
 1680                         ofmt[over_fields + 1].type = OVER_T_ERROR;
 1681                         ofmt[over_fields + 1].name = NULL;
 1682                         xref_supported = TRUE;
 1683                     } else
 1684                         continue;
 1685                 } else
 1686                     continue;
 1687             }
 1688 
 1689             /* for duplicated headers this is last match counts, INN >= 2.5.3 does first match counts */
 1690             if (expensive_over_parse) { /* strange order */
 1691                 /* madatory fields */
 1692                 if (ofmt[count].type == OVER_T_STRING) {
 1693                     if (!strcasecmp(ofmt[count].name, "Subject:")) {
 1694                         if (*ptr)
 1695                             art->subject = hash_str(eat_re(eat_tab(convert_to_printable(rfc1522_decode(ptr), FALSE)), FALSE));
 1696                         else {
 1697                             art->subject = hash_str("");
 1698 #ifdef DEBUG
 1699                             if (debug & DEBUG_NNTP)
 1700                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") empty overview-field %s", nntp_caps.over_cmd, artnum, ofmt[count].name);
 1701 #endif /* DEBUG */
 1702                         }
 1703                         continue;
 1704                     }
 1705 
 1706                     if (!strcasecmp(ofmt[count].name, "From:")) {
 1707                         if (*ptr) {
 1708                             art->gnksa_code = parse_from(ptr, art_from_addr, art_full_name);
 1709                             art->from = hash_str(buffer_to_ascii(art_from_addr));
 1710                             if (*art_full_name)
 1711                                 art->name = hash_str(eat_tab(convert_to_printable(rfc1522_decode(art_full_name), FALSE)));
 1712                         } else {
 1713                             art->from = hash_str("");
 1714 #ifdef DEBUG
 1715                             if (debug & DEBUG_NNTP)
 1716                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") empty overview-field %s", nntp_caps.over_cmd, artnum, ofmt[count].name);
 1717 #endif /* DEBUG */
 1718                         }
 1719                         continue;
 1720                     }
 1721 
 1722                     if (!strcasecmp(ofmt[count].name, "Date:")) {
 1723                         art->date = parsedate(ptr, (TIMEINFO *) 0);
 1724 #ifdef DEBUG
 1725                         if ((debug & DEBUG_NNTP) && art->date == (time_t) -1)
 1726                             debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") bogus overview-field %s %s", nntp_caps.over_cmd, artnum, ofmt[count].name, ptr);
 1727 #endif /* DEBUG */
 1728                         continue;
 1729                     }
 1730 
 1731                     if (!strcasecmp(ofmt[count].name, "Message-ID:")) {
 1732                         if (*ptr) {
 1733                             FreeIfNeeded(art->msgid); /* if field is listed more than once in overview.fmt */
 1734                             art->msgid = my_strdup(ptr);
 1735                         } else {
 1736                             art->msgid = NULL;
 1737 #ifdef DEBUG
 1738                             if (debug & DEBUG_NNTP)
 1739                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") empty overview-field %s", nntp_caps.over_cmd, artnum, ofmt[count].name);
 1740 #endif /* DEBUG */
 1741                         }
 1742                         continue;
 1743                     }
 1744 
 1745                     if (!strcasecmp(ofmt[count].name, "References:")) {
 1746                         if (*ptr) {
 1747                             FreeIfNeeded(art->refs); /* if field is listed more than once in overview.fmt */
 1748                             art->refs = my_strdup(ptr);
 1749                         } else
 1750                             art->refs = NULL;
 1751                         continue;
 1752                     }
 1753                 }
 1754                 /* metadata fields */
 1755                 if (ofmt[count].type == OVER_T_INT) {
 1756                     if (!strcasecmp(ofmt[count].name, "Bytes:")) {
 1757                         if (*ptr) {
 1758 #ifdef DEBUG
 1759                             if ((debug & DEBUG_NNTP) && !isdigit((unsigned char) *ptr))
 1760                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") overview field %d (%s) mismatch: %s", nntp_caps.over_cmd, artnum, count, ofmt[count].name, ptr);
 1761 #endif /* DEBUG */
 1762                         }
 1763                         continue;
 1764                     }
 1765 
 1766                     if (!strcasecmp(ofmt[count].name, "Lines:")) {
 1767                         if (*ptr) {
 1768                             if (isdigit((unsigned char) *ptr))
 1769                                 art->line_count = atoi(ptr);
 1770                             else {
 1771                                 art->line_count = 0;
 1772 #ifdef DEBUG
 1773                                 if (debug & DEBUG_NNTP)
 1774                                     debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") overview field %d (%s) mismatch: %s", nntp_caps.over_cmd, artnum, count, ofmt[count].name, ptr);
 1775 #endif /* DEBUG */
 1776                             }
 1777                         } else
 1778                             art->line_count = 0;
 1779                         continue;
 1780                     }
 1781                 }
 1782             } else { /* first 7 fields are in RFC 3977 order */
 1783                 switch (count) {
 1784                     case 1: /* Subject: */
 1785                         if (*ptr)
 1786                             art->subject = hash_str(eat_re(eat_tab(convert_to_printable(rfc1522_decode(ptr), FALSE)), FALSE));
 1787                         else {
 1788                             art->subject = hash_str("");
 1789 #ifdef DEBUG
 1790                             if (debug & DEBUG_NNTP)
 1791                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") empty overview-field %s", nntp_caps.over_cmd, artnum, ofmt[count].name);
 1792 #endif /* DEBUG */
 1793                         }
 1794                         break;
 1795 
 1796                     case 2: /* From: */
 1797                         if (*ptr) {
 1798                             art->gnksa_code = parse_from(ptr, art_from_addr, art_full_name);
 1799                             art->from = hash_str(buffer_to_ascii(art_from_addr));
 1800                             if (*art_full_name)
 1801                                 art->name = hash_str(eat_tab(convert_to_printable(rfc1522_decode(art_full_name), FALSE)));
 1802                         } else {
 1803                             art->from = hash_str("");
 1804 #ifdef DEBUG
 1805                             if (debug & DEBUG_NNTP)
 1806                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") empty overview-field %s", nntp_caps.over_cmd, artnum, ofmt[count].name);
 1807 #endif /* DEBUG */
 1808                         }
 1809                         break;
 1810 
 1811                     case 3: /* Date: */
 1812                         art->date = parsedate(ptr, (TIMEINFO *) 0);
 1813 #ifdef DEBUG
 1814                         if ((debug & DEBUG_NNTP) && art->date == (time_t) -1)
 1815                             debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") bogus overview-field %s %s", nntp_caps.over_cmd, artnum, ofmt[count].name, ptr);
 1816 #endif /* DEBUG */
 1817                         break;
 1818 
 1819                     case 4: /* Message-ID: */
 1820                         if (*ptr)
 1821                             art->msgid = my_strdup(ptr);
 1822                         else {
 1823                             art->msgid = NULL;
 1824 #ifdef DEBUG
 1825                             if (debug & DEBUG_NNTP)
 1826                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") empty overview-field %s", nntp_caps.over_cmd, artnum, ofmt[count].name);
 1827 #endif /* DEBUG */
 1828                         }
 1829                         break;
 1830 
 1831                     case 5: /* References: */
 1832                         if (*ptr)
 1833                             art->refs = my_strdup(ptr);
 1834                         else
 1835                             art->refs = NULL;
 1836                         break;
 1837 
 1838                     case 6: /* :bytes || Bytes: */
 1839                         if (*ptr) {
 1840 #ifdef DEBUG
 1841                             if ((debug & DEBUG_NNTP) && !isdigit((unsigned char) *ptr))
 1842                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") overview field %d (%s) mismatch: %s", nntp_caps.over_cmd, artnum, count, ofmt[count].name, ptr);
 1843 #endif /* DEBUG */
 1844                         }
 1845                         break;
 1846 
 1847                     case 7: /* :lines || Lines: */
 1848                         if (*ptr) {
 1849                             if (isdigit((unsigned char) *ptr))
 1850                                 art->line_count = atoi(ptr);
 1851                             else {
 1852                                 art->line_count = 0;
 1853 #ifdef DEBUG
 1854                                 if (debug & DEBUG_NNTP)
 1855                                     debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") overview field %d (%s) mismatch: %s", nntp_caps.over_cmd, artnum, count, ofmt[count].name, ptr);
 1856 #endif /* DEBUG */
 1857                             }
 1858                         } else
 1859                             art->line_count = 0;
 1860                         break;
 1861 
 1862                     default:
 1863                         break;
 1864                 }
 1865             }
 1866 
 1867             /* optional fields; for duplicated headers: last match counts, INN >= 2.5.3 does first match counts */
 1868             if (ofmt[count].type == OVER_T_FSTRING) {
 1869                 if (*ptr) {
 1870                     if (!strcasecmp(ofmt[count].name, "Xref:")) {
 1871                         if ((q = parse_header(ptr, "Xref", FALSE, FALSE, FALSE)) != NULL) {
 1872                             FreeIfNeeded(art->xref); /* if field is listed more than once in overview.fmt */
 1873                             art->xref = my_strdup(q);
 1874                         }
 1875 #ifdef DEBUG
 1876                         else {
 1877                             if (debug & DEBUG_NNTP)
 1878                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") bogus overview-field %s %s", nntp_caps.over_cmd, artnum, ofmt[count].name, ptr);
 1879                         }
 1880 #endif /* DEBUG */
 1881                         continue;
 1882                     }
 1883 #if 0 /* code example for hadnling addition overview fields */
 1884                     if (!strcasecmp(ofmt[count].name, "Path:")) {
 1885 #ifdef DEBUG
 1886                         if (debug & DEBUG_NNTP)
 1887                             debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") extra overview-field \"%s\" at position %d %s", nntp_caps.over_cmd, artnum, ofmt[count].name, count, ptr);
 1888 #endif /* DEBUG */
 1889                         continue;
 1890                     }
 1891 #endif /* 0 */
 1892                 }
 1893                 continue;
 1894             }
 1895         }
 1896 
 1897         /*
 1898          * RFC says Message-ID is mandatory in newsgroups (but not in
 1899          * mailgroups etc..) NB. a NULL Message-ID would abort if we ever do
 1900          * threading in mailgroups
 1901          */
 1902         if (!art->msgid && group->type == GROUP_TYPE_NEWS)
 1903             continue;
 1904 
 1905         /* we might loose accuracy here, but that shouldn't hurt */
 1906         if (artnum % MODULO_COUNT_NUM == 0)
 1907             show_progress(group_msg, artnum - min, max - min);
 1908 
 1909         top_art++;              /* Basically this statement commits the article */
 1910     }
 1911 
 1912     free(group_msg);
 1913     TIN_FCLOSE(fp);
 1914 
 1915     if (tin_errno)
 1916         return -1;
 1917 
 1918 #if defined(NNTP_ABLE) && defined(XHDR_XREF)
 1919     if (read_news_via_nntp && !read_saved_news && !xref_supported && nntp_caps.hdr_cmd) {
 1920         char cbuf[HEADER_LEN];
 1921         static t_bool found;
 1922         static t_bool first = TRUE;
 1923 
 1924         if (first) {
 1925             found = TRUE;
 1926             /*
 1927              * TODO: do once at start and cache full result
 1928              *       if "LIST HEADERS RANGE" failed try "LIST HEADERS"?
 1929              */
 1930             if (nntp_caps.type == CAPABILITIES && nntp_caps.list_headers) {
 1931                 int i = new_nntp_command("LIST HEADERS RANGE", 215, cbuf, sizeof(cbuf));
 1932 
 1933                 found = FALSE;
 1934                 switch (i) {
 1935                     case 215:
 1936                         while ((ptr = tin_fgets(FAKE_NNTP_FP, FALSE)) != NULL) {
 1937 #   ifdef DEBUG
 1938                             if (debug & DEBUG_NNTP)
 1939                                 debug_print_file("NNTP", "<<<%s%s", logtime(), ptr);
 1940 #   endif /* DEBUG */
 1941                             if (!found && ((*ptr == ':' && *(ptr + 1) == '\0') || !strncasecmp(ptr, "Xref", 4)))
 1942                                 found = TRUE;
 1943                         }
 1944                         break;
 1945 
 1946                     default:
 1947                         break;
 1948                 }
 1949                 first = FALSE;
 1950             }
 1951         }
 1952 
 1953         if (found) {
 1954             snprintf(cbuf, sizeof(cbuf), "%s XREF %"T_ARTNUM_PFMT"-%"T_ARTNUM_PFMT, nntp_caps.hdr_cmd, min, MAX(min, max));
 1955             group_msg = fmt_string("%s XREF loop", nntp_caps.hdr_cmd); /* TODO: find a better message, move to lang.c */
 1956             if ((fp = nntp_command(cbuf, nntp_caps.hdr ? OK_HDR : OK_HEAD, NULL, 0)) != NULL) { /* RFC 2980 (XHDR) uses 221; RFC 3977 (HDR) uses 225 */
 1957                 while ((ptr = tin_fgets(fp, FALSE)) != NULL) {
 1958                     artnum = atoartnum(ptr);
 1959                     if (artnum <= 0 || artnum < group->xmin || artnum > group->xmax)
 1960                         continue;
 1961                     art = &arts[top_art];
 1962                     set_article(art);
 1963                     if (!art->xref && !strstr(ptr, "(none)")) {
 1964                         if ((q = strchr(ptr, ' ')) == NULL) /* skip article number */
 1965                             continue;
 1966                         ptr = q;
 1967                         while (*ptr && isspace((int) *ptr))
 1968                             ptr++;
 1969                         q = strchr(ptr, '\n');
 1970                         if (q)
 1971                             *q = '\0';
 1972                         art->xref = my_strdup(ptr);
 1973                     }
 1974                     /* we might loose accuracy here, but that shouldn't hurt */
 1975                     if (artnum % MODULO_COUNT_NUM == 0)
 1976                         show_progress(group_msg, artnum - min, max - min);
 1977                 }
 1978             }
 1979             free(group_msg);
 1980         }
 1981     }
 1982 #endif /* NNTP_ABLE && XHDR_XREF */
 1983 
 1984     return expired;
 1985 }
 1986 
 1987 
 1988 /*
 1989  * Write an Nov/Xover index file. Fields are separated by '\t'.
 1990  *
 1991  * Format:
 1992  *  1. article number (ie. 183)                [mandatory]
 1993  *  2. Subject: line  (ie. Which newsreader?)  [mandatory]
 1994  *  3. From: line     (ie. iain@ecrc.de)       [mandatory]
 1995  *  4. Date: line     (rfc822 format)          [mandatory]
 1996  *  5. MessageID:     (ie. <123@ether.net>)    [mandatory]
 1997  *  6. References:    (ie. <message-id> ....)  [optional]
 1998  *  7. Byte count     (Skipped - not used)     [mandatory]
 1999  *  8. Line count     (ie. 23)                 [mandatory]
 2000  *  9. Xref: line     (ie. alt.test:389)       [optional]
 2001  *
 2002  * TODO: as we don't use the original data, we currently can't store
 2003  *       the data (from/subject) in the original charset (we don't store
 2004  *       that info). this has the advantage that we can avoid raw 8bit data
 2005  *       in our overviews, but the disadvantage that we might store the data
 2006  *       with a wrong charset and thus lose information. a simmiliar problem
 2007  *       exists with the data for the from:-line, we don't store it in the
 2008  *       original format, whenever our from-parser (partially) fails we'll
 2009  *       lose information in our overviews (but those couldn't be handeled
 2010  *       by tin anyway, so this is not a real problem).
 2011  *       long-term solution: store the original data in the overview
 2012  *       (tin has to handle raw 8bit data and other ugly stuff in the
 2013  *       overviews anyway and thus we preserver as much info as possible)
 2014  *       this would require some changes in read_overview() and
 2015  *       parse_headers(): don't do the decoding/unfolding there, but in a
 2016  *       second pass right after write_overview(), or two additional fields
 2017  *       which hold the raw data for from/subject. the latter has the
 2018  *       disadvantage that it costs (much) more memory.
 2019  */
 2020 static void
 2021 write_overview(
 2022     struct t_group *group)
 2023 {
 2024     FILE *fp;
 2025     int i;
 2026     struct t_article *article;
 2027 #ifdef CHARSET_CONVERSION
 2028     int c = -1;
 2029 #endif /* CHARSET_CONVERSION */
 2030 
 2031     /*
 2032      * Can't write or caching is off or getart_limit is set
 2033      */
 2034     if (no_write || !tinrc.cache_overview_files || ((cmdline.args & CMDLINE_GETART_LIMIT) ? cmdline.getart_limit : tinrc.getart_limit) != 0)
 2035         return;
 2036 
 2037     if ((fp = open_xover_fp(group, "w", T_ARTNUM_CONST(0), T_ARTNUM_CONST(0), FALSE)) == NULL)
 2038         return;
 2039 
 2040     if (group->attribute->sort_article_type != SORT_ARTICLES_BY_NOTHING)
 2041         SortBy(artnum_comp);
 2042 
 2043     /*
 2044      * Needed to preserve uniqueness in hashed private overview files
 2045      */
 2046     fprintf(fp, "%s\n", group->name);
 2047 
 2048 #ifdef CHARSET_CONVERSION
 2049     /* get undeclared_charset number if required */
 2050     if (group->attribute->undeclared_charset) {
 2051         for (i = 0; txt_mime_charsets[i] != NULL; i++) {
 2052             if (!strcasecmp(group->attribute->undeclared_charset, txt_mime_charsets[i])) {
 2053                 c = i;
 2054                 break;
 2055             }
 2056         }
 2057     }
 2058 #endif /* CHARSET_CONVERSION */
 2059 
 2060     for_each_art(i) {
 2061         char *p;
 2062         char *q, *ref;
 2063 
 2064         article = &arts[i];
 2065 
 2066         if (article->thread != ART_EXPIRED && article->artnum >= group->xmin) {
 2067             ref = NULL;
 2068 
 2069             if (!group->attribute->post_8bit_header) { /* write encoded data */
 2070                 /*
 2071                  * TODO: instead of tinrc.mm_local_charset we'd better use UTF-8
 2072                  *       here and in print_from() in the CHARSET_CONVERSION case.
 2073                  *       note that this requires something like
 2074                  *          buffer_to_network(article->subject, "UTF-8");
 2075                  *       right bfore the rfc1522_encode() call.
 2076                  *
 2077                  *       if we would cache the original undecoded data, we could
 2078                  *       ignore stuff like this.
 2079                  */
 2080                 p = rfc1522_encode(article->subject, tinrc.mm_local_charset, FALSE);
 2081                 /* as the subject might now be folded we have to unfold it */
 2082                 unfold_header(p);
 2083             } else { /* raw data */
 2084                 p = my_strdup(article->subject);
 2085 #ifdef CHARSET_CONVERSION
 2086                 if (group->attribute->undeclared_charset && c != -1) /* use undeclared_charset if set (otherwise local charset is used) */
 2087                     buffer_to_network(p, c);
 2088 #endif /* CHARSET_CONVERSION */
 2089             }
 2090 
 2091             /*
 2092              * replace any '\t's with ' ' in the references-data
 2093              *
 2094              * TODO: nntpext-draft might come up with a new scheme:
 2095              *       For all fields, the value is processed by first
 2096              *       removing all US-ASCII CRLF pairs and then replacing
 2097              *       each remaining US-ASCII NUL, TAB, CR, or LF character
 2098              *       with a single US-ASCII space (for example, CR LF LF TAB
 2099              *       will become two spaces).
 2100              */
 2101             if (article->refs) {
 2102                 ref = q = my_strdup(article->refs);
 2103                 while (*q) {
 2104                     if (*q == '\t')
 2105                         *q = ' ';
 2106                     q++;
 2107                 }
 2108             }
 2109 
 2110             fprintf(fp, "%"T_ARTNUM_PFMT"\t%s\t%s\t%s\t%s\t%s\t%d\t%d",
 2111                 article->artnum,
 2112                 p,
 2113 #ifdef CHARSET_CONVERSION
 2114                 print_from(group, article, c),
 2115 #else
 2116                 print_from(group, article, -1),
 2117 #endif /* CHARSET_CONVERSION */
 2118                 print_date(article->date),
 2119                 BlankIfNull(article->msgid),
 2120                 BlankIfNull(ref),
 2121                 0,  /* bytes */
 2122                 article->line_count);
 2123 
 2124             if (article->xref)
 2125                 fprintf(fp, "\tXref: %s", article->xref);
 2126 
 2127             fprintf(fp, "\n");
 2128             free(p);
 2129             if (article->refs) {
 2130                 FreeIfNeeded(ref);
 2131                 q = NULL;
 2132             }
 2133         }
 2134     }
 2135     fchmod(fileno(fp), (mode_t) (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH));
 2136     fclose(fp);
 2137 }
 2138 
 2139 
 2140 /*
 2141  * A complex little function to determine the correct overview index file
 2142  * according to 'mode' (read or write)
 2143  * NULL is returned if the current setup dictates otherwise
 2144  *
 2145  * GROUP_TYPE_MAIL index files are read/written in ~/.tin/.mail
 2146  * GROUP_TYPE_SAVE index files are read/written in ~/.tin/.save
 2147  *
 2148  * Both of these are hashed
 2149  *
 2150  * GROUP_TYPE_NEWS index files are a little bit more complex
 2151  *
 2152  * When hashing the index filename will be in format number.number.
 2153  * Hashing the groupname gets a number. See if that #.1 file exists;
 2154  * if so, read first line. Is this the group we want? If no, try #.2.
 2155  * Repeat until no such file or we find an existing file that matches
 2156  * our group. Return pointer to path or NULL if not found.
 2157  */
 2158 static char *
 2159 find_nov_file(
 2160     struct t_group *group,
 2161     int mode)
 2162 {
 2163     FILE *fp;
 2164     const char *dir;
 2165     char buf[PATH_LEN];
 2166     int i;
 2167     struct stat sb;
 2168     unsigned long hash;
 2169     static char nov_file[PATH_LEN];
 2170     static t_bool once_only = FALSE;    /* Trap things that are done only 1 time */
 2171 
 2172     if (group == NULL || (mode != R_OK && mode != W_OK))
 2173         return NULL;
 2174 
 2175     switch (group->type) {
 2176         case GROUP_TYPE_MAIL:
 2177             dir = index_maildir;
 2178             break;
 2179 
 2180         case GROUP_TYPE_SAVE:
 2181             dir = index_savedir;
 2182             break;
 2183 
 2184         case GROUP_TYPE_NEWS:
 2185             /*
 2186              * nntp.caps.over_cmd is not an issue here, any gripes and warnings
 2187              * about [X]OVER are handled in nntp_open()
 2188              */
 2189 
 2190             /*
 2191              * When reading via NNTP, system wide overviews are irrelevant, of
 2192              * course, and the private overview filename will be the same for
 2193              * both reading and writing.
 2194              *
 2195              * When working locally, we only use a private cache for reading
 2196              * if requested and when system wide overviews don't already exist.
 2197              * When writing then only private overviews can be used since
 2198              * updating system wide overviews is not safe wrt locking etc.
 2199              *
 2200              * See if local overview file $SPOOLDIR/<groupname>/.overview exists
 2201              */
 2202 #ifndef NNTP_ONLY
 2203             if (!read_news_via_nntp) {
 2204                 make_base_group_path(novrootdir, group->name, buf, sizeof(buf));
 2205                 joinpath(nov_file, sizeof(nov_file), buf, novfilename);
 2206                 if (access(nov_file, R_OK) == 0) {
 2207                     if (mode == R_OK)
 2208                         return nov_file;        /* Use system wide overviews */
 2209                     else
 2210                         return NULL;            /* Don't write cache in this case */
 2211                 }
 2212             }
 2213 #endif /* !NNTP_ONLY */
 2214 
 2215             /*
 2216              * We only get here when private overviews are going to be used
 2217              * Go no further if they are explicitly turned off
 2218              */
 2219             if (!tinrc.cache_overview_files)
 2220                 return NULL;
 2221 
 2222             /*
 2223              * Append -<nntpserver> to private cache dir
 2224              */
 2225             if (!once_only && nntp_server) {
 2226                 size_t sp, ln = strlen(index_newsdir);
 2227 
 2228                 if ((sp = sizeof(index_newsdir) - ln - 1) >= 2) {
 2229                     char *srv = my_strdup(nntp_server);
 2230 
 2231                     strcat(index_newsdir, "-");
 2232                     sp--;
 2233                     ln++;
 2234                     str_lwr(srv);
 2235                     my_strncpy(index_newsdir + ln, srv, sp);
 2236                     free(srv);
 2237                 }
 2238                 once_only = TRUE;
 2239             }
 2240 
 2241             /*
 2242              * Only try to set up the private cache when writing. If it
 2243              * doesn't exist yet, then ergo we can't read from it.
 2244              * The cache will be checked/created on every write; a previous
 2245              * bug report complained that this was not the case
 2246              */
 2247             if (stat(index_newsdir, &sb) == -1) {           /* Private cache doesn't exist */
 2248                 if (mode == R_OK)
 2249                     return NULL;
 2250                 if (my_mkdir(index_newsdir, (mode_t) S_IRWXU) != 0)
 2251                     return NULL;
 2252             } else {
 2253                 if (!S_ISDIR(sb.st_mode))
 2254                     return NULL;
 2255             }
 2256 
 2257             /*
 2258              * Update the newsgroups cache to point to the new location
 2259              * now that we know it is valid
 2260              */
 2261             if (!once_only)
 2262                 joinpath(local_newsgroups_file, sizeof(local_newsgroups_file), index_newsdir, NEWSGROUPS_FILE);
 2263 
 2264             dir = index_newsdir;
 2265             break;
 2266 
 2267         default: /* not reached */
 2268             return NULL;
 2269     }
 2270 
 2271     /*
 2272      * We only get here if writing to a private overview.
 2273      * These always have hashed filenames.
 2274      * Try <hash>.<seqno> and check the group name tagline until
 2275      * matching index file is found. If not found return next unused
 2276      * filename
 2277      */
 2278     hash = hash_groupname(group->name);
 2279 
 2280     for (i = 1; ; i++) {
 2281         char *ptr;
 2282 
 2283         snprintf(buf, sizeof(buf), "%lu.%d", hash, i);
 2284         joinpath(nov_file, sizeof(nov_file), dir, buf);
 2285 
 2286         if ((fp = fopen(nov_file, "r")) == NULL)
 2287             break;
 2288 
 2289         /*
 2290          * No group name header, so not a valid index file => overwrite it
 2291          */
 2292         if (fgets(buf, (int) sizeof(buf), fp) == NULL) {
 2293             fclose(fp);
 2294             break;
 2295         }
 2296         fclose(fp);
 2297 
 2298         if ((ptr = strrchr(buf, '\n')) != NULL)
 2299             *ptr = '\0';
 2300 
 2301         if (strcmp(buf, group->name) == 0)
 2302             break;
 2303     }
 2304 
 2305     return nov_file;
 2306 }
 2307 
 2308 
 2309 /*
 2310  * Run the index file updater only for the groups we've loaded.
 2311  */
 2312 void
 2313 do_update(
 2314     t_bool catchup)
 2315 {
 2316     int i, j, k = 0;
 2317     time_t beg_epoch = 0;
 2318     struct t_article *art;
 2319     struct t_group *group;
 2320 
 2321     if (verbose)
 2322         (void) time(&beg_epoch);
 2323 
 2324     /*
 2325      * loop through groups and update any required index files
 2326      */
 2327     for (i = 0; i < selmenu.max; i++) {
 2328         group = &active[my_group[i]];
 2329         /*
 2330          * FIXME: workaround to get a valid CURR_GROUP
 2331          * it also points to the currently processed group so that
 2332          * the correct attributes are used
 2333          * The correct fix is to get rid of CURR_GROUP
 2334          */
 2335         selmenu.curr = i;
 2336 
 2337         if (group->bogus || !group->subscribed)
 2338             continue;
 2339 
 2340         if (!index_group(group)) {
 2341             for_each_art(j) {
 2342                 art = &arts[j];
 2343                 FreeAndNull(art->refs);
 2344                 FreeAndNull(art->msgid);
 2345             }
 2346             continue;
 2347         }
 2348 
 2349         k++;
 2350 
 2351         if (verbose) {
 2352             my_printf("%s %s\n", (catchup ? _(txt_catchup) : _(txt_updating)), group->name);
 2353             my_flush();
 2354         }
 2355 
 2356         if (catchup) {
 2357             for_each_art(j)
 2358                 art_mark(group, &arts[j], ART_READ);
 2359         }
 2360     }
 2361 
 2362     if (verbose) {
 2363         wait_message(0, _(txt_catchup_update_info),
 2364             (catchup ? _(txt_caughtup) : _(txt_updated)), k,
 2365             PLURAL(selmenu.max, txt_group), (unsigned long int) (time(NULL) - beg_epoch));
 2366     }
 2367 }
 2368 
 2369 
 2370 static int
 2371 artnum_comp(
 2372     t_comptype p1,
 2373     t_comptype p2)
 2374 {
 2375     const struct t_article *s1 = (const struct t_article *) p1;
 2376     const struct t_article *s2 = (const struct t_article *) p2;
 2377 
 2378     /*
 2379      * s1->artnum less than s2->artnum
 2380      */
 2381     if (s1->artnum < s2->artnum)
 2382         return -1;
 2383 
 2384     /*
 2385      * s1->artnum greater than s2->artnum
 2386      */
 2387     if (s1->artnum > s2->artnum)
 2388         return 1;
 2389 
 2390     return 0;
 2391 }
 2392 
 2393 
 2394 /*
 2395  * return result of strcmp (reversed for descending)
 2396  */
 2397 static int
 2398 subj_comp_asc(
 2399     t_comptype p1,
 2400     t_comptype p2)
 2401 {
 2402     int retval;
 2403     const struct t_article *s1 = (const struct t_article *) p1;
 2404     const struct t_article *s2 = (const struct t_article *) p2;
 2405 
 2406     if ((retval = strcasecmp(s1->subject, s2->subject))) /* != 0 */
 2407         return retval;
 2408 
 2409     return s1->date - s2->date > 0 ? 1 : -1;
 2410 }
 2411 
 2412 
 2413 static int
 2414 subj_comp_desc(
 2415     t_comptype p1,
 2416     t_comptype p2)
 2417 {
 2418     int retval;
 2419     const struct t_article *s1 = (const struct t_article *) p1;
 2420     const struct t_article *s2 = (const struct t_article *) p2;
 2421 
 2422     if ((retval = strcasecmp(s2->subject, s1->subject))) /* != 0 */
 2423         return retval;
 2424 
 2425     return s1->date - s2->date > 0 ? 1 : -1;
 2426 }
 2427 
 2428 
 2429 /*
 2430  * return result of strcmp (reversed for descending)
 2431  */
 2432 static int
 2433 from_comp_asc(
 2434     t_comptype p1,
 2435     t_comptype p2)
 2436 {
 2437     int retval;
 2438     const struct t_article *s1 = (const struct t_article *) p1;
 2439     const struct t_article *s2 = (const struct t_article *) p2;
 2440 
 2441     if ((retval = strcasecmp(s1->from, s2->from))) /* != 0 */
 2442         return retval;
 2443 
 2444     return s1->date - s2->date > 0 ? 1 : -1;
 2445 }
 2446 
 2447 
 2448 static int
 2449 from_comp_desc(
 2450     t_comptype p1,
 2451     t_comptype p2)
 2452 {
 2453     int retval;
 2454     const struct t_article *s1 = (const struct t_article *) p1;
 2455     const struct t_article *s2 = (const struct t_article *) p2;
 2456 
 2457     if ((retval = strcasecmp(s2->from, s1->from))) /* != 0 */
 2458         return retval;
 2459 
 2460     return s1->date - s2->date > 0 ? 1 : -1;
 2461 }
 2462 
 2463 
 2464 /*
 2465  * Works like strcmp() for comparing time_t type values
 2466  * Return codes:
 2467  *  -1:     If p1 is before p2
 2468  *   0:     If they are the same time
 2469  *   1:     If p1 is after p2
 2470  * If the sort order is _not_ DATE_ASCEND then the sense of the above
 2471  * is reversed.
 2472  */
 2473 static int
 2474 date_comp_asc(
 2475     t_comptype p1,
 2476     t_comptype p2)
 2477 {
 2478     const struct t_article *s1 = (const struct t_article *) p1;
 2479     const struct t_article *s2 = (const struct t_article *) p2;
 2480 
 2481     /*
 2482      * s1->date less than s2->date
 2483      */
 2484     if (s1->date < s2->date)
 2485         return -1;
 2486 
 2487     /*
 2488      * s1->date greater than s2->date
 2489      */
 2490     if (s1->date > s2->date)
 2491         return 1;
 2492 
 2493     return 0;
 2494 }
 2495 
 2496 
 2497 static int
 2498 date_comp_desc(
 2499     t_comptype p1,
 2500     t_comptype p2)
 2501 {
 2502     const struct t_article *s1 = (const struct t_article *) p1;
 2503     const struct t_article *s2 = (const struct t_article *) p2;
 2504 
 2505     /*
 2506      * s2->date less than s1->date
 2507      */
 2508     if (s2->date < s1->date)
 2509         return -1;
 2510 
 2511     /*
 2512      * s2->date greater than s1->date
 2513      */
 2514     if (s2->date > s1->date)
 2515         return 1;
 2516 
 2517     return 0;
 2518 }
 2519 
 2520 
 2521 /*
 2522  * Same again, but for art[].score
 2523  */
 2524 static int
 2525 score_comp_asc(
 2526     t_comptype p1,
 2527     t_comptype p2)
 2528 {
 2529     const struct t_article *s1 = (const struct t_article *) p1;
 2530     const struct t_article *s2 = (const struct t_article *) p2;
 2531 
 2532     if (s1->score < s2->score)
 2533         return -1;
 2534 
 2535     if (s1->score > s2->score)
 2536         return 1;
 2537 
 2538     return s1->date - s2->date > 0 ? 1 : -1;
 2539 }
 2540 
 2541 
 2542 static int
 2543 score_comp_desc(
 2544     t_comptype p1,
 2545     t_comptype p2)
 2546 {
 2547     const struct t_article *s1 = (const struct t_article *) p1;
 2548     const struct t_article *s2 = (const struct t_article *) p2;
 2549 
 2550     if (s2->score < s1->score)
 2551         return -1;
 2552 
 2553     if (s2->score > s1->score)
 2554         return 1;
 2555 
 2556     return s1->date - s2->date > 0 ? 1 : -1;
 2557 }
 2558 
 2559 
 2560 /*
 2561  * Same again, but for art[].line_count
 2562  */
 2563 static int
 2564 lines_comp_asc(
 2565     t_comptype p1,
 2566     t_comptype p2)
 2567 {
 2568     const struct t_article *s1 = (const struct t_article *) p1;
 2569     const struct t_article *s2 = (const struct t_article *) p2;
 2570 
 2571     if (s1->line_count < s2->line_count)
 2572         return -1;
 2573 
 2574     if (s1->line_count > s2->line_count)
 2575         return 1;
 2576 
 2577     return s1->date - s2->date > 0 ? 1 : -1;
 2578 }
 2579 
 2580 
 2581 static int
 2582 lines_comp_desc(
 2583     t_comptype p1,
 2584     t_comptype p2)
 2585 {
 2586     const struct t_article *s1 = (const struct t_article *) p1;
 2587     const struct t_article *s2 = (const struct t_article *) p2;
 2588 
 2589     if (s2->line_count < s1->line_count)
 2590         return -1;
 2591 
 2592     if (s2->line_count > s1->line_count)
 2593         return 1;
 2594 
 2595     return s1->date - s2->date > 0 ? 1 : -1;
 2596 }
 2597 
 2598 
 2599 /*
 2600  * Compares the total score of two threads. Used for sorting base[].
 2601  */
 2602 static int
 2603 score_comp_base(
 2604     t_comptype p1,
 2605     t_comptype p2)
 2606 {
 2607     int a = get_score_of_thread(*(const long *) p1);
 2608     int b = get_score_of_thread(*(const long *) p2);
 2609 
 2610     /* If scores are equal, compare using the article sort order.
 2611      * This determines the order in a group of equally scored threads.
 2612      */
 2613     if (a == b) {
 2614         const struct t_article *s1 = &arts[*(const long *) p1];
 2615         const struct t_article *s2 = &arts[*(const long *) p2];
 2616         t_compfunc comp_func = eval_sort_arts_func(CURR_GROUP.attribute->sort_article_type);
 2617 
 2618         if (comp_func)
 2619             return (*comp_func)(s1, s2);
 2620         return 0;
 2621     }
 2622 
 2623     if (CURR_GROUP.attribute->sort_threads_type == SORT_THREADS_BY_SCORE_ASCEND)
 2624         return a > b ? 1 : -1;
 2625     return a < b ? 1 : -1;
 2626 }
 2627 
 2628 
 2629 /*
 2630  * Compare the date of the last posted article of two threads.
 2631  * Used for sorting base[].
 2632  */
 2633 static int
 2634 last_date_comp_base_desc(
 2635     t_comptype p1,
 2636     t_comptype p2)
 2637 {
 2638     time_t s1_last = get_last_posting_date(*(const long *) p1);
 2639     time_t s2_last = get_last_posting_date(*(const long *) p2);
 2640 
 2641     if (s2_last < s1_last)
 2642         return -1;
 2643 
 2644     if (s2_last > s1_last)
 2645         return 1;
 2646 
 2647     return 0;
 2648 }
 2649 
 2650 
 2651 static int
 2652 last_date_comp_base_asc(
 2653     t_comptype p1,
 2654     t_comptype p2)
 2655 {
 2656     time_t s1_last = get_last_posting_date(*(const long *) p1);
 2657     time_t s2_last = get_last_posting_date(*(const long *) p2);
 2658 
 2659     if (s2_last > s1_last)
 2660         return -1;
 2661 
 2662     if (s2_last < s1_last)
 2663         return 1;
 2664 
 2665     return 0;
 2666 }
 2667 
 2668 
 2669 static time_t
 2670 get_last_posting_date(
 2671     long n)
 2672 {
 2673     long i;
 2674     time_t last = (time_t) 0;
 2675 
 2676     for (i = n; i >= 0; i = arts[i].thread) {
 2677         if (arts[i].date > last)
 2678             last = arts[i].date;
 2679     }
 2680 
 2681     return last;
 2682 }
 2683 
 2684 
 2685 void
 2686 set_article(
 2687     struct t_article *art)
 2688 {
 2689     art->subject = NULL;
 2690     art->from = NULL;
 2691     art->name = NULL;
 2692     art->date = (time_t) 0;
 2693     art->xref = NULL;
 2694     art->msgid = NULL;
 2695     art->refs = NULL;
 2696     art->refptr = NULL;
 2697     art->line_count = -1;
 2698     art->archive = NULL;
 2699     art->tagged = 0;
 2700     art->thread = ART_EXPIRED;
 2701     art->prev = ART_NORMAL;
 2702     art->score = 0;
 2703     art->status = ART_UNREAD;
 2704     art->killed = ART_NOTKILLED;
 2705     art->zombie = FALSE;
 2706     art->delete_it = FALSE;
 2707     art->selected = FALSE;
 2708     art->inrange = FALSE;
 2709     art->matched = FALSE;
 2710     art->keep_in_base = FALSE;
 2711 }
 2712 
 2713 
 2714 /*
 2715  * Do a binary chop to see if 'art' (an article number) exists in arts[]
 2716  * Naturally arts[] must be sorted on artnum
 2717  * Return index into arts[] or -1
 2718  */
 2719 static int
 2720 valid_artnum(
 2721     t_artnum art)
 2722 {
 2723     int prev, range;
 2724     int dctop = top_art;
 2725     int cur = 1;
 2726 
 2727     while ((dctop >>= 1))
 2728         cur <<= 1;
 2729 
 2730     range = cur >> 1;
 2731     cur--;
 2732 
 2733     forever {
 2734         if (arts[cur].artnum == art)
 2735             return cur;
 2736 
 2737         prev = cur;
 2738         cur += ((arts[cur].artnum < art) ? range : -range);
 2739         if (prev == cur)
 2740             break;
 2741 
 2742         if (cur >= top_art)
 2743             cur = top_art - 1;
 2744 
 2745         range >>= 1;
 2746     }
 2747     return -1;
 2748 }
 2749 
 2750 
 2751 /*
 2752  * Loop over arts[] to see if 'art' (an article number) exists in arts[]
 2753  * Needed if arts[] is not sorted on artnum
 2754  * Return index into arts[] or -1
 2755  */
 2756 int
 2757 find_artnum(
 2758     t_artnum art)
 2759 {
 2760     int i;
 2761 
 2762     for_each_art(i) {
 2763         if (arts[i].artnum == art)
 2764             return i;
 2765     }
 2766     return -1;
 2767 }
 2768 
 2769 
 2770 /*----------------------------- Overview handling -----------------------*/
 2771 /* TODO: use
 2772  *           setlocale(LC_ALL, "POSIX"); setlocale(LC_TIME, "POSIX");
 2773  *           my_strftime(date, sizeof(date) -1, "%d %b %Y %H:%M:%S GMT", gmtime(&secs));
 2774  *       instead?
 2775  */
 2776 static char *
 2777 print_date(
 2778     time_t secs)
 2779 {
 2780     static char date[25];
 2781     struct tm *tm;
 2782     static const char *const months_a[] = {
 2783         "Jan", "Feb", "Mar", "Apr", "May", "Jun",
 2784         "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
 2785     };
 2786 
 2787     if ((tm = gmtime(&secs)) != NULL)
 2788         snprintf(date, sizeof(date), "%02d %.3s %04d %02d:%02d:%02d GMT",
 2789                 tm->tm_mday,
 2790                 months_a[tm->tm_mon],
 2791                 tm->tm_year + 1900,
 2792                 tm->tm_hour, tm->tm_min, tm->tm_sec);
 2793     else
 2794         snprintf(date, sizeof(date), "01 Jan 1970 00:00:00 UTC");
 2795 
 2796     return date;
 2797 }
 2798 
 2799 
 2800 static char *
 2801 print_from(
 2802     struct t_group *group,
 2803     struct t_article *article,
 2804     int charset)
 2805 {
 2806     char *p, *q;
 2807     static char from[PATH_LEN];
 2808 
 2809     *from = '\0';
 2810 
 2811     if (article->name != NULL) {
 2812         q = my_strdup(article->name);
 2813 #ifdef CHARSET_CONVERSION
 2814         if (charset != -1) {
 2815             buffer_to_network(q, charset);
 2816         }
 2817 #endif /* CHARSET_CONVERSION */
 2818         p = rfc1522_encode(article->name, tinrc.mm_local_charset, FALSE);
 2819         unfold_header(p);
 2820         if (strpbrk(article->name, "\".:;<>@[]()\\") != NULL && article->name[0] != '"' && article->name[strlen(article->name)] != '"')
 2821             snprintf(from, sizeof(from), "\"%s\" <%s>", group->attribute->post_8bit_header ? q : p, article->from);
 2822         else
 2823             snprintf(from, sizeof(from), "%s <%s>", group->attribute->post_8bit_header ? q : p, article->from);
 2824 
 2825         free(p);
 2826         free(q);
 2827     } else
 2828         STRCPY(from, article->from);
 2829 
 2830     return from;
 2831 }
 2832 
 2833 
 2834 /*
 2835  * Open a group news overview file
 2836  * Use NNTP XOVER where possible unless 'local' is set
 2837  */
 2838 static FILE *
 2839 open_xover_fp(
 2840     struct t_group *group,
 2841     const char *mode,
 2842     t_artnum min,
 2843     t_artnum max,
 2844     t_bool local)
 2845 {
 2846 #ifdef NNTP_ABLE
 2847     if (!local && nntp_caps.over_cmd && *mode == 'r' && group->type == GROUP_TYPE_NEWS) {
 2848         char line[NNTP_STRLEN];
 2849 
 2850         if (!max)
 2851             return NULL;
 2852         if (min == max)
 2853             snprintf(line, sizeof(line), "%s %"T_ARTNUM_PFMT, nntp_caps.over_cmd, min);
 2854         else
 2855             snprintf(line, sizeof(line), "%s %"T_ARTNUM_PFMT"-%"T_ARTNUM_PFMT, nntp_caps.over_cmd, min, MAX(min, max));
 2856         return (nntp_command(line, OK_XOVER, NULL, 0));
 2857     }
 2858 #endif /* NNTP_ABLE */
 2859     {
 2860         FILE *fp;
 2861         char *nov_file = find_nov_file(group, (*mode == 'r') ? R_OK : W_OK);
 2862 
 2863         if (nov_file != NULL) {
 2864             if ((fp = fopen(nov_file, mode)) != NULL)
 2865                 return fp;
 2866 
 2867             if (*mode != 'r')
 2868                 error_message(2, _(txt_cannot_open), nov_file);
 2869         }
 2870     }
 2871     return NULL;
 2872 }
 2873 
 2874 
 2875 #ifdef USE_HEAPSORT
 2876 int
 2877 tin_sort(
 2878     void *sbase,
 2879     size_t nel,
 2880     size_t width,
 2881     t_compfunc compar)
 2882 {
 2883     int rc;
 2884 
 2885     switch (tinrc.sort_function) {
 2886         case 0:
 2887             qsort(sbase, nel, width, compar);
 2888             rc = 0;
 2889             break;
 2890 
 2891         case 1:
 2892             rc = heapsort(sbase, nel, width, compar);
 2893             break;
 2894 
 2895         default:
 2896             rc = -1;
 2897             break;
 2898     }
 2899     return rc;
 2900 }
 2901 #endif /* USE_HEAPSORT */