1 /**
2  * @file
3  * Read/parse/write an NNTP config file of subscribed newsgroups
4  *
5  * @authors
6  * Copyright (C) 1998 Brandon Long <blong@fiction.net>
7  * Copyright (C) 1999 Andrej Gritsenko <andrej@lucky.net>
8  * Copyright (C) 2000-2017 Vsevolod Volkov <vvv@mutt.org.ua>
9  *
10  * @copyright
11  * This program is free software: you can redistribute it and/or modify it under
12  * the terms of the GNU General Public License as published by the Free Software
13  * Foundation, either version 2 of the License, or (at your option) any later
14  * version.
15  *
16  * This program is distributed in the hope that it will be useful, but WITHOUT
17  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18  * FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
19  * details.
20  *
21  * You should have received a copy of the GNU General Public License along with
22  * this program.  If not, see <http://www.gnu.org/licenses/>.
23  */
24 
25 /**
26  * @page nntp_newsrc Read/write a file of subscribed newsgroups
27  *
28  * Read/parse/write an NNTP config file of subscribed newsgroups
29  */
30 
31 #include "config.h"
32 #include <dirent.h>
33 #include <errno.h>
34 #include <limits.h>
35 #include <stdbool.h>
36 #include <stdio.h>
37 #include <string.h>
38 #include <sys/stat.h>
39 #include <time.h>
40 #include <unistd.h>
41 #include "private.h"
42 #include "mutt/lib.h"
43 #include "config/lib.h"
44 #include "email/lib.h"
45 #include "core/lib.h"
46 #include "conn/lib.h"
47 #include "mutt.h"
48 #include "lib.h"
49 #include "bcache/lib.h"
50 #include "adata.h"
51 #include "edata.h"
52 #include "format_flags.h"
53 #include "mdata.h"
54 #include "mutt_account.h"
55 #include "mutt_logging.h"
56 #include "mutt_socket.h"
57 #include "muttlib.h"
58 #include "protos.h"
59 #ifdef USE_HCACHE
60 #include "hcache/lib.h"
61 #endif
62 
63 struct BodyCache;
64 
65 /**
66  * mdata_find - Find NntpMboxData for given newsgroup or add it
67  * @param adata NNTP server
68  * @param group Newsgroup
69  * @retval ptr  NNTP data
70  * @retval NULL Error
71  */
mdata_find(struct NntpAccountData * adata,const char * group)72 static struct NntpMboxData *mdata_find(struct NntpAccountData *adata, const char *group)
73 {
74   struct NntpMboxData *mdata = mutt_hash_find(adata->groups_hash, group);
75   if (mdata)
76     return mdata;
77 
78   size_t len = strlen(group) + 1;
79   /* create NntpMboxData structure and add it to hash */
80   mdata = mutt_mem_calloc(1, sizeof(struct NntpMboxData) + len);
81   mdata->group = (char *) mdata + sizeof(struct NntpMboxData);
82   mutt_str_copy(mdata->group, group, len);
83   mdata->adata = adata;
84   mdata->deleted = true;
85   mutt_hash_insert(adata->groups_hash, mdata->group, mdata);
86 
87   /* add NntpMboxData to list */
88   if (adata->groups_num >= adata->groups_max)
89   {
90     adata->groups_max *= 2;
91     mutt_mem_realloc(&adata->groups_list, adata->groups_max * sizeof(mdata));
92   }
93   adata->groups_list[adata->groups_num++] = mdata;
94 
95   return mdata;
96 }
97 
98 /**
99  * nntp_acache_free - Remove all temporarily cache files
100  * @param mdata NNTP Mailbox data
101  */
nntp_acache_free(struct NntpMboxData * mdata)102 void nntp_acache_free(struct NntpMboxData *mdata)
103 {
104   for (int i = 0; i < NNTP_ACACHE_LEN; i++)
105   {
106     if (mdata->acache[i].path)
107     {
108       unlink(mdata->acache[i].path);
109       FREE(&mdata->acache[i].path);
110     }
111   }
112 }
113 
114 /**
115  * nntp_newsrc_close - Unlock and close .newsrc file
116  * @param adata NNTP server
117  */
nntp_newsrc_close(struct NntpAccountData * adata)118 void nntp_newsrc_close(struct NntpAccountData *adata)
119 {
120   if (!adata->fp_newsrc)
121     return;
122 
123   mutt_debug(LL_DEBUG1, "Unlocking %s\n", adata->newsrc_file);
124   mutt_file_unlock(fileno(adata->fp_newsrc));
125   mutt_file_fclose(&adata->fp_newsrc);
126 }
127 
128 /**
129  * nntp_group_unread_stat - Count number of unread articles using .newsrc data
130  * @param mdata NNTP Mailbox data
131  */
nntp_group_unread_stat(struct NntpMboxData * mdata)132 void nntp_group_unread_stat(struct NntpMboxData *mdata)
133 {
134   mdata->unread = 0;
135   if ((mdata->last_message == 0) ||
136       (mdata->first_message > mdata->last_message) || !mdata->newsrc_ent)
137   {
138     return;
139   }
140 
141   mdata->unread = mdata->last_message - mdata->first_message + 1;
142   for (unsigned int i = 0; i < mdata->newsrc_len; i++)
143   {
144     anum_t first = mdata->newsrc_ent[i].first;
145     if (first < mdata->first_message)
146       first = mdata->first_message;
147     anum_t last = mdata->newsrc_ent[i].last;
148     if (last > mdata->last_message)
149       last = mdata->last_message;
150     if (first <= last)
151       mdata->unread -= last - first + 1;
152   }
153 }
154 
155 /**
156  * nntp_newsrc_parse - Parse .newsrc file
157  * @param adata NNTP server
158  * @retval  0 Not changed
159  * @retval  1 Parsed
160  * @retval -1 Error
161  */
nntp_newsrc_parse(struct NntpAccountData * adata)162 int nntp_newsrc_parse(struct NntpAccountData *adata)
163 {
164   char *line = NULL;
165   struct stat st = { 0 };
166 
167   if (adata->fp_newsrc)
168   {
169     /* if we already have a handle, close it and reopen */
170     mutt_file_fclose(&adata->fp_newsrc);
171   }
172   else
173   {
174     /* if file doesn't exist, create it */
175     adata->fp_newsrc = mutt_file_fopen(adata->newsrc_file, "a");
176     mutt_file_fclose(&adata->fp_newsrc);
177   }
178 
179   /* open .newsrc */
180   adata->fp_newsrc = mutt_file_fopen(adata->newsrc_file, "r");
181   if (!adata->fp_newsrc)
182   {
183     mutt_perror(adata->newsrc_file);
184     return -1;
185   }
186 
187   /* lock it */
188   mutt_debug(LL_DEBUG1, "Locking %s\n", adata->newsrc_file);
189   if (mutt_file_lock(fileno(adata->fp_newsrc), false, true))
190   {
191     mutt_file_fclose(&adata->fp_newsrc);
192     return -1;
193   }
194 
195   if (stat(adata->newsrc_file, &st) != 0)
196   {
197     mutt_perror(adata->newsrc_file);
198     nntp_newsrc_close(adata);
199     return -1;
200   }
201 
202   if ((adata->size == st.st_size) && (adata->mtime == st.st_mtime))
203     return 0;
204 
205   adata->size = st.st_size;
206   adata->mtime = st.st_mtime;
207   adata->newsrc_modified = true;
208   mutt_debug(LL_DEBUG1, "Parsing %s\n", adata->newsrc_file);
209 
210   /* .newsrc has been externally modified or hasn't been loaded yet */
211   for (unsigned int i = 0; i < adata->groups_num; i++)
212   {
213     struct NntpMboxData *mdata = adata->groups_list[i];
214     if (!mdata)
215       continue;
216 
217     mdata->subscribed = false;
218     mdata->newsrc_len = 0;
219     FREE(&mdata->newsrc_ent);
220   }
221 
222   line = mutt_mem_malloc(st.st_size + 1);
223   while (st.st_size && fgets(line, st.st_size + 1, adata->fp_newsrc))
224   {
225     char *b = NULL, *h = NULL;
226     unsigned int j = 1;
227     bool subs = false;
228 
229     /* find end of newsgroup name */
230     char *p = strpbrk(line, ":!");
231     if (!p)
232       continue;
233 
234     /* ":" - subscribed, "!" - unsubscribed */
235     if (*p == ':')
236       subs = true;
237     *p++ = '\0';
238 
239     /* get newsgroup data */
240     struct NntpMboxData *mdata = mdata_find(adata, line);
241     FREE(&mdata->newsrc_ent);
242 
243     /* count number of entries */
244     b = p;
245     while (*b)
246       if (*b++ == ',')
247         j++;
248     mdata->newsrc_ent = mutt_mem_calloc(j, sizeof(struct NewsrcEntry));
249     mdata->subscribed = subs;
250 
251     /* parse entries */
252     j = 0;
253     while (p)
254     {
255       b = p;
256 
257       /* find end of entry */
258       p = strchr(p, ',');
259       if (p)
260         *p++ = '\0';
261 
262       /* first-last or single number */
263       h = strchr(b, '-');
264       if (h)
265         *h++ = '\0';
266       else
267         h = b;
268 
269       if ((sscanf(b, ANUM, &mdata->newsrc_ent[j].first) == 1) &&
270           (sscanf(h, ANUM, &mdata->newsrc_ent[j].last) == 1))
271       {
272         j++;
273       }
274     }
275     if (j == 0)
276     {
277       mdata->newsrc_ent[j].first = 1;
278       mdata->newsrc_ent[j].last = 0;
279       j++;
280     }
281     if (mdata->last_message == 0)
282       mdata->last_message = mdata->newsrc_ent[j - 1].last;
283     mdata->newsrc_len = j;
284     mutt_mem_realloc(&mdata->newsrc_ent, j * sizeof(struct NewsrcEntry));
285     nntp_group_unread_stat(mdata);
286     mutt_debug(LL_DEBUG2, "%s\n", mdata->group);
287   }
288   FREE(&line);
289   return 1;
290 }
291 
292 /**
293  * nntp_newsrc_gen_entries - Generate array of .newsrc entries
294  * @param m Mailbox
295  */
nntp_newsrc_gen_entries(struct Mailbox * m)296 void nntp_newsrc_gen_entries(struct Mailbox *m)
297 {
298   if (!m)
299     return;
300 
301   struct NntpMboxData *mdata = m->mdata;
302   anum_t last = 0, first = 1;
303   bool series;
304   unsigned int entries;
305 
306   const short c_sort = cs_subset_sort(NeoMutt->sub, "sort");
307   if (c_sort != SORT_ORDER)
308   {
309     cs_subset_str_native_set(NeoMutt->sub, "sort", SORT_ORDER, NULL);
310     mailbox_changed(m, NT_MAILBOX_RESORT);
311   }
312 
313   entries = mdata->newsrc_len;
314   if (!entries)
315   {
316     entries = 5;
317     mdata->newsrc_ent = mutt_mem_calloc(entries, sizeof(struct NewsrcEntry));
318   }
319 
320   /* Set up to fake initial sequence from 1 to the article before the
321    * first article in our list */
322   mdata->newsrc_len = 0;
323   series = true;
324   for (int i = 0; i < m->msg_count; i++)
325   {
326     struct Email *e = m->emails[i];
327     if (!e)
328       break;
329 
330     /* search for first unread */
331     if (series)
332     {
333       /* We don't actually check sequential order, since we mark
334        * "missing" entries as read/deleted */
335       last = nntp_edata_get(e)->article_num;
336       if ((last >= mdata->first_message) && !e->deleted && !e->read)
337       {
338         if (mdata->newsrc_len >= entries)
339         {
340           entries *= 2;
341           mutt_mem_realloc(&mdata->newsrc_ent, entries * sizeof(struct NewsrcEntry));
342         }
343         mdata->newsrc_ent[mdata->newsrc_len].first = first;
344         mdata->newsrc_ent[mdata->newsrc_len].last = last - 1;
345         mdata->newsrc_len++;
346         series = false;
347       }
348     }
349 
350     /* search for first read */
351     else
352     {
353       if (e->deleted || e->read)
354       {
355         first = last + 1;
356         series = true;
357       }
358       last = nntp_edata_get(e)->article_num;
359     }
360   }
361 
362   if (series && (first <= mdata->last_loaded))
363   {
364     if (mdata->newsrc_len >= entries)
365     {
366       entries++;
367       mutt_mem_realloc(&mdata->newsrc_ent, entries * sizeof(struct NewsrcEntry));
368     }
369     mdata->newsrc_ent[mdata->newsrc_len].first = first;
370     mdata->newsrc_ent[mdata->newsrc_len].last = mdata->last_loaded;
371     mdata->newsrc_len++;
372   }
373   mutt_mem_realloc(&mdata->newsrc_ent, mdata->newsrc_len * sizeof(struct NewsrcEntry));
374 
375   if (c_sort != SORT_ORDER)
376   {
377     cs_subset_str_native_set(NeoMutt->sub, "sort", c_sort, NULL);
378     mailbox_changed(m, NT_MAILBOX_RESORT);
379   }
380 }
381 
382 /**
383  * update_file - Update file with new contents
384  * @param filename File to update
385  * @param buf      New context
386  * @retval  0 Success
387  * @retval -1 Failure
388  */
update_file(char * filename,char * buf)389 static int update_file(char *filename, char *buf)
390 {
391   FILE *fp = NULL;
392   char tmpfile[PATH_MAX];
393   int rc = -1;
394 
395   while (true)
396   {
397     snprintf(tmpfile, sizeof(tmpfile), "%s.tmp", filename);
398     fp = mutt_file_fopen(tmpfile, "w");
399     if (!fp)
400     {
401       mutt_perror(tmpfile);
402       *tmpfile = '\0';
403       break;
404     }
405     if (fputs(buf, fp) == EOF)
406     {
407       mutt_perror(tmpfile);
408       break;
409     }
410     if (mutt_file_fclose(&fp) == EOF)
411     {
412       mutt_perror(tmpfile);
413       fp = NULL;
414       break;
415     }
416     fp = NULL;
417     if (rename(tmpfile, filename) < 0)
418     {
419       mutt_perror(filename);
420       break;
421     }
422     *tmpfile = '\0';
423     rc = 0;
424     break;
425   }
426   mutt_file_fclose(&fp);
427 
428   if (*tmpfile)
429     unlink(tmpfile);
430   return rc;
431 }
432 
433 /**
434  * nntp_newsrc_update - Update .newsrc file
435  * @param adata NNTP server
436  * @retval  0 Success
437  * @retval -1 Failure
438  */
nntp_newsrc_update(struct NntpAccountData * adata)439 int nntp_newsrc_update(struct NntpAccountData *adata)
440 {
441   if (!adata)
442     return -1;
443 
444   int rc = -1;
445 
446   size_t buflen = 10240;
447   char *buf = mutt_mem_calloc(1, buflen);
448   size_t off = 0;
449 
450   /* we will generate full newsrc here */
451   for (unsigned int i = 0; i < adata->groups_num; i++)
452   {
453     struct NntpMboxData *mdata = adata->groups_list[i];
454 
455     if (!mdata || !mdata->newsrc_ent)
456       continue;
457 
458     /* write newsgroup name */
459     if (off + strlen(mdata->group) + 3 > buflen)
460     {
461       buflen *= 2;
462       mutt_mem_realloc(&buf, buflen);
463     }
464     snprintf(buf + off, buflen - off, "%s%c ", mdata->group, mdata->subscribed ? ':' : '!');
465     off += strlen(buf + off);
466 
467     /* write entries */
468     for (unsigned int j = 0; j < mdata->newsrc_len; j++)
469     {
470       if (off + 1024 > buflen)
471       {
472         buflen *= 2;
473         mutt_mem_realloc(&buf, buflen);
474       }
475       if (j)
476         buf[off++] = ',';
477       if (mdata->newsrc_ent[j].first == mdata->newsrc_ent[j].last)
478         snprintf(buf + off, buflen - off, "%u", mdata->newsrc_ent[j].first);
479       else if (mdata->newsrc_ent[j].first < mdata->newsrc_ent[j].last)
480       {
481         snprintf(buf + off, buflen - off, "%u-%u", mdata->newsrc_ent[j].first,
482                  mdata->newsrc_ent[j].last);
483       }
484       off += strlen(buf + off);
485     }
486     buf[off++] = '\n';
487   }
488   buf[off] = '\0';
489 
490   /* newrc being fully rewritten */
491   mutt_debug(LL_DEBUG1, "Updating %s\n", adata->newsrc_file);
492   if (adata->newsrc_file && (update_file(adata->newsrc_file, buf) == 0))
493   {
494     struct stat st = { 0 };
495 
496     rc = stat(adata->newsrc_file, &st);
497     if (rc == 0)
498     {
499       adata->size = st.st_size;
500       adata->mtime = st.st_mtime;
501     }
502     else
503     {
504       mutt_perror(adata->newsrc_file);
505     }
506   }
507   FREE(&buf);
508   return rc;
509 }
510 
511 /**
512  * cache_expand - Make fully qualified cache file name
513  * @param dst    Buffer for filename
514  * @param dstlen Length of buffer
515  * @param cac    Account
516  * @param src    Path to add to the URL
517  */
cache_expand(char * dst,size_t dstlen,struct ConnAccount * cac,const char * src)518 static void cache_expand(char *dst, size_t dstlen, struct ConnAccount *cac, const char *src)
519 {
520   char *c = NULL;
521   char file[PATH_MAX];
522 
523   /* server subdirectory */
524   if (cac)
525   {
526     struct Url url = { 0 };
527 
528     mutt_account_tourl(cac, &url);
529     url.path = mutt_str_dup(src);
530     url_tostring(&url, file, sizeof(file), U_PATH);
531     FREE(&url.path);
532   }
533   else
534     mutt_str_copy(file, src ? src : "", sizeof(file));
535 
536   const char *const c_news_cache_dir =
537       cs_subset_path(NeoMutt->sub, "news_cache_dir");
538   snprintf(dst, dstlen, "%s/%s", c_news_cache_dir, file);
539 
540   /* remove trailing slash */
541   c = dst + strlen(dst) - 1;
542   if (*c == '/')
543     *c = '\0';
544 
545   struct Buffer *tmp = mutt_buffer_pool_get();
546   mutt_buffer_addstr(tmp, dst);
547   mutt_buffer_expand_path(tmp);
548   mutt_encode_path(tmp, dst);
549   mutt_str_copy(dst, mutt_buffer_string(tmp), dstlen);
550   mutt_buffer_pool_release(&tmp);
551 }
552 
553 /**
554  * nntp_expand_path - Make fully qualified url from newsgroup name
555  * @param buf    Buffer for the result
556  * @param buflen Length of buffer
557  * @param cac    Account to serialise
558  */
nntp_expand_path(char * buf,size_t buflen,struct ConnAccount * cac)559 void nntp_expand_path(char *buf, size_t buflen, struct ConnAccount *cac)
560 {
561   struct Url url = { 0 };
562 
563   mutt_account_tourl(cac, &url);
564   url.path = mutt_str_dup(buf);
565   url_tostring(&url, buf, buflen, U_NO_FLAGS);
566   FREE(&url.path);
567 }
568 
569 /**
570  * nntp_add_group - Parse newsgroup
571  * @param line String to parse
572  * @param data NNTP data
573  * @retval 0 Always
574  */
nntp_add_group(char * line,void * data)575 int nntp_add_group(char *line, void *data)
576 {
577   struct NntpAccountData *adata = data;
578   struct NntpMboxData *mdata = NULL;
579   char group[1024] = { 0 };
580   char desc[8192] = { 0 };
581   char mod;
582   anum_t first, last;
583 
584   if (!adata || !line)
585     return 0;
586 
587   /* These sscanf limits must match the sizes of the group and desc arrays */
588   if (sscanf(line, "%1023s " ANUM " " ANUM " %c %8191[^\n]", group, &last,
589              &first, &mod, desc) < 4)
590   {
591     mutt_debug(LL_DEBUG2, "Can't parse server line: %s\n", line);
592     return 0;
593   }
594 
595   mdata = mdata_find(adata, group);
596   mdata->deleted = false;
597   mdata->first_message = first;
598   mdata->last_message = last;
599   mdata->allowed = (mod == 'y') || (mod == 'm');
600   mutt_str_replace(&mdata->desc, desc);
601   if (mdata->newsrc_ent || (mdata->last_cached != 0))
602     nntp_group_unread_stat(mdata);
603   else if (mdata->last_message && (mdata->first_message <= mdata->last_message))
604     mdata->unread = mdata->last_message - mdata->first_message + 1;
605   else
606     mdata->unread = 0;
607   return 0;
608 }
609 
610 /**
611  * active_get_cache - Load list of all newsgroups from cache
612  * @param adata NNTP server
613  * @retval  0 Success
614  * @retval -1 Failure
615  */
active_get_cache(struct NntpAccountData * adata)616 static int active_get_cache(struct NntpAccountData *adata)
617 {
618   char buf[8192];
619   char file[4096];
620   time_t t;
621 
622   cache_expand(file, sizeof(file), &adata->conn->account, ".active");
623   mutt_debug(LL_DEBUG1, "Parsing %s\n", file);
624   FILE *fp = mutt_file_fopen(file, "r");
625   if (!fp)
626     return -1;
627 
628   if (!fgets(buf, sizeof(buf), fp) || (sscanf(buf, "%ld%4095s", &t, file) != 1) || (t == 0))
629   {
630     mutt_file_fclose(&fp);
631     return -1;
632   }
633   adata->newgroups_time = t;
634 
635   mutt_message(_("Loading list of groups from cache..."));
636   while (fgets(buf, sizeof(buf), fp))
637     nntp_add_group(buf, adata);
638   nntp_add_group(NULL, NULL);
639   mutt_file_fclose(&fp);
640   mutt_clear_error();
641   return 0;
642 }
643 
644 /**
645  * nntp_active_save_cache - Save list of all newsgroups to cache
646  * @param adata NNTP server
647  * @retval  0 Success
648  * @retval -1 Failure
649  */
nntp_active_save_cache(struct NntpAccountData * adata)650 int nntp_active_save_cache(struct NntpAccountData *adata)
651 {
652   if (!adata->cacheable)
653     return 0;
654 
655   size_t buflen = 10240;
656   char *buf = mutt_mem_calloc(1, buflen);
657   snprintf(buf, buflen, "%lu\n", (unsigned long) adata->newgroups_time);
658   size_t off = strlen(buf);
659 
660   for (unsigned int i = 0; i < adata->groups_num; i++)
661   {
662     struct NntpMboxData *mdata = adata->groups_list[i];
663 
664     if (!mdata || mdata->deleted)
665       continue;
666 
667     if ((off + strlen(mdata->group) + (mdata->desc ? strlen(mdata->desc) : 0) + 50) > buflen)
668     {
669       buflen *= 2;
670       mutt_mem_realloc(&buf, buflen);
671     }
672     snprintf(buf + off, buflen - off, "%s %u %u %c%s%s\n", mdata->group,
673              mdata->last_message, mdata->first_message, mdata->allowed ? 'y' : 'n',
674              mdata->desc ? " " : "", mdata->desc ? mdata->desc : "");
675     off += strlen(buf + off);
676   }
677 
678   char file[PATH_MAX];
679   cache_expand(file, sizeof(file), &adata->conn->account, ".active");
680   mutt_debug(LL_DEBUG1, "Updating %s\n", file);
681   int rc = update_file(file, buf);
682   FREE(&buf);
683   return rc;
684 }
685 
686 #ifdef USE_HCACHE
687 /**
688  * nntp_hcache_namer - Compose hcache file names - Implements ::hcache_namer_t - @ingroup hcache_namer_api
689  */
nntp_hcache_namer(const char * path,struct Buffer * dest)690 static void nntp_hcache_namer(const char *path, struct Buffer *dest)
691 {
692   mutt_buffer_printf(dest, "%s.hcache", path);
693 
694   /* Strip out any directories in the path */
695   char *first = strchr(mutt_buffer_string(dest), '/');
696   char *last = strrchr(mutt_buffer_string(dest), '/');
697   if (first && last && (last > first))
698   {
699     memmove(first, last, strlen(last) + 1);
700   }
701 }
702 
703 /**
704  * nntp_hcache_open - Open newsgroup hcache
705  * @param mdata NNTP Mailbox data
706  * @retval ptr  Header cache
707  * @retval NULL Error
708  */
nntp_hcache_open(struct NntpMboxData * mdata)709 struct HeaderCache *nntp_hcache_open(struct NntpMboxData *mdata)
710 {
711   struct Url url = { 0 };
712   char file[PATH_MAX];
713 
714   const bool c_save_unsubscribed =
715       cs_subset_bool(NeoMutt->sub, "save_unsubscribed");
716   if (!mdata->adata || !mdata->adata->cacheable || !mdata->adata->conn || !mdata->group ||
717       !(mdata->newsrc_ent || mdata->subscribed || c_save_unsubscribed))
718   {
719     return NULL;
720   }
721 
722   mutt_account_tourl(&mdata->adata->conn->account, &url);
723   url.path = mdata->group;
724   url_tostring(&url, file, sizeof(file), U_PATH);
725   const char *const c_news_cache_dir =
726       cs_subset_path(NeoMutt->sub, "news_cache_dir");
727   return mutt_hcache_open(c_news_cache_dir, file, nntp_hcache_namer);
728 }
729 
730 /**
731  * nntp_hcache_update - Remove stale cached headers
732  * @param mdata NNTP Mailbox data
733  * @param hc    Header cache
734  */
nntp_hcache_update(struct NntpMboxData * mdata,struct HeaderCache * hc)735 void nntp_hcache_update(struct NntpMboxData *mdata, struct HeaderCache *hc)
736 {
737   if (!hc)
738     return;
739 
740   char buf[32];
741   bool old = false;
742   anum_t first = 0, last = 0;
743 
744   /* fetch previous values of first and last */
745   size_t dlen = 0;
746   void *hdata = mutt_hcache_fetch_raw(hc, "index", 5, &dlen);
747   if (hdata)
748   {
749     mutt_debug(LL_DEBUG2, "mutt_hcache_fetch index: %s\n", (char *) hdata);
750     if (sscanf(hdata, ANUM " " ANUM, &first, &last) == 2)
751     {
752       old = true;
753       mdata->last_cached = last;
754 
755       /* clean removed headers from cache */
756       for (anum_t current = first; current <= last; current++)
757       {
758         if ((current >= mdata->first_message) && (current <= mdata->last_message))
759           continue;
760 
761         snprintf(buf, sizeof(buf), "%u", current);
762         mutt_debug(LL_DEBUG2, "mutt_hcache_delete_record %s\n", buf);
763         mutt_hcache_delete_record(hc, buf, strlen(buf));
764       }
765     }
766     mutt_hcache_free_raw(hc, &hdata);
767   }
768 
769   /* store current values of first and last */
770   if (!old || (mdata->first_message != first) || (mdata->last_message != last))
771   {
772     snprintf(buf, sizeof(buf), "%u %u", mdata->first_message, mdata->last_message);
773     mutt_debug(LL_DEBUG2, "mutt_hcache_store index: %s\n", buf);
774     mutt_hcache_store_raw(hc, "index", 5, buf, strlen(buf) + 1);
775   }
776 }
777 #endif
778 
779 /**
780  * nntp_bcache_delete - Remove bcache file - Implements ::bcache_list_t - @ingroup bcache_list_api
781  * @retval 0 Always
782  */
nntp_bcache_delete(const char * id,struct BodyCache * bcache,void * data)783 static int nntp_bcache_delete(const char *id, struct BodyCache *bcache, void *data)
784 {
785   struct NntpMboxData *mdata = data;
786   anum_t anum;
787   char c;
788 
789   if (!mdata || (sscanf(id, ANUM "%c", &anum, &c) != 1) ||
790       (anum < mdata->first_message) || (anum > mdata->last_message))
791   {
792     if (mdata)
793       mutt_debug(LL_DEBUG2, "mutt_bcache_del %s\n", id);
794     mutt_bcache_del(bcache, id);
795   }
796   return 0;
797 }
798 
799 /**
800  * nntp_bcache_update - Remove stale cached messages
801  * @param mdata NNTP Mailbox data
802  */
nntp_bcache_update(struct NntpMboxData * mdata)803 void nntp_bcache_update(struct NntpMboxData *mdata)
804 {
805   mutt_bcache_list(mdata->bcache, nntp_bcache_delete, mdata);
806 }
807 
808 /**
809  * nntp_delete_group_cache - Remove hcache and bcache of newsgroup
810  * @param mdata NNTP Mailbox data
811  */
nntp_delete_group_cache(struct NntpMboxData * mdata)812 void nntp_delete_group_cache(struct NntpMboxData *mdata)
813 {
814   if (!mdata || !mdata->adata || !mdata->adata->cacheable)
815     return;
816 
817 #ifdef USE_HCACHE
818   struct Buffer file = mutt_buffer_make(PATH_MAX);
819   nntp_hcache_namer(mdata->group, &file);
820   cache_expand(file.data, file.dsize, &mdata->adata->conn->account,
821                mutt_buffer_string(&file));
822   unlink(mutt_buffer_string(&file));
823   mdata->last_cached = 0;
824   mutt_debug(LL_DEBUG2, "%s\n", mutt_buffer_string(&file));
825   mutt_buffer_dealloc(&file);
826 #endif
827 
828   if (!mdata->bcache)
829   {
830     mdata->bcache = mutt_bcache_open(&mdata->adata->conn->account, mdata->group);
831   }
832   if (mdata->bcache)
833   {
834     mutt_debug(LL_DEBUG2, "%s/*\n", mdata->group);
835     mutt_bcache_list(mdata->bcache, nntp_bcache_delete, NULL);
836     mutt_bcache_close(&mdata->bcache);
837   }
838 }
839 
840 /**
841  * nntp_clear_cache - Clear the NNTP cache
842  * @param adata NNTP server
843  *
844  * Remove hcache and bcache of all unexistent and unsubscribed newsgroups
845  */
nntp_clear_cache(struct NntpAccountData * adata)846 void nntp_clear_cache(struct NntpAccountData *adata)
847 {
848   char file[PATH_MAX];
849   char *fp = NULL;
850   struct dirent *entry = NULL;
851   DIR *dp = NULL;
852 
853   if (!adata || !adata->cacheable)
854     return;
855 
856   cache_expand(file, sizeof(file), &adata->conn->account, NULL);
857   dp = opendir(file);
858   if (dp)
859   {
860     mutt_strn_cat(file, sizeof(file), "/", 1);
861     fp = file + strlen(file);
862     while ((entry = readdir(dp)))
863     {
864       char *group = entry->d_name;
865       struct stat st = { 0 };
866       struct NntpMboxData *mdata = NULL;
867       struct NntpMboxData tmp_mdata;
868 
869       if (mutt_str_equal(group, ".") || mutt_str_equal(group, ".."))
870         continue;
871       *fp = '\0';
872       mutt_strn_cat(file, sizeof(file), group, strlen(group));
873       if (stat(file, &st) != 0)
874         continue;
875 
876 #ifdef USE_HCACHE
877       if (S_ISREG(st.st_mode))
878       {
879         char *ext = group + strlen(group) - 7;
880         if ((strlen(group) < 8) || !mutt_str_equal(ext, ".hcache"))
881           continue;
882         *ext = '\0';
883       }
884       else
885 #endif
886           if (!S_ISDIR(st.st_mode))
887         continue;
888 
889       const bool c_save_unsubscribed =
890           cs_subset_bool(NeoMutt->sub, "save_unsubscribed");
891       mdata = mutt_hash_find(adata->groups_hash, group);
892       if (!mdata)
893       {
894         mdata = &tmp_mdata;
895         mdata->adata = adata;
896         mdata->group = group;
897         mdata->bcache = NULL;
898       }
899       else if (mdata->newsrc_ent || mdata->subscribed || c_save_unsubscribed)
900         continue;
901 
902       nntp_delete_group_cache(mdata);
903       if (S_ISDIR(st.st_mode))
904       {
905         rmdir(file);
906         mutt_debug(LL_DEBUG2, "%s\n", file);
907       }
908     }
909     closedir(dp);
910   }
911 }
912 
913 /**
914  * nntp_format_str - Expand the newsrc filename - Implements ::format_t - @ingroup expando_api
915  *
916  * | Expando | Description
917  * |:--------|:--------------------------------------------------------
918  * | \%a     | Account url
919  * | \%p     | Port
920  * | \%P     | Port if specified
921  * | \%s     | News server name
922  * | \%S     | Url schema
923  * | \%u     | Username
924  */
nntp_format_str(char * buf,size_t buflen,size_t col,int cols,char op,const char * src,const char * prec,const char * if_str,const char * else_str,intptr_t data,MuttFormatFlags flags)925 const char *nntp_format_str(char *buf, size_t buflen, size_t col, int cols, char op,
926                             const char *src, const char *prec, const char *if_str,
927                             const char *else_str, intptr_t data, MuttFormatFlags flags)
928 {
929   struct NntpAccountData *adata = (struct NntpAccountData *) data;
930   struct ConnAccount *cac = &adata->conn->account;
931   char fn[128], fmt[128];
932 
933   switch (op)
934   {
935     case 'a':
936     {
937       struct Url url = { 0 };
938       mutt_account_tourl(cac, &url);
939       url_tostring(&url, fn, sizeof(fn), U_PATH);
940       char *p = strchr(fn, '/');
941       if (p)
942         *p = '\0';
943       snprintf(fmt, sizeof(fmt), "%%%ss", prec);
944       snprintf(buf, buflen, fmt, fn);
945       break;
946     }
947     case 'p':
948       snprintf(fmt, sizeof(fmt), "%%%su", prec);
949       snprintf(buf, buflen, fmt, cac->port);
950       break;
951     case 'P':
952       *buf = '\0';
953       if (cac->flags & MUTT_ACCT_PORT)
954       {
955         snprintf(fmt, sizeof(fmt), "%%%su", prec);
956         snprintf(buf, buflen, fmt, cac->port);
957       }
958       break;
959     case 's':
960       mutt_str_copy(fn, cac->host, sizeof(fn));
961       mutt_str_lower(fn);
962       snprintf(fmt, sizeof(fmt), "%%%ss", prec);
963       snprintf(buf, buflen, fmt, fn);
964       break;
965     case 'S':
966     {
967       struct Url url = { 0 };
968       mutt_account_tourl(cac, &url);
969       url_tostring(&url, fn, sizeof(fn), U_PATH);
970       char *p = strchr(fn, ':');
971       if (p)
972         *p = '\0';
973       snprintf(fmt, sizeof(fmt), "%%%ss", prec);
974       snprintf(buf, buflen, fmt, fn);
975       break;
976     }
977     case 'u':
978       snprintf(fmt, sizeof(fmt), "%%%ss", prec);
979       snprintf(buf, buflen, fmt, cac->user);
980       break;
981   }
982   return src;
983 }
984 
985 /**
986  * nntp_get_field - Get connection login credentials - Implements ConnAccount::get_field()
987  */
nntp_get_field(enum ConnAccountField field,void * gf_data)988 static const char *nntp_get_field(enum ConnAccountField field, void *gf_data)
989 {
990   switch (field)
991   {
992     case MUTT_CA_LOGIN:
993     case MUTT_CA_USER:
994       return cs_subset_string(NeoMutt->sub, "nntp_user");
995     case MUTT_CA_PASS:
996       return cs_subset_string(NeoMutt->sub, "nntp_pass");
997     case MUTT_CA_OAUTH_CMD:
998     case MUTT_CA_HOST:
999     default:
1000       return NULL;
1001   }
1002 }
1003 
1004 /**
1005  * nntp_select_server - Open a connection to an NNTP server
1006  * @param m          Mailbox
1007  * @param server     Server URL
1008  * @param leave_lock Leave the server locked?
1009  * @retval ptr  NNTP server
1010  * @retval NULL Error
1011  *
1012  * Automatically loads a newsrc into memory, if necessary.  Checks the
1013  * size/mtime of a newsrc file, if it doesn't match, load again.  Hmm, if a
1014  * system has broken mtimes, this might mean the file is reloaded every time,
1015  * which we'd have to fix.
1016  */
nntp_select_server(struct Mailbox * m,const char * server,bool leave_lock)1017 struct NntpAccountData *nntp_select_server(struct Mailbox *m, const char *server, bool leave_lock)
1018 {
1019   char file[PATH_MAX];
1020   int rc;
1021   struct ConnAccount cac = { { 0 } };
1022   struct NntpAccountData *adata = NULL;
1023   struct Connection *conn = NULL;
1024 
1025   if (!server || (*server == '\0'))
1026   {
1027     mutt_error(_("No news server defined"));
1028     return NULL;
1029   }
1030 
1031   /* create account from news server url */
1032   cac.flags = 0;
1033   cac.port = NNTP_PORT;
1034   cac.type = MUTT_ACCT_TYPE_NNTP;
1035   cac.service = "nntp";
1036   cac.get_field = nntp_get_field;
1037 
1038   snprintf(file, sizeof(file), "%s%s", strstr(server, "://") ? "" : "news://", server);
1039   struct Url *url = url_parse(file);
1040   if (!url || (url->path && *url->path) ||
1041       !((url->scheme == U_NNTP) || (url->scheme == U_NNTPS)) || !url->host ||
1042       (mutt_account_fromurl(&cac, url) < 0))
1043   {
1044     url_free(&url);
1045     mutt_error(_("%s is an invalid news server specification"), server);
1046     return NULL;
1047   }
1048   if (url->scheme == U_NNTPS)
1049   {
1050     cac.flags |= MUTT_ACCT_SSL;
1051     cac.port = NNTP_SSL_PORT;
1052   }
1053   url_free(&url);
1054 
1055   /* find connection by account */
1056   conn = mutt_conn_find(&cac);
1057   if (!conn)
1058     return NULL;
1059   if (!(conn->account.flags & MUTT_ACCT_USER) && cac.flags & MUTT_ACCT_USER)
1060   {
1061     conn->account.flags |= MUTT_ACCT_USER;
1062     conn->account.user[0] = '\0';
1063   }
1064 
1065   /* news server already exists */
1066   // adata = conn->data;
1067   if (adata)
1068   {
1069     if (adata->status == NNTP_BYE)
1070       adata->status = NNTP_NONE;
1071     if (nntp_open_connection(adata) < 0)
1072       return NULL;
1073 
1074     rc = nntp_newsrc_parse(adata);
1075     if (rc < 0)
1076       return NULL;
1077 
1078     /* check for new newsgroups */
1079     if (!leave_lock && (nntp_check_new_groups(m, adata) < 0))
1080       rc = -1;
1081 
1082     /* .newsrc has been externally modified */
1083     if (rc > 0)
1084       nntp_clear_cache(adata);
1085     if ((rc < 0) || !leave_lock)
1086       nntp_newsrc_close(adata);
1087     return (rc < 0) ? NULL : adata;
1088   }
1089 
1090   /* new news server */
1091   adata = nntp_adata_new(conn);
1092 
1093   rc = nntp_open_connection(adata);
1094 
1095   /* try to create cache directory and enable caching */
1096   adata->cacheable = false;
1097   const char *const c_news_cache_dir =
1098       cs_subset_path(NeoMutt->sub, "news_cache_dir");
1099   if ((rc >= 0) && c_news_cache_dir)
1100   {
1101     cache_expand(file, sizeof(file), &conn->account, NULL);
1102     if (mutt_file_mkdir(file, S_IRWXU) < 0)
1103     {
1104       mutt_error(_("Can't create %s: %s"), file, strerror(errno));
1105     }
1106     adata->cacheable = true;
1107   }
1108 
1109   /* load .newsrc */
1110   if (rc >= 0)
1111   {
1112     const char *const c_newsrc = cs_subset_path(NeoMutt->sub, "newsrc");
1113     mutt_expando_format(file, sizeof(file), 0, sizeof(file), NONULL(c_newsrc),
1114                         nntp_format_str, (intptr_t) adata, MUTT_FORMAT_NO_FLAGS);
1115     mutt_expand_path(file, sizeof(file));
1116     adata->newsrc_file = mutt_str_dup(file);
1117     rc = nntp_newsrc_parse(adata);
1118   }
1119   if (rc >= 0)
1120   {
1121     /* try to load list of newsgroups from cache */
1122     if (adata->cacheable && (active_get_cache(adata) == 0))
1123       rc = nntp_check_new_groups(m, adata);
1124 
1125     /* load list of newsgroups from server */
1126     else
1127       rc = nntp_active_fetch(adata, false);
1128   }
1129 
1130   if (rc >= 0)
1131     nntp_clear_cache(adata);
1132 
1133 #ifdef USE_HCACHE
1134   /* check cache files */
1135   if ((rc >= 0) && adata->cacheable)
1136   {
1137     struct dirent *entry = NULL;
1138     DIR *dp = opendir(file);
1139 
1140     if (dp)
1141     {
1142       while ((entry = readdir(dp)))
1143       {
1144         struct HeaderCache *hc = NULL;
1145         void *hdata = NULL;
1146         char *group = entry->d_name;
1147 
1148         char *p = group + strlen(group) - 7;
1149         if ((strlen(group) < 8) || (strcmp(p, ".hcache") != 0))
1150           continue;
1151         *p = '\0';
1152         struct NntpMboxData *mdata = mutt_hash_find(adata->groups_hash, group);
1153         if (!mdata)
1154           continue;
1155 
1156         hc = nntp_hcache_open(mdata);
1157         if (!hc)
1158           continue;
1159 
1160         /* fetch previous values of first and last */
1161         size_t dlen = 0;
1162         hdata = mutt_hcache_fetch_raw(hc, "index", 5, &dlen);
1163         if (hdata)
1164         {
1165           anum_t first, last;
1166 
1167           if (sscanf(hdata, ANUM " " ANUM, &first, &last) == 2)
1168           {
1169             if (mdata->deleted)
1170             {
1171               mdata->first_message = first;
1172               mdata->last_message = last;
1173             }
1174             if ((last >= mdata->first_message) && (last <= mdata->last_message))
1175             {
1176               mdata->last_cached = last;
1177               mutt_debug(LL_DEBUG2, "%s last_cached=%u\n", mdata->group, last);
1178             }
1179           }
1180           mutt_hcache_free_raw(hc, &hdata);
1181         }
1182         mutt_hcache_close(hc);
1183       }
1184       closedir(dp);
1185     }
1186   }
1187 #endif
1188 
1189   if ((rc < 0) || !leave_lock)
1190     nntp_newsrc_close(adata);
1191 
1192   if (rc < 0)
1193   {
1194     mutt_hash_free(&adata->groups_hash);
1195     FREE(&adata->groups_list);
1196     FREE(&adata->newsrc_file);
1197     FREE(&adata->authenticators);
1198     FREE(&adata);
1199     mutt_socket_close(conn);
1200     FREE(&conn);
1201     return NULL;
1202   }
1203 
1204   return adata;
1205 }
1206 
1207 /**
1208  * nntp_article_status - Get status of articles from .newsrc
1209  * @param m       Mailbox
1210  * @param e       Email
1211  * @param group   Newsgroup
1212  * @param anum    Article number
1213  *
1214  * Full status flags are not supported by nntp, but we can fake some of them:
1215  * Read = a read message number is in the .newsrc
1216  * New = not read and not cached
1217  * Old = not read but cached
1218  */
nntp_article_status(struct Mailbox * m,struct Email * e,char * group,anum_t anum)1219 void nntp_article_status(struct Mailbox *m, struct Email *e, char *group, anum_t anum)
1220 {
1221   struct NntpMboxData *mdata = m->mdata;
1222 
1223   if (group)
1224     mdata = mutt_hash_find(mdata->adata->groups_hash, group);
1225 
1226   if (!mdata)
1227     return;
1228 
1229   for (unsigned int i = 0; i < mdata->newsrc_len; i++)
1230   {
1231     if ((anum >= mdata->newsrc_ent[i].first) && (anum <= mdata->newsrc_ent[i].last))
1232     {
1233       /* can't use mutt_set_flag() because ctx_update() didn't get called yet */
1234       e->read = true;
1235       return;
1236     }
1237   }
1238 
1239   /* article was not cached yet, it's new */
1240   if (anum > mdata->last_cached)
1241     return;
1242 
1243   /* article isn't read but cached, it's old */
1244   const bool c_mark_old = cs_subset_bool(NeoMutt->sub, "mark_old");
1245   if (c_mark_old)
1246     e->old = true;
1247 }
1248 
1249 /**
1250  * mutt_newsgroup_subscribe - Subscribe newsgroup
1251  * @param adata NNTP server
1252  * @param group Newsgroup
1253  * @retval ptr  NNTP data
1254  * @retval NULL Error
1255  */
mutt_newsgroup_subscribe(struct NntpAccountData * adata,char * group)1256 struct NntpMboxData *mutt_newsgroup_subscribe(struct NntpAccountData *adata, char *group)
1257 {
1258   if (!adata || !adata->groups_hash || !group || (*group == '\0'))
1259     return NULL;
1260 
1261   struct NntpMboxData *mdata = mdata_find(adata, group);
1262   mdata->subscribed = true;
1263   if (!mdata->newsrc_ent)
1264   {
1265     mdata->newsrc_ent = mutt_mem_calloc(1, sizeof(struct NewsrcEntry));
1266     mdata->newsrc_len = 1;
1267     mdata->newsrc_ent[0].first = 1;
1268     mdata->newsrc_ent[0].last = 0;
1269   }
1270   return mdata;
1271 }
1272 
1273 /**
1274  * mutt_newsgroup_unsubscribe - Unsubscribe newsgroup
1275  * @param adata NNTP server
1276  * @param group Newsgroup
1277  * @retval ptr  NNTP data
1278  * @retval NULL Error
1279  */
mutt_newsgroup_unsubscribe(struct NntpAccountData * adata,char * group)1280 struct NntpMboxData *mutt_newsgroup_unsubscribe(struct NntpAccountData *adata, char *group)
1281 {
1282   if (!adata || !adata->groups_hash || !group || (*group == '\0'))
1283     return NULL;
1284 
1285   struct NntpMboxData *mdata = mutt_hash_find(adata->groups_hash, group);
1286   if (!mdata)
1287     return NULL;
1288 
1289   mdata->subscribed = false;
1290   const bool c_save_unsubscribed =
1291       cs_subset_bool(NeoMutt->sub, "save_unsubscribed");
1292   if (!c_save_unsubscribed)
1293   {
1294     mdata->newsrc_len = 0;
1295     FREE(&mdata->newsrc_ent);
1296   }
1297   return mdata;
1298 }
1299 
1300 /**
1301  * mutt_newsgroup_catchup - Catchup newsgroup
1302  * @param m     Mailbox
1303  * @param adata NNTP server
1304  * @param group Newsgroup
1305  * @retval ptr  NNTP data
1306  * @retval NULL Error
1307  */
mutt_newsgroup_catchup(struct Mailbox * m,struct NntpAccountData * adata,char * group)1308 struct NntpMboxData *mutt_newsgroup_catchup(struct Mailbox *m,
1309                                             struct NntpAccountData *adata, char *group)
1310 {
1311   if (!adata || !adata->groups_hash || !group || (*group == '\0'))
1312     return NULL;
1313 
1314   struct NntpMboxData *mdata = mutt_hash_find(adata->groups_hash, group);
1315   if (!mdata)
1316     return NULL;
1317 
1318   if (mdata->newsrc_ent)
1319   {
1320     mutt_mem_realloc(&mdata->newsrc_ent, sizeof(struct NewsrcEntry));
1321     mdata->newsrc_len = 1;
1322     mdata->newsrc_ent[0].first = 1;
1323     mdata->newsrc_ent[0].last = mdata->last_message;
1324   }
1325   mdata->unread = 0;
1326   if (m && (m->mdata == mdata))
1327   {
1328     for (unsigned int i = 0; i < m->msg_count; i++)
1329     {
1330       struct Email *e = m->emails[i];
1331       if (!e)
1332         break;
1333       mutt_set_flag(m, e, MUTT_READ, true);
1334     }
1335   }
1336   return mdata;
1337 }
1338 
1339 /**
1340  * mutt_newsgroup_uncatchup - Uncatchup newsgroup
1341  * @param m     Mailbox
1342  * @param adata NNTP server
1343  * @param group Newsgroup
1344  * @retval ptr  NNTP data
1345  * @retval NULL Error
1346  */
mutt_newsgroup_uncatchup(struct Mailbox * m,struct NntpAccountData * adata,char * group)1347 struct NntpMboxData *mutt_newsgroup_uncatchup(struct Mailbox *m,
1348                                               struct NntpAccountData *adata, char *group)
1349 {
1350   if (!adata || !adata->groups_hash || !group || (*group == '\0'))
1351     return NULL;
1352 
1353   struct NntpMboxData *mdata = mutt_hash_find(adata->groups_hash, group);
1354   if (!mdata)
1355     return NULL;
1356 
1357   if (mdata->newsrc_ent)
1358   {
1359     mutt_mem_realloc(&mdata->newsrc_ent, sizeof(struct NewsrcEntry));
1360     mdata->newsrc_len = 1;
1361     mdata->newsrc_ent[0].first = 1;
1362     mdata->newsrc_ent[0].last = mdata->first_message - 1;
1363   }
1364   if (m && (m->mdata == mdata))
1365   {
1366     mdata->unread = m->msg_count;
1367     for (unsigned int i = 0; i < m->msg_count; i++)
1368     {
1369       struct Email *e = m->emails[i];
1370       if (!e)
1371         break;
1372       mutt_set_flag(m, e, MUTT_READ, false);
1373     }
1374   }
1375   else
1376   {
1377     mdata->unread = mdata->last_message;
1378     if (mdata->newsrc_ent)
1379       mdata->unread -= mdata->newsrc_ent[0].last;
1380   }
1381   return mdata;
1382 }
1383 
1384 /**
1385  * nntp_mailbox - Get first newsgroup with new messages
1386  * @param m       Mailbox
1387  * @param buf     Buffer for result
1388  * @param buflen  Length of buffer
1389  */
nntp_mailbox(struct Mailbox * m,char * buf,size_t buflen)1390 void nntp_mailbox(struct Mailbox *m, char *buf, size_t buflen)
1391 {
1392   if (!m)
1393     return;
1394 
1395   for (unsigned int i = 0; i < CurrentNewsSrv->groups_num; i++)
1396   {
1397     struct NntpMboxData *mdata = CurrentNewsSrv->groups_list[i];
1398 
1399     if (!mdata || !mdata->subscribed || !mdata->unread)
1400       continue;
1401 
1402     if ((m->type == MUTT_NNTP) &&
1403         mutt_str_equal(mdata->group, ((struct NntpMboxData *) m->mdata)->group))
1404     {
1405       unsigned int unread = 0;
1406 
1407       for (unsigned int j = 0; j < m->msg_count; j++)
1408       {
1409         struct Email *e = m->emails[j];
1410         if (!e)
1411           break;
1412         if (!e->read && !e->deleted)
1413           unread++;
1414       }
1415       if (unread == 0)
1416         continue;
1417     }
1418     mutt_str_copy(buf, mdata->group, buflen);
1419     break;
1420   }
1421 }
1422