/*
 * Copyright (c) 2009-2012, Vsevolod Stakhov
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above copyright
 *       notice, this list of conditions and the following disclaimer in the
 *       documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY AUTHOR ''AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "main.h"
#include "cfg_file.h"
#include "cfg_xml.h"
#include "util.h"
#include "smtp.h"
#include "smtp_proto.h"
#include "smtp_utils.h"
#include "map.h"
#include "message.h"
#include "settings.h"
#include "dns.h"
#include "lua/lua_common.h"

/* Max line size as it is defined in rfc2822 */
#define OUTBUFSIZ 1000

/* Upstream timeouts */
#define DEFAULT_UPSTREAM_ERROR_TIME 10
#define DEFAULT_UPSTREAM_DEAD_TIME 300
#define DEFAULT_UPSTREAM_MAXERRORS 10

#define DEFAULT_REJECT_MESSAGE "450 4.5.0 Spam message rejected"

static gboolean smtp_write_socket (void *arg);

/* Init functions */
gpointer init_smtp (struct rspamd_config *cfg);
void start_smtp (struct rspamd_worker *worker);

worker_t smtp_worker = {
	"smtp",                     /* Name */
	init_smtp,                  /* Init function */
	start_smtp,                 /* Start function */
	TRUE,                       /* Has socket */
	FALSE,                      /* Non unique */
	FALSE,                      /* Non threaded */
	TRUE,                       /* Killable */
	SOCK_STREAM                 /* TCP socket */
};

static gboolean
call_stage_filters (struct smtp_session *session, enum rspamd_smtp_stage stage)
{
	gboolean res = TRUE;
	GList *list = session->ctx->smtp_filters[stage];
	struct smtp_filter *filter;

	while (list) {
		filter = list->data;
		if (!filter->filter (session, filter->filter_data)) {
			res = FALSE;
			break;
		}
		list = g_list_next (list);
	}

	return res;
}

