/* Copyright (c) 2002-2018 Pigeonhole authors, see the included COPYING file
 */

#include "lib.h"
#include "str.h"
#include "istream.h"
#include "rfc822-parser.h"
#include "message-parser.h"
#include "message-decoder.h"
#include "mail-storage.h"

#include "sieve-common.h"
#include "sieve-code.h"
#include "sieve-message.h"
#include "sieve-extensions.h"
#include "sieve-commands.h"
#include "sieve-actions.h"
#include "sieve-validator.h"
#include "sieve-generator.h"
#include "sieve-interpreter.h"
#include "sieve-dump.h"
#include "sieve-result.h"

#include "ext-notify-common.h"

#include <ctype.h>

/*
 * Importance argument
 */

static bool
tag_importance_validate(struct sieve_validator *valdtr,
			struct sieve_ast_argument **arg,
			struct sieve_command *cmd);

static const struct sieve_argument_def importance_low_tag = {
	.identifier = "low",
	.validate = tag_importance_validate,
};

static const struct sieve_argument_def importance_normal_tag = {
	.identifier = "normal",
	.validate = tag_importance_validate,
};

static const struct sieve_argument_def importance_high_tag = {
	.identifier = "high",
	.validate = tag_importance_validate,
};

static bool
tag_importance_validate(struct sieve_validator *valdtr ATTR_UNUSED,
			struct sieve_ast_argument **arg,
			struct sieve_command *cmd ATTR_UNUSED)
{
	struct sieve_ast_argument *tag = *arg;

	if (sieve_argument_is(tag, importance_low_tag))
		sieve_ast_argument_number_substitute(tag, 3);
	else if (sieve_argument_is(tag, importance_normal_tag))
		sieve_ast_argument_number_substitute(tag, 2);
	else
		sieve_ast_argument_number_substitute(tag, 1);

	tag->argument = sieve_argument_create(tag->ast, &number_argument,
					      tag->argument->ext,
					      tag->argument->id_code);

	/* Skip parameter */
	*arg = sieve_ast_argument_next(*arg);

	return TRUE;
}

void ext_notify_register_importance_tags(
	struct sieve_validator *valdtr,
	struct sieve_command_registration *cmd_reg,
	const struct sieve_extension *ext, unsigned int id_code)
{
	sieve_validator_register_tag(valdtr, cmd_reg, ext,
				     &importance_low_tag, id_code);
	sieve_validator_register_tag(valdtr, cmd_reg, ext,
				     &importance_normal_tag, id_code);
	sieve_validator_register_tag(valdtr, cmd_reg, ext,
				     &importance_high_tag, id_code);
}

/*
 * Body extraction
 */

/* FIXME: overlaps somewhat with body extension */

struct ext_notify_message_context {
	pool_t pool;
	buffer_t *body_text;
};

static struct ext_notify_message_context *
ext_notify_get_message_context(const struct sieve_extension *this_ext,
			       struct sieve_message_context *msgctx)
{
	struct ext_notify_message_context *ctx;

	/* Get message context (contains cached message body information) */
	ctx = (struct ext_notify_message_context *)
		sieve_message_context_extension_get(msgctx, this_ext);

	/* Create it if it does not exist already */
	if (ctx == NULL) {
		pool_t pool = sieve_message_context_pool(msgctx);
		ctx = p_new(pool, struct ext_notify_message_context, 1);
		ctx->pool = pool;
		ctx->body_text = NULL;

		/* Register context */
		sieve_message_context_extension_set(msgctx, this_ext,
						    (void *)ctx);
	}

	return ctx;
}

static bool _is_text_content(const struct message_header_line *hdr)
{
	struct rfc822_parser_context parser;
	string_t *content_type;
	const char *data;

	/* Initialize parsing */
	rfc822_parser_init(&parser, hdr->full_value, hdr->full_value_len, NULL);
	(void)rfc822_skip_lwsp(&parser);

	/* Parse content type */
	content_type = t_str_new(64);
	if (rfc822_parse_content_type(&parser, content_type) < 0)
		return FALSE;

	/* Content-type value must end here, otherwise it is invalid after all
	 */
	(void)rfc822_skip_lwsp(&parser);
	if (parser.data != parser.end && *parser.data != ';')
		return FALSE;

	/* Success */
	data = str_c(content_type);
	if (str_begins(data, "text/"))
		return TRUE;
	return FALSE;
}

