Process Auto-submitted: in the triggering message and add it to
[exim.git] / src / src / smtp_in.c
index 6c27c15deecb651d0c877da5e303d449478ba06f..de7663b1a03dfb33910f4cb3ff06638bfbafc8ad 100644 (file)
@@ -1,10 +1,10 @@
-/* $Cambridge: exim/src/src/smtp_in.c,v 1.42 2006/09/18 14:49:23 ph10 Exp $ */
+/* $Cambridge: exim/src/src/smtp_in.c,v 1.62 2007/09/28 12:21:57 tom Exp $ */
 
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2006 */
+/* Copyright (c) University of Cambridge 1995 - 2007 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Functions for handling an incoming SMTP call. */
@@ -96,6 +96,13 @@ enum {
   TOO_MANY_NONMAIL_CMD };
 
 
+/* This is a convenience macro for adding the identity of an SMTP command
+to the circular buffer that holds a list of the last n received. */
+
+#define HAD(n) \
+    smtp_connection_had[smtp_ch_index++] = n; \
+    if (smtp_ch_index >= SMTP_HBUFF_SIZE) smtp_ch_index = 0
+
 
 /*************************************************
 *                Local static variables          *
@@ -113,12 +120,19 @@ static BOOL helo_seen;
 static BOOL helo_accept_junk;
 static BOOL count_nonmail;
 static BOOL pipelining_advertised;
+static BOOL rcpt_smtp_response_same;
+static BOOL rcpt_in_progress;
 static int  nonmail_command_count;
+static BOOL smtp_exit_function_called = 0;
 static int  synprot_error_count;
 static int  unknown_command_count;
 static int  sync_cmd_limit;
 static int  smtp_write_error = 0;
 
+static uschar *rcpt_smtp_response;
+static uschar *smtp_data_buffer;
+static uschar *smtp_cmd_data;
+
 /* We need to know the position of RSET, HELO, EHLO, AUTH, and STARTTLS. Their
 final fields of all except AUTH are forced TRUE at the start of a new message
 setup, to allow one of each between messages that is not counted as a nonmail
@@ -165,6 +179,15 @@ static smtp_cmd_list *cmd_list_end =
 #define CMD_LIST_AUTH      3
 #define CMD_LIST_STARTTLS  4
 
+/* This list of names is used for performing the smtp_no_mail logging action.
+It must be kept in step with the SCH_xxx enumerations. */
+
+static uschar *smtp_names[] =
+  {
+  US"NONE", US"AUTH", US"DATA", US"EHLO", US"ETRN", US"EXPN", US"HELO",
+  US"HELP", US"MAIL", US"NOOP", US"QUIT", US"RCPT", US"RSET", US"STARTTLS",
+  US"VRFY" };
+
 static uschar *protocols[] = {
   US"local-smtp",        /* HELO */
   US"local-smtps",       /* The rare case EHLO->STARTTLS->HELO */
@@ -311,6 +334,23 @@ return smtp_had_error;
 
 
 
+/*************************************************
+*      Test for characters in the SMTP buffer    *
+*************************************************/
+
+/* Used at the end of a message
+
+Arguments:     none
+Returns:       TRUE/FALSE
+*/
+
+BOOL
+smtp_buffered(void)
+{
+return smtp_inptr < smtp_inend;
+}
+
+
 
 /*************************************************
 *     Write formatted string to SMTP channel     *
@@ -349,28 +389,41 @@ DEBUG(D_receive)
   }
 
 va_start(ap, format);
+if (!string_vformat(big_buffer, big_buffer_size, format, ap))
+  {
+  log_write(0, LOG_MAIN|LOG_PANIC, "string too large in smtp_printf()");
+  smtp_closedown(US"Unexpected error");
+  exim_exit(EXIT_FAILURE);
+  }
+va_end(ap);
 
-/* If in a TLS session we have to format the string, and then write it using a
-TLS function. */
+/* If this is the first output for a (non-batch) RCPT command, see if all RCPTs
+have had the same. Note: this code is also present in smtp_respond(). It would
+be tidier to have it only in one place, but when it was added, it was easier to
+do it that way, so as not to have to mess with the code for the RCPT command,
+which sometimes uses smtp_printf() and sometimes smtp_respond(). */
+
+if (rcpt_in_progress)
+  {
+  if (rcpt_smtp_response == NULL)
+    rcpt_smtp_response = string_copy(big_buffer);
+  else if (rcpt_smtp_response_same &&
+           Ustrcmp(rcpt_smtp_response, big_buffer) != 0)
+    rcpt_smtp_response_same = FALSE;
+  rcpt_in_progress = FALSE;
+  }
+
+/* Now write the string */
 
 #ifdef SUPPORT_TLS
 if (tls_active >= 0)
   {
-  if (!string_vformat(big_buffer, big_buffer_size, format, ap))
-    {
-    log_write(0, LOG_MAIN|LOG_PANIC, "string too large in smtp_printf");
-    smtp_closedown(US"Unexpected error");
-    exim_exit(EXIT_FAILURE);
-    }
   if (tls_write(big_buffer, Ustrlen(big_buffer)) < 0) smtp_write_error = -1;
   }
 else
 #endif
 
-/* Otherwise, just use the standard library function. */
-
-if (vfprintf(smtp_out, format, ap) < 0) smtp_write_error = -1;
-va_end(ap);
+if (fprintf(smtp_out, "%s", big_buffer) < 0) smtp_write_error = -1;
 }
 
 
@@ -418,9 +471,8 @@ log_write(L_lost_incoming_connection,
           host_and_ident(FALSE));
 if (smtp_batched_input)
   moan_smtp_batch(NULL, "421 SMTP command timeout");  /* Does not return */
-smtp_printf("421 %s: SMTP command timeout - closing connection\r\n",
-  smtp_active_hostname);
-mac_smtp_fflush();
+smtp_notquit_exit(US"command-timeout", US"421",
+  US"%s: SMTP command timeout - closing connection", smtp_active_hostname);
 exim_exit(EXIT_FAILURE);
 }
 
@@ -443,13 +495,14 @@ sig = sig;    /* Keep picky compilers happy */
 log_write(0, LOG_MAIN, "%s closed after SIGTERM", smtp_get_connection_info());
 if (smtp_batched_input)
   moan_smtp_batch(NULL, "421 SIGTERM received");  /* Does not return */
-smtp_printf("421 %s: Service not available - closing connection\r\n",
-  smtp_active_hostname);
+smtp_notquit_exit(US"signal-exit", US"421",
+  US"%s: Service not available - closing connection", smtp_active_hostname);
 exim_exit(EXIT_FAILURE);
 }
 
 
 
+
 /*************************************************
 *           Read one command line                *
 *************************************************/
@@ -536,11 +589,16 @@ for (p = cmd_list; p < cmd_list_end; p++)
         !sender_host_notsocket)                        /* Really is a socket */
       return BADSYN_CMD;
 
-    /* Point after the command, but don't skip over leading spaces till after
-    the following test, so that if it fails, the command name can easily be
-    logged. */
+    /* The variables $smtp_command and $smtp_command_argument point into the
+    unmodified input buffer. A copy of the latter is taken for actual
+    processing, so that it can be chopped up into separate parts if necessary,
+    for example, when processing a MAIL command options such as SIZE that can
+    follow the sender address. */
 
     smtp_cmd_argument = smtp_cmd_buffer + p->len;
+    while (isspace(*smtp_cmd_argument)) smtp_cmd_argument++;
+    Ustrcpy(smtp_data_buffer, smtp_cmd_argument);
+    smtp_cmd_data = smtp_data_buffer;
 
     /* Count non-mail commands from those hosts that are controlled in this
     way. The default is all hosts. We don't waste effort checking the list
@@ -558,11 +616,10 @@ for (p = cmd_list; p < cmd_list_end; p++)
         return TOO_MANY_NONMAIL_CMD;
       }
 
-    /* Get the data pointer over leading spaces and return; if there is data
-    for a command that does not expect it, give the error centrally here. */
+    /* If there is data for a command that does not expect it, generate the
+    error here. */
 
-    while (isspace(*smtp_cmd_argument)) smtp_cmd_argument++;
-    return (p->has_arg || *smtp_cmd_argument == 0)? p->cmd : BADARG_CMD;
+    return (p->has_arg || *smtp_cmd_data == 0)? p->cmd : BADARG_CMD;
     }
   }
 
@@ -580,6 +637,60 @@ return OTHER_CMD;
 
 
 
+/*************************************************
+*          Recheck synchronization               *
+*************************************************/
+
+/* Synchronization checks can never be perfect because a packet may be on its
+way but not arrived when the check is done. Such checks can in any case only be
+done when TLS is not in use. Normally, the checks happen when commands are
+read: Exim ensures that there is no more input in the input buffer. In normal
+cases, the response to the command will be fast, and there is no further check.
+
+However, for some commands an ACL is run, and that can include delays. In those
+cases, it is useful to do another check on the input just before sending the
+response. This also applies at the start of a connection. This function does
+that check by means of the select() function, as long as the facility is not
+disabled or inappropriate. A failure of select() is ignored.
+
+When there is unwanted input, we read it so that it appears in the log of the
+error.
+
+Arguments: none
+Returns:   TRUE if all is well; FALSE if there is input pending
+*/
+
+static BOOL
+check_sync(void)
+{
+int fd, rc;
+fd_set fds;
+struct timeval tzero;
+
+if (!smtp_enforce_sync || sender_host_address == NULL ||
+    sender_host_notsocket || tls_active >= 0)
+  return TRUE;
+
+fd = fileno(smtp_in);
+FD_ZERO(&fds);
+FD_SET(fd, &fds);
+tzero.tv_sec = 0;
+tzero.tv_usec = 0;
+rc = select(fd + 1, (SELECT_ARG2_TYPE *)&fds, NULL, NULL, &tzero);
+
+if (rc <= 0) return TRUE;     /* Not ready to read */
+rc = smtp_getc();
+if (rc < 0) return TRUE;      /* End of file or error */
+
+smtp_ungetc(rc);
+rc = smtp_inend - smtp_inptr;
+if (rc > 150) rc = 150;
+smtp_inptr[rc] = 0;
+return FALSE;
+}
+
+
+
 /*************************************************
 *          Forced closedown of call              *
 *************************************************/
