view src/lib-storage/index/maildir/maildir-save.c @ 3530:e9695ec7925b HEAD

Recursive maildir uidlist syncs caused assert crashes. Also did some cleanups.
author Timo Sirainen <tss@iki.fi>
date Sun, 14 Aug 2005 22:02:54 +0300
parents 35be29ba348d
children 1a1f0ec613d6
line wrap: on
line source

/* Copyright (C) 2002-2004 Timo Sirainen */

#include "lib.h"
#include "ioloop.h"
#include "array.h"
#include "buffer.h"
#include "ostream.h"
#include "ostream-crlf.h"
#include "str.h"
#include "maildir-storage.h"
#include "maildir-uidlist.h"

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <utime.h>
#include <sys/stat.h>

struct maildir_filename {
	struct maildir_filename *next;
	const char *basename;

	enum mail_flags flags;
	unsigned int keywords_count;
	/* unsigned int 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_index_sync_context *sync_ctx;

	const char *tmpdir, *newdir, *curdir;
	struct maildir_filename *files;

	buffer_t *keywords_buffer;
	array_t ARRAY_DEFINE(keywords_array, unsigned int);

	struct istream *input;
	struct ostream *output;
	int fd;
	time_t received_date;
	uint32_t seq;

	unsigned int synced:1;
	unsigned int failed:1;
};

static int maildir_file_move(struct maildir_save_context *ctx,
			     const char *basename, const char *dest)
{
	const char *tmp_path, *new_path;
	int ret;

	t_push();

	/* 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, "/", basename, NULL);
	new_path = dest == NULL ?
		t_strconcat(ctx->newdir, "/", basename, NULL) :
		t_strconcat(ctx->curdir, "/", dest, NULL);

	if (link(tmp_path, new_path) == 0)
		ret = 0;
	else {
		ret = -1;
		if (ENOSPACE(errno)) {
			mail_storage_set_error(STORAGE(ctx->mbox->storage),
					       "Not enough disk space");
		} else {
			mail_storage_set_critical(STORAGE(ctx->mbox->storage),
				"link(%s, %s) failed: %m", tmp_path, new_path);
		}
	}

	if (unlink(tmp_path) < 0 && errno != ENOENT) {
		mail_storage_set_critical(STORAGE(ctx->mbox->storage),
			"unlink(%s) failed: %m", tmp_path);
	}
	t_pop();
	return ret;
}

static struct maildir_save_context *
maildir_save_transaction_init(struct maildir_transaction_context *t)
{
        struct maildir_mailbox *mbox = (struct maildir_mailbox *)t->ictx.ibox;
	struct maildir_save_context *ctx;
	pool_t pool;

	pool = pool_alloconly_create("maildir_save_context", 4096);
	ctx = p_new(pool, struct maildir_save_context, 1);
	ctx->ctx.transaction = &t->ictx.mailbox_ctx;
	ctx->pool = pool;
	ctx->mbox = mbox;
	ctx->trans = t->ictx.trans;

	ctx->tmpdir = p_strconcat(pool, mbox->path, "/tmp", NULL);
	ctx->newdir = p_strconcat(pool, mbox->path, "/new", NULL);
	ctx->curdir = p_strconcat(pool, mbox->path, "/cur", NULL);

	ctx->synced = maildir_sync_is_synced(mbox) > 0;

	ctx->keywords_buffer = buffer_create_const_data(pool, NULL, 0);
	array_create_from_buffer(&ctx->keywords_array, ctx->keywords_buffer,
				 sizeof(unsigned int));
	return ctx;
}

struct mail_save_context *
maildir_save_init(struct mailbox_transaction_context *_t,
		  enum mail_flags flags, struct mail_keywords *keywords,
		  time_t received_date, int timezone_offset __attr_unused__,
		  const char *from_envelope __attr_unused__,
		  struct istream *input, int want_mail)
{
	struct maildir_transaction_context *t =
		(struct maildir_transaction_context *)_t;
	struct maildir_save_context *ctx;
	struct maildir_mailbox *mbox = (struct maildir_mailbox *)t->ictx.ibox;
	struct maildir_filename *mf;
	struct ostream *output;
	const char *fname, *path;

	i_assert((t->ictx.flags & MAILBOX_TRANSACTION_FLAG_EXTERNAL) != 0);

	t_push();

	if (t->save_ctx == NULL)
		t->save_ctx = maildir_save_transaction_init(t);
	ctx = t->save_ctx;

	/* create a new file in tmp/ directory */
	ctx->fd = maildir_create_tmp(mbox, ctx->tmpdir, mbox->mail_create_mode,
				     &path);
	if (ctx->fd == -1) {
		ctx->failed = TRUE;
		t_pop();
		return &ctx->ctx;
	}

