Fix FSF address (Tobias Mueller, #470445)
[platform/upstream/evolution-data-server.git] / camel / providers / local / camel-mh-store.c
1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
2  *
3  * Copyright (C) 2000 Ximian, Inc.
4  *
5  * Authors: Michael Zucchi <notzed@ximian.com>
6  *
7  * This program is free software; you can redistribute it and/or 
8  * modify it under the terms of version 2 of the GNU Lesser General Public 
9  * License as published by the Free Software Foundation.
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 Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
19  * USA
20  */
21
22 #ifdef HAVE_CONFIG_H
23 #include <config.h>
24 #endif
25
26 #include <dirent.h>
27 #include <errno.h>
28 #include <string.h>
29 #include <unistd.h>
30 #include <sys/stat.h>
31
32 #include <glib/gi18n-lib.h>
33
34 #include "camel-exception.h"
35 #include "camel-private.h"
36 #include "camel-stream-buffer.h"
37 #include "camel-stream-fs.h"
38 #include "camel-url.h"
39
40 #include "camel-mh-folder.h"
41 #include "camel-mh-store.h"
42 #include "camel-mh-summary.h"
43
44 static CamelLocalStoreClass *parent_class = NULL;
45
46 #define d(x)
47
48 /* Returns the class for a CamelMhStore */
49 #define CMHS_CLASS(so) CAMEL_MH_STORE_CLASS (CAMEL_OBJECT_GET_CLASS(so))
50 #define CF_CLASS(so) CAMEL_FOLDER_CLASS (CAMEL_OBJECT_GET_CLASS(so))
51 #define CMHF_CLASS(so) CAMEL_MH_FOLDER_CLASS (CAMEL_OBJECT_GET_CLASS(so))
52
53 static void construct (CamelService *service, CamelSession *session, CamelProvider *provider, CamelURL *url, CamelException *ex);
54 static CamelFolder *get_folder(CamelStore * store, const char *folder_name, guint32 flags, CamelException * ex);
55 static CamelFolder *get_inbox (CamelStore *store, CamelException *ex);
56 static void delete_folder(CamelStore * store, const char *folder_name, CamelException * ex);
57 static void rename_folder(CamelStore *store, const char *old, const char *new, CamelException *ex);
58 static CamelFolderInfo * get_folder_info (CamelStore *store, const char *top, guint32 flags, CamelException *ex);
59
60 static void camel_mh_store_class_init(CamelObjectClass * camel_mh_store_class)
61 {
62         CamelStoreClass *camel_store_class = CAMEL_STORE_CLASS(camel_mh_store_class);
63         CamelServiceClass *camel_service_class = CAMEL_SERVICE_CLASS(camel_mh_store_class);
64
65         parent_class = (CamelLocalStoreClass *)camel_type_get_global_classfuncs(camel_local_store_get_type());
66
67         /* virtual method overload, use defaults for most */
68         camel_service_class->construct = construct;
69
70         camel_store_class->get_folder = get_folder;
71         camel_store_class->get_inbox = get_inbox;
72         camel_store_class->delete_folder = delete_folder;
73         camel_store_class->rename_folder = rename_folder;
74         camel_store_class->get_folder_info = get_folder_info;
75 }
76
77 CamelType camel_mh_store_get_type(void)
78 {
79         static CamelType camel_mh_store_type = CAMEL_INVALID_TYPE;
80
81         if (camel_mh_store_type == CAMEL_INVALID_TYPE) {
82                 camel_mh_store_type = camel_type_register(CAMEL_LOCAL_STORE_TYPE, "CamelMhStore",
83                                                           sizeof(CamelMhStore),
84                                                           sizeof(CamelMhStoreClass),
85                                                           (CamelObjectClassInitFunc) camel_mh_store_class_init,
86                                                           NULL,
87                                                           NULL,
88                                                           NULL);
89         }
90
91         return camel_mh_store_type;
92 }
93
94 static void
95 construct (CamelService *service, CamelSession *session, CamelProvider *provider, CamelURL *url, CamelException *ex)
96 {
97         CamelMhStore *mh_store = (CamelMhStore *)service;
98
99         CAMEL_SERVICE_CLASS (parent_class)->construct (service, session, provider, url, ex);
100         if (camel_exception_is_set (ex))
101                 return;
102
103         if (camel_url_get_param(url, "dotfolders"))
104                 mh_store->flags |= CAMEL_MH_DOTFOLDERS;
105 }
106
107 enum {
108         UPDATE_NONE,
109         UPDATE_ADD,
110         UPDATE_REMOVE,
111         UPDATE_RENAME,
112 };
113
114 /* update the .folders file if it exists, or create it if it doesn't */
115 static void
116 folders_update(const char *root, int mode, const char *folder, const char *new)
117 {
118         char *tmp, *tmpnew, *line = NULL;
119         CamelStream *stream, *in = NULL, *out = NULL;
120         int flen = strlen(folder);
121
122         tmpnew = g_alloca (strlen (root) + 16);
123         sprintf (tmpnew, "%s.folders~", root);
124         
125         out = camel_stream_fs_new_with_name(tmpnew, O_WRONLY|O_CREAT|O_TRUNC, 0666);
126         if (out == NULL)
127                 goto fail;
128
129         tmp = g_alloca (strlen (root) + 16);
130         sprintf (tmp, "%s.folders", root);
131         stream = camel_stream_fs_new_with_name(tmp, O_RDONLY, 0);
132         if (stream) {
133                 in = camel_stream_buffer_new(stream, CAMEL_STREAM_BUFFER_READ);
134                 camel_object_unref(stream);
135         }
136         if (in == NULL || stream == NULL) {
137                 if (mode == UPDATE_ADD && camel_stream_printf(out, "%s\n", folder) == -1)
138                         goto fail;
139                 goto done;
140         }
141
142         while ((line = camel_stream_buffer_read_line((CamelStreamBuffer *)in))) {
143                 int copy = TRUE;
144
145                 switch (mode) {
146                 case UPDATE_REMOVE:
147                         if (strcmp(line, folder) == 0)
148                                 copy = FALSE;
149                         break;
150                 case UPDATE_RENAME:
151                         if (strncmp(line, folder, flen) == 0
152                             && (line[flen] == 0 || line[flen] == '/')) {
153                                 if (camel_stream_write(out, new, strlen(new)) == -1
154                                     || camel_stream_write(out, line+flen, strlen(line)-flen) == -1
155                                     || camel_stream_write(out, "\n", 1) == -1)
156                                         goto fail;
157                                 copy = FALSE;
158                         }
159                         break;
160                 case UPDATE_ADD: {
161                         int cmp = strcmp(line, folder);
162
163                         if (cmp > 0) {
164                                 /* found insertion point */
165                                 if (camel_stream_printf(out, "%s\n", folder) == -1)
166                                         goto fail;
167                                 mode = UPDATE_NONE;
168                         } else if (tmp == 0) {
169                                 /* already there */
170                                 mode = UPDATE_NONE;
171                         }
172                         break; }
173                 case UPDATE_NONE:
174                         break;
175                 }
176
177                 if (copy && camel_stream_printf(out, "%s\n", line) == -1)
178                         goto fail;
179
180                 g_free(line);
181                 line = NULL;
182         }
183
184         /* add to end? */
185         if (mode == UPDATE_ADD && camel_stream_printf(out, "%s\n", folder) == -1)
186                 goto fail;
187
188         if (camel_stream_close(out) == -1)
189                 goto fail;
190
191 done:
192         /* should we care if this fails?  I suppose so ... */
193         rename(tmpnew, tmp);
194 fail:
195         unlink(tmpnew);         /* remove it if its there */
196         g_free(line);
197         if (in)
198                 camel_object_unref(in);
199         if (out)
200                 camel_object_unref(out);
201 }
202
203 static CamelFolder *
204 get_folder(CamelStore * store, const char *folder_name, guint32 flags, CamelException * ex)
205 {
206         char *name;
207         struct stat st;
208
209         if (!((CamelStoreClass *)parent_class)->get_folder(store, folder_name, flags, ex))
210                 return NULL;
211
212         name = g_strdup_printf("%s%s", CAMEL_LOCAL_STORE(store)->toplevel_dir, folder_name);
213
214         if (stat(name, &st) == -1) {
215                 if (errno != ENOENT) {
216                         camel_exception_setv(ex, CAMEL_EXCEPTION_SYSTEM,
217                                              _("Cannot get folder `%s': %s"),
218                                              folder_name, g_strerror (errno));
219                         g_free (name);
220                         return NULL;
221                 }
222                 if ((flags & CAMEL_STORE_FOLDER_CREATE) == 0) {
223                         camel_exception_setv(ex, CAMEL_EXCEPTION_STORE_NO_FOLDER,
224                                              _("Cannot get folder `%s': folder does not exist."),
225                                              folder_name);
226                         g_free (name);
227                         return NULL;
228                 }
229
230                 if (mkdir(name, 0777) != 0) {
231                         camel_exception_setv(ex, CAMEL_EXCEPTION_SYSTEM,
232                                              _("Could not create folder `%s': %s"),
233                                              folder_name, g_strerror (errno));
234                         g_free (name);
235                         return NULL;
236                 }
237
238                 /* add to .folders if we are supposed to */
239                 /* FIXME: throw exception on error */
240                 if (((CamelMhStore *)store)->flags & CAMEL_MH_DOTFOLDERS)
241                         folders_update(((CamelLocalStore *)store)->toplevel_dir, UPDATE_ADD, folder_name, NULL);
242         } else if (!S_ISDIR(st.st_mode)) {
243                 camel_exception_setv(ex, CAMEL_EXCEPTION_STORE_NO_FOLDER,
244                                      _("Cannot get folder `%s': not a directory."), folder_name);
245                 g_free (name);
246                 return NULL;
247         } else if (flags & CAMEL_STORE_FOLDER_EXCL) {
248                 camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
249                                       _("Cannot create folder `%s': folder exists."), folder_name);
250                 g_free (name);
251                 return NULL;
252         }
253         
254         g_free(name);
255
256         return camel_mh_folder_new(store, folder_name, flags, ex);
257 }
258
259 static CamelFolder *
260 get_inbox (CamelStore *store, CamelException *ex)
261 {
262         return get_folder (store, "inbox", 0, ex);
263 }
264
265 static void delete_folder(CamelStore * store, const char *folder_name, CamelException * ex)
266 {
267         char *name;
268
269         /* remove folder directory - will fail if not empty */
270         name = g_strdup_printf("%s%s", CAMEL_LOCAL_STORE(store)->toplevel_dir, folder_name);
271         if (rmdir(name) == -1) {
272                 camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM,
273                                       _("Could not delete folder `%s': %s"),
274                                       folder_name, g_strerror (errno));
275                 g_free(name);
276                 return;
277         }
278         g_free(name);
279
280         /* remove from .folders if we are supposed to */
281         if (((CamelMhStore *)store)->flags & CAMEL_MH_DOTFOLDERS)
282                 folders_update(((CamelLocalStore *)store)->toplevel_dir, UPDATE_REMOVE, folder_name, NULL);
283
284         /* and remove metadata */
285         ((CamelStoreClass *)parent_class)->delete_folder(store, folder_name, ex);
286 }
287
288 static void
289 rename_folder(CamelStore *store, const char *old, const char *new, CamelException *ex)
290 {
291         CamelException e;
292
293         camel_exception_init(&e);
294         ((CamelStoreClass *)parent_class)->rename_folder(store, old, new, &e);
295         if (camel_exception_is_set(&e)) {
296                 camel_exception_xfer(ex, &e);
297                 return;
298         }
299         camel_exception_clear(&e);
300
301         if (((CamelMhStore *)store)->flags & CAMEL_MH_DOTFOLDERS) {
302                 /* yeah this is messy, but so is mh! */
303                 folders_update(((CamelLocalStore *)store)->toplevel_dir, UPDATE_RENAME, old, new);
304         }
305 }
306
307 static void
308 fill_fi(CamelStore *store, CamelFolderInfo *fi, guint32 flags)
309 {
310         CamelFolder *folder;
311
312         folder = camel_object_bag_get(store->folders, fi->full_name);
313
314         if (folder == NULL
315             && (flags & CAMEL_STORE_FOLDER_INFO_FAST) == 0)
316                 folder = camel_store_get_folder(store, fi->full_name, 0, NULL);
317
318         if (folder) {
319                 if ((flags & CAMEL_STORE_FOLDER_INFO_FAST) == 0)
320                         camel_folder_refresh_info(folder, NULL);
321                 fi->unread = camel_folder_get_unread_message_count(folder);
322                 fi->total = camel_folder_get_message_count(folder);
323                 camel_object_unref(folder);
324         } else {
325                 char *path, *folderpath;
326                 CamelFolderSummary *s;
327                 const char *root;
328
329                 /* This should be fast enough not to have to test for INFO_FAST */
330
331                 /* We could: if we have no folder, and FAST isn't specified, perform a full
332                    scan of all messages for their status flags.  But its probably not worth
333                    it as we need to read the top of every file, i.e. very very slow */
334
335                 root = camel_local_store_get_toplevel_dir((CamelLocalStore *)store);
336                 path = g_strdup_printf("%s/%s.ev-summary", root, fi->full_name);
337                 folderpath = g_strdup_printf("%s/%s", root, fi->full_name);
338                 s = (CamelFolderSummary *)camel_mh_summary_new(NULL, path, folderpath, NULL);
339                 if (camel_folder_summary_header_load(s) != -1) {
340                         fi->unread = s->unread_count;
341                         fi->total = s->saved_count;
342                 }
343                 camel_object_unref(s);
344                 g_free(folderpath);
345                 g_free(path);
346         }
347 }
348
349 static CamelFolderInfo *
350 folder_info_new (CamelStore *store, CamelURL *url, const char *root, const char *path, guint32 flags)
351 {
352         /* FIXME: need to set fi->flags = CAMEL_FOLDER_NOSELECT (and possibly others) when appropriate */
353         CamelFolderInfo *fi;
354         char *base;
355
356         base = strrchr(path, '/');
357         
358         camel_url_set_fragment (url, path);
359         
360         /* Build the folder info structure. */
361         fi = g_malloc0(sizeof(*fi));
362         fi->uri = camel_url_to_string (url, 0);
363         fi->full_name = g_strdup(path);
364         fi->name = g_strdup(base?base+1:path);
365         fill_fi(store, fi, flags);
366
367         d(printf("New folderinfo:\n '%s'\n '%s'\n '%s'\n", fi->full_name, fi->uri, fi->path));
368
369         return fi;
370 }
371
372 /* used to find out where we've visited already */
373 struct _inode {
374         dev_t dnode;
375         ino_t inode;
376 };
377
378 /* Scan path, under root, for directories to add folders for.  Both
379  * root and path should have a trailing "/" if they aren't empty. */
380 static void
381 recursive_scan (CamelStore *store, CamelURL *url, CamelFolderInfo **fip, CamelFolderInfo *parent,
382                 GHashTable *visited, const char *root, const char *path, guint32 flags)
383 {
384         char *fullpath, *tmp;
385         DIR *dp;
386         struct dirent *d;
387         struct stat st;
388         CamelFolderInfo *fi;
389         struct _inode in, *inew;
390
391         /* Open the specified directory. */
392         if (path[0]) {
393                 fullpath = alloca (strlen (root) + strlen (path) + 2);
394                 sprintf (fullpath, "%s/%s", root, path);
395         } else 
396                 fullpath = (char *)root;
397
398         if (stat(fullpath, &st) == -1 || !S_ISDIR(st.st_mode))
399                 return;
400
401         in.dnode = st.st_dev;
402         in.inode = st.st_ino;
403
404         /* see if we've visited already */
405         if (g_hash_table_lookup(visited, &in) != NULL)
406                 return;
407
408         inew = g_malloc(sizeof(*inew));
409         *inew = in;
410         g_hash_table_insert(visited, inew, inew);
411
412         /* link in ... */
413         fi = folder_info_new(store, url, root, path, flags);
414         fi->parent = parent;
415         fi->next = *fip;
416         *fip = fi;
417
418         if (((flags & CAMEL_STORE_FOLDER_INFO_RECURSIVE) || parent == NULL)) {
419                 /* now check content for possible other directories */
420                 dp = opendir(fullpath);
421                 if (dp == NULL)
422                         return;
423
424                 /* Look for subdirectories to add and scan. */
425                 while ((d = readdir(dp)) != NULL) {
426                         /* Skip current and parent directory. */
427                         if (strcmp(d->d_name, ".") == 0
428                             || strcmp(d->d_name, "..") == 0)
429                                 continue;
430
431                         /* skip fully-numerical entries (i.e. mh messages) */
432                         strtoul(d->d_name, &tmp, 10);
433                         if (*tmp == 0)
434                                 continue;
435
436                         /* otherwise, treat at potential node, and recurse, a bit more expensive than needed, but tough! */
437                         if (path[0]) {
438                                 tmp = g_strdup_printf("%s/%s", path, d->d_name);
439                                 recursive_scan(store, url, &fi->child, fi, visited, root, tmp, flags);
440                                 g_free(tmp);
441                         } else {
442                                 recursive_scan(store, url, &fi->child, fi, visited, root, d->d_name, flags);
443                         }
444                 }
445
446                 closedir(dp);
447         }
448 }
449
450 /* scan a .folders file */
451 static void
452 folders_scan(CamelStore *store, CamelURL *url, const char *root, const char *top, CamelFolderInfo **fip, guint32 flags)
453 {
454         CamelFolderInfo *fi;
455         char  line[512], *path, *tmp;
456         CamelStream *stream, *in;
457         struct stat st;
458         GPtrArray *folders;
459         GHashTable *visited;
460         int len;
461
462         tmp = g_alloca (strlen (root) + 16);
463         sprintf (tmp, "%s/.folders", root);
464         stream = camel_stream_fs_new_with_name(tmp, 0, O_RDONLY);
465         if (stream == NULL)
466                 return;
467
468         in = camel_stream_buffer_new(stream, CAMEL_STREAM_BUFFER_READ);
469         camel_object_unref(stream);
470         if (in == NULL)
471                 return;
472
473         visited = g_hash_table_new(g_str_hash, g_str_equal);
474         folders = g_ptr_array_new();
475
476         while ( (len = camel_stream_buffer_gets((CamelStreamBuffer *)in, line, sizeof(line))) > 0) {
477                 /* ignore blank lines */
478                 if (len <= 1)
479                         continue;
480                 /* check for invalidly long lines, we abort evreything and fallback */
481                 if (line[len-1] != '\n') {
482                         int i;
483
484                         for (i=0;i<folders->len;i++)
485                                 camel_folder_info_free(folders->pdata[i]);
486                         g_ptr_array_set_size(folders, 0);
487                         break;
488                 }
489                 line[len-1] = 0;
490
491                 /* check for \r ? */
492
493                 if (top && top[0]) {
494                         int toplen = strlen(top);
495
496                         /* check is dir or subdir */
497                         if (strncmp(top, line, toplen) != 0
498                             || (line[toplen] != 0 && line[toplen] != '/'))
499                                 continue;
500                 
501                         /* check is not sub-subdir if not recursive */
502                         if ((flags & CAMEL_STORE_FOLDER_INFO_RECURSIVE) == 0
503                             && (tmp = strrchr(line, '/'))
504                             && tmp > line+toplen)
505                                 continue;
506                 }
507
508                 if (g_hash_table_lookup(visited, line) != NULL)
509                         continue;
510
511                 tmp = g_strdup(line);
512                 g_hash_table_insert(visited, tmp, tmp);
513
514                 path = g_strdup_printf("%s/%s", root, line);
515                 if (stat(path, &st) == 0 && S_ISDIR(st.st_mode)) {
516                         fi = folder_info_new(store, url, root, line, flags);
517                         g_ptr_array_add(folders, fi);
518                 }
519                 g_free(path);
520         }
521
522         if (folders->len)
523                 *fip = camel_folder_info_build(folders, top, '/', TRUE);
524         g_ptr_array_free(folders, TRUE);
525
526         g_hash_table_foreach(visited, (GHFunc)g_free, NULL);
527         g_hash_table_destroy(visited);
528
529         camel_object_unref(in);
530 }
531
532 /* FIXME: move to camel-local, this is shared with maildir code */
533 static guint inode_hash(const void *d)
534 {
535         const struct _inode *v = d;
536
537         return v->inode ^ v->dnode;
538 }
539
540 static gboolean inode_equal(const void *a, const void *b)
541 {
542         const struct _inode *v1 = a, *v2 = b;
543         
544         return v1->inode == v2->inode && v1->dnode == v2->dnode;
545 }
546
547 static void inode_free(void *k, void *v, void *d)
548 {
549         g_free(k);
550 }
551
552 static CamelFolderInfo *
553 get_folder_info (CamelStore *store, const char *top, guint32 flags, CamelException *ex)
554 {
555         CamelFolderInfo *fi = NULL;
556         CamelURL *url;
557         char *root;
558         
559         root = ((CamelService *)store)->url->path;
560         
561         url = camel_url_copy (((CamelService *) store)->url);
562         
563         /* use .folders if we are supposed to */
564         if (((CamelMhStore *)store)->flags & CAMEL_MH_DOTFOLDERS) {
565                 folders_scan(store, url, root, top, &fi, flags);
566         } else {
567                 GHashTable *visited = g_hash_table_new(inode_hash, inode_equal);
568
569                 if (top == NULL)
570                         top = "";
571
572                 recursive_scan(store, url, &fi, NULL, visited, root, top, flags);
573
574                 /* if we actually scanned from root, we have a "" root node we dont want */
575                 if (fi != NULL && top[0] == 0) {
576                         CamelFolderInfo *rfi;
577
578                         rfi = fi;
579                         fi = rfi->child;
580                         rfi->child = NULL;
581                         camel_folder_info_free(rfi);
582                 }
583
584                 g_hash_table_foreach(visited, inode_free, NULL);
585                 g_hash_table_destroy(visited);
586         }
587         
588         camel_url_free (url);
589         
590         return fi;
591 }