/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- * * Authors: Michael Zucchi * * Copyright (C) 2000 Ximian, Inc. * * This program is free software; you can redistribute it and/or * modify it under the terms of version 2 of the GNU General Public * License as published by the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * USA */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include #include #include "camel-mbox-store.h" #include "camel-mbox-folder.h" #include "camel-file-utils.h" #include "camel-text-index.h" #include "camel-exception.h" #include "camel-url.h" #define d(x) static CamelLocalStoreClass *parent_class = NULL; /* Returns the class for a CamelMboxStore */ #define CMBOXS_CLASS(so) CAMEL_MBOX_STORE_CLASS (CAMEL_OBJECT_GET_CLASS(so)) #define CF_CLASS(so) CAMEL_FOLDER_CLASS (CAMEL_OBJECT_GET_CLASS(so)) #define CMBOXF_CLASS(so) CAMEL_MBOX_FOLDER_CLASS (CAMEL_OBJECT_GET_CLASS(so)) extern char *camel_mbox_folder_get_full_path (const char *toplevel_dir, const char *full_name); extern char *camel_mbox_folder_get_meta_path (const char *toplevel_dir, const char *full_name, const char *ext); static CamelFolder *get_folder(CamelStore *store, const char *folder_name, guint32 flags, CamelException *ex); static void delete_folder(CamelStore *store, const char *folder_name, CamelException *ex); static void rename_folder (CamelStore *store, const char *old, const char *new, CamelException *ex); static CamelFolderInfo *create_folder (CamelStore *store, const char *parent_name, const char *folder_name, CamelException *ex); static CamelFolderInfo *get_folder_info (CamelStore *store, const char *top, guint32 flags, CamelException *ex); static void camel_mbox_store_class_init (CamelMboxStoreClass *camel_mbox_store_class) { CamelStoreClass *camel_store_class = CAMEL_STORE_CLASS (camel_mbox_store_class); parent_class = (CamelLocalStoreClass *)camel_type_get_global_classfuncs(camel_local_store_get_type()); /* virtual method overload */ camel_store_class->get_folder = get_folder; camel_store_class->delete_folder = delete_folder; camel_store_class->rename_folder = rename_folder; camel_store_class->create_folder = create_folder; camel_store_class->get_folder_info = get_folder_info; camel_store_class->free_folder_info = camel_store_free_folder_info_full; } CamelType camel_mbox_store_get_type (void) { static CamelType camel_mbox_store_type = CAMEL_INVALID_TYPE; if (camel_mbox_store_type == CAMEL_INVALID_TYPE) { camel_mbox_store_type = camel_type_register (CAMEL_LOCAL_STORE_TYPE, "CamelMboxStore", sizeof (CamelMboxStore), sizeof (CamelMboxStoreClass), (CamelObjectClassInitFunc) camel_mbox_store_class_init, NULL, NULL, NULL); } return camel_mbox_store_type; } static char * mbox_folder_name_to_path (CamelStore *store, const char *folder_name) { const char *toplevel_dir = CAMEL_LOCAL_STORE (store)->toplevel_dir; return camel_mbox_folder_get_full_path (toplevel_dir, folder_name); } static char * mbox_folder_name_to_meta_path (CamelStore *store, const char *folder_name, const char *ext) { const char *toplevel_dir = CAMEL_LOCAL_STORE (store)->toplevel_dir; return camel_mbox_folder_get_meta_path (toplevel_dir, folder_name, ext); } static char *extensions[] = { ".msf", ".ev-summary", ".ibex.index", ".ibex.index.data", ".cmeta" }; static gboolean ignore_file (const char *filename, gboolean sbd) { int flen, len, i; flen = strlen (filename); for (i = 0; i < (sizeof (extensions) / sizeof (extensions[0])); i++) { len = strlen (extensions[i]); if (len < flen && !strcmp (filename + flen - len, extensions[i])) return TRUE; } if (sbd && flen > 4 && !strcmp (filename + flen - 4, ".sbd")) return TRUE; return FALSE; } static CamelFolder * get_folder (CamelStore *store, const char *folder_name, guint32 flags, CamelException *ex) { struct stat st; char *name; if (!((CamelStoreClass *) parent_class)->get_folder (store, folder_name, flags, ex)) return NULL; name = mbox_folder_name_to_path (store, folder_name); if (stat (name, &st) == -1) { char *dirname; int fd; if (errno != ENOENT) { camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM, _("Could not open file `%s':\n%s"), name, g_strerror (errno)); g_free (name); return NULL; } if ((flags & CAMEL_STORE_FOLDER_CREATE) == 0) { camel_exception_setv (ex, CAMEL_EXCEPTION_STORE_NO_FOLDER, _("Folder `%s' does not exist."), folder_name); g_free (name); return NULL; } dirname = g_path_get_dirname (name); if (camel_mkdir (dirname, 0777) == -1 && errno != EEXIST) { camel_exception_setv (ex, CAMEL_EXCEPTION_STORE_NO_FOLDER, _("Could not create directory `%s':\n%s"), dirname, g_strerror (errno)); g_free (dirname); g_free (name); return NULL; } g_free (dirname); fd = open (name, O_WRONLY | O_CREAT | O_APPEND, 0666); if (fd == -1) { camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM, _("Could not create file `%s':\n%s"), name, g_strerror (errno)); g_free (name); return NULL; } g_free (name); close (fd); } else if (!S_ISREG (st.st_mode)) { camel_exception_setv (ex, CAMEL_EXCEPTION_STORE_NO_FOLDER, _("`%s' is not a regular file."), name); g_free (name); return NULL; } else g_free (name); return camel_mbox_folder_new (store, folder_name, flags, ex); } static void delete_folder (CamelStore *store, const char *folder_name, CamelException *ex) { CamelFolderInfo *fi; char *name, *path; struct stat st; name = mbox_folder_name_to_path (store, folder_name); path = g_strdup_printf ("%s.sbd", name); if (rmdir (path) == -1 && errno != ENOENT) { camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM, _("Could not delete folder `%s':\n%s"), folder_name, g_strerror (errno)); g_free (path); g_free (name); return; } g_free (path); if (stat (name, &st) == -1) { camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM, _("Could not delete folder `%s':\n%s"), folder_name, g_strerror (errno)); g_free (name); return; } if (!S_ISREG (st.st_mode)) { camel_exception_setv (ex, CAMEL_EXCEPTION_STORE_NO_FOLDER, _("`%s' is not a regular file."), name); g_free (name); return; } if (st.st_size != 0) { camel_exception_setv (ex, CAMEL_EXCEPTION_FOLDER_NON_EMPTY, _("Folder `%s' is not empty. Not deleted."), folder_name); g_free (name); return; } if (unlink (name) == -1 && errno != ENOENT) { camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM, _("Could not delete folder `%s':\n%s"), name, g_strerror (errno)); g_free (name); return; } /* FIXME: we have to do our own meta cleanup here rather than * calling our parent class' delete_folder() method since our * naming convention is different. Need to find a way for * CamelLocalStore to be able to construct the folder & meta * paths itself */ path = mbox_folder_name_to_meta_path (store, folder_name, ".ev-summary"); if (unlink (path) == -1 && errno != ENOENT) { camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM, _("Could not delete folder summary file `%s': %s"), path, g_strerror (errno)); g_free (path); g_free (name); return; } g_free (path); path = mbox_folder_name_to_meta_path (store, folder_name, ".ibex"); if (camel_text_index_remove (path) == -1 && errno != ENOENT) { camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM, _("Could not delete folder index file `%s': %s"), path, g_strerror (errno)); g_free (path); g_free (name); return; } g_free (path); g_free (name); fi = g_new0 (CamelFolderInfo, 1); fi->full_name = g_strdup (folder_name); fi->name = g_path_get_basename (folder_name); fi->url = g_strdup_printf ("mbox:%s#%s", ((CamelService *) store)->url->path, folder_name); fi->unread_message_count = -1; camel_folder_info_build_path (fi, '/'); camel_object_trigger_event (store, "folder_deleted", fi); camel_folder_info_free (fi); } static CamelFolderInfo * create_folder (CamelStore *store, const char *parent_name, const char *folder_name, CamelException *ex) { /* FIXME: this is almost an exact copy of CamelLocalStore::create_folder() except that we use * different path schemes... need to find a way to share parent's code? */ const char *toplevel_dir = ((CamelLocalStore *) store)->toplevel_dir; CamelFolderInfo *info = NULL; char *path, *name, *dir; CamelFolder *folder; struct stat st; if (toplevel_dir[0] != '/') { camel_exception_setv (ex, CAMEL_EXCEPTION_STORE_NO_FOLDER, _("Store root %s is not an absolute path"), toplevel_dir); return NULL; } if (folder_name[0] == '.' || ignore_file (folder_name, TRUE)) { camel_exception_set (ex, CAMEL_EXCEPTION_SYSTEM, _("Cannot create a folder by this name.")); return NULL; } if (parent_name && *parent_name) name = g_strdup_printf ("%s/%s", parent_name, folder_name); else name = g_strdup (folder_name); path = mbox_folder_name_to_path (store, name); dir = g_path_get_dirname (path); if (camel_mkdir (dir, 0777) == -1 && errno != EEXIST) { camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM, _("Cannot create directory `%s': %s."), dir, g_strerror (errno)); g_free (path); g_free (name); g_free (dir); return NULL; } g_free (dir); if (stat (path, &st) == 0 || errno != ENOENT) { camel_exception_setv (ex, CAMEL_EXCEPTION_STORE_NO_FOLDER, _("Cannot create folder: %s: %s"), path, errno ? g_strerror (errno) : _("Folder already exists")); g_free (path); g_free (name); return NULL; } g_free (path); folder = ((CamelStoreClass *) ((CamelObject *) store)->klass)->get_folder (store, name, CAMEL_STORE_FOLDER_CREATE, ex); if (folder) { camel_object_unref (folder); info = ((CamelStoreClass *) ((CamelObject *) store)->klass)->get_folder_info (store, name, 0, ex); } g_free (name); return info; } static int xrename (CamelStore *store, const char *old_name, const char *new_name, const char *ext, gboolean missingok, CamelException *ex) { const char *toplevel_dir = ((CamelLocalStore *) store)->toplevel_dir; char *oldpath, *newpath; struct stat st; int ret = -1; int err = 0; if (ext != NULL) { oldpath = camel_mbox_folder_get_meta_path (toplevel_dir, old_name, ext); newpath = camel_mbox_folder_get_meta_path (toplevel_dir, new_name, ext); } else { oldpath = camel_mbox_folder_get_full_path (toplevel_dir, old_name); newpath = camel_mbox_folder_get_full_path (toplevel_dir, new_name); } if (stat (oldpath, &st) == -1) { if (missingok && errno == ENOENT) { ret = 0; } else { err = errno; ret = -1; } } else if (S_ISDIR (st.st_mode)) { /* use rename for dirs */ if (rename (oldpath, newpath) == 0 || stat (newpath, &st) == 0) { ret = 0; } else { err = errno; ret = -1; } } else if (link (oldpath, newpath) == 0 /* and link for files */ || (stat (newpath, &st) == 0 && st.st_nlink == 2)) { if (unlink (oldpath) == 0) { ret = 0; } else { err = errno; unlink (newpath); ret = -1; } } else { err = errno; ret = -1; } if (ret == -1) { camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM, _("Could not rename %s to %s: %s"), oldpath, newpath, g_strerror (err)); } g_free (oldpath); g_free (newpath); return ret; } static void rename_folder (CamelStore *store, const char *old, const char *new, CamelException *ex) { CamelLocalFolder *folder = NULL; char *oldibex, *newibex; if (new[0] == '.' || ignore_file (new, TRUE)) { printf ("exception: The new folder name is illegal.\n"); camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM, _("The new folder name is illegal.")); return; } /* try to rollback failures, has obvious races */ oldibex = mbox_folder_name_to_meta_path (store, old, ".ibex"); newibex = mbox_folder_name_to_meta_path (store, new, ".ibex"); folder = camel_object_bag_get (store->folders, old); if (folder && folder->index) { if (camel_index_rename (folder->index, newibex) == -1) goto ibex_failed; } else { /* TODO: camel_text_index_rename should find out if we have an active index itself? */ if (camel_text_index_rename (oldibex, newibex) == -1) goto ibex_failed; } if (xrename (store, old, new, ".ev-summary", TRUE, ex)) goto summary_failed; if (xrename (store, old, new, NULL, FALSE, ex)) goto base_failed; g_free (oldibex); g_free (newibex); if (folder) camel_object_unref (folder); return; base_failed: xrename (store, new, old, ".ev-summary", TRUE, ex); summary_failed: if (folder) { if (folder->index) camel_index_rename (folder->index, oldibex); } else camel_text_index_rename (newibex, oldibex); ibex_failed: camel_exception_setv (ex, CAMEL_EXCEPTION_SYSTEM, _("Could not rename '%s': %s"), old, g_strerror (errno)); g_free (newibex); g_free (oldibex); if (folder) camel_object_unref (folder); } /* used to find out where we've visited already */ struct _inode { dev_t dnode; ino_t inode; }; static guint inode_hash (const void *d) { const struct _inode *v = d; return v->inode ^ v->dnode; } static gboolean inode_equal (const void *a, const void *b) { const struct _inode *v1 = a, *v2 = b; return v1->inode == v2->inode && v1->dnode == v2->dnode; } static void inode_free (void *k, void *v, void *d) { g_free (k); } static CamelFolderInfo * scan_dir (CamelStore *store, GHashTable *visited, CamelFolderInfo *parent, const char *root, const char *name, guint32 flags, CamelException *ex) { CamelFolderInfo *folders, *tail, *fi; GHashTable *folder_hash; struct dirent *dent; DIR *dir; tail = folders = NULL; if (!(dir = opendir (root))) return NULL; folder_hash = g_hash_table_new (g_str_hash, g_str_equal); /* FIXME: it would be better if we queue'd up the recursive * scans till the end so that we can limit the number of * directory descriptors open at any given time... */ while ((dent = readdir (dir))) { char *short_name, *full_name, *path, *ext; CamelFolder *folder; struct stat st; int unread = -1; if (dent->d_name[0] == '.') continue; if (ignore_file (dent->d_name, FALSE)) continue; path = g_strdup_printf ("%s/%s", root, dent->d_name); if (stat (path, &st) == -1) { g_free (path); continue; } if (S_ISDIR (st.st_mode)) { struct _inode in = { st.st_dev, st.st_ino }; if (g_hash_table_lookup (visited, &in)) { g_free (path); continue; } } short_name = g_strdup (dent->d_name); if ((ext = strrchr (short_name, '.')) && !strcmp (ext, ".sbd")) *ext = '\0'; if (name != NULL) full_name = g_strdup_printf ("%s/%s", name, short_name); else full_name = g_strdup (short_name); if (!S_ISDIR (st.st_mode)) { folder = camel_object_bag_get (store->folders, full_name); if (folder) { if ((flags & CAMEL_STORE_FOLDER_INFO_FAST) == 0) camel_folder_refresh_info (folder, NULL); unread = camel_folder_get_unread_message_count (folder); camel_object_unref (folder); } } if ((fi = g_hash_table_lookup (folder_hash, short_name)) != NULL) { g_free (short_name); g_free (full_name); if (S_ISDIR (st.st_mode)) { fi->flags = (fi->flags & ~CAMEL_FOLDER_NOINFERIORS) | CAMEL_FOLDER_CHILDREN; } else { fi->unread_message_count = unread; fi->flags &= ~CAMEL_FOLDER_NOSELECT; if ((ext = strchr (fi->url, ';')) && !strncmp (ext, ";noselect=yes", 13)) memmove (ext, ext + 13, strlen (ext + 13) + 1); } } else { fi = g_new0 (CamelFolderInfo, 1); fi->parent = parent; /* add ";noselect=yes" if we haven't found the mbox file yet. when we find it, remove the noselect */ fi->url = g_strdup_printf ("mbox:%s%s#%s", ((CamelService *) store)->url->path, S_ISDIR (st.st_mode) ? ";noselect=yes" : "", full_name); fi->name = short_name; fi->full_name = full_name; fi->path = g_strdup_printf ("/%s", full_name); fi->unread_message_count = unread; if (S_ISDIR (st.st_mode)) fi->flags = CAMEL_FOLDER_NOSELECT; else fi->flags = CAMEL_FOLDER_NOINFERIORS; if (tail == NULL) folders = fi; else tail->sibling = fi; tail = fi; g_hash_table_insert (folder_hash, fi->name, fi); } if ((flags & CAMEL_STORE_FOLDER_INFO_RECURSIVE) && S_ISDIR (st.st_mode)) { struct _inode in = { st.st_dev, st.st_ino }; if (g_hash_table_lookup (visited, &in) == NULL) { struct _inode *inew = g_new (struct _inode, 1); *inew = in; g_hash_table_insert (visited, inew, inew); if ((fi->child = scan_dir (store, visited, fi, path, fi->full_name, flags, ex))) fi->flags |= CAMEL_FOLDER_CHILDREN; } } g_free (path); } closedir (dir); g_hash_table_destroy (folder_hash); return folders; } static CamelFolderInfo * get_folder_info (CamelStore *store, const char *top, guint32 flags, CamelException *ex) { GHashTable *visited; struct _inode *inode; char *path, *subdir; CamelFolderInfo *fi; CamelFolder *folder; const char *base; struct stat st; int unread = -1; top = top ? top : ""; path = mbox_folder_name_to_path (store, top); if (*top == '\0') { /* requesting root dir scan */ if (stat (path, &st) == -1 || !S_ISDIR (st.st_mode)) { g_free (path); return NULL; } visited = g_hash_table_new (inode_hash, inode_equal); inode = g_new (struct _inode, 1); inode->dnode = st.st_dev; inode->inode = st.st_ino; g_hash_table_insert (visited, inode, inode); fi = scan_dir (store, visited, NULL, path, NULL, flags, ex); g_hash_table_foreach (visited, inode_free, NULL); g_hash_table_destroy (visited); g_free (path); return fi; } /* requesting scan of specific folder */ if (stat (path, &st) == -1 || !S_ISREG (st.st_mode)) { g_free (path); return NULL; } visited = g_hash_table_new (inode_hash, inode_equal); if (!(base = strrchr (top, '/'))) base = top; else base++; folder = camel_object_bag_get (store->folders, top); if (folder) { if ((flags & CAMEL_STORE_FOLDER_INFO_FAST) == 0) camel_folder_refresh_info (folder, NULL); unread = camel_folder_get_unread_message_count (folder); camel_object_unref (folder); } fi = g_new0 (CamelFolderInfo, 1); fi->parent = NULL; fi->url = g_strdup_printf ("mbox:%s#%s", ((CamelService *) store)->url->path, top); fi->name = g_strdup (base); fi->full_name = g_strdup (top); fi->unread_message_count = unread; fi->path = g_strdup_printf ("/%s", top); subdir = g_strdup_printf ("%s.sbd", path); if (stat (subdir, &st) == 0 && S_ISDIR (st.st_mode)) fi->child = scan_dir (store, visited, fi, subdir, top, flags, ex); if (fi->child) fi->flags |= CAMEL_FOLDER_CHILDREN; else fi->flags |= CAMEL_FOLDER_NOINFERIORS; g_free (subdir); g_hash_table_foreach (visited, inode_free, NULL); g_hash_table_destroy (visited); g_free (path); return fi; }