Fix memory management vs. acl-as-conditional
[users/jgh/exim.git] / src / src / verify.c
index 75e3ce7c6fa71f7ed6c83704d6f400c258a03f30..a09782bcdc4f556b0a1207bb0eafd86befdfe85e 100644 (file)
@@ -1,10 +1,8 @@
-/* $Cambridge: exim/src/src/verify.c,v 1.16 2005/04/06 16:26:42 ph10 Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2005 */
+/* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Functions concerned with verifying things. The original code for callout
@@ -12,6 +10,13 @@ caching was contributed by Kevin Fleming (but I hacked it around a bit). */
 
 
 #include "exim.h"
+#include "transports/smtp.h"
+
+#define CUTTHROUGH_CMD_TIMEOUT  30     /* timeout for cutthrough-routing calls */
+#define CUTTHROUGH_DATA_TIMEOUT 60     /* timeout for cutthrough-routing calls */
+address_item cutthrough_addr;
+static smtp_outblock ctblock;
+uschar ctbuffer[8192];
 
 
 /* Structure for caching DNSBL lookups */
@@ -29,6 +34,12 @@ typedef struct dnsbl_cache_block {
 static tree_node *dnsbl_cache = NULL;
 
 
+/* Bits for match_type in one_check_dnsbl() */
+
+#define MT_NOT 1
+#define MT_ALL 2
+
+
 
 /*************************************************
 *          Retrieve a callout cache record       *
@@ -128,6 +139,7 @@ Arguments:
   options           the verification options - these bits are used:
                       vopt_is_recipient => this is a recipient address
                       vopt_callout_no_cache => don't use callout cache
+                      vopt_callout_fullpm => if postmaster check, do full one
                       vopt_callout_random => do the "random" thing
                       vopt_callout_recipsender => use real sender for recipient
                       vopt_callout_recippmaster => use postmaster for recipient
@@ -147,10 +159,12 @@ BOOL callout_no_cache = (options & vopt_callout_no_cache) != 0;
 BOOL callout_random = (options & vopt_callout_random) != 0;
 
 int yield = OK;
+int old_domain_cache_result = ccache_accept;
 BOOL done = FALSE;
 uschar *address_key;
 uschar *from_address;
 uschar *random_local_part = NULL;
+uschar *save_deliver_domain = deliver_domain;
 uschar **failure_ptr = is_recipient?
   &recipient_verify_failure : &sender_verify_failure;
 open_db dbblock;
@@ -226,10 +240,18 @@ if (dbm_file != NULL)
 
   if (cache_record != NULL)
     {
-    /* If an early command (up to and including MAIL FROM:<>) was rejected,
-    there is no point carrying on. The callout fails. */
-
-    if (cache_record->result == ccache_reject)
+    /* In most cases, if an early command (up to and including MAIL FROM:<>)
+    was rejected, there is no point carrying on. The callout fails. However, if
+    we are doing a recipient verification with use_sender or use_postmaster
+    set, a previous failure of MAIL FROM:<> doesn't count, because this time we
+    will be using a non-empty sender. We have to remember this situation so as
+    not to disturb the cached domain value if this whole verification succeeds
+    (we don't want it turning into "accept"). */
+
+    old_domain_cache_result = cache_record->result;
+
+    if (cache_record->result == ccache_reject ||
+         (*from_address == 0 && cache_record->result == ccache_reject_mfnull))
       {
       setflag(addr, af_verify_nsfail);
       HDEBUG(D_verify)
@@ -347,302 +369,626 @@ if (dbm_file != NULL)
   dbm_file = 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,
-or unless we have a previously cached negative random result. If we are to test
-with a random local part, ensure that such a local part is available. If not,
-log the fact, but carry on without randomming. */
-
-if (callout_random && callout_random_local_part != NULL)
+if (!addr->transport)
   {
-  random_local_part = expand_string(callout_random_local_part);
-  if (random_local_part == NULL)
-    log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand "
-      "callout_random_local_part: %s", expand_string_message);
+  HDEBUG(D_verify) debug_printf("cannot callout via null transport\n");
   }
+else
+  {
+  smtp_transport_options_block *ob =
+    (smtp_transport_options_block *)(addr->transport->options_block);
 
-/* Default the connect and overall callout timeouts if not set, and record the
-time we are starting so that we can enforce it. */
-
-if (callout_overall < 0) callout_overall = 4 * callout;
-if (callout_connect < 0) callout_connect = callout;
-callout_start_time = time(NULL);
-
-/* Now make connections to the hosts and do real callouts. The list of hosts
-is passed in as an argument. */
+  /* 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,
+  or unless we have a previously cached negative random result. If we are to test
+  with a random local part, ensure that such a local part is available. If not,
+  log the fact, but carry on without randomming. */
 
-for (host = host_list; host != NULL && !done; host = host->next)
-  {
-  smtp_inblock inblock;
-  smtp_outblock outblock;
-  int host_af;
-  int port = 25;
-  BOOL send_quit = TRUE;
-  uschar *helo = US"HELO";
-  uschar *interface = NULL;  /* Outgoing interface to use; NULL => any */
-  uschar inbuffer[4096];
-  uschar outbuffer[1024];
-  uschar responsebuffer[4096];
-
-  clearflag(addr, af_verify_pmfail);  /* postmaster callout flag */
-  clearflag(addr, af_verify_nsfail);  /* null sender callout flag */
-
-  /* Skip this host if we don't have an IP address for it. */
-
-  if (host->address == NULL)
+  if (callout_random && callout_random_local_part != NULL)
     {
-    DEBUG(D_verify) debug_printf("no IP address for host name %s: skipping\n",
-      host->name);
-    continue;
+    random_local_part = expand_string(callout_random_local_part);
+    if (random_local_part == NULL)
+      log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand "
+        "callout_random_local_part: %s", expand_string_message);
     }
 
-  /* Check the overall callout timeout */
+  /* Default the connect and overall callout timeouts if not set, and record the
+  time we are starting so that we can enforce it. */
+
+  if (callout_overall < 0) callout_overall = 4 * callout;
+  if (callout_connect < 0) callout_connect = callout;
+  callout_start_time = time(NULL);
 
-  if (time(NULL) - callout_start_time >= callout_overall)
+  /* Before doing a real callout, if this is an SMTP connection, flush the SMTP
+  output because a callout might take some time. When PIPELINING is active and
+  there are many recipients, the total time for doing lots of callouts can add up
+  and cause the client to time out. So in this case we forgo the PIPELINING
+  optimization. */
+
+  if (smtp_out != NULL && !disable_callout_flush) mac_smtp_fflush();
+
+  /* Now make connections to the hosts and do real callouts. The list of hosts
+  is passed in as an argument. */
+
+  for (host = host_list; host != NULL && !done; host = host->next)
     {
-    HDEBUG(D_verify) debug_printf("overall timeout for callout exceeded\n");
-    break;
-    }
+    smtp_inblock inblock;
+    smtp_outblock outblock;
+    int host_af;
+    int port = 25;
+    BOOL send_quit = TRUE;
+    uschar *active_hostname = smtp_active_hostname;
+    BOOL lmtp;
+    BOOL smtps;
+    BOOL esmtp;
+    BOOL suppress_tls = FALSE;
+    uschar *interface = NULL;  /* Outgoing interface to use; NULL => any */
+    uschar inbuffer[4096];
+    uschar outbuffer[1024];
+    uschar responsebuffer[4096];
+
+    clearflag(addr, af_verify_pmfail);  /* postmaster callout flag */
+    clearflag(addr, af_verify_nsfail);  /* null sender callout flag */
+
+    /* Skip this host if we don't have an IP address for it. */
+
+    if (host->address == NULL)
+      {
+      DEBUG(D_verify) debug_printf("no IP address for host name %s: skipping\n",
+        host->name);
+      continue;
+      }
 
-  /* Set IPv4 or IPv6 */
+    /* Check the overall callout timeout */
 
-  host_af = (Ustrchr(host->address, ':') == NULL)? AF_INET:AF_INET6;
+    if (time(NULL) - callout_start_time >= callout_overall)
+      {
+      HDEBUG(D_verify) debug_printf("overall timeout for callout exceeded\n");
+      break;
+      }
 
-  /* Expand and interpret the interface and port strings. This has to
-  be delayed till now, because they may expand differently for different
-  hosts. If there's a failure, log it, but carry on with the defaults. */
+    /* Set IPv4 or IPv6 */
 
-  deliver_host = host->name;
-  deliver_host_address = host->address;
-  if (!smtp_get_interface(tf->interface, host_af, addr, NULL, &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);
-  deliver_host = deliver_host_address = NULL;
+    host_af = (Ustrchr(host->address, ':') == NULL)? AF_INET:AF_INET6;
 
-  /* Set HELO string according to the protocol */
+    /* Expand and interpret the interface and port strings. The latter will not
+    be used if there is a host-specific port (e.g. from a manualroute router).
+    This has to be delayed till now, because they may expand differently for
+    different hosts. If there's a failure, log it, but carry on with the
+    defaults. */
 
-  if (Ustrcmp(tf->protocol, "lmtp") == 0) helo = US"LHLO";
+    deliver_host = host->name;
+    deliver_host_address = host->address;
+    deliver_domain = addr->domain;
 
-  HDEBUG(D_verify) debug_printf("interface=%s port=%d\n", interface, port);
+    if (!smtp_get_interface(tf->interface, host_af, addr, NULL, &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);
 
-  /* Set up the buffer for reading SMTP response packets. */
+    /* Set HELO string according to the protocol */
+    lmtp= Ustrcmp(tf->protocol, "lmtp") == 0;
+    smtps= Ustrcmp(tf->protocol, "smtps") == 0;
 
-  inblock.buffer = inbuffer;
-  inblock.buffersize = sizeof(inbuffer);
-  inblock.ptr = inbuffer;
-  inblock.ptrend = inbuffer;
 
-  /* Set up the buffer for holding SMTP commands while pipelining */
+    HDEBUG(D_verify) debug_printf("interface=%s port=%d\n", interface, port);
 
-  outblock.buffer = outbuffer;
-  outblock.buffersize = sizeof(outbuffer);
-  outblock.ptr = outbuffer;
-  outblock.cmd_count = 0;
-  outblock.authenticating = FALSE;
+    /* Set up the buffer for reading SMTP response packets. */
 
-  /* Connect to the host; on failure, just loop for the next one, but we
-  set the error for the last one. Use the callout_connect timeout. */
+    inblock.buffer = inbuffer;
+    inblock.buffersize = sizeof(inbuffer);
+    inblock.ptr = inbuffer;
+    inblock.ptrend = inbuffer;
 
-  inblock.sock = outblock.sock =
-    smtp_connect(host, host_af, port, interface, callout_connect, TRUE);
-  if (inblock.sock < 0)
-    {
-    addr->message = string_sprintf("could not connect to %s [%s]: %s",
-        host->name, host->address, strerror(errno));
-    continue;
-    }
+    /* Set up the buffer for holding SMTP commands while pipelining */
 
-  /* Wait for initial response, and then run the initial SMTP commands. The
-  smtp_write_command() function leaves its command in big_buffer. This is
-  used in error responses. Initialize it in case the connection is
-  rejected. */
+    outblock.buffer = outbuffer;
+    outblock.buffersize = sizeof(outbuffer);
+    outblock.ptr = outbuffer;
+    outblock.cmd_count = 0;
+    outblock.authenticating = FALSE;
 
-  Ustrcpy(big_buffer, "initial connection");
+    /* Reset the parameters of a TLS session */
+    tls_out.cipher = tls_out.peerdn = NULL;
 
-  done =
-    smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer),
-      '2', callout) &&
+    /* Connect to the host; on failure, just loop for the next one, but we
+    set the error for the last one. Use the callout_connect timeout. */
 
-    smtp_write_command(&outblock, FALSE, "%s %s\r\n", helo,
-      smtp_active_hostname) >= 0 &&
-    smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer),
-      '2', callout) &&
+    tls_retry_connection:
 
