view src/lib-storage/index/dbox/dbox-save.c @ 6636:6d870394cc3b HEAD

Handle maildir -> dbox file conversion when metadata is wanted to be updated.
author Timo Sirainen <tss@iki.fi>
date Sun, 28 Oct 2007 00:09:06 +0300
parents aedda93baa2c
children 4a3cc2968040
line wrap: on
line source

/* Copyright (c) 2007 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "array.h"
#include "hex-dec.h"
#include "str.h"
#include "istream.h"
#include "istream-crlf.h"
#include "ostream.h"
#include "write-full.h"
#include "index-mail.h"
#include "dbox-storage.h"
#include "dbox-index.h"
#include "dbox-file.h"
#include "dbox-sync.h"

#include <stdlib.h>

struct dbox_save_mail {
	struct dbox_file *file;
	uint32_t seq;
	uint32_t append_offset;
	uoff_t message_size;
};

struct dbox_save_context {
	struct mail_save_context ctx;

	struct dbox_mailbox *mbox;
	struct mail_index_transaction *trans;

	struct dbox_index_append_context *append_ctx;
	struct dbox_sync_context *sync_ctx;

	/* updated for each appended mail: */
	uint32_t seq;
	struct istream *input;
	struct mail *mail, *cur_dest_mail;
	time_t cur_received_date;
	enum mail_flags cur_flags;
	string_t *cur_keywords;

	struct dbox_file *cur_file;
	struct ostream *cur_output;

	ARRAY_DEFINE(mails, struct dbox_save_mail);

	unsigned int failed:1;
	unsigned int finished:1;
};

static void dbox_save_keywords(struct dbox_save_context *ctx,
			       struct mail_keywords *keywords)
{
	if (ctx->cur_keywords == NULL)
		ctx->cur_keywords = str_new(default_pool, 128);
	else
		str_truncate(ctx->cur_keywords, 0);
	dbox_mail_metadata_keywords_append(ctx->mbox, ctx->cur_keywords,
					   keywords);
}

int dbox_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, struct mail *dest_mail,
		   struct mail_save_context **ctx_r)
{
	struct dbox_transaction_context *t =
		(struct dbox_transaction_context *)_t;
	struct dbox_mailbox *mbox = (struct dbox_mailbox *)t->ictx.ibox;
	struct dbox_save_context *ctx = t->save_ctx;
	struct dbox_message_header dbox_msg_hdr;
	struct dbox_save_mail *save_mail;
	struct istream *crlf_input;
	enum mail_flags save_flags;
	const struct stat *st;
	uoff_t mail_size;

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

	if (ctx == NULL) {
		ctx = t->save_ctx = i_new(struct dbox_save_context, 1);
		ctx->ctx.transaction = &t->ictx.mailbox_ctx;
		ctx->mbox = mbox;
		ctx->trans = t->ictx.trans;
		ctx->append_ctx = dbox_index_append_begin(mbox->dbox_index);
		i_array_init(&ctx->mails, 32);
	}

	/* get the size of the mail to be saved, if possible */
	st = i_stream_stat(input, TRUE);
	mail_size = st == NULL || st->st_size == -1 ? 0 : st->st_size;

	if (dbox_index_append_next(ctx->append_ctx, mail_size,
				   &ctx->cur_file, &ctx->cur_output) < 0) {
		ctx->failed = TRUE;
		return -1;
	}

	/* add to index */
	save_flags = flags & ~MAIL_RECENT;
	mail_index_append(ctx->trans, 0, &ctx->seq);
	mail_index_update_flags(ctx->trans, ctx->seq, MODIFY_REPLACE,
				save_flags);
	if (keywords != NULL) {
		mail_index_update_keywords(ctx->trans, ctx->seq,
					   MODIFY_REPLACE, keywords);
	}

	if (dest_mail == NULL) {
		if (ctx->mail == NULL)
			ctx->mail = mail_alloc(_t, 0, NULL);
		dest_mail = ctx->mail;
	}
	mail_set_seq(dest_mail, ctx->seq);

