Generalise "send failed" message in smtp transport
[exim.git] / src / src / transports / smtp.c
index c6099f960b2c1b008e58ac677fe7a2998c9c8e42..7bb1249cc1059c1ef8d5e82efc9b7214f0ed592e 100644 (file)
@@ -84,6 +84,7 @@ optionlist smtp_transport_options[] = {
 #if !defined(DISABLE_TLS) && !defined(DISABLE_OCSP)
   { "hosts_request_ocsp",   opt_stringptr, LOFF(hosts_request_ocsp) },
 #endif
+  { "hosts_require_alpn",   opt_stringptr, LOFF(hosts_require_alpn) },
   { "hosts_require_auth",   opt_stringptr, LOFF(hosts_require_auth) },
 #ifndef DISABLE_TLS
 # ifdef SUPPORT_DANE
@@ -123,6 +124,7 @@ optionlist smtp_transport_options[] = {
   { "socks_proxy",          opt_stringptr, LOFF(socks_proxy) },
 #endif
 #ifndef DISABLE_TLS
+  { "tls_alpn",             opt_stringptr, LOFF(tls_alpn) },
   { "tls_certificate",      opt_stringptr, LOFF(tls_certificate) },
   { "tls_crl",              opt_stringptr, LOFF(tls_crl) },
   { "tls_dh_min_bits",      opt_int,      LOFF(tls_dh_min_bits) },
@@ -192,9 +194,7 @@ smtp_transport_options_block smtp_transport_option_defaults = {
   .keepalive =                 TRUE,
   .retry_include_ip_address =  TRUE,
 #ifndef DISABLE_TLS
-# if defined(SUPPORT_SYSDEFAULT_CABUNDLE) || !defined(USE_GNUTLS)
   .tls_verify_certificates =   US"system",
-# endif
   .tls_dh_min_bits =           EXIM_CLIENT_DH_DEFAULT_MIN_BITS,
   .tls_tempfail_tryclear =     TRUE,
   .tls_try_verify_hosts =      US"*",
@@ -274,6 +274,11 @@ if (!regex_IGNOREQUOTA) regex_IGNOREQUOTA =
 if (!regex_EARLY_PIPE) regex_EARLY_PIPE =
   regex_must_compile(US"\\n250[\\s\\-]" EARLY_PIPE_FEATURE_NAME "(\\s|\\n|$)", FALSE, TRUE);
 #endif
+
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+if (!regex_LIMITS) regex_LIMITS =
+  regex_must_compile(US"\\n250[\\s\\-]LIMITS\\s", FALSE, TRUE);
+#endif
 }
 
 
@@ -350,6 +355,7 @@ void
 smtp_transport_init(transport_instance *tblock)
 {
 smtp_transport_options_block *ob = SOB tblock->options_block;
+int old_pool = store_pool;
 
 /* Retry_use_local_part defaults FALSE if unset */
 
@@ -385,7 +391,9 @@ if (ob->hosts_override && ob->hosts) tblock->overrides_hosts = TRUE;
 /* If there are any fallback hosts listed, build a chain of host items
 for them, but do not do any lookups at this time. */
 
-host_build_hostlist(&(ob->fallback_hostlist), ob->fallback_hosts, FALSE);
+store_pool = POOL_PERM;
+host_build_hostlist(&ob->fallback_hostlist, ob->fallback_hosts, FALSE);
+store_pool = old_pool;
 }
 
 
@@ -756,6 +764,82 @@ return TRUE;
 }
 
 
+/******************************************************************************/
+
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+/* If TLS, or TLS not offered, called with the EHLO response in the buffer.
+Check it for a LIMITS keyword and parse values into the smtp context structure.
+
+We don't bother with peers that we won't talk TLS to, even though they can,
+just ignore their LIMITS advice (if any) and treat them as if they do not.
+This saves us dealing with a duplicate set of values. */
+
+static void
+ehlo_response_limits_read(smtp_context * sx)
+{
+int ovec[3];   /* results vector for a main-match only */
+
+/* matches up to just after the first space after the keyword */
+
+if (pcre_exec(regex_LIMITS, NULL, CS sx->buffer, Ustrlen(sx->buffer),
+             0, PCRE_EOPT, ovec, nelem(ovec)) >= 0)
+  for (const uschar * s = sx->buffer + ovec[1]; *s; )
+    {
+    while (isspace(*s)) s++;
+    if (*s == '\n') break;
+
+    if (strncmpic(s, US"MAILMAX=", 8) == 0)
+      {
+      sx->peer_limit_mail = atoi(CS (s += 8));
+      while (isdigit(*s)) s++;
+      }
+    else if (strncmpic(s, US"RCPTMAX=", 8) == 0)
+      {
+      sx->peer_limit_rcpt = atoi(CS (s += 8));
+      while (isdigit(*s)) s++;
+      }
+    else if (strncmpic(s, US"RCPTDOMAINMAX=", 14) == 0)
+      {
+      sx->peer_limit_rcptdom = atoi(CS (s += 14));
+      while (isdigit(*s)) s++;
+      }
+    else
+      while (*s && !isspace(*s)) s++;
+    }
+}
+
+/* Apply given values to the current connection */
+static void
+ehlo_limits_apply(smtp_context * sx,
+  unsigned limit_mail, unsigned limit_rcpt, unsigned limit_rcptdom)
+{
+if (limit_mail && limit_mail < sx->max_mail) sx->max_mail = limit_mail;
+if (limit_rcpt && limit_rcpt < sx->max_rcpt) sx->max_rcpt = limit_rcpt;
+if (limit_rcptdom)
+  {
+  DEBUG(D_transport) debug_printf("will treat as !multi_domain\n");
+  sx->single_rcpt_domain = TRUE;
+  }
+}
+
+/* Apply values from EHLO-resp to the current connection */
+static void
+ehlo_response_limits_apply(smtp_context * sx)
+{
+ehlo_limits_apply(sx, sx->peer_limit_mail, sx->peer_limit_rcpt,
+  sx->peer_limit_rcptdom);
+}
+
+/* Apply values read from cache to the current connection */
+static void
+ehlo_cache_limits_apply(smtp_context * sx)
+{
+ehlo_limits_apply(sx, sx->ehlo_resp.limit_mail, sx->ehlo_resp.limit_rcpt,
+  sx->ehlo_resp.limit_rcptdom);
+}
+#endif
+
+/******************************************************************************/
 
 #ifndef DISABLE_PIPE_CONNECT
 static uschar *
@@ -769,19 +853,45 @@ return Ustrchr(host->address, ':')
     host->port == PORT_NONE ? sx->port : host->port);
 }
 
