xref: /netbsd-src/external/ibm-public/postfix/dist/src/cleanup/cleanup_milter.c (revision fc4f42693f9b1c31f39f9cf50af1bf2010325808)
1 /*	$NetBSD: cleanup_milter.c,v 1.2 2017/02/14 01:16:44 christos Exp $	*/
2 
3 /*++
4 /* NAME
5 /*	cleanup_milter 3
6 /* SUMMARY
7 /*	external mail filter support
8 /* SYNOPSIS
9 /*	#include <cleanup.h>
10 /*
11 /*	void	cleanup_milter_receive(state, count)
12 /*	CLEANUP_STATE *state;
13 /*	int	count;
14 /*
15 /*	void	cleanup_milter_inspect(state, milters)
16 /*	CLEANUP_STATE *state;
17 /*	MILTERS	*milters;
18 /*
19 /*	cleanup_milter_emul_mail(state, milters, sender)
20 /*	CLEANUP_STATE *state;
21 /*	MILTERS	*milters;
22 /*	const char *sender;
23 /*
24 /*	cleanup_milter_emul_rcpt(state, milters, recipient)
25 /*	CLEANUP_STATE *state;
26 /*	MILTERS	*milters;
27 /*	const char *recipient;
28 /*
29 /*	cleanup_milter_emul_data(state, milters)
30 /*	CLEANUP_STATE *state;
31 /*	MILTERS	*milters;
32 /* DESCRIPTION
33 /*	This module implements support for Sendmail-style mail
34 /*	filter (milter) applications, including in-place queue file
35 /*	modification.
36 /*
37 /*	cleanup_milter_receive() receives mail filter definitions,
38 /*	typically from an smtpd(8) server process, and registers
39 /*	local call-back functions for macro expansion and for queue
40 /*	file modification.
41 /*
42 /*	cleanup_milter_inspect() sends the current message headers
43 /*	and body to the mail filters that were received with
44 /*	cleanup_milter_receive(), or that are specified with the
45 /*	cleanup_milters configuration parameter.
46 /*
47 /*	cleanup_milter_emul_mail() emulates connect, helo and mail
48 /*	events for mail that does not arrive via the smtpd(8) server.
49 /*	The emulation pretends that mail arrives from localhost/127.0.0.1
50 /*	via ESMTP. Milters can reject emulated connect, helo, mail
51 /*	or data events, but not emulated rcpt events as described
52 /*	next.
53 /*
54 /*	cleanup_milter_emul_rcpt() emulates an rcpt event for mail
55 /*	that does not arrive via the smtpd(8) server. This reports
56 /*	a server configuration error condition when the milter
57 /*	rejects an emulated rcpt event.
58 /*
59 /*	cleanup_milter_emul_data() emulates a data event for mail
60 /*	that does not arrive via the smtpd(8) server.  It's OK for
61 /*	milters to reject emulated data events.
62 /* SEE ALSO
63 /*	milter(3) generic mail filter interface
64 /* DIAGNOSTICS
65 /*	Fatal errors: memory allocation problem.
66 /*	Panic: interface violation.
67 /*	Warnings: I/O errors (state->errs is updated accordingly).
68 /* LICENSE
69 /* .ad
70 /* .fi
71 /*	The Secure Mailer license must be distributed with this software.
72 /* AUTHOR(S)
73 /*	Wietse Venema
74 /*	IBM T.J. Watson Research
75 /*	P.O. Box 704
76 /*	Yorktown Heights, NY 10598, USA
77 /*
78 /*	Wietse Venema
79 /*	Google, Inc.
80 /*	111 8th Avenue
81 /*	New York, NY 10011, USA
82 /*--*/
83 
84 /* System library. */
85 
86 #include <sys_defs.h>
87 #include <sys/socket.h>			/* AF_INET */
88 #include <string.h>
89 #include <errno.h>
90 
91 #ifdef STRCASECMP_IN_STRINGS_H
92 #include <strings.h>
93 #endif
94 
95 /* Utility library. */
96 
97 #include <msg.h>
98 #include <vstream.h>
99 #include <vstring.h>
100 #include <stringops.h>
101 
102 /* Global library. */
103 
104 #include <off_cvt.h>
105 #include <dsn_mask.h>
106 #include <rec_type.h>
107 #include <cleanup_user.h>
108 #include <record.h>
109 #include <rec_attr_map.h>
110 #include <mail_proto.h>
111 #include <mail_params.h>
112 #include <lex_822.h>
113 #include <is_header.h>
114 #include <quote_821_local.h>
115 #include <dsn_util.h>
116 #include <xtext.h>
117 
118 /* Application-specific. */
119 
120 #include <cleanup.h>
121 
122  /*
123   * How Postfix 2.4 edits queue file information:
124   *
125   * Mail filter applications (Milters) can send modification requests after
126   * receiving the end of the message body.  Postfix implements these
127   * modifications in the cleanup server, so that it can edit the queue file
128   * in place. This avoids the temporary files that would be needed when
129   * modifications were implemented in the SMTP server (Postfix normally does
130   * not store the whole message in main memory). Once a Milter is done
131   * editing, the queue file can be used as input for the next Milter, and so
132   * on. Finally, the cleanup server changes file permissions, calls fsync(),
133   * and waits for successful completion.
134   *
135   * To implement in-place queue file edits, we need to introduce surprisingly
136   * little change to the existing Postfix queue file structure.  All we need
137   * is a way to mark a record as deleted, and to jump from one place in the
138   * queue file to another. We could implement deleted records with jumps, but
139   * marking is sometimes simpler.
140   *
141   * Postfix does not store queue files as plain text files. Instead all
142   * information is stored in records with an explicit type and length, for
143   * sender, recipient, arrival time, and so on.  Even the content that makes
144   * up the message header and body is stored as records with explicit types
145   * and lengths.  This organization makes it very easy to mark a record as
146   * deleted, and to introduce the pointer records that we will use to jump
147   * from one place in a queue file to another place.
148   *
149   * - Deleting a recipient is easiest - simply modify the record type into one
150   * that is skipped by the software that delivers mail. We won't try to reuse
151   * the deleted recipient for other purposes. When deleting a recipient, we
152   * may need to delete multiple recipient records that result from virtual
153   * alias expansion of the original recipient address.
154   *
155   * - Replacing a header record involves pointer records. A record is replaced
156   * by overwriting it with a forward pointer to space after the end of the
157   * queue file, putting the new record there, followed by a reverse pointer
158   * to the record that follows the replaced header. To simplify
159   * implementation we follow a short header record with a filler record so
160   * that we can always overwrite a header record with a pointer.
161   *
162   * N.B. This is a major difference with Postfix version 2.3, which needed
163   * complex code to save records that follow a short header, before it could
164   * overwrite a short header record. This code contained two of the three
165   * post-release bugs that were found with Postfix header editing.
166   *
167   * - Inserting a header record is like replacing one, except that we also
168   * relocate the record that is being overwritten by the forward pointer.
169   *
170   * - Deleting a message header is simplest when we replace it by a "skip"
171   * pointer to the information that follows the header. With a multi-line
172   * header we need to update only the first line.
173   *
174   * - Appending a recipient or header record involves pointer records as well.
175   * To make this convenient, the queue file already contains dummy pointer
176   * records at the locations where we want to append recipient or header
177   * content. To append, change the dummy pointer into a forward pointer to
178   * space after the end of a message, put the new recipient or header record
179   * there, followed by a reverse pointer to the record that follows the
180   * forward pointer.
181   *
182   * - To append another header or recipient record, replace the reverse pointer
183   * by a forward pointer to space after the end of a message, put the new
184   * record there, followed by the value of the reverse pointer that we
185   * replace. Thus, there is no one-to-one correspondence between forward and
186   * backward pointers. Instead, there can be multiple forward pointers for
187   * one reverse pointer.
188   *
189   * - When a mail filter wants to replace an entire body, we overwrite existing
190   * body records until we run out of space, and then write a pointer to space
191   * after the end of the queue file, followed by more body content. There may
192   * be multiple regions with body content; regions are connected by forward
193   * pointers, and the last region ends with a pointer to the marker that ends
194   * the message content segment. Body regions can be large and therefore they
195   * are reused to avoid wasting space. Sendmail mail filters currently do not
196   * replace individual body records, and that is a good thing.
197   *
198   * Making queue file modifications safe:
199   *
200   * Postfix queue files are segmented. The first segment is for envelope
201   * records, the second for message header and body content, and the third
202   * segment is for information that was extracted or generated from the
203   * message header or body content.  Each segment is terminated by a marker
204   * record. For now we don't want to change their location. That is, we want
205   * to avoid moving the records that mark the start or end of a queue file
206   * segment.
207   *
208   * To ensure that we can always replace a header or body record by a pointer
209   * record, without having to relocate a marker record, the cleanup server
210   * places a dummy pointer record at the end of the recipients and at the end
211   * of the message header. To support message body modifications, a dummy
212   * pointer record is also placed at the end of the message content.
213   *
214   * With all these changes in queue file organization, REC_TYPE_END is no longer
215   * guaranteed to be the last record in a queue file. If an application were
216   * to read beyond the REC_TYPE_END marker, it would go into an infinite
217   * loop, because records after REC_TYPE_END alternate with reverse pointers
218   * to the middle of the queue file. For robustness, the record reading
219   * routine skips forward to the end-of-file position after reading the
220   * REC_TYPE_END marker.
221   */
222 
223 /*#define msg_verbose	2*/
224 
225 static void cleanup_milter_set_error(CLEANUP_STATE *, int);
226 static const char *cleanup_add_rcpt_par(void *, const char *, const char *);
227 
228 #define STR(x)		vstring_str(x)
229 #define LEN(x)		VSTRING_LEN(x)
230 
231 /* cleanup_milter_hbc_log - log post-milter header/body_checks action */
232 
233 static void cleanup_milter_hbc_log(void *context, const char *action,
234 				        const char *where, const char *line,
235 				           const char *optional_text)
236 {
237     const CLEANUP_STATE *state = (CLEANUP_STATE *) context;
238     const char *attr;
239 
240     vstring_sprintf(state->temp1, "%s: milter-%s-%s: %s %.60s from %s[%s];",
241 		    state->queue_id, where, action, where, line,
242 		    state->client_name, state->client_addr);
243     if (state->sender)
244 	vstring_sprintf_append(state->temp1, " from=<%s>", state->sender);
245     if (state->recip)
246 	vstring_sprintf_append(state->temp1, " to=<%s>", state->recip);
247     if ((attr = nvtable_find(state->attr, MAIL_ATTR_LOG_PROTO_NAME)) != 0)
248 	vstring_sprintf_append(state->temp1, " proto=%s", attr);
249     if ((attr = nvtable_find(state->attr, MAIL_ATTR_LOG_HELO_NAME)) != 0)
250 	vstring_sprintf_append(state->temp1, " helo=<%s>", attr);
251     if (optional_text)
252 	vstring_sprintf_append(state->temp1, ": %s", optional_text);
253     msg_info("%s", vstring_str(state->temp1));
254 }
255 
256 /* cleanup_milter_header_prepend - prepend header to milter-generated header */
257 
258 static void cleanup_milter_header_prepend(void *context, int rec_type,
259 			         const char *buf, ssize_t len, off_t offset)
260 {
261     /* XXX save prepended header to buffer. */
262     msg_warn("the milter_header/body_checks prepend action is not implemented");
263 }
264 
265 /* cleanup_milter_hbc_extend - additional header/body_checks actions */
266 
267 static char *cleanup_milter_hbc_extend(void *context, const char *command,
268 			         ssize_t cmd_len, const char *optional_text,
269 				         const char *where, const char *buf,
270 				               ssize_t buf_len, off_t offset)
271 {
272     CLEANUP_STATE *state = (CLEANUP_STATE *) context;
273     const char *map_class = VAR_MILT_HEAD_CHECKS;	/* XXX */
274 
275 #define STREQUAL(x,y,l) (strncasecmp((x), (y), (l)) == 0 && (y)[l] == 0)
276 
277     /*
278      * These are currently our mutually-exclusive ways of not receiving mail:
279      * "reject" and "discard". Only these can be reported to the up-stream
280      * Postfix libmilter code, because sending any reply there causes Postfix
281      * libmilter to skip further "edit" requests. By way of safety net, each
282      * of these must also reset CLEANUP_FLAG_FILTER_ALL.
283      */
284 #define CLEANUP_MILTER_REJECTING_OR_DISCARDING_MESSAGE(state) \
285     ((state->flags & CLEANUP_FLAG_DISCARD) || (state->errs & CLEANUP_STAT_CONT))
286 
287     /*
288      * We log all header/body-checks actions here, because we know the
289      * details of the message content that triggered the action. We report
290      * detail-free milter-reply values (reject/discard, stored in the
291      * milter_hbc_reply state member) to the Postfix libmilter code, so that
292      * Postfix libmilter can stop sending requests.
293      *
294      * We also set all applicable cleanup flags here, because there is no
295      * guarantee that Postfix libmilter will propagate our own milter-reply
296      * value to cleanup_milter_inspect() which calls cleanup_milter_apply().
297      * The latter translates responses from Milter applications into cleanup
298      * flags, and logs the response text. Postfix libmilter can convey only
299      * one milter-reply value per email message, and that reply may even come
300      * from outside Postfix.
301      *
302      * To suppress redundant logging, cleanup_milter_apply() does nothing when
303      * the milter-reply value matches the saved text in the milter_hbc_reply
304      * state member. As we remember only one milter-reply value, we can't
305      * report multiple milter-reply values per email message. We satisfy this
306      * constraint, because we already clear the CLEANUP_FLAG_FILTER_ALL flags
307      * to terminate further header inspection.
308      */
309     if ((state->flags & CLEANUP_FLAG_FILTER_ALL) == 0)
310 	return ((char *) buf);
311 
312     if (STREQUAL(command, "BCC", cmd_len)) {
313 	if (strchr(optional_text, '@') == 0) {
314 	    msg_warn("bad BCC address \"%s\" in %s map -- "
315 		     "need user@domain",
316 		     optional_text, VAR_MILT_HEAD_CHECKS);
317 	} else {
318 	    cleanup_milter_hbc_log(context, "bcc", where, buf, optional_text);
319 	    /* Caller checks state error flags. */
320 	    (void) cleanup_add_rcpt_par(state, optional_text, "");
321 	}
322 	return ((char *) buf);
323     }
324     if (STREQUAL(command, "REJECT", cmd_len)) {
325 	const CLEANUP_STAT_DETAIL *detail;
326 
327 	if (state->reason)
328 	    myfree(state->reason);
329 	detail = cleanup_stat_detail(CLEANUP_STAT_CONT);
330 	if (*optional_text) {
331 	    state->reason = dsn_prepend(detail->dsn, optional_text);
332 	    if (*state->reason != '4' && *state->reason != '5') {
333 		msg_warn("bad DSN action in %s -- need 4.x.x or 5.x.x",
334 			 optional_text);
335 		*state->reason = '4';
336 	    }
337 	} else {
338 	    state->reason = dsn_prepend(detail->dsn, detail->text);
339 	}
340 	if (*state->reason == '4')
341 	    state->errs |= CLEANUP_STAT_DEFER;
342 	else
343 	    state->errs |= CLEANUP_STAT_CONT;
344 	state->flags &= ~CLEANUP_FLAG_FILTER_ALL;
345 	cleanup_milter_hbc_log(context, "reject", where, buf, state->reason);
346 	vstring_sprintf(state->milter_hbc_reply, "%d %s",
347 			detail->smtp, state->reason);
348 	STR(state->milter_hbc_reply)[0] = *state->reason;
349 	return ((char *) buf);
350     }
351     if (STREQUAL(command, "FILTER", cmd_len)) {
352 	if (*optional_text == 0) {
353 	    msg_warn("missing FILTER command argument in %s map", map_class);
354 	} else if (strchr(optional_text, ':') == 0) {
355 	    msg_warn("bad FILTER command %s in %s -- "
356 		     "need transport:destination",
357 		     optional_text, map_class);
358 	} else {
359 	    if (state->filter)
360 		myfree(state->filter);
361 	    state->filter = mystrdup(optional_text);
362 	    cleanup_milter_hbc_log(context, "filter", where, buf,
363 				   optional_text);
364 	}
365 	return ((char *) buf);
366     }
367     if (STREQUAL(command, "DISCARD", cmd_len)) {
368 	cleanup_milter_hbc_log(context, "discard", where, buf, optional_text);
369 	vstring_strcpy(state->milter_hbc_reply, "D");
370 	state->flags |= CLEANUP_FLAG_DISCARD;
371 	state->flags &= ~CLEANUP_FLAG_FILTER_ALL;
372 	return ((char *) buf);
373     }
374     if (STREQUAL(command, "HOLD", cmd_len)) {
375 	if ((state->flags & (CLEANUP_FLAG_HOLD | CLEANUP_FLAG_DISCARD)) == 0) {
376 	    cleanup_milter_hbc_log(context, "hold", where, buf, optional_text);
377 	    state->flags |= CLEANUP_FLAG_HOLD;
378 	}
379 	return ((char *) buf);
380     }
381     if (STREQUAL(command, "REDIRECT", cmd_len)) {
382 	if (strchr(optional_text, '@') == 0) {
383 	    msg_warn("bad REDIRECT target \"%s\" in %s map -- "
384 		     "need user@domain",
385 		     optional_text, map_class);
386 	} else {
387 	    if (state->redirect)
388 		myfree(state->redirect);
389 	    state->redirect = mystrdup(optional_text);
390 	    cleanup_milter_hbc_log(context, "redirect", where, buf,
391 				   optional_text);
392 	    state->flags &= ~CLEANUP_FLAG_FILTER_ALL;
393 	}
394 	return ((char *) buf);
395     }
396     return ((char *) HBC_CHECKS_STAT_UNKNOWN);
397 }
398 
399 /* cleanup_milter_header_checks - inspect Milter-generated header */
400 
401 static int cleanup_milter_header_checks(CLEANUP_STATE *state, VSTRING *buf)
402 {
403     char   *ret;
404 
405     /*
406      * Milter application "add/insert/replace header" requests happen at the
407      * end-of-message stage, therefore all the header operations are relative
408      * to the primary message header.
409      */
410     ret = hbc_header_checks((void *) state, state->milter_hbc_checks,
411 			    MIME_HDR_PRIMARY, (HEADER_OPTS *) 0,
412 			    buf, (off_t) 0);
413     if (ret == 0) {
414 	return (0);
415     } else if (ret == HBC_CHECKS_STAT_ERROR) {
416 	msg_warn("%s: %s map lookup problem -- "
417 		 "message not accepted, try again later",
418 		 state->queue_id, VAR_MILT_HEAD_CHECKS);
419 	state->errs |= CLEANUP_STAT_WRITE;
420 	return (0);
421     } else {
422 	if (ret != STR(buf)) {
423 	    vstring_strcpy(buf, ret);
424 	    myfree(ret);
425 	}
426 	return (1);
427     }
428 }
429 
430 /* cleanup_milter_hbc_add_meta_records - add REDIRECT or FILTER meta records */
431 
432 static void cleanup_milter_hbc_add_meta_records(CLEANUP_STATE *state)
433 {
434     const char *myname = "cleanup_milter_hbc_add_meta_records";
435     off_t   reverse_ptr_offset;
436     off_t   new_meta_offset;
437 
438     /*
439      * Note: this code runs while the Milter infrastructure is being torn
440      * down. For this reason we handle all I/O errors here on the spot,
441      * instead of reporting them back through the Milter infrastructure.
442      */
443 
444     /*
445      * Sanity check.
446      */
447     if (state->append_meta_pt_offset < 0)
448 	msg_panic("%s: no meta append pointer location", myname);
449     if (state->append_meta_pt_target < 0)
450 	msg_panic("%s: no meta append pointer target", myname);
451 
452     /*
453      * Allocate space after the end of the queue file, and write the meta
454      * record(s), followed by a reverse pointer record that points to the
455      * target of the old "meta record append" pointer record. This reverse
456      * pointer record becomes the new "meta record append" pointer record.
457      * Although the new "meta record append" pointer record will never be
458      * used, we update it here to make the code more similar to other code
459      * that inserts/appends content, so that common code can be factored out
460      * later.
461      */
462     if ((new_meta_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) {
463 	cleanup_milter_set_error(state, errno);
464 	return;
465     }
466     if (state->filter != 0)
467 	cleanup_out_string(state, REC_TYPE_FILT, state->filter);
468     if (state->redirect != 0)
469 	cleanup_out_string(state, REC_TYPE_RDR, state->redirect);
470     if ((reverse_ptr_offset = vstream_ftell(state->dst)) < 0) {
471 	msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path);
472 	state->errs |= CLEANUP_STAT_WRITE;
473 	return;
474     }
475     cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
476 		       (long) state->append_meta_pt_target);
477 
478     /*
479      * Pointer flipping: update the old "meta record append" pointer record
480      * value with the location of the new meta record.
481      */
482     if (vstream_fseek(state->dst, state->append_meta_pt_offset, SEEK_SET) < 0) {
483 	cleanup_milter_set_error(state, errno);
484 	return;
485     }
486     cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
487 		       (long) new_meta_offset);
488 
489     /*
490      * Update the in-memory "meta append" pointer record location with the
491      * location of the reverse pointer record that follows the new meta
492      * record. The target of the "meta append" pointer record does not
493      * change; it's always the record that follows the dummy pointer record
494      * that was written while Postfix received the message.
495      */
496     state->append_meta_pt_offset = reverse_ptr_offset;
497 
498     /*
499      * Note: state->append_meta_pt_target never changes.
500      */
501 }
502 
503 /* cleanup_milter_header_checks_init - initialize post-Milter header checks */
504 
505 static void cleanup_milter_header_checks_init(CLEANUP_STATE *state)
506 {
507 #define NO_NESTED_HDR_NAME	""
508 #define NO_NESTED_HDR_VALUE	""
509 #define NO_MIME_HDR_NAME	""
510 #define NO_MIME_HDR_VALUE	""
511 
512     static /* XXX not const */ HBC_CALL_BACKS call_backs = {
513 	cleanup_milter_hbc_log,
514 	cleanup_milter_header_prepend,
515 	cleanup_milter_hbc_extend,
516     };
517 
518     state->milter_hbc_checks =
519 	hbc_header_checks_create(VAR_MILT_HEAD_CHECKS, var_milt_head_checks,
520 				 NO_MIME_HDR_NAME, NO_MIME_HDR_VALUE,
521 				 NO_NESTED_HDR_NAME, NO_NESTED_HDR_VALUE,
522 				 &call_backs);
523     state->milter_hbc_reply = vstring_alloc(100);
524     if (state->filter)
525 	myfree(state->filter);
526     state->filter = 0;
527     if (state->redirect)
528 	myfree(state->redirect);
529     state->redirect = 0;
530 }
531 
532 /* cleanup_milter_hbc_finish - finalize post-Milter header checks */
533 
534 static void cleanup_milter_hbc_finish(CLEANUP_STATE *state)
535 {
536     if (state->milter_hbc_checks)
537 	hbc_header_checks_free(state->milter_hbc_checks);
538     state->milter_hbc_checks = 0;
539     if (state->milter_hbc_reply)
540 	vstring_free(state->milter_hbc_reply);
541     state->milter_hbc_reply = 0;
542     if (CLEANUP_OUT_OK(state)
543 	&& !CLEANUP_MILTER_REJECTING_OR_DISCARDING_MESSAGE(state)
544 	&& (state->filter || state->redirect))
545 	cleanup_milter_hbc_add_meta_records(state);
546 }
547 
548  /*
549   * Milter replies.
550   */
551 #define CLEANUP_MILTER_SET_REASON(__state, __reason) do { \
552 	if ((__state)->reason) \
553 	    myfree((__state)->reason); \
554 	(__state)->reason = mystrdup(__reason); \
555 	if ((__state)->smtp_reply) { \
556 	    myfree((__state)->smtp_reply); \
557 	    (__state)->smtp_reply = 0; \
558 	} \
559     } while (0)
560 
561 #define CLEANUP_MILTER_SET_SMTP_REPLY(__state, __smtp_reply) do { \
562 	if ((__state)->reason) \
563 	    myfree((__state)->reason); \
564 	(__state)->reason = mystrdup(__smtp_reply + 4); \
565 	printable((__state)->reason, '_'); \
566 	if ((__state)->smtp_reply) \
567 	    myfree((__state)->smtp_reply); \
568 	(__state)->smtp_reply = mystrdup(__smtp_reply); \
569     } while (0)
570 
571 /* cleanup_milter_set_error - set error flag from errno */
572 
573 static void cleanup_milter_set_error(CLEANUP_STATE *state, int err)
574 {
575     if (err == EFBIG) {
576 	msg_warn("%s: queue file size limit exceeded", state->queue_id);
577 	state->errs |= CLEANUP_STAT_SIZE;
578     } else {
579 	msg_warn("%s: write queue file: %m", state->queue_id);
580 	state->errs |= CLEANUP_STAT_WRITE;
581     }
582 }
583 
584 /* cleanup_milter_error - return dummy error description */
585 
586 static const char *cleanup_milter_error(CLEANUP_STATE *state, int err)
587 {
588     const char *myname = "cleanup_milter_error";
589     const CLEANUP_STAT_DETAIL *dp;
590 
591     /*
592      * For consistency with error reporting within the milter infrastructure,
593      * content manipulation routines return a null pointer on success, and an
594      * SMTP-like response on error.
595      *
596      * However, when cleanup_milter_apply() receives this error response from
597      * the milter infrastructure, it ignores the text since the appropriate
598      * cleanup error flags were already set by cleanup_milter_set_error().
599      *
600      * Specify a null error number when the "errno to error flag" mapping was
601      * already done elsewhere, possibly outside this module.
602      */
603     if (err)
604 	cleanup_milter_set_error(state, err);
605     else if (CLEANUP_OUT_OK(state))
606 	msg_panic("%s: missing errno to error flag mapping", myname);
607     if (state->milter_err_text == 0)
608 	state->milter_err_text = vstring_alloc(50);
609     dp = cleanup_stat_detail(state->errs);
610     return (STR(vstring_sprintf(state->milter_err_text,
611 				"%d %s %s", dp->smtp, dp->dsn, dp->text)));
612 }
613 
614 /* cleanup_add_header - append message header */
615 
616 static const char *cleanup_add_header(void *context, const char *name,
617 				              const char *space,
618 				              const char *value)
619 {
620     const char *myname = "cleanup_add_header";
621     CLEANUP_STATE *state = (CLEANUP_STATE *) context;
622     VSTRING *buf;
623     off_t   reverse_ptr_offset;
624     off_t   new_hdr_offset;
625 
626     /*
627      * To simplify implementation, the cleanup server writes a dummy "header
628      * append" pointer record after the last message header. We cache both
629      * the location and the target of the current "header append" pointer
630      * record.
631      */
632     if (state->append_hdr_pt_offset < 0)
633 	msg_panic("%s: no header append pointer location", myname);
634     if (state->append_hdr_pt_target < 0)
635 	msg_panic("%s: no header append pointer target", myname);
636 
637     /*
638      * Return early when Milter header checks request that this header record
639      * be dropped, or that the message is discarded. Note: CLEANUP_OUT_OK()
640      * tests CLEANUP_FLAG_DISCARD. We don't want to report the latter as an
641      * error.
642      */
643     buf = vstring_alloc(100);
644     vstring_sprintf(buf, "%s:%s%s", name, space, value);
645     if (state->milter_hbc_checks) {
646 	if (cleanup_milter_header_checks(state, buf) == 0
647 	    || (state->flags & CLEANUP_FLAG_DISCARD)) {
648 	    vstring_free(buf);
649 	    return (0);
650 	}
651 	if (CLEANUP_OUT_OK(state) == 0) {
652 	    vstring_free(buf);
653 	    return (cleanup_milter_error(state, 0));
654 	}
655     }
656 
657     /*
658      * Allocate space after the end of the queue file, and write the header
659      * record(s), followed by a reverse pointer record that points to the
660      * target of the old "header append" pointer record. This reverse pointer
661      * record becomes the new "header append" pointer record.
662      */
663     if ((new_hdr_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) {
664 	msg_warn("%s: seek file %s: %m", myname, cleanup_path);
665 	vstring_free(buf);
666 	return (cleanup_milter_error(state, errno));
667     }
668     /* XXX emit prepended header, then clear it. */
669     cleanup_out_header(state, buf);		/* Includes padding */
670     vstring_free(buf);
671     if ((reverse_ptr_offset = vstream_ftell(state->dst)) < 0) {
672 	msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path);
673 	return (cleanup_milter_error(state, errno));
674     }
675     cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
676 		       (long) state->append_hdr_pt_target);
677 
678     /*
679      * Pointer flipping: update the old "header append" pointer record value
680      * with the location of the new header record.
681      *
682      * XXX To avoid unnecessary seek operations when the new header immediately
683      * follows the old append header pointer, write a null pointer or make
684      * the record reading loop smarter. Making vstream_fseek() smarter does
685      * not help, because it doesn't know if we're going to read or write
686      * after a write+seek sequence.
687      */
688     if (vstream_fseek(state->dst, state->append_hdr_pt_offset, SEEK_SET) < 0) {
689 	msg_warn("%s: seek file %s: %m", myname, cleanup_path);
690 	return (cleanup_milter_error(state, errno));
691     }
692     cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
693 		       (long) new_hdr_offset);
694 
695     /*
696      * Update the in-memory "header append" pointer record location with the
697      * location of the reverse pointer record that follows the new header.
698      * The target of the "header append" pointer record does not change; it's
699      * always the record that follows the dummy pointer record that was
700      * written while Postfix received the message.
701      */
702     state->append_hdr_pt_offset = reverse_ptr_offset;
703 
704     /*
705      * In case of error while doing record output.
706      */
707     return (CLEANUP_OUT_OK(state) == 0 ? cleanup_milter_error(state, 0) :
708 	    state->milter_hbc_reply && LEN(state->milter_hbc_reply) ?
709 	    STR(state->milter_hbc_reply) : 0);
710 
711     /*
712      * Note: state->append_hdr_pt_target never changes.
713      */
714 }
715 
716 /* cleanup_find_header_start - find specific header instance */
717 
718 static off_t cleanup_find_header_start(CLEANUP_STATE *state, ssize_t index,
719 				               const char *header_label,
720 				               VSTRING *buf,
721 				               int *prec_type,
722 				               int allow_ptr_backup,
723 				               int skip_headers)
724 {
725     const char *myname = "cleanup_find_header_start";
726     off_t   curr_offset;		/* offset after found record */
727     off_t   ptr_offset;			/* pointer to found record */
728     VSTRING *ptr_buf = 0;
729     int     rec_type = REC_TYPE_ERROR;
730     int     last_type;
731     ssize_t len;
732     int     hdr_count = 0;
733 
734     if (msg_verbose)
735 	msg_info("%s: index %ld name \"%s\"",
736 	      myname, (long) index, header_label ? header_label : "(none)");
737 
738     /*
739      * Sanity checks.
740      */
741     if (index < 1)
742 	msg_panic("%s: bad header index %ld", myname, (long) index);
743 
744     /*
745      * Skip to the start of the message content, and read records until we
746      * either find the specified header, or until we hit the end of the
747      * headers.
748      *
749      * The index specifies the header instance: 1 is the first one. The header
750      * label specifies the header name. A null pointer matches any header.
751      *
752      * When the specified header is not found, the result value is -1.
753      *
754      * When the specified header is found, its first record is stored in the
755      * caller-provided read buffer, and the result value is the queue file
756      * offset of that record. The file read position is left at the start of
757      * the next (non-filler) queue file record, which can be the remainder of
758      * a multi-record header.
759      *
760      * When a header is found and allow_ptr_backup is non-zero, then the result
761      * is either the first record of that header, or it is the pointer record
762      * that points to the first record of that header. In the latter case,
763      * the file read position is undefined. Returning the pointer allows us
764      * to do some optimizations when inserting text multiple times at the
765      * same place.
766      *
767      * XXX We can't use the MIME processor here. It not only buffers up the
768      * input, it also reads the record that follows a complete header before
769      * it invokes the header call-back action. This complicates the way that
770      * we discover header offsets and boundaries. Worse is that the MIME
771      * processor is unaware that multi-record message headers can have PTR
772      * records in the middle.
773      *
774      * XXX The draw-back of not using the MIME processor is that we have to
775      * duplicate some of its logic here and in the routine that finds the end
776      * of the header record. To minimize the duplication we define an ugly
777      * macro that is used in all code that scans for header boundaries.
778      *
779      * XXX Sendmail compatibility (based on Sendmail 8.13.6 measurements).
780      *
781      * - When changing Received: header #1, we change the Received: header that
782      * follows our own one; a request to change Received: header #0 is
783      * silently treated as a request to change Received: header #1.
784      *
785      * - When changing Date: header #1, we change the first Date: header; a
786      * request to change Date: header #0 is silently treated as a request to
787      * change Date: header #1.
788      *
789      * Thus, header change requests are relative to the content as received,
790      * that is, the content after our own Received: header. They can affect
791      * only the headers that the MTA actually exposes to mail filter
792      * applications.
793      *
794      * - However, when inserting a header at position 0, the new header appears
795      * before our own Received: header, and when inserting at position 1, the
796      * new header appears after our own Received: header.
797      *
798      * Thus, header insert operations are relative to the content as delivered,
799      * that is, the content including our own Received: header.
800      *
801      * None of the above is applicable after a Milter inserts a header before
802      * our own Received: header. From then on, our own Received: header
803      * becomes just like other headers.
804      */
805 #define CLEANUP_FIND_HEADER_NOTFOUND	(-1)
806 #define CLEANUP_FIND_HEADER_IOERROR	(-2)
807 
808 #define CLEANUP_FIND_HEADER_RETURN(offs) do { \
809 	if (ptr_buf) \
810 	    vstring_free(ptr_buf); \
811 	return (offs); \
812     } while (0)
813 
814 #define GET_NEXT_TEXT_OR_PTR_RECORD(rec_type, state, buf, curr_offset, quit) \
815     if ((rec_type = rec_get_raw(state->dst, buf, 0, REC_FLAG_NONE)) < 0) { \
816 	msg_warn("%s: read file %s: %m", myname, cleanup_path); \
817 	cleanup_milter_set_error(state, errno); \
818 	do { quit; } while (0); \
819     } \
820     if (msg_verbose > 1) \
821 	msg_info("%s: read: %ld: %.*s", myname, (long) curr_offset, \
822 		 LEN(buf) > 30 ? 30 : (int) LEN(buf), STR(buf)); \
823     if (rec_type == REC_TYPE_DTXT) \
824 	continue; \
825     if (rec_type != REC_TYPE_NORM && rec_type != REC_TYPE_CONT \
826 	&& rec_type != REC_TYPE_PTR) \
827 	break;
828     /* End of hairy macros. */
829 
830     if (vstream_fseek(state->dst, state->data_offset, SEEK_SET) < 0) {
831 	msg_warn("%s: seek file %s: %m", myname, cleanup_path);
832 	cleanup_milter_set_error(state, errno);
833 	CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR);
834     }
835     for (ptr_offset = 0, last_type = 0; /* void */ ; /* void */ ) {
836 	if ((curr_offset = vstream_ftell(state->dst)) < 0) {
837 	    msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path);
838 	    cleanup_milter_set_error(state, errno);
839 	    CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR);
840 	}
841 	/* Don't follow the "append header" pointer. */
842 	if (curr_offset == state->append_hdr_pt_offset)
843 	    break;
844 	/* Caution: this macro terminates the loop at end-of-message. */
845 	/* Don't do complex processing while breaking out of this loop. */
846 	GET_NEXT_TEXT_OR_PTR_RECORD(rec_type, state, buf, curr_offset,
847 		   CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR));
848 	/* Caution: don't assume ptr->header. This may be header-ptr->body. */
849 	if (rec_type == REC_TYPE_PTR) {
850 	    if (rec_goto(state->dst, STR(buf)) < 0) {
851 		msg_warn("%s: read file %s: %m", myname, cleanup_path);
852 		cleanup_milter_set_error(state, errno);
853 		CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR);
854 	    }
855 	    /* Save PTR record, in case it points to the start of a header. */
856 	    if (allow_ptr_backup) {
857 		ptr_offset = curr_offset;
858 		if (ptr_buf == 0)
859 		    ptr_buf = vstring_alloc(100);
860 		vstring_strcpy(ptr_buf, STR(buf));
861 	    }
862 	    /* Don't update last_type; PTR can happen after REC_TYPE_CONT. */
863 	    continue;
864 	}
865 	/* The middle of a multi-record header. */
866 	else if (last_type == REC_TYPE_CONT || IS_SPACE_TAB(STR(buf)[0])) {
867 	    /* Reset the saved PTR record and update last_type. */
868 	}
869 	/* No more message headers. */
870 	else if ((len = is_header(STR(buf))) == 0) {
871 	    break;
872 	}
873 	/* This the start of a message header. */
874 	else if (hdr_count++ < skip_headers)
875 	     /* Reset the saved PTR record and update last_type. */ ;
876 	else if ((header_label == 0
877 		  || (strncasecmp(header_label, STR(buf), len) == 0
878 		      && (strlen(header_label) == len)))
879 		 && --index == 0) {
880 	    /* If we have a saved PTR record, it points to start of header. */
881 	    break;
882 	}
883 	ptr_offset = 0;
884 	last_type = rec_type;
885     }
886 
887     /*
888      * In case of failure, return negative start position.
889      */
890     if (index > 0) {
891 	curr_offset = CLEANUP_FIND_HEADER_NOTFOUND;
892     } else {
893 
894 	/*
895 	 * Skip over short-header padding, so that the file read pointer is
896 	 * always positioned at the first non-padding record after the header
897 	 * record. Insist on padding after short a header record, so that a
898 	 * short header record can safely be overwritten by a pointer record.
899 	 */
900 	if (LEN(buf) < REC_TYPE_PTR_PAYL_SIZE) {
901 	    VSTRING *rbuf = (ptr_offset ? buf :
902 			     (ptr_buf ? ptr_buf :
903 			      (ptr_buf = vstring_alloc(100))));
904 	    int     rval;
905 
906 	    if ((rval = rec_get_raw(state->dst, rbuf, 0, REC_FLAG_NONE)) < 0) {
907 		cleanup_milter_set_error(state, errno);
908 		CLEANUP_FIND_HEADER_RETURN(CLEANUP_FIND_HEADER_IOERROR);
909 	    }
910 	    if (rval != REC_TYPE_DTXT)
911 		msg_panic("%s: short header without padding", myname);
912 	}
913 
914 	/*
915 	 * Optionally return a pointer to the message header, instead of the
916 	 * start of the message header itself. In that case the file read
917 	 * position is undefined (actually it is at the first non-padding
918 	 * record that follows the message header record).
919 	 */
920 	if (ptr_offset != 0) {
921 	    rec_type = REC_TYPE_PTR;
922 	    curr_offset = ptr_offset;
923 	    vstring_strcpy(buf, STR(ptr_buf));
924 	}
925 	*prec_type = rec_type;
926     }
927     if (msg_verbose)
928 	msg_info("%s: index %ld name %s type %d offset %ld",
929 		 myname, (long) index, header_label ?
930 		 header_label : "(none)", rec_type, (long) curr_offset);
931 
932     CLEANUP_FIND_HEADER_RETURN(curr_offset);
933 }
934 
935 /* cleanup_find_header_end - find end of header */
936 
937 static off_t cleanup_find_header_end(CLEANUP_STATE *state,
938 				             VSTRING *rec_buf,
939 				             int last_type)
940 {
941     const char *myname = "cleanup_find_header_end";
942     off_t   read_offset;
943     int     rec_type;
944 
945     /*
946      * This routine is called immediately after cleanup_find_header_start().
947      * rec_buf is the cleanup_find_header_start() result record; last_type is
948      * the corresponding record type: REC_TYPE_PTR or REC_TYPE_NORM; the file
949      * read position is at the first non-padding record after the result
950      * header record.
951      */
952     for (;;) {
953 	if ((read_offset = vstream_ftell(state->dst)) < 0) {
954 	    msg_warn("%s: read file %s: %m", myname, cleanup_path);
955 	    cleanup_milter_error(state, errno);
956 	    return (-1);
957 	}
958 	/* Don't follow the "append header" pointer. */
959 	if (read_offset == state->append_hdr_pt_offset)
960 	    break;
961 	/* Caution: this macro terminates the loop at end-of-message. */
962 	/* Don't do complex processing while breaking out of this loop. */
963 	GET_NEXT_TEXT_OR_PTR_RECORD(rec_type, state, rec_buf, read_offset,
964 	/* Warning and errno->error mapping are done elsewhere. */
965 				    return (-1));
966 	if (rec_type == REC_TYPE_PTR) {
967 	    if (rec_goto(state->dst, STR(rec_buf)) < 0) {
968 		msg_warn("%s: read file %s: %m", myname, cleanup_path);
969 		cleanup_milter_error(state, errno);
970 		return (-1);
971 	    }
972 	    /* Don't update last_type; PTR may follow REC_TYPE_CONT. */
973 	    continue;
974 	}
975 	/* Start of header or message body. */
976 	if (last_type != REC_TYPE_CONT && !IS_SPACE_TAB(STR(rec_buf)[0]))
977 	    break;
978 	last_type = rec_type;
979     }
980     return (read_offset);
981 }
982 
983 /* cleanup_patch_header - patch new header into an existing header */
984 
985 static const char *cleanup_patch_header(CLEANUP_STATE *state,
986 					        const char *new_hdr_name,
987 					        const char *hdr_space,
988 					        const char *new_hdr_value,
989 					        off_t old_rec_offset,
990 					        int old_rec_type,
991 					        VSTRING *old_rec_buf,
992 					        off_t next_offset)
993 {
994     const char *myname = "cleanup_patch_header";
995     VSTRING *buf = vstring_alloc(100);
996     off_t   new_hdr_offset;
997 
998 #define CLEANUP_PATCH_HEADER_RETURN(ret) do { \
999 	vstring_free(buf); \
1000 	return (ret); \
1001     } while (0)
1002 
1003     if (msg_verbose)
1004 	msg_info("%s: \"%s\" \"%s\" at %ld",
1005 		 myname, new_hdr_name, new_hdr_value, (long) old_rec_offset);
1006 
1007     /*
1008      * Allocate space after the end of the queue file for the new header and
1009      * optionally save an existing record to make room for a forward pointer
1010      * record. If the saved record was not a PTR record, follow the saved
1011      * record by a reverse pointer record that points to the record after the
1012      * original location of the saved record.
1013      *
1014      * We update the queue file in a safe manner: save the new header and the
1015      * existing records after the end of the queue file, write the reverse
1016      * pointer, and only then overwrite the saved records with the forward
1017      * pointer to the new header.
1018      *
1019      * old_rec_offset, old_rec_type, and old_rec_buf specify the record that we
1020      * are about to overwrite with a pointer record. If the record needs to
1021      * be saved (i.e. old_rec_type > 0), the buffer contains the data content
1022      * of exactly one PTR or text record.
1023      *
1024      * next_offset specifies the record that follows the to-be-overwritten
1025      * record. It is ignored when the to-be-saved record is a pointer record.
1026      */
1027 
1028     /*
1029      * Return early when Milter header checks request that this header record
1030      * be dropped.
1031      */
1032     vstring_sprintf(buf, "%s:%s%s", new_hdr_name, hdr_space, new_hdr_value);
1033     if (state->milter_hbc_checks
1034 	&& cleanup_milter_header_checks(state, buf) == 0)
1035 	CLEANUP_PATCH_HEADER_RETURN(0);
1036 
1037     /*
1038      * Write the new header to a new location after the end of the queue
1039      * file.
1040      */
1041     if ((new_hdr_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) {
1042 	msg_warn("%s: seek file %s: %m", myname, cleanup_path);
1043 	CLEANUP_PATCH_HEADER_RETURN(cleanup_milter_error(state, errno));
1044     }
1045     /* XXX emit prepended header, then clear it. */
1046     cleanup_out_header(state, buf);		/* Includes padding */
1047     if (msg_verbose > 1)
1048 	msg_info("%s: %ld: write %.*s", myname, (long) new_hdr_offset,
1049 		 LEN(buf) > 30 ? 30 : (int) LEN(buf), STR(buf));
1050 
1051     /*
1052      * Optionally, save the existing text record or pointer record that will
1053      * be overwritten with the forward pointer. Pad a short saved record to
1054      * ensure that it, too, can be overwritten by a pointer.
1055      */
1056     if (old_rec_type > 0) {
1057 	CLEANUP_OUT_BUF(state, old_rec_type, old_rec_buf);
1058 	if (LEN(old_rec_buf) < REC_TYPE_PTR_PAYL_SIZE)
1059 	    rec_pad(state->dst, REC_TYPE_DTXT,
1060 		    REC_TYPE_PTR_PAYL_SIZE - LEN(old_rec_buf));
1061 	if (msg_verbose > 1)
1062 	    msg_info("%s: write %.*s", myname, LEN(old_rec_buf) > 30 ?
1063 		     30 : (int) LEN(old_rec_buf), STR(old_rec_buf));
1064     }
1065 
1066     /*
1067      * If the saved record wasn't a PTR record, write the reverse pointer
1068      * after the saved records. A reverse pointer value of -1 means we were
1069      * confused about what we were going to save.
1070      */
1071     if (old_rec_type != REC_TYPE_PTR) {
1072 	if (next_offset < 0)
1073 	    msg_panic("%s: bad reverse pointer %ld",
1074 		      myname, (long) next_offset);
1075 	cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
1076 			   (long) next_offset);
1077 	if (msg_verbose > 1)
1078 	    msg_info("%s: write PTR %ld", myname, (long) next_offset);
1079     }
1080 
1081     /*
1082      * Write the forward pointer over the old record. Generally, a pointer
1083      * record will be shorter than a header record, so there will be a gap in
1084      * the queue file before the next record. In other words, we must always
1085      * follow pointer records otherwise we get out of sync with the data.
1086      */
1087     if (vstream_fseek(state->dst, old_rec_offset, SEEK_SET) < 0) {
1088 	msg_warn("%s: seek file %s: %m", myname, cleanup_path);
1089 	CLEANUP_PATCH_HEADER_RETURN(cleanup_milter_error(state, errno));
1090     }
1091     cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
1092 		       (long) new_hdr_offset);
1093     if (msg_verbose > 1)
1094 	msg_info("%s: %ld: write PTR %ld", myname, (long) old_rec_offset,
1095 		 (long) new_hdr_offset);
1096 
1097     /*
1098      * In case of error while doing record output.
1099      */
1100     CLEANUP_PATCH_HEADER_RETURN(
1101 	       CLEANUP_OUT_OK(state) == 0 ? cleanup_milter_error(state, 0) :
1102 		   state->milter_hbc_reply && LEN(state->milter_hbc_reply) ?
1103 				STR(state->milter_hbc_reply) : 0);
1104 
1105     /*
1106      * Note: state->append_hdr_pt_target never changes.
1107      */
1108 }
1109 
1110 /* cleanup_ins_header - insert message header */
1111 
1112 static const char *cleanup_ins_header(void *context, ssize_t index,
1113 				              const char *new_hdr_name,
1114 				              const char *hdr_space,
1115 				              const char *new_hdr_value)
1116 {
1117     const char *myname = "cleanup_ins_header";
1118     CLEANUP_STATE *state = (CLEANUP_STATE *) context;
1119     VSTRING *old_rec_buf = vstring_alloc(100);
1120     off_t   old_rec_offset;
1121     int     old_rec_type;
1122     off_t   next_offset;
1123     const char *ret;
1124 
1125 #define CLEANUP_INS_HEADER_RETURN(ret) do { \
1126 	vstring_free(old_rec_buf); \
1127 	return (ret); \
1128     } while (0)
1129 
1130     if (msg_verbose)
1131 	msg_info("%s: %ld \"%s\" \"%s\"",
1132 		 myname, (long) index, new_hdr_name, new_hdr_value);
1133 
1134     /*
1135      * Look for a header at the specified position.
1136      *
1137      * The lookup result may be a pointer record. This allows us to make some
1138      * optimization when multiple insert operations happen in the same place.
1139      *
1140      * Index 1 is the top-most header.
1141      */
1142 #define NO_HEADER_NAME	((char *) 0)
1143 #define ALLOW_PTR_BACKUP	1
1144 #define SKIP_ONE_HEADER		1
1145 #define DONT_SKIP_HEADERS	0
1146 
1147     if (index < 1)
1148 	index = 1;
1149     old_rec_offset = cleanup_find_header_start(state, index, NO_HEADER_NAME,
1150 					       old_rec_buf, &old_rec_type,
1151 					       ALLOW_PTR_BACKUP,
1152 					       DONT_SKIP_HEADERS);
1153     if (old_rec_offset == CLEANUP_FIND_HEADER_IOERROR)
1154 	/* Warning and errno->error mapping are done elsewhere. */
1155 	CLEANUP_INS_HEADER_RETURN(cleanup_milter_error(state, 0));
1156 
1157     /*
1158      * If the header does not exist, simply append the header to the linked
1159      * list at the "header append" pointer record.
1160      */
1161     if (old_rec_offset < 0)
1162 	CLEANUP_INS_HEADER_RETURN(cleanup_add_header(context, new_hdr_name,
1163 						 hdr_space, new_hdr_value));
1164 
1165     /*
1166      * If the header does exist, save both the new and the existing header to
1167      * new storage at the end of the queue file, and link the new storage
1168      * with a forward and reverse pointer (don't write a reverse pointer if
1169      * we are starting with a pointer record).
1170      */
1171     if (old_rec_type == REC_TYPE_PTR) {
1172 	next_offset = -1;
1173     } else {
1174 	if ((next_offset = vstream_ftell(state->dst)) < 0) {
1175 	    msg_warn("%s: read file %s: %m", myname, cleanup_path);
1176 	    CLEANUP_INS_HEADER_RETURN(cleanup_milter_error(state, errno));
1177 	}
1178     }
1179     ret = cleanup_patch_header(state, new_hdr_name, hdr_space, new_hdr_value,
1180 			       old_rec_offset, old_rec_type,
1181 			       old_rec_buf, next_offset);
1182     CLEANUP_INS_HEADER_RETURN(ret);
1183 }
1184 
1185 /* cleanup_upd_header - modify or append message header */
1186 
1187 static const char *cleanup_upd_header(void *context, ssize_t index,
1188 				              const char *new_hdr_name,
1189 				              const char *hdr_space,
1190 				              const char *new_hdr_value)
1191 {
1192     const char *myname = "cleanup_upd_header";
1193     CLEANUP_STATE *state = (CLEANUP_STATE *) context;
1194     VSTRING *rec_buf;
1195     off_t   old_rec_offset;
1196     off_t   next_offset;
1197     int     last_type;
1198     const char *ret;
1199 
1200     if (msg_verbose)
1201 	msg_info("%s: %ld \"%s\" \"%s\"",
1202 		 myname, (long) index, new_hdr_name, new_hdr_value);
1203 
1204     /*
1205      * Sanity check.
1206      */
1207     if (*new_hdr_name == 0)
1208 	msg_panic("%s: null header name", myname);
1209 
1210     /*
1211      * Find the header that is being modified.
1212      *
1213      * The lookup result will never be a pointer record.
1214      *
1215      * Index 1 is the first matching header instance.
1216      *
1217      * XXX When a header is updated repeatedly we create jumps to jumps. To
1218      * eliminate this, rewrite the loop below so that we can start with the
1219      * pointer record that points to the header that's being edited.
1220      */
1221 #define DONT_SAVE_RECORD	0
1222 #define NO_PTR_BACKUP		0
1223 
1224 #define CLEANUP_UPD_HEADER_RETURN(ret) do { \
1225 	vstring_free(rec_buf); \
1226 	return (ret); \
1227     } while (0)
1228 
1229     rec_buf = vstring_alloc(100);
1230     old_rec_offset = cleanup_find_header_start(state, index, new_hdr_name,
1231 					       rec_buf, &last_type,
1232 					       NO_PTR_BACKUP,
1233 					       SKIP_ONE_HEADER);
1234     if (old_rec_offset == CLEANUP_FIND_HEADER_IOERROR)
1235 	/* Warning and errno->error mapping are done elsewhere. */
1236 	CLEANUP_UPD_HEADER_RETURN(cleanup_milter_error(state, 0));
1237 
1238     /*
1239      * If no old header is found, simply append the new header to the linked
1240      * list at the "header append" pointer record.
1241      */
1242     if (old_rec_offset < 0)
1243 	CLEANUP_UPD_HEADER_RETURN(cleanup_add_header(context, new_hdr_name,
1244 						 hdr_space, new_hdr_value));
1245 
1246     /*
1247      * If the old header is found, find the end of the old header, save the
1248      * new header to new storage at the end of the queue file, and link the
1249      * new storage with a forward and reverse pointer.
1250      */
1251     if ((next_offset = cleanup_find_header_end(state, rec_buf, last_type)) < 0)
1252 	/* Warning and errno->error mapping are done elsewhere. */
1253 	CLEANUP_UPD_HEADER_RETURN(cleanup_milter_error(state, 0));
1254     ret = cleanup_patch_header(state, new_hdr_name, hdr_space, new_hdr_value,
1255 			       old_rec_offset, DONT_SAVE_RECORD,
1256 			       (VSTRING *) 0, next_offset);
1257     CLEANUP_UPD_HEADER_RETURN(ret);
1258 }
1259 
1260 /* cleanup_del_header - delete message header */
1261 
1262 static const char *cleanup_del_header(void *context, ssize_t index,
1263 				              const char *hdr_name)
1264 {
1265     const char *myname = "cleanup_del_header";
1266     CLEANUP_STATE *state = (CLEANUP_STATE *) context;
1267     VSTRING *rec_buf;
1268     off_t   header_offset;
1269     off_t   next_offset;
1270     int     last_type;
1271 
1272     if (msg_verbose)
1273 	msg_info("%s: %ld \"%s\"", myname, (long) index, hdr_name);
1274 
1275     /*
1276      * Sanity check.
1277      */
1278     if (*hdr_name == 0)
1279 	msg_panic("%s: null header name", myname);
1280 
1281     /*
1282      * Find the header that is being deleted.
1283      *
1284      * The lookup result will never be a pointer record.
1285      *
1286      * Index 1 is the first matching header instance.
1287      */
1288 #define CLEANUP_DEL_HEADER_RETURN(ret) do { \
1289 	vstring_free(rec_buf); \
1290 	return (ret); \
1291     } while (0)
1292 
1293     rec_buf = vstring_alloc(100);
1294     header_offset = cleanup_find_header_start(state, index, hdr_name, rec_buf,
1295 					      &last_type, NO_PTR_BACKUP,
1296 					      SKIP_ONE_HEADER);
1297     if (header_offset == CLEANUP_FIND_HEADER_IOERROR)
1298 	/* Warning and errno->error mapping are done elsewhere. */
1299 	CLEANUP_DEL_HEADER_RETURN(cleanup_milter_error(state, 0));
1300 
1301     /*
1302      * Overwrite the beginning of the header record with a pointer to the
1303      * information that follows the header. We can't simply overwrite the
1304      * header with cleanup_out_header() and a special record type, because
1305      * there may be a PTR record in the middle of a multi-line header.
1306      */
1307     if (header_offset > 0) {
1308 	if ((next_offset = cleanup_find_header_end(state, rec_buf, last_type)) < 0)
1309 	    /* Warning and errno->error mapping are done elsewhere. */
1310 	    CLEANUP_DEL_HEADER_RETURN(cleanup_milter_error(state, 0));
1311 	/* Mark the header as deleted. */
1312 	if (vstream_fseek(state->dst, header_offset, SEEK_SET) < 0) {
1313 	    msg_warn("%s: seek file %s: %m", myname, cleanup_path);
1314 	    CLEANUP_DEL_HEADER_RETURN(cleanup_milter_error(state, errno));
1315 	}
1316 	rec_fprintf(state->dst, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
1317 		    (long) next_offset);
1318     }
1319     vstring_free(rec_buf);
1320 
1321     /*
1322      * In case of error while doing record output.
1323      */
1324     return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, 0));
1325 }
1326 
1327 /* cleanup_chg_from - replace sender address, ignore ESMTP arguments */
1328 
1329 static const char *cleanup_chg_from(void *context, const char *ext_from,
1330 				            const char *esmtp_args)
1331 {
1332     const char *myname = "cleanup_chg_from";
1333     CLEANUP_STATE *state = (CLEANUP_STATE *) context;
1334     off_t   new_sender_offset;
1335     off_t   after_sender_offs;
1336     int     addr_count;
1337     TOK822 *tree;
1338     TOK822 *tp;
1339     VSTRING *int_sender_buf;
1340 
1341     if (msg_verbose)
1342 	msg_info("%s: \"%s\" \"%s\"", myname, ext_from, esmtp_args);
1343 
1344     if (esmtp_args[0])
1345 	msg_warn("%s: %s: ignoring ESMTP arguments \"%.100s\"",
1346 		 state->queue_id, myname, esmtp_args);
1347 
1348     /*
1349      * The cleanup server remembers the location of the the original sender
1350      * address record (offset in sender_pt_offset) and the file offset of the
1351      * record that follows the sender address (offset in sender_pt_target).
1352      * Short original sender records are padded, so that they can safely be
1353      * overwritten with a pointer record to the new sender address record.
1354      */
1355     if (state->sender_pt_offset < 0)
1356 	msg_panic("%s: no original sender record offset", myname);
1357     if (state->sender_pt_target < 0)
1358 	msg_panic("%s: no post-sender record offset", myname);
1359 
1360     /*
1361      * Allocate space after the end of the queue file, and write the new
1362      * sender record, followed by a reverse pointer record that points to the
1363      * record that follows the original sender address record. No padding is
1364      * needed for a "new" short sender record, since the record is not meant
1365      * to be overwritten. When the "new" sender is replaced, we allocate a
1366      * new record at the end of the queue file.
1367      *
1368      * We update the queue file in a safe manner: save the new sender after the
1369      * end of the queue file, write the reverse pointer, and only then
1370      * overwrite the old sender record with the forward pointer to the new
1371      * sender.
1372      */
1373     if ((new_sender_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) {
1374 	msg_warn("%s: seek file %s: %m", myname, cleanup_path);
1375 	return (cleanup_milter_error(state, errno));
1376     }
1377 
1378     /*
1379      * Transform the address from external form to internal form. This also
1380      * removes the enclosing <>, if present.
1381      *
1382      * XXX vstring_alloc() rejects zero-length requests.
1383      */
1384     int_sender_buf = vstring_alloc(strlen(ext_from) + 1);
1385     tree = tok822_parse(ext_from);
1386     for (addr_count = 0, tp = tree; tp != 0; tp = tp->next) {
1387 	if (tp->type == TOK822_ADDR) {
1388 	    if (addr_count == 0) {
1389 		tok822_internalize(int_sender_buf, tp->head, TOK822_STR_DEFL);
1390 		addr_count += 1;
1391 	    } else {
1392 		msg_warn("%s: Milter request to add multi-sender: \"%s\"",
1393 			 state->queue_id, ext_from);
1394 		break;
1395 	    }
1396 	}
1397     }
1398     tok822_free_tree(tree);
1399     after_sender_offs = cleanup_addr_sender(state, STR(int_sender_buf));
1400     vstring_free(int_sender_buf);
1401     cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
1402 		       (long) state->sender_pt_target);
1403     state->sender_pt_target = after_sender_offs;
1404 
1405     /*
1406      * Overwrite the original sender record with the pointer to the new
1407      * sender address record.
1408      */
1409     if (vstream_fseek(state->dst, state->sender_pt_offset, SEEK_SET) < 0) {
1410 	msg_warn("%s: seek file %s: %m", myname, cleanup_path);
1411 	return (cleanup_milter_error(state, errno));
1412     }
1413     cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
1414 		       (long) new_sender_offset);
1415 
1416     /*
1417      * In case of error while doing record output.
1418      */
1419     return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, 0));
1420 }
1421 
1422 /* cleanup_add_rcpt_par - append recipient address, with ESMTP arguments */
1423 
1424 static const char *cleanup_add_rcpt_par(void *context, const char *ext_rcpt,
1425 					        const char *esmtp_args)
1426 {
1427     const char *myname = "cleanup_add_rcpt_par";
1428     CLEANUP_STATE *state = (CLEANUP_STATE *) context;
1429     off_t   new_rcpt_offset;
1430     off_t   reverse_ptr_offset;
1431     int     addr_count;
1432     TOK822 *tree;
1433     TOK822 *tp;
1434     VSTRING *int_rcpt_buf;
1435     VSTRING *orcpt_buf = 0;
1436     ARGV   *esmtp_argv;
1437     int     dsn_notify = 0;
1438     const char *dsn_orcpt_info = 0;
1439     size_t  type_len;
1440     int     i;
1441     const char *arg;
1442     const char *arg_val;
1443 
1444     if (msg_verbose)
1445 	msg_info("%s: \"%s\" \"%s\"", myname, ext_rcpt, esmtp_args);
1446 
1447     /*
1448      * To simplify implementation, the cleanup server writes a dummy
1449      * "recipient append" pointer record after the last recipient. We cache
1450      * both the location and the target of the current "recipient append"
1451      * pointer record.
1452      */
1453     if (state->append_rcpt_pt_offset < 0)
1454 	msg_panic("%s: no recipient append pointer location", myname);
1455     if (state->append_rcpt_pt_target < 0)
1456 	msg_panic("%s: no recipient append pointer target", myname);
1457 
1458     /*
1459      * Allocate space after the end of the queue file, and write the
1460      * recipient record, followed by a reverse pointer record that points to
1461      * the target of the old "recipient append" pointer record. This reverse
1462      * pointer record becomes the new "recipient append" pointer record.
1463      *
1464      * We update the queue file in a safe manner: save the new recipient after
1465      * the end of the queue file, write the reverse pointer, and only then
1466      * overwrite the old "recipient append" pointer with the forward pointer
1467      * to the new recipient.
1468      */
1469     if ((new_rcpt_offset = vstream_fseek(state->dst, (off_t) 0, SEEK_END)) < 0) {
1470 	msg_warn("%s: seek file %s: %m", myname, cleanup_path);
1471 	return (cleanup_milter_error(state, errno));
1472     }
1473 
1474     /*
1475      * Parse ESMTP parameters. XXX UTF8SMTP don't assume ORCPT is xtext.
1476      */
1477     if (esmtp_args[0]) {
1478 	esmtp_argv = argv_split(esmtp_args, " ");
1479 	for (i = 0; i < esmtp_argv->argc; ++i) {
1480 	    arg = esmtp_argv->argv[i];
1481 	    if (strncasecmp(arg, "NOTIFY=", 7) == 0) {	/* RFC 3461 */
1482 		if (dsn_notify || (dsn_notify = dsn_notify_mask(arg + 7)) == 0)
1483 		    msg_warn("%s: Bad NOTIFY parameter from Milter or "
1484 			     "header/body_checks: \"%.100s\"",
1485 			     state->queue_id, arg);
1486 	    } else if (strncasecmp(arg, "ORCPT=", 6) == 0) {	/* RFC 3461 */
1487 		if (orcpt_buf == 0)
1488 		    orcpt_buf = vstring_alloc(100);
1489 		if (dsn_orcpt_info
1490 		    || (type_len = strcspn(arg_val = arg + 6, ";")) == 0
1491 		    || (arg_val)[type_len] != ';'
1492 		    || xtext_unquote_append(vstring_sprintf(orcpt_buf,
1493 						    "%.*s;", (int) type_len,
1494 							    arg_val),
1495 					    arg_val + type_len + 1) == 0) {
1496 		    msg_warn("%s: Bad ORCPT parameter from Milter or "
1497 			     "header/body_checks: \"%.100s\"",
1498 			     state->queue_id, arg);
1499 		} else {
1500 		    dsn_orcpt_info = STR(orcpt_buf);
1501 		}
1502 	    } else {
1503 		msg_warn("%s: ignoring ESMTP argument from Milter or "
1504 			 "header/body_checks: \"%.100s\"",
1505 			 state->queue_id, arg);
1506 	    }
1507 	}
1508 	argv_free(esmtp_argv);
1509     }
1510 
1511     /*
1512      * Transform recipient from external form to internal form. This also
1513      * removes the enclosing <>, if present.
1514      *
1515      * XXX vstring_alloc() rejects zero-length requests.
1516      */
1517     int_rcpt_buf = vstring_alloc(strlen(ext_rcpt) + 1);
1518     tree = tok822_parse(ext_rcpt);
1519     for (addr_count = 0, tp = tree; tp != 0; tp = tp->next) {
1520 	if (tp->type == TOK822_ADDR) {
1521 	    if (addr_count == 0) {
1522 		tok822_internalize(int_rcpt_buf, tp->head, TOK822_STR_DEFL);
1523 		addr_count += 1;
1524 	    } else {
1525 		msg_warn("%s: Milter or header/body_checks request to "
1526 			 "add multi-recipient: \"%s\"",
1527 			 state->queue_id, ext_rcpt);
1528 		break;
1529 	    }
1530 	}
1531     }
1532     tok822_free_tree(tree);
1533     if (addr_count != 0)
1534 	cleanup_addr_bcc_dsn(state, STR(int_rcpt_buf), dsn_orcpt_info,
1535 			     dsn_notify ? dsn_notify : DEF_DSN_NOTIFY);
1536     else
1537 	msg_warn("%s: ignoring attempt from Milter to add null recipient",
1538 		 state->queue_id);
1539     vstring_free(int_rcpt_buf);
1540     if (orcpt_buf)
1541 	vstring_free(orcpt_buf);
1542 
1543     /*
1544      * Don't update the queue file when we did not write a recipient record
1545      * (malformed or duplicate BCC recipient).
1546      */
1547     if (vstream_ftell(state->dst) == new_rcpt_offset)
1548 	return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, 0));
1549 
1550     /*
1551      * Follow the recipient with a "reverse" pointer to the old recipient
1552      * append target.
1553      */
1554     if ((reverse_ptr_offset = vstream_ftell(state->dst)) < 0) {
1555 	msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path);
1556 	return (cleanup_milter_error(state, errno));
1557     }
1558     cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
1559 		       (long) state->append_rcpt_pt_target);
1560 
1561     /*
1562      * Pointer flipping: update the old "recipient append" pointer record
1563      * value to the location of the new recipient record.
1564      */
1565     if (vstream_fseek(state->dst, state->append_rcpt_pt_offset, SEEK_SET) < 0) {
1566 	msg_warn("%s: seek file %s: %m", myname, cleanup_path);
1567 	return (cleanup_milter_error(state, errno));
1568     }
1569     cleanup_out_format(state, REC_TYPE_PTR, REC_TYPE_PTR_FORMAT,
1570 		       (long) new_rcpt_offset);
1571 
1572     /*
1573      * Update the in-memory "recipient append" pointer record location with
1574      * the location of the reverse pointer record that follows the new
1575      * recipient. The target of the "recipient append" pointer record does
1576      * not change; it's always the record that follows the dummy pointer
1577      * record that was written while Postfix received the message.
1578      */
1579     state->append_rcpt_pt_offset = reverse_ptr_offset;
1580 
1581     /*
1582      * In case of error while doing record output.
1583      */
1584     return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, 0));
1585 }
1586 
1587 /* cleanup_add_rcpt - append recipient address */
1588 
1589 static const char *cleanup_add_rcpt(void *context, const char *ext_rcpt)
1590 {
1591     return (cleanup_add_rcpt_par(context, ext_rcpt, ""));
1592 }
1593 
1594 /* cleanup_del_rcpt - remove recipient and all its expansions */
1595 
1596 static const char *cleanup_del_rcpt(void *context, const char *ext_rcpt)
1597 {
1598     const char *myname = "cleanup_del_rcpt";
1599     CLEANUP_STATE *state = (CLEANUP_STATE *) context;
1600     off_t   curr_offset;
1601     VSTRING *buf;
1602     char   *attr_name;
1603     char   *attr_value;
1604     char   *dsn_orcpt = 0;		/* XXX for dup filter cleanup */
1605     int     dsn_notify = 0;		/* XXX for dup filter cleanup */
1606     char   *orig_rcpt = 0;
1607     char   *start;
1608     int     rec_type;
1609     int     junk;
1610     int     count = 0;
1611     TOK822 *tree;
1612     TOK822 *tp;
1613     VSTRING *int_rcpt_buf;
1614     int     addr_count;
1615 
1616     if (msg_verbose)
1617 	msg_info("%s: \"%s\"", myname, ext_rcpt);
1618 
1619     /*
1620      * Virtual aliasing and other address rewriting happens after the mail
1621      * filter sees the envelope address. Therefore we must delete all
1622      * recipient records whose Postfix (not DSN) original recipient address
1623      * matches the specified address.
1624      *
1625      * As the number of recipients may be very large we can't do an efficient
1626      * two-pass implementation (collect record offsets first, then mark
1627      * records as deleted). Instead we mark records as soon as we find them.
1628      * This is less efficient because we do (seek-write-read) for each marked
1629      * recipient, instead of (seek-write). It's unlikely that VSTREAMs will
1630      * be made smart enough to eliminate unnecessary I/O with small seeks.
1631      *
1632      * XXX When Postfix original recipients are turned off, we have no option
1633      * but to match against the expanded and rewritten recipient address.
1634      *
1635      * XXX Remove the (dsn_orcpt, dsn_notify, orcpt, recip) tuple from the
1636      * duplicate recipient filter. This requires that we maintain reference
1637      * counts.
1638      */
1639     if (vstream_fseek(state->dst, 0L, SEEK_SET) < 0) {
1640 	msg_warn("%s: seek file %s: %m", myname, cleanup_path);
1641 	return (cleanup_milter_error(state, errno));
1642     }
1643 #define CLEANUP_DEL_RCPT_RETURN(ret) do { \
1644 	if (orig_rcpt != 0)	\
1645 	    myfree(orig_rcpt); \
1646 	if (dsn_orcpt != 0) \
1647 	    myfree(dsn_orcpt); \
1648 	vstring_free(buf); \
1649 	vstring_free(int_rcpt_buf); \
1650 	return (ret); \
1651     } while (0)
1652 
1653     /*
1654      * Transform recipient from external form to internal form. This also
1655      * removes the enclosing <>, if present.
1656      *
1657      * XXX vstring_alloc() rejects zero-length requests.
1658      */
1659     int_rcpt_buf = vstring_alloc(strlen(ext_rcpt) + 1);
1660     tree = tok822_parse(ext_rcpt);
1661     for (addr_count = 0, tp = tree; tp != 0; tp = tp->next) {
1662 	if (tp->type == TOK822_ADDR) {
1663 	    if (addr_count == 0) {
1664 		tok822_internalize(int_rcpt_buf, tp->head, TOK822_STR_DEFL);
1665 		addr_count += 1;
1666 	    } else {
1667 		msg_warn("%s: Milter request to drop multi-recipient: \"%s\"",
1668 			 state->queue_id, ext_rcpt);
1669 		break;
1670 	    }
1671 	}
1672     }
1673     tok822_free_tree(tree);
1674 
1675     buf = vstring_alloc(100);
1676     for (;;) {
1677 	if (CLEANUP_OUT_OK(state) == 0)
1678 	    /* Warning and errno->error mapping are done elsewhere. */
1679 	    CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, 0));
1680 	if ((curr_offset = vstream_ftell(state->dst)) < 0) {
1681 	    msg_warn("%s: vstream_ftell file %s: %m", myname, cleanup_path);
1682 	    CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno));
1683 	}
1684 	if ((rec_type = rec_get_raw(state->dst, buf, 0, REC_FLAG_NONE)) <= 0) {
1685 	    msg_warn("%s: read file %s: %m", myname, cleanup_path);
1686 	    CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno));
1687 	}
1688 	if (rec_type == REC_TYPE_END)
1689 	    break;
1690 	/* Skip over message content. */
1691 	if (rec_type == REC_TYPE_MESG) {
1692 	    if (vstream_fseek(state->dst, state->xtra_offset, SEEK_SET) < 0) {
1693 		msg_warn("%s: seek file %s: %m", myname, cleanup_path);
1694 		CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno));
1695 	    }
1696 	    continue;
1697 	}
1698 	start = STR(buf);
1699 	if (rec_type == REC_TYPE_PTR) {
1700 	    if (rec_goto(state->dst, start) < 0) {
1701 		msg_warn("%s: seek file %s: %m", myname, cleanup_path);
1702 		CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno));
1703 	    }
1704 	    continue;
1705 	}
1706 	/* Map attribute names to pseudo record type. */
1707 	if (rec_type == REC_TYPE_ATTR) {
1708 	    if (split_nameval(STR(buf), &attr_name, &attr_value) != 0
1709 		|| *attr_value == 0)
1710 		continue;
1711 	    if ((junk = rec_attr_map(attr_name)) != 0) {
1712 		start = attr_value;
1713 		rec_type = junk;
1714 	    }
1715 	}
1716 	switch (rec_type) {
1717 	case REC_TYPE_DSN_ORCPT:		/* RCPT TO ORCPT parameter */
1718 	    if (dsn_orcpt != 0)			/* can't happen */
1719 		myfree(dsn_orcpt);
1720 	    dsn_orcpt = mystrdup(start);
1721 	    break;
1722 	case REC_TYPE_DSN_NOTIFY:		/* RCPT TO NOTIFY parameter */
1723 	    if (alldig(start) && (junk = atoi(start)) > 0
1724 		&& DSN_NOTIFY_OK(junk))
1725 		dsn_notify = junk;
1726 	    else
1727 		dsn_notify = 0;
1728 	    break;
1729 	case REC_TYPE_ORCP:			/* unmodified RCPT TO address */
1730 	    if (orig_rcpt != 0)			/* can't happen */
1731 		myfree(orig_rcpt);
1732 	    orig_rcpt = mystrdup(start);
1733 	    break;
1734 	case REC_TYPE_RCPT:			/* rewritten RCPT TO address */
1735 	    if (strcmp(orig_rcpt ? orig_rcpt : start, STR(int_rcpt_buf)) == 0) {
1736 		if (vstream_fseek(state->dst, curr_offset, SEEK_SET) < 0) {
1737 		    msg_warn("%s: seek file %s: %m", myname, cleanup_path);
1738 		    CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno));
1739 		}
1740 		if (REC_PUT_BUF(state->dst, REC_TYPE_DRCP, buf) < 0) {
1741 		    msg_warn("%s: write queue file: %m", state->queue_id);
1742 		    CLEANUP_DEL_RCPT_RETURN(cleanup_milter_error(state, errno));
1743 		}
1744 		count++;
1745 	    }
1746 	    /* FALLTHROUGH */
1747 	case REC_TYPE_DRCP:			/* canceled recipient */
1748 	case REC_TYPE_DONE:			/* can't happen */
1749 	    if (orig_rcpt != 0) {
1750 		myfree(orig_rcpt);
1751 		orig_rcpt = 0;
1752 	    }
1753 	    if (dsn_orcpt != 0) {
1754 		myfree(dsn_orcpt);
1755 		dsn_orcpt = 0;
1756 	    }
1757 	    dsn_notify = 0;
1758 	    break;
1759 	}
1760     }
1761 
1762     if (msg_verbose)
1763 	msg_info("%s: deleted %d records for recipient \"%s\"",
1764 		 myname, count, ext_rcpt);
1765 
1766     CLEANUP_DEL_RCPT_RETURN(0);
1767 }
1768 
1769 /* cleanup_repl_body - replace message body */
1770 
1771 static const char *cleanup_repl_body(void *context, int cmd, VSTRING *buf)
1772 {
1773     const char *myname = "cleanup_repl_body";
1774     CLEANUP_STATE *state = (CLEANUP_STATE *) context;
1775     static VSTRING empty;
1776 
1777     /*
1778      * XXX Sendmail compatibility: milters don't see the first body line, so
1779      * don't expect they will send one.
1780      */
1781     switch (cmd) {
1782     case MILTER_BODY_LINE:
1783 	if (cleanup_body_edit_write(state, REC_TYPE_NORM, buf) < 0)
1784 	    return (cleanup_milter_error(state, errno));
1785 	break;
1786     case MILTER_BODY_START:
1787 	VSTRING_RESET(&empty);
1788 	if (cleanup_body_edit_start(state) < 0
1789 	    || cleanup_body_edit_write(state, REC_TYPE_NORM, &empty) < 0)
1790 	    return (cleanup_milter_error(state, errno));
1791 	break;
1792     case MILTER_BODY_END:
1793 	if (cleanup_body_edit_finish(state) < 0)
1794 	    return (cleanup_milter_error(state, errno));
1795 	break;
1796     default:
1797 	msg_panic("%s: bad command: %d", myname, cmd);
1798     }
1799     return (CLEANUP_OUT_OK(state) ? 0 : cleanup_milter_error(state, errno));
1800 }
1801 
1802 /* cleanup_milter_eval - expand macro */
1803 
1804 static const char *cleanup_milter_eval(const char *name, void *ptr)
1805 {
1806     CLEANUP_STATE *state = (CLEANUP_STATE *) ptr;
1807 
1808     /*
1809      * Note: if we use XFORWARD attributes here, then consistency requires
1810      * that we forward all Sendmail macros via XFORWARD.
1811      */
1812 
1813     /*
1814      * System macros.
1815      */
1816     if (strcmp(name, S8_MAC_DAEMON_NAME) == 0)
1817 	return (var_milt_daemon_name);
1818     if (strcmp(name, S8_MAC_V) == 0)
1819 	return (var_milt_v);
1820 
1821     /*
1822      * Connect macros.
1823      */
1824 #ifndef CLIENT_ATTR_UNKNOWN
1825 #define CLIENT_ATTR_UNKNOWN "unknown"
1826 #endif
1827 
1828     if (strcmp(name, S8_MAC__) == 0) {
1829 	vstring_sprintf(state->temp1, "%s [%s]",
1830 			state->reverse_name, state->client_addr);
1831 	if (strcasecmp(state->client_name, state->reverse_name) != 0)
1832 	    vstring_strcat(state->temp1, " (may be forged)");
1833 	return (STR(state->temp1));
1834     }
1835     if (strcmp(name, S8_MAC_J) == 0)
1836 	return (var_myhostname);
1837     if (strcmp(name, S8_MAC_CLIENT_ADDR) == 0)
1838 	return (state->client_addr);
1839     if (strcmp(name, S8_MAC_CLIENT_NAME) == 0)
1840 	return (state->client_name);
1841     if (strcmp(name, S8_MAC_CLIENT_PORT) == 0)
1842 	return (state->client_port
1843 		&& strcmp(state->client_port, CLIENT_ATTR_UNKNOWN) ?
1844 		state->client_port : "0");
1845     if (strcmp(name, S8_MAC_CLIENT_PTR) == 0)
1846 	return (state->reverse_name);
1847 
1848     /*
1849      * MAIL FROM macros.
1850      */
1851     if (strcmp(name, S8_MAC_I) == 0)
1852 	return (state->queue_id);
1853 #ifdef USE_SASL_AUTH
1854     if (strcmp(name, S8_MAC_AUTH_TYPE) == 0)
1855 	return (nvtable_find(state->attr, MAIL_ATTR_SASL_METHOD));
1856     if (strcmp(name, S8_MAC_AUTH_AUTHEN) == 0)
1857 	return (nvtable_find(state->attr, MAIL_ATTR_SASL_USERNAME));
1858     if (strcmp(name, S8_MAC_AUTH_AUTHOR) == 0)
1859 	return (nvtable_find(state->attr, MAIL_ATTR_SASL_SENDER));
1860 #endif
1861     if (strcmp(name, S8_MAC_MAIL_ADDR) == 0)
1862 	return (state->milter_ext_from ? STR(state->milter_ext_from) : 0);
1863 
1864     /*
1865      * RCPT TO macros.
1866      */
1867     if (strcmp(name, S8_MAC_RCPT_ADDR) == 0)
1868 	return (state->milter_ext_rcpt ? STR(state->milter_ext_rcpt) : 0);
1869     return (0);
1870 }
1871 
1872 /* cleanup_milter_receive - receive milter instances */
1873 
1874 void    cleanup_milter_receive(CLEANUP_STATE *state, int count)
1875 {
1876     if (state->milters)
1877 	milter_free(state->milters);
1878     state->milters = milter_receive(state->src, count);
1879     if (state->milters == 0)
1880 	msg_fatal("cleanup_milter_receive: milter receive failed");
1881     if (count <= 0)
1882 	return;
1883     milter_macro_callback(state->milters, cleanup_milter_eval, (void *) state);
1884     milter_edit_callback(state->milters,
1885 			 cleanup_add_header, cleanup_upd_header,
1886 			 cleanup_ins_header, cleanup_del_header,
1887 			 cleanup_chg_from, cleanup_add_rcpt,
1888 			 cleanup_add_rcpt_par, cleanup_del_rcpt,
1889 			 cleanup_repl_body, (void *) state);
1890 }
1891 
1892 /* cleanup_milter_apply - apply Milter reponse, non-zero if rejecting */
1893 
1894 static const char *cleanup_milter_apply(CLEANUP_STATE *state, const char *event,
1895 					        const char *resp)
1896 {
1897     const char *myname = "cleanup_milter_apply";
1898     const char *action;
1899     const char *text;
1900     const char *attr;
1901     const char *ret = 0;
1902 
1903     if (msg_verbose)
1904 	msg_info("%s: %s", myname, resp);
1905 
1906     /*
1907      * Don't process our own milter_header/body checks replies. See comments
1908      * in cleanup_milter_hbc_extend().
1909      */
1910     if (state->milter_hbc_reply &&
1911 	strcmp(resp, STR(state->milter_hbc_reply)) == 0)
1912 	return (0);
1913 
1914     /*
1915      * Don't process Milter replies that are redundant because header/body
1916      * checks already decided that we will not receive the message; or Milter
1917      * replies that would have conflicting effect with the outcome of
1918      * header/body checks (for example, header_checks "discard" action
1919      * followed by Milter "reject" reply). Logging both actions would look
1920      * silly.
1921      */
1922     if (CLEANUP_MILTER_REJECTING_OR_DISCARDING_MESSAGE(state)) {
1923 	if (msg_verbose)
1924 	    msg_info("%s: ignoring redundant or conflicting milter reply: %s",
1925 		     state->queue_id, resp);
1926 	return (0);
1927     }
1928 
1929     /*
1930      * Sanity check.
1931      */
1932     if (state->client_name == 0)
1933 	msg_panic("%s: missing client info initialization", myname);
1934 
1935     /*
1936      * We don't report errors that were already reported by the content
1937      * editing call-back routines. See cleanup_milter_error() above.
1938      */
1939     if (CLEANUP_OUT_OK(state) == 0)
1940 	return (0);
1941     switch (resp[0]) {
1942     case 'H':
1943 	/* XXX Should log the reason here. */
1944 	if (state->flags & CLEANUP_FLAG_HOLD)
1945 	    return (0);
1946 	state->flags |= CLEANUP_FLAG_HOLD;
1947 	action = "milter-hold";
1948 	text = "milter triggers HOLD action";
1949 	break;
1950     case 'D':
1951 	state->flags |= CLEANUP_FLAG_DISCARD;
1952 	action = "milter-discard";
1953 	text = "milter triggers DISCARD action";
1954 	break;
1955     case 'S':
1956 	/* XXX Can this happen after end-of-message? */
1957 	state->flags |= CLEANUP_STAT_CONT;
1958 	action = "milter-reject";
1959 	text = cleanup_strerror(CLEANUP_STAT_CONT);
1960 	break;
1961 
1962 	/*
1963 	 * Override permanent reject with temporary reject. This happens when
1964 	 * the cleanup server has to bounce (hard reject) but is unable to
1965 	 * store the message (soft reject). After a temporary reject we stop
1966 	 * inspecting queue file records, so it can't be overruled by
1967 	 * something else.
1968 	 *
1969 	 * CLEANUP_STAT_CONT and CLEANUP_STAT_DEFER both update the reason
1970 	 * attribute, but CLEANUP_STAT_DEFER takes precedence. It terminates
1971 	 * queue record processing, and prevents bounces from being sent.
1972 	 */
1973     case '4':
1974 	CLEANUP_MILTER_SET_SMTP_REPLY(state, resp);
1975 	ret = state->reason;
1976 	state->errs |= CLEANUP_STAT_DEFER;
1977 	action = "milter-reject";
1978 	text = resp + 4;
1979 	break;
1980     case '5':
1981 	CLEANUP_MILTER_SET_SMTP_REPLY(state, resp);
1982 	ret = state->reason;
1983 	state->errs |= CLEANUP_STAT_CONT;
1984 	action = "milter-reject";
1985 	text = resp + 4;
1986 	break;
1987     default:
1988 	msg_panic("%s: unexpected mail filter reply: %s", myname, resp);
1989     }
1990     vstring_sprintf(state->temp1, "%s: %s: %s from %s[%s]: %s;",
1991 		    state->queue_id, action, event, state->client_name,
1992 		    state->client_addr, text);
1993     if (state->sender)
1994 	vstring_sprintf_append(state->temp1, " from=<%s>", state->sender);
1995     if (state->recip)
1996 	vstring_sprintf_append(state->temp1, " to=<%s>", state->recip);
1997     if ((attr = nvtable_find(state->attr, MAIL_ATTR_LOG_PROTO_NAME)) != 0)
1998 	vstring_sprintf_append(state->temp1, " proto=%s", attr);
1999     if ((attr = nvtable_find(state->attr, MAIL_ATTR_LOG_HELO_NAME)) != 0)
2000 	vstring_sprintf_append(state->temp1, " helo=<%s>", attr);
2001     msg_info("%s", vstring_str(state->temp1));
2002 
2003     return (ret);
2004 }
2005 
2006 /* cleanup_milter_client_init - initialize real or ersatz client info */
2007 
2008 static void cleanup_milter_client_init(CLEANUP_STATE *state)
2009 {
2010     const char *proto_attr;
2011 
2012     /*
2013      * Either the cleanup client specifies a name, address and protocol, or
2014      * we have a local submission and pretend localhost/127.0.0.1/AF_INET.
2015      */
2016 #define NO_CLIENT_PORT	"0"
2017 
2018     state->client_name = nvtable_find(state->attr, MAIL_ATTR_ACT_CLIENT_NAME);
2019     state->reverse_name =
2020 	nvtable_find(state->attr, MAIL_ATTR_ACT_REVERSE_CLIENT_NAME);
2021     state->client_addr = nvtable_find(state->attr, MAIL_ATTR_ACT_CLIENT_ADDR);
2022     state->client_port = nvtable_find(state->attr, MAIL_ATTR_ACT_CLIENT_PORT);
2023     proto_attr = nvtable_find(state->attr, MAIL_ATTR_ACT_CLIENT_AF);
2024 
2025     if (state->client_name == 0 || state->client_addr == 0 || proto_attr == 0
2026 	|| !alldig(proto_attr)) {
2027 	state->client_name = "localhost";
2028 	state->client_addr = "127.0.0.1";
2029 	state->client_af = AF_INET;
2030     } else
2031 	state->client_af = atoi(proto_attr);
2032     if (state->reverse_name == 0)
2033 	state->reverse_name = state->client_name;
2034     /* Compatibility with pre-2.5 queue files. */
2035     if (state->client_port == 0)
2036 	state->client_port = NO_CLIENT_PORT;
2037 }
2038 
2039 /* cleanup_milter_inspect - run message through mail filter */
2040 
2041 void    cleanup_milter_inspect(CLEANUP_STATE *state, MILTERS *milters)
2042 {
2043     const char *myname = "cleanup_milter";
2044     const char *resp;
2045 
2046     if (msg_verbose)
2047 	msg_info("enter %s", myname);
2048 
2049     /*
2050      * Initialize, in case we're called via smtpd(8).
2051      */
2052     if (state->client_name == 0)
2053 	cleanup_milter_client_init(state);
2054 
2055     /*
2056      * Prologue: prepare for Milter header/body checks.
2057      */
2058     if (*var_milt_head_checks)
2059 	cleanup_milter_header_checks_init(state);
2060 
2061     /*
2062      * Process mail filter replies. The reply format is verified by the mail
2063      * filter library.
2064      */
2065     if ((resp = milter_message(milters, state->handle->stream,
2066 			       state->data_offset, state->auto_hdrs)) != 0)
2067 	cleanup_milter_apply(state, "END-OF-MESSAGE", resp);
2068 
2069     /*
2070      * Epilogue: finalize Milter header/body checks.
2071      */
2072     if (*var_milt_head_checks)
2073 	cleanup_milter_hbc_finish(state);
2074 
2075     if (msg_verbose)
2076 	msg_info("leave %s", myname);
2077 }
2078 
2079 /* cleanup_milter_emul_mail - emulate connect/ehlo/mail event */
2080 
2081 void    cleanup_milter_emul_mail(CLEANUP_STATE *state,
2082 				         MILTERS *milters,
2083 				         const char *addr)
2084 {
2085     const char *resp;
2086     const char *helo;
2087     const char *argv[2];
2088 
2089     /*
2090      * Per-connection initialization.
2091      */
2092     milter_macro_callback(milters, cleanup_milter_eval, (void *) state);
2093     milter_edit_callback(milters,
2094 			 cleanup_add_header, cleanup_upd_header,
2095 			 cleanup_ins_header, cleanup_del_header,
2096 			 cleanup_chg_from, cleanup_add_rcpt,
2097 			 cleanup_add_rcpt_par, cleanup_del_rcpt,
2098 			 cleanup_repl_body, (void *) state);
2099     if (state->client_name == 0)
2100 	cleanup_milter_client_init(state);
2101 
2102     /*
2103      * Emulate SMTP events.
2104      */
2105     if ((resp = milter_conn_event(milters, state->client_name, state->client_addr,
2106 			      state->client_port, state->client_af)) != 0) {
2107 	cleanup_milter_apply(state, "CONNECT", resp);
2108 	return;
2109     }
2110 #define PRETEND_ESMTP	1
2111 
2112     if (CLEANUP_MILTER_OK(state)) {
2113 	if ((helo = nvtable_find(state->attr, MAIL_ATTR_ACT_HELO_NAME)) == 0)
2114 	    helo = state->client_name;
2115 	if ((resp = milter_helo_event(milters, helo, PRETEND_ESMTP)) != 0) {
2116 	    cleanup_milter_apply(state, "EHLO", resp);
2117 	    return;
2118 	}
2119     }
2120     if (CLEANUP_MILTER_OK(state)) {
2121 	if (state->milter_ext_from == 0)
2122 	    state->milter_ext_from = vstring_alloc(100);
2123 	/* Sendmail 8.13 does not externalize the null address. */
2124 	if (*addr)
2125 	    quote_821_local(state->milter_ext_from, addr);
2126 	else
2127 	    vstring_strcpy(state->milter_ext_from, addr);
2128 	argv[0] = STR(state->milter_ext_from);
2129 	argv[1] = 0;
2130 	if ((resp = milter_mail_event(milters, argv)) != 0) {
2131 	    cleanup_milter_apply(state, "MAIL", resp);
2132 	    return;
2133 	}
2134     }
2135 }
2136 
2137 /* cleanup_milter_emul_rcpt - emulate rcpt event */
2138 
2139 void    cleanup_milter_emul_rcpt(CLEANUP_STATE *state,
2140 				         MILTERS *milters,
2141 				         const char *addr)
2142 {
2143     const char *myname = "cleanup_milter_emul_rcpt";
2144     const char *resp;
2145     const char *argv[2];
2146 
2147     /*
2148      * Sanity check.
2149      */
2150     if (state->client_name == 0)
2151 	msg_panic("%s: missing client info initialization", myname);
2152 
2153     /*
2154      * CLEANUP_STAT_CONT and CLEANUP_STAT_DEFER both update the reason
2155      * attribute, but CLEANUP_STAT_DEFER takes precedence. It terminates
2156      * queue record processing, and prevents bounces from being sent.
2157      */
2158     if (state->milter_ext_rcpt == 0)
2159 	state->milter_ext_rcpt = vstring_alloc(100);
2160     /* Sendmail 8.13 does not externalize the null address. */
2161     if (*addr)
2162 	quote_821_local(state->milter_ext_rcpt, addr);
2163     else
2164 	vstring_strcpy(state->milter_ext_rcpt, addr);
2165     argv[0] = STR(state->milter_ext_rcpt);
2166     argv[1] = 0;
2167     if ((resp = milter_rcpt_event(milters, MILTER_FLAG_NONE, argv)) != 0
2168 	&& cleanup_milter_apply(state, "RCPT", resp) != 0) {
2169 	msg_warn("%s: milter configuration error: can't reject recipient "
2170 		 "in non-smtpd(8) submission", state->queue_id);
2171 	msg_warn("%s: message not accepted, try again later", state->queue_id);
2172 	CLEANUP_MILTER_SET_REASON(state, "4.3.5 Server configuration error");
2173 	state->errs |= CLEANUP_STAT_DEFER;
2174     }
2175 }
2176 
2177 /* cleanup_milter_emul_data - emulate data event */
2178 
2179 void    cleanup_milter_emul_data(CLEANUP_STATE *state, MILTERS *milters)
2180 {
2181     const char *myname = "cleanup_milter_emul_data";
2182     const char *resp;
2183 
2184     /*
2185      * Sanity check.
2186      */
2187     if (state->client_name == 0)
2188 	msg_panic("%s: missing client info initialization", myname);
2189 
2190     if ((resp = milter_data_event(milters)) != 0)
2191 	cleanup_milter_apply(state, "DATA", resp);
2192 }
2193 
2194 #ifdef TEST
2195 
2196  /*
2197   * Queue file editing driver for regression tests. In this case it is OK to
2198   * report fatal errors after I/O errors.
2199   */
2200 #include <stdio.h>
2201 #include <msg_vstream.h>
2202 #include <vstring_vstream.h>
2203 #include <mail_addr.h>
2204 #include <mail_version.h>
2205 
2206 #undef msg_verbose
2207 
2208 char   *cleanup_path;
2209 VSTRING *cleanup_trace_path;
2210 VSTRING *cleanup_strip_chars;
2211 int     cleanup_comm_canon_flags;
2212 MAPS   *cleanup_comm_canon_maps;
2213 int     cleanup_ext_prop_mask;
2214 ARGV   *cleanup_masq_domains;
2215 int     cleanup_masq_flags;
2216 MAPS   *cleanup_rcpt_bcc_maps;
2217 int     cleanup_rcpt_canon_flags;
2218 MAPS   *cleanup_rcpt_canon_maps;
2219 MAPS   *cleanup_send_bcc_maps;
2220 int     cleanup_send_canon_flags;
2221 MAPS   *cleanup_send_canon_maps;
2222 int     var_dup_filter_limit = DEF_DUP_FILTER_LIMIT;
2223 char   *var_empty_addr = DEF_EMPTY_ADDR;
2224 int     var_enable_orcpt = DEF_ENABLE_ORCPT;
2225 MAPS   *cleanup_virt_alias_maps;
2226 char   *var_milt_daemon_name = "host.example.com";
2227 char   *var_milt_v = DEF_MILT_V;
2228 MILTERS *cleanup_milters = (MILTERS *) ((char *) sizeof(*cleanup_milters));
2229 char   *var_milt_head_checks = "";
2230 
2231 /* Dummies to satisfy unused external references. */
2232 
2233 int     cleanup_masquerade_internal(CLEANUP_STATE *state, VSTRING *addr, ARGV *masq_domains)
2234 {
2235     msg_panic("cleanup_masquerade_internal dummy");
2236 }
2237 
2238 int     cleanup_rewrite_internal(const char *context, VSTRING *result,
2239 				         const char *addr)
2240 {
2241     vstring_strcpy(result, addr);
2242     return (0);
2243 }
2244 
2245 int     cleanup_map11_internal(CLEANUP_STATE *state, VSTRING *addr,
2246 			               MAPS *maps, int propagate)
2247 {
2248     msg_panic("cleanup_map11_internal dummy");
2249 }
2250 
2251 ARGV   *cleanup_map1n_internal(CLEANUP_STATE *state, const char *addr,
2252 			               MAPS *maps, int propagate)
2253 {
2254     msg_panic("cleanup_map1n_internal dummy");
2255 }
2256 
2257 void    cleanup_envelope(CLEANUP_STATE *state, int type, const char *buf,
2258 			         ssize_t len)
2259 {
2260     msg_panic("cleanup_envelope dummy");
2261 }
2262 
2263 static void usage(void)
2264 {
2265     msg_warn("usage:");
2266     msg_warn("    verbose on|off");
2267     msg_warn("    open pathname");
2268     msg_warn("    close");
2269     msg_warn("    add_header index name [value]");
2270     msg_warn("    ins_header index name [value]");
2271     msg_warn("    upd_header index name [value]");
2272     msg_warn("    del_header index name");
2273     msg_warn("    chg_from addr parameters");
2274     msg_warn("    add_rcpt addr");
2275     msg_warn("    add_rcpt_par addr parameters");
2276     msg_warn("    del_rcpt addr");
2277     msg_warn("    replbody pathname");
2278     msg_warn("    header_checks type:name");
2279 }
2280 
2281 /* flatten_args - unparse partial command line */
2282 
2283 static void flatten_args(VSTRING *buf, char **argv)
2284 {
2285     char  **cpp;
2286 
2287     VSTRING_RESET(buf);
2288     for (cpp = argv; *cpp; cpp++) {
2289 	vstring_strcat(buf, *cpp);
2290 	if (cpp[1])
2291 	    VSTRING_ADDCH(buf, ' ');
2292     }
2293     VSTRING_TERMINATE(buf);
2294 }
2295 
2296 /* open_queue_file - open an unedited queue file (all-zero dummy PTRs) */
2297 
2298 static void open_queue_file(CLEANUP_STATE *state, const char *path)
2299 {
2300     VSTRING *buf = vstring_alloc(100);
2301     off_t   curr_offset;
2302     int     rec_type;
2303     long    msg_seg_len;
2304     long    data_offset;
2305     long    rcpt_count;
2306     long    qmgr_opts;
2307 
2308     if (state->dst != 0) {
2309 	msg_warn("closing %s", cleanup_path);
2310 	vstream_fclose(state->dst);
2311 	state->dst = 0;
2312 	myfree(cleanup_path);
2313 	cleanup_path = 0;
2314     }
2315     if ((state->dst = vstream_fopen(path, O_RDWR, 0)) == 0) {
2316 	msg_warn("open %s: %m", path);
2317     } else {
2318 	cleanup_path = mystrdup(path);
2319 	for (;;) {
2320 	    if ((curr_offset = vstream_ftell(state->dst)) < 0)
2321 		msg_fatal("file %s: vstream_ftell: %m", cleanup_path);
2322 	    if ((rec_type = rec_get_raw(state->dst, buf, 0, REC_FLAG_NONE)) < 0)
2323 		msg_fatal("file %s: missing SIZE or PTR record", cleanup_path);
2324 	    if (rec_type == REC_TYPE_SIZE) {
2325 		if (sscanf(STR(buf), "%ld %ld %ld %ld",
2326 			   &msg_seg_len, &data_offset,
2327 			   &rcpt_count, &qmgr_opts) != 4)
2328 		    msg_fatal("file %s: bad SIZE record: %s",
2329 			      cleanup_path, STR(buf));
2330 		state->data_offset = data_offset;
2331 		state->xtra_offset = data_offset + msg_seg_len;
2332 	    } else if (rec_type == REC_TYPE_FROM) {
2333 		state->sender_pt_offset = curr_offset;
2334 		if (LEN(buf) < REC_TYPE_PTR_PAYL_SIZE
2335 		    && rec_get_raw(state->dst, buf, 0, REC_FLAG_NONE) != REC_TYPE_PTR)
2336 		    msg_fatal("file %s: missing PTR record after short sender",
2337 			      cleanup_path);
2338 		if ((state->sender_pt_target = vstream_ftell(state->dst)) < 0)
2339 		    msg_fatal("file %s: missing END record", cleanup_path);
2340 	    } else if (rec_type == REC_TYPE_PTR) {
2341 		if (state->data_offset < 0)
2342 		    msg_fatal("file %s: missing SIZE record", cleanup_path);
2343 		if (curr_offset < state->data_offset
2344 		    || curr_offset > state->xtra_offset) {
2345 		    if (state->append_rcpt_pt_offset < 0) {
2346 			state->append_rcpt_pt_offset = curr_offset;
2347 			if (atol(STR(buf)) != 0)
2348 			    msg_fatal("file %s: bad dummy recipient PTR record: %s",
2349 				      cleanup_path, STR(buf));
2350 			if ((state->append_rcpt_pt_target =
2351 			     vstream_ftell(state->dst)) < 0)
2352 			    msg_fatal("file %s: vstream_ftell: %m", cleanup_path);
2353 		    } else if (curr_offset > state->xtra_offset
2354 			       && state->append_meta_pt_offset < 0) {
2355 			state->append_meta_pt_offset = curr_offset;
2356 			if (atol(STR(buf)) != 0)
2357 			    msg_fatal("file %s: bad dummy meta PTR record: %s",
2358 				      cleanup_path, STR(buf));
2359 			if ((state->append_meta_pt_target =
2360 			     vstream_ftell(state->dst)) < 0)
2361 			    msg_fatal("file %s: vstream_ftell: %m", cleanup_path);
2362 		    }
2363 		} else {
2364 		    if (state->append_hdr_pt_offset < 0) {
2365 			state->append_hdr_pt_offset = curr_offset;
2366 			if (atol(STR(buf)) != 0)
2367 			    msg_fatal("file %s: bad dummy header PTR record: %s",
2368 				      cleanup_path, STR(buf));
2369 			if ((state->append_hdr_pt_target =
2370 			     vstream_ftell(state->dst)) < 0)
2371 			    msg_fatal("file %s: vstream_ftell: %m", cleanup_path);
2372 		    }
2373 		}
2374 	    }
2375 	    if (state->append_rcpt_pt_offset > 0
2376 		&& state->append_hdr_pt_offset > 0
2377 		&& (rec_type == REC_TYPE_END
2378 		    || state->append_meta_pt_offset > 0))
2379 		break;
2380 	}
2381 	if (msg_verbose) {
2382 	    msg_info("append_rcpt_pt_offset %ld append_rcpt_pt_target %ld",
2383 		     (long) state->append_rcpt_pt_offset,
2384 		     (long) state->append_rcpt_pt_target);
2385 	    msg_info("append_hdr_pt_offset %ld append_hdr_pt_target %ld",
2386 		     (long) state->append_hdr_pt_offset,
2387 		     (long) state->append_hdr_pt_target);
2388 	}
2389     }
2390     vstring_free(buf);
2391 }
2392 
2393 static void close_queue_file(CLEANUP_STATE *state)
2394 {
2395     (void) vstream_fclose(state->dst);
2396     state->dst = 0;
2397     myfree(cleanup_path);
2398     cleanup_path = 0;
2399 }
2400 
2401 int     main(int unused_argc, char **argv)
2402 {
2403     VSTRING *inbuf = vstring_alloc(100);
2404     VSTRING *arg_buf = vstring_alloc(100);
2405     char   *bufp;
2406     int     istty = isatty(vstream_fileno(VSTREAM_IN));
2407     CLEANUP_STATE *state = cleanup_state_alloc((VSTREAM *) 0);
2408 
2409     state->queue_id = mystrdup("NOQUEUE");
2410     state->sender = mystrdup("sender");
2411     state->recip = mystrdup("recipient");
2412     state->client_name = "client_name";
2413     state->client_addr = "client_addr";
2414     state->flags |= CLEANUP_FLAG_FILTER_ALL;
2415 
2416     msg_vstream_init(argv[0], VSTREAM_ERR);
2417     var_line_limit = DEF_LINE_LIMIT;
2418     var_header_limit = DEF_HEADER_LIMIT;
2419 
2420     for (;;) {
2421 	ARGV   *argv;
2422 	ssize_t index;
2423 	const char *resp = 0;
2424 
2425 	if (istty) {
2426 	    vstream_printf("- ");
2427 	    vstream_fflush(VSTREAM_OUT);
2428 	}
2429 	if (vstring_fgets_nonl(inbuf, VSTREAM_IN) == 0)
2430 	    break;
2431 
2432 	bufp = vstring_str(inbuf);
2433 	if (!istty) {
2434 	    vstream_printf("> %s\n", bufp);
2435 	    vstream_fflush(VSTREAM_OUT);
2436 	}
2437 	if (*bufp == '#' || *bufp == 0 || allspace(bufp))
2438 	    continue;
2439 	argv = argv_split(bufp, " ");
2440 	if (argv->argc == 0) {
2441 	    msg_warn("missing command");
2442 	} else if (strcmp(argv->argv[0], "?") == 0) {
2443 	    usage();
2444 	} else if (strcmp(argv->argv[0], "verbose") == 0) {
2445 	    if (argv->argc != 2) {
2446 		msg_warn("bad verbose argument count: %ld", (long) argv->argc);
2447 	    } else if (strcmp(argv->argv[1], "on") == 0) {
2448 		msg_verbose = 2;
2449 	    } else if (strcmp(argv->argv[1], "off") == 0) {
2450 		msg_verbose = 0;
2451 	    } else {
2452 		msg_warn("bad verbose argument");
2453 	    }
2454 	} else if (strcmp(argv->argv[0], "open") == 0) {
2455 	    if (state->dst != 0) {
2456 		msg_info("closing %s", VSTREAM_PATH(state->dst));
2457 		close_queue_file(state);
2458 	    }
2459 	    if (argv->argc != 2) {
2460 		msg_warn("bad open argument count: %ld", (long) argv->argc);
2461 	    } else {
2462 		open_queue_file(state, argv->argv[1]);
2463 	    }
2464 	} else if (state->dst == 0) {
2465 	    msg_warn("no open queue file");
2466 	} else if (strcmp(argv->argv[0], "close") == 0) {
2467 	    if (*var_milt_head_checks) {
2468 		cleanup_milter_hbc_finish(state);
2469 		myfree(var_milt_head_checks);
2470 		var_milt_head_checks = "";
2471 	    }
2472 	    close_queue_file(state);
2473 	} else if (state->milter_hbc_reply && LEN(state->milter_hbc_reply)) {
2474 	    /* Postfix libmilter would skip further requests. */
2475 	    msg_info("ignoring: %s %s %s", argv->argv[0],
2476 		     argv->argc > 1 ? argv->argv[1] : "",
2477 		     argv->argc > 2 ? argv->argv[2] : "");
2478 	} else if (strcmp(argv->argv[0], "add_header") == 0) {
2479 	    if (argv->argc < 2) {
2480 		msg_warn("bad add_header argument count: %ld",
2481 			 (long) argv->argc);
2482 	    } else {
2483 		flatten_args(arg_buf, argv->argv + 2);
2484 		resp = cleanup_add_header(state, argv->argv[1], " ", STR(arg_buf));
2485 	    }
2486 	} else if (strcmp(argv->argv[0], "ins_header") == 0) {
2487 	    if (argv->argc < 3) {
2488 		msg_warn("bad ins_header argument count: %ld",
2489 			 (long) argv->argc);
2490 	    } else if ((index = atoi(argv->argv[1])) < 1) {
2491 		msg_warn("bad ins_header index value");
2492 	    } else {
2493 		flatten_args(arg_buf, argv->argv + 3);
2494 		resp = cleanup_ins_header(state, index, argv->argv[2], " ", STR(arg_buf));
2495 	    }
2496 	} else if (strcmp(argv->argv[0], "upd_header") == 0) {
2497 	    if (argv->argc < 3) {
2498 		msg_warn("bad upd_header argument count: %ld",
2499 			 (long) argv->argc);
2500 	    } else if ((index = atoi(argv->argv[1])) < 1) {
2501 		msg_warn("bad upd_header index value");
2502 	    } else {
2503 		flatten_args(arg_buf, argv->argv + 3);
2504 		resp = cleanup_upd_header(state, index, argv->argv[2], " ", STR(arg_buf));
2505 	    }
2506 	} else if (strcmp(argv->argv[0], "del_header") == 0) {
2507 	    if (argv->argc != 3) {
2508 		msg_warn("bad del_header argument count: %ld",
2509 			 (long) argv->argc);
2510 	    } else if ((index = atoi(argv->argv[1])) < 1) {
2511 		msg_warn("bad del_header index value");
2512 	    } else {
2513 		cleanup_del_header(state, index, argv->argv[2]);
2514 	    }
2515 	} else if (strcmp(argv->argv[0], "chg_from") == 0) {
2516 	    if (argv->argc != 3) {
2517 		msg_warn("bad chg_from argument count: %ld", (long) argv->argc);
2518 	    } else {
2519 		cleanup_chg_from(state, argv->argv[1], argv->argv[2]);
2520 	    }
2521 	} else if (strcmp(argv->argv[0], "add_rcpt") == 0) {
2522 	    if (argv->argc != 2) {
2523 		msg_warn("bad add_rcpt argument count: %ld", (long) argv->argc);
2524 	    } else {
2525 		cleanup_add_rcpt(state, argv->argv[1]);
2526 	    }
2527 	} else if (strcmp(argv->argv[0], "add_rcpt_par") == 0) {
2528 	    if (argv->argc != 3) {
2529 		msg_warn("bad add_rcpt_par argument count: %ld",
2530 			 (long) argv->argc);
2531 	    } else {
2532 		cleanup_add_rcpt_par(state, argv->argv[1], argv->argv[2]);
2533 	    }
2534 	} else if (strcmp(argv->argv[0], "del_rcpt") == 0) {
2535 	    if (argv->argc != 2) {
2536 		msg_warn("bad del_rcpt argument count: %ld", (long) argv->argc);
2537 	    } else {
2538 		cleanup_del_rcpt(state, argv->argv[1]);
2539 	    }
2540 	} else if (strcmp(argv->argv[0], "replbody") == 0) {
2541 	    if (argv->argc != 2) {
2542 		msg_warn("bad replbody argument count: %ld", (long) argv->argc);
2543 	    } else {
2544 		VSTREAM *fp;
2545 		VSTRING *buf;
2546 
2547 		if ((fp = vstream_fopen(argv->argv[1], O_RDONLY, 0)) == 0) {
2548 		    msg_warn("open %s file: %m", argv->argv[1]);
2549 		} else {
2550 		    buf = vstring_alloc(100);
2551 		    cleanup_repl_body(state, MILTER_BODY_START, buf);
2552 		    while (vstring_get_nonl(buf, fp) != VSTREAM_EOF)
2553 			cleanup_repl_body(state, MILTER_BODY_LINE, buf);
2554 		    cleanup_repl_body(state, MILTER_BODY_END, buf);
2555 		    vstring_free(buf);
2556 		    vstream_fclose(fp);
2557 		}
2558 	    }
2559 	} else if (strcmp(argv->argv[0], "header_checks") == 0) {
2560 	    if (argv->argc != 2) {
2561 		msg_warn("bad header_checks argument count: %ld",
2562 			 (long) argv->argc);
2563 	    } else if (*var_milt_head_checks) {
2564 		msg_warn("can't change header checks");
2565 	    } else {
2566 		var_milt_head_checks = mystrdup(argv->argv[1]);
2567 		cleanup_milter_header_checks_init(state);
2568 	    }
2569 	} else if (strcmp(argv->argv[0], "sender_bcc_maps") == 0) {
2570 	    if (argv->argc != 2) {
2571 		msg_warn("bad sender_bcc_maps argument count: %ld",
2572 			 (long) argv->argc);
2573 	    } else {
2574 		if (cleanup_send_bcc_maps)
2575 		    maps_free(cleanup_send_bcc_maps);
2576 		cleanup_send_bcc_maps =
2577 		    maps_create("sender_bcc_maps", argv->argv[1],
2578 				DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX
2579 				| DICT_FLAG_UTF8_REQUEST);
2580 		state->flags |= CLEANUP_FLAG_BCC_OK;
2581 		var_rcpt_delim = "";
2582 	    }
2583 	} else {
2584 	    msg_warn("bad command: %s", argv->argv[0]);
2585 	}
2586 	argv_free(argv);
2587 	if (resp)
2588 	    cleanup_milter_apply(state, "END-OF-MESSAGE", resp);
2589     }
2590     vstring_free(inbuf);
2591     vstring_free(arg_buf);
2592     if (state->append_meta_pt_offset >= 0) {
2593 	if (state->flags)
2594 	    msg_info("flags = %s", cleanup_strflags(state->flags));
2595 	if (state->errs)
2596 	    msg_info("errs = %s", cleanup_strerror(state->errs));
2597     }
2598     cleanup_state_free(state);
2599     if (*var_milt_head_checks)
2600 	myfree(var_milt_head_checks);
2601 
2602     return (0);
2603 }
2604 
2605 #endif
2606