"Fossies" - the Fresh Open Source Software Archive

Member "tin-2.4.1/src/art.c" (24 Dec 2016, 76397 Bytes) of package /linux/misc/tin-2.4.1.tar.gz:


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.0_vs_2.4.1.

    1 /*
    2  *  Project   : tin - a Usenet reader
    3  *  Module    : art.c
    4  *  Author    : I.Lea & R.Skrenta
    5  *  Created   : 1991-04-01
    6  *  Updated   : 2016-10-10
    7  *  Notes     :
    8  *
    9  * Copyright (c) 1991-2017 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         chdir(buf);
  733     }
  734 
  735     snprintf(group_msg, sizeof(group_msg), _(txt_group), cCOLS - strlen(_(txt_group)) + 2 - 3, group->name);
  736 
  737     for (i = 0; i < grpmenu.max; i++) { /* for each article number */
  738         art = base[i];
  739 
  740         /*
  741          * Skip articles that are below the low water mark or are
  742          * already present
  743          */
  744         if (valid_artnum(art) >= 0)
  745             continue;
  746         if (art <= top)
  747             continue;
  748 
  749         /*
  750          * Try and open the article
  751          */
  752         if ((fp = open_art_header(group->name, art, &head_next)) == NULL)
  753             continue;
  754 
  755         /*
  756          * Add article to arts[]
  757          */
  758         if (top_art >= max_art)
  759             expand_art();
  760 
  761         set_article(&arts[top_art]);
  762         arts[top_art].artnum = art;
  763         arts[top_art].thread = ART_UNTHREADED;
  764 
  765         res = parse_headers(fp, &arts[top_art]);
  766 
  767         TIN_FCLOSE(fp);
  768         if (tin_errno) {
  769             modified = -1;
  770             break;
  771         }
  772 
  773         if (!res) {
  774 #ifdef DEBUG
  775             if (debug & DEBUG_NNTP) {
  776                 char buf[PATH_LEN];
  777 
  778                 snprintf(buf, sizeof(buf), "FAILED parse_headers(%"T_ARTNUM_PFMT")", art);
  779                 debug_print_file("NNTP", "read_art_headers() %s", buf);
  780             }
  781 #endif /* DEBUG */
  782             arts[top_art].artnum = T_ARTNUM_CONST(0);
  783             arts[top_art].date = (time_t) 0;
  784             FreeAndNull(arts[top_art].xref);
  785             FreeAndNull(arts[top_art].refs);
  786             FreeAndNull(arts[top_art].msgid);
  787             if (arts[top_art].archive) {
  788                 FreeAndNull(arts[top_art].archive->partnum);
  789                 FreeAndNull(arts[top_art].archive);
  790             }
  791             arts[top_art].tagged = 0;
  792             arts[top_art].thread = ART_EXPIRED;
  793             arts[top_art].prev = ART_NORMAL;
  794             arts[top_art].status = ART_UNREAD;
  795             arts[top_art].killed = ART_NOTKILLED;
  796             arts[top_art].selected = FALSE;
  797             continue;
  798         }
  799 
  800         top = arts[top_art].artnum; /* used if arts are killed */
  801         top_art++;
  802 
  803         if (++modified % MODULO_COUNT_NUM == 0)
  804             show_progress(group_msg, modified, total);
  805     }
  806 
  807     /*
  808      * Change back to previous dir before indexing started
  809      */
  810     if (!read_news_via_nntp || group->type != GROUP_TYPE_NEWS)
  811         chdir(dir);
  812 
  813     return modified;
  814 }
  815 
  816 
  817 /*
  818  * The algorithm is elegant, using the fact that identical Subject lines
  819  * are hashed to the same node in table[] (see hashstr.c)
  820  *
  821  * Mark i as being in j's thread list if
  822  * . The article is _not_ being ignored
  823  * . The article is not already threaded
  824  * . One of the following is true:
  825  *    1) The subject lines are the same
  826  *    2) Both are part of the same archive (name's match and arch bit set)
  827  * IMHO the tests for archive name are redundant and have been for years
  828  */
  829 static void
  830 thread_by_subject(
  831     void)
  832 {
  833     int i, j;
  834     struct t_hashnode *h;
  835 
  836     for_each_art(i) {
  837         if (IGNORE_ART_THREAD(i))
  838             continue;
  839 
  840         /*
  841          * Get the contents of the magic marker in the hashnode
  842          */
  843         h = (struct t_hashnode *) (arts[i].subject - sizeof(int) - sizeof(void *)); /* FIXME: cast increases required alignment of target type */
  844 
  845         j = h->aptr;
  846 
  847         if (j != -1 && j < i) {
  848 #if 1
  849             if (arts[i].prev == ART_NORMAL && (arts[i].subject == arts[j].subject))
  850 #else
  851             /* see also refs.c:collate_subjects() */
  852             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))))
  853 #endif /* 1 */
  854             {
  855                 arts[j].thread = i;
  856                 arts[i].prev = j;
  857             }
  858         }
  859 
  860         /*
  861          * Update the magic marker with the highest numbered mesg in
  862          * arts[] that has been used in this thread so far
  863          */
  864         h->aptr = i;
  865     }
  866 
  867 #if 0
  868     fprintf(stderr, "Subj dump\n");
  869     fprintf(stderr, "%3s %3s %3s %3s : %3s %3s\n", "#", "Par", "Sib", "Chd", "In", "Thd");
  870     for_each_art(i) {
  871         fprintf(stderr, "%3d %3d %3d %3d : %3d %3d : %.50s %s\n", i,
  872             (arts[i].refptr->parent) ? arts[i].refptr->parent->article : -2,
  873             (arts[i].refptr->sibling) ? arts[i].refptr->sibling->article : -2,
  874             (arts[i].refptr->child) ? arts[i].refptr->child->article : -2,
  875             arts[i].prev, arts[i].thread, arts[i].refptr->txt, arts[i].subject);
  876     }
  877 #endif /* 0 */
  878 }
  879 
  880 
  881 /*
  882  * This Threading algorithm threads articles into 'buckets' where each bucket
  883  * contains all the articles which match the root article's subject line to
  884  * the configured percentage. Eg, if the root article had the subject "asdf"
  885  * and the match percentage was configured to be 75% then any article would
  886  * match if its subject was no different in more than a single character.
  887  */
  888 static void
  889 thread_by_percentage(
  890     struct t_group *group)
  891 {
  892     int i, j, k;
  893     int root_num = 0; /* The index number of the root we are currently working on. */
  894     unsigned int unmatched; /* This is the number of characters that don't match between the two strings */
  895     unsigned int percentage = 100 - group->attribute->thread_perc;
  896     size_t slen;
  897 
  898     /* First we need to sort art[] to simplify and speed up the matching. */
  899     SortBy(subj_comp_asc);
  900 
  901     /*
  902      * Now we put all the articles which match enough into the thread. If
  903      * an article doesn't match enough we create a new thread and then add
  904      * to that and so on.
  905      */
  906     base[0] = 0;
  907     arts[0].prev = ART_NORMAL;
  908     for_each_art(i) {
  909         if (i == 0)
  910             continue;
  911 
  912         /* Check each character to see if it matched enough */
  913         k = 0;
  914         unmatched = 0;
  915         for (j = 0; arts[base[root_num]].subject[j] != '\0' && arts[i].subject[k] != '\0'; j++, k++) {
  916             if (arts[base[root_num]].subject[j] != arts[i].subject[k])
  917                 unmatched++;
  918         }
  919 
  920         /*
  921          * By getting here we have a number of unmatched characters
  922          * between the two strings. We also have the length of the
  923          * strings available to us easily.
  924          * All we need to do is see if the match is good enough, but
  925          * we count differences in the length of the strings against
  926          * them matching.
  927          */
  928         if (!(slen = strlen(arts[base[root_num]].subject)))
  929             slen++;
  930         unmatched += slen - strlen(arts[i].subject);
  931         if (unmatched * 100 / slen > percentage) {
  932             /*
  933              * If there is less greater than percentage% different start a
  934              * new thread.
  935              */
  936             base[++root_num] = i;
  937             arts[i].prev = ART_NORMAL;
  938             continue;
  939         } else {
  940             /*
  941              * The subject lines match enough to consider them part of a single
  942              * thread, so add the current article to the thread.
  943              */
  944             if (arts[base[root_num]].thread < 0)
  945                 arts[base[root_num]].thread = i;
  946             arts[i].prev = i - 1;
  947             arts[i - 1].thread = i;
  948             continue;
  949         }
  950     }
  951 }
  952 
  953 
  954 /*
  955  * This was brought over from tags.c, however this version doesn't not
  956  * opperate on base_index
  957  *
  958  * Parses a subject header of the type "multipart message subject (01/42)"
  959  * into a MultiPartInfo struct, or fails if the message subject isn't in the
  960  * right form.
  961  *
  962  * @return nonzero on success
  963  */
  964 int
  965 global_get_multipart_info(
  966     int aindex,
  967     MultiPartInfo *setme)
  968 {
  969     int i, j, offi, offj;
  970     MultiPartInfo setmei, setmej;
  971 
  972     i = global_look_for_multipart_info(aindex, &setmei, '[', ']', &offi);
  973     j = global_look_for_multipart_info(aindex, &setmej, '(', ')', &offj);
  974 
  975     /* Ok i hits first */
  976     if (offi > offj) {
  977         *setme = setmei;
  978         return i;
  979     }
  980 
  981     /* Its j or they are both the same (which must be zero!) so we don't care */
  982     *setme = setmej;
  983     return j;
  984 }
  985 
  986 
  987 /*
  988  * Again this was taken from tags.c, but works on global indices into arts
  989  * rather then on base_index.
  990  */
  991 static int
  992 global_look_for_multipart_info(
  993     int aindex,
  994     MultiPartInfo* setme,
  995     char start,
  996     char stop,
  997     int *offset)
  998 {
  999     char *subj;
 1000     char *pch;
 1001     MultiPartInfo tmp;
 1002 
 1003     *offset = 0;
 1004 
 1005     /* entry assertions */
 1006     assert(0 <= aindex && aindex < top_art && "invalid index");
 1007     assert(setme != NULL && "setme must not be NULL");
 1008 
 1009     /* parse the message */
 1010     subj = arts[aindex].subject;
 1011     pch = strrchr(subj, start);
 1012     if (!pch || !isdigit((int) pch[1]))
 1013         return 0;
 1014 
 1015     tmp.base_index = aindex; /* This could be confusing because we are actually storing the index into arts, not the base_index. */
 1016     tmp.subject_compare_len = pch - subj;
 1017     tmp.part_number = (int) strtol(pch + 1, &pch, 10);
 1018     if (*pch != '/' && *pch != '|')
 1019         return 0;
 1020 
 1021     if (!isdigit((int) pch[1]))
 1022         return 0;
 1023 
 1024     tmp.total = (int) strtol(pch + 1, &pch, 10);
 1025     if (*pch != stop)
 1026         return 0;
 1027 
 1028     tmp.subject = subj;
 1029     *setme = tmp;
 1030     *offset = pch - subj;
 1031     return 1;
 1032 }
 1033 
 1034 
 1035 /*
 1036  * Taken from tags.c but changed to use indices into arts[] instead of
 1037  * base_index. Changed so that even when we don't have all the parts we
 1038  * return a valid array.
 1039  *
 1040  * Tries to find all the parts to the multipart message pointed to by
 1041  * base_index.
 1042  *
 1043  * @return on success, the number of parts found. On failure, zero if not a
 1044  * multipart or the negative value of the first missing part.
 1045  * @param base_index index pointing to one of the messages in a multipart
 1046  * message.
 1047  * @param malloc_and_setme_info on success, set to a malloced array the
 1048  * parts found.
 1049  */
 1050 static int
 1051 global_get_multiparts(
 1052     int aindex,
 1053     MultiPartInfo **malloc_and_setme_info)
 1054 {
 1055     int i;
 1056     int part_index;
 1057     MultiPartInfo tmp, tmp2;
 1058     MultiPartInfo *info = NULL;
 1059 
 1060     /* entry assertions */
 1061     assert(0 <= aindex && aindex < top_art && "Invalid index");
 1062     assert(malloc_and_setme_info != NULL && "malloc_and_setme_info must not be NULL");
 1063 
 1064     /* make sure this is a multipart message... */
 1065     if (!global_get_multipart_info(aindex, &tmp) || tmp.total < 1)
 1066         return 0;
 1067 
 1068     /* make a temporary buffer to hold the multipart info... */
 1069     info = my_malloc(sizeof(MultiPartInfo) * tmp.total);
 1070 
 1071     /* zero out part-number for the repost check below */
 1072     for (i = 0; i < tmp.total; ++i) {
 1073         info[i].total = tmp.total; /* Added this for thread_by_multipart */
 1074         info[i].part_number = -1;
 1075     }
 1076 
 1077     /* try to find all the multiparts... */
 1078     for_each_art(i) {
 1079         if (strncmp(arts[i].subject, tmp.subject, tmp.subject_compare_len))
 1080             continue;
 1081 
 1082         if (!global_get_multipart_info(i, &tmp2))
 1083             continue;
 1084 
 1085         /* 'test (1/5)' is not the same as 'test (1/15)' */
 1086         if (tmp.total != tmp2.total)
 1087             continue;
 1088 
 1089         part_index = tmp2.part_number - 1;
 1090 
 1091         /*
 1092          * skip "blah (00/102)" or "blah (103/102)" subjects
 1093          */
 1094         if (part_index < 0 || part_index >= tmp.total)
 1095             continue;
 1096 
 1097         /* repost check: do we already have this part? */
 1098         if (info[part_index].part_number != -1) {
 1099             assert(info[part_index].part_number == tmp2.part_number && "bookkeeping error");
 1100             continue;
 1101         }
 1102 
 1103         /* we have a match, hooray! */
 1104         info[part_index] = tmp2;
 1105     }
 1106 
 1107     /* see if we got them all. */
 1108     for (i = 0; i < tmp.total; ++i) {
 1109         if (info[i].part_number != i + 1) {
 1110             *malloc_and_setme_info = info;
 1111             return -(i + 1); /* missing part #(i+1) */
 1112         }
 1113     }
 1114 
 1115     /* looks like a success .. */
 1116     *malloc_and_setme_info = info;
 1117     return tmp.total;
 1118 }
 1119 
 1120 
 1121 /*
 1122  *  The algorithm uses the tag multipart searches to thread articles together.
 1123  */
 1124 static void
 1125 thread_by_multipart(
 1126     void)
 1127 {
 1128     int i, j, threadNum;
 1129     MultiPartInfo *minfo = NULL;
 1130 
 1131     for_each_art(i) {
 1132         if (IGNORE_ART_THREAD(i) || arts[i].prev >= 0 || !global_get_multiparts(i, &minfo))
 1133             continue;
 1134 
 1135         threadNum = -1;
 1136         for (j = minfo[0].total - 1; j >= 0; j--) {
 1137             if (minfo[j].part_number != -1) {
 1138                 if (threadNum != -1) {
 1139                     arts[minfo[j].base_index].thread = threadNum;
 1140                     arts[threadNum].prev = minfo[j].base_index;
 1141                 }
 1142 
 1143                 threadNum = minfo[j].base_index;
 1144             }
 1145         }
 1146     }
 1147     free(minfo);
 1148 }
 1149 
 1150 
 1151 /*
 1152  * Go through the articles in arts[] and create threads. There are
 1153  * 5 strategies currently defined :
 1154  *
 1155  *  THREAD_NONE     No threading
 1156  *  THREAD_SUBJ     Threads are created using like Subject lines
 1157  *  THREAD_REFS     Threads are created using the References headers
 1158  *  THREAD_BOTH     Threads created using References and then Subject
 1159  *  THREAD_MULTI    Threads created using Subject to search for Multiparts
 1160  *  THREAD_PERC     Threads based upon a char for char match of greater than x%
 1161  *
 1162  * .thread and .prev are used to hold the threading information, see tin.h for
 1163  * more information
 1164  * Only process valid (unexpired) articles we haven't visited yet
 1165  * (ie arts[].thread == ART_UNTHREADED)
 1166  *
 1167  * The rethread parameter is used to force the deletion of existing threading
 1168  * information before threading which happens anyway expect when using
 1169  * THREAD_NONE (I don't immediately see how this is useful)
 1170  */
 1171 /* TODO: rewrite that user can easly combine different 'threading'
 1172  *       methods, i.e:
 1173  *       - thread_by_multipart() + collate_subjects()
 1174  */
 1175 void
 1176 make_threads(
 1177     struct t_group *group,
 1178     t_bool rethread)
 1179 {
 1180     if (!cmd_line && !batch_mode)
 1181         info_message((group->attribute->thread_articles == THREAD_NONE ? _(txt_unthreading_arts) : _(txt_threading_arts)));
 1182 
 1183 #ifdef DEBUG
 1184     if (debug & DEBUG_MISC)
 1185         error_message(2, "rethread=[%d]  thread_articles=[%d]  attr_thread_articles=[%d]",
 1186                 rethread, tinrc.thread_articles, group->attribute->thread_articles);
 1187 #endif /* DEBUG */
 1188 
 1189     /*
 1190      * Sort all the articles using the preferred method
 1191      * When find_base() is called, the bases are created ordered
 1192      * on arts[] and so the base messages under all threading systems
 1193      * will be sorted in this way.
 1194      */
 1195     sort_arts(group->attribute->sort_article_type);
 1196 
 1197     /*
 1198      * Reset all the ptrs to articles following the above sort
 1199      */
 1200     clear_art_ptrs();
 1201 
 1202     /*
 1203      * The threading pointers need to be reset if re-threading
 1204      * If using ref threading, revector the links back to the articles
 1205      */
 1206     if (rethread || group->attribute->thread_articles) {
 1207         int i;
 1208 
 1209         for_each_art(i) {
 1210             if (arts[i].thread >= 0)
 1211                 arts[i].thread = ART_UNTHREADED;
 1212 
 1213             arts[i].prev = ART_NORMAL;
 1214 
 1215             /* Should never happen if tree is built properly */
 1216             if (arts[i].refptr == NULL) {
 1217 #ifdef DEBUG
 1218                 if (debug & DEBUG_REFS) {
 1219                     my_fprintf(stderr, "\nError  : art->refptr is NULL\n");
 1220                     my_fprintf(stderr, "Artnum : %"T_ARTNUM_PFMT"\n", arts[i].artnum);
 1221                     my_fprintf(stderr, "Subject: %s\n", arts[i].subject);
 1222                     my_fprintf(stderr, "From   : %s\n", arts[i].from);
 1223                     assert(arts[i].refptr != NULL);
 1224                 } else
 1225 #endif /* DEBUG */
 1226                     continue;
 1227             }
 1228             arts[i].refptr->article = i;
 1229         }
 1230     }
 1231 
 1232     /*
 1233      * Do the right thing according to the threading strategy
 1234      */
 1235     switch (group->attribute->thread_articles) {
 1236         case THREAD_NONE:
 1237             break;
 1238 
 1239         case THREAD_SUBJ:
 1240             thread_by_subject();
 1241             break;
 1242 
 1243         case THREAD_REFS:
 1244             thread_by_reference();
 1245             break;
 1246 
 1247         case THREAD_BOTH:
 1248             thread_by_reference();
 1249             collate_subjects();
 1250             break;
 1251 
 1252         case THREAD_MULTI:
 1253             thread_by_multipart();
 1254             break;
 1255 
 1256         case THREAD_PERC:
 1257             thread_by_percentage(group);
 1258             break;
 1259 
 1260         default: /* not reached */
 1261             break;
 1262     }
 1263 
 1264     /*
 1265      * Rebuild base[]
 1266      */
 1267     find_base(group);
 1268 }
 1269 
 1270 
 1271 static t_compfunc
 1272 eval_sort_arts_func(
 1273     unsigned int sort_art_type)
 1274 {
 1275     switch (sort_art_type) {
 1276         case SORT_ARTICLES_BY_NOTHING:      /* don't sort at all */
 1277             return artnum_comp;
 1278 
 1279         case SORT_ARTICLES_BY_SUBJ_DESCEND:
 1280             return subj_comp_desc;
 1281 
 1282         case SORT_ARTICLES_BY_SUBJ_ASCEND:
 1283             return subj_comp_asc;
 1284 
 1285         case SORT_ARTICLES_BY_FROM_DESCEND:
 1286             return from_comp_desc;
 1287 
 1288         case SORT_ARTICLES_BY_FROM_ASCEND:
 1289             return from_comp_asc;
 1290 
 1291         case SORT_ARTICLES_BY_DATE_DESCEND:
 1292             return date_comp_desc;
 1293 
 1294         case SORT_ARTICLES_BY_DATE_ASCEND:
 1295             return date_comp_asc;
 1296 
 1297         case SORT_ARTICLES_BY_SCORE_DESCEND:
 1298             return score_comp_desc;
 1299 
 1300         case SORT_ARTICLES_BY_SCORE_ASCEND:
 1301             return score_comp_asc;
 1302 
 1303         case SORT_ARTICLES_BY_LINES_DESCEND:
 1304             return lines_comp_desc;
 1305 
 1306         case SORT_ARTICLES_BY_LINES_ASCEND:
 1307             return lines_comp_asc;
 1308 
 1309         default:
 1310             break;
 1311     }
 1312     return NULL;
 1313 }
 1314 
 1315 
 1316 void
 1317 sort_arts(
 1318     unsigned int sort_art_type)
 1319 {
 1320     t_compfunc comp_func = eval_sort_arts_func(sort_art_type);
 1321 
 1322     if (comp_func)
 1323         SortBy(comp_func);
 1324 }
 1325 
 1326 
 1327 static void
 1328 sort_base(
 1329     unsigned int sort_threads_type)
 1330 {
 1331     switch (sort_threads_type) {
 1332         case SORT_THREADS_BY_SCORE_DESCEND:
 1333         case SORT_THREADS_BY_SCORE_ASCEND:
 1334             tin_sort(base, (size_t) grpmenu.max, sizeof(t_artnum), score_comp_base);
 1335             break;
 1336 
 1337         case SORT_THREADS_BY_LAST_POSTING_DATE_DESCEND:
 1338             tin_sort(base, (size_t) grpmenu.max, sizeof(t_artnum), last_date_comp_base_desc);
 1339             break;
 1340 
 1341         case SORT_THREADS_BY_LAST_POSTING_DATE_ASCEND:
 1342             tin_sort(base, (size_t) grpmenu.max, sizeof(t_artnum), last_date_comp_base_asc);
 1343             break;
 1344     }
 1345 }
 1346 
 1347 
 1348 /*
 1349  * This is called to get header info for articles not already found in the
 1350  * overview files.
 1351  * Code reads (max_lineno) lines of article to catch headers like Archive-name:
 1352  * which are not normally included in XOVER or even the normal block of headers.
 1353  * How this is supposed to be useful when 99% of the time we'll have overview
 1354  * data I don't know...
 1355  * TODO: move Archive-name: parsing to article body parsing, remove the
 1356  * TODO: max_lineno nonsense and parse just the hdrs. Only parse if
 1357  * TODO: currgrp->auto_save is set, otherwise it is redundant info
 1358  */
 1359 static t_bool
 1360 parse_headers(
 1361     FILE *fp,
 1362     struct t_article *h)
 1363 {
 1364     char art_from_addr[HEADER_LEN];
 1365     char art_full_name[HEADER_LEN];
 1366     char *hdr, *ptr;
 1367     unsigned int lineno = 0;
 1368     unsigned int max_lineno = 25;
 1369     t_bool got_from, got_lines, got_received;
 1370 
 1371     got_from = got_lines = got_received = FALSE;
 1372 
 1373     while ((ptr = tin_fgets(fp, TRUE)) != NULL) {
 1374         /*
 1375          * Look for the end of information which tin wants to get.
 1376          * Applies when reading local spool and via NNTP.
 1377          */
 1378 
 1379         /*
 1380          * as Archive-name: is placed in the body it's safe to exit
 1381          * the loop if it was found.
 1382          */
 1383         if (lineno++ > max_lineno || h->archive)
 1384             break;
 1385 
 1386         unfold_header(ptr);
 1387         switch (toupper((unsigned char) *ptr)) {
 1388             case 'A':   /* Archive-name:  optional */
 1389                 /*
 1390                  * Archive-name: {name}/{part|patch}{number}
 1391                  * eg, acorn/faq/part01
 1392                  */
 1393                 if ((hdr = parse_header(ptr + 1, "rchive-name", FALSE, FALSE, FALSE))) {
 1394                     char *s;
 1395 
 1396                     if ((s = strrchr(hdr, '/')) != NULL) {
 1397                         struct t_archive *archptr = my_malloc(sizeof(struct t_archive));
 1398 
 1399                         if (STRNCASECMPEQ(s + 1, "part", 4)) {
 1400                             archptr->partnum = my_strdup(s + 5);
 1401                             archptr->ispart = TRUE;
 1402                         } else if (STRNCASECMPEQ(s + 1, "patch", 5)) {
 1403                             archptr->partnum = my_strdup(s + 6);
 1404                             archptr->ispart = FALSE;
 1405                         } else {        /* part or patch must be present */
 1406                             free(archptr);
 1407                             continue;
 1408                         }
 1409                         strtok(archptr->partnum, "\n");
 1410                         *s = '\0';
 1411                         archptr->name = hash_str(hdr);
 1412                         h->archive = archptr;
 1413                     }
 1414                 }
 1415                 break;
 1416 
 1417             case 'D':   /* Date:  mandatory */
 1418                 if (!h->date) {
 1419                     if ((hdr = parse_header(ptr + 1, "ate", FALSE, FALSE, FALSE)))
 1420                         h->date = parsedate(hdr, (struct _TIMEINFO *) 0);
 1421                 }
 1422                 break;
 1423 
 1424             case 'F':   /* From:  mandatory */
 1425                 if (!got_from) {
 1426                     if ((hdr = parse_header(ptr + 1, "rom", FALSE, FALSE, FALSE))) {
 1427                         h->gnksa_code = parse_from(hdr, art_from_addr, art_full_name);
 1428                         h->from = hash_str(buffer_to_ascii(art_from_addr));
 1429                         if (*art_full_name)
 1430                             h->name = hash_str(eat_tab(convert_to_printable(rfc1522_decode(art_full_name), FALSE)));
 1431                         got_from = TRUE;
 1432                     }
 1433                 }
 1434                 break;
 1435 
 1436             case 'L':   /* Lines:  optional */
 1437                 if (!got_lines) {
 1438                     if ((hdr = parse_header(ptr + 1, "ines", FALSE, FALSE, FALSE))) {
 1439                         h->line_count = atoi(hdr);
 1440                         got_lines = TRUE;
 1441                     }
 1442                 }
 1443                 break;
 1444 
 1445             case 'M':   /* Message-ID:  mandatory */
 1446                 if (!h->msgid) {
 1447                     if ((hdr = parse_header(ptr + 1, "essage-ID", FALSE, FALSE, FALSE)))
 1448                         h->msgid = my_strdup(hdr);
 1449                 }
 1450                 break;
 1451 
 1452             case 'R':   /* References:  optional */
 1453                 if (!h->refs) {
 1454                     if ((hdr = parse_header(ptr + 1, "eferences", FALSE, FALSE, FALSE)))
 1455                         h->refs = my_strdup(hdr);
 1456                 }
 1457 
 1458                 /* Received:  If found it's probably a mail article */
 1459                 if (!got_received) {
 1460                     if (parse_header(ptr + 1, "eceived", FALSE, FALSE, FALSE)) {
 1461                         max_lineno <<= 1;       /* double the max number of line to read for mails */
 1462                         got_received = TRUE;
 1463                     }
 1464                 }
 1465                 break;
 1466 
 1467             case 'S':   /* Subject:  mandatory */
 1468                 if (!h->subject) {
 1469                     if ((hdr = parse_header(ptr + 1, "ubject", FALSE, FALSE, FALSE)))
 1470                         h->subject = hash_str(eat_re(eat_tab(convert_to_printable(rfc1522_decode(hdr), FALSE)), FALSE));
 1471                 }
 1472                 break;
 1473 
 1474             case 'X':   /* Xref:  optional */
 1475                 if (!h->xref) {
 1476                     if ((hdr = parse_header(ptr + 1, "ref", FALSE, FALSE, FALSE)))
 1477                         h->xref = my_strdup(hdr);
 1478                 }
 1479                 break;
 1480 
 1481             default:
 1482                 break;
 1483         } /* switch */
 1484 
 1485     } /* while */
 1486 
 1487 #ifdef NNTP_ABLE
 1488     if (ptr)
 1489         drain_buffer(fp);
 1490 #endif /* NNTP_ABLE */
 1491 
 1492     if (tin_errno)
 1493         return FALSE;
 1494 
 1495     /*
 1496      * The son of RFC 1036 states that the following hdrs are mandatory. It
 1497      * also states that Subject, Newsgroups and Path are too. Ho hum.
 1498      *
 1499      * What about readinng mail from local spool via ~/.tin/active.mail,
 1500      * they might not have a Message-ID but got_received is very likely to
 1501      * be true.
 1502      */
 1503     if (got_from && h->date && h->msgid) {
 1504         if (!h->subject)
 1505             h->subject = hash_str("<No subject>");
 1506 
 1507 #ifdef DEBUG
 1508         debug_print_header(h);
 1509 #endif /* DEBUG */
 1510         return TRUE;
 1511     }
 1512 
 1513     return FALSE;
 1514 }
 1515 
 1516 
 1517 /*
 1518  * Read in an overview index file. Fields are separated by TAB.
 1519  * return the number of expired articles encountered or -1 if the user aborted
 1520  * the read
 1521  * 'top' is set to the highest artnum read
 1522  * If 'local' is set then always open local overview cache in preference to
 1523  * using NNTP XOVER
 1524  *
 1525  * Format (mandatory as far as line count [RFC2980]):
 1526  *  1. article number (ie. 183)                [mandatory]
 1527  *  2. Subject: line  (ie. Which newsreader?)  [mandatory]
 1528  *  3. From: line     (ie. iain@ecrc.de)       [mandatory]
 1529  *  4. Date: line     (rfc822 format)          [mandatory]
 1530  *  5. MessageID:     (ie. <123@ether.net>)    [mandatory]
 1531  *  6. References:    (ie. <message-id> ....)  [optional]
 1532  *  7. Byte count     (Skipped - not used)     [mandatory]
 1533  *  8. Line count     (ie. 23)                 [mandatory]
 1534  *  9. Xref: line     (ie. alt.test:389)       [optional]
 1535  */
 1536 static int
 1537 read_overview(
 1538     struct t_group *group,
 1539     t_artnum min,
 1540     t_artnum max,
 1541     t_artnum *top,
 1542     t_bool local)
 1543 {
 1544     FILE *fp;
 1545     char *ptr;
 1546     char *q;
 1547     char *buf;
 1548     char *group_msg;
 1549     char art_full_name[HEADER_LEN];
 1550     char art_from_addr[HEADER_LEN];
 1551     unsigned int count;
 1552     int expired = 0;
 1553     t_artnum artnum;
 1554     struct t_article *art;
 1555     size_t over_fields = 1;
 1556 
 1557     /*
 1558      * open the overview file (whether it be local or via nntp)
 1559      */
 1560     if ((fp = open_xover_fp(group, "r", min, max, local)) == NULL)
 1561         return expired;
 1562 
 1563     if (group->xmax > max)
 1564         group->xmax = max;
 1565 
 1566     group_msg = fmt_string(_(txt_group), cCOLS - strlen(_(txt_group)) + 2 - 3, group->name);
 1567 
 1568     /* get the number of fields per over-record as announced by LIST OVERVIEW.FMT */
 1569     if (ofmt) {
 1570         for (; ofmt[over_fields].name; over_fields++)
 1571             ;
 1572     }
 1573     if (!--over_fields) { /* e.g. nntp_caps.type == CAPABILITIES && !nntp_caps.list_overview_fmt -> assume defaults */
 1574         ofmt = my_realloc(ofmt, sizeof(struct t_overview_fmt) * (8 + 1));
 1575         ofmt[0].type = OVER_T_INT;
 1576         ofmt[0].name = my_strdup("Artnum:");
 1577         ofmt[1].type = OVER_T_STRING;
 1578         ofmt[1].name = my_strdup("Subject:");
 1579         ofmt[2].type = OVER_T_STRING;
 1580         ofmt[2].name = my_strdup("From:");
 1581         ofmt[3].type = OVER_T_STRING;
 1582         ofmt[3].name = my_strdup("Date:");
 1583         ofmt[4].type = OVER_T_STRING;
 1584         ofmt[4].name = my_strdup("Message-ID:");
 1585         ofmt[5].type = OVER_T_STRING;
 1586         ofmt[5].name = my_strdup("References:");
 1587         ofmt[6].type = OVER_T_INT;
 1588         ofmt[6].name = my_strdup("Bytes:");
 1589         ofmt[7].type = OVER_T_INT;
 1590         ofmt[7].name = my_strdup("Lines:");
 1591         ofmt[8].type = OVER_T_ERROR;
 1592         ofmt[8].name = NULL;
 1593         over_fields = 7;
 1594     }
 1595 
 1596     while ((buf = tin_fgets(fp, FALSE)) != NULL) {
 1597         if (need_resize) {
 1598             handle_resize((need_resize == cRedraw) ? TRUE : FALSE);
 1599             need_resize = cNo;
 1600         }
 1601 
 1602         /*
 1603          * Read artnum
 1604          */
 1605         if ((ptr = tin_strtok(buf, "\t")) == NULL)
 1606             continue;
 1607 
 1608         /*
 1609          * read the article number, guaranteed to be the first field
 1610          */
 1611         artnum = atoartnum(ptr);
 1612 
 1613         /*
 1614          * artnum field invalid/corrupt or is 1st line of local cached overview
 1615          * (group name)
 1616          */
 1617         if (artnum <= 0)
 1618             continue;
 1619 
 1620         /*
 1621          * skip artnums below the given minimum (getart_limit)
 1622          */
 1623         if (artnum < min)
 1624             continue;
 1625 
 1626         /*
 1627          * Check to make sure article in nov file has not expired in group
 1628          */
 1629         if (artnum < group->xmin) {
 1630             expired++;
 1631             continue;
 1632         }
 1633 
 1634         /*
 1635          * artnum in overview data higher than groups high mark
 1636          *
 1637          * TODO: - warn user about broken overviews?
 1638          *       - try to parse the Xref:-line to get the correct artnum
 1639          *       - see also parse_unread_arts()
 1640          */
 1641         if (artnum > group->xmax)
 1642             continue;
 1643 
 1644         if (top_art >= max_art)
 1645             expand_art();
 1646 
 1647         art = &arts[top_art];
 1648         set_article(art);
 1649         art->artnum = *top = artnum;
 1650 
 1651         /*
 1652          * Note: Fields after line count are not mandatory, use "LIST OVERVIEW.FMT"
 1653          *       to check for additions like we do with xref_supported
 1654          */
 1655         for (count = 1; (ptr = tin_strtok(NULL, "\t")) != NULL; count++) {
 1656             /* skip unexpected tailing fields */
 1657             if (count > over_fields) {
 1658 #ifdef DEBUG
 1659                 if (debug & DEBUG_NNTP)
 1660                     debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") Unexpected overview-field %d of %d: %s", nntp_caps.over_cmd, artnum, count, over_fields, ptr);
 1661 #endif /* DEBUG */
 1662 
 1663                 /* "common error" Xref:full in overview-data but not in OVERVIEW.FMT */
 1664                 if (count == over_fields + 1) {
 1665                     if (!strncasecmp(ptr, "Xref: ", 6)) {
 1666 #ifdef DEBUG
 1667                         if (debug & DEBUG_NNTP)
 1668                             debug_print_file("NNTP", "%s: found unexpected Xref: on semi std. position", nntp_caps.over_cmd);
 1669 #endif /* DEBUG */
 1670                         over_fields++;
 1671                         ofmt = my_realloc(ofmt, sizeof(struct t_overview_fmt) * (over_fields + 2)); /* + 2 = artnum and end-marker */
 1672                         ofmt[over_fields].type = OVER_T_FSTRING;
 1673                         ofmt[over_fields].name = my_strdup("Xref:");
 1674                         ofmt[over_fields + 1].type = OVER_T_ERROR;
 1675                         ofmt[over_fields + 1].name = NULL;
 1676                         xref_supported = TRUE;
 1677                     } else
 1678                         continue;
 1679                 } else
 1680                     continue;
 1681             }
 1682 
 1683             /* for duplicated headers this is last match counts, INN >= 2.5.3 does first match counts */
 1684             if (expensive_over_parse) { /* strange order */
 1685                 /* madatory fields */
 1686                 if (ofmt[count].type == OVER_T_STRING) {
 1687                     if (!strcasecmp(ofmt[count].name, "Subject:")) {
 1688                         if (*ptr)
 1689                             art->subject = hash_str(eat_re(eat_tab(convert_to_printable(rfc1522_decode(ptr), FALSE)), FALSE));
 1690                         else {
 1691                             art->subject = hash_str("");
 1692 #ifdef DEBUG
 1693                             if (debug & DEBUG_NNTP)
 1694                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") empty overview-field %s", nntp_caps.over_cmd, artnum, ofmt[count].name);
 1695 #endif /* DEBUG */
 1696                         }
 1697                         continue;
 1698                     }
 1699 
 1700                     if (!strcasecmp(ofmt[count].name, "From:")) {
 1701                         if (*ptr) {
 1702                             art->gnksa_code = parse_from(ptr, art_from_addr, art_full_name);
 1703                             art->from = hash_str(buffer_to_ascii(art_from_addr));
 1704                             if (*art_full_name)
 1705                                 art->name = hash_str(eat_tab(convert_to_printable(rfc1522_decode(art_full_name), FALSE)));
 1706                         } else {
 1707                             art->from = hash_str("");
 1708 #ifdef DEBUG
 1709                             if (debug & DEBUG_NNTP)
 1710                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") empty overview-field %s", nntp_caps.over_cmd, artnum, ofmt[count].name);
 1711 #endif /* DEBUG */
 1712                         }
 1713                         continue;
 1714                     }
 1715 
 1716                     if (!strcasecmp(ofmt[count].name, "Date:")) {
 1717                         art->date = parsedate(ptr, (TIMEINFO *) 0);
 1718 #ifdef DEBUG
 1719                         if ((debug & DEBUG_NNTP) && art->date == (time_t) -1)
 1720                             debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") bogus overview-field %s %s", nntp_caps.over_cmd, artnum, ofmt[count].name, ptr);
 1721 #endif /* DEBUG */
 1722                         continue;
 1723                     }
 1724 
 1725                     if (!strcasecmp(ofmt[count].name, "Message-ID:")) {
 1726                         if (*ptr) {
 1727                             FreeIfNeeded(art->msgid); /* if field is listed more than once in overview.fmt */
 1728                             art->msgid = my_strdup(ptr);
 1729                         } else {
 1730                             art->msgid = NULL;
 1731 #ifdef DEBUG
 1732                             if (debug & DEBUG_NNTP)
 1733                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") empty overview-field %s", nntp_caps.over_cmd, artnum, ofmt[count].name);
 1734 #endif /* DEBUG */
 1735                         }
 1736                         continue;
 1737                     }
 1738 
 1739                     if (!strcasecmp(ofmt[count].name, "References:")) {
 1740                         if (*ptr) {
 1741                             FreeIfNeeded(art->refs); /* if field is listed more than once in overview.fmt */
 1742                             art->refs = my_strdup(ptr);
 1743                         } else
 1744                             art->refs = NULL;
 1745                         continue;
 1746                     }
 1747                 }
 1748                 /* metadata fields */
 1749                 if (ofmt[count].type == OVER_T_INT) {
 1750                     if (!strcasecmp(ofmt[count].name, "Bytes:")) {
 1751                         if (*ptr) {
 1752 #ifdef DEBUG
 1753                             if ((debug & DEBUG_NNTP) && !isdigit((unsigned char) *ptr))
 1754                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") overview field %d (%s) mismatch: %s", nntp_caps.over_cmd, artnum, count, ofmt[count].name, ptr);
 1755 #endif /* DEBUG */
 1756                         }
 1757                         continue;
 1758                     }
 1759 
 1760                     if (!strcasecmp(ofmt[count].name, "Lines:")) {
 1761                         if (*ptr) {
 1762                             if (isdigit((unsigned char) *ptr))
 1763                                 art->line_count = atoi(ptr);
 1764                             else {
 1765                                 art->line_count = 0;
 1766 #ifdef DEBUG
 1767                                 if (debug & DEBUG_NNTP)
 1768                                     debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") overview field %d (%s) mismatch: %s", nntp_caps.over_cmd, artnum, count, ofmt[count].name, ptr);
 1769 #endif /* DEBUG */
 1770                             }
 1771                         } else
 1772                             art->line_count = 0;
 1773                         continue;
 1774                     }
 1775                 }
 1776             } else { /* first 7 fields are in RFC 3977 order */
 1777                 switch (count) {
 1778                     case 1: /* Subject: */
 1779                         if (*ptr)
 1780                             art->subject = hash_str(eat_re(eat_tab(convert_to_printable(rfc1522_decode(ptr), FALSE)), FALSE));
 1781                         else {
 1782                             art->subject = hash_str("");
 1783 #ifdef DEBUG
 1784                             if (debug & DEBUG_NNTP)
 1785                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") empty overview-field %s", nntp_caps.over_cmd, artnum, ofmt[count].name);
 1786 #endif /* DEBUG */
 1787                         }
 1788                         break;
 1789 
 1790                     case 2: /* From: */
 1791                         if (*ptr) {
 1792                             art->gnksa_code = parse_from(ptr, art_from_addr, art_full_name);
 1793                             art->from = hash_str(buffer_to_ascii(art_from_addr));
 1794                             if (*art_full_name)
 1795                                 art->name = hash_str(eat_tab(convert_to_printable(rfc1522_decode(art_full_name), FALSE)));
 1796                         } else {
 1797                             art->from = hash_str("");
 1798 #ifdef DEBUG
 1799                             if (debug & DEBUG_NNTP)
 1800                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") empty overview-field %s", nntp_caps.over_cmd, artnum, ofmt[count].name);
 1801 #endif /* DEBUG */
 1802                         }
 1803                         break;
 1804 
 1805                     case 3: /* Date: */
 1806                         art->date = parsedate(ptr, (TIMEINFO *) 0);
 1807 #ifdef DEBUG
 1808                         if ((debug & DEBUG_NNTP) && art->date == (time_t) -1)
 1809                             debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") bogus overview-field %s %s", nntp_caps.over_cmd, artnum, ofmt[count].name, ptr);
 1810 #endif /* DEBUG */
 1811                         break;
 1812 
 1813                     case 4: /* Message-ID: */
 1814                         if (*ptr)
 1815                             art->msgid = my_strdup(ptr);
 1816                         else {
 1817                             art->msgid = NULL;
 1818 #ifdef DEBUG
 1819                             if (debug & DEBUG_NNTP)
 1820                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") empty overview-field %s", nntp_caps.over_cmd, artnum, ofmt[count].name);
 1821 #endif /* DEBUG */
 1822                         }
 1823                         break;
 1824 
 1825                     case 5: /* References: */
 1826                         if (*ptr)
 1827                             art->refs = my_strdup(ptr);
 1828                         else
 1829                             art->refs = NULL;
 1830                         break;
 1831 
 1832                     case 6: /* :bytes || Bytes: */
 1833                         if (*ptr) {
 1834 #ifdef DEBUG
 1835                             if ((debug & DEBUG_NNTP) && !isdigit((unsigned char) *ptr))
 1836                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") overview field %d (%s) mismatch: %s", nntp_caps.over_cmd, artnum, count, ofmt[count].name, ptr);
 1837 #endif /* DEBUG */
 1838                         }
 1839                         break;
 1840 
 1841                     case 7: /* :lines || Lines: */
 1842                         if (*ptr) {
 1843                             if (isdigit((unsigned char) *ptr))
 1844                                 art->line_count = atoi(ptr);
 1845                             else {
 1846                                 art->line_count = 0;
 1847 #ifdef DEBUG
 1848                                 if (debug & DEBUG_NNTP)
 1849                                     debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") overview field %d (%s) mismatch: %s", nntp_caps.over_cmd, artnum, count, ofmt[count].name, ptr);
 1850 #endif /* DEBUG */
 1851                             }
 1852                         } else
 1853                             art->line_count = 0;
 1854                         break;
 1855 
 1856                     default:
 1857                         break;
 1858                 }
 1859             }
 1860 
 1861             /* optional fields; for duplicated headers: last match counts, INN >= 2.5.3 does first match counts */
 1862             if (ofmt[count].type == OVER_T_FSTRING) {
 1863                 if (*ptr) {
 1864                     if (!strcasecmp(ofmt[count].name, "Xref:")) {
 1865                         if ((q = parse_header(ptr, "Xref", FALSE, FALSE, FALSE)) != NULL) {
 1866                             FreeIfNeeded(art->xref); /* if field is listed more than once in overview.fmt */
 1867                             art->xref = my_strdup(q);
 1868                         }
 1869 #ifdef DEBUG
 1870                         else {
 1871                             if (debug & DEBUG_NNTP)
 1872                                 debug_print_file("NNTP", "%s(%"T_ARTNUM_PFMT") bogus overview-field %s %s", nntp_caps.over_cmd, artnum, ofmt[count].name, ptr);
 1873                         }
 1874 #endif /* DEBUG */
 1875                         continue;
 1876                     }
 1877 #if 0 /* code example for hadnling addition overview fields */
 1878                     if (!strcasecmp(ofmt[count].name, "Path:")) {
 1879 #ifdef DEBUG
 1880                         if (debug & DEBUG_NNTP)
 1881                             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);
 1882 #endif /* DEBUG */
 1883                         continue;
 1884                     }
 1885 #endif /* 0 */
 1886                 }
 1887                 continue;
 1888             }
 1889         }
 1890 
 1891         /*
 1892          * RFC says Message-ID is mandatory in newsgroups (but not in
 1893          * mailgroups etc..) NB. a NULL Message-ID would abort if we ever do
 1894          * threading in mailgroups
 1895          */
 1896         if (!art->msgid && group->type == GROUP_TYPE_NEWS)
 1897             continue;
 1898 
 1899         /* we might loose accuracy here, but that shouldn't hurt */
 1900         if (artnum % MODULO_COUNT_NUM == 0)
 1901             show_progress(group_msg, artnum - min, max - min);
 1902 
 1903         top_art++;              /* Basically this statement commits the article */
 1904     }
 1905 
 1906     free(group_msg);
 1907     TIN_FCLOSE(fp);
 1908 
 1909     if (tin_errno)
 1910         return -1;
 1911 
 1912 #if defined(NNTP_ABLE) && defined(XHDR_XREF)
 1913     if (read_news_via_nntp && !read_saved_news && !xref_supported && nntp_caps.hdr_cmd) {
 1914         char cbuf[HEADER_LEN];
 1915         static t_bool found;
 1916         static t_bool first = TRUE;
 1917 
 1918         if (first) {
 1919             found = TRUE;
 1920             /*
 1921              * TODO: do once a start and cache full result
 1922              *       if "LIST HEADERS RANGE" failed try "LIST HEADERS"?
 1923              */
 1924             if (nntp_caps.type == CAPABILITIES && nntp_caps.list_headers) {
 1925                 int i = new_nntp_command("LIST HEADERS RANGE", 215, cbuf, sizeof(cbuf));
 1926 
 1927                 found = FALSE;
 1928                 switch (i) {
 1929                     case 215:
 1930                         while ((ptr = tin_fgets(FAKE_NNTP_FP, FALSE)) != NULL) {
 1931 #   ifdef DEBUG
 1932                             if (debug & DEBUG_NNTP)
 1933                                 debug_print_file("NNTP", "<<<%s%s", logtime(), ptr);
 1934 #   endif /* DEBUG */
 1935                             if (!found && ((*ptr == ':' && *(ptr + 1) == '\0') || !strncasecmp(ptr, "Xref", 4)))
 1936                                 found = TRUE;
 1937                         }
 1938                         break;
 1939 
 1940                     default:
 1941                         break;
 1942                 }
 1943                 first = FALSE;
 1944             }
 1945         }
 1946 
 1947         if (found) {
 1948             snprintf(cbuf, sizeof(cbuf), "%s XREF %"T_ARTNUM_PFMT"-%"T_ARTNUM_PFMT, nntp_caps.hdr_cmd, min, MAX(min, max));
 1949             group_msg = fmt_string("%s XREF loop", nntp_caps.hdr_cmd); /* TODO: find a better message, move to lang.c */
 1950             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 */
 1951                 while ((ptr = tin_fgets(fp, FALSE)) != NULL) {
 1952                     artnum = atoartnum(ptr);
 1953                     if (artnum <= 0 || artnum < group->xmin || artnum > group->xmax)
 1954                         continue;
 1955                     art = &arts[top_art];
 1956                     set_article(art);
 1957                     if (!art->xref && !strstr(ptr, "(none)")) {
 1958                         if ((q = strchr(ptr, ' ')) == NULL) /* skip article number */
 1959                             continue;
 1960                         ptr = q;
 1961                         while (*ptr && isspace((int) *ptr))
 1962                             ptr++;
 1963                         q = strchr(ptr, '\n');
 1964                         if (q)
 1965                             *q = '\0';
 1966                         art->xref = my_strdup(ptr);
 1967                     }
 1968                     /* we might loose accuracy here, but that shouldn't hurt */
 1969                     if (artnum % MODULO_COUNT_NUM == 0)
 1970                         show_progress(group_msg, artnum - min, max - min);
 1971                 }
 1972             }
 1973             free(group_msg);
 1974         }
 1975     }
 1976 #endif /* NNTP_ABLE && XHDR_XREF */
 1977 
 1978     return expired;
 1979 }
 1980 
 1981 
 1982 /*
 1983  * Write an Nov/Xover index file. Fields are separated by '\t'.
 1984  *
 1985  * Format:
 1986  *  1. article number (ie. 183)                [mandatory]
 1987  *  2. Subject: line  (ie. Which newsreader?)  [mandatory]
 1988  *  3. From: line     (ie. iain@ecrc.de)       [mandatory]
 1989  *  4. Date: line     (rfc822 format)          [mandatory]
 1990  *  5. MessageID:     (ie. <123@ether.net>)    [mandatory]
 1991  *  6. References:    (ie. <message-id> ....)  [optional]
 1992  *  7. Byte count     (Skipped - not used)     [mandatory]
 1993  *  8. Line count     (ie. 23)                 [mandatory]
 1994  *  9. Xref: line     (ie. alt.test:389)       [optional]
 1995  *
 1996  * TODO: as we don't use the original data, we currently can't store
 1997  *       the data (from/subject) in the original charset (we don't store
 1998  *       that info). this has the advantage that we can avoid raw 8bit data
 1999  *       in our overviews, but the disadvantage that we might store the data
 2000  *       with a wrong charset and thus lose information. a simmiliar problem
 2001  *       exists with the data for the from:-line, we don't store it in the
 2002  *       original format, whenever our from-parser (partially) fails we'll
 2003  *       lose information in our overviews (but those couldn't be handeled
 2004  *       by tin anyway, so this is not a real problem).
 2005  *       long-term solution: store the original data in the overview
 2006  *       (tin has to handle raw 8bit data and other ugly stuff in the
 2007  *       overviews anyway and thus we preserver as much info as possible)
 2008  *       this would require some changes in read_overview() and
 2009  *       parse_headers(): don't do the decoding/unfolding there, but in a
 2010  *       second pass right after write_overview(), or two additional fields
 2011  *       which hold the raw data for from/subject. the latter has the
 2012  *       disadvantage that it costs (much) more memory.
 2013  */
 2014 static void
 2015 write_overview(
 2016     struct t_group *group)
 2017 {
 2018     FILE *fp;
 2019     int i;
 2020     struct t_article *article;
 2021 #ifdef CHARSET_CONVERSION
 2022     int c = -1;
 2023 #endif /* CHARSET_CONVERSION */
 2024 
 2025     /*
 2026      * Can't write or caching is off or getart_limit is set
 2027      */
 2028     if (no_write || !tinrc.cache_overview_files || ((cmdline.args & CMDLINE_GETART_LIMIT) ? cmdline.getart_limit : tinrc.getart_limit) != 0)
 2029         return;
 2030 
 2031     if ((fp = open_xover_fp(group, "w", T_ARTNUM_CONST(0), T_ARTNUM_CONST(0), FALSE)) == NULL)
 2032         return;
 2033 
 2034     if (group->attribute->sort_article_type != SORT_ARTICLES_BY_NOTHING)
 2035         SortBy(artnum_comp);
 2036 
 2037     /*
 2038      * Needed to preserve uniqueness in hashed private overview files
 2039      */
 2040     fprintf(fp, "%s\n", group->name);
 2041 
 2042 #ifdef CHARSET_CONVERSION
 2043     /* get undeclared_charset number if required */
 2044     if (group->attribute->undeclared_charset) {
 2045         for (i = 0; txt_mime_charsets[i] != NULL; i++) {
 2046             if (!strcasecmp(group->attribute->undeclared_charset, txt_mime_charsets[i])) {
 2047                 c = i;
 2048                 break;
 2049             }
 2050         }
 2051     }
 2052 #endif /* CHARSET_CONVERSION */
 2053 
 2054     for_each_art(i) {
 2055         char *p;
 2056         char *q, *ref;
 2057 
 2058         article = &arts[i];
 2059 
 2060         if (article->thread != ART_EXPIRED && article->artnum >= group->xmin) {
 2061             ref = NULL;
 2062 
 2063             if (!group->attribute->post_8bit_header) { /* write encoded data */
 2064                 /*
 2065                  * TODO: instead of tinrc.mm_local_charset we'd better use UTF-8
 2066                  *       here and in print_from() in the CHARSET_CONVERSION case.
 2067                  *       note that this requires something like
 2068                  *          buffer_to_network(article->subject, "UTF-8");
 2069                  *       right bfore the rfc1522_encode() call.
 2070                  *
 2071                  *       if we would cache the original undecoded data, we could
 2072                  *       ignore stuff like this.
 2073                  */
 2074                 p = rfc1522_encode(article->subject, tinrc.mm_local_charset, FALSE);
 2075                 /* as the subject might now be folded we have to unfold it */
 2076                 unfold_header(p);
 2077             } else { /* raw data */
 2078                 p = my_strdup(article->subject);
 2079 #ifdef CHARSET_CONVERSION
 2080                 if (group->attribute->undeclared_charset && c != -1) /* use undeclared_charset if set (otherwise local charset is used) */
 2081                     buffer_to_network(p, c);
 2082 #endif /* CHARSET_CONVERSION */
 2083             }
 2084 
 2085             /*
 2086              * replace any '\t's with ' ' in the references-data
 2087              *
 2088              * TODO: nntpext-draft might come up with a new scheme:
 2089              *       For all fields, the value is processed by first
 2090              *       removing all US-ASCII CRLF pairs and then replacing
 2091              *       each remaining US-ASCII NUL, TAB, CR, or LF character
 2092              *       with a single US-ASCII space (for example, CR LF LF TAB
 2093              *       will become two spaces).
 2094              */
 2095             if (article->refs) {
 2096                 ref = q = my_strdup(article->refs);
 2097                 while (*q) {
 2098                     if (*q == '\t')
 2099                         *q = ' ';
 2100                     q++;
 2101                 }
 2102             }
 2103 
 2104             fprintf(fp, "%"T_ARTNUM_PFMT"\t%s\t%s\t%s\t%s\t%s\t%d\t%d",
 2105                 article->artnum,
 2106                 p,
 2107 #ifdef CHARSET_CONVERSION
 2108                 print_from(group, article, c),
 2109 #else
 2110                 print_from(group, article, -1),
 2111 #endif /* CHARSET_CONVERSION */
 2112                 print_date(article->date),
 2113                 BlankIfNull(article->msgid),
 2114                 BlankIfNull(ref),
 2115                 0,  /* bytes */
 2116                 article->line_count);
 2117 
 2118             if (article->xref)
 2119                 fprintf(fp, "\tXref: %s", article->xref);
 2120 
 2121             fprintf(fp, "\n");
 2122             free(p);
 2123             if (article->refs) {
 2124                 FreeIfNeeded(ref);
 2125                 q = NULL;
 2126             }
 2127         }
 2128     }
 2129     fchmod(fileno(fp), (mode_t) (S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH));
 2130     fclose(fp);
 2131 }
 2132 
 2133 
 2134 /*
 2135  * A complex little function to determine the correct overview index file
 2136  * according to 'mode' (read or write)
 2137  * NULL is returned if the current setup dictates otherwise
 2138  *
 2139  * GROUP_TYPE_MAIL index files are read/written in ~/.tin/.mail
 2140  * GROUP_TYPE_SAVE index files are read/written in ~/.tin/.save
 2141  *
 2142  * Both of these are hashed
 2143  *
 2144  * GROUP_TYPE_NEWS index files are a little bit more complex
 2145  *
 2146  * When hashing the index filename will be in format number.number.
 2147  * Hashing the groupname gets a number. See if that #.1 file exists;
 2148  * if so, read first line. Is this the group we want? If no, try #.2.
 2149  * Repeat until no such file or we find an existing file that matches
 2150  * our group. Return pointer to path or NULL if not found.
 2151  */
 2152 static char *
 2153 find_nov_file(
 2154     struct t_group *group,
 2155     int mode)
 2156 {
 2157     FILE *fp;
 2158     const char *dir;
 2159     char buf[PATH_LEN];
 2160     int i;
 2161     struct stat sb;
 2162     unsigned long hash;
 2163     static char nov_file[PATH_LEN];
 2164     static t_bool once_only = FALSE;    /* Trap things that are done only 1 time */
 2165 
 2166     if (group == NULL || (mode != R_OK && mode != W_OK))
 2167         return NULL;
 2168 
 2169     switch (group->type) {
 2170         case GROUP_TYPE_MAIL:
 2171             dir = index_maildir;
 2172             break;
 2173 
 2174         case GROUP_TYPE_SAVE:
 2175             dir = index_savedir;
 2176             break;
 2177 
 2178         case GROUP_TYPE_NEWS:
 2179             /*
 2180              * nntp.caps.over_cmd is not an issue here, any gripes and warnings
 2181              * about [X]OVER are handled in nntp_open()
 2182              */
 2183 
 2184             /*
 2185              * When reading via NNTP, system wide overviews are irrelevant, of
 2186              * course, and the private overview filename will be the same for
 2187              * both reading and writing.
 2188              *
 2189              * When working locally, we only use a private cache for reading
 2190              * if requested and when system wide overviews don't already exist.
 2191              * When writing then only private overviews can be used since
 2192              * updating system wide overviews is not safe wrt locking etc.
 2193              *
 2194              * See if local overview file $SPOOLDIR/<groupname>/.overview exists
 2195              */
 2196 #ifndef NNTP_ONLY
 2197             if (!read_news_via_nntp) {
 2198                 make_base_group_path(novrootdir, group->name, buf, sizeof(buf));
 2199                 joinpath(nov_file, sizeof(nov_file), buf, novfilename);
 2200                 if (access(nov_file, R_OK) == 0) {
 2201                     if (mode == R_OK)
 2202                         return nov_file;        /* Use system wide overviews */
 2203                     else
 2204                         return NULL;            /* Don't write cache in this case */
 2205                 }
 2206             }
 2207 #endif /* !NNTP_ONLY */
 2208 
 2209             /*
 2210              * We only get here when private overviews are going to be used
 2211              * Go no further if they are explicitly turned off
 2212              */
 2213             if (!tinrc.cache_overview_files)
 2214                 return NULL;
 2215 
 2216             /*
 2217              * Append -<nntpserver> to private cache dir
 2218              */
 2219             if (!once_only && nntp_server) {
 2220                 size_t sp, ln = strlen(index_newsdir);
 2221 
 2222                 if ((sp = sizeof(index_newsdir) - ln - 1) >= 2) {
 2223                     char *srv = my_strdup(nntp_server);
 2224 
 2225                     strcat(index_newsdir, "-");
 2226                     sp--;
 2227                     ln++;
 2228                     str_lwr(srv);
 2229                     my_strncpy(index_newsdir + ln, srv, sp);
 2230                     free(srv);
 2231                 }
 2232                 once_only = TRUE;
 2233             }
 2234 
 2235             /*
 2236              * Only try to set up the private cache when writing. If it
 2237              * doesn't exist yet, then ergo we can't read from it.
 2238              * The cache will be checked/created on every write; a previous
 2239              * bug report complained that this was not the case
 2240              */
 2241             if (stat(index_newsdir, &sb) == -1) {           /* Private cache doesn't exist */
 2242                 if (mode == R_OK)
 2243                     return NULL;
 2244                 if (my_mkdir(index_newsdir, (mode_t) S_IRWXU) != 0)
 2245                     return NULL;
 2246             } else {
 2247                 if (!S_ISDIR(sb.st_mode))
 2248                     return NULL;
 2249             }
 2250 
 2251             /*
 2252              * Update the newsgroups cache to point to the new location
 2253              * now that we know it is valid
 2254              */
 2255             if (!once_only)
 2256                 joinpath(local_newsgroups_file, sizeof(local_newsgroups_file), index_newsdir, NEWSGROUPS_FILE);
 2257 
 2258             dir = index_newsdir;
 2259             break;
 2260 
 2261         default: /* not reached */
 2262             return NULL;
 2263     }
 2264 
 2265     /*
 2266      * We only get here if writing to a private overview.
 2267      * These always have hashed filenames.
 2268      * Try <hash>.<seqno> and check the group name tagline until
 2269      * matching index file is found. If not found return next unused
 2270      * filename
 2271      */
 2272     hash = hash_groupname(group->name);
 2273 
 2274     for (i = 1; ; i++) {
 2275         char *ptr;
 2276 
 2277         snprintf(buf, sizeof(buf), "%lu.%d", hash, i);
 2278         joinpath(nov_file, sizeof(nov_file), dir, buf);
 2279 
 2280         if ((fp = fopen(nov_file, "r")) == NULL)
 2281             break;
 2282 
 2283         /*
 2284          * No group name header, so not a valid index file => overwrite it
 2285          */
 2286         if (fgets(buf, (int) sizeof(buf), fp) == NULL) {
 2287             fclose(fp);
 2288             break;
 2289         }
 2290         fclose(fp);
 2291 
 2292         if ((ptr = strrchr(buf, '\n')) != NULL)
 2293             *ptr = '\0';
 2294 
 2295         if (strcmp(buf, group->name) == 0)
 2296             break;
 2297     }
 2298 
 2299     return nov_file;
 2300 }
 2301 
 2302 
 2303 /*
 2304  * Run the index file updater only for the groups we've loaded.
 2305  */
 2306 void
 2307 do_update(
 2308     t_bool catchup)
 2309 {
 2310     int i, j, k = 0;
 2311     time_t beg_epoch = 0;
 2312     struct t_article *art;
 2313     struct t_group *group;
 2314 
 2315     if (verbose)
 2316         (void) time(&beg_epoch);
 2317 
 2318     /*
 2319      * loop through groups and update any required index files
 2320      */
 2321     for (i = 0; i < selmenu.max; i++) {
 2322         group = &active[my_group[i]];
 2323         /*
 2324          * FIXME: workaround to get a valid CURR_GROUP
 2325          * it also points to the currently processed group so that
 2326          * the correct attributes are used
 2327          * The correct fix is to get rid of CURR_GROUP
 2328          */
 2329         selmenu.curr = i;
 2330 
 2331         if (group->bogus || !group->subscribed)
 2332             continue;
 2333 
 2334         if (!index_group(group)) {
 2335             for_each_art(j) {
 2336                 art = &arts[j];
 2337                 FreeAndNull(art->refs);
 2338                 FreeAndNull(art->msgid);
 2339             }
 2340             continue;
 2341         }
 2342 
 2343         k++;
 2344 
 2345         if (verbose) {
 2346             my_printf("%s %s\n", (catchup ? _(txt_catchup) : _(txt_updating)), group->name);
 2347             my_flush();
 2348         }
 2349 
 2350         if (catchup) {
 2351             for_each_art(j)
 2352                 art_mark(group, &arts[j], ART_READ);
 2353         }
 2354     }
 2355 
 2356     if (verbose) {
 2357         wait_message(0, _(txt_catchup_update_info),
 2358             (catchup ? _(txt_caughtup) : _(txt_updated)), k,
 2359             PLURAL(selmenu.max, txt_group), (unsigned long int) (time(NULL) - beg_epoch));
 2360     }
 2361 }
 2362 
 2363 
 2364 static int
 2365 artnum_comp(
 2366     t_comptype p1,
 2367     t_comptype p2)
 2368 {
 2369     const struct t_article *s1 = (const struct t_article *) p1;
 2370     const struct t_article *s2 = (const struct t_article *) p2;
 2371 
 2372     /*
 2373      * s1->artnum less than s2->artnum
 2374      */
 2375     if (s1->artnum < s2->artnum)
 2376         return -1;
 2377 
 2378     /*
 2379      * s1->artnum greater than s2->artnum
 2380      */
 2381     if (s1->artnum > s2->artnum)
 2382         return 1;
 2383 
 2384     return 0;
 2385 }
 2386 
 2387 
 2388 /*
 2389  * return result of strcmp (reversed for descending)
 2390  */
 2391 static int
 2392 subj_comp_asc(
 2393     t_comptype p1,
 2394     t_comptype p2)
 2395 {
 2396     int retval;
 2397     const struct t_article *s1 = (const struct t_article *) p1;
 2398     const struct t_article *s2 = (const struct t_article *) p2;
 2399 
 2400     if ((retval = strcasecmp(s1->subject, s2->subject))) /* != 0 */
 2401         return retval;
 2402 
 2403     return s1->date - s2->date > 0 ? 1 : -1;
 2404 }
 2405 
 2406 
 2407 static int
 2408 subj_comp_desc(
 2409     t_comptype p1,
 2410     t_comptype p2)
 2411 {
 2412     int retval;
 2413     const struct t_article *s1 = (const struct t_article *) p1;
 2414     const struct t_article *s2 = (const struct t_article *) p2;
 2415 
 2416     if ((retval = strcasecmp(s2->subject, s1->subject))) /* != 0 */
 2417         return retval;
 2418 
 2419     return s1->date - s2->date > 0 ? 1 : -1;
 2420 }
 2421 
 2422 
 2423 /*
 2424  * return result of strcmp (reversed for descending)
 2425  */
 2426 static int
 2427 from_comp_asc(
 2428     t_comptype p1,
 2429     t_comptype p2)
 2430 {
 2431     int retval;
 2432     const struct t_article *s1 = (const struct t_article *) p1;
 2433     const struct t_article *s2 = (const struct t_article *) p2;
 2434 
 2435     if ((retval = strcasecmp(s1->from, s2->from))) /* != 0 */
 2436         return retval;
 2437 
 2438     return s1->date - s2->date > 0 ? 1 : -1;
 2439 }
 2440 
 2441 
 2442 static int
 2443 from_comp_desc(
 2444     t_comptype p1,
 2445     t_comptype p2)
 2446 {
 2447     int retval;
 2448     const struct t_article *s1 = (const struct t_article *) p1;
 2449     const struct t_article *s2 = (const struct t_article *) p2;
 2450 
 2451     if ((retval = strcasecmp(s2->from, s1->from))) /* != 0 */
 2452         return retval;
 2453 
 2454     return s1->date - s2->date > 0 ? 1 : -1;
 2455 }
 2456 
 2457 
 2458 /*
 2459  * Works like strcmp() for comparing time_t type values
 2460  * Return codes:
 2461  *  -1:     If p1 is before p2
 2462  *   0:     If they are the same time
 2463  *   1:     If p1 is after p2
 2464  * If the sort order is _not_ DATE_ASCEND then the sense of the above
 2465  * is reversed.
 2466  */
 2467 static int
 2468 date_comp_asc(
 2469     t_comptype p1,
 2470     t_comptype p2)
 2471 {
 2472     const struct t_article *s1 = (const struct t_article *) p1;
 2473     const struct t_article *s2 = (const struct t_article *) p2;
 2474 
 2475     /*
 2476      * s1->date less than s2->date
 2477      */
 2478     if (s1->date < s2->date)
 2479         return -1;
 2480 
 2481     /*
 2482      * s1->date greater than s2->date
 2483      */
 2484     if (s1->date > s2->date)
 2485         return 1;
 2486 
 2487     return 0;
 2488 }
 2489 
 2490 
 2491 static int
 2492 date_comp_desc(
 2493     t_comptype p1,
 2494     t_comptype p2)
 2495 {
 2496     const struct t_article *s1 = (const struct t_article *) p1;
 2497     const struct t_article *s2 = (const struct t_article *) p2;
 2498 
 2499     /*
 2500      * s2->date less than s1->date
 2501      */
 2502     if (s2->date < s1->date)
 2503         return -1;
 2504 
 2505     /*
 2506      * s2->date greater than s1->date
 2507      */
 2508     if (s2->date > s1->date)
 2509         return 1;
 2510 
 2511     return 0;
 2512 }
 2513 
 2514 
 2515 /*
 2516  * Same again, but for art[].score
 2517  */
 2518 static int
 2519 score_comp_asc(
 2520     t_comptype p1,
 2521     t_comptype p2)
 2522 {
 2523     const struct t_article *s1 = (const struct t_article *) p1;
 2524     const struct t_article *s2 = (const struct t_article *) p2;
 2525 
 2526     if (s1->score < s2->score)
 2527         return -1;
 2528 
 2529     if (s1->score > s2->score)
 2530         return 1;
 2531 
 2532     return s1->date - s2->date > 0 ? 1 : -1;
 2533 }
 2534 
 2535 
 2536 static int
 2537 score_comp_desc(
 2538     t_comptype p1,
 2539     t_comptype p2)
 2540 {
 2541     const struct t_article *s1 = (const struct t_article *) p1;
 2542     const struct t_article *s2 = (const struct t_article *) p2;
 2543 
 2544     if (s2->score < s1->score)
 2545         return -1;
 2546 
 2547     if (s2->score > s1->score)
 2548         return 1;
 2549 
 2550     return s1->date - s2->date > 0 ? 1 : -1;
 2551 }
 2552 
 2553 
 2554 /*
 2555  * Same again, but for art[].line_count
 2556  */
 2557 static int
 2558 lines_comp_asc(
 2559     t_comptype p1,
 2560     t_comptype p2)
 2561 {
 2562     const struct t_article *s1 = (const struct t_article *) p1;
 2563     const struct t_article *s2 = (const struct t_article *) p2;
 2564 
 2565     if (s1->line_count < s2->line_count)
 2566         return -1;
 2567 
 2568     if (s1->line_count > s2->line_count)
 2569         return 1;
 2570 
 2571     return s1->date - s2->date > 0 ? 1 : -1;
 2572 }
 2573 
 2574 
 2575 static int
 2576 lines_comp_desc(
 2577     t_comptype p1,
 2578     t_comptype p2)
 2579 {
 2580     const struct t_article *s1 = (const struct t_article *) p1;
 2581     const struct t_article *s2 = (const struct t_article *) p2;
 2582 
 2583     if (s2->line_count < s1->line_count)
 2584         return -1;
 2585 
 2586     if (s2->line_count > s1->line_count)
 2587         return 1;
 2588 
 2589     return s1->date - s2->date > 0 ? 1 : -1;
 2590 }
 2591 
 2592 
 2593 /*
 2594  * Compares the total score of two threads. Used for sorting base[].
 2595  */
 2596 static int
 2597 score_comp_base(
 2598     t_comptype p1,
 2599     t_comptype p2)
 2600 {
 2601     int a = get_score_of_thread(*(const long *) p1);
 2602     int b = get_score_of_thread(*(const long *) p2);
 2603 
 2604     /* If scores are equal, compare using the article sort order.
 2605      * This determines the order in a group of equally scored threads.
 2606      */
 2607     if (a == b) {
 2608         const struct t_article *s1 = &arts[*(const long *) p1];
 2609         const struct t_article *s2 = &arts[*(const long *) p2];
 2610         t_compfunc comp_func = eval_sort_arts_func(CURR_GROUP.attribute->sort_article_type);
 2611 
 2612         if (comp_func)
 2613             return (*comp_func)(s1, s2);
 2614         return 0;
 2615     }
 2616 
 2617     if (CURR_GROUP.attribute->sort_threads_type == SORT_THREADS_BY_SCORE_ASCEND)
 2618         return a > b ? 1 : -1;
 2619     return a < b ? 1 : -1;
 2620 }
 2621 
 2622 
 2623 /*
 2624  * Compare the date of the last posted article of two threads.
 2625  * Used for sorting base[].
 2626  */
 2627 static int
 2628 last_date_comp_base_desc(
 2629     t_comptype p1,
 2630     t_comptype p2)
 2631 {
 2632     time_t s1_last = get_last_posting_date(*(const long *) p1);
 2633     time_t s2_last = get_last_posting_date(*(const long *) p2);
 2634 
 2635     if (s2_last < s1_last)
 2636         return -1;
 2637 
 2638     if (s2_last > s1_last)
 2639         return 1;
 2640 
 2641     return 0;
 2642 }
 2643 
 2644 
 2645 static int
 2646 last_date_comp_base_asc(
 2647     t_comptype p1,
 2648     t_comptype p2)
 2649 {
 2650     time_t s1_last = get_last_posting_date(*(const long *) p1);
 2651     time_t s2_last = get_last_posting_date(*(const long *) p2);
 2652 
 2653     if (s2_last > s1_last)
 2654         return -1;
 2655 
 2656     if (s2_last < s1_last)
 2657         return 1;
 2658 
 2659     return 0;
 2660 }
 2661 
 2662 
 2663 static time_t
 2664 get_last_posting_date(
 2665     long n)
 2666 {
 2667     long i;
 2668     time_t last = (time_t) 0;
 2669 
 2670     for (i = n; i >= 0; i = arts[i].thread) {
 2671         if (arts[i].date > last)
 2672             last = arts[i].date;
 2673     }
 2674 
 2675     return last;
 2676 }
 2677 
 2678 
 2679 void
 2680 set_article(
 2681     struct t_article *art)
 2682 {
 2683     art->subject = NULL;
 2684     art->from = NULL;
 2685     art->name = NULL;
 2686     art->date = (time_t) 0;
 2687     art->xref = NULL;
 2688     art->msgid = NULL;
 2689     art->refs = NULL;
 2690     art->refptr = NULL;
 2691     art->line_count = -1;
 2692     art->archive = NULL;
 2693     art->tagged = 0;
 2694     art->thread = ART_EXPIRED;
 2695     art->prev = ART_NORMAL;
 2696     art->score = 0;
 2697     art->status = ART_UNREAD;
 2698     art->killed = ART_NOTKILLED;
 2699     art->zombie = FALSE;
 2700     art->delete_it = FALSE;
 2701     art->selected = FALSE;
 2702     art->inrange = FALSE;
 2703     art->matched = FALSE;
 2704     art->keep_in_base = FALSE;
 2705 }
 2706 
 2707 
 2708 /*
 2709  * Do a binary chop to see if 'art' (an article number) exists in arts[]
 2710  * Naturally arts[] must be sorted on artnum
 2711  * Return index into arts[] or -1
 2712  */
 2713 static int
 2714 valid_artnum(
 2715     t_artnum art)
 2716 {
 2717     int prev, range;
 2718     int dctop = top_art;
 2719     int cur = 1;
 2720 
 2721     while ((dctop >>= 1))
 2722         cur <<= 1;
 2723 
 2724     range = cur >> 1;
 2725     cur--;
 2726 
 2727     forever {
 2728         if (arts[cur].artnum == art)
 2729             return cur;
 2730 
 2731         prev = cur;
 2732         cur += ((arts[cur].artnum < art) ? range : -range);
 2733         if (prev == cur)
 2734             break;
 2735 
 2736         if (cur >= top_art)
 2737             cur = top_art - 1;
 2738 
 2739         range >>= 1;
 2740     }
 2741     return -1;
 2742 }
 2743 
 2744 
 2745 /*
 2746  * Loop over arts[] to see if 'art' (an article number) exists in arts[]
 2747  * Needed if arts[] is not sorted on artnum
 2748  * Return index into arts[] or -1
 2749  */
 2750 int
 2751 find_artnum(
 2752     t_artnum art)
 2753 {
 2754     int i;
 2755 
 2756     for_each_art(i) {
 2757         if (arts[i].artnum == art)
 2758             return i;
 2759     }
 2760     return -1;
 2761 }
 2762 
 2763 
 2764 /*----------------------------- Overview handling -----------------------*/
 2765 /* TODO: use
 2766  *           setlocale(LC_ALL, "POSIX"); setlocale(LC_TIME, "POSIX");
 2767  *           my_strftime(date, sizeof(date) -1, "%d %b %Y %H:%M:%S GMT", gmtime(&secs));
 2768  *       instead?
 2769  */
 2770 static char *
 2771 print_date(
 2772     time_t secs)
 2773 {
 2774     static char date[25];
 2775     struct tm *tm;
 2776     static const char *const months_a[] = {
 2777         "Jan", "Feb", "Mar", "Apr", "May", "Jun",
 2778         "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
 2779     };
 2780 
 2781     if ((tm = gmtime(&secs)) != NULL)
 2782         snprintf(date, sizeof(date), "%02d %.3s %04d %02d:%02d:%02d GMT",
 2783                 tm->tm_mday,
 2784                 months_a[tm->tm_mon],
 2785                 tm->tm_year + 1900,
 2786                 tm->tm_hour, tm->tm_min, tm->tm_sec);
 2787     else
 2788         snprintf(date, sizeof(date), "01 Jan 1970 00:00:00 UTC");
 2789 
 2790     return date;
 2791 }
 2792 
 2793 
 2794 static char *
 2795 print_from(
 2796     struct t_group *group,
 2797     struct t_article *article,
 2798     int charset)
 2799 {
 2800     char *p, *q;
 2801     static char from[PATH_LEN];
 2802 
 2803     *from = '\0';
 2804 
 2805     if (article->name != NULL) {
 2806         q = my_strdup(article->name);
 2807 #ifdef CHARSET_CONVERSION
 2808         if (charset != -1) {
 2809             buffer_to_network(q, charset);
 2810         }
 2811 #endif /* CHARSET_CONVERSION */
 2812         p = rfc1522_encode(article->name, tinrc.mm_local_charset, FALSE);
 2813         unfold_header(p);
 2814         if (strpbrk(article->name, "\".:;<>@[]()\\") != NULL && article->name[0] != '"' && article->name[strlen(article->name)] != '"')
 2815             snprintf(from, sizeof(from), "\"%s\" <%s>", group->attribute->post_8bit_header ? q : p, article->from);
 2816         else
 2817             snprintf(from, sizeof(from), "%s <%s>", group->attribute->post_8bit_header ? q : p, article->from);
 2818 
 2819         free(p);
 2820         free(q);
 2821     } else
 2822         STRCPY(from, article->from);
 2823 
 2824     return from;
 2825 }
 2826 
 2827 
 2828 /*
 2829  * Open a group news overview file
 2830  * Use NNTP XOVER where possible unless 'local' is set
 2831  */
 2832 static FILE *
 2833 open_xover_fp(
 2834     struct t_group *group,
 2835     const char *mode,
 2836     t_artnum min,
 2837     t_artnum max,
 2838     t_bool local)
 2839 {
 2840 #ifdef NNTP_ABLE
 2841     if (!local && nntp_caps.over_cmd && *mode == 'r' && group->type == GROUP_TYPE_NEWS) {
 2842         char line[NNTP_STRLEN];
 2843 
 2844         if (!max)
 2845             return NULL;
 2846         if (min == max)
 2847             snprintf(line, sizeof(line), "%s %"T_ARTNUM_PFMT, nntp_caps.over_cmd, min);
 2848         else
 2849             snprintf(line, sizeof(line), "%s %"T_ARTNUM_PFMT"-%"T_ARTNUM_PFMT, nntp_caps.over_cmd, min, MAX(min, max));
 2850         return (nntp_command(line, OK_XOVER, NULL, 0));
 2851     }
 2852 #endif /* NNTP_ABLE */
 2853     {
 2854         FILE *fp;
 2855         char *nov_file = find_nov_file(group, (*mode == 'r') ? R_OK : W_OK);
 2856 
 2857         if (nov_file != NULL) {
 2858             if ((fp = fopen(nov_file, mode)) != NULL)
 2859                 return fp;
 2860 
 2861             if (*mode != 'r')
 2862                 error_message(2, _(txt_cannot_open), nov_file);
 2863         }
 2864     }
 2865     return NULL;
 2866 }
 2867 
 2868 
 2869 #ifdef USE_HEAPSORT
 2870 int
 2871 tin_sort(
 2872     void *sbase,
 2873     size_t nel,
 2874     size_t width,
 2875     t_compfunc compar)
 2876 {
 2877     int rc;
 2878 
 2879     switch (tinrc.sort_function) {
 2880         case 0:
 2881             qsort(sbase, nel, width, compar);
 2882             rc = 0;
 2883             break;
 2884 
 2885         case 1:
 2886             rc = heapsort(sbase, nel, width, compar);
 2887             break;
 2888 
 2889         default:
 2890             rc = -1;
 2891             break;
 2892     }
 2893     return rc;
 2894 }
 2895 #endif /* USE_HEAPSORT */