Expand recipients_max
[exim.git] / src / src / verify.c
index 7b9d006f6ecd50236a37ed5b8282820913d15d10..bde660063623a59e4cc3f5477247428c62bffc57 100644 (file)
@@ -2,8 +2,10 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* Copyright (c) The Exim Maintainers 2020 - 2023 */
+/* Copyright (c) University of Cambridge 1995 - 2023 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions concerned with verifying things. The original code for callout
 caching was contributed by Kevin Fleming (but I hacked it around a bit). */
@@ -18,27 +20,6 @@ static smtp_context ctctx;
 uschar ctbuffer[8192];
 
 
-/* Structure for caching DNSBL lookups */
-
-typedef struct dnsbl_cache_block {
-  time_t expiry;
-  dns_address *rhs;
-  uschar *text;
-  int rc;
-  BOOL text_set;
-} dnsbl_cache_block;
-
-
-/* Anchor for DNSBL cache */
-
-static tree_node *dnsbl_cache = NULL;
-
-
-/* Bits for match_type in one_check_dnsbl() */
-
-#define MT_NOT 1
-#define MT_ALL 2
-
 static uschar cutthrough_response(client_conn_ctx *, char, uschar **, int);
 
 
@@ -70,7 +51,7 @@ dbdata_callout_cache *cache_record;
 
 if (!(cache_record = dbfn_read_with_length(dbm_file, key, &length)))
   {
-  HDEBUG(D_verify) debug_printf("callout cache: no %s record found for %s\n", type, key);
+  HDEBUG(D_verify) debug_printf_indent("callout cache: no %s record found for %s\n", type, key);
   return NULL;
   }
 
@@ -84,7 +65,7 @@ now = time(NULL);
 
 if (now - cache_record->time_stamp > expire)
   {
-  HDEBUG(D_verify) debug_printf("callout cache: %s record expired for %s\n", type, key);
+  HDEBUG(D_verify) debug_printf_indent("callout cache: %s record expired for %s\n", type, key);
   return NULL;
   }
 
@@ -98,7 +79,7 @@ if (type[0] == 'd' && cache_record->result != ccache_reject)
   {
   if (length == sizeof(dbdata_callout_cache_obs))
     {
-    dbdata_callout_cache *new = store_get(sizeof(dbdata_callout_cache), FALSE);
+    dbdata_callout_cache * new = store_get(sizeof(dbdata_callout_cache), GET_UNTAINTED);
     memcpy(new, cache_record, length);
     new->postmaster_stamp = new->random_stamp = new->time_stamp;
     cache_record = new;
@@ -111,7 +92,7 @@ if (type[0] == 'd' && cache_record->result != ccache_reject)
     cache_record->random_result = ccache_unknown;
   }
 
-HDEBUG(D_verify) debug_printf("callout cache: found %s record for %s\n", type, key);
+HDEBUG(D_verify) debug_printf_indent("callout cache: found %s record for %s\n", type, key);
 return cache_record;
 }
 
@@ -124,8 +105,8 @@ Return: TRUE if result found
 */
 
 static BOOL
