/* This file is part of Mailfromd.
   Copyright (C) 2005-2025 Sergey Poznyakoff

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3, or (at your option)
   any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>. */

#ifdef HAVE_CONFIG_H
# include <config.h>
#endif
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdarg.h>
#include <syslog.h>
#include <signal.h>
#include <pwd.h>
#include <grp.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <netdb.h>
#include <ctype.h>

#include <mailutils/mailutils.h>
#include <mailutils/daemon.h>

#include "libmf.h"
#include "srvcfg.h"
#include "callout.h"
#include "callout-dbgmod.h"

#define SMTP_MAJOR(c) ((c)/100)

struct smtp_io_state {
	int code;            /* Reply code */
	char *command;       /* Last issued command */
	char *reply;         /* Last received reply */
};

struct smtp_io_data {
	char *id;            /* I/O id */
	char *email;
	char *ehlo;
	char *mailfrom;
	smtp_starttls_t tls; /* STARTTLS support */

	time_t timeout[SMTP_NUM_TIMEOUT];
	smtp_io_callback_t callback;
	void *callback_closure;
	mu_stream_t stream;  /* I/O stream */
	mu_opool_t pool;     /* Opool for keeping commands/replies */
	size_t nlines;       /* Number of lines in the reply */
	char buf[128];       /* Input buffer */
	size_t level;        /* Number of bytes in buf */

	char *last_sent;     /* Printable version of the last issued command */
	char *last_recv;     /* Printable version of the last received reply */

	struct smtp_io_state state;

	char *capa;
};

static void
smtp_io_last_clear(struct smtp_io_data *iop)
{
	free(iop->last_sent);
	iop->last_sent = NULL;
	free(iop->last_recv);
	iop->last_recv = NULL;
}
	
static inline struct smtp_io_state
smtp_io_state_save(struct smtp_io_data *io)
{
	return io->state;
}

static inline void
smtp_io_state_restore(struct smtp_io_data *io, struct smtp_io_state state)
{
	io->state = state;
	smtp_io_last_clear(io);
}

struct smtp_io_data *
smtp_io_create(const char *id, time_t timeout[], smtp_io_callback_t callback,
	       void *closure)
{
	struct smtp_io_data *iop = mu_zalloc(sizeof(*iop));

	if (!id)
		iop->id = mu_strdup("null");
	else {
		size_t len;
		iop->id = mu_strdup(id);
		len = strlen(iop->id);
		if (len > 2 && strcmp(iop->id + len - 2, ": ") == 0)
			iop->id[len - 2] = 0;
	}
	memcpy(&iop->timeout, timeout, sizeof(iop->timeout));
	iop->callback = callback;
	iop->callback_closure = closure;
	mu_opool_create(&iop->pool, MU_OPOOL_ENOMEMABRT);
	iop->state.command = iop->state.reply = NULL;
	iop->nlines = 0;
	iop->level = 0;
	iop->tls = tls_never;
	return iop;
}

void
smtp_io_set_timeouts(struct smtp_io_data *iop, time_t *to)
{
	memcpy(&iop->timeout, to, sizeof(iop->timeout));
}

static void
replstr(char **pdst, const char *str)
{
	if (*pdst)
		free(*pdst);
	*pdst = mu_strdup(str);
}


void
smtp_io_init(struct smtp_io_data *iop)
{
	iop->state.code = 0;
	iop->state.command = iop->state.reply = NULL;
	iop->level = 0;
}

void
smtp_io_setup_callout(struct smtp_io_data *iop,
		      const char *email,
		      const char *ehlo,
		      const char *mailfrom,
		      smtp_starttls_t tls)
{
	replstr(&iop->email, email);
	replstr(&iop->ehlo, ehlo);
	if (!mailfrom || !*mailfrom)
		mailfrom = "<>";
	replstr(&iop->mailfrom, mailfrom);
	iop->tls = tls;
}