-    smtp_write_command(&outblock, FALSE, "MAIL FROM:<%s>\r\n",
-      from_address) >= 0 &&
-    smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer),
-      '2', callout);
+    inblock.sock = outblock.sock =
+      smtp_connect(host, host_af, port, interface, callout_connect, TRUE, NULL);
+    /* reconsider DSCP here */
+    if (inblock.sock < 0)
+      {
+      addr->message = string_sprintf("could not connect to %s [%s]: %s",
+          host->name, host->address, strerror(errno));
+      deliver_host = deliver_host_address = NULL;
+      deliver_domain = save_deliver_domain;
+      continue;
+      }
 
-  /* If the host gave an initial error, or does not accept HELO or MAIL
-  FROM:<>, arrange to cache this information, but don't record anything for an
-  I/O error or a defer. Do not cache rejections when a non-empty sender has
-  been used, because that blocks the whole domain for all senders. */
+    /* Expand the helo_data string to find the host name to use. */
 
-  if (!done)
-    {
-    *failure_ptr = US"mail";
-    if (errno == 0 && responsebuffer[0] == '5')
+    if (tf->helo_data != NULL)
       {
-      setflag(addr, af_verify_nsfail);
-      if (from_address[0] == 0) new_domain_record.result = ccache_reject;
+      uschar *s = expand_string(tf->helo_data);
+      if (s == NULL)
+        log_write(0, LOG_MAIN|LOG_PANIC, "<%s>: failed to expand transport's "
+          "helo_data value for callout: %s", addr->address,
+          expand_string_message);
+      else active_hostname = s;
       }
-    }
 
-  /* Otherwise, proceed to check a "random" address (if required), then the
-  given address, and the postmaster address (if required). Between each check,
-  issue RSET, because some servers accept only one recipient after MAIL
-  FROM:<>. */
+    deliver_host = deliver_host_address = NULL;
+    deliver_domain = save_deliver_domain;
 
-  else
-    {
-    new_domain_record.result = ccache_accept;
+    /* Wait for initial response, and send HELO. The smtp_write_command()
+    function leaves its command in big_buffer. This is used in error responses.
+    Initialize it in case the connection is rejected. */
 
-    /* Do the random local part check first */
+    Ustrcpy(big_buffer, "initial connection");
 
-    if (random_local_part != NULL)
-      {
-      uschar randombuffer[1024];
-      BOOL random_ok =
-        smtp_write_command(&outblock, FALSE,
-          "RCPT TO:<%.1000s@%.1000s>\r\n", random_local_part,
-          addr->domain) >= 0 &&
-        smtp_read_response(&inblock, randombuffer,
-          sizeof(randombuffer), '2', callout);
+    /* Unless ssl-on-connect, wait for the initial greeting */
+    smtps_redo_greeting:
 
-      /* Remember when we last did a random test */
+    #ifdef SUPPORT_TLS
+    if (!smtps || (smtps && tls_out.active >= 0))
+    #endif
+      if (!(done= smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer), '2', callout)))
+        goto RESPONSE_FAILED;
+    
+    /* Not worth checking greeting line for ESMTP support */
+    if (!(esmtp = verify_check_this_host(&(ob->hosts_avoid_esmtp), NULL,
+      host->name, host->address, NULL) != OK))
+      DEBUG(D_transport)
+        debug_printf("not sending EHLO (host matches hosts_avoid_esmtp)\n");
 
-      new_domain_record.random_stamp = time(NULL);
+    tls_redo_helo:
+
+    #ifdef SUPPORT_TLS
+    if (smtps  &&  tls_out.active < 0) /* ssl-on-connect, first pass */
+      {
+      tls_offered = TRUE;
+      ob->tls_tempfail_tryclear = FALSE;
+      }
+      else                             /* all other cases */
+    #endif
 
-      /* If accepted, we aren't going to do any further tests below. */
+      { esmtp_retry:
 
-      if (random_ok)
+      if (!(done= smtp_write_command(&outblock, FALSE, "%s %s\r\n",
+        !esmtp? "HELO" : lmtp? "LHLO" : "EHLO", active_hostname) >= 0))
+        goto SEND_FAILED;
+      if (!smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer), '2', callout))
         {
-        new_domain_record.random_result = ccache_accept;
+       if (errno != 0 || responsebuffer[0] == 0 || lmtp || !esmtp || tls_out.active >= 0)
+         {
+         done= FALSE;
+         goto RESPONSE_FAILED;
+         }
+        #ifdef SUPPORT_TLS
+        tls_offered = FALSE;
+        #endif
+        esmtp = FALSE;
+        goto esmtp_retry;                      /* fallback to HELO */
         }
 
-      /* Otherwise, cache a real negative response, and get back to the right
-      state to send RCPT. Unless there's some problem such as a dropped
-      connection, we expect to succeed, because the commands succeeded above. */
+      /* Set tls_offered if the response to EHLO specifies support for STARTTLS. */
+      #ifdef SUPPORT_TLS
+      if (esmtp && !suppress_tls &&  tls_out.active < 0)
+        {
+          if (regex_STARTTLS == NULL) regex_STARTTLS =
+           regex_must_compile(US"\\n250[\\s\\-]STARTTLS(\\s|\\n|$)", FALSE, TRUE);
 
-      else if (errno == 0)
+          tls_offered = pcre_exec(regex_STARTTLS, NULL, CS responsebuffer,
+                       Ustrlen(responsebuffer), 0, PCRE_EOPT, NULL, 0) >= 0;
+       }
+      else
+        tls_offered = FALSE;
+      #endif
+      }
+
+    /* If TLS is available on this connection attempt to
+    start up a TLS session, unless the host is in hosts_avoid_tls. If successful,
+    send another EHLO - the server may give a different answer in secure mode. We
+    use a separate buffer for reading the response to STARTTLS so that if it is
+    negative, the original EHLO data is available for subsequent analysis, should
+    the client not be required to use TLS. If the response is bad, copy the buffer
+    for error analysis. */
+
+    #ifdef SUPPORT_TLS
+    if (tls_offered &&
+       verify_check_this_host(&(ob->hosts_avoid_tls), NULL, host->name,
+         host->address, NULL) != OK &&
+       verify_check_this_host(&(ob->hosts_verify_avoid_tls), NULL, host->name,
+         host->address, NULL) != OK
+       )
+      {
+      uschar buffer2[4096];
+      if (  !smtps
+         && !(done= smtp_write_command(&outblock, FALSE, "STARTTLS\r\n") >= 0))
+        goto SEND_FAILED;
+
+      /* If there is an I/O error, transmission of this message is deferred. If
+      there is a temporary rejection of STARRTLS and tls_tempfail_tryclear is
+      false, we also defer. However, if there is a temporary rejection of STARTTLS
+      and tls_tempfail_tryclear is true, or if there is an outright rejection of
+      STARTTLS, we carry on. This means we will try to send the message in clear,
+      unless the host is in hosts_require_tls (tested below). */
+
+      if (!smtps && !smtp_read_response(&inblock, buffer2, sizeof(buffer2), '2',
+                       ob->command_timeout))
         {
-        if (randombuffer[0] == '5')
-          new_domain_record.random_result = ccache_reject;
+        if (errno != 0 || buffer2[0] == 0 ||
+               (buffer2[0] == '4' && !ob->tls_tempfail_tryclear))
+       {
+       Ustrncpy(responsebuffer, buffer2, sizeof(responsebuffer));
+       done= FALSE;
+       goto RESPONSE_FAILED;
+       }
+        }
 
-        done =
-          smtp_write_command(&outblock, FALSE, "RSET\r\n") >= 0 &&
-          smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer),
-            '2', callout) &&
+       /* STARTTLS accepted or ssl-on-connect: try to negotiate a TLS session. */
+      else
+        {
+        int rc = tls_client_start(inblock.sock, host, addr,
+        ob->tls_certificate, ob->tls_privatekey,
+        ob->tls_sni,
+        ob->tls_verify_certificates, ob->tls_crl,
+        ob->tls_require_ciphers,
+#ifdef EXPERIMENTAL_OCSP
+        ob->hosts_require_ocsp,
+#endif
+        ob->tls_dh_min_bits,         callout);
 
-          smtp_write_command(&outblock, FALSE, "MAIL FROM:<>\r\n") >= 0 &&
-          smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer),
-            '2', callout);
+        /* TLS negotiation failed; give an error.  Try in clear on a new connection,
+           if the options permit it for this host. */
+        if (rc != OK)
+          {
+       if (rc == DEFER && ob->tls_tempfail_tryclear && !smtps &&
+          verify_check_this_host(&(ob->hosts_require_tls), NULL, host->name,
+            host->address, NULL) != OK)
+         {
+            (void)close(inblock.sock);
+         log_write(0, LOG_MAIN, "TLS session failure: delivering unencrypted "
+           "to %s [%s] (not in hosts_require_tls)", host->name, host->address);
+         suppress_tls = TRUE;
+         goto tls_retry_connection;
+         }
+       /*save_errno = ERRNO_TLSFAILURE;*/
+       /*message = US"failure while setting up TLS session";*/
+       send_quit = FALSE;
+       done= FALSE;
+       goto TLS_FAILED;
+       }
+
+        /* TLS session is set up.  Copy info for logging. */
+        addr->cipher = tls_out.cipher;
+        addr->peerdn = tls_out.peerdn;
+
+        /* For SMTPS we need to wait for the initial OK response, then do HELO. */
+        if (smtps)
+        goto smtps_redo_greeting;
+
+        /* For STARTTLS we need to redo EHLO */
+        goto tls_redo_helo;
         }
