/* Copyright (c) 2005-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "ioloop.h"
#include "array.h"
#include "str.h"
#include "str-sanitize.h"
#include "time-util.h"
#include "unichar.h"
#include "var-expand.h"
#include "message-address.h"
#include "smtp-address.h"
#include "lda-settings.h"
#include "mail-storage.h"
#include "mail-namespace.h"
#include "mail-storage-private.h"
#include "mail-duplicate.h"
#include "mail-deliver.h"

#define DUPLICATE_DB_NAME "lda-dupes"

#define MAIL_DELIVER_USER_CONTEXT(obj) \
	MODULE_CONTEXT_REQUIRE(obj, mail_deliver_user_module)
#define MAIL_DELIVER_STORAGE_CONTEXT(obj) \
	MODULE_CONTEXT_REQUIRE(obj, mail_deliver_storage_module)

struct event_category event_category_mail_delivery = {
	.name = "local-delivery",
};

struct mail_deliver_user {
	union mail_user_module_context module_ctx;
	struct mail_deliver_context *deliver_ctx;
	bool want_storage_id;
};

deliver_mail_func_t *deliver_mail = NULL;

struct mail_deliver_mailbox {
	union mailbox_module_context module_ctx;
};

struct mail_deliver_transaction {
	union mailbox_transaction_module_context module_ctx;

	struct mail_deliver_fields deliver_fields;
};

static const char *lda_log_wanted_headers[] = {
	"From", "Message-ID", "Subject",
	NULL
};
static enum mail_fetch_field lda_log_wanted_fetch_fields =
	MAIL_FETCH_PHYSICAL_SIZE | MAIL_FETCH_VIRTUAL_SIZE;
static MODULE_CONTEXT_DEFINE_INIT(mail_deliver_user_module,
				  &mail_user_module_register);
static MODULE_CONTEXT_DEFINE_INIT(mail_deliver_storage_module,
				  &mail_storage_module_register);

static struct message_address *
mail_deliver_get_message_address(struct mail *mail, const char *header)
{
	struct message_address *addr;
	const char *str;

	if (mail_get_first_header(mail, header, &str) <= 0)
		return NULL;
	addr = message_address_parse(pool_datastack_create(),
				     (const unsigned char *)str,
				     strlen(str), 1, 0);
	if (addr == NULL || addr->mailbox == NULL || addr->domain == NULL ||
	    *addr->mailbox == '\0' || *addr->domain == '\0')
		return NULL;
	return addr;
}

const struct smtp_address *
mail_deliver_get_address(struct mail *mail, const char *header)
{
	struct message_address *addr;
	struct smtp_address *smtp_addr;

	addr = mail_deliver_get_message_address(mail, header);
	if (addr == NULL ||
	    smtp_address_create_from_msg_temp(addr, &smtp_addr) < 0)
		return NULL;
	return smtp_addr;
}

static void
mail_deliver_update_event(struct mail_deliver_context *ctx)
{
	event_add_str(ctx->event, "message_id", ctx->fields.message_id);
	event_add_str(ctx->event, "message_subject", ctx->fields.subject);
	event_add_str(ctx->event, "message_from", ctx->fields.from);
	if (ctx->fields.psize != UOFF_T_MAX)
		event_add_int(ctx->event, "message_size", ctx->fields.psize);
	if (ctx->fields.vsize != UOFF_T_MAX)
		event_add_int(ctx->event, "message_vsize", ctx->fields.vsize);
}

static void
update_str_field(pool_t pool, const char **old_str, const char *new_str)
{
	if (new_str == NULL || new_str[0] == '\0')
		*old_str = NULL;
	else if (*old_str == NULL || strcmp(*old_str, new_str) != 0)
		*old_str = p_strdup(pool, new_str);
}

static void
mail_deliver_fields_update(struct mail_deliver_fields *fields, pool_t pool,
			   struct mail *mail)
{
	const char *message_id = NULL, *subject = NULL, *from_envelope = NULL;
	static struct message_address *from_addr;
	const char *from;

	if (fields->filled)
		return;
	fields->filled = TRUE;

	if (mail_get_message_id(mail, &message_id) > 0)
		message_id = str_sanitize(message_id, 200);
	update_str_field(pool, &fields->message_id, message_id);

	if (mail_get_first_header_utf8(mail, "Subject", &subject) > 0)
		subject = str_sanitize(subject, 80);
	update_str_field(pool, &fields->subject, subject);

	from_addr = mail_deliver_get_message_address(mail, "From");
	from = (from_addr == NULL ? NULL :
		t_strconcat(from_addr->mailbox, "@", from_addr->domain, NULL));
	update_str_field(pool, &fields->from, from);

	if (mail_get_special(mail, MAIL_FETCH_FROM_ENVELOPE, &from_envelope) > 0)
		from_envelope = str_sanitize(from_envelope, 80);
	update_str_field(pool, &fields->from_envelope, from_envelope);

	if (mail_get_physical_size(mail, &fields->psize) < 0)
		fields->psize = 0;
	if (mail_get_virtual_size(mail, &fields->vsize) < 0)
		fields->vsize = 0;
}

const struct var_expand_table *
mail_deliver_ctx_get_log_var_expand_table(struct mail_deliver_context *ctx,
					  const char *message)
{
	long long delivery_time_msecs;

	/* If a mail was saved/copied, the fields are already filled and the
	   following call is ignored. Otherwise, only the source mail exists. */
	mail_deliver_fields_update(&ctx->fields, ctx->pool, ctx->src_mail);
	/* This call finishes a mail delivery. With Sieve there may be multiple
	   mail deliveries. */
	ctx->fields.filled = FALSE;

	mail_deliver_update_event(ctx);

	io_loop_time_refresh();
	delivery_time_msecs = timeval_diff_msecs(&ioloop_timeval,
						 &ctx->delivery_time_started);

	const struct var_expand_table stack_tab[] = {
		{ .key = "message", .value = message },
		{ .key = "msgid", .value = ctx->fields.message_id != NULL ?
		       ctx->fields.message_id : "unspecified" },
		{ .key = "subject", .value = ctx->fields.subject },
		{ .key = "from", .value = ctx->fields.from },
		{ .key = "from_envelope", .value = ctx->fields.from_envelope },
		{ .key = "size", .value = dec2str(ctx->fields.psize) },
		{ .key = "vsize", .value = dec2str(ctx->fields.vsize) },
		{ .key = "delivery_time", .value = dec2str(delivery_time_msecs) },
		{ .key = "session_time", .value = dec2str(ctx->session_time_msecs) },
		{ .key = "to_envelope", .value = smtp_address_encode(ctx->rcpt_params.orcpt.addr) },
		{ .key = "storage_id", .value = ctx->fields.storage_id },
		VAR_EXPAND_TABLE_END
	};
	return p_memdup(unsafe_data_stack_pool, stack_tab, sizeof(stack_tab));
}

