/* Copyright (c) Dovecot authors, see top-level COPYING file */

#include "lib.h"
#include "safe-mkstemp.h"
#include "istream.h"
#include "ostream.h"
#include "base64.h"
#include "hash-format.h"
#include "str.h"
#include "message-parser.h"
#include "rfc822-parser.h"
#include "fs-api.h"
#include "istream-fs-file.h"
#include "istream-attachment-connector.h"
#include "istream-attachment-extractor.h"
#include "mail-user.h"
#include "index-mail.h"
#include "index-attachment.h"

enum mail_attachment_decode_option {
	MAIL_ATTACHMENT_DECODE_OPTION_NONE = '-',
	MAIL_ATTACHMENT_DECODE_OPTION_BASE64 = 'B',
	MAIL_ATTACHMENT_DECODE_OPTION_CRLF = 'C'
};

struct mail_save_attachment {
	pool_t pool;
	struct fs *fs;
	struct istream *input;

	struct fs_file *cur_file;
	ARRAY_TYPE(mail_attachment_extref) extrefs;
};

static bool index_attachment_want(const struct istream_attachment_header *hdr,
				  void *context)
{
	struct mail_save_context *ctx = context;
	struct mail_attachment_part apart;

	i_zero(&apart);
	apart.part = hdr->part;
	apart.content_type = hdr->content_type;
	apart.content_disposition = hdr->content_disposition;

	if (ctx->part_is_attachment != NULL)
		return ctx->part_is_attachment(ctx, &apart);

	/* don't treat text/ parts as attachments */
	return hdr->content_type != NULL &&
		!str_begins_icase_with(hdr->content_type, "text/");
}

static int index_attachment_open_temp_fd(void *context)
{
	struct mail_save_context *ctx = context;
	struct mail_storage *storage = ctx->transaction->box->storage;
	string_t *temp_path;
	int fd;

	temp_path = t_str_new(256);
	mail_user_set_get_temp_prefix(temp_path, storage->user->set);
	fd = safe_mkstemp_hostpid(temp_path, 0600, (uid_t)-1, (gid_t)-1);
	if (fd == -1) {
		mailbox_set_critical(ctx->transaction->box,
			"safe_mkstemp(%s) failed: %m", str_c(temp_path));
		return -1;
	}
	if (unlink(str_c(temp_path)) < 0) {
		mailbox_set_critical(ctx->transaction->box,
			"unlink(%s) failed: %m", str_c(temp_path));
		i_close_fd(&fd);
		return -1;
	}
	return fd;
}

static int
index_attachment_open_ostream(struct istream_attachment_info *info,
			      struct ostream **output_r,
			      const char **error_r ATTR_UNUSED, void *context)
{
	struct mail_save_context *ctx = context;
	struct mail_save_attachment *attach = ctx->data.attach;
	struct mail_storage *storage = ctx->transaction->box->storage;
	struct mail_attachment_extref *extref;
	enum fs_open_flags flags = 0;
	const char *path, *digest = info->hash;
	guid_128_t guid_128;

	i_assert(attach->cur_file == NULL);

	if (storage->set->parsed_fsync_mode != FSYNC_MODE_NEVER)
		flags |= FS_OPEN_FLAG_FSYNC;

	if (strlen(digest) < 4) {
		/* make sure we can access first 4 bytes without accessing
		   out of bounds memory */
		digest = t_strconcat(digest, "\0\0\0\0", NULL);
	}

	guid_128_generate(guid_128);
	path = t_strdup_printf("%s/%c%c/%c%c/%s-%s",
			       storage->set->mail_ext_attachment_path,
			       digest[0], digest[1],
			       digest[2], digest[3], digest,
			       guid_128_to_string(guid_128));
	attach->cur_file = fs_file_init(attach->fs, path,
					FS_OPEN_MODE_REPLACE | flags);

	extref = array_append_space(&attach->extrefs);
	extref->start_offset = info->start_offset;
	extref->size = info->encoded_size;
	extref->path = p_strdup(attach->pool, path +
			strlen(storage->set->mail_ext_attachment_path) + 1);
	extref->base64_blocks_per_line = info->base64_blocks_per_line;
	extref->base64_have_crlf = info->base64_have_crlf;

	*output_r = fs_write_stream(attach->cur_file);
	return 0;
}

