Eine aufbereitete Darstellung der Quelle

 
     
 
 
Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 

Benutzer

Quelle  cleanup_milter.c

  Sprache: C
 

/*++
/* NAME
/* cleanup_milter 3
/* SUMMARY
/* external mail filter support
/* SYNOPSIS
/* #include <cleanup.h>
/*
/* void cleanup_milter_header_checks_init(void)
/*
/* void cleanup_milter_receive(state, count)
/* CLEANUP_STATE *state;
/* int count;
/*
/* void cleanup_milter_inspect(state, milters)
/* CLEANUP_STATE *state;
/* MILTERS *milters;
/*
/* cleanup_milter_emul_mail(state, milters, sender)
/* CLEANUP_STATE *state;
/* MILTERS *milters;
/* const char *sender;
/*
/* cleanup_milter_emul_rcpt(state, milters, recipient)
/* CLEANUP_STATE *state;
/* MILTERS *milters;
/* const char *recipient;
/*
/* cleanup_milter_emul_data(state, milters)
/* CLEANUP_STATE *state;
/* MILTERS *milters;
/* DESCRIPTION
/* This module implements support for Sendmail-style mail
/* filter (milter) applications, including in-place queue file
/* modification.
/*
/* cleanup_milter_header_checks_init() does pre-jail
/* initializations.
/*
/* cleanup_milter_receive() receives mail filter definitions,
/* typically from an smtpd(8) server process, and registers
/* local call-back functions for macro expansion and for queue
/* file modification.
/*
/* cleanup_milter_inspect() sends the current message headers
/* and body to the mail filters that were received with
/* cleanup_milter_receive(), or that are specified with the
/* cleanup_milters configuration parameter.
/*
/* cleanup_milter_emul_mail() emulates connect, helo and mail
/* events for mail that does not arrive via the smtpd(8) server.
/* The emulation pretends that mail arrives from localhost/127.0.0.1
/* via ESMTP. Milters can reject emulated connect, helo, mail
/* or data events, but not emulated rcpt events as described
/* next.
/*
/* cleanup_milter_emul_rcpt() emulates an rcpt event for mail
/* that does not arrive via the smtpd(8) server. This reports
/* a server configuration error condition when the milter
/* rejects an emulated rcpt event.
/*
/* cleanup_milter_emul_data() emulates a data event for mail
/* that does not arrive via the smtpd(8) server.  It's OK for
/* milters to reject emulated data events.
/* SEE ALSO
/* milter(3) generic mail filter interface
/* DIAGNOSTICS
/* Fatal errors: memory allocation problem.
/* Panic: interface violation.
/* Warnings: I/O errors (state->errs is updated accordingly).
/* 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
/*--*/


/* System library. */

#include <sys_defs.h>
#include <sys/socket.h>   /* AF_INET */
#include <string.h>
#include <errno.h>

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

/* Utility library. */

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

/* Global library. */

#include <off_cvt.h>
#include <dsn_mask.h>
#include <rec_type.h>
#include <cleanup_user.h>
#include <record.h>
#include <rec_attr_map.h>
#include <mail_proto.h>
#include <mail_params.h>
#include <lex_822.h>
#include <is_header.h>
#include <quote_821_local.h>
#include <dsn_util.h>
#include <xtext.h>
#include <info_log_addr_form.h>
#include <header_opts.h>

/* Application-specific. */

#include <cleanup.h>

 /*
  * How Postfix 2.4 edits queue file information:
  * 
  * Mail filter applications (Milters) can send modification requests after
  * receiving the end of the message body.  Postfix implements these
  * modifications in the cleanup server, so that it can edit the queue file
  * in place. This avoids the temporary files that would be needed when
  * modifications were implemented in the SMTP server (Postfix normally does
  * not store the whole message in main memory). Once a Milter is done
  * editing, the queue file can be used as input for the next Milter, and so
  * on. Finally, the cleanup server changes file permissions, calls fsync(),
  * and waits for successful completion.
  * 
  * To implement in-place queue file edits, we need to introduce surprisingly
  * little change to the existing Postfix queue file structure.  All we need
  * is a way to mark a record as deleted, and to jump from one place in the
  * queue file to another. We could implement deleted records with jumps, but
  * marking is sometimes simpler.
  * 
  * Postfix does not store queue files as plain text files. Instead all
  * information is stored in records with an explicit type and length, for
  * sender, recipient, arrival time, and so on.  Even the content that makes
  * up the message header and body is stored as records with explicit types
  * and lengths.  This organization makes it very easy to mark a record as
  * deleted, and to introduce the pointer records that we will use to jump
  * from one place in a queue file to another place.
  * 
  * - Deleting a recipient is easiest - simply modify the record type into one
  * that is skipped by the software that delivers mail. We won't try to reuse
  * the deleted recipient for other purposes. When deleting a recipient, we
  * may need to delete multiple recipient records that result from virtual
  * alias expansion of the original recipient address.
  * 
  * - Replacing a header record involves pointer records. A record is replaced
  * by overwriting it with a forward pointer to space after the end of the
  * queue file, putting the new record there, followed by a reverse pointer
  * to the record that follows the replaced header. To simplify
  * implementation we follow a short header record with a filler record so
  * that we can always overwrite a header record with a pointer.
  * 
  * N.B. This is a major difference with Postfix version 2.3, which needed
  * complex code to save records that follow a short header, before it could
  * overwrite a short header record. This code contained two of the three
  * post-release bugs that were found with Postfix header editing.
  * 
  * - Inserting a header record is like replacing one, except that we also
  * relocate the record that is being overwritten by the forward pointer.
  * 
  * - Deleting a message header is simplest when we replace it by a "skip"
  * pointer to the information that follows the header. With a multi-line
  * header we need to update only the first line.
  * 
  * - Appending a recipient or header record involves pointer records as well.
  * To make this convenient, the queue file already contains dummy pointer
  * records at the locations where we want to append recipient or header
  * content. To append, change the dummy pointer into a forward pointer to
  * space after the end of a message, put the new recipient or header record
  * there, followed by a reverse pointer to the record that follows the
  * forward pointer.
  * 
  * - To append another header or recipient record, replace the reverse pointer
  * by a forward pointer to space after the end of a message, put the new
  * record there, followed by the value of the reverse pointer that we
  * replace. Thus, there is no one-to-one correspondence between forward and
  * backward pointers. Instead, there can be multiple forward pointers for
  * one reverse pointer.
  * 
  * - When a mail filter wants to replace an entire body, we overwrite existing
  * body records until we run out of space, and then write a pointer to space
  * after the end of the queue file, followed by more body content. There may
  * be multiple regions with body content; regions are connected by forward
  * pointers, and the last region ends with a pointer to the marker that ends
  * the message content segment. Body regions can be large and therefore they
  * are reused to avoid wasting space. Sendmail mail filters currently do not
  * replace individual body records, and that is a good thing.
  * 
  * Making queue file modifications safe:
  * 
  * Postfix queue files are segmented. The first segment is for envelope
  * records, the second for message header and body content, and the third
  * segment is for information that was extracted or generated from the
  * message header or body content.  Each segment is terminated by a marker
  * record. For now we don't want to change their location. That is, we want
  * to avoid moving the records that mark the start or end of a queue file
  * segment.
  * 
  * To ensure that we can always replace a header or body record by a pointer
  * record, without having to relocate a marker record, the cleanup server
  * places a dummy pointer record at the end of the recipients and at the end
  * of the message header. To support message body modifications, a dummy
  * pointer record is also placed at the end of the message content.
  * 
  * With all these changes in queue file organization, REC_TYPE_END is no longer
  * guaranteed to be the last record in a queue file. If an application were
  * to read beyond the REC_TYPE_END marker, it would go into an infinite
  * loop, because records after REC_TYPE_END alternate with reverse pointers
  * to the middle of the queue file. For robustness, the record reading
  * routine skips forward to the end-of-file position after reading the
  * REC_TYPE_END marker.
  */


/*#define msg_verbose 2*/

static HBC_CHECKS *cleanup_milter_hbc_checks;
static VSTRING *cleanup_milter_hbc_reply;
static void cleanup_milter_set_error(CLEANUP_STATE *, int);
static const char *cleanup_add_rcpt_par(void *, const char *, const char *);

#define STR(x)  vstring_str(x)
#define LEN(x)  VSTRING_LEN(x)

/* cleanup_milter_hbc_log - log post-milter header/body_checks action */

static void cleanup_milter_hbc_log(void *context, const char *action,
            const char *where, const char *line,
               const char *optional_text)
{
    const CLEANUP_STATE *state = (CLEANUP_STATE *) context;
    const char *attr;

    vstring_sprintf(state->temp1, "%s: milter-%s-%s: %s %.200s from %s[%s];",
      state->queue_id, where, action, where, line,
      state->client_name, state->client_addr);
    if (state->sender)
 vstring_sprintf_append(state->temp1, " from=<%s>",
          info_log_addr_form_sender(state->sender));
    if (state->recip)
 vstring_sprintf_append(state->temp1, " to=<%s>",
          info_log_addr_form_recipient(state->recip));
    if ((attr = nvtable_find(state->attr, MAIL_ATTR_LOG_PROTO_NAME)) != 0)
 vstring_sprintf_append(state->temp1, " proto=%s", attr);
    if ((attr = nvtable_find(state->attr, MAIL_ATTR_LOG_HELO_NAME)) != 0)
 vstring_sprintf_append(state->temp1, " helo=<%s>", attr);
    if (optional_text)
 vstring_sprintf_append(state->temp1, ": %s", optional_text);
    msg_info("%s", vstring_str(state->temp1));
}

/* cleanup_milter_header_prepend - prepend header to milter-generated header */

static void cleanup_milter_header_prepend(void *context, int rec_type,
            const char *buf, ssize_t len, off_t offset)
{
    /* XXX save prepended header to buffer. */
    msg_warn("the milter_header/body_checks prepend action is not implemented");
}

/* cleanup_milter_hbc_extend - additional header/body_checks actions */

static char *cleanup_milter_hbc_extend(void *context, const char *command,
            ssize_t cmd_len, const char *optional_text,
             const char *where, const char *buf,
                   ssize_t buf_len, off_t offset)
{
    CLEANUP_STATE *state = (CLEANUP_STATE *) context;
    const char *map_class = VAR_MILT_HEAD_CHECKS; /* XXX */

#define STREQUAL(x,y,l) (strncasecmp((x), (y), (l)) == 0 && (y)[l] == 0)

    /*
     * These are currently our mutually-exclusive ways of not receiving mail:
     * "reject" and "discard". Only these can be reported to the up-stream
     * Postfix libmilter code, because sending any reply there causes Postfix
     * libmilter to skip further "edit" requests. By way of safety net, each
     * of these must also reset CLEANUP_FLAG_FILTER_ALL.
     */

#define CLEANUP_MILTER_REJECTING_OR_DISCARDING_MESSAGE(state) \
    ((state->flags & CLEANUP_FLAG_DISCARD) || (state->errs & CLEANUP_STAT_CONT))

    /*
     * We log all header/body-checks actions here, because we know the
     * details of the message content that triggered the action. We report
     * detail-free milter-reply values (reject/discard, stored in the
     * milter_hbc_reply state member) to the Postfix libmilter code, so that
     * Postfix libmilter can stop sending requests.
     * 
     * We also set all applicable cleanup flags here, because there is no
     * guarantee that Postfix libmilter will propagate our own milter-reply
     * value to cleanup_milter_inspect() which calls cleanup_milter_apply().
     * The latter translates responses from Milter applications into cleanup
     * flags, and logs the response text. Postfix libmilter can convey only
     * one milter-reply value per email message, and that reply may even come
     * from outside Postfix.
     * 
     * To suppress redundant logging, cleanup_milter_apply() does nothing when
     * the milter-reply value matches the saved text in the milter_hbc_reply
     * state member. As we remember only one milter-reply value, we can't
     * report multiple milter-reply values per email message. We satisfy this
     * constraint, because we already clear the CLEANUP_FLAG_FILTER_ALL flags
     * to terminate further header inspection.
     */

    if ((state->flags & CLEANUP_FLAG_FILTER_ALL) == 0)
 return ((char *) buf);

    if (STREQUAL(command, "BCC", cmd_len)) {
 if (strchr(optional_text, '@') == 0) {
     msg_warn("bad BCC address \"%s\" in %s map -- "
       "need user@domain",
       optional_text, VAR_MILT_HEAD_CHECKS);
 } else {
     cleanup_milter_hbc_log(context, "bcc", where, buf, optional_text);
     /* Caller checks state error flags. */
     (void) cleanup_add_rcpt_par(state, optional_text, "");
 }
 return ((char *) buf);
    }
    if (STREQUAL(command, "REJECT", cmd_len)) {
 const CLEANUP_STAT_DETAIL *detail;

 if (state->reason)
     myfree(state->reason);
 detail = cleanup_stat_detail(CLEANUP_STAT_CONT);
 if (*optional_text) {
     state->reason = dsn_prepend(detail->dsn, optional_text);
     if (*state->reason != '4' && *state->reason != '5') {
  msg_warn("bad DSN action in %s -- need 4.x.x or 5.x.x",
    optional_text);
  *state->reason = '4';
     }
 } else {
     state->reason = dsn_prepend(detail->dsn, detail->text);
 }
 if (*state->reason == '4')
     state->errs |= CLEANUP_STAT_DEFER;
 else
     state->errs |= CLEANUP_STAT_CONT;
 state->flags &= ~CLEANUP_FLAG_FILTER_ALL;
 cleanup_milter_hbc_log(context, "reject", where, buf, state->reason);
 vstring_sprintf(cleanup_milter_hbc_reply, "%d %s",
   detail->smtp, state->reason);
 STR(cleanup_milter_hbc_reply)[0] = *state->reason;
 return ((char *) buf);
    }
    if (STREQUAL(command, "FILTER", cmd_len)) {
 if (*optional_text == 0) {
     msg_warn("missing FILTER command argument in %s map", map_class);
 } else if (strchr(optional_text, ':') == 0) {
     msg_warn("bad FILTER command %s in %s -- "
       "need transport:destination",
       optional_text, map_class);
 } else {
     if (state->filter)
  myfree(state->filter);
     state->filter = mystrdup(optional_text);
     cleanup_milter_hbc_log(context, "filter", where, buf,
       optional_text);
 }
 return ((char *) buf);
    }
    if (STREQUAL(command, "DISCARD", cmd_len)) {
 cleanup_milter_hbc_log(context, "discard", where, buf, optional_text);
 vstring_strcpy(cleanup_milter_hbc_reply, "D");
 state->flags |= CLEANUP_FLAG_DISCARD;
 state->flags &= ~CLEANUP_FLAG_FILTER_ALL;
 return ((char *) buf);
    }
    if (STREQUAL(command, "HOLD", cmd_len)) {
 if ((state->flags & (CLEANUP_FLAG_HOLD | CLEANUP_FLAG_DISCARD)) == 0) {
     cleanup_milter_hbc_log(context, "hold", where, buf, optional_text);
     state->flags |= CLEANUP_FLAG_HOLD;
 }
 return ((char *) buf);
    }
    if (STREQUAL(command, "REDIRECT", cmd_len)) {
 if (strchr(optional_text, '@') == 0) {
     msg_warn("bad REDIRECT target \"%s\" in %s map -- "
       "need user@domain",
       optional_text, map_class);
 } else {
     if (state->redirect)
  myfree(state->redirect);
     state->redirect = mystrdup(optional_text);
     cleanup_milter_hbc_log(context, "redirect", where, buf,
       optional_text);
     state->flags &= ~CLEANUP_FLAG_FILTER_ALL;
 }
 return ((char *) buf);
    }
    return ((char *) HBC_CHECKS_STAT_UNKNOWN);
}