-      else done = FALSE;    /* Some timeout/connection problem */
-      }                     /* Random check */
+      }
+
+    /* If the host is required to use a secure channel, ensure that we have one. */
+    if (tls_out.active < 0)
+      if (verify_check_this_host(&(ob->hosts_require_tls), NULL, host->name,
+       host->address, NULL) == OK)
+        {
+        /*save_errno = ERRNO_TLSREQUIRED;*/
+        log_write(0, LOG_MAIN, "a TLS session is required for %s [%s], but %s",
+          host->name, host->address,
+       tls_offered? "an attempt to start TLS failed" : "the server did not offer TLS support");
+        done= FALSE;
+        goto TLS_FAILED;
+        }
+
+    #endif /*SUPPORT_TLS*/
 
-    /* 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. */
+    done = TRUE; /* so far so good; have response to HELO */
 
-    if (new_domain_record.random_result != ccache_accept && done)
+    /*XXX the EHLO response would be analyzed here for IGNOREQUOTA, SIZE, PIPELINING, AUTH */
+    /* If we haven't authenticated, but are required to, give up. */
+
+    /*XXX "filter command specified for this transport" ??? */
+    /* for now, transport_filter by cutthrough-delivery is not supported */
+    /* Need proper integration with the proper transport mechanism. */
+
+
+    SEND_FAILED:
+    RESPONSE_FAILED:
+    TLS_FAILED:
+    ;
+    /* Clear down of the TLS, SMTP and TCP layers on error is handled below.  */
+
+
+    /* Failure to accept HELO is cached; this blocks the whole domain for all
+    senders. I/O errors and defer responses are not cached. */
+
+    if (!done)
       {
-      done =
-        smtp_write_command(&outblock, FALSE, "RCPT TO:<%.1000s>\r\n",
-          transport_rcpt_address(addr,
-            addr->transport->rcpt_include_affixes)) >= 0 &&
-        smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer),
-          '2', callout);
-
-      if (done)
-        new_address_record.result = ccache_accept;
-      else if (errno == 0 && responsebuffer[0] == '5')
+      *failure_ptr = US"mail";     /* At or before MAIL */
+      if (errno == 0 && responsebuffer[0] == '5')
         {
-        *failure_ptr = US"recipient";
-        new_address_record.result = ccache_reject;
+        setflag(addr, af_verify_nsfail);
+        new_domain_record.result = ccache_reject;
         }
+      }
 
-      /* Do postmaster check if requested */
+    /* Try to AUTH */
 
-      if (done && pm_mailfrom != NULL)
+    else done = smtp_auth(responsebuffer, sizeof(responsebuffer),
+       addr, host, ob, esmtp, &inblock, &outblock) == OK  &&
+
+               /* Copy AUTH info for logging */
+      ( (addr->authenticator = client_authenticator),
+        (addr->auth_id = client_authenticated_id),
+
+    /* Build a mail-AUTH string (re-using responsebuffer for convenience */
+        !smtp_mail_auth_str(responsebuffer, sizeof(responsebuffer), addr, ob)
+      )  &&
+
+      ( (addr->auth_sndr = client_authenticated_sender),
+
+    /* Send the MAIL command */
+        (smtp_write_command(&outblock, FALSE, "MAIL FROM:<%s>%s\r\n",
+          from_address, responsebuffer) >= 0)
+      )  &&
+
+      smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer),
+        '2', callout);
+
+    /* If the host does not accept MAIL FROM:<>, arrange to cache this
+    information, but again, don't record anything for an I/O error or a defer. Do
+    not cache rejections of MAIL when a non-empty sender has been used, because
+    that blocks the whole domain for all senders. */
+
+    if (!done)
+      {
+      *failure_ptr = US"mail";     /* At or before MAIL */
+      if (errno == 0 && responsebuffer[0] == '5')
         {
-        done =
-          smtp_write_command(&outblock, FALSE, "RSET\r\n") >= 0 &&
-          smtp_read_response(&inblock, responsebuffer,
-            sizeof(responsebuffer), '2', callout) &&
+        setflag(addr, af_verify_nsfail);
+        if (from_address[0] == 0)
+          new_domain_record.result = ccache_reject_mfnull;
+        }
+      }
 
-          smtp_write_command(&outblock, FALSE,
-            "MAIL FROM:<%s>\r\n", pm_mailfrom) >= 0 &&
-          smtp_read_response(&inblock, responsebuffer,
-            sizeof(responsebuffer), '2', callout) &&
+    /* Otherwise, proceed to check a "random" address (if required), then the
+    given address, and the postmaster address (if required). Between each check,
+    issue RSET, because some servers accept only one recipient after MAIL
+    FROM:<>.
+
+    Before doing this, set the result in the domain cache record to "accept",
+    unless its previous value was ccache_reject_mfnull. In that case, the domain
+    rejects MAIL FROM:<> and we want to continue to remember that. When that is
+    the case, we have got here only in the case of a recipient verification with
+    a non-null sender. */
 
+    else
+      {
+      new_domain_record.result =
+        (old_domain_cache_result == ccache_reject_mfnull)?
+          ccache_reject_mfnull: ccache_accept;
+
+      /* Do the random local part check first */
+
+      if (random_local_part != NULL)
+        {
+        uschar randombuffer[1024];
+        BOOL random_ok =
           smtp_write_command(&outblock, FALSE,
-            "RCPT TO:<postmaster@%.1000s>\r\n", addr->domain) >= 0 &&
-          smtp_read_response(&inblock, responsebuffer,
-            sizeof(responsebuffer), '2', callout);
+            "RCPT TO:<%.1000s@%.1000s>\r\n", random_local_part,
+            addr->domain) >= 0 &&
+          smtp_read_response(&inblock, randombuffer,
+            sizeof(randombuffer), '2', callout);
+
+        /* Remember when we last did a random test */
+
+        new_domain_record.random_stamp = time(NULL);
+
+        /* If accepted, we aren't going to do any further tests below. */
+
+        if (random_ok)
+          {
+          new_domain_record.random_result = ccache_accept;
+          }
+
+        /* Otherwise, cache a real negative response, and get back to the right
+        state to send RCPT. Unless there's some problem such as a dropped
+        connection, we expect to succeed, because the commands succeeded above. */
+
+        else if (errno == 0)
+          {
+          if (randombuffer[0] == '5')
+            new_domain_record.random_result = ccache_reject;
+
+          done =
+            smtp_write_command(&outblock, FALSE, "RSET\r\n") >= 0 &&
+            smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer),
+              '2', callout) &&
+
+            smtp_write_command(&outblock, FALSE, "MAIL FROM:<%s>\r\n",
+              from_address) >= 0 &&
+            smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer),
+              '2', callout);
+          }
+        else done = FALSE;    /* Some timeout/connection problem */
+        }                     /* Random check */
 
-        new_domain_record.postmaster_stamp = time(NULL);
+      /* 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. */
+
+      if (new_domain_record.random_result != ccache_accept && done)
+        {
+        /* Get the rcpt_include_affixes flag from the transport if there is one,
+        but assume FALSE if there is not. */
+
+        done =
+          smtp_write_command(&outblock, FALSE, "RCPT TO:<%.1000s>\r\n",
+            transport_rcpt_address(addr,
+              (addr->transport == NULL)? FALSE :
+               addr->transport->rcpt_include_affixes)) >= 0 &&
+          smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer),
+            '2', callout);
 
         if (done)
-          new_domain_record.postmaster_result = ccache_accept;
+          new_address_record.result = ccache_accept;
         else if (errno == 0 && responsebuffer[0] == '5')
           {
-          *failure_ptr = US"postmaster";
-          setflag(addr, af_verify_pmfail);
-          new_domain_record.postmaster_result = ccache_reject;
+          *failure_ptr = US"recipient";
+          new_address_record.result = ccache_reject;
           }
-        }
-      }           /* Random not accepted */
-    }             /* MAIL FROM:<> accepted */
 
-  /* For any failure of the main check, other than a negative response, we just
-  close the connection and carry on. We can identify a negative response by the
-  fact that errno is zero. For I/O errors it will be non-zero
+        /* Do postmaster check if requested; if a full check is required, we
+        check for RCPT TO:<postmaster> (no domain) in accordance with RFC 821. */
 
-  Set up different error texts for logging and for sending back to the caller
-  as an SMTP response. Log in all cases, using a one-line format. For sender
-  callouts, give a full response to the caller, but for recipient callouts,
-  don't give the IP address because this may be an internal host whose identity
-  is not to be widely broadcast. */
+        if (done && pm_mailfrom != NULL)
+          {
+          /*XXX not suitable for cutthrough - sequencing problems */
+       cutthrough_delivery= FALSE;
+       HDEBUG(D_acl|D_v) debug_printf("Cutthrough cancelled by presence of postmaster verify\n");
 
-  if (!done)
-    {
-    if (errno == ETIMEDOUT)
-      {
-      HDEBUG(D_verify) debug_printf("SMTP timeout\n");
-      send_quit = FALSE;
-      }
-    else if (errno == 0)
-      {
-      if (*responsebuffer == 0) Ustrcpy(responsebuffer, US"connection dropped");
+          done =
+            smtp_write_command(&outblock, FALSE, "RSET\r\n") >= 0 &&
+            smtp_read_response(&inblock, responsebuffer,
+              sizeof(responsebuffer), '2', callout) &&
 
-      addr->message =
-        string_sprintf("response to \"%s\" from %s [%s] was: %s",
-          big_buffer, host->name, host->address,
-          string_printing(responsebuffer));
+            smtp_write_command(&outblock, FALSE,
+              "MAIL FROM:<%s>\r\n", pm_mailfrom) >= 0 &&
+            smtp_read_response(&inblock, responsebuffer,
+              sizeof(responsebuffer), '2', callout) &&
 
-      addr->user_message = is_recipient?
-        string_sprintf("Callout verification failed:\n%s", responsebuffer)
-        :
-        string_sprintf("Called:   %s\nSent:     %s\nResponse: %s",
-          host->address, big_buffer, responsebuffer);
+            /* First try using the current domain */
+
+            ((
+            smtp_write_command(&outblock, FALSE,
+              "RCPT TO:<postmaster@%.1000s>\r\n", addr->domain) >= 0 &&
+            smtp_read_response(&inblock, responsebuffer,
+              sizeof(responsebuffer), '2', callout)
+            )
+
+            ||
+
+            /* If that doesn't work, and a full check is requested,
+            try without the domain. */
+
+            (
+            (options & vopt_callout_fullpm) != 0 &&
+            smtp_write_command(&outblock, FALSE,
+              "RCPT TO:<postmaster>\r\n") >= 0 &&
+            smtp_read_response(&inblock, responsebuffer,
+              sizeof(responsebuffer), '2', callout)
+            ));
+
+          /* Sort out the cache record */
 
-      /* Hard rejection ends the process */
+          new_domain_record.postmaster_stamp = time(NULL);
 
-      if (responsebuffer[0] == '5')   /* Address rejected */
+          if (done)
+            new_domain_record.postmaster_result = ccache_accept;
+          else if (errno == 0 && responsebuffer[0] == '5')
+            {
+            *failure_ptr = US"postmaster";
+            setflag(addr, af_verify_pmfail);
+            new_domain_record.postmaster_result = ccache_reject;
+            }
+          }
+        }           /* Random not accepted */
+      }             /* MAIL FROM: accepted */
+
+    /* For any failure of the main check, other than a negative response, we just
+    close the connection and carry on. We can identify a negative response by the
+    fact that errno is zero. For I/O errors it will be non-zero
+
+    Set up different error texts for logging and for sending back to the caller
+    as an SMTP response. Log in all cases, using a one-line format. For sender
+    callouts, give a full response to the caller, but for recipient callouts,
+    don't give the IP address because this may be an internal host whose identity
+    is not to be widely broadcast. */
+
+    if (!done)
+      {
+      if (errno == ETIMEDOUT)
         {
-        yield = FAIL;
-        done = TRUE;
+        HDEBUG(D_verify) debug_printf("SMTP timeout\n");
+        send_quit = FALSE;
+        }
+      else if (errno == 0)
+        {
+        if (*responsebuffer == 0) Ustrcpy(responsebuffer, US"connection dropped");
+
+        addr->message =
+          string_sprintf("response to \"%s\" from %s [%s] was: %s",
+            big_buffer, host->name, host->address,
+            string_printing(responsebuffer));
+
+        addr->user_message = is_recipient?
+          string_sprintf("Callout verification failed:\n%s", responsebuffer)
+          :
+          string_sprintf("Called:   %s\nSent:     %s\nResponse: %s",
+            host->address, big_buffer, responsebuffer);
+
+        /* Hard rejection ends the process */
+
+        if (responsebuffer[0] == '5')   /* Address rejected */
+          {
+          yield = FAIL;
+          done = TRUE;
+          }
         }
       }