static int
index_attachment_close_ostream(struct ostream *output, bool success,
			       const char **error, void *context)
{
	struct mail_save_context *ctx = context;
	struct mail_save_attachment *attach = ctx->data.attach;
	int ret = success ? 0 : -1;

	i_assert(attach->cur_file != NULL);

	if (ret < 0)
		fs_write_stream_abort_error(attach->cur_file, &output, "%s", *error);
	else if (fs_write_stream_finish(attach->cur_file, &output) < 0) {
		*error = t_strdup_printf("Couldn't create attachment %s: %s",
					 fs_file_path(attach->cur_file),
					 fs_file_last_error(attach->cur_file));
		ret = -1;
	}
	fs_file_deinit(&attach->cur_file);

	if (ret < 0) {
		array_pop_back(&attach->extrefs);
	}
	return ret;
}

void index_attachment_save_begin(struct mail_save_context *ctx,
				 struct fs *fs, struct istream *input)
{
	struct mail_storage *storage = ctx->transaction->box->storage;
	struct mail_save_attachment *attach;
	struct istream_attachment_settings set;
	const char *error;
	pool_t pool;

	i_assert(ctx->data.attach == NULL);

	if (*storage->set->mail_ext_attachment_path == '\0')
		return;

	i_zero(&set);
	set.min_size = storage->set->mail_ext_attachment_min_size;
	if (hash_format_init(storage->set->mail_ext_attachment_hash,
			     &set.hash_format, &error) < 0) {
		/* we already checked this when verifying settings */
		i_panic("mail_attachment_hash=%s unexpectedly failed: %s",
			storage->set->mail_ext_attachment_hash, error);
	}
	set.want_attachment = index_attachment_want;
	set.open_temp_fd = index_attachment_open_temp_fd;
	set.open_attachment_ostream = index_attachment_open_ostream;
	set.close_attachment_ostream = index_attachment_close_ostream;

	pool = pool_alloconly_create("save attachment", 1024);
	attach = p_new(pool, struct mail_save_attachment, 1);
	attach->pool = pool;
	attach->fs = fs;
	attach->input = i_stream_create_attachment_extractor(input, &set, ctx);
	p_array_init(&attach->extrefs, attach->pool, 8);
	ctx->data.attach = attach;
}

static int save_check_write_error(struct mail_save_context *ctx,
				  struct ostream *output)
{
	struct mail_storage *storage = ctx->transaction->box->storage;

	if (output->stream_errno == 0)
		return 0;

	if (!mail_storage_set_error_from_errno(storage)) {
		mail_set_critical(ctx->dest_mail, "write(%s) failed: %s",
			o_stream_get_name(output), o_stream_get_error(output));
	}
	return -1;
}

int index_attachment_save_continue(struct mail_save_context *ctx)
{
	struct mail_save_attachment *attach = ctx->data.attach;
	const unsigned char *data;
	size_t size;
	ssize_t ret;

	if (attach->input->stream_errno != 0)
		return -1;

	do {
		ret = i_stream_read(attach->input);
		if (ret > 0 || ret == -2) {
			data = i_stream_get_data(attach->input, &size);
			o_stream_nsend(ctx->data.output, data, size);
			i_stream_skip(attach->input, size);
		}
		index_mail_cache_parse_continue(ctx->dest_mail);
		if (ret == 0 && !i_stream_attachment_extractor_can_retry(attach->input)) {
			/* need more input */
			return 0;
		}
	} while (ret != -1);

	if (attach->input->stream_errno != 0) {
		mail_set_critical(ctx->dest_mail, "read(%s) failed: %s",
					  i_stream_get_name(attach->input),
					  i_stream_get_error(attach->input));
		return -1;
	}
	if (ctx->data.output != NULL) {
		if (save_check_write_error(ctx, ctx->data.output) < 0)
			return -1;
	}
	return 0;
}

