xref: /netbsd-src/external/ibm-public/postfix/dist/src/smtp/smtp_chat.c (revision bdc22b2e01993381dcefeff2bc9b56ca75a4235c)
1 /*	$NetBSD: smtp_chat.c,v 1.2 2017/02/14 01:16:48 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 
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      * We can't parse or store input that exceeds var_line_limit, so we just
262      * skip over it to simplify the remainder of the code below.
263      */
264     VSTRING_RESET(rdata.str_buf);
265     for (;;) {
266 	last_char = smtp_get(session->buffer, session->stream, var_line_limit,
267 			     SMTP_GET_FLAG_SKIP);
268 	/* XXX Update the per-line time limit. */
269 	printable(STR(session->buffer), '?');
270 	if (last_char != '\n')
271 	    msg_warn("%s: response longer than %d: %.30s...",
272 		session->namaddrport, var_line_limit, STR(session->buffer));
273 	if (msg_verbose)
274 	    msg_info("< %s: %.100s", session->namaddrport, STR(session->buffer));
275 
276 	/*
277 	 * Defend against a denial of service attack by limiting the amount
278 	 * of multi-line text that we are willing to store.
279 	 */
280 	chat_append_flag = (LEN(rdata.str_buf) < var_line_limit);
281 	if (chat_append_flag)
282 	    smtp_chat_append(session, "In:  ", STR(session->buffer));
283 	else {
284 	    if (chat_append_skipped == 0)
285 		msg_warn("%s: multi-line response longer than %d %.30s...",
286 		  session->namaddrport, var_line_limit, STR(rdata.str_buf));
287 	    if (chat_append_skipped < INT_MAX)
288 		chat_append_skipped++;
289 	}
290 
291 	/*
292 	 * Server reply substitution, for fault-injection testing, or for
293 	 * working around broken systems. Use with care.
294 	 */
295 	if (smtp_chat_resp_filter != 0) {
296 	    new_reply = dict_get(smtp_chat_resp_filter, STR(session->buffer));
297 	    if (new_reply != 0) {
298 		msg_info("%s: replacing server reply \"%s\" with \"%s\"",
299 		     session->namaddrport, STR(session->buffer), new_reply);
300 		vstring_strcpy(session->buffer, new_reply);
301 		if (chat_append_flag) {
302 		    smtp_chat_append(session, "Replaced-by: ", "");
303 		    smtp_chat_append(session, "     ", new_reply);
304 		}
305 	    } else if (smtp_chat_resp_filter->error != 0) {
306 		msg_warn("%s: table %s:%s lookup error for %s",
307 			 session->state->request->queue_id,
308 			 smtp_chat_resp_filter->type,
309 			 smtp_chat_resp_filter->name,
310 			 printable(STR(session->buffer), '?'));
311 		vstream_longjmp(session->stream, SMTP_ERR_DATA);
312 	    }
313 	}
314 	if (chat_append_flag) {
315 	    if (LEN(rdata.str_buf))
316 		VSTRING_ADDCH(rdata.str_buf, '\n');
317 	    vstring_strcat(rdata.str_buf, STR(session->buffer));
318 	}
319 
320 	/*
321 	 * Parse into code and text. Do not ignore garbage (see below).
322 	 */
323 	for (cp = STR(session->buffer); *cp && ISDIGIT(*cp); cp++)
324 	     /* void */ ;
325 	if ((three_digs = (cp - STR(session->buffer) == 3)) != 0) {
326 	    if (*cp == '-')
327 		continue;
328 	    if (*cp == ' ' || *cp == 0)
329 		break;
330 	}
331 
332 	/*
333 	 * XXX Do not simply ignore garbage in the server reply when ESMTP
334 	 * command pipelining is turned on.  For example, after sending
335 	 * ".<CR><LF>QUIT<CR><LF>" and receiving garbage followed by a
336 	 * legitimate 2XX reply, Postfix recognizes the server's QUIT reply
337 	 * as the END-OF-DATA reply after garbage, causing mail to be lost.
338 	 *
339 	 * Without the ability to store per-domain status information in queue
340 	 * files, automatic workarounds are problematic:
341 	 *
342 	 * - Automatically deferring delivery creates a "repeated delivery"
343 	 * problem when garbage arrives after the DATA stage. Without the
344 	 * workaround, Postfix delivers only once.
345 	 *
346 	 * - Automatically deferring delivery creates a "no delivery" problem
347 	 * when the garbage arrives before the DATA stage. Without the
348 	 * workaround, mail might still get through.
349 	 *
350 	 * - Automatically turning off pipelining for delayed mail affects
351 	 * deliveries to correctly implemented servers, and may also affect
352 	 * delivery of large mailing lists.
353 	 *
354 	 * So we leave the decision with the administrator, but we don't force
355 	 * them to take action, like we would with automatic deferral.  If
356 	 * loss of mail is not acceptable then they can turn off pipelining
357 	 * for specific sites, or they can turn off pipelining globally when
358 	 * they find that there are just too many broken sites.
359 	 */
360 	session->error_mask |= MAIL_ERROR_PROTOCOL;
361 	if (session->features & SMTP_FEATURE_PIPELINING) {
362 	    msg_warn("%s: non-%s response from %s: %.100s",
363 		     session->state->request->queue_id,
364 		     smtp_mode ? "ESMTP" : "LMTP",
365 		     session->namaddrport, STR(session->buffer));
366 	    if (var_helpful_warnings)
367 		msg_warn("to prevent loss of mail, turn off command pipelining "
368 			 "for %s with the %s parameter",
369 			 STR(session->iterator->addr),
370 			 VAR_LMTP_SMTP(EHLO_DIS_MAPS));
371 	}
372     }
373 
374     /*
375      * Extract RFC 821 reply code and RFC 2034 detail. Use a default detail
376      * code if none was given.
377      *
378      * Ignore out-of-protocol enhanced status codes: codes that accompany 3XX
379      * replies, or codes whose initial digit is out of sync with the reply
380      * code.
381      *
382      * XXX Potential stability problem. In order to save memory, the queue
383      * manager stores DSNs in a compact manner:
384      *
385      * - empty strings are represented by null pointers,
386      *
387      * - the status and reason are required to be non-empty.
388      *
389      * Other Postfix daemons inherit this behavior, because they use the same
390      * DSN support code. This means that everything that receives DSNs must
391      * cope with null pointers for the optional DSN attributes, and that
392      * everything that provides DSN information must provide a non-empty
393      * status and reason, otherwise the DSN support code wil panic().
394      *
395      * Thus, when the remote server sends a malformed reply (or 3XX out of
396      * context) we should not panic() in DSN_COPY() just because we don't
397      * have a status. Robustness suggests that we supply a status here, and
398      * that we leave it up to the down-stream code to override the
399      * server-supplied status in case of an error we can't detect here, such
400      * as an out-of-order server reply.
401      */
402     VSTRING_TERMINATE(rdata.str_buf);
403     vstring_strcpy(rdata.dsn_buf, "5.5.0");	/* SAFETY! protocol error */
404     if (three_digs != 0) {
405 	rdata.code = atoi(STR(session->buffer));
406 	if (strchr("245", STR(session->buffer)[0]) != 0) {
407 	    for (cp = STR(session->buffer) + 4; *cp == ' '; cp++)
408 		 /* void */ ;
409 	    if ((len = dsn_valid(cp)) > 0 && *cp == *STR(session->buffer)) {
410 		vstring_strncpy(rdata.dsn_buf, cp, len);
411 	    } else {
412 		vstring_strcpy(rdata.dsn_buf, "0.0.0");
413 		STR(rdata.dsn_buf)[0] = STR(session->buffer)[0];
414 	    }
415 	}
416     } else {
417 	rdata.code = 0;
418     }
419     rdata.dsn = STR(rdata.dsn_buf);
420     rdata.str = STR(rdata.str_buf);
421     return (&rdata);
422 }
423 
424 /* print_line - line_wrap callback */
425 
426 static void print_line(const char *str, int len, int indent, void *context)
427 {
428     VSTREAM *notice = (VSTREAM *) context;
429 
430     post_mail_fprintf(notice, " %*s%.*s", indent, "", len, str);
431 }
432 
433 /* smtp_chat_notify - notify postmaster */
434 
435 void    smtp_chat_notify(SMTP_SESSION *session)
436 {
437     const char *myname = "smtp_chat_notify";
438     VSTREAM *notice;
439     char  **cpp;
440 
441     /*
442      * Sanity checks.
443      */
444     if (session->history == 0)
445 	msg_panic("%s: no conversation history", myname);
446     if (msg_verbose)
447 	msg_info("%s: notify postmaster", myname);
448 
449     /*
450      * Construct a message for the postmaster, explaining what this is all
451      * about. This is junk mail: don't send it when the mail posting service
452      * is unavailable, and use the double bounce sender address, to prevent
453      * mail bounce wars. Always prepend one space to message content that we
454      * generate from untrusted data.
455      */
456 #define NULL_TRACE_FLAGS	0
457 #define NO_QUEUE_ID		((VSTRING *) 0)
458 #define LENGTH	78
459 #define INDENT	4
460 
461     notice = post_mail_fopen_nowait(mail_addr_double_bounce(),
462 				    var_error_rcpt,
463 				    MAIL_SRC_MASK_NOTIFY, NULL_TRACE_FLAGS,
464 				    SMTPUTF8_FLAG_NONE, NO_QUEUE_ID);
465     if (notice == 0) {
466 	msg_warn("postmaster notify: %m");
467 	return;
468     }
469     post_mail_fprintf(notice, "From: %s (Mail Delivery System)",
470 		      mail_addr_mail_daemon());
471     post_mail_fprintf(notice, "To: %s (Postmaster)", var_error_rcpt);
472     post_mail_fprintf(notice, "Subject: %s %s client: errors from %s",
473 		      var_mail_name, smtp_mode ? "SMTP" : "LMTP",
474 		      session->namaddrport);
475     post_mail_fputs(notice, "");
476     post_mail_fprintf(notice, "Unexpected response from %s.",
477 		      session->namaddrport);
478     post_mail_fputs(notice, "");
479     post_mail_fputs(notice, "Transcript of session follows.");
480     post_mail_fputs(notice, "");
481     argv_terminate(session->history);
482     for (cpp = session->history->argv; *cpp; cpp++)
483 	line_wrap(printable(*cpp, '?'), LENGTH, INDENT, print_line,
484 		  (void *) notice);
485     post_mail_fputs(notice, "");
486     post_mail_fprintf(notice, "For other details, see the local mail logfile");
487     (void) post_mail_fclose(notice);
488 }
489