	fname = strrchr(path, '/');
	i_assert(fname != NULL);
	fname++;

	ctx->received_date = received_date;
	ctx->input = input;

	output = o_stream_create_file(ctx->fd, system_pool, 0, FALSE);
	ctx->output = (STORAGE(ctx->mbox->storage)->flags &
		       MAIL_STORAGE_FLAG_SAVE_CRLF) != 0 ?
		o_stream_create_crlf(default_pool, output) :
		o_stream_create_lf(default_pool, output);
	o_stream_unref(output);

	flags &= ~MAIL_RECENT;
	if (mbox->ibox.keep_recent)
		flags |= MAIL_RECENT;

	/* 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/. */
	/* @UNSAFE */
	mf = p_malloc(ctx->pool, sizeof(*mf) +
		      sizeof(unsigned int) * (keywords == NULL ? 0 :
					      keywords->count));
	mf->next = ctx->files;
	mf->basename = p_strdup(ctx->pool, fname);
	mf->flags = flags;
	ctx->files = mf;

	if (keywords != NULL) {
		i_assert(sizeof(keywords->idx[0]) == sizeof(unsigned int));

		/* @UNSAFE */
		mf->keywords_count = keywords->count;
		memcpy(mf + 1, keywords->idx,
		       sizeof(unsigned int) * keywords->count);
	}

	if (!ctx->synced && want_mail) {
		if (maildir_storage_sync_force(mbox) < 0)
			ctx->failed = TRUE;
		else
			ctx->synced = TRUE;
	}

	if (ctx->synced) {
		/* insert into index */
		mail_index_append(ctx->trans, 0, &ctx->seq);
		mail_index_update_flags(ctx->trans, ctx->seq,
					MODIFY_REPLACE, flags);
		if (keywords != NULL) {
			mail_index_update_keywords(ctx->trans, ctx->seq,
						   MODIFY_REPLACE, keywords);
		}
	}
	t_pop();

	ctx->failed = FALSE;
	return &ctx->ctx;
}

int maildir_save_continue(struct mail_save_context *_ctx)
{
	struct maildir_save_context *ctx = (struct maildir_save_context *)_ctx;

	if (ctx->failed)
		return -1;

	if (o_stream_send_istream(ctx->output, ctx->input) < 0) {
		ctx->failed = TRUE;
		return -1;
	}
	return 0;
}

int maildir_save_finish(struct mail_save_context *_ctx, struct mail *dest_mail)
{
	struct maildir_save_context *ctx = (struct maildir_save_context *)_ctx;
	struct utimbuf buf;
	const char *path;
	int output_errno;

	if (ctx->failed && ctx->fd == -1) {
		/* tmp file creation failed */
		return -1;
	}

	t_push();
	path = t_strconcat(ctx->tmpdir, "/", ctx->files->basename, NULL);

	if (ctx->received_date != (time_t)-1) {
		/* set the received_date by modifying mtime */
		buf.actime = ioloop_time;
		buf.modtime = ctx->received_date;

		if (utime(path, &buf) < 0) {
			ctx->failed = TRUE;
			mail_storage_set_critical(STORAGE(ctx->mbox->storage),
						  "utime(%s) failed: %m", path);
		}
	}

	output_errno = ctx->output->stream_errno;
	o_stream_unref(ctx->output);
	ctx->output = NULL;

	/* FIXME: when saving multiple messages, we could get better
	   performance if we left the fd open and fsync()ed it later */
	if (fsync(ctx->fd) < 0) {
		mail_storage_set_critical(STORAGE(ctx->mbox->storage),
					  "fsync(%s) failed: %m", path);
		ctx->failed = TRUE;
	}
	if (close(ctx->fd) < 0) {
		mail_storage_set_critical(STORAGE(ctx->mbox->storage),
					  "close(%s) failed: %m", path);
		ctx->failed = TRUE;
	}
	ctx->fd = -1;

	if (ctx->failed) {
		/* delete the tmp file */
		if (unlink(path) < 0 && errno != ENOENT) {
			mail_storage_set_critical(STORAGE(ctx->mbox->storage),
				"unlink(%s) failed: %m", path);
		}

		errno = output_errno;
		if (ENOSPACE(errno)) {
			mail_storage_set_error(STORAGE(ctx->mbox->storage),
					       "Not enough disk space");
		} else if (errno != 0) {
			mail_storage_set_critical(STORAGE(ctx->mbox->storage),
				"write(%s) failed: %m", ctx->mbox->path);
		}

		ctx->files = ctx->files->next;
		t_pop();
		return -1;
	}

	if (dest_mail != NULL) {
		i_assert(ctx->seq != 0);

		if (mail_set_seq(dest_mail, ctx->seq) < 0)
			return -1;
	}

	t_pop();
	return 0;
}

