/* Copyright (c) 2013-2016 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "array.h"
#include "str.h"
#include "safe-mkstemp.h"
#include "mail-user.h"
#include "mail-storage-private.h"
#include "istream.h"
#include "istream-seekable.h"
#include "istream-ext-filter.h"
#include "ostream-ext-filter.h"
#include "mail-filter-plugin.h"

/* After buffer grows larger than this, create a temporary file to /tmp
   where to read the mail. */
#define MAIL_MAX_MEMORY_BUFFER (1024*128)

#define MAIL_FILTER_MAIL_CONTEXT(obj) \
	MODULE_CONTEXT(obj, mail_filter_mail_module)
#define MAIL_FILTER_CONTEXT(obj) \
	MODULE_CONTEXT(obj, mail_filter_storage_module)
#define MAIL_FILTER_USER_CONTEXT(obj) \
	MODULE_CONTEXT(obj, mail_filter_user_module)

struct mail_filter_user {
	union mail_user_module_context module_ctx;

	const char *socket_path, *args;
	const char *out_socket_path, *out_args;
};

const char *mail_filter_plugin_version = DOVECOT_ABI_VERSION;

static MODULE_CONTEXT_DEFINE_INIT(mail_filter_user_module,
				  &mail_user_module_register);
static MODULE_CONTEXT_DEFINE_INIT(mail_filter_storage_module,
				  &mail_storage_module_register);
static MODULE_CONTEXT_DEFINE_INIT(mail_filter_mail_module,
				  &mail_module_register);

static int
mail_filter_mail_save_begin(struct mail_save_context *ctx,
			    struct istream *input)
{
	struct mailbox *box = ctx->transaction->box;
	struct mail_filter_user *muser =
		MAIL_FILTER_USER_CONTEXT(box->storage->user);
	union mailbox_module_context *mbox = MAIL_FILTER_CONTEXT(box);
	struct ostream *output;

	if (mbox->super.save_begin(ctx, input) < 0)
		return -1;

	output = o_stream_create_ext_filter(ctx->data.output,
					    muser->out_socket_path,
					    muser->out_args);
	ctx->data.output = output;
	return 0;
}

static int seekable_fd_callback(const char **path_r, void *context)
{
	struct mail_user *user = context;
	string_t *path;
	int fd;

	path = t_str_new(128);
	mail_user_set_get_temp_prefix(path, user->set);
	fd = safe_mkstemp(path, 0600, (uid_t)-1, (gid_t)-1);
	if (fd == -1) {
		i_error("safe_mkstemp(%s) failed: %m", str_c(path));
		return -1;
	}

	/* we just want the fd, unlink it */
	if (i_unlink(str_c(path)) < 0) {
		/* shouldn't happen.. */
		i_close_fd(&fd);
		return -1;
	}

	*path_r = str_c(path);
	return fd;
}

static int
mail_filter_istream_opened(struct mail *_mail, struct istream **stream)
{
	struct mail_private *mail = (struct mail_private *)_mail;
	struct mail_user *user = _mail->box->storage->user;
	struct mail_filter_user *muser = MAIL_FILTER_USER_CONTEXT(user);
	union mail_module_context *mmail = MAIL_FILTER_MAIL_CONTEXT(mail);
	struct istream *input, *inputs[2];

	input = *stream;
	*stream = i_stream_create_ext_filter(input, muser->socket_path,
					     muser->args);
	i_stream_unref(&input);

	inputs[0] = *stream;
	inputs[1] = NULL;
	*stream = i_stream_create_seekable(inputs, MAIL_MAX_MEMORY_BUFFER,
					   seekable_fd_callback, user);
	i_stream_unref(&inputs[0]);

	return mmail->super.istream_opened(_mail, stream);
}

static void mail_filter_mailbox_allocated(struct mailbox *box)
{
	struct mailbox_vfuncs *v = box->vlast;
	struct mail_filter_user *muser =
		MAIL_FILTER_USER_CONTEXT(box->storage->user);
	union mailbox_module_context *mbox;
	enum mail_storage_class_flags class_flags = box->storage->class_flags;

	mbox = p_new(box->pool, union mailbox_module_context, 1);
	mbox->super = *v;
	box->vlast = &mbox->super;

	MODULE_CONTEXT_SET_SELF(box, mail_filter_storage_module, mbox);

	if ((class_flags & MAIL_STORAGE_CLASS_FLAG_OPEN_STREAMS) == 0 &&
	    (class_flags & MAIL_STORAGE_CLASS_FLAG_BINARY_DATA) != 0 &&
	    muser->out_socket_path != NULL)
		v->save_begin = mail_filter_mail_save_begin;
}

static void mail_filter_mail_allocated(struct mail *_mail)
{
	struct mail_private *mail = (struct mail_private *)_mail;
	struct mail_filter_user *muser =
		MAIL_FILTER_USER_CONTEXT(_mail->box->storage->user);
	struct mail_vfuncs *v = mail->vlast;
	union mail_module_context *mmail;

	mmail = p_new(mail->pool, union mail_module_context, 1);
	mmail->super = *v;
	mail->vlast = &mmail->super;

	if (muser->socket_path != NULL)
		v->istream_opened = mail_filter_istream_opened;
	MODULE_CONTEXT_SET_SELF(mail, mail_filter_mail_module, mmail);
}

static void
mail_filter_parse_setting(struct mail_user *user, const char *name,
			  const char **socket_path_r, const char **args_r)
{
	const char *value, *p;

	value = mail_user_plugin_getenv(user, name);
	if (value == NULL)
		return;

	p = strchr(value, ' ');
	if (p == NULL) {
		*socket_path_r = p_strdup(user->pool, value);
		*args_r = "";
	} else {
		*socket_path_r = p_strdup_until(user->pool, value, p);
		*args_r = p_strdup(user->pool, p + 1);
	}
	if (**socket_path_r != '/') {
		/* relative to base_dir */
		*socket_path_r = p_strdup_printf(user->pool, "%s/%s",
			user->set->base_dir, *socket_path_r);
	}
	if (user->mail_debug) {
		i_debug("mail_filter: Filtering %s via socket %s",
			name, *socket_path_r);
	}
}

static void mail_filter_mail_user_created(struct mail_user *user)
{
	struct mail_user_vfuncs *v = user->vlast;
	struct mail_filter_user *muser;

	muser = p_new(user->pool, struct mail_filter_user, 1);
	muser->module_ctx.super = *v;
	user->vlast = &muser->module_ctx.super;

	mail_filter_parse_setting(user, "mail_filter",
				  &muser->socket_path, &muser->args);
	mail_filter_parse_setting(user, "mail_filter_out",
				  &muser->out_socket_path, &muser->out_args);
	if (user->mail_debug && muser->socket_path == NULL &&
	    muser->out_socket_path == NULL) {
		i_debug("mail_filter and mail_filter_out settings missing, "
			"ignoring mail_filter plugin");
	}

	MODULE_CONTEXT_SET(user, mail_filter_user_module, muser);
}

static struct mail_storage_hooks mail_filter_mail_storage_hooks = {
	.mail_user_created = mail_filter_mail_user_created,
	.mailbox_allocated = mail_filter_mailbox_allocated,
	.mail_allocated = mail_filter_mail_allocated
};

void mail_filter_plugin_init(struct module *module)
{
	mail_storage_hooks_add(module, &mail_filter_mail_storage_hooks);
}

void mail_filter_plugin_deinit(void)
{
	mail_storage_hooks_remove(&mail_filter_mail_storage_hooks);
}