void
smtp_io_free(struct smtp_io_data *iop)
{
	if (iop->stream) {
		mu_stream_close(iop->stream);
		mu_stream_destroy(&iop->stream);
	}
	mu_opool_destroy(&iop->pool);
	free(iop->id);
	free(iop->email);
	free(iop->ehlo);
	free(iop->mailfrom);
	free(iop->last_sent);
	free(iop->last_recv);
	free(iop);
}


static int
smtp_wait(struct smtp_io_data *iop, int flags, struct timeout_ctl *tctl)
{
	return mf_stream_wait(iop->stream, flags, tctl);
}

static int
smtp_send(struct smtp_io_data *iop, const char *command)
{
	size_t len = strlen(command);
	struct timeout_ctl tctl;

	smtp_io_last_clear(iop);

	init_timeout_ctl (&tctl, io_timeout);

	iop->state.reply = NULL; /* Clear reply for logging purposes */
	do {
		size_t nb;
		int rc;

		UPDATE_TTW(tctl);

		rc = mu_stream_write(iop->stream, command, len, &nb);
		if (rc == 0) {
			if (nb == 0) {
				mu_error(_("%s: stream_write: wrote 0 bytes"),
					 iop->id);
				return -1;
			}
			len -= nb;
			command += nb;
		} else if (rc == EAGAIN) {
			rc = smtp_wait(iop, MU_STREAM_READY_WR, &tctl);
			if (rc) {
				mu_error(_("%s: smtp_wait failed: %s"),
					 iop->id, mu_strerror(rc));
				return -1;
			}
			continue;
		} else {
			mu_error("%s: mu_stream_write: %s",
				  iop->id, mu_strerror (rc));
			return -1;
		}
	} while (len > 0);
	return 0;
}

static int
smtp_send2(struct smtp_io_data *iop, const char *command, const char *arg)
{
	mu_opool_appendz(iop->pool, command);
	if (arg)
		mu_opool_appendz(iop->pool, arg);
	mu_opool_appendz(iop->pool, "\r\n");
	mu_opool_append_char(iop->pool, 0);
	iop->state.command = mu_opool_finish(iop->pool, NULL);

	return smtp_send(iop, iop->state.command);
}

static int
smtp_send3(struct smtp_io_data *iop, const char *command,
	   const char *arg1, const char *arg2)
{
	mu_opool_appendz(iop->pool, command);
	mu_opool_appendz(iop->pool, arg1);
	mu_opool_appendz(iop->pool, arg2);
	mu_opool_appendz(iop->pool, "\r\n");
	mu_opool_append_char(iop->pool, 0);
	iop->state.command = mu_opool_finish(iop->pool, NULL);

	return smtp_send(iop, iop->state.command);
}

static int
smtp_recvline(struct smtp_io_data *iop, enum smtp_timeout to,
	      char **ret_ptr, size_t *ret_plen)
{
	struct timeout_ctl tctl;
	size_t text_offset = mu_opool_size(iop->pool);

	init_timeout_ctl(&tctl, iop->timeout[to]);
	for (;;) {
		char *p;

		UPDATE_TTW(tctl);

		if (iop->level == 0) {
			int rc = mu_stream_read(iop->stream,
						iop->buf, sizeof iop->buf,
						&iop->level);
			if (rc == 0) {
				if (iop->level == 0) {
					mu_error(_("%s: stream_read: read 0 bytes"), iop->id);
					return -1;
				}
			} else if (rc == EAGAIN) {
				rc = smtp_wait(iop,
					       MU_STREAM_READY_RD, &tctl);
				if (rc) {
					mu_error(_("%s: smtp_wait failed: %s"),
						 iop->id, mu_strerror(rc));
					return -1;
				}
				continue;
			} else {
				mu_error("%s: mu_stream_read: %s",
					  iop->id, mu_strerror (rc));
				return -1;
			}
		}

		p = memchr(iop->buf, '\n', iop->level);
		if (!p) {
			mu_opool_append(iop->pool, iop->buf, iop->level);
			iop->level = 0;
			continue;
		} else {
			size_t sz, len = p - iop->buf + 1;
			mu_opool_append(iop->pool, iop->buf, len);
			mu_opool_append_char(iop->pool, 0);

			mu_opool_coalesce(iop->pool, NULL);
			p = mu_opool_head(iop->pool, &sz);
			*ret_ptr = p + text_offset;
			*ret_plen = sz - text_offset - 1;

			iop->level -= len;
			memmove(iop->buf, iop->buf + len, iop->level);
			break;
		}
	}
	return 0;
}