+/* Cache EHLO-response info for use by early-pipe.
+Called
+- During a normal flow on EHLO response (either cleartext or under TLS),
+  when we are willing to do PIPE_CONNECT and it is offered
+- During an early-pipe flow on receiving the actual EHLO response and noting
+  disparity versus the cached info used, when PIPE_CONNECT is still being offered
+
+We assume that suitable values have been set in the sx.ehlo_resp structure for
+features and auths; we handle the copy of limits. */
+
 static void
-write_ehlo_cache_entry(const smtp_context * sx)
+write_ehlo_cache_entry(smtp_context * sx)
 {
 open_db dbblock, * dbm_file;
 
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+sx->ehlo_resp.limit_mail = sx->peer_limit_mail;
+sx->ehlo_resp.limit_rcpt = sx->peer_limit_rcpt;
+sx->ehlo_resp.limit_rcptdom = sx->peer_limit_rcptdom;
+#endif
+
 if ((dbm_file = dbfn_open(US"misc", O_RDWR, &dbblock, TRUE, TRUE)))
   {
   uschar * ehlo_resp_key = ehlo_cache_key(sx);
   dbdata_ehlo_resp er = { .data = sx->ehlo_resp };
 
-  HDEBUG(D_transport) debug_printf("writing clr %04x/%04x cry %04x/%04x\n",
-    sx->ehlo_resp.cleartext_features, sx->ehlo_resp.cleartext_auths,
-    sx->ehlo_resp.crypted_features, sx->ehlo_resp.crypted_auths);
+  HDEBUG(D_transport)
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+    if (sx->ehlo_resp.limit_mail || sx->ehlo_resp.limit_rcpt || sx->ehlo_resp.limit_rcptdom)
+      debug_printf("writing clr %04x/%04x cry %04x/%04x lim %05d/%05d/%05d\n",
+       sx->ehlo_resp.cleartext_features, sx->ehlo_resp.cleartext_auths,
+       sx->ehlo_resp.crypted_features, sx->ehlo_resp.crypted_auths,
+       sx->ehlo_resp.limit_mail, sx->ehlo_resp.limit_rcpt,
+       sx->ehlo_resp.limit_rcptdom);
+    else
+#endif
+      debug_printf("writing clr %04x/%04x cry %04x/%04x\n",
+       sx->ehlo_resp.cleartext_features, sx->ehlo_resp.cleartext_auths,
+       sx->ehlo_resp.crypted_features, sx->ehlo_resp.crypted_auths);
 
   dbfn_write(dbm_file, ehlo_resp_key, &er, (int)sizeof(er));
   dbfn_close(dbm_file);
@@ -826,12 +936,26 @@ else
     }
   else
     {
+    DEBUG(D_transport)
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+      if (er->data.limit_mail || er->data.limit_rcpt || er->data.limit_rcptdom)
+       debug_printf("EHLO response bits from cache:"
+         " cleartext 0x%04x/0x%04x crypted 0x%04x/0x%04x lim %05d/%05d/%05d\n",
+         er->data.cleartext_features, er->data.cleartext_auths,
+         er->data.crypted_features, er->data.crypted_auths,
+         er->data.limit_mail, er->data.limit_rcpt, er->data.limit_rcptdom);
+      else
+#endif
+       debug_printf("EHLO response bits from cache:"
+         " cleartext 0x%04x/0x%04x crypted 0x%04x/0x%04x\n",
+         er->data.cleartext_features, er->data.cleartext_auths,
+         er->data.crypted_features, er->data.crypted_auths);
+
     sx->ehlo_resp = er->data;
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+    ehlo_cache_limits_apply(sx);
+#endif
     dbfn_close(dbm_file);
-    DEBUG(D_transport) debug_printf(
-       "EHLO response bits from cache: cleartext 0x%04x/0x%04x crypted 0x%04x/0x%04x\n",
-       er->data.cleartext_features, er->data.cleartext_auths,
-       er->data.crypted_features, er->data.crypted_auths);
     return TRUE;
     }
   dbfn_close(dbm_file);
@@ -933,8 +1057,9 @@ if (pending_EHLO)
     goto fail;
     }
 
