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>
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.
17 #include "mutt_curses.h"
36 void nntp_add_to_list (NNTP_SERVER * s, NNTP_DATA * d)
43 l = safe_calloc (1, sizeof (LIST));
52 static int nntp_parse_newsrc_line (NNTP_SERVER * news, char *line)
55 char group[LONG_STRING];
57 char *p = line, *b, *h;
66 while (*p && (*p != ':' && *p != '!'))
71 if (len > sizeof (group))
73 strfcpy (group, line, len);
74 if ((data = (NNTP_DATA *) hash_find (news->newsgroups, group)) == NULL) {
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);
81 if (news->newsgroups->nelem < news->newsgroups->curnelem * 2)
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);
88 FREE ((void **) &data->entries);
91 data->entries = safe_calloc (x * 2, sizeof (NEWSRC_ENTRY));
103 while (*p && *p != ',' && *p != '\n')
109 if ((h = strchr (b, '-'))) {
112 data->entries[x].first = atoi (b);
113 data->entries[x].last = atoi (h);
116 data->entries[x].first = atoi (b);
117 data->entries[x].last = data->entries[x].first;
120 if (data->entries[x].last != 0)
123 if (x && !data->lastMessage)
124 data->lastMessage = data->entries[x - 1].last;
126 mutt_newsgroup_stat (data);
127 dprint (2, (debugfile, "parse_line: Newsgroup %s\n", data->group));
132 static int slurp_newsrc (NNTP_SERVER * news)
138 news->stat = stat (news->newsrc, &sb);
139 news->size = sb.st_size;
140 news->mtime = sb.st_mtime;
142 if ((fp = safe_fopen (news->newsrc, "r")) == NULL)
144 /* hmm, should we use dotlock? */
145 if (mx_lock_file (news->newsrc, fileno (fp), 0, 0, 1)) {
150 buf = safe_malloc (sb.st_size + 1);
151 while (fgets (buf, sb.st_size + 1, fp))
152 nntp_parse_newsrc_line (news, buf);
155 mx_unlock_file (news->newsrc, fileno (fp), 0);
160 void nntp_cache_expand (char *dst, const char *src)
162 snprintf (dst, _POSIX_PATH_MAX, "%s/%s", NewsCacheDir, src);
163 mutt_expand_path (dst, _POSIX_PATH_MAX);
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)
171 char buf[HUGE_STRING], *cp;
172 char dir[_POSIX_PATH_MAX], file[_POSIX_PATH_MAX];
177 /* check is server name defined or not */
178 if (!news || !news->conn || !news->conn->account.host)
180 unset_option (OPTNEWSCACHE);
181 if (!NewsCacheDir || !*NewsCacheDir)
184 strfcpy (dir, NewsCacheDir, sizeof (dir));
185 mutt_expand_path (dir, sizeof (dir));
187 if (lstat (dir, &st) || (st.st_mode & S_IFDIR) == 0) {
188 snprintf (buf, sizeof (buf), _("Directory %s not exist. Create it?"),
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!");
199 set_option (OPTNEWSCACHE);
202 snprintf (buf, sizeof (buf), "%s/.index", dir);
203 if (!(index = safe_fopen (buf, "a+")))
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))
212 while (fgets (buf, sizeof (buf), index)) {
214 while (*cp && *cp != ' ')
219 if (!safe_strcmp (buf, "#:"))
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;
226 else if (news->newsgroups) {
227 if ((data = (NNTP_DATA *) hash_find (news->newsgroups, buf)) == NULL) {
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);
235 if (news->newsgroups->nelem < news->newsgroups->curnelem * 2)
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);
241 data->cache = safe_strdup (file);
243 if (!data->firstMessage || data->lastMessage < m)
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);
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)
263 char fn[SHORT_STRING], tmp[SHORT_STRING];
267 strncpy (fn, NewsServer, sizeof (fn) - 1);
269 snprintf (tmp, sizeof (tmp), "%%%ss", fmt);
270 snprintf (dest, destlen, tmp, fn);
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)
287 acct->port = NNTP_PORT;
288 acct->type = M_ACCT_TYPE_NNTP;
290 c = safe_strdup (server);
291 url_parse_ciss (&url, c);
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;
301 strfcpy (group, url.path, group_len);
303 ret = mutt_account_fromurl (acct, &url);
310 void nntp_expand_path (char *line, size_t len, ACCOUNT * acct)
314 url.path = safe_strdup (line);
315 mutt_account_tourl (acct, &url);
316 url_ciss_tostring (&url, line, len, 0);
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.
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.
330 NNTP_SERVER *mutt_select_newsserver (char *server)
332 char file[_POSIX_PATH_MAX];
339 if (!server || !*server) {
340 mutt_error _("No newsserver defined!");
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');
352 if ((nntp_parse_url (buf, &acct, file, sizeof (file))) < 0 || *file) {
354 mutt_error (_("%s is an invalid newsserver specification!"), server);
359 conn = mutt_conn_find (NULL, &acct);
363 mutt_FormatString (file, sizeof (file), NONULL (NewsRc), nntp_format_str, 0,
365 mutt_expand_path (file, sizeof (file));
367 serv = (NNTP_SERVER *) conn->data;
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;
379 data->subscribed = 0;
385 nntp_clear_cacheindex (serv);
388 if (serv->status == NNTP_BYE)
389 serv->status = NNTP_NONE;
390 nntp_check_newgroups (serv, 0);
395 serv = safe_calloc (1, sizeof (NNTP_SERVER));
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)
407 mutt_free_list (&serv->list);
408 FREE (&serv->newsrc);
413 nntp_clear_cacheindex (serv);
414 conn->data = (void *) serv;
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
429 void nntp_get_status (CONTEXT * ctx, HEADER * h, char *group, int article)
431 NNTP_DATA *data = (NNTP_DATA *) ctx->data;
435 data = (NNTP_DATA *) hash_find (data->nserv->newsgroups, group);
440 dprint (3, (debugfile, "newsgroup %s not found\n", group));
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()
454 /* If article was not cached yet, it is new! :) */
455 if (!data->cache || article > data->lastCached)
457 /* Old articles are articles which aren't read but an article after them
459 if (option (OPTMARKOLD))
463 void mutt_newsgroup_stat (NNTP_DATA * data)
466 unsigned int first, last;
469 if (data->lastMessage == 0 || data->firstMessage > data->lastMessage)
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;
481 data->unread -= last - first + 1;
485 static int puti (char *line, int num)
490 *p++ = '0' + num % 10;
494 *line++ = *--p, num++;
499 static void nntp_create_newsrc_line (NNTP_DATA * data, char **buf,
500 char **pline, size_t * buflen)
503 size_t len = *buflen - (*pline - *buf);
506 if (len < LONG_STRING * 10) {
510 safe_realloc (buf, *buflen);
511 line = *buf + (*pline - line);
513 strcpy (line, data->group);
514 len -= safe_strlen (line) + 1;
515 line += safe_strlen (line);
516 *line++ = data->subscribed ? ':' : '!';
520 for (x = 0; x < data->num; x++) {
521 if (len < LONG_STRING) {
526 safe_realloc (buf, *buflen);
527 line = *buf + (*pline - line);
535 if (data->entries[x].first == data->entries[x].last)
536 snprintf (line, len, "%d%n", data->entries[x].first, &i);
538 snprintf (line, len, "%d-%d%n",
539 data->entries[x].first, data->entries[x].last, &i);
543 i = puti (line, data->entries[x].first);
546 if (data->entries[x].first != data->entries[x].last) {
549 i = puti (line, data->entries[x].last);
560 void newsrc_gen_entries (CONTEXT * ctx)
562 NNTP_DATA *data = (NNTP_DATA *) ctx->data;
564 unsigned int last = 0, first = 1;
565 int save_sort = SORT_ORDER;
567 if (Sort != SORT_ORDER) {
570 mutt_sort_headers (ctx, 0);
574 data->entries = safe_calloc (5, sizeof (NEWSRC_ENTRY));
579 * Set up to fake initial sequence from 1 to the article before the
580 * first article in our list
585 for (x = 0; x < ctx->msgcount; x++) {
586 if (series) { /* search for first unread */
588 * We don't actually check sequential order, since we mark
589 * "missing" entries as read/deleted
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));
598 data->entries[data->num].first = first;
599 data->entries[data->num].last = last - 1;
604 else { /* search for first read */
606 if (ctx->hdrs[x]->deleted || ctx->hdrs[x]->read) {
610 last = ctx->hdrs[x]->article_num;
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));
618 data->entries[data->num].first = first;
619 data->entries[data->num].last = data->lastLoaded;
623 if (save_sort != Sort) {
625 mutt_sort_headers (ctx, 0);
629 int mutt_newsrc_update (NNTP_SERVER * news)
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)
646 nntp_create_newsrc_line (data, &buf, &line, &llen);
647 dprint (2, (debugfile, "Added to newsrc: %s", line));
648 line += safe_strlen (line);
650 /* newrc being fully rewritten */
652 (r = mutt_update_list_file (news->newsrc, NULL, "", buf)) == 0) {
655 stat (news->newsrc, &st);
656 news->size = st.st_size;
657 news->mtime = st.st_mtime;
663 static FILE *mutt_mkname (char *s)
665 char buf[_POSIX_PATH_MAX], *pc;
669 nntp_cache_expand (buf, s);
670 if ((fp = safe_fopen (buf, "w")))
673 nntp_cache_expand (buf, "cache-XXXXXX");
674 pc = buf + safe_strlen (buf) - 12; /* positioning to "cache-XXXXXX" */
675 if ((fd = mkstemp (buf)) == -1)
677 strcpy (s, pc); /* generated name */
678 return fdopen (fd, "w");
681 /* Updates info into .index file: ALL or about selected newsgroup */
682 static int nntp_update_cacheindex (NNTP_SERVER * serv, NNTP_DATA * data)
684 char buf[LONG_STRING], *key = "ALL";
685 char file[_POSIX_PATH_MAX];
687 if (!serv || !serv->conn || !serv->conn->account.host)
690 if (data && data->group) {
692 snprintf (buf, sizeof (buf), "%s %s %d %d", key, data->cache,
693 data->firstMessage, data->lastLoaded);
696 strfcpy (file, serv->cache, sizeof (file));
697 snprintf (buf, sizeof (buf), "ALL %s 0 %d", file,
698 (int) serv->newgroups_time);
700 nntp_cache_expand (file, ".index");
701 return mutt_update_list_file (file, serv->conn->account.host, key, buf);
704 /* Remove cache files of unsubscribed newsgroups */
705 void nntp_clear_cacheindex (NNTP_SERVER * news)
710 if (option (OPTSAVEUNSUB) || !news)
713 for (tmp = news->list; tmp; tmp = tmp->next) {
714 data = (NNTP_DATA *) tmp->data;
715 if (!data || data->subscribed || !data->cache)
717 nntp_delete_cache (data);
718 dprint (2, (debugfile, "Removed from .index: %s\n", data->group));
723 int nntp_save_cache_index (NNTP_SERVER * news)
725 char buf[HUGE_STRING];
726 char file[_POSIX_PATH_MAX];
731 if (!news || !news->newsgroups)
733 if (!option (OPTNEWSCACHE))
737 nntp_cache_expand (file, news->cache);
739 f = safe_fopen (file, "w");
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);
750 for (l = news->list; l; l = l->next) {
751 if ((d = (NNTP_DATA *) l->data) && !d->deleted) {
753 snprintf (buf, sizeof (buf), "%s %d %d %c %s\n", d->group,
754 d->lastMessage, d->firstMessage, d->allowed ? 'y' : 'n',
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) {
768 if (nntp_update_cacheindex (news, NULL)) {
775 int nntp_save_cache_group (CONTEXT * ctx)
777 char buf[HUGE_STRING], addr[STRING];
778 char file[_POSIX_PATH_MAX];
782 int i = 0, save = SORT_ORDER;
785 if (!option (OPTNEWSCACHE))
787 if (!ctx || !ctx->data || ctx->magic != M_NNTP)
790 if (((NNTP_DATA *) ctx->data)->cache) {
791 nntp_cache_expand (file, ((NNTP_DATA *) ctx->data)->cache);
793 f = safe_fopen (file, "w");
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);
806 if (Sort != SORT_ORDER) {
809 mutt_sort_headers (ctx, 0);
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) {
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);
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) {
838 prev = ctx->hdrs[i]->article_num;
843 mutt_sort_headers (ctx, 0);
847 if (nntp_update_cacheindex (((NNTP_DATA *) ctx->data)->nserv,
848 (NNTP_DATA *) ctx->data)) {
852 ((NNTP_DATA *) ctx->data)->lastCached =
853 ((NNTP_DATA *) ctx->data)->lastLoaded;
857 void nntp_delete_cache (NNTP_DATA * data)
859 char buf[_POSIX_PATH_MAX];
861 if (!option (OPTNEWSCACHE) || !data || !data->cache || !data->nserv)
864 nntp_cache_expand (buf, 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,
873 NNTP_DATA *mutt_newsgroup_subscribe (NNTP_SERVER * news, char *group)
877 if (!news || !news->newsgroups || !group || !*group)
879 if (!(data = (NNTP_DATA *) hash_find (news->newsgroups, group))) {
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);
886 if (news->newsgroups->nelem < news->newsgroups->curnelem * 2)
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);
892 if (!data->subscribed) {
893 data->subscribed = 1;
899 NNTP_DATA *mutt_newsgroup_unsubscribe (NNTP_SERVER * news, char *group)
903 if (!news || !news->newsgroups || !group || !*group ||
904 !(data = (NNTP_DATA *) hash_find (news->newsgroups, group)))
906 if (data->subscribed) {
907 data->subscribed = 0;
908 if (!option (OPTSAVEUNSUB))
914 NNTP_DATA *mutt_newsgroup_catchup (NNTP_SERVER * news, char *group)
918 if (!news || !news->newsgroups || !group || !*group ||
919 !(data = (NNTP_DATA *) hash_find (news->newsgroups, group)))
922 data->entries = safe_calloc (5, sizeof (NEWSRC_ENTRY));
926 data->entries[0].first = 1;
928 data->entries[0].last = data->lastMessage;
929 if (Context && Context->data == data) {
932 for (x = 0; x < Context->msgcount; x++)
933 mutt_set_flag (Context, Context->hdrs[x], M_READ, 1);
938 NNTP_DATA *mutt_newsgroup_uncatchup (NNTP_SERVER * news, char *group)
942 if (!news || !news->newsgroups || !group || !*group ||
943 !(data = (NNTP_DATA *) hash_find (news->newsgroups, group)))
946 data->entries = safe_calloc (5, sizeof (NEWSRC_ENTRY));
950 data->entries[0].first = 1;
951 data->entries[0].last = data->firstMessage - 1;
952 if (Context && Context->data == data) {
955 data->unread = Context->msgcount;
956 for (x = 0; x < Context->msgcount; x++)
957 mutt_set_flag (Context, Context->hdrs[x], M_READ, 0);
960 data->unread = data->lastMessage - data->entries[0].last;
964 /* this routine gives the first newsgroup with new messages */
965 void nntp_buffy (char *s)
969 for (list = CurrentNewsSrv->list; list; list = list->next) {
970 NNTP_DATA *data = (NNTP_DATA *) list->data;
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;
977 for (i = 0; i < Context->msgcount; i++)
978 if (!Context->hdrs[i]->read && !Context->hdrs[i]->deleted)
983 strcpy (s, data->group);