view src/lib-lda/mail-deliver.c @ 21554:7a2f1c800c8c

lib-lda: Code cleanup - remove unnecessary dest_mail check
author Timo Sirainen <timo.sirainen@dovecot.fi>
date Tue, 14 Feb 2017 20:23:16 +0200
parents 4a2884480ddc
children a66e16598d6e
line wrap: on
line source

/* Copyright (c) 2005-2017 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 "lda-settings.h"
#include "mail-storage.h"
#include "mail-namespace.h"
#include "mail-storage-private.h"
#include "duplicate.h"
#include "mail-deliver.h"

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

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_cache {
	bool filled;

	const char *message_id;
	const char *subject;
	const char *from;
	const char *from_envelope;
	const char *storage_id;

	uoff_t psize, vsize;
};

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);

const char *mail_deliver_get_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, FALSE);
	return addr == NULL || addr->mailbox == NULL || addr->domain == NULL ||
		*addr->mailbox == '\0' || *addr->domain == '\0' ?
		NULL : t_strconcat(addr->mailbox, "@", addr->domain, NULL);
}

static void update_cache(struct mail_deliver_context *ctx,
			 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(ctx->pool, new_str);
}

static void
mail_deliver_log_update_cache(struct mail_deliver_context *ctx,
			      struct mail *mail)
{
	const char *message_id = NULL, *subject = NULL, *from_envelope = NULL;
	const char *from;

	if (ctx->cache == NULL)
		ctx->cache = p_new(ctx->pool, struct mail_deliver_cache, 1);
	else if (ctx->cache->filled)
		return;
	ctx->cache->filled = TRUE;

	if (mail_get_first_header(mail, "Message-ID", &message_id) > 0)
		message_id = str_sanitize(message_id, 200);
	update_cache(ctx, &ctx->cache->message_id, message_id);

	if (mail_get_first_header_utf8(mail, "Subject", &subject) > 0)
		subject = str_sanitize(subject, 80);
	update_cache(ctx, &ctx->cache->subject, subject);

	from = str_sanitize(mail_deliver_get_address(mail, "From"), 80);
	update_cache(ctx, &ctx->cache->from, from);

	if (mail_get_special(mail, MAIL_FETCH_FROM_ENVELOPE, &from_envelope) > 0)
		from_envelope = str_sanitize(from_envelope, 80);
	update_cache(ctx, &ctx->cache->from_envelope, from_envelope);

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

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

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

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

	const struct var_expand_table stack_tab[] = {
		{ '$', message, NULL },
		{ 'm', ctx->cache->message_id != NULL ?
		       ctx->cache->message_id : "unspecified", "msgid" },
		{ 's', ctx->cache->subject, "subject" },
		{ 'f', ctx->cache->from, "from" },
		{ 'e', ctx->cache->from_envelope, "from_envelope" },
		{ 'p', dec2str(ctx->cache->psize), "size" },
		{ 'w', dec2str(ctx->cache->vsize), "vsize" },
		{ '\0', dec2str(delivery_time_msecs), "delivery_time" },
		{ '\0', dec2str(ctx->session_time_msecs), "session_time" },
		{ '\0', ctx->dest_addr, "to_envelope" },
		{ '\0', ctx->cache->storage_id, "storage_id" },
		{ '\0', NULL, NULL }
	};
	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 struct var_expand_table *tab;
	const char *msg;

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

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

	str = t_str_new(256);
	tab = mail_deliver_ctx_get_log_var_expand_table(ctx, msg);
	var_expand(str, ctx->set->deliver_log_format, tab);

	i_info("%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 mail_namespace *ns;
	struct mailbox *box;
	enum mailbox_flags flags =
		MAILBOX_FLAG_SAVEONLY | 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;
	}

	ns = mail_namespace_find(ctx->user->namespaces, name);
	if (strcmp(name, ns->prefix) == 0 &&
	    (ns->flags & NAMESPACE_FLAG_INBOX_USER) != 0) {
		/* delivering to a namespace prefix means we actually want to
		   deliver to the INBOX instead */
		name = "INBOX";
		ns = mail_namespace_find_inbox(ctx->user->namespaces);
	}

	if (strcasecmp(name, "INBOX") == 0) {
		/* deliveries to INBOX must always succeed,
		   regardless of ACLs */
		flags |= MAILBOX_FLAG_IGNORE_ACLS;
	}

	*box_r = box = mailbox_alloc(ns->list, name, flags);
	if (mailbox_open(box) == 0)
		return 0;

	*error_str_r = mailbox_get_last_error(box, error_r);
	if (!ctx->lda_mailbox_autocreate || *error_r != MAIL_ERROR_NOTFOUND)
		return -1;

	/* try creating it. */
	if (mailbox_create(box, NULL, FALSE) < 0) {
		*error_str_r = mailbox_get_last_error(box, error_r);
		if (*error_r != MAIL_ERROR_EXISTS)
			return -1;
		/* someone else just created it */
	}
	if (ctx->lda_mailbox_autosubscribe) {
		/* (try to) subscribe to it */
		(void)mailbox_set_subscribed(box, TRUE);
	}

	/* and try opening again */
	if (mailbox_open(box) < 0) {
		*error_str_r = mailbox_get_last_error(box, error_r);
		return -1;
	}
	return 0;
}

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_append(&session->inbox_guids, &metadata.guid, 1);
	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));
	}
}

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);
	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->dest_mailbox_name) == 0;
	if (default_save)
		ctx->tried_default_save = TRUE;

	i_zero(&open_ctx);
	open_ctx.user = ctx->dest_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);

	kw = str_array_length(keywords) == 0 ? NULL :
		mailbox_keywords_create_valid(box, keywords);
	save_ctx = mailbox_save_alloc(t);
	if (ctx->src_envelope_sender != NULL)
		mailbox_save_set_from_envelope(save_ctx, ctx->src_envelope_sender);
	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 */
			i_assert(array_count(&changes.saved_uids) == 1);
			const struct seq_range *range =
				array_idx(&changes.saved_uids, 0);
			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 (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_error(*storage_r, &error));
	}

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

