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

#include "login-common.h"
#include "array.h"
#include "ioloop.h"
#include "istream.h"
#include "ostream.h"
#include "settings.h"
#include "master-service.h"
#include "ssl-settings.h"
#include "client.h"
#include "client-authenticate.h"
#include "submission-proxy.h"
#include "submission-login-settings.h"
#include "settings-parser.h"

/* Disconnect client when it sends too many bad commands */
#define CLIENT_MAX_BAD_COMMANDS 10

static const struct smtp_server_callbacks smtp_callbacks;

static struct smtp_server *smtp_server = NULL;

static void
client_parse_backend_capabilities(struct submission_client *subm_client )
{
	const struct submission_login_settings *set = subm_client->set;
	const char *const *str;

	if (array_is_empty(&set->submission_backend_capabilities)) {
		subm_client->backend_capabilities = SMTP_CAPABILITY_8BITMIME;
#ifdef EXPERIMENTAL_MAIL_UTF8
		if (subm_client->set->mail_utf8_extensions)
			subm_client->backend_capabilities |= SMTP_CAPABILITY_SMTPUTF8;
#endif
		return;
	}

	subm_client->backend_capabilities = SMTP_CAPABILITY_NONE;
	str = settings_boollist_get(&set->submission_backend_capabilities);
	for (; *str != NULL; str++) {
		if (strcmp(*str, "none") == 0)
			continue;
		enum smtp_capability cap = smtp_capability_find_by_name(*str);

		if (cap == SMTP_CAPABILITY_NONE) {
			e_warning(subm_client->common.event,
				  "Unknown SMTP capability in submission_backend_capabilities: "
				  "%s", *str);
			continue;
		}

		subm_client->backend_capabilities |= cap;
	}

	/* Make sure CHUNKING support is always enabled when BINARYMIME is
	   enabled by explicit configuration. */
	if (HAS_ALL_BITS(subm_client->backend_capabilities,
			 SMTP_CAPABILITY_BINARYMIME)) {
		subm_client->backend_capabilities |= SMTP_CAPABILITY_CHUNKING;
	}
}

static int submission_login_start_tls(void *conn_ctx,
	struct istream **input, struct ostream **output)
{
	struct submission_client *subm_client = conn_ctx;
	struct client *client = &subm_client->common;

	client->connection_used_starttls = TRUE;
	if (client_init_ssl(client) < 0) {
		client_notify_disconnect(client,
			CLIENT_DISCONNECT_INTERNAL_ERROR,
			"TLS initialization failed.");
		client_destroy(client,
			"Disconnected: TLS initialization failed.");
		return -1;
	}
	login_refresh_proctitle();

	*input = client->input;
	*output = client->output;
	return 0;
}

static int
submission_client_reload_config(struct client *client,
				const char **error_r ATTR_UNUSED)
{
	struct submission_client *subm_client =
		container_of(client, struct submission_client, common);
	smtp_server_connection_set_greeting(subm_client->conn,
					    client->set->login_greeting);
	return 0;
}

static struct client *submission_client_alloc(pool_t pool)
{
	struct submission_client *subm_client;

	subm_client = p_new(pool, struct submission_client, 1);
	return &subm_client->common;
}

