/*++
/* NAME
/*	bounce_notify_util 3
/* SUMMARY
/*	send non-delivery report to sender, server side
/* SYNOPSIS
/*	#include "bounce_service.h"
/*
/*	typedef struct {
/* .in +4
/*		/* All private members... */
/* .in -4
/*	} BOUNCE_INFO;
/*
/*	BOUNCE_INFO *bounce_mail_init(service, queue_name, queue_id, encoding,
/*					sendopts, dsn_envid, template)
/*	const char *service;
/*	const char *queue_name;
/*	const char *queue_id;
/*	const char *encoding;
/*	int	sendopts;
/*	const char *dsn_envid;
/*	const BOUNCE_TEMPLATE *template;
/*
/*	BOUNCE_INFO *bounce_mail_one_init(queue_name, queue_id, encoding,
/*					sendopts, dsn_envid, dsn_notify,
/*					rcpt_buf, dsn_buf, template)
/*	const char *queue_name;
/*	const char *queue_id;
/*	const char *encoding;
/*	int	sendopts;
/*	int	dsn_notify;
/*	const char *dsn_envid;
/*	RCPT_BUF *rcpt_buf;
/*	DSN_BUF	*dsn_buf;
/*	const BOUNCE_TEMPLATE *template;
/*
/*	void	bounce_mail_free(bounce_info)
/*	BOUNCE_INFO *bounce_info;
/*
/*	int	bounce_header(fp, bounce_info, recipient, postmaster_copy)
/*	VSTREAM *fp;
/*	BOUNCE_INFO *bounce_info;
/*	const char *recipient;
/*	int	postmaster_copy;
/*
/*	int	bounce_boilerplate(fp, bounce_info)
/*	VSTREAM *fp;
/*	BOUNCE_INFO *bounce_info;
/*
/*	int     bounce_recipient_log(fp, bounce_info)
/*	VSTREAM *fp;
/*	BOUNCE_INFO *bounce_info;
/*
/*	int     bounce_diagnostic_log(fp, bounce_info, notify_filter)
/*	VSTREAM *fp;
/*	BOUNCE_INFO *bounce_info;
/*	int	notify_filter;
/*
/*	int     bounce_header_dsn(fp, bounce_info)
/*	VSTREAM *fp;
/*	BOUNCE_INFO *bounce_info;
/*
/*	int     bounce_recipient_dsn(fp, bounce_info)
/*	VSTREAM *fp;
/*	BOUNCE_INFO *bounce_info;
/*
/*	int     bounce_diagnostic_dsn(fp, bounce_info, notify_filter)
/*	VSTREAM *fp;
/*	BOUNCE_INFO *bounce_info;
/*	int	notify_filter;
/*
/*	int	bounce_original(fp, bounce_info, headers_only)
/*	VSTREAM *fp;
/*	BOUNCE_INFO *bounce_info;
/*	int	headers_only;
/*
/*	void	bounce_delrcpt(bounce_info)
/*	BOUNCE_INFO *bounce_info;
/*
/*	void	bounce_delrcpt_one(bounce_info)
/*	BOUNCE_INFO *bounce_info;
/* DESCRIPTION
/*	This module implements the grunt work of sending a non-delivery
/*	notification. A bounce is sent in a form that satisfies RFC 1894
/*	(delivery status notifications).
/*
/*	bounce_mail_init() bundles up its argument and attempts to
/*	open the corresponding logfile and message file. A BOUNCE_INFO
/*	structure contains all the necessary information about an
/*	undeliverable message.
/*
/*	bounce_mail_one_init() provides the same function for only
/*	one recipient that is not read from bounce logfile.
/*
/*	bounce_mail_free() releases memory allocated by bounce_mail_init()
/*	and closes any files opened by bounce_mail_init().
/*
/*	bounce_header() produces a standard mail header with the specified
/*	recipient and starts a text/plain message segment for the
/*	human-readable problem description. postmaster_copy is either
/*	POSTMASTER_COPY or NO_POSTMASTER_COPY.
/*
/*	bounce_boilerplate() produces the standard "sorry" text that
/*	creates the illusion that mail systems are civilized.
/*
/*	bounce_recipient_log() sends a human-readable representation of
/*	logfile information for one recipient, with the recipient address
/*	and with the text why the recipient was undeliverable.
/*
/*	bounce_diagnostic_log() sends a human-readable representation of
/*	logfile information for all undeliverable recipients. The
/*	notify_filter specifies what recipient status records should be
/*	reported: DSN_NOTIFY_SUCCESS, DSN_NOTIFY_FAILURE, DSN_NOTIFY_DELAY.
/*	In the absence of DSN NOTIFY information all records are reported.
/*	The result value is -1 in case of error, the number of reported
/*	recipients in case of success.
/*
/*	bounce_header_dsn() starts a message/delivery-status message
/*	segment and sends the machine-readable information that identifies
/*	the reporting MTA.
/*
/*	bounce_recipient_dsn() sends a machine-readable representation of
/*	logfile information for one recipient, with the recipient address
/*	and with the text why the recipient was undeliverable.
/*
/*	bounce_diagnostic_dsn() sends a machine-readable representation of
/*	logfile information for all undeliverable recipients. The
/*	notify_filter specifies what recipient status records should be
/*	reported: DSN_NOTIFY_SUCCESS, DSN_NOTIFY_FAILURE, DSN_NOTIFY_DELAY.
/*	In the absence of DSN NOTIFY information all records are reported.
/*	The result value is -1 in case of error, the number of reported
/*	recipients in case of success.
/*
/*	bounce_original() starts a message/rfc822 or text/rfc822-headers
/*	message segment and sends the original message, either full
/*	(DSN_RET_FULL) or message headers only (DSN_RET_HDRS).
/*
/*	bounce_delrcpt() deletes recipients in the logfile from the original
/*	queue file.
/*
/*	bounce_delrcpt_one() deletes one recipient from the original
/*	queue file.
/* DIAGNOSTICS
/*	Fatal error: error opening existing file.
/* BUGS
/* SEE ALSO
/*	bounce(3) basic bounce service client 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
/*
/*	Wietse Venema
/*	Google, Inc.
/*	111 8th Avenue
/*	New York, NY 10011, USA
/*
/*	Wietse Venema
/*	porcupine.org
/*--*/

