Remove attempts to quieten compiler static-checking
[users/heiko/exim.git] / src / src / verify.c
index 3dd2d3a39c9c850ee6bf196f6ec0194c8495bdce..a0ab1b873875ed1db1c778037dfee0f5eee61419 100644 (file)
@@ -2,7 +2,8 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2017 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Functions concerned with verifying things. The original code for callout
@@ -14,7 +15,7 @@ caching was contributed by Kevin Fleming (but I hacked it around a bit). */
 
 #define CUTTHROUGH_CMD_TIMEOUT  30     /* timeout for cutthrough-routing calls */
 #define CUTTHROUGH_DATA_TIMEOUT 60     /* timeout for cutthrough-routing calls */
-static smtp_outblock ctblock;
+static smtp_context ctctx;
 uschar ctbuffer[8192];
 
 
@@ -39,7 +40,7 @@ static tree_node *dnsbl_cache = NULL;
 #define MT_NOT 1
 #define MT_ALL 2
 
-static uschar cutthrough_response(char, uschar **, int);
+static uschar cutthrough_response(client_conn_ctx *, char, uschar **, int);
 
 
 
@@ -68,11 +69,9 @@ int length, expire;
 time_t now;
 dbdata_callout_cache *cache_record;
 
-cache_record = dbfn_read_with_length(dbm_file, key, &length);
-
-if (cache_record == NULL)
+if (!(cache_record = dbfn_read_with_length(dbm_file, key, &length)))
   {
-  HDEBUG(D_verify) debug_printf("callout cache: no %s record found for %s\n", type, key);
+  HDEBUG(D_verify) debug_printf_indent("callout cache: no %s record found for %s\n", type, key);
   return NULL;
   }
 
@@ -86,7 +85,7 @@ now = time(NULL);
 
 if (now - cache_record->time_stamp > expire)
   {
-  HDEBUG(D_verify) debug_printf("callout cache: %s record expired for %s\n", type, key);
+  HDEBUG(D_verify) debug_printf_indent("callout cache: %s record expired for %s\n", type, key);
   return NULL;
   }
 
@@ -100,7 +99,7 @@ if (type[0] == 'd' && cache_record->result != ccache_reject)
   {
   if (length == sizeof(dbdata_callout_cache_obs))
     {
-    dbdata_callout_cache *new = store_get(sizeof(dbdata_callout_cache));
+    dbdata_callout_cache *new = store_get(sizeof(dbdata_callout_cache), FALSE);
     memcpy(new, cache_record, length);
     new->postmaster_stamp = new->random_stamp = new->time_stamp;
     cache_record = new;
@@ -113,7 +112,7 @@ if (type[0] == 'd' && cache_record->result != ccache_reject)
     cache_record->random_result = ccache_unknown;
   }
 
-HDEBUG(D_verify) debug_printf("callout cache: found %s record for %s\n", type, key);
+HDEBUG(D_verify) debug_printf_indent("callout cache: found %s record for %s\n", type, key);
 return cache_record;
 }
 
@@ -140,11 +139,11 @@ stage, unless caching has been disabled. */
 
 if (options & vopt_callout_no_cache)
   {
-  HDEBUG(D_verify) debug_printf("callout cache: disabled by no_cache\n");
+  HDEBUG(D_verify) debug_printf_indent("callout cache: disabled by no_cache\n");
   }