/* cleanup_milter_header_checks - inspect Milter-generated header */

static int cleanup_milter_header_checks(CLEANUP_STATE *state, VSTRING *buf)
{
    char   *ret;

    /*
     * Milter application "add/insert/replace header" requests happen at the
     * end-of-message stage, therefore all the header operations are relative
     * to the primary message header.
     */

    ret = hbc_header_checks((void *) state, cleanup_milter_hbc_checks,
       MIME_HDR_PRIMARY, (HEADER_OPTS *) 0,
       buf, (off_t) 0);
    if (ret == 0) {
 return (0);
    } else if (ret == HBC_CHECKS_STAT_ERROR) {
 msg_warn("%s: %s map lookup problem -- "
   "message not accepted, try again later",
   state->queue_id, VAR_MILT_HEAD_CHECKS);
 state->errs |= CLEANUP_STAT_WRITE;
 return (0);
    } else {
 if (ret != STR(buf)) {
     vstring_strcpy(buf, ret);
     myfree(ret);
 }
 return (1);
    }
}

/* cleanup_milter_hbc_add_meta_records - add REDIRECT or FILTER meta records */

static void cleanup_milter_hbc_add_meta_records(CLEANUP_STATE *state)
{
    const char *myname = "cleanup_milter_hbc_add_meta_records";
    off_t   reverse_ptr_offset;
    off_t   new_meta_offset;

    /*
     * Note: this code runs while the Milter infrastructure is being torn
     * down. For this reason we handle all I/O errors here on the spot,
     * instead of reporting them back through the Milter infrastructure.
     */


    /*
     * Sanity check.
     */

    if (state->append_meta_pt_offset < 0)
 msg_panic("%s: no meta append pointer location", myname);
    if (state->append_meta_pt_target < 0)
 msg_panic("%s: no meta append pointer target", myname);

    /*
     * Allocate space after the end of the queue file, and write the meta
     * record(s), followed by a reverse pointer record that points to the
     * target of the old "meta record append" pointer record. This reverse
     * pointer record becomes the new "meta record append" pointer record.
     * Although the new "meta record append" pointer record will never be
     * used, we update it here to make the code more similar to other code
     * that inserts/appends content, so that common code can be factored out
     * later.
     */

    if ((new_meta_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) {
 cleanup_milter_set_error(state, errno);
 return;
    }
    if (state->filter != 0)
 cleanup_out_string(state, REC_TYPE_FILT, state->filter);
    if (state->redirect != 0)
 cleanup_out_string(state, REC_TYPE_RDR, state->redirect);
    if ((reverse_ptr_offset = vstream_ftell(state->dst)) < 0) {
 msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path);
 state->errs |= CLEANUP_STAT_WRITE;
 return;
    }
    cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
         (long) state->append_meta_pt_target);

    /*
     * Pointer flipping: update the old "meta record append" pointer record
     * value with the location of the new meta record.
     */

    if (vstream_fseek(state->dst, state->append_meta_pt_offset, SEEK_SET) < 0) {
 cleanup_milter_set_error(state, errno);
 return;
    }
    cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
         (long) new_meta_offset);

    /*
     * Update the in-memory "meta append" pointer record location with the
     * location of the reverse pointer record that follows the new meta
     * record. The target of the "meta append" pointer record does not
     * change; it's always the record that follows the dummy pointer record
     * that was written while Postfix received the message.
     */

    state->append_meta_pt_offset = reverse_ptr_offset;

    /*
     * Note: state->append_meta_pt_target never changes.
     */

}

/* cleanup_milter_header_checks_init - initialize post-Milter header checks */

void    cleanup_milter_header_checks_init(void)
{
    static const char myname[] = "cleanup_milter_header_checks_init";

#define NO_NESTED_HDR_NAME ""
#define NO_NESTED_HDR_VALUE ""
#define NO_MIME_HDR_NAME ""
#define NO_MIME_HDR_VALUE ""

    static /* XXX not const */ HBC_CALL_BACKS call_backs = {
 cleanup_milter_hbc_log,
 cleanup_milter_header_prepend,
 cleanup_milter_hbc_extend,
    };

    if (*var_milt_head_checks == 0)
 msg_panic("%s: %s is empty", myname, VAR_MILT_HEAD_CHECKS);

    if (cleanup_milter_hbc_checks)
 msg_panic("%s: cleanup_milter_hbc_checks is not null", myname);
    cleanup_milter_hbc_checks =
 hbc_header_checks_create(VAR_MILT_HEAD_CHECKS, var_milt_head_checks,
     NO_MIME_HDR_NAME, NO_MIME_HDR_VALUE,
     NO_NESTED_HDR_NAME, NO_NESTED_HDR_VALUE,
     &call_backs);

    if (cleanup_milter_hbc_reply)
 msg_panic("%s: cleanup_milter_hbc_reply is not null", myname);
    cleanup_milter_hbc_reply = vstring_alloc(100);
}

#ifdef TEST

/* cleanup_milter_header_checks_deinit - undo cleanup_milter_header_checks_init */

static void cleanup_milter_header_checks_deinit(void)
{
    static const char myname[] = "cleanup_milter_header_checks_deinit";

    if (cleanup_milter_hbc_checks == 0)
 msg_panic("%s: cleanup_milter_hbc_checks is null", myname);
    hbc_header_checks_free(cleanup_milter_hbc_checks);
    cleanup_milter_hbc_checks = 0;

    if (cleanup_milter_hbc_reply == 0)
 msg_panic("%s: cleanup_milter_hbc_reply is null", myname);
    vstring_free(cleanup_milter_hbc_reply);
    cleanup_milter_hbc_reply = 0;
}

#endif

/* cleanup_milter_header_checks_reinit - re-init post-Milter header checks */

static void cleanup_milter_header_checks_reinit(CLEANUP_STATE *state)
{
    if (state->filter)
 myfree(state->filter);
    state->filter = 0;
    if (state->redirect)
 myfree(state->redirect);
    state->redirect = 0;
    VSTRING_RESET(cleanup_milter_hbc_reply);
}

/* cleanup_milter_hbc_finish - finalize post-Milter header checks */

static void cleanup_milter_hbc_finish(CLEANUP_STATE *state)
{
    if (CLEANUP_OUT_OK(state)
 && !CLEANUP_MILTER_REJECTING_OR_DISCARDING_MESSAGE(state)
 && (state->filter || state->redirect))
 cleanup_milter_hbc_add_meta_records(state);
}

 /*
  * Milter replies.
  */

#define CLEANUP_MILTER_SET_REASON(__state, __reason) do { \
 if ((__state)->reason) \
     myfree((__state)->reason); \
 (__state)->reason = mystrdup(__reason); \
 if ((__state)->smtp_reply) { \
     myfree((__state)->smtp_reply); \
     (__state)->smtp_reply = 0; \
 } \
    } while (0)

#define CLEANUP_MILTER_SET_SMTP_REPLY(__state, __smtp_reply) do { \
 if ((__state)->reason) \
     myfree((__state)->reason); \
 (__state)->reason = mystrdup(__smtp_reply + 4); \
 printable((__state)->reason, '_'); \
 if ((__state)->smtp_reply) \
     myfree((__state)->smtp_reply); \
 (__state)->smtp_reply = mystrdup(__smtp_reply); \
    } while (0)

/* cleanup_milter_set_error - set error flag from errno */

static void cleanup_milter_set_error(CLEANUP_STATE *state, int err)
{
    if (err == EFBIG) {
 msg_warn("%s: queue file size limit exceeded", state->queue_id);
 state->errs |= CLEANUP_STAT_SIZE;
    } else {
 msg_warn("%s: write queue file: %m", state->queue_id);
 state->errs |= CLEANUP_STAT_WRITE;
    }
}

/* cleanup_milter_error - return dummy error description */

static const char *cleanup_milter_error(CLEANUP_STATE *state, int err)
{
    const char *myname = "cleanup_milter_error";
    const CLEANUP_STAT_DETAIL *dp;

    /*
     * For consistency with error reporting within the milter infrastructure,
     * content manipulation routines return a null pointer on success, and an
     * SMTP-like response on error.
     * 
     * However, when cleanup_milter_apply() receives this error response from
     * the milter infrastructure, it ignores the text since the appropriate
     * cleanup error flags were already set by cleanup_milter_set_error().
     * 
     * Specify a null error number when the "errno to error flag" mapping was
     * already done elsewhere, possibly outside this module.
     */

    if (err)
 cleanup_milter_set_error(state, err);
    else if (CLEANUP_OUT_OK(state))
 msg_panic("%s: missing errno to error flag mapping", myname);
    if (state->milter_err_text == 0)
 state->milter_err_text = vstring_alloc(50);
    dp = cleanup_stat_detail(state->errs);
    return (STR(vstring_sprintf(state->milter_err_text,
    "%d %s %s", dp->smtp, dp->dsn, dp->text)));
}

/* cleanup_add_header - append message header */

static const char *cleanup_add_header(void *context, const char *name,
                  const char *space,
                  const char *value)
{
    const char *myname = "cleanup_add_header";
    CLEANUP_STATE *state = (CLEANUP_STATE *) context;
    VSTRING *buf;
    off_t   reverse_ptr_offset;
    off_t   new_hdr_offset;

    /*
     * To simplify implementation, the cleanup server writes a dummy "header
     * append" pointer record after the last message header. We cache both
     * the location and the target of the current "header append" pointer
     * record.
     */

    if (state->append_hdr_pt_offset < 0)
 msg_panic("%s: no header append pointer location", myname);
    if (state->append_hdr_pt_target < 0)
 msg_panic("%s: no header append pointer target", myname);

    /*
     * Return early when Milter header checks request that this header record
     * be dropped, or that the message is discarded. Note: CLEANUP_OUT_OK()
     * tests CLEANUP_FLAG_DISCARD. We don't want to report the latter as an
     * error.
     */

    buf = vstring_alloc(100);
    vstring_sprintf(buf, "%s:%s%s", name, space, value);
    if (cleanup_milter_hbc_checks) {
 if (cleanup_milter_header_checks(state, buf) == 0
     || (state->flags & CLEANUP_FLAG_DISCARD)) {
     vstring_free(buf);
     return (0);
 }
 if (CLEANUP_OUT_OK(state) == 0) {
     vstring_free(buf);
     return (cleanup_milter_error(state, 0));
 }
    }

    /*
     * Allocate space after the end of the queue file, and write the header
     * record(s), followed by a reverse pointer record that points to the
     * target of the old "header append" pointer record. This reverse pointer
     * record becomes the new "header append" pointer record.
     */

    if ((new_hdr_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) {
 msg_warn("%s: seek file %s: %m", myname, cleanup_path);
 vstring_free(buf);
 return (cleanup_milter_error(state, errno));
    }
    /* XXX emit prepended header, then clear it. */
    cleanup_out_header(state, buf);  /* Includes padding */
    vstring_free(buf);
    if ((reverse_ptr_offset = vstream_ftell(state->dst)) < 0) {
 msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path);
 return (cleanup_milter_error(state, errno));
    }
    cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
         (long) state->append_hdr_pt_target);

    /*
     * Pointer flipping: update the old "header append" pointer record value
     * with the location of the new header record.
     * 
     * XXX To avoid unnecessary seek operations when the new header immediately
     * follows the old append header pointer, write a null pointer or make
     * the record reading loop smarter. Making vstream_fseek() smarter does
     * not help, because it doesn't know if we're going to read or write
     * after a write+seek sequence.
     */

    if (vstream_fseek(state->dst, state->append_hdr_pt_offset, SEEK_SET) < 0) {
 msg_warn("%s: seek file %s: %m", myname, cleanup_path);
 return (cleanup_milter_error(state, errno));
    }
    cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
         (long) new_hdr_offset);

    /*
     * Update the in-memory "header append" pointer record location with the
     * location of the reverse pointer record that follows the new header.
     * The target of the "header append" pointer record does not change; it's
     * always the record that follows the dummy pointer record that was
     * written while Postfix received the message.
     */

    state->append_hdr_pt_offset = reverse_ptr_offset;

    /*
     * In case of error while doing record output.
     */

    return (CLEANUP_OUT_OK(state) == 0 ? cleanup_milter_error(state, 0) :
     cleanup_milter_hbc_reply && LEN(cleanup_milter_hbc_reply) ?
     STR(cleanup_milter_hbc_reply) : 0);

    /*
     * Note: state->append_hdr_pt_target never changes.
     */

}