-    }
 
-  /* End the SMTP conversation and close the connection. */
+    /* End the SMTP conversation and close the connection. */
+
+    /* Cutthrough - on a successfull connect and recipient-verify with use-sender
+    and we have no cutthrough conn so far
+    here is where we want to leave the conn open */
+    if (  cutthrough_delivery
+       && done
+       && yield == OK
+       && (options & (vopt_callout_recipsender|vopt_callout_recippmaster)) == vopt_callout_recipsender
+       && !random_local_part
+       && !pm_mailfrom
+       && cutthrough_fd < 0
+       )
+      {
+      cutthrough_fd= outblock.sock;    /* We assume no buffer in use in the outblock */
+      cutthrough_addr = *addr;         /* Save the address_item for later logging */
+      cutthrough_addr.host_used = store_get(sizeof(host_item));
+      cutthrough_addr.host_used->name =    host->name;
+      cutthrough_addr.host_used->address = host->address;
+      cutthrough_addr.host_used->port =    port;
+      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;
+      }
+    else
+      {
+      /* Ensure no cutthrough on multiple address verifies */
+      if (options & vopt_callout_recipsender)
+        cancel_cutthrough_connection("multiple verify calls");
+      if (send_quit) (void)smtp_write_command(&outblock, FALSE, "QUIT\r\n");
+
+      #ifdef SUPPORT_TLS
+      tls_close(FALSE, TRUE);
+      #endif
+      (void)close(inblock.sock);
+      }
 
-  if (send_quit) (void)smtp_write_command(&outblock, FALSE, "QUIT\r\n");
-  close(inblock.sock);
-  }    /* Loop through all hosts, while !done */
+    }    /* Loop through all hosts, while !done */
+  }
 
 /* If we get here with done == TRUE, a successful callout happened, and yield
 will be set OK or FAIL according to the response to the RCPT command.
@@ -650,9 +996,9 @@ Otherwise, we looped through the hosts but couldn't complete the business.
 However, there may be domain-specific information to cache in both cases.
 
 The value of the result field in the new_domain record is ccache_unknown if
-there was an error before or with MAIL FROM:<>, and errno was not zero,
+there was an error before or with MAIL FROM:, and errno was not zero,
 implying some kind of I/O error. We don't want to write the cache in that case.
-Otherwise the value is ccache_accept or ccache_reject. */
+Otherwise the value is ccache_accept, ccache_reject, or ccache_reject_mfnull. */
 
 if (!callout_no_cache && new_domain_record.result != ccache_unknown)
   {
@@ -734,6 +1080,252 @@ return yield;
 
 
 
+/* Called after recipient-acl to get a cutthrough connection open when
+   one was requested and a recipient-verify wasn't subsequently done.
+*/
+void
+open_cutthrough_connection( address_item * addr )
+{
+address_item addr2;
+
+/* Use a recipient-verify-callout to set up the cutthrough connection. */
+/* We must use a copy of the address for verification, because it might
+get rewritten. */
+
+addr2 = *addr;
+HDEBUG(D_acl) debug_printf("----------- start cutthrough setup ------------\n");
+(void) verify_address(&addr2, NULL,
+       vopt_is_recipient | vopt_callout_recipsender | vopt_callout_no_cache,
+       CUTTHROUGH_CMD_TIMEOUT, -1, -1,
+       NULL, NULL, NULL);
+HDEBUG(D_acl) debug_printf("----------- end cutthrough setup ------------\n");
+return;
+}
+
+
+
+/* Send given number of bytes from the buffer */
+static BOOL
+cutthrough_send(int n)
+{
+if(cutthrough_fd < 0)
+  return TRUE;
+
+if(
+#ifdef SUPPORT_TLS
+   (tls_out.active == cutthrough_fd) ? tls_write(FALSE, ctblock.buffer, n) :
+#endif
+   send(cutthrough_fd, ctblock.buffer, n, 0) > 0
+  )
+{
+  transport_count += n;
+  ctblock.ptr= ctblock.buffer;
+  return TRUE;
+}
+
+HDEBUG(D_transport|D_acl) debug_printf("cutthrough_send failed: %s\n", strerror(errno));
+return FALSE;
+}
+
+
+
+static BOOL
+_cutthrough_puts(uschar * cp, int n)
+{
+while(n--)
+ {
+ if(ctblock.ptr >= ctblock.buffer+ctblock.buffersize)
+   if(!cutthrough_send(ctblock.buffersize))
+     return FALSE;
+
+ *ctblock.ptr++ = *cp++;
+ }
+return TRUE;
+}
+
+/* Buffered output of counted data block.   Return boolean success */
+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");
+return FALSE;
+}
+
+
+static BOOL
+_cutthrough_flush_send( void )
+{
+int n= ctblock.ptr-ctblock.buffer;
+
+if(n>0)
+  if(!cutthrough_send(n))
+    return FALSE;
+return TRUE;
+}
+
+
+/* Send out any bufferred output.  Return boolean success. */
+BOOL
+cutthrough_flush_send( void )
+{
+if (_cutthrough_flush_send()) return TRUE;
+cancel_cutthrough_connection("transmit failed");
+return FALSE;
+}
+
+
+BOOL
+cutthrough_put_nl( void )
+{
+return cutthrough_puts(US"\r\n", 2);
+}
+
+
+/* Get and check response from cutthrough target */
+static uschar
+cutthrough_response(char expect, uschar ** copy)
+{
+smtp_inblock inblock;
+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, CUTTHROUGH_DATA_TIMEOUT))
+  cancel_cutthrough_connection("target timeout on read");
+
+if(copy != NULL)
+  {
+  uschar * cp;
+  *copy= cp= string_copy(responsebuffer);
+  /* Trim the trailing end of line */
+  cp += Ustrlen(responsebuffer);
+  if(cp > *copy  &&  cp[-1] == '\n') *--cp = '\0';
+  if(cp > *copy  &&  cp[-1] == '\r') *--cp = '\0';
+  }
+
+return responsebuffer[0];
+}
+
+
+/* Negotiate dataphase with the cutthrough target, returning success boolean */
+BOOL
+cutthrough_predata( void )
+{
+if(cutthrough_fd < 0)
+  return FALSE;
+
+HDEBUG(D_transport|D_acl|D_v) debug_printf("  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) == '3';
+}
+
+
+/* Buffered send of headers.  Return success boolean. */
+/* Expands newlines to wire format (CR,NL).           */
+/* Also sends header-terminating blank line.          */
+BOOL
+cutthrough_headers_send( void )
+{
+header_line * h;
+uschar * cp1, * cp2;
+
+if(cutthrough_fd < 0)
+  return FALSE;
+
+for(h= header_list; h != NULL; h= h->next)
+  if(h->type != htype_old  &&  h->text != NULL)
+    for (cp1 = h->text; *cp1 && (cp2 = Ustrchr(cp1, '\n')); cp1 = cp2+1)
+      if(  !cutthrough_puts(cp1, cp2-cp1)
+        || !cutthrough_put_nl())
+        return FALSE;
+
+HDEBUG(D_transport|D_acl|D_v) debug_printf("  SMTP>>(nl)\n");
+return cutthrough_put_nl();
+}
+
+
+static void
+close_cutthrough_connection( const char * why )
+{
+if(cutthrough_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");
+  _cutthrough_puts(US"QUIT\r\n", 6);   /* avoid recursion */
+  _cutthrough_flush_send();
+  /* No wait for response */
+
+  #ifdef SUPPORT_TLS
+  tls_close(FALSE, TRUE);
+  #endif
+  (void)close(cutthrough_fd);
+  cutthrough_fd= -1;
+  HDEBUG(D_acl) debug_printf("----------- cutthrough shutdown (%s) ------------\n", why);
+  }
+ctblock.ptr = ctbuffer;
+}
+
+void
+cancel_cutthrough_connection( const char * why )
+{
+close_cutthrough_connection(why);
+cutthrough_delivery= FALSE;
+}
+
+
+
+
+/* Have senders final-dot.  Send one to cutthrough target, and grab the response.
+   Log an OK response as a transmission.
+   Close the connection.
+   Return smtp response-class digit.
+*/
+uschar *
+cutthrough_finaldot( void )
+{
+HDEBUG(D_transport|D_acl|D_v) debug_printf("  SMTP>> .\n");
+
+/* Assume data finshed with new-line */
+if(!cutthrough_puts(US".", 1) || !cutthrough_put_nl() || !cutthrough_flush_send())
+  return cutthrough_addr.message;
+
+switch(cutthrough_response('2', &cutthrough_addr.message))
+  {
+  case '2':
+    delivery_log(LOG_MAIN, &cutthrough_addr, (int)'>', NULL);
+    close_cutthrough_connection("delivered");
+    break;
+
+  case '4':
+    delivery_log(LOG_MAIN, &cutthrough_addr, 0, US"tmp-reject from cutthrough after DATA:");
+    break;
+
+  case '5':
+    delivery_log(LOG_MAIN|LOG_REJECT, &cutthrough_addr, 0, US"rejected after DATA:");
+    break;
+
+  default:
+    break;
+  }
+  return cutthrough_addr.message;
+}
+
+
+
 /*************************************************
 *           Copy error to toplevel address       *
 *************************************************/
@@ -760,6 +1352,8 @@ if (addr != vaddr)
   vaddr->user_message = addr->user_message;
   vaddr->basic_errno = addr->basic_errno;
   vaddr->more_errno = addr->more_errno;
+  vaddr->p.address_data = addr->p.address_data;
+  copyflag(vaddr, addr, af_pass_message);
   }
 return yield;
 }