-cached_callout_lookup(address_item * addr, uschar * address_key,
-  uschar * from_address, int * opt_ptr, uschar ** pm_ptr,
+cached_callout_lookup(address_item * addr, const uschar * address_key,
+  const uschar * from_address, int * opt_ptr, uschar ** pm_ptr,
   int * yield, uschar ** failure_ptr,
   dbdata_callout_cache * new_domain_record, int * old_domain_res)
 {
@@ -133,16 +114,16 @@ int options = *opt_ptr;
 open_db dbblock;
 open_db *dbm_file = NULL;
 
-/* Open the callout cache database, it it exists, for reading only at this
+/* Open the callout cache database, if it exists, for reading only at this
 stage, unless caching has been disabled. */
 
 if (options & vopt_callout_no_cache)
   {
-  HDEBUG(D_verify) debug_printf("callout cache: disabled by no_cache\n");
+  HDEBUG(D_verify) debug_printf_indent("callout cache: disabled by no_cache\n");
   }
 else if (!(dbm_file = dbfn_open(US"callout", O_RDWR, &dbblock, FALSE, TRUE)))
   {
-  HDEBUG(D_verify) debug_printf("callout cache: not available\n");
+  HDEBUG(D_verify) debug_printf_indent("callout cache: not available\n");
   }
 else
   {
@@ -173,7 +154,7 @@ else
        || *from_address == 0 && cache_record->result == ccache_reject_mfnull)
       {
       HDEBUG(D_verify)
-       debug_printf("callout cache: domain gave initial rejection, or "
+       debug_printf_indent("callout cache: domain gave initial rejection, or "
          "does not accept HELO or MAIL FROM:<>\n");
       setflag(addr, af_verify_nsfail);
       addr->user_message = US"(result of an earlier callout reused).";
@@ -194,14 +175,14 @@ else
       {
       case ccache_accept:
        HDEBUG(D_verify)
-         debug_printf("callout cache: domain accepts random addresses\n");
+         debug_printf_indent("callout cache: domain accepts random addresses\n");
        *failure_ptr = US"random";
        dbfn_close(dbm_file);
        return TRUE;     /* Default yield is OK */
 
       case ccache_reject:
        HDEBUG(D_verify)
-         debug_printf("callout cache: domain rejects random addresses\n");
+         debug_printf_indent("callout cache: domain rejects random addresses\n");
        *opt_ptr = options & ~vopt_callout_random;
        new_domain_record->random_result = ccache_reject;
        new_domain_record->random_stamp = cache_record->random_stamp;
@@ -209,7 +190,7 @@ else
 
       default:
        HDEBUG(D_verify)
-         debug_printf("callout cache: need to check random address handling "
+         debug_printf_indent("callout cache: need to check random address handling "
            "(not cached or cache expired)\n");
        dbfn_close(dbm_file);
        return FALSE;
@@ -226,7 +207,7 @@ else
        {
        setflag(addr, af_verify_pmfail);
        HDEBUG(D_verify)
-         debug_printf("callout cache: domain does not accept "
+         debug_printf_indent("callout cache: domain does not accept "
            "RCPT TO:<postmaster@domain>\n");
        *yield = FAIL;
        *failure_ptr = US"postmaster";
@@ -238,7 +219,7 @@ else
       if (cache_record->postmaster_result == ccache_unknown)
        {
        HDEBUG(D_verify)
-         debug_printf("callout cache: need to check RCPT "
+         debug_printf_indent("callout cache: need to check RCPT "
            "TO:<postmaster@domain> (not cached or cache expired)\n");
        dbfn_close(dbm_file);
        return FALSE;
@@ -249,7 +230,7 @@ else
       that the value in the cache record is preserved (with its old timestamp).
       */
 
-      HDEBUG(D_verify) debug_printf("callout cache: domain accepts RCPT "
+      HDEBUG(D_verify) debug_printf_indent("callout cache: domain accepts RCPT "
        "TO:<postmaster@domain>\n");
       *pm_ptr = NULL;
       new_domain_record->postmaster_result = ccache_accept;
@@ -273,12 +254,12 @@ else
   if (cache_address_record->result == ccache_accept)
     {
     HDEBUG(D_verify)
-      debug_printf("callout cache: address record is positive\n");
+      debug_printf_indent("callout cache: address record is positive\n");
     }
   else
     {
     HDEBUG(D_verify)
-      debug_printf("callout cache: address record is negative\n");
+      debug_printf_indent("callout cache: address record is negative\n");
     addr->user_message = US"Previous (cached) callout verification failure";
     *failure_ptr = US"recipient";
     *yield = FAIL;
@@ -297,10 +278,10 @@ return FALSE;
 */
 static void
 cache_callout_write(dbdata_callout_cache * dom_rec, const uschar * domain,
-  int done, dbdata_callout_cache_address * addr_rec, uschar * address_key)
+  int done, dbdata_callout_cache_address * addr_rec, const uschar * address_key)
 {
 open_db dbblock;
-open_db *dbm_file = NULL;
+open_db * dbm_file = NULL;
 
 /* 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.
@@ -315,13 +296,13 @@ Otherwise the value is ccache_accept, ccache_reject, or ccache_reject_mfnull. */
 if (dom_rec->result != ccache_unknown)
   if (!(dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE, TRUE)))
     {
-    HDEBUG(D_verify) debug_printf("callout cache: not available\n");
+    HDEBUG(D_verify) debug_printf_indent("callout cache: not available\n");
     }
   else
     {
     (void)dbfn_write(dbm_file, domain, dom_rec,
       (int)sizeof(dbdata_callout_cache));
-    HDEBUG(D_verify) debug_printf("wrote callout cache domain record for %s:\n"
+    HDEBUG(D_verify) debug_printf_indent("wrote callout cache domain record for %s:\n"
       "  result=%d postmaster=%d random=%d\n",
       domain,
       dom_rec->result,
@@ -338,13 +319,13 @@ if (done  &&  addr_rec->result != ccache_unknown)
     dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE, TRUE);
   if (!dbm_file)
     {
-    HDEBUG(D_verify) debug_printf("no callout cache available\n");
+    HDEBUG(D_verify) debug_printf_indent("no callout cache available\n");
     }
   else
     {
     (void)dbfn_write(dbm_file, address_key, addr_rec,
       (int)sizeof(dbdata_callout_cache_address));
-    HDEBUG(D_verify) debug_printf("wrote %s callout cache address record for %s\n",
+    HDEBUG(D_verify) debug_printf_indent("wrote %s callout cache address record for %s\n",
       addr_rec->result == ccache_accept ? "positive" : "negative",
       address_key);
     }
@@ -420,7 +401,7 @@ if (addr->transport == cutthrough.addr.transport)
 
        if (done)
          {
-         address_item * na = store_get(sizeof(address_item), FALSE);
+         address_item * na = store_get(sizeof(address_item), GET_UNTAINTED);
          *na = cutthrough.addr;
          cutthrough.addr = *addr;
          cutthrough.addr.host_used = &cutthrough.host;
@@ -465,6 +446,21 @@ return done;
 }
 
 
+
+
+/* A rcpt callout, or cached record of one, verified the address.
+Set $domain_data and $local_part_data to detainted versions.
+*/
+static void
+callout_verified_rcpt(const address_item * addr)
+{
+address_item a = {.address = addr->address};
+if (deliver_split_address(&a) != OK) return;
+deliver_localpart_data = string_copy_taint(a.local_part, GET_UNTAINTED);
+deliver_domain_data =    string_copy_taint(a.domain,     GET_UNTAINTED);
+}
+
+
 /*************************************************
 *      Do callout verification for an address    *
 *************************************************/
@@ -506,11 +502,11 @@ do_callout(address_item *addr, host_item *host_list, transport_feedback *tf,
 int yield = OK;
 int old_domain_cache_result = ccache_accept;
 BOOL done = FALSE;
-uschar *address_key;
-uschar *from_address;
-uschar *random_local_part = NULL;
-const uschar *save_deliver_domain = deliver_domain;
-uschar **failure_ptr = options & vopt_is_recipient
+const uschar * address_key;
+const uschar * from_address;
+uschar * random_local_part = NULL;
+const uschar * save_deliver_domain = deliver_domain;
+uschar ** failure_ptr = options & vopt_is_recipient
   ? &recipient_verify_failure : &sender_verify_failure;
 dbdata_callout_cache new_domain_record;
 dbdata_callout_cache_address new_address_record;
@@ -567,6 +563,7 @@ if (!addr->transport)
   {
   HDEBUG(D_verify) debug_printf("cannot callout via null transport\n");
   }
+
 else if (Ustrcmp(addr->transport->driver_name, "smtp") != 0)
   log_write(0, LOG_MAIN|LOG_PANIC|LOG_CONFIG_FOR, "callout transport '%s': %s is non-smtp",
     addr->transport->name, addr->transport->driver_name);
@@ -670,16 +667,17 @@ coding means skipping this whole loop and doing the append separately.  */
       log_write(0, LOG_MAIN|LOG_PANIC, "<%s>: %s", addr->address,
         addr->message);
 
-    if (!sx) sx = store_get(sizeof(*sx), TRUE);        /* tainted buffers */
+    if (!sx) sx = store_get(sizeof(*sx), GET_TAINTED); /* tainted buffers */
     memset(sx, 0, sizeof(*sx));
 
-    sx->addrlist = addr;
+    sx->addrlist = sx->first_addr = addr;
     sx->conn_args.host = host;
     sx->conn_args.host_af = host_af,
     sx->port = port;
     sx->conn_args.interface = interface;
     sx->helo_data = tf->helo_data;
     sx->conn_args.tblock = addr->transport;
+    sx->cctx.sock = sx->conn_args.sock = -1;
     sx->verify = TRUE;
 
 tls_retry_connection:
@@ -711,6 +709,30 @@ tls_retry_connection:
     if (yield != OK)
       {
       errno = addr->basic_errno;
+
+      /* For certain errors we want specifically to log the transport name,
+      for ease of fixing config errors. Slightly ugly doing it here, but we want
+      to not leak that also in the SMTP response. */
+      switch (errno)
+       {
+       case EPROTOTYPE:
+       case ENOPROTOOPT:
+       case EPROTONOSUPPORT:
+       case ESOCKTNOSUPPORT:
+       case EOPNOTSUPP:
+       case EPFNOSUPPORT:
+       case EAFNOSUPPORT:
+       case EADDRINUSE:
+       case EADDRNOTAVAIL:
+       case ENETDOWN:
+       case ENETUNREACH:
+         log_write(0, LOG_MAIN|LOG_PANIC,
+           "%s verify %s (making calloout connection): T=%s %s",
+           options & vopt_is_recipient ? "sender" : "recipient",
+           yield == FAIL ? "fail" : "defer",
+           transport_name, strerror(errno));
+       }
+
       transport_name = NULL;
       deliver_host = deliver_host_address = NULL;
       deliver_domain = save_deliver_domain;
@@ -749,7 +771,7 @@ tls_retry_connection:
 
     if (random_local_part)
       {
-      uschar * main_address = addr->address;
+      const uschar * main_address = addr->address;
       const uschar * rcpt_domain = addr->domain;
 
 #ifdef SUPPORT_I18N
@@ -801,7 +823,7 @@ tls_retry_connection:
       /* Remember when we last did a random test */
       new_domain_record.random_stamp = time(NULL);
 
-      if (smtp_write_mail_and_rcpt_cmds(sx, &yield) == 0)
+      if (smtp_write_mail_and_rcpt_cmds(sx, &yield) == sw_mrc_ok)
        switch(addr->transport_return)
          {
          case PENDING_OK:      /* random was accepted, unfortunately */
@@ -834,7 +856,7 @@ tls_retry_connection:
            sx->cctx.sock = -1;
 #ifndef DISABLE_EVENT
            (void) event_raise(addr->transport->event_action,
-                             US"tcp:close", NULL);
+                             US"tcp:close", NULL, NULL);
 #endif
            addr->address = main_address;
            addr->transport_return = PENDING_DEFER;
@@ -869,33 +891,34 @@ tls_retry_connection:
       done = FALSE;
       switch(smtp_write_mail_and_rcpt_cmds(sx, &yield))
        {
-       case 0:  switch(addr->transport_return) /* ok so far */
-                   {
-                   case PENDING_OK:  done = TRUE;
-                                     new_address_record.result = ccache_accept;
-                                     break;
-                   case FAIL:      done = TRUE;
-                                     yield = FAIL;
-                                     *failure_ptr = US"recipient";
-                                     new_address_record.result = ccache_reject;
-                                     break;
-                   default:        break;
-                   }
-                 break;
+       case sw_mrc_ok:
+         switch(addr->transport_return)        /* ok so far */
+           {
+           case PENDING_OK:  done = TRUE;
+                             new_address_record.result = ccache_accept;
+                             break;
+           case FAIL:        done = TRUE;
+                             yield = FAIL;
+                             *failure_ptr = US"recipient";
+                             new_address_record.result = ccache_reject;
+                             break;
+           default:          break;
+           }
+         break;
 
-       case -1:                                /* MAIL response error */
-                 *failure_ptr = US"mail";
-                 if (errno == 0 && sx->buffer[0] == '5')
-                   {
-                   setflag(addr, af_verify_nsfail);
-                   if (from_address[0] == 0)
-                     new_domain_record.result = ccache_reject_mfnull;
-                   }
-                 break;
-                                               /* non-MAIL read i/o error */
-                                               /* non-MAIL response timeout */
-                                               /* internal error; channel still usable */
-       default:  break;                        /* transmit failed */
+       case sw_mrc_bad_mail:                   /* MAIL response error */
+         *failure_ptr = US"mail";
+         if (errno == 0 && sx->buffer[0] == '5')
+           {
+           setflag(addr, af_verify_nsfail);
+           if (from_address[0] == 0)
+             new_domain_record.result = ccache_reject_mfnull;
+           }
+         break;
+                                       /* non-MAIL read i/o error */
+                                       /* non-MAIL response timeout */
+                                       /* internal error; channel still usable */
+       default:  break;                /* transmit failed */
        }
       }
 
@@ -920,7 +943,7 @@ tls_retry_connection:
 
       if (done)
        {
-       uschar * main_address = addr->address;
+       const uschar * main_address = addr->address;
 
        /*XXX oops, affixes */
        addr->address = string_sprintf("postmaster@%.1000s", addr->domain);
@@ -933,7 +956,7 @@ tls_retry_connection:
        sx->completed_addr = FALSE;
        sx->avoid_option = OPTION_SIZE;
 
-       if(  smtp_write_mail_and_rcpt_cmds(sx, &yield) == 0
+       if(  smtp_write_mail_and_rcpt_cmds(sx, &yield) == sw_mrc_ok
          && addr->transport_return == PENDING_OK
          )
          done = TRUE;
@@ -1058,6 +1081,8 @@ no_conn:
         HDEBUG(D_acl|D_v) debug_printf("Cutthrough cancelled by presence of transport filter\n");
         }
 #ifndef DISABLE_DKIM
+      /* DKIM signing needs to add a header after seeing the whole body, so we cannot just copy
+      body bytes to the outbound as they are received, which is the intent of cutthrough. */
       if (ob->dkim.dkim_domain)
         {
         cutthrough.delivery= FALSE;
@@ -1115,7 +1140,7 @@ no_conn:
       for (address_item * caddr = &cutthrough.addr, * parent = addr->parent;
           parent;
           caddr = caddr->parent, parent = parent->parent)
-        *(caddr->parent = store_get(sizeof(address_item), FALSE)) = *parent;
+        *(caddr->parent = store_get(sizeof(address_item), GET_UNTAINTED)) = *parent;
 
       ctctx.outblock.buffer = ctbuffer;
       ctctx.outblock.buffersize = sizeof(ctbuffer);
@@ -1128,7 +1153,7 @@ no_conn:
       /* Ensure no cutthrough on multiple verifies that were incompatible */
       if (options & vopt_callout_recipsender)
         cancel_cutthrough_connection(TRUE, US"not usable for cutthrough");
-      if (sx->send_quit)
+      if (sx->send_quit && sx->cctx.sock >= 0)
        if (smtp_write_command(sx, SCMD_FLUSH, "QUIT\r\n") != -1)
          /* Wait a short time for response, and discard it */
          smtp_read_response(sx, sx->buffer, sizeof(sx->buffer), '2', 1);
@@ -1145,8 +1170,9 @@ no_conn:
        HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP(close)>>\n");
        (void)close(sx->cctx.sock);
        sx->cctx.sock = -1;
+       smtp_debug_cmd_report();
 #ifndef DISABLE_EVENT
-       (void) event_raise(addr->transport->event_action, US"tcp:close", NULL);
+       (void) event_raise(addr->transport->event_action, US"tcp:close", NULL, NULL);
 #endif
        }
       }
@@ -1261,7 +1287,7 @@ return FALSE;
 
 
 static BOOL
-_cutthrough_puts(uschar * cp, int n)
+_cutthrough_puts(const uschar * cp, int n)
 {
 while(n--)
  {
@@ -1276,7 +1302,7 @@ return TRUE;
 
 /* Buffered output of counted data block.   Return boolean success */
 static BOOL
-cutthrough_puts(uschar * cp, int n)
+cutthrough_puts(const uschar * cp, int n)
 {
 if (cutthrough.cctx.sock < 0) return TRUE;
 if (_cutthrough_puts(cp, n))  return TRUE;
@@ -1328,7 +1354,13 @@ cutthrough_data_puts(US"\r\n", 2);
 }
 
 
-/* Get and check response from cutthrough target */
+/* Get and check response from cutthrough target.
+Used for
+- nonfirst RCPT
+- predata
+- data finaldot
+- cutthrough conn close
+*/
 static uschar
 cutthrough_response(client_conn_ctx * cctx, char expect, uschar ** copy, int timeout)
 {
@@ -1342,7 +1374,7 @@ sx.inblock.ptr = inbuffer;
 sx.inblock.ptrend = inbuffer;
 sx.inblock.cctx = cctx;
 if(!smtp_read_response(&sx, responsebuffer, sizeof(responsebuffer), expect, timeout))
-  cancel_cutthrough_connection(TRUE, US"target timeout on read");
+  cancel_cutthrough_connection(TRUE, US"unexpected response to smtp command");
 
 if(copy)
   {
@@ -1365,7 +1397,7 @@ cutthrough_predata(void)
 if(cutthrough.cctx.sock < 0 || cutthrough.callout_hold_only)
   return FALSE;
 
-HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP>> DATA\n");
+smtp_debug_cmd(US"DATA", 0);
 cutthrough_puts(US"DATA\r\n", 6);
 cutthrough_flush_send();
 
@@ -1376,9 +1408,9 @@ return cutthrough_response(&cutthrough.cctx, '3', NULL, CUTTHROUGH_DATA_TIMEOUT)
 
 /* tctx arg only to match write_chunk() */
 static BOOL
-cutthrough_write_chunk(transport_ctx * tctx, uschar * s, int len)
+cutthrough_write_chunk(transport_ctx * tctx, const uschar * s, int len)
 {
-uschar * s2;
+const uschar * s2;
 while(s && (s2 = Ustrchr(s, '\n')))
  {
  if(!cutthrough_puts(s, s2-s) || !cutthrough_put_nl())
@@ -1433,7 +1465,7 @@ if(fd >= 0)
   */
   client_conn_ctx tmp_ctx = cutthrough.cctx;
   ctctx.outblock.ptr = ctbuffer;
-  HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP>> QUIT\n");
+  smtp_debug_cmd(US"QUIT", 0);
   _cutthrough_puts(US"QUIT\r\n", 6);   /* avoid recursion */
   _cutthrough_flush_send();
   cutthrough.cctx.sock = -1;           /* avoid recursion via read timeout */
@@ -1452,6 +1484,7 @@ if(fd >= 0)
 #endif
   HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP(close)>>\n");
   (void)close(fd);
+  smtp_debug_cmd_report();
   HDEBUG(D_acl) debug_printf_indent("----------- cutthrough shutdown (%s) ------------\n", why);
   }
 ctctx.outblock.ptr = ctbuffer;
@@ -1611,7 +1644,7 @@ address testing (-bt), which is indicated by address_test_mode being set.
 Arguments:
   vaddr            contains the address to verify; the next field in this block
                      must be NULL
-  f                if not NULL, write the result to this file
+  fp               if not NULL, write the result to this file
   options          various option bits:
                      vopt_fake_sender => this sender verify is not for the real
                        sender (it was verify=sender=xxxx or an address from a
@@ -1662,19 +1695,19 @@ 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 :
-   f.address_test_mode? v_none :
-          options & vopt_is_recipient? v_recipient : v_sender;
-address_item *addr_list;
-address_item *addr_new = NULL;
-address_item *addr_remote = NULL;
-address_item *addr_local = NULL;
-address_item *addr_succeed = NULL;
-uschar **failure_ptr = options & vopt_is_recipient
+int verify_type = expn ? v_expn :
+   f.address_test_mode ? v_none :
+          options & vopt_is_recipient ? v_recipient : v_sender;
+address_item * addr_list;
+address_item * addr_new = NULL;
+address_item * addr_remote = NULL;
+address_item * addr_local = NULL;
+address_item * addr_succeed = NULL;
+uschar ** failure_ptr = options & vopt_is_recipient
   ? &recipient_verify_failure : &sender_verify_failure;
-uschar *ko_prefix, *cr;
-uschar *address = vaddr->address;
-uschar *save_sender;
+uschar * ko_prefix, * cr;
+const uschar * address = vaddr->address;
+const uschar * save_sender;
 uschar null_sender[] = { 0 };             /* Ensure writeable memory */
 
 /* Clear, just in case */
@@ -1704,7 +1737,8 @@ if (parse_find_at(address) == NULL)
     *failure_ptr = US"qualify";
     return FAIL;
     }
-  address = rewrite_address_qualify(address, options & vopt_is_recipient);
+  /* deconst ok as address was not const */
+  address = US rewrite_address_qualify(address, options & vopt_is_recipient);
   }
 
 DEBUG(D_verify)
@@ -1718,7 +1752,7 @@ may have been set by domains and local part tests during an ACL. */
 
 if (global_rewrite_rules)
   {
-  uschar *old = address;
+  const uschar * old = address;
   address = rewrite_address(address, options & vopt_is_recipient, FALSE,
     global_rewrite_rules, rewrite_existflags);
   if (address != old)
@@ -1845,6 +1879,8 @@ while (addr_new)
 
   if (rc == OK)
     {
+    BOOL local_verify = FALSE;
+
     if (routed) *routed = TRUE;
     if (callout > 0)
       {
@@ -1871,72 +1907,76 @@ while (addr_new)
       transport's options, so as to mimic what would happen if we were really
       sending a message to this address. */
 
-      if ((tp = addr->transport) && !tp->info->local)
-        {
-        (void)(tp->setup)(tp, addr, &tf, 0, 0, NULL);
+      if ((tp = addr->transport))
+       if (!tp->info->local)
+         {
+         (void)(tp->setup)(tp, addr, &tf, 0, 0, NULL);
 
-        /* If the transport has hosts and the router does not, or if the
-        transport is configured to override the router's hosts, we must build a
-        host list of the transport's hosts, and find the IP addresses */
+         /* If the transport has hosts and the router does not, or if the
+         transport is configured to override the router's hosts, we must build a
+         host list of the transport's hosts, and find the IP addresses */
 
-        if (tf.hosts && (!host_list || tf.hosts_override))
-          {
-          uschar *s;
-          const uschar *save_deliver_domain = deliver_domain;
-          uschar *save_deliver_localpart = deliver_localpart;
-
-          host_list = NULL;    /* Ignore the router's hosts */
-
-          deliver_domain = addr->domain;
-          deliver_localpart = addr->local_part;
-          s = expand_string(tf.hosts);
-          deliver_domain = save_deliver_domain;
-          deliver_localpart = save_deliver_localpart;
-
-          if (!s)
-            {
-            log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand list of hosts "
-              "\"%s\" in %s transport for callout: %s", tf.hosts,
-              tp->name, expand_string_message);
-            }
-          else
-            {
-            int flags;
-            host_build_hostlist(&host_list, s, tf.hosts_randomize);
-
-            /* Just ignore failures to find a host address. If we don't manage
-            to find any addresses, the callout will defer. Note that more than
-            one address may be found for a single host, which will result in
-            additional host items being inserted into the chain. Hence we must
-            save the next host first. */
-
-            flags = HOST_FIND_BY_A | HOST_FIND_BY_AAAA;
-            if (tf.qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE;
-            if (tf.search_parents) flags |= HOST_FIND_SEARCH_PARENTS;
-
-            for (host_item * host = host_list, * nexthost; host; host = nexthost)
-              {
-              nexthost = host->next;
-              if (tf.gethostbyname ||
-                  string_is_ip_address(host->name, NULL) != 0)
-                (void)host_find_byname(host, NULL, flags, NULL, TRUE);
-              else
+         if (tf.hosts && (!host_list || tf.hosts_override))
+           {
+           uschar *s;
+           const uschar * save_deliver_domain = deliver_domain;
+           const uschar * save_deliver_localpart = deliver_localpart;
+
+           host_list = NULL;    /* Ignore the router's hosts */
+
+           deliver_domain = addr->domain;
+           deliver_localpart = addr->local_part;
+           s = expand_string(tf.hosts);
+           deliver_domain = save_deliver_domain;
+           deliver_localpart = save_deliver_localpart;
+
+           if (!s)
+             {
+             log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand list of hosts "
+               "\"%s\" in %s transport for callout: %s", tf.hosts,
+               tp->name, expand_string_message);
+             }
+           else
+             {
+             int flags;
+             host_build_hostlist(&host_list, s, tf.hosts_randomize);
+
+             /* Just ignore failures to find a host address. If we don't manage
+             to find any addresses, the callout will defer. Note that more than
+             one address may be found for a single host, which will result in
+             additional host items being inserted into the chain. Hence we must
+             save the next host first. */
+
+             flags = HOST_FIND_BY_A | HOST_FIND_BY_AAAA;
+             if (tf.qualify_single) flags |= HOST_FIND_QUALIFY_SINGLE;
+             if (tf.search_parents) flags |= HOST_FIND_SEARCH_PARENTS;
+
+             for (host_item * host = host_list, * nexthost; host; host = nexthost)
                {
-               const dnssec_domains * dsp = NULL;
-               if (Ustrcmp(tp->driver_name, "smtp") == 0)
+               nexthost = host->next;
+               if (tf.gethostbyname ||
+                   string_is_ip_address(host->name, NULL) != 0)
+                 (void)host_find_byname(host, NULL, flags, NULL, TRUE);
+               else
                  {
-                 smtp_transport_options_block * ob =
-                     (smtp_transport_options_block *) tp->options_block;
-                 dsp = &ob->dnssec;
-                 }
+                 const dnssec_domains * dsp = NULL;
+                 if (Ustrcmp(tp->driver_name, "smtp") == 0)
+                   {
+                   smtp_transport_options_block * ob =
+                       (smtp_transport_options_block *) tp->options_block;
+                   dsp = &ob->dnssec;
+                   }
 
-                (void) host_find_bydns(host, NULL, flags, NULL, NULL, NULL,
-                 dsp, NULL, NULL);
+                 (void) host_find_bydns(host, NULL, flags, NULL, NULL, NULL,
+                   dsp, NULL, NULL);
+                 }
                }
-              }
-            }
-          }
-        }
+             }
+           }
+         }
+       else if (  options & vopt_quota
+               && Ustrcmp(tp->driver_name, "appendfile") == 0)
+         local_verify = TRUE;
 
       /* Can only do a callout if we have at least one host! If the callout
       fails, it will have set ${sender,recipient}_verify_failure. */
@@ -1960,13 +2000,25 @@ while (addr_new)
 #ifndef DISABLE_TLS
          deliver_set_expansions(NULL);
 #endif
+         if (  options & vopt_is_recipient
+            && rc == OK
+                        /* set to "random", with OK, for an accepted random */
+            && !recipient_verify_failure
+            )
+           callout_verified_rcpt(addr);
           }
         }
+      else if (local_verify)
+       {
+        HDEBUG(D_verify) debug_printf("Attempting quota verification\n");
+
+       deliver_set_expansions(addr);
+       deliver_local(addr, TRUE);
+       rc = addr->transport_return;
+       }
       else
-        {
         HDEBUG(D_verify) debug_printf("Cannot do callout: neither router nor "
           "transport provided a host list, or transport is not smtp\n");
-        }
       }
     }
 
@@ -2155,10 +2207,6 @@ for (addr_list = addr_local, i = 0; i < 2; addr_list = addr_remote, i++)
     addr_list = addr->next;
 
     fprintf(fp, "%s", CS addr->address);
-#ifdef EXPERIMENTAL_SRS
-    if(addr->prop.srs_sender)
-      fprintf(fp, "    [srs = %s]", addr->prop.srs_sender);
-#endif
 
     /* If the address is a duplicate, show something about it. */
 
@@ -2260,7 +2308,7 @@ for (header_line * h = header_list; h && yield == OK; h = h->next)
 
   colon = Ustrchr(h->text, ':');
   s = colon + 1;
-  while (isspace(*s)) s++;
+  Uskip_whitespace(&s);
 
   /* Loop for multiple addresses in the header, enabling group syntax. Note
   that we have to reset this after the header has been scanned. */
@@ -2339,7 +2387,7 @@ for (header_line * h = header_list; h && yield == OK; h = h->next)
     /* Advance to the next address */
 
     s = ss + (terminator ? 1 : 0);
-    while (isspace(*s)) s++;
+    Uskip_whitespace(&s);
     }   /* Next address */
 
   f.parse_allow_group = FALSE;
@@ -2407,17 +2455,17 @@ verify_check_notblind(BOOL case_sensitive)
 for (int i = 0; i < recipients_count; i++)
   {
   BOOL found = FALSE;
-  uschar *address = recipients_list[i].address;
+  const uschar * address = recipients_list[i].address;
 
   for (header_line * h = header_list; !found && h; h = h->next)
     {
-    uschar *colon, *s;
+    uschar * colon, * s;
 
     if (h->type != htype_to && h->type != htype_cc) continue;
 
     colon = Ustrchr(h->text, ':');
     s = colon + 1;
-    while (isspace(*s)) s++;
+    Uskip_whitespace(&s);
 
     /* Loop for multiple addresses in the header, enabling group syntax. Note
     that we have to reset this after the header has been scanned. */
@@ -2455,7 +2503,7 @@ for (int i = 0; i < recipients_count; i++)
       /* Advance to the next address */
 
       s = ss + (terminator ? 1:0);
-      while (isspace(*s)) s++;
+      Uskip_whitespace(&s);
       }   /* Next address */
 
     f.parse_allow_group = FALSE;
@@ -2485,7 +2533,7 @@ Returns:     pointer to an address item, or NULL
 */
 
 address_item *
-verify_checked_sender(uschar *sender)
+verify_checked_sender(const uschar * sender)
 {
 for (address_item * addr = sender_verified_list; addr; addr = addr->next)
   if (Ustrcmp(sender, addr->address) == 0) return addr;
@@ -2881,35 +2929,33 @@ Returns:         OK      matched
 */
 
 int
-check_host(void *arg, const uschar *ss, const uschar **valueptr, uschar **error)
+check_host(void * arg, const uschar * ss, const uschar ** valueptr, uschar ** error)
 {
-check_host_block *cb = (check_host_block *)arg;
+check_host_block * cb = (check_host_block *)arg;
 int mlen = -1;
 int maskoffset;
-BOOL iplookup = FALSE;
-BOOL isquery = FALSE;
-BOOL isiponly = cb->host_name != NULL && cb->host_name[0] == 0;
-const uschar *t;
-uschar *semicolon;
-uschar **aliases;
+BOOL iplookup = FALSE, isquery = FALSE;
+BOOL isiponly = cb->host_name && !cb->host_name[0];
+const uschar * t;
+uschar * semicolon, * endname, * opts;
+uschar ** aliases;
 
 /* Optimize for the special case when the pattern is "*". */
 
-if (*ss == '*' && ss[1] == 0) return OK;
+if (*ss == '*' && !ss[1]) return OK;
 
 /* If the pattern is empty, it matches only in the case when there is no host -
 this can occur in ACL checking for SMTP input using the -bs option. In this
 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 (!cb->host_address[0]) return *ss ? FAIL : OK;
+if (!*ss) return FAIL;
 
 /* 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)
     {
     if (isiponly) return ERROR;
@@ -2921,13 +2967,12 @@ if (*ss == '@')
       if (Ustrcmp(ip->address, cb->host_address) == 0) return OK;
     return FAIL;
     }
-  }
 
 /* If the pattern is an IP address, optionally followed by a bitmask count, do
 a (possibly masked) comparison with the current IP address. */
 
 if (string_is_ip_address(ss, &maskoffset) != 0)
-  return (host_is_in_net(cb->host_address, ss, maskoffset)? OK : FAIL);
+  return host_is_in_net(cb->host_address, ss, maskoffset) ? OK : FAIL;
 
 /* 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
@@ -2938,26 +2983,49 @@ 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). */
+dots).  Then the equivalent for IPv6 (roughly). */
 
-for (t = ss; isdigit(*t) || *t == '.'; ) t++;
-if (*t == 0 || (*t == '/' && t != ss))
+if (Ustrchr(ss, ':'))
   {
-  *error = US"malformed IPv4 address or address mask";
-  return ERROR;
+  for (t = ss; isxdigit(*t) || *t == ':' || *t == '.'; ) t++;
+  if (!*t  ||  (*t == '/' || *t == '%') && t != ss)
+    {
+    *error = string_sprintf("malformed IPv6 address or address mask: %.*s", (int)(t - ss), ss);
+    return ERROR;
+    }
+  }
+else
+  {
+  for (t = ss; isdigit(*t) || *t == '.'; ) t++;
+  if (!*t  || (*t == '/' && t != ss))
+    {
+    *error = string_sprintf("malformed IPv4 address or address mask: %.*s", (int)(t - ss), ss);
+    return ERROR;
+    }
   }
 
-/* See if there is a semicolon in the pattern */
+/* See if there is a semicolon in the pattern, separating a searchtype
+prefix.  If there is one then check for comma-sep options. */
 
-semicolon = Ustrchr(ss, ';');
+if ((semicolon = Ustrchr(ss, ';')))
+  if ((opts = Ustrchr(ss, ',')) && opts < semicolon)
+    {
+    endname = opts++;
+    opts = string_copyn(opts, semicolon - opts);
+    }
+  else
+    {
+    endname = semicolon;
+    opts = NULL;
+    }
+else
+  opts = NULL;
 
 /* If we are doing an IP address only match, then all lookups must be IP
 address lookups, even if there is no "net-". */
 
 if (isiponly)
-  {
   iplookup = semicolon != NULL;
-  }
 
 /* Otherwise, if the item is of the form net[n]-lookup;<file|query> then it is
 a lookup on a masked IP network, in textual form. We obey this code even if we
@@ -2967,12 +3035,12 @@ key is implicit. For query-style lookups the key is specified in the query.
 From release 4.30, the use of net- for query style is no longer needed, but we
 retain it for backward compatibility. */
 
-if (Ustrncmp(ss, "net", 3) == 0 && semicolon != NULL)
+if (Ustrncmp(ss, "net", 3) == 0 && semicolon)
   {
   mlen = 0;
   for (t = ss + 3; isdigit(*t); t++) mlen = mlen * 10 + *t - '0';
   if (mlen == 0 && t == ss+3) mlen = -1;  /* No mask supplied */
-  iplookup = (*t++ == '-');
+  iplookup = *t++ == '-';
   }
 else
   t = ss;
@@ -2990,7 +3058,7 @@ if (iplookup)
 
   /* Find the search type */
 
-  search_type = search_findtype(t, semicolon - t);
+  search_type = search_findtype(t, endname - t);
 
   if (search_type < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s",
     search_error_message);
@@ -3033,7 +3101,7 @@ if (iplookup)
   if (!(handle = search_open(filename, search_type, 0, NULL, NULL)))
     log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s", search_error_message);
 
-  result = search_find(handle, filename, key, -1, NULL, 0, 0, NULL);
+  result = search_find(handle, filename, key, -1, NULL, 0, 0, NULL, opts);
   if (valueptr) *valueptr = result;
   return result ? OK : f.search_find_defer ? DEFER: FAIL;
   }
@@ -3053,7 +3121,7 @@ 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++)
+for (t = ss; *t; t++)
   if (!isalnum(*t) && *t != '.' && *t != '-' && *t != '_' &&
       (!allow_utf8_domains || *t < 128)) break;
 
@@ -3061,7 +3129,7 @@ for (t = ss; *t != 0; t++)
 its IP address and match against that. Note that a multi-homed host will add
 items to the chain. */
 
-if (*t == 0)
+if (!*t)
   {
   int rc;
   host_item h;
@@ -3091,9 +3159,9 @@ using the general string matching function. When this function is called for
 outgoing hosts, the name is always given explicitly. If it is NULL, it means we
 must use sender_host_name and its aliases, looking them up if necessary. */
 
-if (cb->host_name != NULL)   /* Explicit host name given */
-  return match_check_string(cb->host_name, ss, -1, TRUE, TRUE, TRUE,
-    valueptr);
+if (cb->host_name)   /* Explicit host name given */
+  return match_check_string(cb->host_name, ss, -1,
+    MCS_PARTIAL | MCS_CASELESS | MCS_AT_SPECIAL | cb->flags, valueptr);
 
 /* Host name not given; in principle we need the sender host name and its
 aliases. However, for query-style lookups, we do not need the name if the
@@ -3101,13 +3169,14 @@ query does not contain $sender_host_name. From release 4.23, a reference to
 $sender_host_name causes it to be looked up, so we don't need to do the lookup
 on spec. */
 
-if ((semicolon = Ustrchr(ss, ';')) != NULL)
+if ((semicolon = Ustrchr(ss, ';')))
   {
-  const uschar *affix;
+  const uschar * affix, * opts;
   int partial, affixlen, starflags, id;
 
   *semicolon = 0;
-  id = search_findtype_partial(ss, &partial, &affix, &affixlen, &starflags);
+  id = search_findtype_partial(ss, &partial, &affix, &affixlen, &starflags,
+         &opts);
   *semicolon=';';
 
   if (id < 0)                           /* Unknown lookup type */
@@ -3121,7 +3190,9 @@ if ((semicolon = Ustrchr(ss, ';')) != NULL)
 
 if (isquery)
   {
-  switch(match_check_string(US"", ss, -1, TRUE, TRUE, TRUE, valueptr))
+  switch(match_check_string(US"", ss, -1,
+      MCS_PARTIAL| MCS_CASELESS| MCS_AT_SPECIAL | (cb->flags & MCS_CACHEABLE),
+      valueptr))
     {
     case OK:    return OK;
     case DEFER: return DEFER;
@@ -3147,7 +3218,9 @@ if (!sender_host_name)
 
 /* Match on the sender host name, using the general matching function */
 
-switch(match_check_string(sender_host_name, ss, -1, TRUE, TRUE, TRUE, valueptr))
+switch(match_check_string(sender_host_name, ss, -1,
+      MCS_PARTIAL| MCS_CASELESS| MCS_AT_SPECIAL | (cb->flags & MCS_CACHEABLE),
+      valueptr))
   {
   case OK:    return OK;
   case DEFER: return DEFER;
@@ -3157,7 +3230,9 @@ switch(match_check_string(sender_host_name, ss, -1, TRUE, TRUE, TRUE, valueptr))
 
 aliases = sender_host_aliases;
 while (*aliases)
-  switch(match_check_string(*aliases++, ss, -1, TRUE, TRUE, TRUE, valueptr))
+  switch(match_check_string(*aliases++, ss, -1,
+      MCS_PARTIAL| MCS_CASELESS| MCS_AT_SPECIAL | (cb->flags & MCS_CACHEABLE),
+      valueptr))
     {
     case OK:    return OK;
     case DEFER: return DEFER;
@@ -3233,8 +3308,8 @@ rc = match_check_list(
        check_host,                             /* function for testing */
        &cb,                                    /* argument for function */
        MCL_HOST,                               /* type of check */
-       (host_address == sender_host_address)?
-         US"host" : host_address,              /* text for debugging */
+       host_address == sender_host_address
+         ? US"host" : host_address,           /* text for debugging */
        valueptr);                              /* where to pass back data */
 deliver_host_address = save_host_address;
 return rc;
@@ -3343,573 +3418,246 @@ same format string, "%s.%s" */
 
 
 
-/*************************************************
-*          Perform a single dnsbl lookup         *
-*************************************************/
+/****************************************************
+  Verify a local user account for quota sufficiency
+****************************************************/
 
-/* 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.
+/* The real work, done via a re-exec for privs, calls
+down to the transport for the quota check.
 
-Arguments:
-  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)
-  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
-  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
-                 FAIL if not
-*/
-
-static int
-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 = store_get_dns_answer();
-dns_scan dnss;
-tree_node *t;
-dnsbl_cache_block *cb;
-int old_pool = store_pool;
-uschar query[256];         /* DNS domain max length */
-
-/* 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. */
+Route and transport (in recipient-verify mode) the
+given recipient. 
 
-if (  (t = tree_search(dnsbl_cache, query))
-   && (cb = t->data.ptr)->expiry > time(NULL)
-   )
+A routing result indicating any transport type other than appendfile
+results in a fail.
 
-/* Previous lookup was cached */
-
-  {
-  HDEBUG(D_dnsbl) debug_printf("dnslists: using result of previous lookup\n");
-  }
+Return, on stdout, a result string containing:
+- highlevel result code (OK, DEFER, FAIL)
+- errno
+- where string
+- message string
+*/
 
-/* If not cached from a previous lookup, we must do a DNS lookup, and
-cache the result in permanent memory. */
+void
+verify_quota(uschar * address)
+{
+address_item vaddr = {.address = address};
+BOOL routed;
+uschar * msg = US"\0";
+int rc, len = 1;
 
-else
+if ((rc = verify_address(&vaddr, NULL, vopt_is_recipient | vopt_quota,
+    1, 0, 0, NULL, NULL, &routed)) != OK)
   {
-  uint ttl = 3600;     /* max TTL for positive cache entries */
-
-  store_pool = POOL_PERM;
-
-  if (t)
+  uschar * where = recipient_verify_failure;
+  msg = acl_verify_message ? acl_verify_message : vaddr.message;
+  if (!msg) msg = US"";
+  if (rc == DEFER && vaddr.basic_errno == ERRNO_EXIMQUOTA)
     {
-    HDEBUG(D_dnsbl) debug_printf("cached data found but past valid time; ");
+    rc = FAIL;                                 /* DEFER -> FAIL */
+    where = US"quota";
+    vaddr.basic_errno = 0;
     }
+  else if (!where) where = US"";
 
-  else
-    {  /* Set up a tree entry to cache the lookup */
-    t = store_get(sizeof(tree_node) + Ustrlen(query), is_tainted(query));
-    Ustrcpy(t->name, query);
-    t->data.ptr = cb = store_get(sizeof(dnsbl_cache_block), FALSE);
-    (void)tree_insertnode(&dnsbl_cache, t);
-    }
-
-  /* Do the DNS lookup . */
-
-  HDEBUG(D_dnsbl) debug_printf("new DNS lookup for %s\n", query);
-  cb->rc = dns_basic_lookup(dnsa, query, T_A);
-  cb->text_set = FALSE;
-  cb->text = NULL;
-  cb->rhs = NULL;
-
-  /* If the lookup succeeded, cache the RHS address. The code allows for
-  more than one address - this was for complete generality and the possible
-  use of A6 records. However, A6 records are no longer supported. Leave the code
-  here, just in case.
-
-  Quite apart from one A6 RR generating multiple addresses, there are DNS
-  lists that return more than one A record, so we must handle multiple
-  addresses generated in that way as well.
-
-  Mark the cache entry with the "now" plus the minimum of the address TTLs,
-  or the RFC 2308 negative-cache value from the SOA if none were found. */
-
-  switch (cb->rc)
-    {
-    case DNS_SUCCEED:
-      {
-      dns_address ** addrp = &cb->rhs;
-      dns_address * da;
-      for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
-          rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
-       if (rr->type == T_A && (da = dns_address_from_rr(dnsa, rr)))
-         {
-         *addrp = da;
-         while (da->next) da = da->next;
-         addrp = &da->next;
-         if (ttl > rr->ttl) ttl = rr->ttl;
-         }
-
-      if (cb->rhs)
-       {
-       cb->expiry = time(NULL) + ttl;
-       break;
-       }
-
-      /* If we didn't find any A records, change the return code. This can
-      happen when there is a CNAME record but there are no A records for what
-      it points to. */
-
-      cb->rc = DNS_NODATA;
-      }
-      /*FALLTHROUGH*/
-
-    case DNS_NOMATCH:
-    case DNS_NODATA:
-      {
-      /* Although there already is a neg-cache layer maintained by
-      dns_basic_lookup(), we have a dnslist cache entry allocated and
-      tree-inserted. So we may as well use it. */
-
-      time_t soa_negttl = dns_expire_from_soa(dnsa, T_A);
-      cb->expiry = soa_negttl ? soa_negttl : time(NULL) + ttl;
-      break;
-      }
-
-    default:
-      cb->expiry = time(NULL) + ttl;
-      break;
-    }
-
-  store_pool = old_pool;
-  HDEBUG(D_dnsbl) debug_printf("dnslists: wrote cache entry, ttl=%d\n",
-    (int)(cb->expiry - time(NULL)));
+  len = 5 + Ustrlen(msg) + 1 + Ustrlen(where);
+  msg = string_sprintf("%c%c%c%c%c%s%c%s", (uschar)rc,
+    (vaddr.basic_errno >> 24) & 0xff, (vaddr.basic_errno >> 16) & 0xff,
+    (vaddr.basic_errno >> 8) & 0xff, vaddr.basic_errno & 0xff,
+    where, '\0', msg);
   }
 
-/* We now have the result of the DNS lookup, either newly done, or cached
-from a previous call. If the lookup succeeded, check against the address
-list if there is one. This may be a positive equality list (introduced by
-"="), a negative equality list (introduced by "!="), a positive bitmask
-list (introduced by "&"), or a negative bitmask list (introduced by "!&").*/
-
-if (cb->rc == DNS_SUCCEED)
-  {
-  dns_address * da = NULL;
-  uschar *addlist = cb->rhs->address;
-
-  /* For A and AAAA records, there may be multiple addresses from multiple
-  records. For A6 records (currently not expected to be used) there may be
-  multiple addresses from a single record. */
-
-  for (da = cb->rhs->next; da; da = da->next)
-    addlist = string_sprintf("%s, %s", addlist, da->address);
-
-  HDEBUG(D_dnsbl) debug_printf("DNS lookup for %s succeeded (yielding %s)\n",
-    query, addlist);
-
-  /* Address list check; this can be either for equality, or via a bitmask.
-  In the latter case, all the bits must match. */
-
-  if (iplist)
-    {
-    for (da = cb->rhs; da; da = da->next)
-      {
-      int ipsep = ',';
-      uschar ip[46];
-      const uschar *ptr = iplist;
-      uschar *res;
-
-      /* Handle exact matching */
-
-      if (!bitmask)
-       {
-        while ((res = string_nextinlist(&ptr, &ipsep, ip, sizeof(ip))))
-          if (Ustrcmp(CS da->address, ip) == 0)
-           break;
-       }
-
-      /* Handle bitmask matching */
-
-      else
-        {
-        int address[4];
-        int mask = 0;
-
-        /* At present, all known DNS blocking lists use A records, with
-        IPv4 addresses on the RHS encoding the information they return. I
-        wonder if this will linger on as the last vestige of IPv4 when IPv6
-        is ubiquitous? Anyway, for now we use paranoia code to completely
-        ignore IPv6 addresses. The default mask is 0, which always matches.
-        We change this only for IPv4 addresses in the list. */
-
-        if (host_aton(da->address, address) == 1) mask = address[0];
-
-        /* Scan the returned addresses, skipping any that are IPv6 */
-
-        while ((res = string_nextinlist(&ptr, &ipsep, ip, sizeof(ip))))
-          {
-          if (host_aton(ip, address) != 1) continue;
-          if ((address[0] & mask) == address[0]) break;
-          }
-        }
-
-      /* If either
-
-         (a) An IP address in an any ('=') list matched, or
-         (b) No IP address in an all ('==') list matched
-
-      then we're done searching. */
-
-      if (((match_type & MT_ALL) != 0) == (res == NULL)) break;
-      }
-
-    /* If da == NULL, either
-
-       (a) No IP address in an any ('=') list matched, or
-       (b) An IP address in an all ('==') list didn't match
-
-    so behave as if the DNSBL lookup had not succeeded, i.e. the host is not on
-    the list. */
-
-    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 %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, 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);
+DEBUG(D_verify) debug_printf_indent("verify_quota: len %d\n", len);
+write(1, msg, len);
+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)
-    {
-    cb->text_set = TRUE;
-    if (dns_basic_lookup(dnsa, query, T_TXT) == DNS_SUCCEED)
-      for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
-           rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
-        if (rr->type == T_TXT)
-         {
-         int len = (rr->data)[0];
-         if (len > 511) len = 127;
-         store_pool = POOL_PERM;
-         cb->text = string_sprintf("%.*s", len, CUS (rr->data+1));
-         store_pool = old_pool;
-         break;
-         }
-    }
+/******************************************************************************/
 
-  dnslist_value = addlist;
-  dnslist_text = cb->text;
-  return OK;
-  }
+/* Quota cache lookup.  We use the callout hints db also for the quota cache.
+Return TRUE if a nonexpired record was found, having filled in the yield
+argument.
+*/
 
-/* There was a problem with the DNS lookup */
+static BOOL
+cached_quota_lookup(const uschar * rcpt, int * yield,
+  int pos_cache, int neg_cache)
+{
+open_db dbblock, *dbm_file = NULL;
+dbdata_callout_cache_address * cache_address_record;
 
-if (cb->rc != DNS_NOMATCH && cb->rc != DNS_NODATA)
+if (!pos_cache && !neg_cache)
+  return FALSE;
+if (!(dbm_file = dbfn_open(US"callout", O_RDWR, &dbblock, FALSE, TRUE)))
   {
-  log_write(L_dnslist_defer, LOG_MAIN,
-    "DNS list lookup defer (probably timeout) for %s: %s", query,
-    (defer_return == OK)?   US"assumed in list" :
-    (defer_return == FAIL)? US"assumed not in list" :
-                            US"returned DEFER");
-  return defer_return;
+  HDEBUG(D_verify) debug_printf_indent("quota cache: not available\n");
+  return FALSE;
   }
-
-/* No entry was found in the DNS; continue for next domain */
-
-HDEBUG(D_dnsbl)
+if (!(cache_address_record = (dbdata_callout_cache_address *)
+    get_callout_cache_record(dbm_file, rcpt, US"address",
+      pos_cache, neg_cache)))
   {
-  debug_printf("DNS lookup for %s failed\n", query);
-  debug_printf("=> that means %s is not listed at %s\n",
-     keydomain, domain);
+  dbfn_close(dbm_file);
+  return FALSE;
   }
-
-return FAIL;
+if (cache_address_record->result == ccache_accept)
+  *yield = OK;
+dbfn_close(dbm_file);
+return TRUE;
 }
 
+/* Quota cache write */
 
+static void
+cache_quota_write(const uschar * rcpt, int yield, int pos_cache, int neg_cache)
+{
+open_db dbblock, *dbm_file = NULL;
+dbdata_callout_cache_address cache_address_record;
 
+if (!pos_cache && !neg_cache)
+  return;
+if (!(dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE, TRUE)))
+  {
+  HDEBUG(D_verify) debug_printf_indent("quota cache: not available\n");
+  return;
+  }
 
-/*************************************************
-*        Check host against DNS black lists      *
-*************************************************/
-
-/* This function runs checks against a list of DNS black lists, until one
-matches. Each item on the list can be of the form
-
-  domain=ip-address/key
-
-The domain is the right-most domain that is used for the query, for example,
-blackholes.mail-abuse.org. If the IP address is present, there is a match only
-if the DNS lookup returns a matching IP address. Several addresses may be
-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:
-
-  dsn.rfc-ignorant.org/$sender_address_domain
-
-After finding a match in the DNS, the domain is placed in $dnslist_domain, and
-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.
+cache_address_record.result = yield == OK ? ccache_accept : ccache_reject;
 
-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:
+(void)dbfn_write(dbm_file, rcpt, &cache_address_record,
+       (int)sizeof(dbdata_callout_cache_address));
+HDEBUG(D_verify) debug_printf_indent("wrote %s quota cache record for %s\n",
+      yield == OK ? "positive" : "negative", rcpt);
 
-  dnslists = http.dnsbl.sorbs.net,dnsbl.sorbs.net=127.0.0.2 : \
-             socks.dnsbl.sorbs.net,dnsbl.sorbs.net=127.0.0.3
+dbfn_close(dbm_file);
+return;
+}
 
-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
+/* To evaluate a local user's quota, starting in ACL, we need to
+fork & exec to regain privileges, to that we can change to the user's
+identity for access to their files.
 
 Arguments:
-  where        the acl type
-  listptr      the domain/address/data list
-  log_msgptr   log message on error
-
-Returns:    OK      successful lookup (i.e. the address is on the list), or
-                      lookup deferred after +include_unknown
-            FAIL    name not found, or no data found for the given type, or
-                      lookup deferred after +exclude_unknown (default)
-            DEFER   lookup failure, if +defer_unknown was set
+ rcpt          Recipient account
+ pos_cache     Number of seconds to cache a positive result (delivery
+               to be accepted).  Zero to disable caching.
+ neg_cache     Number of seconds to cache a negative result.  Zero to disable.
+ msg           Pointer to result string pointer
+
+Return:                OK/DEFER/FAIL code
 */
 
 int
-verify_check_dnsbl(int where, const uschar ** listptr, uschar ** log_msgptr)
+verify_quota_call(const uschar * rcpt, int pos_cache, int neg_cache,
+  uschar ** msg)
 {
-int sep = 0;
-int defer_return = FAIL;
-const uschar *list = *listptr;
-uschar *domain;
-uschar buffer[1024];
-uschar revadd[128];        /* Long enough for IPv6 address */
-
-/* Indicate that the inverted IP address is not yet set up */
-
-revadd[0] = 0;
-
-/* In case this is the first time the DNS resolver is being used. */
+int pfd[2], pid, save_errno, yield = FAIL;
+void (*oldsignal)(int);
+const uschar * where = US"socketpair";
 
-dns_init(FALSE, FALSE, FALSE); /*XXX dnssec? */
+*msg = NULL;
 
-/* Loop through all the domains supplied, until something matches */
-
-while ((domain = string_nextinlist(&list, &sep, buffer, sizeof(buffer))))
+if (cached_quota_lookup(rcpt, &yield, pos_cache, neg_cache))
   {
-  int rc;
-  BOOL bitmask = FALSE;
-  int match_type = 0;
-  uschar *domain_txt;
-  uschar *comma;
-  uschar *iplist;
-  uschar *key;
-
-  HDEBUG(D_dnsbl) debug_printf("dnslists check: %s\n", domain);
-
-  /* Deal with special values that change the behaviour on defer */
-
-  if (domain[0] == '+')
+  HDEBUG(D_verify) debug_printf_indent("quota cache: address record is %s\n",
+    yield == OK ? "positive" : "negative");
+  if (yield != OK)
     {
-    if      (strcmpic(domain, US"+include_unknown") == 0) defer_return = OK;
-    else if (strcmpic(domain, US"+exclude_unknown") == 0) defer_return = FAIL;
-    else if (strcmpic(domain, US"+defer_unknown") == 0)   defer_return = DEFER;
-    else
-      log_write(0, LOG_MAIN|LOG_PANIC, "unknown item in dnslist (ignored): %s",
-        domain);
-    continue;
+    recipient_verify_failure = US"quota";
+    acl_verify_message = *msg =
+      US"Previous (cached) quota verification failure";
     }
+  return yield;
+  }
 
-  /* See if there's explicit data to be looked up */
-
-  if ((key = Ustrchr(domain, '/'))) *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 require all matches
-  and if preceded by ! we invert the result. */
-
-  if (!(iplist = Ustrchr(domain, '=')))
-    {
-    bitmask = TRUE;
-    iplist = Ustrchr(domain, '&');
-    }
-
-  if (iplist)                                 /* Found either = or & */
-    {
-    if (iplist > domain && iplist[-1] == '!')  /* Handle preceding ! */
-      {
-      match_type |= MT_NOT;
-      iplist[-1] = 0;
-      }
-
-    *iplist++ = 0;                             /* Terminate domain, move on */
+if (pipe(pfd) != 0)
+  goto fail;
 
-    /* If we found = (bitmask == FALSE), check for == or =& */
+where = US"fork";
+oldsignal = signal(SIGCHLD, SIG_DFL);
+if ((pid = exim_fork(US"quota-verify")) < 0)
+  {
+  save_errno = errno;
+  close(pfd[pipe_write]);
+  close(pfd[pipe_read]);
+  errno = save_errno;
+  goto fail;
+  }
 
-    if (!bitmask && (*iplist == '=' || *iplist == '&'))
-      {
-      bitmask = *iplist++ == '&';
-      match_type |= MT_ALL;
-      }
-    }
+if (pid == 0)          /* child */
+  {
+  close(pfd[pipe_read]);
+  force_fd(pfd[pipe_write], 1);                /* stdout to pipe */
+  close(pfd[pipe_write]);
+  dup2(1, 0);
+  if (debug_fd > 0) force_fd(debug_fd, 2);
+
+  child_exec_exim(CEE_EXEC_EXIT, FALSE, NULL, FALSE, 3,
+    US"-MCq", string_sprintf("%d", message_size), rcpt);
+  /*NOTREACHED*/
+  }
 
+save_errno = errno;
+close(pfd[pipe_write]);
 
-  /* If 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. */
+if (pid < 0)
+  {
+  DEBUG(D_verify) debug_printf_indent(" fork: %s\n", strerror(save_errno));
+  }
+else
+  {
+  uschar buf[128];
+  int n = read(pfd[pipe_read], buf, sizeof(buf));
+  int status;
 
-  domain_txt = domain;
-  if ((comma = Ustrchr(domain, ',')))
+  waitpid(pid, &status, 0);
+  if (status == 0)
     {
-    *comma++ = 0;
-    domain = comma;
-    }
-
-  /* Check that what we have left is a sensible domain name. There is no reason
-  why these domains should in fact use the same syntax as hosts and email
-  domains, but in practice they seem to. However, there is little point in
-  actually causing an error here, because that would no doubt hold up incoming
-  mail. Instead, I'll just log it. */
-
-  for (uschar * s = domain; *s; s++)
-    if (!isalnum(*s) && *s != '-' && *s != '.' && *s != '_')
-      {
-      log_write(0, LOG_MAIN, "dnslists domain \"%s\" contains "
-        "strange characters - is this right?", domain);
-      break;
-      }
+    uschar * s;
 
-  /* Check the alternate domain if present */
-
-  if (domain_txt != domain) for (uschar * s = domain_txt; *s; s++)
-    if (!isalnum(*s) && *s != '-' && *s != '.' && *s != '_')
+    if (n > 0) yield = buf[0];
+    if (n > 4)
+      save_errno = (buf[1] << 24) | (buf[2] << 16) | (buf[3] << 8) | buf[4];
+    if ((recipient_verify_failure = n > 5
+       ? string_copyn_taint(buf+5, n-5, GET_UNTAINTED) : NULL))
       {
-      log_write(0, LOG_MAIN, "dnslists domain \"%s\" contains "
-        "strange characters - is this right?", domain_txt);
-      break;
+      int m;
+      s = buf + 5 + Ustrlen(recipient_verify_failure) + 1;
+      m = n - (s - buf);
+      acl_verify_message = *msg =
+       m > 0 ? string_copyn_taint(s, m, GET_UNTAINTED) : NULL;
       }
 
-  /* 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. */
+    DEBUG(D_verify) debug_printf_indent("verify call response:"
+      " len %d yield %s errno '%s' where '%s' msg '%s'\n",
+      n, rc_names[yield], strerror(save_errno), recipient_verify_failure, *msg);
 
-  if (!key)
-    {
-    if (where == ACL_WHERE_NOTSMTP_START || where == ACL_WHERE_NOTSMTP)
-      {
-      *log_msgptr = string_sprintf
-       ("cannot test auto-keyed dnslists condition in %s ACL",
-         acl_wherenames[where]);
-      return ERROR;
-      }
-    if (!sender_host_address) return FAIL;    /* can never match */
-    if (revadd[0] == 0) invert_address(revadd, sender_host_address);
-    rc = one_check_dnsbl(domain, domain_txt, sender_host_address, revadd,
-      iplist, bitmask, match_type, defer_return);
-    if (rc == OK)
-      {
-      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, dnslist_domain);
-      }
-    if (rc != FAIL) return rc;     /* OK or DEFER */
+    if (  yield == OK
+       || save_errno == 0 && Ustrcmp(recipient_verify_failure, "quota") == 0)
+      cache_quota_write(rcpt, yield, pos_cache, neg_cache);
+    else DEBUG(D_verify)
+      debug_printf_indent("result not cacheable\n");
     }
-
-  /* If there is a key string, it can be a list of domains or IP addresses to
-  be concatenated with the main domain. */
-
   else
     {
-    int keysep = 0;
-    BOOL defer = FALSE;
-    uschar *keydomain;
-    uschar keyrevadd[128];
-
-    while ((keydomain = string_nextinlist(CUSS &key, &keysep, NULL, 0)))
-      {
-      uschar *prepend = keydomain;
-
-      if (string_is_ip_address(keydomain, NULL) != 0)
-        {
-        invert_address(keyrevadd, keydomain);
-        prepend = keyrevadd;
-        }
-
-      rc = one_check_dnsbl(domain, domain_txt, keydomain, prepend, iplist,
-        bitmask, match_type, defer_return);
-      if (rc == OK)
-        {
-        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, dnslist_domain);
-        return OK;
-        }
-
-      /* If the lookup deferred, remember this fact. We keep trying the rest
-      of the list to see if we get a useful result, and if we don't, we return
-      DEFER at the end. */
-
-      if (rc == DEFER) defer = TRUE;
-      }    /* continue with next keystring domain/address */
-
-    if (defer) return DEFER;
+    DEBUG(D_verify)
+      debug_printf_indent("verify call response: waitpid status 0x%04x\n", status);
     }
-  }        /* continue with next dnsdb outer domain */
+  }
 
-return FAIL;
+close(pfd[pipe_read]);
+signal(SIGCHLD, oldsignal);
+errno = save_errno;
+return yield;
+
+fail:
+DEBUG(D_verify) debug_printf_indent("verify_quota_call fail in %s\n", where);
+return yield;
 }
 
+
 /* vi: aw ai sw=2
 */
 /* End of verify.c */