Fix FSF address (Tobias Mueller, #470445)
[platform/upstream/evolution-data-server.git] / camel / providers / nntp / camel-nntp-newsrc.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2 /*
3  *  Authors: Chris Toshok <toshok@ximian.com>
4  *
5  *  Copyright 2000-2003 Ximian, Inc. (www.ximian.com)
6  *
7  *  This program is free software; you can redistribute it and/or modify
8  *  it under the terms of the GNU Lesser General Public License as published by
9  *  the Free Software Foundation; either version 2 of the License, or
10  *  (at your option) any later version.
11  *
12  *  This program is distributed in the hope that it will be useful,
13  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
14  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  *  GNU Lesser General Public License for more details.
16  *
17  *  You should have received a copy of the GNU Lesser General Public License
18  *  along with this program; if not, write to the Free Software
19  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22
23 #ifndef HAVE_CONFIG_H
24 #include <config.h>
25 #endif
26
27 #include <errno.h>
28 #include <fcntl.h>
29 #include <pthread.h>
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <unistd.h>
34
35 #include <glib.h>
36 #include <glib/gstdio.h>
37
38 #include "camel-folder-summary.h"
39
40 #include "camel-nntp-newsrc.h"
41
42 #define NEWSRC_LOCK(f, l) (g_mutex_lock(((CamelNNTPNewsrc *)f)->l))
43 #define NEWSRC_UNLOCK(f, l) (g_mutex_unlock(((CamelNNTPNewsrc *)f)->l))
44
45 typedef struct {
46         guint low;
47         guint high;
48 } ArticleRange;
49
50 typedef struct {
51         char *name;
52         GArray *ranges;
53         gboolean subscribed;
54 } NewsrcGroup;
55
56 struct CamelNNTPNewsrc {
57         gchar *filename;
58         GHashTable *groups;
59         gboolean dirty;
60         GMutex *lock;
61 };
62
63
64 static NewsrcGroup *
65 camel_nntp_newsrc_group_add (CamelNNTPNewsrc *newsrc, const char *group_name, gboolean subscribed)
66 {
67         NewsrcGroup *new_group = g_malloc(sizeof(NewsrcGroup));
68
69         new_group->name = g_strdup(group_name);
70         new_group->subscribed = subscribed;
71         new_group->ranges = g_array_new (FALSE, FALSE, sizeof (ArticleRange));
72
73         g_hash_table_insert (newsrc->groups, new_group->name, new_group);
74
75         newsrc->dirty = TRUE;
76
77         return new_group;
78 }
79
80 static int
81 camel_nntp_newsrc_group_get_highest_article_read(CamelNNTPNewsrc *newsrc, NewsrcGroup *group)
82 {
83         if (!group || group->ranges->len == 0)
84                 return 0;
85
86         return g_array_index(group->ranges, ArticleRange, group->ranges->len - 1).high;
87 }
88
89 static int
90 camel_nntp_newsrc_group_get_num_articles_read(CamelNNTPNewsrc *newsrc, NewsrcGroup *group)
91 {
92         int i;
93         int count = 0;
94
95         if (group == NULL)
96                 return 0;
97
98         for (i = 0; i < group->ranges->len; i ++)
99                 count += (g_array_index(group->ranges, ArticleRange, i).high -
100                           g_array_index(group->ranges, ArticleRange, i).low) + 1;
101
102         return count;
103 }
104
105
106 static void
107 camel_nntp_newsrc_group_mark_range_read(CamelNNTPNewsrc *newsrc, NewsrcGroup *group, long low, long high)
108 {
109         int i;
110
111         if (group->ranges->len == 1
112             && g_array_index (group->ranges, ArticleRange, 0).low == 0
113             && g_array_index (group->ranges, ArticleRange, 0).high == 0) {
114                 g_array_index (group->ranges, ArticleRange, 0).low = low;
115                 g_array_index (group->ranges, ArticleRange, 0).high = high;
116
117                 newsrc->dirty = TRUE;
118         }
119         else  {
120                 ArticleRange tmp_range;
121
122                 for (i = 0; i < group->ranges->len; i ++) {
123                         guint range_low = g_array_index (group->ranges, ArticleRange, i).low;
124                         guint range_high = g_array_index (group->ranges, ArticleRange, i).high;
125                         
126                         /* if it's already part of a range, return immediately. */
127                         if (low >= range_low &&
128                             low <= range_high &&
129                             high >= range_low &&
130                             high <= range_high) {
131                                 return;
132                         }
133                         /* if we have a new lower bound for this range, set it. */
134                         else if (low <= range_low
135                                  && high >= range_low
136                                  && high <= range_high) {
137                                 g_array_index (group->ranges, ArticleRange, i).low = low;
138                                 newsrc->dirty = TRUE;
139                                 return;
140                         }
141                         /* if we have a new upper bound for this range, set it. */
142                         else if (high >= range_high
143                                  && low >= range_low
144                                  && low <= range_high) {
145                                 g_array_index (group->ranges, ArticleRange, i).high = high;
146                                 newsrc->dirty = TRUE;
147                                 return;
148                         }
149                         /* if we would be inserting another range that
150                            starts one index higher than an existing
151                            one, make the upper value of the existing
152                            range the upper value of the new one. */
153                         else if (low == range_high + 1) {
154                                 g_array_index (group->ranges, ArticleRange, i).high = high;
155                                 newsrc->dirty = TRUE;
156                                 return;
157                         }
158                         /* if we would be inserting another range that
159                            ends one index lower than an existing one,
160                            group the existing range by setting its low
161                            to the new low */
162                         else if (high == range_low - 1) {
163                                 g_array_index (group->ranges, ArticleRange, i).low = low;
164                                 newsrc->dirty = TRUE;
165                                 return;
166                         }
167                         /* if the range lies entirely outside another
168                            range, doesn't coincide with it's
169                            endpoints, and has lower values, insert it
170                            into the middle of the list. */
171                         else if (low < range_low
172                                  && high < range_low) {
173                                 tmp_range.low = low;
174                                 tmp_range.high = high;
175
176                                 group->ranges = g_array_insert_val (group->ranges, i, tmp_range);
177                                 newsrc->dirty = TRUE;
178
179                                 return;
180                         }
181                 }
182
183                 /* if we made it here, the range needs to go at the end */
184                 tmp_range.low = low;
185                 tmp_range.high = high;
186                 group->ranges = g_array_append_val (group->ranges, tmp_range);
187                 newsrc->dirty = TRUE;
188         } 
189 }
190
191 int
192 camel_nntp_newsrc_get_highest_article_read (CamelNNTPNewsrc *newsrc, const char *group_name)
193 {
194         NewsrcGroup *group;
195         int ret;
196
197         NEWSRC_LOCK(newsrc, lock);
198
199         group = g_hash_table_lookup (newsrc->groups, group_name);
200
201         if (group)
202                 ret = camel_nntp_newsrc_group_get_highest_article_read (newsrc, group);
203         else
204                 ret = 0;
205
206         NEWSRC_UNLOCK(newsrc, lock);
207
208         return ret;
209 }
210
211 int
212 camel_nntp_newsrc_get_num_articles_read (CamelNNTPNewsrc *newsrc, const char *group_name)
213 {
214         NewsrcGroup *group;
215         int ret;
216
217         NEWSRC_LOCK(newsrc, lock);
218
219         group = g_hash_table_lookup (newsrc->groups, group_name);
220
221         if (group)
222                 ret = camel_nntp_newsrc_group_get_num_articles_read (newsrc, group);
223         else
224                 ret = 0;
225
226         NEWSRC_UNLOCK(newsrc, lock);
227
228         return ret;
229 }
230
231 void
232 camel_nntp_newsrc_mark_article_read (CamelNNTPNewsrc *newsrc, const char *group_name, int num)
233 {
234         camel_nntp_newsrc_mark_range_read (newsrc, group_name, num, num);
235 }
236
237 void
238 camel_nntp_newsrc_mark_range_read(CamelNNTPNewsrc *newsrc, const char *group_name, long low, long high)
239 {
240         NewsrcGroup *group;
241
242         /* swap them if they're in the wrong order. */
243         if (low > high) {
244                 long tmp;
245
246                 tmp = high;
247                 high = low;
248                 low = tmp;
249         }
250
251         NEWSRC_LOCK(newsrc, lock);
252         group = g_hash_table_lookup (newsrc->groups, group_name);
253
254         if (group)
255                 camel_nntp_newsrc_group_mark_range_read (newsrc, group, low, high);
256         NEWSRC_UNLOCK(newsrc, lock);
257 }
258
259 gboolean
260 camel_nntp_newsrc_article_is_read (CamelNNTPNewsrc *newsrc, const char *group_name, long num)
261 {
262         int i;
263         NewsrcGroup *group;
264         int ret = FALSE;
265
266         NEWSRC_LOCK(newsrc, lock);
267         group = g_hash_table_lookup (newsrc->groups, group_name);
268
269         if (group) {
270                 for (i = 0; i < group->ranges->len; i++) {
271                         if (num >= g_array_index (group->ranges, ArticleRange, i).low && 
272                             num <= g_array_index (group->ranges, ArticleRange, i).high) {
273                                 ret = TRUE;
274                                 break;
275                         }
276                 }
277         }
278
279         NEWSRC_UNLOCK(newsrc, lock);
280
281         return FALSE;
282 }
283
284 gboolean  
285 camel_nntp_newsrc_group_is_subscribed (CamelNNTPNewsrc *newsrc, const char *group_name)
286 {
287         NewsrcGroup *group;
288         int ret = FALSE;
289
290         NEWSRC_LOCK(newsrc, lock);
291
292         group = g_hash_table_lookup (newsrc->groups, group_name);
293
294         if (group) {
295                 ret = group->subscribed;
296         }
297
298         NEWSRC_UNLOCK(newsrc, lock);
299
300         return ret;
301 }
302
303 void
304 camel_nntp_newsrc_subscribe_group (CamelNNTPNewsrc *newsrc, const char *group_name)
305 {
306         NewsrcGroup *group;
307
308         NEWSRC_LOCK(newsrc, lock);
309
310         group = g_hash_table_lookup (newsrc->groups, group_name);
311
312         if (group) {
313                 if (!group->subscribed)
314                         newsrc->dirty = TRUE;
315                 group->subscribed = TRUE;
316         }
317         else {
318                 camel_nntp_newsrc_group_add (newsrc, group_name, TRUE);
319         }
320
321         NEWSRC_UNLOCK(newsrc, lock);
322 }
323
324 void
325 camel_nntp_newsrc_unsubscribe_group (CamelNNTPNewsrc *newsrc, const char *group_name)
326 {
327         NewsrcGroup *group;
328
329         NEWSRC_LOCK(newsrc, lock);
330
331         group = g_hash_table_lookup (newsrc->groups, group_name);
332         if (group) {
333                 if (group->subscribed)
334                         newsrc->dirty = TRUE;
335                 group->subscribed = FALSE;
336         }
337         else {
338                 camel_nntp_newsrc_group_add (newsrc, group_name, FALSE);
339         }
340
341         NEWSRC_UNLOCK(newsrc, lock);
342 }
343
344 struct newsrc_ptr_array {
345         GPtrArray *ptr_array;
346         gboolean subscribed_only;
347 };
348
349 /* this needs to strdup the grup_name, if the group array is likely to change */
350 static void
351 get_group_foreach (char *group_name, NewsrcGroup *group, struct newsrc_ptr_array *npa)
352 {
353         if (group->subscribed || !npa->subscribed_only) {
354                 g_ptr_array_add (npa->ptr_array, group_name);
355         }
356 }
357
358 GPtrArray *
359 camel_nntp_newsrc_get_subscribed_group_names (CamelNNTPNewsrc *newsrc)
360 {
361         struct newsrc_ptr_array npa;
362
363         g_return_val_if_fail (newsrc, NULL);
364
365         NEWSRC_LOCK(newsrc, lock);
366
367         npa.ptr_array = g_ptr_array_new();
368         npa.subscribed_only = TRUE;
369
370         g_hash_table_foreach (newsrc->groups,
371                               (GHFunc)get_group_foreach, &npa);
372
373         NEWSRC_UNLOCK(newsrc, lock);
374
375         return npa.ptr_array;
376 }
377
378 GPtrArray *
379 camel_nntp_newsrc_get_all_group_names (CamelNNTPNewsrc *newsrc)
380 {
381         struct newsrc_ptr_array npa;
382
383         g_return_val_if_fail (newsrc, NULL);
384
385         NEWSRC_LOCK(newsrc, lock);
386
387         npa.ptr_array = g_ptr_array_new();
388         npa.subscribed_only = FALSE;
389
390         g_hash_table_foreach (newsrc->groups,
391                               (GHFunc)get_group_foreach, &npa);
392
393         NEWSRC_UNLOCK(newsrc, lock);
394
395         return npa.ptr_array;
396 }
397
398 void
399 camel_nntp_newsrc_free_group_names (CamelNNTPNewsrc *newsrc, GPtrArray *group_names)
400 {
401         g_ptr_array_free (group_names, TRUE);
402 }
403
404 struct newsrc_fp {
405         CamelNNTPNewsrc *newsrc;
406         FILE *fp;
407 };
408
409 static void
410 camel_nntp_newsrc_write_group_line(gpointer key, NewsrcGroup *group, struct newsrc_fp *newsrc_fp)
411 {
412         CamelNNTPNewsrc *newsrc;
413         FILE *fp;
414         int i;
415
416         fp = newsrc_fp->fp;
417         newsrc = newsrc_fp->newsrc;
418
419         fprintf (fp, "%s%c", group->name, group->subscribed ? ':' : '!');
420
421         if (group->ranges->len == 1
422             && g_array_index (group->ranges, ArticleRange, 0).low == 0
423             && g_array_index (group->ranges, ArticleRange, 0).high == 0) {
424                 fprintf (fp, "\n");
425
426                 return; /* special case since our parsing code will insert this
427                            bogus range if there were no read articles.  The code
428                            to add a range is smart enough to remove this one if we
429                            ever mark an article read, but we still need to deal with
430                            it if that code doesn't get hit. */
431         }
432
433         fprintf (fp, " ");
434
435         for (i = 0; i < group->ranges->len; i ++) {
436                 char range_buffer[100];
437                 guint low = g_array_index (group->ranges, ArticleRange, i).low;
438                 guint high = g_array_index (group->ranges, ArticleRange, i).high;
439
440                 if (low == high)
441                         sprintf(range_buffer, "%d", low);
442                 else if (low == high - 1)
443                         sprintf(range_buffer, "%d,%d", low, high);
444                 else
445                         sprintf(range_buffer, "%d-%d", low, high);
446
447                 if (i != group->ranges->len - 1)
448                         strcat(range_buffer, ",");
449
450                 fprintf (fp, range_buffer);
451         }
452
453         fprintf (fp, "\n");
454 }
455
456 void 
457 camel_nntp_newsrc_write_to_file(CamelNNTPNewsrc *newsrc, FILE *fp)
458 {
459         struct newsrc_fp newsrc_fp;
460
461         g_return_if_fail (newsrc);
462
463         newsrc_fp.newsrc = newsrc;
464         newsrc_fp.fp = fp;
465
466         NEWSRC_LOCK(newsrc, lock);
467
468         g_hash_table_foreach (newsrc->groups,
469                               (GHFunc)camel_nntp_newsrc_write_group_line,
470                               &newsrc_fp);
471
472         NEWSRC_UNLOCK(newsrc, lock);
473 }
474
475 void
476 camel_nntp_newsrc_write(CamelNNTPNewsrc *newsrc)
477 {
478         FILE *fp;
479
480         g_return_if_fail (newsrc);
481
482         NEWSRC_LOCK(newsrc, lock);
483
484         if (!newsrc->dirty) {
485                 NEWSRC_UNLOCK(newsrc, lock);
486                 return;
487         }
488
489         if ((fp = g_fopen(newsrc->filename, "w")) == NULL) {
490                 g_warning ("Couldn't open newsrc file '%s'.\n", newsrc->filename);
491                 NEWSRC_UNLOCK(newsrc, lock);
492                 return;
493         }
494
495         newsrc->dirty = FALSE;
496         NEWSRC_UNLOCK(newsrc, lock);
497
498         camel_nntp_newsrc_write_to_file(newsrc, fp);
499
500         fclose(fp);
501 }
502
503 static void
504 camel_nntp_newsrc_parse_line(CamelNNTPNewsrc *newsrc, char *line)
505 {
506         char *p, *comma, *dash;
507         gboolean is_subscribed;
508         NewsrcGroup *group;
509
510         p = strchr(line, ':');
511
512         if (p) {
513                 is_subscribed = TRUE;
514         }
515         else {
516                 p = strchr(line, '!');
517                 if (p)
518                         is_subscribed = FALSE;
519                 else
520                         return; /* bogus line. */
521         }
522
523         *p++ = '\0';
524
525         group = camel_nntp_newsrc_group_add (newsrc, line, is_subscribed);
526
527         do {
528                 guint high, low;
529
530                 comma = strchr(p, ',');
531
532                 if (comma)
533                         *comma = '\0';
534
535                 dash = strchr(p, '-');
536
537                 if (!dash) { /* there wasn't a dash.  must be just one number */
538                         high = low = atol(p);
539                 }
540                 else { /* there was a dash. */
541                         *dash = '\0';
542                         low = atol(p);
543                         *dash = '-';
544                         p = dash + 1;
545                         high = atol(p);
546                 }
547
548                 camel_nntp_newsrc_group_mark_range_read (newsrc, group, low, high);
549
550                 if (comma) {
551                         *comma = ',';
552                         p = comma + 1;
553                 }
554
555         } while(comma);
556 }
557
558 static char*
559 get_line (char *buf, char **p)
560 {
561         char *l;
562         char *line;
563
564         g_assert (*p == NULL || **p == '\n' || **p == '\0');
565
566         if (*p == NULL) {
567                 *p = buf;
568
569                 if (**p == '\0')
570                         return NULL;
571         }
572         else {
573                 if (**p == '\0')
574                         return NULL;
575
576                 (*p) ++;
577         
578                 /* if we just incremented to the end of the buffer, return NULL */
579                 if (**p == '\0')
580                         return NULL;
581         }
582
583         l = strchr (*p, '\n');
584         if (l) {
585                 *l = '\0';
586                 line = g_strdup (*p);
587                 *l = '\n';
588                 *p = l;
589         }
590         else {
591                 /* we're at the last line (which isn't terminated by a \n, btw) */
592                 line = g_strdup (*p);
593                 (*p) += strlen (*p);
594         }
595
596         return line;
597 }
598
599 CamelNNTPNewsrc *
600 camel_nntp_newsrc_read_for_server (const char *server)
601 {
602         int fd;
603         char buf[1024];
604         char *file_contents, *line, *p;
605         char *filename;
606         CamelNNTPNewsrc *newsrc;
607         int newsrc_len;
608         int len_read = 0;
609         struct stat sb;
610
611         filename = g_strdup_printf ("%s/.newsrc-%s", g_get_home_dir(), server);
612
613         newsrc = g_new0(CamelNNTPNewsrc, 1);
614         newsrc->filename = filename;
615         newsrc->groups = g_hash_table_new (g_str_hash, g_str_equal);
616         newsrc->lock = g_mutex_new();
617         
618         if ((fd = g_open(filename, O_RDONLY, 0)) == -1) {
619                 g_warning ("~/.newsrc-%s not present.\n", server);
620                 return newsrc;
621         }
622
623         if (fstat (fd, &sb) == -1) {
624                 g_warning ("failed fstat on ~/.newsrc-%s: %s\n", server, strerror(errno));
625                 close (fd);
626                 return newsrc;
627         }
628         newsrc_len = sb.st_size;
629
630         file_contents = g_malloc (newsrc_len + 1);
631
632         while (len_read < newsrc_len) {
633                 int c = read (fd, buf, sizeof (buf));
634
635                 if (c == -1)
636                         break;
637
638                 memcpy (&file_contents[len_read], buf, c);
639                 len_read += c;
640         }
641         file_contents [len_read] = 0;
642
643         p = NULL;
644         while ((line = get_line (file_contents, &p))) {
645                 camel_nntp_newsrc_parse_line(newsrc, line);
646                 g_free (line);
647         }
648
649         close (fd);
650         g_free (file_contents);
651
652         return newsrc;
653 }