@@ -591,7 +702,9 @@ phase, sends the reply string, and gives an error to all subsequent commands
 except QUIT. The existence of an SMTP call is detected by the non-NULLness of
 smtp_in.
 
-Argument:   SMTP reply string to send, excluding the code
+Arguments:
+  message   SMTP reply string to send, excluding the code
+
 Returns:    nothing
 */
 
@@ -666,6 +779,75 @@ return string_sprintf("SMTP connection from %s", hostname);
 
 
 
+/*************************************************
+*      Log lack of MAIL if so configured         *
+*************************************************/
+
+/* This function is called when an SMTP session ends. If the log selector
+smtp_no_mail is set, write a log line giving some details of what has happened
+in the SMTP session.
+
+Arguments:   none
+Returns:     nothing
+*/
+
+void
+smtp_log_no_mail(void)
+{
+int size, ptr, i;
+uschar *s, *sep;
+
+if (smtp_mailcmd_count > 0 || (log_extra_selector & LX_smtp_no_mail) == 0)
+  return;
+
+s = NULL;
+size = ptr = 0;
+
+if (sender_host_authenticated != NULL)
+  {
+  s = string_append(s, &size, &ptr, 2, US" A=", sender_host_authenticated);
+  if (authenticated_id != NULL)
+    s = string_append(s, &size, &ptr, 2, US":", authenticated_id);
+  }
+
+#ifdef SUPPORT_TLS
+if ((log_extra_selector & LX_tls_cipher) != 0 && tls_cipher != NULL)
+  s = string_append(s, &size, &ptr, 2, US" X=", tls_cipher);
+if ((log_extra_selector & LX_tls_certificate_verified) != 0 &&
+     tls_cipher != NULL)
+  s = string_append(s, &size, &ptr, 2, US" CV=",
+    tls_certificate_verified? "yes":"no");
+if ((log_extra_selector & LX_tls_peerdn) != 0 && tls_peerdn != NULL)
+  s = string_append(s, &size, &ptr, 3, US" DN=\"",
+    string_printing(tls_peerdn), US"\"");
+#endif
+
+sep = (smtp_connection_had[SMTP_HBUFF_SIZE-1] != SCH_NONE)?
+  US" C=..." : US" C=";
+for (i = smtp_ch_index; i < SMTP_HBUFF_SIZE; i++)
+  {
+  if (smtp_connection_had[i] != SCH_NONE)
+    {
+    s = string_append(s, &size, &ptr, 2, sep,
+      smtp_names[smtp_connection_had[i]]);
+    sep = US",";
+    }
+  }
+
+for (i = 0; i < smtp_ch_index; i++)
+  {
+  s = string_append(s, &size, &ptr, 2, sep, smtp_names[smtp_connection_had[i]]);
+  sep = US",";
+  }
+
+if (s != NULL) s[ptr] = 0; else s = US"";
+log_write(0, LOG_MAIN, "no MAIL in SMTP connection from %s D=%s%s",
+  host_and_ident(FALSE),
+  readconf_printtime(time(NULL) - smtp_connection_start), s);
+}
+
+
+
 /*************************************************
 *   Check HELO line and set sender_helo_name     *
 *************************************************/
@@ -755,7 +937,7 @@ return yield;
 *         Extract SMTP command option            *
 *************************************************/
 
-/* This function picks the next option setting off the end of smtp_cmd_argument. It
+/* This function picks the next option setting off the end of smtp_cmd_data. It
 is called for MAIL FROM and RCPT TO commands, to pick off the optional ESMTP
 things that can appear there.
 
@@ -770,11 +952,11 @@ static BOOL
 extract_option(uschar **name, uschar **value)
 {
 uschar *n;
-uschar *v = smtp_cmd_argument + Ustrlen(smtp_cmd_argument) -1;
+uschar *v = smtp_cmd_data + Ustrlen(smtp_cmd_data) - 1;
 while (isspace(*v)) v--;
 v[1] = 0;
 
-while (v > smtp_cmd_argument && *v != '=' && !isspace(*v)) v--;
+while (v > smtp_cmd_data && *v != '=' && !isspace(*v)) v--;
 if (*v != '=') return FALSE;
 
 n = v;
@@ -793,8 +975,6 @@ return TRUE;
 
 
 
-
-
 /*************************************************
 *         Reset for new message                  *
 *************************************************/
