/*++
/* NAME
/*	smtp_trouble 3
/* SUMMARY
/*	error handler policies
/* SYNOPSIS
/*	#include "smtp.h"
/*
/*	int	smtp_sess_fail(state)
/*	SMTP_STATE *state;
/*
/*	int	smtp_site_fail(state, mta_name, resp, format, ...)
/*	SMTP_STATE *state;
/*	const char *mta_name;
/*	SMTP_RESP *resp;
/*	const char *format;
/*
/*	int	smtp_mesg_fail(state, mta_name, resp, format, ...)
/*	SMTP_STATE *state;
/*	const char *mta_name;
/*	SMTP_RESP *resp;
/*	const char *format;
/*
/*	void	smtp_rcpt_fail(state, recipient, mta_name, resp, format, ...)
/*	SMTP_STATE *state;
/*	RECIPIENT *recipient;
/*	const char *mta_name;
/*	SMTP_RESP *resp;
/*	const char *format;
/*
/*	int	smtp_stream_except(state, exception, description)
/*	SMTP_STATE *state;
/*	int	exception;
/*	const char *description;
/* AUXILIARY FUNCTIONS
/*	int	smtp_misc_fail(state, throttle, mta_name, resp, format, ...)
/*	SMTP_STATE *state;
/*	int	throttle;
/*	const char *mta_name;
/*	SMTP_RESP *resp;
/*	const char *format;
/* DESCRIPTION
/*	This module handles all non-fatal errors that can happen while
/*	attempting to deliver mail via SMTP, and implements the policy
/*	of how to deal with the error. Depending on the nature of
/*	the problem, delivery of a single message is deferred, delivery
/*	of all messages to the same domain is deferred, or one or more
/*	recipients are given up as non-deliverable and a bounce log is
/*	updated. In any case, the recipient is marked as either KEEP
/*	(try again with a backup host) or DROP (delete recipient from
/*	delivery request).
/*
/*	In addition, when an unexpected response code is seen such
/*	as 3xx where only 4xx or 5xx are expected, or any error code
/*	that suggests a syntax error or something similar, the
/*	protocol error flag is set so that the postmaster receives
/*	a transcript of the session. No notification is generated for
/*	what appear to be configuration errors - very likely, they
/*	would suffer the same problem and just cause more trouble.
/*
/*	In case of a soft error, action depends on whether the error
/*	qualifies for trying the request with other mail servers (log
/*	an informational record only and try a backup server) or
/*	whether this is the final server (log recipient delivery status
/*	records and delete the recipient from the request).
/*
/*	smtp_sess_fail() takes a pre-formatted error report after
/*	failure to complete some protocol handshake.  The policy is
/*	as with smtp_site_fail().
/*
/*	smtp_site_fail() handles the case where the program fails to
/*	complete the initial handshake: the server is not reachable,
/*	is not running, does not want talk to us, or we talk to ourselves.
/*	The \fIcode\fR gives an error status code; the \fIformat\fR
/*	argument gives a textual description.
/*	The policy is: soft error, non-final server: log an informational
/*	record why the host is being skipped; soft error, final server:
/*	defer delivery of all remaining recipients and mark the destination
/*	as problematic; hard error: bounce all remaining recipients.
/*	The session is marked as "do not cache".
/*	The result is non-zero.
/*
/*	smtp_mesg_fail() handles the case where the smtp server
/*	does not accept the sender address or the message data,
/*	or when the local MTA is unable to convert the message data.
/*	The policy is: soft error, non-final server: log an informational
/*	record why the host is being skipped; soft error, final server:
/*	defer delivery of all remaining recipients; hard error: bounce all
/*	remaining recipients.
/*	The result is non-zero.
/*
/*	smtp_misc_fail() provides a more detailed interface than
/*	smtp_site_fail() and smtp_mesg_fail(), which are convenience
/*	wrappers around smtp_misc_fail(). The throttle argument
/*	is either SMTP_THROTTLE or SMTP_NOTHROTTLE; it is used only
/*	in the "soft error, final server" policy, and determines
/*	whether a destination will be marked as problematic.
/*
/*	smtp_rcpt_fail() handles the case where a recipient is not
/*	accepted by the server for reasons other than that the server
/*	recipient limit is reached.
/*	The policy is: soft error, non-final server: log an informational
/*	record why the recipient is being skipped; soft error, final server:
/*	defer delivery of this recipient; hard error: bounce this
/*	recipient.
/*
/*	smtp_stream_except() handles the exceptions generated by
/*	the smtp_stream(3) module (i.e. timeouts and I/O errors).
/*	The \fIexception\fR argument specifies the type of problem.
/*	The \fIdescription\fR argument describes at what stage of
/*	the SMTP dialog the problem happened.
/*	The policy is: non-final server: log an informational record
/*	with the reason why the host is being skipped; final server:
/*	defer delivery of all remaining recipients.
/*	Retry plaintext delivery after TLS post-handshake session
/*	failure, provided that at least one recipient was not
/*	deferred or rejected during the TLS phase, and that global
/*	preconditions for plaintext fallback are met.
/*	The session is marked as "do not cache".
/*	The result is non-zero.
/*
/*	Arguments:
/* .IP state
/*	SMTP client state per delivery request.
/* .IP resp
/*	Server response including reply code and text.
/* .IP recipient
/*	Undeliverable recipient address information.
/* .IP format
/*	Human-readable description of why mail is not deliverable.
/* DIAGNOSTICS
/*	Panic: unknown exception code.
/* SEE ALSO
/*	smtp_proto(3) smtp high-level protocol
/*	smtp_stream(3) smtp low-level protocol
/*	defer(3) basic message defer interface
/*	bounce(3) basic message bounce interface
/* LICENSE
/* .ad
/* .fi
/*	The Secure Mailer license must be distributed with this software.
/* AUTHOR(S)
/*	Wietse Venema
/*	IBM T.J. Watson Research
/*	P.O. Box 704
/*	Yorktown Heights, NY 10598, USA
/*--*/

