1 /* $NetBSD: smtp_chat.c,v 1.4 2022/10/08 16:12:49 christos 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 /* Wietse Venema
109 /* Google, Inc.
110 /* 111 8th Avenue
111 /* New York, NY 10011, USA
112 /*--*/
113
114 /* System library. */
115
116 #include <sys_defs.h>
117 #include <stdlib.h> /* 44BSD stdarg.h uses abort() */
118 #include <stdarg.h>
119 #include <ctype.h>
120 #include <stdlib.h>
121 #include <setjmp.h>
122 #include <string.h>
123 #include <limits.h>
124
125 /* Utility library. */
126
127 #include <msg.h>
128 #include <vstring.h>
129 #include <vstream.h>
130 #include <argv.h>
131 #include <stringops.h>
132 #include <line_wrap.h>
133 #include <mymalloc.h>
134
135 /* Global library. */
136
137 #include <recipient_list.h>
138 #include <deliver_request.h>
139 #include <smtp_stream.h>
140 #include <mail_params.h>
141 #include <mail_addr.h>
142 #include <post_mail.h>
143 #include <mail_error.h>
144 #include <dsn_util.h>
145 #include <hfrom_format.h>
146
147 /* Application-specific. */
148
149 #include "smtp.h"
150
151 /*
152 * Server reply transformations.
153 */
154 DICT *smtp_chat_resp_filter;
155
156 /* smtp_chat_init - initialize SMTP transaction log */
157
smtp_chat_init(SMTP_SESSION * session)158 void smtp_chat_init(SMTP_SESSION *session)
159 {
160 session->history = 0;
161 }
162
163 /* smtp_chat_reset - reset SMTP transaction log */
164
smtp_chat_reset(SMTP_SESSION * session)165 void smtp_chat_reset(SMTP_SESSION *session)
166 {
167 if (session->history) {
168 argv_free(session->history);
169 session->history = 0;
170 }
171 }
172
173 /* smtp_chat_append - append record to SMTP transaction log */
174
smtp_chat_append(SMTP_SESSION * session,const char * direction,const char * data)175 static void smtp_chat_append(SMTP_SESSION *session, const char *direction,
176 const char *data)
177 {
178 char *line;
179
180 if (session->history == 0)
181 session->history = argv_alloc(10);
182 line = concatenate(direction, data, (char *) 0);
183 argv_add(session->history, line, (char *) 0);
184 myfree(line);
185 }
186
187 /* smtp_chat_cmd - send an SMTP command */
188
smtp_chat_cmd(SMTP_SESSION * session,const char * fmt,...)189 void smtp_chat_cmd(SMTP_SESSION *session, const char *fmt,...)
190 {
191 va_list ap;
192
193 /*
194 * Format the command, and update the transaction log.
195 */
196 va_start(ap, fmt);
197 vstring_vsprintf(session->buffer, fmt, ap);
198 va_end(ap);
199 smtp_chat_append(session, "Out: ", STR(session->buffer));
200
201 /*
202 * Optionally log the command first, so we can see in the log what the
203 * program is trying to do.
204 */
205 if (msg_verbose)
206 msg_info("> %s: %s", session->namaddrport, STR(session->buffer));
207
208 /*
209 * Send the command to the SMTP server.
210 */
211 smtp_fputs(STR(session->buffer), LEN(session->buffer), session->stream);
212
213 /*
214 * Force flushing of output does not belong here. It is done in the
215 * smtp_loop() main protocol loop when reading the server response, and
216 * in smtp_helo() when reading the EHLO response after sending the EHLO
217 * command.
218 *
219 * If we do forced flush here, then we must longjmp() on error, and a
220 * matching "prepare for disaster" error handler must be set up before
221 * every smtp_chat_cmd() call.
222 */
223 #if 0
224
225 /*
226 * Flush unsent data to avoid timeouts after slow DNS lookups.
227 */
228 if (time((time_t *) 0) - vstream_ftime(session->stream) > 10)
229 vstream_fflush(session->stream);
230
231 /*
232 * Abort immediately if the connection is broken.
233 */
234 if (vstream_ftimeout(session->stream))
235 vstream_longjmp(session->stream, SMTP_ERR_TIME);
236 if (vstream_ferror(session->stream))
237 vstream_longjmp(session->stream, SMTP_ERR_EOF);
238 #endif
239 }
240
241 /* smtp_chat_resp - read and process SMTP server response */
242
smtp_chat_resp(SMTP_SESSION * session)243 SMTP_RESP *smtp_chat_resp(SMTP_SESSION *session)
244 {
245 static SMTP_RESP rdata;
246 char *cp;
247 int last_char;
248 int three_digs = 0;
249 size_t len;
250 const char *new_reply;
251 int chat_append_flag;
252 int chat_append_skipped = 0;
253
254 /*
255 * Initialize the response data buffer.
256 */
257 if (rdata.str_buf == 0) {
258 rdata.dsn_buf = vstring_alloc(10);
259 rdata.str_buf = vstring_alloc(100);
260 }
261
262 /*
263 * Censor out non-printable characters in server responses. Concatenate
264 * multi-line server responses. Separate the status code from the text.
265 * Leave further parsing up to the application.
266 *
267 * We can't parse or store input that exceeds var_line_limit, so we just
268 * skip over it to simplify the remainder of the code below.
269 */
270 VSTRING_RESET(rdata.str_buf);
271 for (;;) {
272 last_char = smtp_get(session->buffer, session->stream, var_line_limit,
273 SMTP_GET_FLAG_SKIP);
274 /* XXX Update the per-line time limit. */
275 printable(STR(session->buffer), '?');
276 if (last_char != '\n')
277 msg_warn("%s: response longer than %d: %.30s...",
278 session->namaddrport, var_line_limit, STR(session->buffer));
279 if (msg_verbose)
280 msg_info("< %s: %.100s", session->namaddrport, STR(session->buffer));
281
282 /*
283 * Defend against a denial of service attack by limiting the amount
284 * of multi-line text that we are willing to store.
285 */
286 chat_append_flag = (LEN(rdata.str_buf) < var_line_limit);
287 if (chat_append_flag)
288 smtp_chat_append(session, "In: ", STR(session->buffer));
289 else {
290 if (chat_append_skipped == 0)
291 msg_warn("%s: multi-line response longer than %d %.30s...",
292 session->namaddrport, var_line_limit, STR(rdata.str_buf));
293 if (chat_append_skipped < INT_MAX)
294 chat_append_skipped++;
295 }
296
297 /*
298 * Server reply substitution, for fault-injection testing, or for
299 * working around broken systems. Use with care.
300 */
301 if (smtp_chat_resp_filter != 0) {
302 new_reply = dict_get(smtp_chat_resp_filter, STR(session->buffer));
303 if (new_reply != 0) {
304 msg_info("%s: replacing server reply \"%s\" with \"%s\"",
305 session->namaddrport, STR(session->buffer), new_reply);
306 vstring_strcpy(session->buffer, new_reply);
307 if (chat_append_flag) {
308 smtp_chat_append(session, "Replaced-by: ", "");
309 smtp_chat_append(session, " ", new_reply);
310 }
311 } else if (smtp_chat_resp_filter->error != 0) {
312 msg_warn("%s: table %s:%s lookup error for %s",
313 session->state->request->queue_id,
314 smtp_chat_resp_filter->type,
315 smtp_chat_resp_filter->name,
316 printable(STR(session->buffer), '?'));
317 vstream_longjmp(session->stream, SMTP_ERR_DATA);
318 }
319 }
320 if (chat_append_flag) {
321 if (LEN(rdata.str_buf))
322 VSTRING_ADDCH(rdata.str_buf, '\n');
323 vstring_strcat(rdata.str_buf, STR(session->buffer));
324 }
325
326 /*
327 * Parse into code and text. Do not ignore garbage (see below).
328 */
329 for (cp = STR(session->buffer); *cp && ISDIGIT(*cp); cp++)
330 /* void */ ;
331 if ((three_digs = (cp - STR(session->buffer) == 3)) != 0) {
332 if (*cp == '-')
333 continue;
334 if (*cp == ' ' || *cp == 0)
335 break;
336 }
337
338 /*
339 * XXX Do not simply ignore garbage in the server reply when ESMTP
340 * command pipelining is turned on. For example, after sending
341 * ".<CR><LF>QUIT<CR><LF>" and receiving garbage followed by a
342 * legitimate 2XX reply, Postfix recognizes the server's QUIT reply
343 * as the END-OF-DATA reply after garbage, causing mail to be lost.
344 *
345 * Without the ability to store per-domain status information in queue
346 * files, automatic workarounds are problematic:
347 *
348 * - Automatically deferring delivery creates a "repeated delivery"
349 * problem when garbage arrives after the DATA stage. Without the
350 * workaround, Postfix delivers only once.
351 *
352 * - Automatically deferring delivery creates a "no delivery" problem
353 * when the garbage arrives before the DATA stage. Without the
354 * workaround, mail might still get through.
355 *
356 * - Automatically turning off pipelining for delayed mail affects
357 * deliveries to correctly implemented servers, and may also affect
358 * delivery of large mailing lists.
359 *
360 * So we leave the decision with the administrator, but we don't force
361 * them to take action, like we would with automatic deferral. If
362 * loss of mail is not acceptable then they can turn off pipelining
363 * for specific sites, or they can turn off pipelining globally when
364 * they find that there are just too many broken sites.
365 *
366 * Fix 20190621: don't cache an SMTP session after an SMTP protocol
367 * error. The protocol may be in a bad state. Disable caching here so
368 * that the protocol engine will send QUIT.
369 */
370 session->error_mask |= MAIL_ERROR_PROTOCOL;
371 DONT_CACHE_THIS_SESSION;
372 if (session->features & SMTP_FEATURE_PIPELINING) {
373 msg_warn("%s: non-%s response from %s: %.100s",
374 session->state->request->queue_id,
375 smtp_mode ? "ESMTP" : "LMTP",
376 session->namaddrport, STR(session->buffer));
377 if (var_helpful_warnings)
378 msg_warn("to prevent loss of mail, turn off command pipelining "
379 "for %s with the %s parameter",
380 STR(session->iterator->addr),
381 VAR_LMTP_SMTP(EHLO_DIS_MAPS));
382 }
383 }
384
385 /*
386 * Extract RFC 821 reply code and RFC 2034 detail. Use a default detail
387 * code if none was given.
388 *
389 * Ignore out-of-protocol enhanced status codes: codes that accompany 3XX
390 * replies, or codes whose initial digit is out of sync with the reply
391 * code.
392 *
393 * XXX Potential stability problem. In order to save memory, the queue
394 * manager stores DSNs in a compact manner:
395 *
396 * - empty strings are represented by null pointers,
397 *
398 * - the status and reason are required to be non-empty.
399 *
400 * Other Postfix daemons inherit this behavior, because they use the same
401 * DSN support code. This means that everything that receives DSNs must
402 * cope with null pointers for the optional DSN attributes, and that
403 * everything that provides DSN information must provide a non-empty
404 * status and reason, otherwise the DSN support code wil panic().
405 *
406 * Thus, when the remote server sends a malformed reply (or 3XX out of
407 * context) we should not panic() in DSN_COPY() just because we don't
408 * have a status. Robustness suggests that we supply a status here, and
409 * that we leave it up to the down-stream code to override the
410 * server-supplied status in case of an error we can't detect here, such
411 * as an out-of-order server reply.
412 */
413 VSTRING_TERMINATE(rdata.str_buf);
414 vstring_strcpy(rdata.dsn_buf, "5.5.0"); /* SAFETY! protocol error */
415 if (three_digs != 0) {
416 rdata.code = atoi(STR(session->buffer));
417 if (strchr("245", STR(session->buffer)[0]) != 0) {
418 for (cp = STR(session->buffer) + 4; *cp == ' '; cp++)
419 /* void */ ;
420 if ((len = dsn_valid(cp)) > 0 && *cp == *STR(session->buffer)) {
421 vstring_strncpy(rdata.dsn_buf, cp, len);
422 } else {
423 vstring_strcpy(rdata.dsn_buf, "0.0.0");
424 STR(rdata.dsn_buf)[0] = STR(session->buffer)[0];
425 }
426 }
427 } else {
428 rdata.code = 0;
429 }
430 rdata.dsn = STR(rdata.dsn_buf);
431 rdata.str = STR(rdata.str_buf);
432 return (&rdata);
433 }
434
435 /* print_line - line_wrap callback */
436
print_line(const char * str,int len,int indent,void * context)437 static void print_line(const char *str, int len, int indent, void *context)
438 {
439 VSTREAM *notice = (VSTREAM *) context;
440
441 post_mail_fprintf(notice, " %*s%.*s", indent, "", len, str);
442 }
443
444 /* smtp_chat_notify - notify postmaster */
445
smtp_chat_notify(SMTP_SESSION * session)446 void smtp_chat_notify(SMTP_SESSION *session)
447 {
448 const char *myname = "smtp_chat_notify";
449 VSTREAM *notice;
450 char **cpp;
451
452 /*
453 * Sanity checks.
454 */
455 if (session->history == 0)
456 msg_panic("%s: no conversation history", myname);
457 if (msg_verbose)
458 msg_info("%s: notify postmaster", myname);
459
460 /*
461 * Construct a message for the postmaster, explaining what this is all
462 * about. This is junk mail: don't send it when the mail posting service
463 * is unavailable, and use the double bounce sender address, to prevent
464 * mail bounce wars. Always prepend one space to message content that we
465 * generate from untrusted data.
466 */
467 #define NULL_TRACE_FLAGS 0
468 #define NO_QUEUE_ID ((VSTRING *) 0)
469 #define LENGTH 78
470 #define INDENT 4
471
472 notice = post_mail_fopen_nowait(mail_addr_double_bounce(),
473 var_error_rcpt,
474 MAIL_SRC_MASK_NOTIFY, NULL_TRACE_FLAGS,
475 SMTPUTF8_FLAG_NONE, NO_QUEUE_ID);
476 if (notice == 0) {
477 msg_warn("postmaster notify: %m");
478 return;
479 }
480 if (smtp_hfrom_format == HFROM_FORMAT_CODE_STD) {
481 post_mail_fprintf(notice, "From: Mail Delivery System <%s>",
482 mail_addr_mail_daemon());
483 post_mail_fprintf(notice, "To: Postmaster <%s>", var_error_rcpt);
484 } else {
485 post_mail_fprintf(notice, "From: %s (Mail Delivery System)",
486 mail_addr_mail_daemon());
487 post_mail_fprintf(notice, "To: %s (Postmaster)", var_error_rcpt);
488 }
489 post_mail_fprintf(notice, "Subject: %s %s client: errors from %s",
490 var_mail_name, smtp_mode ? "SMTP" : "LMTP",
491 session->namaddrport);
492 post_mail_fputs(notice, "");
493 post_mail_fprintf(notice, "Unexpected response from %s.",
494 session->namaddrport);
495 post_mail_fputs(notice, "");
496 post_mail_fputs(notice, "Transcript of session follows.");
497 post_mail_fputs(notice, "");
498 argv_terminate(session->history);
499 for (cpp = session->history->argv; *cpp; cpp++)
500 line_wrap(printable(*cpp, '?'), LENGTH, INDENT, print_line,
501 (void *) notice);
502 post_mail_fputs(notice, "");
503 post_mail_fprintf(notice, "For other details, see the local mail logfile");
504 (void) post_mail_fclose(notice);
505 }
506