void maildir_save_cancel(struct mail_save_context *_ctx)
{
	struct maildir_save_context *ctx = (struct maildir_save_context *)_ctx;

	ctx->failed = TRUE;
	(void)maildir_save_finish(_ctx, NULL);
}

static const char *
maildir_get_updated_filename(struct maildir_save_context *ctx,
			     struct maildir_filename *mf)
{
	if (mf->flags == MAIL_RECENT && mf->keywords_count == 0)
		return NULL;

	buffer_update_const_data(ctx->keywords_buffer, mf + 1,
				 mf->keywords_count * sizeof(unsigned int));
	return maildir_filename_set_flags(ctx->sync_ctx, mf->basename,
					  mf->flags, &ctx->keywords_array);
}

static void
maildir_save_commit_abort(struct maildir_save_context *ctx,
			  struct maildir_filename *pos)
{
	struct maildir_filename *mf;
	const char *path, *dest;

	/* try to unlink the mails already moved */
	for (mf = ctx->files; mf != pos; mf = mf->next) {
		t_push();
		dest = maildir_get_updated_filename(ctx, mf);
		if (dest != NULL)
			path = t_strdup_printf("%s/%s", ctx->curdir, dest);
		else {
			path = t_strdup_printf("%s/%s",
					       ctx->newdir, mf->basename);
		}
		(void)unlink(path);
	}
	ctx->files = pos;

	maildir_transaction_save_rollback(ctx);
}

int maildir_transaction_save_commit_pre(struct maildir_save_context *ctx)
{
	struct maildir_filename *mf;
	uint32_t first_uid, last_uid;
	enum maildir_uidlist_rec_flag flags;
	const char *dest, *fname;
	int ret;

	i_assert(ctx->output == NULL);

	ctx->sync_ctx = maildir_sync_index_begin(ctx->mbox);
	if (ctx->sync_ctx == NULL) {
		maildir_save_commit_abort(ctx, ctx->files);
		return -1;
	}

	if (maildir_uidlist_sync_init(ctx->mbox->uidlist, TRUE,
				      &ctx->uidlist_sync_ctx) <= 0) {
		/* error or timeout - our transaction is broken */
		maildir_sync_index_abort(ctx->sync_ctx);
		maildir_save_commit_abort(ctx, ctx->files);
		return -1;
	}

	if (ctx->synced) {
		first_uid = maildir_uidlist_get_next_uid(ctx->mbox->uidlist);
		mail_index_append_assign_uids(ctx->trans, first_uid, &last_uid);
	}

	flags = MAILDIR_UIDLIST_REC_FLAG_NEW_DIR |
		MAILDIR_UIDLIST_REC_FLAG_RECENT;

	/* move them into new/ */
	ret = 0;
	for (mf = ctx->files; mf != NULL; mf = mf->next) {
		t_push();
		dest = maildir_get_updated_filename(ctx, mf);
		fname = dest != NULL ? dest : mf->basename;

		if (maildir_file_move(ctx, mf->basename, dest) < 0 ||
		    maildir_uidlist_sync_next(ctx->uidlist_sync_ctx,
					      fname, flags) < 0) {
			maildir_save_commit_abort(ctx, mf);
			t_pop();
			ret = -1;
			break;
		}
		t_pop();
	}

	if (ret == 0) {
		/* finish uidlist syncing, but keep it still locked */
		maildir_uidlist_sync_finish(ctx->uidlist_sync_ctx);
	}

	if (ret < 0) {
		/* deinit only if we failed. otherwise save_commit_post()
		   does it. */
		if (maildir_uidlist_sync_deinit(ctx->uidlist_sync_ctx) < 0)
			ret = -1;
		ctx->uidlist_sync_ctx = NULL;
	}

	return ret;
}

void maildir_transaction_save_commit_post(struct maildir_save_context *ctx)
{
	/* can't do anything anymore if we fail */
	(void)maildir_uidlist_sync_deinit(ctx->uidlist_sync_ctx);

	/* to avoid deadlocks uidlist must not be left locked without index
	   being locked, so we can't put call to save_commit_pre(). */
	maildir_sync_index_abort(ctx->sync_ctx);

	pool_unref(ctx->pool);
}

void maildir_transaction_save_rollback(struct maildir_save_context *ctx)
{
	struct maildir_filename *mf;
	string_t *str;

	i_assert(ctx->output == NULL);

	t_push();
	str = t_str_new(1024);

	/* clean up the temp files */
	for (mf = ctx->files; mf != NULL; mf = mf->next) {
		str_truncate(str, 0);
		str_printfa(str, "%s/%s", ctx->tmpdir, mf->basename);
		(void)unlink(str_c(str));
	}
	t_pop();

	pool_unref(ctx->pool);
}