/* System library. */

#include <sys_defs.h>
#include <stdlib.h>			/* 44BSD stdarg.h uses abort() */
#include <stdarg.h>
#include <string.h>

/* Utility library. */

#include <msg.h>
#include <vstring.h>
#include <stringops.h>

/* Global library. */

#include <smtp_stream.h>
#include <deliver_request.h>
#include <deliver_completed.h>
#include <bounce.h>
#include <defer.h>
#include <mail_error.h>
#include <dsn_buf.h>
#include <dsn.h>
#include <mail_params.h>

/* Application-specific. */

#include "smtp.h"
#include "smtp_sasl.h"

/* smtp_check_code - check response code */

static void smtp_check_code(SMTP_SESSION *session, int code)
{

    /*
     * The intention of this code is to alert the postmaster when the local
     * Postfix SMTP client screws up, protocol wise. RFC 821 says that x0z
     * replies "refer to syntax errors, syntactically correct commands that
     * don't fit any functional category, and unimplemented or superfluous
     * commands". Unfortunately, this also triggers postmaster notices when
     * remote servers screw up, protocol wise. This is becoming a common
     * problem now that response codes are configured manually as part of
     * anti-UCE systems, by people who aren't aware of RFC details.
     */
    if (code < 400 || code > 599
	|| code == 555			/* RFC 1869, section 6.1. */
	|| (code >= 500 && code < 510))
	session->error_mask |= MAIL_ERROR_PROTOCOL;
}

/* smtp_bulk_fail - skip, defer or bounce recipients, maybe throttle queue */

