summaryrefslogtreecommitdiffstats
path: root/src/lib-storage/index/maildir/maildir-save.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 09:51:24 +0000
commitf7548d6d28c313cf80e6f3ef89aed16a19815df1 (patch)
treea3f6f2a3f247293bee59ecd28e8cd8ceb6ca064a /src/lib-storage/index/maildir/maildir-save.c
parentInitial commit. (diff)
downloaddovecot-f7548d6d28c313cf80e6f3ef89aed16a19815df1.tar.xz
dovecot-f7548d6d28c313cf80e6f3ef89aed16a19815df1.zip
Adding upstream version 1:2.3.19.1+dfsg1.upstream/1%2.3.19.1+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/lib-storage/index/maildir/maildir-save.c')
-rw-r--r--src/lib-storage/index/maildir/maildir-save.c1084
1 files changed, 1084 insertions, 0 deletions
diff --git a/src/lib-storage/index/maildir/maildir-save.c b/src/lib-storage/index/maildir/maildir-save.c
new file mode 100644
index 0000000..5cf7d6a
--- /dev/null
+++ b/src/lib-storage/index/maildir/maildir-save.c
@@ -0,0 +1,1084 @@
+/* Copyright (c) 2002-2018 Dovecot authors, see the included COPYING file */
+
+#include "lib.h"
+#include "ioloop.h"
+#include "array.h"
+#include "buffer.h"
+#include "istream.h"
+#include "istream-crlf.h"
+#include "ostream.h"
+#include "fdatasync-path.h"
+#include "eacces-error.h"
+#include "str.h"
+#include "index-mail.h"
+#include "maildir-storage.h"
+#include "maildir-uidlist.h"
+#include "maildir-keywords.h"
+#include "maildir-filename.h"
+#include "maildir-filename-flags.h"
+#include "maildir-sync.h"
+#include "mailbox-recent-flags.h"
+
+#include <stdio.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <utime.h>
+#include <sys/stat.h>
+
+#define MAILDIR_FILENAME_FLAG_MOVED 0x10000000
+
+struct maildir_filename {
+ struct maildir_filename *next;
+ const char *tmp_name, *dest_basename;
+ const char *pop3_uidl, *guid;
+
+ uoff_t size, vsize;
+ enum mail_flags flags;
+ unsigned int pop3_order;
+ bool preserve_filename:1;
+ ARRAY_TYPE(keyword_indexes) keywords;
+};
+
+struct maildir_save_context {
+ struct mail_save_context ctx;
+ pool_t pool;
+
+ struct maildir_mailbox *mbox;
+ struct mail_index_transaction *trans;
+ struct maildir_uidlist_sync_ctx *uidlist_sync_ctx;
+ struct maildir_keywords_sync_ctx *keywords_sync_ctx;
+ struct maildir_index_sync_context *sync_ctx;
+ struct mail *cur_dest_mail;
+
+ const char *tmpdir, *newdir, *curdir;
+ struct maildir_filename *files, **files_tail, *file_last;
+ unsigned int files_count;
+
+ struct istream *input;
+ int fd;
+ uint32_t first_seq, seq, last_nonrecent_uid;
+
+ bool have_keywords:1;
+ bool have_preserved_filenames:1;
+ bool locked:1;
+ bool failed:1;
+ bool last_save_finished:1;
+ bool locked_uidlist_refresh:1;
+};
+
+#define MAILDIR_SAVECTX(s) container_of(s, struct maildir_save_context, ctx)
+
+static int maildir_file_move(struct maildir_save_context *ctx,
+ struct maildir_filename *mf, const char *destname,
+ bool newdir)
+{
+ struct mail_storage *storage = &ctx->mbox->storage->storage;
+ const char *tmp_path, *new_path;
+
+ i_assert(*destname != '\0');
+ i_assert(*mf->tmp_name != '\0');
+
+ /* if we have flags, we'll move it to cur/ directly, because files in
+ new/ directory can't have flags. alternative would be to write it
+ in new/ and set the flags dirty in index file, but in that case
+ external MUAs would see wrong flags. */
+ tmp_path = t_strconcat(ctx->tmpdir, "/", mf->tmp_name, NULL);
+ new_path = newdir ?
+ t_strconcat(ctx->newdir, "/", destname, NULL) :
+ t_strconcat(ctx->curdir, "/", destname, NULL);
+
+ /* maildir spec says we should use link() + unlink() here. however
+ since our filename is guaranteed to be unique, rename() works just
+ as well, except faster. even if the filename wasn't unique, the
+ problem could still happen if the file was already moved from
+ new/ to cur/, so link() doesn't really provide any safety anyway.
+
+ Besides the small temporary performance benefits, this rename() is
+ almost required with OSX's HFS+ filesystem, since it implements
+ hard links in a pretty ugly way, which makes the performance crawl
+ when a lot of hard links are used. */
+ if (rename(tmp_path, new_path) == 0) {
+ mf->flags |= MAILDIR_FILENAME_FLAG_MOVED;
+ return 0;
+ } else if (ENOQUOTA(errno)) {
+ mail_storage_set_error(storage, MAIL_ERROR_NOQUOTA,
+ MAIL_ERRSTR_NO_QUOTA);
+ return -1;
+ } else {
+ mailbox_set_critical(&ctx->mbox->box,
+ "rename(%s, %s) failed: %m",
+ tmp_path, new_path);
+ return -1;
+ }
+}
+
+static struct mail_save_context *
+maildir_save_transaction_init(struct mailbox_transaction_context *t)
+{
+ struct maildir_mailbox *mbox = MAILDIR_MAILBOX(t->box);
+ struct maildir_save_context *ctx;
+ const char *path;
+ pool_t pool;
+
+ pool = pool_alloconly_create("maildir_save_context", 4096);
+ ctx = p_new(pool, struct maildir_save_context, 1);
+ ctx->ctx.transaction = t;
+ ctx->pool = pool;
+ ctx->mbox = mbox;
+ ctx->trans = t->itrans;
+ ctx->files_tail = &ctx->files;
+ ctx->fd = -1;
+
+ path = mailbox_get_path(&mbox->box);
+ ctx->tmpdir = p_strconcat(pool, path, "/tmp", NULL);
+ ctx->newdir = p_strconcat(pool, path, "/new", NULL);
+ ctx->curdir = p_strconcat(pool, path, "/cur", NULL);
+
+ ctx->last_save_finished = TRUE;
+ return &ctx->ctx;
+}
+
+struct maildir_filename *
+maildir_save_add(struct mail_save_context *_ctx, const char *tmp_fname,
+ struct mail *src_mail)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+ struct mail_save_data *mdata = &_ctx->data;
+ struct maildir_filename *mf;
+ struct istream *input;
+
+ i_assert(*tmp_fname != '\0');
+
+ /* allow caller to specify recent flag only when uid is specified
+ (we're replicating, converting, etc.). */
+ if (mdata->uid == 0)
+ mdata->flags |= MAIL_RECENT;
+ else if ((mdata->flags & MAIL_RECENT) == 0 &&
+ ctx->last_nonrecent_uid < mdata->uid)
+ ctx->last_nonrecent_uid = mdata->uid;
+
+ /* now, we want to be able to rollback the whole append session,
+ so we'll just store the name of this temp file and move it later
+ into new/ or cur/. */
+ mf = p_new(ctx->pool, struct maildir_filename, 1);
+ mf->tmp_name = mf->dest_basename = p_strdup(ctx->pool, tmp_fname);
+ mf->flags = mdata->flags;
+ mf->size = UOFF_T_MAX;
+ mf->vsize = UOFF_T_MAX;
+
+ ctx->file_last = mf;
+ i_assert(*ctx->files_tail == NULL);
+ *ctx->files_tail = mf;
+ ctx->files_tail = &mf->next;
+ ctx->files_count++;
+
+ if (mdata->pop3_uidl != NULL)
+ mf->pop3_uidl = p_strdup(ctx->pool, mdata->pop3_uidl);
+ mf->pop3_order = mdata->pop3_order;
+
+ /* insert into index */
+ mail_index_append(ctx->trans, mdata->uid, &ctx->seq);
+ mail_index_update_flags(ctx->trans, ctx->seq,
+ MODIFY_REPLACE,
+ mdata->flags & ENUM_NEGATE(MAIL_RECENT));
+ if (mdata->keywords != NULL) {
+ mail_index_update_keywords(ctx->trans, ctx->seq,
+ MODIFY_REPLACE, mdata->keywords);
+ }
+ if (mdata->min_modseq != 0) {
+ mail_index_update_modseq(ctx->trans, ctx->seq,
+ mdata->min_modseq);
+ }
+
+ if (ctx->first_seq == 0) {
+ ctx->first_seq = ctx->seq;
+ i_assert(ctx->files->next == NULL);
+ }
+
+ mail_set_seq_saving(_ctx->dest_mail, ctx->seq);
+
+ if (ctx->input == NULL) {
+ /* copying with hardlinking. */
+ i_assert(src_mail != NULL);
+ index_copy_cache_fields(_ctx, src_mail, ctx->seq);
+ ctx->cur_dest_mail = NULL;
+ } else {
+ input = index_mail_cache_parse_init(_ctx->dest_mail,
+ ctx->input);
+ i_stream_unref(&ctx->input);
+ ctx->input = input;
+ ctx->cur_dest_mail = _ctx->dest_mail;
+ }
+ return mf;
+}
+
+void maildir_save_set_dest_basename(struct mail_save_context *_ctx,
+ struct maildir_filename *mf,
+ const char *basename)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+
+ mf->preserve_filename = TRUE;
+ mf->dest_basename = p_strdup(ctx->pool, basename);
+ ctx->have_preserved_filenames = TRUE;
+}
+
+void maildir_save_set_sizes(struct maildir_filename *mf,
+ uoff_t size, uoff_t vsize)
+{
+ mf->size = size;
+ mf->vsize = vsize;
+}
+
+static bool
+maildir_get_dest_filename(struct maildir_save_context *ctx,
+ struct maildir_filename *mf,
+ const char **fname_r)
+{
+ const char *basename = mf->dest_basename;
+
+ if (mf->size != UOFF_T_MAX && !mf->preserve_filename) {
+ basename = t_strdup_printf("%s,%c=%"PRIuUOFF_T, basename,
+ MAILDIR_EXTRA_FILE_SIZE, mf->size);
+ }
+
+ if (mf->vsize != UOFF_T_MAX && !mf->preserve_filename) {
+ basename = t_strdup_printf("%s,%c=%"PRIuUOFF_T, basename,
+ MAILDIR_EXTRA_VIRTUAL_SIZE,
+ mf->vsize);
+ }
+
+ if (!array_is_created(&mf->keywords) || array_count(&mf->keywords) == 0) {
+ if ((mf->flags & MAIL_FLAGS_MASK) == MAIL_RECENT) {
+ *fname_r = basename;
+ return TRUE;
+ }
+
+ *fname_r = maildir_filename_flags_set(basename,
+ mf->flags & MAIL_FLAGS_MASK);
+ return FALSE;
+ }
+
+ i_assert(ctx->keywords_sync_ctx != NULL ||
+ !array_is_created(&mf->keywords) || array_count(&mf->keywords) == 0);
+ *fname_r = maildir_filename_flags_kw_set(ctx->keywords_sync_ctx,
+ basename,
+ mf->flags & MAIL_FLAGS_MASK,
+ &mf->keywords);
+ return FALSE;
+}
+
+static const char *maildir_mf_get_path(struct maildir_save_context *ctx,
+ struct maildir_filename *mf)
+{
+ const char *fname, *dir;
+
+ if ((mf->flags & MAILDIR_FILENAME_FLAG_MOVED) == 0) {
+ /* file is still in tmp/ */
+ return t_strdup_printf("%s/%s", ctx->tmpdir, mf->tmp_name);
+ }
+
+ /* already moved to new/ or cur/ */
+ dir = maildir_get_dest_filename(ctx, mf, &fname) ?
+ ctx->newdir : ctx->curdir;
+ return t_strdup_printf("%s/%s", dir, fname);
+}
+
+
+static struct maildir_filename *
+maildir_save_get_mf(struct mailbox_transaction_context *t, uint32_t seq)
+{
+ struct maildir_save_context *save_ctx = MAILDIR_SAVECTX(t->save_ctx);
+ struct maildir_filename *mf;
+
+ i_assert(seq >= save_ctx->first_seq);
+
+ seq -= save_ctx->first_seq;
+ mf = save_ctx->files;
+ while (seq > 0) {
+ mf = mf->next;
+ i_assert(mf != NULL);
+ seq--;
+ }
+ return mf;
+}
+
+int maildir_save_file_get_size(struct mailbox_transaction_context *t,
+ uint32_t seq, bool vsize, uoff_t *size_r)
+{
+ struct maildir_filename *mf = maildir_save_get_mf(t, seq);
+
+ *size_r = vsize ? mf->vsize : mf->size;
+ return *size_r == UOFF_T_MAX ? -1 : 0;
+}
+
+const char *maildir_save_file_get_path(struct mailbox_transaction_context *t,
+ uint32_t seq)
+{
+ struct maildir_save_context *save_ctx = MAILDIR_SAVECTX(t->save_ctx);
+ struct maildir_filename *mf = maildir_save_get_mf(t, seq);
+
+ return maildir_mf_get_path(save_ctx, mf);
+}
+
+static int maildir_create_tmp(struct maildir_mailbox *mbox, const char *dir,
+ const char **fname_r)
+{
+ struct mailbox *box = &mbox->box;
+ const struct mailbox_permissions *perm = mailbox_get_permissions(box);
+ size_t prefix_len;
+ const char *tmp_fname;
+ string_t *path;
+ mode_t old_mask;
+ int fd;
+
+ path = t_str_new(256);
+ str_append(path, dir);
+ str_append_c(path, '/');
+ prefix_len = str_len(path);
+
+ do {
+ tmp_fname = maildir_filename_generate();
+ str_truncate(path, prefix_len);
+ str_append(path, tmp_fname);
+
+ /* the generated filename is unique. the only reason why it
+ might return an existing filename is if the time moved
+ backwards. so we'll use O_EXCL anyway, although it's mostly
+ useless. */
+ old_mask = umask(0777 & ~perm->file_create_mode);
+ fd = open(str_c(path),
+ O_WRONLY | O_CREAT | O_TRUNC | O_EXCL, 0777);
+ umask(old_mask);
+ } while (fd == -1 && errno == EEXIST);
+
+ *fname_r = tmp_fname;
+ if (fd == -1) {
+ if (ENOQUOTA(errno)) {
+ mail_storage_set_error(box->storage,
+ MAIL_ERROR_NOQUOTA, MAIL_ERRSTR_NO_QUOTA);
+ } else {
+ mailbox_set_critical(box,
+ "open(%s) failed: %m", str_c(path));
+ }
+ } else if (perm->file_create_gid != (gid_t)-1) {
+ if (fchown(fd, (uid_t)-1, perm->file_create_gid) < 0) {
+ if (errno == EPERM) {
+ mailbox_set_critical(box, "%s",
+ eperm_error_get_chgrp("fchown",
+ str_c(path),
+ perm->file_create_gid,
+ perm->file_create_gid_origin));
+ } else {
+ mailbox_set_critical(box,
+ "fchown(%s) failed: %m", str_c(path));
+ }
+ }
+ }
+
+ return fd;
+}
+
+struct mail_save_context *
+maildir_save_alloc(struct mailbox_transaction_context *t)
+{
+ i_assert((t->flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0);
+
+ if (t->save_ctx == NULL)
+ t->save_ctx = maildir_save_transaction_init(t);
+ return t->save_ctx;
+}
+
+int maildir_save_begin(struct mail_save_context *_ctx, struct istream *input)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+ struct maildir_filename *mf;
+
+ /* new mail, new failure state */
+ ctx->failed = FALSE;
+
+ T_BEGIN {
+ /* create a new file in tmp/ directory */
+ const char *fname;
+
+ ctx->fd = maildir_create_tmp(ctx->mbox, ctx->tmpdir, &fname);
+ if (ctx->fd == -1)
+ ctx->failed = TRUE;
+ else {
+ if (ctx->mbox->storage->storage.set->mail_save_crlf)
+ ctx->input = i_stream_create_crlf(input);
+ else
+ ctx->input = i_stream_create_lf(input);
+ mf = maildir_save_add(_ctx, fname, NULL);
+ if (_ctx->data.guid != NULL) {
+ maildir_save_set_dest_basename(_ctx, mf,
+ _ctx->data.guid);
+ }
+ }
+ } T_END;
+
+ if (!ctx->failed) {
+ _ctx->data.output = o_stream_create_fd_file(ctx->fd, 0, FALSE);
+ o_stream_set_name(_ctx->data.output, t_strdup_printf(
+ "%s/%s", ctx->tmpdir, ctx->file_last->tmp_name));
+ o_stream_cork(_ctx->data.output);
+ ctx->last_save_finished = FALSE;
+ }
+ return ctx->failed ? -1 : 0;
+}
+
+int maildir_save_continue(struct mail_save_context *_ctx)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+
+ if (ctx->failed)
+ return -1;
+
+ if (index_storage_save_continue(_ctx, ctx->input,
+ ctx->cur_dest_mail) < 0) {
+ ctx->failed = TRUE;
+ return -1;
+ }
+ return 0;
+}
+
+static int maildir_save_finish_received_date(struct maildir_save_context *ctx,
+ const char *path)
+{
+ struct utimbuf buf;
+ struct stat st;
+
+ if (ctx->ctx.data.received_date != (time_t)-1) {
+ /* set the received_date by modifying mtime */
+ buf.actime = ioloop_time;
+ buf.modtime = ctx->ctx.data.received_date;
+
+ if (utime(path, &buf) < 0) {
+ mail_set_critical(ctx->ctx.dest_mail,
+ "utime(%s) failed: %m", path);
+ return -1;
+ }
+ } else if (ctx->fd != -1) {
+ if (fstat(ctx->fd, &st) == 0)
+ ctx->ctx.data.received_date = st.st_mtime;
+ else {
+ mail_set_critical(ctx->ctx.dest_mail,
+ "fstat(%s) failed: %m", path);
+ return -1;
+ }
+ } else {
+ /* hardlinked */
+ if (stat(path, &st) == 0)
+ ctx->ctx.data.received_date = st.st_mtime;
+ else {
+ mail_set_critical(ctx->ctx.dest_mail,
+ "stat(%s) failed: %m", path);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static void maildir_save_remove_last_filename(struct maildir_save_context *ctx)
+{
+ struct maildir_filename **fm;
+
+ index_storage_save_abort_last(&ctx->ctx, ctx->seq);
+ ctx->seq--;
+
+ for (fm = &ctx->files; (*fm)->next != NULL; fm = &(*fm)->next) ;
+ i_assert(*fm == ctx->file_last);
+ *fm = NULL;
+
+ ctx->files_tail = fm;
+ ctx->file_last = NULL;
+ ctx->files_count--;
+}
+
+void maildir_save_finish_keywords(struct mail_save_context *_ctx)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+
+ ARRAY_TYPE(keyword_indexes) keyword_idx;
+ t_array_init(&keyword_idx, 8);
+ mail_index_lookup_keywords(ctx->ctx.transaction->view, ctx->seq,
+ &keyword_idx);
+
+ if (array_count(&keyword_idx) > 0) {
+ /* copy keywords */
+ p_array_init(&ctx->file_last->keywords, ctx->pool,
+ array_count(&keyword_idx));
+ array_copy(&ctx->file_last->keywords.arr, 0, &keyword_idx.arr, 0,
+ array_count(&keyword_idx));
+ ctx->have_keywords = TRUE;
+ }
+}
+
+static int maildir_save_finish_real(struct mail_save_context *_ctx)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+ struct mail_storage *storage = &ctx->mbox->storage->storage;
+ const char *path, *output_errstr;
+ off_t real_size;
+ uoff_t size;
+ int output_errno;
+
+ ctx->last_save_finished = TRUE;
+ if (ctx->failed && ctx->fd == -1) {
+ /* tmp file creation failed */
+ return -1;
+ }
+
+ path = t_strconcat(ctx->tmpdir, "/", ctx->file_last->tmp_name, NULL);
+ if (o_stream_finish(_ctx->data.output) < 0) {
+ if (!mail_storage_set_error_from_errno(storage)) {
+ mail_set_critical(_ctx->dest_mail,
+ "write(%s) failed: %s", path,
+ o_stream_get_error(_ctx->data.output));
+ }
+ ctx->failed = TRUE;
+ }
+
+ if (_ctx->data.save_date != (time_t)-1) {
+ /* we can't change ctime, but we can add the date to cache */
+ struct index_mail *mail = INDEX_MAIL(_ctx->dest_mail);
+ uint32_t t = _ctx->data.save_date;
+
+ index_mail_cache_add(mail, MAIL_CACHE_SAVE_DATE, &t, sizeof(t));
+ }
+
+ if (maildir_save_finish_received_date(ctx, path) < 0)
+ ctx->failed = TRUE;
+
+ if (ctx->cur_dest_mail != NULL) {
+ index_mail_cache_parse_deinit(ctx->cur_dest_mail,
+ ctx->ctx.data.received_date,
+ !ctx->failed);
+ }
+ i_stream_unref(&ctx->input);
+
+ /* remember the size in case we want to add it to filename */
+ ctx->file_last->size = _ctx->data.output->offset;
+ if (ctx->cur_dest_mail == NULL ||
+ mail_get_virtual_size(ctx->cur_dest_mail,
+ &ctx->file_last->vsize) < 0)
+ ctx->file_last->vsize = UOFF_T_MAX;
+
+ output_errno = _ctx->data.output->stream_errno;
+ output_errstr = t_strdup(o_stream_get_error(_ctx->data.output));
+ o_stream_destroy(&_ctx->data.output);
+
+ maildir_save_finish_keywords(_ctx);
+
+ if (storage->set->parsed_fsync_mode != FSYNC_MODE_NEVER &&
+ !ctx->failed) {
+ if (fsync(ctx->fd) < 0) {
+ if (!mail_storage_set_error_from_errno(storage)) {
+ mail_set_critical(_ctx->dest_mail,
+ "fsync(%s) failed: %m", path);
+ }
+ ctx->failed = TRUE;
+ }
+ }
+ real_size = lseek(ctx->fd, 0, SEEK_END);
+ if (real_size == (off_t)-1) {
+ mail_set_critical(_ctx->dest_mail, "lseek(%s) failed: %m", path);
+ } else if (real_size != (off_t)ctx->file_last->size &&
+ (!maildir_filename_get_size(ctx->file_last->dest_basename,
+ MAILDIR_EXTRA_FILE_SIZE, &size) ||
+ size != ctx->file_last->size)) {
+ /* e.g. zlib plugin was used. the "physical size" must be in
+ the maildir filename, since stat() will return wrong size */
+ ctx->file_last->preserve_filename = FALSE;
+ /* preserve the GUID if needed */
+ if (ctx->file_last->guid == NULL)
+ ctx->file_last->guid = ctx->file_last->dest_basename;
+ /* reset the base name as well, just in case there's a
+ ,W=vsize */
+ ctx->file_last->dest_basename = ctx->file_last->tmp_name;
+ }
+ if (close(ctx->fd) < 0) {
+ if (!mail_storage_set_error_from_errno(storage)) {
+ mail_set_critical(_ctx->dest_mail,
+ "close(%s) failed: %m", path);
+ }
+ ctx->failed = TRUE;
+ }
+ ctx->fd = -1;
+
+ if (ctx->failed) {
+ /* delete the tmp file */
+ i_unlink_if_exists(path);
+
+ if (ENOQUOTA(output_errno)) {
+ mail_storage_set_error(storage,
+ MAIL_ERROR_NOQUOTA, MAIL_ERRSTR_NO_QUOTA);
+ } else if (output_errno != 0) {
+ mail_set_critical(_ctx->dest_mail,
+ "write(%s) failed: %s", path, output_errstr);
+ }
+
+ maildir_save_remove_last_filename(ctx);
+ return -1;
+ }
+
+ ctx->file_last = NULL;
+ return 0;
+}
+
+int maildir_save_finish(struct mail_save_context *ctx)
+{
+ int ret;
+
+ T_BEGIN {
+ ret = maildir_save_finish_real(ctx);
+ } T_END;
+ index_save_context_free(ctx);
+ return ret;
+}
+
+void maildir_save_cancel(struct mail_save_context *_ctx)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+
+ ctx->failed = TRUE;
+ (void)maildir_save_finish(_ctx);
+}
+
+static void
+maildir_save_unlink_files(struct maildir_save_context *ctx)
+{
+ struct maildir_filename *mf;
+
+ for (mf = ctx->files; mf != NULL; mf = mf->next) T_BEGIN {
+ i_unlink(maildir_mf_get_path(ctx, mf));
+ } T_END;
+ ctx->files = NULL;
+}
+
+static int maildir_transaction_fsync_dirs(struct maildir_save_context *ctx,
+ bool new_changed, bool cur_changed)
+{
+ struct mail_storage *storage = &ctx->mbox->storage->storage;
+
+ if (storage->set->parsed_fsync_mode == FSYNC_MODE_NEVER)
+ return 0;
+
+ if (new_changed) {
+ if (fdatasync_path(ctx->newdir) < 0) {
+ mailbox_set_critical(&ctx->mbox->box,
+ "fdatasync_path(%s) failed: %m", ctx->newdir);
+ return -1;
+ }
+ }
+ if (cur_changed) {
+ if (fdatasync_path(ctx->curdir) < 0) {
+ mailbox_set_critical(&ctx->mbox->box,
+ "fdatasync_path(%s) failed: %m", ctx->curdir);
+ return -1;
+ }
+ }
+ return 0;
+}
+
+static int seq_range_cmp(const struct seq_range *r1, const struct seq_range *r2)
+{
+ if (r1->seq1 < r2->seq2)
+ return -1;
+ else if (r1->seq1 > r2->seq2)
+ return 1;
+ else
+ return 0;
+}
+
+static uint32_t
+maildir_save_set_recent_flags(struct maildir_save_context *ctx)
+{
+ struct maildir_mailbox *mbox = ctx->mbox;
+ ARRAY_TYPE(seq_range) saved_sorted_uids;
+ const struct seq_range *uids;
+ unsigned int i, count;
+ uint32_t uid;
+
+ count = array_count(&ctx->ctx.transaction->changes->saved_uids);
+ if (count == 0)
+ return 0;
+
+ t_array_init(&saved_sorted_uids, count);
+ array_append_array(&saved_sorted_uids,
+ &ctx->ctx.transaction->changes->saved_uids);
+ array_sort(&saved_sorted_uids, seq_range_cmp);
+
+ uids = array_get(&saved_sorted_uids, &count);
+ for (i = 0; i < count; i++) {
+ for (uid = uids[i].seq1; uid <= uids[i].seq2; uid++)
+ mailbox_recent_flags_set_uid(&mbox->box, uid);
+ }
+ return uids[count-1].seq2 + 1;
+}
+
+static int
+maildir_save_sync_index(struct maildir_save_context *ctx)
+{
+ struct mailbox_transaction_context *_t = ctx->ctx.transaction;
+ struct maildir_mailbox *mbox = ctx->mbox;
+ uint32_t first_uid, next_uid, first_recent_uid;
+ int ret;
+
+ /* we'll need to keep the lock past the sync deinit */
+ ret = maildir_uidlist_lock(mbox->uidlist);
+ i_assert(ret > 0);
+
+ if (maildir_sync_header_refresh(mbox) < 0)
+ return -1;
+ if ((ret = maildir_uidlist_refresh_fast_init(mbox->uidlist)) < 0)
+ return -1;
+
+ if (ret == 0) {
+ /* uidlist doesn't exist. make sure all existing message
+ are added to uidlist first. */
+ (void)maildir_storage_sync_force(mbox, 0);
+ }
+
+ if (maildir_sync_index_begin(mbox, NULL, &ctx->sync_ctx) < 0)
+ return -1;
+ ctx->keywords_sync_ctx =
+ maildir_sync_get_keywords_sync_ctx(ctx->sync_ctx);
+
+ /* now that uidlist is locked, make sure all the existing mails
+ have been added to index. we don't really look into the
+ maildir, just add all the new mails listed in
+ dovecot-uidlist to index. */
+ if (maildir_sync_index(ctx->sync_ctx, TRUE) < 0)
+ return -1;
+
+ /* if messages were added to index, assign them UIDs */
+ first_uid = maildir_uidlist_get_next_uid(mbox->uidlist);
+ i_assert(first_uid != 0);
+ mail_index_append_finish_uids(ctx->trans, first_uid,
+ &_t->changes->saved_uids);
+ i_assert(ctx->files_count == seq_range_count(&_t->changes->saved_uids));
+
+ /* these mails are all recent in our session */
+ T_BEGIN {
+ next_uid = maildir_save_set_recent_flags(ctx);
+ } T_END;
+
+ if ((mbox->box.flags & MAILBOX_FLAG_DROP_RECENT) != 0)
+ first_recent_uid = next_uid;
+ else if (ctx->last_nonrecent_uid != 0)
+ first_recent_uid = ctx->last_nonrecent_uid + 1;
+ else
+ first_recent_uid = 0;
+
+ if (first_recent_uid != 0) {
+ /* maildir_sync_index() dropped recent flags from
+ existing messages. we'll still need to drop recent
+ flags from these newly added messages. */
+ mail_index_update_header(ctx->trans,
+ offsetof(struct mail_index_header,
+ first_recent_uid),
+ &first_recent_uid,
+ sizeof(first_recent_uid), FALSE);
+ }
+ return 0;
+}
+
+static void
+maildir_save_rollback_index_changes(struct maildir_save_context *ctx)
+{
+ uint32_t seq;
+
+ if (ctx->seq == 0)
+ return;
+
+ for (seq = ctx->seq; seq >= ctx->first_seq; seq--)
+ mail_index_expunge(ctx->trans, seq);
+
+ mail_cache_transaction_reset(ctx->ctx.transaction->cache_trans);
+}
+
+static bool maildir_filename_has_conflict(struct maildir_filename *mf,
+ struct maildir_filename *prev_mf)
+{
+ if (strcmp(mf->dest_basename, prev_mf->dest_basename) == 0) {
+ /* already used this */
+ return TRUE;
+ }
+ if (prev_mf->guid != NULL &&
+ strcmp(mf->dest_basename, prev_mf->guid) == 0) {
+ /* previous filename also had a conflict */
+ return TRUE;
+ }
+ return FALSE;
+}
+
+static void
+maildir_filename_check_conflicts(struct maildir_save_context *ctx,
+ struct maildir_filename *mf,
+ struct maildir_filename *prev_mf)
+{
+ uoff_t size, vsize;
+
+ if (!ctx->locked_uidlist_refresh && ctx->locked) {
+ (void)maildir_uidlist_refresh(ctx->mbox->uidlist);
+ ctx->locked_uidlist_refresh = TRUE;
+ }
+
+ if (!maildir_filename_get_size(mf->dest_basename,
+ MAILDIR_EXTRA_FILE_SIZE, &size))
+ size = UOFF_T_MAX;
+ if (!maildir_filename_get_size(mf->dest_basename,
+ MAILDIR_EXTRA_VIRTUAL_SIZE, &vsize))
+ vsize = UOFF_T_MAX;
+
+ if (size != mf->size || vsize != mf->vsize ||
+ !ctx->locked_uidlist_refresh ||
+ (prev_mf != NULL && maildir_filename_has_conflict(mf, prev_mf)) ||
+ maildir_uidlist_get_full_filename(ctx->mbox->uidlist,
+ mf->dest_basename) != NULL) {
+ /* a) dest_basename didn't contain the (correct) size/vsize.
+ they're required for good performance.
+
+ b) file already exists. give it another name.
+ but preserve the size/vsize in the filename if possible */
+ if (mf->size == UOFF_T_MAX)
+ mf->size = size;
+ if (mf->vsize == UOFF_T_MAX)
+ mf->vsize = size;
+
+ mf->guid = mf->dest_basename;
+ mf->dest_basename = p_strdup(ctx->pool,
+ maildir_filename_generate());
+ mf->preserve_filename = FALSE;
+ }
+}
+
+static int
+maildir_filename_dest_basename_cmp(struct maildir_filename *const *f1,
+ struct maildir_filename *const *f2)
+{
+ return strcmp((*f1)->dest_basename, (*f2)->dest_basename);
+}
+
+static int
+maildir_save_move_files_to_newcur(struct maildir_save_context *ctx)
+{
+ ARRAY(struct maildir_filename *) files;
+ struct maildir_filename *mf, *prev_mf;
+ bool newdir, new_changed, cur_changed;
+ int ret;
+
+ /* put files into an array sorted by the destination filename.
+ this way we can easily check if there are duplicate destination
+ filenames within this transaction. */
+ t_array_init(&files, ctx->files_count);
+ for (mf = ctx->files; mf != NULL; mf = mf->next)
+ array_push_back(&files, &mf);
+ array_sort(&files, maildir_filename_dest_basename_cmp);
+
+ new_changed = cur_changed = FALSE;
+ prev_mf = NULL;
+ array_foreach_elem(&files, mf) {
+ T_BEGIN {
+ const char *dest;
+
+ if (mf->preserve_filename) {
+ maildir_filename_check_conflicts(ctx, mf,
+ prev_mf);
+ }
+
+ newdir = maildir_get_dest_filename(ctx, mf, &dest);
+ if (newdir)
+ new_changed = TRUE;
+ else
+ cur_changed = TRUE;
+ ret = maildir_file_move(ctx, mf, dest, newdir);
+ } T_END;
+ if (ret < 0)
+ return -1;
+ prev_mf = mf;
+ }
+
+ if (ctx->locked) {
+ i_assert(ctx->sync_ctx != NULL);
+ maildir_sync_set_new_msgs_count(ctx->sync_ctx,
+ array_count(&files));
+ }
+ return maildir_transaction_fsync_dirs(ctx, new_changed, cur_changed);
+}
+
+static void maildir_save_sync_uidlist(struct maildir_save_context *ctx)
+{
+ struct mailbox_transaction_context *t = ctx->ctx.transaction;
+ struct maildir_filename *mf;
+ struct seq_range_iter iter;
+ enum maildir_uidlist_rec_flag flags;
+ struct maildir_uidlist_rec *rec;
+ unsigned int n = 0;
+ uint32_t uid;
+ bool newdir, bret;
+ int ret;
+
+ seq_range_array_iter_init(&iter, &t->changes->saved_uids);
+ for (mf = ctx->files; mf != NULL; mf = mf->next) T_BEGIN {
+ const char *dest;
+
+ bret = seq_range_array_iter_nth(&iter, n++, &uid);
+ i_assert(bret);
+
+ newdir = maildir_get_dest_filename(ctx, mf, &dest);
+ flags = MAILDIR_UIDLIST_REC_FLAG_RECENT;
+ if (newdir)
+ flags |= MAILDIR_UIDLIST_REC_FLAG_NEW_DIR;
+ ret = maildir_uidlist_sync_next_uid(ctx->uidlist_sync_ctx,
+ dest, uid, flags, &rec);
+ i_assert(ret > 0);
+ i_assert(rec != NULL);
+ if (mf->guid != NULL) {
+ maildir_uidlist_sync_set_ext(ctx->uidlist_sync_ctx, rec,
+ MAILDIR_UIDLIST_REC_EXT_GUID, mf->guid);
+ }
+ if (mf->pop3_uidl != NULL) {
+ maildir_uidlist_sync_set_ext(ctx->uidlist_sync_ctx, rec,
+ MAILDIR_UIDLIST_REC_EXT_POP3_UIDL,
+ mf->pop3_uidl);
+ }
+ if (mf->pop3_order > 0) {
+ maildir_uidlist_sync_set_ext(ctx->uidlist_sync_ctx, rec,
+ MAILDIR_UIDLIST_REC_EXT_POP3_ORDER,
+ t_strdup_printf("%u", mf->pop3_order));
+ }
+ } T_END;
+ i_assert(!seq_range_array_iter_nth(&iter, n, &uid));
+}
+
+int maildir_transaction_save_commit_pre(struct mail_save_context *_ctx)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+ struct mailbox_transaction_context *_t = _ctx->transaction;
+ enum maildir_uidlist_sync_flags sync_flags;
+ int ret;
+
+ i_assert(_ctx->data.output == NULL);
+ i_assert(ctx->last_save_finished);
+
+ if (ctx->files_count == 0)
+ return 0;
+
+ sync_flags = MAILDIR_UIDLIST_SYNC_PARTIAL |
+ MAILDIR_UIDLIST_SYNC_NOREFRESH;
+
+ if ((_t->flags & MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS) != 0) {
+ /* we want to assign UIDs, we must lock uidlist */
+ } else if (ctx->have_keywords) {
+ /* keywords file updating relies on uidlist lock. */
+ } else if (ctx->have_preserved_filenames) {
+ /* we're trying to use some potentially existing filenames.
+ we must lock to avoid race conditions where two sessions
+ try to save the same filename. */
+ } else {
+ /* no requirement to lock uidlist. if we happen to get a lock,
+ assign uids. */
+ sync_flags |= MAILDIR_UIDLIST_SYNC_TRYLOCK;
+ }
+ ret = maildir_uidlist_sync_init(ctx->mbox->uidlist, sync_flags,
+ &ctx->uidlist_sync_ctx);
+ if (ret > 0) {
+ ctx->locked = TRUE;
+ if (maildir_save_sync_index(ctx) < 0) {
+ maildir_transaction_save_rollback(_ctx);
+ return -1;
+ }
+ } else if (ret == 0 &&
+ (sync_flags & MAILDIR_UIDLIST_SYNC_TRYLOCK) != 0) {
+ ctx->locked = FALSE;
+ i_assert(ctx->uidlist_sync_ctx == NULL);
+ /* since we couldn't lock uidlist, we'll have to drop the
+ appends to index. */
+ maildir_save_rollback_index_changes(ctx);
+ } else {
+ maildir_transaction_save_rollback(_ctx);
+ return -1;
+ }
+
+ T_BEGIN {
+ ret = maildir_save_move_files_to_newcur(ctx);
+ } T_END;
+ if (ctx->locked) {
+ if (ret == 0) {
+ /* update dovecot-uidlist file. */
+ maildir_save_sync_uidlist(ctx);
+ }
+
+ if (maildir_uidlist_sync_deinit(&ctx->uidlist_sync_ctx,
+ ret == 0) < 0)
+ ret = -1;
+ }
+
+ _t->changes->uid_validity =
+ maildir_uidlist_get_uid_validity(ctx->mbox->uidlist);
+
+ if (ctx->locked) {
+ /* It doesn't matter if index syncing fails */
+ ctx->keywords_sync_ctx = NULL;
+ if (ret < 0)
+ maildir_sync_index_rollback(&ctx->sync_ctx);
+ else
+ (void)maildir_sync_index_commit(&ctx->sync_ctx);
+ }
+
+ if (ret < 0) {
+ ctx->keywords_sync_ctx = !ctx->have_keywords ? NULL :
+ maildir_keywords_sync_init(ctx->mbox->keywords,
+ ctx->mbox->box.index);
+
+ /* unlink the files we just moved in an attempt to rollback
+ the transaction. uidlist is still locked, so at least other
+ Dovecot instances haven't yet seen the files. we need
+ to have the keywords sync context to be able to generate
+ the destination filenames if keywords were used. */
+ maildir_save_unlink_files(ctx);
+
+ if (ctx->keywords_sync_ctx != NULL)
+ maildir_keywords_sync_deinit(&ctx->keywords_sync_ctx);
+ /* returning failure finishes the save_context */
+ maildir_transaction_save_rollback(_ctx);
+ return -1;
+ }
+ return 0;
+}
+
+void maildir_transaction_save_commit_post(struct mail_save_context *_ctx,
+ struct mail_index_transaction_commit_result *result ATTR_UNUSED)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+
+ _ctx->transaction = NULL; /* transaction is already freed */
+
+ if (ctx->locked)
+ maildir_uidlist_unlock(ctx->mbox->uidlist);
+ pool_unref(&ctx->pool);
+}
+
+void maildir_transaction_save_rollback(struct mail_save_context *_ctx)
+{
+ struct maildir_save_context *ctx = MAILDIR_SAVECTX(_ctx);
+
+ i_assert(_ctx->data.output == NULL);
+
+ if (!ctx->last_save_finished)
+ maildir_save_cancel(&ctx->ctx);
+
+ /* delete files in tmp/ */
+ maildir_save_unlink_files(ctx);
+
+ if (ctx->uidlist_sync_ctx != NULL)
+ (void)maildir_uidlist_sync_deinit(&ctx->uidlist_sync_ctx, FALSE);
+ if (ctx->sync_ctx != NULL)
+ maildir_sync_index_rollback(&ctx->sync_ctx);
+ if (ctx->locked)
+ maildir_uidlist_unlock(ctx->mbox->uidlist);
+
+ pool_unref(&ctx->pool);
+}