/* hidden_header - respect milter header hiding protocol */

static int hidden_header(VSTRING *buf, ARGV *auto_hdrs, int *hide_done)
{
    char  **cpp;
    int     mask;

    for (cpp = auto_hdrs->argv, mask = 1; *cpp; cpp++, mask <<= 1)
 if ((*hide_done & mask) == 0 && strncmp(*cpp, STR(buf), LEN(buf)) == 0)
     return (*hide_done |= mask);
    return (0);
}

/* cleanup_find_header_start - find specific header instance */

static off_t cleanup_find_header_start(CLEANUP_STATE *state, ssize_t index,
                   const char *header_label,
                   VSTRING *buf,
                   int *prec_type,
                   int allow_ptr_backup)
{
    const char *myname = "cleanup_find_header_start";
    off_t   curr_offset;  /* offset after found record */
    off_t   ptr_offset;   /* pointer to found record */
    VSTRING *ptr_buf = 0;
    int     rec_type = REC_TYPE_ERROR;
    int     last_type;
    ssize_t len;
    int     hide_done = 0;

    if (msg_verbose)
 msg_info("%s: index %ld name \"%s\"",
       myname, (long) index, header_label ? header_label : "(none)");

    /*
     * Sanity checks.
     */

    if (index < 1)
 msg_panic("%s: bad header index %ld", myname, (long) index);

    /*
     * Skip to the start of the message content, and read records until we
     * either find the specified header, or until we hit the end of the
     * headers.
     * 
     * The index specifies the header instance: 1 is the first one. The header
     * label specifies the header name. A null pointer matches any header.
     * 
     * When the specified header is not found, the result value is -1.
     * 
     * When the specified header is found, its first record is stored in the
     * caller-provided read buffer, and the result value is the queue file
     * offset of that record. The file read position is left at the start of
     * the next (non-filler) queue file record, which can be the remainder of
     * a multi-record header.
     * 
     * When a header is found and allow_ptr_backup is non-zero, then the result
     * is either the first record of that header, or it is the pointer record
     * that points to the first record of that header. In the latter case,
     * the file read position is undefined. Returning the pointer allows us
     * to do some optimizations when inserting text multiple times at the
     * same place.
     * 
     * XXX We can't use the MIME processor here. It not only buffers up the
     * input, it also reads the record that follows a complete header before
     * it invokes the header call-back action. This complicates the way that
     * we discover header offsets and boundaries. Worse is that the MIME
     * processor is unaware that multi-record message headers can have PTR
     * records in the middle.
     * 
     * XXX The draw-back of not using the MIME processor is that we have to
     * duplicate some of its logic here and in the routine that finds the end
     * of the header record. To minimize the duplication we define an ugly
     * macro that is used in all code that scans for header boundaries.
     * 
     * XXX Sendmail compatibility (based on Sendmail 8.13.6 measurements).
     * 
     * - When changing Received: header #1, we change the Received: header that
     * follows our own one; a request to change Received: header #0 is
     * silently treated as a request to change Received: header #1.
     * 
     * - When changing Date: header #1, we change the first Date: header; a
     * request to change Date: header #0 is silently treated as a request to
     * change Date: header #1.
     * 
     * Thus, header change requests are relative to the content as received,
     * that is, the content after our own Received: header. They can affect
     * only the headers that the MTA actually exposes to mail filter
     * applications.
     * 
     * - However, when inserting a header at position 0, the new header appears
     * before our own Received: header, and when inserting at position 1, the
     * new header appears after our own Received: header.
     * 
     * Thus, header insert operations are relative to the content as delivered,
     * that is, the content including our own Received: header.
     * 
     * None of the above is applicable after a Milter inserts a header before
     * our own Received: header. From then on, our own Received: header
     * becomes just like other headers.
     */

#define CLEANUP_FIND_HEADER_NOTFOUND (-1)
#define CLEANUP_FIND_HEADER_IOERROR (-2)

#define CLEANUP_FIND_HEADER_RETURN(offs) do { \
 if (ptr_buf) \
     vstring_free(ptr_buf); \
 return (offs); \
    } while (0)

#define GET_NEXT_TEXT_OR_PTR_RECORD(rec_type, state, buf, curr_offset, quit) \
    if ((rec_type = rec_get_raw(state->dst, buf, 0, REC_FLAG_NONE)) < 0) { \
 msg_warn("%s: read file %s: %m", myname, cleanup_path); \
 cleanup_milter_set_error(state, errno); \
 do { quit; } while (0); \
    } \
    if (msg_verbose > 1) \
 msg_info("%s: read: %ld: %.*s", myname, (long) curr_offset, \
   LEN(buf) > 30 ? 30 : (int) LEN(buf), STR(buf)); \
    if (rec_type == REC_TYPE_DTXT) \
 continue; \
    if (rec_type != REC_TYPE_NORM && rec_type != REC_TYPE_CONT \
 && rec_type != REC_TYPE_PTR) \
 break;
    /* End of hairy macros. */

    if (vstream_fseek(state->dst, state->data_offset, SEEK_SET) < 0) {
 msg_warn("%s: seek file %s: %m", myname, cleanup_path);
 cleanup_milter_set_error(state, errno);
 CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR);
    }
    for (ptr_offset = 0, last_type = 0/* void */ ; /* void */ ) {
 if ((curr_offset = vstream_ftell(state->dst)) < 0) {
     msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path);
     cleanup_milter_set_error(state, errno);
     CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR);
 }
 /* Don't follow the "append header" pointer. */
 if (curr_offset == state->append_hdr_pt_offset)
     break;
 /* Caution: this macro terminates the loop at end-of-message. */
 /* Don't do complex processing while breaking out of this loop. */
 GET_NEXT_TEXT_OR_PTR_RECORD(rec_type, state, buf, curr_offset,
     CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR));
 /* Caution: don't assume ptr->header. This may be header-ptr->body. */
 if (rec_type == REC_TYPE_PTR) {
     if (rec_goto(state->dst, STR(buf)) < 0) {
  msg_warn("%s: read file %s: %m", myname, cleanup_path);
  cleanup_milter_set_error(state, errno);
  CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR);
     }
     /* Save PTR record, in case it points to the start of a header. */
     if (allow_ptr_backup) {
  ptr_offset = curr_offset;
  if (ptr_buf == 0)
      ptr_buf = vstring_alloc(100);
  vstring_strcpy(ptr_buf, STR(buf));
     }
     /* Don't update last_type; PTR can happen after REC_TYPE_CONT. */
     continue;
 }
 /* The middle of a multi-record header. */
 else if (last_type == REC_TYPE_CONT || IS_SPACE_TAB(STR(buf)[0])) {
     /* Reset the saved PTR record and update last_type. */
 }
 /* No more message headers. */
 else if ((len = is_header(STR(buf))) == 0) {
     break;
 }
 /* This the start of a message header. */
 else if ((header_label == 0
    || (strncasecmp(header_label, STR(buf), len) == 0
        && strlen(header_label) == len
        && !hidden_header(buf, state->auto_hdrs, &hide_done)))
   && --index == 0) {
     /* If we have a saved PTR record, it points to start of header. */
     break;
 }
 ptr_offset = 0;
 last_type = rec_type;
    }

    /*
     * In case of failure, return negative start position.
     */

    if (index > 0) {
 curr_offset = CLEANUP_FIND_HEADER_NOTFOUND;
    } else {

 /*
  * Skip over short-header padding, so that the file read pointer is
  * always positioned at the first non-padding record after the header
  * record. Insist on padding after short a header record, so that a
  * short header record can safely be overwritten by a pointer record.
 */

 if (LEN(buf) < REC_TYPE_PTR_PAYL_SIZE) {
     VSTRING *rbuf = (ptr_offset ? buf :
        (ptr_buf ? ptr_buf :
         (ptr_buf = vstring_alloc(100))));
     int     rval;

     if ((rval = rec_get_raw(state->dst, rbuf, 0, REC_FLAG_NONE)) < 0) {
  cleanup_milter_set_error(state, errno);
  CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR);
     }
     if (rval != REC_TYPE_DTXT)
  msg_panic("%s: short header without padding", myname);
 }

 /*
  * Optionally return a pointer to the message header, instead of the
  * start of the message header itself. In that case the file read
  * position is undefined (actually it is at the first non-padding
  * record that follows the message header record).
 */

 if (ptr_offset != 0) {
     rec_type = REC_TYPE_PTR;
     curr_offset = ptr_offset;
     vstring_strcpy(buf, STR(ptr_buf));
 }
 *prec_type = rec_type;
    }
    if (msg_verbose)
 msg_info("%s: index %ld name %s type %d offset %ld",
   myname, (long) index, header_label ?
   header_label : "(none)", rec_type, (long) curr_offset);

    CLEANUP_FIND_HEADER_RETURN(curr_offset);
}

/* cleanup_find_header_end - find end of header */

static off_t cleanup_find_header_end(CLEANUP_STATE *state,
                 VSTRING *rec_buf,
                 int last_type)
{
    const char *myname = "cleanup_find_header_end";
    off_t   read_offset;
    int     rec_type;

    /*
     * This routine is called immediately after cleanup_find_header_start().
     * rec_buf is the cleanup_find_header_start() result record; last_type is
     * the corresponding record type: REC_TYPE_PTR or REC_TYPE_NORM; the file
     * read position is at the first non-padding record after the result
     * header record.
     */

    for (;;) {
 if ((read_offset = vstream_ftell(state->dst)) < 0) {
     msg_warn("%s: read file %s: %m", myname, cleanup_path);
     cleanup_milter_error(state, errno);
     return (-1);
 }
 /* Don't follow the "append header" pointer. */
 if (read_offset == state->append_hdr_pt_offset)
     break;
 /* Caution: this macro terminates the loop at end-of-message. */
 /* Don't do complex processing while breaking out of this loop. */
 GET_NEXT_TEXT_OR_PTR_RECORD(rec_type, state, rec_buf, read_offset,
 /* Warning and errno->error mapping are done elsewhere. */
        return (-1));
 if (rec_type == REC_TYPE_PTR) {
     if (rec_goto(state->dst, STR(rec_buf)) < 0) {
  msg_warn("%s: read file %s: %m", myname, cleanup_path);
  cleanup_milter_error(state, errno);
  return (-1);
     }
     /* Don't update last_type; PTR may follow REC_TYPE_CONT. */
     continue;
 }
 /* Start of header or message body. */
 if (last_type != REC_TYPE_CONT && !IS_SPACE_TAB(STR(rec_buf)[0]))
     break;
 last_type = rec_type;
    }
    return (read_offset);
}

/* cleanup_patch_header - patch new header into an existing header */