static int smtp_bulk_fail(SMTP_STATE *state, int throttle_queue)
{
    DELIVER_REQUEST *request = state->request;
    SMTP_SESSION *session = state->session;
    DSN_BUF *why = state->why;
    RECIPIENT *rcpt;
    int     status;
    int     aggregate_status;
    int     soft_error = (STR(why->status)[0] == '4');
    int     soft_bounce_error = (STR(why->status)[0] == '5' && var_soft_bounce);
    int     nrcpt;

    /*
     * Don't defer the recipients just yet when this error qualifies them for
     * delivery to a backup server. Just log something informative to show
     * why we're skipping this host.
     */
    if ((soft_error || soft_bounce_error)
	&& (state->misc_flags & SMTP_MISC_FLAG_FINAL_SERVER) == 0) {
	msg_info("%s: %s", request->queue_id, STR(why->reason));
	for (nrcpt = 0; nrcpt < SMTP_RCPT_LEFT(state); nrcpt++) {
	    rcpt = request->rcpt_list.info + nrcpt;
	    if (SMTP_RCPT_ISMARKED(rcpt))
		continue;
	    SMTP_RCPT_KEEP(state, rcpt);
	}
    }

    /*
     * Defer or bounce all the remaining recipients, and delete them from the
     * delivery request. If a bounce fails, defer instead and do not qualify
     * the recipient for delivery to a backup server.
     */
    else {

	/*
	 * If we are still in the connection set-up phase, update the set-up
	 * completion time here, otherwise the time spent in set-up latency
	 * will be attributed as message transfer latency.
	 * 
	 * All remaining recipients have failed at this point, so we update the
	 * delivery completion time stamp so that multiple recipient status
	 * records show the same delay values.
	 */
	if (request->msg_stats.conn_setup_done.tv_sec == 0) {
	    GETTIMEOFDAY(&request->msg_stats.conn_setup_done);
	    request->msg_stats.deliver_done =
		request->msg_stats.conn_setup_done;
	} else
	    GETTIMEOFDAY(&request->msg_stats.deliver_done);

	(void) DSN_FROM_DSN_BUF(why);
	aggregate_status = 0;
	for (nrcpt = 0; nrcpt < SMTP_RCPT_LEFT(state); nrcpt++) {
	    rcpt = request->rcpt_list.info + nrcpt;
	    if (SMTP_RCPT_ISMARKED(rcpt))
		continue;
	    status = (soft_error ? defer_append : bounce_append)
		(DEL_REQ_TRACE_FLAGS(request->flags), request->queue_id,
		 &request->msg_stats, rcpt,
		 session ? session->namaddrport : "none", &why->dsn);
	    if (status == 0)
		deliver_completed(state->src, rcpt->offset);
	    SMTP_RCPT_DROP(state, rcpt);
	    aggregate_status |= status;
	}
	state->status |= aggregate_status;
	if ((state->misc_flags & SMTP_MISC_FLAG_COMPLETE_SESSION) == 0
	    && throttle_queue && aggregate_status
	    && request->hop_status == 0)
	    request->hop_status = DSN_COPY(&why->dsn);
    }

    /*
     * Don't cache this session. We can't talk to this server.
     */
    if (throttle_queue && session)
	DONT_CACHE_THROTTLED_SESSION;

    return (-1);
}

/* smtp_sess_fail - skip site, defer or bounce all recipients */

int     smtp_sess_fail(SMTP_STATE *state)
{

    /*
     * We can't avoid copying copying lots of strings into VSTRING buffers,
     * because this error information is collected by a routine that
     * terminates BEFORE the error is reported.
     */
    return (smtp_bulk_fail(state, SMTP_THROTTLE));
}

/* vsmtp_fill_dsn - fill in temporary DSN structure */

static void vsmtp_fill_dsn(SMTP_STATE *state, const char *mta_name,
			           const char *status, const char *reply,
			           const char *format, va_list ap)
{
    DSN_BUF *why = state->why;

    /*
     * We could avoid copying lots of strings into VSTRING buffers, because
     * this error information is given to us by a routine that terminates
     * AFTER the error is reported. However, this results in ugly kludges
     * when informal text needs to be formatted. So we maintain consistency
     * with other error reporting in the SMTP client even if we waste a few
     * cycles.
     */
    VSTRING_RESET(why->reason);
    if (mta_name && status && status[0] != '4' && status[0] != '5') {
	vstring_strcpy(why->reason, "Protocol error: ");
	status = "5.5.0";
    }
    vstring_vsprintf_append(why->reason, format, ap);
    dsb_formal(why, status, DSB_DEF_ACTION,
	       mta_name ? DSB_MTYPE_DNS : DSB_MTYPE_NONE, mta_name,
	       reply ? DSB_DTYPE_SMTP : DSB_DTYPE_NONE, reply);
}

/* smtp_misc_fail - maybe throttle queue; skip/defer/bounce all recipients */

int     smtp_misc_fail(SMTP_STATE *state, int throttle, const char *mta_name, 
				SMTP_RESP *resp, const char *format,...)
{
    va_list ap;

    /*
     * Initialize.
     */
    va_start(ap, format);
    vsmtp_fill_dsn(state, mta_name, resp->dsn, resp->str, format, ap);
    va_end(ap);

    if (state->session && mta_name)
	smtp_check_code(state->session, resp->code);

    /*
     * Skip, defer or bounce recipients, and throttle this queue.
     */
    return (smtp_bulk_fail(state, throttle));
}