@@ -809,7 +989,6 @@ Returns:    nothing
 static void
 smtp_reset(void *reset_point)
 {
-int i;
 store_reset(reset_point);
 recipients_list = NULL;
 rcpt_count = rcpt_defer_count = rcpt_fail_count =
@@ -818,6 +997,9 @@ message_linecount = 0;
 message_size = -1;
 acl_added_headers = NULL;
 queue_only_policy = FALSE;
+rcpt_smtp_response = NULL;
+rcpt_smtp_response_same = TRUE;
+rcpt_in_progress = FALSE;
 deliver_freeze = FALSE;                              /* Can be set by ACL */
 freeze_tell = freeze_tell_config;                    /* Can be set by ACL */
 fake_response = OK;                                  /* Can be set by ACL */
@@ -843,6 +1025,9 @@ bmi_verdicts = NULL;
 #ifdef EXPERIMENTAL_DOMAINKEYS
 dk_do_verify = 0;
 #endif
+#ifdef EXPERIMENTAL_DKIM
+dkim_do_verify = 0;
+#endif
 #ifdef EXPERIMENTAL_SPF
 spf_header_comment = NULL;
 spf_received = NULL;
@@ -855,9 +1040,9 @@ sender_rate = sender_rate_limit = sender_rate_period = NULL;
 ratelimiters_mail = NULL;           /* Updated by ratelimit ACL condition */
                    /* Note that ratelimiters_conn persists across resets. */
 
-/* The message variables follow the connection variables. */
+/* Reset message ACL variables */
 
-for (i = 0; i < ACL_MVARS; i++) acl_var[ACL_CVARS + i] = NULL;
+acl_var_m = NULL;
 
 /* The message body variables use malloc store. They may be set if this is
 not the first message in an SMTP session and the previous message caused them
@@ -877,7 +1062,7 @@ if (message_body_end != NULL)
 
 /* Warning log messages are also saved in malloc store. They are saved to avoid
 repetition in the same message, but it seems right to repeat them for different
-messagess. */
+messages. */
 
 while (acl_warn_logged != NULL)
   {
@@ -941,7 +1126,7 @@ while (done <= 0)
     case HELO_CMD:
     case EHLO_CMD:
 
-    check_helo(smtp_cmd_argument);
+    check_helo(smtp_cmd_data);
     /* Fall through */
 
     case RSET_CMD:
@@ -961,7 +1146,7 @@ while (done <= 0)
       /* The function moan_smtp_batch() does not return. */
       moan_smtp_batch(smtp_cmd_buffer, "503 Sender already given");
 
-    if (smtp_cmd_argument[0] == 0)
+    if (smtp_cmd_data[0] == 0)
       /* The function moan_smtp_batch() does not return. */
       moan_smtp_batch(smtp_cmd_buffer, "501 MAIL FROM must have an address operand");
 
@@ -972,8 +1157,8 @@ while (done <= 0)
     /* Apply SMTP rewrite */
 
     raw_sender = ((rewrite_existflags & rewrite_smtp) != 0)?
-      rewrite_one(smtp_cmd_argument, rewrite_smtp|rewrite_smtp_sender, NULL, FALSE,
-        US"", global_rewrite_rules) : smtp_cmd_argument;
+      rewrite_one(smtp_cmd_data, rewrite_smtp|rewrite_smtp_sender, NULL, FALSE,
+        US"", global_rewrite_rules) : smtp_cmd_data;
 
     /* Extract the address; the TRUE flag allows <> as valid */
 
@@ -1016,7 +1201,7 @@ while (done <= 0)
       /* The function moan_smtp_batch() does not return. */
       moan_smtp_batch(smtp_cmd_buffer, "503 No sender yet given");
 
-    if (smtp_cmd_argument[0] == 0)
+    if (smtp_cmd_data[0] == 0)
       /* The function moan_smtp_batch() does not return. */
       moan_smtp_batch(smtp_cmd_buffer, "501 RCPT TO must have an address operand");
 
@@ -1031,8 +1216,8 @@ while (done <= 0)
     recipient address */
 
     recipient = ((rewrite_existflags & rewrite_smtp) != 0)?
-      rewrite_one(smtp_cmd_argument, rewrite_smtp, NULL, FALSE, US"",
-        global_rewrite_rules) : smtp_cmd_argument;
+      rewrite_one(smtp_cmd_data, rewrite_smtp, NULL, FALSE, US"",
+        global_rewrite_rules) : smtp_cmd_data;
 
     /* rfc821_domains = TRUE; << no longer needed */
     recipient = parse_extract_address(recipient, &errmess, &start, &end,
@@ -1144,18 +1329,28 @@ BOOL
 smtp_start_session(void)
 {
 int size = 256;
-int i, ptr;
+int ptr, esclen;
+uschar *user_msg, *log_msg;
+uschar *code, *esc;
 uschar *p, *s, *ss;
 
+smtp_connection_start = time(NULL);
+for (smtp_ch_index = 0; smtp_ch_index < SMTP_HBUFF_SIZE; smtp_ch_index++)
+  smtp_connection_had[smtp_ch_index] = SCH_NONE;
+smtp_ch_index = 0;
+
 /* Default values for certain variables */
 
 helo_seen = esmtp = helo_accept_junk = FALSE;
+smtp_mailcmd_count = 0;
 count_nonmail = TRUE_UNSET;
 synprot_error_count = unknown_command_count = nonmail_command_count = 0;
 smtp_delay_mail = smtp_rlm_base;
 auth_advertised = FALSE;
 pipelining_advertised = FALSE;
+pipelining_enable = TRUE;
 sync_cmd_limit = NON_SYNC_CMD_NON_PIPELINING;
+smtp_exit_function_called = FALSE;    /* For avoiding loop in not-quit exit */
 
 memset(sender_host_cache, 0, sizeof(sender_host_cache));
 
@@ -1172,14 +1367,15 @@ tls_advertised = FALSE;
 
 /* Reset ACL connection variables */
 
-for (i = 0; i < ACL_CVARS; i++) acl_var[i] = NULL;
+acl_var_c = NULL;
 
-/* Allow for trailing 0 in the command buffer. */
+/* Allow for trailing 0 in the command and data buffers. */
 
-smtp_cmd_buffer = (uschar *)malloc(smtp_cmd_buffer_size + 1);
+smtp_cmd_buffer = (uschar *)malloc(2*smtp_cmd_buffer_size + 2);
 if (smtp_cmd_buffer == NULL)
   log_write(0, LOG_MAIN|LOG_PANIC_DIE,
     "malloc() failed for SMTP command buffer");
+smtp_data_buffer = smtp_cmd_buffer + smtp_cmd_buffer_size + 1;
 
 /* For batched input, the protocol setting can be overridden from the
 command line by a trusted caller. */
@@ -1206,6 +1402,7 @@ receive_getc = smtp_getc;
 receive_ungetc = smtp_ungetc;
 receive_feof = smtp_feof;
 receive_ferror = smtp_ferror;
+receive_smtp_buffered = smtp_buffered;
 smtp_inptr = smtp_inend = smtp_inbuffer;
 smtp_had_eof = smtp_had_error = 0;
 
@@ -1451,7 +1648,9 @@ if (!sender_host_unknown)
   smtps port for use with older style SSL MTAs. */
 
   #ifdef SUPPORT_TLS
-  if (tls_on_connect && tls_server_start(tls_require_ciphers) != OK)
+  if (tls_on_connect &&
+      tls_server_start(tls_require_ciphers,
+        gnutls_require_mac, gnutls_require_kx, gnutls_require_proto) != OK)
     return FALSE;
   #endif
 
@@ -1503,18 +1702,18 @@ if (!sender_host_unknown)
     }
   #endif
 
-  /* Check for reserved slots. Note that the count value doesn't include
-  this process, as it gets upped in the parent process. */
+  /* Check for reserved slots. The value of smtp_accept_count has already been
+  incremented to include this process. */
 
   if (smtp_accept_max > 0 &&
-      smtp_accept_count + 1 > smtp_accept_max - smtp_accept_reserve)
+      smtp_accept_count > smtp_accept_max - smtp_accept_reserve)
     {
     if ((rc = verify_check_host(&smtp_reserve_hosts)) != OK)
       {
       log_write(L_connection_reject,
         LOG_MAIN, "temporarily refused connection from %s: not in "
         "reserve list: connected=%d max=%d reserve=%d%s",
-        host_and_ident(FALSE), smtp_accept_count, smtp_accept_max,
+        host_and_ident(FALSE), smtp_accept_count - 1, smtp_accept_max,
         smtp_accept_reserve, (rc == DEFER)? " (lookup deferred)" : "");
       smtp_printf("421 %s: Too many concurrent SMTP connections; "
         "please try again later\r\n", smtp_active_hostname);
@@ -1574,10 +1773,10 @@ if (smtp_batched_input) return TRUE;
 
 /* Run the ACL if it exists */
 
+user_msg = NULL;
 if (acl_smtp_connect != NULL)
   {
   int rc;
-  uschar *user_msg, *log_msg;
   rc = acl_check(ACL_WHERE_CONNECT, NULL, acl_smtp_connect, &user_msg,
     &log_msg);
   if (rc != OK)
@@ -1590,10 +1789,28 @@ if (acl_smtp_connect != NULL)
 /* Output the initial message for a two-way SMTP connection. It may contain
 newlines, which then cause a multi-line response to be given. */
 
-s = expand_string(smtp_banner);
-if (s == NULL)
-  log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Expansion of \"%s\" (smtp_banner) "
-    "failed: %s", smtp_banner, expand_string_message);
+code = US"220";   /* Default status code */
+esc = US"";       /* Default extended status code */
+esclen = 0;       /* Length of esc */
+
+if (user_msg == NULL)
+  {
+  s = expand_string(smtp_banner);
+  if (s == NULL)
+    log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Expansion of \"%s\" (smtp_banner) "
+      "failed: %s", smtp_banner, expand_string_message);
+  }
+else
+  {
+  int codelen = 3;
+  s = user_msg;
+  smtp_message_code(&code, &codelen, &s, NULL);
+  if (codelen > 4)
+    {
+    esc = code + 4;
+    esclen = codelen - 4;
+    }
+  }
 
 /* Remove any terminating newlines; might as well remove trailing space too */
 
@@ -1618,16 +1835,18 @@ do       /* At least once, in case we have an empty string */
   {
   int len;
   uschar *linebreak = Ustrchr(p, '\n');
+  ss = string_cat(ss, &size, &ptr, code, 3);
   if (linebreak == NULL)
     {
     len = Ustrlen(p);
-    ss = string_cat(ss, &size, &ptr, US"220 ", 4);
+    ss = string_cat(ss, &size, &ptr, US" ", 1);
     }
   else
     {
     len = linebreak - p;
-    ss = string_cat(ss, &size, &ptr, US"220-", 4);
+    ss = string_cat(ss, &size, &ptr, US"-", 1);
     }
+  ss = string_cat(ss, &size, &ptr, esc, esclen);
   ss = string_cat(ss, &size, &ptr, p, len);
   ss = string_cat(ss, &size, &ptr, US"\r\n", 2);
   p += len;
@@ -1640,30 +1859,14 @@ ss[ptr] = 0;  /* string_cat leaves room for this */
 /* Before we write the banner, check that there is no input pending, unless
 this synchronisation check is disabled. */
 
-if (smtp_enforce_sync && sender_host_address != NULL && !sender_host_notsocket)
+if (!check_sync())
   {
-  fd_set fds;
-  struct timeval tzero;
-  tzero.tv_sec = 0;
-  tzero.tv_usec = 0;
-  FD_ZERO(&fds);
-  FD_SET(fileno(smtp_in), &fds);
-  if (select(fileno(smtp_in) + 1, (SELECT_ARG2_TYPE *)&fds, NULL, NULL,
-      &tzero) > 0)
-    {
-    int rc = read(fileno(smtp_in), smtp_inbuffer, in_buffer_size);
-    if (rc > 0)
-      {
-      if (rc > 150) rc = 150;
-      smtp_inbuffer[rc] = 0;
-      log_write(0, LOG_MAIN|LOG_REJECT, "SMTP protocol "
-        "synchronization error (input sent without waiting for greeting): "
-        "rejected connection from %s input=\"%s\"", host_and_ident(TRUE),
-        string_printing(smtp_inbuffer));
-      smtp_printf("554 SMTP synchronization error\r\n");
-      return FALSE;
-      }
-    }
+  log_write(0, LOG_MAIN|LOG_REJECT, "SMTP protocol "
+    "synchronization error (input sent without waiting for greeting): "
+    "rejected connection from %s input=\"%s\"", host_and_ident(TRUE),
+    string_printing(smtp_inptr));
+  smtp_printf("554 SMTP synchronization error\r\n");
+  return FALSE;
   }
 
 /* Now output the banner */
@@ -1774,7 +1977,7 @@ output nothing for non-final calls, and only the first line for anything else.
 
 Arguments:
   code          SMTP code, may involve extended status codes
-  codelen       length of smtp code; uf > 3 there's an ESC
+  codelen       length of smtp code; if > 4 there's an ESC
   final         FALSE if the last line isn't the final line
   msg           message text, possibly containing newlines
 
@@ -1789,12 +1992,30 @@ uschar *esc = US"";
 
 if (!final && no_multiline_responses) return;
 
-if (codelen > 3)
+if (codelen > 4)
   {
   esc = code + 4;
   esclen = codelen - 4;
   }
 
+/* If this is the first output for a (non-batch) RCPT command, see if all RCPTs
+have had the same. Note: this code is also present in smtp_printf(). It would
+be tidier to have it only in one place, but when it was added, it was easier to
+do it that way, so as not to have to mess with the code for the RCPT command,
+which sometimes uses smtp_printf() and sometimes smtp_respond(). */
+
+if (rcpt_in_progress)
+  {
+  if (rcpt_smtp_response == NULL)
+    rcpt_smtp_response = string_copy(msg);
+  else if (rcpt_smtp_response_same &&
+           Ustrcmp(rcpt_smtp_response, msg) != 0)
+    rcpt_smtp_response_same = FALSE;
+  rcpt_in_progress = FALSE;
+  }
+
+/* Not output the message, splitting it up into multiple lines if necessary. */
+
 for (;;)
   {
   uschar *nl = Ustrchr(msg, '\n');
@@ -1821,6 +2042,65 @@ for (;;)
 
 
 
+/*************************************************
+*            Parse user SMTP message             *
+*************************************************/
+
+/* This function allows for user messages overriding the response code details
+by providing a suitable response code string at the start of the message
+user_msg. Check the message for starting with a response code and optionally an
+extended status code. If found, check that the first digit is valid, and if so,
+change the code pointer and length to use the replacement. An invalid code
+causes a panic log; in this case, if the log messages is the same as the user
+message, we must also adjust the value of the log message to show the code that
+is actually going to be used (the original one).
+
+This function is global because it is called from receive.c as well as within
+this module.
+
+Note that the code length returned includes the terminating whitespace
+character, which is always included in the regex match.
+
+Arguments:
+  code          SMTP code, may involve extended status codes
+  codelen       length of smtp code; if > 4 there's an ESC
+  msg           message text
+  log_msg       optional log message, to be adjusted with the new SMTP code
+
+Returns:        nothing
+*/
+
+void
+smtp_message_code(uschar **code, int *codelen, uschar **msg, uschar **log_msg)
+{
+int n;
+int ovector[3];
+
+if (msg == NULL || *msg == NULL) return;
+
+n = pcre_exec(regex_smtp_code, NULL, CS *msg, Ustrlen(*msg), 0,
+  PCRE_EOPT, ovector, sizeof(ovector)/sizeof(int));
+if (n < 0) return;
+
+if ((*msg)[0] != (*code)[0])
+  {
+  log_write(0, LOG_MAIN|LOG_PANIC, "configured error code starts with "
+    "incorrect digit (expected %c) in \"%s\"", (*code)[0], *msg);
+  if (log_msg != NULL && *log_msg == *msg)
+    *log_msg = string_sprintf("%s %s", *code, *log_msg + ovector[1]);
+  }
+else
+  {
+  *code = *msg;
+  *codelen = ovector[1];    /* Includes final space */
+  }
+*msg += ovector[1];         /* Chop the code off the message */
+return;
+}
+
+
+
+
 /*************************************************
 *           Handle an ACL failure                *
 *************************************************/
@@ -1861,7 +2141,6 @@ smtp_handle_acl_fail(int where, int rc, uschar *user_msg, uschar *log_msg)
 {
 BOOL drop = rc == FAIL_DROP;
 int codelen = 3;
-int ovector[3];
 uschar *smtp_code;
 uschar *lognl;
 uschar *sender_info = US"";
@@ -1871,46 +2150,16 @@ uschar *what =
 #endif
   (where == ACL_WHERE_PREDATA)? US"DATA" :
   (where == ACL_WHERE_DATA)? US"after DATA" :
-  (smtp_cmd_argument == NULL)?
+  (smtp_cmd_data == NULL)?
     string_sprintf("%s in \"connect\" ACL", acl_wherenames[where]) :
-    string_sprintf("%s %s", acl_wherenames[where], smtp_cmd_argument);
+    string_sprintf("%s %s", acl_wherenames[where], smtp_cmd_data);
 
 if (drop) rc = FAIL;
 
-/* Set the default SMTP code */
+/* Set the default SMTP code, and allow a user message to change it. */
 
 smtp_code = (rc != FAIL)? US"451" : acl_wherecodes[where];
-
-/* Check a user message for starting with a response code and optionally an
-extended status code. If found, check that the first digit is valid, and if so,
-use it instead of the default code. */
-
-if (user_msg != NULL)
-  {
-  int n = pcre_exec(regex_smtp_code, NULL, CS user_msg, Ustrlen(user_msg), 0,
-    PCRE_EOPT, ovector, sizeof(ovector)/sizeof(int));
-  if (n >= 0)
-    {
-    if (user_msg[0] != smtp_code[0])
-      {
-      log_write(0, LOG_MAIN|LOG_PANIC, "configured error code starts with "
-        "incorrect digit (expected %c) in \"%s\"", smtp_code[0], user_msg);
-
-      /* If log_msg == user_msg (the default set in acl.c if no log message is
-      specified, we must adjust the log message to show the code that is
-      actually going to be used. */
-
-      if (log_msg == user_msg)
-        log_msg = string_sprintf("%s %s", smtp_code, log_msg + ovector[1]);
-      }
-    else
-      {
-      smtp_code = user_msg;
-      codelen = ovector[1];    /* Includes final space */
-      }
-    user_msg += ovector[1];    /* Chop the code off the message */
-    }
-  }
+smtp_message_code(&smtp_code, &codelen, &user_msg, &log_msg);
 
 /* We used to have sender_address here; however, there was a bug that was not
 updating sender_address after a rewrite during a verify. When this bug was
@@ -1936,6 +2185,9 @@ unless the sender_verify_fail log selector has been turned off. */
 if (sender_verified_failed != NULL &&
     !testflag(sender_verified_failed, af_sverify_told))
   {
+  BOOL save_rcpt_in_progress = rcpt_in_progress;
+  rcpt_in_progress = FALSE;  /* So as not to treat these as the error */
+
   setflag(sender_verified_failed, af_sverify_told);
 
   if (rc != FAIL || (log_extra_selector & LX_sender_verify_fail) != 0)
@@ -1965,6 +2217,8 @@ if (sender_verified_failed != NULL &&
           "Verification failed for <%s>\n%s",
         sender_verified_failed->address,
         sender_verified_failed->user_message));
+
+  rcpt_in_progress = save_rcpt_in_progress;
   }
 
 /* Sort out text for logging */
@@ -2005,23 +2259,112 @@ else
       US"Temporary local problem - please try later");
   }
 
-/* Log the incident. If the connection is not forcibly to be dropped, return 0.
-Otherwise, log why it is closing if required and return 2.  */
+/* Log the incident to the logs that are specified by log_reject_target
+(default main, reject). This can be empty to suppress logging of rejections. If
+the connection is not forcibly to be dropped, return 0. Otherwise, log why it
+is closing if required and return 2.  */
 
-log_write(0, LOG_MAIN|LOG_REJECT, "%s %s%srejected %s%s",
-  host_and_ident(TRUE),
-  sender_info, (rc == FAIL)? US"" : US"temporarily ", what, log_msg);
+if (log_reject_target != 0)
+  log_write(0, log_reject_target, "%s %s%srejected %s%s",
+    host_and_ident(TRUE),
+    sender_info, (rc == FAIL)? US"" : US"temporarily ", what, log_msg);
 
 if (!drop) return 0;
 
 log_write(L_smtp_connection, LOG_MAIN, "%s closed by DROP in ACL",
   smtp_get_connection_info());
+
+/* Run the not-quit ACL, but without any custom messages. This should not be a
+problem, because we get here only if some other ACL has issued "drop", and
+in that case, *its* custom messages will have been used above. */
+
+smtp_notquit_exit(US"acl-drop", NULL, NULL);
 return 2;
 }
 
 
 
 
+/*************************************************
+*     Handle SMTP exit when QUIT is not given    *
+*************************************************/
+
+/* This function provides a logging/statistics hook for when an SMTP connection
+is dropped on the floor or the other end goes away. It's a global function
+because it's called from receive.c as well as this module. As well as running
+the NOTQUIT ACL, if there is one, this function also outputs a final SMTP
+response, either with a custom message from the ACL, or using a default. There
+is one case, however, when no message is output - after "drop". In that case,
+the ACL that obeyed "drop" has already supplied the custom message, and NULL is
+passed to this function.
+
+In case things go wrong while processing this function, causing an error that
+may re-enter this funtion, there is a recursion check.
+
+Arguments:
+  reason          What $smtp_notquit_reason will be set to in the ACL;
+                    if NULL, the ACL is not run
+  code            The error code to return as part of the response
+  defaultrespond  The default message if there's no user_msg
+
+Returns:          Nothing
+*/
+
+void
+smtp_notquit_exit(uschar *reason, uschar *code, uschar *defaultrespond, ...)
+{
+int rc;
+uschar *user_msg = NULL;
+uschar *log_msg = NULL;
+
+/* Check for recursive acll */
+
+if (smtp_exit_function_called)
+  {
+  log_write(0, LOG_PANIC, "smtp_notquit_exit() called more than once (%s)",
+    reason);
+  return;
+  }
+smtp_exit_function_called = TRUE;
+
+/* Call the not-QUIT ACL, if there is one, unless no reason is given. */
+
+if (acl_smtp_notquit != NULL && reason != NULL)
+  {
+  smtp_notquit_reason = reason;
+  rc = acl_check(ACL_WHERE_NOTQUIT, NULL, acl_smtp_notquit, &user_msg,
+    &log_msg);
+  if (rc == ERROR)
+    log_write(0, LOG_MAIN|LOG_PANIC, "ACL for not-QUIT returned ERROR: %s",
+      log_msg);
+  }
+
+/* Write an SMTP response if we are expected to give one. As the default
+responses are all internal, they should always fit in the buffer, but code a
+warning, just in case. Note that string_vformat() still leaves a complete
+string, even if it is incomplete. */
+
+if (code != NULL && defaultrespond != NULL)
+  {
+  if (user_msg == NULL)
+    {
+    uschar buffer[128];
+    va_list ap;
+    va_start(ap, defaultrespond);
+    if (!string_vformat(buffer, sizeof(buffer), CS defaultrespond, ap))
+      log_write(0, LOG_MAIN|LOG_PANIC, "string too large in smtp_notquit_exit()");
+    smtp_printf("%s %s\r\n", code, buffer);
+    va_end(ap);
+    }
+  else
+    smtp_respond(code, 3, TRUE, user_msg);
+  mac_smtp_fflush();
+  }
+}
+
+
+
+
 /*************************************************
 *             Verify HELO argument               *
 *************************************************/
@@ -2054,6 +2397,16 @@ if (sender_helo_name == NULL)
   HDEBUG(D_receive) debug_printf("no EHLO/HELO command was issued\n");
   }
 
+/* Deal with the case of -bs without an IP address */
+
+else if (sender_host_address == NULL)
+  {
+  HDEBUG(D_receive) debug_printf("no client IP address: assume success\n");
+  helo_verified = TRUE;
+  }
+
+/* Deal with the more common case when there is a sending IP address */
+
 else if (sender_helo_name[0] == '[')
   {
   helo_verified = Ustrncmp(sender_helo_name+1, sender_host_address,
@@ -2119,7 +2472,7 @@ else
     h.next = NULL;
     HDEBUG(D_receive) debug_printf("getting IP address for %s\n",
       sender_helo_name);
-    rc = host_find_byname(&h, NULL, NULL, TRUE);
+    rc = host_find_byname(&h, NULL, 0, NULL, TRUE);
     if (rc == HOST_FOUND || rc == HOST_FOUND_LOCAL)
       {
       host_item *hh = &h;
@@ -2139,13 +2492,40 @@ else
     }
   }
 
-if (!helo_verified) helo_verify_failed = FALSE;  /* We've tried ... */
+if (!helo_verified) helo_verify_failed = TRUE;  /* We've tried ... */
 return yield;
 }
 
 
 
 
+/*************************************************
+*        Send user response message              *
+*************************************************/
+
+/* This function is passed a default response code and a user message. It calls
+smtp_message_code() to check and possibly modify the response code, and then
+calls smtp_respond() to transmit the response. I put this into a function
+just to avoid a lot of repetition.
+
+Arguments:
+  code         the response code
+  user_msg     the user message
+
+Returns:       nothing
+*/
+
+static void
+smtp_user_msg(uschar *code, uschar *user_msg)
+{
+int len = 3;
+smtp_message_code(&code, &len, &user_msg, NULL);
+smtp_respond(code, len, TRUE, user_msg);
+}
+
+
+
+
 /*************************************************
 *       Initialize for SMTP incoming message     *
 *************************************************/
@@ -2216,7 +2596,8 @@ while (done <= 0)
   uschar *etrn_command;
   uschar *etrn_serialize_key;
   uschar *errmess;
-  uschar *user_msg, *log_msg;
+  uschar *log_msg, *smtp_code;
+  uschar *user_msg = NULL;
   uschar *recipient = NULL;
   uschar *hello = NULL;
   uschar *set_id = NULL;
@@ -2247,6 +2628,7 @@ while (done <= 0)
     AUTHS will eventually hit the nonmail threshold. */
 
     case AUTH_CMD:
+    HAD(SCH_AUTH);
     authentication_failed = TRUE;
     cmd_list[CMD_LIST_AUTH].is_mail_cmd = FALSE;
 
@@ -2283,8 +2665,8 @@ while (done <= 0)
 
     /* Find the name of the requested authentication mechanism. */
 
-    s = smtp_cmd_argument;
-    while ((c = *smtp_cmd_argument) != 0 && !isspace(c))
+    s = smtp_cmd_data;
+    while ((c = *smtp_cmd_data) != 0 && !isspace(c))
       {
       if (!isalnum(c) && c != '-' && c != '_')
         {
@@ -2292,16 +2674,16 @@ while (done <= 0)
           US"invalid character in authentication mechanism name");
         goto COMMAND_LOOP;
         }
-      smtp_cmd_argument++;
+      smtp_cmd_data++;
       }
 
     /* If not at the end of the line, we must be at white space. Terminate the
     name and move the pointer on to any data that may be present. */
 
