1 /* $NetBSD: smtp_chat.c,v 1.1.1.2 2010/06/17 18:07:03 tron Exp $ */ 2 3 /*++ 4 /* NAME 5 /* smtp_chat 3 6 /* SUMMARY 7 /* SMTP client request/response support 8 /* SYNOPSIS 9 /* #include "smtp.h" 10 /* 11 /* typedef struct { 12 /* .in +4 13 /* int code; /* SMTP code, not sanitized */ 14 /* char *dsn; /* enhanced status, sanitized */ 15 /* char *str; /* unmodified SMTP reply */ 16 /* VSTRING *dsn_buf; 17 /* VSTRING *str_buf; 18 /* .in -4 19 /* } SMTP_RESP; 20 /* 21 /* void smtp_chat_cmd(session, format, ...) 22 /* SMTP_SESSION *session; 23 /* const char *format; 24 /* 25 /* DICT *smtp_chat_resp_filter; 26 /* 27 /* SMTP_RESP *smtp_chat_resp(session) 28 /* SMTP_SESSION *session; 29 /* 30 /* void smtp_chat_notify(session) 31 /* SMTP_SESSION *session; 32 /* 33 /* void smtp_chat_init(session) 34 /* SMTP_SESSION *session; 35 /* 36 /* void smtp_chat_reset(session) 37 /* SMTP_SESSION *session; 38 /* DESCRIPTION 39 /* This module implements SMTP client support for request/reply 40 /* conversations, and maintains a limited SMTP transaction log. 41 /* 42 /* smtp_chat_cmd() formats a command and sends it to an SMTP server. 43 /* Optionally, the command is logged. 44 /* 45 /* smtp_chat_resp() reads one SMTP server response. It extracts 46 /* the SMTP reply code and enhanced status code from the text, 47 /* and concatenates multi-line responses to one string, using 48 /* a newline as separator. Optionally, the server response 49 /* is logged. 50 /* .IP \(bu 51 /* Postfix never sanitizes the extracted SMTP reply code except 52 /* to ensure that it is a three-digit code. A malformed reply 53 /* results in a null extracted SMTP reply code value. 54 /* .IP \(bu 55 /* Postfix always sanitizes the extracted enhanced status code. 56 /* When the server's SMTP status code is 2xx, 4xx or 5xx, 57 /* Postfix requires that the first digit of the server's 58 /* enhanced status code matches the first digit of the server's 59 /* SMTP status code. In case of a mis-match, or when the 60 /* server specified no status code, the extracted enhanced 61 /* status code is set to 2.0.0, 4.0.0 or 5.0.0 instead. With 62 /* SMTP reply codes other than 2xx, 4xx or 5xx, the extracted 63 /* enhanced status code is set to a default value of 5.5.0 64 /* (protocol error) for reasons outlined under the next bullet. 65 /* .IP \(bu 66 /* Since the SMTP reply code may violate the protocol even 67 /* when it is correctly formatted, Postfix uses the sanitized 68 /* extracted enhanced status code to decide whether an error 69 /* condition is permanent or transient. This means that the 70 /* caller may have to update the enhanced status code when it 71 /* discovers that a server reply violates the SMTP protocol, 72 /* even though it was correctly formatted. This happens when 73 /* the client and server get out of step due to a broken proxy 74 /* agent. 75 /* .PP 76 /* smtp_chat_resp_filter specifies an optional filter to 77 /* transform one server reply line before it is parsed. The 78 /* filter is invoked once for each line of a multi-line reply. 79 /* 80 /* smtp_chat_notify() sends a copy of the SMTP transaction log 81 /* to the postmaster for review. The postmaster notice is sent only 82 /* when delivery is possible immediately. It is an error to call 83 /* smtp_chat_notify() when no SMTP transaction log exists. 84 /* 85 /* smtp_chat_init() initializes the per-session transaction log. 86 /* This must be done at the beginning of a new SMTP session. 87 /* 88 /* smtp_chat_reset() resets the transaction log. This is 89 /* typically done at the beginning or end of an SMTP session, 90 /* or within a session to discard non-error information. 91 /* DIAGNOSTICS 92 /* Fatal errors: memory allocation problem, server response exceeds 93 /* configurable limit. 94 /* All other exceptions are handled by long jumps (see smtp_stream(3)). 95 /* SEE ALSO 96 /* smtp_stream(3) SMTP session I/O support 97 /* msg(3) generic logging interface 98 /* LICENSE 99 /* .ad 100 /* .fi 101 /* The Secure Mailer license must be distributed with this software. 102 /* AUTHOR(S) 103 /* Wietse Venema 104 /* IBM T.J. Watson Research 105 /* P.O. Box 704 106 /* Yorktown Heights, NY 10598, USA 107 /*--*/ 108 109 /* System library. */ 110 111 #include <sys_defs.h> 112 #include <stdlib.h> /* 44BSD stdarg.h uses abort() */ 113 #include <stdarg.h> 114 #include <ctype.h> 115 #include <stdlib.h> 116 #include <setjmp.h> 117 #include <string.h> 118 #include <limits.h> 119 120 /* Utility library. */ 121 122 #include <msg.h> 123 #include <vstring.h> 124 #include <vstream.h> 125 #include <argv.h> 126 #include <stringops.h> 127 #include <line_wrap.h> 128 #include <mymalloc.h> 129 130 /* Global library. */ 131 132 #include <recipient_list.h> 133 #include <deliver_request.h> 134 #include <smtp_stream.h> 135 #include <mail_params.h> 136 #include <mail_addr.h> 137 #include <post_mail.h> 138 #include <mail_error.h> 139 #include <dsn_util.h> 140 141 /* Application-specific. */ 142 143 #include "smtp.h" 144 145 /* 146 * Server reply transformations. 147 */ 148 DICT *smtp_chat_resp_filter; 149 150 /* smtp_chat_init - initialize SMTP transaction log */ 151 152 void smtp_chat_init(SMTP_SESSION *session) 153 { 154 session->history = 0; 155 } 156 157 /* smtp_chat_reset - reset SMTP transaction log */ 158 159 void smtp_chat_reset(SMTP_SESSION *session) 160 { 161 if (session->history) { 162 argv_free(session->history); 163 session->history = 0; 164 } 165 } 166 167 /* smtp_chat_append - append record to SMTP transaction log */ 168 169 static void smtp_chat_append(SMTP_SESSION *session, const char *direction, 170 const char *data) 171 { 172 char *line; 173 174 if (session->history == 0) 175 session->history = argv_alloc(10); 176 line = concatenate(direction, data, (char *) 0); 177 argv_add(session->history, line, (char *) 0); 178 myfree(line); 179 } 180 181 /* smtp_chat_cmd - send an SMTP command */ 182 183 void smtp_chat_cmd(SMTP_SESSION *session, const char *fmt,...) 184 { 185 va_list ap; 186 187 /* 188 * Format the command, and update the transaction log. 189 */ 190 va_start(ap, fmt); 191 vstring_vsprintf(session->buffer, fmt, ap); 192 va_end(ap); 193 smtp_chat_append(session, "Out: ", STR(session->buffer)); 194 195 /* 196 * Optionally log the command first, so we can see in the log what the 197 * program is trying to do. 198 */ 199 if (msg_verbose) 200 msg_info("> %s: %s", session->namaddrport, STR(session->buffer)); 201 202 /* 203 * Send the command to the SMTP server. 204 */ 205 smtp_fputs(STR(session->buffer), LEN(session->buffer), session->stream); 206 207 /* 208 * Force flushing of output does not belong here. It is done in the 209 * smtp_loop() main protocol loop when reading the server response, and 210 * in smtp_helo() when reading the EHLO response after sending the EHLO 211 * command. 212 * 213 * If we do forced flush here, then we must longjmp() on error, and a 214 * matching "prepare for disaster" error handler must be set up before 215 * every smtp_chat_cmd() call. 216 */ 217 #if 0 218 219 /* 220 * Flush unsent data to avoid timeouts after slow DNS lookups. 221 */ 222 if (time((time_t *) 0) - vstream_ftime(session->stream) > 10) 223 vstream_fflush(session->stream); 224 225 /* 226 * Abort immediately if the connection is broken. 227 */ 228 if (vstream_ftimeout(session->stream)) 229 vstream_longjmp(session->stream, SMTP_ERR_TIME); 230 if (vstream_ferror(session->stream)) 231 vstream_longjmp(session->stream, SMTP_ERR_EOF); 232 #endif 233 } 234 235 /* smtp_chat_resp - read and process SMTP server response */ 236 237 SMTP_RESP *smtp_chat_resp(SMTP_SESSION *session) 238 { 239 static SMTP_RESP rdata; 240 char *cp; 241 int last_char; 242 int three_digs = 0; 243 size_t len; 244 const char *new_reply; 245 int chat_append_flag; 246 int chat_append_skipped = 0; 247 248 /* 249 * Initialize the response data buffer. 250 */ 251 if (rdata.str_buf == 0) { 252 rdata.dsn_buf = vstring_alloc(10); 253 rdata.str_buf = vstring_alloc(100); 254 } 255 256 /* 257 * Censor out non-printable characters in server responses. Concatenate 258 * multi-line server responses. Separate the status code from the text. 259 * Leave further parsing up to the application. 260 */ 261 VSTRING_RESET(rdata.str_buf); 262 for (;;) { 263 last_char = smtp_get(session->buffer, session->stream, var_line_limit); 264 printable(STR(session->buffer), '?'); 265 if (last_char != '\n') 266 msg_warn("%s: response longer than %d: %.30s...", 267 session->namaddrport, var_line_limit, STR(session->buffer)); 268 if (msg_verbose) 269 msg_info("< %s: %.100s", session->namaddrport, STR(session->buffer)); 270 271 /* 272 * Defend against a denial of service attack by limiting the amount 273 * of multi-line text that we are willing to store. 274 */ 275 chat_append_flag = (LEN(rdata.str_buf) < var_line_limit); 276 if (chat_append_flag) 277 smtp_chat_append(session, "In: ", STR(session->buffer)); 278 else { 279 if (chat_append_skipped == 0) 280 msg_warn("%s: multi-line response longer than %d %.30s...", 281 session->namaddrport, var_line_limit, STR(rdata.str_buf)); 282 if (chat_append_skipped < INT_MAX) 283 chat_append_skipped++; 284 } 285 286 /* 287 * Server reply substitution, for fault-injection testing, or for 288 * working around broken systems. Use with care. 289 */ 290 if (smtp_chat_resp_filter != 0) { 291 new_reply = dict_get(smtp_chat_resp_filter, STR(session->buffer)); 292 if (new_reply != 0) { 293 msg_info("%s: replacing server reply \"%s\" with \"%s\"", 294 session->namaddrport, STR(session->buffer), new_reply); 295 vstring_strcpy(session->buffer, new_reply); 296 if (chat_append_flag) { 297 smtp_chat_append(session, "Replaced-by: ", ""); 298 smtp_chat_append(session, " ", new_reply); 299 } 300 } 301 } 302 if (chat_append_flag) { 303 if (LEN(rdata.str_buf)) 304 VSTRING_ADDCH(rdata.str_buf, '\n'); 305 vstring_strcat(rdata.str_buf, STR(session->buffer)); 306 } 307 308 /* 309 * Parse into code and text. Do not ignore garbage (see below). 310 */ 311 for (cp = STR(session->buffer); *cp && ISDIGIT(*cp); cp++) 312 /* void */ ; 313 if ((three_digs = (cp - STR(session->buffer) == 3)) != 0) { 314 if (*cp == '-') 315 continue; 316 if (*cp == ' ' || *cp == 0) 317 break; 318 } 319 320 /* 321 * XXX Do not simply ignore garbage in the server reply when ESMTP 322 * command pipelining is turned on. For example, after sending 323 * ".<CR><LF>QUIT<CR><LF>" and receiving garbage followed by a 324 * legitimate 2XX reply, Postfix recognizes the server's QUIT reply 325 * as the END-OF-DATA reply after garbage, causing mail to be lost. 326 * 327 * Without the ability to store per-domain status information in queue 328 * files, automatic workarounds are problematic: 329 * 330 * - Automatically deferring delivery creates a "repeated delivery" 331 * problem when garbage arrives after the DATA stage. Without the 332 * workaround, Postfix delivers only once. 333 * 334 * - Automatically deferring delivery creates a "no delivery" problem 335 * when the garbage arrives before the DATA stage. Without the 336 * workaround, mail might still get through. 337 * 338 * - Automatically turning off pipelining for delayed mail affects 339 * deliveries to correctly implemented servers, and may also affect 340 * delivery of large mailing lists. 341 * 342 * So we leave the decision with the administrator, but we don't force 343 * them to take action, like we would with automatic deferral. If 344 * loss of mail is not acceptable then they can turn off pipelining 345 * for specific sites, or they can turn off pipelining globally when 346 * they find that there are just too many broken sites. 347 */ 348 session->error_mask |= MAIL_ERROR_PROTOCOL; 349 if (session->features & SMTP_FEATURE_PIPELINING) { 350 msg_warn("%s: non-%s response from %s: %.100s", 351 session->state->request->queue_id, 352 (session->state->misc_flags & SMTP_MISC_FLAG_USE_LMTP) ? 353 "LMTP" : "ESMTP", session->namaddrport, 354 STR(session->buffer)); 355 if (var_helpful_warnings) 356 msg_warn("to prevent loss of mail, turn off command pipelining " 357 "for %s with the %s parameter", session->addr, 358 (session->state->misc_flags & SMTP_MISC_FLAG_USE_LMTP) ? 359 VAR_LMTP_EHLO_DIS_MAPS : VAR_SMTP_EHLO_DIS_MAPS); 360 } 361 } 362 363 /* 364 * Extract RFC 821 reply code and RFC 2034 detail. Use a default detail 365 * code if none was given. 366 * 367 * Ignore out-of-protocol enhanced status codes: codes that accompany 3XX 368 * replies, or codes whose initial digit is out of sync with the reply 369 * code. 370 * 371 * XXX Potential stability problem. In order to save memory, the queue 372 * manager stores DSNs in a compact manner: 373 * 374 * - empty strings are represented by null pointers, 375 * 376 * - the status and reason are required to be non-empty. 377 * 378 * Other Postfix daemons inherit this behavior, because they use the same 379 * DSN support code. This means that everything that receives DSNs must 380 * cope with null pointers for the optional DSN attributes, and that 381 * everything that provides DSN information must provide a non-empty 382 * status and reason, otherwise the DSN support code wil panic(). 383 * 384 * Thus, when the remote server sends a malformed reply (or 3XX out of 385 * context) we should not panic() in DSN_COPY() just because we don't 386 * have a status. Robustness suggests that we supply a status here, and 387 * that we leave it up to the down-stream code to override the 388 * server-supplied status in case of an error we can't detect here, such 389 * as an out-of-order server reply. 390 */ 391 VSTRING_TERMINATE(rdata.str_buf); 392 vstring_strcpy(rdata.dsn_buf, "5.5.0"); /* SAFETY! protocol error */ 393 if (three_digs != 0) { 394 rdata.code = atoi(STR(session->buffer)); 395 if (strchr("245", STR(session->buffer)[0]) != 0) { 396 for (cp = STR(session->buffer) + 4; *cp == ' '; cp++) 397 /* void */ ; 398 if ((len = dsn_valid(cp)) > 0 && *cp == *STR(session->buffer)) { 399 vstring_strncpy(rdata.dsn_buf, cp, len); 400 } else { 401 vstring_strcpy(rdata.dsn_buf, "0.0.0"); 402 STR(rdata.dsn_buf)[0] = STR(session->buffer)[0]; 403 } 404 } 405 } else { 406 rdata.code = 0; 407 } 408 rdata.dsn = STR(rdata.dsn_buf); 409 rdata.str = STR(rdata.str_buf); 410 return (&rdata); 411 } 412 413 /* print_line - line_wrap callback */ 414 415 static void print_line(const char *str, int len, int indent, char *context) 416 { 417 VSTREAM *notice = (VSTREAM *) context; 418 419 post_mail_fprintf(notice, " %*s%.*s", indent, "", len, str); 420 } 421 422 /* smtp_chat_notify - notify postmaster */ 423 424 void smtp_chat_notify(SMTP_SESSION *session) 425 { 426 const char *myname = "smtp_chat_notify"; 427 VSTREAM *notice; 428 char **cpp; 429 430 /* 431 * Sanity checks. 432 */ 433 if (session->history == 0) 434 msg_panic("%s: no conversation history", myname); 435 if (msg_verbose) 436 msg_info("%s: notify postmaster", myname); 437 438 /* 439 * Construct a message for the postmaster, explaining what this is all 440 * about. This is junk mail: don't send it when the mail posting service 441 * is unavailable, and use the double bounce sender address, to prevent 442 * mail bounce wars. Always prepend one space to message content that we 443 * generate from untrusted data. 444 */ 445 #define NULL_TRACE_FLAGS 0 446 #define NO_QUEUE_ID ((VSTRING *) 0) 447 #define LENGTH 78 448 #define INDENT 4 449 450 notice = post_mail_fopen_nowait(mail_addr_double_bounce(), 451 var_error_rcpt, 452 INT_FILT_MASK_NOTIFY, 453 NULL_TRACE_FLAGS, NO_QUEUE_ID); 454 if (notice == 0) { 455 msg_warn("postmaster notify: %m"); 456 return; 457 } 458 post_mail_fprintf(notice, "From: %s (Mail Delivery System)", 459 mail_addr_mail_daemon()); 460 post_mail_fprintf(notice, "To: %s (Postmaster)", var_error_rcpt); 461 post_mail_fprintf(notice, "Subject: %s %s client: errors from %s", 462 var_mail_name, 463 (session->state->misc_flags & 464 SMTP_MISC_FLAG_USE_LMTP) ? "LMTP" : "SMTP", 465 session->namaddrport); 466 post_mail_fputs(notice, ""); 467 post_mail_fprintf(notice, "Unexpected response from %s.", 468 session->namaddrport); 469 post_mail_fputs(notice, ""); 470 post_mail_fputs(notice, "Transcript of session follows."); 471 post_mail_fputs(notice, ""); 472 argv_terminate(session->history); 473 for (cpp = session->history->argv; *cpp; cpp++) 474 line_wrap(printable(*cpp, '?'), LENGTH, INDENT, print_line, 475 (char *) notice); 476 post_mail_fputs(notice, ""); 477 post_mail_fprintf(notice, "For other details, see the local mail logfile"); 478 (void) post_mail_fclose(notice); 479 } 480