int index_attachment_save_finish(struct mail_save_context *ctx)
{
	struct mail_save_attachment *attach = ctx->data.attach;

	(void)i_stream_read(attach->input);
	i_assert(attach->input->eof);
	return attach->input->stream_errno == 0 ? 0 : -1;
}

void index_attachment_save_free(struct mail_save_context *ctx)
{
	struct mail_save_attachment *attach = ctx->data.attach;

	if (attach != NULL) {
		i_stream_unref(&attach->input);
		pool_unref(&attach->pool);
		ctx->data.attach = NULL;
	}
}

const ARRAY_TYPE(mail_attachment_extref) *
index_attachment_save_get_extrefs(struct mail_save_context *ctx)
{
	return ctx->data.attach == NULL ? NULL :
		&ctx->data.attach->extrefs;
}

static int
index_attachment_delete_real(struct mail_storage *storage,
			     struct fs *fs, const char *name)
{
	struct fs_file *file;
	const char *path;
	int ret;

	path = t_strdup_printf("%s/%s", storage->set->mail_ext_attachment_path, name);
	file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY);
	if ((ret = fs_delete(file)) < 0)
		mail_storage_set_critical(storage, "%s", fs_file_last_error(file));
	fs_file_deinit(&file);
	return ret;
}

int index_attachment_delete(struct mail_storage *storage,
			    struct fs *fs, const char *name)
{
	int ret;

	T_BEGIN {
		ret = index_attachment_delete_real(storage, fs, name);
	} T_END;
	return ret;
}

void index_attachment_append_extrefs(string_t *str,
	const ARRAY_TYPE(mail_attachment_extref) *extrefs)
{
	const struct mail_attachment_extref *extref;
	bool add_space = FALSE;
	unsigned int startpos;

	array_foreach(extrefs, extref) {
		if (!add_space)
			add_space = TRUE;
		else
			str_append_c(str, ' ');
		str_printfa(str, "%"PRIuUOFF_T" %"PRIuUOFF_T" ",
			    extref->start_offset, extref->size);

		startpos = str_len(str);
		if (extref->base64_have_crlf)
			str_append_c(str, MAIL_ATTACHMENT_DECODE_OPTION_CRLF);
		if (extref->base64_blocks_per_line > 0) {
			str_printfa(str, "%c%u",
				    MAIL_ATTACHMENT_DECODE_OPTION_BASE64,
				    extref->base64_blocks_per_line * 4);
		}
		if (startpos == str_len(str)) {
			/* make it clear there are no options */
			str_append_c(str, MAIL_ATTACHMENT_DECODE_OPTION_NONE);
		}
		str_append_c(str, ' ');
		str_append(str, extref->path);
	}
}

static bool
parse_extref_decode_options(const char *str,
			    struct mail_attachment_extref *extref)
{
	unsigned int num;

	if (*str == MAIL_ATTACHMENT_DECODE_OPTION_NONE)
		return str[1] == '\0';

	while (*str != '\0') {
		switch (*str) {
		case MAIL_ATTACHMENT_DECODE_OPTION_BASE64:
			str++; num = 0;
			while (*str >= '0' && *str <= '9') {
				num = num*10 + (*str-'0');
				str++;
			}
			if (num == 0 || num % 4 != 0)
				return FALSE;

			extref->base64_blocks_per_line = num/4;
			break;
		case MAIL_ATTACHMENT_DECODE_OPTION_CRLF:
			extref->base64_have_crlf = TRUE;
			str++;
			break;
		default:
			return FALSE;
		}
	}
	return TRUE;
}