-else if (!(dbm_file = dbfn_open(US"callout", O_RDWR, &dbblock, FALSE)))
+else if (!(dbm_file = dbfn_open(US"callout", O_RDWR, &dbblock, FALSE, TRUE)))
   {
-  HDEBUG(D_verify) debug_printf("callout cache: not available\n");
+  HDEBUG(D_verify) debug_printf_indent("callout cache: not available\n");
   }
 else
   {
@@ -174,9 +173,8 @@ else
     if (  cache_record->result == ccache_reject
        || *from_address == 0 && cache_record->result == ccache_reject_mfnull)
       {
-      setflag(addr, af_verify_nsfail);
       HDEBUG(D_verify)
-       debug_printf("callout cache: domain gave initial rejection, or "
+       debug_printf_indent("callout cache: domain gave initial rejection, or "
          "does not accept HELO or MAIL FROM:<>\n");
       setflag(addr, af_verify_nsfail);
       addr->user_message = US"(result of an earlier callout reused).";
@@ -197,13 +195,14 @@ else
       {
       case ccache_accept:
        HDEBUG(D_verify)
-         debug_printf("callout cache: domain accepts random addresses\n");
+         debug_printf_indent("callout cache: domain accepts random addresses\n");
+       *failure_ptr = US"random";
        dbfn_close(dbm_file);
        return TRUE;     /* Default yield is OK */
 
       case ccache_reject:
        HDEBUG(D_verify)
-         debug_printf("callout cache: domain rejects random addresses\n");
+         debug_printf_indent("callout cache: domain rejects random addresses\n");
        *opt_ptr = options & ~vopt_callout_random;
        new_domain_record->random_result = ccache_reject;
        new_domain_record->random_stamp = cache_record->random_stamp;
@@ -211,7 +210,7 @@ else
 
       default:
        HDEBUG(D_verify)
-         debug_printf("callout cache: need to check random address handling "
+         debug_printf_indent("callout cache: need to check random address handling "
            "(not cached or cache expired)\n");
        dbfn_close(dbm_file);
        return FALSE;
@@ -228,7 +227,7 @@ else
        {
        setflag(addr, af_verify_pmfail);
        HDEBUG(D_verify)
-         debug_printf("callout cache: domain does not accept "
+         debug_printf_indent("callout cache: domain does not accept "
            "RCPT TO:<postmaster@domain>\n");
        *yield = FAIL;
        *failure_ptr = US"postmaster";
@@ -240,7 +239,7 @@ else
       if (cache_record->postmaster_result == ccache_unknown)
        {
        HDEBUG(D_verify)
-         debug_printf("callout cache: need to check RCPT "
+         debug_printf_indent("callout cache: need to check RCPT "
            "TO:<postmaster@domain> (not cached or cache expired)\n");
        dbfn_close(dbm_file);
        return FALSE;
@@ -251,7 +250,7 @@ else
       that the value in the cache record is preserved (with its old timestamp).
       */
 
-      HDEBUG(D_verify) debug_printf("callout cache: domain accepts RCPT "
+      HDEBUG(D_verify) debug_printf_indent("callout cache: domain accepts RCPT "
        "TO:<postmaster@domain>\n");
       *pm_ptr = NULL;
       new_domain_record->postmaster_result = ccache_accept;
@@ -275,12 +274,12 @@ else
   if (cache_address_record->result == ccache_accept)
     {
     HDEBUG(D_verify)
-      debug_printf("callout cache: address record is positive\n");
+      debug_printf_indent("callout cache: address record is positive\n");
     }
   else
     {
     HDEBUG(D_verify)
-      debug_printf("callout cache: address record is negative\n");
+      debug_printf_indent("callout cache: address record is negative\n");
     addr->user_message = US"Previous (cached) callout verification failure";
     *failure_ptr = US"recipient";
     *yield = FAIL;
@@ -315,15 +314,15 @@ implying some kind of I/O error. We don't want to write the cache in that case.
 Otherwise the value is ccache_accept, ccache_reject, or ccache_reject_mfnull. */
 
 if (dom_rec->result != ccache_unknown)
-  if (!(dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE)))
+  if (!(dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE, TRUE)))
     {
-    HDEBUG(D_verify) debug_printf("callout cache: not available\n");
+    HDEBUG(D_verify) debug_printf_indent("callout cache: not available\n");
     }
   else
     {
     (void)dbfn_write(dbm_file, domain, dom_rec,
       (int)sizeof(dbdata_callout_cache));
-    HDEBUG(D_verify) debug_printf("wrote callout cache domain record for %s:\n"
+    HDEBUG(D_verify) debug_printf_indent("wrote callout cache domain record for %s:\n"
       "  result=%d postmaster=%d random=%d\n",
       domain,
       dom_rec->result,
@@ -337,16 +336,16 @@ is disabled. */
 if (done  &&  addr_rec->result != ccache_unknown)
   {
   if (!dbm_file)
-    dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE);
+    dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE, TRUE);
   if (!dbm_file)
     {
-    HDEBUG(D_verify) debug_printf("no callout cache available\n");
+    HDEBUG(D_verify) debug_printf_indent("no callout cache available\n");
     }
   else
     {
     (void)dbfn_write(dbm_file, address_key, addr_rec,
       (int)sizeof(dbdata_callout_cache_address));
-    HDEBUG(D_verify) debug_printf("wrote %s callout cache address record for %s\n",
+    HDEBUG(D_verify) debug_printf_indent("wrote %s callout cache address record for %s\n",
       addr_rec->result == ccache_accept ? "positive" : "negative",
       address_key);
     }
@@ -372,10 +371,9 @@ cutthrough_multi(address_item * addr, host_item * host_list,
   transport_feedback * tf, int * yield)
 {
 BOOL done = FALSE;
-host_item * host;
 
 if (addr->transport == cutthrough.addr.transport)
-  for (host = host_list; host; host = host->next)
+  for (host_item * host = host_list; host; host = host->next)
     if (Ustrcmp(host->address, cutthrough.host.address) == 0)
       {
       int host_af;
@@ -388,30 +386,34 @@ if (addr->transport == cutthrough.addr.transport)
       deliver_domain = addr->domain;
       transport_name = addr->transport->name;
 
-      host_af = (Ustrchr(host->address, ':') == NULL)? AF_INET:AF_INET6;
+      host_af = Ustrchr(host->address, ':') ? AF_INET6 : AF_INET;
 
-      if (!smtp_get_interface(tf->interface, host_af, addr, &interface,
-             US"callout") ||
-         !smtp_get_port(tf->port, addr, &port, US"callout"))
+      if (  !smtp_get_interface(tf->interface, host_af, addr, &interface,
+             US"callout")
+        || !smtp_get_port(tf->port, addr, &port, US"callout")
+        )
        log_write(0, LOG_MAIN|LOG_PANIC, "<%s>: %s", addr->address,
          addr->message);
 
+      smtp_port_for_connect(host, port);
+
       if (  (  interface == cutthrough.interface
            || (  interface
               && cutthrough.interface
               && Ustrcmp(interface, cutthrough.interface) == 0
            )  )
-        && port == cutthrough.host.port
+        && host->port == cutthrough.host.port
         )
        {
        uschar * resp = NULL;
 
        /* Match!  Send the RCPT TO, set done from the response */
        done =
-         smtp_write_command(&ctblock, FALSE, "RCPT TO:<%.1000s>\r\n",
-           transport_rcpt_address(addr,
-              addr->transport->rcpt_include_affixes)) >= 0 &&
-         cutthrough_response('2', &resp, CUTTHROUGH_DATA_TIMEOUT) == '2';
+            smtp_write_command(&ctctx, SCMD_FLUSH, "RCPT TO:<%.1000s>\r\n",
+             transport_rcpt_address(addr,
+                addr->transport->rcpt_include_affixes)) >= 0
+         && cutthrough_response(&cutthrough.cctx, '2', &resp,
+             CUTTHROUGH_DATA_TIMEOUT) == '2';
 
        /* This would go horribly wrong if a callout fail was ignored by ACL.
        We punt by abandoning cutthrough on a reject, like the
@@ -419,7 +421,7 @@ if (addr->transport == cutthrough.addr.transport)
 
        if (done)
          {
-         address_item * na = store_get(sizeof(address_item));
+         address_item * na = store_get(sizeof(address_item), FALSE);
          *na = cutthrough.addr;
          cutthrough.addr = *addr;
          cutthrough.addr.host_used = &cutthrough.host;
@@ -429,7 +431,7 @@ if (addr->transport == cutthrough.addr.transport)
          }
        else
          {
-         cancel_cutthrough_connection("recipient rejected");
+         cancel_cutthrough_connection(TRUE, US"recipient rejected");
          if (!resp || errno == ETIMEDOUT)
            {
            HDEBUG(D_verify) debug_printf("SMTP timeout\n");
@@ -459,7 +461,7 @@ if (addr->transport == cutthrough.addr.transport)
       break;   /* host_list */
       }
 if (!done)
-  cancel_cutthrough_connection("incompatible connection");
+  cancel_cutthrough_connection(TRUE, US"incompatible connection");
 return done;
 }
 
@@ -490,6 +492,7 @@ Arguments:
                       vopt_callout_random => do the "random" thing
                       vopt_callout_recipsender => use real sender for recipient
                       vopt_callout_recippmaster => use postmaster for recipient
+                     vopt_callout_hold         => lazy close connection
   se_mailfrom         MAIL FROM address for sender verify; NULL => ""
   pm_mailfrom         if non-NULL, do the postmaster check with this sender
 
@@ -556,12 +559,16 @@ else
 if (cached_callout_lookup(addr, address_key, from_address,
       &options, &pm_mailfrom, &yield, failure_ptr,
       &new_domain_record, &old_domain_cache_result))
+  {
+  cancel_cutthrough_connection(TRUE, US"cache-hit");
   goto END_CALLOUT;
+  }
 
 if (!addr->transport)
   {
   HDEBUG(D_verify) debug_printf("cannot callout via null transport\n");
   }
+
 else if (Ustrcmp(addr->transport->driver_name, "smtp") != 0)
   log_write(0, LOG_MAIN|LOG_PANIC|LOG_CONFIG_FOR, "callout transport '%s': %s is non-smtp",
     addr->transport->name, addr->transport->driver_name);
@@ -569,7 +576,7 @@ else
   {
   smtp_transport_options_block *ob =
     (smtp_transport_options_block *)addr->transport->options_block;
-  host_item * host;
+  smtp_context * sx = NULL;
 
   /* The information wasn't available in the cache, so we have to do a real
   callout and save the result in the cache for next time, unless no_cache is set,
@@ -582,6 +589,10 @@ else
       log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand "
         "callout_random_local_part: %s", expand_string_message);
 
+  /* Compile regex' used by client-side smtp */
+
+  smtp_deliver_init();
+
   /* Default the connect and overall callout timeouts if not set, and record the
   time we are starting so that we can enforce it. */
 
@@ -595,7 +606,7 @@ else
   and cause the client to time out. So in this case we forgo the PIPELINING
   optimization. */
 
-  if (smtp_out && !disable_callout_flush) mac_smtp_fflush();
+  if (smtp_out && !f.disable_callout_flush) mac_smtp_fflush();
 
   clearflag(addr, af_verify_pmfail);  /* postmaster callout flag */
   clearflag(addr, af_verify_nsfail);  /* null sender callout flag */
@@ -606,7 +617,7 @@ that conn for verification purposes (and later delivery also).  Simplest
 coding means skipping this whole loop and doing the append separately.  */
 
   /* Can we re-use an open cutthrough connection? */
-  if (  cutthrough.fd >= 0
+  if (  cutthrough.cctx.sock >= 0
      && (options & (vopt_callout_recipsender | vopt_callout_recippmaster))
        == vopt_callout_recipsender
      && !random_local_part
@@ -617,12 +628,11 @@ coding means skipping this whole loop and doing the append separately.  */
   /* If we did not use a cached connection, make connections to the hosts
   and do real callouts. The list of hosts is passed in as an argument. */
 
-  for (host = host_list; host && !done; host = host->next)
+  for (host_item * host = host_list; host && !done; host = host->next)
     {
     int host_af;
     int port = 25;
-    uschar *interface = NULL;  /* Outgoing interface to use; NULL => any */
-    smtp_context sx;
+    uschar * interface = NULL;  /* Outgoing interface to use; NULL => any */
 
     if (!host->address)
       {
@@ -662,14 +672,17 @@ coding means skipping this whole loop and doing the append separately.  */
       log_write(0, LOG_MAIN|LOG_PANIC, "<%s>: %s", addr->address,
         addr->message);
 
-    sx.addrlist = addr;
-    sx.host = host;
-    sx.host_af = host_af,
-    sx.port = port;
-    sx.interface = interface;
-    sx.helo_data = tf->helo_data;
-    sx.tblock = addr->transport;
-    sx.verify = TRUE;
+    if (!sx) sx = store_get(sizeof(*sx), TRUE);        /* tainted buffers */
+    memset(sx, 0, sizeof(*sx));
+
+    sx->addrlist = addr;
+    sx->conn_args.host = host;
+    sx->conn_args.host_af = host_af,
+    sx->port = port;
+    sx->conn_args.interface = interface;
+    sx->helo_data = tf->helo_data;
+    sx->conn_args.tblock = addr->transport;
+    sx->verify = TRUE;
 
 tls_retry_connection:
     /* Set the address state so that errors are recorded in it */
@@ -682,18 +695,21 @@ tls_retry_connection:
     SMTP command to send.  If we tried TLS but it failed, try again without
     if permitted */
 
-    if (  (yield = smtp_setup_conn(&sx, FALSE)) == DEFER
+    yield = smtp_setup_conn(sx, FALSE);
+#ifndef DISABLE_TLS
+    if (  yield == DEFER
        && addr->basic_errno == ERRNO_TLSFAILURE
        && ob->tls_tempfail_tryclear
-       && verify_check_given_host(&ob->hosts_require_tls, host) != OK
+       && verify_check_given_host(CUSS &ob->hosts_require_tls, host) != OK
        )
       {
-      log_write(0, LOG_MAIN, "TLS session failure:"
-       " callout unencrypted to %s [%s] (not in hosts_require_tls)",
-       host->name, host->address);
+      log_write(0, LOG_MAIN,
+       "%s: callout unencrypted to %s [%s] (not in hosts_require_tls)",
+       addr->message, host->name, host->address);
       addr->transport_return = PENDING_DEFER;
-      yield = smtp_setup_conn(&sx, TRUE);
+      yield = smtp_setup_conn(sx, TRUE);
       }
+#endif
     if (yield != OK)
       {
       errno = addr->basic_errno;
@@ -721,14 +737,13 @@ tls_retry_connection:
     addr->authenticator = client_authenticator;
     addr->auth_id = client_authenticated_id;
 
-    sx.from_addr = from_address;
-    sx.first_addr = sx.sync_addr = addr;
-    sx.ok = FALSE;                     /*XXX these 3 last might not be needed for verify? */
-    sx.send_rset = TRUE;
-    sx.completed_addr = FALSE;
+    sx->from_addr = from_address;
+    sx->first_addr = sx->sync_addr = addr;
+    sx->ok = FALSE;                    /*XXX these 3 last might not be needed for verify? */
+    sx->send_rset = TRUE;
+    sx->completed_addr = FALSE;
 
-    new_domain_record.result =
-      old_domain_cache_result == ccache_reject_mfnull
+    new_domain_record.result = old_domain_cache_result == ccache_reject_mfnull
       ? ccache_reject_mfnull : ccache_accept;
 
     /* Do the random local part check first. Temporarily replace the recipient
@@ -754,9 +769,12 @@ tls_retry_connection:
        }
 #endif
 
-      /* This would be ok for 1st rcpt of a cutthrough (XXX do we have a count?) , but no way to
-      handle a subsequent because of the RSET.  So refuse to support any. */
-      cancel_cutthrough_connection("random-recipient");
+      /* This would be ok for 1st rcpt of a cutthrough (the case handled here;
+      subsequents are done in cutthrough_multi()), but no way to
+      handle a subsequent because of the RSET vaporising the MAIL FROM.
+      So refuse to support any.  Most cutthrough use will not involve
+      random_local_part, so no loss. */
+      cancel_cutthrough_connection(TRUE, US"random-recipient");
 
       addr->address = string_sprintf("%s@%.1000s",
                                    random_local_part, rcpt_domain);
@@ -776,66 +794,82 @@ tls_retry_connection:
       postmaster-verify.
       The sync_responses() would need to be taught about it and we'd
       need another return code filtering out to here.
+
+      Avoid using a SIZE option on the MAIL for all random-rcpt checks.
       */
 
-      if (smtp_write_mail_and_rcpt_cmds(&sx, &yield) == 0)
+      sx->avoid_option = OPTION_SIZE;
+
+      /* Remember when we last did a random test */
+      new_domain_record.random_stamp = time(NULL);
+
+      if (smtp_write_mail_and_rcpt_cmds(sx, &yield) == 0)
        switch(addr->transport_return)
          {
-         case PENDING_OK:
+         case PENDING_OK:      /* random was accepted, unfortunately */
            new_domain_record.random_result = ccache_accept;
-           break;
-         case FAIL:
+           yield = OK;         /* Only usable verify result we can return */
+           done = TRUE;
+           *failure_ptr = US"random";
+           goto no_conn;
+         case FAIL:            /* rejected: the preferred result */
            new_domain_record.random_result = ccache_reject;
+           sx->avoid_option = 0;
 
            /* Between each check, issue RSET, because some servers accept only
            one recipient after MAIL FROM:<>.
            XXX We don't care about that for postmaster_full.  Should we? */
 
            if ((done =
-             smtp_write_command(&sx.outblock, FALSE, "RSET\r\n") >= 0 &&
-             smtp_read_response(&sx.inblock, sx.buffer, sizeof(sx.buffer),
-               '2', callout)))
+             smtp_write_command(sx, SCMD_FLUSH, "RSET\r\n") >= 0 &&
+             smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2', callout)))
              break;
 
            HDEBUG(D_acl|D_v)
-             debug_printf("problem after random/rset/mfrom; reopen conn\n");
+             debug_printf_indent("problem after random/rset/mfrom; reopen conn\n");
            random_local_part = NULL;
-#ifdef SUPPORT_TLS
-           tls_close(FALSE, TRUE);
+#ifndef DISABLE_TLS
+           tls_close(sx->cctx.tls_ctx, TLS_SHUTDOWN_NOWAIT);
 #endif
-           HDEBUG(D_transport|D_acl|D_v) debug_printf("  SMTP(close)>>\n");
-           (void)close(sx.inblock.sock);
-           sx.inblock.sock = sx.outblock.sock = -1;
+           HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP(close)>>\n");
+           (void)close(sx->cctx.sock);
+           sx->cctx.sock = -1;
 #ifndef DISABLE_EVENT
            (void) event_raise(addr->transport->event_action,
                              US"tcp:close", NULL);
 #endif
+           addr->address = main_address;
+           addr->transport_return = PENDING_DEFER;
+           sx->first_addr = sx->sync_addr = addr;
+           sx->ok = FALSE;
+           sx->send_rset = TRUE;
+           sx->completed_addr = FALSE;
            goto tls_retry_connection;
+         case DEFER:           /* 4xx response to random */
+           break;              /* Just to be clear. ccache_unknown, !done. */
          }
 
-      /* Remember when we last did a random test */
-
-      new_domain_record.random_stamp = time(NULL);
-
       /* Re-setup for main verify, or for the error message when failing */
       addr->address = main_address;
       addr->transport_return = PENDING_DEFER;
-      sx.first_addr = sx.sync_addr = addr;
-      sx.ok = FALSE;
-      sx.send_rset = TRUE;
-      sx.completed_addr = FALSE;
+      sx->first_addr = sx->sync_addr = addr;
+      sx->ok = FALSE;
+      sx->send_rset = TRUE;
+      sx->completed_addr = FALSE;
       }
     else
       done = TRUE;
 
-    /* Main verify. If the host is accepting all local parts, as determined
-    by the "random" check, we don't need to waste time doing any further
-    checking. */
+    /* Main verify.  For rcpt-verify use SIZE if we know it and we're not cacheing;
+    for sndr-verify never use it. */
 
     if (done)
       {
+      if (!(options & vopt_is_recipient  &&  options & vopt_callout_no_cache))
+       sx->avoid_option = OPTION_SIZE;
+
       done = FALSE;
-      switch(smtp_write_mail_and_rcpt_cmds(&sx, &yield))
+      switch(smtp_write_mail_and_rcpt_cmds(sx, &yield))
        {
        case 0:  switch(addr->transport_return) /* ok so far */
                    {
@@ -853,7 +887,7 @@ tls_retry_connection:
 
        case -1:                                /* MAIL response error */
                  *failure_ptr = US"mail";
-                 if (errno == 0 && sx.buffer[0] == '5')
+                 if (errno == 0 && sx->buffer[0] == '5')
                    {
                    setflag(addr, af_verify_nsfail);
                    if (from_address[0] == 0)
@@ -880,12 +914,11 @@ tls_retry_connection:
       /* Could possibly shift before main verify, just above, and be ok
       for cutthrough.  But no way to handle a subsequent rcpt, so just
       refuse any */
-      cancel_cutthrough_connection("postmaster verify");
-      HDEBUG(D_acl|D_v) debug_printf("Cutthrough cancelled by presence of postmaster verify\n");
+      cancel_cutthrough_connection(TRUE, US"postmaster verify");
+      HDEBUG(D_acl|D_v) debug_printf_indent("Cutthrough cancelled by presence of postmaster verify\n");
 
-      done = smtp_write_command(&sx.outblock, FALSE, "RSET\r\n") >= 0
-          && smtp_read_response(&sx.inblock, sx.buffer,
-                               sizeof(sx.buffer), '2', callout);
+      done = smtp_write_command(sx, SCMD_FLUSH, "RSET\r\n") >= 0
+          && smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2', callout);
 
       if (done)
        {
@@ -895,22 +928,23 @@ tls_retry_connection:
        addr->address = string_sprintf("postmaster@%.1000s", addr->domain);
        addr->transport_return = PENDING_DEFER;
 
-       sx.from_addr = pm_mailfrom;
-       sx.first_addr = sx.sync_addr = addr;
-       sx.ok = FALSE;
-       sx.send_rset = TRUE;
-       sx.completed_addr = FALSE;
+       sx->from_addr = pm_mailfrom;
+       sx->first_addr = sx->sync_addr = addr;
+       sx->ok = FALSE;
+       sx->send_rset = TRUE;
+       sx->completed_addr = FALSE;
+       sx->avoid_option = OPTION_SIZE;
 
-       if(  smtp_write_mail_and_rcpt_cmds(&sx, &yield) == 0
+       if(  smtp_write_mail_and_rcpt_cmds(sx, &yield) == 0
          && addr->transport_return == PENDING_OK
          )
          done = TRUE;
        else
          done = (options & vopt_callout_fullpm) != 0
-             && smtp_write_command(&sx.outblock, FALSE,
+             && smtp_write_command(sx, SCMD_FLUSH,
                            "RCPT TO:<postmaster>\r\n") >= 0
-             && smtp_read_response(&sx.inblock, sx.buffer,
-                           sizeof(sx.buffer), '2', callout);
+             && smtp_read_response(sx, sx->buffer,
+                           sizeof(sx->buffer), '2', callout);
 
        /* Sort out the cache record */
 
@@ -918,7 +952,7 @@ tls_retry_connection:
 
        if (done)
          new_domain_record.postmaster_result = ccache_accept;
-       else if (errno == 0 && sx.buffer[0] == '5')
+       else if (errno == 0 && sx->buffer[0] == '5')
          {
          *failure_ptr = US"postmaster";
          setflag(addr, af_verify_pmfail);
@@ -943,7 +977,7 @@ no_conn:
       {
       case ETIMEDOUT:
        HDEBUG(D_verify) debug_printf("SMTP timeout\n");
-       sx.send_quit = FALSE;
+       sx->send_quit = FALSE;
        break;
 
 #ifdef SUPPORT_I18N
@@ -951,8 +985,7 @@ no_conn:
        {
        extern int acl_where;   /* src/acl.c */
        errno = 0;
-       addr->message = string_sprintf(
-           "response to \"EHLO\" did not include SMTPUTF8");
+       addr->message = US"response to \"EHLO\" did not include SMTPUTF8";
        addr->user_message = acl_where == ACL_WHERE_RCPT
          ? US"533 no support for internationalised mailbox name"
          : US"550 mailbox unavailable";
@@ -962,29 +995,49 @@ no_conn:
        break;
 #endif
       case ECONNREFUSED:
-       sx.send_quit = FALSE;
+       sx->send_quit = FALSE;
        break;
 
       case 0:
-       if (*sx.buffer == 0) Ustrcpy(sx.buffer, US"connection dropped");
+       if (*sx->buffer == 0) Ustrcpy(sx->buffer, US"connection dropped");
 
        /*XXX test here is ugly; seem to have a split of responsibility for
-       building this message.  Need to reationalise.  Where is it done
+       building this message.  Need to rationalise.  Where is it done
        before here, and when not?
        Not == 5xx resp to MAIL on main-verify
        */
        if (!addr->message) addr->message =
          string_sprintf("response to \"%s\" was: %s",
-                         big_buffer, string_printing(sx.buffer));
+                         big_buffer, string_printing(sx->buffer));
+
+       /* RFC 5321 section 4.2: the text portion of the response may have only
+       HT, SP, Printable US-ASCII.  Deal with awkward chars by cutting the
+       received message off before passing it onward.  Newlines are ok; they
+       just become a multiline response (but wrapped in the error code we
+       produce). */
 
+       for (uschar * s = sx->buffer;
+            *s && s < sx->buffer + sizeof(sx->buffer);
+            s++)
+         {
+         uschar c = *s;
+         if (c != '\t' && c != '\n' && (c < ' ' || c > '~'))
+           {
+           if (s - sx->buffer < sizeof(sx->buffer) - 12)
+             memcpy(s, "(truncated)", 12);
+           else
+             *s = '\0';
+           break;
+           }
+         }
        addr->user_message = options & vopt_is_recipient
-         ? string_sprintf("Callout verification failed:\n%s", sx.buffer)
+         ? string_sprintf("Callout verification failed:\n%s", sx->buffer)
          : string_sprintf("Called:   %s\nSent:     %s\nResponse: %s",
-           host->address, big_buffer, sx.buffer);
+           host->address, big_buffer, sx->buffer);
 
        /* Hard rejection ends the process */
 
-       if (sx.buffer[0] == '5')   /* Address rejected */
+       if (sx->buffer[0] == '5')   /* Address rejected */
          {
          yield = FAIL;
          done = TRUE;
@@ -996,8 +1049,33 @@ no_conn:
 
     /* Cutthrough - on a successful connect and recipient-verify with
     use-sender and we are 1st rcpt and have no cutthrough conn so far
-    here is where we want to leave the conn open */
-    if (  cutthrough.delivery
+    here is where we want to leave the conn open.  Ditto for a lazy-close
+    verify. */
+
+    if (cutthrough.delivery)
+      {
+      if (addr->transport->filter_command)
+        {
+        cutthrough.delivery= FALSE;
+        HDEBUG(D_acl|D_v) debug_printf("Cutthrough cancelled by presence of transport filter\n");
+        }
+#ifndef DISABLE_DKIM
+      if (ob->dkim.dkim_domain)
+        {
+        cutthrough.delivery= FALSE;
+        HDEBUG(D_acl|D_v) debug_printf("Cutthrough cancelled by presence of DKIM signing\n");
+        }
+#endif
+#ifdef EXPERIMENTAL_ARC
+      if (ob->arc_sign)
+        {
+        cutthrough.delivery= FALSE;
+        HDEBUG(D_acl|D_v) debug_printf("Cutthrough cancelled by presence of ARC signing\n");
+        }
+#endif
+      }
+
+    if (  (cutthrough.delivery || options & vopt_callout_hold)
        && rcpt_count == 1
        && done
        && yield == OK
@@ -1005,50 +1083,70 @@ no_conn:
           == vopt_callout_recipsender
        && !random_local_part
        && !pm_mailfrom
-       && cutthrough.fd < 0
-       && !sx.lmtp
+       && cutthrough.cctx.sock < 0
+       && !sx->lmtp
        )
       {
-      HDEBUG(D_acl|D_v) debug_printf("holding verify callout open for cutthrough delivery\n");
-
-      cutthrough.fd = sx.outblock.sock;        /* We assume no buffer in use in the outblock */
-      cutthrough.nrcpt = 1;
-      cutthrough.interface = interface;
-      cutthrough.host = *host;
-      cutthrough.addr = *addr;         /* Save the address_item for later logging */
-      cutthrough.addr.next =     NULL;
+      HDEBUG(D_acl|D_v) debug_printf_indent("holding verify callout open for %s\n",
+       cutthrough.delivery
+       ? "cutthrough delivery" : "potential further verifies and delivery");
+
+      cutthrough.callout_hold_only = !cutthrough.delivery;
+      cutthrough.is_tls =      tls_out.active.sock >= 0;
+      /* We assume no buffer in use in the outblock */
+      cutthrough.cctx =                sx->cctx;
+      cutthrough.nrcpt =       1;
+      cutthrough.transport =   addr->transport->name;
+      cutthrough.interface =   interface;
+      cutthrough.snd_port =    sending_port;
+      cutthrough.peer_options =        smtp_peer_options;
+      cutthrough.host =                *host;
+       {
+       int oldpool = store_pool;
+       store_pool = POOL_PERM;
+       cutthrough.snd_ip = string_copy(sending_ip_address);
+       cutthrough.host.name = string_copy(host->name);
+       cutthrough.host.address = string_copy(host->address);
+       store_pool = oldpool;
+       }
+
+      /* Save the address_item and parent chain for later logging */
+      cutthrough.addr =                *addr;
+      cutthrough.addr.next =   NULL;
       cutthrough.addr.host_used = &cutthrough.host;
-      if (addr->parent)
-        *(cutthrough.addr.parent = store_get(sizeof(address_item))) =
-         *addr->parent;
-      ctblock.buffer = ctbuffer;
-      ctblock.buffersize = sizeof(ctbuffer);
-      ctblock.ptr = ctbuffer;
-      /* ctblock.cmd_count = 0; ctblock.authenticating = FALSE; */
-      ctblock.sock = cutthrough.fd;
+      for (address_item * caddr = &cutthrough.addr, * parent = addr->parent;
+          parent;
+          caddr = caddr->parent, parent = parent->parent)
+        *(caddr->parent = store_get(sizeof(address_item), FALSE)) = *parent;
+
+      ctctx.outblock.buffer = ctbuffer;
+      ctctx.outblock.buffersize = sizeof(ctbuffer);
+      ctctx.outblock.ptr = ctbuffer;
+      /* ctctx.outblock.cmd_count = 0; ctctx.outblock.authenticating = FALSE; */
+      ctctx.outblock.cctx = &cutthrough.cctx;
       }
     else
       {
-      /* Ensure no cutthrough on multiple address verifies */
+      /* Ensure no cutthrough on multiple verifies that were incompatible */
       if (options & vopt_callout_recipsender)
-        cancel_cutthrough_connection("not usable for cutthrough");
-      if (sx.send_quit)
-       {
-       (void) smtp_write_command(&sx.outblock, FALSE, "QUIT\r\n");
+        cancel_cutthrough_connection(TRUE, US"not usable for cutthrough");
+      if (sx->send_quit)
+       if (smtp_write_command(sx, SCMD_FLUSH, "QUIT\r\n") != -1)
+         /* Wait a short time for response, and discard it */
+         smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2', 1);
 
-       /* Wait a short time for response, and discard it */
-       smtp_read_response(&sx.inblock, sx.buffer, sizeof(sx.buffer),
-         '2', 1);
-       }
-
-      if (sx.inblock.sock >= 0)
+      if (sx->cctx.sock >= 0)
        {
-#ifdef SUPPORT_TLS
-       tls_close(FALSE, TRUE);
+#ifndef DISABLE_TLS
+       if (sx->cctx.tls_ctx)
+         {
+         tls_close(sx->cctx.tls_ctx, TLS_SHUTDOWN_NOWAIT);
+         sx->cctx.tls_ctx = NULL;
+         }
 #endif
-       HDEBUG(D_transport|D_acl|D_v) debug_printf("  SMTP(close)>>\n");
-       (void)close(sx.inblock.sock);
-       sx.inblock.sock = sx.outblock.sock = -1;
+       HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP(close)>>\n");
+       (void)close(sx->cctx.sock);
+       sx->cctx.sock = -1;
 #ifndef DISABLE_EVENT
        (void) event_raise(addr->transport->event_action, US"tcp:close", NULL);
 #endif
@@ -1103,7 +1201,7 @@ if (!done)
 /* Come here from within the cache-reading code on fast-track exit. */
 
 END_CALLOUT:
-tls_modify_variables(&tls_in);
+tls_modify_variables(&tls_in); /* return variables to inbound values */
 return yield;
 }
 
@@ -1113,7 +1211,7 @@ return yield;
    one was requested and a recipient-verify wasn't subsequently done.
 */
 int
-open_cutthrough_connection( address_item * addr )
+open_cutthrough_connection(address_item * addr)
 {
 address_item addr2;
 int rc;
@@ -1123,7 +1221,7 @@ int rc;
 get rewritten. */
 
 addr2 = *addr;
-HDEBUG(D_acl) debug_printf("----------- %s cutthrough setup ------------\n",
+HDEBUG(D_acl) debug_printf_indent("----------- %s cutthrough setup ------------\n",
   rcpt_count > 1 ? "more" : "start");
 rc = verify_address(&addr2, NULL,
        vopt_is_recipient | vopt_callout_recipsender | vopt_callout_no_cache,
@@ -1131,7 +1229,7 @@ rc = verify_address(&addr2, NULL,
        NULL, NULL, NULL);
 addr->message = addr2.message;
 addr->user_message = addr2.user_message;
-HDEBUG(D_acl) debug_printf("----------- end cutthrough setup ------------\n");
+HDEBUG(D_acl) debug_printf_indent("----------- end cutthrough setup ------------\n");
 return rc;
 }
 
@@ -1141,22 +1239,24 @@ return rc;
 static BOOL
 cutthrough_send(int n)
 {
-if(cutthrough.fd < 0)
+if(cutthrough.cctx.sock < 0)
   return TRUE;
 
 if(
-#ifdef SUPPORT_TLS
-   (tls_out.active == cutthrough.fd) ? tls_write(FALSE, ctblock.buffer, n) :
+#ifndef DISABLE_TLS
+   cutthrough.is_tls
+   ? tls_write(cutthrough.cctx.tls_ctx, ctctx.outblock.buffer, n, FALSE)
+   :
 #endif
-   send(cutthrough.fd, ctblock.buffer, n, 0) > 0
+     send(cutthrough.cctx.sock, ctctx.outblock.buffer, n, 0) > 0
   )
 {
   transport_count += n;
-  ctblock.ptr= ctblock.buffer;
+  ctctx.outblock.ptr= ctctx.outblock.buffer;
   return TRUE;
 }
 
-HDEBUG(D_transport|D_acl) debug_printf("cutthrough_send failed: %s\n", strerror(errno));
+HDEBUG(D_transport|D_acl) debug_printf_indent("cutthrough_send failed: %s\n", strerror(errno));
 return FALSE;
 }
 
@@ -1167,30 +1267,37 @@ _cutthrough_puts(uschar * cp, int n)
 {
 while(n--)
  {
- if(ctblock.ptr >= ctblock.buffer+ctblock.buffersize)
-   if(!cutthrough_send(ctblock.buffersize))
+ if(ctctx.outblock.ptr >= ctctx.outblock.buffer+ctctx.outblock.buffersize)
+   if(!cutthrough_send(ctctx.outblock.buffersize))
      return FALSE;
 
- *ctblock.ptr++ = *cp++;
+ *ctctx.outblock.ptr++ = *cp++;
  }
 return TRUE;
 }
 
 /* Buffered output of counted data block.   Return boolean success */
-BOOL
+static BOOL
 cutthrough_puts(uschar * cp, int n)
 {
-if (cutthrough.fd < 0)       return TRUE;
-if (_cutthrough_puts(cp, n)) return TRUE;
-cancel_cutthrough_connection("transmit failed");
+if (cutthrough.cctx.sock < 0) return TRUE;
+if (_cutthrough_puts(cp, n))  return TRUE;
+cancel_cutthrough_connection(TRUE, US"transmit failed");
 return FALSE;
 }
 
+void
+cutthrough_data_puts(uschar * cp, int n)
+{
+if (cutthrough.delivery) (void) cutthrough_puts(cp, n);
+return;
+}
+
 
 static BOOL
 _cutthrough_flush_send(void)
 {
-int n= ctblock.ptr-ctblock.buffer;
+int n = ctctx.outblock.ptr - ctctx.outblock.buffer;
 
 if(n>0)
   if(!cutthrough_send(n))
@@ -1204,36 +1311,42 @@ BOOL
 cutthrough_flush_send(void)
 {
 if (_cutthrough_flush_send()) return TRUE;
-cancel_cutthrough_connection("transmit failed");
+cancel_cutthrough_connection(TRUE, US"transmit failed");
 return FALSE;
 }
 
 
-BOOL
+static BOOL
 cutthrough_put_nl(void)
 {
 return cutthrough_puts(US"\r\n", 2);
 }
 
 
+void
+cutthrough_data_put_nl(void)
+{
+cutthrough_data_puts(US"\r\n", 2);
+}
+
+
 /* Get and check response from cutthrough target */
 static uschar
-cutthrough_response(char expect, uschar ** copy, int timeout)
+cutthrough_response(client_conn_ctx * cctx, char expect, uschar ** copy, int timeout)
 {
-smtp_inblock inblock;
+smtp_context sx = {0};
 uschar inbuffer[4096];
 uschar responsebuffer[4096];
 
-inblock.buffer = inbuffer;
-inblock.buffersize = sizeof(inbuffer);
-inblock.ptr = inbuffer;
-inblock.ptrend = inbuffer;
-inblock.sock = cutthrough.fd;
-/* this relies on (inblock.sock == tls_out.active) */
-if(!smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer), expect, timeout))
-  cancel_cutthrough_connection("target timeout on read");
+sx.inblock.buffer = inbuffer;
+sx.inblock.buffersize = sizeof(inbuffer);
+sx.inblock.ptr = inbuffer;
+sx.inblock.ptrend = inbuffer;
+sx.inblock.cctx = cctx;
+if(!smtp_read_response(&sx, responsebuffer, sizeof(responsebuffer), expect, timeout))
+  cancel_cutthrough_connection(TRUE, US"target timeout on read");
 
-if(copy != NULL)
+if(copy)
   {
   uschar * cp;
   *copy = cp = string_copy(responsebuffer);
@@ -1251,21 +1364,21 @@ return responsebuffer[0];
 BOOL
 cutthrough_predata(void)
 {
-if(cutthrough.fd < 0)
+if(cutthrough.cctx.sock < 0 || cutthrough.callout_hold_only)
   return FALSE;
 
-HDEBUG(D_transport|D_acl|D_v) debug_printf("  SMTP>> DATA\n");
+HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP>> DATA\n");
 cutthrough_puts(US"DATA\r\n", 6);
 cutthrough_flush_send();
 
 /* Assume nothing buffered.  If it was it gets ignored. */
-return cutthrough_response('3', NULL, CUTTHROUGH_DATA_TIMEOUT) == '3';
+return cutthrough_response(&cutthrough.cctx, '3', NULL, CUTTHROUGH_DATA_TIMEOUT) == '3';
 }
 
 
-/* fd and tctx args only to match write_chunk() */
+/* tctx arg only to match write_chunk() */
 static BOOL
-cutthrough_write_chunk(int fd, transport_ctx * tctx, uschar * s, int len)
+cutthrough_write_chunk(transport_ctx * tctx, uschar * s, int len)
 {
 uschar * s2;
 while(s && (s2 = Ustrchr(s, '\n')))
@@ -1286,61 +1399,83 @@ cutthrough_headers_send(void)
 {
 transport_ctx tctx;
 
-if(cutthrough.fd < 0)
+if(cutthrough.cctx.sock < 0 || cutthrough.callout_hold_only)
   return FALSE;
 
 /* We share a routine with the mainline transport to handle header add/remove/rewrites,
    but having a separate buffered-output function (for now)
 */
-HDEBUG(D_acl) debug_printf("----------- start cutthrough headers send -----------\n");
+HDEBUG(D_acl) debug_printf_indent("----------- start cutthrough headers send -----------\n");
 
+tctx.u.fd = cutthrough.cctx.sock;
 tctx.tblock = cutthrough.addr.transport;
 tctx.addr = &cutthrough.addr;
 tctx.check_string = US".";
 tctx.escape_string = US"..";
+/*XXX check under spool_files_wireformat.  Might be irrelevant */
 tctx.options = topt_use_crlf;
 
-if (!transport_headers_send(cutthrough.fd, &tctx, &cutthrough_write_chunk))
+if (!transport_headers_send(&tctx, &cutthrough_write_chunk))
   return FALSE;
 
-HDEBUG(D_acl) debug_printf("----------- done cutthrough headers send ------------\n");
+HDEBUG(D_acl) debug_printf_indent("----------- done cutthrough headers send ------------\n");
 return TRUE;
 }
 
 
 static void
-close_cutthrough_connection(const char * why)
+close_cutthrough_connection(const uschar * why)
 {
-if(cutthrough.fd >= 0)
+int fd = cutthrough.cctx.sock;
+if(fd >= 0)
   {
   /* We could be sending this after a bunch of data, but that is ok as
      the only way to cancel the transfer in dataphase is to drop the tcp
      conn before the final dot.
   */
-  ctblock.ptr = ctbuffer;
-  HDEBUG(D_transport|D_acl|D_v) debug_printf("  SMTP>> QUIT\n");
+  client_conn_ctx tmp_ctx = cutthrough.cctx;
+  ctctx.outblock.ptr = ctbuffer;
+  HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP>> QUIT\n");
   _cutthrough_puts(US"QUIT\r\n", 6);   /* avoid recursion */
   _cutthrough_flush_send();
+  cutthrough.cctx.sock = -1;           /* avoid recursion via read timeout */
+  cutthrough.nrcpt = 0;                        /* permit re-cutthrough on subsequent message */
 
   /* Wait a short time for response, and discard it */
-  cutthrough_response('2', NULL, 1);
-
-  #ifdef SUPPORT_TLS
-  tls_close(FALSE, TRUE);
-  #endif
-  HDEBUG(D_transport|D_acl|D_v) debug_printf("  SMTP(close)>>\n");
-  (void)close(cutthrough.fd);
-  cutthrough.fd = -1;
-  HDEBUG(D_acl) debug_printf("----------- cutthrough shutdown (%s) ------------\n", why);
+  cutthrough_response(&tmp_ctx, '2', NULL, 1);
+
+#ifndef DISABLE_TLS
+  if (cutthrough.is_tls)
+    {
+    tls_close(cutthrough.cctx.tls_ctx, TLS_SHUTDOWN_NOWAIT);
+    cutthrough.cctx.tls_ctx = NULL;
+    cutthrough.is_tls = FALSE;
+    }
+#endif
+  HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP(close)>>\n");
+  (void)close(fd);
+  HDEBUG(D_acl) debug_printf_indent("----------- cutthrough shutdown (%s) ------------\n", why);
   }
-ctblock.ptr = ctbuffer;
+ctctx.outblock.ptr = ctbuffer;
+}
+
+void
+cancel_cutthrough_connection(BOOL close_noncutthrough_verifies, const uschar * why)
+{
+if (cutthrough.delivery || close_noncutthrough_verifies)
+  close_cutthrough_connection(why);
+cutthrough.delivery = cutthrough.callout_hold_only = FALSE;
 }
 
+
 void
-cancel_cutthrough_connection(const char * why)
+release_cutthrough_connection(const uschar * why)
 {
-close_cutthrough_connection(why);
-cutthrough.delivery = FALSE;
+if (cutthrough.cctx.sock < 0) return;
+HDEBUG(D_acl) debug_printf_indent("release cutthrough conn: %s\n", why);
+cutthrough.cctx.sock = -1;
+cutthrough.cctx.tls_ctx = NULL;
+cutthrough.delivery = cutthrough.callout_hold_only = FALSE;
 }
 
 
@@ -1355,8 +1490,7 @@ uschar *
 cutthrough_finaldot(void)
 {
 uschar res;
-address_item * addr;
-HDEBUG(D_transport|D_acl|D_v) debug_printf("  SMTP>> .\n");
+HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP>> .\n");
 
 /* Assume data finshed with new-line */
 if(  !cutthrough_puts(US".", 1)
@@ -1365,15 +1499,16 @@ if(  !cutthrough_puts(US".", 1)
   )
   return cutthrough.addr.message;
 
-res = cutthrough_response('2', &cutthrough.addr.message, CUTTHROUGH_DATA_TIMEOUT);
-for (addr = &cutthrough.addr; addr; addr = addr->next)
+res = cutthrough_response(&cutthrough.cctx, '2', &cutthrough.addr.message,
+       CUTTHROUGH_DATA_TIMEOUT);
+for (address_item * addr = &cutthrough.addr; addr; addr = addr->next)
   {
   addr->message = cutthrough.addr.message;
   switch(res)
     {
     case '2':
       delivery_log(LOG_MAIN, addr, (int)'>', NULL);
-      close_cutthrough_connection("delivered");
+      close_cutthrough_connection(US"delivered");
       break;
 
     case '4':
@@ -1422,6 +1557,8 @@ if (addr != vaddr)
   vaddr->basic_errno = addr->basic_errno;
   vaddr->more_errno = addr->more_errno;
   vaddr->prop.address_data = addr->prop.address_data;
+  vaddr->prop.variables = NULL;
+  tree_dup((tree_node **)&vaddr->prop.variables, addr->prop.variables);
   copyflag(vaddr, addr, af_pass_message);
   }
 return yield;
@@ -1458,7 +1595,7 @@ va_list ap;
 
 va_start(ap, format);
 if (smtp_out && (f == smtp_out))
-  smtp_vprintf(format, ap);
+  smtp_vprintf(format, FALSE, ap);
 else
   vfprintf(f, format, ap);
 va_end(ap);
@@ -1476,7 +1613,7 @@ address testing (-bt), which is indicated by address_test_mode being set.
 Arguments:
   vaddr            contains the address to verify; the next field in this block
                      must be NULL
-  f                if not NULL, write the result to this file
+  fp               if not NULL, write the result to this file
   options          various option bits:
                      vopt_fake_sender => this sender verify is not for the real
                        sender (it was verify=sender=xxxx or an address from a
@@ -1517,19 +1654,19 @@ Returns:           OK      address verified
 */
 
 int
-verify_address(address_item *vaddr, FILE *f, int options, int callout,
-  int callout_overall, int callout_connect, uschar *se_mailfrom,
+verify_address(address_item * vaddr, FILE * fp, int options, int callout,
+  int callout_overall, int callout_connect, uschar * se_mailfrom,
   uschar *pm_mailfrom, BOOL *routed)
 {
 BOOL allok = TRUE;
-BOOL full_info = (f == NULL)? FALSE : (debug_selector != 0);
+BOOL full_info = fp ? debug_selector != 0 : FALSE;
 BOOL expn         = (options & vopt_expn) != 0;
 BOOL success_on_redirect = (options & vopt_success_on_redirect) != 0;
 int i;
 int yield = OK;
-int verify_type = expn? v_expn :
-     address_test_mode? v_none :
-          options & vopt_is_recipient? v_recipient : v_sender;
+int verify_type = expn ? v_expn :
+   f.address_test_mode ? v_none :
+          options & vopt_is_recipient ? v_recipient : v_sender;
 address_item *addr_list;
 address_item *addr_new = NULL;
 address_item *addr_remote = NULL;
@@ -1561,10 +1698,10 @@ else ko_prefix = cr = US"";
 
 if (parse_find_at(address) == NULL)
   {
-  if ((options & vopt_qualify) == 0)
+  if (!(options & vopt_qualify))
     {
-    if (f != NULL)
-      respond_printf(f, "%sA domain is required for \"%s\"%s\n",
+    if (fp)
+      respond_printf(fp, "%sA domain is required for \"%s\"%s\n",
         ko_prefix, address, cr);
     *failure_ptr = US"qualify";
     return FAIL;
@@ -1575,36 +1712,36 @@ if (parse_find_at(address) == NULL)
 DEBUG(D_verify)
   {
   debug_printf(">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>\n");
-  debug_printf("%s %s\n", address_test_mode? "Testing" : "Verifying", address);
+  debug_printf("%s %s\n", f.address_test_mode? "Testing" : "Verifying", address);
   }
 
 /* Rewrite and report on it. Clear the domain and local part caches - these
 may have been set by domains and local part tests during an ACL. */
 
-if (global_rewrite_rules != NULL)
+if (global_rewrite_rules)
   {
   uschar *old = address;
   address = rewrite_address(address, options & vopt_is_recipient, FALSE,
     global_rewrite_rules, rewrite_existflags);
   if (address != old)
     {
-    for (i = 0; i < (MAX_NAMED_LIST * 2)/32; i++) vaddr->localpart_cache[i] = 0;
-    for (i = 0; i < (MAX_NAMED_LIST * 2)/32; i++) vaddr->domain_cache[i] = 0;
-    if (f != NULL && !expn) fprintf(f, "Address rewritten as: %s\n", address);
+    for (int i = 0; i < (MAX_NAMED_LIST * 2)/32; i++) vaddr->localpart_cache[i] = 0;
+    for (int i = 0; i < (MAX_NAMED_LIST * 2)/32; i++) vaddr->domain_cache[i] = 0;
+    if (fp && !expn) fprintf(fp, "Address rewritten as: %s\n", address);
     }
   }
 
 /* If this is the real sender address, we must update sender_address at
 this point, because it may be referred to in the routers. */
 
-if ((options & (vopt_fake_sender|vopt_is_recipient)) == 0)
+if (!(options & (vopt_fake_sender|vopt_is_recipient)))
   sender_address = address;
 
 /* If the address was rewritten to <> no verification can be done, and we have
 to return OK. This rewriting is permitted only for sender addresses; for other
 addresses, such rewriting fails. */
 
-if (address[0] == 0) return OK;
+if (!address[0]) return OK;
 
 /* Flip the legacy TLS-related variables over to the outbound set in case
 they're used in the context of a transport used by verification. Reset them
@@ -1656,29 +1793,29 @@ while (addr_new)
   if (testflag(addr, af_pfr))
     {
     allok = FALSE;
-    if (f != NULL)
+    if (fp)
       {
       BOOL allow;
 
       if (addr->address[0] == '>')
         {
         allow = testflag(addr, af_allow_reply);
-        fprintf(f, "%s -> mail %s", addr->parent->address, addr->address + 1);
+        fprintf(fp, "%s -> mail %s", addr->parent->address, addr->address + 1);
         }
       else
         {
-        allow = (addr->address[0] == '|')?
-          testflag(addr, af_allow_pipe) : testflag(addr, af_allow_file);
-        fprintf(f, "%s -> %s", addr->parent->address, addr->address);
+        allow = addr->address[0] == '|'
+          testflag(addr, af_allow_pipe) : testflag(addr, af_allow_file);
+        fprintf(fp, "%s -> %s", addr->parent->address, addr->address);
         }
 
       if (addr->basic_errno == ERRNO_BADTRANSPORT)
-        fprintf(f, "\n*** Error in setting up pipe, file, or autoreply:\n"
+        fprintf(fp, "\n*** Error in setting up pipe, file, or autoreply:\n"
           "%s\n", addr->message);
       else if (allow)
-        fprintf(f, "\n  transport = %s\n", addr->transport->name);
+        fprintf(fp, "\n  transport = %s\n", addr->transport->name);
       else
-        fprintf(f, " *** forbidden ***\n");
+        fprintf(fp, " *** forbidden ***\n");
       }
     continue;
     }
@@ -1710,6 +1847,8 @@ while (addr_new)
 
   if (rc == OK)
     {
+    BOOL local_verify = FALSE;
+
     if (routed) *routed = TRUE;
     if (callout > 0)
       {
@@ -1720,89 +1859,92 @@ while (addr_new)
       transport. */
 
       transport_feedback tf = {
-        NULL,                       /* interface (=> any) */
-        US"smtp",                   /* port */
-        US"smtp",                   /* protocol */
-        NULL,                       /* hosts */
-        US"$smtp_active_hostname",  /* helo_data */
-        FALSE,                      /* hosts_override */
-        FALSE,                      /* hosts_randomize */
-        FALSE,                      /* gethostbyname */
-        TRUE,                       /* qualify_single */
-        FALSE                       /* search_parents */
+        .interface =           NULL,                       /* interface (=> any) */
+        .port =                        US"smtp",
+        .protocol =            US"smtp",
+        .hosts =               NULL,
+        .helo_data =           US"$smtp_active_hostname",
+        .hosts_override =      FALSE,
+        .hosts_randomize =     FALSE,
+        .gethostbyname =       FALSE,
+        .qualify_single =      TRUE,
+        .search_parents =      FALSE
         };
 
       /* If verification yielded a remote transport, we want to use that
       transport's options, so as to mimic what would happen if we were really
       sending a message to this address. */
 
-      if ((tp = addr->transport) && !tp->info->local)
-        {
-        (void)(tp->setup)(tp, addr, &tf, 0, 0, NULL);
+      if ((tp = addr->transport))
+       if (!tp->info->local)
+         {
+         (void)(tp->setup)(tp, addr, &tf, 0, 0, NULL);
 
-        /* If the transport has hosts and the router does not, or if the
-        transport is configured to override the router's hosts, we must build a
-        host list of the transport's hosts, and find the IP addresses */
+         /* If the transport has hosts and the router does not, or if the
+         transport is configured to override the router's hosts, we must build a
+         host list of the transport's hosts, and find the IP addresses */
 
-        if (tf.hosts && (!host_list || tf.hosts_override))
-          {
-          uschar *s;
-          const uschar *save_deliver_domain = deliver_domain;
-          uschar *save_deliver_localpart = deliver_localpart;
-
-          host_list = NULL;    /* Ignore the router's hosts */
-
-          deliver_domain = addr->domain;
-          deliver_localpart = addr->local_part;
-          s = expand_string(tf.hosts);
-          deliver_domain = save_deliver_domain;
-          deliver_localpart = save_deliver_localpart;
-
-          if (!s)
-            {
-            log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand list of hosts "
-              "\"%s\" in %s transport for callout: %s", tf.hosts,
-              tp->name, expand_string_message);
-            }
-          else
-            {
-            int flags;
-            host_item *host, *nexthost;
-            host_build_hostlist(&host_list, s, tf.hosts_randomize);
-
-            /* Just ignore failures to find a host address. If we don't manage
-            to find any addresses, the callout will defer. Note that more than
-            one address may be found for a single host, which will result in
-            additional host items being inserted into the chain. Hence we must
-            save the next host first. */
-
-            flags = HOST_FIND_BY_A;
-            if (tf.qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE;
-            if (tf.search_parents) flags |= HOST_FIND_SEARCH_PARENTS;
-
-            for (host = host_list; host; host = nexthost)
-              {
-              nexthost = host->next;
-              if (tf.gethostbyname ||
-                  string_is_ip_address(host->name, NULL) != 0)
-                (void)host_find_byname(host, NULL, flags, NULL, TRUE);
-              else
+         if (tf.hosts && (!host_list || tf.hosts_override))
+           {
+           uschar *s;
+           const uschar *save_deliver_domain = deliver_domain;
+           uschar *save_deliver_localpart = deliver_localpart;
+
+           host_list = NULL;    /* Ignore the router's hosts */
+
+           deliver_domain = addr->domain;
+           deliver_localpart = addr->local_part;
+           s = expand_string(tf.hosts);
+           deliver_domain = save_deliver_domain;
+           deliver_localpart = save_deliver_localpart;
+
+           if (!s)
+             {
+             log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand list of hosts "
+               "\"%s\" in %s transport for callout: %s", tf.hosts,
+               tp->name, expand_string_message);
+             }
+           else
+             {
+             int flags;
+             host_build_hostlist(&host_list, s, tf.hosts_randomize);
+
+             /* Just ignore failures to find a host address. If we don't manage
+             to find any addresses, the callout will defer. Note that more than
+             one address may be found for a single host, which will result in
+             additional host items being inserted into the chain. Hence we must
+             save the next host first. */
+
+             flags = HOST_FIND_BY_A | HOST_FIND_BY_AAAA;
+             if (tf.qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE;
+             if (tf.search_parents) flags |= HOST_FIND_SEARCH_PARENTS;
+
+             for (host_item * host = host_list, * nexthost; host; host = nexthost)
                {
-               dnssec_domains * dnssec_domains = NULL;
-               if (Ustrcmp(tp->driver_name, "smtp") == 0)
+               nexthost = host->next;
+               if (tf.gethostbyname ||
+                   string_is_ip_address(host->name, NULL) != 0)
+                 (void)host_find_byname(host, NULL, flags, NULL, TRUE);
+               else
                  {
-                 smtp_transport_options_block * ob =
-                     (smtp_transport_options_block *) tp->options_block;
-                 dnssec_domains = &ob->dnssec;
-                 }
+                 const dnssec_domains * dsp = NULL;
+                 if (Ustrcmp(tp->driver_name, "smtp") == 0)
+                   {
+                   smtp_transport_options_block * ob =
+                       (smtp_transport_options_block *) tp->options_block;
+                   dsp = &ob->dnssec;
+                   }
 
-                (void)host_find_bydns(host, NULL, flags, NULL, NULL, NULL,
-                 dnssec_domains, NULL, NULL);
+                 (void) host_find_bydns(host, NULL, flags, NULL, NULL, NULL,
+                   dsp, NULL, NULL);
+                 }
                }
-              }
-            }
-          }
-        }
+             }
+           }
+         }
+       else if (  options & vopt_quota
+               && Ustrcmp(tp->driver_name, "appendfile") == 0)
+         local_verify = TRUE;
 
       /* Can only do a callout if we have at least one host! If the callout
       fails, it will have set ${sender,recipient}_verify_failure. */
@@ -1810,7 +1952,7 @@ while (addr_new)
       if (host_list)
         {
         HDEBUG(D_verify) debug_printf("Attempting full verification using callout\n");
-        if (host_checking && !host_checking_callout)
+        if (host_checking && !f.host_checking_callout)
           {
           HDEBUG(D_verify)
             debug_printf("... callout omitted by default when host testing\n"
@@ -1818,18 +1960,27 @@ while (addr_new)
           }
         else
           {
-#ifdef SUPPORT_TLS
+#ifndef DISABLE_TLS
          deliver_set_expansions(addr);
 #endif
           rc = do_callout(addr, host_list, &tf, callout, callout_overall,
             callout_connect, options, se_mailfrom, pm_mailfrom);
+#ifndef DISABLE_TLS
+         deliver_set_expansions(NULL);
+#endif
           }
         }
+      else if (local_verify)
+       {
+        HDEBUG(D_verify) debug_printf("Attempting quota verification\n");
+
+       deliver_set_expansions(addr);
+       deliver_local(addr, TRUE);
+       rc = addr->transport_return;
+       }
       else
-        {
         HDEBUG(D_verify) debug_printf("Cannot do callout: neither router nor "
-          "transport provided a host list\n");
-        }
+          "transport provided a host list, or transport is not smtp\n");
       }
     }
 
@@ -1848,31 +1999,31 @@ while (addr_new)
   if (rc == FAIL)
     {
     allok = FALSE;
-    if (f)
+    if (fp)
       {
       address_item *p = addr->parent;
 
-      respond_printf(f, "%s%s %s", ko_prefix,
+      respond_printf(fp, "%s%s %s", ko_prefix,
         full_info ? addr->address : address,
-        address_test_mode ? "is undeliverable" : "failed to verify");
-      if (!expn && admin_user)
+        f.address_test_mode ? "is undeliverable" : "failed to verify");
+      if (!expn && f.admin_user)
         {
         if (addr->basic_errno > 0)
-          respond_printf(f, ": %s", strerror(addr->basic_errno));
+          respond_printf(fp, ": %s", strerror(addr->basic_errno));
         if (addr->message)
-          respond_printf(f, ": %s", addr->message);
+          respond_printf(fp, ": %s", addr->message);
         }
 
       /* Show parents iff doing full info */
 
       if (full_info) while (p)
         {
-        respond_printf(f, "%s\n    <-- %s", cr, p->address);
+        respond_printf(fp, "%s\n    <-- %s", cr, p->address);
         p = p->parent;
         }
-      respond_printf(f, "%s\n", cr);
+      respond_printf(fp, "%s\n", cr);
       }
-    cancel_cutthrough_connection("routing hard fail");
+    cancel_cutthrough_connection(TRUE, US"routing hard fail");
 
     if (!full_info)
       {
@@ -1887,31 +2038,31 @@ while (addr_new)
   else if (rc == DEFER)
     {
     allok = FALSE;
-    if (f)
+    if (fp)
       {
       address_item *p = addr->parent;
-      respond_printf(f, "%s%s cannot be resolved at this time", ko_prefix,
+      respond_printf(fp, "%s%s cannot be resolved at this time", ko_prefix,
         full_info? addr->address : address);
-      if (!expn && admin_user)
+      if (!expn && f.admin_user)
         {
         if (addr->basic_errno > 0)
-          respond_printf(f, ": %s", strerror(addr->basic_errno));
+          respond_printf(fp, ": %s", strerror(addr->basic_errno));
         if (addr->message)
-          respond_printf(f, ": %s", addr->message);
+          respond_printf(fp, ": %s", addr->message);
         else if (addr->basic_errno <= 0)
-          respond_printf(f, ": unknown error");
+          respond_printf(fp, ": unknown error");
         }
 
       /* Show parents iff doing full info */
 
       if (full_info) while (p)
         {
-        respond_printf(f, "%s\n    <-- %s", cr, p->address);
+        respond_printf(fp, "%s\n    <-- %s", cr, p->address);
         p = p->parent;
         }
-      respond_printf(f, "%s\n", cr);
+      respond_printf(fp, "%s\n", cr);
       }
-    cancel_cutthrough_connection("routing soft fail");
+    cancel_cutthrough_connection(TRUE, US"routing soft fail");
 
     if (!full_info)
       {
@@ -1930,16 +2081,16 @@ while (addr_new)
 
     if (!addr_new)
       if (!addr_local && !addr_remote)
-        respond_printf(f, "250 mail to <%s> is discarded\r\n", address);
+        respond_printf(fp, "250 mail to <%s> is discarded\r\n", address);
       else
-        respond_printf(f, "250 <%s>\r\n", address);
+        respond_printf(fp, "250 <%s>\r\n", address);
 
     else do
       {
       address_item *addr2 = addr_new;
       addr_new = addr2->next;
       if (!addr_new) ok_prefix = US"250 ";
-      respond_printf(f, "%s<%s>\r\n", ok_prefix, addr2->address);
+      respond_printf(fp, "%s<%s>\r\n", ok_prefix, addr2->address);
       } while (addr_new);
     yield = OK;
     goto out;
@@ -1973,18 +2124,20 @@ while (addr_new)
          )  )
        )
       {
-      if (f) fprintf(f, "%s %s\n",
-        address, address_test_mode ? "is deliverable" : "verified");
+      if (fp) fprintf(fp, "%s %s\n",
+        address, f.address_test_mode ? "is deliverable" : "verified");
 
       /* If we have carried on to verify a child address, we want the value
       of $address_data to be that of the child */
 
       vaddr->prop.address_data = addr->prop.address_data;
+      vaddr->prop.variables = NULL;
+      tree_dup((tree_node **)&vaddr->prop.variables, addr->prop.variables);
 
       /* If stopped because more than one new address, cannot cutthrough */
 
       if (addr_new && addr_new->next)
-       cancel_cutthrough_connection("multiple addresses from routing");
+       cancel_cutthrough_connection(TRUE, US"multiple addresses from routing");
 
       yield = OK;
       goto out;
@@ -1993,7 +2146,7 @@ while (addr_new)
   }     /* Loop for generated addresses */
 
 /* Display the full results of the successful routing, including any generated
-addresses. Control gets here only when full_info is set, which requires f not
+addresses. Control gets here only when full_info is set, which requires fp not
 to be NULL, and this occurs only when a top-level verify is called with the
 debugging switch on.
 
@@ -2003,7 +2156,7 @@ discarded, usually because of the use of :blackhole: in an alias file. */
 
 if (allok && !addr_local && !addr_remote)
   {
-  fprintf(f, "mail to %s is discarded\n", address);
+  fprintf(fp, "mail to %s is discarded\n", address);
   goto out;
   }
 
@@ -2011,15 +2164,14 @@ for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++)
   while (addr_list)
     {
     address_item *addr = addr_list;
-    address_item *p = addr->parent;
     transport_instance * tp = addr->transport;
 
     addr_list = addr->next;
 
-    fprintf(f, "%s", CS addr->address);
-#ifdef EXPERIMENTAL_SRS
+    fprintf(fp, "%s", CS addr->address);
+#ifdef EXPERIMENTAL_SRS_ALT
     if(addr->prop.srs_sender)
-      fprintf(f, "    [srs = %s]", addr->prop.srs_sender);
+      fprintf(fp, "    [srs = %s]", addr->prop.srs_sender);
 #endif
 
     /* If the address is a duplicate, show something about it. */
@@ -2028,19 +2180,19 @@ for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++)
       {
       tree_node *tnode;
       if ((tnode = tree_search(tree_duplicates, addr->unique)))
-        fprintf(f, "   [duplicate, would not be delivered]");
+        fprintf(fp, "   [duplicate, would not be delivered]");
       else tree_add_duplicate(addr->unique, addr);
       }
 
     /* Now show its parents */
 
-    for (p = addr->parent; p; p = p->parent)
-      fprintf(f, "\n    <-- %s", p->address);
-    fprintf(f, "\n  ");
+    for (address_item * p = addr->parent; p; p = p->parent)
+      fprintf(fp, "\n    <-- %s", p->address);
+    fprintf(fp, "\n  ");
 
     /* Show router, and transport */
 
-    fprintf(f, "router = %s, transport = %s\n",
+    fprintf(fp, "router = %s, transport = %s\n",
       addr->router->name, tp ? tp->name : US"unset");
 
     /* Show any hosts that are set up by a router unless the transport
@@ -2048,32 +2200,31 @@ for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++)
 
     if (addr->host_list && tp && !tp->overrides_hosts)
       {
-      host_item *h;
       int maxlen = 0;
       int maxaddlen = 0;
-      for (h = addr->host_list; h; h = h->next)
+      for (host_item * h = addr->host_list; h; h = h->next)
         {                              /* get max lengths of host names, addrs */
         int len = Ustrlen(h->name);
         if (len > maxlen) maxlen = len;
         len = h->address ? Ustrlen(h->address) : 7;
         if (len > maxaddlen) maxaddlen = len;
         }
-      for (h = addr->host_list; h; h = h->next)
+      for (host_item * h = addr->host_list; h; h = h->next)
        {
-       fprintf(f, "  host %-*s ", maxlen, h->name);
+       fprintf(fp, "  host %-*s ", maxlen, h->name);
 
        if (h->address)
-         fprintf(f, "[%s%-*c", h->address, maxaddlen+1 - Ustrlen(h->address), ']');
+         fprintf(fp, "[%s%-*c", h->address, maxaddlen+1 - Ustrlen(h->address), ']');
        else if (tp->info->local)
-         fprintf(f, " %-*s ", maxaddlen, "");  /* Omit [unknown] for local */
+         fprintf(fp, " %-*s ", maxaddlen, "");  /* Omit [unknown] for local */
        else
-         fprintf(f, "[%s%-*c", "unknown", maxaddlen+1 - 7, ']');
+         fprintf(fp, "[%s%-*c", "unknown", maxaddlen+1 - 7, ']');
 
-        if (h->mx >= 0) fprintf(f, " MX=%d", h->mx);
-        if (h->port != PORT_NONE) fprintf(f, " port=%d", h->port);
-        if (running_in_test_harness  &&  h->dnssec == DS_YES) fputs(" AD", f);
-        if (h->status == hstatus_unusable) fputs(" ** unusable **", f);
-       fputc('\n', f);
+        if (h->mx >= 0) fprintf(fp, " MX=%d", h->mx);
+        if (h->port != PORT_NONE) fprintf(fp, " port=%d", h->port);
+        if (f.running_in_test_harness  &&  h->dnssec == DS_YES) fputs(" AD", fp);
+        if (h->status == hstatus_unusable) fputs(" ** unusable **", fp);
+       fputc('\n', fp);
         }
       }
     }
@@ -2083,7 +2234,7 @@ the -bv or -bt case). */
 
 out:
 verify_mode = NULL;
-tls_modify_variables(&tls_in);
+tls_modify_variables(&tls_in); /* return variables to inbound values */
 
 return yield;
 }
@@ -2096,7 +2247,7 @@ return yield;
 *************************************************/
 
 /* This function checks those header lines that contain addresses, and verifies
-that all the addresses therein are syntactially correct.
+that all the addresses therein are 5322-syntactially correct.
 
 Arguments:
   msgptr     where to put an error message
@@ -2108,11 +2259,10 @@ Returns:     OK
 int
 verify_check_headers(uschar **msgptr)
 {
-header_line *h;
 uschar *colon, *s;
 int yield = OK;
 
-for (h = header_list; h != NULL && yield == OK; h = h->next)
+for (header_line * h = header_list; h && yield == OK; h = h->next)
   {
   if (h->type != htype_from &&
       h->type != htype_reply_to &&
@@ -2124,14 +2274,14 @@ for (h = header_list; h != NULL && yield == OK; h = h->next)
 
   colon = Ustrchr(h->text, ':');
   s = colon + 1;
-  while (isspace(*s)) s++;
+  Uskip_whitespace(&s);
 
   /* Loop for multiple addresses in the header, enabling group syntax. Note
   that we have to reset this after the header has been scanned. */
 
-  parse_allow_group = TRUE;
+  f.parse_allow_group = TRUE;
 
-  while (*s != 0)
+  while (*s)
     {
     uschar *ss = parse_find_address_end(s, FALSE);
     uschar *recipient, *errmess;
@@ -2148,23 +2298,23 @@ for (h = header_list; h != NULL && yield == OK; h = h->next)
     /* Permit an unqualified address only if the message is local, or if the
     sending host is configured to be permitted to send them. */
 
-    if (recipient != NULL && domain == 0)
+    if (recipient && !domain)
       {
       if (h->type == htype_from || h->type == htype_sender)
         {
-        if (!allow_unqualified_sender) recipient = NULL;
+        if (!f.allow_unqualified_sender) recipient = NULL;
         }
       else
         {
-        if (!allow_unqualified_recipient) recipient = NULL;
+        if (!f.allow_unqualified_recipient) recipient = NULL;
         }
-      if (recipient == NULL) errmess = US"unqualified address not permitted";
+      if (!recipient) errmess = US"unqualified address not permitted";
       }
 
     /* It's an error if no address could be extracted, except for the special
     case of an empty address. */
 
-    if (recipient == NULL && Ustrcmp(errmess, "empty address") != 0)
+    if (!recipient && Ustrcmp(errmess, "empty address") != 0)
       {
       uschar *verb = US"is";
       uschar *t = ss;
@@ -2194,7 +2344,7 @@ for (h = header_list; h != NULL && yield == OK; h = h->next)
       /* deconst cast ok as we're passing a non-const to string_printing() */
       *msgptr = US string_printing(
         string_sprintf("%s: failing address in \"%.*s:\" header %s: %.*s",
-          errmess, tt - h->text, h->text, verb, len, s));
+          errmess, (int)(tt - h->text), h->text, verb, len, s));
 
       yield = FAIL;
       break;          /* Out of address loop */
@@ -2202,12 +2352,12 @@ for (h = header_list; h != NULL && yield == OK; h = h->next)
 
     /* Advance to the next address */
 
-    s = ss + (terminator? 1:0);
-    while (isspace(*s)) s++;
+    s = ss + (terminator ? 1 : 0);
+    Uskip_whitespace(&s);
     }   /* Next address */
 
-  parse_allow_group = FALSE;
-  parse_found_group = FALSE;
+  f.parse_allow_group = FALSE;
+  f.parse_found_group = FALSE;
   }     /* Next header unless yield has been set FALSE */
 
 return yield;
@@ -2231,21 +2381,18 @@ Returns:     OK
 int
 verify_check_header_names_ascii(uschar **msgptr)
 {
-header_line *h;
-uschar *colon, *s;
+uschar *colon;
 
-for (h = header_list; h != NULL; h = h->next)
+for (header_line * h = header_list; h; h = h->next)
   {
-   colon = Ustrchr(h->text, ':');
-   for(s = h->text; s < colon; s++)
-     {
-        if ((*s < 33) || (*s > 126))
-        {
-                *msgptr = string_sprintf("Invalid character in header \"%.*s\" found",
-                                         colon - h->text, h->text);
-                return FAIL;
-        }
-     }
+  colon = Ustrchr(h->text, ':');
+  for(uschar * s = h->text; s < colon; s++)
+    if ((*s < 33) || (*s > 126))
+      {
+      *msgptr = string_sprintf("Invalid character in header \"%.*s\" found",
+                            (int)(colon - h->text), h->text);
+      return FAIL;
+      }
   }
 return OK;
 }
@@ -2263,22 +2410,20 @@ The original proposed patch did the former, but I have chosen to do the latter,
 because (a) it requires no memory and (b) will use fewer resources when there
 are many addresses in To: and/or Cc: and only one or two envelope recipients.
 
-Arguments:   none
+Arguments:   case_sensitive   true if case sensitive matching should be used
 Returns:     OK    if there are no blind recipients
              FAIL  if there is at least one blind recipient
 */
 
 int
-verify_check_notblind(void)
+verify_check_notblind(BOOL case_sensitive)
 {
-int i;
-for (i = 0; i < recipients_count; i++)
+for (int i = 0; i < recipients_count; i++)
   {
-  header_line *h;
   BOOL found = FALSE;
   uschar *address = recipients_list[i].address;
 
-  for (h = header_list; !found && h != NULL; h = h->next)
+  for (header_line * h = header_list; !found && h; h = h->next)
     {
     uschar *colon, *s;
 
@@ -2286,17 +2431,17 @@ for (i = 0; i < recipients_count; i++)
 
     colon = Ustrchr(h->text, ':');
     s = colon + 1;
-    while (isspace(*s)) s++;
+    Uskip_whitespace(&s);
 
     /* Loop for multiple addresses in the header, enabling group syntax. Note
     that we have to reset this after the header has been scanned. */
 
-    parse_allow_group = TRUE;
+    f.parse_allow_group = TRUE;
 
-    while (*s != 0)
+    while (*s)
       {
-      uschar *ss = parse_find_address_end(s, FALSE);
-      uschar *recipient,*errmess;
+      uschar * ss = parse_find_address_end(s, FALSE);
+      uschar * recipient, * errmess;
       int terminator = *ss;
       int start, end, domain;
 
@@ -2308,26 +2453,27 @@ for (i = 0; i < recipients_count; i++)
       *ss = terminator;
 
       /* If we found a valid recipient that has a domain, compare it with the
-      envelope recipient. Local parts are compared case-sensitively, domains
-      case-insensitively. By comparing from the start with length "domain", we
-      include the "@" at the end, which ensures that we are comparing the whole
-      local part of each address. */
-
-      if (recipient != NULL && domain != 0)
-        {
-        found = Ustrncmp(recipient, address, domain) == 0 &&
-                strcmpic(recipient + domain, address + domain) == 0;
-        if (found) break;
-        }
+      envelope recipient. Local parts are compared with case-sensitivity
+      according to the routine arg, domains case-insensitively.
+      By comparing from the start with length "domain", we include the "@" at
+      the end, which ensures that we are comparing the whole local part of each
+      address. */
+
+      if (recipient && domain != 0)
+        if ((found = (case_sensitive
+               ? Ustrncmp(recipient, address, domain) == 0
+               : strncmpic(recipient, address, domain) == 0)
+             && strcmpic(recipient + domain, address + domain) == 0))
+         break;
 
       /* Advance to the next address */
 
-      s = ss + (terminator? 1:0);
-      while (isspace(*s)) s++;
+      s = ss + (terminator ? 1:0);
+      Uskip_whitespace(&s);
       }   /* Next address */
 
-    parse_allow_group = FALSE;
-    parse_found_group = FALSE;
+    f.parse_allow_group = FALSE;
+    f.parse_found_group = FALSE;
     }     /* Next header (if found is false) */
 
   if (!found) return FAIL;
@@ -2355,10 +2501,9 @@ Returns:     pointer to an address item, or NULL
 address_item *
 verify_checked_sender(uschar *sender)
 {
-address_item *addr;
-for (addr = sender_verified_list; addr != NULL; addr = addr->next)
-  if (Ustrcmp(sender, addr->address) == 0) break;
-return addr;
+for (address_item * addr = sender_verified_list; addr; addr = addr->next)
+  if (Ustrcmp(sender, addr->address) == 0) return addr;
+return NULL;
 }
 
 
@@ -2412,12 +2557,9 @@ verify_check_header_address(uschar **user_msgptr, uschar **log_msgptr,
 static int header_types[] = { htype_sender, htype_reply_to, htype_from };
 BOOL done = FALSE;
 int yield = FAIL;
-int i;
 
-for (i = 0; i < 3 && !done; i++)
-  {
-  header_line *h;
-  for (h = header_list; h != NULL && !done; h = h->next)
+for (int i = 0; i < 3 && !done; i++)
+  for (header_line * h = header_list; h != NULL && !done; h = h->next)
     {
     int terminator, new_ok;
     uschar *s, *ss, *endname;
@@ -2428,7 +2570,7 @@ for (i = 0; i < 3 && !done; i++)
     /* Scan the addresses in the header, enabling group syntax. Note that we
     have to reset this after the header has been scanned. */
 
-    parse_allow_group = TRUE;
+    f.parse_allow_group = TRUE;
 
     while (*s != 0)
       {
@@ -2480,7 +2622,7 @@ for (i = 0; i < 3 && !done; i++)
         /* If we found an empty address, just carry on with the next one, but
         kill the message. */
 
-        if (address == NULL && Ustrcmp(*log_msgptr, "empty address") == 0)
+        if (!address && Ustrcmp(*log_msgptr, "empty address") == 0)
           {
           *log_msgptr = NULL;
           s = ss;
@@ -2491,13 +2633,13 @@ for (i = 0; i < 3 && !done; i++)
         function, and ensure that the failing address gets added to the error
         message. */
 
-        if (address == NULL)
+        if (!address)
           {
           new_ok = FAIL;
           while (ss > s && isspace(ss[-1])) ss--;
           *log_msgptr = string_sprintf("syntax error in '%.*s' header when "
             "scanning for sender: %s in \"%.*s\"",
-            endname - h->text, h->text, *log_msgptr, ss - s, s);
+            (int)(endname - h->text), h->text, *log_msgptr, (int)(ss - s), s);
           yield = FAIL;
           done = TRUE;
           break;
@@ -2525,11 +2667,9 @@ for (i = 0; i < 3 && !done; i++)
         {
         *verrno = vaddr->basic_errno;
         if (smtp_return_error_details)
-          {
           *user_msgptr = string_sprintf("Rejected after DATA: "
             "could not verify \"%.*s\" header address\n%s: %s",
-            endname - h->text, h->text, vaddr->address, vaddr->message);
-          }
+            (int)(endname - h->text), h->text, vaddr->address, vaddr->message);
         }
 
       /* Success or defer */
@@ -2548,10 +2688,10 @@ for (i = 0; i < 3 && !done; i++)
       s = ss;
       }     /* Next address */
 
-    parse_allow_group = FALSE;
-    parse_found_group = FALSE;
+    f.parse_allow_group = FALSE;
+    f.parse_found_group = FALSE;
     }       /* Next header, unless done */
-  }         /* Next header type unless done */
+           /* Next header type unless done */
 
 if (yield == FAIL && *log_msgptr == NULL)
   *log_msgptr = US"there is no valid sender in any header line";
@@ -2587,9 +2727,11 @@ Side effect: any received ident value is put in sender_ident (NULL otherwise)
 void
 verify_get_ident(int port)
 {
-int sock, host_af, qlen;
+client_conn_ctx ident_conn_ctx = {0};
+int host_af, qlen;
 int received_sender_port, received_interface_port, n;
 uschar *p;
+blob early_data;
 uschar buffer[2048];
 
 /* Default is no ident. Check whether we want to do an ident check for this
@@ -2606,17 +2748,25 @@ to the incoming interface address. If the sender host address is an IPv6
 address, the incoming interface address will also be IPv6. */
 
 host_af = Ustrchr(sender_host_address, ':') == NULL ? AF_INET : AF_INET6;
-if ((sock = ip_socket(SOCK_STREAM, host_af)) < 0) return;
+if ((ident_conn_ctx.sock = ip_socket(SOCK_STREAM, host_af)) < 0) return;
 
-if (ip_bind(sock, host_af, interface_address, 0) < 0)
+if (ip_bind(ident_conn_ctx.sock, host_af, interface_address, 0) < 0)
   {
   DEBUG(D_ident) debug_printf("bind socket for ident failed: %s\n",
     strerror(errno));
   goto END_OFF;
   }
 
-if (ip_connect(sock, host_af, sender_host_address, port,
-               rfc1413_query_timeout, TRUE) < 0)
+/* Construct and send the query. */
+
+qlen = snprintf(CS buffer, sizeof(buffer), "%d , %d\r\n",
+  sender_host_port, interface_port);
+early_data.data = buffer;
+early_data.len = qlen;
+
+/*XXX we trust that the query is idempotent */
+if (ip_connect(ident_conn_ctx.sock, host_af, sender_host_address, port,
+               rfc1413_query_timeout, &early_data) < 0)
   {
   if (errno == ETIMEDOUT && LOGGING(ident_timeout))
     log_write(0, LOG_MAIN, "ident connection to %s timed out",
@@ -2627,16 +2777,6 @@ if (ip_connect(sock, host_af, sender_host_address, port,
   goto END_OFF;
   }
 
-/* Construct and send the query. */
-
-sprintf(CS buffer, "%d , %d\r\n", sender_host_port, interface_port);
-qlen = Ustrlen(buffer);
-if (send(sock, buffer, qlen, 0) < 0)
-  {
-  DEBUG(D_ident) debug_printf("ident send failed: %s\n", strerror(errno));
-  goto END_OFF;
-  }
-
 /* Read a response line. We put it into the rest of the buffer, using several
 recv() calls if necessary. */
 
@@ -2649,7 +2789,7 @@ for (;;)
   int size = sizeof(buffer) - (p - buffer);
 
   if (size <= 0) goto END_OFF;   /* Buffer filled without seeing \n. */
-  count = ip_recv(sock, p, size, rfc1413_query_timeout);
+  count = ip_recv(&ident_conn_ctx, p, size, time(NULL) + rfc1413_query_timeout);
   if (count <= 0) goto END_OFF;  /* Read error or EOF */
 
   /* Scan what we just read, to see if we have reached the terminating \r\n. Be
@@ -2714,7 +2854,7 @@ sender_ident = US string_printing(string_copyn(p, 127));
 DEBUG(D_ident) debug_printf("sender_ident = %s\n", sender_ident);
 
 END_OFF:
-(void)close(sock);
+(void)close(ident_conn_ctx.sock);
 return;
 }
 
@@ -2764,7 +2904,7 @@ BOOL iplookup = FALSE;
 BOOL isquery = FALSE;
 BOOL isiponly = cb->host_name != NULL && cb->host_name[0] == 0;
 const uschar *t;
-uschar *semicolon;
+uschar * semicolon, * endname, * opts;
 uschar **aliases;
 
 /* Optimize for the special case when the pattern is "*". */
@@ -2783,7 +2923,6 @@ provided that host name matching is permitted; if it's "@[]" match against the
 local host's IP addresses. */
 
 if (*ss == '@')
-  {
   if (ss[1] == 0)
     {
     if (isiponly) return ERROR;
@@ -2791,12 +2930,10 @@ if (*ss == '@')
     }
   else if (Ustrcmp(ss, "@[]") == 0)
     {
-    ip_address_item *ip;
-    for (ip = host_find_interfaces(); ip != NULL; ip = ip->next)
+    for (ip_address_item * ip = host_find_interfaces(); ip; ip = ip->next)
       if (Ustrcmp(ip->address, cb->host_address) == 0) return OK;
     return FAIL;
     }
-  }
 
 /* If the pattern is an IP address, optionally followed by a bitmask count, do
 a (possibly masked) comparison with the current IP address. */
@@ -2815,24 +2952,33 @@ only by digits and dots (a slash at the start indicates a file name and of
 course slashes may be present in lookups, but not preceded only by digits and
 dots). */
 
-for (t = ss; isdigit(*t) || *t == '.'; t++);
+for (t = ss; isdigit(*t) || *t == '.'; ) t++;
 if (*t == 0 || (*t == '/' && t != ss))
   {
   *error = US"malformed IPv4 address or address mask";
   return ERROR;
   }
 
-/* See if there is a semicolon in the pattern */
+/* See if there is a semicolon in the pattern, separating a searchtype
+prefix.  If there is one then check for comma-sep options. */
 
-semicolon = Ustrchr(ss, ';');
+if ((semicolon = Ustrchr(ss, ';')))
+  if ((opts = Ustrchr(ss, ',')) && opts < semicolon)
+    {
+    endname = opts++;
+    opts = string_copyn(opts, semicolon - opts);
+    }
+  else
+    {
+    endname = semicolon;
+    opts = NULL;
+    }
 
 /* If we are doing an IP address only match, then all lookups must be IP
 address lookups, even if there is no "net-". */
 
 if (isiponly)
-  {
   iplookup = semicolon != NULL;
-  }
 
 /* Otherwise, if the item is of the form net[n]-lookup;<file|query> then it is
 a lookup on a masked IP network, in textual form. We obey this code even if we
@@ -2842,14 +2988,15 @@ key is implicit. For query-style lookups the key is specified in the query.
 From release 4.30, the use of net- for query style is no longer needed, but we
 retain it for backward compatibility. */
 
-if (Ustrncmp(ss, "net", 3) == 0 && semicolon != NULL)
+if (Ustrncmp(ss, "net", 3) == 0 && semicolon)
   {
   mlen = 0;
   for (t = ss + 3; isdigit(*t); t++) mlen = mlen * 10 + *t - '0';
   if (mlen == 0 && t == ss+3) mlen = -1;  /* No mask supplied */
-  iplookup = (*t++ == '-');
+  iplookup = *t++ == '-';
   }
-else t = ss;
+else
+  t = ss;
 
 /* Do the IP address lookup if that is indeed what we have */
 
@@ -2864,7 +3011,7 @@ if (iplookup)
 
   /* Find the search type */
 
-  search_type = search_findtype(t, semicolon - t);
+  search_type = search_findtype(t, endname - t);
 
   if (search_type < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s",
     search_error_message);
@@ -2907,9 +3054,9 @@ if (iplookup)
   if (!(handle = search_open(filename, search_type, 0, NULL, NULL)))
     log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", search_error_message);
 
-  result = search_find(handle, filename, key, -1, NULL, 0, 0, NULL);
-  if (valueptr != NULL) *valueptr = result;
-  return (result != NULL)? OK : search_find_defer? DEFER: FAIL;
+  result = search_find(handle, filename, key, -1, NULL, 0, 0, NULL, opts);
+  if (valueptr) *valueptr = result;
+  return result ? OK : f.search_find_defer ? DEFER: FAIL;
   }
 
 /* The pattern is not an IP address or network reference of any kind. That is,
@@ -2951,11 +3098,8 @@ if (*t == 0)
   rc = host_find_byname(&h, NULL, HOST_FIND_QUALIFY_SINGLE, NULL, FALSE);
   if (rc == HOST_FOUND || rc == HOST_FOUND_LOCAL)
     {
-    host_item *hh;
-    for (hh = &h; hh != NULL; hh = hh->next)
-      {
+    for (host_item * hh = &h; hh; hh = hh->next)
       if (host_is_in_net(hh->address, cb->host_address, 0)) return OK;
-      }
     return FAIL;
     }
   if (rc == HOST_FIND_AGAIN) return DEFER;
@@ -2968,7 +3112,7 @@ using the general string matching function. When this function is called for
 outgoing hosts, the name is always given explicitly. If it is NULL, it means we
 must use sender_host_name and its aliases, looking them up if necessary. */
 
-if (cb->host_name != NULL)   /* Explicit host name given */
+if (cb->host_name)   /* Explicit host name given */
   return match_check_string(cb->host_name, ss, -1, TRUE, TRUE, TRUE,
     valueptr);
 
@@ -2978,13 +3122,14 @@ query does not contain $sender_host_name. From release 4.23, a reference to
 $sender_host_name causes it to be looked up, so we don't need to do the lookup
 on spec. */
 
-if ((semicolon = Ustrchr(ss, ';')) != NULL)
+if ((semicolon = Ustrchr(ss, ';')))
   {
-  const uschar *affix;
+  const uschar * affix, * opts;
   int partial, affixlen, starflags, id;
 
   *semicolon = 0;
-  id = search_findtype_partial(ss, &partial, &affix, &affixlen, &starflags);
+  id = search_findtype_partial(ss, &partial, &affix, &affixlen, &starflags,
+         &opts);
   *semicolon=';';
 
   if (id < 0)                           /* Unknown lookup type */
@@ -3009,7 +3154,7 @@ if (isquery)
 /* Not a query-style lookup; must ensure the host name is present, and then we
 do a check on the name and all its aliases. */
 
-if (sender_host_name == NULL)
+if (!sender_host_name)
   {
   HDEBUG(D_host_lookup)
     debug_printf("sender host name required, to match against %s\n", ss);
@@ -3024,8 +3169,7 @@ if (sender_host_name == NULL)
 
 /* Match on the sender host name, using the general matching function */
 
-switch(match_check_string(sender_host_name, ss, -1, TRUE, TRUE, TRUE,
-       valueptr))
+switch(match_check_string(sender_host_name, ss, -1, TRUE, TRUE, TRUE, valueptr))
   {
   case OK:    return OK;
   case DEFER: return DEFER;
@@ -3034,14 +3178,12 @@ switch(match_check_string(sender_host_name, ss, -1, TRUE, TRUE, TRUE,
 /* If there are aliases, try matching on them. */
 
 aliases = sender_host_aliases;
-while (*aliases != NULL)
-  {
+while (*aliases)
   switch(match_check_string(*aliases++, ss, -1, TRUE, TRUE, TRUE, valueptr))
     {
     case OK:    return OK;
     case DEFER: return DEFER;
     }
-  }
 return FAIL;
 }
 
@@ -3087,18 +3229,16 @@ verify_check_this_host(const uschar **listptr, unsigned int *cache_bits,
 int rc;
 unsigned int *local_cache_bits = cache_bits;
 const uschar *save_host_address = deliver_host_address;
-check_host_block cb;
-cb.host_name = host_name;
-cb.host_address = host_address;
+check_host_block cb = { .host_name = host_name, .host_address = host_address };
 
-if (valueptr != NULL) *valueptr = NULL;
+if (valueptr) *valueptr = NULL;
 
 /* If the host address starts off ::ffff: it is an IPv6 address in
 IPv4-compatible mode. Find the IPv4 part for checking against IPv4
 addresses. */
 
-cb.host_ipv4 = (Ustrncmp(host_address, "::ffff:", 7) == 0)?
-  host_address + 7 : host_address;
+cb.host_ipv4 = Ustrncmp(host_address, "::ffff:", 7) == 0
+  host_address + 7 : host_address;
 
 /* During the running of the check, put the IP address into $host_address. In
 the case of calls from the smtp transport, it will already be there. However,
@@ -3129,9 +3269,9 @@ return rc;
 *      Check the given host item matches a list  *
 *************************************************/
 int
-verify_check_given_host(uschar **listptr, host_item *host)
+verify_check_given_host(const uschar **listptr, const host_item *host)
 {
-return verify_check_this_host(CUSS listptr, NULL, host->name, host->address, NULL);
+return verify_check_this_host(listptr, NULL, host->name, host->address, NULL);
 }
 
 /*************************************************
@@ -3154,7 +3294,7 @@ int
 verify_check_host(uschar **listptr)
 {
 return verify_check_this_host(CUSS listptr, sender_host_cache, NULL,
-  (sender_host_address == NULL)? US"" : sender_host_address, NULL);
+  sender_host_address ? sender_host_address : US"", NULL);
 }
 
 
@@ -3189,9 +3329,8 @@ always 1. */
 
 if (host_aton(address, bin) == 1)
   {
-  int i;
   int x = bin[0];
-  for (i = 0; i < 4; i++)
+  for (int i = 0; i < 4; i++)
     {
     sprintf(CS bptr, "%d.", x & 255);
     while (*bptr) bptr++;
@@ -3205,19 +3344,16 @@ unknown. This is just a guess. */
 
 #if HAVE_IPV6
 else
-  {
-  int i, j;
-  for (j = 3; j >= 0; j--)
+  for (int j = 3; j >= 0; j--)
     {
     int x = bin[j];
-    for (i = 0; i < 8; i++)
+    for (int i = 0; i < 8; i++)
       {
       sprintf(CS bptr, "%x.", x & 15);
       while (*bptr) bptr++;
       x >>= 4;
       }
     }
-  }
 #endif
 
 /* Remove trailing period -- this is needed so that both arbitrary
@@ -3264,16 +3400,18 @@ one_check_dnsbl(uschar *domain, uschar *domain_txt, uschar *keydomain,
   uschar *prepend, uschar *iplist, BOOL bitmask, int match_type,
   int defer_return)
 {
-dns_answer dnsa;
+dns_answer * dnsa = store_get_dns_answer();
 dns_scan dnss;
 tree_node *t;
 dnsbl_cache_block *cb;
 int old_pool = store_pool;
-uschar query[256];         /* DNS domain max length */
+uschar * query;
+int qlen;
 
 /* Construct the specific query domainname */
 
-if (!string_format(query, sizeof(query), "%s.%s", prepend, domain))
+query = string_sprintf("%s.%s", prepend, domain);
+if ((qlen = Ustrlen(query)) >= 256)
   {
   log_write(0, LOG_MAIN|LOG_PANIC, "dnslist query is too long "
     "(ignored): %s...", query);
@@ -3289,7 +3427,7 @@ if (  (t = tree_search(dnsbl_cache, query))
 /* Previous lookup was cached */
 
   {
-  HDEBUG(D_dnsbl) debug_printf("using result of previous DNS lookup\n");
+  HDEBUG(D_dnsbl) debug_printf("dnslists: using result of previous lookup\n");
   }
 
 /* If not cached from a previous lookup, we must do a DNS lookup, and
@@ -3297,7 +3435,7 @@ cache the result in permanent memory. */
 
 else
   {
-  uint ttl = 3600;
+  uint ttl = 3600;     /* max TTL for positive cache entries */
 
   store_pool = POOL_PERM;
 
@@ -3308,63 +3446,82 @@ else
 
   else
     {  /* Set up a tree entry to cache the lookup */
-    t = store_get(sizeof(tree_node) + Ustrlen(query));
+    t = store_get(sizeof(tree_node) + qlen + 1 + 1, is_tainted(query));
     Ustrcpy(t->name, query);
-    t->data.ptr = cb = store_get(sizeof(dnsbl_cache_block));
+    t->data.ptr = cb = store_get(sizeof(dnsbl_cache_block), FALSE);
     (void)tree_insertnode(&dnsbl_cache, t);
     }
 
   /* Do the DNS lookup . */
 
   HDEBUG(D_dnsbl) debug_printf("new DNS lookup for %s\n", query);
-  cb->rc = dns_basic_lookup(&dnsa, query, T_A);
+  cb->rc = dns_basic_lookup(dnsa, query, T_A);
   cb->text_set = FALSE;
   cb->text = NULL;
   cb->rhs = NULL;
 
   /* If the lookup succeeded, cache the RHS address. The code allows for
   more than one address - this was for complete generality and the possible
-  use of A6 records. However, A6 records have been reduced to experimental
-  status (August 2001) and may die out. So they may never get used at all,
-  let alone in dnsbl records. However, leave the code here, just in case.
+  use of A6 records. However, A6 records are no longer supported. Leave the code
+  here, just in case.
 
   Quite apart from one A6 RR generating multiple addresses, there are DNS
   lists that return more than one A record, so we must handle multiple
   addresses generated in that way as well.
 
   Mark the cache entry with the "now" plus the minimum of the address TTLs,
-  or some suitably far-future time if none were found. */
+  or the RFC 2308 negative-cache value from the SOA if none were found. */
 
-  if (cb->rc == DNS_SUCCEED)
+  switch (cb->rc)
     {
-    dns_record *rr;
-    dns_address **addrp = &(cb->rhs);
-    for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS);
-         rr;
-         rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT))
+    case DNS_SUCCEED:
       {
-      if (rr->type == T_A)
-        {
-        dns_address *da = dns_address_from_rr(&dnsa, rr);
-        if (da)
-          {
-          *addrp = da;
-          while (da->next != NULL) da = da->next;
-          addrp = &(da->next);
+      dns_address ** addrp = &cb->rhs;
+      dns_address * da;
+      for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
+          rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
+       if (rr->type == T_A && (da = dns_address_from_rr(dnsa, rr)))
+         {
+         *addrp = da;
+         while (da->next) da = da->next;
+         addrp = &da->next;
          if (ttl > rr->ttl) ttl = rr->ttl;
-          }
-        }
+         }
+
+      if (cb->rhs)
+       {
+       cb->expiry = time(NULL) + ttl;
+       break;
+       }
+
+      /* If we didn't find any A records, change the return code. This can
+      happen when there is a CNAME record but there are no A records for what
+      it points to. */
+
+      cb->rc = DNS_NODATA;
       }
+      /*FALLTHROUGH*/
 
-    /* If we didn't find any A records, change the return code. This can
-    happen when there is a CNAME record but there are no A records for what
-    it points to. */
+    case DNS_NOMATCH:
+    case DNS_NODATA:
+      {
+      /* Although there already is a neg-cache layer maintained by
+      dns_basic_lookup(), we have a dnslist cache entry allocated and
+      tree-inserted. So we may as well use it. */
 
-    if (cb->rhs == NULL) cb->rc = DNS_NODATA;
+      time_t soa_negttl = dns_expire_from_soa(dnsa, T_A);
+      cb->expiry = soa_negttl ? soa_negttl : time(NULL) + ttl;
+      break;
+      }
+
+    default:
+      cb->expiry = time(NULL) + ttl;
+      break;
     }
 
-  cb->expiry = time(NULL)+ttl;
   store_pool = old_pool;
+  HDEBUG(D_dnsbl) debug_printf("dnslists: wrote cache entry, ttl=%d\n",
+    (int)(cb->expiry - time(NULL)));
   }
 
 /* We now have the result of the DNS lookup, either newly done, or cached
@@ -3375,14 +3532,14 @@ list (introduced by "&"), or a negative bitmask list (introduced by "!&").*/
 
 if (cb->rc == DNS_SUCCEED)
   {
-  dns_address *da = NULL;
+  dns_address * da = NULL;
   uschar *addlist = cb->rhs->address;
 
   /* For A and AAAA records, there may be multiple addresses from multiple
   records. For A6 records (currently not expected to be used) there may be
   multiple addresses from a single record. */
 
-  for (da = cb->rhs->next; da != NULL; da = da->next)
+  for (da = cb->rhs->next; da; da = da->next)
     addlist = string_sprintf("%s, %s", addlist, da->address);
 
   HDEBUG(D_dnsbl) debug_printf("DNS lookup for %s succeeded (yielding %s)\n",
@@ -3391,24 +3548,22 @@ if (cb->rc == DNS_SUCCEED)
   /* Address list check; this can be either for equality, or via a bitmask.
   In the latter case, all the bits must match. */
 
-  if (iplist != NULL)
+  if (iplist)
     {
-    for (da = cb->rhs; da != NULL; da = da->next)
+    for (da = cb->rhs; da; da = da->next)
       {
       int ipsep = ',';
-      uschar ip[46];
       const uschar *ptr = iplist;
       uschar *res;
 
       /* Handle exact matching */
 
       if (!bitmask)
-        {
-        while ((res = string_nextinlist(&ptr, &ipsep, ip, sizeof(ip))) != NULL)
-          {
-          if (Ustrcmp(CS da->address, ip) == 0) break;
-          }
-        }
+       {
+        while ((res = string_nextinlist(&ptr, &ipsep, NULL, 0)))
+          if (Ustrcmp(CS da->address, res) == 0)
+           break;
+       }
 
       /* Handle bitmask matching */
 
@@ -3428,9 +3583,9 @@ if (cb->rc == DNS_SUCCEED)
 
         /* Scan the returned addresses, skipping any that are IPv6 */
 
-        while ((res = string_nextinlist(&ptr, &ipsep, ip, sizeof(ip))) != NULL)
+        while ((res = string_nextinlist(&ptr, &ipsep, NULL, 0)))
           {
-          if (host_aton(ip, address) != 1) continue;
+          if (host_aton(res, address) != 1) continue;
           if ((address[0] & mask) == address[0]) break;
           }
         }
@@ -3461,17 +3616,13 @@ if (cb->rc == DNS_SUCCEED)
         switch(match_type)
           {
           case 0:
-          res = US"was no match";
-          break;
+           res = US"was no match"; break;
           case MT_NOT:
-          res = US"was an exclude match";
-          break;
+           res = US"was an exclude match"; break;
           case MT_ALL:
-          res = US"was an IP address that did not match";
-          break;
+           res = US"was an IP address that did not match"; break;
           case MT_NOT|MT_ALL:
-          res = US"were no IP addresses that did not match";
-          break;
+           res = US"were no IP addresses that did not match"; break;
           }
         debug_printf("=> but we are not accepting this block class because\n");
         debug_printf("=> there %s for %s%c%s\n",
@@ -3499,22 +3650,18 @@ if (cb->rc == DNS_SUCCEED)
   if (!cb->text_set)
     {
     cb->text_set = TRUE;
-    if (dns_basic_lookup(&dnsa, query, T_TXT) == DNS_SUCCEED)
-      {
-      dns_record *rr;
-      for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS);
-           rr != NULL;
-           rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT))
-        if (rr->type == T_TXT) break;
-      if (rr != NULL)
-        {
-        int len = (rr->data)[0];
-        if (len > 511) len = 127;
-        store_pool = POOL_PERM;
-        cb->text = string_sprintf("%.*s", len, (const uschar *)(rr->data+1));
-        store_pool = old_pool;
-        }
-      }
+    if (dns_basic_lookup(dnsa, query, T_TXT) == DNS_SUCCEED)
+      for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
+           rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
+        if (rr->type == T_TXT)
+         {
+         int len = (rr->data)[0];
+         if (len > 511) len = 127;
+         store_pool = POOL_PERM;
+         cb->text = string_sprintf("%.*s", len, CUS (rr->data+1));
+         store_pool = old_pool;
+         break;
+         }
     }
 
   dnslist_value = addlist;
@@ -3608,8 +3755,6 @@ int sep = 0;
 int defer_return = FAIL;
 const uschar *list = *listptr;
 uschar *domain;
-uschar *s;
-uschar buffer[1024];
 uschar revadd[128];        /* Long enough for IPv6 address */
 
 /* Indicate that the inverted IP address is not yet set up */
@@ -3622,7 +3767,7 @@ dns_init(FALSE, FALSE, FALSE);    /*XXX dnssec? */
 
 /* Loop through all the domains supplied, until something matches */
 
-while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL)
+while ((domain = string_nextinlist(&list, &sep, NULL, 0)))
   {
   int rc;
   BOOL bitmask = FALSE;
@@ -3632,7 +3777,7 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
   uschar *iplist;
   uschar *key;
 
-  HDEBUG(D_dnsbl) debug_printf("DNS list check: %s\n", domain);
+  HDEBUG(D_dnsbl) debug_printf("dnslists check: %s\n", domain);
 
   /* Deal with special values that change the behaviour on defer */
 
@@ -3686,8 +3831,7 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
   set domain_txt == domain. */
 
   domain_txt = domain;
-  comma = Ustrchr(domain, ',');
-  if (comma != NULL)
+  if ((comma = Ustrchr(domain, ',')))
     {
     *comma++ = 0;
     domain = comma;
@@ -3699,32 +3843,28 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
   actually causing an error here, because that would no doubt hold up incoming
   mail. Instead, I'll just log it. */
 
-  for (s = domain; *s != 0; s++)
-    {
+  for (uschar * s = domain; *s; s++)
     if (!isalnum(*s) && *s != '-' && *s != '.' && *s != '_')
       {
       log_write(0, LOG_MAIN, "dnslists domain \"%s\" contains "
         "strange characters - is this right?", domain);
       break;
       }
-    }
 
   /* Check the alternate domain if present */
 
-  if (domain_txt != domain) for (s = domain_txt; *s != 0; s++)
-    {
+  if (domain_txt != domain) for (uschar * s = domain_txt; *s; s++)
     if (!isalnum(*s) && *s != '-' && *s != '.' && *s != '_')
       {
       log_write(0, LOG_MAIN, "dnslists domain \"%s\" contains "
         "strange characters - is this right?", domain_txt);
       break;
       }
-    }
 
   /* If there is no key string, construct the query by adding the domain name
   onto the inverted host address, and perform a single DNS lookup. */
 
-  if (key == NULL)
+  if (!key)
     {
     if (where == ACL_WHERE_NOTSMTP_START || where == ACL_WHERE_NOTSMTP)
       {
@@ -3733,7 +3873,7 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
          acl_wherenames[where]);
       return ERROR;
       }
-    if (sender_host_address == NULL) return FAIL;    /* can never match */
+    if (!sender_host_address) return FAIL;    /* can never match */
     if (revadd[0] == 0) invert_address(revadd, sender_host_address);
     rc = one_check_dnsbl(domain, domain_txt, sender_host_address, revadd,
       iplist, bitmask, match_type, defer_return);
@@ -3755,11 +3895,9 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
     int keysep = 0;
     BOOL defer = FALSE;
     uschar *keydomain;
-    uschar keybuffer[256];
     uschar keyrevadd[128];
 
-    while ((keydomain = string_nextinlist(CUSS &key, &keysep, keybuffer,
-            sizeof(keybuffer))) != NULL)
+    while ((keydomain = string_nextinlist(CUSS &key, &keysep, NULL, 0)))
       {
       uschar *prepend = keydomain;
 
@@ -3771,7 +3909,6 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
 
       rc = one_check_dnsbl(domain, domain_txt, keydomain, prepend, iplist,
         bitmask, match_type, defer_return);
-
       if (rc == OK)
         {
         dnslist_domain = string_copy(domain_txt);
@@ -3795,6 +3932,246 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
 return FAIL;
 }
 
+
+
+/****************************************************
+  Verify a local user account for quota sufficiency
+****************************************************/
+
+/* The real work, done via a re-exec for privs, calls
+down to the transport for the quota check.
+
+Route and transport (in recipient-verify mode) the
+given recipient. 
+
+A routing result indicating any transport type other than appendfile
+results in a fail.
+
+Return, on stdout, a result string containing:
+- highlevel result code (OK, DEFER, FAIL)
+- errno
+- where string
+- message string
+*/
+
+void
+verify_quota(uschar * address)
+{
+address_item vaddr = {.address = address};
+BOOL routed;
+uschar * msg = US"\0";
+int rc, len = 1;
+
+if ((rc = verify_address(&vaddr, NULL, vopt_is_recipient | vopt_quota,
+    1, 0, 0, NULL, NULL, &routed)) != OK)
+  {
+  uschar * where = recipient_verify_failure;
+  msg = acl_verify_message ? acl_verify_message : vaddr.message;
+  if (!msg) msg = US"";
+  if (rc == DEFER && vaddr.basic_errno == ERRNO_EXIMQUOTA)
+    {
+    rc = FAIL;                                 /* DEFER -> FAIL */
+    where = US"quota";
+    vaddr.basic_errno = 0;
+    }
+  else if (!where) where = US"";
+
+  len = 5 + Ustrlen(msg) + 1 + Ustrlen(where);
+  msg = string_sprintf("%c%c%c%c%c%s%c%s", (uschar)rc,
+    (vaddr.basic_errno >> 24) && 0xff, (vaddr.basic_errno >> 16) && 0xff,
+    (vaddr.basic_errno >> 8) && 0xff, vaddr.basic_errno && 0xff,
+    where, '\0', msg);
+  }
+
+DEBUG(D_verify) debug_printf_indent("verify_quota: len %d\n", len);
+write(1, msg, len);
+return;
+}
+
+
+/******************************************************************************/
+
+/* Quota cache lookup.  We use the callout hints db also for the quota cache.
+Return TRUE if a nonexpired record was found, having filled in the yield
+argument.
+*/
+
+static BOOL
+cached_quota_lookup(const uschar * rcpt, int * yield,
+  int pos_cache, int neg_cache)
+{
+open_db dbblock, *dbm_file = NULL;
+dbdata_callout_cache_address * cache_address_record;
+
+if (!pos_cache && !neg_cache)
+  return FALSE;
+if (!(dbm_file = dbfn_open(US"callout", O_RDWR, &dbblock, FALSE, TRUE)))
+  {
+  HDEBUG(D_verify) debug_printf_indent("quota cache: not available\n");
+  return FALSE;
+  }
+if (!(cache_address_record = (dbdata_callout_cache_address *)
+    get_callout_cache_record(dbm_file, rcpt, US"address",
+      pos_cache, neg_cache)))
+  {
+  dbfn_close(dbm_file);
+  return FALSE;
+  }
+if (cache_address_record->result == ccache_accept)
+  *yield = OK;
+dbfn_close(dbm_file);
+return TRUE;
+}
+
+/* Quota cache write */
+
+static void
+cache_quota_write(const uschar * rcpt, int yield, int pos_cache, int neg_cache)
+{
+open_db dbblock, *dbm_file = NULL;
+dbdata_callout_cache_address cache_address_record;
+
+if (!pos_cache && !neg_cache)
+  return;
+if (!(dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE, TRUE)))
+  {
+  HDEBUG(D_verify) debug_printf_indent("quota cache: not available\n");
+  return;
+  }
+
+cache_address_record.result = yield == OK ? ccache_accept : ccache_reject;
+
+(void)dbfn_write(dbm_file, rcpt, &cache_address_record,
+       (int)sizeof(dbdata_callout_cache_address));
+HDEBUG(D_verify) debug_printf_indent("wrote %s quota cache record for %s\n",
+      yield == OK ? "positive" : "negative", rcpt);
+
+dbfn_close(dbm_file);
+return;
+}
+
+
+/* To evaluate a local user's quota, starting in ACL, we need to
+fork & exec to regain privileges, to that we can change to the user's
+identity for access to their files.
+
+Arguments:
+ rcpt          Recipient account
+ pos_cache     Number of seconds to cache a positive result (delivery
+               to be accepted).  Zero to disable caching.
+ neg_cache     Number of seconds to cache a negative result.  Zero to disable.
+ msg           Pointer to result string pointer
+
+Return:                OK/DEFER/FAIL code
+*/
+
+int
+verify_quota_call(const uschar * rcpt, int pos_cache, int neg_cache,
+  uschar ** msg)
+{
+int pfd[2], pid, save_errno, yield = FAIL;
+void (*oldsignal)(int);
+const uschar * where = US"socketpair";
+
+*msg = NULL;
+
+if (cached_quota_lookup(rcpt, &yield, pos_cache, neg_cache))
+  {
+  HDEBUG(D_verify) debug_printf_indent("quota cache: address record is %s\n",
+    yield == OK ? "positive" : "negative");
+  if (yield != OK)
+    {
+    recipient_verify_failure = US"quota";
+    acl_verify_message = *msg =
+      US"Previous (cached) quota verification failure";
+    }
+  return yield;
+  }
+
+if (pipe(pfd) != 0)
+  goto fail;
+
+where = US"fork";
+oldsignal = signal(SIGCHLD, SIG_DFL);
+if ((pid = exim_fork(US"quota-verify")) < 0)
+  {
+  save_errno = errno;
+  close(pfd[pipe_write]);
+  close(pfd[pipe_read]);
+  errno = save_errno;
+  goto fail;
+  }
+
+if (pid == 0)          /* child */
+  {
+  close(pfd[pipe_read]);
+  force_fd(pfd[pipe_write], 1);                /* stdout to pipe */
+  close(pfd[pipe_write]);
+  dup2(1, 0);
+  if (debug_fd > 0) force_fd(debug_fd, 2);
+
+  child_exec_exim(CEE_EXEC_EXIT, FALSE, NULL, FALSE, 3,
+    US"-MCq", string_sprintf("%d", message_size), rcpt);
+  /*NOTREACHED*/
+  }
+
+save_errno = errno;
+close(pfd[pipe_write]);
+
+if (pid < 0)
+  {
+  DEBUG(D_verify) debug_printf_indent(" fork: %s\n", strerror(save_errno));
+  }
+else
+  {
+  uschar buf[128];
+  int n = read(pfd[pipe_read], buf, sizeof(buf));
+  int status;
+
+  waitpid(pid, &status, 0);
+  if (status == 0)
+    {
+    uschar * s;
+
+    if (n > 0) yield = buf[0];
+    if (n > 4)
+      save_errno = (buf[1] << 24) | (buf[2] << 16) | (buf[3] << 8) | buf[4];
+    if ((recipient_verify_failure = n > 5
+       ? string_copyn_taint(buf+5, n-5, FALSE) : NULL))
+      {
+      int m;
+      s = buf + 5 + Ustrlen(recipient_verify_failure) + 1;
+      m = n - (s - buf);
+      acl_verify_message = *msg =
+       m > 0 ? string_copyn_taint(s, m, FALSE) : NULL;
+      }
+
+    DEBUG(D_verify) debug_printf_indent("verify call response:"
+      " len %d yield %s errno '%s' where '%s' msg '%s'\n",
+      n, rc_names[yield], strerror(save_errno), recipient_verify_failure, *msg);
+
+    if (  yield == OK
+       || save_errno == 0 && Ustrcmp(recipient_verify_failure, "quota") == 0)
+      cache_quota_write(rcpt, yield, pos_cache, neg_cache);
+    else DEBUG(D_verify)
+      debug_printf_indent("result not cacheable\n");
+    }
+  else
+    {
+    DEBUG(D_verify)
+      debug_printf_indent("verify call response: waitpid status 0x%04x\n", status);
+    }
+  }
+
+close(pfd[pipe_read]);
+errno = save_errno;
+
+fail:
+
+return yield;
+}
+
+
 /* vi: aw ai sw=2
 */
 /* End of verify.c */