static int
smtp_recv(struct smtp_io_data *iop, enum smtp_timeout to)
{
	char *ptr, *p;
	size_t len;

	iop->nlines = 0;
	do {
		int code;
		int rc = smtp_recvline(iop, to, &ptr, &len);
		if (rc) {
			mu_opool_less(iop->pool, 0);
			return -1;
		}
		code = strtoul(ptr, &p, 0);
		if (p - ptr != 3 || (*p != '-' && *p != ' ')) {
			mu_error(_("%s: unexpected reply from server: %s"),
				 iop->id, ptr);
			mu_opool_less(iop->pool, 0);
			return -1;
		} else if (iop->nlines == 0) {
			iop->state.code = code;
		} else if (iop->state.code != code) {
			mu_error(_("%s: unexpected reply code from server: %d"),
				 iop->id, code);
			mu_opool_less(iop->pool, 0);
			return -1;
		}
		iop->nlines++;
	} while (*p == '-');
	mu_opool_append_char(iop->pool, 0);
	iop->state.reply = mu_opool_finish(iop->pool, NULL);

	return 0;
}

/* Return the first line (terminated by \n or \r\n) from STR,
   or the word "nothing" if STR is NULL.
   Returned value is allocated. */
static char *
first_line_of(char *str)
{
	if (str) {
		size_t len = strcspn(str, "\r\n");
		char *p = mu_alloc(len+1);
		memcpy(p, str, len);
		p[len] = 0;
		return p;
	}
	return mu_strdup("nothing");
}

const char *
smtp_last_sent(struct smtp_io_data *iop)
{
	if (!iop->last_sent)
		iop->last_sent = first_line_of(iop->state.command);
	return iop->last_sent;
}

const char *
smtp_last_received(struct smtp_io_data *iop)
{
	if (!iop->last_recv)
		iop->last_recv = first_line_of(iop->state.reply);
	return iop->last_recv;
}

const char *
smtp_io_id(struct smtp_io_data *iop)
{
	return iop->id;
}

const char *
smtp_io_email(struct smtp_io_data *iop)
{
	return iop->email;
}


/* Milter-specific functions */

static mf_status
reset(struct smtp_io_data *io)
{
	smtp_send2(io, "RSET", NULL);
	if (smtp_recv(io, smtp_timeout_rset))
		return mf_timeout;
	else if (SMTP_MAJOR(io->state.code) != 2)
		return SMTP_MAJOR(io->state.code) == 4 ?
			mf_temp_failure : mf_failure;
	return mf_success;
}

static mf_status
esmtp_vrfy(struct smtp_io_data *io)
{
	smtp_send2(io, "VRFY ", io->email);
	if (smtp_recv(io, smtp_timeout_rcpt))
		/* FIXME: Need a separate timeout? */
		return mf_timeout;
	if (io->state.code / 10 == 25)
		return mf_success;
	else if (SMTP_MAJOR(io->state.code) == 5)
		return mf_not_found;
	return mf_failure;
}

static char const *
smtp_io_capa_find(struct smtp_io_data *io, char const *capa)
{
	char *p;
	size_t capalen = strlen(capa);

	for (p = io->capa; *p; p += strlen(p) + 1) {
		size_t len = strlen(p) - 6;
		if (len >= capalen && memcmp(p + 4, capa, capalen) == 0 &&
		    p[4 + capalen] == '\r' || p[4 + capalen] == '=')
			return &p[4 + capalen];
	}
	return NULL;
}

mf_status smtp_io_reopen(struct smtp_io_data *io);
void smtp_io_close(struct smtp_io_data *io);