/* System library. */

#include <sys_defs.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <stdio.h>			/* sscanf() */
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <ctype.h>

#ifdef STRCASECMP_IN_STRINGS_H
#include <strings.h>
#endif

/* Utility library. */

#include <msg.h>
#include <mymalloc.h>
#include <events.h>
#include <vstring.h>
#include <vstream.h>
#include <line_wrap.h>
#include <stringops.h>
#include <myflock.h>

/* Global library. */

#include <mail_queue.h>
#include <quote_822_local.h>
#include <mail_params.h>
#include <is_header.h>
#include <record.h>
#include <rec_type.h>
#include <post_mail.h>
#include <mail_addr.h>
#include <mail_error.h>
#include <bounce_log.h>
#include <mail_date.h>
#include <mail_proto.h>
#include <lex_822.h>
#include <deliver_completed.h>
#include <dsn_mask.h>
#include <smtputf8.h>
#include <header_opts.h>

/* Application-specific. */

#include "bounce_service.h"

#define STR vstring_str
#define LEN VSTRING_LEN

/* bounce_mail_alloc - initialize */

static BOUNCE_INFO *bounce_mail_alloc(const char *service,
				              const char *queue_name,
				              const char *queue_id,
				              const char *encoding,
				              int sendopts,
				              const char *dsn_envid,
				              RCPT_BUF *rcpt_buf,
				              DSN_BUF *dsn_buf,
				              BOUNCE_TEMPLATE *template,
				              BOUNCE_LOG *log_handle)
{
    BOUNCE_INFO *bounce_info;
    int     rec_type;
    int     skip_message_segment = 0;

    /*
     * Bundle up a bunch of parameters and initialize information that will
     * be discovered on the fly.
     * 
     * XXX Instead of overriding the returned-message MIME encoding, separate
     * the returned-message MIME encoding from the (boiler plate, delivery
     * status) MIME encoding.
     */
    bounce_info = (BOUNCE_INFO *) mymalloc(sizeof(*bounce_info));
    bounce_info->service = service;
    bounce_info->queue_name = queue_name;
    bounce_info->queue_id = queue_id;
    bounce_info->sendopts = sendopts;
    /* Fix 20140708: override MIME encoding: addresses may be 8bit. */
    /* Fix 20140718: override MIME encoding: 8bit $myhostname expansion. */
    if (var_smtputf8_enable /* was: bounce_info->smtputf8 */ ) {
	bounce_info->mime_encoding = "8bit";
    } else if (strcmp(encoding, MAIL_ATTR_ENC_8BIT) == 0) {
	bounce_info->mime_encoding = "8bit";
    } else if (strcmp(encoding, MAIL_ATTR_ENC_7BIT) == 0) {
	bounce_info->mime_encoding = "7bit";
    } else {
	if (strcmp(encoding, MAIL_ATTR_ENC_NONE) != 0)
	    msg_warn("%s: unknown encoding: %.200s",
		     bounce_info->queue_id, encoding);
	bounce_info->mime_encoding = 0;
    }
    if (dsn_envid && *dsn_envid)
	bounce_info->dsn_envid = dsn_envid;
    else
	bounce_info->dsn_envid = 0;
    bounce_info->template = template;
    bounce_info->buf = vstring_alloc(100);
    bounce_info->sender = vstring_alloc(100);
    bounce_info->arrival_time = 0;
    bounce_info->orig_offs = 0;
    bounce_info->message_size = 0;
    bounce_info->orig_msgid = vstring_alloc(100);
    bounce_info->rcpt_buf = rcpt_buf;
    bounce_info->dsn_buf = dsn_buf;
    bounce_info->log_handle = log_handle;

    /*
     * RFC 1894: diagnostic-type is an RFC 822 atom. We use X-$mail_name and
     * must ensure it is valid.
     */
    bounce_info->mail_name = mystrdup(var_mail_name);
    translit(bounce_info->mail_name, " \t\r\n()<>@,;:\\\".[]",
	     "-----------------");

    /*
     * Compute a supposedly unique boundary string. This assumes that a queue
     * ID and a hostname contain acceptable characters for a boundary string,
     * but the assumption is not verified.
     */
    vstring_sprintf(bounce_info->buf, "%s.%lu/%s",
		    queue_id, (unsigned long) event_time(), var_myhostname);
    bounce_info->mime_boundary = mystrdup(STR(bounce_info->buf));

    /*
     * If the original message cannot be found, do not raise a run-time
     * error. There is nothing we can do about the error, and all we are
     * doing is to inform the sender of a delivery problem. Bouncing a
     * message does not have to be a perfect job. But if the system IS
     * running out of resources, raise a fatal run-time error and force a
     * backoff.
     */
    if ((bounce_info->orig_fp = mail_queue_open(queue_name, queue_id,
						O_RDWR, 0)) == 0
	&& errno != ENOENT)
	msg_fatal("open %s %s: %m", service, queue_id);

    /*
     * Get time/size/sender information from the original message envelope
     * records. If the envelope is corrupted just send whatever we can
     * (remember this is a best effort, it does not have to be perfect).
     * 
     * Lock the file for shared use, so that queue manager leaves it alone after
     * restarting.
     */
#define DELIVER_LOCK_MODE (MYFLOCK_OP_SHARED | MYFLOCK_OP_NOWAIT)

    if (bounce_info->orig_fp != 0) {
	if (myflock(vstream_fileno(bounce_info->orig_fp), INTERNAL_LOCK,
		    DELIVER_LOCK_MODE) < 0)
	    msg_fatal("cannot get shared lock on %s: %m",
		      VSTREAM_PATH(bounce_info->orig_fp));
	while ((rec_type =
		rec_get(bounce_info->orig_fp, bounce_info->buf, 0)) > 0) {

	    /*
	     * Postfix version dependent: data offset in SIZE record.
	     */
	    if (rec_type == REC_TYPE_SIZE) {
		if (bounce_info->message_size == 0)
		    sscanf(STR(bounce_info->buf), "%ld %ld",
			   &bounce_info->message_size,
			   &bounce_info->orig_offs);
		if (bounce_info->message_size < 0)
		    bounce_info->message_size = 0;
		if (bounce_info->orig_offs < 0)
		    bounce_info->orig_offs = 0;
	    }

	    /*
	     * Information for the Arrival-Date: attribute.
	     */
	    else if (rec_type == REC_TYPE_TIME) {
		if (bounce_info->arrival_time == 0
		    && (bounce_info->arrival_time = atol(STR(bounce_info->buf))) < 0)
		    bounce_info->arrival_time = 0;
	    }

	    /*
	     * Information for the X-Postfix-Sender: attribute.
	     */
	    else if (rec_type == REC_TYPE_FROM) {
		quote_822_local_flags(bounce_info->sender,
				      VSTRING_LEN(bounce_info->buf) ?
				      STR(bounce_info->buf) :
				      mail_addr_mail_daemon(),
				      QUOTE_FLAG_8BITCLEAN);
	    }

	    /*
	     * Backwards compatibility: no data offset in SIZE record.
	     */
	    else if (rec_type == REC_TYPE_MESG) {
		/* XXX Future: sender+recipient after message content. */
		if (VSTRING_LEN(bounce_info->sender) == 0)
		    msg_warn("%s: no sender before message content record",
			     bounce_info->queue_id);
		bounce_info->orig_offs = vstream_ftell(bounce_info->orig_fp);
		skip_message_segment = 1;
	    }

	    /*
	     * Extract Message-ID from extracted segment, for use in threaded
	     * bounces.
	     */
	    else if (rec_type == REC_TYPE_ATTR && var_threaded_bounce) {
		char   *cp = STR(bounce_info->buf);
		ssize_t len = sizeof(MAIL_ATTR_MESSAGE_ID);
		char   *err;

		if (strncmp(cp, MAIL_ATTR_MESSAGE_ID "=", len) == 0) {
		    cp += len;
		    if ((err = extpar(&cp, "<>", EXTPAR_FLAG_NONE)) != 0) {
			msg_warn("%s: malformed Message-ID attribute: %s",
				 bounce_info->queue_id, err);
			myfree(err);
		    } else {
			vstring_sprintf(bounce_info->orig_msgid, "<%s>", cp);
		    }
		}
	    }

	    /*
	     * Are we done yet?
	     */
	    if (bounce_info->orig_offs > 0
		&& bounce_info->arrival_time > 0
		&& VSTRING_LEN(bounce_info->sender) > 0
		&& (var_threaded_bounce == 0
		    || VSTRING_LEN(bounce_info->orig_msgid) > 0)) {
		break;
	    }

	    /*
	     * Skip over (the remainder of) the message segment. If that
	     * fails, degrade.
	     */
	    if (skip_message_segment) {
		if (vstream_fseek(bounce_info->orig_fp,
				  bounce_info->orig_offs +
				  bounce_info->message_size,
				  SEEK_SET) < 0)
		     /* void */ ;
		skip_message_segment = 0;
	    }
	}
    }
    return (bounce_info);
}

