New NNTP authentication code.
[claws.git] / src / news.c
1 /*
2  * Sylpheed -- a GTK+ based, lightweight, and fast e-mail client
3  * Copyright (C) 1999,2000 Hiroyuki Yamamoto
4  *
5  * This program is free software; you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation; either version 2 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program; if not, write to the Free Software
17  * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
18  */
19
20 #ifdef HAVE_CONFIG_H
21 #  include "config.h"
22 #endif
23
24 #include <glib.h>
25 #include <stdio.h>
26 #include <string.h>
27 #include <stdlib.h>
28 #include <dirent.h>
29 #include <unistd.h>
30
31 #include "defs.h"
32 #include "intl.h"
33 #include "news.h"
34 #include "nntp.h"
35 #include "socket.h"
36 #include "recv.h"
37 #include "procmsg.h"
38 #include "procheader.h"
39 #include "folder.h"
40 #include "session.h"
41 #include "statusbar.h"
42 #include "codeconv.h"
43 #include "utils.h"
44 #include "prefs_common.h"
45 #include "prefs_account.h"
46 #include "inputdialog.h"
47 #include "alertpanel.h"
48
49 static gint news_get_article_cmd         (NNTPSession   *session,
50                                           const gchar   *cmd,
51                                           gint           num,
52                                           gchar         *filename);
53 static gint news_get_article             (NNTPSession   *session,
54                                           gint           num,
55                                           gchar         *filename);
56 static gint news_get_header              (NNTPSession   *session,
57                                           gint           num,
58                                           gchar         *filename);
59
60 static GSList *news_get_uncached_articles(NNTPSession   *session,
61                                           FolderItem    *item,
62                                           gint           cache_last,
63                                           gint          *rfirst,
64                                           gint          *rlast);
65 static MsgInfo *news_parse_xover         (const gchar   *xover_str);
66 static GSList *news_delete_old_article   (GSList        *alist,
67                                           gint           first);
68 static void news_delete_all_article      (FolderItem    *item);
69
70
71 static Session *news_session_new(const gchar *server, gushort port,
72                                  const gchar *userid, const gchar *passwd)
73 {
74         gchar buf[NNTPBUFSIZE];
75         NNTPSession *session;
76         NNTPSockInfo *nntp_sock;
77
78         g_return_val_if_fail(server != NULL, NULL);
79
80         log_message(_("creating NNTP connection to %s:%d ...\n"), server, port);
81
82         if (userid && passwd)
83                 nntp_sock = nntp_open_auth(server, port, buf, userid, passwd);
84         else
85                 nntp_sock = nntp_open(server, port, buf);
86         if (nntp_sock < 0)
87                 return NULL;
88
89         session = g_new(NNTPSession, 1);
90         SESSION(session)->type      = SESSION_NEWS;
91         SESSION(session)->server    = g_strdup(server);
92         session->nntp_sock          = nntp_sock;
93         SESSION(session)->sock      = nntp_sock->sock;
94         SESSION(session)->connected = TRUE;
95         SESSION(session)->phase     = SESSION_READY;
96         SESSION(session)->data      = NULL;
97         session->group = NULL;
98
99         return SESSION(session);
100 }
101
102 void news_session_destroy(NNTPSession *session)
103 {
104         nntp_close(session->nntp_sock);
105         session->nntp_sock = NULL;
106         SESSION(session)->sock = NULL;
107
108         g_free(session->group);
109 }
110
111 static gchar *news_query_password(const gchar *server,
112                                   const gchar *user)
113 {
114         gchar *message;
115         gchar *pass;
116
117         message = g_strdup_printf(_("Input password for %s on %s:"),
118                                   user, server);
119
120         pass = input_dialog_with_invisible(_("Input password"),
121                                            message, NULL);
122         g_free(message);
123 /*      manage_window_focus_in(inc_dialog->mainwin->window, */
124 /*                             NULL, NULL); */
125         return pass;
126 }
127
128 static Session *news_session_new_for_folder(Folder *folder)
129 {
130         Session *session;
131         PrefsAccount *ac;
132         const gchar *userid;
133         gchar *passwd;
134
135         ac = folder->account;
136         if (ac->userid && ac->userid[0]) {
137                 userid = ac->userid;
138                 if (ac->passwd && ac->passwd[0])
139                         passwd = g_strdup(ac->passwd);
140                 else
141                         passwd = news_query_password(ac->nntp_server, userid);
142         } else {
143                 userid = passwd = NULL;
144         }
145         session = news_session_new(ac->nntp_server, 119, userid, passwd);
146         g_free(passwd);
147         return session;
148 }
149
150 NNTPSession *news_session_get(Folder *folder)
151 {
152         NNTPSession *session;
153
154         g_return_val_if_fail(folder != NULL, NULL);
155         g_return_val_if_fail(folder->type == F_NEWS, NULL);
156         g_return_val_if_fail(folder->account != NULL, NULL);
157
158         if (!REMOTE_FOLDER(folder)->session) {
159                 REMOTE_FOLDER(folder)->session =
160                         news_session_new_for_folder(folder);
161         } else {
162                 session = NNTP_SESSION(REMOTE_FOLDER(folder)->session);
163                 if (nntp_mode(session->nntp_sock, FALSE) != NN_SUCCESS) {
164                         log_warning(_("NNTP connection to %s:%d has been"
165                                       " disconnected. Reconnecting...\n"),
166                                     folder->account->nntp_server, 119);
167                         session_destroy(REMOTE_FOLDER(folder)->session);
168                         REMOTE_FOLDER(folder)->session =
169                                 news_session_new_for_folder(folder);
170                 }
171         }
172
173         return NNTP_SESSION(REMOTE_FOLDER(folder)->session);
174 }
175
176 GSList *news_get_article_list(Folder *folder, FolderItem *item,
177                               gboolean use_cache)
178 {
179         GSList *alist;
180         NNTPSession *session;
181
182         g_return_val_if_fail(folder != NULL, NULL);
183         g_return_val_if_fail(item != NULL, NULL);
184         g_return_val_if_fail(folder->type == F_NEWS, NULL);
185
186         session = news_session_get(folder);
187
188         if (!session) {
189                 alist = procmsg_read_cache(item, FALSE);
190                 item->last_num = procmsg_get_last_num_in_cache(alist);
191         } else if (use_cache) {
192                 GSList *newlist;
193                 gint cache_last;
194                 gint first, last;
195
196                 alist = procmsg_read_cache(item, FALSE);
197
198                 cache_last = procmsg_get_last_num_in_cache(alist);
199                 newlist = news_get_uncached_articles
200                         (session, item, cache_last, &first, &last);
201                 alist = news_delete_old_article(alist, first);
202
203                 alist = g_slist_concat(alist, newlist);
204                 item->last_num = last;
205         } else {
206                 gint last;
207
208                 alist = news_get_uncached_articles
209                         (session, item, 0, NULL, &last);
210                 news_delete_all_article(item);
211                 item->last_num = last;
212         }
213
214         procmsg_set_flags(alist, item);
215
216         statusbar_pop_all();
217
218         return alist;
219 }
220
221 gchar *news_fetch_msg(Folder *folder, FolderItem *item, gint num)
222 {
223         gchar *path, *filename;
224         gint ok;
225
226         g_return_val_if_fail(folder != NULL, NULL);
227         g_return_val_if_fail(item != NULL, NULL);
228
229         path = folder_item_get_path(item);
230         filename = g_strconcat(path, G_DIR_SEPARATOR_S, itos(num), NULL);
231         g_free(path);
232
233         if (is_file_exist(filename)) {
234                 debug_print(_("article %d has been already cached.\n"), num);
235                 return filename;
236         }
237
238         if (!REMOTE_FOLDER(folder)->session) {
239                 g_free(filename);
240                 return NULL;
241         }
242
243         debug_print(_("getting article %d...\n"), num);
244         ok = news_get_article(NNTP_SESSION(REMOTE_FOLDER(folder)->session),
245                               num, filename);
246         statusbar_pop_all();
247         if (ok < 0) {
248                 g_warning(_("can't read article %d\n"), num);
249                 g_free(filename);
250                 return NULL;
251         }
252
253         return filename;
254 }
255
256 void news_scan_group(Folder *folder, FolderItem *item)
257 {
258 }
259
260 gint news_post(Folder *folder, const gchar *file)
261 {
262         NNTPSession *session;
263         FILE *fp;
264         gint ok;
265
266         g_return_val_if_fail(folder != NULL, -1);
267         g_return_val_if_fail(folder->type == F_NEWS, -1);
268         g_return_val_if_fail(file != NULL, -1);
269
270         session = news_session_get(folder);
271         if (!session) return -1;
272
273         if ((fp = fopen(file, "r")) == NULL) {
274                 FILE_OP_ERROR(file, "fopen");
275                 return -1;
276         }
277
278         ok = nntp_post(session->nntp_sock, fp);
279         if (ok != NN_SUCCESS) {
280                 log_warning(_("can't post article.\n"));
281                 return -1;
282         }
283
284         fclose(fp);
285
286         statusbar_pop_all();
287
288         return 0;
289 }
290
291 static gint news_get_article_cmd(NNTPSession *session, const gchar *cmd,
292                                  gint num, gchar *filename)
293 {
294         gchar *msgid;
295
296         if (nntp_get_article(session->nntp_sock, cmd, num, &msgid)
297             != NN_SUCCESS)
298                 return -1;
299
300         debug_print("Message-Id = %s, num = %d\n", msgid, num);
301         g_free(msgid);
302
303         if (recv_write_to_file(session->nntp_sock->sock, filename) < 0) {
304                 log_warning(_("can't retrieve article %d\n"), num);
305                 return -1;
306         }
307
308         return 0;
309 }
310
311 static gint news_get_article(NNTPSession *session, gint num, gchar *filename)
312 {
313         return news_get_article_cmd(session, "ARTICLE", num, filename);
314 }
315
316 static gint news_get_header(NNTPSession *session, gint num, gchar *filename)
317 {
318         return news_get_article_cmd(session, "HEAD", num, filename);
319 }
320
321 static GSList *news_get_uncached_articles(NNTPSession *session,
322                                           FolderItem *item, gint cache_last,
323                                           gint *rfirst, gint *rlast)
324 {
325         gint ok;
326         gint num = 0, first = 0, last = 0, begin = 0, end = 0;
327         gchar buf[NNTPBUFSIZE];
328         GSList *newlist = NULL;
329         GSList *llast = NULL;
330         MsgInfo *msginfo;
331
332         if (rfirst) *rfirst = 0;
333         if (rlast)  *rlast  = 0;
334
335         g_return_val_if_fail(session != NULL, NULL);
336         g_return_val_if_fail(item != NULL, NULL);
337         g_return_val_if_fail(item->folder != NULL, NULL);
338         g_return_val_if_fail(item->folder->type == F_NEWS, NULL);
339
340         ok = nntp_group(session->nntp_sock, item->path,
341                         &num, &first, &last);
342         if (ok != NN_SUCCESS) {
343                 log_warning(_("can't set group: %s\n"), item->path);
344                 return NULL;
345         }
346
347         /* calculate getting overview range */
348         if (first > last) {
349                 log_warning(_("invalid article range: %d - %d\n"),
350                             first, last);
351                 return NULL;
352         }
353         if (cache_last < first)
354                 begin = first;
355         else if (last < cache_last)
356                 begin = first;
357         else if (last == cache_last) {
358                 debug_print(_("no new articles.\n"));
359                 return NULL;
360         } else
361                 begin = cache_last + 1;
362         end = last;
363
364         if (prefs_common.max_articles > 0 &&
365             end - begin + 1 > prefs_common.max_articles)
366                 begin = end - prefs_common.max_articles + 1;
367
368         log_message(_("getting xover %d - %d in %s...\n"),
369                     begin, end, item->path);
370         if (nntp_xover(session->nntp_sock, begin, end) != NN_SUCCESS) {
371                 log_warning(_("can't get xover\n"));
372                 return NULL;
373         }
374
375         for (;;) {
376                 if (sock_read(SESSION(session)->sock, buf, sizeof(buf)) < 0) {
377                         log_warning(_("error occurred while getting xover.\n"));
378                         return newlist;
379                 }
380
381                 if (buf[0] == '.' && buf[1] == '\r') break;
382
383                 msginfo = news_parse_xover(buf);
384                 if (!msginfo) {
385                         log_warning(_("invalid xover line: %s\n"), buf);
386                         continue;
387                 }
388
389                 msginfo->folder = item;
390                 msginfo->flags = MSG_NEW|MSG_UNREAD|MSG_NEWS;
391
392                 if (!newlist)
393                         llast = newlist = g_slist_append(newlist, msginfo);
394                 else {
395                         llast = g_slist_append(llast, msginfo);
396                         llast = llast->next;
397                 }
398         }
399
400         if (rfirst) *rfirst = first;
401         if (rlast)  *rlast  = last;
402         return newlist;
403 }
404
405 #define PARSE_ONE_PARAM(p, srcp) \
406 { \
407         p = strchr(srcp, '\t'); \
408         if (!p) return NULL; \
409         else \
410                 *p++ = '\0'; \
411 }
412
413 static MsgInfo *news_parse_xover(const gchar *xover_str)
414 {
415         MsgInfo *msginfo;
416         gchar buf[NNTPBUFSIZE];
417         gchar *subject, *sender, *size, *line, *date, *msgid, *ref, *tmp;
418         gchar *p;
419         gint num, size_int, line_int;
420         gchar *xover_buf;
421
422         Xalloca(xover_buf, strlen(xover_str) + 1, return NULL);
423         strcpy(xover_buf, xover_str);
424
425         PARSE_ONE_PARAM(subject, xover_buf);
426         PARSE_ONE_PARAM(sender, subject);
427         PARSE_ONE_PARAM(date, sender);
428         PARSE_ONE_PARAM(msgid, date);
429         PARSE_ONE_PARAM(ref, msgid);
430         PARSE_ONE_PARAM(size, ref);
431         PARSE_ONE_PARAM(line, size);
432
433         tmp = strchr(line, '\t');
434         if (!tmp) tmp = strchr(line, '\r');
435         if (!tmp) tmp = strchr(line, '\n');
436         if (tmp) *tmp = '\0';
437
438         num = atoi(xover_str);
439         size_int = atoi(size);
440         line_int = atoi(line);
441
442         /* set MsgInfo */
443         msginfo = g_new0(MsgInfo, 1);
444         msginfo->msgnum = num;
445         msginfo->size = size_int;
446
447         msginfo->date = g_strdup(date);
448         msginfo->date_t = procheader_date_parse(NULL, date, 0);
449
450         conv_unmime_header(buf, sizeof(buf), sender, NULL);
451         msginfo->from = g_strdup(buf);
452         msginfo->fromname = procheader_get_fromname(buf);
453
454         conv_unmime_header(buf, sizeof(buf), subject, NULL);
455         msginfo->subject = g_strdup(buf);
456
457         extract_parenthesis(msgid, '<', '>');
458         remove_space(msgid);
459         if (*msgid != '\0')
460                 msginfo->msgid = g_strdup(msgid);
461
462         eliminate_parenthesis(ref, '(', ')');
463         if ((p = strrchr(ref, '<')) != NULL) {
464                 extract_parenthesis(p, '<', '>');
465                 remove_space(p);
466                 if (*p != '\0')
467                         msginfo->inreplyto = g_strdup(p);
468         }
469
470         return msginfo;
471 }
472
473 static GSList *news_delete_old_article(GSList *alist, gint first)
474 {
475         GSList *cur, *next;
476         MsgInfo *msginfo;
477         gchar *cache_file;
478
479         if (first < 2) return alist;
480
481         for (cur = alist; cur != NULL; ) {
482                 next = cur->next;
483
484                 msginfo = (MsgInfo *)cur->data;
485                 if (msginfo && msginfo->msgnum < first) {
486                         debug_print(_("deleting article %d...\n"),
487                                     msginfo->msgnum);
488
489                         cache_file = procmsg_get_message_file_path(msginfo);
490                         if (is_file_exist(cache_file)) unlink(cache_file);
491                         g_free(cache_file);
492
493                         procmsg_msginfo_free(msginfo);
494                         alist = g_slist_remove(alist, msginfo);
495                 }
496
497                 cur = next;
498         }
499
500         return alist;
501 }
502
503 static void news_delete_all_article(FolderItem *item)
504 {
505         DIR *dp;
506         struct dirent *d;
507         gchar *dir;
508         gchar *file;
509
510         dir = folder_item_get_path(item);
511         if ((dp = opendir(dir)) == NULL) {
512                 FILE_OP_ERROR(dir, "opendir");
513                 g_free(dir);
514                 return;
515         }
516
517         debug_print(_("\tDeleting all cached articles... "));
518
519         while ((d = readdir(dp)) != NULL) {
520                 if (to_number(d->d_name) < 0) continue;
521
522                 file = g_strconcat(dir, G_DIR_SEPARATOR_S, d->d_name, NULL);
523
524                 if (is_file_exist(file)) {
525                         if (unlink(file) < 0)
526                                 FILE_OP_ERROR(file, "unlink");
527                 }
528
529                 g_free(file);
530         }
531
532         closedir(dp);
533         g_free(dir);
534
535         debug_print(_("done.\n"));
536 }
537
538 /*
539   news_get_group_list returns a strings list.
540   These strings are the names of the newsgroups of a server.
541   item is the FolderItem of the news server.
542   The names of the newsgroups are cached into a file so that
543   when the function is called again, there is no need to make
544   a request to the server.
545  */
546
547 GSList * news_get_group_list(FolderItem *item)
548 {
549         gchar *path, *filename;
550         gint ok;
551         NNTPSession *session;
552         GSList * group_list = NULL;
553         FILE * f;
554         gchar buf[NNTPBUFSIZE];
555         int len;
556
557         if (item == NULL)
558           return NULL;
559
560         path = folder_item_get_path(item);
561
562         if (!is_dir_exist(path))
563                 make_dir_hier(path);
564
565         filename = g_strconcat(path, G_DIR_SEPARATOR_S, GROUPLIST_FILE, NULL);
566         g_free(path);
567
568         session = news_session_get(item->folder);
569
570         if (session == NULL)
571           return NULL;
572
573         if (is_file_exist(filename)) {
574                 debug_print(_("group list has been already cached.\n"));
575         }
576         else {
577             ok = nntp_list(session->nntp_sock);
578             if (ok != NN_SUCCESS)
579               return NULL;
580             
581             if (recv_write_to_file(SESSION(session)->sock, filename) < 0) {
582               log_warning(_("can't retrieve group list\n"));
583               return NULL;
584             }
585         }
586
587         f = fopen(filename, "r");
588         while (fgets(buf, NNTPBUFSIZE, f)) {
589             char * s;
590
591             len = 0;
592             while ((buf[len] != 0) && (buf[len] != ' '))
593               len++;
594             buf[len] = 0;
595             s = g_strdup(buf);
596
597             group_list = g_slist_append(group_list, s);
598         }
599         fclose(f);
600         g_free(filename);
601
602         group_list = g_slist_sort(group_list, (GCompareFunc) g_strcasecmp);
603
604         return group_list;
605 }
606
607 /*
608   remove the cache file of the names of the newsgroups.
609  */
610
611 void news_reset_group_list(FolderItem *item)
612 {
613         gchar *path, *filename;
614
615         debug_print(_("\tDeleting cached group list... "));
616         path = folder_item_get_path(item);
617         filename = g_strconcat(path, G_DIR_SEPARATOR_S, GROUPLIST_FILE, NULL);
618         g_free(path);
619         if (remove(filename) != 0)
620           log_warning(_("can't delete cached group list %s\n"), filename);
621         g_free(filename);
622 }