-    if (*smtp_cmd_argument != 0)
+    if (*smtp_cmd_data != 0)
       {
-      *smtp_cmd_argument++ = 0;
-      while (isspace(*smtp_cmd_argument)) smtp_cmd_argument++;
+      *smtp_cmd_data++ = 0;
+      while (isspace(*smtp_cmd_data)) smtp_cmd_data++;
       }
 
     /* Search for an authentication mechanism which is configured for use
@@ -2337,7 +2719,7 @@ while (done <= 0)
     expand_nmax = 0;
     expand_nlength[0] = 0;   /* $0 contains nothing */
 
-    c = (au->info->servercode)(au, smtp_cmd_argument);
+    c = (au->info->servercode)(au, smtp_cmd_data);
     if (au->set_id != NULL) set_id = expand_string(au->set_id);
     expand_nmax = -1;        /* Reset numeric variables */
     for (i = 0; i < AUTH_VARS; i++) auth_vars[i] = NULL;   /* Reset $auth<n> */
@@ -2439,11 +2821,13 @@ while (done <= 0)
     it did the reset first. */
 
     case HELO_CMD:
+    HAD(SCH_HELO);
     hello = US"HELO";
     esmtp = FALSE;
     goto HELO_EHLO;
 
     case EHLO_CMD:
+    HAD(SCH_EHLO);
     hello = US"EHLO";
     esmtp = TRUE;
 
@@ -2454,7 +2838,7 @@ while (done <= 0)
     /* Reject the HELO if its argument was invalid or non-existent. A
     successful check causes the argument to be saved in malloc store. */
 
-    if (!check_helo(smtp_cmd_argument))
+    if (!check_helo(smtp_cmd_data))
       {
       smtp_printf("501 Syntactically invalid %s argument(s)\r\n", hello);
 
@@ -2484,7 +2868,7 @@ while (done <= 0)
     if (!sender_host_unknown)
       {
       BOOL old_helo_verified = helo_verified;
-      uschar *p = smtp_cmd_argument;
+      uschar *p = smtp_cmd_data;
 
       while (*p != 0 && !isspace(*p)) { *p = tolower(*p); p++; }
       *p = 0;
@@ -2539,7 +2923,8 @@ while (done <= 0)
     spf_init(sender_helo_name, sender_host_address);
 #endif
 
-    /* Apply an ACL check if one is defined */
+    /* Apply an ACL check if one is defined; afterwards, recheck
+    synchronization in case the client started sending in a delay. */
 
     if (acl_smtp_helo != NULL)
       {
@@ -2551,28 +2936,14 @@ while (done <= 0)
         host_build_sender_fullhost();  /* Rebuild */
         break;
         }
+      else if (!check_sync()) goto SYNC_FAILURE;
       }
 
-    /* The EHLO/HELO command is acceptable. Reset the protocol and the state,
-    abandoning any previous message. */
-
-    received_protocol = (esmtp?
-      protocols[pextend +
-        ((sender_host_authenticated != NULL)? pauthed : 0) +
-        ((tls_active >= 0)? pcrpted : 0)]
-      :
-      protocols[pnormal + ((tls_active >= 0)? pcrpted : 0)])
-      +
-      ((sender_host_address != NULL)? pnlocal : 0);
-
-    smtp_reset(reset_point);
-    toomany = FALSE;
-
-    /* Generate an OK reply, including the ident if present, and also
-    the IP address if present. Reflecting back the ident is intended
-    as a deterrent to mail forgers. For maximum efficiency, and also
-    because some broken systems expect each response to be in a single
-    packet, arrange that it is sent in one write(). */
+    /* Generate an OK reply. The default string includes the ident if present,
+    and also the IP address if present. Reflecting back the ident is intended
+    as a deterrent to mail forgers. For maximum efficiency, and also because
+    some broken systems expect each response to be in a single packet, arrange
+    that the entire reply is sent in one write(). */
 
     auth_advertised = FALSE;
     pipelining_advertised = FALSE;
@@ -2580,21 +2951,46 @@ while (done <= 0)
     tls_advertised = FALSE;
     #endif
 
-    s = string_sprintf("250 %s Hello %s%s%s",
-      smtp_active_hostname,
-      (sender_ident == NULL)?  US"" : sender_ident,
-      (sender_ident == NULL)?  US"" : US" at ",
-      (sender_host_name == NULL)? sender_helo_name : sender_host_name);
+    smtp_code = US"250 ";        /* Default response code plus space*/
+    if (user_msg == NULL)
+      {
+      s = string_sprintf("%.3s %s Hello %s%s%s",
+        smtp_code,
+        smtp_active_hostname,
+        (sender_ident == NULL)?  US"" : sender_ident,
+        (sender_ident == NULL)?  US"" : US" at ",
+        (sender_host_name == NULL)? sender_helo_name : sender_host_name);
 
-    ptr = Ustrlen(s);
-    size = ptr + 1;
+      ptr = Ustrlen(s);
+      size = ptr + 1;
 
-    if (sender_host_address != NULL)
+      if (sender_host_address != NULL)
+        {
+        s = string_cat(s, &size, &ptr, US" [", 2);
+        s = string_cat(s, &size, &ptr, sender_host_address,
+          Ustrlen(sender_host_address));
+        s = string_cat(s, &size, &ptr, US"]", 1);
+        }
+      }
+
+    /* A user-supplied EHLO greeting may not contain more than one line. Note
+    that the code returned by smtp_message_code() includes the terminating
+    whitespace character. */
+
+    else
       {
-      s = string_cat(s, &size, &ptr, US" [", 2);
-      s = string_cat(s, &size, &ptr, sender_host_address,
-        Ustrlen(sender_host_address));
-      s = string_cat(s, &size, &ptr, US"]", 1);
+      char *ss;
+      int codelen = 4;
+      smtp_message_code(&smtp_code, &codelen, &user_msg, NULL);
+      s = string_sprintf("%.*s%s", codelen, smtp_code, user_msg);
+      if ((ss = strpbrk(CS s, "\r\n")) != NULL)
+        {
+        log_write(0, LOG_MAIN|LOG_PANIC, "EHLO/HELO response must not contain "
+          "newlines: message truncated: %s", string_printing(s));
+        *ss = 0;
+        }
+      ptr = Ustrlen(s);
+      size = ptr + 1;
       }
 
     s = string_cat(s, &size, &ptr, US"\r\n", 2);
@@ -2614,12 +3010,14 @@ while (done <= 0)
 
       if (thismessage_size_limit > 0)
         {
-        sprintf(CS big_buffer, "250-SIZE %d\r\n", thismessage_size_limit);
+        sprintf(CS big_buffer, "%.3s-SIZE %d\r\n", smtp_code,
+          thismessage_size_limit);
         s = string_cat(s, &size, &ptr, big_buffer, Ustrlen(big_buffer));
         }
       else
         {
-        s = string_cat(s, &size, &ptr, US"250-SIZE\r\n", 10);
+        s = string_cat(s, &size, &ptr, smtp_code, 3);
+        s = string_cat(s, &size, &ptr, US"-SIZE\r\n", 7);
         }
 
       /* Exim does not do protocol conversion or data conversion. It is 8-bit
@@ -2630,14 +3028,18 @@ while (done <= 0)
       provided as an option. */
 
       if (accept_8bitmime)
-        s = string_cat(s, &size, &ptr, US"250-8BITMIME\r\n", 14);
+        {
+        s = string_cat(s, &size, &ptr, smtp_code, 3);
+        s = string_cat(s, &size, &ptr, US"-8BITMIME\r\n", 11);
+        }
 
       /* Advertise ETRN if there's an ACL checking whether a host is
       permitted to issue it; a check is made when any host actually tries. */
 
       if (acl_smtp_etrn != NULL)
         {
-        s = string_cat(s, &size, &ptr, US"250-ETRN\r\n", 10);
+        s = string_cat(s, &size, &ptr, smtp_code, 3);
+        s = string_cat(s, &size, &ptr, US"-ETRN\r\n", 7);
         }
 
       /* Advertise EXPN if there's an ACL checking whether a host is
@@ -2645,15 +3047,18 @@ while (done <= 0)
 
       if (acl_smtp_expn != NULL)
         {
-        s = string_cat(s, &size, &ptr, US"250-EXPN\r\n", 10);
+        s = string_cat(s, &size, &ptr, smtp_code, 3);
+        s = string_cat(s, &size, &ptr, US"-EXPN\r\n", 7);
         }
 
       /* Exim is quite happy with pipelining, so let the other end know that
       it is safe to use it, unless advertising is disabled. */
 
-      if (verify_check_host(&pipelining_advertise_hosts) == OK)
+      if (pipelining_enable &&
+          verify_check_host(&pipelining_advertise_hosts) == OK)
         {
-        s = string_cat(s, &size, &ptr, US"250-PIPELINING\r\n", 16);
+        s = string_cat(s, &size, &ptr, smtp_code, 3);
+        s = string_cat(s, &size, &ptr, US"-PIPELINING\r\n", 13);
         sync_cmd_limit = NON_SYNC_CMD_PIPELINING;
         pipelining_advertised = TRUE;
         }
@@ -2683,7 +3088,8 @@ while (done <= 0)
               int saveptr;
               if (first)
                 {
-                s = string_cat(s, &size, &ptr, US"250-AUTH", 8);
+                s = string_cat(s, &size, &ptr, smtp_code, 3);
+                s = string_cat(s, &size, &ptr, US"-AUTH", 5);
                 first = FALSE;
                 auth_advertised = TRUE;
                 }
@@ -2709,14 +3115,16 @@ while (done <= 0)
       if (tls_active < 0 &&
           verify_check_host(&tls_advertise_hosts) != FAIL)
         {
-        s = string_cat(s, &size, &ptr, US"250-STARTTLS\r\n", 14);
+        s = string_cat(s, &size, &ptr, smtp_code, 3);
+        s = string_cat(s, &size, &ptr, US"-STARTTLS\r\n", 11);
         tls_advertised = TRUE;
         }
       #endif
 
       /* Finish off the multiline reply with one that is always available. */
 
-      s = string_cat(s, &size, &ptr, US"250 HELP\r\n", 10);
+      s = string_cat(s, &size, &ptr, smtp_code, 3);
+      s = string_cat(s, &size, &ptr, US" HELP\r\n", 7);
       }
 
     /* Terminate the string (for debug), write it, and note that HELO/EHLO
@@ -2737,6 +3145,20 @@ while (done <= 0)
       debug_printf("SMTP>> %s", s);
       }
     helo_seen = TRUE;
+
+    /* Reset the protocol and the state, abandoning any previous message. */
+
+    received_protocol = (esmtp?
+      protocols[pextend +
+        ((sender_host_authenticated != NULL)? pauthed : 0) +
+        ((tls_active >= 0)? pcrpted : 0)]
+      :
+      protocols[pnormal + ((tls_active >= 0)? pcrpted : 0)])
+      +
+      ((sender_host_address != NULL)? pnlocal : 0);
+
+    smtp_reset(reset_point);
+    toomany = FALSE;
     break;   /* HELO/EHLO */
 
 