@@ -767,6 +1361,42 @@ return yield;
 
 
 
+/**************************************************
+* printf that automatically handles TLS if needed *
+***************************************************/
+
+/* This function is used by verify_address() as a substitute for all fprintf()
+calls; a direct fprintf() will not produce output in a TLS SMTP session, such
+as a response to an EXPN command.  smtp_in.c makes smtp_printf available but
+that assumes that we always use the smtp_out FILE* when not using TLS or the
+ssl buffer when we are.  Instead we take a FILE* parameter and check to see if
+that is smtp_out; if so, smtp_printf() with TLS support, otherwise regular
+fprintf().
+
+Arguments:
+  f           the candidate FILE* to write to
+  format      format string
+  ...         optional arguments
+
+Returns:
+              nothing
+*/
+
+static void PRINTF_FUNCTION(2,3)
+respond_printf(FILE *f, const char *format, ...)
+{
+va_list ap;
+
+va_start(ap, format);
+if (smtp_out && (f == smtp_out))
+  smtp_vprintf(format, ap);
+else
+  vfprintf(f, format, ap);
+va_end(ap);
+}
+
+
+
 /*************************************************
 *            Verify an email address             *
 *************************************************/
@@ -787,10 +1417,13 @@ Arguments:
                        rewriting and messages from callouts
                      vopt_qualify => qualify an unqualified address; else error
                      vopt_expn => called from SMTP EXPN command
+                     vopt_success_on_redirect => when a new address is generated
+                       the verification instantly succeeds
 
                      These ones are used by do_callout() -- the options variable
                        is passed to it.
 
+                     vopt_callout_fullpm => if postmaster check, do full one
                      vopt_callout_no_cache => don't use callout cache
                      vopt_callout_random => do the "random" thing
                      vopt_callout_recipsender => use real sender for recipient
@@ -823,6 +1456,7 @@ BOOL allok = TRUE;
 BOOL full_info = (f == NULL)? FALSE : (debug_selector != 0);
 BOOL is_recipient = (options & vopt_is_recipient) != 0;
 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 :
@@ -862,8 +1496,8 @@ if (parse_find_at(address) == NULL)
   if ((options & vopt_qualify) == 0)
     {
     if (f != NULL)
-      fprintf(f, "%sA domain is required for \"%s\"%s\n", ko_prefix, address,
-        cr);
+      respond_printf(f, "%sA domain is required for \"%s\"%s\n",
+        ko_prefix, address, cr);
     *failure_ptr = US"qualify";
     return FAIL;
     }
@@ -904,6 +1538,18 @@ addresses, such rewriting fails. */
 
 if (address[0] == 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
+at exit from this routine. */
+
+modify_variable(US"tls_bits",                 &tls_out.bits);
+modify_variable(US"tls_certificate_verified", &tls_out.certificate_verified);
+modify_variable(US"tls_cipher",               &tls_out.cipher);
+modify_variable(US"tls_peerdn",               &tls_out.peerdn);
+#if defined(SUPPORT_TLS) && !defined(USE_GNUTLS)
+modify_variable(US"tls_sni",                  &tls_out.sni);
+#endif
+
 /* Save a copy of the sender address for re-instating if we change it to <>
 while verifying a sender address (a nice bit of self-reference there). */
 
@@ -1003,10 +1649,21 @@ while (addr_new != NULL)
       {
       host_item *host_list = addr->host_list;
 
-      /* Default, if no remote transport, to NULL for the interface (=> any),
-      "smtp" for the port, and "smtp" for the protocol. */
-
-      transport_feedback tf = { NULL, US"smtp", US"smtp", NULL, FALSE, FALSE };
+      /* Make up some data for use in the case where there is no remote
+      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 */
+        };
 
       /* 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
@@ -1014,7 +1671,7 @@ while (addr_new != NULL)
 
       if (addr->transport != NULL && !addr->transport->info->local)
         {
-        (void)(addr->transport->setup)(addr->transport, addr, &tf, NULL);
+        (void)(addr->transport->setup)(addr->transport, 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
@@ -1023,13 +1680,16 @@ while (addr_new != NULL)
         if (tf.hosts != NULL && (host_list == NULL || tf.hosts_override))
           {
           uschar *s;
+          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 = deliver_localpart = NULL;
+          deliver_domain = save_deliver_domain;
+          deliver_localpart = save_deliver_localpart;
 
           if (s == NULL)
             {
@@ -1039,6 +1699,7 @@ while (addr_new != NULL)
             }
           else
             {
+            int flags;
             uschar *canonical_name;
             host_item *host, *nexthost;
             host_build_hostlist(&host_list, s, tf.hosts_randomize);
@@ -1049,20 +1710,19 @@ while (addr_new != NULL)
             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 != NULL; host = nexthost)
               {
               nexthost = host->next;
               if (tf.gethostbyname ||
-                  string_is_ip_address(host->name, NULL) > 0)
-                (void)host_find_byname(host, NULL, &canonical_name, TRUE);
+                  string_is_ip_address(host->name, NULL) != 0)
+                (void)host_find_byname(host, NULL, flags, &canonical_name, TRUE);
               else
-                {
-                int flags = HOST_FIND_BY_A;
-                if (tf.qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE;
-                if (tf.search_parents) flags |= HOST_FIND_SEARCH_PARENTS;
                 (void)host_find_bydns(host, NULL, flags, NULL, NULL, NULL,
                   &canonical_name, NULL);
-                }
               }
             }
           }
@@ -1082,6 +1742,9 @@ while (addr_new != NULL)
           }
         else
           {
+#ifdef SUPPORT_TLS
+         deliver_set_expansions(addr);
+#endif
           rc = do_callout(addr, host_list, &tf, callout, callout_overall,
             callout_connect, options, se_mailfrom, pm_mailfrom);
           }
@@ -1111,20 +1774,36 @@ while (addr_new != NULL)
     allok = FALSE;
     if (f != NULL)
       {
-      fprintf(f, "%s%s %s", ko_prefix, address,
+      address_item *p = addr->parent;
+
+      respond_printf(f, "%s%s %s", ko_prefix,
+        full_info? addr->address : address,
         address_test_mode? "is undeliverable" : "failed to verify");
       if (!expn && admin_user)
         {
         if (addr->basic_errno > 0)
-          fprintf(f, ": %s", strerror(addr->basic_errno));
+          respond_printf(f, ": %s", strerror(addr->basic_errno));
         if (addr->message != NULL)
-          fprintf(f, ":\n  %s", addr->message);
+          respond_printf(f, ": %s", addr->message);
+        }
+
+      /* Show parents iff doing full info */
+
+      if (full_info) while (p != NULL)
+        {
+        respond_printf(f, "%s\n    <-- %s", cr, p->address);
+        p = p->parent;
         }
-      fprintf(f, "%s\n", cr);
+      respond_printf(f, "%s\n", cr);
       }
+    cancel_cutthrough_connection("routing hard fail");
 
-    if (!full_info) return copy_error(vaddr, addr, FAIL);
-      else yield = FAIL;
+    if (!full_info)
+    {
+      yield = copy_error(vaddr, addr, FAIL);
+      goto out;
+    }
+    else yield = FAIL;
     }
 
   /* Soft failure */
@@ -1134,25 +1813,40 @@ while (addr_new != NULL)
     allok = FALSE;
     if (f != NULL)
       {
-      fprintf(f, "%s%s cannot be resolved at this time", ko_prefix, address);
+      address_item *p = addr->parent;
+      respond_printf(f, "%s%s cannot be resolved at this time", ko_prefix,
+        full_info? addr->address : address);
       if (!expn && admin_user)
         {
         if (addr->basic_errno > 0)
-          fprintf(f, ":\n  %s", strerror(addr->basic_errno));
+          respond_printf(f, ": %s", strerror(addr->basic_errno));
         if (addr->message != NULL)
-          fprintf(f, ":\n  %s", addr->message);
+          respond_printf(f, ": %s", addr->message);
         else if (addr->basic_errno <= 0)
-          fprintf(f, ":\n  unknown error");
+          respond_printf(f, ": unknown error");
         }
 
-      fprintf(f, "%s\n", cr);
+      /* Show parents iff doing full info */
+
+      if (full_info) while (p != NULL)
+        {
+        respond_printf(f, "%s\n    <-- %s", cr, p->address);
+        p = p->parent;
+        }
+      respond_printf(f, "%s\n", cr);
+      }
+    cancel_cutthrough_connection("routing soft fail");
+
+    if (!full_info)
+      {
+      yield = copy_error(vaddr, addr, DEFER);
+      goto out;
       }
-    if (!full_info) return copy_error(vaddr, addr, DEFER);
-      else if (yield == OK) yield = DEFER;
+    else if (yield == OK) yield = DEFER;
     }
 
   /* If we are handling EXPN, we do not want to continue to route beyond
-  the top level. */
+  the top level (whose address is in "address"). */
 
   else if (expn)
     {
@@ -1160,18 +1854,19 @@ while (addr_new != NULL)
     if (addr_new == NULL)
       {
       if (addr_local == NULL && addr_remote == NULL)
-        fprintf(f, "250 mail to <%s> is discarded\r\n", address);
+        respond_printf(f, "250 mail to <%s> is discarded\r\n", address);
       else
-        fprintf(f, "250 <%s>\r\n", address);
+        respond_printf(f, "250 <%s>\r\n", address);
       }
     else while (addr_new != NULL)
       {
       address_item *addr2 = addr_new;
       addr_new = addr2->next;
       if (addr_new == NULL) ok_prefix = US"250 ";
-      fprintf(f, "%s<%s>\r\n", ok_prefix, addr2->address);
+      respond_printf(f, "%s<%s>\r\n", ok_prefix, addr2->address);
       }
-    return OK;
+    yield = OK;
+    goto out;
     }
 
   /* Successful routing other than EXPN. */
@@ -1192,9 +1887,12 @@ while (addr_new != NULL)
     generated address. */
 
     if (!full_info &&                    /* Stop if short info wanted AND */
-         (addr_new == NULL ||            /* No new address OR */
-          addr_new->next != NULL ||      /* More than one new address OR */
-          testflag(addr_new, af_pfr)))   /* New address is pfr */
+         (((addr_new == NULL ||          /* No new address OR */
+           addr_new->next != NULL ||     /* More than one new address OR */
+           testflag(addr_new, af_pfr)))  /* New address is pfr */
+         ||                              /* OR */
+         (addr_new != NULL &&            /* At least one new address AND */
+          success_on_redirect)))         /* success_on_redirect is set */
       {
       if (f != NULL) fprintf(f, "%s %s\n", address,
         address_test_mode? "is deliverable" : "verified");
@@ -1203,7 +1901,8 @@ while (addr_new != NULL)
       of $address_data to be that of the child */
 
       vaddr->p.address_data = addr->p.address_data;
-      return OK;
+      yield = OK;
+      goto out;
       }
     }
   }     /* Loop for generated addresses */