/* smtp_rcpt_fail - skip, defer, or bounce recipient */

void    smtp_rcpt_fail(SMTP_STATE *state, RECIPIENT *rcpt, const char *mta_name,
		               SMTP_RESP *resp, const char *format,...)
{
    DELIVER_REQUEST *request = state->request;
    SMTP_SESSION *session = state->session;
    DSN_BUF *why = state->why;
    int     status;
    int     soft_error;
    int     soft_bounce_error;
    va_list ap;

    /*
     * Sanity check.
     */
    if (SMTP_RCPT_ISMARKED(rcpt))
	msg_panic("smtp_rcpt_fail: recipient <%s> is marked", rcpt->address);

    /*
     * Initialize.
     */
    va_start(ap, format);
    vsmtp_fill_dsn(state, mta_name, resp->dsn, resp->str, format, ap);
    va_end(ap);
    soft_error = STR(why->status)[0] == '4';
    soft_bounce_error = (STR(why->status)[0] == '5' && var_soft_bounce);

    if (state->session && mta_name)
	smtp_check_code(state->session, resp->code);

    /*
     * Don't defer this recipient record just yet when this error qualifies
     * for trying other mail servers. Just log something informative to show
     * why we're skipping this recipient now.
     */
    if ((soft_error || soft_bounce_error)
	&& (state->misc_flags & SMTP_MISC_FLAG_FINAL_SERVER) == 0) {
	msg_info("%s: %s", request->queue_id, STR(why->reason));
	SMTP_RCPT_KEEP(state, rcpt);
    }

    /*
     * Defer or bounce this recipient, and delete from the delivery request.
     * If the bounce fails, defer instead and do not qualify the recipient
     * for delivery to a backup server.
     * 
     * Note: we may still make an SMTP connection to deliver other recipients
     * that did qualify for delivery to a backup server.
     */
    else {
	(void) DSN_FROM_DSN_BUF(state->why);
	status = (soft_error ? defer_append : bounce_append)
	    (DEL_REQ_TRACE_FLAGS(request->flags), request->queue_id,
	     &request->msg_stats, rcpt,
	     session ? session->namaddrport : "none", &why->dsn);
	if (status == 0)
	    deliver_completed(state->src, rcpt->offset);
	SMTP_RCPT_DROP(state, rcpt);
	state->status |= status;
    }
}

/* smtp_stream_except - defer domain after I/O problem */

int     smtp_stream_except(SMTP_STATE *state, int code, const char *description)
{
    SMTP_SESSION *session = state->session;
    DSN_BUF *why = state->why;

    /*
     * Sanity check.
     */
    if (session == 0)
	msg_panic("smtp_stream_except: no session");

    /*
     * Initialize.
     */
    switch (code) {
    default:
	msg_panic("smtp_stream_except: unknown exception %d", code);
    case SMTP_ERR_EOF:
	dsb_simple(why, "4.4.2", "lost connection with %s while %s",
		   session->namaddr, description);
#ifdef USE_TLS
	if (PLAINTEXT_FALLBACK_OK_AFTER_TLS_SESSION_FAILURE)
	    RETRY_AS_PLAINTEXT;
#endif
	break;
    case SMTP_ERR_TIME:
	dsb_simple(why, "4.4.2", "conversation with %s timed out while %s",
		   session->namaddr, description);
#ifdef USE_TLS
	if (PLAINTEXT_FALLBACK_OK_AFTER_TLS_SESSION_FAILURE)
	    RETRY_AS_PLAINTEXT;
#endif
	break;
    case SMTP_ERR_DATA:
	session->error_mask |= MAIL_ERROR_DATA;
	dsb_simple(why, "4.3.0", "local data error while talking to %s",
		   session->namaddr);
    }

    /*
     * The smtp_bulk_fail() call below will not throttle the destination when
     * falling back to plaintext, because RETRY_AS_PLAINTEXT clears the
     * FINAL_SERVER flag.
     */
    return (smtp_bulk_fail(state, SMTP_THROTTLE));
}