enum callout_mode {
	callout_init,          /* Initial call. */
	callout_restart,       /* Called after session restart. */
	callout_restart_ehlo   /* Called to restart from EHLO command. */
};

static mf_status
callout_io(struct smtp_io_data *io, const char *hostname, mu_address_t addr,
	   size_t i, enum callout_mode mode)
{
	size_t mailcount;
	mf_status status = 0;
	int rc;

	if (mode != callout_restart_ehlo) {
		if (io->callback)
			io->callback(io->callback_closure, "INIT", hostname);

		if (smtp_recv(io, smtp_timeout_initial))
			return mf_timeout;

		if (io->callback)
			io->callback(io->callback_closure, "GRTNG",
				     smtp_last_received(io));

		if (SMTP_MAJOR(io->state.code) != 2)
			return SMTP_MAJOR(io->state.code) == 4 ?
				mf_temp_failure : mf_not_found;
	}

	smtp_send2(io, "EHLO ", io->ehlo);
	if (smtp_recv(io, smtp_timeout_helo))
		return mf_timeout;

	if (SMTP_MAJOR(io->state.code) == 5) {
		/* Let's try HELO, then */
		smtp_send2(io, "HELO ", io->ehlo);
		if (smtp_recv(io, smtp_timeout_helo))
			return mf_not_found;
	}

	if (io->callback)
		io->callback(io->callback_closure,
			     "HELO", smtp_last_received(io));

	if (SMTP_MAJOR(io->state.code) != 2)
		return SMTP_MAJOR(io->state.code) == 4 ?
			mf_temp_failure : mf_not_found;

	io->capa = io->state.reply;

	if (WITH_MAILUTILS_GNUTLS && io->tls == tls_always &&
	    smtp_io_capa_find(io, "STARTTLS")) {
		smtp_send2(io, "STARTTLS", NULL);
		if (smtp_recv(io, smtp_timeout_mail)) {
			return mf_timeout;
		}

		if (SMTP_MAJOR(io->state.code) == 2) {
			rc = mu_starttls(&io->stream, &tls_conf, MU_TLS_CLIENT);
			/*
			 * Clear TLS as a precaution, to make sure we'll
			 * not get here again, even in the unlikely case if
			 * the second EHLO still reports STARTTLS.
			 */
			io->tls = tls_never;
			if (rc == 0) {
				/* Success */
				return callout_io(io, hostname, addr,
						  i, callout_restart_ehlo);
			} else {
				/*
				 * Failure.  According to RFC 3207, abort
				 * the connection and restart, if possible.
				 * If not, return mf_temp_failure.
				 */
				if (smtp_transcript) {
					mu_error(_("STARTTLS failed (%s)"),
						 mu_strerror(rc));
				}
				status = smtp_io_reopen(io);
				if (status != mf_success || mode != callout_init) {
					return mf_temp_failure;
				}
				if (smtp_transcript) {
					mu_error("%s", _("restarting session"));
				}
				return callout_io(io, hostname, addr,
						  i, callout_restart);
			}
		}
	}

	if (enable_vrfy && smtp_io_capa_find(io, "VRFY")) {
		status = esmtp_vrfy(io);
		if (mf_resolved(status))
			return status;
	}

	status = mf_success;
	if (i == 0) {
		i = 1;
		if ((rc = mu_address_get_count(addr, &mailcount)) != 0) {
			mu_diag_funcall(MU_DIAG_ERROR, "mu_address_get_count",
					NULL, rc);
			return mf_temp_failure;
		}
	} else {
		mailcount = i;
	}
	for (; i <= mailcount; i++) {
		const char *fromaddr;

		mu_address_sget_email(addr, i, &fromaddr);

		smtp_send3(io, "MAIL FROM:<", fromaddr, ">");

		if (smtp_recv(io, smtp_timeout_mail))
			return mf_timeout;
		else if (SMTP_MAJOR(io->state.code) == 2) {
			status = mf_success;
			break;
		} else if (SMTP_MAJOR(io->state.code) == 4) {
			/*
			 * Issue a RSET command.  If it fails, return
			 * immediately, but preserve the SMTP state
			 * from the previous command (return from
			 * RSET is not relevant for the result of the
			 * callout).
			 */
			struct smtp_io_state state = smtp_io_state_save(io);
			int rc = reset(io);
			smtp_io_state_restore(io, state);
			status = mf_temp_failure;
			if (rc)
				return status;
		} else if (WITH_MAILUTILS_GNUTLS &&
			   io->tls == tls_ondemand &&
			   io->state.code == 530 &&
			   smtp_io_capa_find(io, "STARTTLS")) {
			struct smtp_io_state state = smtp_io_state_save(io);

			smtp_send2(io, "STARTTLS", NULL);
			if (smtp_recv(io, smtp_timeout_mail)) {
				smtp_io_state_restore(io, state);
				return mf_timeout;
			}
			if (SMTP_MAJOR(io->state.code) == 2) {
				int rc = mu_starttls(&io->stream, &tls_conf, MU_TLS_CLIENT);
				if (rc == 0) {
					/* Success */
					return callout_io(io, hostname, addr,
							  i, callout_restart_ehlo);
				} else {
					if (smtp_transcript) {
						mu_error(_("STARTTLS failed (%s)"),
							 mu_strerror(rc));
					}
					smtp_io_close(io);
					return mf_temp_failure;
				}
			} else
				status = mf_temp_failure;
		} else
			status = mf_not_found;
	}
	if (status != mf_success)
		return status;

	smtp_send3(io, "RCPT TO:<", io->email, ">");
	if (smtp_recv(io, smtp_timeout_rcpt))
		return mf_timeout;
	else if (WITH_MAILUTILS_GNUTLS &&
		 io->tls == tls_ondemand &&
		 io->state.code == 530 &&
		 smtp_io_capa_find(io, "STARTTLS")) {
		/*
		 * Reportedly, some servers claim that TLS is required after
		 * RCPT TO command.  Although that contradicts the RFC, try
		 * to handle that case as well.
		 * Note, however, that 530 may well mean authentication (not
		 * TLS) is required.
		 */
		struct smtp_io_state state = smtp_io_state_save(io);

		if (reset(io)) {
			smtp_io_state_restore(io, state);
			return mf_failure;
		}
		io->tls = tls_always;
		return callout_io(io, hostname, addr, i, callout_restart_ehlo);
	} else if (SMTP_MAJOR(io->state.code) != 2)
		return SMTP_MAJOR(io->state.code) == 4 ?
				mf_temp_failure : mf_not_found;
	return mf_success;
}