static const char *cleanup_patch_header(CLEANUP_STATE *state,
             const char *new_hdr_name,
             const char *hdr_space,
             const char *new_hdr_value,
             off_t old_rec_offset,
             int old_rec_type,
             VSTRING *old_rec_buf,
             off_t next_offset)
{
    const char *myname = "cleanup_patch_header";
    VSTRING *buf = vstring_alloc(100);
    off_t   new_hdr_offset;

#define CLEANUP_PATCH_HEADER_RETURN(ret) do { \
 vstring_free(buf); \
 return (ret); \
    } while (0)

    if (msg_verbose)
 msg_info("%s: \"%s\" \"%s\" at %ld",
   myname, new_hdr_name, new_hdr_value, (long) old_rec_offset);

    /*
     * Allocate space after the end of the queue file for the new header and
     * optionally save an existing record to make room for a forward pointer
     * record. If the saved record was not a PTR record, follow the saved
     * record by a reverse pointer record that points to the record after the
     * original location of the saved record.
     * 
     * We update the queue file in a safe manner: save the new header and the
     * existing records after the end of the queue file, write the reverse
     * pointer, and only then overwrite the saved records with the forward
     * pointer to the new header.
     * 
     * old_rec_offset, old_rec_type, and old_rec_buf specify the record that we
     * are about to overwrite with a pointer record. If the record needs to
     * be saved (i.e. old_rec_type > 0), the buffer contains the data content
     * of exactly one PTR or text record.
     * 
     * next_offset specifies the record that follows the to-be-overwritten
     * record. It is ignored when the to-be-saved record is a pointer record.
     */


    /*
     * Return early when Milter header checks request that this header record
     * be dropped.
     */

    vstring_sprintf(buf, "%s:%s%s", new_hdr_name, hdr_space, new_hdr_value);
    if (cleanup_milter_hbc_checks
 && cleanup_milter_header_checks(state, buf) == 0)
 CLEANUP_PATCH_HEADER_RETURN(0);

    /*
     * Write the new header to a new location after the end of the queue
     * file.
     */

    if ((new_hdr_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) {
 msg_warn("%s: seek file %s: %m", myname, cleanup_path);
 CLEANUP_PATCH_HEADER_RETURN(cleanup_milter_error(state, errno));
    }
    /* XXX emit prepended header, then clear it. */
    cleanup_out_header(state, buf);  /* Includes padding */
    if (msg_verbose > 1)
 msg_info("%s: %ld: write %.*s", myname, (long) new_hdr_offset,
   LEN(buf) > 30 ? 30 : (int) LEN(buf), STR(buf));

    /*
     * Optionally, save the existing text record or pointer record that will
     * be overwritten with the forward pointer. Pad a short saved record to
     * ensure that it, too, can be overwritten by a pointer.
     */

    if (old_rec_type > 0) {
 CLEANUP_OUT_BUF(state, old_rec_type, old_rec_buf);
 if (LEN(old_rec_buf) < REC_TYPE_PTR_PAYL_SIZE)
     rec_pad(state->dst, REC_TYPE_DTXT,
      REC_TYPE_PTR_PAYL_SIZE - LEN(old_rec_buf));
 if (msg_verbose > 1)
     msg_info("%s: write %.*s", myname, LEN(old_rec_buf) > 30 ?
       30 : (int) LEN(old_rec_buf), STR(old_rec_buf));
    }

    /*
     * If the saved record wasn't a PTR record, write the reverse pointer
     * after the saved records. A reverse pointer value of -1 means we were
     * confused about what we were going to save.
     */

    if (old_rec_type != REC_TYPE_PTR) {
 if (next_offset < 0)
     msg_panic("%s: bad reverse pointer %ld",
        myname, (long) next_offset);
 cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
      (long) next_offset);
 if (msg_verbose > 1)
     msg_info("%s: write PTR %ld", myname, (long) next_offset);
    }

    /*
     * Write the forward pointer over the old record. Generally, a pointer
     * record will be shorter than a header record, so there will be a gap in
     * the queue file before the next record. In other words, we must always
     * follow pointer records otherwise we get out of sync with the data.
     */

    if (vstream_fseek(state->dst, old_rec_offset, SEEK_SET) < 0) {
 msg_warn("%s: seek file %s: %m", myname, cleanup_path);
 CLEANUP_PATCH_HEADER_RETURN(cleanup_milter_error(state, errno));
    }
    cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
         (long) new_hdr_offset);
    if (msg_verbose > 1)
 msg_info("%s: %ld: write PTR %ld", myname, (long) old_rec_offset,
   (long) new_hdr_offset);

    /*
     * In case of error while doing record output.
     */

    CLEANUP_PATCH_HEADER_RETURN(
        CLEANUP_OUT_OK(state) == 0 ? cleanup_milter_error(state, 0) :
   cleanup_milter_hbc_reply && LEN(cleanup_milter_hbc_reply) ?
    STR(cleanup_milter_hbc_reply) : 0);

    /*
     * Note: state->append_hdr_pt_target never changes.
     */

}

/* cleanup_ins_header - insert message header */

static const char *cleanup_ins_header(void *context, ssize_t index,
                  const char *new_hdr_name,
                  const char *hdr_space,
                  const char *new_hdr_value)
{
    const char *myname = "cleanup_ins_header";
    CLEANUP_STATE *state = (CLEANUP_STATE *) context;
    VSTRING *old_rec_buf = vstring_alloc(100);
    off_t   old_rec_offset;
    int     old_rec_type;
    off_t   next_offset;
    const char *ret;

#define CLEANUP_INS_HEADER_RETURN(ret) do { \
 vstring_free(old_rec_buf); \
 return (ret); \
    } while (0)

    if (msg_verbose)
 msg_info("%s: %ld \"%s\" \"%s\"",
   myname, (long) index, new_hdr_name, new_hdr_value);

    /*
     * Look for a header at the specified position.
     * 
     * The lookup result may be a pointer record. This allows us to make some
     * optimization when multiple insert operations happen in the same place.
     * 
     * Index 1 is the top-most header.
     */

#define NO_HEADER_NAME ((char *) 0)
#define ALLOW_PTR_BACKUP 1

    if (index < 1)
 index = 1;
    old_rec_offset = cleanup_find_header_start(state, index, NO_HEADER_NAME,
            old_rec_buf, &old_rec_type,
            ALLOW_PTR_BACKUP);
    if (old_rec_offset == CLEANUP_FIND_HEADER_IOERROR)
 /* Warning and errno->error mapping are done elsewhere. */
 CLEANUP_INS_HEADER_RETURN(cleanup_milter_error(state, 0));

    /*
     * If the header does not exist, simply append the header to the linked
     * list at the "header append" pointer record.
     */

    if (old_rec_offset < 0)
 CLEANUP_INS_HEADER_RETURN(cleanup_add_header(context, new_hdr_name,
       hdr_space, new_hdr_value));

    /*
     * If the header does exist, save both the new and the existing header to
     * new storage at the end of the queue file, and link the new storage
     * with a forward and reverse pointer (don't write a reverse pointer if
     * we are starting with a pointer record).
     */

    if (old_rec_type == REC_TYPE_PTR) {
 next_offset = -1;
    } else {
 if ((next_offset = vstream_ftell(state->dst)) < 0) {
     msg_warn("%s: read file %s: %m", myname, cleanup_path);
     CLEANUP_INS_HEADER_RETURN(cleanup_milter_error(state, errno));
 }
    }
    ret = cleanup_patch_header(state, new_hdr_name, hdr_space, new_hdr_value,
          old_rec_offset, old_rec_type,
          old_rec_buf, next_offset);
    CLEANUP_INS_HEADER_RETURN(ret);
}

/* cleanup_upd_header - modify or append message header */

static const char *cleanup_upd_header(void *context, ssize_t index,
                  const char *new_hdr_name,
                  const char *hdr_space,
                  const char *new_hdr_value)
{
    const char *myname = "cleanup_upd_header";
    CLEANUP_STATE *state = (CLEANUP_STATE *) context;
    VSTRING *rec_buf;
    off_t   old_rec_offset;
    off_t   next_offset;
    int     last_type;
    const char *ret;

    if (msg_verbose)
 msg_info("%s: %ld \"%s\" \"%s\"",
   myname, (long) index, new_hdr_name, new_hdr_value);

    /*
     * Sanity check.
     */

    if (*new_hdr_name == 0)
 msg_panic("%s: null header name", myname);

    /*
     * Find the header that is being modified.
     * 
     * The lookup result will never be a pointer record.
     * 
     * Index 1 is the first matching header instance.
     * 
     * XXX When a header is updated repeatedly we create jumps to jumps. To
     * eliminate this, rewrite the loop below so that we can start with the
     * pointer record that points to the header that's being edited.
     */

#define DONT_SAVE_RECORD 0
#define NO_PTR_BACKUP  0

#define CLEANUP_UPD_HEADER_RETURN(ret) do { \
 vstring_free(rec_buf); \
 return (ret); \
    } while (0)

    rec_buf = vstring_alloc(100);
    old_rec_offset = cleanup_find_header_start(state, index, new_hdr_name,
            rec_buf, &last_type,
            NO_PTR_BACKUP);
    if (old_rec_offset == CLEANUP_FIND_HEADER_IOERROR)
 /* Warning and errno->error mapping are done elsewhere. */
 CLEANUP_UPD_HEADER_RETURN(cleanup_milter_error(state, 0));

    /*
     * If no old header is found, simply append the new header to the linked
     * list at the "header append" pointer record.
     */

    if (old_rec_offset < 0)
 CLEANUP_UPD_HEADER_RETURN(cleanup_add_header(context, new_hdr_name,
       hdr_space, new_hdr_value));

    /*
     * If the old header is found, find the end of the old header, save the
     * new header to new storage at the end of the queue file, and link the
     * new storage with a forward and reverse pointer.
     */

    if ((next_offset = cleanup_find_header_end(state, rec_buf, last_type)) < 0)
 /* Warning and errno->error mapping are done elsewhere. */
 CLEANUP_UPD_HEADER_RETURN(cleanup_milter_error(state, 0));
    ret = cleanup_patch_header(state, new_hdr_name, hdr_space, new_hdr_value,
          old_rec_offset, DONT_SAVE_RECORD,
          (VSTRING *) 0, next_offset);
    CLEANUP_UPD_HEADER_RETURN(ret);
}

/* cleanup_del_header - delete message header */

static const char *cleanup_del_header(void *context, ssize_t index,
                  const char *hdr_name)
{
    const char *myname = "cleanup_del_header";
    CLEANUP_STATE *state = (CLEANUP_STATE *) context;
    VSTRING *rec_buf;
    off_t   header_offset;
    off_t   next_offset;
    int     last_type;

    if (msg_verbose)
 msg_info("%s: %ld \"%s\"", myname, (long) index, hdr_name);

    /*
     * Sanity check.
     */

    if (*hdr_name == 0)
 msg_panic("%s: null header name", myname);

    /*
     * Find the header that is being deleted.
     * 
     * The lookup result will never be a pointer record.
     * 
     * Index 1 is the first matching header instance.
     */

#define CLEANUP_DEL_HEADER_RETURN(ret) do { \
 vstring_free(rec_buf); \
 return (ret); \
    } while (0)

    rec_buf = vstring_alloc(100);
    header_offset = cleanup_find_header_start(state, index, hdr_name, rec_buf,
           &last_type, NO_PTR_BACKUP);
    if (header_offset == CLEANUP_FIND_HEADER_IOERROR)
 /* Warning and errno->error mapping are done elsewhere. */
 CLEANUP_DEL_HEADER_RETURN(cleanup_milter_error(state, 0));

    /*
     * Overwrite the beginning of the header record with a pointer to the
     * information that follows the header. We can't simply overwrite the
     * header with cleanup_out_header() and a special record type, because
     * there may be a PTR record in the middle of a multi-line header.
     */

    if (header_offset > 0) {
 if ((next_offset = cleanup_find_header_end(state, rec_buf, last_type)) < 0)
     /* Warning and errno->error mapping are done elsewhere. */
     CLEANUP_DEL_HEADER_RETURN(cleanup_milter_error(state, 0));
 /* Mark the header as deleted. */
 if (vstream_fseek(state->dst, header_offset, SEEK_SET) < 0) {
     msg_warn("%s: seek file %s: %m", myname, cleanup_path);
     CLEANUP_DEL_HEADER_RETURN(cleanup_milter_error(state, errno));
 }
 rec_fprintf(state->dst, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
      (long) next_offset);
    }
    vstring_free(rec_buf);

    /*
     * In case of error while doing record output.
     */

    return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, 0));
}

/* cleanup_chg_from - replace sender address, ignore ESMTP arguments */

