commit 9f1a75f126ae217a3a3568b106c9133b3c5c413a
Merge: 57318bfd2 89318c714
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Jun 26 12:10:03 2022 +0100

    Merge branch '4.next'

diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index 78f5516a7..0c64d45d4 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -1,4 +1,5 @@
 # This file is the basis of the main makefile for Exim and friends. The
+#
 # makefile at the top level arranges to build the main makefile by calling
 # scripts/Configure-Makefile from within the build directory. This
 # concatenates the configuration settings from Local/Makefile and other,
@@ -485,9 +486,9 @@ OBJ_LOOKUPS = lookups/lf_quote.o lookups/lf_check_file.o lookups/lf_sqlperform.o
 OBJ_EXIM = acl.o base64.o child.o crypt16.o daemon.o dbfn.o debug.o deliver.o \
         directory.o dns.o drtables.o enq.o exim.o expand.o filter.o \
         filtertest.o globals.o dkim.o dkim_transport.o dnsbl.o hash.o \
-        header.o host.o ip.o log.o lss.o match.o md5.o moan.o \
+        header.o host.o host_address.o ip.o log.o lss.o match.o md5.o moan.o \
         os.o parse.o priv.o queue.o \
-        rda.o readconf.o receive.o retry.o rewrite.o rfc2047.o \
+        rda.o readconf.o receive.o retry.o rewrite.o rfc2047.o regex_cache.o \
         route.o search.o sieve.o smtp_in.o smtp_out.o spool_in.o spool_out.o \
         std-crypto.o store.o string.o tls.o tod.o transport.o tree.o verify.o \
         environment.o macro.o \
@@ -601,10 +602,11 @@ MONBIN = em_StripChart.o $(EXIMON_TEXTPOP) em_globals.o em_init.o \
 # The complete modules list also includes some specially compiled versions of
 # code from the main Exim source tree.
 
-OBJ_MONBIN = util-spool_in.o \
+OBJ_MONBIN = util-host_address.o \
+	     util-queue.o \
+	     util-spool_in.o \
 	     util-store.o \
 	     util-string.o \
-	     util-queue.o \
 	     util-tod.o \
 	     util-tree.o \
 	     $(MONBIN)
@@ -721,6 +723,14 @@ exim_dbmbuild.o: $(HDRS) exim_dbmbuild.c
 # Utilities use special versions of some modules - typically with debugging
 # calls cut out.
 
+util-host_address.o:   $(HDRS) host_address.c
+	@echo "$(CC) -DCOMPILE_UTILITY host_address.c"
+	$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) -DCOMPILE_UTILITY -o util-host_address.o host_address.c
+
+util-md5.o:   $(HDRS) md5.c
+	@echo "$(CC) -DCOMPILE_UTILITY queue.c"
+	$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) -DCOMPILE_UTILITY -o util-md5.o md5.c
+
 util-spool_in.o: $(HDRS) spool_in.c
 	@echo "$(CC) -DCOMPILE_UTILITY spool_in.c"
 	$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) -DCOMPILE_UTILITY -o util-spool_in.o spool_in.c
@@ -733,10 +743,6 @@ util-string.o:   $(HDRS) string.c
 	@echo "$(CC) -DCOMPILE_UTILITY string.c"
 	$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) -DCOMPILE_UTILITY -o util-string.o string.c
 
-util-md5.o:   $(HDRS) md5.c
-	@echo "$(CC) -DCOMPILE_UTILITY queue.c"
-	$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) -DCOMPILE_UTILITY -o util-md5.o md5.c
-
 util-queue.o:   $(HDRS) queue.c
 	@echo "$(CC) -DCOMPILE_UTILITY queue.c"
 	$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) -DCOMPILE_UTILITY -o util-queue.o queue.c
@@ -787,6 +793,7 @@ globals.o:       $(HDRS) globals.c
 hash.o:          $(HDRS) hash.c
 header.o:        $(HDRS) header.c
 host.o:          $(HDRS) host.c
+host_address.o:  $(HDRS) host_address.c
 ip.o:            $(HDRS) ip.c
 log.o:           $(HDRS) log.c
 lss.o:           $(HDRS) lss.c
@@ -802,6 +809,7 @@ readconf.o:      $(HDRS) readconf.c
 receive.o:       $(HDRS) receive.c
 retry.o:         $(HDRS) retry.c
 rewrite.o:       $(HDRS) rewrite.c
+regex_cache.o:   $(HDRS) regex_cache.c
 rfc2047.o:       $(HDRS) rfc2047.c
 route.o:         $(HDRS) route.c
 search.o:        $(HDRS) search.c
@@ -962,13 +970,13 @@ test_dbfn:   config.h dbfn.c dummies.o sa-globals.o sa-os.o store.o \
 	rm -f dbfn.o store.o
 
 test_host:   config.h child.c host.c dns.c dummies.c sa-globals.o os.o \
-	       store.o string.o tod.o tree.o
+	       host_address.o store.o string.o tod.o tree.o
 	$(CC) -c $(CFLAGS) $(INCLUDE) -DSTAND_ALONE -DTEST_HOST child.c
 	$(CC) -c $(CFLAGS) $(INCLUDE) -DSTAND_ALONE -DTEST_HOST host.c
 	$(CC) -c $(CFLAGS) $(INCLUDE) -DSTAND_ALONE -DTEST_HOST dns.c
 	$(CC) -c $(CFLAGS) $(INCLUDE) -DSTAND_ALONE -DTEST_HOST dummies.c
 	$(LNCC) -o test_host $(LFLAGS) \
-	  host.o child.o dns.o dummies.o sa-globals.o os.o store.o string.o \
+	  host.o host_address.o child.o dns.o dummies.o sa-globals.o os.o store.o string.o \
 	  tod.o tree.o $(LIBS) $(LIBRESOLV)
 	rm -f child.o dummies.o host.o dns.o
 
diff --git a/src/OS/Makefile-FreeBSD b/src/OS/Makefile-FreeBSD
index 02253d9c6..4793a438b 100644
--- a/src/OS/Makefile-FreeBSD
+++ b/src/OS/Makefile-FreeBSD
@@ -13,7 +13,7 @@ PERL_COMMAND=/usr/local/bin/perl
 HAVE_SA_LEN=YES
 
 # crypt() is in a separate library
-LIBS=-lcrypt -lm -lutil
+LIBS=-lcrypt -lm -lutil -lexecinfo
 
 # Dynamically loaded modules need to be built with -fPIC
 CFLAGS_DYNAMIC=-shared -rdynamic -fPIC
diff --git a/src/OS/Makefile-OpenBSD b/src/OS/Makefile-OpenBSD
index 697632682..7c451e2cc 100644
--- a/src/OS/Makefile-OpenBSD
+++ b/src/OS/Makefile-OpenBSD
@@ -23,6 +23,7 @@ EXIWHAT_EGREP_ARG='/exim( |$$)'
 EXIWHAT_KILL_SIGNAL=-USR1
 
 HAVE_IPV6=YES
+CFLAGS += -DNO_EXECINFO
 
 # OpenBSD ships with a too-old Berkeley DB.  NDBM is the default if we don't specify one.
 #USE_DB=yes
diff --git a/src/OS/Makefile-SunOS5 b/src/OS/Makefile-SunOS5
index 3b436f4bf..d07adcff4 100644
--- a/src/OS/Makefile-SunOS5
+++ b/src/OS/Makefile-SunOS5
@@ -20,5 +20,6 @@ X11LIB=$(X11)/lib
 
 OS_C_INCLUDES=setenv.c
 CFLAGS += -D_XOPEN_SOURCE -D_XOPEN_SOURCE_EXTENDED=1 -D__EXTENSIONS__
+CFLAGS += -DNO_EXECINFO
 
 # End
diff --git a/src/exim_monitor/em_main.c b/src/exim_monitor/em_main.c
index 5714b999c..86caf71eb 100644
--- a/src/exim_monitor/em_main.c
+++ b/src/exim_monitor/em_main.c
@@ -179,70 +179,6 @@ va_end(ap);
 
 
 
-/*************************************************
-*        Extract port from address string        *
-*************************************************/
-
-/* In the spool file, a host plus port is given as an IP address followed by a
-dot and a port number. This function decodes this. It is needed by the
-spool-reading function, and copied here to avoid having to include the whole
-host.c module. One day the interaction between exim and eximon with regard to
-included code MUST be tidied up!
-
-Argument:
-  address    points to the string; if there is a port, the '.' in the string
-             is overwritten with zero to terminate the address
-
-Returns:     0 if there is no port, else the port number.
-*/
-
-int
-host_address_extract_port(uschar * address)
-{
-int port = 0;
-uschar *endptr;
-
-/* Handle the "bracketed with colon on the end" format */
-
-if (*address == '[')
-  {
-  uschar *rb = address + 1;
-  while (*rb != 0 && *rb != ']') rb++;
-  if (*rb++ == 0) return 0;		/* Missing ]; leave invalid address */
-  if (*rb == ':')
-    {
-    port = Ustrtol(rb + 1, &endptr, 10);
-    if (*endptr != 0) return 0;		/* Invalid port; leave invalid address */
-    }
-  else if (*rb != 0) return 0;		/* Bad syntax; leave invalid address */
-  memmove(address, address + 1, rb - address - 2);
-  rb[-2] = 0;
-  }
-
-/* Handle the "dot on the end" format */
-
-else
-  {
-  int skip = -3;			/* Skip 3 dots in IPv4 addresses */
-  address--;
-  while (*(++address) != 0)
-    {
-    int ch = *address;
-    if (ch == ':') skip = 0;		/* Skip 0 dots in IPv6 addresses */
-      else if (ch == '.' && skip++ >= 0) break;
-    }
-  if (*address == 0) return 0;
-  port = Ustrtol(address + 1, &endptr, 10);
-  if (*endptr != 0) return 0;		/* Invalid port; leave invalid address */
-  *address = 0;
-  }
-
-return port;
-}
-
-
-
-
 /*************************************************
 *                SIGCHLD handler                 *
 *************************************************/
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index 9cdb931f3..471b3a369 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -102,9 +102,9 @@ for f in blob.h dbfunctions.h exim.h functions.h globals.h \
   acl.c buildconfig.c base64.c child.c crypt16.c daemon.c dbfn.c debug.c \
   deliver.c directory.c dns.c dnsbl.c drtables.c dummies.c enq.c exim.c \
   exim_dbmbuild.c exim_dbutil.c exim_lock.c expand.c filter.c filtertest.c \
-  globals.c hash.c header.c host.c ip.c log.c lss.c match.c md5.c moan.c \
+  globals.c hash.c header.c host.c host_address.c ip.c log.c lss.c match.c md5.c moan.c \
   parse.c perl.c priv.c queue.c rda.c readconf.c receive.c retry.c rewrite.c \
-  rfc2047.c route.c search.c setenv.c environment.c \
+  regex_cache.c rfc2047.c route.c search.c setenv.c environment.c \
   sieve.c smtp_in.c smtp_out.c spool_in.c spool_out.c std-crypto.c store.c \
   string.c tls.c tlscert-gnu.c tlscert-openssl.c tls-cipher-stdname.c \
   tls-gnu.c tls-openssl.c \
diff --git a/src/src/acl.c b/src/src/acl.c
index fb78a7b5f..0078aca7d 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -3125,8 +3125,9 @@ int sep = -'/';
 
 for (; cb; cb = cb->next)
   {
-  const uschar *arg;
+  const uschar * arg;
   int control_type;
+  BOOL textonly = FALSE;
 
   /* The message and log_message items set up messages to be used in
   case of rejection. They are expanded later. */
@@ -3160,7 +3161,8 @@ for (; cb; cb = cb->next)
 
   if (!conditions[cb->type].expand_at_top)
     arg = cb->arg;
-  else if (!(arg = expand_string(cb->arg)))
+
+  else if (!(arg = expand_string_2(cb->arg, &textonly)))
     {
     if (f.expand_string_forcedfail) continue;
     *log_msgptr = string_sprintf("failed to expand ACL string \"%s\": %s",
@@ -3217,8 +3219,8 @@ for (; cb; cb = cb->next)
   switch(cb->type)
     {
     case ACLC_ADD_HEADER:
-    setup_header(arg);
-    break;
+      setup_header(arg);
+      break;
 
     /* A nested ACL that returns "discard" makes sense only for an "accept" or
     "discard" verb. */
@@ -3232,12 +3234,12 @@ for (; cb; cb = cb->next)
           verbs[verb]);
         return ERROR;
         }
-    break;
+      break;
 
     case ACLC_AUTHENTICATED:
       rc = sender_host_authenticated ? match_isinlist(sender_host_authenticated,
 	      &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL) : FAIL;
-    break;
+      break;
 
     #ifdef EXPERIMENTAL_BRIGHTMAIL
     case ACLC_BMI_OPTIN:
@@ -3254,21 +3256,21 @@ for (; cb; cb = cb->next)
     /* The true/false parsing here should be kept in sync with that used in
     expand.c when dealing with ECOND_BOOL so that we don't have too many
     different definitions of what can be a boolean. */
-    if (*arg == '-'
-	? Ustrspn(arg+1, "0123456789") == Ustrlen(arg+1)    /* Negative number */
-	: Ustrspn(arg,   "0123456789") == Ustrlen(arg))     /* Digits, or empty */
-      rc = (Uatoi(arg) == 0)? FAIL : OK;
-    else
-      rc = (strcmpic(arg, US"no") == 0 ||
-            strcmpic(arg, US"false") == 0)? FAIL :
-           (strcmpic(arg, US"yes") == 0 ||
-            strcmpic(arg, US"true") == 0)? OK : DEFER;
-    if (rc == DEFER)
-      *log_msgptr = string_sprintf("invalid \"condition\" value \"%s\"", arg);
-    break;
+      if (*arg == '-'
+	  ? Ustrspn(arg+1, "0123456789") == Ustrlen(arg+1)    /* Negative number */
+	  : Ustrspn(arg,   "0123456789") == Ustrlen(arg))     /* Digits, or empty */
+	rc = (Uatoi(arg) == 0)? FAIL : OK;
+      else
+	rc = (strcmpic(arg, US"no") == 0 ||
+	      strcmpic(arg, US"false") == 0)? FAIL :
+	     (strcmpic(arg, US"yes") == 0 ||
+	      strcmpic(arg, US"true") == 0)? OK : DEFER;
+      if (rc == DEFER)
+	*log_msgptr = string_sprintf("invalid \"condition\" value \"%s\"", arg);
+      break;
 
     case ACLC_CONTINUE:    /* Always succeeds */
-    break;
+      break;
 
     case ACLC_CONTROL:
       {
@@ -3647,14 +3649,14 @@ for (; cb; cb = cb->next)
       while ((ss = string_nextinlist(&list, &sep, NULL, 0)))
         if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER)
           rc = FAIL;   /* FAIL so that the message is passed to the next ACL */
+      break;
       }
-    break;
     #endif
 
     #ifdef WITH_CONTENT_SCAN
     case ACLC_DECODE:
-    rc = mime_decode(&arg);
-    break;
+      rc = mime_decode(&arg);
+      break;
     #endif
 
     case ACLC_DELAY:
@@ -3719,44 +3721,44 @@ for (; cb; cb = cb->next)
 #endif
           }
         }
+      break;
       }
-    break;
 
 #ifndef DISABLE_DKIM
     case ACLC_DKIM_SIGNER:
-    if (dkim_cur_signer)
-      rc = match_isinlist(dkim_cur_signer,
+      if (dkim_cur_signer)
+	rc = match_isinlist(dkim_cur_signer,
                           &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
-    else
-      rc = FAIL;
-    break;
+      else
+	rc = FAIL;
+      break;
 
     case ACLC_DKIM_STATUS:
-    rc = match_isinlist(dkim_verify_status,
-                        &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
-    break;
+      rc = match_isinlist(dkim_verify_status,
+			  &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
+      break;
 #endif
 
 #ifdef SUPPORT_DMARC
     case ACLC_DMARC_STATUS:
-    if (!f.dmarc_has_been_checked)
-      dmarc_process();
-    f.dmarc_has_been_checked = TRUE;
-    /* used long way of dmarc_exim_expand_query() in case we need more
-     * view into the process in the future. */
-    rc = match_isinlist(dmarc_exim_expand_query(DMARC_VERIFY_STATUS),
-                        &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
-    break;
+      if (!f.dmarc_has_been_checked)
+	dmarc_process();
+      f.dmarc_has_been_checked = TRUE;
+      /* used long way of dmarc_exim_expand_query() in case we need more
+       * view into the process in the future. */
+      rc = match_isinlist(dmarc_exim_expand_query(DMARC_VERIFY_STATUS),
+			  &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
+      break;
 #endif
 
     case ACLC_DNSLISTS:
-    rc = verify_check_dnsbl(where, &arg, log_msgptr);
-    break;
+      rc = verify_check_dnsbl(where, &arg, log_msgptr);
+      break;
 
     case ACLC_DOMAINS:
-    rc = match_isinlist(addr->domain, &arg, 0, &domainlist_anchor,
-      addr->domain_cache, MCL_DOMAIN, TRUE, CUSS &deliver_domain_data);
-    break;
+      rc = match_isinlist(addr->domain, &arg, 0, &domainlist_anchor,
+	addr->domain_cache, MCL_DOMAIN, TRUE, CUSS &deliver_domain_data);
+      break;
 
     /* The value in tls_cipher is the full cipher name, for example,
     TLSv1:DES-CBC3-SHA:168, whereas the values to test for are just the
@@ -3765,19 +3767,20 @@ for (; cb; cb = cb->next)
     writing is poorly documented. */
 
     case ACLC_ENCRYPTED:
-    if (tls_in.cipher == NULL) rc = FAIL; else
-      {
-      uschar *endcipher = NULL;
-      uschar *cipher = Ustrchr(tls_in.cipher, ':');
-      if (!cipher) cipher = tls_in.cipher; else
-        {
-        endcipher = Ustrchr(++cipher, ':');
-        if (endcipher) *endcipher = 0;
-        }
-      rc = match_isinlist(cipher, &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
-      if (endcipher) *endcipher = ':';
-      }
-    break;
+      if (!tls_in.cipher) rc = FAIL;
+      else
+	{
+	uschar *endcipher = NULL;
+	uschar *cipher = Ustrchr(tls_in.cipher, ':');
+	if (!cipher) cipher = tls_in.cipher; else
+	  {
+	  endcipher = Ustrchr(++cipher, ':');
+	  if (endcipher) *endcipher = 0;
+	  }
+	rc = match_isinlist(cipher, &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
+	if (endcipher) *endcipher = ':';
+	}
+      break;
 
     /* Use verify_check_this_host() instead of verify_check_host() so that
     we can pass over &host_data to catch any looked up data. Once it has been
@@ -3787,17 +3790,17 @@ for (; cb; cb = cb->next)
     message in the same SMTP connection. */
 
     case ACLC_HOSTS:
-    rc = verify_check_this_host(&arg, sender_host_cache, NULL,
-      sender_host_address ? sender_host_address : US"", CUSS &host_data);
-    if (rc == DEFER) *log_msgptr = search_error_message;
-    if (host_data) host_data = string_copy_perm(host_data, TRUE);
-    break;
+      rc = verify_check_this_host(&arg, sender_host_cache, NULL,
+	sender_host_address ? sender_host_address : US"", CUSS &host_data);
+      if (rc == DEFER) *log_msgptr = search_error_message;
+      if (host_data) host_data = string_copy_perm(host_data, TRUE);
+      break;
 
     case ACLC_LOCAL_PARTS:
-    rc = match_isinlist(addr->cc_local_part, &arg, 0,
-      &localpartlist_anchor, addr->localpart_cache, MCL_LOCALPART, TRUE,
-      CUSS &deliver_localpart_data);
-    break;
+      rc = match_isinlist(addr->cc_local_part, &arg, 0,
+	&localpartlist_anchor, addr->localpart_cache, MCL_LOCALPART, TRUE,
+	CUSS &deliver_localpart_data);
+      break;
 
     case ACLC_LOG_REJECT_TARGET:
       {
@@ -3818,8 +3821,8 @@ for (; cb; cb = cb->next)
           }
         }
       log_reject_target = logbits;
+      break;
       }
-    break;
 
     case ACLC_LOGWRITE:
       {
@@ -3850,8 +3853,8 @@ for (; cb; cb = cb->next)
 
       if (logbits == 0) logbits = LOG_MAIN;
       log_write(0, logbits, "%s", string_printing(s));
+      break;
       }
-    break;
 
     #ifdef WITH_CONTENT_SCAN
     case ACLC_MALWARE:			/* Run the malware backend. */
@@ -3874,55 +3877,55 @@ for (; cb; cb = cb->next)
 	  return ERROR;
 	  }
 
-      rc = malware(ss, timeout);
+      rc = malware(ss, textonly, timeout);
       if (rc == DEFER && defer_ok)
 	rc = FAIL;	/* FAIL so that the message is passed to the next ACL */
+      break;
       }
-    break;
 
     case ACLC_MIME_REGEX:
-    rc = mime_regex(&arg);
-    break;
+      rc = mime_regex(&arg, textonly);
+      break;
     #endif
 
     case ACLC_QUEUE:
-    if (is_tainted(arg))
-      {
-      *log_msgptr = string_sprintf("Tainted name '%s' for queue not permitted",
-				    arg);
-      return ERROR;
-      }
-    if (Ustrchr(arg, '/'))
-      {
-      *log_msgptr = string_sprintf(
-	      "Directory separator not permitted in queue name: '%s'", arg);
-      return ERROR;
-      }
-    queue_name = string_copy_perm(arg, FALSE);
-    break;
+      if (is_tainted(arg))
+	{
+	*log_msgptr = string_sprintf("Tainted name '%s' for queue not permitted",
+				      arg);
+	return ERROR;
+	}
+      if (Ustrchr(arg, '/'))
+	{
+	*log_msgptr = string_sprintf(
+		"Directory separator not permitted in queue name: '%s'", arg);
+	return ERROR;
+	}
+      queue_name = string_copy_perm(arg, FALSE);
+      break;
 
     case ACLC_RATELIMIT:
-    rc = acl_ratelimit(arg, where, log_msgptr);
-    break;
+      rc = acl_ratelimit(arg, where, log_msgptr);
+      break;
 
     case ACLC_RECIPIENTS:
-    rc = match_address_list(CUS addr->address, TRUE, TRUE, &arg, NULL, -1, 0,
-      CUSS &recipient_data);
-    break;
+      rc = match_address_list(CUS addr->address, TRUE, TRUE, &arg, NULL, -1, 0,
+	CUSS &recipient_data);
+      break;
 
     #ifdef WITH_CONTENT_SCAN
     case ACLC_REGEX:
-    rc = regex(&arg);
-    break;
+      rc = regex(&arg, textonly);
+      break;
     #endif
 
     case ACLC_REMOVE_HEADER:
-    setup_remove_header(arg);
-    break;
+      setup_remove_header(arg);
+      break;
 
     case ACLC_SEEN:
-    rc = acl_seen(arg, where, log_msgptr);
-    break;
+      rc = acl_seen(arg, where, log_msgptr);
+      break;
 
     case ACLC_SENDER_DOMAINS:
       {
@@ -3931,13 +3934,13 @@ for (; cb; cb = cb->next)
       sdomain = sdomain ? sdomain + 1 : US"";
       rc = match_isinlist(sdomain, &arg, 0, &domainlist_anchor,
         sender_domain_cache, MCL_DOMAIN, TRUE, NULL);
+      break;
       }
-    break;
 
     case ACLC_SENDERS:
-    rc = match_address_list(CUS sender_address, TRUE, TRUE, &arg,
-      sender_address_cache, -1, 0, CUSS &sender_data);
-    break;
+      rc = match_address_list(CUS sender_address, TRUE, TRUE, &arg,
+	sender_address_cache, -1, 0, CUSS &sender_data);
+      break;
 
     /* Connection variables must persist forever; message variables not */
 
@@ -3959,8 +3962,8 @@ for (; cb; cb = cb->next)
 #endif
 	acl_var_create(cb->u.varname)->data.ptr = string_copy(arg);
       store_pool = old_pool;
+      break;
       }
-    break;
 
 #ifdef WITH_CONTENT_SCAN
     case ACLC_SPAM:
@@ -3974,22 +3977,23 @@ for (; cb; cb = cb->next)
       while ((ss = string_nextinlist(&list, &sep, NULL, 0)))
         if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER)
           rc = FAIL;	/* FAIL so that the message is passed to the next ACL */
+      break;
       }
-    break;
 #endif
 
 #ifdef SUPPORT_SPF
     case ACLC_SPF:
       rc = spf_process(&arg, sender_address, SPF_PROCESS_NORMAL);
-    break;
+      break;
+
     case ACLC_SPF_GUESS:
       rc = spf_process(&arg, sender_address, SPF_PROCESS_GUESS);
-    break;
+      break;
 #endif
 
     case ACLC_UDPSEND:
-    rc = acl_udpsend(arg, log_msgptr);
-    break;
+      rc = acl_udpsend(arg, log_msgptr);
+      break;
 
     /* If the verb is WARN, discard any user message from verification, because
     such messages are SMTP responses, not header additions. The latter come
@@ -3998,16 +4002,16 @@ for (; cb; cb = cb->next)
     (until something changes it). */
 
     case ACLC_VERIFY:
-    rc = acl_verify(where, addr, arg, user_msgptr, log_msgptr, basic_errno);
-    if (*user_msgptr)
-      acl_verify_message = *user_msgptr;
-    if (verb == ACL_WARN) *user_msgptr = NULL;
-    break;
+      rc = acl_verify(where, addr, arg, user_msgptr, log_msgptr, basic_errno);
+      if (*user_msgptr)
+	acl_verify_message = *user_msgptr;
+      if (verb == ACL_WARN) *user_msgptr = NULL;
+      break;
 
     default:
-    log_write(0, LOG_MAIN|LOG_PANIC_DIE, "internal ACL error: unknown "
-      "condition %d", cb->type);
-    break;
+      log_write(0, LOG_MAIN|LOG_PANIC_DIE, "internal ACL error: unknown "
+	"condition %d", cb->type);
+      break;
     }
 
   /* If a condition was negated, invert OK/FAIL. */
diff --git a/src/src/daemon.c b/src/src/daemon.c
index 8e8a515e4..54725e07d 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -1132,13 +1132,43 @@ exim_exit(EXIT_SUCCESS);
 *	Listener socket for local work prompts	 *
 *************************************************/
 
+ssize_t
+daemon_client_sockname(struct sockaddr_un * sup, uschar ** sname)
+{
+#ifdef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
+sup->sun_path[0] = 0;  /* Abstract local socket addr - Linux-specific? */
+return offsetof(struct sockaddr_un, sun_path) + 1
+  + snprintf(sup->sun_path+1, sizeof(sup->sun_path)-1, "exim_%d", getpid());
+#else
+*sname = string_sprintf("%s/p_%d", spool_directory, getpid());
+return offsetof(struct sockaddr_un, sun_path)
+  + snprintf(sup->sun_path, sizeof(sup->sun_path), "%s", sname);
+#endif
+}
+
+ssize_t
+daemon_notifier_sockname(struct sockaddr_un * sup)
+{
+#ifdef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
+sup->sun_path[0] = 0;  /* Abstract local socket addr - Linux-specific? */
+return offsetof(struct sockaddr_un, sun_path) + 1
+  + snprintf(sup->sun_path+1, sizeof(sup->sun_path)-1, "%s",
+              expand_string(notifier_socket));
+#else
+return offsetof(struct sockaddr_un, sun_path)
+  + snprintf(sup->sun_path, sizeof(sup->sun_path), "%s",
+              expand_string(notifier_socket));
+#endif
+}
+
+
 static void
 daemon_notifier_socket(void)
 {
 int fd;
 const uschar * where;
 struct sockaddr_un sa_un = {.sun_family = AF_UNIX};
-int len;
+ssize_t len;
 
 if (!notifier_socket || !*notifier_socket)
   {
@@ -1163,20 +1193,15 @@ if ((fd = socket(PF_UNIX, SOCK_DGRAM, 0)) < 0)
 (void)fcntl(fd, F_SETFD, fcntl(fd, F_GETFD) | FD_CLOEXEC);
 #endif
 
+len = daemon_notifier_sockname(&sa_un);
+
 #ifdef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
-sa_un.sun_path[0] = 0;	/* Abstract local socket addr - Linux-specific? */
-len = offsetof(struct sockaddr_un, sun_path) + 1
-  + snprintf(sa_un.sun_path+1, sizeof(sa_un.sun_path)-1, "%s",
-	      expand_string(notifier_socket));
 DEBUG(D_any) debug_printf(" @%s\n", sa_un.sun_path+1);
 #else			/* filesystem-visible and persistent; will neeed removal */
-len = offsetof(struct sockaddr_un, sun_path)
-  + snprintf(sa_un.sun_path, sizeof(sa_un.sun_path), "%s",
-	      expand_string(notifier_socket));
 DEBUG(D_any) debug_printf(" %s\n", sa_un.sun_path);
 #endif
 
-if (bind(fd, (const struct sockaddr *)&sa_un, len) < 0)
+if (bind(fd, (const struct sockaddr *)&sa_un, (socklen_t)len) < 0)
   { where = US"bind"; goto bad; }
 
 #ifdef SO_PASSCRED		/* Linux */
@@ -1205,7 +1230,11 @@ bad:
 
 static uschar queuerun_msgid[MESSAGE_ID_LENGTH+1];
 
-/* Return TRUE if a sigalrm should be emulated */
+/* The notifier socket has something to read. Pull the message from it, decode
+and do the action.
+
+Return TRUE if a sigalrm should be emulated */
+
 static BOOL
 daemon_notification(void)
 {
@@ -1255,7 +1284,6 @@ for (struct cmsghdr * cp = CMSG_FIRSTHDR(&msg);
     {
     DEBUG(D_queue_run) debug_printf("%s: sender creds pid %d uid %d gid %d\n",
       __FUNCTION__, (int)cr->pid, (int)cr->uid, (int)cr->gid);
-    return FALSE;
     }
 # elif defined(LOCAL_CREDS)				/* BSD-ish */
   struct sockcred * cr = (struct sockcred *) CMSG_DATA(cp);
@@ -1263,7 +1291,6 @@ for (struct cmsghdr * cp = CMSG_FIRSTHDR(&msg);
     {
     DEBUG(D_queue_run) debug_printf("%s: sender creds pid ??? uid %d gid %d\n",
       __FUNCTION__, (int)cr->sc_uid, (int)cr->sc_gid);
-    return FALSE;
     }
 # endif
   break;
@@ -1294,15 +1321,18 @@ switch (buf[0])
 		(const struct sockaddr *)&sa_un, msg.msg_namelen) < 0)
       log_write(0, LOG_MAIN|LOG_PANIC,
 	"%s: sendto: %s\n", __FUNCTION__, strerror(errno));
-    return FALSE;
+    break;
     }
+
+  case NOTIFY_REGEX:
+    regex_at_daemon(buf);
+    break;
   }
 return FALSE;
 }
 
 
 
-
 /*************************************************
 *              Exim Daemon Mainline              *
 *************************************************/
diff --git a/src/src/deliver.c b/src/src/deliver.c
index 8a9a174e3..725d0c872 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -7203,7 +7203,7 @@ local and remote LMTP deliveries. */
 
 if (!regex_IGNOREQUOTA)
   regex_IGNOREQUOTA =
-    regex_must_compile(US"\\n250[\\s\\-]IGNOREQUOTA(\\s|\\n|$)", FALSE, TRUE);
+    regex_must_compile(US"\\n250[\\s\\-]IGNOREQUOTA(\\s|\\n|$)", MCS_NOFLAGS, TRUE);
 
 /* Handle local deliveries */
 
diff --git a/src/src/dns.c b/src/src/dns.c
index 7d7ee0c04..4071c5822 100644
--- a/src/src/dns.c
+++ b/src/src/dns.c
@@ -1324,7 +1324,7 @@ dns_pattern_init(void)
 {
 if (check_dns_names_pattern[0] != 0 && !regex_check_dns_names)
   regex_check_dns_names =
-    regex_must_compile(check_dns_names_pattern, FALSE, TRUE);
+    regex_must_compile(check_dns_names_pattern, MCS_NOFLAGS, TRUE);
 }
 
 /* vi: aw ai sw=2
diff --git a/src/src/drtables.c b/src/src/drtables.c
index 513ef6c4a..b2f2a4b33 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -728,8 +728,8 @@ if (!(dd = exim_opendir(LOOKUP_MODULE_DIR)))
   }
 else
   {
-  const pcre2_code *regex_islookupmod = regex_must_compile(
-    US"\\." DYNLIB_FN_EXT "$", FALSE, TRUE);
+  const pcre2_code * regex_islookupmod = regex_must_compile(
+    US"\\." DYNLIB_FN_EXT "$", MCS_NOFLAGS, TRUE);
 
   DEBUG(D_lookup) debug_printf("Loading lookup modules from %s\n", LOOKUP_MODULE_DIR);
   while ((ent = readdir(dd)))
diff --git a/src/src/exim.c b/src/src/exim.c
index fd01d1355..dec8de4b4 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -17,6 +17,13 @@ Also a few functions that don't naturally fit elsewhere. */
 # include <gnu/libc-version.h>
 #endif
 
+#ifndef _TIME_H
+# include <time.h>
+#endif
+#ifndef NO_EXECINFO
+# include <execinfo.h>
+#endif
+
 #ifdef USE_GNUTLS
 # include <gnutls/gnutls.h>
 # if GNUTLS_VERSION_NUMBER < 0x030103 && !defined(DISABLE_OCSP)
@@ -24,10 +31,6 @@ Also a few functions that don't naturally fit elsewhere. */
 # endif
 #endif
 
-#ifndef _TIME_H
-# include <time.h>
-#endif
-
 extern void init_lookup_list(void);
 
 
@@ -56,78 +59,40 @@ if (block) store_free(block);
 }
 
 
+static void *
+function_store_get(PCRE2_SIZE size, void * tag)
+{
+return store_get((int)size, GET_UNTAINTED);	/* loses track of taint */
+}
 
-
-/*************************************************
-*         Enums for cmdline interface            *
-*************************************************/
-
-enum commandline_info { CMDINFO_NONE=0,
-  CMDINFO_HELP, CMDINFO_SIEVE, CMDINFO_DSCP };
+static void
+function_store_nullfree(void * block, void * tag)
+{
+}
 
 
 
 
 /*************************************************
-*  Compile regular expression and panic on fail  *
+*         Enums for cmdline interface            *
 *************************************************/
 
-/* This function is called when failure to compile a regular expression leads
-to a panic exit. In other cases, pcre_compile() is called directly. In many
-cases where this function is used, the results of the compilation are to be
-placed in long-lived store, so we temporarily reset the store management
-functions that PCRE uses if the use_malloc flag is set.
-
-Argument:
-  pattern     the pattern to compile
-  caseless    TRUE if caseless matching is required
-  use_malloc  TRUE if compile into malloc store
-
-Returns:      pointer to the compiled pattern
-*/
-
-const pcre2_code *
-regex_must_compile(const uschar * pattern, BOOL caseless, BOOL use_malloc)
-{
-size_t offset;
-int options = caseless ? PCRE_COPT|PCRE2_CASELESS : PCRE_COPT;
-const pcre2_code * yield;
-int err;
-pcre2_general_context * gctx;
-pcre2_compile_context * cctx;
-
-if (use_malloc)
-  {
-  gctx = pcre2_general_context_create(function_store_malloc, function_store_free, NULL);
-  cctx = pcre2_compile_context_create(gctx);
-  }
-else
-  cctx = pcre_cmp_ctx;
+enum commandline_info { CMDINFO_NONE=0,
+  CMDINFO_HELP, CMDINFO_SIEVE, CMDINFO_DSCP };
 
-if (!(yield = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED, options,
-  &err, &offset, cctx)))
-  {
-  uschar errbuf[128];
-  pcre2_get_error_message(err, errbuf, sizeof(errbuf));
-  log_write(0, LOG_MAIN|LOG_PANIC_DIE, "regular expression error: "
-    "%s at offset %ld while compiling %s", errbuf, (long)offset, pattern);
-  }
 
-if (use_malloc)
-  {
-  pcre2_compile_context_free(cctx);
-  pcre2_general_context_free(gctx);
-  }
-return yield;
-}
 
 
 static void
 pcre_init(void)
 {
-pcre_gen_ctx = pcre2_general_context_create(function_store_malloc, function_store_free, NULL);
-pcre_cmp_ctx = pcre2_compile_context_create(pcre_gen_ctx);
-pcre_mtc_ctx = pcre2_match_context_create(pcre_gen_ctx);
+pcre_mlc_ctx = pcre2_general_context_create(function_store_malloc, function_store_free, NULL);
+pcre_gen_ctx = pcre2_general_context_create(function_store_get, function_store_nullfree, NULL);
+
+pcre_mlc_cmp_ctx = pcre2_compile_context_create(pcre_mlc_ctx);
+pcre_gen_cmp_ctx = pcre2_compile_context_create(pcre_gen_ctx);
+
+pcre_gen_mtc_ctx = pcre2_match_context_create(pcre_gen_ctx);
 }
 
 
@@ -157,7 +122,7 @@ regex_match_and_setup(const pcre2_code * re, const uschar * subject, int options
 {
 pcre2_match_data * md = pcre2_match_data_create_from_pattern(re, pcre_gen_ctx);
 int res = pcre2_match(re, (PCRE2_SPTR)subject, PCRE2_ZERO_TERMINATED, 0,
-			PCRE_EOPT | options, md, pcre_mtc_ctx);
+			PCRE_EOPT | options, md, pcre_gen_mtc_ctx);
 BOOL yield;
 
 if ((yield = (res >= 0)))
@@ -179,7 +144,7 @@ else if (res != PCRE2_ERROR_NOMATCH) DEBUG(D_any)
   pcre2_get_error_message(res, errbuf, sizeof(errbuf));
   debug_printf_indent("pcre2: %s\n", errbuf);
   }
-pcre2_match_data_free(md);
+/* pcre2_match_data_free(md);	gen ctx needs no free */
 return yield;
 }
 
@@ -201,13 +166,18 @@ regex_match(const pcre2_code * re, const uschar * subject, int slen, uschar ** r
 pcre2_match_data * md = pcre2_match_data_create(1, pcre_gen_ctx);
 int rc = pcre2_match(re, (PCRE2_SPTR)subject,
 		      slen >= 0 ? slen : PCRE2_ZERO_TERMINATED,
-		      0, PCRE_EOPT, md, pcre_mtc_ctx);
+		      0, PCRE_EOPT, md, pcre_gen_mtc_ctx);
 PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
-if (rc < 0)
-  return FALSE;
-if (rptr)
-  *rptr = string_copyn(subject + ovec[0], ovec[1] - ovec[0]);
-return TRUE;
+BOOL ret = FALSE;
+
+if (rc >= 0)
+  {
+  if (rptr)
+    *rptr = string_copyn(subject + ovec[0], ovec[1] - ovec[0]);
+  ret = TRUE;
+  }
+/* pcre2_match_data_free(md);	gen ctx needs no free */
+return ret;
 }
 
 
@@ -261,6 +231,31 @@ exit(1);
 *            Handler for SIGSEGV               *
 ***********************************************/
 
+#define STACKDUMP_MAX 24
+static void
+stackdump(void)
+{
+#ifndef NO_EXECINFO
+void * buf[STACKDUMP_MAX];
+char ** ss;
+int nptrs = backtrace(buf, STACKDUMP_MAX);
+
+log_write(0, LOG_MAIN|LOG_PANIC, "backtrace\n");
+log_write(0, LOG_MAIN|LOG_PANIC, "---\n");
+if ((ss = backtrace_symbols(buf, nptrs)))
+  {
+  for (int i = 0; i < nptrs; i++)
+    log_write(0, LOG_MAIN|LOG_PANIC, "\t%s\n", ss[i]);
+  free(ss);
+  }
+else
+  log_write(0, LOG_MAIN|LOG_PANIC, "backtrace_symbols: %s\n", strerror(errno));
+log_write(0, LOG_MAIN|LOG_PANIC, "---\n");
+#endif
+}
+#undef STACKDUMP_MAX
+
+
 static void
 #ifdef SA_SIGINFO
 segv_handler(int sig, siginfo_t * info, void * uctx)
@@ -281,6 +276,7 @@ else
   log_write(0, LOG_MAIN|LOG_PANIC, "SIGSEGV (maybe attempt to write to immutable memory)");
 if (process_info_len > 0)
   log_write(0, LOG_MAIN|LOG_PANIC, "SIGSEGV (%.*s)", process_info_len, process_info);
+stackdump();
 signal(SIGSEGV, SIG_DFL);
 kill(getpid(), sig);
 }
@@ -291,6 +287,7 @@ segv_handler(int sig)
 log_write(0, LOG_MAIN|LOG_PANIC, "SIGSEGV (maybe attempt to write to immutable memory)");
 if (process_info_len > 0)
   log_write(0, LOG_MAIN|LOG_PANIC, "SIGSEGV (%.*s)", process_info_len, process_info);
+stackdump();
 signal(SIGSEGV, SIG_DFL);
 kill(getpid(), sig);
 }
@@ -1983,7 +1980,7 @@ this here, because the -M options check their arguments for syntactic validity
 using mac_ismsgid, which uses this. */
 
 regex_ismsgid =
-  regex_must_compile(US"^(?:[^\\W_]{6}-){2}[^\\W_]{2}$", FALSE, TRUE);
+  regex_must_compile(US"^(?:[^\\W_]{6}-){2}[^\\W_]{2}$", MCS_NOFLAGS, TRUE);
 
 /* Precompile the regular expression that is used for matching an SMTP error
 code, possibly extended, at the start of an error message. Note that the
@@ -1991,14 +1988,14 @@ terminating whitespace character is included. */
 
 regex_smtp_code =
   regex_must_compile(US"^\\d\\d\\d\\s(?:\\d\\.\\d\\d?\\d?\\.\\d\\d?\\d?\\s)?",
-    FALSE, TRUE);
+    MCS_NOFLAGS, TRUE);
 
 #ifdef WHITELIST_D_MACROS
 /* Precompile the regular expression used to filter the content of macros
 given to -D for permissibility. */
 
 regex_whitelisted_macro =
-  regex_must_compile(US"^[A-Za-z0-9_/.-]*$", FALSE, TRUE);
+  regex_must_compile(US"^[A-Za-z0-9_/.-]*$", MCS_NOFLAGS, TRUE);
 #endif
 
 for (i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
@@ -2216,7 +2213,7 @@ on the second character (the one after '-'), to save some effort. */
 	   -bdf: Ditto, but in the foreground.
 	*/
 	case 'd':
-	  f.daemon_listen = TRUE;
+	  f.daemon_listen = f.daemon_scion = TRUE;
 	  if (*argrest == 'f') f.background_daemon = FALSE;
 	  else if (*argrest) badarg = TRUE;
 	  break;
@@ -2476,7 +2473,7 @@ on the second character (the one after '-'), to save some effort. */
 	case 'w':
 	  f.inetd_wait_mode = TRUE;
 	  f.background_daemon = FALSE;
-	  f.daemon_listen = TRUE;
+	  f.daemon_listen = f.daemon_scion = TRUE;
 	  if (*argrest)
 	    if ((inetd_wait_timeout = readconf_readtime(argrest, 0, FALSE)) <= 0)
 	      exim_fail("exim: bad time value %s: abandoned\n", argv[i]);
@@ -5003,7 +5000,7 @@ for (i = 0;;)
         if (gecos_pattern && gecos_name)
           {
           const pcre2_code *re;
-          re = regex_must_compile(gecos_pattern, FALSE, TRUE); /* Use malloc */
+          re = regex_must_compile(gecos_pattern, MCS_NOFLAGS, TRUE); /* Use malloc */
 
           if (regex_match_and_setup(re, name, 0, -1))
             {
@@ -5399,7 +5396,10 @@ if (host_checking)
 
   memset(sender_host_cache, 0, sizeof(sender_host_cache));
   if (verify_check_host(&hosts_connection_nolog) == OK)
+    {
     BIT_CLEAR(log_selector, log_selector_size, Li_smtp_connection);
+    BIT_CLEAR(log_selector, log_selector_size, Li_smtp_no_mail);
+    }
   log_write(L_smtp_connection, LOG_MAIN, "%s", smtp_get_connection_info());
 
   /* NOTE: We do *not* call smtp_log_no_mail() if smtp_start_session() fails,
@@ -5588,7 +5588,10 @@ if (smtp_input)
   smtp_out = stdout;
   memset(sender_host_cache, 0, sizeof(sender_host_cache));
   if (verify_check_host(&hosts_connection_nolog) == OK)
+    {
     BIT_CLEAR(log_selector, log_selector_size, Li_smtp_connection);
+    BIT_CLEAR(log_selector, log_selector_size, Li_smtp_no_mail);
+    }
   log_write(L_smtp_connection, LOG_MAIN, "%s", smtp_get_connection_info());
   if (!smtp_start_session())
     {
diff --git a/src/src/expand.c b/src/src/expand.c
index 36c9f423b..ffbdc14e5 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -12,9 +12,15 @@
 
 #include "exim.h"
 
+typedef unsigned esi_flags;
+#define ESI_NOFLAGS		0
+#define ESI_BRACE_ENDS		BIT(0)	/* expansion should stop at } */
+#define ESI_HONOR_DOLLAR	BIT(1)	/* $ is meaningfull */
+#define ESI_SKIPPING		BIT(2)	/* value will not be needed */
+
 /* Recursively called function */
 
-static uschar *expand_string_internal(const uschar *, BOOL, const uschar **, BOOL, BOOL, BOOL *);
+static uschar *expand_string_internal(const uschar *, esi_flags, const uschar **, BOOL *, BOOL *);
 static int_eximarith_t expanded_string_integer(const uschar *, BOOL);
 
 #ifdef STAND_ALONE
@@ -686,6 +692,7 @@ static var_entry var_table[] = {
   { "recipient_verify_failure",vtype_stringptr,&recipient_verify_failure },
   { "recipients",          vtype_string_func, (void *) &fn_recipients },
   { "recipients_count",    vtype_int,         &recipients_count },
+  { "regex_cachesize",     vtype_int,         &regex_cachesize },/* undocumented; devel observability */
 #ifdef WITH_CONTENT_SCAN
   { "regex_match_string",  vtype_stringptr,   &regex_match_string },
 #endif
@@ -1748,9 +1755,7 @@ uschar buf[16];
 int fd;
 ssize_t len;
 const uschar * where;
-#ifndef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
 uschar * sname;
-#endif
 
 if ((fd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0)
   {
@@ -1758,17 +1763,9 @@ if ((fd = socket(AF_UNIX, SOCK_DGRAM, 0)) < 0)
   return NULL;
   }
 
-#ifdef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
-sa_un.sun_path[0] = 0;	/* Abstract local socket addr - Linux-specific? */
-len = offsetof(struct sockaddr_un, sun_path) + 1
-  + snprintf(sa_un.sun_path+1, sizeof(sa_un.sun_path)-1, "exim_%d", getpid());
-#else
-sname = string_sprintf("%s/p_%d", spool_directory, getpid());
-len = offsetof(struct sockaddr_un, sun_path)
-  + snprintf(sa_un.sun_path, sizeof(sa_un.sun_path), "%s", sname);
-#endif
+len = daemon_client_sockname(&sa_un, &sname);
 
-if (bind(fd, (const struct sockaddr *)&sa_un, len) < 0)
+if (bind(fd, (const struct sockaddr *)&sa_un, (socklen_t)len) < 0)
   { where = US"bind"; goto bad; }
 
 #ifdef notdef
@@ -1777,17 +1774,7 @@ debug_printf("local addr '%s%s'\n",
   sa_un.sun_path + (*sa_un.sun_path ? 0 : 1));
 #endif
 
-#ifdef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
-sa_un.sun_path[0] = 0;	/* Abstract local socket addr - Linux-specific? */
-len = offsetof(struct sockaddr_un, sun_path) + 1
-  + snprintf(sa_un.sun_path+1, sizeof(sa_un.sun_path)-1, "%s",
-	      expand_string(notifier_socket));
-#else
-len = offsetof(struct sockaddr_un, sun_path)
-  + snprintf(sa_un.sun_path, sizeof(sa_un.sun_path), "%s",
-	      expand_string(notifier_socket));
-#endif
-
+len = daemon_notifier_sockname(&sa_un);
 if (connect(fd, (const struct sockaddr *)&sa_un, len) < 0)
   { where = US"connect"; goto bad2; }
 
@@ -2114,27 +2101,33 @@ Arguments:
   n          maximum number of substrings
   m          minimum required
   sptr       points to current string pointer
-  skipping   the skipping flag
+  flags
+   skipping   the skipping flag
   check_end  if TRUE, check for final '}'
   name       name of item, for error message
   resetok    if not NULL, pointer to flag - write FALSE if unsafe to reset
-	     the store.
+	     the store
+  textonly_p if not NULL, pointer to bitmask of which subs were text-only
+	     (did not change when expended)
 
-Returns:     0 OK; string pointer updated
+Returns:     -1 OK; string pointer updated, but in "skipping" mode
+	     0 OK; string pointer updated
              1 curly bracketing error (too few arguments)
              2 too many arguments (only if check_end is set); message set
              3 other error (expansion failure)
 */
 
 static int
-read_subs(uschar **sub, int n, int m, const uschar **sptr, BOOL skipping,
-  BOOL check_end, uschar *name, BOOL *resetok)
+read_subs(uschar ** sub, int n, int m, const uschar ** sptr, esi_flags flags,
+  BOOL check_end, uschar * name, BOOL * resetok, unsigned * textonly_p)
 {
-const uschar *s = *sptr;
+const uschar * s = *sptr;
+unsigned textonly_l = 0;
 
 Uskip_whitespace(&s);
 for (int i = 0; i < n; i++)
   {
+  BOOL textonly;
   if (*s != '{')
     {
     if (i < m)
@@ -2146,11 +2139,14 @@ for (int i = 0; i < n; i++)
     sub[i] = NULL;
     break;
     }
-  if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, resetok)))
+  if (!(sub[i] = expand_string_internal(s+1,
+	  ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags & ESI_SKIPPING, &s, resetok,
+	  textonly_p ? &textonly : NULL)))
     return 3;
   if (*s++ != '}') return 1;
+  if (textonly_p && textonly) textonly_l |= BIT(i);
   Uskip_whitespace(&s);
-  }
+  }						/*{*/
 if (check_end && *s++ != '}')
   {
   if (s[-1] == '{')
@@ -2163,8 +2159,9 @@ if (check_end && *s++ != '}')
   return 1;
   }
 
+if (textonly_p) *textonly_p = textonly_l;
 *sptr = s;
-return 0;
+return flags & ESI_SKIPPING ? -1 : 0;
 }
 
 
@@ -2523,11 +2520,11 @@ Returns:   a pointer to the first character after the condition, or
 */
 
 static const uschar *
-eval_condition(const uschar *s, BOOL *resetok, BOOL *yield)
+eval_condition(const uschar * s, BOOL * resetok, BOOL * yield)
 {
 BOOL testfor = TRUE;
 BOOL tempcond, combined_cond;
-BOOL *subcondptr;
+BOOL * subcondptr;
 BOOL sub2_honour_dollar = TRUE;
 BOOL is_forany, is_json, is_jsons;
 int rc, cond_type;
@@ -2535,7 +2532,8 @@ int_eximarith_t num[2];
 struct stat statbuf;
 uschar * opname;
 uschar name[256];
-const uschar *sub[10];
+const uschar * sub[10];
+unsigned sub_textonly = 0;
 
 for (;;)
   if (Uskip_whitespace(&s) == '!') { testfor = !testfor; s++; } else break;
@@ -2629,8 +2627,14 @@ switch(cond_type = identify_operator(&s, &opname))
 
   if (Uskip_whitespace(&s) != '{') goto COND_FAILED_CURLY_START; /* }-for-text-editors */
 
-  sub[0] = expand_string_internal(s+1, TRUE, &s, yield == NULL, TRUE, resetok);
-  if (!sub[0]) return NULL;
+   {
+    BOOL textonly;
+    sub[0] = expand_string_internal(s+1,
+      ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | (yield ? ESI_NOFLAGS : ESI_SKIPPING),
+      &s, resetok, &textonly);
+    if (!sub[0]) return NULL;
+    if (textonly) sub_textonly |= BIT(0);
+   }
   /* {-for-text-editors */
   if (*s++ != '}') goto COND_FAILED_CURLY_END;
 
@@ -2727,8 +2731,8 @@ switch(cond_type = identify_operator(&s, &opname))
     Uskip_whitespace(&s);
     if (*s++ != '{') goto COND_FAILED_CURLY_START;	/*}*/
 
-    switch(read_subs(sub, nelem(sub), 1,
-      &s, yield == NULL, TRUE, name, resetok))
+    switch(read_subs(sub, nelem(sub), 1, &s,
+	yield ? ESI_NOFLAGS : ESI_SKIPPING, TRUE, name, resetok, NULL))
       {
       case 1: expand_string_message = US"too few arguments or bracketing "
         "error for acl";
@@ -2779,8 +2783,8 @@ switch(cond_type = identify_operator(&s, &opname))
     uschar *sub[4];
     Uskip_whitespace(&s);
     if (*s++ != '{') goto COND_FAILED_CURLY_START;	/* }-for-text-editors */
-    switch(read_subs(sub, nelem(sub), 2, &s, yield == NULL, TRUE, name,
-		    resetok))
+    switch(read_subs(sub, nelem(sub), 2, &s,
+	yield ? ESI_NOFLAGS : ESI_SKIPPING, TRUE, name, resetok, NULL))
       {
       case 1: expand_string_message = US"too few arguments or bracketing "
 	"error for saslauthd";
@@ -2848,12 +2852,16 @@ switch(cond_type = identify_operator(&s, &opname))
 
   for (int i = 0; i < 2; i++)
     {
+    BOOL textonly;
     /* Sometimes, we don't expand substrings; too many insecure configurations
     created using match_address{}{} and friends, where the second param
     includes information from untrustworthy sources. */
-    BOOL honour_dollar = TRUE;
-    if ((i > 0) && !sub2_honour_dollar)
-      honour_dollar = FALSE;
+    /*XXX is this moot given taint-tracking? */
+
+    esi_flags flags = ESI_BRACE_ENDS;
+
+    if (!(i > 0 && !sub2_honour_dollar)) flags |= ESI_HONOR_DOLLAR;
+    if (!yield) flags |= ESI_SKIPPING;
 
     if (Uskip_whitespace(&s) != '{')
       {
@@ -2862,9 +2870,9 @@ switch(cond_type = identify_operator(&s, &opname))
         "after \"%s\"", opname);
       return NULL;
       }
-    if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, yield == NULL,
-        honour_dollar, resetok)))
+    if (!(sub[i] = expand_string_internal(s+1, flags, &s, resetok, &textonly)))
       return NULL;
+    if (textonly) sub_textonly |= BIT(i);
     DEBUG(D_expand) if (i == 1 && !sub2_honour_dollar && Ustrchr(sub[1], '$'))
       debug_printf_indent("WARNING: the second arg is NOT expanded,"
 			" for security reasons\n");
@@ -2944,19 +2952,11 @@ switch(cond_type = identify_operator(&s, &opname))
 
     case ECOND_MATCH:   /* Regular expression match */
       {
-      const pcre2_code * re;
-      PCRE2_SIZE offset;
-      int err;
-
-      if (!(re = pcre2_compile((PCRE2_SPTR)sub[1], PCRE2_ZERO_TERMINATED,
-				PCRE_COPT, &err, &offset, pcre_cmp_ctx)))
-	{
-	uschar errbuf[128];
-	pcre2_get_error_message(err, errbuf, sizeof(errbuf));
-	expand_string_message = string_sprintf("regular expression error in "
-	  "\"%s\": %s at offset %ld", sub[1], errbuf, (long)offset);
+      const pcre2_code * re = regex_compile(sub[1],
+		  sub_textonly & BIT(1) ? MCS_CACHEABLE : MCS_NOFLAGS,
+		  &expand_string_message, pcre_gen_cmp_ctx);
+      if (!re)
 	return NULL;
-	}
 
       tempcond = regex_match_and_setup(re, sub[0], 0, -1);
       break;
@@ -3274,7 +3274,9 @@ switch(cond_type = identify_operator(&s, &opname))
 
     Uskip_whitespace(&s);
     if (*s++ != '{') goto COND_FAILED_CURLY_START;	/* }-for-text-editors */
-    if (!(sub[0] = expand_string_internal(s, TRUE, &s, yield == NULL, TRUE, resetok)))
+    if (!(sub[0] = expand_string_internal(s,
+      ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | (yield ? ESI_NOFLAGS : ESI_SKIPPING),
+      &s, resetok, NULL)))
       return NULL;
     /* {-for-text-editors */
     if (*s++ != '}') goto COND_FAILED_CURLY_END;
@@ -3362,7 +3364,8 @@ switch(cond_type = identify_operator(&s, &opname))
 
     if (Uskip_whitespace(&s) != '{') goto COND_FAILED_CURLY_START;	/* }-for-text-editors */
     ourname = cond_type == ECOND_BOOL_LAX ? US"bool_lax" : US"bool";
-    switch(read_subs(sub_arg, 1, 1, &s, yield == NULL, FALSE, ourname, resetok))
+    switch(read_subs(sub_arg, 1, 1, &s,
+	    yield ? ESI_NOFLAGS : ESI_SKIPPING, FALSE, ourname, resetok, NULL))
       {
       case 1: expand_string_message = string_sprintf(
                   "too few arguments or bracketing error for %s",
@@ -3430,7 +3433,8 @@ switch(cond_type = identify_operator(&s, &opname))
     uschar cksum[4];
     BOOL boolvalue = FALSE;
 
-    switch(read_subs(sub, 2, 2, CUSS &s, yield == NULL, FALSE, name, resetok))
+    switch(read_subs(sub, 2, 2, CUSS &s,
+	    yield ? ESI_NOFLAGS : ESI_SKIPPING, FALSE, name, resetok, NULL))
       {
       case 1: expand_string_message = US"too few arguments or bracketing "
 	"error for inbound_srs";
@@ -3441,10 +3445,10 @@ switch(cond_type = identify_operator(&s, &opname))
     /* Match the given local_part against the SRS-encoded pattern */
 
     re = regex_must_compile(US"^(?i)SRS0=([^=]+)=([A-Z2-7]+)=([^=]*)=(.*)$",
-			    TRUE, FALSE);
+			    MCS_CASELESS | MCS_CACHEABLE, FALSE);
     md = pcre2_match_data_create(4+1, pcre_gen_ctx);
     if (pcre2_match(re, sub[0], PCRE2_ZERO_TERMINATED, 0, PCRE_EOPT,
-		    md, pcre_mtc_ctx) < 0)
+		    md, pcre_gen_mtc_ctx) < 0)
       {
       DEBUG(D_expand) debug_printf("no match for SRS'd local-part pattern\n");
       goto srs_result;
@@ -3521,6 +3525,7 @@ switch(cond_type = identify_operator(&s, &opname))
     boolvalue = TRUE;
 
 srs_result:
+    /* pcre2_match_data_free(md);	gen ctx needs no free */
     if (yield) *yield = (boolvalue == testfor);
     return s;
     }
@@ -3628,7 +3633,8 @@ expanded, to check their syntax, but "skipping" is set when the result is not
 needed - this avoids unnecessary nested lookups.
 
 Arguments:
-  skipping       TRUE if we were skipping when this item was reached
+  flags
+   skipping       TRUE if we were skipping when this item was reached
   yes            TRUE if the first string is to be used, else use the second
   save_lookup    a value to put back into lookup_value before the 2nd expansion
   sptr           points to the input string pointer
@@ -3644,7 +3650,7 @@ Returns:         0 OK; lookup_value has been reset to save_lookup
 */
 
 static int
-process_yesno(BOOL skipping, BOOL yes, uschar *save_lookup, const uschar **sptr,
+process_yesno(esi_flags flags, BOOL yes, uschar *save_lookup, const uschar **sptr,
   gstring ** yieldptr, uschar *type, BOOL *resetok)
 {
 int rc = 0;
@@ -3652,6 +3658,8 @@ const uschar *s = *sptr;    /* Local value */
 uschar *sub1, *sub2;
 const uschar * errwhere;
 
+flags &= ESI_SKIPPING;		/* Ignore all buf the skipping flag */
+
 /* If there are no following strings, we substitute the contents of $value for
 lookups and for extractions in the success case. For the ${if item, the string
 "true" is substituted. In the fail case, nothing is substituted for all three
@@ -3661,12 +3669,12 @@ if (skip_whitespace(&s) == '}')
   {
   if (type[0] == 'i')
     {
-    if (yes && !skipping)
+    if (yes && !(flags & ESI_SKIPPING))
       *yieldptr = string_catn(*yieldptr, US"true", 4);
     }
   else
     {
-    if (yes && lookup_value && !skipping)
+    if (yes && lookup_value && !(flags & ESI_SKIPPING))
       *yieldptr = string_cat(*yieldptr, lookup_value);
     lookup_value = save_lookup;
     }
@@ -3678,7 +3686,7 @@ if (skip_whitespace(&s) == '}')
 
 if (*s++ != '{')
   {
-  errwhere = US"'yes' part did not start with '{'";
+  errwhere = US"'yes' part did not start with '{'";		/*}}*/
   goto FAILED_CURLY;
   }
 
@@ -3686,9 +3694,12 @@ if (*s++ != '{')
 want this string. Set skipping in the call in the fail case (this will always
 be the case if we were already skipping). */
 
-sub1 = expand_string_internal(s, TRUE, &s, !yes, TRUE, resetok);
+sub1 = expand_string_internal(s,
+  ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | (yes ? ESI_NOFLAGS : ESI_SKIPPING),
+  &s, resetok, NULL);
 if (sub1 == NULL && (yes || !f.expand_string_forcedfail)) goto FAILED;
 f.expand_string_forcedfail = FALSE;
+								/*{{*/
 if (*s++ != '}')
   {
   errwhere = US"'yes' part did not end with '}'";
@@ -3713,14 +3724,16 @@ time, forced failures are noticed only if we want the second string. We must
 set skipping in the nested call if we don't want this string, or if we were
 already skipping. */
 
-if (skip_whitespace(&s) == '{')
+if (skip_whitespace(&s) == '{')					/*}*/
   {
-  sub2 = expand_string_internal(s+1, TRUE, &s, yes || skipping, TRUE, resetok);
-  if (sub2 == NULL && (!yes || !f.expand_string_forcedfail)) goto FAILED;
-  f.expand_string_forcedfail = FALSE;
+  esi_flags s_flags = ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags;
+  if (yes) s_flags |= ESI_SKIPPING;
+  sub2 = expand_string_internal(s+1, s_flags, &s, resetok, NULL);
+  if (!sub2 && (!yes || !f.expand_string_forcedfail)) goto FAILED;
+  f.expand_string_forcedfail = FALSE;				/*{*/
   if (*s++ != '}')
     {
-    errwhere = US"'no' part did not start with '{'";
+    errwhere = US"'no' part did not start with '{'";		/*}*/
     goto FAILED_CURLY;
     }
 
@@ -3729,7 +3742,7 @@ if (skip_whitespace(&s) == '{')
   if (!yes)
     *yieldptr = string_cat(*yieldptr, sub2);
   }
-
+								/*{{*/
 /* If there is no second string, but the word "fail" is present when the use of
 the second string is wanted, set a flag indicating it was a forced failure
 rather than a syntactic error. Swallow the terminating } in case this is nested
@@ -3742,9 +3755,9 @@ else if (*s != '}')
   s = US read_name(name, sizeof(name), s, US"_");
   if (Ustrcmp(name, "fail") == 0)
     {
-    if (!yes && !skipping)
+    if (!yes && !(flags & ESI_SKIPPING))
       {
-      Uskip_whitespace(&s);
+      Uskip_whitespace(&s);					/*{{*/
       if (*s++ != '}')
         {
 	errwhere = US"did not close with '}' after forcedfail";
@@ -3766,7 +3779,7 @@ else if (*s != '}')
 
 /* All we have to do now is to check on the final closing brace. */
 
-skip_whitespace(&s);
+skip_whitespace(&s);						/*{{*/
 if (*s++ != '}')
   {
   errwhere = US"did not close with '}'";
@@ -4445,15 +4458,17 @@ string expansion becoming too powerful.
 
 Arguments:
   string         the string to be expanded
-  ket_ends       true if expansion is to stop at }
+  flags
+   brace_ends     expansion is to stop at }
+   honour_dollar  TRUE if $ is to be expanded,
+                  FALSE if it's just another character
+   skipping       TRUE for recursive calls when the value isn't actually going
+                  to be used (to allow for optimisation)
   left           if not NULL, a pointer to the first character after the
-                 expansion is placed here (typically used with ket_ends)
-  skipping       TRUE for recursive calls when the value isn't actually going
-                 to be used (to allow for optimisation)
-  honour_dollar  TRUE if $ is to be expanded,
-                 FALSE if it's just another character
+                 expansion is placed here (typically used with brace_ends)
   resetok_p	 if not NULL, pointer to flag - write FALSE if unsafe to reset
 		 the store.
+  textonly_p	 if not NULL, pointer to flag - write bool for only-met-text
 
 Returns:         NULL if expansion fails:
                    expand_string_forcedfail is set TRUE if failure was forced
@@ -4462,8 +4477,8 @@ Returns:         NULL if expansion fails:
 */
 
 static uschar *
-expand_string_internal(const uschar *string, BOOL ket_ends, const uschar **left,
-  BOOL skipping, BOOL honour_dollar, BOOL *resetok_p)
+expand_string_internal(const uschar * string, esi_flags flags, const uschar ** left,
+  BOOL *resetok_p, BOOL * textonly_p)
 {
 rmark reset_point = store_mark();
 gstring * yield = string_get(Ustrlen(string) + 64);
@@ -4471,7 +4486,7 @@ int item_type;
 const uschar * s = string;
 const uschar * save_expand_nstring[EXPAND_MAXN+1];
 int save_expand_nlength[EXPAND_MAXN+1];
-BOOL resetok = TRUE, first = TRUE;
+BOOL resetok = TRUE, first = TRUE, textonly = TRUE;
 
 expand_level++;
 f.expand_string_forcedfail = FALSE;
@@ -4494,11 +4509,11 @@ while (*s)
     DEBUG(D_noutf8)
       debug_printf_indent("%c%s: %s\n",
 	first ? '/' : '|',
-	skipping ? "---scanning" : "considering", s);
+	flags & ESI_SKIPPING ? "---scanning" : "considering", s);
     else
       debug_printf_indent("%s%s: %s\n",
 	first ? UTF8_DOWN_RIGHT : UTF8_VERT_RIGHT,
-	skipping
+	flags & ESI_SKIPPING
 	? UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ "scanning"
 	: "considering",
 	s);
@@ -4524,7 +4539,7 @@ while (*s)
       for (s = t; *s ; s++) if (*s == '\\' && s[1] == 'N') break;
 
       DEBUG(D_expand)
-	debug_expansion_interim(US"protected", t, (int)(s - t), skipping);
+	debug_expansion_interim(US"protected", t, (int)(s - t), !!(flags & ESI_SKIPPING));
       yield = string_catn(yield, t, s - t);
       if (*s) s += 2;
       }
@@ -4547,20 +4562,21 @@ while (*s)
   /* Anything other than $ is just copied verbatim, unless we are
   looking for a terminating } character. */
 
-  if (ket_ends && *s == '}') break;
+  if (flags & ESI_BRACE_ENDS && *s == '}') break;
 
-  if (*s != '$' || !honour_dollar)
+  if (*s != '$' || !(flags & ESI_HONOR_DOLLAR))
     {
     int i = 1;								/*{*/
     for (const uschar * t = s+1;
 	*t && *t != '$' && *t != '}' && *t != '\\'; t++) i++;
 
-    DEBUG(D_expand) debug_expansion_interim(US"text", s, i, skipping);
+    DEBUG(D_expand) debug_expansion_interim(US"text", s, i, !!(flags & ESI_SKIPPING));
 
     yield = string_catn(yield, s, i);
     s += i;
     continue;
     }
+  textonly = FALSE;
 
   /* No { after the $ - must be a plain name or a number for string
   match variable. There has to be a fudge for variables that are the
@@ -4622,7 +4638,7 @@ while (*s)
 
     /* Variable */
 
-    else if (!(value = find_variable(name, FALSE, skipping, &newsize)))
+    else if (!(value = find_variable(name, FALSE, !!(flags & ESI_SKIPPING), &newsize)))
       {
       expand_string_message =
 	string_sprintf("unknown variable name \"%s\"", name);
@@ -4722,14 +4738,13 @@ while (*s)
       uschar * user_msg;
       int rc;
 
-      switch(read_subs(sub, nelem(sub), 1, &s, skipping, TRUE, name,
-		      &resetok))
+      switch(read_subs(sub, nelem(sub), 1, &s, flags, TRUE, name, &resetok, NULL))
         {
+	case -1: continue;		/* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
-      if (skipping) continue;
 
       resetok = FALSE;
       switch(rc = eval_acl(sub, nelem(sub), &user_msg))
@@ -4758,13 +4773,13 @@ while (*s)
       {
       uschar * sub_arg[1];
 
-      switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, skipping, TRUE, name,
-		      &resetok))
+      switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, flags, TRUE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
+      /*XXX no skipping-optimisation? */
 
       yield = string_append(yield, 3,
 			US"Authentication-Results: ", sub_arg[0], US"; none");
@@ -4802,14 +4817,14 @@ while (*s)
       uschar * save_lookup_value = lookup_value;
 
       Uskip_whitespace(&s);
-      if (!(next_s = eval_condition(s, &resetok, skipping ? NULL : &cond)))
+      if (!(next_s = eval_condition(s, &resetok, flags & ESI_SKIPPING ? NULL : &cond)))
 	goto EXPAND_FAILED;  /* message already set */
 
       DEBUG(D_expand)
 	{
-	debug_expansion_interim(US"condition", s, (int)(next_s - s), skipping);
+	debug_expansion_interim(US"condition", s, (int)(next_s - s), !!(flags & ESI_SKIPPING));
 	debug_expansion_interim(US"result",
-	  cond ? US"true" : US"false", cond ? 4 : 5, skipping);
+	  cond ? US"true" : US"false", cond ? 4 : 5, !!(flags & ESI_SKIPPING));
 	}
 
       s = next_s;
@@ -4818,12 +4833,12 @@ while (*s)
       function that is also used by ${lookup} and ${extract} and ${run}. */
 
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               cond,                         /* success/failure indicator */
-               lookup_value,                 /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"if",                       /* condition type */
+               flags,			/* were previously skipping */
+               cond,			/* success/failure indicator */
+               lookup_value,			/* value to reset for string2 */
+               &s,			/* input pointer */
+               &yield,			/* output pointer */
+               US"if",			/* condition type */
 	       &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
@@ -4845,13 +4860,13 @@ while (*s)
       uschar *sub_arg[3];
       uschar *encoded;
 
-      switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, skipping, TRUE, name,
-		      &resetok))
+      switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, flags, TRUE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
+      /*XXX no skipping-optimisation? */
 
       if (!sub_arg[1])			/* One argument */
 	{
@@ -4867,7 +4882,7 @@ while (*s)
 	goto EXPAND_FAILED;
 	}
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
 
       if (!(encoded = imap_utf7_encode(sub_arg[0], headers_charset,
 			  sub_arg[1][0], sub_arg[2], &expand_string_message)))
@@ -4906,7 +4921,8 @@ while (*s)
 
       if (Uskip_whitespace(&s) == '{')					/*}*/
         {
-        key = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
+        key = expand_string_internal(s+1,
+		ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
         if (!key) goto EXPAND_FAILED;			/*{{*/
         if (*s++ != '}')
 	  {
@@ -4976,7 +4992,8 @@ while (*s)
 	expand_string_message = US"missing '{' for lookup file-or-query arg";
 	goto EXPAND_FAILED_CURLY;						/*}}*/
 	}
-      if (!(filename = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok)))
+      if (!(filename = expand_string_internal(s+1,
+		ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL)))
 	goto EXPAND_FAILED;
       										/*{{*/
       if (*s++ != '}')
@@ -5007,7 +5024,7 @@ while (*s)
       since new variables will have been set. Note that at the end of this
       "lookup" section, the old numeric variables are restored. */
 
-      if (skipping)
+      if (flags & ESI_SKIPPING)
         lookup_value = NULL;
       else
         {
@@ -5033,12 +5050,12 @@ while (*s)
       function that is also used by ${if} and ${extract}. */
 
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               lookup_value != NULL,         /* success/failure indicator */
-               save_lookup_value,            /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"lookup",                   /* condition type */
+               flags,			/* were previously skipping */
+               lookup_value != NULL,	/* success/failure indicator */
+               save_lookup_value,	/* value to reset for string2 */
+               &s,			/* input pointer */
+               &yield,			/* output pointer */
+               US"lookup",		/* condition type */
 	       &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
@@ -5051,7 +5068,7 @@ while (*s)
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -5079,18 +5096,15 @@ while (*s)
         goto EXPAND_FAILED;
         }
 
-      switch(read_subs(sub_arg, EXIM_PERL_MAX_ARGS + 1, 1, &s, skipping, TRUE,
-           name, &resetok))
+      switch(read_subs(sub_arg, EXIM_PERL_MAX_ARGS + 1, 1, &s, flags, TRUE,
+           name, &resetok, NULL))
         {
+	case -1: continue;	/* If skipping, we don't actually do anything */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      /* If skipping, we don't actually do anything */
-
-      if (skipping) continue;
-
       /* Start the interpreter if necessary */
 
       if (!opt_perl_started)
@@ -5150,16 +5164,14 @@ while (*s)
       {
       uschar * sub_arg[3], * p, * domain;
 
-      switch(read_subs(sub_arg, 3, 2, &s, skipping, TRUE, name, &resetok))
+      switch(read_subs(sub_arg, 3, 2, &s, flags, TRUE, name, &resetok, NULL))
         {
+	case -1: continue;	/* If skipping, we don't actually do anything */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      /* If skipping, we don't actually do anything */
-      if (skipping) continue;
-
       /* sub_arg[0] is the address */
       if (  !(domain = Ustrrchr(sub_arg[0],'@'))
 	 || domain == sub_arg[0] || Ustrlen(domain) == 1)
@@ -5208,30 +5220,21 @@ while (*s)
       gstring * g;
       const pcre2_code * re;
 
-      /* TF: Ugliness: We want to expand parameter 1 first, then set
-         up expansion variables that are used in the expansion of
-         parameter 2. So we clone the string for the first
-         expansion, where we only expand parameter 1.
-
-         PH: Actually, that isn't necessary. The read_subs() function is
-         designed to work this way for the ${if and ${lookup expansions. I've
-         tidied the code.
-      */								/*}}*/
-
       /* Reset expansion variables */
       prvscheck_result = NULL;
       prvscheck_address = NULL;
       prvscheck_keynum = NULL;
 
-      switch(read_subs(sub_arg, 1, 1, &s, skipping, FALSE, name, &resetok))
+      switch(read_subs(sub_arg, 1, 1, &s, flags, FALSE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      re = regex_must_compile(US"^prvs\\=([0-9])([0-9]{3})([A-F0-9]{6})\\=(.+)\\@(.+)$",
-                              TRUE,FALSE);
+      re = regex_must_compile(
+	US"^prvs\\=([0-9])([0-9]{3})([A-F0-9]{6})\\=(.+)\\@(.+)$",
+	MCS_CASELESS | MCS_CACHEABLE, FALSE);
 
       if (regex_match_and_setup(re,sub_arg[0],0,-1))
         {
@@ -5241,11 +5244,14 @@ while (*s)
         uschar * hash = string_copyn(expand_nstring[3],expand_nlength[3]);
         uschar * domain = string_copyn(expand_nstring[5],expand_nlength[5]);
 
-        DEBUG(D_expand) debug_printf_indent("prvscheck localpart: %s\n", local_part);
-        DEBUG(D_expand) debug_printf_indent("prvscheck key number: %s\n", key_num);
-        DEBUG(D_expand) debug_printf_indent("prvscheck daystamp: %s\n", daystamp);
-        DEBUG(D_expand) debug_printf_indent("prvscheck hash: %s\n", hash);
-        DEBUG(D_expand) debug_printf_indent("prvscheck domain: %s\n", domain);
+        DEBUG(D_expand)
+	  {
+	  debug_printf_indent("prvscheck localpart: %s\n", local_part);
+	  debug_printf_indent("prvscheck key number: %s\n", key_num);
+	  debug_printf_indent("prvscheck daystamp: %s\n", daystamp);
+	  debug_printf_indent("prvscheck hash: %s\n", hash);
+	  debug_printf_indent("prvscheck domain: %s\n", domain);
+	  }
 
         /* Set up expansion variables */
         g = string_cat (NULL, local_part);
@@ -5255,7 +5261,7 @@ while (*s)
         prvscheck_keynum = string_copy(key_num);
 
         /* Now expand the second argument */
-        switch(read_subs(sub_arg, 1, 1, &s, skipping, FALSE, name, &resetok))
+        switch(read_subs(sub_arg, 1, 1, &s, flags, FALSE, name, &resetok, NULL))
           {
           case 1: goto EXPAND_FAILED_CURLY;
           case 2:
@@ -5266,7 +5272,6 @@ while (*s)
 
         p = prvs_hmac_sha1(prvscheck_address, sub_arg[0], prvscheck_keynum,
           daystamp);
-
         if (!p)
           {
           expand_string_message = US"hmac-sha1 conversion failed";
@@ -5309,7 +5314,7 @@ while (*s)
         /* Now expand the final argument. We leave this till now so that
         it can include $prvscheck_result. */
 
-        switch(read_subs(sub_arg, 1, 0, &s, skipping, TRUE, name, &resetok))
+        switch(read_subs(sub_arg, 1, 0, &s, flags, TRUE, name, &resetok, NULL))
           {
           case 1: goto EXPAND_FAILED_CURLY;
           case 2:
@@ -5330,14 +5335,14 @@ while (*s)
            We need to make sure all subs are expanded first, so as to skip over
            the entire item. */
 
-        switch(read_subs(sub_arg, 2, 1, &s, skipping, TRUE, name, &resetok))
+        switch(read_subs(sub_arg, 2, 1, &s, flags, TRUE, name, &resetok, NULL))
           {
           case 1: goto EXPAND_FAILED_CURLY;
           case 2:
           case 3: goto EXPAND_FAILED;
           }
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -5354,7 +5359,7 @@ while (*s)
         goto EXPAND_FAILED;
         }
 
-      switch(read_subs(sub_arg, 2, 1, &s, skipping, TRUE, name, &resetok))
+      switch(read_subs(sub_arg, 2, 1, &s, flags, TRUE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
@@ -5363,7 +5368,7 @@ while (*s)
 
       /* If skipping, we don't actually do anything */
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
 
       /* Open the file and read it */
 
@@ -5395,7 +5400,7 @@ while (*s)
       /* Read up to 4 arguments, but don't do the end of item check afterwards,
       because there may be a string for expansion on failure. */
 
-      switch(read_subs(sub_arg, 4, 2, &s, skipping, FALSE, name, &resetok))
+      switch(read_subs(sub_arg, 4, 2, &s, flags, FALSE, name, &resetok, NULL))
         {
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:                             /* Won't occur: no end check */
@@ -5405,7 +5410,7 @@ while (*s)
       /* If skipping, we don't actually do anything. Otherwise, arrange to
       connect to either an IP or a Unix socket. */
 
-      if (!skipping)
+      if (!(flags & ESI_SKIPPING))
         {
 	int stype = search_findtype(US"readsock", 8);
 	gstring * g = NULL;
@@ -5483,7 +5488,8 @@ while (*s)
 
       if (*s == '{')							/*}*/
         {
-        if (!expand_string_internal(s+1, TRUE, &s, TRUE, TRUE, &resetok))
+        if (!expand_string_internal(s+1,
+	  ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | ESI_SKIPPING, &s, &resetok, NULL))
           goto EXPAND_FAILED;						/*{*/
         if (*s++ != '}')
 	  {								/*{*/
@@ -5499,7 +5505,7 @@ while (*s)
 	expand_string_message = US"missing '}' closing readsocket";
 	goto EXPAND_FAILED_CURLY;
 	}
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
 
       /* Come here on failure to create socket, connect socket, write to the
@@ -5509,7 +5515,8 @@ while (*s)
     SOCK_FAIL:
       if (*s != '{') goto EXPAND_FAILED;				/*}*/
       DEBUG(D_any) debug_printf("%s\n", expand_string_message);
-      if (!(arg = expand_string_internal(s+1, TRUE, &s, FALSE, TRUE, &resetok)))
+      if (!(arg = expand_string_internal(s+1,
+		    ESI_BRACE_ENDS | ESI_HONOR_DOLLAR, &s, &resetok, NULL)))
         goto EXPAND_FAILED;
       yield = string_cat(yield, arg);					/*{*/
       if (*s++ != '}')
@@ -5560,14 +5567,15 @@ while (*s)
       s++;
 
       if (late_expand)		/* this is the default case */
-	{
+	{						/*{*/
 	int n = Ustrcspn(s, "}");
-	arg = skipping ? NULL : string_copyn(s, n);
+	arg = flags & ESI_SKIPPING ? NULL : string_copyn(s, n);
 	s += n;
 	}
       else
 	{
-	if (!(arg = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok)))
+	if (!(arg = expand_string_internal(s,
+		ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL)))
 	  goto EXPAND_FAILED;
 	Uskip_whitespace(&s);
 	}
@@ -5578,7 +5586,7 @@ while (*s)
 	goto EXPAND_FAILED_CURLY;
 	}
 
-      if (skipping)   /* Just pretend it worked when we're skipping */
+      if (flags & ESI_SKIPPING)   /* Just pretend it worked when we're skipping */
 	{
         runrc = 0;
 	lookup_value = NULL;
@@ -5652,19 +5660,19 @@ while (*s)
       /* Process the yes/no strings; $value may be useful in both cases */
 
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               runrc == 0,                   /* success/failure indicator */
-               lookup_value,                 /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"run",                      /* condition type */
+               flags,			/* were previously skipping */
+               runrc == 0,		/* success/failure indicator */
+               lookup_value,		/* value to reset for string2 */
+               &s,			/* input pointer */
+               &yield,			/* output pointer */
+               US"run",			/* condition type */
 	       &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
         case 2: goto EXPAND_FAILED_CURLY;    /* returned value is 0 */
         }
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -5676,8 +5684,9 @@ while (*s)
       int o2m;
       uschar * sub[3];
 
-      switch(read_subs(sub, 3, 3, &s, skipping, TRUE, name, &resetok))
+      switch(read_subs(sub, 3, 3, &s, flags, TRUE, name, &resetok, NULL))
         {
+	case -1: continue;	/* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
@@ -5696,7 +5705,6 @@ while (*s)
           }
         }
 
-      if (skipping) continue;
       break;
       }
 
@@ -5717,9 +5725,10 @@ while (*s)
       Ensure that sub[2] is set in the ${length } case. */
 
       sub[2] = NULL;
-      switch(read_subs(sub, (item_type == EITEM_LENGTH)? 2:3, 2, &s, skipping,
-             TRUE, name, &resetok))
+      switch(read_subs(sub, item_type == EITEM_LENGTH ? 2:3, 2, &s, flags,
+             TRUE, name, &resetok, NULL))
         {
+	case -1: continue;	/* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
@@ -5760,7 +5769,6 @@ while (*s)
       if (!ret)
 	goto EXPAND_FAILED;
       yield = string_catn(yield, ret, len);
-      if (skipping) continue;
       break;
       }
 
@@ -5793,15 +5801,14 @@ while (*s)
       uschar innerkey[MAX_HASHBLOCKLEN];
       uschar outerkey[MAX_HASHBLOCKLEN];
 
-      switch (read_subs(sub, 3, 3, &s, skipping, TRUE, name, &resetok))
+      switch (read_subs(sub, 3, 3, &s, flags, TRUE, name, &resetok, NULL))
         {
+	case -1: continue;	/* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      if (skipping) continue;
-
       if (Ustrcmp(sub[0], "md5") == 0)
 	{
 	type = HMAC_MD5;
@@ -5881,32 +5888,29 @@ while (*s)
       {
       const pcre2_code * re;
       int moffset, moffsetextra, slen;
-      PCRE2_SIZE roffset;
       pcre2_match_data * md;
-      int err, emptyopt;
+      int emptyopt;
       uschar * subject, * sub[3];
       int save_expand_nmax =
         save_expand_strings(save_expand_nstring, save_expand_nlength);
+      unsigned sub_textonly = 0;
 
-      switch(read_subs(sub, 3, 3, &s, skipping, TRUE, name, &resetok))
+      switch(read_subs(sub, 3, 3, &s, flags, TRUE, name, &resetok, &sub_textonly))
         {
+	case -1: continue;	/* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      /*XXX no handling of skipping? */
       /* Compile the regular expression */
 
-      if (!(re = pcre2_compile((PCRE2_SPTR)sub[1], PCRE2_ZERO_TERMINATED,
-		  PCRE_COPT, &err, &roffset, pcre_cmp_ctx)))
-        {
-        uschar errbuf[128];
-	pcre2_get_error_message(err, errbuf, sizeof(errbuf));
-        expand_string_message = string_sprintf("regular expression error in "
-          "\"%s\": %s at offset %ld", sub[1], errbuf, (long)roffset);
+      re = regex_compile(sub[1],
+	      sub_textonly & BIT(1) ? MCS_CACHEABLE : MCS_NOFLAGS,
+	      &expand_string_message, pcre_gen_cmp_ctx);
+      if (!re)
         goto EXPAND_FAILED;
-        }
+
       md = pcre2_match_data_create(EXPAND_MAXN + 1, pcre_gen_ctx);
 
       /* Now run a loop to do the substitutions as often as necessary. It ends
@@ -5922,7 +5926,7 @@ while (*s)
         {
 	PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
 	int n = pcre2_match(re, (PCRE2_SPTR)subject, slen, moffset + moffsetextra,
-	  PCRE_EOPT | emptyopt, md, pcre_mtc_ctx);
+	  PCRE_EOPT | emptyopt, md, pcre_gen_mtc_ctx);
         uschar * insert;
 
         /* No match - if we previously set PCRE_NOTEMPTY after a null match, this
@@ -5984,9 +5988,9 @@ while (*s)
 
       /* All done - restore numerical variables. */
 
+      /* pcre2_match_data_free(md);	gen ctx needs no free */
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
-      if (skipping) continue;
       break;
       }
 
@@ -6021,11 +6025,12 @@ while (*s)
       available (eg. $item) hence cannot decide on numeric vs. keyed.
       Read a maximum of 5 arguments (including the yes/no) */
 
-      if (skipping)
+      if (flags & ESI_SKIPPING)
 	{
         for (int j = 5; j > 0 && *s == '{'; j--)			/*'}'*/
 	  {
-          if (!expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok))
+          if (!expand_string_internal(s+1,
+		ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL))
 	    goto EXPAND_FAILED;					/*'{'*/
           if (*s++ != '}')
 	    {
@@ -6052,7 +6057,8 @@ while (*s)
         {
 	if (Uskip_whitespace(&s) == '{') 				/*'}'*/
           {
-          if (!(sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok)))
+          if (!(sub[i] = expand_string_internal(s+1,
+		ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL)))
 	    goto EXPAND_FAILED;						/*'{'*/
           if (*s++ != '}')
 	    {
@@ -6111,7 +6117,7 @@ while (*s)
       /* Extract either the numbered or the keyed substring into $value. If
       skipping, just pretend the extraction failed. */
 
-      if (skipping)
+      if (flags & ESI_SKIPPING)
 	lookup_value = NULL;
       else switch (fmt)
 	{
@@ -6205,12 +6211,12 @@ while (*s)
       be yes/no strings, as for lookup or if. */
 
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               lookup_value != NULL,         /* success/failure indicator */
-               save_lookup_value,            /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"extract",                  /* condition type */
+               flags,			/* were previously skipping */
+               lookup_value != NULL,	/* success/failure indicator */
+               save_lookup_value,	/* value to reset for string2 */
+               &s,			/* input pointer */
+               &yield,			/* output pointer */
+               US"extract",		/* condition type */
 	       &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
@@ -6222,7 +6228,7 @@ while (*s)
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -6246,7 +6252,8 @@ while (*s)
 	  goto EXPAND_FAILED_CURLY;
 	  }
 
-	sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
+	sub[i] = expand_string_internal(s+1,
+	      ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
 	if (!sub[i])     goto EXPAND_FAILED;				/*{{*/
 	if (*s++ != '}')
 	  {
@@ -6271,7 +6278,7 @@ while (*s)
 	  while (len > 0 && isspace(p[len-1])) len--;
 	  p[len] = 0;
 
-	  if (!*p && !skipping)
+	  if (!*p && !(flags & ESI_SKIPPING))
 	    {
 	    expand_string_message = US"first argument of \"listextract\" must "
 	      "not be empty";
@@ -6297,18 +6304,18 @@ while (*s)
       /* Extract the numbered element into $value. If
       skipping, just pretend the extraction failed. */
 
-      lookup_value = skipping ? NULL : expand_getlistele(field_number, sub[1]);
+      lookup_value = flags & ESI_SKIPPING ? NULL : expand_getlistele(field_number, sub[1]);
 
       /* If no string follows, $value gets substituted; otherwise there can
       be yes/no strings, as for lookup or if. */
 
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               lookup_value != NULL,         /* success/failure indicator */
-               save_lookup_value,            /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"listextract",              /* condition type */
+               flags,				/* were previously skipping */
+               lookup_value != NULL,		/* success/failure indicator */
+               save_lookup_value,		/* value to reset for string2 */
+               &s,				/* input pointer */
+               &yield,				/* output pointer */
+               US"listextract",			/* condition type */
 	       &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
@@ -6320,15 +6327,16 @@ while (*s)
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
     case EITEM_LISTQUOTE:
       {
       uschar * sub[2];
-      switch(read_subs(sub, 2, 2, &s, skipping, TRUE, name, &resetok))
+      switch(read_subs(sub, 2, 2, &s, flags, TRUE, name, &resetok, NULL))
         {
+	case -1: continue;	/* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
@@ -6339,7 +6347,6 @@ while (*s)
 	yield = string_catn(yield, sub[1], 1);
 	}
       else yield = string_catn(yield, US" ", 1);
-      if (skipping) continue;
       break;
       }
 
@@ -6356,7 +6363,8 @@ while (*s)
 	expand_string_message = US"missing '{' for field arg of certextract";
 	goto EXPAND_FAILED_CURLY;					/*}*/
 	}
-      sub[0] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
+      sub[0] = expand_string_internal(s+1,
+		ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
       if (!sub[0])     goto EXPAND_FAILED;				/*{{*/
       if (*s++ != '}')
         {
@@ -6388,7 +6396,8 @@ while (*s)
 	  "be a certificate variable";
 	goto EXPAND_FAILED;
 	}
-      sub[1] = expand_string_internal(s+1, TRUE, &s, skipping, FALSE, &resetok);
+      sub[1] = expand_string_internal(s+1,
+		ESI_BRACE_ENDS | flags & ESI_SKIPPING, &s, &resetok, NULL);
       if (!sub[1])     goto EXPAND_FAILED;				/*{{*/
       if (*s++ != '}')
         {
@@ -6396,7 +6405,7 @@ while (*s)
 	goto EXPAND_FAILED_CURLY;
 	}
 
-      if (skipping)
+      if (flags & ESI_SKIPPING)
 	lookup_value = NULL;
       else
 	{
@@ -6404,12 +6413,12 @@ while (*s)
 	if (*expand_string_message) goto EXPAND_FAILED;
 	}
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               lookup_value != NULL,         /* success/failure indicator */
-               save_lookup_value,            /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"certextract",              /* condition type */
+               flags,				/* were previously skipping */
+               lookup_value != NULL,		/* success/failure indicator */
+               save_lookup_value,		/* value to reset for string2 */
+               &s,				/* input pointer */
+               &yield,				/* output pointer */
+               US"certextract",			/* condition type */
 	       &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
@@ -6418,7 +6427,7 @@ while (*s)
 
       restore_expand_strings(save_expand_nmax, save_expand_nstring,
         save_expand_nlength);
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 #endif	/*DISABLE_TLS*/
@@ -6443,7 +6452,8 @@ while (*s)
 	goto EXPAND_FAILED_CURLY;					/*}*/
 	}
 
-      if (!(list = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok)))
+      if (!(list = expand_string_internal(s,
+	      ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL)))
 	goto EXPAND_FAILED;						/*{{*/
       if (*s++ != '}')
         {
@@ -6461,7 +6471,8 @@ while (*s)
 	  expand_string_message = US"missing '{' for second arg of reduce";
 	  goto EXPAND_FAILED_CURLY;					/*}*/
 	  }
-        t = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok);
+        t = expand_string_internal(s,
+	      ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
         if (!t) goto EXPAND_FAILED;
         lookup_value = t;						/*{{*/
         if (*s++ != '}')
@@ -6488,7 +6499,8 @@ while (*s)
       the normal internal expansion function. */
 
       if (item_type != EITEM_FILTER)
-        temp = expand_string_internal(s, TRUE, &s, TRUE, TRUE, &resetok);
+        temp = expand_string_internal(s,
+	  ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | ESI_SKIPPING, &s, &resetok, NULL);
       else
         if ((temp = eval_condition(expr, &resetok, NULL))) s = temp;
 
@@ -6519,7 +6531,7 @@ while (*s)
       /* If we are skipping, we can now just move on to the next item. When
       processing for real, we perform the iteration. */
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       while ((iterate_item = string_nextinlist(&list, &sep, NULL, 0)))
         {
         *outsep = (uschar)sep;      /* Separator as a string */
@@ -6550,7 +6562,8 @@ while (*s)
 
         else
           {
-	  uschar * t = expand_string_internal(expr, TRUE, NULL, skipping, TRUE, &resetok);
+	  uschar * t = expand_string_internal(expr,
+	    ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, NULL, &resetok, NULL);
           temp = t;
           if (!temp)
             {
@@ -6622,7 +6635,7 @@ while (*s)
       /* Restore preserved $item */
 
       iterate_item = save_iterate_item;
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -6641,7 +6654,8 @@ while (*s)
 	goto EXPAND_FAILED_CURLY;					/*}*/
 	}
 
-      srclist = expand_string_internal(s, TRUE, &s, skipping, TRUE, &resetok);
+      srclist = expand_string_internal(s,
+	      ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
       if (!srclist) goto EXPAND_FAILED;					/*{{*/
       if (*s++ != '}')
         {
@@ -6656,7 +6670,8 @@ while (*s)
 	goto EXPAND_FAILED_CURLY;					/*}*/
 	}
 
-      cmp = expand_string_internal(s, TRUE, &s, skipping, FALSE, &resetok);
+      cmp = expand_string_internal(s,
+	      ESI_BRACE_ENDS | flags & ESI_SKIPPING, &s, &resetok, NULL);
       if (!cmp) goto EXPAND_FAILED;					/*{{*/
       if (*s++ != '}')
         {
@@ -6691,7 +6706,8 @@ while (*s)
 	}
 
       xtract = s;
-      if (!(tmp = expand_string_internal(s, TRUE, &s, TRUE, TRUE, &resetok)))
+      if (!(tmp = expand_string_internal(s,
+	ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | ESI_SKIPPING, &s, &resetok, NULL)))
 	goto EXPAND_FAILED;
       xtract = string_copyn(xtract, s - xtract);
 									/*{{*/
@@ -6707,7 +6723,7 @@ while (*s)
         goto EXPAND_FAILED;
         }
 
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
 
       while ((srcitem = string_nextinlist(&srclist, &sep, NULL, 0)))
 	{
@@ -6718,8 +6734,8 @@ while (*s)
 
 	/* extract field for comparisons */
 	iterate_item = srcitem;
-	if (  !(srcfield = expand_string_internal(xtract, FALSE, NULL, FALSE,
-					  TRUE, &resetok))
+	if (  !(srcfield = expand_string_internal(xtract,
+				  ESI_HONOR_DOLLAR, NULL, &resetok, NULL))
 	   || !*srcfield)
 	  {
 	  expand_string_message = string_sprintf(
@@ -6824,18 +6840,15 @@ while (*s)
         goto EXPAND_FAILED;
         }
 
-      switch(read_subs(argv, EXPAND_DLFUNC_MAX_ARGS + 2, 2, &s, skipping,
-           TRUE, name, &resetok))
+      switch(read_subs(argv, EXPAND_DLFUNC_MAX_ARGS + 2, 2, &s, flags,
+           TRUE, name, &resetok, NULL))
         {
+	case -1: continue;	/* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      /* If skipping, we don't actually do anything */
-
-      if (skipping) continue;
-
       /* Look up the dynamically loaded object handle in the tree. If it isn't
       found, dlopen() the file and put the handle in the tree for next time. */
 
@@ -6901,7 +6914,8 @@ while (*s)
       if (Uskip_whitespace(&s) != '{')					/*}*/
 	goto EXPAND_FAILED;
 
-      key = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
+      key = expand_string_internal(s+1,
+	      ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
       if (!key) goto EXPAND_FAILED;					/*{{*/
       if (*s++ != '}')
         {
@@ -6912,18 +6926,18 @@ while (*s)
       lookup_value = US getenv(CS key);
 
       switch(process_yesno(
-               skipping,                     /* were previously skipping */
-               lookup_value != NULL,         /* success/failure indicator */
-               save_lookup_value,            /* value to reset for string2 */
-               &s,                           /* input pointer */
-               &yield,                       /* output pointer */
-               US"env",                      /* condition type */
+               flags,				/* were previously skipping */
+               lookup_value != NULL,		/* success/failure indicator */
+               save_lookup_value,		/* value to reset for string2 */
+               &s,				/* input pointer */
+               &yield,				/* output pointer */
+               US"env",				/* condition type */
 	       &resetok))
         {
         case 1: goto EXPAND_FAILED;          /* when all is well, the */
         case 2: goto EXPAND_FAILED_CURLY;    /* returned value is 0 */
         }
-      if (skipping) continue;
+      if (flags & ESI_SKIPPING) continue;
       break;
       }
 
@@ -6936,13 +6950,13 @@ while (*s)
       gstring * g = NULL;
       BOOL quoted = FALSE;
 
-      switch (read_subs(sub, 3, 3, CUSS &s, skipping, TRUE, name, &resetok))
+      switch (read_subs(sub, 3, 3, CUSS &s, flags, TRUE, name, &resetok, NULL))
         {
+	case -1: continue;	/* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
-      if (skipping) continue;
 
       if (sub[1] && *(sub[1]))
 	{
@@ -7024,7 +7038,7 @@ while (*s)
   DEBUG(D_expand)
     if (yield && (start > 0 || *s))	/* only if not the sole expansion of the line */
       debug_expansion_interim(US"item-res",
-			      yield->s + start, yield->ptr - start, skipping);
+			      yield->s + start, yield->ptr - start, !!(flags & ESI_SKIPPING));
   continue;
 
 NOT_ITEM: ;
@@ -7070,8 +7084,8 @@ NOT_ITEM: ;
 	if (s[1] == '$')
 	  {
 	  const uschar * s1 = s;
-	  sub = expand_string_internal(s+2, TRUE, &s1, skipping,
-		  FALSE, &resetok);
+	  sub = expand_string_internal(s+2,
+	      ESI_BRACE_ENDS | flags & ESI_SKIPPING, &s1, &resetok, NULL);
 	  if (!sub)       goto EXPAND_FAILED;		/*{*/
 	  if (*s1 != '}')
 	    {						/*{*/
@@ -7089,7 +7103,8 @@ NOT_ITEM: ;
         /*FALLTHROUGH*/
 #endif
       default:
-	sub = expand_string_internal(s+1, TRUE, &s, skipping, TRUE, &resetok);
+	sub = expand_string_internal(s+1,
+		ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL);
 	if (!sub) goto EXPAND_FAILED;
 	s++;
 	break;
@@ -7101,7 +7116,7 @@ NOT_ITEM: ;
     for the existence of $sender_host_address before trying to mask it. For
     other operations, doing them may not fail, but it is a waste of time. */
 
-    if (skipping && c >= 0) continue;
+    if (flags & ESI_SKIPPING && c >= 0) continue;
 
     /* Otherwise, switch on the operator type.  After handling go back
     to the main loop top. */
@@ -7186,7 +7201,8 @@ NOT_ITEM: ;
 
       case EOP_EXPAND:
 	{
-	uschar *expanded = expand_string_internal(sub, FALSE, NULL, skipping, TRUE, &resetok);
+	uschar *expanded = expand_string_internal(sub,
+		ESI_HONOR_DOLLAR | flags & ESI_SKIPPING, NULL, &resetok, NULL);
 	if (!expanded)
 	  {
 	  expand_string_message =
@@ -8177,7 +8193,7 @@ NOT_ITEM: ;
 	  debug_printf_indent("|-----op-res: %.*s\n", i, s);
 	  if (tainted)
 	    {
-	    debug_printf_indent("%s     \\__", skipping ? "|     " : "      ");
+	    debug_printf_indent("%s     \\__", flags & ESI_SKIPPING ? "|     " : "      ");
 	    debug_print_taint(yield->s);
 	    }
 	  }
@@ -8189,7 +8205,7 @@ NOT_ITEM: ;
 	  if (tainted)
 	    {
 	    debug_printf_indent("%s",
-	      skipping
+	      flags & ESI_SKIPPING
 	      ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
 	    debug_print_taint(yield->s);
 	    }
@@ -8223,7 +8239,7 @@ NOT_ITEM: ;
       reset_point = store_mark();
       g = store_get(sizeof(gstring), GET_UNTAINTED);	/* alloc _before_ calling find_variable() */
       }
-    if (!(value = find_variable(name, FALSE, skipping, &newsize)))
+    if (!(value = find_variable(name, FALSE, !!(flags & ESI_SKIPPING), &newsize)))
       {
       expand_string_message =
         string_sprintf("unknown variable in \"${%s}\"", name);
@@ -8251,11 +8267,11 @@ NOT_ITEM: ;
   goto EXPAND_FAILED;
   }
 
-/* If we hit the end of the string when ket_ends is set, there is a missing
+/* If we hit the end of the string when brace_ends is set, there is a missing
 terminating brace. */
 
-if (ket_ends && !*s)
-  {
+if (flags & ESI_BRACE_ENDS && !*s)
+  {							/*{{*/
   expand_string_message = malformed_header
     ? US"missing } at end of string - could be header name not terminated by colon"
     : US"missing } at end of string";
@@ -8285,13 +8301,13 @@ DEBUG(D_expand)
     {
     debug_printf_indent("|--expanding: %.*s\n", (int)(s - string), string);
     debug_printf_indent("%sresult: %s\n",
-      skipping ? "|-----" : "\\_____", yield->s);
+      flags & ESI_SKIPPING ? "|-----" : "\\_____", yield->s);
     if (tainted)
       {
-      debug_printf_indent("%s     \\__", skipping ? "|     " : "      ");
+      debug_printf_indent("%s     \\__", flags & ESI_SKIPPING ? "|     " : "      ");
       debug_print_taint(yield->s);
       }
-    if (skipping)
+    if (flags & ESI_SKIPPING)
       debug_printf_indent("\\___skipping: result is not used\n");
     }
   else
@@ -8301,20 +8317,21 @@ DEBUG(D_expand)
       (int)(s - string), string);
     debug_printf_indent("%s" UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
       "result: %s\n",
-      skipping ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
+      flags & ESI_SKIPPING ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
       yield->s);
     if (tainted)
       {
       debug_printf_indent("%s",
-	skipping
+	flags & ESI_SKIPPING
 	? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
       debug_print_taint(yield->s);
       }
-    if (skipping)
+    if (flags & ESI_SKIPPING)
       debug_printf_indent(UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
 	"skipping: result is not used\n");
     }
   }
+if (textonly_p) *textonly_p = textonly;
 expand_level--;
 return yield->s;
 
@@ -8363,16 +8380,20 @@ return NULL;
 }
 
 
+
 /* This is the external function call. Do a quick check for any expansion
 metacharacters, and if there are none, just return the input string.
 
-Argument: the string to be expanded
+Arguments
+	the string to be expanded
+	optional pointer for return boolean indicating no-dynamic-expansions
+
 Returns:  the expanded string, or NULL if expansion failed; if failure was
           due to a lookup deferring, search_find_defer will be TRUE
 */
 
 const uschar *
-expand_cstring(const uschar * string)
+expand_string_2(const uschar * string, BOOL * textonly_p)
 {
 if (Ustrpbrk(string, "$\\") != NULL)
   {
@@ -8382,19 +8403,22 @@ if (Ustrpbrk(string, "$\\") != NULL)
   f.search_find_defer = FALSE;
   malformed_header = FALSE;
   store_pool = POOL_MAIN;
-    s = expand_string_internal(string, FALSE, NULL, FALSE, TRUE, NULL);
+    s = expand_string_internal(string, ESI_HONOR_DOLLAR, NULL, NULL, textonly_p);
   store_pool = old_pool;
   return s;
   }
+if (textonly_p) *textonly_p = TRUE;
 return string;
 }
 
+const uschar *
+expand_cstring(const uschar * string)
+{ return expand_string_2(string, NULL); }
 
 uschar *
 expand_string(uschar * string)
-{
-return US expand_cstring(CUS string);
-}
+{ return US expand_string_2(CUS string, NULL); }
+
 
 
 
diff --git a/src/src/filter.c b/src/src/filter.c
index ad017e567..cc4af230e 100644
--- a/src/src/filter.c
+++ b/src/src/filter.c
@@ -1424,213 +1424,203 @@ Returns:         TRUE if the condition is met
 */
 
 static BOOL
-test_condition(condition_block *c, BOOL toplevel)
+test_condition(condition_block * c, BOOL toplevel)
 {
-BOOL yield = FALSE;
-const uschar *exp[2], * p, * pp;
+BOOL yield = FALSE, textonly_re;
+const uschar * exp[2], * p, * pp;
 int val[2];
-int i;
 
-if (c == NULL) return TRUE;  /* does this ever occur? */
+if (!c) return TRUE;  /* does this ever occur? */
 
 switch (c->type)
   {
   case cond_and:
-  yield = test_condition(c->left.c, FALSE) &&
-          *error_pointer == NULL &&
-          test_condition(c->right.c, FALSE);
-  break;
+    yield = test_condition(c->left.c, FALSE) &&
+	    *error_pointer == NULL &&
+	    test_condition(c->right.c, FALSE);
+    break;
 
   case cond_or:
-  yield = test_condition(c->left.c, FALSE) ||
-          (*error_pointer == NULL &&
-          test_condition(c->right.c, FALSE));
-  break;
+    yield = test_condition(c->left.c, FALSE) ||
+	    (*error_pointer == NULL &&
+	    test_condition(c->right.c, FALSE));
+    break;
 
-  /* The personal test is meaningless in a system filter. The tests are now in
-  a separate function (so Sieve can use them). However, an Exim filter does not
-  scan Cc: (hence the FALSE argument). */
+    /* The personal test is meaningless in a system filter. The tests are now in
+    a separate function (so Sieve can use them). However, an Exim filter does not
+    scan Cc: (hence the FALSE argument). */
 
   case cond_personal:
-  yield = f.system_filtering? FALSE : filter_personal(c->left.a, FALSE);
-  break;
+    yield = f.system_filtering? FALSE : filter_personal(c->left.a, FALSE);
+    break;
 
   case cond_delivered:
-  yield = filter_delivered;
-  break;
+    yield = filter_delivered;
+    break;
 
-  /* Only TRUE if a message is actually being processed; FALSE for address
-  testing and verification. */
+    /* Only TRUE if a message is actually being processed; FALSE for address
+    testing and verification. */
 
   case cond_errormsg:
-  yield = message_id[0] != 0 &&
-    (sender_address == NULL || sender_address[0] == 0);
-  break;
+    yield = message_id[0] != 0 &&
+      (sender_address == NULL || sender_address[0] == 0);
+    break;
 
-  /* Only FALSE if a message is actually being processed; TRUE for address
-  and filter testing and verification. */
+    /* Only FALSE if a message is actually being processed; TRUE for address
+    and filter testing and verification. */
 
   case cond_firsttime:
-  yield = filter_test != FTEST_NONE || message_id[0] == 0 || f.deliver_firsttime;
-  break;
+    yield = filter_test != FTEST_NONE || message_id[0] == 0 || f.deliver_firsttime;
+    break;
 
-  /* Only TRUE if a message is actually being processed; FALSE for address
-  testing and verification. */
+    /* Only TRUE if a message is actually being processed; FALSE for address
+    testing and verification. */
 
   case cond_manualthaw:
-  yield = message_id[0] != 0 && f.deliver_manual_thaw;
-  break;
+    yield = message_id[0] != 0 && f.deliver_manual_thaw;
+    break;
 
-  /* The foranyaddress condition loops through a list of addresses */
+    /* The foranyaddress condition loops through a list of addresses */
 
   case cond_foranyaddress:
-  p = c->left.u;
-  if (!(pp = expand_cstring(p)))
-    {
-    *error_pointer = string_sprintf("failed to expand \"%s\" in "
-      "filter file: %s", p, expand_string_message);
-    return FALSE;
-    }
+    p = c->left.u;
+    if (!(pp = expand_cstring(p)))
+      {
+      *error_pointer = string_sprintf("failed to expand \"%s\" in "
+	"filter file: %s", p, expand_string_message);
+      return FALSE;
+      }
 
-  yield = FALSE;
-  f.parse_allow_group = TRUE;     /* Allow group syntax */
+    yield = FALSE;
+    f.parse_allow_group = TRUE;     /* Allow group syntax */
 
-  while (*pp)
-    {
-    uschar *error;
-    int start, end, domain;
-    uschar * s;
+    while (*pp)
+      {
+      uschar *error;
+      int start, end, domain;
+      uschar * s;
 
-    p = parse_find_address_end(pp, FALSE);
-    s = string_copyn(pp, p - pp);
+      p = parse_find_address_end(pp, FALSE);
+      s = string_copyn(pp, p - pp);
 
-    filter_thisaddress =
-      parse_extract_address(s, &error, &start, &end, &domain, FALSE);
+      filter_thisaddress =
+	parse_extract_address(s, &error, &start, &end, &domain, FALSE);
 
-    if (filter_thisaddress)
-      {
-      if ((filter_test != FTEST_NONE && debug_selector != 0) ||
-          (debug_selector & D_filter) != 0)
-        {
-        indent();
-        debug_printf_indent("Extracted address %s\n", filter_thisaddress);
-        }
-      yield = test_condition(c->right.c, FALSE);
-      }
+      if (filter_thisaddress)
+	{
+	if ((filter_test != FTEST_NONE && debug_selector != 0) ||
+	    (debug_selector & D_filter) != 0)
+	  {
+	  indent();
+	  debug_printf_indent("Extracted address %s\n", filter_thisaddress);
+	  }
+	yield = test_condition(c->right.c, FALSE);
+	}
 
-    if (yield) break;
-    if (!*p) break;
-    pp = p + 1;
-    }
+      if (yield) break;
+      if (!*p) break;
+      pp = p + 1;
+      }
 
-  f.parse_allow_group = FALSE;      /* Reset group syntax flags */
-  f.parse_found_group = FALSE;
-  break;
+    f.parse_allow_group = FALSE;      /* Reset group syntax flags */
+    f.parse_found_group = FALSE;
+    break;
 
-  /* All other conditions have left and right values that need expanding;
-  on error, it doesn't matter what value is returned. */
+    /* All other conditions have left and right values that need expanding;
+    on error, it doesn't matter what value is returned. */
 
-  default:
-  p = c->left.u;
-  for (i = 0; i < 2; i++)
-    {
-    if (!(exp[i] = expand_cstring(p)))
+    default:
+    p = c->left.u;
+    for (int i = 0; i < 2; i++)
       {
-      *error_pointer = string_sprintf("failed to expand \"%s\" in "
-        "filter file: %s", p, expand_string_message);
-      return FALSE;
+      if (!(exp[i] = expand_string_2(p, &textonly_re)))
+	{
+	*error_pointer = string_sprintf("failed to expand \"%s\" in "
+	  "filter file: %s", p, expand_string_message);
+	return FALSE;
+	}
+      p = c->right.u;
       }
-    p = c->right.u;
-    }
 
-  /* Inner switch for the different cases */
-
-  switch(c->type)
-    {
-    case cond_is:
-    yield = strcmpic(exp[0], exp[1]) == 0;
-    break;
+    /* Inner switch for the different cases */
 
-    case cond_IS:
-    yield = Ustrcmp(exp[0], exp[1]) == 0;
-    break;
-
-    case cond_contains:
-    yield = strstric_c(exp[0], exp[1], FALSE) != NULL;
-    break;
+    switch(c->type)
+      {
+      case cond_is:
+	yield = strcmpic(exp[0], exp[1]) == 0;
+	break;
 
-    case cond_CONTAINS:
-    yield = Ustrstr(exp[0], exp[1]) != NULL;
-    break;
+      case cond_IS:
+	yield = Ustrcmp(exp[0], exp[1]) == 0;
+	break;
 
-    case cond_begins:
-    yield = strncmpic(exp[0], exp[1], Ustrlen(exp[1])) == 0;
-    break;
+      case cond_contains:
+	yield = strstric_c(exp[0], exp[1], FALSE) != NULL;
+	break;
 
-    case cond_BEGINS:
-    yield = Ustrncmp(exp[0], exp[1], Ustrlen(exp[1])) == 0;
-    break;
+      case cond_CONTAINS:
+	yield = Ustrstr(exp[0], exp[1]) != NULL;
+	break;
 
-    case cond_ends:
-    case cond_ENDS:
-      {
-      int len = Ustrlen(exp[1]);
-      const uschar *s = exp[0] + Ustrlen(exp[0]) - len;
-      yield = s < exp[0]
-	? FALSE
-	: (c->type == cond_ends ? strcmpic(s, exp[1]) : Ustrcmp(s, exp[1])) == 0;
-      }
-    break;
+      case cond_begins:
+	yield = strncmpic(exp[0], exp[1], Ustrlen(exp[1])) == 0;
+	break;
 
-    case cond_matches:
-    case cond_MATCHES:
-      {
-      const pcre2_code *re;
-      int err;
-      PCRE2_SIZE offset;
+      case cond_BEGINS:
+	yield = Ustrncmp(exp[0], exp[1], Ustrlen(exp[1])) == 0;
+	break;
 
-      if ((filter_test != FTEST_NONE && debug_selector != 0) ||
-	  (debug_selector & D_filter) != 0)
+      case cond_ends:
+      case cond_ENDS:
 	{
-	debug_printf_indent("Match expanded arguments:\n");
-	debug_printf_indent("  Subject = %s\n", exp[0]);
-	debug_printf_indent("  Pattern = %s\n", exp[1]);
+	int len = Ustrlen(exp[1]);
+	const uschar *s = exp[0] + Ustrlen(exp[0]) - len;
+	yield = s < exp[0]
+	  ? FALSE
+	  : (c->type == cond_ends ? strcmpic(s, exp[1]) : Ustrcmp(s, exp[1])) == 0;
+	break;
 	}
 
-      if (!(re = pcre2_compile((PCRE2_SPTR)exp[1], PCRE2_ZERO_TERMINATED,
-		  PCRE_COPT | (c->type == cond_matches ? PCRE2_CASELESS : 0),
-		  &err, &offset, pcre_cmp_ctx)))
+      case cond_matches:
+      case cond_MATCHES:
 	{
-	uschar errbuf[128];
-	pcre2_get_error_message(err, errbuf, sizeof(errbuf));
-	*error_pointer = string_sprintf("error while compiling "
-	  "regular expression \"%s\": %s at offset %ld",
-	  exp[1], errbuf, (long)offset);
-	return FALSE;
-	}
+	const pcre2_code * re;
+	mcs_flags flags = textonly_re ? MCS_CACHEABLE : MCS_NOFLAGS;
 
-      yield = regex_match_and_setup(re, exp[0], PCRE_EOPT, -1);
-      break;
-      }
+	if ((filter_test != FTEST_NONE && debug_selector != 0) ||
+	    (debug_selector & D_filter) != 0)
+	  {
+	  debug_printf_indent("Match expanded arguments:\n");
+	  debug_printf_indent("  Subject = %s\n", exp[0]);
+	  debug_printf_indent("  Pattern = %s\n", exp[1]);
+	  }
 
-    /* For above and below, convert the strings to numbers */
+	if (c->type == cond_matches) flags |= MCS_CASELESS;
+	if (!(re = regex_compile(exp[1], flags, error_pointer, pcre_gen_cmp_ctx)))
+	  return FALSE;
 
-    case cond_above:
-    case cond_below:
-    for (i = 0; i < 2; i++)
-      {
-      val[i] = get_number(exp[i], &yield);
-      if (!yield)
-        {
-        *error_pointer = string_sprintf("malformed numerical string \"%s\"",
-          exp[i]);
-        return FALSE;
-        }
+	yield = regex_match_and_setup(re, exp[0], PCRE_EOPT, -1);
+	break;
+	}
+
+      /* For above and below, convert the strings to numbers */
+
+      case cond_above:
+      case cond_below:
+	for (int i = 0; i < 2; i++)
+	  {
+	  val[i] = get_number(exp[i], &yield);
+	  if (!yield)
+	    {
+	    *error_pointer = string_sprintf("malformed numerical string \"%s\"",
+	      exp[i]);
+	    return FALSE;
+	    }
+	  }
+	yield = c->type == cond_above ? (val[0] > val[1]) : (val[0] < val[1]);
+	break;
       }
-    yield = (c->type == cond_above)? (val[0] > val[1]) : (val[0] < val[1]);
     break;
-    }
-  break;
   }
 
 if ((filter_test != FTEST_NONE && debug_selector != 0) ||
@@ -2356,7 +2346,7 @@ while (commands)
   commands = commands->next;
   }
 
-return filter_delivered? FF_DELIVERED : FF_NOTDELIVERED;
+return filter_delivered ? FF_DELIVERED : FF_NOTDELIVERED;
 }
 
 
diff --git a/src/src/functions.h b/src/src/functions.h
index 224666cb1..4caae346d 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -182,6 +182,10 @@ extern BOOL    cutthrough_predata(void);
 extern void    release_cutthrough_connection(const uschar *);
 
 extern void    daemon_go(void);
+#ifndef COMPILE_UTILITY
+extern ssize_t daemon_client_sockname(struct sockaddr_un *, uschar **);
+extern ssize_t daemon_notifier_sockname(struct sockaddr_un *);
+#endif
 
 #ifdef EXPERIMENTAL_DCC
 extern int     dcc_process(uschar **);
@@ -260,6 +264,7 @@ extern int     exp_bool(address_item *addr,
 extern BOOL    expand_check_condition(uschar *, uschar *, uschar *);
 extern uschar *expand_file_big_buffer(const uschar *);
 extern uschar *expand_string(uschar *);	/* public, cannot make const */
+extern const uschar *expand_string_2(const uschar *, BOOL *);
 extern const uschar *expand_cstring(const uschar *); /* ... so use this one */
 extern uschar *expand_getkeyed(const uschar *, const uschar *);
 
@@ -332,7 +337,7 @@ extern BOOL    macro_read_assignment(uschar *);
 extern uschar *macros_expand(int, int *, BOOL *);
 extern void    mainlog_close(void);
 #ifdef WITH_CONTENT_SCAN
-extern int     malware(const uschar *, int);
+extern int     malware(const uschar *, BOOL, int);
 extern int     malware_in_file(uschar *);
 extern void    malware_init(void);
 extern gstring * malware_show_supported(gstring *);
@@ -345,7 +350,7 @@ extern int     match_check_list(const uschar **, int, tree_node **, unsigned int
                  const uschar *, const uschar **);
 extern int     match_isinlist(const uschar *, const uschar **, int, tree_node **,
                  unsigned int *, int, BOOL, const uschar **);
-extern int     match_check_string(const uschar *, const uschar *, int, BOOL, BOOL, BOOL,
+extern int     match_check_string(const uschar *, const uschar *, int, mcs_flags,
                  const uschar **);
 
 extern void    message_start(void);
@@ -360,7 +365,7 @@ extern int     mime_acl_check(uschar *acl, FILE *f,
                  struct mime_boundary_context *, uschar **, uschar **);
 extern int     mime_decode(const uschar **);
 extern ssize_t mime_decode_base64(FILE *, FILE *, uschar *);
-extern int     mime_regex(const uschar **);
+extern int     mime_regex(const uschar **, BOOL);
 extern void    mime_set_anomaly(int);
 #endif
 extern uschar *moan_check_errorcopy(uschar *);
@@ -433,11 +438,14 @@ extern BOOL    receive_msg(BOOL);
 extern int_eximarith_t receive_statvfs(BOOL, int *);
 extern void    receive_swallow_smtp(void);
 #ifdef WITH_CONTENT_SCAN
-extern int     regex(const uschar **);
+extern int     regex(const uschar **, BOOL);
 #endif
+extern void    regex_at_daemon(const uschar *);
 extern BOOL    regex_match(const pcre2_code *, const uschar *, int, uschar **);
 extern BOOL    regex_match_and_setup(const pcre2_code *, const uschar *, int, int);
-extern const pcre2_code *regex_must_compile(const uschar *, BOOL, BOOL);
+extern const pcre2_code *regex_compile(const uschar *, mcs_flags, uschar **,
+		pcre2_compile_context *);
+extern const pcre2_code *regex_must_compile(const uschar *, mcs_flags, BOOL);
 extern void    retry_add_item(address_item *, uschar *, int);
 extern BOOL    retry_check_address(const uschar *, host_item *, uschar *, BOOL,
                  uschar **, uschar **);
@@ -1221,6 +1229,7 @@ pid_t pid;
 DEBUG(D_any) debug_printf("%s forking for %s\n", process_purpose, purpose);
 if ((pid = fork()) == 0)
   {
+  f.daemon_listen = FALSE;
   process_purpose = purpose;
   DEBUG(D_any) debug_printf("postfork: %s\n", purpose);
   }
diff --git a/src/src/globals.c b/src/src/globals.c
index ff246feb4..c95d24b47 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -234,6 +234,7 @@ struct global_flags f =
 	.continue_more          = FALSE,
 
 	.daemon_listen          = FALSE,
+	.daemon_scion           = FALSE,
 	.debug_daemon           = FALSE,
 	.deliver_firsttime      = FALSE,
 	.deliver_force          = FALSE,
@@ -391,7 +392,7 @@ BOOL    proxy_session          = FALSE;
 #endif
 
 #ifndef DISABLE_QUEUE_RAMP
-BOOL    queue_fast_ramp		= FALSE;
+BOOL    queue_fast_ramp		= TRUE;
 #endif
 BOOL    queue_list_requires_admin = TRUE;
 BOOL    queue_only             = FALSE;
@@ -1207,9 +1208,12 @@ uid_t   originator_uid;
 uschar *override_local_interfaces = NULL;
 uschar *override_pid_file_path = NULL;
 
+BOOL    panic_coredump	       = FALSE;
 pcre2_general_context * pcre_gen_ctx = NULL;
-pcre2_compile_context * pcre_cmp_ctx = NULL;
-pcre2_match_context * pcre_mtc_ctx = NULL;
+pcre2_compile_context * pcre_gen_cmp_ctx = NULL;
+pcre2_match_context * pcre_gen_mtc_ctx = NULL;
+pcre2_general_context * pcre_mlc_ctx = NULL;
+pcre2_compile_context * pcre_mlc_cmp_ctx = NULL;
 
 uschar *percent_hack_domains   = NULL;
 uschar *pid_file_path          = US PID_FILE_PATH
@@ -1313,6 +1317,7 @@ const pcre2_code *regex_SIZE         = NULL;
 #ifndef DISABLE_PIPE_CONNECT
 const pcre2_code *regex_EARLY_PIPE   = NULL;
 #endif
+int    regex_cachesize		     = 0;
 const pcre2_code *regex_ismsgid      = NULL;
 const pcre2_code *regex_smtp_code    = NULL;
 const uschar *regex_vars[REGEX_VARS];
@@ -1323,7 +1328,7 @@ const pcre2_code *regex_whitelisted_macro = NULL;
 uschar *regex_match_string     = NULL;
 #endif
 int     remote_delivery_count  = 0;
-int     remote_max_parallel    = 2;
+int     remote_max_parallel    = 4;
 uschar *remote_sort_domains    = NULL;
 int     retry_data_expire      = 7*24*60*60;
 int     retry_interval_max     = 24*60*60;
diff --git a/src/src/globals.h b/src/src/globals.h
index fe099e402..c9ef5e484 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -199,6 +199,7 @@ extern struct global_flags {
  BOOL   continue_more			:1; /* Flag more addresses waiting */
 
  BOOL   daemon_listen			:1; /* True if listening required */
+ BOOL   daemon_scion			:1; /* Ancestor proc is daemon, and not re-exec'd */
  BOOL   debug_daemon			:1; /* Debug the daemon process only */
  BOOL   deliver_firsttime		:1; /* True for first delivery attempt */
  BOOL   deliver_force			:1; /* TRUE if delivery was forced */
@@ -792,9 +793,12 @@ extern uid_t   originator_uid;         /* Uid of ditto */
 extern uschar *override_local_interfaces; /* Value of -oX argument */
 extern uschar *override_pid_file_path; /* Value of -oP argument */
 
+extern BOOL    panic_coredump;	       /* SEGV rather than exit, on LOG_PANIC_DIE */
 extern pcre2_general_context * pcre_gen_ctx;	/* pcre memory management */
-extern pcre2_compile_context * pcre_cmp_ctx;
-extern pcre2_match_context *   pcre_mtc_ctx;
+extern pcre2_compile_context * pcre_gen_cmp_ctx;
+extern pcre2_match_context *   pcre_gen_mtc_ctx;
+extern pcre2_general_context * pcre_mlc_ctx;
+extern pcre2_compile_context * pcre_mlc_cmp_ctx;
 
 extern uschar *percent_hack_domains;   /* Local domains for which '% operates */
 extern uschar *pid_file_path;          /* For writing daemon pids */
@@ -895,6 +899,7 @@ extern const pcre2_code  *regex_SIZE;        /* For recognizing SIZE settings */
 #ifndef DISABLE_PIPE_CONNECT
 extern const pcre2_code  *regex_EARLY_PIPE;  /* For recognizing PIPE_CONNCT */
 #endif
+extern int    regex_cachesize;		     /* number of entries */
 extern const pcre2_code  *regex_ismsgid;     /* Compiled r.e. for message ID */
 extern const pcre2_code  *regex_smtp_code;   /* For recognizing SMTP codes */
 extern const uschar *regex_vars[];           /* $regexN variables */
diff --git a/src/src/header.c b/src/src/header.c
index 898d8d5c4..7ef59ff53 100644
--- a/src/src/header.c
+++ b/src/src/header.c
@@ -368,7 +368,7 @@ Returns:         cond if the header exists and contains one of the strings;
 /* First we have a local subroutine to handle a single pattern */
 
 static BOOL
-one_pattern_match(uschar *name, int slen, BOOL has_addresses, uschar *pattern)
+one_pattern_match(uschar * name, int slen, BOOL has_addresses, uschar * pattern)
 {
 BOOL yield = FALSE;
 const pcre2_code *re = NULL;
@@ -376,7 +376,7 @@ const pcre2_code *re = NULL;
 /* If the pattern is a regex, compile it. Bomb out if compiling fails; these
 patterns are all constructed internally and should be valid. */
 
-if (*pattern == '^') re = regex_must_compile(pattern, TRUE, FALSE);
+if (*pattern == '^') re = regex_must_compile(pattern, MCS_CASELESS, FALSE);
 
 /* Scan for the required header(s) and scan each one */
 
@@ -443,7 +443,7 @@ return yield;
 /* The externally visible interface */
 
 BOOL
-header_match(uschar *name, BOOL has_addresses, BOOL cond, string_item *strings,
+header_match(uschar * name, BOOL has_addresses, BOOL cond, string_item * strings,
   int count, ...)
 {
 va_list ap;
diff --git a/src/src/host.c b/src/src/host.c
index e43b507e5..fed9f4b5f 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -363,80 +363,6 @@ while ((name = string_nextinlist(&list, &sep, NULL, 0)))
 }
 
 
-
-
-
-/*************************************************
-*        Extract port from address string        *
-*************************************************/
-
-/* In the -oMa and -oMi options, a host plus port is given as an IP address
-followed by a dot and a port number. This function decodes this.
-
-An alternative format for the -oMa and -oMi options is [ip address]:port which
-is what Exim uses for output, because it seems to becoming commonly used,
-whereas the dot form confuses some programs/people. So we recognize that form
-too.
-
-The spool file used to use the first form, but this breaks with a v4mapped ipv6
-hybrid, because the parsing here is not clever.  So for spool we now use the
-second form.
-
-Argument:
-  address    points to the string; if there is a port, the '.' in the string
-             is overwritten with zero to terminate the address; if the string
-             is in the [xxx]:ppp format, the address is shifted left and the
-             brackets are removed
-
-Returns:     0 if there is no port, else the port number. If there's a syntax
-             error, leave the incoming address alone, and return 0.
-*/
-
-int
-host_address_extract_port(uschar *address)
-{
-int port = 0;
-uschar *endptr;
-
-/* Handle the "bracketed with colon on the end" format */
-
-if (*address == '[')
-  {
-  uschar *rb = address + 1;
-  while (*rb != 0 && *rb != ']') rb++;
-  if (*rb++ == 0) return 0;        /* Missing ]; leave invalid address */
-  if (*rb == ':')
-    {
-    port = Ustrtol(rb + 1, &endptr, 10);
-    if (*endptr != 0) return 0;    /* Invalid port; leave invalid address */
-    }
-  else if (*rb != 0) return 0;     /* Bad syntax; leave invalid address */
-  memmove(address, address + 1, rb - address - 2);
-  rb[-2] = 0;
-  }
-
-/* Handle the "dot on the end" format */
-
-else
-  {
-  int skip = -3;                   /* Skip 3 dots in IPv4 addresses */
-  address--;
-  while (*(++address) != 0)
-    {
-    int ch = *address;
-    if (ch == ':') skip = 0;       /* Skip 0 dots in IPv6 addresses */
-      else if (ch == '.' && skip++ >= 0) break;
-    }
-  if (*address == 0) return 0;
-  port = Ustrtol(address + 1, &endptr, 10);
-  if (*endptr != 0) return 0;      /* Invalid port; leave invalid address */
-  *address = 0;
-  }
-
-return port;
-}
-
-
 /*************************************************
 *         Get port from a host item's name       *
 *************************************************/
diff --git a/src/src/host_address.c b/src/src/host_address.c
new file mode 100644
index 000000000..9e6f958be
--- /dev/null
+++ b/src/src/host_address.c
@@ -0,0 +1,80 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "exim.h"
+
+/*************************************************
+*        Extract port from address string        *
+*************************************************/
+
+/* In the spool file, and in the -oMa and -oMi options, a host plus port is
+given as an IP address followed by a dot and a port number. This function
+decodes this.
+
+An alternative format for the -oMa and -oMi options is [ip address]:port which
+is what Exim 4 uses for output, because it seems to becoming commonly used,
+whereas the dot form confuses some programs/people. So we recognize that form
+too.
+
+Argument:
+  address    points to the string; if there is a port, the '.' in the string
+             is overwritten with zero to terminate the address; if the string
+             is in the [xxx]:ppp format, the address is shifted left and the
+             brackets are removed
+
+Returns:     0 if there is no port, else the port number. If there's a syntax
+             error, leave the incoming address alone, and return 0.
+*/
+
+int
+host_address_extract_port(uschar * address)
+{
+int port = 0;
+uschar *endptr;
+
+/* Handle the "bracketed with colon on the end" format */
+
+if (*address == '[')
+  {
+  uschar *rb = address + 1;
+  while (*rb != 0 && *rb != ']') rb++;
+  if (*rb++ == 0) return 0;        /* Missing ]; leave invalid address */
+  if (*rb == ':')
+    {
+    port = Ustrtol(rb + 1, &endptr, 10);
+    if (*endptr != 0) return 0;    /* Invalid port; leave invalid address */
+    }
+  else if (*rb != 0) return 0;     /* Bad syntax; leave invalid address */
+  memmove(address, address + 1, rb - address - 2);
+  rb[-2] = 0;
+  }
+
+/* Handle the "dot on the end" format */
+
+else
+  {
+  int skip = -3;                   /* Skip 3 dots in IPv4 addresses */
+  address--;
+  while (*(++address) != 0)
+    {
+    int ch = *address;
+    if (ch == ':') skip = 0;       /* Skip 0 dots in IPv6 addresses */
+      else if (ch == '.' && skip++ >= 0) break;
+    }
+  if (*address == 0) return 0;
+  port = Ustrtol(address + 1, &endptr, 10);
+  if (*endptr != 0) return 0;      /* Invalid port; leave invalid address */
+  *address = 0;
+  }
+
+return port;
+}
+
+/* vi: aw ai sw=2
+*/
+/* End of host.c */
diff --git a/src/src/log.c b/src/src/log.c
index 8ca973f2d..a46d523db 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -1278,7 +1278,10 @@ if (flags & LOG_PANIC)
   /* Give up if the DIE flag is set */
 
   if ((flags & LOG_PANIC_DIE) != LOG_PANIC)
-    die(NULL, US"Unexpected failure, please try later");
+    if (panic_coredump)
+      kill(getpid(), SIGSEGV);	/* deliberate trap */
+    else
+      die(NULL, US"Unexpected failure, please try later");
   }
 }
 
diff --git a/src/src/macros.h b/src/src/macros.h
index fa89de12d..adbe6a267 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -1111,7 +1111,17 @@ should not be one active. */
 
 
 #define NOTIFIER_SOCKET_NAME	"exim_daemon_notify"
-#define NOTIFY_MSG_QRUN		1	/* Notify message types */
+/* Notify message types */
+#define NOTIFY_MSG_QRUN		1
 #define NOTIFY_QUEUE_SIZE_REQ	2
+#define NOTIFY_REGEX		3
+
+/* Flags for match_check_string() */
+typedef unsigned mcs_flags;
+#define MCS_NOFLAGS		0
+#define MCS_PARTIAL		BIT(0)	/* permit partial- search types */
+#define MCS_CASELESS		BIT(1)	/* caseless matching where possible */
+#define MCS_AT_SPECIAL		BIT(2)	/* recognize @, @[], etc. */
+#define MCS_CACHEABLE		BIT(3)	/* no dynamic expansions used for pattern */
 
 /* End of macros.h */
diff --git a/src/src/malware.c b/src/src/malware.c
index 4719a5d61..8b5ec27c4 100644
--- a/src/src/malware.c
+++ b/src/src/malware.c
@@ -299,39 +299,29 @@ return sock;
 }
 
 static const pcre2_code *
-m_pcre_compile(const uschar * re, uschar ** errstr)
+m_pcre_compile(const uschar * re, BOOL cacheable, uschar ** errstr)
 {
-int err;
-PCRE2_SIZE roffset;
-const pcre2_code * cre;
-
-if (!(cre = pcre2_compile((PCRE2_SPTR)re, PCRE2_ZERO_TERMINATED,
-	      PCRE_COPT, &err, &roffset, pcre_cmp_ctx)))
-  {
-  uschar errbuf[128];
-  pcre2_get_error_message(err, errbuf, sizeof(errbuf));
-  *errstr= string_sprintf("regular expression error in '%s': %s at offset %ld",
-      re, errbuf, (long)roffset);
-  }
-return cre;
+return regex_compile(re, cacheable ? MCS_CACHEABLE : MCS_NOFLAGS, errstr,
+		      pcre_gen_cmp_ctx);
 }
 
 uschar *
 m_pcre_exec(const pcre2_code * cre, uschar * text)
 {
 pcre2_match_data * md = pcre2_match_data_create(2, pcre_gen_ctx);
-int i = pcre2_match(cre, text, PCRE2_ZERO_TERMINATED, 0, 0, md, pcre_mtc_ctx);
+int i = pcre2_match(cre, text, PCRE2_ZERO_TERMINATED, 0, 0, md, pcre_gen_mtc_ctx);
 PCRE2_UCHAR * substr = NULL;
 PCRE2_SIZE slen;
 
 if (i >= 2)				/* Got it */
-  pcre2_substring_get_bynumber(md, 1, &substr, &slen);
+  pcre2_substring_get_bynumber(md, 1, &substr, &slen);	/* uses same ctx as md */
+/* pcre2_match_data_free(md);	gen ctx needs no free */
 return US substr;
 }
 
 static const pcre2_code *
 m_pcre_nextinlist(const uschar ** list, int * sep,
- char * listerr, uschar ** errstr)
+ BOOL cacheable, char * listerr, uschar ** errstr)
 {
 const uschar * list_ele;
 const pcre2_code * cre = NULL;
@@ -342,7 +332,7 @@ else
   {
   DEBUG(D_acl) debug_printf_indent("%15s%10s'%s'\n", "", "RE: ",
     string_printing(list_ele));
-  cre = m_pcre_compile(CUS list_ele, errstr);
+  cre = m_pcre_compile(CUS list_ele, cacheable, errstr);
   }
 return cre;
 }
@@ -568,6 +558,7 @@ is via malware(), or there's malware_in_file() used for testing/debugging.
 
 Arguments:
   malware_re    match condition for "malware="
+  cacheable	the RE did not use any dynamic elements during expansion
   scan_filename  the file holding the email to be scanned, if we're faking
 		this up for the -bmalware test, else NULL
   timeout	if nonzero, non-default timeoutl
@@ -576,11 +567,12 @@ Returns:        Exim message processing code (OK, FAIL, DEFER, ...)
                 where true means malware was found (condition applies)
 */
 static int
-malware_internal(const uschar * malware_re, const uschar * scan_filename,
-  int timeout)
+malware_internal(const uschar * malware_re, BOOL cacheable,
+  const uschar * scan_filename, int timeout)
 {
 int sep = 0;
 const uschar *av_scanner_work = av_scanner;
+BOOL av_scanner_textonly;
 uschar *scanner_name;
 unsigned long mbox_size;
 FILE *mbox_file;
@@ -607,30 +599,30 @@ the name), so we can close it right away.  Get the directory too. */
 eml_dir = string_copyn(eml_filename, Ustrrchr(eml_filename, '/') - eml_filename);
 
 /* parse 1st option */
-if (strcmpic(malware_re, US"false") == 0  ||  Ustrcmp(malware_re,"0") == 0)
+if (strcmpic(malware_re, US"false") == 0  ||  Ustrcmp(malware_re, "0") == 0)
   return FAIL;		/* explicitly no matching */
 
 /* special cases (match anything except empty) */
-if (  strcmpic(malware_re,US"true") == 0
-   || Ustrcmp(malware_re,"*") == 0
-   || Ustrcmp(malware_re,"1") == 0
+if (  strcmpic(malware_re, US"true") == 0
+   || Ustrcmp(malware_re, "*") == 0
+   || Ustrcmp(malware_re, "1") == 0
    )
   {
   if (  !malware_default_re
-     && !(malware_default_re = m_pcre_compile(malware_regex_default, &errstr)))
+     && !(malware_default_re = m_pcre_compile(malware_regex_default, FALSE, &errstr)))
     return malware_panic_defer(errstr);
   malware_re = malware_regex_default;
   re = malware_default_re;
   }
 
 /* compile the regex, see if it works */
-else if (!(re = m_pcre_compile(malware_re, &errstr)))
+else if (!(re = m_pcre_compile(malware_re, cacheable, &errstr)))
   return malware_panic_defer(errstr);
 
 /* if av_scanner starts with a dollar, expand it first */
 if (*av_scanner == '$')
   {
-  if (!(av_scanner_work = expand_string(av_scanner)))
+  if (!(av_scanner_work = expand_string_2(av_scanner, &av_scanner_textonly)))
     return malware_panic_defer(
 	 string_sprintf("av_scanner starts with $, but expansion failed: %s",
 	 expand_string_message));
@@ -641,6 +633,8 @@ if (*av_scanner == '$')
   malware_name = NULL;
   malware_ok = FALSE;
   }
+else
+  av_scanner_textonly = TRUE;
 
 /* Do not scan twice (unless av_scanner is dynamic). */
 if (!malware_ok)
@@ -745,13 +739,11 @@ if (!malware_ok)
     case M_FPROT6D: /* "f-prot6d" scanner type ----------------------------------- */
       {
       int bread;
-      uschar * e;
-      uschar * linebuffer;
-      uschar * scanrequest;
+      uschar * e, * linebuffer, * scanrequest;
       uschar av_buffer[1024];
 
-      if ((!fprot6d_re_virus && !(fprot6d_re_virus = m_pcre_compile(fprot6d_re_virus_str, &errstr)))
-        || (!fprot6d_re_error && !(fprot6d_re_error = m_pcre_compile(fprot6d_re_error_str, &errstr))))
+      if ((!fprot6d_re_virus && !(fprot6d_re_virus = m_pcre_compile(fprot6d_re_virus_str, FALSE, &errstr)))
+        || (!fprot6d_re_error && !(fprot6d_re_error = m_pcre_compile(fprot6d_re_error_str, FALSE, &errstr))))
         return malware_panic_defer(errstr);
 
       scanrequest = string_sprintf("SCAN FILE %s\n", eml_filename);
@@ -921,7 +913,7 @@ badseek:  err = errno;
 
 	/* set up match regex */
 	if (!drweb_re)
-	  drweb_re = m_pcre_compile(drweb_re_str, &errstr);
+	  drweb_re = m_pcre_compile(drweb_re_str, FALSE, &errstr);
 
 	/* read and concatenate virus names into one string */
 	for (int i = 0; i < drweb_vnum; i++)
@@ -945,7 +937,7 @@ badseek:  err = errno;
 
 	  /* try matcher on the line, grab substring */
 	  result = pcre2_match(drweb_re, (PCRE2_SPTR)tmpbuf, PCRE2_ZERO_TERMINATED,
-				0, 0, md, pcre_mtc_ctx);
+				0, 0, md, pcre_gen_mtc_ctx);
 	  if (result >= 2)
 	    {
 	    PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
@@ -959,6 +951,7 @@ badseek:  err = errno;
 	      g = string_catn(g, US ovec[2], ovec[3] - ovec[2]);
 	      }
 	    }
+	  /* pcre2_match_data_free(md);	gen ctx needs no free */
 	  }
 	  malware_name = string_from_gstring(g);
 	}
@@ -1099,7 +1092,7 @@ badseek:  err = errno;
       /* set up match */
       /* todo also SUSPICION\t */
       if (!fsec_re)
-	fsec_re = m_pcre_compile(fsec_re_str, &errstr);
+	fsec_re = m_pcre_compile(fsec_re_str, FALSE, &errstr);
 
       /* read report, linewise. Apply a timeout as the Fsecure daemon
       sometimes wants an answer to "PING" but they won't tell us what */
@@ -1223,12 +1216,12 @@ badseek:  err = errno;
 	    /* set up match regex, depends on retcode */
 	    if (kav_rc == 3)
 	      {
-	      if (!kav_re_sus) kav_re_sus = m_pcre_compile(kav_re_sus_str, &errstr);
+	      if (!kav_re_sus) kav_re_sus = m_pcre_compile(kav_re_sus_str, FALSE, &errstr);
 	      kav_re = kav_re_sus;
 	      }
 	    else
 	      {
-	      if (!kav_re_inf) kav_re_inf = m_pcre_compile(kav_re_inf_str, &errstr);
+	      if (!kav_re_inf) kav_re_inf = m_pcre_compile(kav_re_inf_str, FALSE, &errstr);
 	      kav_re = kav_re_inf;
 	      }
 
@@ -1277,13 +1270,13 @@ badseek:  err = errno;
 	return m_panic_defer(scanent, NULL, errstr);
 
       /* find scanner output trigger */
-      cmdline_trigger_re = m_pcre_nextinlist(&av_scanner_work, &sep,
+      cmdline_trigger_re = m_pcre_nextinlist(&av_scanner_work, &sep, av_scanner_textonly,
 				"missing trigger specification", &errstr);
       if (!cmdline_trigger_re)
 	return m_panic_defer(scanent, NULL, errstr);
 
       /* find scanner name regex */
-      cmdline_regex_re = m_pcre_nextinlist(&av_scanner_work, &sep,
+      cmdline_regex_re = m_pcre_nextinlist(&av_scanner_work, &sep, av_scanner_textonly,
 			  "missing virus name regex specification", &errstr);
       if (!cmdline_regex_re)
 	return m_panic_defer(scanent, NULL, errstr);
@@ -1906,13 +1899,13 @@ badseek:  err = errno;
 	string_printing(sockline_scanner));
 
       /* find scanner output trigger */
-      sockline_trig_re = m_pcre_nextinlist(&av_scanner_work, &sep,
+      sockline_trig_re = m_pcre_nextinlist(&av_scanner_work, &sep, av_scanner_textonly,
 				"missing trigger specification", &errstr);
       if (!sockline_trig_re)
 	return m_panic_defer_3(scanent, NULL, errstr, malware_daemon_ctx.sock);
 
       /* find virus name regex */
-      sockline_name_re = m_pcre_nextinlist(&av_scanner_work, &sep,
+      sockline_name_re = m_pcre_nextinlist(&av_scanner_work, &sep, av_scanner_textonly,
 			  "missing virus name regex specification", &errstr);
       if (!sockline_name_re)
 	return m_panic_defer_3(scanent, NULL, errstr, malware_daemon_ctx.sock);
@@ -2043,11 +2036,11 @@ badseek:  err = errno;
        */
 
       if (  (  !ava_re_clean
-            && !(ava_re_clean = m_pcre_compile(ava_re_clean_str, &errstr)))
+            && !(ava_re_clean = m_pcre_compile(ava_re_clean_str, FALSE, &errstr)))
 	 || (  !ava_re_virus
-	    && !(ava_re_virus = m_pcre_compile(ava_re_virus_str, &errstr)))
+	    && !(ava_re_virus = m_pcre_compile(ava_re_virus_str, FALSE, &errstr)))
 	 || (  !ava_re_error
-	    && !(ava_re_error = m_pcre_compile(ava_re_error_str, &errstr)))
+	    && !(ava_re_error = m_pcre_compile(ava_re_error_str, FALSE, &errstr)))
 	 )
 	return malware_panic_defer(errstr);
 
@@ -2209,15 +2202,16 @@ filename; it's a wrapper around the malware_file function.
 
 Arguments:
   malware_re  match condition for "malware="
+  cacheable   the RE did not use any dynamic elements during expansion
   timeout     if nonzero, timeout in seconds
 
 Returns:      Exim message processing code (OK, FAIL, DEFER, ...)
               where true means malware was found (condition applies)
 */
 int
-malware(const uschar * malware_re, int timeout)
+malware(const uschar * malware_re, BOOL cacheable, int timeout)
 {
-int ret = malware_internal(malware_re, NULL, timeout);
+int ret = malware_internal(malware_re, cacheable, NULL, timeout);
 
 if (ret == DEFER) av_failed = TRUE;
 return ret;
@@ -2257,7 +2251,7 @@ recipients_list = NULL;
 receive_add_recipient(US"malware-victim@example.net", -1);
 f.enable_dollar_recipients = TRUE;
 
-ret = malware_internal(US"*", eml_filename, 0);
+ret = malware_internal(US"*", TRUE, eml_filename, 0);
 
 Ustrncpy(spooled_message_id, message_id, sizeof(spooled_message_id));
 spool_mbox_ok = 1;
@@ -2278,35 +2272,35 @@ void
 malware_init(void)
 {
 if (!malware_default_re)
-  malware_default_re = regex_must_compile(malware_regex_default, FALSE, TRUE);
+  malware_default_re = regex_must_compile(malware_regex_default, MCS_NOFLAGS, TRUE);
 
 #ifndef DISABLE_MAL_DRWEB
 if (!drweb_re)
-  drweb_re = regex_must_compile(drweb_re_str, FALSE, TRUE);
+  drweb_re = regex_must_compile(drweb_re_str, MCS_NOFLAGS, TRUE);
 #endif
 #ifndef DISABLE_MAL_FSECURE
 if (!fsec_re)
-  fsec_re = regex_must_compile(fsec_re_str, FALSE, TRUE);
+  fsec_re = regex_must_compile(fsec_re_str, MCS_NOFLAGS, TRUE);
 #endif
 #ifndef DISABLE_MAL_KAV
 if (!kav_re_sus)
-  kav_re_sus = regex_must_compile(kav_re_sus_str, FALSE, TRUE);
+  kav_re_sus = regex_must_compile(kav_re_sus_str, MCS_NOFLAGS, TRUE);
 if (!kav_re_inf)
-  kav_re_inf = regex_must_compile(kav_re_inf_str, FALSE, TRUE);
+  kav_re_inf = regex_must_compile(kav_re_inf_str, MCS_NOFLAGS, TRUE);
 #endif
 #ifndef DISABLE_MAL_AVAST
 if (!ava_re_clean)
-  ava_re_clean = regex_must_compile(ava_re_clean_str, FALSE, TRUE);
+  ava_re_clean = regex_must_compile(ava_re_clean_str, MCS_NOFLAGS, TRUE);
 if (!ava_re_virus)
-  ava_re_virus = regex_must_compile(ava_re_virus_str, FALSE, TRUE);
+  ava_re_virus = regex_must_compile(ava_re_virus_str, MCS_NOFLAGS, TRUE);
 if (!ava_re_error)
-  ava_re_error = regex_must_compile(ava_re_error_str, FALSE, TRUE);
+  ava_re_error = regex_must_compile(ava_re_error_str, MCS_NOFLAGS, TRUE);
 #endif
 #ifndef DISABLE_MAL_FFROT6D
 if (!fprot6d_re_error)
-  fprot6d_re_error = regex_must_compile(fprot6d_re_error_str, FALSE, TRUE);
+  fprot6d_re_error = regex_must_compile(fprot6d_re_error_str, MCS_NOFLAGS, TRUE);
 if (!fprot6d_re_virus)
-  fprot6d_re_virus = regex_must_compile(fprot6d_re_virus_str, FALSE, TRUE);
+  fprot6d_re_virus = regex_must_compile(fprot6d_re_virus_str, MCS_NOFLAGS, TRUE);
 #endif
 }
 
diff --git a/src/src/match.c b/src/src/match.c
index 2e4bff078..b4a0352ee 100644
--- a/src/src/match.c
+++ b/src/src/match.c
@@ -19,9 +19,7 @@ typedef struct check_string_block {
   const uschar *origsubject;           /* caseful; keep these two first, in */
   const uschar *subject;               /* step with the block below */
   int    expand_setup;
-  BOOL   use_partial;
-  BOOL   caseless;
-  BOOL   at_is_special;
+  mcs_flags flags;			/* MCS_* defs in macros.h */
 } check_string_block;
 
 
@@ -32,7 +30,7 @@ typedef struct check_address_block {
   const uschar *origaddress;         /* caseful; keep these two first, in */
   uschar *address;                   /* step with the block above */
   int    expand_setup;
-  BOOL   caseless;
+  mcs_flags flags;			/* MCS_CASELESS, MCS_TEXTONLY_RE */
 } check_address_block;
 
 
@@ -93,9 +91,10 @@ Returns:       OK    if matched
 */
 
 static int
-check_string(void *arg, const uschar *pattern, const uschar **valueptr, uschar **error)
+check_string(void * arg, const uschar * pattern, const uschar ** valueptr,
+  uschar ** error)
 {
-const check_string_block *cb = arg;
+const check_string_block * cb = arg;
 int search_type, partial, affixlen, starflags;
 int expand_setup = cb->expand_setup;
 const uschar * affix, * opts;
@@ -128,7 +127,8 @@ required. */
 
 if (pattern[0] == '^')
   {
-  const pcre2_code * re = regex_must_compile(pattern, cb->caseless, FALSE);
+  const pcre2_code * re = regex_must_compile(pattern,
+      cb->flags & (MCS_CACHEABLE | MCS_CASELESS), FALSE);
   if (expand_setup < 0
       ? !regex_match(re, s, -1, NULL)
       : !regex_match_and_setup(re, s, 0, expand_setup)
@@ -147,7 +147,7 @@ if (pattern[0] == '*')
 
   patlen = Ustrlen(++pattern);
   if (patlen > slen) return FAIL;
-  if (cb->caseless
+  if (cb->flags & MCS_CASELESS
       ? strncmpic(s + slen - patlen, pattern, patlen) != 0
       : Ustrncmp(s + slen - patlen, pattern, patlen) != 0)
     return FAIL;
@@ -166,7 +166,7 @@ the primary host name - implement this by changing the pattern. For the other
 cases we have to do some more work. If we don't recognize a special pattern,
 just fall through - the match will fail. */
 
-if (cb->at_is_special && pattern[0] == '@')
+if (cb->flags & MCS_AT_SPECIAL && pattern[0] == '@')
   {
   if (pattern[1] == 0)
     {
@@ -260,10 +260,10 @@ NOT_AT_SPECIAL:
 
 if ((semicolon = Ustrchr(pattern, ';')) == NULL)
   {
-  if (cb->caseless ? strcmpic(s, pattern) != 0 : Ustrcmp(s, pattern) != 0)
+  if (cb->flags & MCS_CASELESS ? strcmpic(s, pattern) != 0 : Ustrcmp(s, pattern) != 0)
     return FAIL;
-  if (expand_setup >= 0) expand_nmax = expand_setup;	/* Original code!   $0 gets the matched subject */
-  if (valueptr) *valueptr = pattern;	/* "value" gets the pattern */
+  if (expand_setup >= 0) expand_nmax = expand_setup;	/* $0 gets the matched subject */
+  if (valueptr) *valueptr = pattern;			/* "value" gets the pattern */
   return OK;
   }
 
@@ -280,7 +280,7 @@ if (search_type < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE, "%s",
 /* Partial matching is not appropriate for certain lookups (e.g. when looking
 up user@domain for sender rejection). There's a flag to disable it. */
 
-if (!cb->use_partial) partial = -1;
+if (!(cb->flags & MCS_PARTIAL)) partial = -1;
 
 /* Set the parameters for the three different kinds of lookup. */
 
@@ -316,9 +316,10 @@ Arguments:
   s            the subject string to be checked
   pattern      the pattern to check it against
   expand_setup expansion setup option (see check_string())
-  use_partial  if FALSE, override any partial- search types
-  caseless     TRUE for caseless matching where possible
-  at_is_special TRUE to recognize @, @[], etc.
+  flags
+   use_partial  if FALSE, override any partial- search types
+   caseless     TRUE for caseless matching where possible
+   at_is_special TRUE to recognize @, @[], etc.
   valueptr     if not NULL, and a file lookup was done, return the result
                  here instead of discarding it; else set it to point to NULL
 
@@ -328,16 +329,14 @@ Returns:       OK    if matched
 */
 
 int
-match_check_string(const uschar *s, const uschar *pattern, int expand_setup,
-  BOOL use_partial, BOOL caseless, BOOL at_is_special, const uschar **valueptr)
+match_check_string(const uschar * s, const uschar * pattern, int expand_setup,
+  mcs_flags flags, const uschar ** valueptr)
 {
 check_string_block cb;
 cb.origsubject = s;
-cb.subject = caseless ? string_copylc(s) : string_copy(s);
+cb.subject = flags & MCS_CASELESS ? string_copylc(s) : string_copy(s);
 cb.expand_setup = expand_setup;
-cb.use_partial = use_partial;
-cb.caseless = caseless;
-cb.at_is_special = at_is_special;
+cb.flags = flags;
 return check_string(&cb, pattern, valueptr, NULL);
 }
 
@@ -364,14 +363,9 @@ switch(type)
   {
   case MCL_STRING:
   case MCL_DOMAIN:
-  case MCL_LOCALPART:
-    return ((check_string_block *)arg)->subject;
-
-  case MCL_HOST:
-    return ((check_host_block *)arg)->host_address;
-
-  case MCL_ADDRESS:
-    return ((check_address_block *)arg)->address;
+  case MCL_LOCALPART:	return ((check_string_block *)arg)->subject;
+  case MCL_HOST:	return ((check_host_block *)arg)->host_address;
+  case MCL_ADDRESS:	return ((check_address_block *)arg)->address;
   }
 return US"";  /* In practice, should never happen */
 }
@@ -438,6 +432,7 @@ BOOL include_unknown = FALSE, ignore_unknown = FALSE,
 const uschar *list;
 uschar *sss;
 uschar *ot = NULL;
+BOOL textonly_re;
 
 /* Save time by not scanning for the option name when we don't need it. */
 
@@ -465,6 +460,7 @@ if (type >= MCL_NOEXPAND)
   {
   list = *listptr;
   type -= MCL_NOEXPAND;       /* Remove the "no expand" flag */
+  textonly_re = TRUE;
   }
 else
   {
@@ -475,11 +471,11 @@ else
     {
     check_string_block *cb = (check_string_block *)arg;
     deliver_domain = string_copy(cb->subject);
-    list = expand_cstring(*listptr);
+    list = expand_string_2(*listptr, &textonly_re);
     deliver_domain = NULL;
     }
   else
-    list = expand_cstring(*listptr);
+    list = expand_string_2(*listptr, &textonly_re);
 
   if (!list)
     {
@@ -495,6 +491,15 @@ else
     }
   }
 
+if (textonly_re) switch (type)
+  {
+  case MCL_STRING:
+  case MCL_DOMAIN:
+  case MCL_LOCALPART: ((check_string_block *)arg)->flags |= MCS_CACHEABLE; break;
+  case MCL_HOST:     ((check_host_block *)arg)->flags |= MCS_CACHEABLE; break;
+  case MCL_ADDRESS: ((check_address_block *)arg)->flags |= MCS_CACHEABLE; break;
+  }
+
 /* For an unnamed list, use the expanded version in comments */
 #define LIST_LIMIT_PR 2048
 
@@ -530,7 +535,7 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
 
       if (at)
         Ustrncpy(cb->address, cb->origaddress, at - cb->origaddress);
-      cb->caseless = FALSE;
+      cb->flags &= ~MCS_CASELESS;
       continue;
       }
     }
@@ -543,7 +548,7 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
       {
       check_string_block *cb = (check_string_block *)arg;
       Ustrcpy(US cb->subject, cb->origsubject);
-      cb->caseless = FALSE;
+      cb->flags &= ~MCS_CASELESS;
       continue;
       }
     }
@@ -958,15 +963,13 @@ unsigned int *local_cache_bits = cache_bits;
 check_string_block cb;
 cb.origsubject = s;
 cb.subject = caseless ? string_copylc(s) : string_copy(s);
-cb.at_is_special = FALSE;
+cb.flags = caseless ? MCS_PARTIAL+MCS_CASELESS : MCS_PARTIAL;
 switch (type & ~MCL_NOEXPAND)
   {
-  case MCL_DOMAIN:	cb.at_is_special = TRUE;	/*FALLTHROUGH*/
+  case MCL_DOMAIN:	cb.flags |= MCS_AT_SPECIAL;	/*FALLTHROUGH*/
   case MCL_LOCALPART:	cb.expand_setup = 0;				break;
   default:		cb.expand_setup = sep > UCHAR_MAX ? 0 : -1;	break;
   }
-cb.use_partial = TRUE;
-cb.caseless = caseless;
 if (valueptr) *valueptr = NULL;
 return  match_check_list(listptr, sep, anchorptr, &local_cache_bits,
   check_string, &cb, type, s, valueptr);
@@ -1003,7 +1006,8 @@ Returns:         OK     for a match
 */
 
 static int
-check_address(void *arg, const uschar *pattern, const uschar **valueptr, uschar **error)
+check_address(void * arg, const uschar * pattern, const uschar ** valueptr,
+  uschar ** error)
 {
 check_address_block * cb = (check_address_block *)arg;
 check_string_block csb;
@@ -1026,7 +1030,7 @@ sdomain = Ustrrchr(subject, '@');
 /* The only case where a subject may not have a domain is if the subject is
 empty. Otherwise, a subject with no domain is a serious configuration error. */
 
-if (sdomain == NULL && *subject != 0)
+if (!sdomain && *subject)
   {
   log_write(0, LOG_MAIN|LOG_PANIC, "no @ found in the subject of an "
     "address list match: subject=\"%s\" pattern=\"%s\"", subject, pattern);
@@ -1037,14 +1041,14 @@ if (sdomain == NULL && *subject != 0)
 This may be the empty address. */
 
 if (*pattern == '^')
-  return match_check_string(subject, pattern, cb->expand_setup, TRUE,
-    cb->caseless, FALSE, NULL);
+  return match_check_string(subject, pattern, cb->expand_setup,
+	    cb->flags | MCS_PARTIAL, NULL);
 
 /* Handle a pattern that is just a lookup. Skip over possible lookup names
 (letters, digits, hyphens). Skip over a possible * or *@ at the end. Then we
 must have a semicolon for it to be a lookup. */
 
-for (s = pattern; isalnum(*s) || *s == '-'; s++);
+for (s = pattern; isalnum(*s) || *s == '-'; s++) ;
 if (*s == '*') s++;
 if (*s == '@') s++;
 
@@ -1057,8 +1061,7 @@ if (*s == ';')
   if (Ustrncmp(pattern, "partial-", 8) == 0)
     log_write(0, LOG_MAIN|LOG_PANIC, "partial matching is not applicable to "
       "whole-address lookups: ignored \"partial-\" in \"%s\"", pattern);
-  return match_check_string(subject, pattern, -1, FALSE, cb->caseless, FALSE,
-    valueptr);
+  return match_check_string(subject, pattern, -1, cb->flags, valueptr);
   }
 
 /* For the remaining cases, an empty subject matches only an empty pattern,
@@ -1085,19 +1088,20 @@ if (pattern[0] == '@' && pattern[1] == '@')
     {
     int sep = 0;
 
-    if ((rc = match_check_string(key, pattern + 2, -1, TRUE, FALSE, FALSE,
-      CUSS &list)) != OK) return rc;
+    if ((rc = match_check_string(key, pattern + 2, -1, MCS_PARTIAL, CUSS &list))
+	!= OK)
+      return rc;
 
     /* Check for chaining from the last item; set up the next key if one
     is found. */
 
     ss = Ustrrchr(list, ':');
-    if (ss == NULL) ss = list; else ss++;
-    while (isspace(*ss)) ss++;
+    if (!ss) ss = list; else ss++;
+    Uskip_whitespace(&ss);
     if (*ss == '>')
       {
       *ss++ = 0;
-      while (isspace(*ss)) ss++;
+      Uskip_whitespace(&ss);
       key = string_copy(ss);
       }
     else key = NULL;
@@ -1117,8 +1121,7 @@ if (pattern[0] == '@' && pattern[1] == '@')
       else local_yield = OK;
 
       *sdomain = 0;
-      rc = match_check_string(subject, ss, -1, TRUE, cb->caseless, FALSE,
-        valueptr);
+      rc = match_check_string(subject, ss, -1, cb->flags + MCS_PARTIAL, valueptr);
       *sdomain = '@';
 
       switch(rc)
@@ -1148,8 +1151,7 @@ if (pattern[0] == '@' && pattern[1] == '@')
 /* We get here if the pattern is not a lookup or a regular expression. If it
 contains an @ there is both a local part and a domain. */
 
-pdomain = Ustrrchr(pattern, '@');
-if (pdomain != NULL)
+if ((pdomain = Ustrrchr(pattern, '@')))
   {
   int pllen, sllen;
 
@@ -1177,7 +1179,7 @@ if (pdomain != NULL)
     {
     int cllen = pllen - 1;
     if (sllen < cllen) return FAIL;
-    if (cb->caseless
+    if (cb->flags & MCS_CASELESS
         ? strncmpic(subject+sllen-cllen, pattern + 1, cllen) != 0
         : Ustrncmp(subject+sllen-cllen, pattern + 1, cllen) != 0)
         return FAIL;
@@ -1192,7 +1194,7 @@ if (pdomain != NULL)
   else
     {
     if (sllen != pllen) return FAIL;
-    if (cb->caseless
+    if (cb->flags & MCS_CASELESS
         ? strncmpic(subject, pattern, sllen) != 0
 	: Ustrncmp(subject, pattern, sllen) != 0) return FAIL;
     }
@@ -1205,18 +1207,17 @@ original code read as follows:
 
   return match_check_string(sdomain + 1,
       pdomain ? pdomain + 1 : pattern,
-      cb->expand_setup + expand_inc, TRUE, cb->caseless, TRUE, NULL);
+      cb->expand_setup + expand_inc, cb->flags, NULL);
 
 This supported only literal domains and *.x.y patterns. In order to allow for
-named domain lists (so that you can right, for example, "senders=+xxxx"), it
+named domain lists (so that you can write, for example, "senders=+xxxx"), it
 was changed to use the list scanning function. */
 
 csb.origsubject = sdomain + 1;
-csb.subject = cb->caseless ? string_copylc(sdomain+1) : string_copy(sdomain+1);
+csb.subject = cb->flags & MCS_CASELESS
+  ? string_copylc(sdomain+1) : string_copy(sdomain+1);
 csb.expand_setup = cb->expand_setup + expand_inc;
-csb.use_partial = TRUE;
-csb.caseless = cb->caseless;
-csb.at_is_special = TRUE;
+csb.flags = MCS_PARTIAL | MCS_AT_SPECIAL | cb->flags & MCS_CASELESS;
 
 listptr = pdomain ? pdomain + 1 : pattern;
 if (valueptr) *valueptr = NULL;
@@ -1321,10 +1322,10 @@ if (expand_setup == 0)
 ab.origaddress = address;
 /* ab.address is above */
 ab.expand_setup = expand_setup;
-ab.caseless = caseless;
+ab.flags = caseless ? MCS_CASELESS : 0;
 
 return match_check_list(listptr, sep, &addresslist_anchor, &local_cache_bits,
-  check_address, &ab, MCL_ADDRESS + (expand? 0:MCL_NOEXPAND), address,
+  check_address, &ab, MCL_ADDRESS + (expand ? 0 : MCL_NOEXPAND), address,
     valueptr);
 }
 
diff --git a/src/src/queue.c b/src/src/queue.c
index 4bdd6fb14..6e47d2c8a 100644
--- a/src/src/queue.c
+++ b/src/src/queue.c
@@ -423,11 +423,11 @@ if (!recurse)
 /* If deliver_selectstring is a regex, compile it. */
 
 if (deliver_selectstring && f.deliver_selectstring_regex)
-  selectstring_regex = regex_must_compile(deliver_selectstring, TRUE, FALSE);
+  selectstring_regex = regex_must_compile(deliver_selectstring, MCS_CASELESS, FALSE);
 
 if (deliver_selectstring_sender && f.deliver_selectstring_sender_regex)
   selectstring_regex_sender =
-    regex_must_compile(deliver_selectstring_sender, TRUE, FALSE);
+    regex_must_compile(deliver_selectstring_sender, MCS_CASELESS, FALSE);
 
 /* If the spool is split into subdirectories, we want to process it one
 directory at a time, so as to spread out the directory scanning and the
@@ -1562,19 +1562,9 @@ memcpy(buf+1, msgid, MESSAGE_ID_LENGTH+1);
 if ((fd = socket(AF_UNIX, SOCK_DGRAM, 0)) >= 0)
   {
   struct sockaddr_un sa_un = {.sun_family = AF_UNIX};
+  ssize_t len = daemon_notifier_sockname(&sa_un);
 
-#ifdef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
-  int len = offsetof(struct sockaddr_un, sun_path) + 1
-    + snprintf(sa_un.sun_path+1, sizeof(sa_un.sun_path)-1, "%s",
-		expand_string(notifier_socket));
-  sa_un.sun_path[0] = 0;
-#else
-  int len = offsetof(struct sockaddr_un, sun_path)
-    + snprintf(sa_un.sun_path, sizeof(sa_un.sun_path), "%s",
-		expand_string(notifier_socket));
-#endif
-
-  if (sendto(fd, buf, sizeof(buf), 0, (struct sockaddr *)&sa_un, len) < 0)
+  if (sendto(fd, buf, sizeof(buf), 0, (struct sockaddr *)&sa_un, (socklen_t)len) < 0)
     DEBUG(D_queue_run)
       debug_printf("%s: sendto %s\n", __FUNCTION__, strerror(errno));
   close(fd);
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 06bc50fd8..5068dc60e 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -243,6 +243,7 @@ static optionlist optionlist_config[] = {
 #ifdef LOOKUP_ORACLE
   { "oracle_servers",           opt_stringptr,   {&oracle_servers} },
 #endif
+  { "panic_coredump",           opt_bool,        {&panic_coredump} },
   { "percent_hack_domains",     opt_stringptr,   {&percent_hack_domains} },
 #ifdef EXIM_PERL
   { "perl_at_start",            opt_bool,        {&opt_perl_at_start} },
@@ -2667,8 +2668,8 @@ switch(ol->type & opt_mask)
     break;
 
   case opt_bit:
-    printf("%s%s\n", ((*((int *)value)) & (1 << ((ol->type >> 16) & 31)))?
-      "" : "no_", name);
+    printf("%s%s\n", (*((int *)value)) & (1 << ((ol->type >> 16) & 31))
+      ? "" : "no_", name);
     break;
 
   case opt_expand_bool:
@@ -2693,7 +2694,7 @@ switch(ol->type & opt_mask)
   case opt_bool:
   case opt_bool_verify:
   case opt_bool_set:
-    printf("%s%s\n", (*((BOOL *)value))? "" : "no_", name);
+    printf("%s%s\n", *((BOOL *)value) ? "" : "no_", name);
     break;
 
   case opt_func:
@@ -3497,7 +3498,7 @@ if (!process_log_path || !*process_log_path)
 /* Compile the regex for matching a UUCP-style "From_" line in an incoming
 message. */
 
-regex_From = regex_must_compile(uucp_from_pattern, FALSE, TRUE);
+regex_From = regex_must_compile(uucp_from_pattern, MCS_NOFLAGS, TRUE);
 
 /* Unpick the SMTP rate limiting options, if set */
 
diff --git a/src/src/regex.c b/src/src/regex.c
index 5c0f7c4e0..5de1c1704 100644
--- a/src/src/regex.c
+++ b/src/src/regex.c
@@ -18,9 +18,9 @@
 
 /* Structure to hold a list of Regular expressions */
 typedef struct pcre_list {
-  pcre2_code *re;
-  uschar *pcre_text;
-  struct pcre_list *next;
+  const pcre2_code *	re;
+  uschar *		pcre_text;
+  struct pcre_list *	next;
 } pcre_list;
 
 uschar regex_match_string_buffer[1024];
@@ -28,31 +28,27 @@ uschar regex_match_string_buffer[1024];
 extern FILE *mime_stream;
 extern uschar *mime_current_boundary;
 
+
 static pcre_list *
-compile(const uschar * list)
+compile(const uschar * list, BOOL cacheable)
 {
 int sep = 0;
-uschar *regex_string;
-pcre_list *re_list_head = NULL;
-pcre_list *ri;
+uschar * regex_string;
+pcre_list * re_list_head = NULL;
+pcre_list * ri;
 
 /* precompile our regexes */
 while ((regex_string = string_nextinlist(&list, &sep, NULL, 0)))
   if (strcmpic(regex_string, US"false") != 0 && Ustrcmp(regex_string, "0") != 0)
     {
-    pcre2_code * re;
-    int err;
-    PCRE2_SIZE pcre_erroffset;
-
     /* compile our regular expression */
-    if (!(re = pcre2_compile( (PCRE2_SPTR) regex_string, PCRE2_ZERO_TERMINATED,
-		  0, &err, &pcre_erroffset, pcre_cmp_ctx)))
+    uschar * errstr;
+    const pcre2_code * re = regex_compile(regex_string,
+      cacheable ? MCS_CACHEABLE : MCS_NOFLAGS, &errstr, pcre_gen_cmp_ctx);
+
+    if (!re)
       {
-      uschar errbuf[128];
-      pcre2_get_error_message(err, errbuf, sizeof(errbuf));
-      log_write(0, LOG_MAIN,
-	   "regex acl condition warning - error in regex '%s': %s at offset %ld, skipped.",
-	   regex_string, errbuf, (long)pcre_erroffset);
+      log_write(0, LOG_MAIN, "regex acl condition warning - %s, skipped", errstr);
       continue;
       }
 
@@ -75,7 +71,7 @@ for (pcre_list * ri = re_list_head; ri; ri = ri->next)
   int n;
 
   /* try matcher on the line */
-  if ((n = pcre2_match(ri->re, (PCRE2_SPTR)linebuffer, len, 0, 0, md, pcre_mtc_ctx)) > 0)
+  if ((n = pcre2_match(ri->re, (PCRE2_SPTR)linebuffer, len, 0, 0, md, pcre_gen_mtc_ctx)) > 0)
     {
     Ustrncpy(regex_match_string_buffer, ri->pcre_text,
 	      sizeof(regex_match_string_buffer)-1);
@@ -85,19 +81,21 @@ for (pcre_list * ri = re_list_head; ri; ri = ri->next)
       {
       PCRE2_UCHAR * cstr;
       PCRE2_SIZE cslen;
-      pcre2_substring_get_bynumber(md, nn, &cstr, &cslen);
+      pcre2_substring_get_bynumber(md, nn, &cstr, &cslen);	/* uses same ctx as md */
       regex_vars[nn-1] = CUS cstr;
       }
 
     return OK;
     }
   }
-pcre2_match_data_free(md);
+/* pcre2_match_data_free(md);	gen ctx needs no free */
 return FAIL;
 }
 
+
+
 int
-regex(const uschar **listptr)
+regex(const uschar **listptr, BOOL cacheable)
 {
 unsigned long mbox_size;
 FILE *mbox_file;
@@ -130,7 +128,7 @@ else
   }
 
 /* precompile our regexes */
-if (!(re_list_head = compile(*listptr)))
+if (!(re_list_head = compile(*listptr, cacheable)))
   return FAIL;			/* no regexes -> nothing to do */
 
 /* match each line against all regexes */
@@ -167,7 +165,7 @@ return ret;
 
 
 int
-mime_regex(const uschar **listptr)
+mime_regex(const uschar **listptr, BOOL cacheable)
 {
 pcre_list *re_list_head = NULL;
 FILE *f;
@@ -179,7 +177,7 @@ int ret;
 regex_match_string = NULL;
 
 /* precompile our regexes */
-if (!(re_list_head = compile(*listptr)))
+if (!(re_list_head = compile(*listptr, cacheable)))
   return FAIL;			/* no regexes -> nothing to do */
 
 /* check if the file is already decoded */
diff --git a/src/src/regex_cache.c b/src/src/regex_cache.c
new file mode 100644
index 000000000..63cddce1d
--- /dev/null
+++ b/src/src/regex_cache.c
@@ -0,0 +1,252 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/*
+ * Copyright (c) The Exim Maintainers 2022
+ * License: GPL
+ */
+
+/* Caching layers for compiled REs.  There is a local layer in the process,
+implemented as a tree for inserts and lookup.  This cache is inherited from
+the daemon, for the process tree deriving from there - but not by re-exec'd
+proceses or commandline submission processes.
+
+If the process has to compile, and is not the daemon or a re-exec'd exim,
+it notifies the use of the RE to the daemon via a unix-domain socket.
+This is a fire-and-forget send with no response, hence cheap from the point-of
+view of the sender.  I have not measured the overall comms costs.  The
+daemon also compiles the RE, and caches the result.
+
+A second layer would be possible by asking the daemon via the notifier socket
+(for a result from its cache, or a compile if it must).  The comms overhead
+is significant, not only for the channel but also for de/serialisation of
+the compiled object.  This makes it untenable for the primary use-case, the
+transport process which has been re-exec'd to gain privs - and therefore does not
+have the daemon-maintained cache.  Using shared-memory might reduce that cost
+(the attach time for the memory segment will matter); the implimentation
+would require suitable R/W locks.
+*/
+
+#include "exim.h"
+
+typedef struct re_req {
+  uschar	notifier_reqtype;
+  BOOL		caseless;
+  uschar	re[1];		/* extensible */
+} re_req;
+
+static tree_node * regex_cache = NULL;
+static tree_node * regex_caseless_cache = NULL;
+
+#define REGEX_CACHESIZE_LIMIT 1000
+
+/******************************************************************************/
+
+static void
+regex_to_daemon(const uschar * key, BOOL caseless)
+{
+int klen = Ustrlen(key) + 1;
+int rlen = sizeof(re_req) + klen;
+re_req * req;
+int fd, old_pool = store_pool;
+
+DEBUG(D_expand|D_lists)
+  debug_printf_indent("sending RE '%s' to daemon\n", key);
+
+store_pool = POOL_MAIN;
+  req = store_get(rlen, key);	/* maybe need a size limit */
+store_pool = old_pool;;
+req->notifier_reqtype = NOTIFY_REGEX;
+req->caseless = caseless;
+memcpy(req->re, key, klen);
+
+if ((fd = socket(AF_UNIX, SOCK_DGRAM, 0)) >= 0)
+  {
+  struct sockaddr_un sa_un = {.sun_family = AF_UNIX};
+  ssize_t len = daemon_notifier_sockname(&sa_un);
+
+  if (sendto(fd, req, rlen, 0, (struct sockaddr *)&sa_un, (socklen_t)len) < 0)
+    DEBUG(D_queue_run)
+      debug_printf("%s: sendto %s\n", __FUNCTION__, strerror(errno));
+  close(fd);
+  }
+else DEBUG(D_queue_run) debug_printf(" socket: %s\n", strerror(errno));
+}
+
+
+static const pcre2_code *
+regex_from_cache(const uschar * key, BOOL caseless)
+{
+tree_node * node  =
+  tree_search(caseless ? regex_caseless_cache : regex_cache, key);
+DEBUG(D_expand|D_lists)
+  debug_printf_indent("compiled %sRE '%s' %sfound in local cache\n",
+		      caseless ? "caseless " : "", key, node ? "" : "not ");
+
+return node ? node->data.ptr : NULL;
+}
+
+
+static void
+regex_to_cache(const uschar * key, BOOL caseless, const pcre2_code * cre)
+{
+PCRE2_SIZE srelen;
+uschar * sre;
+tree_node * node;
+
+node = store_get(sizeof(tree_node) + Ustrlen(key) + 1, key);	/* we are called with STORE_PERM */
+Ustrcpy(node->name, key);
+node->data.ptr = (void *)cre;
+
+if (!tree_insertnode(caseless ? &regex_caseless_cache : &regex_cache, node))
+  { DEBUG(D_expand|D_lists) debug_printf_indent("duplicate key!\n"); }
+else DEBUG(D_expand|D_lists)
+  debug_printf_indent("compiled RE '%s' saved in local cache\n", key);
+
+/* Additionally, if not re-execed and not the daemon, tell the daemon of the RE
+so it can add to the cache */
+
+if (f.daemon_scion && !f.daemon_listen)
+  regex_to_daemon(key, caseless);
+
+return;
+}
+
+/******************************************************************************/
+
+/*************************************************
+*  Compile regular expression and panic on fail  *
+*************************************************/
+
+/* This function is called when failure to compile a regular expression leads
+to a panic exit. In other cases, pcre_compile() is called directly. In many
+cases where this function is used, the results of the compilation are to be
+placed in long-lived store, so we temporarily reset the store management
+functions that PCRE uses if the use_malloc flag is set.
+
+Argument:
+  pattern     the pattern to compile
+  flags
+   caseless    caseless matching is required
+   cacheable   use (writeback) cache
+  use_malloc  TRUE if compile into malloc store
+
+Returns:      pointer to the compiled pattern
+*/
+
+const pcre2_code *
+regex_must_compile(const uschar * pattern, mcs_flags flags, BOOL use_malloc)
+{
+BOOL caseless = !!(flags & MCS_CASELESS);
+size_t offset;
+const pcre2_code * yield;
+int old_pool = store_pool, err;
+
+/* Optionall, check the cache and return if found */
+
+if (  flags & MCS_CACHEABLE
+   && (yield = regex_from_cache(pattern, caseless)))
+  return yield;
+
+store_pool = POOL_PERM;
+
+if (!(yield = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED,
+  caseless ? PCRE_COPT|PCRE2_CASELESS : PCRE_COPT,
+  &err, &offset, use_malloc ? pcre_mlc_cmp_ctx : pcre_gen_cmp_ctx)))
+  {
+  uschar errbuf[128];
+  pcre2_get_error_message(err, errbuf, sizeof(errbuf));
+  log_write(0, LOG_MAIN|LOG_PANIC_DIE, "regular expression error: "
+    "%s at offset %ld while compiling %s", errbuf, (long)offset, pattern);
+  }
+
+if (use_malloc)
+  {
+  /*pcre2_general_context_free(gctx);*/
+  }
+
+if (flags & MCS_CACHEABLE)
+  regex_to_cache(pattern, caseless, yield);
+
+store_pool = old_pool;
+return yield;
+}
+
+
+
+
+/* Wrapper for pcre2_compile() and error-message handling.
+
+Arguments:	pattern		regex to compile
+		flags
+		 caseless	flag for match variant
+		 cacheable	use (writeback) cache
+		errstr		on error, filled in with error message
+		cctx		compile-context for pcre2
+
+Return:		NULL on error, with errstr set. Otherwise, the compiled RE object
+*/
+
+const pcre2_code *
+regex_compile(const uschar * pattern, mcs_flags flags, uschar ** errstr,
+  pcre2_compile_context * cctx)
+{
+const uschar * key = pattern;
+BOOL caseless = !!(flags & MCS_CASELESS);
+int err;
+PCRE2_SIZE offset;
+const pcre2_code * yield;
+int old_pool = store_pool;
+
+/* Optionally, check the cache and return if found */
+
+if (  flags & MCS_CACHEABLE
+   && (yield = regex_from_cache(key, caseless)))
+  return yield;
+
+DEBUG(D_expand|D_lists) debug_printf_indent("compiling %sRE '%s'\n",
+				caseless ? "caseless " : "", pattern);
+
+store_pool = POOL_PERM;
+if (!(yield = pcre2_compile((PCRE2_SPTR)pattern, PCRE2_ZERO_TERMINATED,
+		caseless ? PCRE_COPT|PCRE2_CASELESS : PCRE_COPT,
+		&err, &offset, cctx)))
+  {
+  uschar errbuf[128];
+  pcre2_get_error_message(err, errbuf, sizeof(errbuf));
+  store_pool = old_pool;
+  *errstr = string_sprintf("regular expression error in "
+	    "\"%s\": %s at offset %ld", pattern, errbuf, (long)offset);
+  }
+else if (flags & MCS_CACHEABLE)
+  regex_to_cache(key, caseless, yield);
+store_pool = old_pool;
+
+return yield;
+}
+
+
+
+/* Handle a regex notify arriving at the daemon.  We get sent the original RE;
+compile it (again) and write to the cache.  Later forked procs will be able to
+read from the cache, unless they re-execed.  Therefore, those latter never bother
+sending us a notification. */
+
+void
+regex_at_daemon(const uschar * reqbuf)
+{
+const re_req * req = (const re_req *)reqbuf;
+uschar * errstr;
+const pcre2_code * cre;
+
+if (regex_cachesize >= REGEX_CACHESIZE_LIMIT)
+  errstr = US"regex cache size limit reached";
+else if ((cre = regex_compile(req->re,
+	    req->caseless ? MCS_CASELESS | MCS_CACHEABLE : MCS_CACHEABLE,
+	    &errstr, pcre_gen_cmp_ctx)))
+  regex_cachesize++;
+
+DEBUG(D_any) if (!cre) debug_printf("%s\n", errstr);
+return;
+}
diff --git a/src/src/rewrite.c b/src/src/rewrite.c
index 005dc51fe..bfd78b5f1 100644
--- a/src/src/rewrite.c
+++ b/src/src/rewrite.c
@@ -136,7 +136,8 @@ for (rewrite_rule * rule = rewrite_rules;
 
   if (flag & rewrite_smtp)
     {
-    uschar *key = expand_string(rule->key);
+    BOOL textonly_re;
+    const uschar * key = expand_string_2(rule->key, &textonly_re);
     if (!key)
       {
       if (!f.expand_string_forcedfail)
@@ -144,7 +145,8 @@ for (rewrite_rule * rule = rewrite_rules;
           "checking for SMTP rewriting: %s", rule->key, expand_string_message);
       continue;
       }
-    if (match_check_string(subject, key, 0, TRUE, FALSE, FALSE, NULL) != OK)
+    if (match_check_string(subject, key, 0,
+      textonly_re ? MCS_CACHEABLE | MCS_PARTIAL : MCS_PARTIAL, NULL) != OK)
       continue;
     new = expand_string(rule->replacement);
     }
diff --git a/src/src/routers/iplookup.c b/src/src/routers/iplookup.c
index 94cde4e04..8b67f3116 100644
--- a/src/src/routers/iplookup.c
+++ b/src/src/routers/iplookup.c
@@ -84,10 +84,10 @@ iplookup_router_options_block iplookup_router_option_defaults = {
 consistency checks to be done, or anything else that needs to be set up. */
 
 void
-iplookup_router_init(router_instance *rblock)
+iplookup_router_init(router_instance * rblock)
 {
-iplookup_router_options_block *ob =
-  (iplookup_router_options_block *)(rblock->options_block);
+iplookup_router_options_block * ob =
+  (iplookup_router_options_block *) rblock->options_block;
 
 /* A port and a host list must be given */
 
@@ -95,13 +95,13 @@ if (ob->port < 0)
   log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n  "
     "a port must be specified", rblock->name);
 
-if (ob->hosts == NULL)
+if (!ob->hosts)
   log_write(0, LOG_PANIC_DIE|LOG_CONFIG_FOR, "%s router:\n  "
     "a host list must be specified", rblock->name);
 
 /* Translate protocol name into value */
 
-if (ob->protocol_name != NULL)
+if (ob->protocol_name)
   {
   if (Ustrcmp(ob->protocol_name, "udp") == 0) ob->protocol = ip_udp;
   else if (Ustrcmp(ob->protocol_name, "tcp") == 0) ob->protocol = ip_tcp;
@@ -111,9 +111,9 @@ if (ob->protocol_name != NULL)
 
 /* If a response pattern is given, compile it now to get the error early. */
 
-if (ob->response_pattern != NULL)
+if (ob->response_pattern)
   ob->re_response_pattern =
-    regex_must_compile(ob->response_pattern, FALSE, TRUE);
+    regex_must_compile(ob->response_pattern, MCS_NOFLAGS, TRUE);
 }
 
 
diff --git a/src/src/structs.h b/src/src/structs.h
index b38aa6a9d..06cd06084 100644
--- a/src/src/structs.h
+++ b/src/src/structs.h
@@ -895,7 +895,7 @@ typedef struct check_host_block {
   const uschar *host_name;
   const uschar *host_address;
   const uschar *host_ipv4;
-  BOOL   negative;
+  mcs_flags	flags;
 } check_host_block;
 
 /* Structure for remembering lookup data when caching the result of
diff --git a/src/src/transports/appendfile.c b/src/src/transports/appendfile.c
index 93281efda..7e29dd3bc 100644
--- a/src/src/transports/appendfile.c
+++ b/src/src/transports/appendfile.c
@@ -681,7 +681,7 @@ for (struct dirent *ent; ent = readdir(dir); )
     {
     pcre2_match_data * md = pcre2_match_data_create(2, pcre_gen_ctx);
     int rc = pcre2_match(re, (PCRE2_SPTR)name, PCRE2_ZERO_TERMINATED,
-			  0, 0, md, pcre_mtc_ctx);
+			  0, 0, md, pcre_gen_mtc_ctx);
     PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
     if (  rc >= 0
        && (rc = pcre2_get_ovector_count(md)) >= 2)
@@ -694,9 +694,11 @@ for (struct dirent *ent; ent = readdir(dir); )
         DEBUG(D_transport)
           debug_printf("check_dir_size: size from %s is " OFF_T_FMT "\n", name,
             size);
+	/* pcre2_match_data_free(md);	gen ctx needs no free */
         continue;
         }
       }
+    /* pcre2_match_data_free(md);	gen ctx needs no free */
     DEBUG(D_transport)
       debug_printf("check_dir_size: regex did not match %s\n", name);
     }
@@ -2211,23 +2213,14 @@ else
 
   if (ob->quota_value > 0 || THRESHOLD_CHECK || ob->maildir_use_size_file)
     {
-    PCRE2_SIZE offset;
-    int err;
-
     /* Compile the regex if there is one. */
 
     if (ob->quota_size_regex)
       {
-      if (!(re = pcre2_compile((PCRE2_SPTR)ob->quota_size_regex,
-		  PCRE2_ZERO_TERMINATED, PCRE_COPT, &err, &offset, pcre_cmp_ctx)))
-        {
-	uschar errbuf[128];
-	pcre2_get_error_message(err, errbuf, sizeof(errbuf));
-        addr->message = string_sprintf("appendfile: regular expression "
-          "error: %s at offset %ld while compiling %s", errbuf, (long)offset,
-          ob->quota_size_regex);
+      if (!(re = regex_compile(ob->quota_size_regex,
+		  MCS_NOFLAGS, &addr->message, pcre_gen_cmp_ctx)))
         return FALSE;
-        }
+
       DEBUG(D_transport) debug_printf("using regex for file sizes: %s\n",
         ob->quota_size_regex);
       }
@@ -2300,23 +2293,14 @@ else
   if (ob->maildir_use_size_file)
     {
     const pcre2_code * dir_regex = NULL;
-    PCRE2_SIZE offset;
-    int err;
 
     if (ob->maildir_dir_regex)
       {
       int check_path_len = Ustrlen(check_path);
 
-      if (!(dir_regex = pcre2_compile((PCRE2_SPTR)ob->maildir_dir_regex,
-	    PCRE2_ZERO_TERMINATED, PCRE_COPT, &err, &offset, pcre_cmp_ctx)))
-        {
-	uschar errbuf[128];
-	pcre2_get_error_message(err, errbuf, sizeof(errbuf));
-        addr->message = string_sprintf("appendfile: regular expression "
-          "error: %s at offset %ld while compiling %s", errbuf, (long)offset,
-          ob->maildir_dir_regex);
+      if (!(dir_regex = regex_compile(ob->maildir_dir_regex,
+	    MCS_NOFLAGS, &addr->message, pcre_gen_cmp_ctx)))
         return FALSE;
-        }
 
       DEBUG(D_transport)
         debug_printf("using regex for maildir directory selection: %s\n",
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index 7f529b7ca..6eee04d03 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -275,7 +275,7 @@ struct list
 
 for (struct list * l = list; l < list + nelem(list); l++)
   if (!*l->re)
-    *l->re = regex_must_compile(l->string, FALSE, TRUE);
+    *l->re = regex_must_compile(l->string, MCS_NOFLAGS, TRUE);
 }
 
 
@@ -1000,7 +1000,7 @@ uschar authnum;
 unsigned short authbits = 0;
 
 if (!sx->esmtp) return 0;
-if (!regex_AUTH) regex_AUTH = regex_must_compile(AUTHS_REGEX, FALSE, TRUE);
+if (!regex_AUTH) regex_AUTH = regex_must_compile(AUTHS_REGEX, MCS_NOFLAGS, TRUE);
 if (!regex_match_and_setup(regex_AUTH, sx->buffer, 0, -1)) return 0;
 expand_nmax = -1;						/* reset */
 names = string_copyn(expand_nstring[1], expand_nlength[1]);
@@ -1563,7 +1563,7 @@ f.smtp_authenticated = FALSE;
 client_authenticator = client_authenticated_id = client_authenticated_sender = NULL;
 
 if (!regex_AUTH)
-  regex_AUTH = regex_must_compile(AUTHS_REGEX, FALSE, TRUE);
+  regex_AUTH = regex_must_compile(AUTHS_REGEX, MCS_NOFLAGS, TRUE);
 
 /* Is the server offering AUTH? */
 
@@ -1849,57 +1849,57 @@ pcre2_match_data * md = pcre2_match_data_create(1, pcre_gen_ctx);
 #ifndef DISABLE_TLS
 if (  checks & OPTION_TLS
    && pcre2_match(regex_STARTTLS,
-		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_gen_mtc_ctx) < 0)
 #endif
   checks &= ~OPTION_TLS;
 
 if (  checks & OPTION_IGNQ
    && pcre2_match(regex_IGNOREQUOTA,
-		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_gen_mtc_ctx) < 0)
   checks &= ~OPTION_IGNQ;
 
 if (  checks & OPTION_CHUNKING
    && pcre2_match(regex_CHUNKING,
-		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_gen_mtc_ctx) < 0)
   checks &= ~OPTION_CHUNKING;
 
 #ifndef DISABLE_PRDR
 if (  checks & OPTION_PRDR
    && pcre2_match(regex_PRDR,
-		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_gen_mtc_ctx) < 0)
 #endif
   checks &= ~OPTION_PRDR;
 
 #ifdef SUPPORT_I18N
 if (  checks & OPTION_UTF8
    && pcre2_match(regex_UTF8,
-		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_gen_mtc_ctx) < 0)
 #endif
   checks &= ~OPTION_UTF8;
 
 if (  checks & OPTION_DSN
    && pcre2_match(regex_DSN,
-		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_gen_mtc_ctx) < 0)
   checks &= ~OPTION_DSN;
 
 if (  checks & OPTION_PIPE
    && pcre2_match(regex_PIPELINING,
-		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_gen_mtc_ctx) < 0)
   checks &= ~OPTION_PIPE;
 
 if (  checks & OPTION_SIZE
    && pcre2_match(regex_SIZE,
-		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_gen_mtc_ctx) < 0)
   checks &= ~OPTION_SIZE;
 
 #ifndef DISABLE_PIPE_CONNECT
 if (  checks & OPTION_EARLY_PIPE
    && pcre2_match(regex_EARLY_PIPE,
-		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_mtc_ctx) < 0)
+		  (PCRE2_SPTR)buf, bsize, 0, PCRE_EOPT, md, pcre_gen_mtc_ctx) < 0)
 #endif
   checks &= ~OPTION_EARLY_PIPE;
 
-pcre2_match_data_free(md);
+/* pcre2_match_data_free(md);	gen ctx needs no free */
 /* debug_printf("%s: found     0x%04x\n", __FUNCTION__, checks); */
 return checks;
 }
diff --git a/src/src/transports/tf_maildir.c b/src/src/transports/tf_maildir.c
index a83fc6f09..205ee41cb 100644
--- a/src/src/transports/tf_maildir.c
+++ b/src/src/transports/tf_maildir.c
@@ -142,22 +142,13 @@ a subfolder, and should ensure that a maildirfolder file exists. */
 
 if (maildirfolder_create_regex)
   {
-  int err;
-  PCRE2_SIZE offset;
   const pcre2_code * re;
 
   DEBUG(D_transport) debug_printf("checking for maildirfolder requirement\n");
 
-  if (!(re = pcre2_compile((PCRE2_SPTR)maildirfolder_create_regex,
-	      PCRE2_ZERO_TERMINATED, PCRE_COPT, &err, &offset, pcre_cmp_ctx)))
-    {
-    uschar errbuf[128];
-    pcre2_get_error_message(err, errbuf, sizeof(errbuf));
-    addr->message = string_sprintf("appendfile: regular expression "
-      "error: %s at offset %ld while compiling %s", errbuf, (long)offset,
-      maildirfolder_create_regex);
+  if (!(re = regex_compile(maildirfolder_create_regex,
+	      MCS_NOFLAGS, &addr->message, pcre_gen_cmp_ctx)))
     return FALSE;
-    }
 
   if (regex_match(re, path, -1, NULL))
     {
diff --git a/src/src/verify.c b/src/src/verify.c
index b4c2b9a8f..afc18d553 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -3074,7 +3074,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;
 
@@ -3082,7 +3082,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;
@@ -3113,8 +3113,8 @@ 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)   /* Explicit host name given */
-  return match_check_string(cb->host_name, ss, -1, TRUE, TRUE, TRUE,
-    valueptr);
+  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
@@ -3143,7 +3143,9 @@ if ((semicolon = Ustrchr(ss, ';')))
 
 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;
@@ -3169,7 +3171,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;
@@ -3179,7 +3183,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;
@@ -3255,8 +3261,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;

commit d05685413efd3262b4a5622717f90bba351f1074
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Jun 26 12:29:05 2022 +0100

    typo

diff --git a/src/src/drtables.c b/src/src/drtables.c
index b2f2a4b33..30eb855e3 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -736,7 +736,7 @@ else
     {
     char * name = ent->d_name;
     int len = (int)strlen(name);
-    if (regex_match(regex_islookupmod, US name, len, NUL))
+    if (regex_match(regex_islookupmod, US name, len, NULL))
       {
       int pathnamelen = len + (int)strlen(LOOKUP_MODULE_DIR) + 2;
       void *dl;

commit c6887a05b9c56d373086e9a79e20c26bebd300b2
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Jun 26 15:27:32 2022 +0100

    Variable setting in -be

diff --git a/src/src/acl.c b/src/src/acl.c
index 0078aca7d..3af3a4eee 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -734,6 +734,78 @@ return -1;
 }
 
 
+static BOOL
+acl_varname_to_cond(const uschar ** sp, acl_condition_block * cond, uschar ** error)
+{
+const uschar * s = *sp, * endptr;
+
+#ifndef DISABLE_DKIM
+if (  Ustrncmp(s, "dkim_verify_status", 18) == 0
+   || Ustrncmp(s, "dkim_verify_reason", 18) == 0)
+  {
+  endptr = s+18;
+  if (isalnum(*endptr))
+    {
+    *error = string_sprintf("invalid variable name after \"set\" in ACL "
+      "modifier \"set %s\" "
+      "(only \"dkim_verify_status\" or \"dkim_verify_reason\" permitted)",
+      s);
+    return FALSE;
+    }
+  cond->u.varname = string_copyn(s, 18);
+  }
+else
+#endif
+  {
+  if (Ustrncmp(s, "acl_c", 5) != 0 && Ustrncmp(s, "acl_m", 5) != 0)
+    {
+    *error = string_sprintf("invalid variable name after \"set\" in ACL "
+      "modifier \"set %s\" (must start \"acl_c\" or \"acl_m\")", s);
+    return FALSE;
+    }
+
+  endptr = s + 5;
+  if (!isdigit(*endptr) && *endptr != '_')
+    {
+    *error = string_sprintf("invalid variable name after \"set\" in ACL "
+      "modifier \"set %s\" (digit or underscore must follow acl_c or acl_m)",
+      s);
+    return FALSE;
+    }
+
+  for ( ; *endptr && *endptr != '=' && !isspace(*endptr); endptr++)
+    if (!isalnum(*endptr) && *endptr != '_')
+      {
+      *error = string_sprintf("invalid character \"%c\" in variable name "
+	"in ACL modifier \"set %s\"", *endptr, s);
+      return FALSE;
+      }
+
+  cond->u.varname = string_copyn(s + 4, endptr - s - 4);
+  }
+s = endptr;
+Uskip_whitespace(&s);
+*sp = s;
+return TRUE;
+}
+
+
+static BOOL
+acl_data_to_cond(const uschar * s, acl_condition_block * cond,
+  const uschar * name, uschar ** error)
+{
+if (*s++ != '=')
+  {
+  *error = string_sprintf("\"=\" missing after ACL \"%s\" %s", name,
+    conditions[cond->type].is_modifier ? US"modifier" : US"condition");
+  return FALSE;;
+  }
+Uskip_whitespace(&s);
+cond->arg = string_copy(s);
+return TRUE;
+}
+
+
 /*************************************************
 *            Read and parse one ACL              *
 *************************************************/
@@ -760,7 +832,7 @@ acl_block **lastp = &yield;
 acl_block *this = NULL;
 acl_condition_block *cond;
 acl_condition_block **condp = NULL;
-uschar * s;
+const uschar * s;
 
 *error = NULL;
 
@@ -768,7 +840,7 @@ while ((s = (*func)()))
   {
   int v, c;
   BOOL negated = FALSE;
-  uschar *saveline = s;
+  const uschar * saveline = s;
   uschar name[EXIM_DRIVERNAME_MAX];
 
   /* Conditions (but not verbs) are allowed to be negated by an initial
@@ -808,16 +880,15 @@ while ((s = (*func)()))
       *error = string_sprintf("malformed ACL line \"%s\"", saveline);
       return NULL;
       }
-    this = store_get(sizeof(acl_block), GET_UNTAINTED);
-    *lastp = this;
-    lastp = &(this->next);
+    *lastp = this = store_get(sizeof(acl_block), GET_UNTAINTED);
+    lastp = &this->next;
     this->next = NULL;
     this->condition = NULL;
     this->verb = v;
     this->srcline = config_lineno;	/* for debug output */
     this->srcfile = config_filename;	/**/
-    condp = &(this->condition);
-    if (*s == 0) continue;               /* No condition on this line */
+    condp = &this->condition;
+    if (!*s) continue;               /* No condition on this line */
     if (*s == '!')
       {
       negated = TRUE;
@@ -861,7 +932,7 @@ while ((s = (*func)()))
   cond->u.negated = negated;
 
   *condp = cond;
-  condp = &(cond->next);
+  condp = &cond->next;
 
   /* The "set" modifier is different in that its argument is "name=value"
   rather than just a value, and we can check the validity of the name, which
@@ -874,75 +945,13 @@ while ((s = (*func)()))
   compatibility. */
 
   if (c == ACLC_SET)
-#ifndef DISABLE_DKIM
-    if (  Ustrncmp(s, "dkim_verify_status", 18) == 0
-       || Ustrncmp(s, "dkim_verify_reason", 18) == 0)
-      {
-      uschar * endptr = s+18;
-
-      if (isalnum(*endptr))
-	{
-	*error = string_sprintf("invalid variable name after \"set\" in ACL "
-	  "modifier \"set %s\" "
-	  "(only \"dkim_verify_status\" or \"dkim_verify_reason\" permitted)",
-	  s);
-	return NULL;
-	}
-      cond->u.varname = string_copyn(s, 18);
-      s = endptr;
-      Uskip_whitespace(&s);
-      }
-    else
-#endif
-    {
-    uschar *endptr;
-
-    if (Ustrncmp(s, "acl_c", 5) != 0 && Ustrncmp(s, "acl_m", 5) != 0)
-      {
-      *error = string_sprintf("invalid variable name after \"set\" in ACL "
-	"modifier \"set %s\" (must start \"acl_c\" or \"acl_m\")", s);
-      return NULL;
-      }
-
-    endptr = s + 5;
-    if (!isdigit(*endptr) && *endptr != '_')
-      {
-      *error = string_sprintf("invalid variable name after \"set\" in ACL "
-	"modifier \"set %s\" (digit or underscore must follow acl_c or acl_m)",
-	s);
-      return NULL;
-      }
-
-    while (*endptr && *endptr != '=' && !isspace(*endptr))
-      {
-      if (!isalnum(*endptr) && *endptr != '_')
-	{
-	*error = string_sprintf("invalid character \"%c\" in variable name "
-	  "in ACL modifier \"set %s\"", *endptr, s);
-	return NULL;
-	}
-      endptr++;
-      }
-
-    cond->u.varname = string_copyn(s + 4, endptr - s - 4);
-    s = endptr;
-    Uskip_whitespace(&s);
-    }
+    if (!acl_varname_to_cond(&s, cond, error)) return NULL;
 
   /* For "set", we are now positioned for the data. For the others, only
   "endpass" has no data */
 
   if (c != ACLC_ENDPASS)
-    {
-    if (*s++ != '=')
-      {
-      *error = string_sprintf("\"=\" missing after ACL \"%s\" %s", name,
-        conditions[c].is_modifier ? US"modifier" : US"condition");
-      return NULL;
-      }
-    Uskip_whitespace(&s);
-    cond->arg = string_copy(s);
-    }
+    if (!acl_data_to_cond(s, cond, name, error)) return NULL;
   }
 
 return yield;
@@ -4894,6 +4903,29 @@ if (is_tainted(value))
 fprintf(f, "acl%c %s %d\n%s\n", name[0], name+1, Ustrlen(value), value);
 }
 
+
+
+
+uschar *
+acl_standalone_setvar(const uschar * s)
+{
+acl_condition_block * cond = store_get(sizeof(acl_condition_block), GET_UNTAINTED);
+uschar * errstr = NULL, * log_msg = NULL;
+BOOL endpass_seen;
+int e;
+
+cond->next = NULL;
+cond->type = ACLC_SET;
+if (!acl_varname_to_cond(&s, cond, &errstr)) return errstr;
+if (!acl_data_to_cond(s, cond, US"'-be'", &errstr)) return errstr;
+
+if (acl_check_condition(ACL_WARN, cond, ACL_WHERE_UNKNOWN,
+			    NULL, 0, &endpass_seen, &errstr, &log_msg, &e) != OK)
+  return string_sprintf("oops: %s", errstr);
+return string_sprintf("variable %s set", cond->u.varname);
+}
+
+
 #endif	/* !MACRO_PREDEF */
 /* vi: aw ai sw=2
 */
diff --git a/src/src/exim.c b/src/src/exim.c
index dec8de4b4..23e206d2a 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1670,6 +1670,8 @@ if (isupper(big_buffer[0]))
   if (macro_read_assignment(big_buffer))
     printf("Defined macro '%s'\n", mlast->name);
   }
+else if (Ustrncmp(big_buffer, "set ", 4) == 0)
+  printf("%s\n", acl_standalone_setvar(big_buffer+4));
 else
   if ((s = expand_string(big_buffer))) printf("%s\n", CS s);
   else printf("Failed: %s\n", expand_string_message);
diff --git a/src/src/functions.h b/src/src/functions.h
index 4caae346d..e71823410 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -100,6 +100,7 @@ extern acl_block *acl_read(uschar *(*)(void), uschar **);
 extern int     acl_check(int, uschar *, uschar *, uschar **, uschar **);
 extern uschar *acl_current_verb(void);
 extern int     acl_eval(int, uschar *, uschar **, uschar **);
+extern uschar *acl_standalone_setvar(const uschar *);
 
 extern tree_node *acl_var_create(uschar *);
 extern void    acl_var_write(uschar *, uschar *, void *);
@@ -425,7 +426,7 @@ extern void    readconf_main(BOOL);
 extern void    readconf_options_from_list(optionlist *, unsigned, const uschar *, uschar *);
 extern BOOL    readconf_print(const uschar *, uschar *, BOOL);
 extern uschar *readconf_printtime(int);
-extern uschar *readconf_readname(uschar *, int, uschar *);
+extern const uschar *readconf_readname(uschar *, int, const uschar *);
 extern int     readconf_readtime(const uschar *, int, BOOL);
 extern void    readconf_rest(void);
 extern uschar *readconf_retry_error(const uschar *, const uschar *, int *, int *);
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 5068dc60e..83ee51b65 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -1172,8 +1172,8 @@ Arguments:
 Returns:    new input pointer
 */
 
-uschar *
-readconf_readname(uschar *name, int len, uschar *s)
+const uschar *
+readconf_readname(uschar * name, int len, const uschar * s)
 {
 int p = 0;
 BOOL broken = FALSE;
@@ -1632,7 +1632,7 @@ rmark reset_point;
 int intbase = 0;
 uschar *inttype = US"";
 uschar *sptr;
-uschar *s = buffer;
+const uschar * s = buffer;
 uschar **str_target;
 uschar name[EXIM_DRIVERNAME_MAX];
 uschar name2[EXIM_DRIVERNAME_MAX];
@@ -1752,337 +1752,337 @@ switch (type)
   case opt_gidlist:
   case opt_rewrite:
 
-  reset_point = store_mark();
-  sptr = read_string(s, name);
+    reset_point = store_mark();
+    sptr = read_string(s, name);
 
-  /* Having read a string, we now have several different ways of using it,
-  depending on the data type, so do another switch. If keeping the actual
-  string is not required (because it is interpreted), freesptr is set TRUE,
-  and at the end we reset the pool. */
+    /* Having read a string, we now have several different ways of using it,
+    depending on the data type, so do another switch. If keeping the actual
+    string is not required (because it is interpreted), freesptr is set TRUE,
+    and at the end we reset the pool. */
 
-  switch (type)
-    {
-    /* If this was a string, set the variable to point to the new string,
-    and set the flag so its store isn't reclaimed. If it was a list of rewrite
-    rules, we still keep the string (for printing), and parse the rules into a
-    control block and flags word. */
-
-    case opt_stringptr:
-    str_target = data_block ? USS (US data_block + ol->v.offset)
-			    : USS ol->v.value;
-    if (ol->type & opt_rep_con)
-      {
-      uschar * saved_condition;
-      /* We already have a condition, we're conducting a crude hack to let
-      multiple condition rules be chained together, despite storing them in
-      text form. */
-      *str_target = string_copy_perm( (saved_condition = *str_target)
-	? string_sprintf("${if and{{bool_lax{%s}}{bool_lax{%s}}}}",
-	    saved_condition, sptr)
-	: sptr,
-	FALSE);
-      /* TODO(pdp): there is a memory leak here and just below
-      when we set 3 or more conditions; I still don't
-      understand the store mechanism enough to know
-      what's the safe way to free content from an earlier store.
-      AFAICT, stores stack, so freeing an early stored item also stores
-      all data alloc'd after it.  If we knew conditions were adjacent,
-      we could survive that, but we don't.  So I *think* we need to take
-      another bit from opt_type to indicate "malloced"; this seems like
-      quite a hack, especially for this one case.  It also means that
-      we can't ever reclaim the store from the *first* condition.
-
-      Because we only do this once, near process start-up, I'm prepared to
-      let this slide for the time being, even though it rankles.  */
-      }
-    else if (ol->type & opt_rep_str)
+    switch (type)
       {
-      uschar sep_o =
-	Ustrncmp(name, "headers_add", 11) == 0	? '\n'
-	: Ustrncmp(name, "set", 3) == 0		? ';'
-	: ':';
-      int    sep_i = -(int)sep_o;
-      const uschar * list = sptr;
-      uschar * s;
-      gstring * list_o = NULL;
-
-      if (*str_target)
-	{
-	list_o = string_get(Ustrlen(*str_target) + Ustrlen(sptr));
-	list_o = string_cat(list_o, *str_target);
-	}
-
-      while ((s = string_nextinlist(&list, &sep_i, NULL, 0)))
-	list_o = string_append_listele(list_o, sep_o, s);
+      /* If this was a string, set the variable to point to the new string,
+      and set the flag so its store isn't reclaimed. If it was a list of rewrite
+      rules, we still keep the string (for printing), and parse the rules into a
+      control block and flags word. */
+
+      case opt_stringptr:
+	str_target = data_block ? USS (US data_block + ol->v.offset)
+				: USS ol->v.value;
+	if (ol->type & opt_rep_con)
+	  {
+	  uschar * saved_condition;
+	  /* We already have a condition, we're conducting a crude hack to let
+	  multiple condition rules be chained together, despite storing them in
+	  text form. */
+	  *str_target = string_copy_perm( (saved_condition = *str_target)
+	    ? string_sprintf("${if and{{bool_lax{%s}}{bool_lax{%s}}}}",
+		saved_condition, sptr)
+	    : sptr,
+	    FALSE);
+	  /* TODO(pdp): there is a memory leak here and just below
+	  when we set 3 or more conditions; I still don't
+	  understand the store mechanism enough to know
+	  what's the safe way to free content from an earlier store.
+	  AFAICT, stores stack, so freeing an early stored item also stores
+	  all data alloc'd after it.  If we knew conditions were adjacent,
+	  we could survive that, but we don't.  So I *think* we need to take
+	  another bit from opt_type to indicate "malloced"; this seems like
+	  quite a hack, especially for this one case.  It also means that
+	  we can't ever reclaim the store from the *first* condition.
+
+	  Because we only do this once, near process start-up, I'm prepared to
+	  let this slide for the time being, even though it rankles.  */
+	  }
+	else if (ol->type & opt_rep_str)
+	  {
+	  uschar sep_o =
+	    Ustrncmp(name, "headers_add", 11) == 0	? '\n'
+	    : Ustrncmp(name, "set", 3) == 0		? ';'
+	    : ':';
+	  int    sep_i = -(int)sep_o;
+	  const uschar * list = sptr;
+	  uschar * s;
+	  gstring * list_o = NULL;
+
+	  if (*str_target)
+	    {
+	    list_o = string_get(Ustrlen(*str_target) + Ustrlen(sptr));
+	    list_o = string_cat(list_o, *str_target);
+	    }
 
-      if (list_o)
-	*str_target = string_copy_perm(string_from_gstring(list_o), FALSE);
-      }
-    else
-      {
-      *str_target = sptr;
-      freesptr = FALSE;
-      }
-    break;
+	  while ((s = string_nextinlist(&list, &sep_i, NULL, 0)))
+	    list_o = string_append_listele(list_o, sep_o, s);
 
-    case opt_rewrite:
-    if (data_block)
-      *USS (US data_block + ol->v.offset) = sptr;
-    else
-      *USS ol->v.value = sptr;
-    freesptr = FALSE;
-    if (type == opt_rewrite)
-      {
-      int sep = 0;
-      int *flagptr;
-      uschar *p = sptr;
-      rewrite_rule **chain;
-      optionlist *ol3;
-
-      sprintf(CS name2, "*%.50s_rules", name);
-      ol2 = find_option(name2, oltop, last);
-      sprintf(CS name2, "*%.50s_flags", name);
-      ol3 = find_option(name2, oltop, last);
-
-      if (!ol2 || !ol3)
-        log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
-          "rewrite rules not available for driver");
+	  if (list_o)
+	    *str_target = string_copy_perm(string_from_gstring(list_o), FALSE);
+	  }
+	else
+	  {
+	  *str_target = sptr;
+	  freesptr = FALSE;
+	  }
+	break;
 
-      if (data_block)
-        {
-        chain = (rewrite_rule **)(US data_block + ol2->v.offset);
-        flagptr = (int *)(US data_block + ol3->v.offset);
-        }
-      else
-        {
-        chain = (rewrite_rule **)ol2->v.value;
-        flagptr = (int *)ol3->v.value;
-        }
+      case opt_rewrite:
+	if (data_block)
+	  *USS (US data_block + ol->v.offset) = sptr;
+	else
+	  *USS ol->v.value = sptr;
+	freesptr = FALSE;
+	if (type == opt_rewrite)
+	  {
+	  int sep = 0;
+	  int *flagptr;
+	  uschar *p = sptr;
+	  rewrite_rule **chain;
+	  optionlist *ol3;
+
+	  sprintf(CS name2, "*%.50s_rules", name);
+	  ol2 = find_option(name2, oltop, last);
+	  sprintf(CS name2, "*%.50s_flags", name);
+	  ol3 = find_option(name2, oltop, last);
+
+	  if (!ol2 || !ol3)
+	    log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
+	      "rewrite rules not available for driver");
+
+	  if (data_block)
+	    {
+	    chain = (rewrite_rule **)(US data_block + ol2->v.offset);
+	    flagptr = (int *)(US data_block + ol3->v.offset);
+	    }
+	  else
+	    {
+	    chain = (rewrite_rule **)ol2->v.value;
+	    flagptr = (int *)ol3->v.value;
+	    }
 
-      /* This will trap if sptr is tainted. Not sure if that can happen */
-      while ((p = string_nextinlist(CUSS &sptr, &sep, big_buffer, BIG_BUFFER_SIZE)))
-        {
-        rewrite_rule *next = readconf_one_rewrite(p, flagptr, FALSE);
-        *chain = next;
-        chain = &(next->next);
-        }
+	  /* This will trap if sptr is tainted. Not sure if that can happen */
+	  while ((p = string_nextinlist(CUSS &sptr, &sep, big_buffer, BIG_BUFFER_SIZE)))
+	    {
+	    rewrite_rule *next = readconf_one_rewrite(p, flagptr, FALSE);
+	    *chain = next;
+	    chain = &(next->next);
+	    }
 
-      if ((*flagptr & (rewrite_all_envelope | rewrite_smtp)) != 0)
-        log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "rewrite rule specifies a "
-          "non-header rewrite - not allowed at transport time -");
-      }
-    break;
+	  if ((*flagptr & (rewrite_all_envelope | rewrite_smtp)) != 0)
+	    log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "rewrite rule specifies a "
+	      "non-header rewrite - not allowed at transport time -");
+	  }
+	break;
 
-    /* If it was an expanded uid, see if there is any expansion to be
-    done by checking for the presence of a $ character. If there is, save it
-    in the corresponding *expand_user option field. Otherwise, fall through
-    to treat it as a fixed uid. Ensure mutual exclusivity of the two kinds
-    of data. */
+      /* If it was an expanded uid, see if there is any expansion to be
+      done by checking for the presence of a $ character. If there is, save it
+      in the corresponding *expand_user option field. Otherwise, fall through
+      to treat it as a fixed uid. Ensure mutual exclusivity of the two kinds
+      of data. */
 
-    case opt_expand_uid:
-    sprintf(CS name2, "*expand_%.50s", name);
-    if ((ol2 = find_option(name2, oltop, last)))
-      {
-      uschar *ss = (Ustrchr(sptr, '$') != NULL) ? sptr : NULL;
+      case opt_expand_uid:
+	sprintf(CS name2, "*expand_%.50s", name);
+	if ((ol2 = find_option(name2, oltop, last)))
+	  {
+	  uschar *ss = (Ustrchr(sptr, '$') != NULL) ? sptr : NULL;
 
-      if (data_block)
-        *(USS(US data_block + ol2->v.offset)) = ss;
-      else
-        *(USS ol2->v.value) = ss;
+	  if (data_block)
+	    *(USS(US data_block + ol2->v.offset)) = ss;
+	  else
+	    *(USS ol2->v.value) = ss;
 
-      if (ss)
-        {
-        *(get_set_flag(name, oltop, last, data_block)) = FALSE;
-        freesptr = FALSE;
-        break;
-        }
-      }
+	  if (ss)
+	    {
+	    *(get_set_flag(name, oltop, last, data_block)) = FALSE;
+	    freesptr = FALSE;
+	    break;
+	    }
+	  }
 
-    /* Look up a fixed uid, and also make use of the corresponding gid
-    if a passwd entry is returned and the gid has not been set. */
+      /* Look up a fixed uid, and also make use of the corresponding gid
+      if a passwd entry is returned and the gid has not been set. */
 
-    case opt_uid:
-    if (!route_finduser(sptr, &pw, &uid))
-      log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "user %s was not found", sptr);
-    if (data_block)
-      *(uid_t *)(US data_block + ol->v.offset) = uid;
-    else
-      *(uid_t *)ol->v.value = uid;
+      case opt_uid:
+	if (!route_finduser(sptr, &pw, &uid))
+	  log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "user %s was not found", sptr);
+	if (data_block)
+	  *(uid_t *)(US data_block + ol->v.offset) = uid;
+	else
+	  *(uid_t *)ol->v.value = uid;
 
-    /* Set the flag indicating a fixed value is set */
+	/* Set the flag indicating a fixed value is set */
 
-    *(get_set_flag(name, oltop, last, data_block)) = TRUE;
+	*(get_set_flag(name, oltop, last, data_block)) = TRUE;
 
-    /* Handle matching gid if we have a passwd entry: done by finding the
-    same name with terminating "user" changed to "group"; if not found,
-    ignore. Also ignore if the value is already set. */
+	/* Handle matching gid if we have a passwd entry: done by finding the
+	same name with terminating "user" changed to "group"; if not found,
+	ignore. Also ignore if the value is already set. */
 
-    if (pw == NULL) break;
-    Ustrcpy(name+Ustrlen(name)-4, US"group");
-    ol2 = find_option(name, oltop, last);
-    if (ol2 && ((ol2->type & opt_mask) == opt_gid ||
-        (ol2->type & opt_mask) == opt_expand_gid))
-      {
-      BOOL *set_flag = get_set_flag(name, oltop, last, data_block);
-      if (!*set_flag)
-        {
-        if (data_block)
-          *((gid_t *)(US data_block + ol2->v.offset)) = pw->pw_gid;
-        else
-          *((gid_t *)ol2->v.value) = pw->pw_gid;
-        *set_flag = TRUE;
-        }
-      }
-    break;
+	if (pw == NULL) break;
+	Ustrcpy(name+Ustrlen(name)-4, US"group");
+	ol2 = find_option(name, oltop, last);
+	if (ol2 && ((ol2->type & opt_mask) == opt_gid ||
+	    (ol2->type & opt_mask) == opt_expand_gid))
+	  {
+	  BOOL *set_flag = get_set_flag(name, oltop, last, data_block);
+	  if (!*set_flag)
+	    {
+	    if (data_block)
+	      *((gid_t *)(US data_block + ol2->v.offset)) = pw->pw_gid;
+	    else
+	      *((gid_t *)ol2->v.value) = pw->pw_gid;
+	    *set_flag = TRUE;
+	    }
+	  }
+	break;
 
-    /* If it was an expanded gid, see if there is any expansion to be
-    done by checking for the presence of a $ character. If there is, save it
-    in the corresponding *expand_user option field. Otherwise, fall through
-    to treat it as a fixed gid. Ensure mutual exclusivity of the two kinds
-    of data. */
+      /* If it was an expanded gid, see if there is any expansion to be
+      done by checking for the presence of a $ character. If there is, save it
+      in the corresponding *expand_user option field. Otherwise, fall through
+      to treat it as a fixed gid. Ensure mutual exclusivity of the two kinds
+      of data. */
 
-    case opt_expand_gid:
-    sprintf(CS name2, "*expand_%.50s", name);
-    if ((ol2 = find_option(name2, oltop, last)))
-      {
-      uschar *ss = (Ustrchr(sptr, '$') != NULL) ? sptr : NULL;
+      case opt_expand_gid:
+	sprintf(CS name2, "*expand_%.50s", name);
+	if ((ol2 = find_option(name2, oltop, last)))
+	  {
+	  uschar *ss = (Ustrchr(sptr, '$') != NULL) ? sptr : NULL;
 
-      if (data_block)
-        *(USS(US data_block + ol2->v.offset)) = ss;
-      else
-        *(USS ol2->v.value) = ss;
+	  if (data_block)
+	    *(USS(US data_block + ol2->v.offset)) = ss;
+	  else
+	    *(USS ol2->v.value) = ss;
 
-      if (ss)
-        {
-        *(get_set_flag(name, oltop, last, data_block)) = FALSE;
-        freesptr = FALSE;
-        break;
-        }
-      }
+	  if (ss)
+	    {
+	    *(get_set_flag(name, oltop, last, data_block)) = FALSE;
+	    freesptr = FALSE;
+	    break;
+	    }
+	  }
 
-    /* Handle freestanding gid */
+      /* Handle freestanding gid */
 
-    case opt_gid:
-    if (!route_findgroup(sptr, &gid))
-      log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "group %s was not found", sptr);
-    if (data_block)
-      *((gid_t *)(US data_block + ol->v.offset)) = gid;
-    else
-      *((gid_t *)ol->v.value) = gid;
-    *(get_set_flag(name, oltop, last, data_block)) = TRUE;
-    break;
+      case opt_gid:
+	if (!route_findgroup(sptr, &gid))
+	  log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "group %s was not found", sptr);
+	if (data_block)
+	  *((gid_t *)(US data_block + ol->v.offset)) = gid;
+	else
+	  *((gid_t *)ol->v.value) = gid;
+	*(get_set_flag(name, oltop, last, data_block)) = TRUE;
+	break;
 
-    /* If it was a uid list, look up each individual entry, and build
-    a vector of uids, with a count in the first element. Put the vector
-    in malloc store so we can free the string. (We are reading into
-    permanent store already.) */
+      /* If it was a uid list, look up each individual entry, and build
+      a vector of uids, with a count in the first element. Put the vector
+      in malloc store so we can free the string. (We are reading into
+      permanent store already.) */
 
-    case opt_uidlist:
-      {
-      int count = 1;
-      uid_t *list;
-      int ptr = 0;
-      const uschar *p;
-      const uschar *op = expand_string (sptr);
-
-      if (op == NULL)
-        log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "failed to expand %s: %s",
-          name, expand_string_message);
-
-      p = op;
-      if (*p != 0) count++;
-      while (*p != 0) if (*p++ == ':' && *p != 0) count++;
-      list = store_malloc(count*sizeof(uid_t));
-      list[ptr++] = (uid_t)(count - 1);
-
-      if (data_block)
-        *((uid_t **)(US data_block + ol->v.offset)) = list;
-      else
-        *((uid_t **)ol->v.value) = list;
+      case opt_uidlist:
+	{
+	int count = 1;
+	uid_t *list;
+	int ptr = 0;
+	const uschar *p;
+	const uschar *op = expand_string (sptr);
+
+	if (op == NULL)
+	  log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "failed to expand %s: %s",
+	    name, expand_string_message);
+
+	p = op;
+	if (*p != 0) count++;
+	while (*p != 0) if (*p++ == ':' && *p != 0) count++;
+	list = store_malloc(count*sizeof(uid_t));
+	list[ptr++] = (uid_t)(count - 1);
+
+	if (data_block)
+	  *((uid_t **)(US data_block + ol->v.offset)) = list;
+	else
+	  *((uid_t **)ol->v.value) = list;
 
-      p = op;
-      while (count-- > 1)
-        {
-        int sep = 0;
-	/* If p is tainted we trap.  Not sure that can happen */
-        (void)string_nextinlist(&p, &sep, big_buffer, BIG_BUFFER_SIZE);
-        if (!route_finduser(big_buffer, NULL, &uid))
-          log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "user %s was not found",
-            big_buffer);
-        list[ptr++] = uid;
-        }
-      }
-    break;
+	p = op;
+	while (count-- > 1)
+	  {
+	  int sep = 0;
+	  /* If p is tainted we trap.  Not sure that can happen */
+	  (void)string_nextinlist(&p, &sep, big_buffer, BIG_BUFFER_SIZE);
+	  if (!route_finduser(big_buffer, NULL, &uid))
+	    log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "user %s was not found",
+	      big_buffer);
+	  list[ptr++] = uid;
+	  }
+	break;
+	}
 
-    /* If it was a gid list, look up each individual entry, and build
-    a vector of gids, with a count in the first element. Put the vector
-    in malloc store so we can free the string. (We are reading into permanent
-    store already.) */
+      /* If it was a gid list, look up each individual entry, and build
+      a vector of gids, with a count in the first element. Put the vector
+      in malloc store so we can free the string. (We are reading into permanent
+      store already.) */
 
-    case opt_gidlist:
-      {
-      int count = 1;
-      gid_t *list;
-      int ptr = 0;
-      const uschar *p;
-      const uschar *op = expand_string (sptr);
-
-      if (!op)
-        log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "failed to expand %s: %s",
-          name, expand_string_message);
-
-      p = op;
-      if (*p != 0) count++;
-      while (*p != 0) if (*p++ == ':' && *p != 0) count++;
-      list = store_malloc(count*sizeof(gid_t));
-      list[ptr++] = (gid_t)(count - 1);
-
-      if (data_block)
-        *((gid_t **)(US data_block + ol->v.offset)) = list;
-      else
-        *((gid_t **)ol->v.value) = list;
+      case opt_gidlist:
+	{
+	int count = 1;
+	gid_t *list;
+	int ptr = 0;
+	const uschar *p;
+	const uschar *op = expand_string (sptr);
+
+	if (!op)
+	  log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "failed to expand %s: %s",
+	    name, expand_string_message);
+
+	p = op;
+	if (*p != 0) count++;
+	while (*p != 0) if (*p++ == ':' && *p != 0) count++;
+	list = store_malloc(count*sizeof(gid_t));
+	list[ptr++] = (gid_t)(count - 1);
+
+	if (data_block)
+	  *((gid_t **)(US data_block + ol->v.offset)) = list;
+	else
+	  *((gid_t **)ol->v.value) = list;
 
-      p = op;
-      while (count-- > 1)
-        {
-        int sep = 0;
-	/* If p is tainted we trap.  Not sure that can happen */
-        (void)string_nextinlist(&p, &sep, big_buffer, BIG_BUFFER_SIZE);
-        if (!route_findgroup(big_buffer, &gid))
-          log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "group %s was not found",
-            big_buffer);
-        list[ptr++] = gid;
-        }
+	p = op;
+	while (count-- > 1)
+	  {
+	  int sep = 0;
+	  /* If p is tainted we trap.  Not sure that can happen */
+	  (void)string_nextinlist(&p, &sep, big_buffer, BIG_BUFFER_SIZE);
+	  if (!route_findgroup(big_buffer, &gid))
+	    log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "group %s was not found",
+	      big_buffer);
+	  list[ptr++] = gid;
+	  }
+	break;
+	}
       }
-    break;
-    }
 
-  /* Release store if the value of the string doesn't need to be kept. */
+    /* Release store if the value of the string doesn't need to be kept. */
 
-  if (freesptr) reset_point = store_reset(reset_point);
-  break;
+    if (freesptr) reset_point = store_reset(reset_point);
+    break;
 
   /* Expanded boolean: if no characters follow, or if there are no dollar
   characters, this is a fixed-valued boolean, and we fall through. Otherwise,
   save the string for later expansion in the alternate place. */
 
   case opt_expand_bool:
-  if (*s && Ustrchr(s, '$') != 0)
-    {
-    sprintf(CS name2, "*expand_%.50s", name);
-    if ((ol2 = find_option(name2, oltop, last)))
+    if (*s && Ustrchr(s, '$') != 0)
       {
-      reset_point = store_mark();
-      sptr = read_string(s, name);
-      if (data_block)
-        *(USS(US data_block + ol2->v.offset)) = sptr;
-      else
-        *(USS ol2->v.value) = sptr;
-      freesptr = FALSE;
-      break;
+      sprintf(CS name2, "*expand_%.50s", name);
+      if ((ol2 = find_option(name2, oltop, last)))
+	{
+	reset_point = store_mark();
+	sptr = read_string(s, name);
+	if (data_block)
+	  *(USS(US data_block + ol2->v.offset)) = sptr;
+	else
+	  *(USS ol2->v.value) = sptr;
+	freesptr = FALSE;
+	break;
+	}
       }
-    }
-  /* Fall through */
+    /* Fall through */
 
   /* Boolean: if no characters follow, the value is boolvalue. Otherwise
   look for yes/not/true/false. Some booleans are stored in a single bit in
@@ -2095,121 +2095,121 @@ switch (type)
   case opt_bit:
   case opt_bool_verify:
   case opt_bool_set:
-  if (*s != 0)
-    {
-    s = readconf_readname(name2, EXIM_DRIVERNAME_MAX, s);
-    if (strcmpic(name2, US"true") == 0 || strcmpic(name2, US"yes") == 0)
-      boolvalue = TRUE;
-    else if (strcmpic(name2, US"false") == 0 || strcmpic(name2, US"no") == 0)
-      boolvalue = FALSE;
-    else log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
-      "\"%s\" is not a valid value for the \"%s\" option", name2, name);
-    if (*s != 0) extra_chars_error(s, string_sprintf("\"%s\" ", name2),
-      US"for boolean option ", name);
-    }
+    if (*s)
+      {
+      s = readconf_readname(name2, EXIM_DRIVERNAME_MAX, s);
+      if (strcmpic(name2, US"true") == 0 || strcmpic(name2, US"yes") == 0)
+	boolvalue = TRUE;
+      else if (strcmpic(name2, US"false") == 0 || strcmpic(name2, US"no") == 0)
+	boolvalue = FALSE;
+      else log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
+	"\"%s\" is not a valid value for the \"%s\" option", name2, name);
+      if (*s != 0) extra_chars_error(s, string_sprintf("\"%s\" ", name2),
+	US"for boolean option ", name);
+      }
 
-  /* Handle single-bit type. */
+    /* Handle single-bit type. */
 
-  if (type == opt_bit)
-    {
-    int bit = 1 << ((ol->type >> 16) & 31);
-    int * ptr = data_block
-      ? (int *)(US data_block + ol->v.offset)
-      : (int *)ol->v.value;
-    if (boolvalue) *ptr |= bit; else *ptr &= ~bit;
-    break;
-    }
+    if (type == opt_bit)
+      {
+      int bit = 1 << ((ol->type >> 16) & 31);
+      int * ptr = data_block
+	? (int *)(US data_block + ol->v.offset)
+	: (int *)ol->v.value;
+      if (boolvalue) *ptr |= bit; else *ptr &= ~bit;
+      break;
+      }
 
-  /* Handle full BOOL types */
+    /* Handle full BOOL types */
 
-  if (data_block)
-    *((BOOL *)(US data_block + ol->v.offset)) = boolvalue;
-  else
-    *((BOOL *)ol->v.value) = boolvalue;
+    if (data_block)
+      *((BOOL *)(US data_block + ol->v.offset)) = boolvalue;
+    else
+      *((BOOL *)ol->v.value) = boolvalue;
 
-  /* Verify fudge */
+    /* Verify fudge */
 
-  if (type == opt_bool_verify)
-    {
-    sprintf(CS name2, "%.50s_recipient", name + offset);
-    if ((ol2 = find_option(name2, oltop, last)))
-      if (data_block)
-        *((BOOL *)(US data_block + ol2->v.offset)) = boolvalue;
-      else
-        *((BOOL *)ol2->v.value) = boolvalue;
-    }
+    if (type == opt_bool_verify)
+      {
+      sprintf(CS name2, "%.50s_recipient", name + offset);
+      if ((ol2 = find_option(name2, oltop, last)))
+	if (data_block)
+	  *((BOOL *)(US data_block + ol2->v.offset)) = boolvalue;
+	else
+	  *((BOOL *)ol2->v.value) = boolvalue;
+      }
 
-  /* Note that opt_bool_set type is set, if there is somewhere to do so */
+    /* Note that opt_bool_set type is set, if there is somewhere to do so */
 
-  else if (type == opt_bool_set)
-    {
-    sprintf(CS name2, "*set_%.50s", name + offset);
-    if ((ol2 = find_option(name2, oltop, last)))
-      if (data_block)
-        *((BOOL *)(US data_block + ol2->v.offset)) = TRUE;
-      else
-        *((BOOL *)ol2->v.value) = TRUE;
-    }
-  break;
+    else if (type == opt_bool_set)
+      {
+      sprintf(CS name2, "*set_%.50s", name + offset);
+      if ((ol2 = find_option(name2, oltop, last)))
+	if (data_block)
+	  *((BOOL *)(US data_block + ol2->v.offset)) = TRUE;
+	else
+	  *((BOOL *)ol2->v.value) = TRUE;
+      }
+    break;
 
   /* Octal integer */
 
   case opt_octint:
-  intbase = 8;
-  inttype = US"octal ";
+    intbase = 8;
+    inttype = US"octal ";
 
   /*  Integer: a simple(ish) case; allow octal and hex formats, and
   suffixes K, M, G, and T.  The different types affect output, not input. */
 
   case opt_mkint:
   case opt_int:
-    {
-    uschar *endptr;
-    long int lvalue;
+     {
+      uschar *endptr;
+      long int lvalue;
 
-    errno = 0;
-    lvalue = strtol(CS s, CSS &endptr, intbase);
+      errno = 0;
+      lvalue = strtol(CS s, CSS &endptr, intbase);
 
-    if (endptr == s)
-      log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "%sinteger expected for %s",
-        inttype, name);
-
-    if (errno != ERANGE && *endptr)
-      {
-      uschar * mp = US"TtGgMmKk\0";	/* YyZzEePpTtGgMmKk */
+      if (endptr == s)
+	log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "%sinteger expected for %s",
+	  inttype, name);
 
-      if ((mp = Ustrchr(mp, *endptr)))
+      if (errno != ERANGE && *endptr)
 	{
-	endptr++;
-	do
+	uschar * mp = US"TtGgMmKk\0";	/* YyZzEePpTtGgMmKk */
+
+	if ((mp = Ustrchr(mp, *endptr)))
 	  {
-	  if (lvalue > INT_MAX/1024 || lvalue < INT_MIN/1024)
+	  endptr++;
+	  do
 	    {
-	    errno = ERANGE;
-	    break;
+	    if (lvalue > INT_MAX/1024 || lvalue < INT_MIN/1024)
+	      {
+	      errno = ERANGE;
+	      break;
+	      }
+	    lvalue *= 1024;
 	    }
-	  lvalue *= 1024;
+	  while (*(mp += 2));
 	  }
-	while (*(mp += 2));
 	}
-      }
 
-    if (errno == ERANGE || lvalue > INT_MAX || lvalue < INT_MIN)
-      log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
-        "absolute value of integer \"%s\" is too large (overflow)", s);
+      if (errno == ERANGE || lvalue > INT_MAX || lvalue < INT_MIN)
+	log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
+	  "absolute value of integer \"%s\" is too large (overflow)", s);
 
-    while (isspace(*endptr)) endptr++;
-    if (*endptr)
-      extra_chars_error(endptr, inttype, US"integer value for ", name);
+      while (isspace(*endptr)) endptr++;
+      if (*endptr)
+	extra_chars_error(endptr, inttype, US"integer value for ", name);
 
-    value = (int)lvalue;
-    }
+      value = (int)lvalue;
+     }
 
-  if (data_block)
-    *(int *)(US data_block + ol->v.offset) = value;
-  else
-    *(int *)ol->v.value = value;
-  break;
+    if (data_block)
+      *(int *)(US data_block + ol->v.offset) = value;
+    else
+      *(int *)ol->v.value = value;
+    break;
 
   /*  Integer held in K: again, allow formats and suffixes as above. */
 
@@ -2261,56 +2261,56 @@ switch (type)
   /*  Fixed-point number: held to 3 decimal places. */
 
   case opt_fixed:
-  if (sscanf(CS s, "%d%n", &value, &count) != 1)
-    log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
-      "fixed-point number expected for %s", name);
+    if (sscanf(CS s, "%d%n", &value, &count) != 1)
+      log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
+	"fixed-point number expected for %s", name);
 
-  if (value < 0) log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
-    "integer \"%s\" is too large (overflow)", s);
+    if (value < 0) log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
+      "integer \"%s\" is too large (overflow)", s);
 
-  value *= 1000;
+    value *= 1000;
 
-  if (value < 0) log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
-    "integer \"%s\" is too large (overflow)", s);
+    if (value < 0) log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
+      "integer \"%s\" is too large (overflow)", s);
 
-  /* We get a coverity error here for using count, as it derived
-  from the tainted buffer pointed to by s, as parsed by sscanf().
-  By the definition of sscanf we must be accessing between start
-  and end of s (assuming it is nul-terminated...) so ignore the error.  */
-  /* coverity[tainted_data] */
-  if (s[count] == '.')
-    {
-    int d = 100;
-    while (isdigit(s[++count]))
+    /* We get a coverity error here for using count, as it derived
+    from the tainted buffer pointed to by s, as parsed by sscanf().
+    By the definition of sscanf we must be accessing between start
+    and end of s (assuming it is nul-terminated...) so ignore the error.  */
+    /* coverity[tainted_data] */
+    if (s[count] == '.')
       {
-      value += (s[count] - '0') * d;
-      d /= 10;
+      int d = 100;
+      while (isdigit(s[++count]))
+	{
+	value += (s[count] - '0') * d;
+	d /= 10;
+	}
       }
-    }
 
-  while (isspace(s[count])) count++;
+    while (isspace(s[count])) count++;
 
-  if (s[count] != 0)
-    extra_chars_error(s+count, US"fixed-point value for ", name, US"");
+    if (s[count] != 0)
+      extra_chars_error(s+count, US"fixed-point value for ", name, US"");
 
-  if (data_block)
-    *((int *)(US data_block + ol->v.offset)) = value;
-  else
-    *((int *)ol->v.value) = value;
-  break;
+    if (data_block)
+      *((int *)(US data_block + ol->v.offset)) = value;
+    else
+      *((int *)ol->v.value) = value;
+    break;
 
   /* There's a special routine to read time values. */
 
   case opt_time:
-  value = readconf_readtime(s, 0, FALSE);
-  if (value < 0)
-    log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "invalid time value for %s",
-      name);
-  if (data_block)
-    *((int *)(US data_block + ol->v.offset)) = value;
-  else
-    *((int *)ol->v.value) = value;
-  break;
+    value = readconf_readtime(s, 0, FALSE);
+    if (value < 0)
+      log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN, "invalid time value for %s",
+	name);
+    if (data_block)
+      *((int *)(US data_block + ol->v.offset)) = value;
+    else
+      *((int *)ol->v.value) = value;
+    break;
 
   /* A time list is a list of colon-separated times, with the first
   element holding the size of the list and the second the number of
@@ -3714,14 +3714,14 @@ readconf_driver_init(
   optionlist *driver_optionlist,
   int  driver_optionlist_count)
 {
-driver_instance **p = anchor;
-driver_instance *d = NULL;
-uschar *buffer;
+driver_instance ** p = anchor;
+driver_instance * d = NULL;
+uschar * buffer;
 
 while ((buffer = get_config_line()))
   {
   uschar name[EXIM_DRIVERNAME_MAX];
-  uschar *s;
+  const uschar * s;
 
   /* Read the first name on the line and test for the start of a new driver. A
   macro definition indicates the end of the previous driver. If this isn't the
@@ -4241,8 +4241,6 @@ Returns:      nothing
 static void
 readconf_acl(void)
 {
-uschar *p;
-
 /* Read each ACL and add it into the tree. Macro (re)definitions are allowed
 between ACLs. */
 
@@ -4251,10 +4249,10 @@ acl_line = get_config_line();
 while(acl_line)
   {
   uschar name[EXIM_DRIVERNAME_MAX];
-  tree_node *node;
-  uschar *error;
+  tree_node * node;
+  uschar * error;
+  const uschar * p = readconf_readname(name, sizeof(name), acl_line);
 
-  p = readconf_readname(name, sizeof(name), acl_line);
   if (isupper(*name) && *p == '=')
     {
     if (!macro_read_assignment(acl_line)) exim_exit(EXIT_FAILURE);

commit 5c0ee71aefd482e1bfa8720b62e46bfce280a6a3
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Jun 29 14:14:44 2022 +0100

    Build: fix commentary describing configuration directives

diff --git a/src/src/EDITME b/src/src/EDITME
index 53022e593..3c33dc5f3 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -473,11 +473,11 @@ PCRE2_CONFIG=yes
 
 
 #------------------------------------------------------------------------------
-# Comment out the following line to remove DANE support
-# Note: Enabling this unconditionally overrides DISABLE_DNSSEC
-# forces you to have SUPPORT_TLS enabled (the default).  For DANE under
-# GnuTLS we need an additional library.  See TLS_LIBS or USE_GNUTLS_PC
-# below.
+# Comment out the following line to remove DANE support.
+# Note: DANE support requires DNSSEC support (the default) and
+# SUPPORT_TLS (the default).  For DANE under GnuTLS we need an additional
+# library.  See TLS_LIBS or USE_GNUTLS_PC below.
+
 SUPPORT_DANE=yes
 
 #------------------------------------------------------------------------------
@@ -568,7 +568,7 @@ DISABLE_MAL_MKS=yes
 # By default, Exim has support for checking the AD bit in a DNS response, to
 # determine if DNSSEC validation was successful.  If your system libraries
 # do not support that bit, then set DISABLE_DNSSEC to "yes"
-# Note: Enabling SUPPORT_DANE unconditionally overrides this setting.
+# Note: DNSSEC is required for DANE support.
 
 # DISABLE_DNSSEC=yes
 

commit 6d05006594ffacbfde1c8c4e759332873a702087
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Jul 3 12:42:31 2022 +0100

    OpenSSL: add IP & DN to OCSP fail log line

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 2b8a4e61c..4c61fc0e6 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -2405,7 +2405,7 @@ BIO_puts(bp, "\n");
 }
 
 static int
-tls_client_stapling_cb(SSL *s, void *arg)
+tls_client_stapling_cb(SSL * ssl, void * arg)
 {
 exim_openssl_state_st * cbinfo = arg;
 const unsigned char * p;
@@ -2415,10 +2415,10 @@ OCSP_BASICRESP * bs;
 int i;
 
 DEBUG(D_tls) debug_printf("Received TLS status callback (OCSP stapling):\n");
-len = SSL_get_tlsext_status_ocsp_resp(s, &p);
+len = SSL_get_tlsext_status_ocsp_resp(ssl, &p);
 if(!p)
  {				/* Expect this when we requested ocsp but got none */
-  if (SSL_session_reused(s) && tls_out.ocsp == OCSP_VFIED)
+  if (SSL_session_reused(ssl) && tls_out.ocsp == OCSP_VFIED)
     {
     DEBUG(D_tls) debug_printf(" null, but resumed; ocsp vfy stored with session is good\n");
     return 1;
@@ -2476,9 +2476,19 @@ if (!(bs = OCSP_response_get1_basic(rsp)))
       if (ERR_peek_error())
 	{
 	tls_out.ocsp = OCSP_FAILED;
-	if (LOGGING(tls_cipher)) log_write(0, LOG_MAIN,
-		"Received TLS cert status response, itself unverifiable: %s",
-		ERR_reason_error_string(ERR_peek_error()));
+	if (LOGGING(tls_cipher))
+	  {
+	  const uschar * errstr = CUS ERR_reason_error_string(ERR_peek_error());
+	  static uschar peerdn[256];
+	  X509_NAME_oneline(X509_get_subject_name(SSL_get_peer_certificate(ssl)),
+						  CS peerdn, sizeof(peerdn));
+	  log_write(0, LOG_MAIN,
+		"[%s] %s Received TLS cert (DN: '%.*s') status response, "
+		"itself unverifiable: %s",
+		sender_host_address, sender_host_name,
+		(int)sizeof(peerdn), peerdn,
+		errstr);
+	  }
 	DEBUG(D_tls)
 	  {
 	  BIO_printf(bp, "OCSP response verify failure\n");

commit e7ec503729970a03d4509921342bc81313976126
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Jul 12 22:14:04 2022 +0100

    Fix exit on attempt to rewrite a malformed address.  Bug 2903

diff --git a/src/src/rewrite.c b/src/src/rewrite.c
index bfd78b5f1..90614e626 100644
--- a/src/src/rewrite.c
+++ b/src/src/rewrite.c
@@ -497,15 +497,14 @@ while (*s)
 
   if (!recipient)
     {
-    /* Handle unparesable addresses in the header. Slightly ugly because a
+    /* Log unparesable addresses in the header. Slightly ugly because a
     null output from the extract can also result from a header without an
-    address, "To: undisclosed recpients:;" being the classic case. */
+    address, "To: undisclosed recpients:;" being the classic case. Ignore
+    this one and carry on. */
 
     if ((rewrite_rules || routed_old) && Ustrcmp(errmess, "empty address") != 0)
-      {
       log_write(0, LOG_MAIN, "rewrite: %s", errmess);
-      exim_exit(EXIT_FAILURE);
-      }
+
     loop_reset_point = store_reset(loop_reset_point);
     continue;
     }

commit 7a97480c2bb86d9385597680ec2d4461e1656193
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Jul 19 23:37:27 2022 +0100

    Event for auth fail

diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index edb0adfaf..4a5c44714 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -3765,7 +3765,7 @@ smtp_respond(code, len, TRUE, user_msg);
 
 
 static int
-smtp_in_auth(auth_instance *au, uschar ** s, uschar ** ss)
+smtp_in_auth(auth_instance *au, uschar ** smtp_resp, uschar ** errmsg)
 {
 const uschar *set_id = NULL;
 int rc;
@@ -3829,7 +3829,7 @@ switch(rc)
       received_protocol =
 	(sender_host_address ? protocols : protocols_local)
 	  [pextend + pauthed + (tls_in.active.sock >= 0 ? pcrpted:0)];
-      *s = *ss = US"235 Authentication succeeded";
+      *smtp_resp = *errmsg = US"235 Authentication succeeded";
       authenticated_by = au;
       break;
       }
@@ -3842,34 +3842,34 @@ switch(rc)
 
   case DEFER:
     if (set_id) authenticated_fail_id = string_copy_perm(set_id, TRUE);
-    *s = string_sprintf("435 Unable to authenticate at present%s",
+    *smtp_resp = string_sprintf("435 Unable to authenticate at present%s",
       auth_defer_user_msg);
-    *ss = string_sprintf("435 Unable to authenticate at present%s: %s",
+    *errmsg = string_sprintf("435 Unable to authenticate at present%s: %s",
       set_id, auth_defer_msg);
     break;
 
   case BAD64:
-    *s = *ss = US"501 Invalid base64 data";
+    *smtp_resp = *errmsg = US"501 Invalid base64 data";
     break;
 
   case CANCELLED:
-    *s = *ss = US"501 Authentication cancelled";
+    *smtp_resp = *errmsg = US"501 Authentication cancelled";
     break;
 
   case UNEXPECTED:
-    *s = *ss = US"553 Initial data not expected";
+    *smtp_resp = *errmsg = US"553 Initial data not expected";
     break;
 
   case FAIL:
     if (set_id) authenticated_fail_id = string_copy_perm(set_id, TRUE);
-    *s = US"535 Incorrect authentication data";
-    *ss = string_sprintf("535 Incorrect authentication data%s", set_id);
+    *smtp_resp = US"535 Incorrect authentication data";
+    *errmsg = string_sprintf("535 Incorrect authentication data%s", set_id);
     break;
 
   default:
     if (set_id) authenticated_fail_id = string_copy_perm(set_id, TRUE);
-    *s = US"435 Internal error";
-    *ss = string_sprintf("435 Internal error%s: return %d from authentication "
+    *smtp_resp = US"435 Internal error";
+    *errmsg = string_sprintf("435 Internal error%s: return %d from authentication "
       "check", set_id, rc);
     break;
   }
@@ -4098,7 +4098,13 @@ while (done <= 0)
 	  if (smtp_in_auth(au, &s, &ss) == OK)
 	    { DEBUG(D_auth) debug_printf("tls auth succeeded\n"); }
 	  else
-	    { DEBUG(D_auth) debug_printf("tls auth not succeeded\n"); }
+	    {
+	    uschar * save_name = sender_host_authenticated;
+	    DEBUG(D_auth) debug_printf("tls auth not succeeded\n");
+	    sender_host_authenticated = au->name;
+	    (void) event_raise(event_action, US"auth:fail", s, NULL);
+	    sender_host_authenticated = save_name;
+	    }
 	  }
 	break;
 	}
@@ -4188,6 +4194,8 @@ while (done <= 0)
 
 	{
 	auth_instance * au;
+	uschar * smtp_resp, * errmsg;
+
 	for (au = auths; au; au = au->next)
 	  if (strcmpic(s, au->public_name) == 0 && au->server &&
 	      (au->advertised || f.allow_auth_unadvertised))
@@ -4195,12 +4203,19 @@ while (done <= 0)
 
 	if (au)
 	  {
-	  c = smtp_in_auth(au, &s, &ss);
+	  int rc = smtp_in_auth(au, &smtp_resp, &errmsg);
+
+	  smtp_printf("%s\r\n", FALSE, smtp_resp);
+	  if (rc != OK)
+	    {
+	    uschar * save_name = sender_host_authenticated;
 
-	  smtp_printf("%s\r\n", FALSE, s);
-	  if (c != OK)
 	    log_write(0, LOG_MAIN|LOG_REJECT, "%s authenticator failed for %s: %s",
-	      au->name, host_and_ident(FALSE), ss);
+	      au->name, host_and_ident(FALSE), errmsg);
+	    sender_host_authenticated = au->name;
+	    (void) event_raise(event_action, US"auth:fail", smtp_resp, NULL);
+	    sender_host_authenticated = save_name;
+	    }
 	  }
 	else
 	  done = synprot_error(L_smtp_protocol_error, 504, NULL,
@@ -4389,7 +4404,7 @@ while (done <= 0)
 
       else
 	{
-	char *ss;
+	char * ss;
 	int codelen = 4;
 	smtp_message_code(&smtp_code, &codelen, &user_msg, NULL, TRUE);
 	s = string_sprintf("%.*s%s", codelen, smtp_code, user_msg);

commit 9b3a1518f52a1de4469c85af8dde74489b974a66
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Jul 21 16:41:25 2022 +0100

    Event for auth fail: client side

diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 4a5c44714..11e7436b9 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -4099,11 +4099,16 @@ while (done <= 0)
 	    { DEBUG(D_auth) debug_printf("tls auth succeeded\n"); }
 	  else
 	    {
-	    uschar * save_name = sender_host_authenticated;
 	    DEBUG(D_auth) debug_printf("tls auth not succeeded\n");
-	    sender_host_authenticated = au->name;
-	    (void) event_raise(event_action, US"auth:fail", s, NULL);
-	    sender_host_authenticated = save_name;
+#ifndef DISABLE_EVENT
+	     {
+	      uschar * save_name = sender_host_authenticated, * logmsg;
+	      sender_host_authenticated = au->name;
+	      if ((logmsg = event_raise(event_action, US"auth:fail", s, NULL)))
+		log_write(0, LOG_MAIN, "%s", logmsg);
+	      sender_host_authenticated = save_name;
+	     }
+#endif
 	    }
 	  }
 	break;
@@ -4208,13 +4213,19 @@ while (done <= 0)
 	  smtp_printf("%s\r\n", FALSE, smtp_resp);
 	  if (rc != OK)
 	    {
-	    uschar * save_name = sender_host_authenticated;
-
-	    log_write(0, LOG_MAIN|LOG_REJECT, "%s authenticator failed for %s: %s",
-	      au->name, host_and_ident(FALSE), errmsg);
-	    sender_host_authenticated = au->name;
-	    (void) event_raise(event_action, US"auth:fail", smtp_resp, NULL);
-	    sender_host_authenticated = save_name;
+	    uschar * logmsg = NULL;
+#ifndef DISABLE_EVENT
+	     {uschar * save_name = sender_host_authenticated;
+	      sender_host_authenticated = au->name;
+	      logmsg = event_raise(event_action, US"auth:fail", smtp_resp, NULL);
+	      sender_host_authenticated = save_name;
+	     }
+#endif
+	    if (logmsg)
+	      log_write(0, LOG_MAIN|LOG_REJECT, "%s", logmsg);
+	    else
+	      log_write(0, LOG_MAIN|LOG_REJECT, "%s authenticator failed for %s: %s",
+		au->name, host_and_ident(FALSE), errmsg);
 	    }
 	  }
 	else
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index 6eee04d03..d7f251b0b 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -1476,7 +1476,7 @@ rc = (au->info->clientcode)(au, sx, ob->command_timeout,
 			    sx->buffer, sizeof(sx->buffer));
 sx->outblock.authenticating = FALSE;
 driver_srcfile = authenticator_name = NULL; driver_srcline = 0;
-DEBUG(D_transport) debug_printf("%s authenticator yielded %d\n", au->name, rc);
+DEBUG(D_transport) debug_printf("%s authenticator yielded %s\n", au->name, rc_names[rc]);
 
 /* A temporary authentication failure must hold up delivery to
 this host. After a permanent authentication failure, we carry on
@@ -1500,10 +1500,25 @@ switch(rc)
   /* Failure after reading a response */
 
   case FAIL:
+    {
+    uschar * logmsg = NULL;
+
     if (errno != 0 || sx->buffer[0] != '5') return FAIL;
-    log_write(0, LOG_MAIN, "%s authenticator failed H=%s [%s] %s",
-      au->name, host->name, host->address, sx->buffer);
+#ifndef DISABLE_EVENT
+     {
+      uschar * save_name = sender_host_authenticated;
+      sender_host_authenticated = au->name;
+      if ((logmsg = event_raise(sx->conn_args.tblock->event_action, US"auth:fail",
+				sx->buffer, NULL)))
+	log_write(0, LOG_MAIN, "%s", logmsg);
+      sender_host_authenticated = save_name;
+     }
+#endif
+    if (!logmsg)
+      log_write(0, LOG_MAIN, "%s authenticator failed H=%s [%s] %s",
+	au->name, host->name, host->address, sx->buffer);
     break;
+    }
 
   /* Failure by some other means. In effect, the authenticator
   decided it wasn't prepared to handle this case. Typically this

commit 93c722ce0549360af68269f088f4e59ed8fc130e
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Aug 7 17:00:27 2022 +0100

    SPF: fix memory accounting for error case

diff --git a/src/src/spf.c b/src/src/spf.c
index db6eea3a8..a8c0f75c4 100644
--- a/src/src/spf.c
+++ b/src/src/spf.c
@@ -204,7 +204,7 @@ spf_nxdomain = SPF_dns_rr_new_init(spf_dns_server,
   "", ns_t_any, 24 * 60 * 60, HOST_NOT_FOUND);
 if (!spf_nxdomain)
   {
-  free(spf_dns_server);
+  store_free(spf_dns_server);
   return NULL;
   }
 

commit ef57b25bfa7623c3f8a8e65f927165c4ddc7c43b
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Aug 8 19:46:03 2022 +0100

    Symlink following for TLS creds files

diff --git a/src/src/tls.c b/src/src/tls.c
index 3ed37bbb0..76e72b5f5 100644
--- a/src/src/tls.c
+++ b/src/src/tls.c
@@ -143,15 +143,29 @@ static BOOL
 tls_set_one_watch(const uschar * filename)
 # ifdef EXIM_HAVE_INOTIFY
 {
+uschar buf[PATH_MAX];
+ssize_t len;
 uschar * s;
 
 if (Ustrcmp(filename, "system,cache") == 0) return TRUE;
-
 if (!(s = Ustrrchr(filename, '/'))) return FALSE;
+
+for (unsigned loop = 20;
+     (len = readlink(CCS filename, CS buf, sizeof(buf))) >= 0; )
+  {						/* a symlink */
+  if (--loop == 0) { errno = ELOOP; return FALSE; }
+  filename = buf[0] == '/'
+    ? string_copyn(buf, (unsigned)len)	/* mem released by tls_set_watch */
+    : string_sprintf("%.*s/%.*s", (int)(s - filename), (int)len);
+  s = Ustrrchr(filename, '/');
+  }
+if (errno != EINVAL)
+  return FALSE;					/* other error */
+
+/* not a symlink */
 s = string_copyn(filename, s - filename);	/* mem released by tls_set_watch */
-DEBUG(D_tls) debug_printf("watch dir '%s'\n", s);
 
-/*XXX unclear what effect symlinked files will have for inotify */
+DEBUG(D_tls) debug_printf("watch dir '%s'\n", s);
 
 if (inotify_add_watch(tls_watch_fd, CCS s,
       IN_ONESHOT | IN_CLOSE_WRITE | IN_DELETE | IN_DELETE_SELF

commit b855ee2fb3776667c8b08aa6b80453d60e4fb509
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Aug 9 10:57:56 2022 +0100

    Filter rDNS returns for bad chars

diff --git a/src/src/globals.c b/src/src/globals.c
index c95d24b47..574ee60a4 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -1016,6 +1016,10 @@ uschar *keep_environment       = NULL;
 int     keep_malformed         = 4*24*60*60;    /* 4 days */
 
 uschar *eldap_dn               = NULL;
+const uschar *letter_digit_hyphen_dot =
+    US"abcdefghijklmnopqrstuvwxyz"
+      ".-0123456789"
+      "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 #ifdef EXPERIMENTAL_ESMTP_LIMITS
 uschar *limits_advertise_hosts = US"*";
 #endif
diff --git a/src/src/globals.h b/src/src/globals.h
index c9ef5e484..3f3c798b7 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -680,6 +680,7 @@ extern uschar *keep_environment;       /* Whitelist for environment variables */
 extern int     keep_malformed;         /* Time to keep malformed messages */
 
 extern uschar *eldap_dn;               /* Where LDAP DNs are left */
+extern const uschar *letter_digit_hyphen_dot; /* Legitimate DNS host name chars */
 #ifdef EXPERIMENTAL_ESMTP_LIMITS
 extern uschar *limits_advertise_hosts; /* for banner/EHLO pipelining */
 #endif
diff --git a/src/src/host.c b/src/src/host.c
index fed9f4b5f..b6c2ea082 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -84,13 +84,13 @@ random_number(int limit)
 if (limit < 1)
   return 0;
 if (random_seed == 0)
-  {
-  if (f.running_in_test_harness) random_seed = 42; else
+  if (f.running_in_test_harness)
+    random_seed = 42;
+  else
     {
     int p = (int)getpid();
     random_seed = (int)time(NULL) ^ ((p << 16) | p);
     }
-  }
 random_seed = 1103515245 * random_seed + 12345;
 return (unsigned int)(random_seed >> 16) % limit;
 }
@@ -1646,6 +1646,7 @@ while ((ordername = string_nextinlist(&list, &sep, NULL, 0)))
            rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) if (rr->type == T_PTR)
         {
         uschar * s = store_get(ssize, GET_TAINTED);	/* names are tainted */
+	unsigned slen;
 
         /* If an overlong response was received, the data will have been
         truncated and dn_expand may fail. */
@@ -1658,13 +1659,19 @@ while ((ordername = string_nextinlist(&list, &sep, NULL, 0)))
           break;
           }
 
-        store_release_above(s + Ustrlen(s) + 1);
-        if (!s[0])
+        store_release_above(s + (slen = Ustrlen(s)) + 1);
+        if (!*s)
           {
           HDEBUG(D_host_lookup) debug_printf("IP address lookup yielded an "
             "empty name: treated as non-existent host name\n");
           continue;
           }
+	if (Ustrspn(s, letter_digit_hyphen_dot) != slen)
+          {
+          HDEBUG(D_host_lookup) debug_printf("IP address lookup yielded an "
+            "illegal name (bad char): treated as non-existent host name\n");
+          continue;
+          }
         if (!sender_host_name) sender_host_name = s;
 	else *aptr++ = s;
         while (*s) { *s = tolower(*s); s++; }

commit 766af058762b2c4e21b69f335adf77265fe9509a
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Aug 13 20:58:29 2022 +0100

    Dump stack for "bad memory reference".  Bug 2904

diff --git a/src/src/exim.c b/src/src/exim.c
index 23e206d2a..ea4286af3 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -232,7 +232,7 @@ exit(1);
 ***********************************************/
 
 #define STACKDUMP_MAX 24
-static void
+void
 stackdump(void)
 {
 #ifndef NO_EXECINFO
diff --git a/src/src/functions.h b/src/src/functions.h
index e71823410..92a4831e3 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -547,6 +547,7 @@ extern int     stdin_ferror(void);
 extern BOOL    stdin_hasc(void);
 extern int     stdin_ungetc(int);
 
+extern void    stackdump(void);
 extern void    store_exit(void);
 extern void    store_init(void);
 extern void    store_writeprotect(int);
diff --git a/src/src/store.c b/src/src/store.c
index c98fcbf21..d99d2ab69 100644
--- a/src/src/store.c
+++ b/src/src/store.c
@@ -274,6 +274,9 @@ for (pp = paired_pools; pp < paired_pools + N_PAIRED_POOLS; pp++)
   for (b = pp->chainbase; b; b = b->next)
     if (is_pointer_in_block(b, p)) return pp;
 
+#ifndef COMPILE_UTILITY
+stackdump();
+#endif
 log_write(0, LOG_MAIN|LOG_PANIC_DIE,
   "bad memory reference; pool not found, at %s %d", func, linenumber);
 return NULL;

commit fd9f0b7354ffc2986f0b2e7b074117feb29b5102
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Aug 13 20:19:16 2022 +0100

    Partial workaround for TCP Fast Open issue.  Bug 2907

diff --git a/src/src/smtp_out.c b/src/src/smtp_out.c
index 7b7bdf752..7f364d942 100644
--- a/src/src/smtp_out.c
+++ b/src/src/smtp_out.c
@@ -372,7 +372,7 @@ if (!save_errno)
 # ifdef TCP_FASTOPEN_CONNECT
     else
       {						/* expecting client data */
-      debug_printf(" set up lazy-connect\n");
+      DEBUG(D_transport|D_acl|D_v) debug_printf(" set up lazy-connect\n");
       setsockopt(sock, IPPROTO_TCP, TCP_FASTOPEN_CONNECT, US &on, sizeof(on));
       /* fastopen_blob = NULL;		 lazy TFO, triggered by data write */
       }
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index d7f251b0b..bbff1cad8 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -5312,6 +5312,17 @@ retry_non_continued:
     uschar *retry_message_key = NULL;
     uschar *serialize_key = NULL;
 
+    /* Deal slightly better with a possible Linux kernel bug that results
+    in intermittent TFO-conn fails deep into the TCP flow.  Bug 2907 tracks.
+    Hack: Clear TFO option for any further hosts on this tpt run. */
+
+    if (total_hosts_tried > 0)
+      {
+      DEBUG(D_transport|D_acl|D_v)
+	debug_printf("Clearing TFO as not first host for message\n");
+      ob->hosts_try_fastopen = US"";
+      }
+
     /* Default next host is next host. :-) But this can vary if the
     hosts_max_try limit is hit (see below). It may also be reset if a host
     address is looked up here (in case the host was multihomed). */

commit a1ec98dd963767551514cae86c7af56c6aa3f36e
Author: Ulrich Landgraf <landgraf@physik.uni-freiburg.de>
Date:   Mon Aug 15 16:50:36 2022 +0100

    Fix symlink-following.  Bug 2909
    
    Broken-by: ef57b25bfa

diff --git a/src/src/tls.c b/src/src/tls.c
index 76e72b5f5..32b29ee3e 100644
--- a/src/src/tls.c
+++ b/src/src/tls.c
@@ -156,7 +156,7 @@ for (unsigned loop = 20;
   if (--loop == 0) { errno = ELOOP; return FALSE; }
   filename = buf[0] == '/'
     ? string_copyn(buf, (unsigned)len)	/* mem released by tls_set_watch */
-    : string_sprintf("%.*s/%.*s", (int)(s - filename), (int)len);
+    : string_sprintf("%.*s/%.*s", (int)(s - filename), filename, (int)len, buf);
   s = Ustrrchr(filename, '/');
   }
 if (errno != EINVAL)

commit 9b5fdee115fdd09588bd03f3ebdc85cfe7357fc5
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Aug 15 18:42:28 2022 +0100

    OpenSSL: strip spaces & newlines from cert signature expansion

diff --git a/src/src/tlscert-openssl.c b/src/src/tlscert-openssl.c
index 168e35b7f..ac353b25f 100644
--- a/src/src/tlscert-openssl.c
+++ b/src/src/tlscert-openssl.c
@@ -261,11 +261,19 @@ if (X509_print_ex(bp, (X509 *)cert, 0,
   X509_FLAG_NO_AUX) == 1)
   {
   long len = BIO_get_mem_data(bp, &cp);
+  gstring * g = NULL;
 
   /* Strip leading "Signature Algorithm" line */
   while (*cp && *cp != '\n') { cp++; len--; }
+  if (*cp) { cp++; len--; }
 
-  cp = string_copyn(cp+1, len-1);
+  /* Strip possible leading "    Signature Value:\n" (seen with OpenSSL 3.0.5) */
+  if (Ustrncmp(cp, "    Signature Value:\n", 21) == 0) { cp += 21; len -= 21; }
+
+  /* Copy only hexchars and colon (different OpenSSL versions do different spacing) */
+  for ( ; len-- && *cp; cp++)
+    if (Ustrchr("0123456789abcdef:", *cp)) g = string_catn(g, cp, 1);
+  cp = string_from_gstring(g);
   }
 BIO_free(bp);
 return cp;

commit 4e3a01c2607937d5fbc477b6e14495adc2281941
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Aug 18 20:47:01 2022 +0100

    GSASL: use tls-exporter for SCRAM*PLUS methods under TLSv1.3

diff --git a/src/src/auths/gsasl_exim.c b/src/src/auths/gsasl_exim.c
index bae5f081b..e49e83b81 100644
--- a/src/src/auths/gsasl_exim.c
+++ b/src/src/auths/gsasl_exim.c
@@ -39,22 +39,34 @@ static void dummy(int x) { dummy2(x-1); }
 #include "gsasl_exim.h"
 
 
-#if GSASL_VERSION_MINOR >= 10
-# define EXIM_GSASL_HAVE_SCRAM_SHA_256
-# define EXIM_GSASL_SCRAM_S_KEY
+#if GSASL_VERSION_MAJOR == 2
 
-#elif GSASL_VERSION_MINOR == 9
 # define EXIM_GSASL_HAVE_SCRAM_SHA_256
+# define EXIM_GSASL_SCRAM_S_KEY
+# if GSASL_VERSION_MINOR >= 1
+#  define EXIM_GSASL_HAVE_EXPORTER
+# elif GSASL_VERSION_PATCH >= 1
+#  define EXIM_GSASL_HAVE_EXPORTER
+# endif
 
-# if GSASL_VERSION_PATCH >= 1
+#elif GSASL_VERSION_MAJOR == 1
+# if GSASL_VERSION_MINOR >= 10
+#  define EXIM_GSASL_HAVE_SCRAM_SHA_256
 #  define EXIM_GSASL_SCRAM_S_KEY
-# endif
-# if GSASL_VERSION_PATCH < 2
+
+# elif GSASL_VERSION_MINOR == 9
+#  define EXIM_GSASL_HAVE_SCRAM_SHA_256
+
+#  if GSASL_VERSION_PATCH >= 1
+#   define EXIM_GSASL_SCRAM_S_KEY
+#  endif
+#  if GSASL_VERSION_PATCH < 2
+#   define CHANNELBIND_HACK
+#  endif
+
+# else
 #  define CHANNELBIND_HACK
 # endif
-
-#else
-# define CHANNELBIND_HACK
 #endif
 
 /* Convenience for testing strings */
@@ -258,7 +270,7 @@ if (!cb_state)
   if (prop == GSASL_CB_TLS_UNIQUE)
     {
     uschar * s;
-    if ((s = gsasl_callback_hook_get(ctx)))
+    if ((s = gsasl_callback_hook_get(ctx)))	/* Gross hack for early lib vers */
       {
       HDEBUG(D_auth) debug_printf("GSASL_CB_TLS_UNIQUE from ctx hook\n");
       gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CS s);
@@ -332,6 +344,9 @@ switch (prop)
 #ifdef EXIM_GSASL_SCRAM_S_KEY
   case GSASL_SCRAM_STOREDKEY:		return US"SCRAM_STOREDKEY";
   case GSASL_SCRAM_SERVERKEY:		return US"SCRAM_SERVERKEY";
+#endif
+#ifdef EXIM_GSASL_HAVE_EXPORTER		/* v. 2.1.0 */
+  case GSASL_CB_TLS_EXPORTER:		return US"CB_TLS_EXPORTER";
 #endif
   case GSASL_CB_TLS_UNIQUE:		return US"CB_TLS_UNIQUE";
   case GSASL_SAML20_IDP_IDENTIFIER:	return US"SAML20_IDP_IDENTIFIER";
@@ -351,6 +366,14 @@ switch (prop)
 return CUS string_sprintf("(unknown prop: %d)", (int)prop);
 }
 
+static void
+preload_prop(Gsasl_session * sctx, Gsasl_property propcode, const uschar * val)
+{
+DEBUG(D_auth) debug_printf("preloading prop %s val %s\n",
+  gsasl_prop_code_to_name(propcode), val);
+gsasl_property_set(sctx, propcode, CCS val);
+}
+
 /*************************************************
 *             Server entry point                 *
 *************************************************/
@@ -358,12 +381,12 @@ return CUS string_sprintf("(unknown prop: %d)", (int)prop);
 /* For interface, see auths/README */
 
 int
-auth_gsasl_server(auth_instance *ablock, uschar *initial_data)
+auth_gsasl_server(auth_instance * ablock, uschar * initial_data)
 {
-char *tmps;
-char *to_send, *received;
-Gsasl_session *sctx = NULL;
-auth_gsasl_options_block *ob =
+uschar * tmps;
+char * to_send, * received;
+Gsasl_session * sctx = NULL;
+auth_gsasl_options_block * ob =
   (auth_gsasl_options_block *)(ablock->options_block);
 struct callback_exim_state cb_state;
 int rc, auth_result, exim_error, exim_error_override;
@@ -406,18 +429,18 @@ cb_state.ablock = ablock;
 cb_state.currently = CURRENTLY_SERVER;
 gsasl_session_hook_set(sctx, &cb_state);
 
-tmps = CS expand_string(ob->server_service);
-gsasl_property_set(sctx, GSASL_SERVICE, tmps);
-tmps = CS expand_string(ob->server_hostname);
-gsasl_property_set(sctx, GSASL_HOSTNAME, tmps);
+tmps = expand_string(ob->server_service);
+preload_prop(sctx, GSASL_SERVICE, tmps);
+tmps = expand_string(ob->server_hostname);
+preload_prop(sctx, GSASL_HOSTNAME, tmps);
 if (ob->server_realm)
   {
-  tmps = CS expand_string(ob->server_realm);
+  tmps = expand_string(ob->server_realm);
   if (tmps && *tmps)
-    gsasl_property_set(sctx, GSASL_REALM, tmps);
+    preload_prop(sctx, GSASL_REALM, tmps);
   }
 /* We don't support protection layers. */
-gsasl_property_set(sctx, GSASL_QOPS, "qop-auth");
+preload_prop(sctx, GSASL_QOPS, US "qop-auth");
 
 #ifndef DISABLE_TLS
 if (tls_in.channelbinding)
@@ -451,7 +474,12 @@ if (tls_in.channelbinding)
     HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n",
 	ablock->name);
 # ifndef CHANNELBIND_HACK
-    gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_in.channelbinding);
+    preload_prop(sctx,
+#  ifdef EXIM_GSASL_HAVE_EXPORTER
+      tls_in.channelbind_exporter ? GSASL_CB_TLS_EXPORTER :
+#  endif
+				    GSASL_CB_TLS_UNIQUE,
+      tls_in.channelbinding);
 # endif
     }
   else
@@ -811,13 +839,13 @@ return TRUE;
 
 int
 auth_gsasl_client(
-  auth_instance *ablock,		/* authenticator block */
+  auth_instance * ablock,		/* authenticator block */
   void * sx,				/* connection */
   int timeout,				/* command timeout */
-  uschar *buffer,			/* buffer for reading response */
+  uschar * buffer,			/* buffer for reading response */
   int buffsize)				/* size of buffer */
 {
-auth_gsasl_options_block *ob =
+auth_gsasl_options_block * ob =
   (auth_gsasl_options_block *)(ablock->options_block);
 Gsasl_session * sctx = NULL;
 struct callback_exim_state cb_state;
@@ -883,7 +911,12 @@ if (tls_out.channelbinding)
     HDEBUG(D_auth) debug_printf("Auth %s: Enabling channel-binding\n",
 	ablock->name);
 # ifndef CHANNELBIND_HACK
-    gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding);
+    preload_prop(sctx,
+#  ifdef EXIM_GSASL_HAVE_EXPORTER
+      tls_out.channelbind_exporter ? GSASL_CB_TLS_EXPORTER :
+#  endif
+				     GSASL_CB_TLS_UNIQUE,
+      tls_out.channelbinding);
 # endif
     }
   else
@@ -968,9 +1001,18 @@ HDEBUG(D_auth) debug_printf("GNU SASL callback %s for %s/%s as client\n",
 	    gsasl_prop_code_to_name(prop), ablock->name, ablock->public_name);
 switch (prop)
   {
-  case GSASL_CB_TLS_UNIQUE:	/*XXX should never get called for this */
-    HDEBUG(D_auth)
-      debug_printf(" filling in\n");
+#ifdef EXIM_GSASL_HAVE_EXPORTER
+  case GSASL_CB_TLS_EXPORTER:	/* Should never get called for this, as pre-set */
+    if (!tls_out.channelbind_exporter) break;
+    HDEBUG(D_auth) debug_printf(" filling in\n");
+    gsasl_property_set(sctx, GSASL_CB_TLS_EXPORTER, CCS tls_out.channelbinding);
+    return GSASL_OK;
+#endif
+  case GSASL_CB_TLS_UNIQUE:	/* Should never get called for this, as pre-set */
+#ifdef EXIM_GSASL_HAVE_EXPORTER
+    if (tls_out.channelbind_exporter) break;
+#endif
+    HDEBUG(D_auth) debug_printf(" filling in\n");
     gsasl_property_set(sctx, GSASL_CB_TLS_UNIQUE, CCS tls_out.channelbinding);
     return GSASL_OK;
   case GSASL_SCRAM_SALTED_PASSWORD:
diff --git a/src/src/globals.h b/src/src/globals.h
index 3f3c798b7..c40ae4beb 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -117,6 +117,7 @@ typedef struct {
 #endif
   BOOL	  verify_override:1;	/* certificate_verified only due to tls_try_verify_hosts */
   BOOL	  ext_master_secret:1;	/* extended-master-secret was used */
+  BOOL	  channelbind_exporter:1; /* channelbinding is EXPORTER not UNIQUE */
 } tls_support;
 extern tls_support tls_in;
 extern tls_support tls_out;
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index fcb8f7ac4..7a6db94e1 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -121,6 +121,10 @@ require current GnuTLS, then we'll drop support for the ancient libraries).
 # endif
 #endif
 
+#if GNUTLS_VERSION_NUMBER >= 0x030702
+# define HAVE_GNUTLS_EXPORTER
+#endif
+
 #ifndef DISABLE_OCSP
 # include <gnutls/ocsp.h>
 #endif
@@ -646,14 +650,20 @@ tlsp->channelbinding = NULL;
 #ifdef HAVE_GNUTLS_SESSION_CHANNEL_BINDING
   {
   gnutls_datum_t channel = {.data = NULL, .size = 0};
-  uschar * buf;
   int rc;
 
-# ifdef HAVE_GNUTLS_PRF_RFC5705
+# ifdef HAVE_GNUTLS_EXPORTER
+  if (gnutls_protocol_get_version(state->session) >= GNUTLS_TLS1_3)
+    {
+    rc = gnutls_session_channel_binding(state->session, GNUTLS_CB_TLS_EXPORTER, &channel);
+    tlsp->channelbind_exporter = TRUE;
+    }
+  else
+# elif defined(HAVE_GNUTLS_PRF_RFC5705)
   /* Older libraries may not have GNUTLS_TLS1_3 defined! */
   if (gnutls_protocol_get_version(state->session) > GNUTLS_TLS1_2)
     {
-    buf = store_get(32, state->host ? GET_TAINTED : GET_UNTAINTED);
+    uschar * buf = store_get(32, state->host ? GET_TAINTED : GET_UNTAINTED);
     rc = gnutls_prf_rfc5705(state->session,
 				(size_t)24,  "EXPORTER-Channel-Binding", (size_t)0, "",
 				32, CS buf);
@@ -670,11 +680,11 @@ tlsp->channelbinding = NULL;
     {
     int old_pool = store_pool;
     /* Declare the taintedness of the binding info.  On server, untainted; on
-    client, tainted - being the Finish msg from the server. */
+    client, tainted if we used the Finish msg from the server. */
 
     store_pool = POOL_PERM;
     tlsp->channelbinding = b64encode_taint(CUS channel.data, (int)channel.size,
-					    state->host ? GET_TAINTED : GET_UNTAINTED);
+		!tlsp->channelbind_exporter && state->host ? GET_TAINTED : GET_UNTAINTED);
     store_pool = old_pool;
     DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage\n");
     }
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 4c61fc0e6..22750d273 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -94,6 +94,10 @@ change this guard and punt the issue for a while longer. */
 # define EXIM_HAVE_OPENSSL_CIPHER_GET_ID
 #endif
 
+#if !defined(LIBRESSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER >= 0x030000000L)
+# define EXIM_HAVE_EXPORT_CHNL_BNGNG
+#endif
+
 #if !defined(LIBRESSL_VERSION_NUMBER) \
     || LIBRESSL_VERSION_NUMBER >= 0x20010000L
 # if !defined(OPENSSL_NO_ECDH)
@@ -111,11 +115,16 @@ change this guard and punt the issue for a while longer. */
 #  define OPENSSL_HAVE_KEYLOG_CB
 #  define OPENSSL_HAVE_NUM_TICKETS
 #  define EXIM_HAVE_OPENSSL_CIPHER_STD_NAME
+#  define EXIM_HAVE_EXP_CHNL_BNGNG
 # else
 #  define OPENSSL_BAD_SRVR_OURCERT
 # endif
 #endif
 
+#if !defined(LIBRESSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER >= 0x010002000L)
+# define EXIM_HAVE_EXPORT_CHNL_BNGNG
+#endif
+
 #if !defined(EXIM_HAVE_OPENSSL_TLSEXT) && !defined(DISABLE_OCSP)
 # warning "OpenSSL library version too old; define DISABLE_OCSP in Makefile"
 # define DISABLE_OCSP
@@ -3170,6 +3179,52 @@ tls_dump_keylog(SSL * ssl)
 }
 
 
+/* Channel-binding info for authenticators
+See description in https://paquier.xyz/postgresql-2/channel-binding-openssl/
+for pre-TLS1.3
+*/
+
+static void
+tls_get_channel_binding(SSL * ssl, tls_support * tlsp, const void * taintval)
+{
+uschar c, * s;
+size_t len;
+
+#ifdef EXIM_HAVE_EXPORT_CHNL_BNGNG
+if (SSL_version(ssl) >= TLS1_3_VERSION)
+  {
+  /* It's not documented by OpenSSL how big the output buffer must be.
+  The OpenSSL testcases use 80 bytes but don't say why. The GnuTLS impl only
+  serves out 32B.  RFC 9266 says it is 32B.
+  Interop fails unless we use the same each end. */
+  len = 32;
+
+  tlsp->channelbind_exporter = TRUE;
+  taintval = GET_UNTAINTED;
+  if (SSL_export_keying_material(ssl,
+	s = store_get((int)len, taintval), len,
+	"EXPORTER-Channel-Binding", (size_t) 24,
+	NULL, 0, 0) != 1)
+    len = 0;
+  }
+else
+#endif
+  {
+  len = SSL_get_peer_finished(ssl, &c, 0);
+  len = SSL_get_peer_finished(ssl, s = store_get((int)len, taintval), len);
+  }
+
+if (len > 0)
+  {
+  int old_pool = store_pool;
+  store_pool = POOL_PERM;
+    tlsp->channelbinding = b64encode_taint(CUS s, (int)len, taintval);
+  store_pool = old_pool;
+  DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage %p %p\n", tlsp->channelbinding, tlsp);
+  }
+}
+
+
 /*************************************************
 *       Start a TLS session in a server          *
 *************************************************/
@@ -3446,6 +3501,7 @@ else DEBUG(D_tls)
 adjust the input functions to read via TLS, and initialize things. */
 
 #ifdef SSL_get_extms_support
+/*XXX what does this return for tls1.3 ? */
 tls_in.ext_master_secret = SSL_get_extms_support(ssl) == 1;
 #endif
 peer_cert(ssl, &tls_in, peerdn, sizeof(peerdn));
@@ -3478,19 +3534,7 @@ DEBUG(D_tls)
   tls_in.ourcert = crt ? X509_dup(crt) : NULL;
   }
 
-/* Channel-binding info for authenticators
-See description in https://paquier.xyz/postgresql-2/channel-binding-openssl/ */
-  {
-  uschar c, * s;
-  size_t len = SSL_get_peer_finished(ssl, &c, 0);
-  int old_pool = store_pool;
-
-  SSL_get_peer_finished(ssl, s = store_get((int)len, GET_UNTAINTED), len);
-  store_pool = POOL_PERM;
-    tls_in.channelbinding = b64encode_taint(CUS s, (int)len, GET_UNTAINTED);
-  store_pool = old_pool;
-  DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage %p\n", tls_in.channelbinding);
-  }
+tls_get_channel_binding(ssl, &tls_in, GET_UNTAINTED);
 
 /* Only used by the server-side tls (tls_in), including tls_getc.
    Client-side (tls_out) reads (seem to?) go via
@@ -4168,18 +4212,7 @@ tlsp->cipher_stdname = cipher_stdname_ssl(exim_client_ctx->ssl);
   }
 
 /*XXX will this work with continued-TLS? */
-/* Channel-binding info for authenticators */
-  {
-  uschar c, * s;
-  size_t len = SSL_get_finished(exim_client_ctx->ssl, &c, 0);
-  int old_pool = store_pool;
-
-  SSL_get_finished(exim_client_ctx->ssl, s = store_get((int)len, GET_TAINTED), len);
-  store_pool = POOL_PERM;
-    tlsp->channelbinding = b64encode_taint(CUS s, (int)len, GET_TAINTED);
-  store_pool = old_pool;
-  DEBUG(D_tls) debug_printf("Have channel bindings cached for possible auth usage %p %p\n", tlsp->channelbinding, tlsp);
-  }
+tls_get_channel_binding(exim_client_ctx->ssl, tlsp, GET_TAINTED);
 
 tlsp->active.sock = cctx->sock;
 tlsp->active.tls_ctx = exim_client_ctx;
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index bbff1cad8..0fca4584d 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -4695,7 +4695,10 @@ if (sx->completed_addr && sx->ok && sx->send_quit)
 	    open, we must shut down TLS.  Not all MTAs allow for the continuation
 	    of the SMTP session when TLS is shut down. We test for this by sending
 	    a new EHLO. If we don't get a good response, we don't attempt to pass
-	    the socket on. */
+	    the socket on.
+	    NB: TLS close is *required* per RFC 9266 when tls-exporter info has
+	    been used, which we do under TLSv1.3 for the gsasl SCRAM*PLUS methods.
+	    But we were always doing it anyway. */
 
 	  tls_close(sx->cctx.tls_ctx,
 	    sx->send_tlsclose ? TLS_SHUTDOWN_WAIT : TLS_SHUTDOWN_WONLY);

commit ba47f2909371787b76d213ea012c6b950f9f8080
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Aug 18 21:20:15 2022 +0100

    OpenSSL: unbreak build with older library version
    
    Broken-by: 4e3a01c260

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 22750d273..c46bc75a5 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -3191,7 +3191,7 @@ uschar c, * s;
 size_t len;
 
 #ifdef EXIM_HAVE_EXPORT_CHNL_BNGNG
-if (SSL_version(ssl) >= TLS1_3_VERSION)
+if (SSL_version(ssl) > TLS1_2_VERSION)
   {
   /* It's not documented by OpenSSL how big the output buffer must be.
   The OpenSSL testcases use 80 bytes but don't say why. The GnuTLS impl only

commit 9641b6648d2d2d87e14856f9c3383deb86772757
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Aug 20 16:43:03 2022 +0100

    OpenSSL: fix reload of changed OCSP proof

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index c46bc75a5..c63e56c0e 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -1477,12 +1477,12 @@ return;
 
 
 static void
-ocsp_free_response_list(exim_openssl_state_st * cbinfo)
+ocsp_free_response_list(exim_openssl_state_st * state)
 {
-for (ocsp_resplist * olist = cbinfo->u_ocsp.server.olist; olist;
+for (ocsp_resplist * olist = state->u_ocsp.server.olist; olist;
      olist = olist->next)
   OCSP_RESPONSE_free(olist->resp);
-cbinfo->u_ocsp.server.olist = NULL;
+state->u_ocsp.server.olist = NULL;
 }
 #endif	/*!DISABLE_OCSP*/
 
@@ -1574,6 +1574,11 @@ else
       if (olist && !*olist)
 	olist = NULL;
 
+      /* If doing a re-expand after SNI, avoid reloading the OCSP
+      responses when the list of filenames has not changed.
+      The creds-invali on content change wipes file_expanded, so that
+      always reloads here. */
+
       if (  state->u_ocsp.server.file_expanded && olist
 	 && (Ustrcmp(olist, state->u_ocsp.server.file_expanded) == 0))
 	{
@@ -1918,6 +1923,7 @@ tls_server_creds_invalidate(void)
 {
 SSL_CTX_free(state_server.lib_state.lib_ctx);
 state_server.lib_state = null_tls_preload;
+state_server.u_ocsp.server.file_expanded = NULL;
 }
 
 
@@ -2763,7 +2769,7 @@ if (state->lib_state.conn_certs)
 else
   {
 #ifndef DISABLE_OCSP
-  if (!host)
+  if (!host)					/* server */
     {
     state->u_ocsp.server.file = ocsp_file;
     state->u_ocsp.server.file_expanded = NULL;
diff --git a/src/src/tls.c b/src/src/tls.c
index 32b29ee3e..d7cefce67 100644
--- a/src/src/tls.c
+++ b/src/src/tls.c
@@ -361,6 +361,8 @@ tls_watch_invalidate();
 #endif
 
 tls_server_creds_invalidate();
+
+/* _expire is for a time-limited selfsign server cert */
 tls_creds_expire = (lifetime = tls_server_creds_init())
   ? time(NULL) + lifetime : 0;
 

commit 1072af868662ea8fec30454c2d62afdee24f2c8e
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Aug 23 18:34:29 2022 +0100

    OpenSSL: fix non-OCSP build
    
    Broken-by: 9641b6648d

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index c63e56c0e..043755c84 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -1923,7 +1923,9 @@ tls_server_creds_invalidate(void)
 {
 SSL_CTX_free(state_server.lib_state.lib_ctx);
 state_server.lib_state = null_tls_preload;
+#ifndef DISABLE_OCSP
 state_server.u_ocsp.server.file_expanded = NULL;
+#endif
 }
 
 

commit 4e9ed49f8f12eb331b29bd5b6dc3693c520fddc2
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Aug 31 15:37:40 2022 +0100

    Fix $regex<n> use-after-free.  Bug 2915

diff --git a/src/src/exim.c b/src/src/exim.c
index ea4286af3..b9328f017 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -2000,8 +2000,6 @@ regex_whitelisted_macro =
   regex_must_compile(US"^[A-Za-z0-9_/.-]*$", MCS_NOFLAGS, TRUE);
 #endif
 
-for (i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
-
 /* If the program is called as "mailq" treat it as equivalent to "exim -bp";
 this seems to be a generally accepted convention, since one finds symbolic
 links called "mailq" in standard OS configurations. */
@@ -6089,7 +6087,7 @@ MORELOOP:
   deliver_localpart_data = deliver_domain_data =
   recipient_data = sender_data = NULL;
   acl_var_m = NULL;
-  for(int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
+  regex_vars_clear();
 
   store_reset(reset_point);
   }
diff --git a/src/src/expand.c b/src/src/expand.c
index ffbdc14e5..89de56255 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -1860,7 +1860,7 @@ else if (Ustrncmp(name, "r_", 2) == 0)
   return node ? node->data.ptr : strict_acl_vars ? NULL : US"";
   }
 
-/* Handle $auth<n> variables. */
+/* Handle $auth<n>, $regex<n> variables. */
 
 if (Ustrncmp(name, "auth", 4) == 0)
   {
diff --git a/src/src/functions.h b/src/src/functions.h
index 92a4831e3..345d7bce6 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -447,6 +447,7 @@ extern BOOL    regex_match_and_setup(const pcre2_code *, const uschar *, int, in
 extern const pcre2_code *regex_compile(const uschar *, mcs_flags, uschar **,
 		pcre2_compile_context *);
 extern const pcre2_code *regex_must_compile(const uschar *, mcs_flags, BOOL);
+extern void    regex_vars_clear(void);
 extern void    retry_add_item(address_item *, uschar *, int);
 extern BOOL    retry_check_address(const uschar *, host_item *, uschar *, BOOL,
                  uschar **, uschar **);
diff --git a/src/src/globals.c b/src/src/globals.c
index 574ee60a4..cafb15992 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -1324,7 +1324,7 @@ const pcre2_code *regex_EARLY_PIPE   = NULL;
 int    regex_cachesize		     = 0;
 const pcre2_code *regex_ismsgid      = NULL;
 const pcre2_code *regex_smtp_code    = NULL;
-const uschar *regex_vars[REGEX_VARS];
+const uschar *regex_vars[REGEX_VARS] = { 0 };;
 #ifdef WHITELIST_D_MACROS
 const pcre2_code *regex_whitelisted_macro = NULL;
 #endif
diff --git a/src/src/regex.c b/src/src/regex.c
index 5de1c1704..25496f950 100644
--- a/src/src/regex.c
+++ b/src/src/regex.c
@@ -93,19 +93,27 @@ return FAIL;
 }
 
 
+/* reset expansion variables */
+void
+regex_vars_clear(void)
+{
+regex_match_string = NULL;
+for (int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
+}
+
+
 
 int
-regex(const uschar **listptr, BOOL cacheable)
+regex(const uschar ** listptr, BOOL cacheable)
 {
 unsigned long mbox_size;
-FILE *mbox_file;
-pcre_list *re_list_head;
-uschar *linebuffer;
+FILE * mbox_file;
+pcre_list * re_list_head;
+uschar * linebuffer;
 long f_pos = 0;
 int ret = FAIL;
 
-/* reset expansion variable */
-regex_match_string = NULL;
+regex_vars_clear();
 
 if (!mime_stream)				/* We are in the DATA ACL */
   {
@@ -167,14 +175,13 @@ return ret;
 int
 mime_regex(const uschar **listptr, BOOL cacheable)
 {
-pcre_list *re_list_head = NULL;
-FILE *f;
-uschar *mime_subject = NULL;
+pcre_list * re_list_head = NULL;
+FILE * f;
+uschar * mime_subject = NULL;
 int mime_subject_len = 0;
 int ret;
 
-/* reset expansion variable */
-regex_match_string = NULL;
+regex_vars_clear();
 
 /* precompile our regexes */
 if (!(re_list_head = compile(*listptr, cacheable)))
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 11e7436b9..a15280bdc 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -2157,8 +2157,10 @@ prdr_requested = FALSE;
 #ifdef SUPPORT_I18N
 message_smtputf8 = FALSE;
 #endif
+regex_vars_clear();
 body_linecount = body_zerocount = 0;
 
+lookup_value = NULL;				/* Can be set by ACL */
 sender_rate = sender_rate_limit = sender_rate_period = NULL;
 ratelimiters_mail = NULL;           /* Updated by ratelimit ACL condition */
                    /* Note that ratelimiters_conn persists across resets. */

commit d8ecc7bf97934a1e2244788c610c958cacd740bd
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Aug 31 17:03:37 2022 +0100

    Fix non-WITH_CONTENT_SCAN build.
    
    Broken-by: 4e9ed49f8f

diff --git a/src/src/exim.c b/src/src/exim.c
index b9328f017..82e833067 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1678,6 +1678,17 @@ else
 }
 
 
+/* reset regex expansion variables */
+void
+regex_vars_clear(void)
+{
+regex_match_string = NULL;
+for (int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
+}
+
+
+
+
 
 /*************************************************
 *          Entry point and high-level code       *
diff --git a/src/src/regex.c b/src/src/regex.c
index 25496f950..3088f0066 100644
--- a/src/src/regex.c
+++ b/src/src/regex.c
@@ -93,16 +93,6 @@ return FAIL;
 }
 
 
-/* reset expansion variables */
-void
-regex_vars_clear(void)
-{
-regex_match_string = NULL;
-for (int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
-}
-
-
-
 int
 regex(const uschar ** listptr, BOOL cacheable)
 {

commit 158dff9936e36a2d31d037d3988b9353458d6471
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Aug 31 17:17:59 2022 +0100

    Fix non-WITH_CONTENT_SCAN build (2)
    
    Broken-by: d8ecc7bf97

diff --git a/src/src/exim.c b/src/src/exim.c
index 82e833067..f2a787e8a 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1678,17 +1678,6 @@ else
 }
 
 
-/* reset regex expansion variables */
-void
-regex_vars_clear(void)
-{
-regex_match_string = NULL;
-for (int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
-}
-
-
-
-
 
 /*************************************************
 *          Entry point and high-level code       *
@@ -6092,13 +6081,13 @@ MORELOOP:
   dnslist_domain = dnslist_matched = NULL;
 #ifdef WITH_CONTENT_SCAN
   malware_name = NULL;
+  regex_vars_clear();
 #endif
   callout_address = NULL;
   sending_ip_address = NULL;
   deliver_localpart_data = deliver_domain_data =
   recipient_data = sender_data = NULL;
   acl_var_m = NULL;
-  regex_vars_clear();
 
   store_reset(reset_point);
   }
diff --git a/src/src/functions.h b/src/src/functions.h
index 345d7bce6..d5164547a 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -440,6 +440,7 @@ extern int_eximarith_t receive_statvfs(BOOL, int *);
 extern void    receive_swallow_smtp(void);
 #ifdef WITH_CONTENT_SCAN
 extern int     regex(const uschar **, BOOL);
+extern void    regex_vars_clear(void);
 #endif
 extern void    regex_at_daemon(const uschar *);
 extern BOOL    regex_match(const pcre2_code *, const uschar *, int, uschar **);
@@ -447,7 +448,6 @@ extern BOOL    regex_match_and_setup(const pcre2_code *, const uschar *, int, in
 extern const pcre2_code *regex_compile(const uschar *, mcs_flags, uschar **,
 		pcre2_compile_context *);
 extern const pcre2_code *regex_must_compile(const uschar *, mcs_flags, BOOL);
-extern void    regex_vars_clear(void);
 extern void    retry_add_item(address_item *, uschar *, int);
 extern BOOL    retry_check_address(const uschar *, host_item *, uschar *, BOOL,
                  uschar **, uschar **);
diff --git a/src/src/globals.h b/src/src/globals.h
index c40ae4beb..ae74147d4 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -904,12 +904,12 @@ extern const pcre2_code  *regex_EARLY_PIPE;  /* For recognizing PIPE_CONNCT */
 extern int    regex_cachesize;		     /* number of entries */
 extern const pcre2_code  *regex_ismsgid;     /* Compiled r.e. for message ID */
 extern const pcre2_code  *regex_smtp_code;   /* For recognizing SMTP codes */
-extern const uschar *regex_vars[];           /* $regexN variables */
 #ifdef WHITELIST_D_MACROS
 extern const pcre2_code  *regex_whitelisted_macro; /* For -D macro values */
 #endif
 #ifdef WITH_CONTENT_SCAN
 extern uschar *regex_match_string;     /* regex that matched a line (regex ACL condition) */
+extern const uschar *regex_vars[];
 #endif
 extern int     remote_delivery_count;  /* Number of remote addresses */
 extern int     remote_max_parallel;    /* Maximum parallel delivery */
diff --git a/src/src/regex.c b/src/src/regex.c
index 3088f0066..25496f950 100644
--- a/src/src/regex.c
+++ b/src/src/regex.c
@@ -93,6 +93,16 @@ return FAIL;
 }
 
 
+/* reset expansion variables */
+void
+regex_vars_clear(void)
+{
+regex_match_string = NULL;
+for (int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
+}
+
+
+
 int
 regex(const uschar ** listptr, BOOL cacheable)
 {
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index a15280bdc..28e529bae 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -2157,7 +2157,9 @@ prdr_requested = FALSE;
 #ifdef SUPPORT_I18N
 message_smtputf8 = FALSE;
 #endif
+#ifdef WITH_CONTENT_SCAN
 regex_vars_clear();
+#endif
 body_linecount = body_zerocount = 0;
 
 lookup_value = NULL;				/* Can be set by ACL */

commit 32da6327e434e986a18b75a84f2d8c687ba14619
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Sep 1 15:54:35 2022 +0100

    Fix non-WITH_CONTENT_SCAN build (3)
    
    Broken-by: d8ecc7bf97

diff --git a/src/src/expand.c b/src/src/expand.c
index 89de56255..831ca2b75 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -1869,6 +1869,7 @@ if (Ustrncmp(name, "auth", 4) == 0)
   if (!*endptr && n != 0 && n <= AUTH_VARS)
     return auth_vars[n-1] ? auth_vars[n-1] : US"";
   }
+#ifdef WITH_CONTENT_SCAN
 else if (Ustrncmp(name, "regex", 5) == 0)
   {
   uschar *endptr;
@@ -1876,6 +1877,7 @@ else if (Ustrncmp(name, "regex", 5) == 0)
   if (!*endptr && n != 0 && n <= REGEX_VARS)
     return regex_vars[n-1] ? regex_vars[n-1] : US"";
   }
+#endif
 
 /* For all other variables, search the table */
 
@@ -8715,9 +8717,11 @@ assert_variable_notin() treats as const, so deconst is safe. */
 for (int i = 0; i < AUTH_VARS; i++) if (auth_vars[i])
   assert_variable_notin(US"auth<n>", US auth_vars[i], &e);
 
+#ifdef WITH_CONTENT_SCAN
 /* check regex<n> variables. assert_variable_notin() treats as const. */
 for (int i = 0; i < REGEX_VARS; i++) if (regex_vars[i])
   assert_variable_notin(US"regex<n>", US regex_vars[i], &e);
+#endif
 
 /* check known-name variables */
 for (var_entry * v = var_table; v < var_table + var_table_size; v++)

commit 04644f4ca7c85d5e4dff1bd9e48b3429130e4fdd
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Sep 2 11:15:33 2022 +0100

    EXPERIMENTAL_DCC: make build without WITH_CONTENT_SCAN fail
    rather than auto-include the support

diff --git a/src/src/EDITME b/src/src/EDITME
index 3c33dc5f3..80bd07817 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -596,7 +596,7 @@ DISABLE_MAL_MKS=yes
 
 # Uncomment the following line to add support for talking to dccifd.  This
 # defaults the socket path to /usr/local/dcc/var/dccifd.
-# Doing so will also explicitly turn on the WITH_CONTENT_SCAN option.
+# This support also requires WITH_CONTENT_SCAN enabled.
 
 # EXPERIMENTAL_DCC=yes
 
diff --git a/src/src/acl.c b/src/src/acl.c
index 3af3a4eee..1e7d28a90 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -3125,12 +3125,9 @@ acl_check_condition(int verb, acl_condition_block *cb, int where,
   address_item *addr, int level, BOOL *epp, uschar **user_msgptr,
   uschar **log_msgptr, int *basic_errno)
 {
-uschar *user_message = NULL;
-uschar *log_message = NULL;
+uschar * user_message = NULL;
+uschar * log_message = NULL;
 int rc = OK;
-#ifdef WITH_CONTENT_SCAN
-int sep = -'/';
-#endif
 
 for (; cb; cb = cb->next)
   {
@@ -3646,12 +3643,13 @@ for (; cb; cb = cb->next)
       break;
       }
 
-    #ifdef EXPERIMENTAL_DCC
+#ifdef EXPERIMENTAL_DCC
     case ACLC_DCC:
       {
       /* Separate the regular expression and any optional parameters. */
       const uschar * list = arg;
-      uschar *ss = string_nextinlist(&list, &sep, NULL, 0);
+      int sep = -'/';
+      uschar * ss = string_nextinlist(&list, &sep, NULL, 0);
       /* Run the dcc backend. */
       rc = dcc_process(&ss);
       /* Modify return code based upon the existence of options. */
@@ -3660,13 +3658,13 @@ for (; cb; cb = cb->next)
           rc = FAIL;   /* FAIL so that the message is passed to the next ACL */
       break;
       }
-    #endif
+#endif
 
-    #ifdef WITH_CONTENT_SCAN
+#ifdef WITH_CONTENT_SCAN
     case ACLC_DECODE:
       rc = mime_decode(&arg);
       break;
-    #endif
+#endif
 
     case ACLC_DELAY:
       {
@@ -3813,11 +3811,10 @@ for (; cb; cb = cb->next)
 
     case ACLC_LOG_REJECT_TARGET:
       {
-      int logbits = 0;
-      int sep = 0;
-      const uschar *s = arg;
-      uschar * ss;
-      while ((ss = string_nextinlist(&s, &sep, NULL, 0)))
+      int logbits = 0, sep = 0;
+      const uschar * s = arg;
+
+      for (uschar * ss; ss = string_nextinlist(&s, &sep, NULL, 0); )
         {
         if (Ustrcmp(ss, "main") == 0) logbits |= LOG_MAIN;
         else if (Ustrcmp(ss, "panic") == 0) logbits |= LOG_PANIC;
@@ -3865,17 +3862,16 @@ for (; cb; cb = cb->next)
       break;
       }
 
-    #ifdef WITH_CONTENT_SCAN
+#ifdef WITH_CONTENT_SCAN
     case ACLC_MALWARE:			/* Run the malware backend. */
       {
       /* Separate the regular expression and any optional parameters. */
       const uschar * list = arg;
-      uschar * ss = string_nextinlist(&list, &sep, NULL, 0);
-      uschar * opt;
       BOOL defer_ok = FALSE;
-      int timeout = 0;
+      int timeout = 0, sep = -'/';
+      uschar * ss = string_nextinlist(&list, &sep, NULL, 0);
 
-      while ((opt = string_nextinlist(&list, &sep, NULL, 0)))
+      for (uschar * opt; opt = string_nextinlist(&list, &sep, NULL, 0); )
         if (strcmpic(opt, US"defer_ok") == 0)
 	  defer_ok = TRUE;
 	else if (  strncmpic(opt, US"tmo=", 4) == 0
@@ -3895,7 +3891,7 @@ for (; cb; cb = cb->next)
     case ACLC_MIME_REGEX:
       rc = mime_regex(&arg, textonly);
       break;
-    #endif
+#endif
 
     case ACLC_QUEUE:
       if (is_tainted(arg))
@@ -3979,7 +3975,8 @@ for (; cb; cb = cb->next)
       {
       /* Separate the regular expression and any optional parameters. */
       const uschar * list = arg;
-      uschar *ss = string_nextinlist(&list, &sep, NULL, 0);
+      int sep = -'/';
+      uschar * ss = string_nextinlist(&list, &sep, NULL, 0);
 
       rc = spam(CUSS &ss);
       /* Modify return code based upon the existence of options. */
diff --git a/src/src/buildconfig.c b/src/src/buildconfig.c
index f9a8febdf..bbbaefe9c 100644
--- a/src/src/buildconfig.c
+++ b/src/src/buildconfig.c
@@ -740,19 +740,6 @@ else if (isgroup)
     continue;
     }
 
-  /* WITH_CONTENT_SCAN is another special case: it must be set if it or
-  EXPERIMENTAL_DCC is set. */
-
-  if (strcmp(name, "WITH_CONTENT_SCAN") == 0)
-    {
-    char *wcs = getenv("WITH_CONTENT_SCAN");
-    char *dcc = getenv("EXPERIMENTAL_DCC");
-    fprintf(new, wcs || dcc
-      ? "#define WITH_CONTENT_SCAN     yes\n"
-      : "/* WITH_CONTENT_SCAN not set */\n");
-    continue;
-    }
-
   /* DISABLE_DKIM is special; must be forced if DISABLE_TLS */
   if (strcmp(name, "DISABLE_DKIM") == 0)
     {
diff --git a/src/src/dcc.c b/src/src/dcc.c
index a9124a013..56dd6d570 100644
--- a/src/src/dcc.c
+++ b/src/src/dcc.c
@@ -7,13 +7,17 @@
  * wbreyha@gmx.net
  * See the file NOTICE for conditions of use and distribution.
  *
- * Copyright (c) The Exim Maintainers 2015 - 2021
+ * Copyright (c) The Exim Maintainers 2015 - 2022
  */
 
 /* Code for calling dccifd. Called from acl.c. */
 
 #include "exim.h"
 #ifdef EXPERIMENTAL_DCC
+#ifndef WITH_CONTENT_SCAN
+# error EXPERIMENTAL_DCC requires WITH_CONTENT_SCAN
+#endif
+
 #include "dcc.h"
 #include "unistd.h"
 

commit a3d3e7ef81a649d2bbd6599fc561cf22c6875e70
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Sep 22 15:09:07 2022 +0100

    Fix ${filter } for conditions modifying $value

diff --git a/src/src/expand.c b/src/src/expand.c
index 831ca2b75..ff0b4d600 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -6544,6 +6544,9 @@ while (*s)
         if (item_type == EITEM_FILTER)
           {
           BOOL condresult;
+	  /* the condition could modify $value, as a side-effect */
+	  uschar * save_value = lookup_value;
+
           if (!eval_condition(expr, &resetok, &condresult))
             {
             iterate_item = save_iterate_item;
@@ -6552,6 +6555,7 @@ while (*s)
               expand_string_message, name);
             goto EXPAND_FAILED;
             }
+	  lookup_value = save_value;
           DEBUG(D_expand) debug_printf_indent("%s: condition is %s\n", name,
             condresult? "true":"false");
           if (condresult)
@@ -6560,14 +6564,12 @@ while (*s)
             continue;               /* FALSE => skip this item */
           }
 
-        /* EITEM_MAP and EITEM_REDUCE */
-
-        else
+        else			/* EITEM_MAP and EITEM_REDUCE */
           {
+	  /* the expansion could modify $value, as a side-effect */
 	  uschar * t = expand_string_internal(expr,
 	    ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, NULL, &resetok, NULL);
-          temp = t;
-          if (!temp)
+          if (!(temp = t))
             {
             iterate_item = save_iterate_item;
             expand_string_message = string_sprintf("%s inside \"%s\" item",

commit ece23f05d6a430a461a75639197271c23f6858ec
Author: Jasen Betts <jasen@xnet.co.nz>
Date:   Fri Sep 30 13:49:41 2022 +0100

    GnuTLS: fix for clients offering no TLS extensions

diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index 7a6db94e1..1fc7828cf 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -1142,8 +1142,9 @@ tls_server_clienthello_cb(gnutls_session_t session, unsigned int htype,
   unsigned when, unsigned int incoming, const gnutls_datum_t * msg)
 {
 /* Call fn for each extension seen.  3.6.3 onwards */
-return gnutls_ext_raw_parse(NULL, tls_server_clienthello_ext, msg,
+int rc = gnutls_ext_raw_parse(NULL, tls_server_clienthello_ext, msg,
 			   GNUTLS_EXT_RAW_FLAG_TLS_CLIENT_HELLO);
+return rc == GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE ? 0 : rc;
 }
 
 
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 043755c84..0129fb93e 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -951,36 +951,35 @@ Returns:    nothing
 */
 
 static void
-info_callback(SSL *s, int where, int ret)
+info_callback(SSL * s, int where, int ret)
 {
 DEBUG(D_tls)
   {
-  const uschar * str;
-
-  if (where & SSL_ST_CONNECT)
-     str = US"SSL_connect";
-  else if (where & SSL_ST_ACCEPT)
-     str = US"SSL_accept";
-  else
-     str = US"SSL info (undefined)";
+  gstring * g = NULL;
+
+  if (where & SSL_ST_CONNECT) g = string_append_listele(g, ',', US"SSL_connect");
+  if (where & SSL_ST_ACCEPT)  g = string_append_listele(g, ',', US"SSL_accept");
+  if (where & SSL_CB_LOOP)    g = string_append_listele(g, ',', US"state_chg");
+  if (where & SSL_CB_EXIT)    g = string_append_listele(g, ',', US"hshake_exit");
+  if (where & SSL_CB_READ)    g = string_append_listele(g, ',', US"read");
+  if (where & SSL_CB_WRITE)   g = string_append_listele(g, ',', US"write");
+  if (where & SSL_CB_ALERT)   g = string_append_listele(g, ',', US"alert");
+  if (where & SSL_CB_HANDSHAKE_START) g = string_append_listele(g, ',', US"hshake_start");
+  if (where & SSL_CB_HANDSHAKE_DONE)  g = string_append_listele(g, ',', US"hshake_done");
 
   if (where & SSL_CB_LOOP)
-     debug_printf("%s: %s\n", str, SSL_state_string_long(s));
+     debug_printf("SSL %s: %s\n", g->s, SSL_state_string_long(s));
   else if (where & SSL_CB_ALERT)
-    debug_printf("SSL3 alert %s:%s:%s\n",
-	  str = where & SSL_CB_READ ? US"read" : US"write",
+    debug_printf("SSL %s %s:%s\n", g->s,
 	  SSL_alert_type_string_long(ret), SSL_alert_desc_string_long(ret));
   else if (where & SSL_CB_EXIT)
     {
-    if (ret == 0)
-      debug_printf("%s: failed in %s\n", str, SSL_state_string_long(s));
-    else if (ret < 0)
-      debug_printf("%s: error in %s\n", str, SSL_state_string_long(s));
+    if (ret <= 0)
+      debug_printf("SSL %s: %s in %s\n", g->s,
+	ret == 0 ? "failed" : "error", SSL_state_string_long(s));
     }
-  else if (where & SSL_CB_HANDSHAKE_START)
-     debug_printf("%s: hshake start: %s\n", str, SSL_state_string_long(s));
-  else if (where & SSL_CB_HANDSHAKE_DONE)
-     debug_printf("%s: hshake done: %s\n", str, SSL_state_string_long(s));
+  else if (where & (SSL_CB_HANDSHAKE_START | SSL_CB_HANDSHAKE_DONE))
+     debug_printf("SSL %s: %s\n", g->s, SSL_state_string_long(s));
   }
 }
 

commit 96751ae76e6c6db435f5a4f141511d6f973b6583
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Oct 4 13:20:29 2022 +0100

    OpenSSL: fix configuration of older TLS protocol versions

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 0129fb93e..9ddb16fc4 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -2210,7 +2210,9 @@ already exists.  Might even need this selfsame callback, for reneg? */
   SSL_CTX * ctx = state_server.lib_state.lib_ctx;
   SSL_CTX_set_info_callback(server_sni, SSL_CTX_get_info_callback(ctx));
   SSL_CTX_set_mode(server_sni, SSL_CTX_get_mode(ctx));
+  SSL_CTX_set_min_proto_version(server_sni, SSL3_VERSION);
   SSL_CTX_set_options(server_sni, SSL_CTX_get_options(ctx));
+  SSL_CTX_clear_options(server_sni, ~SSL_CTX_get_options(ctx));
   SSL_CTX_set_timeout(server_sni, SSL_CTX_get_timeout(ctx));
   SSL_CTX_set_tlsext_servername_callback(server_sni, tls_servername_cb);
   SSL_CTX_set_tlsext_servername_arg(server_sni, state);
@@ -2726,10 +2728,15 @@ if (init_options)
     }
 #endif
 
-  DEBUG(D_tls) debug_printf("setting SSL CTX options: %#lx\n", init_options);
-  if (!(SSL_CTX_set_options(ctx, init_options)))
-    return tls_error(string_sprintf(
+  SSL_CTX_set_min_proto_version(ctx, SSL3_VERSION);
+  DEBUG(D_tls) debug_printf("setting  SSL CTX options: %016lx\n", init_options);
+  SSL_CTX_set_options(ctx, init_options);
+   {
+    ulong readback = SSL_CTX_clear_options(ctx, ~init_options);
+    if (readback != init_options)
+      return tls_error(string_sprintf(
           "SSL_CTX_set_option(%#lx)", init_options), host, NULL, errstr);
+   }
   }
 else
   DEBUG(D_tls) debug_printf("no SSL CTX options to set\n");

commit 27e646d341ea843245b8305166686c2fd888bf0e
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Oct 4 19:11:03 2022 +0100

    Fix build with older OpenSSL.
    
    Broken-by: 96751ae76e

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 9ddb16fc4..b46c0c7fd 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -48,6 +48,7 @@ functions from the OpenSSL library. */
 #if OPENSSL_VERSION_NUMBER >= 0x10100000L
 # define EXIM_HAVE_OCSP_RESP_COUNT
 # define OPENSSL_AUTO_SHA256
+# define OPENSSL_MIN_PROTO_VERSION
 #else
 # define EXIM_HAVE_EPHEM_RSA_KEX
 # define EXIM_HAVE_RAND_PSEUDO
@@ -2210,7 +2211,9 @@ already exists.  Might even need this selfsame callback, for reneg? */
   SSL_CTX * ctx = state_server.lib_state.lib_ctx;
   SSL_CTX_set_info_callback(server_sni, SSL_CTX_get_info_callback(ctx));
   SSL_CTX_set_mode(server_sni, SSL_CTX_get_mode(ctx));
+#ifdef OPENSSL_MIN_PROTO_VERSION
   SSL_CTX_set_min_proto_version(server_sni, SSL3_VERSION);
+#endif
   SSL_CTX_set_options(server_sni, SSL_CTX_get_options(ctx));
   SSL_CTX_clear_options(server_sni, ~SSL_CTX_get_options(ctx));
   SSL_CTX_set_timeout(server_sni, SSL_CTX_get_timeout(ctx));
@@ -2728,7 +2731,9 @@ if (init_options)
     }
 #endif
 
+#ifdef OPENSSL_MIN_PROTO_VERSION
   SSL_CTX_set_min_proto_version(ctx, SSL3_VERSION);
+#endif
   DEBUG(D_tls) debug_printf("setting  SSL CTX options: %016lx\n", init_options);
   SSL_CTX_set_options(ctx, init_options);
    {

commit 635e657ce2c75c781d40469bbdd28645e64d89b0
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Oct 4 19:12:13 2022 +0100

    Quieten clang build

diff --git a/src/src/mytypes.h b/src/src/mytypes.h
index 49fed0a9d..141d2c613 100644
--- a/src/src/mytypes.h
+++ b/src/src/mytypes.h
@@ -42,8 +42,12 @@ so we have to give up on all of the available parameter checking. */
 # define FUNC_MAYBE_UNUSED	__attribute__((__unused__))
 # define WARN_UNUSED_RESULT	__attribute__((__warn_unused_result__))
 # define ALLOC			__attribute__((malloc))
-# define ALLOC_SIZE(A)		__attribute__((alloc_size(A)))
 # define NORETURN		__attribute__((noreturn))
+# ifndef __clang__
+#  define ALLOC_SIZE(A)		__attribute__((alloc_size(A)))
+# else
+#  define ALLOC_SIZE(A)		/**/
+# endif
 #else
 # define ARG_UNUSED		/**/
 # define FUNC_MAYBE_UNUSED	/**/

commit 699f306744e5f0e1ad860a460454efe85fe63c74
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Oct 4 21:30:55 2022 +0100

    Unbreak FreeBSD build
    
    Broken-by: 96751ae76e

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index b46c0c7fd..68ad6f15b 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -2737,7 +2737,7 @@ if (init_options)
   DEBUG(D_tls) debug_printf("setting  SSL CTX options: %016lx\n", init_options);
   SSL_CTX_set_options(ctx, init_options);
    {
-    ulong readback = SSL_CTX_clear_options(ctx, ~init_options);
+    uint64_t readback = SSL_CTX_clear_options(ctx, ~init_options);
     if (readback != init_options)
       return tls_error(string_sprintf(
           "SSL_CTX_set_option(%#lx)", init_options), host, NULL, errstr);

commit 1561c5d88b3a23a4348d8e3c1ce28554fcbcfe46
Author: Heiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Date:   Sat Oct 15 19:30:58 2022 +0200

    Fix: Build with libopendmarc 1.4.x (fixes 2728)

diff --git a/src/src/EDITME b/src/src/EDITME
index 80bd07817..28d3d538e 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -602,14 +602,17 @@ DISABLE_MAL_MKS=yes
 
 # Uncomment the following line to add DMARC checking capability, implemented
 # using libopendmarc libraries. You must have SPF and DKIM support enabled also.
-# Library version libopendmarc-1.4.1-1.fc33.x86_64  (on Fedora 33) is known broken;
-# 1.3.2-3 works.  I seems that the OpenDMARC project broke their API.
 # SUPPORT_DMARC=yes
 # CFLAGS += -I/usr/local/include
 # LDFLAGS += -lopendmarc
 # Uncomment the following if you need to change the default. You can
 # override it at runtime (main config option dmarc_tld_file)
 # DMARC_TLD_FILE=/etc/exim/opendmarc.tlds
+#
+# Library version libopendmarc-1.4.1-1.fc33.x86_64  (on Fedora 33) is known broken;
+# 1.3.2-3 works.  It seems that the OpenDMARC project broke their API.
+# Use this option if you need to build with an old library (1.3.x)
+# DMARC_API=100300
 
 # Uncomment the following line to add ARC (Authenticated Received Chain)
 # support.  You must have SPF and DKIM support enabled also.
diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults
index 25ab75506..221705224 100644
--- a/src/src/config.h.defaults
+++ b/src/src/config.h.defaults
@@ -150,6 +150,7 @@ Do not put spaces between # and the 'define'.
 #define SUPPORT_CRYPTEQ
 #define SUPPORT_DANE
 #define SUPPORT_DMARC
+#define DMARC_API 100400
 #define DMARC_TLD_FILE "/etc/exim/opendmarc.tlds"
 #define SUPPORT_I18N
 #define SUPPORT_I18N_2008
diff --git a/src/src/dmarc.c b/src/src/dmarc.c
index 17bba9d75..ad0c26c91 100644
--- a/src/src/dmarc.c
+++ b/src/src/dmarc.c
@@ -459,7 +459,12 @@ if (!dmarc_abort && !sender_host_authenticated)
 		  vs == PDKIM_VERIFY_INVALID ? DMARC_POLICY_DKIM_OUTCOME_TMPFAIL :
 		  DMARC_POLICY_DKIM_OUTCOME_NONE;
     libdm_status = opendmarc_policy_store_dkim(dmarc_pctx, US sig->domain,
-					       dkim_result, US"");
+/* The opendmarc project broke its API in a way we can't detect * easily.
+ * The EDITME provides a DMARC_API variable */
+#if DMARC_API >= 100400
+                                               sig->selector,
+#endif
+                                               dkim_result, US"");
     DEBUG(D_receive)
       debug_printf("DMARC adding DKIM sender domain = %s\n", sig->domain);
     if (libdm_status != DMARC_PARSE_OKAY)

commit 12fb3842f81bcbd4a4519d5728f2d7e0e3ca1445
Author: Lorenz Brun <lorenz@brun.one>
Date:   Fri Oct 14 21:02:51 2022 +0200

    DMARC: fix use-after-free in dmarc_dns_lookup
    
    This fixes a use-after-free in dmarc_dns_lookup where the result
    of dns_lookup in dnsa is freed before the required data is copied out.
    
    Fixes: 9258363 ("DNS: explicit alloc/free of workspace")

diff --git a/src/src/dmarc.c b/src/src/dmarc.c
index ad0c26c91..53c2752ac 100644
--- a/src/src/dmarc.c
+++ b/src/src/dmarc.c
@@ -230,8 +230,9 @@ if (rc == DNS_SUCCEED)
        rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
     if (rr->type == T_TXT && rr->size > 3)
       {
+      uschar *record = string_copyn_taint(US rr->data, rr->size, GET_TAINTED);
       store_free_dns_answer(dnsa);
-      return string_copyn_taint(US rr->data, rr->size, GET_TAINTED);
+      return record;
       }
 store_free_dns_answer(dnsa);
 return NULL;

commit d5eda1c258df2996ace50a723ba8b1d78bb86be6
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Oct 31 10:18:28 2022 +0000

    Release process: permit minor version numbers after 99


commit 221321d2c51b83d1feced80ecd6c2fe33ec5456c
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Nov 3 20:08:25 2022 +0000

    Fix daemon startup.  Bug 2930
    
    Broken-by: 7d5055276a

diff --git a/src/src/daemon.c b/src/src/daemon.c
index 54725e07d..8446f81cb 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -1776,15 +1776,19 @@ if (f.background_daemon)
   daemon as the result of a SIGHUP. In this case, there is no need to do
   anything, because the controlling terminal has long gone. Otherwise, fork, in
   case current process is a process group leader (see 'man setsid' for an
-  explanation) before calling setsid(). */
+  explanation) before calling setsid().
+  All other forks want daemon_listen cleared. Rather than blow a register, jsut
+  restore it here. */
 
   if (getppid() != 1)
     {
+    BOOL daemon_listen = f.daemon_listen;
     pid_t pid = exim_fork(US"daemon");
     if (pid < 0) log_write(0, LOG_MAIN|LOG_PANIC_DIE,
       "fork() failed when starting daemon: %s", strerror(errno));
     if (pid > 0) exit(EXIT_SUCCESS);      /* in parent process, just exit */
     (void)setsid();                       /* release controlling terminal */
+    f.daemon_listen = daemon_listen;
     }
   }
 
@@ -2122,7 +2126,7 @@ else if (f.daemon_listen)
 	      if (*--p == '}') *p = '\0';	/* drop EOL */
 	      while (isdigit(*--p)) ;		/* char before port */
 
-	      i2->log = *p == ':'		/* no list yet? */
+	      i2->log = *p == ':'		/* no list yet?     { */
 		? string_sprintf("%.*s{%s,%d}",
 		  (int)(p - i2->log + 1), i2->log, p+1, ipa->port)
 		: string_sprintf("%s,%d}", i2->log, ipa->port);

commit 6b331d5834d12bdda21857cd6fffac17038ce3c7
Author: Ruben Jenster <r.jenster@drachenfels.de>
Date:   Thu Nov 3 21:38:15 2022 +0000

    Fix $reccipients after ${run...}.  Bug 2929
    
    Broken-by: cfe6acff2d

diff --git a/src/src/transport.c b/src/src/transport.c
index cce1c46ae..7477882cb 100644
--- a/src/src/transport.c
+++ b/src/src/transport.c
@@ -2344,9 +2344,10 @@ if (expand_arguments)
     else
       {
       const uschar *expanded_arg;
+      BOOL enable_dollar_recipients_g = f.enable_dollar_recipients;
       f.enable_dollar_recipients = allow_dollar_recipients;
       expanded_arg = expand_cstring(argv[i]);
-      f.enable_dollar_recipients = FALSE;
+      f.enable_dollar_recipients = enable_dollar_recipients_g;
 
       if (!expanded_arg)
         {

commit 20edc59755798df57b094d1f5ccf24726acbe319
Author: Mehmet Suslu <msuslu@gmail.com>
Date:   Tue Mar 1 10:33:23 2022 +0300

    typo: error message referenced wrong option
    
    Fix typo, -oMas -> -oMai
    
    (cherry picked from commit 330ab60ef29c01fb472b6ce14e935ccb93e905ac)

diff --git a/src/src/exim.c b/src/src/exim.c
index f2a787e8a..b3fd9eff0 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -3329,7 +3329,7 @@ on the second character (the one after '-'), to save some effort. */
 
 	else if (Ustrcmp(argrest, "ai") == 0)
 	  authenticated_id = string_copy_taint(
-	    exim_str_fail_toolong(argv[++i], EXIM_EMAILADDR_MAX, "-oMas"),
+	    exim_str_fail_toolong(argv[++i], EXIM_EMAILADDR_MAX, "-oMai"),
 	    GET_TAINTED);
 
 	/* -oMi: Set incoming interface address */

commit 36d65c9fc4bd2f0f260439fa624b667e29e5a280
Author: Tim Gates <tim.gates@iress.com>
Date:   Thu Dec 17 21:07:18 2020 +1100

    typo: code comment signture -> signature
    
    There is a small typo in src/src/globals.h.
    
    Should read `signature` rather than `signture`.

diff --git a/src/src/globals.h b/src/src/globals.h
index ae74147d4..1be96daec 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -545,7 +545,7 @@ extern uschar *dkim_signing_selector;  /* Expansion variable, selector used for
 extern uschar *dkim_verify_hashes;     /* Preference order for signatures */
 extern uschar *dkim_verify_keytypes;   /* Preference order for signatures */
 extern uschar *dkim_verify_min_keysizes; /* list of minimum key sizes, keyed by algo */
-extern BOOL    dkim_verify_minimal;    /* Shortcircuit signture verification */
+extern BOOL    dkim_verify_minimal;    /* Shortcircuit signature verification */
 extern uschar *dkim_verify_overall;    /* First successful domain verified, or null */
 extern uschar *dkim_verify_signers;    /* Colon-separated list of domains for each of which we call the DKIM ACL */
 extern uschar *dkim_verify_status;     /* result for this signature */

commit e63825824cc406c160ccbf2b154c5d81b168604a
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Nov 11 00:05:59 2022 +0000

    Fix regext substring capture variables for null matches.  Bug 2933
    
    broken-by: 59d66fdc13f0

diff --git a/src/src/exim.c b/src/src/exim.c
index b3fd9eff0..47a685aa7 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -134,6 +134,8 @@ if ((yield = (res >= 0)))
     PCRE2_SIZE len;
     pcre2_substring_get_bynumber(md, matchnum,
       (PCRE2_UCHAR **)&expand_nstring[expand_nmax], &len);
+    if (!expand_nstring[expand_nmax])
+      { expand_nstring[expand_nmax] = US""; len = 0; }
     expand_nlength[expand_nmax++] = (int)len;
     }
   expand_nmax--;
diff --git a/src/src/malware.c b/src/src/malware.c
index 8b5ec27c4..423a5b692 100644
--- a/src/src/malware.c
+++ b/src/src/malware.c
@@ -314,7 +314,10 @@ PCRE2_UCHAR * substr = NULL;
 PCRE2_SIZE slen;
 
 if (i >= 2)				/* Got it */
+  {
   pcre2_substring_get_bynumber(md, 1, &substr, &slen);	/* uses same ctx as md */
+  if (!substr) substr = US"";
+  }
 /* pcre2_match_data_free(md);	gen ctx needs no free */
 return US substr;
 }
diff --git a/src/src/regex.c b/src/src/regex.c
index 25496f950..b401ba0d7 100644
--- a/src/src/regex.c
+++ b/src/src/regex.c
@@ -82,7 +82,7 @@ for (pcre_list * ri = re_list_head; ri; ri = ri->next)
       PCRE2_UCHAR * cstr;
       PCRE2_SIZE cslen;
       pcre2_substring_get_bynumber(md, nn, &cstr, &cslen);	/* uses same ctx as md */
-      regex_vars[nn-1] = CUS cstr;
+      regex_vars[nn-1] = cstr ? CUS cstr : CUS"";
       }
 
     return OK;

commit 7ad1a2b2cc57b5f4bcb59186a9a8abcbed9f4f76
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Nov 11 18:22:00 2022 +0000

    Fix regex substring capture variables for null matches (again).  Bug 2933
    
    Broken-by: 59d66fdc13f0

diff --git a/src/src/exim.c b/src/src/exim.c
index 47a685aa7..16c0184e0 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -127,16 +127,15 @@ BOOL yield;
 
 if ((yield = (res >= 0)))
   {
+  PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
   res = pcre2_get_ovector_count(md);
   expand_nmax = setup < 0 ? 0 : setup + 1;
   for (int matchnum = setup < 0 ? 0 : 1; matchnum < res; matchnum++)
     {
-    PCRE2_SIZE len;
-    pcre2_substring_get_bynumber(md, matchnum,
-      (PCRE2_UCHAR **)&expand_nstring[expand_nmax], &len);
-    if (!expand_nstring[expand_nmax])
-      { expand_nstring[expand_nmax] = US""; len = 0; }
-    expand_nlength[expand_nmax++] = (int)len;
+    int off = matchnum * 2;
+    int len = ovec[off + 1] - ovec[off];
+    expand_nstring[expand_nmax] = string_copyn(subject + ovec[off], len);
+    expand_nlength[expand_nmax++] = len;
     }
   expand_nmax--;
   }
diff --git a/src/src/malware.c b/src/src/malware.c
index 423a5b692..01dd455ba 100644
--- a/src/src/malware.c
+++ b/src/src/malware.c
@@ -310,16 +310,16 @@ m_pcre_exec(const pcre2_code * cre, uschar * text)
 {
 pcre2_match_data * md = pcre2_match_data_create(2, pcre_gen_ctx);
 int i = pcre2_match(cre, text, PCRE2_ZERO_TERMINATED, 0, 0, md, pcre_gen_mtc_ctx);
-PCRE2_UCHAR * substr = NULL;
-PCRE2_SIZE slen;
+uschar * substr = NULL;
 
 if (i >= 2)				/* Got it */
   {
-  pcre2_substring_get_bynumber(md, 1, &substr, &slen);	/* uses same ctx as md */
-  if (!substr) substr = US"";
+  PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
+  int len = ovec[3] - ovec[2];
+  substr = string_copyn(text + ovec[2], len);
   }
 /* pcre2_match_data_free(md);	gen ctx needs no free */
-return US substr;
+return substr;
 }
 
 static const pcre2_code *
diff --git a/src/src/regex.c b/src/src/regex.c
index b401ba0d7..210620f26 100644
--- a/src/src/regex.c
+++ b/src/src/regex.c
@@ -79,10 +79,10 @@ for (pcre_list * ri = re_list_head; ri; ri = ri->next)
 
     for (int nn = 1; nn < n; nn++)
       {
-      PCRE2_UCHAR * cstr;
-      PCRE2_SIZE cslen;
-      pcre2_substring_get_bynumber(md, nn, &cstr, &cslen);	/* uses same ctx as md */
-      regex_vars[nn-1] = cstr ? CUS cstr : CUS"";
+      PCRE2_SIZE * ovec = pcre2_get_ovector_pointer(md);
+      int off = nn * 2;
+      int len = ovec[off + 1] - ovec[off];
+      regex_vars[nn-1] = string_copyn(linebuffer + ovec[off], len);
       }
 
     return OK;

commit 9ba47886c71d40edc99b026a99edee269d9c9c6f
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Nov 12 12:38:22 2022 +0000

    Fix regex substring capture - commentary.  Bug 2933
    
    Broken-by (corrected): 22ed7a5295f1

diff --git a/src/src/exim.c b/src/src/exim.c
index 16c0184e0..625494ce4 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -104,7 +104,9 @@ pcre_gen_mtc_ctx = pcre2_match_context_create(pcre_gen_ctx);
 
 /* This function runs a regular expression match, and sets up the pointers to
 the matched substrings.  The matched strings are copied so the lifetime of
-the subject is not a problem.
+the subject is not a problem.  Matched strings will have the same taint status
+as the subject string (this is not a de-taint method, and must not be made so
+given the support for wildcards in REs).
 
 Arguments:
   re          the compiled expression
@@ -132,6 +134,11 @@ if ((yield = (res >= 0)))
   expand_nmax = setup < 0 ? 0 : setup + 1;
   for (int matchnum = setup < 0 ? 0 : 1; matchnum < res; matchnum++)
     {
+    /* Although PCRE2 has a pcre2_substring_get_bynumber() conveneience, it
+    seems to return a bad pointer when a capture group had no data, eg. (.*)
+    matching zero letters.  So use the underlying ovec and hope (!) that the
+    offsets are sane (including that case).  Should we go further and range-
+    check each one vs. the subject string length? */
     int off = matchnum * 2;
     int len = ovec[off + 1] - ovec[off];
     expand_nstring[expand_nmax] = string_copyn(subject + ovec[off], len);

commit dae16fc62c042f1c300db82ec1fc0d95cb8d66d3
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Nov 12 20:17:55 2022 +0000

    tidy segv stacktrace logging

diff --git a/src/src/exim.c b/src/src/exim.c
index 625494ce4..3cd1d5f6d 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -248,17 +248,17 @@ void * buf[STACKDUMP_MAX];
 char ** ss;
 int nptrs = backtrace(buf, STACKDUMP_MAX);
 
-log_write(0, LOG_MAIN|LOG_PANIC, "backtrace\n");
-log_write(0, LOG_MAIN|LOG_PANIC, "---\n");
+log_write(0, LOG_MAIN|LOG_PANIC, "backtrace");
+log_write(0, LOG_MAIN|LOG_PANIC, "---");
 if ((ss = backtrace_symbols(buf, nptrs)))
   {
   for (int i = 0; i < nptrs; i++)
-    log_write(0, LOG_MAIN|LOG_PANIC, "\t%s\n", ss[i]);
+    log_write(0, LOG_MAIN|LOG_PANIC, "\t%s", ss[i]);
   free(ss);
   }
 else
-  log_write(0, LOG_MAIN|LOG_PANIC, "backtrace_symbols: %s\n", strerror(errno));
-log_write(0, LOG_MAIN|LOG_PANIC, "---\n");
+  log_write(0, LOG_MAIN|LOG_PANIC, "backtrace_symbols: %s", strerror(errno));
+log_write(0, LOG_MAIN|LOG_PANIC, "---");
 #endif
 }
 #undef STACKDUMP_MAX

commit 7f65a63b60c6ea86db683ac00e221939f3bb1d47
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Oct 25 21:26:30 2022 +0100

    OpenSSL: when preloading creds do the server certs before the OCSP proofs so that the latter can ve verified before loading

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 68ad6f15b..fdf0d92b2 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -445,6 +445,8 @@ setup_certs(SSL_CTX *sctx, uschar *certs, uschar *crl, host_item *host,
 /* Callbacks */
 #ifndef DISABLE_OCSP
 static int tls_server_stapling_cb(SSL *s, void *arg);
+static void x509_stack_dump_cert_s_names(const STACK_OF(X509) * sk);
+static void x509_store_dump_cert_s_names(X509_STORE * store);
 #endif
 
 
@@ -1311,7 +1313,6 @@ OCSP_BASICRESP * basic_response;
 OCSP_SINGLERESP * single_response;
 ASN1_GENERALIZEDTIME * rev, * thisupd, * nextupd;
 STACK_OF(X509) * sk;
-unsigned long verify_flags;
 int status, reason, i;
 
 DEBUG(D_tls)
@@ -1376,20 +1377,20 @@ if (!(basic_response = OCSP_response_get1_basic(resp)))
   goto bad;
   }
 
-sk = state->verify_stack;
-verify_flags = OCSP_NOVERIFY; /* check sigs, but not purpose */
+sk = state->verify_stack;	/* set by setup_certs() / chain_from_pem_file() */
 
 /* May need to expose ability to adjust those flags?
 OCSP_NOSIGS OCSP_NOVERIFY OCSP_NOCHAIN OCSP_NOCHECKS OCSP_NOEXPLICIT
 OCSP_TRUSTOTHER OCSP_NOINTERN */
 
-/* This does a full verify on the OCSP proof before we load it for serving
-up; possibly overkill - just date-checks might be nice enough.
+/* This does a partial verify (only the signer link, not the whole chain-to-CA)
+on the OCSP proof before we load it for serving up; possibly overkill -
+just date-checks might be nice enough.
 
 OCSP_basic_verify takes a "store" arg, but does not
-use it for the chain verification, which is all we do
-when OCSP_NOVERIFY is set.  The content from the wire
-"basic_response" and a cert-stack "sk" are all that is used.
+use it for the chain verification, when OCSP_NOVERIFY is set.
+The content from the wire "basic_response" and a cert-stack "sk" are all
+that is used.
 
 We have a stack, loaded in setup_certs() if tls_verify_certificates
 was a file (not a directory, or "system").  It is unfortunate we
@@ -1406,7 +1407,7 @@ But what with?  We also use OCSP_basic_verify in the client stapling callback.
 And there we NEED it; we must verify that status... unless the
 library does it for us anyway?  */
 
-if ((i = OCSP_basic_verify(basic_response, sk, NULL, verify_flags)) < 0)
+if ((i = OCSP_basic_verify(basic_response, sk, NULL, OCSP_NOVERIFY)) < 0)
   {
   DEBUG(D_tls)
     {
@@ -1751,53 +1752,10 @@ else
   DEBUG(D_tls) debug_printf("TLS: not preloading ECDH curve for server\n");
 
 #if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT)
-/* If we can, preload the server-side cert, key and ocsp */
-
-if (  opt_set_and_noexpand(tls_certificate)
-# ifndef DISABLE_OCSP
-   && opt_unset_or_noexpand(tls_ocsp_file)
-#endif
-   && opt_unset_or_noexpand(tls_privatekey))
-  {
-  /* Set watches on the filenames.  The implementation does de-duplication
-  so we can just blindly do them all.  */
-
-  if (  tls_set_watch(tls_certificate, TRUE)
-# ifndef DISABLE_OCSP
-     && tls_set_watch(tls_ocsp_file, TRUE)
-#endif
-     && tls_set_watch(tls_privatekey, TRUE))
-    {
-    state_server.certificate = tls_certificate;
-    state_server.privatekey = tls_privatekey;
-#ifndef DISABLE_OCSP
-    state_server.u_ocsp.server.file = tls_ocsp_file;
-#endif
-
-    DEBUG(D_tls) debug_printf("TLS: preloading server certs\n");
-    if (tls_expand_session_files(ctx, &state_server, &dummy_errstr) == OK)
-      state_server.lib_state.conn_certs = TRUE;
-    }
-  }
-else if (  !tls_certificate && !tls_privatekey
-# ifndef DISABLE_OCSP
-	&& !tls_ocsp_file
-#endif
-   )
-  {		/* Generate & preload a selfsigned cert. No files to watch. */
-  if (tls_expand_session_files(ctx, &state_server, &dummy_errstr) == OK)
-    {
-    state_server.lib_state.conn_certs = TRUE;
-    lifetime = f.running_in_test_harness ? 2 : 60 * 60;		/* 1 hour */
-    }
-  }
-else
-  DEBUG(D_tls) debug_printf("TLS: not preloading server certs\n");
-
-
 /* If we can, preload the Authorities for checking client certs against.
 Actual choice to do verify is made (tls_{,try_}verify_hosts)
-at TLS conn startup */
+at TLS conn startup.
+Do this before the server ocsp so that its info can verify the ocsp. */
 
 if (  opt_set_and_noexpand(tls_verify_certificates)
    && opt_unset_or_noexpand(tls_crl))
@@ -1813,10 +1771,55 @@ if (  opt_set_and_noexpand(tls_verify_certificates)
     if (setup_certs(ctx, tls_verify_certificates, tls_crl, NULL, &dummy_errstr)
 	== OK)
       state_server.lib_state.cabundle = TRUE;
-    }
+
+    /* If we can, preload the server-side cert, key and ocsp */
+
+    if (  opt_set_and_noexpand(tls_certificate)
+# ifndef DISABLE_OCSP
+       && opt_unset_or_noexpand(tls_ocsp_file)
+# endif
+       && opt_unset_or_noexpand(tls_privatekey))
+      {
+      /* Set watches on the filenames.  The implementation does de-duplication
+      so we can just blindly do them all.  */
+
+      if (  tls_set_watch(tls_certificate, TRUE)
+# ifndef DISABLE_OCSP
+	 && tls_set_watch(tls_ocsp_file, TRUE)
+# endif
+	 && tls_set_watch(tls_privatekey, TRUE))
+	{
+	state_server.certificate = tls_certificate;
+	state_server.privatekey = tls_privatekey;
+#ifndef DISABLE_OCSP
+	state_server.u_ocsp.server.file = tls_ocsp_file;
+# endif
+
+	DEBUG(D_tls) debug_printf("TLS: preloading server certs\n");
+	if (tls_expand_session_files(ctx, &state_server, &dummy_errstr) == OK)
+	  state_server.lib_state.conn_certs = TRUE;
+	}
+      }
+    else if (  !tls_certificate && !tls_privatekey
+# ifndef DISABLE_OCSP
+	    && !tls_ocsp_file
+# endif
+       )
+      {		/* Generate & preload a selfsigned cert. No files to watch. */
+      if (tls_expand_session_files(ctx, &state_server, &dummy_errstr) == OK)
+	{
+	state_server.lib_state.conn_certs = TRUE;
+	lifetime = f.running_in_test_harness ? 2 : 60 * 60;		/* 1 hour */
+	}
+      }
+    else
+      DEBUG(D_tls) debug_printf("TLS: not preloading server certs\n");
+	}
   }
 else
   DEBUG(D_tls) debug_printf("TLS: not preloading CA bundle for server\n");
+
+
 #endif	/* EXIM_HAVE_INOTIFY */
 
 

commit 62b97c2ecf148ee86053d82e5509e4c3a5a20054
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Oct 29 22:33:43 2022 +0100

    OpenSSL: fix double-expansion of tls_verify_certificates

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index fdf0d92b2..2e09882d2 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -439,7 +439,7 @@ exim_openssl_state_st *client_static_state = NULL;	/*XXX should not use static;
 exim_openssl_state_st state_server = {.is_server = TRUE};
 
 static int
-setup_certs(SSL_CTX *sctx, uschar *certs, uschar *crl, host_item *host,
+setup_certs(SSL_CTX * sctx, uschar ** certs, uschar * crl, host_item * host,
     uschar ** errstr );
 
 /* Callbacks */
@@ -1766,10 +1766,10 @@ if (  opt_set_and_noexpand(tls_verify_certificates)
      && tls_set_watch(tls_verify_certificates, FALSE)
      && tls_set_watch(tls_crl, FALSE))
     {
+    uschar * v_certs = tls_verify_certificates;
     DEBUG(D_tls) debug_printf("TLS: preloading CA bundle for server\n");
 
-    if (setup_certs(ctx, tls_verify_certificates, tls_crl, NULL, &dummy_errstr)
-	== OK)
+    if (setup_certs(ctx, &v_certs, tls_crl, NULL, &dummy_errstr) == OK)
       state_server.lib_state.cabundle = TRUE;
 
     /* If we can, preload the server-side cert, key and ocsp */
@@ -1901,10 +1901,11 @@ if (  opt_set_and_noexpand(ob->tls_verify_certificates)
 	&& tls_set_watch(ob->tls_crl, FALSE)
      )
     {
+    uschar * v_certs = ob->tls_verify_certificates;
     DEBUG(D_tls)
       debug_printf("TLS: preloading CA bundle for transport '%s'\n", t->name);
 
-    if (setup_certs(ctx, ob->tls_verify_certificates,
+    if (setup_certs(ctx, &v_certs,
 	  ob->tls_crl, dummy_host, &dummy_errstr) == OK)
       ob->tls_preload.cabundle = TRUE;
     }
@@ -2242,14 +2243,12 @@ if (state->u_ocsp.server.file)
 #endif
 
   {
-  uschar * expcerts;
-  if (  !expand_check(tls_verify_certificates, US"tls_verify_certificates",
-		  &expcerts, &dummy_errstr)
-     || (rc = setup_certs(server_sni, expcerts, tls_crl, NULL,
+  uschar * v_certs = tls_verify_certificates;
+  if ((rc = setup_certs(server_sni, &v_certs, tls_crl, NULL,
 			&dummy_errstr)) != OK)
     goto bad;
 
-  if (expcerts && *expcerts)
+  if (v_certs && *v_certs)
     setup_cert_verify(server_sni, FALSE, verify_callback_server);
   }
 
@@ -3021,7 +3020,7 @@ repeated after a Server Name Indication.
 
 Arguments:
   sctx          SSL_CTX* to initialise
-  certs         certs file, expanded
+  certs         certs file, returned expanded
   crl           CRL file or NULL
   host          NULL in a server; the remote host in a client
   errstr	error string pointer
@@ -3030,15 +3029,16 @@ Returns:        OK/DEFER/FAIL
 */
 
 static int
-setup_certs(SSL_CTX *sctx, uschar *certs, uschar *crl, host_item *host,
+setup_certs(SSL_CTX * sctx, uschar ** certsp, uschar * crl, host_item * host,
     uschar ** errstr)
 {
-uschar *expcerts, *expcrl;
+uschar * expcerts, * expcrl;
 
-if (!expand_check(certs, US"tls_verify_certificates", &expcerts, errstr))
+if (!expand_check(*certsp, US"tls_verify_certificates", &expcerts, errstr))
   return DEFER;
 DEBUG(D_tls) debug_printf("tls_verify_certificates: %s\n", expcerts);
 
+*certsp = expcerts;
 if (expcerts && *expcerts)
   {
   /* Tell the library to use its compiled-in location for the system default
@@ -3334,20 +3334,20 @@ else
   goto skip_certs;
 
  {
-  uschar * expcerts;
-  if (!expand_check(tls_verify_certificates, US"tls_verify_certificates",
-		    &expcerts, errstr))
-    return DEFER;
-  DEBUG(D_tls) debug_printf("tls_verify_certificates: %s\n", expcerts);
+  uschar * v_certs = tls_verify_certificates;
 
   if (state_server.lib_state.cabundle)
-    { DEBUG(D_tls) debug_printf("TLS: CA bundle for server was preloaded\n"); }
+    {
+    DEBUG(D_tls) debug_printf("TLS: CA bundle for server was preloaded\n");
+    setup_cert_verify(ctx, server_verify_optional, verify_callback_server);
+    }
   else
-    if ((rc = setup_certs(ctx, expcerts, tls_crl, NULL, errstr)) != OK)
+    {
+    if ((rc = setup_certs(ctx, &v_certs, tls_crl, NULL, errstr)) != OK)
       return rc;
-
-  if (expcerts && *expcerts)
-    setup_cert_verify(ctx, server_verify_optional, verify_callback_server);
+    if (v_certs && *v_certs)
+      setup_cert_verify(ctx, server_verify_optional, verify_callback_server);
+    }
  }
 skip_certs: ;
 
@@ -3610,20 +3610,20 @@ else
   return OK;
 
  {
-  uschar * expcerts;
-  if (!expand_check(ob->tls_verify_certificates, US"tls_verify_certificates",
-		    &expcerts, errstr))
-    return DEFER;
-  DEBUG(D_tls) debug_printf("tls_verify_certificates: %s\n", expcerts);
+  uschar * v_certs = ob->tls_verify_certificates;
 
   if (state->lib_state.cabundle)
-    { DEBUG(D_tls) debug_printf("TLS: CA bundle was preloaded\n"); }
+    {
+    DEBUG(D_tls) debug_printf("TLS: CA bundle for tpt was preloaded\n");
+    setup_cert_verify(ctx, client_verify_optional, verify_callback_client);
+    }
   else
-    if ((rc = setup_certs(ctx, expcerts, ob->tls_crl, host, errstr)) != OK)
+    {
+    if ((rc = setup_certs(ctx, &v_certs, ob->tls_crl, host, errstr)) != OK)
       return rc;
-
-  if (expcerts && *expcerts)
-    setup_cert_verify(ctx, client_verify_optional, verify_callback_client);
+    if (v_certs && *v_certs)
+      setup_cert_verify(ctx, client_verify_optional, verify_callback_client);
+    }
  }
 
 if (verify_check_given_host(CUSS &ob->tls_verify_cert_hostnames, host) == OK)

commit 415c5379af11bf8777af1a082a336ad7c5369525
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Nov 22 22:32:59 2022 +0000

    OpenSSL: OCSP under DANE

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 2e09882d2..3873bbba3 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -97,6 +97,7 @@ change this guard and punt the issue for a while longer. */
 
 #if !defined(LIBRESSL_VERSION_NUMBER) && (OPENSSL_VERSION_NUMBER >= 0x030000000L)
 # define EXIM_HAVE_EXPORT_CHNL_BNGNG
+# define EXIM_HAVE_OPENSSL_X509_STORE_GET1_ALL_CERTS
 #endif
 
 #if !defined(LIBRESSL_VERSION_NUMBER) \
@@ -117,6 +118,7 @@ change this guard and punt the issue for a while longer. */
 #  define OPENSSL_HAVE_NUM_TICKETS
 #  define EXIM_HAVE_OPENSSL_CIPHER_STD_NAME
 #  define EXIM_HAVE_EXP_CHNL_BNGNG
+#  define EXIM_HAVE_OPENSSL_OCSP_RESP_GET0_SIGNER
 # else
 #  define OPENSSL_BAD_SRVR_OURCERT
 # endif
@@ -408,15 +410,16 @@ typedef struct exim_openssl_state {
   uschar *	privatekey;
   BOOL		is_server;
 #ifndef DISABLE_OCSP
-  STACK_OF(X509) *verify_stack;		/* chain for verifying the proof */
   union {
     struct {
       uschar        *file;
       const uschar  *file_expanded;
       ocsp_resplist *olist;
+      STACK_OF(X509) *verify_stack;	/* chain for verifying the proof */
     } server;
     struct {
       X509_STORE    *verify_store;	/* non-null if status requested */
+      uschar	    *verify_errstr;	/* only if _required */
       BOOL	    verify_required;
     } client;
   } u_ocsp;
@@ -440,7 +443,7 @@ exim_openssl_state_st state_server = {.is_server = TRUE};
 
 static int
 setup_certs(SSL_CTX * sctx, uschar ** certs, uschar * crl, host_item * host,
-    uschar ** errstr );
+    uschar ** errstr);
 
 /* Callbacks */
 #ifndef DISABLE_OCSP
@@ -1119,18 +1122,6 @@ if (preverify_ok == 0)
 else if (depth != 0)
   {
   DEBUG(D_tls) debug_printf("SSL verify ok: depth=%d SN=%s\n", depth, dn);
-#ifndef DISABLE_OCSP
-  if (tlsp == &tls_out && client_static_state->u_ocsp.client.verify_store)
-    {	/* client, wanting stapling  */
-    /* Add the server cert's signing chain as the one
-    for the verification of the OCSP stapled information. */
-
-    if (!X509_STORE_add_cert(client_static_state->u_ocsp.client.verify_store,
-                             cert))
-      ERR_clear_error();
-    sk_X509_push(client_static_state->verify_stack, cert);
-    }
-#endif
 #ifndef DISABLE_EVENT
     if (verify_event(tlsp, cert, depth, dn, calledp, optionalp, US"SSL"))
       return 0;				/* reject, with peercert set */
@@ -1258,21 +1249,7 @@ DEBUG(D_tls) debug_printf("verify_callback_client_dane: %s depth %d %s\n",
 #endif
 
 if (preverify_ok == 1)
-  {
   tls_out.dane_verified = TRUE;
-#ifndef DISABLE_OCSP
-  if (client_static_state->u_ocsp.client.verify_store)
-    {	/* client, wanting stapling  */
-    /* Add the server cert's signing chain as the one
-    for the verification of the OCSP stapled information. */
-
-    if (!X509_STORE_add_cert(client_static_state->u_ocsp.client.verify_store,
-                             cert))
-      ERR_clear_error();
-    sk_X509_push(client_static_state->verify_stack, cert);
-    }
-#endif
-  }
 else
   {
   int err = X509_STORE_CTX_get_error(x509ctx);
@@ -1288,6 +1265,14 @@ return preverify_ok;
 
 
 #ifndef DISABLE_OCSP
+static void
+time_print(BIO * bp, const char * str, ASN1_GENERALIZEDTIME * time)
+{
+BIO_printf(bp, "\t%s: ", str);
+ASN1_GENERALIZEDTIME_print(bp, time);
+BIO_puts(bp, "\n");
+}
+
 /*************************************************
 *       Load OCSP information into state         *
 *************************************************/
@@ -1377,7 +1362,7 @@ if (!(basic_response = OCSP_response_get1_basic(resp)))
   goto bad;
   }
 
-sk = state->verify_stack;	/* set by setup_certs() / chain_from_pem_file() */
+sk = state->u_ocsp.server.verify_stack;	/* set by setup_certs() / chain_from_pem_file() */
 
 /* May need to expose ability to adjust those flags?
 OCSP_NOSIGS OCSP_NOVERIFY OCSP_NOCHAIN OCSP_NOCHECKS OCSP_NOEXPLICIT
@@ -1398,11 +1383,13 @@ cannot used the connection context store, as that would neatly
 handle the "system" case too, but there seems to be no library
 function for getting a stack from a store.
 [ In OpenSSL 1.1 - ?  X509_STORE_CTX_get0_chain(ctx) ? ]
+[ 3.0.0 - sk = X509_STORE_get1_all_certs(store) ]
 We do not free the stack since it could be needed a second time for
 SNI handling.
 
 Separately we might try to replace using OCSP_basic_verify() - which seems to not
 be a public interface into the OpenSSL library (there's no manual entry) -
+(in 3.0.0 + is is public)
 But what with?  We also use OCSP_basic_verify in the client stapling callback.
 And there we NEED it; we must verify that status... unless the
 library does it for us anyway?  */
@@ -1412,7 +1399,7 @@ if ((i = OCSP_basic_verify(basic_response, sk, NULL, OCSP_NOVERIFY)) < 0)
   DEBUG(D_tls)
     {
     ERR_error_string_n(ERR_get_error(), ssl_errstring, sizeof(ssl_errstring));
-    debug_printf("OCSP response verify failure: %s\n", US ssl_errstring);
+    debug_printf("OCSP response has bad signature: %s\n", US ssl_errstring);
     }
   goto bad;
   }
@@ -1446,7 +1433,16 @@ if (status != V_OCSP_CERTSTATUS_GOOD)
 
 if (!OCSP_check_validity(thisupd, nextupd, EXIM_OCSP_SKEW_SECONDS, EXIM_OCSP_MAX_AGE))
   {
-  DEBUG(D_tls) debug_printf("OCSP status invalid times.\n");
+  DEBUG(D_tls)
+    {
+    BIO * bp = BIO_new(BIO_s_mem());
+    uschar * s = NULL;
+    int len;
+    time_print(bp, "This OCSP Update", thisupd);
+    if (nextupd) time_print(bp, "Next OCSP Update", nextupd);
+    if ((len = (int) BIO_get_mem_data(bp, CSS &s)) > 0) debug_printf("%.*s", len, s);
+    debug_printf("OCSP status invalid times.\n");
+    }
   goto bad;
   }
 
@@ -1921,7 +1917,7 @@ else
 #if defined(EXIM_HAVE_INOTIFY) || defined(EXIM_HAVE_KEVENT)
 /* Invalidate the creds cached, by dropping the current ones.
 Call when we notice one of the source files has changed. */
- 
+
 static void
 tls_server_creds_invalidate(void)
 {
@@ -1955,28 +1951,61 @@ tls_client_creds_invalidate(transport_instance * t)
 
 
 /* Extreme debug
+ * */
 #ifndef DISABLE_OCSP
-void
-x509_store_dump_cert_s_names(X509_STORE * store)
+static void
+debug_print_sn(const X509 * cert)
 {
-STACK_OF(X509_OBJECT) * roots= store->objs;
+X509_NAME * sn = X509_get_subject_name(cert);
 static uschar name[256];
+if (X509_NAME_oneline(sn, CS name, sizeof(name)))
+  {
+  name[sizeof(name)-1] = '\0';
+  debug_printf(" %s\n", name);
+  }
+}
 
-for (int i= 0; i < sk_X509_OBJECT_num(roots); i++)
+static void
+x509_stack_dump_cert_s_names(const STACK_OF(X509) * sk)
+{
+if (!sk)
+  debug_printf(" (null)\n");
+else
   {
-  X509_OBJECT * tmp_obj= sk_X509_OBJECT_value(roots, i);
-  if(tmp_obj->type == X509_LU_X509)
+  int idx = sk_X509_num(sk);
+  if (!idx)
+    debug_printf(" (empty)\n");
+  else
+    while (--idx >= 0) debug_print_sn(sk_X509_value(sk, idx));
+  }
+}
+
+static void
+x509_store_dump_cert_s_names(X509_STORE * store)
+{
+# ifdef EXIM_HAVE_OPENSSL_X509_STORE_GET1_ALL_CERTS
+STACK_OF(X509) * sk = X509_STORE_get1_all_certs(store);
+x509_stack_dump_cert_s_names(sk);
+sk_X509_pop_free(sk, X509_free);
+
+# else
+if (!store)
+  debug_printf(" (no store)\n");
+else
+  {
+  STACK_OF(X509_OBJECT) * objs = X509_STORE_get0_objects(store);
+  if (!objs)
+    debug_printf(" (null objectlist)\n");
+  else for (int i = 0; i < sk_X509_OBJECT_num(objs); i++)
     {
-    X509_NAME * sn = X509_get_subject_name(tmp_obj->data.x509);
-    if (X509_NAME_oneline(sn, CS name, sizeof(name)))
-      {
-      name[sizeof(name)-1] = '\0';
-      debug_printf(" %s\n", name);
-      }
+    X509 * cert = X509_OBJECT_get0_X509(sk_X509_OBJECT_value(objs, i));
+    if (cert) debug_print_sn(cert);
     }
   }
+# endif
 }
-#endif
+#endif	/*!DISABLE_OCSP*/
+/*
 */
 
 
@@ -2420,11 +2449,21 @@ return SSL_TLSEXT_ERR_OK;
 
 
 static void
-time_print(BIO * bp, const char * str, ASN1_GENERALIZEDTIME * time)
+add_chain_to_store(X509_STORE * store, STACK_OF(X509) * sk,
+  const char * debug_text)
 {
-BIO_printf(bp, "\t%s: ", str);
-ASN1_GENERALIZEDTIME_print(bp, time);
-BIO_puts(bp, "\n");
+int idx;
+
+DEBUG(D_tls)
+  {
+  debug_printf("chain for %s:\n", debug_text);
+  x509_stack_dump_cert_s_names(sk);
+  }
+if (sk)
+  if ((idx = sk_X509_num(sk)) > 0)
+    while (--idx >= 0)
+      X509_STORE_add_cert(store, sk_X509_value(sk, idx));
+
 }
 
 static int
@@ -2440,18 +2479,24 @@ int i;
 DEBUG(D_tls) debug_printf("Received TLS status callback (OCSP stapling):\n");
 len = SSL_get_tlsext_status_ocsp_resp(ssl, &p);
 if(!p)
- {				/* Expect this when we requested ocsp but got none */
+  {				/* Expect this when we requested ocsp but got none */
   if (SSL_session_reused(ssl) && tls_out.ocsp == OCSP_VFIED)
     {
     DEBUG(D_tls) debug_printf(" null, but resumed; ocsp vfy stored with session is good\n");
     return 1;
     }
+
   if (cbinfo->u_ocsp.client.verify_required && LOGGING(tls_cipher))
     log_write(0, LOG_MAIN, "Required TLS certificate status not received");
   else
     DEBUG(D_tls) debug_printf(" null\n");
-  return cbinfo->u_ocsp.client.verify_required ? 0 : 1;
- }
+
+  if (!cbinfo->u_ocsp.client.verify_required)
+    return 1;
+  cbinfo->u_ocsp.client.verify_errstr =
+			US"(SSL_connect) Required TLS certificate status not received";
+  return 0;
+  }
 
 if (!(rsp = d2i_OCSP_RESPONSE(NULL, &p, len)))
   {
@@ -2483,39 +2528,140 @@ if (!(bs = OCSP_response_get1_basic(rsp)))
 */
   {
     BIO * bp = NULL;
+    X509_STORE * verify_store = NULL;
+    BOOL have_verified_OCSP_signer = FALSE;
 #ifndef EXIM_HAVE_OCSP_RESP_COUNT
     STACK_OF(OCSP_SINGLERESP) * sresp = bs->tbsResponseData->responses;
 #endif
 
     DEBUG(D_tls) bp = BIO_new(BIO_s_mem());
 
-    /*OCSP_RESPONSE_print(bp, rsp, 0);   extreme debug: stapling content */
+    /* Use the CA & chain that verified the server cert to verify the stapled info */
+
+   {
+    /* If this routine is not available, we've avoided [in tls_client_start()]
+    asking for certificate-status under DANE, so this callback won't run for
+    that combination. It still will for non-DANE. */
+
+#ifdef EXIM_HAVE_OPENSSL_OCSP_RESP_GET0_SIGNER
+    X509 * signer;
+
+    if (  tls_out.dane_verified
+       && (have_verified_OCSP_signer =
+	OCSP_resp_get0_signer(bs, &signer, SSL_get0_verified_chain(ssl)) == 1))
+      {
+      DEBUG(D_tls)
+	debug_printf("signer for OCSP basicres is in the verified chain;"
+		      " shortcut its verification\n");
+      }
+    else
+#endif
+      {
+      STACK_OF(X509) * verified_chain;
+
+      verify_store = X509_STORE_new();
+
+      SSL_get0_chain_certs(ssl, &verified_chain);
+      add_chain_to_store(verify_store, verified_chain,
+			      "'current cert' per SSL_get0_chain_certs()");
+
+      verified_chain = SSL_get0_verified_chain(ssl);
+      add_chain_to_store(verify_store, verified_chain,
+			      "SSL_get0_verified_chain()");
+      }
+   }
+
+    DEBUG(D_tls)
+      {
+      debug_printf("Untrusted intermediate cert stack (from SSL_get_peer_cert_chain()):\n");
+      x509_stack_dump_cert_s_names(SSL_get_peer_cert_chain(ssl));
+
+      debug_printf("will use this CA store for verifying basicresp:\n");
+      x509_store_dump_cert_s_names(verify_store);
+
+      /* OCSP_RESPONSE_print(bp, rsp, 0);   extreme debug: stapling content */
+
+      debug_printf("certs contained in basicresp:\n");
+      x509_stack_dump_cert_s_names((STACK_OF(X509 *))OCSP_resp_get0_certs(bs));
+
+#ifdef EXIM_HAVE_OPENSSL_X509_STORE_GET1_ALL_CERTS	/* else, could bodge via X509_STORE_get0_objects()
+						- but is OCSP_resp_get0_signer) avail? from 1.1.1 */
+       {
+	X509 * signer;
+	if (OCSP_resp_get0_signer(bs, &signer, X509_STORE_get1_all_certs(verify_store)) == 1)
+	  {
+	  debug_printf("found signer for basicres:\n");
+	  debug_print_sn(signer);
+	  }
+	else
+	  {
+	  debug_printf("failed to find signer for basicres:\n");
+	  ERR_print_errors(bp);
+	  }
+       }
+#endif
+
+      }
+
+    ERR_clear_error();
+
+    /* Under DANE the trust-anchor (at least in TA mode) is indicated by the TLSA
+    record in DNS, and probably is not the root of the chain of certificates. So
+    accept a partial chain for that case (and hope that anchor is visible for
+    verifying the OCSP stapling).
+    XXX for EE mode it won't even be that.  Does that make OCSP useless for EE?
 
-    /* Use the chain that verified the server cert to verify the stapled info */
-    /* DEBUG(D_tls) x509_store_dump_cert_s_names(cbinfo->u_ocsp.client.verify_store); */
+    Worse, for LetsEncrypt-mode (ocsp signer is leaf-signer) under DANE, the
+    data used within OpenSSL for the signer has nil pointers for signing
+    algorithms - and a crash results.  Avoid this by shortcutting verification,
+    having determined that the OCSP signer is in the (DANE-)validated set.
+    */
+
+#ifndef OCSP_PARTIAL_CHAIN	/* defined for 3.0.0 onwards */
+# define OCSP_PARTIAL_CHAIN 0
+#endif
 
-    if ((i = OCSP_basic_verify(bs, cbinfo->verify_stack,
-	      cbinfo->u_ocsp.client.verify_store, OCSP_NOEXPLICIT)) <= 0)
+    if ((i = OCSP_basic_verify(bs, SSL_get_peer_cert_chain(ssl),
+		verify_store,
+		tls_out.dane_verified
+		? have_verified_OCSP_signer
+		  ? OCSP_NOVERIFY | OCSP_NOEXPLICIT
+		  : OCSP_PARTIAL_CHAIN | OCSP_NOEXPLICIT
+		: OCSP_NOEXPLICIT)) <= 0)
+      {
+      DEBUG(D_tls) debug_printf("OCSP_basic_verify() fail: returned %d\n", i);
       if (ERR_peek_error())
 	{
 	tls_out.ocsp = OCSP_FAILED;
 	if (LOGGING(tls_cipher))
 	  {
-	  const uschar * errstr = CUS ERR_reason_error_string(ERR_peek_error());
 	  static uschar peerdn[256];
+	  const uschar * errstr;;
+
+#if OPENSSL_VERSION_NUMBER >= 0x30000000L
+	  ERR_peek_error_all(NULL, NULL, NULL, CCSS &errstr, NULL);
+	  if (!errstr)
+#endif
+	    errstr = CUS ERR_reason_error_string(ERR_peek_error());
+
 	  X509_NAME_oneline(X509_get_subject_name(SSL_get_peer_certificate(ssl)),
 						  CS peerdn, sizeof(peerdn));
 	  log_write(0, LOG_MAIN,
 		"[%s] %s Received TLS cert (DN: '%.*s') status response, "
 		"itself unverifiable: %s",
-		sender_host_address, sender_host_name,
-		(int)sizeof(peerdn), peerdn,
-		errstr);
+		deliver_host_address, deliver_host,
+		(int)sizeof(peerdn), peerdn, errstr);
 	  }
 	DEBUG(D_tls)
 	  {
 	  BIO_printf(bp, "OCSP response verify failure\n");
 	  ERR_print_errors(bp);
+  {
+  uschar * s = NULL;
+  int len = (int) BIO_get_mem_data(bp, CSS &s);
+  if (len > 0) debug_printf("%.*s", len, s);
+  BIO_reset(bp);
+  }
 	  OCSP_RESPONSE_print(bp, rsp, 0);
 	  }
 	goto failed;
@@ -2523,6 +2669,7 @@ if (!(bs = OCSP_response_get1_basic(rsp)))
       else
 	DEBUG(D_tls) debug_printf("no explicit trust for OCSP signing"
 	  " in the root CA certificate; ignoring\n");
+      }
 
     DEBUG(D_tls) debug_printf("OCSP response well-formed and signed OK\n");
 
@@ -2565,6 +2712,8 @@ if (!(bs = OCSP_response_get1_basic(rsp)))
 	{
 	tls_out.ocsp = OCSP_FAILED;
 	DEBUG(D_tls) ERR_print_errors(bp);
+	cbinfo->u_ocsp.client.verify_errstr =
+		    US"(SSL_connect) Server certificate status is out-of-date";
 	log_write(0, LOG_MAIN, "OCSP dates invalid");
 	goto failed;
 	}
@@ -2576,12 +2725,16 @@ if (!(bs = OCSP_response_get1_basic(rsp)))
 	case V_OCSP_CERTSTATUS_GOOD:
 	  continue;	/* the idx loop */
 	case V_OCSP_CERTSTATUS_REVOKED:
+	  cbinfo->u_ocsp.client.verify_errstr =
+			US"(SSL_connect) Server certificate revoked";
 	  log_write(0, LOG_MAIN, "Server certificate revoked%s%s",
 	      reason != -1 ? "; reason: " : "",
 	      reason != -1 ? OCSP_crl_reason_str(reason) : "");
 	  DEBUG(D_tls) time_print(bp, "Revocation Time", rev);
 	  break;
 	default:
+	  cbinfo->u_ocsp.client.verify_errstr =
+			US"(SSL_connect) Server certificate has unknown status";
 	  log_write(0, LOG_MAIN,
 	      "Server certificate status unknown, in OCSP stapling");
 	  break;
@@ -2635,8 +2788,7 @@ tls_init(host_item * host, smtp_transport_options_block * ob,
   uschar *ocsp_file,
 #endif
   address_item *addr, exim_openssl_state_st ** caller_state,
-  tls_support * tlsp,
-  uschar ** errstr)
+  tls_support * tlsp, uschar ** errstr)
 {
 SSL_CTX * ctx;
 exim_openssl_state_st * state;
@@ -2798,7 +2950,7 @@ else
 
 #ifdef EXIM_HAVE_OPENSSL_TLSEXT
 # ifndef DISABLE_OCSP
-  if (!(state->verify_stack = sk_X509_new_null()))
+  if (!host && !(state->u_ocsp.server.verify_stack = sk_X509_new_null()))
     {
     DEBUG(D_tls) debug_printf("failed to create stack for stapling verify\n");
     return FAIL;
@@ -2847,11 +2999,12 @@ else			/* client */
       DEBUG(D_tls) debug_printf("failed to create store for stapling verify\n");
       return FAIL;
       }
+
     SSL_CTX_set_tlsext_status_cb(ctx, tls_client_stapling_cb);
     SSL_CTX_set_tlsext_status_arg(ctx, state);
     }
 # endif
-#endif
+#endif	/*EXIM_HAVE_OPENSSL_TLSEXT*/
 
 state->verify_cert_hostnames = NULL;
 
@@ -2990,7 +3143,7 @@ if (tlsp->peercert)
 *************************************************/
 
 #ifndef DISABLE_OCSP
-/* Load certs from file, return TRUE on success */
+/* In the server, load certs from file, return TRUE on success */
 
 static BOOL
 chain_from_pem_file(const uschar * file, STACK_OF(X509) ** vp)
@@ -3020,7 +3173,7 @@ repeated after a Server Name Indication.
 
 Arguments:
   sctx          SSL_CTX* to initialise
-  certs         certs file, returned expanded
+  certsp        certs file, returned expanded
   crl           CRL file or NULL
   host          NULL in a server; the remote host in a client
   errstr	error string pointer
@@ -3066,19 +3219,20 @@ if (expcerts && *expcerts)
 	{
 	STACK_OF(X509) * verify_stack =
 #ifndef DISABLE_OCSP
-	  !host ? state_server.verify_stack :
+	  !host ? state_server.u_ocsp.server.verify_stack :
 #endif
 	  NULL;
 	STACK_OF(X509) ** vp = &verify_stack;
 
 	file = expcerts; dir = NULL;
 #ifndef DISABLE_OCSP
-	/* In the server if we will be offering an OCSP proof, load chain from
+	/* In the server if we will be offering an OCSP proof; load chain from
 	file for verifying the OCSP proof at load time. */
 
 /*XXX Glitch!   The file here is tls_verify_certs: the chain for verifying the client cert.
 This is inconsistent with the need to verify the OCSP proof of the server cert.
 */
+/* *debug_printf("file for checking server ocsp stapling is: %s\n", file); */
 	if (  !host
 	   && statbuf.st_size > 0
 	   && state_server.u_ocsp.server.file
@@ -3304,7 +3458,7 @@ TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256:TLS_AES_128_GCM_SHA256
 
 if (state_server.lib_state.pri_string)
   { DEBUG(D_tls) debug_printf("TLS: cipher list was preloaded\n"); }
-else 
+else
   {
   if (!expand_check(tls_require_ciphers, US"tls_require_ciphers", &expciphers, errstr))
     return FAIL;
@@ -3707,7 +3861,6 @@ tls_retrieve_session(tls_support * tlsp, SSL * ssl)
 {
 if (tlsp->host_resumable)
   {
-  const uschar * key = tlsp->resume_index;
   dbdata_tls_session * dt;
   int len;
   open_db dbblock, * dbm_file;
@@ -3956,7 +4109,6 @@ tlsp->tlsa_usage = 0;
 #ifndef DISABLE_OCSP
   {
 # ifdef SUPPORT_DANE
-  /*XXX this should be moved to caller, to be common across gnutls/openssl */
   if (  conn_args->dane
      && ob->hosts_request_ocsp[0] == '*'
      && ob->hosts_request_ocsp[1] == '\0'
@@ -3979,6 +4131,15 @@ tlsp->tlsa_usage = 0;
 # endif
       request_ocsp =
 	verify_check_given_host(CUSS &ob->hosts_request_ocsp, host) == OK;
+
+# if defined(SUPPORT_DANE) && !defined(EXIM_HAVE_OPENSSL_OCSP_RESP_GET0_SIGNER)
+  if (conn_args->dane && (require_ocsp || request_ocsp))
+    {
+    DEBUG(D_tls) debug_printf("OpenSSL version to early to combine OCSP"
+			      " and DANE; disabling OCSP\n");
+    require_ocsp = request_ocsp = FALSE;
+    }
+# endif
   }
 #endif
 
@@ -4050,6 +4211,7 @@ if (conn_args->dane)
     tls_error(US"context init", host, NULL, errstr);
     return FALSE;
     }
+  DEBUG(D_tls) debug_printf("since dane-mode conn, not loading the usual CA bundle\n");
   }
 else
 
@@ -4186,7 +4348,12 @@ if (conn_args->dane)
 
 if (rc <= 0)
   {
-  tls_error(US"SSL_connect", host, sigalrm_seen ? US"timed out" : NULL, errstr);
+#ifndef DISABLE_OCSP
+  if (client_static_state->u_ocsp.client.verify_errstr)
+    { if (errstr) *errstr = client_static_state->u_ocsp.client.verify_errstr; }
+  else
+#endif
+    tls_error(US"SSL_connect", host, sigalrm_seen ? US"timed out" : NULL, errstr);
   return FALSE;
   }
 
@@ -4373,7 +4540,6 @@ tls_get_cache(unsigned lim)
 {
 #ifndef DISABLE_DKIM
 int n = ssl_xfer_buffer_hwm - ssl_xfer_buffer_lwm;
-debug_printf("tls_get_cache\n");
 if (n > lim)
   n = lim;
 if (n > 0)
@@ -4633,8 +4799,8 @@ if (do_shutdown > TLS_NO_SHUTDOWN)
 if (!o_ctx)		/* server side */
   {
 #ifndef DISABLE_OCSP
-  sk_X509_pop_free(state_server.verify_stack, X509_free);
-  state_server.verify_stack = NULL;
+  sk_X509_pop_free(state_server.u_ocsp.server.verify_stack, X509_free);
+  state_server.u_ocsp.server.verify_stack = NULL;
 #endif
 
   receive_getc =	smtp_getc;

commit 6bf0021993572586f031ac7d973ca33358c2dac8
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Nov 25 12:33:28 2022 +0000

    OpenSSL: fix build on older library version

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 3873bbba3..e673cb9f9 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -47,6 +47,7 @@ functions from the OpenSSL library. */
 #endif
 #if OPENSSL_VERSION_NUMBER >= 0x10100000L
 # define EXIM_HAVE_OCSP_RESP_COUNT
+# define EXIM_HAVE_SSL_GET0_VERIFIED_CHAIN
 # define OPENSSL_AUTO_SHA256
 # define OPENSSL_MIN_PROTO_VERSION
 #else
@@ -78,6 +79,7 @@ change this guard and punt the issue for a while longer. */
 #  define EXIM_HAVE_SESSION_TICKET
 #  define EXIM_HAVE_OPESSL_TRACE
 #  define EXIM_HAVE_OPESSL_GET0_SERIAL
+#  define EXIM_HAVE_OPESSL_OCSP_RESP_GET0_CERTS
 #  ifndef DISABLE_OCSP
 #   define EXIM_HAVE_OCSP
 #  endif
@@ -1956,7 +1958,7 @@ tls_client_creds_invalidate(transport_instance * t)
 static void
 debug_print_sn(const X509 * cert)
 {
-X509_NAME * sn = X509_get_subject_name(cert);
+X509_NAME * sn = X509_get_subject_name((X509 *)cert);
 static uschar name[256];
 if (X509_NAME_oneline(sn, CS name, sizeof(name)))
   {
@@ -1984,23 +1986,13 @@ static void
 x509_store_dump_cert_s_names(X509_STORE * store)
 {
 # ifdef EXIM_HAVE_OPENSSL_X509_STORE_GET1_ALL_CERTS
-STACK_OF(X509) * sk = X509_STORE_get1_all_certs(store);
-x509_stack_dump_cert_s_names(sk);
-sk_X509_pop_free(sk, X509_free);
-
-# else
 if (!store)
   debug_printf(" (no store)\n");
 else
   {
-  STACK_OF(X509_OBJECT) * objs = X509_STORE_get0_objects(store);
-  if (!objs)
-    debug_printf(" (null objectlist)\n");
-  else for (int i = 0; i < sk_X509_OBJECT_num(objs); i++)
-    {
-    X509 * cert = X509_OBJECT_get0_X509(sk_X509_OBJECT_value(objs, i));
-    if (cert) debug_print_sn(cert);
-    }
+  STACK_OF(X509) * sk = X509_STORE_get1_all_certs(store);
+  x509_stack_dump_cert_s_names(sk);
+  sk_X509_pop_free(sk, X509_free);
   }
 # endif
 }
@@ -2564,10 +2556,11 @@ if (!(bs = OCSP_response_get1_basic(rsp)))
       SSL_get0_chain_certs(ssl, &verified_chain);
       add_chain_to_store(verify_store, verified_chain,
 			      "'current cert' per SSL_get0_chain_certs()");
-
+#ifdef EXIM_HAVE_SSL_GET0_VERIFIED_CHAIN
       verified_chain = SSL_get0_verified_chain(ssl);
       add_chain_to_store(verify_store, verified_chain,
 			      "SSL_get0_verified_chain()");
+#endif
       }
    }
 
@@ -2582,10 +2575,16 @@ if (!(bs = OCSP_response_get1_basic(rsp)))
       /* OCSP_RESPONSE_print(bp, rsp, 0);   extreme debug: stapling content */
 
       debug_printf("certs contained in basicresp:\n");
-      x509_stack_dump_cert_s_names((STACK_OF(X509 *))OCSP_resp_get0_certs(bs));
+      x509_stack_dump_cert_s_names(
+#ifdef EXIM_HAVE_OPESSL_OCSP_RESP_GET0_CERTS
+	OCSP_resp_get0_certs(bs)
+#else
+	bs->certs
+#endif
+	);
 
-#ifdef EXIM_HAVE_OPENSSL_X509_STORE_GET1_ALL_CERTS	/* else, could bodge via X509_STORE_get0_objects()
-						- but is OCSP_resp_get0_signer) avail? from 1.1.1 */
+#ifdef EXIM_HAVE_OPENSSL_X509_STORE_GET1_ALL_CERTS
+/* could do via X509_STORE_get0_objects(); not worth it just for debug info */
        {
 	X509 * signer;
 	if (OCSP_resp_get0_signer(bs, &signer, X509_STORE_get1_all_certs(verify_store)) == 1)
@@ -2623,11 +2622,14 @@ if (!(bs = OCSP_response_get1_basic(rsp)))
 
     if ((i = OCSP_basic_verify(bs, SSL_get_peer_cert_chain(ssl),
 		verify_store,
+#ifdef SUPPORT_DANE
 		tls_out.dane_verified
 		? have_verified_OCSP_signer
 		  ? OCSP_NOVERIFY | OCSP_NOEXPLICIT
 		  : OCSP_PARTIAL_CHAIN | OCSP_NOEXPLICIT
-		: OCSP_NOEXPLICIT)) <= 0)
+		:
+#endif
+		OCSP_NOEXPLICIT)) <= 0)
       {
       DEBUG(D_tls) debug_printf("OCSP_basic_verify() fail: returned %d\n", i);
       if (ERR_peek_error())

commit 02a33248dc5981a63931530a57c76ff27464bde5
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 27 11:29:40 2022 +0000

    Fix LibreSSL build

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index e673cb9f9..8ed413e91 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -47,7 +47,6 @@ functions from the OpenSSL library. */
 #endif
 #if OPENSSL_VERSION_NUMBER >= 0x10100000L
 # define EXIM_HAVE_OCSP_RESP_COUNT
-# define EXIM_HAVE_SSL_GET0_VERIFIED_CHAIN
 # define OPENSSL_AUTO_SHA256
 # define OPENSSL_MIN_PROTO_VERSION
 #else
@@ -80,6 +79,7 @@ change this guard and punt the issue for a while longer. */
 #  define EXIM_HAVE_OPESSL_TRACE
 #  define EXIM_HAVE_OPESSL_GET0_SERIAL
 #  define EXIM_HAVE_OPESSL_OCSP_RESP_GET0_CERTS
+#  define EXIM_HAVE_SSL_GET0_VERIFIED_CHAIN
 #  ifndef DISABLE_OCSP
 #   define EXIM_HAVE_OCSP
 #  endif
@@ -2232,7 +2232,7 @@ if (lib_ctx_new(&server_sni, NULL, &dummy_errstr) != OK)
 /* Not sure how many of these are actually needed, since SSL object
 already exists.  Might even need this selfsame callback, for reneg? */
 
-  {
+ {
   SSL_CTX * ctx = state_server.lib_state.lib_ctx;
   SSL_CTX_set_info_callback(server_sni, SSL_CTX_get_info_callback(ctx));
   SSL_CTX_set_mode(server_sni, SSL_CTX_get_mode(ctx));
@@ -2244,7 +2244,7 @@ already exists.  Might even need this selfsame callback, for reneg? */
   SSL_CTX_set_timeout(server_sni, SSL_CTX_get_timeout(ctx));
   SSL_CTX_set_tlsext_servername_callback(server_sni, tls_servername_cb);
   SSL_CTX_set_tlsext_servername_arg(server_sni, state);
-  }
+ }
 
 if (  !init_dh(server_sni, state->dhparam, &dummy_errstr)
    || !init_ecdh(server_sni, &dummy_errstr)

commit a85c067ba6c6940512cf57ec213277a370d87e70
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 27 14:38:37 2022 +0000

    SPDX: license tags (mostly by guesswork)

diff --git a/src/exim_monitor/em_StripChart.c b/src/exim_monitor/em_StripChart.c
index 3b94c2231..3eb98e4fb 100644
--- a/src/exim_monitor/em_StripChart.c
+++ b/src/exim_monitor/em_StripChart.c
@@ -1,3 +1,4 @@
+/* SPDX-License-Identifier: GPL-2.0-only */
 /***********************************************************
 Copyright 1987, 1988 by Digital Equipment Corporation, Maynard, Massachusetts,
 and the Massachusetts Institute of Technology, Cambridge, Massachusetts.
diff --git a/src/exim_monitor/em_TextPop.c b/src/exim_monitor/em_TextPop.c
index ff5d1a8f7..0e87cb580 100644
--- a/src/exim_monitor/em_TextPop.c
+++ b/src/exim_monitor/em_TextPop.c
@@ -2,6 +2,7 @@
 Copyright (c) The Exim Maintainers 2022
 Copyright 1989 by the Massachusetts Institute of Technology,
 Cambridge, Massachusetts.
+SPDX-License-Identifier: GPL-2.0-only
 
                         All Rights Reserved
 
diff --git a/src/exim_monitor/em_globals.c b/src/exim_monitor/em_globals.c
index 3d452c6ec..2943065b4 100644
--- a/src/exim_monitor/em_globals.c
+++ b/src/exim_monitor/em_globals.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "em_hdr.h"
diff --git a/src/exim_monitor/em_hdr.h b/src/exim_monitor/em_hdr.h
index ab37806b7..76970c997 100644
--- a/src/exim_monitor/em_hdr.h
+++ b/src/exim_monitor/em_hdr.h
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* This is the general header file for all the modules that comprise
diff --git a/src/exim_monitor/em_init.c b/src/exim_monitor/em_init.c
index e0bc3b066..e1f53fbba 100644
--- a/src/exim_monitor/em_init.c
+++ b/src/exim_monitor/em_init.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This module contains code to initialize things from the
 environment and the arguments. */
diff --git a/src/exim_monitor/em_log.c b/src/exim_monitor/em_log.c
index 8d85c13f1..d625056c5 100644
--- a/src/exim_monitor/em_log.c
+++ b/src/exim_monitor/em_log.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainters 2021 - 2022 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This module contains code for scanning the main log,
 extracting information from it, and displaying a "tail". */
diff --git a/src/exim_monitor/em_main.c b/src/exim_monitor/em_main.c
index 86caf71eb..919cde632 100644
--- a/src/exim_monitor/em_main.c
+++ b/src/exim_monitor/em_main.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "em_hdr.h"
diff --git a/src/exim_monitor/em_menu.c b/src/exim_monitor/em_menu.c
index 881f3748c..afcd31540 100644
--- a/src/exim_monitor/em_menu.c
+++ b/src/exim_monitor/em_menu.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "em_hdr.h"
diff --git a/src/exim_monitor/em_queue.c b/src/exim_monitor/em_queue.c
index 9badd2451..d9cfad38a 100644
--- a/src/exim_monitor/em_queue.c
+++ b/src/exim_monitor/em_queue.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "em_hdr.h"
diff --git a/src/exim_monitor/em_strip.c b/src/exim_monitor/em_strip.c
index 03864d290..cfafe9257 100644
--- a/src/exim_monitor/em_strip.c
+++ b/src/exim_monitor/em_strip.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "em_hdr.h"
diff --git a/src/exim_monitor/em_text.c b/src/exim_monitor/em_text.c
index 3a3682959..1077353e0 100644
--- a/src/exim_monitor/em_text.c
+++ b/src/exim_monitor/em_text.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "em_hdr.h"
diff --git a/src/exim_monitor/em_version.c b/src/exim_monitor/em_version.c
index b627a6e2b..c5d4d62d8 100644
--- a/src/exim_monitor/em_version.c
+++ b/src/exim_monitor/em_version.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #define EM_VERSION_C
 
diff --git a/src/exim_monitor/em_xs.c b/src/exim_monitor/em_xs.c
index ee91f7c15..dd19c7f43 100644
--- a/src/exim_monitor/em_xs.c
+++ b/src/exim_monitor/em_xs.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge, 1995 - 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This file contains a number of subroutines that are in effect
 just alternative packaging for calls to various X functions that
diff --git a/src/scripts/Configure-config.h b/src/scripts/Configure-config.h
index 75d366fca..c750f64f1 100755
--- a/src/scripts/Configure-config.h
+++ b/src/scripts/Configure-config.h
@@ -1,4 +1,5 @@
 #! /bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
 
 # Build the config.h file, using the buildconfig program, first ensuring that
 # it exists.
diff --git a/src/scripts/Configure-os.c b/src/scripts/Configure-os.c
index f00a5627a..ab40ad183 100755
--- a/src/scripts/Configure-os.c
+++ b/src/scripts/Configure-os.c
@@ -1,4 +1,5 @@
 #! /bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
 
 # Shell script to build os.c. There doesn't have to be an OS-specific os.c
 # file, but if there is, it gets copied at the start of os.c. The basic src
diff --git a/src/scripts/Configure-os.h b/src/scripts/Configure-os.h
index ae1ecf943..f95a61a5d 100755
--- a/src/scripts/Configure-os.h
+++ b/src/scripts/Configure-os.h
@@ -1,4 +1,5 @@
 #! /bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
 
 # Shell script to create a link to the appropriate OS-specific header file.
 
diff --git a/src/src/acl.c b/src/src/acl.c
index 1e7d28a90..92af9991f 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Code for handling Access Control Lists (ACLs) */
 
diff --git a/src/src/arc.c b/src/src/arc.c
index 86688f6e3..e3a2b3dad 100644
--- a/src/src/arc.c
+++ b/src/src/arc.c
@@ -5,6 +5,7 @@
    Copyright (c) Jeremy Harris 2018 - 2020
    Copyright (c) The Exim Maintainers 2021 - 2022
    License: GPL
+   SPDX-License-Identifier: GPL-2.0-only
 */
 
 #include "exim.h"
diff --git a/src/src/auths/auth-spa.c b/src/src/auths/auth-spa.c
index 8d886b6b6..32276c3f3 100644
--- a/src/src/auths/auth-spa.c
+++ b/src/src/auths/auth-spa.c
@@ -10,6 +10,7 @@
  * Samba project (by Andrew Tridgell, Jeremy Allison, and others).
  *
  * Copyright (c) The Exim Maintainers 2021
+ * SPDX-License-Identifier: GPL-2.0-only
 
  * Tom Kistner provided additional code, adding spa_build_auth_challenge() to
  * support server authentication mode.
diff --git a/src/src/auths/auth-spa.h b/src/src/auths/auth-spa.h
index cfe1b086d..df250d7a6 100644
--- a/src/src/auths/auth-spa.h
+++ b/src/src/auths/auth-spa.h
@@ -9,6 +9,7 @@
  * All the code used here was torn by Marc Prud'hommeaux out of the
  * Samba project (by Andrew Tridgell, Jeremy Allison, and others).
  */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* December 2004: The spa_base64_to_bits() function has no length checking in
 it. I have added a check. PH */
diff --git a/src/src/auths/call_pam.c b/src/src/auths/call_pam.c
index 80f80f1a8..483b083be 100644
--- a/src/src/auths/call_pam.c
+++ b/src/src/auths/call_pam.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 
diff --git a/src/src/auths/call_pwcheck.c b/src/src/auths/call_pwcheck.c
index 0adde4471..436b1a8de 100644
--- a/src/src/auths/call_pwcheck.c
+++ b/src/src/auths/call_pwcheck.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This module contains interface functions to the two Cyrus authentication
 daemons. The original one was "pwcheck", which gives its name to the source
diff --git a/src/src/auths/call_radius.c b/src/src/auths/call_radius.c
index e7f9f521d..f4d7a9569 100644
--- a/src/src/auths/call_radius.c
+++ b/src/src/auths/call_radius.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This file was originally supplied by Ian Kirk. The libradius support came
 from Alex Kiernan. */
diff --git a/src/src/auths/check_serv_cond.c b/src/src/auths/check_serv_cond.c
index 457a7150c..033d2026b 100644
--- a/src/src/auths/check_serv_cond.c
+++ b/src/src/auths/check_serv_cond.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 
diff --git a/src/src/auths/cram_md5.c b/src/src/auths/cram_md5.c
index 2c0616ca2..60128b83e 100644
--- a/src/src/auths/cram_md5.c
+++ b/src/src/auths/cram_md5.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* The stand-alone version just tests the algorithm. We have to drag
diff --git a/src/src/auths/cram_md5.h b/src/src/auths/cram_md5.h
index 95644db68..25470a61b 100644
--- a/src/src/auths/cram_md5.h
+++ b/src/src/auths/cram_md5.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/auths/cyrus_sasl.c b/src/src/auths/cyrus_sasl.c
index c8e2da5de..4fe257ba7 100644
--- a/src/src/auths/cyrus_sasl.c
+++ b/src/src/auths/cyrus_sasl.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This code was originally contributed by Matthew Byng-Maddick */
 
diff --git a/src/src/auths/cyrus_sasl.h b/src/src/auths/cyrus_sasl.h
index 6cf883454..144ac5c80 100644
--- a/src/src/auths/cyrus_sasl.h
+++ b/src/src/auths/cyrus_sasl.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Copyright (c) A L Digital Ltd 2004 */
 
diff --git a/src/src/auths/dovecot.c b/src/src/auths/dovecot.c
index 5d7713389..ed56ab8cd 100644
--- a/src/src/auths/dovecot.c
+++ b/src/src/auths/dovecot.c
@@ -1,6 +1,7 @@
 /*
  * Copyright (c) The Exim Maintainers 2006 - 2022
  * Copyright (c) 2004 Andrey Panin <pazke@donpac.ru>
+ * SPDX-License-Identifier: GPL-2.0-or-later
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published
diff --git a/src/src/auths/dovecot.h b/src/src/auths/dovecot.h
index bfe1f0775..b5eaf4f16 100644
--- a/src/src/auths/dovecot.h
+++ b/src/src/auths/dovecot.h
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainters 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/auths/external.c b/src/src/auths/external.c
index 7e7fca841..736c33982 100644
--- a/src/src/auths/external.c
+++ b/src/src/auths/external.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 2019-2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This file provides an Exim authenticator driver for
 a server to verify a client SSL certificate, using the EXTERNAL
diff --git a/src/src/auths/external.h b/src/src/auths/external.h
index 7d43650bb..9abb46a6b 100644
--- a/src/src/auths/external.h
+++ b/src/src/auths/external.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 2019 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/auths/get_data.c b/src/src/auths/get_data.c
index e0d79db3c..0c85d2651 100644
--- a/src/src/auths/get_data.c
+++ b/src/src/auths/get_data.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 
diff --git a/src/src/auths/get_no64_data.c b/src/src/auths/get_no64_data.c
index a019756e5..76f421473 100644
--- a/src/src/auths/get_no64_data.c
+++ b/src/src/auths/get_no64_data.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 
diff --git a/src/src/auths/gsasl_exim.c b/src/src/auths/gsasl_exim.c
index e49e83b81..aac9c84e6 100644
--- a/src/src/auths/gsasl_exim.c
+++ b/src/src/auths/gsasl_exim.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2019 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Copyright (c) Twitter Inc 2012
    Author: Phil Pennock <pdp@exim.org> */
diff --git a/src/src/auths/gsasl_exim.h b/src/src/auths/gsasl_exim.h
index 19c903611..691d7d706 100644
--- a/src/src/auths/gsasl_exim.h
+++ b/src/src/auths/gsasl_exim.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2019 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Copyright (c) Twitter Inc 2012 */
 
diff --git a/src/src/auths/heimdal_gssapi.c b/src/src/auths/heimdal_gssapi.c
index 381763299..12267e3fc 100644
--- a/src/src/auths/heimdal_gssapi.c
+++ b/src/src/auths/heimdal_gssapi.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Copyright (c) Twitter Inc 2012
    Author: Phil Pennock <pdp@exim.org> */
diff --git a/src/src/auths/heimdal_gssapi.h b/src/src/auths/heimdal_gssapi.h
index 49775aff9..031b580ef 100644
--- a/src/src/auths/heimdal_gssapi.h
+++ b/src/src/auths/heimdal_gssapi.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Copyright (c) Twitter Inc 2012
    Author: Phil Pennock <pdp@exim.org> */
diff --git a/src/src/auths/plaintext.c b/src/src/auths/plaintext.c
index 58d178359..61be5867e 100644
--- a/src/src/auths/plaintext.c
+++ b/src/src/auths/plaintext.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "plaintext.h"
diff --git a/src/src/auths/plaintext.h b/src/src/auths/plaintext.h
index 4c6d01136..49862ff9b 100644
--- a/src/src/auths/plaintext.h
+++ b/src/src/auths/plaintext.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/auths/pwcheck.c b/src/src/auths/pwcheck.c
index 7dd529fbc..aff5ed39c 100644
--- a/src/src/auths/pwcheck.c
+++ b/src/src/auths/pwcheck.c
@@ -4,6 +4,7 @@
  * $Id: checkpw.c,v 1.49 2002/03/07 19:14:04 ken3 Exp $
  */
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
+/* SPDX-License-Identifier: GPL-2.0-only */
 /*
  * Copyright (c) 2001 Carnegie Mellon University.  All rights reserved.
  *
diff --git a/src/src/auths/pwcheck.h b/src/src/auths/pwcheck.h
index 1287ea253..4247b4e8b 100644
--- a/src/src/auths/pwcheck.h
+++ b/src/src/auths/pwcheck.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This file provides support for authentication via the Cyrus SASL pwcheck
 daemon (whence its name) and the newer saslauthd daemon. */
diff --git a/src/src/auths/spa.c b/src/src/auths/spa.c
index ff90d33a3..c5e6d83d5 100644
--- a/src/src/auths/spa.c
+++ b/src/src/auths/spa.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This file, which provides support for Microsoft's Secure Password
 Authentication, was contributed by Marc Prud'hommeaux. Tom Kistner added SPA
diff --git a/src/src/auths/spa.h b/src/src/auths/spa.h
index ca93469a3..4321971d9 100644
--- a/src/src/auths/spa.h
+++ b/src/src/auths/spa.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This file, which provides support for Microsoft's Secure Password
 Authentication, was contributed by Marc Prud'hommeaux. */
diff --git a/src/src/auths/tls.c b/src/src/auths/tls.c
index 325e7b4c7..d3ca8f796 100644
--- a/src/src/auths/tls.c
+++ b/src/src/auths/tls.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 1995 - 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This file provides an Exim authenticator driver for
 a server to verify a client SSL certificate
diff --git a/src/src/auths/tls.h b/src/src/auths/tls.h
index 7aa95b6c7..24e116464 100644
--- a/src/src/auths/tls.h
+++ b/src/src/auths/tls.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/auths/xtextdecode.c b/src/src/auths/xtextdecode.c
index 746dfbdb4..39c1f2b60 100644
--- a/src/src/auths/xtextdecode.c
+++ b/src/src/auths/xtextdecode.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 
diff --git a/src/src/auths/xtextencode.c b/src/src/auths/xtextencode.c
index fc571c799..89f12f1f1 100644
--- a/src/src/auths/xtextencode.c
+++ b/src/src/auths/xtextencode.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 
diff --git a/src/src/base64.c b/src/src/base64.c
index fa06a7a34..14e8cfffe 100644
--- a/src/src/base64.c
+++ b/src/src/base64.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004, 2015 */
 /* License: GPL */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
diff --git a/src/src/blob.h b/src/src/blob.h
index a3f1e24d4..95a987f0a 100644
--- a/src/src/blob.h
+++ b/src/src/blob.h
@@ -2,6 +2,7 @@
  *  Blob - a general pointer/size item for a memory chunk
  *
  *  Copyright (C) 2016  Exim maintainers
+ *  SPDX-License-Identifier: GPL-2.0-only
  */
 
 #ifndef BLOB_H	/* entire file */
diff --git a/src/src/bmi_spam.c b/src/src/bmi_spam.c
index af4bc4640..140bec48f 100644
--- a/src/src/bmi_spam.c
+++ b/src/src/bmi_spam.c
@@ -6,6 +6,7 @@
    Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004
    License: GPL */
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "exim.h"
 #ifdef EXPERIMENTAL_BRIGHTMAIL
diff --git a/src/src/bmi_spam.h b/src/src/bmi_spam.h
index a9af7784e..a9833b200 100644
--- a/src/src/bmi_spam.h
+++ b/src/src/bmi_spam.h
@@ -5,6 +5,7 @@
 /* Code for calling Brightmail AntiSpam.
    Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004
    License: GPL */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #ifdef EXPERIMENTAL_BRIGHTMAIL
 
diff --git a/src/src/buildconfig.c b/src/src/buildconfig.c
index bbbaefe9c..710b53550 100644
--- a/src/src/buildconfig.c
+++ b/src/src/buildconfig.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /*************************************************
diff --git a/src/src/child.c b/src/src/child.c
index 1f38b585c..b94e814a1 100644
--- a/src/src/child.c
+++ b/src/src/child.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "exim.h"
diff --git a/src/src/crypt16.c b/src/src/crypt16.c
index 56353c326..659bf88bd 100644
--- a/src/src/crypt16.c
+++ b/src/src/crypt16.c
@@ -2,6 +2,7 @@
  * Copyright (c) 2000-2002
  *   Chris Adams <cmadams@iruntheinter.net>
  *   written for HiWAAY Internet Services
+ * SPDX-License-Identifier: GPL-2.0-or-later
  *
  * This program is free software; you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
diff --git a/src/src/daemon.c b/src/src/daemon.c
index 8446f81cb..0afc7ca86 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions concerned with running Exim as a daemon */
 
diff --git a/src/src/dane.c b/src/src/dane.c
index 5ba61961a..f2ad22481 100644
--- a/src/src/dane.c
+++ b/src/src/dane.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2012, 2014 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This module provides DANE (RFC6659) support for Exim.  See also
 the draft RFC for DANE-over-SMTP, "SMTP security via opportunistic DANE TLS"
diff --git a/src/src/dbfn.c b/src/src/dbfn.c
index ea94b7ff0..f932520f9 100644
--- a/src/src/dbfn.c
+++ b/src/src/dbfn.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "exim.h"
diff --git a/src/src/dbfunctions.h b/src/src/dbfunctions.h
index 07d4a6233..93e1d3405 100644
--- a/src/src/dbfunctions.h
+++ b/src/src/dbfunctions.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #ifndef DBFUNCTIONS_H
 #define DBFUNCTIONS_H
diff --git a/src/src/dcc.c b/src/src/dcc.c
index 56dd6d570..d8e22b30c 100644
--- a/src/src/dcc.c
+++ b/src/src/dcc.c
@@ -8,6 +8,7 @@
  * See the file NOTICE for conditions of use and distribution.
  *
  * Copyright (c) The Exim Maintainers 2015 - 2022
+ * SPDX-License-Identifier: GPL-2.0-only
  */
 
 /* Code for calling dccifd. Called from acl.c. */
diff --git a/src/src/debug.c b/src/src/debug.c
index 26d09ea2f..38e8f8001 100644
--- a/src/src/debug.c
+++ b/src/src/debug.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2015 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "exim.h"
diff --git a/src/src/deliver.c b/src/src/deliver.c
index 725d0c872..719fa9d93 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* The main code for delivering a message. */
 
diff --git a/src/src/directory.c b/src/src/directory.c
index 189020855..c3b341bbb 100644
--- a/src/src/directory.c
+++ b/src/src/directory.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2010 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "exim.h"
 
diff --git a/src/src/dkim.c b/src/src/dkim.c
index bb916d2eb..9b6e14a3f 100644
--- a/src/src/dkim.c
+++ b/src/src/dkim.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge, 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Code for DKIM support. Other DKIM relevant code is in
    receive.c, transport.c and transports/smtp.c */
diff --git a/src/src/dkim.h b/src/src/dkim.h
index 7b94f22ef..61d83a9df 100644
--- a/src/src/dkim.h
+++ b/src/src/dkim.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge, 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 void    dkim_exim_init(void);
 gstring * dkim_exim_sign(int, off_t, uschar *, struct ob_dkim *, const uschar **);
diff --git a/src/src/dkim_transport.c b/src/src/dkim_transport.c
index cfd4b9085..142f4552a 100644
--- a/src/src/dkim_transport.c
+++ b/src/src/dkim_transport.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Transport shim for dkim signing */
 
diff --git a/src/src/dmarc.c b/src/src/dmarc.c
index 53c2752ac..c0313bf5c 100644
--- a/src/src/dmarc.c
+++ b/src/src/dmarc.c
@@ -5,6 +5,7 @@
    Copyright (c) The Exim Maintainers 2019 - 2022
    Copyright (c) Todd Lyons <tlyons@exim.org> 2012 - 2014
    License: GPL */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Portions Copyright (c) 2012, 2013, The Trusted Domain Project;
    All rights reserved, licensed for use per LICENSE.opendmarc. */
diff --git a/src/src/dmarc.h b/src/src/dmarc.h
index 899cd7e13..f71f1fd5a 100644
--- a/src/src/dmarc.h
+++ b/src/src/dmarc.h
@@ -6,6 +6,7 @@
    Copyright (c) The Exim Maintainers 2021 - 2022
    Copyright (c) Todd Lyons <tlyons@exim.org> 2012 - 2014
    License: GPL */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Portions Copyright (c) 2012, 2013, The Trusted Domain Project;
    All rights reserved, licensed for use per LICENSE.opendmarc. */
diff --git a/src/src/dns.c b/src/src/dns.c
index 4071c5822..8106fb688 100644
--- a/src/src/dns.c
+++ b/src/src/dns.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions for interfacing with the DNS. */
 
diff --git a/src/src/dnsbl.c b/src/src/dnsbl.c
index db839af04..af80f6be1 100644
--- a/src/src/dnsbl.c
+++ b/src/src/dnsbl.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions concerned with dnsbls */
 
diff --git a/src/src/drtables.c b/src/src/drtables.c
index 30eb855e3..a6cbede89 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "exim.h"
diff --git a/src/src/dummies.c b/src/src/dummies.c
index 38b514b6c..a72767ccd 100644
--- a/src/src/dummies.c
+++ b/src/src/dummies.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This file is not part of the main Exim code. There are little bits of test
 code for some of Exim's modules, and when they are used, the module they are
diff --git a/src/src/enq.c b/src/src/enq.c
index f7f8c9c16..054aeaed8 100644
--- a/src/src/enq.c
+++ b/src/src/enq.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions concerned with serialization. */
 
diff --git a/src/src/environment.c b/src/src/environment.c
index 9cb90c86f..06f804e69 100644
--- a/src/src/environment.c
+++ b/src/src/environment.c
@@ -5,6 +5,7 @@
 /* Copyright (c) Heiko Schlittermann 2016
  * hs@schlittermann.de
  * See the file NOTICE for conditions of use and distribution.
+ * SPDX-License-Identifier: GPL-2.0-only
  */
 
 #include "exim.h"
diff --git a/src/src/exim.c b/src/src/exim.c
index 3cd1d5f6d..62413e367 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* The main function: entry point, initialization, and high-level control.
diff --git a/src/src/exim.h b/src/src/exim.h
index 61642b5e7..24fedcced 100644
--- a/src/src/exim.h
+++ b/src/src/exim.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* Source files for exim all #include this header, which drags in everything
diff --git a/src/src/exim_dbmbuild.c b/src/src/exim_dbmbuild.c
index 5c046347b..bb04adc02 100644
--- a/src/src/exim_dbmbuild.c
+++ b/src/src/exim_dbmbuild.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* A small freestanding program to build dbm databases from serial input. For
diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index 3824309b5..669f7098d 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* This single source file is used to compile three utility programs for
diff --git a/src/src/exim_lock.c b/src/src/exim_lock.c
index 427d22c1e..1a54a92ad 100644
--- a/src/src/exim_lock.c
+++ b/src/src/exim_lock.c
@@ -11,6 +11,7 @@ Default is -fcntl -lockfile.
 Argument: the name of the lock file
 
 Copyright (c) The Exim Maintainers 2016 - 2021
+SPDX-License-Identifier: GPL-2.0-only
 */
 
 #include "os.h"
diff --git a/src/src/expand.c b/src/src/expand.c
index ff0b4d600..050f01297 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* Functions for handling string expansion. */
diff --git a/src/src/filter.c b/src/src/filter.c
index cc4af230e..82a9122c6 100644
--- a/src/src/filter.c
+++ b/src/src/filter.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* Code for mail filtering functions. */
diff --git a/src/src/filtertest.c b/src/src/filtertest.c
index 87ebfb14f..8bab65e78 100644
--- a/src/src/filtertest.c
+++ b/src/src/filtertest.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* Code for the filter test function. */
diff --git a/src/src/functions.h b/src/src/functions.h
index d5164547a..be1fae00d 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* Prototypes for functions that appear in various modules. Gathered together
diff --git a/src/src/globals.c b/src/src/globals.c
index cafb15992..429952edc 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* All the global variables are defined together in this one module, so
 that they are easy to find. */
diff --git a/src/src/globals.h b/src/src/globals.h
index 1be96daec..48d93a1c3 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Almost all the global variables are defined together in this one header, so
 that they are easy to find. However, those that are visible during the
diff --git a/src/src/hash.c b/src/src/hash.c
index 51bcd4604..2b71463f1 100644
--- a/src/src/hash.c
+++ b/src/src/hash.c
@@ -3,6 +3,7 @@
  *
  *  Copyright (c) The Exim Maintainers 2010 - 2022
  *  Copyright (c) University of Cambridge 1995 - 2009
+ *  SPDX-License-Identifier: GPL-2.0-only
  *
  *  Hash interface functions
  */
diff --git a/src/src/hash.h b/src/src/hash.h
index 588325baf..c94e53129 100644
--- a/src/src/hash.h
+++ b/src/src/hash.h
@@ -1,6 +1,7 @@
 /*
  *  Exim - an Internet mail transport agent
  *  Copyright (c) The Exim Maintainers 1995 - 2022
+ *  SPDX-License-Identifier: GPL-2.0-only
  *
  *  Hash interface functions
  */
diff --git a/src/src/header.c b/src/src/header.c
index 7ef59ff53..2a8fbfe64 100644
--- a/src/src/header.c
+++ b/src/src/header.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "exim.h"
diff --git a/src/src/hintsdb.h b/src/src/hintsdb.h
index b8e6744d6..e79e6bd49 100644
--- a/src/src/hintsdb.h
+++ b/src/src/hintsdb.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This header file contains macro definitions so that a variety of DBM
 libraries can be used by Exim. Nigel Metheringham provided the original set for
diff --git a/src/src/hintsdb_structs.h b/src/src/hintsdb_structs.h
index e0670a10e..27cd9edf9 100644
--- a/src/src/hintsdb_structs.h
+++ b/src/src/hintsdb_structs.h
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This header file contains the definitions of the structures used in the
 various hints databases are also kept in this file, which is used by the
diff --git a/src/src/host.c b/src/src/host.c
index b6c2ea082..874e19a08 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions for finding hosts, either by gethostbyname(), gethostbyaddr(), or
 directly via the DNS. When IPv6 is supported, getipnodebyname() and
diff --git a/src/src/host_address.c b/src/src/host_address.c
index 9e6f958be..28a8a685f 100644
--- a/src/src/host_address.c
+++ b/src/src/host_address.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "exim.h"
 
diff --git a/src/src/imap_utf7.c b/src/src/imap_utf7.c
index aac0fef6a..75ecb1a4b 100644
--- a/src/src/imap_utf7.c
+++ b/src/src/imap_utf7.c
@@ -1,5 +1,6 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "exim.h"
 
diff --git a/src/src/ip.c b/src/src/ip.c
index aa42343fb..2ac2b267d 100644
--- a/src/src/ip.c
+++ b/src/src/ip.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions for doing things with sockets. With the advent of IPv6 this has
 got messier, so that it's worth pulling out the code into separate functions
diff --git a/src/src/local_scan.c b/src/src/local_scan.c
index 7a3bae7e3..85ecba8cb 100644
--- a/src/src/local_scan.c
+++ b/src/src/local_scan.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /******************************************************************************
diff --git a/src/src/local_scan.h b/src/src/local_scan.h
index c609a27e3..2eabc5929 100644
--- a/src/src/local_scan.h
+++ b/src/src/local_scan.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This file is the header that is the only Exim header to be included in the
 source for the local_scan.c() function. It contains definitions that are made
diff --git a/src/src/log.c b/src/src/log.c
index a46d523db..f1d435ce4 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions for writing log files. The code for maintaining datestamped
 log files was originally contributed by Tony Sheen. */
diff --git a/src/src/lookupapi.h b/src/src/lookupapi.h
index 41cc239ce..cec8f976b 100644
--- a/src/src/lookupapi.h
+++ b/src/src/lookupapi.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* The "type" field in each item is a set of bit flags:
diff --git a/src/src/lookups/cdb.c b/src/src/lookups/cdb.c
index 966078f1a..696e52019 100644
--- a/src/src/lookups/cdb.c
+++ b/src/src/lookups/cdb.c
@@ -8,6 +8,7 @@
  *
  * Copyright (c) The Exim Maintainers 2020 - 2022
  * Copyright (c) 1998 Nigel Metheringham, Planet Online Ltd
+ * SPDX-License-Identifier: GPL-2.0-or-later
  *
  * This program is free software; you can redistribute it and/or
  * modify it under the terms of the GNU General Public License
diff --git a/src/src/lookups/dbmdb.c b/src/src/lookups/dbmdb.c
index 32514afcc..c99e948b5 100644
--- a/src/src/lookups/dbmdb.c
+++ b/src/src/lookups/dbmdb.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/dnsdb.c b/src/src/lookups/dnsdb.c
index 355be1b5d..4f43cf674 100644
--- a/src/src/lookups/dnsdb.c
+++ b/src/src/lookups/dnsdb.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c
index a7691024a..6cae0dafb 100644
--- a/src/src/lookups/dsearch.c
+++ b/src/src/lookups/dsearch.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* The idea for this code came from Matthew Byng-Maddick, but his original has
 been heavily reworked a lot for Exim 4 (and it now uses stat() (more precisely:
diff --git a/src/src/lookups/ibase.c b/src/src/lookups/ibase.c
index c4fff71bd..d42e490c4 100644
--- a/src/src/lookups/ibase.c
+++ b/src/src/lookups/ibase.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* The code in this module was contributed by Ard Biesheuvel. */
 
diff --git a/src/src/lookups/json.c b/src/src/lookups/json.c
index c9abf8c4c..b1e5fb742 100644
--- a/src/src/lookups/json.c
+++ b/src/src/lookups/json.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) Jeremy Harris 2019 - 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/ldap.c b/src/src/lookups/ldap.c
index 9751fa3b3..17c431e5c 100644
--- a/src/src/lookups/ldap.c
+++ b/src/src/lookups/ldap.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Many thanks to Stuart Lynne for contributing the original code for this
 driver. Further contributions from Michael Haardt, Brian Candler, Barry
diff --git a/src/src/lookups/ldap.h b/src/src/lookups/ldap.h
index ddfda8597..30228aebe 100644
--- a/src/src/lookups/ldap.h
+++ b/src/src/lookups/ldap.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Header for eldapauth_find */
 
diff --git a/src/src/lookups/lf_check_file.c b/src/src/lookups/lf_check_file.c
index 7f0f12806..1649f9f83 100644
--- a/src/src/lookups/lf_check_file.c
+++ b/src/src/lookups/lf_check_file.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/lookups/lf_functions.h b/src/src/lookups/lf_functions.h
index fd9eb303a..b83b9652a 100644
--- a/src/src/lookups/lf_functions.h
+++ b/src/src/lookups/lf_functions.h
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Header for the functions that are shared by the lookups */
 
diff --git a/src/src/lookups/lf_quote.c b/src/src/lookups/lf_quote.c
index 6f4143d9f..816fe01e2 100644
--- a/src/src/lookups/lf_quote.c
+++ b/src/src/lookups/lf_quote.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/lookups/lf_sqlperform.c b/src/src/lookups/lf_sqlperform.c
index ce6f1635a..cf4b9cd0b 100644
--- a/src/src/lookups/lf_sqlperform.c
+++ b/src/src/lookups/lf_sqlperform.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/lookups/lmdb.c b/src/src/lookups/lmdb.c
index a32c7f7fb..ccfdbe453 100644
--- a/src/src/lookups/lmdb.c
+++ b/src/src/lookups/lmdb.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 2016 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 
diff --git a/src/src/lookups/lsearch.c b/src/src/lookups/lsearch.c
index dcfdec91e..da73ae8bf 100644
--- a/src/src/lookups/lsearch.c
+++ b/src/src/lookups/lsearch.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/mysql.c b/src/src/lookups/mysql.c
index 78b8c2b19..a8dae4ade 100644
--- a/src/src/lookups/mysql.c
+++ b/src/src/lookups/mysql.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Thanks to Paul Kelly for contributing the original code for these
 functions. */
diff --git a/src/src/lookups/nis.c b/src/src/lookups/nis.c
index 0024f44cf..e7c124757 100644
--- a/src/src/lookups/nis.c
+++ b/src/src/lookups/nis.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/nisplus.c b/src/src/lookups/nisplus.c
index d9f3f7d83..78c79563f 100644
--- a/src/src/lookups/nisplus.c
+++ b/src/src/lookups/nisplus.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/oracle.c b/src/src/lookups/oracle.c
index d32b5e44d..3f3868fe9 100644
--- a/src/src/lookups/oracle.c
+++ b/src/src/lookups/oracle.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Interface to an Oracle database. This code was originally supplied by
 Paul Kelly, but I have hacked it around for various reasons, and tried to add
diff --git a/src/src/lookups/passwd.c b/src/src/lookups/passwd.c
index eaf78b210..7df1a5aaf 100644
--- a/src/src/lookups/passwd.c
+++ b/src/src/lookups/passwd.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 
diff --git a/src/src/lookups/pgsql.c b/src/src/lookups/pgsql.c
index 4bb693ad1..4fcd0e3ed 100644
--- a/src/src/lookups/pgsql.c
+++ b/src/src/lookups/pgsql.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Thanks to Petr Cech for contributing the original code for these
 functions. Thanks to Joachim Wieland for the initial patch for the Unix domain
diff --git a/src/src/lookups/readsock.c b/src/src/lookups/readsock.c
index 22179c91c..7c7b9cfa8 100644
--- a/src/src/lookups/readsock.c
+++ b/src/src/lookups/readsock.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) Jeremy Harris 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/redis.c b/src/src/lookups/redis.c
index 9c8559c1e..c7fcb66f0 100644
--- a/src/src/lookups/redis.c
+++ b/src/src/lookups/redis.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 
diff --git a/src/src/lookups/spf.c b/src/src/lookups/spf.c
index 78d954c8c..a1052d7fc 100644
--- a/src/src/lookups/spf.c
+++ b/src/src/lookups/spf.c
@@ -7,6 +7,7 @@
 
 Copyright (c) The Exim Maintainers 2020 - 2022
 Copyright (c) 2005 Chris Webb, Arachsys Internet Services Ltd
+SPDX-License-Identifier: GPL-2.0-or-later
 
 This program is free software; you can redistribute it and/or
 modify it under the terms of the GNU General Public License
diff --git a/src/src/lookups/sqlite.c b/src/src/lookups/sqlite.c
index 9080ae702..b9a735e18 100644
--- a/src/src/lookups/sqlite.c
+++ b/src/src/lookups/sqlite.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/testdb.c b/src/src/lookups/testdb.c
index 48241615f..f94150b68 100644
--- a/src/src/lookups/testdb.c
+++ b/src/src/lookups/testdb.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/whoson.c b/src/src/lookups/whoson.c
index 990703fd0..cd236787e 100644
--- a/src/src/lookups/whoson.c
+++ b/src/src/lookups/whoson.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This code originally came from Robert Wal. */
 
diff --git a/src/src/lss.c b/src/src/lss.c
index 167522d2f..0d20c07b6 100644
--- a/src/src/lss.c
+++ b/src/src/lss.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Support functions for calling from local_scan(). These are mostly just
 wrappers for various internal functions. */
diff --git a/src/src/macro_predef.c b/src/src/macro_predef.c
index a0c659c9f..fe814500d 100644
--- a/src/src/macro_predef.c
+++ b/src/src/macro_predef.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) Jeremy Harris 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Create a static data structure with the predefined macros, to be
 included in the main Exim build */
diff --git a/src/src/macro_predef.h b/src/src/macro_predef.h
index 59b1bbe3b..d3bc5c074 100644
--- a/src/src/macro_predef.h
+++ b/src/src/macro_predef.h
@@ -5,6 +5,7 @@
 /* Copyright (c) Jeremy Harris 2017 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Global functions */
 
diff --git a/src/src/macros.h b/src/src/macros.h
index adbe6a267..db1c34b65 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* These two macros make it possible to obtain the result of macro-expanding
diff --git a/src/src/malware.c b/src/src/malware.c
index 01dd455ba..96514e276 100644
--- a/src/src/malware.c
+++ b/src/src/malware.c
@@ -6,6 +6,7 @@
  * Copyright (c) The Exim Maintainers 2015 - 2022
  * Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015
  * License: GPL
+ * SPDX-License-Identifier: GPL-2.0-only
  */
 
 /* Code for calling virus (malware) scanners. Called from acl.c. */
diff --git a/src/src/match.c b/src/src/match.c
index b4a0352ee..a877aef3b 100644
--- a/src/src/match.c
+++ b/src/src/match.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions for matching strings */
 
diff --git a/src/src/md5.c b/src/src/md5.c
index fdb144e67..e6df32c7a 100644
--- a/src/src/md5.c
+++ b/src/src/md5.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #ifndef STAND_ALONE
 #include "exim.h"
diff --git a/src/src/mime.c b/src/src/mime.c
index c1921992e..bebe9bf44 100644
--- a/src/src/mime.c
+++ b/src/src/mime.c
@@ -6,6 +6,7 @@
  * Copyright (c) The Exim Maintainers 2015 - 2022
  * Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004 - 2015
  * License: GPL
+ * SPDX-License-Identifier: GPL-2.0-only
  */
 
 #include "exim.h"
diff --git a/src/src/mime.h b/src/src/mime.h
index 5fd4392d5..48bbd81de 100644
--- a/src/src/mime.h
+++ b/src/src/mime.h
@@ -5,6 +5,7 @@
 /* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004, 2015
  * License: GPL
  * Copyright (c) The Exim Maintainers 2016
+ * SPDX-License-Identifier: GPL-2.0-only
  */
 
 #ifdef WITH_CONTENT_SCAN
diff --git a/src/src/moan.c b/src/src/moan.c
index 4f2550d82..387359458 100644
--- a/src/src/moan.c
+++ b/src/src/moan.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions for sending messages to sender or to mailmaster. */
 
diff --git a/src/src/mytypes.h b/src/src/mytypes.h
index 141d2c613..06426af7a 100644
--- a/src/src/mytypes.h
+++ b/src/src/mytypes.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* This header file contains type definitions and macros that I use as
diff --git a/src/src/os.c b/src/src/os.c
index ac5f61b65..4edb2ca98 100644
--- a/src/src/os.c
+++ b/src/src/os.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #ifdef STAND_ALONE
 # include <signal.h>
diff --git a/src/src/osfunctions.h b/src/src/osfunctions.h
index 547cf1379..454e9f77f 100644
--- a/src/src/osfunctions.h
+++ b/src/src/osfunctions.h
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2016 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Prototypes for os-specific functions. For utilities, we don't need the one
 that uses a type that isn't defined for them. */
diff --git a/src/src/parse.c b/src/src/parse.c
index bdba3ecd0..93b12bc77 100644
--- a/src/src/parse.c
+++ b/src/src/parse.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions for parsing addresses */
 
diff --git a/src/src/pdkim/crypt_ver.h b/src/src/pdkim/crypt_ver.h
index a6d7e36af..367527e39 100644
--- a/src/src/pdkim/crypt_ver.h
+++ b/src/src/pdkim/crypt_ver.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Signing and hashing routine selection for PDKIM */
 
diff --git a/src/src/pdkim/pdkim.c b/src/src/pdkim/pdkim.c
index e47bfc505..eb26b3864 100644
--- a/src/src/pdkim/pdkim.c
+++ b/src/src/pdkim/pdkim.c
@@ -4,6 +4,7 @@
  *  Copyright (c) The Exim Maintainers 2021 - 2022
  *  Copyright (C) 2009 - 2016  Tom Kistner <tom@duncanthrax.net>
  *  Copyright (C) 2016 - 2020  Jeremy Harris <jgh@exim.org>
+ *  SPDX-License-Identifier: GPL-2.0-or-later
  *
  *  http://duncanthrax.net/pdkim/
  *
diff --git a/src/src/pdkim/pdkim.h b/src/src/pdkim/pdkim.h
index f6ff78251..f918938e8 100644
--- a/src/src/pdkim/pdkim.h
+++ b/src/src/pdkim/pdkim.h
@@ -3,6 +3,7 @@
  *
  *  Copyright (C) 2009 - 2012  Tom Kistner <tom@duncanthrax.net>
  *  Copyright (c) 2016 - 2020  Jeremy Harris
+ *  SPDX-License-Identifier: GPL-2.0-or-later
  *
  *  http://duncanthrax.net/pdkim/
  *
diff --git a/src/src/pdkim/pdkim_hash.h b/src/src/pdkim/pdkim_hash.h
index 8f9a12628..e470299a4 100644
--- a/src/src/pdkim/pdkim_hash.h
+++ b/src/src/pdkim/pdkim_hash.h
@@ -2,6 +2,7 @@
  *  PDKIM - a RFC4871 (DKIM) implementation
  *
  *  Copyright (C) 1995 - 2018  Exim maintainers
+ *  SPDX-License-Identifier: GPL-2.0-only
  *
  *  Hash interface functions
  */
diff --git a/src/src/pdkim/signing.c b/src/src/pdkim/signing.c
index d78f31acf..8876f617c 100644
--- a/src/src/pdkim/signing.c
+++ b/src/src/pdkim/signing.c
@@ -1,6 +1,7 @@
 /*
  *  PDKIM - a RFC4871 (DKIM) implementation
  *  Copyright (c) The Exim Maintainers 1995 - 2022
+ *  SPDX-License-Identifier: GPL-2.0-only
  *
  *  signing/verification interface
  */
diff --git a/src/src/pdkim/signing.h b/src/src/pdkim/signing.h
index ed6f397ce..880342b6f 100644
--- a/src/src/pdkim/signing.h
+++ b/src/src/pdkim/signing.h
@@ -2,6 +2,7 @@
  *  PDKIM - a RFC4871 (DKIM) implementation
  *
  *  Copyright (C) 1995 - 2020  Exim maintainers
+ *  SPDX-License-Identifier: GPL-2.0-only
  *
  *  RSA signing/verification interface
  */
diff --git a/src/src/perl.c b/src/src/perl.c
index f07ee2e27..b5c3f882e 100644
--- a/src/src/perl.c
+++ b/src/src/perl.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) The Exim Maintainers 1999 - 2022 */
 /* Copyright (c) 1998 Malcolm Beattie */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Modified by PH to get rid of the "na" usage, March 1999.
    Modified further by PH for general tidying for Exim 4.
diff --git a/src/src/priv.c b/src/src/priv.c
index 94d425401..c818b5b78 100644
--- a/src/src/priv.c
+++ b/src/src/priv.c
@@ -1,3 +1,6 @@
+/* Copyright (c)  The Exim Maintainers 2022 *
+/* SPDX-License-Identifier: GPL-2.0-only */
+
 #include "exim.h"
 #include <sys/types.h>
 #include <unistd.h>
diff --git a/src/src/queue.c b/src/src/queue.c
index 6e47d2c8a..fd84d303f 100644
--- a/src/src/queue.c
+++ b/src/src/queue.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions that operate on the input queue. */
 
diff --git a/src/src/rda.c b/src/src/rda.c
index b635ebfde..1bc322e34 100644
--- a/src/src/rda.c
+++ b/src/src/rda.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This module contains code for extracting addresses from a forwarding list
 (from an alias or forward file) or by running the filter interpreter. It may do
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 83ee51b65..b0d688772 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions for reading the configuration file, and for displaying
 overall configuration values. Thanks to Brian Candler for the original
diff --git a/src/src/receive.c b/src/src/receive.c
index 0a27c7950..d9d0757ef 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Code for receiving a message and setting up spool files. */
 
diff --git a/src/src/regex.c b/src/src/regex.c
index 210620f26..eefba8ecf 100644
--- a/src/src/regex.c
+++ b/src/src/regex.c
@@ -6,6 +6,7 @@
  * Copyright (c) The Exim Maintainers 2016 - 2022
  * Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003-2015
  * License: GPL
+ * SPDX-License-Identifier: GPL-2.0-only
  */
 
 /* Code for matching regular expressions against headers and body.
diff --git a/src/src/regex_cache.c b/src/src/regex_cache.c
index 63cddce1d..e4771aab9 100644
--- a/src/src/regex_cache.c
+++ b/src/src/regex_cache.c
@@ -5,6 +5,7 @@
 /*
  * Copyright (c) The Exim Maintainers 2022
  * License: GPL
+ * SPDX-License-Identifier: GPL-2.0-only
  */
 
 /* Caching layers for compiled REs.  There is a local layer in the process,
diff --git a/src/src/retry.c b/src/src/retry.c
index 033afb4bf..e1490266c 100644
--- a/src/src/retry.c
+++ b/src/src/retry.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions concerned with retrying unsuccessful deliveries. */
 
diff --git a/src/src/rewrite.c b/src/src/rewrite.c
index 90614e626..040525ec5 100644
--- a/src/src/rewrite.c
+++ b/src/src/rewrite.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions concerned with rewriting headers */
 
diff --git a/src/src/rfc2047.c b/src/src/rfc2047.c
index 1ed1dd809..af8993695 100644
--- a/src/src/rfc2047.c
+++ b/src/src/rfc2047.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This file contains a function for decoding message header lines that may
 contain encoded "words" according to the rules described in
diff --git a/src/src/route.c b/src/src/route.c
index fa69b8b74..7e6e4eb69 100644
--- a/src/src/route.c
+++ b/src/src/route.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions concerned with routing, and the list of generic router options. */
 
diff --git a/src/src/routers/accept.c b/src/src/routers/accept.c
index 110a1efe4..ab02716eb 100644
--- a/src/src/routers/accept.c
+++ b/src/src/routers/accept.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/accept.h b/src/src/routers/accept.h
index 43494fbc4..e025179ee 100644
--- a/src/src/routers/accept.h
+++ b/src/src/routers/accept.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Private structure for the private options (there aren't any). */
 
diff --git a/src/src/routers/dnslookup.c b/src/src/routers/dnslookup.c
index a845b4e52..0b1abeb8e 100644
--- a/src/src/routers/dnslookup.c
+++ b/src/src/routers/dnslookup.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/dnslookup.h b/src/src/routers/dnslookup.h
index b7e091587..0272db300 100644
--- a/src/src/routers/dnslookup.h
+++ b/src/src/routers/dnslookup.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/routers/ipliteral.c b/src/src/routers/ipliteral.c
index 3d68642a2..cfbf276ff 100644
--- a/src/src/routers/ipliteral.c
+++ b/src/src/routers/ipliteral.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/ipliteral.h b/src/src/routers/ipliteral.h
index 1ddb38b10..f7e99b9c2 100644
--- a/src/src/routers/ipliteral.h
+++ b/src/src/routers/ipliteral.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* Private structure for the private options. Some compilers do not like empty
diff --git a/src/src/routers/iplookup.c b/src/src/routers/iplookup.c
index 8b67f3116..970ec7704 100644
--- a/src/src/routers/iplookup.c
+++ b/src/src/routers/iplookup.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/iplookup.h b/src/src/routers/iplookup.h
index dbcb03c9e..3b7295574 100644
--- a/src/src/routers/iplookup.h
+++ b/src/src/routers/iplookup.h
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* Private structure for the private options. */
diff --git a/src/src/routers/manualroute.c b/src/src/routers/manualroute.c
index 974ad0cf4..6a500d09a 100644
--- a/src/src/routers/manualroute.c
+++ b/src/src/routers/manualroute.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/manualroute.h b/src/src/routers/manualroute.h
index 9c20b6fa0..e27d90953 100644
--- a/src/src/routers/manualroute.h
+++ b/src/src/routers/manualroute.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Header for the manualroute router */
 
diff --git a/src/src/routers/queryprogram.c b/src/src/routers/queryprogram.c
index 55f03a477..51b7b7551 100644
--- a/src/src/routers/queryprogram.c
+++ b/src/src/routers/queryprogram.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/queryprogram.h b/src/src/routers/queryprogram.h
index 93046bd0f..148846fae 100644
--- a/src/src/routers/queryprogram.h
+++ b/src/src/routers/queryprogram.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* Private structure for the private options. */
diff --git a/src/src/routers/redirect.c b/src/src/routers/redirect.c
index 31c07f518..6126555fe 100644
--- a/src/src/routers/redirect.c
+++ b/src/src/routers/redirect.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/redirect.h b/src/src/routers/redirect.h
index 4c0399a36..76d02f261 100644
--- a/src/src/routers/redirect.h
+++ b/src/src/routers/redirect.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Header for the redirect router */
 
diff --git a/src/src/routers/rf_change_domain.c b/src/src/routers/rf_change_domain.c
index d7c9c1cb8..4f8d47299 100644
--- a/src/src/routers/rf_change_domain.c
+++ b/src/src/routers/rf_change_domain.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/rf_expand_data.c b/src/src/routers/rf_expand_data.c
index 6a8ad1779..5fbf0ac78 100644
--- a/src/src/routers/rf_expand_data.c
+++ b/src/src/routers/rf_expand_data.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/rf_functions.h b/src/src/routers/rf_functions.h
index f310d5a42..d8fca68c5 100644
--- a/src/src/routers/rf_functions.h
+++ b/src/src/routers/rf_functions.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Header for the functions that are shared by the routers */
 
diff --git a/src/src/routers/rf_get_errors_address.c b/src/src/routers/rf_get_errors_address.c
index b9cf78124..1189fd9e0 100644
--- a/src/src/routers/rf_get_errors_address.c
+++ b/src/src/routers/rf_get_errors_address.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/rf_get_munge_headers.c b/src/src/routers/rf_get_munge_headers.c
index d304d1145..be86a4e0e 100644
--- a/src/src/routers/rf_get_munge_headers.c
+++ b/src/src/routers/rf_get_munge_headers.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/rf_get_transport.c b/src/src/routers/rf_get_transport.c
index 2f639e037..0c1fa8173 100644
--- a/src/src/routers/rf_get_transport.c
+++ b/src/src/routers/rf_get_transport.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/rf_get_ugid.c b/src/src/routers/rf_get_ugid.c
index 1735e5919..15144beb1 100644
--- a/src/src/routers/rf_get_ugid.c
+++ b/src/src/routers/rf_get_ugid.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/rf_lookup_hostlist.c b/src/src/routers/rf_lookup_hostlist.c
index 79a779927..affd70b6e 100644
--- a/src/src/routers/rf_lookup_hostlist.c
+++ b/src/src/routers/rf_lookup_hostlist.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/rf_queue_add.c b/src/src/routers/rf_queue_add.c
index 0693c8c6e..49dd83117 100644
--- a/src/src/routers/rf_queue_add.c
+++ b/src/src/routers/rf_queue_add.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/rf_self_action.c b/src/src/routers/rf_self_action.c
index 9a4dc3cc7..7cc592039 100644
--- a/src/src/routers/rf_self_action.c
+++ b/src/src/routers/rf_self_action.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/rf_set_ugid.c b/src/src/routers/rf_set_ugid.c
index e1346b4be..65f5200a2 100644
--- a/src/src/routers/rf_set_ugid.c
+++ b/src/src/routers/rf_set_ugid.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/search.c b/src/src/search.c
index eec543776..32099ab9e 100644
--- a/src/src/search.c
+++ b/src/src/search.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* A set of functions to search databases in various formats. An open
 database is represented by a void * value which is returned from a lookup-
diff --git a/src/src/setenv.c b/src/src/setenv.c
index 90e679304..877fe388f 100644
--- a/src/src/setenv.c
+++ b/src/src/setenv.c
@@ -6,6 +6,7 @@
  * Copyright (c) Jeremy Harris 2015 - 2016
  * Copyright (c) The Exim Maintainers 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This module provides (un)setenv routines for those environments
 lacking them in libraries. It is #include'd by OS/os.c-foo files. */
diff --git a/src/src/sha_ver.h b/src/src/sha_ver.h
index bc2b2f89e..8385c1377 100644
--- a/src/src/sha_ver.h
+++ b/src/src/sha_ver.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* SHA routine selection */
 
diff --git a/src/src/sieve.c b/src/src/sieve.c
index af3bc9d9a..033a9734a 100644
--- a/src/src/sieve.c
+++ b/src/src/sieve.c
@@ -6,6 +6,7 @@
  * Copyright (c) The Exim Maintainers 2016 - 2022
  * Copyright (c) Michael Haardt 2003 - 2015
  * See the file NOTICE for conditions of use and distribution.
+ * SPDX-License-Identifier: GPL-2.0-only
  */
 
 /* This code was contributed by Michael Haardt. */
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 28e529bae..3c6339c82 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions for handling an incoming SMTP call. */
 
diff --git a/src/src/smtp_out.c b/src/src/smtp_out.c
index 7f364d942..db39dcab5 100644
--- a/src/src/smtp_out.c
+++ b/src/src/smtp_out.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* A number of functions for driving outgoing SMTP calls. */
 
diff --git a/src/src/spam.c b/src/src/spam.c
index a68b9bf60..f6575c293 100644
--- a/src/src/spam.c
+++ b/src/src/spam.c
@@ -6,6 +6,7 @@
  * Copyright (c) The Exim Maintainers 2016 - 2022
  * Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015
  * License: GPL
+ * SPDX-License-Identifier: GPL-2.0-only
  */
 
 /* Code for calling spamassassin's spamd. Called from acl.c. */
diff --git a/src/src/spam.h b/src/src/spam.h
index cc36ffd3f..c410198d7 100644
--- a/src/src/spam.h
+++ b/src/src/spam.h
@@ -5,6 +5,7 @@
 /* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* License: GPL */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* spam defines */
 
diff --git a/src/src/spf.c b/src/src/spf.c
index a8c0f75c4..a94bc9677 100644
--- a/src/src/spf.c
+++ b/src/src/spf.c
@@ -6,6 +6,7 @@
    Copyright (c) The Exim Maintainers 2015 - 2022
    Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004 - 2014
    License: GPL
+   SPDX-License-Identifier: GPL-2.0-only
 */
 
 /* Code for calling spf checks via libspf-alt. Called from acl.c. */
diff --git a/src/src/spf.h b/src/src/spf.h
index f32d06906..8fb7b04cb 100644
--- a/src/src/spf.h
+++ b/src/src/spf.h
@@ -6,6 +6,7 @@
    Copyright (c) The Exim Maintainers 2016 - 2022
    Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004
    License: GPL
+   SPDX-License-Identifier: GPL-2.0-only
 */
 
 #ifdef SUPPORT_SPF
diff --git a/src/src/spool_in.c b/src/src/spool_in.c
index 2aa0b0b55..82d11bf65 100644
--- a/src/src/spool_in.c
+++ b/src/src/spool_in.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions for reading spool files. When compiling for a utility (eximon),
 not all are needed, and some functionality can be cut out. */
diff --git a/src/src/spool_mbox.c b/src/src/spool_mbox.c
index 8b2aae394..d9767ec3c 100644
--- a/src/src/spool_mbox.c
+++ b/src/src/spool_mbox.c
@@ -5,6 +5,7 @@
 /* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015
  * License: GPL
  * Copyright (c) The Exim Maintainers 2016 - 2021
+ * SPDX-License-Identifier: GPL-2.0-only
  */
 
 /* Code for setting up a MBOX style spool file inside a /scan/<msgid>
diff --git a/src/src/spool_out.c b/src/src/spool_out.c
index 510eda6c1..cb409c641 100644
--- a/src/src/spool_out.c
+++ b/src/src/spool_out.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions for writing spool files, and moving them about. */
 
diff --git a/src/src/std-crypto.c b/src/src/std-crypto.c
index 200fb7144..c1eef6c35 100644
--- a/src/src/std-crypto.c
+++ b/src/src/std-crypto.c
@@ -7,6 +7,8 @@
  * But almost everything here is fixed published constants from RFCs, so also:
  * Copyright (C) The Internet Society (2003)
  * Copyright (C) The IETF Trust (2008)
+ * SPDX-License-Identifier: GPL-2.0-only
+ *
  * Most of the text in RFC referencing comments is copy/paste from RFC,
  * as is undoubtedly the intention.
  * The constants are generated from that text using util/gen_pkcs3.c invoked
diff --git a/src/src/store.c b/src/src/store.c
index d99d2ab69..449fb4ead 100644
--- a/src/src/store.c
+++ b/src/src/store.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim maintainers 2019 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Exim gets and frees all its store through these functions. In the original
 implementation there was a lot of mallocing and freeing of small bits of store.
diff --git a/src/src/store.h b/src/src/store.h
index ee6d79c36..47ed963cd 100644
--- a/src/src/store.h
+++ b/src/src/store.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Header for Exim's memory allocation functions */
 
diff --git a/src/src/string.c b/src/src/string.c
index a5161bb31..ff4180b2b 100644
--- a/src/src/string.c
+++ b/src/src/string.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Miscellaneous string-handling functions. Some are not required for
 utilities and tests, and are cut out by the COMPILE_UTILITY macro. */
diff --git a/src/src/structs.h b/src/src/structs.h
index 06cd06084..1cb69236e 100644
--- a/src/src/structs.h
+++ b/src/src/structs.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 /* Definitions of various structures. In addition, those that are visible for
diff --git a/src/src/tls-cipher-stdname.c b/src/src/tls-cipher-stdname.c
index ab973af51..fe442a050 100644
--- a/src/src/tls-cipher-stdname.c
+++ b/src/src/tls-cipher-stdname.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 2019 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Translate an IETF TLS ciphersuite code to an IETF ciphersuite name,
 for use when the TLS library do not provide such names.
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index 1fc7828cf..c98760202 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -6,6 +6,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) Phil Pennock 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This file provides TLS/SSL support for Exim using the GnuTLS library,
 one of the available supported implementations.  This file is #included into
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 8ed413e91..052d8161d 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2019 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Portions Copyright (c) The OpenSSL Project 1999 */
 
diff --git a/src/src/tls.c b/src/src/tls.c
index d7cefce67..9e20b5bca 100644
--- a/src/src/tls.c
+++ b/src/src/tls.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This module provides TLS (aka SSL) support for Exim. The code for OpenSSL is
 based on a patch that was originally contributed by Steve Haslam. It was
diff --git a/src/src/tlscert-gnu.c b/src/src/tlscert-gnu.c
index a40bb30bf..8840d5cfd 100644
--- a/src/src/tlscert-gnu.c
+++ b/src/src/tlscert-gnu.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) Jeremy Harris 2014 - 2018 */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This file provides TLS/SSL support for Exim using the GnuTLS library,
 one of the available supported implementations.  This file is #included into
diff --git a/src/src/tlscert-openssl.c b/src/src/tlscert-openssl.c
index ac353b25f..a4c3d19fa 100644
--- a/src/src/tlscert-openssl.c
+++ b/src/src/tlscert-openssl.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) Jeremy Harris 2014 - 2019 */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* This module provides TLS (aka SSL) support for Exim using the OpenSSL
 library. It is #included into the tls.c file when that library is used.
diff --git a/src/src/tod.c b/src/src/tod.c
index 1f0bcc05e..ac4ed35b9 100644
--- a/src/src/tod.c
+++ b/src/src/tod.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* A function for returning the time of day in various formats */
 
diff --git a/src/src/transport.c b/src/src/transport.c
index 7477882cb..ff2e0b1d4 100644
--- a/src/src/transport.c
+++ b/src/src/transport.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* General functions concerned with transportation, and generic options for all
 transports. */
diff --git a/src/src/transports/appendfile.c b/src/src/transports/appendfile.c
index 7e29dd3bc..18badde86 100644
--- a/src/src/transports/appendfile.c
+++ b/src/src/transports/appendfile.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/transports/appendfile.h b/src/src/transports/appendfile.h
index 3fd2f467c..108114905 100644
--- a/src/src/transports/appendfile.h
+++ b/src/src/transports/appendfile.h
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/transports/autoreply.c b/src/src/transports/autoreply.c
index 211e32810..eff1a3792 100644
--- a/src/src/transports/autoreply.c
+++ b/src/src/transports/autoreply.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/transports/autoreply.h b/src/src/transports/autoreply.h
index fcfd981e7..5f278a51e 100644
--- a/src/src/transports/autoreply.h
+++ b/src/src/transports/autoreply.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/transports/lmtp.c b/src/src/transports/lmtp.c
index f751771c1..c734c3a26 100644
--- a/src/src/transports/lmtp.c
+++ b/src/src/transports/lmtp.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/transports/lmtp.h b/src/src/transports/lmtp.h
index 93f0f89cc..71fd77f26 100644
--- a/src/src/transports/lmtp.h
+++ b/src/src/transports/lmtp.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/transports/pipe.c b/src/src/transports/pipe.c
index bdbe27d57..b8103e2f6 100644
--- a/src/src/transports/pipe.c
+++ b/src/src/transports/pipe.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "../exim.h"
diff --git a/src/src/transports/pipe.h b/src/src/transports/pipe.h
index ed5c142b3..416569608 100644
--- a/src/src/transports/pipe.h
+++ b/src/src/transports/pipe.h
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2014 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/transports/queuefile.c b/src/src/transports/queuefile.c
index 74131cc64..3a2bae22f 100644
--- a/src/src/transports/queuefile.c
+++ b/src/src/transports/queuefile.c
@@ -6,6 +6,7 @@
 /* Copyright (c) University of Cambridge 2016 */
 /* Copyright (c) The Exim Maintainers 1995 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 
diff --git a/src/src/transports/queuefile.h b/src/src/transports/queuefile.h
index 0e45b51b0..22759a7f7 100644
--- a/src/src/transports/queuefile.h
+++ b/src/src/transports/queuefile.h
@@ -5,6 +5,7 @@
 /* Copyright (c) Andrew Colin Kissa <andrew@topdog.za.net> 2016 */
 /* Copyright (c) University of Cambridge 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index 0fca4584d..2d2db1b10 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #include "../exim.h"
 #include "smtp.h"
diff --git a/src/src/transports/smtp.h b/src/src/transports/smtp.h
index 319e8494e..8793ece4f 100644
--- a/src/src/transports/smtp.h
+++ b/src/src/transports/smtp.h
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 #define DELIVER_BUFFER_SIZE 4096
 
diff --git a/src/src/transports/smtp_socks.c b/src/src/transports/smtp_socks.c
index 0e58732c6..353a69618 100644
--- a/src/src/transports/smtp_socks.c
+++ b/src/src/transports/smtp_socks.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) Jeremy Harris 2015 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* SOCKS version 5 proxy, client-mode */
 
diff --git a/src/src/transports/tf_maildir.c b/src/src/transports/tf_maildir.c
index 205ee41cb..925b8fac0 100644
--- a/src/src/transports/tf_maildir.c
+++ b/src/src/transports/tf_maildir.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions in support of the use of maildirsize files for handling quotas in
 maildir directories. Some of the rules are a bit baroque:
diff --git a/src/src/transports/tf_maildir.h b/src/src/transports/tf_maildir.h
index b3707b1a9..d314c3203 100644
--- a/src/src/transports/tf_maildir.h
+++ b/src/src/transports/tf_maildir.h
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Header file for the functions that are used to support the use of
 maildirsize files for quota handling in maildir directories. */
diff --git a/src/src/tree.c b/src/src/tree.c
index bb8ad44db..e340d986a 100644
--- a/src/src/tree.c
+++ b/src/src/tree.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions for maintaining binary balanced trees and some associated
 functions as well. */
diff --git a/src/src/utf8.c b/src/src/utf8.c
index bc7adb828..6604727ff 100644
--- a/src/src/utf8.c
+++ b/src/src/utf8.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) Jeremy Harris 2015 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 
 #include "exim.h"
diff --git a/src/src/verify.c b/src/src/verify.c
index afc18d553..3a8914e38 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -5,6 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Functions concerned with verifying things. The original code for callout
 caching was contributed by Kevin Fleming (but I hacked it around a bit). */
diff --git a/src/src/version.c b/src/src/version.c
index 118ebbdf2..cbaaef24b 100644
--- a/src/src/version.c
+++ b/src/src/version.c
@@ -5,6 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2010 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-only */
 
 /* Function for setting up the version string. */
 
diff --git a/src/util/chunking_fixqueue_finalnewlines.pl b/src/util/chunking_fixqueue_finalnewlines.pl
index 5dddfa505..b8ab17bd7 100755
--- a/src/util/chunking_fixqueue_finalnewlines.pl
+++ b/src/util/chunking_fixqueue_finalnewlines.pl
@@ -1,4 +1,5 @@
 #!/usr/bin/env perl
+# SPDX-License-Identifier: GPL-2.0-only
 
 use warnings;
 use strict;
diff --git a/src/util/cramtest.pl b/src/util/cramtest.pl
index 48f989a0c..a6124628b 100755
--- a/src/util/cramtest.pl
+++ b/src/util/cramtest.pl
@@ -1,4 +1,5 @@
 #!/usr/bin/perl
+# SPDX-License-Identifier: GPL-2.0-only
 
 # This script is contributed by Vadim Vygonets to aid in debugging CRAM-MD5
 # authentication.
diff --git a/src/util/gen_pkcs3.c b/src/util/gen_pkcs3.c
index 2fe9a6c5e..3ef3110de 100644
--- a/src/util/gen_pkcs3.c
+++ b/src/util/gen_pkcs3.c
@@ -2,6 +2,7 @@
  * Copyright (c) The Exim Maintainers 2021
  * This is distributed as part of Exim and licensed under the GPL.
  * See the file "NOTICE" for more details.
+ * SPDX-License-Identifier: GPL-2.0-only
  */
 
 /* Build with:
diff --git a/src/util/logargs.sh b/src/util/logargs.sh
index 87369a64b..814228d10 100755
--- a/src/util/logargs.sh
+++ b/src/util/logargs.sh
@@ -1,4 +1,5 @@
 #! /bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
 
 # This script can be interposed between a calling program and another
 # program, in order to log the arguments which are being used. This can
diff --git a/src/util/mkcdb.pl b/src/util/mkcdb.pl
index 691849dcd..e2b5931e7 100755
--- a/src/util/mkcdb.pl
+++ b/src/util/mkcdb.pl
@@ -1,4 +1,5 @@
 #!/usr/bin/perl -wT
+# SPDX-License-Identifier: GPL-2.0-only
 #
 # Create cdb file from flat alias file. DPC: 15/10/98.
 # Args:      source  (may be relative or absolute)
diff --git a/src/util/ocsp_fetch.pl b/src/util/ocsp_fetch.pl
index 08ca4cbe4..2afbdbdac 100755
--- a/src/util/ocsp_fetch.pl
+++ b/src/util/ocsp_fetch.pl
@@ -1,6 +1,8 @@
 #!/usr/bin/perl
 # Copyright (C) 2012 Wizards Internet Ltd
 # License GPLv2: GNU GPL version 2 <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
+# SPDX-License-Identifier: GPL-2.0-only
+
 use strict;
 BEGIN { pop @INC if $INC[-1] eq '.' };
 use Getopt::Std;
diff --git a/src/util/proxy_protocol_client.pl b/src/util/proxy_protocol_client.pl
index 67a171d5d..8b4311b64 100644
--- a/src/util/proxy_protocol_client.pl
+++ b/src/util/proxy_protocol_client.pl
@@ -3,6 +3,7 @@
 # Copyright (C) 2014 Todd Lyons
 # License GPLv2: GNU GPL version 2
 # <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
+# SPDX-License-Identifier: GPL-2.0-only
 #
 # This script emulates a proxy which uses Proxy Protocol to communicate
 # to a backend server.  It should be run from an IP which is configured
diff --git a/src/util/ratelimit.pl b/src/util/ratelimit.pl
index e212fa241..2ceeebbfa 100644
--- a/src/util/ratelimit.pl
+++ b/src/util/ratelimit.pl
@@ -1,4 +1,5 @@
 #!/usr/bin/perl -wT
+# SPDX-License-Identifier: GPL-2.0-only
 
 use strict;
 
diff --git a/src/util/renew-opendmarc-tlds.sh b/src/util/renew-opendmarc-tlds.sh
index 9967018e2..583dc0e40 100755
--- a/src/util/renew-opendmarc-tlds.sh
+++ b/src/util/renew-opendmarc-tlds.sh
@@ -1,4 +1,5 @@
 #!/bin/sh -eu
+# SPDX-License-Identifier: GPL-2.0-only
 #
 # Short version of this script:
 #   curl -f -o /var/cache/exim/opendmarc.tlds https://publicsuffix.org/list/public_suffix_list.dat
diff --git a/src/util/unknownuser.sh b/src/util/unknownuser.sh
index fe04dc6b1..01e617978 100755
--- a/src/util/unknownuser.sh
+++ b/src/util/unknownuser.sh
@@ -1,4 +1,5 @@
 #! /bin/sh
+# SPDX-License-Identifier: GPL-2.0-only
 
 # This is a sample script for demonstrating how to handle unknown users in
 # a more friendly way than just returning a "user unknown" error. It can

commit 1d28cc061677bd07d9bed48dd84bd5c590247043
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 27 14:40:20 2022 +0000

    SPDX: Mass-update to GPL-2.0-or-later
    
    This is based on the text in src/NOTICE
    which is taken to override text in individual files

diff --git a/src/Makefile b/src/Makefile
index b774b43dd..d190d9aa0 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -4,6 +4,7 @@
 
 # Copyright (c) The Exim Maintainers 2022
 # Copyright (c) University of Cambridge, 1995 - 2018
+# SPDX-License-Identifier: GPL-2.0-or-later
 # See the file NOTICE for conditions of use and distribution.
 
 # IRIX make uses the shell that is in the SHELL variable, which often defaults
diff --git a/src/NOTICE b/src/NOTICE
index b4a5c407f..4aa562ea3 100644
--- a/src/NOTICE
+++ b/src/NOTICE
@@ -2,6 +2,7 @@ THE EXIM MAIL TRANSFER AGENT
 ----------------------------
 
 Copyright (c) 2004 University of Cambridge
+SPDX-License-Identifier: GPL-2.0-or-later
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index 0c64d45d4..4e1f61a26 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -1,5 +1,4 @@
 # This file is the basis of the main makefile for Exim and friends. The
-#
 # makefile at the top level arranges to build the main makefile by calling
 # scripts/Configure-Makefile from within the build directory. This
 # concatenates the configuration settings from Local/Makefile and other,
@@ -7,6 +6,7 @@
 # build directory.
 #
 # Copyright (c) The Exim Maintainers 1995 - 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 SHELL      = $(MAKE_SHELL)
 SCRIPTS    = ../scripts
diff --git a/src/OS/Makefile-Default b/src/OS/Makefile-Default
index 6e42db471..7481d3163 100644
--- a/src/OS/Makefile-Default
+++ b/src/OS/Makefile-Default
@@ -2,6 +2,9 @@
 #          The Exim mail transport agent         #
 ##################################################
 
+# Copyright (c) The Exim Maintainers 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
+
 # Generic default make file containing settings that relate to the OS or
 # to selectable features within the OS. The configuration options for Exim
 # itself live in Local/Makefile, which is constructed by editing src/EDITME.
diff --git a/src/OS/Makefile-FreeBSD b/src/OS/Makefile-FreeBSD
index 4793a438b..f0fb8f2c9 100644
--- a/src/OS/Makefile-FreeBSD
+++ b/src/OS/Makefile-FreeBSD
@@ -1,5 +1,7 @@
 # Exim: OS-specific make file for FreeBSD
+#
 # Copyright (c) The Exim Maintainers 2020
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 CHOWN_COMMAND=/usr/sbin/chown
 #STRIP_COMMAND=/usr/bin/strip
diff --git a/src/OS/Makefile-GNU b/src/OS/Makefile-GNU
index b49976f8c..366ce4263 100644
--- a/src/OS/Makefile-GNU
+++ b/src/OS/Makefile-GNU
@@ -1,5 +1,7 @@
 # Exim: OS-specific make file for GNU and variants.
+#
 # Copyright (c) The Exim Maintainers 2020
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 HAVE_ICONV=yes
 
diff --git a/src/OS/Makefile-OpenBSD b/src/OS/Makefile-OpenBSD
index 7c451e2cc..da648df43 100644
--- a/src/OS/Makefile-OpenBSD
+++ b/src/OS/Makefile-OpenBSD
@@ -1,5 +1,6 @@
 # Exim: OS-specific make file for OpenBSD
 # Copyright (c) The Exim Maintainers 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 CHOWN_COMMAND=/usr/sbin/chown
 CHGRP_COMMAND=/usr/sbin/chgrp
diff --git a/src/OS/Makefile-SunOS5 b/src/OS/Makefile-SunOS5
index d07adcff4..e8b0d9506 100644
--- a/src/OS/Makefile-SunOS5
+++ b/src/OS/Makefile-SunOS5
@@ -1,5 +1,6 @@
 # Exim: OS-specific make file for SunOS5
 # Copyright (c) The Exim Maintainers 2020
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 HAVE_ICONV=yes
 
diff --git a/src/OS/eximon.conf-Default b/src/OS/eximon.conf-Default
index 2f874ef53..d003156e6 100644
--- a/src/OS/eximon.conf-Default
+++ b/src/OS/eximon.conf-Default
@@ -2,6 +2,9 @@
 # These can be overridden by OS-specific scripts and local installation
 # scripts, and also at run time by shell variables.
 
+# Copyright (c) The Exim Maintainers 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
+
 # The name of the eximon binary, usually the same as the eximon script,
 # with .bin stuck on the end.
 
diff --git a/src/OS/os.c-FreeBSD b/src/OS/os.c-FreeBSD
index 02b78587b..d65a789c0 100644
--- a/src/OS/os.c-FreeBSD
+++ b/src/OS/os.c-FreeBSD
@@ -4,6 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 1995 - 2020 */
 /* Copyright (c) The Exim Maintainers 2021 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* FreeBSD-specific code. This is concatenated onto the generic
diff --git a/src/OS/os.c-GNU b/src/OS/os.c-GNU
index dbd014918..2b2aeed22 100644
--- a/src/OS/os.c-GNU
+++ b/src/OS/os.c-GNU
@@ -3,6 +3,7 @@
 *************************************************/
 
 /* Copyright (c) The Exim Maintainers 2020 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* GNU-specific code. This is concatenated onto the generic src/os.c file.
diff --git a/src/OS/os.c-Linux b/src/OS/os.c-Linux
index 59d81f8ad..34dd7c190 100644
--- a/src/OS/os.c-Linux
+++ b/src/OS/os.c-Linux
@@ -3,6 +3,7 @@
 *************************************************/
 
 /* Copyright (c) University of Cambridge 1997 - 2018 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Linux-specific code. This is concatenated onto the generic
diff --git a/src/OS/os.c-SunOS5 b/src/OS/os.c-SunOS5
index 162486958..ec4bb8d42 100644
--- a/src/OS/os.c-SunOS5
+++ b/src/OS/os.c-SunOS5
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 2016 */
 /* Copyright (c) Jeremy Harris 2016 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Solaris-specific code. This is concatenated onto the generic
diff --git a/src/OS/os.h-FreeBSD b/src/OS/os.h-FreeBSD
index 06e217a44..6756d42f6 100644
--- a/src/OS/os.h-FreeBSD
+++ b/src/OS/os.h-FreeBSD
@@ -1,6 +1,7 @@
 /* Exim: OS-specific C header file for FreeBSD */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 
diff --git a/src/OS/os.h-GNU b/src/OS/os.h-GNU
index 4b222600e..4fe55fa5e 100644
--- a/src/OS/os.h-GNU
+++ b/src/OS/os.h-GNU
@@ -1,5 +1,6 @@
 /* Exim: OS-specific C header file for GNU/Hurd */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include <features.h>
 
diff --git a/src/OS/os.h-Linux b/src/OS/os.h-Linux
index e6a8d288e..25a12862b 100644
--- a/src/OS/os.h-Linux
+++ b/src/OS/os.h-Linux
@@ -1,6 +1,7 @@
 /* Exim: OS-specific C header file for Linux */
 /* Copyright (c) University of Cambridge 1995 - 2020 */
 /* Copyright (c) The Exim Maintainers 2021 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 
diff --git a/src/OS/os.h-OpenBSD b/src/OS/os.h-OpenBSD
index c690b6e4a..19db7189a 100644
--- a/src/OS/os.h-OpenBSD
+++ b/src/OS/os.h-OpenBSD
@@ -1,6 +1,7 @@
 /* Exim: OS-specific C header file for OpenBSD */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 
diff --git a/src/OS/os.h-SunOS5 b/src/OS/os.h-SunOS5
index 71253213c..b3f55bf83 100644
--- a/src/OS/os.h-SunOS5
+++ b/src/OS/os.h-SunOS5
@@ -1,5 +1,6 @@
 /* Exim: OS-specific C header file for SunOS5 aka Solaris */
 /* Copyright (c) The Exim Maintainers 2021 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define CRYPT_H
 #define HAVE_MMAP
diff --git a/src/OS/unsupported/Makefile-AIX b/src/OS/unsupported/Makefile-AIX
index fc32aa286..f407cbcaf 100644
--- a/src/OS/unsupported/Makefile-AIX
+++ b/src/OS/unsupported/Makefile-AIX
@@ -1,6 +1,7 @@
 # Exim: OS-specific make file for AIX
 # Written by Nick Waterman (nick@cimio.co.uk)
 # Modified by PH following a message from Mike Meredith
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # Note that the output of uname -m is probably not what Philip expected,
 # so you might end up with more build-AIX-random_number directories than
diff --git a/src/OS/unsupported/Makefile-BSDI b/src/OS/unsupported/Makefile-BSDI
index d56aa9b57..6ea176319 100644
--- a/src/OS/unsupported/Makefile-BSDI
+++ b/src/OS/unsupported/Makefile-BSDI
@@ -1,5 +1,6 @@
 # Exim: OS-specific make file for BSDI aka BSD/OS. Its antique link editor
 # cannot handle the TextPop overriding.
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 CFLAGS=-O
 CHOWN_COMMAND=/usr/sbin/chown
diff --git a/src/OS/unsupported/Makefile-CYGWIN b/src/OS/unsupported/Makefile-CYGWIN
index 5e608fe9e..bc4545b47 100644
--- a/src/OS/unsupported/Makefile-CYGWIN
+++ b/src/OS/unsupported/Makefile-CYGWIN
@@ -1,4 +1,5 @@
 # OS-specific file for Cygwin.
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # This file provided by Pierre A. Humblet <Pierre.Humblet@ieee.org>
 
diff --git a/src/OS/unsupported/Makefile-DGUX b/src/OS/unsupported/Makefile-DGUX
index 667c63f49..1258ba4e4 100644
--- a/src/OS/unsupported/Makefile-DGUX
+++ b/src/OS/unsupported/Makefile-DGUX
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for DGUX
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Written by Ken Bailey (K.Bailey@rbgkew.org.uk) Feb 1998
 # on dgux R4.11MU04 generic AViiON mc88100
diff --git a/src/OS/unsupported/Makefile-Darwin b/src/OS/unsupported/Makefile-Darwin
index 517bbc493..bdd9d110d 100644
--- a/src/OS/unsupported/Makefile-Darwin
+++ b/src/OS/unsupported/Makefile-Darwin
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for Darwin (Mac OS X).
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 CC=cc
 
diff --git a/src/OS/unsupported/Makefile-DragonFly b/src/OS/unsupported/Makefile-DragonFly
index c49c59ffc..9d845f13f 100644
--- a/src/OS/unsupported/Makefile-DragonFly
+++ b/src/OS/unsupported/Makefile-DragonFly
@@ -1,4 +1,6 @@
 # Exim: OS-specific make file for DragonFly
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
 # There's no setting of CFLAGS here, to allow the system default
 # for "make" to be the default.
 
diff --git a/src/OS/unsupported/Makefile-GNUkFreeBSD b/src/OS/unsupported/Makefile-GNUkFreeBSD
index 801928143..b5ed325d1 100644
--- a/src/OS/unsupported/Makefile-GNUkFreeBSD
+++ b/src/OS/unsupported/Makefile-GNUkFreeBSD
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for GNU and variants.
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 HAVE_ICONV=yes
 
diff --git a/src/OS/unsupported/Makefile-GNUkNetBSD b/src/OS/unsupported/Makefile-GNUkNetBSD
index 801928143..b5ed325d1 100644
--- a/src/OS/unsupported/Makefile-GNUkNetBSD
+++ b/src/OS/unsupported/Makefile-GNUkNetBSD
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for GNU and variants.
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 HAVE_ICONV=yes
 
diff --git a/src/OS/unsupported/Makefile-HI-OSF b/src/OS/unsupported/Makefile-HI-OSF
index da3d487a4..66aac36b9 100644
--- a/src/OS/unsupported/Makefile-HI-OSF
+++ b/src/OS/unsupported/Makefile-HI-OSF
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for HI-OSF/1-MJ and HI-UX/MPP
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 CC=cc
 CFLAGS=-O
diff --git a/src/OS/unsupported/Makefile-HI-UX b/src/OS/unsupported/Makefile-HI-UX
index 870ee8460..c1f4fe51a 100644
--- a/src/OS/unsupported/Makefile-HI-UX
+++ b/src/OS/unsupported/Makefile-HI-UX
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for HI-UX
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 CC=cc -Aa -D_HIUX_SOURCE
 HAVE_SETRESUID=YES
diff --git a/src/OS/unsupported/Makefile-HP-UX b/src/OS/unsupported/Makefile-HP-UX
index ea35144bb..7e6cd9dba 100644
--- a/src/OS/unsupported/Makefile-HP-UX
+++ b/src/OS/unsupported/Makefile-HP-UX
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for HP-UX later than 9
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # HP ANSI C compiler
 #CC=cc
diff --git a/src/OS/unsupported/Makefile-HP-UX-9 b/src/OS/unsupported/Makefile-HP-UX-9
index 153000944..ad28d4fdb 100644
--- a/src/OS/unsupported/Makefile-HP-UX-9
+++ b/src/OS/unsupported/Makefile-HP-UX-9
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for HP-UX 9
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 CFLAGS=-O
 BASENAME_COMMAND=/bin/basename
diff --git a/src/OS/unsupported/Makefile-IRIX b/src/OS/unsupported/Makefile-IRIX
index 7b9578339..a9925956b 100644
--- a/src/OS/unsupported/Makefile-IRIX
+++ b/src/OS/unsupported/Makefile-IRIX
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for IRIX
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 HAVE_ICONV=yes
 BASENAME_COMMAND=/sbin/basename
diff --git a/src/OS/unsupported/Makefile-IRIX6 b/src/OS/unsupported/Makefile-IRIX6
index be0113846..6140649d1 100644
--- a/src/OS/unsupported/Makefile-IRIX6
+++ b/src/OS/unsupported/Makefile-IRIX6
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for IRIX6 on 64-bit systems
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 HAVE_ICONV=yes
 HOSTNAME_COMMAND=/usr/bsd/hostname
diff --git a/src/OS/unsupported/Makefile-IRIX632 b/src/OS/unsupported/Makefile-IRIX632
index b567fc64e..2b19463ff 100644
--- a/src/OS/unsupported/Makefile-IRIX632
+++ b/src/OS/unsupported/Makefile-IRIX632
@@ -1,4 +1,6 @@
 # Exim: OS-specific make file for IRIX 6 on 32-bit systems.
+# SPDX-License-Identifier: GPL-2.0-or-later
+
 # There seems to be some variation. The commented settings show
 # some alternatives.
 
diff --git a/src/OS/unsupported/Makefile-IRIX65 b/src/OS/unsupported/Makefile-IRIX65
index 50e77450a..4ad77a66f 100644
--- a/src/OS/unsupported/Makefile-IRIX65
+++ b/src/OS/unsupported/Makefile-IRIX65
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for IRIX 6.5
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 HAVE_ICONV=yes
 HOSTNAME_COMMAND=/usr/bsd/hostname
diff --git a/src/OS/unsupported/Makefile-NetBSD b/src/OS/unsupported/Makefile-NetBSD
index 35d03a24a..6792da216 100644
--- a/src/OS/unsupported/Makefile-NetBSD
+++ b/src/OS/unsupported/Makefile-NetBSD
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for NetBSD (ELF object format)
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 CHOWN_COMMAND=/usr/sbin/chown
 CHMOD_COMMAND=/bin/chmod
diff --git a/src/OS/unsupported/Makefile-NetBSD-a.out b/src/OS/unsupported/Makefile-NetBSD-a.out
index e210efdda..5bee773ae 100644
--- a/src/OS/unsupported/Makefile-NetBSD-a.out
+++ b/src/OS/unsupported/Makefile-NetBSD-a.out
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for NetBSD (a.out/COFF object format)
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 CHOWN_COMMAND=/usr/sbin/chown
 CHMOD_COMMAND=/bin/chmod
diff --git a/src/OS/unsupported/Makefile-OSF1 b/src/OS/unsupported/Makefile-OSF1
index 811ca07a9..73814cec6 100644
--- a/src/OS/unsupported/Makefile-OSF1
+++ b/src/OS/unsupported/Makefile-OSF1
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for OSF1
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 CFLAGS=-O
 LIBS=-liconv -lm
diff --git a/src/OS/unsupported/Makefile-OpenUNIX b/src/OS/unsupported/Makefile-OpenUNIX
index e4d726158..dbab2b557 100644
--- a/src/OS/unsupported/Makefile-OpenUNIX
+++ b/src/OS/unsupported/Makefile-OpenUNIX
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for OpenUNIX
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 CC=/usr/bin/cc
 CFLAGS=-O -I/usr/local/include
diff --git a/src/OS/unsupported/Makefile-QNX b/src/OS/unsupported/Makefile-QNX
index 3cf81c41b..7d957b27c 100644
--- a/src/OS/unsupported/Makefile-QNX
+++ b/src/OS/unsupported/Makefile-QNX
@@ -1,4 +1,5 @@
 # Exim: OS-specific makefile for QNX
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 BASENAME_COMMAND=/bin/basename
 MAKE_SHELL=/usr/bin/bash
diff --git a/src/OS/unsupported/Makefile-SCO b/src/OS/unsupported/Makefile-SCO
index baa61d80b..c936ec536 100644
--- a/src/OS/unsupported/Makefile-SCO
+++ b/src/OS/unsupported/Makefile-SCO
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for SCO
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # It was reported that some versions of gcc (e.g. 2.8.1) require this to be
 # CFLAGS=-melf
diff --git a/src/OS/unsupported/Makefile-SCO_SV b/src/OS/unsupported/Makefile-SCO_SV
index 249b81a0f..9baf1c470 100644
--- a/src/OS/unsupported/Makefile-SCO_SV
+++ b/src/OS/unsupported/Makefile-SCO_SV
@@ -1,6 +1,7 @@
 # Exim: OS-specific make file for SCO_SV release 5 (tested on 5.0.5 & 5.0.5)
 #       (see the UNIX_SV files for SCO 4.2)
 # Supplied by: Tony Earnshaw <tonye@ilion.nl>
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # Note that 'gcc -melf -m486' applies to gcc 2.7.2 and higher;
 # 2.7.1 and SCO's SDK need '-belf'.
diff --git a/src/OS/unsupported/Makefile-SunOS4 b/src/OS/unsupported/Makefile-SunOS4
index c876998e5..0dec44b65 100644
--- a/src/OS/unsupported/Makefile-SunOS4
+++ b/src/OS/unsupported/Makefile-SunOS4
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for SunOS4
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 CFLAGS=-O
 
diff --git a/src/OS/unsupported/Makefile-SunOS5-hal b/src/OS/unsupported/Makefile-SunOS5-hal
index 05ea893af..05759f369 100644
--- a/src/OS/unsupported/Makefile-SunOS5-hal
+++ b/src/OS/unsupported/Makefile-SunOS5-hal
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for SunOS5 on a HAL
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # Note: The HAL runs a standard SunOS5 except that it has a 64 bit C
 # compiler called hcc.  To make things work pass the -KV7 flag to force
diff --git a/src/OS/unsupported/Makefile-ULTRIX b/src/OS/unsupported/Makefile-ULTRIX
index 9e912b3cb..00a95b950 100644
--- a/src/OS/unsupported/Makefile-ULTRIX
+++ b/src/OS/unsupported/Makefile-ULTRIX
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for Ultrix
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 MAKE_SHELL=/usr/bin/sh5
 
diff --git a/src/OS/unsupported/Makefile-UNIX_SV b/src/OS/unsupported/Makefile-UNIX_SV
index bfcfae1c0..5ea8567a5 100644
--- a/src/OS/unsupported/Makefile-UNIX_SV
+++ b/src/OS/unsupported/Makefile-UNIX_SV
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for SCO SVR4.2MP (and maybe Unixware)
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 #  *** Note that for SCO 5 the configuration file is called SCO_SV,
 #  *** and that Unixware7 has its own configuration. This is an old
diff --git a/src/OS/unsupported/Makefile-USG b/src/OS/unsupported/Makefile-USG
index 753a2d7e8..69a31bc15 100644
--- a/src/OS/unsupported/Makefile-USG
+++ b/src/OS/unsupported/Makefile-USG
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for Unixware 2.x
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Note that Unixware does not include db/dbm/ndbm with their standard compiler
 # (it is available with /usr/ucb/cc, but that has bugs of its own). You
diff --git a/src/OS/unsupported/Makefile-Unixware7 b/src/OS/unsupported/Makefile-Unixware7
index 88a883801..0dc1afb1a 100644
--- a/src/OS/unsupported/Makefile-Unixware7
+++ b/src/OS/unsupported/Makefile-Unixware7
@@ -1,5 +1,6 @@
 # Exim: OS-specific make file for Unixware7
 # Based on information from James FitzGibbon <james@ehlo.com>
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # If you want to use libbind, you need to
 #     add -I/usr/local/bind/include to CFLAGS
diff --git a/src/OS/unsupported/Makefile-mips b/src/OS/unsupported/Makefile-mips
index ff3313937..d48adb411 100644
--- a/src/OS/unsupported/Makefile-mips
+++ b/src/OS/unsupported/Makefile-mips
@@ -1,4 +1,5 @@
 # Exim: OS-specific make file for RiscOS4bsd
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 HOSTNAME_COMMAND=/usr/ucb/hostname
 EXIT_FAILURE=1
diff --git a/src/OS/unsupported/os.c-BSDI b/src/OS/unsupported/os.c-BSDI
index 03a7a1cef..42160046b 100644
--- a/src/OS/unsupported/os.c-BSDI
+++ b/src/OS/unsupported/os.c-BSDI
@@ -3,6 +3,7 @@
 *************************************************/
 
 /* Copyright (c) 2016 Heiko Schlittermann <hs@schlittermann.de> */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* BSDI-specific code. This is concatenated onto the generic
diff --git a/src/OS/unsupported/os.c-HI-OSF b/src/OS/unsupported/os.c-HI-OSF
index 5e3d336f2..56db57fd5 100644
--- a/src/OS/unsupported/os.c-HI-OSF
+++ b/src/OS/unsupported/os.c-HI-OSF
@@ -3,6 +3,7 @@
 *************************************************/
 
 /* Copyright (c) University of Cambridge 2001 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* HI-OSF-specific code. This is concatenated onto the generic
diff --git a/src/OS/unsupported/os.c-HP-UX b/src/OS/unsupported/os.c-HP-UX
index fdd8708a2..1178b782f 100644
--- a/src/OS/unsupported/os.c-HP-UX
+++ b/src/OS/unsupported/os.c-HP-UX
@@ -4,6 +4,7 @@
 
 /* Copyright (c) University of Cambridge 2016 */
 /* Copyright (c) Jeremy Harris 2016 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* HP-UX-specific code. This is concatenated onto the generic
diff --git a/src/OS/unsupported/os.c-IRIX b/src/OS/unsupported/os.c-IRIX
index 19172389b..1019901dc 100644
--- a/src/OS/unsupported/os.c-IRIX
+++ b/src/OS/unsupported/os.c-IRIX
@@ -4,6 +4,7 @@
 
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 2001 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Irix-specific code. This is concatenated onto the generic src/os.c file.
diff --git a/src/OS/unsupported/os.c-IRIX6 b/src/OS/unsupported/os.c-IRIX6
index 19172389b..1019901dc 100644
--- a/src/OS/unsupported/os.c-IRIX6
+++ b/src/OS/unsupported/os.c-IRIX6
@@ -4,6 +4,7 @@
 
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 2001 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Irix-specific code. This is concatenated onto the generic src/os.c file.
diff --git a/src/OS/unsupported/os.c-IRIX632 b/src/OS/unsupported/os.c-IRIX632
index 19172389b..1019901dc 100644
--- a/src/OS/unsupported/os.c-IRIX632
+++ b/src/OS/unsupported/os.c-IRIX632
@@ -4,6 +4,7 @@
 
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 2001 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Irix-specific code. This is concatenated onto the generic src/os.c file.
diff --git a/src/OS/unsupported/os.c-IRIX65 b/src/OS/unsupported/os.c-IRIX65
index 19172389b..1019901dc 100644
--- a/src/OS/unsupported/os.c-IRIX65
+++ b/src/OS/unsupported/os.c-IRIX65
@@ -4,6 +4,7 @@
 
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 2001 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Irix-specific code. This is concatenated onto the generic src/os.c file.
diff --git a/src/OS/unsupported/os.c-OSF1 b/src/OS/unsupported/os.c-OSF1
index ad91b63d1..4dc700f79 100644
--- a/src/OS/unsupported/os.c-OSF1
+++ b/src/OS/unsupported/os.c-OSF1
@@ -3,6 +3,7 @@
 *************************************************/
 
 /* Copyright (c) University of Cambridge 2001 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* OSF1-specific code. This is concatenated onto the generic src/os.c file.
diff --git a/src/OS/unsupported/os.c-cygwin b/src/OS/unsupported/os.c-cygwin
index 5ca05a8db..56085b857 100644
--- a/src/OS/unsupported/os.c-cygwin
+++ b/src/OS/unsupported/os.c-cygwin
@@ -1,6 +1,7 @@
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Cygwin-specific code. December 2002. Updated Jan 2015.
    This is prefixed to the src/os.c file.
diff --git a/src/OS/unsupported/os.h-AIX b/src/OS/unsupported/os.h-AIX
index 5cd4501a4..d70b907f5 100644
--- a/src/OS/unsupported/os.h-AIX
+++ b/src/OS/unsupported/os.h-AIX
@@ -1,4 +1,6 @@
 /* Exim: OS-specific C header file for AIX */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
 /* Written by Nick Waterman <nick@cimio.co.uk> */
 /* Modified by Philip Hazel with data from
    Niels Provos <provos@wserver.physnet.uni-hamburg.de>
diff --git a/src/OS/unsupported/os.h-BSDI b/src/OS/unsupported/os.h-BSDI
index a1705ec95..0a5dd93e6 100644
--- a/src/OS/unsupported/os.h-BSDI
+++ b/src/OS/unsupported/os.h-BSDI
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for BSDI */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define HAVE_BSD_GETLOADAVG
 #define HAVE_SETCLASSRESOURCES
diff --git a/src/OS/unsupported/os.h-DGUX b/src/OS/unsupported/os.h-DGUX
index 9040f0e72..a4918b649 100644
--- a/src/OS/unsupported/os.h-DGUX
+++ b/src/OS/unsupported/os.h-DGUX
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for DGUX */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Written by Ken Bailey (K.Bailey@rbgkew.org.uk) Feb 1998 */
 /* on dgux R4.11MU04 generic AViiON mc88100                */
diff --git a/src/OS/unsupported/os.h-Darwin b/src/OS/unsupported/os.h-Darwin
index a85e92f05..8cec8a90f 100644
--- a/src/OS/unsupported/os.h-Darwin
+++ b/src/OS/unsupported/os.h-Darwin
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for Darwin (Mac OS X) */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* #define CRYPT_H */  /* Apparently this isn't needed */
 
diff --git a/src/OS/unsupported/os.h-DragonFly b/src/OS/unsupported/os.h-DragonFly
index 4c2f1d508..122957f8d 100644
--- a/src/OS/unsupported/os.h-DragonFly
+++ b/src/OS/unsupported/os.h-DragonFly
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for DragonFly */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define HAVE_BSD_GETLOADAVG
 #define HAVE_MMAP
diff --git a/src/OS/unsupported/os.h-GNUkFreeBSD b/src/OS/unsupported/os.h-GNUkFreeBSD
index ab3503131..9326951c2 100644
--- a/src/OS/unsupported/os.h-GNUkFreeBSD
+++ b/src/OS/unsupported/os.h-GNUkFreeBSD
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for GNU/kFreeBSD */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define CRYPT_H
 #define GLIBC_IP_OPTIONS
diff --git a/src/OS/unsupported/os.h-GNUkNetBSD b/src/OS/unsupported/os.h-GNUkNetBSD
index bc3bc259d..f255b383d 100644
--- a/src/OS/unsupported/os.h-GNUkNetBSD
+++ b/src/OS/unsupported/os.h-GNUkNetBSD
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for GNU/kNetBSD */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define CRYPT_H
 #define GLIBC_IP_OPTIONS
diff --git a/src/OS/unsupported/os.h-HI-OSF b/src/OS/unsupported/os.h-HI-OSF
index 0f50fb660..9dc59cbcd 100644
--- a/src/OS/unsupported/os.h-HI-OSF
+++ b/src/OS/unsupported/os.h-HI-OSF
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for HI-OSF/1-MJ and HI-UX/MPP */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define HAVE_SYS_MOUNT_H
 
diff --git a/src/OS/unsupported/os.h-HI-UX b/src/OS/unsupported/os.h-HI-UX
index f3df9638c..8a83ce0b0 100644
--- a/src/OS/unsupported/os.h-HI-UX
+++ b/src/OS/unsupported/os.h-HI-UX
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for HI-UX */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define LOAD_AVG_NEEDS_ROOT
 #define HAVE_DEV_KMEM
diff --git a/src/OS/unsupported/os.h-HP-UX b/src/OS/unsupported/os.h-HP-UX
index 4998734f6..e74c254f6 100644
--- a/src/OS/unsupported/os.h-HP-UX
+++ b/src/OS/unsupported/os.h-HP-UX
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for HP-UX versions greater than 9 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define EXIM_SOCKLEN_T size_t
 
diff --git a/src/OS/unsupported/os.h-HP-UX-9 b/src/OS/unsupported/os.h-HP-UX-9
index 5a260d607..cb967a001 100644
--- a/src/OS/unsupported/os.h-HP-UX-9
+++ b/src/OS/unsupported/os.h-HP-UX-9
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for HP-UX version 9 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define LOAD_AVG_NEEDS_ROOT
 #define HAVE_DEV_KMEM
diff --git a/src/OS/unsupported/os.h-IRIX b/src/OS/unsupported/os.h-IRIX
index 1d4bf46ba..ceff6ea05 100644
--- a/src/OS/unsupported/os.h-IRIX
+++ b/src/OS/unsupported/os.h-IRIX
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for IRIX */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define DN_EXPAND_ARG4_TYPE  u_char *
 
diff --git a/src/OS/unsupported/os.h-IRIX6 b/src/OS/unsupported/os.h-IRIX6
index bf3076713..59bf6c46e 100644
--- a/src/OS/unsupported/os.h-IRIX6
+++ b/src/OS/unsupported/os.h-IRIX6
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for IRIX */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define CRYPT_H
 #define LOAD_AVG_NEEDS_ROOT
diff --git a/src/OS/unsupported/os.h-IRIX632 b/src/OS/unsupported/os.h-IRIX632
index 90f1c582c..8c171147b 100644
--- a/src/OS/unsupported/os.h-IRIX632
+++ b/src/OS/unsupported/os.h-IRIX632
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for IRIX */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define CRYPT_H
 #define DN_EXPAND_ARG4_TYPE  u_char *
diff --git a/src/OS/unsupported/os.h-IRIX65 b/src/OS/unsupported/os.h-IRIX65
index 4b248fe36..381acbd57 100644
--- a/src/OS/unsupported/os.h-IRIX65
+++ b/src/OS/unsupported/os.h-IRIX65
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for IRIX 6.5 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define CRYPT_H
 #define LOAD_AVG_NEEDS_ROOT
diff --git a/src/OS/unsupported/os.h-NetBSD b/src/OS/unsupported/os.h-NetBSD
index 0b9fc03f0..a8e32107a 100644
--- a/src/OS/unsupported/os.h-NetBSD
+++ b/src/OS/unsupported/os.h-NetBSD
@@ -1,5 +1,6 @@
 /* Exim: OS-specific C header file for NetBSD */
 /* Copyright (c) The Exim Maintainers 2021 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define HAVE_BSD_GETLOADAVG
 #define HAVE_GETIFADDRS
diff --git a/src/OS/unsupported/os.h-NetBSD-a.out b/src/OS/unsupported/os.h-NetBSD-a.out
index 29a8feeff..59eb79241 100644
--- a/src/OS/unsupported/os.h-NetBSD-a.out
+++ b/src/OS/unsupported/os.h-NetBSD-a.out
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for NetBSD (a.out binary format) */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../OS/os.h-NetBSD"     /* Same as for ELF format */
 
diff --git a/src/OS/unsupported/os.h-OSF1 b/src/OS/unsupported/os.h-OSF1
index 6b5fa4973..4c985e8f2 100644
--- a/src/OS/unsupported/os.h-OSF1
+++ b/src/OS/unsupported/os.h-OSF1
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for OSF1 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define HAVE_SYS_MOUNT_H
 #define HAVE_GETIPNODEBYNAME    1
diff --git a/src/OS/unsupported/os.h-OpenUNIX b/src/OS/unsupported/os.h-OpenUNIX
index 67d1063b0..e8fae863a 100644
--- a/src/OS/unsupported/os.h-OpenUNIX
+++ b/src/OS/unsupported/os.h-OpenUNIX
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for OpenUNIX */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define NO_SYSEXITS
 
diff --git a/src/OS/unsupported/os.h-QNX b/src/OS/unsupported/os.h-QNX
index 798f799ed..e5b04826c 100644
--- a/src/OS/unsupported/os.h-QNX
+++ b/src/OS/unsupported/os.h-QNX
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for QNX */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /* Modified for QNX 6.2.0 with diffs from Samuli Tuomola. */
 
 #include <sys/select.h>
diff --git a/src/OS/unsupported/os.h-SCO b/src/OS/unsupported/os.h-SCO
index e5e915ed0..99585ae46 100644
--- a/src/OS/unsupported/os.h-SCO
+++ b/src/OS/unsupported/os.h-SCO
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for SCO */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define DN_EXPAND_ARG4_TYPE    u_char *
 
diff --git a/src/OS/unsupported/os.h-SCO_SV b/src/OS/unsupported/os.h-SCO_SV
index 0ca29f74a..724753e29 100644
--- a/src/OS/unsupported/os.h-SCO_SV
+++ b/src/OS/unsupported/os.h-SCO_SV
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for SCO_SV */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define LOAD_AVG_NEEDS_ROOT
 #define HAVE_DEV_KMEM
diff --git a/src/OS/unsupported/os.h-SunOS4 b/src/OS/unsupported/os.h-SunOS4
index 65556204c..d12acc05c 100644
--- a/src/OS/unsupported/os.h-SunOS4
+++ b/src/OS/unsupported/os.h-SunOS4
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for SunOS4 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define LOAD_AVG_NEEDS_ROOT
 #define HAVE_DEV_KMEM
diff --git a/src/OS/unsupported/os.h-SunOS5-hal b/src/OS/unsupported/os.h-SunOS5-hal
index cd9e877a9..1a1e43f95 100644
--- a/src/OS/unsupported/os.h-SunOS5-hal
+++ b/src/OS/unsupported/os.h-SunOS5-hal
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for SunOS5 on HAL */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define HAVE_MMAP
 
diff --git a/src/OS/unsupported/os.h-ULTRIX b/src/OS/unsupported/os.h-ULTRIX
index 08db5aec8..83b0fc4aa 100644
--- a/src/OS/unsupported/os.h-ULTRIX
+++ b/src/OS/unsupported/os.h-ULTRIX
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for Ultrix */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Well, it *does* have statfs(), but its structure is called something
 different, all the members have different names, and the function returns
diff --git a/src/OS/unsupported/os.h-UNIX_SV b/src/OS/unsupported/os.h-UNIX_SV
index 4943a07de..275475322 100644
--- a/src/OS/unsupported/os.h-UNIX_SV
+++ b/src/OS/unsupported/os.h-UNIX_SV
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for SCO SVR4.2 (and maybe Unixware) */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /**
 *** Note that for SCO 5 the configuration file is called SCO_SV,
diff --git a/src/OS/unsupported/os.h-USG b/src/OS/unsupported/os.h-USG
index e76922067..597c52f9d 100644
--- a/src/OS/unsupported/os.h-USG
+++ b/src/OS/unsupported/os.h-USG
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for Unixware 2.x */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define NO_SYSEXITS
 
diff --git a/src/OS/unsupported/os.h-Unixware7 b/src/OS/unsupported/os.h-Unixware7
index 4d3ed42f2..e8f8c3210 100644
--- a/src/OS/unsupported/os.h-Unixware7
+++ b/src/OS/unsupported/os.h-Unixware7
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for Unixware 7 */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define NO_SYSEXITS
 
diff --git a/src/OS/unsupported/os.h-cygwin b/src/OS/unsupported/os.h-cygwin
index 6ef59e0cc..7f81c074f 100644
--- a/src/OS/unsupported/os.h-cygwin
+++ b/src/OS/unsupported/os.h-cygwin
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for Cygwin */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This code was supplied by Pierre A. Humblet <Pierre.Humblet@ieee.org>
    December 2002. Updated Jan 2015. */
diff --git a/src/OS/unsupported/os.h-mips b/src/OS/unsupported/os.h-mips
index 325e3a134..39e0019cb 100644
--- a/src/OS/unsupported/os.h-mips
+++ b/src/OS/unsupported/os.h-mips
@@ -1,4 +1,5 @@
 /* Exim: OS-specific C header file for RiscOS4bsd */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define LOAD_AVG_NEEDS_ROOT
 #define HAVE_DEV_KMEM
diff --git a/src/README b/src/README
index d9379f7f8..e335cc22e 100644
--- a/src/README
+++ b/src/README
@@ -2,6 +2,7 @@ THE EXIM MAIL TRANSFER AGENT VERSION 4
 --------------------------------------
 
 Copyright (c) 1995 - 2018 University of Cambridge.
+SPDX-License-Identifier: GPL-2.0-or-later
 See the file NOTICE for conditions of use and distribution.
 
 There is a book about Exim by Philip Hazel called "The Exim SMTP Mail Server",
diff --git a/src/exim_monitor/EDITME b/src/exim_monitor/EDITME
index a70b7cb61..1b6320c8f 100644
--- a/src/exim_monitor/EDITME
+++ b/src/exim_monitor/EDITME
@@ -2,6 +2,9 @@
 #                The Exim Monitor                #
 ##################################################
 
+# Copyright (c) The Exim Maintainers 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
+
 # This is the template for the Exim monitor's main build-time configuration
 # file. It contains settings that are independent of any operating system. It
 # should be edited and then saved to a file called Local/eximon.conf before
diff --git a/src/exim_monitor/em_StripChart.c b/src/exim_monitor/em_StripChart.c
index 3eb98e4fb..a974790d3 100644
--- a/src/exim_monitor/em_StripChart.c
+++ b/src/exim_monitor/em_StripChart.c
@@ -1,4 +1,4 @@
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /***********************************************************
 Copyright 1987, 1988 by Digital Equipment Corporation, Maynard, Massachusetts,
 and the Massachusetts Institute of Technology, Cambridge, Massachusetts.
diff --git a/src/exim_monitor/em_TextPop.c b/src/exim_monitor/em_TextPop.c
index 0e87cb580..0360f852b 100644
--- a/src/exim_monitor/em_TextPop.c
+++ b/src/exim_monitor/em_TextPop.c
@@ -2,7 +2,7 @@
 Copyright (c) The Exim Maintainers 2022
 Copyright 1989 by the Massachusetts Institute of Technology,
 Cambridge, Massachusetts.
-SPDX-License-Identifier: GPL-2.0-only
+SPDX-License-Identifier: GPL-2.0-or-later
 
                         All Rights Reserved
 
diff --git a/src/exim_monitor/em_globals.c b/src/exim_monitor/em_globals.c
index 2943065b4..cf9b1075e 100644
--- a/src/exim_monitor/em_globals.c
+++ b/src/exim_monitor/em_globals.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "em_hdr.h"
diff --git a/src/exim_monitor/em_hdr.h b/src/exim_monitor/em_hdr.h
index 76970c997..61f390d2c 100644
--- a/src/exim_monitor/em_hdr.h
+++ b/src/exim_monitor/em_hdr.h
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* This is the general header file for all the modules that comprise
diff --git a/src/exim_monitor/em_init.c b/src/exim_monitor/em_init.c
index e1f53fbba..b9289715d 100644
--- a/src/exim_monitor/em_init.c
+++ b/src/exim_monitor/em_init.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This module contains code to initialize things from the
 environment and the arguments. */
diff --git a/src/exim_monitor/em_log.c b/src/exim_monitor/em_log.c
index d625056c5..55925d786 100644
--- a/src/exim_monitor/em_log.c
+++ b/src/exim_monitor/em_log.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainters 2021 - 2022 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This module contains code for scanning the main log,
 extracting information from it, and displaying a "tail". */
diff --git a/src/exim_monitor/em_main.c b/src/exim_monitor/em_main.c
index 919cde632..50b8cd8bd 100644
--- a/src/exim_monitor/em_main.c
+++ b/src/exim_monitor/em_main.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "em_hdr.h"
diff --git a/src/exim_monitor/em_menu.c b/src/exim_monitor/em_menu.c
index afcd31540..e4db84915 100644
--- a/src/exim_monitor/em_menu.c
+++ b/src/exim_monitor/em_menu.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "em_hdr.h"
diff --git a/src/exim_monitor/em_queue.c b/src/exim_monitor/em_queue.c
index d9cfad38a..5eb44648a 100644
--- a/src/exim_monitor/em_queue.c
+++ b/src/exim_monitor/em_queue.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "em_hdr.h"
diff --git a/src/exim_monitor/em_strip.c b/src/exim_monitor/em_strip.c
index cfafe9257..21e5739db 100644
--- a/src/exim_monitor/em_strip.c
+++ b/src/exim_monitor/em_strip.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "em_hdr.h"
diff --git a/src/exim_monitor/em_text.c b/src/exim_monitor/em_text.c
index 1077353e0..4d03570fd 100644
--- a/src/exim_monitor/em_text.c
+++ b/src/exim_monitor/em_text.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "em_hdr.h"
diff --git a/src/exim_monitor/em_version.c b/src/exim_monitor/em_version.c
index c5d4d62d8..4c562925c 100644
--- a/src/exim_monitor/em_version.c
+++ b/src/exim_monitor/em_version.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define EM_VERSION_C
 
diff --git a/src/exim_monitor/em_xs.c b/src/exim_monitor/em_xs.c
index dd19c7f43..ac36b8f58 100644
--- a/src/exim_monitor/em_xs.c
+++ b/src/exim_monitor/em_xs.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge, 1995 - 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This file contains a number of subroutines that are in effect
 just alternative packaging for calls to various X functions that
diff --git a/src/scripts/Configure b/src/scripts/Configure
index 6c340ee25..4c68b5940 100755
--- a/src/scripts/Configure
+++ b/src/scripts/Configure
@@ -2,6 +2,9 @@
 
 # A script to be called to run all the other configuring scripts manually.
 
+# Copyright (c) The Exim Maintainters 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
+
 scripts/Configure-Makefile
 scripts/Configure-os.h
 scripts/Configure-os.c
diff --git a/src/scripts/Configure-Makefile b/src/scripts/Configure-Makefile
index ed77b6a8f..dc5015f6f 100755
--- a/src/scripts/Configure-Makefile
+++ b/src/scripts/Configure-Makefile
@@ -1,13 +1,15 @@
 #! /bin/sh
-LC_ALL=C
-export LC_ALL
-
 # Shell script to build Makefile in a build directory. It must be called
 # from inside the directory. It does its own checking of when to rebuild; it
 # just got too horrendous to get it right in "make", because of the optionally
 # existing configuration files.
 #
 # Copyright (c) The Exim Maintainers 1995 - 2021
+# SPDX-License-Identifier: GPL-2.0-or-later
+
+
+LC_ALL=C
+export LC_ALL
 
 
 # First off, get the OS type, and check that there is a make file for it.
diff --git a/src/scripts/Configure-config.h b/src/scripts/Configure-config.h
index c750f64f1..6744755f7 100755
--- a/src/scripts/Configure-config.h
+++ b/src/scripts/Configure-config.h
@@ -1,5 +1,6 @@
 #! /bin/sh
-# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (c) The Exim Maintainters 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # Build the config.h file, using the buildconfig program, first ensuring that
 # it exists.
diff --git a/src/scripts/Configure-eximon b/src/scripts/Configure-eximon
index 0aca3b338..00076154d 100755
--- a/src/scripts/Configure-eximon
+++ b/src/scripts/Configure-eximon
@@ -1,5 +1,8 @@
 #! /bin/sh
 
+# Copyright (c) The Exim Maintainters 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
+
 # Shell script to build the configurable part of the Exim monitor's start-up
 # script. This is built from various configuration files. The final part is
 # added in the Makefile, using various macros that are available at that stage.
diff --git a/src/scripts/Configure-os.c b/src/scripts/Configure-os.c
index ab40ad183..7e9f14421 100755
--- a/src/scripts/Configure-os.c
+++ b/src/scripts/Configure-os.c
@@ -1,5 +1,7 @@
 #! /bin/sh
-# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (c) The Exim Maintainters 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
+
 
 # Shell script to build os.c. There doesn't have to be an OS-specific os.c
 # file, but if there is, it gets copied at the start of os.c. The basic src
diff --git a/src/scripts/Configure-os.h b/src/scripts/Configure-os.h
index f95a61a5d..dca0b9a3f 100755
--- a/src/scripts/Configure-os.h
+++ b/src/scripts/Configure-os.h
@@ -1,5 +1,7 @@
 #! /bin/sh
-# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (c) The Exim Maintainters 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
+
 
 # Shell script to create a link to the appropriate OS-specific header file.
 
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index 471b3a369..6e0b65f5d 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -4,6 +4,7 @@
 # specific build directory. It should be run from within that directory.
 #
 # Copyright (c) The Exim Maintainers 1995 - 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 test ! -d ../src && \
   echo "*** $0 should be run in a system-specific subdirectory." && \
diff --git a/src/scripts/arch-type b/src/scripts/arch-type
index f5fe8ebd3..0218c0233 100755
--- a/src/scripts/arch-type
+++ b/src/scripts/arch-type
@@ -1,7 +1,9 @@
 #! /bin/sh
-
 # Shell script to determine the architecture type.
 
+# Copyright (c) The Exim Maintainters 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
+
 # If EXIM_ARCHTYPE is set, use it. This allows a manual override.
 
 case "$EXIM_ARCHTYPE" in ?*) arch="$EXIM_ARCHTYPE";; esac
diff --git a/src/scripts/exim_install b/src/scripts/exim_install
index 1e8805024..827841ffc 100755
--- a/src/scripts/exim_install
+++ b/src/scripts/exim_install
@@ -1,5 +1,8 @@
 #! /bin/sh
 
+# Copyright (c) The Exim Maintainters 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
+
 # Script to install Exim binaries in BIN_DIRECTORY, which is defined in
 # the local Makefile. It expects to be run in a build directory. It needs
 # to be run as root in order to make exim setuid to root. If exim runs setuid
diff --git a/src/scripts/lookups-Makefile b/src/scripts/lookups-Makefile
index ed8b0cb5c..8dcac585b 100755
--- a/src/scripts/lookups-Makefile
+++ b/src/scripts/lookups-Makefile
@@ -1,6 +1,7 @@
 #! /bin/sh
 
 # Copyright (c) The Exim Maintainers 1995 - 2021
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # We turn the configure-built build-$foo/lookups/Makefile.predynamic into Makefile
 
diff --git a/src/scripts/newer b/src/scripts/newer
index 24c09e8f0..3bd65fb41 100755
--- a/src/scripts/newer
+++ b/src/scripts/newer
@@ -5,6 +5,9 @@
 # if the second does not exist, the answer is "yes";
 # otherwise their ages are compared using "find".
 
+# Copyright (c) The Exim Maintainters 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
+
 if [ $# -ne 2 ]; then
   echo "*** Two file names needed for 'newer' ***"
   exit 2;
diff --git a/src/scripts/os-type b/src/scripts/os-type
index a188c4a56..1fcd37bc3 100755
--- a/src/scripts/os-type
+++ b/src/scripts/os-type
@@ -1,5 +1,8 @@
 #! /bin/sh
 
+# Copyright (c) The Exim Maintainters 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
+
 # Shell script to determine the operating system type. Some of the heuristics
 # herein have accumulated over the years and may not strictly be needed now,
 # but they are left in under the principle of "If it ain't broke, don't fix
diff --git a/src/scripts/reversion b/src/scripts/reversion
index b932224f5..6fd0782a4 100755
--- a/src/scripts/reversion
+++ b/src/scripts/reversion
@@ -1,5 +1,6 @@
 #!/bin/sh
 # Copyright (c) The Exim Maintainers 1995 - 2021
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 set -e
 LC_ALL=C
diff --git a/src/scripts/source_checks b/src/scripts/source_checks
index 918a6f8eb..8620b27f5 100644
--- a/src/scripts/source_checks
+++ b/src/scripts/source_checks
@@ -1,4 +1,6 @@
 #!/bin/sh
+# Copyright (c) The Exim Maintainters 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 cd src;
 
diff --git a/src/src/EDITME b/src/src/EDITME
index 28d3d538e..625df18f5 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -1,6 +1,8 @@
 ##################################################
 #          The Exim mail transport agent         #
 ##################################################
+# Copyright (c) The Exim Maintainers 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # This is the template for Exim's main build-time configuration file. It
 # contains settings that are independent of any operating system. These are
diff --git a/src/src/acl.c b/src/src/acl.c
index 92af9991f..143890668 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Code for handling Access Control Lists (ACLs) */
 
diff --git a/src/src/arc.c b/src/src/arc.c
index e3a2b3dad..30a66320e 100644
--- a/src/src/arc.c
+++ b/src/src/arc.c
@@ -5,7 +5,7 @@
    Copyright (c) Jeremy Harris 2018 - 2020
    Copyright (c) The Exim Maintainers 2021 - 2022
    License: GPL
-   SPDX-License-Identifier: GPL-2.0-only
+   SPDX-License-Identifier: GPL-2.0-or-later
 */
 
 #include "exim.h"
diff --git a/src/src/auths/auth-spa.c b/src/src/auths/auth-spa.c
index 32276c3f3..bcf88c84d 100644
--- a/src/src/auths/auth-spa.c
+++ b/src/src/auths/auth-spa.c
@@ -10,7 +10,7 @@
  * Samba project (by Andrew Tridgell, Jeremy Allison, and others).
  *
  * Copyright (c) The Exim Maintainers 2021
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
 
  * Tom Kistner provided additional code, adding spa_build_auth_challenge() to
  * support server authentication mode.
diff --git a/src/src/auths/auth-spa.h b/src/src/auths/auth-spa.h
index df250d7a6..db93891ab 100644
--- a/src/src/auths/auth-spa.h
+++ b/src/src/auths/auth-spa.h
@@ -9,7 +9,7 @@
  * All the code used here was torn by Marc Prud'hommeaux out of the
  * Samba project (by Andrew Tridgell, Jeremy Allison, and others).
  */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* December 2004: The spa_base64_to_bits() function has no length checking in
 it. I have added a check. PH */
diff --git a/src/src/auths/call_pam.c b/src/src/auths/call_pam.c
index 483b083be..3ff15711d 100644
--- a/src/src/auths/call_pam.c
+++ b/src/src/auths/call_pam.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 
diff --git a/src/src/auths/call_pwcheck.c b/src/src/auths/call_pwcheck.c
index 436b1a8de..88f708f62 100644
--- a/src/src/auths/call_pwcheck.c
+++ b/src/src/auths/call_pwcheck.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This module contains interface functions to the two Cyrus authentication
 daemons. The original one was "pwcheck", which gives its name to the source
diff --git a/src/src/auths/call_radius.c b/src/src/auths/call_radius.c
index f4d7a9569..65882c108 100644
--- a/src/src/auths/call_radius.c
+++ b/src/src/auths/call_radius.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This file was originally supplied by Ian Kirk. The libradius support came
 from Alex Kiernan. */
diff --git a/src/src/auths/check_serv_cond.c b/src/src/auths/check_serv_cond.c
index 033d2026b..5201d2177 100644
--- a/src/src/auths/check_serv_cond.c
+++ b/src/src/auths/check_serv_cond.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 
diff --git a/src/src/auths/cram_md5.c b/src/src/auths/cram_md5.c
index 60128b83e..280b5293a 100644
--- a/src/src/auths/cram_md5.c
+++ b/src/src/auths/cram_md5.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* The stand-alone version just tests the algorithm. We have to drag
diff --git a/src/src/auths/cram_md5.h b/src/src/auths/cram_md5.h
index 25470a61b..984bc14c7 100644
--- a/src/src/auths/cram_md5.h
+++ b/src/src/auths/cram_md5.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/auths/cyrus_sasl.c b/src/src/auths/cyrus_sasl.c
index 4fe257ba7..b5d2d1d3b 100644
--- a/src/src/auths/cyrus_sasl.c
+++ b/src/src/auths/cyrus_sasl.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This code was originally contributed by Matthew Byng-Maddick */
 
diff --git a/src/src/auths/cyrus_sasl.h b/src/src/auths/cyrus_sasl.h
index 144ac5c80..05071b6e5 100644
--- a/src/src/auths/cyrus_sasl.h
+++ b/src/src/auths/cyrus_sasl.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Copyright (c) A L Digital Ltd 2004 */
 
diff --git a/src/src/auths/dovecot.h b/src/src/auths/dovecot.h
index b5eaf4f16..74c451930 100644
--- a/src/src/auths/dovecot.h
+++ b/src/src/auths/dovecot.h
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainters 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/auths/external.c b/src/src/auths/external.c
index 736c33982..078aad0fa 100644
--- a/src/src/auths/external.c
+++ b/src/src/auths/external.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 2019-2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This file provides an Exim authenticator driver for
 a server to verify a client SSL certificate, using the EXTERNAL
diff --git a/src/src/auths/external.h b/src/src/auths/external.h
index 9abb46a6b..0a9b0b50e 100644
--- a/src/src/auths/external.h
+++ b/src/src/auths/external.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 2019 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/auths/get_data.c b/src/src/auths/get_data.c
index 0c85d2651..caf4cfdb8 100644
--- a/src/src/auths/get_data.c
+++ b/src/src/auths/get_data.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 
diff --git a/src/src/auths/get_no64_data.c b/src/src/auths/get_no64_data.c
index 76f421473..e2cadfbc6 100644
--- a/src/src/auths/get_no64_data.c
+++ b/src/src/auths/get_no64_data.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 
diff --git a/src/src/auths/gsasl_exim.c b/src/src/auths/gsasl_exim.c
index aac9c84e6..2c39d0f21 100644
--- a/src/src/auths/gsasl_exim.c
+++ b/src/src/auths/gsasl_exim.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2019 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Copyright (c) Twitter Inc 2012
    Author: Phil Pennock <pdp@exim.org> */
diff --git a/src/src/auths/gsasl_exim.h b/src/src/auths/gsasl_exim.h
index 691d7d706..a56535710 100644
--- a/src/src/auths/gsasl_exim.h
+++ b/src/src/auths/gsasl_exim.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2019 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Copyright (c) Twitter Inc 2012 */
 
diff --git a/src/src/auths/heimdal_gssapi.c b/src/src/auths/heimdal_gssapi.c
index 12267e3fc..1336d0fab 100644
--- a/src/src/auths/heimdal_gssapi.c
+++ b/src/src/auths/heimdal_gssapi.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Copyright (c) Twitter Inc 2012
    Author: Phil Pennock <pdp@exim.org> */
diff --git a/src/src/auths/heimdal_gssapi.h b/src/src/auths/heimdal_gssapi.h
index 031b580ef..6c9b24298 100644
--- a/src/src/auths/heimdal_gssapi.h
+++ b/src/src/auths/heimdal_gssapi.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Copyright (c) Twitter Inc 2012
    Author: Phil Pennock <pdp@exim.org> */
diff --git a/src/src/auths/plaintext.c b/src/src/auths/plaintext.c
index 61be5867e..6692a676e 100644
--- a/src/src/auths/plaintext.c
+++ b/src/src/auths/plaintext.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "plaintext.h"
diff --git a/src/src/auths/plaintext.h b/src/src/auths/plaintext.h
index 49862ff9b..fdf0feb93 100644
--- a/src/src/auths/plaintext.h
+++ b/src/src/auths/plaintext.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/auths/pwcheck.c b/src/src/auths/pwcheck.c
index aff5ed39c..bf305832f 100644
--- a/src/src/auths/pwcheck.c
+++ b/src/src/auths/pwcheck.c
@@ -4,7 +4,7 @@
  * $Id: checkpw.c,v 1.49 2002/03/07 19:14:04 ken3 Exp $
  */
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 /*
  * Copyright (c) 2001 Carnegie Mellon University.  All rights reserved.
  *
diff --git a/src/src/auths/pwcheck.h b/src/src/auths/pwcheck.h
index 4247b4e8b..4c1d71d92 100644
--- a/src/src/auths/pwcheck.h
+++ b/src/src/auths/pwcheck.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This file provides support for authentication via the Cyrus SASL pwcheck
 daemon (whence its name) and the newer saslauthd daemon. */
diff --git a/src/src/auths/spa.c b/src/src/auths/spa.c
index c5e6d83d5..222ccea86 100644
--- a/src/src/auths/spa.c
+++ b/src/src/auths/spa.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This file, which provides support for Microsoft's Secure Password
 Authentication, was contributed by Marc Prud'hommeaux. Tom Kistner added SPA
diff --git a/src/src/auths/spa.h b/src/src/auths/spa.h
index 4321971d9..625a252d6 100644
--- a/src/src/auths/spa.h
+++ b/src/src/auths/spa.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This file, which provides support for Microsoft's Secure Password
 Authentication, was contributed by Marc Prud'hommeaux. */
diff --git a/src/src/auths/tls.c b/src/src/auths/tls.c
index d3ca8f796..72ad56c4e 100644
--- a/src/src/auths/tls.c
+++ b/src/src/auths/tls.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 1995 - 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This file provides an Exim authenticator driver for
 a server to verify a client SSL certificate
diff --git a/src/src/auths/tls.h b/src/src/auths/tls.h
index 24e116464..472a3e260 100644
--- a/src/src/auths/tls.h
+++ b/src/src/auths/tls.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/auths/xtextdecode.c b/src/src/auths/xtextdecode.c
index 39c1f2b60..b6a927194 100644
--- a/src/src/auths/xtextdecode.c
+++ b/src/src/auths/xtextdecode.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 
diff --git a/src/src/auths/xtextencode.c b/src/src/auths/xtextencode.c
index 89f12f1f1..c08288831 100644
--- a/src/src/auths/xtextencode.c
+++ b/src/src/auths/xtextencode.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 
diff --git a/src/src/base64.c b/src/src/base64.c
index 14e8cfffe..e9ac41a55 100644
--- a/src/src/base64.c
+++ b/src/src/base64.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004, 2015 */
 /* License: GPL */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
diff --git a/src/src/blob.h b/src/src/blob.h
index 95a987f0a..72b7fe564 100644
--- a/src/src/blob.h
+++ b/src/src/blob.h
@@ -2,7 +2,7 @@
  *  Blob - a general pointer/size item for a memory chunk
  *
  *  Copyright (C) 2016  Exim maintainers
- *  SPDX-License-Identifier: GPL-2.0-only
+ *  SPDX-License-Identifier: GPL-2.0-or-later
  */
 
 #ifndef BLOB_H	/* entire file */
diff --git a/src/src/bmi_spam.c b/src/src/bmi_spam.c
index 140bec48f..03e8defa6 100644
--- a/src/src/bmi_spam.c
+++ b/src/src/bmi_spam.c
@@ -6,7 +6,7 @@
    Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004
    License: GPL */
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "exim.h"
 #ifdef EXPERIMENTAL_BRIGHTMAIL
diff --git a/src/src/bmi_spam.h b/src/src/bmi_spam.h
index a9833b200..bb1c859a9 100644
--- a/src/src/bmi_spam.h
+++ b/src/src/bmi_spam.h
@@ -5,7 +5,7 @@
 /* Code for calling Brightmail AntiSpam.
    Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004
    License: GPL */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #ifdef EXPERIMENTAL_BRIGHTMAIL
 
diff --git a/src/src/buildconfig.c b/src/src/buildconfig.c
index 710b53550..8f37e508a 100644
--- a/src/src/buildconfig.c
+++ b/src/src/buildconfig.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /*************************************************
diff --git a/src/src/child.c b/src/src/child.c
index b94e814a1..359b791e8 100644
--- a/src/src/child.c
+++ b/src/src/child.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "exim.h"
diff --git a/src/src/daemon.c b/src/src/daemon.c
index 0afc7ca86..be008c3d4 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions concerned with running Exim as a daemon */
 
diff --git a/src/src/dane.c b/src/src/dane.c
index f2ad22481..a12e16238 100644
--- a/src/src/dane.c
+++ b/src/src/dane.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2012, 2014 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This module provides DANE (RFC6659) support for Exim.  See also
 the draft RFC for DANE-over-SMTP, "SMTP security via opportunistic DANE TLS"
diff --git a/src/src/dbfn.c b/src/src/dbfn.c
index f932520f9..3c51162a4 100644
--- a/src/src/dbfn.c
+++ b/src/src/dbfn.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "exim.h"
diff --git a/src/src/dbfunctions.h b/src/src/dbfunctions.h
index 93e1d3405..1f0dec1f7 100644
--- a/src/src/dbfunctions.h
+++ b/src/src/dbfunctions.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #ifndef DBFUNCTIONS_H
 #define DBFUNCTIONS_H
diff --git a/src/src/dcc.c b/src/src/dcc.c
index d8e22b30c..8986dedde 100644
--- a/src/src/dcc.c
+++ b/src/src/dcc.c
@@ -8,7 +8,7 @@
  * See the file NOTICE for conditions of use and distribution.
  *
  * Copyright (c) The Exim Maintainers 2015 - 2022
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
 /* Code for calling dccifd. Called from acl.c. */
diff --git a/src/src/debug.c b/src/src/debug.c
index 38e8f8001..44ad763e1 100644
--- a/src/src/debug.c
+++ b/src/src/debug.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2015 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "exim.h"
diff --git a/src/src/deliver.c b/src/src/deliver.c
index 719fa9d93..c4fce4602 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* The main code for delivering a message. */
 
diff --git a/src/src/directory.c b/src/src/directory.c
index c3b341bbb..94303db0b 100644
--- a/src/src/directory.c
+++ b/src/src/directory.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2010 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "exim.h"
 
diff --git a/src/src/dkim.c b/src/src/dkim.c
index 9b6e14a3f..0a8ab6fb3 100644
--- a/src/src/dkim.c
+++ b/src/src/dkim.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge, 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Code for DKIM support. Other DKIM relevant code is in
    receive.c, transport.c and transports/smtp.c */
diff --git a/src/src/dkim.h b/src/src/dkim.h
index 61d83a9df..915c6c739 100644
--- a/src/src/dkim.h
+++ b/src/src/dkim.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge, 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 void    dkim_exim_init(void);
 gstring * dkim_exim_sign(int, off_t, uschar *, struct ob_dkim *, const uschar **);
diff --git a/src/src/dkim_transport.c b/src/src/dkim_transport.c
index 142f4552a..c127d5b73 100644
--- a/src/src/dkim_transport.c
+++ b/src/src/dkim_transport.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Transport shim for dkim signing */
 
diff --git a/src/src/dmarc.c b/src/src/dmarc.c
index c0313bf5c..118720750 100644
--- a/src/src/dmarc.c
+++ b/src/src/dmarc.c
@@ -5,7 +5,7 @@
    Copyright (c) The Exim Maintainers 2019 - 2022
    Copyright (c) Todd Lyons <tlyons@exim.org> 2012 - 2014
    License: GPL */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Portions Copyright (c) 2012, 2013, The Trusted Domain Project;
    All rights reserved, licensed for use per LICENSE.opendmarc. */
diff --git a/src/src/dmarc.h b/src/src/dmarc.h
index f71f1fd5a..86d3b1e1c 100644
--- a/src/src/dmarc.h
+++ b/src/src/dmarc.h
@@ -6,7 +6,7 @@
    Copyright (c) The Exim Maintainers 2021 - 2022
    Copyright (c) Todd Lyons <tlyons@exim.org> 2012 - 2014
    License: GPL */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Portions Copyright (c) 2012, 2013, The Trusted Domain Project;
    All rights reserved, licensed for use per LICENSE.opendmarc. */
diff --git a/src/src/dns.c b/src/src/dns.c
index 8106fb688..4e01d8661 100644
--- a/src/src/dns.c
+++ b/src/src/dns.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions for interfacing with the DNS. */
 
diff --git a/src/src/dnsbl.c b/src/src/dnsbl.c
index af80f6be1..1172d6183 100644
--- a/src/src/dnsbl.c
+++ b/src/src/dnsbl.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions concerned with dnsbls */
 
diff --git a/src/src/drtables.c b/src/src/drtables.c
index a6cbede89..cf7c4e0b1 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "exim.h"
diff --git a/src/src/dummies.c b/src/src/dummies.c
index a72767ccd..df8ef6d5b 100644
--- a/src/src/dummies.c
+++ b/src/src/dummies.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This file is not part of the main Exim code. There are little bits of test
 code for some of Exim's modules, and when they are used, the module they are
diff --git a/src/src/enq.c b/src/src/enq.c
index 054aeaed8..43f53a585 100644
--- a/src/src/enq.c
+++ b/src/src/enq.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions concerned with serialization. */
 
diff --git a/src/src/environment.c b/src/src/environment.c
index 06f804e69..68adf3c0c 100644
--- a/src/src/environment.c
+++ b/src/src/environment.c
@@ -5,7 +5,7 @@
 /* Copyright (c) Heiko Schlittermann 2016
  * hs@schlittermann.de
  * See the file NOTICE for conditions of use and distribution.
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
 #include "exim.h"
diff --git a/src/src/exim.c b/src/src/exim.c
index 62413e367..35f4ae4f7 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* The main function: entry point, initialization, and high-level control.
diff --git a/src/src/exim.h b/src/src/exim.h
index 24fedcced..ccf14f0fd 100644
--- a/src/src/exim.h
+++ b/src/src/exim.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* Source files for exim all #include this header, which drags in everything
diff --git a/src/src/exim_dbmbuild.c b/src/src/exim_dbmbuild.c
index bb04adc02..0ea71795f 100644
--- a/src/src/exim_dbmbuild.c
+++ b/src/src/exim_dbmbuild.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* A small freestanding program to build dbm databases from serial input. For
diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index 669f7098d..f16570d86 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* This single source file is used to compile three utility programs for
diff --git a/src/src/exim_lock.c b/src/src/exim_lock.c
index 1a54a92ad..363c1bc71 100644
--- a/src/src/exim_lock.c
+++ b/src/src/exim_lock.c
@@ -11,7 +11,7 @@ Default is -fcntl -lockfile.
 Argument: the name of the lock file
 
 Copyright (c) The Exim Maintainers 2016 - 2021
-SPDX-License-Identifier: GPL-2.0-only
+SPDX-License-Identifier: GPL-2.0-or-later
 */
 
 #include "os.h"
diff --git a/src/src/expand.c b/src/src/expand.c
index 050f01297..657cf3cb9 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* Functions for handling string expansion. */
diff --git a/src/src/filter.c b/src/src/filter.c
index 82a9122c6..530d772b3 100644
--- a/src/src/filter.c
+++ b/src/src/filter.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* Code for mail filtering functions. */
diff --git a/src/src/filtertest.c b/src/src/filtertest.c
index 8bab65e78..2426f7feb 100644
--- a/src/src/filtertest.c
+++ b/src/src/filtertest.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* Code for the filter test function. */
diff --git a/src/src/functions.h b/src/src/functions.h
index be1fae00d..a2c8976e8 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* Prototypes for functions that appear in various modules. Gathered together
diff --git a/src/src/globals.c b/src/src/globals.c
index 429952edc..e5b72592f 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* All the global variables are defined together in this one module, so
 that they are easy to find. */
diff --git a/src/src/globals.h b/src/src/globals.h
index 48d93a1c3..5aae73fba 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Almost all the global variables are defined together in this one header, so
 that they are easy to find. However, those that are visible during the
diff --git a/src/src/hash.c b/src/src/hash.c
index 2b71463f1..10af1b43d 100644
--- a/src/src/hash.c
+++ b/src/src/hash.c
@@ -3,7 +3,7 @@
  *
  *  Copyright (c) The Exim Maintainers 2010 - 2022
  *  Copyright (c) University of Cambridge 1995 - 2009
- *  SPDX-License-Identifier: GPL-2.0-only
+ *  SPDX-License-Identifier: GPL-2.0-or-later
  *
  *  Hash interface functions
  */
diff --git a/src/src/hash.h b/src/src/hash.h
index c94e53129..788c9f0ad 100644
--- a/src/src/hash.h
+++ b/src/src/hash.h
@@ -1,7 +1,7 @@
 /*
  *  Exim - an Internet mail transport agent
  *  Copyright (c) The Exim Maintainers 1995 - 2022
- *  SPDX-License-Identifier: GPL-2.0-only
+ *  SPDX-License-Identifier: GPL-2.0-or-later
  *
  *  Hash interface functions
  */
diff --git a/src/src/header.c b/src/src/header.c
index 2a8fbfe64..a4dd6e72e 100644
--- a/src/src/header.c
+++ b/src/src/header.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "exim.h"
diff --git a/src/src/hintsdb.h b/src/src/hintsdb.h
index e79e6bd49..2d7199eda 100644
--- a/src/src/hintsdb.h
+++ b/src/src/hintsdb.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This header file contains macro definitions so that a variety of DBM
 libraries can be used by Exim. Nigel Metheringham provided the original set for
diff --git a/src/src/hintsdb_structs.h b/src/src/hintsdb_structs.h
index 27cd9edf9..0e5853b2b 100644
--- a/src/src/hintsdb_structs.h
+++ b/src/src/hintsdb_structs.h
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This header file contains the definitions of the structures used in the
 various hints databases are also kept in this file, which is used by the
diff --git a/src/src/host.c b/src/src/host.c
index 874e19a08..ecdc6d681 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions for finding hosts, either by gethostbyname(), gethostbyaddr(), or
 directly via the DNS. When IPv6 is supported, getipnodebyname() and
diff --git a/src/src/host_address.c b/src/src/host_address.c
index 28a8a685f..4c13f0bb7 100644
--- a/src/src/host_address.c
+++ b/src/src/host_address.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "exim.h"
 
diff --git a/src/src/imap_utf7.c b/src/src/imap_utf7.c
index 75ecb1a4b..1c09db621 100644
--- a/src/src/imap_utf7.c
+++ b/src/src/imap_utf7.c
@@ -1,6 +1,6 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "exim.h"
 
diff --git a/src/src/ip.c b/src/src/ip.c
index 2ac2b267d..b50130be3 100644
--- a/src/src/ip.c
+++ b/src/src/ip.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions for doing things with sockets. With the advent of IPv6 this has
 got messier, so that it's worth pulling out the code into separate functions
diff --git a/src/src/local_scan.c b/src/src/local_scan.c
index 85ecba8cb..da44cb7e6 100644
--- a/src/src/local_scan.c
+++ b/src/src/local_scan.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /******************************************************************************
diff --git a/src/src/local_scan.h b/src/src/local_scan.h
index 2eabc5929..69b3c6cdb 100644
--- a/src/src/local_scan.h
+++ b/src/src/local_scan.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This file is the header that is the only Exim header to be included in the
 source for the local_scan.c() function. It contains definitions that are made
diff --git a/src/src/log.c b/src/src/log.c
index f1d435ce4..6c483216a 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions for writing log files. The code for maintaining datestamped
 log files was originally contributed by Tony Sheen. */
diff --git a/src/src/lookupapi.h b/src/src/lookupapi.h
index cec8f976b..62c8c0524 100644
--- a/src/src/lookupapi.h
+++ b/src/src/lookupapi.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* The "type" field in each item is a set of bit flags:
diff --git a/src/src/lookups/dbmdb.c b/src/src/lookups/dbmdb.c
index c99e948b5..aa930e654 100644
--- a/src/src/lookups/dbmdb.c
+++ b/src/src/lookups/dbmdb.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/dnsdb.c b/src/src/lookups/dnsdb.c
index 4f43cf674..5482cd9d1 100644
--- a/src/src/lookups/dnsdb.c
+++ b/src/src/lookups/dnsdb.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c
index 6cae0dafb..74439bfc8 100644
--- a/src/src/lookups/dsearch.c
+++ b/src/src/lookups/dsearch.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* The idea for this code came from Matthew Byng-Maddick, but his original has
 been heavily reworked a lot for Exim 4 (and it now uses stat() (more precisely:
diff --git a/src/src/lookups/ibase.c b/src/src/lookups/ibase.c
index d42e490c4..7e4973e01 100644
--- a/src/src/lookups/ibase.c
+++ b/src/src/lookups/ibase.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* The code in this module was contributed by Ard Biesheuvel. */
 
diff --git a/src/src/lookups/json.c b/src/src/lookups/json.c
index b1e5fb742..43575cacf 100644
--- a/src/src/lookups/json.c
+++ b/src/src/lookups/json.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) Jeremy Harris 2019 - 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/ldap.c b/src/src/lookups/ldap.c
index 17c431e5c..feeea0e41 100644
--- a/src/src/lookups/ldap.c
+++ b/src/src/lookups/ldap.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Many thanks to Stuart Lynne for contributing the original code for this
 driver. Further contributions from Michael Haardt, Brian Candler, Barry
diff --git a/src/src/lookups/ldap.h b/src/src/lookups/ldap.h
index 30228aebe..2ce62fc05 100644
--- a/src/src/lookups/ldap.h
+++ b/src/src/lookups/ldap.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Header for eldapauth_find */
 
diff --git a/src/src/lookups/lf_check_file.c b/src/src/lookups/lf_check_file.c
index 1649f9f83..5c74816ef 100644
--- a/src/src/lookups/lf_check_file.c
+++ b/src/src/lookups/lf_check_file.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/lookups/lf_functions.h b/src/src/lookups/lf_functions.h
index b83b9652a..b7acbb5a9 100644
--- a/src/src/lookups/lf_functions.h
+++ b/src/src/lookups/lf_functions.h
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Header for the functions that are shared by the lookups */
 
diff --git a/src/src/lookups/lf_quote.c b/src/src/lookups/lf_quote.c
index 816fe01e2..72e37bfb4 100644
--- a/src/src/lookups/lf_quote.c
+++ b/src/src/lookups/lf_quote.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/lookups/lf_sqlperform.c b/src/src/lookups/lf_sqlperform.c
index cf4b9cd0b..ecb0a3221 100644
--- a/src/src/lookups/lf_sqlperform.c
+++ b/src/src/lookups/lf_sqlperform.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/lookups/lmdb.c b/src/src/lookups/lmdb.c
index ccfdbe453..3a3eebcba 100644
--- a/src/src/lookups/lmdb.c
+++ b/src/src/lookups/lmdb.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 2016 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 
diff --git a/src/src/lookups/lsearch.c b/src/src/lookups/lsearch.c
index da73ae8bf..f668f60f2 100644
--- a/src/src/lookups/lsearch.c
+++ b/src/src/lookups/lsearch.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/mysql.c b/src/src/lookups/mysql.c
index a8dae4ade..7e0343233 100644
--- a/src/src/lookups/mysql.c
+++ b/src/src/lookups/mysql.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Thanks to Paul Kelly for contributing the original code for these
 functions. */
diff --git a/src/src/lookups/nis.c b/src/src/lookups/nis.c
index e7c124757..6a08ebc37 100644
--- a/src/src/lookups/nis.c
+++ b/src/src/lookups/nis.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/nisplus.c b/src/src/lookups/nisplus.c
index 78c79563f..3f89c7f81 100644
--- a/src/src/lookups/nisplus.c
+++ b/src/src/lookups/nisplus.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/oracle.c b/src/src/lookups/oracle.c
index 3f3868fe9..d0604c245 100644
--- a/src/src/lookups/oracle.c
+++ b/src/src/lookups/oracle.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Interface to an Oracle database. This code was originally supplied by
 Paul Kelly, but I have hacked it around for various reasons, and tried to add
diff --git a/src/src/lookups/passwd.c b/src/src/lookups/passwd.c
index 7df1a5aaf..f185a9091 100644
--- a/src/src/lookups/passwd.c
+++ b/src/src/lookups/passwd.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 
diff --git a/src/src/lookups/pgsql.c b/src/src/lookups/pgsql.c
index 4fcd0e3ed..1583378d5 100644
--- a/src/src/lookups/pgsql.c
+++ b/src/src/lookups/pgsql.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Thanks to Petr Cech for contributing the original code for these
 functions. Thanks to Joachim Wieland for the initial patch for the Unix domain
diff --git a/src/src/lookups/readsock.c b/src/src/lookups/readsock.c
index 7c7b9cfa8..b1ea42c7f 100644
--- a/src/src/lookups/readsock.c
+++ b/src/src/lookups/readsock.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) Jeremy Harris 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/redis.c b/src/src/lookups/redis.c
index c7fcb66f0..7b680f086 100644
--- a/src/src/lookups/redis.c
+++ b/src/src/lookups/redis.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 
diff --git a/src/src/lookups/sqlite.c b/src/src/lookups/sqlite.c
index b9a735e18..6c7af4225 100644
--- a/src/src/lookups/sqlite.c
+++ b/src/src/lookups/sqlite.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/testdb.c b/src/src/lookups/testdb.c
index f94150b68..8de7090b7 100644
--- a/src/src/lookups/testdb.c
+++ b/src/src/lookups/testdb.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "lf_functions.h"
diff --git a/src/src/lookups/whoson.c b/src/src/lookups/whoson.c
index cd236787e..cd6c7e85c 100644
--- a/src/src/lookups/whoson.c
+++ b/src/src/lookups/whoson.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This code originally came from Robert Wal. */
 
diff --git a/src/src/lss.c b/src/src/lss.c
index 0d20c07b6..e6ec1d6d1 100644
--- a/src/src/lss.c
+++ b/src/src/lss.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Support functions for calling from local_scan(). These are mostly just
 wrappers for various internal functions. */
diff --git a/src/src/macro_predef.c b/src/src/macro_predef.c
index fe814500d..618c914dc 100644
--- a/src/src/macro_predef.c
+++ b/src/src/macro_predef.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) Jeremy Harris 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Create a static data structure with the predefined macros, to be
 included in the main Exim build */
diff --git a/src/src/macro_predef.h b/src/src/macro_predef.h
index d3bc5c074..8b7b375c6 100644
--- a/src/src/macro_predef.h
+++ b/src/src/macro_predef.h
@@ -5,7 +5,7 @@
 /* Copyright (c) Jeremy Harris 2017 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Global functions */
 
diff --git a/src/src/macros.h b/src/src/macros.h
index db1c34b65..243c1e5a0 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* These two macros make it possible to obtain the result of macro-expanding
diff --git a/src/src/malware.c b/src/src/malware.c
index 96514e276..f36c46b0f 100644
--- a/src/src/malware.c
+++ b/src/src/malware.c
@@ -6,7 +6,7 @@
  * Copyright (c) The Exim Maintainers 2015 - 2022
  * Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015
  * License: GPL
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
 /* Code for calling virus (malware) scanners. Called from acl.c. */
diff --git a/src/src/match.c b/src/src/match.c
index a877aef3b..15209f84a 100644
--- a/src/src/match.c
+++ b/src/src/match.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions for matching strings */
 
diff --git a/src/src/md5.c b/src/src/md5.c
index e6df32c7a..1d54ab4f1 100644
--- a/src/src/md5.c
+++ b/src/src/md5.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #ifndef STAND_ALONE
 #include "exim.h"
diff --git a/src/src/mime.c b/src/src/mime.c
index bebe9bf44..7c3a33d62 100644
--- a/src/src/mime.c
+++ b/src/src/mime.c
@@ -6,7 +6,7 @@
  * Copyright (c) The Exim Maintainers 2015 - 2022
  * Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004 - 2015
  * License: GPL
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
 #include "exim.h"
diff --git a/src/src/mime.h b/src/src/mime.h
index 48bbd81de..d436bcacb 100644
--- a/src/src/mime.h
+++ b/src/src/mime.h
@@ -5,7 +5,7 @@
 /* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004, 2015
  * License: GPL
  * Copyright (c) The Exim Maintainers 2016
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
 #ifdef WITH_CONTENT_SCAN
diff --git a/src/src/moan.c b/src/src/moan.c
index 387359458..ebfd440f6 100644
--- a/src/src/moan.c
+++ b/src/src/moan.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions for sending messages to sender or to mailmaster. */
 
diff --git a/src/src/mytypes.h b/src/src/mytypes.h
index 06426af7a..954f683c8 100644
--- a/src/src/mytypes.h
+++ b/src/src/mytypes.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* This header file contains type definitions and macros that I use as
diff --git a/src/src/os.c b/src/src/os.c
index 4edb2ca98..fc29f1766 100644
--- a/src/src/os.c
+++ b/src/src/os.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #ifdef STAND_ALONE
 # include <signal.h>
diff --git a/src/src/osfunctions.h b/src/src/osfunctions.h
index 454e9f77f..f88210943 100644
--- a/src/src/osfunctions.h
+++ b/src/src/osfunctions.h
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2016 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Prototypes for os-specific functions. For utilities, we don't need the one
 that uses a type that isn't defined for them. */
diff --git a/src/src/parse.c b/src/src/parse.c
index 93b12bc77..53d660869 100644
--- a/src/src/parse.c
+++ b/src/src/parse.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions for parsing addresses */
 
diff --git a/src/src/pdkim/crypt_ver.h b/src/src/pdkim/crypt_ver.h
index 367527e39..56ae236c1 100644
--- a/src/src/pdkim/crypt_ver.h
+++ b/src/src/pdkim/crypt_ver.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Signing and hashing routine selection for PDKIM */
 
diff --git a/src/src/pdkim/pdkim_hash.h b/src/src/pdkim/pdkim_hash.h
index e470299a4..d56e3ce34 100644
--- a/src/src/pdkim/pdkim_hash.h
+++ b/src/src/pdkim/pdkim_hash.h
@@ -2,7 +2,7 @@
  *  PDKIM - a RFC4871 (DKIM) implementation
  *
  *  Copyright (C) 1995 - 2018  Exim maintainers
- *  SPDX-License-Identifier: GPL-2.0-only
+ *  SPDX-License-Identifier: GPL-2.0-or-later
  *
  *  Hash interface functions
  */
diff --git a/src/src/pdkim/signing.c b/src/src/pdkim/signing.c
index 8876f617c..07737ab41 100644
--- a/src/src/pdkim/signing.c
+++ b/src/src/pdkim/signing.c
@@ -1,7 +1,7 @@
 /*
  *  PDKIM - a RFC4871 (DKIM) implementation
  *  Copyright (c) The Exim Maintainers 1995 - 2022
- *  SPDX-License-Identifier: GPL-2.0-only
+ *  SPDX-License-Identifier: GPL-2.0-or-later
  *
  *  signing/verification interface
  */
diff --git a/src/src/pdkim/signing.h b/src/src/pdkim/signing.h
index 880342b6f..7760ce73f 100644
--- a/src/src/pdkim/signing.h
+++ b/src/src/pdkim/signing.h
@@ -2,7 +2,7 @@
  *  PDKIM - a RFC4871 (DKIM) implementation
  *
  *  Copyright (C) 1995 - 2020  Exim maintainers
- *  SPDX-License-Identifier: GPL-2.0-only
+ *  SPDX-License-Identifier: GPL-2.0-or-later
  *
  *  RSA signing/verification interface
  */
diff --git a/src/src/perl.c b/src/src/perl.c
index b5c3f882e..2a10452d3 100644
--- a/src/src/perl.c
+++ b/src/src/perl.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) The Exim Maintainers 1999 - 2022 */
 /* Copyright (c) 1998 Malcolm Beattie */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Modified by PH to get rid of the "na" usage, March 1999.
    Modified further by PH for general tidying for Exim 4.
diff --git a/src/src/priv.c b/src/src/priv.c
index c818b5b78..3a100cd9e 100644
--- a/src/src/priv.c
+++ b/src/src/priv.c
@@ -1,5 +1,5 @@
 /* Copyright (c)  The Exim Maintainers 2022 *
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "exim.h"
 #include <sys/types.h>
diff --git a/src/src/queue.c b/src/src/queue.c
index fd84d303f..f86e24b42 100644
--- a/src/src/queue.c
+++ b/src/src/queue.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions that operate on the input queue. */
 
diff --git a/src/src/rda.c b/src/src/rda.c
index 1bc322e34..9c2aa5022 100644
--- a/src/src/rda.c
+++ b/src/src/rda.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This module contains code for extracting addresses from a forwarding list
 (from an alias or forward file) or by running the filter interpreter. It may do
diff --git a/src/src/readconf.c b/src/src/readconf.c
index b0d688772..48b648bb2 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions for reading the configuration file, and for displaying
 overall configuration values. Thanks to Brian Candler for the original
diff --git a/src/src/receive.c b/src/src/receive.c
index d9d0757ef..9bf834aaf 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Code for receiving a message and setting up spool files. */
 
diff --git a/src/src/regex.c b/src/src/regex.c
index eefba8ecf..757243e7f 100644
--- a/src/src/regex.c
+++ b/src/src/regex.c
@@ -6,7 +6,7 @@
  * Copyright (c) The Exim Maintainers 2016 - 2022
  * Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003-2015
  * License: GPL
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
 /* Code for matching regular expressions against headers and body.
diff --git a/src/src/regex_cache.c b/src/src/regex_cache.c
index e4771aab9..a9b482174 100644
--- a/src/src/regex_cache.c
+++ b/src/src/regex_cache.c
@@ -5,7 +5,7 @@
 /*
  * Copyright (c) The Exim Maintainers 2022
  * License: GPL
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
 /* Caching layers for compiled REs.  There is a local layer in the process,
diff --git a/src/src/retry.c b/src/src/retry.c
index e1490266c..f073af665 100644
--- a/src/src/retry.c
+++ b/src/src/retry.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions concerned with retrying unsuccessful deliveries. */
 
diff --git a/src/src/rewrite.c b/src/src/rewrite.c
index 040525ec5..c6a7b9fa5 100644
--- a/src/src/rewrite.c
+++ b/src/src/rewrite.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions concerned with rewriting headers */
 
diff --git a/src/src/rfc2047.c b/src/src/rfc2047.c
index af8993695..c40518a5d 100644
--- a/src/src/rfc2047.c
+++ b/src/src/rfc2047.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This file contains a function for decoding message header lines that may
 contain encoded "words" according to the rules described in
diff --git a/src/src/route.c b/src/src/route.c
index 7e6e4eb69..82d51bc68 100644
--- a/src/src/route.c
+++ b/src/src/route.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions concerned with routing, and the list of generic router options. */
 
diff --git a/src/src/routers/accept.c b/src/src/routers/accept.c
index ab02716eb..63c8c22e4 100644
--- a/src/src/routers/accept.c
+++ b/src/src/routers/accept.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/accept.h b/src/src/routers/accept.h
index e025179ee..ca768b26e 100644
--- a/src/src/routers/accept.h
+++ b/src/src/routers/accept.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Private structure for the private options (there aren't any). */
 
diff --git a/src/src/routers/dnslookup.c b/src/src/routers/dnslookup.c
index 0b1abeb8e..d27757c7e 100644
--- a/src/src/routers/dnslookup.c
+++ b/src/src/routers/dnslookup.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/dnslookup.h b/src/src/routers/dnslookup.h
index 0272db300..ce1daf3c0 100644
--- a/src/src/routers/dnslookup.h
+++ b/src/src/routers/dnslookup.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/routers/ipliteral.c b/src/src/routers/ipliteral.c
index cfbf276ff..1297b97f2 100644
--- a/src/src/routers/ipliteral.c
+++ b/src/src/routers/ipliteral.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/ipliteral.h b/src/src/routers/ipliteral.h
index f7e99b9c2..7db24cfd1 100644
--- a/src/src/routers/ipliteral.h
+++ b/src/src/routers/ipliteral.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* Private structure for the private options. Some compilers do not like empty
diff --git a/src/src/routers/iplookup.c b/src/src/routers/iplookup.c
index 970ec7704..7faaea0cd 100644
--- a/src/src/routers/iplookup.c
+++ b/src/src/routers/iplookup.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/iplookup.h b/src/src/routers/iplookup.h
index 3b7295574..ccce2eef5 100644
--- a/src/src/routers/iplookup.h
+++ b/src/src/routers/iplookup.h
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* Private structure for the private options. */
diff --git a/src/src/routers/manualroute.c b/src/src/routers/manualroute.c
index 6a500d09a..160c866b9 100644
--- a/src/src/routers/manualroute.c
+++ b/src/src/routers/manualroute.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/manualroute.h b/src/src/routers/manualroute.h
index e27d90953..1201c8362 100644
--- a/src/src/routers/manualroute.h
+++ b/src/src/routers/manualroute.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Header for the manualroute router */
 
diff --git a/src/src/routers/queryprogram.c b/src/src/routers/queryprogram.c
index 51b7b7551..51fdad229 100644
--- a/src/src/routers/queryprogram.c
+++ b/src/src/routers/queryprogram.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/queryprogram.h b/src/src/routers/queryprogram.h
index 148846fae..f2cff1dab 100644
--- a/src/src/routers/queryprogram.h
+++ b/src/src/routers/queryprogram.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* Private structure for the private options. */
diff --git a/src/src/routers/redirect.c b/src/src/routers/redirect.c
index 6126555fe..6a17c2f8d 100644
--- a/src/src/routers/redirect.c
+++ b/src/src/routers/redirect.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/redirect.h b/src/src/routers/redirect.h
index 76d02f261..8aeb892cb 100644
--- a/src/src/routers/redirect.h
+++ b/src/src/routers/redirect.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Header for the redirect router */
 
diff --git a/src/src/routers/rf_change_domain.c b/src/src/routers/rf_change_domain.c
index 4f8d47299..144fabbcd 100644
--- a/src/src/routers/rf_change_domain.c
+++ b/src/src/routers/rf_change_domain.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/rf_expand_data.c b/src/src/routers/rf_expand_data.c
index 5fbf0ac78..9892567f0 100644
--- a/src/src/routers/rf_expand_data.c
+++ b/src/src/routers/rf_expand_data.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/rf_functions.h b/src/src/routers/rf_functions.h
index d8fca68c5..91ccfb132 100644
--- a/src/src/routers/rf_functions.h
+++ b/src/src/routers/rf_functions.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Header for the functions that are shared by the routers */
 
diff --git a/src/src/routers/rf_get_errors_address.c b/src/src/routers/rf_get_errors_address.c
index 1189fd9e0..f70bdf25e 100644
--- a/src/src/routers/rf_get_errors_address.c
+++ b/src/src/routers/rf_get_errors_address.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/rf_get_munge_headers.c b/src/src/routers/rf_get_munge_headers.c
index be86a4e0e..58b5bc7ad 100644
--- a/src/src/routers/rf_get_munge_headers.c
+++ b/src/src/routers/rf_get_munge_headers.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/rf_get_transport.c b/src/src/routers/rf_get_transport.c
index 0c1fa8173..d54e3c296 100644
--- a/src/src/routers/rf_get_transport.c
+++ b/src/src/routers/rf_get_transport.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/rf_get_ugid.c b/src/src/routers/rf_get_ugid.c
index 15144beb1..cefe527a5 100644
--- a/src/src/routers/rf_get_ugid.c
+++ b/src/src/routers/rf_get_ugid.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/rf_lookup_hostlist.c b/src/src/routers/rf_lookup_hostlist.c
index affd70b6e..f10ff5921 100644
--- a/src/src/routers/rf_lookup_hostlist.c
+++ b/src/src/routers/rf_lookup_hostlist.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/rf_queue_add.c b/src/src/routers/rf_queue_add.c
index 49dd83117..8d94f5725 100644
--- a/src/src/routers/rf_queue_add.c
+++ b/src/src/routers/rf_queue_add.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/routers/rf_self_action.c b/src/src/routers/rf_self_action.c
index 7cc592039..73d07db40 100644
--- a/src/src/routers/rf_self_action.c
+++ b/src/src/routers/rf_self_action.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/routers/rf_set_ugid.c b/src/src/routers/rf_set_ugid.c
index 65f5200a2..ac63222b3 100644
--- a/src/src/routers/rf_set_ugid.c
+++ b/src/src/routers/rf_set_ugid.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "rf_functions.h"
diff --git a/src/src/search.c b/src/src/search.c
index 32099ab9e..2b6e5d37f 100644
--- a/src/src/search.c
+++ b/src/src/search.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* A set of functions to search databases in various formats. An open
 database is represented by a void * value which is returned from a lookup-
diff --git a/src/src/setenv.c b/src/src/setenv.c
index 877fe388f..a5f056aee 100644
--- a/src/src/setenv.c
+++ b/src/src/setenv.c
@@ -6,7 +6,7 @@
  * Copyright (c) Jeremy Harris 2015 - 2016
  * Copyright (c) The Exim Maintainers 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This module provides (un)setenv routines for those environments
 lacking them in libraries. It is #include'd by OS/os.c-foo files. */
diff --git a/src/src/sha_ver.h b/src/src/sha_ver.h
index 8385c1377..0d57db84f 100644
--- a/src/src/sha_ver.h
+++ b/src/src/sha_ver.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* SHA routine selection */
 
diff --git a/src/src/sieve.c b/src/src/sieve.c
index 033a9734a..0b347e48d 100644
--- a/src/src/sieve.c
+++ b/src/src/sieve.c
@@ -6,7 +6,7 @@
  * Copyright (c) The Exim Maintainers 2016 - 2022
  * Copyright (c) Michael Haardt 2003 - 2015
  * See the file NOTICE for conditions of use and distribution.
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
 /* This code was contributed by Michael Haardt. */
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 3c6339c82..9b60702c1 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions for handling an incoming SMTP call. */
 
diff --git a/src/src/smtp_out.c b/src/src/smtp_out.c
index db39dcab5..e705965ba 100644
--- a/src/src/smtp_out.c
+++ b/src/src/smtp_out.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* A number of functions for driving outgoing SMTP calls. */
 
diff --git a/src/src/spam.c b/src/src/spam.c
index f6575c293..401fdb269 100644
--- a/src/src/spam.c
+++ b/src/src/spam.c
@@ -6,7 +6,7 @@
  * Copyright (c) The Exim Maintainers 2016 - 2022
  * Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015
  * License: GPL
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
 /* Code for calling spamassassin's spamd. Called from acl.c. */
diff --git a/src/src/spam.h b/src/src/spam.h
index c410198d7..0362e49dd 100644
--- a/src/src/spam.h
+++ b/src/src/spam.h
@@ -5,7 +5,7 @@
 /* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* License: GPL */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* spam defines */
 
diff --git a/src/src/spf.c b/src/src/spf.c
index a94bc9677..6f0917a9c 100644
--- a/src/src/spf.c
+++ b/src/src/spf.c
@@ -6,7 +6,7 @@
    Copyright (c) The Exim Maintainers 2015 - 2022
    Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004 - 2014
    License: GPL
-   SPDX-License-Identifier: GPL-2.0-only
+   SPDX-License-Identifier: GPL-2.0-or-later
 */
 
 /* Code for calling spf checks via libspf-alt. Called from acl.c. */
diff --git a/src/src/spf.h b/src/src/spf.h
index 8fb7b04cb..76c7522bd 100644
--- a/src/src/spf.h
+++ b/src/src/spf.h
@@ -6,7 +6,7 @@
    Copyright (c) The Exim Maintainers 2016 - 2022
    Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004
    License: GPL
-   SPDX-License-Identifier: GPL-2.0-only
+   SPDX-License-Identifier: GPL-2.0-or-later
 */
 
 #ifdef SUPPORT_SPF
diff --git a/src/src/spool_in.c b/src/src/spool_in.c
index 82d11bf65..6d6651f57 100644
--- a/src/src/spool_in.c
+++ b/src/src/spool_in.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions for reading spool files. When compiling for a utility (eximon),
 not all are needed, and some functionality can be cut out. */
diff --git a/src/src/spool_mbox.c b/src/src/spool_mbox.c
index d9767ec3c..7ea565a58 100644
--- a/src/src/spool_mbox.c
+++ b/src/src/spool_mbox.c
@@ -5,7 +5,7 @@
 /* Copyright (c) Tom Kistner <tom@duncanthrax.net> 2003 - 2015
  * License: GPL
  * Copyright (c) The Exim Maintainers 2016 - 2021
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
 /* Code for setting up a MBOX style spool file inside a /scan/<msgid>
diff --git a/src/src/spool_out.c b/src/src/spool_out.c
index cb409c641..9c5e8eb33 100644
--- a/src/src/spool_out.c
+++ b/src/src/spool_out.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions for writing spool files, and moving them about. */
 
diff --git a/src/src/std-crypto.c b/src/src/std-crypto.c
index c1eef6c35..29efa6997 100644
--- a/src/src/std-crypto.c
+++ b/src/src/std-crypto.c
@@ -7,7 +7,7 @@
  * But almost everything here is fixed published constants from RFCs, so also:
  * Copyright (C) The Internet Society (2003)
  * Copyright (C) The IETF Trust (2008)
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
  *
  * Most of the text in RFC referencing comments is copy/paste from RFC,
  * as is undoubtedly the intention.
diff --git a/src/src/store.c b/src/src/store.c
index 449fb4ead..9e4536eae 100644
--- a/src/src/store.c
+++ b/src/src/store.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim maintainers 2019 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Exim gets and frees all its store through these functions. In the original
 implementation there was a lot of mallocing and freeing of small bits of store.
diff --git a/src/src/store.h b/src/src/store.h
index 47ed963cd..834457aaa 100644
--- a/src/src/store.h
+++ b/src/src/store.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Header for Exim's memory allocation functions */
 
diff --git a/src/src/string.c b/src/src/string.c
index ff4180b2b..2cb419517 100644
--- a/src/src/string.c
+++ b/src/src/string.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Miscellaneous string-handling functions. Some are not required for
 utilities and tests, and are cut out by the COMPILE_UTILITY macro. */
diff --git a/src/src/structs.h b/src/src/structs.h
index 1cb69236e..e1d93a943 100644
--- a/src/src/structs.h
+++ b/src/src/structs.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 /* Definitions of various structures. In addition, those that are visible for
diff --git a/src/src/tls-cipher-stdname.c b/src/src/tls-cipher-stdname.c
index fe442a050..ef3de99cb 100644
--- a/src/src/tls-cipher-stdname.c
+++ b/src/src/tls-cipher-stdname.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) Jeremy Harris 2019 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Translate an IETF TLS ciphersuite code to an IETF ciphersuite name,
 for use when the TLS library do not provide such names.
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index c98760202..703a0a4ca 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -6,7 +6,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) Phil Pennock 2012 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This file provides TLS/SSL support for Exim using the GnuTLS library,
 one of the available supported implementations.  This file is #included into
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 052d8161d..2b3f02712 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2019 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Portions Copyright (c) The OpenSSL Project 1999 */
 
diff --git a/src/src/tls.c b/src/src/tls.c
index 9e20b5bca..4a23aaae9 100644
--- a/src/src/tls.c
+++ b/src/src/tls.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This module provides TLS (aka SSL) support for Exim. The code for OpenSSL is
 based on a patch that was originally contributed by Steve Haslam. It was
diff --git a/src/src/tlscert-gnu.c b/src/src/tlscert-gnu.c
index 8840d5cfd..a3f6d4434 100644
--- a/src/src/tlscert-gnu.c
+++ b/src/src/tlscert-gnu.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) Jeremy Harris 2014 - 2018 */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This file provides TLS/SSL support for Exim using the GnuTLS library,
 one of the available supported implementations.  This file is #included into
diff --git a/src/src/tlscert-openssl.c b/src/src/tlscert-openssl.c
index a4c3d19fa..32177ea81 100644
--- a/src/src/tlscert-openssl.c
+++ b/src/src/tlscert-openssl.c
@@ -4,7 +4,7 @@
 
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) Jeremy Harris 2014 - 2019 */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* This module provides TLS (aka SSL) support for Exim using the OpenSSL
 library. It is #included into the tls.c file when that library is used.
diff --git a/src/src/tod.c b/src/src/tod.c
index ac4ed35b9..364703d53 100644
--- a/src/src/tod.c
+++ b/src/src/tod.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* A function for returning the time of day in various formats */
 
diff --git a/src/src/transport.c b/src/src/transport.c
index ff2e0b1d4..d04ea516a 100644
--- a/src/src/transport.c
+++ b/src/src/transport.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* General functions concerned with transportation, and generic options for all
 transports. */
diff --git a/src/src/transports/appendfile.c b/src/src/transports/appendfile.c
index 18badde86..c39c07c9f 100644
--- a/src/src/transports/appendfile.c
+++ b/src/src/transports/appendfile.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2020 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/transports/appendfile.h b/src/src/transports/appendfile.h
index 108114905..c3b7c59c4 100644
--- a/src/src/transports/appendfile.h
+++ b/src/src/transports/appendfile.h
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/transports/autoreply.c b/src/src/transports/autoreply.c
index eff1a3792..fa884cec4 100644
--- a/src/src/transports/autoreply.c
+++ b/src/src/transports/autoreply.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/transports/autoreply.h b/src/src/transports/autoreply.h
index 5f278a51e..83061719c 100644
--- a/src/src/transports/autoreply.h
+++ b/src/src/transports/autoreply.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/transports/lmtp.c b/src/src/transports/lmtp.c
index c734c3a26..e04c991ab 100644
--- a/src/src/transports/lmtp.c
+++ b/src/src/transports/lmtp.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/transports/lmtp.h b/src/src/transports/lmtp.h
index 71fd77f26..fe51b18cc 100644
--- a/src/src/transports/lmtp.h
+++ b/src/src/transports/lmtp.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/transports/pipe.c b/src/src/transports/pipe.c
index b8103e2f6..c3547eefe 100644
--- a/src/src/transports/pipe.c
+++ b/src/src/transports/pipe.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "../exim.h"
diff --git a/src/src/transports/pipe.h b/src/src/transports/pipe.h
index 416569608..be5ec3424 100644
--- a/src/src/transports/pipe.h
+++ b/src/src/transports/pipe.h
@@ -4,7 +4,7 @@
 
 /* Copyright (c) University of Cambridge 1995 - 2014 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/transports/queuefile.c b/src/src/transports/queuefile.c
index 3a2bae22f..b6511133c 100644
--- a/src/src/transports/queuefile.c
+++ b/src/src/transports/queuefile.c
@@ -6,7 +6,7 @@
 /* Copyright (c) University of Cambridge 2016 */
 /* Copyright (c) The Exim Maintainers 1995 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 
diff --git a/src/src/transports/queuefile.h b/src/src/transports/queuefile.h
index 22759a7f7..f5362515b 100644
--- a/src/src/transports/queuefile.h
+++ b/src/src/transports/queuefile.h
@@ -5,7 +5,7 @@
 /* Copyright (c) Andrew Colin Kissa <andrew@topdog.za.net> 2016 */
 /* Copyright (c) University of Cambridge 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Private structure for the private options. */
 
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index 2d2db1b10..1183fa478 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "../exim.h"
 #include "smtp.h"
diff --git a/src/src/transports/smtp.h b/src/src/transports/smtp.h
index 8793ece4f..cb1d72625 100644
--- a/src/src/transports/smtp.h
+++ b/src/src/transports/smtp.h
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #define DELIVER_BUFFER_SIZE 4096
 
diff --git a/src/src/transports/smtp_socks.c b/src/src/transports/smtp_socks.c
index 353a69618..22ee74bd8 100644
--- a/src/src/transports/smtp_socks.c
+++ b/src/src/transports/smtp_socks.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) Jeremy Harris 2015 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* SOCKS version 5 proxy, client-mode */
 
diff --git a/src/src/transports/tf_maildir.c b/src/src/transports/tf_maildir.c
index 925b8fac0..df932b13e 100644
--- a/src/src/transports/tf_maildir.c
+++ b/src/src/transports/tf_maildir.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* Copyright (c) The Exim Maintainers 2020 - 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions in support of the use of maildirsize files for handling quotas in
 maildir directories. Some of the rules are a bit baroque:
diff --git a/src/src/transports/tf_maildir.h b/src/src/transports/tf_maildir.h
index d314c3203..9f2f0e3f8 100644
--- a/src/src/transports/tf_maildir.h
+++ b/src/src/transports/tf_maildir.h
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2021 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Header file for the functions that are used to support the use of
 maildirsize files for quota handling in maildir directories. */
diff --git a/src/src/tree.c b/src/src/tree.c
index e340d986a..13fc28cc2 100644
--- a/src/src/tree.c
+++ b/src/src/tree.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Functions for maintaining binary balanced trees and some associated
 functions as well. */
diff --git a/src/src/utf8.c b/src/src/utf8.c
index 6604727ff..c05853838 100644
--- a/src/src/utf8.c
+++ b/src/src/utf8.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2022 */
 /* Copyright (c) Jeremy Harris 2015 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 
 #include "exim.h"
diff --git a/src/src/verify.c b/src/src/verify.c
index 3a8914e38..0ca096130 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -5,7 +5,7 @@
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* 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). */
diff --git a/src/src/version.c b/src/src/version.c
index cbaaef24b..2637ee94a 100644
--- a/src/src/version.c
+++ b/src/src/version.c
@@ -5,7 +5,7 @@
 /* Copyright (c) University of Cambridge 1995 - 2009 */
 /* Copyright (c) The Exim Maintainers 2010 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
-/* SPDX-License-Identifier: GPL-2.0-only */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
 
 /* Function for setting up the version string. */
 
diff --git a/src/util/chunking_fixqueue_finalnewlines.pl b/src/util/chunking_fixqueue_finalnewlines.pl
index b8ab17bd7..832b6a71f 100755
--- a/src/util/chunking_fixqueue_finalnewlines.pl
+++ b/src/util/chunking_fixqueue_finalnewlines.pl
@@ -1,5 +1,6 @@
 #!/usr/bin/env perl
-# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (c) The Exim Maintainers 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 use warnings;
 use strict;
diff --git a/src/util/cramtest.pl b/src/util/cramtest.pl
index a6124628b..9fc797ba2 100755
--- a/src/util/cramtest.pl
+++ b/src/util/cramtest.pl
@@ -1,5 +1,6 @@
 #!/usr/bin/perl
-# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (c) The Exim Maintainers 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # This script is contributed by Vadim Vygonets to aid in debugging CRAM-MD5
 # authentication.
diff --git a/src/util/gen_pkcs3.c b/src/util/gen_pkcs3.c
index 3ef3110de..c5a4453cb 100644
--- a/src/util/gen_pkcs3.c
+++ b/src/util/gen_pkcs3.c
@@ -2,7 +2,7 @@
  * Copyright (c) The Exim Maintainers 2021
  * This is distributed as part of Exim and licensed under the GPL.
  * See the file "NOTICE" for more details.
- * SPDX-License-Identifier: GPL-2.0-only
+ * SPDX-License-Identifier: GPL-2.0-or-later
  */
 
 /* Build with:
diff --git a/src/util/logargs.sh b/src/util/logargs.sh
index 814228d10..782ff8566 100755
--- a/src/util/logargs.sh
+++ b/src/util/logargs.sh
@@ -1,5 +1,6 @@
 #! /bin/sh
-# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (c) The Exim Maintainers 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # This script can be interposed between a calling program and another
 # program, in order to log the arguments which are being used. This can
diff --git a/src/util/mkcdb.pl b/src/util/mkcdb.pl
index e2b5931e7..3a438182c 100755
--- a/src/util/mkcdb.pl
+++ b/src/util/mkcdb.pl
@@ -1,5 +1,6 @@
 #!/usr/bin/perl -wT
-# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (c) The Exim Maintainers 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Create cdb file from flat alias file. DPC: 15/10/98.
 # Args:      source  (may be relative or absolute)
diff --git a/src/util/ocsp_fetch.pl b/src/util/ocsp_fetch.pl
index 2afbdbdac..9255311a4 100755
--- a/src/util/ocsp_fetch.pl
+++ b/src/util/ocsp_fetch.pl
@@ -1,7 +1,7 @@
 #!/usr/bin/perl
 # Copyright (C) 2012 Wizards Internet Ltd
 # License GPLv2: GNU GPL version 2 <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
-# SPDX-License-Identifier: GPL-2.0-only
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 use strict;
 BEGIN { pop @INC if $INC[-1] eq '.' };
diff --git a/src/util/proxy_protocol_client.pl b/src/util/proxy_protocol_client.pl
index 8b4311b64..8253fc4ac 100644
--- a/src/util/proxy_protocol_client.pl
+++ b/src/util/proxy_protocol_client.pl
@@ -3,7 +3,7 @@
 # Copyright (C) 2014 Todd Lyons
 # License GPLv2: GNU GPL version 2
 # <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>
-# SPDX-License-Identifier: GPL-2.0-only
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # This script emulates a proxy which uses Proxy Protocol to communicate
 # to a backend server.  It should be run from an IP which is configured
diff --git a/src/util/ratelimit.pl b/src/util/ratelimit.pl
index 2ceeebbfa..ce54fe6c3 100644
--- a/src/util/ratelimit.pl
+++ b/src/util/ratelimit.pl
@@ -1,5 +1,6 @@
 #!/usr/bin/perl -wT
-# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (c) The Exim Maintainers 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 use strict;
 
diff --git a/src/util/renew-opendmarc-tlds.sh b/src/util/renew-opendmarc-tlds.sh
index 583dc0e40..d626aac37 100755
--- a/src/util/renew-opendmarc-tlds.sh
+++ b/src/util/renew-opendmarc-tlds.sh
@@ -1,5 +1,6 @@
 #!/bin/sh -eu
-# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (c) The Exim Maintainers 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 #
 # Short version of this script:
 #   curl -f -o /var/cache/exim/opendmarc.tlds https://publicsuffix.org/list/public_suffix_list.dat
diff --git a/src/util/unknownuser.sh b/src/util/unknownuser.sh
index 01e617978..a57a84bba 100755
--- a/src/util/unknownuser.sh
+++ b/src/util/unknownuser.sh
@@ -1,5 +1,6 @@
 #! /bin/sh
-# SPDX-License-Identifier: GPL-2.0-only
+# Copyright (c) The Exim Maintainers 2022
+# SPDX-License-Identifier: GPL-2.0-or-later
 
 # This is a sample script for demonstrating how to handle unknown users in
 # a more friendly way than just returning a "user unknown" error. It can

commit f5730918ef684baafbd9e606a1d4eb06914563cc
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Aug 15 20:41:56 2022 +0100

    tidying

diff --git a/src/src/lookups/lf_check_file.c b/src/src/lookups/lf_check_file.c
index 5c74816ef..c4c05e44d 100644
--- a/src/src/lookups/lf_check_file.c
+++ b/src/src/lookups/lf_check_file.c
@@ -45,8 +45,7 @@ lf_check_file(int fd, const uschar * filename, int s_type, int modemask,
 {
 struct stat statbuf;
 
-if ((fd >= 0 && fstat(fd, &statbuf) != 0) ||
-    (fd  < 0 && Ustat(filename, &statbuf) != 0))
+if ((fd  < 0 ? Ustat(filename, &statbuf) : fstat(fd, &statbuf)) != 0)
   {
   int save_errno = errno;
   *errmsg = string_sprintf("%s: stat failed", filename);
@@ -80,7 +79,7 @@ if ((statbuf.st_mode & modemask) != 0)
   return +1;
   }
 
-if (owners != NULL)
+if (owners)
   {
   BOOL uid_ok = FALSE;
   for (int i = 1; i <= (int)owners[0]; i++)
@@ -94,7 +93,7 @@ if (owners != NULL)
     }
   }
 
-if (owngroups != NULL)
+if (owngroups)
   {
   BOOL gid_ok = FALSE;
   for (int i = 1; i <= (int)owngroups[0]; i++)
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index 703a0a4ca..69387a3a7 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -2743,25 +2743,25 @@ exim_gnutls_state_st * state = gnutls_session_get_ptr(session);
 
 if ((cert_list = gnutls_certificate_get_peers(session, &cert_list_size)))
   while (cert_list_size--)
-  {
-  if ((rc = import_cert(&cert_list[cert_list_size], &crt)) != GNUTLS_E_SUCCESS)
     {
-    DEBUG(D_tls) debug_printf("TLS: peer cert problem: depth %d: %s\n",
-      cert_list_size, gnutls_strerror(rc));
-    break;
-    }
+    if ((rc = import_cert(&cert_list[cert_list_size], &crt)) != GNUTLS_E_SUCCESS)
+      {
+      DEBUG(D_tls) debug_printf("TLS: peer cert problem: depth %d: %s\n",
+	cert_list_size, gnutls_strerror(rc));
+      break;
+      }
 
-  state->tlsp->peercert = crt;
-  if ((yield = event_raise(state->event_action,
-	      US"tls:cert", string_sprintf("%d", cert_list_size), &errno)))
-    {
-    log_write(0, LOG_MAIN,
-	      "SSL verify denied by event-action: depth=%d: %s",
-	      cert_list_size, yield);
-    return 1;                     /* reject */
+    state->tlsp->peercert = crt;
+    if ((yield = event_raise(state->event_action,
+		US"tls:cert", string_sprintf("%d", cert_list_size), &errno)))
+      {
+      log_write(0, LOG_MAIN,
+		"SSL verify denied by event-action: depth=%d: %s",
+		cert_list_size, yield);
+      return 1;                     /* reject */
+      }
+    state->tlsp->peercert = NULL;
     }
-  state->tlsp->peercert = NULL;
-  }
 
 return 0;
 }
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 2b3f02712..eabe34f31 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -2530,6 +2530,8 @@ if (!(bs = OCSP_response_get1_basic(rsp)))
     DEBUG(D_tls) bp = BIO_new(BIO_s_mem());
 
     /* Use the CA & chain that verified the server cert to verify the stapled info */
+    /*XXX could we do an event here, for observability of ocsp?  What reasonable data could we give access to? */
+    /* Dates would be a start. Do we need another opaque variable type, as for certs, plus an extract expansion? */
 
    {
     /* If this routine is not available, we've avoided [in tls_client_start()]
diff --git a/src/src/verify.c b/src/src/verify.c
index 0ca096130..125df8d91 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -1056,6 +1056,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;

commit 340e5f5781abb5388c41fa326d6e1cabf7ba96ff
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Nov 29 15:02:01 2022 +0000

    Tweak debug output

diff --git a/src/src/retry.c b/src/src/retry.c
index f073af665..a34bf80ca 100644
--- a/src/src/retry.c
+++ b/src/src/retry.c
@@ -135,7 +135,7 @@ dbdata_retry * host_retry_record, * message_retry_record;
 
 *retry_host_key = *retry_message_key = NULL;
 
-DEBUG(D_transport|D_retry) debug_printf("checking status of %s\n", host->name);
+DEBUG(D_transport|D_retry) debug_printf("checking retry status of %s\n", host->name);
 
 /* Do nothing if status already set; otherwise initialize status as usable. */
 

commit fc37f2acaaa440c5265dc01fd693d8f5406f5cf9
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Nov 29 15:55:05 2022 +0000

    Add predefined macros for expansions

diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index 4e1f61a26..a290b90b0 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -111,8 +111,8 @@ MACRO_HSRC = macro_predef.h os.h globals.h config.h macros.h \
 	routers/redirect.h
 
 OBJ_MACRO = macro_predef.o \
-	macro-globals.o macro-readconf.o macro-route.o macro-transport.o macro-drtables.o \
-	macro-acl.o macro-tls.o \
+	macro-globals.o macro-readconf.o macro-expand.o macro-route.o \
+	macro-transport.o macro-drtables.o macro-acl.o macro-tls.o \
 	macro-appendfile.o macro-autoreply.o macro-lmtp.o macro-pipe.o macro-queuefile.o \
 	macro-smtp.o macro-accept.o macro-dnslookup.o macro-ipliteral.o macro-iplookup.o \
 	macro-manualroute.o macro-queryprogram.o macro-redirect.o \
@@ -131,6 +131,9 @@ macro-globals.o :	globals.c
 macro-readconf.o :	readconf.c
 	@echo "$(CC) -DMACRO_PREDEF readconf.c"
 	$(FE)$(CC) -c $(CFLAGS) -DMACRO_PREDEF $(INCLUDE) -o $@ readconf.c
+macro-expand.o :	expand.c
+	@echo "$(CC) -DMACRO_PREDEF expand.c"
+	$(FE)$(CC) -c $(CFLAGS) -DMACRO_PREDEF $(INCLUDE) -o $@ expand.c
 macro-route.o :		route.c
 	@echo "$(CC) -DMACRO_PREDEF route.c"
 	$(FE)$(CC) -c $(CFLAGS) -DMACRO_PREDEF $(INCLUDE) -o $@ route.c
diff --git a/src/src/expand.c b/src/src/expand.c
index 657cf3cb9..6def3c102 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -13,6 +13,10 @@
 
 #include "exim.h"
 
+#ifdef MACRO_PREDEF
+# include "macro_predef.h"
+#endif
+
 typedef unsigned esi_flags;
 #define ESI_NOFLAGS		0
 #define ESI_BRACE_ENDS		BIT(0)	/* expansion should stop at } */
@@ -831,6 +835,76 @@ static var_entry var_table[] = {
 };
 
 static int var_table_size = nelem(var_table);
+
+#ifdef MACRO_PREDEF
+
+/* dummies */
+uschar * fn_arc_domains(void) {return NULL;}
+uschar * fn_hdrs_added(void) {return NULL;}
+uschar * fn_queue_size(void) {return NULL;}
+uschar * fn_recipients(void) {return NULL;}
+uschar * sender_helo_verified_boolstr(void) {return NULL;}
+uschar * smtp_cmd_hist(void) {return NULL;}
+
+
+
+static void
+expansion_items(void)
+{
+uschar buf[64];
+for (int i = 0; i < nelem(item_table); i++)
+  {
+  spf(buf, sizeof(buf), CUS"_EXP_ITEM_%T", item_table[i]);
+  builtin_macro_create(buf);
+  }
+}
+static void
+expansion_operators(void)
+{
+uschar buf[64];
+for (int i = 0; i < nelem(op_table_underscore); i++)
+  {
+  spf(buf, sizeof(buf), CUS"_EXP_OP_%T", op_table_underscore[i]);
+  builtin_macro_create(buf);
+  }
+for (int i = 0; i < nelem(op_table_main); i++)
+  {
+  spf(buf, sizeof(buf), CUS"_EXP_OP_%T", op_table_main[i]);
+  builtin_macro_create(buf);
+  }
+}
+static void
+expansion_conditions(void)
+{
+uschar buf[64];
+for (int i = 0; i < nelem(cond_table); i++)
+  {
+  spf(buf, sizeof(buf), CUS"_EXP_COND_%T", cond_table[i]);
+  builtin_macro_create(buf);
+  }
+}
+static void
+expansion_variables(void)
+{
+uschar buf[64];
+for (int i = 0; i < nelem(var_table); i++)
+  {
+  spf(buf, sizeof(buf), CUS"_EXP_VAR_%T", var_table[i].name);
+  builtin_macro_create(buf);
+  }
+}
+
+void
+expansions(void)
+{
+expansion_items();
+expansion_operators();
+expansion_conditions();
+expansion_variables();
+}
+
+#else	/*!MACRO_PREDEF*/
+
 static uschar var_buffer[256];
 static BOOL malformed_header;
 
@@ -8861,8 +8935,9 @@ search_tidyup();
 return 0;
 }
 
-#endif
+#endif	/*STAND_ALONE*/
 
+#endif	/*!MACRO_PREDEF*/
 /* vi: aw ai sw=2
 */
 /* End of expand.c */
diff --git a/src/src/macro_predef.c b/src/src/macro_predef.c
index 618c914dc..0053cb245 100644
--- a/src/src/macro_predef.c
+++ b/src/src/macro_predef.c
@@ -338,6 +338,7 @@ printf("#include \"exim.h\"\n");
 features();
 exp_features();
 options();
+expansions();
 params();
 
 printf("macro_item * macros = &p%d;\n", mp_index-1);
diff --git a/src/src/macro_predef.h b/src/src/macro_predef.h
index 8b7b375c6..94f68dfa3 100644
--- a/src/src/macro_predef.h
+++ b/src/src/macro_predef.h
@@ -22,6 +22,7 @@ extern void options_routers(void);
 extern void options_transports(void);
 extern void options_auths(void);
 extern void options_logging(void);
+extern void expansions(void);
 extern void params_dkim(void);
 #ifndef DISABLE_TLS
 extern void options_tls(void);

commit 2484a8253df7795a45fb5b4aff6df0bf0e4d56e5
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Nov 29 14:34:25 2022 +0000

    Add variable $sender_helo_verified

diff --git a/src/src/acl.c b/src/src/acl.c
index 143890668..8e1d92457 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -1643,6 +1643,30 @@ return period;
 
 
 
+static BOOL
+sender_helo_verified_internal(void)
+{
+/* We can test the result of optional HELO verification that might have
+occurred earlier. If not, we can attempt the verification now. */
+
+if (!f.helo_verified && !f.helo_verify_failed) smtp_verify_helo();
+return f.helo_verified;
+}
+
+static int
+sender_helo_verified_cond(void)
+{
+return sender_helo_verified_internal() ? OK : FAIL;
+}
+
+uschar *
+sender_helo_verified_boolstr(void)
+{
+return sender_helo_verified_internal() ? US"yes" : US"no";
+}
+
+
+
 /* This function implements the "verify" condition. It is called when
 encountered in any ACL, because some tests are almost always permitted. Some
 just don't make sense, and always fail (for example, an attempt to test a host
@@ -1739,11 +1763,7 @@ switch(vp->value)
     return FAIL;
 
   case VERIFY_HELO:
-    /* We can test the result of optional HELO verification that might have
-    occurred earlier. If not, we can attempt the verification now. */
-
-    if (!f.helo_verified && !f.helo_verify_failed) smtp_verify_helo();
-    return f.helo_verified ? OK : FAIL;
+    return sender_helo_verified_cond();
 
   case VERIFY_CSA:
     /* Do Client SMTP Authorization checks in a separate function, and turn the
diff --git a/src/src/expand.c b/src/src/expand.c
index 6def3c102..57ad76f77 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -715,6 +715,7 @@ static var_entry var_table[] = {
   { "sender_fullhost",     vtype_stringptr,   &sender_fullhost },
   { "sender_helo_dnssec",  vtype_bool,        &sender_helo_dnssec },
   { "sender_helo_name",    vtype_stringptr,   &sender_helo_name },
+  { "sender_helo_verified",vtype_string_func, (void *) &sender_helo_verified_boolstr },
   { "sender_host_address", vtype_stringptr,   &sender_host_address },
   { "sender_host_authenticated",vtype_stringptr, &sender_host_authenticated },
   { "sender_host_dnssec",  vtype_bool,        &sender_host_dnssec },
diff --git a/src/src/functions.h b/src/src/functions.h
index a2c8976e8..3ca346c04 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -489,6 +489,7 @@ extern int     search_findtype_partial(const uschar *, int *, const uschar **, i
                  int *, const uschar **);
 extern void   *search_open(const uschar *, int, int, uid_t *, gid_t *);
 extern void    search_tidyup(void);
+extern uschar *sender_helo_verified_boolstr(void);
 extern void    set_process_info(const char *, ...) PRINTF_FUNCTION(1,2);
 extern void    sha1_end(hctx *, const uschar *, int, uschar *);
 extern void    sha1_mid(hctx *, const uschar *);

commit e8297f953ed9c8e42f1b406b5ecad4ccdd9d95d3
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Dec 3 15:16:48 2022 +0000

    tidying

diff --git a/src/src/log.c b/src/src/log.c
index 6c483216a..a01e9c59f 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -1544,6 +1544,7 @@ else DEBUG(D_deliver)
 void
 debug_logging_stop(BOOL kill)
 {
+debug_printf("debug terminated by %s\n", kill ? "kill" : "stop");
 debug_pretrigger_discard();
 if (!debug_file || !debuglog_name[0]) return;
 
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index eabe34f31..db77a1274 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -3999,7 +3999,7 @@ if (tlsp->host_resumable)
     tls_error(US"set ex_data", host, NULL, errstr);
     return FALSE;
     }
-  debug_printf("tls_exdata_idx %d cbinfo %p\n", tls_exdata_idx, client_static_state);
+  /* debug_printf("tls_exdata_idx %d cbinfo %p\n", tls_exdata_idx, client_static_state); */
   }
 
 tlsp->resumption = RESUME_SUPPORTED;
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index 1183fa478..ed5f83b3e 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -990,7 +990,7 @@ return FALSE;
 
 
 /* Return an auths bitmap for the set of AUTH methods offered by the server
-which match our authenticators. */
+which match our client-side authenticators. */
 
 static unsigned short
 study_ehlo_auths(smtp_context * sx)
@@ -1016,7 +1016,7 @@ for (au = auths, authnum = 0; au; au = au->next, authnum++) if (au->client)
   }
 
 DEBUG(D_transport)
-  debug_printf("server offers %s AUTH, methods '%s', bitmap 0x%04x\n",
+  debug_printf("server offers %s AUTH, methods '%s', usable-bitmap 0x%04x\n",
     tls_out.active.sock >= 0 ? "crypted" : "plaintext", names, authbits);
 
 if (tls_out.active.sock >= 0)

commit 0da41dc541d0f2536f9d2afc7188e9dfb97b0c09
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Dec 3 17:50:17 2022 +0000

    compiler quietening

diff --git a/src/src/lookups/ldap.c b/src/src/lookups/ldap.c
index feeea0e41..ba77c98c2 100644
--- a/src/src/lookups/ldap.c
+++ b/src/src/lookups/ldap.c
@@ -1470,27 +1470,20 @@ t = quoted = store_get_quoted(len + count + 1, s, idx);
 /* Handle plain quote_ldap */
 
 if (!dn)
-  {
-  while ((c = *s++))
-    {
+  for (; c = *s++; *t++ = c)
     if (!isalnum(c))
       {
       if (Ustrchr(LDAP_QUOTE, c) != NULL)
         {
         sprintf(CS t, "%%5C%02X", c);        /* e.g. * => %5C2A */
         t += 5;
-        continue;
         }
-      if (Ustrchr(URL_NONQUOTE, c) == NULL)  /* e.g. ] => %5D */
+      else if (Ustrchr(URL_NONQUOTE, c) == NULL)  /* e.g. ] => %5D */
         {
         sprintf(CS t, "%%%02X", c);
         t += 3;
-        continue;
         }
       }
-    *t++ = c;                                /* unquoted character */
-    }
-  }
 
 /* Handle quote_ldap_dn */
 
@@ -1520,8 +1513,8 @@ else
       {
       if (Ustrchr(LDAP_DN_QUOTE, c) != NULL)
         {
-        Ustrncpy(t, US"%5C", 3);               /* insert \ where needed */
-        t += 3;                              /* fall through to check URL */
+        Ustrcpy(t, US"%5C");			/* insert \ where needed */
+        t += 3;					/* fall through to check URL */
         }
       if (Ustrchr(URL_NONQUOTE, c) == NULL)  /* e.g. ] => %5D */
         {
@@ -1535,9 +1528,9 @@ else
 
   /* Handle the trailing spaces */
 
-  while (*ss++ != 0)
+  while (*ss++)
     {
-    Ustrncpy(t, US"%5C%20", 6);
+    Ustrcpy(t, US"%5C%20");
     t += 6;
     }
   }

commit 44b6e099b76f403a55e77650821f8a69e9d2682e
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Dec 3 23:13:53 2022 +0000

    Fix ${run } arg parsing
    
    Broken-by: cfe6acff2ddc

diff --git a/src/src/expand.c b/src/src/expand.c
index 57ad76f77..7bb2e4274 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -5614,7 +5614,7 @@ while (*s)
       const uschar * arg, ** argv;
       BOOL late_expand = TRUE;
 
-      if ((expand_forbid & RDO_RUN) != 0)
+      if (expand_forbid & RDO_RUN)
         {
         expand_string_message = US"running a command is not permitted";
         goto EXPAND_FAILED;
@@ -5645,13 +5645,20 @@ while (*s)
       s++;
 
       if (late_expand)		/* this is the default case */
-	{						/*{*/
-	int n = Ustrcspn(s, "}");
+	{
+	int n;
+	const uschar * t;
+	/* Locate the end of the args */
+	(void) expand_string_internal(s,
+	  ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | ESI_SKIPPING, &t, NULL, NULL);
+	n = t - s;
 	arg = flags & ESI_SKIPPING ? NULL : string_copyn(s, n);
 	s += n;
 	}
       else
 	{
+	DEBUG(D_expand)
+	  debug_printf_indent("args string for ${run} expand before split\n");
 	if (!(arg = expand_string_internal(s,
 		ESI_BRACE_ENDS | ESI_HONOR_DOLLAR | flags, &s, &resetok, NULL)))
 	  goto EXPAND_FAILED;
diff --git a/src/src/transport.c b/src/src/transport.c
index d04ea516a..d6cedf911 100644
--- a/src/src/transport.c
+++ b/src/src/transport.c
@@ -2190,6 +2190,8 @@ if (expand_arguments)
 
   for (int i = 0; argv[i]; i++)
     {
+    DEBUG(D_expand) debug_printf_indent("arg %d\n", i);
+
     /* Handle special fudge for passing an address list */
 
     if (addr &&
@@ -2364,7 +2366,7 @@ if (expand_arguments)
         return FALSE;
         }
 
-      if ( f.running_in_test_harness && is_tainted(expanded_arg)
+      if (  f.running_in_test_harness && is_tainted(expanded_arg)
 	 && Ustrcmp(etext, "queryprogram router") == 0)
 	{			/* hack, would be good to not need it */
 	DEBUG(D_transport)

commit 4243a209fd9499f30bebd58ceaa2d0d9845407ae
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Dec 10 10:47:05 2022 +0000

    Move connect ACL before TLS-on-connect

diff --git a/src/src/EDITME b/src/src/EDITME
index 625df18f5..4fcaeda5b 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -401,7 +401,7 @@ TRANSPORT_SMTP=yes
 # For Redis you need to have hiredis installed on your system
 # (https://github.com/redis/hiredis).
 # Depending on where it is installed you may have to edit the CFLAGS
-# (often += -I/usr/local/include) and LDFLAGS (-lhiredis) lines.
+# (often += -I/usr/local/include) and LOOKUP_LIBS (-lhiredis) lines.
 
 # If your system has pkg-config then the _INCLUDE/_LIBS setting can be
 # handled for you automatically by also defining the _PC variable to reference
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 9b60702c1..b161f362d 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -2505,6 +2505,22 @@ else DEBUG(D_receive)
 #endif
 
 
+static void
+log_connect_tls_drop(const uschar * what, const uschar * log_msg)
+{
+gstring * g = s_tlslog(NULL);
+uschar * tls = string_from_gstring(g);
+
+log_write(L_connection_reject,
+  log_reject_target, "%s%s%s dropped by %s%s%s",
+  LOGGING(dnssec) && sender_host_dnssec ? US" DS" : US"",
+  host_and_ident(TRUE),
+  tls ? tls : US"",
+  what,
+  log_msg ? US": " : US"", log_msg);
+}
+
+
 /*************************************************
 *          Start an SMTP session                 *
 *************************************************/
@@ -2857,7 +2873,10 @@ if (!f.sender_host_unknown)
     {
     log_write(L_connection_reject, LOG_MAIN|LOG_REJECT, "refused connection "
       "from %s (host_reject_connection)", host_and_ident(FALSE));
-    smtp_printf("554 SMTP service not available\r\n", FALSE);
+#ifndef DISABLE_TLS
+    if (!tls_in.on_connect)
+#endif
+      smtp_printf("554 SMTP service not available\r\n", FALSE);
     return FALSE;
     }
 
@@ -2983,18 +3002,6 @@ if (check_proxy_protocol_host())
   setup_proxy_protocol_host();
 #endif
 
-/* Start up TLS if tls_on_connect is set. This is for supporting the legacy
-smtps port for use with older style SSL MTAs. */
-
-#ifndef DISABLE_TLS
-if (tls_in.on_connect)
-  {
-  if (tls_server_start(&user_msg) != OK)
-    return smtp_log_tls_fail(user_msg);
-  cmd_list[CMD_LIST_TLS_AUTH].is_mail_cmd = TRUE;
-  }
-#endif
-
 /* Run the connect ACL if it exists */
 
 user_msg = NULL;
@@ -3004,11 +3011,28 @@ if (acl_smtp_connect)
   if ((rc = acl_check(ACL_WHERE_CONNECT, NULL, acl_smtp_connect, &user_msg,
 		      &log_msg)) != OK)
     {
-    (void) smtp_handle_acl_fail(ACL_WHERE_CONNECT, rc, user_msg, log_msg);
+#ifndef DISABLE_TLS
+    if (tls_in.on_connect)
+      log_connect_tls_drop(US"'connect' ACL", log_msg);
+    else
+#endif
+      (void) smtp_handle_acl_fail(ACL_WHERE_CONNECT, rc, user_msg, log_msg);
     return FALSE;
     }
   }
 
+/* Start up TLS if tls_on_connect is set. This is for supporting the legacy
+smtps port for use with older style SSL MTAs. */
+
+#ifndef DISABLE_TLS
+if (tls_in.on_connect)
+  {
+  if (tls_server_start(&user_msg) != OK)
+    return smtp_log_tls_fail(user_msg);
+  cmd_list[CMD_LIST_TLS_AUTH].is_mail_cmd = TRUE;
+  }
+#endif
+
 /* Output the initial message for a two-way SMTP connection. It may contain
 newlines, which then cause a multi-line response to be given. */
 

commit 4f7a93c27e3d43b44c42d3fc503f03b9b42ca622
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Dec 10 16:19:16 2022 +0000

    Allow a forced-fail for banner expansion to close connection without panic-log

diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index b161f362d..6c043d434 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -3040,13 +3040,7 @@ code = US"220";   /* Default status code */
 esc = US"";       /* Default extended status code */
 esclen = 0;       /* Length of esc */
 
-if (!user_msg)
-  {
-  if (!(s = expand_string(smtp_banner)))
-    log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Expansion of \"%s\" (smtp_banner) "
-      "failed: %s", smtp_banner, expand_string_message);
-  }
-else
+if (user_msg)
   {
   int codelen = 3;
   s = user_msg;
@@ -3057,6 +3051,17 @@ else
     esclen = codelen - 4;
     }
   }
+else if (!(s = expand_string(smtp_banner)))
+  {
+  log_write(0, f.expand_string_forcedfail ? LOG_MAIN : LOG_MAIN|LOG_PANIC_DIE,
+    "Expansion of \"%s\" (smtp_banner) failed: %s",
+    smtp_banner, expand_string_message);
+  /* for force-fail */
+#ifndef DISABLE_TLS
+  if (tls_in.on_connect) tls_close(NULL, TLS_SHUTDOWN_WAIT);
+#endif
+  return FALSE;
+  }
 
 /* Remove any terminating newlines; might as well remove trailing space too */
 

commit 520ef00f56cea3d35688bf4e13599a6e37ba275f
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Dec 11 15:14:54 2022 +0000

    TLS: Fix handling for server cert/key file SNI re-expansion forced-fail

diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index 69387a3a7..f8cc34406 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -398,7 +398,8 @@ tls_error_gnu(exim_gnutls_state_st * state, const uschar *prefix, int err,
 {
 return tls_error(prefix,
   state && err == GNUTLS_E_FATAL_ALERT_RECEIVED
-  ? US gnutls_alert_get_name(gnutls_alert_get(state->session))
+  ? string_sprintf("rxd alert: %s",
+		  US gnutls_alert_get_name(gnutls_alert_get(state->session)))
   : US gnutls_strerror(err),
   state ? state->host : NULL,
   errstr);
@@ -1293,7 +1294,7 @@ while (cfile = string_nextinlist(&clist, &csep, NULL, 0))
 
   if (!(kfile = string_nextinlist(&klist, &ksep, NULL, 0)))
     return tls_error(US"cert/key setup: out of keys", NULL, NULL, errstr);
-  else if ((rc = tls_add_certfile(state, NULL, cfile, kfile, errstr)) > 0)
+  else if ((rc = tls_add_certfile(state, NULL, cfile, kfile, errstr)) != OK)
     return rc;
   else
     {
@@ -1810,8 +1811,13 @@ D-H generation. */
 
 if (!state->lib_state.conn_certs)
   {
-  if (!Expand_check_tlsvar(tls_certificate, errstr))
+  if (  !Expand_check_tlsvar(tls_certificate, errstr)
+     || f.expand_string_forcedfail)
+    {
+    if (f.expand_string_forcedfail)
+      *errstr = US"expansion of tls_certificate failed";
     return DEFER;
+    }
 
   /* certificate is mandatory in server, optional in client */
 
@@ -1823,8 +1829,14 @@ if (!state->lib_state.conn_certs)
     else
       DEBUG(D_tls) debug_printf("TLS: no client certificate specified; okay\n");
 
-  if (state->tls_privatekey && !Expand_check_tlsvar(tls_privatekey, errstr))
+  if (  state->tls_privatekey && !Expand_check_tlsvar(tls_privatekey, errstr)
+     || f.expand_string_forcedfail
+     )
+    {
+    if (f.expand_string_forcedfail)
+      *errstr = US"expansion of tls_privatekey failed";
     return DEFER;
+    }
 
   /* tls_privatekey is optional, defaulting to same file as certificate */
 
@@ -1866,7 +1878,11 @@ if (!state->lib_state.conn_certs)
 			      tls_ocsp_file,
 #endif
 			      errstr)
-       )  ) return rc;
+       )  )
+      {
+      DEBUG(D_tls) debug_printf("load-cert: '%s'\n", *errstr);
+      return rc;
+      }
     }
   }
 else
@@ -2710,11 +2726,12 @@ if ((rc = tls_expand_session_files(state, &dummy_errstr)) != OK)
   {
   /* If the setup of certs/etc failed before handshake, TLS would not have
   been offered.  The best we can do now is abort. */
-  return GNUTLS_E_APPLICATION_ERROR_MIN;
+  DEBUG(D_tls) debug_printf("expansion for SNI-dependent session files failed\n");
+  return GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE;
   }
 
 rc = tls_set_remaining_x509(state, &dummy_errstr);
-if (rc != OK) return GNUTLS_E_APPLICATION_ERROR_MIN;
+if (rc != OK) return GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE;
 
 return 0;
 }
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index db77a1274..3b060cc9c 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -1553,8 +1553,13 @@ else
      )  )
     reexpand_tls_files_for_sni = TRUE;
 
-  if (!expand_check(state->certificate, US"tls_certificate", &expanded, errstr))
+  if (  !expand_check(state->certificate, US"tls_certificate", &expanded, errstr)
+     || f.expand_string_forcedfail)
+    {
+    if (f.expand_string_forcedfail)
+      *errstr = US"expansion of tls_certificate failed";
     return DEFER;
+    }
 
   if (expanded)
     if (state->is_server)
@@ -1622,9 +1627,14 @@ else
       if ((err = tls_add_certfile(sctx, state, expanded, errstr)))
 	return err;
 
-  if (  state->privatekey
-     && !expand_check(state->privatekey, US"tls_privatekey", &expanded, errstr))
+  if (     state->privatekey
+        && !expand_check(state->privatekey, US"tls_privatekey", &expanded, errstr)
+     || f.expand_string_forcedfail)
+    {
+    if (f.expand_string_forcedfail)
+      *errstr = US"expansion of tls_privatekey failed";
     return DEFER;
+    }
 
   /* If expansion was forced to fail, key_expanded will be NULL. If the result
   of the expansion is an empty string, ignore it also, and assume the private
@@ -2201,13 +2211,13 @@ per https://www.openssl.org/docs/manmaster/man3/SSL_client_hello_cb_fn.html
 
 #ifdef EXIM_HAVE_OPENSSL_TLSEXT
 static int
-tls_servername_cb(SSL *s, int *ad ARG_UNUSED, void *arg)
+tls_servername_cb(SSL * s, int * ad ARG_UNUSED, void * arg)
 {
-const char *servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name);
-exim_openssl_state_st *state = (exim_openssl_state_st *) arg;
+const char * servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name);
+exim_openssl_state_st * state = (exim_openssl_state_st *) arg;
 int rc;
 int old_pool = store_pool;
-uschar * dummy_errstr;
+uschar * errstr;
 
 if (!servername)
   return SSL_TLSEXT_ERR_OK;
@@ -2227,7 +2237,7 @@ if (!reexpand_tls_files_for_sni)
 not confident that memcpy wouldn't break some internal reference counting.
 Especially since there's a references struct member, which would be off. */
 
-if (lib_ctx_new(&server_sni, NULL, &dummy_errstr) != OK)
+if (lib_ctx_new(&server_sni, NULL, &errstr) != OK)
   goto bad;
 
 /* Not sure how many of these are actually needed, since SSL object
@@ -2247,8 +2257,8 @@ already exists.  Might even need this selfsame callback, for reneg? */
   SSL_CTX_set_tlsext_servername_arg(server_sni, state);
  }
 
-if (  !init_dh(server_sni, state->dhparam, &dummy_errstr)
-   || !init_ecdh(server_sni, &dummy_errstr)
+if (  !init_dh(server_sni, state->dhparam, &errstr)
+   || !init_ecdh(server_sni, &errstr)
    )
   goto bad;
 
@@ -2267,7 +2277,7 @@ if (state->u_ocsp.server.file)
   {
   uschar * v_certs = tls_verify_certificates;
   if ((rc = setup_certs(server_sni, &v_certs, tls_crl, NULL,
-			&dummy_errstr)) != OK)
+			&errstr)) != OK)
     goto bad;
 
   if (v_certs && *v_certs)
@@ -2276,14 +2286,16 @@ if (state->u_ocsp.server.file)
 
 /* do this after setup_certs, because this can require the certs for verifying
 OCSP information. */
-if ((rc = tls_expand_session_files(server_sni, state, &dummy_errstr)) != OK)
+if ((rc = tls_expand_session_files(server_sni, state, &errstr)) != OK)
   goto bad;
 
 DEBUG(D_tls) debug_printf("Switching SSL context.\n");
 SSL_set_SSL_CTX(s, server_sni);
 return SSL_TLSEXT_ERR_OK;
 
-bad: return SSL_TLSEXT_ERR_ALERT_FATAL;
+bad:
+  log_write(0, LOG_MAIN|LOG_PANIC, "%s", errstr);
+  return SSL_TLSEXT_ERR_ALERT_FATAL;
 }
 #endif /* EXIM_HAVE_OPENSSL_TLSEXT */
 

commit 60b8e1d8c24d1ab487134d8b5fb1e8523f786c33
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Nov 12 20:13:32 2022 +0000

    tidying

diff --git a/src/src/daemon.c b/src/src/daemon.c
index be008c3d4..05ef3bfdd 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -1258,10 +1258,14 @@ if (sz >= sizeof(buf)) return FALSE;
 #ifdef notdef
 debug_printf("addrlen %d\n", msg.msg_namelen);
 #endif
-DEBUG(D_queue_run) debug_printf("%s from addr '%s%.*s'\n", __FUNCTION__,
-  *sa_un.sun_path ? "" : "@",
-  (int)msg.msg_namelen - (*sa_un.sun_path ? 0 : 1),
-  sa_un.sun_path + (*sa_un.sun_path ? 0 : 1));
+DEBUG(D_queue_run)
+  if (msg.msg_namelen > 0)
+    debug_printf("%s from addr '%s%.*s'\n", __FUNCTION__,
+      *sa_un.sun_path ? "" : "@",
+      (int)msg.msg_namelen - (*sa_un.sun_path ? 0 : 1),
+      sa_un.sun_path + (*sa_un.sun_path ? 0 : 1));
+  else
+    debug_printf("%s (from unknown addr)\n", __FUNCTION__);
 
 /* Refuse to handle the item unless the peer has good credentials */
 #ifdef SCM_CREDENTIALS
diff --git a/src/src/expand.c b/src/src/expand.c
index 7bb2e4274..62b4a1890 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -4853,11 +4853,11 @@ while (*s)
 
       switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, flags, TRUE, name, &resetok, NULL))
         {
+	case -1: continue;	/* If skipping, we don't actually do anything */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
-      /*XXX no skipping-optimisation? */
 
       yield = string_append(yield, 3,
 			US"Authentication-Results: ", sub_arg[0], US"; none");
@@ -4944,7 +4944,6 @@ while (*s)
         case 2:
         case 3: goto EXPAND_FAILED;
         }
-      /*XXX no skipping-optimisation? */
 
       if (!sub_arg[1])			/* One argument */
 	{
@@ -5439,15 +5438,12 @@ while (*s)
 
       switch(read_subs(sub_arg, 2, 1, &s, flags, TRUE, name, &resetok, NULL))
         {
+	case -1: continue;	/* If skipping, we don't actually do anything */
         case 1: goto EXPAND_FAILED_CURLY;
         case 2:
         case 3: goto EXPAND_FAILED;
         }
 
-      /* If skipping, we don't actually do anything */
-
-      if (flags & ESI_SKIPPING) continue;
-
       /* Open the file and read it */
 
       if (!(f = Ufopen(sub_arg[0], "rb")))
@@ -6327,6 +6323,7 @@ while (*s)
         save_expand_strings(save_expand_nstring, save_expand_nlength);
 
       /* Read the field & list arguments */
+      /*XXX Could we use read_subs here (and get better efficiency for skipping)? */
 
       for (int i = 0; i < 2; i++)
         {

commit 419ad98ed05e32ccbf1a05549984017e8f0bd79a
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Dec 13 13:38:53 2022 +0000

    GnuTLS: fix cert loading
    
    Broken-by: 520ef00f56ce

diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index f8cc34406..729fb5879 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -379,7 +379,7 @@ Argument:
             the connected host if setting up a client
   errstr    pointer to returned error string
 
-Returns:    OK/DEFER/FAIL
+Returns:    DEFER/FAIL
 */
 
 static int
@@ -392,6 +392,7 @@ return host ? FAIL : DEFER;
 }
 
 
+/* Returns:    DEFER/FAIL */
 static int
 tls_error_gnu(exim_gnutls_state_st * state, const uschar *prefix, int err,
   uschar ** errstr)
@@ -1271,6 +1272,7 @@ DEBUG(D_tls)
   debug_printf("TLS: basic cred init, %s\n", server ? "server" : "client");
 }
 
+/* Returns OK/DEFER/FAIL */
 static int
 creds_load_server_certs(exim_gnutls_state_st * state, const uschar * cert,
   const uschar * pkey, const uschar * ocsp, uschar ** errstr)
@@ -1294,7 +1296,7 @@ while (cfile = string_nextinlist(&clist, &csep, NULL, 0))
 
   if (!(kfile = string_nextinlist(&klist, &ksep, NULL, 0)))
     return tls_error(US"cert/key setup: out of keys", NULL, NULL, errstr);
-  else if ((rc = tls_add_certfile(state, NULL, cfile, kfile, errstr)) != OK)
+  else if ((rc = tls_add_certfile(state, NULL, cfile, kfile, errstr)) > OK)
     return rc;
   else
     {
@@ -1372,7 +1374,7 @@ while (cfile = string_nextinlist(&clist, &csep, NULL, 0))
       }
 #endif /* DISABLE_OCSP */
     }
-return 0;
+return OK;
 }
 
 static int
@@ -1382,7 +1384,7 @@ creds_load_client_certs(exim_gnutls_state_st * state, const host_item * host,
 int rc = tls_add_certfile(state, host, cert, pkey, errstr);
 if (rc > 0) return rc;
 DEBUG(D_tls) debug_printf("TLS: cert/key registered\n");
-return 0;
+return OK;
 }
 
 static int

commit 85f4056d71b45977bf269c7e595386647538d14b
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Dec 13 15:46:01 2022 +0000

    ACL: Permit the "encrypted" condition to be used in a HELO/EHLO ACL

diff --git a/src/src/acl.c b/src/src/acl.c
index 8e1d92457..74b59b0fe 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -223,7 +223,7 @@ static condition_def conditions[] = {
   },
   [ACLC_ENCRYPTED] =		{ US"encrypted",	FALSE, FALSE,
 				  ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START |
-				    ACL_BIT_HELO,
+				    ACL_BIT_CONNECT
   },
 
   [ACLC_ENDPASS] =		{ US"endpass",	TRUE, TRUE,	0 },

commit 5fd353d843a09c53c6767cbc89671a751b719555
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Dec 17 16:20:19 2022 +0000

    Add template lines for alternate DBM libraries, in the teplate Makefile

diff --git a/src/src/EDITME b/src/src/EDITME
index 4fcaeda5b..6344561d6 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -647,6 +647,29 @@ DISABLE_MAL_MKS=yes
 # understand these issues, go with the defaults, which are used by many sites.
 
 
+#------------------------------------------------------------------------------
+# Which DBM library to use.  If you do not specify a specific here, you get
+# the platform default.  Uncomment the pair of lines as preferred.
+# Note: when changing an installation from one DB type to another all the
+# hints-DB files, in spool/db, should be removed.
+
+# gdbm in native mode
+# USE_GDBM = yes
+# DBMLIB = -lgdbm
+
+# gdbm in Berkeley-DB compatibility mode
+# USE_NDBM = yes
+# DBMLIB = -lgdbm -lgdbm_compat
+
+# tdb
+# USE_TDB = yes
+# DBMLIB = -ltdb
+
+# Berkeley DB
+# USE_DB = yes
+# DBMLIB = -ldb
+
+
 #------------------------------------------------------------------------------
 # Although Exim is normally a setuid program, owned by root, it refuses to run
 # local deliveries as root by default. There is a runtime option called

commit 1ed24e36e279c922d3366f6c3144570cc5f54d7a
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Dec 19 21:09:17 2022 +0000

    Fix logging of max-size log line
    
    Broken-by: d12746bc15d8

diff --git a/src/src/log.c b/src/src/log.c
index a01e9c59f..d11b933f9 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -806,7 +806,7 @@ log_write(unsigned int selector, int flags, const char *format, ...)
 {
 int paniclogfd;
 ssize_t written_len;
-gstring gs = { .size = LOG_BUFFER_SIZE-1, .ptr = 0, .s = log_buffer };
+gstring gs = { .size = LOG_BUFFER_SIZE-2, .ptr = 0, .s = log_buffer };
 gstring * g;
 va_list ap;
 
@@ -952,11 +952,10 @@ DEBUG(D_any|D_v)
     }
   va_end(ap);
 
-  g->size = LOG_BUFFER_SIZE;
   g = string_catn(g, US"\n", 1);
   debug_printf("%s", string_from_gstring(g));
 
-  gs.size = LOG_BUFFER_SIZE-1;	/* Having used the buffer for debug output, */
+  gs.size = LOG_BUFFER_SIZE-2;	/* Having used the buffer for debug output, */
   gs.ptr = 0;			/* reset it for the real use. */
   gs.s = log_buffer;
   }
@@ -1038,6 +1037,8 @@ if (  flags & LOG_RECIPIENTS
     }
   }
 
+/* actual size, now we are placing the newline (and space for NUL) */
+gs.size = LOG_BUFFER_SIZE;
 g = string_catn(g, US"\n", 1);
 string_from_gstring(g);
 

commit d439520cf2ccd61b0a2190bb331b1dded18547b8
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Dec 20 14:38:26 2022 +0000

    cppcheck sliencing

diff --git a/src/src/acl.c b/src/src/acl.c
index 74b59b0fe..5ab674776 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -2550,6 +2550,7 @@ else switch(mode)
     anchor = NULL; /* silence an "unused" complaint */
     log_write(0, LOG_MAIN|LOG_PANIC_DIE,
       "internal ACL error: unknown ratelimit mode %d", mode);
+    /*NOTREACHED*/
     break;
   }
 
diff --git a/src/src/deliver.c b/src/src/deliver.c
index c4fce4602..c5e00eaef 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -342,13 +342,7 @@ if (Ustrstr(filename, US"/../"))
 for (int i = 2; i > 0; i--)
   {
   int fd = Uopen(filename,
-#ifdef O_CLOEXEC
-    O_CLOEXEC |
-#endif
-#ifdef O_NOFOLLOW
-    O_NOFOLLOW |
-#endif
-		O_WRONLY|O_APPEND|O_CREAT, mode);
+		EXIM_CLOEXEC | EXIM_NOFOLLOW | O_WRONLY|O_APPEND|O_CREAT, mode);
   if (fd >= 0)
     {
     /* Set the close-on-exec flag and change the owner to the exim uid/gid (this
@@ -4705,17 +4699,13 @@ all pipes, so I do not see a reason to use non-blocking IO here
     {
     uschar * fname = spool_fname(US"input", message_subdir, message_id, US"-D");
 
-    if ((deliver_datafile = Uopen(fname,
-#ifdef O_CLOEXEC
-					O_CLOEXEC |
-#endif
-					O_RDWR | O_APPEND, 0)) < 0)
+    if (  (deliver_datafile = Uopen(fname, EXIM_CLOEXEC | O_RDWR | O_APPEND, 0))
+	< 0)
       log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Failed to reopen %s for remote "
         "parallel delivery: %s", fname, strerror(errno));
     }
 
-    /* Set the close-on-exec flag */
-#ifndef O_CLOEXEC
+#ifndef O_CLOEXEC			/* Set the close-on-exec flag */
     (void)fcntl(deliver_datafile, F_SETFD, fcntl(deliver_datafile, F_GETFD) |
       FD_CLOEXEC);
 #endif
@@ -5749,14 +5739,8 @@ Otherwise it might be needed again. */
   uschar * fname = spool_fname(US"input", message_subdir, id, US"-J");
   FILE * jread;
 
-  if (  (journal_fd = Uopen(fname, O_RDWR|O_APPEND
-#ifdef O_CLOEXEC
-				    | O_CLOEXEC
-#endif
-#ifdef O_NOFOLLOW
-				    | O_NOFOLLOW
-#endif
-	, SPOOL_MODE)) >= 0
+  if (  (journal_fd = Uopen(fname,
+	      O_RDWR|O_APPEND | EXIM_CLOEXEC | EXIM_NOFOLLOW, SPOOL_MODE)) >= 0
      && lseek(journal_fd, 0, SEEK_SET) == 0
      && (jread = fdopen(journal_fd, "rb"))
      )
@@ -7154,10 +7138,7 @@ if (addr_local || addr_remote)
     uschar * fname = spool_fname(US"input", message_subdir, id, US"-J");
 
     if ((journal_fd = Uopen(fname,
-#ifdef O_CLOEXEC
-			O_CLOEXEC |
-#endif
-			O_WRONLY|O_APPEND|O_CREAT|O_EXCL, SPOOL_MODE)) < 0)
+	      EXIM_CLOEXEC | O_WRONLY|O_APPEND|O_CREAT|O_EXCL, SPOOL_MODE)) < 0)
       {
       log_write(0, LOG_MAIN|LOG_PANIC, "Couldn't open journal file %s: %s",
 	fname, strerror(errno));
diff --git a/src/src/exim.c b/src/src/exim.c
index 35f4ae4f7..dc082f392 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -841,6 +841,7 @@ exim_fail(const char * fmt, ...)
 va_list ap;
 va_start(ap, fmt);
 vfprintf(stderr, fmt, ap);
+va_end(ap);
 exit(EXIT_FAILURE);
 }
 
@@ -1224,13 +1225,11 @@ DEBUG(D_any)
 #if defined(__clang__)
   g = string_fmt_append(g, "Compiler: CLang [%s]\n", __clang_version__);
 #elif defined(__GNUC__)
-  g = string_fmt_append(g, "Compiler: GCC [%s]\n",
 # ifdef __VERSION__
-      __VERSION__
+  g = string_fmt_append(g, "Compiler: GCC [%s]\n", __VERSION__);
 # else
-      "? unknown version ?"
+  g = string_fmt_append(g, "Compiler: GCC [%s]\n", "? unknown version ?";
 # endif
-      );
 #else
   g = string_cat(g, US"Compiler: <unknown>\n");
 #endif
diff --git a/src/src/hash.c b/src/src/hash.c
index 10af1b43d..95860fc50 100644
--- a/src/src/hash.c
+++ b/src/src/hash.c
@@ -408,7 +408,7 @@ Returns:    nothing
 */
 
 static void
-native_sha1_end(sha1 *base, const uschar *text, int length, uschar *digest)
+native_sha1_end(sha1 * base, const uschar * text, int length, uschar * digest)
 {
 uschar work[64];
 
@@ -426,7 +426,7 @@ out to 64, process it, and then set up the final chunk as 56 bytes of
 padding. If it has less than 56 bytes, we pad it out to 56 bytes as the
 final chunk. */
 
-memcpy(work, text, length);
+if (length) memcpy(work, text, length);
 work[length] = 0x80;
 
 if (length > 55)
diff --git a/src/src/host.c b/src/src/host.c
index ecdc6d681..8d53eb3de 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -912,7 +912,7 @@ Returns:     the number of ints used
 */
 
 int
-host_aton(const uschar *address, int *bin)
+host_aton(const uschar * address, int * bin)
 {
 int x[4];
 int v4offset = 0;
@@ -924,13 +924,10 @@ supported. */
 
 if (Ustrchr(address, ':') != NULL)
   {
-  const uschar *p = address;
-  const uschar *component[8];
+  const uschar * p = address;
+  const uschar * component[8];
   BOOL ipv4_ends = FALSE;
-  int ci = 0;
-  int nulloffset = 0;
-  int v6count = 8;
-  int i;
+  int ci = 0, nulloffset = 0, v6count = 8, i;
 
   /* If the address starts with a colon, it will start with two colons.
   Just lose the first one, which will leave a null first component. */
@@ -942,7 +939,7 @@ if (Ustrchr(address, ':') != NULL)
   overlooked; to guard against that happening again, check here and crash if
   there are too many components. */
 
-  while (*p != 0 && *p != '%')
+  while (*p && *p != '%')
     {
     int len = Ustrcspn(p, ":%");
     if (len == 0) nulloffset = ci;
diff --git a/src/src/macros.h b/src/src/macros.h
index 243c1e5a0..a631877a1 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -1125,4 +1125,15 @@ typedef unsigned mcs_flags;
 #define MCS_AT_SPECIAL		BIT(2)	/* recognize @, @[], etc. */
 #define MCS_CACHEABLE		BIT(3)	/* no dynamic expansions used for pattern */
 
+/* Flags for open() */
+#ifdef O_CLOEXEC
+# define EXIM_CLOEXEC O_CLOEXEC
+#else
+# define EXIM_CLOEXEC 0
+#endif
+#ifdef O_NOFOLLOW
+# define EXIM_NOFOLLOW O_NOFOLLOW
+#else
+# define EXIM_NOFOLLOW 0
+#endif
 /* End of macros.h */
diff --git a/src/src/os.c b/src/src/os.c
index fc29f1766..87a336935 100644
--- a/src/src/os.c
+++ b/src/src/os.c
@@ -11,6 +11,8 @@
 # include <signal.h>
 # include <stdio.h>
 # include <time.h>
+#else
+# define DEBUG(x) if (debug_selector & (x))
 #endif
 
 #ifndef CS
@@ -50,9 +52,9 @@ sigemptyset(&(act.sa_mask));
 act.sa_flags = SA_RESTART;
 sigaction(sig, &act, NULL);
 
-#ifdef STAND_ALONE
+# ifdef STAND_ALONE
 printf("Used SA_RESTART\n");
-#endif
+# endif
 
 /* SunOS4 and Ultrix default to non-interruptable signals, with SV_INTERRUPT
 for making them interruptable. This seems to be a dying fashion. */
@@ -60,9 +62,9 @@ for making them interruptable. This seems to be a dying fashion. */
 #elif defined SV_INTERRUPT
 signal(sig, handler);
 
-#ifdef STAND_ALONE
+# ifdef STAND_ALONE
 printf("Used default signal()\n");
-#endif
+# endif
 
 
 /* If neither SA_RESTART nor SV_INTERRUPT is available we don't know how to
@@ -71,9 +73,9 @@ set up a restarting signal, so simply suppress the facility. */
 #else
 signal(sig, SIG_IGN);
 
-#ifdef STAND_ALONE
+# ifdef STAND_ALONE
 printf("Used SIG_IGN\n");
-#endif
+# endif
 
 #endif
 }
@@ -361,9 +363,9 @@ here as there is the -hal variant, and other systems might follow this road one
 day. */
 
 #if !defined(OS_LOAD_AVERAGE) && defined(HAVE_KSTAT)
-#define OS_LOAD_AVERAGE
+# define OS_LOAD_AVERAGE
 
-#include <kstat.h>
+# include <kstat.h>
 
 int
 os_getloadavg(void)
@@ -397,7 +399,7 @@ return avg;
 #if !defined(OS_LOAD_AVERAGE) && defined(HAVE_DEV_KMEM)
 #define OS_LOAD_AVERAGE
 
-#include <nlist.h>
+# include <nlist.h>
 
 static int  avg_kd = -1;
 static long avg_offset;
@@ -481,7 +483,7 @@ Returns:      a chain of ip_address_items, each pointing to a textual
 
 #ifdef HAVE_GETIFADDRS
 
-#include <ifaddrs.h>
+# include <ifaddrs.h>
 
 ip_address_item *
 os_common_find_running_interfaces(void)
@@ -630,13 +632,13 @@ what we want to know. */
 
 if ((vs = socket(FAMILY, SOCK_DGRAM, 0)) < 0)
   {
-  #if HAVE_IPV6
+#if HAVE_IPV6
   DEBUG(D_interface)
     debug_printf("Unable to create IPv6 socket to find interface addresses:\n  "
       "error %d %s\nTrying for an IPv4 socket\n", errno, strerror(errno));
   vs = socket(AF_INET, SOCK_DGRAM, 0);
   if (vs < 0)
-  #endif
+#endif
   log_write(0, LOG_PANIC_DIE, "Unable to create IPv4 socket to find interface "
     "addresses: %d %s", errno, strerror(errno));
   }
@@ -816,7 +818,7 @@ programmer creates their own structs. */
 
 #if !defined(OS_GET_DNS_RESOLVER_RES) && !defined(COMPILE_UTILITY)
 
-#include <resolv.h>
+# include <resolv.h>
 
 /* confirmed that res_state is typedef'd as a struct* on BSD and Linux, will
 find out how unportable it is on other OSes, but most resolver implementations
diff --git a/src/src/retry.c b/src/src/retry.c
index a34bf80ca..1897c782f 100644
--- a/src/src/retry.c
+++ b/src/src/retry.c
@@ -518,8 +518,8 @@ Returns:        nothing
 */
 
 void
-retry_update(address_item **addr_defer, address_item **addr_failed,
-  address_item **addr_succeed)
+retry_update(address_item ** addr_defer, address_item ** addr_failed,
+  address_item ** addr_succeed)
 {
 open_db dbblock;
 open_db *dbm_file = NULL;
@@ -533,11 +533,10 @@ to the failed chain if they have timed out. */
 
 for (int i = 0; i < 3; i++)
   {
-  address_item *endaddr, *addr;
-  address_item *last_first = NULL;
-  address_item **paddr = i==0 ? addr_succeed :
-    i==1 ? addr_failed : addr_defer;
-  address_item **saved_paddr = NULL;
+  address_item * endaddr, *addr;
+  address_item * last_first = NULL;
+  address_item ** paddr = i==0 ? addr_succeed : i==1 ? addr_failed : addr_defer;
+  address_item ** saved_paddr = NULL;
 
   DEBUG(D_retry) debug_printf("%s addresses:\n",
     i == 0 ? "Succeeded" : i == 1 ? "Failed" : "Deferred");
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 6c043d434..1cfcc0404 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -2680,32 +2680,32 @@ if (!f.sender_host_unknown)
 
 #if !HAVE_IPV6 && !defined(NO_IP_OPTIONS)
 
-  #ifdef GLIBC_IP_OPTIONS
-    #if (!defined __GLIBC__) || (__GLIBC__ < 2)
-    #define OPTSTYLE 1
-    #else
-    #define OPTSTYLE 2
-    #endif
-  #elif defined DARWIN_IP_OPTIONS
-    #define OPTSTYLE 2
-  #else
-    #define OPTSTYLE 3
-  #endif
+# ifdef GLIBC_IP_OPTIONS
+#  if (!defined __GLIBC__) || (__GLIBC__ < 2)
+#   define OPTSTYLE 1
+#  else
+#   define OPTSTYLE 2
+#  endif
+# elif defined DARWIN_IP_OPTIONS
+# define OPTSTYLE 2
+# else
+# define OPTSTYLE 3
+# endif
 
   if (!host_checking && !f.sender_host_notsocket)
     {
-    #if OPTSTYLE == 1
+# if OPTSTYLE == 1
     EXIM_SOCKLEN_T optlen = sizeof(struct ip_options) + MAX_IPOPTLEN;
     struct ip_options *ipopt = store_get(optlen, GET_UNTAINTED);
-    #elif OPTSTYLE == 2
+# elif OPTSTYLE == 2
     struct ip_opts ipoptblock;
     struct ip_opts *ipopt = &ipoptblock;
     EXIM_SOCKLEN_T optlen = sizeof(ipoptblock);
-    #else
+# else
     struct ipoption ipoptblock;
     struct ipoption *ipopt = &ipoptblock;
     EXIM_SOCKLEN_T optlen = sizeof(ipoptblock);
-    #endif
+# endif
 
     /* Occasional genuine failures of getsockopt() have been seen - for
     example, "reset by peer". Therefore, just log and give up on this
@@ -2735,19 +2735,19 @@ if (!f.sender_host_unknown)
 
     else if (optlen > 0)
       {
-      uschar *p = big_buffer;
-      uschar *pend = big_buffer + big_buffer_size;
-      uschar *adptr;
+      uschar * p = big_buffer;
+      uschar * pend = big_buffer + big_buffer_size;
+      uschar * adptr;
       int optcount;
       struct in_addr addr;
 
-      #if OPTSTYLE == 1
-      uschar *optstart = US (ipopt->__data);
-      #elif OPTSTYLE == 2
-      uschar *optstart = US (ipopt->ip_opts);
-      #else
-      uschar *optstart = US (ipopt->ipopt_list);
-      #endif
+# if OPTSTYLE == 1
+      uschar * optstart = US (ipopt->__data);
+# elif OPTSTYLE == 2
+      uschar * optstart = US (ipopt->ip_opts);
+# else
+      uschar * optstart = US (ipopt->ipopt_list);
+# endif
 
       DEBUG(D_receive) debug_printf("IP options exist\n");
 
@@ -2758,59 +2758,65 @@ if (!f.sender_host_unknown)
         switch (*opt)
           {
           case IPOPT_EOL:
-          opt = NULL;
-          break;
+	    opt = NULL;
+	    break;
 
           case IPOPT_NOP:
-          opt++;
-          break;
+	    opt++;
+	    break;
 
           case IPOPT_SSRR:
           case IPOPT_LSRR:
-          if (!string_format(p, pend-p, " %s [@%s",
-               (*opt == IPOPT_SSRR)? "SSRR" : "LSRR",
-               #if OPTSTYLE == 1
-               inet_ntoa(*((struct in_addr *)(&(ipopt->faddr))))))
-               #elif OPTSTYLE == 2
-               inet_ntoa(ipopt->ip_dst)))
-               #else
-               inet_ntoa(ipopt->ipopt_dst)))
-               #endif
-            {
-            opt = NULL;
-            break;
-            }
+	    if (!
+# if OPTSTYLE == 1
+	         string_format(p, pend-p, " %s [@%s",
+		 (*opt == IPOPT_SSRR)? "SSRR" : "LSRR",
+		 inet_ntoa(*((struct in_addr *)(&(ipopt->faddr)))))
+# elif OPTSTYLE == 2
+	         string_format(p, pend-p, " %s [@%s",
+		 (*opt == IPOPT_SSRR)? "SSRR" : "LSRR",
+		 inet_ntoa(ipopt->ip_dst))
+# else
+	         string_format(p, pend-p, " %s [@%s",
+		 (*opt == IPOPT_SSRR)? "SSRR" : "LSRR",
+		 inet_ntoa(ipopt->ipopt_dst))
+# endif
+	      )
+	      {
+	      opt = NULL;
+	      break;
+	      }
 
-          p += Ustrlen(p);
-          optcount = (opt[1] - 3) / sizeof(struct in_addr);
-          adptr = opt + 3;
-          while (optcount-- > 0)
-            {
-            memcpy(&addr, adptr, sizeof(addr));
-            if (!string_format(p, pend - p - 1, "%s%s",
-                  (optcount == 0)? ":" : "@", inet_ntoa(addr)))
-              {
-              opt = NULL;
-              break;
-              }
-            p += Ustrlen(p);
-            adptr += sizeof(struct in_addr);
-            }
-          *p++ = ']';
-          opt += opt[1];
-          break;
+	    p += Ustrlen(p);
+	    optcount = (opt[1] - 3) / sizeof(struct in_addr);
+	    adptr = opt + 3;
+	    while (optcount-- > 0)
+	      {
+	      memcpy(&addr, adptr, sizeof(addr));
+	      if (!string_format(p, pend - p - 1, "%s%s",
+		    (optcount == 0)? ":" : "@", inet_ntoa(addr)))
+		{
+		opt = NULL;
+		break;
+		}
+	      p += Ustrlen(p);
+	      adptr += sizeof(struct in_addr);
+	      }
+	    *p++ = ']';
+	    opt += opt[1];
+	    break;
 
           default:
-            {
-            if (pend - p < 4 + 3*opt[1]) { opt = NULL; break; }
-            Ustrcat(p, "[ ");
-            p += 2;
-            for (int i = 0; i < opt[1]; i++)
-              p += sprintf(CS p, "%2.2x ", opt[i]);
-            *p++ = ']';
-            }
-          opt += opt[1];
-          break;
+	      {
+	      if (pend - p < 4 + 3*opt[1]) { opt = NULL; break; }
+	      Ustrcat(p, "[ ");
+	      p += 2;
+	      for (int i = 0; i < opt[1]; i++)
+		p += sprintf(CS p, "%2.2x ", opt[i]);
+	      *p++ = ']';
+	      }
+	    opt += opt[1];
+	    break;
           }
 
       *p = 0;
diff --git a/src/src/spool_in.c b/src/src/spool_in.c
index 6d6651f57..e785f695b 100644
--- a/src/src/spool_in.c
+++ b/src/src/spool_in.c
@@ -64,13 +64,7 @@ for (int i = 0; i < 2; i++)
    * No -D file inside the spool area should be a symlink.
    */
   if ((fd = Uopen(fname,
-#ifdef O_CLOEXEC
-		      O_CLOEXEC |
-#endif
-#ifdef O_NOFOLLOW
-		      O_NOFOLLOW |
-#endif
-		      O_RDWR | O_APPEND, 0)) >= 0)
+		  EXIM_CLOEXEC | EXIM_NOFOLLOW | O_RDWR | O_APPEND, 0)) >= 0)
     break;
   save_errno = errno;
   if (errno == ENOENT)
diff --git a/src/src/string.c b/src/src/string.c
index 2cb419517..b30673c04 100644
--- a/src/src/string.c
+++ b/src/src/string.c
@@ -1783,7 +1783,7 @@ while (fgets(CS buffer, sizeof(buffer), stdin) != NULL)
   int llflag = 0;
   int n = 0;
   int count;
-  int countset = 0;
+  BOOL countset = FASE;
   uschar format[256];
   uschar outbuf[256];
   uschar *s;
@@ -1825,7 +1825,7 @@ while (fgets(CS buffer, sizeof(buffer), stdin) != NULL)
     else if (Ustrcmp(ss, "*") == 0)
       {
       args[n++] = (void *)(&count);
-      countset = 1;
+      countset = TRUE;
       }
 
     else

commit c7bec9723a721f566c67df0a526e4de18b723659
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Dec 18 18:44:54 2022 +0000

    Rework "compiler quietening"
    
    This partially reverts commit 0da41dc541d0f2536f9d2afc7188e9dfb97b0c09.

diff --git a/src/src/lookups/ldap.c b/src/src/lookups/ldap.c
index ba77c98c2..b2ad3bbbc 100644
--- a/src/src/lookups/ldap.c
+++ b/src/src/lookups/ldap.c
@@ -1470,20 +1470,27 @@ t = quoted = store_get_quoted(len + count + 1, s, idx);
 /* Handle plain quote_ldap */
 
 if (!dn)
-  for (; c = *s++; *t++ = c)
+  {
+  while ((c = *s++))
+    {
     if (!isalnum(c))
       {
       if (Ustrchr(LDAP_QUOTE, c) != NULL)
         {
         sprintf(CS t, "%%5C%02X", c);        /* e.g. * => %5C2A */
         t += 5;
+        continue;
         }
-      else if (Ustrchr(URL_NONQUOTE, c) == NULL)  /* e.g. ] => %5D */
+      if (Ustrchr(URL_NONQUOTE, c) == NULL)  /* e.g. ] => %5D */
         {
         sprintf(CS t, "%%%02X", c);
         t += 3;
+        continue;
         }
       }
+    *t++ = c;                                /* unquoted character */
+    }
+  }
 
 /* Handle quote_ldap_dn */
 
@@ -1513,7 +1520,7 @@ else
       {
       if (Ustrchr(LDAP_DN_QUOTE, c) != NULL)
         {
-        Ustrcpy(t, US"%5C");			/* insert \ where needed */
+        memcpy(t, US"%5C", 3);			/* insert \ where needed */
         t += 3;					/* fall through to check URL */
         }
       if (Ustrchr(URL_NONQUOTE, c) == NULL)  /* e.g. ] => %5D */
@@ -1530,7 +1537,7 @@ else
 
   while (*ss++)
     {
-    Ustrcpy(t, US"%5C%20");
+    memcpy(t, US"%5C%20", 6);
     t += 6;
     }
   }

commit 0762e1a4d6de4b7b0206314302297c9dd6d7ae73
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Dec 23 18:02:25 2022 +0000

    Expand max_rcpt option on smtp transport.  Bug 2946

diff --git a/src/src/deliver.c b/src/src/deliver.c
index c5e00eaef..ca31df587 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -4290,10 +4290,14 @@ So look out for the place it gets used.
     }
 
   /* Get the maximum it can handle in one envelope, with zero meaning
-  unlimited, which is forced for the MUA wrapper case. */
-
-  address_count_max = tp->max_addresses;
-  if (address_count_max == 0 || mua_wrapper) address_count_max = 999999;
+  unlimited, which is forced for the MUA wrapper case and if the
+  value could vary depending on the messages.
+  For those, we only split (below) by (tpt,dest,erraddr,hdrs) and rely on the
+  transport splitting further by max_rcp.  So we potentially lose some
+  parallellism. */
+
+  address_count_max = mua_wrapper || Ustrchr(tp->max_addresses, '$')
+    ? UNLIMITED_ADDRS : expand_max_rcpt(tp->max_addresses);
 
 
   /************************************************************************/
diff --git a/src/src/functions.h b/src/src/functions.h
index 3ca346c04..1817144ea 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -1307,6 +1307,14 @@ debug_printf("cmdlog: '%s'\n", client_cmd_log ? client_cmd_log->s : US"(unset)")
 
 
 
+static inline int
+expand_max_rcpt(const uschar * str_max_rcpt)
+{
+const uschar * s = expand_cstring(str_max_rcpt);
+int res;
+return !s || !*s || (res = Uatoi(s)) == 0 ? UNLIMITED_ADDRS : res;
+}
+
 # endif	/* !COMPILE_UTILITY */
 
 /******************************************************************************/
@@ -1314,6 +1322,6 @@ debug_printf("cmdlog: '%s'\n", client_cmd_log ? client_cmd_log->s : US"(unset)")
 
 #endif  /* _FUNCTIONS_H_ */
 
-/* vi: aw
+/* vi: aw ai sw=2
 */
 /* End of functions.h */
diff --git a/src/src/globals.c b/src/src/globals.c
index e5b72592f..efe34902a 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -1592,7 +1592,7 @@ transport_instance  transport_defaults = {
     /* All non-mentioned elements zero/NULL/FALSE */
     .batch_max =		1,
     .multi_domain =		TRUE,
-    .max_addresses =		100,
+    .max_addresses =		US"100",
     .connection_max_messages =	500,
     .uid =			(uid_t)(-1),
     .gid =			(gid_t)(-1),
diff --git a/src/src/macros.h b/src/src/macros.h
index a631877a1..585067fc9 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -1136,4 +1136,8 @@ typedef unsigned mcs_flags;
 #else
 # define EXIM_NOFOLLOW 0
 #endif
+
+/* A big number for (effectively) unlimited envelope addresses */
+#define UNLIMITED_ADDRS		999999
+
 /* End of macros.h */
diff --git a/src/src/structs.h b/src/src/structs.h
index e1d93a943..eae66e88d 100644
--- a/src/src/structs.h
+++ b/src/src/structs.h
@@ -183,7 +183,7 @@ typedef struct transport_instance {
   uschar *expand_multi_domain;    /* )                                  */
   BOOL    multi_domain;           /* )                                  */
   BOOL    overrides_hosts;        /* ) Used only for remote transports  */
-  int     max_addresses;          /* )                                  */
+  uschar *max_addresses;          /* )                                  */
   int     connection_max_messages;/* )                                  */
                                   /**************************************/
   BOOL    deliver_as_creator;     /* Used only by pipe at present */
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index ed5f83b3e..c5951832b 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -114,7 +114,7 @@ optionlist smtp_transport_options[] = {
   { "interface",            opt_stringptr, LOFF(interface) },
   { "keepalive",            opt_bool,	   LOFF(keepalive) },
   { "lmtp_ignore_quota",    opt_bool,	   LOFF(lmtp_ignore_quota) },
-  { "max_rcpt",             opt_int | opt_public,
+  { "max_rcpt",             opt_stringptr | opt_public,
       OPT_OFF(transport_instance, max_addresses) },
   { "message_linelength_limit", opt_int,   LOFF(message_linelength_limit) },
   { "multi_domain",         opt_expand_bool | opt_public,
@@ -2121,8 +2121,9 @@ sx->dane_required =
   verify_check_given_host(CUSS &ob->hosts_require_dane, sx->conn_args.host) == OK;
 #endif
 
-if ((sx->max_mail = sx->conn_args.tblock->connection_max_messages) == 0) sx->max_mail = 999999;
-if ((sx->max_rcpt = sx->conn_args.tblock->max_addresses) == 0)           sx->max_rcpt = 999999;
+if ((sx->max_mail = sx->conn_args.tblock->connection_max_messages) == 0)
+  sx->max_mail = UNLIMITED_ADDRS;
+sx->max_rcpt = expand_max_rcpt(sx->conn_args.tblock->max_addresses);
 sx->igquotstr = US"";
 if (!sx->helo_data) sx->helo_data = ob->helo_data;
 
@@ -2819,8 +2820,9 @@ if (tls_out.active.sock >= 0)
 #ifdef EXPERIMMENTAL_ESMTP_LIMITS
   /* As we are about to send another EHLO, forget any LIMITS received so far. */
   sx->peer_limit_mail = sx->peer_limit_rcpt = sx->peer_limit_rcptdom = 0;
-  if ((sx->max_mail = sx->conn_args.tblock->connection_max_message) == 0) sx->max_mail = 999999;
-  if ((sx->max_rcpt = sx->conn_args.tblock->max_addresses) == 0)          sx->max_rcpt = 999999;
+  if ((sx->max_mail = sx->conn_args.tblock->connection_max_message) == 0)
+    sx->max_mail = UNLIMITED_ADDRS;
+  sx->max_rcpt = expand_max_rcpt(sx->conn_args.tblock->max_addresses);
   sx->single_rcpt_domain = FALSE;
 #endif
 

commit 2b7e98456504911562b1b5aca7fa94492bbe5204
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Dec 25 21:05:42 2022 +0000

    FreeBSD: fix notifier socket use

diff --git a/src/src/daemon.c b/src/src/daemon.c
index 05ef3bfdd..d3fec42ee 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -45,6 +45,9 @@ static smtp_slot *smtp_slots = NULL;
 
 static BOOL  write_pid = TRUE;
 
+#ifndef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
+static uschar * notifier_socket_name;
+#endif
 
 
 /*************************************************
@@ -129,15 +132,14 @@ if (smtp_out) smtp_printf("421 %s\r\n", FALSE, smtp_msg);
 /*************************************************
 *************************************************/
 
-#ifndef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
 static void
 unlink_notifier_socket(void)
 {
-uschar * s = expand_string(notifier_socket);
-DEBUG(D_any) debug_printf("unlinking notifier socket %s\n", s);
-Uunlink(s);
-}
+#ifndef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
+DEBUG(D_any) debug_printf("unlinking notifier socket %s\n", notifier_socket_name);
+Uunlink(notifier_socket_name);
 #endif
+}
 
 
 static void
@@ -148,9 +150,6 @@ if (daemon_notifier_fd >= 0)
   {
   (void) close(daemon_notifier_fd);
   daemon_notifier_fd = -1;
-#ifndef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
-  unlink_notifier_socket();
-#endif
   }
 
 for (int i = 0; i < listen_socket_count; i++) (void) close(fd_polls[i].fd);
@@ -1105,9 +1104,7 @@ if (daemon_notifier_fd >= 0)
   {
   close(daemon_notifier_fd);
   daemon_notifier_fd = -1;
-#ifndef EXIM_HAVE_ABSTRACT_UNIX_SOCKETS
   unlink_notifier_socket();
-#endif
   }
 
 if (f.running_in_test_harness || write_pid)
@@ -1143,7 +1140,7 @@ return offsetof(struct sockaddr_un, sun_path) + 1
 #else
 *sname = string_sprintf("%s/p_%d", spool_directory, getpid());
 return offsetof(struct sockaddr_un, sun_path)
-  + snprintf(sup->sun_path, sizeof(sup->sun_path), "%s", sname);
+  + snprintf(sup->sun_path, sizeof(sup->sun_path), "%s", CS *sname);
 #endif
 }
 
@@ -1154,11 +1151,12 @@ daemon_notifier_sockname(struct sockaddr_un * sup)
 sup->sun_path[0] = 0;  /* Abstract local socket addr - Linux-specific? */
 return offsetof(struct sockaddr_un, sun_path) + 1
   + snprintf(sup->sun_path+1, sizeof(sup->sun_path)-1, "%s",
-              expand_string(notifier_socket));
+              CS expand_string(notifier_socket));
 #else
+notifier_socket_name = expand_string(notifier_socket);
 return offsetof(struct sockaddr_un, sun_path)
   + snprintf(sup->sun_path, sizeof(sup->sun_path), "%s",
-              expand_string(notifier_socket));
+              CS notifier_socket_name);
 #endif
 }
 
@@ -1260,10 +1258,17 @@ debug_printf("addrlen %d\n", msg.msg_namelen);
 #endif
 DEBUG(D_queue_run)
   if (msg.msg_namelen > 0)
-    debug_printf("%s from addr '%s%.*s'\n", __FUNCTION__,
-      *sa_un.sun_path ? "" : "@",
-      (int)msg.msg_namelen - (*sa_un.sun_path ? 0 : 1),
-      sa_un.sun_path + (*sa_un.sun_path ? 0 : 1));
+    {
+    BOOL abstract = !*sa_un.sun_path;
+    char * name = sa_un.sun_path + (abstract ? 1 : 0);
+    int namelen =  (int)msg.msg_namelen - abstract ? 1 : 0;
+    if (*name)
+      debug_printf("%s from addr '%s%.*s'\n", __FUNCTION__,
+	abstract ? "@" : "",
+	namelen, name);
+    else
+      debug_printf("%s (from unknown addr)\n", __FUNCTION__);
+    }
   else
     debug_printf("%s (from unknown addr)\n", __FUNCTION__);
 
@@ -2673,6 +2678,7 @@ for (;;)
     log_write(0, LOG_MAIN, "pid %d: SIGHUP received: re-exec daemon",
       getpid());
     close_daemon_sockets(daemon_notifier_fd, fd_polls, listen_socket_count);
+    unlink_notifier_socket();
     ALARM_CLR(0);
     signal(SIGHUP, SIG_IGN);
     sighup_argv[0] = exim_path;

commit 1d38781da934809e6ce0b8c3718c4b3bccdfe1d2
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Dec 28 19:39:06 2022 +0000

    Fix recursion on dns_again_means_nonexist. Bug 2911

diff --git a/src/src/dns.c b/src/src/dns.c
index 4e01d8661..2355409ec 100644
--- a/src/src/dns.c
+++ b/src/src/dns.c
@@ -802,6 +802,7 @@ dns_basic_lookup(dns_answer * dnsa, const uschar * name, int type)
 int rc;
 #ifndef STAND_ALONE
 const uschar * save_domain;
+static BOOL try_again_recursion = FALSE;
 #endif
 
 /* DNS lookup failures of any kind are cached in a tree. This is mainly so that
@@ -906,11 +907,22 @@ if (dnsa->answerlen < 0) switch (h_errno)
 
     /* Cut this out for various test programs */
 #ifndef STAND_ALONE
+    if (try_again_recursion)
+      {
+      log_write(0, LOG_MAIN|LOG_PANIC,
+	"dns_again_means_nonexist recursion seen for %s (assuming nonexist)",
+	name);
+      return dns_fail_return(name, type, dns_expire_from_soa(dnsa, type), DNS_NOMATCH);
+      }
+
+    try_again_recursion = TRUE;
     save_domain = deliver_domain;
     deliver_domain = string_copy(name);  /* set $domain */
     rc = match_isinlist(name, CUSS &dns_again_means_nonexist, 0,
       &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL);
     deliver_domain = save_domain;
+    try_again_recursion = FALSE;
+
     if (rc != OK)
       {
       DEBUG(D_dns) debug_printf("returning DNS_AGAIN\n");

commit 6fc54bd18aa7a51e11dce5a905e754cedb526230
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Dec 29 00:50:50 2022 +0000

    Debug: list searching

diff --git a/src/src/match.c b/src/src/match.c
index 15209f84a..91a49c0f0 100644
--- a/src/src/match.c
+++ b/src/src/match.c
@@ -430,9 +430,9 @@ int yield = OK;
 unsigned int * original_cache_bits = *cache_ptr;
 BOOL include_unknown = FALSE, ignore_unknown = FALSE,
       include_defer = FALSE, ignore_defer = FALSE;
-const uschar *list;
-uschar *sss;
-uschar *ot = NULL;
+const uschar * list;
+uschar * sss;
+uschar * ot = NULL;
 BOOL textonly_re;
 
 /* Save time by not scanning for the option name when we don't need it. */
@@ -514,6 +514,11 @@ HDEBUG(D_any) if (!ot)
   gstring_release_unused(g);
   ot = string_from_gstring(g);
   }
+HDEBUG(D_lists)
+  {
+  debug_printf_indent("%s\n", ot);
+  expand_level++;
+  }
 
 /* Now scan the list and process each item in turn, until one of them matches,
 or we hit an error. */
@@ -522,6 +527,8 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
   {
   uschar * ss = sss;
 
+  HDEBUG(D_lists) debug_printf_indent("list element: %s\n", ss);
+
   /* Address lists may contain +caseful, to restore caseful matching of the
   local part. We have to know the layout of the control block, unfortunately.
   The lower cased address is in a temporary buffer, so we just copy the local
@@ -605,14 +612,15 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
     {
     if (*ss == '+' && anchorptr)
       {
-      int bits = 0;
-      int offset = 0;
-      int shift = 0;
-      unsigned int *use_cache_bits = original_cache_bits;
-      uschar *cached = US"";
-      namedlist_block *nb;
+      int bits = 0, offset = 0, shift = 0;
+      unsigned int * use_cache_bits = original_cache_bits;
+      uschar * cached = US"";
+      namedlist_block * nb;
       tree_node * t;
 
+      DEBUG(D_lists)
+	{ debug_printf_indent(" start sublist %s\n", ss+1); expand_level += 2; }
+
       if (!(t = tree_search(*anchorptr, ss+1)))
 	{
         log_write(0, LOG_MAIN|LOG_PANIC, "unknown named%s list \"%s\"",
@@ -621,7 +629,7 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
           type == MCL_ADDRESS ?   " address" :
           type == MCL_LOCALPART ? " local part" : "",
           ss);
-	return DEFER;
+	goto DEFER_RETURN;
 	}
       nb = t->data.ptr;
 
@@ -645,8 +653,12 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
 
       if (bits == 0)
         {
-        switch (match_check_list(&(nb->string), 0, anchorptr, &use_cache_bits,
-                func, arg, type, name, valueptr))
+        int res = match_check_list(&(nb->string), 0, anchorptr, &use_cache_bits,
+                func, arg, type, name, valueptr);
+	DEBUG(D_lists)
+	  { expand_level -= 2; debug_printf_indent(" end sublist %s\n", ss+1); }
+
+        switch (res)
           {
           case OK:   bits = 1; break;
           case FAIL: bits = 3; break;
@@ -695,8 +707,12 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
 
       else
         {
-        DEBUG(D_lists) debug_printf_indent("cached %s match for %s\n",
-          (bits & (-bits)) == bits ? "yes" : "no", ss);
+        DEBUG(D_lists)
+	  {
+	  expand_level -= 2;
+	  debug_printf_indent("cached %s match for %s\n",
+	    (bits & (-bits)) == bits ? "yes" : "no", ss);
+	  }
 
         cached = US" - cached";
         if (valueptr)
@@ -720,7 +736,7 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
         {
         HDEBUG(D_lists) debug_printf_indent("%s %s (matched \"%s\"%s)\n", ot,
           yield == OK ? "yes" : "no", sss, cached);
-        return yield;
+	goto YIELD_RETURN;
         }
       }
 
@@ -734,7 +750,7 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
         case OK:
 	  HDEBUG(D_lists) debug_printf_indent("%s %s (matched \"%s\")\n", ot,
 	    (yield == OK)? "yes" : "no", sss);
-	  return yield;
+	  goto YIELD_RETURN;
 
         case DEFER:
 	  if (!error)
@@ -852,7 +868,8 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
 	  Copy it to allocated memory now we know it matched. */
 
 	  if (valueptr) *valueptr = string_copy(ss);
-	  return file_yield;
+	  yield = file_yield;
+	  goto YIELD_RETURN;
 
         case DEFER:
 	  if (!error)
@@ -864,12 +881,10 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
 	    break;
 	    }
 	  (void)fclose(f);
-	  if (include_defer)
-	    {
-	    log_write(0, LOG_MAIN, "%s: accepted by +include_defer", error);
-	    return OK;
-	    }
-	  goto DEFER_RETURN;
+	  if (!include_defer)
+	    goto DEFER_RETURN;
+	  log_write(0, LOG_MAIN, "%s: accepted by +include_defer", error);
+	  goto OK_RETURN;
 
         case ERROR:		/* host name lookup failed - this can only */
 	  if (ignore_unknown)	/* be for an incoming host (not outgoing) */
@@ -886,10 +901,10 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
 	      {
 	      if (LOGGING(unknown_in_list))
 		log_write(0, LOG_MAIN, "list matching forced to fail: %s", error);
-	      return FAIL;
+	      goto FAIL_RETURN;
 	      }
 	    log_write(0, LOG_MAIN, "%s: accepted by +include_unknown", error);
-	    return OK;
+	    goto OK_RETURN;
 	    }
         }
       }
@@ -905,14 +920,33 @@ while ((sss = string_nextinlist(&list, &sep, NULL, 0)))
 /* End of list reached: if the last item was negated yield OK, else FAIL. */
 
 HDEBUG(D_lists)
-  debug_printf_indent("%s %s (end of list)\n", ot, yield == OK ? "no":"yes");
-return yield == OK ? FAIL : OK;
+  HDEBUG(D_lists)
+    {
+    expand_level--;
+    debug_printf_indent("%s %s (end of list)\n", ot, yield == OK ? "no":"yes");
+    }
+  return yield == OK ? FAIL : OK;
 
 /* Something deferred */
 
 DEFER_RETURN:
-HDEBUG(D_lists) debug_printf("%s list match deferred for %s\n", ot, sss);
-return DEFER;
+  HDEBUG(D_lists)
+    {
+    expand_level--;
+    debug_printf_indent("%s list match deferred for %s\n", ot, sss);
+    }
+  return DEFER;
+
+FAIL_RETURN:
+  yield = FAIL;
+  goto YIELD_RETURN;
+
+OK_RETURN:
+  yield = OK;
+
+YIELD_RETURN:
+  HDEBUG(D_lists) expand_level--;
+  return yield;
 }
 
 

commit caf28f95ce0a617b075cf66e24a0e4b0b8aaf18e
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Dec 29 17:14:31 2022 +0000

    Debug: quieten environment-cleaning

diff --git a/src/src/environment.c b/src/src/environment.c
index 68adf3c0c..b05b1aefd 100644
--- a/src/src/environment.c
+++ b/src/src/environment.c
@@ -4,6 +4,7 @@
 
 /* Copyright (c) Heiko Schlittermann 2016
  * hs@schlittermann.de
+ * Copyright (c) The Exim Maintainers 2022
  * See the file NOTICE for conditions of use and distribution.
  * SPDX-License-Identifier: GPL-2.0-or-later
  */
@@ -25,10 +26,7 @@ Returns:    TRUE if successful
 BOOL
 cleanup_environment()
 {
-int old_pool = store_pool;
-store_pool = POOL_PERM;		/* Need perm memory for any created env vars */
-
-if (!keep_environment || *keep_environment == '\0')
+if (!keep_environment || !*keep_environment)
   {
   /* From: https://github.com/dovecot/core/blob/master/src/lib/env-util.c#L55
   Try to clear the environment.
@@ -43,6 +41,11 @@ if (!keep_environment || *keep_environment == '\0')
 else if (Ustrcmp(keep_environment, "*") != 0)
   {
   rmark reset_point = store_mark();
+  unsigned deb = debug_selector;
+  BOOL hc = host_checking;
+  debug_selector = 0;			/* quieten this clearout */
+  host_checking = FALSE;
+
   if (environ) for (uschar ** p = USS environ; *p; /* see below */)
     {
     /* It's considered broken if we do not find the '=', according to
@@ -54,31 +57,42 @@ else if (Ustrcmp(keep_environment, "*") != 0)
       {
       uschar * name = string_copyn(*p, eqp - *p);
 
-      if (OK != match_isinlist(name, CUSS &keep_environment,
-          0, NULL, NULL, MCL_NOEXPAND, FALSE, NULL))
-        if (os_unsetenv(name) < 0) return FALSE;
-        else p = USS environ; /* RESTART from the beginning */
-      else p++;
+      if (match_isinlist(name, CUSS &keep_environment,
+          0, NULL, NULL, MCL_NOEXPAND, FALSE, NULL) == OK)
+	p++;			/* next */
+      else if (os_unsetenv(name) == 0)
+	p = USS environ;	/* RESTART from the beginning */
+      else
+	{ debug_selector = deb; host_checking = hc; return FALSE; }
       }
     }
+  debug_selector = deb;
+  host_checking = hc;
   store_reset(reset_point);
   }
+DEBUG(D_expand)
+  {
+  debug_printf("environment after trimming:\n");
+  if (environ) for (uschar ** p = USS environ; *p; p++)
+    debug_printf(" %s\n", *p);
+  }
 if (add_environment)
   {
-  uschar * p;
   int sep = 0;
   const uschar * envlist = add_environment;
+  int old_pool = store_pool;
+  store_pool = POOL_PERM;		/* Need perm memory for any created env vars */
 
-  while ((p = string_nextinlist(&envlist, &sep, NULL, 0)))
+  for (const uschar * p; p = string_nextinlist(&envlist, &sep, NULL, 0); )
     {
     DEBUG(D_expand) debug_printf("adding %s\n", p);
     putenv(CS p);
     }
+  store_pool = old_pool;
   }
 #ifndef DISABLE_TLS
 tls_clean_env();
 #endif
 
-store_pool = old_pool;
 return TRUE;
 }

commit 5281dce92f37ab268bfa781e384d64dc5947203f
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Dec 30 18:53:51 2022 +0000

    FreeBSD: fix listener-socket backlog monitoring

diff --git a/src/src/daemon.c b/src/src/daemon.c
index d3fec42ee..05d94b188 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -2553,7 +2553,19 @@ for (;;)
 	  if (p->revents & POLLIN)
             {
 	    EXIM_SOCKLEN_T alen = sizeof(accepted);
-#ifdef TCP_INFO
+#if defined(__FreeBSD__) && defined(SO_LISTENQLEN)
+	    int backlog;
+	    socklen_t blen = sizeof(backlog);
+
+	    if (  smtp_backlog_monitor > 0
+	       && getsockopt(p->fd, SOL_SOCKET, SO_LISTENQLEN, &backlog, &blen) == 0)
+	      {
+	      DEBUG(D_interface)
+		debug_printf("listen fd %d queue curr %d\n", p->fd, backlog);
+	      smtp_listen_backlog = backlog;
+	      }
+
+#elif defined(TCP_INFO) && defined(EXIM_HAVE_TCPI_UNACKED)
 	    struct tcp_info ti;
 	    socklen_t tlen = sizeof(ti);
 
@@ -2563,15 +2575,9 @@ for (;;)
 	    if (  smtp_backlog_monitor > 0
 	       && getsockopt(p->fd, IPPROTO_TCP, TCP_INFO, &ti, &tlen) == 0)
 	      {
-# ifdef EXIM_HAVE_TCPI_UNACKED
 	      DEBUG(D_interface) debug_printf("listen fd %d queue max %u curr %u\n",
 		      p->fd, ti.tcpi_sacked, ti.tcpi_unacked);
 	      smtp_listen_backlog = ti.tcpi_unacked;
-# elif defined(__FreeBSD__)	/* This does not work. Investigate kernel sourcecode. */
-	      DEBUG(D_interface) debug_printf("listen fd %d queue max %u curr %u\n",
-		      p->fd, ti.__tcpi_sacked, ti.__tcpi_unacked);
-	      smtp_listen_backlog = ti.__tcpi_unacked;
-# endif
 	      }
 #endif
 	    p->revents = 0;

commit 57d70161718e02927a22d6a3481803b72035ac46
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Dec 31 13:37:17 2022 +0000

    Close server smtp socket explicitly on connect ACL "drop"

diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 1cfcc0404..6880e3c09 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -3565,6 +3565,19 @@ problem, because we get here only if some other ACL has issued "drop", and
 in that case, *its* custom messages will have been used above. */
 
 smtp_notquit_exit(US"acl-drop", NULL, NULL);
+
+/* An overenthusiastic fail2ban/iptables implimentation has been seen to result
+in the TCP conn staying open, and retrying, despite this process exiting. A
+malicious client could possibly do the same, tying up server netowrking
+resources. Close the socket explicitly to try to avoid that (there's a note in
+the Linux socket(7) manpage, SO_LINGER para, to the effect that exim() without
+close() results in the socket always lingering). */
+
+(void) poll_one_fd(fileno(smtp_in), POLLIN, 200);
+DEBUG(D_any) debug_printf_indent("SMTP(close)>>\n");
+(void) fclose(smtp_in);
+(void) fclose(smtp_out);
+
 return 2;
 }
 

commit 313dcd5968cd8a02995322fa771f4d56b9f15e49
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Dec 31 18:32:37 2022 +0000

    Testsuite: longer timing for OpenSSL resumption tests

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 3b060cc9c..ae0986aac 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -155,6 +155,7 @@ change this guard and punt the issue for a while longer. */
 # endif
 #endif
 
+#define TESTSUITE_TICKET_LIFE 10	/* seconds */
 /*************************************************
 *        OpenSSL option parse                    *
 *************************************************/
@@ -2044,7 +2045,7 @@ if (exim_tk.name[0])
   exim_tk_old = exim_tk;
   }
 
-if (f.running_in_test_harness) ssl_session_timeout = 6;
+if (f.running_in_test_harness) ssl_session_timeout = TESTSUITE_TICKET_LIFE;
 
 DEBUG(D_tls) debug_printf("OpenSSL: %s STEK\n", exim_tk.name[0] ? "rotating" : "creating");
 if (RAND_bytes(exim_tk.aes_key, sizeof(exim_tk.aes_key)) <= 0) return;
@@ -3908,16 +3909,17 @@ if (tlsp->host_resumable)
 #ifdef EXIM_HAVE_SESSION_TICKET
 	  SSL_SESSION_get_ticket_lifetime_hint(ss);
 #else			/* Use, fairly arbitrilarily, what we as server would */
-	  f.running_in_test_harness ? 6 : ssl_session_timeout;
+	  f.running_in_test_harness ? TESTSUITE_TICKET_LIFE : ssl_session_timeout;
 #endif
-	if (lifetime + dt->time_stamp < time(NULL))
+	time_t now = time(NULL), expires = lifetime + dt->time_stamp;
+	if (expires < now)
 	  {
-	  DEBUG(D_tls) debug_printf("session expired\n");
+	  DEBUG(D_tls) debug_printf("session expired (by " TIME_T_FMT "s from %lus)\n", now - expires, lifetime);
 	  dbfn_delete(dbm_file, tlsp->resume_index);
 	  }
 	else if (SSL_set_session(ssl, ss))
 	  {
-	  DEBUG(D_tls) debug_printf("good session\n");
+	  DEBUG(D_tls) debug_printf("good session (" TIME_T_FMT "s left of %lus)\n", expires - now, lifetime);
 	  tlsp->resumption |= RESUME_CLIENT_SUGGESTED;
 	  tlsp->verify_override = dt->verify_override;
 	  tlsp->ocsp = dt->ocsp;

commit ca4014de81e6aa367aa0a54c49b4c3d4b137814c
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Jan 1 12:18:38 2023 +0000

    OpenSSL: fix tls_eccurve setting explicit curve/group.  Bug 2954

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index ae0986aac..4d0f99ea9 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -676,12 +676,12 @@ if (dh_bitsize <= tls_dh_max_bits)
     }
   else
     DEBUG(D_tls)
-      debug_printf("Diffie-Hellman initialized from %s with %d-bit prime\n",
+      debug_printf(" Diffie-Hellman initialized from %s with %d-bit prime\n",
 	dhexpanded ? dhexpanded : US"default", dh_bitsize);
   }
 else
   DEBUG(D_tls)
-    debug_printf("dhparams '%s' %d bits, is > tls_dh_max_bits limit of %d\n",
+    debug_printf(" dhparams '%s' %d bits, is > tls_dh_max_bits limit of %d\n",
 	dhexpanded ? dhexpanded : US"default", dh_bitsize, tls_dh_max_bits);
 
 #if OPENSSL_VERSION_NUMBER < 0x30000000L
@@ -731,19 +731,27 @@ return TRUE;
 #else
 
 uschar * exp_curve;
-int nid;
-BOOL rv;
+int nid, rc;
 
 # ifndef EXIM_HAVE_ECDH
 DEBUG(D_tls)
-  debug_printf("No OpenSSL API to define ECDH parameters, skipping\n");
+  debug_printf(" No OpenSSL API to define ECDH parameters, skipping\n");
 return TRUE;
 # else
 
 if (!expand_check(tls_eccurve, US"tls_eccurve", &exp_curve, errstr))
   return FALSE;
+
+/* Is the option deliberately empty? */
+
 if (!exp_curve || !*exp_curve)
+  {
+#if OPENSSL_VERSION_NUMBER >= 0x10002000L
+  DEBUG(D_tls) debug_printf( " ECDH OpenSSL 1.0.2+: clearing curves list\n");
+  (void) SSL_CTX_set1_curves(sctx, &nid, 0);
+#endif
   return TRUE;
+  }
 
 /* "auto" needs to be handled carefully.
  * OpenSSL <  1.0.2: we do not select anything, but fallback to prime256v1
@@ -756,23 +764,22 @@ if (Ustrcmp(exp_curve, "auto") == 0)
   {
 #if OPENSSL_VERSION_NUMBER < 0x10002000L
   DEBUG(D_tls) debug_printf(
-    "ECDH OpenSSL < 1.0.2: temp key parameter settings: overriding \"auto\" with \"prime256v1\"\n");
+    " ECDH OpenSSL < 1.0.2: temp key parameter settings: overriding \"auto\" with \"prime256v1\"\n");
   exp_curve = US"prime256v1";
 #else
 # if defined SSL_CTRL_SET_ECDH_AUTO
   DEBUG(D_tls) debug_printf(
-    "ECDH OpenSSL 1.0.2+: temp key parameter settings: autoselection\n");
+    " ECDH OpenSSL 1.0.2+: temp key parameter settings: autoselection\n");
   SSL_CTX_set_ecdh_auto(sctx, 1);
   return TRUE;
 # else
   DEBUG(D_tls) debug_printf(
-    "ECDH OpenSSL 1.1.0+: temp key parameter settings: default selection\n");
+    " ECDH OpenSSL 1.1.0+: temp key parameter settings: library default selection\n");
   return TRUE;
 # endif
 #endif
   }
 
-DEBUG(D_tls) debug_printf("ECDH: curve '%s'\n", exp_curve);
 if (  (nid = OBJ_sn2nid       (CCS exp_curve)) == NID_undef
 #   ifdef EXIM_HAVE_OPENSSL_EC_NIST2NID
    && (nid = EC_curve_nist2nid(CCS exp_curve)) == NID_undef
@@ -796,23 +803,23 @@ if (  (nid = OBJ_sn2nid       (CCS exp_curve)) == NID_undef
   /* The "tmp" in the name here refers to setting a temporary key
   not to the stability of the interface. */
 
-  if ((rv = SSL_CTX_set_tmp_ecdh(sctx, ecdh) == 0))
+  if ((rc = SSL_CTX_set_tmp_ecdh(sctx, ecdh) == 0))
     tls_error(string_sprintf("Error enabling '%s' curve", exp_curve), NULL, NULL, errstr);
   else
-    DEBUG(D_tls) debug_printf("ECDH: enabled '%s' curve\n", exp_curve);
+    DEBUG(D_tls) debug_printf(" ECDH: enabled '%s' curve\n", exp_curve);
   EC_KEY_free(ecdh);
  }
 
 #else	/* v 3.0.0 + */
 
-if ((rv = SSL_CTX_set1_groups(sctx, &nid, 1)) == 0)
+if ((rc = SSL_CTX_set1_groups(sctx, &nid, 1)) == 0)
   tls_error(string_sprintf("Error enabling '%s' group", exp_curve), NULL, NULL, errstr);
 else
-  DEBUG(D_tls) debug_printf("ECDH: enabled '%s' group\n", exp_curve);
+  DEBUG(D_tls) debug_printf(" ECDH: enabled '%s' group\n", exp_curve);
 
 #endif
 
-return !rv;
+return !!rc;
 
 # endif	/*EXIM_HAVE_ECDH*/
 #endif /*OPENSSL_NO_ECDH*/
@@ -1746,7 +1753,7 @@ state_server.lib_state.lib_ctx = ctx;
 
 if (opt_unset_or_noexpand(tls_dhparam))
   {
-  DEBUG(D_tls) debug_printf("TLS: preloading DH params for server\n");
+  DEBUG(D_tls) debug_printf("TLS: preloading DH params '%s' for server\n", tls_dhparam);
   if (init_dh(ctx, tls_dhparam, &dummy_errstr))
     state_server.lib_state.dh = TRUE;
   }
@@ -1754,7 +1761,7 @@ else
   DEBUG(D_tls) debug_printf("TLS: not preloading DH params for server\n");
 if (opt_unset_or_noexpand(tls_eccurve))
   {
-  DEBUG(D_tls) debug_printf("TLS: preloading ECDH curve for server\n");
+  DEBUG(D_tls) debug_printf("TLS: preloading ECDH curve '%s' for server\n", tls_eccurve);
   if (init_ecdh(ctx, &dummy_errstr))
     state_server.lib_state.ecdh = TRUE;
   }

commit 7fa5764c203f2f4a900898a79ed02d674075313f
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Jan 2 15:04:14 2023 +0000

    OpenSSL: Fix tls_eccurve on earlier versions than 3.0.0.  Bug 2954
    
    Broken-by: ca4014de81e6

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 4d0f99ea9..e063d29bd 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -786,8 +786,9 @@ if (  (nid = OBJ_sn2nid       (CCS exp_curve)) == NID_undef
 #   endif
    )
   {
-  tls_error(string_sprintf("Unknown curve name tls_eccurve '%s'", exp_curve),
-    NULL, NULL, errstr);
+  uschar * s = string_sprintf("Unknown curve name tls_eccurve '%s'", exp_curve);
+  DEBUG(D_tls) debug_printf("TLS error '%s'\n", s);
+  if (errstr) *errstr = s;
   return FALSE;
   }
 
@@ -803,7 +804,7 @@ if (  (nid = OBJ_sn2nid       (CCS exp_curve)) == NID_undef
   /* The "tmp" in the name here refers to setting a temporary key
   not to the stability of the interface. */
 
-  if ((rc = SSL_CTX_set_tmp_ecdh(sctx, ecdh) == 0))
+  if ((rc = SSL_CTX_set_tmp_ecdh(sctx, ecdh)) == 0)
     tls_error(string_sprintf("Error enabling '%s' curve", exp_curve), NULL, NULL, errstr);
   else
     DEBUG(D_tls) debug_printf(" ECDH: enabled '%s' curve\n", exp_curve);

commit e1aca33756f73c22b00a98d40ce2be8ed94464b1
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Jan 5 13:03:37 2023 +0000

    OpenSSL: log conns rejected for bad ALPN, with the offered value
    
    Unfortunately, no way to do this under GnuTLS

diff --git a/src/src/match.c b/src/src/match.c
index 91a49c0f0..07070362d 100644
--- a/src/src/match.c
+++ b/src/src/match.c
@@ -968,6 +968,7 @@ Arguments:
   s              string to search for
   listptr        ptr to ptr to colon separated list of patterns, or NULL
   sep            a separator value for the list (see string_nextinlist())
+		 or zero for auto
   anchorptr      ptr to tree for named items, or NULL if no named items
   cache_bits     ptr to cache_bits for ditto, or NULL if not caching
   type           MCL_DOMAIN when matching a domain list
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index 729fb5879..b47fabf1d 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -1119,21 +1119,28 @@ switch (tls_id)
     /* The format of "data" here doesn't seem to be documented, but appears
     to be a 2-byte field with a (redundant, given the "size" arg) total length
     then a sequence of one-byte size then string (not nul-term) names.  The
-    latter is as described in OpenSSL documentation. */
+    latter is as described in OpenSSL documentation.
+    Note that we do not get called for a match_fail, making it hard to log
+    a single bad ALPN being offered (the common case). */
+    {
+    gstring * g = NULL;
 
     DEBUG(D_tls) debug_printf("Seen ALPN extension from client (s=%u):", size);
     for (const uschar * s = data+2; s-data < size-1; s += *s + 1)
       {
       server_seen_alpn++;
+      g = string_append_listele_n(g, ':', s+1, *s);
       DEBUG(D_tls) debug_printf(" '%.*s'", (int)*s, s+1);
       }
     DEBUG(D_tls) debug_printf("\n");
     if (server_seen_alpn > 1)
       {
+      log_write(0, LOG_MAIN, "TLS ALPN (%s) rejected", string_from_gstring(g));
       DEBUG(D_tls) debug_printf("TLS: too many ALPNs presented in handshake\n");
       return GNUTLS_E_NO_APPLICATION_PROTOCOL;
       }
     break;
+    }
 #endif
   }
 return 0;
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index e063d29bd..513ba0d3a 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -2324,6 +2324,8 @@ static int
 tls_server_alpn_cb(SSL *ssl, const uschar ** out, uschar * outlen,
   const uschar * in, unsigned int inlen, void * arg)
 {
+gstring * g = NULL;
+
 server_seen_alpn = TRUE;
 DEBUG(D_tls)
   {
@@ -2354,12 +2356,19 @@ if (  inlen > 1		/* at least one name */
       }
   }
 
-/* More than one name from clilent, or name did not match our list. */
+/* More than one name from client, or name did not match our list. */
 
 /* This will be fatal to the TLS conn; would be nice to kill TCP also.
 Maybe as an option in future; for now leave control to the config (must-tls). */
 
-DEBUG(D_tls) debug_printf("TLS ALPN rejected\n");
+for (int pos = 0, siz; pos < inlen; pos += siz+1)
+  {
+  siz = in[pos];
+  if (pos + 1 + siz > inlen) siz = inlen - pos - 1;
+  g = string_append_listele_n(g, ':', in + pos + 1, siz);
+  }
+log_write(0, LOG_MAIN, "TLS ALPN (%s) rejected", string_from_gstring(g));
+gstring_release_unused(g);
 return SSL_TLSEXT_ERR_ALERT_FATAL;
 }
 #endif	/* EXIM_HAVE_ALPN */

commit 30520c8f87fcf660ed99a2344cae7f9787f7bc89
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Jan 5 18:39:51 2023 +0000

    DANE: do not check dns_again_means_nonexist for TLSA results of TRY_AGAIN

diff --git a/src/src/dns.c b/src/src/dns.c
index 2355409ec..d39b4b590 100644
--- a/src/src/dns.c
+++ b/src/src/dns.c
@@ -907,21 +907,30 @@ if (dnsa->answerlen < 0) switch (h_errno)
 
     /* Cut this out for various test programs */
 #ifndef STAND_ALONE
-    if (try_again_recursion)
+    /* Permitting dns_again_means nonexist for TLSA lookups breaks the
+    doewngrade resistance of dane, so avoid for those. */
+
+    if (type == T_TLSA)
+      rc = FAIL;
+    else
       {
-      log_write(0, LOG_MAIN|LOG_PANIC,
-	"dns_again_means_nonexist recursion seen for %s (assuming nonexist)",
-	name);
-      return dns_fail_return(name, type, dns_expire_from_soa(dnsa, type), DNS_NOMATCH);
-      }
+      if (try_again_recursion)
+	{
+	log_write(0, LOG_MAIN|LOG_PANIC,
+	  "dns_again_means_nonexist recursion seen for %s"
+	  " (assuming nonexist)", name);
+	return dns_fail_return(name, type, dns_expire_from_soa(dnsa, type),
+			      DNS_NOMATCH);
+	}
 
-    try_again_recursion = TRUE;
-    save_domain = deliver_domain;
-    deliver_domain = string_copy(name);  /* set $domain */
-    rc = match_isinlist(name, CUSS &dns_again_means_nonexist, 0,
-      &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL);
-    deliver_domain = save_domain;
-    try_again_recursion = FALSE;
+      try_again_recursion = TRUE;
+      save_domain = deliver_domain;
+      deliver_domain = string_copy(name);  /* set $domain */
+      rc = match_isinlist(name, CUSS &dns_again_means_nonexist, 0,
+	&domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL);
+      deliver_domain = save_domain;
+      try_again_recursion = FALSE;
+      }
 
     if (rc != OK)
       {

commit 31c546c4d0c3baf1b1e0ab292b4d096cffe64c34
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Jan 6 20:50:23 2023 +0000

    Debug: show received Proxy Protocol bytes

diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 6880e3c09..5d8ffd3d0 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -1187,6 +1187,16 @@ errno = EOVERFLOW;
 return -1;
 }
 
+
+static void
+proxy_debug(uschar * buf, unsigned start, unsigned end)
+{
+debug_printf("PROXY<<");
+while (start < end) debug_printf(" %02x", buf[start++]);
+debug_printf("\n");
+}
+
+
 /*************************************************
 *         Setup host for proxy protocol          *
 *************************************************/
@@ -1263,11 +1273,11 @@ So to safely handle v1 and v2, with client-sent-first supported correctly,
 we have to do a minimum of 3 read calls, not 1.  Eww.
 */
 
-#define PROXY_INITIAL_READ 14
-#define PROXY_V2_HEADER_SIZE 16
-#if PROXY_INITIAL_READ > PROXY_V2_HEADER_SIZE
-# error Code bug in sizes of data to read for proxy usage
-#endif
+# define PROXY_INITIAL_READ 14
+# define PROXY_V2_HEADER_SIZE 16
+# if PROXY_INITIAL_READ > PROXY_V2_HEADER_SIZE
+#  error Code bug in sizes of data to read for proxy usage
+# endif
 
 int get_ok = 0;
 int size, ret;
@@ -1287,11 +1297,11 @@ do
   "safe". Can't take it all because TLS-on-connect clients follow
   immediately with TLS handshake. */
   ret = read(fd, &hdr, PROXY_INITIAL_READ);
-  }
-  while (ret == -1 && errno == EINTR && !had_command_timeout);
+  } while (ret == -1 && errno == EINTR && !had_command_timeout);
 
 if (ret == -1)
   goto proxyfail;
+DEBUG(D_receive) proxy_debug(US &hdr, 0, ret);
 
 /* For v2, handle reading the length, and then the rest. */
 if ((ret == PROXY_INITIAL_READ) && (memcmp(&hdr.v2, v2sig, sizeof(v2sig)) == 0))
@@ -1299,6 +1309,8 @@ if ((ret == PROXY_INITIAL_READ) && (memcmp(&hdr.v2, v2sig, sizeof(v2sig)) == 0))
   int retmore;
   uint8_t ver;
 
+  DEBUG(D_receive) debug_printf("v2\n");
+
   /* First get the length fields. */
   do
     {
@@ -1306,6 +1318,8 @@ if ((ret == PROXY_INITIAL_READ) && (memcmp(&hdr.v2, v2sig, sizeof(v2sig)) == 0))
     } while (retmore == -1 && errno == EINTR && !had_command_timeout);
   if (retmore == -1)
     goto proxyfail;
+  DEBUG(D_receive) proxy_debug(US &hdr, ret, ret + retmore);
+
   ret += retmore;
 
   ver = (hdr.v2.ver_cmd & 0xf0) >> 4;
@@ -1343,6 +1357,7 @@ if ((ret == PROXY_INITIAL_READ) && (memcmp(&hdr.v2, v2sig, sizeof(v2sig)) == 0))
       } while (retmore == -1 && errno == EINTR && !had_command_timeout);
     if (retmore == -1)
       goto proxyfail;
+    DEBUG(D_receive) proxy_debug(US &hdr, ret, ret + retmore);
     ret += retmore;
     DEBUG(D_receive) debug_printf("PROXYv2: have %d/%d required octets\n", ret, size);
     } while (ret < size);
@@ -1588,7 +1603,7 @@ bad:
 ALARM(0);
 return;
 }
-#endif
+#endif	/*SUPPORT_PROXY*/
 
 /*************************************************
 *           Read one command line                *

commit 42f1855e94bd87f98bc6c74255be53ed6d805ba6
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Jan 7 00:17:08 2023 +0000

    OpenSSL: tls_eccurves list support.  Bug 2955

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 513ba0d3a..96be7c4a2 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -122,6 +122,7 @@ change this guard and punt the issue for a while longer. */
 #  define EXIM_HAVE_OPENSSL_CIPHER_STD_NAME
 #  define EXIM_HAVE_EXP_CHNL_BNGNG
 #  define EXIM_HAVE_OPENSSL_OCSP_RESP_GET0_SIGNER
+#  define EXIM_HAVE_OPENSSL_SET1_GROUPS
 # else
 #  define OPENSSL_BAD_SRVR_OURCERT
 # endif
@@ -700,6 +701,41 @@ return TRUE;
 *               Initialize for ECDH              *
 *************************************************/
 
+/* "auto" needs to be handled carefully.
+OpenSSL <  1.0.2: we do not select anything, but fallback to prime256v1
+OpenSSL <  1.1.0: we have to call SSL_CTX_set_ecdh_auto
+                  (openssl/ssl.h defines SSL_CTRL_SET_ECDH_AUTO)
+OpenSSL >= 1.1.0: we do not set anything, the libray does autoselection
+                  https://github.com/openssl/openssl/commit/fe6ef2472db933f01b59cad82aa925736935984b
+
+*/
+
+static uschar *
+init_ecdh_auto(SSL_CTX * ctx)
+{
+#if OPENSSL_VERSION_NUMBER < 0x10002000L
+DEBUG(D_tls) debug_printf(
+  " ECDH OpenSSL < 1.0.2: temp key parameter settings: overriding \"auto\" with \"prime256v1\"\n");
+return US"prime256v1";
+
+#else
+# if defined SSL_CTRL_SET_ECDH_AUTO
+
+DEBUG(D_tls) debug_printf(
+  " ECDH OpenSSL 1.0.2+: temp key parameter settings: autoselection\n");
+SSL_CTX_set_ecdh_auto(sctx, 1);
+return NULL;
+
+# else
+
+DEBUG(D_tls) debug_printf(
+  " ECDH OpenSSL 1.1.0+: temp key parameter settings: library default selection\n");
+return NULL;
+
+# endif
+#endif
+}
+
 /* Load parameters for ECDH encryption.  Server only.
 
 For now, we stick to NIST P-256 because: it's simple and easy to configure;
@@ -730,72 +766,76 @@ init_ecdh(SSL_CTX * sctx, uschar ** errstr)
 return TRUE;
 #else
 
-uschar * exp_curve;
-int nid, rc;
-
 # ifndef EXIM_HAVE_ECDH
 DEBUG(D_tls)
   debug_printf(" No OpenSSL API to define ECDH parameters, skipping\n");
 return TRUE;
 # else
 
+uschar * exp_curve;
+int ngroups, rc, sep;
+const uschar * curves_list,  * curve;
+#  ifdef EXIM_HAVE_OPENSSL_SET1_GROUPS
+int nids[16];
+#  else
+int nids[1];
+#  endif
+
 if (!expand_check(tls_eccurve, US"tls_eccurve", &exp_curve, errstr))
   return FALSE;
 
 /* Is the option deliberately empty? */
 
 if (!exp_curve || !*exp_curve)
-  {
-#if OPENSSL_VERSION_NUMBER >= 0x10002000L
-  DEBUG(D_tls) debug_printf( " ECDH OpenSSL 1.0.2+: clearing curves list\n");
-  (void) SSL_CTX_set1_curves(sctx, &nid, 0);
-#endif
   return TRUE;
-  }
 
-/* "auto" needs to be handled carefully.
- * OpenSSL <  1.0.2: we do not select anything, but fallback to prime256v1
- * OpenSSL <  1.1.0: we have to call SSL_CTX_set_ecdh_auto
- *                   (openssl/ssl.h defines SSL_CTRL_SET_ECDH_AUTO)
- * OpenSSL >= 1.1.0: we do not set anything, the libray does autoselection
- *                   https://github.com/openssl/openssl/commit/fe6ef2472db933f01b59cad82aa925736935984b
- */
-if (Ustrcmp(exp_curve, "auto") == 0)
-  {
-#if OPENSSL_VERSION_NUMBER < 0x10002000L
-  DEBUG(D_tls) debug_printf(
-    " ECDH OpenSSL < 1.0.2: temp key parameter settings: overriding \"auto\" with \"prime256v1\"\n");
-  exp_curve = US"prime256v1";
-#else
-# if defined SSL_CTRL_SET_ECDH_AUTO
-  DEBUG(D_tls) debug_printf(
-    " ECDH OpenSSL 1.0.2+: temp key parameter settings: autoselection\n");
-  SSL_CTX_set_ecdh_auto(sctx, 1);
-  return TRUE;
-# else
-  DEBUG(D_tls) debug_printf(
-    " ECDH OpenSSL 1.1.0+: temp key parameter settings: library default selection\n");
-  return TRUE;
-# endif
-#endif
-  }
+/* Limit the list to hardwired array size. Drop out if any element is "suto". */
 
-if (  (nid = OBJ_sn2nid       (CCS exp_curve)) == NID_undef
-#   ifdef EXIM_HAVE_OPENSSL_EC_NIST2NID
-   && (nid = EC_curve_nist2nid(CCS exp_curve)) == NID_undef
-#   endif
-   )
-  {
-  uschar * s = string_sprintf("Unknown curve name tls_eccurve '%s'", exp_curve);
-  DEBUG(D_tls) debug_printf("TLS error '%s'\n", s);
-  if (errstr) *errstr = s;
-  return FALSE;
-  }
+curves_list = exp_curve;
+sep = 0;
+for (ngroups = 0;
+       ngroups < nelem(nids)
+    && (curve = string_nextinlist(&curves_list, &sep, NULL, 0));
+    )
+  if (Ustrcmp(curve, "auto") == 0)
+    {
+    DEBUG(D_tls) if (ngroups > 0)
+      debug_printf(" tls_eccurve 'auto' item takes precedence\n");
+    if ((exp_curve = init_ecdh_auto(sctx))) break; /* have a curve name to set */
+    return TRUE;				   /* all done */
+    }
+  else
+    ngroups++;
 
-# if OPENSSL_VERSION_NUMBER < 0x30000000L
+/* Translate to NIDs */
+
+curves_list = exp_curve;
+for (ngroups = 0; curve = string_nextinlist(&curves_list, &sep, NULL, 0);
+     ngroups++)
+  if (  (nids[ngroups] = OBJ_sn2nid       (CCS curve)) == NID_undef
+#  ifdef EXIM_HAVE_OPENSSL_EC_NIST2NID
+     && (nids[ngroups] = EC_curve_nist2nid(CCS curve)) == NID_undef
+#  endif
+     )
+    {
+    uschar * s = string_sprintf("Unknown curve name in tls_eccurve '%s'", curve);
+    DEBUG(D_tls) debug_printf("TLS error: %s\n", s);
+    if (errstr) *errstr = s;
+    return FALSE;
+    }
+
+#  ifdef EXIM_HAVE_OPENSSL_SET1_GROUPS
+/* Set the groups */
+
+if ((rc = SSL_CTX_set1_groups(sctx, nids, ngroups)) == 0)
+  tls_error(string_sprintf("Error enabling '%s' group(s)", exp_curve), NULL, NULL, errstr);
+else
+  DEBUG(D_tls) debug_printf(" ECDH: enabled '%s' group(s)\n", exp_curve);
+
+#  else		/* Cannot handle a list; only 1 element nids array */
  {
   EC_KEY * ecdh;
-  if (!(ecdh = EC_KEY_new_by_curve_name(nid)))
+  if (!(ecdh = EC_KEY_new_by_curve_name(nids[0])))
     {
     tls_error(US"Unable to create ec curve", NULL, NULL, errstr);
     return FALSE;
@@ -810,15 +850,7 @@ if (  (nid = OBJ_sn2nid       (CCS exp_curve)) == NID_undef
     DEBUG(D_tls) debug_printf(" ECDH: enabled '%s' curve\n", exp_curve);
   EC_KEY_free(ecdh);
  }
-
-#else	/* v 3.0.0 + */
-
-if ((rc = SSL_CTX_set1_groups(sctx, &nid, 1)) == 0)
-  tls_error(string_sprintf("Error enabling '%s' group", exp_curve), NULL, NULL, errstr);
-else
-  DEBUG(D_tls) debug_printf(" ECDH: enabled '%s' group\n", exp_curve);
-
-#endif
+#  endif	/*!EXIM_HAVE_OPENSSL_SET1_GROUPS*/
 
 return !!rc;
 
diff --git a/src/src/tls.c b/src/src/tls.c
index 4a23aaae9..f7be5293d 100644
--- a/src/src/tls.c
+++ b/src/src/tls.c
@@ -106,7 +106,8 @@ Returns:    TRUE if OK; result may still be NULL after forced failure
 */
 
 static BOOL
-expand_check(const uschar *s, const uschar *name, uschar **result, uschar ** errstr)
+expand_check(const uschar * s, const uschar * name,
+  uschar ** result, uschar ** errstr)
 {
 if (!s)
   *result = NULL;

commit fb09cfc3f2b667aa09deef8a0f9933a2e710be8f
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Jan 7 16:15:46 2023 +0000

    OpenSSL: fix build for pre-1.1.0
    
    Broken-by: 42f1855e94bd

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 96be7c4a2..10b5f2aa5 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -711,7 +711,7 @@ OpenSSL >= 1.1.0: we do not set anything, the libray does autoselection
 */
 
 static uschar *
-init_ecdh_auto(SSL_CTX * ctx)
+init_ecdh_auto(SSL_CTX * sctx)
 {
 #if OPENSSL_VERSION_NUMBER < 0x10002000L
 DEBUG(D_tls) debug_printf(

commit d23b36cd691de33b709eae04f6f100272a8081eb
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Jan 15 22:08:48 2023 +0000

    tidying

diff --git a/src/src/expand.c b/src/src/expand.c
index 62b4a1890..2949579c5 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -23,16 +23,18 @@ typedef unsigned esi_flags;
 #define ESI_HONOR_DOLLAR	BIT(1)	/* $ is meaningfull */
 #define ESI_SKIPPING		BIT(2)	/* value will not be needed */
 
+#ifdef STAND_ALONE
+# ifndef SUPPORT_CRYPTEQ
+#  define SUPPORT_CRYPTEQ
+# endif
+#else
+
 /* Recursively called function */
 
 static uschar *expand_string_internal(const uschar *, esi_flags, const uschar **, BOOL *, BOOL *);
 static int_eximarith_t expanded_string_integer(const uschar *, BOOL);
 
-#ifdef STAND_ALONE
-# ifndef SUPPORT_CRYPTEQ
-#  define SUPPORT_CRYPTEQ
-# endif
-#endif
+#endif	/*!STAND_ALONE*/
 
 #ifdef LOOKUP_LDAP
 # include "lookups/ldap.h"
@@ -835,8 +837,6 @@ static var_entry var_table[] = {
   { "warnmsg_recipients",  vtype_stringptr,   &warnmsg_recipients }
 };
 
-static int var_table_size = nelem(var_table);
-
 #ifdef MACRO_PREDEF
 
 /* dummies */
@@ -1278,7 +1278,7 @@ static var_entry *
 find_var_ent(uschar * name)
 {
 int first = 0;
-int last = var_table_size;
+int last = nelem(var_table);
 
 while (last > first)
   {
@@ -8806,7 +8806,7 @@ for (int i = 0; i < REGEX_VARS; i++) if (regex_vars[i])
 #endif
 
 /* check known-name variables */
-for (var_entry * v = var_table; v < var_table + var_table_size; v++)
+for (var_entry * v = var_table; v < var_table + nelem(var_table); v++)
   if (v->type == vtype_stringptr)
     assert_variable_notin(US v->name, *(USS v->value), &e);
 
diff --git a/src/src/filter.c b/src/src/filter.c
index 530d772b3..d878acb8f 100644
--- a/src/src/filter.c
+++ b/src/src/filter.c
@@ -671,8 +671,8 @@ for (;;)
       {
 //      if (toplevel) *saveptr = 0;
 //      else
-   if (!toplevel)
-      *error_pointer = string_sprintf("missing \")\" at end of "
+      if (!toplevel)
+        *error_pointer = string_sprintf("missing \")\" at end of "
           "condition near line %d of filter file", line_number);
       break;
       }
diff --git a/src/src/priv.c b/src/src/priv.c
index 3a100cd9e..9305f8b45 100644
--- a/src/src/priv.c
+++ b/src/src/priv.c
@@ -1,4 +1,4 @@
-/* Copyright (c)  The Exim Maintainers 2022 *
+/* Copyright (c)  The Exim Maintainers 2022 */
 /* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include "exim.h"
diff --git a/src/src/regex_cache.c b/src/src/regex_cache.c
index a9b482174..1ca3c96d5 100644
--- a/src/src/regex_cache.c
+++ b/src/src/regex_cache.c
@@ -239,7 +239,7 @@ regex_at_daemon(const uschar * reqbuf)
 {
 const re_req * req = (const re_req *)reqbuf;
 uschar * errstr;
-const pcre2_code * cre;
+const pcre2_code * cre = NULL;
 
 if (regex_cachesize >= REGEX_CACHESIZE_LIMIT)
   errstr = US"regex cache size limit reached";
diff --git a/src/src/spool_in.c b/src/src/spool_in.c
index e785f695b..1291197de 100644
--- a/src/src/spool_in.c
+++ b/src/src/spool_in.c
@@ -496,15 +496,18 @@ for (;;)
   if (*var == '(')				/* marker for quoted value */
     {
     uschar * s;
-    int idx;
     for (s = ++var; *s != ')'; ) s++;
 #ifndef COMPILE_UTILITY
-    if ((idx = search_findtype(var, s - var)) < 0)
       {
-      DEBUG(D_any) debug_printf("Unrecognised quoter %.*s\n", (int)(s - var), var+1);
-      goto SPOOL_FORMAT_ERROR;
+      int idx;
+      if ((idx = search_findtype(var, s - var)) < 0)
+	{
+	DEBUG(D_any)
+	  debug_printf("Unrecognised quoter %.*s\n", (int)(s - var), var+1);
+	goto SPOOL_FORMAT_ERROR;
+	}
+      proto_mem = store_get_quoted(1, GET_TAINTED, idx);
       }
-    proto_mem = store_get_quoted(1, GET_TAINTED, idx);
 #endif  /* COMPILE_UTILITY */
     var = s + 1;
     }
diff --git a/src/src/tls.c b/src/src/tls.c
index f7be5293d..ba7c2de38 100644
--- a/src/src/tls.c
+++ b/src/src/tls.c
@@ -26,11 +26,6 @@ functions from the OpenSSL or GNU TLS libraries. */
 #endif
 
 
-/* Forward decl. */
-static void tls_client_resmption_key(tls_support *, smtp_connect_args *,
-  smtp_transport_options_block *);
-
-
 #if defined(MACRO_PREDEF) && !defined(DISABLE_TLS)
 # include "macro_predef.h"
 # ifdef USE_GNUTLS
@@ -459,6 +454,10 @@ tzset();
 /*************************************************
 *        Many functions are package-specific     *
 *************************************************/
+/* Forward decl. */
+static void tls_client_resmption_key(tls_support *, smtp_connect_args *,
+  smtp_transport_options_block *);
+
 
 #ifdef USE_GNUTLS
 # include "tls-gnu.c"

commit aae673d7db4b26e8c4a8cc3d59fe94de4c47ba16
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Jan 18 11:34:07 2023 +0000

    Logging: Add "D=" to more connection closure log lines.  Bug 2434

diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 5d8ffd3d0..04b20d27c 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -333,7 +333,7 @@ Returns:    nothing
 */
 
 static void
-incomplete_transaction_log(uschar *what)
+incomplete_transaction_log(uschar * what)
 {
 if (!sender_address				/* No transaction in progress */
    || !LOGGING(smtp_incomplete_transaction))
@@ -355,13 +355,21 @@ log_write(L_smtp_incomplete_transaction, LOG_MAIN|LOG_SENDER|LOG_RECIPIENTS,
 
 
 
+static void
+log_close_event(const uschar * reason)
+{
+log_write(L_smtp_connection, LOG_MAIN, "%s D=%s closed %s",
+  smtp_get_connection_info(), string_timesince(&smtp_connection_start), reason);
+}
+
 
 void
 smtp_command_timeout_exit(void)
 {
 log_write(L_lost_incoming_connection,
-	  LOG_MAIN, "SMTP command timeout on%s connection from %s",
-	  tls_in.active.sock >= 0 ? " TLS" : "", host_and_ident(FALSE));
+	  LOG_MAIN, "SMTP command timeout on%s connection from %s D=%s",
+	  tls_in.active.sock >= 0 ? " TLS" : "", host_and_ident(FALSE),
+	  string_timesince(&smtp_connection_start));
 if (smtp_batched_input)
   moan_smtp_batch(NULL, "421 SMTP command timeout"); /* Does not return */
 smtp_notquit_exit(US"command-timeout", US"421",
@@ -373,7 +381,7 @@ exim_exit(EXIT_FAILURE);
 void
 smtp_command_sigterm_exit(void)
 {
-log_write(0, LOG_MAIN, "%s closed after SIGTERM", smtp_get_connection_info());
+log_close_event(US"after SIGTERM");
 if (smtp_batched_input)
   moan_smtp_batch(NULL, "421 SIGTERM received");  /* Does not return */
 smtp_notquit_exit(US"signal-exit", US"421",
@@ -384,9 +392,10 @@ exim_exit(EXIT_FAILURE);
 void
 smtp_data_timeout_exit(void)
 {
-log_write(L_lost_incoming_connection,
-  LOG_MAIN, "SMTP data timeout (message abandoned) on connection from %s F=<%s>",
-  sender_fullhost ? sender_fullhost : US"local process", sender_address);
+log_write(L_lost_incoming_connection, LOG_MAIN,
+  "SMTP data timeout (message abandoned) on connection from %s F=<%s> D=%s",
+  sender_fullhost ? sender_fullhost : US"local process", sender_address,
+  string_timesince(&smtp_connection_start));
 receive_bomb_out(US"data-timeout", US"SMTP incoming data timeout");
 /* Does not return */
 }
@@ -394,8 +403,7 @@ receive_bomb_out(US"data-timeout", US"SMTP incoming data timeout");
 void
 smtp_data_sigint_exit(void)
 {
-log_write(0, LOG_MAIN, "%s closed after %s",
-  smtp_get_connection_info(), had_data_sigint == SIGTERM ? "SIGTERM":"SIGINT");
+log_close_event(had_data_sigint == SIGTERM ? US"SIGTERM":US"SIGINT");
 receive_bomb_out(US"signal-exit",
   US"Service not available - SIGTERM or SIGINT received");
 /* Does not return */
@@ -3572,8 +3580,7 @@ if (log_reject_target != 0)
 
 if (!drop) return 0;
 
-log_write(L_smtp_connection, LOG_MAIN, "%s closed by DROP in ACL",
-  smtp_get_connection_info());
+log_close_event(US"by DROP in ACL");
 
 /* Run the not-quit ACL, but without any custom messages. This should not be a
 problem, because we get here only if some other ACL has issued "drop", and
@@ -3999,16 +4006,14 @@ else
 tls_close(NULL, TLS_SHUTDOWN_NOWAIT);
 # endif
 
-log_write(L_smtp_connection, LOG_MAIN, "%s closed by QUIT",
-  smtp_get_connection_info());
+log_close_event(US"by QUIT");
 #else
 
 # ifndef DISABLE_TLS
 tls_close(NULL, TLS_SHUTDOWN_WAIT);
 # endif
 
-log_write(L_smtp_connection, LOG_MAIN, "%s closed by QUIT",
-  smtp_get_connection_info());
+log_close_event(US"by QUIT");
 
 /* Pause, hoping client will FIN first so that they get the TIME_WAIT.
 The socket should become readble (though with no data) */
@@ -5763,8 +5768,7 @@ while (done <= 0)
       while (done <= 0) switch(smtp_read_command(FALSE, GETC_BUFFER_UNLIMITED))
 	{
 	case EOF_CMD:
-	  log_write(L_smtp_connection, LOG_MAIN, "%s closed by EOF",
-	    smtp_get_connection_info());
+	  log_close_event(US"by EOF");
 	  smtp_notquit_exit(US"tls-failed", NULL, NULL);
 	  done = 2;
 	  break;
@@ -5786,8 +5790,7 @@ while (done <= 0)
 	    smtp_respond(US"221", 3, TRUE, user_msg);
 	  else
 	    smtp_printf("221 %s closing connection\r\n", FALSE, smtp_active_hostname);
-	  log_write(L_smtp_connection, LOG_MAIN, "%s closed by QUIT",
-	    smtp_get_connection_info());
+	  log_close_event(US"by QUIT");
 	  done = 2;
 	  break;
 

commit f108ec74c278b14363dc9ad1274596f284d21868
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Jan 27 10:31:11 2023 +0000

    Support use-but-not-create of notifier socket

diff --git a/src/src/daemon.c b/src/src/daemon.c
index 05d94b188..c3ab735d0 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -1169,7 +1169,7 @@ const uschar * where;
 struct sockaddr_un sa_un = {.sun_family = AF_UNIX};
 ssize_t len;
 
-if (!notifier_socket || !*notifier_socket)
+if (!f.notifier_socket_en)
   {
   DEBUG(D_any) debug_printf("-oY used so not creating notifier socket\n");
   return;
@@ -1180,6 +1180,11 @@ if (override_local_interfaces && !override_pid_file_path)
     debug_printf("-oX used without -oP so not creating notifier socket\n");
   return;
   }
+if (!notifier_socket || !*notifier_socket)
+  {
+  DEBUG(D_any) debug_printf("no name for notifier socket\n");
+  return;
+  }
 
 DEBUG(D_any) debug_printf("creating notifier socket\n");
 
diff --git a/src/src/exim.c b/src/src/exim.c
index dc082f392..9072ffd54 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -3450,7 +3450,7 @@ on the second character (the one after '-'), to save some effort. */
 
       case 'Y':
 	if (*argrest) badarg = TRUE;
-	else notifier_socket = NULL;
+	else f.notifier_socket_en = FALSE;
 	break;
 
       /* Unknown -o argument */
diff --git a/src/src/globals.c b/src/src/globals.c
index efe34902a..7af345465 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -280,6 +280,7 @@ struct global_flags f =
 	.no_mbox_unspool        = FALSE,
 #endif
 	.no_multiline_responses = FALSE,
+	.notifier_socket_en	= TRUE,
 
 	.parse_allow_group      = FALSE,
 	.parse_found_group      = FALSE,
diff --git a/src/src/globals.h b/src/src/globals.h
index 5aae73fba..f2e147670 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -246,6 +246,7 @@ extern struct global_flags {
  BOOL   no_mbox_unspool			:1; /* don't unlink files in /scan directory */
 #endif
  BOOL   no_multiline_responses		:1; /* For broken clients */
+ BOOL   notifier_socket_en		:1; /* Permit create of notifier socket */
 
  BOOL   parse_allow_group		:1; /* Allow group syntax */
  BOOL   parse_found_group		:1; /* In the middle of a group */

commit dbb05434c6730ae4a13d5879f6df652d93cd6019
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Jan 27 20:02:58 2023 +0000

    Docs: add note on daemon shutdown

diff --git a/src/src/exim.c b/src/src/exim.c
index 9072ffd54..c5de167c6 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1923,6 +1923,7 @@ signal(SIGSEGV, segv_handler);				/* log faults */
 
 /* If running in a dockerized environment, the TERM signal is only
 delegated to the PID 1 if we request it by setting an signal handler */
+
 if (getpid() == 1) signal(SIGTERM, term_handler);
 
 /* SIGHUP is used to get the daemon to reconfigure. It gets set as appropriate

commit 36afd696f0000b50e9c14ad61c96b46fb3d68cd0
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Jan 28 17:05:43 2023 +0000

    refactor

diff --git a/src/src/daemon.c b/src/src/daemon.c
index c3ab735d0..7666626ed 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -1348,6 +1348,184 @@ return FALSE;
 
 
 
+static void
+daemon_inetd_wtimeout(time_t last_connection_time)
+{
+time_t resignal_interval = inetd_wait_timeout;
+
+if (last_connection_time == (time_t)0)
+  {
+  DEBUG(D_any)
+    debug_printf("inetd wait timeout expired, but still not seen first message, ignoring\n");
+  }
+else
+  {
+  time_t now = time(NULL);
+  if (now == (time_t)-1)
+    {
+    DEBUG(D_any) debug_printf("failed to get time: %s\n", strerror(errno));
+    }
+  else if ((now - last_connection_time) >= inetd_wait_timeout)
+    {
+    DEBUG(D_any)
+      debug_printf("inetd wait timeout %d expired, ending daemon\n",
+	  inetd_wait_timeout);
+    log_write(0, LOG_MAIN, "exim %s daemon terminating, inetd wait timeout reached.\n",
+	version_string);
+    daemon_die();		/* Does not return */
+    }
+  else
+    resignal_interval -= (now - last_connection_time);
+  }
+
+sigalrm_seen = FALSE;
+ALARM(resignal_interval);
+}
+
+
+static void
+daemon_qrun(int local_queue_run_max, struct pollfd * fd_polls, int listen_socket_count)
+{
+DEBUG(D_any) debug_printf("%s received\n",
+#ifndef DISABLE_QUEUE_RAMP
+  *queuerun_msgid ? "qrun notification" :
+#endif
+  "SIGALRM");
+
+/* Do a full queue run in a child process, if required, unless we already
+have enough queue runners on the go. If we are not running as root, a
+re-exec is required. */
+
+if (  queue_interval > 0
+   && (local_queue_run_max <= 0 || queue_run_count < local_queue_run_max))
+  {
+pid_t pid;
+
+  if ((pid = exim_fork(US"queue-runner")) == 0)
+    {
+    /* Disable debugging if it's required only for the daemon process. We
+    leave the above message, because it ties up with the "child ended"
+    debugging messages. */
+
+    if (f.debug_daemon) debug_selector = 0;
+
+    /* Close any open listening sockets in the child */
+
+    close_daemon_sockets(daemon_notifier_fd,
+      fd_polls, listen_socket_count);
+
+    /* Reset SIGHUP and SIGCHLD in the child in both cases. */
+
+    signal(SIGHUP,  SIG_DFL);
+    signal(SIGCHLD, SIG_DFL);
+    signal(SIGTERM, SIG_DFL);
+    signal(SIGINT, SIG_DFL);
+
+    /* Re-exec if privilege has been given up, unless deliver_drop_
+    privilege is set. Reset SIGALRM before exec(). */
+
+    if (geteuid() != root_uid && !deliver_drop_privilege)
+      {
+      uschar opt[8];
+      uschar *p = opt;
+      uschar *extra[7];
+      int extracount = 1;
+
+      signal(SIGALRM, SIG_DFL);
+      *p++ = '-';
+      *p++ = 'q';
+      if (  f.queue_2stage
+#ifndef DISABLE_QUEUE_RAMP
+	 && !*queuerun_msgid
+#endif
+	 ) *p++ = 'q';
+      if (f.queue_run_first_delivery) *p++ = 'i';
+      if (f.queue_run_force) *p++ = 'f';
+      if (f.deliver_force_thaw) *p++ = 'f';
+      if (f.queue_run_local) *p++ = 'l';
+      *p = 0;
+      extra[0] = *queue_name
+	? string_sprintf("%sG%s", opt, queue_name) : opt;
+
+#ifndef DISABLE_QUEUE_RAMP
+      if (*queuerun_msgid)
+	{
+	log_write(0, LOG_MAIN, "notify triggered queue run");
+	extra[extracount++] = queuerun_msgid;	/* Trigger only the */
+	extra[extracount++] = queuerun_msgid;	/* one message      */
+	}
+#endif
+
+      /* If -R or -S were on the original command line, ensure they get
+      passed on. */
+
+      if (deliver_selectstring)
+	{
+	extra[extracount++] = f.deliver_selectstring_regex ? US"-Rr" : US"-R";
+	extra[extracount++] = deliver_selectstring;
+	}
+
+      if (deliver_selectstring_sender)
+	{
+	extra[extracount++] = f.deliver_selectstring_sender_regex
+	  ? US"-Sr" : US"-S";
+	extra[extracount++] = deliver_selectstring_sender;
+	}
+
+      /* Overlay this process with a new execution. */
+
+      (void)child_exec_exim(CEE_EXEC_PANIC, FALSE, NULL, FALSE, extracount,
+	extra[0], extra[1], extra[2], extra[3], extra[4], extra[5], extra[6]);
+
+      /* Control never returns here. */
+      }
+
+    /* No need to re-exec; SIGALRM remains set to the default handler */
+
+#ifndef DISABLE_QUEUE_RAMP
+    if (*queuerun_msgid)
+      {
+      log_write(0, LOG_MAIN, "notify triggered queue run");
+      f.queue_2stage = FALSE;
+      queue_run(queuerun_msgid, queuerun_msgid, FALSE);
+      }
+    else
+#endif
+      queue_run(NULL, NULL, FALSE);
+    exim_underbar_exit(EXIT_SUCCESS);
+    }
+
+  if (pid < 0)
+    {
+    log_write(0, LOG_MAIN|LOG_PANIC, "daemon: fork of queue-runner "
+      "process failed: %s", strerror(errno));
+    log_close_all();
+    }
+  else
+    {
+    for (int i = 0; i < local_queue_run_max; ++i)
+      if (queue_pid_slots[i] <= 0)
+	{
+	queue_pid_slots[i] = pid;
+	queue_run_count++;
+	break;
+	}
+    DEBUG(D_any) debug_printf("%d queue-runner process%s running\n",
+      queue_run_count, queue_run_count == 1 ? "" : "es");
+    }
+  }
+
+/* Reset the alarm clock */
+
+sigalrm_seen = FALSE;
+#ifndef DISABLE_QUEUE_RAMP
+if (*queuerun_msgid)
+  *queuerun_msgid = 0;
+else
+#endif
+  ALARM(queue_interval);
+}
+
 /*************************************************
 *              Exim Daemon Mainline              *
 *************************************************/
@@ -2267,8 +2445,6 @@ report_time_since(&timestamp_startup, US"daemon loop start");	/* testcase 0022 *
 
 for (;;)
   {
-  pid_t pid;
-
   if (sigterm_seen)
     daemon_die();	/* Does not return */
 
@@ -2279,186 +2455,10 @@ for (;;)
   The other option is that we have an inetd wait timeout specified to -bw. */
 
   if (sigalrm_seen)
-    {
     if (inetd_wait_timeout > 0)
-      {
-      time_t resignal_interval = inetd_wait_timeout;
-
-      if (last_connection_time == (time_t)0)
-        {
-        DEBUG(D_any)
-          debug_printf("inetd wait timeout expired, but still not seen first message, ignoring\n");
-        }
-      else
-        {
-        time_t now = time(NULL);
-        if (now == (time_t)-1)
-          {
-          DEBUG(D_any) debug_printf("failed to get time: %s\n", strerror(errno));
-          }
-        else
-          {
-          if ((now - last_connection_time) >= inetd_wait_timeout)
-            {
-            DEBUG(D_any)
-              debug_printf("inetd wait timeout %d expired, ending daemon\n",
-                  inetd_wait_timeout);
-            log_write(0, LOG_MAIN, "exim %s daemon terminating, inetd wait timeout reached.\n",
-                version_string);
-            exit(EXIT_SUCCESS);
-            }
-          else
-            {
-            resignal_interval -= (now - last_connection_time);
-            }
-          }
-        }
-
-      sigalrm_seen = FALSE;
-      ALARM(resignal_interval);
-      }
-
+      daemon_inetd_wtimeout(last_connection_time);	/* Might not return */
     else
-      {
-      DEBUG(D_any) debug_printf("%s received\n",
-#ifndef DISABLE_QUEUE_RAMP
-	*queuerun_msgid ? "qrun notification" :
-#endif
-	"SIGALRM");
-
-      /* Do a full queue run in a child process, if required, unless we already
-      have enough queue runners on the go. If we are not running as root, a
-      re-exec is required. */
-
-      if (  queue_interval > 0
-         && (local_queue_run_max <= 0 || queue_run_count < local_queue_run_max))
-        {
-        if ((pid = exim_fork(US"queue-runner")) == 0)
-          {
-          /* Disable debugging if it's required only for the daemon process. We
-          leave the above message, because it ties up with the "child ended"
-          debugging messages. */
-
-          if (f.debug_daemon) debug_selector = 0;
-
-          /* Close any open listening sockets in the child */
-
-	  close_daemon_sockets(daemon_notifier_fd,
-	    fd_polls, listen_socket_count);
-
-          /* Reset SIGHUP and SIGCHLD in the child in both cases. */
-
-          signal(SIGHUP,  SIG_DFL);
-          signal(SIGCHLD, SIG_DFL);
-          signal(SIGTERM, SIG_DFL);
-          signal(SIGINT, SIG_DFL);
-
-          /* Re-exec if privilege has been given up, unless deliver_drop_
-          privilege is set. Reset SIGALRM before exec(). */
-
-          if (geteuid() != root_uid && !deliver_drop_privilege)
-            {
-            uschar opt[8];
-            uschar *p = opt;
-            uschar *extra[7];
-            int extracount = 1;
-
-            signal(SIGALRM, SIG_DFL);
-            *p++ = '-';
-            *p++ = 'q';
-            if (  f.queue_2stage
-#ifndef DISABLE_QUEUE_RAMP
-	       && !*queuerun_msgid
-#endif
-	       ) *p++ = 'q';
-            if (f.queue_run_first_delivery) *p++ = 'i';
-            if (f.queue_run_force) *p++ = 'f';
-            if (f.deliver_force_thaw) *p++ = 'f';
-            if (f.queue_run_local) *p++ = 'l';
-            *p = 0;
-	    extra[0] = *queue_name
-	      ? string_sprintf("%sG%s", opt, queue_name) : opt;
-
-#ifndef DISABLE_QUEUE_RAMP
-	    if (*queuerun_msgid)
-	      {
-	      log_write(0, LOG_MAIN, "notify triggered queue run");
-	      extra[extracount++] = queuerun_msgid;	/* Trigger only the */
-	      extra[extracount++] = queuerun_msgid;	/* one message      */
-	      }
-#endif
-
-            /* If -R or -S were on the original command line, ensure they get
-            passed on. */
-
-            if (deliver_selectstring)
-              {
-              extra[extracount++] = f.deliver_selectstring_regex ? US"-Rr" : US"-R";
-              extra[extracount++] = deliver_selectstring;
-              }
-
-            if (deliver_selectstring_sender)
-              {
-              extra[extracount++] = f.deliver_selectstring_sender_regex
-	        ? US"-Sr" : US"-S";
-              extra[extracount++] = deliver_selectstring_sender;
-              }
-
-            /* Overlay this process with a new execution. */
-
-            (void)child_exec_exim(CEE_EXEC_PANIC, FALSE, NULL, FALSE, extracount,
-              extra[0], extra[1], extra[2], extra[3], extra[4], extra[5], extra[6]);
-
-            /* Control never returns here. */
-            }
-
-          /* No need to re-exec; SIGALRM remains set to the default handler */
-
-#ifndef DISABLE_QUEUE_RAMP
-	  if (*queuerun_msgid)
-	    {
-	    log_write(0, LOG_MAIN, "notify triggered queue run");
-	    f.queue_2stage = FALSE;
-	    queue_run(queuerun_msgid, queuerun_msgid, FALSE);
-	    }
-	  else
-#endif
-	    queue_run(NULL, NULL, FALSE);
-          exim_underbar_exit(EXIT_SUCCESS);
-          }
-
-        if (pid < 0)
-          {
-          log_write(0, LOG_MAIN|LOG_PANIC, "daemon: fork of queue-runner "
-            "process failed: %s", strerror(errno));
-          log_close_all();
-          }
-        else
-          {
-          for (int i = 0; i < local_queue_run_max; ++i)
-            if (queue_pid_slots[i] <= 0)
-              {
-              queue_pid_slots[i] = pid;
-              queue_run_count++;
-              break;
-              }
-          DEBUG(D_any) debug_printf("%d queue-runner process%s running\n",
-            queue_run_count, queue_run_count == 1 ? "" : "es");
-          }
-        }
-
-      /* Reset the alarm clock */
-
-      sigalrm_seen = FALSE;
-#ifndef DISABLE_QUEUE_RAMP
-      if (*queuerun_msgid)
-	*queuerun_msgid = 0;
-      else
-#endif
-	ALARM(queue_interval);
-      }
-
-    } /* sigalrm_seen */
+      daemon_qrun(local_queue_run_max, fd_polls, listen_socket_count);
 
 
   /* Sleep till a connection happens if listening, and handle the connection if

commit 9ee30919f807678b0bc9f675dcfa73225b486574
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Jan 29 21:31:27 2023 +0000

    Debug: include variable content as expansion interim item

diff --git a/src/src/expand.c b/src/src/expand.c
index 2949579c5..10f009ce2 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -4733,6 +4733,7 @@ while (*s)
     reset in the middle of the buffer will make it inaccessible. */
 
     len = Ustrlen(value);
+    DEBUG(D_expand) debug_expansion_interim(US"value", value, len, !!(flags & ESI_SKIPPING));
     if (!yield && newsize != 0)
       {
       yield = g;
@@ -4751,7 +4752,10 @@ while (*s)
     int n;
     s = read_cnumber(&n, s);
     if (n >= 0 && n <= expand_nmax)
+      {
+      DEBUG(D_expand) debug_expansion_interim(US"value", expand_nstring[n], expand_nlength[n], !!(flags & ESI_SKIPPING));
       yield = string_catn(yield, expand_nstring[n], expand_nlength[n]);
+      }
     continue;
     }
 
@@ -4776,7 +4780,10 @@ while (*s)
       goto EXPAND_FAILED;
       }
     if (n >= 0 && n <= expand_nmax)
+      {
+      DEBUG(D_expand) debug_expansion_interim(US"value", expand_nstring[n], expand_nlength[n], !!(flags & ESI_SKIPPING));
       yield = string_catn(yield, expand_nstring[n], expand_nlength[n]);
+      }
     continue;
     }
 

commit 04e5caa9a7e84b2afca642d28096d988cb6802e7
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Jan 30 13:31:40 2023 +0000

    Testsuite: dovecot suthenticator testing

diff --git a/src/src/auths/check_serv_cond.c b/src/src/auths/check_serv_cond.c
index 5201d2177..1b0430ab1 100644
--- a/src/src/auths/check_serv_cond.c
+++ b/src/src/auths/check_serv_cond.c
@@ -31,9 +31,9 @@ Returns:
 */
 
 int
-auth_check_serv_cond(auth_instance *ablock)
+auth_check_serv_cond(auth_instance * ablock)
 {
-  return auth_check_some_cond(ablock,
+return auth_check_some_cond(ablock,
       US"server_condition", ablock->server_condition, OK);
 }
 
@@ -58,10 +58,10 @@ Returns:
 */
 
 int
-auth_check_some_cond(auth_instance *ablock,
-    uschar *label, uschar *condition, int unset)
+auth_check_some_cond(auth_instance * ablock,
+    uschar * label, uschar * condition, int unset)
 {
-uschar *cond;
+uschar * cond;
 
 HDEBUG(D_auth)
   {
diff --git a/src/src/auths/dovecot.c b/src/src/auths/dovecot.c
index ed56ab8cd..85d029c9c 100644
--- a/src/src/auths/dovecot.c
+++ b/src/src/auths/dovecot.c
@@ -97,13 +97,15 @@ static int socket_buffer_left;
 enable consistency checks to be done, or anything else that needs
 to be set up. */
 
-void auth_dovecot_init(auth_instance *ablock)
+void
+auth_dovecot_init(auth_instance * ablock)
 {
-auth_dovecot_options_block *ob =
+auth_dovecot_options_block * ob =
        (auth_dovecot_options_block *)(ablock->options_block);
 
 if (!ablock->public_name) ablock->public_name = ablock->name;
 if (ob->server_socket) ablock->server = TRUE;
+else DEBUG(D_auth) debug_printf("Dovecot auth driver: no server_socket for %s\n", ablock->public_name);
 ablock->client = FALSE;
 }
 
@@ -304,16 +306,14 @@ auth_defer_msg = US"authentication socket protocol error";
 socket_buffer_left = 0;  /* Global, used to read more than a line but return by line */
 for (;;)
   {
-debug_printf("%s %d\n", __FUNCTION__, __LINE__);
   if (!dc_gets(buffer, sizeof(buffer), &cctx))
     OUT("authentication socket read error or premature eof");
-debug_printf("%s %d\n", __FUNCTION__, __LINE__);
   p = buffer + Ustrlen(buffer) - 1;
   if (*p != '\n')
     OUT("authentication socket protocol line too long");
 
   *p = '\0';
-  HDEBUG(D_auth) debug_printf("received: '%s'\n", buffer);
+  HDEBUG(D_auth) debug_printf("  DOVECOT<< '%s'\n", buffer);
 
   nargs = strcut(buffer, args, nelem(args));
 
@@ -423,12 +423,12 @@ if ((
   HDEBUG(D_auth) debug_printf("error sending auth_command: %s\n",
     strerror(errno));
 
-HDEBUG(D_auth) debug_printf("sent: '%s'\n", auth_command);
+HDEBUG(D_auth) debug_printf("  DOVECOT>> '%s'\n", auth_command);
 
 while (1)
   {
-  uschar *temp;
-  uschar *auth_id_pre = NULL;
+  uschar * temp;
+  uschar * auth_id_pre = NULL;
 
   if (!dc_gets(buffer, sizeof(buffer), &cctx))
     {
@@ -437,7 +437,7 @@ while (1)
     }
 
   buffer[Ustrlen(buffer) - 1] = 0;
-  HDEBUG(D_auth) debug_printf("received: '%s'\n", buffer);
+  HDEBUG(D_auth) debug_printf("  DOVECOT<< '%s'\n", buffer);
   nargs = strcut(buffer, args, nelem(args));
   HDEBUG(D_auth) debug_strcut(args, nargs, nelem(args));
 
@@ -471,6 +471,8 @@ while (1)
 #endif
 	  write(cctx.sock, temp, Ustrlen(temp))) < 0)
 	OUT("authentication socket write error");
+
+      HDEBUG(D_auth) debug_printf("  DOVECOT>> '%s'\n", temp);
       break;
 
     case 'F':
@@ -524,7 +526,10 @@ if (cctx.sock >= 0)
   close(cctx.sock);
 
 /* Expand server_condition as an authorization check */
-return ret == OK ? auth_check_serv_cond(ablock) : ret;
+if (ret == OK) ret = auth_check_serv_cond(ablock);
+
+HDEBUG(D_auth) debug_printf("dovecot auth ret: %s\n", rc_names[ret]);
+return ret;
 }
 
 
diff --git a/src/src/auths/plaintext.c b/src/src/auths/plaintext.c
index 6692a676e..1392b369f 100644
--- a/src/src/auths/plaintext.c
+++ b/src/src/auths/plaintext.c
@@ -62,9 +62,9 @@ auth_plaintext_init(auth_instance *ablock)
 {
 auth_plaintext_options_block *ob =
   (auth_plaintext_options_block *)(ablock->options_block);
-if (ablock->public_name == NULL) ablock->public_name = ablock->name;
-if (ablock->server_condition != NULL) ablock->server = TRUE;
-if (ob->client_send != NULL) ablock->client = TRUE;
+if (!ablock->public_name) ablock->public_name = ablock->name;
+if (ablock->server_condition) ablock->server = TRUE;
+if (ob->client_send) ablock->client = TRUE;
 }
 
 

commit 70069b65a39a7ba73a36fbd95371ff03cde1eb23
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Feb 2 20:00:35 2023 +0000

    Fix crash in expansions
    
    Broken-by: 1058096b8c53

diff --git a/src/src/expand.c b/src/src/expand.c
index 10f009ce2..a7e6e4fb3 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -4747,7 +4747,7 @@ while (*s)
     continue;
     }
 
-  if (isdigit(*s))
+  if (isdigit(*s))		/* A $<n> variable */
     {
     int n;
     s = read_cnumber(&n, s);
@@ -7165,6 +7165,7 @@ NOT_ITEM: ;
 
     /* Deal specially with operators that might take a certificate variable
     as we do not want to do the usual expansion. For most, expand the string.*/
+
     switch(c)
       {
 #ifndef DISABLE_TLS
@@ -7213,7 +7214,7 @@ NOT_ITEM: ;
     to the main loop top. */
 
      {
-     int start = yield->ptr;
+     unsigned expansion_start = gstring_length(yield);
      switch(c)
       {
       case EOP_BASE32:
@@ -8275,8 +8276,8 @@ NOT_ITEM: ;
 
        DEBUG(D_expand)
 	{
-	const uschar * s = yield->s + start;
-	int i = yield->ptr - start;
+	const uschar * s = yield->s + expansion_start;
+	int i = gstring_length(yield) - expansion_start;
 	BOOL tainted = is_tainted(s);
 
 	DEBUG(D_noutf8)

commit 63deec8a3ba77fcabf405d9c30fdd65a8b909526
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Feb 5 16:04:14 2023 +0000

    More abstraction of the gstring API

diff --git a/src/src/acl.c b/src/src/acl.c
index 5ab674776..17d6c68da 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -1092,7 +1092,7 @@ for (header_line * h = acl_added_headers; h; h = h->next)
   g = string_append_listele_n(g, '\n', h->text, i);
   }
 
-return g ? g->s : NULL;
+return string_from_gstring(g);
 }
 
 
diff --git a/src/src/arc.c b/src/src/arc.c
index 30a66320e..ef44672f8 100644
--- a/src/src/arc.c
+++ b/src/src/arc.c
@@ -261,8 +261,7 @@ while ((c = *s))
 	    if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
 	      g = string_catn(g, s, 1);
 	  if (!g) return US"no b= value";
-	  al->b.data = string_from_gstring(g);
-	  al->b.len = g->ptr;
+	  al->b.len = len_string_from_gstring(g, &al->b.data);
 	  gstring_release_unused(g);
 	  bend = s;
 	  break;
@@ -278,8 +277,7 @@ while ((c = *s))
 	    if (c != ' ' && c != '\t' && c != '\n' && c != '\r')
 	      g = string_catn(g, s, 1);
 	  if (!g) return US"no bh= value";
-	  al->bh.data = string_from_gstring(g);
-	  al->bh.len = g->ptr;
+	  al->bh.len = len_string_from_gstring(g, &al->bh.data);
 	  gstring_release_unused(g);
 	  break;
 	default:
@@ -1308,7 +1306,7 @@ header_line * h = (header_line *)(al+1);
 
 /* Construct the to-be-signed AMS pseudo-header: everything but the sig. */
 
-ams_off = g->ptr;
+ams_off = gstring_length(g);
 g = string_fmt_append(g, "%s i=%d; a=rsa-sha256; c=relaxed; d=%s; s=%s",
       ARC_HDR_AMS, instance, identity, selector);	/*XXX hardwired a= */
 if (options & ARC_SIGN_OPT_TSTAMP)
@@ -1352,7 +1350,7 @@ for(col = 3; rheaders; rheaders = rheaders->prev)
 
 /* Lose the last colon from the h= list */
 
-if (g->s[g->ptr - 1] == ':') g->ptr--;
+gstring_trim_trailing(g, ':');
 
 g = string_catn(g, US";\r\n\tb=;", 7);
 
@@ -1370,7 +1368,7 @@ if (!arc_sig_from_pseudoheader(hdata, hashtype, privkey, &sig, US"AMS"))
 /* Lose the trailing semicolon from the psuedo-header, and append the signature
 (folded over lines) and termination to complete it. */
 
-g->ptr--;
+gstring_trim(g, 1);
 g = arc_sign_append_sig(g, &sig);
 
 h->slen = g->ptr - ams_off;
@@ -1548,7 +1546,7 @@ into the copies.
 static const uschar *
 arc_header_sign_feed(gstring * g)
 {
-uschar * s = string_copyn(g->s, g->ptr);
+uschar * s = string_copy_from_gstring(g);
 headers_rlist = arc_rlist_entry(headers_rlist, s, g->ptr);
 return arc_try_header(&arc_sign_ctx, headers_rlist->h, TRUE);
 }
@@ -1772,10 +1770,9 @@ if (strncmpic(ARC_HDR_AMS, g->s, ARC_HDRLEN_AMS) != 0) return US"not AMS";
 DEBUG(D_receive) debug_printf("ARC: spotted AMS header\n");
 /* Parse the AMS header */
 
-h.next = NULL;
-h.slen = g->size;
-h.text = g->s;
 memset(&al, 0, sizeof(arc_line));
+h.next = NULL;
+h.slen = len_string_from_gstring(g, &h.text);
 if ((errstr = arc_parse_line(&al, &h, ARC_HDRLEN_AMS, FALSE)))
   {
   DEBUG(D_acl) if (errstr) debug_printf("ARC: %s\n", errstr);
@@ -1857,7 +1854,8 @@ for (as = arc_verify_ctx.arcset_chain, inst = 1; as; as = as->next, inst++)
   else
     g = string_catn(g, US":", 1);
   }
-return g ? g->s : US"";
+if (!g) return US"";
+return string_from_gstring(g);
 }
 
 
@@ -1870,7 +1868,7 @@ if (arc_state)
   {
   arc_line * highest_ams;
   int start = 0;		/* Compiler quietening */
-  DEBUG(D_acl) start = g->ptr;
+  DEBUG(D_acl) start = gstring_length(g);
 
   g = string_append(g, 2, US";\n\tarc=", arc_state);
   if (arc_received_instance > 0)
@@ -1890,7 +1888,7 @@ if (arc_state)
   else if (arc_state_reason)
     g = string_append(g, 3, US" (", arc_state_reason, US")");
   DEBUG(D_acl) debug_printf("ARC:  authres '%.*s'\n",
-		  g->ptr - start - 3, g->s + start + 3);
+		  gstring_length(g) - start - 3, g->s + start + 3);
   }
 else
   DEBUG(D_acl) debug_printf("ARC:  no authres\n");
diff --git a/src/src/deliver.c b/src/src/deliver.c
index ca31df587..084a048c9 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -1045,7 +1045,7 @@ splitting is done; in those cases use the original field. */
 else
   {
   uschar * cmp;
-  int off = g->ptr;	/* start of the "full address" */
+  int off = gstring_length(g);	/* start of the "full address" */
 
   if (addr->local_part)
     {
@@ -1338,23 +1338,25 @@ if (LOGGING(deliver_time))
 if (addr->message)
   g = string_append(g, 2, US": ", addr->message);
 
-(void) string_from_gstring(g);
+ {
+  const uschar * s = string_from_gstring(g);
 
-/* Log the deferment in the message log, but don't clutter it
-up with retry-time defers after the first delivery attempt. */
+  /* Log the deferment in the message log, but don't clutter it
+  up with retry-time defers after the first delivery attempt. */
 
-if (f.deliver_firsttime || addr->basic_errno > ERRNO_RETRY_BASE)
-  deliver_msglog("%s %s\n", now, g->s);
+  if (f.deliver_firsttime || addr->basic_errno > ERRNO_RETRY_BASE)
+    deliver_msglog("%s %s\n", now, s);
 
-/* Write the main log and reset the store.
-For errors of the type "retry time not reached" (also remotes skipped
-on queue run), logging is controlled by L_retry_defer. Note that this kind
-of error number is negative, and all the retry ones are less than any
-others. */
+  /* Write the main log and reset the store.
+  For errors of the type "retry time not reached" (also remotes skipped
+  on queue run), logging is controlled by L_retry_defer. Note that this kind
+  of error number is negative, and all the retry ones are less than any
+  others. */
 
 
-log_write(addr->basic_errno <= ERRNO_RETRY_BASE ? L_retry_defer : 0, logflags,
-  "== %s", g->s);
+  log_write(addr->basic_errno <= ERRNO_RETRY_BASE ? L_retry_defer : 0, logflags,
+    "== %s", s);
+ }
 
 store_reset(reset_point);
 return;
@@ -1417,17 +1419,19 @@ if (addr->message)
 if (LOGGING(deliver_time))
   g = string_append(g, 2, US" DT=", string_timediff(&addr->delivery_time));
 
-(void) string_from_gstring(g);
-
 /* Do the logging. For the message log, "routing failed" for those cases,
 just to make it clearer. */
 
-if (driver_kind)
-  deliver_msglog("%s %s failed for %s\n", now, driver_kind, g->s);
-else
-  deliver_msglog("%s %s\n", now, g->s);
+ {
+  const uschar * s = string_from_gstring(g);
+
+  if (driver_kind)
+    deliver_msglog("%s %s failed for %s\n", now, driver_kind, s);
+  else
+    deliver_msglog("%s %s\n", now, s);
 
-log_write(0, LOG_MAIN, "** %s", g->s);
+  log_write(0, LOG_MAIN, "** %s", s);
+ }
 
 store_reset(reset_point);
 return;
diff --git a/src/src/dkim.c b/src/src/dkim.c
index 0a8ab6fb3..4c19f752f 100644
--- a/src/src/dkim.c
+++ b/src/src/dkim.c
@@ -83,7 +83,7 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
       return string_from_gstring(g);
       }
 
-    g->ptr = 0;		/* overwrite previous record */
+    gstring_reset(g);		/* overwrite previous record */
     }
 
 bad:
@@ -822,7 +822,7 @@ authres_dkim(gstring * g)
 {
 int start = 0;		/* compiler quietening */
 
-DEBUG(D_acl) start = g->ptr;
+DEBUG(D_acl) start = gstring_length(g);
 
 for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next)
   {
@@ -884,7 +884,7 @@ for (pdkim_signature * sig = dkim_signatures; sig; sig = sig->next)
   }
 
 DEBUG(D_acl)
-  if (g->ptr == start)
+  if (gstring_length(g) == start)
     debug_printf("DKIM: no authres\n");
   else
     debug_printf("DKIM: authres '%.*s'\n", g->ptr - start - 3, g->s + start + 3);
diff --git a/src/src/exim.c b/src/src/exim.c
index c5de167c6..dcc71ea45 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -203,15 +203,16 @@ Returns:   nothing
 */
 
 void
-set_process_info(const char *format, ...)
+set_process_info(const char * format, ...)
 {
 gstring gs = { .size = PROCESS_INFO_SIZE - 2, .ptr = 0, .s = process_info };
 gstring * g;
 int len;
+uschar * s;
 va_list ap;
 
 g = string_fmt_append(&gs, "%5d ", (int)getpid());
-len = g->ptr;
+len = gstring_length(g);
 va_start(ap, format);
 if (!string_vformat(g, 0, format, ap))
   {
@@ -219,8 +220,7 @@ if (!string_vformat(g, 0, format, ap))
   g = string_cat(&gs, US"**** string overflowed buffer ****");
   }
 g = string_catn(g, US"\n", 1);
-string_from_gstring(g);
-process_info_len = g->ptr;
+process_info_len = len_string_from_gstring(g, &s);
 DEBUG(D_process_info) debug_printf("set_process_info: %s", process_info);
 va_end(ap);
 }
@@ -1501,10 +1501,10 @@ for (int i = 0;; i++)
 #endif
 
   /* g can only be NULL if ss==p */
-  if (ss == p || g->s[g->ptr-1] != '\\') /* not continuation; done */
+  if (ss == p || gstring_last_char(g) != '\\') /* not continuation; done */
     break;
 
-  --g->ptr;				/* drop the \ */
+  gstring_trim(g, 1);				/* drop the \ */
   }
 
 if (had_input) return g ? string_from_gstring(g) : US"";
diff --git a/src/src/expand.c b/src/src/expand.c
index a7e6e4fb3..1daf10044 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -1667,12 +1667,13 @@ Returns:        NULL if the header does not exist, else a pointer to a new
 */
 
 static uschar *
-find_header(uschar *name, int *newsize, unsigned flags, const uschar *charset)
+find_header(uschar * name, int * newsize, unsigned flags, const uschar * charset)
 {
 BOOL found = !name;
 int len = name ? Ustrlen(name) : 0;
 BOOL comma = FALSE;
 gstring * g = NULL;
+uschar * rawhdr;
 
 for (header_line * h = header_list; h; h = h->next)
   if (h->type != htype_old && h->text)  /* NULL => Received: placeholder */
@@ -1735,8 +1736,9 @@ if (!g) return US"";
 /* That's all we do for raw header expansion. */
 
 *newsize = g->size;
+rawhdr = string_from_gstring(g);
 if (flags & FH_WANT_RAW)
-  return string_from_gstring(g);
+  return rawhdr;
 
 /* Otherwise do RFC 2047 decoding, translating the charset if requested.
 The rfc2047_decode2() function can return an error with decoded data if the
@@ -1744,12 +1746,12 @@ charset translation fails. If decoding fails, it returns NULL. */
 
 else
   {
-  uschar * error, * decoded = rfc2047_decode2(string_from_gstring(g),
+  uschar * error, * decoded = rfc2047_decode2(rawhdr,
     check_rfc2047_length, charset, '?', NULL, newsize, &error);
   if (error)
     DEBUG(D_any) debug_printf("*** error in RFC 2047 decoding: %s\n"
-      "    input was: %s\n", error, g->s);
-  return decoded ? decoded : string_from_gstring(g);
+      "    input was: %s\n", error, rawhdr);
+  return decoded ? decoded : rawhdr;
   }
 }
 
@@ -1814,7 +1816,7 @@ for (int i = 0; i < recipients_count; i++)
   s = recipients_list[i].address;
   g = string_append2_listele_n(g, US", ", s, Ustrlen(s));
   }
-return g ? g->s : NULL;
+return string_from_gstring(g);
 }
 
 
@@ -4804,7 +4806,7 @@ while (*s)
   skipping, but "break" otherwise so we get debug output for the item
   expansion. */
   {
-  int start = gstring_length(yield);
+  int expansion_start = gstring_length(yield);
   switch(item_type)
     {
     /* Call an ACL from an expansion.  We feed data in via $acl_arg1 - $acl_arg9.
@@ -4868,7 +4870,7 @@ while (*s)
 
       yield = string_append(yield, 3,
 			US"Authentication-Results: ", sub_arg[0], US"; none");
-      yield->ptr -= 6;
+      yield->ptr -= 6;			/* ignore tha ": none" for now */
 
       yield = authres_local(yield, sub_arg[0]);
       yield = authres_iprev(yield);
@@ -5785,7 +5787,7 @@ while (*s)
 
       if (o2m >= 0) for (; oldptr < yield->ptr; oldptr++)
         {
-        uschar *m = Ustrrchr(sub[1], yield->s[oldptr]);
+        uschar * m = Ustrrchr(sub[1], yield->s[oldptr]);
         if (m)
           {
           int o = m - sub[1];
@@ -7107,7 +7109,7 @@ while (*s)
 	it was for good reason */
 
 	if (quoted) yield = string_catn(yield, US"\"", 1);
-	yield = string_catn(yield, g->s, g->ptr);
+	yield = gstring_append(yield, g);
 	if (quoted) yield = string_catn(yield, US"\"", 1);
 
 	/* @$original_domain */
@@ -7126,10 +7128,11 @@ while (*s)
     }	/* EITEM_* switch */
     /*NOTREACHED*/
 
-  DEBUG(D_expand)
-    if (yield && (start > 0 || *s))	/* only if not the sole expansion of the line */
+  DEBUG(D_expand)		/* only if not the sole expansion of the line */
+    if (yield && (expansion_start > 0 || *s))
       debug_expansion_interim(US"item-res",
-			      yield->s + start, yield->ptr - start, !!(flags & ESI_SKIPPING));
+	  yield->s + expansion_start, yield->ptr - expansion_start,
+	  !!(flags & ESI_SKIPPING));
   continue;
 
 NOT_ITEM: ;
@@ -7219,11 +7222,11 @@ NOT_ITEM: ;
       {
       case EOP_BASE32:
 	{
-	uschar *t;
+	uschar * t;
 	unsigned long int n = Ustrtoul(sub, &t, 10);
 	gstring * g = NULL;
 
-	if (*t != 0)
+	if (*t)
 	  {
 	  expand_string_message = string_sprintf("argument for base32 "
 	    "operator is \"%s\", which is not a decimal number", sub);
@@ -7832,16 +7835,19 @@ NOT_ITEM: ;
 
 	case EOP_UTF8CLEAN:
 	  {
-	  int seq_len = 0, index = 0;
-	  int bytes_left = 0;
+	  int seq_len = 0, index = 0, bytes_left = 0, complete;
 	  long codepoint = -1;
-	  int complete;
 	  uschar seq_buff[4];			/* accumulate utf-8 here */
 
 	  /* Manually track tainting, as we deal in individual chars below */
 
-	  if (!yield->s || !yield->ptr)
+	  if (!yield)
+	    yield = string_get_tainted(Ustrlen(sub), sub);
+	  else if (!yield->s || !yield->ptr)
+	    {
 	    yield->s = store_get(yield->size = Ustrlen(sub), sub);
+	    gstring_reset(yield);
+	    }
 	  else if (is_incompatible(yield->s, sub))
 	    gstring_rebuffer(yield, sub);
 
@@ -7967,7 +7973,7 @@ NOT_ITEM: ;
 	    goto EXPAND_FAILED;
 	    }
 	  yield = string_cat(yield, s);
-	  DEBUG(D_expand) debug_printf_indent("yield: '%s'\n", yield->s);
+	  DEBUG(D_expand) debug_printf_indent("yield: '%s'\n", string_from_gstring(yield));
 	  break;
 	  }
 
@@ -8276,7 +8282,8 @@ NOT_ITEM: ;
 
        DEBUG(D_expand)
 	{
-	const uschar * s = yield->s + expansion_start;
+	const uschar * res = string_from_gstring(yield);
+	const uschar * s = res + expansion_start;
 	int i = gstring_length(yield) - expansion_start;
 	BOOL tainted = is_tainted(s);
 
@@ -8286,7 +8293,7 @@ NOT_ITEM: ;
 	  if (tainted)
 	    {
 	    debug_printf_indent("%s     \\__", flags & ESI_SKIPPING ? "|     " : "      ");
-	    debug_print_taint(yield->s);
+	    debug_print_taint(res);
 	    }
 	  }
 	else
@@ -8299,7 +8306,7 @@ NOT_ITEM: ;
 	    debug_printf_indent("%s",
 	      flags & ESI_SKIPPING
 	      ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
-	    debug_print_taint(yield->s);
+	    debug_print_taint(res);
 	    }
 	  }
 	}
@@ -8374,58 +8381,62 @@ if (flags & ESI_BRACE_ENDS && !*s)
 added to the string. If so, set up an empty string. Add a terminating zero. If
 left != NULL, return a pointer to the terminator. */
 
-if (!yield)
-  yield = string_get(1);
-(void) string_from_gstring(yield);
-if (left) *left = s;
+ {
+  uschar * res;
 
-/* Any stacking store that was used above the final string is no longer needed.
-In many cases the final string will be the first one that was got and so there
-will be optimal store usage. */
+  if (!yield)
+    yield = string_get(1);
+  res = string_from_gstring(yield);
+  if (left) *left = s;
 
-if (resetok) gstring_release_unused(yield);
-else if (resetok_p) *resetok_p = FALSE;
+  /* Any stacking store that was used above the final string is no longer needed.
+  In many cases the final string will be the first one that was got and so there
+  will be optimal store usage. */
 
-DEBUG(D_expand)
-  {
-  BOOL tainted = is_tainted(yield->s);
-  DEBUG(D_noutf8)
+  if (resetok) gstring_release_unused(yield);
+  else if (resetok_p) *resetok_p = FALSE;
+
+  DEBUG(D_expand)
     {
-    debug_printf_indent("|--expanding: %.*s\n", (int)(s - string), string);
-    debug_printf_indent("%sresult: %s\n",
-      flags & ESI_SKIPPING ? "|-----" : "\\_____", yield->s);
-    if (tainted)
+    BOOL tainted = is_tainted(res);
+    DEBUG(D_noutf8)
       {
-      debug_printf_indent("%s     \\__", flags & ESI_SKIPPING ? "|     " : "      ");
-      debug_print_taint(yield->s);
+      debug_printf_indent("|--expanding: %.*s\n", (int)(s - string), string);
+      debug_printf_indent("%sresult: %s\n",
+	flags & ESI_SKIPPING ? "|-----" : "\\_____", res);
+      if (tainted)
+	{
+	debug_printf_indent("%s     \\__", flags & ESI_SKIPPING ? "|     " : "      ");
+	debug_print_taint(res);
+	}
+      if (flags & ESI_SKIPPING)
+	debug_printf_indent("\\___skipping: result is not used\n");
       }
-    if (flags & ESI_SKIPPING)
-      debug_printf_indent("\\___skipping: result is not used\n");
-    }
-  else
-    {
-    debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
-      "expanding: %.*s\n",
-      (int)(s - string), string);
-    debug_printf_indent("%s" UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
-      "result: %s\n",
-      flags & ESI_SKIPPING ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
-      yield->s);
-    if (tainted)
+    else
       {
-      debug_printf_indent("%s",
-	flags & ESI_SKIPPING
-	? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
-      debug_print_taint(yield->s);
+      debug_printf_indent(UTF8_VERT_RIGHT UTF8_HORIZ UTF8_HORIZ
+	"expanding: %.*s\n",
+	(int)(s - string), string);
+      debug_printf_indent("%s" UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
+	"result: %s\n",
+	flags & ESI_SKIPPING ? UTF8_VERT_RIGHT : UTF8_UP_RIGHT,
+	res);
+      if (tainted)
+	{
+	debug_printf_indent("%s",
+	  flags & ESI_SKIPPING
+	  ? UTF8_VERT "             " : "           " UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ);
+	debug_print_taint(res);
+	}
+      if (flags & ESI_SKIPPING)
+	debug_printf_indent(UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
+	  "skipping: result is not used\n");
       }
-    if (flags & ESI_SKIPPING)
-      debug_printf_indent(UTF8_UP_RIGHT UTF8_HORIZ UTF8_HORIZ UTF8_HORIZ
-	"skipping: result is not used\n");
     }
-  }
-if (textonly_p) *textonly_p = textonly;
-expand_level--;
-return yield->s;
+  if (textonly_p) *textonly_p = textonly;
+  expand_level--;
+  return res;
+ }
 
 /* This is the failure exit: easiest to program with a goto. We still need
 to update the pointer to the terminator, for cases of nested calls with "fail".
diff --git a/src/src/functions.h b/src/src/functions.h
index 1817144ea..961db2dc0 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -963,12 +963,58 @@ g->s[g->ptr] = '\0';
 return g->s;
 }
 
+static inline int
+len_string_from_gstring(gstring * g, uschar ** sp)
+{
+if (g)
+  {
+  *sp = g->s;
+  g->s[g->ptr] = '\0';
+  return g->ptr;
+  }
+else
+  {
+  *sp = NULL;
+  return 0;
+  }
+}
+
+static inline uschar *
+string_copy_from_gstring(gstring * g)
+{
+return g ? string_copyn(g->s, g->ptr) : NULL;
+}
+
 static inline unsigned
 gstring_length(const gstring * g)
 {
 return g ? (unsigned)g->ptr : 0;
 }
 
+static inline uschar
+gstring_last_char(gstring * g)
+{
+return g->s[g->ptr-1];
+}
+
+static inline void
+gstring_trim(gstring * g, unsigned amount)
+{
+g->ptr -= amount;
+}
+
+static inline void
+gstring_trim_trailing(gstring * g, uschar c)
+{
+if (gstring_last_char(g) == c) gstring_trim(g, 1);
+}
+
+static inline void
+gstring_reset(gstring * g)
+{
+g->ptr = 0;
+}
+
 
 #define gstring_release_unused(g) \
 	gstring_release_unused_trc(g, __FUNCTION__, __LINE__)
@@ -1014,6 +1060,13 @@ memcpy(s, g->s, g->ptr);
 g->s = s;
 }
 
+/* Append one gstring to another */
+static inline gstring *
+gstring_append(gstring * dest, gstring * item)
+{
+return string_catn(dest, item->s, item->ptr);
+}
+
 
 # ifndef COMPILE_UTILITY
 /******************************************************************************/
diff --git a/src/src/imap_utf7.c b/src/src/imap_utf7.c
index 1c09db621..6c9b5c179 100644
--- a/src/src/imap_utf7.c
+++ b/src/src/imap_utf7.c
@@ -200,9 +200,7 @@ iconv_close(icd);
 #endif
 
 yield = string_catn(yield, outbuf, outptr - outbuf);
-
-if (yield->s[yield->ptr-1] == '.')
-  yield->ptr--;
+gstring_trim_trailing(yield, '.');
 
 return string_from_gstring(yield);
 }
diff --git a/src/src/log.c b/src/src/log.c
index d11b933f9..08ece6158 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -684,11 +684,11 @@ Returns:
   length actually written, persisting an errno from write()
 */
 ssize_t
-write_to_fd_buf(int fd, const uschar *buf, size_t length)
+write_to_fd_buf(int fd, const uschar * buf, size_t length)
 {
 ssize_t wrote;
 size_t total_written = 0;
-const uschar *p = buf;
+const uschar * p = buf;
 size_t left = length;
 
 while (1)
@@ -711,6 +711,12 @@ while (1)
 return total_written;
 }
 
+static inline ssize_t
+write_gstring_to_fd_buf(int fd, const gstring * g)
+{
+return write_to_fd_buf(fd, g->s, g->ptr);
+}
+
 
 
 static void
@@ -1113,7 +1119,7 @@ if (  flags & LOG_MAIN
 
     /* Failing to write to the log is disastrous */
 
-    written_len = write_to_fd_buf(mainlogfd, g->s, g->ptr);
+    written_len = write_gstring_to_fd_buf(mainlogfd, g);
     if (written_len != g->ptr)
       {
       log_write_failed(US"main log", g->ptr, written_len);
@@ -1172,8 +1178,8 @@ if (flags & LOG_REJECT)
 	g = g2;
       else		/* Buffer is full; truncate */
         {
-        g->ptr -= 100;        /* For message and separator */
-        if (g->s[g->ptr-1] == '\n') g->ptr--;
+	gstring_trim(g, 100);        /* For message and separator */
+	gstring_trim_trailing(g, '\n');
         g = string_cat(g, US"\n*** truncated ***\n");
         break;
         }
@@ -1228,7 +1234,7 @@ if (flags & LOG_REJECT)
       if (fstat(rejectlogfd, &statbuf) >= 0) rejectlog_inode = statbuf.st_ino;
       }
 
-    written_len = write_to_fd_buf(rejectlogfd, g->s, g->ptr);
+    written_len = write_gstring_to_fd_buf(rejectlogfd, g);
     if (written_len != g->ptr)
       {
       log_write_failed(US"reject log", g->ptr, written_len);
@@ -1263,7 +1269,7 @@ if (flags & LOG_PANIC)
     if (panic_save_buffer)
       (void) write(paniclogfd, panic_save_buffer, Ustrlen(panic_save_buffer));
 
-    written_len = write_to_fd_buf(paniclogfd, g->s, g->ptr);
+    written_len = write_gstring_to_fd_buf(paniclogfd, g);
     if (written_len != g->ptr)
       {
       int save_errno = errno;
diff --git a/src/src/lookups/dnsdb.c b/src/src/lookups/dnsdb.c
index 5482cd9d1..1563eda56 100644
--- a/src/src/lookups/dnsdb.c
+++ b/src/src/lookups/dnsdb.c
@@ -136,15 +136,12 @@ dnsdb_find(void * handle, const uschar * filename, const uschar * keystring,
 {
 int rc;
 int sep = 0;
-int defer_mode = PASS;
-int dnssec_mode = PASS;
-int save_retrans = dns_retrans;
-int save_retry =   dns_retry;
+int defer_mode = PASS, dnssec_mode = PASS;
+int save_retrans = dns_retrans, save_retry =   dns_retry;
 int type;
 int failrc = FAIL;
-const uschar *outsep = CUS"\n";
-const uschar *outsep2 = NULL;
-uschar *equals, *domain, *found;
+const uschar * outsep = CUS"\n", * outsep2 = NULL;
+uschar * equals, * domain, * found;
 
 dns_answer * dnsa = store_get_dns_answer();
 dns_scan dnss;
@@ -385,10 +382,7 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
       if (type == T_A || type == T_AAAA || type == T_ADDRESSES)
         {
         for (dns_address * da = dns_address_from_rr(dnsa, rr); da; da = da->next)
-          {
-          if (yield->ptr) yield = string_catn(yield, outsep, 1);
-          yield = string_cat(yield, da->address);
-          }
+	  yield = string_append_listele(yield, *outsep, da->address);
         continue;
         }
 
@@ -399,21 +393,17 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 
       if (type == T_TXT || type == T_SPF)
         {
-        if (outsep2 == NULL)	/* output only the first item of data */
+        if (!outsep2)			/* output only the first item of data */
           yield = string_catn(yield, US (rr->data+1), (rr->data)[0]);
         else
-          {
-          /* output all items */
-          int data_offset = 0;
-          while (data_offset < rr->size)
+          for (unsigned data_offset = 0; data_offset < rr->size; )
             {
-            uschar chunk_len = (rr->data)[data_offset++];
-            if (outsep2[0] != '\0' && data_offset != 1)
+            uschar chunk_len = (rr->data)[data_offset];
+            if (*outsep2  && data_offset != 0)
               yield = string_catn(yield, outsep2, 1);
-            yield = string_catn(yield, US ((rr->data)+data_offset), chunk_len);
+            yield = string_catn(yield, US ((rr->data) + ++data_offset), chunk_len);
             data_offset += chunk_len;
             }
-          }
         }
       else if (type == T_TLSA)
         {
diff --git a/src/src/lookups/ldap.c b/src/src/lookups/ldap.c
index b2ad3bbbc..82d6954ff 100644
--- a/src/src/lookups/ldap.c
+++ b/src/src/lookups/ldap.c
@@ -326,17 +326,19 @@ if (!lcp)
     g = string_catn(NULL, ldap_url, init_ptr - ldap_url);
     g = string_fmt_append(g, "//%s:%d/", shost, port);
     }
-  string_from_gstring(g);
 
   /* Call ldap_initialize() and check the result */
+   {
+    const uschar * s = string_from_gstring(g);
 
-  DEBUG(D_lookup) debug_printf_indent("ldap_initialize with URL %s\n", g->s);
-  if ((rc = ldap_initialize(&ld, CS g->s)) != LDAP_SUCCESS)
-    {
-    *errmsg = string_sprintf("ldap_initialize: (error %d) URL \"%s\"\n",
-      rc, g->s);
-    goto RETURN_ERROR;
-    }
+    DEBUG(D_lookup) debug_printf_indent("ldap_initialize with URL %s\n", s);
+    if ((rc = ldap_initialize(&ld, CS s)) != LDAP_SUCCESS)
+      {
+      *errmsg = string_sprintf("ldap_initialize: (error %d) URL \"%s\"\n",
+	rc, s);
+      goto RETURN_ERROR;
+      }
+   }
   store_reset(reset_point);   /* Might as well save memory when we can */
 
 
diff --git a/src/src/mime.c b/src/src/mime.c
index 7c3a33d62..d4c26540a 100644
--- a/src/src/mime.c
+++ b/src/src/mime.c
@@ -755,7 +755,7 @@ while(1)
     int result = 0;
 
     /* must find first free sequential filename */
-    for (gstring * g = string_get(64); result != -1; g->ptr = 0)
+    for (gstring * g = string_get(64); result != -1; gstring_reset(g))
       {
       struct stat mystat;
       g = string_fmt_append(g,
diff --git a/src/src/parse.c b/src/src/parse.c
index 53d660869..ead8751ae 100644
--- a/src/src/parse.c
+++ b/src/src/parse.c
@@ -875,26 +875,26 @@ Returns:       pointer to the original string, if no quoting needed, or
 */
 
 const uschar *
-parse_quote_2047(const uschar *string, int len, const uschar *charset,
+parse_quote_2047(const uschar * string, int len, const uschar * charset,
   BOOL fold)
 {
 const uschar * s = string;
-int hlen, l;
+int hlen, line_off;
 BOOL coded = FALSE;
 BOOL first_byte = FALSE;
 gstring * g =
-  string_fmt_append(NULL, "=?%s?Q?", charset ? charset : US"iso-8859-1");
+  string_fmt_append(NULL, "=?%s?Q?%n", charset ? charset : US"iso-8859-1", &hlen);
 
-hlen = l = g->ptr;
+line_off = hlen;
 
 for (s = string; len > 0; s++, len--)
   {
   int ch = *s;
 
-  if (g->ptr - l > 67 && !first_byte)
+  if (g->ptr - line_off > 67 && !first_byte)
     {
     g = fold ? string_catn(g, US"?=\n ", 4) : string_catn(g, US"?= ", 3);
-    l = g->ptr;
+    line_off = g->ptr;
     g = string_catn(g, g->s, hlen);
     }
 
diff --git a/src/src/pdkim/pdkim.c b/src/src/pdkim/pdkim.c
index eb26b3864..c8f180a58 100644
--- a/src/src/pdkim/pdkim.c
+++ b/src/src/pdkim/pdkim.c
@@ -957,9 +957,8 @@ return;
 static int
 pdkim_header_complete(pdkim_ctx * ctx)
 {
-if ( (ctx->cur_header->ptr > 1) &&
-     (ctx->cur_header->s[ctx->cur_header->ptr-1] == '\r') )
-  --ctx->cur_header->ptr;
+if (ctx->cur_header->ptr > 1)
+  gstring_trim_trailing(ctx->cur_header, '\r');
 (void) string_from_gstring(ctx->cur_header);
 
 #ifdef EXPERIMENTAL_ARC
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 48b648bb2..6dba11ca1 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -3219,7 +3219,7 @@ if (config_file)
     g = string_cat(NULL, buf);
 
     /* If the dir does not end with a "/", append one */
-    if (g->s[g->ptr-1] != '/')
+    if (gstring_last_char(g) != '/')
       g = string_catn(g, US"/", 1);
 
     /* If the config file contains a "/", extract the directory part */
diff --git a/src/src/receive.c b/src/src/receive.c
index 9bf834aaf..77665d89f 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -4214,7 +4214,8 @@ if (message_logs && !blackholed_by)
       }
     else
       {
-      uschar *now = tod_stamp(tod_log);
+      uschar * now = tod_stamp(tod_log);
+      /* Drop the initial "<= " */
       fprintf(message_log, "%s Received from %s\n", now, g->s+3);
       if (f.deliver_freeze) fprintf(message_log, "%s frozen by %s\n", now,
         frozen_by);
@@ -4266,7 +4267,7 @@ if (  smtp_input && sender_host_address && !f.sender_host_notsocket
 
       /* Re-use the log line workspace */
 
-      g->ptr = 0;
+      gstring_reset(g);
       g = string_cat(g, US"SMTP connection lost after final dot");
       g = add_host_info_for_log(g);
       log_write(0, LOG_MAIN, "%s", string_from_gstring(g));
diff --git a/src/src/rfc2047.c b/src/src/rfc2047.c
index c40518a5d..d5e33b9b1 100644
--- a/src/src/rfc2047.c
+++ b/src/src/rfc2047.c
@@ -192,9 +192,9 @@ rfc2047_decode2(uschar *string, BOOL lencheck, const uschar *target,
 {
 int size = Ustrlen(string);
 size_t dlen;
-uschar *dptr;
-gstring *yield;
-uschar *mimeword, *q1, *q2, *endword;
+uschar * dptr;
+gstring * yield;
+uschar * mimeword, * q1, * q2, * endword;
 
 *error = NULL;
 mimeword = decode_mimeword(string, lencheck, &q1, &q2, &endword, &dlen, &dptr);
@@ -210,17 +210,14 @@ building the result as we go. The result may be longer than the input if it is
 translated into a multibyte code such as UTF-8. That's why we use the dynamic
 string building code. */
 
-yield = store_get(sizeof(gstring) + ++size, string);
-yield->size = size;
-yield->ptr = 0;
-yield->s = US(yield + 1);
+yield = string_get_tainted(++size, string);
 
 while (mimeword)
   {
 
-  #if HAVE_ICONV
+#if HAVE_ICONV
   iconv_t icd = (iconv_t)(-1);
-  #endif
+#endif
 
   if (mimeword != string)
     yield = string_catn(yield, string, mimeword - string);
@@ -233,7 +230,7 @@ while (mimeword)
   of long strings - the RFC puts limits on the length, but it's best to be
   robust. */
 
-  #if HAVE_ICONV
+#if HAVE_ICONV
   *q1 = 0;
   if (target && strcmpic(target, mimeword+2) != 0)
     if ((icd = iconv_open(CS target, CS(mimeword+2))) == (iconv_t)-1)
@@ -241,14 +238,14 @@ while (mimeword)
         target, mimeword+2, strerror(errno),
         (errno == EINVAL)? " (maybe unsupported conversion)" : "");
   *q1 = '?';
-  #endif
+#endif
 
   while (dlen > 0)
     {
     uschar *tptr = NULL;   /* Stops compiler warning */
     int tlen = -1;
 
-    #if HAVE_ICONV
+#if HAVE_ICONV
     uschar tbuffer[256];
     uschar *outptr = tbuffer;
     size_t outleft = sizeof(tbuffer);
@@ -281,7 +278,7 @@ while (mimeword)
         }
       }
 
-    #endif
+#endif
 
     /* No charset translation is happening or there was a translation error;
     just set up the original as the string to be added, and mark it all used.
@@ -305,9 +302,9 @@ while (mimeword)
     yield = string_catn(yield, tptr, tlen);
     }
 
-  #if HAVE_ICONV
+#if HAVE_ICONV
   if (icd != (iconv_t)(-1))  iconv_close(icd);
-  #endif
+#endif
 
   /* Update string past the MIME word; skip any white space if the next thing
   is another MIME word. */
diff --git a/src/src/sieve.c b/src/src/sieve.c
index 0b347e48d..4793d5756 100644
--- a/src/src/sieve.c
+++ b/src/src/sieve.c
@@ -439,8 +439,7 @@ if (*uri && *uri!='?')
       {
       gstring * g = string_catn(NULL, start, uri-start);
 
-      to.character = string_from_gstring(g);
-      to.length = g->ptr;
+      to.length = len_string_from_gstring(g, &to.character);
       if (uri_decode(&to)==-1)
         {
         filter->errmsg=US"Invalid URI encoding";
@@ -472,8 +471,7 @@ if (*uri=='?')
       {
       gstring * g = string_catn(NULL, start, uri-start);
 
-      hname.character = string_from_gstring(g);
-      hname.length = g->ptr;
+      hname.length = len_string_from_gstring(g, &hname.character);
       if (uri_decode(&hname)==-1)
         {
         filter->errmsg=US"Invalid URI encoding";
@@ -494,8 +492,7 @@ if (*uri=='?')
       {
       gstring * g = string_catn(NULL, start, uri-start);
 
-      hname.character = string_from_gstring(g);
-      hname.length = g->ptr;
+      hname.length = len_string_from_gstring(g, &hname.character);
       if (uri_decode(&hvalue)==-1)
         {
         filter->errmsg=US"Invalid URI encoding";
@@ -541,8 +538,7 @@ if (*uri=='?')
         g = string_catn(g, hvalue.character, hvalue.length);
         g = string_catn(g, CUS "\n", 1);
 
-	header->character = string_from_gstring(g);
-	header->length = g->ptr;
+	hname.length = len_string_from_gstring(g, &hname.character);
         }
       }
     if (*uri=='&') ++uri;
@@ -1482,10 +1478,7 @@ if (*filter->pc=='"') /* quoted string */
       ++filter->pc;
 
       if (g)
-	{
-	data->character = string_from_gstring(g);
-	data->length = g->ptr;
-	}
+	data->length = len_string_from_gstring(g, &data->character);
       else
 	data->character = US"\0";
       /* that way, there will be at least one character allocated */
@@ -1569,10 +1562,7 @@ else if (Ustrncmp(filter->pc,CUS "text:",5)==0) /* multiline string */
 #endif
         {
 	if (g)
-	  {
-	  data->character = string_from_gstring(g);
-	  data->length = g->ptr;
-	  }
+	  data->length = len_string_from_gstring(g, &data->character);
 	else
 	  data->character = US"\0";
 	/* that way, there will be at least one character allocated */
@@ -3303,7 +3293,7 @@ while (*filter->pc)
 
           if (subject.length==-1)
             {
-            uschar *subject_def;
+            uschar * subject_def;
 
             subject_def = expand_string(US"${if def:header_subject {true}{false}}");
             if (subject_def && Ustrcmp(subject_def,"true")==0)
@@ -3312,13 +3302,12 @@ while (*filter->pc)
 
               expand_header(&subject,&str_subject);
               g = string_catn(g, subject.character, subject.length);
-	      subject.character = string_from_gstring(g);
-              subject.length = g->ptr;
+	      subject.length = len_string_from_gstring(g, &subject.character);
               }
             else
               {
-              subject.character=US"Automated reply";
-              subject.length=Ustrlen(subject.character);
+              subject.character = US"Automated reply";
+              subject.length = Ustrlen(subject.character);
               }
             }
 
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 04b20d27c..a13af867a 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -4508,7 +4508,7 @@ while (done <= 0)
 
       if (fl.esmtp)
 	{
-	g->s[3] = '-';
+	g->s[3] = '-';	/* overwrite the space after the SMTP response code */
 
 	/* I'm not entirely happy with this, as an MTA is supposed to check
 	that it has enough room to accept a message of maximum size before
@@ -4639,9 +4639,9 @@ while (done <= 0)
 		  first = FALSE;
 		  fl.auth_advertised = TRUE;
 		  }
-		saveptr = g->ptr;
+		saveptr = gstring_length(g);
 		g = string_catn(g, US" ", 1);
-		g = string_cat (g, au->public_name);
+		g = string_cat(g, au->public_name);
 		while (++saveptr < g->ptr) g->s[saveptr] = toupper(g->s[saveptr]);
 		au->advertised = TRUE;
 		}
@@ -4704,25 +4704,29 @@ while (done <= 0)
       /* Terminate the string (for debug), write it, and note that HELO/EHLO
       has been seen. */
 
+       {
+	uschar * ehlo_resp;
+	int len = len_string_from_gstring(g, &ehlo_resp);
 #ifndef DISABLE_TLS
-      if (tls_in.active.sock >= 0)
-	(void)tls_write(NULL, g->s, g->ptr,
+	if (tls_in.active.sock >= 0)
+	  (void) tls_write(NULL, ehlo_resp, len,
 # ifndef DISABLE_PIPE_CONNECT
-			fl.pipe_connect_acceptable && pipeline_connect_sends());
+			  fl.pipe_connect_acceptable && pipeline_connect_sends());
 # else
-			FALSE);
+			  FALSE);
 # endif
-      else
+	else
 #endif
-	(void) fwrite(g->s, 1, g->ptr, smtp_out);
-
-      DEBUG(D_receive) for (const uschar * t, * s = string_from_gstring(g);
-			    s && (t = Ustrchr(s, '\r'));
-			    s = t + 2)				/* \r\n */
-	  debug_printf("%s %.*s\n",
-			s == g->s ? "SMTP>>" : "      ",
-			(int)(t - s), s);
-      fl.helo_seen = TRUE;
+	  (void) fwrite(ehlo_resp, 1, len, smtp_out);
+
+	DEBUG(D_receive) for (const uschar * t, * s = ehlo_resp;
+			      s && (t = Ustrchr(s, '\r'));
+			      s = t + 2)				/* \r\n */
+	    debug_printf("%s %.*s\n",
+			  s == g->s ? "SMTP>>" : "      ",
+			  (int)(t - s), s);
+	fl.helo_seen = TRUE;
+       }
 
       /* Reset the protocol and the state, abandoning any previous message. */
       received_protocol =
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index b47fabf1d..f3f70d2e0 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -2302,7 +2302,7 @@ old_pool = store_pool;
 
     for (s++; (c = *s) && c != ')'; s++) g = string_catn(g, s, 1);
 
-    tlsp->ver = string_copyn(g->s, g->ptr);
+    tlsp->ver = string_copy_from_gstring(g);
     for (uschar * p = US tlsp->ver; *p; p++)
       if (*p == '-') { *p = '\0'; break; }	/* TLS1.0-PKIX -> TLS1.0 */
 

commit 1e835086d1592bdfbcd8577133965b78470840ac
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Feb 13 11:34:38 2023 +0000

    Named queues: support multiple queue-runners from single daemon

diff --git a/src/src/daemon.c b/src/src/daemon.c
index 7666626ed..b0533c28f 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -3,7 +3,7 @@
 *************************************************/
 
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
-/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* 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 */
 
@@ -16,17 +16,20 @@
 /* Structure for holding data for each SMTP connection */
 
 typedef struct smtp_slot {
-  pid_t pid;                       /* pid of the spawned reception process */
-  uschar *host_address;            /* address of the client host */
+  pid_t		pid;		/* pid of the spawned reception process */
+  uschar *	host_address;	/* address of the client host */
 } smtp_slot;
 
+typedef struct runner_slot {
+  pid_t		pid;		/* pid of spawned queue-runner process */
+  const uschar *queue_name;	/* pointer to the name in the qrunner struct */
+} runner_slot;
+
 /* An empty slot for initializing (Standard C does not allow constructor
 expressions in assignments except as initializers in declarations). */
 
 static smtp_slot empty_smtp_slot = { .pid = 0, .host_address = NULL };
 
-
-
 /*************************************************
 *               Local static variables           *
 *************************************************/
@@ -39,9 +42,11 @@ static int   accept_retry_count = 0;
 static int   accept_retry_errno;
 static BOOL  accept_retry_select_failed;
 
-static int   queue_run_count = 0;
-static pid_t *queue_pid_slots = NULL;
-static smtp_slot *smtp_slots = NULL;
+static int   queue_run_count = 0;	/* current runners */
+
+static unsigned queue_runner_slot_count = 0;
+static runner_slot * queue_runner_slots = NULL;
+static smtp_slot * smtp_slots = NULL;
 
 static BOOL  write_pid = TRUE;
 
@@ -920,19 +925,30 @@ while ((pid = waitpid(-1, &status, WNOHANG)) > 0)
   /* If it wasn't an accepting process, see if it was a queue-runner
   process that we are tracking. */
 
-  if (queue_pid_slots)
-    {
-    int max = atoi(CS expand_string(queue_run_max));
-    for (int i = 0; i < max; i++)
-      if (queue_pid_slots[i] == pid)
+  if (queue_runner_slots)
+    for (unsigned i = 0; i < queue_runner_slot_count; i++)
+      {
+      runner_slot * r = queue_runner_slots + i;
+      if (r->pid == pid)
         {
-        queue_pid_slots[i] = 0;
+        r->pid = 0;			/* free up the slot */
+
         if (--queue_run_count < 0) queue_run_count = 0;
         DEBUG(D_any) debug_printf("%d queue-runner process%s now running\n",
-          queue_run_count, (queue_run_count == 1)? "" : "es");
+          queue_run_count, queue_run_count == 1 ? "" : "es");
+
+	for (qrunner ** p = &qrunners, * q = qrunners; q; p = &q->next, q = *p)
+	  if (q->name == r->queue_name)
+	    {
+	    if (q->interval)		/* a periodic queue run */
+	      q->run_count--;
+	    else			/* a one-time run */
+	      *p = q->next;		/* drop this qrunner */
+	    break;
+	    }
         break;
         }
-    }
+      }
   }
 }
 
@@ -1232,7 +1248,11 @@ bad:
 }
 
 
+/* Data for notifier-triggered queue runs */
+
 static uschar queuerun_msgid[MESSAGE_ID_LENGTH+1];
+static const uschar * queuerun_msg_qname;
+
 
 /* The notifier socket has something to read. Pull the message from it, decode
 and do the action.
@@ -1320,7 +1340,12 @@ switch (buf[0])
     /* this should be a message_id */
     DEBUG(D_queue_run)
       debug_printf("%s: qrunner trigger: %s\n", __FUNCTION__, buf+1);
+
     memcpy(queuerun_msgid, buf+1, MESSAGE_ID_LENGTH+1);
+
+    for (qrunner * q = qrunners; q; q = q->next)
+      if (Ustrcmp(q->name, buf+1+MESSAGE_ID_LENGTH+1) == 0)
+	{ queuerun_msg_qname = q->name; break; }
     return TRUE;
 #endif
 
@@ -1383,7 +1408,45 @@ ALARM(resignal_interval);
 }
 
 
-static void
+/* Re-sort the qrunners list, and return the shortest interval.
+That could be negatime.
+The next-tick times should have been updated by any runs initiated,
+though will not be when the global limit on runners was reached.
+
+Unlikely to have many queues, so insertion-sort.
+*/
+
+static int
+next_qrunner_interval(void)
+{
+qrunner * sorted = NULL;
+for (qrunner * q = qrunners, * next; q; q = next)
+  {
+  next = q->next;
+  q->next = NULL;
+  if (sorted)
+    {
+    qrunner ** p = &sorted;
+    for (qrunner * qq; qq = *p; p = &(qq->next))
+      if (  q->next_tick < qq->next_tick
+	 || q->next_tick == qq->next_tick && q->interval < qq->interval
+	 )
+	{
+	*p = q;
+	q->next = qq;
+	goto INSERTED;
+	}
+    *p = q;
+  INSERTED: ;
+    }
+  else
+    sorted = q;
+  }
+qrunners = sorted;
+return qrunners ? qrunners->next_tick - time(NULL) : 0;
+}
+
+static int
 daemon_qrun(int local_queue_run_max, struct pollfd * fd_polls, int listen_socket_count)
 {
 DEBUG(D_any) debug_printf("%s received\n",
@@ -1392,140 +1455,207 @@ DEBUG(D_any) debug_printf("%s received\n",
 #endif
   "SIGALRM");
 
-/* Do a full queue run in a child process, if required, unless we already
-have enough queue runners on the go. If we are not running as root, a
-re-exec is required. */
-
-if (  queue_interval > 0
-   && (local_queue_run_max <= 0 || queue_run_count < local_queue_run_max))
-  {
-pid_t pid;
+/* Do a full queue run in a child process, if required, unless we already have
+enough queue runners on the go. If we are not running as root, a re-exec is
+required. In the calling process, restart the alamr timer for the next run.  */
 
-  if ((pid = exim_fork(US"queue-runner")) == 0)
+if (is_multiple_qrun())
+  if (local_queue_run_max <= 0 || queue_run_count < local_queue_run_max)
     {
-    /* Disable debugging if it's required only for the daemon process. We
-    leave the above message, because it ties up with the "child ended"
-    debugging messages. */
+    qrunner * q = NULL;
+
+#ifndef DISABLE_QUEUE_RAMP
+    if (*queuerun_msgid)	/* See if we can start another runner for this queue */
+      {
+      for (qrunner * qq = qrunners; qq; qq = qq->next)
+	if (qq->name == queuerun_msg_qname)
+	  {
+	  q = qq->run_count < qq->run_max ? qq : NULL;
+	  break;
+	  }
+      }
+    else
+#endif
+      /* In order of run priority, find the first queue for which we can start
+      a runner */
 
-    if (f.debug_daemon) debug_selector = 0;
+      for (q = qrunners; q; q = q->next)
+	if (q->run_count < q->run_max) break;
+
+    if (q)
+      {
+      pid_t pid;
 
-    /* Close any open listening sockets in the child */
+      /* Bump this queue's next-tick by it's interval */
 
-    close_daemon_sockets(daemon_notifier_fd,
-      fd_polls, listen_socket_count);
+      if (q->interval)
+	{
+	time_t now = time(NULL);
+	do ; while ((q->next_tick += q->interval) <= now);
+	}
 
-    /* Reset SIGHUP and SIGCHLD in the child in both cases. */
+      if ((pid = exim_fork(US"queue-runner")) == 0)
+	{
+	/* Disable debugging if it's required only for the daemon process. We
+	leave the above message, because it ties up with the "child ended"
+	debugging messages. */
 
-    signal(SIGHUP,  SIG_DFL);
-    signal(SIGCHLD, SIG_DFL);
-    signal(SIGTERM, SIG_DFL);
-    signal(SIGINT, SIG_DFL);
+	if (f.debug_daemon) debug_selector = 0;
 
-    /* Re-exec if privilege has been given up, unless deliver_drop_
-    privilege is set. Reset SIGALRM before exec(). */
+	/* Close any open listening sockets in the child */
 
-    if (geteuid() != root_uid && !deliver_drop_privilege)
-      {
-      uschar opt[8];
-      uschar *p = opt;
-      uschar *extra[7];
-      int extracount = 1;
-
-      signal(SIGALRM, SIG_DFL);
-      *p++ = '-';
-      *p++ = 'q';
-      if (  f.queue_2stage
+	close_daemon_sockets(daemon_notifier_fd,
+	  fd_polls, listen_socket_count);
+
+	/* Reset SIGHUP and SIGCHLD in the child in both cases. */
+
+	signal(SIGHUP,  SIG_DFL);
+	signal(SIGCHLD, SIG_DFL);
+	signal(SIGTERM, SIG_DFL);
+	signal(SIGINT, SIG_DFL);
+
+	/* Re-exec if privilege has been given up, unless deliver_drop_
+	privilege is set. Reset SIGALRM before exec(). */
+
+	if (geteuid() != root_uid && !deliver_drop_privilege)
+	  {
+	  uschar opt[8];
+	  uschar *p = opt;
+	  uschar *extra[7];
+	  int extracount = 1;
+
+	  signal(SIGALRM, SIG_DFL);
+	  queue_name = US"";
+
+	  *p++ = '-';
+	  *p++ = 'q';
+	  if (  q->queue_2stage
 #ifndef DISABLE_QUEUE_RAMP
-	 && !*queuerun_msgid
+	     && !*queuerun_msgid
 #endif
-	 ) *p++ = 'q';
-      if (f.queue_run_first_delivery) *p++ = 'i';
-      if (f.queue_run_force) *p++ = 'f';
-      if (f.deliver_force_thaw) *p++ = 'f';
-      if (f.queue_run_local) *p++ = 'l';
-      *p = 0;
-      extra[0] = *queue_name
-	? string_sprintf("%sG%s", opt, queue_name) : opt;
+	     ) *p++ = 'q';
+	  if (q->queue_run_first_delivery) *p++ = 'i';
+	  if (q->queue_run_force) *p++ = 'f';
+	  if (q->deliver_force_thaw) *p++ = 'f';
+	  if (q->queue_run_local) *p++ = 'l';
+	  *p = 0;
+
+	  extra[0] = q->name
+	    ? string_sprintf("%sG%s", opt, q->name) : opt;
 
 #ifndef DISABLE_QUEUE_RAMP
-      if (*queuerun_msgid)
-	{
-	log_write(0, LOG_MAIN, "notify triggered queue run");
-	extra[extracount++] = queuerun_msgid;	/* Trigger only the */
-	extra[extracount++] = queuerun_msgid;	/* one message      */
-	}
+	  if (*queuerun_msgid)
+	    {
+	    log_write(0, LOG_MAIN, "notify triggered queue run");
+	    extra[extracount++] = queuerun_msgid;	/* Trigger only the */
+	    extra[extracount++] = queuerun_msgid;	/* one message      */
+	    }
 #endif
 
-      /* If -R or -S were on the original command line, ensure they get
-      passed on. */
+	  /* If -R or -S were on the original command line, ensure they get
+	  passed on. */
 
-      if (deliver_selectstring)
-	{
-	extra[extracount++] = f.deliver_selectstring_regex ? US"-Rr" : US"-R";
-	extra[extracount++] = deliver_selectstring;
-	}
+	  if (deliver_selectstring)
+	    {
+	    extra[extracount++] = f.deliver_selectstring_regex ? US"-Rr" : US"-R";
+	    extra[extracount++] = deliver_selectstring;
+	    }
 
-      if (deliver_selectstring_sender)
-	{
-	extra[extracount++] = f.deliver_selectstring_sender_regex
-	  ? US"-Sr" : US"-S";
-	extra[extracount++] = deliver_selectstring_sender;
-	}
+	  if (deliver_selectstring_sender)
+	    {
+	    extra[extracount++] = f.deliver_selectstring_sender_regex
+	      ? US"-Sr" : US"-S";
+	    extra[extracount++] = deliver_selectstring_sender;
+	    }
 
-      /* Overlay this process with a new execution. */
+	  /* Overlay this process with a new execution. */
 
-      (void)child_exec_exim(CEE_EXEC_PANIC, FALSE, NULL, FALSE, extracount,
-	extra[0], extra[1], extra[2], extra[3], extra[4], extra[5], extra[6]);
+	  (void)child_exec_exim(CEE_EXEC_PANIC, FALSE, NULL, FALSE, extracount,
+	    extra[0], extra[1], extra[2], extra[3], extra[4], extra[5], extra[6]);
 
-      /* Control never returns here. */
-      }
+	  /* Control never returns here. */
+	  }
 
-    /* No need to re-exec; SIGALRM remains set to the default handler */
+	/* No need to re-exec; SIGALRM remains set to the default handler */
 
 #ifndef DISABLE_QUEUE_RAMP
-    if (*queuerun_msgid)
-      {
-      log_write(0, LOG_MAIN, "notify triggered queue run");
-      f.queue_2stage = FALSE;
-      queue_run(queuerun_msgid, queuerun_msgid, FALSE);
-      }
-    else
+	if (*queuerun_msgid)
+	  {
+	  log_write(0, LOG_MAIN, "notify triggered queue run");
+	  f.queue_2stage = FALSE;
+	  queue_run(q, queuerun_msgid, queuerun_msgid, FALSE);
+	  }
+	else
 #endif
-      queue_run(NULL, NULL, FALSE);
-    exim_underbar_exit(EXIT_SUCCESS);
-    }
+	  queue_run(q, NULL, NULL, FALSE);
+	exim_underbar_exit(EXIT_SUCCESS);
+	}
 
-  if (pid < 0)
-    {
-    log_write(0, LOG_MAIN|LOG_PANIC, "daemon: fork of queue-runner "
-      "process failed: %s", strerror(errno));
-    log_close_all();
-    }
-  else
-    {
-    for (int i = 0; i < local_queue_run_max; ++i)
-      if (queue_pid_slots[i] <= 0)
+      if (pid < 0)
 	{
-	queue_pid_slots[i] = pid;
-	queue_run_count++;
-	break;
+	log_write(0, LOG_MAIN|LOG_PANIC, "daemon: fork of queue-runner "
+	  "process failed: %s", strerror(errno));
+	log_close_all();
 	}
-    DEBUG(D_any) debug_printf("%d queue-runner process%s running\n",
-      queue_run_count, queue_run_count == 1 ? "" : "es");
+      else
+	{
+	for (int i = 0; i < local_queue_run_max; ++i)
+	  if (queue_runner_slots[i].pid <= 0)
+	    {
+	    queue_runner_slots[i].pid = pid;
+	    queue_runner_slots[i].queue_name = q->name;
+	    q->run_count++;
+	    queue_run_count++;
+	    break;
+	    }
+	DEBUG(D_any) debug_printf("%d queue-runner process%s running\n",
+	  queue_run_count, queue_run_count == 1 ? "" : "es");
+	}
+      }
     }
-  }
-
-/* Reset the alarm clock */
 
 sigalrm_seen = FALSE;
 #ifndef DISABLE_QUEUE_RAMP
-if (*queuerun_msgid)
+if (*queuerun_msgid)		/* it was a fast-ramp kick */
   *queuerun_msgid = 0;
-else
+else				/* periodic or one-time queue run */
 #endif
-  ALARM(queue_interval);
+  {		/* Impose a minimum 1s tick, even when a run was outstanding */
+  int interval = next_qrunner_interval();
+  if (interval <= 0) interval = 1;
+
+  if (qrunners)			/* there are still periodic qrunners */
+    {
+    ALARM(interval);
+    return interval;
+    }
+  }
+return 0;
 }
 
+
+
+
+const uschar *
+describe_queue_runners(void)
+{
+gstring * g = NULL;
+
+if (!is_multiple_qrun()) return US"no queue runs";
+
+for (qrunner * q = qrunners; q; q = q->next)
+  {
+  g = string_catn(g, US"-q", 2);
+  if (q->name) g = string_append(g, 3, US"G", q->name, US"/");
+  g = string_cat(g, readconf_printtime(q->interval));
+  g = string_catn(g, US" ", 1);
+  }
+gstring_trim(g, 1);
+gstring_release_unused(g);
+return string_from_gstring(g);
+}
+
+
 /*************************************************
 *              Exim Daemon Mainline              *
 *************************************************/
@@ -1557,7 +1687,32 @@ struct pollfd * fd_polls, * tls_watch_poll = NULL, * dnotify_poll = NULL;
 int listen_socket_count = 0, poll_fd_count;
 ip_address_item * addresses = NULL;
 time_t last_connection_time = (time_t)0;
-int local_queue_run_max = atoi(CS expand_string(queue_run_max));
+int local_queue_run_max = 0;
+BOOL queue_run_max_has_dollar;
+
+if (is_multiple_qrun())
+
+  /* Nuber of runner-tracking structs needed:  If the option queue_run_max has
+  no expandable elements then it is the overall maximum; else we assume it
+  depends on the queue name, and add them up to get the maximum.
+  Evaluate both that and the individual limits. */
+
+  if (Ustrchr(queue_run_max, '$') != NULL)
+    {
+    for (qrunner * q = qrunners; q; q = q->next)
+      {
+      queue_name = q->name;
+      local_queue_run_max +=
+	(q->run_max = atoi(CS expand_string(queue_run_max)));
+      }
+    queue_name = US"";
+    }
+  else
+    {
+    local_queue_run_max = atoi(CS expand_string(queue_run_max));
+    for (qrunner * q = qrunners; q; q = q->next)
+      q->run_max = local_queue_run_max;
+    }
 
 process_purpose = US"daemon";
 
@@ -2216,10 +2371,11 @@ originator_login = (pw = getpwuid(exim_uid))
 /* Get somewhere to keep the list of queue-runner pids if we are keeping track
 of them (and also if we are doing queue runs). */
 
-if (queue_interval > 0 && local_queue_run_max > 0)
+if (is_multiple_qrun() && local_queue_run_max > 0)
   {
-  queue_pid_slots = store_get(local_queue_run_max * sizeof(pid_t), GET_UNTAINTED);
-  for (int i = 0; i < local_queue_run_max; i++) queue_pid_slots[i] = 0;
+  queue_runner_slot_count = local_queue_run_max;
+  queue_runner_slots = store_get(local_queue_run_max * sizeof(runner_slot), GET_UNTAINTED);
+  memset(queue_runner_slots, 0, local_queue_run_max * sizeof(runner_slot));
   }
 
 /* Set up the handler for termination of child processes, and the one
@@ -2233,9 +2389,12 @@ os_non_restarting_signal(SIGTERM, main_sigterm_handler);
 os_non_restarting_signal(SIGINT, main_sigterm_handler);
 
 /* If we are to run the queue periodically, pretend the alarm has just gone
-off. This will cause the first queue-runner to get kicked off straight away. */
+off. This will cause the first queue-runner to get kicked off straight away.
+Get an initial sort of the list of queues, to prioritize the initial q-runs */
 
-sigalrm_seen = (queue_interval > 0);
+
+if ((sigalrm_seen = is_multiple_qrun()))
+  (void) next_qrunner_interval();
 
 /* Log the start up of a daemon - at least one of listening or queue running
 must be set up. */
@@ -2264,20 +2423,16 @@ else if (f.daemon_listen)
   int smtps_ports = 0;
   ip_address_item * ipa;
   uschar * p;
-  uschar * qinfo = queue_interval > 0
-    ? string_sprintf("-q%s%s",
-	f.queue_2stage ? "q" : "", readconf_printtime(queue_interval))
-    : US"no queue runs";
+  const uschar * qinfo = describe_queue_runners();
 
   /* Build a list of listening addresses in big_buffer, but limit it to 10
   items. The style is for backwards compatibility.
 
-  It is now possible to have some ports listening for SMTPS (the old,
-  deprecated protocol that starts TLS without using STARTTLS), and others
-  listening for standard SMTP. Keep their listings separate. */
+  It is possible to have some ports listening for SMTPS (as opposed to TLS
+  startted by STARTTLS), and others listening for standard SMTP. Keep their
+  listings separate. */
 
   for (int j = 0, i; j < 2; j++)
-    {
     for (i = 0, ipa = addresses; i < 10 && ipa; i++, ipa = ipa->next)
       {
       /* First time round, look for SMTP ports; second time round, look for
@@ -2315,7 +2470,7 @@ else if (f.daemon_listen)
 	       && Ustrcmp(ipa->address, i2->address) == 0
 	       )
 	      {				/* found; append port to list */
-	      for (p = i2->log; *p; ) p++;	/* end of existing string */
+	      for (p = i2->log; *p; ) p++;	/* end of existing string   { */
 	      if (*--p == '}') *p = '\0';	/* drop EOL */
 	      while (isdigit(*--p)) ;		/* char before port */
 
@@ -2331,7 +2486,6 @@ else if (f.daemon_listen)
 	  }
 	}
       }
-    }
 
   p = big_buffer;
   for (int j = 0, i; j < 2; j++)
@@ -2367,11 +2521,9 @@ else if (f.daemon_listen)
     version_string, qinfo, big_buffer);
   }
 
-else
+else	/* no listening sockets, only queue-runs */
   {
-  uschar * s = *queue_name
-    ? string_sprintf("-qG%s/%s", queue_name, readconf_printtime(queue_interval))
-    : string_sprintf("-q%s", readconf_printtime(queue_interval));
+  const uschar * s = describe_queue_runners();
   log_write(0, LOG_MAIN,
     "exim %s daemon started: pid=%d, %s, not listening for SMTP",
     version_string, getpid(), s);
@@ -2445,6 +2597,8 @@ report_time_since(&timestamp_startup, US"daemon loop start");	/* testcase 0022 *
 
 for (;;)
   {
+  int nolisten_sleep = 60;
+
   if (sigterm_seen)
     daemon_die();	/* Does not return */
 
@@ -2458,7 +2612,8 @@ for (;;)
     if (inetd_wait_timeout > 0)
       daemon_inetd_wtimeout(last_connection_time);	/* Might not return */
     else
-      daemon_qrun(local_queue_run_max, fd_polls, listen_socket_count);
+      nolisten_sleep =
+	daemon_qrun(local_queue_run_max, fd_polls, listen_socket_count);
 
 
   /* Sleep till a connection happens if listening, and handle the connection if
@@ -2663,7 +2818,7 @@ for (;;)
   else
     {
     struct pollfd p;
-    poll(&p, 0, queue_interval * 1000);
+    poll(&p, 0, nolisten_sleep * 1000);
     handle_ending_processes();
     }
 
diff --git a/src/src/exim.c b/src/src/exim.c
index dcc71ea45..9cba8d51e 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1687,6 +1687,33 @@ else
 
 
 
+/*************************************************
+*          Queue-runner operations               *
+*************************************************/
+
+/* Prefix a new qrunner descriptor to the qrunners list */
+
+static qrunner *
+alloc_qrunner(void)
+{
+qrunner * q = qrunners;
+qrunners = store_get(sizeof(qrunner), GET_UNTAINTED);
+memset(qrunners, 0, sizeof(qrunner));		/* default queue, zero interval */
+qrunners->next = q;
+qrunners->next_tick = time(NULL);		/* run right away */
+return qrunners;
+}
+
+static qrunner *
+alloc_onetime_qrunner(void)
+{
+qrunners = store_get_perm(sizeof(qrunner), GET_UNTAINTED);
+memset(qrunners, 0, sizeof(qrunner));		/* default queue, zero interval */
+qrunners->next_tick = time(NULL);		/* run right away */
+qrunners->run_max = 1;
+}
+
+
 /*************************************************
 *          Entry point and high-level code       *
 *************************************************/
@@ -2051,7 +2078,7 @@ this is a smail convention. */
 if ((namelen == 4 && Ustrcmp(argv[0], "runq") == 0) ||
     (namelen  > 4 && Ustrncmp(argv[0] + namelen - 5, "/runq", 5) == 0))
   {
-  queue_interval = 0;
+  alloc_onetime_qrunner();
   receiving_message = FALSE;
   called_as = US"-runq";
   }
@@ -2110,7 +2137,7 @@ on the second character (the one after '-'), to save some effort. */
   BOOL badarg = FALSE;
   uschar * arg = argv[i];
   uschar * argrest;
-  int switchchar;
+  uschar switchchar;
 
   /* An argument not starting with '-' is the start of a recipients list;
   break out of the options-scanning loop. */
@@ -3504,87 +3531,100 @@ on the second character (the one after '-'), to save some effort. */
       }
     break;
 
+    /* -q:  set up queue runs */
 
     case 'q':
-    receiving_message = FALSE;
-    if (queue_interval >= 0)
-      exim_fail("exim: -q specified more than once\n");
-
-    /* -qq...: Do queue runs in a 2-stage manner */
-
-    if (*argrest == 'q')
       {
-      f.queue_2stage = TRUE;
-      argrest++;
-      }
+      BOOL two_stage, first_del, force, thaw = FALSE, local;
 
-    /* -qi...: Do only first (initial) deliveries */
+      receiving_message = FALSE;
 
-    if (*argrest == 'i')
-      {
-      f.queue_run_first_delivery = TRUE;
-      argrest++;
-      }
+      /* -qq...: Do queue runs in a 2-stage manner */
 
-    /* -qf...: Run the queue, forcing deliveries
-       -qff..: Ditto, forcing thawing as well */
+      if ((two_stage = *argrest == 'q'))
+	argrest++;
 
-    if (*argrest == 'f')
-      {
-      f.queue_run_force = TRUE;
-      if (*++argrest == 'f')
-        {
-        f.deliver_force_thaw = TRUE;
-        argrest++;
-        }
-      }
+      /* -qi...: Do only first (initial) deliveries */
 
-    /* -q[f][f]l...: Run the queue only on local deliveries */
+      if ((first_del = *argrest == 'i'))
+	argrest++;
 
-    if (*argrest == 'l')
-      {
-      f.queue_run_local = TRUE;
-      argrest++;
-      }
+      /* -qf...: Run the queue, forcing deliveries
+	 -qff..: Ditto, forcing thawing as well */
 
-    /* -q[f][f][l][G<name>]... Work on the named queue */
+      if ((force = *argrest == 'f'))
+	if ((thaw = *++argrest == 'f'))
+	  argrest++;
 
-    if (*argrest == 'G')
-      {
-      int i;
-      for (argrest++, i = 0; argrest[i] && argrest[i] != '/'; ) i++;
-      exim_len_fail_toolong(i, EXIM_DRIVERNAME_MAX, "-q*G<name>");
-      queue_name = string_copyn(argrest, i);
-      argrest += i;
-      if (*argrest == '/') argrest++;
-      }
+      /* -q[f][f]l...: Run the queue only on local deliveries */
+
+      if ((local = *argrest == 'l'))
+	argrest++;
 
-    /* -q[f][f][l][G<name>]: Run the queue, optionally forced, optionally local
-    only, optionally named, optionally starting from a given message id. */
+      /* -q[f][f][l][G<name>]... Work on the named queue */
 
-    if (!(list_queue || count_queue))
-      if (  !*argrest
-	 && (i + 1 >= argc || argv[i+1][0] == '-' || mac_ismsgid(argv[i+1])))
+      if (*argrest == 'G')
 	{
-	queue_interval = 0;
-	if (i+1 < argc && mac_ismsgid(argv[i+1]))
-	  start_queue_run_id = string_copy_taint(argv[++i], GET_TAINTED);
-	if (i+1 < argc && mac_ismsgid(argv[i+1]))
-	  stop_queue_run_id = string_copy_taint(argv[++i], GET_TAINTED);
+	int i;
+	for (argrest++, i = 0; argrest[i] && argrest[i] != '/'; ) i++;
+	exim_len_fail_toolong(i, EXIM_DRIVERNAME_MAX, "-q*G<name>");
+	queue_name = string_copyn(argrest, i);
+	argrest += i;
+	if (*argrest == '/') argrest++;
 	}
 
-    /* -q[f][f][l][G<name>/]<n>: Run the queue at regular intervals, optionally
-    forced, optionally local only, optionally named. */
+      /* -q[f][f][l][G<name>]: Run the queue, optionally forced, optionally local
+      only, optionally named, optionally starting from a given message id. */
 
-      else if ((queue_interval = readconf_readtime(*argrest ? argrest : argv[++i],
-						  0, FALSE)) <= 0)
-	exim_fail("exim: bad time value %s: abandoned\n", argv[i]);
-    break;
+      if (!(list_queue || count_queue))
+	{
+	qrunner * q;
+
+	if (  !*argrest
+	   && (i + 1 >= argc || argv[i+1][0] == '-' || mac_ismsgid(argv[i+1])))
+	  {
+	  q = alloc_onetime_qrunner();
+	  if (i+1 < argc && mac_ismsgid(argv[i+1]))
+	    start_queue_run_id = string_copy_taint(argv[++i], GET_TAINTED);
+	  if (i+1 < argc && mac_ismsgid(argv[i+1]))
+	    stop_queue_run_id = string_copy_taint(argv[++i], GET_TAINTED);
+	  }
+
+      /* -q[f][f][l][G<name>/]<n>: Run the queue at regular intervals, optionally
+      forced, optionally local only, optionally named. */
+
+	else
+	  {
+	  int intvl = readconf_readtime(*argrest ? argrest : argv[++i], 0, FALSE);
+	  if (intvl <= 0)
+	    exim_fail("exim: bad time value %s: abandoned\n", argv[i]);
+
+	  for (qrunner * qq = qrunners; qq; qq = qq->next)
+	    if (  queue_name && qq->name && Ustrcmp(queue_name, qq->name) == 0
+	       || !queue_name && !qq->name)
+	      exim_fail("exim: queue-runner specified more than once\n");
+
+	  q = alloc_qrunner();
+	  q->interval = intvl;
+	  }
+
+	q->name = *queue_name ? queue_name : NULL;	/* will be NULL for the default queue */
+	q->queue_run_force = force;
+	q->deliver_force_thaw = thaw;
+	q->queue_run_first_delivery = first_del;
+	q->queue_run_local = local;
+	q->queue_2stage = two_stage;
+	}
+
+      break;
+      }
 
 
     case 'R':   /* Synonymous with -qR... */
+    case 'S':   /* Synonymous with -qS... */
       {
-      const uschar *tainted_selectstr;
+      const uschar * tainted_selectstr;
+      uschar * s;
 
       receiving_message = FALSE;
 
@@ -3594,20 +3634,28 @@ on the second character (the one after '-'), to save some effort. */
        -Rrf:  Regex and force
        -Rrff: Regex and force and thaw
 
+       -S...: Like -R but works on sender.
+
     in all cases provided there are no further characters in this
     argument. */
 
+      alloc_onetime_qrunner();
+      qrunners->queue_2stage = f.queue_2stage;
       if (*argrest)
 	for (int i = 0; i < nelem(rsopts); i++)
 	  if (Ustrcmp(argrest, rsopts[i]) == 0)
 	    {
-	    if (i != 2) f.queue_run_force = TRUE;
-	    if (i >= 2) f.deliver_selectstring_regex = TRUE;
-	    if (i == 1 || i == 4) f.deliver_force_thaw = TRUE;
+	    if (i != 2) qrunners->queue_run_force = TRUE;
+	    if (i >= 2)
+	      if (switchchar == 'R')
+		f.deliver_selectstring_regex = TRUE;
+	      else
+		f.deliver_selectstring_sender_regex = TRUE;
+	    if (i == 1 || i == 4) qrunners->deliver_force_thaw = TRUE;
 	    argrest += Ustrlen(rsopts[i]);
 	    }
 
-    /* -R: Set string to match in addresses for forced queue run to
+    /* -R or -S: Set string to match in addresses for forced queue run to
     pick out particular messages. */
 
       /* Avoid attacks from people providing very long strings, and do so before
@@ -3617,58 +3665,22 @@ on the second character (the one after '-'), to save some effort. */
       else if (i+1 < argc)
 	tainted_selectstr = argv[++i];
       else
-	exim_fail("exim: string expected after -R\n");
-      deliver_selectstring = string_copy_taint(
+	exim_fail("exim: string expected after %s\n", switchchar == 'R' ? "-R" : "-S");
+
+      s = string_copy_taint(
 	exim_str_fail_toolong(tainted_selectstr, EXIM_EMAILADDR_MAX, "-R"),
 	GET_TAINTED);
-      }
-    break;
-
-    /* -r: an obsolete synonym for -f (see above) */
-
-
-    /* -S: Like -R but works on sender. */
-
-    case 'S':   /* Synonymous with -qS... */
-      {
-      const uschar *tainted_selectstr;
-
-      receiving_message = FALSE;
-
-    /* -Sf:   As -S (below) but force all deliveries,
-       -Sff:  Ditto, but also thaw all frozen messages,
-       -Sr:   String is regex
-       -Srf:  Regex and force
-       -Srff: Regex and force and thaw
-
-    in all cases provided there are no further characters in this
-    argument. */
-
-      if (*argrest)
-	for (int i = 0; i < nelem(rsopts); i++)
-	  if (Ustrcmp(argrest, rsopts[i]) == 0)
-	    {
-	    if (i != 2) f.queue_run_force = TRUE;
-	    if (i >= 2) f.deliver_selectstring_sender_regex = TRUE;
-	    if (i == 1 || i == 4) f.deliver_force_thaw = TRUE;
-	    argrest += Ustrlen(rsopts[i]);
-	    }
-
-    /* -S: Set string to match in addresses for forced queue run to
-    pick out particular messages. */
 
-      if (*argrest)
-	tainted_selectstr = argrest;
-      else if (i+1 < argc)
-	tainted_selectstr = argv[++i];
+      if (switchchar == 'R')
+	deliver_selectstring = s;
       else
-	exim_fail("exim: string expected after -S\n");
-      deliver_selectstring_sender = string_copy_taint(
-	exim_str_fail_toolong(tainted_selectstr, EXIM_EMAILADDR_MAX, "-S"),
-	GET_TAINTED);
+	deliver_selectstring_sender = s;
       }
     break;
 
+
+    /* -r: an obsolete synonym for -f (see above) */
+
     /* -Tqt is an option that is exclusively for use by the testing suite.
     It is not recognized in other circumstances. It allows for the setting up
     of explicit "queue times" so that various warning/retry things can be
@@ -3777,9 +3789,8 @@ on the second character (the one after '-'), to save some effort. */
 
 /* If -R or -S have been specified without -q, assume a single queue run. */
 
- if (  (deliver_selectstring || deliver_selectstring_sender)
-    && queue_interval < 0)
-  queue_interval = 0;
+ if ((deliver_selectstring || deliver_selectstring_sender) && !qrunners)
+  alloc_onetime_qrunner();
 
 
 END_ARG:
@@ -3791,22 +3802,22 @@ if (usage_wanted) exim_usage(called_as);
 
 /* Arguments have been processed. Check for incompatibilities. */
 if (  (  (smtp_input || extract_recipients || recipients_arg < argc)
-      && (  f.daemon_listen || queue_interval >= 0 || bi_option
+      && (  f.daemon_listen || qrunners || bi_option
 	 || test_retry_arg >= 0 || test_rewrite_arg >= 0
 	 || filter_test != FTEST_NONE
 	 || msg_action_arg > 0 && !one_msg_action
       )  )
    || (  msg_action_arg > 0
-      && (  f.daemon_listen || queue_interval > 0 || list_options
+      && (  f.daemon_listen || is_multiple_qrun() || list_options
 	 || checking && msg_action != MSG_LOAD
 	 || bi_option || test_retry_arg >= 0 || test_rewrite_arg >= 0
       )  )
-   || (  (f.daemon_listen || queue_interval > 0)
+   || (  (f.daemon_listen || is_multiple_qrun())
       && (  sender_address || list_options || list_queue || checking
 	 || bi_option
       )  )
-   || f.daemon_listen && queue_interval == 0
-   || f.inetd_wait_mode && queue_interval >= 0
+   || f.daemon_listen && is_onetime_qrun()
+   || f.inetd_wait_mode && qrunners
    || (  list_options
       && (  checking || smtp_input || extract_recipients
 	 || filter_test != FTEST_NONE || bi_option
@@ -3822,7 +3833,7 @@ if (  (  (smtp_input || extract_recipients || recipients_arg < argc)
    || (  smtp_input
       && (sender_address || filter_test != FTEST_NONE || extract_recipients)
       )
-   || deliver_selectstring && queue_interval < 0
+   || deliver_selectstring && !qrunners
    || msg_action == MSG_LOAD && (!expansion_test || expansion_test_message)
    )
   exim_fail("exim: incompatible command-line options or arguments\n");
@@ -4444,7 +4455,7 @@ if (!f.admin_user)
   if (  deliver_give_up || f.daemon_listen || malware_test_file
      || count_queue && queue_list_requires_admin
      || list_queue && queue_list_requires_admin
-     || queue_interval >= 0 && prod_requires_admin
+     || qrunners && prod_requires_admin
      || queue_name_dest && prod_requires_admin
      || debugset && !f.running_in_test_harness
      )
@@ -4460,7 +4471,7 @@ regression testing. */
 if (  real_uid != root_uid && real_uid != exim_uid
    && (  continue_hostname
       || (  f.dont_deliver
-	 && (queue_interval >= 0 || f.daemon_listen || msg_action_arg > 0)
+	 && (qrunners || f.daemon_listen || msg_action_arg > 0)
       )  )
    && !f.running_in_test_harness
    )
@@ -4577,11 +4588,11 @@ to the state Exim usually runs in. */
 if (  !unprivileged				/* originally had root AND */
    && !removed_privilege			/* still got root AND      */
    && !f.daemon_listen				/* not starting the daemon */
-   && queue_interval <= 0			/* (either kind of daemon) */
+   && (!qrunners || is_onetime_qrun())		/* (either kind of daemon) */
    && (						/*    AND EITHER           */
          deliver_drop_privilege			/* requested unprivileged  */
       || (					/*       OR                */
-            queue_interval < 0			/* not running the queue   */
+            !qrunners				/* not running the queue   */
          && (  msg_action_arg < 0		/*       and               */
             || msg_action != MSG_DELIVER	/* not delivering          */
 	    )					/*       and               */
@@ -4696,7 +4707,7 @@ if (msg_action_arg > 0 && msg_action != MSG_DELIVER && msg_action != MSG_LOAD)
   }
 
 /* We used to set up here to skip reading the ACL section, on
- (msg_action_arg > 0 || (queue_interval == 0 && !f.daemon_listen)
+ (msg_action_arg > 0 || (is_onetime_qrun() && !f.daemon_listen)
 Now, since the intro of the ${acl } expansion, ACL definitions may be
 needed in transports so we lost the optimisation. */
 
@@ -4947,18 +4958,9 @@ if (msg_action_arg > 0 && msg_action != MSG_LOAD)
 /* If only a single queue run is requested, without SMTP listening, we can just
 turn into a queue runner, with an optional starting message id. */
 
-if (queue_interval == 0 && !f.daemon_listen)
+if (is_onetime_qrun() && !f.daemon_listen)
   {
-  DEBUG(D_queue_run) debug_printf("Single queue run%s%s%s%s\n",
-    start_queue_run_id ? US" starting at " : US"",
-    start_queue_run_id ? start_queue_run_id: US"",
-    stop_queue_run_id ?  US" stopping at " : US"",
-    stop_queue_run_id ?  stop_queue_run_id : US"");
-  if (*queue_name)
-    set_process_info("running the '%s' queue (single queue run)", queue_name);
-  else
-    set_process_info("running the queue (single queue run)");
-  queue_run(start_queue_run_id, stop_queue_run_id, FALSE);
+  single_queue_run(qrunners, start_queue_run_id, stop_queue_run_id);
   exim_exit(EXIT_SUCCESS);
   }
 
@@ -5083,7 +5085,7 @@ returns. We leave this till here so that the originator_ fields are available
 for incoming messages via the daemon. The daemon cannot be run in mua_wrapper
 mode. */
 
-if (f.daemon_listen || f.inetd_wait_mode || queue_interval > 0)
+if (f.daemon_listen || f.inetd_wait_mode || is_multiple_qrun())
   {
   if (mua_wrapper)
     {
diff --git a/src/src/functions.h b/src/src/functions.h
index 961db2dc0..37f0a57bc 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -411,7 +411,7 @@ extern void    queue_list(int, uschar **, int);
 #ifndef DISABLE_QUEUE_RAMP
 extern void    queue_notify_daemon(const uschar * hostname);
 #endif
-extern void    queue_run(uschar *, uschar *, BOOL);
+extern void    queue_run(qrunner *, uschar *, uschar *, BOOL);
 
 extern int     random_number(int);
 extern const uschar *rc_to_string(int);
@@ -498,6 +498,7 @@ extern int     sieve_interpret(const uschar *, int, const uschar *,
 		 const uschar *, const uschar *, const uschar *,
 		 address_item **, uschar **);
 extern void    sigalrm_handler(int);
+extern void    single_queue_run(qrunner *, uschar *, uschar *);
 extern int     smtp_boundsock(smtp_connect_args *);
 extern void    smtp_closedown(uschar *);
 extern void    smtp_command_timeout_exit(void) NORETURN;
@@ -1368,6 +1369,22 @@ int res;
 return !s || !*s || (res = Uatoi(s)) == 0 ? UNLIMITED_ADDRS : res;
 }
 
+/******************************************************************************/
+/* Queue-runner operations */
+
+static inline BOOL
+is_onetime_qrun(void)
+{
+return qrunners && !qrunners->next && qrunners->interval == 0;
+}
+
+static inline BOOL
+is_multiple_qrun(void)
+{
+return qrunners && (qrunners->interval > 0 || qrunners->next);
+}
+
+
 # endif	/* !COMPILE_UTILITY */
 
 /******************************************************************************/
diff --git a/src/src/globals.c b/src/src/globals.c
index 7af345465..a4b2c6a9c 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -291,8 +291,6 @@ struct global_flags f =
 
 	.queue_2stage           = FALSE,
 	.queue_only_policy      = FALSE,
-	.queue_run_first_delivery = FALSE,
-	.queue_run_force        = FALSE,
 	.queue_run_local        = FALSE,
 	.queue_running          = FALSE,
 	.queue_smtp             = FALSE,
@@ -1248,6 +1246,8 @@ uschar *prvscheck_keynum       = NULL;
 uschar *prvscheck_result       = NULL;
 
 
+qrunner *qrunners	       = NULL;
+
 const uschar *qualify_domain_recipient = NULL;
 uschar *qualify_domain_sender  = NULL;
 uschar *queue_domains          = NULL;
diff --git a/src/src/globals.h b/src/src/globals.h
index f2e147670..914e2d0f9 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -257,8 +257,6 @@ extern struct global_flags {
 
  BOOL   queue_2stage			:1; /* Run queue in 2-stage manner */
  BOOL   queue_only_policy		:1; /* ACL or local_scan wants queue_only */
- BOOL   queue_run_first_delivery	:1; /* If TRUE, first deliveries only */
- BOOL   queue_run_force			:1; /* TRUE to force during queue run */
  BOOL   queue_run_local			:1; /* Local deliveries only in queue run */
  BOOL   queue_running			:1; /* TRUE for queue running process and */
  BOOL   queue_smtp			:1; /* Disable all immediate SMTP (-odqs)*/
@@ -837,6 +835,8 @@ extern uschar *prvscheck_address;      /* Set during prvscheck expansion item */
 extern uschar *prvscheck_keynum;       /* Set during prvscheck expansion item */
 extern uschar *prvscheck_result;       /* Set during prvscheck expansion item */
 
+extern qrunner *qrunners;	       /* tracking data for queues */
+
 extern const uschar *qualify_domain_recipient; /* Domain to qualify recipients with */
 extern uschar *qualify_domain_sender;  /* Domain to qualify senders with */
 extern uschar *queue_domains;          /* Queue these domains */
diff --git a/src/src/macros.h b/src/src/macros.h
index 585067fc9..3b0293b97 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -1113,9 +1113,9 @@ should not be one active. */
 
 #define NOTIFIER_SOCKET_NAME	"exim_daemon_notify"
 /* Notify message types */
-#define NOTIFY_MSG_QRUN		1
-#define NOTIFY_QUEUE_SIZE_REQ	2
-#define NOTIFY_REGEX		3
+#define NOTIFY_MSG_QRUN		1	/* 2stage qrun fast-ramp trigger */
+#define NOTIFY_QUEUE_SIZE_REQ	2	/* obtain current queue count */
+#define NOTIFY_REGEX		3	/* an RE for caching */
 
 /* Flags for match_check_string() */
 typedef unsigned mcs_flags;
diff --git a/src/src/queue.c b/src/src/queue.c
index f86e24b42..d01cde655 100644
--- a/src/src/queue.c
+++ b/src/src/queue.c
@@ -325,8 +325,8 @@ previous lexically lesser one if the given stop message doesn't exist. Because
 a queue run can take some time, stat each file before forking, in case it has
 been delivered in the meantime by some other means.
 
-The global variables queue_run_force and queue_run_local may be set to cause
-forced deliveries or local-only deliveries, respectively.
+The qrun descriptor  variables queue_run_force and queue_run_local may be set to
+cause forced deliveries or local-only deliveries, respectively.
 
 If deliver_selectstring[_sender] is not NULL, skip messages whose recipients do
 not contain the string. As this option is typically used when a machine comes
@@ -339,6 +339,7 @@ is set so that routing is done for all messages. Thus in the second run those
 that are routed to the same host should go down the same SMTP connection.
 
 Arguments:
+  q	     queue-runner descriptor
   start_id   message id to start at, or NULL for all
   stop_id    message id to end at, or NULL for all
   recurse    TRUE if recursing for 2-stage run
@@ -347,10 +348,10 @@ Returns:     nothing
 */
 
 void
-queue_run(uschar *start_id, uschar *stop_id, BOOL recurse)
+queue_run(qrunner * q, uschar * start_id, uschar * stop_id, BOOL recurse)
 {
-BOOL force_delivery = f.queue_run_force || deliver_selectstring != NULL ||
-  deliver_selectstring_sender != NULL;
+BOOL force_delivery = q->queue_run_force
+  || deliver_selectstring || deliver_selectstring_sender;
 const pcre2_code *selectstring_regex = NULL;
 const pcre2_code *selectstring_regex_sender = NULL;
 uschar *log_detail = NULL;
@@ -363,6 +364,13 @@ BOOL single_id = FALSE;
 report_time_since(&timestamp_startup, US"queue_run start");
 #endif
 
+/* Copy the legacy globals from the newer per-qrunner-desc */
+
+queue_name =		q->name ? q->name : US"";
+f.queue_2stage =        q->queue_2stage;
+f.deliver_force_thaw =  q->deliver_force_thaw;
+f.queue_run_local =     q->queue_run_local;
+
 /* Cancel any specific queue domains. Turn off the flag that causes SMTP
 deliveries not to happen, unless doing a 2-stage queue run, when the SMTP flag
 gets set. Save the queue_runner's pid and the flag that indicates any
@@ -371,7 +379,7 @@ on TCP/IP channels have queue_run_pid set, but not queue_running. */
 
 queue_domains = NULL;
 queue_smtp_domains = NULL;
-f.queue_smtp = f.queue_2stage;
+f.queue_smtp = q->queue_2stage;
 
 queue_run_pid = getpid();
 f.queue_running = TRUE;
@@ -383,11 +391,11 @@ if (!recurse)
   uschar extras[8];
   uschar *p = extras;
 
-  if (f.queue_2stage) *p++ = 'q';
-  if (f.queue_run_first_delivery) *p++ = 'i';
-  if (f.queue_run_force) *p++ = 'f';
-  if (f.deliver_force_thaw) *p++ = 'f';
-  if (f.queue_run_local) *p++ = 'l';
+  if (q->queue_2stage)		*p++ = 'q';
+  if (q->queue_run_first_delivery) *p++ = 'i';
+  if (q->queue_run_force)	*p++ = 'f';
+  if (q->deliver_force_thaw)	*p++ = 'f';
+  if (q->queue_run_local)	*p++ = 'l';
   *p = 0;
 
   p = big_buffer;
@@ -399,25 +407,25 @@ if (!recurse)
   if (deliver_selectstring)
     {
     snprintf(CS p, big_buffer_size - (p - big_buffer), " -R%s %s",
-      f.deliver_selectstring_regex? "r" : "", deliver_selectstring);
+      f.deliver_selectstring_regex ? "r" : "", deliver_selectstring);
     p += Ustrlen(CCS p);
     }
 
   if (deliver_selectstring_sender)
     {
     snprintf(CS p, big_buffer_size - (p - big_buffer), " -S%s %s",
-      f.deliver_selectstring_sender_regex? "r" : "", deliver_selectstring_sender);
+      f.deliver_selectstring_sender_regex ? "r" : "", deliver_selectstring_sender);
     p += Ustrlen(CCS p);
     }
 
   log_detail = string_copy(big_buffer);
-  if (*queue_name)
+  if (q->name)
     log_write(L_queue_run, LOG_MAIN, "Start '%s' queue run: %s",
-      queue_name, log_detail);
+      q->name, log_detail);
   else
     log_write(L_queue_run, LOG_MAIN, "Start queue run: %s", log_detail);
 
-  single_id = start_id && stop_id && !f.queue_2stage
+  single_id = start_id && stop_id && !q->queue_2stage
 	      && Ustrcmp(start_id, stop_id) == 0;
   }
 
@@ -474,7 +482,7 @@ for (int i = queue_run_in_order ? -1 : 0;
     /* Unless deliveries are forced, if deliver_queue_load_max is non-negative,
     check that the load average is low enough to permit deliveries. */
 
-    if (!f.queue_run_force && deliver_queue_load_max >= 0)
+    if (!q->queue_run_force && deliver_queue_load_max >= 0)
       if ((load_average = os_getloadavg()) > deliver_queue_load_max)
         {
         log_write(L_queue_run, LOG_MAIN, "Abandon queue run: %s (load %.2f, max %.2f)",
@@ -492,7 +500,7 @@ for (int i = queue_run_in_order ? -1 : 0;
     /* If initial of a 2-phase run, maintain a set of child procs
     to get disk parallelism */
 
-    if (f.queue_2stage && !queue_run_in_order)
+    if (q->queue_2stage && !queue_run_in_order)
       {
       int i;
       if (qpid[f.running_in_test_harness ? 0 : nelem(qpid) - 1])
@@ -530,7 +538,7 @@ for (int i = queue_run_in_order ? -1 : 0;
     message when many are not going to be delivered. */
 
     if (deliver_selectstring || deliver_selectstring_sender ||
-        f.queue_run_first_delivery)
+        q->queue_run_first_delivery)
       {
       BOOL wanted = TRUE;
       BOOL orig_dont_deliver = f.dont_deliver;
@@ -548,7 +556,7 @@ for (int i = queue_run_in_order ? -1 : 0;
       header file, we might as well do the freeze test now, and save forking
       another process. */
 
-      if (f.deliver_freeze && !f.deliver_force_thaw)
+      if (f.deliver_freeze && !q->deliver_force_thaw)
         {
         log_write(L_skip_delivery, LOG_MAIN, "Message is frozen");
         wanted = FALSE;
@@ -556,7 +564,7 @@ for (int i = queue_run_in_order ? -1 : 0;
 
       /* Check first_delivery in the case when there are no message logs. */
 
-      else if (f.queue_run_first_delivery && !f.deliver_firsttime)
+      else if (q->queue_run_first_delivery && !f.deliver_firsttime)
         {
         DEBUG(D_queue_run) debug_printf("%s: not first delivery\n", fq->text);
         wanted = FALSE;
@@ -688,7 +696,7 @@ single_item_retry:
     /* A zero return means a delivery was attempted; turn off the force flag
     for any subsequent calls unless queue_force is set. */
 
-    if (!(status & 0xffff)) force_delivery = f.queue_run_force;
+    if (!(status & 0xffff)) force_delivery = q->queue_run_force;
 
     /* If the process crashed, tell somebody */
 
@@ -723,13 +731,13 @@ single_item_retry:
     set_process_info("running queue");
 
     /* If initial of a 2-phase run, we are a child - so just exit */
-    if (f.queue_2stage && !queue_run_in_order)
+    if (q->queue_2stage && !queue_run_in_order)
       exim_exit(EXIT_SUCCESS);
 
     /* If we are in the test harness, and this is not the first of a 2-stage
     queue run, update fudged queue times. */
 
-    if (f.running_in_test_harness && !f.queue_2stage)
+    if (f.running_in_test_harness && !q->queue_2stage)
       {
       uschar * fqtnext = Ustrchr(fudged_queue_times, '/');
       if (fqtnext) fudged_queue_times = fqtnext + 1;
@@ -740,7 +748,7 @@ single_item_retry:
 
   go_around:
     /* If initial of a 2-phase run, we are a child - so just exit */
-    if (f.queue_2stage && !queue_run_in_order)
+    if (q->queue_2stage && !queue_run_in_order)
       exim_exit(EXIT_SUCCESS);
     }                                  /* End loop for list of messages */
 
@@ -767,7 +775,7 @@ single_item_retry:
 /* If queue_2stage is true, we do it all again, with the 2stage flag
 turned off. */
 
-if (f.queue_2stage)
+if (q->queue_2stage)
   {
 
   /* wait for last children */
@@ -782,22 +790,40 @@ if (f.queue_2stage)
 #ifdef MEASURE_TIMING
   report_time_since(&timestamp_startup, US"queue_run 1st phase done");
 #endif
-  f.queue_2stage = FALSE;
-  queue_run(start_id, stop_id, TRUE);
+  q->queue_2stage = f.queue_2stage = FALSE;
+  queue_run(q, start_id, stop_id, TRUE);
   }
 
 /* At top level, log the end of the run. */
 
 if (!recurse)
-  if (*queue_name)
+  if (q->name)
     log_write(L_queue_run, LOG_MAIN, "End '%s' queue run: %s",
-      queue_name, log_detail);
+      q->name, log_detail);
   else
     log_write(L_queue_run, LOG_MAIN, "End queue run: %s", log_detail);
 }
 
 
 
+void
+single_queue_run(qrunner * q, uschar * start_id, uschar * stop_id)
+{
+DEBUG(D_queue_run) debug_printf("Single queue run%s%s%s%s\n",
+  start_id ? US" starting at " : US"",
+  start_id ? start_id: US"",
+  stop_id ?  US" stopping at " : US"",
+  stop_id ?  stop_id : US"");
+
+if (*queue_name)
+  set_process_info("running the '%s' queue (single queue run)", queue_name);
+else
+  set_process_info("running the queue (single queue run)");
+queue_run(q, start_id, stop_id, FALSE);
+}
+
+
+
 
 /************************************************
 *         Count messages on the queue           *
@@ -1552,20 +1578,22 @@ if (s)
 void
 queue_notify_daemon(const uschar * msgid)
 {
-uschar buf[MESSAGE_ID_LENGTH + 2];
+int bsize = 1 + MESSAGE_ID_LENGTH + 1 + Ustrlen(queue_name) + 1;
+uschar * buf = store_get(bsize, GET_UNTAINTED);
 int fd;
 
 DEBUG(D_queue_run) debug_printf("%s: %s\n", __FUNCTION__, msgid);
 
 buf[0] = NOTIFY_MSG_QRUN;
 memcpy(buf+1, msgid, MESSAGE_ID_LENGTH+1);
+Ustrcpy(buf+1+MESSAGE_ID_LENGTH+1, queue_name);
 
 if ((fd = socket(AF_UNIX, SOCK_DGRAM, 0)) >= 0)
   {
   struct sockaddr_un sa_un = {.sun_family = AF_UNIX};
   ssize_t len = daemon_notifier_sockname(&sa_un);
 
-  if (sendto(fd, buf, sizeof(buf), 0, (struct sockaddr *)&sa_un, (socklen_t)len) < 0)
+  if (sendto(fd, buf, bsize, 0, (struct sockaddr *)&sa_un, (socklen_t)len) < 0)
     DEBUG(D_queue_run)
       debug_printf("%s: sendto %s\n", __FUNCTION__, strerror(errno));
   close(fd);
diff --git a/src/src/structs.h b/src/src/structs.h
index eae66e88d..3f237fce5 100644
--- a/src/src/structs.h
+++ b/src/src/structs.h
@@ -958,4 +958,22 @@ struct ob_dkim {
 #endif
 };
 
+
+/* per-queue-runner info */
+typedef struct qrunner {
+  struct qrunner * next;	/* list sorted by next tick */
+
+  uschar *	name;		/* NULL for the default queue */
+  unsigned	interval;	/* tick rate, seconds */
+  time_t	next_tick;	/* next run should, or should have, start(ed) */
+  unsigned	run_max;	/* concurrent queue runner limit */
+  unsigned	run_count;	/* current runners */
+
+  BOOL queue_run_force :1;
+  BOOL deliver_force_thaw :1;
+  BOOL queue_run_first_delivery :1;
+  BOOL queue_run_local :1;
+  BOOL queue_2stage :1;
+} qrunner;
+
 /* End of structs.h */

commit 2ea09d783ec32eea87d0592ac941e8849d780f9d
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Feb 14 11:56:40 2023 +0000

    Fix onetime qrunner alloc
    
    Broken-by: 1e835086d159

diff --git a/src/src/exim.c b/src/src/exim.c
index 9cba8d51e..8d13bd478 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1711,6 +1711,7 @@ qrunners = store_get_perm(sizeof(qrunner), GET_UNTAINTED);
 memset(qrunners, 0, sizeof(qrunner));		/* default queue, zero interval */
 qrunners->next_tick = time(NULL);		/* run right away */
 qrunners->run_max = 1;
+return qrunners;
 }
 
 

commit 73d6e13f9b0cc4f708210372c59893950b3f7097
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Feb 14 11:57:27 2023 +0000

    tidying

diff --git a/src/src/exim_dbmbuild.c b/src/src/exim_dbmbuild.c
index 0ea71795f..43628234d 100644
--- a/src/src/exim_dbmbuild.c
+++ b/src/src/exim_dbmbuild.c
@@ -52,6 +52,9 @@ void
 store_release_above_3(void *ptr, const char *func, int linenumber)
 { }
 gstring *
+string_catn(gstring * g, const uschar * s, int count)
+{ return NULL; }
+gstring *
 string_vformat_trc(gstring * g, const uschar * func, unsigned line,
   unsigned size_limit, unsigned flags, const char *format, va_list ap)
 { return NULL; }
diff --git a/src/src/expand.c b/src/src/expand.c
index 1daf10044..baf7134cd 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -478,8 +478,8 @@ typedef struct {
   int  *length;
 } alblock;
 
-static uschar * fn_recipients(void);
 typedef uschar * stringptr_fn_t(void);
+static uschar * fn_recipients(void);
 static uschar * fn_queue_size(void);
 
 /* This table must be kept in alphabetical order. */
@@ -685,7 +685,7 @@ static var_entry var_table[] = {
   { "qualify_domain",      vtype_stringptr,   &qualify_domain_sender },
   { "qualify_recipient",   vtype_stringptr,   &qualify_domain_recipient },
   { "queue_name",          vtype_stringptr,   &queue_name },
-  { "queue_size",          vtype_string_func, &fn_queue_size },
+  { "queue_size",          vtype_string_func, (void *) &fn_queue_size },
   { "rcpt_count",          vtype_int,         &rcpt_count },
   { "rcpt_defer_count",    vtype_int,         &rcpt_defer_count },
   { "rcpt_fail_count",     vtype_int,         &rcpt_fail_count },
diff --git a/src/src/globals.c b/src/src/globals.c
index a4b2c6a9c..78b225fbc 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -1326,7 +1326,7 @@ const pcre2_code *regex_EARLY_PIPE   = NULL;
 int    regex_cachesize		     = 0;
 const pcre2_code *regex_ismsgid      = NULL;
 const pcre2_code *regex_smtp_code    = NULL;
-const uschar *regex_vars[REGEX_VARS] = { 0 };;
+const uschar *regex_vars[REGEX_VARS] = { NULL };
 #ifdef WHITELIST_D_MACROS
 const pcre2_code *regex_whitelisted_macro = NULL;
 #endif

commit 7b5fe03f9c6c2a322dc385ab78b60ccfe1fe33fe
Author: Andrew Aitchison <exim@aitchison.me.uk>
Date:   Sun Feb 12 11:28:49 2023 +0000

    Utility: exim_msgdate

diff --git a/src/ACKNOWLEDGMENTS b/src/ACKNOWLEDGMENTS
index 22e9909c0..c318d3fea 100644
--- a/src/ACKNOWLEDGMENTS
+++ b/src/ACKNOWLEDGMENTS
@@ -357,6 +357,7 @@ David Woodhouse           Dynamic modules. Security.
 Contributors
 ------------
 Andrew Aitchison          Spotted cmdline AV scanner regression with -bmalware
+                          exim_msgdate
 Simon Arlott              Code for outbound SSL-on-connect
                           Patch implementing %M datestamping in log filenames
                           Patch restoring SIGPIPE handler for child_open_uid
diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index a290b90b0..29c037401 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -249,7 +249,8 @@ utils: $(EXIM_MONITOR) exicyclog exinext exiwhat \
         exigrep eximstats exipick exiqgrep exiqsumm \
         transport-filter.pl convert4r3 convert4r4 \
         exim_checkaccess \
-        exim_dbmbuild exim_dumpdb exim_fixdb exim_tidydb exim_lock
+        exim_dbmbuild exim_dumpdb exim_fixdb exim_tidydb exim_lock \
+        exim_msgdate
 
 
 # Targets for special-purpose configuration header builders
@@ -383,6 +384,23 @@ exigrep: config ../src/exigrep.src
 	@chmod a+x exigrep
 	@echo ">>> exigrep script built"
 
+exim_msgdate: config ../src/exim_msgdate.src
+	@rm -f exim_msgdate
+	@. ./version.sh && sed \
+	  -e "s?PROCESSED_FLAG?This file has been so processed.?"\
+	  -e "/^[ \t]*# /p" \
+	  -e "/^[ \t]*# /d" \
+	  -e "s?BIN_DIRECTORY?$(BIN_DIRECTORY)?" \
+	  -e "s?PERL_COMMAND?$(PERL_COMMAND)?" \
+	  -e "s?BASE_62?$${BASE_62:-62}?" \
+	  -e "s?CONFIGURE_FILE\"?$(CONFIGURE_FILE)\"?" \
+	  -e "s?EXIM_RELEASE_VERSION?$${EXIM_RELEASE_VERSION}?" \
+	  -e "s?EXIM_VARIANT_VERSION?$${EXIM_VARIANT_VERSION}?" \
+	  ../src/exim_msgdate.src > exim_msgdate-t
+	@mv exim_msgdate-t exim_msgdate
+	@chmod a+x exim_msgdate
+	@echo ">>> exim_msgdate script built"
+
 eximstats: config ../src/eximstats.src
 	@rm -f eximstats
 	@. ./version.sh && sed \
diff --git a/src/scripts/exim_install b/src/scripts/exim_install
index 827841ffc..e6857adaf 100755
--- a/src/scripts/exim_install
+++ b/src/scripts/exim_install
@@ -198,7 +198,7 @@ else
   set exim${EXE} ${exim_monitor} exim_dumpdb${EXE} exim_fixdb${EXE} \
       exim_tidydb${EXE} exinext exiwhat exim_dbmbuild${EXE} exicyclog \
       exigrep eximstats exipick exiqgrep exiqsumm exim_lock${EXE} \
-      exim_checkaccess
+      exim_checkaccess exim_msgdate
 fi
 
 echo $com ""
diff --git a/src/src/exim_msgdate.src b/src/src/exim_msgdate.src
new file mode 100755
index 000000000..e5c357bca
--- /dev/null
+++ b/src/src/exim_msgdate.src
@@ -0,0 +1,579 @@
+#!PERL_COMMAND -WT
+#
+# Utility to convert an exim message-id to a human readable form
+#
+# https://bugs.exim.org/show_bug.cgi?id=2956
+# Written by Andrew C Aitchison
+#
+# Copyright (c) 2023 The Exim Maintainers 2023
+# SPDX-License-Identifier: GPL-2.0-or-later
+#
+# Portions taken from exicyclog.src, which is
+#   Copyright (c) University of Cambridge, 1995 - 2015
+#   See the file NOTICE for conditions of use and distribution.
+
+# https://bugs.exim.org/show_bug.cgi?id=2956
+# https://exim.org/exim-html-current/doc/html/spec_html/ch-how_exim_receives_and_delivers_mail.html#SECTmessiden
+
+# Except when they appear in comments, the following placeholders in this
+# source are replaced when it is turned into a runnable script:
+#
+# BASE_62
+# BIN_DIRECTORY
+# CONFIGURE_FILE
+# PERL_COMMAND
+# EXIM_RELEASE_VERSION
+# EXIM_VARIANT_VERSION
+#
+# PROCESSED_FLAG
+
+use strict;
+use File::Basename;
+use Getopt::Long;
+use Pod::Usage;
+
+use constant { TRUE => 1, FALSE => 0 };
+
+if (defined $ENV{TZ}) {
+   my $zonefile = "/usr/share/zoneinfo/$ENV{TZ}";
+   if (defined $ENV{TZDIR}) {
+       if (-d $ENV{TZDIR}) {
+	   $zonefile="$ENV{TZDIR}/$ENV{TZ}";
+       } else {
+	   warn "No directory TZDIR=$ENV{TZDIR}\n"
+       }
+   }
+   warn "Cannot read timezone file $zonefile (from TZDIR/TZ)\n\t'man tzset' may help.\n"
+       unless -r $zonefile;
+}
+
+my $localhost_number;  # An Exim config value
+
+my $p_name    = basename $0;
+my $p_version = "20230203.0";
+my $p_cp      = <<EOM;
+ Copyright (c) 2023 The Exim Maintainers 2023
+
+ Portions taken from exicyclog.src, which is
+   Copyright (c) University of Cambridge, 1995 - 2015
+   See the file NOTICE for conditions of use and distribution.
+EOM
+
+$ENV{PATH} = "/bin:/usr/bin:/usr/sbin";
+
+use POSIX qw(strftime);
+
+sub main::VERSION_MESSAGE()
+{
+    print basename($0), ": $0\n";
+    print "build: EXIM_RELEASE_VERSIONEXIM_VARIANT_VERSION\n";
+    print "perl(	      runtime): $]\n";
+}
+
+my ($debug, $nodebug,
+    $optbase, $optbase36, $optbase62,
+    $optunix, $optgmt, $optlocal,
+    $optpid,
+    $opteximpath,$optconfigfile);
+
+# Cannot use $debug here, since we haven't read ARGV yet.
+if (FALSE) {
+    warn join(" ", $0, @ARGV), "\n";
+}
+
+# Case is ignored, abbreviations are allowed.
+GetOptions (
+    # Allow windows style arguments /...
+    # "--|-|\+|\/" => \$prefix_pattern,
+    # "--|\/" => \$long_prefix_pattern,
+
+    "b=i" => \$optbase,
+    "base=i" => \$optbase,
+    "b36" => \$optbase36,
+    "base36" => \$optbase36,
+    "b62" => \$optbase62,
+    "base62" => \$optbase62,
+
+    "localhost_number=s" => \$localhost_number,  # cf "local"
+
+    "unix" => \$optunix,
+    "u" => \$optunix,
+    "GMT" => \$optgmt,
+    "UTC" => \$optgmt,
+    "zulu" => \$optgmt,
+    "local" => \$optlocal,   # cf "localhost_number"
+    "l" => \$optlocal,   # cf "localhost_number"
+
+    "pid" => \$optpid,
+
+    # exim args given by the test harness
+    "C=s" => \$optconfigfile,
+    "dexim_path=s" => \$opteximpath,
+
+    "debug" => \$debug,
+    "nodebug" => \$nodebug,
+    "no-debug" => \$nodebug,
+
+    'help' => sub { pod2usage(-exit => 0) },
+    'man'  => sub {
+        pod2usage(
+            -exit      => 0,
+            -verbose   => 2,
+            -noperldoc => system('perldoc -V 2>/dev/null 1>&2')
+       );
+    },
+) or pod2usage;
+# die("Error in command line arguments\n");
+
+$debug = undef if $nodebug;
+
+   
+if ($debug) {
+    warn "$0 ", join(" ", @ARGV), "\n";
+    warn "C=$optconfigfile\n" if defined $optconfigfile;
+    warn "dexim_path=$opteximpath\n" if defined $opteximpath;
+}
+
+unless ($optgmt || $optunix || $optlocal) {
+    $optlocal = TRUE;
+}
+
+if (defined($optbase36) && defined($optbase62)) {
+    die "cannot be base36 and base62\n";
+}
+
+if (defined $optbase36) {
+    $optbase = 36;
+}
+if (defined $optbase62) {
+    $optbase = 62;
+}
+if (defined $optbase) {
+    if ($optbase =~ 62) {
+        $optbase = 62;
+    } elsif ($optbase =~ 36) {
+        $optbase = 36;
+    } else {
+        warn "\toptbase36=$optbase36\n" if defined $optbase36;
+        warn "\toptbase62=$optbase62\n"if defined $optbase62;
+        die "unknown base option $optbase\n";
+    }
+}
+
+# Some Operating Systems have case-insensitive file systems
+# (at least by default).
+# This limits the characters available for the message-id
+# and hence the base Exim uses to encode numbers.
+#
+# We use Perl's idea of the operating system.
+# Should we instead use the script "scripts/os-type" which comes with Exim ?
+my $defaultbase;
+if ($^O =~ /darwin|cygwin/i) { # darwin aka MacOS X
+    $defaultbase = 36;
+} else {
+    $defaultbase = 62;
+}
+
+if ("BASE_62" != $defaultbase and !defined $optbase) {
+    die "base_62 mismatch: OS implies $defaultbase but config has BASE_62\n";
+}
+
+my $base=$defaultbase;
+$base = $optbase if $optbase;
+
+my $base62_chars =
+        "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+my $base36_chars="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+my $base_chars;
+if ($base == 62) {
+    $base_chars = $base62_chars;
+} else {
+    $base_chars = $base36_chars;
+}
+
+# We use this to decode both base62 and base36
+sub decode62($) {
+    #warn "decode62(", join(",", @_), ")\n";
+    my ($text) = @_;
+    unless ($text =~ /^[$base_chars]+$/) {
+        die "$text is not base $base\n";
+    }
+    my $n=0;
+    foreach my $tt (split //, $text) {
+        $n = $n * $base + index($base_chars, $tt);
+    }
+    #warn "$text -> $n\n";
+    return $n;
+} # decode62
+
+sub get_configfilename()
+{
+    if (defined $optconfigfile) {
+        if ( -r $optconfigfile ) {
+            warn "using config $optconfigfile\n" if $debug;
+            return $optconfigfile;
+        } else {
+            die "cannot read $optconfigfile\n";
+        }
+    }
+
+    # See if this installation is using the esoteric "USE_EUID" feature of
+    # Exim, in which it uses the effective user id as a suffix for the
+    # configuration file name. In order for this to work, exim_msgdate
+    # must be run under the appropriate euid.
+    my $euid = "";
+    if ("CONFIGURE_FILE_USE_EUID" eq "yes" ) {
+        $euid=`id -u`;
+    }
+
+    # See if this installation is using the esoteric "USE_NODE"
+    # feature of Exim, in which it uses the host's name as a suffix
+    # for the configuration file name.
+    my $hostsuffix="";
+    if ("CONFIGURE_FILE_USE_NODE" eq "yes") {
+        $hostsuffix=`uname -n`;
+    }
+
+    # Now find the configuration file name.
+    # This has got complicated because the CONFIGURE_FILE value may now
+    # be a list of files. The one that is used is the first one that
+    # exists. Mimic the code in readconf.c by testing first for the
+    # suffixed file in each case.
+
+    my $config="";
+    my $baseconfig;
+    foreach $baseconfig (split /:/, "CONFIGURE_FILE") {
+        chomp $baseconfig;
+        if (-f "$baseconfig$euid$hostsuffix" ) {
+            $config="$baseconfig$euid$hostsuffix";
+        } elsif (-f "$baseconfig$euid" ) {
+            $config="$baseconfig$euid";
+        } elsif (-f "$baseconfig$hostsuffix" ) {
+            $config="$baseconfig$hostsuffix";
+        } elsif (-f "$baseconfig" ) {
+            $config="$baseconfig";
+        }
+        last if $config;
+    }
+    unless ($config) {
+            die "No config file found\n";
+    }
+
+    return $config;
+} # sub get_configfilename
+
+
+if ($debug) {
+    warn "before reading configfiles:\n";
+    if (defined $localhost_number) {
+        warn "localhost_number=$localhost_number\n";
+    } else {
+        warn "localhost_number unset\n";
+    }
+}
+
+if (defined $localhost_number) {
+    if ($localhost_number eq "none") {
+        $localhost_number = undef;
+    }
+} else {
+    my $config = get_configfilename();
+    warn "Reading config $config to find localhost_number\n" if $debug;
+
+    if (-r $config) {
+        # This does not do any expansions or lookups,
+        # so could be end up with a different value for localhost_number
+        # from the one that exim finds.
+        open(CONFIG, "<", $config) or
+        die "cannot open config $config :$!\n";
+
+        while(<CONFIG>) {
+            if (/^\s*localhost_number\s*=\s*(\d+)\s*$/) {
+                $localhost_number = $1;
+            }
+        }
+        close CONFIG or die "cannot close config $config: $!\n";
+        warn "$config gives localhost_number $localhost_number\n"
+            if $debug and defined $localhost_number;
+    } else {
+        # This way we get the expanded value for localhost_number
+        # directly from exim, but we have to guess which exim binary ...
+        # On Debian and Ubuntu, /usr/sbin/exim is a link to exim4 so is OK.
+        #
+        # Even if given on command line, we cannot use $opteximpath
+        # since it is the full path to this script,
+        # or $config since it is tainted.
+        #
+        warn "running system exim -bP localhost_number\n" if $debug;
+        my $exim_bP_localhost_number = `/usr/sbin/exim -bP localhost_number`;
+        if ($exim_bP_localhost_number =~ /^localhost_number\s*=\s*(\d*)/) {
+            $localhost_number = $1;
+        }
+        warn "exim_bP_localhost_number $exim_bP_localhost_number gives localhost_number $localhost_number\n"
+            if $debug and defined $localhost_number; 
+    }
+}
+
+if (defined $localhost_number) {
+    die "localhost_number > 16\n"
+        if $localhost_number > 16;
+    die "localhost_number > 10\n"
+        if $localhost_number > 10 && ($base != 62);
+}
+
+if ($debug) {
+    if (defined $localhost_number) {
+        warn "localhost_number=$localhost_number\n";
+    } else {
+        warn "localhost_number unset\n";
+    }
+}
+
+sub unpack_time($$) {
+    my ($seconds, $fractions) = @_;
+    # warn "encoded: seconds: $seconds fractions: $fractions\n";
+    $seconds = decode62($seconds);
+    $fractions = decode62($fractions) if $fractions;
+    my $id_resolution;
+    if (defined $localhost_number && $localhost_number ne "none") {
+        print "localhost_number $localhost_number\n" if $debug;
+        if ($base != 62) {
+            # MacOS/Darwin and Cygwin
+            $id_resolution = 100;
+        } else {
+            # Standard UNIX etc.
+            $id_resolution = 200;
+        }
+        $fractions -= $localhost_number * $id_resolution;
+    } else {
+        if ($base != 62) {
+            # MacOS/Darwin and Cygwin
+            $id_resolution = 1000;
+        } else {
+            # Standard UNIX etc.
+            $id_resolution = 2000;
+        }
+    }
+    while ($fractions > $id_resolution) {
+        $seconds++;
+        $fractions -= $id_resolution;
+    }
+    while ($fractions < -1e-7) {
+        $seconds--;
+        $fractions += $id_resolution;
+    }
+    # $seconds += $fractions / $id_resolution;
+
+    # warn "decoded: seconds: $seconds, fractions: $fractions/$id_resolution\n";
+
+    return ($seconds, $fractions / $id_resolution);
+} # sub unpack_time($$)
+
+sub print_time($$$$$$)
+{
+    my ($seconds, $decimal, $unix, $zulu, $localtm, $pid) = @_;
+
+    if ($debug) {
+        my $ounix = defined($unix) ? $unix : "undef";
+        my $ozulu = defined($zulu) ? $zulu : "undef";
+        my $olocal = defined($localtm) ? $localtm : "undef";
+        my $opid = defined($pid) ? $pid : "undef";
+        warn "print_time($seconds, $decimal, $ounix, $ozulu, $olocal, $opid)\n"
+    }
+
+    my $pidstring = "";
+    $pidstring = "\tpid $pid" if defined $pid;
+
+    my $decimalstring = "";
+    # if ($decimal>0)
+    {
+        $decimalstring = sprintf(".%6.6d", 1000000*$decimal);
+    }
+    my $secondsstring;
+    unless (defined $unix or defined $zulu or defined $localtm) {
+        warn "No time type requested. Reporting UNIX time\n";
+        $unix = TRUE;
+    }
+    if (defined $unix) {
+        $secondsstring = $seconds;
+        print "$secondsstring$decimalstring$pidstring\n";
+    }
+    if (defined $zulu) {
+        $secondsstring = strftime("%F %T", gmtime($seconds));
+        print "$secondsstring$decimalstring$pidstring\n";
+    }
+    if (defined $localtm) {
+        $secondsstring = strftime("%F %T%%s %Z%%s\n", localtime($seconds));
+        # print "secondstring $secondsstring\n" if $debug;
+        printf($secondsstring, $decimalstring, $pidstring);
+    }
+
+} # sub print_time($$$$$$)
+
+foreach my $msgid (@ARGV) {
+    my ($seconds, $pid, $fractions, $decimal);
+
+    if ($msgid =~
+        /(^|[\s<])E?([a-zA-Z0-9]{6})-([a-zA-Z0-9]{6})-([a-zA-Z0-9]{2})/)
+    {
+        # Should take either the log form of timestamp,
+        # the Message-ID: header form with the leading 'E', ...
+        ($seconds, $pid, $fractions) = ($2, $3, $4);
+        ($seconds, $decimal) = unpack_time($seconds, $fractions);
+        $pid = decode62($pid);
+        #warn "$seconds, $pid, $fractions\n";
+    } elsif ($msgid =~ /(^|[^0-9A-Za-z])([a-zA-Z0-9]{6})$/) {
+        # ... or just the timecode section before the first '-'
+        ($seconds, $pid, $decimal) = (decode62($2), undef, 0);
+    } else {
+        warn "$msgid not parsed\n";
+        next;
+    }
+
+    if ($debug) {
+        print "msgid: $msgid\n";
+        my $ogmt = defined($optgmt) ? $optgmt : "undef";
+        my $ounix = defined($optunix) ? $optunix : "undef";
+        my $olocal = defined($optlocal) ? $optlocal : "undef";
+        my $opid = defined($optpid) ? $optpid : "undef";
+        print "print_time($seconds, $decimal, $ounix, $ogmt, $olocal, $opid)\n";
+    }
+    $pid = undef unless $optpid;
+    print_time($seconds, $decimal, $optunix, $optgmt, $optlocal, $pid);
+}
+
+=head1 NAME
+
+  exim_msgdate -  Utility to convert an exim message-id to a human readable date+time
+
+=head1 SYNOPSIS
+
+B<exim_msgdate> [ -u|--unix | --GMT | --z|-Zulu | --UTC | -l|--local ]
+      [ --base 36 | --base 62 | --base36 | --base62 | --b36 | --b62 ]
+      [ --pid ] [ --debug ] [ --localhost_number ]
+      [ -c c<full path to exim cnfig file> ]
+      exim-message-id [ | exim-message-id ...]
+
+B<exim_msgdate> --help|--man
+
+=head1 DESCRIPTION
+
+B<exim_msgdate> is a tool which converts an exim message-id to a human
+readable form, usuall just the date+time, but with the I<--pid> option
+the process id as well.
+
+=head1 Message IDs:
+
+Three exim message ID formats are recognized.
+In each case the 'X's are taken from the base (see below) which depends upon the platform.
+
+=over 4
+
+=item XXXXXX-XXXXXX-XX
+
+found in the exim logfile,
+
+=item EXXXXXX-XXXXXX-XX
+
+found in the Message-Id header,
+
+=item XXXXXX
+
+just the first six characters of the message id.
+
+=back
+
+=head1 OPTIONS
+
+=head2 Time Zones and Unix Time
+
+=over 4
+
+=item     B<-u | --unix>
+
+Display time as seconds since 1 Jan 1970, the Unix Epoch.
+
+=item     B<--GMT> B<-u|--UTC> B<-z|--zulu>
+
+Display time in GMT/UTC - we assume these are the same.
+Zulu time is another name for GMT.
+
+=item     B<-l | --local>
+
+Display time in the local time-zone.
+
+Do not confuse this with the L<--localhost_number|/--localhost_number-n> option.
+
+=back
+
+The default is the local timezone.
+
+=head2 User Assistance Options
+
+=over 4
+
+=item     B<--help>
+
+A brief list of the options
+
+=item     B<--man>
+
+A more detailed manual for B<exim_msgdate>
+
+=item B<--debug>
+
+Information about what went wrong, mostly for developers.
+
+=back
+
+=head2 Specialized Options
+
+=over 4
+
+=item B<--base> n | B<--base36> | B<--base62>
+
+The message-id is usually encoded in base-62 (0-9A-Za-z),
+but on systems with case-insensitive file systems, such as MacOS and Cygwin,
+base-36 (0-9A-Z) is used instead.
+The installation script should have set the default appropriately,
+but these options allow the default base to be overridden.
+
+The default matches C<exim>; in this installation it is base-BASE_62.
+
+=item B<--pid>
+
+Report the process id as well as the date and time in the message-id.
+
+=item B<--localhost_number> n
+
+If the Exim configuration option B<localhost_number> has been set,
+the third and final section of the message-id will include this and
+the timer resolution will change (see the Exim Spec. for details).
+C<Exim_msgdate> reads the Exim config file (see L<--C|/C-full-path-to-exim-configuration-file>) to find this value,
+but it can be overridden with this option.
+
+The value is an integer between 0 and 16, or the value "none" which
+means there is no localhost_number.
+
+Do not confuse this with the L<--local|/l---local> option, which displays times
+ in the local timezone.
+
+=item B<--C> B<full path to exim configuration file>
+
+This overrides the usual exim search path.
+We set C<localhost_number> from the exim configfile.
+
+=item B<-dexim_path>
+
+The test test harness passes the full path of the C<exim> binary,
+or here the C<exim_msgdate> being tested. Not currently used.
+
+=back
+
+=head1 SEE ALSO:
+
+L<exim(8)>
+
+L<Exim spec.txt chapter 4|https://exim.org/exim-html-current/doc/html/spec_html/ch-how_exim_receives_and_delivers_mail.html#SECTmessiden>
+
+=cut

commit e95b96eb1dc35bae278f237f9d65734305dba6b2
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Feb 16 19:02:42 2023 +0000

    Solaris: more build-fixing
    
    Broken-by: 1e835086d159
    See-also: 73d6e13f9b0c

diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index f16570d86..b2f5f6028 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -61,6 +61,9 @@ uschar *
 readconf_printtime(int t)
 { return NULL; }
 gstring *
+string_catn(gstring * g, const uschar * s, int count)
+{ return NULL; }
+gstring *
 string_vformat_trc(gstring * g, const uschar * func, unsigned line,
   unsigned size_limit, unsigned flags, const char *format, va_list ap)
 { return NULL; }

commit 3b06efc10bd99da03c446e85839c2933b3e309b0
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Feb 21 20:58:22 2023 +0000

    Include address declared malformed, in error message

diff --git a/src/src/verify.c b/src/src/verify.c
index 125df8d91..5c0a3e408 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -2958,7 +2958,7 @@ dots). */
 for (t = ss; isdigit(*t) || *t == '.'; ) t++;
 if (!*t  || (*t == '/' && t != ss))
   {
-  *error = US"malformed IPv4 address or address mask";
+  *error = string_sprintf("malformed IPv4 address or address mask: %.*s", (int)(t - ss), ss);
   return ERROR;
   }
 

commit 63874787bdc51535a040baa38be3ff07c97f0bdc
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Feb 23 13:39:14 2023 +0000

    Split out separate fn for bounce-message send

diff --git a/src/src/deliver.c b/src/src/deliver.c
index 084a048c9..efb85348c 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -38,8 +38,8 @@ enum { RECIP_ACCEPT, RECIP_IGNORE, RECIP_DEFER,
 
 /* Mutually recursive functions for marking addresses done. */
 
-static void child_done(address_item *, uschar *);
-static void address_done(address_item *, uschar *);
+static void child_done(address_item *, const uschar *);
+static void address_done(address_item *, const uschar *);
 
 /* Table for turning base-62 numbers into binary */
 
@@ -663,7 +663,7 @@ Returns:      nothing
 */
 
 static void
-address_done(address_item *addr, uschar *now)
+address_done(address_item * addr, const uschar * now)
 {
 update_spool = TRUE;        /* Ensure spool gets updated */
 
@@ -720,7 +720,7 @@ Returns:    nothing
 */
 
 static void
-child_done(address_item *addr, uschar *now)
+child_done(address_item * addr, const uschar * now)
 {
 while (addr->parent)
   {
@@ -1458,12 +1458,12 @@ Returns:       nothing
 */
 
 static void
-post_process_one(address_item *addr, int result, int logflags, int driver_type,
+post_process_one(address_item * addr, int result, int logflags, int driver_type,
   int logchar)
 {
-uschar *now = tod_stamp(tod_log);
-uschar *driver_kind = NULL;
-uschar *driver_name = NULL;
+uschar * now = tod_stamp(tod_log);
+uschar * driver_kind = NULL;
+uschar * driver_name = NULL;
 
 DEBUG(D_deliver) debug_printf("post-process %s (%d)\n", addr->address, result);
 
@@ -2316,7 +2316,7 @@ if ((pid = exim_fork(US"delivery-local")) == 0)
 
   if (addr->transport->setup)
     switch((addr->transport->setup)(addr->transport, addr, NULL, uid, gid,
-           &(addr->message)))
+           &addr->message))
       {
       case DEFER:
 	addr->transport_return = DEFER;
@@ -3481,7 +3481,7 @@ while (!done)
     guarantee it won't be split in the pipe. */
 
 #ifndef DISABLE_TLS
-    case 'X':
+    case 'X':		/* TLS details */
       if (!addr) goto ADDR_MISMATCH;          /* Below, in 'A' handler */
       switch (*subid)
 	{
@@ -3565,7 +3565,7 @@ while (!done)
       DEBUG(D_deliver) debug_printf("DSN read: addr->dsn_aware = %d\n", addr->dsn_aware);
       break;
 
-    case 'A':
+    case 'A':		/* Per-address info */
       if (!addr)
 	{
 	ADDR_MISMATCH:
@@ -3609,7 +3609,7 @@ while (!done)
 	  break;
 #endif
 
-	case '0':
+	case '0':	/* results of trying to send to this address */
 	  DEBUG(D_deliver) debug_printf("A0 %s tret %d\n", addr->address, *ptr);
 	  addr->transport_return = *ptr++;
 	  addr->special_action = *ptr++;
@@ -3758,7 +3758,7 @@ Returns:     nothing
 */
 
 static void
-remote_post_process(address_item *addr, int logflags, uschar *msg,
+remote_post_process(address_item * addr, int logflags, uschar * msg,
   BOOL fallback)
 {
 /* If any host addresses were found to be unusable, add them to the unusable
@@ -3773,7 +3773,7 @@ into the special_action field for each successful delivery. */
 
 while (addr)
   {
-  address_item *next = addr->next;
+  address_item * next = addr->next;
 
   /* If msg == NULL (normal processing) and the result is DEFER and we are
   processing the main hosts and there are fallback hosts available, put the
@@ -4098,7 +4098,7 @@ par_reduce(int max, BOOL fallback)
 {
 while (parcount > max)
   {
-  address_item *doneaddr = par_wait();
+  address_item * doneaddr = par_wait();
   if (!doneaddr)
     {
     log_write(0, LOG_MAIN|LOG_PANIC,
@@ -5552,6 +5552,447 @@ else if (!(fp = Ufopen(s, "rb")))
 return fp;
 }
 
+/*************************************************
+*              Send a bounce message             *
+*************************************************/
+
+/* Find the error address for the first address, then send a message that
+includes all failed addresses that have the same error address. Note the
+bounce_recipient is a global so that it can be accessed by $bounce_recipient
+while creating a customized error message. */
+
+static void
+send_bounce_message(time_t now, const uschar * logtod)
+{
+pid_t pid;
+int fd;
+
+if (!(bounce_recipient = addr_failed->prop.errors_address))
+  bounce_recipient = sender_address;
+
+/* Make a subprocess to send a message, using its stdin */
+
+if ((pid = child_open_exim(&fd, US"bounce-message")) < 0)
+  log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Process %d (parent %d) failed to "
+    "create child process to send failure message: %s", getpid(),
+    getppid(), strerror(errno));
+
+/* Creation of child succeeded */
+
+else
+  {
+  int ch, rc, filecount = 0, rcount = 0;
+  uschar * bcc, * emf_text;
+  FILE * fp = fdopen(fd, "wb"), * emf = NULL;
+  BOOL to_sender = strcmpic(sender_address, bounce_recipient) == 0;
+  int max = (bounce_return_size_limit/DELIVER_IN_BUFFER_SIZE + 1) *
+    DELIVER_IN_BUFFER_SIZE;
+  uschar * bound, * dsnlimitmsg, * dsnnotifyhdr;
+  int topt;
+  address_item ** paddr;
+  address_item * msgchain = NULL, ** pmsgchain = &msgchain;
+  address_item * handled_addr = NULL;
+
+  DEBUG(D_deliver)
+    debug_printf("sending error message to: %s\n", bounce_recipient);
+
+  /* Scan the addresses for all that have the same errors address, removing
+  them from the addr_failed chain, and putting them on msgchain. */
+
+  paddr = &addr_failed;
+  for (address_item * addr = addr_failed; addr; addr = *paddr)
+    if (Ustrcmp(bounce_recipient, addr->prop.errors_address
+	  ? addr->prop.errors_address : sender_address) == 0)
+      {                          /* The same - dechain */
+      *paddr = addr->next;
+      *pmsgchain = addr;
+      addr->next = NULL;
+      pmsgchain = &addr->next;
+      }
+    else
+      paddr = &addr->next;        /* Not the same; skip */
+
+  /* Include X-Failed-Recipients: for automatic interpretation, but do
+  not let any one header line get too long. We do this by starting a
+  new header every 50 recipients. Omit any addresses for which the
+  "hide_child" flag is set. */
+
+  for (address_item * addr = msgchain; addr; addr = addr->next)
+    {
+    if (testflag(addr, af_hide_child)) continue;
+    if (rcount >= 50)
+      {
+      fprintf(fp, "\n");
+      rcount = 0;
+      }
+    fprintf(fp, "%s%s",
+      rcount++ == 0
+      ? "X-Failed-Recipients: "
+      : ",\n  ",
+      testflag(addr, af_pfr) && addr->parent
+      ? string_printing(addr->parent->address)
+      : string_printing(addr->address));
+    }
+  if (rcount > 0) fprintf(fp, "\n");
+
+  /* Output the standard headers */
+
+  if (errors_reply_to)
+    fprintf(fp, "Reply-To: %s\n", errors_reply_to);
+  fprintf(fp, "Auto-Submitted: auto-replied\n");
+  moan_write_from(fp);
+  fprintf(fp, "To: %s\n", bounce_recipient);
+  moan_write_references(fp, NULL);
+
+  /* generate boundary string and output MIME-Headers */
+  bound = string_sprintf(TIME_T_FMT "-eximdsn-%d", time(NULL), rand());
+
+  fprintf(fp, "Content-Type: multipart/report;"
+	" report-type=delivery-status; boundary=%s\n"
+      "MIME-Version: 1.0\n",
+    bound);
+
+  /* Open a template file if one is provided. Log failure to open, but
+  carry on - default texts will be used. */
+
+  if (bounce_message_file)
+    emf = expand_open(bounce_message_file,
+	    US"bounce_message_file", US"error");
+
+  /* Quietly copy to configured additional addresses if required. */
+
+  if ((bcc = moan_check_errorcopy(bounce_recipient)))
+    fprintf(fp, "Bcc: %s\n", bcc);
+
+  /* The texts for the message can be read from a template file; if there
+  isn't one, or if it is too short, built-in texts are used. The first
+  emf text is a Subject: and any other headers. */
+
+  if ((emf_text = next_emf(emf, US"header")))
+    fprintf(fp, "%s\n", emf_text);
+  else
+    fprintf(fp, "Subject: Mail delivery failed%s\n\n",
+      to_sender? ": returning message to sender" : "");
+
+  /* output human readable part as text/plain section */
+  fprintf(fp, "--%s\n"
+      "Content-type: text/plain; charset=us-ascii\n\n",
+    bound);
+
+  if ((emf_text = next_emf(emf, US"intro")))
+    fprintf(fp, "%s", CS emf_text);
+  else
+    {
+    fprintf(fp,
+/* This message has been reworded several times. It seems to be confusing to
+somebody, however it is worded. I have retreated to the original, simple
+wording. */
+"This message was created automatically by mail delivery software.\n");
+
+    if (bounce_message_text)
+      fprintf(fp, "%s", CS bounce_message_text);
+    if (to_sender)
+      fprintf(fp,
+"\nA message that you sent could not be delivered to one or more of its\n"
+"recipients. This is a permanent error. The following address(es) failed:\n");
+    else
+      fprintf(fp,
+"\nA message sent by\n\n  <%s>\n\n"
+"could not be delivered to one or more of its recipients. The following\n"
+"address(es) failed:\n", sender_address);
+    }
+  fputc('\n', fp);
+
+  /* Process the addresses, leaving them on the msgchain if they have a
+  file name for a return message. (There has already been a check in
+  post_process_one() for the existence of data in the message file.) A TRUE
+  return from print_address_information() means that the address is not
+  hidden. */
+
+  paddr = &msgchain;
+  for (address_item * addr = msgchain; addr; addr = *paddr)
+    {
+    if (print_address_information(addr, fp, US"  ", US"\n    ", US""))
+      print_address_error(addr, fp, US"");
+
+    /* End the final line for the address */
+
+    fputc('\n', fp);
+
+    /* Leave on msgchain if there's a return file. */
+
+    if (addr->return_file >= 0)
+      {
+      paddr = &(addr->next);
+      filecount++;
+      }
+
+    /* Else save so that we can tick off the recipient when the
+    message is sent. */
+
+    else
+      {
+      *paddr = addr->next;
+      addr->next = handled_addr;
+      handled_addr = addr;
+      }
+    }
+
+  fputc('\n', fp);
+
+  /* Get the next text, whether we need it or not, so as to be
+  positioned for the one after. */
+
+  emf_text = next_emf(emf, US"generated text");
+
+  /* If there were any file messages passed by the local transports,
+  include them in the message. Then put the address on the handled chain.
+  In the case of a batch of addresses that were all sent to the same
+  transport, the return_file field in all of them will contain the same
+  fd, and the return_filename field in the *last* one will be set (to the
+  name of the file). */
+
+  if (msgchain)
+    {
+    address_item * nextaddr;
+
+    if (emf_text)
+      fprintf(fp, "%s", CS emf_text);
+    else
+      fprintf(fp,
+	"The following text was generated during the delivery "
+	"attempt%s:\n", (filecount > 1)? "s" : "");
+
+    for (address_item * addr = msgchain; addr; addr = nextaddr)
+      {
+      FILE *fm;
+      address_item *topaddr = addr;
+
+      /* List all the addresses that relate to this file */
+
+      fputc('\n', fp);
+      while(addr)                   /* Insurance */
+	{
+	print_address_information(addr, fp, US"------ ",  US"\n       ",
+	  US" ------\n");
+	if (addr->return_filename) break;
+	addr = addr->next;
+	}
+      fputc('\n', fp);
+
+      /* Now copy the file */
+
+      if (!(fm = Ufopen(addr->return_filename, "rb")))
+	fprintf(fp, "    +++ Exim error... failed to open text file: %s\n",
+	  strerror(errno));
+      else
+	{
+	while ((ch = fgetc(fm)) != EOF) fputc(ch, fp);
+	(void)fclose(fm);
+	}
+      Uunlink(addr->return_filename);
+
+      /* Can now add to handled chain, first fishing off the next
+      address on the msgchain. */
+
+      nextaddr = addr->next;
+      addr->next = handled_addr;
+      handled_addr = topaddr;
+      }
+    fputc('\n', fp);
+    }
+
+  /* output machine readable part */
+#ifdef SUPPORT_I18N
+  if (message_smtputf8)
+    fprintf(fp, "--%s\n"
+	"Content-type: message/global-delivery-status\n\n"
+	"Reporting-MTA: dns; %s\n",
+      bound, smtp_active_hostname);
+  else
+#endif
+    fprintf(fp, "--%s\n"
+	"Content-type: message/delivery-status\n\n"
+	"Reporting-MTA: dns; %s\n",
+      bound, smtp_active_hostname);
+
+  if (dsn_envid)
+    {
+    /* must be decoded from xtext: see RFC 3461:6.3a */
+    uschar *xdec_envid;
+    if (auth_xtextdecode(dsn_envid, &xdec_envid) > 0)
+      fprintf(fp, "Original-Envelope-ID: %s\n", dsn_envid);
+    else
+      fprintf(fp, "X-Original-Envelope-ID: error decoding xtext formatted ENVID\n");
+    }
+  fputc('\n', fp);
+
+  for (address_item * addr = handled_addr; addr; addr = addr->next)
+    {
+    host_item * hu;
+
+    print_dsn_addr_action(fp, addr, US"failed", US"5.0.0");
+
+    if ((hu = addr->host_used) && hu->name)
+      {
+      fprintf(fp, "Remote-MTA: dns; %s\n", hu->name);
+#ifdef EXPERIMENTAL_DSN_INFO
+      {
+      const uschar * s;
+      if (hu->address)
+	{
+	uschar * p = hu->port == 25
+	  ? US"" : string_sprintf(":%d", hu->port);
+	fprintf(fp, "Remote-MTA: X-ip; [%s]%s\n", hu->address, p);
+	}
+      if ((s = addr->smtp_greeting) && *s)
+	fprintf(fp, "X-Remote-MTA-smtp-greeting: X-str; %.900s\n", s);
+      if ((s = addr->helo_response) && *s)
+	fprintf(fp, "X-Remote-MTA-helo-response: X-str; %.900s\n", s);
+      if ((s = addr->message) && *s)
+	fprintf(fp, "X-Exim-Diagnostic: X-str; %.900s\n", s);
+      }
+#endif
+      print_dsn_diagnostic_code(addr, fp);
+      }
+    fputc('\n', fp);
+    }
+
+  /* Now copy the message, trying to give an intelligible comment if
+  it is too long for it all to be copied. The limit isn't strictly
+  applied because of the buffering. There is, however, an option
+  to suppress copying altogether. */
+
+  emf_text = next_emf(emf, US"copy");
+
+  /* add message body
+     we ignore the intro text from template and add
+     the text for bounce_return_size_limit at the end.
+
+     bounce_return_message is ignored
+     in case RET= is defined we honor these values
+     otherwise bounce_return_body is honored.
+
+     bounce_return_size_limit is always honored.
+  */
+
+  fprintf(fp, "--%s\n", bound);
+
+  dsnlimitmsg = US"X-Exim-DSN-Information: Due to administrative limits only headers are returned";
+  dsnnotifyhdr = NULL;
+  topt = topt_add_return_path;
+
+  /* RET=HDRS? top priority */
+  if (dsn_ret == dsn_ret_hdrs)
+    topt |= topt_no_body;
+  else
+    {
+    struct stat statbuf;
+
+    /* no full body return at all? */
+    if (!bounce_return_body)
+      {
+      topt |= topt_no_body;
+      /* add header if we overrule RET=FULL */
+      if (dsn_ret == dsn_ret_full)
+	dsnnotifyhdr = dsnlimitmsg;
+      }
+    /* line length limited... return headers only if oversize */
+    /* size limited ... return headers only if limit reached */
+    else if (  max_received_linelength > bounce_return_linesize_limit
+	    || (  bounce_return_size_limit > 0
+	       && fstat(deliver_datafile, &statbuf) == 0
+	       && statbuf.st_size > max
+	    )  )
+      {
+      topt |= topt_no_body;
+      dsnnotifyhdr = dsnlimitmsg;
+      }
+    }
+
+#ifdef SUPPORT_I18N
+  if (message_smtputf8)
+    fputs(topt & topt_no_body ? "Content-type: message/global-headers\n\n"
+			      : "Content-type: message/global\n\n",
+	  fp);
+  else
+#endif
+    fputs(topt & topt_no_body ? "Content-type: text/rfc822-headers\n\n"
+			      : "Content-type: message/rfc822\n\n",
+	  fp);
+
+  fflush(fp);
+  transport_filter_argv = NULL;   /* Just in case */
+  return_path = sender_address;   /* In case not previously set */
+    {			      /* Dummy transport for headers add */
+    transport_ctx tctx = {{0}};
+    transport_instance tb = {0};
+
+    tctx.u.fd = fileno(fp);
+    tctx.tblock = &tb;
+    tctx.options = topt;
+    tb.add_headers = dsnnotifyhdr;
+
+    /*XXX no checking for failure!  buggy! */
+    transport_write_message(&tctx, 0);
+    }
+  fflush(fp);
+
+  /* we never add the final text. close the file */
+  if (emf)
+    (void)fclose(emf);
+
+  fprintf(fp, "\n--%s--\n", bound);
+
+  /* Close the file, which should send an EOF to the child process
+  that is receiving the message. Wait for it to finish. */
+
+  (void)fclose(fp);
+  rc = child_close(pid, 0);     /* Waits for child to close, no timeout */
+
+  /* If the process failed, there was some disaster in setting up the
+  error message. Unless the message is very old, ensure that addr_defer
+  is non-null, which will have the effect of leaving the message on the
+  spool. The failed addresses will get tried again next time. However, we
+  don't really want this to happen too often, so freeze the message unless
+  there are some genuine deferred addresses to try. To do this we have
+  to call spool_write_header() here, because with no genuine deferred
+  addresses the normal code below doesn't get run. */
+
+  if (rc != 0)
+    {
+    uschar *s = US"";
+    if (now - received_time.tv_sec < retry_maximum_timeout && !addr_defer)
+      {
+      addr_defer = (address_item *)(+1);
+      f.deliver_freeze = TRUE;
+      deliver_frozen_at = time(NULL);
+      /* Panic-dies on error */
+      (void)spool_write_header(message_id, SW_DELIVERING, NULL);
+      s = US" (frozen)";
+      }
+    deliver_msglog("Process failed (%d) when writing error message "
+      "to %s%s", rc, bounce_recipient, s);
+    log_write(0, LOG_MAIN, "Process failed (%d) when writing error message "
+      "to %s%s", rc, bounce_recipient, s);
+    }
+
+  /* The message succeeded. Ensure that the recipients that failed are
+  now marked finished with on the spool and their parents updated. */
+
+  else
+    {
+    for (address_item * addr = handled_addr; addr; addr = addr->next)
+      {
+      address_done(addr, logtod);
+      child_done(addr, logtod);
+      }
+    /* Panic-dies on error */
+    (void)spool_write_header(message_id, SW_DELIVERING, NULL);
+    }
+  }
+}
+
 /*************************************************
 *              Deliver one message               *
 *************************************************/
@@ -6394,7 +6835,7 @@ deliver_out_buffer = store_malloc(DELIVER_OUT_BUFFER_SIZE);
 f.header_rewritten = FALSE;          /* No headers rewritten yet */
 while (addr_new)           /* Loop until all addresses dealt with */
   {
-  address_item *addr, *parent;
+  address_item * addr, * parent;
 
   /* Failure to open the retry database is treated the same as if it does
   not exist. In both cases, dbm_file is NULL. */
@@ -7449,7 +7890,7 @@ if (addr_senddsn)
 
     if (dsn_envid)
       {			/* must be decoded from xtext: see RFC 3461:6.3a */
-      uschar *xdec_envid;
+      uschar * xdec_envid;
       if (auth_xtextdecode(dsn_envid, &xdec_envid) > 0)
         fprintf(f, "Original-Envelope-ID: %s\n", dsn_envid);
       else
@@ -7501,14 +7942,8 @@ requirements. */
 
 while (addr_failed)
   {
-  pid_t pid;
-  int fd;
-  uschar *logtod = tod_stamp(tod_log);
-  address_item *addr;
-  address_item *handled_addr = NULL;
-  address_item **paddr;
-  address_item *msgchain = NULL;
-  address_item **pmsgchain = &msgchain;
+  const uschar * logtod = tod_stamp(tod_log);
+  address_item * addr;
 
   /* There are weird cases when logging is disabled in the transport. However,
   there may not be a transport (address failed by a router). */
@@ -7578,439 +8013,10 @@ while (addr_failed)
 
   /* Otherwise, handle the sending of a message. Find the error address for
   the first address, then send a message that includes all failed addresses
-  that have the same error address. Note the bounce_recipient is a global so
-  that it can be accessed by $bounce_recipient while creating a customized
-  error message. */
+  that have the same error address. */
 
   else
-    {
-    if (!(bounce_recipient = addr_failed->prop.errors_address))
-      bounce_recipient = sender_address;
-
-    /* Make a subprocess to send a message */
-
-    if ((pid = child_open_exim(&fd, US"bounce-message")) < 0)
-      log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Process %d (parent %d) failed to "
-        "create child process to send failure message: %s", getpid(),
-        getppid(), strerror(errno));
-
-    /* Creation of child succeeded */
-
-    else
-      {
-      int ch, rc;
-      int filecount = 0;
-      int rcount = 0;
-      uschar *bcc, *emf_text;
-      FILE * fp = fdopen(fd, "wb");
-      FILE * emf = NULL;
-      BOOL to_sender = strcmpic(sender_address, bounce_recipient) == 0;
-      int max = (bounce_return_size_limit/DELIVER_IN_BUFFER_SIZE + 1) *
-        DELIVER_IN_BUFFER_SIZE;
-      uschar * bound;
-      uschar *dsnlimitmsg;
-      uschar *dsnnotifyhdr;
-      int topt;
-
-      DEBUG(D_deliver)
-        debug_printf("sending error message to: %s\n", bounce_recipient);
-
-      /* Scan the addresses for all that have the same errors address, removing
-      them from the addr_failed chain, and putting them on msgchain. */
-
-      paddr = &addr_failed;
-      for (addr = addr_failed; addr; addr = *paddr)
-        if (Ustrcmp(bounce_recipient, addr->prop.errors_address
-	      ? addr->prop.errors_address : sender_address) == 0)
-          {                          /* The same - dechain */
-          *paddr = addr->next;
-          *pmsgchain = addr;
-          addr->next = NULL;
-          pmsgchain = &(addr->next);
-          }
-        else
-          paddr = &addr->next;        /* Not the same; skip */
-
-      /* Include X-Failed-Recipients: for automatic interpretation, but do
-      not let any one header line get too long. We do this by starting a
-      new header every 50 recipients. Omit any addresses for which the
-      "hide_child" flag is set. */
-
-      for (addr = msgchain; addr; addr = addr->next)
-        {
-        if (testflag(addr, af_hide_child)) continue;
-        if (rcount >= 50)
-          {
-          fprintf(fp, "\n");
-          rcount = 0;
-          }
-        fprintf(fp, "%s%s",
-          rcount++ == 0
-	  ? "X-Failed-Recipients: "
-	  : ",\n  ",
-          testflag(addr, af_pfr) && addr->parent
-	  ? string_printing(addr->parent->address)
-	  : string_printing(addr->address));
-        }
-      if (rcount > 0) fprintf(fp, "\n");
-
-      /* Output the standard headers */
-
-      if (errors_reply_to)
-        fprintf(fp, "Reply-To: %s\n", errors_reply_to);
-      fprintf(fp, "Auto-Submitted: auto-replied\n");
-      moan_write_from(fp);
-      fprintf(fp, "To: %s\n", bounce_recipient);
-      moan_write_references(fp, NULL);
-
-      /* generate boundary string and output MIME-Headers */
-      bound = string_sprintf(TIME_T_FMT "-eximdsn-%d", time(NULL), rand());
-
-      fprintf(fp, "Content-Type: multipart/report;"
-	    " report-type=delivery-status; boundary=%s\n"
-	  "MIME-Version: 1.0\n",
-	bound);
-
-      /* Open a template file if one is provided. Log failure to open, but
-      carry on - default texts will be used. */
-
-      if (bounce_message_file)
-	emf = expand_open(bounce_message_file,
-		US"bounce_message_file", US"error");
-
-      /* Quietly copy to configured additional addresses if required. */
-
-      if ((bcc = moan_check_errorcopy(bounce_recipient)))
-	fprintf(fp, "Bcc: %s\n", bcc);
-
-      /* The texts for the message can be read from a template file; if there
-      isn't one, or if it is too short, built-in texts are used. The first
-      emf text is a Subject: and any other headers. */
-
-      if ((emf_text = next_emf(emf, US"header")))
-	fprintf(fp, "%s\n", emf_text);
-      else
-        fprintf(fp, "Subject: Mail delivery failed%s\n\n",
-          to_sender? ": returning message to sender" : "");
-
-      /* output human readable part as text/plain section */
-      fprintf(fp, "--%s\n"
-	  "Content-type: text/plain; charset=us-ascii\n\n",
-	bound);
-
-      if ((emf_text = next_emf(emf, US"intro")))
-	fprintf(fp, "%s", CS emf_text);
-      else
-        {
-        fprintf(fp,
-/* This message has been reworded several times. It seems to be confusing to
-somebody, however it is worded. I have retreated to the original, simple
-wording. */
-"This message was created automatically by mail delivery software.\n");
-
-        if (bounce_message_text)
-	  fprintf(fp, "%s", CS bounce_message_text);
-        if (to_sender)
-          fprintf(fp,
-"\nA message that you sent could not be delivered to one or more of its\n"
-"recipients. This is a permanent error. The following address(es) failed:\n");
-        else
-          fprintf(fp,
-"\nA message sent by\n\n  <%s>\n\n"
-"could not be delivered to one or more of its recipients. The following\n"
-"address(es) failed:\n", sender_address);
-        }
-      fputc('\n', fp);
-
-      /* Process the addresses, leaving them on the msgchain if they have a
-      file name for a return message. (There has already been a check in
-      post_process_one() for the existence of data in the message file.) A TRUE
-      return from print_address_information() means that the address is not
-      hidden. */
-
-      paddr = &msgchain;
-      for (addr = msgchain; addr; addr = *paddr)
-        {
-        if (print_address_information(addr, fp, US"  ", US"\n    ", US""))
-          print_address_error(addr, fp, US"");
-
-        /* End the final line for the address */
-
-        fputc('\n', fp);
-
-        /* Leave on msgchain if there's a return file. */
-
-        if (addr->return_file >= 0)
-          {
-          paddr = &(addr->next);
-          filecount++;
-          }
-
-        /* Else save so that we can tick off the recipient when the
-        message is sent. */
-
-        else
-          {
-          *paddr = addr->next;
-          addr->next = handled_addr;
-          handled_addr = addr;
-          }
-        }
-
-      fputc('\n', fp);
-
-      /* Get the next text, whether we need it or not, so as to be
-      positioned for the one after. */
-
-      emf_text = next_emf(emf, US"generated text");
-
-      /* If there were any file messages passed by the local transports,
-      include them in the message. Then put the address on the handled chain.
-      In the case of a batch of addresses that were all sent to the same
-      transport, the return_file field in all of them will contain the same
-      fd, and the return_filename field in the *last* one will be set (to the
-      name of the file). */
-
-      if (msgchain)
-        {
-        address_item *nextaddr;
-
-        if (emf_text)
-	  fprintf(fp, "%s", CS emf_text);
-	else
-          fprintf(fp,
-            "The following text was generated during the delivery "
-            "attempt%s:\n", (filecount > 1)? "s" : "");
-
-        for (addr = msgchain; addr; addr = nextaddr)
-          {
-          FILE *fm;
-          address_item *topaddr = addr;
-
-          /* List all the addresses that relate to this file */
-
-	  fputc('\n', fp);
-          while(addr)                   /* Insurance */
-            {
-            print_address_information(addr, fp, US"------ ",  US"\n       ",
-              US" ------\n");
-            if (addr->return_filename) break;
-            addr = addr->next;
-            }
-	  fputc('\n', fp);
-
-          /* Now copy the file */
-
-          if (!(fm = Ufopen(addr->return_filename, "rb")))
-            fprintf(fp, "    +++ Exim error... failed to open text file: %s\n",
-              strerror(errno));
-          else
-            {
-            while ((ch = fgetc(fm)) != EOF) fputc(ch, fp);
-            (void)fclose(fm);
-            }
-          Uunlink(addr->return_filename);
-
-          /* Can now add to handled chain, first fishing off the next
-          address on the msgchain. */
-
-          nextaddr = addr->next;
-          addr->next = handled_addr;
-          handled_addr = topaddr;
-          }
-	fputc('\n', fp);
-        }
-
-      /* output machine readable part */
-#ifdef SUPPORT_I18N
-      if (message_smtputf8)
-	fprintf(fp, "--%s\n"
-	    "Content-type: message/global-delivery-status\n\n"
-	    "Reporting-MTA: dns; %s\n",
-	  bound, smtp_active_hostname);
-      else
-#endif
-	fprintf(fp, "--%s\n"
-	    "Content-type: message/delivery-status\n\n"
-	    "Reporting-MTA: dns; %s\n",
-	  bound, smtp_active_hostname);
-
-      if (dsn_envid)
-	{
-        /* must be decoded from xtext: see RFC 3461:6.3a */
-        uschar *xdec_envid;
-        if (auth_xtextdecode(dsn_envid, &xdec_envid) > 0)
-          fprintf(fp, "Original-Envelope-ID: %s\n", dsn_envid);
-        else
-          fprintf(fp, "X-Original-Envelope-ID: error decoding xtext formatted ENVID\n");
-        }
-      fputc('\n', fp);
-
-      for (addr = handled_addr; addr; addr = addr->next)
-        {
-	host_item * hu;
-
-	print_dsn_addr_action(fp, addr, US"failed", US"5.0.0");
-
-        if ((hu = addr->host_used) && hu->name)
-	  {
-	  fprintf(fp, "Remote-MTA: dns; %s\n", hu->name);
-#ifdef EXPERIMENTAL_DSN_INFO
-	  {
-	  const uschar * s;
-	  if (hu->address)
-	    {
-	    uschar * p = hu->port == 25
-	      ? US"" : string_sprintf(":%d", hu->port);
-	    fprintf(fp, "Remote-MTA: X-ip; [%s]%s\n", hu->address, p);
-	    }
-	  if ((s = addr->smtp_greeting) && *s)
-	    fprintf(fp, "X-Remote-MTA-smtp-greeting: X-str; %.900s\n", s);
-	  if ((s = addr->helo_response) && *s)
-	    fprintf(fp, "X-Remote-MTA-helo-response: X-str; %.900s\n", s);
-	  if ((s = addr->message) && *s)
-	    fprintf(fp, "X-Exim-Diagnostic: X-str; %.900s\n", s);
-	  }
-#endif
-	  print_dsn_diagnostic_code(addr, fp);
-	  }
-	fputc('\n', fp);
-        }
-
-      /* Now copy the message, trying to give an intelligible comment if
-      it is too long for it all to be copied. The limit isn't strictly
-      applied because of the buffering. There is, however, an option
-      to suppress copying altogether. */
-
-      emf_text = next_emf(emf, US"copy");
-
-      /* add message body
-         we ignore the intro text from template and add
-         the text for bounce_return_size_limit at the end.
-
-         bounce_return_message is ignored
-         in case RET= is defined we honor these values
-         otherwise bounce_return_body is honored.
-
-         bounce_return_size_limit is always honored.
-      */
-
-      fprintf(fp, "--%s\n", bound);
-
-      dsnlimitmsg = US"X-Exim-DSN-Information: Due to administrative limits only headers are returned";
-      dsnnotifyhdr = NULL;
-      topt = topt_add_return_path;
-
-      /* RET=HDRS? top priority */
-      if (dsn_ret == dsn_ret_hdrs)
-        topt |= topt_no_body;
-      else
-	{
-	struct stat statbuf;
-
-        /* no full body return at all? */
-        if (!bounce_return_body)
-          {
-          topt |= topt_no_body;
-          /* add header if we overrule RET=FULL */
-          if (dsn_ret == dsn_ret_full)
-            dsnnotifyhdr = dsnlimitmsg;
-          }
-	/* line length limited... return headers only if oversize */
-        /* size limited ... return headers only if limit reached */
-	else if (  max_received_linelength > bounce_return_linesize_limit
-		|| (  bounce_return_size_limit > 0
-		   && fstat(deliver_datafile, &statbuf) == 0
-		   && statbuf.st_size > max
-		)  )
-	  {
-	  topt |= topt_no_body;
-	  dsnnotifyhdr = dsnlimitmsg;
-          }
-	}
-
-#ifdef SUPPORT_I18N
-      if (message_smtputf8)
-	fputs(topt & topt_no_body ? "Content-type: message/global-headers\n\n"
-				  : "Content-type: message/global\n\n",
-	      fp);
-      else
-#endif
-	fputs(topt & topt_no_body ? "Content-type: text/rfc822-headers\n\n"
-				  : "Content-type: message/rfc822\n\n",
-	      fp);
-
-      fflush(fp);
-      transport_filter_argv = NULL;   /* Just in case */
-      return_path = sender_address;   /* In case not previously set */
-	{			      /* Dummy transport for headers add */
-	transport_ctx tctx = {{0}};
-	transport_instance tb = {0};
-
-	tctx.u.fd = fileno(fp);
-	tctx.tblock = &tb;
-	tctx.options = topt;
-	tb.add_headers = dsnnotifyhdr;
-
-	/*XXX no checking for failure!  buggy! */
-	transport_write_message(&tctx, 0);
-	}
-      fflush(fp);
-
-      /* we never add the final text. close the file */
-      if (emf)
-        (void)fclose(emf);
-
-      fprintf(fp, "\n--%s--\n", bound);
-
-      /* Close the file, which should send an EOF to the child process
-      that is receiving the message. Wait for it to finish. */
-
-      (void)fclose(fp);
-      rc = child_close(pid, 0);     /* Waits for child to close, no timeout */
-
-      /* If the process failed, there was some disaster in setting up the
-      error message. Unless the message is very old, ensure that addr_defer
-      is non-null, which will have the effect of leaving the message on the
-      spool. The failed addresses will get tried again next time. However, we
-      don't really want this to happen too often, so freeze the message unless
-      there are some genuine deferred addresses to try. To do this we have
-      to call spool_write_header() here, because with no genuine deferred
-      addresses the normal code below doesn't get run. */
-
-      if (rc != 0)
-        {
-        uschar *s = US"";
-        if (now - received_time.tv_sec < retry_maximum_timeout && !addr_defer)
-          {
-          addr_defer = (address_item *)(+1);
-          f.deliver_freeze = TRUE;
-          deliver_frozen_at = time(NULL);
-          /* Panic-dies on error */
-          (void)spool_write_header(message_id, SW_DELIVERING, NULL);
-          s = US" (frozen)";
-          }
-        deliver_msglog("Process failed (%d) when writing error message "
-          "to %s%s", rc, bounce_recipient, s);
-        log_write(0, LOG_MAIN, "Process failed (%d) when writing error message "
-          "to %s%s", rc, bounce_recipient, s);
-        }
-
-      /* The message succeeded. Ensure that the recipients that failed are
-      now marked finished with on the spool and their parents updated. */
-
-      else
-        {
-        for (addr = handled_addr; addr; addr = addr->next)
-          {
-          address_done(addr, logtod);
-          child_done(addr, logtod);
-          }
-        /* Panic-dies on error */
-        (void)spool_write_header(message_id, SW_DELIVERING, NULL);
-        }
-      }
-    }
+    send_bounce_message(now, logtod);
   }
 
 f.disable_logging = FALSE;  /* In case left set */

commit e6841985b1abe753ea39f12665444234344cdc15
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Feb 23 14:17:29 2023 +0000

    Split out separate fn for delay-warning-message send

diff --git a/src/src/deliver.c b/src/src/deliver.c
index efb85348c..c86c4bb4b 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -5993,6 +5993,181 @@ wording. */
   }
 }
 
+/*************************************************
+*              Send a warning message            *
+*************************************************/
+/* Return: boolean success */
+
+static BOOL
+send_warning_message(const uschar * recipients, int queue_time, int show_time)
+{
+int fd;
+pid_t pid = child_open_exim(&fd, US"delay-warning-message");
+FILE * wmf = NULL, * f = fdopen(fd, "wb");
+uschar * wmf_text, * bound;
+transport_ctx tctx = {{0}};
+
+
+if (pid <= 0) return FALSE;
+
+if (warn_message_file)
+  wmf = expand_open(warn_message_file,
+	  US"warn_message_file", US"warning");
+
+warnmsg_recipients = recipients;
+warnmsg_delay = queue_time < 120*60
+  ? string_sprintf("%d minutes", show_time/60)
+  : string_sprintf("%d hours", show_time/3600);
+
+if (errors_reply_to)
+  fprintf(f, "Reply-To: %s\n", errors_reply_to);
+fprintf(f, "Auto-Submitted: auto-replied\n");
+moan_write_from(f);
+fprintf(f, "To: %s\n", recipients);
+moan_write_references(f, NULL);
+
+/* generated boundary string and output MIME-Headers */
+bound = string_sprintf(TIME_T_FMT "-eximdsn-%d", time(NULL), rand());
+
+fprintf(f, "Content-Type: multipart/report;"
+    " report-type=delivery-status; boundary=%s\n"
+    "MIME-Version: 1.0\n",
+  bound);
+
+if ((wmf_text = next_emf(wmf, US"header")))
+  fprintf(f, "%s\n", wmf_text);
+else
+  fprintf(f, "Subject: Warning: message %s delayed %s\n\n",
+    message_id, warnmsg_delay);
+
+/* output human readable part as text/plain section */
+fprintf(f, "--%s\n"
+    "Content-type: text/plain; charset=us-ascii\n\n",
+  bound);
+
+if ((wmf_text = next_emf(wmf, US"intro")))
+  fprintf(f, "%s", CS wmf_text);
+else
+  {
+  fprintf(f,
+"This message was created automatically by mail delivery software.\n");
+
+  if (Ustrcmp(recipients, sender_address) == 0)
+    fprintf(f,
+"A message that you sent has not yet been delivered to one or more of its\n"
+"recipients after more than ");
+
+  else
+    fprintf(f,
+"A message sent by\n\n  <%s>\n\n"
+"has not yet been delivered to one or more of its recipients after more than \n",
+      sender_address);
+
+  fprintf(f, "%s on the queue on %s.\n\n"
+      "The message identifier is:     %s\n",
+    warnmsg_delay, primary_hostname, message_id);
+
+  for (header_line * h = header_list; h; h = h->next)
+    if (strncmpic(h->text, US"Subject:", 8) == 0)
+      fprintf(f, "The subject of the message is: %s", h->text + 9);
+    else if (strncmpic(h->text, US"Date:", 5) == 0)
+      fprintf(f, "The date of the message is:    %s", h->text + 6);
+  fputc('\n', f);
+
+  fprintf(f, "The address%s to which the message has not yet been "
+    "delivered %s:\n",
+    !addr_defer->next ? "" : "es",
+    !addr_defer->next ? "is": "are");
+  }
+
+/* List the addresses, with error information if allowed */
+
+fputc('\n', f);
+for (address_item * addr = addr_defer; addr; addr = addr->next)
+  {
+  if (print_address_information(addr, f, US"  ", US"\n    ", US""))
+    print_address_error(addr, f, US"Delay reason: ");
+  fputc('\n', f);
+  }
+fputc('\n', f);
+
+/* Final text */
+
+if (wmf)
+  {
+  if ((wmf_text = next_emf(wmf, US"final")))
+    fprintf(f, "%s", CS wmf_text);
+  (void)fclose(wmf);
+  }
+else
+  {
+  fprintf(f,
+"No action is required on your part. Delivery attempts will continue for\n"
+"some time, and this warning may be repeated at intervals if the message\n"
+"remains undelivered. Eventually the mail delivery software will give up,\n"
+"and when that happens, the message will be returned to you.\n");
+  }
+
+/* output machine readable part */
+fprintf(f, "\n--%s\n"
+    "Content-type: message/delivery-status\n\n"
+    "Reporting-MTA: dns; %s\n",
+  bound,
+  smtp_active_hostname);
+
+
+if (dsn_envid)
+  {
+  /* must be decoded from xtext: see RFC 3461:6.3a */
+  uschar *xdec_envid;
+  if (auth_xtextdecode(dsn_envid, &xdec_envid) > 0)
+    fprintf(f,"Original-Envelope-ID: %s\n", dsn_envid);
+  else
+    fprintf(f,"X-Original-Envelope-ID: error decoding xtext formatted ENVID\n");
+  }
+fputc('\n', f);
+
+for (address_item * addr = addr_defer; addr; addr = addr->next)
+  {
+  host_item * hu;
+
+  print_dsn_addr_action(f, addr, US"delayed", US"4.0.0");
+
+  if ((hu = addr->host_used) && hu->name)
+    {
+    fprintf(f, "Remote-MTA: dns; %s\n", hu->name);
+    print_dsn_diagnostic_code(addr, f);
+    }
+  fputc('\n', f);
+  }
+
+fprintf(f, "--%s\n"
+    "Content-type: text/rfc822-headers\n\n",
+  bound);
+
+fflush(f);
+/* header only as required by RFC. only failure DSN needs to honor RET=FULL */
+tctx.u.fd = fileno(f);
+tctx.options = topt_add_return_path | topt_no_body;
+transport_filter_argv = NULL;   /* Just in case */
+return_path = sender_address;   /* In case not previously set */
+
+/* Write the original email out */
+/*XXX no checking for failure!  buggy! */
+transport_write_message(&tctx, 0);
+fflush(f);
+
+fprintf(f,"\n--%s--\n", bound);
+
+fflush(f);
+
+/* Close and wait for child process to complete, without a timeout.
+If there's an error, don't update the count. */
+
+(void)fclose(f);
+return child_close(pid, 0) == 0;
+}
+
 /*************************************************
 *              Deliver one message               *
 *************************************************/
@@ -8115,7 +8290,7 @@ was set just to keep the message on the spool, so there is nothing to do here.
 
 else if (addr_defer != (address_item *)(+1))
   {
-  uschar *recipients = US"";
+  uschar * recipients = US"";
   BOOL want_warning_msg = FALSE;
 
   deliver_domain = testflag(addr_defer, af_pfr)
@@ -8123,7 +8298,7 @@ else if (addr_defer != (address_item *)(+1))
 
   for (address_item * addr = addr_defer; addr; addr = addr->next)
     {
-    address_item *otaddr;
+    address_item * otaddr;
 
     if (addr->basic_errno > ERRNO_WARN_BASE) want_warning_msg = TRUE;
 
@@ -8245,181 +8420,11 @@ else if (addr_defer != (address_item *)(+1))
     have been. */
 
     if (warning_count < count)
-      {
-      header_line *h;
-      int fd;
-      pid_t pid = child_open_exim(&fd, US"delay-warning-message");
-
-      if (pid > 0)
-        {
-        uschar * wmf_text;
-        FILE * wmf = NULL;
-        FILE * f = fdopen(fd, "wb");
-	uschar * bound;
-	transport_ctx tctx = {{0}};
-
-        if (warn_message_file)
-	  wmf = expand_open(warn_message_file,
-		  US"warn_message_file", US"warning");
-
-        warnmsg_recipients = recipients;
-        warnmsg_delay = queue_time < 120*60
-	  ? string_sprintf("%d minutes", show_time/60)
-	  : string_sprintf("%d hours", show_time/3600);
-
-        if (errors_reply_to)
-          fprintf(f, "Reply-To: %s\n", errors_reply_to);
-        fprintf(f, "Auto-Submitted: auto-replied\n");
-        moan_write_from(f);
-        fprintf(f, "To: %s\n", recipients);
-	moan_write_references(f, NULL);
-
-        /* generated boundary string and output MIME-Headers */
-        bound = string_sprintf(TIME_T_FMT "-eximdsn-%d", time(NULL), rand());
-
-        fprintf(f, "Content-Type: multipart/report;"
-	    " report-type=delivery-status; boundary=%s\n"
-	    "MIME-Version: 1.0\n",
-	  bound);
-
-        if ((wmf_text = next_emf(wmf, US"header")))
-          fprintf(f, "%s\n", wmf_text);
-        else
-          fprintf(f, "Subject: Warning: message %s delayed %s\n\n",
-            message_id, warnmsg_delay);
-
-        /* output human readable part as text/plain section */
-        fprintf(f, "--%s\n"
-	    "Content-type: text/plain; charset=us-ascii\n\n",
-	  bound);
-
-        if ((wmf_text = next_emf(wmf, US"intro")))
-	  fprintf(f, "%s", CS wmf_text);
-	else
-          {
-          fprintf(f,
-"This message was created automatically by mail delivery software.\n");
-
-          if (Ustrcmp(recipients, sender_address) == 0)
-            fprintf(f,
-"A message that you sent has not yet been delivered to one or more of its\n"
-"recipients after more than ");
-
-          else
-	    fprintf(f,
-"A message sent by\n\n  <%s>\n\n"
-"has not yet been delivered to one or more of its recipients after more than \n",
-	      sender_address);
-
-          fprintf(f, "%s on the queue on %s.\n\n"
-	      "The message identifier is:     %s\n",
-	    warnmsg_delay, primary_hostname, message_id);
-
-          for (h = header_list; h; h = h->next)
-            if (strncmpic(h->text, US"Subject:", 8) == 0)
-              fprintf(f, "The subject of the message is: %s", h->text + 9);
-            else if (strncmpic(h->text, US"Date:", 5) == 0)
-              fprintf(f, "The date of the message is:    %s", h->text + 6);
-          fputc('\n', f);
-
-          fprintf(f, "The address%s to which the message has not yet been "
-            "delivered %s:\n",
-            !addr_defer->next ? "" : "es",
-            !addr_defer->next ? "is": "are");
-          }
-
-        /* List the addresses, with error information if allowed */
-
-        fputc('\n', f);
-	for (address_item * addr = addr_defer; addr; addr = addr->next)
-          {
-          if (print_address_information(addr, f, US"  ", US"\n    ", US""))
-            print_address_error(addr, f, US"Delay reason: ");
-          fputc('\n', f);
-          }
-        fputc('\n', f);
-
-        /* Final text */
-
-        if (wmf)
-          {
-          if ((wmf_text = next_emf(wmf, US"final")))
-	    fprintf(f, "%s", CS wmf_text);
-          (void)fclose(wmf);
-          }
-        else
-          {
-          fprintf(f,
-"No action is required on your part. Delivery attempts will continue for\n"
-"some time, and this warning may be repeated at intervals if the message\n"
-"remains undelivered. Eventually the mail delivery software will give up,\n"
-"and when that happens, the message will be returned to you.\n");
-          }
-
-        /* output machine readable part */
-        fprintf(f, "\n--%s\n"
-	    "Content-type: message/delivery-status\n\n"
-	    "Reporting-MTA: dns; %s\n",
-	  bound,
-	  smtp_active_hostname);
-
-
-        if (dsn_envid)
-	  {
-          /* must be decoded from xtext: see RFC 3461:6.3a */
-          uschar *xdec_envid;
-          if (auth_xtextdecode(dsn_envid, &xdec_envid) > 0)
-            fprintf(f,"Original-Envelope-ID: %s\n", dsn_envid);
-          else
-            fprintf(f,"X-Original-Envelope-ID: error decoding xtext formatted ENVID\n");
-          }
-        fputc('\n', f);
-
-	for (address_item * addr = addr_defer; addr; addr = addr->next)
-          {
-	  host_item * hu;
-
-	  print_dsn_addr_action(f, addr, US"delayed", US"4.0.0");
-
-          if ((hu = addr->host_used) && hu->name)
-            {
-            fprintf(f, "Remote-MTA: dns; %s\n", hu->name);
-            print_dsn_diagnostic_code(addr, f);
-            }
-	  fputc('\n', f);
-          }
-
-        fprintf(f, "--%s\n"
-	    "Content-type: text/rfc822-headers\n\n",
-	  bound);
-
-        fflush(f);
-        /* header only as required by RFC. only failure DSN needs to honor RET=FULL */
-	tctx.u.fd = fileno(f);
-        tctx.options = topt_add_return_path | topt_no_body;
-        transport_filter_argv = NULL;   /* Just in case */
-        return_path = sender_address;   /* In case not previously set */
-
-        /* Write the original email out */
-	/*XXX no checking for failure!  buggy! */
-        transport_write_message(&tctx, 0);
-        fflush(f);
-
-        fprintf(f,"\n--%s--\n", bound);
-
-        fflush(f);
-
-        /* Close and wait for child process to complete, without a timeout.
-        If there's an error, don't update the count. */
-
-        (void)fclose(f);
-        if (child_close(pid, 0) == 0)
-          {
-          warning_count = count;
-          update_spool = TRUE;    /* Ensure spool rewritten */
-          }
-        }
-      }
+      if (send_warning_message(recipients, queue_time, show_time))
+	{
+	warning_count = count;
+	update_spool = TRUE;    /* Ensure spool rewritten */
+	}
     }
 
   /* Clear deliver_domain */
diff --git a/src/src/globals.c b/src/src/globals.c
index 78b225fbc..c6bacc02f 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -1657,8 +1657,8 @@ uschar *version_string         = US"?";
 
 uschar *warn_message_file      = NULL;
 int     warning_count          = 0;
-uschar *warnmsg_delay          = NULL;
-uschar *warnmsg_recipients     = NULL;
+const uschar *warnmsg_delay    = NULL;
+const uschar *warnmsg_recipients = NULL;
 
 
 /*  End of globals.c */
diff --git a/src/src/globals.h b/src/src/globals.h
index 914e2d0f9..81d052fd5 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -1114,8 +1114,8 @@ extern uschar *uucp_from_pattern;      /* For recognizing "From " lines */
 extern uschar *uucp_from_sender;       /* For building the sender */
 
 extern uschar *warn_message_file;      /* Template for warning messages */
-extern uschar *warnmsg_delay;          /* String form of delay time */
-extern uschar *warnmsg_recipients;     /* Recipients of warning message */
+extern const uschar *warnmsg_delay;    /* String form of delay time */
+extern const uschar *warnmsg_recipients; /* Recipients of warning message */
 extern BOOL    write_rejectlog;        /* Control of reject logging */
 
 extern uschar *verify_mode;	       /* Running a router in verify mode */

commit e603a342100ed93b09a152b6150e65f77abe5b0d
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Feb 23 14:33:05 2023 +0000

    Split out separate fn for success-DSN send

diff --git a/src/src/deliver.c b/src/src/deliver.c
index c86c4bb4b..993b24231 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -68,7 +68,6 @@ static address_item *addr_new = NULL;
 static address_item *addr_remote = NULL;
 static address_item *addr_route = NULL;
 static address_item *addr_succeed = NULL;
-static address_item *addr_senddsn = NULL;
 
 static FILE *message_log = NULL;
 static BOOL update_spool;
@@ -6168,6 +6167,162 @@ If there's an error, don't update the count. */
 return child_close(pid, 0) == 0;
 }
 
+/*************************************************
+*              Send a success-DSN                *
+*************************************************/
+
+static void
+maybe_send_dsn(void)
+{
+address_item * addr_senddsn = NULL;
+
+for (address_item * a = addr_succeed; a; a = a->next)
+  {
+  /* af_ignore_error not honored here. it's not an error */
+  DEBUG(D_deliver) debug_printf("DSN: processing router : %s\n"
+      "DSN: processing successful delivery address: %s\n"
+      "DSN: Sender_address: %s\n"
+      "DSN: orcpt: %s  flags: 0x%x\n"
+      "DSN: envid: %s  ret: %d\n"
+      "DSN: Final recipient: %s\n"
+      "DSN: Remote SMTP server supports DSN: %d\n",
+      a->router ? a->router->name : US"(unknown)",
+      a->address,
+      sender_address,
+      a->dsn_orcpt ? a->dsn_orcpt : US"NULL",
+      a->dsn_flags,
+      dsn_envid ? dsn_envid : US"NULL", dsn_ret,
+      a->address,
+      a->dsn_aware
+      );
+
+  /* send report if next hop not DSN aware or a router flagged "last DSN hop"
+  and a report was requested */
+
+  if (  (a->dsn_aware != dsn_support_yes || a->dsn_flags & rf_dsnlasthop)
+     && a->dsn_flags & rf_notify_success
+     )
+    {
+    /* copy and relink address_item and send report with all of them at once later */
+    address_item * addr_next = addr_senddsn;
+    addr_senddsn = store_get(sizeof(address_item), GET_UNTAINTED);
+    *addr_senddsn = *a;
+    addr_senddsn->next = addr_next;
+    }
+  else
+    DEBUG(D_deliver) debug_printf("DSN: not sending DSN success message\n");
+  }
+
+if (addr_senddsn)
+  {				/* create exim process to send message */
+  int fd;
+  pid_t pid = child_open_exim(&fd, US"DSN");
+
+  DEBUG(D_deliver) debug_printf("DSN: child_open_exim returns: %d\n", pid);
+
+  if (pid < 0)  /* Creation of child failed */
+    {
+    log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Process %d (parent %d) failed to "
+      "create child process to send success-dsn message: %s", getpid(),
+      getppid(), strerror(errno));
+
+    DEBUG(D_deliver) debug_printf("DSN: child_open_exim failed\n");
+    }
+  else  /* Creation of child succeeded */
+    {
+    FILE * f = fdopen(fd, "wb");
+    /* header only as required by RFC. only failure DSN needs to honor RET=FULL */
+    uschar * bound;
+    transport_ctx tctx = {{0}};
+
+    DEBUG(D_deliver)
+      debug_printf("sending success-dsn to: %s\n", sender_address);
+
+    /* build unique id for MIME boundary */
+    bound = string_sprintf(TIME_T_FMT "-eximdsn-%d", time(NULL), rand());
+    DEBUG(D_deliver) debug_printf("DSN: MIME boundary: %s\n", bound);
+
+    if (errors_reply_to)
+      fprintf(f, "Reply-To: %s\n", errors_reply_to);
+
+    moan_write_from(f);
+    fprintf(f, "Auto-Submitted: auto-generated\n"
+	"To: %s\n"
+	"Subject: Delivery Status Notification\n",
+      sender_address);
+    moan_write_references(f, NULL);
+    fprintf(f, "Content-Type: multipart/report;"
+    		" report-type=delivery-status; boundary=%s\n"
+	"MIME-Version: 1.0\n\n"
+
+	"--%s\n"
+	"Content-type: text/plain; charset=us-ascii\n\n"
+
+	"This message was created automatically by mail delivery software.\n"
+	" ----- The following addresses had successful delivery notifications -----\n",
+      bound, bound);
+
+    for (address_item * a = addr_senddsn; a; a = a->next)
+      fprintf(f, "<%s> (relayed %s)\n\n",
+	a->address,
+	a->dsn_flags & rf_dsnlasthop ? "via non DSN router"
+	: a->dsn_aware == dsn_support_no ? "to non-DSN-aware mailer"
+	: "via non \"Remote SMTP\" router"
+	);
+
+    fprintf(f, "--%s\n"
+	"Content-type: message/delivery-status\n\n"
+	"Reporting-MTA: dns; %s\n",
+      bound, smtp_active_hostname);
+
+    if (dsn_envid)
+      {			/* must be decoded from xtext: see RFC 3461:6.3a */
+      uschar * xdec_envid;
+      if (auth_xtextdecode(dsn_envid, &xdec_envid) > 0)
+        fprintf(f, "Original-Envelope-ID: %s\n", dsn_envid);
+      else
+        fprintf(f, "X-Original-Envelope-ID: error decoding xtext formatted ENVID\n");
+      }
+    fputc('\n', f);
+
+    for (address_item * a = addr_senddsn; a; a = a->next)
+      {
+      host_item * hu;
+
+      print_dsn_addr_action(f, a, US"delivered", US"2.0.0");
+
+      if ((hu = a->host_used) && hu->name)
+        fprintf(f, "Remote-MTA: dns; %s\nDiagnostic-Code: smtp; 250 Ok\n\n",
+	  hu->name);
+      else
+	fprintf(f, "Diagnostic-Code: X-Exim; relayed via non %s router\n\n",
+	  a->dsn_flags & rf_dsnlasthop ? "DSN" : "SMTP");
+      }
+
+    fprintf(f, "--%s\nContent-type: text/rfc822-headers\n\n", bound);
+
+    fflush(f);
+    transport_filter_argv = NULL;   /* Just in case */
+    return_path = sender_address;   /* In case not previously set */
+
+    /* Write the original email out */
+
+    tctx.u.fd = fd;
+    tctx.options = topt_add_return_path | topt_no_body;
+    /*XXX hmm, FALSE(fail) retval ignored.
+    Could error for any number of reasons, and they are not handled. */
+    transport_write_message(&tctx, 0);
+    fflush(f);
+
+    fprintf(f,"\n--%s--\n", bound);
+
+    fflush(f);
+    fclose(f);
+    (void) child_close(pid, 0);     /* Waits for child to close, no timeout */
+    }
+  }
+}
+
 /*************************************************
 *              Deliver one message               *
 *************************************************/
@@ -7959,156 +8114,8 @@ else if (!f.dont_deliver)
   retry_update(&addr_defer, &addr_failed, &addr_succeed);
 
 /* Send DSN for successful messages if requested */
-addr_senddsn = NULL;
 
-for (address_item * a = addr_succeed; a; a = a->next)
-  {
-  /* af_ignore_error not honored here. it's not an error */
-  DEBUG(D_deliver) debug_printf("DSN: processing router : %s\n"
-      "DSN: processing successful delivery address: %s\n"
-      "DSN: Sender_address: %s\n"
-      "DSN: orcpt: %s  flags: 0x%x\n"
-      "DSN: envid: %s  ret: %d\n"
-      "DSN: Final recipient: %s\n"
-      "DSN: Remote SMTP server supports DSN: %d\n",
-      a->router ? a->router->name : US"(unknown)",
-      a->address,
-      sender_address,
-      a->dsn_orcpt ? a->dsn_orcpt : US"NULL",
-      a->dsn_flags,
-      dsn_envid ? dsn_envid : US"NULL", dsn_ret,
-      a->address,
-      a->dsn_aware
-      );
-
-  /* send report if next hop not DSN aware or a router flagged "last DSN hop"
-  and a report was requested */
-
-  if (  (a->dsn_aware != dsn_support_yes || a->dsn_flags & rf_dsnlasthop)
-     && a->dsn_flags & rf_notify_success
-     )
-    {
-    /* copy and relink address_item and send report with all of them at once later */
-    address_item * addr_next = addr_senddsn;
-    addr_senddsn = store_get(sizeof(address_item), GET_UNTAINTED);
-    *addr_senddsn = *a;
-    addr_senddsn->next = addr_next;
-    }
-  else
-    DEBUG(D_deliver) debug_printf("DSN: not sending DSN success message\n");
-  }
-
-if (addr_senddsn)
-  {
-  pid_t pid;
-  int fd;
-
-  /* create exim process to send message */
-  pid = child_open_exim(&fd, US"DSN");
-
-  DEBUG(D_deliver) debug_printf("DSN: child_open_exim returns: %d\n", pid);
-
-  if (pid < 0)  /* Creation of child failed */
-    {
-    log_write(0, LOG_MAIN|LOG_PANIC_DIE, "Process %d (parent %d) failed to "
-      "create child process to send success-dsn message: %s", getpid(),
-      getppid(), strerror(errno));
-
-    DEBUG(D_deliver) debug_printf("DSN: child_open_exim failed\n");
-    }
-  else  /* Creation of child succeeded */
-    {
-    FILE * f = fdopen(fd, "wb");
-    /* header only as required by RFC. only failure DSN needs to honor RET=FULL */
-    uschar * bound;
-    transport_ctx tctx = {{0}};
-
-    DEBUG(D_deliver)
-      debug_printf("sending success-dsn to: %s\n", sender_address);
-
-    /* build unique id for MIME boundary */
-    bound = string_sprintf(TIME_T_FMT "-eximdsn-%d", time(NULL), rand());
-    DEBUG(D_deliver) debug_printf("DSN: MIME boundary: %s\n", bound);
-
-    if (errors_reply_to)
-      fprintf(f, "Reply-To: %s\n", errors_reply_to);
-
-    moan_write_from(f);
-    fprintf(f, "Auto-Submitted: auto-generated\n"
-	"To: %s\n"
-	"Subject: Delivery Status Notification\n",
-      sender_address);
-    moan_write_references(f, NULL);
-    fprintf(f, "Content-Type: multipart/report;"
-    		" report-type=delivery-status; boundary=%s\n"
-	"MIME-Version: 1.0\n\n"
-
-	"--%s\n"
-	"Content-type: text/plain; charset=us-ascii\n\n"
-
-	"This message was created automatically by mail delivery software.\n"
-	" ----- The following addresses had successful delivery notifications -----\n",
-      bound, bound);
-
-    for (address_item * a = addr_senddsn; a; a = a->next)
-      fprintf(f, "<%s> (relayed %s)\n\n",
-	a->address,
-	a->dsn_flags & rf_dsnlasthop ? "via non DSN router"
-	: a->dsn_aware == dsn_support_no ? "to non-DSN-aware mailer"
-	: "via non \"Remote SMTP\" router"
-	);
-
-    fprintf(f, "--%s\n"
-	"Content-type: message/delivery-status\n\n"
-	"Reporting-MTA: dns; %s\n",
-      bound, smtp_active_hostname);
-
-    if (dsn_envid)
-      {			/* must be decoded from xtext: see RFC 3461:6.3a */
-      uschar * xdec_envid;
-      if (auth_xtextdecode(dsn_envid, &xdec_envid) > 0)
-        fprintf(f, "Original-Envelope-ID: %s\n", dsn_envid);
-      else
-        fprintf(f, "X-Original-Envelope-ID: error decoding xtext formatted ENVID\n");
-      }
-    fputc('\n', f);
-
-    for (address_item * a = addr_senddsn; a; a = a->next)
-      {
-      host_item * hu;
-
-      print_dsn_addr_action(f, a, US"delivered", US"2.0.0");
-
-      if ((hu = a->host_used) && hu->name)
-        fprintf(f, "Remote-MTA: dns; %s\nDiagnostic-Code: smtp; 250 Ok\n\n",
-	  hu->name);
-      else
-	fprintf(f, "Diagnostic-Code: X-Exim; relayed via non %s router\n\n",
-	  a->dsn_flags & rf_dsnlasthop ? "DSN" : "SMTP");
-      }
-
-    fprintf(f, "--%s\nContent-type: text/rfc822-headers\n\n", bound);
-
-    fflush(f);
-    transport_filter_argv = NULL;   /* Just in case */
-    return_path = sender_address;   /* In case not previously set */
-
-    /* Write the original email out */
-
-    tctx.u.fd = fd;
-    tctx.options = topt_add_return_path | topt_no_body;
-    /*XXX hmm, FALSE(fail) retval ignored.
-    Could error for any number of reasons, and they are not handled. */
-    transport_write_message(&tctx, 0);
-    fflush(f);
-
-    fprintf(f,"\n--%s--\n", bound);
-
-    fflush(f);
-    fclose(f);
-    rc = child_close(pid, 0);     /* Waits for child to close, no timeout */
-    }
-  }
+maybe_send_dsn();
 
 /* If any addresses failed, we must send a message to somebody, unless
 af_ignore_error is set, in which case no action is taken. It is possible for
@@ -8449,27 +8456,23 @@ else if (addr_defer != (address_item *)(+1))
 
   if (f.deliver_freeze)
     {
-    if (freeze_tell && freeze_tell[0] != 0 && !f.local_error_message)
+    if (freeze_tell && *freeze_tell && !f.local_error_message)
       {
-      uschar *s = string_copy(frozen_info);
-      uschar *ss = Ustrstr(s, " by the system filter: ");
+      uschar * s = string_copy(frozen_info);
+      uschar * ss = Ustrstr(s, " by the system filter: ");
 
-      if (ss != NULL)
+      if (ss)
         {
         ss[21] = '.';
         ss[22] = '\n';
         }
 
-      ss = s;
-      while (*ss != 0)
-        {
+      for (ss = s; *ss; )
         if (*ss == '\\' && ss[1] == 'n')
-          {
-          *ss++ = ' ';
-          *ss++ = '\n';
-          }
-        else ss++;
-        }
+          { *ss++ = ' '; *ss++ = '\n'; }
+        else
+	  ss++;
+
       moan_tell_someone(freeze_tell, addr_defer, US"Message frozen",
         "Message %s has been frozen%s.\nThe sender is <%s>.\n", message_id,
         s, sender_address);

commit 7c60296900bdff369ffd2bf54eecfe6097b997a4
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Feb 25 15:50:58 2023 +0000

    Linewrap long lines in bounce bodies.  Bug 2979

diff --git a/src/src/deliver.c b/src/src/deliver.c
index 993b24231..d0e6d1c2e 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -5400,19 +5400,18 @@ uschar * s = testflag(addr, af_pass_message) ? addr->message : NULL;
 unsigned cnt;
 
 /* af_pass_message and addr->message set ? print remote host answer */
-if (s)
-  {
-  DEBUG(D_deliver)
-    debug_printf("DSN Diagnostic-Code: addr->message = %s\n", addr->message);
+if (!s)
+  return;
 
-  /* search first ": ". we assume to find the remote-MTA answer there */
-  if (!(s = Ustrstr(addr->message, ": ")))
-    return;				/* not found, bail out */
-  s += 2;  /* skip ": " */
-  cnt = fprintf(f, "Diagnostic-Code: smtp; ");
-  }
-/* no message available. do nothing */
-else return;
+DEBUG(D_deliver)
+  debug_printf("DSN Diagnostic-Code: addr->message = %s\n", addr->message);
+
+/* search first ": ". we assume to find the remote-MTA answer there */
+if (!(s = Ustrstr(addr->message, ": ")))
+  return;				/* not found, bail out */
+
+s += 2;  /* skip ": " */
+cnt = fprintf(f, "Diagnostic-Code: smtp; ");
 
 while (*s)
   {
@@ -5551,6 +5550,82 @@ else if (!(fp = Ufopen(s, "rb")))
 return fp;
 }
 
+
+/* Output the given header and string, converting either
+the sequence "\n" or a real newline into newline plus space.
+If that still takes us past column 78, look for the last space
+and split there too.
+Append a newline if string did not have one.
+Limit to about 1024 chars total. */
+
+static void
+dsn_put_wrapped(FILE * fp, const uschar * header, const uschar * s)
+{
+const uschar * t;
+int llen = fprintf(fp, "%s", CS header), sleft = Ustrlen(s);
+int remain = 1022 - llen;
+
+if (*s && remain > 0)
+  {
+  for(;;)
+    {
+    unsigned ltail;	/* source chars to skip */
+
+    /* Chop at a newline, or end of string */
+
+    if ((t = Ustrchr(s, '\\')) && t[1] == 'n')
+      ltail = 2;
+    else if ((t = Ustrchr(s, '\n')))
+      ltail = 1;
+    else
+      {
+      t = s + sleft;
+      ltail = 0;
+      }
+
+    /* If that is too long, search backward for a space */
+
+    if ((llen + t - s) > 78)
+      {
+      const uschar * u;
+      for (u = s + 78 - llen; u > s + 10; --u) if (*u == ' ') break;
+      if (u > s + 10)
+	{				/* found a space to linebreak at */
+	llen = u - s;
+	remain -= fprintf(fp, "%.*s", (int)llen, s);
+	s += ++llen;			/* skip the space also */
+	}
+      else if (llen < 78)
+	{				/* just linebreak at 78 */
+	llen = 78 - llen;
+	remain -= fprintf(fp, "%.*s", llen, s);
+	s += llen;
+	}
+      else				/* header rather long */
+	llen = 0;
+      }
+    else
+      {
+      llen = t - s;
+      remain -= fprintf(fp, "%.*s", llen, s);
+      s = t + ltail;
+      }
+
+    sleft -= llen;
+    remain -= 2;
+    if (!*s || remain <= 0)
+      break;
+    fputs("\n ", fp);
+    llen = 1;		/* one for the leading space output above */
+    }
+
+  if (s[-1] != '\n') fputs("\n", fp);
+  }
+else
+  fputs("\n", fp);
+}
+
+
 /*************************************************
 *              Send a bounce message             *
 *************************************************/
@@ -5818,7 +5893,7 @@ wording. */
   if (dsn_envid)
     {
     /* must be decoded from xtext: see RFC 3461:6.3a */
-    uschar *xdec_envid;
+    uschar * xdec_envid;
     if (auth_xtextdecode(dsn_envid, &xdec_envid) > 0)
       fprintf(fp, "Original-Envelope-ID: %s\n", dsn_envid);
     else
@@ -5845,11 +5920,11 @@ wording. */
 	fprintf(fp, "Remote-MTA: X-ip; [%s]%s\n", hu->address, p);
 	}
       if ((s = addr->smtp_greeting) && *s)
-	fprintf(fp, "X-Remote-MTA-smtp-greeting: X-str; %.900s\n", s);
+	dsn_put_wrapped(fp, US"X-Remote-MTA-smtp-greeting: X-str; ", s);
       if ((s = addr->helo_response) && *s)
-	fprintf(fp, "X-Remote-MTA-helo-response: X-str; %.900s\n", s);
+	dsn_put_wrapped(fp, US"X-Remote-MTA-helo-response: X-str; ", s);
       if ((s = addr->message) && *s)
-	fprintf(fp, "X-Exim-Diagnostic: X-str; %.900s\n", s);
+	dsn_put_wrapped(fp, US"X-Exim-Diagnostic: X-str; ", s);
       }
 #endif
       print_dsn_diagnostic_code(addr, fp);

commit bd0f95ded48f560cb1f9f8b808e1abaabeb4d4ec
Author: Andrew Aitchison <exim@aitchison.me.uk>
Date:   Sat Mar 4 17:23:09 2023 +0000

    exim_msgdate: more options, better perl version compatibility

diff --git a/src/src/exim_msgdate.src b/src/src/exim_msgdate.src
index e5c357bca..c591f306e 100755
--- a/src/src/exim_msgdate.src
+++ b/src/src/exim_msgdate.src
@@ -1,4 +1,4 @@
-#!PERL_COMMAND -WT
+#!PERL_COMMAND -T
 #
 # Utility to convert an exim message-id to a human readable form
 #
@@ -27,6 +27,11 @@
 #
 # PROCESSED_FLAG
 
+# These match runtest
+use v5.10.1;
+use warnings;
+use if $^V >= v5.19.11, experimental => 'smartmatch';
+
 use strict;
 use File::Basename;
 use Getopt::Long;
@@ -48,6 +53,7 @@ if (defined $ENV{TZ}) {
 }
 
 my $localhost_number;  # An Exim config value
+my $nolocalhost_number;
 
 my $p_name    = basename $0;
 my $p_version = "20230203.0";
@@ -56,20 +62,13 @@ my $p_cp      = <<EOM;
 
  Portions taken from exicyclog.src, which is
    Copyright (c) University of Cambridge, 1995 - 2015
-   See the file NOTICE for conditions of use and distribution.
+ See the file NOTICE for conditions of use and distribution.
 EOM
 
 $ENV{PATH} = "/bin:/usr/bin:/usr/sbin";
 
 use POSIX qw(strftime);
 
-sub main::VERSION_MESSAGE()
-{
-    print basename($0), ": $0\n";
-    print "build: EXIM_RELEASE_VERSIONEXIM_VARIANT_VERSION\n";
-    print "perl(	      runtime): $]\n";
-}
-
 my ($debug, $nodebug,
     $optbase, $optbase36, $optbase62,
     $optunix, $optgmt, $optlocal,
@@ -95,6 +94,9 @@ GetOptions (
     "base62" => \$optbase62,
 
     "localhost_number=s" => \$localhost_number,  # cf "local"
+    "nolocalhost_number" => \$nolocalhost_number,
+    "no-localhost_number" => \$nolocalhost_number,
+    "no_localhost_number" => \$nolocalhost_number,
 
     "unix" => \$optunix,
     "u" => \$optunix,
@@ -122,6 +124,11 @@ GetOptions (
             -noperldoc => system('perldoc -V 2>/dev/null 1>&2')
        );
     },
+    'version'  => sub {
+	print basename($0), ": $0\n";
+	print "build: EXIM_RELEASE_VERSIONEXIM_VARIANT_VERSION\n";
+	print "perl(runtime): $]\n";
+    },
 ) or pod2usage;
 # die("Error in command line arguments\n");
 
@@ -270,13 +277,27 @@ if ($debug) {
     } else {
         warn "localhost_number unset\n";
     }
+    if (defined $nolocalhost_number) {
+        warn "nolocalhost_number=$nolocalhost_number\n";
+    } else {
+        warn "nolocalhost_number unset\n";
+    }
 }
 
 if (defined $localhost_number) {
     if ($localhost_number eq "none") {
         $localhost_number = undef;
+        $nolocalhost_number = TRUE;
+    } else {
+	if ($nolocalhost_number) {
+	    die "aborting: localhost_number and nolocalhost_number both set\n ";
+	}
+        $nolocalhost_number = FALSE;
     }
-} else {
+}
+
+unless (defined $nolocalhost_number) {
+    warn "Looking for config file\n" if $debug;
     my $config = get_configfilename();
     warn "Reading config $config to find localhost_number\n" if $debug;
 
@@ -296,6 +317,9 @@ if (defined $localhost_number) {
         warn "$config gives localhost_number $localhost_number\n"
             if $debug and defined $localhost_number;
     } else {
+	if ($debug) {
+	    warn "cannot read config file $config\n";
+	}
         # This way we get the expanded value for localhost_number
         # directly from exim, but we have to guess which exim binary ...
         # On Debian and Ubuntu, /usr/sbin/exim is a link to exim4 so is OK.
@@ -315,10 +339,18 @@ if (defined $localhost_number) {
 }
 
 if (defined $localhost_number) {
-    die "localhost_number > 16\n"
-        if $localhost_number > 16;
-    die "localhost_number > 10\n"
-        if $localhost_number > 10 && ($base != 62);
+    if ($localhost_number =~ /\D/) {
+	die "localhost_number must be a number >=0\n";
+    } elsif ($localhost_number =~ /^\d*$/) {
+	die "localhost_number > 16\n"
+	    if $localhost_number > 16;
+	die "localhost_number > 10\n"
+	    if $localhost_number > 10 && ($base != 62);
+    } else {
+	warn "clearing localhost_number - was $localhost_number\n";
+	undef $localhost_number;
+	$nolocalhost_number=TRUE;
+    }
 }
 
 if ($debug) {

commit c5722bc5a8df130f4733ca3c4398ae51cc7842e0
Author: Andrew Aitchison <exim@aitchison.me.uk>
Date:   Sun Mar 5 12:47:17 2023 +0000

    exim_msgdate: version output does not require an exim config

diff --git a/src/src/exim_msgdate.src b/src/src/exim_msgdate.src
index c591f306e..bfb5bc81e 100755
--- a/src/src/exim_msgdate.src
+++ b/src/src/exim_msgdate.src
@@ -56,7 +56,7 @@ my $localhost_number;  # An Exim config value
 my $nolocalhost_number;
 
 my $p_name    = basename $0;
-my $p_version = "20230203.0";
+my $p_version = "20230304.0";
 my $p_cp      = <<EOM;
  Copyright (c) 2023 The Exim Maintainers 2023
 
@@ -125,9 +125,10 @@ GetOptions (
        );
     },
     'version'  => sub {
-	print basename($0), ": $0\n";
-	print "build: EXIM_RELEASE_VERSIONEXIM_VARIANT_VERSION\n";
+	print basename($0), ": $p_version $0\n";
+	print "exim build: EXIM_RELEASE_VERSIONEXIM_VARIANT_VERSION\n";
 	print "perl(runtime): $]\n";
+	exit 0;
     },
 ) or pod2usage;
 # die("Error in command line arguments\n");

commit cfeaa009b9b407d8b404438afbf4c5d85d61e8c1
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Mar 4 22:55:01 2023 +0000

    Truncate overlong lines in DSNs.  Bug 2979

diff --git a/src/src/deliver.c b/src/src/deliver.c
index d0e6d1c2e..9b77b3619 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -5370,6 +5370,11 @@ while (*s)
       fprintf(f, "\n   ");  /* sic (because space follows) */
       count = 0;
       }
+    else if (count > 254)	/* arbitrary limit */
+      {
+      fprintf(f, "[truncated]");
+      do s++; while (*s && !(*s == '\\' && s[1] == '\n'));
+      }
     }
 }
 

commit ca8410e981982edd16bcc8689e09c2c15d8267e7
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Mar 5 00:40:49 2023 +0000

    Fix crash in queue-ramp
    
    Broken-by: 1e835086d159

diff --git a/src/src/daemon.c b/src/src/daemon.c
index b0533c28f..caed44bb3 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -1344,7 +1344,10 @@ switch (buf[0])
     memcpy(queuerun_msgid, buf+1, MESSAGE_ID_LENGTH+1);
 
     for (qrunner * q = qrunners; q; q = q->next)
-      if (Ustrcmp(q->name, buf+1+MESSAGE_ID_LENGTH+1) == 0)
+      if (q->name
+	  ? Ustrcmp(q->name, buf+1+MESSAGE_ID_LENGTH+1) == 0
+	  : !buf[1+MESSAGE_ID_LENGTH+1]
+	 )
 	{ queuerun_msg_qname = q->name; break; }
     return TRUE;
 #endif

commit a6d90e094d4e2a84d18859cf2005d10c2020e2d4
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Mar 11 17:48:28 2023 +0000

    Header-wrap expansion.  Bug 2843

diff --git a/src/src/deliver.c b/src/src/deliver.c
index 9b77b3619..e2994b116 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -2,7 +2,7 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) The Exim Maintainers 2020 - 2023 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
 /* SPDX-License-Identifier: GPL-2.0-or-later */
@@ -5566,69 +5566,14 @@ Limit to about 1024 chars total. */
 static void
 dsn_put_wrapped(FILE * fp, const uschar * header, const uschar * s)
 {
-const uschar * t;
-int llen = fprintf(fp, "%s", CS header), sleft = Ustrlen(s);
-int remain = 1022 - llen;
+gstring * g = string_cat(NULL, header);
 
-if (*s && remain > 0)
-  {
-  for(;;)
-    {
-    unsigned ltail;	/* source chars to skip */
-
-    /* Chop at a newline, or end of string */
-
-    if ((t = Ustrchr(s, '\\')) && t[1] == 'n')
-      ltail = 2;
-    else if ((t = Ustrchr(s, '\n')))
-      ltail = 1;
-    else
-      {
-      t = s + sleft;
-      ltail = 0;
-      }
-
-    /* If that is too long, search backward for a space */
-
-    if ((llen + t - s) > 78)
-      {
-      const uschar * u;
-      for (u = s + 78 - llen; u > s + 10; --u) if (*u == ' ') break;
-      if (u > s + 10)
-	{				/* found a space to linebreak at */
-	llen = u - s;
-	remain -= fprintf(fp, "%.*s", (int)llen, s);
-	s += ++llen;			/* skip the space also */
-	}
-      else if (llen < 78)
-	{				/* just linebreak at 78 */
-	llen = 78 - llen;
-	remain -= fprintf(fp, "%.*s", llen, s);
-	s += llen;
-	}
-      else				/* header rather long */
-	llen = 0;
-      }
-    else
-      {
-      llen = t - s;
-      remain -= fprintf(fp, "%.*s", llen, s);
-      s = t + ltail;
-      }
+g = string_cat(g, s);
+gstring_release_unused(g);
+fprintf(fp, "%s\n", wrap_header(string_from_gstring(g), 79, 1023, US" ", 1));
+}
 
-    sleft -= llen;
-    remain -= 2;
-    if (!*s || remain <= 0)
-      break;
-    fputs("\n ", fp);
-    llen = 1;		/* one for the leading space output above */
-    }
 
-  if (s[-1] != '\n') fputs("\n", fp);
-  }
-else
-  fputs("\n", fp);
-}
 
 
 /*************************************************
diff --git a/src/src/expand.c b/src/src/expand.c
index baf7134cd..6dcd45062 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -2,7 +2,7 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) The Exim Maintainers 2020 - 2023 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
 /* SPDX-License-Identifier: GPL-2.0-or-later */
@@ -237,6 +237,7 @@ static uschar *op_table_main[] = {
   US"expand",
   US"h",
   US"hash",
+  US"headerwrap",
   US"hex2b64",
   US"hexquote",
   US"ipv6denorm",
@@ -284,6 +285,7 @@ enum {
   EOP_EXPAND,
   EOP_H,
   EOP_HASH,
+  EOP_HEADERWRAP,
   EOP_HEX2B64,
   EOP_HEXQUOTE,
   EOP_IPV6DENORM,
@@ -7262,7 +7264,7 @@ NOT_ITEM: ;
 	{
 	uschar *t;
 	unsigned long int n = Ustrtoul(sub, &t, 10);
-	if (*t != 0)
+	if (*t)
 	  {
 	  expand_string_message = string_sprintf("argument for base62 "
 	    "operator is \"%s\", which is not a decimal number", sub);
@@ -7278,7 +7280,7 @@ NOT_ITEM: ;
 	{
 	uschar *tt = sub;
 	unsigned long int n = 0;
-	while (*tt != 0)
+	while (*tt)
 	  {
 	  uschar *t = Ustrchr(base62_chars, *tt++);
 	  if (!t)
@@ -7428,6 +7430,29 @@ NOT_ITEM: ;
 	goto EXPAND_FAILED;
 #endif
 
+      /* Line-wrap a string as if it is a header line */
+
+      case EOP_HEADERWRAP:
+	{
+	unsigned col = 80, lim = 998;
+	uschar * s;
+
+	if (arg)
+	  {
+	  const uschar * list = arg;
+	  int sep = '_';
+	  if ((s = string_nextinlist(&list, &sep, NULL, 0)))
+	    {
+	    col = atoi(CS s);
+	    if ((s = string_nextinlist(&list, &sep, NULL, 0)))
+	      lim = atoi(CS s);
+	    }
+	  }
+	  if ((s =  wrap_header(sub, col, lim, US"\t", 8)))
+	    yield = string_cat(yield, s);
+	}
+	break;
+
       /* Convert hex encoding to base64 encoding */
 
       case EOP_HEX2B64:
diff --git a/src/src/functions.h b/src/src/functions.h
index 37f0a57bc..5fbb426ec 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -2,7 +2,7 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) The Exim Maintainers 2020 - 2023 */
 /* Copyright (c) University of Cambridge 1995 - 2018 */
 /* See the file NOTICE for conditions of use and distribution. */
 /* SPDX-License-Identifier: GPL-2.0-or-later */
@@ -678,6 +678,7 @@ extern void    version_init(void);
 
 extern BOOL    write_chunk(transport_ctx *, uschar *, int);
 extern ssize_t write_to_fd_buf(int, const uschar *, size_t);
+extern uschar *wrap_header(const uschar *, unsigned, unsigned, const uschar *, unsigned);
 
 
 /******************************************************************************/
diff --git a/src/src/header.c b/src/src/header.c
index a4dd6e72e..e2b3d8a9c 100644
--- a/src/src/header.c
+++ b/src/src/header.c
@@ -2,7 +2,7 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) The Exim Maintainers 2020 - 2023 */
 /* Copyright (c) University of Cambridge 1995 - 2016 */
 /* See the file NOTICE for conditions of use and distribution. */
 /* SPDX-License-Identifier: GPL-2.0-or-later */
@@ -466,4 +466,85 @@ va_end(ap);
 return !cond;
 }
 
+
+
+/* Wrap and truncate a string for use as a header.
+Convert either the sequence "\n" or a real newline into newline plus indent.
+If that still takes us past the column limit, look for the last space
+and split there too.
+Limit to the given max total char count.
+
+Return: string or NULL */
+
+uschar *
+wrap_header(const uschar * s, unsigned cols, unsigned maxchars,
+  const uschar * indent, unsigned indent_cols)
+{
+gstring * g = NULL;
+
+if (maxchars == 0) maxchars = INT_MAX;
+if (cols == 0) cols = INT_MAX;
+
+if (s && *s)
+  {
+  int sleft = Ustrlen(s);
+  for(unsigned llen = 0; ; llen = indent_cols)
+    {
+    const uschar * t;
+    unsigned ltail = 0, glen;
+
+    if ((t = Ustrchr(s, '\\')) && t[1] == 'n')
+      ltail = 2;
+    else if ((t = Ustrchr(s, '\n')))
+      ltail = 1;
+    else
+      t = s + sleft;
+
+    if ((llen + t - s) > cols)		/* more than a linesworth of s */
+      {					/* look backward for whitespace */
+      for (const uschar * u = s + cols - llen; u > s + 10; --u) if (isspace(*u))
+	{
+	llen = u - s;
+	while (u > s+1 && isspace(u[-1])) --u;	/* find start of whitespace */
+	g = string_catn(g, s, u - s);
+	s += ++llen;				/* skip the space */
+	while (*s && isspace(*s))		/* and any trailing */
+	  s++, llen++;
+	goto LDONE;
+	}
+					/* no whitespace */
+      if (llen < cols)
+	{					/* just linebreak at 80 */
+	llen = cols - llen;
+	g = string_catn(g, s, llen);
+	s += llen;
+	}
+      else
+        llen = 0;
+      LDONE:
+      }
+    else				/* rest of s fits in line */
+      {
+      llen = t - s;
+      g = string_catn(g, s, llen);
+      s = t + ltail;
+      }
+
+    if (!*s)
+      break;				/* no trailing linebreak */
+    if ((glen = gstring_length(g)) >= maxchars)
+      {
+      gstring_trim(g, glen - maxchars);
+      break;				/* no trailing linebreak */
+      }
+    sleft -= llen;
+    g = string_catn(g, US"\n", 1);
+    g = string_catn(g, indent, 1);
+    }
+  }
+gstring_release_unused(g);
+return string_from_gstring(g);
+}
+
+
 /* End of header.c */

commit ecc7ae95e36be550fa4b47de2d8dfc7115eac9cb
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Mar 11 17:57:29 2023 +0000

    Compiler quietening
    
    Broken-by: a6d90e094d4e

diff --git a/src/src/header.c b/src/src/header.c
index e2b3d8a9c..d5f1dcd6b 100644
--- a/src/src/header.c
+++ b/src/src/header.c
@@ -521,7 +521,7 @@ if (s && *s)
 	}
       else
         llen = 0;
-      LDONE:
+      LDONE: ;
       }
     else				/* rest of s fits in line */
       {

commit 1d904e0470fed2e5c7a867f63d39ee44dbe80a2a
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Mar 12 19:02:31 2023 +0000

    Compiler quietening.  Bug 2983

diff --git a/src/src/local_scan.h b/src/src/local_scan.h
index 69b3c6cdb..c88994442 100644
--- a/src/src/local_scan.h
+++ b/src/src/local_scan.h
@@ -133,7 +133,7 @@ typedef struct {
   union {
     void *	value;
     long	offset;
-    void (*	fn)();
+    void (*	fn)(const uschar *, const uschar *, unsigned);
   } v;
 } optionlist;
 #define OPT_OFF(s, field) {.offset = offsetof(s, field)}
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 6dba11ca1..3b26e87d5 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -2356,11 +2356,8 @@ switch (type)
     }
 
   case opt_func:
-    {
-    void (*fn)() = ol->v.fn;
-    fn(name, s, 0);
+    ol->v.fn(name, s, 0);
     break;
-    }
   }
 
 return TRUE;
@@ -2384,11 +2381,7 @@ readconf_printtime(int t)
 int s, m, h, d, w;
 uschar *p = time_buffer;
 
-if (t < 0)
-  {
-  *p++ = '-';
-  t = -t;
-  }
+if (t < 0) *p++ = '-', t = -t;
 
 s = t % 60;
 t /= 60;

commit 8ff2ba119ba654e9238f157f94bf10ed640ed877
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Mar 12 20:57:40 2023 +0000

    Cmdine option for only IDs of queue

diff --git a/src/src/exim.c b/src/src/exim.c
index 8d13bd478..c16beb1af 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1745,7 +1745,7 @@ int  filter_sfd = -1;
 int  filter_ufd = -1;
 int  group_count;
 int  i, rv;
-int  list_queue_option = 0;
+int  list_queue_option = QL_BASIC;
 int  msg_action = 0;
 int  msg_action_arg = -1;
 int  namelen = argv[0] ? Ustrlen(argv[0]) : 0;
@@ -2388,11 +2388,9 @@ on the second character (the one after '-'), to save some effort. */
 	    }
 
 	  if (*argrest == 'r')
-	    {
-	    list_queue_option = 8;
-	    argrest++;
-	    }
-	  else list_queue_option = 0;
+	    list_queue_option = QL_UNSORTED, argrest++;
+	  else
+	    list_queue_option = QL_BASIC;
 
 	  list_queue = TRUE;
 
@@ -2402,11 +2400,15 @@ on the second character (the one after '-'), to save some effort. */
 
 	  /* -bpu: List the contents of the mail queue, top-level undelivered */
 
-	  else if (Ustrcmp(argrest, "u") == 0) list_queue_option += 1;
+	  else if (Ustrcmp(argrest, "u") == 0) list_queue_option |= QL_UNDELIVERED_ONLY;
 
 	  /* -bpa: List the contents of the mail queue, including all delivered */
 
-	  else if (Ustrcmp(argrest, "a") == 0) list_queue_option += 2;
+	  else if (Ustrcmp(argrest, "a") == 0) list_queue_option |= QL_PLUS_GENERATED;
+
+	  /* -bpi: List only message IDs */
+
+	  else if (Ustrcmp(argrest, "i") == 0) list_queue_option |= QL_MSGID_ONLY;
 
 	  /* Unknown after -bp[r] */
 
diff --git a/src/src/macros.h b/src/src/macros.h
index 3b0293b97..9f3a7b06a 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -1140,4 +1140,11 @@ typedef unsigned mcs_flags;
 /* A big number for (effectively) unlimited envelope addresses */
 #define UNLIMITED_ADDRS		999999
 
+/* Flags for queue_list() */
+#define QL_BASIC		0
+#define QL_UNDELIVERED_ONLY	1
+#define QL_PLUS_GENERATED	2
+#define QL_MSGID_ONLY		3
+#define QL_UNSORTED		8
+
 /* End of macros.h */
diff --git a/src/src/queue.c b/src/src/queue.c
index d01cde655..b6e7907d7 100644
--- a/src/src/queue.c
+++ b/src/src/queue.c
@@ -913,7 +913,7 @@ Returns:      nothing
 */
 
 void
-queue_list(int option, uschar **list, int count)
+queue_list(int option, uschar ** list, int count)
 {
 int subcount;
 int now = (int)time(NULL);
@@ -942,21 +942,25 @@ if (count > 0)
 
 else
   qf = queue_get_spool_list(
-          -1,             /* entire queue */
-          subdirs,        /* for holding sub list */
-          &subcount,      /* for subcount */
-          option >= 8,	  /* randomize if required */
-	  NULL);	  /* don't just count */
+          -1,				/* entire queue */
+          subdirs,			/* for holding sub list */
+          &subcount,			/* for subcount */
+          option >= QL_UNSORTED,	/* randomize if required */
+	  NULL);			/* don't just count */
 
-if (option >= 8) option -= 8;
+option &= ~QL_UNSORTED;
 
 /* Now scan the chain and print information, resetting store used
 each time. */
 
-for (;
-    qf && (reset_point = store_mark());
-    spool_clear_header_globals(), store_reset(reset_point), qf = qf->next
-    )
+if (option == QL_MSGID_ONLY)	/* Print only the message IDs from the chain */
+  for (; qf; qf = qf->next)
+    fprintf(stdout, "%.*s\n", MESSAGE_ID_LENGTH, qf->text);
+
+else for (;
+	  qf && (reset_point = store_mark());
+	  spool_clear_header_globals(), store_reset(reset_point), qf = qf->next
+	 )
   {
   int rc, save_errno;
   int size = 0;
@@ -1010,8 +1014,8 @@ for (;
       }
     }
 
-  fprintf(stdout, "%s ", string_format_size(size, big_buffer));
-  for (int i = 0; i < 16; i++) fputc(qf->text[i], stdout);
+  fprintf(stdout, "%s %.*s",
+    string_format_size(size, big_buffer), MESSAGE_ID_LENGTH, qf->text);
 
   if (env_read && sender_address)
     {
@@ -1048,14 +1052,14 @@ for (;
     {
     for (int i = 0; i < recipients_count; i++)
       {
-      tree_node *delivered =
+      tree_node * delivered =
         tree_search(tree_nonrecipients, recipients_list[i].address);
-      if (!delivered || option != 1)
+      if (!delivered || option != QL_UNDELIVERED_ONLY)
         printf("        %s %s\n",
 	  delivered ? "D" : " ", recipients_list[i].address);
       if (delivered) delivered->data.val = TRUE;
       }
-    if (option == 2 && tree_nonrecipients)
+    if (option == QL_PLUS_GENERATED && tree_nonrecipients)
       queue_list_extras(tree_nonrecipients);
     printf("\n");
     }

commit 6fdf76d0eae42ce4507fe317f095572100c5d6b8
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Mar 13 00:43:01 2023 +0000

    SNI for ${readsocket }

diff --git a/src/src/functions.h b/src/src/functions.h
index 5fbb426ec..896122a69 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -54,6 +54,8 @@ extern uschar * tls_cert_fprt_sha256(void *);
 extern void    tls_clean_env(void);
 extern BOOL    tls_client_start(client_conn_ctx *, smtp_connect_args *,
 		  void *, tls_support *, uschar **);
+extern BOOL    tls_client_adjunct_start(host_item *, client_conn_ctx *,
+		  const uschar *, uschar **);
 extern void    tls_client_creds_reload(BOOL);
 
 extern void    tls_close(void *, int);
diff --git a/src/src/lookups/readsock.c b/src/src/lookups/readsock.c
index b1ea42c7f..a3f87108a 100644
--- a/src/src/lookups/readsock.c
+++ b/src/src/lookups/readsock.c
@@ -13,7 +13,7 @@
 
 static int
 internal_readsock_open(client_conn_ctx * cctx, const uschar * sspec,
-  int timeout, BOOL do_tls, uschar ** errmsg)
+  int timeout, uschar * do_tls, uschar ** errmsg)
 {
 const uschar * server_name;
 host_item host;
@@ -116,27 +116,8 @@ else
 
 #ifndef DISABLE_TLS
 if (do_tls)
-  {
-  union sockaddr_46 interface_sock;
-  EXIM_SOCKLEN_T size = sizeof(interface_sock);
-  smtp_connect_args conn_args = {.host = &host };
-  tls_support tls_dummy = { .sni = NULL };
-  uschar * errstr;
-
-  if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0)
-    conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL);
-  else
-    {
-    *errmsg = string_sprintf("getsockname failed: %s", strerror(errno));
+  if (!tls_client_adjunct_start(&host, cctx, do_tls, errmsg))
     goto bad;
-    }
-
-  if (!tls_client_start(cctx, &conn_args, NULL, &tls_dummy, &errstr))
-    {
-    *errmsg = string_sprintf("TLS connect failed: %s", errstr);
-    goto bad;
-    }
-  }
 #endif
 
 DEBUG(D_expand|D_lookup) debug_printf_indent("  connected to socket %s\n", sspec);
@@ -187,8 +168,8 @@ client_conn_ctx * cctx = handle;
 int sep = ',';
 struct {
 	BOOL do_shutdown:1;
-	BOOL do_tls:1;
 	BOOL cache:1;
+	uschar * do_tls;	/* NULL, empty-string, or SNI */
 } lf = {.do_shutdown = TRUE};
 uschar * eol = NULL;
 int timeout = 5;
@@ -207,8 +188,10 @@ if (opts) for (uschar * s; s = string_nextinlist(&opts, &sep, NULL, 0); )
   else if (Ustrncmp(s, "shutdown=", 9) == 0)
     lf.do_shutdown = Ustrcmp(s + 9, "no") != 0;
 #ifndef DISABLE_TLS
-  else if (Ustrncmp(s, "tls=", 4) == 0 && Ustrcmp(s + 4, US"no") != 0)
-    lf.do_tls = TRUE;
+  else if (Ustrncmp(s, "tls=", 4) == 0 && Ustrcmp(s + 4, US"no") != 0 && !lf.do_tls)
+    lf.do_tls = US"";
+  else if (Ustrncmp(s, "sni=", 4) == 0)
+    lf.do_tls = s + 4;
 #endif
   else if (Ustrncmp(s, "eol=", 4) == 0)
     eol = string_unprinting(s + 4);
diff --git a/src/src/tls.c b/src/src/tls.c
index ba7c2de38..825313a9a 100644
--- a/src/src/tls.c
+++ b/src/src/tls.c
@@ -852,6 +852,57 @@ DEBUG(D_tls) debug_printf("TLS: resume session index %s\n", tlsp->resume_index);
 #endif
 }
 
+
+
+/* Start TLS as a client for an ajunct connection, eg. readsocket
+Return boolean success.
+*/
+
+BOOL
+tls_client_adjunct_start(host_item * host, client_conn_ctx * cctx,
+  const uschar * sni, uschar ** errmsg)
+{
+union sockaddr_46 interface_sock;
+EXIM_SOCKLEN_T size = sizeof(interface_sock);
+smtp_connect_args conn_args = {.host = host };
+tls_support tls_dummy = { .sni = NULL };
+uschar * errstr;
+
+if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0)
+  conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL);
+else
+  {
+  *errmsg = string_sprintf("getsockname failed: %s", strerror(errno));
+  return FALSE;
+  }
+
+/* To handle SNI we need to emulate more of a real transport because the
+base tls code assumes that is where the SNI string lives. */
+
+if (*sni)
+  {
+  transport_instance * tb;
+  smtp_transport_options_block * ob;
+
+  conn_args.tblock = tb = store_get(sizeof(*tb), GET_UNTAINTED);
+  memset(tb, 0, sizeof(*tb));
+
+  tb->options_block = ob = store_get(sizeof(*ob), GET_UNTAINTED);
+  memcpy(ob, &smtp_transport_option_defaults, sizeof(*ob));
+
+  ob->tls_sni = sni;
+  }
+
+if (!tls_client_start(cctx, &conn_args, NULL, &tls_dummy, &errstr))
+  {
+  *errmsg = string_sprintf("TLS connect failed: %s", errstr);
+  return FALSE;
+  }
+return TRUE;
+}
+
+
+
 #endif	/*!DISABLE_TLS*/
 #endif	/*!MACRO_PREDEF*/
 

commit 7ce1ced40351c3cd5982d37ff4ccdb02afd82365
Author: Andrew Aitchison <exim@aitchison.me.uk>
Date:   Mon Mar 13 15:59:23 2023 +0000

    Compiler quietening.  Bug 2983

diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 10b5f2aa5..9d0ab2fdf 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -1001,7 +1001,7 @@ Returns:    nothing
 */
 
 static void
-info_callback(SSL * s, int where, int ret)
+info_callback(const SSL * s, int where, int ret)
 {
 DEBUG(D_tls)
   {
@@ -1750,13 +1750,13 @@ level. */
 
 DEBUG(D_tls)
   {
-  SSL_CTX_set_info_callback(ctx, (void (*)())info_callback);
+  SSL_CTX_set_info_callback(ctx, info_callback);
 #if defined(EXIM_HAVE_OPESSL_TRACE) && !defined(OPENSSL_NO_SSL_TRACE)
   /* this needs a debug build of OpenSSL */
-  SSL_CTX_set_msg_callback(ctx, (void (*)())SSL_trace);
+  SSL_CTX_set_msg_callback(ctx, SSL_trace);
 #endif
 #ifdef OPENSSL_HAVE_KEYLOG_CB
-  SSL_CTX_set_keylog_callback(ctx, (void (*)())keylog_callback);
+  SSL_CTX_set_keylog_callback(ctx, keylog_callback);
 #endif
   }
 

commit 3607e3e00236f6039b765882edd0200dff6a31fc
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Mar 16 15:49:59 2023 +0000

    Limit expanded References header to RFC max size.  Bug 2827

diff --git a/src/src/deliver.c b/src/src/deliver.c
index e2994b116..57a435eeb 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -6178,6 +6178,7 @@ return_path = sender_address;   /* In case not previously set */
 
 /* Write the original email out */
 /*XXX no checking for failure!  buggy! */
+/*XXX overlong headers in the original become overlong body lines here*/
 transport_write_message(&tctx, 0);
 fflush(f);
 
diff --git a/src/src/moan.c b/src/src/moan.c
index ebfd440f6..9c30c8edd 100644
--- a/src/src/moan.c
+++ b/src/src/moan.c
@@ -80,11 +80,17 @@ if (!h)
 /* We limit the total length of references.  Although there is no fixed
 limit, some systems do not like headers growing beyond recognition.
 Keep the first message ID for the thread root and the last few for
-the position inside the thread, up to a maximum of 12 altogether. */
+the position inside the thread, up to a maximum of 12 altogether.
+Also apply the max line length limit from RFC 2822 2.1.1
+
+XXX preferably we would get any limit from the outbound transport,
+passed in here for a limit value.
+*/
 
 if (h || message_id)
   {
-  fprintf(fp, "References:");
+  unsigned use = fprintf(fp, "References:");
+  if (message_id) use += Ustrlen(message_id) + 1;
   if (h)
     {
     const uschar * s;
@@ -95,14 +101,27 @@ if (h || message_id)
     s = Ustrchr(h->text, ':') + 1;
     f.parse_allow_group = FALSE;
     while (*s && (s = parse_message_id(s, &id, &error)))
-      if (reference_count == nelem(referenced_ids))
-        {
-        memmove(referenced_ids + 1, referenced_ids + 2,
-           sizeof(referenced_ids) - 2*sizeof(uschar *));
-        referenced_ids[reference_count - 1] = id;
-        }
+      {
+      unsigned this = Ustrlen(id);
+      if (  reference_count == nelem(referenced_ids)
+	 || use + this + reference_count > 998
+         )
+	{
+	if (reference_count > 1)
+	  {
+	  /* drop position 1 and shuffle down */
+	  use -= Ustrlen(referenced_ids + 1);
+	  memmove(referenced_ids + 1, referenced_ids + 2,
+	     sizeof(referenced_ids) - 2*sizeof(*referenced_ids));
+
+	  /* append new one */
+	  referenced_ids[reference_count - 1] = id;
+	  }
+	}
       else
 	referenced_ids[reference_count++] = id;
+      use += this;
+      }
 
     for (int i = 0; i < reference_count; ++i)
       fprintf(fp, " %s", referenced_ids[i]);

commit 35d78f064b4e9f3ec28481e5e842c33a68171721
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Mar 16 19:35:48 2023 +0000

    Fix long headers going into DSN bodies.  Bug 1760

diff --git a/src/src/deliver.c b/src/src/deliver.c
index 57a435eeb..f3a406990 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -5954,7 +5954,7 @@ wording. */
 
     tctx.u.fd = fileno(fp);
     tctx.tblock = &tb;
-    tctx.options = topt;
+    tctx.options = topt | topt_truncate_headers;
     tb.add_headers = dsnnotifyhdr;
 
     /*XXX no checking for failure!  buggy! */
@@ -6172,13 +6172,12 @@ fprintf(f, "--%s\n"
 fflush(f);
 /* header only as required by RFC. only failure DSN needs to honor RET=FULL */
 tctx.u.fd = fileno(f);
-tctx.options = topt_add_return_path | topt_no_body;
+tctx.options = topt_add_return_path | topt_truncate_headers | topt_no_body;
 transport_filter_argv = NULL;   /* Just in case */
 return_path = sender_address;   /* In case not previously set */
 
 /* Write the original email out */
 /*XXX no checking for failure!  buggy! */
-/*XXX overlong headers in the original become overlong body lines here*/
 transport_write_message(&tctx, 0);
 fflush(f);
 
@@ -6334,7 +6333,7 @@ if (addr_senddsn)
     /* Write the original email out */
 
     tctx.u.fd = fd;
-    tctx.options = topt_add_return_path | topt_no_body;
+    tctx.options = topt_add_return_path | topt_truncate_headers | topt_no_body;
     /*XXX hmm, FALSE(fail) retval ignored.
     Could error for any number of reasons, and they are not handled. */
     transport_write_message(&tctx, 0);
diff --git a/src/src/macros.h b/src/src/macros.h
index 9f3a7b06a..73c6ac2c6 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -868,19 +868,20 @@ enum {
 
 /* Options for transport_write_message */
 
-#define topt_add_return_path    0x0001
-#define topt_add_delivery_date  0x0002
-#define topt_add_envelope_to    0x0004
-#define topt_escape_headers     0x0008	/* Apply escape check to headers */
-#define topt_use_crlf           0x0010	/* Terminate lines with CRLF */
-#define topt_no_headers         0x0020	/* Omit headers */
-#define topt_no_body            0x0040	/* Omit body */
-#define topt_end_dot            0x0080	/* Send terminating dot line */
-#define topt_no_flush		0x0100	/* more data expected after message (eg QUIT) */
-#define topt_use_bdat		0x0200	/* prepend chunks with RFC3030 BDAT header */
-#define topt_output_string	0x0400	/* create string rather than write to fd */
-#define topt_continuation	0x0800	/* do not reset buffer */
-#define topt_not_socket		0x1000	/* cannot do socket-only syscalls */
+#define topt_add_return_path    BIT(0)
+#define topt_add_delivery_date  BIT(1)
+#define topt_add_envelope_to    BIT(2)
+#define topt_escape_headers     BIT(3)	/* Apply escape check to headers */
+#define topt_truncate_headers   BIT(4)	/* Truncate header lines at 998 chars */
+#define topt_use_crlf           BIT(5)	/* Terminate lines with CRLF */
+#define topt_no_headers         BIT(6)	/* Omit headers */
+#define topt_no_body            BIT(7)	/* Omit body */
+#define topt_end_dot            BIT(8)	/* Send terminating dot line */
+#define topt_no_flush		BIT(9)	/* more data expected after message (eg QUIT) */
+#define topt_use_bdat		BIT(10)	/* prepend chunks with RFC3030 BDAT header */
+#define topt_output_string	BIT(11)	/* create string rather than write to fd */
+#define topt_continuation	BIT(12)	/* do not reset buffer */
+#define topt_not_socket		BIT(13)	/* cannot do socket-only syscalls */
 
 /* Options for smtp_write_command */
 
diff --git a/src/src/transport.c b/src/src/transport.c
index d6cedf911..80ba1eece 100644
--- a/src/src/transport.c
+++ b/src/src/transport.c
@@ -706,7 +706,7 @@ BOOL
 transport_headers_send(transport_ctx * tctx,
   BOOL (*sendfn)(transport_ctx * tctx, uschar * s, int len))
 {
-const uschar *list;
+const uschar * list;
 transport_instance * tblock = tctx ? tctx->tblock : NULL;
 address_item * addr = tctx ? tctx->addr : NULL;
 
@@ -761,15 +761,18 @@ for (header_line * h = header_list; h; h = h->next) if (h->type != htype_old)
 
   if (include_header)
     {
+    int len;
     if (tblock && tblock->rewrite_rules)
       {
       rmark reset_point = store_mark();
-      header_line *hh;
+      header_line * hh;
 
       if ((hh = rewrite_header(h, NULL, NULL, tblock->rewrite_rules,
 		  tblock->rewrite_existflags, FALSE)))
 	{
-	if (!sendfn(tctx, hh->text, hh->slen)) return FALSE;
+	len = hh->slen;
+	if (tctx->options & topt_truncate_headers && len > 998) len = 998;
+	if (!sendfn(tctx, hh->text, len)) return FALSE;
 	store_reset(reset_point);
 	continue;     /* With the next header line */
 	}
@@ -777,7 +780,9 @@ for (header_line * h = header_list; h; h = h->next) if (h->type != htype_old)
 
     /* Either no rewriting rules, or it didn't get rewritten */
 
-    if (!sendfn(tctx, h->text, h->slen)) return FALSE;
+    len = h->slen;
+    if (tctx->options & topt_truncate_headers && len > 998) len = 998;
+    if (!sendfn(tctx, h->text, len)) return FALSE;
     }
 
   /* Header removed */

commit 1745d310db8102be38db65401df57747e9beaf6a
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Mar 17 18:39:46 2023 +0000

    ACL: patterns for remove_headers.  Bug 2985

diff --git a/src/src/header.c b/src/src/header.c
index d5f1dcd6b..59a9a13b3 100644
--- a/src/src/header.c
+++ b/src/src/header.c
@@ -30,11 +30,12 @@ Returns:    TRUE or FALSE
 */
 
 BOOL
-header_testname(header_line *h, const uschar *name, int len, BOOL notdel)
+header_testname(const header_line * h, const uschar * name, int len,
+  BOOL notdel)
 {
 uschar *tt;
 if (h->type == '*' && notdel) return FALSE;
-if (h->text == NULL || strncmpic(h->text, name, len) != 0) return FALSE;
+if (!h->text || strncmpic(h->text, name, len) != 0) return FALSE;
 tt = h->text + len;
 while (*tt == ' ' || *tt == '\t') tt++;
 return *tt == ':';
@@ -46,11 +47,11 @@ return *tt == ':';
    header_testname() above. */
 
 BOOL
-header_testname_incomplete(header_line *h, const uschar *name,
+header_testname_incomplete(const header_line * h, const uschar * name,
     int len, BOOL notdel)
 {
 if (h->type == '*' && notdel) return FALSE;
-if (h->text == NULL || strncmpic(h->text, name, len) != 0) return FALSE;
+if (!h->text || strncmpic(h->text, name, len) != 0) return FALSE;
 return TRUE;
 }
 
diff --git a/src/src/local_scan.h b/src/src/local_scan.h
index c88994442..72f2ac47d 100644
--- a/src/src/local_scan.h
+++ b/src/src/local_scan.h
@@ -188,8 +188,8 @@ extern uschar *expand_string(uschar *);
 extern void    header_add(int, const char *, ...);
 extern void    header_add_at_position(BOOL, uschar *, BOOL, int, const char *, ...);
 extern void    header_remove(int, const uschar *);
-extern BOOL    header_testname(header_line *, const uschar *, int, BOOL);
-extern BOOL    header_testname_incomplete(header_line *, const uschar *, int, BOOL);
+extern BOOL    header_testname(const header_line *, const uschar *, int, BOOL);
+extern BOOL    header_testname_incomplete(const header_line *, const uschar *, int, BOOL);
 extern void    log_write(unsigned int, int, const char *format, ...) PRINTF_FUNCTION(3,4);
 extern int     lss_b64decode(uschar *, uschar **);
 extern uschar *lss_b64encode(uschar *, int);
diff --git a/src/src/receive.c b/src/src/receive.c
index 77665d89f..94fa6d5de 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -1230,9 +1230,9 @@ Returns:     nothing
 */
 
 static void
-add_acl_headers(int where, uschar *acl_name)
+add_acl_headers(int where, uschar * acl_name)
 {
-header_line *last_received = NULL;
+header_line * last_received = NULL;
 
 switch(where)
   {
@@ -1254,15 +1254,22 @@ if (acl_removed_headers)
 
   for (header_line * h = header_list; h; h = h->next) if (h->type != htype_old)
     {
-    const uschar * list = acl_removed_headers;
+    const uschar * list = acl_removed_headers, * s;
     int sep = ':';         /* This is specified as a colon-separated list */
-    uschar *s;
 
+    /* If a list element has a leading '^' then it is an RE for
+    the whole header, else just a header name. */
     while ((s = string_nextinlist(&list, &sep, NULL, 0)))
-      if (header_testname(h, s, Ustrlen(s), FALSE))
+      if (  (  *s == '^'
+	    && regex_match(
+		regex_must_compile(s, MCS_CACHEABLE, FALSE),
+		h->text, h->slen, NULL)
+            )
+	 || header_testname(h, s, Ustrlen(s), FALSE)
+	 )
 	{
 	h->type = htype_old;
-        DEBUG(D_receive|D_acl) debug_printf_indent("  %s", h->text);
+	DEBUG(D_receive|D_acl) debug_printf_indent("  %s", h->text);
 	}
     }
   acl_removed_headers = NULL;

commit 3e6d406e8ae9681a8cc1b404e7f5d1bd6d65d201
Author: Andrew Aitchison <exim@aitchison.me.uk>
Date:   Sat Mar 18 15:36:21 2023 +0000

    Docs: tweak standards status of port 465

diff --git a/src/src/configure.default b/src/src/configure.default
index 3761dafbf..83380bdaa 100644
--- a/src/src/configure.default
+++ b/src/src/configure.default
@@ -183,11 +183,15 @@ tls_resumption_hosts = ${if inlist {$received_port}{587:465} {:}{*}}
 # In order to support roaming users who wish to send email from anywhere,
 # you may want to make Exim listen on other ports as well as port 25, in
 # case these users need to send email from a network that blocks port 25.
-# The standard port for this purpose is port 587, the "message submission"
-# port. See RFC 4409 for details. Microsoft MUAs cannot be configured to
+# The standard ports for this purpose are:
+# port 587, the "message submission" port - see RFC 4409 for details,
+# and 465 the TLS-encrypted "submission" port, service name is "submissions",
+# see RFC 8314.
+
+# Microsoft MUAs cannot be configured to
 # talk the message submission protocol correctly, so if you need to support
-# them you should also allow TLS-on-connect on the traditional but
-# non-standard port 465.
+# them you should also allow TLS-on-connect on the traditional (and now
+# standard) port 465.
 
 # daemon_smtp_ports = 25 : 465 : 587
 # tls_on_connect_ports = 465

commit e08a679856effe96c0099d3516e6edec1f5616d6
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Mar 19 20:01:53 2023 +0000

    Tidying

diff --git a/src/src/macros.h b/src/src/macros.h
index 73c6ac2c6..36ed185ed 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -817,9 +817,7 @@ local_scan.h */
 #define DEBUG_FROM_CONFIG       0x0001
 
 /* SMTP command identifiers for the smtp_connection_had field that records the
-most recent SMTP commands. Must be kept in step with the list of names in
-smtp_in.c that is used for creating the smtp_no_mail logging action. SCH_NONE
-is "empty". */
+most recent SMTP commands. SCH_NONE is "empty". */
 
 enum { SCH_NONE, SCH_AUTH, SCH_DATA, SCH_BDAT,
        SCH_EHLO, SCH_ETRN, SCH_EXPN, SCH_HELO,
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index a13af867a..5b2df7805 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -189,16 +189,22 @@ count of non-mail commands and possibly provoke an error.
 tls_auth is a pseudo-command, never expected in input.  It is activated
 on TLS startup and looks for a tls authenticator. */
 
+enum {	CL_RSET, CL_HELO, CL_EHLO, CL_AUTH,
+#ifndef DISABLE_TLS
+	CL_STLS, CL_TLAU,
+#endif
+};
+
 static smtp_cmd_list cmd_list[] = {
   /* name         len                     cmd     has_arg is_mail_cmd */
 
-  { "rset",       sizeof("rset")-1,       RSET_CMD, FALSE, FALSE },  /* First */
-  { "helo",       sizeof("helo")-1,       HELO_CMD, TRUE,  FALSE },
-  { "ehlo",       sizeof("ehlo")-1,       EHLO_CMD, TRUE,  FALSE },
-  { "auth",       sizeof("auth")-1,       AUTH_CMD, TRUE,  TRUE  },
+  [CL_RSET] = { "rset",       sizeof("rset")-1,       RSET_CMD,	FALSE, FALSE },  /* First */
+  [CL_HELO] = { "helo",       sizeof("helo")-1,       HELO_CMD, TRUE,  FALSE },
+  [CL_EHLO] = { "ehlo",       sizeof("ehlo")-1,       EHLO_CMD, TRUE,  FALSE },
+  [CL_AUTH] = { "auth",       sizeof("auth")-1,       AUTH_CMD,     TRUE,  TRUE  },
 #ifndef DISABLE_TLS
-  { "starttls",   sizeof("starttls")-1,   STARTTLS_CMD, FALSE, FALSE },
-  { "tls_auth",   0,                      TLS_AUTH_CMD, FALSE, FALSE },
+  [CL_STLS] = { "starttls",   sizeof("starttls")-1,   STARTTLS_CMD, FALSE, FALSE },
+  [CL_TLAU] = { "tls_auth",   0,                      TLS_AUTH_CMD, FALSE, FALSE },
 #endif
 
 /* If you change anything above here, also fix the definitions below. */
@@ -215,24 +221,27 @@ static smtp_cmd_list cmd_list[] = {
   { "help",       sizeof("help")-1,       HELP_CMD, TRUE,  FALSE }
 };
 
-static smtp_cmd_list *cmd_list_end =
-  cmd_list + sizeof(cmd_list)/sizeof(smtp_cmd_list);
-
-#define CMD_LIST_RSET      0
-#define CMD_LIST_HELO      1
-#define CMD_LIST_EHLO      2
-#define CMD_LIST_AUTH      3
-#define CMD_LIST_STARTTLS  4
-#define CMD_LIST_TLS_AUTH  5
-
-/* This list of names is used for performing the smtp_no_mail logging action.
-It must be kept in step with the SCH_xxx enumerations. */
+/* This list of names is used for performing the smtp_no_mail logging action. */
 
 uschar * smtp_names[] =
   {
-  US"NONE", US"AUTH", US"DATA", US"BDAT", US"EHLO", US"ETRN", US"EXPN",
-  US"HELO", US"HELP", US"MAIL", US"NOOP", US"QUIT", US"RCPT", US"RSET",
-  US"STARTTLS", US"VRFY" };
+  [SCH_NONE] = US"NONE",
+  [SCH_AUTH] = US"AUTH",
+  [SCH_DATA] = US"DATA",
+  [SCH_BDAT] = US"BDAT",
+  [SCH_EHLO] = US"EHLO",
+  [SCH_ETRN] = US"ETRN",
+  [SCH_EXPN] = US"EXPN",
+  [SCH_HELO] = US"HELO",
+  [SCH_HELP] = US"HELP",
+  [SCH_MAIL] = US"MAIL",
+  [SCH_NOOP] = US"NOOP",
+  [SCH_QUIT] = US"QUIT",
+  [SCH_RCPT] = US"RCPT",
+  [SCH_RSET] = US"RSET",
+  [SCH_STARTTLS] = US"STARTTLS",
+  [SCH_VRFY] = US"VRFY",
+  };
 
 static uschar *protocols_local[] = {
   US"local-smtp",        /* HELO */
@@ -1685,7 +1694,7 @@ if (hadnull) return BADCHAR_CMD;
 to the start of the actual data characters. Check for SMTP synchronization
 if required. */
 
-for (smtp_cmd_list * p = cmd_list; p < cmd_list_end; p++)
+for (smtp_cmd_list * p = cmd_list; p < cmd_list + nelem(cmd_list); p++)
   {
 #ifdef SUPPORT_PROXY
   /* Only allow QUIT command if Proxy Protocol parsing failed */
@@ -3058,7 +3067,7 @@ if (tls_in.on_connect)
   {
   if (tls_server_start(&user_msg) != OK)
     return smtp_log_tls_fail(user_msg);
-  cmd_list[CMD_LIST_TLS_AUTH].is_mail_cmd = TRUE;
+  cmd_list[CL_TLAU].is_mail_cmd = TRUE;
   }
 #endif
 
@@ -4029,7 +4038,7 @@ smtp_rset_handler(void)
 HAD(SCH_RSET);
 incomplete_transaction_log(US"RSET");
 smtp_printf("250 Reset OK\r\n", FALSE);
-cmd_list[CMD_LIST_RSET].is_mail_cmd = FALSE;
+cmd_list[CL_RSET].is_mail_cmd = FALSE;
 if (chunking_state > CHUNKING_OFFERED)
   chunking_state = CHUNKING_OFFERED;
 }
@@ -4091,11 +4100,11 @@ message_ended = END_NOTSTARTED;
 
 chunking_state = f.chunking_offered ? CHUNKING_OFFERED : CHUNKING_NOT_OFFERED;
 
-cmd_list[CMD_LIST_RSET].is_mail_cmd = TRUE;
-cmd_list[CMD_LIST_HELO].is_mail_cmd = TRUE;
-cmd_list[CMD_LIST_EHLO].is_mail_cmd = TRUE;
+cmd_list[CL_RSET].is_mail_cmd = TRUE;
+cmd_list[CL_HELO].is_mail_cmd = TRUE;
+cmd_list[CL_EHLO].is_mail_cmd = TRUE;
 #ifndef DISABLE_TLS
-cmd_list[CMD_LIST_STARTTLS].is_mail_cmd = TRUE;
+cmd_list[CL_STLS].is_mail_cmd = TRUE;
 #endif
 
 if (lwr_receive_getc != NULL)
@@ -4151,10 +4160,10 @@ while (done <= 0)
   if (  tls_in.active.sock >= 0
      && tls_in.peercert
      && tls_in.certificate_verified
-     && cmd_list[CMD_LIST_TLS_AUTH].is_mail_cmd
+     && cmd_list[CL_TLAU].is_mail_cmd
      )
     {
-    cmd_list[CMD_LIST_TLS_AUTH].is_mail_cmd = FALSE;
+    cmd_list[CL_TLAU].is_mail_cmd = FALSE;
 
     for (auth_instance * au = auths; au; au = au->next)
       if (strcmpic(US"tls", au->driver_name) == 0)
@@ -4214,7 +4223,7 @@ while (done <= 0)
     case AUTH_CMD:
       HAD(SCH_AUTH);
       authentication_failed = TRUE;
-      cmd_list[CMD_LIST_AUTH].is_mail_cmd = FALSE;
+      cmd_list[CL_AUTH].is_mail_cmd = FALSE;
 
       if (!fl.auth_advertised && !f.allow_auth_unadvertised)
 	{
@@ -4336,8 +4345,8 @@ while (done <= 0)
       fl.esmtp = TRUE;
 
     HELO_EHLO:      /* Common code for HELO and EHLO */
-      cmd_list[CMD_LIST_HELO].is_mail_cmd = FALSE;
-      cmd_list[CMD_LIST_EHLO].is_mail_cmd = FALSE;
+      cmd_list[CL_HELO].is_mail_cmd = FALSE;
+      cmd_list[CL_EHLO].is_mail_cmd = FALSE;
 
       /* Reject the HELO if its argument was invalid or non-existent. A
       successful check causes the argument to be saved in malloc store. */
@@ -5685,7 +5694,7 @@ while (done <= 0)
       cancel_cutthrough_connection(TRUE, US"STARTTLS received");
       reset_point = smtp_reset(reset_point);
       toomany = FALSE;
-      cmd_list[CMD_LIST_STARTTLS].is_mail_cmd = FALSE;
+      cmd_list[CL_STLS].is_mail_cmd = FALSE;
 
       /* There's an attack where more data is read in past the STARTTLS command
       before TLS is negotiated, then assumed to be part of the secure session
@@ -5726,9 +5735,9 @@ while (done <= 0)
 	{
 	if (!tls_remember_esmtp)
 	  fl.helo_seen = fl.esmtp = fl.auth_advertised = f.smtp_in_pipelining_advertised = FALSE;
-	cmd_list[CMD_LIST_EHLO].is_mail_cmd = TRUE;
-	cmd_list[CMD_LIST_AUTH].is_mail_cmd = TRUE;
-	cmd_list[CMD_LIST_TLS_AUTH].is_mail_cmd = TRUE;
+	cmd_list[CL_EHLO].is_mail_cmd = TRUE;
+	cmd_list[CL_AUTH].is_mail_cmd = TRUE;
+	cmd_list[CL_TLAU].is_mail_cmd = TRUE;
 	if (sender_helo_name)
 	  {
 	  sender_helo_name = NULL;
@@ -5838,23 +5847,19 @@ while (done <= 0)
 
     case HELP_CMD:
       HAD(SCH_HELP);
-      smtp_printf("214-Commands supported:\r\n", TRUE);
-	{
-	uschar buffer[256];
-	buffer[0] = 0;
-	Ustrcat(buffer, US" AUTH");
-	#ifndef DISABLE_TLS
-	if (tls_in.active.sock < 0 &&
-	    verify_check_host(&tls_advertise_hosts) != FAIL)
-	  Ustrcat(buffer, US" STARTTLS");
-	#endif
-	Ustrcat(buffer, US" HELO EHLO MAIL RCPT DATA BDAT");
-	Ustrcat(buffer, US" NOOP QUIT RSET HELP");
-	if (acl_smtp_etrn) Ustrcat(buffer, US" ETRN");
-	if (acl_smtp_expn) Ustrcat(buffer, US" EXPN");
-	if (acl_smtp_vrfy) Ustrcat(buffer, US" VRFY");
-	smtp_printf("214%s\r\n", FALSE, buffer);
-	}
+      smtp_printf("214-Commands supported:\r\n214", TRUE);
+      smtp_printf(" AUTH", TRUE);
+#ifndef DISABLE_TLS
+      if (tls_in.active.sock < 0 &&
+	  verify_check_host(&tls_advertise_hosts) != FAIL)
+	smtp_printf(" STARTTLS", TRUE);
+#endif
+      smtp_printf(" HELO EHLO MAIL RCPT DATA BDAT", TRUE);
+      smtp_printf(" NOOP QUIT RSET HELP", TRUE);
+      if (acl_smtp_etrn) smtp_printf(" ETRN", TRUE);
+      if (acl_smtp_expn) smtp_printf(" EXPN", TRUE);
+      if (acl_smtp_vrfy) smtp_printf(" VRFY", TRUE);
+      smtp_printf("\r\n", FALSE);
       break;
 
 

commit df0dc54a7666ef64b8a6681ab7b50a4836905203
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Mar 21 20:02:18 2023 +0000

    Move Proxy-Protocol impl to separate srcfile

diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index 29c037401..d00ab9404 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -508,7 +508,7 @@ OBJ_EXIM = acl.o base64.o child.o crypt16.o daemon.o dbfn.o debug.o deliver.o \
         directory.o dns.o drtables.o enq.o exim.o expand.o filter.o \
         filtertest.o globals.o dkim.o dkim_transport.o dnsbl.o hash.o \
         header.o host.o host_address.o ip.o log.o lss.o match.o md5.o moan.o \
-        os.o parse.o priv.o queue.o \
+        os.o parse.o priv.o proxy.o queue.o \
         rda.o readconf.o receive.o retry.o rewrite.o rfc2047.o regex_cache.o \
         route.o search.o sieve.o smtp_in.o smtp_out.o spool_in.o spool_out.o \
         std-crypto.o store.o string.o tls.o tod.o transport.o tree.o verify.o \
@@ -824,6 +824,7 @@ moan.o:          $(HDRS) moan.c
 os.o:            $(HDRS) $(OS_C_INCLUDES) os.c
 parse.o:         $(HDRS) parse.c
 priv.o:          $(HDRS) priv.c
+proxy.o:         $(HDRS) proxy.c
 queue.o:         $(HDRS) queue.c
 rda.o:           $(HDRS) rda.c
 readconf.o:      $(HDRS) readconf.c
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index 6e0b65f5d..af6138063 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -104,7 +104,7 @@ for f in blob.h dbfunctions.h exim.h functions.h globals.h \
   deliver.c directory.c dns.c dnsbl.c drtables.c dummies.c enq.c exim.c \
   exim_dbmbuild.c exim_dbutil.c exim_lock.c expand.c filter.c filtertest.c \
   globals.c hash.c header.c host.c host_address.c ip.c log.c lss.c match.c md5.c moan.c \
-  parse.c perl.c priv.c queue.c rda.c readconf.c receive.c retry.c rewrite.c \
+  parse.c perl.c priv.c proxy.c queue.c rda.c readconf.c receive.c retry.c rewrite.c \
   regex_cache.c rfc2047.c route.c search.c setenv.c environment.c \
   sieve.c smtp_in.c smtp_out.c spool_in.c spool_out.c std-crypto.c store.c \
   string.c tls.c tlscert-gnu.c tlscert-openssl.c tls-cipher-stdname.c \
diff --git a/src/src/functions.h b/src/src/functions.h
index 896122a69..76392f304 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -400,9 +400,9 @@ extern const uschar *parse_quote_2047(const uschar *, int, const uschar *,
 extern const uschar *parse_date_time(const uschar *str, time_t *t);
 extern void priv_drop_temp(const uid_t, const gid_t);
 extern void priv_restore(void);
-extern int     vaguely_random_number(int);
-#ifndef DISABLE_TLS
-extern int     vaguely_random_number_fallback(int);
+#ifdef SUPPORT_PROXY
+extern BOOL	proxy_protocol_host(void);
+extern void	proxy_protocol_setup(void);
 #endif
 
 extern BOOL    queue_action(uschar *, int, uschar **, int, int);
@@ -658,6 +658,10 @@ extern void    unspool_mbox(void);
 extern gstring *utf8_version_report(gstring *);
 #endif
 
+extern int     vaguely_random_number(int);
+#ifndef DISABLE_TLS
+extern int     vaguely_random_number_fallback(int);
+#endif
 extern int     verify_address(address_item *, FILE *, int, int, int, int,
                  uschar *, uschar *, BOOL *);
 extern int     verify_check_dnsbl(int, const uschar **, uschar **);
diff --git a/src/src/globals.c b/src/src/globals.c
index c6bacc02f..539bae00e 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -387,7 +387,7 @@ BOOL    mua_wrapper            = FALSE;
 BOOL    preserve_message_logs  = FALSE;
 BOOL    print_topbitchars      = FALSE;
 BOOL    prod_requires_admin    = TRUE;
-#if defined(SUPPORT_PROXY) || defined(SUPPORT_SOCKS)
+#if defined(SUPPORT_PROXY) || defined(SUPPORT_SOCKS) || defined(EXPERIMETAL_XCLIENT)
 BOOL    proxy_session          = FALSE;
 #endif
 
diff --git a/src/src/globals.h b/src/src/globals.h
index 81d052fd5..e216b9208 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -821,7 +821,7 @@ extern uschar *process_log_path;       /* Alternate path */
 extern const uschar *process_purpose;  /* for debug output */
 extern BOOL    prod_requires_admin;    /* TRUE if prodding requires admin */
 
-#if defined(SUPPORT_PROXY) || defined(SUPPORT_SOCKS)
+#if defined(SUPPORT_PROXY) || defined(SUPPORT_SOCKS) || defined(EXPERIMENTAL_XCLIENT)
 extern uschar *hosts_proxy;            /* Hostlist which (require) use proxy protocol */
 extern uschar *proxy_external_address; /* IP of remote interface of proxy */
 extern int     proxy_external_port;    /* Port on remote interface of proxy */
diff --git a/src/src/proxy.c b/src/src/proxy.c
new file mode 100644
index 000000000..fbce11163
--- /dev/null
+++ b/src/src/proxy.c
@@ -0,0 +1,529 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2023 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/************************************************
+*            Proxy-Protocol support             *
+************************************************/
+
+#include "exim.h"
+
+#ifdef SUPPORT_PROXY
+/*************************************************
+*       Check if host is required proxy host     *
+*************************************************/
+/* The function determines if inbound host will be a regular smtp host
+or if it is configured that it must use Proxy Protocol.  A local
+connection cannot.
+
+Arguments: none
+Returns:   boolean for Proxy Protocol needed
+*/
+
+BOOL
+proxy_protocol_host(void)
+{
+int rc;
+
+if (  sender_host_address
+   && (rc = verify_check_this_host(CUSS &hosts_proxy, NULL, NULL,
+                           sender_host_address, NULL)) == OK)
+  {
+  DEBUG(D_receive)
+    debug_printf("Detected proxy protocol configured host\n");
+  proxy_session = TRUE;
+  }
+return proxy_session;
+}
+
+
+/*************************************************
+*    Read data until newline or end of buffer    *
+*************************************************/
+/* While SMTP is server-speaks-first, TLS is client-speaks-first, so we can't
+read an entire buffer and assume there will be nothing past a proxy protocol
+header.  Our approach normally is to use stdio, but again that relies upon
+"STARTTLS\r\n" and a server response before the client starts TLS handshake, or
+reading _nothing_ before client TLS handshake.  So we don't want to use the
+usual buffering reads which may read enough to block TLS starting.
+
+So unfortunately we're down to "read one byte at a time, with a syscall each,
+and expect a little overhead", for all proxy-opened connections which are v1,
+just to handle the TLS-on-connect case.  Since SSL functions wrap the
+underlying fd, we can't assume that we can feed them any already-read content.
+
+We need to know where to read to, the max capacity, and we'll read until we
+get a CR and one more character.  Let the caller scream if it's CR+!LF.
+
+Return the amount read.
+*/
+
+static int
+swallow_until_crlf(int fd, uschar *base, int already, int capacity)
+{
+uschar *to = base + already;
+uschar *cr;
+int have = 0;
+int ret;
+int last = 0;
+
+/* For "PROXY UNKNOWN\r\n" we, at time of writing, expect to have read
+up through the \r; for the _normal_ case, we haven't yet seen the \r. */
+
+cr = memchr(base, '\r', already);
+if (cr != NULL)
+  {
+  if ((cr - base) < already - 1)
+    {
+    /* \r and presumed \n already within what we have; probably not
+    actually proxy protocol, but abort cleanly. */
+    return 0;
+    }
+  /* \r is last character read, just need one more. */
+  last = 1;
+  }
+
+while (capacity > 0)
+  {
+  do { ret = read(fd, to, 1); } while (ret == -1 && errno == EINTR && !had_command_timeout);
+  if (ret == -1)
+    return -1;
+  have++;
+  if (last)
+    return have;
+  if (*to == '\r')
+    last = 1;
+  capacity--;
+  to++;
+  }
+
+/* reached end without having room for a final newline, abort */
+errno = EOVERFLOW;
+return -1;
+}
+
+
+static void
+proxy_debug(uschar * buf, unsigned start, unsigned end)
+{
+debug_printf("PROXY<<");
+while (start < end) debug_printf(" %02x", buf[start++]);
+debug_printf("\n");
+}
+
+
+/*************************************************
+*         Setup host for proxy protocol          *
+*************************************************/
+/* The function configures the connection based on a header from the
+inbound host to use Proxy Protocol. The specification is very exact
+so exit with an error if do not find the exact required pieces. This
+includes an incorrect number of spaces separating args.
+
+Arguments: none
+Returns:   Boolean success
+*/
+
+void
+proxy_protocol_setup(void)
+{
+union {
+  struct {
+    uschar line[108];
+  } v1;
+  struct {
+    uschar sig[12];
+    uint8_t ver_cmd;
+    uint8_t fam;
+    uint16_t len;
+    union {
+      struct { /* TCP/UDP over IPv4, len = 12 */
+        uint32_t src_addr;
+        uint32_t dst_addr;
+        uint16_t src_port;
+        uint16_t dst_port;
+      } ip4;
+      struct { /* TCP/UDP over IPv6, len = 36 */
+        uint8_t  src_addr[16];
+        uint8_t  dst_addr[16];
+        uint16_t src_port;
+        uint16_t dst_port;
+      } ip6;
+      struct { /* AF_UNIX sockets, len = 216 */
+        uschar   src_addr[108];
+        uschar   dst_addr[108];
+      } unx;
+    } addr;
+  } v2;
+} hdr;
+
+/* Temp variables used in PPv2 address:port parsing */
+uint16_t tmpport;
+char tmpip[INET_ADDRSTRLEN];
+struct sockaddr_in tmpaddr;
+char tmpip6[INET6_ADDRSTRLEN];
+struct sockaddr_in6 tmpaddr6;
+
+/* We can't read "all data until end" because while SMTP is
+server-speaks-first, the TLS handshake is client-speaks-first, so for
+TLS-on-connect ports the proxy protocol header will usually be immediately
+followed by a TLS handshake, and with N TLS libraries, we can't reliably
+reinject data for reading by those.  So instead we first read "enough to be
+safely read within the header, and figure out how much more to read".
+For v1 we will later read to the end-of-line, for v2 we will read based upon
+the stated length.
+
+The v2 sig is 12 octets, and another 4 gets us the length, so we know how much
+data is needed total.  For v1, where the line looks like:
+PROXY TCPn L3src L3dest SrcPort DestPort \r\n
+
+However, for v1 there's also `PROXY UNKNOWN\r\n` which is only 15 octets.
+We seem to support that.  So, if we read 14 octets then we can tell if we're
+v2 or v1.  If we're v1, we can continue reading as normal.
+
+If we're v2, we can't slurp up the entire header.  We need the length in the
+15th & 16th octets, then to read everything after that.
+
+So to safely handle v1 and v2, with client-sent-first supported correctly,
+we have to do a minimum of 3 read calls, not 1.  Eww.
+*/
+
+# define PROXY_INITIAL_READ 14
+# define PROXY_V2_HEADER_SIZE 16
+# if PROXY_INITIAL_READ > PROXY_V2_HEADER_SIZE
+#  error Code bug in sizes of data to read for proxy usage
+# endif
+
+int get_ok = 0;
+int size, ret;
+int fd = fileno(smtp_in);
+const char v2sig[12] = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A";
+uschar * iptype;  /* To display debug info */
+socklen_t vslen = sizeof(struct timeval);
+BOOL yield = FALSE;
+
+ALARM(proxy_protocol_timeout);
+
+do
+  {
+  /* The inbound host was declared to be a Proxy Protocol host, so
+  don't do a PEEK into the data, actually slurp up enough to be
+  "safe". Can't take it all because TLS-on-connect clients follow
+  immediately with TLS handshake. */
+  ret = read(fd, &hdr, PROXY_INITIAL_READ);
+  } while (ret == -1 && errno == EINTR && !had_command_timeout);
+
+if (ret == -1)
+  goto proxyfail;
+DEBUG(D_receive) proxy_debug(US &hdr, 0, ret);
+
+/* For v2, handle reading the length, and then the rest. */
+if ((ret == PROXY_INITIAL_READ) && (memcmp(&hdr.v2, v2sig, sizeof(v2sig)) == 0))
+  {
+  int retmore;
+  uint8_t ver;
+
+  DEBUG(D_receive) debug_printf("v2\n");
+
+  /* First get the length fields. */
+  do
+    {
+    retmore = read(fd, (uschar*)&hdr + ret, PROXY_V2_HEADER_SIZE - PROXY_INITIAL_READ);
+    } while (retmore == -1 && errno == EINTR && !had_command_timeout);
+  if (retmore == -1)
+    goto proxyfail;
+  DEBUG(D_receive) proxy_debug(US &hdr, ret, ret + retmore);
+
+  ret += retmore;
+
+  ver = (hdr.v2.ver_cmd & 0xf0) >> 4;
+
+  /* May 2014: haproxy combined the version and command into one byte to
+  allow two full bytes for the length field in order to proxy SSL
+  connections.  SSL Proxy is not supported in this version of Exim, but
+  must still separate values here. */
+
+  if (ver != 0x02)
+    {
+    DEBUG(D_receive) debug_printf("Invalid Proxy Protocol version: %d\n", ver);
+    goto proxyfail;
+    }
+
+  /* The v2 header will always be 16 bytes per the spec. */
+  size = 16 + ntohs(hdr.v2.len);
+  DEBUG(D_receive) debug_printf("Detected PROXYv2 header, size %d (limit %d)\n",
+      size, (int)sizeof(hdr));
+
+  /* We should now have 16 octets (PROXY_V2_HEADER_SIZE), and we know the total
+  amount that we need.  Double-check that the size is not unreasonable, then
+  get the rest. */
+  if (size > sizeof(hdr))
+    {
+    DEBUG(D_receive) debug_printf("PROXYv2 header size unreasonably large; security attack?\n");
+    goto proxyfail;
+    }
+
+  do
+    {
+    do
+      {
+      retmore = read(fd, (uschar*)&hdr + ret, size-ret);
+      } while (retmore == -1 && errno == EINTR && !had_command_timeout);
+    if (retmore == -1)
+      goto proxyfail;
+    DEBUG(D_receive) proxy_debug(US &hdr, ret, ret + retmore);
+    ret += retmore;
+    DEBUG(D_receive) debug_printf("PROXYv2: have %d/%d required octets\n", ret, size);
+    } while (ret < size);
+
+  } /* end scope for getting rest of data for v2 */
+
+/* At this point: if PROXYv2, we've read the exact size required for all data;
+if PROXYv1 then we've read "less than required for any valid line" and should
+read the rest". */
+
+if (ret >= 16 && memcmp(&hdr.v2, v2sig, 12) == 0)
+  {
+  uint8_t cmd = (hdr.v2.ver_cmd & 0x0f);
+
+  switch (cmd)
+    {
+    case 0x01: /* PROXY command */
+      switch (hdr.v2.fam)
+        {
+        case 0x11:  /* TCPv4 address type */
+          iptype = US"IPv4";
+          tmpaddr.sin_addr.s_addr = hdr.v2.addr.ip4.src_addr;
+          inet_ntop(AF_INET, &tmpaddr.sin_addr, CS &tmpip, sizeof(tmpip));
+          if (!string_is_ip_address(US tmpip, NULL))
+            {
+            DEBUG(D_receive) debug_printf("Invalid %s source IP\n", iptype);
+            goto proxyfail;
+            }
+          proxy_local_address = sender_host_address;
+          sender_host_address = string_copy(US tmpip);
+          tmpport             = ntohs(hdr.v2.addr.ip4.src_port);
+          proxy_local_port    = sender_host_port;
+          sender_host_port    = tmpport;
+          /* Save dest ip/port */
+          tmpaddr.sin_addr.s_addr = hdr.v2.addr.ip4.dst_addr;
+          inet_ntop(AF_INET, &tmpaddr.sin_addr, CS &tmpip, sizeof(tmpip));
+          if (!string_is_ip_address(US tmpip, NULL))
+            {
+            DEBUG(D_receive) debug_printf("Invalid %s dest port\n", iptype);
+            goto proxyfail;
+            }
+          proxy_external_address = string_copy(US tmpip);
+          tmpport              = ntohs(hdr.v2.addr.ip4.dst_port);
+          proxy_external_port  = tmpport;
+          goto done;
+        case 0x21:  /* TCPv6 address type */
+          iptype = US"IPv6";
+          memmove(tmpaddr6.sin6_addr.s6_addr, hdr.v2.addr.ip6.src_addr, 16);
+          inet_ntop(AF_INET6, &tmpaddr6.sin6_addr, CS &tmpip6, sizeof(tmpip6));
+          if (!string_is_ip_address(US tmpip6, NULL))
+            {
+            DEBUG(D_receive) debug_printf("Invalid %s source IP\n", iptype);
+            goto proxyfail;
+            }
+          proxy_local_address = sender_host_address;
+          sender_host_address = string_copy(US tmpip6);
+          tmpport             = ntohs(hdr.v2.addr.ip6.src_port);
+          proxy_local_port    = sender_host_port;
+          sender_host_port    = tmpport;
+          /* Save dest ip/port */
+          memmove(tmpaddr6.sin6_addr.s6_addr, hdr.v2.addr.ip6.dst_addr, 16);
+          inet_ntop(AF_INET6, &tmpaddr6.sin6_addr, CS &tmpip6, sizeof(tmpip6));
+          if (!string_is_ip_address(US tmpip6, NULL))
+            {
+            DEBUG(D_receive) debug_printf("Invalid %s dest port\n", iptype);
+            goto proxyfail;
+            }
+          proxy_external_address = string_copy(US tmpip6);
+          tmpport              = ntohs(hdr.v2.addr.ip6.dst_port);
+          proxy_external_port  = tmpport;
+          goto done;
+        default:
+          DEBUG(D_receive)
+            debug_printf("Unsupported PROXYv2 connection type: 0x%02x\n",
+                         hdr.v2.fam);
+          goto proxyfail;
+        }
+      /* Unsupported protocol, keep local connection address */
+      break;
+    case 0x00: /* LOCAL command */
+      /* Keep local connection address for LOCAL */
+      iptype = US"local";
+      break;
+    default:
+      DEBUG(D_receive)
+        debug_printf("Unsupported PROXYv2 command: 0x%x\n", cmd);
+      goto proxyfail;
+    }
+  }
+else if (ret >= 8 && memcmp(hdr.v1.line, "PROXY", 5) == 0)
+  {
+  uschar *p;
+  uschar *end;
+  uschar *sp;     /* Utility variables follow */
+  int     tmp_port;
+  int     r2;
+  char   *endc;
+
+  /* get the rest of the line */
+  r2 = swallow_until_crlf(fd, (uschar*)&hdr, ret, sizeof(hdr)-ret);
+  if (r2 == -1)
+    goto proxyfail;
+  ret += r2;
+
+  p = string_copy(hdr.v1.line);
+  end = memchr(p, '\r', ret - 1);
+
+  if (!end || (end == (uschar*)&hdr + ret) || end[1] != '\n')
+    {
+    DEBUG(D_receive) debug_printf("Partial or invalid PROXY header\n");
+    goto proxyfail;
+    }
+  *end = '\0'; /* Terminate the string */
+  size = end + 2 - p; /* Skip header + CRLF */
+  DEBUG(D_receive) debug_printf("Detected PROXYv1 header\n");
+  DEBUG(D_receive) debug_printf("Bytes read not within PROXY header: %d\n", ret - size);
+  /* Step through the string looking for the required fields. Ensure
+  strict adherence to required formatting, exit for any error. */
+  p += 5;
+  if (!isspace(*(p++)))
+    {
+    DEBUG(D_receive) debug_printf("Missing space after PROXY command\n");
+    goto proxyfail;
+    }
+  if (!Ustrncmp(p, CCS"TCP4", 4))
+    iptype = US"IPv4";
+  else if (!Ustrncmp(p,CCS"TCP6", 4))
+    iptype = US"IPv6";
+  else if (!Ustrncmp(p,CCS"UNKNOWN", 7))
+    {
+    iptype = US"Unknown";
+    goto done;
+    }
+  else
+    {
+    DEBUG(D_receive) debug_printf("Invalid TCP type\n");
+    goto proxyfail;
+    }
+
+  p += Ustrlen(iptype);
+  if (!isspace(*(p++)))
+    {
+    DEBUG(D_receive) debug_printf("Missing space after TCP4/6 command\n");
+    goto proxyfail;
+    }
+  /* Find the end of the arg */
+  if ((sp = Ustrchr(p, ' ')) == NULL)
+    {
+    DEBUG(D_receive)
+      debug_printf("Did not find proxied src %s\n", iptype);
+    goto proxyfail;
+    }
+  *sp = '\0';
+  if(!string_is_ip_address(p, NULL))
+    {
+    DEBUG(D_receive)
+      debug_printf("Proxied src arg is not an %s address\n", iptype);
+    goto proxyfail;
+    }
+  proxy_local_address = sender_host_address;
+  sender_host_address = p;
+  p = sp + 1;
+  if ((sp = Ustrchr(p, ' ')) == NULL)
+    {
+    DEBUG(D_receive)
+      debug_printf("Did not find proxy dest %s\n", iptype);
+    goto proxyfail;
+    }
+  *sp = '\0';
+  if(!string_is_ip_address(p, NULL))
+    {
+    DEBUG(D_receive)
+      debug_printf("Proxy dest arg is not an %s address\n", iptype);
+    goto proxyfail;
+    }
+  proxy_external_address = p;
+  p = sp + 1;
+  if ((sp = Ustrchr(p, ' ')) == NULL)
+    {
+    DEBUG(D_receive) debug_printf("Did not find proxied src port\n");
+    goto proxyfail;
+    }
+  *sp = '\0';
+  tmp_port = strtol(CCS p, &endc, 10);
+  if (*endc || tmp_port == 0)
+    {
+    DEBUG(D_receive)
+      debug_printf("Proxied src port '%s' not an integer\n", p);
+    goto proxyfail;
+    }
+  proxy_local_port = sender_host_port;
+  sender_host_port = tmp_port;
+  p = sp + 1;
+  if ((sp = Ustrchr(p, '\0')) == NULL)
+    {
+    DEBUG(D_receive) debug_printf("Did not find proxy dest port\n");
+    goto proxyfail;
+    }
+  tmp_port = strtol(CCS p, &endc, 10);
+  if (*endc || tmp_port == 0)
+    {
+    DEBUG(D_receive)
+      debug_printf("Proxy dest port '%s' not an integer\n", p);
+    goto proxyfail;
+    }
+  proxy_external_port = tmp_port;
+  /* Already checked for /r /n above. Good V1 header received. */
+  }
+else
+  {
+  /* Wrong protocol */
+  DEBUG(D_receive) debug_printf("Invalid proxy protocol version negotiation\n");
+  (void) swallow_until_crlf(fd, (uschar*)&hdr, ret, sizeof(hdr)-ret);
+  goto proxyfail;
+  }
+
+done:
+  DEBUG(D_receive)
+    debug_printf("Valid %s sender from Proxy Protocol header\n", iptype);
+  yield = proxy_session;
+
+/* Don't flush any potential buffer contents. Any input on proxyfail
+should cause a synchronization failure */
+
+proxyfail:
+  DEBUG(D_receive) if (had_command_timeout)
+    debug_printf("Timeout while reading proxy header\n");
+
+bad:
+  if (yield)
+    {
+    sender_host_name = NULL;
+    (void) host_name_lookup();
+    host_build_sender_fullhost();
+    }
+  else
+    {
+    f.proxy_session_failed = TRUE;
+    DEBUG(D_receive)
+      debug_printf("Failure to extract proxied host, only QUIT allowed\n");
+    }
+
+ALARM(0);
+return;
+}
+#endif	/*SUPPORT_PROXY*/
+
+/* vi: aw ai sw=2
+*/
+/* End of proxy.c */
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 5b2df7805..7a45772ce 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -1110,518 +1110,6 @@ had_command_sigterm = sig;
 
 
 
-#ifdef SUPPORT_PROXY
-/*************************************************
-*       Check if host is required proxy host     *
-*************************************************/
-/* The function determines if inbound host will be a regular smtp host
-or if it is configured that it must use Proxy Protocol.  A local
-connection cannot.
-
-Arguments: none
-Returns:   bool
-*/
-
-static BOOL
-check_proxy_protocol_host()
-{
-int rc;
-
-if (  sender_host_address
-   && (rc = verify_check_this_host(CUSS &hosts_proxy, NULL, NULL,
-                           sender_host_address, NULL)) == OK)
-  {
-  DEBUG(D_receive)
-    debug_printf("Detected proxy protocol configured host\n");
-  proxy_session = TRUE;
-  }
-return proxy_session;
-}
-
-
-/*************************************************
-*    Read data until newline or end of buffer    *
-*************************************************/
-/* While SMTP is server-speaks-first, TLS is client-speaks-first, so we can't
-read an entire buffer and assume there will be nothing past a proxy protocol
-header.  Our approach normally is to use stdio, but again that relies upon
-"STARTTLS\r\n" and a server response before the client starts TLS handshake, or
-reading _nothing_ before client TLS handshake.  So we don't want to use the
-usual buffering reads which may read enough to block TLS starting.
-
-So unfortunately we're down to "read one byte at a time, with a syscall each,
-and expect a little overhead", for all proxy-opened connections which are v1,
-just to handle the TLS-on-connect case.  Since SSL functions wrap the
-underlying fd, we can't assume that we can feed them any already-read content.
-
-We need to know where to read to, the max capacity, and we'll read until we
-get a CR and one more character.  Let the caller scream if it's CR+!LF.
-
-Return the amount read.
-*/
-
-static int
-swallow_until_crlf(int fd, uschar *base, int already, int capacity)
-{
-uschar *to = base + already;
-uschar *cr;
-int have = 0;
-int ret;
-int last = 0;
-
-/* For "PROXY UNKNOWN\r\n" we, at time of writing, expect to have read
-up through the \r; for the _normal_ case, we haven't yet seen the \r. */
-
-cr = memchr(base, '\r', already);
-if (cr != NULL)
-  {
-  if ((cr - base) < already - 1)
-    {
-    /* \r and presumed \n already within what we have; probably not
-    actually proxy protocol, but abort cleanly. */
-    return 0;
-    }
-  /* \r is last character read, just need one more. */
-  last = 1;
-  }
-
-while (capacity > 0)
-  {
-  do { ret = read(fd, to, 1); } while (ret == -1 && errno == EINTR && !had_command_timeout);
-  if (ret == -1)
-    return -1;
-  have++;
-  if (last)
-    return have;
-  if (*to == '\r')
-    last = 1;
-  capacity--;
-  to++;
-  }
-
-/* reached end without having room for a final newline, abort */
-errno = EOVERFLOW;
-return -1;
-}
-
-
-static void
-proxy_debug(uschar * buf, unsigned start, unsigned end)
-{
-debug_printf("PROXY<<");
-while (start < end) debug_printf(" %02x", buf[start++]);
-debug_printf("\n");
-}
-
-
-/*************************************************
-*         Setup host for proxy protocol          *
-*************************************************/
-/* The function configures the connection based on a header from the
-inbound host to use Proxy Protocol. The specification is very exact
-so exit with an error if do not find the exact required pieces. This
-includes an incorrect number of spaces separating args.
-
-Arguments: none
-Returns:   Boolean success
-*/
-
-static void
-setup_proxy_protocol_host()
-{
-union {
-  struct {
-    uschar line[108];
-  } v1;
-  struct {
-    uschar sig[12];
-    uint8_t ver_cmd;
-    uint8_t fam;
-    uint16_t len;
-    union {
-      struct { /* TCP/UDP over IPv4, len = 12 */
-        uint32_t src_addr;
-        uint32_t dst_addr;
-        uint16_t src_port;
-        uint16_t dst_port;
-      } ip4;
-      struct { /* TCP/UDP over IPv6, len = 36 */
-        uint8_t  src_addr[16];
-        uint8_t  dst_addr[16];
-        uint16_t src_port;
-        uint16_t dst_port;
-      } ip6;
-      struct { /* AF_UNIX sockets, len = 216 */
-        uschar   src_addr[108];
-        uschar   dst_addr[108];
-      } unx;
-    } addr;
-  } v2;
-} hdr;
-
-/* Temp variables used in PPv2 address:port parsing */
-uint16_t tmpport;
-char tmpip[INET_ADDRSTRLEN];
-struct sockaddr_in tmpaddr;
-char tmpip6[INET6_ADDRSTRLEN];
-struct sockaddr_in6 tmpaddr6;
-
-/* We can't read "all data until end" because while SMTP is
-server-speaks-first, the TLS handshake is client-speaks-first, so for
-TLS-on-connect ports the proxy protocol header will usually be immediately
-followed by a TLS handshake, and with N TLS libraries, we can't reliably
-reinject data for reading by those.  So instead we first read "enough to be
-safely read within the header, and figure out how much more to read".
-For v1 we will later read to the end-of-line, for v2 we will read based upon
-the stated length.
-
-The v2 sig is 12 octets, and another 4 gets us the length, so we know how much
-data is needed total.  For v1, where the line looks like:
-PROXY TCPn L3src L3dest SrcPort DestPort \r\n
-
-However, for v1 there's also `PROXY UNKNOWN\r\n` which is only 15 octets.
-We seem to support that.  So, if we read 14 octets then we can tell if we're
-v2 or v1.  If we're v1, we can continue reading as normal.
-
-If we're v2, we can't slurp up the entire header.  We need the length in the
-15th & 16th octets, then to read everything after that.
-
-So to safely handle v1 and v2, with client-sent-first supported correctly,
-we have to do a minimum of 3 read calls, not 1.  Eww.
-*/
-
-# define PROXY_INITIAL_READ 14
-# define PROXY_V2_HEADER_SIZE 16
-# if PROXY_INITIAL_READ > PROXY_V2_HEADER_SIZE
-#  error Code bug in sizes of data to read for proxy usage
-# endif
-
-int get_ok = 0;
-int size, ret;
-int fd = fileno(smtp_in);
-const char v2sig[12] = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A";
-uschar * iptype;  /* To display debug info */
-socklen_t vslen = sizeof(struct timeval);
-BOOL yield = FALSE;
-
-os_non_restarting_signal(SIGALRM, command_timeout_handler);
-ALARM(proxy_protocol_timeout);
-
-do
-  {
-  /* The inbound host was declared to be a Proxy Protocol host, so
-  don't do a PEEK into the data, actually slurp up enough to be
-  "safe". Can't take it all because TLS-on-connect clients follow
-  immediately with TLS handshake. */
-  ret = read(fd, &hdr, PROXY_INITIAL_READ);
-  } while (ret == -1 && errno == EINTR && !had_command_timeout);
-
-if (ret == -1)
-  goto proxyfail;
-DEBUG(D_receive) proxy_debug(US &hdr, 0, ret);
-
-/* For v2, handle reading the length, and then the rest. */
-if ((ret == PROXY_INITIAL_READ) && (memcmp(&hdr.v2, v2sig, sizeof(v2sig)) == 0))
-  {
-  int retmore;
-  uint8_t ver;
-
-  DEBUG(D_receive) debug_printf("v2\n");
-
-  /* First get the length fields. */
-  do
-    {
-    retmore = read(fd, (uschar*)&hdr + ret, PROXY_V2_HEADER_SIZE - PROXY_INITIAL_READ);
-    } while (retmore == -1 && errno == EINTR && !had_command_timeout);
-  if (retmore == -1)
-    goto proxyfail;
-  DEBUG(D_receive) proxy_debug(US &hdr, ret, ret + retmore);
-
-  ret += retmore;
-
-  ver = (hdr.v2.ver_cmd & 0xf0) >> 4;
-
-  /* May 2014: haproxy combined the version and command into one byte to
-  allow two full bytes for the length field in order to proxy SSL
-  connections.  SSL Proxy is not supported in this version of Exim, but
-  must still separate values here. */
-
-  if (ver != 0x02)
-    {
-    DEBUG(D_receive) debug_printf("Invalid Proxy Protocol version: %d\n", ver);
-    goto proxyfail;
-    }
-
-  /* The v2 header will always be 16 bytes per the spec. */
-  size = 16 + ntohs(hdr.v2.len);
-  DEBUG(D_receive) debug_printf("Detected PROXYv2 header, size %d (limit %d)\n",
-      size, (int)sizeof(hdr));
-
-  /* We should now have 16 octets (PROXY_V2_HEADER_SIZE), and we know the total
-  amount that we need.  Double-check that the size is not unreasonable, then
-  get the rest. */
-  if (size > sizeof(hdr))
-    {
-    DEBUG(D_receive) debug_printf("PROXYv2 header size unreasonably large; security attack?\n");
-    goto proxyfail;
-    }
-
-  do
-    {
-    do
-      {
-      retmore = read(fd, (uschar*)&hdr + ret, size-ret);
-      } while (retmore == -1 && errno == EINTR && !had_command_timeout);
-    if (retmore == -1)
-      goto proxyfail;
-    DEBUG(D_receive) proxy_debug(US &hdr, ret, ret + retmore);
-    ret += retmore;
-    DEBUG(D_receive) debug_printf("PROXYv2: have %d/%d required octets\n", ret, size);
-    } while (ret < size);
-
-  } /* end scope for getting rest of data for v2 */
-
-/* At this point: if PROXYv2, we've read the exact size required for all data;
-if PROXYv1 then we've read "less than required for any valid line" and should
-read the rest". */
-
-if (ret >= 16 && memcmp(&hdr.v2, v2sig, 12) == 0)
-  {
-  uint8_t cmd = (hdr.v2.ver_cmd & 0x0f);
-
-  switch (cmd)
-    {
-    case 0x01: /* PROXY command */
-      switch (hdr.v2.fam)
-        {
-        case 0x11:  /* TCPv4 address type */
-          iptype = US"IPv4";
-          tmpaddr.sin_addr.s_addr = hdr.v2.addr.ip4.src_addr;
-          inet_ntop(AF_INET, &tmpaddr.sin_addr, CS &tmpip, sizeof(tmpip));
-          if (!string_is_ip_address(US tmpip, NULL))
-            {
-            DEBUG(D_receive) debug_printf("Invalid %s source IP\n", iptype);
-            goto proxyfail;
-            }
-          proxy_local_address = sender_host_address;
-          sender_host_address = string_copy(US tmpip);
-          tmpport             = ntohs(hdr.v2.addr.ip4.src_port);
-          proxy_local_port    = sender_host_port;
-          sender_host_port    = tmpport;
-          /* Save dest ip/port */
-          tmpaddr.sin_addr.s_addr = hdr.v2.addr.ip4.dst_addr;
-          inet_ntop(AF_INET, &tmpaddr.sin_addr, CS &tmpip, sizeof(tmpip));
-          if (!string_is_ip_address(US tmpip, NULL))
-            {
-            DEBUG(D_receive) debug_printf("Invalid %s dest port\n", iptype);
-            goto proxyfail;
-            }
-          proxy_external_address = string_copy(US tmpip);
-          tmpport              = ntohs(hdr.v2.addr.ip4.dst_port);
-          proxy_external_port  = tmpport;
-          goto done;
-        case 0x21:  /* TCPv6 address type */
-          iptype = US"IPv6";
-          memmove(tmpaddr6.sin6_addr.s6_addr, hdr.v2.addr.ip6.src_addr, 16);
-          inet_ntop(AF_INET6, &tmpaddr6.sin6_addr, CS &tmpip6, sizeof(tmpip6));
-          if (!string_is_ip_address(US tmpip6, NULL))
-            {
-            DEBUG(D_receive) debug_printf("Invalid %s source IP\n", iptype);
-            goto proxyfail;
-            }
-          proxy_local_address = sender_host_address;
-          sender_host_address = string_copy(US tmpip6);
-          tmpport             = ntohs(hdr.v2.addr.ip6.src_port);
-          proxy_local_port    = sender_host_port;
-          sender_host_port    = tmpport;
-          /* Save dest ip/port */
-          memmove(tmpaddr6.sin6_addr.s6_addr, hdr.v2.addr.ip6.dst_addr, 16);
-          inet_ntop(AF_INET6, &tmpaddr6.sin6_addr, CS &tmpip6, sizeof(tmpip6));
-          if (!string_is_ip_address(US tmpip6, NULL))
-            {
-            DEBUG(D_receive) debug_printf("Invalid %s dest port\n", iptype);
-            goto proxyfail;
-            }
-          proxy_external_address = string_copy(US tmpip6);
-          tmpport              = ntohs(hdr.v2.addr.ip6.dst_port);
-          proxy_external_port  = tmpport;
-          goto done;
-        default:
-          DEBUG(D_receive)
-            debug_printf("Unsupported PROXYv2 connection type: 0x%02x\n",
-                         hdr.v2.fam);
-          goto proxyfail;
-        }
-      /* Unsupported protocol, keep local connection address */
-      break;
-    case 0x00: /* LOCAL command */
-      /* Keep local connection address for LOCAL */
-      iptype = US"local";
-      break;
-    default:
-      DEBUG(D_receive)
-        debug_printf("Unsupported PROXYv2 command: 0x%x\n", cmd);
-      goto proxyfail;
-    }
-  }
-else if (ret >= 8 && memcmp(hdr.v1.line, "PROXY", 5) == 0)
-  {
-  uschar *p;
-  uschar *end;
-  uschar *sp;     /* Utility variables follow */
-  int     tmp_port;
-  int     r2;
-  char   *endc;
-
-  /* get the rest of the line */
-  r2 = swallow_until_crlf(fd, (uschar*)&hdr, ret, sizeof(hdr)-ret);
-  if (r2 == -1)
-    goto proxyfail;
-  ret += r2;
-
-  p = string_copy(hdr.v1.line);
-  end = memchr(p, '\r', ret - 1);
-
-  if (!end || (end == (uschar*)&hdr + ret) || end[1] != '\n')
-    {
-    DEBUG(D_receive) debug_printf("Partial or invalid PROXY header\n");
-    goto proxyfail;
-    }
-  *end = '\0'; /* Terminate the string */
-  size = end + 2 - p; /* Skip header + CRLF */
-  DEBUG(D_receive) debug_printf("Detected PROXYv1 header\n");
-  DEBUG(D_receive) debug_printf("Bytes read not within PROXY header: %d\n", ret - size);
-  /* Step through the string looking for the required fields. Ensure
-  strict adherence to required formatting, exit for any error. */
-  p += 5;
-  if (!isspace(*(p++)))
-    {
-    DEBUG(D_receive) debug_printf("Missing space after PROXY command\n");
-    goto proxyfail;
-    }
-  if (!Ustrncmp(p, CCS"TCP4", 4))
-    iptype = US"IPv4";
-  else if (!Ustrncmp(p,CCS"TCP6", 4))
-    iptype = US"IPv6";
-  else if (!Ustrncmp(p,CCS"UNKNOWN", 7))
-    {
-    iptype = US"Unknown";
-    goto done;
-    }
-  else
-    {
-    DEBUG(D_receive) debug_printf("Invalid TCP type\n");
-    goto proxyfail;
-    }
-
-  p += Ustrlen(iptype);
-  if (!isspace(*(p++)))
-    {
-    DEBUG(D_receive) debug_printf("Missing space after TCP4/6 command\n");
-    goto proxyfail;
-    }
-  /* Find the end of the arg */
-  if ((sp = Ustrchr(p, ' ')) == NULL)
-    {
-    DEBUG(D_receive)
-      debug_printf("Did not find proxied src %s\n", iptype);
-    goto proxyfail;
-    }
-  *sp = '\0';
-  if(!string_is_ip_address(p, NULL))
-    {
-    DEBUG(D_receive)
-      debug_printf("Proxied src arg is not an %s address\n", iptype);
-    goto proxyfail;
-    }
-  proxy_local_address = sender_host_address;
-  sender_host_address = p;
-  p = sp + 1;
-  if ((sp = Ustrchr(p, ' ')) == NULL)
-    {
-    DEBUG(D_receive)
-      debug_printf("Did not find proxy dest %s\n", iptype);
-    goto proxyfail;
-    }
-  *sp = '\0';
-  if(!string_is_ip_address(p, NULL))
-    {
-    DEBUG(D_receive)
-      debug_printf("Proxy dest arg is not an %s address\n", iptype);
-    goto proxyfail;
-    }
-  proxy_external_address = p;
-  p = sp + 1;
-  if ((sp = Ustrchr(p, ' ')) == NULL)
-    {
-    DEBUG(D_receive) debug_printf("Did not find proxied src port\n");
-    goto proxyfail;
-    }
-  *sp = '\0';
-  tmp_port = strtol(CCS p, &endc, 10);
-  if (*endc || tmp_port == 0)
-    {
-    DEBUG(D_receive)
-      debug_printf("Proxied src port '%s' not an integer\n", p);
-    goto proxyfail;
-    }
-  proxy_local_port = sender_host_port;
-  sender_host_port = tmp_port;
-  p = sp + 1;
-  if ((sp = Ustrchr(p, '\0')) == NULL)
-    {
-    DEBUG(D_receive) debug_printf("Did not find proxy dest port\n");
-    goto proxyfail;
-    }
-  tmp_port = strtol(CCS p, &endc, 10);
-  if (*endc || tmp_port == 0)
-    {
-    DEBUG(D_receive)
-      debug_printf("Proxy dest port '%s' not an integer\n", p);
-    goto proxyfail;
-    }
-  proxy_external_port = tmp_port;
-  /* Already checked for /r /n above. Good V1 header received. */
-  }
-else
-  {
-  /* Wrong protocol */
-  DEBUG(D_receive) debug_printf("Invalid proxy protocol version negotiation\n");
-  (void) swallow_until_crlf(fd, (uschar*)&hdr, ret, sizeof(hdr)-ret);
-  goto proxyfail;
-  }
-
-done:
-  DEBUG(D_receive)
-    debug_printf("Valid %s sender from Proxy Protocol header\n", iptype);
-  yield = proxy_session;
-
-/* Don't flush any potential buffer contents. Any input on proxyfail
-should cause a synchronization failure */
-
-proxyfail:
-  DEBUG(D_receive) if (had_command_timeout)
-    debug_printf("Timeout while reading proxy header\n");
-
-bad:
-  if (yield)
-    {
-    sender_host_name = NULL;
-    (void) host_name_lookup();
-    host_build_sender_fullhost();
-    }
-  else
-    {
-    f.proxy_session_failed = TRUE;
-    DEBUG(D_receive)
-      debug_printf("Failure to extract proxied host, only QUIT allowed\n");
-    }
-
-ALARM(0);
-return;
-}
-#endif	/*SUPPORT_PROXY*/
-
 /*************************************************
 *           Read one command line                *
 *************************************************/
@@ -3030,14 +2518,20 @@ if (!f.sender_host_unknown)
 
 if (smtp_batched_input) return TRUE;
 
+#if defined(SUPPORT_PROXY) || defined(SUPPORT_SOCKS) || defined(EXPERIMETAL_XCLIENT)
+proxy_session = FALSE;
+#endif
+
+#ifdef SUPPORT_PROXY
 /* If valid Proxy Protocol source is connecting, set up session.
 Failure will not allow any SMTP function other than QUIT. */
 
-#ifdef SUPPORT_PROXY
-proxy_session = FALSE;
 f.proxy_session_failed = FALSE;
-if (check_proxy_protocol_host())
-  setup_proxy_protocol_host();
+if (proxy_protocol_host())
+  {
+  os_non_restarting_signal(SIGALRM, command_timeout_handler);
+  proxy_protocol_setup();
+  }
 #endif
 
 /* Run the connect ACL if it exists */

commit 24cda181fb88542cf38db2beae5d0ddb37f59c5c
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Mar 25 23:21:15 2023 +0000

    Experimental_XCLIENT.  Bug 2702

diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index d00ab9404..71aee4d93 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -497,7 +497,8 @@ OBJ_EXPERIMENTAL =	arc.o \
 			dmarc.o \
 			imap_utf7.o \
 			spf.o \
-			utf8.o
+			utf8.o \
+			xclient.o
 
 # Targets for final binaries; the main one has a build number which is
 # updated each time. We don't bother with that for the auxiliaries.
@@ -873,6 +874,7 @@ dmarc.o:	$(HDRS) pdkim/pdkim.h dmarc.h dmarc.c
 imap_utf7.o:	$(HDRS) imap_utf7.c
 spf.o:		$(HDRS) spf.h spf.c
 utf8.o:		$(HDRS) utf8.c
+xclient.o:	$(HDRS) xclient.c
 
 # The module containing tables of available lookups, routers, auths, and
 # transports must be rebuilt if any of them are. However, because the makefiles
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index af6138063..0694af4c0 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -125,7 +125,7 @@ done
 
 # EXPERIMENTAL_*
 for f in  arc.c bmi_spam.c bmi_spam.h dcc.c dcc.h dane.c dane-openssl.c \
-  danessl.h imap_utf7.c spf.c spf.h srs.c srs.h utf8.c
+  danessl.h imap_utf7.c spf.c spf.h srs.c srs.h utf8.c xclient.c
 do
   ln -s ../src/$f $f
 done
diff --git a/src/src/auths/xtextdecode.c b/src/src/auths/xtextdecode.c
index b6a927194..edd2282d0 100644
--- a/src/src/auths/xtextdecode.c
+++ b/src/src/auths/xtextdecode.c
@@ -32,9 +32,9 @@ Returns:      the number of bytes in the result, excluding the final zero;
 */
 
 int
-auth_xtextdecode(uschar *code, uschar **ptr)
+auth_xtextdecode(uschar * code, uschar ** ptr)
 {
-register int x;
+int x;
 uschar * result = store_get(Ustrlen(code) + 1, code);
 *ptr = result;
 
diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults
index 221705224..fb5fe3603 100644
--- a/src/src/config.h.defaults
+++ b/src/src/config.h.defaults
@@ -211,6 +211,7 @@ Do not put spaces between # and the 'define'.
 #define EXPERIMENTAL_DSN_INFO
 #define EXPERIMENTAL_ESMTP_LIMITS
 #define EXPERIMENTAL_QUEUEFILE
+#define EXPERIMENTAL_XCLIENT
 
 
 /* For developers */
diff --git a/src/src/exim.c b/src/src/exim.c
index c16beb1af..06863347d 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1132,6 +1132,9 @@ g = string_cat(g, US"Support for:");
 #ifdef EXPERIMENTAL_QUEUEFILE
   g = string_cat(g, US" Experimental_QUEUEFILE");
 #endif
+#ifdef EXPERIMENTAL_XCLIENT
+  g = string_cat(g, US" Experimental_XCLIENT");
+#endif
 g = string_cat(g, US"\n");
 
 g = string_cat(g, US"Lookups (built-in):");
diff --git a/src/src/functions.h b/src/src/functions.h
index 76392f304..aa5057a83 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -686,6 +686,11 @@ extern BOOL    write_chunk(transport_ctx *, uschar *, int);
 extern ssize_t write_to_fd_buf(int, const uschar *, size_t);
 extern uschar *wrap_header(const uschar *, unsigned, unsigned, const uschar *, unsigned);
 
+#ifdef EXPERIMENTAL_XCLIENT
+extern uschar * xclient_smtp_command(uschar *, int *, BOOL *);
+extern gstring * xclient_smtp_advertise_str(gstring *);
+#endif
+
 
 /******************************************************************************/
 /* Predicate: if an address is in a tainted pool.
diff --git a/src/src/globals.c b/src/src/globals.c
index 539bae00e..9f4053937 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -995,11 +995,18 @@ uschar *host_lookup_msg        = US"";
 int     host_number            = 0;
 uschar *host_number_string     = NULL;
 uschar *host_reject_connection = NULL;
-tree_node *hostlist_anchor     = NULL;
-int     hostlist_count         = 0;
+uschar *hosts_connection_nolog = NULL;
+#ifdef SUPPORT_PROXY
+uschar *hosts_proxy            = NULL;
+#endif
 uschar *hosts_treat_as_local   = NULL;
 uschar *hosts_require_helo     = US"*";
-uschar *hosts_connection_nolog = NULL;
+#ifdef EXPERIMENTAL_XCLIENT
+uschar *hosts_xclient	       = NULL;
+#endif
+tree_node *hostlist_anchor     = NULL;
+int     hostlist_count         = 0;
+
 
 int     ignore_bounce_errors_after = 10*7*24*60*60;  /* 10 weeks */
 uschar *ignore_fromline_hosts  = NULL;
@@ -1232,8 +1239,7 @@ int     process_info_len       = 0;
 uschar *process_log_path       = NULL;
 const uschar *process_purpose  = US"fresh-exec";
 
-#if defined(SUPPORT_PROXY) || defined(SUPPORT_SOCKS)
-uschar *hosts_proxy            = NULL;
+#if defined(SUPPORT_PROXY) || defined(SUPPORT_SOCKS) || defined(EXPERIMENTAL_XCLIENT)
 uschar *proxy_external_address = NULL;
 int     proxy_external_port    = 0;
 uschar *proxy_local_address    = NULL;
@@ -1660,5 +1666,4 @@ int     warning_count          = 0;
 const uschar *warnmsg_delay    = NULL;
 const uschar *warnmsg_recipients = NULL;
 
-
 /*  End of globals.c */
diff --git a/src/src/globals.h b/src/src/globals.h
index e216b9208..3a5513382 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -661,12 +661,16 @@ extern uschar *host_lookup_order;      /* Order of host lookup types */
 extern uschar *host_lookup_msg;        /* Text for why it failed */
 extern int     host_number;            /* For sharing spools */
 extern uschar *host_number_string;     /* For expanding */
-extern uschar *hosts_require_helo;     /* check for HELO/EHLO before MAIL */
 extern uschar *host_reject_connection; /* Reject these hosts */
-extern tree_node *hostlist_anchor;     /* Tree of defined host lists */
-extern int     hostlist_count;         /* Number defined */
 extern uschar *hosts_connection_nolog; /* Limits the logging option */
+extern uschar *hosts_require_helo;     /* check for HELO/EHLO before MAIL */
 extern uschar *hosts_treat_as_local;   /* For routing */
+#ifdef EXPERIMENTAL_XCLIENT
+extern uschar *hosts_xclient;	       /* Allow XCLIENT command for specified hosts */
+#endif
+extern tree_node *hostlist_anchor;     /* Tree of defined host lists */
+extern int     hostlist_count;         /* Number defined */
+
 
 extern int     ignore_bounce_errors_after; /* Keep them for this time. */
 extern BOOL    ignore_fromline_local;  /* Local SMTP ignore fromline */
@@ -828,7 +832,8 @@ extern int     proxy_external_port;    /* Port on remote interface of proxy */
 extern uschar *proxy_local_address;    /* IP of local interface of proxy */
 extern int     proxy_local_port;       /* Port on local interface of proxy */
 extern int     proxy_protocol_timeout; /* Timeout for proxy negotiation */
-extern BOOL    proxy_session;          /* TRUE if receiving mail from valid proxy  */
+extern BOOL    proxy_session;          /* TRUE if receiving mail from valid proxy
+					  or sending via one */
 #endif
 
 extern uschar *prvscheck_address;      /* Set during prvscheck expansion item */
diff --git a/src/src/host.c b/src/src/host.c
index 8d53eb3de..136ee8953 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -824,9 +824,9 @@ Returns:     pointer to character string
 */
 
 uschar *
-host_ntoa(int type, const void *arg, uschar *buffer, int *portptr)
+host_ntoa(int type, const void * arg, uschar * buffer, int * portptr)
 {
-uschar *yield;
+uschar * yield;
 
 /* The new world. It is annoying that we have to fish out the address from
 different places in the block, depending on what kind of address it is. It
diff --git a/src/src/macro_predef.c b/src/src/macro_predef.c
index 0053cb245..8fade68ca 100644
--- a/src/src/macro_predef.c
+++ b/src/src/macro_predef.c
@@ -205,6 +205,9 @@ due to conflicts with other common macros. */
 #ifndef DISABLE_TLS_RESUME
   builtin_macro_create(US"_HAVE_TLS_RESUME");
 #endif
+#ifdef EXPERIMENTAL_XCLIENT
+  builtin_macro_create(US"_HAVE_XCLIENT");
+#endif
 
 #ifdef LOOKUP_LSEARCH
   builtin_macro_create(US"_HAVE_LOOKUP_LSEARCH");
diff --git a/src/src/macros.h b/src/src/macros.h
index 36ed185ed..c55276332 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -822,7 +822,11 @@ most recent SMTP commands. SCH_NONE is "empty". */
 enum { SCH_NONE, SCH_AUTH, SCH_DATA, SCH_BDAT,
        SCH_EHLO, SCH_ETRN, SCH_EXPN, SCH_HELO,
        SCH_HELP, SCH_MAIL, SCH_NOOP, SCH_QUIT, SCH_RCPT, SCH_RSET, SCH_STARTTLS,
-       SCH_VRFY };
+       SCH_VRFY,
+#ifdef EXPERIMENTAL_XCLIENT
+       SCH_XCLIENT,
+#endif
+       };
 
 /* Returns from host_find_by{name,dns}() */
 
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 3b26e87d5..7d48f085d 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -187,6 +187,9 @@ static optionlist optionlist_config[] = {
 #endif
   { "hosts_require_helo",       opt_stringptr,   {&hosts_require_helo} },
   { "hosts_treat_as_local",     opt_stringptr,   {&hosts_treat_as_local} },
+#ifdef EXPERIMENTAL_XCLIENT
+  { "hosts_xclient",		opt_stringptr,	 {&hosts_xclient} },
+#endif
 #ifdef LOOKUP_IBASE
   { "ibase_servers",            opt_stringptr,   {&ibase_servers} },
 #endif
@@ -399,7 +402,7 @@ static optionlist optionlist_config[] = {
   { "uucp_from_pattern",        opt_stringptr,   {&uucp_from_pattern} },
   { "uucp_from_sender",         opt_stringptr,   {&uucp_from_sender} },
   { "warn_message_file",        opt_stringptr,   {&warn_message_file} },
-  { "write_rejectlog",          opt_bool,        {&write_rejectlog} }
+  { "write_rejectlog",          opt_bool,        {&write_rejectlog} },
 };
 
 #ifndef MACRO_PREDEF
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 7a45772ce..6f4ad9495 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -75,6 +75,9 @@ enum {
   ETRN_CMD,                     /* This by analogy with TURN from the RFC */
   STARTTLS_CMD,                 /* Required by the STARTTLS RFC */
   TLS_AUTH_CMD,			/* auto-command at start of SSL */
+#ifdef EXPERIMENTAL_XCLIENT
+  XCLIENT_CMD,			/* per xlexkiro implementation */
+#endif
 
   /* This is a dummy to identify the non-sync commands when pipelining */
 
@@ -189,14 +192,22 @@ count of non-mail commands and possibly provoke an error.
 tls_auth is a pseudo-command, never expected in input.  It is activated
 on TLS startup and looks for a tls authenticator. */
 
-enum {	CL_RSET, CL_HELO, CL_EHLO, CL_AUTH,
+enum {
+	CL_RSET = 0,
+	CL_HELO,
+	CL_EHLO,
+	CL_AUTH,
 #ifndef DISABLE_TLS
-	CL_STLS, CL_TLAU,
+	CL_STLS,
+	CL_TLAU,
+#endif
+#ifdef EXPERIMENTAL_XCLIENT
+	CL_XCLI,
 #endif
 };
 
 static smtp_cmd_list cmd_list[] = {
-  /* name         len                     cmd     has_arg is_mail_cmd */
+  /*             name         len                     cmd     has_arg is_mail_cmd */
 
   [CL_RSET] = { "rset",       sizeof("rset")-1,       RSET_CMD,	FALSE, FALSE },  /* First */
   [CL_HELO] = { "helo",       sizeof("helo")-1,       HELO_CMD, TRUE,  FALSE },
@@ -206,8 +217,9 @@ static smtp_cmd_list cmd_list[] = {
   [CL_STLS] = { "starttls",   sizeof("starttls")-1,   STARTTLS_CMD, FALSE, FALSE },
   [CL_TLAU] = { "tls_auth",   0,                      TLS_AUTH_CMD, FALSE, FALSE },
 #endif
-
-/* If you change anything above here, also fix the definitions below. */
+#ifdef EXPERIMENTAL_XCLIENT
+  [CL_XCLI] = { "xclient",    sizeof("xclient")-1,    XCLIENT_CMD, TRUE,  FALSE },
+#endif
 
   { "mail from:", sizeof("mail from:")-1, MAIL_CMD, TRUE,  TRUE  },
   { "rcpt to:",   sizeof("rcpt to:")-1,   RCPT_CMD, TRUE,  TRUE  },
@@ -241,6 +253,9 @@ uschar * smtp_names[] =
   [SCH_RSET] = US"RSET",
   [SCH_STARTTLS] = US"STARTTLS",
   [SCH_VRFY] = US"VRFY",
+#ifdef EXPERIMENTAL_XCLIENT
+  [SCH_XCLIENT] = US"XCLIENT",
+#endif
   };
 
 static uschar *protocols_local[] = {
@@ -1260,6 +1275,7 @@ return OTHER_CMD;
 
 
 
+
 /*************************************************
 *          Forced closedown of call              *
 *************************************************/
@@ -1774,7 +1790,6 @@ while (done <= 0)
       bsmtp_transaction_linecount = receive_linecount;
       break;
 
-
     /* The MAIL FROM command requires an address as an operand. All we
     do here is to parse it for syntactic correctness. The form "<>" is
     a special case which converts into an empty string. The start/end
@@ -4178,7 +4193,13 @@ while (done <= 0)
 	  fl.tls_advertised = TRUE;
 	  }
 #endif
-
+#ifdef EXPERIMENTAL_XCLIENT
+	if (proxy_session || verify_check_host(&hosts_xclient) != FAIL)
+	  {
+	  g = string_catn(g, smtp_code, 3);
+	  g = xclient_smtp_advertise_str(g);
+	  }
+#endif
 #ifndef DISABLE_PRDR
 	/* Per Recipient Data Response, draft by Eric A. Hall extending RFC */
 	if (prdr_enable)
@@ -4244,6 +4265,41 @@ while (done <= 0)
       toomany = FALSE;
       break;   /* HELO/EHLO */
 
+#ifdef EXPERIMENTAL_XCLIENT
+    case XCLIENT_CMD:
+      {
+      BOOL fatal = fl.helo_seen;
+      uschar * errmsg;
+      int resp;
+
+      HAD(SCH_XCLIENT);
+      smtp_mailcmd_count++;
+
+      if ((errmsg = xclient_smtp_command(smtp_cmd_data, &resp, &fatal)))
+	if (fatal)
+	  done = synprot_error(L_smtp_syntax_error, resp, NULL, errmsg);
+	else
+	  {
+	  smtp_printf("%d %s\r\n", FALSE, resp, errmsg);
+	  log_write(0, LOG_MAIN|LOG_REJECT, "rejected XCLIENT from %s: %s",
+	    host_and_ident(FALSE), errmsg);
+	  }
+      else
+	{
+	fl.helo_seen = FALSE;			/* Require another EHLO */
+	smtp_code = string_sprintf("%d", resp);
+
+	/*XXX unclear in spec. if this needs to be an ESMTP banner,
+	nor whether we get the original client's HELO after (or a proxy fake).
+	We require that we do; the following HELO/EHLO handling will set
+	sender_helo_name as normal. */
+
+	smtp_printf("%s XCLIENT success\r\n", FALSE, smtp_code);
+	}
+      break; /* XCLIENT */
+      }
+#endif
+
 
     /* The MAIL command requires an address as an operand. All we do
     here is to parse it for syntactic correctness. The form "<>" is
@@ -5353,6 +5409,10 @@ while (done <= 0)
       if (acl_smtp_etrn) smtp_printf(" ETRN", TRUE);
       if (acl_smtp_expn) smtp_printf(" EXPN", TRUE);
       if (acl_smtp_vrfy) smtp_printf(" VRFY", TRUE);
+#ifdef EXPERIMENTAL_XCLIENT
+      if (proxy_session || verify_check_host(&hosts_xclient) != FAIL)
+	smtp_printf(" XCLIENT", TRUE);
+#endif
       smtp_printf("\r\n", FALSE);
       break;
 
diff --git a/src/src/xclient.c b/src/src/xclient.c
new file mode 100644
index 000000000..2a8be9b0e
--- /dev/null
+++ b/src/src/xclient.c
@@ -0,0 +1,299 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2023 */
+/* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "exim.h"
+
+#ifdef EXPERIMENTAL_XCLIENT
+
+/* From https://www.postfix.org/XCLIENT_README.html I infer two generations of
+protocol.  The more recent one obviates the utility of the HELO attribute, since
+it mandates the proxy always sending a HELO/EHLO smtp command following (a
+successful) XCLIENT command, and that will carry a NELO name (which we assume,
+though it isn't specified, will be the actual one presented to the proxy by the
+possibly-new client).  The same applies to the PROTO attribute. */
+
+# define XCLIENT_V2
+
+enum xclient_cmd_e {
+  XCLIENT_CMD_UNKNOWN,
+  XCLIENT_CMD_ADDR,
+  XCLIENT_CMD_NAME,
+  XCLIENT_CMD_PORT,
+  XCLIENT_CMD_LOGIN,
+  XCLIENT_CMD_DESTADDR,
+  XCLIENT_CMD_DESTPORT,
+# ifdef XCLIENT_V1
+  XCLIENT_CMD_HELO,
+  XCLIENT_CMD_PROTO,
+# endif
+};
+
+struct xclient_cmd {
+  const uschar *	str;
+  unsigned		len;
+} xclient_cmds[] = {
+  [XCLIENT_CMD_UNKNOWN] = { NULL },
+  [XCLIENT_CMD_ADDR] =	{ US"ADDR",  4 },
+  [XCLIENT_CMD_NAME] =	{ US"NAME",  4 },
+  [XCLIENT_CMD_PORT] =	{ US"PORT",  4 },
+  [XCLIENT_CMD_LOGIN] =	{ US"LOGIN", 5 },
+  [XCLIENT_CMD_DESTADDR] =	{ US"DESTADDR", 8 },
+  [XCLIENT_CMD_DESTPORT] =	{ US"DESTPORT", 8 },
+# ifdef XCLIENT_V1
+  [XCLIENT_CMD_HELO] =	{ US"HELO",  4 },
+  [XCLIENT_CMD_PROTO] =	{ US"PROTO", 5 },
+# endif
+};
+
+/*************************************************
+*          XCLIENT proxy implementation          *
+*************************************************/
+
+/* Arguments:
+  code        points to the coded string
+  end         points to the end of coded string
+  ptr         where to put the pointer to the result, which is in
+              dynamic store
+Returns:      the number of bytes in the result, excluding the final zero;
+              -1 if the input is malformed
+*/
+
+static int
+xclient_xtextdecode(uschar * code, uschar * end, uschar ** ptr)
+{
+return auth_xtextdecode(string_copyn(code, end-code), ptr);
+}
+
+/*************************************************
+*   Check XCLIENT line and set sender_address    *
+*************************************************/
+
+
+/* Check the format of a XCLIENT line.
+Arguments:
+  s       	the data portion of the line (already past any white space)
+  resp		result: smtp respose code
+  flag		input: helo seen  output: fail is fatal
+
+Return: NULL on success, or error message
+*/
+
+# define XCLIENT_UNAVAIL     US"[UNAVAILABLE]"
+# define XCLIENT_TEMPUNAVAIL US"[TEMPUNAVAIL]"
+
+uschar *
+xclient_smtp_command(uschar * s, int * resp, BOOL * flag)
+{
+uschar * word = s;
+enum {
+  XCLIENT_READ_COMMAND = 0,
+  XCLIENT_READ_VALUE,
+  XCLIENT_SKIP_SPACES
+} state = XCLIENT_SKIP_SPACES;
+enum xclient_cmd_e cmd;
+
+if (  !flag
+   && verify_check_host(&hosts_require_helo) == OK)
+  {
+  *resp = 503;
+  *flag = FALSE;
+  return US"no HELO/EHLO given";
+  }
+
+/* If already in a proxy session, do not re-check permission.
+Strictly we should avoid doing this for a Proxy-Protocol
+session to avoid mixups. */
+
+if(!proxy_session && verify_check_host(&hosts_xclient) == FAIL)
+  {
+  *resp = 550;
+  *flag = TRUE;
+  return US"XCLIENT command used when not advertised";
+  }
+
+if (sender_address)
+  {
+  *resp = 503;
+  *flag = FALSE;
+  return US"mail transaction in progress";
+  }
+
+if (!*word)
+  {
+  s = US"XCLIENT must have at least one operand";
+  goto fatal_501;
+  }
+
+for (state = XCLIENT_SKIP_SPACES; *s; )
+  switch (state)
+    {
+    case XCLIENT_READ_COMMAND:
+      {
+      int len;
+
+      word = s;
+      while (*s && *s != '=') s++;
+      len = s - word;
+      if (!*s)
+	{
+	s = string_sprintf("XCLIENT: missing value for parameter '%.*s'",
+			  len, word);
+	goto fatal_501;
+	}
+
+      DEBUG(D_transport) debug_printf(" XCLIENT: cmd %.*s\n", len, word);
+      cmd = XCLIENT_CMD_UNKNOWN;
+      for (struct xclient_cmd * x = xclient_cmds + 1;
+	   x < xclient_cmds + nelem(xclient_cmds); x++)
+	if (len == x->len && strncmpic(word, x->str, len) == 0)
+	  {
+	  cmd = x - xclient_cmds;
+	  break;
+	  }
+      if (cmd == XCLIENT_CMD_UNKNOWN)
+	{
+	s = string_sprintf("XCLIENT: unrecognised parameter '%.*s'",
+			  len, word);
+	goto fatal_501;
+	}
+      state = XCLIENT_READ_VALUE;
+      }
+      break;
+
+    case XCLIENT_READ_VALUE:
+      {
+      int old_pool = store_pool;
+      int len;
+      uschar * val;
+
+      word = ++s;			/* skip the = */
+      while (*s && !isspace(*s)) s++;
+      len = s - word;
+
+      DEBUG(D_transport) debug_printf(" XCLIENT: \tvalue %.*s\n", len, word);
+      if (len == 0)
+	{ s = US"XCLIENT: zero-length value for param"; goto fatal_501; }
+
+      if (  len == 13
+	 && (  strncmpic(word, XCLIENT_UNAVAIL, 13) == 0
+	    || strncmpic(word, XCLIENT_TEMPUNAVAIL, 13) == 0
+	 )  )
+	val = NULL;
+
+      else if ((len = xclient_xtextdecode(word, s, &val)) == -1)
+	{
+	s = string_sprintf("failed xtext decode for XCLIENT: '%.*s'", len, word);
+	goto fatal_501;
+	}
+
+      store_pool = POOL_PERM;
+      switch (cmd)
+	{
+	case XCLIENT_CMD_ADDR:
+	  proxy_local_address = sender_host_address;
+	  sender_host_address = val ? string_copyn(val, len) : NULL;
+	  break;
+	case XCLIENT_CMD_NAME:
+	  sender_host_name = val ? string_copyn(val, len) : NULL;
+	  break;
+	case XCLIENT_CMD_PORT:
+	  proxy_local_port = sender_host_port;
+	  sender_host_port = val ? Uatoi(val) : 0;
+	  break;
+	case XCLIENT_CMD_DESTADDR:
+	  proxy_external_address = val ? string_copyn(val, len) : NULL;
+	  break;
+	case XCLIENT_CMD_DESTPORT:
+	  proxy_external_port = val ? Uatoi(val) : 0;
+	  break;
+
+	case XCLIENT_CMD_LOGIN:
+	  if (val)
+	    {
+	    authenticated_id = string_copyn(val, len);
+	    sender_host_authenticated = US"xclient";
+	    authentication_failed = FALSE;
+	    }
+	  else
+	    {
+	    authenticated_id = NULL;
+	    sender_host_authenticated = NULL;
+	    }
+	  break;
+
+# ifdef XCLIENT_V1
+	case XCLIENT_CMD_HELO:
+	  sender_helo_name = val ? string_copyn(val, len) : NULL;
+	  break;
+	case XCLIENT_CMD_PROTO:
+	  if (!val)
+	    { store_pool = old_pool; s = US"missing proto for XCLIENT"; goto fatal_501; }
+	  else if (len == 4 && strncmpic(val, US"SMTP", 4) == 0)
+	    *esmtpflag = FALSE;	/* function arg */
+	  else if (len == 5 && strncmpic(val, US"ESMTP", 5) == 0)
+	    *esmtpflag = TRUE;
+	  else
+	    { store_pool = old_pool; s = US"bad proto for XCLIENT"; goto fatal_501; }
+	  break;
+# endif
+	}
+      store_pool = old_pool;
+      state = XCLIENT_SKIP_SPACES;
+      break;
+      }
+
+    case XCLIENT_SKIP_SPACES:
+      while (*s && isspace (*s)) s++;
+      state = XCLIENT_READ_COMMAND;
+      break;
+
+    default:
+      s = US"unhandled XCLIENT parameter type";
+      goto fatal_501;
+    }
+
+if (!proxy_local_address)
+  { s = US"missing ADDR for XCLIENT"; goto fatal_501; }
+if (!proxy_local_port)
+  { s = US"missing PORT for XCLIENT"; goto fatal_501; }
+if (state != XCLIENT_SKIP_SPACES)
+  { s = US"bad state parsing XCLIENT parameters"; goto fatal_501; }
+
+host_build_sender_fullhost();
+proxy_session = TRUE;
+*resp = 220;
+return NULL;
+
+fatal_501:
+  *flag = TRUE;
+  *resp = 501;
+  return s;
+}
+
+# undef XCLIENT_UNAVAIL
+# undef XCLIENT_TEMPUNAVAIL
+
+
+gstring *
+xclient_smtp_advertise_str(gstring * g)
+{
+g = string_catn(g, US"-XCLIENT ", 8);
+for (int i = 1; i < nelem(xclient_cmds); i++)
+  {
+  g = string_catn(g, US" ", 1);
+  g = string_cat(g, xclient_cmds[i].str);
+  }
+return string_catn(g, US"\r\n", 2);
+}
+
+
+#endif	/*EXPERIMENTAL_XCLIENT*/
+
+/* vi: aw ai sw=2
+*/
+/* End of xclient.c */

commit a31b8b2c91b548cc11f9e482315beb2373211b5e
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Mar 26 16:17:16 2023 +0100

    Make $router_name usable from transport

diff --git a/src/src/deliver.c b/src/src/deliver.c
index f3a406990..ad045c8cc 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -2371,7 +2371,9 @@ if ((pid = exim_fork(US"delivery-local")) == 0)
      addr->local_part, tp->name);
 
     /* Setting these globals in the subprocess means we need never clear them */
-    transport_name = addr->transport->name;
+
+    transport_name = tp->name;
+    if (addr->router) router_name = addr->router->name;
     driver_srcfile = tp->srcfile;
     driver_srcline = tp->srcline;
 
@@ -4663,7 +4665,9 @@ all pipes, so I do not see a reason to use non-blocking IO here
     host_item *h;
 
     /* Setting these globals in the subprocess means we need never clear them */
-    transport_name = addr->transport->name;
+
+    transport_name = tp->name;
+    if (addr->router) router_name = addr->router->name;
     driver_srcfile = tp->srcfile;
     driver_srcline = tp->srcline;
 

commit e093f239f9d0c6e5f590d9979bcbc3e60c540edf
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Mar 29 13:14:14 2023 +0100

    tidying

diff --git a/src/src/EDITME b/src/src/EDITME
index 6344561d6..ac323fe18 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -634,6 +634,9 @@ DISABLE_MAL_MKS=yes
 
 # Uncomment the following line to add queuefile transport support
 # EXPERIMENTAL_QUEUEFILE=yes
+#
+# Uncomment the following line to add XCLIENT support
+# EXPERIMENTAL_XCLIENT=yes
 
 ###############################################################################
 #                 THESE ARE THINGS YOU MIGHT WANT TO SPECIFY                  #

commit d5939cf05037d4a70ca43ec4d436c2e699530444
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Apr 1 19:48:15 2023 +0100

    Docs: options after "run" of ${run } may not have space.  Bug 2932

diff --git a/src/src/expand.c b/src/src/expand.c
index 6dcd45062..9f80439cb 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -5630,7 +5630,6 @@ while (*s)
       /* Handle options to the "run" */
 
       while (*s == ',')
-	{
 	if (Ustrncmp(++s, "preexpand", 9) == 0)
 	  { late_expand = FALSE; s += 9; }
 	else
@@ -5641,7 +5640,6 @@ while (*s)
 						  (int)(t-s), s);
 	  goto EXPAND_FAILED;
 	  }
-	}
       Uskip_whitespace(&s);
 
       if (*s != '{')					/*}*/

commit 8d960c19a447e105a4375c2cbcd0c9493622b6a2
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Apr 1 21:15:24 2023 +0100

    Cutthrough: fix error message for unexpected response from onward connection.  Bug 2912
    Only affects debug.

diff --git a/src/src/verify.c b/src/src/verify.c
index 5c0a3e408..228f63020 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -3,7 +3,7 @@
 *************************************************/
 
 /* Copyright (c) The Exim Maintainers 2020 - 2022 */
-/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* 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 */
 
@@ -1329,7 +1329,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)
 {
@@ -1343,7 +1349,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)
   {

commit f1bf269876f4e32b074d271168edc2af64e1c7a6
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Apr 1 21:47:20 2023 +0100

    Expansions: Fix ${readsocket } to do nicer TLS close

diff --git a/src/src/lookups/readsock.c b/src/src/lookups/readsock.c
index a3f87108a..73cc02813 100644
--- a/src/src/lookups/readsock.c
+++ b/src/src/lookups/readsock.c
@@ -275,6 +275,10 @@ if (!lf.cache) *do_cache = 0;
 
 out:
 
+#ifndef DISABLE_TLS
+if (cctx->tls_ctx) tls_close(cctx->tls_ctx, TLS_SHUTDOWN_NOWAIT);
+#endif
+
 (void) close(cctx->sock);
 cctx->sock = -1;
 return ret;
@@ -294,7 +298,7 @@ readsock_close(void * handle)
 client_conn_ctx * cctx = handle;
 if (cctx->sock < 0) return;
 #ifndef DISABLE_TLS
-if (cctx->tls_ctx) tls_close(cctx->tls_ctx, TRUE);
+if (cctx->tls_ctx) tls_close(cctx->tls_ctx, TLS_SHUTDOWN_NOWAIT);
 #endif
 close(cctx->sock);
 cctx->sock = -1;

commit 12e1cfcb1649e2ea213c2965adf8479f1cff06f7
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Apr 2 22:08:40 2023 +0100

    tidying

diff --git a/src/src/daemon.c b/src/src/daemon.c
index caed44bb3..8ceeac3a6 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -1691,7 +1691,6 @@ int listen_socket_count = 0, poll_fd_count;
 ip_address_item * addresses = NULL;
 time_t last_connection_time = (time_t)0;
 int local_queue_run_max = 0;
-BOOL queue_run_max_has_dollar;
 
 if (is_multiple_qrun())
 
diff --git a/src/src/expand.c b/src/src/expand.c
index 9f80439cb..fe0fd1469 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -27,13 +27,6 @@ typedef unsigned esi_flags;
 # ifndef SUPPORT_CRYPTEQ
 #  define SUPPORT_CRYPTEQ
 # endif
-#else
-
-/* Recursively called function */
-
-static uschar *expand_string_internal(const uschar *, esi_flags, const uschar **, BOOL *, BOOL *);
-static int_eximarith_t expanded_string_integer(const uschar *, BOOL);
-
 #endif	/*!STAND_ALONE*/
 
 #ifdef LOOKUP_LDAP
@@ -942,6 +935,10 @@ static uschar *mtable_sticky[] =
 #define FH_WANT_RAW	BIT(1)
 #define FH_WANT_LIST	BIT(2)
 
+/* Recursively called function */
+static uschar *expand_string_internal(const uschar *, esi_flags, const uschar **, BOOL *, BOOL *);
+static int_eximarith_t expanded_string_integer(const uschar *, BOOL);
+
 
 /*************************************************
 *           Tables for UTF-8 support             *
diff --git a/src/src/regex_cache.c b/src/src/regex_cache.c
index 1ca3c96d5..91ca8ca02 100644
--- a/src/src/regex_cache.c
+++ b/src/src/regex_cache.c
@@ -92,11 +92,9 @@ return node ? node->data.ptr : NULL;
 static void
 regex_to_cache(const uschar * key, BOOL caseless, const pcre2_code * cre)
 {
-PCRE2_SIZE srelen;
-uschar * sre;
-tree_node * node;
 
-node = store_get(sizeof(tree_node) + Ustrlen(key) + 1, key);	/* we are called with STORE_PERM */
+/* we are called with STORE_PERM */
+tree_node * node = store_get(sizeof(tree_node) + Ustrlen(key) + 1, key);
 Ustrcpy(node->name, key);
 node->data.ptr = (void *)cre;
 

commit 51f9c07cd341c9c1a09b3816df988c6f44477c99
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Apr 11 11:59:08 2023 +0100

    Fix ${srs_encode ..} for mod-1024 day zero

diff --git a/src/src/expand.c b/src/src/expand.c
index fe0fd1469..26df25795 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -3523,7 +3523,7 @@ switch(cond_type = identify_operator(&s, &opname))
 
     /* Match the given local_part against the SRS-encoded pattern */
 
-    re = regex_must_compile(US"^(?i)SRS0=([^=]+)=([A-Z2-7]+)=([^=]*)=(.*)$",
+    re = regex_must_compile(US"^(?i)SRS0=([^=]+)=([A-Z2-7]{2})=([^=]*)=(.*)$",
 			    MCS_CASELESS | MCS_CACHEABLE, FALSE);
     md = pcre2_match_data_create(4+1, pcre_gen_ctx);
     if (pcre2_match(re, sub[0], PCRE2_ZERO_TERMINATED, 0, PCRE_EOPT,
@@ -7061,13 +7061,11 @@ while (*s)
 	  {
 	  struct timeval now;
 	  unsigned long i;
-	  gstring * h = NULL;
 
 	  gettimeofday(&now, NULL);
-	  for (unsigned long i = (now.tv_sec / 86400) & 0x3ff; i; i >>= 5)
-	    h = string_catn(h, &base32_chars[i & 0x1f], 1);
-	  if (h) while (h->ptr > 0)
-	    g = string_catn(g, &h->s[--h->ptr], 1);
+	  i = (now.tv_sec / 86400) & 0x3ff;
+	  g = string_catn(g, &base32_chars[i >> 5], 1);
+	  g = string_catn(g, &base32_chars[i & 0x1f], 1);
 	  }
 	g = string_catn(g, US"=", 1);
 

commit 37688315a566d2bfaeae040ee1cbaae3102efced
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Apr 22 00:21:25 2023 +0100

    Docs: clarify TLS cert name verification

diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index f3f70d2e0..76176a64e 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -2620,7 +2620,7 @@ else
      )
     {
     DEBUG(D_tls)
-      debug_printf("TLS certificate verification failed: cert name mismatch\n");
+      debug_printf("TLS certificate verification failed: cert name mismatch (per GnuTLS)\n");
     if (state->verify_requirement >= VERIFY_REQUIRED)
       goto badcert;
     return TRUE;
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 9d0ab2fdf..cd715cc18 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -1192,6 +1192,8 @@ else
     uschar * name;
     int rc;
     while ((name = string_nextinlist(&list, &sep, NULL, 0)))
+      {
+      DEBUG(D_tls|D_lookup) debug_printf_indent("%s suitable for cert, per OpenSSL?", name);
       if ((rc = X509_check_host(cert, CCS name, 0,
 		  X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS
 		  | X509_CHECK_FLAG_SINGLE_LABEL_SUBDOMAINS,
@@ -1203,8 +1205,11 @@ else
 	    tlsp == &tls_out ? deliver_host_address : sender_host_address);
 	  name = NULL;
 	  }
+	DEBUG(D_tls|D_lookup) debug_printf_indent("  yes\n");
 	break;
 	}
+      else DEBUG(D_tls|D_lookup) debug_printf_indent("  no\n");
+      }
     if (!name)
 #else
     if (!tls_is_name_for_cert(verify_cert_hostnames, cert))
diff --git a/src/src/tls.c b/src/src/tls.c
index 825313a9a..8f4344c6c 100644
--- a/src/src/tls.c
+++ b/src/src/tls.c
@@ -670,21 +670,24 @@ Returns:
 BOOL
 tls_is_name_for_cert(const uschar * namelist, void * cert)
 {
-uschar * altnames = tls_cert_subject_altname(cert, US"dns");
-uschar * subjdn;
-uschar * certname;
+uschar * altnames, * subjdn, * certname, * cmpname;
 int cmp_sep = 0;
-uschar * cmpname;
 
 if ((altnames = tls_cert_subject_altname(cert, US"dns")))
   {
   int alt_sep = '\n';
+  DEBUG(D_tls|D_lookup) debug_printf_indent("cert has SAN\n");
   while ((cmpname = string_nextinlist(&namelist, &cmp_sep, NULL, 0)))
     {
     const uschar * an = altnames;
+    DEBUG(D_tls|D_lookup) debug_printf_indent(" %s in SANs?", cmpname);
     while ((certname = string_nextinlist(&an, &alt_sep, NULL, 0)))
       if (is_name_match(cmpname, certname))
+	{
+	DEBUG(D_tls|D_lookup) debug_printf_indent("  yes (matched %s)\n", certname);
 	return TRUE;
+	}
+    DEBUG(D_tls|D_lookup) debug_printf_indent(" no (end of SAN list)\n");
     }
   }
 
@@ -696,13 +699,18 @@ else if ((subjdn = tls_cert_subject(cert, NULL)))
   while ((cmpname = string_nextinlist(&namelist, &cmp_sep, NULL, 0)))
     {
     const uschar * sn = subjdn;
+    DEBUG(D_tls|D_lookup) debug_printf_indent(" %s in SN?", cmpname);
     while ((certname = string_nextinlist(&sn, &sn_sep, NULL, 0)))
       if (  *certname++ == 'C'
 	 && *certname++ == 'N'
 	 && *certname++ == '='
 	 && is_name_match(cmpname, certname)
 	 )
+	{
+	DEBUG(D_tls|D_lookup) debug_printf_indent("  yes (matched %s)\n", certname);
 	return TRUE;
+	}
+    DEBUG(D_tls|D_lookup) debug_printf_indent(" no (end of CN)\n");
     }
   }
 return FALSE;

commit 7533e17a427d6ae51bba9af028b0d9496f487caf
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Apr 23 22:24:30 2023 +0100

    tidying

diff --git a/src/src/daemon.c b/src/src/daemon.c
index 8ceeac3a6..ea7db0f25 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -564,7 +564,7 @@ if (pid == 0)
         smtp_log_no_mail();               /* Log no mail if configured */
         exim_underbar_exit(EXIT_SUCCESS);
         }
-      if (message_id[0] == 0) continue;   /* No message was accepted */
+      if (!message_id[0]) continue;	/* No message was accepted */
       }
     else				/* bad smtp_setup_msg() */
       {
diff --git a/src/src/filter.c b/src/src/filter.c
index d878acb8f..fc6970f23 100644
--- a/src/src/filter.c
+++ b/src/src/filter.c
@@ -1463,22 +1463,21 @@ switch (c->type)
     testing and verification. */
 
   case cond_errormsg:
-    yield = message_id[0] != 0 &&
-      (sender_address == NULL || sender_address[0] == 0);
+    yield = message_id[0] && (!sender_address || !*sender_address);
     break;
 
     /* Only FALSE if a message is actually being processed; TRUE for address
     and filter testing and verification. */
 
   case cond_firsttime:
-    yield = filter_test != FTEST_NONE || message_id[0] == 0 || f.deliver_firsttime;
+    yield = filter_test != FTEST_NONE || !message_id[0] || f.deliver_firsttime;
     break;
 
     /* Only TRUE if a message is actually being processed; FALSE for address
     testing and verification. */
 
   case cond_manualthaw:
-    yield = message_id[0] != 0 && f.deliver_manual_thaw;
+    yield = message_id[0] && f.deliver_manual_thaw;
     break;
 
     /* The foranyaddress condition loops through a list of addresses */
diff --git a/src/src/host.c b/src/src/host.c
index 136ee8953..9c66e9aac 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -2078,11 +2078,11 @@ so we pass that back. */
 if (!host->address)
   {
   uschar *msg =
-    #ifndef STAND_ALONE
+#ifndef STAND_ALONE
     !message_id[0] && smtp_in
       ? string_sprintf("no IP address found for host %s (during %s)", host->name,
           smtp_get_connection_info()) :
-    #endif
+#endif
     string_sprintf("no IP address found for host %s", host->name);
 
   HDEBUG(D_host_lookup) debug_printf("%s\n", msg);
diff --git a/src/src/log.c b/src/src/log.c
index 08ece6158..54d2b8027 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -996,7 +996,7 @@ if (LOGGING(pid))
   if (!syslog_pid) pid_position[1] = g->ptr;		/*  … and end+1 of the PID */
   }
 
-if (f.really_exim && message_id[0] != 0)
+if (f.really_exim && message_id[0])
   g = string_fmt_append(g, "%s ", message_id);
 
 if (flags & LOG_CONFIG)
diff --git a/src/src/receive.c b/src/src/receive.c
index 94fa6d5de..19f8962c6 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -3143,9 +3143,8 @@ if (cutthrough.cctx.sock >= 0 && cutthrough.delivery)
       sender_address,
       sender_fullhost ? "H=" : "", sender_fullhost ? sender_fullhost : US"",
       sender_ident ? "U=" : "", sender_ident ? sender_ident : US"");
-    message_id[0] = 0;                       /* Indicate no message accepted */
     smtp_reply = US"550 Too many \"Received\" headers - suspected mail loop";
-    goto TIDYUP;                             /* Skip to end of function */
+    goto NOT_ACCEPTED;				/* Skip to end of function */
     }
   received_header_gen();
   add_acl_headers(ACL_WHERE_RCPT, US"MAIL or RCPT");
@@ -3242,12 +3241,12 @@ if (!ferror(spool_data_file) && !(receive_feof)() && message_ended != END_DOT)
     case END_EOF:
       if (smtp_input)
 	{
-	Uunlink(spool_name);                 /* Lose data file when closed */
+	Uunlink(spool_name);		/* Lose data file when closed */
 	cancel_cutthrough_connection(TRUE, US"sender closed connection");
-	message_id[0] = 0;                   /* Indicate no message accepted */
+	message_id[0] = 0;		/* Indicate no message_accepted */
 	smtp_reply = handle_lost_connection(US"");
 	smtp_yield = FALSE;
-	goto TIDYUP;                         /* Skip to end of function */
+	goto TIDYUP;				/* Skip to end of function */
 	}
       break;
 
@@ -3272,8 +3271,7 @@ if (!ferror(spool_data_file) && !(receive_feof)() && message_ended != END_DOT)
       if (smtp_input)
 	{
 	smtp_reply = US"552 Message size exceeds maximum permitted";
-	message_id[0] = 0;               /* Indicate no message accepted */
-	goto TIDYUP;                     /* Skip to end of function */
+	goto NOT_ACCEPTED;			/* Skip to end of function */
 	}
       else
 	{
@@ -3291,8 +3289,7 @@ if (!ferror(spool_data_file) && !(receive_feof)() && message_ended != END_DOT)
       Uunlink(spool_name);		/* Lose the data file when closed */
       cancel_cutthrough_connection(TRUE, US"sender protocol error");
       smtp_reply = US"";		/* Response already sent */
-      message_id[0] = 0;		/* Indicate no message accepted */
-      goto TIDYUP;			/* Skip to end of function */
+      goto NOT_ACCEPTED;			/* Skip to end of function */
     }
   }
 
@@ -3333,8 +3330,7 @@ if (fflush(spool_data_file) == EOF || ferror(spool_data_file) ||
       smtp_reply = US"451 Error while writing spool file";
       receive_swallow_smtp();
       }
-    message_id[0] = 0;               /* Indicate no message accepted */
-    goto TIDYUP;                     /* Skip to end of function */
+    goto NOT_ACCEPTED;			/* Skip to end of function */
     }
 
   else
@@ -3567,8 +3563,7 @@ else
 	  if (smtp_handle_acl_fail(ACL_WHERE_DKIM, rc, user_msg, log_msg) != 0)
 	    smtp_yield = FALSE;    /* No more messages after dropped connection */
 	  smtp_reply = US"";       /* Indicate reply already sent */
-	  message_id[0] = 0;       /* Indicate no message accepted */
-	  goto TIDYUP;             /* Skip to end of function */
+	  goto NOT_ACCEPTED;			/* Skip to end of function */
 	  }
         }
       else
@@ -3649,10 +3644,7 @@ else
 			   ? US"accepted"
 			   : US"accepted for some recipients");
       if (recipients_count == 0)
-        {
-        message_id[0] = 0;       /* Indicate no message accepted */
-	goto TIDYUP;
-	}
+	goto NOT_ACCEPTED;
       }
     else
       prdr_requested = FALSE;
@@ -3686,8 +3678,7 @@ else
         if (smtp_handle_acl_fail(ACL_WHERE_DATA, rc, user_msg, log_msg) != 0)
           smtp_yield = FALSE;    /* No more messages after dropped connection */
         smtp_reply = US"";       /* Indicate reply already sent */
-        message_id[0] = 0;       /* Indicate no message accepted */
-        goto TIDYUP;             /* Skip to end of function */
+        goto NOT_ACCEPTED;			/* Skip to end of function */
         }
       }
     }
@@ -3925,9 +3916,8 @@ else
     if (!smtp_batched_input)
       {
       smtp_respond(smtp_code, 3, TRUE, errmsg);
-      message_id[0] = 0;            /* Indicate no message accepted */
       smtp_reply = US"";            /* Indicate reply already sent */
-      goto TIDYUP;                  /* Skip to end of function */
+      goto NOT_ACCEPTED;			/* Skip to end of function */
       }
     else
       moan_smtp_batch(NULL, "%s %s", smtp_code, errmsg);
@@ -4004,8 +3994,7 @@ else
     if (smtp_input)
       {
       smtp_reply = US"451 Error in writing spool file";
-      message_id[0] = 0;          /* Indicate no message accepted */
-      goto TIDYUP;
+      goto NOT_ACCEPTED;
       }
     else
       {
@@ -4035,8 +4024,7 @@ if (fflush(spool_data_file))
   if (smtp_input)
     {
     smtp_reply = US"451 Error in writing spool file";
-    message_id[0] = 0;          /* Indicate no message accepted */
-    goto TIDYUP;
+    goto NOT_ACCEPTED;
     }
   else
     {
@@ -4386,6 +4374,11 @@ if this happens?  We can at least log it; if it is observed on some platform
 then we can think about properly declaring the message not-received. */
 
 
+goto TIDYUP;
+
+NOT_ACCEPTED:
+message_id[0] = 0;				/* Indicate no message accepted */
+
 TIDYUP:
 process_info[process_info_len] = 0;			/* Remove message id */
 if (spool_data_file && cutthrough_done == NOT_TRIED)
diff --git a/src/src/routers/rf_self_action.c b/src/src/routers/rf_self_action.c
index 73d07db40..e5da4cb91 100644
--- a/src/src/routers/rf_self_action.c
+++ b/src/src/routers/rf_self_action.c
@@ -74,7 +74,7 @@ switch (code)
     and where it has come from. Otherwise, during message delivery, the normal
     logging for the address will be sufficient. */
 
-    if (message_id[0] == 0)
+    if (!message_id[0])
       if (sender_fullhost)
 	log_write(0, LOG_MAIN, "%s: %s (while verifying <%s> from host %s)",
 	  msg, addr->domain, addr->address, sender_fullhost);

commit a8786a66feb3c003c74551399b345b1634cc6739
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu May 4 15:41:46 2023 +0100

    Fix variable initialisation in smtp transport.  Bug 2996

diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index c5951832b..c72028ce9 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -4973,7 +4973,7 @@ smtp_transport_closedown(transport_instance *tblock)
 {
 smtp_transport_options_block * ob = SOB tblock->options_block;
 client_conn_ctx cctx;
-smtp_context sx;
+smtp_context sx = {0};
 uschar buffer[256];
 uschar inbuffer[4096];
 uschar outbuffer[16];

commit 138860785cc96f077f052043d46f697abbf87947
Author: Heiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Date:   Tue May 9 16:49:30 2023 +0200

    fix the list URL