static int
create_transcript_stream (mu_stream_t *pstream, struct smtp_io_data *io)
{
	int rc;
	mu_stream_t stream = *pstream;
	mu_stream_t dstr, xstr;
	char *fltargs[3] = { "INLINE-COMMENT", };

	rc = mu_dbgstream_create (&dstr, MU_DIAG_DEBUG);
	if (rc) {
		mu_error (_("cannot create debug stream: %s; "
			    "transcript disabled"),
			  mu_strerror (rc));
		return rc;
	}

	mu_asprintf (&fltargs[1], "%s: ", io->id);
	fltargs[2] = NULL;
	rc = mu_filter_create_args (&xstr, dstr,
				    "INLINE-COMMENT",
				    2, (const char**)fltargs,
				    MU_FILTER_ENCODE, MU_STREAM_WRITE);
	free (fltargs[1]);

	if (rc == 0) {
		mu_stream_unref(dstr);
		dstr = xstr;
		mu_stream_set_buffer (dstr, mu_buffer_line, 0);
	} else
		mu_error (_("cannot create transcript filter"
			    "stream: %s"), mu_strerror (rc));

	rc = mu_xscript_stream_create (&xstr, stream, dstr, NULL);
	if (rc)
		mu_error (_("cannot create transcript stream: %s; "
			    "transcript disabled"),
			  mu_strerror (rc));
	else {
		mu_stream_unref (stream);
		*pstream = xstr;
	}
	return rc;
}