@@ -2747,6 +3169,7 @@ while (done <= 0)
     it is the canonical extracted address which is all that is kept. */
 
     case MAIL_CMD:
+    HAD(SCH_MAIL);
     smtp_mailcmd_count++;              /* Count for limit and ratelimit */
     was_rej_mail = TRUE;               /* Reset if accepted */
 
@@ -2765,7 +3188,7 @@ while (done <= 0)
       break;
       }
 
-    if (smtp_cmd_argument[0] == 0)
+    if (smtp_cmd_data[0] == 0)
       {
       done = synprot_error(L_smtp_protocol_error, 501, NULL,
         US"MAIL must have an address operand");
@@ -2804,7 +3227,7 @@ while (done <= 0)
       in order to be able to log the sender address on failure. */
 
       if (strcmpic(name, US"SIZE") == 0 &&
-          ((size = (int)Ustrtoul(value, &end, 10)), *end == 0))
+          ((size = Ustrtoul(value, &end, 10)), *end == 0))
         {
         if ((size == ULONG_MAX && errno == ERANGE) || size > INT_MAX)
           size = INT_MAX;
@@ -2924,8 +3347,8 @@ while (done <= 0)
     TRUE flag allows "<>" as a sender address. */
 
     raw_sender = ((rewrite_existflags & rewrite_smtp) != 0)?
-      rewrite_one(smtp_cmd_argument, rewrite_smtp, NULL, FALSE, US"",
-        global_rewrite_rules) : smtp_cmd_argument;
+      rewrite_one(smtp_cmd_data, rewrite_smtp, NULL, FALSE, US"",
+        global_rewrite_rules) : smtp_cmd_data;
 
     /* rfc821_domains = TRUE; << no longer needed */
     raw_sender =
@@ -2935,7 +3358,7 @@ while (done <= 0)
 
     if (raw_sender == NULL)
       {
-      done = synprot_error(L_smtp_syntax_error, 501, smtp_cmd_argument, errmess);
+      done = synprot_error(L_smtp_syntax_error, 501, smtp_cmd_data, errmess);
       break;
       }
 
@@ -2995,7 +3418,7 @@ while (done <= 0)
       else
         {
         smtp_printf("501 %s: sender address must contain a domain\r\n",
-          smtp_cmd_argument);
+          smtp_cmd_data);
         log_write(L_smtp_syntax_error,
           LOG_MAIN|LOG_REJECT,
           "unqualified sender rejected: <%s> %s%s",
@@ -3007,19 +3430,25 @@ while (done <= 0)
         }
       }
 
-    /* Apply an ACL check if one is defined, before responding */
+    /* Apply an ACL check if one is defined, before responding. Afterwards,
+    when pipelining is not advertised, do another sync check in case the ACL
+    delayed and the client started sending in the meantime. */
 
-    rc = (acl_smtp_mail == NULL)? OK :
-      acl_check(ACL_WHERE_MAIL, NULL, acl_smtp_mail, &user_msg, &log_msg);
+    if (acl_smtp_mail == NULL) rc = OK; else
+      {
+      rc = acl_check(ACL_WHERE_MAIL, NULL, acl_smtp_mail, &user_msg, &log_msg);
+      if (rc == OK && !pipelining_advertised && !check_sync())
+        goto SYNC_FAILURE;
+      }
 
     if (rc == OK || rc == DISCARD)
       {
-      smtp_printf("250 OK\r\n");
+      if (user_msg == NULL) smtp_printf("250 OK\r\n");
+        else smtp_user_msg(US"250", user_msg);
       smtp_delay_rcpt = smtp_rlr_base;
       recipients_discarded = (rc == DISCARD);
       was_rej_mail = FALSE;
       }
