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