mf_status
smtp_io_open(struct smtp_io_data *io, const char *hostname)
{
	int rc;
	mu_stream_t stream;
	struct timeout_ctl tctl;
	struct mu_sockaddr_hints hints;
	struct mu_sockaddr *address, *srcaddr;

	memset(&hints, 0, sizeof hints);
	hints.family = AF_INET;
	hints.socktype = SOCK_STREAM;
	hints.protocol = IPPROTO_TCP;
	hints.port = 25;
	rc = mu_sockaddr_from_node(&address, hostname, NULL, &hints);
	if (rc) {
		mu_error(_("cannot convert %s to sockaddr: %s"),
			 hostname, mu_strerror(rc));
		return mf_failure;
	}

	mu_sockaddr_copy(&srcaddr, source_address);
	rc = mu_tcp_stream_create_from_sa(&stream, address, source_address,
					  MU_STREAM_NONBLOCK);

	if (rc && !(rc == EAGAIN || rc == EINPROGRESS)) {
		mu_error(_("%s: cannot connect to `%s': %s"),
			 io->id, hostname,
			 mu_strerror(rc));
		mu_sockaddr_free(srcaddr);
		return mf_failure;
	}
	mu_stream_set_buffer(stream, mu_buffer_line, 0);

	init_timeout_ctl(&tctl, io->timeout[smtp_timeout_connect]);
	while (rc) {
		if ((rc == EAGAIN || rc == EINPROGRESS) && tctl.timeout) {
			rc = mf_stream_wait(stream, MU_STREAM_READY_WR,
					    &tctl);
			if (rc == 0) {
				UPDATE_TTW(tctl);
				rc = mu_stream_open(stream);
				continue;
			}
		}
		mu_error("%s: stream_open(%s): %s",
			 io->id, hostname, mu_strerror(rc));
		mu_stream_destroy(&stream);
		return mf_timeout;
	}
	mu_debug(MF_SOURCE_CALLOUT, MU_DEBUG_TRACE9, ("stream opened"));
	if (smtp_transcript)
		create_transcript_stream(&stream, io);

	io->stream = stream;
	return mf_success;
}

mf_status
smtp_io_reopen(struct smtp_io_data *io)
{
	struct timeout_ctl tctl;

	mu_debug(MF_SOURCE_CALLOUT, MU_DEBUG_TRACE1, ("%s: reopening stream",
						      io->id));
	mu_stream_close(io->stream);
	init_timeout_ctl(&tctl, io->timeout[smtp_timeout_connect]);
	for (;;) {
		int rc = mu_stream_open(io->stream);

		if (rc == 0)
			break;

		if ((rc == EAGAIN || rc == EINPROGRESS) && tctl.timeout) {
			rc = mf_stream_wait(io->stream, MU_STREAM_READY_WR, &tctl);
			if (rc == 0) {
				UPDATE_TTW(tctl);
				continue;
			}
		}

		mu_error("%s: smtp_io_reopen(): %s",
			 io->id, mu_strerror(rc));
		return mf_timeout;
	}
	mu_debug(MF_SOURCE_CALLOUT, MU_DEBUG_TRACE9, ("%s: stream reopened",
		 io->id));
	return mf_success;
}

void
smtp_io_close(struct smtp_io_data *io)
{
	if (io->stream) {
		mu_stream_close(io->stream);
		mu_stream_destroy(&io->stream);
	}
}

