/* -*-mode: c; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-*/ /* ** Copyright (C) 2008-2011 Dirk-Jan C. Binnema ** ** This program is free software; you can redistribute it and/or modify it ** under the terms of the GNU General Public License as published by the ** Free Software Foundation; either version 3, or (at your option) any ** later version. ** ** 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. ** */ #if HAVE_CONFIG_H #include "config.h" #endif /*HAVE_CONFIG_H*/ #include #include #include #include /* hopefully, the should get us a sane PATH_MAX */ #include /* not all systems provide PATH_MAX in limits.h */ #ifndef PATH_MAX #include #ifndef PATH_MAX #define PATH_MAX MAXPATHLEN #endif /*!PATH_MAX */ #endif /*PATH_MAX */ #include #include #include #include "mu-util.h" #include "mu-maildir.h" #include "mu-str.h" #define MU_MAILDIR_NOINDEX_FILE ".noindex" /* On Linux (and some BSD), we have entry->d_type, but some file * systems (XFS, ReiserFS) do not support it, and set it DT_UNKNOWN. * On other OSs, notably Solaris, entry->d_type is not present at all. * For these cases, we use lstat (in get_dtype) as a slower fallback, * and return it in the d_type parameter */ #ifdef HAVE_STRUCT_DIRENT_D_TYPE #define GET_DTYPE(DE,FP) \ ((DE)->d_type == DT_UNKNOWN ? mu_util_get_dtype_with_lstat((FP)) : (DE)->d_type) #else #define GET_DTYPE(DE,FP) \ mu_util_get_dtype_with_lstat((FP)) #endif /*HAVE_STRUCT_DIRENT_D_TYPE*/ static gboolean create_maildir (const char *path, mode_t mode, GError **err) { int i; const gchar* subdirs[] = {"new", "cur", "tmp"}; /* make sure it does not exist yet */ if (access (path, F_OK) == 0) errno = EEXIST; if (errno != ENOENT) { g_set_error (err, 0, MU_ERROR_FILE, "%s", strerror (errno)); return FALSE; } for (i = 0; i != G_N_ELEMENTS(subdirs); ++i) { const char *fullpath; int rv; /* static buffer */ fullpath = mu_str_fullpath_s (path, subdirs[i]); rv = g_mkdir_with_parents (fullpath, (int)mode); if (rv != 0) { g_set_error (err, 0, MU_ERROR_FILE_CANNOT_MKDIR, "g_mkdir_with_parents failed: %s", strerror (errno)); return FALSE; } } return TRUE; } static gboolean create_noindex (const char *path, GError **err) { /* create a noindex file if requested */ int fd; const char *noindexpath; /* static buffer */ noindexpath = mu_str_fullpath_s (path, MU_MAILDIR_NOINDEX_FILE); fd = creat (noindexpath, 0644); /* note, if the 'close' failed, creation may still have * succeeded...*/ if (fd < 0 || close (fd) != 0) { g_set_error (err, 0, MU_ERROR_FILE_CANNOT_CREATE, "error in create_noindex: %s", strerror (errno)); return FALSE; } return TRUE; } gboolean mu_maildir_mkdir (const char* path, mode_t mode, gboolean noindex, GError **err) { g_return_val_if_fail (path, FALSE); MU_WRITE_LOG ("%s (%s, %o, %s)", __FUNCTION__, path, mode, noindex ? "TRUE" : "FALSE"); if (!create_maildir (path, mode, err)) return FALSE; if (noindex && !create_noindex (path, err)) return FALSE; return TRUE; } /* determine whether the source message is in 'new' or in 'cur'; * we ignore messages in 'tmp' for obvious reasons */ static gboolean check_subdir (const char *src, gboolean *in_cur, GError **err) { gchar *srcpath; srcpath = g_path_get_dirname (src); if (g_str_has_suffix (srcpath, "new")) *in_cur = FALSE; else if (g_str_has_suffix (srcpath, "cur")) *in_cur = TRUE; else { g_set_error(err, 0, MU_ERROR_FILE_INVALID_SOURCE, "invalid source message '%s'", src); return FALSE; } g_free (srcpath); return TRUE; } static gchar* get_target_fullpath (const char* src, const gchar *targetpath, GError **err) { gchar *targetfullpath, *srcfile; gboolean in_cur; if (!check_subdir (src, &in_cur, err)) return NULL; srcfile = g_path_get_basename (src); /* create targetpath; note: make the filename cough* unique by *including a hash * of the srcname in the targetname. This *helps if there are * copies of a message (which all have the *same basename)*/ targetfullpath = g_strdup_printf ("%s%c%s%c%u_%s", targetpath, G_DIR_SEPARATOR, in_cur ? "cur" : "new", G_DIR_SEPARATOR, g_str_hash(src), srcfile); g_free (srcfile); return targetfullpath; } gboolean mu_maildir_link (const char* src, const char *targetpath, GError **err) { gchar *targetfullpath; int rv; g_return_val_if_fail (src, FALSE); g_return_val_if_fail (targetpath, FALSE); targetfullpath = get_target_fullpath (src, targetpath, err); if (!targetfullpath) return FALSE; rv = symlink (src, targetfullpath); if (rv != 0) { g_set_error (err, 0, MU_ERROR_FILE_CANNOT_LINK, "error creating link %s => %s: %s", targetfullpath, src, strerror (errno)); g_free (targetfullpath); return FALSE; } g_free (targetfullpath); return TRUE; } static MuError process_dir (const char* path, const gchar *mdir, MuMaildirWalkMsgCallback msg_cb, MuMaildirWalkDirCallback dir_cb, void *data); static MuError process_file (const char* fullpath, const gchar* mdir, MuMaildirWalkMsgCallback msg_cb, void *data) { MuError result; struct stat statbuf; if (!msg_cb) return MU_OK; if (G_UNLIKELY(access(fullpath, R_OK) != 0)) { g_warning ("cannot access %s: %s", fullpath, strerror(errno)); return MU_ERROR; } if (G_UNLIKELY(stat (fullpath, &statbuf) != 0)) { g_warning ("cannot stat %s: %s", fullpath, strerror(errno)); return MU_ERROR; } result = (msg_cb)(fullpath, mdir, &statbuf, data); if (result == MU_STOP) g_debug ("callback said 'MU_STOP' for %s", fullpath); else if (result == MU_ERROR) g_warning ("%s: error in callback (%s)", __FUNCTION__, fullpath); return result; } /* * determine if path is a maildir leaf-dir; ie. if it's 'cur' or 'new' * (we're skipping 'tmp' for obvious reasons) */ G_GNUC_CONST static gboolean is_maildir_new_or_cur (const char *path) { size_t len; g_return_val_if_fail (path, FALSE); /* path is the full path; it cannot possibly be shorter * than 4 for a maildir (/cur or /new) */ len = strlen (path); if (G_UNLIKELY(len < 4)) return FALSE; /* optimization; one further idea would be cast the 4 bytes to an integer * and compare that -- need to think about alignment, endianness */ if (path[len - 4] == G_DIR_SEPARATOR && path[len - 3] == 'c' && path[len - 2] == 'u' && path[len - 1] == 'r') return TRUE; if (path[len - 4] == G_DIR_SEPARATOR && path[len - 3] == 'n' && path[len - 2] == 'e' && path[len - 1] == 'w') return TRUE; return FALSE; } /* check if there is a noindex file (MU_MAILDIR_NOINDEX_FILE) in this * dir; */ static gboolean has_noindex_file (const char *path) { const char* noindexpath; /* static buffer */ noindexpath = mu_str_fullpath_s (path, MU_MAILDIR_NOINDEX_FILE); if (access (noindexpath, F_OK) == 0) return TRUE; else if (G_UNLIKELY(errno != ENOENT)) g_warning ("error testing for noindex file %s: %s", noindexpath, strerror(errno)); return FALSE; } static gboolean is_dotdir_to_ignore (const char* dir) { int i; const char* ignore[] = { ".notmuch", ".nnmaildir", ".#evolution" }; /* when adding names, check the optimization below */ if (dir[0] != '.') return FALSE; /* not a dotdir */ if (dir[1] == '\0' || (dir[1] == '.' && dir[2] == '\0')) return TRUE; /* ignore '.' and '..' */ /* optimization: special dirs have 'n' or '#' in pos 1 */ if (dir[1] != 'n' && dir[1] != '#') return FALSE; /* not special: don't ignore */ for (i = 0; i != G_N_ELEMENTS(ignore); ++i) if (strcmp(dir, ignore[i]) == 0) return TRUE; return FALSE; /* don't ignore */ } static gboolean ignore_dir_entry (struct dirent *entry, unsigned char d_type) { const char *name; /* if it's not a dir and not a file, ignore it. * note, this means also symlinks (DT_LNK) are ignored, * maybe make this optional */ if (G_UNLIKELY(d_type != DT_REG && d_type != DT_DIR)) return TRUE; name = entry->d_name; /* ignore '.' and '..' dirs, as well as .notmuch and * .nnmaildir */ return is_dotdir_to_ignore (entry->d_name); } static gchar* get_mdir_for_path (const gchar *old_mdir, const gchar *dir) { if (dir[0] != 'n' && dir[0] != 'c' && strcmp(dir, "cur") != 0 && strcmp(dir, "new") != 0) return g_strconcat (old_mdir ? old_mdir : "", G_DIR_SEPARATOR_S, dir, NULL); else return strdup (old_mdir ? old_mdir : G_DIR_SEPARATOR_S); } static MuError process_dir_entry (const char* path, const char* mdir, struct dirent *entry, MuMaildirWalkMsgCallback cb_msg, MuMaildirWalkDirCallback cb_dir, void *data) { const char *fp; char* fullpath; unsigned char d_type; /* we have to copy the buffer from fullpath_s, because it * returns a static buffer, and we maybe called reentrantly */ fp = mu_str_fullpath_s (path, entry->d_name); fullpath = g_newa (char, strlen(fp) + 1); strcpy (fullpath, fp); d_type = GET_DTYPE(entry, fullpath); /* ignore special files/dirs */ if (ignore_dir_entry (entry, d_type)) return MU_OK; switch (d_type) { case DT_REG: /* we only want files in cur/ and new/ */ if (!is_maildir_new_or_cur (path)) return MU_OK; return process_file (fullpath, mdir, cb_msg, data); case DT_DIR: { char *my_mdir; MuError rv; my_mdir = get_mdir_for_path (mdir, entry->d_name); rv = process_dir (fullpath, my_mdir, cb_msg, cb_dir, data); g_free (my_mdir); return rv; } default: return MU_OK; /* ignore other types */ } } static struct dirent* dirent_copy (struct dirent *entry) { struct dirent *d; d = g_slice_new (struct dirent); /* NOTE: simply memcpy'ing sizeof(struct dirent) bytes will * give memory errors. */ return (struct dirent*) memcpy (d, entry, entry->d_reclen); } static void dirent_destroy (struct dirent *entry) { g_slice_free (struct dirent, entry); } #ifdef HAVE_STRUCT_DIRENT_D_INO static int dirent_cmp (struct dirent *d1, struct dirent *d2) { /* we do it his way instead of a simple d1->d_ino - d2->d_ino * because this way, we don't need 64-bit numbers for the * actual sorting */ if (d1->d_ino < d2->d_ino) return -1; else if (d1->d_ino > d2->d_ino) return 1; else return 0; } #endif /*HAVE_STRUCT_DIRENT_D_INO*/ static MuError process_dir_entries (DIR *dir, const char* path, const char* mdir, MuMaildirWalkMsgCallback msg_cb, MuMaildirWalkDirCallback dir_cb, void *data) { MuError result; GSList *lst, *c; struct dirent *entry; lst = NULL; while ((entry = readdir (dir))) lst = g_slist_prepend (lst, dirent_copy(entry)); /* we sort by inode; this makes things much faster on * extfs2,3 */ #if HAVE_STRUCT_DIRENT_D_INO c = lst = g_slist_sort (lst, (GCompareFunc)dirent_cmp); #endif /*HAVE_STRUCT_DIRENT_D_INO*/ for (c = lst, result = MU_OK; c && result == MU_OK; c = g_slist_next(c)) { result = process_dir_entry (path, mdir, (struct dirent*)c->data, msg_cb, dir_cb, data); /* hmmm, break on MU_ERROR as well? */ if (result == MU_STOP) break; } g_slist_foreach (lst, (GFunc)dirent_destroy, NULL); g_slist_free (lst); return result; } static MuError process_dir (const char* path, const char* mdir, MuMaildirWalkMsgCallback msg_cb, MuMaildirWalkDirCallback dir_cb, void *data) { MuError result; DIR* dir; /* if it has a noindex file, we ignore this dir */ if (has_noindex_file (path)) { g_debug ("found .noindex: ignoring dir %s", path); return MU_OK; } dir = opendir (path); if (G_UNLIKELY(!dir)) { g_warning ("%s: ignoring %s: %s", __FUNCTION__, path, strerror(errno)); return MU_OK; } if (dir_cb) { MuError rv; rv = dir_cb (path, TRUE, data); if (rv != MU_OK) { closedir (dir); return rv; } } result = process_dir_entries (dir, path, mdir, msg_cb, dir_cb, data); closedir (dir); /* only run dir_cb if it exists and so far, things went ok */ if (dir_cb && result == MU_OK) return dir_cb (path, FALSE, data); return result; } MuError mu_maildir_walk (const char *path, MuMaildirWalkMsgCallback cb_msg, MuMaildirWalkDirCallback cb_dir, void *data) { MuError rv; char *mypath; g_return_val_if_fail (path && cb_msg, MU_ERROR); g_return_val_if_fail (mu_util_check_dir(path, TRUE, FALSE), MU_ERROR); /* strip the final / or \ */ mypath = g_strdup (path); if (mypath[strlen(mypath)-1] == G_DIR_SEPARATOR) mypath[strlen(mypath)-1] = '\0'; rv = process_dir (mypath, NULL, cb_msg, cb_dir, data); g_free (mypath); return rv; } static gboolean clear_links (const gchar* dirname, DIR *dir, GError **err) { struct dirent *entry; gboolean rv; rv = TRUE; errno = 0; while ((entry = readdir (dir))) { const char *fp; char *fullpath; unsigned char d_type; /* ignore empty, dot thingies */ if (!entry->d_name || entry->d_name[0] == '.') continue; /* we have to copy the buffer from fullpath_s, because * it returns a static buffer and we are * recursive*/ fp = mu_str_fullpath_s (dirname, entry->d_name); fullpath = g_newa (char, strlen(fp) + 1); strcpy (fullpath, fp); d_type = GET_DTYPE (entry, fullpath); /* ignore non-links / non-dirs */ if (d_type != DT_LNK && d_type != DT_DIR) continue; if (d_type == DT_LNK) { if (unlink (fullpath) != 0) { /* don't use err */ g_warning ("error unlinking %s: %s", fullpath, strerror(errno)); rv = FALSE; } } else /* DT_DIR, see check before*/ rv = mu_maildir_clear_links (fullpath, err); } if (errno != 0) g_set_error (err, 0, MU_ERROR_FILE, "file error: %s", strerror(errno)); return (rv == FALSE && errno == 0); } gboolean mu_maildir_clear_links (const gchar* path, GError **err) { DIR *dir; gboolean rv; g_return_val_if_fail (path, FALSE); dir = opendir (path); if (!dir) { g_set_error (err, 0, MU_ERROR_FILE_CANNOT_OPEN, "failed to open %s: %s", path, strerror(errno)); return FALSE; } rv = clear_links (path, dir, err); closedir (dir); return rv; } MuFlags mu_maildir_get_flags_from_path (const char *path) { g_return_val_if_fail (path, MU_FLAG_INVALID); /* try to find the info part */ /* note that we can use either the ':' or '!' as separator; * the former is the official, but as it does not work on e.g. VFAT * file systems, some Maildir implementations use the latter instead * (or both). For example, Tinymail/modest does this. The python * documentation at http://docs.python.org/lib/mailbox-maildir.html * mentions the '!' as well as a 'popular choice' */ /* we check the dir -- */ if (strstr (path, G_DIR_SEPARATOR_S "new" G_DIR_SEPARATOR_S)) { char *dir, *dir2; MuFlags flags; dir = g_path_get_dirname (path); dir2 = g_path_get_basename (dir); if (g_strcmp0 (dir2, "new") == 0) flags = MU_FLAG_NEW; g_free (dir); g_free (dir2); /* NOTE: new/ message should not have :2,-stuff, as * per http://cr.yp.to/proto/maildir.html. If they, do * we ignore it */ if (flags == MU_FLAG_NEW) return flags; } /* get the file flags */ { char *info; info = strrchr (path, '2'); if (!info || info == path || (info[-1] != ':' && info[-1] != '!') || (info[1] != ',')) return MU_FLAG_NONE; else return mu_flags_from_str (&info[2], MU_FLAG_TYPE_MAILFILE); } } /* * take an exising message path, and return a new path, based on * whether it should be in 'new' or 'cur'; ie. * * /home/user/Maildir/foo/bar/cur/abc:2,F and flags == MU_FLAG_NEW * => /home/user/Maildir/foo/bar/new * and * /home/user/Maildir/foo/bar/new/abc and flags == MU_FLAG_REPLIED * => /home/user/Maildir/foo/bar/cur * * so only difference is whether MuFlags matches MU_FLAG_NEW is set or not * */ static gchar* get_new_path (const char *mdir, const char *mfile, MuFlags flags) { if (flags & MU_FLAG_NEW) return g_strdup_printf ("%s%cnew%c%s", mdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR, mfile); else { const char *flagstr; flagstr = mu_flags_to_str_s (flags, MU_FLAG_TYPE_MAILFILE); return g_strdup_printf ("%s%ccur%c%s:2,%s", mdir, G_DIR_SEPARATOR, G_DIR_SEPARATOR, mfile, flagstr); } } char* mu_maildir_get_new_path (const char *oldpath, const char *new_mdir, MuFlags newflags) { char *mfile, *mdir, *newpath, *cur; g_return_val_if_fail (oldpath, NULL); mfile = newpath = NULL; /* determine the maildir */ mdir = g_path_get_dirname (oldpath); if (!g_str_has_suffix (mdir, "cur") && !g_str_has_suffix (mdir, "new")) { g_warning ("%s: not a valid maildir path: %s", __FUNCTION__, oldpath); goto leave; } /* remove the 'cur' or 'new' */ mdir[strlen(mdir) - 4] = '\0'; /* determine the name of the mailfile, stripped of its flags */ mfile = g_path_get_basename (oldpath); for (cur = &mfile[strlen(mfile)-1]; cur > mfile; --cur) { if ((*cur == ':' || *cur == '!') && (cur[1] == '2' && cur[2] == ',')) { cur[0] = '\0'; /* strip the flags */ break; } } newpath = get_new_path (new_mdir ? new_mdir : mdir, mfile, newflags); leave: g_free (mfile); g_free (mdir); return newpath; } static gboolean msg_move_check_pre (const gchar *src, const gchar *dst, GError **err) { if (!g_path_is_absolute(src)) { g_set_error (err, 0, MU_ERROR_FILE, "source is not an absolute path: '%s'", src); return FALSE; } if (!g_path_is_absolute(dst)) { g_set_error (err, 0, MU_ERROR_FILE, "target is not an absolute path: '%s'", dst); return FALSE; } if (access (src, R_OK) != 0) { g_set_error (err, 0, MU_ERROR_FILE, "cannot read %s", src); return FALSE; } if (access (dst, F_OK) == 0) { g_set_error (err, 0, MU_ERROR_FILE, "%s already exists", dst); return FALSE; } return TRUE; } static gboolean msg_move_check_post (const char *src, const char *dst, GError **err) { /* double check -- is the target really there? */ if (access (dst, F_OK) != 0) { g_set_error (err, 0, MU_ERROR_FILE, "can't find target (%s)", dst); return FALSE; } if (access (src, F_OK) == 0) { g_set_error (err, 0, MU_ERROR_FILE, "source is still there (%s)", src); return FALSE; } return TRUE; } static gboolean msg_move (const char* src, const char *dst, GError **err) { if (!msg_move_check_pre (src, dst, err)) return FALSE; if (rename (src, dst) != 0) { g_set_error (err, 0, MU_ERROR_FILE, "error moving %s to %s", src, dst); return FALSE; } if (!msg_move_check_post (src, dst, err)) return FALSE; return TRUE; } gchar* mu_maildir_move_message (const char* oldpath, const char* targetmdir, MuFlags newflags, gboolean ignore_dups, GError **err) { char *newfullpath; gboolean rv; gboolean src_is_target; g_return_val_if_fail (oldpath, FALSE); newfullpath = mu_maildir_get_new_path (oldpath, targetmdir, newflags); if (!newfullpath) { g_set_error (err, 0, MU_ERROR_FILE, "failed to determine target full path"); return FALSE; } src_is_target = (g_strcmp0 (oldpath, newfullpath) == 0); if (!ignore_dups && src_is_target) { g_set_error (err, 0, MU_ERROR_FILE_TARGET_EQUALS_SOURCE, "target equals source"); return FALSE; } if (!src_is_target) { rv = msg_move (oldpath, newfullpath, err); if (!rv) { g_free (newfullpath); return NULL; } } return newfullpath; }