static int submission_client_create(struct client *client)
{
	static const char *const xclient_extensions[] =
		{ "FORWARD", NULL };
	struct submission_client *subm_client =
		container_of(client, struct submission_client, common);
	struct smtp_server_settings smtp_set;
	const char *error;

	if (settings_get(client->event, &submission_login_setting_parser_info,
			 0, &subm_client->set, &error) < 0) {
		e_error(client->event, "%s", error);
		return -1;
	}

	client_parse_backend_capabilities(subm_client);

	i_zero(&smtp_set);
	smtp_set.capabilities = SMTP_CAPABILITY_SIZE |
		SMTP_CAPABILITY_ENHANCEDSTATUSCODES | SMTP_CAPABILITY_AUTH |
		SMTP_CAPABILITY_XCLIENT;
#ifdef EXPERIMENTAL_MAIL_UTF8
	if (subm_client->set->mail_utf8_extensions)
		smtp_set.capabilities |= SMTP_CAPABILITY_SMTPUTF8;
#endif
	if (client_is_tls_enabled(client))
		smtp_set.capabilities |= SMTP_CAPABILITY_STARTTLS;
	smtp_set.hostname = subm_client->set->hostname;
	smtp_set.login_greeting = client->set->login_greeting;
	smtp_set.tls_required = !client->connection_secured &&
		(strcmp(client->ssl_server_set->ssl, "required") == 0);
	smtp_set.xclient_extensions = xclient_extensions;
	smtp_set.command_limits.max_parameters_size = LOGIN_MAX_INBUF_SIZE;
	smtp_set.command_limits.max_auth_size = LOGIN_MAX_AUTH_BUF_SIZE;
	smtp_set.debug = event_want_debug(client->event);
	smtp_set.event_parent = client->event;
	smtp_set.max_recipients = SET_UINT_UNLIMITED;

	subm_client->conn = smtp_server_connection_create_from_streams(
		smtp_server, client->input, client->output,
		&client->real_local_ip, client->real_local_port,
		&client->real_remote_ip, client->real_remote_port,
		&smtp_set, &smtp_callbacks, subm_client);
	return 0;
}

static void submission_client_destroy(struct client *client)
{
	struct submission_client *subm_client =
		container_of(client, struct submission_client, common);

	if (subm_client->conn != NULL)
		smtp_server_connection_close(&subm_client->conn, NULL);
	settings_free(subm_client->set);
	i_free_and_null(subm_client->proxy_xclient);
}

static void submission_client_notify_auth_ready(struct client *client)
{
	struct submission_client *subm_client =
		container_of(client, struct submission_client, common);

	i_assert(client->io == NULL);
	client->banner_sent = TRUE;
	if (!smtp_server_connection_is_started(subm_client->conn))
		smtp_server_connection_start(subm_client->conn);
}

static void submission_client_notify_auth_connected(struct client *client)
{
	struct submission_client *subm_client =
		container_of(client, struct submission_client, common);

	if (subm_client->auth_cmd_deferred)
		cmd_auth_begin(subm_client);
}

static void
submission_client_notify_disconnect(struct client *_client,
				    enum client_disconnect_reason reason,
				    const char *text)
{
	struct submission_client *client =
		container_of(_client, struct submission_client, common);
	struct smtp_server_connection *conn;

	conn = client->conn;
	client->conn = NULL;
	if (conn != NULL) {
		switch (reason) {
		case CLIENT_DISCONNECT_TIMEOUT:
			smtp_server_connection_terminate(&conn, "4.4.2", text);
			break;
		case CLIENT_DISCONNECT_SYSTEM_SHUTDOWN:
			smtp_server_connection_terminate(&conn, "4.3.2", text);
			break;
		case CLIENT_DISCONNECT_INTERNAL_ERROR:
		default:
			smtp_server_connection_terminate(&conn, "4.0.0", text);
			break;
		}
	}
}

static void
client_connection_cmd_xclient(void *context,
			      struct smtp_server_cmd_ctx *cmd,
			      struct smtp_proxy_data *data)
{
	unsigned int i;

	struct submission_client *client = context;

	if (data->source_ip.family != 0)
		client->common.ip = data->source_ip;
	if (data->source_port != 0)
		client->common.remote_port = data->source_port;
	if (data->dest_ip.family != 0)
		client->common.local_ip = data->dest_ip;
	if (data->dest_port != 0)
		client->common.local_port = data->dest_port;
	if (data->ttl_plus_1 > 0)
		client->common.proxy_ttl = data->ttl_plus_1 - 1;
	if (data->session != NULL) {
		client->common.session_id =
			p_strdup(client->common.pool, data->session);
	}
	if (data->local_name != NULL) {
		client->common.local_name =
			p_strdup(client->common.preproxy_pool, data->local_name);
	}
	if (data->client_transport != NULL) {
		client->common.end_client_tls_secured_set = TRUE;
		client->common.end_client_tls_secured =
			str_begins_with(data->client_transport,
					CLIENT_TRANSPORT_TLS);
	}