	ctx->cur_dest_mail = dest_mail;

	crlf_input = i_stream_create_lf(input);
	ctx->input = index_mail_cache_parse_init(dest_mail, crlf_input);
	i_stream_unref(&crlf_input);

	save_mail = array_append_space(&ctx->mails);
	save_mail->file = ctx->cur_file;
	save_mail->seq = ctx->seq;
	i_assert(ctx->cur_output->offset <= (uint32_t)-1);
	save_mail->append_offset = ctx->cur_output->offset;

	/* write a dummy header. it'll get rewritten when we're finished */
	memset(&dbox_msg_hdr, 0, sizeof(dbox_msg_hdr));
	o_stream_cork(ctx->cur_output);
	if (o_stream_send(ctx->cur_output, &dbox_msg_hdr,
			  sizeof(dbox_msg_hdr)) < 0) {
		mail_storage_set_critical(_t->box->storage,
			"o_stream_send(%s) failed: %m",
			ctx->cur_file->path);
		ctx->failed = TRUE;
	}

	ctx->cur_received_date = received_date != (time_t)-1 ?
		received_date : ioloop_time;
	ctx->cur_flags = flags;
	dbox_save_keywords(ctx, keywords);

	*ctx_r = &ctx->ctx;
	return ctx->failed ? -1 : 0;
}

int dbox_save_continue(struct mail_save_context *_ctx)
{
	struct dbox_save_context *ctx = (struct dbox_save_context *)_ctx;
	struct mail_storage *storage = &ctx->mbox->storage->storage;

	if (ctx->failed)
		return -1;

	do {
		if (o_stream_send_istream(ctx->cur_output, ctx->input) < 0) {
			if (!mail_storage_set_error_from_errno(storage)) {
				mail_storage_set_critical(storage,
					"o_stream_send_istream(%s) failed: %m",
					ctx->cur_file->path);
			}
			ctx->failed = TRUE;
			return -1;
		}
		index_mail_cache_parse_continue(ctx->cur_dest_mail);

		/* both tee input readers may consume data from our primary
		   input stream. we'll have to make sure we don't return with
		   one of the streams still having data in them. */
	} while (i_stream_read(ctx->input) > 0);
	return 0;
}

static void dbox_save_write_metadata(struct dbox_save_context *ctx)
{
	struct dbox_metadata_header metadata_hdr;
	char space[DBOX_EXTRA_SPACE];
	string_t *str;
	uoff_t vsize;

	memset(&metadata_hdr, 0, sizeof(metadata_hdr));
	memcpy(metadata_hdr.magic_post, DBOX_MAGIC_POST,
	       sizeof(metadata_hdr.magic_post));
	o_stream_send(ctx->cur_output, &metadata_hdr, sizeof(metadata_hdr));

	str = t_str_new(256);
	/* write first fields that don't change */
	str_printfa(str, "%c%lx\n", DBOX_METADATA_RECEIVED_TIME,
		    (unsigned long)ctx->cur_received_date);
	str_printfa(str, "%c%lx\n", DBOX_METADATA_SAVE_TIME,
		    (unsigned long)ioloop_time);
	if (mail_get_virtual_size(ctx->cur_dest_mail, &vsize) < 0)
		i_unreached();
	str_printfa(str, "%c%llx\n", DBOX_METADATA_VIRTUAL_SIZE,
		    (unsigned long long)vsize);

	/* flags */
	str_append_c(str, DBOX_METADATA_FLAGS);
	dbox_mail_metadata_flags_append(str, ctx->cur_flags);
	str_append_c(str, '\n');

	/* keywords */
	if (ctx->cur_keywords != NULL && str_len(ctx->cur_keywords) > 0) {
		str_append_c(str, DBOX_METADATA_KEYWORDS);
		str_append_str(str, ctx->cur_keywords);
		str_append_c(str, '\n');
	}

	o_stream_send(ctx->cur_output, str_data(str), str_len(str));
	memset(space, ' ', sizeof(space));
	o_stream_send(ctx->cur_output, space, sizeof(space));
	o_stream_send(ctx->cur_output, "\n", 1);
}