@@ -1218,9 +1917,12 @@ or autoreplies, and there were no errors or deferments, the message is to be
 discarded, usually because of the use of :blackhole: in an alias file. */
 
 if (allok && addr_local == NULL && addr_remote == NULL)
+  {
   fprintf(f, "mail to %s is discarded\n", address);
+  goto out;
+  }
 
-else for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++)
+for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++)
   {
   while (addr_list != NULL)
     {
@@ -1229,6 +1931,23 @@ else for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++)
     addr_list = addr->next;
 
     fprintf(f, "%s", CS addr->address);
+#ifdef EXPERIMENTAL_SRS
+    if(addr->p.srs_sender)
+      fprintf(f, "    [srs = %s]", addr->p.srs_sender);
+#endif
+
+    /* If the address is a duplicate, show something about it. */
+
+    if (!testflag(addr, af_pfr))
+      {
+      tree_node *tnode;
+      if ((tnode = tree_search(tree_duplicates, addr->unique)) != NULL)
+        fprintf(f, "   [duplicate, would not be delivered]");
+      else tree_add_duplicate(addr->unique, addr);
+      }
+
+    /* Now show its parents */
+
     while (p != NULL)
       {
       fprintf(f, "\n    <-- %s", p->address);
@@ -1284,9 +2003,19 @@ else for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++)
     }
   }
 
-/* Will be DEFER or FAIL if any one address has, only for full_info (which is
+/* Yield will be DEFER or FAIL if any one address has, only for full_info (which is
 the -bv or -bt case). */
 
+out:
+
+modify_variable(US"tls_bits",                 &tls_in.bits);
+modify_variable(US"tls_certificate_verified", &tls_in.certificate_verified);
+modify_variable(US"tls_cipher",               &tls_in.cipher);
+modify_variable(US"tls_peerdn",               &tls_in.peerdn);
+#if defined(SUPPORT_TLS) && !defined(USE_GNUTLS)
+modify_variable(US"tls_sni",                  &tls_in.sni);
+#endif
+
 return yield;
 }
 
@@ -1312,8 +2041,9 @@ verify_check_headers(uschar **msgptr)
 {
 header_line *h;
 uschar *colon, *s;
+int yield = OK;
 
-for (h = header_list; h != NULL; h = h->next)
+for (h = header_list; h != NULL && yield == OK; h = h->next)
   {
   if (h->type != htype_from &&
       h->type != htype_reply_to &&
@@ -1327,9 +2057,10 @@ for (h = header_list; h != NULL; h = h->next)
   s = colon + 1;
   while (isspace(*s)) s++;
 
-  parse_allow_group = TRUE;     /* Allow group syntax */
+  /* Loop for multiple addresses in the header, enabling group syntax. Note
+  that we have to reset this after the header has been scanned. */
 
-  /* Loop for multiple addresses in the header */
+  parse_allow_group = TRUE;
 
   while (*s != 0)
     {
@@ -1339,7 +2070,7 @@ for (h = header_list; h != NULL; h = h->next)
     int start, end, domain;
 
     /* Temporarily terminate the string at this point, and extract the
-    operative address within. */
+    operative address within, allowing group syntax. */
 
     *ss = 0;
     recipient = parse_extract_address(s,&errmess,&start,&end,&domain,FALSE);
@@ -1368,14 +2099,16 @@ for (h = header_list; h != NULL; h = h->next)
       {
       uschar *verb = US"is";
       uschar *t = ss;
+      uschar *tt = colon;
       int len;
 
       /* Arrange not to include any white space at the end in the
-      error message. */
+      error message or the header name. */
 
       while (t > s && isspace(t[-1])) t--;
+      while (tt > h->text && isspace(tt[-1])) tt--;
 
-      /* Add the address which failed to the error message, since in a
+      /* Add the address that failed to the error message, since in a
       header with very many addresses it is sometimes hard to spot
       which one is at fault. However, limit the amount of address to
       quote - cases have been seen where, for example, a missing double
@@ -1390,10 +2123,11 @@ for (h = header_list; h != NULL; h = h->next)
         }
 
       *msgptr = string_printing(
-        string_sprintf("%s: failing address in \"%.*s\" header %s: %.*s",
-          errmess, colon - h->text, h->text, verb, len, s));
+        string_sprintf("%s: failing address in \"%.*s:\" header %s: %.*s",
+          errmess, tt - h->text, h->text, verb, len, s));
 
-      return FAIL;
+      yield = FAIL;
+      break;          /* Out of address loop */
       }
 
     /* Advance to the next address */
@@ -1401,13 +2135,103 @@ for (h = header_list; h != NULL; h = h->next)
     s = ss + (terminator? 1:0);
     while (isspace(*s)) s++;
     }   /* Next address */
-  }     /* Next header */
 
-return OK;
+  parse_allow_group = FALSE;
+  parse_found_group = FALSE;
+  }     /* Next header unless yield has been set FALSE */
+
+return yield;
 }
 
 
 
+/*************************************************
+*          Check for blind recipients            *
+*************************************************/
+
+/* This function checks that every (envelope) recipient is mentioned in either
+the To: or Cc: header lines, thus detecting blind carbon copies.
+
+There are two ways of scanning that could be used: either scan the header lines
+and tick off the recipients, or scan the recipients and check the header lines.
+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
+Returns:     OK    if there are no blind recipients
+             FAIL  if there is at least one blind recipient
+*/
+
+int
+verify_check_notblind(void)
+{
+int i;
+for (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)
+    {
+    uschar *colon, *s;
+
+    if (h->type != htype_to && h->type != htype_cc) continue;
+
+    colon = Ustrchr(h->text, ':');
+    s = colon + 1;
+    while (isspace(*s)) 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;
+
+    while (*s != 0)
+      {
+      uschar *ss = parse_find_address_end(s, FALSE);
+      uschar *recipient,*errmess;
+      int terminator = *ss;
+      int start, end, domain;
+
+      /* Temporarily terminate the string at this point, and extract the
+      operative address within, allowing group syntax. */
+
+      *ss = 0;
+      recipient = parse_extract_address(s,&errmess,&start,&end,&domain,FALSE);
+      *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;
+        }
+
+      /* Advance to the next address */
+
+      s = ss + (terminator? 1:0);
+      while (isspace(*s)) s++;
+      }   /* Next address */
+
+    parse_allow_group = FALSE;
+    parse_found_group = FALSE;
+    }     /* Next header (if found is false) */
+
+  if (!found) return FAIL;
+  }       /* Next recipient */
+
+return OK;
+}
+
+
 
 /*************************************************
 *          Find if verified sender               *
@@ -1481,13 +2305,14 @@ verify_check_header_address(uschar **user_msgptr, uschar **log_msgptr,
   uschar *pm_mailfrom, int options, int *verrno)
 {
 static int header_types[] = { htype_sender, htype_reply_to, htype_from };
+BOOL done = FALSE;
 int yield = FAIL;
 int i;
 
-for (i = 0; i < 3; i++)
+for (i = 0; i < 3 && !done; i++)
   {
   header_line *h;
-  for (h = header_list; h != NULL; h = h->next)
+  for (h = header_list; h != NULL && !done; h = h->next)
     {
     int terminator, new_ok;
     uschar *s, *ss, *endname;
@@ -1495,6 +2320,11 @@ for (i = 0; i < 3; i++)
     if (h->type != header_types[i]) continue;
     s = endname = Ustrchr(h->text, ':') + 1;
 
+    /* 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;
+
     while (*s != 0)
       {
       address_item *vaddr;
@@ -1537,11 +2367,21 @@ for (i = 0; i < 3; i++)
       else
         {
         int start, end, domain;
-        uschar *address = parse_extract_address(s, log_msgptr, &start,
-          &end, &domain, FALSE);
+        uschar *address = parse_extract_address(s, log_msgptr, &start, &end,
+          &domain, FALSE);
 
         *ss = terminator;
 
+        /* 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)
+          {
+          *log_msgptr = NULL;
+          s = ss;
+          continue;
+          }
+
         /* If verification failed because of a syntax error, fail this
         function, and ensure that the failing address gets added to the error
         message. */
@@ -1549,14 +2389,13 @@ for (i = 0; i < 3; i++)
         if (address == NULL)
           {
           new_ok = FAIL;
-          if (*log_msgptr != NULL)
-            {
-            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);
-            return 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);
+          yield = FAIL;
+          done = TRUE;
+          break;
           }
 
         /* Else go ahead with the sender verification. But it isn't *the*
@@ -1590,15 +2429,24 @@ for (i = 0; i < 3; i++)
 
       /* Success or defer */
 
-      if (new_ok == OK) return OK;
+      if (new_ok == OK)
+        {
+        yield = OK;
+        done = TRUE;
+        break;
+        }
+
       if (new_ok == DEFER) yield = DEFER;
 
       /* Move on to any more addresses in the header */
 
       s = ss;
-      }
-    }
-  }
+      }     /* Next address */
+
+    parse_allow_group = FALSE;
+    parse_found_group = FALSE;
+    }       /* Next header, 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";
@@ -1766,7 +2614,7 @@ sender_ident = string_printing(string_copyn(p, 127));
 DEBUG(D_ident) debug_printf("sender_ident = %s\n", sender_ident);
 
 END_OFF:
-close(sock);
+(void)close(sock);
 return;
 }
 
@@ -1789,25 +2637,34 @@ Arguments:
   error          for error message when returning ERROR
 
 The block contains:
-  host_name      the host name or NULL, implying use sender_host_name and
-                   sender_host_aliases, looking them up if required
+  host_name      (a) the host name, or
+                 (b) NULL, implying use sender_host_name and
+                       sender_host_aliases, looking them up if required, or
+                 (c) the empty string, meaning that only IP address matches
+                       are permitted
   host_address   the host address
   host_ipv4      the IPv4 address taken from an IPv6 one
 
 Returns:         OK      matched
                  FAIL    did not match
                  DEFER   lookup deferred
-                 ERROR   failed to find the host name or IP address
-                         unknown lookup type specified
+                 ERROR   (a) failed to find the host name or IP address, or
+                         (b) unknown lookup type specified, or
+                         (c) host name encountered when only IP addresses are
+                               being matched
 */
 