void mail_deliver_log(struct mail_deliver_context *ctx, const char *fmt, ...)
{
	va_list args;
	string_t *str;
	const char *msg, *error;

	if (*ctx->set->deliver_log_format == '\0')
		return;

	va_start(args, fmt);
	msg = t_strdup_vprintf(fmt, args);

	str = t_str_new(256);
	const struct var_expand_params params = {
		.table = mail_deliver_ctx_get_log_var_expand_table(ctx, msg),
		.event = ctx->event,
	};
	if (var_expand(str, ctx->set->deliver_log_format,
			   &params, &error) < 0) {
		e_error(ctx->event,
			"Failed to expand deliver_log_format=%s: %s",
			ctx->set->deliver_log_format, error);
	}

	e_info(ctx->event, "%s", str_c(str));
	va_end(args);
}

struct mail_deliver_session *mail_deliver_session_init(void)
{
	struct mail_deliver_session *session;
	pool_t pool;

	pool = pool_alloconly_create("mail deliver session", 1024);
	session = p_new(pool, struct mail_deliver_session, 1);
	session->pool = pool;
	return session;
}

void mail_deliver_session_deinit(struct mail_deliver_session **_session)
{
	struct mail_deliver_session *session = *_session;

	*_session = NULL;
	pool_unref(&session->pool);
}

int mail_deliver_save_open(struct mail_deliver_save_open_context *ctx,
			   const char *name, struct mailbox **box_r,
			   enum mail_error *error_r, const char **error_str_r)
{
	struct mailbox *box;
	enum mailbox_flags flags = MAILBOX_FLAG_POST_SESSION;

	*box_r = NULL;
	*error_r = MAIL_ERROR_NONE;
	*error_str_r = NULL;

	if (!uni_utf8_str_is_valid(name)) {
		*error_str_r = "Mailbox name not valid UTF-8";
		*error_r = MAIL_ERROR_PARAMS;
		return -1;
	}