-
     else
       {
       done = smtp_handle_acl_fail(ACL_WHERE_MAIL, rc, user_msg, log_msg);
@@ -3028,16 +3457,15 @@ while (done <= 0)
     break;
 
 
-    /* The RCPT command requires an address as an operand. All we do
-    here is to parse it for syntactic correctness. There may be any number
-    of RCPT commands, specifying multiple senders. We build them all into
-    a data structure that is in argc/argv format. The start/end values
-    given by parse_extract_address are not used, as we keep only the
-    extracted address. */
+    /* The RCPT command requires an address as an operand. There may be any
+    number of RCPT commands, specifying multiple recipients. We build them all
+    into a data structure. The start/end values given by parse_extract_address
+    are not used, as we keep only the extracted address. */
 
     case RCPT_CMD:
+    HAD(SCH_RCPT);
     rcpt_count++;
-    was_rcpt = TRUE;
+    was_rcpt = rcpt_in_progress = TRUE;
 
     /* There must be a sender address; if the sender was rejected and
     pipelining was advertised, we assume the client was pipelining, and do not
@@ -3063,7 +3491,7 @@ while (done <= 0)
 
     /* Check for an operand */
 
-    if (smtp_cmd_argument[0] == 0)
+    if (smtp_cmd_data[0] == 0)
       {
       done = synprot_error(L_smtp_syntax_error, 501, NULL,
         US"RCPT must have an address operand");
@@ -3075,8 +3503,8 @@ while (done <= 0)
     as a recipient address */
 
     recipient = ((rewrite_existflags & rewrite_smtp) != 0)?
-      rewrite_one(smtp_cmd_argument, rewrite_smtp, NULL, FALSE, US"",
-        global_rewrite_rules) : smtp_cmd_argument;
+      rewrite_one(smtp_cmd_data, rewrite_smtp, NULL, FALSE, US"",
+        global_rewrite_rules) : smtp_cmd_data;
 
     /* rfc821_domains = TRUE; << no longer needed */
     recipient = parse_extract_address(recipient, &errmess, &start, &end,
@@ -3085,7 +3513,7 @@ while (done <= 0)
 
     if (recipient == NULL)
       {
-      done = synprot_error(L_smtp_syntax_error, 501, smtp_cmd_argument, errmess);
+      done = synprot_error(L_smtp_syntax_error, 501, smtp_cmd_data, errmess);
       rcpt_fail_count++;
       break;
       }
@@ -3115,7 +3543,7 @@ while (done <= 0)
         {
         rcpt_fail_count++;
         smtp_printf("501 %s: recipient address must contain a domain\r\n",
-          smtp_cmd_argument);
+          smtp_cmd_data);
         log_write(L_smtp_syntax_error,
           LOG_MAIN|LOG_REJECT, "unqualified recipient rejected: "
           "<%s> %s%s", recipient, host_and_ident(TRUE),
@@ -3165,16 +3593,24 @@ while (done <= 0)
       }
 
     /* If the MAIL ACL discarded all the recipients, we bypass ACL checking
-    for them. Otherwise, check the access control list for this recipient. */
+    for them. Otherwise, check the access control list for this recipient. As
+    there may be a delay in this, re-check for a synchronization error
+    afterwards, unless pipelining was advertised. */
 
-    rc = recipients_discarded? DISCARD :
-      acl_check(ACL_WHERE_RCPT, recipient, acl_smtp_rcpt, &user_msg, &log_msg);
+    if (recipients_discarded) rc = DISCARD; else
+      {
+      rc = acl_check(ACL_WHERE_RCPT, recipient, acl_smtp_rcpt, &user_msg,
+        &log_msg);
+      if (rc == OK && !pipelining_advertised && !check_sync())
+        goto SYNC_FAILURE;
+      }
 
     /* The ACL was happy */
 
     if (rc == OK)
       {
-      smtp_printf("250 Accepted\r\n");
+      if (user_msg == NULL) smtp_printf("250 Accepted\r\n");
+        else smtp_user_msg(US"250", user_msg);
       receive_add_recipient(recipient, -1);
       }
 
@@ -3182,7 +3618,8 @@ while (done <= 0)
 
     else if (rc == DISCARD)
       {
-      smtp_printf("250 Accepted\r\n");
+      if (user_msg == NULL) smtp_printf("250 Accepted\r\n");
+        else smtp_user_msg(US"250", user_msg);
       rcpt_fail_count++;
       discarded = TRUE;
       log_write(0, LOG_MAIN|LOG_REJECT, "%s F=<%s> rejected RCPT %s: "
@@ -3218,13 +3655,29 @@ while (done <= 0)
         DATA command.
 
     The example in the pipelining RFC 2920 uses 554, but I use 503 here
-    because it is the same whether pipelining is in use or not. */
+    because it is the same whether pipelining is in use or not.
+
+    If all the RCPT commands that precede DATA provoked the same error message
+    (often indicating some kind of system error), it is helpful to include it
+    with the DATA rejection (an idea suggested by Tony Finch). */
 
     case DATA_CMD:
+    HAD(SCH_DATA);
     if (!discarded && recipients_count <= 0)
       {
+      if (rcpt_smtp_response_same && rcpt_smtp_response != NULL)
+        {
+        uschar *code = US"503";
+        int len = Ustrlen(rcpt_smtp_response);
+        smtp_respond(code, 3, FALSE, US"All RCPT commands were rejected with "
+          "this error:");
+        /* Responses from smtp_printf() will have \r\n on the end */
+        if (len > 2 && rcpt_smtp_response[len-2] == '\r')
+          rcpt_smtp_response[len-2] = 0;
+        smtp_respond(code, 3, FALSE, rcpt_smtp_response);
+        }
       if (pipelining_advertised && last_was_rcpt)
-        smtp_printf("503 valid RCPT command must precede DATA\r\n");
+        smtp_printf("503 Valid RCPT command must precede DATA\r\n");
       else
         done = synprot_error(L_smtp_protocol_error, 503, NULL,
           US"valid RCPT command must precede DATA");
@@ -3239,17 +3692,23 @@ while (done <= 0)
       break;
       }
 
+    /* If there is an ACL, re-check the synchronization afterwards, since the
+    ACL may have delayed. */
+
     if (acl_smtp_predata == NULL) rc = OK; else
       {
       enable_dollar_recipients = TRUE;
       rc = acl_check(ACL_WHERE_PREDATA, NULL, acl_smtp_predata, &user_msg,
         &log_msg);
       enable_dollar_recipients = FALSE;
+      if (rc == OK && !check_sync()) goto SYNC_FAILURE;
       }
 
     if (rc == OK)
       {
-      smtp_printf("354 Enter message, ending with \".\" on a line by itself\r\n");
+      if (user_msg == NULL)
+        smtp_printf("354 Enter message, ending with \".\" on a line by itself\r\n");
+      else smtp_user_msg(US"354", user_msg);
       done = 3;
       message_ended = END_NOTENDED;   /* Indicate in middle of data */
       }
@@ -3258,11 +3717,11 @@ while (done <= 0)
 
     else
       done = smtp_handle_acl_fail(ACL_WHERE_PREDATA, rc, user_msg, log_msg);
-
     break;
 
 
     case VRFY_CMD:
+    HAD(SCH_VRFY);
     rc = acl_check(ACL_WHERE_VRFY, NULL, acl_smtp_vrfy, &user_msg, &log_msg);
     if (rc != OK)
       done = smtp_handle_acl_fail(ACL_WHERE_VRFY, rc, user_msg, log_msg);
@@ -3272,7 +3731,7 @@ while (done <= 0)
       uschar *s = NULL;
 
       /* rfc821_domains = TRUE; << no longer needed */
-      address = parse_extract_address(smtp_cmd_argument, &errmess, &start, &end,
+      address = parse_extract_address(smtp_cmd_data, &errmess, &start, &end,
         &recipient_domain, FALSE);
       /* rfc821_domains = FALSE; << no longer needed */
 
@@ -3310,6 +3769,7 @@ while (done <= 0)
 
 
     case EXPN_CMD:
+    HAD(SCH_EXPN);
     rc = acl_check(ACL_WHERE_EXPN, NULL, acl_smtp_expn, &user_msg, &log_msg);
     if (rc != OK)
       done = smtp_handle_acl_fail(ACL_WHERE_EXPN, rc, user_msg, log_msg);
@@ -3317,7 +3777,7 @@ while (done <= 0)
       {
       BOOL save_log_testing_mode = log_testing_mode;
       address_test_mode = log_testing_mode = TRUE;
-      (void) verify_address(deliver_make_addr(smtp_cmd_argument, FALSE),
+      (void) verify_address(deliver_make_addr(smtp_cmd_data, FALSE),
         smtp_out, vopt_is_recipient | vopt_qualify | vopt_expn, -1, -1, -1,
         NULL, NULL, NULL);
       address_test_mode = FALSE;
@@ -3329,6 +3789,7 @@ while (done <= 0)
     #ifdef SUPPORT_TLS
 
     case STARTTLS_CMD:
+    HAD(SCH_STARTTLS);
     if (!tls_advertised)
       {
       done = synprot_error(L_smtp_protocol_error, 503, NULL,
@@ -3367,7 +3828,8 @@ while (done <= 0)
     We must allow for an extra EHLO command and an extra AUTH command after
     STARTTLS that don't add to the nonmail command count. */
 
-    if ((rc = tls_server_start(tls_require_ciphers)) == OK)
+    if ((rc = tls_server_start(tls_require_ciphers, gnutls_require_mac,
+           gnutls_require_kx, gnutls_require_proto)) == OK)
       {
       if (!tls_remember_esmtp)
         helo_seen = esmtp = auth_advertised = pipelining_advertised = FALSE;
@@ -3417,11 +3879,29 @@ while (done <= 0)
         case EOF_CMD:
         log_write(L_smtp_connection, LOG_MAIN, "%s closed by EOF",
           smtp_get_connection_info());
+        smtp_notquit_exit(US"tls-failed", NULL, NULL);
         done = 2;
         break;
 
+        /* It is perhaps arguable as to which exit ACL should be called here,
+        but as it is probably a situtation that almost never arises, it
+        probably doesn't matter. We choose to call the real QUIT ACL, which in
+        some sense is perhaps "right". */
+
         case QUIT_CMD:
-        smtp_printf("221 %s closing connection\r\n", smtp_active_hostname);
+        user_msg = NULL;
+        if (acl_smtp_quit != NULL)
+          {
+          rc = acl_check(ACL_WHERE_QUIT, NULL, acl_smtp_quit, &user_msg,
+            &log_msg);
+          if (rc == ERROR)
+            log_write(0, LOG_MAIN|LOG_PANIC, "ACL for QUIT returned ERROR: %s",
+              log_msg);
+          }
+        if (user_msg == NULL)
+          smtp_printf("221 %s closing connection\r\n", smtp_active_hostname);
+        else
+          smtp_respond(US"221", 3, TRUE, user_msg);
         log_write(L_smtp_connection, LOG_MAIN, "%s closed by QUIT",
           smtp_get_connection_info());
         done = 2;
@@ -3442,21 +3922,19 @@ while (done <= 0)
     message. */
 
     case QUIT_CMD:
+    HAD(SCH_QUIT);
     incomplete_transaction_log(US"QUIT");
-
     if (acl_smtp_quit != NULL)
       {
-      rc = acl_check(ACL_WHERE_QUIT, NULL, acl_smtp_quit,&user_msg,&log_msg);
+      rc = acl_check(ACL_WHERE_QUIT, NULL, acl_smtp_quit, &user_msg, &log_msg);
       if (rc == ERROR)
         log_write(0, LOG_MAIN|LOG_PANIC, "ACL for QUIT returned ERROR: %s",
           log_msg);
       }
-    else user_msg = NULL;
-
     if (user_msg == NULL)
       smtp_printf("221 %s closing connection\r\n", smtp_active_hostname);
     else
-      smtp_printf("221 %s\r\n", user_msg);
+      smtp_respond(US"221", 3, TRUE, user_msg);
 
     #ifdef SUPPORT_TLS
     tls_close(TRUE);
@@ -3469,6 +3947,7 @@ while (done <= 0)
 
 
     case RSET_CMD:
+    HAD(SCH_RSET);
     incomplete_transaction_log(US"RSET");
     smtp_reset(reset_point);
     toomany = FALSE;
@@ -3478,22 +3957,27 @@ while (done <= 0)
 
 
     case NOOP_CMD:
+    HAD(SCH_NOOP);
     smtp_printf("250 OK\r\n");
     break;
 
 
-    /* Show ETRN/EXPN/VRFY if there's
-    an ACL for checking hosts; if actually used, a check will be done for
-    permitted hosts. */
+    /* Show ETRN/EXPN/VRFY if there's an ACL for checking hosts; if actually
+    used, a check will be done for permitted hosts. Show STARTTLS only if not
+    already in a TLS session and if it would be advertised in the EHLO
+    response. */
 
     case HELP_CMD:
+    HAD(SCH_HELP);
     smtp_printf("214-Commands supported:\r\n");
       {
       uschar buffer[256];
       buffer[0] = 0;
       Ustrcat(buffer, " AUTH");
       #ifdef SUPPORT_TLS
-      Ustrcat(buffer, " STARTTLS");
+      if (tls_active < 0 &&
+          verify_check_host(&tls_advertise_hosts) != FAIL)
+        Ustrcat(buffer, " STARTTLS");
       #endif
       Ustrcat(buffer, " HELO EHLO MAIL RCPT DATA");
       Ustrcat(buffer, " NOOP QUIT RSET HELP");
@@ -3507,7 +3991,8 @@ while (done <= 0)
 
     case EOF_CMD:
     incomplete_transaction_log(US"connection lost");
-    smtp_printf("421 %s lost input connection\r\n", smtp_active_hostname);
+    smtp_notquit_exit(US"connection-lost", US"421",
+      US"%s lost input connection", smtp_active_hostname);
 
     /* Don't log by default unless in the middle of a message, as some mailers
     just drop the call rather than sending QUIT, and it clutters up the logs.
@@ -3528,6 +4013,7 @@ while (done <= 0)
 
 
     case ETRN_CMD:
+    HAD(SCH_ETRN);
     if (sender_address != NULL)
       {
       done = synprot_error(L_smtp_protocol_error, 503, NULL,
@@ -3547,7 +4033,7 @@ while (done <= 0)
 
     /* Compute the serialization key for this command. */
 
-    etrn_serialize_key = string_sprintf("etrn-%s\n", smtp_cmd_argument);
+    etrn_serialize_key = string_sprintf("etrn-%s\n", smtp_cmd_data);
 
     /* If a command has been specified for running as a result of ETRN, we
     permit any argument to ETRN. If not, only the # standard form is permitted,
@@ -3559,7 +4045,7 @@ while (done <= 0)
       uschar *error;
       BOOL rc;
       etrn_command = smtp_etrn_command;
-      deliver_domain = smtp_cmd_argument;
+      deliver_domain = smtp_cmd_data;
       rc = transport_set_up_command(&argv, smtp_etrn_command, TRUE, 0, NULL,
         US"ETRN processing", &error);
       deliver_domain = NULL;
@@ -3576,7 +4062,7 @@ while (done <= 0)
 
     else
       {
-      if (*smtp_cmd_argument++ != '#')
+      if (*smtp_cmd_data++ != '#')
         {
         done = synprot_error(L_smtp_syntax_error, 501, NULL,
           US"argument must begin with #");
@@ -3584,7 +4070,7 @@ while (done <= 0)
         }
       etrn_command = US"exim -R";
       argv = child_exec_exim(CEE_RETURN_ARGV, TRUE, NULL, TRUE, 2, US"-R",
-        smtp_cmd_argument);
+        smtp_cmd_data);
       }
 
     /* If we are host-testing, don't actually do anything. */
@@ -3596,7 +4082,8 @@ while (done <= 0)
         debug_printf("ETRN command is: %s\n", etrn_command);
         debug_printf("ETRN command execution skipped\n");
         }
-      smtp_printf("250 OK\r\n");
+      if (user_msg == NULL) smtp_printf("250 OK\r\n");
+        else smtp_user_msg(US"250", user_msg);
       break;
       }
 
@@ -3606,7 +4093,7 @@ while (done <= 0)
 
     if (smtp_etrn_serialize && !enq_start(etrn_serialize_key))
       {
-      smtp_printf("458 Already processing %s\r\n", smtp_cmd_argument);
+      smtp_printf("458 Already processing %s\r\n", smtp_cmd_data);
       break;
       }
 
@@ -3672,7 +4159,11 @@ while (done <= 0)
       smtp_printf("458 Unable to fork process\r\n");
       if (smtp_etrn_serialize) enq_end(etrn_serialize_key);
       }
-    else smtp_printf("250 OK\r\n");
+    else
+      {
+      if (user_msg == NULL) smtp_printf("250 OK\r\n");
+        else smtp_user_msg(US"250", user_msg);
+      }
 
     signal(SIGCHLD, oldsignal);
     break;
@@ -3694,6 +4185,7 @@ while (done <= 0)
 
 
     case BADSYN_CMD:
+    SYNC_FAILURE:
     if (smtp_inend >= smtp_inbuffer + in_buffer_size)
       smtp_inend = smtp_inbuffer + in_buffer_size - 1;
     c = smtp_inend - smtp_inptr;
@@ -3706,17 +4198,20 @@ while (done <= 0)
       pipelining_advertised? "" : " not",
       smtp_cmd_buffer, host_and_ident(TRUE),
       string_printing(smtp_inptr));
-    smtp_printf("554 SMTP synchronization error\r\n");
+    smtp_notquit_exit(US"synchronization-error", US"554",
+      US"SMTP synchronization error");
     done = 1;   /* Pretend eof - drops connection */
     break;
 
 
     case TOO_MANY_NONMAIL_CMD:
+    s = smtp_cmd_buffer;
+    while (*s != 0 && !isspace(*s)) s++;
     incomplete_transaction_log(US"too many non-mail commands");
     log_write(0, LOG_MAIN|LOG_REJECT, "SMTP call from %s dropped: too many "
       "nonmail commands (last was \"%.*s\")",  host_and_ident(FALSE),
-      smtp_cmd_argument - smtp_cmd_buffer, smtp_cmd_buffer);
-    smtp_printf("554 Too many nonmail commands\r\n");
+      s - smtp_cmd_buffer, smtp_cmd_buffer);
+    smtp_notquit_exit(US"bad-commands", US"554", US"Too many nonmail commands");
     done = 1;   /* Pretend eof - drops connection */
     break;
 
@@ -3729,7 +4224,8 @@ while (done <= 0)
         string_printing(smtp_cmd_buffer), host_and_ident(TRUE),
         US"unrecognized command");
       incomplete_transaction_log(US"unrecognized command");
-      smtp_printf("500 Too many unrecognized commands\r\n");
+      smtp_notquit_exit(US"bad-commands", US"500",
+        US"Too many unrecognized commands");
       done = 2;
       log_write(0, LOG_MAIN|LOG_REJECT, "SMTP call from %s dropped: too many "
         "unrecognized commands (last was \"%s\")", host_and_ident(FALSE),