static const char *cleanup_chg_from(void *context, const char *ext_from,
                const char *esmtp_args)
{
    const char *myname = "cleanup_chg_from";
    CLEANUP_STATE *state = (CLEANUP_STATE *) context;
    off_t   new_offset;
    off_t   new_sender_offset;
    off_t   after_sender_offs;
    int     addr_count;
    TOK822 *tree;
    TOK822 *tp;
    VSTRING *int_sender_buf;
    int     dsn_envid = 0;
    int     dsn_ret = 0;

    if (msg_verbose)
 msg_info("%s: \"%s\" \"%s\"", myname, ext_from, esmtp_args);

    /*
     * ESMTP support is limited to RET and ENVID, i.e. things that are stored
     * together with the sender queue file record.
     */

    if (esmtp_args[0]) {
 ARGV   *esmtp_argv;
 int     i;
 const char *arg;

 esmtp_argv = argv_split(esmtp_args, " ");
 for (i = 0; i < esmtp_argv->argc; ++i) {
     arg = esmtp_argv->argv[i];
     if (strncasecmp(arg, "RET="4) == 0) {
  if ((dsn_ret = dsn_ret_code(arg + 4)) == 0) {
      msg_warn("Ignoring bad ESMTP parameter \"%s\" in "
        "SMFI_CHGFROM request", arg);
  } else {
      state->dsn_ret = dsn_ret;
  }
     } else if (strncasecmp(arg, "ENVID="6) == 0) {
  if (state->milter_dsn_buf == 0)
      state->milter_dsn_buf = vstring_alloc(20);
  dsn_envid = (xtext_unquote(state->milter_dsn_buf, arg + 6)
        && allprint(STR(state->milter_dsn_buf)));
  if (!dsn_envid) {
      msg_warn("Ignoring bad ESMTP parameter \"%s\" in "
        "SMFI_CHGFROM request", arg);
  } else {
      if (state->dsn_envid)
   myfree(state->dsn_envid);
      state->dsn_envid = mystrdup(STR(state->milter_dsn_buf));
  }
     } else {
  msg_warn("Ignoring bad ESMTP parameter \"%s\" in "
    "SMFI_CHGFROM request", arg);
     }
 }
 argv_free(esmtp_argv);
    }

    /*
     * The cleanup server remembers the file offset of the current sender
     * address record (offset in sender_pt_offset) and the file offset of the
     * record that follows the sender address (offset in sender_pt_target).
     * Short original sender records are padded, so that they can safely be
     * overwritten with a pointer record to the new sender address record.
     */

    if (state->sender_pt_offset < 0)
 msg_panic("%s: no original sender record offset", myname);
    if (state->sender_pt_target < 0)
 msg_panic("%s: no post-sender record offset", myname);

    /*
     * Allocate space after the end of the queue file, and write the new {DSN
     * envid, DSN ret, sender address, sender BCC} records, followed by a
     * reverse pointer record that points to the record that follows the
     * original sender record.
     * 
     * We update the queue file in a safe manner: save the new sender after the
     * end of the queue file, write the reverse pointer, and only then
     * overwrite the old sender record with the forward pointer to the new
     * sender.
     */

    if ((new_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) {
 msg_warn("%s: seek file %s: %m", myname, cleanup_path);
 return (cleanup_milter_error(state, errno));
    }

    /*
     * Sender DSN attribute records precede the sender record.
     */

    if (dsn_envid)
 rec_fprintf(state->dst, REC_TYPE_ATTR, "%s=%s",
      MAIL_ATTR_DSN_ENVID, STR(state->milter_dsn_buf));
    if (dsn_ret)
 rec_fprintf(state->dst, REC_TYPE_ATTR, "%s=%d",
      MAIL_ATTR_DSN_RET, dsn_ret);
    if (dsn_envid == 0 && dsn_ret == 0) {
 new_sender_offset = new_offset;
    } else if ((new_sender_offset = vstream_ftell(state->dst)) < 0) {
 msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path);
 return (cleanup_milter_error(state, errno));
    }

    /*
     * Transform the address from external form to internal form. This also
     * removes the enclosing <>, if present.
     * 
     * XXX vstring_alloc() rejects zero-length requests.
     */

    int_sender_buf = vstring_alloc(strlen(ext_from) + 1);
    tree = tok822_parse(ext_from);
    for (addr_count = 0, tp = tree; tp != 0; tp = tp->next) {
 if (tp->type == TOK822_ADDR) {
     if (addr_count == 0) {
  tok822_internalize(int_sender_buf, tp->head, TOK822_STR_DEFL);
  addr_count += 1;
     } else {
  msg_warn("%s: Milter request to add multi-sender: \"%s\"",
    state->queue_id, ext_from);
  break;
     }
 }
    }
    tok822_free_tree(tree);
    after_sender_offs = cleanup_addr_sender(state, STR(int_sender_buf));
    vstring_free(int_sender_buf);
    cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
         (long) state->sender_pt_target);
    state->sender_pt_target = after_sender_offs;

    /*
     * Overwrite the current sender record with the pointer to the new {DSN
     * envid, DSN ret, sender address, sender BCC} records.
     */

    if (vstream_fseek(state->dst, state->sender_pt_offset, SEEK_SET) < 0) {
 msg_warn("%s: seek file %s: %m", myname, cleanup_path);
 return (cleanup_milter_error(state, errno));
    }
    cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
         (long) new_offset);

    /*
     * Remember the location of the new current sender record.
     */

    state->sender_pt_offset = new_sender_offset;

    /*
     * In case of error while doing record output.
     */

    return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, 0));
}

/* cleanup_add_rcpt_par - append recipient address, with ESMTP arguments */

static const char *cleanup_add_rcpt_par(void *context, const char *ext_rcpt,
             const char *esmtp_args)
{
    const char *myname = "cleanup_add_rcpt_par";
    CLEANUP_STATE *state = (CLEANUP_STATE *) context;
    off_t   new_rcpt_offset;
    off_t   reverse_ptr_offset;
    int     addr_count;
    TOK822 *tree;
    TOK822 *tp;
    VSTRING *int_rcpt_buf;
    VSTRING *orcpt_buf = 0;
    ARGV   *esmtp_argv;
    int     dsn_notify = 0;
    const char *dsn_orcpt_info = 0;
    size_t  type_len;
    int     i;
    const char *arg;
    const char *arg_val;

    if (msg_verbose)
 msg_info("%s: \"%s\" \"%s\"", myname, ext_rcpt, esmtp_args);

    /*
     * To simplify implementation, the cleanup server writes a dummy
     * "recipient append" pointer record after the last recipient. We cache
     * both the location and the target of the current "recipient append"
     * pointer record.
     */

    if (state->append_rcpt_pt_offset < 0)
 msg_panic("%s: no recipient append pointer location", myname);
    if (state->append_rcpt_pt_target < 0)
 msg_panic("%s: no recipient append pointer target", myname);

    /*
     * Allocate space after the end of the queue file, and write the
     * recipient record, followed by a reverse pointer record that points to
     * the target of the old "recipient append" pointer record. This reverse
     * pointer record becomes the new "recipient append" pointer record.
     * 
     * We update the queue file in a safe manner: save the new recipient after
     * the end of the queue file, write the reverse pointer, and only then
     * overwrite the old "recipient append" pointer with the forward pointer
     * to the new recipient.
     */

    if ((new_rcpt_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) {
 msg_warn("%s: seek file %s: %m", myname, cleanup_path);
 return (cleanup_milter_error(state, errno));
    }

    /*
     * Parse ESMTP parameters. XXX UTF8SMTP don't assume ORCPT is xtext.
     */

    if (esmtp_args[0]) {
 esmtp_argv = argv_split(esmtp_args, " ");
 for (i = 0; i < esmtp_argv->argc; ++i) {
     arg = esmtp_argv->argv[i];
     if (strncasecmp(arg, "NOTIFY="7) == 0) { /* RFC 3461 */
  if (dsn_notify || (dsn_notify = dsn_notify_mask(arg + 7)) == 0)
      msg_warn("%s: Bad NOTIFY parameter from Milter or "
        "header/body_checks: \"%.100s\"",
        state->queue_id, arg);
     } else if (strncasecmp(arg, "ORCPT="6) == 0) { /* RFC 3461 */
  if (orcpt_buf == 0)
      orcpt_buf = vstring_alloc(100);
  if (dsn_orcpt_info
      || (type_len = strcspn(arg_val = arg + 6";")) == 0
      || (arg_val)[type_len] != ';'
      || xtext_unquote_append(vstring_sprintf(orcpt_buf,
          "%.*s;", (int) type_len,
           arg_val),
         arg_val + type_len + 1) == 0) {
      msg_warn("%s: Bad ORCPT parameter from Milter or "
        "header/body_checks: \"%.100s\"",
        state->queue_id, arg);
  } else {
      dsn_orcpt_info = STR(orcpt_buf);
  }
     } else {
  msg_warn("%s: ignoring ESMTP argument from Milter or "
    "header/body_checks: \"%.100s\"",
    state->queue_id, arg);
     }
 }
 argv_free(esmtp_argv);
    }

    /*
     * Transform recipient from external form to internal form. This also
     * removes the enclosing <>, if present.
     * 
     * XXX vstring_alloc() rejects zero-length requests.
     */

    int_rcpt_buf = vstring_alloc(strlen(ext_rcpt) + 1);
    tree = tok822_parse(ext_rcpt);
    for (addr_count = 0, tp = tree; tp != 0; tp = tp->next) {
 if (tp->type == TOK822_ADDR) {
     if (addr_count == 0) {
  tok822_internalize(int_rcpt_buf, tp->head, TOK822_STR_DEFL);
  addr_count += 1;
     } else {
  msg_warn("%s: Milter or header/body_checks request to "
    "add multi-recipient: \"%s\"",
    state->queue_id, ext_rcpt);
  break;
     }
 }
    }
    tok822_free_tree(tree);
    if (addr_count != 0)
 cleanup_addr_bcc_dsn(state, STR(int_rcpt_buf), dsn_orcpt_info,
        dsn_notify ? dsn_notify : DEF_DSN_NOTIFY);
    else
 msg_warn("%s: ignoring attempt from Milter to add null recipient",
   state->queue_id);
    vstring_free(int_rcpt_buf);
    if (orcpt_buf)
 vstring_free(orcpt_buf);

    /*
     * Don't update the queue file when we did not write a recipient record
     * (malformed or duplicate BCC recipient).
     */

    if (vstream_ftell(state->dst) == new_rcpt_offset)
 return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, 0));

    /*
     * Follow the recipient with a "reverse" pointer to the old recipient
     * append target.
     */

    if ((reverse_ptr_offset = vstream_ftell(state->dst)) < 0) {
 msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path);
 return (cleanup_milter_error(state, errno));
    }
    cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
         (long) state->append_rcpt_pt_target);

    /*
     * Pointer flipping: update the old "recipient append" pointer record
     * value to the location of the new recipient record.
     */

    if (vstream_fseek(state->dst, state->append_rcpt_pt_offset, SEEK_SET) < 0) {
 msg_warn("%s: seek file %s: %m", myname, cleanup_path);
 return (cleanup_milter_error(state, errno));
    }
    cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
         (long) new_rcpt_offset);

    /*
     * Update the in-memory "recipient append" pointer record location with
     * the location of the reverse pointer record that follows the new
     * recipient. The target of the "recipient append" pointer record does
     * not change; it's always the record that follows the dummy pointer
     * record that was written while Postfix received the message.
     */

    state->append_rcpt_pt_offset = reverse_ptr_offset;

    /*
     * In case of error while doing record output.
     */

    return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, 0));
}

/* cleanup_add_rcpt - append recipient address */

static const char *cleanup_add_rcpt(void *context, const char *ext_rcpt)
{
    return (cleanup_add_rcpt_par(context, ext_rcpt, ""));
}

/* cleanup_del_rcpt - remove recipient and all its expansions */