-static int
+int
 check_host(void *arg, uschar *ss, uschar **valueptr, uschar **error)
 {
 check_host_block *cb = (check_host_block *)arg;
+int mlen = -1;
 int maskoffset;
+BOOL iplookup = FALSE;
 BOOL isquery = FALSE;
-uschar *semicolon, *t;
+BOOL isiponly = cb->host_name != NULL && cb->host_name[0] == 0;
+uschar *t;
+uschar *semicolon;
 uschar **aliases;
 
 /* Optimize for the special case when the pattern is "*". */
@@ -1821,12 +2678,17 @@ situation, the host address is the empty string. */
 if (cb->host_address[0] == 0) return (*ss == 0)? OK : FAIL;
 if (*ss == 0) return FAIL;
 
-/* If the pattern is precisely "@" then match against the primary host name;
-if it's "@[]" match against the local host's IP addresses. */
+/* If the pattern is precisely "@" then match against the primary host name,
+provided that host name matching is permitted; if it's "@[]" match against the
+local host's IP addresses. */
 
 if (*ss == '@')
   {
-  if (ss[1] == 0) ss = primary_hostname;
+  if (ss[1] == 0)
+    {
+    if (isiponly) return ERROR;
+    ss = primary_hostname;
+    }
   else if (Ustrcmp(ss, "@[]") == 0)
     {
     ip_address_item *ip;
@@ -1839,76 +2701,131 @@ if (*ss == '@')
 /* If the pattern is an IP address, optionally followed by a bitmask count, do
 a (possibly masked) comparision with the current IP address. */
 
-if (string_is_ip_address(ss, &maskoffset) > 0)
+if (string_is_ip_address(ss, &maskoffset) != 0)
   return (host_is_in_net(cb->host_address, ss, maskoffset)? OK : FAIL);
 
-/* 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. The net- stuff really only applies to
-single-key lookups where the 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 = Ustrchr(ss, ';')) != NULL)
+/* The pattern is not an IP address. A common error that people make is to omit
+one component of an IPv4 address, either by accident, or believing that, for
+example, 1.2.3/24 is the same as 1.2.3.0/24, or 1.2.3 is the same as 1.2.3.0,
+which it isn't. (Those applications that do accept 1.2.3 as an IP address
+interpret it as 1.2.0.3 because the final component becomes 16-bit - this is an
+ancient specification.) To aid in debugging these cases, we give a specific
+error if the pattern contains only digits and dots or contains a slash preceded
+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++);
+if (*t == 0 || (*t == '/' && t != ss))
   {
-  int mlen = 0;
-  for (t = ss + 3; isdigit(*t); t++) mlen = mlen * 10 + *t - '0';
-  if (*t++ == '-')
-    {
-    int insize;
-    int search_type;
-    int incoming[4];
-    void *handle;
-    uschar *filename, *key, *result;
-    uschar buffer[64];
+  *error = US"malformed IPv4 address or address mask";
+  return ERROR;
+  }
 
-    /* If no mask was supplied, set a negative value */
+/* See if there is a semicolon in the pattern */
 
-    if (mlen == 0 && t == ss+4) mlen = -1;
+semicolon = Ustrchr(ss, ';');
 
-    /* Find the search type */
+/* If we are doing an IP address only match, then all lookups must be IP
+address lookups, even if there is no "net-". */
 
-    search_type = search_findtype(t, semicolon - t);
+if (isiponly)
+  {
+  iplookup = semicolon != NULL;
+  }
 
-    if (search_type < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s",
-      search_error_message);
+/* 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
+have already set iplookup, so as to skip over the "net-" prefix and to set the
+mask length. The net- stuff really only applies to single-key lookups where the
+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. */
 
-    /* Adjust parameters for the type of lookup. For a query-style
-    lookup, there is no file name, and the "key" is just the query. For
-    a single-key lookup, the key is the current IP address, masked
-    appropriately, and reconverted to text form, with the mask appended.
-    For IPv6 addresses, specify dot separators instead of colons. */
+if (Ustrncmp(ss, "net", 3) == 0 && semicolon != NULL)
+  {
+  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++ == '-');
+  }
+else t = ss;
 
-    if (mac_islookup(search_type, lookup_querystyle))
-      {
-      filename = NULL;
-      key = semicolon + 1;
-      }
-    else
-      {
-      insize = host_aton(cb->host_address, incoming);
-      host_mask(insize, incoming, mlen);
-      (void)host_nmtoa(insize, incoming, mlen, buffer, '.');
-      key = buffer;
-      filename = semicolon + 1;
-      }
+/* Do the IP address lookup if that is indeed what we have */
 
-    /* Now do the actual lookup; note that there is no search_close() because
-    of the caching arrangements. */
+if (iplookup)
+  {
+  int insize;
+  int search_type;
+  int incoming[4];
+  void *handle;
+  uschar *filename, *key, *result;
+  uschar buffer[64];
+
+  /* Find the search type */
+
+  search_type = search_findtype(t, semicolon - t);
+
+  if (search_type < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s",
+    search_error_message);
+
+  /* Adjust parameters for the type of lookup. For a query-style lookup, there
+  is no file name, and the "key" is just the query. For query-style with a file
+  name, we have to fish the file off the start of the query. For a single-key
+  lookup, the key is the current IP address, masked appropriately, and
+  reconverted to text form, with the mask appended. For IPv6 addresses, specify
+  dot separators instead of colons, except when the lookup type is "iplsearch".
+  */
 
-    handle = search_open(filename, search_type, 0, NULL, NULL);
-    if (handle == 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;
+  if (mac_islookup(search_type, lookup_absfilequery))
+    {
+    filename = semicolon + 1;
+    key = filename;
+    while (*key != 0 && !isspace(*key)) key++;
+    filename = string_copyn(filename, key - filename);
+    while (isspace(*key)) key++;
+    }
+  else if (mac_islookup(search_type, lookup_querystyle))
+    {
+    filename = NULL;
+    key = semicolon + 1;
+    }
+  else   /* Single-key style */
+    {
+    int sep = (Ustrcmp(lookup_list[search_type]->name, "iplsearch") == 0)?
+      ':' : '.';
+    insize = host_aton(cb->host_address, incoming);
+    host_mask(insize, incoming, mlen);
+    (void)host_nmtoa(insize, incoming, mlen, buffer, sep);
+    key = buffer;
+    filename = semicolon + 1;
     }
+
+  /* Now do the actual lookup; note that there is no search_close() because
+  of the caching arrangements. */
+
+  handle = search_open(filename, search_type, 0, NULL, NULL);
+  if (handle == 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;
   }
 
 /* The pattern is not an IP address or network reference of any kind. That is,
-it is a host name pattern. Check the characters of the pattern to see if they
-comprise only letters, digits, full stops, and hyphens (the constituents of
-domain names). Allow underscores, as they are all too commonly found. Sigh.
-Also, if allow_utf8_domains is set, allow top-bit characters. */
+it is a host name pattern. If this is an IP only match, there's an error in the
+host list. */
+
+if (isiponly)
+  {
+  *error = US"cannot match host name in match_ip list";
+  return ERROR;
+  }
+
+/* Check the characters of the pattern to see if they comprise only letters,
+digits, full stops, and hyphens (the constituents of domain names). Allow
+underscores, as they are all too commonly found. Sigh. Also, if
+allow_utf8_domains is set, allow top-bit characters. */
 
 for (t = ss; *t != 0; t++)
   if (!isalnum(*t) && *t != '.' && *t != '-' && *t != '_' &&
@@ -1926,15 +2843,14 @@ if (*t == 0)
   h.name = ss;
   h.address = NULL;
   h.mx = MX_NONE;
-  rc = host_find_byname(&h, NULL, NULL, FALSE);
+
+  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)
       {
-      if (Ustrcmp(hh->address, (Ustrchr(hh->address, ':') == NULL)?
-        cb->host_ipv4 : cb->host_address) == 0)
-          return OK;
+      if (host_is_in_net(hh->address, cb->host_address, 0)) return OK;
       }
     return FAIL;
     }
@@ -1973,7 +2889,7 @@ if ((semicolon = Ustrchr(ss, ';')) != NULL)
       search_error_message, ss);
     return DEFER;
     }
-  isquery = mac_islookup(id, lookup_querystyle);
+  isquery = mac_islookup(id, lookup_querystyle|lookup_absfilequery);
   }
 
 if (isquery)
@@ -2133,16 +3049,18 @@ return verify_check_this_host(listptr, sender_host_cache, NULL,
 
 
 /*************************************************
-*    Invert an IP address for a DNS black list   *
+*              Invert an IP address              *
 *************************************************/
 
-/*
+/* Originally just used for DNS xBL lists, now also used for the
+reverse_ip expansion operator.
+
 Arguments:
   buffer         where to put the answer
   address        the address to invert
 */
 
-static void
+void
 invert_address(uschar *buffer, uschar *address)
 {
 int bin[4];
@@ -2188,6 +3106,12 @@ else
     }
   }
 #endif
+
+/* Remove trailing period -- this is needed so that both arbitrary
+dnsbl keydomains and inverted addresses may be combined with the
+same format string, "%s.%s" */
+
+*(--bptr) = 0;
 }
 
 
@@ -2196,15 +3120,26 @@ else
 *          Perform a single dnsbl lookup         *
 *************************************************/
 
-/* This function is called from verify_check_dnsbl() below.
+/* This function is called from verify_check_dnsbl() below. It is also called
+recursively from within itself when domain and domain_txt are different
+pointers, in order to get the TXT record from the alternate domain.
 
 Arguments:
-  domain         the outer dnsbl domain (for debug message)
+  domain         the outer dnsbl domain
+  domain_txt     alternate domain to lookup TXT record on success; when the
+                   same domain is to be used, domain_txt == domain (that is,
+                   the pointers must be identical, not just the text)
   keydomain      the current keydomain (for debug message)
-  query          the domain to be looked up
-  iplist         the list of matching IP addresses
+  prepend        subdomain to lookup (like keydomain, but
+                   reversed if IP address)
+  iplist         the list of matching IP addresses, or NULL for "any"
   bitmask        true if bitmask matching is wanted
-  invert_result  true if result to be inverted
+  match_type     condition for 'succeed' result
+                   0 => Any RR in iplist     (=)
+                   1 => No RR in iplist      (!=)
+                   2 => All RRs in iplist    (==)
+                   3 => Some RRs not in iplist (!==)
+                   the two bits are defined as MT_NOT and MT_ALL
   defer_return   what to return for a defer
 
 Returns:         OK if lookup succeeded
@@ -2212,14 +3147,25 @@ Returns:         OK if lookup succeeded
 */
 
 static int
-one_check_dnsbl(uschar *domain, uschar *keydomain, uschar *query,
-  uschar *iplist, BOOL bitmask, BOOL invert_result, int defer_return)
+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_scan dnss;
 tree_node *t;
 dnsbl_cache_block *cb;
 int old_pool = store_pool;
+uschar query[256];         /* DNS domain max length */
+
+/* Construct the specific query domainname */
+
+if (!string_format(query, sizeof(query), "%s.%s", prepend, domain))
+  {
+  log_write(0, LOG_MAIN|LOG_PANIC, "dnslist query is too long "
+    "(ignored): %s...", query);
+  return FAIL;
+  }
 
 /* Look for this query in the cache. */
 