	if (ctx->lda_mailbox_autocreate)
		flags |= MAILBOX_FLAG_AUTO_CREATE;
	if (ctx->lda_mailbox_autosubscribe)
		flags |= MAILBOX_FLAG_AUTO_SUBSCRIBE;
	*box_r = box = mailbox_alloc_for_user(ctx->user, name, flags);

	if (mailbox_open(box) == 0)
		return 0;
	*error_str_r = mailbox_get_last_internal_error(box, error_r);
	return -1;
}

static bool mail_deliver_check_duplicate(struct mail_deliver_session *session,
					 struct mailbox *box)
{
	struct mailbox_metadata metadata;
	const guid_128_t *guid;

	if (mailbox_get_metadata(box, MAILBOX_METADATA_GUID, &metadata) < 0) {
		/* just play it safe and assume a duplicate */
		return TRUE;
	}

	/* there shouldn't be all that many recipients,
	   so just do a linear search */
	if (!array_is_created(&session->inbox_guids))
		p_array_init(&session->inbox_guids, session->pool, 8);
	array_foreach(&session->inbox_guids, guid) {
		if (memcmp(metadata.guid, *guid, sizeof(metadata.guid)) == 0)
			return TRUE;
	}
	array_push_back(&session->inbox_guids, &metadata.guid);
	return FALSE;
}

void mail_deliver_deduplicate_guid_if_needed(struct mail_deliver_session *session,
					     struct mail_save_context *save_ctx)
{
	struct mailbox_transaction_context *trans =
		mailbox_save_get_transaction(save_ctx);
	struct mailbox *box = mailbox_transaction_get_mailbox(trans);
	guid_128_t guid;

	if (strcmp(mailbox_get_name(box), "INBOX") != 0)
		return;

	/* avoid storing duplicate GUIDs to delivered mails to INBOX. this
	   happens if mail is delivered to same user multiple times within a
	   session. the problem with this is that if GUIDs are used as POP3
	   UIDLs, some clients can't handle the duplicates well. */
	if (mail_deliver_check_duplicate(session, box)) {
		guid_128_generate(guid);
		mailbox_save_set_guid(save_ctx, guid_128_to_string(guid));
	}
}

void mail_deliver_init(struct mail_deliver_context *ctx,
		       struct mail_deliver_input *input)
{
	i_zero(ctx);
	ctx->set = input->set;
	ctx->smtp_set = input->smtp_set;

	ctx->session = input->session;
	ctx->pool = input->session->pool;
	pool_ref(ctx->pool);

	ctx->session_time_msecs = input->session_time_msecs;
	ctx->delivery_time_started = input->delivery_time_started;
	ctx->session_id = p_strdup(ctx->pool, input->session_id);
	ctx->src_mail = input->src_mail;
	ctx->save_dest_mail = input->save_dest_mail;

	ctx->mail_from = smtp_address_clone(ctx->pool, input->mail_from);
	smtp_params_mail_copy(ctx->pool, &ctx->mail_params,
			      &input->mail_params);
	ctx->rcpt_to = smtp_address_clone(ctx->pool, input->rcpt_to);
	smtp_params_rcpt_copy(ctx->pool, &ctx->rcpt_params,
			      &input->rcpt_params);
	ctx->rcpt_user = input->rcpt_user;
	ctx->rcpt_default_mailbox = p_strdup(ctx->pool,
					     input->rcpt_default_mailbox);

	ctx->event = event_create(input->event_parent);
	event_add_category(ctx->event, &event_category_mail_delivery);

	mail_deliver_fields_update(&ctx->fields, ctx->pool, ctx->src_mail);
	mail_deliver_update_event(ctx);

	if (ctx->rcpt_to != NULL) {
		event_add_str(ctx->event, "rcpt_to",
			      smtp_address_encode(ctx->rcpt_to));
	}
	smtp_params_rcpt_add_to_event(&ctx->rcpt_params, ctx->event);
}

void mail_deliver_deinit(struct mail_deliver_context *ctx)
{
	event_unref(&ctx->event);
	pool_unref(&ctx->pool);
}

static struct mail *
mail_deliver_open_mail(struct mailbox *box, uint32_t uid,
		       enum mail_fetch_field wanted_fields,
		       struct mailbox_transaction_context **trans_r)
{
	struct mailbox_transaction_context *t;
	struct mail *mail;

