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

#include "lib.h"
#include "strfuncs.h"
#include "str.h"
#include "array.h"
#include "net.h"
#include "istream.h"
#include "ostream.h"
#include "imap-url.h"
#include "imap-quote.h"
#include "imap-common.h"
#include "imap-commands.h"
#include "imap-urlauth.h"
#include "imap-urlauth-fetch.h"

struct cmd_urlfetch_context {
	struct imap_urlauth_fetch *ufetch;
	struct istream *input;
	uoff_t size;

	unsigned int failed:1;
	unsigned int finished:1;
	unsigned int extended:1;
	unsigned int in_io_handler:1;
};

struct cmd_urlfetch_url {
	const char *url;
	
	enum imap_urlauth_fetch_flags flags;
};

static void cmd_urlfetch_finish(struct client_command_context *cmd)
{
	struct cmd_urlfetch_context *ctx =
		(struct cmd_urlfetch_context *)cmd->context;

	if (ctx->finished)
		return;
	ctx->finished = TRUE;

	if (ctx->input != NULL)
		i_stream_unref(&ctx->input);
	if (ctx->ufetch != NULL)
		imap_urlauth_fetch_deinit(&ctx->ufetch);

	if (ctx->failed) {
		if (cmd->client->output_cmd_lock == cmd) {
			/* failed in the middle of a literal.
			   we need to disconnect. */
			cmd->client->output_cmd_lock = NULL;
			client_disconnect(cmd->client, "URLFETCH failed");
		} else {
			client_send_internal_error(cmd);
		}
		return;
	}

	client_send_tagline(cmd, "OK URLFETCH completed.");
}

static bool cmd_urlfetch_cancel(struct client_command_context *cmd)
{
	struct cmd_urlfetch_context *ctx =
		(struct cmd_urlfetch_context *)cmd->context;

	if (!cmd->cancel)
		return FALSE;

	if (ctx->ufetch != NULL) {
		if (cmd->client->user->mail_debug) {
			i_debug("URLFETCH: Canceling command; "
				"aborting URLAUTH fetch requests prematurely");
		}
		imap_urlauth_fetch_deinit(&ctx->ufetch);
	}
	return TRUE;
}

static int cmd_urlfetch_transfer_literal(struct client_command_context *cmd)
{
	struct client *client = cmd->client;
	struct cmd_urlfetch_context *ctx =
		(struct cmd_urlfetch_context *)cmd->context;
	int ret;

	/* are we in the middle of an urlfetch literal? */
	if (ctx->input == NULL)
		return 1;

	/* flush output to client if buffer is filled above optimum */
	if (o_stream_get_buffer_used_size(client->output) >=
	    CLIENT_OUTPUT_OPTIMAL_SIZE) {
		if ((ret = o_stream_flush(client->output)) <= 0)
			return ret;
	}

	/* transfer literal to client */
	o_stream_set_max_buffer_size(client->output, 0);
	(void)o_stream_send_istream(client->output, ctx->input);
	o_stream_set_max_buffer_size(client->output, (size_t)-1);

	if (ctx->input->v_offset == ctx->size) {
		/* finished successfully */
		i_stream_unref(&ctx->input);
		return 1;
	}
	if (client->output->closed) {
		/* client disconnected */
		return -1;
	}
	if (ctx->input->stream_errno != 0) {
		errno = ctx->input->stream_errno;
		i_error("read(%s) failed: %s (URLFETCH)",
			i_stream_get_name(ctx->input),
			i_stream_get_error(ctx->input));
		client_disconnect(client, "URLFETCH failed");
		return -1;
	}
	if (i_stream_have_bytes_left(ctx->input)) {
		o_stream_set_flush_pending(client->output, TRUE);
		return 0;
	}

	i_error("URLFETCH got too little data: %"PRIuUOFF_T" vs %"PRIuUOFF_T,
		ctx->input->v_offset, ctx->size);
	client_disconnect(client, "FETCH failed");
	return -1;
}