int dbox_save_finish(struct mail_save_context *_ctx)
{
	struct dbox_save_context *ctx = (struct dbox_save_context *)_ctx;
	struct mail_storage *storage = &ctx->mbox->storage->storage;
	struct dbox_save_mail *save_mail;
	uoff_t offset = 0;
	unsigned int count;

	ctx->finished = TRUE;
	if (ctx->cur_output == NULL)
		return -1;

	index_mail_cache_parse_deinit(ctx->cur_dest_mail,
				      ctx->cur_received_date);

	if (!ctx->failed) {
		offset = ctx->cur_output->offset;
		dbox_save_write_metadata(ctx);
		if (o_stream_flush(ctx->cur_output) < 0) {
			mail_storage_set_critical(storage,
				"o_stream_flush(%s) failed: %m",
				ctx->cur_file->path);
			ctx->failed = TRUE;
		}
	}

	o_stream_destroy(&ctx->cur_output);
	i_stream_unref(&ctx->input);

	count = array_count(&ctx->mails);
	save_mail = array_idx_modifiable(&ctx->mails, count - 1);
	if (ctx->failed) {
		dbox_file_cancel_append(save_mail->file,
					save_mail->append_offset);
		dbox_file_unref(&save_mail->file);
		array_delete(&ctx->mails, count - 1, 1);
		return -1;
	} else {
		dbox_file_finish_append(save_mail->file);
		save_mail->message_size = offset - save_mail->append_offset -
			save_mail->file->msg_header_size;
		return 0;
	}
}

void dbox_save_cancel(struct mail_save_context *_ctx)
{
	struct dbox_save_context *ctx = (struct dbox_save_context *)_ctx;

	ctx->failed = TRUE;
	(void)dbox_save_finish(_ctx);
}

static int
dbox_save_mail_write_header(struct dbox_save_mail *mail, uint32_t uid)
{
	struct dbox_message_header dbox_msg_hdr;
	struct ostream *output = mail->file->output;
	uoff_t orig_offset;
	int ret = 0;

	i_assert(mail->file->msg_header_size == sizeof(dbox_msg_hdr));

	mail->file->last_append_uid = uid;
	dbox_msg_header_fill(&dbox_msg_hdr, uid, mail->message_size);

	orig_offset = output->offset;
	o_stream_seek(output, mail->append_offset);
	if (o_stream_send(output, &dbox_msg_hdr, sizeof(dbox_msg_hdr)) < 0 ||
	    o_stream_flush(output) < 0) {
		dbox_file_set_syscall_error(mail->file, "write");
		ret = -1;
	}
	o_stream_seek(output, orig_offset);
	return ret;
}

static int
dbox_save_file_write_append_offset(struct dbox_file *file, uoff_t append_offset)
{
	char buf[8+1];

	i_assert(append_offset <= (uint32_t)-1);

	i_snprintf(buf, sizeof(buf), "%08x", (unsigned int)append_offset);
	if (pwrite_full(file->fd, buf, sizeof(buf)-1,
			file->append_offset_header_pos) < 0) {
		dbox_file_set_syscall_error(file, "pwrite");
		return -1;
	}
	return 0;
}

static int dbox_save_file_commit_header(struct dbox_save_mail *mail)
{
	uoff_t append_offset;

	append_offset = dbox_file_get_next_append_offset(mail->file);
	return dbox_save_file_write_append_offset(mail->file, append_offset);
}

static void dbox_save_file_uncommit_header(struct dbox_save_mail *mail)
{
	if (mail->file->file_id == 0) {
		/* temporary file, we'll just unlink it later */
		return;
	}
	(void)dbox_save_file_write_append_offset(mail->file,
						 mail->append_offset);
}