	*trans_r = NULL;

	if (mailbox_sync(box, MAILBOX_SYNC_FLAG_FAST) < 0)
		return NULL;

	t = mailbox_transaction_begin(box, 0, __func__);
	mail = mail_alloc(t, wanted_fields, NULL);

	if (!mail_set_uid(mail, uid)) {
		mail_free(&mail);
		mailbox_transaction_rollback(&t);
	}
	*trans_r = t;
	return mail;
}

int mail_deliver_save(struct mail_deliver_context *ctx, const char *mailbox,
		      enum mail_flags flags, const char *const *keywords,
		      struct mail_storage **storage_r)
{
	struct mail_deliver_save_open_context open_ctx;
	struct mailbox *box;
	enum mailbox_transaction_flags trans_flags;
	struct mailbox_transaction_context *t;
	struct mail_save_context *save_ctx;
	struct mailbox_header_lookup_ctx *headers_ctx;
	struct mail_keywords *kw;
	struct mail *dest_mail;
	enum mail_error error;
	const char *mailbox_name, *errstr, *guid;
	struct mail_transaction_commit_changes changes;
	bool default_save;
	int ret = 0;

	i_assert(ctx->dest_mail == NULL);

	default_save = strcmp(mailbox, ctx->rcpt_default_mailbox) == 0;
	if (default_save)
		ctx->tried_default_save = TRUE;

	i_zero(&open_ctx);
	open_ctx.user = ctx->rcpt_user;
	open_ctx.lda_mailbox_autocreate = ctx->set->lda_mailbox_autocreate;
	open_ctx.lda_mailbox_autosubscribe = ctx->set->lda_mailbox_autosubscribe;

	mailbox_name = str_sanitize(mailbox, 80);
	if (mail_deliver_save_open(&open_ctx, mailbox, &box,
				   &error, &errstr) < 0) {
		if (box != NULL) {
			*storage_r = mailbox_get_storage(box);
			mailbox_free(&box);
		}
		mail_deliver_log(ctx, "save failed to open mailbox %s: %s",
				 mailbox_name, errstr);
		return -1;
	}
	*storage_r = mailbox_get_storage(box);

	trans_flags = MAILBOX_TRANSACTION_FLAG_EXTERNAL;
	if (ctx->save_dest_mail)
		trans_flags |= MAILBOX_TRANSACTION_FLAG_ASSIGN_UIDS;
	t = mailbox_transaction_begin(box, trans_flags, __func__);

	kw = str_array_length(keywords) == 0 ? NULL :
		mailbox_keywords_create_valid(box, keywords);
	save_ctx = mailbox_save_alloc(t);
	if (ctx->mail_from != NULL) {
		mailbox_save_set_from_envelope(save_ctx,
			smtp_address_encode(ctx->mail_from));
	}
	mailbox_save_set_flags(save_ctx, flags, kw);

	headers_ctx = mailbox_header_lookup_init(box, lda_log_wanted_headers);
	dest_mail = mailbox_save_get_dest_mail(save_ctx);
	mail_add_temp_wanted_fields(dest_mail, lda_log_wanted_fetch_fields, NULL);
	mailbox_header_lookup_unref(&headers_ctx);
	mail_deliver_deduplicate_guid_if_needed(ctx->session, save_ctx);

	if (mailbox_save_using_mail(&save_ctx, ctx->src_mail) < 0)
		ret = -1;
	if (kw != NULL)
		mailbox_keywords_unref(&kw);

	if (ret < 0)
		mailbox_transaction_rollback(&t);
	else
		ret = mailbox_transaction_commit_get_changes(&t, &changes);

	if (ret == 0) {
		ctx->saved_mail = TRUE;
		if (ctx->save_dest_mail) {
			/* copying needs the message body. with maildir we also
			   need to get the GUID in case the message gets
			   expunged. get these early so the copying won't fail
			   later on. */
			i_assert(array_count(&changes.saved_uids) == 1);
			const struct seq_range *range =
				array_front(&changes.saved_uids);
			i_assert(range->seq1 == range->seq2);
			ctx->dest_mail = mail_deliver_open_mail(box, range->seq1,
				MAIL_FETCH_STREAM_BODY | MAIL_FETCH_GUID, &t);
			if (ctx->dest_mail == NULL) {
				i_assert(t == NULL);
			} else if (mail_get_special(ctx->dest_mail, MAIL_FETCH_GUID, &guid) < 0) {
				mail_free(&ctx->dest_mail);
				mailbox_transaction_rollback(&t);
			}
		}
		mail_deliver_log(ctx, "saved mail to %s", mailbox_name);
		pool_unref(&changes.pool);
	} else {
		mail_deliver_log(ctx, "save failed to %s: %s", mailbox_name,
			mail_storage_get_last_internal_error(*storage_r, &error));
	}