const char *mail_deliver_get_return_address(struct mail_deliver_context *ctx)
{
	if (ctx->src_envelope_sender != NULL)
		return ctx->src_envelope_sender;

	return mail_deliver_get_address(ctx->src_mail, "Return-Path");
}

const char *mail_deliver_get_new_message_id(struct mail_deliver_context *ctx)
{
	static int count = 0;

	return t_strdup_printf("<dovecot-%s-%s-%d@%s>",
			       dec2str(ioloop_timeval.tv_sec),
			       dec2str(ioloop_timeval.tv_usec),
			       count++, ctx->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;
}

int mail_deliver(struct mail_deliver_context *ctx,
		 struct mail_storage **storage_r)
{
	struct mail_deliver_user *muser =
		MAIL_DELIVER_USER_CONTEXT(ctx->dest_user);
	int ret;

	i_assert(muser->deliver_ctx == NULL);

	muser->want_storage_id =
		var_has_key(ctx->set->deliver_log_format, '\0', "storage_id");

	muser->deliver_ctx = ctx;
	*storage_r = NULL;
	if (deliver_mail == NULL)
		ret = -1;
	else {
		ctx->dup_ctx = duplicate_init(ctx->dest_user);
		if (deliver_mail(ctx, storage_r) <= 0) {
			/* if message was saved, don't bounce it even though
			   the script failed later. */
			ret = ctx->saved_mail ? 0 : -1;
		} else {
			/* success. message may or may not have been saved. */
			ret = 0;
		}
		duplicate_deinit(&ctx->dup_ctx);
		if (ret < 0 && mail_deliver_is_tempfailed(ctx, *storage_r)) {
			muser->deliver_ctx = NULL;
			return -1;
		}
	}

	if (ret < 0 && !ctx->tried_default_save) {
		/* plugins didn't handle this. save into the default mailbox. */
		ret = mail_deliver_save(ctx, ctx->dest_mailbox_name, 0, NULL,
					storage_r);
		if (ret < 0 && mail_deliver_is_tempfailed(ctx, *storage_r)) {
			muser->deliver_ctx = NULL;
			return -1;
		}
	}
	if (ret < 0 && strcasecmp(ctx->dest_mailbox_name, "INBOX") != 0) {
		/* still didn't work. try once more to save it
		   to INBOX. */
		ret = mail_deliver_save(ctx, "INBOX", 0, NULL, storage_r);
	}
	muser->deliver_ctx = NULL;
	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;
	union mailbox_module_context *mbox = MAIL_DELIVER_STORAGE_CONTEXT(box);
	struct mail_deliver_user *muser =
		MAIL_DELIVER_USER_CONTEXT(box->storage->user);

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

	/* initialize most of the fields from dest_mail */
	mail_deliver_log_update_cache(muser->deliver_ctx, ctx->dest_mail);
	return 0;
}

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

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

	/* initialize most of the fields from dest_mail */
	mail_deliver_log_update_cache(muser->deliver_ctx, ctx->dest_mail);
	return 0;
}

static void
mail_deliver_cache_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->cache->storage_id =
			p_strdup(muser->deliver_ctx->pool, storage_id);
		mail_free(&mail);
		(void)mailbox_transaction_commit(&t);
	} else {
		muser->deliver_ctx->cache->storage_id = NULL;
	}
	mailbox_free(&box);
}

static int
mail_deliver_transaction_commit(struct mailbox_transaction_context *ctx,
				struct mail_transaction_commit_changes *changes_r)
{
	struct mailbox *box = ctx->box;
	union mailbox_module_context *mbox = MAIL_DELIVER_STORAGE_CONTEXT(box);

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

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

		mail_deliver_cache_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;
	union mailbox_module_context *mbox;

	mbox = p_new(box->pool, union mailbox_module_context, 1);
	mbox->super = *v;
	box->vlast = &mbox->super;
	v->save_finish = mail_deliver_save_finish;
	v->copy = mail_deliver_copy;
	v->transaction_commit = mail_deliver_transaction_commit;

	MODULE_CONTEXT_SET_SELF(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);
}