/* bounce_mail_init - initialize */

BOUNCE_INFO *bounce_mail_init(const char *service,
			              const char *queue_name,
			              const char *queue_id,
			              const char *encoding,
			              int sendopts,
			              const char *dsn_envid,
			              BOUNCE_TEMPLATE *template)
{
    BOUNCE_INFO *bounce_info;
    BOUNCE_LOG *log_handle;
    RCPT_BUF *rcpt_buf;
    DSN_BUF *dsn_buf;

    /*
     * Initialize the bounce_info structure. If the bounce log cannot be
     * found, do not raise a fatal run-time error. There is nothing we can do
     * about the error, and all we are doing is to inform the sender of a
     * delivery problem, Bouncing a message does not have to be a perfect
     * job. But if the system IS running out of resources, raise a fatal
     * run-time error and force a backoff.
     */
    if ((log_handle = bounce_log_open(service, queue_id, O_RDONLY, 0)) == 0) {
	if (errno != ENOENT)
	    msg_fatal("open %s %s: %m", service, queue_id);
	rcpt_buf = 0;
	dsn_buf = 0;
    } else {
	rcpt_buf = rcpb_create();
	dsn_buf = dsb_create();
    }
    bounce_info = bounce_mail_alloc(service, queue_name, queue_id, encoding,
				    sendopts, dsn_envid, rcpt_buf, dsn_buf,
				    template, log_handle);
    return (bounce_info);
}