	if (ctx->dest_mail == NULL)
		mailbox_free(&box);
	return ret;
}

const struct smtp_address *
mail_deliver_get_return_address(struct mail_deliver_context *ctx)
{
	struct message_address *addr;
	struct smtp_address *smtp_addr;
	const char *path;
	int ret;

	if (!smtp_address_isnull(ctx->mail_from))
		return ctx->mail_from;

	if ((ret=mail_get_first_header(ctx->src_mail,
				       "Return-Path", &path)) <= 0) {
		if (ret < 0) {
			struct mailbox *box = ctx->src_mail->box;
			e_warning(ctx->event,
				  "Failed read return-path header: %s",
				  mailbox_get_last_internal_error(box, NULL));
		}
		return NULL;
	}
	if (message_address_parse_path(pool_datastack_create(),
				       (const unsigned char *)path,
				       strlen(path), &addr) < 0 ||
	    smtp_address_create_from_msg(ctx->pool, addr, &smtp_addr) < 0) {
		e_warning(ctx->event, "Failed to parse return-path header");
		return NULL;
	}
	return smtp_addr;
}

const char *mail_deliver_get_new_message_id(struct mail_deliver_context *ctx)
{
	static int count = 0;
	struct mail_user *user = ctx->rcpt_user;

	return t_strdup_printf("<dovecot-%s-%s-%d@%s>",
			       dec2str(ioloop_timeval.tv_sec),
			       dec2str(ioloop_timeval.tv_usec),
			       count++, user->set->hostname);
}

static bool mail_deliver_is_tempfailed(struct mail_deliver_context *ctx,
				       struct mail_storage *storage)
{
	enum mail_error error;

	if (ctx->tempfail_error != NULL)
		return TRUE;
	if (storage != NULL) {
		(void)mail_storage_get_last_error(storage, &error);
		return error == MAIL_ERROR_TEMP;
	}
	return FALSE;
}

static int
mail_do_deliver(struct mail_deliver_context *ctx,
		struct mail_storage **storage_r)
{
	int ret;

	*storage_r = NULL;
	if (deliver_mail == NULL)
		ret = -1;
	else {
		ctx->dup_db = mail_duplicate_db_init(ctx->rcpt_user,
						     DUPLICATE_DB_NAME);
		ret = deliver_mail(ctx, storage_r);
		mail_duplicate_db_deinit(&ctx->dup_db);
		if (ret > 0) {
			/* success. message may or may not have been saved. */
			ret = 0;
		} else if (ctx->saved_mail) {
			/* if message was saved, don't bounce it even though
			   the script failed later. */
			ret = 0;
		} else if (ret == 0) {
			/* message wasn't delivered yet. */
			ret = -1;
		} else if (ret < 0) {
			/* delivery failed. */
			if (mail_deliver_is_tempfailed(ctx, *storage_r))
				return -1;
			ret = -1;
		}
	}

	if (ret < 0 && !ctx->tried_default_save) {
		/* plugins didn't handle this. save into the default mailbox. */
		ret = mail_deliver_save(ctx, ctx->rcpt_default_mailbox, 0, NULL,
					storage_r);
		if (ret < 0 && mail_deliver_is_tempfailed(ctx, *storage_r))
			return -1;
	}
	if (ret < 0 && strcasecmp(ctx->rcpt_default_mailbox, "INBOX") != 0) {
		/* still didn't work. try once more to save it
		   to INBOX. */
		ret = mail_deliver_save(ctx, "INBOX", 0, NULL, storage_r);
	}
	return ret;
}

