32df8520a902d0256ee76d03a2ede337df099b1d
[apps/madmutt.git] / nntp / newsrc.c
1 /*
2  * Copyright notice from original mutt:
3  * Copyright (C) 1998 Brandon Long <blong@fiction.net>
4  * Copyright (C) 1999 Andrej Gritsenko <andrej@lucky.net>
5  * Copyright (C) 2000-2001 Vsevolod Volkov <vvv@mutt.org.ua>
6  *
7  * This file is part of mutt-ng, see http://www.muttng.org/.
8  * It's licensed under the GNU General Public License,
9  * please see the file GPL in the top level source directory.
10  */
11
12 #ifdef HAVE_CONFIG_H
13 #include <config.h>
14 #endif
15
16 #include "mutt.h"
17 #include "mutt_curses.h"
18 #include "sort.h"
19 #include "mx.h"
20 #include "mime.h"
21 #include "nntp.h"
22 #include "rfc822.h"
23 #include "rfc1524.h"
24 #include "rfc2047.h"
25
26 #include "lib/mem.h"
27 #include "lib/str.h"
28 #include "lib/intl.h"
29
30 #include <unistd.h>
31 #include <string.h>
32 #include <ctype.h>
33 #include <stdlib.h>
34 #include <sys/stat.h>
35
36 void nntp_add_to_list (NNTP_SERVER * s, NNTP_DATA * d)
37 {
38   LIST *l;
39
40   if (!s || !d)
41     return;
42
43   l = safe_calloc (1, sizeof (LIST));
44   if (s->list)
45     s->tail->next = l;
46   else
47     s->list = l;
48   s->tail = l;
49   l->data = (void *) d;
50 }
51
52 static int nntp_parse_newsrc_line (NNTP_SERVER * news, char *line)
53 {
54   NNTP_DATA *data;
55   char group[LONG_STRING];
56   int x = 1;
57   char *p = line, *b, *h;
58   size_t len;
59
60   while (*p) {
61     if (*p++ == ',')
62       x++;
63   }
64
65   p = line;
66   while (*p && (*p != ':' && *p != '!'))
67     p++;
68   if (!*p)
69     return -1;
70   len = p + 1 - line;
71   if (len > sizeof (group))
72     len = sizeof (group);
73   strfcpy (group, line, len);
74   if ((data = (NNTP_DATA *) hash_find (news->newsgroups, group)) == NULL) {
75     data =
76       (NNTP_DATA *) safe_calloc (1, sizeof (NNTP_DATA) + safe_strlen (group) + 1);
77     data->group = (char *) data + sizeof (NNTP_DATA);
78     strcpy (data->group, group);
79     data->nserv = news;
80     data->deleted = 1;
81     if (news->newsgroups->nelem < news->newsgroups->curnelem * 2)
82       news->newsgroups =
83         hash_resize (news->newsgroups, news->newsgroups->nelem * 2);
84     hash_insert (news->newsgroups, data->group, data, 0);
85     nntp_add_to_list (news, data);
86   }
87   else
88     FREE ((void **) &data->entries);
89
90   data->rc = 1;
91   data->entries = safe_calloc (x * 2, sizeof (NEWSRC_ENTRY));
92   data->max = x * 2;
93
94   if (*p == ':')
95     data->subscribed = 1;
96   else
97     data->subscribed = 0;
98
99   p++;
100   b = p;
101   x = 0;
102   while (*b) {
103     while (*p && *p != ',' && *p != '\n')
104       p++;
105     if (*p) {
106       *p = '\0';
107       p++;
108     }
109     if ((h = strchr (b, '-'))) {
110       *h = '\0';
111       h++;
112       data->entries[x].first = atoi (b);
113       data->entries[x].last = atoi (h);
114     }
115     else {
116       data->entries[x].first = atoi (b);
117       data->entries[x].last = data->entries[x].first;
118     }
119     b = p;
120     if (data->entries[x].last != 0)
121       x++;
122   }
123   if (x && !data->lastMessage)
124     data->lastMessage = data->entries[x - 1].last;
125   data->num = x;
126   mutt_newsgroup_stat (data);
127   dprint (2, (debugfile, "parse_line: Newsgroup %s\n", data->group));
128
129   return 0;
130 }
131
132 static int slurp_newsrc (NNTP_SERVER * news)
133 {
134   FILE *fp;
135   char *buf;
136   struct stat sb;
137
138   news->stat = stat (news->newsrc, &sb);
139   news->size = sb.st_size;
140   news->mtime = sb.st_mtime;
141
142   if ((fp = safe_fopen (news->newsrc, "r")) == NULL)
143     return -1;
144   /* hmm, should we use dotlock? */
145   if (mx_lock_file (news->newsrc, fileno (fp), 0, 0, 1)) {
146     fclose (fp);
147     return -1;
148   }
149
150   buf = safe_malloc (sb.st_size + 1);
151   while (fgets (buf, sb.st_size + 1, fp))
152     nntp_parse_newsrc_line (news, buf);
153   FREE (&buf);
154
155   mx_unlock_file (news->newsrc, fileno (fp), 0);
156   fclose (fp);
157   return 0;
158 }
159
160 void nntp_cache_expand (char *dst, const char *src)
161 {
162   snprintf (dst, _POSIX_PATH_MAX, "%s/%s", NewsCacheDir, src);
163   mutt_expand_path (dst, _POSIX_PATH_MAX);
164 }
165
166 /* Loads $news_cache_dir/.index into memory, loads newsserver data
167  * and newsgroup cache names */
168 static int nntp_parse_cacheindex (NNTP_SERVER * news)
169 {
170   struct stat st;
171   char buf[HUGE_STRING], *cp;
172   char dir[_POSIX_PATH_MAX], file[_POSIX_PATH_MAX];
173   FILE *index;
174   NNTP_DATA *data;
175   int l, m, t;
176
177   /* check is server name defined or not */
178   if (!news || !news->conn || !news->conn->account.host)
179     return -1;
180   unset_option (OPTNEWSCACHE);
181   if (!NewsCacheDir || !*NewsCacheDir)
182     return 0;
183
184   strfcpy (dir, NewsCacheDir, sizeof (dir));
185   mutt_expand_path (dir, sizeof (dir));
186
187   if (lstat (dir, &st) || (st.st_mode & S_IFDIR) == 0) {
188     snprintf (buf, sizeof (buf), _("Directory %s not exist. Create it?"),
189               dir);
190     if (mutt_yesorno (buf, M_YES) != M_YES
191         || mkdir (dir, (S_IRWXU + S_IRWXG + S_IRWXO))) {
192       mutt_error _("Cache directory not created!");
193
194       return -1;
195     }
196     mutt_clear_error ();
197   }
198
199   set_option (OPTNEWSCACHE);
200
201   FREE (&news->cache);
202   snprintf (buf, sizeof (buf), "%s/.index", dir);
203   if (!(index = safe_fopen (buf, "a+")))
204     return 0;
205   rewind (index);
206   while (fgets (buf, sizeof (buf), index)) {
207     buf[safe_strlen (buf) - 1] = 0;  /* strip ending '\n' */
208     if (!safe_strncmp (buf, "#: ", 3) &&
209         !safe_strcasecmp (buf + 3, news->conn->account.host))
210       break;
211   }
212   while (fgets (buf, sizeof (buf), index)) {
213     cp = buf;
214     while (*cp && *cp != ' ')
215       cp++;
216     if (!*cp)
217       continue;
218     cp[0] = 0;
219     if (!safe_strcmp (buf, "#:"))
220       break;
221     sscanf (cp + 1, "%s %d %d", file, &l, &m);
222     if (!safe_strcmp (buf, "ALL")) {
223       news->cache = safe_strdup (file);
224       news->newgroups_time = m;
225     }
226     else if (news->newsgroups) {
227       if ((data = (NNTP_DATA *) hash_find (news->newsgroups, buf)) == NULL) {
228         data =
229           (NNTP_DATA *) safe_calloc (1,
230                                      sizeof (NNTP_DATA) + safe_strlen (buf) + 1);
231         data->group = (char *) data + sizeof (NNTP_DATA);
232         strcpy (data->group, buf);
233         data->nserv = news;
234         data->deleted = 1;
235         if (news->newsgroups->nelem < news->newsgroups->curnelem * 2)
236           news->newsgroups =
237             hash_resize (news->newsgroups, news->newsgroups->nelem * 2);
238         hash_insert (news->newsgroups, data->group, data, 0);
239         nntp_add_to_list (news, data);
240       }
241       data->cache = safe_strdup (file);
242       t = 0;
243       if (!data->firstMessage || data->lastMessage < m)
244         t = 1;
245       if (!data->firstMessage)
246         data->firstMessage = l;
247       if (data->lastMessage < m)
248         data->lastMessage = m;
249       data->lastCached = m;
250       if (t || !data->unread)
251         mutt_newsgroup_stat (data);
252     }
253   }
254   fclose (index);
255   return 0;
256 }
257
258 const char *nntp_format_str (char *dest, size_t destlen, char op,
259                              const char *src, const char *fmt,
260                              const char *ifstring, const char *elsestring,
261                              unsigned long data, format_flag flags)
262 {
263   char fn[SHORT_STRING], tmp[SHORT_STRING];
264
265   switch (op) {
266   case 's':
267     strncpy (fn, NewsServer, sizeof (fn) - 1);
268     str_tolower (fn);
269     snprintf (tmp, sizeof (tmp), "%%%ss", fmt);
270     snprintf (dest, destlen, tmp, fn);
271     break;
272   }
273   return (src);
274 }
275
276 /* nntp_parse_url: given an NNPT URL, return host, port,
277  * username, password and newsgroup will recognise. */
278 int nntp_parse_url (const char *server, ACCOUNT * acct,
279                     char *group, size_t group_len)
280 {
281   ciss_url_t url;
282   char *c;
283   int ret = -1;
284
285   /* Defaults */
286   acct->flags = 0;
287   acct->port = NNTP_PORT;
288   acct->type = M_ACCT_TYPE_NNTP;
289
290   c = safe_strdup (server);
291   url_parse_ciss (&url, c);
292
293   if (url.scheme == U_NNTP || url.scheme == U_NNTPS) {
294     if (url.scheme == U_NNTPS) {
295       acct->flags |= M_ACCT_SSL;
296       acct->port = NNTP_SSL_PORT;
297     }
298
299     *group = '\0';
300     if (url.path)
301       strfcpy (group, url.path, group_len);
302
303     ret = mutt_account_fromurl (acct, &url);
304   }
305
306   FREE (&c);
307   return ret;
308 }
309
310 void nntp_expand_path (char *line, size_t len, ACCOUNT * acct)
311 {
312   ciss_url_t url;
313
314   url.path = safe_strdup (line);
315   mutt_account_tourl (acct, &url);
316   url_ciss_tostring (&url, line, len, 0);
317   FREE (&url.path);
318 }
319
320 /*
321  * Automatically loads a newsrc into memory, if necessary.
322  * Checks the size/mtime of a newsrc file, if it doesn't match, load
323  * again.  Hmm, if a system has broken mtimes, this might mean the file
324  * is reloaded every time, which we'd have to fix.
325  *
326  * a newsrc file is a line per newsgroup, with the newsgroup, then a 
327  * ':' denoting subscribed or '!' denoting unsubscribed, then a 
328  * comma separated list of article numbers and ranges.
329  */
330 NNTP_SERVER *mutt_select_newsserver (char *server)
331 {
332   char file[_POSIX_PATH_MAX];
333   char *buf, *p;
334   LIST *list;
335   ACCOUNT acct;
336   NNTP_SERVER *serv;
337   CONNECTION *conn;
338
339   if (!server || !*server) {
340     mutt_error _("No newsserver defined!");
341
342     return NULL;
343   }
344
345   buf = p = safe_calloc (safe_strlen (server) + 10, sizeof (char));
346   if (url_check_scheme (server) == U_UNKNOWN) {
347     strcpy (buf, "nntp://");
348     p = strchr (buf, '\0');
349   }
350   strcpy (p, server);
351
352   if ((nntp_parse_url (buf, &acct, file, sizeof (file))) < 0 || *file) {
353     FREE (&buf);
354     mutt_error (_("%s is an invalid newsserver specification!"), server);
355     return NULL;
356   }
357   FREE (&buf);
358
359   conn = mutt_conn_find (NULL, &acct);
360   if (!conn)
361     return NULL;
362
363   mutt_FormatString (file, sizeof (file), NONULL (NewsRc), nntp_format_str, 0,
364                      0);
365   mutt_expand_path (file, sizeof (file));
366
367   serv = (NNTP_SERVER *) conn->data;
368   if (serv) {
369     struct stat sb;
370
371     /* externally modified? */
372     if (serv->stat != stat (file, &sb) || (!serv->stat &&
373                                            (serv->size != sb.st_size
374                                             || serv->mtime != sb.st_mtime))) {
375       for (list = serv->list; list; list = list->next) {
376         NNTP_DATA *data = (NNTP_DATA *) list->data;
377
378         if (data) {
379           data->subscribed = 0;
380           data->rc = 0;
381           data->num = 0;
382         }
383       }
384       slurp_newsrc (serv);
385       nntp_clear_cacheindex (serv);
386     }
387
388     if (serv->status == NNTP_BYE)
389       serv->status = NNTP_NONE;
390     nntp_check_newgroups (serv, 0);
391     return serv;
392   }
393
394   /* New newsserver */
395   serv = safe_calloc (1, sizeof (NNTP_SERVER));
396   serv->conn = conn;
397   serv->newsrc = safe_strdup (file);
398   serv->newsgroups = hash_create (1009);
399   slurp_newsrc (serv);          /* load .newsrc */
400   nntp_parse_cacheindex (serv); /* load .index */
401   if (option (OPTNEWSCACHE) && serv->cache && nntp_get_cache_all (serv) >= 0)
402     nntp_check_newgroups (serv, 1);
403   else if (nntp_get_active (serv) < 0) {
404     hash_destroy (&serv->newsgroups, nntp_delete_data);
405     for (list = serv->list; list; list = list->next)
406       list->data = NULL;
407     mutt_free_list (&serv->list);
408     FREE (&serv->newsrc);
409     FREE (&serv->cache);
410     FREE (&serv);
411     return NULL;
412   }
413   nntp_clear_cacheindex (serv);
414   conn->data = (void *) serv;
415
416   return serv;
417 }
418
419 /* 
420  * full status flags are not supported by nntp, but we can fake some
421  * of them.  This is how:
422  * Read = a read message number is in the .newsrc
423  * New = a message is new since we last read this newsgroup
424  * Old = anything else
425  * So, Read is marked as such in the newsrc, old is anything that is 
426  * "skipped" in the newsrc, and new is anything not in the newsrc nor
427  * in the cache. By skipped, I mean before the last unread message
428  */
429 void nntp_get_status (CONTEXT * ctx, HEADER * h, char *group, int article)
430 {
431   NNTP_DATA *data = (NNTP_DATA *) ctx->data;
432   int x;
433
434   if (group)
435     data = (NNTP_DATA *) hash_find (data->nserv->newsgroups, group);
436
437   if (!data) {
438 #ifdef DEBUG
439     if (group)
440       dprint (3, (debugfile, "newsgroup %s not found\n", group));
441 #endif
442     return;
443   }
444
445   for (x = 0; x < data->num; x++) {
446     if ((article >= data->entries[x].first) &&
447         (article <= data->entries[x].last)) {
448       /* we cannot use mutt_set_flag() because mx_update_context()
449          didn't called yet */
450       h->read = 1;
451       return;
452     }
453   }
454   /* If article was not cached yet, it is new! :) */
455   if (!data->cache || article > data->lastCached)
456     return;
457   /* Old articles are articles which aren't read but an article after them
458    * has been cached */
459   if (option (OPTMARKOLD))
460     h->old = 1;
461 }
462
463 void mutt_newsgroup_stat (NNTP_DATA * data)
464 {
465   int i;
466   unsigned int first, last;
467
468   data->unread = 0;
469   if (data->lastMessage == 0 || data->firstMessage > data->lastMessage)
470     return;
471
472   data->unread = data->lastMessage - data->firstMessage + 1;
473   for (i = 0; i < data->num; i++) {
474     first = data->entries[i].first;
475     if (first < data->firstMessage)
476       first = data->firstMessage;
477     last = data->entries[i].last;
478     if (last > data->lastMessage)
479       last = data->lastMessage;
480     if (first <= last)
481       data->unread -= last - first + 1;
482   }
483 }
484
485 static int puti (char *line, int num)
486 {
487   char *p, s[32];
488
489   for (p = s; num;) {
490     *p++ = '0' + num % 10;
491     num /= 10;
492   }
493   while (p > s)
494     *line++ = *--p, num++;
495   *line = '\0';
496   return num;
497 }
498
499 static void nntp_create_newsrc_line (NNTP_DATA * data, char **buf,
500                                      char **pline, size_t * buflen)
501 {
502   char *line = *pline;
503   size_t len = *buflen - (*pline - *buf);
504   int x, i;
505
506   if (len < LONG_STRING * 10) {
507     len += *buflen;
508     *buflen *= 2;
509     line = *buf;
510     safe_realloc (buf, *buflen);
511     line = *buf + (*pline - line);
512   }
513   strcpy (line, data->group);
514   len -= safe_strlen (line) + 1;
515   line += safe_strlen (line);
516   *line++ = data->subscribed ? ':' : '!';
517   *line++ = ' ';
518   *line = '\0';
519
520   for (x = 0; x < data->num; x++) {
521     if (len < LONG_STRING) {
522       len += *buflen;
523       *buflen *= 2;
524       *pline = line;
525       line = *buf;
526       safe_realloc (buf, *buflen);
527       line = *buf + (*pline - line);
528     }
529     if (x) {
530       *line++ = ',';
531       len--;
532     }
533
534 #if 0
535     if (data->entries[x].first == data->entries[x].last)
536       snprintf (line, len, "%d%n", data->entries[x].first, &i);
537     else
538       snprintf (line, len, "%d-%d%n",
539                 data->entries[x].first, data->entries[x].last, &i);
540     len -= i;
541     line += i;
542 #else
543     i = puti (line, data->entries[x].first);
544     line += i;
545     len -= i;
546     if (data->entries[x].first != data->entries[x].last) {
547       *line++ = '-';
548       len--;
549       i = puti (line, data->entries[x].last);
550       line += i;
551       len -= i;
552     }
553 #endif
554   }
555   *line++ = '\n';
556   *line = '\0';
557   *pline = line;
558 }
559
560 void newsrc_gen_entries (CONTEXT * ctx)
561 {
562   NNTP_DATA *data = (NNTP_DATA *) ctx->data;
563   int series, x;
564   unsigned int last = 0, first = 1;
565   int save_sort = SORT_ORDER;
566
567   if (Sort != SORT_ORDER) {
568     save_sort = Sort;
569     Sort = SORT_ORDER;
570     mutt_sort_headers (ctx, 0);
571   }
572
573   if (!data->max) {
574     data->entries = safe_calloc (5, sizeof (NEWSRC_ENTRY));
575     data->max = 5;
576   }
577
578   /*
579    * Set up to fake initial sequence from 1 to the article before the 
580    * first article in our list
581    */
582   data->num = 0;
583   series = 1;
584
585   for (x = 0; x < ctx->msgcount; x++) {
586     if (series) {               /* search for first unread */
587       /*
588        * We don't actually check sequential order, since we mark 
589        * "missing" entries as read/deleted
590        */
591       last = ctx->hdrs[x]->article_num;
592       if (last >= data->firstMessage && !ctx->hdrs[x]->deleted &&
593           !ctx->hdrs[x]->read) {
594         if (data->num >= data->max) {
595           data->max = data->max * 2;
596           safe_realloc (&data->entries, data->max * sizeof (NEWSRC_ENTRY));
597         }
598         data->entries[data->num].first = first;
599         data->entries[data->num].last = last - 1;
600         data->num++;
601         series = 0;
602       }
603     }
604     else {                      /* search for first read */
605
606       if (ctx->hdrs[x]->deleted || ctx->hdrs[x]->read) {
607         first = last + 1;
608         series = 1;
609       }
610       last = ctx->hdrs[x]->article_num;
611     }
612   }
613   if (series && first <= data->lastLoaded) {
614     if (data->num >= data->max) {
615       data->max = data->max * 2;
616       safe_realloc (&data->entries, data->max * sizeof (NEWSRC_ENTRY));
617     }
618     data->entries[data->num].first = first;
619     data->entries[data->num].last = data->lastLoaded;
620     data->num++;
621   }
622
623   if (save_sort != Sort) {
624     Sort = save_sort;
625     mutt_sort_headers (ctx, 0);
626   }
627 }
628
629 int mutt_newsrc_update (NNTP_SERVER * news)
630 {
631   char *buf, *line;
632   NNTP_DATA *data;
633   LIST *tmp;
634   int r = -1;
635   size_t len, llen;
636
637   if (!news)
638     return -1;
639   llen = len = 10 * LONG_STRING;
640   line = buf = safe_calloc (1, len);
641   /* we will generate full newsrc here */
642   for (tmp = news->list; tmp; tmp = tmp->next) {
643     data = (NNTP_DATA *) tmp->data;
644     if (!data || !data->rc)
645       continue;
646     nntp_create_newsrc_line (data, &buf, &line, &llen);
647     dprint (2, (debugfile, "Added to newsrc: %s", line));
648     line += safe_strlen (line);
649   }
650   /* newrc being fully rewritten */
651   if (news->newsrc &&
652       (r = mutt_update_list_file (news->newsrc, NULL, "", buf)) == 0) {
653     struct stat st;
654
655     stat (news->newsrc, &st);
656     news->size = st.st_size;
657     news->mtime = st.st_mtime;
658   }
659   FREE (&buf);
660   return r;
661 }
662
663 static FILE *mutt_mkname (char *s)
664 {
665   char buf[_POSIX_PATH_MAX], *pc;
666   int fd;
667   FILE *fp;
668
669   nntp_cache_expand (buf, s);
670   if ((fp = safe_fopen (buf, "w")))
671     return fp;
672
673   nntp_cache_expand (buf, "cache-XXXXXX");
674   pc = buf + safe_strlen (buf) - 12; /* positioning to "cache-XXXXXX" */
675   if ((fd = mkstemp (buf)) == -1)
676     return NULL;
677   strcpy (s, pc);               /* generated name */
678   return fdopen (fd, "w");
679 }
680
681 /* Updates info into .index file: ALL or about selected newsgroup */
682 static int nntp_update_cacheindex (NNTP_SERVER * serv, NNTP_DATA * data)
683 {
684   char buf[LONG_STRING], *key = "ALL";
685   char file[_POSIX_PATH_MAX];
686
687   if (!serv || !serv->conn || !serv->conn->account.host)
688     return -1;
689
690   if (data && data->group) {
691     key = data->group;
692     snprintf (buf, sizeof (buf), "%s %s %d %d", key, data->cache,
693               data->firstMessage, data->lastLoaded);
694   }
695   else {
696     strfcpy (file, serv->cache, sizeof (file));
697     snprintf (buf, sizeof (buf), "ALL %s 0 %d", file,
698               (int) serv->newgroups_time);
699   }
700   nntp_cache_expand (file, ".index");
701   return mutt_update_list_file (file, serv->conn->account.host, key, buf);
702 }
703
704 /* Remove cache files of unsubscribed newsgroups */
705 void nntp_clear_cacheindex (NNTP_SERVER * news)
706 {
707   NNTP_DATA *data;
708   LIST *tmp;
709
710   if (option (OPTSAVEUNSUB) || !news)
711     return;
712
713   for (tmp = news->list; tmp; tmp = tmp->next) {
714     data = (NNTP_DATA *) tmp->data;
715     if (!data || data->subscribed || !data->cache)
716       continue;
717     nntp_delete_cache (data);
718     dprint (2, (debugfile, "Removed from .index: %s\n", data->group));
719   }
720   return;
721 }
722
723 int nntp_save_cache_index (NNTP_SERVER * news)
724 {
725   char buf[HUGE_STRING];
726   char file[_POSIX_PATH_MAX];
727   NNTP_DATA *d;
728   FILE *f;
729   LIST *l;
730
731   if (!news || !news->newsgroups)
732     return -1;
733   if (!option (OPTNEWSCACHE))
734     return 0;
735
736   if (news->cache) {
737     nntp_cache_expand (file, news->cache);
738     unlink (file);
739     f = safe_fopen (file, "w");
740   }
741   else {
742     strfcpy (buf, news->conn->account.host, sizeof (buf));
743     f = mutt_mkname (buf);
744     news->cache = safe_strdup (buf);
745     nntp_cache_expand (file, buf);
746   }
747   if (!f)
748     return -1;
749
750   for (l = news->list; l; l = l->next) {
751     if ((d = (NNTP_DATA *) l->data) && !d->deleted) {
752       if (d->desc)
753         snprintf (buf, sizeof (buf), "%s %d %d %c %s\n", d->group,
754                   d->lastMessage, d->firstMessage, d->allowed ? 'y' : 'n',
755                   d->desc);
756       else
757         snprintf (buf, sizeof (buf), "%s %d %d %c\n", d->group,
758                   d->lastMessage, d->firstMessage, d->allowed ? 'y' : 'n');
759       if (fputs (buf, f) == EOF) {
760         fclose (f);
761         unlink (file);
762         return -1;
763       }
764     }
765   }
766   fclose (f);
767
768   if (nntp_update_cacheindex (news, NULL)) {
769     unlink (file);
770     return -1;
771   }
772   return 0;
773 }
774
775 int nntp_save_cache_group (CONTEXT * ctx)
776 {
777   char buf[HUGE_STRING], addr[STRING];
778   char file[_POSIX_PATH_MAX];
779   FILE *f;
780   HEADER *h;
781   struct tm *tm;
782   int i = 0, save = SORT_ORDER;
783   int prev = 0;
784
785   if (!option (OPTNEWSCACHE))
786     return 0;
787   if (!ctx || !ctx->data || ctx->magic != M_NNTP)
788     return -1;
789
790   if (((NNTP_DATA *) ctx->data)->cache) {
791     nntp_cache_expand (file, ((NNTP_DATA *) ctx->data)->cache);
792     unlink (file);
793     f = safe_fopen (file, "w");
794   }
795   else {
796     snprintf (buf, sizeof (buf), "%s-%s",
797               ((NNTP_DATA *) ctx->data)->nserv->conn->account.host,
798               ((NNTP_DATA *) ctx->data)->group);
799     f = mutt_mkname (buf);
800     ((NNTP_DATA *) ctx->data)->cache = safe_strdup (buf);
801     nntp_cache_expand (file, buf);
802   }
803   if (!f)
804     return -1;
805
806   if (Sort != SORT_ORDER) {
807     save = Sort;
808     Sort = SORT_ORDER;
809     mutt_sort_headers (ctx, 0);
810   }
811
812   /* Save only $nntp_context messages... */
813   ((NNTP_DATA *) ctx->data)->lastCached = 0;
814   if (NntpContext && ctx->msgcount > NntpContext)
815     i = ctx->msgcount - NntpContext;
816   for (; i < ctx->msgcount; i++) {
817     if (!ctx->hdrs[i]->deleted && ctx->hdrs[i]->article_num != prev) {
818       h = ctx->hdrs[i];
819       addr[0] = 0;
820       rfc822_write_address (addr, sizeof (addr), h->env->from, 0);
821       tm = gmtime (&h->date_sent);
822       snprintf (buf, sizeof (buf),
823                 "%d\t%s\t%s\t%d %s %d %02d:%02d:%02d GMT\t%s\t",
824                 h->article_num, h->env->subject, addr, tm->tm_mday,
825                 Months[tm->tm_mon], tm->tm_year + 1900, tm->tm_hour,
826                 tm->tm_min, tm->tm_sec, h->env->message_id);
827       fputs (buf, f);
828       if (h->env->references)
829         mutt_write_references (h->env->references, f);
830       snprintf (buf, sizeof (buf), "\t%ld\t%d\tXref: %s\n",
831                 h->content->length, h->lines, NONULL (h->env->xref));
832       if (fputs (buf, f) == EOF) {
833         fclose (f);
834         unlink (file);
835         return -1;
836       }
837     }
838     prev = ctx->hdrs[i]->article_num;
839   }
840
841   if (save != Sort) {
842     Sort = save;
843     mutt_sort_headers (ctx, 0);
844   }
845   fclose (f);
846
847   if (nntp_update_cacheindex (((NNTP_DATA *) ctx->data)->nserv,
848                               (NNTP_DATA *) ctx->data)) {
849     unlink (file);
850     return -1;
851   }
852   ((NNTP_DATA *) ctx->data)->lastCached =
853     ((NNTP_DATA *) ctx->data)->lastLoaded;
854   return 0;
855 }
856
857 void nntp_delete_cache (NNTP_DATA * data)
858 {
859   char buf[_POSIX_PATH_MAX];
860
861   if (!option (OPTNEWSCACHE) || !data || !data->cache || !data->nserv)
862     return;
863
864   nntp_cache_expand (buf, data->cache);
865   unlink (buf);
866   FREE (&data->cache);
867   data->lastCached = 0;
868   nntp_cache_expand (buf, ".index");
869   mutt_update_list_file (buf, data->nserv->conn->account.host, data->group,
870                          NULL);
871 }
872
873 NNTP_DATA *mutt_newsgroup_subscribe (NNTP_SERVER * news, char *group)
874 {
875   NNTP_DATA *data;
876
877   if (!news || !news->newsgroups || !group || !*group)
878     return NULL;
879   if (!(data = (NNTP_DATA *) hash_find (news->newsgroups, group))) {
880     data =
881       (NNTP_DATA *) safe_calloc (1, sizeof (NNTP_DATA) + safe_strlen (group) + 1);
882     data->group = (char *) data + sizeof (NNTP_DATA);
883     strcpy (data->group, group);
884     data->nserv = news;
885     data->deleted = 1;
886     if (news->newsgroups->nelem < news->newsgroups->curnelem * 2)
887       news->newsgroups =
888         hash_resize (news->newsgroups, news->newsgroups->nelem * 2);
889     hash_insert (news->newsgroups, data->group, data, 0);
890     nntp_add_to_list (news, data);
891   }
892   if (!data->subscribed) {
893     data->subscribed = 1;
894     data->rc = 1;
895   }
896   return data;
897 }
898
899 NNTP_DATA *mutt_newsgroup_unsubscribe (NNTP_SERVER * news, char *group)
900 {
901   NNTP_DATA *data;
902
903   if (!news || !news->newsgroups || !group || !*group ||
904       !(data = (NNTP_DATA *) hash_find (news->newsgroups, group)))
905     return NULL;
906   if (data->subscribed) {
907     data->subscribed = 0;
908     if (!option (OPTSAVEUNSUB))
909       data->rc = 0;
910   }
911   return data;
912 }
913
914 NNTP_DATA *mutt_newsgroup_catchup (NNTP_SERVER * news, char *group)
915 {
916   NNTP_DATA *data;
917
918   if (!news || !news->newsgroups || !group || !*group ||
919       !(data = (NNTP_DATA *) hash_find (news->newsgroups, group)))
920     return NULL;
921   if (!data->max) {
922     data->entries = safe_calloc (5, sizeof (NEWSRC_ENTRY));
923     data->max = 5;
924   }
925   data->num = 1;
926   data->entries[0].first = 1;
927   data->unread = 0;
928   data->entries[0].last = data->lastMessage;
929   if (Context && Context->data == data) {
930     int x;
931
932     for (x = 0; x < Context->msgcount; x++)
933       mutt_set_flag (Context, Context->hdrs[x], M_READ, 1);
934   }
935   return data;
936 }
937
938 NNTP_DATA *mutt_newsgroup_uncatchup (NNTP_SERVER * news, char *group)
939 {
940   NNTP_DATA *data;
941
942   if (!news || !news->newsgroups || !group || !*group ||
943       !(data = (NNTP_DATA *) hash_find (news->newsgroups, group)))
944     return NULL;
945   if (!data->max) {
946     data->entries = safe_calloc (5, sizeof (NEWSRC_ENTRY));
947     data->max = 5;
948   }
949   data->num = 1;
950   data->entries[0].first = 1;
951   data->entries[0].last = data->firstMessage - 1;
952   if (Context && Context->data == data) {
953     int x;
954
955     data->unread = Context->msgcount;
956     for (x = 0; x < Context->msgcount; x++)
957       mutt_set_flag (Context, Context->hdrs[x], M_READ, 0);
958   }
959   else
960     data->unread = data->lastMessage - data->entries[0].last;
961   return data;
962 }
963
964 /* this routine gives the first newsgroup with new messages */
965 void nntp_buffy (char *s)
966 {
967   LIST *list;
968
969   for (list = CurrentNewsSrv->list; list; list = list->next) {
970     NNTP_DATA *data = (NNTP_DATA *) list->data;
971
972     if (data && data->subscribed && data->unread) {
973       if (Context && Context->magic == M_NNTP &&
974           !safe_strcmp (data->group, ((NNTP_DATA *) Context->data)->group)) {
975         unsigned int i, unread = 0;
976
977         for (i = 0; i < Context->msgcount; i++)
978           if (!Context->hdrs[i]->read && !Context->hdrs[i]->deleted)
979             unread++;
980         if (!unread)
981           continue;
982       }
983       strcpy (s, data->group);
984       break;
985     }
986   }
987 }