summaryrefslogtreecommitdiffstats
path: root/src/lib-storage/index/maildir/maildir-keywords.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib-storage/index/maildir/maildir-keywords.c')
-rw-r--r--src/lib-storage/index/maildir/maildir-keywords.c499
1 files changed, 499 insertions, 0 deletions
diff --git a/src/lib-storage/index/maildir/maildir-keywords.c b/src/lib-storage/index/maildir/maildir-keywords.c
new file mode 100644
index 0000000..a25d112
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-keywords.c
@@ -0,0 +1,499 @@
+/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */
+
+/* note that everything here depends on uidlist file being locked the whole
+ time. that's why we don't have any locking of our own, or that we do things
+ that would be racy otherwise. */
+
+#include "lib.h"
+#include "array.h"
+#include "ioloop.h"
+#include "hash.h"
+#include "str.h"
+#include "istream.h"
+#include "eacces-error.h"
+#include "file-dotlock.h"
+#include "write-full.h"
+#include "nfs-workarounds.h"
+#include "maildir-storage.h"
+#include "maildir-uidlist.h"
+#include "maildir-keywords.h"
+
+#include <sys/stat.h>
+#include <utime.h>
+
+/* how many seconds to wait before overriding dovecot-keywords.lock */
+#define KEYWORDS_LOCK_STALE_TIMEOUT (60*2)
+
+struct maildir_keywords {
+ struct maildir_mailbox *mbox;
+ struct mail_storage *storage;
+ char *path;
+
+ pool_t pool;
+ ARRAY_TYPE(keywords) list;
+ HASH_TABLE(char *, void *) hash; /* name -> idx+1 */
+
+ struct dotlock_settings dotlock_settings;
+
+ time_t synced_mtime;
+ bool synced:1;
+ bool changed:1;
+};
+
+struct maildir_keywords_sync_ctx {
+ struct maildir_keywords *mk;
+ struct mail_index *index;
+
+ const ARRAY_TYPE(keywords) *keywords;
+ ARRAY(char) idx_to_chr;
+ unsigned int chridx_to_idx[MAILDIR_MAX_KEYWORDS];
+ bool readonly;
+};
+
+struct maildir_keywords *maildir_keywords_init(struct maildir_mailbox *mbox)
+{
+ struct maildir_keywords *mk;
+
+ mk = maildir_keywords_init_readonly(&mbox->box);
+ mk->mbox = mbox;
+ return mk;
+}
+
+struct maildir_keywords *
+maildir_keywords_init_readonly(struct mailbox *box)
+{
+ struct maildir_keywords *mk;
+ const char *dir;
+
+ if (mailbox_get_path_to(box, MAILBOX_LIST_PATH_TYPE_CONTROL, &dir) <= 0)
+ i_unreached();
+
+ mk = i_new(struct maildir_keywords, 1);
+ mk->storage = box->storage;
+ mk->path = i_strconcat(dir, "/" MAILDIR_KEYWORDS_NAME, NULL);
+ mk->pool = pool_alloconly_create("maildir keywords", 512);
+ i_array_init(&mk->list, MAILDIR_MAX_KEYWORDS);
+ hash_table_create(&mk->hash, mk->pool, 0, strcase_hash, strcasecmp);
+
+ mk->dotlock_settings.use_excl_lock =
+ box->storage->set->dotlock_use_excl;
+ mk->dotlock_settings.nfs_flush =
+ box->storage->set->mail_nfs_storage;
+ mk->dotlock_settings.timeout =
+ mail_storage_get_lock_timeout(box->storage,
+ KEYWORDS_LOCK_STALE_TIMEOUT + 2);
+ mk->dotlock_settings.stale_timeout = KEYWORDS_LOCK_STALE_TIMEOUT;
+ mk->dotlock_settings.temp_prefix =
+ mailbox_list_get_temp_prefix(box->list);
+ return mk;
+}
+
+void maildir_keywords_deinit(struct maildir_keywords **_mk)
+{
+ struct maildir_keywords *mk = *_mk;
+
+ *_mk = NULL;
+ hash_table_destroy(&mk->hash);
+ array_free(&mk->list);
+ pool_unref(&mk->pool);
+ i_free(mk->path);
+ i_free(mk);
+}
+
+static void maildir_keywords_clear(struct maildir_keywords *mk)
+{
+ array_clear(&mk->list);
+ hash_table_clear(mk->hash, TRUE);
+ p_clear(mk->pool);
+}
+
+static int maildir_keywords_sync(struct maildir_keywords *mk)
+{
+ struct istream *input;
+ struct stat st;
+ char *line, *p, *new_name;
+ const char **strp;
+ unsigned int idx;
+ int fd;
+
+ /* Remember that we rely on uidlist file locking in here. That's why
+ we rely on stat()'s timestamp and don't bother handling ESTALE
+ errors. */
+
+ if (mk->storage->set->mail_nfs_storage) {
+ /* file is updated only by replacing it, no need to flush
+ attribute cache */
+ nfs_flush_file_handle_cache(mk->path);
+ }
+
+ if (nfs_safe_stat(mk->path, &st) < 0) {
+ if (errno == ENOENT) {
+ maildir_keywords_clear(mk);
+ mk->synced = TRUE;
+ return 0;
+ }
+ mailbox_set_critical(&mk->mbox->box,
+ "stat(%s) failed: %m", mk->path);
+ return -1;
+ }
+
+ if (st.st_mtime == mk->synced_mtime) {
+ /* hasn't changed */
+ mk->synced = TRUE;
+ return 0;
+ }
+ mk->synced_mtime = st.st_mtime;
+
+ fd = open(mk->path, O_RDONLY);
+ if (fd == -1) {
+ if (errno == ENOENT) {
+ maildir_keywords_clear(mk);
+ mk->synced = TRUE;
+ return 0;
+ }
+ mailbox_set_critical(&mk->mbox->box,
+ "open(%s) failed: %m", mk->path);
+ return -1;
+ }
+
+ maildir_keywords_clear(mk);
+ input = i_stream_create_fd(fd, 1024);
+ while ((line = i_stream_read_next_line(input)) != NULL) {
+ p = strchr(line, ' ');
+ if (p == NULL) {
+ /* note that when converting .customflags file this
+ case happens in the first line. */
+ continue;
+ }
+ *p++ = '\0';
+
+ if (str_to_uint(line, &idx) < 0 ||
+ idx >= MAILDIR_MAX_KEYWORDS || *p == '\0' ||
+ hash_table_lookup(mk->hash, p) != NULL) {
+ /* shouldn't happen */
+ continue;
+ }
+
+ /* save it */
+ new_name = p_strdup(mk->pool, p);
+ hash_table_insert(mk->hash, new_name, POINTER_CAST(idx + 1));
+
+ strp = array_idx_get_space(&mk->list, idx);
+ *strp = new_name;
+ }
+ i_stream_destroy(&input);
+
+ if (close(fd) < 0) {
+ mailbox_set_critical(&mk->mbox->box,
+ "close(%s) failed: %m", mk->path);
+ return -1;
+ }
+
+ mk->synced = TRUE;
+ return 0;
+}
+
+static int
+maildir_keywords_lookup(struct maildir_keywords *mk, const char *name,
+ unsigned int *chridx_r)
+{
+ void *value;
+
+ value = hash_table_lookup(mk->hash, name);
+ if (value == NULL) {
+ if (mk->synced)
+ return 0;
+
+ if (maildir_keywords_sync(mk) < 0)
+ return -1;
+ i_assert(mk->synced);
+
+ value = hash_table_lookup(mk->hash, name);
+ if (value == NULL)
+ return 0;
+ }
+
+ *chridx_r = POINTER_CAST_TO(value, unsigned int)-1;
+ return 1;
+}
+
+static void
+maildir_keywords_create(struct maildir_keywords *mk, const char *name,
+ unsigned int chridx)
+{
+ const char **strp;
+ char *new_name;
+
+ i_assert(chridx < MAILDIR_MAX_KEYWORDS);
+
+ new_name = p_strdup(mk->pool, name);
+ hash_table_insert(mk->hash, new_name, POINTER_CAST(chridx + 1));
+
+ strp = array_idx_get_space(&mk->list, chridx);
+ *strp = new_name;
+
+ mk->changed = TRUE;
+}
+
+static int
+maildir_keywords_lookup_or_create(struct maildir_keywords *mk, const char *name,
+ unsigned int *chridx_r)
+{
+ const char *const *keywords;
+ unsigned int i, count;
+ int ret;
+
+ if ((ret = maildir_keywords_lookup(mk, name, chridx_r)) != 0)
+ return ret;
+
+ /* see if we are full */
+ keywords = array_get(&mk->list, &count);
+ for (i = 0; i < count; i++) {
+ if (keywords[i] == NULL)
+ break;
+ }
+
+ if (i == count && count >= MAILDIR_MAX_KEYWORDS)
+ return -1;
+
+ if (!maildir_uidlist_is_locked(mk->mbox->uidlist))
+ return -1;
+
+ maildir_keywords_create(mk, name, i);
+ *chridx_r = i;
+ return 1;
+}
+
+static const char *
+maildir_keywords_idx(struct maildir_keywords *mk, unsigned int idx)
+{
+ const char *const *keywords;
+ unsigned int count;
+
+ keywords = array_get(&mk->list, &count);
+ if (idx >= count) {
+ if (mk->synced)
+ return NULL;
+
+ if (maildir_keywords_sync(mk) < 0)
+ return NULL;
+ i_assert(mk->synced);
+
+ keywords = array_get(&mk->list, &count);
+ }
+ return idx >= count ? NULL : keywords[idx];
+}
+
+static int maildir_keywords_write_fd(struct maildir_keywords *mk,
+ const char *path, int fd)
+{
+ struct maildir_mailbox *mbox = mk->mbox;
+ struct mailbox *box = &mbox->box;
+ const struct mailbox_permissions *perm = mailbox_get_permissions(box);
+ const char *const *keywords;
+ unsigned int i, count;
+ string_t *str;
+ struct stat st;
+
+ str = t_str_new(256);
+ keywords = array_get(&mk->list, &count);
+ for (i = 0; i < count; i++) {
+ if (keywords[i] != NULL)
+ str_printfa(str, "%u %s\n", i, keywords[i]);
+ }
+ if (write_full(fd, str_data(str), str_len(str)) < 0) {
+ mailbox_set_critical(&mk->mbox->box,
+ "write_full(%s) failed: %m", path);
+ return -1;
+ }
+
+ if (fstat(fd, &st) < 0) {
+ mailbox_set_critical(&mk->mbox->box,
+ "fstat(%s) failed: %m", path);
+ return -1;
+ }
+
+ if (st.st_gid != perm->file_create_gid &&
+ perm->file_create_gid != (gid_t)-1) {
+ if (fchown(fd, (uid_t)-1, perm->file_create_gid) < 0) {
+ if (errno == EPERM) {
+ mailbox_set_critical(&mk->mbox->box, "%s",
+ eperm_error_get_chgrp("fchown", path,
+ perm->file_create_gid,
+ perm->file_create_gid_origin));
+ } else {
+ mailbox_set_critical(&mk->mbox->box,
+ "fchown(%s) failed: %m", path);
+ }
+ }
+ }
+
+ /* mtime must grow every time */
+ if (st.st_mtime <= mk->synced_mtime) {
+ struct utimbuf ut;
+
+ mk->synced_mtime = ioloop_time <= mk->synced_mtime ?
+ mk->synced_mtime + 1 : ioloop_time;
+ ut.actime = ioloop_time;
+ ut.modtime = mk->synced_mtime;
+ if (utime(path, &ut) < 0) {
+ mailbox_set_critical(&mk->mbox->box,
+ "utime(%s) failed: %m", path);
+ return -1;
+ }
+ }
+
+ if (fsync(fd) < 0) {
+ mailbox_set_critical(&mk->mbox->box,
+ "fsync(%s) failed: %m", path);
+ return -1;
+ }
+ return 0;
+}
+
+static int maildir_keywords_commit(struct maildir_keywords *mk)
+{
+ const struct mailbox_permissions *perm;
+ struct dotlock *dotlock;
+ const char *lock_path;
+ mode_t old_mask;
+ int i, fd;
+
+ mk->synced = FALSE;
+
+ if (!mk->changed || mk->mbox == NULL)
+ return 0;
+
+ lock_path = t_strconcat(mk->path, ".lock", NULL);
+ i_unlink_if_exists(lock_path);
+
+ perm = mailbox_get_permissions(&mk->mbox->box);
+ for (i = 0;; i++) {
+ /* we could just create the temp file directly, but doing it
+ this ways avoids potential problems with overwriting
+ contents in malicious symlinks */
+ old_mask = umask(0777 & ~perm->file_create_mode);
+ fd = file_dotlock_open(&mk->dotlock_settings, mk->path,
+ DOTLOCK_CREATE_FLAG_NONBLOCK, &dotlock);
+ umask(old_mask);
+ if (fd != -1)
+ break;
+
+ if (errno != ENOENT || i == MAILDIR_DELETE_RETRY_COUNT) {
+ mailbox_set_critical(&mk->mbox->box,
+ "file_dotlock_open(%s) failed: %m", mk->path);
+ return -1;
+ }
+ /* the control dir doesn't exist. create it unless the whole
+ mailbox was just deleted. */
+ if (!maildir_set_deleted(&mk->mbox->box))
+ return -1;
+ }
+
+ if (maildir_keywords_write_fd(mk, lock_path, fd) < 0) {
+ file_dotlock_delete(&dotlock);
+ return -1;
+ }
+
+ if (file_dotlock_replace(&dotlock, 0) < 0) {
+ mailbox_set_critical(&mk->mbox->box,
+ "file_dotlock_replace(%s) failed: %m", mk->path);
+ return -1;
+ }
+
+ mk->changed = FALSE;
+ return 0;
+}
+
+struct maildir_keywords_sync_ctx *
+maildir_keywords_sync_init(struct maildir_keywords *mk,
+ struct mail_index *index)
+{
+ struct maildir_keywords_sync_ctx *ctx;
+
+ ctx = i_new(struct maildir_keywords_sync_ctx, 1);
+ ctx->mk = mk;
+ ctx->index = index;
+ ctx->keywords = mail_index_get_keywords(index);
+ i_array_init(&ctx->idx_to_chr, MAILDIR_MAX_KEYWORDS);
+ return ctx;
+}
+
+struct maildir_keywords_sync_ctx *
+maildir_keywords_sync_init_readonly(struct maildir_keywords *mk,
+ struct mail_index *index)
+{
+ struct maildir_keywords_sync_ctx *ctx;
+
+ ctx = maildir_keywords_sync_init(mk, index);
+ ctx->readonly = TRUE;
+ return ctx;
+}
+
+void maildir_keywords_sync_deinit(struct maildir_keywords_sync_ctx **_ctx)
+{
+ struct maildir_keywords_sync_ctx *ctx = *_ctx;
+
+ *_ctx = NULL;
+
+ T_BEGIN {
+ (void)maildir_keywords_commit(ctx->mk);
+ } T_END;
+
+ array_free(&ctx->idx_to_chr);
+ i_free(ctx);
+}
+
+unsigned int maildir_keywords_char_idx(struct maildir_keywords_sync_ctx *ctx,
+ char keyword)
+{
+ const char *name;
+ unsigned int chridx, idx;
+
+ i_assert(keyword >= MAILDIR_KEYWORD_FIRST &&
+ keyword <= MAILDIR_KEYWORD_LAST);
+ chridx = keyword - MAILDIR_KEYWORD_FIRST;
+
+ if (ctx->chridx_to_idx[chridx] != 0)
+ return ctx->chridx_to_idx[chridx];
+
+ /* lookup / create */
+ name = maildir_keywords_idx(ctx->mk, chridx);
+ if (name == NULL) {
+ /* name is lost. just generate one ourself. */
+ name = t_strdup_printf("unknown-%u", chridx);
+ while (maildir_keywords_lookup(ctx->mk, name, &idx) > 0) {
+ /* don't create a duplicate name.
+ keep changing the name until it doesn't exist */
+ name = t_strconcat(name, "?", NULL);
+ }
+ maildir_keywords_create(ctx->mk, name, chridx);
+ }
+
+ mail_index_keyword_lookup_or_create(ctx->index, name, &idx);
+ ctx->chridx_to_idx[chridx] = idx;
+ return idx;
+}
+
+char maildir_keywords_idx_char(struct maildir_keywords_sync_ctx *ctx,
+ unsigned int idx)
+{
+ const char *name;
+ char *chr_p;
+ unsigned int chridx;
+ int ret;
+
+ chr_p = array_idx_get_space(&ctx->idx_to_chr, idx);
+ if (*chr_p != '\0')
+ return *chr_p;
+
+ name = array_idx_elem(ctx->keywords, idx);
+ ret = !ctx->readonly ?
+ maildir_keywords_lookup_or_create(ctx->mk, name, &chridx) :
+ maildir_keywords_lookup(ctx->mk, name, &chridx);
+ if (ret <= 0)
+ return '\0';
+
+ *chr_p = chridx + MAILDIR_KEYWORD_FIRST;
+ return *chr_p;
+}