-  /* Compare the actual EHLO response to the cached value we assumed;
-  on difference, dump or rewrite the cache and arrange for a retry. */
+  /* Compare the actual EHLO response extensions and AUTH methods to the cached
+  value we assumed; on difference, dump or rewrite the cache and arrange for a
+  retry. */
 
   ap = tls_out.active.sock < 0
       ? &sx->ehlo_resp.cleartext_auths : &sx->ehlo_resp.crypted_auths;
@@ -944,6 +1069,10 @@ if (pending_EHLO)
        | OPTION_CHUNKING | OPTION_PRDR | OPTION_DSN | OPTION_PIPE | OPTION_SIZE
        | OPTION_UTF8 | OPTION_EARLY_PIPE
        );
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+  if (tls_out.active.sock >= 0 || !(peer_offered & OPTION_TLS))
+    ehlo_response_limits_read(sx);
+#endif
   if (  peer_offered != sx->peer_offered
      || (authbits = study_ehlo_auths(sx)) != *ap)
     {
@@ -951,16 +1080,44 @@ if (pending_EHLO)
       debug_printf("EHLO %s extensions changed, 0x%04x/0x%04x -> 0x%04x/0x%04x\n",
                    tls_out.active.sock < 0 ? "cleartext" : "crypted",
                    sx->peer_offered, *ap, peer_offered, authbits);
-    *(tls_out.active.sock < 0
-      ? &sx->ehlo_resp.cleartext_features : &sx->ehlo_resp.crypted_features) = peer_offered;
-    *ap = authbits;
     if (peer_offered & OPTION_EARLY_PIPE)
+      {
+      *(tls_out.active.sock < 0
+       ? &sx->ehlo_resp.cleartext_features : &sx->ehlo_resp.crypted_features) =
+         peer_offered;
+      *ap = authbits;
       write_ehlo_cache_entry(sx);
+      }
     else
       invalidate_ehlo_cache_entry(sx);
 
     return OK;         /* just carry on */
     }
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+    /* If we are handling LIMITS, compare the actual EHLO LIMITS values with the
+    cached values and invalidate cache if different.  OK to carry on with
+    connect since values are advisory. */
+    {
+    if (  (tls_out.active.sock >= 0 || !(peer_offered & OPTION_TLS))
+       && (  sx->peer_limit_mail != sx->ehlo_resp.limit_mail
+          || sx->peer_limit_rcpt != sx->ehlo_resp.limit_rcpt
+          || sx->peer_limit_rcptdom != sx->ehlo_resp.limit_rcptdom
+       )  )
+      {
+      HDEBUG(D_transport)
+       {
+       debug_printf("EHLO LIMITS changed:");
+       if (sx->peer_limit_mail != sx->ehlo_resp.limit_mail)
+         debug_printf(" MAILMAX %u -> %u\n", sx->ehlo_resp.limit_mail, sx->peer_limit_mail);
+       else if (sx->peer_limit_rcpt != sx->ehlo_resp.limit_rcpt)
+         debug_printf(" RCPTMAX %u -> %u\n", sx->ehlo_resp.limit_rcpt, sx->peer_limit_rcpt);
+       else
+         debug_printf(" RCPTDOMAINMAX %u -> %u\n", sx->ehlo_resp.limit_rcptdom, sx->peer_limit_rcptdom);
+       }
+      invalidate_ehlo_cache_entry(sx);
+      }
+    }
+#endif
   }
 return OK;
 
@@ -1827,15 +1984,13 @@ return OK;
 
 
 
-
-
 /*************************************************
 *       Make connection for given message        *
 *************************************************/
 
 /*
 Arguments:
-  ctx            connection context
+  sx             connection context
   suppress_tls    if TRUE, don't attempt a TLS connection - this is set for
                     a second attempt after TLS initialization fails
 
@@ -1884,7 +2039,8 @@ sx->dane_required =
 /* sx->pending_BANNER = sx->pending_EHLO = sx->pending_MAIL = FALSE; */
 #endif
 
-if ((sx->max_rcpt = sx->conn_args.tblock->max_addresses) == 0) sx->max_rcpt = 999999;
+if ((sx->max_mail = sx->conn_args.tblock->connection_max_messages) == 0) sx->max_mail = 999999;
+if ((sx->max_rcpt = sx->conn_args.tblock->max_addresses) == 0)           sx->max_rcpt = 999999;
 /* sx->peer_offered = 0; */
 /* sx->avoid_option = 0; */
 sx->igquotstr = US"";
