/*
#ident	"@(#)smail/src/transports:RELEASE-3_2_0_108:smtplib.c,v 1.38 1999/09/19 22:22:48 woods Exp"
 */

/*
 *    Copyright (C) 1987, 1988 Ronald S. Karr and Landon Curt Noll
 *    Copyright (C) 1992  Ronald S. Karr
 * 
 * See the file COPYING, distributed with smail, for restriction
 * and warranty information.
 */

/*
 * smtplib.c:
 *	Send mail using the SMTP protocol.  This soure file is a set
 *	of library routines that can be used by transport drivers that
 *	can handle creating the virtual circuit connections.
 *
 * A NOTE ABOUT SMTP ERROR LOGGING:
 *
 * Note that the reply from the remote server should be logged (with
 * write_log() and/or note_error(), as appropriate) starting with a preceding
 * newline so that entire mult-line replies will remain intact and properly
 * formatted in the log files.  There is code in log.c send_log(), for example,
 * which properly parses these messages in such a way that they will be treated
 * as the single entry that they are.
 */
#include <sys/types.h>
#include <stdio.h>
#include <signal.h>
#include <ctype.h>
#include "defs.h"
#if defined(POSIX_OS)
# include <unistd.h>
#else /* not POSIX_OS */
# if defined (HAVE_UNISTD_H)
#  include <unistd.h>
# endif
#endif	/* not POSIX_OS */

#include "../smail.h"
#include "../dys.h"
#include "../jump.h"
#include "../addr.h"
#include "../transport.h"
#include "../spool.h"
#include "smtplib.h"
#ifndef DEPEND
# include "../extern.h"
# include "../error.h"
# include "../debug.h"
#endif
#if !defined(NO_LOG_EHLO)
#  include "../log.h"
#endif

#ifdef ANSI_C
# define P_(x)  x
# define VOLATILE volatile
#else
# define P_(x)  ()
# define VOLATILE /**/
#endif

/* supported SMTP commands */
#define EHLO(domain)	"EHLO %s", domain
#define HELO(domain)	"HELO %s", domain
#define MAIL_BEGIN	"MAIL FROM:<"
#define MAIL_END	">"
#define RCPT_BEGIN	"RCPT TO:<"
#define RCPT_END	">"
#define DATA_BEGIN	"DATA"
#define DATA_END	"."
#define QUIT		"QUIT"
#define VERB		"VERB"
#define CMD_RESET	"RSET"

/* reply code groups, encoded in hex */
#define POSITIVE_DEBUG		0x000	/* debugging messages */
#define POSITIVE_PRELIM		0x100	/* positive preliminary replies */
#define POSITIVE_COMPLETE	0x200	/* positive completion replies */
#define POSITIVE_INTERMEDIATE	0x300	/* positive intermediate replies */
#define NEGATIVE_TRY_AGAIN	0x400	/* transient negative completion */
#define NEGATIVE_FAILED		0x500	/* permanent negative completion */

#define REPLY_GROUP(c)	   ((c)&0xf00)	/* mask out reply code group */

/* specific reply codes */
#define REPLY_READY		0x220	/* SMTP service ready */
#define REPLY_FINISHED		0x221	/* SMTP service finished, closing */
#define REPLY_OK		0x250	/* successful command completion */
#define REPLY_WILLFORWARD	0x251	/* user address will be forwarded */
#define REPLY_START_DATA	0x354	/* okay to start sending message */
#define REPLY_DOWN		0x421	/* remote SMTP closing down */
#define REPLY_MAILBOX_UNAVAIL	0x450	/* mailbox busy */
#define REPLY_REMOTE_FAILURE	0x451	/* remote error in processing */
#define REPLY_STORAGE_FULL	0x452	/* remote system storage full */
#define REPLY_COMMAND_ERROR	0x500	/* Syntax error in command */
#define REPLY_PARAMETER_ERROR	0x501	/* Syntax error in parameter(s) */
#define REPLY_CMD_NOT_IMPL	0x502	/* Command recognized but not implemented */
#define REPLY_SEQUENCE_ERROR	0x503	/* Bad sequence of commands */
#define REPLY_BAD_PARAMETER	0x504	/* Parameter recognized but not implemented */
#define REPLY_UNKNOWN_USER	0x550	/* remote says mailbox unknown */
#define REPLY_USER_NOT_LOCAL	0x551	/* remote says mailbox is not local */
#define REPLY_OVER_QUOTA	0x552	/* remote says mailbox exceeded storage quota */
#define REPLY_ILLEGAL_USER	0x553	/* remote says mailbox name not allowed */
#define REPLY_TRANS_FAILURE	0x554	/* transaction failed (no SMTP service avail) */
#define REPLY_ESMTP_PARAM_ERR	0x555	/* unsupported or invalid ESMTP RCPT_TO or MAIL_FROM parameter */
/* pseudo-reply codes */
#define REPLY_PROTO_ERROR	0x498	/* protocol error on read */
#define REPLY_TIMEOUT		0x499	/* timeout on read, or EOF on read */

/* variables local to this file */
static struct str smtp_out;		/* region for outgoing commands */
static struct str smtp_in;		/* region from incoming responses */
int smtp_init_flag = FALSE;		/* TRUE if SMTP system initialized */
static JUMP_ENVBUF timeout_buf;		/* timeouts jump here */

/* functions local to this file */
#ifdef __STDC__
extern int smtp_startup(struct smtp *, struct error **, int);
static char *skip_to_eol(char *);
static void log_malformed_ehlo_reply(char *, char *);
extern int smtp_send(struct smtp *, struct addr *, struct addr **, struct addr **, struct addr **, struct error **);
extern void smtp_shutdown(struct smtp *);
static void do_smtp_shutdown(struct smtp *, int);
static int write_command_maybe_wait(struct smtp *, unsigned, char *, int, char **);
static int wait_write_command(struct smtp *, unsigned, char *, int, char **);
static int write_command_nowait(struct smtp *, char *, int);
static int flush_command_stream(struct smtp *, char **);
static int wait_read_response(struct smtp *, unsigned, char **);
static int read_response_internal(struct smtp *, unsigned, char **);
static void catch_timeout(int);
static struct error * no_remote(struct transport *, char *);
static struct error * try_again(struct transport *, char *);
static struct error * fatal_error(struct transport *, char *);
static struct error * remote_storage_full(struct transport *, char *);
static struct error * remote_mailbox_unavail(struct transport *, char *);
static struct error * remote_failure(struct transport *, char *);
static struct error * remote_unknown_user(struct transport *, char *);
static struct error * remote_user_not_local(struct transport *, char *);
static struct error * remote_over_quota(struct transport *, char *);
static struct error * remote_illegal_user(struct transport *, char *);
static struct error * write_failed(struct transport *);
static struct error * read_failed(struct transport *); 
#else /* not __STDC__ */
extern int smtp_startup();
static char *skip_to_eol();
static void log_malformed_ehlo_reply();
extern int smtp_send();
extern void smtp_shutdown();
static void do_smtp_shutdown();
static int write_command_maybe_wait();
static int wait_write_command();
static int write_command_nowait();
static int flush_command_stream();
static int wait_read_response();
static int read_response_internal();
static void catch_timeout();
static struct error * no_remote();
static struct error * try_again();
static struct error * fatal_error();
static struct error * remote_storage_full();
static struct error * remote_mailbox_unavail();
static struct error * remote_failure();
static struct error * remote_unknown_user();
static struct error * remote_user_not_local();
static struct error * remote_over_quota();
static struct error * remote_illegal_user();
static struct error * write_failed();
static struct error * read_failed(); 
#endif /* not __STDC__ */