/* bounce_mail_one_init - initialize */

BOUNCE_INFO *bounce_mail_one_init(const char *queue_name,
				          const char *queue_id,
				          const char *encoding,
				          int sendopts,
				          const char *dsn_envid,
				          RCPT_BUF *rcpt_buf,
				          DSN_BUF *dsn_buf,
				          BOUNCE_TEMPLATE *template)
{
    BOUNCE_INFO *bounce_info;

    /*
     * Initialize the bounce_info structure for just one recipient.
     */
    bounce_info = bounce_mail_alloc("none", queue_name, queue_id, encoding,
				    sendopts, dsn_envid, rcpt_buf, dsn_buf,
				    template, (BOUNCE_LOG *) 0);
    return (bounce_info);
}

/* bounce_mail_free - undo bounce_mail_init */

void    bounce_mail_free(BOUNCE_INFO *bounce_info)
{
    if (bounce_info->log_handle) {
	if (bounce_log_close(bounce_info->log_handle))
	    msg_warn("%s: read bounce log %s: %m",
		     bounce_info->queue_id, bounce_info->queue_id);
	vstring_free(bounce_info->orig_msgid);
	rcpb_free(bounce_info->rcpt_buf);
	dsb_free(bounce_info->dsn_buf);
    }
    if (bounce_info->orig_fp && vstream_fclose(bounce_info->orig_fp))
	msg_warn("%s: read message file %s %s: %m",
		 bounce_info->queue_id, bounce_info->queue_name,
		 bounce_info->queue_id);
    vstring_free(bounce_info->buf);
    vstring_free(bounce_info->sender);
    myfree(bounce_info->mail_name);
    myfree((void *) bounce_info->mime_boundary);
    myfree((void *) bounce_info);
}

/* bounce_header - generate bounce message header */