bool index_attachment_parse_extrefs(const char *line, pool_t pool,
				    ARRAY_TYPE(mail_attachment_extref) *extrefs)
{
	struct mail_attachment_extref extref;
	const char *const *args;
	unsigned int i, len;
	uoff_t last_voffset;

	args = t_strsplit(line, " ");
	len = str_array_length(args);
	if ((len % 4) != 0)
		return FALSE;

	last_voffset = 0;
	for (i = 0; args[i] != NULL; i += 4) {
		const char *start_offset_str = args[i+0];
		const char *size_str = args[i+1];
		const char *decode_options = args[i+2];
		const char *path = args[i+3];

		i_zero(&extref);
		if (str_to_uoff(start_offset_str, &extref.start_offset) < 0 ||
		    str_to_uoff(size_str, &extref.size) < 0 ||
		    extref.start_offset < last_voffset ||
		    !parse_extref_decode_options(decode_options, &extref))
			return FALSE;

		last_voffset += extref.size +
			(extref.start_offset - last_voffset);

		extref.path = p_strdup(pool, path);
		array_push_back(extrefs, &extref);
	}
	return TRUE;
}

uoff_t index_attachment_base64_decoded_size(const struct mail_attachment_extref *extref)
{
	uoff_t encoded_size = extref->size;
	unsigned int nl_size = extref->base64_have_crlf ? 2 : 1;
	unsigned int line_size = extref->base64_blocks_per_line * 4 + nl_size;
	uoff_t nl_count = (encoded_size - 1) / line_size;
	uoff_t nl_bytes = nl_size * nl_count;

	i_assert(extref->base64_blocks_per_line > 0);
	return MAX_BASE64_DECODED_SIZE(encoded_size - nl_bytes);
}

int index_attachment_stream_get(struct fs *fs, const char *attachment_dir,
				const char *path_suffix,
				struct istream **stream, uoff_t full_size,
				const char *ext_refs, const char **error_r)
{
	ARRAY_TYPE(mail_attachment_extref) extrefs_arr;
	const struct mail_attachment_extref *extref;
	struct istream_attachment_connector *conn;
	struct istream *input;
	struct fs_file *file;
	const char *path;
	int ret;

	*error_r = NULL;

	t_array_init(&extrefs_arr, 16);
	if (!index_attachment_parse_extrefs(ext_refs, pool_datastack_create(),
					    &extrefs_arr)) {
		*error_r = "Broken ext-refs string";
		return -1;
	}
	conn = istream_attachment_connector_begin(*stream, full_size);

	array_foreach(&extrefs_arr, extref) {
		path = t_strdup_printf("%s/%s%s", attachment_dir,
				       extref->path, path_suffix);
		file = fs_file_init(fs, path, FS_OPEN_MODE_READONLY |
				    FS_OPEN_FLAG_SEEKABLE);
		uoff_t raw_size;
		if (extref->base64_blocks_per_line > 0)
			raw_size = index_attachment_base64_decoded_size(extref);
		else
			raw_size = extref->size;
		fs_set_metadata(file, FS_METADATA_FILE_SIZE, t_strdup_printf("%"PRIuUOFF_T, raw_size));
		input = i_stream_create_fs_file(&file, IO_BLOCK_SIZE);

		ret = istream_attachment_connector_add(conn, input,
					extref->start_offset, extref->size,
					extref->base64_blocks_per_line,
					extref->base64_have_crlf, error_r);
		i_stream_unref(&input);
		if (ret < 0) {
			istream_attachment_connector_abort(&conn);
			return -1;
		}
	}

	input = istream_attachment_connector_finish(&conn);
	i_stream_set_name(input, t_strdup_printf(
		"attachments-connector(%s)", i_stream_get_name(*stream)));
	i_stream_unref(stream);
	*stream = input;
	return 0;
}