/*
 * smtp_startup - initiate contact on an SMTP connection
 *
 * given input and output channels to a remote SMTP process, initiate
 * the session for future mail commands.  Once the startup has been
 * acomplished, smtp_send() can be used to send individual messages.
 *
 * If try_ehlo is non-zero and ESMTP support is configured, perform
 * ESMTP negotiaition using the EHLO greeting command.
 *
 * return:
 *	SMTP_SUCCEED on successful startup
 *	SMTP_FAIL if the connection should not be retried
 *	SMTP_AGAIN if the connection should be retried later
 *	SMTP_EHLO_FAIL if the receiver hung up in response
 *		to an EHLO command.  This can only occur when
 *		try_ehlo is set.
 *
 * For SMTP_FAIL and SMTP_AGAIN (but not SMTP_EHLO_FAIL), return a
 * filled-in error structure.
 */
int
smtp_startup(smtpb, error_p, try_ehlo)
    struct smtp *smtpb;			/* SMTP description block */
    struct error **error_p;		/* error description */
    int try_ehlo;			/* whether to use ESMTP (UNUSED!) */
{
    int reply = REPLY_OK;		/* If we're in batch mode, always
					 * return success. */
    char *reply_text;
#ifdef HAVE_EHLO
    int tried_rset = 0;
#endif

    if (! smtp_init_flag) {
	STR_INIT(&smtp_in);
	STR_INIT(&smtp_out);
	smtp_init_flag = TRUE;
    }

    /*
     * wait for the sender to say he is ready.
     * Possible reponses:
     *	220 - service ready (continue conversation)
     *  421 - closing down  (try again later)
     */
    if (smtpb->in) {
	reply = wait_read_response(smtpb, smtpb->short_timeout, &reply_text);
    }

    if (reply != REPLY_READY) {
	/* didn't get an OK reponse, try again later */
	*error_p = no_remote(smtpb->tp, reply_text);
	return SMTP_AGAIN;
    }

#ifdef HAVE_EHLO
    if (try_ehlo) {
	/*
	 * say who we are.
	 * Possible responses:
	 *	250 - okay	    (continue conversation)
	 *	421 - closing down  (try again later)
	 *  5xx - fatal error   (try HELO)
	 */
	STR_CLEAR(&smtp_out);
	(void) str_printf(&smtp_out, EHLO(primary_name));

	reply = wait_write_command(smtpb, smtpb->short_timeout,
				   STR(&smtp_out), (int) STR_LEN(&smtp_out),
				   &reply_text);

	if (reply == REPLY_TIMEOUT) {
	    /* Some gateways just terminate the conection when they
	       receive an EHLO.  We handle this case by returning a
	       special value here.  Also write a message to the log
	       file because it is interesting to know which mailers
	       expose this problem. */
	    write_log(WRITE_LOG_SYS, "link broken by EHLO!");
	    return SMTP_EHLO_FAIL;
	}
	if (REPLY_GROUP(reply) == NEGATIVE_TRY_AGAIN) {
	    /* remote SMTP closed, try again later */
	    *error_p = try_again(smtpb->tp, reply_text);
	    return SMTP_AGAIN;
	}
	if (reply == REPLY_OK) {
	    char * cp = reply_text;
	    int on_greet_line = 1;	/* we start on the greeting line */

	    smtpb->esmtp_flags.ESMTP_basic = 1;
	    /*
	     * Parse the EHLO reply to find out what ESMTP options the remote
	     * server supports....
	     */
	    while (*cp) {
		char *pp;

		/*
		 * every line must begin with the three digits "250", followed
		 * by a '-' for continued lines, and a ' ' for the final line
		 */
		if (strncmp(cp, "250", 3) != 0) {
		    log_malformed_ehlo_reply("Not a 250 reply!", reply_text);
		    return SMTP_SUCCEED;
		}
		cp += 3;
		if (*cp != ' ' && *cp != '-') {
		    log_malformed_ehlo_reply("250 not followed by space or dash", reply_text);
		    return SMTP_SUCCEED;
		}
		cp++;			/* skip the ' ' or '-' */
		if (on_greet_line) {
		    /*
		     * Ignore greeting on first line
		     */
		    on_greet_line = 0;
		    cp = skip_to_eol(cp);
		    continue;
		}
		/*
		 * search for whitespace (or newline) after the keyword
		 */
		for (pp = cp; *(pp) && !isspace(*pp); ++pp) {
		     ;
		}
		/*
		 * in the ESMTP greeting the parameter values are separated
		 * by whitespace, not an equal sign!  We'll generously skip
		 * any whitespace before a newline too...
		 */
		while (isspace(*pp)) {
		    ++pp;
		}
		/*
		 * decipher which keyword it is...
		 */
		if (strncmpic(cp, "8BITMIME", sizeof("8BITMIME")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_8bitmime = 1;
		} else if (strncmpic(cp, "ATRN", sizeof("ATRN")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_atrn = 1;
		} else if (strncmpic(cp, "AUTH", sizeof("AUTH")-1) == 0) {
		    /* XXX followed by a space separated list of the names of supported SASL mechanisms */
		    smtpb->esmtp_flags.ESMTP_auth = 1;
		} else if (strncmpic(cp, "CHUNKING", sizeof("CHUNKING")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_chunking = 1;
		} else if (strncmpic(cp, "DSN", sizeof("DSN")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_dsn = 1;
		} else if (strncmpic(cp, "ENHANCEDSTATUSCODES", sizeof("ENHANCEDSTATUSCODES")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_enhancedstatuscodes = 1;
		} else if (strncmpic(cp, "ETRN", sizeof("ETRN")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_etrn = 1;
		} else if (strncmpic(cp, "EXPN", sizeof("EXPN")-1) == 0) {
		    ; /* NOP XXX Smail never requests EXPN so we can ignore it in here */
		} else if (strncmpic(cp, "HELP", sizeof("HELP")-1) == 0) {
		    ; /* NOP XXX smail never requests HELP so we can ignore it in here */
		} else if (strncmpic(cp, "ONEX", sizeof("ONEX")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_onex = 1;
		} else if (strncmpic(cp, "PIPELINING", sizeof("PIPELINING")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_pipelining = 1;
		} else if (strncmpic(cp, "SAML", sizeof("SAML")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_saml = 1;
		} else if (strncmpic(cp, "SEND", sizeof("SEND")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_send = 1;
		} else if (strncmpic(cp, "SIZE", sizeof("SIZE")-1) == 0) {
		    char *errbuf = NULL;
		    char *ep = strchr(cp, '\n');

		    if (ep+1 == pp) {	/* pp points at next line if no parameter */
			cp = skip_to_eol(cp);
			continue;
		    }
		    if (ep) {
			*ep = '\0';
		    }
		    smtpb->esmtp_flags.ESMTP_size = 1;
		    smtpb->max_size = c_atol(pp, &errbuf); /* XXX this is very generous! */
		    if (ep) {
			*ep = '\n';
		    }
		    if (errbuf) {
			log_malformed_ehlo_reply(errbuf, reply_text);
			cp = skip_to_eol(cp);
			continue;
		    }
		} else if (strncmpic(cp, "SOML", sizeof("SOML")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_soml = 1;
		} else if (strncmpic(cp, "STARTTLS", sizeof("STARTTLS")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_starttls = 1;
		} else if (strncmpic(cp, "TURN", sizeof("TURN")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_turn = 1;
		} else if (strncmpic(cp, "VERB", sizeof("VERB")-1) == 0) {
		    smtpb->esmtp_flags.ESMTP_verb = 1;
		} else if (*cp == 'X' || *cp == 'x') {
		    smtpb->esmtp_flags.ESMTP_x = 1;
		} else {
		    char *ep = strchr(cp, '\n');

		    if (ep) {
			*ep = '\0';
		    }
# ifdef NO_LOG_EHLO
		    DEBUG1(DBG_DRIVER_LO, "destination offers unsupported ESMTP option '%s'.\n", cp);
# else
		    write_log(WRITE_LOG_SYS, "destination offers unsupported ESMTP option '%s'.", cp);
# endif
		    if (ep) {
			*ep = '\n';
		    }
		}
		cp = skip_to_eol(cp);
	    }
	    DEBUG9(DBG_DRIVER_MID, "destination supports ESMTP%s%s%s%s%s%s%s%s%s",
		   smtpb->esmtp_flags.ESMTP_8bitmime ? " 8BITMIME" : "",
		   smtpb->esmtp_flags.ESMTP_atrn ? " ATRN" : "",
		   smtpb->esmtp_flags.ESMTP_auth ? " AUTH" : "",
		   smtpb->esmtp_flags.ESMTP_chunking ? " CHUNKING" : "",
		   smtpb->esmtp_flags.ESMTP_dsn ? " DSN" : "",
		   smtpb->esmtp_flags.ESMTP_enhancedstatuscodes ? " ENHANCEDSTATUSCODES" : "",
		   smtpb->esmtp_flags.ESMTP_etrn ? " ETRN" : "",
		   smtpb->esmtp_flags.ESMTP_expn ? " EXPN" : "",
		   smtpb->esmtp_flags.ESMTP_help ? " HELP" : "");
	    DEBUG9(DBG_DRIVER_MID, "%s%s%s%s%s%s%s%s%s",
		   smtpb->esmtp_flags.ESMTP_onex ? " ONEX" : "",
		   smtpb->esmtp_flags.ESMTP_pipelining ? " PIPELINING" : "",
		   smtpb->esmtp_flags.ESMTP_saml ? " SAML" : "",
		   smtpb->esmtp_flags.ESMTP_send ? " SEND" : "",
		   smtpb->esmtp_flags.ESMTP_size ? " SIZE" : "",
		   smtpb->esmtp_flags.ESMTP_soml ? " SOML" : "",
		   smtpb->esmtp_flags.ESMTP_starttls ? " STARTTLS" : "",
		   smtpb->esmtp_flags.ESMTP_turn ? " TURN" : "",
		   smtpb->esmtp_flags.ESMTP_verb ? " VERB" : "");
	    DEBUG1(DBG_DRIVER_MID, "%s.\n",
		   smtpb->esmtp_flags.ESMTP_x ? " X*" : "");
	    if (smtpb->esmtp_flags.ESMTP_verb && debug >= DBG_DRIVER_MID) {
		STR_CLEAR(&smtp_out);
		(void) str_printf(&smtp_out, VERB);
		reply = wait_write_command(smtpb, smtpb->short_timeout,
					   STR(&smtp_out), (int) STR_LEN(&smtp_out),
					   &reply_text);
		if (REPLY_GROUP (reply) == NEGATIVE_TRY_AGAIN) {
		    /* remote SMTP closed, try again later */
		    *error_p = try_again(smtpb->tp, reply_text);
		    return SMTP_AGAIN;
		}
	    }
	    return SMTP_SUCCEED;
	}
    }
#endif /* HAVE_EHLO */
    /*
     * say who we are.
     * Possible responses:
     *	250 - okay	    (continue conversation)
     *	421 - closing down  (try again later)
     *  5xx - fatal error   (return message to sender)
     */
#ifdef HAVE_EHLO
  try_helo:
#endif
    STR_CLEAR(&smtp_out);
    (void) str_printf(&smtp_out, HELO(primary_name));

    reply = wait_write_command(smtpb, smtpb->short_timeout,
			       STR(&smtp_out), (int) STR_LEN(&smtp_out),
			       &reply_text);

    if (REPLY_GROUP(reply) == NEGATIVE_TRY_AGAIN) {
	/* remote SMTP closed, try again later */
	*error_p = try_again(smtpb->tp, reply_text);
	return SMTP_AGAIN;
    }
    if (reply == REPLY_SEQUENCE_ERROR) {
	/* ignore 503 Bad sequence of commands after EHLO/RSET/HELO */
	write_log(WRITE_LOG_SYS, "503 after EHLO/RSET/HELO:\n%s",
		  reply_text);
    } else if (reply != REPLY_OK) {
#ifdef HAVE_EHLO
	if (! tried_rset) {
	    /* The following */
	    /* fatal error, maybe it doesn't understand EHLO */
	    STR_CLEAR(&smtp_out);
	    (void) str_printf(&smtp_out, "RSET");

	    reply = wait_write_command(smtpb, smtpb->short_timeout,
				       STR(&smtp_out), (int) STR_LEN(&smtp_out),
				       &reply_text);

	    if (REPLY_GROUP(reply) == NEGATIVE_TRY_AGAIN) {
		/* remote SMTP closed, try again later */
		*error_p = try_again(smtpb->tp, reply_text);
		return SMTP_AGAIN;
	    } else if (reply != REPLY_OK) {
		/* Some gateways don't accept any commands before they get
		 * a HELO, not even RSET.  Fortunately it is usually safe
		 * to ignore the error messages. */
		write_log(WRITE_LOG_SYS, "unexpected response to RSET:\n%s",
			  reply_text);
	    }
	    /* the RSET command has been accepted; try again with HELO */
	    tried_rset = 1;
	    goto try_helo;	/* GASP!!! */
	} else
#endif /* HAVE_EHLO */
	{
	    /* fatal error, return message to sender */
	    *error_p = fatal_error(smtpb->tp, reply_text);
	    smtp_shutdown(smtpb);	/* be nice and say goodby! */
	    return SMTP_FAIL;
	}
    }

    /* connection established */
    return SMTP_SUCCEED;
}

/*
 * custom code to safely skip to the end of a line.
 */
static char *
skip_to_eol(cp)
    char *cp;
{
	while (*cp && *cp != '\n') {
	    ++cp;
	}
	if (*cp == '\n') {
	    ++cp;
	}
	return cp;
}

/*
 * This seems to be a reasonable way to handle malformed EHLO replies....
 */
static void
log_malformed_ehlo_reply(error_msg, reply_text)
    char *error_msg;
    char *reply_text;
{
#ifdef NO_LOG_EHLO
    DEBUG2(DBG_DRIVER_LO, "destination supports esmtp, but is buggy: %s;\n%s\n",
	   error_msg, reply_text);
#else
    /* XXX this is really ugly though as it leaves the newlines in place,
     * though it should always be with normal SMTP continuation lines so
     * shouldn't be too hard to parse....
     */
    write_log(WRITE_LOG_SYS, "destination supports esmtp, but is buggy: %s;\n%s",
	      error_msg, reply_text);
#endif /* not NO_LOG_EHLO */
    return;
}



/*
 * smtp_send - mail a message to a remote SMTP process
 *
 * Using a virtual circuit connection of some kind, transmit
 * the current spooled message to the given set of recipient
 * addresses.  If the circuit has only a write channel, and no read
 * channel, then transmit batch SMTP.
 *
 * Return:
 *	SUCCEED	- more smtp messages can be sent.
 *	FAIL	- don't try to send any more messages.
 *
 * For FAIL, return a filled-in error structure.
 */
int
smtp_send(smtpb, addr, succeed, defer, fail, error_p)
    struct smtp *smtpb;			/* SMTP description block */
    struct addr *addr;			/* list of recipient addresses */
    struct addr **succeed;		/* successful addresses */
    struct addr **defer;		/* addresses to be retried */
    struct addr **fail;			/* failed addresses */
    struct error **error_p;		/* error description */
{
    register int reply;			/* reply code from SMTP commands */
    char *reply_text;			/* text of reply */
    int success;			/* success value from calls */
    struct addr *cur;			/* current address being sent */
    struct addr *next;			/* next addr to send */
    struct addr *okay = NULL;		/* partially successful addrs */
    int local_owner = FALSE;            /* TRUE if owner is local */
    struct transport *tp = smtpb->tp;

    if (! smtp_init_flag) {
	STR_INIT(&smtp_in);
	STR_INIT(&smtp_out);
	smtp_init_flag = TRUE;
    }
    STR_CLEAR(&smtp_out);

    /*
     * send MAIL command
     *
     * signal that we are sending a new message,
     * and give the sender address.
     */

    /* The envelope sender is <> for error messages (with <+> used for
     * internally generated ones that are being pushed back into smail).
     * Otherwise, use the message's original sender, unless there is an owner
     * field set for the first address - this field is set for addresses
     * generated by mailing list directors, and should be the same for all
     * addresses in a given call to the transport.
     * 
     * If the owner contains no "@", flag it as local so that the visible name
     * gets added below.
     */
    local_owner = (!error_sender &&
		   addr->owner &&
		   !strchr(addr->owner, '@'));

    str_printf(&smtp_out, "%s%s%s%s%s",
               MAIL_BEGIN,
               (error_sender? ((islocal && tp->flags & LOCAL_XFORM)? "+": ""):
               (addr->owner)? addr->owner :
               get_sender_addr(tp)),
               local_owner? "@" : "",
               local_owner? visible_name : "",
	       MAIL_END);

    /*
     * If the remote-SMTP supports the ESMTP size option then send (a guess of)
     * the size of the message to be transported.  Of course the guess could be
     * a bit more educated than it currently is, but usually it doesn't matter
     * if it is slightly incorrect.  We simply add 2% in order to account for
     * \n -> \r\n conversion.
     */
    if (smtpb->esmtp_flags.ESMTP_size) {
	str_printf(&smtp_out, " SIZE=%lu",
		   (unsigned long) ((msg_size * 102L) / 100L));
    }
#if 0
    /*
     * This is commented out because we are not supposed to send a non-MIME
     * message on 8BITMIME mode and we don't have any way to either detect if
     * the message is already in MIME format or to force a MIME conversion if
     * necessary.  Sending a non-7bit clean message in 7BIT mode isn't a good
     * idea either, but is a common practice.  Sigh.  We'll just avoid saying
     * what we're sending....
     */
    if (smtpb->esmtp_flags.ESMTP_8bitmime) {
	str_printf(&smtp_out, " BODY=8BITMIME");
    } else {
	str_printf(&smtp_out, " BODY=7BIT");
    }
#endif

    reply = write_command_maybe_wait(smtpb, smtpb->long_timeout,
				     STR(&smtp_out), (int) STR_LEN(&smtp_out),
				     &reply_text);
    /*
     * Possible responses:
     *	250 - okay	    (continue conversation)
     *	4xx - temporary error (try message again later)
     *  5xx - fatal error   (return message to sender)
     */
    if (REPLY_GROUP(reply) == NEGATIVE_TRY_AGAIN) {
	*error_p = try_again(tp, reply_text);
	insert_addr_list(addr, defer, *error_p);
	do_smtp_shutdown(smtpb, reply);
	return FAIL;
    } else if (REPLY_GROUP(reply) == NEGATIVE_FAILED) {
	*error_p = fatal_error(tp, reply_text);
	insert_addr_list(addr, fail, *error_p);
	do_smtp_shutdown(smtpb, reply);
	return FAIL;
    }

    /* give all of the recipient addresses to the remote SMTP */
    for (cur = addr; cur; cur = next) {
	next = cur->succ;

	/*
	 * each recipient specified individually.
	 * Possible responses:
	 *  250, 251 - okay, or forwarded (continue conversation)
	 *  421 - connection closing   (try again later)
	 *  451 - remote error         (try again later)
	 *  452 - remote storage full  (try this addr again later)
	 *  550 - unknown user         (return this addr to sender)
	 *  5xx - failure	       (return message to sender)
	 */
	STR_CLEAR(&smtp_out);
	str_printf(&smtp_out, "%s%s%s", RCPT_BEGIN, cur->next_addr, RCPT_END);

	reply = write_command_maybe_wait(smtpb, smtpb->long_timeout,
					 STR(&smtp_out), (int) STR_LEN(&smtp_out),
					 &reply_text);

	if (REPLY_GROUP(reply) == NEGATIVE_TRY_AGAIN) {
	    /*
	     * NOTE: keep these in sync with the pipeline handling below
	     */
	    switch (reply) {
	    case REPLY_MAILBOX_UNAVAIL:
		cur->error = remote_mailbox_unavail(tp, reply_text);
		cur->succ = *defer;	/* defer only the current recipient */
		*defer = cur;
		break;
	    case REPLY_REMOTE_FAILURE:
		cur->error = remote_failure(tp, reply_text);
		cur->succ = *defer;	/* defer only the current recipient */
		*defer = cur;
		break;
	    case REPLY_STORAGE_FULL:
		cur->error = remote_storage_full(tp, reply_text);
		cur->succ = *defer;	/* defer only the current recipient */
		*defer = cur;
		break;
	    default:
		*error_p = try_again(tp, reply_text);
		/*
		 * unknown codes defer all recipients
		 * XXX is this sane?
		 */
		insert_addr_list(cur, defer, *error_p);
		insert_addr_list(okay, defer, *error_p);
		do_smtp_shutdown(smtpb, reply);
		return FAIL;
	    }
	} else if (REPLY_GROUP(reply) == NEGATIVE_FAILED) {
	    /*
	     * NOTE: keep these in sync with the pipeline handling below
	     */
	    switch (reply) {
	    case REPLY_UNKNOWN_USER:
		cur->error = remote_unknown_user(tp, reply_text);
		cur->succ = *fail;	/* fail only the current recipient */
		*fail = cur;
		break;
	    case REPLY_USER_NOT_LOCAL:
		cur->error = remote_user_not_local(tp, reply_text);
		cur->succ = *fail;	/* fail only the current recipient */
		*fail = cur;
		break;
	    case REPLY_OVER_QUOTA:
		cur->error = remote_over_quota(tp, reply_text);
		cur->succ = *fail;	/* fail only the current recipient */
		*fail = cur;
		break;
	    case REPLY_ILLEGAL_USER:
		cur->error = remote_illegal_user(tp, reply_text);
		cur->succ = *fail;	/* fail only the current recipient */
		*fail = cur;
		break;
	    case REPLY_SEQUENCE_ERROR:
	    default:
		*error_p = fatal_error(tp, reply_text);
		/*
		 * unknown codes are fatal for all recipients; i.e. don't send
		 * to anyone at a mis-behaving remote-SMTP site.
		 */
		insert_addr_list(cur, fail, *error_p);
		insert_addr_list(okay, fail, *error_p);
		do_smtp_shutdown(smtpb, reply);
		return FAIL;
	    }
	} else if (!smtpb->esmtp_flags.ESMTP_pipelining) {
	    /* successful thus far.... */
	    cur->succ = okay;
	    okay = cur;
	}
    }
    if (!smtpb->esmtp_flags.ESMTP_pipelining && !okay) {
	do_smtp_shutdown(smtpb, REPLY_TRANS_FAILURE);
	return FAIL;
    }

    /*
     * say that we will next be sending the actual data
     * Possible responses:
     *	354 - go ahead with the message (continue conversation)
     *  421 - closing connection (try all recipients again later)
     *	4xx - remote error       (try all recipients again later)
     *  5xx - fatal error        (return message to sender)
     */
    reply = write_command_maybe_wait(smtpb, smtpb->long_timeout,
			       DATA_BEGIN, sizeof(DATA_BEGIN) - 1, &reply_text);

    /*
     * we only "pipeline" the commands up to the DATA_BEGIN....
     */
    if (smtpb->esmtp_flags.ESMTP_pipelining) {
	if (flush_command_stream(smtpb, &reply_text) != REPLY_OK) {
	    *error_p = try_again(tp, reply_text);
	    insert_addr_list(addr, defer, *error_p);
	    /* XXX no need to shut down? -- this indicates a write failed */
	    return FAIL;
	}

	/* Now read all the responses.  First the MAIL FROM: reply */
	DEBUG(DBG_DRIVER_MID, "SMTP-pipeline: expecting reply from 'MAIL FROM:'\n");
	reply = wait_read_response(smtpb, smtpb->long_timeout,
				   &reply_text);
	if (REPLY_GROUP(reply) == NEGATIVE_TRY_AGAIN) {
	    *error_p = try_again(tp, reply_text);
	    insert_addr_list(addr, defer, *error_p);
	    do_smtp_shutdown(smtpb, reply);
	    return FAIL;
	} else if (REPLY_GROUP(reply) == NEGATIVE_FAILED) {
	    *error_p = fatal_error(tp, reply_text);
	    insert_addr_list(addr, fail, *error_p);
	    do_smtp_shutdown(smtpb, reply);
	    return FAIL;
	}

	/* ... next the reply from each RCPT TO: command */
	for (cur = addr; cur; cur = next) {
	    next = cur->succ;

	    DEBUG1(DBG_DRIVER_MID, "SMTP-pipeline: expecting reply from 'RCPT TO:%s'\n", cur->next_addr);
	    reply = wait_read_response(smtpb, smtpb->long_timeout,
				       &reply_text);

	    /*
	     * here we should only defer/fail individual recipients else we'll
	     * leave un-read replies on the input channel.
	     */
	    if (REPLY_GROUP(reply) == NEGATIVE_TRY_AGAIN) {
		/*
		 * NOTE: keep these in sync with the non-pipeline handling above
		 */
		switch (reply) {
		case REPLY_MAILBOX_UNAVAIL:
		    cur->error = remote_mailbox_unavail(tp, reply_text);
		    break;
		case REPLY_REMOTE_FAILURE:
		    cur->error = remote_failure(tp, reply_text);
		    break;
		case REPLY_STORAGE_FULL:
		    cur->error = remote_storage_full(tp, reply_text);
		    break;
		default:
		    cur->error = try_again(tp, reply_text);
		    break;
		}
		cur->succ = *defer;	/* defer only the current recipient */
		*defer = cur;
	    } else if (REPLY_GROUP(reply) == NEGATIVE_FAILED) {
		/*
		 * NOTE: keep these in sync with the non-pipeline handling above
		 */
		switch (reply) {
		case REPLY_UNKNOWN_USER:
		    cur->error = remote_unknown_user(tp, reply_text);
		    break;
		case REPLY_USER_NOT_LOCAL:
		    cur->error = remote_user_not_local(tp, reply_text);
		    break;
		case REPLY_OVER_QUOTA:
		    cur->error = remote_over_quota(tp, reply_text);
		    break;
		case REPLY_ILLEGAL_USER:
		    cur->error = remote_illegal_user(tp, reply_text);
		    break;
		default:
		    cur->error = fatal_error(tp, reply_text);
		    break;
		}
		cur->succ = *fail;	/* fail only the current recipient */
		*fail = cur;
	    } else {
		/* successful thus far */
		cur->succ = okay;
		okay = cur;
	    }
	}
	/* ... and finally the reply from the DATA command */
	DEBUG(DBG_DRIVER_MID, "SMTP-pipeline: expecting reply from 'DATA'\n");
	reply = wait_read_response(smtpb, smtpb->long_timeout,
				   &reply_text);
    }

    if (REPLY_GROUP(reply) == NEGATIVE_TRY_AGAIN) {
	/* temporary error, defer message */
	*error_p = try_again(tp, reply_text);
	insert_addr_list(okay, defer, *error_p);
	do_smtp_shutdown(smtpb, reply);
	return FAIL;
    } else if (REPLY_GROUP(reply) == NEGATIVE_FAILED) {
	/* fatal error, return message to sender */
	*error_p = fatal_error(tp, reply_text);
	insert_addr_list(okay, fail, *error_p);
	do_smtp_shutdown(smtpb, reply);
	return FAIL;
    } else if (reply != REPLY_START_DATA) {
	char *p;

	for (p = reply_text; *p; p++) {
	    if (*p == '\r') *p = '.';
	    if (*p == '\n') *p = '.';
	}
	*error_p = fatal_error(tp, xprintf("599 remote protocol error: expecting 354 (or failure), got: %s", reply_text));
	insert_addr_list(okay, fail, *error_p);
	do_smtp_shutdown(smtpb, REPLY_PROTO_ERROR);
	return REPLY_PROTO_ERROR;
    }

    /*
     * send the message, using the hidden dot protocol.
     */
    tp->flags |= PUT_DOTS;
    success = write_message(smtpb->out, tp, addr);

    if (success == WRITE_FAIL) {
	*error_p = write_failed(tp);
	insert_addr_list(okay, defer, *error_p);

	/* assume connection has gone away */
	return FAIL;
    }
    if (success == READ_FAIL) {
	*error_p = read_failed(tp);
	insert_addr_list(okay, defer, *error_p);

	/* okay to advance to the next message */
	return SUCCEED;
    }

    /*
     * Finish by sending the final "."
     * Possible responses:
     *	250 - message accepted
     *  451 - local error in processing		(try all recipients again later)
     *  452 - insufficient system storage	(try all recipients again later)
     *	4xx - other temporary error		(try all recipients again later)
     *  552 - exceeded storage allocation	(return message to sender)
     *  554 - Transaction failed		(return message to sender
     *  5xx - other fatal error			(return message to sender)
     */
    reply = wait_write_command(smtpb, smtpb->long_timeout,
			       DATA_END, sizeof(DATA_END)-1, &reply_text);

    if (REPLY_GROUP(reply) == NEGATIVE_TRY_AGAIN) {
	/* temporary error, defer message */
	*error_p = try_again(tp, reply_text);
	insert_addr_list(okay, defer, *error_p);
	do_smtp_shutdown(smtpb, reply);
	return FAIL;
    }
    switch (reply) {
    case REPLY_DOWN:
    case REPLY_TIMEOUT:
	/* possible error, defer, but note possible duplicate */
	*error_p = try_again(tp, "499-read error from remote SMTP server after sending '.'.\n499 NOTE: duplicate delivery may occur!");
	insert_addr_list(okay, defer, *error_p);
	do_smtp_shutdown(smtpb, reply);
	return FAIL;
    }
    if (reply != REPLY_OK) {
	/* fatal error, return message to sender */
	*error_p = fatal_error(tp, reply_text);
	insert_addr_list(okay, fail, *error_p);
	do_smtp_shutdown(smtpb, reply);
	return FAIL;
    }

    insert_addr_list(okay, succeed, (struct error *)NULL);
    return SUCCEED;
}


/*
 * smtp_shutdown - end an interactive SMTP conversation
 */
void
smtp_shutdown(smtpb)
    struct smtp *smtpb;			/* SMTP description block */
{
    char *reply_text;			/* where to store reply text */

    (void) wait_write_command(smtpb, smtpb->short_timeout,
			      QUIT, sizeof(QUIT)-1, &reply_text);
}

/*
 * do_smtp_shutdown - call shutdown if the connection wasn't dropped
 */
static void
do_smtp_shutdown(smtpb, reply)
    struct smtp *smtpb;			/* SMTP description block */
    int reply;				/* response code from command */
{
    switch (reply) {
    case REPLY_DOWN:
    case REPLY_PROTO_ERROR:
    case REPLY_TIMEOUT:
	break;

    default:
	smtp_shutdown(smtpb);
	break;
    }
}

static int
write_command_maybe_wait (smtpb, timeout, text, len, reply_text)
    struct smtp *smtpb;			/* SMTP description block */
    unsigned timeout;			/* read timeout */
    char *text;				/* text of command */
    register int len;			/* length of command */
    char **reply_text;			/* text of response from remote */
{
    if (smtpb->esmtp_flags.ESMTP_pipelining) {
	return write_command_nowait (smtpb, text, len);
    } else {
	return wait_write_command (smtpb, timeout, text, len, reply_text);
    }
}

/*
 * wait_write_command - send a command, then wait for the response
 *
 * For batched SMTP, return a response code of REPLY_OK, unless there
 * was a write error.
 */
static int
wait_write_command(smtpb, timeout, text, len, reply_text)
    struct smtp *smtpb;			/* SMTP description block */
    unsigned timeout;			/* read timeout */
    char *text;				/* text of command */
    register int len;			/* length of command */
    char **reply_text;			/* text of response from remote */
{
    int reply;

    reply = write_command_nowait (smtpb, text, len);
    if (reply != REPLY_OK)
	return reply;

    reply = flush_command_stream (smtpb, reply_text);
    if (reply != REPLY_OK)
	return reply;

    /* wait for the response to come back */
    reply = wait_read_response(smtpb, timeout, reply_text);
    return reply;
}

/*
 * write_command_nowait - send a command
 */
static int
write_command_nowait(smtpb, text, len)
    struct smtp *smtpb;			/* SMTP description block */
    char *text;				/* text of command */
    register int len;			/* length of command */
{
    register FILE *f = smtpb->out;
    register char *cp;

#ifndef NODEBUG
    DEBUG(DBG_DRIVER_MID, "SMTP-send:  ");
    if (debug >= DBG_DRIVER_MID) {
	char *temp_cp = text;
	int temp_len = len;

	for (temp_cp = text; temp_len; temp_cp++, --temp_len) {
	    DEBUG1(DBG_DRIVER_MID, "%c", *temp_cp);
	}
	DEBUG(DBG_DRIVER_MID, "\n");
    }
#endif	/* NODEBUG */

    /* send out the command */
    for (cp = text; len; cp++, --len) {
	putc(*cp, f);
    }

    /* terminate the command line */
    for (cp = smtpb->nl; *cp; cp++) {
	putc(*cp, f);
    }
    return REPLY_OK;
}

static int
flush_command_stream(smtpb, reply_text)
    struct smtp *smtpb;			/* SMTP description block */
    char **reply_text;			/* text of response from remote */
{
    register FILE *f = smtpb->out;

    (void) fflush(f);
    if (ferror(f)) {
	*reply_text = "499 write error, remote probably down";
	DEBUG1(DBG_DRIVER_MID, "SMTP-reply: %s\n", *reply_text);
	return REPLY_TIMEOUT;
    }
    return REPLY_OK;
}

/*
 * wait_read_response - wait for a response from the remote SMTP process
 *
 * return the response code, and the response text.  Abort on timeouts
 * or end-of-file or protocol errors.
 */
static int
wait_read_response(smtpb, timeout, reply_text)
    struct smtp *smtpb;			/* SMTP description block */
    unsigned timeout;			/* read timeout */
    char **reply_text;			/* return text of response here */
{
    int result;

    /* If we're in batch mode, always return success. */
    if (! smtpb->in) {
	return REPLY_OK;
    }
    do
      {
	result = read_response_internal (smtpb, timeout, reply_text);
	DEBUG1(DBG_DRIVER_MID, "SMTP-reply: %s\n", *reply_text);
      }
    while (REPLY_GROUP (result) == POSITIVE_DEBUG);
    return result;
}

static int
read_response_internal(smtpb, timeout, reply_text)
    struct smtp *smtpb;			/* SMTP description block */
    unsigned timeout;			/* read timeout */
    char **reply_text;			/* return text of response here */
{
    register int lstart;		/* offset to start of an input line */
    int success;			/* success value from calls */
    VOLATILE unsigned save_alarm;	/* previous alarm value */
    VOLATILE JUMPSIG save_sig;		/* previous alarm signal catcher */
    register int c;
    register char *rp;

    /* reset the input buffer */
    STR_CLEAR(&smtp_in);

    save_alarm = alarm(0);
    JUMP_SETSIG(SIGALRM, catch_timeout, &save_sig);

    /* if we timeout, say so */
    if (JUMP_SETJMP(timeout_buf)) {
	(void) alarm(0);
	JUMP_CLEARSIG(SIGALRM, &save_sig);
	if (save_alarm) {
	    (void) alarm(save_alarm);
	}
	*reply_text = "499 timeout on read from remote SMTP process";
	return REPLY_TIMEOUT;
    }

    /* don't let reads block forever */
    (void) alarm(timeout);

    /* loop until the response is completed */
    do {
	lstart = STR_LEN(&smtp_in);	/* remember the beginning of this line */
	while ((c = getc(smtpb->in)) != EOF && c != '\n') {
	    STR_NEXT(&smtp_in, c);
	}
	rp = STR(&smtp_in);		/* get current pointer */
	if (STR_LEN(&smtp_in) > 0 && rp[STR_LEN(&smtp_in) - 1] == '\r') {
	    STR_PREV(&smtp_in);
	}
	STR_NEXT(&smtp_in, '\n');
	rp = STR(&smtp_in);		/* get current pointer */
    } while (c != EOF &&
	     isdigit(rp[lstart]) &&
	     isdigit(rp[lstart + 1]) &&
	     isdigit(rp[lstart + 2]) &&
	     rp[lstart + 3] == '-');

    /* replace last newline with a NUL byte */
    /* XXX or should we? */
    if (STR_LEN(&smtp_in) > 0) {
	rp[STR_LEN(&smtp_in) - 1] = '\0';
    }

    /* restore previous alarm catcher and setting */
    (void) alarm(0);
    JUMP_CLEARSIG(SIGALRM, &save_sig);
    if (save_alarm) {
	(void) alarm(save_alarm);
    }

    /* 
     * finally it's safe to see if there was a read error....
     */
    if (c == EOF) {
	*reply_text = "499 read error from remote SMTP process";
	return REPLY_TIMEOUT;
    }
    /* .... or other protocol error */
    rp = STR(&smtp_in);
    if (! isdigit(rp[lstart]) ||
	! isdigit(rp[lstart + 1]) ||
	! isdigit(rp[lstart + 2]) ||
	! isspace(rp[lstart + 3]))
    {
	*reply_text = "498 protocol error in reply from remote SMTP process";
	return REPLY_PROTO_ERROR;
    }

    *reply_text = rp;

    /* snarf out the reply code for our return value.... */
    (void) sscanf(rp, "%3x", &success);

    return success;
}

/* catch_timeout - longjmp after an alarm */
/* ARGSUSED */
static void
catch_timeout(signo)
	int	signo;			/* UNUSED */
{
    JUMP_LONGJMP(timeout_buf, 1);
}

static struct error *
no_remote(tp, reply_text)
    struct transport *tp;
    char *reply_text;
{
    char *error_text;

    /*
     * ERR_172 - no connection to remote SMTP server
     *
     * DESCRIPTION
     *	    No 220 reply was received from the remote SMTP server over
     *	    the virtual circuit, assume that the remote host was not
     *	    really reachable.
     *
     * ACTIONS
     *	    Try again later.
     *
     * RESOLUTION
     *	    Retries should eventually take care of the problem.
     */
    error_text = xprintf("%s transport failure: no connection to remote SMTP server:\n%s", tp->name, reply_text);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_172, error_text);
}

static struct error *
try_again(tp, reply_text)
    struct transport *tp;
    char *reply_text;
{
    char *error_text;

    /*
     * ERR_151 - temporary SMTP error
     *
     * DESCRIPTION
     *	    wait_write_command() received a temporary error response
     *      while conversing with the remote host.  These can be read
     *	    errors or read timeouts, or they can be negative responses
     *	    from the remote side.
     *
     * ACTIONS
     *      Defer the input address(es).
     *
     * RESOLUTION
     *      Retries should eventually take care of the problem.
     */
    error_text = xprintf("temporary failure from %s transport:\n%s", tp->name, reply_text);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_151, error_text);
}

static struct error *
fatal_error(tp, reply_text)
    struct transport *tp;
    char *reply_text;
{
    char *error_text;

    /*
     * ERR_152 - permanent SMTP error
     *
     * DESCRIPTION
     *	    wait_write_command() received a permanent error response
     *      while conversing with the remote host.  These are generally
     *	    permanent negative response codes from the remote side.
     *
     * ACTIONS
     *      Fail the input addresses, and return to the address owner
     *	    or the message originator.
     *
     * RESOLUTION
     *      The resolution depends upon the error message.  See RFC822
     *	    for details.
     */
    error_text = xprintf("fatal error from %s transport:\n%s", tp->name, reply_text);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_152 | ERR_NSOWNER, error_text);
}

static struct error *
remote_mailbox_unavail(tp, reply_text)
    struct transport *tp;
    char *reply_text;
{
    char *error_text;

    /*
     * ERR_187 - Remote mailbox is busy (SMTP 450)
     *
     * DESCRIPTION
     *      The remote host returned status indicating that it
     *      cannot accept mail for this mailbox at this time.
     *
     * ACTIONS
     *      Defer this recipient.
     *
     * RESOLUTION
     *      Retries should eventually take care of the problem.
     */
    error_text = xprintf("remote mailbox busy, reported by %s transport:\n%s", tp->name, reply_text);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_187, error_text);
}

static struct error *
remote_failure(tp, reply_text)
    struct transport *tp;
    char *reply_text;
{
    char *error_text;

    /*
     * ERR_188 - Remote failure in processing
     *
     * DESCRIPTION
     *      The remote host returned status indicating that it
     *      is experiencing temporary problems processing this
     *      recipient address.
     *
     * ACTIONS
     *      Defer this recipient.
     *
     * RESOLUTION
     *      Retries should eventually take care of the problem.
     */
    error_text = xprintf("remote processing error reported by %s transport:\n%s", tp->name, reply_text);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_188, error_text);
}

static struct error *
remote_storage_full(tp, reply_text)
    struct transport *tp;
    char *reply_text;
{
    char *error_text;

    /*
     * ERR_153 - Remote host's storage is full (SMTP 452)
     *
     * DESCRIPTION
     *      The remote host returned status indicating that he
     *      cannot take more recipient addresses.
     *
     * ACTIONS
     *      Defer the remaining addresses in hopes that other
     *      storage is not affected.
     *
     * RESOLUTION
     *      Hopefully by sending a few addresses now and a few later
     *      the message will eventually be delivered to all
     *      recipients.
     */
    error_text = xprintf("remote storage full error reported by %s transport:\n%s", tp->name, reply_text);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_153, error_text);
}

static struct error *
remote_unknown_user(tp, reply_text)
    struct transport *tp;
    char *reply_text;
{
    char *error_text;

    /*
     * ERR_156 - remote host returned bad address status (SMTP 550)
     *
     * DESCRIPTION
     *      Status from the remote host indicates an error in the
     *      recipient address just sent to it.
     *
     * ACTIONS
     *      Return mail to the address owner or to the sender.
     *
     * RESOLUTION
     *      An alternate address should be attempted or the
     *      postmaster at the site that generated the error message
     *      should be consulted.
     */
    error_text = xprintf("%s transport reports unknown user:\n%s", tp->name, reply_text);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_156 | ERR_NSOWNER, error_text);
}

static struct error *
remote_user_not_local(tp, reply_text)
    struct transport *tp;
    char *reply_text;
{
    char *error_text;

    /*
     * ERR_189 - remote host says user not local (SMTP 551)
     *
     * DESCRIPTION
     *      Status from the remote host indicates this user is not local.
     *
     * ACTIONS
     *      Return mail to the address owner or to the sender.
     *
     * RESOLUTION
     *      An alternate address should be attempted or the
     *      postmaster at the site that generated the error message
     *      should be consulted.
     */
    error_text = xprintf("%s transport reports user not local:\n%s", tp->name, reply_text);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_189 | ERR_NSOWNER, error_text);
}

static struct error *
remote_over_quota(tp, reply_text)
    struct transport *tp;
    char *reply_text;
{
    char *error_text;

    /*
     * ERR_190 - remote host says user over quota (SMTP 552)
     *
     * DESCRIPTION
     *      Status from the remote host indicates this user has exceeded
     *      their maximum allowed mailbox size.
     *
     * ACTIONS
     *      Return mail to the address owner or to the sender.
     *
     * RESOLUTION
     *      Out-of-band communications should be attempted or the
     *      postmaster at the site that generated the error message
     *      should be consulted.
     */
    error_text = xprintf("%s transport reports user over quota:\n%s", tp->name, reply_text);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_190 | ERR_NSOWNER, error_text);
}

static struct error *
remote_illegal_user(tp, reply_text)
    struct transport *tp;
    char *reply_text;
{
    char *error_text;

    /*
     * ERR_191 - remote host says user is illegal (SMTP 553)
     *
     * DESCRIPTION
     *      Status from the remote host indicates this user account
     *      is not allowed to receive mail (or the syntax is incorrect).
     *
     * ACTIONS
     *      Return mail to the address owner or to the sender.
     *
     * RESOLUTION
     *      Out-of-band communications should be attempted or the
     *      postmaster at the site that generated the error message
     *      should be consulted.
     */
    error_text = xprintf("%s transport reports illegal user:\n%s", tp->name, reply_text);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_191 | ERR_NSOWNER, error_text);
}

static struct error *
write_failed(tp)
    struct transport *tp;
{
    char *error_text;

    /*
     * ERR_154 - Error writing to remote host
     *
     * DESCRIPTION
     *      An error occured when transmitting the message text to the
     *      remote host.
     *
     * ACTIONS
     *      Defer all of the remaining input addresses.
     *
     * RESOLUTION
     *      Hopefully retries should take care of the problem.
     */
    error_text = xprintf("%s transport failure: Error writing to remote host", tp->name);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_154, error_text);
}

static struct error *
read_failed(tp)
    struct transport *tp;
{
    char *error_text;

    /*
     * ERR_155 - Failed to read spooled message
     *
     * DESCRIPTION
     *      We failed to read the spooled message on our side while
     *      sending the message text to a remote host.
     *
     * ACTIONS
     *      Defer the message with a configuration error.  If we are
     *      unable to read the spool file, there is little we can do.
     *
     * RESOLUTION
     *      "This should never happen".
     */
    error_text = xprintf("%s transport failure: Error reading from spool file", tp->name);
    DEBUG1(DBG_DRIVER_LO, "%s\n", error_text);
    return note_error(ERR_155 | ERR_CONFERR, error_text);
}