static int dbox_save_mail_file_cmp(const void *p1, const void *p2)
{
	const struct dbox_save_mail *m1 = p1, *m2 = p2;
	int ret;

	ret = strcmp(m1->file->path, m2->file->path);
	if (ret == 0) {
		/* the oldest sequence is first. this is needed for uncommit
		   to work right. */
		ret = (int)m1->seq - (int)m2->seq;
	}
	return ret;
}

static int dbox_save_commit(struct dbox_save_context *ctx, uint32_t first_uid)
{
	struct dbox_mail_index_record rec;
	struct dbox_save_mail *mails;
	unsigned int i, count;

	/* first write updated mail headers and collect all files we wrote to */
	mails = array_get_modifiable(&ctx->mails, &count);
	for (i = 0; i < count; i++) {
		if (dbox_save_mail_write_header(&mails[i], first_uid++) < 0)
			return -1;
	}

	/* update append offsets in file headers */
	qsort(mails, count, sizeof(*mails), dbox_save_mail_file_cmp);
	for (i = 0; i < count; i++) {
		if (i > 0 && mails[i].file == mails[i-1].file) {
			/* already written */
			continue;
		}

		if (dbox_save_file_commit_header(&mails[i]) < 0) {
			/* have to uncommit all changes so far */
			for (; i > 0; i--) {
				if (i > 1 &&
				    mails[i-2].file == mails[i-1].file)
					continue;
				dbox_save_file_uncommit_header(&mails[i-1]);
			}
			return -1;
		}
	}

	/* set file_id / offsets to records */
	if (dbox_index_append_assign_file_ids(ctx->append_ctx) < 0)
		return -1;

	memset(&rec, 0, sizeof(rec));
	for (i = 0; i < count; i++) {
		rec.file_id = mails[i].file->file_id;
		rec.offset = mails[i].append_offset;

		if ((rec.file_id & DBOX_FILE_ID_FLAG_UID) == 0) {
			mail_index_update_ext(ctx->trans, mails[i].seq,
					      ctx->mbox->dbox_ext_id,
					      &rec, NULL);
		}
	}
	return 0;
}

int dbox_transaction_save_commit_pre(struct dbox_save_context *ctx)
{
	struct dbox_transaction_context *t =
		(struct dbox_transaction_context *)ctx->ctx.transaction;
	const struct mail_index_header *hdr;
	uint32_t uid, next_uid;

	i_assert(ctx->finished);

	if (dbox_sync_begin(ctx->mbox, &ctx->sync_ctx, FALSE) < 0) {
		ctx->failed = TRUE;
		dbox_transaction_save_rollback(ctx);
		return -1;
	}

	hdr = mail_index_get_header(ctx->sync_ctx->sync_view);
	uid = hdr->next_uid;
	mail_index_append_assign_uids(ctx->trans, uid, &next_uid);

	if (dbox_save_commit(ctx, uid) < 0) {
		ctx->failed = TRUE;
		dbox_transaction_save_rollback(ctx);
		return -1;
	}

	*t->ictx.saved_uid_validity = hdr->uid_validity;
	*t->ictx.first_saved_uid = uid;
	*t->ictx.last_saved_uid = next_uid - 1;

	dbox_index_append_commit(&ctx->append_ctx);
	return 0;
}

void dbox_transaction_save_commit_post(struct dbox_save_context *ctx)
{
	ctx->ctx.transaction = NULL; /* transaction is already freed */

	(void)dbox_sync_finish(&ctx->sync_ctx, TRUE);
	dbox_transaction_save_rollback(ctx);
}

void dbox_transaction_save_rollback(struct dbox_save_context *ctx)
{
	if (!ctx->finished)
		dbox_save_cancel(&ctx->ctx);
	if (ctx->append_ctx != NULL)
		dbox_index_append_rollback(&ctx->append_ctx);

	if (ctx->sync_ctx != NULL)
		(void)dbox_sync_finish(&ctx->sync_ctx, FALSE);

	if (ctx->mail != NULL)
		mail_free(&ctx->mail);
	if (ctx->cur_keywords != NULL)
		str_free(&ctx->cur_keywords);
	array_free(&ctx->mails);
	i_free(ctx);
}