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