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