mf_status
callout_host(struct smtp_io_data *io, const char *hostname)
{
	int rc;
	mf_status status = mf_success;
	mu_address_t addr;
	const char *mailfrom;

	mu_debug(MF_SOURCE_CALLOUT, MU_DEBUG_TRACE5,
	      ("email = %s, hostname = %s",
	       io->email, hostname));

	smtp_io_init(io);
	status = smtp_io_open(io, hostname);
	if (status != mf_success)
		return status;

	/* FIXME-MU: compensate for mailutils deficiency */
	mailfrom = (io->mailfrom[0] == 0) ? "<>" : io->mailfrom;
	rc = mu_address_create(&addr, mailfrom);
	if (rc) {
		mu_error(_("%s: cannot create address `%s': %s"),
			 io->id, mailfrom, mu_strerror(rc));
		return mf_timeout;
	}

	status = callout_io(io, hostname, addr, 0, callout_init);

	mu_address_destroy(&addr);

	if (io->callback) {
		io->callback(io->callback_closure,
			     "SENT", smtp_last_sent(io));
		io->callback(io->callback_closure,
			     "RECV", smtp_last_received(io));
	}

	mu_debug(MF_SOURCE_CALLOUT, MU_DEBUG_TRACE0,
		 ("%s: verification of <%s> finished with status: %s; sent \"%s\", got \"%s\"",
		 io->id,
		 smtp_io_email(io),
		 mf_status_str(status),
		 smtp_last_sent(io),
		 smtp_last_received(io)));

	if (io->stream) {
		smtp_send2(io, "QUIT", NULL);
		smtp_recv(io, smtp_timeout_quit);

		smtp_io_close(io);
	}

	return status;
}

mf_status
callout_mx(struct smtp_io_data *iop, const char *hostname, int *pcount)
{
	int i;
	struct dns_reply reply;
	mf_status mxstat, status;
	static int weight[] = {
		[mf_success] = 0,
		[mf_not_found] = 100,
		[mf_failure] = 20,
		[mf_temp_failure] = 10,
		[mf_timeout] = 15
	};

	mxstat = dns_to_mf_status(mx_lookup(hostname, 0, &reply));

	if (pcount)
		*pcount = 0;
	switch (mxstat) {
	case mf_success:
		mu_debug(MF_SOURCE_CALLOUT, MU_DEBUG_TRACE1,
			 ("Checking MX servers for %s", iop->email));
		status = mf_not_found;
		for (i = 0; i < reply.count; i++) {
			mf_status rc = callout_host(iop, reply.data.str[i]);
			if (mf_resolved(rc)) {
				status = rc;
				break;
			} else if (weight[status] > weight[rc])
				status = rc;
		}
		if (pcount)
			*pcount = reply.count;
		dns_reply_free(&reply);
		break;

	default:
		status = mxstat;
		break;
	}
	return status;
}

/* Method "strict". Verifies whether EMAIL is understood either by
   host CLIENT_ADDR or one of MX servers of its domain */
mf_status
callout_strict(struct smtp_io_data *iop, const char *hostname)
{
	mf_status rc;

	rc = callout_host(iop, hostname);
	if (!mf_resolved(rc)) {
		int mxcount;
		mf_status mx_stat;
		mx_stat = callout_mx(iop, hostname, &mxcount);
		if (!(mx_stat == mf_not_found && mxcount == 0)
		    && (mf_resolved(mx_stat)
			|| mx_stat == mf_timeout
			|| mx_stat == mf_temp_failure))
			rc = mx_stat;
	}
	return rc;
}

mf_status
callout_standard(struct smtp_io_data *iop)
{
	int rc;

	char *p = strchr(iop->email, '@');
	if (p == NULL) {
		mu_error(_("%s: invalid address: %s"), iop->id, iop->email);
		rc = mf_not_found;
	} else {
		int mxcount;
		p++;
		rc = callout_mx(iop, p, &mxcount);
		if (rc != mf_success && mxcount == 0) {
			mf_status host_stat;
			host_stat = callout_host(iop, p);
			if (mf_resolved(host_stat)
			    || host_stat == mf_timeout
			    || host_stat == mf_temp_failure)
				rc = host_stat;
		}
	}
	return rc;
}

static char *modnames[] = {
#define __DBGMOD_C_ARRAY
# include "callout-dbgmod.h"
#undef __DBGMOD_C_ARRAY
	NULL
};

mu_debug_handle_t callout_debug_handle;

void
libcallout_init()
{
	int i;

	callout_debug_handle = mu_debug_register_category (modnames[0]);
	for (i = 1; modnames[i]; i++)
		mu_debug_register_category (modnames[i]);
}