@@ -2321,21 +3267,25 @@ if (cb->rc == DNS_SUCCEED)
 
   if (iplist != NULL)
     {
-    int ipsep = ',';
-    uschar ip[46];
-    uschar *ptr = iplist;
-
-    while (string_nextinlist(&ptr, &ipsep, ip, sizeof(ip)) != NULL)
+    for (da = cb->rhs; da != NULL; da = da->next)
       {
+      int ipsep = ',';
+      uschar ip[46];
+      uschar *ptr = iplist;
+      uschar *res;
+
       /* Handle exact matching */
+
       if (!bitmask)
         {
-        for (da = cb->rhs; da != NULL; da = da->next)
+        while ((res = string_nextinlist(&ptr, &ipsep, ip, sizeof(ip))) != NULL)
           {
           if (Ustrcmp(CS da->address, ip) == 0) break;
           }
         }
+
       /* Handle bitmask matching */
+
       else
         {
         int address[4];
@@ -2348,44 +3298,77 @@ if (cb->rc == DNS_SUCCEED)
         ignore IPv6 addresses. The default mask is 0, which always matches.
         We change this only for IPv4 addresses in the list. */
 
-        if (host_aton(ip, address) == 1) mask = address[0];
+        if (host_aton(da->address, address) == 1) mask = address[0];
 
         /* Scan the returned addresses, skipping any that are IPv6 */
 
-        for (da = cb->rhs; da != NULL; da = da->next)
+        while ((res = string_nextinlist(&ptr, &ipsep, ip, sizeof(ip))) != NULL)
           {
-          if (host_aton(da->address, address) != 1) continue;
-          if ((address[0] & mask) == mask) break;
+          if (host_aton(ip, address) != 1) continue;
+          if ((address[0] & mask) == address[0]) break;
           }
         }
 
-      /* Break out if a match has been found */
+      /* If either
+
+         (a) An IP address in an any ('=') list matched, or
+         (b) No IP address in an all ('==') list matched
 
-      if (da != NULL) break;
+      then we're done searching. */
+
+      if (((match_type & MT_ALL) != 0) == (res == NULL)) break;
       }
 
-    /* If either
+    /* If da == NULL, either
 
-       (a) No IP address in a positive list matched, or
-       (b) An IP address in a negative list did match
+       (a) No IP address in an any ('=') list matched, or
+       (b) An IP address in an all ('==') list didn't match
 
-    then behave as if the DNSBL lookup had not succeeded, i.e. the host is
-    not on the list. */
+    so behave as if the DNSBL lookup had not succeeded, i.e. the host is not on
+    the list. */
 
-    if (invert_result != (da == NULL))
+    if ((match_type == MT_NOT || match_type == MT_ALL) != (da == NULL))
       {
       HDEBUG(D_dnsbl)
         {
+        uschar *res = NULL;
+        switch(match_type)
+          {
+          case 0:
+          res = US"was no match";
+          break;
+          case MT_NOT:
+          res = US"was an exclude match";
+          break;
+          case MT_ALL:
+          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;
+          }
         debug_printf("=> but we are not accepting this block class because\n");
-        debug_printf("=> there was %s match for %c%s\n",
-          invert_result? "an exclude":"no", bitmask? '&' : '=', iplist);
+        debug_printf("=> there %s for %s%c%s\n",
+          res,
+          ((match_type & MT_ALL) == 0)? "" : "=",
+          bitmask? '&' : '=', iplist);
         }
       return FAIL;
       }
     }
 
-  /* Either there was no IP list, or the record matched. Look up a TXT record
-  if it hasn't previously been done. */
+  /* Either there was no IP list, or the record matched, implying that the
+  domain is on the list. We now want to find a corresponding TXT record. If an
+  alternate domain is specified for the TXT record, call this function
+  recursively to look that up; this has the side effect of re-checking that
+  there is indeed an A record at the alternate domain. */
+
+  if (domain_txt != domain)
+    return one_check_dnsbl(domain_txt, domain_txt, keydomain, prepend, NULL,
+      FALSE, match_type, defer_return);
+
+  /* If there is no alternate domain, look up a TXT record in the main domain
+  if it has not previously been cached. */
 
   if (!cb->text_set)
     {
@@ -2456,7 +3439,7 @@ given, comma-separated, for example: x.y.z=127.0.0.1,127.0.0.2.
 
 If no key is given, what is looked up in the domain is the inverted IP address
 of the current client host. If a key is given, it is used to construct the
-domain for the lookup. For example,
+domain for the lookup. For example:
 
   dsn.rfc-ignorant.org/$sender_address_domain
 
@@ -2465,6 +3448,17 @@ then we check for a TXT record for an error message, and if found, save its
 value in $dnslist_text. We also cache everything in a tree, to optimize
 multiple lookups.
 
+The TXT record is normally looked up in the same domain as the A record, but
+when many lists are combined in a single DNS domain, this will not be a very
+specific message. It is possible to specify a different domain for looking up
+TXT records; this is given before the main domain, comma-separated. For
+example:
+
+  dnslists = http.dnsbl.sorbs.net,dnsbl.sorbs.net=127.0.0.2 : \
+             socks.dnsbl.sorbs.net,dnsbl.sorbs.net=127.0.0.3
+
+The caching ensures that only one lookup in dnsbl.sorbs.net is done.
+
 Note: an address for testing RBL is 192.203.178.39
 Note: an address for testing DUL is 192.203.178.4
 Note: a domain for testing RFCI is example.tld.dsn.rfc-ignorant.org
@@ -2484,12 +3478,10 @@ verify_check_dnsbl(uschar **listptr)
 {
 int sep = 0;
 int defer_return = FAIL;
-BOOL invert_result = FALSE;
 uschar *list = *listptr;
 uschar *domain;
 uschar *s;
 uschar buffer[1024];
-uschar query[256];         /* DNS domain max length */
 uschar revadd[128];        /* Long enough for IPv6 address */
 
 /* Indicate that the inverted IP address is not yet set up */
@@ -2505,8 +3497,10 @@ dns_init(FALSE, FALSE);
 while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL)
   {
   int rc;
-  BOOL frc;
   BOOL bitmask = FALSE;
+  int match_type = 0;
+  uschar *domain_txt;
+  uschar *comma;
   uschar *iplist;
   uschar *key;
 
@@ -2531,8 +3525,8 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
   if (key != NULL) *key++ = 0;
 
   /* See if there's a list of addresses supplied after the domain name. This is
-  introduced by an = or a & character; if preceded by ! we invert the result.
-  */
+  introduced by an = or a & character; if preceded by = we require all matches
+  and if preceded by ! we invert the result. */
 
   iplist = Ustrchr(domain, '=');
   if (iplist == NULL)
@@ -2541,14 +3535,35 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
     iplist = Ustrchr(domain, '&');
     }
 
-  if (iplist != NULL)
+  if (iplist != NULL)                          /* Found either = or & */
     {
-    if (iplist > domain && iplist[-1] == '!')
+    if (iplist > domain && iplist[-1] == '!')  /* Handle preceding ! */
       {
-      invert_result = TRUE;
+      match_type |= MT_NOT;
       iplist[-1] = 0;
       }
-    *iplist++ = 0;
+
+    *iplist++ = 0;                             /* Terminate domain, move on */
+
+    /* If we found = (bitmask == FALSE), check for == or =& */
+
+    if (!bitmask && (*iplist == '=' || *iplist == '&'))
+      {
+      bitmask = *iplist++ == '&';
+      match_type |= MT_ALL;
+      }
+    }
+
+  /* If there is a comma in the domain, it indicates that a second domain for
+  looking up TXT records is provided, before the main domain. Otherwise we must
+  set domain_txt == domain. */
+
+  domain_txt = domain;
+  comma = Ustrchr(domain, ',');
+  if (comma != NULL)
+    {
+    *comma++ = 0;
+    domain = comma;
     }
 
   /* Check that what we have left is a sensible domain name. There is no reason
@@ -2559,7 +3574,7 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
 
   for (s = domain; *s != 0; s++)
     {
-    if (!isalnum(*s) && *s != '-' && *s != '.')
+    if (!isalnum(*s) && *s != '-' && *s != '.' && *s != '_')
       {
       log_write(0, LOG_MAIN, "dnslists domain \"%s\" contains "
         "strange characters - is this right?", domain);
@@ -2567,6 +3582,18 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
       }
     }
 
+  /* Check the alternate domain if present */
+
+  if (domain_txt != domain) for (s = domain_txt; *s != 0; 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. */
 
@@ -2574,25 +3601,15 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
     {
     if (sender_host_address == NULL) return FAIL;    /* can never match */
     if (revadd[0] == 0) invert_address(revadd, sender_host_address);
-    frc = string_format(query, sizeof(query), "%s%s", revadd, domain);
-
-    if (!frc)
-      {
-      log_write(0, LOG_MAIN|LOG_PANIC, "dnslist query is too long "
-        "(ignored): %s...", query);
-      continue;
-      }
-
-    rc = one_check_dnsbl(domain, sender_host_address, query, iplist, bitmask,
-      invert_result, defer_return);
-
+    rc = one_check_dnsbl(domain, domain_txt, sender_host_address, revadd,
+      iplist, bitmask, match_type, defer_return);
     if (rc == OK)
       {
-      dnslist_domain = string_copy(domain);
+      dnslist_domain = string_copy(domain_txt);
+      dnslist_matched = string_copy(sender_host_address);
       HDEBUG(D_dnsbl) debug_printf("=> that means %s is listed at %s\n",
-        sender_host_address, domain);
+        sender_host_address, dnslist_domain);
       }
-
     if (rc != FAIL) return rc;     /* OK or DEFER */
     }
 
@@ -2605,36 +3622,28 @@ while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL
     BOOL defer = FALSE;
     uschar *keydomain;
     uschar keybuffer[256];
+    uschar keyrevadd[128];
 
     while ((keydomain = string_nextinlist(&key, &keysep, keybuffer,
             sizeof(keybuffer))) != NULL)
       {
-      if (string_is_ip_address(keydomain, NULL) > 0)
-        {
-        uschar keyrevadd[128];
-        invert_address(keyrevadd, keydomain);
-        frc = string_format(query, sizeof(query), "%s%s", keyrevadd, domain);
-        }
-      else
-        {
-        frc = string_format(query, sizeof(query), "%s.%s", keydomain, domain);
-        }
+      uschar *prepend = keydomain;
 
-      if (!frc)
+      if (string_is_ip_address(keydomain, NULL) != 0)
         {
-        log_write(0, LOG_MAIN|LOG_PANIC, "dnslist query is too long "
-          "(ignored): %s...", query);
-        continue;
+        invert_address(keyrevadd, keydomain);
+        prepend = keyrevadd;
         }
 
-      rc = one_check_dnsbl(domain, keydomain, query, iplist, bitmask,
-        invert_result, defer_return);
+      rc = one_check_dnsbl(domain, domain_txt, keydomain, prepend, iplist,
+        bitmask, match_type, defer_return);
 
       if (rc == OK)
         {
-        dnslist_domain = string_copy(domain);
+        dnslist_domain = string_copy(domain_txt);
+        dnslist_matched = string_copy(keydomain);
         HDEBUG(D_dnsbl) debug_printf("=> that means %s is listed at %s\n",
-          keydomain, domain);
+          keydomain, dnslist_domain);
         return OK;
         }