int     bounce_header(VSTREAM *bounce, BOUNCE_INFO *bounce_info,
		              const char *dest, int postmaster_copy)
{
    BOUNCE_TEMPLATE *template = bounce_info->template;

    /*
     * Print a minimal bounce header. The cleanup service will add other
     * headers and will make all addresses fully qualified.
     */
#define STREQ(a, b) (strcasecmp((a), (b)) == 0)
#define STRNE(a, b) (strcasecmp((a), (b)) != 0)

    /*
     * Generic headers.
     */
    bounce_template_headers(post_mail_fprintf, bounce, template,
			    STR(quote_822_local(bounce_info->buf, dest)),
			    postmaster_copy);

    /*
     * References and Reply-To header that references the original message-id
     * for better threading in MUAs.
     */
    if (VSTRING_LEN(bounce_info->orig_msgid) > 0) {
	post_mail_fprintf(bounce, "References: %s", STR(bounce_info->orig_msgid));
	post_mail_fprintf(bounce, "In-Reply-To: %s", STR(bounce_info->orig_msgid));
    }

    /*
     * Trade confidentiality against availability. See also up-stream code in
     * edit_notification_properties().
     */
    if (var_tls_required_enable
	&& (bounce_info->sendopts & SOPT_REQUIRETLS_HEADER) != 0)
	post_mail_fprintf(bounce, "TLS-Required: no");

    /*
     * Auto-Submitted header, as per RFC 3834.
     */
    post_mail_fprintf(bounce, "Auto-Submitted: %s", postmaster_copy ?
		      "auto-generated" : "auto-replied");

    /*
     * MIME header. Use 8bit encoding when either the bounced message or the
     * template requires it.
     */
    post_mail_fprintf(bounce, "MIME-Version: 1.0");
    post_mail_fprintf(bounce, "Content-Type: %s; report-type=%s;",
		      "multipart/report", "delivery-status");
    post_mail_fprintf(bounce, "\tboundary=\"%s\"", bounce_info->mime_boundary);
    if (bounce_info->mime_encoding)
	post_mail_fprintf(bounce, "Content-Transfer-Encoding: %s",
		     STREQ(bounce_info->mime_encoding, MAIL_ATTR_ENC_7BIT) ?
			  bounce_template_encoding(template) :
			  bounce_info->mime_encoding);
    post_mail_fputs(bounce, "");
    post_mail_fputs(bounce, "This is a MIME-encapsulated message.");

    /*
     * MIME header.
     */
#define NOT_US_ASCII(tp) \
	STRNE(bounce_template_charset(template), "us-ascii")

#define NOT_7BIT_MIME(bp) \
	(bp->mime_encoding && STRNE(bp->mime_encoding, MAIL_ATTR_ENC_7BIT))

    post_mail_fputs(bounce, "");
    post_mail_fprintf(bounce, "--%s", bounce_info->mime_boundary);
    post_mail_fprintf(bounce, "Content-Description: %s", "Notification");
    /* Fix 20140718: UTF-8 address or $myhostname expansion. */
    post_mail_fprintf(bounce, "Content-Type: %s; charset=%s",
		      "text/plain", NOT_US_ASCII(template) ?
		      bounce_template_charset(template) :
		      NOT_7BIT_MIME(bounce_info) ?
		      "utf-8" : "us-ascii");
    /* Fix 20140709: addresses may be 8bit. */
    if (NOT_7BIT_MIME(bounce_info))
	post_mail_fprintf(bounce, "Content-Transfer-Encoding: %s",
			  bounce_info->mime_encoding);
    post_mail_fputs(bounce, "");

    return (vstream_ferror(bounce));
}

/* bounce_boilerplate - generate boiler-plate text */

int     bounce_boilerplate(VSTREAM *bounce, BOUNCE_INFO *bounce_info)
{

    /*
     * Print the boiler-plate text.
     */
    bounce_template_expand(post_mail_fputs, bounce, bounce_info->template);
    return (vstream_ferror(bounce));
}

/* bounce_print - line_wrap callback */

static void bounce_print(const char *str, int len, int indent, void *context)
{
    VSTREAM *bounce = (VSTREAM *) context;

    post_mail_fprintf(bounce, "%*s%.*s", indent, "", len, str);
}

/* bounce_print_wrap - print and wrap a line */

static void bounce_print_wrap(VSTREAM *bounce, BOUNCE_INFO *bounce_info,
			              const char *format,...)
{
    va_list ap;

#define LENGTH	79
#define INDENT	4

    va_start(ap, format);
    vstring_vsprintf(bounce_info->buf, format, ap);
    va_end(ap);
    line_wrap(STR(bounce_info->buf), LENGTH, INDENT,
	      bounce_print, (void *) bounce);
}

/* bounce_recipient_log - send one bounce log report entry */

int     bounce_recipient_log(VSTREAM *bounce, BOUNCE_INFO *bounce_info)
{
    RECIPIENT *rcpt = &bounce_info->rcpt_buf->rcpt;
    DSN    *dsn = &bounce_info->dsn_buf->dsn;

    /*
     * Mask control and non-ASCII characters (done in bounce_log_read()),
     * wrap long lines and prepend one blank, so this data can safely be
     * piped into other programs. Sort of like TCP Wrapper's safe_finger
     * program.
     */
#define NON_NULL_EMPTY(s) ((s) && *(s))

    post_mail_fputs(bounce, "");
    if (NON_NULL_EMPTY(rcpt->orig_addr)) {
	bounce_print_wrap(bounce, bounce_info, "<%s> (expanded from <%s>): %s",
			  rcpt->address, rcpt->orig_addr, dsn->reason);
    } else {
	bounce_print_wrap(bounce, bounce_info, "<%s>: %s",
			  rcpt->address, dsn->reason);
    }
    return (vstream_ferror(bounce));
}