static int
cmd_notify_extract_body_text(const struct sieve_runtime_env *renv,
			     const char **body_text_r, size_t *body_size_r)
{
	const struct sieve_execute_env *eenv = renv->exec_env;
	const struct sieve_extension *this_ext = renv->oprtn->ext;
	struct ext_notify_message_context *mctx;
	struct mail *mail = eenv->msgdata->mail;
	struct message_parser_ctx *parser;
	struct message_decoder_context *decoder;
	struct message_part *parts;
	struct message_block block, decoded;
	struct istream *input;
	bool is_text, save_body;
	int ret = 1;

	*body_text_r = NULL;
	*body_size_r = 0;

	/* Return cached result if available */
	mctx = ext_notify_get_message_context(this_ext, renv->msgctx);
	if (mctx->body_text != NULL) {
		*body_text_r = (const char *)
			buffer_get_data(mctx->body_text, body_size_r);
		return SIEVE_EXEC_OK;
	}

	/* Create buffer */
	mctx->body_text = buffer_create_dynamic(mctx->pool, 1024*64);

	/* Get the message stream */
	if (mail_get_stream(mail, NULL, NULL, &input) < 0) {
		return sieve_runtime_mail_error(renv, mail, "notify action: "
						"failed to read input message");
	}

	/* Initialize body decoder */
	decoder = message_decoder_init(NULL, 0);

	struct message_parser_settings mparser_set = {
		.hdr_flags = 0,
		.flags = 0,
	};
	parser = message_parser_init(mctx->pool, input, &mparser_set);
	is_text = TRUE;
	save_body = FALSE;
	while ((ret = message_parser_parse_next_block(parser, &block)) > 0) {
		if (block.hdr != NULL || block.size == 0) {
			/* Decode block */
			(void)message_decoder_decode_next_block(decoder, &block,
								&decoded);

			/* Check for end of headers */
			if (block.hdr == NULL) {
				save_body = is_text;
				continue;
			}

			/* We're interested of only Content-Type: header */
			if (strcasecmp(block.hdr->name, "Content-Type") != 0)
				continue;

			/* Header can have folding whitespace. Acquire the full
			   value before continuing */
			if (block.hdr->continues) {
				block.hdr->use_full_value = TRUE;
				continue;
			}

			/* Is it a text part? */
			T_BEGIN {
				is_text = _is_text_content(block.hdr);
			} T_END;

			continue;
		}

		/* Read text body */
		if (save_body) {
			(void)message_decoder_decode_next_block(decoder, &block,
								&decoded);
			buffer_append(mctx->body_text, decoded.data,
				      decoded.size);
			is_text = TRUE;
		}
	}

	/* Cleanup */
	(void)message_parser_deinit(&parser, &parts);
	message_decoder_deinit(&decoder);

	if (ret < 0 && input->stream_errno != 0) {
		sieve_runtime_critical(renv, NULL, "notify action: "
				       "failed to read input message",
				       "notify action: read(%s) failed: %s",
				       i_stream_get_name(input),
				       i_stream_get_error(input));
		return SIEVE_EXEC_TEMP_FAILURE;
	}

	/* Return status */
	*body_text_r = (const char *)buffer_get_data(mctx->body_text,
						     body_size_r);
	return SIEVE_EXEC_OK;
}

int ext_notify_construct_message(const struct sieve_runtime_env *renv,
				 const char *msg_format,
				 string_t *out_msg)
{
	const struct sieve_execute_env *eenv = renv->exec_env;
	const struct sieve_message_data *msgdata = eenv->msgdata;
	struct sieve_message_context *msgctx = renv->msgctx;
	const struct smtp_address *return_path =
		sieve_message_get_sender(msgctx);
	const char *p;
	int ret;

	if (msg_format == NULL)
		msg_format = "$from$: $subject$";

	/* Scan message for substitutions */
	p = msg_format;
	while (*p != '\0') {
		const char *header;

		if (strncasecmp(p, "$from$", 6) == 0) {
			p += 6;

			/* Fetch sender from original message */
			if ((ret = mail_get_first_header_utf8(
				msgdata->mail, "from", &header)) < 0) {
				return sieve_runtime_mail_error(
					renv, msgdata->mail,
					"failed to read header field `from'");
			}
			if (ret > 0)
				str_append(out_msg, header);
		} else if (strncasecmp(p, "$env-from$", 10) == 0) {
			p += 10;

			if (return_path != NULL)
				smtp_address_write(out_msg, return_path);
		} else if (strncasecmp(p, "$subject$", 9) == 0) {
			p += 9;

			/* Fetch sender from oriinal message */
			if ((ret = mail_get_first_header_utf8(
				msgdata->mail, "subject", &header)) < 0) {
				return sieve_runtime_mail_error(
					renv, msgdata->mail,
					"failed to read header field `subject'");
			}
			if (ret > 0)
				 str_append(out_msg, header);
		} else if (strncasecmp(p, "$text", 5) == 0 &&
			   (p[5] == '[' || p[5] == '$')) {
			size_t num = 0;
			const char *begin = p;
			bool valid = TRUE;

			p += 5;
			if (*p == '[') {
				p += 1;

				while (i_isdigit(*p)) {
					num = num * 10 + (*p - '0');
					p++;
				}

				if (*p++ != ']' || *p++ != '$') {
					str_append_data(out_msg, begin,
							p-begin);
					valid = FALSE;
				}
			} else {
				p += 1;
			}

			if (valid) {
				size_t body_size;
				const char *body_text;

				if ((ret = cmd_notify_extract_body_text(
					renv, &body_text, &body_size)) <= 0)
					return ret;

				if (num > 0 && num < body_size) {
					str_append_data(out_msg, body_text,
							num);
				} else {
					str_append_data(out_msg, body_text,
							body_size);
				}
			}
		} else {
			size_t len;

			/* Find next substitution */
			len = strcspn(p + 1, "$") + 1;

			/* Copy normal text */
			str_append_data(out_msg, p, len);
			p += len;
		}
	}

	return SIEVE_EXEC_OK;
}
