Home | History | Annotate | Line # | Download | only in smtpd
      1 /*	$NetBSD: smtpd_chat.c,v 1.5 2026/05/09 18:49:20 christos Exp $	*/
      2 
      3 /*++
      4 /* NAME
      5 /*	smtpd_chat 3
      6 /* SUMMARY
      7 /*	SMTP server request/response support
      8 /* SYNOPSIS
      9 /*	#include <smtpd.h>
     10 /*	#include <smtpd_chat.h>
     11 /*
     12 /*	void	smtpd_chat_pre_jail_init(void)
     13 /*
     14 /*	int	smtpd_chat_query_limit(state, limit)
     15 /*	SMTPD_STATE *state;
     16 /*	int limit;
     17 /*
     18 /*	void	smtpd_chat_query(state)
     19 /*	SMTPD_STATE *state;
     20 /*
     21 /*	void	smtpd_chat_reply(state, format, ...)
     22 /*	SMTPD_STATE *state;
     23 /*	char	*format;
     24 /*
     25 /*	void	smtpd_chat_notify(state)
     26 /*	SMTPD_STATE *state;
     27 /*
     28 /*	void	smtpd_chat_reset(state)
     29 /*	SMTPD_STATE *state;
     30 /* DESCRIPTION
     31 /*	This module implements SMTP server support for request/reply
     32 /*	conversations, and maintains a limited SMTP transaction log.
     33 /*
     34 /*	smtpd_chat_pre_jail_init() performs one-time initialization.
     35 /*
     36 /*	smtpd_chat_query_limit() reads a line from the client that is
     37 /*	at most "limit" bytes long.  A copy is appended to the SMTP
     38 /*	transaction log.  The return value is non-zero for a complete
     39 /*	line or else zero if the length limit was exceeded.
     40 /*
     41 /*	smtpd_chat_query() receives a client request and appends a copy
     42 /*	to the SMTP transaction log.
     43 /*
     44 /*	smtpd_chat_reply() formats a server reply, sends it to the
     45 /*	client, and appends a copy to the SMTP transaction log.
     46 /*	When soft_bounce is enabled, all 5xx (reject) responses are
     47 /*	replaced by 4xx (try again). In case of a 421 reply the
     48 /*	SMTPD_FLAG_HANGUP flag is set for orderly disconnect.
     49 /*
     50 /*	smtpd_chat_notify() sends a copy of the SMTP transaction log
     51 /*	to the postmaster for review. The postmaster notice is sent only
     52 /*	when delivery is possible immediately. It is an error to call
     53 /*	smtpd_chat_notify() when no SMTP transaction log exists.
     54 /*
     55 /*	smtpd_chat_reset() resets the transaction log. This is
     56 /*	typically done at the beginning of an SMTP session, or
     57 /*	within a session to discard non-error information.
     58 /* DIAGNOSTICS
     59 /*	Panic: interface violations. Fatal errors: out of memory.
     60 /*	internal protocol errors.
     61 /* LICENSE
     62 /* .ad
     63 /* .fi
     64 /*	The Secure Mailer license must be distributed with this software.
     65 /* AUTHOR(S)
     66 /*	Wietse Venema
     67 /*	IBM T.J. Watson Research
     68 /*	P.O. Box 704
     69 /*	Yorktown Heights, NY 10598, USA
     70 /*
     71 /*	Wietse Venema
     72 /*	Google, Inc.
     73 /*	111 8th Avenue
     74 /*	New York, NY 10011, USA
     75 /*--*/
     76 
     77 /* System library. */
     78 
     79 #include <sys_defs.h>
     80 #include <setjmp.h>
     81 #include <unistd.h>
     82 #include <time.h>
     83 #include <stdlib.h>			/* 44BSD stdarg.h uses abort() */
     84 #include <stdarg.h>
     85 
     86 /* Utility library. */
     87 
     88 #include <msg.h>
     89 #include <argv.h>
     90 #include <vstring.h>
     91 #include <vstream.h>
     92 #include <stringops.h>
     93 #include <line_wrap.h>
     94 #include <mymalloc.h>
     95 
     96 /* Global library. */
     97 
     98 #include <smtp_stream.h>
     99 #include <record.h>
    100 #include <rec_type.h>
    101 #include <mail_proto.h>
    102 #include <mail_params.h>
    103 #include <mail_addr.h>
    104 #include <maps.h>
    105 #include <post_mail.h>
    106 #include <mail_error.h>
    107 #include <smtp_reply_footer.h>
    108 #include <hfrom_format.h>
    109 
    110 /* Application-specific. */
    111 
    112 #include "smtpd.h"
    113 #include "smtpd_expand.h"
    114 #include "smtpd_chat.h"
    115 
    116  /*
    117   * Reject filter and footer maps.
    118   */
    119 static MAPS *smtpd_reject_filter_maps;
    120 static MAPS *smtpd_rej_ftr_maps;
    121 
    122 #define STR	vstring_str
    123 #define LEN	VSTRING_LEN
    124 
    125 /* smtpd_chat_pre_jail_init - initialize */
    126 
    127 void    smtpd_chat_pre_jail_init(void)
    128 {
    129     static int init_count = 0;
    130 
    131     if (init_count++ != 0)
    132 	msg_panic("smtpd_chat_pre_jail_init: multiple calls");
    133 
    134     /*
    135      * SMTP server reject filter.
    136      */
    137     if (*var_smtpd_reject_filter_maps)
    138 	smtpd_reject_filter_maps = maps_create(VAR_SMTPD_REJECT_FILTER_MAPS,
    139 					       var_smtpd_reject_filter_maps,
    140 					       DICT_FLAG_LOCK);
    141 
    142     /*
    143      * SMTP server reject footer.
    144      */
    145     if (*var_smtpd_rej_ftr_maps)
    146 	smtpd_rej_ftr_maps = maps_create(VAR_SMTPD_REJ_FTR_MAPS,
    147 					 var_smtpd_rej_ftr_maps,
    148 					 DICT_FLAG_LOCK);
    149 }
    150 
    151 /* smtp_chat_reset - reset SMTP transaction log */
    152 
    153 void    smtpd_chat_reset(SMTPD_STATE *state)
    154 {
    155     if (state->history) {
    156 	argv_free(state->history);
    157 	state->history = 0;
    158     }
    159 }
    160 
    161 /* smtp_chat_append - append record to SMTP transaction log */
    162 
    163 static void smtp_chat_append(SMTPD_STATE *state, char *direction,
    164 			             const char *text)
    165 {
    166     char   *line;
    167 
    168     if (state->notify_mask == 0)
    169 	return;
    170 
    171     if (state->history == 0)
    172 	state->history = argv_alloc(10);
    173     line = concatenate(direction, text, (char *) 0);
    174     argv_add(state->history, line, (char *) 0);
    175     myfree(line);
    176 }
    177 
    178 /* smtpd_chat_query - receive and record an SMTP request */
    179 
    180 int     smtpd_chat_query_limit(SMTPD_STATE *state, int limit)
    181 {
    182     int     last_char;
    183 
    184     /*
    185      * We can't parse or store input that exceeds var_line_limit, so we skip
    186      * over it to avoid loss of synchronization.
    187      */
    188     last_char = smtp_get(state->buffer, state->client, limit,
    189 			 SMTP_GET_FLAG_SKIP);
    190     smtp_chat_append(state, "In:  ", STR(state->buffer));
    191     if (last_char != '\n')
    192 	msg_warn("%s: request longer than %d: %.30s...",
    193 		 state->namaddr, limit,
    194 		 printable(STR(state->buffer), '?'));
    195 
    196     if (msg_verbose)
    197 	msg_info("< %s: %s", state->namaddr, STR(state->buffer));
    198     return (last_char == '\n');
    199 }
    200 
    201 /* smtpd_chat_reply - format, send and record an SMTP response */
    202 
    203 void    smtpd_chat_reply(SMTPD_STATE *state, const char *format,...)
    204 {
    205     va_list ap;
    206 
    207     va_start(ap, format);
    208     vsmtpd_chat_reply(state, format, ap);
    209     va_end(ap);
    210 }
    211 
    212 /* vsmtpd_chat_reply - format, send and record an SMTP response */
    213 
    214 void    vsmtpd_chat_reply(SMTPD_STATE *state, const char *format, va_list ap)
    215 {
    216     int     delay = 0;
    217     char   *cp;
    218     char   *next;
    219     char   *end;
    220     const char *alt_reply;
    221     const char *footer;
    222 
    223     /*
    224      * Slow down clients that make errors. Sleep-on-anything slows down
    225      * clients that make an excessive number of errors within a session.
    226      */
    227     if (state->error_count >= var_smtpd_soft_erlim)
    228 	sleep(delay = var_smtpd_err_sleep);
    229 
    230     /*
    231      * Postfix generates single-line reject responses, but Milters may
    232      * generate multi-line rejects with the SMFIR_REPLYCODE request.
    233      */
    234     vstring_vsprintf(state->buffer, format, ap);
    235     cp = STR(state->buffer);
    236     if ((*cp == '4' || *cp == '5')
    237 	&& smtpd_reject_filter_maps != 0
    238 	&& (alt_reply = maps_find(smtpd_reject_filter_maps, cp, 0)) != 0) {
    239 	const char *queue_id = state->queue_id ? state->queue_id : "NOQUEUE";
    240 
    241 	/* XXX Enforce this for each line of a multi-line reply. */
    242 	if ((alt_reply[0] != '4' && alt_reply[0] != '5')
    243 	    || !ISDIGIT(alt_reply[1]) || !ISDIGIT(alt_reply[2])
    244 	    || (alt_reply[3] != ' ' && alt_reply[3] != '-')
    245 	    || (ISDIGIT(alt_reply[4]) && (alt_reply[4] != alt_reply[0]))) {
    246 	    msg_warn("%s: ignoring invalid reject filter result: %s",
    247 		     queue_id, alt_reply);
    248 	} else {
    249 	    msg_info("%s: reply filter in: %s", queue_id, cp);
    250 	    msg_info("%s: reply filter out: %s", queue_id, alt_reply);
    251 	    vstring_strcpy(state->buffer, alt_reply);
    252 	}
    253     }
    254     if ((*(cp = STR(state->buffer)) == '4' || *cp == '5')
    255 	&& ((smtpd_rej_ftr_maps != 0
    256 	     && (footer = maps_find(smtpd_rej_ftr_maps, cp, 0)) != 0)
    257 	    || *(footer = var_smtpd_rej_footer) != 0))
    258 	smtp_reply_footer(state->buffer, 0, footer, STR(smtpd_expand_filter),
    259 			  smtpd_expand_lookup, (void *) state);
    260 
    261     /* All 5xx replies must have a 5.xx.xx detail code. */
    262     for (cp = STR(state->buffer), end = cp + strlen(STR(state->buffer));;) {
    263 	if (var_soft_bounce) {
    264 	    if (cp[0] == '5') {
    265 		cp[0] = '4';
    266 		if (cp[4] == '5')
    267 		    cp[4] = '4';
    268 	    }
    269 	}
    270 	/* This is why we use strlen() above instead of VSTRING_LEN(). */
    271 	if ((next = strstr(cp, "\r\n")) != 0) {
    272 	    *next = 0;
    273 	    if (next[2] != 0)
    274 		cp[3] = '-';			/* contact footer kludge */
    275 	    else
    276 		next = end;			/* strip trailing \r\n */
    277 	} else {
    278 	    next = end;
    279 	}
    280 	smtp_chat_append(state, "Out: ", cp);
    281 
    282 	if (msg_verbose)
    283 	    msg_info("> %s: %s", state->namaddr, cp);
    284 
    285 	smtp_fputs(cp, next - cp, state->client);
    286 	if (next < end)
    287 	    cp = next + 2;
    288 	else
    289 	    break;
    290     }
    291 
    292     /*
    293      * Flush unsent output if no I/O happened for a while. This avoids
    294      * timeouts with pipelined SMTP sessions that have lots of server-side
    295      * delays (tarpit delays or DNS lookups for UCE restrictions).
    296      */
    297     if (delay || time((time_t *) 0) - vstream_ftime(state->client) > 10)
    298 	vstream_fflush(state->client);
    299 
    300     /*
    301      * Abort immediately if the connection is broken.
    302      */
    303     if (vstream_ftimeout(state->client))
    304 	vstream_longjmp(state->client, SMTP_ERR_TIME);
    305     if (vstream_ferror(state->client))
    306 	vstream_longjmp(state->client, SMTP_ERR_EOF);
    307 
    308     /*
    309      * Orderly disconnect in case of 421 or 521 reply.
    310      */
    311     if (strncmp(STR(state->buffer), "421", 3) == 0
    312 	|| strncmp(STR(state->buffer), "521", 3) == 0)
    313 	state->flags |= SMTPD_FLAG_HANGUP;
    314 }
    315 
    316 /* print_line - line_wrap callback */
    317 
    318 static void print_line(const char *str, int len, int indent, void *context)
    319 {
    320     VSTREAM *notice = (VSTREAM *) context;
    321 
    322     post_mail_fprintf(notice, " %*s%.*s", indent, "", len, str);
    323 }
    324 
    325 /* smtpd_chat_notify - notify postmaster */
    326 
    327 void    smtpd_chat_notify(SMTPD_STATE *state)
    328 {
    329     const char *myname = "smtpd_chat_notify";
    330     VSTREAM *notice;
    331     char  **cpp;
    332 
    333     /*
    334      * Sanity checks.
    335      */
    336     if (state->history == 0)
    337 	msg_panic("%s: no conversation history", myname);
    338     if (msg_verbose)
    339 	msg_info("%s: notify postmaster", myname);
    340 
    341     /*
    342      * Construct a message for the postmaster, explaining what this is all
    343      * about. This is junk mail: don't send it when the mail posting service
    344      * is unavailable, and use the double bounce sender address to prevent
    345      * mail bounce wars. Always prepend one space to message content that we
    346      * generate from untrusted data.
    347      */
    348 #define NULL_TRACE_FLAGS	0
    349 #define NO_QUEUE_ID		((VSTRING *) 0)
    350 #define LENGTH	78
    351 #define INDENT	4
    352 
    353     notice = post_mail_fopen_nowait(mail_addr_double_bounce(),
    354 				    (state->error_mask & MAIL_ERROR_BOUNCE) ?
    355 				    var_bounce_rcpt : var_error_rcpt,
    356 				    MAIL_SRC_MASK_NOTIFY, NULL_TRACE_FLAGS,
    357 				    SMTPUTF8_FLAG_NONE, NO_QUEUE_ID);
    358     if (notice == 0) {
    359 	msg_warn("postmaster notify: %m");
    360 	return;
    361     }
    362     if (smtpd_hfrom_format == HFROM_FORMAT_CODE_STD) {
    363 	post_mail_fprintf(notice, "From: Mail Delivery System <%s>",
    364 			  mail_addr_mail_daemon());
    365 	post_mail_fprintf(notice, "To: Postmaster <%s>", var_error_rcpt);
    366     } else {
    367 	post_mail_fprintf(notice, "From: %s (Mail Delivery System)",
    368 			  mail_addr_mail_daemon());
    369 	post_mail_fprintf(notice, "To: %s (Postmaster)", var_error_rcpt);
    370     }
    371     post_mail_fprintf(notice, "Subject: %s SMTP server: errors from %s",
    372 		      var_mail_name, state->namaddr);
    373     post_mail_fputs(notice, "");
    374     post_mail_fputs(notice, "Transcript of session follows.");
    375     post_mail_fputs(notice, "");
    376     argv_terminate(state->history);
    377     for (cpp = state->history->argv; *cpp; cpp++)
    378 	line_wrap(printable(*cpp, '?'), LENGTH, INDENT, print_line,
    379 		  (void *) notice);
    380     post_mail_fputs(notice, "");
    381     if (state->reason)
    382 	post_mail_fprintf(notice, "Session aborted, reason: %s", state->reason);
    383     post_mail_fputs(notice, "");
    384     post_mail_fprintf(notice, "For other details, see the local mail logfile");
    385     (void) post_mail_fclose(notice);
    386 }
    387