/* bounce_diagnostic_log - send bounce log report */

int     bounce_diagnostic_log(VSTREAM *bounce, BOUNCE_INFO *bounce_info,
			              int notify_filter)
{
    RECIPIENT *rcpt = &bounce_info->rcpt_buf->rcpt;
    int     count = 0;

    /*
     * Append a human-readable copy of the delivery error log. We're doing a
     * best effort, so there is no point raising a fatal run-time error in
     * case of a logfile read error.
     * 
     * XXX DSN If the logfile with failed recipients is unavailable, pretend
     * that we found something anyway, so that this notification will not be
     * canceled.
     */
    if (bounce_info->log_handle == 0
	|| bounce_log_rewind(bounce_info->log_handle)) {
	if (IS_FAILURE_TEMPLATE(bounce_info->template)) {
	    post_mail_fputs(bounce, "");
	    post_mail_fputs(bounce, "\t--- Delivery report unavailable ---");
	    count = 1;				/* XXX don't abort */
	}
    } else {
	while (bounce_log_read(bounce_info->log_handle, bounce_info->rcpt_buf,
			       bounce_info->dsn_buf) != 0) {
	    if (rcpt->dsn_notify == 0		/* compat */
		|| (rcpt->dsn_notify & notify_filter)) {
		count++;
		if (bounce_recipient_log(bounce, bounce_info) != 0)
		    break;
	    }
	}
    }
    return (vstream_ferror(bounce) ? -1 : count);
}

/* bounce_header_dsn - send per-MTA bounce DSN records */

int     bounce_header_dsn(VSTREAM *bounce, BOUNCE_INFO *bounce_info)
{

    /*
     * MIME header.
     */
    post_mail_fputs(bounce, "");
    post_mail_fprintf(bounce, "--%s", bounce_info->mime_boundary);
    post_mail_fprintf(bounce, "Content-Description: %s",
		      "Delivery report");
    /* Generate *global* only if the original requested SMTPUTF8 support. */
    post_mail_fprintf(bounce, "Content-Type: message/%sdelivery-status",
		      (bounce_info->sendopts & SMTPUTF8_FLAG_REQUESTED) ?
		      "global-" : "");
    /* Fix 20140709: addresses may be 8bit. */
    if (NOT_7BIT_MIME(bounce_info)
    /* BC Fix 20170610: prevent MIME downgrade of message/delivery-status. */
	&& (bounce_info->sendopts & SMTPUTF8_FLAG_REQUESTED))
	post_mail_fprintf(bounce, "Content-Transfer-Encoding: %s",
			  bounce_info->mime_encoding);

    /*
     * According to RFC 1894: The body of a message/delivery-status consists
     * of one or more "fields" formatted according to the ABNF of RFC 822
     * header "fields" (see [6]).  The per-message fields appear first,
     * followed by a blank line.
     */
    post_mail_fputs(bounce, "");
    post_mail_fprintf(bounce, "Reporting-MTA: dns; %s", var_myhostname);
#if 0
    post_mail_fprintf(bounce, "Received-From-MTA: dns; %s", "whatever");
#endif
    if (NON_NULL_EMPTY(bounce_info->dsn_envid)) {
	post_mail_fprintf(bounce, "Original-Envelope-Id: %s",
			  bounce_info->dsn_envid);
    }
    post_mail_fprintf(bounce, "X-%s-Queue-ID: %s",
		      bounce_info->mail_name, bounce_info->queue_id);

#define IS_UTF8_ADDRESS(str) \
	((str)[0] != 0 && !allascii(str) && valid_utf8_stringz(str))

    /* Fix 20140708: use "utf-8" or "rfc822" as appropriate. */
    if (VSTRING_LEN(bounce_info->sender) > 0)
	post_mail_fprintf(bounce, "X-%s-Sender: %s; %s",
			  bounce_info->mail_name,
			  (bounce_info->sendopts & SMTPUTF8_FLAG_ALL)
			  && IS_UTF8_ADDRESS(STR(bounce_info->sender)) ?
			  "utf-8" : "rfc822", STR(bounce_info->sender));
    if (bounce_info->arrival_time > 0)
	post_mail_fprintf(bounce, "Arrival-Date: %s",
			  mail_date(bounce_info->arrival_time));
    return (vstream_ferror(bounce));
}

/* bounce_recipient_dsn - send per-recipient DSN records */