int mail_deliver(struct mail_deliver_context *ctx,
		 enum mail_deliver_error *error_code_r,
		 const char **error_r)
{
	struct mail_deliver_user *muser =
		MAIL_DELIVER_USER_CONTEXT(ctx->rcpt_user);
	struct event_passthrough *e;
	struct mail_storage *storage = NULL;
	enum mail_deliver_error error_code = MAIL_DELIVER_ERROR_NONE;
	const char *error = NULL;
	int ret;

	i_assert(muser->deliver_ctx == NULL);

	mail_deliver_fields_update(&ctx->fields, ctx->pool, ctx->src_mail);
	mail_deliver_update_event(ctx);

	muser->want_storage_id = ctx->set->parsed_want_storage_id;

	muser->deliver_ctx = ctx;

	e = event_create_passthrough(ctx->event)->
		set_name("mail_delivery_started");
	e_debug(e->event(), "Local delivery started");

	ret = mail_do_deliver(ctx, &storage);

	if (ret >= 0)
		i_assert(ret == 0); /* ret > 0 has no defined meaning */
	else if (ctx->tempfail_error != NULL) {
		error = ctx->tempfail_error;
		error_code = MAIL_DELIVER_ERROR_TEMPORARY;
	} else if (storage != NULL) {
		enum mail_error mail_error;

		error = mail_storage_get_last_error(storage, &mail_error);
		if (mail_error == MAIL_ERROR_NOQUOTA) {
			error_code = MAIL_DELIVER_ERROR_NOQUOTA;
		} else {
			error_code = MAIL_DELIVER_ERROR_TEMPORARY;
		}
	} else {
		/* This shouldn't happen */
		e_error(ctx->event, "BUG: Saving failed to unknown storage");
		error = "Temporary internal error";
		error_code = MAIL_DELIVER_ERROR_INTERNAL;
	}

	e = event_create_passthrough(ctx->event)->
		set_name("mail_delivery_finished");
	if (ret == 0) {
		e_debug(e->event(), "Local delivery finished successfully");
	} else {
		e->add_str("error", error);
		e_debug(e->event(), "Local delivery failed: %s", error);
	}

	muser->deliver_ctx = NULL;

	*error_code_r = error_code;
	*error_r = error;
	return ret;
}

deliver_mail_func_t *mail_deliver_hook_set(deliver_mail_func_t *new_hook)
{
	deliver_mail_func_t *old_hook = deliver_mail;

	deliver_mail = new_hook;
	return old_hook;
}

static int mail_deliver_save_finish(struct mail_save_context *ctx)
{
	struct mailbox *box = ctx->transaction->box;
	struct mail_deliver_mailbox *mbox = MAIL_DELIVER_STORAGE_CONTEXT(box);
	struct mail_deliver_user *muser =
		MAIL_DELIVER_USER_CONTEXT(box->storage->user);
	struct mail_deliver_transaction *dt =
		MAIL_DELIVER_STORAGE_CONTEXT(ctx->transaction);

	if (mbox->module_ctx.super.save_finish(ctx) < 0)
		return -1;

	/* initialize most of the fields from dest_mail */
	mail_deliver_fields_update(&dt->deliver_fields,
				   muser->deliver_ctx->pool,
				   ctx->dest_mail);
	return 0;
}

static int mail_deliver_copy(struct mail_save_context *ctx, struct mail *mail)
{
	struct mailbox *box = ctx->transaction->box;
	struct mail_deliver_mailbox *mbox = MAIL_DELIVER_STORAGE_CONTEXT(box);
	struct mail_deliver_user *muser =
		MAIL_DELIVER_USER_CONTEXT(box->storage->user);
	struct mail_deliver_transaction *dt =
		MAIL_DELIVER_STORAGE_CONTEXT(ctx->transaction);

	if (mbox->module_ctx.super.copy(ctx, mail) < 0)
		return -1;

	/* initialize most of the fields from dest_mail */
	mail_deliver_fields_update(&dt->deliver_fields,
				   muser->deliver_ctx->pool,
				   ctx->dest_mail);
	return 0;
}

static void
mail_deliver_fields_update_post_commit(struct mailbox *orig_box, uint32_t uid)
{
	struct mail_deliver_user *muser =
		MAIL_DELIVER_USER_CONTEXT(orig_box->storage->user);
	struct mailbox *box;
	struct mailbox_transaction_context *t;
	struct mail *mail;
	const char *storage_id;

	if (!muser->want_storage_id)
		return;

	/* getting storage_id requires a whole new mailbox view that is
	   synced, so it'll contain the newly written mail. this is racy, so
	   it's possible another process has already deleted the mail. */
	box = mailbox_alloc(orig_box->list, orig_box->vname, 0);
	mail = mail_deliver_open_mail(box, uid, MAIL_FETCH_STORAGE_ID, &t);
	if (mail != NULL) {
		if (mail_get_special(mail, MAIL_FETCH_STORAGE_ID, &storage_id) < 0 ||
		    storage_id[0] == '\0')
			storage_id = NULL;
		muser->deliver_ctx->fields.storage_id =
			p_strdup(muser->deliver_ctx->pool, storage_id);
		mail_free(&mail);
		(void)mailbox_transaction_commit(&t);
	} else {
		muser->deliver_ctx->fields.storage_id = NULL;
	}
	mailbox_free(&box);
}

