xref: /netbsd-src/external/ibm-public/postfix/dist/src/smtp/smtp_chat.c (revision 67b9b338a7386232ac596b5fd0cd5a9cc8a03c71)
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