int     bounce_recipient_dsn(VSTREAM *bounce, BOUNCE_INFO *bounce_info)
{
    RECIPIENT *rcpt = &bounce_info->rcpt_buf->rcpt;
    DSN    *dsn = &bounce_info->dsn_buf->dsn;

    post_mail_fputs(bounce, "");
    /* Fix 20140708: Don't send "utf-8" type with non-UTF8 address. */
    post_mail_fprintf(bounce, "Final-Recipient: %s; %s",
		      (bounce_info->sendopts & SMTPUTF8_FLAG_ALL)
		      && IS_UTF8_ADDRESS(rcpt->address) ?
		      "utf-8" : "rfc822", rcpt->address);

    /*
     * XXX DSN
     * 
     * RFC 3464 section 6.3.d: "If no ORCPT parameter was provided for this
     * recipient, the Original-Recipient field MUST NOT appear."
     * 
     * This is inconsistent with section 5.2.1.d: "If no ORCPT parameter was
     * present in the RCPT command when the message was received, an ORCPT
     * parameter MAY be added to the RCPT command when the message is
     * relayed.". Postfix adds an ORCPT parameter under these conditions.
     * 
     * Therefore, all down-stream MTAs will send DSNs with Original-Recipient
     * field containing this same ORCPT value. When a down-stream MTA can use
     * that information in their DSNs, it makes no sense that an up-stream
     * MTA can't use that same information in its own DSNs.
     * 
     * Postfix always reports an Original-Recipient field, because it is more
     * more useful and more consistent.
     */
    if (NON_NULL_EMPTY(rcpt->dsn_orcpt)) {
	post_mail_fprintf(bounce, "Original-Recipient: %s", rcpt->dsn_orcpt);
    } else if (NON_NULL_EMPTY(rcpt->orig_addr)) {
	/* Fix 20140708: Don't send "utf-8" type with non-UTF8 address. */
	post_mail_fprintf(bounce, "Original-Recipient: %s; %s",
			  (bounce_info->sendopts & SMTPUTF8_FLAG_ALL)
			  && IS_UTF8_ADDRESS(rcpt->orig_addr) ?
			  "utf-8" : "rfc822", rcpt->orig_addr);
    }
    post_mail_fprintf(bounce, "Action: %s",
		      IS_FAILURE_TEMPLATE(bounce_info->template) ?
		      "failed" : dsn->action);
    post_mail_fprintf(bounce, "Status: %s", dsn->status);
    if (NON_NULL_EMPTY(dsn->mtype) && NON_NULL_EMPTY(dsn->mname))
	bounce_print_wrap(bounce, bounce_info, "Remote-MTA: %s; %s",
			  dsn->mtype, dsn->mname);
    if (NON_NULL_EMPTY(dsn->dtype) && NON_NULL_EMPTY(dsn->dtext))
	bounce_print_wrap(bounce, bounce_info, "Diagnostic-Code: %s; %s",
			  dsn->dtype, dsn->dtext);
    else
	bounce_print_wrap(bounce, bounce_info, "Diagnostic-Code: X-%s; %s",
			  bounce_info->mail_name, dsn->reason);
#if 0
    if (dsn->time > 0)
	post_mail_fprintf(bounce, "Last-Attempt-Date: %s",
			  mail_date(dsn->time));
#endif
    if (IS_DELAY_TEMPLATE(bounce_info->template))
	post_mail_fprintf(bounce, "Will-Retry-Until: %s",
		 mail_date(bounce_info->arrival_time + var_max_queue_time));
    return (vstream_ferror(bounce));
}

/* bounce_diagnostic_dsn - send bounce log report, machine readable form */

int     bounce_diagnostic_dsn(VSTREAM *bounce, BOUNCE_INFO *bounce_info,
			              int notify_filter)
{
    RECIPIENT *rcpt = &bounce_info->rcpt_buf->rcpt;
    int     count = 0;

    /*
     * Append a machine-readable copy of the delivery error log. We're doing
     * a best effort, so there is no point raising a fatal run-time error in
     * case of a logfile read error.
     * 
     * XXX DSN If the logfile with failed recipients is unavailable, pretend
     * that we found something anyway, so that this notification will not be
     * canceled.
     */
    if (bounce_info->log_handle == 0
	|| bounce_log_rewind(bounce_info->log_handle)) {
	if (IS_FAILURE_TEMPLATE(bounce_info->template))
	    count = 1;				/* XXX don't abort */
    } else {
	while (bounce_log_read(bounce_info->log_handle, bounce_info->rcpt_buf,
			       bounce_info->dsn_buf) != 0) {
	    if (rcpt->dsn_notify == 0		/* compat */
		|| (rcpt->dsn_notify & notify_filter)) {
		count++;
		if (bounce_recipient_dsn(bounce, bounce_info) != 0)
		    break;
	    }
	}
    }
    return (vstream_ferror(bounce) ? -1 : count);
}