static bool cmd_urlfetch_continue(struct client_command_context *cmd)
{
	struct client *client = cmd->client;
	struct cmd_urlfetch_context *ctx =
		(struct cmd_urlfetch_context *)cmd->context;
	bool urls_pending;
	int ret = 1;

	if (cmd->cancel)
		return cmd_urlfetch_cancel(cmd);

	i_assert(client->output_cmd_lock == NULL ||
		 client->output_cmd_lock == cmd);

	/* finish a pending literal transfer */
	ret = cmd_urlfetch_transfer_literal(cmd);
	if (ret < 0) {
		ctx->failed = TRUE;
		cmd_urlfetch_finish(cmd);
		return TRUE;
	}
	if (ret == 0) {
		/* not finished; apparently output blocked again */
		return FALSE;
	}

	if (ctx->extended)
		client_send_line(client, ")");
	else
		client_send_line(client, "");
	client->output_cmd_lock = NULL;

	ctx->in_io_handler = TRUE;
	urls_pending = imap_urlauth_fetch_continue(ctx->ufetch);
	ctx->in_io_handler = FALSE;

	if (urls_pending) {
		/* waiting for imap urlauth service */
		cmd->state = CLIENT_COMMAND_STATE_WAIT_EXTERNAL;
		cmd->func = cmd_urlfetch_cancel;

		/* retrieve next url */
		return FALSE;
	}

	/* finished */
	cmd_urlfetch_finish(cmd);
	return TRUE;
}

static int cmd_urlfetch_url_sucess(struct client_command_context *cmd,
				   struct imap_urlauth_fetch_reply *reply)
{
	struct cmd_urlfetch_context *ctx = cmd->context;
	string_t *response = t_str_new(256);
	int ret;

	str_append(response, "* URLFETCH ");
	imap_append_astring(response, reply->url);