static const char *cleanup_del_rcpt(void *context, const char *ext_rcpt)
{
    const char *myname = "cleanup_del_rcpt";
    CLEANUP_STATE *state = (CLEANUP_STATE *) context;
    off_t   curr_offset;
    VSTRING *buf;
    char   *attr_name;
    char   *attr_value;
    char   *dsn_orcpt = 0;  /* XXX for dup filter cleanup */
    int     dsn_notify = 0;  /* XXX for dup filter cleanup */
    char   *orig_rcpt = 0;
    char   *start;
    int     rec_type;
    int     junk;
    int     count = 0;
    TOK822 *tree;
    TOK822 *tp;
    VSTRING *int_rcpt_buf;
    int     addr_count;

    if (msg_verbose)
 msg_info("%s: \"%s\"", myname, ext_rcpt);

    /*
     * Virtual aliasing and other address rewriting happens after the mail
     * filter sees the envelope address. Therefore we must delete all
     * recipient records whose Postfix (not DSN) original recipient address
     * matches the specified address.
     * 
     * As the number of recipients may be very large we can't do an efficient
     * two-pass implementation (collect record offsets first, then mark
     * records as deleted). Instead we mark records as soon as we find them.
     * This is less efficient because we do (seek-write-read) for each marked
     * recipient, instead of (seek-write). It's unlikely that VSTREAMs will
     * be made smart enough to eliminate unnecessary I/O with small seeks.
     * 
     * XXX When Postfix original recipients are turned off, we have no option
     * but to match against the expanded and rewritten recipient address.
     * 
     * XXX Remove the (dsn_orcpt, dsn_notify, orcpt, recip) tuple from the
     * duplicate recipient filter. This requires that we maintain reference
     * counts.
     */

    if (vstream_fseek(state->dst, 0L, SEEK_SET) < 0) {
 msg_warn("%s: seek file %s: %m", myname, cleanup_path);
 return (cleanup_milter_error(state, errno));
    }
#define CLEANUP_DEL_RCPT_RETURN(ret) do { \
 if (orig_rcpt != 0) \
     myfree(orig_rcpt); \
 if (dsn_orcpt != 0) \
     myfree(dsn_orcpt); \
 vstring_free(buf); \
 vstring_free(int_rcpt_buf); \
 return (ret); \
    } while (0)

    /*
     * Transform recipient from external form to internal form. This also
     * removes the enclosing <>, if present.
     * 
     * XXX vstring_alloc() rejects zero-length requests.
     */

    int_rcpt_buf = vstring_alloc(strlen(ext_rcpt) + 1);
    tree = tok822_parse(ext_rcpt);
    for (addr_count = 0, tp = tree; tp != 0; tp = tp->next) {
 if (tp->type == TOK822_ADDR) {
     if (addr_count == 0) {
  tok822_internalize(int_rcpt_buf, tp->head, TOK822_STR_DEFL);
  addr_count += 1;
     } else {
  msg_warn("%s: Milter request to drop multi-recipient: \"%s\"",
    state->queue_id, ext_rcpt);
  break;
     }
 }
    }
    tok822_free_tree(tree);

    buf = vstring_alloc(100);
    for (;;) {
 if (CLEANUP_OUT_OK(state) == 0)
     /* Warning and errno->error mapping are done elsewhere. */
     CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, 0));
 if ((curr_offset = vstream_ftell(state->dst)) < 0) {
     msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path);
     CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno));
 }
 if ((rec_type = rec_get_raw(state->dst, buf, 0, REC_FLAG_NONE)) <= 0) {
     msg_warn("%s: read file %s: %m", myname, cleanup_path);
     CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno));
 }
 if (rec_type == REC_TYPE_END)
     break;
 /* Skip over message content. */
 if (rec_type == REC_TYPE_MESG) {
     if (vstream_fseek(state->dst, state->xtra_offset, SEEK_SET) < 0) {
  msg_warn("%s: seek file %s: %m", myname, cleanup_path);
  CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno));
     }
     continue;
 }
 start = STR(buf);
 if (rec_type == REC_TYPE_PTR) {
     if (rec_goto(state->dst, start) < 0) {
  msg_warn("%s: seek file %s: %m", myname, cleanup_path);
  CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno));
     }
     continue;
 }
 /* Map attribute names to pseudo record type. */
 if (rec_type == REC_TYPE_ATTR) {
     if (split_nameval(STR(buf), &attr_name, &attr_value) != 0
  || *attr_value == 0)
  continue;
     if ((junk = rec_attr_map(attr_name)) != 0) {
  start = attr_value;
  rec_type = junk;
     }
 }
 switch (rec_type) {
 case REC_TYPE_DSN_ORCPT:  /* RCPT TO ORCPT parameter */
     if (dsn_orcpt != 0)   /* can't happen */
  myfree(dsn_orcpt);
     dsn_orcpt = mystrdup(start);
     break;
 case REC_TYPE_DSN_NOTIFY:  /* RCPT TO NOTIFY parameter */
     if (alldig(start) && (junk = atoi(start)) > 0
  && DSN_NOTIFY_OK(junk))
  dsn_notify = junk;
     else
  dsn_notify = 0;
     break;
 case REC_TYPE_ORCP:   /* unmodified RCPT TO address */
     if (orig_rcpt != 0)   /* can't happen */
  myfree(orig_rcpt);
     orig_rcpt = mystrdup(start);
     break;
 case REC_TYPE_RCPT:   /* rewritten RCPT TO address */
     if (strcmp(orig_rcpt ? orig_rcpt : start, STR(int_rcpt_buf)) == 0) {
  if (vstream_fseek(state->dst, curr_offset, SEEK_SET) < 0) {
      msg_warn("%s: seek file %s: %m", myname, cleanup_path);
      CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno));
  }
  if (REC_PUT_BUF(state->dst, REC_TYPE_DRCP, buf) < 0) {
      msg_warn("%s: write queue file: %m", state->queue_id);
      CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno));
  }
  count++;
     }
     if (var_enable_orcpt)
  /* Matches been_here() call in cleanup_out_recipient(). */
  been_here_drop(state->dups, "%s\n%d\n%s\n%s",
          dsn_orcpt ? dsn_orcpt : "", dsn_notify,
        orig_rcpt ? orig_rcpt : "", STR(int_rcpt_buf));
     /* FALLTHROUGH */
 case REC_TYPE_DRCP:   /* canceled recipient */
 case REC_TYPE_DONE:   /* can't happen */
     if (orig_rcpt != 0) {
  myfree(orig_rcpt);
  orig_rcpt = 0;
     }
     if (dsn_orcpt != 0) {
  myfree(dsn_orcpt);
  dsn_orcpt = 0;
     }
     dsn_notify = 0;
     break;
 }
    }
    /* Matches been_here_fixed() call in cleanup_out_recipient(). */
    if (var_enable_orcpt == 0 && count > 0)
 been_here_drop_fixed(state->dups, STR(int_rcpt_buf));

    if (msg_verbose)
 msg_info("%s: deleted %d records for recipient \"%s\"",
   myname, count, ext_rcpt);

    CLEANUP_DEL_RCPT_RETURN(0);
}

/* cleanup_repl_body - replace message body */

static const char *cleanup_repl_body(void *context, int cmd, int rec_type,
                 VSTRING *buf)
{
    const char *myname = "cleanup_repl_body";
    CLEANUP_STATE *state = (CLEANUP_STATE *) context;
    static VSTRING empty;

    /*
     * XXX Sendmail compatibility: milters don't see the first body line, so
     * don't expect they will send one.
     */

    switch (cmd) {
    case MILTER_BODY_LINE:
 if (cleanup_body_edit_write(state, rec_type, buf) < 0)
     return (cleanup_milter_error(state, errno));
 break;
    case MILTER_BODY_START:
 VSTRING_RESET(&empty);
 if (cleanup_body_edit_start(state) < 0
     || cleanup_body_edit_write(state, REC_TYPE_NORM, &empty) < 0)
     return (cleanup_milter_error(state, errno));
 break;
    case MILTER_BODY_END:
 if (cleanup_body_edit_finish(state) < 0)
     return (cleanup_milter_error(state, errno));
 break;
    default:
 msg_panic("%s: bad command: %d", myname, cmd);
    }
    return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, errno));
}

/* cleanup_milter_eval - expand macro */

static const char *cleanup_milter_eval(const char *name, void *ptr)
{
    CLEANUP_STATE *state = (CLEANUP_STATE *) ptr;

    /*
     * Note: if we use XFORWARD attributes here, then consistency requires
     * that we forward all Sendmail macros via XFORWARD.
     */


    /*
     * System macros.
     */

    if (strcmp(name, S8_MAC_DAEMON_NAME) == 0)
 return (var_milt_daemon_name);
    if (strcmp(name, S8_MAC_V) == 0)
 return (var_milt_v);

    /*
     * Connect macros.
     */

#ifndef CLIENT_ATTR_UNKNOWN
#define CLIENT_ATTR_UNKNOWN "unknown"
#define SERVER_ATTR_UNKNOWN "unknown"
#endif

    if (strcmp(name, S8_MAC__) == 0) {
 vstring_sprintf(state->temp1, "%s [%s]",
   state->reverse_name, state->client_addr);
 if (strcasecmp(state->client_name, state->reverse_name) != 0)
     vstring_strcat(state->temp1, " (may be forged)");
 return (STR(state->temp1));
    }
    if (strcmp(name, S8_MAC_J) == 0)
 return (var_myhostname);
    if (strcmp(name, S8_MAC_CLIENT_ADDR) == 0)
 return (state->client_addr);
    if (strcmp(name, S8_MAC_CLIENT_NAME) == 0)
 return (state->client_name);
    if (strcmp(name, S8_MAC_CLIENT_PORT) == 0)
 return (state->client_port
  && strcmp(state->client_port, CLIENT_ATTR_UNKNOWN) ?
  state->client_port : "0");
    if (strcmp(name, S8_MAC_CLIENT_PTR) == 0)
 return (state->reverse_name);
    /* XXX S8_MAC_CLIENT_RES needs SMTPD_PEER_CODE_XXX from smtpd. */
    if (strcmp(name, S8_MAC_DAEMON_ADDR) == 0)
 return (state->server_addr);
    if (strcmp(name, S8_MAC_DAEMON_PORT) == 0)
 return (state->server_port
  && strcmp(state->server_port, SERVER_ATTR_UNKNOWN) ?
  state->server_port : "0");

    /*
     * MAIL FROM macros.
     */

    if (strcmp(name, S8_MAC_I) == 0)
 return (state->queue_id);
#ifdef USE_SASL_AUTH
    if (strcmp(name, S8_MAC_AUTH_TYPE) == 0)
 return (nvtable_find(state->attr, MAIL_ATTR_SASL_METHOD));
    if (strcmp(name, S8_MAC_AUTH_AUTHEN) == 0)
 return (nvtable_find(state->attr, MAIL_ATTR_SASL_USERNAME));
    if (strcmp(name, S8_MAC_AUTH_AUTHOR) == 0)
 return (nvtable_find(state->attr, MAIL_ATTR_SASL_SENDER));
#endif
    if (strcmp(name, S8_MAC_MAIL_ADDR) == 0)
 return (state->milter_ext_from ? STR(state->milter_ext_from) : 0);

    /*
     * RCPT TO macros.
     */

    if (strcmp(name, S8_MAC_RCPT_ADDR) == 0)
 return (state->milter_ext_rcpt ? STR(state->milter_ext_rcpt) : 0);
    return (0);
}

/* cleanup_milter_receive - receive milter instances */

void    cleanup_milter_receive(CLEANUP_STATE *state, int count)
{
    if (state->milters)
 milter_free(state->milters);
    state->milters = milter_receive(state->src, count);
    if (state->milters == 0)
 msg_fatal("cleanup_milter_receive: milter receive failed");
    if (count <= 0)
 return;
    milter_macro_callback(state->milters, cleanup_milter_eval, (void *) state);
    milter_edit_callback(state->milters,
    cleanup_add_header, cleanup_upd_header,
    cleanup_ins_header, cleanup_del_header,
    cleanup_chg_from, cleanup_add_rcpt,
    cleanup_add_rcpt_par, cleanup_del_rcpt,
    cleanup_repl_body, (void *) state);
}

/* cleanup_milter_apply - apply Milter response, non-zero if rejecting */

static const char *cleanup_milter_apply(CLEANUP_STATE *state, const char *event,
             const char *resp)
{
    const char *myname = "cleanup_milter_apply";
    const char *action;
    const char *text;
    const char *attr;
    const char *ret = 0;

    if (msg_verbose)
 msg_info("%s: %s", myname, resp);

    /*
     * Don't process our own milter_header/body checks replies. See comments
     * in cleanup_milter_hbc_extend().
     */

    if (cleanup_milter_hbc_reply &&
 strcmp(resp, STR(cleanup_milter_hbc_reply)) == 0)
 return (0);

    /*
     * Don't process Milter replies that are redundant because header/body
     * checks already decided that we will not receive the message; or Milter
     * replies that would have conflicting effect with the outcome of
     * header/body checks (for example, header_checks "discard" action
     * followed by Milter "reject" reply). Logging both actions would look
     * silly.
     */

    if (CLEANUP_MILTER_REJECTING_OR_DISCARDING_MESSAGE(state)) {
 if (msg_verbose)
     msg_info("%s: ignoring redundant or conflicting milter reply: %s",
       state->queue_id, resp);
 return (0);
    }

    /*
     * Sanity check.
     */

    if (state->client_name == 0)
 msg_panic("%s: missing client info initialization", myname);

    /*
     * We don't report errors that were already reported by the content
     * editing call-back routines. See cleanup_milter_error() above.
     */

    if (CLEANUP_OUT_OK(state) == 0)
 return (0);
    switch (resp[0]) {
    case 'H':
 if (state->flags & CLEANUP_FLAG_HOLD)
     return (0);
 state->flags |= CLEANUP_FLAG_HOLD;
 action = "milter-hold";
 text = resp[1] ? resp + 1 : "milter triggers HOLD action";
 break;
    case 'D':
 if (state->flags & CLEANUP_FLAG_DISCARD)
     return (0);
 state->flags |= CLEANUP_FLAG_DISCARD;
 action = "milter-discard";
 text = "milter triggers DISCARD action";
 break;
    case 'S':
 if (state->flags & CLEANUP_STAT_CONT)
     return (0);
 /* Shutdown' may be the default action for an I/O error. */
 CLEANUP_MILTER_SET_SMTP_REPLY(state, resp);
 ret = state->reason;
 state->errs |= CLEANUP_STAT_WRITE;
 action = "milter-reject";
 text = resp + 4;
 break;

 /*
  * Override permanent reject with temporary reject. This happens when
  * the cleanup server has to bounce (hard reject) but is unable to
  * store the message (soft reject). After a temporary reject we stop
  * inspecting queue file records, so it can't be overruled by
  * something else.
  * 
  * CLEANUP_STAT_CONT and CLEANUP_STAT_DEFER both update the reason
  * attribute, but CLEANUP_STAT_DEFER takes precedence. It terminates
  * queue record processing, and prevents bounces from being sent.
 */

    case '4':
 CLEANUP_MILTER_SET_SMTP_REPLY(state, resp);
 ret = state->reason;
 state->errs |= CLEANUP_STAT_DEFER;
 action = "milter-reject";
 text = resp + 4;
 break;
    case '5':
 CLEANUP_MILTER_SET_SMTP_REPLY(state, resp);
 ret = state->reason;
 state->errs |= CLEANUP_STAT_CONT;
 action = "milter-reject";
 text = resp + 4;
 break;
    default:
 msg_panic("%s: unexpected mail filter reply: %s", myname, resp);
    }
    vstring_sprintf(state->temp1, "%s: %s: %s from %s[%s]: %s;",
      state->queue_id, action, event, state->client_name,
      state->client_addr, text);
    if (state->sender)
 vstring_sprintf_append(state->temp1, " from=<%s>",
          info_log_addr_form_sender(state->sender));
    if (state->recip)
 vstring_sprintf_append(state->temp1, " to=<%s>",
          info_log_addr_form_recipient(state->recip));
    if ((attr = nvtable_find(state->attr, MAIL_ATTR_LOG_PROTO_NAME)) != 0)
 vstring_sprintf_append(state->temp1, " proto=%s", attr);
    if ((attr = nvtable_find(state->attr, MAIL_ATTR_LOG_HELO_NAME)) != 0)
 vstring_sprintf_append(state->temp1, " helo=<%s>", attr);
    msg_info("%s", vstring_str(state->temp1));

    return (ret);
}