	for (i = 0; i < data->extra_fields_count; i++) {
		const char *name = data->extra_fields[i].name;
		const char *value = data->extra_fields[i].value;

		if (strcasecmp(name, "FORWARD") == 0) {
			if (!client_forward_decode_base64(&client->common, value)) {
				smtp_server_reply(cmd, 501, "5.5.4",
						  "Invalid FORWARD parameter");
			}
		}
	}

	const char *error;
	if (client_addresses_changed(&client->common, &error) < 0) {
		client_notify_disconnect(&client->common,
			CLIENT_DISCONNECT_INTERNAL_ERROR,
			"Failed to reload configuration");
		client_disconnect(&client->common, error);
	}
}

static void client_connection_disconnect(void *context, const char *reason)
{
	struct submission_client *client = context;

	client->auth_cmd = NULL;
	client->auth_cmd_data = NULL;
	client_disconnect(&client->common, reason);
}

static void client_connection_free(void *context)
{
	struct submission_client *client = context;

	if (client->conn == NULL)
		return;
	client->conn = NULL;
	client_destroy(&client->common, NULL);
}

static bool client_connection_is_trusted(void *context)
{
	struct submission_client *client = context;

	return client->common.connection_trusted;
}

static void submission_login_die(void)
{
	/* do nothing. submission connections typically die pretty quick anyway.
	 */
}

static void submission_login_preinit(void)
{
}

static void submission_login_init(void)
{
	struct smtp_server_settings smtp_server_set;

	/* override the default login_die() */
	master_service_set_die_callback(master_service, submission_login_die);

	/* initialize SMTP server */
	i_zero(&smtp_server_set);
	smtp_server_set.protocol = SMTP_PROTOCOL_SMTP;
	smtp_server_set.max_pipelined_commands = 5;
	smtp_server_set.max_bad_commands = CLIENT_MAX_BAD_COMMANDS;
	smtp_server_set.reason_code_module = "submission";
	/* Pre-auth state is always logged either as GREETING or READY.
	   It's not very useful. */
	smtp_server_set.no_state_in_reason = TRUE;
	smtp_server_set.max_recipients = SET_UINT_UNLIMITED;
	smtp_server = smtp_server_init(&smtp_server_set);
	smtp_server_command_override(smtp_server, "MAIL", cmd_mail,
				     SMTP_SERVER_CMD_FLAG_PREAUTH);
}

static void submission_login_deinit(void)
{
	clients_destroy_all();

	smtp_server_deinit(&smtp_server);
}

static const struct smtp_server_callbacks smtp_callbacks = {
	.conn_cmd_helo = cmd_helo,

	.conn_start_tls = submission_login_start_tls,

	.conn_cmd_auth = cmd_auth,
	.conn_cmd_auth_continue = cmd_auth_continue,

	.conn_cmd_xclient = client_connection_cmd_xclient,

	.conn_disconnect = client_connection_disconnect,
	.conn_free = client_connection_free,

	.conn_is_trusted = client_connection_is_trusted
};

static struct client_vfuncs submission_client_vfuncs = {
	.alloc = submission_client_alloc,
	.create = submission_client_create,
	.destroy = submission_client_destroy,
	.reload_config = submission_client_reload_config,
	.notify_auth_ready = submission_client_notify_auth_ready,
	.notify_auth_connected = submission_client_notify_auth_connected,
	.notify_disconnect = submission_client_notify_disconnect,
	.auth_send_challenge = submission_client_auth_send_challenge,
	.auth_result = submission_client_auth_result,
	.proxy_reset = submission_proxy_reset,
	.proxy_parse_line = submission_proxy_parse_line,
	.proxy_failed = submission_proxy_failed,
	.proxy_get_state = submission_proxy_get_state,
};

static struct login_binary submission_login_binary = {
	.protocol = "submission",
	.service_name = "submission",
	.process_name = "submission-login",
	.default_port = 587,

	.event_category = {
		.name = "submission",
	},

	.client_vfuncs = &submission_client_vfuncs,
	.preinit = submission_login_preinit,
	.init = submission_login_init,
	.deinit = submission_login_deinit,

	.sasl_support_final_reply = FALSE,
	.anonymous_login_acceptable = FALSE,

	.application_protocols = (const char *const[]) {
		"submission", NULL
	},
};

int main(int argc, char *argv[])
{
	return login_binary_run(&submission_login_binary, argc, argv);
}
