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