static gboolean
read_smtp_command (struct smtp_session *session, rspamd_fstring_t *line)
{
	struct smtp_command *cmd;
	gchar outbuf[BUFSIZ];
	gint r;

	if (!parse_smtp_command (session, line, &cmd)) {
		session->error = SMTP_ERROR_BAD_COMMAND;
		session->errors++;
		return FALSE;
	}

	switch (cmd->command) {
	case SMTP_COMMAND_HELO:
	case SMTP_COMMAND_EHLO:
		if (session->state == SMTP_STATE_GREETING || session->state ==
			SMTP_STATE_HELO) {
			if (parse_smtp_helo (session, cmd)) {
				session->state = SMTP_STATE_FROM;
			}
			else {
				session->errors++;
			}
			if (!call_stage_filters (session, SMTP_STAGE_HELO)) {
				return FALSE;
			}
			return TRUE;
		}
		else {
			goto improper_sequence;
		}
		break;
	case SMTP_COMMAND_QUIT:
		session->state = SMTP_STATE_QUIT;
		break;
	case SMTP_COMMAND_NOOP:
		break;
	case SMTP_COMMAND_MAIL:
		if (((session->state == SMTP_STATE_GREETING || session->state ==
			SMTP_STATE_HELO) && !session->ctx->helo_required)
			|| session->state == SMTP_STATE_FROM) {
			if (parse_smtp_from (session, cmd)) {
				session->state = SMTP_STATE_RCPT;
			}
			else {
				session->errors++;
				return FALSE;
			}
			if (!call_stage_filters (session, SMTP_STAGE_MAIL)) {
				return FALSE;
			}
		}
		else {
			goto improper_sequence;
		}
		break;
	case SMTP_COMMAND_RCPT:
		if (session->state == SMTP_STATE_RCPT) {
			if (parse_smtp_rcpt (session, cmd)) {
				if (!call_stage_filters (session, SMTP_STAGE_RCPT)) {
					return FALSE;
				}
				/* Make upstream connection */
				if (session->upstream == NULL) {
					if (!create_smtp_upstream_connection (session)) {
						session->error = SMTP_ERROR_UPSTREAM;
						session->state = SMTP_STATE_CRITICAL_ERROR;
						return FALSE;
					}
				}
				else {
					/* Send next rcpt to upstream */
					session->state = SMTP_STATE_WAIT_UPSTREAM;
					session->upstream_state = SMTP_STATE_BEFORE_DATA;
					rspamd_dispatcher_restore (session->upstream_dispatcher);
					r = rspamd_snprintf (outbuf, sizeof (outbuf), "RCPT TO: ");
					r += smtp_upstream_write_list (session->rcpt->data,
							outbuf + r,
							sizeof (outbuf) - r);
					session->cur_rcpt = NULL;
					return rspamd_dispatcher_write (
						session->upstream_dispatcher,
						outbuf,
						r,
						FALSE,
						FALSE);
				}
				session->state = SMTP_STATE_WAIT_UPSTREAM;
				return TRUE;
			}
			else {
				session->errors++;
				return FALSE;
			}
		}
		else {
			goto improper_sequence;
		}
		break;
	case SMTP_COMMAND_RSET:
		session->from = NULL;
		if (session->rcpt) {
			g_list_free (session->rcpt);
		}
		if (session->upstream) {
			rspamd_session_remove_event (session->s,
				smtp_upstream_finalize_connection,
				session);
			session->upstream = NULL;
		}
		session->state = SMTP_STATE_GREETING;
		break;
	case SMTP_COMMAND_DATA:
		if (session->state == SMTP_STATE_RCPT) {
			if (session->rcpt == NULL) {
				session->error = SMTP_ERROR_RECIPIENTS;
				session->errors++;
				return FALSE;
			}
			if (!call_stage_filters (session, SMTP_STAGE_DATA)) {
				return FALSE;
			}
			if (session->upstream == NULL) {
				session->error = SMTP_ERROR_UPSTREAM;
				session->state = SMTP_STATE_CRITICAL_ERROR;
				return FALSE;
			}
			else {
				session->upstream_state = SMTP_STATE_DATA;
				rspamd_dispatcher_restore (session->upstream_dispatcher);
				r = rspamd_snprintf (outbuf, sizeof (outbuf), "DATA" CRLF);
				session->state = SMTP_STATE_WAIT_UPSTREAM;
				session->error = SMTP_ERROR_DATA_OK;
				return rspamd_dispatcher_write (session->upstream_dispatcher,
						   outbuf,
						   r,
						   FALSE,
						   FALSE);
			}
		}
		else {
			goto improper_sequence;
		}
	case SMTP_COMMAND_VRFY:
	case SMTP_COMMAND_EXPN:
	case SMTP_COMMAND_HELP:
		session->error = SMTP_ERROR_UNIMPLIMENTED;
		return FALSE;
	}

	session->error = SMTP_ERROR_OK;
	return TRUE;

improper_sequence:
	session->errors++;
	session->error = SMTP_ERROR_SEQUENCE;
	return FALSE;
}