	if ((reply->flags & IMAP_URLAUTH_FETCH_FLAG_EXTENDED) == 0) {
		/* simple */
		ctx->extended = FALSE;

		str_printfa(response, " {%"PRIuUOFF_T"}", reply->size);
		client_send_line(cmd->client, str_c(response));
		i_assert(reply->size == 0 || reply->input != NULL);
	} else {
		bool metadata = FALSE;

		/* extended */
		ctx->extended = TRUE;

		str_append(response, " (");
		if ((reply->flags & IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE) != 0 &&
		    reply->bodypartstruct != NULL) {
			str_append(response, "BODYPARTSTRUCTURE (");
			str_append(response, reply->bodypartstruct);
			str_append_c(response, ')');
			metadata = TRUE;
		}
		if ((reply->flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0 ||
		    (reply->flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0) {
			if (metadata)
				str_append_c(response, ' ');
			if ((reply->flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0) {
				str_append(response, "BODY ");
			} else {
				str_append(response, "BINARY ");
				if (reply->binary_has_nuls)
					str_append_c(response, '~');
			}
			str_printfa(response, "{%"PRIuUOFF_T"}", reply->size);
			i_assert(reply->size == 0 || reply->input != NULL);
		} else {
			str_append_c(response, ')');
			ctx->extended = FALSE;
		}

		client_send_line(cmd->client, str_c(response));
	}

	if (reply->input != NULL) {
		ctx->input = reply->input;
		ctx->size = reply->size;
		i_stream_ref(ctx->input);

		ret = cmd_urlfetch_transfer_literal(cmd);
		if (ret < 0) {
			ctx->failed = TRUE;
			return -1;
		}
		if (ret == 0) {
			/* not finished; apparently output blocked */
			cmd->state = CLIENT_COMMAND_STATE_WAIT_OUTPUT;
			cmd->func = cmd_urlfetch_continue;
			cmd->client->output_cmd_lock = cmd;
			return 0;
		}

		if (ctx->extended)
			client_send_line(cmd->client, ")");
		else
			client_send_line(cmd->client, "");
	}
	return 1;
}

static int
cmd_urlfetch_url_callback(struct imap_urlauth_fetch_reply *reply,
			  bool last, void *context)
{
	struct client_command_context *cmd = context;
	struct client *client = cmd->client;
	struct cmd_urlfetch_context *ctx = cmd->context;
	bool in_io_handler = ctx->in_io_handler;
	int ret;

	if (!in_io_handler)
		o_stream_cork(client->output);
	if (reply == NULL) {
		/* fatal failure */
		ctx->failed = TRUE;
		ret = -1;
	} else if (reply->succeeded) {
		/* URL fetch succeeded */
		ret = cmd_urlfetch_url_sucess(cmd, reply);
	} else {
		/* URL fetch failed */
		string_t *response = t_str_new(128);

		str_append(response, "* URLFETCH ");
		imap_append_astring(response, reply->url);
		str_append(response, " NIL");
		client_send_line(client, str_c(response));
		if (reply->error != NULL) {
			client_send_line(client, t_strdup_printf(
				"* NO %s.", reply->error));
		}
		ret = 1;
	}

	if ((last && cmd->state == CLIENT_COMMAND_STATE_WAIT_EXTERNAL) ||
	    ret < 0) {
		cmd_urlfetch_finish(cmd);
		client_command_free(&cmd);
	}
	if (!in_io_handler)
		o_stream_uncork(client->output);
	return ret;
}

static int
cmd_urlfetch_parse_arg(struct client_command_context *cmd,
		       const struct imap_arg *arg,
		       struct cmd_urlfetch_url *ufurl_r)
{
	enum imap_urlauth_fetch_flags url_flags = 0;
	const struct imap_arg *params;
	const char *url_text;

	if (imap_arg_get_list(arg, &params))
		url_flags |= IMAP_URLAUTH_FETCH_FLAG_EXTENDED;
	else
		params = arg;

	/* read url */
	if (!imap_arg_get_astring(params++, &url_text)) {
		client_send_command_error(cmd, "Invalid arguments.");
		return -1;
	}
	ufurl_r->url = t_strdup(url_text);
	if (url_flags == 0)
		return 0;

	while (!IMAP_ARG_IS_EOL(params)) {
		const char *fetch_param;

		if (!imap_arg_get_atom(params++, &fetch_param)) {
			client_send_command_error(cmd,
				"Invalid URL fetch parameter.");
			return -1;
		}

		if (strcasecmp(fetch_param, "BODY") == 0)
			url_flags |= IMAP_URLAUTH_FETCH_FLAG_BODY;
		else if (strcasecmp(fetch_param, "BINARY") == 0)
			url_flags |= IMAP_URLAUTH_FETCH_FLAG_BINARY;
		else if (strcasecmp(fetch_param, "BODYPARTSTRUCTURE") == 0)
			url_flags |= IMAP_URLAUTH_FETCH_FLAG_BODYPARTSTRUCTURE;
		else {
			client_send_command_error(cmd,
				t_strdup_printf("Unknown URL fetch parameter: %s",
						fetch_param));
			return -1;
		}
	}

	if ((url_flags & IMAP_URLAUTH_FETCH_FLAG_BODY) != 0 &&
	    (url_flags & IMAP_URLAUTH_FETCH_FLAG_BINARY) != 0) {
		client_send_command_error(cmd,
			"URL cannot have both BODY and BINARY fetch parameters.");
		return -1;
	}

	if (url_flags == IMAP_URLAUTH_FETCH_FLAG_EXTENDED)
		url_flags |= IMAP_URLAUTH_FETCH_FLAG_BODY;
	ufurl_r->flags = url_flags;
	return 0;
}

bool cmd_urlfetch(struct client_command_context *cmd)
{
	struct client *client = cmd->client;
	struct cmd_urlfetch_context *ctx;
	ARRAY(struct cmd_urlfetch_url) urls;
	const struct cmd_urlfetch_url *url;
	const struct imap_arg *args;
	struct cmd_urlfetch_url *ufurl;

	if (client->urlauth_ctx == NULL) {
		client_send_command_error(cmd, "URLAUTH disabled.");
		return TRUE;
	}

	if (!client_read_args(cmd, 0, 0, &args))
		return FALSE;

	if (IMAP_ARG_IS_EOL(args)) {
		client_send_command_error(cmd, "Invalid arguments.");
		return TRUE;
	}
	
	t_array_init(&urls, 32);

	/* parse url arguments and group them per userid */
	for (; !IMAP_ARG_IS_EOL(args); args++) {
		ufurl = array_append_space(&urls);
		if (cmd_urlfetch_parse_arg(cmd, args, ufurl) < 0)
			return TRUE;
	}
	cmd->context = ctx = p_new(cmd->pool, struct cmd_urlfetch_context, 1);
	cmd->func = cmd_urlfetch_cancel;
	cmd->state = CLIENT_COMMAND_STATE_WAIT_INPUT;

	ctx->ufetch = imap_urlauth_fetch_init(client->urlauth_ctx,
					      cmd_urlfetch_url_callback, cmd);

	ctx->in_io_handler = TRUE;
	array_foreach(&urls, url) {
		if (imap_urlauth_fetch_url(ctx->ufetch, url->url, url->flags) < 0) {
			/* fatal error */
			ctx->failed = TRUE;
			break;
		}
	}
	ctx->in_io_handler = FALSE;

	if ((ctx->failed || !imap_urlauth_fetch_is_pending(ctx->ufetch))
		&& cmd->client->output_cmd_lock != cmd) {
		/* finished */
		cmd_urlfetch_finish(cmd);
		return TRUE;
	}

	if (cmd->client->output_cmd_lock != cmd)
		cmd->state = CLIENT_COMMAND_STATE_WAIT_EXTERNAL;
	return FALSE;	
}