@@ -1965,7 +2121,7 @@ if (continue_hostname && continue_proxy_cipher)
       {
       case OK:         sx->conn_args.dane = TRUE;
                        ob->tls_tempfail_tryclear = FALSE;      /* force TLS */
-                       ob->tls_sni = sx->first_addr->domain;   /* force SNI */
+                        ob->tls_sni = sx->conn_args.host->name; /* force SNI */
                        break;
       case FAIL_FORCED:        break;
       default:         set_errno_nohost(sx->addrlist, ERRNO_DNSDEFER,
@@ -2027,6 +2183,11 @@ if (!continue_hostname)
   if (sx->verify)
     HDEBUG(D_verify) debug_printf("interface=%s port=%d\n", sx->conn_args.interface, sx->port);
 
+  /* Arrange to report to calling process this is a new connection */
+
+  clearflag(sx->first_addr, af_cont_conn);
+  setflag(sx->first_addr, af_new_conn);
+
   /* Get the actual port the connection will use, into sx->conn_args.host */
 
   smtp_port_for_connect(sx->conn_args.host, sx->port);
@@ -2047,7 +2208,7 @@ if (!continue_hostname)
          {
          case OK:              sx->conn_args.dane = TRUE;
                                ob->tls_tempfail_tryclear = FALSE;      /* force TLS */
-                               ob->tls_sni = sx->first_addr->domain;   /* force SNI */
+                               ob->tls_sni = sx->conn_args.host->name; /* force SNI */
                                break;
          case FAIL_FORCED:     break;
          default:              set_errno_nohost(sx->addrlist, ERRNO_DNSDEFER,
@@ -2080,6 +2241,9 @@ if (!continue_hostname)
 
   sx->cctx.tls_ctx = NULL;
   sx->inblock.cctx = sx->outblock.cctx = &sx->cctx;
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+  sx->peer_limit_mail = sx->peer_limit_rcpt = sx->peer_limit_rcptdom =
+#endif
   sx->avoid_option = sx->peer_offered = smtp_peer_options = 0;
 
 #ifndef DISABLE_PIPE_CONNECT
@@ -2314,8 +2478,8 @@ goto SEND_QUIT;
       int n = sizeof(sx->buffer);
       uschar * rsp = sx->buffer;
 
-      if (sx->esmtp_sent && (n = Ustrlen(sx->buffer)) < sizeof(sx->buffer)/2)
-       { rsp = sx->buffer + n + 1; n = sizeof(sx->buffer) - n; }
+      if (sx->esmtp_sent && (n = Ustrlen(sx->buffer) + 1) < sizeof(sx->buffer)/2)
+       { rsp = sx->buffer + n; n = sizeof(sx->buffer) - n; }
 
       if (smtp_write_command(sx, SCMD_FLUSH, "HELO %s\r\n", sx->helo_data) < 0)
        goto SEND_FAILED;
@@ -2358,6 +2522,13 @@ goto SEND_QUIT;
          )
 #endif
        );
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+      if (tls_out.active.sock >= 0 || !(sx->peer_offered & OPTION_TLS))
+       {
+       ehlo_response_limits_read(sx);
+       ehlo_response_limits_apply(sx);
+       }
+#endif
 #ifndef DISABLE_PIPE_CONNECT
       if (sx->early_pipe_ok)
        {
@@ -2412,6 +2583,13 @@ else
   sx->inblock.cctx = sx->outblock.cctx = &sx->cctx;
   smtp_command = big_buffer;
   sx->peer_offered = smtp_peer_options;
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+  /* Limits passed by cmdline over exec. */
+  ehlo_limits_apply(sx,
+                   sx->peer_limit_mail = continue_limit_mail,
+                   sx->peer_limit_rcpt = continue_limit_rcpt,
+                   sx->peer_limit_rcptdom = continue_limit_rcptdom);
+#endif
   sx->helo_data = NULL;                /* ensure we re-expand ob->helo_data */
 
   /* For a continued connection with TLS being proxied for us, or a
@@ -2453,7 +2631,8 @@ if (  smtp_peer_options & OPTION_TLS
 
 #ifndef DISABLE_PIPE_CONNECT
   /* If doing early-pipelining reap the banner and EHLO-response but leave
-  the response for the STARTTLS we just sent alone. */
+  the response for the STARTTLS we just sent alone.  On fail, assume wrong
+  cached capability and retry with the pipelining disabled. */
 
   if (sx->early_pipe_active && sync_responses(sx, 2, 0) != 0)
     {
@@ -2589,6 +2768,13 @@ if (tls_out.active.sock >= 0)
       DEBUG(D_transport) debug_printf("Using cached crypted PIPE_CONNECT\n");
     }
 #endif
+#ifdef EXPERIMMENTAL_ESMTP_LIMITS
+  /* As we are about to send another EHLO, forget any LIMITS received so far. */
+  sx->peer_limit_mail = sx->peer_limit_rcpt = sx->peer_limit_rcptdom = 0;
+  if ((sx->max_mail = sx->conn_args.tblock->connection_max_message) == 0) sx->max_mail = 999999;
+  if ((sx->max_rcpt = sx->conn_args.tblock->max_addresses) == 0)          sx->max_rcpt = 999999;
+  sx->single_rcpt_domain = FALSE;
+#endif
 
   /* For SMTPS we need to wait for the initial OK response. */
   if (sx->smtps)
@@ -2718,6 +2904,14 @@ if (   !continue_hostname
     if (tls_out.active.sock >= 0)
       sx->ehlo_resp.crypted_features = sx->peer_offered;
 #endif
+
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+    if (tls_out.active.sock >= 0 || !(sx->peer_offered & OPTION_TLS))
+      {
+      ehlo_response_limits_read(sx);
+      ehlo_response_limits_apply(sx);
+      }
+#endif
     }
 
     /* Set for IGNOREQUOTA if the response to LHLO specifies support and the
@@ -2885,7 +3079,7 @@ return OK;
 
   SEND_FAILED:
     code = '4';
-    message = US string_sprintf("send() to %s [%s] failed: %s",
+    message = US string_sprintf("smtp send to %s [%s] failed: %s",
       sx->conn_args.host->name, sx->conn_args.host->address, strerror(errno));
     sx->send_quit = FALSE;
     yield = DEFER;
@@ -3051,7 +3245,7 @@ if (  sx->peer_offered & OPTION_UTF8
 
 /* check if all addresses have DSN-lasthop flag; do not send RET and ENVID if so */
 for (sx->dsn_all_lasthop = TRUE, addr = addrlist, address_count = 0;
-     addr && address_count < sx->max_rcpt;
+     addr && address_count < sx->max_rcpt;     /*XXX maybe also || sx->single_rcpt_domain ? */
      addr = addr->next) if (addr->transport_return == PENDING_DEFER)
   {
   address_count++;
@@ -3139,6 +3333,9 @@ int
 smtp_write_mail_and_rcpt_cmds(smtp_context * sx, int * yield)
 {
 address_item * addr;
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+address_item * restart_addr = NULL;
+#endif
 int address_count, pipe_limit;
 int rc;
 
@@ -3223,13 +3420,31 @@ that max_rcpt will be large, so all addresses will be done at once.
 For verify we flush the pipeline after any (the only) rcpt address. */
 
 for (addr = sx->first_addr, address_count = 0, pipe_limit = 100;
-     addr  &&  address_count < sx->max_rcpt;
+     addr &&  address_count < sx->max_rcpt;
      addr = addr->next) if (addr->transport_return == PENDING_DEFER)
   {
   int cmds_sent;
   BOOL no_flush;
   uschar * rcpt_addr;
 
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+  if (  sx->single_rcpt_domain                                 /* restriction on domains */
+     && address_count > 0                                      /* not first being sent */
+     && Ustrcmp(addr->domain, sx->first_addr->domain) != 0     /* dom diff from first */
+     )
+    {
+    DEBUG(D_transport) debug_printf("skipping different domain %s\n", addr->domain);
+
+    /* Ensure the smtp-response reaper does not think the address had a RCPT
+    command sent for it.  Reset to PENDING_DEFER in smtp_deliver(), where we
+    goto SEND_MESSAGE.  */
+
+    addr->transport_return = SKIP;
+    if (!restart_addr) restart_addr = addr;    /* note restart point */
+    continue;                                  /* skip this one */
+    }
+#endif
+
   addr->dsn_aware = sx->peer_offered & OPTION_DSN
     ? dsn_support_yes : dsn_support_no;
 
@@ -3301,7 +3516,11 @@ for (addr = sx->first_addr, address_count = 0, pipe_limit = 100;
     }
   }      /* Loop for next address */
 
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+sx->next_addr = restart_addr ? restart_addr : addr;
+#else
 sx->next_addr = addr;
+#endif
 return 0;
 }
 
@@ -3391,13 +3610,12 @@ for (int fd_bits = 3; fd_bits; )
       timeout = 5;
       }
     else
-      {
       for (int nbytes = 0; rc - nbytes > 0; nbytes += i)
        if ((i = write(pfd[0], buf + nbytes, rc - nbytes)) < 0) goto done;
-      }
 
   /* Handle outbound data.  We cannot combine payload and the TLS-close
-  due to the limitations of the (pipe) channel feeding us. */
+  due to the limitations of the (pipe) channel feeding us.  Maybe use a unix-domain
+  socket? */
   if (FD_ISSET(pfd[0], &rfds))
     if ((rc = read(pfd[0], buf, bsize)) <= 0)
       {
@@ -3412,11 +3630,9 @@ for (int fd_bits = 3; fd_bits; )
       shutdown(tls_out.active.sock, SHUT_WR);
       }
     else
-      {
       for (int nbytes = 0; rc - nbytes > 0; nbytes += i)
        if ((i = tls_write(ct_ctx, buf + nbytes, rc - nbytes, FALSE)) < 0)
          goto done;
-      }
 
   if (fd_bits & 1) FD_SET(tls_out.active.sock, &rfds);
   if (fd_bits & 2) FD_SET(pfd[0], &rfds);
@@ -3490,10 +3706,13 @@ int yield = OK;
 int save_errno;
 int rc;
 
-BOOL pass_message = FALSE;
 uschar *message = NULL;
 uschar new_message_id[MESSAGE_ID_LENGTH + 1];
 smtp_context * sx = store_get(sizeof(*sx), TRUE);      /* tainted, for the data buffers */
+BOOL pass_message = FALSE;
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+BOOL mail_limit = FALSE;
+#endif
 #ifdef SUPPORT_DANE
 BOOL dane_held;
 #endif
@@ -3513,8 +3732,8 @@ sx->conn_args.tblock = tblock;
 gettimeofday(&sx->delivery_start, NULL);
 sx->sync_addr = sx->first_addr = addrlist;
 
+REPEAT_CONN:
 #ifdef SUPPORT_DANE
-DANE_DOMAINS:
 dane_held = FALSE;
 #endif
 
@@ -3883,7 +4102,7 @@ else
       sx->send_quit = FALSE;   /* avoid sending it later */
 
 #ifndef DISABLE_TLS
-      if (sx->cctx.tls_ctx)    /* need to send TLS Cloe Notify */
+      if (sx->cctx.tls_ctx)    /* need to send TLS Close Notify */
        {
 # ifdef EXIM_TCP_CORK          /* Use _CORK to get Close Notify in FIN segment */
        (void) setsockopt(sx->cctx.sock, IPPROTO_TCP, EXIM_TCP_CORK, US &on, sizeof(on));
@@ -4087,7 +4306,7 @@ else
         else
           sprintf(CS sx->buffer, "%.500s\n", addr->unique);
 
-        DEBUG(D_deliver) debug_printf("S:journalling %s\n", sx->buffer);
+        DEBUG(D_deliver) debug_printf("S:journalling %s", sx->buffer);
         len = Ustrlen(CS sx->buffer);
         if (write(journal_fd, sx->buffer, len) != len)
           log_write(0, LOG_MAIN|LOG_PANIC, "failed to write journal for "
@@ -4181,7 +4400,7 @@ if (!sx->ok)
     {
     save_errno = errno;
     code = '4';
-    message = string_sprintf("send() to %s [%s] failed: %s",
+    message = string_sprintf("smtp send to %s [%s] failed: %s",
       host->name, host->address, message ? message : US strerror(save_errno));
     sx->send_quit = FALSE;
     goto FAILED;
@@ -4332,79 +4551,95 @@ DEBUG(D_transport)
     sx->send_rset, f.continue_more, yield, sx->first_addr ? "not " : "");
 
 if (sx->completed_addr && sx->ok && sx->send_quit)
-  {
-  smtp_compare_t t_compare =
-    {.tblock = tblock, .current_sender_address = sender_address};
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+  if (mail_limit = continue_sequence >= sx->max_mail)
+    {
+    DEBUG(D_transport)
+      debug_printf("reached limit %u for MAILs per conn\n", sx->max_mail);
+    }
+  else
+#endif
+    {
+    smtp_compare_t t_compare =
+      {.tblock = tblock, .current_sender_address = sender_address};
 
-  if (  sx->first_addr                 /* more addrs for this message */
-     || f.continue_more                        /* more addrs for continued-host */
-     || tcw_done && tcw                        /* more messages for host */
-     || (
+    if (  sx->first_addr                       /* more addrs for this message */
+       || f.continue_more                      /* more addrs for continued-host */
+       || tcw_done && tcw                      /* more messages for host */
+       || (
 #ifndef DISABLE_TLS
-          (  tls_out.active.sock < 0  &&  !continue_proxy_cipher
-           || verify_check_given_host(CUSS &ob->hosts_nopass_tls, host) != OK
-          )
-        &&
+            (  tls_out.active.sock < 0  &&  !continue_proxy_cipher
+            || verify_check_given_host(CUSS &ob->hosts_nopass_tls, host) != OK
+            )
+         &&
 #endif
-           transport_check_waiting(tblock->name, host->name,
-             tblock->connection_max_messages, new_message_id,
-            (oicf)smtp_are_same_identities, (void*)&t_compare)
-     )  )
-    {
-    uschar *msg;
-    BOOL pass_message;
+            transport_check_waiting(tblock->name, host->name,
+              sx->max_mail, new_message_id,
+              (oicf)smtp_are_same_identities, (void*)&t_compare)
+       )  )
+      {
+      uschar *msg;
+      BOOL pass_message;
 
-    if (sx->send_rset)
-      if (! (sx->ok = smtp_write_command(sx, SCMD_FLUSH, "RSET\r\n") >= 0))
-        {
-        msg = US string_sprintf("send() to %s [%s] failed: %s", host->name,
-          host->address, strerror(errno));
-        sx->send_quit = FALSE;
-        }
-      else if (! (sx->ok = smtp_read_response(sx, sx->buffer, sizeof(sx->buffer),
-                 '2', ob->command_timeout)))
-        {
-        int code;
-        sx->send_quit = check_response(host, &errno, 0, sx->buffer, &code, &msg,
-          &pass_message);
-        if (!sx->send_quit)
-          {
-          DEBUG(D_transport) debug_printf("H=%s [%s] %s\n",
-           host->name, host->address, msg);
-          }
-        }
+      if (sx->send_rset)
+       if (! (sx->ok = smtp_write_command(sx, SCMD_FLUSH, "RSET\r\n") >= 0))
+         {
+         msg = US string_sprintf("smtp send to %s [%s] failed: %s", host->name,
+           host->address, strerror(errno));
+         sx->send_quit = FALSE;
+         }
+       else if (! (sx->ok = smtp_read_response(sx, sx->buffer, sizeof(sx->buffer),
+                   '2', ob->command_timeout)))
+         {
+         int code;
+         sx->send_quit = check_response(host, &errno, 0, sx->buffer, &code, &msg,
+           &pass_message);
+         if (!sx->send_quit)
+           {
+           DEBUG(D_transport) debug_printf("H=%s [%s] %s\n",
+             host->name, host->address, msg);
+           }
+         }
 
-    /* Either RSET was not needed, or it succeeded */
+      /* Either RSET was not needed, or it succeeded */
 
-    if (sx->ok)
-      {
+      if (sx->ok)
+       {
 #ifndef DISABLE_TLS
-      int pfd[2];
-#endif
-      int socket_fd = sx->cctx.sock;
-
-      if (sx->first_addr)              /* More addresses still to be sent */
-        {                              /*   for this message              */
-        continue_sequence++;           /* Causes * in logging */
-       pipelining_active = sx->pipelining_used;    /* was cleared at DATA */
-        goto SEND_MESSAGE;
-        }
+       int pfd[2];
+#endif
+       int socket_fd = sx->cctx.sock;
+
+       if (sx->first_addr)             /* More addresses still to be sent */
+         {                             /*   for this message              */
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+         /* Any that we marked as skipped, reset to do now */
+         for (address_item * a = sx->first_addr; a; a = a->next)
+           if (a->transport_return == SKIP)
+             a->transport_return = PENDING_DEFER;
+#endif
+         continue_sequence++;                          /* for consistency */
+         clearflag(sx->first_addr, af_new_conn);
+         setflag(sx->first_addr, af_cont_conn);        /* Causes * in logging */
+         pipelining_active = sx->pipelining_used;      /* was cleared at DATA */
+         goto SEND_MESSAGE;
+         }
 
-      /* Unless caller said it already has more messages listed for this host,
-      pass the connection on to a new Exim process (below, the call to
-      transport_pass_socket).  If the caller has more ready, just return with
-      the connection still open. */
+       /* Unless caller said it already has more messages listed for this host,
+       pass the connection on to a new Exim process (below, the call to
+       transport_pass_socket).  If the caller has more ready, just return with
+       the connection still open. */
 
 #ifndef DISABLE_TLS
-      if (tls_out.active.sock >= 0)
-       if (  f.continue_more
-          || verify_check_given_host(CUSS &ob->hosts_noproxy_tls, host) == OK)
-         {
-         /* Before passing the socket on, or returning to caller with it still
-         open, we must shut down TLS.  Not all MTAs allow for the continuation
-         of the SMTP session when TLS is shut down. We test for this by sending
-         a new EHLO. If we don't get a good response, we don't attempt to pass
-         the socket on. */
+       if (tls_out.active.sock >= 0)
+         if (  f.continue_more
+            || verify_check_given_host(CUSS &ob->hosts_noproxy_tls, host) == OK)
+           {
+           /* Before passing the socket on, or returning to caller with it still
+           open, we must shut down TLS.  Not all MTAs allow for the continuation
+           of the SMTP session when TLS is shut down. We test for this by sending
+           a new EHLO. If we don't get a good response, we don't attempt to pass
+           the socket on. */
 
          tls_close(sx->cctx.tls_ctx, TLS_SHUTDOWN_WAIT);
          sx->send_tlsclose = FALSE;
@@ -4417,84 +4652,88 @@ if (sx->completed_addr && sx->ok && sx->send_quit)
            && smtp_read_response(sx, sx->buffer, sizeof(sx->buffer),
                                      '2', ob->command_timeout);
 
-         if (sx->ok && f.continue_more)
-           goto TIDYUP;                /* More addresses for another run */
-         }
-       else
-         {
-         /* Set up a pipe for proxying TLS for the new transport process */
-
-         smtp_peer_options |= OPTION_TLS;
-         if ((sx->ok = socketpair(AF_UNIX, SOCK_STREAM, 0, pfd) == 0))
-           socket_fd = pfd[1];
+           if (sx->ok && f.continue_more)
+             goto TIDYUP;              /* More addresses for another run */
+           }
          else
-           set_errno(sx->first_addr, errno, US"internal allocation problem",
-                   DEFER, FALSE, host,
+           {
+           /* Set up a pipe for proxying TLS for the new transport process */
+
+           smtp_peer_options |= OPTION_TLS;
+           if ((sx->ok = socketpair(AF_UNIX, SOCK_STREAM, 0, pfd) == 0))
+             socket_fd = pfd[1];
+           else
+             set_errno(sx->first_addr, errno, US"internal allocation problem",
+                     DEFER, FALSE, host,
 # ifdef EXPERIMENTAL_DSN_INFO
-                   sx->smtp_greeting, sx->helo_response,
+                     sx->smtp_greeting, sx->helo_response,
 # endif
-                   &sx->delivery_start);
-         }
-      else
+                     &sx->delivery_start);
+           }
+       else
 #endif
-       if (f.continue_more)
-         goto TIDYUP;                  /* More addresses for another run */
+         if (f.continue_more)
+           goto TIDYUP;                        /* More addresses for another run */
 
-      /* If the socket is successfully passed, we mustn't send QUIT (or
-      indeed anything!) from here. */
+       /* If the socket is successfully passed, we mustn't send QUIT (or
+       indeed anything!) from here. */
 
-/*XXX DSN_INFO: assume likely to do new HELO; but for greet we'll want to
-propagate it from the initial
-*/
-      if (sx->ok && transport_pass_socket(tblock->name, host->name,
-           host->address, new_message_id, socket_fd))
-       {
-        sx->send_quit = FALSE;
-
-       /* We have passed the client socket to a fresh transport process.
-       If TLS is still active, we need to proxy it for the transport we
-       just passed the baton to.  Fork a child to to do it, and return to
-       get logging done asap.  Which way to place the work makes assumptions
-       about post-fork prioritisation which may not hold on all platforms. */
-#ifndef DISABLE_TLS
-       if (tls_out.active.sock >= 0)
+  /*XXX DSN_INFO: assume likely to do new HELO; but for greet we'll want to
+  propagate it from the initial
+  */
+       if (sx->ok && transport_pass_socket(tblock->name, host->name,
+             host->address, new_message_id, socket_fd
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+             , sx->peer_limit_mail, sx->peer_limit_rcpt, sx->peer_limit_rcptdom
+#endif
+             ))
          {
-         int pid = exim_fork(US"tls-proxy-interproc");
-         if (pid == 0)         /* child; fork again to disconnect totally */
-           {
-           /* does not return */
-           smtp_proxy_tls(sx->cctx.tls_ctx, sx->buffer, sizeof(sx->buffer), pfd,
-                           ob->command_timeout);
-           }
+         sx->send_quit = FALSE;
 
-         if (pid > 0)          /* parent */
+         /* We have passed the client socket to a fresh transport process.
+         If TLS is still active, we need to proxy it for the transport we
+         just passed the baton to.  Fork a child to to do it, and return to
+         get logging done asap.  Which way to place the work makes assumptions
+         about post-fork prioritisation which may not hold on all platforms. */
+#ifndef DISABLE_TLS
+         if (tls_out.active.sock >= 0)
            {
-           close(pfd[0]);
-           /* tidy the inter-proc to disconn the proxy proc */
-           waitpid(pid, NULL, 0);
-           tls_close(sx->cctx.tls_ctx, TLS_NO_SHUTDOWN);
-           sx->cctx.tls_ctx = NULL;
-           (void)close(sx->cctx.sock);
-           sx->cctx.sock = -1;
-           continue_transport = NULL;
-           continue_hostname = NULL;
-           goto TIDYUP;
+           int pid = exim_fork(US"tls-proxy-interproc");
+           if (pid == 0)               /* child; fork again to disconnect totally */
+             {
+             /* does not return */
+             smtp_proxy_tls(sx->cctx.tls_ctx, sx->buffer, sizeof(sx->buffer), pfd,
+                             ob->command_timeout);
+             }
+
+           if (pid > 0)                /* parent */
+             {
+             close(pfd[0]);
+             /* tidy the inter-proc to disconn the proxy proc */
+             waitpid(pid, NULL, 0);
+             tls_close(sx->cctx.tls_ctx, TLS_NO_SHUTDOWN);
+             sx->cctx.tls_ctx = NULL;
+             (void)close(sx->cctx.sock);
+             sx->cctx.sock = -1;
+             continue_transport = NULL;
+             continue_hostname = NULL;
+             goto TIDYUP;
+             }
+           log_write(0, LOG_PANIC_DIE, "fork failed");
            }
-         log_write(0, LOG_PANIC_DIE, "fork failed");
-         }
 #endif
+         }
        }
-      }
 
-    /* If RSET failed and there are addresses left, they get deferred. */
-    else
-      set_errno(sx->first_addr, errno, msg, DEFER, FALSE, host,
+      /* If RSET failed and there are addresses left, they get deferred. */
+      else
+       set_errno(sx->first_addr, errno, msg, DEFER, FALSE, host,
 #ifdef EXPERIMENTAL_DSN_INFO
-                 sx->smtp_greeting, sx->helo_response,
+                   sx->smtp_greeting, sx->helo_response,
 #endif
-                 &sx->delivery_start);
+                   &sx->delivery_start);
+      }
     }
-  }
 
 /* End off tidily with QUIT unless the connection has died or the socket has
 been passed to another process. */
@@ -4580,6 +4819,9 @@ if (sx->send_quit || tcw_done && !tcw)
   }
 HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP(close)>>\n");
 (void)close(sx->cctx.sock);
+sx->cctx.sock = -1;
+continue_transport = NULL;
+continue_hostname = NULL;
 
 #ifndef DISABLE_EVENT
 (void) event_raise(tblock->event_action, US"tcp:close", NULL);
@@ -4599,15 +4841,30 @@ if (dane_held)
        to get the domain string for SNI */
 
        sx->first_addr = a;
+       clearflag(a, af_cont_conn);
+       setflag(a, af_new_conn);                /* clear * from logging */
        DEBUG(D_transport) debug_printf("DANE: go-around for %s\n", a->domain);
        }
       }
-  goto DANE_DOMAINS;
+  continue_sequence = 1;                       /* for consistency */
+  goto REPEAT_CONN;
+  }
+#endif
+
+#ifdef EXPERIMENTAL_ESMTP_LIMITS
+if (mail_limit && sx->first_addr)
+  {
+  /* Reset the sequence count since we closed the connection.  This is flagged
+  on the pipe back to the delivery process so that a non-continued-conn delivery
+  is logged. */
+
+  continue_sequence = 1;                       /* for consistency */
+  clearflag(sx->first_addr, af_cont_conn);
+  setflag(sx->first_addr, af_new_conn);                /* clear  * from logging */
+  goto REPEAT_CONN;
   }
 #endif
 
-continue_transport = NULL;
-continue_hostname = NULL;
 return yield;
 
 TIDYUP:
@@ -4838,11 +5095,8 @@ if (!hostlist || (ob->hosts_override && ob->hosts))
     else
       if (ob->hosts_randomize) s = expanded_hosts = string_copy(s);
 
-    if (is_tainted(s))
+    if (is_tainted2(s, LOG_MAIN|LOG_PANIC, "Tainted host list '%s' from '%s' in transport %s", s, ob->hosts, tblock->name))
       {
-      log_write(0, LOG_MAIN|LOG_PANIC,
-       "attempt to use tainted host list '%s' from '%s' in transport %s",
-       s, ob->hosts, tblock->name);
       /* Avoid leaking info to an attacker */
       addrlist->message = US"internal configuration error";
       addrlist->transport_return = PANIC;