/* bounce_original - send a copy of the original to the victim */

int     bounce_original(VSTREAM *bounce, BOUNCE_INFO *bounce_info,
			        int headers_only)
{
    int     status = 0;
    int     rec_type = 0;

    /*
     * When truncating a large message, don't damage the MIME structure: send
     * the message headers only.
     */
    if (var_bounce_limit > 0
	&& bounce_info->orig_fp
	&& (bounce_info->message_size <= 0
	    || bounce_info->message_size > var_bounce_limit))
	headers_only = DSN_RET_HDRS;

    /*
     * MIME headers.
     */
#define IS_UNDELIVERED_TEMPLATE(template) \
        (IS_FAILURE_TEMPLATE(template) || IS_DELAY_TEMPLATE(template))

    post_mail_fputs(bounce, "");
    post_mail_fprintf(bounce, "--%s", bounce_info->mime_boundary);
    post_mail_fprintf(bounce, "Content-Description: %s%s",
		      IS_UNDELIVERED_TEMPLATE(bounce_info->template) ?
		      "Undelivered " : "",
		      headers_only == DSN_RET_HDRS ?
		      "Message Headers" : "Message");
    /* Generate *global* only if the original requested SMTPUTF8 support. */
    if (bounce_info->sendopts & SMTPUTF8_FLAG_REQUESTED)
	post_mail_fprintf(bounce, "Content-Type: message/%s",
			  headers_only == DSN_RET_HDRS ?
			  "global-headers" : "global");
    else
	post_mail_fprintf(bounce, "Content-Type: %s",
			  headers_only == DSN_RET_HDRS ?
			  "text/rfc822-headers" : "message/rfc822");
    if (NOT_7BIT_MIME(bounce_info))
	post_mail_fprintf(bounce, "Content-Transfer-Encoding: %s",
			  bounce_info->mime_encoding);
    post_mail_fputs(bounce, "");

    /*
     * Send place holder if original is unavailable.
     */
    if (bounce_info->orig_offs == 0 || vstream_fseek(bounce_info->orig_fp,
				    bounce_info->orig_offs, SEEK_SET) < 0) {
	post_mail_fputs(bounce, "\t--- Undelivered message unavailable ---");
	return (vstream_ferror(bounce));
    }

    /*
     * XXX The cleanup server removes Return-Path: headers. This should be
     * done only with mail that enters via a non-SMTP channel, but changing
     * this now could break other software. Removing Return-Path: could break
     * digital signatures, though this is unlikely. In any case,
     * header_checks are more effective when the Return-Path: header is
     * present, so we prepend one to the bounce message.
     */
    post_mail_fprintf(bounce, "Return-Path: <%s>", STR(bounce_info->sender));

    /*
     * Copy the original message contents. We're doing raw record output here
     * so that we don't throw away binary transparency yet.
     */
#define IS_HEADER(s) (IS_SPACE_TAB(*(s)) || is_header(s))

    while (status == 0 && (rec_type = rec_get(bounce_info->orig_fp, bounce_info->buf, 0)) > 0) {
	if (rec_type != REC_TYPE_NORM && rec_type != REC_TYPE_CONT)
	    break;
	if (headers_only == DSN_RET_HDRS
	    && !IS_HEADER(vstring_str(bounce_info->buf)))
	    break;
	status = (REC_PUT_BUF(bounce, rec_type, bounce_info->buf) != rec_type);
    }

    /*
     * Final MIME headers. These require -- at the end of the boundary
     * string.
     * 
     * XXX This should be a separate bounce_terminate() entry so we can be
     * assured that the terminator will always be sent.
     */
    post_mail_fputs(bounce, "");
    post_mail_fprintf(bounce, "--%s--", bounce_info->mime_boundary);

    return (status);
}

/* bounce_delrcpt - delete recipients from original queue file */

void    bounce_delrcpt(BOUNCE_INFO *bounce_info)
{
    RECIPIENT *rcpt = &bounce_info->rcpt_buf->rcpt;

    if (bounce_info->orig_fp != 0
	&& bounce_info->log_handle != 0
	&& bounce_log_rewind(bounce_info->log_handle) == 0)
	while (bounce_log_read(bounce_info->log_handle, bounce_info->rcpt_buf,
			       bounce_info->dsn_buf) != 0)
	    if (rcpt->offset > 0)
		deliver_completed(bounce_info->orig_fp, rcpt->offset);
}

/* bounce_delrcpt_one - delete one recipient from original queue file */

void    bounce_delrcpt_one(BOUNCE_INFO *bounce_info)
{
    RECIPIENT *rcpt = &bounce_info->rcpt_buf->rcpt;

    if (bounce_info->orig_fp != 0 && rcpt->offset > 0)
	deliver_completed(bounce_info->orig_fp, rcpt->offset);
}