/* cleanup_milter_client_init - initialize real or ersatz client info */

static void cleanup_milter_client_init(CLEANUP_STATE *state)
{
    static const INET_PROTO_INFO *proto_info;
    const char *proto_attr;

    /*
     * Either the cleanup client specifies a name, address and protocol, or
     * we have a local submission and pretend localhost/127.0.0.1/AF_INET.
     */

#define NO_CLIENT_PORT "0"

    state->client_name = nvtable_find(state->attr, MAIL_ATTR_ACT_CLIENT_NAME);
    state->reverse_name =
 nvtable_find(state->attr, MAIL_ATTR_ACT_REVERSE_CLIENT_NAME);
    state->client_addr = nvtable_find(state->attr, MAIL_ATTR_ACT_CLIENT_ADDR);
    state->client_port = nvtable_find(state->attr, MAIL_ATTR_ACT_CLIENT_PORT);
    proto_attr = nvtable_find(state->attr, MAIL_ATTR_ACT_CLIENT_AF);
    state->server_addr = nvtable_find(state->attr, MAIL_ATTR_ACT_SERVER_ADDR);
    state->server_port = nvtable_find(state->attr, MAIL_ATTR_ACT_SERVER_PORT);

    if (state->client_name == 0 || state->client_addr == 0 || proto_attr == 0
 || !alldig(proto_attr)) {
 state->client_name = "localhost";
#ifdef AF_INET6
 if (proto_info == 0)
     proto_info = inet_proto_info();
 if (proto_info->sa_family_list[0] == PF_INET6) {
     state->client_addr = "::1";
     state->client_af = AF_INET6;
 } else
#endif
 {
     state->client_addr = "127.0.0.1";
     state->client_af = AF_INET;
 }
 state->server_addr = state->client_addr;
    } else
 state->client_af = atoi(proto_attr);
    if (state->reverse_name == 0)
 state->reverse_name = state->client_name;
    /* Compatibility with pre-2.5 queue files. */
    if (state->client_port == 0) {
 state->client_port = NO_CLIENT_PORT;
 state->server_port = state->client_port;
    }
}

/* cleanup_milter_inspect - run message through mail filter */

void    cleanup_milter_inspect(CLEANUP_STATE *state, MILTERS *milters)
{
    const char *myname = "cleanup_milter";
    const char *resp;

    if (msg_verbose)
 msg_info("enter %s", myname);

    /*
     * Initialize, in case we're called via smtpd(8).
     */

    if (state->client_name == 0)
 cleanup_milter_client_init(state);

    /*
     * Prologue: prepare for Milter header/body checks.
     */

    if (*var_milt_head_checks)
 cleanup_milter_header_checks_reinit(state);

    /*
     * Process mail filter replies. The reply format is verified by the mail
     * filter library.
     */

    if ((resp = milter_message(milters, state->handle->stream,
          state->data_offset, state->auto_hdrs)) != 0)
 cleanup_milter_apply(state, "END-OF-MESSAGE", resp);

    /*
     * Epilogue: finalize Milter header/body checks.
     */

    if (*var_milt_head_checks)
 cleanup_milter_hbc_finish(state);

    if (msg_verbose)
 msg_info("leave %s", myname);
}

/* cleanup_milter_emul_mail - emulate connect/ehlo/mail event */

void    cleanup_milter_emul_mail(CLEANUP_STATE *state,
             MILTERS *milters,
             const char *addr)
{
    const char *resp;
    const char *helo;
    const char *argv[2];

    /*
     * Per-connection initialization.
     */

    milter_macro_callback(milters, cleanup_milter_eval, (void *) state);
    milter_edit_callback(milters,
    cleanup_add_header, cleanup_upd_header,
    cleanup_ins_header, cleanup_del_header,
    cleanup_chg_from, cleanup_add_rcpt,
    cleanup_add_rcpt_par, cleanup_del_rcpt,
    cleanup_repl_body, (void *) state);
    if (state->client_name == 0)
 cleanup_milter_client_init(state);

    /*
     * Emulate SMTP events.
     */

    if ((resp = milter_conn_event(milters, state->client_name, state->client_addr,
         state->client_port, state->client_af)) != 0) {
 cleanup_milter_apply(state, "CONNECT", resp);
 return;
    }
#define PRETEND_ESMTP 1

    if (CLEANUP_MILTER_OK(state)) {
 if ((helo = nvtable_find(state->attr, MAIL_ATTR_ACT_HELO_NAME)) == 0)
     helo = state->client_name;
 if ((resp = milter_helo_event(milters, helo, PRETEND_ESMTP)) != 0) {
     cleanup_milter_apply(state, "EHLO", resp);
     return;
 }
    }
    if (CLEANUP_MILTER_OK(state)) {
 if (state->milter_ext_from == 0)
     state->milter_ext_from = vstring_alloc(100);
 /* Sendmail 8.13 does not externalize the null address. */
 if (*addr)
     quote_821_local(state->milter_ext_from, addr);
 else
     vstring_strcpy(state->milter_ext_from, addr);
 argv[0] = STR(state->milter_ext_from);
 argv[1] = 0;
 if ((resp = milter_mail_event(milters, argv)) != 0) {
     cleanup_milter_apply(state, "MAIL", resp);
     return;
 }
    }
}

/* cleanup_milter_emul_rcpt - emulate rcpt event */

void    cleanup_milter_emul_rcpt(CLEANUP_STATE *state,
             MILTERS *milters,
             const char *addr)
{
    const char *myname = "cleanup_milter_emul_rcpt";
    const char *resp;
    const char *argv[2];

    /*
     * Sanity check.
     */

    if (state->client_name == 0)
 msg_panic("%s: missing client info initialization", myname);

    /*
     * CLEANUP_STAT_CONT and CLEANUP_STAT_DEFER both update the reason
     * attribute, but CLEANUP_STAT_DEFER takes precedence. It terminates
     * queue record processing, and prevents bounces from being sent.
     */

    if (state->milter_ext_rcpt == 0)
 state->milter_ext_rcpt = vstring_alloc(100);
    /* Sendmail 8.13 does not externalize the null address. */
    if (*addr)
 quote_821_local(state->milter_ext_rcpt, addr);
    else
 vstring_strcpy(state->milter_ext_rcpt, addr);
    argv[0] = STR(state->milter_ext_rcpt);
    argv[1] = 0;
    if ((resp = milter_rcpt_event(milters, MILTER_FLAG_NONE, argv)) != 0
 && cleanup_milter_apply(state, "RCPT", resp) != 0) {
 msg_warn("%s: milter configuration error: can't reject recipient "
   "in non-smtpd(8) submission", state->queue_id);
 msg_warn("%s: message not accepted, try again later", state->queue_id);
 CLEANUP_MILTER_SET_REASON(state, "4.3.5 Server configuration error");
 state->errs |= CLEANUP_STAT_DEFER;
    }
}

/* cleanup_milter_emul_data - emulate data event */

void    cleanup_milter_emul_data(CLEANUP_STATE *state, MILTERS *milters)
{
    const char *myname = "cleanup_milter_emul_data";
    const char *resp;

    /*
     * Sanity check.
     */

    if (state->client_name == 0)
 msg_panic("%s: missing client info initialization", myname);

    if ((resp = milter_data_event(milters)) != 0)
 cleanup_milter_apply(state, "DATA", resp);
}

#ifdef TEST

 /*
  * Queue file editing driver for regression tests. In this case it is OK to
  * report fatal errors after I/O errors.
  */

#include <stdio.h>
#include <msg_vstream.h>
#include <vstring_vstream.h>
#include <mail_addr.h>
#include <mail_version.h>

#undef msg_verbose

char   *cleanup_path;
VSTRING *cleanup_trace_path;
VSTRING *cleanup_strip_chars;
int     cleanup_comm_canon_flags;
MAPS   *cleanup_comm_canon_maps;
int     cleanup_ext_prop_mask;
ARGV   *cleanup_masq_domains;
int     cleanup_masq_flags;
MAPS   *cleanup_rcpt_bcc_maps;
int     cleanup_rcpt_canon_flags;
MAPS   *cleanup_rcpt_canon_maps;
MAPS   *cleanup_send_bcc_maps;
int     cleanup_send_canon_flags;
MAPS   *cleanup_send_canon_maps;
int     var_dup_filter_limit = DEF_DUP_FILTER_LIMIT;
char   *var_empty_addr = DEF_EMPTY_ADDR;
MAPS   *cleanup_virt_alias_maps;
char   *var_milt_daemon_name = "host.example.com";
char   *var_milt_v = DEF_MILT_V;
MILTERS *cleanup_milters = (MILTERS *) ((char *) sizeof(*cleanup_milters));
char   *var_milt_head_checks = "";

/* Dummies to satisfy unused external references. */

int     cleanup_masquerade_internal(CLEANUP_STATE *state, VSTRING *addr, ARGV *masq_domains)
{
    msg_panic("cleanup_masquerade_internal dummy");
}

int     cleanup_rewrite_internal(const char *context, VSTRING *result,
             const char *addr)
{
    vstring_strcpy(result, addr);
    return (0);
}

int     cleanup_map11_internal(CLEANUP_STATE *state, VSTRING *addr,
                  MAPS *maps, int propagate)
{
    msg_panic("cleanup_map11_internal dummy");
}

ARGV   *cleanup_map1n_internal(CLEANUP_STATE *state, const char *addr,
                  MAPS *maps, int propagate)
{
    msg_panic("cleanup_map1n_internal dummy");
}

void    cleanup_envelope(CLEANUP_STATE *state, int type, const char *buf,
            ssize_t len)
{
    msg_panic("cleanup_envelope dummy");
}

static void usage(void)
{
    msg_warn("usage:");
    msg_warn("    verbose on|off");
    msg_warn("    open pathname");
    msg_warn("    close");
    msg_warn("    add_header index name [value]");
    msg_warn("    ins_header index name [value]");
    msg_warn("    upd_header index name [value]");
    msg_warn("    del_header index name");
    msg_warn("    chg_from addr parameters");
    msg_warn("    add_rcpt addr");
    msg_warn("    add_rcpt_par addr parameters");
    msg_warn("    del_rcpt addr");
    msg_warn("    replbody pathname");
    msg_warn("    header_checks type:name");
}

/* flatten_args - unparse partial command line */

static void flatten_args(VSTRING *buf, char **argv)
{
    char  **cpp;

    VSTRING_RESET(buf);
    for (cpp = argv; *cpp; cpp++) {
 vstring_strcat(buf, *cpp);
 if (cpp[1])
     VSTRING_ADDCH(buf, ' ');
    }
    VSTRING_TERMINATE(buf);
}

/* open_queue_file - open an unedited queue file (all-zero dummy PTRs) */

static void open_queue_file(CLEANUP_STATE *state, const char *path)
{
    VSTRING *buf = vstring_alloc(100);
    off_t   curr_offset;
    int     rec_type;
    long    msg_seg_len;
    long    data_offset;
    long    rcpt_count;
    long    qmgr_opts;
    const HEADER_OPTS *opts;

    if (state->dst != 0) {
 msg_warn("closing %s", cleanup_path);
 vstream_fclose(state->dst);
 state->dst = 0;
 myfree(cleanup_path);
 cleanup_path = 0;
    }
    if ((state->dst = vstream_fopen(path, O_RDWR, 0)) == 0) {
 msg_warn("open %s: %m", path);
    } else {
 var_drop_hdrs = "";
 cleanup_path = mystrdup(path);
 for (;;) {
     if ((curr_offset = vstream_ftell(state->dst)) < 0)
  msg_fatal("file %s: vstream_ftell: %m", cleanup_path);
     if ((rec_type = rec_get_raw(state->dst, buf, 0, REC_FLAG_NONE)) < 0)
  msg_fatal("file %s: missing SIZE or PTR record", cleanup_path);
     if (rec_type == REC_TYPE_SIZE) {
  if (sscanf(STR(buf), "%ld %ld %ld %ld",
      &msg_seg_len, &data_offset,
      &rcpt_count, &qmgr_opts) != 4)
      msg_fatal("file %s: bad SIZE record: %s",
         cleanup_path, STR(buf));
  state->data_offset = data_offset;
  state->xtra_offset = data_offset + msg_seg_len;
     } else if (rec_type == REC_TYPE_FROM) {
  state->sender_pt_offset = curr_offset;
  if (LEN(buf) < REC_TYPE_PTR_PAYL_SIZE
      && rec_get_raw(state->dst, buf, 0, REC_FLAG_NONE) != REC_TYPE_PTR)
      msg_fatal("file %s: missing PTR record after short sender",
         cleanup_path);
  if ((state->sender_pt_target = vstream_ftell(state->dst)) < 0)
      msg_fatal("file %s: missing END record", cleanup_path);
     } else if (rec_type == REC_TYPE_PTR) {
  if (state->data_offset < 0)
      msg_fatal("file %s: missing SIZE record", cleanup_path);
  if (curr_offset < state->data_offset
      || curr_offset > state->xtra_offset) {
      if (state->append_rcpt_pt_offset < 0) {
   state->append_rcpt_pt_offset = curr_offset;
   if (atol(STR(buf)) != 0)
       msg_fatal("file %s: bad dummy recipient PTR record: %s",
          cleanup_path, STR(buf));
   if ((state->append_rcpt_pt_target =
        vstream_ftell(state->dst)) < 0)
       msg_fatal("file %s: vstream_ftell: %m", cleanup_path);
      } else if (curr_offset > state->xtra_offset
          && state->append_meta_pt_offset < 0) {
   state->append_meta_pt_offset = curr_offset;
   if (atol(STR(buf)) != 0)
       msg_fatal("file %s: bad dummy meta PTR record: %s",
          cleanup_path, STR(buf));
   if ((state->append_meta_pt_target =
        vstream_ftell(state->dst)) < 0)
       msg_fatal("file %s: vstream_ftell: %m", cleanup_path);
      }
  } else {
      if (state->append_hdr_pt_offset < 0) {
   state->append_hdr_pt_offset = curr_offset;
   if (atol(STR(buf)) != 0)
       msg_fatal("file %s: bad dummy header PTR record: %s",
          cleanup_path, STR(buf));
   if ((state->append_hdr_pt_target =
        vstream_ftell(state->dst)) < 0)
       msg_fatal("file %s: vstream_ftell: %m", cleanup_path);
      }
  }
     } else if (rec_type == REC_TYPE_NORM && state->hop_count == 0
         && (opts = header_opts_find(STR(buf))) != 0
         && opts->type == HDR_RECEIVED) {
  state->hop_count += 1;
  /* XXX Only the first line of the first Received: header. */
  argv_add(state->auto_hdrs, STR(buf), ARGV_END);
     }
     if (state->append_rcpt_pt_offset > 0
  && state->append_hdr_pt_offset > 0
  && state->hop_count > 0
  && (rec_type == REC_TYPE_END
      || state->append_meta_pt_offset > 0))
  break;
 }
 if (msg_verbose) {
     msg_info("append_rcpt_pt_offset %ld append_rcpt_pt_target %ld",
       (long) state->append_rcpt_pt_offset,
       (long) state->append_rcpt_pt_target);
     msg_info("append_hdr_pt_offset %ld append_hdr_pt_target %ld",
       (long) state->append_hdr_pt_offset,
       (long) state->append_hdr_pt_target);
 }
    }
    vstring_free(buf);
}