static gboolean
process_smtp_data (struct smtp_session *session)
{
	struct stat st;
	gint r;
	GList *cur, *t;
	rspamd_fstring_t *f;
	gchar *s;

	if (fstat (session->temp_fd, &st) == -1) {
		msg_err ("fstat failed: %s", strerror (errno));
		goto err;
	}
	/* Now mmap temp file if it is small enough */
	session->temp_size = st.st_size;
	if (session->ctx->max_size == 0 || st.st_size <
		(off_t)session->ctx->max_size) {
		session->task = rspamd_task_new (session->worker);
		session->task->resolver = session->resolver;
		session->task->fin_callback = smtp_write_socket;
		session->task->fin_arg = session;
		session->task->msg =
			rspamd_mempool_alloc (session->pool, sizeof (GString));
		session->task->s = session->s;
#ifdef HAVE_MMAP_NOCORE
		if ((session->task->msg->str =
			mmap (NULL, st.st_size, PROT_READ, MAP_SHARED | MAP_NOCORE,
			session->temp_fd, 0)) == MAP_FAILED) {
#else
		if ((session->task->msg->str =
			mmap (NULL, st.st_size, PROT_READ, MAP_SHARED, session->temp_fd,
			0)) == MAP_FAILED) {
#endif
			msg_err ("mmap failed: %s", strerror (errno));
			goto err;
		}
		session->task->msg->len = st.st_size;
		session->task->helo = session->helo;
		/* Save MAIL FROM */
		cur = session->from;
		if (cur) {
			f = cur->data;
			s = rspamd_mempool_alloc (session->pool, f->len + 1);
			rspamd_strlcpy (s, f->begin, f->len + 1);
			session->task->from = s;
		}
		/* Save recipients */
		t = session->rcpt;
		while (t) {
			cur = t->data;
			if (cur) {
				f = cur->data;
				s = rspamd_mempool_alloc (session->pool, f->len + 1);
				rspamd_strlcpy (s, f->begin, f->len + 1);
				session->task->rcpt = g_list_prepend (session->task->rcpt, s);
			}
			t = g_list_next (t);
		}

		memcpy (&session->task->from_addr, &session->client_addr,
			sizeof (struct in_addr));
		session->task->cmd = CMD_CHECK;

		if (rspamd_message_parse (session->task) == -1) {
			msg_err ("cannot process message");
			munmap (session->task->msg->str, st.st_size);
			goto err;
		}
		if (session->task->cfg->pre_filters == NULL) {
			r = rspamd_process_filters (session->task);
			if (r == -1) {
				msg_err ("cannot process message");
				munmap (session->task->msg->str, st.st_size);
				goto err;
			}
		}
		else {
			rspamd_lua_call_pre_filters (session->task);
			/* We want fin_task after pre filters are processed */
			session->task->s->wanna_die = TRUE;
			session->task->state = WAIT_PRE_FILTER;
			rspamd_session_pending (session->task->s);
		}
	}
	else {
		msg_info ("not scan message as it is %z bytes and maximum is %z",
			st.st_size,
			session->ctx->max_size);
		session->task = NULL;
		return smtp_send_upstream_message (session);
	}

	return TRUE;
err:
	session->error = SMTP_ERROR_FILE;
	session->state = SMTP_STATE_CRITICAL_ERROR;
	if (!rspamd_dispatcher_write (session->dispatcher, session->error, 0, FALSE,
		TRUE)) {
		return FALSE;
	}
	rspamd_session_destroy (session->s);
	return FALSE;
}

/*
 * Callback that is called when there is data to read in buffer
 */
static gboolean
smtp_read_socket (rspamd_fstring_t * in, void *arg)
{
	struct smtp_session *session = arg;

	switch (session->state) {
	case SMTP_STATE_RESOLVE_REVERSE:
	case SMTP_STATE_RESOLVE_NORMAL:
	case SMTP_STATE_DELAY:
		session->error = make_smtp_error (session->pool,
				550,
				"%s Improper use of SMTP command pipelining",
				"5.5.0");
		session->state = SMTP_STATE_ERROR;
		break;
	case SMTP_STATE_GREETING:
	case SMTP_STATE_HELO:
	case SMTP_STATE_FROM:
	case SMTP_STATE_RCPT:
	case SMTP_STATE_DATA:
		read_smtp_command (session, in);
		if (session->state != SMTP_STATE_WAIT_UPSTREAM) {
			if (session->errors > session->ctx->max_errors) {
				session->error = SMTP_ERROR_LIMIT;
				session->state = SMTP_STATE_CRITICAL_ERROR;
				if (!rspamd_dispatcher_write (session->dispatcher,
					session->error, 0, FALSE, TRUE)) {
					return FALSE;
				}
				rspamd_session_destroy (session->s);
				return FALSE;
			}
			if (!smtp_write_socket (session)) {
				return FALSE;
			}
		}
		break;
	case SMTP_STATE_AFTER_DATA:
		if (in->len == 0) {
			return TRUE;
		}
		if (in->len == 3 &&
			memcmp (in->begin, DATA_END_TRAILER, in->len) == 0) {
			return process_smtp_data (session);
		}

		if (write (session->temp_fd, in->begin, in->len) != (ssize_t)in->len) {
			msg_err ("cannot write to temp file: %s", strerror (errno));
			session->error = SMTP_ERROR_FILE;
			session->state = SMTP_STATE_CRITICAL_ERROR;
			if (!rspamd_dispatcher_write (session->dispatcher, session->error,
				0, FALSE, TRUE)) {
				return FALSE;
			}
			rspamd_session_destroy (session->s);
			return FALSE;
		}
		break;
	case SMTP_STATE_WAIT_UPSTREAM:
		rspamd_dispatcher_pause (session->dispatcher);
		break;
	default:
		session->error = make_smtp_error (session->pool,
				550,
				"%s Internal error",
				"5.5.0");
		session->state = SMTP_STATE_ERROR;
		break;
	}

	if (session->state == SMTP_STATE_QUIT) {
		rspamd_session_destroy (session->s);
		return FALSE;
	}
	else if (session->state == SMTP_STATE_WAIT_UPSTREAM) {
		rspamd_dispatcher_pause (session->dispatcher);
	}

	return TRUE;
}

/*
 * Callback for socket writing
 */
static gboolean
smtp_write_socket (void *arg)
{
	struct smtp_session *session = arg;

	if (session->state == SMTP_STATE_CRITICAL_ERROR) {
		if (session->error != NULL) {
			if (!rspamd_dispatcher_write (session->dispatcher, session->error,
				0, FALSE, TRUE)) {
				return FALSE;
			}
		}
		rspamd_session_destroy (session->s);
		return FALSE;
	}
	else if (session->state == SMTP_STATE_END) {
		if (session->task != NULL) {
			return write_smtp_reply (session);
		}
		else {
			if (session->error != NULL) {
				if (!rspamd_dispatcher_write (session->dispatcher,
					session->error, 0, FALSE, TRUE)) {
					return FALSE;
				}
			}
		}
	}
	else {
		if (session->error != NULL) {
			if (!rspamd_dispatcher_write (session->dispatcher, session->error,
				0, FALSE, TRUE)) {
				return FALSE;
			}
		}
	}

	return TRUE;
}

/*
 * Called if something goes wrong
 */
static void
smtp_err_socket (GError * err, void *arg)
{
	struct smtp_session *session = arg;

	msg_info ("abnormally closing connection, error: %s", err->message);
	/* Free buffers */
	rspamd_session_destroy (session->s);
}

/*
 * Write greeting to client
 */
static gboolean
write_smtp_greeting (struct smtp_session *session)
{
	if (session->ctx->smtp_banner) {
		if (!rspamd_dispatcher_write (session->dispatcher,
			session->ctx->smtp_banner, 0, FALSE, TRUE)) {
			return FALSE;
		}
	}

	return TRUE;
}

/*
 * Return from a delay
 */
static void
smtp_delay_handler (gint fd, short what, void *arg)
{
	struct smtp_session *session = arg;

	rspamd_session_remove_event (session->s,
		(event_finalizer_t)event_del,
		session->delay_timer);
	if (session->state == SMTP_STATE_DELAY) {
		session->state = SMTP_STATE_GREETING;
		write_smtp_greeting (session);
	}
	else {
		session->state = SMTP_STATE_CRITICAL_ERROR;
		(void)smtp_write_socket (session);
	}
}

/*
 * Make delay for a client
 */
static void
smtp_make_delay (struct smtp_session *session)
{
	struct event *tev;
	struct timeval *tv;
	gint32 jitter;

	if (session->ctx->smtp_delay != 0 && session->state == SMTP_STATE_DELAY) {
		tev = rspamd_mempool_alloc (session->pool, sizeof (struct event));
		tv = rspamd_mempool_alloc (session->pool, sizeof (struct timeval));
		if (session->ctx->delay_jitter != 0) {
			jitter = g_random_int_range (0, session->ctx->delay_jitter);
			msec_to_tv (session->ctx->smtp_delay + jitter, tv);
		}
		else {
			msec_to_tv (session->ctx->smtp_delay, tv);
		}

		evtimer_set (tev, smtp_delay_handler, session);
		evtimer_add (tev, tv);
		rspamd_session_add_event (session->s,
			(event_finalizer_t)event_del,
			tev,
			g_quark_from_static_string ("smtp proxy"));
		session->delay_timer = tev;
	}
	else if (session->state == SMTP_STATE_DELAY) {
		session->state = SMTP_STATE_GREETING;
		write_smtp_greeting (session);
	}
}

/*
 * Handle DNS replies
 */
static void
smtp_dns_cb (struct rspamd_dns_reply *reply, void *arg)
{
	struct smtp_session *session = arg;
	gint res = 0;
	union rspamd_reply_element *elt;
	GList *cur;

	switch (session->state) {
	case SMTP_STATE_RESOLVE_REVERSE:
		/* Parse reverse reply and start resolve of this ip */
		if (reply->code != RDNS_RC_NOERROR) {
			rspamd_conditional_debug (rspamd_main->logger,
				session->client_addr.s_addr,
				__FUNCTION__,
				"DNS error: %s",
				dns_strerror (reply->code));

			if (reply->code == RDNS_RC_NXDOMAIN) {
				session->hostname = rspamd_mempool_strdup (session->pool,
						XCLIENT_HOST_UNAVAILABLE);
			}
			else {
				session->hostname = rspamd_mempool_strdup (session->pool,
						XCLIENT_HOST_TEMPFAIL);
			}
			session->state = SMTP_STATE_DELAY;
			smtp_make_delay (session);
		}
		else {
			if (reply->elements) {
				elt = reply->elements->data;
				session->hostname = rspamd_mempool_strdup (session->pool,
						elt->ptr.name);
				session->state = SMTP_STATE_RESOLVE_NORMAL;
				make_dns_request (session->resolver, session->s, session->pool,
					smtp_dns_cb, session, RDNS_REQUEST_A, session->hostname);

			}
		}
		break;
	case SMTP_STATE_RESOLVE_NORMAL:
		if (reply->code != RDNS_RC_NOERROR) {
			rspamd_conditional_debug (rspamd_main->logger,
				session->client_addr.s_addr,
				__FUNCTION__,
				"DNS error: %s",
				dns_strerror (reply->code));

			if (reply->code == RDNS_RC_NXDOMAIN) {
				session->hostname = rspamd_mempool_strdup (session->pool,
						XCLIENT_HOST_UNAVAILABLE);
			}
			else {
				session->hostname = rspamd_mempool_strdup (session->pool,
						XCLIENT_HOST_TEMPFAIL);
			}
			session->state = SMTP_STATE_DELAY;
			smtp_make_delay (session);
		}
		else {
			res = 0;
			cur = reply->elements;
			while (cur) {
				elt = cur->data;
				if (memcmp (&session->client_addr, &elt->a.addr[0],
					sizeof (struct in_addr)) == 0) {
					res = 1;
					session->resolved = TRUE;
					break;
				}
				cur = g_list_next (cur);
			}

			if (res == 0) {
				msg_info ("cannot find address for hostname: %s, ip: %s",
					session->hostname,
					inet_ntoa (session->client_addr));
				session->hostname = rspamd_mempool_strdup (session->pool,
						XCLIENT_HOST_UNAVAILABLE);
			}
			session->state = SMTP_STATE_DELAY;
			smtp_make_delay (session);
		}
		break;
	case SMTP_STATE_ERROR:
		session->state = SMTP_STATE_WRITE_ERROR;
		smtp_write_socket (session);
		break;
	default:
		/*
		 * This callback is called on unknown state, usually this indicates
		 * an error (invalid pipelining)
		 */
		break;
	}
}

/*
 * Accept new connection and construct task
 */
static void
accept_socket (gint fd, short what, void *arg)
{
	struct rspamd_worker *worker = (struct rspamd_worker *)arg;
	union sa_union su;
	struct smtp_session *session;
	struct smtp_worker_ctx *ctx;

	socklen_t addrlen = sizeof (su.ss);
	gint nfd;

	if ((nfd =
		accept_from_socket (fd, (struct sockaddr *)&su.ss, &addrlen)) == -1) {
		msg_warn ("accept failed: %s", strerror (errno));
		return;
	}
	/* Check for EAGAIN */
	if (nfd == 0) {
		return;
	}

	ctx = worker->ctx;
	session = g_malloc0 (sizeof (struct smtp_session));
	session->pool = rspamd_mempool_new (rspamd_mempool_suggest_size ());

	if (su.ss.ss_family == AF_UNIX) {
		msg_info ("accepted connection from unix socket");
		session->client_addr.s_addr = INADDR_NONE;
	}
	else if (su.ss.ss_family == AF_INET) {
		msg_info ("accepted connection from %s port %d",
			inet_ntoa (su.s4.sin_addr), ntohs (su.s4.sin_port));
		memcpy (&session->client_addr, &su.s4.sin_addr,
			sizeof (struct in_addr));
	}

	session->sock = nfd;
	session->temp_fd = -1;
	session->worker = worker;
	session->ctx = ctx;
	session->cfg = worker->srv->cfg;
	session->session_time = time (NULL);
	session->resolver = ctx->resolver;
	session->ev_base = ctx->ev_base;
	worker->srv->stat->connections_count++;

	/* Resolve client's addr */
	/* Set up async session */
	session->s = rspamd_session_create (session->pool,
			NULL,
			NULL,
			free_smtp_session,
			session);
	session->state = SMTP_STATE_RESOLVE_REVERSE;
	if (!make_dns_request (session->resolver, session->s, session->pool,
		smtp_dns_cb, session, RDNS_REQUEST_PTR, &session->client_addr)) {
		msg_err ("cannot resolve %s", inet_ntoa (session->client_addr));
		g_free (session);
		close (nfd);
		return;
	}
	else {
		session->dispatcher = rspamd_create_dispatcher (session->ev_base,
				nfd,
				BUFFER_LINE,
				smtp_read_socket,
				smtp_write_socket,
				smtp_err_socket,
				&session->ctx->smtp_timeout,
				session);
		session->dispatcher->peer_addr = session->client_addr.s_addr;
	}
}

static void
parse_smtp_banner (struct smtp_worker_ctx *ctx, const gchar *line)
{
	gint hostmax, banner_len = sizeof ("220 ") - 1;
	gchar *p, *t, *hostbuf = NULL;
	gboolean has_crlf = FALSE;

	p = (gchar *)line;
	while (*p) {
		if (*p == '%') {
			p++;
			switch (*p) {
			case 'n':
				/* Assume %n as CRLF */
				banner_len += sizeof (CRLF) - 1 + sizeof ("220 -") - 1;
				has_crlf = TRUE;
				break;
			case 'h':
				hostmax = sysconf (_SC_HOST_NAME_MAX) + 1;
				hostbuf = alloca (hostmax);
				gethostname (hostbuf, hostmax);
				hostbuf[hostmax - 1] = '\0';
				banner_len += strlen (hostbuf);
				break;
			case '%':
				banner_len += 1;
				break;
			default:
				banner_len += 2;
				break;
			}
		}
		else {
			banner_len++;
		}
		p++;
	}

	if (has_crlf) {
		banner_len += sizeof (CRLF "220 " CRLF);
	}
	else {
		banner_len += sizeof (CRLF);
	}

	ctx->smtp_banner = rspamd_mempool_alloc (ctx->pool, banner_len + 1);
	t = ctx->smtp_banner;
	p = (gchar *)line;

	if (has_crlf) {
		t = g_stpcpy (t, "220-");
	}
	else {
		t = g_stpcpy (t, "220 ");
	}

	while (*p) {
		if (*p == '%') {
			p++;
			switch (*p) {
			case 'n':
				/* Assume %n as CRLF */
				*t++ = CR; *t++ = LF;
				t = g_stpcpy (t, "220-");
				p++;
				break;
			case 'h':
				t = g_stpcpy (t, hostbuf);
				p++;
				break;
			case '%':
				*t++ = '%';
				p++;
				break;
			default:
				/* Copy all %<gchar> to dest */
				*t++ = *(p - 1); *t++ = *p;
				break;
			}
		}
		else {
			*t++ = *p++;
		}
	}
	if (has_crlf) {
		t = g_stpcpy (t, CRLF "220 " CRLF);
	}
	else {
		t = g_stpcpy (t, CRLF);
	}
}

static void
make_capabilities (struct smtp_worker_ctx *ctx, const gchar *line)
{
	gchar **strv, *p, *result, *hostbuf;
	guint32 num, i, len, hostmax;

	strv = g_strsplit_set (line, ",;", -1);
	num = g_strv_length (strv);

	hostmax = sysconf (_SC_HOST_NAME_MAX) + 1;
	hostbuf = alloca (hostmax);
	gethostname (hostbuf, hostmax);
	hostbuf[hostmax - 1] = '\0';

	len = sizeof ("250-") + strlen (hostbuf) + sizeof (CRLF) - 1;

	for (i = 0; i < num; i++) {
		p = strv[i];
		len += sizeof ("250-") + sizeof (CRLF) + strlen (p) - 2;
	}

	result = rspamd_mempool_alloc (ctx->pool, len);
	ctx->smtp_capabilities = result;

	p = result;
	if (num == 0) {
		p += rspamd_snprintf (p, len - (p - result), "250 %s" CRLF, hostbuf);
	}
	else {
		p += rspamd_snprintf (p, len - (p - result), "250-%s" CRLF, hostbuf);
		for (i = 0; i < num; i++) {
			if (i != num - 1) {
				p += rspamd_snprintf (p,
						len - (p - result),
						"250-%s" CRLF,
						strv[i]);
			}
			else {
				p += rspamd_snprintf (p,
						len - (p - result),
						"250 %s" CRLF,
						strv[i]);
			}
		}
	}

	g_strfreev (strv);
}

gpointer
init_smtp (struct rspamd_config *cfg)
{
	struct smtp_worker_ctx *ctx;
	GQuark type;

	type = g_quark_try_string ("smtp");

	ctx = g_malloc0 (sizeof (struct smtp_worker_ctx));
	ctx->pool = rspamd_mempool_new (rspamd_mempool_suggest_size ());

	/* Set default values */
	ctx->smtp_timeout_raw = 300000;
	ctx->smtp_delay = 0;
	ctx->smtp_banner = "220 ESMTP Ready." CRLF;
	bzero (ctx->smtp_filters, sizeof (GList *) * SMTP_STAGE_MAX);
	ctx->max_errors = DEFAULT_MAX_ERRORS;
	ctx->reject_message = DEFAULT_REJECT_MESSAGE;

	rspamd_rcl_register_worker_option (cfg, type, "upstreams",
		rspamd_rcl_parse_struct_string, ctx,
		G_STRUCT_OFFSET (struct smtp_worker_ctx, upstreams_str), 0);

	rspamd_rcl_register_worker_option (cfg, type, "banner",
		rspamd_rcl_parse_struct_string, ctx,
		G_STRUCT_OFFSET (struct smtp_worker_ctx, smtp_banner_str), 0);

	rspamd_rcl_register_worker_option (cfg, type, "timeout",
		rspamd_rcl_parse_struct_time, ctx,
		G_STRUCT_OFFSET (struct smtp_worker_ctx,
		smtp_timeout_raw), RSPAMD_CL_FLAG_TIME_UINT_32);

	rspamd_rcl_register_worker_option (cfg, type, "delay",
		rspamd_rcl_parse_struct_time, ctx,
		G_STRUCT_OFFSET (struct smtp_worker_ctx,
		smtp_delay), RSPAMD_CL_FLAG_TIME_UINT_32);

	rspamd_rcl_register_worker_option (cfg, type, "jitter",
		rspamd_rcl_parse_struct_time, ctx,
		G_STRUCT_OFFSET (struct smtp_worker_ctx,
		delay_jitter), RSPAMD_CL_FLAG_TIME_UINT_32);

	rspamd_rcl_register_worker_option (cfg, type, "capabilities",
		rspamd_rcl_parse_struct_string, ctx,
		G_STRUCT_OFFSET (struct smtp_worker_ctx, smtp_capabilities_str), 0);

	rspamd_rcl_register_worker_option (cfg, type, "xclient",
		rspamd_rcl_parse_struct_boolean, ctx,
		G_STRUCT_OFFSET (struct smtp_worker_ctx, use_xclient), 0);

	rspamd_rcl_register_worker_option (cfg, type, "reject_message",
		rspamd_rcl_parse_struct_string, ctx,
		G_STRUCT_OFFSET (struct smtp_worker_ctx, reject_message), 0);

	rspamd_rcl_register_worker_option (cfg, type, "max_errors",
		rspamd_rcl_parse_struct_integer, ctx,
		G_STRUCT_OFFSET (struct smtp_worker_ctx,
		max_errors), RSPAMD_CL_FLAG_INT_32);

	rspamd_rcl_register_worker_option (cfg, type, "max_size",
		rspamd_rcl_parse_struct_integer, ctx,
		G_STRUCT_OFFSET (struct smtp_worker_ctx,
		max_size), RSPAMD_CL_FLAG_INT_SIZE);

	return ctx;
}

/* Make post-init configuration */
static gboolean
config_smtp_worker (struct rspamd_worker *worker)
{
	struct smtp_worker_ctx *ctx = worker->ctx;
	gchar *value;

	/* Init timeval */
	msec_to_tv (ctx->smtp_timeout_raw, &ctx->smtp_timeout);

	/* Init upstreams */
	if ((value = ctx->upstreams_str) != NULL) {
		if (!parse_upstreams_line (ctx->pool, ctx->upstreams, value,
			&ctx->upstream_num)) {
			return FALSE;
		}
	}
	else {
		msg_err ("no upstreams defined, don't know what to do");
		return FALSE;
	}
	/* Create smtp banner */
	if ((value = ctx->smtp_banner_str) != NULL) {
		parse_smtp_banner (ctx, value);
	}

	/* Parse capabilities */
	if ((value = ctx->smtp_capabilities_str) != NULL) {
		make_capabilities (ctx, value);
	}

	return TRUE;
}


/*
 * Start worker process
 */
void
start_smtp (struct rspamd_worker *worker)
{
	struct smtp_worker_ctx *ctx = worker->ctx;

	ctx->ev_base = rspamd_prepare_worker (worker, "smtp_worker", accept_socket);

	/* Set smtp options */
	if ( !config_smtp_worker (worker)) {
		msg_err ("cannot configure smtp worker, exiting");
		exit (EXIT_SUCCESS);
	}

	/* Maps events */
	rspamd_map_watch (worker->srv->cfg, ctx->ev_base);

	/* DNS resolver */
	ctx->resolver = dns_resolver_init (ctx->ev_base, worker->srv->cfg);

	/* Set umask */
	umask (S_IWGRP | S_IWOTH | S_IROTH | S_IRGRP);

	event_base_loop (ctx->ev_base, 0);

	rspamd_log_close (rspamd_main->logger);
	exit (EXIT_SUCCESS);
}

void
register_smtp_filter (struct smtp_worker_ctx *ctx,
	enum rspamd_smtp_stage stage,
	smtp_filter_t filter,
	gpointer filter_data)
{
	struct smtp_filter *new;

	new = rspamd_mempool_alloc (ctx->pool, sizeof (struct smtp_filter));

	new->filter = filter;
	new->filter_data = filter_data;

	if (stage >= SMTP_STAGE_MAX) {
		msg_err ("invalid smtp stage: %d", stage);
	}
	else {
		ctx->smtp_filters[stage] =
			g_list_prepend (ctx->smtp_filters[stage], new);
	}
}

/*
 * vi:ts=4
 */