static struct mailbox_transaction_context *
mail_deliver_transaction_begin(struct mailbox *box,
			       enum mailbox_transaction_flags flags,
			       const char *reason)
{
	struct mail_deliver_mailbox *mbox = MAIL_DELIVER_STORAGE_CONTEXT(box);
	struct mail_deliver_user *muser =
		MAIL_DELIVER_USER_CONTEXT(box->storage->user);
	struct mailbox_transaction_context *t;
	struct mail_deliver_transaction *dt;

	i_assert(muser->deliver_ctx != NULL);

	t = mbox->module_ctx.super.transaction_begin(box, flags, reason);
	dt = p_new(muser->deliver_ctx->pool, struct mail_deliver_transaction, 1);

	MODULE_CONTEXT_SET(t, mail_deliver_storage_module, dt);
	return t;
}

static int
mail_deliver_transaction_commit(struct mailbox_transaction_context *ctx,
				struct mail_transaction_commit_changes *changes_r)
{
	struct mailbox *box = ctx->box;
	struct mail_deliver_mailbox *mbox = MAIL_DELIVER_STORAGE_CONTEXT(box);
	struct mail_deliver_transaction *dt = MAIL_DELIVER_STORAGE_CONTEXT(ctx);
	struct mail_deliver_user *muser =
		MAIL_DELIVER_USER_CONTEXT(box->storage->user);

	i_assert(muser->deliver_ctx != NULL);

	/* sieve creates multiple transactions, saves the mails and
	   then commits all of them at the end. we'll need to keep
	   switching the deliver_ctx->fields for each commit.

	   we also want to do this only for commits generated by sieve.
	   other plugins or storage backends may be creating transactions as
	   well, which we need to ignore. */
	if ((box->flags & MAILBOX_FLAG_POST_SESSION) != 0) {
		muser->deliver_ctx->fields = dt->deliver_fields;
		mail_deliver_update_event(muser->deliver_ctx);
	}

	if (mbox->module_ctx.super.transaction_commit(ctx, changes_r) < 0)
		return -1;

	if (array_count(&changes_r->saved_uids) > 0) {
		const struct seq_range *range =
			array_front(&changes_r->saved_uids);

		mail_deliver_fields_update_post_commit(box, range->seq1);
	}
	return 0;
}

static void mail_deliver_mail_user_created(struct mail_user *user)
{
	struct mail_deliver_user *muser;

	muser = p_new(user->pool, struct mail_deliver_user, 1);
	MODULE_CONTEXT_SET(user, mail_deliver_user_module, muser);
}

static void mail_deliver_mailbox_allocated(struct mailbox *box)
{
	struct mailbox_vfuncs *v = box->vlast;
	struct mail_deliver_mailbox *mbox;
	struct mail_deliver_user *muser =
		MAIL_DELIVER_USER_CONTEXT(box->storage->user);

	/* we are doing something other than lda/lmtp delivery
	   and should not be involved */
	if (muser->deliver_ctx == NULL)
		return;

	mbox = p_new(box->pool, struct mail_deliver_mailbox, 1);
	mbox->module_ctx.super = *v;
	box->vlast = &mbox->module_ctx.super;
	v->save_finish = mail_deliver_save_finish;
	v->copy = mail_deliver_copy;
	v->transaction_begin = mail_deliver_transaction_begin;
	v->transaction_commit = mail_deliver_transaction_commit;

	MODULE_CONTEXT_SET(box, mail_deliver_storage_module, mbox);
 }

static struct mail_storage_hooks mail_deliver_hooks = {
	.mail_user_created = mail_deliver_mail_user_created,
	.mailbox_allocated = mail_deliver_mailbox_allocated
};

void mail_deliver_hooks_init(void)
{
	mail_storage_hooks_add_internal(&mail_deliver_hooks);
}