static void close_queue_file(CLEANUP_STATE *state)
{
    (void) vstream_fclose(state->dst);
    state->dst = 0;
    myfree(cleanup_path);
    cleanup_path = 0;
}

int     main(int unused_argc, char **argv)
{
    VSTRING *inbuf = vstring_alloc(100);
    VSTRING *arg_buf = vstring_alloc(100);
    char   *bufp;
    int     istty = isatty(vstream_fileno(VSTREAM_IN));
    CLEANUP_STATE *state = cleanup_state_alloc((VSTREAM *) 0);
    const char *parens = "{}";

    state->queue_id = mystrdup("NOQUEUE");
    state->sender = mystrdup("sender");
    state->recip = mystrdup("recipient");
    state->client_name = "client_name";
    state->client_addr = "client_addr";
    state->flags |= CLEANUP_FLAG_FILTER_ALL;

    msg_vstream_init(argv[0], VSTREAM_ERR);
    var_line_limit = DEF_LINE_LIMIT;
    var_header_limit = DEF_HEADER_LIMIT;
    var_enable_orcpt = DEF_ENABLE_ORCPT;
    var_info_log_addr_form = DEF_INFO_LOG_ADDR_FORM;

    for (;;) {
 ARGV   *argv;
 ssize_t index;
 const char *resp = 0;

 if (istty) {
     vstream_printf("- ");
     vstream_fflush(VSTREAM_OUT);
 }
 if (vstring_fgets_nonl(inbuf, VSTREAM_IN) == 0)
     break;

 bufp = vstring_str(inbuf);
 if (!istty) {
     vstream_printf("> %s\n", bufp);
     vstream_fflush(VSTREAM_OUT);
 }
 if (*bufp == '#' || *bufp == 0 || allspace(bufp))
     continue;
 argv = argv_splitq(bufp, " ", parens);
 if (argv->argc == 0) {
     msg_warn("missing command");
 } else if (strcmp(argv->argv[0], "?") == 0) {
     usage();
 } else if (strcmp(argv->argv[0], "verbose") == 0) {
     if (argv->argc != 2) {
  msg_warn("bad verbose argument count: %ld", (long) argv->argc);
     } else if (strcmp(argv->argv[1], "on") == 0) {
  msg_verbose = 2;
     } else if (strcmp(argv->argv[1], "off") == 0) {
  msg_verbose = 0;
     } else {
  msg_warn("bad verbose argument");
     }
 } else if (strcmp(argv->argv[0], "line_length_limit") == 0) {
     if (argv->argc != 2) {
  msg_warn("bad line_length_limit argument count: %ld",
    (long) argv->argc);
     } else if (alldig(argv->argv[1]) == 0) {
  msg_warn("bad line_length_limit argument count: %ld",
    (long) argv->argc);
     } else if ((var_line_limit = atoi(argv->argv[1])) < DEF_LINE_LIMIT) {
  msg_warn("bad line_length_limit argument");
     }
 } else if (strcmp(argv->argv[0], "open") == 0) {
     if (state->dst != 0) {
  msg_info("closing %s", VSTREAM_PATH(state->dst));
  close_queue_file(state);
     }
     if (argv->argc != 2) {
  msg_warn("bad open argument count: %ld", (long) argv->argc);
     } else {
  open_queue_file(state, argv->argv[1]);
     }
 } else if (strcmp(argv->argv[0], "enable_original_recipient") == 0) {
     if (argv->argc == 1) {
  msg_info("enable_original_recipient: %d", var_enable_orcpt);
     } else if (argv->argc != 2) {
  msg_warn("bad enable_original_recipient argument count: %ld",
    (long) argv->argc);
     } else if (!alldig(argv->argv[1])) {
  msg_warn("non-numeric enable_original_recipient argument: %s",
    argv->argv[1]);
     } else {
  var_enable_orcpt = atoi(argv->argv[1]);
     }
 } else if (state->dst == 0) {
     msg_warn("no open queue file");
 } else if (strcmp(argv->argv[0], "close") == 0) {
     if (*var_milt_head_checks) {
  cleanup_milter_hbc_finish(state);
  myfree(var_milt_head_checks);
  var_milt_head_checks = "";
  cleanup_milter_header_checks_deinit();
     }
     close_queue_file(state);
 } else if (cleanup_milter_hbc_reply && LEN(cleanup_milter_hbc_reply)) {
     /* Postfix libmilter would skip further requests. */
     msg_info("ignoring: %s %s %s", argv->argv[0],
       argv->argc > 1 ? argv->argv[1] : "",
       argv->argc > 2 ? argv->argv[2] : "");
 } else if (strcmp(argv->argv[0], "add_header") == 0) {
     if (argv->argc < 2) {
  msg_warn("bad add_header argument count: %ld",
    (long) argv->argc);
     } else {
  flatten_args(arg_buf, argv->argv + 2);
  resp = cleanup_add_header(state, argv->argv[1], " ", STR(arg_buf));
     }
 } else if (strcmp(argv->argv[0], "ins_header") == 0) {
     if (argv->argc < 3) {
  msg_warn("bad ins_header argument count: %ld",
    (long) argv->argc);
     } else if ((index = atoi(argv->argv[1])) < 1) {
  msg_warn("bad ins_header index value");
     } else {
  flatten_args(arg_buf, argv->argv + 3);
  resp = cleanup_ins_header(state, index, argv->argv[2], " ", STR(arg_buf));
     }
 } else if (strcmp(argv->argv[0], "upd_header") == 0) {
     if (argv->argc < 3) {
  msg_warn("bad upd_header argument count: %ld",
    (long) argv->argc);
     } else if ((index = atoi(argv->argv[1])) < 1) {
  msg_warn("bad upd_header index value");
     } else {
  flatten_args(arg_buf, argv->argv + 3);
  resp = cleanup_upd_header(state, index, argv->argv[2], " ", STR(arg_buf));
     }
 } else if (strcmp(argv->argv[0], "del_header") == 0) {
     if (argv->argc != 3) {
  msg_warn("bad del_header argument count: %ld",
    (long) argv->argc);
     } else if ((index = atoi(argv->argv[1])) < 1) {
  msg_warn("bad del_header index value");
     } else {
  cleanup_del_header(state, index, argv->argv[2]);
     }
 } else if (strcmp(argv->argv[0], "chg_from") == 0) {
     if (argv->argc != 3) {
  msg_warn("bad chg_from argument count: %ld", (long) argv->argc);
     } else {
  char   *arg = argv->argv[2];
  const char *err;

  if (*arg == parens[0]
      && (err = extpar(&arg, parens, EXTPAR_FLAG_NONE)) != 0) {
      msg_warn("%s in \"%s\"", err, arg);
  } else {
      cleanup_chg_from(state, argv->argv[1], arg);
  }
     }
 } else if (strcmp(argv->argv[0], "add_rcpt") == 0) {
     if (argv->argc != 2) {
  msg_warn("bad add_rcpt argument count: %ld", (long) argv->argc);
     } else {
  cleanup_add_rcpt(state, argv->argv[1]);
     }
 } else if (strcmp(argv->argv[0], "add_rcpt_par") == 0) {
     if (argv->argc != 3) {
  msg_warn("bad add_rcpt_par argument count: %ld",
    (long) argv->argc);
     } else {
  cleanup_add_rcpt_par(state, argv->argv[1], argv->argv[2]);
     }
 } else if (strcmp(argv->argv[0], "del_rcpt") == 0) {
     if (argv->argc != 2) {
  msg_warn("bad del_rcpt argument count: %ld", (long) argv->argc);
     } else {
  cleanup_del_rcpt(state, argv->argv[1]);
     }
 } else if (strcmp(argv->argv[0], "replbody") == 0) {
     if (argv->argc != 2) {
  msg_warn("bad replbody argument count: %ld", (long) argv->argc);
     } else {
  VSTREAM *fp;
  VSTRING *buf;

  if ((fp = vstream_fopen(argv->argv[1], O_RDONLY, 0)) == 0) {
      msg_warn("open %s file: %m", argv->argv[1]);
  } else {
      buf = vstring_alloc(100);
      cleanup_repl_body(state, MILTER_BODY_START,
          REC_TYPE_NORM, buf);
      while (vstring_get_nonl(buf, fp) != VSTREAM_EOF)
   cleanup_repl_body(state, MILTER_BODY_LINE,
       REC_TYPE_NORM, buf);
      cleanup_repl_body(state, MILTER_BODY_END,
          REC_TYPE_NORM, buf);
      vstring_free(buf);
      vstream_fclose(fp);
  }
     }
 } else if (strcmp(argv->argv[0], "header_checks") == 0) {
     if (argv->argc != 2) {
  msg_warn("bad header_checks argument count: %ld",
    (long) argv->argc);
     } else if (*var_milt_head_checks) {
  msg_warn("can't change header checks");
     } else {
  var_milt_head_checks = mystrdup(argv->argv[1]);
  cleanup_milter_header_checks_init();
     }
 } else if (strcmp(argv->argv[0], "sender_bcc_maps") == 0) {
     if (argv->argc != 2) {
  msg_warn("bad sender_bcc_maps argument count: %ld",
    (long) argv->argc);
     } else {
  if (cleanup_send_bcc_maps)
      maps_free(cleanup_send_bcc_maps);
  cleanup_send_bcc_maps =
      maps_create("sender_bcc_maps", argv->argv[1],
    DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX
    | DICT_FLAG_UTF8_REQUEST);
  state->flags |= CLEANUP_FLAG_BCC_OK;
  var_rcpt_delim = "";
     }
 } else {
     msg_warn("bad command: %s", argv->argv[0]);
 }
 argv_free(argv);
 if (resp)
     cleanup_milter_apply(state, "END-OF-MESSAGE", resp);
    }
    vstring_free(inbuf);
    vstring_free(arg_buf);
    if (state->append_meta_pt_offset >= 0) {
 if (state->flags)
     msg_info("flags = %s", cleanup_strflags(state->flags));
 if (state->errs)
     msg_info("errs = %s", cleanup_strerror(state->errs));
    }
    cleanup_state_free(state);
    if (*var_milt_head_checks)
 myfree(var_milt_head_checks);

    return (0);
}

#endif

Messung V0.5 in Prozent
C=76 H=94 G=85

¤ Dauer der Verarbeitung: 0.97 Sekunden  (vorverarbeitet am  2026-08-08) ¤

*© Formatika GbR, Deutschland






Wurzel

Suchen

PVS Prover

Isabelle Prover

NIST Cobol Testsuite

Cephes Mathematical Library

Vienna Development Method

Haftungshinweis

Die Informationen auf dieser Webseite wurden nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit, noch Qualität der bereit gestellten Informationen zugesichert.

Bemerkung:

Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.






                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Open Source Software

     Quellcodebibliothek
     Eigene Quellcodes
     Fremde Quellcodes
     Suchen

Jenseits des Üblichen ....
    

Besucherstatistik

Besucherstatistik

Statistik
#Sources=277311
#Domains=752002
 




Impressum  | Ethik und Gesetz  | Haftungsausschluß  | Kontakt  |   | © 2026 JDD |