commit eb39a357cb57fdf83b89c9039dd6e7a7983fd5df
Merge: d997c8cd3 dddc92898
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Oct 28 13:26:04 2025 +0000

    Merge branch '4.next'

diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index 5a15b0a6b..e24ef95f2 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -539,8 +539,7 @@ transport-filter.pl: config ../src/transport-filter.src
 # are thrown away by the linker.
 
 OBJ_WITH_CONTENT_SCAN = malware.o mime.o regex.o spam.o spool_mbox.o
-OBJ_EXPERIMENTAL =	bmi_spam.o \
-			dane.o \
+OBJ_EXPERIMENTAL =	dane.o \
 			dcc.o \
 			imap_utf7.o \
 			utf8.o \
@@ -938,7 +937,6 @@ spool_mbox.o:    $(HDRS) spool_mbox.c
 
 # Dependencies for EXPERIMENTAL_* modules
 
-bmi_spam.o:	$(HDRS) bmi_spam.c
 dane.o:		$(HDRS) dane.c dane-openssl.c
 dcc.o:		$(HDRS) dcc.h dcc.c
 imap_utf7.o:	$(HDRS) imap_utf7.c
diff --git a/src/exim_monitor/em_globals.c b/src/exim_monitor/em_globals.c
index 491a9dda6..5627a2277 100644
--- a/src/exim_monitor/em_globals.c
+++ b/src/exim_monitor/em_globals.c
@@ -44,11 +44,6 @@ uschar  actioned_message[24];
 uschar *action_required;
 uschar *alternate_config = NULL;
 
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-int     bmi_run                = 0;
-uschar *bmi_verdicts           = NULL;
-#endif
-
 int     body_max = 20000;
 
 uschar *exim_path              = US BIN_DIRECTORY "/exim"
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index d35962f98..8f55be0b5 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -32,9 +32,9 @@ d="lookups"
 mkdir $d
 cd $d
 # Makefile is generated
-for f in README cdb.c dbmdb.c dnsdb.c dsearch.c ibase.c json.c ldap.c \
+for f in README cdb.c dbmdb.c dnsdb.c dsearch.c json.c ldap.c \
   lmdb.c lsearch.c mysql.c nis.c nisplus.c nmh.c oracle.c passwd.c \
-  pgsql.c readsock.c redis.c spf.c sqlite.c testdb.c whoson.c \
+  pgsql.c psl.c readsock.c redis.c spf.c sqlite.c testdb.c whoson.c \
   lf_functions.h lf_check_file.c lf_quote.c lf_sqlperform.c
 do
   ln -s ../../src/$d/$f $f
@@ -155,7 +155,7 @@ do
 done
 
 # EXPERIMENTAL_*
-for f in  bmi_spam.c bmi_spam.h dcc.c dcc.h dane.c dane-openssl.c \
+for f in  dcc.c dcc.h dane.c dane-openssl.c \
   danessl.h imap_utf7.c utf8.c xclient.c
 do
   ln -s ../src/$f $f
diff --git a/src/scripts/lookups-Makefile b/src/scripts/lookups-Makefile
index 0617e31cd..6555e820c 100755
--- a/src/scripts/lookups-Makefile
+++ b/src/scripts/lookups-Makefile
@@ -101,7 +101,8 @@ want_dynamic() {
 
 want_at_all() {
   local want_name="$1"
-  local re="(LOOKUP|EXPERIMENTAL)_${want_name}[ $tab]*=[ $tab]*."
+  # SUPPORT is here purely for the spf lookup
+  local re="(LOOKUP|EXPERIMENTAL|SUPPORT)_${want_name}[ $tab]*=[ $tab]*."
   env | ${egrep} -q "^$re"
   if [ $? -eq 0 ]; then return 0; fi
   ${egrep} -q "^[ $tab]*$re" "$defs_source"
@@ -156,23 +157,18 @@ exec > "$target"
 sed -n "1,/$tag_marker/p" < "$input"
 
 for name_mod in \
-    CDB DBM:dbmdb DNSDB DSEARCH IBASE JSON LMDB LDAP LSEARCH MYSQL NIS NISPLUS \
-    NMH ORACLE PASSWD PGSQL REDIS SQLITE TESTDB WHOSON
+    CDB DBM:dbmdb DNSDB DSEARCH JSON LMDB LDAP LSEARCH MYSQL NIS NISPLUS \
+    NMH ORACLE PASSWD PGSQL PSL REDIS SQLITE SPF TESTDB WHOSON
 do
   emit_module_rule $name_mod
 done
 
-# Because the variable is SUPPORT_SPF and not LOOKUP_SPF we
-# always include spf.o and compile a dummy if SUPPORT_SPF is not
-# defined.
-
-OBJ="${OBJ} spf.o"
-
 # readsock is always wanted as it implements the ${readsock } expansion
-OBJ="${OBJ} readsock.o"
+OBJ_ALWAYS="${OBJ_ALWAYS} readsock.o"
 
 echo "MODS = $MODS"
 echo "OBJ = $OBJ"
+echo "OBJ_ALWAYS = $OBJ_ALWAYS"
 
 sed -n "/$tag_marker/,\$p" < "$input"
 
diff --git a/src/src/EDITME b/src/src/EDITME
index 3a9b167de..998406359 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -436,7 +436,6 @@ LOOKUP_DNSDB=yes
 
 # LOOKUP_CDB=yes
 # LOOKUP_DSEARCH=yes
-# LOOKUP_IBASE=yes
 # LOOKUP_JSON=yes
 # LOOKUP_LDAP=yes
 # LOOKUP_LMDB=yes
@@ -448,6 +447,7 @@ LOOKUP_DNSDB=yes
 # LOOKUP_ORACLE=yes
 # LOOKUP_PASSWD=yes
 # LOOKUP_PGSQL=yes
+# LOOKUP_PSL=yes
 # LOOKUP_REDIS=yes
 # LOOKUP_SQLITE=yes
 # LOOKUP_SQLITE_PC=sqlite3
@@ -461,9 +461,6 @@ LOOKUP_DNSDB=yes
 # LOOKUP_NWILDLSEARCH=yes
 
 
-# For IBASE you may need:
-#LIBS += -lfbclient
-
 #------------------------------------------------------------------------------
 # If you have set LOOKUP_LDAP, you should set LDAP_LIB_TYPE to indicate
 # which LDAP library you have. Unfortunately, though most of their functions
@@ -512,7 +509,7 @@ SUPPORT_DANE=yes
 # the command for linking Exim itself, not on any auxiliary programs. You
 # don't need to set LOOKUP_INCLUDE if the relevant directories are already
 # specified in INCLUDE. The settings below are just examples; -lpq is for
-# PostgreSQL, -lgds is for Interbase, -lsqlite3 is for SQLite, -lhiredis
+# PostgreSQL, -lsqlite3 is for SQLite, -lhiredis
 # is for Redis, -ljansson for JSON.
 #
 # You do not need to use this for any lookup information added via pkg-config.
@@ -526,15 +523,16 @@ SUPPORT_DANE=yes
 
 # LOOKUP_INCLUDE=-I /usr/local/ldap/include -I /usr/local/mysql/include -I /usr/local/pgsql/include
 # LOOKUP_INCLUDE +=-I /usr/local/include
-# LOOKUP_LIBS=-L/usr/local/lib -lldap -llber -lmysqlclient -lpq -lgds -lsqlite3 -llmdb
+# LOOKUP_LIBS=-L/usr/local/lib -lldap -llber -lmysqlclient -lpq -lsqlite3 -llmdb
 
 # LOOKUP_LIBS=-L/usr/local/lib -lldap -llber
 # Some platforms may need this for LOOKUP_NIS:
 #LOOKUP_LIBS += -lnsl
+
+# These lookup types need appropriate libraries
 #LOOKUP_LIBS += -ljansson
 #LOOKUP_LIBS += -lhiredis
 
-#------------------------------------------------------------------------------
 # If you included LOOKUP_LMDB above you will need the library. Depending
 # on where installed you may also need an include directory
 #
@@ -542,6 +540,7 @@ SUPPORT_DANE=yes
 # LOOKUP_LIBS += -llmdb
 # For dynamic-modules builds, use instead LOOKUP_LMDB_INCLUDE & LOOKUP_LMDB_LIBS
 
+# LOOKUP_PSL will require that SUPPORT_I18N is defined (but needs no libraries)
 
 #------------------------------------------------------------------------------
 # Compiling the Exim monitor: If you want to compile the Exim monitor, a
@@ -694,15 +693,6 @@ DISABLE_MAL_MKS=yes
 # support.  You must have SPF and DKIM support enabled also.
 # EXPERIMENTAL_ARC=yes
 
-# Uncomment the following lines to add Brightmail AntiSpam support. You need
-# to have the Brightmail client SDK installed. Please check the experimental
-# documentation for implementation details. You need to edit the CFLAGS and
-# LDFLAGS lines.
-
-# EXPERIMENTAL_BRIGHTMAIL=yes
-# CFLAGS  += -I/opt/brightmail/bsdk-6.0/include
-# LDFLAGS += -lxml2_single -lbmiclient_single -L/opt/brightmail/bsdk-6.0/lib
-
 # Uncomment the following to include extra information in fail DSN message (bounces)
 # EXPERIMENTAL_DSN_INFO=yes
 
diff --git a/src/src/acl.c b/src/src/acl.c
index 80a17c937..36fb850c8 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -63,9 +63,6 @@ enum { ACLC_ACL,
        ACLC_ADD_HEADER,
        ACLC_ATRN_DOMAINS,
        ACLC_AUTHENTICATED,
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-       ACLC_BMI_OPTIN,
-#endif
        ACLC_CONDITION,
        ACLC_CONTINUE,
        ACLC_CONTROL,
@@ -168,19 +165,6 @@ static condition_def conditions[] = {
 				    ACL_BIT_NOTSMTP_START |
 				    ACL_BIT_CONNECT | ACL_BIT_HELO),
   },
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-  [ACLC_BMI_OPTIN] =		{ US"bmi_optin",	ACD_EXP | ACD_MOD,
-				  FORBIDDEN(ACL_BIT_AUTH |
-				    ACL_BIT_CONNECT | ACL_BIT_HELO |
-				    ACL_BIT_DATA | ACL_BIT_MIME |
-				    ACL_BIT_PRDR |
-				    ACL_BIT_ETRN | ACL_BIT_EXPN |
-				    ACL_BIT_MAILAUTH |
-				    ACL_BIT_MAIL | ACL_BIT_STARTTLS |
-				    ACL_BIT_VRFY | ACL_BIT_PREDATA |
-				    ACL_BIT_NOTSMTP_START),
-  },
-#endif
   [ACLC_CONDITION] =		{ US"condition",	ACD_EXP,
 				  FORBIDDEN(0) },
   [ACLC_CONTINUE] =		{ US"continue",		ACD_EXP | ACD_MOD,
@@ -433,9 +417,6 @@ static condition_module condition_modules[] = {
 
 enum {
   CONTROL_AUTH_UNADVERTISED,
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-  CONTROL_BMI_RUN,
-#endif
   CONTROL_CASEFUL_LOCAL_PART,
   CONTROL_CASELOWER_LOCAL_PART,
   CONTROL_CUTTHROUGH_DELIVERY,
@@ -493,10 +474,6 @@ static control_def controls_list[] = {
 				  (unsigned)
 				  ~(ACL_BIT_CONNECT | ACL_BIT_HELO)
   },
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-[CONTROL_BMI_RUN] =
-  { US"bmi_run",                 FALSE,		0 },
-#endif
 [CONTROL_CASEFUL_LOCAL_PART] =
   { US"caseful_local_part",      FALSE, (unsigned) ~ACL_BIT_RCPT },
 [CONTROL_CASELOWER_LOCAL_PART] =
@@ -3460,17 +3437,6 @@ for (; cb; cb = cb->next)
 	      &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL) : FAIL;
       break;
 
-    #ifdef EXPERIMENTAL_BRIGHTMAIL
-    case ACLC_BMI_OPTIN:
-      {
-      int old_pool = store_pool;
-      store_pool = POOL_PERM;
-      bmi_current_optin = string_copy(arg);
-      store_pool = old_pool;
-      }
-    break;
-    #endif
-
     case ACLC_CONDITION:
     /* 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
@@ -3512,12 +3478,6 @@ for (; cb; cb = cb->next)
 	  f.allow_auth_unadvertised = TRUE;
 	  break;
 
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-	case CONTROL_BMI_RUN:
-	  bmi_run = 1;
-	  break;
-#endif
-
 #ifndef DISABLE_DKIM
 	case CONTROL_DKIM_VERIFY:
 	  f.dkim_disable_verify = TRUE;
diff --git a/src/src/auths/cram_md5.c b/src/src/auths/cram_md5.c
index df861f6c4..2f6a56626 100644
--- a/src/src/auths/cram_md5.c
+++ b/src/src/auths/cram_md5.c
@@ -343,7 +343,7 @@ auth_info cram_md5_auth_info = {
   .options_block =	&auth_cram_md5_option_defaults,
   .options_len =	sizeof(auth_cram_md5_options_block),
   .init =		auth_cram_md5_init,
-# ifdef DYNLOOKUP
+# if AUTH_CRAM_MD5==2
   .dyn_magic =		AUTH_MAGIC,
 # endif
   },
diff --git a/src/src/auths/cyrus_sasl.c b/src/src/auths/cyrus_sasl.c
index 8d39945bc..0ccb1f5fe 100644
--- a/src/src/auths/cyrus_sasl.c
+++ b/src/src/auths/cyrus_sasl.c
@@ -521,7 +521,7 @@ auth_info cyrus_sasl_auth_info = {
   .options_block =	&auth_cyrus_sasl_option_defaults,
   .options_len =	sizeof(auth_cyrus_sasl_options_block),
   .init =		auth_cyrus_sasl_init,
-# ifdef DYNLOOKUP
+# if AUTH_CYRUS_SASL==2
   .dyn_magic =		AUTH_MAGIC,
 # endif
   },
diff --git a/src/src/auths/dovecot.c b/src/src/auths/dovecot.c
index 5319878e2..118e95f29 100644
--- a/src/src/auths/dovecot.c
+++ b/src/src/auths/dovecot.c
@@ -564,7 +564,7 @@ auth_info dovecot_auth_info = {
   .options_block =	&auth_dovecot_option_defaults,
   .options_len =	sizeof(auth_dovecot_options_block),
   .init =		auth_dovecot_init,
-# ifdef DYNLOOKUP
+# if AUTH_DOVECOT==2
   .dyn_magic =		AUTH_MAGIC,
 # endif
   },
diff --git a/src/src/auths/external.c b/src/src/auths/external.c
index 0cdfcdcfe..883c9cae8 100644
--- a/src/src/auths/external.c
+++ b/src/src/auths/external.c
@@ -172,7 +172,7 @@ auth_info external_auth_info = {
   .options_block =	&auth_external_option_defaults,
   .options_len =	sizeof(auth_external_options_block),
   .init =		auth_external_init,
-# ifdef DYNLOOKUP
+# if AUTH_EXTERNAL==2
   .dyn_magic =		AUTH_MAGIC,
 # endif
   },
diff --git a/src/src/auths/gsasl.c b/src/src/auths/gsasl.c
index 9c315fc92..ad4c9b479 100644
--- a/src/src/auths/gsasl.c
+++ b/src/src/auths/gsasl.c
@@ -1073,7 +1073,7 @@ auth_info gsasl_auth_info = {
   .options_block =	&auth_gsasl_option_defaults,
   .options_len =	sizeof(auth_gsasl_options_block),
   .init =		auth_gsasl_init,
-# ifdef DYNLOOKUP
+# if AUTH_GSASL==2
   .dyn_magic =		AUTH_MAGIC,
 # endif
   },
diff --git a/src/src/auths/heimdal_gssapi.c b/src/src/auths/heimdal_gssapi.c
index 8014e61bb..1c79112c3 100644
--- a/src/src/auths/heimdal_gssapi.c
+++ b/src/src/auths/heimdal_gssapi.c
@@ -625,7 +625,7 @@ auth_info heimdal_gssapi_auth_info = {
   .options_block =	&auth_heimdal_gssapi_option_defaults,
   .options_len =	sizeof(auth_heimdal_gssapi_options_block),
   .init =		auth_heimdal_gssapi_init,
-# ifdef DYNLOOKUP
+# if AUTH_HEIMDAL_GSSAPI==2
   .dyn_magic =		AUTH_MAGIC,
 # endif
   },
diff --git a/src/src/auths/plaintext.c b/src/src/auths/plaintext.c
index 598311e65..772b49ff9 100644
--- a/src/src/auths/plaintext.c
+++ b/src/src/auths/plaintext.c
@@ -194,7 +194,7 @@ auth_info plaintext_auth_info = {
   .options_block =	&auth_plaintext_option_defaults,
   .options_len =	sizeof(auth_plaintext_options_block),
   .init =		auth_plaintext_init,
-# ifdef DYNLOOKUP
+# if AUTH_PLAINTEXT==2
   .dyn_magic =		AUTH_MAGIC,
 # endif
   },
@@ -204,5 +204,5 @@ auth_info plaintext_auth_info = {
 .macros_create =	NULL,
 };
 
-#endif	/*AUTH_PLAINTEST*/
+#endif	/*AUTH_PLAINTEXT*/
 /* End of plaintext.c */
diff --git a/src/src/auths/spa.c b/src/src/auths/spa.c
index 8bd95813e..3e2cccde9 100644
--- a/src/src/auths/spa.c
+++ b/src/src/auths/spa.c
@@ -389,7 +389,7 @@ auth_info spa_auth_info = {
   .options_block =	&auth_spa_option_defaults,
   .options_len =	sizeof(auth_spa_options_block),
   .init =		auth_spa_init,
-# ifdef DYNLOOKUP
+# if AUTH_SPA==2
   .dyn_magic =		AUTH_MAGIC,
 # endif
   },
diff --git a/src/src/auths/tls.c b/src/src/auths/tls.c
index b427d29aa..d44750098 100644
--- a/src/src/auths/tls.c
+++ b/src/src/auths/tls.c
@@ -108,7 +108,7 @@ auth_info tls_auth_info = {
   .options_block =	&auth_tls_option_defaults,
   .options_len =	sizeof(auth_tls_options_block),
   .init =		auth_tls_init,
-# ifdef DYNLOOKUP
+# if AUTH_TLS==2
   .dyn_magic =		AUTH_MAGIC,
 # endif
   },
diff --git a/src/src/bmi_spam.c b/src/src/bmi_spam.c
index 03e8defa6..e69de29bb 100644
--- a/src/src/bmi_spam.c
+++ b/src/src/bmi_spam.c
@@ -1,477 +0,0 @@
-/*************************************************
-*     Exim - an Internet mail transport agent    *
-*************************************************/
-
-/* Code for calling Brightmail AntiSpam.
-   Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004
-   License: GPL */
-/* Copyright (c) The Exim Maintainers 2021 - 2022 */
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-
-#include "exim.h"
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-
-#include "bmi_spam.h"
-
-uschar *bmi_current_optin = NULL;
-
-uschar *bmi_process_message(header_line *header_list, int data_fd) {
-  BmiSystem *system = NULL;
-  BmiMessage *message = NULL;
-  BmiError err;
-  BmiErrorLocation err_loc;
-  BmiErrorType err_type;
-  const BmiVerdict *verdict = NULL;
-  FILE *data_file;
-  uschar data_buffer[4096];
-  uschar localhost[] = "127.0.0.1";
-  uschar *host_address;
-  uschar *verdicts = NULL;
-  int i,j;
-
-  err = bmiInitSystem(BMI_VERSION, CS bmi_config_file, &system);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: could not initialize Brightmail system.", (int)err_loc, (int)err_type);
-    return NULL;
-  }
-
-  err = bmiInitMessage(system, &message);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: could not initialize Brightmail message.", (int)err_loc, (int)err_type);
-    bmiFreeSystem(system);
-    return NULL;
-  }
-
-  /* Send IP address of sending host */
-  if (sender_host_address == NULL)
-    host_address = localhost;
-  else
-    host_address = sender_host_address;
-  err = bmiProcessConnection(CS host_address, message);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: bmiProcessConnection() failed (IP %s).", (int)err_loc, (int)err_type, CS host_address);
-    bmiFreeMessage(message);
-    bmiFreeSystem(system);
-    return NULL;
-  };
-
-  /* Send envelope sender address */
-  err = bmiProcessFROM(CS sender_address, message);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: bmiProcessFROM() failed (address %s).", (int)err_loc, (int)err_type, CS sender_address);
-    bmiFreeMessage(message);
-    bmiFreeSystem(system);
-    return NULL;
-  };
-
-  /* Send envelope recipients */
-  for(i=0;i<recipients_count;i++) {
-    recipient_item *r = recipients_list + i;
-    BmiOptin *optin = NULL;
-
-    /* create optin object if optin string is given */
-    if ((r->bmi_optin != NULL) && (Ustrlen(r->bmi_optin) > 1)) {
-      debug_printf("passing bmiOptin string: %s\n", r->bmi_optin);
-      bmiOptinInit(&optin);
-      err = bmiOptinMset(optin, r->bmi_optin, ':');
-      if (bmiErrorIsFatal(err) == BMI_TRUE) {
-        log_write(0, LOG_PANIC|LOG_MAIN,
-                   "bmi warning: [loc %d type %d]: bmiOptinMSet() failed (address '%s', string '%s').", (int)err_loc, (int)err_type, CS r->address, CS r->bmi_optin);
-        if (optin != NULL)
-          bmiOptinFree(optin);
-        optin = NULL;
-      };
-    };
-
-    err = bmiAccumulateTO(CS r->address, optin, message);
-
-    if (optin != NULL)
-      bmiOptinFree(optin);
-
-    if (bmiErrorIsFatal(err) == BMI_TRUE) {
-      err_loc = bmiErrorGetLocation(err);
-      err_type = bmiErrorGetType(err);
-      log_write(0, LOG_PANIC,
-                 "bmi error [loc %d type %d]: bmiAccumulateTO() failed (address %s).", (int)err_loc, (int)err_type, CS r->address);
-      bmiFreeMessage(message);
-      bmiFreeSystem(system);
-      return NULL;
-    };
-  };
-  err = bmiEndTO(message);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: bmiEndTO() failed.", (int)err_loc, (int)err_type);
-    bmiFreeMessage(message);
-    bmiFreeSystem(system);
-    return NULL;
-  };
-
-  /* Send message headers */
-  while (header_list != NULL) {
-    /* skip deleted headers */
-    if (header_list->type == '*') {
-      header_list = header_list->next;
-      continue;
-    };
-    err = bmiAccumulateHeaders(CCS header_list->text, header_list->slen, message);
-    if (bmiErrorIsFatal(err) == BMI_TRUE) {
-      err_loc = bmiErrorGetLocation(err);
-      err_type = bmiErrorGetType(err);
-      log_write(0, LOG_PANIC,
-                 "bmi error [loc %d type %d]: bmiAccumulateHeaders() failed.", (int)err_loc, (int)err_type);
-      bmiFreeMessage(message);
-      bmiFreeSystem(system);
-      return NULL;
-    };
-    header_list = header_list->next;
-  };
-  err = bmiEndHeaders(message);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: bmiEndHeaders() failed.", (int)err_loc, (int)err_type);
-    bmiFreeMessage(message);
-    bmiFreeSystem(system);
-    return NULL;
-  };
-
-  /* Send body */
-  data_file = fdopen(data_fd,"r");
-  do {
-    j = fread(data_buffer, 1, sizeof(data_buffer), data_file);
-    if (j > 0) {
-      err = bmiAccumulateBody(CCS data_buffer, j, message);
-      if (bmiErrorIsFatal(err) == BMI_TRUE) {
-        err_loc = bmiErrorGetLocation(err);
-        err_type = bmiErrorGetType(err);
-        log_write(0, LOG_PANIC,
-                   "bmi error [loc %d type %d]: bmiAccumulateBody() failed.", (int)err_loc, (int)err_type);
-        bmiFreeMessage(message);
-        bmiFreeSystem(system);
-        return NULL;
-      };
-    };
-  } while (j > 0);
-  err = bmiEndBody(message);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: bmiEndBody() failed.", (int)err_loc, (int)err_type);
-    bmiFreeMessage(message);
-    bmiFreeSystem(system);
-    return NULL;
-  };
-
-
-  /* End message */
-  err = bmiEndMessage(message);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: bmiEndMessage() failed.", (int)err_loc, (int)err_type);
-    bmiFreeMessage(message);
-    bmiFreeSystem(system);
-    return NULL;
-  };
-
-  /* Get store for the verdict string.  Since we are processing message data, assume that
-  the verdict is tainted.  XXX this should use a growable-string  */
-
-  verdicts = store_get(1, GET_TAINTED);
-  *verdicts = '\0';
-
-  for ( err = bmiAccessFirstVerdict(message, &verdict);
-        verdict;
-        err = bmiAccessNextVerdict(message, verdict, &verdict) ) {
-    char *verdict_str;
-
-    err = bmiCreateStrFromVerdict(verdict,&verdict_str);
-    if (!store_extend(verdicts,
-	  Ustrlen(verdicts)+1, Ustrlen(verdicts)+1+strlen(verdict_str)+1)) {
-      /* can't allocate more store */
-      return NULL;
-    };
-    if (*verdicts != '\0')
-      Ustrcat(verdicts, US ":");
-    Ustrcat(verdicts, US verdict_str);
-    bmiFreeStr(verdict_str);
-  };
-
-  DEBUG(D_receive) debug_printf("bmi verdicts: %s\n", verdicts);
-
-  if (Ustrlen(verdicts) == 0)
-    return NULL;
-  else
-    return verdicts;
-}
-
-
-int bmi_get_delivery_status(uschar *base64_verdict) {
-  BmiError err;
-  BmiErrorLocation err_loc;
-  BmiErrorType err_type;
-  BmiVerdict *verdict = NULL;
-  int rc = 1;   /* deliver by default */
-
-  /* always deliver when there is no verdict */
-  if (base64_verdict == NULL)
-    return 1;
-
-  /* create verdict from base64 string */
-  err = bmiCreateVerdictFromStr(CS base64_verdict, &verdict);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict);
-    return 1;
-  };
-
-  err = bmiVerdictError(verdict);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    /* deliver normally due to error */
-    rc = 1;
-  }
-  else if (bmiVerdictDestinationIsDefault(verdict) == BMI_TRUE) {
-    /* deliver normally */
-    rc = 1;
-  }
-  else if (bmiVerdictAccessDestination(verdict) == NULL) {
-    /* do not deliver */
-    rc = 0;
-  }
-  else {
-    /* deliver to alternate location */
-    rc = 1;
-  };
-
-  bmiFreeVerdict(verdict);
-  return rc;
-}
-
-
-uschar *bmi_get_alt_location(uschar *base64_verdict) {
-  BmiError err;
-  BmiErrorLocation err_loc;
-  BmiErrorType err_type;
-  BmiVerdict *verdict = NULL;
-  uschar *rc = NULL;
-
-  /* always deliver when there is no verdict */
-  if (base64_verdict == NULL)
-    return NULL;
-
-  /* create verdict from base64 string */
-  err = bmiCreateVerdictFromStr(CS base64_verdict, &verdict);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict);
-    return NULL;
-  };
-
-  err = bmiVerdictError(verdict);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    /* deliver normally due to error */
-    rc = NULL;
-  }
-  else if (bmiVerdictDestinationIsDefault(verdict) == BMI_TRUE) {
-    /* deliver normally */
-    rc = NULL;
-  }
-  else if (bmiVerdictAccessDestination(verdict) == NULL) {
-    /* do not deliver */
-    rc = NULL;
-  }
-  else {
-    /* deliver to alternate location */
-    rc = store_get(strlen(bmiVerdictAccessDestination(verdict))+1, GET_TAINTED);
-    Ustrcpy(rc, bmiVerdictAccessDestination(verdict));
-    rc[strlen(bmiVerdictAccessDestination(verdict))] = '\0';
-  };
-
-  bmiFreeVerdict(verdict);
-  return rc;
-}
-
-uschar *bmi_get_base64_verdict(uschar *bmi_local_part, uschar *bmi_domain) {
-  BmiError err;
-  BmiErrorLocation err_loc;
-  BmiErrorType err_type;
-  BmiVerdict *verdict = NULL;
-  const BmiRecipient *recipient = NULL;
-  const char *verdict_str = NULL;
-  uschar *verdict_ptr;
-  uschar *verdict_buffer = NULL;
-  int sep = 0;
-
-  /* return nothing if there are no verdicts available */
-  if (bmi_verdicts == NULL)
-    return NULL;
-
-  /* allocate room for the b64 verdict string */
-  verdict_buffer = store_get(Ustrlen(bmi_verdicts)+1, GET_TAINTED);
-
-  /* loop through verdicts */
-  verdict_ptr = bmi_verdicts;
-  while ((verdict_str = CCS string_nextinlist(&verdict_ptr, &sep,
-                                          verdict_buffer,
-                                          Ustrlen(bmi_verdicts)+1)) != NULL) {
-
-    /* create verdict from base64 string */
-    err = bmiCreateVerdictFromStr(verdict_str, &verdict);
-    if (bmiErrorIsFatal(err) == BMI_TRUE) {
-      err_loc = bmiErrorGetLocation(err);
-      err_type = bmiErrorGetType(err);
-      log_write(0, LOG_PANIC,
-                 "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, verdict_str);
-      return NULL;
-    };
-
-    /* loop through rcpts for this verdict */
-    for ( recipient = bmiVerdictAccessFirstRecipient(verdict);
-          recipient != NULL;
-          recipient = bmiVerdictAccessNextRecipient(verdict, recipient)) {
-      uschar *rcpt_local_part;
-      uschar *rcpt_domain;
-
-      /* compare address against our subject */
-      rcpt_local_part = US bmiRecipientAccessAddress(recipient);
-      rcpt_domain = Ustrchr(rcpt_local_part,'@');
-      if (rcpt_domain == NULL) {
-        rcpt_domain = US"";
-      }
-      else {
-        *rcpt_domain = '\0';
-        rcpt_domain++;
-      };
-
-      if ( (strcmpic(rcpt_local_part, bmi_local_part) == 0) &&
-           (strcmpic(rcpt_domain, bmi_domain) == 0) ) {
-        /* found verdict */
-        bmiFreeVerdict(verdict);
-        return US verdict_str;
-      };
-    };
-
-    bmiFreeVerdict(verdict);
-  };
-
-  return NULL;
-}
-
-
-uschar *bmi_get_base64_tracker_verdict(uschar *base64_verdict) {
-  BmiError err;
-  BmiErrorLocation err_loc;
-  BmiErrorType err_type;
-  BmiVerdict *verdict = NULL;
-  uschar *rc = NULL;
-
-  /* always deliver when there is no verdict */
-  if (base64_verdict == NULL)
-    return NULL;
-
-  /* create verdict from base64 string */
-  err = bmiCreateVerdictFromStr(CS base64_verdict, &verdict);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict);
-    return NULL;
-  };
-
-  /* create old tracker string from verdict */
-  err = bmiCreateOldStrFromVerdict(verdict, &rc);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: bmiCreateOldStrFromVerdict() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict);
-    return NULL;
-  };
-
-  bmiFreeVerdict(verdict);
-  return rc;
-}
-
-
-int bmi_check_rule(uschar *base64_verdict, uschar *option_list) {
-  BmiError err;
-  BmiErrorLocation err_loc;
-  BmiErrorType err_type;
-  BmiVerdict *verdict = NULL;
-  int rc = 0;
-  uschar *rule_num;
-  uschar *rule_ptr;
-  uschar rule_buffer[32];
-  int sep = 0;
-
-
-  /* no verdict -> no rule fired */
-  if (base64_verdict == NULL)
-    return 0;
-
-  /* create verdict from base64 string */
-  err = bmiCreateVerdictFromStr(CS base64_verdict, &verdict);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    err_loc = bmiErrorGetLocation(err);
-    err_type = bmiErrorGetType(err);
-    log_write(0, LOG_PANIC,
-               "bmi error [loc %d type %d]: bmiCreateVerdictFromStr() failed. [%s]", (int)err_loc, (int)err_type, base64_verdict);
-    return 0;
-  };
-
-  err = bmiVerdictError(verdict);
-  if (bmiErrorIsFatal(err) == BMI_TRUE) {
-    /* error -> no rule fired */
-    bmiFreeVerdict(verdict);
-    return 0;
-  }
-
-  /* loop through numbers */
-  /* option_list doesn't seem to be expanded so cannot be tainted.  If it ever is we
-  will trap here */
-  rule_ptr = option_list;
-  while ((rule_num = string_nextinlist(&rule_ptr, &sep,
-                                       rule_buffer, sizeof(rule_buffer)))) {
-    int rule_int = -1;
-
-    /* try to translate to int */
-    (void)sscanf(rule_num, "%d", &rule_int);
-    if (rule_int > 0) {
-      debug_printf("checking rule #%d\n", rule_int);
-      /* check if rule fired on the message */
-      if (bmiVerdictRuleFired(verdict, rule_int) == BMI_TRUE) {
-        debug_printf("rule #%d fired\n", rule_int);
-        rc = 1;
-        break;
-      };
-    };
-  };
-
-  bmiFreeVerdict(verdict);
-  return rc;
-};
-
-#endif
diff --git a/src/src/bmi_spam.h b/src/src/bmi_spam.h
index bb1c859a9..e69de29bb 100644
--- a/src/src/bmi_spam.h
+++ b/src/src/bmi_spam.h
@@ -1,23 +0,0 @@
-/*************************************************
-*     Exim - an Internet mail transport agent    *
-*************************************************/
-
-/* Code for calling Brightmail AntiSpam.
-   Copyright (c) Tom Kistner <tom@duncanthrax.net> 2004
-   License: GPL */
-/* SPDX-License-Identifier: GPL-2.0-or-later */
-
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-
-#include <bmi_api.h>
-
-extern uschar *bmi_process_message(header_line *, int);
-extern uschar *bmi_get_base64_verdict(uschar *, uschar *);
-extern uschar *bmi_get_base64_tracker_verdict(uschar *);
-extern int bmi_get_delivery_status(uschar *);
-extern uschar *bmi_get_alt_location(uschar *);
-extern int bmi_check_rule(uschar *,uschar *);
-
-extern uschar *bmi_current_optin;
-
-#endif
diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults
index ebf67ad57..05bdcd6d3 100644
--- a/src/src/config.h.defaults
+++ b/src/src/config.h.defaults
@@ -102,7 +102,6 @@ Do not put spaces between # and the 'define'.
 #define LOOKUP_DBM
 #define LOOKUP_DNSDB
 #define LOOKUP_DSEARCH
-#define LOOKUP_IBASE
 #define LOOKUP_JSON
 #define LOOKUP_LDAP
 #define LOOKUP_LMDB
@@ -113,6 +112,7 @@ Do not put spaces between # and the 'define'.
 #define LOOKUP_ORACLE
 #define LOOKUP_PASSWD
 #define LOOKUP_PGSQL
+#define LOOKUP_PSL
 #define LOOKUP_REDIS
 #define LOOKUP_SQLITE
 #define LOOKUP_TESTDB
@@ -217,7 +217,6 @@ Do not put spaces between # and the 'define'.
 
 /* EXPERIMENTAL features */
 #define EXPERIMENTAL_ARC
-#define EXPERIMENTAL_BRIGHTMAIL
 #define EXPERIMENTAL_DCC
 #define EXPERIMENTAL_DSN_INFO
 #define EXPERIMENTAL_NMH
diff --git a/src/src/configure.default b/src/src/configure.default
index 633c6539e..c05ed182e 100644
--- a/src/src/configure.default
+++ b/src/src/configure.default
@@ -260,21 +260,6 @@ host_lookup = *
 dns_dnssec_ok = 1
 
 
-# The settings below cause Exim to make RFC 1413 (ident) callbacks
-# for all incoming SMTP calls. You can limit the hosts to which these
-# calls are made, and/or change the timeout that is used. If you set
-# the timeout to zero, all RFC 1413 calls are disabled. RFC 1413 calls
-# are cheap and can provide useful information for tracing problem
-# messages, but some hosts and firewalls have problems with them.
-# This can result in a timeout instead of an immediate refused
-# connection, leading to delays on starting up SMTP sessions.
-# (The default was reduced from 30s to 5s for release 4.61. and to
-# disabled for release 4.86)
-#
-#rfc1413_hosts = *
-#rfc1413_query_timeout = 5s
-
-
 # Enable an efficiency feature.  We advertise the feature; clients
 # may request to use it.  For multi-recipient mails we then can
 # reject or accept per-user after the message is received.
diff --git a/src/src/daemon.c b/src/src/daemon.c
index a31ef1eb5..824fe0187 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -479,20 +479,9 @@ if (pid == 0)
   signal(SIGTERM, SIG_DFL);
   signal(SIGINT, SIG_DFL);
 
-  /* Attempt to get an id from the sending machine via the RFC 1413
-  protocol. We do this in the sub-process in order not to hold up the
-  main process if there is any delay. Then set up the fullhost information
-  in case there is no HELO/EHLO.
+  /* Set up the fullhost information in case there is no HELO/EHLO. */
 
-  If debugging is enabled only for the daemon, we must turn if off while
-  finding the id, but turn it on again afterwards so that information about the
-  incoming connection is output. */
-
-  if (f.debug_daemon) debug_selector = 0;
-  verify_get_ident(IDENT_PORT);
   host_build_sender_fullhost();
-  debug_selector = save_debug_selector;
-
   DEBUG(D_any)
     debug_printf("Process %d is handling incoming connection from %s\n",
       (int)getpid(), sender_fullhost);
diff --git a/src/src/deliver.c b/src/src/deliver.c
index 21a610859..497c62e81 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -206,13 +206,6 @@ router_var = addr->prop.variables;
 deliver_domain = addr->domain;
 self_hostname = addr->self_hostname;
 
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-bmi_deliver = 1;    /* deliver by default */
-bmi_alt_location = NULL;
-bmi_base64_verdict = NULL;
-bmi_base64_tracker_verdict = NULL;
-#endif
-
 /* If there's only one address we can set everything. */
 
 if (!addr->next)
@@ -269,17 +262,6 @@ if (!addr->next)
       }
     }
 
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-    /* Set expansion variables related to Brightmail AntiSpam */
-    bmi_base64_verdict = bmi_get_base64_verdict(deliver_localpart_orig, deliver_domain_orig);
-    bmi_base64_tracker_verdict = bmi_get_base64_tracker_verdict(bmi_base64_verdict);
-    /* get message delivery status (0 - don't deliver | 1 - deliver) */
-    bmi_deliver = bmi_get_delivery_status(bmi_base64_verdict);
-    /* if message is to be delivered, get eventual alternate location */
-    if (bmi_deliver == 1)
-      bmi_alt_location = bmi_get_alt_location(bmi_base64_verdict);
-#endif
-
   }
 
 /* For multiple addresses, don't set local part, and leave the domain and
@@ -1059,7 +1041,7 @@ else
     s = addr->domain;
 #ifdef SUPPORT_I18N
     if (testflag(addr, af_utf8_downcvt))
-      s = string_localpart_utf8_to_alabel(s, NULL);
+      s = string_domain_utf8_to_alabel(US s, NULL);
 #endif
     g = string_cat(g, s);
     }
@@ -2656,10 +2638,12 @@ if (addr->special_action == SPECIAL_WARN)
 
 /* Check transport for the given concurrency limit.  Return TRUE if over
 the limit (or an expansion failure), else FALSE and if there was a limit,
-the key for the hints database used for the concurrency count. */
+the key for the hints database used for the concurrency count
+and a string for observability. */
 
 static BOOL
-tpt_parallel_check(transport_instance * tp, address_item * addr, uschar ** key)
+tpt_parallel_check(const transport_instance * tp, address_item * addr,
+  uschar ** key, uschar ** state)
 {
 const uschar * trname = tp->drinst.name;
 unsigned max_parallel;
@@ -2679,22 +2663,22 @@ if (expand_string_message)
 if (max_parallel > 0)
   {
   uschar * serialize_key = string_sprintf("tpt-serialize-%s", trname);
-  if (!enq_start(serialize_key, max_parallel))
+  unsigned running;
+  if (!(running = enq_start(serialize_key, max_parallel)))
     {
-    address_item * next;
     DEBUG(D_transport)
       debug_printf("skipping tpt %s because concurrency limit %u reached\n",
 		  trname, max_parallel);
     do
       {
-      next = addr->next;
       addr->message = US"concurrency limit reached for transport";
       addr->basic_errno = ERRNO_TRETRY;
       post_process_one(addr, DEFER, LOG_MAIN, EXIM_DTYPE_TRANSPORT, 0);
-      } while ((addr = next));
+      } while ((addr = addr->next));
     return TRUE;
     }
   *key = serialize_key;
+  *state = string_sprintf(" (%u/max_parallel:%u)", running, max_parallel);
   }
 return FALSE;
 }
@@ -2731,7 +2715,7 @@ while (addr_local)
   int logflags = LOG_MAIN;
   int logchar = f.dont_deliver? '*' : '=';
   transport_instance * tp;
-  uschar * serialize_key = NULL;
+  uschar * serialize_key = NULL, * tpt_dummy;
   const uschar * trname;
 
   /* Pick the first undelivered address off the chain */
@@ -3018,7 +3002,7 @@ while (addr_local)
   We use a hints DB entry, incremented here and decremented after
   the transport (and any shadow transport) completes. */
 
-  if (tpt_parallel_check(tp, addr, &serialize_key))
+  if (tpt_parallel_check(tp, addr, &serialize_key, &tpt_dummy))
     {
     if (expand_string_message)
       {
@@ -3996,20 +3980,22 @@ can be created, or when waiting for the last ones to complete. It must wait for
 the completion of one subprocess, empty the control block slot, and return a
 pointer to the address chain.
 
-Arguments:    none
-Returns:      pointer to the chain of addresses handled by the process;
-              NULL if no subprocess found - this is an unexpected error
+Arguments:
+  reason:	observability: reason for call
+
+Returns:	pointer to the chain of addresses handled by the process;
+		NULL if no subprocess found - this is an unexpected error
 */
 
 static address_item *
-par_wait(void)
+par_wait(const uschar * reason)
 {
 int poffset, status;
 address_item * addrlist;
 pid_t pid;
 
 set_process_info("delivering %s: waiting for a remote delivery subprocess "
-  "to finish", message_id);
+  "to finish (%s)", message_id, reason);
 
 /* Loop until either a subprocess completes, or there are no subprocesses in
 existence - in which case give an error return. We cannot proceed just by
@@ -4256,16 +4242,17 @@ log and proceed as if all done.
 Arguments:
   max         maximum number of subprocesses to leave running
   fallback    TRUE if processing fallback hosts
+  reason      observability: reason for call
 
 Returns:      nothing
 */
 
 static void
-par_reduce(int max, BOOL fallback)
+par_reduce(int max, BOOL fallback, const uschar * reason)
 {
 while (parcount > max)
   {
-  address_item * doneaddr = par_wait();
+  address_item * doneaddr = par_wait(reason);
   if (!doneaddr)
     {
     log_write(0, LOG_MAIN|LOG_PANIC,
@@ -4357,6 +4344,7 @@ static BOOL
 do_remote_deliveries(BOOL fallback)
 {
 int parmax, poffset;
+const uschar * plimit_reason = US"remote_max_parallel";
 
 parcount = 0;    /* Number of executing subprocesses */
 
@@ -4364,7 +4352,8 @@ parcount = 0;    /* Number of executing subprocesses */
 We use a local variable (parmax) to hold the maximum number of processes;
 this gets reduced from remote_max_parallel if we can't create enough pipes. */
 
-if (continue_transport) remote_max_parallel = 1;
+if (continue_transport)
+  { remote_max_parallel = 1; plimit_reason = US"continue_transport"; }
 parmax = remote_max_parallel;
 
 /* If the data for keeping a list of processes hasn't yet been
@@ -4388,12 +4377,12 @@ for (int delivery_count = 0; addr_remote; delivery_count++)
   uid_t uid;
   gid_t gid;
   int pfd[2];
-  int address_count = 1, address_count_max;
+  unsigned address_count = 1, address_count_max;
   BOOL pipe_done = FALSE, multi_domain, use_initgroups;
   transport_instance * tp;
   address_item ** anchor = &addr_remote;
   address_item * addr = addr_remote, * last = addr, * next;
-  uschar * serialize_key = NULL, * panicmsg;
+  uschar * serialize_key = NULL, * tpt_parallel_level, * panicmsg;
 
   /* Pull the first address right off the list. */
 
@@ -4457,7 +4446,7 @@ So look out for the place it gets used.
   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
+  transport splitting further by max_rcpt.  So we potentially lose some
   parallellism. */
 
   GET_OPTION("max_rcpt");
@@ -4573,7 +4562,8 @@ Does that also apply to address_data?
       last = next;
       address_count++;
       }
-    else anchor = &(next->next);
+    else
+      anchor = &next->next;
     deliver_set_expansions(NULL);
     }
 
@@ -4591,7 +4581,8 @@ Does that also apply to address_data?
   The hints DB entry is decremented in par_reduce(), when we reap the
   transport process. */
 
-  if (tpt_parallel_check(tp, addr, &serialize_key))
+  tpt_parallel_level = US"";
+  if (tpt_parallel_check(tp, addr, &serialize_key, &tpt_parallel_level))
     if ((panicmsg = expand_string_message))
       goto panic_continue;
     else
@@ -4788,9 +4779,12 @@ parmax * tpt-max is exceeded? */
 
   while (!pipe_done)
     {
-    if (socketpair(AF_UNIX, SOCK_STREAM, 0, pfd) == 0) pipe_done = TRUE;
-    else if (parcount > 0) parmax = parcount;
-    else break;
+    if (socketpair(AF_UNIX, SOCK_STREAM, 0, pfd) == 0)
+      pipe_done = TRUE;
+    else if (parcount > 0)
+      { parmax = parcount; plimit_reason = US"fildesc limit"; }
+    else
+      break;
 
     /* We need to make the reading end of the pipe non-blocking. There are
     two different options for this. Exim is cunningly (I hope!) coded so
@@ -4811,7 +4805,7 @@ all pipes, so I do not see a reason to use non-blocking IO here
     to finish. If we ran out of file descriptors, parmax will have been reduced
     from its initial value of remote_max_parallel. */
 
-    par_reduce(parmax - 1, fallback);
+    par_reduce(parmax - 1, fallback, plimit_reason);
     }
 
   /* If we failed to create a pipe and there were no processes to wait
@@ -4956,7 +4950,9 @@ do_remote_deliveries par_reduce par_wait par_read_pipe
     of bytes written. */
 
     (void)close(pfd[pipe_read]);
-    set_process_info("delivering %s using %s", message_id, tp->drinst.name);
+    set_process_info("delivering %s (%u addr%s) using %s%s",
+		    message_id, address_count, address_count>1?"s":"",
+		    tp->drinst.name, tpt_parallel_level);
     debug_print_string(tp->debug_string);
 
       {
@@ -5364,7 +5360,7 @@ do_remote_deliveries par_reduce par_wait par_read_pipe
 
   if (continue_transport)
     {
-    par_reduce(0, fallback);
+    par_reduce(0, fallback, US"continue_transport wait-complete");
     if (!*continue_next_id && continue_wait_db)
 	{ dbfn_close_multi(continue_wait_db); continue_wait_db = NULL; }
 
@@ -5393,7 +5389,7 @@ panic_continue:
 /* Reached the end of the list of addresses. Wait for all the subprocesses that
 are still running and post-process their addresses. */
 
-par_reduce(0, fallback);
+par_reduce(0, fallback, US"delivery wait-all-complete");
 return TRUE;
 }
 
diff --git a/src/src/dns.c b/src/src/dns.c
index 4c8f1dd14..dd754f6c9 100644
--- a/src/src/dns.c
+++ b/src/src/dns.c
@@ -841,15 +841,14 @@ if ((rc = dns_fail_cache_hit(name, type)) > 0)
 #ifdef SUPPORT_I18N
 /* Convert all names to a-label form before doing lookup */
   {
-  uschar * alabel;
+  const uschar * alabel;
   uschar * errstr = NULL;
   DEBUG(D_dns) if (string_is_utf8(name))
     debug_printf_indent("convert utf8 '%s' to alabel for for lookup\n", name);
   if ((alabel = string_domain_utf8_to_alabel(name, &errstr)), errstr)
     {
-    DEBUG(D_dns)
-      debug_printf_indent("DNS name '%s' utf8 conversion to alabel failed: %s\n", name,
-        errstr);
+    DEBUG(D_dns) debug_printf_indent(
+      "DNS name '%s' utf8 conversion to alabel failed: %s\n", name, errstr);
     f.host_find_failed_syntax = TRUE;
     return DNS_NOMATCH;
     }
diff --git a/src/src/drtables.c b/src/src/drtables.c
index adbf7d539..5993b5c29 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -31,200 +31,52 @@ transport_info * transports_available = NULL;
 
 #ifndef MACRO_PREDEF
 
-gstring *
-auth_show_supported(gstring * g)
+static gstring *
+dr_show_list(gstring * g, const uschar ** list, const uschar * label,
+  const uschar * class)
 {
-uschar * b = US""               /* static-build authenticatornames */
-#if defined(AUTH_CRAM_MD5) && AUTH_CRAM_MD5!=2
-  " cram_md5"
-#endif
-#if defined(AUTH_CYRUS_SASL) && AUTH_CYRUS_SASL!=2
-  " cyrus_sasl"
-#endif
-#if defined(AUTH_DOVECOT) && AUTH_DOVECOT!=2
-  " dovecot"
-#endif
-#if defined(AUTH_EXTERNAL) && AUTH_EXTERNAL!=2
-  " external"
-#endif
-#if defined(AUTH_GSASL) && AUTH_GSASL!=2
-  " gsasl"
-#endif
-#if defined(AUTH_HEIMDAL_GSSAPI) && AUTH_HEIMDAL_GSSAPI!=2
-  " heimdal_gssapi"
-#endif
-#if defined(AUTH_PLAINTEXT) && AUTH_PLAINTEXT!=2
-  " plaintext"
-#endif
-#if defined(AUTH_SPA) && AUTH_SPA!=2
-  " spa"
-#endif
-#if defined(AUTH_TLS) && AUTH_TLS!=2
-  " tls"
-#endif
-  ;
-
-uschar * d = US""		/* dynamic-module authenticator names */
-#if defined(AUTH_CRAM_MD5) && AUTH_CRAM_MD5==2
-  " cram_md5"
-#endif
-#if defined(AUTH_CYRUS_SASL) && AUTH_CYRUS_SASL==2
-  " cyrus_sasl"
-#endif
-#if defined(AUTH_DOVECOT) && AUTH_DOVECOT==2
-  " dovecot"
-#endif
-#if defined(AUTH_EXTERNAL) && AUTH_EXTERNAL==2
-  " external"
-#endif
-#if defined(AUTH_GSASL) && AUTH_GSASL==2
-  " gsasl"
-#endif
-#if defined(AUTH_HEIMDAL_GSSAPI) && AUTH_HEIMDAL_GSSAPI==2
-  " heimdal_gssapi"
-#endif
-#if defined(AUTH_PLAINTEXT) && AUTH_PLAINTEXT==2
-  " plaintext"
-#endif
-#if defined(AUTH_SPA) && AUTH_SPA==2
-  " spa"
-#endif
-#if defined(AUTH_TLS) && AUTH_TLS==2
-  " tls"
-#endif
-  ;
+if (*list)
+  {
+  const uschar ** ele = list;
+  g = string_fmt_append(g, "%s (%s): ", class, label);
+  while (*ele) ele++;
+  while (--ele >= list) g = string_fmt_append(g, " %s", *ele);
+  g = string_catn(g, US"\n", 1);
+  }
+return g;
+}
 
-if (*b) g = string_fmt_append(g, "Authenticators (built-in):%s\n", b);
-if (*d) g = string_fmt_append(g, "Authenticators (dynamic): %s\n", d);
+static gstring *
+dr_show_supported(gstring * g,
+  const uschar ** statics, const uschar ** dynamics, const uschar * class)
+{
+g = dr_show_list(g, statics, US"built-in", class);
+g = dr_show_list(g, dynamics, US"dynamic", class);
 return g;
 }
 
 gstring *
-route_show_supported(gstring * g)
+auth_show_supported(gstring * g)
 {
-uschar * b = US""		/* static-build router names */
-#if defined(ROUTER_ACCEPT) && ROUTER_ACCEPT!=2
-  " accept"
-#endif
-#if defined(ROUTER_DNSLOOKUP) && ROUTER_DNSLOOKUP!=2
-  " dnslookup"
-#endif
-# if defined(ROUTER_IPLITERAL) && ROUTER_IPLITERAL!=2
-  " ipliteral"
-#endif
-#if defined(ROUTER_IPLOOKUP) && ROUTER_IPLOOKUP!=2
-  " iplookup"
-#endif
-#if defined(ROUTER_MANUALROUTE) && ROUTER_MANUALROUTE!=2
-  " manualroute"
-#endif
-#if defined(ROUTER_REDIRECT) && ROUTER_REDIRECT!=2
-  " redirect"
-#endif
-#if defined(ROUTER_QUERYPROGRAM) && ROUTER_QUERYPROGRAM!=2
-  " queryprogram"
-#endif
-  ;
-
-uschar * d = US""		/* dynamic-module router names */
-#if defined(ROUTER_ACCEPT) && ROUTER_ACCEPT==2
-  " accept"
-#endif
-#if defined(ROUTER_DNSLOOKUP) && ROUTER_DNSLOOKUP==2
-  " dnslookup"
-#endif
-# if defined(ROUTER_IPLITERAL) && ROUTER_IPLITERAL==2
-  " ipliteral"
-#endif
-#if defined(ROUTER_IPLOOKUP) && ROUTER_IPLOOKUP==2
-  " iplookup"
-#endif
-#if defined(ROUTER_MANUALROUTE) && ROUTER_MANUALROUTE==2
-  " manualroute"
-#endif
-#if defined(ROUTER_REDIRECT) && ROUTER_REDIRECT==2
-  " redirect"
-#endif
-#if defined(ROUTER_QUERYPROGRAM) && ROUTER_QUERYPROGRAM==2
-  " queryprogram"
-#endif
-  ;
+return dr_show_supported(g, avail_static_auths, avail_dynamic_auths,
+  US"Authenticators");
+}
 
-if (*b) g = string_fmt_append(g, "Routers (built-in):%s\n", b);
-if (*d) g = string_fmt_append(g, "Routers (dynamic): %s\n", d);
-return g;
+gstring *
+route_show_supported(gstring * g)
+{
+return dr_show_supported(g, avail_static_routers, avail_dynamic_routers,
+  US"Routers");
 }
 
 gstring *
 transport_show_supported(gstring * g)
 {
-uschar * b = US""		/* static-build transportnames */
-#if defined(TRANSPORT_APPENDFILE) && TRANSPORT_APPENDFILE!=2
-  " appendfile"
-# ifdef SUPPORT_MAILDIR
-    "/maildir"
-# endif
-# ifdef SUPPORT_MAILSTORE
-    "/mailstore"
-# endif
-# ifdef SUPPORT_MBX
-    "/mbx"
-# endif
-#endif
-#if defined(TRANSPORT_AUTOREPLY) && TRANSPORT_AUTOREPLY!=2
-  " autoreply"
-#endif
-#if defined(TRANSPORT_LMTP) && TRANSPORT_LMTP!=2
-  " lmtp"
-#endif
-#if defined(TRANSPORT_PIPE) && TRANSPORT_PIPE!=2
-  " pipe"
-#endif
-#if defined(EXPERIMENTAL_QUEUEFILE) && EXPERIMENTAL_QUEUEFILE!=2
-  " queuefile"
-#endif
-#if defined(TRANSPORT_SMTP) && TRANSPORT_SMTP!=2
-  " smtp"
-#endif
-  ;
-
-uschar * d = US""		/* dynamic-module transportnames */
-#if defined(TRANSPORT_APPENDFILE) && TRANSPORT_APPENDFILE==2
-  " appendfile"
-# ifdef SUPPORT_MAILDIR
-    "/maildir"
-# endif
-# ifdef SUPPORT_MAILSTORE
-    "/mailstore"
-# endif
-# ifdef SUPPORT_MBX
-    "/mbx"
-# endif
-#endif
-#if defined(TRANSPORT_AUTOREPLY) && TRANSPORT_AUTOREPLY==2
-  " autoreply"
-#endif
-#if defined(TRANSPORT_LMTP) && TRANSPORT_LMTP==2
-  " lmtp"
-#endif
-#if defined(TRANSPORT_PIPE) && TRANSPORT_PIPE==2
-  " pipe"
-#endif
-#if defined(EXPERIMENTAL_QUEUEFILE) && EXPERIMENTAL_QUEUEFILE==2
-  " queuefile"
-#endif
-#if defined(TRANSPORT_SMTP) && TRANSPORT_SMTP==2
-  " smtp"
-#endif
-  ;
-
-if (*b) g = string_fmt_append(g, "Transports (built-in):%s\n", b);
-if (*d) g = string_fmt_append(g, "Transports (dynamic): %s\n", d);
-return g;
+return dr_show_supported(g, avail_static_transports, avail_dynamic_transports,
+  US"Transports");
 }
 
 
-
 static void
 add_lookup_to_tree(lookup_info * li)
 {
@@ -271,77 +123,6 @@ return li;
 
 
 
-/* These need to be at file level for old versions of gcc (2.95.2 reported),
-which give parse errors on an extern in function scope.  Each entry needs
-to also be invoked in init_lookup_list() below  */
-
-#if defined(LOOKUP_CDB) && LOOKUP_CDB!=2
-extern lookup_module_info cdb_lookup_module_info;
-#endif
-#if defined(LOOKUP_DBM) && LOOKUP_DBM!=2
-extern lookup_module_info dbmdb_lookup_module_info;
-#endif
-#if defined(LOOKUP_DNSDB) && LOOKUP_DNSDB!=2
-extern lookup_module_info dnsdb_lookup_module_info;
-#endif
-#if defined(LOOKUP_DSEARCH) && LOOKUP_DSEARCH!=2
-extern lookup_module_info dsearch_lookup_module_info;
-#endif
-#if defined(LOOKUP_IBASE) && LOOKUP_IBASE!=2
-extern lookup_module_info ibase_lookup_module_info;
-#endif
-#if defined(LOOKUP_JSON) && LOOKUP_JSON!=2
-extern lookup_module_info json_lookup_module_info;
-#endif
-#if defined(LOOKUP_LDAP) && LOOKUP_LDAP!=2
-extern lookup_module_info ldap_lookup_module_info;
-#endif
-#if defined(LOOKUP_LSEARCH) && LOOKUP_LSEARCH!=2
-extern lookup_module_info lsearch_lookup_module_info;
-#endif
-#if defined(LOOKUP_MYSQL) && LOOKUP_MYSQL!=2
-extern lookup_module_info mysql_lookup_module_info;
-#endif
-#if defined(LOOKUP_NIS) && LOOKUP_NIS!=2
-extern lookup_module_info nis_lookup_module_info;
-#endif
-#if defined(LOOKUP_NISPLUS) && LOOKUP_NISPLUS!=2
-extern lookup_module_info nisplus_lookup_module_info;
-#endif
-#if defined(EXPERIMENTAL_NMH) && EXPERIMENTAL_NMH!=2
-extern lookup_module_info nmh_lookup_module_info;
-#endif
-#if defined(LOOKUP_ORACLE) && LOOKUP_ORACLE!=2
-extern lookup_module_info oracle_lookup_module_info;
-#endif
-#if defined(LOOKUP_PASSWD) && LOOKUP_PASSWD!=2
-extern lookup_module_info passwd_lookup_module_info;
-#endif
-#if defined(LOOKUP_PGSQL) && LOOKUP_PGSQL!=2
-extern lookup_module_info pgsql_lookup_module_info;
-#endif
-#if defined(LOOKUP_REDIS) && LOOKUP_REDIS!=2
-extern lookup_module_info redis_lookup_module_info;
-#endif
-#if defined(LOOKUP_LMDB) && LOOKUP_LMDB!=2
-extern lookup_module_info lmdb_lookup_module_info;
-#endif
-#if defined(EXIM_HAVE_SPF)
-extern lookup_module_info spf_lookup_module_info;	/* see below */
-#endif
-#if defined(LOOKUP_SQLITE) && LOOKUP_SQLITE!=2
-extern lookup_module_info sqlite_lookup_module_info;
-#endif
-#if defined(LOOKUP_TESTDB) && LOOKUP_TESTDB!=2
-extern lookup_module_info testdb_lookup_module_info;
-#endif
-#if defined(LOOKUP_WHOSON) && LOOKUP_WHOSON!=2
-extern lookup_module_info whoson_lookup_module_info;
-#endif
-
-extern lookup_module_info readsock_lookup_module_info;
-
-
 #ifdef LOOKUP_MODULE_DIR
 static void *
 mod_open(const uschar * name, const uschar * class, uschar ** errstr)
@@ -381,13 +162,13 @@ static BOOL
 lookup_mod_load(const uschar * name, uschar ** errstr)
 {
 void * dl;
-struct lookup_module_info * info;
+lookup_module_info * info;
 const char * errormsg;
 
 if (!(dl = mod_open(name, US"lookup", errstr)))
   return FALSE;
 
-info = (struct lookup_module_info *) dlsym(dl, "_lookup_module_info");
+info = (lookup_module_info *) dlsym(dl, "_lookup_module_info");
 if ((errormsg = dlerror()))
   {
   EARLY_DEBUG(D_any, "%s does not appear to be a lookup module (%s)\n", name, errormsg);
@@ -426,6 +207,45 @@ return TRUE;
 
 #endif	/*LOOKUP_MODULE_DIR*/
 
+/* Look at all the lookup module files and add a name from each lookup type */
+
+gstring *
+lookup_dynamic_supported(gstring * g)
+{
+#ifdef LOOKUP_MODULE_DIR
+DIR * dd;
+const pcre2_code * regex_islookupmod = regex_must_compile(
+  US"^([a-z0-9]+)_lookup\\." DYNLIB_FN_EXT "$", MCS_NOFLAGS, TRUE);
+
+if (!(dd = exim_opendir(CUS LOOKUP_MODULE_DIR)))
+  g = string_cat(g, US"FAIL exim_opendir");
+else
+  for (struct dirent * ent; ent = readdir(dd); )
+    {
+    void * dl;
+    uschar * errstr;
+
+    if (  regex_match_and_setup(regex_islookupmod, US ent->d_name, 0, 0)
+       && (dl = mod_open(expand_nstring[1], US"lookup", &errstr))
+       )
+      {
+      lookup_module_info * lmi=
+	(lookup_module_info *) dlsym(dl, "_lookup_module_info");
+
+      if (  ! dlerror()
+	 && lmi->magic == LOOKUP_MODULE_INFO_MAGIC
+         )
+	for (lookup_info ** lip = lmi->lookups;
+	    lip < lmi->lookups + lmi->lookupcount; lip++)
+	  g = string_fmt_append(g, " %s", (*lip)->name);
+
+      dlclose(dl);
+      }
+    }
+#endif	/*!LOOKUP_MODULE_DIR*/
+return g;
+}
+
 
 
 misc_module_info * misc_module_list = NULL;
@@ -612,99 +432,8 @@ if (lookup_list_init_done)
   return;
 lookup_list_init_done = TRUE;
 
-#if defined(LOOKUP_CDB) && LOOKUP_CDB!=2
-addlookupmodule(&cdb_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_DBM) && LOOKUP_DBM!=2
-addlookupmodule(&dbmdb_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_DNSDB) && LOOKUP_DNSDB!=2
-addlookupmodule(&dnsdb_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_DSEARCH) && LOOKUP_DSEARCH!=2
-addlookupmodule(&dsearch_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_IBASE) && LOOKUP_IBASE!=2
-addlookupmodule(&ibase_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_LDAP) && LOOKUP_LDAP!=2
-addlookupmodule(&ldap_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_JSON) && LOOKUP_JSON!=2
-addlookupmodule(&json_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_LSEARCH) && LOOKUP_LSEARCH!=2
-addlookupmodule(&lsearch_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_MYSQL) && LOOKUP_MYSQL!=2
-addlookupmodule(&mysql_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_NIS) && LOOKUP_NIS!=2
-addlookupmodule(&nis_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_NISPLUS) && LOOKUP_NISPLUS!=2
-addlookupmodule(&nisplus_lookup_module_info);
-#endif
-
-#if defined(EXPERIMENTAL_NMH) && EXPERIMENTAL_NMH!=2
-addlookupmodule(&nmh_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_ORACLE) && LOOKUP_ORACLE!=2
-addlookupmodule(&oracle_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_PASSWD) && LOOKUP_PASSWD!=2
-addlookupmodule(&passwd_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_PGSQL) && LOOKUP_PGSQL!=2
-addlookupmodule(&pgsql_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_REDIS) && LOOKUP_REDIS!=2
-addlookupmodule(&redis_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_LMDB) && LOOKUP_LMDB!=2
-addlookupmodule(&lmdb_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_SQLITE) && LOOKUP_SQLITE!=2
-addlookupmodule(&sqlite_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_TESTDB) && LOOKUP_TESTDB!=2
-addlookupmodule(&testdb_lookup_module_info);
-#endif
-
-#if defined(LOOKUP_WHOSON) && LOOKUP_WHOSON!=2
-addlookupmodule(&whoson_lookup_module_info);
-#endif
-
-/* This is provided by the spf "misc" module, and the lookup aspect is always
-linked statically whether or not the "misc" module (and hence libspf2) is
-dynamic-load. */
-
-#ifdef EXIM_HAVE_SPF
-addlookupmodule(&spf_lookup_module_info);
-#endif
-
-/* This is a custom expansion, and not available as either
-a list-syntax lookup or a lookup expansion. However, it is
-implemented by a lookup module. */
-
-addlookupmodule(&readsock_lookup_module_info);
+for (lookup_module_info ** avi = avail_static_lookups; *avi; avi++)
+  addlookupmodule(*avi);
 
 DEBUG(D_lookup) debug_printf_indent("Total %d built-in lookups\n", lookup_list_count);
 
@@ -727,10 +456,7 @@ else
 
   EARLY_DEBUG(D_lookup, "Loading lookup modules from %s\n", LOOKUP_MODULE_DIR);
   while ((ent = readdir(dd)))
-    {
-    char * name = ent->d_name;
-    int len = (int)strlen(name);
-    if (regex_match_and_setup(regex_islookupmod, US name, 0, 0))
+    if (regex_match_and_setup(regex_islookupmod, US ent->d_name, 0, 0))
       {
       uschar * errstr;
       if (lookup_mod_load(expand_nstring[1], &errstr))
@@ -741,7 +467,6 @@ else
 	log_write(0, LOG_MAIN|LOG_PANIC, "%s", errstr);
 	}
       }
-    }
   closedir(dd);
   }
 
diff --git a/src/src/enq.c b/src/src/enq.c
index 842f2d6ba..8129b3b2b 100644
--- a/src/src/enq.c
+++ b/src/src/enq.c
@@ -23,24 +23,24 @@
 connections. It is also called when ETRN is listed for serialization. We open
 the misc database and look for a record, which implies an existing connection
 or ETRN run. If increasing the count would take us past the given limit
-value return FALSE.  If not, bump it and return TRUE.  If not found, create
+value return FALSE.  If not, bump it and return the new count.  If not found, create
 one with value 1 and return TRUE.
 
 Arguments:
   key            string on which to serialize
   lim            parallelism limit
 
-Returns:         TRUE if OK to proceed; FALSE otherwise
+Returns:         >0 if OK to proceed; 0 otherwise
 */
 
 
-BOOL
-enq_start(uschar *key, unsigned lim)
+unsigned
+enq_start(uschar * key, unsigned lim)
 {
 const dbdata_serialize * serial_record;
 dbdata_serialize new_record;
 open_db dbblock;
-open_db *dbm_file;
+open_db * dbm_file;
 
 DEBUG(D_transport) debug_printf("check serialized: %s\n", key);
 
@@ -60,7 +60,7 @@ if (serial_record && time(NULL) - serial_record->time_stamp < 6*60*60)
     dbfn_close(dbm_file);
     DEBUG(D_transport) debug_printf("outstanding serialization record for %s\n",
       key);
-    return FALSE;
+    return 0;
     }
   new_record.count = serial_record->count + 1;
   }
@@ -73,7 +73,7 @@ DEBUG(D_transport) debug_printf("write serialization record for %s val %d\n",
       key, new_record.count);
 dbfn_write(dbm_file, key, &new_record, (int)sizeof(dbdata_serialize));
 dbfn_close(dbm_file);
-return TRUE;
+return new_record.count;
 }
 
 
diff --git a/src/src/exim.c b/src/src/exim.c
index b1d77576f..88ed33a8e 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1074,153 +1074,12 @@ lookup_show_supported(gstring * g)
 {
 gstring * b = NULL, * d = NULL;
 
-#ifdef LOOKUP_LSEARCH
-# if LOOKUP_LSEARCH!=2
-  b = string_cat(b, US" lsearch wildlsearch nwildlsearch iplsearch");
-# else
-  d = string_cat(d, US" lsearch wildlsearch nwildlsearch iplsearch");
-# endif
-#endif
-#ifdef LOOKUP_CDB
-# if LOOKUP_CDB!=2
-  b = string_cat(b, US" cdb");
-# else
-  d = string_cat(d, US" cdb");
-# endif
-#endif
-#ifdef LOOKUP_DBM
-# if LOOKUP_DBM!=2
-  b = string_cat(b, US" dbm dbmjz dbmnz");
-# else
-  d = string_cat(d, US" dbm dbmjz dbmnz");
-# endif
-#endif
-#ifdef LOOKUP_DNSDB
-# if LOOKUP_DNSDB!=2
-  b = string_cat(b, US" dnsdb");
-# else
-  d = string_cat(d, US" dnsdb");
-# endif
-#endif
-#ifdef LOOKUP_DSEARCH
-# if LOOKUP_DSEARCH!=2
-  b = string_cat(b, US" dsearch");
-# else
-  d = string_cat(d, US" dsearch");
-# endif
-#endif
-#ifdef LOOKUP_IBASE
-# if LOOKUP_IBASE!=2
-  b = string_cat(b, US" ibase");
-# else
-  d = string_cat(d, US" ibase");
-# endif
-#endif
-#ifdef LOOKUP_JSON
-# if LOOKUP_JSON!=2
-  b = string_cat(b, US" json");
-# else
-  d = string_cat(d, US" json");
-# endif
-#endif
-#ifdef LOOKUP_LDAP
-# if LOOKUP_LDAP!=2
-  b = string_cat(b, US" ldap ldapdn ldapm");
-# else
-  d = string_cat(d, US" ldap ldapdn ldapm");
-# endif
-#endif
-#ifdef LOOKUP_LMDB
-# if LOOKUP_LMDB!=2
-  b = string_cat(b, US" lmdb");
-# else
-  d = string_cat(d, US" lmdb");
-# endif
-#endif
-#ifdef LOOKUP_MYSQL
-# if LOOKUP_MYSQL!=2
-  b = string_cat(b, US" mysql");
-# else
-  d = string_cat(d, US" mysql");
-# endif
-#endif
-#ifdef LOOKUP_NIS
-# if LOOKUP_NIS!=2
-  b = string_cat(b, US" nis nis0");
-# else
-  d = string_cat(d, US" nis nis0");
-# endif
-#endif
-#ifdef LOOKUP_NISPLUS
-# if LOOKUP_NISPLUS!=2
-  b = string_cat(b, US" nisplus");
-# else
-  d = string_cat(d, US" nisplus");
-# endif
-#endif
-#ifdef EXPERIMENTAL_NMH
-# if EXPERIMENTAL_NMH!=2
-  b = string_cat(b, US" nmh");
-# else
-  d = string_cat(d, US" nmh");
-# endif
-#endif
-#ifdef LOOKUP_ORACLE
-# if LOOKUP_ORACLE!=2
-  b = string_cat(b, US" oracle");
-# else
-  d = string_cat(d, US" oracle");
-# endif
-#endif
-#ifdef LOOKUP_PASSWD
-# if LOOKUP_PASSWD!=2
-  b = string_cat(b, US" passwd");
-# else
-  d = string_cat(d, US" passwd");
-# endif
-#endif
-#ifdef LOOKUP_PGSQL
-# if LOOKUP_PGSQL!=2
-  b = string_cat(b, US" pgsql");
-# else
-  d = string_cat(d, US" pgsql");
-# endif
-#endif
-#ifdef LOOKUP_REDIS
-# if LOOKUP_REDIS!=2
-  b = string_cat(b, US" redis");
-# else
-  d = string_cat(d, US" redis");
-# endif
-#endif
-#ifdef EXIM_HAVE_SPF
-# if EXIM_HAVE_SPF !=2
-  b = string_cat(b, US" spf");
-# else
-  d = string_cat(d, US" spf");
-# endif
-#endif
-#ifdef LOOKUP_SQLITE
-# if LOOKUP_SQLITE!=2
-  b = string_cat(b, US" sqlite");
-# else
-  d = string_cat(d, US" sqlite");
-# endif
-#endif
-#ifdef LOOKUP_TESTDB
-# if LOOKUP_TESTDB!=2
-  b = string_cat(b, US" testdb");
-# else
-  d = string_cat(d, US" testdb");
-# endif
-#endif
-#ifdef LOOKUP_WHOSON
-# if LOOKUP_WHOSON!=2
-  b = string_cat(b, US" whoson");
-# else
-  d = string_cat(d, US" whoson");
-# endif
-#endif
+for (lookup_module_info ** lmip = avail_static_lookups; *lmip; lmip++)
+  for (lookup_info ** lip = (*lmip)->lookups;
+      lip < (*lmip)->lookups + (*lmip)->lookupcount; lip++)
+    b = string_fmt_append(b, " %s", (*lip)->name);
+
+d = lookup_dynamic_supported(d);
 
 if (b) g = string_fmt_append(g, "Lookups (built-in):%Y\n", b);
 if (d) g = string_fmt_append(g, "Lookups (dynamic): %Y\n", d);
@@ -1369,9 +1228,6 @@ g = string_cat(g, US"Support for:");
 #ifdef EXPERIMENTAL_ARC
   g = string_cat(g, US" Experimental_ARC");
 #endif
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-  g = string_cat(g, US" Experimental_Brightmail");
-#endif
 #ifdef EXPERIMENTAL_DCC
   g = string_cat(g, US" Experimental_DCC");
 #endif
@@ -1965,7 +1821,6 @@ BOOL arg_queue_only = FALSE;
 BOOL bi_option = FALSE;
 BOOL checking = FALSE;
 BOOL count_queue = FALSE;
-BOOL expansion_test = FALSE;
 BOOL extract_recipients = FALSE;
 BOOL flag_G = FALSE;
 BOOL flag_n = FALSE;
@@ -2483,7 +2338,7 @@ on the second character (the one after '-'), to save some effort. */
 	   -bem: Ditto, but read a message from a file first
 	*/
 	case 'e':
-	  expansion_test = checking = TRUE;
+	  f.expansion_test = checking = TRUE;
 	  if (*argrest == 'm')
 	    {
 	    if (++i >= argc) { badarg = TRUE; break; }
@@ -2914,14 +2769,9 @@ on the second character (the one after '-'), to save some effort. */
 
     case 'd':
 
-    /* -dropcr: Set this option.  Now a no-op, retained for compatibility only. */
-
-    if (Ustrcmp(argrest, "ropcr") == 0)
-      /* drop_cr = TRUE */ ;
-
     /* -dp: Set up a debug pretrigger buffer with given size. */
 
-    else if (Ustrcmp(argrest, "p") == 0)
+    if (Ustrcmp(argrest, "p") == 0)
       if (++i >= argc)
 	badarg = TRUE;
       else
@@ -4070,9 +3920,9 @@ if (	 (smtp_input || extract_recipients || recipients_arg < argc)
    ||	 smtp_input
       && (sender_address || filter_test != FTEST_NONE || extract_recipients)
    || deliver_selectstring && !qrunners
-   || msg_action == MSG_LOAD && (!expansion_test || expansion_test_message)
+   || msg_action == MSG_LOAD && (!f.expansion_test || expansion_test_message)
    ||	 atrn_mode
-      && (  f.daemon_listen || expansion_test || filter_test != FTEST_NONE
+      && (  f.daemon_listen || f.expansion_test || filter_test != FTEST_NONE
 	 || checking /* || bi_option || info_stdout || receiving_message
 	 || malware_test_file || list_queue || list_config || list_options
 	 || version_printed || msg_action_arg > 0 || qrunners
@@ -4230,7 +4080,7 @@ if ((                                            /* EITHER */
     real_uid != root_uid &&                      /* Not root, and */
     !f.running_in_test_harness                     /* Not fudged */
     ) ||                                         /*   OR   */
-    expansion_test                               /* expansion testing */
+    f.expansion_test                             /* expansion testing */
     ||                                           /*   OR   */
     filter_test != FTEST_NONE)                   /* Filter testing */
   {
@@ -4960,7 +4810,7 @@ needed in transports so we lost the optimisation. */
   /* -be can add macro definitions, needing to link to the macro structure
   chain.  Otherwise, make the memory used for config data readonly. */
 
-  if (!expansion_test)
+  if (!f.expansion_test)
     store_writeprotect(POOL_CONFIG);
 
 #ifdef MEASURE_TIMING
@@ -5498,7 +5348,7 @@ from stdin if there aren't any. If -Mset was specified, load the message so
 that its variables can be used, but restrict this facility to admin users.
 Otherwise, if -bem was used, read a message from stdin. */
 
-if (expansion_test)
+if (f.expansion_test)
   {
   set_process_info("expansion-test");
   dns_init(FALSE, FALSE, FALSE);
@@ -5615,9 +5465,7 @@ if (raw_active_hostname)
 /* Handle host checking: this facility mocks up an incoming SMTP call from a
 given IP address so that the blocking and relay configuration can be tested.
 Unless a sender_ident was set by -oMt, we discard it (the default is the
-caller's login name). An RFC 1413 call is made only if we are running in the
-test harness and an incoming interface and both ports are specified, because
-there is no TCP/IP call to find the ident for. */
+caller's login name). */
 
 if (host_checking)
   {
@@ -5625,12 +5473,7 @@ if (host_checking)
   int size;
 
   if (!sender_ident_set)
-    {
     sender_ident = NULL;
-    if (f.running_in_test_harness && sender_host_port
-       && interface_address && interface_port)
-      verify_get_ident(test_harness_identd_port);
-    }
 
   /* In case the given address is a non-canonical IPv6 address, canonicalize
   it. Use the compressed form for IPv6. */
@@ -5653,7 +5496,6 @@ if (host_checking)
   debug_file = stderr;
   debug_fd = fileno(debug_file);
   dprintf(smtp_out_fd, "\n**** SMTP testing session as if from host %s\n"
-    "**** but without any ident (RFC 1413) callback.\n"
     "**** This is not for real!\n\n",
       sender_host_address);
 
@@ -5765,14 +5607,12 @@ sendmail error modes other than -oem ever actually used? Later: yes.) */
 if (!smtp_input) error_handling = arg_error_handling;
 
 /* If this is an inetd call, ensure that stderr is closed to prevent panic
-logging being sent down the socket and make an identd call to get the
-sender_ident. */
+logging being sent down the socket. */
 
 else if (f.is_inetd && !atrn_mode)
   {
   (void)fclose(stderr);
   exim_nullstd();                       /* Re-open to /dev/null */
-  verify_get_ident(IDENT_PORT);
   host_build_sender_fullhost();
   set_process_info("handling incoming connection from %s via inetd",
     sender_fullhost);
diff --git a/src/src/exim.h b/src/src/exim.h
index 8109c2a7c..fa56f2c2c 100644
--- a/src/src/exim.h
+++ b/src/src/exim.h
@@ -156,8 +156,6 @@ configuration file.  We also use this for some other short strings, such
 as queue names.
 Also TLS ciphersuite name (no real known limit since the protocols use
 integers, but max seen in reality is 45 octets).
-
-RFC 1413 gives us the 512 limit on IDENT protocol userids.
 */
 
 #define EXIM_EMAILADDR_MAX     256
@@ -541,9 +539,6 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly.
 #endif
 #include "osfunctions.h"
 
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-# include "bmi_spam.h"
-#endif
 #if defined(SUPPORT_SPF) || defined(EXPERIMENTAL_SPF_PERL)
 # include "miscmods/spf.h"
 # include "miscmods/spf_api.h"
diff --git a/src/src/expand.c b/src/src/expand.c
index e852c27c7..554d68ba6 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -474,12 +474,6 @@ static var_entry var_table[] = {
   { "authentication_failed",vtype_int,        &authentication_failed },
 #ifdef WITH_CONTENT_SCAN
   { "av_failed",           vtype_int,         &av_failed },
-#endif
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-  { "bmi_alt_location",    vtype_stringptr,   &bmi_alt_location },
-  { "bmi_base64_tracker_verdict", vtype_stringptr, &bmi_base64_tracker_verdict },
-  { "bmi_base64_verdict",  vtype_stringptr,   &bmi_base64_verdict },
-  { "bmi_deliver",         vtype_int,         &bmi_deliver },
 #endif
   { "body_linecount",      vtype_int,         &body_linecount },
   { "body_zerocount",      vtype_int,         &body_zerocount },
diff --git a/src/src/functions.h b/src/src/functions.h
index 0825b1b57..a29dddbed 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -218,7 +218,7 @@ extern void    dscp_list_to_stream(FILE *);
 extern BOOL    dscp_lookup(const uschar *, int, int *, int *, int *);
 
 extern void    enq_end(uschar *);
-extern BOOL    enq_start(uschar *, unsigned);
+extern unsigned enq_start(uschar *, unsigned);
 #ifndef DISABLE_EVENT
 extern uschar *event_raise(const uschar *, const uschar *, const uschar *, int *);
 extern void    msg_event_raise(const uschar *, const address_item *);
@@ -319,6 +319,7 @@ extern void    log_write_die(unsigned, int, const char * format, ...)
 		PRINTF_FUNCTION(3,4) NORETURN;
 
 extern const lookup_info * lookup_with_acq_num(unsigned);
+extern gstring *lookup_dynamic_supported(gstring *);
 #ifdef LOOKUP_MODULE_DIR
 extern BOOL    lookup_one_mod_load(const uschar *, uschar **);
 #endif
@@ -601,11 +602,11 @@ extern const uschar *string_printing2(const uschar *, int);
 extern uschar *string_split_message(uschar *);
 extern uschar *string_unprinting(uschar *);
 #ifdef SUPPORT_I18N
-extern uschar *string_address_utf8_to_alabel(const uschar *, uschar **);
+extern const uschar *string_address_utf8_to_alabel(const uschar *, uschar **);
 extern uschar *string_domain_alabel_to_utf8(const uschar *, uschar **);
-extern uschar *string_domain_utf8_to_alabel(const uschar *, uschar **);
+extern const uschar *string_domain_utf8_to_alabel(const uschar *, uschar **);
 extern uschar *string_localpart_alabel_to_utf8(const uschar *, uschar **);
-extern uschar *string_localpart_utf8_to_alabel(const uschar *, uschar **);
+extern const uschar *string_localpart_utf8_to_alabel(const uschar *, uschar **);
 #endif
 
 #define string_format(buf, siz, fmt, ...) \
@@ -695,7 +696,6 @@ extern int     verify_check_given_host(const uschar **, const host_item *);
 extern int     verify_check_this_host(const uschar **, unsigned int *,
 	         const uschar*, const uschar *, const uschar **);
 extern address_item *verify_checked_sender(const uschar *);
-extern void    verify_get_ident(int);
 extern void    verify_quota(uschar *);
 extern int     verify_quota_call(const uschar *, int, int, uschar **);
 extern BOOL    verify_sender(int *, uschar **);
diff --git a/src/src/globals.c b/src/src/globals.c
index 3eda97628..be2b7ec13 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -58,10 +58,6 @@ BOOL    opt_perl_taintmode     = FALSE;
 tree_node *dlobj_anchor        = NULL;
 #endif
 
-#ifdef LOOKUP_IBASE
-uschar *ibase_servers          = NULL;
-#endif
-
 #ifdef LOOKUP_LDAP
 uschar *eldap_ca_cert_dir      = NULL;
 uschar *eldap_ca_cert_file     = NULL;
@@ -265,6 +261,7 @@ struct global_flags f =
 
 	.enable_dollar_recipients = FALSE,
 	.expand_string_forcedfail = FALSE,
+	.expansion_test		= FALSE,
 
 	.filter_running         = FALSE,
 
@@ -366,7 +363,6 @@ BOOL    disable_fsync          = FALSE;
 #endif
 BOOL    disable_ipv6           = FALSE;
 BOOL    dns_csa_use_reverse    = TRUE;
-BOOL    drop_cr                = FALSE;         /* No longer used */
 
 BOOL    envelope_to_remove     = TRUE;
 BOOL    exim_gid_set           = TRUE;          /* This gid is always set */
@@ -585,15 +581,6 @@ uschar *base62_chars= US"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 uschar *bi_command             = NULL;
 uschar *big_buffer             = NULL;
 int     big_buffer_size        = BIG_BUFFER_SIZE;
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-uschar *bmi_alt_location       = NULL;
-uschar *bmi_base64_tracker_verdict = NULL;
-uschar *bmi_base64_verdict     = NULL;
-uschar *bmi_config_file        = US"/opt/brightmail/etc/brightmail.cfg";
-int     bmi_deliver            = 1;
-int     bmi_run                = 0;
-uschar *bmi_verdicts           = NULL;
-#endif
 int     bsmtp_transaction_linecount = 0;
 int     body_8bitmime          = 0;
 int     body_linecount         = 0;
@@ -720,7 +707,6 @@ bit_table debug_options[]      = { /* must be in alphabetical order and use
   BIT_TABLE(D, filter),
   BIT_TABLE(D, hints_lookup),
   BIT_TABLE(D, host_lookup),
-  BIT_TABLE(D, ident),
   BIT_TABLE(D, interface),
   BIT_TABLE(D, lists),
   BIT_TABLE(D, load),
@@ -1003,7 +989,6 @@ bit_table log_options[]        = { /* must be in alphabetical order,
   BIT_TABLE(L, dnssec),
   BIT_TABLE(L, etrn),
   BIT_TABLE(L, host_lookup_failed),
-  BIT_TABLE(L, ident_timeout),
   BIT_TABLE(L, incoming_interface),
   BIT_TABLE(L, incoming_port),
   BIT_TABLE(L, lost_incoming_connection),
@@ -1245,7 +1230,7 @@ const pcre2_code *regex_whitelisted_macro = NULL;
 uschar *regex_match_string     = NULL;
 #endif
 int     remote_delivery_count  = 0;
-int     remote_max_parallel    = 4;
+int     remote_max_parallel    = 6;
 uschar *remote_sort_domains    = NULL;
 int     retry_data_expire      = 7*24*60*60;
 int     retry_interval_max     = 24*60*60;
@@ -1253,8 +1238,6 @@ int     retry_maximum_timeout  = 0;        /* set from retry config */
 retry_config  *retries         = NULL;
 const uschar *return_path            = NULL;
 int     rewrite_existflags     = 0;
-uschar *rfc1413_hosts          = US"@[]";
-int     rfc1413_query_timeout  = 0;
 uid_t   root_gid               = ROOT_GID;
 uid_t   root_uid               = ROOT_UID;
 
@@ -1416,7 +1399,6 @@ uid_t   system_filter_uid      = (uid_t)-1;
 
 blob	tcp_fastopen_nodata    = { .data = NULL, .len = 0 };
 tfo_state_t tcp_out_fastopen   = TFO_NOT_USED;
-int	test_harness_identd_port  = IDENT_PORT;
 int     test_harness_load_avg  = 0;
 int     thismessage_size_limit = 0;
 int     timeout_frozen_after   = 0;
diff --git a/src/src/globals.h b/src/src/globals.h
index cb6b91c5a..ccd790cc2 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -40,10 +40,6 @@ extern BOOL    opt_perl_taintmode;     /* Enable taint mode in Perl */
 extern tree_node *dlobj_anchor;        /* Tree of dynamically-loaded objects */
 #endif
 
-#ifdef LOOKUP_IBASE
-extern uschar *ibase_servers;
-#endif
-
 #ifdef LOOKUP_LDAP
 extern uschar *eldap_ca_cert_dir;      /* Directory with CA certificates */
 extern uschar *eldap_ca_cert_file;     /* CA certificate file */
@@ -123,6 +119,7 @@ typedef struct {
   BOOL	  channelbind_exporter:1; /* channelbinding is EXPORTER not UNIQUE */
   BOOL    on_connect:1;         /* For older MTAs that don't STARTTLS */
   BOOL	  verify_override:1;	/* certificate_verified only due to tls_try_verify_hosts */
+  BOOL	  smtp_quit:1;		/* QUIT has been sent or received */
 } tls_support;
 extern tls_support tls_in;
 extern tls_support tls_out;
@@ -234,6 +231,7 @@ extern struct global_flags {
 
  BOOL   enable_dollar_recipients	:1; /* Make $recipients available */
  BOOL   expand_string_forcedfail	:1; /* TRUE if failure was "expected" */
+ BOOL   expansion_test			:1; /* TRUE for -be test mode */
 
  BOOL   filter_running			:1; /* TRUE while running a filter */
 
@@ -392,19 +390,19 @@ extern int     av_failed;              /* TRUE if the AV process failed */
 extern uschar *av_scanner;             /* AntiVirus scanner to use for the malware condition */
 #endif
 
+extern lookup_module_info * avail_static_lookups[];/* List of built-in lookup drivers */
+
+extern const uschar * avail_static_auths[];	/* List of built-in driver names */
+extern const uschar * avail_dynamic_auths[];	/* List of dynamic-load driver names */
+extern const uschar * avail_static_routers[];
+extern const uschar * avail_dynamic_routers[];
+extern const uschar * avail_static_transports[];
+extern const uschar * avail_dynamic_transports[];
+
 extern uschar *base62_chars;           /* Table of base-62 characters */
 extern uschar *bi_command;             /* Command for -bi option */
 extern uschar *big_buffer;             /* Used for various temp things */
 extern int     big_buffer_size;        /* Current size (can expand) */
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-extern uschar *bmi_alt_location;       /* expansion variable that contains the alternate location for the rcpt (available during routing) */
-extern uschar *bmi_base64_tracker_verdict; /* expansion variable with base-64 encoded OLD verdict string (available during routing) */
-extern uschar *bmi_base64_verdict;     /* expansion variable with base-64 encoded verdict string (available during routing) */
-extern uschar *bmi_config_file;        /* Brightmail config file */
-extern int     bmi_deliver;            /* Flag that determines if the message should be delivered to the rcpt (available during routing) */
-extern int     bmi_run;                /* Flag that determines if message should be run through Brightmail server */
-extern uschar *bmi_verdicts;           /* BASE64-encoded verdicts with recipient lists */
-#endif
 extern int     bsmtp_transaction_linecount; /* Start of last transaction */
 extern int     body_8bitmime;          /* sender declared BODY= ; 7=7BIT, 8=8BITMIME */
 extern uschar *bounce_message_file;    /* Template file */
@@ -575,8 +573,6 @@ extern uschar *dnslist_value;          /* DNS (black) list IP address */
 extern tree_node *domainlist_anchor;   /* Tree of defined domain lists */
 extern int     domainlist_count;       /* Number defined */
 
-/* This option is now a no-opt, retained for compatibility */
-extern BOOL    drop_cr;                /* For broken local MUAs */
 extern const uschar *driver_srcfile;   /* For debug & errors */
 extern int     driver_srcline;	       /* For debug & errors */
 
@@ -923,8 +919,6 @@ extern int     retry_maximum_timeout;  /* The maximum timeout */
 extern const uschar *return_path;            /* Return path for a message */
 extern BOOL    return_path_remove;     /* Remove return-path headers */
 extern int     rewrite_existflags;     /* Indicate which headers have rewrites */
-extern uschar *rfc1413_hosts;          /* RFC hosts */
-extern int     rfc1413_query_timeout;  /* Timeout on RFC 1413 calls */
 /* extern BOOL    rfc821_domains;  */       /* If set, syntax is 821, not 822 => being abolished */
 extern uid_t   root_gid;               /* The gid for root */
 extern uid_t   root_uid;               /* The uid for root */
@@ -1065,7 +1059,6 @@ extern BOOL    system_filter_uid_set;  /* TRUE if uid set */
 extern blob    tcp_fastopen_nodata;    /* for zero-data TFO connect requests */
 extern BOOL    tcp_nodelay;            /* Controls TCP_NODELAY on daemon */
 extern tfo_state_t tcp_out_fastopen;   /* TCP fast open */
-extern int     test_harness_identd_port; /* For use when testing */
 extern int     test_harness_load_avg;  /* For use when testing */
 extern int     thismessage_size_limit; /* Limit for this message */
 extern int     timeout_frozen_after;   /* Max time to keep frozen messages */
diff --git a/src/src/local_scan.h b/src/src/local_scan.h
index a97fb4612..355487b0d 100644
--- a/src/src/local_scan.h
+++ b/src/src/local_scan.h
@@ -160,9 +160,6 @@ typedef struct recipient_item {
   const uschar *errors_to;      /* the errors_to address or NULL */
   uschar *orcpt;                /* DSN orcpt */
   int     dsn_flags;            /* DSN flags */
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-  uschar *bmi_optin;
-#endif
 } recipient_item;
 
 
diff --git a/src/src/lookupapi.h b/src/src/lookupapi.h
index 2e857c5d3..b8ec17880 100644
--- a/src/src/lookupapi.h
+++ b/src/src/lookupapi.h
@@ -18,9 +18,10 @@
 */
 
 typedef struct lookup_info {
-  uschar *name;                   /* e.g. "lsearch" */
-  int type;                       /* query/singlekey/abs-file */
-  unsigned acq_num;		  /* acquisition number */
+  uschar *	name;			/* e.g. "lsearch" */
+  int		type;			/* query/singlekey/abs-file */
+  unsigned	acq_num;		/* acquisition number */
+
   void *(*open)(                  /* open function */
     const uschar *,               /* file name for those that have one */
     uschar **);                   /* for error message */
@@ -60,9 +61,9 @@ typedef struct lookup_info {
 /* Version 5 change: version report now adds to a gstring */
 
 typedef struct lookup_module_info {
-  uint magic;
+  uint		magic;
   lookup_info **lookups;
-  uint lookupcount;
+  uint		lookupcount;
 } lookup_module_info;
 
 /* End of lookupapi.h */
diff --git a/src/src/lookups/Makefile b/src/src/lookups/Makefile
index 74e705e37..d794fe895 100644
--- a/src/src/lookups/Makefile
+++ b/src/src/lookups/Makefile
@@ -8,17 +8,38 @@
 #     extra variable definitions and prepended to it and module build rules
 #     interpolated below. This is done by scripts/lookups-Makefile.
 
-# When adding a new driver here, attend also to scripts/lookups-Makefile
-# and scripts/MakeLinks
+# When adding a new driver here, attend also to scripts/lookups-Makefile,
+# and scripts/MakeLinks.
 
 # MAGIC-TAG-MODS-OBJ-RULES-GO-HERE
 
+OBJS = $(OBJ) $(OBJ_ALWAYS)
+
+# This assumes that the driver and object-file names match
+AVAIL= $(OBJS:.o=)
+
 all:             Makefile lookups.a $(MODS)
 
-lookups.a:       $(OBJ)
+# We assume here that the driver name is used as the prefix for
+# its module_info struct.
+avail_static_lookups.c: Makefile
+	@echo "make $@"
+	$(FE)echo '/* File built by lookups/Makefile */' >$@
+	$(FE)echo '#include "../exim.h"' >>$@
+	$(FE)for f in $(AVAIL); do \
+	 echo "extern lookup_module_info $${f}_lookup_module_info;" >>$@; \
+	done
+	$(FE)echo 'lookup_module_info * avail_static_lookups[] = {' >>$@
+	$(FE)for f in $(AVAIL); do \
+	 echo "& $${f}_lookup_module_info," >>$@; \
+	done
+	$(FE)echo 'NULL };' >>$@
+avail_static_lookups.o: $(HDRS) avail_static_lookups.c
+
+lookups.a:       avail_static_lookups.o $(OBJS)
 		 @$(RM_COMMAND) -f lookups.a
 		 @echo "$(AR) lookups.a"
-		 @$(AR) lookups.a $(OBJ)
+		 @$(AR) lookups.a avail_static_lookups.o $(OBJS)
 		 $(RANLIB) $@
 
 .SUFFIXES:       .o .c .so
@@ -32,7 +53,6 @@ cdb.o cdb.so:           $(HDRS) cdb.c
 dbmdb.o dbmdb.so:       $(HDRS) dbmdb.c
 dnsdb.o dnsdb.so:       $(HDRS) dnsdb.c
 dsearch.o dsearch.so:   $(HDRS) dsearch.c
-ibase.o ibase.so:       $(HDRS) ibase.c
 ldap.o ldap.so:         $(HDRS) ldap.c
 lmdb.o lmdb.so:         $(HDRS) lmdb.c
 json.o json.so:         $(HDRS) json.c
@@ -44,6 +64,7 @@ nmh.o nmh.so:		$(HDRS) nmh.c
 oracle.o oracle.so:     $(HDRS) oracle.c
 passwd.o passwd.so:     $(HDRS) passwd.c
 pgsql.o pgsql.so:       $(HDRS) pgsql.c
+psl.o psl.so:       	$(HDRS) psl.c
 readsock.o readsock.so: $(HDRS) readsock.c
 redis.o redis.so:       $(HDRS) redis.c
 spf.o spf.so:           $(HDRS) spf.c
diff --git a/src/src/lookups/ibase.c b/src/src/lookups/ibase.c
deleted file mode 100644
index 141681aab..000000000
--- a/src/src/lookups/ibase.c
+++ /dev/null
@@ -1,550 +0,0 @@
-/*************************************************
-*     Exim - an Internet mail transport agent    *
-*************************************************/
-
-/* Copyright (c) The Exim Maintainers 2020 - 2025 */
-/* 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 */
-
-/* The code in this module was contributed by Ard Biesheuvel. */
-
-#include "../exim.h"
-#include "lf_functions.h"
-
-#include <ibase.h>              /* The system header */
-
-/* Structure and anchor for caching connections. */
-
-typedef struct ibase_connection {
-    struct ibase_connection *next;
-    uschar *server;
-    isc_db_handle dbh;
-    isc_tr_handle transh;
-} ibase_connection;
-
-static ibase_connection *ibase_connections = NULL;
-
-
-#if defined(_LP64) || defined(__LP64__) || defined(__arch64__) || defined(_WIN64)
-# define ISC_NULL 0
-#else
-# define ISC_NULL NULL
-#endif
-
-/*************************************************
-*              Open entry point                  *
-*************************************************/
-
-/* See local README for interface description. */
-
-static void *ibase_open(const uschar * filename, uschar ** errmsg)
-{
-return (void *) (1);        /* Just return something non-null */
-}
-
-
-
-/*************************************************
-*               Tidy entry point                 *
-*************************************************/
-
-/* See local README for interface description. */
-
-static void
-ibase_tidy(void)
-{
-ibase_connection *cn;
-ISC_STATUS status[20];
-
-while ((cn = ibase_connections))
-  {
-  ibase_connections = cn->next;
-  DEBUG(D_lookup) debug_printf_indent("close Interbase connection: %s\n",
-			       cn->server);
-  isc_commit_transaction(status, &cn->transh);
-  isc_detach_database(status, &cn->dbh);
-  }
-}
-
-static int
-fetch_field(uschar * buffer, int buffer_size, XSQLVAR * var)
-{
-if (buffer_size < var->sqllen)
-  return 0;
-
-switch (var->sqltype & ~1)
-  {
-  case SQL_VARYING:
-      strncpy(CS buffer, &var->sqldata[2], *(short *) var->sqldata);
-      return *(short *) var->sqldata;
-  case SQL_TEXT:
-      strncpy(CS buffer, var->sqldata, var->sqllen);
-      return var->sqllen;
-  case SQL_SHORT:
-      return sprintf(CS buffer, "%d", *(short *) var->sqldata);
-  case SQL_LONG:
-      return sprintf(CS buffer, "%ld", *(ISC_LONG *) var->sqldata);
-  #ifdef SQL_INT64
-  case SQL_INT64:
-      return sprintf(CS buffer, "%lld", *(ISC_INT64 *) var->sqldata);
-  #endif
-  default:
-      /* not implemented */
-      return 0;
-  }
-}
-
-/*************************************************
-*        Internal search function                *
-*************************************************/
-
-/* This function is called from the find entry point to do the search for a
-single server.
-
-Arguments:
-  query        the query string
-  server       the server string
-  resultptr    where to store the result
-  errmsg       where to point an error message
-  defer_break  TRUE if no more servers are to be tried after DEFER
-
-The server string is of the form "host:dbname|user|password". The host can be
-host:port. This string is in a nextinlist temporary buffer, so can be
-overwritten.
-
-Returns:       OK, FAIL, or DEFER
-*/
-
-static int
-perform_ibase_search(const uschar * query, uschar * server, uschar ** resultptr,
-                     uschar ** errmsg, BOOL * defer_break)
-{
-isc_stmt_handle stmth;
-XSQLDA *out_sqlda;
-XSQLVAR *var;
-int i;
-rmark reset_point;
-
-uschar buffer[256];
-ISC_STATUS status[20], *statusp = status;
-
-gstring * result = NULL;
-int yield = DEFER;
-ibase_connection *cn;
-uschar *server_copy = NULL;
-uschar *sdata[3];
-
-/* Disaggregate the parameters from the server argument. The order is host,
-database, user, password. We can write to the string, since it is in a
-nextinlist temporary buffer. The copy of the string that is used for caching
-has the password removed. This copy is also used for debugging output. */
-
-for (int i = 2; i > 0; i--)
-  {
-  uschar * pp = Ustrrchr(server, '|');
-
-  if (!pp)
-    {
-    *errmsg = string_sprintf("incomplete Interbase server data: %s",
-		       i == 3 ? server : server_copy);
-    *defer_break = TRUE;
-    return DEFER;
-    }
-  *pp++ = 0;
-  sdata[i] = pp;
-  if (i == 2)
-      server_copy = string_copy(server);   /* sans password */
-  }
-sdata[0] = server;          /* What's left at the start */
-
-/* See if we have a cached connection to the server */
-
-for (cn = ibase_connections; cn; cn = cn->next)
-  if (Ustrcmp(cn->server, server_copy) == 0)
-    break;
-
-/* Use a previously cached connection ? */
-
-if (cn)
-  {
-  static char db_info_options[] = { isc_info_base_level };
-
-  /* test if the connection is alive */
-  if (isc_database_info(status, &cn->dbh, sizeof(db_info_options),
-	db_info_options, sizeof(buffer), CS buffer))
-    {
-    /* error occurred: assume connection is down */
-    DEBUG(D_lookup)
-      debug_printf("Interbase cleaning up cached connection: %s\n", cn->server);
-    isc_detach_database(status, &cn->dbh);
-    }
-  else
-    DEBUG(D_lookup)
-      debug_printf_indent("Interbase using cached connection for %s\n",
-		     server_copy);
-  }
-else
-  {
-  cn = store_get(sizeof(ibase_connection), GET_UNTAINTED);
-  cn->server = server_copy;
-  cn->dbh = ISC_NULL;
-  cn->transh = ISC_NULL;
-  cn->next = ibase_connections;
-  ibase_connections = cn;
-  }
-
-/* If no cached connection, we must set one up. */
-
-if (!cn->dbh || !cn->transh)
-  {
-  uschar * dpb;
-  short dpb_length;
-  static char trans_options[] =
-      { isc_tpb_version3, isc_tpb_read, isc_tpb_read_committed,
-      isc_tpb_rec_version };
-
-  /* Construct the database parameter buffer. */
-  dpb = buffer;
-  *dpb++ = isc_dpb_version1;
-  *dpb++ = isc_dpb_user_name;
-  *dpb++ = Ustrlen(sdata[1]);
-  for (uschar * p = sdata[1]; *p;) *dpb++ = *p++;
-  *dpb++ = isc_dpb_password;
-  *dpb++ = Ustrlen(sdata[2]);
-  for (uschar * p = sdata[2]; *p;) *dpb++ = *p++;
-  dpb_length = dpb - buffer;
-
-  DEBUG(D_lookup)
-      debug_printf_indent("new Interbase connection: database=%s user=%s\n",
-		   sdata[0], sdata[1]);
-
-  /* Connect to the database */
-  if (isc_attach_database(status, 0, CS sdata[0], &cn->dbh,
-			  dpb_length, CS buffer))
-    {
-    isc_interprete(CS buffer, &statusp);
-    *errmsg = string_sprintf("Interbase attach() failed: %s", buffer);
-    *defer_break = FALSE;
-    goto IBASE_EXIT;
-    }
-
-  /* Now start a read-only read-committed transaction */
-  if (isc_start_transaction(status, &cn->transh, 1, &cn->dbh,
-			    sizeof(trans_options), trans_options))
-    {
-    isc_interprete(CS buffer, &statusp);
-    isc_detach_database(status, &cn->dbh);
-    *errmsg = string_sprintf("Interbase start_transaction() failed: %s",
-		       buffer);
-    *defer_break = FALSE;
-    goto IBASE_EXIT;
-    }
-  }
-
-/* Run the query */
-if (isc_dsql_allocate_statement(status, &cn->dbh, &stmth))
-  {
-  isc_interprete(CS buffer, &statusp);
-  *errmsg = string_sprintf("Interbase alloc_statement() failed: %s", buffer);
-  *defer_break = FALSE;
-  goto IBASE_EXIT;
-  }
-
-/* Lacking any information, assume that the data is untainted */
-reset_point = store_mark();
-out_sqlda = store_get(XSQLDA_LENGTH(1), GET_UNTAINTED);
-out_sqlda->version = SQLDA_VERSION1;
-out_sqlda->sqln = 1;
-
-if (isc_dsql_prepare(status, &cn->transh, &stmth, 0, CCS query, 1, out_sqlda))
-  {
-  isc_interprete(CS buffer, &statusp);
-  reset_point = store_reset(reset_point);
-  out_sqlda = NULL;
-  *errmsg = string_sprintf("Interbase prepare_statement() failed: %s", buffer);
-  *defer_break = FALSE;
-  goto IBASE_EXIT;
-  }
-
-/* re-allocate the output structure if there's more than one field */
-if (out_sqlda->sqln < out_sqlda->sqld)
-  {
-  XSQLDA *new_sqlda = store_get(XSQLDA_LENGTH(out_sqlda->sqld), GET_UNTAINTED);
-  if (isc_dsql_describe
-      (status, &stmth, out_sqlda->version, new_sqlda))
-    {
-    isc_interprete(CS buffer, &statusp);
-    isc_dsql_free_statement(status, &stmth, DSQL_drop);
-    reset_point = store_reset(reset_point);
-    out_sqlda = NULL;
-    *errmsg = string_sprintf("Interbase describe_statement() failed: %s",
-		       buffer);
-    *defer_break = FALSE;
-    goto IBASE_EXIT;
-    }
-  out_sqlda = new_sqlda;
-  }
-
-/* allocate storage for every returned field */
-for (i = 0, var = out_sqlda->sqlvar; i < out_sqlda->sqld; i++, var++)
-  {
-  switch (var->sqltype & ~1)
-    {
-    case SQL_VARYING:
-	var->sqldata = CS store_get(sizeof(char) * var->sqllen + 2, GET_UNTAINTED);
-	break;
-    case SQL_TEXT:
-	var->sqldata = CS store_get(sizeof(char) * var->sqllen, GET_UNTAINTED);
-	break;
-    case SQL_SHORT:
-	var->sqldata = CS  store_get(sizeof(short), GET_UNTAINTED);
-	break;
-    case SQL_LONG:
-	var->sqldata = CS  store_get(sizeof(ISC_LONG), GET_UNTAINTED);
-	break;
-#ifdef SQL_INT64
-    case SQL_INT64:
-	var->sqldata = CS  store_get(sizeof(ISC_INT64), GET_UNTAINTED);
-	break;
-#endif
-    case SQL_FLOAT:
-	var->sqldata = CS  store_get(sizeof(float), GET_UNTAINTED);
-	break;
-    case SQL_DOUBLE:
-	var->sqldata = CS  store_get(sizeof(double), GET_UNTAINTED);
-	break;
-#ifdef SQL_TIMESTAMP
-    case SQL_DATE:
-	var->sqldata = CS  store_get(sizeof(ISC_QUAD), GET_UNTAINTED);
-	break;
-#else
-    case SQL_TIMESTAMP:
-	var->sqldata = CS  store_get(sizeof(ISC_TIMESTAMP), GET_UNTAINTED);
-	break;
-    case SQL_TYPE_DATE:
-	var->sqldata = CS  store_get(sizeof(ISC_DATE), GET_UNTAINTED);
-	break;
-    case SQL_TYPE_TIME:
-	var->sqldata = CS  store_get(sizeof(ISC_TIME), GET_UNTAINTED);
-	break;
-  #endif
-    }
-  if (var->sqltype & 1)
-    var->sqlind = (short *) store_get(sizeof(short), GET_UNTAINTED);
-  }
-
-/* finally, we're ready to execute the statement */
-if (isc_dsql_execute(status, &cn->transh, &stmth, out_sqlda->version, NULL))
-  {
-  isc_interprete(CS buffer, &statusp);
-  *errmsg = string_sprintf("Interbase describe_statement() failed: %s", buffer);
-  isc_dsql_free_statement(status, &stmth, DSQL_drop);
-  *defer_break = FALSE;
-  goto IBASE_EXIT;
-  }
-
-while (isc_dsql_fetch(status, &stmth, out_sqlda->version, out_sqlda) != 100L)
-  {
-  /* check if an error occurred */
-  if (status[0] & status[1])
-    {
-    isc_interprete(CS buffer, &statusp);
-    *errmsg = string_sprintf("Interbase fetch() failed: %s", buffer);
-    isc_dsql_free_statement(status, &stmth, DSQL_drop);
-    *defer_break = FALSE;
-    goto IBASE_EXIT;
-    }
-
-  if (result)
-    result = string_catn(result, US "\n", 1);
-
-  /* Find the number of fields returned. If this is one, we don't add field
-     names to the data. Otherwise we do. */
-  if (out_sqlda->sqld == 1)
-    {
-    if (out_sqlda->sqlvar[0].sqlind == NULL || *out_sqlda->sqlvar[0].sqlind != -1)     /* NULL value yields nothing */
-      result = string_catn(result, US buffer,
-		   fetch_field(buffer, sizeof(buffer), &out_sqlda->sqlvar[0]));
-    }
-
-  else
-    for (int i = 0; i < out_sqlda->sqld; i++)
-      {
-      int len = fetch_field(buffer, sizeof(buffer), &out_sqlda->sqlvar[i]);
-
-      result = string_catn(result, US out_sqlda->sqlvar[i].aliasname,
-		     out_sqlda->sqlvar[i].aliasname_length);
-      result = string_catn(result, US "=", 1);
-
-      /* Quote the value if it contains spaces or is empty */
-
-      if (*out_sqlda->sqlvar[i].sqlind == -1)       /* NULL value */
-	result = string_catn(result, US "\"\"", 2);
-
-      else if (buffer[0] == 0 || Ustrchr(buffer, ' ') != NULL)
-	{
-	result = string_catn(result, US "\"", 1);
-	for (int j = 0; j < len; j++)
-	  {
-	  if (buffer[j] == '\"' || buffer[j] == '\\')
-	      result = string_catn(result, US "\\", 1);
-	  result = string_catn(result, US buffer + j, 1);
-	  }
-	result = string_catn(result, US "\"", 1);
-	}
-      else
-	result = string_catn(result, US buffer, len);
-      result = string_catn(result, US " ", 1);
-      }
-  }
-
-/* If result is NULL then no data has been found and so we return FAIL.
-Otherwise, we must terminate the string which has been built; string_cat()
-always leaves enough room for a terminating zero. */
-
-if (!result)
-  {
-  yield = FAIL;
-  *errmsg = US "Interbase: no data found";
-  }
-else
-  gstring_release_unused(result);
-
-
-/* Get here by goto from various error checks. */
-
-IBASE_EXIT:
-
-if (stmth)
-  isc_dsql_free_statement(status, &stmth, DSQL_drop);
-
-/* Non-NULL result indicates a successful result */
-
-if (result)
-  {
-  *resultptr = string_from_gstring(result);
-  return OK;
-  }
-else
-  {
-  DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
-  return yield;           /* FAIL or DEFER */
-  }
-}
-
-
-
-
-/*************************************************
-*               Find entry point                 *
-*************************************************/
-
-/* See local README for interface description. The handle and filename
-arguments are not used. Loop through a list of servers while the query is
-deferred with a retryable error. */
-
-static int
-ibase_find(void * handle, const uschar * filename, const uschar * query,
-  int length, uschar ** result, uschar ** errmsg, uint * do_cache,
-  const uschar * opts)
-{
-uschar * server;
-const uschar * list = ibase_servers;
-
-DEBUG(D_lookup) debug_printf_indent("Interbase query: %s\n", query);
-
-for (int sep = 0; server = string_nextinlist(&list, &sep, NULL, 0); )
-  {
-  BOOL defer_break = FALSE;
-  int rc = perform_ibase_search(query, server, result, errmsg, &defer_break);
-  if (rc != DEFER || defer_break)
-    return rc;
-  }
-
-if (!ibase_servers)
-  *errmsg = US "no Interbase servers defined (ibase_servers option)";
-
-return DEFER;
-}
-
-
-
-/*************************************************
-*               Quote entry point                *
-*************************************************/
-
-/* The only characters that need to be quoted (with backslash) are newline,
-tab, carriage return, backspace, backslash itself, and the quote characters.
-Percent, and underscore and not escaped. They are only special in contexts
-where they can be wild cards, and this isn't usually the case for data inserted
-from messages, since that isn't likely to be treated as a pattern of any kind.
-Sadly, MySQL doesn't seem to behave like other programs. If you use something
-like "where id="ab\%cd" it does not treat the string as "ab%cd". So you really
-can't quote "on spec".
-
-Arguments:
-  s          the string to be quoted
-  opt        additional option text or NULL if none
-  idx	     lookup type index
-
-Returns:     the processed string or NULL for a bad option
-*/
-
-static uschar *
-ibase_quote(uschar * s, const uschar * opt, unsigned idx)
-{
-gstring * quoted = store_get_quoted(1, s, idx, US"ibase");
-
-if (opt)
-  return NULL;            /* No options recognized */
-
-for (uschar c; c = *s; s++)
-  {
-  if (c == '\'') quoted = string_catn(quoted, US"\\", 1);
-  quoted = string_catn(quoted, s, 1);
-  }
-gstring_release_unused(quoted);
-return(string_from_gstring(quoted));
-}
-
-
-
-/*************************************************
-*         Version reporting entry point          *
-*************************************************/
-
-/* See local README for interface description. */
-
-#include "../version.h"
-
-gstring *
-ibase_version_report(gstring * g)
-{
-#ifdef DYNLOOKUP
-g = string_fmt_append(g, "Library version: ibase: Exim version %s\n", EXIM_VERSION_STR));
-#endif
-return g;
-}
-
-
-static lookup_info _lookup_info = {
-  .name = US"ibase",			/* lookup name */
-  .type = lookup_querystyle,		/* query-style lookup */
-  .open = ibase_open,			/* open function */
-  .check = NULL,			/* no check function */
-  .find = ibase_find,			/* find function */
-  .close = NULL,			/* no close function */
-  .tidy = ibase_tidy,			/* tidy function */
-  .quote = ibase_quote,			/* quoting function */
-  .version_report = ibase_version_report           /* version reporting */
-};
-
-#ifdef DYNLOOKUP
-#define ibase_lookup_module_info _lookup_module_info
-#endif
-
-static lookup_info *_lookup_list[] = { &_lookup_info };
-lookup_module_info ibase_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
-
-/* End of lookups/ibase.c */
diff --git a/src/src/lookups/ldap.c b/src/src/lookups/ldap.c
index 624b947d6..a6e99f567 100644
--- a/src/src/lookups/ldap.c
+++ b/src/src/lookups/ldap.c
@@ -1580,7 +1580,7 @@ static lookup_info ldap_lookup_info = {
   .close = NULL,			/* no close function */
   .tidy = eldap_tidy,			/* tidy function */
   .quote = eldap_quote,			/* quoting function */
-  .version_report = ldap_version_report            /* version reporting */
+  .version_report = ldap_version_report	/* version reporting */
 };
 
 static lookup_info ldapdn_lookup_info = {
@@ -1592,7 +1592,7 @@ static lookup_info ldapdn_lookup_info = {
   .close = NULL,			/* no close function */
   .tidy = eldap_tidy,			/* sic */    /* tidy function */
   .quote = eldap_quote,			/* sic */    /* quoting function */
-  .version_report = NULL                           /* no version reporting (redundant) */
+  .version_report = NULL		/* no version reporting (redundant) */
 };
 
 static lookup_info ldapm_lookup_info = {
@@ -1604,7 +1604,7 @@ static lookup_info ldapm_lookup_info = {
   .close = NULL,			/* no close function */
   .tidy = eldap_tidy,			/* sic */    /* tidy function */
   .quote = eldap_quote,			/* sic */    /* quoting function */
-  .version_report = NULL                           /* no version reporting (redundant) */
+  .version_report = NULL		/* no version reporting (redundant) */
 };
 
 static lookup_info ldapauth_lookup_info = {
@@ -1616,7 +1616,7 @@ static lookup_info ldapauth_lookup_info = {
   .close = NULL,			/* no close function */
   .tidy = eldap_tidy,			/* sic */    /* tidy function */
   .quote = NULL,			/* NO quoting function */
-  .version_report = NULL                           /* no version reporting (redundant) */
+  .version_report = NULL		/* no version reporting (redundant) */
 };
 
 #ifdef DYNLOOKUP
diff --git a/src/src/lookups/lsearch.c b/src/src/lookups/lsearch.c
index c85cda5d5..89a9e6c80 100644
--- a/src/src/lookups/lsearch.c
+++ b/src/src/lookups/lsearch.c
@@ -474,7 +474,7 @@ static lookup_info wildlsearch_lookup_info = {
 };
 
 #ifdef DYNLOOKUP
-#define lsearch_lookup_module_info _lookup_module_info
+# define lsearch_lookup_module_info _lookup_module_info
 #endif
 
 static lookup_info *_lookup_list[] = { &iplsearch_lookup_info,
diff --git a/src/src/lookups/psl.c b/src/src/lookups/psl.c
new file mode 100644
index 000000000..1592fe32d
--- /dev/null
+++ b/src/src/lookups/psl.c
@@ -0,0 +1,252 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2025 */
+/* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "../exim.h"
+
+#ifndef SUPPORT_I18N
+# error PSL lookup requires internationalisation support
+#endif
+
+
+/*************************************************
+*              Open entry point                  *
+*************************************************/
+
+/* See local README for interface description */
+
+static void *
+psl_open(const uschar * filename, uschar ** errmsg)
+{
+FILE * f = fopen(CCS filename, "r");
+if (f) return (void *) f;
+*errmsg = US strerror(errno);
+return NULL;
+}
+
+static void
+psl_close(void * handle)
+{
+(void) fclose(handle);
+}
+
+
+
+
+/*************************************************
+*         Generic "find" implementation          *
+*************************************************/
+
+static int
+psl_gen_find(void * handle, const uschar * keystring,
+  int length, uschar ** result, uschar ** errmsg, BOOL is_regdom)
+{
+uschar rulebuf[128], * res = NULL;
+const uschar * s, * k, * kmatch;
+unsigned res_label_cnt = 0, nlabels;
+BOOL key_utf8;
+
+/* Ensure key is punycode and lowercase */
+
+if ((key_utf8 = string_is_utf8(keystring)))
+  {
+  DEBUG(D_lookup) debug_printf_indent("converting utf8 key %q\n", keystring);
+  if (!(keystring = string_domain_utf8_to_alabel(keystring, errmsg)))
+    return FAIL;
+  length = Ustrlen(keystring);
+  DEBUG(D_lookup) debug_printf_indent(" result %q\n", keystring);
+  }
+else
+  for (k = keystring; *k; k++)
+    if (isupper(*k)) { keystring = string_copylc(keystring); break; }
+
+while ((s = US fgets(CS rulebuf, sizeof(rulebuf), handle)))
+  {
+  const uschar * r;
+
+  if (!*s || *s == '\n') continue;		/* empty line */
+  if (s[0] == '/' && s[1] == '/') continue;	/* comment line */
+
+  nlabels = 1;
+  if ((r = US strsep(CSS &s, " \n\t")))
+    {
+    BOOL exception = *r == '!';
+    const uschar * t;
+
+    /* We convert any utf8 to punycode before starting comparison. It might
+    be more efficient to wait until hitting a top-bit-set byte? */
+    if (!(t = string_domain_utf8_to_alabel(r, errmsg)))
+      goto fail;
+    if (t != r)
+      {
+      DEBUG(D_lookup) debug_printf_indent("converting utf8 psl entry %q\n"
+	" result %q\n", r, t);
+      r = t;
+      }
+
+    for (s = r + Ustrlen(r), k = keystring + length; ; )
+      {
+      uschar rch = s[-1];
+
+      if (rch == '.')		/* label separator */
+	nlabels++;
+      if (rch == '*')		/* wildcard in rule (assume leading) */
+	{
+	/* take the current label from the key */
+	while (k > keystring && k[-1] != '.')
+	  k--;
+	s = k;
+	/* s is the match */
+	/* k is the key trail after the regdom. label */
+	/* nlabels describes k */
+	break;			/* - match */
+	}
+      if (rch == '!')		/* exception rule */
+	{
+	if (!is_regdom)
+	  /* Remove the LH label then treat as a match */
+	  while (TRUE)
+	    if (!*s || *s++ == '.') break;
+
+	/* s is the match, or the regdom */
+	/* nlabels don't care */
+	res = string_copy_taint(s, GET_UNTAINTED);
+	goto found;		/* ER's have priority; stop reading file */
+	}
+
+      s--; k--;
+
+      if (rch != *k)		/* character difference */
+	goto nonmatch;
+      if (k <= keystring && !exception)	/* ran out of key */
+	goto nonmatch;
+      if (s == r)		/* run out of rule */
+	if (k[-1] != '.')	/* key has prefix on rule */
+	  goto nonmatch;
+	else
+	  break;		/* out of rule: s is the match */
+      }
+
+    if (nlabels > res_label_cnt)
+      {				/* new longest match (by cnt of labels) */
+      res = string_copy_taint(s, GET_UNTAINTED);
+      res_label_cnt = nlabels;
+      kmatch = k;
+      }
+
+    nonmatch:
+    }
+  }
+    /* kmatch is the key trail after the regdom. label */
+    /* res_label_cnt describes kmatch */
+
+if (is_regdom && res)		/* prepend label from key to pub-suffix */
+  {
+  s = kmatch;
+  /* back up one label in key */
+  if (s-- <= keystring) goto fail;	/* there must ba a dot */
+  if (s-- <= keystring) goto fail;	/* there must ba at least one ch */
+  while (s > keystring && s[-1] != '.') s--;
+  res = string_sprintf("%.*s%s", (int)(kmatch - s), s, res);
+  }
+
+found:
+
+if (key_utf8 && res)
+  {
+  if (!(*result = string_domain_alabel_to_utf8(res, errmsg)))
+    goto fail;
+  DEBUG(D_lookup)
+    debug_printf_indent("utf8 converting result %q\n to %q\n", res, *result);
+  }
+else
+  *result = res;
+
+rewind(handle);
+return OK;
+
+fail:
+rewind(handle);
+return FAIL;
+}
+
+
+
+/*************************************************
+*  Find entry points for pub-suffix and regdom   *
+*************************************************/
+
+/* See local README for interface description */
+
+static int
+psl_find(void * handle, const uschar * filename, const uschar * keystring,
+  int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+  const uschar * opts)
+{
+return psl_gen_find(handle, keystring, length, result, errmsg, FALSE);
+}
+
+static int
+regdom_find(void * handle, const uschar * filename, const uschar * keystring,
+  int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+  const uschar * opts)
+{
+return psl_gen_find(handle, keystring, length, result, errmsg, TRUE);
+}
+
+
+
+
+/*************************************************
+*         Version reporting entry point          *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+psl_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: psl: Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+static lookup_info psl_lookup_info = {
+  .name =	US"psl",		/* lookup name */
+  .type =	lookup_absfile,		/* lookup from file */
+  .open =	psl_open,		/* open function */
+  .check =	NULL,			/* no check function */
+  .find =	psl_find,		/* find function */
+  .close =	psl_close,		/* close function */
+  .tidy =	NULL,			/* no tidy function */
+  .quote =	NULL,			/* no quoting function */
+  .version_report = psl_version_report	/* version reporting */
+};
+
+static lookup_info regdom_lookup_info = {
+  .name =	US"regdom",		/* lookup name */
+  .type =	lookup_absfile,		/* lookup from file */
+  .open =	psl_open,		/* open function */
+  .check =	NULL,			/* no check function */
+  .find =	regdom_find,		/* find function */
+  .close =	psl_close,		/* close function */
+  .tidy =	NULL,			/* no tidy function */
+  .quote =	NULL,			/* no quoting function */
+  .version_report = NULL		/* no version reporting (redundant) */
+};
+
+#ifdef DYNLOOKUP
+#define psl_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &psl_lookup_info, &regdom_lookup_info };
+lookup_module_info psl_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 2 };
+
+/* End of lookups/psl.c */
diff --git a/src/src/lookups/spf.c b/src/src/lookups/spf.c
index ac03983c2..e1931ca0f 100644
--- a/src/src/lookups/spf.c
+++ b/src/src/lookups/spf.c
@@ -15,13 +15,6 @@ of the License, or (at your option) any later version.
 */
 
 #include "../exim.h"
-
-#ifndef EXIM_HAVE_SPF
-static void dummy(int x);
-static void dummy2(int x) { dummy(x-1); }
-static void dummy(int x) { dummy2(x-1); }
-#else
-
 #include "lf_functions.h"
 
 #ifndef EXPERIMENTAL_SPF_PERL
@@ -119,4 +112,3 @@ static lookup_info spf_lookup_info = {
 static lookup_info *_lookup_list[] = { &spf_lookup_info };
 lookup_module_info spf_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
 
-#endif /* EXIM_HAVE_SPF */
diff --git a/src/src/macro_predef.c b/src/src/macro_predef.c
index 48287131a..caa6bf7c3 100644
--- a/src/src/macro_predef.c
+++ b/src/src/macro_predef.c
@@ -8,7 +8,7 @@
 /* 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 */
+included in the main Exim build. Also, table for various drivers. */
 
 #include "exim.h"
 #include "macro_predef.h"
@@ -203,9 +203,6 @@ due to conflicts with other common macros. */
 #ifdef EXPERIMENTAL_ARC
   builtin_macro_create(US"_HAVE_ARC");
 #endif
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-  builtin_macro_create(US"_HAVE_BRIGHTMAIL");
-#endif
 #ifdef SUPPORT_DANE
   builtin_macro_create(US"_HAVE_DANE");
 #endif
@@ -252,9 +249,6 @@ due to conflicts with other common macros. */
 #ifdef LOOKUP_DSEARCH
   builtin_macro_create(US"_HAVE_LOOKUP_DSEARCH");
 #endif
-#ifdef LOOKUP_IBASE
-  builtin_macro_create(US"_HAVE_LOOKUP_IBASE");
-#endif
 #ifdef LOOKUP_LMDB
   builtin_macro_create(US"_HAVE_LMDB");
   builtin_macro_create(US"_HAVE_LOOKUP_LMDB");
@@ -283,6 +277,10 @@ due to conflicts with other common macros. */
 #ifdef LOOKUP_PGSQL
   builtin_macro_create(US"_HAVE_LOOKUP_PGSQL");
 #endif
+#ifdef LOOKUP_PSL
+  builtin_macro_create(US"_HAVE_LOOKUP_PSL");
+  builtin_macro_create(US"_HAVE_LOOKUP_REGDOM");
+#endif
 #ifdef LOOKUP_REDIS
   builtin_macro_create(US"_HAVE_LOOKUP_REDIS");
 #endif
@@ -322,9 +320,6 @@ exp_features(void)
 #ifdef EXPERIMENTAL_ARC
   builtin_macro_create(US"_EXP_ARC");
 #endif
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-  builtin_macro_create(US"_EXP_BMI");
-#endif
 #ifdef EXPERIMENTAL_DCC
   builtin_macro_create(US"_EXP_DCC");
 #endif
@@ -358,6 +353,41 @@ params_dkim();
 #endif
 }
 
+/******************************************************************************/
+static void
+avail_append(const driver_info * di)
+{
+const uschar * avail_string = di->avail_string;
+if (!avail_string) avail_string = di->driver_name;
+  printf(" US\"%s\",", avail_string);
+}
+
+static void
+avail_list(const driver_info * drtable, const uschar * tag,
+  const uschar * group, BOOL magic)
+{
+printf("const uschar * avail_%s_%s[] = {", group, tag);
+for (const driver_info * di = drtable; di; di = di->next)
+  if (!!di->dyn_magic == magic) avail_append(di);
+printf(" NULL};\n");
+}
+
+static void
+avail_drs(const driver_info * drtable, const uschar * tag)
+{
+avail_list(drtable, tag, US"static", FALSE);
+avail_list(drtable, tag, US"dynamic", TRUE);
+}
+
+static void
+avail(void)
+{
+avail_drs((driver_info *)auths_available, US"auths");
+avail_drs((driver_info *)routers_available, US"routers");
+avail_drs((driver_info *)transports_available, US"transports");
+}
+
+/******************************************************************************/
 
 int
 main(void)
@@ -368,8 +398,10 @@ exp_features();
 options();
 expansions();
 params();
-
 printf("macro_item * macros = &p%u;\n", mp_index-1);
 printf("macro_item * mlast = &p0;\n");
+
+avail();
+
 exit(0);
 }
diff --git a/src/src/macros.h b/src/src/macros.h
index 75563d86b..2e0efc9b8 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -90,10 +90,6 @@ don't make the file descriptors two-way. */
 #define pipe_read  0
 #define pipe_write 1
 
-/* The RFC 1413 ident port */
-
-#define IDENT_PORT 113
-
 /* A macro to simplify testing bits in lookup types */
 
 #define mac_islookup(li,b) ((li)->type & (b))
@@ -385,26 +381,25 @@ enum {
   DEBUG_BIT(filter),
   DEBUG_BIT(hints_lookup),
   DEBUG_BIT(host_lookup),
-  DEBUG_BIT(ident),
   DEBUG_BIT(interface),
   DEBUG_BIT(lists),
-  DEBUG_BIT(load),		/* 15 */
-  DEBUG_BIT(lookup),
+  DEBUG_BIT(load),
+  DEBUG_BIT(lookup),		/* 15 */
   DEBUG_BIT(memory),
   DEBUG_BIT(noutf8),
   DEBUG_BIT(pid),
   DEBUG_BIT(process_info),
   DEBUG_BIT(queue_run),
   DEBUG_BIT(receive),
-  DEBUG_BIT(resolver),		/* 23 */
-  DEBUG_BIT(retry),
+  DEBUG_BIT(resolver),
+  DEBUG_BIT(retry),		/* 23 */
   DEBUG_BIT(rewrite),
   DEBUG_BIT(route),
   DEBUG_BIT(timestamp),
   DEBUG_BIT(tls),
   DEBUG_BIT(transport),
   DEBUG_BIT(uid),
-  DEBUG_BIT(verify),		/* 31 */
+  DEBUG_BIT(verify),		/* 30 - one spare! */
 };
 
 /* Multi-bit debug masks */
@@ -474,7 +469,6 @@ enum logbit {
   Li_dkim,
   Li_dkim_verbose,
   Li_dnssec,
-  Li_ident_timeout,
   Li_incoming_interface,
   Li_incoming_port,
   Li_millisec,
diff --git a/src/src/queue.c b/src/src/queue.c
index b3d972f41..97238ba1a 100644
--- a/src/src/queue.c
+++ b/src/src/queue.c
@@ -351,7 +351,8 @@ Returns:     nothing
 */
 
 void
-queue_run(qrunner * q, const uschar * start_id, const uschar * stop_id, BOOL recurse)
+queue_run(qrunner * q, const uschar * start_id, const uschar * stop_id,
+  BOOL recurse)
 {
 BOOL force_delivery = q->queue_run_force
   || deliver_selectstring || deliver_selectstring_sender;
@@ -519,6 +520,10 @@ for (int i = queue_run_in_order ? -1 : 0;
 #endif
 	      nelem(qpid) - 1])
 	{		/* The child table is maxed out; wait for the oldest */
+			/*XXX It might be nicer to wait for the first one
+			that happens to complete. */
+	set_process_info("running queue (ph 1): parallel limit %u",
+			nelem(qpid));
 	DEBUG(D_queue_run)
 	  debug_printf("q2stage waiting for child %d\n", (int)qpid[0]);
 	waitpid(qpid[0], NULL, 0);
@@ -799,15 +804,20 @@ turned off. */
 
 if (q->queue_2stage)
   {
+  unsigned active;
+  for (active = 0; active < nelem(qpid); active++)
+    if (!qpid[active]) { --active; break; }
 
-  /* wait for last children */
-  for (int i = 0; i < nelem(qpid); i++)
+  /* wait for all first-stage children */
+  for (unsigned i = 0; i < nelem(qpid); i++)
     if (qpid[i])
       {
-      DEBUG(D_queue_run) debug_printf("q2stage reaped child %d\n", (int)qpid[i]);
+      set_process_info("running queue (ph 1): wait-all, child %u/%u",
+		      i+1, active);
       waitpid(qpid[i], NULL, 0);
+      DEBUG(D_queue_run) debug_printf("q2stage reaped child %d\n", (int)qpid[i]);
       }
-    else break;
+    else break;		/* should be no holes in table, so we're done */
 
 #ifdef MEASURE_TIMING
   report_time_since(&timestamp_startup, US"queue_run phase 1 done");
diff --git a/src/src/readconf.c b/src/src/readconf.c
index cec89f9bb..f4da20b6a 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -79,9 +79,6 @@ static optionlist optionlist_config[] = {
   { "av_scanner",               opt_stringptr,   {&av_scanner} },
 #endif
   { "bi_command",               opt_stringptr,   {&bi_command} },
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-  { "bmi_config_file",          opt_stringptr,   {&bmi_config_file} },
-#endif
   { "bounce_message_file",      opt_stringptr,   {&bounce_message_file} },
   { "bounce_message_text",      opt_stringptr,   {&bounce_message_text} },
   { "bounce_return_body",       opt_bool,        {&bounce_return_body} },
@@ -143,9 +140,6 @@ static optionlist optionlist_config[] = {
   { "dns_retry",                opt_int,         {&dns_retry} },
   { "dns_trust_aa",             opt_stringptr,   {&dns_trust_aa} },
   { "dns_use_edns0",            opt_int,         {&dns_use_edns0} },
- /* This option is now a no-op, retained for compatibility */
-  { "drop_cr",                  opt_bool,        {&drop_cr} },
-/*********************************************************/
   { "dsn_advertise_hosts",      opt_stringptr,   {&dsn_advertise_hosts} },
   { "dsn_from",                 opt_stringptr,   {&dsn_from} },
   { "envelope_to_remove",       opt_bool,        {&envelope_to_remove} },
@@ -191,9 +185,6 @@ static optionlist optionlist_config[] = {
   { "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
   { "ignore_bounce_errors_after", opt_time,      {&ignore_bounce_errors_after} },
   { "ignore_fromline_hosts",    opt_stringptr,   {&ignore_fromline_hosts} },
@@ -307,9 +298,6 @@ static optionlist optionlist_config[] = {
   { "retry_interval_max",       opt_time,        {&retry_interval_max} },
   { "return_path_remove",       opt_bool,        {&return_path_remove} },
   { "return_size_limit",        opt_mkint|opt_hidden, {&bounce_return_size_limit} },
-  { "rfc1413_hosts",            opt_stringptr,   {&rfc1413_hosts} },
-  { "rfc1413_port",		opt_int|opt_hidden, {&test_harness_identd_port} },
-  { "rfc1413_query_timeout",    opt_time,        {&rfc1413_query_timeout} },
   { "sender_unqualified_hosts", opt_stringptr,   {&sender_unqualified_hosts} },
   { "slow_lookup_log",          opt_int,         {&slow_lookup_log} },
   { "smtp_accept_keepalive",    opt_bool,        {&smtp_accept_keepalive} },
@@ -987,8 +975,13 @@ if (*s) for (macro_item * m = *s == '_' ? macros : macros_user; m; m = m->next)
     {
     int moveby;
 
-    EARLY_DEBUG(D_any, "%s: matched '%s' in '%.*s'\n", __FUNCTION__,
-      m->name, (int) Ustrlen(ss)-1, ss);
+    DEBUG(D_any)
+      if (f.expansion_test)
+	printf("macro '%s' -> '%s'\n", m->name, m->replacement);
+      else
+	EARLY_DEBUG(D_any, "%s: matched '%s' in '%.*s'\n", __FUNCTION__,
+	  m->name, (int) Ustrlen(ss)-1, ss);
+
     /* Expand the buffer if necessary */
 
     while (*newlen - m->namelen + m->replen + 1 > big_buffer_size)
@@ -3824,13 +3817,12 @@ for (di = *info_anchor; di; di = di->next)
 /* Potentially a loadable module. Look for a file with the right name. */
 
 if (!(dd = exim_opendir(CUS LOOKUP_MODULE_DIR)))
-  {
   log_write(0, LOG_MAIN|LOG_PANIC,
-	    "Couldn't open %s: not loading driver modules\n", LOOKUP_MODULE_DIR);
-  }
+	  "Couldn't open %s: not loading driver modules\n", LOOKUP_MODULE_DIR);
 else
   {
-  uschar * fname = string_sprintf("%s_%s." DYNLIB_FN_EXT, d->driver_name, class), * sname;
+  uschar * fname = string_sprintf("%s_%s." DYNLIB_FN_EXT,
+				  d->driver_name, class);
   const char * errormsg;
 
   DEBUG(D_any) debug_printf("Loading %s %s driver from %s\n",
diff --git a/src/src/receive.c b/src/src/receive.c
index 9d8bfa89c..cde09c40a 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -533,11 +533,6 @@ if (recipients_count >= recipients_list_max)
 
 recipients_list[recipients_count].address = recipient;
 recipients_list[recipients_count].pno = pno;
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-recipients_list[recipients_count].bmi_optin = bmi_current_optin;
-/* reset optin string pointer for next recipient */
-bmi_current_optin = NULL;
-#endif
 recipients_list[recipients_count].orcpt = NULL;
 recipients_list[recipients_count].dsn_flags = 0;
 recipients_list[recipients_count++].errors_to = NULL;
@@ -3982,14 +3977,6 @@ if (fake_response != OK)
 
 f.deliver_firsttime = TRUE;
 
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-if (bmi_run == 1)
-  { /* rewind data file */
-  lseek(data_fd, (long int)spool_data_start_offset(message_id), SEEK_SET);
-  bmi_verdicts = bmi_process_message(header_list, data_fd);
-  }
-#endif
-
 /* Update the timestamp in our Received: header to account for any time taken by
 an ACL or by local_scan(). The new time is the time that all reception
 processing is complete. */
diff --git a/src/src/route.c b/src/src/route.c
index 534198a85..778e524c1 100644
--- a/src/src/route.c
+++ b/src/src/route.c
@@ -35,16 +35,6 @@ optionlist optionlist_routers[] = {
                  LOFF(address_data) },
   { "address_test",       opt_bool|opt_public,
                  LOFF(address_test) },
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-  { "bmi_deliver_alternate",   opt_bool | opt_public,
-                 LOFF(bmi_deliver_alternate) },
-  { "bmi_deliver_default",   opt_bool | opt_public,
-                 LOFF(bmi_deliver_default) },
-  { "bmi_dont_deliver",   opt_bool | opt_public,
-                 LOFF(bmi_dont_deliver) },
-  { "bmi_rule",           opt_stringptr|opt_public,
-                 LOFF(bmi_rule) },
-#endif
   { "cannot_route_message", opt_stringptr | opt_public,
                  LOFF(cannot_route_message) },
   { "caseful_local_part", opt_bool | opt_public,
@@ -1095,48 +1085,6 @@ if (r->condition)
     }
   }
 
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-/* check if a specific Brightmail AntiSpam rule fired on the message */
-if (r->bmi_rule)
-  {
-  DEBUG(D_route) debug_printf_indent("checking bmi_rule\n");
-  if (bmi_check_rule(bmi_base64_verdict, r->bmi_rule) == 0)
-    {    /* none of the rules fired */
-    DEBUG(D_route)
-      debug_printf_indent("%s router skipped: none of bmi_rule rules fired\n", rname);
-    return SKIP;
-    }
-  }
-
-/* check if message should not be delivered */
-if (r->bmi_dont_deliver && bmi_deliver == 1)
-  {
-  DEBUG(D_route)
-    debug_printf_indent("%s router skipped: bmi_dont_deliver is FALSE\n", rname);
-  return SKIP;
-  }
-
-/* check if message should go to an alternate location */
-if (  r->bmi_deliver_alternate
-   && (bmi_deliver == 0 || !bmi_alt_location)
-   )
-  {
-  DEBUG(D_route)
-    debug_printf_indent("%s router skipped: bmi_deliver_alternate is FALSE\n", rname);
-  return SKIP;
-  }
-
-/* check if message should go to default location */
-if (  r->bmi_deliver_default
-   && (bmi_deliver == 0 || bmi_alt_location)
-   )
-  {
-  DEBUG(D_route)
-    debug_printf_indent("%s router skipped: bmi_deliver_default is FALSE\n", rname);
-  return SKIP;
-  }
-#endif
-
 /* All the checks passed. */
 
 return OK;
diff --git a/src/src/routers/accept.c b/src/src/routers/accept.c
index 09992b85f..9986c39aa 100644
--- a/src/src/routers/accept.c
+++ b/src/src/routers/accept.c
@@ -156,7 +156,7 @@ router_info accept_router_info =
   .options_block =	&accept_router_option_defaults,
   .options_len =	sizeof(accept_router_options_block),
   .init =		accept_router_init,
-# ifdef DYNLOOKUP
+# if ROUTER_ACCEPT==2
   .dyn_magic =		ROUTER_MAGIC,
 # endif
   },
diff --git a/src/src/routers/dnslookup.c b/src/src/routers/dnslookup.c
index 97e273dca..5df6ea8f9 100644
--- a/src/src/routers/dnslookup.c
+++ b/src/src/routers/dnslookup.c
@@ -484,7 +484,7 @@ router_info dnslookup_router_info =
   .options_block =	&dnslookup_router_option_defaults,
   .options_len =	sizeof(dnslookup_router_options_block),
   .init =		dnslookup_router_init,
-# ifdef DYNLOOKUP
+# if ROUTER_DNSLOOKUP==2
   .dyn_magic =		ROUTER_MAGIC,
 # endif
   },
diff --git a/src/src/routers/ipliteral.c b/src/src/routers/ipliteral.c
index 9052f2d48..022592c9e 100644
--- a/src/src/routers/ipliteral.c
+++ b/src/src/routers/ipliteral.c
@@ -218,7 +218,7 @@ router_info ipliteral_router_info =
   .options_block =	&ipliteral_router_option_defaults,
   .options_len =	sizeof(ipliteral_router_options_block),
   .init =		ipliteral_router_init,
-# ifdef DYNLOOKUP
+# if ROUTER_IPLITERAL==2
   .dyn_magic =		ROUTER_MAGIC,
 # endif
   },
diff --git a/src/src/routers/iplookup.c b/src/src/routers/iplookup.c
index 1f3945bc8..5d9c04d71 100644
--- a/src/src/routers/iplookup.c
+++ b/src/src/routers/iplookup.c
@@ -433,7 +433,7 @@ router_info iplookup_router_info =
   .options_block =	&iplookup_router_option_defaults,
   .options_len =	sizeof(iplookup_router_options_block),
   .init =		iplookup_router_init,
-# ifdef DYNLOOKUP
+# if ROUTER_IPLOOKUP==2
   .dyn_magic =		ROUTER_MAGIC,
 # endif
   },
diff --git a/src/src/routers/manualroute.c b/src/src/routers/manualroute.c
index 5a4c72bb0..589db7ead 100644
--- a/src/src/routers/manualroute.c
+++ b/src/src/routers/manualroute.c
@@ -517,7 +517,7 @@ router_info manualroute_router_info =
   .options_block =	&manualroute_router_option_defaults,
   .options_len =	sizeof(manualroute_router_options_block),
   .init =		manualroute_router_init,
-# ifdef DYNLOOKUP
+# if ROUTER_MANUALROUTE==2
   .dyn_magic =		ROUTER_MAGIC,
 # endif
   },
diff --git a/src/src/routers/queryprogram.c b/src/src/routers/queryprogram.c
index 92a2524ab..3e12ec0d7 100644
--- a/src/src/routers/queryprogram.c
+++ b/src/src/routers/queryprogram.c
@@ -549,7 +549,7 @@ router_info queryprogram_router_info =
   .options_block =    &queryprogram_router_option_defaults,
   .options_len =      sizeof(queryprogram_router_options_block),
   .init =             queryprogram_router_init,
-# ifdef DYNLOOKUP
+# if ROUTER_QUERYPROGRAM==2
   .dyn_magic =	      ROUTER_MAGIC,
 # endif
   },
diff --git a/src/src/routers/redirect.c b/src/src/routers/redirect.c
index 6d0574c14..e90ad2147 100644
--- a/src/src/routers/redirect.c
+++ b/src/src/routers/redirect.c
@@ -804,7 +804,7 @@ router_info redirect_router_info =
   .options_block =	&redirect_router_option_defaults,
   .options_len =	sizeof(redirect_router_options_block),
   .init =		redirect_router_init,
-# ifdef DYNLOOKUP
+# if ROUTER_REDIRECT==2
   .dyn_magic =		ROUTER_MAGIC,
 # endif
   },
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index b5a054a16..e6c9dbacc 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -1780,10 +1780,7 @@ memset(sender_address_cache, 0, sizeof(sender_address_cache));
 memset(sender_domain_cache, 0, sizeof(sender_domain_cache));
 
 authenticated_sender = NULL;
-#ifdef EXPERIMENTAL_BRIGHTMAIL
- bmi_run = 0;
- bmi_verdicts = NULL;
-#endif
+dnslist_domain = dnslist_matched = NULL;
 
 dsn_ret = 0;
 dsn_envid = NULL;
diff --git a/src/src/spool_in.c b/src/src/spool_in.c
index ff9edfc2e..b475c3955 100644
--- a/src/src/spool_in.c
+++ b/src/src/spool_in.c
@@ -263,11 +263,6 @@ f.spool_file_wireformat = FALSE;
 #endif
 tree_nonrecipients = NULL;
 
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-bmi_run = 0;
-bmi_verdicts = NULL;
-#endif
-
 #ifndef DISABLE_DKIM
 f.dkim_disable_verify = FALSE;
 # ifdef COMPILE_UTILITY
@@ -615,10 +610,6 @@ for (;;)
       body_linecount = Uatoi(var + 14);
     else if (Ustrncmp(p, "ody_zerocount", 13) == 0)
       body_zerocount = Uatoi(var + 14);
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-    else if (Ustrncmp(p, "mi_verdicts ", 12) == 0)
-      bmi_verdicts = string_copy_taint(var + 13, proto_mem);
-#endif
     break;
 
     case 'd':
diff --git a/src/src/spool_out.c b/src/src/spool_out.c
index e5cded966..91b776df2 100644
--- a/src/src/spool_out.c
+++ b/src/src/spool_out.c
@@ -269,15 +269,12 @@ if (spam_score_int) spool_var_write(fp, US"spam_score_int", spam_score_int);
 if (f.deliver_manual_thaw) fprintf(fp, "-manual_thaw\n");
 if (f.sender_set_untrusted) fprintf(fp, "-sender_set_untrusted\n");
 
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-if (bmi_verdicts) spool_var_write(fp, US"bmi_verdicts", bmi_verdicts);
-#endif
-
 #ifndef DISABLE_TLS
 if (tls_in.certificate_verified) fprintf(fp, "-tls_certificate_verified\n");
 if (tls_in.cipher) spool_var_write(fp, US"tls_cipher", tls_in.cipher);
 if (tls_in.peercert)
   {
+  /* -- marks as tainted */
   if (tls_export_cert(big_buffer, big_buffer_size, tls_in.peercert))
     fprintf(fp, "--tls_peercert %s\n", CS big_buffer);
   }
diff --git a/src/src/structs.h b/src/src/structs.h
index 8f3eaacbf..147819dd5 100644
--- a/src/src/structs.h
+++ b/src/src/structs.h
@@ -153,6 +153,7 @@ typedef struct driver_instance {
 typedef struct driver_info {
   struct driver_info * next;
   uschar *driver_name;            /* Name of driver */
+  uschar *avail_string;		  /* if set, display rather than name */
 
   optionlist *options;            /* Table of private options names */
   int    *options_count;          /* -> Number of entries in table */
@@ -299,9 +300,6 @@ typedef struct router_instance {
   driver_instance drinst;
 
   uschar *address_data;           /* Arbitrary data */
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-  uschar *bmi_rule;               /* Brightmail AntiSpam rule checking */
-#endif
   uschar *cannot_route_message;   /* Used when routing fails */
   uschar *condition;              /* General condition */
   uschar *current_directory;      /* For use during delivery */
@@ -330,11 +328,6 @@ typedef struct router_instance {
   uschar *transport_name;         /* Transport name */
 
   BOOL    address_test;           /* Use this router when testing addresses */
-#ifdef EXPERIMENTAL_BRIGHTMAIL
-  BOOL    bmi_deliver_alternate;  /* TRUE => BMI said that message should be delivered to alternate location */
-  BOOL    bmi_deliver_default;    /* TRUE => BMI said that message should be delivered to default location */
-  BOOL    bmi_dont_deliver;       /* TRUE => BMI said that message should not be delivered at all */
-#endif
   BOOL    expn;                   /* Use this router when processing EXPN */
   BOOL    caseful_local_part;     /* TRUE => don't lowercase */
   BOOL    check_local_user;       /* TRUE => check local user */
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index 66359bb3e..80fb419d0 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -578,16 +578,23 @@ msg = rc == GNUTLS_E_FATAL_ALERT_RECEIVED
 
 (void) tls_error(when, msg, state->host, &errstr);
 
-if (state->host)
-  log_write(0, LOG_MAIN, "H=%s [%s] TLS error on connection %s",
-    state->host->name, state->host->address, errstr);
-else
+if (!state->host)
   {
   uschar * conn_info = smtp_get_connection_info();
   if (Ustrncmp(conn_info, US"SMTP ", 5) == 0) conn_info += 5;
   /* I'd like to get separated H= here, but too hard for now */
   log_write(0, LOG_MAIN, "TLS error on %s %s", conn_info, errstr);
   }
+else if (  !tls_out.smtp_quit
+#ifdef GNUTLS_E_PREMATURE_TERMINATION
+	|| rc != GNUTLS_E_PREMATURE_TERMINATION
+#endif
+	)
+  log_write(0, LOG_MAIN, "H=%s [%s] TLS error on connection %s",
+    state->host->name, state->host->address, errstr);
+else DEBUG(D_tls)
+  debug_printf("H=%s [%s] TLS error on connection %s\n",
+    state->host->name, state->host->address, errstr);
 }
 
 
@@ -3523,7 +3530,7 @@ if (gnutls_session_get_flags(session) & GNUTLS_SFLAGS_SESSION_TICKET)
       debug_printf(" extract session data: %s\n", US gnutls_strerror(rc));
       }
   else DEBUG(D_tls)
-      debug_printf(" host not resmable; not saving ticket\n");
+      debug_printf(" host not resumable; not saving ticket\n");
   }
 }
 
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 66661e94e..d5a6d368d 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -509,6 +509,22 @@ return host ? FAIL : DEFER;
 }
 
 
+static void
+tls_debug_err(SSL * ssl, uschar * where, int rc)
+{
+BIO * bio = BIO_new(BIO_s_mem());
+char * buf = NULL;
+size_t len;
+int error = SSL_get_error(ssl, rc);
+
+ERR_error_string_n(ERR_peek_error(), ssl_errstring, sizeof(ssl_errstring));
+ERR_print_errors(bio);
+len = BIO_get_mem_data(bio, &buf);
+debug_printf("%s: error %d\n%.*s\n", where, error, (int)len, buf);
+BIO_free (bio);
+}
+
+
 
 /**************************************************
 * General library initalisation                   *
@@ -983,24 +999,24 @@ DEBUG(D_tls)
   if (where & SSL_CB_HANDSHAKE_DONE)  g = string_append_listele(g, ',', US"hshake_done");
 
   if (where & SSL_CB_LOOP)
-     debug_printf("SSL %s: %s\n", g->s, SSL_state_string_long(s));
+     debug_printf("SSL %Y: %s\n", g, SSL_state_string_long(s));
   else if (where & SSL_CB_ALERT)
-    debug_printf("SSL %s %s:%s\n", g->s,
+    debug_printf("SSL %Y %s:%s\n", g,
 	  SSL_alert_type_string_long(ret), SSL_alert_desc_string_long(ret));
   else if (where & SSL_CB_EXIT)
     {
     if (ret <= 0)
-      debug_printf("SSL %s: %s in %s\n", g->s,
+      debug_printf("SSL %Y: %s in %s\n", g,
 	ret == 0 ? "failed" : "error", 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));
+     debug_printf("SSL %Y: %s\n", g, SSL_state_string_long(s));
   }
 }
 
 #ifdef OPENSSL_HAVE_KEYLOG_CB
 static void
-keylog_callback(const SSL *ssl, const char *line)
+keylog_callback(const SSL * ssl, const char * line)
 {
 char * filename;
 FILE * fp;
@@ -4533,11 +4549,10 @@ static BOOL
 tls_refill(unsigned lim)
 {
 SSL * ssl = state_server.lib_state.lib_ssl;
-int error;
-int inbytes;
+int error, inbytes;
 
-DEBUG(D_tls) debug_printf("Calling SSL_read(%p, %p, %u)\n", ssl,
-  ssl_xfer_buffer, ssl_xfer_buffer_size);
+DEBUG(D_tls) debug_printf("Calling SSL_read(tls_refill %p, %p, %u)\n",
+  ssl, ssl_xfer_buffer, ssl_xfer_buffer_size);
 
 ERR_clear_error();
 if (smtp_receive_timeout > 0) ALARM(smtp_receive_timeout);
@@ -4579,8 +4594,9 @@ switch(error)
     uschar * conn_info = smtp_get_connection_info();
     if (Ustrncmp(conn_info, US"SMTP ", 5) == 0) conn_info += 5;
     /* I'd like to get separated H= here, but too hard for now */
-    ERR_error_string_n(ERR_get_error(), ssl_errstring, sizeof(ssl_errstring));
+    ERR_error_string_n(ERR_peek_error(), ssl_errstring, sizeof(ssl_errstring));
     log_write(0, LOG_MAIN, "TLS error (SSL_read): on %s %s", conn_info, ssl_errstring);
+    DEBUG(D_tls) tls_debug_err(ssl, US"SSL_read", inbytes);
     ssl_xfer_error = TRUE;
     return FALSE;
     }
@@ -4701,29 +4717,30 @@ Only used by the client-side TLS.
 */
 
 int
-tls_read(void * ct_ctx, uschar *buff, size_t len)
+tls_read(void * ct_ctx, uschar * buff, size_t len)
 {
 SSL * ssl = ct_ctx ? ((exim_openssl_client_tls_ctx *)ct_ctx)->ssl
 		  : state_server.lib_state.lib_ssl;
 int inbytes;
 int error;
 
-DEBUG(D_tls) debug_printf("Calling SSL_read(%p, %p, %u)\n", ssl,
-  buff, (unsigned int)len);
+DEBUG(D_tls) debug_printf("Calling SSL_read(tls_read %p, %p, %u)\n",
+  ssl, buff, (unsigned int)len);
 
 ERR_clear_error();
 inbytes = SSL_read(ssl, CS buff, len);
 error = SSL_get_error(ssl, inbytes);
 
-if (error == SSL_ERROR_ZERO_RETURN)
-  {
-  DEBUG(D_tls) debug_printf("Got SSL_ERROR_ZERO_RETURN\n");
-  return -1;
-  }
-else if (error != SSL_ERROR_NONE)
-  return -1;
+if (error == SSL_ERROR_NONE)
+  return inbytes;
 
-return inbytes;
+else DEBUG(D_tls)
+  if (error == SSL_ERROR_ZERO_RETURN)
+    debug_printf("Got SSL_ERROR_ZERO_RETURN\n");
+  else
+    tls_debug_err(ssl, US"SSL_read", inbytes);
+ERR_clear_error();
+return -1;
 }
 
 
@@ -4861,12 +4878,9 @@ if ((o_ctx ? tls_out.active.sock : tls_in.active.sock) < 0)
 tls_write(ct_ctx, NULL, 0, FALSE);	/* flush write buffer */
 
 HDEBUG(D_transport|D_tls|D_acl|D_v) debug_printf_indent("  SMTP(TLS shutdown)>>\n");
-rc = SSL_shutdown(ssl);
-if (rc < 0) DEBUG(D_tls)
-  {
-  ERR_error_string_n(ERR_get_error(), ssl_errstring, sizeof(ssl_errstring));
-  debug_printf("SSL_shutdown: %s\n", ssl_errstring);
-  }
+ERR_clear_error();
+if ((rc = SSL_shutdown(ssl)) < 0)
+  DEBUG(D_tls) tls_debug_err(ssl, US"SSL_shutdown", rc);
 }
 
 /*************************************************
@@ -4905,6 +4919,7 @@ if (do_shutdown > TLS_NO_SHUTDOWN)
 
   tls_write(ct_ctx, NULL, 0, FALSE);	/* flush write buffer */
 
+  ERR_clear_error();
   if (  (  do_shutdown >= TLS_SHUTDOWN_WONLY
 	|| (rc = SSL_shutdown(*sslp)) == 0	/* send "close notify" alert */
 	)
@@ -4919,11 +4934,7 @@ if (do_shutdown > TLS_NO_SHUTDOWN)
     ALARM_CLR(0);
     }
 
-  if (rc < 0) DEBUG(D_tls)
-    {
-    ERR_error_string_n(ERR_get_error(), ssl_errstring, sizeof(ssl_errstring));
-    debug_printf("SSL_shutdown: %s\n", ssl_errstring);
-    }
+  if (rc < 0) DEBUG(D_tls) tls_debug_err(*sslp, US"SSL_shutdown", rc);
   }
 
 if (!o_ctx)		/* server side */
diff --git a/src/src/tlscert-gnu.c b/src/src/tlscert-gnu.c
index 6bbea7516..43d362904 100644
--- a/src/src/tlscert-gnu.c
+++ b/src/src/tlscert-gnu.c
@@ -150,6 +150,7 @@ if ((ret = gnutls_x509_crt_get_issuer_dn(cert, CS cp, &siz))
     != GNUTLS_E_SHORT_MEMORY_BUFFER)
   return g_err("gi0", __FUNCTION__, ret);
 
+/*XXX we might want to distinguish ourcert from peercert (but this is safe) */
 cp = store_get(siz, GET_TAINTED);
 if ((ret = gnutls_x509_crt_get_issuer_dn(cert, CS cp, &siz)) < 0)
   return g_err("gi1", __FUNCTION__, ret);
@@ -226,14 +227,15 @@ return algo < 0 ? NULL : string_copy(US gnutls_sign_get_name(algo));
 uschar *
 tls_cert_subject(void * cert, const uschar * mod)
 {
-uschar * cp = NULL;
+uschar * cp;
 int ret;
 size_t siz = 0;
 
-if ((ret = gnutls_x509_crt_get_dn(cert, CS cp, &siz))
+if ((ret = gnutls_x509_crt_get_dn(cert, NULL, &siz))
     != GNUTLS_E_SHORT_MEMORY_BUFFER)
   return g_err("gs0", __FUNCTION__, ret);
 
+/*XXX we might want to distinguish ourcert from peercert (but this is safe) */
 cp = store_get(siz, GET_TAINTED);
 if ((ret = gnutls_x509_crt_get_dn(cert, CS cp, &siz)) < 0)
   return g_err("gs1", __FUNCTION__, ret);
@@ -262,6 +264,7 @@ ret = gnutls_x509_crt_get_extension_by_oid ((gnutls_x509_crt_t)cert,
 if (ret != GNUTLS_E_SHORT_MEMORY_BUFFER)
   return g_err("ge0", __FUNCTION__, ret);
 
+/*XXX we might want to distinguish ourcert from peercert (but this is safe) */
 cp1 = store_get(siz*4 + 1, GET_TAINTED);
 
 ret = gnutls_x509_crt_get_extension_by_oid ((gnutls_x509_crt_t)cert,
@@ -317,6 +320,7 @@ for (int index = 0;; index++)
       return g_err("gs0", __FUNCTION__, ret);
     }
 
+  /*XXX we might want to distinguish ourcert from peercert (but this is safe) */
   ele = store_get(siz+1, GET_TAINTED);
   if ((ret = gnutls_x509_crt_get_subject_alt_name(
     (gnutls_x509_crt_t)cert, index, ele, &siz, NULL)) < 0)
@@ -347,7 +351,8 @@ tls_cert_ocsp_uri(void * cert, const uschar * mod)
 gnutls_datum_t uri;
 int ret;
 uschar sep = '\n';
-gstring * list = NULL;
+/*XXX we might want to distinguish ourcert from peercert (but this is safe) */
+gstring * list = string_get_tainted(0, GET_TAINTED);
 
 if (mod)
   if (*mod == '>' && *++mod) sep = *mod++;
@@ -381,7 +386,8 @@ tls_cert_crl_uri(void * cert, const uschar * mod)
 {
 int ret;
 uschar sep = '\n';
-gstring * list = NULL;
+/*XXX we might want to distinguish ourcert from peercert (but this is safe) */
+gstring * list = string_get_tainted(0, GET_TAINTED);
 
 if (mod)
   if (*mod == '>' && *++mod) sep = *mod++;
@@ -394,7 +400,7 @@ for (int index = 0;; index++)
     (gnutls_x509_crt_t)cert, index, NULL, &siz, NULL, NULL))
     {
     case GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE:
-      return string_from_gstring(list);
+      return gstring_length(list) > 0 ? string_from_gstring(list) : NULL;
     case GNUTLS_E_SHORT_MEMORY_BUFFER:
       break;
     default:
@@ -449,6 +455,7 @@ if ((ret = gnutls_x509_crt_get_fingerprint(cert, algo, NULL, &siz))
     != GNUTLS_E_SHORT_MEMORY_BUFFER)
   return g_err("gf0", __FUNCTION__, ret);
 
+/*XXX we might want to distinguish ourcert from peercert (but this is safe) */
 cp = store_get(siz*3+1, GET_TAINTED);
 if ((ret = gnutls_x509_crt_get_fingerprint(cert, algo, cp, &siz)) < 0)
   return g_err("gf1", __FUNCTION__, ret);
diff --git a/src/src/tlscert-openssl.c b/src/src/tlscert-openssl.c
index cdac8f6b2..5e3ce22a4 100644
--- a/src/src/tlscert-openssl.c
+++ b/src/src/tlscert-openssl.c
@@ -213,7 +213,9 @@ uschar *
 tls_cert_issuer(void * cert, const uschar * mod)
 {
 uschar * cp = x509_name_copy(X509_get_issuer_name((X509 *)cert));
-return mod ? tls_field_from_dn(cp, mod) : cp;
+if (mod) cp = tls_field_from_dn(cp, mod);
+/*XXX we might want to distinguish ourcert from peercert (but this is safe) */
+return cp ? string_copy_taint(cp, GET_TAINTED) : cp;
 }
 
 uschar *
@@ -312,7 +314,9 @@ uschar *
 tls_cert_subject(void * cert, const uschar * mod)
 {
 uschar * cp = x509_name_copy(X509_get_subject_name((X509 *)cert));
-return mod ? tls_field_from_dn(cp, mod) : cp;
+if (mod) cp = tls_field_from_dn(cp, mod);
+/*XXX we might want to distinguish ourcert from peercert (but this is safe) */
+return cp ? string_copy_taint(cp, GET_TAINTED) : cp;
 }
 
 uschar *
@@ -345,6 +349,7 @@ M_ASN1_OCTET_STRING_print(bp, adata);
 /* binary data, DER encoded */
 /* just dump for now */
 len = BIO_get_mem_data(bp, &cp1);
+/*XXX we might want to distinguish ourcert from peercert (but this is safe) */
 cp3 = cp2 = store_get(len*3+1, GET_TAINTED);
 
 while(len)
@@ -360,14 +365,13 @@ return cp3;
 uschar *
 tls_cert_subject_altname(void * cert, const uschar * mod)
 {
-gstring * list = NULL;
 STACK_OF(GENERAL_NAME) * san = (STACK_OF(GENERAL_NAME) *)
   X509_get_ext_d2i((X509 *)cert, NID_subject_alt_name, NULL, NULL);
 uschar osep = '\n';
-uschar * tag = US"";
-uschar * ele;
-int match = -1;
-int len;
+uschar * tag = US"", * ele;
+int match = -1,  len;
+/*XXX we might want to distinguish ourcert from peercert (but this is safe) */
+gstring * list = string_get_tainted(0, GET_TAINTED);
 
 if (!san) return NULL;
 
@@ -417,7 +421,7 @@ while (sk_GENERAL_NAME_num(san) > 0)
   }
 
 sk_GENERAL_NAME_free(san);
-return string_from_gstring(list);
+return gstring_length(list) > 0 ? string_from_gstring(list) : NULL;
 }
 
 uschar *
@@ -427,7 +431,8 @@ STACK_OF(ACCESS_DESCRIPTION) * ads = (STACK_OF(ACCESS_DESCRIPTION) *)
   X509_get_ext_d2i((X509 *)cert, NID_info_access, NULL, NULL);
 int adsnum = sk_ACCESS_DESCRIPTION_num(ads);
 uschar sep = '\n';
-gstring * list = NULL;
+/*XXX we might want to distinguish ourcert from peercert (but this is safe) */
+gstring * list = string_get_tainted(0, GET_TAINTED);
 
 if (mod)
   if (*mod == '>' && *++mod) sep = *mod++;
@@ -442,7 +447,7 @@ for (int i = 0; i < adsnum; i++)
       ASN1_STRING_length(ad->location->d.ia5));
   }
 sk_ACCESS_DESCRIPTION_free(ads);
-return string_from_gstring(list);
+return gstring_length(list) > 0 ? string_from_gstring(list) : NULL;
 }
 
 uschar *
@@ -453,7 +458,8 @@ STACK_OF(DIST_POINT) * dps = (STACK_OF(DIST_POINT) *)
     NULL, NULL);
 DIST_POINT * dp;
 uschar sep = '\n';
-gstring * list = NULL;
+/*XXX we might want to distinguish ourcert from peercert (but this is safe) */
+gstring * list = string_get_tainted(0, GET_TAINTED);
 
 if (mod)
   if (*mod == '>' && *++mod) sep = *mod++;
@@ -473,7 +479,7 @@ if (dps) for (int i = 0, dpsnum = sk_DIST_POINT_num(dps); i < dpsnum; i++)
 	  ASN1_STRING_length(np->d.uniformResourceIdentifier));
     }
 sk_DIST_POINT_free(dps);
-return string_from_gstring(list);
+return gstring_length(list) > 0 ? string_from_gstring(list) : NULL;
 }
 
 
diff --git a/src/src/transports/appendfile.c b/src/src/transports/appendfile.c
index ebf11b7f7..cad14b889 100644
--- a/src/src/transports/appendfile.c
+++ b/src/src/transports/appendfile.c
@@ -3343,12 +3343,23 @@ ret_panic:
 transport_info appendfile_transport_info = {
 .drinfo = {
   .driver_name =	US"appendfile",
+  .avail_string =	US" appendfile"
+# ifdef SUPPORT_MAILDIR
+			  "/maildir"
+# endif
+# ifdef SUPPORT_MAILSTORE
+			  "/mailstore"
+# endif
+# ifdef SUPPORT_MBX
+			  "/mbx"
+# endif
+			,
   .options =		appendfile_transport_options,
   .options_count =	&appendfile_transport_options_count,
   .options_block =	&appendfile_transport_option_defaults,	/* private options defaults */
   .options_len =	sizeof(appendfile_transport_options_block),
   .init =		appendfile_transport_init,
-# ifdef DYNLOOKUP
+# if TRANSPORT_APPENDFILE==2
   .dyn_magic =		TRANSPORT_MAGIC,
 # endif
   },
diff --git a/src/src/transports/autoreply.c b/src/src/transports/autoreply.c
index 62eb888d0..d9d9d7a9d 100644
--- a/src/src/transports/autoreply.c
+++ b/src/src/transports/autoreply.c
@@ -817,7 +817,7 @@ transport_info autoreply_transport_info = {
   .options_block =	&autoreply_transport_option_defaults,
   .options_len =	sizeof(autoreply_transport_options_block),
   .init =		autoreply_transport_init,
-# ifdef DYNLOOKUP
+# if TRANSPORT_AUTOREPLY==2
   .dyn_magic =		TRANSPORT_MAGIC,
 # endif
   },
diff --git a/src/src/transports/lmtp.c b/src/src/transports/lmtp.c
index 3403dce7a..322dfbb71 100644
--- a/src/src/transports/lmtp.c
+++ b/src/src/transports/lmtp.c
@@ -825,7 +825,7 @@ transport_info lmtp_transport_info = {
   .options_block =	&lmtp_transport_option_defaults,
   .options_len =	sizeof(lmtp_transport_options_block),
   .init =		lmtp_transport_init,
-# ifdef DYNLOOKUP
+# if TRANSPORT_LMTP==2
   .dyn_magic =		TRANSPORT_MAGIC,
 # endif
   },
diff --git a/src/src/transports/pipe.c b/src/src/transports/pipe.c
index 68036ce03..fd248113a 100644
--- a/src/src/transports/pipe.c
+++ b/src/src/transports/pipe.c
@@ -1142,7 +1142,7 @@ transport_info pipe_transport_info = {
   .options_block =	&pipe_transport_option_defaults,
   .options_len =	sizeof(pipe_transport_options_block),
   .init =		pipe_transport_init,
-# ifdef DYNLOOKUP
+# if TRANSPORT_PIPE==2
   .dyn_magic =		TRANSPORT_MAGIC,
 # endif
   },
diff --git a/src/src/transports/queuefile.c b/src/src/transports/queuefile.c
index 7ae71a8da..5bd02682e 100644
--- a/src/src/transports/queuefile.c
+++ b/src/src/transports/queuefile.c
@@ -300,7 +300,7 @@ transport_info queuefile_transport_info = {
   .options_block =	&queuefile_transport_option_defaults,
   .options_len =	sizeof(queuefile_transport_options_block),
   .init =		queuefile_transport_init,
-# ifdef DYNLOOKUP
+# if EXPERIMENTAL_QUEUEFILE==2
   .dyn_magic =		TRANSPORT_MAGIC,
 # endif
   },
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index e9dc0957c..36b6370fd 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -2381,6 +2381,7 @@ if (continue_hostname && continue_proxy_cipher)
     if (write(0, "QUIT\r\n", 6) < 0)
       DEBUG(D_any) debug_printf("stupid compiler\n");
     close(0);
+    tls_out.active.sock = -1;
     continue_hostname = continue_proxy_cipher = NULL;
     f.continue_more = FALSE;
     continue_sequence = 1;	/* Ensure proper logging of non-cont-conn */
@@ -2882,6 +2883,7 @@ else
       if (write(0, "QUIT\r\n", 6) < 0)
 	DEBUG(D_any) debug_printf("stupid compiler\n");
       close(0);
+      tls_out.active.sock = -1;
       continue_hostname = continue_proxy_cipher = NULL;
       f.continue_more = FALSE;
       continue_sequence = 1;	/* Ensure proper logging of non-cont-conn */
@@ -3027,6 +3029,7 @@ if (  smtp_peer_options & OPTION_TLS
       sx->send_quit = FALSE;
       goto TLS_FAILED;
       }
+    tls_out.smtp_quit = FALSE;
     sx->send_tlsclose = TRUE;
 
 # ifdef TCP_FASTOPEN
@@ -3484,7 +3487,10 @@ FAILED:
 SEND_QUIT:
 
 if (sx->send_quit)
+  {
   (void)smtp_write_command(sx, SCMD_FLUSH, "QUIT\r\n");
+  tls_out.smtp_quit = TRUE;
+  }
 
 #ifndef DISABLE_TLS
 if (sx->cctx.tls_ctx)
@@ -4476,6 +4482,7 @@ else
       HDEBUG(D_transport|D_acl|D_v) debug_printf_indent("  SMTP(shutdown)>>\n");
       shutdown(sx->cctx.sock, SHUT_WR);	/* flush output buffer, with TCP FIN */
       }
+    tls_out.smtp_quit = TRUE;
     }
 
   if (smtp_peer_options & OPTION_CHUNKING && sx->cmd_count > 1)
@@ -5170,6 +5177,10 @@ if (sx->send_quit)
   {			/* Use _MORE to get QUIT in FIN segment */
   (void)smtp_write_command(sx, SCMD_MORE, "QUIT\r\n");
 #ifndef DISABLE_TLS
+  /* This flag is a custom hack to avoid logging, under GnuTLS, Google's
+  habit of just dropping the TCP conn (which violates TLS spec) */
+  tls_out.smtp_quit = TRUE;
+
   if (sx->cctx.tls_ctx && sx->send_tlsclose)
     {
 # ifdef EXIM_TCP_CORK	/* Use _CORK to get TLS Close Notify in FIN segment */
@@ -5357,6 +5368,7 @@ sx.outblock.cmd_count = 0;
 sx.outblock.authenticating = FALSE;
 
 (void)smtp_write_command(&sx, SCMD_FLUSH, "QUIT\r\n");
+tls_out.smtp_quit = TRUE;
 (void)smtp_read_response(&sx, buffer, sizeof(buffer), '2', ob->command_timeout);
 (void)close(cctx.sock);
 }
@@ -6545,7 +6557,7 @@ transport_info smtp_transport_info = {
   .options_block =	&smtp_transport_option_defaults,
   .options_len =	sizeof(smtp_transport_options_block),
   .init =		smtp_transport_init,
-# ifdef DYNLOOKUP
+# if TRANSPORT_SMTP==2
   .dyn_magic =		TRANSPORT_MAGIC,
 # endif
   },
diff --git a/src/src/transports/smtp.h b/src/src/transports/smtp.h
index df6c9e963..49e3c8b96 100644
--- a/src/src/transports/smtp.h
+++ b/src/src/transports/smtp.h
@@ -202,7 +202,7 @@ typedef struct {
 
   unsigned	avoid_option;
   uschar *	igquotstr;
-  uschar *	helo_data;
+  const uschar * helo_data;
 #ifdef EXPERIMENTAL_DSN_INFO
   uschar *	smtp_greeting;
   uschar *	helo_response;
diff --git a/src/src/utf8.c b/src/src/utf8.c
index c05853838..6cb104ddd 100644
--- a/src/src/utf8.c
+++ b/src/src/utf8.c
@@ -47,37 +47,40 @@ The *err string pointer should be null before the call
 Return NULL for error, with optional errstr pointer filled in
 */
 
-uschar *
+const uschar *
 string_domain_utf8_to_alabel(const uschar * utf8, uschar ** err)
 {
-uschar * s1, * s;
+const uschar * cs;
+uschar * t, * s;
 int rc;
 
 #ifdef SUPPORT_I18N_2008
 /* Avoid lowercasing plain-ascii domains */
 if (!string_is_utf8(utf8))
-  return string_copy(utf8);
+  return utf8;
 
 /* Only lowercase is accepted by the library call.  A pity since we lose
 any mixed-case annotation.  This does not really matter for a domain. */
   {
+  const uschar * cs1;
   uschar c;
-  for (s1 = s = US utf8; (c = *s1); s1++) if (!(c & 0x80) && isupper(c))
+  for (cs1 = cs = utf8; (c = *cs1); cs1++) if (!(c & 0x80) && isupper(c))
     {
     s = string_copy(utf8);
-    for (s1 = s + (s1 - utf8); (c = *s1); s1++) if (!(c & 0x80) && isupper(c))
-      *s1 = tolower(c);
+    for (t = s + (cs1 - utf8); (c = *t); t++) if (!(c & 0x80) && isupper(c))
+      *t = tolower(c);
+    cs = s;
     break;
     }
   }
-if ((rc = idn2_lookup_u8((const uint8_t *) s, &s1, IDN2_NFC_INPUT)) != IDN2_OK)
+if ((rc = idn2_lookup_u8((const uint8_t *) cs, &t, IDN2_NFC_INPUT)) != IDN2_OK)
   {
   if (err) *err = US idn2_strerror(rc);
   return NULL;
   }
 #else
 s = US stringprep_utf8_nfkc_normalize(CCS utf8, -1);
-if (  (rc = idna_to_ascii_8z(CCS s, CSS &s1, IDNA_ALLOW_UNASSIGNED))
+if (  (rc = idna_to_ascii_8z(CCS s, CSS &t, IDNA_ALLOW_UNASSIGNED))
    != IDNA_SUCCESS)
   {
   free(s);
@@ -86,9 +89,9 @@ if (  (rc = idna_to_ascii_8z(CCS s, CSS &s1, IDNA_ALLOW_UNASSIGNED))
   }
 free(s);
 #endif
-s = string_copy(s1);
-free(s1);
-return s;
+cs = string_copy(t);
+free(t);
+return cs;
 }
 
 
@@ -132,7 +135,7 @@ return s;
 /* the *err string pointer should be null before the call */
 
 
-uschar *
+const uschar *
 string_localpart_utf8_to_alabel(const uschar * utf8, uschar ** err)
 {
 size_t ucs4_len = 0;
@@ -141,7 +144,7 @@ size_t p_len;
 uschar * res;
 int rc;
 
-if (!string_is_utf8(utf8)) return string_copy(utf8);
+if (!string_is_utf8(utf8)) return utf8;
 
 p = (punycode_uint *) stringprep_utf8_to_ucs4(CCS utf8, -1, &ucs4_len);
 if (!p || !ucs4_len)
@@ -212,10 +215,10 @@ The *err string pointer should be null before the call.
 Return NULL on error, with (optional) errstring pointer filled in
 */
 
-uschar *
+const uschar *
 string_address_utf8_to_alabel(const uschar * utf8, uschar ** err)
 {
-uschar * l, * d;
+const uschar * l, * d;
 
 if (!*utf8) return string_copy(utf8);
 
diff --git a/src/src/utils/eximstats.src b/src/src/utils/eximstats.src
index 84b60ceaa..41912c1fb 100644
--- a/src/src/utils/eximstats.src
+++ b/src/src/utils/eximstats.src
@@ -1852,7 +1852,7 @@ EoText
 sub generate_parser {
   my $parser = '
   my($ip,$host,$email,$edomain,$domain,$thissize,$size,$old,$new);
-  my($tod,$m_hour,$m_min,$id,$flag,$extra,$length);
+  my($tod,$m_hour,$m_min,$id,$flag,$offset,$length);
   my($seconds,$queued,$rcpt_time,$local_domain);
   my $rej_id = 0;
   while (<$fh>) {
@@ -1865,72 +1865,68 @@ sub generate_parser {
     $length = length($_);
     next if ($length < 38);
     next unless /^
-		(\\d{4}\\-\\d\\d-\\d\\d\\s	# 1: YYYYMMDD HHMMSS
-			(\\d\\d)		# 2: HH
-			:
-			(\\d\\d)		# 3: MM
-			:\\d\\d
-		)
-		(\\.\\d+)?			# 4: subseconds
-		(\s[-+]\\d\\d\\d\\d)?		# 5: tz-offset
-		(\s\\[\\d+\\])?			# 6: pid
+		(\\d{4}\\-\\d\\d-\\d\\d\\s	# 1: YYYYMMDD HHMMSS	11
+			(\\d\\d)		# 2: HH			 2
+			:			#			 1
+			(\\d\\d)		# 3: MM			 2
+			:\\d\\d			#			 3
+		)					#	=	19
+		(\\.\\d+)?			# 4: subseconds		(var)
+		(\s[-+]\\d{4})?			# 5: tz-offset		 6
+		(\s\\[\\d+\\])?			# 6: pid		(var)
 		/ox;
+    $offset = 19;
 
     $tod = defined($5) ?  $1 . $5 : $1;
     ($m_hour,$m_min) = ($2,$3);
 
+    # watch for subsecond precision
+    if (defined($4)) {
+      $offset += length($4);
+      next if ($length < 19 + $offset);
+    }
+
     # PH - watch for GMT offsets in the timestamp.
     if (defined($5)) {
-      $extra = 6;
+      $offset += 6;
       next if ($length < 44);
-    }
-    else {
-      $extra = 0;
-    }
-
-    # watch for subsecond precision
-    if (defined($4)) {
-      $extra += length($4);
-      next if ($length < 38 + $extra);
+      next if ($length < 19 + $offset);
     }
 
     # PH - watch for PID added after the timestamp.
     if (defined($6)) {
-      $extra += length($6);
-      next if ($length < 38 + $extra);
+      $offset += length($6);
+      next if ($length < 19 + $offset);
     }
 
-    # jgh 2025/09/30 - I really dislike this magic "20" offset...
-    # It would be better to develop it from the substrings captured so far.
+    # skip space after the initial groups
+    $offset++;
 
-    # $id = substr($_, 20 + $extra, 16);	# old ID was 16 chars
-    $id = substr($_, 20 + $extra, 23);		# new ID is 23 chars
+    $id = substr($_, $offset, 23);		# new ID is 23 chars, old 16
     $id =~ s/(\S+).*/$1/;
-    #
-    # jgh 2025/09/30 - very fragile when this word was not an ID;
-    # could be shorter than 16.
-    $extra += length($id) - 16;
 
-    # 37 = 20+16+1     so next field after ID.  Assuming there was really an ID.
-    $flag = substr($_, 37 + $extra, 2);
+    # skip ID and space
+    $offset += length($id) + 1;
+
+    $flag = substr($_, $offset, 2);
 
     # jgh 2025/09/30 -
-    # 2020-05-12 17:31:41.630 [23229] auth_login authenticator failed for (User) [185.143.75.81]:28556 I=[192.168.56.110]:25: 535 Incorrect authentication data (set_id=dropped@kiev.ua)
-    # OTOH the "refused" and "dropped" probably are referring to connectios
-    # rather than messages.
+    # 2020-05-12 17:31:41.630 [23229] auth_login authenticator failed for (User) [185.143.75.81]:28556 I=[192.168.56.110]:25: 535 Incorrect authentication data (set_id=dropped@foo.us)
+    # We now count these as cause for connection-drop, since the client often
+    # does on getting an auth-fail (distributed password-guessing attacks...).
     #
     # The "[-<>=*(]+" will be for ignoring accept & delivery lines.
     # What is the "SA"?  SpamAssissin ?
     if ($flag !~ /^([-<>=*(]+|SA)$/ && / (rejected|refused|dropped|authenticator failed)/) {
       $flag = "Re";
-      $extra -= 3;
+    } else {
+      # skip flag and space
+      $offset += 3;
     }
 
     # Rejects might have no MSGID...
-    # Note that $extra can go negative here.  Really, a $offset would be
-    # much more clear.
     if ($flag eq "Re" && $id !~ /^[-0-9a-zA-Z]+$/) {
-      $extra -= length($id) + 1;
+      $offset -= length($id) + 1;
       $id   = "reject:" . ++$rej_id;
     }
 ';
@@ -1947,11 +1943,13 @@ sub generate_parser {
   }
 
   $parser .= '
-    next unless ($flag =~ /<=|=>|->|==|\\*\\*|Co|SA|Re/);
+    next unless ($flag =~ /<=|\(=|=>|->|==|\\*\\*|Co|SA|Re/);
+
+    # TODO: handling for fakereject [ flag (= ]
 
-    #Strip away the timestamp, ID and flag to speed up later pattern matches.
-    #The flags include Co (Completed), Re (Rejected), and SA (SpamAssassin).
-    $_ = substr($_, 40 + $extra);  # PH
+    # Strip away the timestamp, ID and flag to speed up later pattern matches.
+    # The flags include Co (Completed), Re (Rejected), and SA (SpamAssassin).
+    $_ = substr($_, $offset);  # PH
 
     # Alias @message to the array of information about the message.
     # This minimises the number of calls to hash functions.
diff --git a/src/src/utils/exipick.src b/src/src/utils/exipick.src
index c441936a2..788f7efae 100644
--- a/src/src/utils/exipick.src
+++ b/src/src/utils/exipick.src
@@ -941,8 +941,6 @@ sub _parse_header {
     } elsif ($tag eq '-spam_score_int') {
       $self->{_vars}{spam_score_int} = $arg;
       $self->{_vars}{spam_score}     = $arg / 10;
-    } elsif ($tag eq '-bmi_verdicts') {
-      $self->{_vars}{bmi_verdicts} = $arg;
     } elsif ($tag eq '-host_lookup_deferred') {
       $self->{_vars}{host_lookup_deferred} = 1;
     } elsif ($tag eq '-host_lookup_failed') {
@@ -1569,10 +1567,6 @@ The value of AUTH= param for smtp messages, or a generated value from the callin
 
 Value of the header(s) with the same name with any RFC2047 words decoded if present.  See section 11.5 of Exim's spec.txt for full details.
 
-=item S + B<$bmi_verdicts>
-
-The verdict string provided by a Brightmail content scan
-
 =item N . B<$body_linecount>
 
 The number of lines in the message's body.
diff --git a/src/src/verify.c b/src/src/verify.c
index a12e74ce6..a3ade640b 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -2779,162 +2779,6 @@ return yield;
 
 
 
-/*************************************************
-*            Get RFC 1413 identification         *
-*************************************************/
-
-/* Attempt to get an id from the sending machine via the RFC 1413 protocol. If
-the timeout is set to zero, then the query is not done. There may also be lists
-of hosts and nets which are exempt. To guard against malefactors sending
-non-printing characters which could, for example, disrupt a message's headers,
-make sure the string consists of printing characters only.
-
-Argument:
-  port    the port to connect to; usually this is IDENT_PORT (113), but when
-          running in the test harness with -bh a different value is used.
-
-Returns:  nothing
-
-Side effect: any received ident value is put in sender_ident (NULL otherwise)
-*/
-
-void
-verify_get_ident(int port)
-{
-client_conn_ctx ident_conn_ctx = {0};
-int host_af, qlen;
-int received_sender_port, received_interface_port, n;
-uschar *p;
-blob early_data;
-uschar buffer[2048];
-
-/* Default is no ident. Check whether we want to do an ident check for this
-host. */
-
-sender_ident = NULL;
-if (rfc1413_query_timeout <= 0 || verify_check_host(&rfc1413_hosts) != OK)
-  return;
-
-DEBUG(D_ident) debug_printf("doing ident callback\n");
-
-/* Set up a connection to the ident port of the remote host. Bind the local end
-to the incoming interface address. If the sender host address is an IPv6
-address, the incoming interface address will also be IPv6. */
-
-host_af = Ustrchr(sender_host_address, ':') == NULL ? AF_INET : AF_INET6;
-if ((ident_conn_ctx.sock = ip_socket(SOCK_STREAM, host_af)) < 0) return;
-
-if (ip_bind(ident_conn_ctx.sock, host_af, interface_address, 0) < 0)
-  {
-  DEBUG(D_ident) debug_printf("bind socket for ident failed: %s\n",
-    strerror(errno));
-  goto END_OFF;
-  }
-
-/* Construct and send the query. */
-
-qlen = snprintf(CS buffer, sizeof(buffer), "%d , %d\r\n",
-  sender_host_port, interface_port);
-early_data.data = buffer;
-early_data.len = qlen;
-
-/*XXX we trust that the query is idempotent */
-if (ip_connect(ident_conn_ctx.sock, host_af, sender_host_address, port,
-		rfc1413_query_timeout, &early_data) < 0)
-  {
-  if (errno == ETIMEDOUT && LOGGING(ident_timeout))
-    log_write(0, LOG_MAIN, "ident connection to %s timed out",
-      sender_host_address);
-  else
-    DEBUG(D_ident) debug_printf("ident connection to %s failed: %s\n",
-      sender_host_address, strerror(errno));
-  goto END_OFF;
-  }
-
-/* Read a response line. We put it into the rest of the buffer, using several
-recv() calls if necessary. */
-
-p = buffer + qlen;
-
-for (;;)
-  {
-  uschar *pp;
-  int count;
-  int size = sizeof(buffer) - (p - buffer);
-
-  if (size <= 0) goto END_OFF;   /* Buffer filled without seeing \n. */
-  count = ip_recv(&ident_conn_ctx, p, size, time(NULL) + rfc1413_query_timeout);
-  if (count <= 0) goto END_OFF;  /* Read error or EOF */
-
-  /* Scan what we just read, to see if we have reached the terminating \r\n. Be
-  generous, and accept a plain \n terminator as well. The only illegal
-  character is 0. */
-
-  for (pp = p; pp < p + count; pp++)
-    {
-    if (*pp == 0) goto END_OFF;   /* Zero octet not allowed */
-    if (*pp == '\n')
-      {
-      if (pp[-1] == '\r') pp--;
-      *pp = 0;
-      goto GOT_DATA;             /* Break out of both loops */
-      }
-    }
-
-  /* Reached the end of the data without finding \n. Let the loop continue to
-  read some more, if there is room. */
-
-  p = pp;
-  }
-
-GOT_DATA:
-
-/* We have received a line of data. Check it carefully. It must start with the
-same two port numbers that we sent, followed by data as defined by the RFC. For
-example,
-
-  12345 , 25 : USERID : UNIX :root
-
-However, the amount of white space may be different to what we sent. In the
-"osname" field there may be several sub-fields, comma separated. The data we
-actually want to save follows the third colon. Some systems put leading spaces
-in it - we discard those. */
-
-if (sscanf(CS buffer + qlen, "%d , %d%n", &received_sender_port,
-      &received_interface_port, &n) != 2 ||
-    received_sender_port != sender_host_port ||
-    received_interface_port != interface_port)
-  goto END_OFF;
-
-p = buffer + qlen + n;
-Uskip_whitespace(&p);
-if (*p++ != ':') goto END_OFF;
-Uskip_whitespace(&p);
-if (Ustrncmp(p, "USERID", 6) != 0) goto END_OFF;
-p += 6;
-Uskip_whitespace(&p);
-if (*p++ != ':') goto END_OFF;
-while (*p && *p != ':') p++;
-if (!*p++) goto END_OFF;
-Uskip_whitespace(&p);
-if (!*p) goto END_OFF;
-
-/* The rest of the line is the data we want. We turn it into printing
-characters when we save it, so that it cannot mess up the format of any logging
-or Received: lines into which it gets inserted. We keep a maximum of 127
-characters. The deconst cast is ok as we fed a nonconst to string_printing() */
-
-sender_ident = US string_printing(string_copyn(p, 127));
-DEBUG(D_ident) debug_printf("sender_ident = %s\n", sender_ident);
-
-END_OFF:
-(void)close(ident_conn_ctx.sock);
-return;
-}
-
-
-
-
 /*************************************************
 *      Match host to a single host-list item     *
 *************************************************/

commit 3858878623272c18ad8b4d3f856c3a3dbe22577c
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Oct 30 09:42:01 2025 +0000

    Fix taint status for dbm lookups.  Bug 3169
    
    Broken-by: c66a6edf7ba8

diff --git a/src/src/dbfn.c b/src/src/dbfn.c
index 7f6951ee3..30015fe73 100644
--- a/src/src/dbfn.c
+++ b/src/src/dbfn.c
@@ -386,13 +386,15 @@ Arguments:
   key       the key of the record to be read
   klen	    length of key including a terminating NUL (if present)
   length    a pointer to an int into which to return the length, if not NULL
+  hintsdb   TRUE for hints DB use, FALSE for lookup dbm use
 
 Returns: a pointer to the retrieved record, or
          NULL if the record is not found
 */
 
 void *
-dbfn_read_klen(open_db * dbblock, const uschar * key, int klen, int * length)
+dbfn_read_klen(open_db * dbblock, const uschar * key, int klen, int * length,
+  BOOL hintsdb)
 {
 void * yield;
 EXIM_DATUM key_datum, result_datum;
@@ -423,7 +425,7 @@ store the taint status with the data. */
 dlen = exim_datum_size_get(&result_datum);
 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: size %u return\n", dlen);
 
-yield = store_get(dlen+1, GET_TAINTED);
+yield = store_get(dlen+1, hintsdb ? GET_TAINTED : GET_UNTAINTED);
 memcpy(yield, exim_datum_data_get(&result_datum), dlen);
 ((uschar *)yield)[dlen] = '\0';
 if (length) *length = dlen;
@@ -451,7 +453,7 @@ Returns: a pointer to the retrieved record, or
 void *
 dbfn_read_with_length(open_db * dbblock, const uschar * key, int * lenp)
 {
-return dbfn_read_klen(dbblock, key, Ustrlen(key)+1, lenp);
+return dbfn_read_klen(dbblock, key, Ustrlen(key)+1, lenp, TRUE);
 }
 
 
diff --git a/src/src/dbfunctions.h b/src/src/dbfunctions.h
index 1b0e446f7..a6fb2b51e 100644
--- a/src/src/dbfunctions.h
+++ b/src/src/dbfunctions.h
@@ -18,7 +18,7 @@ int      dbfn_delete(open_db *, const uschar *);
 open_db *dbfn_open(const uschar *, int, open_db *, BOOL, BOOL);
 open_db * dbfn_open_path(const uschar *, open_db *);
 open_db *dbfn_open_multi(const uschar *, int, open_db *);
-void    *dbfn_read_klen(open_db *, const uschar *, int, int *);
+void    *dbfn_read_klen(open_db *, const uschar *, int, int *, BOOL);
 void    *dbfn_read_with_length(open_db *, const uschar *, int *);
 void    *dbfn_read_enforce_length(open_db *, const uschar *, size_t);
 uschar  *dbfn_scan(open_db *, BOOL, EXIM_CURSOR **);
diff --git a/src/src/lookups/dbmdb.c b/src/src/lookups/dbmdb.c
index d3b26cf0a..58a839e84 100644
--- a/src/src/lookups/dbmdb.c
+++ b/src/src/lookups/dbmdb.c
@@ -91,7 +91,8 @@ dbmdb_find(void * handle, const uschar * filename, const uschar * keystring,
   const uschar * opts)
 {
 open_db * d = (open_db *)handle;
-return (*result = dbfn_read_klen(d, keystring, length+1, NULL)) ? OK : FAIL;
+return (*result = dbfn_read_klen(d, keystring, length+1, NULL, FALSE))
+	? OK : FAIL;
 }
 
 

commit 50a6abf200c5116e9b86f12afbcc973ccd021261
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Oct 31 12:55:40 2025 +0000

    Testsuite: expand store_free() checking

diff --git a/src/src/deliver.c b/src/src/deliver.c
index 497c62e81..94dc092a3 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -9088,6 +9088,38 @@ fail:
 #endif
 }
 
+
+/* When running with debug_store this is called on every store_reset(). Walk all
+the address lists we maintain checking that none of the pointers are in the region
+being freed. */
+/*XXX what about all the pointers contained in the addr?
+If implemented, beware unbounded recursion. */
+
+static void
+check_addr_list(const uschar * name, const address_item * a,
+  void (*f)(const uschar*, const uschar*, void*), void * ctx)
+{
+while (a)
+  {
+  f(name, CUS a, ctx);	/* We lie about the data type */
+  a = a->next;
+  }
+}
+
+void
+check_deliver_addrs_not_freed(void (*f)(const uschar*, const uschar*, void*), void * ctx)
+{
+check_addr_list(US"(addr_defer)",	addr_defer,	f, ctx);
+check_addr_list(US"(addr_failed)",	addr_failed,	f, ctx);
+check_addr_list(US"(addr_fallback)",	addr_fallback,	f, ctx);
+check_addr_list(US"(addr_local)",	addr_local,	f, ctx);
+check_addr_list(US"(addr_new)",		addr_new,	f, ctx);
+check_addr_list(US"(addr_remote)",	addr_remote,	f, ctx);
+check_addr_list(US"(addr_route)",	addr_route,	f, ctx);
+check_addr_list(US"(addr_succeed)",	addr_succeed,	f, ctx);
+check_addr_list(US"(addr_duplicate)",	addr_duplicate, f, ctx);
+}
+
 /* vi: aw ai sw=2
 */
 /* End of deliver.c */
diff --git a/src/src/expand.c b/src/src/expand.c
index 554d68ba6..9bfa39fdc 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -9017,15 +9017,18 @@ typedef struct {
   const uschar *var_data;
 } err_ctx;
 
-/* Called via tree_walk, which allows nonconst name/data.  Our usage is const. */
+/* Called via tree_walk, which allows nonconst name/data. Our usage is const. */
+typedef void (*twalk_compat)(uschar *, uschar *, void *);
+
 static void
-assert_variable_notin(uschar * var_name, uschar * var_data, void * ctx)
+assert_variable_notin(const uschar * var_name, const uschar * var_data,
+  void * ctx)
 {
 err_ctx * e = ctx;
 if (var_data >= e->region_start  &&  var_data < e->region_end)
   {
-  e->var_name = CUS var_name;
-  e->var_data = CUS var_data;
+  e->var_name = var_name;
+  e->var_data = var_data;
   }
 }
 
@@ -9036,8 +9039,8 @@ err_ctx e = { .region_start = ptr, .region_end = US ptr + len,
 	      .var_name = NULL, .var_data = NULL };
 
 /* check acl_ variables */
-tree_walk(acl_var_c, assert_variable_notin, &e);
-tree_walk(acl_var_m, assert_variable_notin, &e);
+tree_walk(acl_var_c, (twalk_compat) assert_variable_notin, &e);
+tree_walk(acl_var_m, (twalk_compat) assert_variable_notin, &e);
 
 /* check auth<n> variables.
 assert_variable_notin() treats as const, so deconst is safe. */
@@ -9056,10 +9059,13 @@ for (var_entry * v = var_table; v < var_table + nelem(var_table); v++)
     assert_variable_notin(US v->name, *(USS v->value), &e);
 
 /* check dns and address trees */
-tree_walk(tree_dns_fails,     assert_variable_notin, &e);
-tree_walk(tree_duplicates,    assert_variable_notin, &e);
-tree_walk(tree_nonrecipients, assert_variable_notin, &e);
-tree_walk(tree_unusable,      assert_variable_notin, &e);
+tree_walk(tree_dns_fails,     (twalk_compat) assert_variable_notin, &e);
+tree_walk(tree_duplicates,    (twalk_compat) assert_variable_notin, &e);
+tree_walk(tree_nonrecipients, (twalk_compat) assert_variable_notin, &e);
+tree_walk(tree_unusable,      (twalk_compat) assert_variable_notin, &e);
+
+/* check address-lists */
+check_deliver_addrs_not_freed(assert_variable_notin, &e);
 
 if (e.var_name)
   log_write_die(0, LOG_MAIN,
diff --git a/src/src/functions.h b/src/src/functions.h
index a29dddbed..223723b81 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -144,6 +144,7 @@ extern void    bits_set(unsigned int *, size_t, int *);
 extern void    cancel_cutthrough_connection(BOOL, const uschar *);
 extern gstring *cat_file(FILE *, gstring *, const uschar *);
 extern gstring *cat_file_tls(void *, gstring *, const uschar *);
+extern void    check_deliver_addrs_not_freed(void (*)(const uschar*, const uschar*, void*), void *);
 extern int     check_host(void *, const uschar *, const uschar **, uschar **);
 extern uschar **child_exec_exim(int, BOOL, int *, BOOL, int, ...);
 extern pid_t   child_open_exim_function(int *, const uschar *);
diff --git a/src/src/transport.c b/src/src/transport.c
index d88653964..a59a5ae3d 100644
--- a/src/src/transport.c
+++ b/src/src/transport.c
@@ -705,10 +705,10 @@ Returns:    FALSE if writing failed
 */
 
 static BOOL
-write_env_to(address_item *p, struct aci **pplist, struct aci **pdlist,
-  BOOL *first, transport_ctx * tctx)
+write_env_to(address_item * p, struct aci ** pplist, struct aci ** pdlist,
+  BOOL * first, transport_ctx * tctx)
 {
-address_item *pp;
+address_item * pp;
 struct aci *ppp;
 
 /* Do nothing if we have already handled this address. If not, remember it
@@ -725,8 +725,7 @@ ppp->ptr = p;
 
 for (pp = p;; pp = pp->parent)
   {
-  address_item *dup;
-  for (dup = addr_duplicate; dup; dup = dup->next)
+  for (address_item * dup = addr_duplicate; dup; dup = dup->next)
     if (dup->dupof == pp)   /* a dup of our address */
       if (!write_env_to(dup, pplist, pdlist, first, tctx))
 	return FALSE;

commit baad2e56cdcffa83e2e5d138537dcef858bdf5b6
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Oct 31 12:59:39 2025 +0000

    Fix duplicate address processing vs. continued-transport
    
    Broken-by: 79344067b96a

diff --git a/src/src/deliver.c b/src/src/deliver.c
index 94dc092a3..29c5dbfc6 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -8980,7 +8980,9 @@ report_time_since(&timestamp_startup, US"delivery end"); /* testcase 0005 */
 if (final_yield == DELIVER_ATTEMPTED_NORMAL && *continue_next_id)
   {
   addr_defer = addr_failed = addr_succeed = NULL;
+
   tree_duplicates = NULL;	/* discard dups info from old message */
+  addr_duplicate = NULL;
 
   spool_clear_header_globals();
   deliver_set_expansions(NULL);

commit d021d9bddfbe66cfec7027999aa3ee501198e20a
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 2 10:12:55 2025 +0000

    tidying

diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index 80fb419d0..154d3be38 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -2163,10 +2163,7 @@ if (host)
   /* For client-side sessions we allocate a context. This lets us run
   several in parallel. */
 
-  int old_pool = store_pool;
-  store_pool = POOL_PERM;
-  state = store_get(sizeof(exim_gnutls_state_st), GET_UNTAINTED);
-  store_pool = old_pool;
+  state = store_get_perm(sizeof(exim_gnutls_state_st), GET_UNTAINTED);
 
   memcpy(state, &exim_gnutls_state_init, sizeof(exim_gnutls_state_init));
   state->lib_state = ob->tls_preload;
@@ -2806,7 +2803,7 @@ char sni_name[MAX_HOST_LEN];
 size_t data_len = MAX_HOST_LEN;
 exim_gnutls_state_st *state = &state_server;
 unsigned int sni_type;
-int rc, old_pool;
+int rc;
 uschar * dummy_errstr;
 
 rc = gnutls_server_name_get(session, sni_name, &data_len, &sni_type, 0);
@@ -2828,10 +2825,7 @@ if (sni_type != GNUTLS_NAME_DNS)
   }
 
 /* We now have a UTF-8 string in sni_name */
-old_pool = store_pool;
-store_pool = POOL_PERM;
-state->received_sni = string_copy_taint(US sni_name, GET_TAINTED);
-store_pool = old_pool;
+state->received_sni = string_copy_perm(US sni_name, TRUE);
 
 /* We set this one now so that variable expansions below will work */
 state->tlsp->sni = state->received_sni;
@@ -4386,8 +4380,9 @@ host_item * h;
 int old_pool = store_pool;
 
 store_pool = POOL_PERM;
-state = store_get(sizeof(exim_gnutls_state_st), GET_UNTAINTED);
-h = store_get(sizeof(host_item), GET_UNTAINTED);
+state = store_get(sizeof(exim_gnutls_state_st) + sizeof(host_item),
+		  GET_UNTAINTED);
+h = ((host_item *)state) + 1;
 
 memset(h, 0, sizeof(host_item));
 h->name = h->address = string_copy(ipaddr);
@@ -4419,12 +4414,8 @@ store_pool = old_pool;
 void
 tls_state_out_to_in(int newfd, const uschar * ipaddr, int port)
 {
-host_item * h;
-int old_pool = store_pool;
+host_item * h = store_get_perm(sizeof(host_item), GET_UNTAINTED);
 
-store_pool = POOL_PERM;
-h = store_get(sizeof(host_item), GET_UNTAINTED);
-store_pool = old_pool;
 memset(h, 0, sizeof(host_item));
 h->name = h->address = string_copy(ipaddr);
 h->port = port;
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index d5a6d368d..ddc72bfa2 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -2231,7 +2231,6 @@ 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;
 int rc;
-int old_pool = store_pool;
 uschar * errstr;
 
 if (!servername)
@@ -2241,9 +2240,7 @@ DEBUG(D_tls) debug_printf("Received TLS SNI %q%s\n", servername,
     reexpand_tls_files_for_sni ? "" : " (unused for certificate selection)");
 
 /* Make the extension value available for expansion */
-store_pool = POOL_PERM;
-tls_in.sni = string_copy_taint(US servername, GET_TAINTED);
-store_pool = old_pool;
+tls_in.sni = string_copy_perm(US servername, TRUE);
 
 if (!reexpand_tls_files_for_sni)
   return SSL_TLSEXT_ERR_OK;
@@ -3119,13 +3116,8 @@ return cipher_stdname(id >> 8, id & 0xff);
 static const uschar *
 tlsver_name(const SSL * ssl)
 {
-const uschar * s;
-uschar * p;
-int pool = store_pool;
+uschar * s = string_copy_perm(US SSL_get_version(ssl), FALSE), * p;
 
-store_pool = POOL_PERM;
-s = string_copy(US SSL_get_version(ssl));
-store_pool = pool;
 if ((p = Ustrchr(s, 'v')))	/* TLSv1.2 -> TLS1.2 */
   for (;; p++) if (!(*p = p[1])) break;
 return CUS s;
@@ -3151,12 +3143,8 @@ if (tlsp->peercert)
     { DEBUG(D_tls) debug_printf("X509_NAME_oneline() error\n"); }
   else
     {
-    int oldpool = store_pool;
-
     peerdn[siz-1] = '\0';		/* paranoia */
-    store_pool = POOL_PERM;
-    tlsp->peerdn = string_copy(peerdn);
-    store_pool = oldpool;
+    tlsp->peerdn = string_copy_perm(peerdn, TRUE);
 
     /* We used to set CV in the cert-verify callbacks (either plain or dane)
     but they don't get called on session-resumption.  So use the official
@@ -4237,11 +4225,9 @@ BOOL request_ocsp = FALSE;
 BOOL require_ocsp = FALSE;
 #endif
 
-rc = store_pool;
-store_pool = POOL_PERM;
-exim_client_ctx = store_get(sizeof(exim_openssl_client_tls_ctx), GET_UNTAINTED);
+exim_client_ctx = store_get_perm(sizeof(exim_openssl_client_tls_ctx),
+				GET_UNTAINTED);
 exim_client_ctx->corked = NULL;
-store_pool = rc;
 
 #ifdef SUPPORT_DANE
 tlsp->tlsa_usage = 0;
@@ -5268,18 +5254,16 @@ void
 tls_state_in_to_out(int newfd, const uschar * ipaddr, int port)
 {
 exim_openssl_client_tls_ctx * exim_client_ctx;
-int old_pool = store_pool;
 
 state_server.is_server = FALSE;
 state_server.tlsp = &tls_out;
 client_static_state = &state_server;
 
-store_pool = POOL_PERM;
-exim_client_ctx = store_get(sizeof(exim_openssl_client_tls_ctx), GET_UNTAINTED);
+exim_client_ctx = store_get_perm(sizeof(exim_openssl_client_tls_ctx),
+				GET_UNTAINTED);
 exim_client_ctx->ctx = client_static_state->lib_state.lib_ctx;
 exim_client_ctx->ssl = client_static_state->lib_state.lib_ssl;
 exim_client_ctx->corked = NULL;
-store_pool = old_pool;
 
 SSL_set_fd(exim_client_ctx->ssl, newfd);
 

commit 195bf3719bb6d673f6730b221cfcd0dfec0281b4
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 2 14:31:22 2025 +0000

        Revert "Retire identd support"
    
        This reverts commit 106c3eb8ee31297588d5ec0555195be966dfd7b2.

diff --git a/src/src/configure.default b/src/src/configure.default
index c05ed182e..633c6539e 100644
--- a/src/src/configure.default
+++ b/src/src/configure.default
@@ -260,6 +260,21 @@ host_lookup = *
 dns_dnssec_ok = 1
 
 
+# The settings below cause Exim to make RFC 1413 (ident) callbacks
+# for all incoming SMTP calls. You can limit the hosts to which these
+# calls are made, and/or change the timeout that is used. If you set
+# the timeout to zero, all RFC 1413 calls are disabled. RFC 1413 calls
+# are cheap and can provide useful information for tracing problem
+# messages, but some hosts and firewalls have problems with them.
+# This can result in a timeout instead of an immediate refused
+# connection, leading to delays on starting up SMTP sessions.
+# (The default was reduced from 30s to 5s for release 4.61. and to
+# disabled for release 4.86)
+#
+#rfc1413_hosts = *
+#rfc1413_query_timeout = 5s
+
+
 # Enable an efficiency feature.  We advertise the feature; clients
 # may request to use it.  For multi-recipient mails we then can
 # reject or accept per-user after the message is received.
diff --git a/src/src/daemon.c b/src/src/daemon.c
index 824fe0187..a31ef1eb5 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -479,9 +479,20 @@ if (pid == 0)
   signal(SIGTERM, SIG_DFL);
   signal(SIGINT, SIG_DFL);
 
-  /* Set up the fullhost information in case there is no HELO/EHLO. */
+  /* Attempt to get an id from the sending machine via the RFC 1413
+  protocol. We do this in the sub-process in order not to hold up the
+  main process if there is any delay. Then set up the fullhost information
+  in case there is no HELO/EHLO.
 
+  If debugging is enabled only for the daemon, we must turn if off while
+  finding the id, but turn it on again afterwards so that information about the
+  incoming connection is output. */
+
+  if (f.debug_daemon) debug_selector = 0;
+  verify_get_ident(IDENT_PORT);
   host_build_sender_fullhost();
+  debug_selector = save_debug_selector;
+
   DEBUG(D_any)
     debug_printf("Process %d is handling incoming connection from %s\n",
       (int)getpid(), sender_fullhost);
diff --git a/src/src/exim.c b/src/src/exim.c
index 88ed33a8e..df343b5a1 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -5465,7 +5465,9 @@ if (raw_active_hostname)
 /* Handle host checking: this facility mocks up an incoming SMTP call from a
 given IP address so that the blocking and relay configuration can be tested.
 Unless a sender_ident was set by -oMt, we discard it (the default is the
-caller's login name). */
+caller's login name). An RFC 1413 call is made only if we are running in the
+test harness and an incoming interface and both ports are specified, because
+there is no TCP/IP call to find the ident for. */
 
 if (host_checking)
   {
@@ -5473,7 +5475,12 @@ if (host_checking)
   int size;
 
   if (!sender_ident_set)
+    {
     sender_ident = NULL;
+    if (f.running_in_test_harness && sender_host_port
+       && interface_address && interface_port)
+      verify_get_ident(test_harness_identd_port);
+    }
 
   /* In case the given address is a non-canonical IPv6 address, canonicalize
   it. Use the compressed form for IPv6. */
@@ -5496,6 +5503,7 @@ if (host_checking)
   debug_file = stderr;
   debug_fd = fileno(debug_file);
   dprintf(smtp_out_fd, "\n**** SMTP testing session as if from host %s\n"
+    "**** but without any ident (RFC 1413) callback.\n"
     "**** This is not for real!\n\n",
       sender_host_address);
 
@@ -5607,12 +5615,14 @@ sendmail error modes other than -oem ever actually used? Later: yes.) */
 if (!smtp_input) error_handling = arg_error_handling;
 
 /* If this is an inetd call, ensure that stderr is closed to prevent panic
-logging being sent down the socket. */
+logging being sent down the socket and make an identd call to get the
+sender_ident. */
 
 else if (f.is_inetd && !atrn_mode)
   {
   (void)fclose(stderr);
   exim_nullstd();                       /* Re-open to /dev/null */
+  verify_get_ident(IDENT_PORT);
   host_build_sender_fullhost();
   set_process_info("handling incoming connection from %s via inetd",
     sender_fullhost);
diff --git a/src/src/exim.h b/src/src/exim.h
index fa56f2c2c..5b931e9f4 100644
--- a/src/src/exim.h
+++ b/src/src/exim.h
@@ -156,6 +156,8 @@ configuration file.  We also use this for some other short strings, such
 as queue names.
 Also TLS ciphersuite name (no real known limit since the protocols use
 integers, but max seen in reality is 45 octets).
+
+RFC 1413 gives us the 512 limit on IDENT protocol userids.
 */
 
 #define EXIM_EMAILADDR_MAX     256
diff --git a/src/src/functions.h b/src/src/functions.h
index 223723b81..98dc1a461 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -697,6 +697,7 @@ extern int     verify_check_given_host(const uschar **, const host_item *);
 extern int     verify_check_this_host(const uschar **, unsigned int *,
 	         const uschar*, const uschar *, const uschar **);
 extern address_item *verify_checked_sender(const uschar *);
+extern void    verify_get_ident(int);
 extern void    verify_quota(uschar *);
 extern int     verify_quota_call(const uschar *, int, int, uschar **);
 extern BOOL    verify_sender(int *, uschar **);
diff --git a/src/src/globals.c b/src/src/globals.c
index be2b7ec13..c8636e191 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -707,6 +707,7 @@ bit_table debug_options[]      = { /* must be in alphabetical order and use
   BIT_TABLE(D, filter),
   BIT_TABLE(D, hints_lookup),
   BIT_TABLE(D, host_lookup),
+  BIT_TABLE(D, ident),
   BIT_TABLE(D, interface),
   BIT_TABLE(D, lists),
   BIT_TABLE(D, load),
@@ -989,6 +990,7 @@ bit_table log_options[]        = { /* must be in alphabetical order,
   BIT_TABLE(L, dnssec),
   BIT_TABLE(L, etrn),
   BIT_TABLE(L, host_lookup_failed),
+  BIT_TABLE(L, ident_timeout),
   BIT_TABLE(L, incoming_interface),
   BIT_TABLE(L, incoming_port),
   BIT_TABLE(L, lost_incoming_connection),
@@ -1238,6 +1240,8 @@ int     retry_maximum_timeout  = 0;        /* set from retry config */
 retry_config  *retries         = NULL;
 const uschar *return_path            = NULL;
 int     rewrite_existflags     = 0;
+uschar *rfc1413_hosts          = US"@[]";
+int     rfc1413_query_timeout  = 0;
 uid_t   root_gid               = ROOT_GID;
 uid_t   root_uid               = ROOT_UID;
 
@@ -1399,6 +1403,7 @@ uid_t   system_filter_uid      = (uid_t)-1;
 
 blob	tcp_fastopen_nodata    = { .data = NULL, .len = 0 };
 tfo_state_t tcp_out_fastopen   = TFO_NOT_USED;
+int	test_harness_identd_port  = IDENT_PORT;
 int     test_harness_load_avg  = 0;
 int     thismessage_size_limit = 0;
 int     timeout_frozen_after   = 0;
diff --git a/src/src/globals.h b/src/src/globals.h
index ccd790cc2..ddf78b4d8 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -919,6 +919,8 @@ extern int     retry_maximum_timeout;  /* The maximum timeout */
 extern const uschar *return_path;            /* Return path for a message */
 extern BOOL    return_path_remove;     /* Remove return-path headers */
 extern int     rewrite_existflags;     /* Indicate which headers have rewrites */
+extern uschar *rfc1413_hosts;          /* RFC hosts */
+extern int     rfc1413_query_timeout;  /* Timeout on RFC 1413 calls */
 /* extern BOOL    rfc821_domains;  */       /* If set, syntax is 821, not 822 => being abolished */
 extern uid_t   root_gid;               /* The gid for root */
 extern uid_t   root_uid;               /* The uid for root */
@@ -1059,6 +1061,7 @@ extern BOOL    system_filter_uid_set;  /* TRUE if uid set */
 extern blob    tcp_fastopen_nodata;    /* for zero-data TFO connect requests */
 extern BOOL    tcp_nodelay;            /* Controls TCP_NODELAY on daemon */
 extern tfo_state_t tcp_out_fastopen;   /* TCP fast open */
+extern int     test_harness_identd_port; /* For use when testing */
 extern int     test_harness_load_avg;  /* For use when testing */
 extern int     thismessage_size_limit; /* Limit for this message */
 extern int     timeout_frozen_after;   /* Max time to keep frozen messages */
diff --git a/src/src/macros.h b/src/src/macros.h
index 2e0efc9b8..75563d86b 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -90,6 +90,10 @@ don't make the file descriptors two-way. */
 #define pipe_read  0
 #define pipe_write 1
 
+/* The RFC 1413 ident port */
+
+#define IDENT_PORT 113
+
 /* A macro to simplify testing bits in lookup types */
 
 #define mac_islookup(li,b) ((li)->type & (b))
@@ -381,25 +385,26 @@ enum {
   DEBUG_BIT(filter),
   DEBUG_BIT(hints_lookup),
   DEBUG_BIT(host_lookup),
+  DEBUG_BIT(ident),
   DEBUG_BIT(interface),
   DEBUG_BIT(lists),
-  DEBUG_BIT(load),
-  DEBUG_BIT(lookup),		/* 15 */
+  DEBUG_BIT(load),		/* 15 */
+  DEBUG_BIT(lookup),
   DEBUG_BIT(memory),
   DEBUG_BIT(noutf8),
   DEBUG_BIT(pid),
   DEBUG_BIT(process_info),
   DEBUG_BIT(queue_run),
   DEBUG_BIT(receive),
-  DEBUG_BIT(resolver),
-  DEBUG_BIT(retry),		/* 23 */
+  DEBUG_BIT(resolver),		/* 23 */
+  DEBUG_BIT(retry),
   DEBUG_BIT(rewrite),
   DEBUG_BIT(route),
   DEBUG_BIT(timestamp),
   DEBUG_BIT(tls),
   DEBUG_BIT(transport),
   DEBUG_BIT(uid),
-  DEBUG_BIT(verify),		/* 30 - one spare! */
+  DEBUG_BIT(verify),		/* 31 */
 };
 
 /* Multi-bit debug masks */
@@ -469,6 +474,7 @@ enum logbit {
   Li_dkim,
   Li_dkim_verbose,
   Li_dnssec,
+  Li_ident_timeout,
   Li_incoming_interface,
   Li_incoming_port,
   Li_millisec,
diff --git a/src/src/readconf.c b/src/src/readconf.c
index f4da20b6a..1fe2b2bfc 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -298,6 +298,9 @@ static optionlist optionlist_config[] = {
   { "retry_interval_max",       opt_time,        {&retry_interval_max} },
   { "return_path_remove",       opt_bool,        {&return_path_remove} },
   { "return_size_limit",        opt_mkint|opt_hidden, {&bounce_return_size_limit} },
+  { "rfc1413_hosts",            opt_stringptr,   {&rfc1413_hosts} },
+  { "rfc1413_port",		opt_int|opt_hidden, {&test_harness_identd_port} },
+  { "rfc1413_query_timeout",    opt_time,        {&rfc1413_query_timeout} },
   { "sender_unqualified_hosts", opt_stringptr,   {&sender_unqualified_hosts} },
   { "slow_lookup_log",          opt_int,         {&slow_lookup_log} },
   { "smtp_accept_keepalive",    opt_bool,        {&smtp_accept_keepalive} },
diff --git a/src/src/verify.c b/src/src/verify.c
index a3ade640b..a12e74ce6 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -2779,6 +2779,162 @@ return yield;
 
 
 
+/*************************************************
+*            Get RFC 1413 identification         *
+*************************************************/
+
+/* Attempt to get an id from the sending machine via the RFC 1413 protocol. If
+the timeout is set to zero, then the query is not done. There may also be lists
+of hosts and nets which are exempt. To guard against malefactors sending
+non-printing characters which could, for example, disrupt a message's headers,
+make sure the string consists of printing characters only.
+
+Argument:
+  port    the port to connect to; usually this is IDENT_PORT (113), but when
+          running in the test harness with -bh a different value is used.
+
+Returns:  nothing
+
+Side effect: any received ident value is put in sender_ident (NULL otherwise)
+*/
+
+void
+verify_get_ident(int port)
+{
+client_conn_ctx ident_conn_ctx = {0};
+int host_af, qlen;
+int received_sender_port, received_interface_port, n;
+uschar *p;
+blob early_data;
+uschar buffer[2048];
+
+/* Default is no ident. Check whether we want to do an ident check for this
+host. */
+
+sender_ident = NULL;
+if (rfc1413_query_timeout <= 0 || verify_check_host(&rfc1413_hosts) != OK)
+  return;
+
+DEBUG(D_ident) debug_printf("doing ident callback\n");
+
+/* Set up a connection to the ident port of the remote host. Bind the local end
+to the incoming interface address. If the sender host address is an IPv6
+address, the incoming interface address will also be IPv6. */
+
+host_af = Ustrchr(sender_host_address, ':') == NULL ? AF_INET : AF_INET6;
+if ((ident_conn_ctx.sock = ip_socket(SOCK_STREAM, host_af)) < 0) return;
+
+if (ip_bind(ident_conn_ctx.sock, host_af, interface_address, 0) < 0)
+  {
+  DEBUG(D_ident) debug_printf("bind socket for ident failed: %s\n",
+    strerror(errno));
+  goto END_OFF;
+  }
+
+/* Construct and send the query. */
+
+qlen = snprintf(CS buffer, sizeof(buffer), "%d , %d\r\n",
+  sender_host_port, interface_port);
+early_data.data = buffer;
+early_data.len = qlen;
+
+/*XXX we trust that the query is idempotent */
+if (ip_connect(ident_conn_ctx.sock, host_af, sender_host_address, port,
+		rfc1413_query_timeout, &early_data) < 0)
+  {
+  if (errno == ETIMEDOUT && LOGGING(ident_timeout))
+    log_write(0, LOG_MAIN, "ident connection to %s timed out",
+      sender_host_address);
+  else
+    DEBUG(D_ident) debug_printf("ident connection to %s failed: %s\n",
+      sender_host_address, strerror(errno));
+  goto END_OFF;
+  }
+
+/* Read a response line. We put it into the rest of the buffer, using several
+recv() calls if necessary. */
+
+p = buffer + qlen;
+
+for (;;)
+  {
+  uschar *pp;
+  int count;
+  int size = sizeof(buffer) - (p - buffer);
+
+  if (size <= 0) goto END_OFF;   /* Buffer filled without seeing \n. */
+  count = ip_recv(&ident_conn_ctx, p, size, time(NULL) + rfc1413_query_timeout);
+  if (count <= 0) goto END_OFF;  /* Read error or EOF */
+
+  /* Scan what we just read, to see if we have reached the terminating \r\n. Be
+  generous, and accept a plain \n terminator as well. The only illegal
+  character is 0. */
+
+  for (pp = p; pp < p + count; pp++)
+    {
+    if (*pp == 0) goto END_OFF;   /* Zero octet not allowed */
+    if (*pp == '\n')
+      {
+      if (pp[-1] == '\r') pp--;
+      *pp = 0;
+      goto GOT_DATA;             /* Break out of both loops */
+      }
+    }
+
+  /* Reached the end of the data without finding \n. Let the loop continue to
+  read some more, if there is room. */
+
+  p = pp;
+  }
+
+GOT_DATA:
+
+/* We have received a line of data. Check it carefully. It must start with the
+same two port numbers that we sent, followed by data as defined by the RFC. For
+example,
+
+  12345 , 25 : USERID : UNIX :root
+
+However, the amount of white space may be different to what we sent. In the
+"osname" field there may be several sub-fields, comma separated. The data we
+actually want to save follows the third colon. Some systems put leading spaces
+in it - we discard those. */
+
+if (sscanf(CS buffer + qlen, "%d , %d%n", &received_sender_port,
+      &received_interface_port, &n) != 2 ||
+    received_sender_port != sender_host_port ||
+    received_interface_port != interface_port)
+  goto END_OFF;
+
+p = buffer + qlen + n;
+Uskip_whitespace(&p);
+if (*p++ != ':') goto END_OFF;
+Uskip_whitespace(&p);
+if (Ustrncmp(p, "USERID", 6) != 0) goto END_OFF;
+p += 6;
+Uskip_whitespace(&p);
+if (*p++ != ':') goto END_OFF;
+while (*p && *p != ':') p++;
+if (!*p++) goto END_OFF;
+Uskip_whitespace(&p);
+if (!*p) goto END_OFF;
+
+/* The rest of the line is the data we want. We turn it into printing
+characters when we save it, so that it cannot mess up the format of any logging
+or Received: lines into which it gets inserted. We keep a maximum of 127
+characters. The deconst cast is ok as we fed a nonconst to string_printing() */
+
+sender_ident = US string_printing(string_copyn(p, 127));
+DEBUG(D_ident) debug_printf("sender_ident = %s\n", sender_ident);
+
+END_OFF:
+(void)close(ident_conn_ctx.sock);
+return;
+}
+
+
+
+
 /*************************************************
 *      Match host to a single host-list item     *
 *************************************************/

commit e35ada6dc21d111ac30c30e8b9d792fbca55c9b4
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 2 14:34:25 2025 +0000

    TLS: log key-exchange group

diff --git a/src/src/exim.h b/src/src/exim.h
index 5b931e9f4..18d1b1f40 100644
--- a/src/src/exim.h
+++ b/src/src/exim.h
@@ -155,7 +155,8 @@ The driver name is a name of a router/transport/authenticator etc in the
 configuration file.  We also use this for some other short strings, such
 as queue names.
 Also TLS ciphersuite name (no real known limit since the protocols use
-integers, but max seen in reality is 45 octets).
+integers, but max seen so far is 70 octets.  Likely to increase further with
+postquantum methods).
 
 RFC 1413 gives us the 512 limit on IDENT protocol userids.
 */
@@ -168,7 +169,7 @@ RFC 1413 gives us the 512 limit on IDENT protocol userids.
 #define EXIM_HUMANNAME_MAX     256
 #define EXIM_DISPLAYMAIL_MAX  1024
 #define EXIM_DRIVERNAME_MAX     64
-#define EXIM_CIPHERNAME_MAX     64
+#define EXIM_CIPHERNAME_MAX    128
 #define EXIM_IDENTUSER_MAX     512
 
 
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index 154d3be38..f63d2ee12 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -57,11 +57,6 @@ require current GnuTLS, then we'll drop support for the ancient libraries).
 #if GNUTLS_VERSION_NUMBER >= 0x030000
 # define SUPPORT_SELFSIGN	/* Uncertain what version is first usable but 2.12.23 is not */
 #endif
-#if GNUTLS_VERSION_NUMBER >= 0x030306
-# define SUPPORT_CA_DIR
-#else
-# undef  SUPPORT_CA_DIR
-#endif
 #if GNUTLS_VERSION_NUMBER >= 0x030014
 # define SUPPORT_SYSDEFAULT_CABUNDLE
 #endif
@@ -78,6 +73,11 @@ require current GnuTLS, then we'll drop support for the ancient libraries).
 # define GNUTLS_AUTO_GLOBAL_INIT
 # define GNUTLS_AUTO_PKCS11_MANUAL
 #endif
+#if GNUTLS_VERSION_NUMBER >= 0x030306
+# define SUPPORT_CA_DIR
+#else
+# undef  SUPPORT_CA_DIR
+#endif
 #if (GNUTLS_VERSION_NUMBER >= 0x030404) \
   || (GNUTLS_VERSION_NUMBER >= 0x030311) && (GNUTLS_VERSION_NUMBER & 0xffff00 == 0x030300)
 # ifndef DISABLE_OCSP
@@ -90,6 +90,9 @@ require current GnuTLS, then we'll drop support for the ancient libraries).
 #if GNUTLS_VERSION_NUMBER >= 0x030506 && !defined(DISABLE_OCSP)
 # define SUPPORT_SRV_OCSP_STACK
 #endif
+#if GNUTLS_VERSION_NUMBER >= 0x030600
+# define EXIM_TLS_KEX_GROUP
+#endif
 #if GNUTLS_VERSION_NUMBER >= 0x030603
 # define EXIM_HAVE_TLS1_3
 # define SUPPORT_GNUTLS_EXT_RAW_PARSE
@@ -2385,11 +2388,15 @@ kx =
 old_pool = store_pool;
   {
   tls_support * tlsp = state->tlsp;
+  gstring * g = NULL;
+#ifdef EXIM_TLS_KEX_GROUP
+  const char * kex_gp = gnutls_group_get_name(gnutls_group_get(session));
+#endif
+
   store_pool = POOL_PERM;
 
 #ifdef SUPPORT_GNUTLS_SESS_DESC
     {
-    gstring * g = NULL;
     uschar * s = US gnutls_session_get_desc(session), c;
 
     if (!s)
@@ -2424,15 +2431,25 @@ old_pool = store_pool;
       if ((c = *s) && *++s == '-') g = string_catn(g, US"__", 2);
       /* now on _ between groups */
       }
-    g = string_catn(g, US":", 1);
-    g = string_cat(g, string_sprintf("%d", (int) gnutls_cipher_get_key_size(cipher) * 8));
+    g = string_fmt_append(g, ":%d", 
+		      (int) gnutls_cipher_get_key_size(cipher) * 8);
+
+# ifdef EXIM_TLS_KEX_GROUP
+    if (kex_gp)
+      g = string_fmt_append(g, ":%s", kex_gp);
+# endif
     state->ciphersuite = string_from_gstring(g);
     }
 #else
-  state->ciphersuite = string_sprintf("%s:%s:%d",
-      gnutls_protocol_get_name(protocol),
-      gnutls_cipher_suite_get_name(kx, cipher, mac),
-      (int) gnutls_cipher_get_key_size(cipher) * 8);
+  g = string_fmt_append(NULL, "%s:%s:%d",
+			gnutls_protocol_get_name(protocol),
+			gnutls_cipher_suite_get_name(kx, cipher, mac),
+			(int) gnutls_cipher_get_key_size(cipher) * 8);
+# ifdef EXIM_TLS_KEX_GROUP
+  if (kex_gp)
+    g = string_fmt_append(g, ":%s", kex_gp);
+# endif
+  state->ciphersuite = string_from_gstring(g);
 
   /* I don't see a way that spaces could occur, in the current GnuTLS
   code base, but it was a concern in the old code and perhaps older GnuTLS
@@ -2441,7 +2458,7 @@ old_pool = store_pool;
   for (uschar * p = state->ciphersuite; *p; p++) if (isspace(*p)) *p = '-';
   tlsp->ver = string_copyn(state->ciphersuite,
 			Ustrchr(state->ciphersuite, ':') - state->ciphersuite);
-#endif
+#endif /*SUPPORT_GNUTLS_SESS_DESC*/
 
 /* debug_printf("peer_status: ciphersuite %s\n", state->ciphersuite); */
 
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index ddc72bfa2..eea1f15d3 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -86,6 +86,9 @@ change this guard and punt the issue for a while longer. */
 # if OPENSSL_VERSION_NUMBER >= 0x010101000L
 #  define EXIM_TLS_EARLY_BANNER
 # endif
+# if OPENSSL_VERSION_NUMBER >= 0x030200000L
+#  define EXIM_TLS_KEX_GROUP
+# endif
 # if OPENSSL_VERSION_NUMBER <  0x030200020L
 #  define EXIM_OPENSSL_BOGUS_SERVER_ALPN	/*XXX when was this fixed? */
 # endif
@@ -3070,8 +3073,11 @@ return OK;
 *************************************************/
 
 /*
-Argument:   pointer to an SSL structure for the connection
-	    pointer to number of bits for cipher
+Arguments:
+  ssl	pointer to an SSL structure for the connection
+  ver	TLS version string
+  bits	pointer for return of number of bits for cipher
+
 Returns:    pointer to allocated string in perm-pool
 */
 
@@ -3089,7 +3095,15 @@ uschar * s;
 SSL_CIPHER_get_bits(c, bits);
 
 store_pool = POOL_PERM;
-s = string_sprintf("%s:%s:%u", ver, SSL_CIPHER_get_name(c), *bits);
+  {
+#ifdef EXIM_TLS_KEX_GROUP
+  const char * cs = SSL_get0_group_name(ssl);
+  if (cs)
+    s = string_sprintf("%s:%s:%u:%s", ver, SSL_CIPHER_get_name(c), *bits, cs);
+  else
+#endif
+    s = string_sprintf("%s:%s:%u", ver, SSL_CIPHER_get_name(c), *bits);
+  }
 store_pool = pool;
 DEBUG(D_tls) debug_printf("Cipher: %s\n", s);
 return s;

commit b93ee3883ef8a11c440c5519812f3cb6c074a02f
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 2 19:58:29 2025 +0000

    Build: quieten sqlite-hints -Wunused-function build

diff --git a/src/src/hintsdb.h b/src/src/hintsdb.h
index 92cb91909..d2d30969b 100644
--- a/src/src/hintsdb.h
+++ b/src/src/hintsdb.h
@@ -74,8 +74,8 @@ extern void    debug_printf_indent(const char *, ...) PRINTF_FUNCTION(1,2);
 #  error USE_SQLITE conflict with alternate definition
 # endif
 # include "hintsdb/hints_sqlite.h"
-#elif defined(USE_TDB)
 
+#elif defined(USE_TDB)
 # if defined(USE_DB) || defined(USE_GDBM) || defined(USE_SQLITE)
 #  error USE_TDB conflict with alternate definition
 # endif
diff --git a/src/src/hintsdb/hints_sqlite.h b/src/src/hintsdb/hints_sqlite.h
index 9e1e90322..9e4f65a51 100644
--- a/src/src/hintsdb/hints_sqlite.h
+++ b/src/src/hintsdb/hints_sqlite.h
@@ -354,17 +354,17 @@ exim_dbclose_multi__(dbp);
 
 /* Datum access */
 
-static uschar *
+static inline uschar *
 exim_datum_data_get(EXIM_DATUM * dp)
 { return US dp->data; }
-static void
+static inline void
 exim_datum_data_set(EXIM_DATUM * dp, void * s)
 { dp->data = s; }
  
-static unsigned
+static inline unsigned
 exim_datum_size_get(EXIM_DATUM * dp)
 { return dp->len; }
-static void
+static inline void
 exim_datum_size_set(EXIM_DATUM * dp, unsigned n)
 { dp->len = n; }
 

commit c7b6065cb9b945155491477297662bae458919d4
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 2 21:06:43 2025 +0000

    RFC 2047: fix encode operation.  Bug 3168

diff --git a/src/src/parse.c b/src/src/parse.c
index d840beb6d..cdb1e0c1e 100644
--- a/src/src/parse.c
+++ b/src/src/parse.c
@@ -882,13 +882,10 @@ const uschar *
 parse_quote_2047(const uschar * string, int len, const uschar * charset,
   BOOL fold)
 {
-int hlen, line_off;
-BOOL coded = FALSE;
-BOOL first_byte = FALSE;
-gstring * g =
-  string_fmt_append(NULL, "=?%s?Q?%n", charset ? charset : US"iso-8859-1", &hlen);
-
-line_off = hlen;
+int line_off = 0, hlen;
+BOOL coded = FALSE, first_byte = FALSE;
+gstring * g = string_fmt_append(NULL, "=?%s?Q?%n",
+				charset ? charset : US"iso-8859-1", &hlen);
 
 for (const uschar * s = string; len > 0; s++, len--)
   {
@@ -898,7 +895,7 @@ for (const uschar * s = string; len > 0; s++, len--)
     {
     g = fold ? string_catn(g, US"?=\n ", 4) : string_catn(g, US"?= ", 3);
     line_off = g->ptr;
-    g = string_catn(g, g->s, hlen);
+    g = string_catn(g, g->s, hlen);		/* dup the leader */
     }
 
   if (  ch < 33 || ch > 126

commit dd081f9869df90ab7f7d7911c1bdd3ef976f1439
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Oct 29 12:13:09 2025 +0000

    constify

diff --git a/src/src/acl.c b/src/src/acl.c
index 36fb850c8..7d07ab698 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -3939,7 +3939,7 @@ for (; cb; cb = cb->next)
       /* See comment on ACLC_SPF wrt. coding issues */
       {
       misc_module_info * mi = misc_mod_find(US"dmarc", &log_message);
-      typedef uschar * (*efn_t)(int);
+      typedef const uschar * (*efn_t)(void);
       const uschar * expanded_query;
 
       if (!mi)
@@ -3956,8 +3956,7 @@ for (; cb; cb = cb->next)
       view into the process in the future. */
 
       /*XXX is this call used with any other arg? */
-      expanded_query = (((efn_t *) mi->functions)[DMARC_EXPAND_QUERY])
-						      (DMARC_VERIFY_STATUS);
+      expanded_query = (((efn_t *) mi->functions)[DMARC_EXPAND_QUERY]) ();
       rc = match_isinlist(expanded_query,
 			  &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
       }
diff --git a/src/src/functions.h b/src/src/functions.h
index 98dc1a461..4801eb8b5 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -374,15 +374,17 @@ extern int     misc_mod_msg_init(void);
 extern void    misc_mod_smtp_reset(void);
 
 extern uschar *moan_check_errorcopy(const uschar *);
-extern BOOL    moan_skipped_syntax_errors(uschar *, error_block *, uschar *,
-                 BOOL, uschar *);
+extern BOOL    moan_skipped_syntax_errors(const uschar *, const error_block *,
+		const uschar *, BOOL, const uschar *);
 extern void    moan_smtp_batch(const uschar *, const char *, ...)
 		  PRINTF_FUNCTION(2,3);
-extern BOOL    moan_send_message(const uschar *, int, error_block *eblock,
-		 header_line *, FILE *, const uschar *);
-extern void    moan_tell_someone(const uschar *, address_item *,
-                 const uschar *, const char *, ...) PRINTF_FUNCTION(4,5);
-extern BOOL    moan_to_sender(int, error_block *, header_line *, FILE *, BOOL);
+extern BOOL    moan_send_message(const uschar *, int,
+		const error_block * eblock, const header_line *,
+		FILE *, const uschar *);
+extern void    moan_tell_someone(const uschar *, const address_item *,
+                const uschar *, const char *, ...) PRINTF_FUNCTION(4,5);
+extern BOOL    moan_to_sender(int, const error_block *, const header_line *,
+		FILE *, BOOL);
 extern void    moan_write_from(FILE *);
 extern void    moan_write_references(FILE *, uschar *);
 #ifdef LOOKUP_MODULE_DIR
diff --git a/src/src/miscmods/dmarc.c b/src/src/miscmods/dmarc.c
index bc96607e0..185ef9f4f 100644
--- a/src/src/miscmods/dmarc.c
+++ b/src/src/miscmods/dmarc.c
@@ -60,10 +60,10 @@ static dmarc_exim_p dmarc_policy_description[] = {
 /* $variables */
 BOOL	 dmarc_alignment_dkim	 = FALSE; /* Subtest result */
 BOOL	 dmarc_alignment_spf	 = FALSE; /* Subtest result */
-uschar * dmarc_domain_policy     = NULL; /* Declared policy of used domain */
-uschar * dmarc_status            = NULL; /* One word value */
-uschar * dmarc_status_text       = NULL; /* Human readable value */
-uschar * dmarc_used_domain       = NULL; /* Domain libopendmarc chose for DMARC policy lookup */
+const uschar * dmarc_domain_policy = NULL; /* Declared policy of used domain */
+const uschar * dmarc_status;		/* One word value */
+const uschar * dmarc_status_text   = NULL; /* Human readable value */
+uschar * dmarc_used_domain;		/* Domain libopendmarc chose for DMARC policy lookup */
 
 /* options */
 uschar * dmarc_forensic_sender   = NULL; /* Set sender address for forensic reports */
@@ -108,9 +108,9 @@ end, and append the two strings passed to it.  Used for adding
 variable amounts of value:pair data to the forensic emails. */
 
 static error_block *
-add_to_eblock(error_block *eblock, uschar *t1, uschar *t2)
+add_to_eblock(error_block * eblock, const uschar * t1, const uschar * t2)
 {
-error_block *eb = store_malloc(sizeof(error_block));
+error_block * eb = store_malloc(sizeof(error_block));
 if (!eblock)
   eblock = eb;
 else
@@ -645,10 +645,7 @@ The EDITME provides a DMARC_API variable */
   libdm_status = opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans);
   for (c = 0; dmarc_policy_description[c].name; c++)
     if (tmp_ans == dmarc_policy_description[c].value)
-      {
-      dmarc_domain_policy = string_sprintf("%s",dmarc_policy_description[c].name);
-      break;
-      }
+      { dmarc_domain_policy = dmarc_policy_description[c].name; break; }
 
   /* Can't use exim's string manipulation functions so allocate memory
   for libopendmarc using its max hostname length definition. */
@@ -743,23 +740,19 @@ if (!f.dmarc_disable_verify)
 return OK;
 }
 
-static uschar *
-dmarc_exim_expand_defaults(int what)
+static const uschar *
+dmarc_exim_expand_defaults(void)
 {
-if (what == DMARC_VERIFY_STATUS)
-  return f.dmarc_disable_verify ?  US"off" : US"none";
-return US"";
+return f.dmarc_disable_verify ?  US"off" : US"none";
 }
 
-static uschar *
-dmarc_exim_expand_query(int what)
+static const uschar *
+dmarc_exim_expand_query(void)
 {
 if (f.dmarc_disable_verify || !dmarc_pctx)
-  return dmarc_exim_expand_defaults(what);
+  return dmarc_exim_expand_defaults();
 
-if (what == DMARC_VERIFY_STATUS)
-  return dmarc_status;
-return US"";
+return dmarc_status;
 }
 
 
diff --git a/src/src/moan.c b/src/src/moan.c
index efac33681..5635f93e2 100644
--- a/src/src/moan.c
+++ b/src/src/moan.c
@@ -159,8 +159,9 @@ Returns:         TRUE if message successfully sent
 */
 
 BOOL
-moan_send_message(const uschar * recipient, int ident, error_block * eblock,
-  header_line * headers, FILE * message_file, const uschar * firstline)
+moan_send_message(const uschar * recipient, int ident,
+  const error_block * eblock, const header_line * headers,
+  FILE * message_file, const uschar * firstline)
 {
 int written = 0, fd, status, count = 0, size_limit = bounce_return_size_limit;
 FILE * fp;
@@ -389,7 +390,7 @@ if (bounce_return_message)
 
   while (headers)
     {
-    if (headers->text != NULL) fprintf(fp, "%s", CS headers->text);
+    if (headers->text) fprintf(fp, "%s", CS headers->text);
     headers = headers->next;
     }
 
@@ -494,8 +495,8 @@ Returns:        FALSE if there is no sender_address to send to;
 */
 
 BOOL
-moan_to_sender(int ident, error_block *eblock, header_line *headers,
-  FILE *message_file, BOOL check_sender)
+moan_to_sender(int ident, const error_block * eblock,
+  const header_line * headers, FILE * message_file, BOOL check_sender)
 {
 uschar *firstline = NULL;
 uschar *msg = US"Error while reading message with no usable sender address";
@@ -601,7 +602,7 @@ Returns:        nothing
 */
 
 void
-moan_tell_someone(const uschar * who, address_item * addr,
+moan_tell_someone(const uschar * who, const address_item * addr,
   const uschar * subject, const char * format, ...)
 {
 FILE * f;
@@ -814,14 +815,14 @@ Returns:            FALSE if string expansion failed; TRUE otherwise
 */
 
 BOOL
-moan_skipped_syntax_errors(uschar *rname, error_block *eblock,
-  uschar *syntax_errors_to, BOOL some, uschar *custom)
+moan_skipped_syntax_errors(const uschar * rname, const error_block * eblock,
+  const uschar * syntax_errors_to, BOOL some, const uschar * custom)
 {
 int pid, fd;
 const uschar * s;
 FILE * f;
 
-for (error_block * e = eblock; e; e = e->next)
+for (const error_block * e = eblock; e; e = e->next)
   if (e->text2)
     log_write(0, LOG_MAIN, "%s router: skipped error: %s in %q",
       rname, e->text1, e->text2);
@@ -870,11 +871,10 @@ if (custom)
 fprintf(f, "The %s router encountered the following error(s):\n\n",
   rname);
 
-for (error_block * e = eblock; e; e = e->next)
+for (const error_block * e = eblock; e; e = e->next)
   {
   fprintf(f, "  %s", e->text1);
-  if (e->text2 != NULL)
-    fprintf(f, " in the address\n  \"%s\"", e->text2);
+  if (e->text2) fprintf(f, " in the address\n  \"%s\"", e->text2);
   fprintf(f, "\n\n");
   }
 
diff --git a/src/src/structs.h b/src/src/structs.h
index 147819dd5..a6261afcb 100644
--- a/src/src/structs.h
+++ b/src/src/structs.h
@@ -680,8 +680,8 @@ typedef struct {
 
 typedef struct error_block {
   struct error_block *next;
-  const uschar *text1;
-  uschar *text2;
+  const uschar * text1;
+  const uschar * text2;
 } error_block;
 
 /* Chain of file names when processing the queue */

commit dea05068da8cbd8cb9c9707aa8c3e432bd967e55
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Nov 3 16:40:26 2025 +0000

    fix constification
    
    Broken-by: 96f8f12e212b

diff --git a/src/src/exim.c b/src/src/exim.c
index df343b5a1..c6fa25fc7 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -5320,7 +5320,7 @@ if (verify_address_mode || f.address_test_mode)
       while (*s)
         {
         BOOL finished = FALSE;
-        uschar *ss = parse_find_address_end(s, FALSE);
+        uschar * ss = parse_find_address_end(s, FALSE);
         if (*ss == ',') *ss = 0; else finished = TRUE;
         test_address(s, flags, &exit_value);
         s = ss;
@@ -5875,7 +5875,7 @@ for (BOOL more = TRUE; more; )
       uschar * errmess;
       /* There can be multiple addresses, so EXIM_DISPLAYMAIL_MAX (tuned for 1) is too short.
       We'll still want to cap it to something, just in case. */
-      const uschar * s = string_copy_taint(
+      uschar * s = string_copy_taint(
 	exim_str_fail_toolong(list[i], BIG_BUFFER_SIZE, "address argument"),
 	GET_TAINTED);
 
diff --git a/src/src/functions.h b/src/src/functions.h
index 4801eb8b5..ef1c59dc3 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -398,7 +398,17 @@ extern uschar *parse_extract_address(const uschar *, uschar **, int *, int *, in
                  BOOL);
 extern int     parse_forward_list(const uschar *, int, address_item **, uschar **,
                  const uschar *, const uschar *, error_block **);
-extern uschar *parse_find_address_end(const uschar *, BOOL);
+
+extern const uschar * parse_find_address_end_gen(const uschar *, BOOL);
+static inline uschar * parse_find_address_end_nc(uschar * s, BOOL b)
+{ return US parse_find_address_end_gen(s, b); }
+static inline const uschar * parse_find_address_end_c(const uschar * s, BOOL b)
+{ return    parse_find_address_end_gen(s, b); }
+#define parse_find_address_end(X, B) _Generic((X),     \
+	      uschar *:		parse_find_address_end_nc, \
+	      const uschar *:	parse_find_address_end_c \
+	      )(X, B)
+
 extern const uschar *parse_find_at(const uschar *);
 extern const uschar *parse_fix_phrase(const uschar *, int);
 extern const uschar *parse_message_id(const uschar *, uschar **, uschar **);
diff --git a/src/src/miscmods/exim_filter.c b/src/src/miscmods/exim_filter.c
index 08124b3f7..21ede4ad5 100644
--- a/src/src/miscmods/exim_filter.c
+++ b/src/src/miscmods/exim_filter.c
@@ -2419,15 +2419,14 @@ while (commands)
 	  tt = to;
 	  while (*tt)
 	    {
-	    uschar * ss = parse_find_address_end(tt, FALSE), * errmess;
-	    const uschar * recipient;
+	    const uschar * ss = parse_find_address_end(tt, FALSE);
+	    const uschar * ttt, * recipient;
+	    uschar * errmess;
 	    int start, end, domain;
-	    int temp = *ss;
 
-	    *ss = 0;
-	    recipient = parse_extract_address(tt, &errmess,
+	    ttt = *ss ? string_copyn(tt, ss - tt) : tt;
+	    recipient = parse_extract_address(ttt, &errmess,
 					      &start, &end, &domain, FALSE);
-	    *ss = temp;
 
 	    /* Ignore empty addresses and errors; an error will occur later if
 	    there's something really bad. */
diff --git a/src/src/miscmods/sieve_filter.c b/src/src/miscmods/sieve_filter.c
index 38b408f1b..cb57fa11f 100644
--- a/src/src/miscmods/sieve_filter.c
+++ b/src/src/miscmods/sieve_filter.c
@@ -2036,7 +2036,7 @@ if (parse_identifier(filter, CUS "address"))
   for (gstring * h = hdr; h->ptr != -1 && !*cond; ++h)
     {
     const uschar * header_value = NULL;
-    uschar * extracted_addr, * end_addr;
+    uschar * extracted_addr;
 
     if (  !eq_asciicase(h, &str_from, FALSE)
        && !eq_asciicase(h, &str_to, FALSE)
@@ -2061,15 +2061,15 @@ if (parse_identifier(filter, CUS "address"))
       f.parse_allow_group = TRUE;
       while (*header_value && !*cond)
         {
-        uschar *error;
+        uschar * part = NULL, * error;
+	const uschar * end_addr, * ss;
         int start, end, domain;
-        int saveend;
-        uschar *part = NULL;
 
         end_addr = parse_find_address_end(header_value, FALSE);
-        saveend = *end_addr;
-        *end_addr = 0;
-        extracted_addr = parse_extract_address(header_value, &error, &start, &end, &domain, FALSE);
+	ss = *end_addr
+	  ? header_value : string_copyn(header_value, end_addr - header_value);
+        extracted_addr = parse_extract_address(ss, &error,
+						&start, &end, &domain, FALSE);
 
         if (extracted_addr) switch (addressPart)
           {
@@ -2084,7 +2084,6 @@ if (parse_identifier(filter, CUS "address"))
 #endif
           }
 
-        *end_addr = saveend;
         if (part && extracted_addr)
 	  {
 	  gstring partStr = {.s = part, .ptr = Ustrlen(part), .size = Ustrlen(part)+1};
@@ -2096,7 +2095,7 @@ if (parse_identifier(filter, CUS "address"))
             }
 	  }
 
-        if (saveend == 0) break;
+        if (*end_addr) break;
         header_value = end_addr + 1;
         }
       f.parse_allow_group = FALSE;
diff --git a/src/src/parse.c b/src/src/parse.c
index cdb1e0c1e..1fe4f70f8 100644
--- a/src/src/parse.c
+++ b/src/src/parse.c
@@ -68,8 +68,8 @@ Returns:   pointer past the end of the address
            (i.e. points to null or comma)
 */
 
-uschar *
-parse_find_address_end(const uschar * s, BOOL nl_ends)
+const uschar *
+parse_find_address_end_gen(const uschar * s, BOOL nl_ends)
 {
 BOOL source_routing = *s == '@';
 int no_term = source_routing ? 1 : 0;
@@ -129,7 +129,7 @@ while (*s && (*s != ',' || no_term > 0) && (*s != '\n' || !nl_ends))
     }
   }
 
-return US s;
+return s;
 }
 
 
diff --git a/src/src/verify.c b/src/src/verify.c
index a12e74ce6..1fee97150 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -2338,9 +2338,9 @@ Returns:     OK
 */
 
 int
-verify_check_headers(uschar **msgptr)
+verify_check_headers(uschar ** msgptr)
 {
-uschar *colon, *s;
+uschar * colon, * s;
 int yield = OK;
 
 for (header_line * h = header_list; h && yield == OK; h = h->next)
@@ -2639,8 +2639,7 @@ int yield = FAIL;
 for (int i = 0; i < 3 && !done; i++)
   for (const header_line * h = header_list; h && !done; h = h->next)
     {
-    const uschar * endname, * s;
-    uschar * ss;
+    const uschar * endname, * s, * ss, * es;
 
     if (h->type != header_types[i]) continue;
     s = endname = Ustrchr(h->text, ':') + 1;
@@ -2652,38 +2651,36 @@ for (int i = 0; i < 3 && !done; i++)
 
     while (*s)
       {
-      int terminator, new_ok;
+      int new_ok;
       address_item * vaddr;
 
       while (isspace(*s) || *s == ',') s++;
       if (!*s) break;			/* End of header */
 
-      ss = parse_find_address_end(s, FALSE);
+      es = parse_find_address_end(s, FALSE);
 
       /* The terminator is a comma or end of header, but there may be white
       space preceding it (including newline for the last address). Move back
       past any white space so we can check against any cached envelope sender
       address verifications. */
 
-      while (isspace(ss[-1])) ss--;
-      terminator = *ss;
-      *ss = '\0';
+      while (isspace(es[-1])) es--;
+      ss = *es ? string_copyn(s, es - s) : s;
 
       HDEBUG(D_verify) debug_printf("verifying %.*s header address %s\n",
-        (int)(endname - h->text), h->text, s);
+        (int)(endname - h->text), h->text, ss);
 
       /* See if we have already verified this address as an envelope sender,
       and if so, use the previous answer. */
 
-      vaddr = verify_checked_sender(s);
+      vaddr = verify_checked_sender(ss);
 
-      if (vaddr != NULL &&                   /* Previously checked */
-           (callout <= 0 ||                  /* No callout needed; OR */
-            vaddr->special_action > 256))    /* Callout was done */
+      if (   vaddr				/* Previously checked */
+          && (  callout <= 0			/* No callout needed; OR */
+	     || vaddr->special_action > 256))	/* Callout was done */
         {
         new_ok = vaddr->special_action & 255;
         HDEBUG(D_verify) debug_printf("previously checked as envelope sender\n");
-        *ss = terminator;  /* Restore shortened string */
         }
 
       /* Otherwise we run the verification now. We must restore the shortened
@@ -2693,9 +2690,8 @@ for (int i = 0; i < 3 && !done; i++)
       else
         {
         int start, end, domain;
-        const uschar * address = parse_extract_address(s, log_msgptr,
+        const uschar * address = parse_extract_address(ss, log_msgptr,
 						  &start, &end, &domain, FALSE);
-        *ss = terminator;
 
         /* If we found an empty address, just carry on with the next one, but
         kill the message. */
@@ -2703,7 +2699,7 @@ for (int i = 0; i < 3 && !done; i++)
         if (!address && Ustrcmp(*log_msgptr, "empty address") == 0)
           {
           *log_msgptr = NULL;
-          s = ss;
+          s = es;
           continue;
           }
 
@@ -2713,10 +2709,10 @@ for (int i = 0; i < 3 && !done; i++)
 
         if (!address)
           {
-          while (ss > s && isspace(ss[-1])) ss--;
+          while (es > s && isspace(es[-1])) es--;
           *log_msgptr = string_sprintf("syntax error in '%.*s' header when "
             "scanning for sender: %s in \"%.*s\"",
-            (int)(endname - h->text), h->text, *log_msgptr, (int)(ss - s), s);
+            (int)(endname - h->text), h->text, *log_msgptr, (int)(es - s), s);
           yield = FAIL;
           done = TRUE;
           break;
@@ -2759,7 +2755,7 @@ for (int i = 0; i < 3 && !done; i++)
 
       /* Move on to any more addresses in the header */
 
-      s = ss;
+      s = es;
       }     /* Next address */
 
     f.parse_allow_group = FALSE;

commit e5dc9209cde969a66ac528c2d5fc5a244c5f5999
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Nov 4 09:58:43 2025 +0000

    compiler quietening

diff --git a/src/src/auths/cram_md5.c b/src/src/auths/cram_md5.c
index 2f6a56626..44b05465c 100644
--- a/src/src/auths/cram_md5.c
+++ b/src/src/auths/cram_md5.c
@@ -272,7 +272,6 @@ auth_cram_md5_options_block * ob = ablock->drinst.options_block;
 uschar *secret = expand_string(ob->client_secret);
 uschar *name = expand_string(ob->client_name);
 uschar *challenge, *p;
-int i;
 uschar digest[16];
 
 /* If expansion of either the secret or the user name failed, return CANCELLED
diff --git a/src/src/hintsdb/hints_sqlite.h b/src/src/hintsdb/hints_sqlite.h
index 9e4f65a51..01906f839 100644
--- a/src/src/hintsdb/hints_sqlite.h
+++ b/src/src/hintsdb/hints_sqlite.h
@@ -127,7 +127,6 @@ exim_dbget(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * res)
 {
 # define FMT "SELECT dat FROM tbl WHERE ky = '%s';"
 uschar * encoded_key, * qry;
-int i;
 BOOL ret;
 
 # ifdef COMPILE_UTILITY
@@ -140,12 +139,14 @@ encoded_key = xtextencode(key->data, key->len);
 				  (int)key->len, encoded_key); */
 
 # ifdef COMPILE_UTILITY
-i = snprintf(NULL, 0, FMT, encoded_key) + 1;
-if (!(qry = malloc(i)))
-  return FALSE;
-snprintf(CS qry, i, FMT, encoded_key);
-ret = exim_dbget__(dbp, qry, res);
-free(qry);
+  {
+  int i = snprintf(NULL, 0, FMT, encoded_key) + 1;
+  if (!(qry = malloc(i)))
+    return FALSE;
+  snprintf(CS qry, i, FMT, encoded_key);
+  ret = exim_dbget__(dbp, qry, res);
+  free(qry);
+  }
 free(encoded_key);
 # else
 qry = string_sprintf(FMT, encoded_key);
@@ -172,7 +173,7 @@ int hlen = data->len * 2, off = 0, res;
 # define FMT "INSERT OR %s INTO tbl (ky,dat) VALUES ('%s', X'%.*s');"
 uschar * encoded_key, * qry;
 # ifdef COMPILE_UTILITY
-uschar * hex = malloc(hlen+1);
+uschar * hex = malloc(hlen+1), dummy[1];
 if (!hex) return EXIM_DBPUTB_DUP;	/* best we can do */
 # else
 uschar * hex = store_get(hlen+1, data->data);
@@ -186,7 +187,7 @@ for (const uschar * s = data->data, * t = s + data->len; s < t; s++, off += 2)
 # ifdef COMPILE_UTILITY
 if (!(encoded_key = xtextencode(key->data, key->len)))
   return EXIM_DBPUTB_DUP;
-res = snprintf(CS hex, 0, FMT, alt, encoded_key, hlen, hex) +1;
+res = snprintf(CS dummy, 0, FMT, alt, encoded_key, hlen, hex) +1;
 if (!(qry = malloc(res))) return EXIM_DBPUTB_DUP;
 snprintf(CS qry, res, FMT, alt, encoded_key, hlen, hex);
 DEBUG(D_hints_lookup) debug_printf_indent("exim_s_dbp(%s)\n", qry);
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index f63d2ee12..b78c33fcb 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -393,7 +393,7 @@ Returns:    DEFER/FAIL
 */
 
 static int
-tls_error(const uschar *prefix, const uschar *msg, const host_item *host,
+tls_error(const uschar * prefix, const uschar * msg, const host_item * host,
   uschar ** errstr)
 {
 if (errstr)
@@ -404,7 +404,7 @@ return host ? FAIL : DEFER;
 
 /* Returns:    DEFER/FAIL */
 static int
-tls_error_gnu(exim_gnutls_state_st * state, const uschar *prefix, int err,
+tls_error_gnu(exim_gnutls_state_st * state, const uschar * prefix, int err,
   uschar ** errstr)
 {
 return tls_error(prefix,
@@ -461,7 +461,6 @@ return FALSE;
 static int
 tls_g_init(uschar ** errstr)
 {
-int rc;
 DEBUG(D_tls) debug_printf("GnuTLS global init required\n");
 
 #if defined(HAVE_GNUTLS_PKCS11) && !defined(GNUTLS_AUTO_PKCS11_MANUAL)
@@ -472,13 +471,19 @@ environment variables are used and so breaks for users calling mailq.
 To prevent this, we init PKCS11 first, which is the documented approach. */
 
 if (!gnutls_allow_auto_pkcs11)
+  {
+  int rc;
   if ((rc = gnutls_pkcs11_init(GNUTLS_PKCS11_FLAG_MANUAL, NULL)))
     return tls_error_gnu(NULL, US"gnutls_pkcs11_init", rc, errstr);
+  }
 #endif
 
 #ifndef GNUTLS_AUTO_GLOBAL_INIT
-if ((rc = gnutls_global_init()))
-  return tls_error_gnu(NULL, US"gnutls_global_init", rc, errstr);
+  {
+  int rc;
+  if ((rc = gnutls_global_init()))
+    return tls_error_gnu(NULL, US"gnutls_global_init", rc, errstr);
+  }
 #endif
 
 #if EXIM_GNUTLS_LIBRARY_LOG_LEVEL >= 0
@@ -833,7 +838,6 @@ const uschar * filename = NULL;
 size_t sz;
 uschar * exp_tls_dhparam;
 BOOL use_file_in_spool = FALSE;
-const host_item * host = NULL; /* dummy for macros */
 
 DEBUG(D_tls) debug_printf("Initialising GnuTLS server params\n");
 
@@ -1380,12 +1384,14 @@ creds_load_server_certs(exim_gnutls_state_st * state, const uschar * cert,
   const uschar * pkey, const uschar * ocsp, uschar ** errstr)
 {
 const uschar * clist = cert, * klist = pkey, * kfile, * cfile;
-int csep = 0, ksep = 0, cnt = 0, rc;
+int csep = 0, ksep = 0, rc;
 #ifndef DISABLE_OCSP
 uschar * ofile;
 const uschar * olist;
 # ifdef SUPPORT_GNUTLS_EXT_RAW_PARSE
 gnutls_x509_crt_fmt_t ocsp_fmt = GNUTLS_X509_FMT_DER;
+# else
+int ocsp_file_cnt = 0;
 # endif
 
 if (!expand_check(ocsp, US"tls_ocsp_file", &ofile, errstr))
@@ -1460,7 +1466,7 @@ while (cfile = string_nextinlist(&clist, &csep, NULL, 0))
 	else
 #  endif
 	  {
-	  if (cnt++ > 0)
+	  if (ocsp_file_cnt++ > 0)
 	    {
 	    DEBUG(D_tls)
 	      debug_printf("oops; multiple OCSP files not supported\n");

commit 2486cac31dc0ce68d3705695c6fe61d68db51d31
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Nov 4 11:12:11 2025 +0000

    Unbreak Solaris build
    
    Broken-by: dea05068da8c

diff --git a/src/src/macro_predef.c b/src/src/macro_predef.c
index caa6bf7c3..e7070f554 100644
--- a/src/src/macro_predef.c
+++ b/src/src/macro_predef.c
@@ -20,9 +20,12 @@ unsigned mp_index = 0;
 void fn_smtp_receive_timeout(const uschar * name, const uschar * str) {}
 uschar * syslog_facility_str;
 
-/* Solaris needs this one for the macro expand_string() */
+/* Solaris needs these for the macros expand_string, parse_find_address_end */
 const uschar * expand_string_2(const uschar * string, BOOL * textonly_p)
 {return NULL; }
+const uschar * parse_find_address_end_gen(const uschar *, BOOL)
+{return NULL; }
+
 
 /******************************************************************************/
 

commit acfcde05f2800db3bf24ba521c62599d95713a08
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Nov 4 11:48:08 2025 +0000

    Unbreak Solaris build
    
    Broken-by: dea05068da8c

diff --git a/src/src/macro_predef.c b/src/src/macro_predef.c
index e7070f554..79cbd5290 100644
--- a/src/src/macro_predef.c
+++ b/src/src/macro_predef.c
@@ -23,7 +23,7 @@ uschar * syslog_facility_str;
 /* Solaris needs these for the macros expand_string, parse_find_address_end */
 const uschar * expand_string_2(const uschar * string, BOOL * textonly_p)
 {return NULL; }
-const uschar * parse_find_address_end_gen(const uschar *, BOOL)
+const uschar * parse_find_address_end_gen(const uschar * s, BOOL b)
 {return NULL; }
 
 

commit 826a1788778a6655a25b7d157ff4000531b9a215
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Nov 4 12:46:33 2025 +0000

        Unbreak Solaris build
    
        Broken-by: dea05068da8c

diff --git a/src/src/exim_dbmbuild.c b/src/src/exim_dbmbuild.c
index 02754d0ee..99ea5c826 100644
--- a/src/src/exim_dbmbuild.c
+++ b/src/src/exim_dbmbuild.c
@@ -71,6 +71,9 @@ string_format_trc(uschar * buf, int len, const uschar * func, unsigned line,
 void
 log_write(unsigned int selector, int flags, const char *format, ...)
 { }
+const uschar * parse_find_address_end_gen(const uschar * s, BOOL b)
+{return NULL; }
+
 
 
 struct global_flags	f;
diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index 339d022bb..cd98ff053 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -77,6 +77,8 @@ BOOL
 string_format_trc(uschar * buf, int len, const uschar * func, unsigned line,
   const char * fmt, ...)
 { return FALSE; }
+const uschar * parse_find_address_end_gen(const uschar * s, BOOL b)
+{return NULL; }
 
 struct global_flags	f;
 unsigned int		log_selector[1];

commit e5c1a2ba01a0bd00615ec1bfd05b47c6127bec38
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Nov 4 14:52:05 2025 +0000

    Fix local delivery defers
    
    Broken-by: 3cee6033bae8

diff --git a/src/src/deliver.c b/src/src/deliver.c
index 29c5dbfc6..445b9dacb 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -2903,65 +2903,57 @@ while (addr_local)
     {
     BOOL ok = TRUE;   /* to deliver this address */
 
-    if (f.queue_2stage)
-      {
-      DEBUG(D_deliver)
-	debug_printf_indent("no router retry check (ph1 qrun)\n");
-      }
-    else
+    /* Set up the retry key to include the domain or not, and change its
+    leading character from "R" to "T". Must make a copy before doing this,
+    because the old key may be pointed to from a "delete" retry item after
+    a routing delay. */
+    uschar * retry_key = string_copy(tp->retry_use_local_part
+		      ? addr2->address_retry_key : addr2->domain_retry_key);
+    *retry_key = 'T';
+
+    /* Inspect the retry data. If there is no hints file, delivery happens. */
+
+    if (dbm_file)
       {
-      /* Set up the retry key to include the domain or not, and change its
-      leading character from "R" to "T". Must make a copy before doing this,
-      because the old key may be pointed to from a "delete" retry item after
-      a routing delay. */
-      uschar * retry_key = string_copy(tp->retry_use_local_part
-			? addr2->address_retry_key : addr2->domain_retry_key);
-      *retry_key = 'T';
+      dbdata_retry * retry_record = dbfn_read(dbm_file, retry_key);
 
-      /* Inspect the retry data. If there is no hints file, delivery happens. */
+      /* If there is no retry record, delivery happens. If there is,
+      remember it exists so it can be deleted after a successful delivery. */
 
-      if (dbm_file)
+      if (retry_record)
 	{
-	dbdata_retry * retry_record = dbfn_read(dbm_file, retry_key);
+	setflag(addr2, af_lt_retry_exists);
 
-	/* If there is no retry record, delivery happens. If there is,
-	remember it exists so it can be deleted after a successful delivery. */
+	/* A retry record exists for this address. If queue running and not
+	forcing, inspect its contents. If the record is too old, or if its
+	retry time has come, or if it has passed its cutoff time, delivery
+	will go ahead. */
 
-	if (retry_record)
+	DEBUG(D_retry)
 	  {
-	  setflag(addr2, af_lt_retry_exists);
-
-	  /* A retry record exists for this address. If queue running and not
-	  forcing, inspect its contents. If the record is too old, or if its
-	  retry time has come, or if it has passed its cutoff time, delivery
-	  will go ahead. */
-
-	  DEBUG(D_retry)
-	    {
-	    debug_printf("retry record exists: age=%s ",
-	      readconf_printtime(now - retry_record->time_stamp));
-	    debug_printf("(max %s)\n", readconf_printtime(retry_data_expire));
-	    debug_printf("  time to retry = %s expired = %d\n",
-	      readconf_printtime(retry_record->next_try - now),
-	      retry_record->expired);
-	    }
+	  debug_printf("retry record exists: age=%s ",
+	    readconf_printtime(now - retry_record->time_stamp));
+	  debug_printf("(max %s)\n", readconf_printtime(retry_data_expire));
+	  debug_printf("  time to retry = %s expired = %d\n",
+	    readconf_printtime(retry_record->next_try - now),
+	    retry_record->expired);
+	  }
 
-	  if (f.queue_running && !f.deliver_force)
-	    {
-	    ok = (now - retry_record->time_stamp > retry_data_expire)
-	      || (now >= retry_record->next_try)
-	      || retry_record->expired;
+	if (f.queue_running && !f.deliver_force)
+	  {
+	  ok = (now - retry_record->time_stamp > retry_data_expire)
+	    || (now >= retry_record->next_try)
+	    || retry_record->expired;
 
-	    /* If we haven't reached the retry time, there is one more check
-	    to do, which is for the ultimate address timeout. */
+	  /* If we haven't reached the retry time, there is one more check
+	  to do, which is for the ultimate address timeout. */
 
-	    if (!ok)
-	      ok = retry_ultimate_address_timeout(retry_key, addr2->domain,
-		  retry_record, now);
-	    }
+	  if (!ok)
+	    ok = retry_ultimate_address_timeout(retry_key, addr2->domain,
+		retry_record, now);
 	  }
-	else DEBUG(D_retry) debug_printf("no retry record exists\n");
 	}
+      else DEBUG(D_retry) debug_printf("no retry record exists\n");
       }
 
     /* This address is to be delivered. Leave it on the chain. */
@@ -7821,82 +7813,74 @@ while (addr_new)           /* Loop until all addresses dealt with */
       continue;
       }
 
-    if (f.queue_2stage)
+    /* Get the routing retry status, saving the two retry keys (with and
+    without the local part) for subsequent use. If there is no retry record
+    for the standard address routing retry key, we look for the same key with
+    the sender attached, because this form is used by the smtp transport after
+    a 4xx response to RCPT when address_retry_include_sender is true. */
+
+    DEBUG(D_deliver|D_retry)
       {
-      DEBUG(D_deliver)
-	debug_printf_indent("no router retry check (ph1 qrun)\n");
+      debug_printf_indent("checking router retry status\n");
+      acl_level++;
       }
-    else
-      {
-      /* Get the routing retry status, saving the two retry keys (with and
-      without the local part) for subsequent use. If there is no retry record
-      for the standard address routing retry key, we look for the same key with
-      the sender attached, because this form is used by the smtp transport after
-      a 4xx response to RCPT when address_retry_include_sender is true. */
+    addr->domain_retry_key = string_sprintf("R:%s", addr->domain);
+    addr->address_retry_key = string_sprintf("R:%s@%s", addr->local_part,
+      addr->domain);
 
-      DEBUG(D_deliver|D_retry)
+    if (dbm_file)
+      {
+      domain_retry_record = dbfn_read(dbm_file, addr->domain_retry_key);
+      if (  domain_retry_record
+	 && now - domain_retry_record->time_stamp > retry_data_expire
+	 )
 	{
-	debug_printf_indent("checking router retry status\n");
-	acl_level++;
+	DEBUG(D_deliver|D_retry)
+	  debug_printf_indent("domain retry record present but expired\n");
+	domain_retry_record = NULL;    /* Ignore if too old */
 	}
-      addr->domain_retry_key = string_sprintf("R:%s", addr->domain);
-      addr->address_retry_key = string_sprintf("R:%s@%s", addr->local_part,
-	addr->domain);
 
-      if (dbm_file)
+      address_retry_record = dbfn_read(dbm_file, addr->address_retry_key);
+      if (  address_retry_record
+	 && now - address_retry_record->time_stamp > retry_data_expire
+	 )
 	{
-	domain_retry_record = dbfn_read(dbm_file, addr->domain_retry_key);
-	if (  domain_retry_record
-	   && now - domain_retry_record->time_stamp > retry_data_expire
-	   )
-	  {
-	  DEBUG(D_deliver|D_retry)
-	    debug_printf_indent("domain retry record present but expired\n");
-	  domain_retry_record = NULL;    /* Ignore if too old */
-	  }
+	DEBUG(D_deliver|D_retry)
+	  debug_printf_indent("address retry record present but expired\n");
+	address_retry_record = NULL;   /* Ignore if too old */
+	}
 
-	address_retry_record = dbfn_read(dbm_file, addr->address_retry_key);
+      if (!address_retry_record)
+	{
+	const uschar * altkey = string_sprintf("%s:<%s>",
+				  addr->address_retry_key, sender_address);
+	address_retry_record = dbfn_read(dbm_file, altkey);
 	if (  address_retry_record
-	   && now - address_retry_record->time_stamp > retry_data_expire
-	   )
+	   && now - address_retry_record->time_stamp > retry_data_expire)
 	  {
 	  DEBUG(D_deliver|D_retry)
-	    debug_printf_indent("address retry record present but expired\n");
+	    debug_printf_indent("address<sender> retry record present but expired\n");
 	  address_retry_record = NULL;   /* Ignore if too old */
 	  }
-
-	if (!address_retry_record)
-	  {
-	  const uschar * altkey = string_sprintf("%s:<%s>",
-				    addr->address_retry_key, sender_address);
-	  address_retry_record = dbfn_read(dbm_file, altkey);
-	  if (  address_retry_record
-	     && now - address_retry_record->time_stamp > retry_data_expire)
-	    {
-	    DEBUG(D_deliver|D_retry)
-	      debug_printf_indent("address<sender> retry record present but expired\n");
-	    address_retry_record = NULL;   /* Ignore if too old */
-	    }
-	  }
 	}
+      }
 
-      DEBUG(D_deliver|D_retry)
-	{
-	if (!domain_retry_record)
-	  debug_printf_indent("no   domain  retry record\n");
-	else
-	  debug_printf_indent("have domain  retry record; next_try = now%+d\n",
-			f.running_in_test_harness ? 0 :
-			(int)(domain_retry_record->next_try - now));
+    DEBUG(D_deliver|D_retry)
+      {
+      if (!domain_retry_record)
+	debug_printf_indent("no   domain  retry record\n");
+      else
+	debug_printf_indent("have domain  retry record; next_try = now%+d\n",
+		      f.running_in_test_harness ? 0 :
+		      (int)(domain_retry_record->next_try - now));
 
-	if (!address_retry_record)
-	  debug_printf_indent("no   address retry record\n");
-	else
-	  debug_printf_indent("have address retry record; next_try = now%+d\n",
-			f.running_in_test_harness ? 0 :
-			(int)(address_retry_record->next_try - now));
-	acl_level--;
-	}
+      if (!address_retry_record)
+	debug_printf_indent("no   address retry record\n");
+      else
+	debug_printf_indent("have address retry record; next_try = now%+d\n",
+		      f.running_in_test_harness ? 0 :
+		      (int)(address_retry_record->next_try - now));
+      acl_level--;
       }
 
     /* If we are sending a message down an existing SMTP connection, we must

commit 140c289d1170334e29ee3fd4e2c385cdb7bd837c
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Nov 4 18:44:53 2025 +0000

    Fix remote-delivery DNS defers.  Bug 3172
    
    Broken-by: 6748707c6446

diff --git a/src/src/deliver.c b/src/src/deliver.c
index 445b9dacb..3dc1aef73 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -7540,42 +7540,37 @@ while (addr_new)           /* Loop until all addresses dealt with */
   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.  For the first stage of a 2-phase
-  queue run don't bother checking domain- or address-retry info; they will take
-  effect on the second stage. */
-
-  if (!f.queue_2stage)
-    {
-    /* If we have transaction-capable hintsdbs, open the retry db without
-    locking, and leave open for the transport process and for subsequent
-    deliveries. Use a writeable open as we can keep it open all the way through
-    to writing retry records if needed due to message fails.
-    If the open fails, tag that explicitly for the transport but retry the open
-    next time around, in case it was created in the interim.
-    If non-transaction, we are only reading records at this stage and
-    we close the db before running the transport.
-    Either way we do a non-creating open. */
-
-    if (continue_retry_db == (open_db *)-1)
-      continue_retry_db = NULL;
-
-    if (continue_retry_db)
-      {
-      DEBUG(D_hints_lookup) debug_printf("using cached retry hintsdb handle\n");
-      dbm_file = continue_retry_db;
-      }
-    else if (!exim_lockfile_needed())
-      {
-      dbm_file = dbfn_open_multi(US"retry", O_RDWR, &dbblock);
-      continue_retry_db = dbm_file ? dbm_file : (open_db *)-1;
-      }
-    else
-      dbm_file = dbfn_open(US"retry", O_RDONLY, &dbblock, FALSE, TRUE);
+  not exist. In both cases, dbm_file is NULL. */
+
+  /* If we have transaction-capable hintsdbs, open the retry db without
+  locking, and leave open for the transport process and for subsequent
+  deliveries. Use a writeable open as we can keep it open all the way through
+  to writing retry records if needed due to message fails.
+  If the open fails, tag that explicitly for the transport but retry the open
+  next time around, in case it was created in the interim.
+  If non-transaction, we are only reading records at this stage and
+  we close the db before running the transport.
+  Either way we do a non-creating open. */
 
-    if (!dbm_file)
-      DEBUG(D_deliver|D_retry|D_route|D_hints_lookup)
-	debug_printf("no retry data available\n");
+  if (continue_retry_db == (open_db *)-1)
+    continue_retry_db = NULL;
+
+  if (continue_retry_db)
+    {
+    DEBUG(D_hints_lookup) debug_printf("using cached retry hintsdb handle\n");
+    dbm_file = continue_retry_db;
+    }
+  else if (!exim_lockfile_needed())
+    {
+    dbm_file = dbfn_open_multi(US"retry", O_RDWR, &dbblock);
+    continue_retry_db = dbm_file ? dbm_file : (open_db *)-1;
     }
+  else
+    dbm_file = dbfn_open(US"retry", O_RDONLY, &dbblock, FALSE, TRUE);
+
+  if (!dbm_file)
+    DEBUG(D_deliver|D_retry|D_route|D_hints_lookup)
+      debug_printf("no retry data available\n");
 
   /* Scan the current batch of new addresses, to handle pipes, files and
   autoreplies, and determine which others are ready for routing. */

commit ea41f8373bd699697585193862afab334a62ec1a
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Nov 5 10:17:04 2025 +0000

    Build: more files for cscope

diff --git a/src/Makefile b/src/Makefile
index 9c08915ef..45711a05b 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -121,10 +121,15 @@ cscope.files: FRC
 	echo "-p3" >> $@
 	-bd=build-$(buildname); [ -d $$bd ] && echo -e "$$bd/config.h\n$$bd/Makefile" >> $@
 	find src Local OS exim_monitor -name "*.[cshyl]" -print \
+		    -o -name "*.src" -print \
 		    -o -name "os.[ch]*" -print \
 		    -o -name "*akefile*" -print \
 		    -o -name config.h.defaults -print \
 		    -o -name EDITME -print >> $@
+	echo scripts/* >> $@
+
+cscope:	cscope.files
+	cscope -b -p 3
 
 FRC:
 

commit d3dd48e449dcd329126c4365c8f92335c8afa350
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Nov 6 10:55:05 2025 +0000

    fix radius expansion condition

diff --git a/src/src/miscmods/pam.c b/src/src/miscmods/pam.c
index 842282ba5..f39e09208 100644
--- a/src/src/miscmods/pam.c
+++ b/src/src/miscmods/pam.c
@@ -68,7 +68,7 @@ static int
 pam_converse (int num_msg, PAM_CONVERSE_ARG2_TYPE **msg,
   struct pam_response **resp, void *appdata_ptr)
 {
-int sep = 0;
+int sep = ':';
 struct pam_response *reply;
 
 /* It seems that PAM frees reply[] */
@@ -131,7 +131,7 @@ Returns:   OK if authentication succeeded
 static int
 auth_call_pam(const uschar * s, uschar ** errptr)
 {
-pam_handle_t *pamh = NULL;
+pam_handle_t * pamh = NULL;
 struct pam_conv pamc;
 int pam_error;
 int sep = ':';	/* Do not permit change-of-separator */
diff --git a/src/src/miscmods/radius.c b/src/src/miscmods/radius.c
index 3f8232756..fd75b3ae4 100644
--- a/src/src/miscmods/radius.c
+++ b/src/src/miscmods/radius.c
@@ -59,8 +59,8 @@ using its original API. At release 0.4.0 the API changed. */
 more data strings.
 
 Arguments:
-  s        a colon-separated list of strings
-  errptr   where to point an error message
+  radius_args	a colon-separated list of strings
+  errptr	where to point an error message
 
 Returns:   OK if authentication succeeded
            FAIL if authentication failed
@@ -68,12 +68,10 @@ Returns:   OK if authentication succeeded
 */
 
 static int
-auth_call_radius(const uschar *s, uschar **errptr)
+auth_call_radius(const uschar * radius_args, uschar ** errptr)
 {
-uschar *user;
-const uschar *radius_args = s;
-int result;
-int sep = ':';
+uschar * user, * pwd;
+int sep = ':', result;
 
 #ifdef RADIUS_LIB_RADLIB
   struct rad_handle *h;
@@ -89,9 +87,10 @@ int sep = ':';
 
 
 if (!(user = string_nextinlist(&radius_args, &sep, NULL, 0))) user = US"";
+pwd = string_nextinlist(&radius_args, &sep, NULL, 0);
 
 DEBUG(D_auth) debug_printf("Running RADIUS authentication for user %q "
-               "and %q\n", user, radius_args);
+               "and %q\n", user, pwd);
 
 *errptr = NULL;
 
@@ -112,7 +111,7 @@ else if (rc_read_dictionary(rc_conf_str("dictionary")) != 0)
 else if (!rc_avpair_add(&send, PW_USER_NAME, user, 0))
   *errptr = US"RADIUS: add user name failed";
 
-else if (!rc_avpair_add(&send, PW_USER_PASSWORD, CS radius_args, 0))
+else if (!rc_avpair_add(&send, PW_USER_PASSWORD, pwd, 0))
   *errptr = US"RADIUS: add password failed");
 
 else if (!rc_avpair_add(&send, PW_SERVICE_TYPE, &service, 0))

commit eaf7eae8474ded19ec64022f71d1b9e610013d5c
Author: Samuel Thibault <samuel.thibault@ens-lyon.org>
Date:   Fri Nov 7 09:37:04 2025 +0000

    Fix file open modes for Gnu/Hurd.  Bug 3175

diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index cd98ff053..5065b6e9b 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -335,7 +335,7 @@ if (  asprintf(CSS &dirname, "%s/db", spool_directory) < 0
    || asprintf(CSS &filename, "%s/%s.lockfile", dirname, name) < 0)
   return NULL;
 
-dbblock->readonly = (flags & (O_WRONLY|O_RDWR)) == O_RDONLY;
+dbblock->readonly = (flags & O_ACCMODE) == O_RDONLY;
 dbblock->lockfd = -1;
 if (exim_lockfile_needed())
   {
diff --git a/src/src/hintsdb/hints_bdb.h b/src/src/hintsdb/hints_bdb.h
index 7285e85f9..85e8984ec 100644
--- a/src/src/hintsdb/hints_bdb.h
+++ b/src/src/hintsdb/hints_bdb.h
@@ -115,7 +115,7 @@ if (db_create(&b, dbp, 0) == 0)
   if (b->open(b, NULL, CS name, NULL,
 	      flags & O_CREAT ? DB_HASH : DB_UNKNOWN,
 	      flags & O_CREAT ? DB_CREATE
-	      : flags & (O_WRONLY|O_RDWR) ? 0 : DB_RDONLY,
+	      : (flags & O_ACCMODE) == O_RDONLY ? DB_RDONLY : 0,
 	      mode) == 0
 	  )
     return dbp;
@@ -266,7 +266,7 @@ return db_create(&dbp, NULL, 0) == 0
 	dbp->open(dbp, CS name, NULL,
 	  flags & O_CREAT ? DB_HASH : DB_UNKNOWN,
 	  flags & O_CREAT ? DB_CREATE
-	  : flags & (O_WRONLY|O_RDWR) ? 0 : DB_RDONLY,
+	  : (flags & O_ACCMODE) == O_RDONLY ? DB_RDONLY : 0,
 	  mode)
      ) == 0
   ? dbp : NULL;
diff --git a/src/src/hintsdb/hints_gdbm.h b/src/src/hintsdb/hints_gdbm.h
index 00c94a777..34df8c84f 100644
--- a/src/src/hintsdb/hints_gdbm.h
+++ b/src/src/hintsdb/hints_gdbm.h
@@ -58,7 +58,7 @@ if (dbp)
   dbp->lkey.dptr = NULL;
   dbp->gdbm = gdbm_open(CS name, 0,
     flags & O_CREAT ? GDBM_WRCREAT
-    : flags & (O_RDWR|O_WRONLY) ? GDBM_WRITER : GDBM_READER,
+    : (flags & O_ACCMODE) == O_RDONLY ? GDBM_READER : GDBM_WRITER,
     mode, 0);
   if (dbp->gdbm)
     return dbp;
diff --git a/src/src/hintsdb/hints_sqlite.h b/src/src/hintsdb/hints_sqlite.h
index 01906f839..40d50b8c0 100644
--- a/src/src/hintsdb/hints_sqlite.h
+++ b/src/src/hintsdb/hints_sqlite.h
@@ -45,7 +45,8 @@ exim_dbopen_multi__(const uschar * name, const uschar * dirname, int flags,
   unsigned mode)
 {
 EXIM_DB * dbp;
-int ret, sflags = flags & O_RDWR ? SQLITE_OPEN_READWRITE : SQLITE_OPEN_READONLY;
+int ret, sflags = (flags & O_ACCMODE) == O_RDONLY
+		  ? SQLITE_OPEN_READONLY : SQLITE_OPEN_READWRITE;
 
 if (flags & O_CREAT) sflags |= SQLITE_OPEN_CREATE;
 if ((ret = sqlite3_open_v2(CCS name, &dbp, sflags, NULL)) == SQLITE_OK)

commit 0cf5f1656eff31bcc1131ca047030f4866b4224b
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Nov 7 17:50:28 2025 +0000

    Retire "pwcheck" expansion condition

diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index e24ef95f2..25a8e82a7 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -552,8 +552,7 @@ OBJ_LOOKUPS = lf_quote.o lf_check_file.o lf_sqlperform.o
 OBJ_ROUTERS = rf_change_domain.o rf_expand_data.o rf_get_errors_address.o \
 	rf_get_munge_headers.o rf_get_transport.o rf_get_ugid.o \
 	rf_lookup_hostlist.o rf_queue_add.o rf_self_action.o rf_set_ugid.o
-OBJ_AUTHS = call_pwcheck.o check_serv_cond.o \
-	get_data.o get_no64_data.o pwcheck.o
+OBJ_AUTHS = check_serv_cond.o get_data.o get_no64_data.o pwcheck.o
 
 OBJ_EXIM = acl.o atrn.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 \
@@ -1003,9 +1002,6 @@ rf_set_ugid.o:           routers/rf_set_ugid.c
 auth-spa.o:         auths/auth-spa.c
 	@echo "$(CC) $<"
 	$(FE)$(CC) -c $(CFLAGS) -I. $(INCLUDE) $<
-call_pwcheck.o:     auths/call_pwcheck.c auths/pwcheck.h
-	@echo "$(CC) $<"
-	$(FE)$(CC) -c $(CFLAGS) -I. $(INCLUDE) $<
 check_serv_cond.o:  auths/check_serv_cond.c
 	@echo "$(CC) $<"
 	$(FE)$(CC) -c $(CFLAGS) -I. $(INCLUDE) $<
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index 8f55be0b5..765472195 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -78,7 +78,7 @@ d="auths"
 mkdir $d
 cd $d
 # Makefile is generated
-for f in README call_pwcheck.c \
+for f in README \
   check_serv_cond.c cyrus_sasl.c cyrus_sasl.h gsasl.c \
   gsasl.h get_data.c get_no64_data.c heimdal_gssapi.c heimdal_gssapi.h \
   cram_md5.c cram_md5.h plaintext.c plaintext.h \
diff --git a/src/src/EDITME b/src/src/EDITME
index 998406359..d38ac98e5 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -1205,23 +1205,6 @@ ZCAT_COMMAND=/usr/bin/zcat
 # using the original API.
 
 
-#------------------------------------------------------------------------------
-# Support for authentication via the Cyrus SASL pwcheck daemon is available.
-# Note, however, that pwcheck is now deprecated in favour of saslauthd (see
-# next item). The Exim support for pwcheck, which is intented for use in
-# conjunction with the SMTP AUTH facilities, is included only when requested by
-# setting the following parameter to the location of the pwcheck daemon's
-# socket.
-#
-# There is no need to install all of SASL on your system. You just need to run
-# ./configure --with-pwcheck, cd to the pwcheck directory within the sources,
-# make and make install. You must create the socket directory (default
-# /var/pwcheck) and chown it to Exim's user and group. Once you have installed
-# pwcheck, you should arrange for it to be started by root at boot time.
-
-# CYRUS_PWCHECK_SOCKET=/var/pwcheck/pwcheck
-
-
 #------------------------------------------------------------------------------
 # Support for authentication via the Cyrus SASL saslauthd daemon is available.
 # The Exim support, which is intended for use in conjunction with the SMTP AUTH
diff --git a/src/src/auths/call_pwcheck.c b/src/src/auths/call_pwcheck.c
deleted file mode 100644
index 9e0d5d41f..000000000
--- a/src/src/auths/call_pwcheck.c
+++ /dev/null
@@ -1,120 +0,0 @@
-/*************************************************
-*     Exim - an Internet mail transport agent    *
-*************************************************/
-
-/* Copyright (c) The Exim Maintainers 2020 - 2025 */
-/* Copyright (c) University of Cambridge 1995 - 2015 */
-/* See the file NOTICE for conditions of use and distribution. */
-/* 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
-file. This is now deprecated in favour of "saslauthd". */
-
-
-#include "../exim.h"
-#include "pwcheck.h"
-
-
-/*************************************************
-*      External entry point for pwcheck          *
-*************************************************/
-
-/* This function calls the now-deprecated "pwcheck" Cyrus-SASL authentication
-daemon, passing over a colon-separated user name and password. As this is
-called from the string expander, the string will always be in dynamic store and
-can be overwritten.
-
-Arguments:
-  s        a colon-separated username:password string
-  errptr   where to point an error message
-
-Returns:   OK if authentication succeeded
-           FAIL if authentication failed
-           ERROR some other error condition
-*/
-
-int
-auth_call_pwcheck(uschar *s, uschar **errptr)
-{
-uschar * reply = NULL, * pw = Ustrrchr(s, ':');
-
-if (!pw)
-  {
-  *errptr = US"pwcheck: malformed input - missing colon";
-  return ERROR;
-  }
-
-*pw++ = 0;   /* Separate user and password */
-
-DEBUG(D_auth) debug_printf("Running pwcheck authentication for user %q\n", s);
-
-switch (pwcheck_verify_password(CS s, CS pw, CCSS &reply))
-  {
-  case PWCHECK_OK:
-  DEBUG(D_auth) debug_printf("pwcheck: success (%s)\n", reply);
-  return OK;
-
-  case PWCHECK_NO:
-  DEBUG(D_auth) debug_printf("pwcheck: access denied (%s)\n", reply);
-  return FAIL;
-
-  default:
-  DEBUG(D_auth) debug_printf("pwcheck: query failed (%s)\n", reply);
-  *errptr = reply;
-  return ERROR;
-  }
-}
-
-
-/*************************************************
-*       External entry point for pwauthd         *
-*************************************************/
-
-/* This function calls the "saslauthd" Cyrus-SASL authentication daemon,
-saslauthd, As this is called from the string expander, all the strings will
-always be in dynamic store and can be overwritten.
-
-Arguments:
-  username        username
-  password        password
-  service         optional service
-  realm           optional realm
-  errptr          where to point an error message
-
-Returns:   OK if authentication succeeded
-           FAIL if authentication failed
-           ERROR some other error condition
-*/
-
-int
-auth_call_saslauthd(const uschar *username, const uschar *password,
-  const uschar *service, const uschar *realm, uschar **errptr)
-{
-uschar *reply = NULL;
-
-if (service == NULL) service = US"";
-if (realm == NULL) realm = US"";
-
-DEBUG(D_auth)
-  debug_printf("Running saslauthd authentication for user %q \n", username);
-
-switch (saslauthd_verify_password(username, password, service,
-        realm, (const uschar **)(&reply)))
-  {
-  case PWCHECK_OK:
-  DEBUG(D_auth) debug_printf("saslauthd: success (%s)\n", reply);
-  return OK;
-
-  case PWCHECK_NO:
-  DEBUG(D_auth) debug_printf("saslauthd: access denied (%s)\n", reply);
-  return FAIL;
-
-  default:
-  DEBUG(D_auth) debug_printf("saslauthd: query failed (%s)\n", reply);
-  *errptr = reply;
-  return ERROR;
-  }
-}
-
-/* End of call_pwcheck.c */
diff --git a/src/src/auths/pwcheck.c b/src/src/auths/pwcheck.c
index bf305832f..cd8ed1e10 100644
--- a/src/src/auths/pwcheck.c
+++ b/src/src/auths/pwcheck.c
@@ -59,16 +59,16 @@
  */
 
 /* Originally this module supported only the pwcheck daemon, which is where its
-name comes from. Nowadays it supports saslauthd as well; pwcheck is in fact
-deprecated. The definitions of CYRUS_PWCHECK_SOCKET and CYRUS_SASLAUTHD_SOCKET
-determine whether the facilities are actually supported or not. */
+name comes from. Nowadays it supports saslauthd instead; pwcheck is in fact
+deprecated. The definition of CYRUS_SASLAUTHD_SOCKET
+determines whether the facilities are actually supported or not. */
 
 
 #include "../exim.h"
 #include "pwcheck.h"
 
 
-#if defined(CYRUS_PWCHECK_SOCKET) || defined(CYRUS_SASLAUTHD_SOCKET)
+#if defined(CYRUS_SASLAUTHD_SOCKET)
 
 #include <sys/uio.h>
 
@@ -80,79 +80,6 @@ static int write_string(int, const uschar *, int);
 #endif
 
 
-/* A dummy function that always fails if pwcheck support is not
-wanted. */
-
-#ifndef CYRUS_PWCHECK_SOCKET
-int pwcheck_verify_password(const char *userid,
-                            const char *passwd,
-                            const char **reply)
-{
-*reply = "pwcheck support is not included in this Exim binary";
-return PWCHECK_FAIL;
-}
-
-
-/* This is the real function */
-
-#else
-
- /* taken from cyrus-sasl file checkpw.c */
- /* pwcheck daemon-authenticated login */
- int pwcheck_verify_password(const char *userid,
-                                  const char *passwd,
-                                  const char **reply)
- {
-     int s, start, r, n;
-     struct sockaddr_un srvaddr;
-     struct iovec iov[2];
-     static char response[1024];
-
-     *reply = NULL;
-
-     s = socket(AF_UNIX, SOCK_STREAM, 0);
-     if (s == -1) { return PWCHECK_FAIL; }
-
-     memset(CS &srvaddr, 0, sizeof(srvaddr));
-     srvaddr.sun_family = AF_UNIX;
-     strncpy(srvaddr.sun_path, CYRUS_PWCHECK_SOCKET, sizeof(srvaddr.sun_path));
-     r = connect(s, (struct sockaddr *)&srvaddr, sizeof(srvaddr));
-     if (r == -1) {
-        DEBUG(D_auth)
-            debug_printf("Cannot connect to pwcheck daemon (at '%s')\n",CYRUS_PWCHECK_SOCKET);
-       *reply = "cannot connect to pwcheck daemon";
-       return PWCHECK_FAIL;
-     }
-
-     iov[0].iov_base = CS userid;
-     iov[0].iov_len = strlen(userid)+1;
-     iov[1].iov_base = CS passwd;
-     iov[1].iov_len = strlen(passwd)+1;
-
-     retry_writev(s, iov, 2);
-
-     start = 0;
-     while (start < sizeof(response) - 1) {
-       n = read(s, response+start, sizeof(response) - 1 - start);
-       if (n < 1) break;
-       start += n;
-     }
-
-     (void)close(s);
-
-     if (start > 1 && !strncmp(response, "OK", 2)) {
-       return PWCHECK_OK;
-     }
-
-     response[start] = '\0';
-     *reply = response;
-     return PWCHECK_NO;
- }
-
-#endif
-
-
-
  /* A dummy function that always fails if saslauthd support is not
 wanted. */
 
@@ -268,7 +195,7 @@ int saslauthd_verify_password(const uschar *userid,
 
 
 /* helper functions */
-#if defined(CYRUS_PWCHECK_SOCKET) || defined(CYRUS_SASLAUTHD_SOCKET)
+#if defined(CYRUS_SASLAUTHD_SOCKET)
 
 #define MAX_REQ_LEN 1024
 
diff --git a/src/src/auths/pwcheck.h b/src/src/auths/pwcheck.h
index 4c1d71d92..0c20292ce 100644
--- a/src/src/auths/pwcheck.h
+++ b/src/src/auths/pwcheck.h
@@ -6,8 +6,8 @@
 /* See the file NOTICE for conditions of use and distribution. */
 /* 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. */
+/* This file originally provided support for authentication via the Cyrus
+SASL pwcheck daemon (whence its name), but now the newer saslauthd daemon. */
 
 /* Error codes used internally within the authentication functions */
 
@@ -21,7 +21,6 @@ daemon (whence its name) and the newer saslauthd daemon. */
 
 /* Cyrus functions for doing the business. */
 
-extern int pwcheck_verify_password(const char *, const char *, const char **);
 extern int saslauthd_verify_password(const uschar *, const uschar *,
            const uschar *, const uschar *, const uschar **);
 
diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults
index 05bdcd6d3..f331ad0d5 100644
--- a/src/src/config.h.defaults
+++ b/src/src/config.h.defaults
@@ -40,7 +40,6 @@ Do not put spaces between # and the 'define'.
 #define CONFIGURE_FILE_USE_NODE
 #define CONFIGURE_GROUP
 #define CONFIGURE_OWNER
-#define CYRUS_PWCHECK_SOCKET
 #define CYRUS_SASLAUTHD_SOCKET
 
 #define DEFAULT_CRYPT              crypt
diff --git a/src/src/exim.c b/src/src/exim.c
index c6fa25fc7..9d6f858bd 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1158,9 +1158,6 @@ g = string_cat(g, US"Support for:");
 #ifdef USE_OPENSSL
   g = string_cat(g, US" OpenSSL");
 #endif
-#if defined(CYRUS_PWCHECK_SOCKET)
-  g = string_cat(g, US" pwcheck");
-#endif
 #if defined(RADIUS_CONFIG_FILE)
   g = string_cat(g, US" radius");
 #endif
diff --git a/src/src/expand.c b/src/src/expand.c
index 9bfa39fdc..fb32ce985 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -362,7 +362,6 @@ static uschar *cond_table[] = {
   US"match_local_part",
   US"or",
   US"pam",
-  US"pwcheck",
   US"queue_running",
   US"radius",
   US"saslauthd"
@@ -415,7 +414,6 @@ enum {
   ECOND_MATCH_LOCAL_PART,
   ECOND_OR,
   ECOND_PAM,
-  ECOND_PWCHECK,
   ECOND_QUEUE_RUNNING,
   ECOND_RADIUS,
   ECOND_SASLAUTHD
@@ -2740,7 +2738,6 @@ switch(cond_type = identify_operator(&s, &opname))
   case ECOND_PAM:
   case ECOND_RADIUS:
   case ECOND_LDAPAUTH:
-  case ECOND_PWCHECK:
 
   if (Uskip_whitespace(&s) != '{') goto COND_FAILED_CURLY_START; /* }-for-text-editors */
 
@@ -2835,16 +2832,8 @@ switch(cond_type = identify_operator(&s, &opname))
     goto COND_FAILED_NOT_COMPILED;
     #endif  /* LOOKUP_LDAP */
 
-    case ECOND_PWCHECK:
-    #ifdef CYRUS_PWCHECK_SOCKET
-    rc = auth_call_pwcheck(sub[0], &expand_string_message);
-    goto END_AUTH;
-    #else
-    goto COND_FAILED_NOT_COMPILED;
-    #endif  /* CYRUS_PWCHECK_SOCKET */
-
     #if defined(SUPPORT_PAM) || defined(RADIUS_CONFIG_FILE) || \
-        defined(LOOKUP_LDAP) || defined(CYRUS_PWCHECK_SOCKET)
+        defined(LOOKUP_LDAP)
     END_AUTH:
     if (rc == ERROR || rc == DEFER) goto failout;
     *yield = (rc == OK) == testfor;
@@ -3724,7 +3713,7 @@ goto failout;
 /* A condition requires code that is not compiled */
 
 #if !defined(SUPPORT_PAM) || !defined(RADIUS_CONFIG_FILE) || \
-    !defined(LOOKUP_LDAP) || !defined(CYRUS_PWCHECK_SOCKET) || \
+    !defined(LOOKUP_LDAP) || \
     !defined(SUPPORT_CRYPTEQ) || !defined(CYRUS_SASLAUTHD_SOCKET)
 COND_FAILED_NOT_COMPILED:
 expand_string_message = string_sprintf("support for %q not compiled",
diff --git a/src/src/functions.h b/src/src/functions.h
index ef1c59dc3..bd390cce6 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -112,7 +112,6 @@ extern void    assert_no_variables(void *, int, const char *, int);
 extern void    atrn_handle_customer(void);
 extern int     atrn_handle_provider(uschar **, uschar **);
 
-extern int     auth_call_pwcheck(uschar *, uschar **);
 extern int     auth_call_saslauthd(const uschar *, const uschar *,
 	         const uschar *, const uschar *, uschar **);
 extern int     auth_check_serv_cond(auth_instance *);

commit 3bac7a1bac4fbccea548b3f8ed9a788b8b41d943
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Nov 10 10:09:49 2025 +0000

    Fix CYRUS_SASLAUTHD_SOCKET build.  Bug 3176
    
    Broken-by: 0cf5f1656eff

diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index 25a8e82a7..40befe325 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -552,7 +552,8 @@ OBJ_LOOKUPS = lf_quote.o lf_check_file.o lf_sqlperform.o
 OBJ_ROUTERS = rf_change_domain.o rf_expand_data.o rf_get_errors_address.o \
 	rf_get_munge_headers.o rf_get_transport.o rf_get_ugid.o \
 	rf_lookup_hostlist.o rf_queue_add.o rf_self_action.o rf_set_ugid.o
-OBJ_AUTHS = check_serv_cond.o get_data.o get_no64_data.o pwcheck.o
+OBJ_AUTHS = call_saslauthd.o check_serv_cond.o \
+	get_data.o get_no64_data.o pwcheck.o
 
 OBJ_EXIM = acl.o atrn.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 \
@@ -1002,6 +1003,9 @@ rf_set_ugid.o:           routers/rf_set_ugid.c
 auth-spa.o:         auths/auth-spa.c
 	@echo "$(CC) $<"
 	$(FE)$(CC) -c $(CFLAGS) -I. $(INCLUDE) $<
+call_saslauthd.o:  auths/call_saslauthd.c auths/pwcheck.h
+	@echo "$(CC) $<"
+	$(FE)$(CC) -c $(CFLAGS) -I. $(INCLUDE) $<
 check_serv_cond.o:  auths/check_serv_cond.c
 	@echo "$(CC) $<"
 	$(FE)$(CC) -c $(CFLAGS) -I. $(INCLUDE) $<
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index 765472195..4e4a638c7 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -78,7 +78,7 @@ d="auths"
 mkdir $d
 cd $d
 # Makefile is generated
-for f in README \
+for f in README call_saslauthd.c \
   check_serv_cond.c cyrus_sasl.c cyrus_sasl.h gsasl.c \
   gsasl.h get_data.c get_no64_data.c heimdal_gssapi.c heimdal_gssapi.h \
   cram_md5.c cram_md5.h plaintext.c plaintext.h \
diff --git a/src/src/auths/call_saslauthd.c b/src/src/auths/call_saslauthd.c
new file mode 100644
index 000000000..b5735fc6c
--- /dev/null
+++ b/src/src/auths/call_saslauthd.c
@@ -0,0 +1,69 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2025 */
+/* Copyright (c) University of Cambridge 1995 - 2015 */
+/* See the file NOTICE for conditions of use and distribution. */
+/* 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
+file. This is now deprecated in favour of "saslauthd". */
+
+
+#include "../exim.h"
+#include "pwcheck.h"
+
+
+/*************************************************
+*       External entry point for saslauthd       *
+*************************************************/
+
+/* This function calls the "saslauthd" Cyrus-SASL authentication daemon,
+saslauthd, As this is called from the string expander, all the strings will
+always be in dynamic store and can be overwritten.
+
+Arguments:
+  username        username
+  password        password
+  service         optional service
+  realm           optional realm
+  errptr          where to point an error message
+
+Returns:   OK if authentication succeeded
+           FAIL if authentication failed
+           ERROR some other error condition
+*/
+
+int
+auth_call_saslauthd(const uschar *username, const uschar *password,
+  const uschar *service, const uschar *realm, uschar **errptr)
+{
+uschar *reply = NULL;
+
+if (service == NULL) service = US"";
+if (realm == NULL) realm = US"";
+
+DEBUG(D_auth)
+  debug_printf("Running saslauthd authentication for user %q \n", username);
+
+switch (saslauthd_verify_password(username, password, service,
+        realm, (const uschar **)(&reply)))
+  {
+  case PWCHECK_OK:
+  DEBUG(D_auth) debug_printf("saslauthd: success (%s)\n", reply);
+  return OK;
+
+  case PWCHECK_NO:
+  DEBUG(D_auth) debug_printf("saslauthd: access denied (%s)\n", reply);
+  return FAIL;
+
+  default:
+  DEBUG(D_auth) debug_printf("saslauthd: query failed (%s)\n", reply);
+  *errptr = reply;
+  return ERROR;
+  }
+}
+
+/* End of call_pwcheck.c */
diff --git a/src/src/expand.c b/src/src/expand.c
index fb32ce985..fcf699d2e 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -3712,14 +3712,10 @@ goto failout;
 
 /* A condition requires code that is not compiled */
 
-#if !defined(SUPPORT_PAM) || !defined(RADIUS_CONFIG_FILE) || \
-    !defined(LOOKUP_LDAP) || \
-    !defined(SUPPORT_CRYPTEQ) || !defined(CYRUS_SASLAUTHD_SOCKET)
 COND_FAILED_NOT_COMPILED:
 expand_string_message = string_sprintf("support for %q not compiled",
   opname);
 goto failout;
-#endif
 
 failout:
   next = NULL;

commit 1cb819c5032dc7797999c60c023f65c842dcf5d4
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 9 12:15:07 2025 +0000

    Build: quietening

diff --git a/src/Makefile b/src/Makefile
index 45711a05b..a19c6928a 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -29,6 +29,9 @@ RM_COMMAND=/bin/rm
 
 buildname=$${build:-`$(SHELL) scripts/os-type`-`$(SHELL) scripts/arch-type`}$${EXIM_BUILD_SUFFIX:+.$$EXIM_BUILD_SUFFIX}
 
+# Quieten gnumake wrt. sub-make directories
+GNUMAKEFLAGS=--no-print-directory
+
 # The default target checks for the existence of Local/Makefile, that the main
 # makefile is built and up-to-date, and then it runs it.
 # If Local/Makefile-<buildname> exists, it is read too.
@@ -63,12 +66,14 @@ build-directory:
 	  (mkdir $$builddir; cd $$builddir; $(SHELL) ../scripts/MakeLinks)";
 
 checks:
-	$(SHELL) scripts/source_checks
+	@$(SHELL) scripts/source_checks
 
 # The "configure" target ensures that the build directory exists, then arranges
 # to build the main makefile from inside the build directory, by calling the
 # Configure-Makefile script. This does its own dependency checking because of
 # the optional files.
+# We always run the script (as there is no actual"configure" file)
+# but it quietly dooes nothing if nothing is needed.
 
 configure: checks build-directory \
 		scripts/lookups-Makefile scripts/drivers-Makefile
diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index 40befe325..80f1f1f97 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -36,8 +36,9 @@ FE       = $(FULLECHO)
 # up-to-date. Then the os-specific source files and the C configuration file
 # are set up, and finally it goes to the main Exim target.
 
-all:       utils exim dynmodules
-config:    $(EDITME) checklocalmake Makefile os.c config.h version.h version.sh macro.c
+CONFIG_DEPS = Makefile os.c config.h version.h version.sh macro.c
+
+all:       $(EDITME) checklocalmake $(CONFIG_DEPS) utils exim dynmodules
 
 exim_openssl exim_gnutls: clean exim
 	cp exim $@
@@ -243,9 +244,7 @@ macro.c: macro_predef
 # therefore always be run, even if the files exist. This shouldn't in fact be a
 # problem, but it does no harm. Other make programs will just ignore this.
 
-.PHONY: all config utils \
-	buildauths buildlookups buildrouters \
-        buildtransports buildmisc dynmodules checklocalmake clean
+.PHONY: all config utils dynmodules clean
 
 
 utils: $(EXIM_MONITOR) exicyclog exinext exiwhat \
@@ -261,12 +260,12 @@ buildconfig: buildconfig.c
 	$(FE)$(CC) $(CFLAGS) $(INCLUDE) -o buildconfig buildconfig.c $(LIBS)
 
 
-util_deps: config ../src/utils/msgid.frag
+UTIL_DEPS = $(CONFIG_DEPS) ../src/utils/msgid.frag
 
 # Target for the exicyclog utility script
-exicyclog: util_deps ../src/utils/exicyclog.src
-	@rm -f exicyclog
-	@. ./version.sh && sed \
+exicyclog: $(UTIL_DEPS) ../src/utils/exicyclog.src
+	$(FE)rm -f exicyclog
+	$(FE). ./version.sh && sed \
 	  -e "s?PROCESSED_FLAG?This file has been so processed.?"\
 	  -e "/^# /p" \
 	  -e "/^# /d" \
@@ -292,15 +291,15 @@ exicyclog: util_deps ../src/utils/exicyclog.src
 	  -e "s?^?# Insert ?" \
 	  -e "r ../src/utils/msgid.frag" \
 	  ../src/utils/exicyclog.src > exicyclog-t
-	@mv exicyclog-t exicyclog
-	@chmod a+x exicyclog
-	@./exicyclog -v 2>&1 >/dev/null
-	@echo ">>> exicyclog script built"
+	$(FE)mv exicyclog-t exicyclog
+	$(FE)chmod a+x exicyclog
+	$(FE)./exicyclog -v 2>&1 >/dev/null
+#	$(FE)echo ">>> exicyclog script built"
 
 # Target for the exinext utility script
-exinext: util_deps ../src/utils/exinext.src
-	@rm -f exinext
-	@. ./version.sh && sed \
+exinext: $(UTIL_DEPS) ../src/utils/exinext.src
+	$(FE)rm -f exinext
+	$(FE). ./version.sh && sed \
 	  -e "s?PROCESSED_FLAG?This file has been so processed.?"\
 	  -e "/^# /p" \
 	  -e "/^# /d" \
@@ -316,15 +315,15 @@ exinext: util_deps ../src/utils/exinext.src
 	  -e "s?^?# Insert ?" \
 	  -e "r ../src/utils/msgid.frag" \
 	  ../src/utils/exinext.src > exinext-t
-	@mv exinext-t exinext
-	@chmod a+x exinext
-	@./exinext -v 2>&1 >/dev/null
-	@echo ">>> exinext script built"
+	$(FE)mv exinext-t exinext
+	$(FE)chmod a+x exinext
+	$(FE)./exinext -v 2>&1 >/dev/null
+#	$(FE)echo ">>> exinext script built"
 
 # Target for the exiwhat utility script
-exiwhat: config ../src/utils/exiwhat.src
-	@rm -f exiwhat
-	@. ./version.sh && sed \
+exiwhat: $(CONFIG_DEPS) ../src/utils/exiwhat.src
+	$(FE)rm -f exiwhat
+	$(FE). ./version.sh && sed \
 	  -e "s?PROCESSED_FLAG?This file has been so processed.?"\
 	  -e "/^# /p" \
 	  -e "/^# /d" \
@@ -341,15 +340,15 @@ exiwhat: config ../src/utils/exiwhat.src
 	  -e "s?EXIM_VARIANT_VERSION?$${EXIM_VARIANT_VERSION}?" \
 	  -e "s?RM_COMMAND?$(RM_COMMAND)?" \
 	  ../src/utils/exiwhat.src > exiwhat-t
-	@mv exiwhat-t exiwhat
-	@chmod a+x exiwhat
-	@./exiwhat -v 2>&1 >/dev/null
-	@echo ">>> exiwhat script built"
+	$(FE)mv exiwhat-t exiwhat
+	$(FE)chmod a+x exiwhat
+	$(FE)./exiwhat -v 2>&1 >/dev/null
+#	$(FE)echo ">>> exiwhat script built"
 
 # Target for the exim_checkaccess utility script
-exim_checkaccess: config ../src/utils/exim_checkaccess.src
-	@rm -f exim_checkaccess
-	@. ./version.sh && sed \
+exim_checkaccess: $(CONFIG_DEPS) ../src/utils/exim_checkaccess.src
+	$(FE)rm -f exim_checkaccess
+	$(FE). ./version.sh && sed \
 	  -e "s?PROCESSED_FLAG?This file has been so processed.?"\
 	  -e "/^# /p" \
 	  -e "/^# /d" \
@@ -360,17 +359,17 @@ exim_checkaccess: config ../src/utils/exim_checkaccess.src
 	  -e "s?EXIM_RELEASE_VERSION?$${EXIM_RELEASE_VERSION}?" \
 	  -e "s?EXIM_VARIANT_VERSION?$${EXIM_VARIANT_VERSION}?" \
 	  ../src/utils/exim_checkaccess.src > exim_checkaccess-t
-	@mv exim_checkaccess-t exim_checkaccess
-	@chmod a+x exim_checkaccess
-	# @./exim_checkaccess -v 2>&1 >/dev/null
-	@echo ">>> exim_checkaccess script built"; echo ""
+	$(FE)mv exim_checkaccess-t exim_checkaccess
+	$(FE)chmod a+x exim_checkaccess
+#	@./exim_checkaccess -v 2>&1 >/dev/null
+#	$(FE)echo ">>> exim_checkaccess script built"; echo ""
 
 # Target for the Exim monitor start-up script
-eximon: config ../src/utils/eximon.src ../OS/eximon.conf-Default \
+eximon: $(CONFIG_DEPS) ../src/utils/eximon.src ../OS/eximon.conf-Default \
           ../Local/eximon.conf
-	@rm -f eximon
+	$(FE)rm -f eximon
 	$(SHELL) $(SCRIPTS)/Configure-eximon
-	@. ./version.sh && sed \
+	$(FE). ./version.sh && sed \
 	  -e "s?PROCESSED_FLAG?This file has been so processed.?"\
 	  -e "/^# /p" \
 	  -e "/^# /d" \
@@ -383,15 +382,15 @@ eximon: config ../src/utils/eximon.src ../OS/eximon.conf-Default \
 	  -e "s?EXIM_RELEASE_VERSION?$${EXIM_RELEASE_VERSION}?" \
 	  -e "s?EXIM_VARIANT_VERSION?$${EXIM_VARIANT_VERSION}?" \
 	  ../src/utils/eximon.src >> eximon
-	@./eximon -v 2>&1 >/dev/null
-	@echo ">>> eximon script built"; echo ""
+	$(FE)./eximon -v 2>&1 >/dev/null
+#	$(FE)echo ">>> eximon script built"; echo ""
 
 # Targets for utilities; these are all Perl scripts that have to get the
 # location of Perl put in them. A few need other things as well.
 
-exigrep: util_deps ../src/utils/exigrep.src
-	@rm -f exigrep
-	@. ./version.sh && sed \
+exigrep: $(UTIL_DEPS) ../src/utils/exigrep.src
+	$(FE)rm -f exigrep
+	$(FE). ./version.sh && sed \
 	  -e "s?PROCESSED_FLAG?This file has been so processed.?"\
 	  -e "/^# /p" \
 	  -e "/^# /d" \
@@ -407,14 +406,14 @@ exigrep: util_deps ../src/utils/exigrep.src
 	  -e "s?^?# Insert ?" \
 	  -e "r ../src/utils/msgid.frag" \
 	  ../src/utils/exigrep.src > exigrep-t
-	@mv exigrep-t exigrep
-	@chmod a+x exigrep
-	@./exigrep --version 2>&1 >/dev/null
-	@echo ">>> exigrep script built"
-
-exim_msgdate: util_deps ../src/utils/exim_msgdate.src
-	@rm -f exim_msgdate
-	@. ./version.sh && sed \
+	$(FE)mv exigrep-t exigrep
+	$(FE)chmod a+x exigrep
+	$(FE)./exigrep --version 2>&1 >/dev/null
+#	$(FE)echo ">>> exigrep script built"
+
+exim_msgdate: $(UTIL_DEPS) ../src/utils/exim_msgdate.src
+	$(FE)rm -f exim_msgdate
+	$(FE). ./version.sh && sed \
 	  -e "s?PROCESSED_FLAG?This file has been so processed.?"\
 	  -e "/^[ \t]*# /p" \
 	  -e "/^[ \t]*# /d" \
@@ -431,26 +430,26 @@ exim_msgdate: util_deps ../src/utils/exim_msgdate.src
 	  -e "s?^?# Insert ?" \
 	  -e "r ../src/utils/msgid.frag" \
 	  ../src/utils/exim_msgdate.src > exim_msgdate-t
-	@mv exim_msgdate-t exim_msgdate
-	@chmod a+x exim_msgdate
-	@./exim_msgdate -v 2>&1 >/dev/null
-	@echo ">>> exim_msgdate script built"
-
-eximstats: config ../src/utils/eximstats.src
-	@rm -f eximstats
-	@. ./version.sh && sed \
+	$(FE)mv exim_msgdate-t exim_msgdate
+	$(FE)chmod a+x exim_msgdate
+	$(FE)./exim_msgdate -v 2>&1 >/dev/null
+#	$(FE)echo ">>> exim_msgdate script built"
+
+eximstats: $(CONFIG_DEPS) ../src/utils/eximstats.src
+	$(FE)rm -f eximstats
+	$(FE). ./version.sh && sed \
 	  -e "s?PERL_COMMAND?$(PERL_COMMAND)?" \
 	  -e "s?EXIM_RELEASE_VERSION?$${EXIM_RELEASE_VERSION}?" \
 	  -e "s?EXIM_VARIANT_VERSION?$${EXIM_VARIANT_VERSION}?" \
 	  ../src/utils/eximstats.src > eximstats-t
-	@mv eximstats-t eximstats
-	@chmod a+x eximstats
-	@./eximstats -v 2>&1 >/dev/null
-	@echo ">>> eximstats script built"
-
-exiqgrep: util_deps ../src/utils/exiqgrep.src
-	@rm -f exiqgrep
-	@. ./version.sh && sed \
+	$(FE)mv eximstats-t eximstats
+	$(FE)chmod a+x eximstats
+	$(FE)./eximstats -v 2>&1 >/dev/null
+#	$(FE)echo ">>> eximstats script built"
+
+exiqgrep: $(UTIL_DEPS) ../src/utils/exiqgrep.src
+	$(FE)rm -f exiqgrep
+	$(FE). ./version.sh && sed \
 	  -e "s?PROCESSED_FLAG?This file has been so processed.?"\
 	  -e "/^# /p" \
 	  -e "/^# /d" \
@@ -465,14 +464,14 @@ exiqgrep: util_deps ../src/utils/exiqgrep.src
 	  -e "s?^?# Insert ?" \
 	  -e "r ../src/utils/msgid.frag" \
 	  ../src/utils/exiqgrep.src > exiqgrep-t
-	@mv exiqgrep-t exiqgrep
-	@chmod a+x exiqgrep
-	@./exiqgrep -v 2>&1 >/dev/null
-	@echo ">>> exiqgrep script built"
-
-exiqsumm: util_deps ../src/utils/exiqsumm.src
-	@rm -f exiqsumm
-	@. ./version.sh && sed \
+	$(FE)mv exiqgrep-t exiqgrep
+	$(FE)chmod a+x exiqgrep
+	$(FE)./exiqgrep -v 2>&1 >/dev/null
+#	$(FE)echo ">>> exiqgrep script built"
+
+exiqsumm: $(UTIL_DEPS) ../src/utils/exiqsumm.src
+	$(FE)rm -f exiqsumm
+	$(FE). ./version.sh && sed \
 	  -e "/^MSGID_RE/b mi" \
 	  -e "s?PERL_COMMAND?$(PERL_COMMAND)?" \
 	  -e "s?EXIM_RELEASE_VERSION?$${EXIM_RELEASE_VERSION}?" \
@@ -483,28 +482,28 @@ exiqsumm: util_deps ../src/utils/exiqsumm.src
 	  -e "s?^?# Insert ?" \
 	  -e "r ../src/utils/msgid.frag" \
 	  ../src/utils/exiqsumm.src > exiqsumm-t
-	@mv exiqsumm-t exiqsumm
-	@chmod a+x exiqsumm
-	@./exiqsumm -v 2>&1 >/dev/null
-	@echo ">>> exiqsumm script built"
-
-exipick: config ../src/utils/exipick.src
-	@rm -f exipick
-	@. ./version.sh && sed \
+	$(FE)mv exiqsumm-t exiqsumm
+	$(FE)chmod a+x exiqsumm
+	$(FE)./exiqsumm -v 2>&1 >/dev/null
+#	$(FE)echo ">>> exiqsumm script built"
+
+exipick: $(CONFIG_DEPS) ../src/utils/exipick.src
+	$(FE)rm -f exipick
+	$(FE). ./version.sh && sed \
 	  -e "s?PERL_COMMAND?$(PERL_COMMAND)?" \
 	  -e "s?SPOOL_DIRECTORY?$(SPOOL_DIRECTORY)?" \
 	  -e "s?BIN_DIRECTORY?$(BIN_DIRECTORY)?" \
 	  -e "s?EXIM_RELEASE_VERSION?$${EXIM_RELEASE_VERSION}?" \
 	  -e "s?EXIM_VARIANT_VERSION?$${EXIM_VARIANT_VERSION}?" \
 	  ../src/utils/exipick.src > exipick-t
-	@mv exipick-t exipick
-	@chmod a+x exipick
-	@./exipick -v 2>&1 >/dev/null
-	@echo ">>> exipick script built"
-
-exim_id_update: util_deps ../src/utils/exim_id_update.src
-	@rm -f exim_id_update
-	@. ./version.sh && sed \
+	$(FE)mv exipick-t exipick
+	$(FE)chmod a+x exipick
+	$(FE)./exipick -v 2>&1 >/dev/null
+#	$(FE)echo ">>> exipick script built"
+
+exim_id_update: $(UTIL_DEPS) ../src/utils/exim_id_update.src
+	$(FE)rm -f exim_id_update
+	$(FE). ./version.sh && sed \
 	  -e "/^MSGID_RE/b mi" \
 	  -e "s?PERL_COMMAND?$(PERL_COMMAND)?" \
 	  -e "s?SPOOL_DIRECTORY?$(SPOOL_DIRECTORY)?" \
@@ -517,21 +516,21 @@ exim_id_update: util_deps ../src/utils/exim_id_update.src
 	  -e "s?^?# Insert ?" \
 	  -e "r ../src/utils/msgid.frag" \
 	  ../src/utils/exim_id_update.src > exim_id_update-t
-	@mv exim_id_update-t exim_id_update
-	@chmod a+x exim_id_update
-	@./exim_id_update -v 2>&1 >/dev/null
-	@echo ">>> exim_id_update script built"
-
-transport-filter.pl: config ../src/transport-filter.src
-	@rm -f transport-filter.pl
-	@. ./version.sh && sed \
+	$(FE)mv exim_id_update-t exim_id_update
+	$(FE)chmod a+x exim_id_update
+	$(FE)./exim_id_update -v 2>&1 >/dev/null
+#	$(FE)echo ">>> exim_id_update script built"
+
+transport-filter.pl: $(CONFIG_DEPS) ../src/transport-filter.src
+	$(FE)rm -f transport-filter.pl
+	$(FE). ./version.sh && sed \
 	  -e "s?PERL_COMMAND?$(PERL_COMMAND)?" \
 	  -e "s?EXIM_RELEASE_VERSION?$${EXIM_RELEASE_VERSION}?" \
 	  -e "s?EXIM_VARIANT_VERSION?$${EXIM_VARIANT_VERSION}?" \
 	  ../src/transport-filter.src > transport-filter.pl-t
-	@mv transport-filter.pl-t transport-filter.pl
-	@chmod a+x transport-filter.pl
-	@echo ">>> transport-filter.pl script built"
+	$(FE)mv transport-filter.pl-t transport-filter.pl
+	$(FE)chmod a+x transport-filter.pl
+#	$(FE)echo ">>> transport-filter.pl script built"
 
 
 # These are objects of optional features. They are always compiled, but
@@ -568,9 +567,8 @@ OBJ_EXIM = acl.o atrn.o base64.o child.o crypt16.o daemon.o dbfn.o debug.o \
         local_scan.o $(OBJ_WITH_CONTENT_SCAN) \
         $(OBJ_EXPERIMENTAL)
 
-exim:   buildlookups buildauths \
-        buildrouters buildtransports buildmisc \
-        $(OBJ_EXIM) version.o
+exim:   buildlookups buildauths buildrouters buildtransports buildmisc \
+        	$(OBJ_EXIM) version.o
 	@echo "$(LNCC) -o exim"
 	$(FE)$(PURIFY) $(LNCC) -o exim $(LFLAGS) $(OBJ_EXIM) version.o \
 	  routers/routers.a transports/transports.a lookups/lookups.a \
@@ -583,9 +581,9 @@ exim:   buildlookups buildauths \
 	  $(STRIP_COMMAND) exim; \
 	fi
 	$(EXIM_CHMOD)
-	@echo " "
-	@echo ">>> exim binary built"
-	@echo " "
+#	@echo " "
+#	@echo ">>> exim binary built"
+#	@echo " "
 
 # The utility for dumping the contents of an exim database
 
@@ -599,8 +597,8 @@ exim_dumpdb: $(OBJ_DUMPDB)
 	  echo $(STRIP_COMMAND) exim_dumpdb; \
 	  $(STRIP_COMMAND) exim_dumpdb; \
 	fi
-	@echo ">>> exim_dumpdb utility built"
-	@echo " "
+#	@echo ">>> exim_dumpdb utility built"
+#	@echo " "
 
 # The utility for interrogating/fixing the contents of an exim database
 
@@ -614,8 +612,8 @@ exim_fixdb:  $(OBJ_FIXDB)
 	  echo $(STRIP_COMMAND) exim_fixdb; \
 	  $(STRIP_COMMAND) exim_fixdb; \
 	fi
-	@echo ">>> exim_fixdb utility built"
-	@echo " "
+#	@echo ">>> exim_fixdb utility built"
+#	@echo " "
 
 # The utility for tidying the contents of an exim database
 
@@ -630,8 +628,8 @@ exim_tidydb: $(OBJ_TIDYDB)
 	  echo $(STRIP_COMMAND) exim_tidydb; \
 	  $(STRIP_COMMAND) exim_tidydb; \
 	fi
-	@echo ">>> exim_tidydb utility built"
-	@echo " "
+#	@echo ">>> exim_tidydb utility built"
+#	@echo " "
 
 # The utility for building dbm files
 
@@ -646,8 +644,8 @@ exim_dbmbuild: $(OBJ_DBMBUILD)
 	  echo $(STRIP_COMMAND) exim_dbmbuild; \
 	  $(STRIP_COMMAND) exim_dbmbuild; \
 	fi
-	@echo ">>> exim_dbmbuild utility built"
-	@echo " "
+#	@echo ">>> exim_dbmbuild utility built"
+#	@echo " "
 
 # The utility for locking a mailbox while messing around with it
 
@@ -661,8 +659,8 @@ exim_lock: exim_lock.c os.h
 	  echo $(STRIP_COMMAND) exim_lock; \
 	  $(STRIP_COMMAND) exim_lock; \
 	fi
-	@echo ">>> exim_lock utility built"
-	@echo " "
+#	@echo ">>> exim_lock utility built"
+#	@echo " "
 
 # The X-based Exim monitor program's binary part. There's a macro for cutting
 # out the modified TextPop module, because some antique link editors cannot
@@ -701,8 +699,8 @@ eximon.bin: $(EXIMON_EDITME) eximon $(OBJ_MONBIN) ../exim_monitor/em_version.c \
 	  echo $(STRIP_COMMAND) eximon.bin; \
 	  $(STRIP_COMMAND) eximon.bin; \
 	fi
-	@echo ">>> exim monitor binary built"
-	@echo " "
+#	@echo ">>> exim monitor binary built"
+#	@echo " "
 
 
 # Compile step for most of the exim modules. HDRS is a list of headers
@@ -861,7 +859,7 @@ util-os.o:       $(HDRS) os.c
 # The local scan module depends only on its own special header, and is compiled
 # from a source whose location is set by configuration.
 
-local_scan.o:    config local_scan.h ../$(LOCAL_SCAN_SOURCE)
+local_scan.o:    $(CONFIG_DEPS) local_scan.h ../$(LOCAL_SCAN_SOURCE)
 	@echo "$(CC) local_scan.c"
 	$(FE)$(CC) -DLOCAL_SCAN -c $(CFLAGS) -I. $(INCLUDE) -o local_scan.o ../$(LOCAL_SCAN_SOURCE)
 
@@ -1047,11 +1045,10 @@ $(MONBIN): $(HDRS)
 
 # Copies of modules built as dynamic-load libraries
 
-dynmodules: buildlookups buildrouters buildtransports buildauths \
-		buildmisc
-	rm -fr dynmodules
-	mkdir dynmodules
-	for d in lookup router transport auth miscmod; do \
+dynmodules: buildlookups buildrouters buildtransports buildauths buildmisc
+	$(FE)rm -fr dynmodules
+	$(FE)mkdir dynmodules
+	$(FE)for d in lookup router transport auth miscmod; do \
 	  for f in $${d}s/*.so; do \
 	    [ -e $$f ] && ln $$f dynmodules/`basename $$f .so`_$$d.so; \
 	  done; \
@@ -1060,42 +1057,46 @@ dynmodules: buildlookups buildrouters buildtransports buildauths \
 
 # The lookups library.
 
-buildlookups: config
-	 @cd lookups && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) CC="$(CC)" CFLAGS="$(CFLAGS)" \
+buildlookups: $(CONFIG_DEPS)
+	@cd lookups && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) CC="$(CC)" CFLAGS="$(CFLAGS)" \
 	   CFLAGS_DYNAMIC="$(CFLAGS_DYNAMIC)" HDRS="../version.h $(PHDRS)" \
 	   FE="$(FE)" RANLIB="$(RANLIB)" RM_COMMAND="$(RM_COMMAND)" \
 	   INCLUDE="$(INCLUDE) $(IPV6_INCLUDE) $(TLS_INCLUDE) $(LOOKUP_INCLUDE)"
-	 @echo " "
+	$(FE)touch $@
+#	@echo " "
 
 # The routers library.
 
-buildrouters: config
-	 @cd routers && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) CC="$(CC)" CFLAGS="$(CFLAGS)" \
+buildrouters: $(CONFIG_DEPS)
+	@cd routers && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) CC="$(CC)" CFLAGS="$(CFLAGS)" \
 	   CFLAGS_DYNAMIC="$(CFLAGS_DYNAMIC)" \
 	   FE="$(FE)" RANLIB="$(RANLIB)" RM_COMMAND="$(RM_COMMAND)" HDRS="$(PHDRS)" \
 	   INCLUDE="$(INCLUDE) $(IPV6_INCLUDE) $(TLS_INCLUDE)"
-	 @echo " "
+	$(FE)touch $@
+#	@echo " "
 
 # The transports library.
 
-buildtransports: config
-	 @cd transports && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) CC="$(CC)" CFLAGS="$(CFLAGS)" \
+buildtransports: $(CONFIG_DEPS)
+	@cd transports && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) CC="$(CC)" CFLAGS="$(CFLAGS)" \
 	   CFLAGS_DYNAMIC="$(CFLAGS_DYNAMIC)" \
 	   FE="$(FE)" RANLIB="$(RANLIB)" RM_COMMAND="$(RM_COMMAND)" HDRS="$(PHDRS)" \
 	   INCLUDE="$(INCLUDE) $(IPV6_INCLUDE) $(TLS_INCLUDE)"
-	 @echo " "
+	$(FE)touch $@
+#	@echo " "
 
 # The library of authorization modules
 
-buildauths: config
-	 @cd auths && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) CC="$(CC)" CFLAGS="$(CFLAGS)" \
+buildauths: $(CONFIG_DEPS)
+	@cd auths && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) CC="$(CC)" CFLAGS="$(CFLAGS)" \
 	   CFLAGS_DYNAMIC="$(CFLAGS_DYNAMIC)" \
 	   FE="$(FE)" RANLIB="$(RANLIB)" RM_COMMAND="$(RM_COMMAND)" HDRS="$(PHDRS)" \
 	   INCLUDE="$(INCLUDE) $(IPV6_INCLUDE) $(TLS_INCLUDE)"
-	 @echo " "
+	$(FE)touch $@
+#	@echo " "
 
-buildmisc: config
-	 @cd miscmods && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) \
+buildmisc: $(CONFIG_DEPS)
+	@cd miscmods && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) \
 	   CC="$(CC)" CFLAGS="$(CFLAGS)" \
 	     CFLAGS_DYNAMIC="$(CFLAGS_DYNAMIC)" \
 	     LDFLAGS_PARTIAL="$(LDFLAGS_PARTIAL)" HDRS="../version.h $(PHDRS)" \
@@ -1103,7 +1104,8 @@ buildmisc: config
 	     PERL_CC="$(PERL_CC)" PERL_CCOPTS="$(PERL_CCOPTS)" \
 	     PERL_CFLAGS="$(PERL_CFLAGS)" PERL_LFLAGS="$(PERL_LFLAGS)" \
 	     INCLUDE="$(INCLUDE) $(IPV6_INCLUDE)" TLS_INCLUDE="$(TLS_INCLUDE)"
-	 @echo " "
+	$(FE)touch $@
+#	@echo " "
 
 # The "clean", "install", and "makefile" targets just pass themselves back to
 # the main Exim makefile. These targets will be obeyed only if "make" is obeyed
diff --git a/src/scripts/Configure-Makefile b/src/scripts/Configure-Makefile
index 73737274e..cf85d6306 100755
--- a/src/scripts/Configure-Makefile
+++ b/src/scripts/Configure-Makefile
@@ -38,15 +38,20 @@ rebuild=yes
 
 if [ -f Makefile ] ; then
   rebuild=no
-  if ../scripts/newer $editme Makefile || \
-     ../scripts/newer $editme-$ostype Makefile || \
-     ../scripts/newer $editme-$archtype Makefile || \
-     ../scripts/newer $editme-$ostype-$archtype Makefile || \
-     ../scripts/newer ../scripts/Configure-Makefile Makefile || \
-     ../scripts/newer ../scripts/lookups-Makefile Makefile || \
-     ../scripts/newer ../scripts/drivers-Makefile Makefile || \
-     ../scripts/newer ../OS/Makefile-Base Makefile || \
-     ../scripts/newer ../OS/Makefile-Default Makefile
+  if ../scripts/newer $editme Makefile \
+     || ../scripts/newer $editme-$ostype Makefile \
+     || ../scripts/newer $editme-$archtype Makefile \
+     || ../scripts/newer $editme-$ostype-$archtype Makefile \
+     || ../scripts/newer ../scripts/Configure-Makefile Makefile \
+     || ../scripts/newer ../scripts/lookups-Makefile Makefile \
+     || ../scripts/newer ../scripts/drivers-Makefile Makefile \
+     || ../scripts/newer ../OS/Makefile-Base Makefile \
+     || ../scripts/newer ../OS/Makefile-Default Makefile \
+     || ../scripts/newer ../src/auths/Makefile Makefile \
+     || ../scripts/newer ../src/lookups/Makefile Makefile \
+     || ../scripts/newer ../src/miscmods/Makefile Makefile \
+     || ../scripts/newer ../src/routers/Makefile Makefile \
+     || ../scripts/newer ../src/transports/Makefile Makefile
   then
     rebuild=yes
   fi
@@ -78,8 +83,8 @@ fi
 # If Makefile is up-to-date, no need to rebuild it.
 
 if [ $rebuild = no ] ; then
-  echo "\`Makefile' is up to date."
-  echo " "
+#  echo "\`Makefile' is up to date."
+#  echo " "
   exit
 fi
 
diff --git a/src/src/auths/Makefile b/src/src/auths/Makefile
index b929f6742..6eab96a66 100644
--- a/src/src/auths/Makefile
+++ b/src/src/auths/Makefile
@@ -17,6 +17,7 @@
 OBJ += auth-spa.o
 
 all:		auths.a $(MODS)
+		$(FE):
 
 auths.a:	$(OBJ)
 		@$(RM_COMMAND) -f auths.a
diff --git a/src/src/lookups/Makefile b/src/src/lookups/Makefile
index d794fe895..23347e83b 100644
--- a/src/src/lookups/Makefile
+++ b/src/src/lookups/Makefile
@@ -18,7 +18,8 @@ OBJS = $(OBJ) $(OBJ_ALWAYS)
 # This assumes that the driver and object-file names match
 AVAIL= $(OBJS:.o=)
 
-all:             Makefile lookups.a $(MODS)
+all:	Makefile lookups.a $(MODS)
+	$(FE):
 
 # We assume here that the driver name is used as the prefix for
 # its module_info struct.
diff --git a/src/src/miscmods/Makefile b/src/src/miscmods/Makefile
index 1e46d4456..cac4bd316 100644
--- a/src/src/miscmods/Makefile
+++ b/src/src/miscmods/Makefile
@@ -14,6 +14,7 @@
 OBJ += dummy.o
 
 all:		miscmods.a $(MODS)
+		$(FE):
 
 miscmods.a:	$(OBJ)
 		@$(RM_COMMAND) -f miscmods.a
diff --git a/src/src/routers/Makefile b/src/src/routers/Makefile
index 6c1dc6e9a..1b2953d40 100644
--- a/src/src/routers/Makefile
+++ b/src/src/routers/Makefile
@@ -12,7 +12,8 @@
 
 # MAGIC-TAG-MODS-OBJ-RULES-GO-HERE
 
-all:             routers.a $(MODS)
+all:	routers.a $(MODS)
+	$(FE):
 
 routers.a:       $(OBJ)
 		 @$(RM_COMMAND) -f routers.a
diff --git a/src/src/transports/Makefile b/src/src/transports/Makefile
index 454d36347..89cadc568 100644
--- a/src/src/transports/Makefile
+++ b/src/src/transports/Makefile
@@ -13,7 +13,8 @@
 
 OBJ += smtp_socks.o tf_maildir.o
 
-all:		transports.a $(MODS)
+all:	transports.a $(MODS)
+	$(FE):
 
 transports.a:    $(OBJ) smtp_socks.o tf_maildir.o
 		 @$(RM_COMMAND) -f transports.a

commit 998636a4f8493855a351bb1080fc7687269cb9f9
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Nov 12 11:20:42 2025 +0000

    library versions

diff --git a/src/scripts/reversion b/src/scripts/reversion
index 6fd0782a4..00b4058d3 100755
--- a/src/scripts/reversion
+++ b/src/scripts/reversion
@@ -122,9 +122,9 @@ then
   test -n "$EXIM_VARIANT_VERSION" && \
   echo '#define EXIM_VARIANT_VERSION "'"$EXIM_VARIANT_VERSION"'"'
   echo '#ifdef EXIM_VARIANT_VERSION'
-  echo '#define EXIM_VERSION_STR EXIM_RELEASE_VERSION "-" EXIM_VARIANT_VERSION'
+  echo '# define EXIM_VERSION_STR EXIM_RELEASE_VERSION "-" EXIM_VARIANT_VERSION'
   echo '#else'
-  echo '#define EXIM_VERSION_STR EXIM_RELEASE_VERSION'
+  echo '# define EXIM_VERSION_STR EXIM_RELEASE_VERSION'
   echo '#endif'
   if [ ".${exim_build_date_override:-}" != "." ]; then
     echo '#define EXIM_BUILD_DATE_OVERRIDE "'"${exim_build_date_override}"'"'
diff --git a/src/src/exim.c b/src/src/exim.c
index 9d6f858bd..13cc4ff2c 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1316,19 +1316,15 @@ Currently they are output in misc_mod_add() */
 #ifndef PCRE_PRERELEASE
 # define PCRE_PRERELEASE
 #endif
-#define QUOTE(X) #X
-#define EXPAND_AND_QUOTE(X) QUOTE(X)
     {
     uschar buf[24];
     pcre2_config(PCRE2_CONFIG_VERSION, buf);
     g = string_fmt_append(g, "Library version: PCRE2: Compile: %d.%d%s\n"
 		"                        Runtime: %s\n",
 	    PCRE2_MAJOR, PCRE2_MINOR,
-	    EXPAND_AND_QUOTE(PCRE2_PRERELEASE) "",
+	    mac_expanded_string(PCRE2_PRERELEASE) "",
 	    buf);
     }
-#undef QUOTE
-#undef EXPAND_AND_QUOTE
 
   show_string(is_stdout, g);
   g = NULL;
diff --git a/src/src/lookups/cdb.c b/src/src/lookups/cdb.c
index b56306a7f..98ff462a3 100644
--- a/src/src/lookups/cdb.c
+++ b/src/src/lookups/cdb.c
@@ -462,10 +462,8 @@ if (cdbp->cdb_map)
 gstring *
 cdb_version_report(gstring * g)
 {
-#ifdef DYNLOOKUP
-g = string_fmt_append(g, "Library version: CDB: Exim version %s\n", EXIM_VERSION_STR);
-#endif
-return g;
+return string_fmt_append(g, "Library version: CDB: Exim %s builtin\n",
+			  EXIM_VERSION_STR);
 }
 
 
diff --git a/src/src/lookups/dnsdb.c b/src/src/lookups/dnsdb.c
index d08f6122e..6e2be8ae1 100644
--- a/src/src/lookups/dnsdb.c
+++ b/src/src/lookups/dnsdb.c
@@ -591,10 +591,8 @@ return rc;
 gstring *
 dnsdb_version_report(gstring * g)
 {
-#ifdef DYNLOOKUP
-g = string_fmt_append(g, "Library version: DNSDB: Exim version %s\n", EXIM_VERSION_STR);
-#endif
-return g;
+return string_fmt_append(g, "Library version: DNSDB: Exim %s builtin\n",
+			    EXIM_VERSION_STR);
 }
 
 
diff --git a/src/src/lookups/dsearch.c b/src/src/lookups/dsearch.c
index 99e1c8648..4fb8f0c27 100644
--- a/src/src/lookups/dsearch.c
+++ b/src/src/lookups/dsearch.c
@@ -172,10 +172,8 @@ handle = handle;   /* Avoid compiler warning */
 gstring *
 dsearch_version_report(gstring * g)
 {
-#ifdef DYNLOOKUP
-g = string_fmt_append(g, "Library version: dsearch: Exim version %s\n", EXIM_VERSION_STR);
-#endif
-return g;
+return string_fmt_append(g, "Library version: dsearch: Exim %s builtin\n",
+			  EXIM_VERSION_STR);
 }
 
 
diff --git a/src/src/lookups/json.c b/src/src/lookups/json.c
index 43575cacf..3e5f9609e 100644
--- a/src/src/lookups/json.c
+++ b/src/src/lookups/json.c
@@ -162,7 +162,8 @@ json_close(void *handle)
 gstring *
 json_version_report(gstring * g)
 {
-return string_fmt_append(g, "Library version: json: Jansonn version %s\n", JANSSON_VERSION);
+return string_fmt_append(g, "Library version: json: Jansonn version %s\n",
+			  JANSSON_VERSION);
 }
 
 
diff --git a/src/src/lookups/ldap.c b/src/src/lookups/ldap.c
index a6e99f567..c657797d9 100644
--- a/src/src/lookups/ldap.c
+++ b/src/src/lookups/ldap.c
@@ -1563,11 +1563,29 @@ return quoted;
 gstring *
 ldap_version_report(gstring * g)
 {
-#ifdef DYNLOOKUP
-/*XXX it would be nice to haul a version string for the underlying ldap library */
-g = string_fmt_append(g, "Library version: LDAP: Exim version %s\n", EXIM_VERSION_STR);
-#endif
+#ifdef LDAP_LIB_OPENLDAP2
+LDAP * ld = ldap_init("", 0);
+LDAPAPIInfo info = {.ldapai_info_version = LDAP_API_INFO_VERSION};
+
+g = string_fmt_append(g, "Library version: LDAP: Compile %s %u.%u.%u\n",
+    LDAP_VENDOR_NAME, LDAP_VENDOR_VERSION_MAJOR,
+    LDAP_VENDOR_VERSION_MINOR, LDAP_VENDOR_VERSION_PATCH);
+
+if (ldap_get_option(ld, LDAP_OPT_API_INFO, &info) == LDAP_OPT_SUCCESS)
+  {
+  g = string_fmt_append(g, "                       Runtime %s %d\n",
+      info.ldapai_vendor_name, info.ldapai_vendor_version);
+
+  for (char ** sp = info.ldapai_extensions; *sp; *sp++)
+    ldap_memfree(*sp);
+  ldap_memfree(info.ldapai_extensions);
+  ldap_memfree(info.ldapai_vendor_name);
+  }
 return g;
+
+#else
+return string_fmt_append(g, "Library version: LDAP: (unknown)\n");
+#endif
 }
 
 
diff --git a/src/src/lookups/lmdb.c b/src/src/lookups/lmdb.c
index 3a3eebcba..c90632cee 100644
--- a/src/src/lookups/lmdb.c
+++ b/src/src/lookups/lmdb.c
@@ -134,10 +134,9 @@ gstring *
 lmdb_version_report(gstring * g)
 {
 g = string_fmt_append(g, "Library version: LMDB: Compile: %d.%d.%d\n",
-			  MDB_VERSION_MAJOR, MDB_VERSION_MINOR, MDB_VERSION_PATCH);
-#ifdef DYNLOOKUP
-g = string_fmt_append(g, "                        Exim version %s\n", EXIM_VERSION_STR);
-#endif
+		      MDB_VERSION_MAJOR, MDB_VERSION_MINOR, MDB_VERSION_PATCH);
+g = string_fmt_append(g, "                       Runtime: %s\n",
+		      mdb_version(NULL, NULL, NULL));
 return g;
 }
 
diff --git a/src/src/lookups/lsearch.c b/src/src/lookups/lsearch.c
index 89a9e6c80..fc7ebaf62 100644
--- a/src/src/lookups/lsearch.c
+++ b/src/src/lookups/lsearch.c
@@ -418,10 +418,8 @@ lsearch_close(void *handle)
 gstring *
 lsearch_version_report(gstring * g)
 {
-#ifdef DYNLOOKUP
-g = string_fmt_append(g, "Library version: lsearch: Exim version %s\n", EXIM_VERSION_STR);
-#endif
-return g;
+return string_fmt_append(g, "Library version: lsearch: Exim %s builtin\n",
+			    EXIM_VERSION_STR);
 }
 
 
diff --git a/src/src/lookups/mysql.c b/src/src/lookups/mysql.c
index bbf98cb0c..76954ca43 100644
--- a/src/src/lookups/mysql.c
+++ b/src/src/lookups/mysql.c
@@ -475,12 +475,8 @@ mysql_version_report(gstring * g)
 g = string_fmt_append(g,
   "Library version: MySQL: Compile: %lu %s [%s]\n"
   "                        Runtime: %lu %s\n",
-        (long)EXIM_MxSQL_VERSION_ID, EXIM_MxSQL_VERSION_STR, EXIM_MxSQL_BASE_STR,
-        mysql_get_client_version(), mysql_get_client_info());
-#ifdef DYNLOOKUP
-g = string_fmt_append(g,
-  "                        Exim version %s\n", EXIM_VERSION_STR);
-#endif
+      (long)EXIM_MxSQL_VERSION_ID, EXIM_MxSQL_VERSION_STR, EXIM_MxSQL_BASE_STR,
+      mysql_get_client_version(), mysql_get_client_info());
 return g;
 }
 
diff --git a/src/src/lookups/nis.c b/src/src/lookups/nis.c
index aa12f4742..77f6b0f68 100644
--- a/src/src/lookups/nis.c
+++ b/src/src/lookups/nis.c
@@ -101,6 +101,7 @@ return (rc == YPERR_KEY || rc == YPERR_MAP)? FAIL : DEFER;
 gstring *
 nis_version_report(gstring * g)
 {
+/* maybe part of glibc? */
 #ifdef DYNLOOKUP
 g = string_fmt_append(g, "Library version: NIS: Exim version %s\n", EXIM_VERSION_STR);
 #endif
diff --git a/src/src/lookups/nmh.c b/src/src/lookups/nmh.c
index c2fd0e61c..0e93186c1 100644
--- a/src/src/lookups/nmh.c
+++ b/src/src/lookups/nmh.c
@@ -347,6 +347,7 @@ while ((cn = nmh_connections))
 gstring *
 nmh_version_report(gstring * g)
 {
+/* NMH has no version api! */
 #ifdef DYNLOOKUP
 g = string_fmt_append(g, "Library version: NMH: Exim version %s\n", EXIM_VERSION_STR);
 #endif
diff --git a/src/src/lookups/passwd.c b/src/src/lookups/passwd.c
index 7bf499920..c22c2d4cc 100644
--- a/src/src/lookups/passwd.c
+++ b/src/src/lookups/passwd.c
@@ -58,10 +58,8 @@ return OK;
 gstring *
 passwd_version_report(gstring * g)
 {
-#ifdef DYNLOOKUP
-g = string_fmt_append(g, "Library version: passwd: Exim version %s\n", EXIM_VERSION_STR);
-#endif
-return g;
+return string_fmt_append(g, "Library version: passwd: Exim %s builtin\n",
+			    EXIM_VERSION_STR);
 }
 
 static lookup_info _lookup_info = {
diff --git a/src/src/lookups/pgsql.c b/src/src/lookups/pgsql.c
index a578645ff..8ddf0e058 100644
--- a/src/src/lookups/pgsql.c
+++ b/src/src/lookups/pgsql.c
@@ -477,9 +477,7 @@ return quoted;
 gstring *
 pgsql_version_report(gstring * g)
 {
-#ifdef DYNLOOKUP
-g = string_fmt_append(g, "Library version: PostgreSQL: Exim version %s\n", EXIM_VERSION_STR);
-#endif
+int ver = PQlibVersion();
 
 /* Version reporting: there appears to be no available information about
 the client library in libpq-fe.h; once you have a connection object, you
@@ -487,6 +485,9 @@ can access the server version and the chosen protocol version, but those
 aren't really what we want.  It might make sense to debug_printf those
 when the connection is established though? */
 
+/* version 9.1 onwards */
+g = string_fmt_append(g, "Library version: PostgreSQL: Runtime: %d.%d\n",
+			ver/10000, ver%10000);
 return g;
 }
 
diff --git a/src/src/lookups/psl.c b/src/src/lookups/psl.c
index 1592fe32d..9171192a3 100644
--- a/src/src/lookups/psl.c
+++ b/src/src/lookups/psl.c
@@ -212,10 +212,8 @@ return psl_gen_find(handle, keystring, length, result, errmsg, TRUE);
 gstring *
 psl_version_report(gstring * g)
 {
-#ifdef DYNLOOKUP
-g = string_fmt_append(g, "Library version: psl: Exim version %s\n", EXIM_VERSION_STR);
-#endif
-return g;
+return string_fmt_append(g, "Library version: psl: Exim %s builtin\n",
+				EXIM_VERSION_STR);
 }
 
 static lookup_info psl_lookup_info = {
diff --git a/src/src/lookups/redis.c b/src/src/lookups/redis.c
index 4b11475d0..a721d0605 100644
--- a/src/src/lookups/redis.c
+++ b/src/src/lookups/redis.c
@@ -433,13 +433,8 @@ return quoted;
 gstring *
 redis_version_report(gstring * g)
 {
-g = string_fmt_append(g,
-  "Library version: REDIS: Compile: %d [%d]\n", HIREDIS_MAJOR, HIREDIS_MINOR);
-#ifdef DYNLOOKUP
-g = string_fmt_append(g,
-  "                        Exim version %s\n", EXIM_VERSION_STR);
-#endif
-return g;
+return string_fmt_append(g, "Library version: REDIS: Compile: %d.%d.%d\n",
+    HIREDIS_MAJOR, HIREDIS_MINOR, HIREDIS_PATCH);
 }
 
 
diff --git a/src/src/lookups/spf.c b/src/src/lookups/spf.c
index e1931ca0f..ce314f7e5 100644
--- a/src/src/lookups/spf.c
+++ b/src/src/lookups/spf.c
@@ -86,10 +86,11 @@ return FAIL;
 gstring *
 spf_version_report(gstring * g)
 {
-#ifdef DYNLOOKUP
-g = string_fmt_append(g, "Library version: SPF: Exim version %s\n", EXIM_VERSION_STR));
-#endif
-return g;
+int maj, min, patch;
+
+SPF_get_lib_version(&maj, &min, &patch);
+return string_fmt_append(g, "Library version: SPF: Runtime: %d.%d.%d\n",
+      maj, min, patch);
 }
 
 
diff --git a/src/src/lookups/sqlite.c b/src/src/lookups/sqlite.c
index 722916cf3..e358c9f84 100644
--- a/src/src/lookups/sqlite.c
+++ b/src/src/lookups/sqlite.c
@@ -172,10 +172,6 @@ g = string_fmt_append(g,
   "Library version: SQLite: Compile: %s\n"
   "                         Runtime: %s\n",
         SQLITE_VERSION, sqlite3_libversion());
-#ifdef DYNLOOKUP
-g = string_fmt_append(g,
-  "                         Exim version %s\n", EXIM_VERSION_STR);
-#endif
 return g;
 }
 
diff --git a/src/src/lookups/testdb.c b/src/src/lookups/testdb.c
index 44d77b8c8..2e3ed36c2 100644
--- a/src/src/lookups/testdb.c
+++ b/src/src/lookups/testdb.c
@@ -83,10 +83,8 @@ return quoted;
 gstring *
 testdb_version_report(gstring * g)
 {
-#ifdef DYNLOOKUP
-g = string_fmt_append(g, "Library version: TestDB: Exim version %s\n", EXIM_VERSION_STR);
-#endif
-return g;
+return string_fmt_append(g, "Library version: TestDB: Exim version %s\n",
+			EXIM_VERSION_STR);
 }
 
 
diff --git a/src/src/lookups/whoson.c b/src/src/lookups/whoson.c
index cd6c7e85c..f783a905a 100644
--- a/src/src/lookups/whoson.c
+++ b/src/src/lookups/whoson.c
@@ -68,13 +68,8 @@ switch (wso_query(CS query, CS buffer, sizeof(buffer)))
 gstring *
 whoson_version_report(gstring * g)
 {
-g = string_fmt_append(g,
+return string_fmt_append(g,
   "Library version: Whoson: Runtime: %s\n", wso_version());
-#ifdef DYNLOOKUP
-g = string_fmt_append(g,
-  "                         Exim version %s\n", EXIM_VERSION_STR);
-#endif
-return g;
 }
 
 static lookup_info _lookup_info = {
diff --git a/src/src/miscmods/spf_perl.c b/src/src/miscmods/spf_perl.c
index d491ae64e..e7bc1e331 100644
--- a/src/src/miscmods/spf_perl.c
+++ b/src/src/miscmods/spf_perl.c
@@ -121,16 +121,14 @@ return g;
 /******************************************************************************/
 
 
-#ifdef notdef
 /*API*/
 static gstring *
 spf_lib_version_report(gstring * g)
 {
 /*XXX Does Mail::SPF have a version? MetaCPAN says yes, but does not
 document a method that returns it. */
-return g;
+return string_fmt_append(g, "Library_version: SPF: perl Mail::SPF\n");
 }
-#endif
 
 
 
@@ -389,7 +387,7 @@ misc_module_info spf_module_info =
 # ifdef DYNLOOKUP
   .dyn_magic =		MISC_MODULE_MAGIC,
 # endif
-  /* .lib_vers_report =	spf_lib_version_report, */
+  .lib_vers_report =	spf_lib_version_report,
   .conn_init =		spf_conn_init,
   .smtp_reset =		spf_smtp_reset,
   .authres =		authres_spf,

commit ed97fea3e920289ac87f1736146c68f4ce3050a8
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Nov 12 14:37:42 2025 +0000

    Fix non-openldap2 build
    
    Broken-by: 998636a4f849

diff --git a/src/src/lookups/ldap.c b/src/src/lookups/ldap.c
index c657797d9..7d240309c 100644
--- a/src/src/lookups/ldap.c
+++ b/src/src/lookups/ldap.c
@@ -1584,7 +1584,7 @@ if (ldap_get_option(ld, LDAP_OPT_API_INFO, &info) == LDAP_OPT_SUCCESS)
 return g;
 
 #else
-return string_fmt_append(g, "Library version: LDAP: (unknown)\n");
+return string_cat(g, "Library version: LDAP: (unknown)\n");
 #endif
 }
 

commit 86161af9d94e2beeb7ad630e80247ef791c22313
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Nov 12 21:40:04 2025 +0000

    Experimental native DMARC
    
    Bug 3140

diff --git a/src/Makefile b/src/Makefile
index a19c6928a..eac96323e 100644
--- a/src/Makefile
+++ b/src/Makefile
@@ -124,7 +124,7 @@ distclean: clean_doc
 cscope.files: FRC
 	echo "-q" > $@
 	echo "-p3" >> $@
-	-bd=build-$(buildname); [ -d $$bd ] && echo -e "$$bd/config.h\n$$bd/Makefile" >> $@
+	-bd=build-$(buildname); [ -d $$bd ] && echo -e "$$bd/config.h\n$$bd/version.h\n$$bd/Makefile" >> $@
 	find src Local OS exim_monitor -name "*.[cshyl]" -print \
 		    -o -name "*.src" -print \
 		    -o -name "os.[ch]*" -print \
diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index 80f1f1f97..f713ad3ca 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -1062,7 +1062,6 @@ buildlookups: $(CONFIG_DEPS)
 	   CFLAGS_DYNAMIC="$(CFLAGS_DYNAMIC)" HDRS="../version.h $(PHDRS)" \
 	   FE="$(FE)" RANLIB="$(RANLIB)" RM_COMMAND="$(RM_COMMAND)" \
 	   INCLUDE="$(INCLUDE) $(IPV6_INCLUDE) $(TLS_INCLUDE) $(LOOKUP_INCLUDE)"
-	$(FE)touch $@
 #	@echo " "
 
 # The routers library.
@@ -1072,7 +1071,6 @@ buildrouters: $(CONFIG_DEPS)
 	   CFLAGS_DYNAMIC="$(CFLAGS_DYNAMIC)" \
 	   FE="$(FE)" RANLIB="$(RANLIB)" RM_COMMAND="$(RM_COMMAND)" HDRS="$(PHDRS)" \
 	   INCLUDE="$(INCLUDE) $(IPV6_INCLUDE) $(TLS_INCLUDE)"
-	$(FE)touch $@
 #	@echo " "
 
 # The transports library.
@@ -1082,17 +1080,15 @@ buildtransports: $(CONFIG_DEPS)
 	   CFLAGS_DYNAMIC="$(CFLAGS_DYNAMIC)" \
 	   FE="$(FE)" RANLIB="$(RANLIB)" RM_COMMAND="$(RM_COMMAND)" HDRS="$(PHDRS)" \
 	   INCLUDE="$(INCLUDE) $(IPV6_INCLUDE) $(TLS_INCLUDE)"
-	$(FE)touch $@
 #	@echo " "
 
-# The library of authorization modules
+# The library of authentication modules
 
 buildauths: $(CONFIG_DEPS)
 	@cd auths && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) CC="$(CC)" CFLAGS="$(CFLAGS)" \
 	   CFLAGS_DYNAMIC="$(CFLAGS_DYNAMIC)" \
 	   FE="$(FE)" RANLIB="$(RANLIB)" RM_COMMAND="$(RM_COMMAND)" HDRS="$(PHDRS)" \
 	   INCLUDE="$(INCLUDE) $(IPV6_INCLUDE) $(TLS_INCLUDE)"
-	$(FE)touch $@
 #	@echo " "
 
 buildmisc: $(CONFIG_DEPS)
@@ -1104,7 +1100,6 @@ buildmisc: $(CONFIG_DEPS)
 	     PERL_CC="$(PERL_CC)" PERL_CCOPTS="$(PERL_CCOPTS)" \
 	     PERL_CFLAGS="$(PERL_CFLAGS)" PERL_LFLAGS="$(PERL_LFLAGS)" \
 	     INCLUDE="$(INCLUDE) $(IPV6_INCLUDE)" TLS_INCLUDE="$(TLS_INCLUDE)"
-	$(FE)touch $@
 #	@echo " "
 
 # The "clean", "install", and "makefile" targets just pass themselves back to
diff --git a/src/scripts/Configure-Makefile b/src/scripts/Configure-Makefile
index cf85d6306..70dc886ab 100755
--- a/src/scripts/Configure-Makefile
+++ b/src/scripts/Configure-Makefile
@@ -326,7 +326,7 @@ done <<-END
  routers    ROUTER	ACCEPT DNSLOOKUP IPLITERAL IPLOOKUP MANUALROUTE QUERYPROGRAM REDIRECT
  transports TRANSPORT	APPENDFILE AUTOREPLY LMTP PIPE QUEUEFILE SMTP
  auths	    AUTH	CRAM_MD5 CYRUS_SASL DOVECOT EXTERNAL GSASL HEIMDAL_GSSAPI PLAINTEXT SPA TLS
- miscmods   SUPPORT	ARC _DKIM DMARC _EXIM_FILTER PAM PERL RADIUS _SIEVE_FILTER SPF SPF_PERL
+ miscmods   SUPPORT	ARC _DKIM DMARC DMARC_NATIVE _EXIM_FILTER PAM PERL RADIUS _SIEVE_FILTER SPF SPF_PERL
 END
 
 # See if there is a definition of EXIM_PERL in what we have built so far.
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index 4e4a638c7..517310c8a 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -101,7 +101,7 @@ for f in dummy.c \
 	dkim.c dkim_transport.c dkim.h dkim_api.h \
 	pdkim/crypt_ver.h pdkim/pdkim.c pdkim/pdkim.h \
 	pdkim/pdkim_hash.h pdkim/signing.c pdkim/signing.h \
-	dmarc.c dmarc.h dmarc_api.h \
+	dmarc.c dmarc_common.c dmarc.h dmarc_api.h dmarc_native.c \
 	exim_filter.c exim_filter_api.h \
 	pam.c pam_api.h \
 	perl.c perl_api.h \
diff --git a/src/src/acl.c b/src/src/acl.c
index 7d07ab698..be61fc953 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -77,7 +77,7 @@ enum { ACLC_ACL,
        ACLC_DKIM_SIGNER,
        ACLC_DKIM_STATUS,
 #endif
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
        ACLC_DMARC_STATUS,
 #endif
        ACLC_DNSLISTS,
@@ -206,9 +206,9 @@ static condition_def conditions[] = {
       ),
   },
 #endif
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
   [ACLC_DMARC_STATUS] =		{ US"dmarc_status",
-# if SUPPORT_DMARC==2
+# if EXIM_HAVE_DMARC==2
 				  ACD_LOAD |
 # endif
 				  ACD_EXP,
@@ -390,7 +390,7 @@ static int spf_condx[] = { ACLC_SPF, ACLC_SPF_GUESS, -1 };
 #  if SUPPORT_DKIM==2
 static int dkim_condx[] = { ACLC_DKIM_SIGNER, ACLC_DKIM_STATUS, -1 };
 #  endif
-#  if SUPPORT_DMARC==2
+#  if EXIM_HAVE_DMARC==2
 static int dmarc_condx[] = { ACLC_DMARC_STATUS, -1 };
 #  endif
 
@@ -404,7 +404,7 @@ static condition_module condition_modules[] = {
 #  if SUPPORT_DKIM==2
   {.mod_name = US"dkim", .conditions = dkim_condx},
 #  endif
-#  if SUPPORT_DMARC==2
+#  if EXIM_HAVE_DMARC==2
   {.mod_name = US"dmarc", .conditions = dmarc_condx},
 #  endif
 };
@@ -424,7 +424,7 @@ enum {
 #ifndef DISABLE_DKIM
   CONTROL_DKIM_VERIFY,
 #endif
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
   CONTROL_DMARC_VERIFY,
   CONTROL_DMARC_FORENSIC,
 #endif
@@ -492,7 +492,7 @@ static control_def controls_list[] = {
   },
 #endif
 
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
 [CONTROL_DMARC_VERIFY] =
   { US"dmarc_disable_verify",    FALSE,
 	  ACL_BIT_DATA | ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START
@@ -3481,7 +3481,7 @@ for (; cb; cb = cb->next)
 #ifndef DISABLE_DKIM
 	case CONTROL_DKIM_VERIFY:
 	  f.dkim_disable_verify = TRUE;
-# ifdef SUPPORT_DMARC
+# ifdef EXIM_HAVE_DMARC
 	  /* Since DKIM was blocked, skip DMARC too */
 	  f.dmarc_disable_verify = TRUE;
 	  f.dmarc_enable_forensic = FALSE;
@@ -3489,7 +3489,7 @@ for (; cb; cb = cb->next)
 	break;
 #endif
 
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
 	case CONTROL_DMARC_VERIFY:
 	  f.dmarc_disable_verify = TRUE;
 	  break;
@@ -3934,7 +3934,7 @@ for (; cb; cb = cb->next)
       }
 #endif
 
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
     case ACLC_DMARC_STATUS:
       /* See comment on ACLC_SPF wrt. coding issues */
       {
@@ -3945,7 +3945,7 @@ for (; cb; cb = cb->next)
       if (!mi)
 	{ rc = DEFER; break; }			/* shouldn't happen */
 
-      if (!f.dmarc_has_been_checked)
+      if (!f.dmarc_has_been_checked)		/* only once per message */
 	{
 	typedef int (*pfn_t)(void);
 	(void) (((pfn_t *) mi->functions)[DMARC_PROCESS]) ();
@@ -3955,7 +3955,6 @@ for (; cb; cb = cb->next)
       /* used long way of dmarc_exim_expand_query() in case we need more
       view into the process in the future. */
 
-      /*XXX is this call used with any other arg? */
       expanded_query = (((efn_t *) mi->functions)[DMARC_EXPAND_QUERY]) ();
       rc = match_isinlist(expanded_query,
 			  &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults
index f331ad0d5..71146ae3b 100644
--- a/src/src/config.h.defaults
+++ b/src/src/config.h.defaults
@@ -217,6 +217,7 @@ Do not put spaces between # and the 'define'.
 /* EXPERIMENTAL features */
 #define EXPERIMENTAL_ARC
 #define EXPERIMENTAL_DCC
+#define EXPERIMENTAL_DMARC_NATIVE
 #define EXPERIMENTAL_DSN_INFO
 #define EXPERIMENTAL_NMH
 #define EXPERIMENTAL_QUEUEFILE
diff --git a/src/src/drtables.c b/src/src/drtables.c
index 5993b5c29..9e25472d3 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -481,7 +481,7 @@ built as static */
 #if !defined(DISABLE_DKIM) && (!defined(SUPPORT_DKIM) || SUPPORT_DKIM!=2)
 extern misc_module_info dkim_module_info;
 #endif
-#if defined(SUPPORT_DMARC) && SUPPORT_DMARC!=2
+#if defined(EXIM_HAVE_DMARC) && EXIM_HAVE_DMARC!=2
 extern misc_module_info dmarc_module_info;
 #endif
 #if defined(EXIM_HAVE_SPF) && EXIM_HAVE_SPF!=2
@@ -519,13 +519,13 @@ onetime = TRUE;
 #if defined(EXIM_HAVE_SPF) && EXIM_HAVE_SPF!=2
   misc_mod_add(&spf_module_info);
 #endif
-#if defined(SUPPORT_DMARC) && SUPPORT_DMARC!=2
-/* dmarc depends on spf so this add must go after, for the both-static case */
-  misc_mod_add(&dmarc_module_info);
-#endif
 #if defined(EXPERIMENTAL_ARC) && (!defined(SUPPORT_ARC) || SUPPORT_ARC!=2)
   misc_mod_add(&arc_module_info);
 #endif
+#if defined(EXIM_HAVE_DMARC) && EXIM_HAVE_DMARC!=2
+/* dmarc depends on spf/dkim/arc so this add must go after, for the both-static case */
+  misc_mod_add(&dmarc_module_info);
+#endif
 #if defined(RADIUS_CONFIG_FILE) && (!defined(SUPPORT_RADIUS) || SUPPORT_RADIUS!=2)
   misc_mod_add(&radius_module_info);
 #endif
diff --git a/src/src/exim.c b/src/src/exim.c
index 13cc4ff2c..c23eb868c 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1176,7 +1176,7 @@ g = string_cat(g, US"Support for:");
 #ifndef DISABLE_DKIM
   g = string_cat(g, US" DKIM");
 #endif
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
   g = string_cat(g, US" DMARC");
 #endif
 #ifndef DISABLE_DNSSEC
diff --git a/src/src/exim.h b/src/src/exim.h
index 18d1b1f40..538f354e2 100644
--- a/src/src/exim.h
+++ b/src/src/exim.h
@@ -535,8 +535,6 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly.
 #if !defined(MACRO_PREDEF) && !defined(COMPILE_UTILITY)
 # include "hash.h"
 #endif
-#include "globals.h"
-#include "functions.h"
 #if !defined(MACRO_PREDEF) && !defined(COMPILE_UTILITY)
 # include "dbfunctions.h"
 #endif
@@ -550,10 +548,12 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly.
 # include "miscmods/dkim.h"
 # include "miscmods/dkim_api.h"
 #endif
-#ifdef SUPPORT_DMARC
+#if defined(SUPPORT_DMARC) || defined(EXPERIMENTAL_DMARC_NATIVE)
 # include "miscmods/dmarc.h"
 # include "miscmods/dmarc_api.h"
-# include <opendmarc/dmarc.h>
+# ifdef SUPPORT_DMARC
+#  include <opendmarc/dmarc.h>
+# endif
 #endif
 #ifdef EXPERIMENTAL_ARC
 # include "miscmods/arc_api.h"
@@ -570,6 +570,9 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly.
 #include "miscmods/exim_filter_api.h"
 #include "miscmods/sieve_filter_api.h"
 
+#include "globals.h"
+#include "functions.h"
+
 /* The following stuff must follow the inclusion of config.h because it
 requires various things that are set therein. */
 
diff --git a/src/src/expand.c b/src/src/expand.c
index fcf699d2e..1b5980f5b 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -514,7 +514,7 @@ static var_entry var_table[] = {
   { "dkim_verify_signers", vtype_module,	US"dkim" },
   { "dkim_verify_status",  vtype_module,	US"dkim" },
 #endif
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
   { "dmarc_alignment_dkim",vtype_module,	US"dmarc" },
   { "dmarc_alignment_spf", vtype_module,	US"dmarc" },
   { "dmarc_domain_policy", vtype_module,	US"dmarc" },
diff --git a/src/src/globals.c b/src/src/globals.c
index c8636e191..36f364996 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -251,7 +251,7 @@ struct global_flags f =
 	.dkim_disable_verify      = FALSE,
 	.dkim_init_done           = FALSE,
 #endif
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
 	.dmarc_has_been_checked  = FALSE,
 	.dmarc_disable_verify    = FALSE,
 	.dmarc_enable_forensic   = FALSE,
diff --git a/src/src/globals.h b/src/src/globals.h
index ddf78b4d8..9f4d04a2d 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -221,7 +221,7 @@ extern struct global_flags {
  BOOL   dkim_disable_verify		:1; /* Set via ACL control statement. When set, DKIM verification is disabled for the current message */
  BOOL   dkim_init_done			:1; /* lazy-init status */
 #endif
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
  BOOL   dmarc_has_been_checked		:1; /* Global variable to check if test has been called yet */
  BOOL   dmarc_disable_verify		:1; /* Set via ACL control statement. When set, DMARC verification is disabled for the current message */
  BOOL   dmarc_enable_forensic		:1; /* Set via ACL control statement. When set, DMARC forensic reports are enabled for the current message */
diff --git a/src/src/log.c b/src/src/log.c
index c485fc285..41f91a1ad 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -260,9 +260,6 @@ subprocess when the original process is root.
 Arguments:
   name         the file name
 
-The file name has been build in a working buffer, so it is permissible to
-overwrite it temporarily if it is necessary to create the directory.
-
 Returns:       a file descriptor, or < 0 on failure (errno set)
 */
 
@@ -286,15 +283,14 @@ problem. */
 if (fd < 0 && errno == ENOENT)
   {
   BOOL created;
-  uschar *lastslash = Ustrrchr(name, '/');
-  *lastslash = 0;
-  created = directory_make(NULL, name, LOG_DIRECTORY_MODE, FALSE);
+  const uschar * lastslash = Ustrrchr(name, '/');
+  uschar * dirname = string_copyn(name, lastslash - name);
+  created = directory_make(NULL, dirname, LOG_DIRECTORY_MODE, FALSE);
   DEBUG(D_any)
     if (created)
-      debug_printf("created log directory %s\n", name);
+      debug_printf("created log directory %s\n", dirname);
     else
-      debug_printf("failed to create log directory %s: %s\n", name, strerror(errno));
-  *lastslash = '/';
+      debug_printf("failed to create log directory %s: %s\n", dirname, strerror(errno));
   if (created) fd = Uopen(name, flags, LOG_MODE);
   }
 
diff --git a/src/src/lookups/testdb.c b/src/src/lookups/testdb.c
index 2e3ed36c2..7745bd193 100644
--- a/src/src/lookups/testdb.c
+++ b/src/src/lookups/testdb.c
@@ -109,7 +109,7 @@ static lookup_info testdb2_lookup_info = {
   .close = NULL,			/* no close function */
   .tidy = NULL,				/* no tidy function */
   .quote = testdb_quote,		/* same quoting function */
-  .version_report = testdb_version_report          /* version reporting */
+  .version_report = NULL          /* version reporting */
 };
 
 static lookup_info testdb3_lookup_info = {
@@ -121,7 +121,7 @@ static lookup_info testdb3_lookup_info = {
   .close = NULL,			/* no close function */
   .tidy = NULL,				/* no tidy function */
   .quote = NULL,			/* NO quoting function */
-  .version_report = testdb_version_report          /* version reporting */
+  .version_report = NULL          /* version reporting */
 };
 
 #ifdef DYNLOOKUP
diff --git a/src/src/macro_predef.c b/src/src/macro_predef.c
index 79cbd5290..a8ebc40d4 100644
--- a/src/src/macro_predef.c
+++ b/src/src/macro_predef.c
@@ -158,7 +158,7 @@ due to conflicts with other common macros. */
 #ifndef DISABLE_DKIM
   builtin_macro_create(US"_HAVE_DKIM");
 #endif
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
   builtin_macro_create(US"_HAVE_DMARC");
 #endif
 #ifndef DISABLE_DNSSEC
diff --git a/src/src/macros.h b/src/src/macros.h
index 75563d86b..c0900eee9 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -240,10 +240,8 @@ enum {
   ERRMESS_TOOBIG,           /* Message too big */
   ERRMESS_TOOMANYRECIP,     /* Too many recipients */
   ERRMESS_LOCAL_SCAN,       /* Rejected by local scan */
-  ERRMESS_LOCAL_ACL         /* Rejected by non-SMTP ACL */
-#ifdef SUPPORT_DMARC
- ,ERRMESS_DMARC_FORENSIC    /* DMARC Forensic Report */
-#endif
+  ERRMESS_LOCAL_ACL,        /* Rejected by non-SMTP ACL */
+  ERRMESS_DMARC_FORENSIC    /* DMARC Forensic Report */
 };
 
 /* Error handling styles - set by option, and apply only when receiving
diff --git a/src/src/miscmods/Makefile b/src/src/miscmods/Makefile
index cac4bd316..e92c34941 100644
--- a/src/src/miscmods/Makefile
+++ b/src/src/miscmods/Makefile
@@ -37,7 +37,10 @@ arc.o	arc.so:			$(HDRS) pdkim.h arc.c
 dkim.o  dkim.so:		$(HDRS) dkim.h dkim.c dkim_transport.c \
 				crypt_ver.h pdkim.h pdkim_hash.h pdkim.c \
 				signing.h signing.c
-dmarc.o dmarc.so:		$(HDRS) spf.h pdkim.h dmarc.h dmarc.c
+dmarc.o dmarc.so:		$(HDRS) spf.h pdkim.h dmarc.h \
+				dmarc.c dmarc_common.c
+dmarc_native.o dmarc_native.so:	$(HDRS) spf.h pdkim.h dmarc.h \
+				dmarc_native.c dmarc_common.c
 dummy.o:			dummy.c
 exim_filter.o exim_filter.so:	$(HDRS) exim_filter.c
 pam.o   pam.so:			$(HDRS) pam.c
@@ -52,10 +55,6 @@ spf_perl.o spf_perl.so:		$(HDRS) spf.h spf_perl.c
 # We need a single .o because that's what scripts/Configure-Makefile
 # understands and fills in to $(OBJ).
 # Try desparately to get the Solaris cc/ld to build one.
-#dkim.o:
-#	@echo "$(CC) dkim.c dkim_transport.c pdkim.c signing.c"
-#	$(FE)$(CC) -r $(LDFLAGS_PARTIAL) -o $@ $(CFLAGS) $(INCLUDE) \
-#		dkim.c dkim_transport.c pdkim.c signing.c
 dkim.o:
 	@echo "$(CC) dkim.c dkim_transport.c pdkim.c signing.c"
 	$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) dkim.c
@@ -86,6 +85,22 @@ spf_perl.so:
 			-DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) \
 			$(DLFLAGS) spf_perl.c -o spf.so
 
+# DMARC
+dmarc.o dmarc_native.o:
+	@echo "$(CC) $*.c dmarc_common.c"
+	$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) $*.c
+	$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) dmarc_common.c
+	$(FE)mv $@ dmarc_tmp.o
+	$(FE)ld -r -o $@ $(LDFLAGS_PARTIAL) dmarc_tmp.o dmarc_common.o
+
+# dmarc_native is special in the same way as spf_perl
+dmarc.so dmarc_native.so:
+	@echo "$(CC) -shared $*.c dmarc_common.c"
+	$(FE)$(CC) -DDYNLOOKUP $(CFLAGS_DYNAMIC) -o dmarc.so \
+		$(SUPPORT_$*_INCLUDE) $(SUPPORT_$*_LIBS) \
+		$(CFLAGS) $(INCLUDE) $(TLS_INCLUDE) $(DLFLAGS) \
+		$*.c dmarc_common.c
+
 # Compile instructions for static perl.o for when EXIM_PERL is set
 # Dynamic is managed all via scripts/Configure-Makefile
 
diff --git a/src/src/miscmods/arc.c b/src/src/miscmods/arc.c
index 4767d45bf..cdd746547 100644
--- a/src/src/miscmods/arc.c
+++ b/src/src/miscmods/arc.c
@@ -2090,7 +2090,7 @@ return g;
 }
 
 
-#  ifdef SUPPORT_DMARC
+#  ifdef EXIM_HAVE_DMARC
 
 /* Module API: obtain ARC info for DMARC history.
 Arguments:
@@ -2144,7 +2144,7 @@ static void * arc_functions[] = {
   [ARC_STATE_IS_PASS] =	(void *) arc_is_pass,
   [ARC_SIGN_INIT] =	(void *) arc_sign_init,
   [ARC_SIGN] =		(void *) arc_sign,
-# ifdef SUPPORT_DMARC
+# ifdef EXIM_HAVE_DMARC
   [ARC_ARCSET_INFO] =	(void *) arc_arcset_string,
 # endif
 };
diff --git a/src/src/miscmods/dkim.c b/src/src/miscmods/dkim.c
index 4a2c50516..ccb649ca1 100644
--- a/src/src/miscmods/dkim.c
+++ b/src/src/miscmods/dkim.c
@@ -1137,7 +1137,7 @@ expand_bad:
 
 
 
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
 
 /* Module API */
 
@@ -1331,7 +1331,7 @@ static void * dkim_functions[] = {
   [DKIM_TRANSPORT_INIT] =	(void *) dkim_exim_sign_init,
   [DKIM_TRANSPORT_WRITE] =	(void *) dkim_transport_write_message,
 
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
   [DKIM_SIGS_LIST] =		(void *) dkim_sigs_list,
 #endif
 #ifdef EXPERIMENTAL_ARC
diff --git a/src/src/miscmods/dmarc.c b/src/src/miscmods/dmarc.c
index 185ef9f4f..be5d68c3a 100644
--- a/src/src/miscmods/dmarc.c
+++ b/src/src/miscmods/dmarc.c
@@ -26,21 +26,13 @@
 #  include "dmarc.h"
 #  include "pdkim.h"
 
+extern void dmarc_send_forensic_report(const uschar **);
+extern uschar * dmarc_dns_lookup(const uschar *);
+extern void dmarc_write_history_file(const gstring *);
+
 OPENDMARC_LIB_T     dmarc_ctx;
-DMARC_POLICY_T     *dmarc_pctx = NULL;
-OPENDMARC_STATUS_T  libdm_status, action, dmarc_policy;
-OPENDMARC_STATUS_T  da, sa, action;
-BOOL dmarc_abort  = FALSE;
-uschar *dmarc_pass_fail = US"skipped";
-header_line *from_header   = NULL;
-
-static misc_module_info * dmarc_dkim_mod_info;
-static misc_module_info * dmarc_spf_mod_info;
-int dmarc_spf_ares_result  = ARES_RESULT_UNDEFINED;
-uschar *spf_sender_domain  = NULL;
-uschar *spf_human_readable = NULL;
-u_char *header_from_sender = NULL;
-int history_file_status    = DMARC_HIST_OK;
+DMARC_POLICY_T     *dmarc_pctx;
+OPENDMARC_STATUS_T  libdm_status;
 
 typedef struct dmarc_exim_p {
   uschar *name;
@@ -58,41 +50,24 @@ static dmarc_exim_p dmarc_policy_description[] = {
 
 
 /* $variables */
-BOOL	 dmarc_alignment_dkim	 = FALSE; /* Subtest result */
-BOOL	 dmarc_alignment_spf	 = FALSE; /* Subtest result */
-const uschar * dmarc_domain_policy = NULL; /* Declared policy of used domain */
-const uschar * dmarc_status;		/* One word value */
-const uschar * dmarc_status_text   = NULL; /* Human readable value */
-uschar * dmarc_used_domain;		/* Domain libopendmarc chose for DMARC policy lookup */
+extern BOOL	 dmarc_alignment_dkim;	   /* Subtest result */
+extern BOOL	 dmarc_alignment_spf;	   /* Subtest result */
+extern const uschar * dmarc_domain_policy; /* Declared policy of used domain */
+extern const uschar * dmarc_status;	   /* One word value */
+extern const uschar * dmarc_status_text;   /* Human readable value */
+extern uschar * dmarc_used_domain;
 
 /* options */
-uschar * dmarc_forensic_sender   = NULL; /* Set sender address for forensic reports */
-uschar * dmarc_history_file      = NULL; /* File to store dmarc results */
-uschar * dmarc_tld_file          = NULL; /* Mozilla TLDs text file */
-
+extern uschar * dmarc_forensic_sender;	/* Set sender address for forensic reports */
+extern uschar * dmarc_history_file;	/* File to store dmarc results */
+extern uschar * dmarc_tld_file;		/* Mozilla TLDs text file */
 
-/* One-time initialisation for dmarc.  Ensure the spf module is available. */
-
-static BOOL
-dmarc_init(void * dummy)
-{
-uschar * errstr;
-if (!(dmarc_spf_mod_info = misc_mod_find(US"spf", &errstr)))
-  {
-  log_write(0, LOG_MAIN|LOG_PANIC, "dmarc: %s", errstr);
-  return FALSE;
-  }
 
-if (!(dmarc_dkim_mod_info = misc_mod_find(US"dkim", &errstr)))
-  {
-  log_write(0, LOG_MAIN|LOG_PANIC, "dmarc: %s", errstr);
-  return FALSE;
-  }
+void
+dmarc_local_init(void)
+{}
 
-return TRUE;
-}
-
-static gstring *
+gstring *
 dmarc_version_report(gstring * g)
 {
 return string_fmt_append(g, "Library version: dmarc: Compile: %d.%d.%d.%d\n",
@@ -103,58 +78,10 @@ return string_fmt_append(g, "Library version: dmarc: Compile: %d.%d.%d.%d\n",
 }
 
 
-/* Accept an error_block struct, initialize if empty, parse to the
-end, and append the two strings passed to it.  Used for adding
-variable amounts of value:pair data to the forensic emails. */
-
-static error_block *
-add_to_eblock(error_block * eblock, const uschar * t1, const uschar * t2)
+void
+dmarc_local_msg_init(void)
 {
-error_block * eb = store_malloc(sizeof(error_block));
-if (!eblock)
-  eblock = eb;
-else
-  {
-  /* Find the end of the eblock struct and point it at eb */
-  error_block *tmp = eblock;
-  while(tmp->next)
-    tmp = tmp->next;
-  tmp->next = eb;
-  }
-eb->text1 = t1;
-eb->text2 = t2;
-eb->next  = NULL;
-return eblock;
-}
-
-/* dmarc_msg_init sets up a context that can be re-used for several
-messages on the same SMTP connection (that come from the
-same host with the same HELO string).
-However, we seem to only use it for one; we destroy some sort of context
-at the tail end of dmarc_process(). */
-
-static int
-dmarc_msg_init()
-{
-int *netmask   = NULL;   /* Ignored */
-uschar * s;
-
-/* Set some sane defaults.  Also clears previous results when
-multiple messages in one connection. */
-
 dmarc_pctx         = NULL;
-dmarc_status       = US"none";
-dmarc_abort        = FALSE;
-dmarc_pass_fail    = US"skipped";
-dmarc_used_domain  = US"";
-f.dmarc_has_been_checked = FALSE;
-header_from_sender = NULL;
-spf_sender_domain  = NULL;
-spf_human_readable = NULL;
-
-/* ACLs have "control=dmarc_disable_verify" */
-if (f.dmarc_disable_verify)
-  return OK;
 
 (void) memset(&dmarc_ctx, '\0', sizeof dmarc_ctx);
 dmarc_ctx.nscount = 0;
@@ -165,27 +92,17 @@ if (libdm_status != DMARC_PARSE_OKAY)
 		       opendmarc_policy_status_to_str(libdm_status));
   dmarc_abort = TRUE;
   }
-GET_OPTION("dmarc_tld_file");
-if (!(s = dmarc_tld_file) || !(s = expand_string(s)) || !*s)
-  {
-  DEBUG(D_receive) debug_printf_indent("DMARC: no dmarc_tld_file\n");
-  dmarc_abort = TRUE;
-  }
 else if (opendmarc_tld_read_file(CS dmarc_tld_file, NULL, NULL, NULL))
   {
   log_write(0, LOG_MAIN|LOG_PANIC, "DMARC failure to load tld list '%s': %s",
 		       dmarc_tld_file, strerror(errno));
   dmarc_abort = TRUE;
   }
-if (!sender_host_address)
-  {
-  DEBUG(D_receive) debug_printf_indent("DMARC: no sender_host_address\n");
-  dmarc_abort = TRUE;
-  }
+
 /* This catches locally originated email and startup errors above. */
 if (!dmarc_abort)
   {
-  int is_ipv6 = string_is_ip_address(sender_host_address, netmask) == 6;
+  int is_ipv6 = string_is_ip_address(sender_host_address, NULL) == 6;
   if (!(dmarc_pctx = opendmarc_policy_connect_init(sender_host_address, is_ipv6)))
     {
     log_write(0, LOG_MAIN|LOG_PANIC,
@@ -193,244 +110,39 @@ if (!dmarc_abort)
     dmarc_abort = TRUE;
     }
   }
-
-return OK;
-}
-
-
-static void
-dmarc_smtp_reset(void)
-{
-f.dmarc_has_been_checked = f.dmarc_disable_verify =
-f.dmarc_enable_forensic = FALSE;
-dmarc_domain_policy = dmarc_status = dmarc_status_text =
-dmarc_used_domain = NULL;
-}
-
-
-/* dmarc_store_data stores the header data so that subsequent dmarc_process can
-access the data.
-Called after the entire message has been received, with the From: header. */
-
-static int
-dmarc_store_data(header_line * hdr)
-{
-/* No debug output because would change every test debug output */
-if (!f.dmarc_disable_verify)
-  from_header = hdr;
-return OK;
 }
 
 
 static void
-dmarc_send_forensic_report(u_char ** ruf)
+dmarc_local_send_forensic_report(u_char ** ruf)
 {
-uschar * recipient;
-error_block * eblock = NULL;
-FILE *message_file = NULL;
-
 /* Earlier ACL does not have *required* control=dmarc_enable_forensic */
 if (!f.dmarc_enable_forensic)
   return;
 
-if (  dmarc_policy == DMARC_POLICY_REJECT     && action == DMARC_RESULT_REJECT
-   || dmarc_policy == DMARC_POLICY_QUARANTINE && action == DMARC_RESULT_QUARANTINE
-   || dmarc_policy == DMARC_POLICY_NONE       && action == DMARC_RESULT_REJECT
-   || dmarc_policy == DMARC_POLICY_NONE       && action == DMARC_RESULT_QUARANTINE
+if (   dmarc_policy == DMARC_POLICY_REJECT
+    && dmarc_action == DMARC_RESULT_REJECT
+   ||  dmarc_policy == DMARC_POLICY_QUARANTINE
+    && dmarc_action == DMARC_RESULT_QUARANTINE
+   ||  dmarc_policy == DMARC_POLICY_NONE
+    && dmarc_action == DMARC_RESULT_REJECT
+   ||  dmarc_policy == DMARC_POLICY_NONE
+    && dmarc_action == DMARC_RESULT_QUARANTINE
    )
   if (ruf)
-    {
-    eblock = add_to_eblock(eblock, US"Sender Domain", dmarc_used_domain);
-    eblock = add_to_eblock(eblock, US"Sender IP Address", sender_host_address);
-    eblock = add_to_eblock(eblock, US"Received Date", tod_stamp(tod_full));
-    eblock = add_to_eblock(eblock, US"SPF Alignment",
-		     sa == DMARC_POLICY_SPF_ALIGNMENT_PASS ? US"yes" : US"no");
-    eblock = add_to_eblock(eblock, US"DKIM Alignment",
-		     da == DMARC_POLICY_DKIM_ALIGNMENT_PASS ? US"yes" : US"no");
-    eblock = add_to_eblock(eblock, US"DMARC Results", dmarc_status_text);
-
-    for (int c = 0; ruf[c]; c++)
-      {
-      recipient = string_copylc(ruf[c]);
-      if (Ustrncmp(recipient, "mailto:",7))
-	continue;
-      /* Move to first character past the colon */
-      recipient += 7;
-      DEBUG(D_receive)
-	debug_printf_indent("DMARC forensic report to %s%s\n", recipient,
-	     (host_checking || f.running_in_test_harness) ? " (not really)" : "");
-      if (host_checking || f.running_in_test_harness)
-	continue;
-
-      if (!moan_send_message(recipient, ERRMESS_DMARC_FORENSIC, eblock,
-			    header_list, message_file, NULL))
-	log_write(0, LOG_MAIN|LOG_PANIC,
-	  "failure to send DMARC forensic report to %s", recipient);
-      }
-    }
-}
-
-
-/* Look up a DNS dmarc record for the given domain.  Return it or NULL */
-
-static uschar *
-dmarc_dns_lookup(uschar * dom)
-{
-dns_answer * dnsa = store_get_dns_answer();
-dns_scan dnss;
-uschar * res = NULL;
-
-expand_level++;
-
-if (dns_lookup(dnsa, string_sprintf("_dmarc.%s", dom), T_TXT, NULL)
-    == DNS_SUCCEED)
-  for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
-       rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
-    if (rr->type == T_TXT && rr->size > 3)
-      res = string_copyn_taint(US rr->data, rr->size, GET_TAINTED);
-
-expand_level--;
-store_free_dns_answer(dnsa);
-return res;
+    dmarc_send_forensic_report(CUSS ruf);
 }
 
 
-static int
-dmarc_write_history_file(const gstring * dkim_history_buffer)
-{
-int history_file_fd = 0, tmp_ans;
-u_char ** rua; /* aggregate report addressees */
-uschar * s;
-gstring * g;
 
-GET_OPTION("dmarc_history_file");
-if (!(s = dmarc_history_file) || !(s = expand_string(s)) || !*s)
-  {
-  DEBUG(D_receive) debug_printf_indent("DMARC history file not set\n");
-  return DMARC_HIST_DISABLED;
-  }
-if (!host_checking)
-  /* Ensure we use a modifiiable copy for the filename */
-  if ((history_file_fd =
-	log_open_as_exim(s == dmarc_history_file ? string_copy(s) : s)) < 0)
-    {
-    log_write(0, LOG_MAIN|LOG_PANIC,
-	      "failure to create DMARC history file: %s: %s",
-	      s, strerror(errno));
-    return DMARC_HIST_FILE_ERR;
-    }
-
-/* Generate the contents of the history file entry */
-
-g = string_fmt_append(NULL,
-  "job %s\nreporter %s\nreceived %ld\nipaddr %s\nfrom %s\nmfrom %s\n",
-  message_id, primary_hostname, time(NULL), sender_host_address,
-  header_from_sender, expand_string(US"$sender_address_domain"));
-
-if (dmarc_spf_ares_result != ARES_RESULT_UNDEFINED)
-  g = string_fmt_append(g, "spf %d\n", dmarc_spf_ares_result);
-
-if (dkim_history_buffer)
-  g = string_fmt_append(g, "%Y", dkim_history_buffer);
-
-g = string_fmt_append(g, "pdomain %s\npolicy %d\n",
-  dmarc_used_domain, dmarc_policy);
-
-if ((rua = opendmarc_policy_fetch_rua(dmarc_pctx, NULL, 0, 1)))
-  for (tmp_ans = 0; rua[tmp_ans]; tmp_ans++)
-    g = string_fmt_append(g, "rua %s\n", rua[tmp_ans]);
-else
-  g = string_catn(g, US"rua -\n", 6);
-
-opendmarc_policy_fetch_pct(dmarc_pctx, &tmp_ans);
-g = string_fmt_append(g, "pct %d\n", tmp_ans);
-
-opendmarc_policy_fetch_adkim(dmarc_pctx, &tmp_ans);
-g = string_fmt_append(g, "adkim %d\n", tmp_ans);
-
-opendmarc_policy_fetch_aspf(dmarc_pctx, &tmp_ans);
-g = string_fmt_append(g, "aspf %d\n", tmp_ans);
-
-opendmarc_policy_fetch_p(dmarc_pctx, &tmp_ans);
-g = string_fmt_append(g, "p %d\n", tmp_ans);
-
-opendmarc_policy_fetch_sp(dmarc_pctx, &tmp_ans);
-g = string_fmt_append(g, "sp %d\n", tmp_ans);
-
-g = string_fmt_append(g, "align_dkim %d\nalign_spf %d\naction %d\n",
-  da, sa, action);
-
-#if DMARC_API >= 100400
-# ifdef EXPERIMENTAL_ARC
-  {
-  const misc_module_info * mi = misc_mod_findonly(US"arc");
-  const uschar * t;
-  gstring * g2 = NULL;
-  typedef const uschar * (*fn_t)(gstring **);
-
-  if (mi && (t = (((fn_t *) mi->functions)[ARC_ARCSET_INFO]) (&g2)))
-    {
-    int i = Ustrcmp(t, "pass") == 0 ? ARES_RESULT_PASS
-	    : Ustrcmp(t, "fail") == 0 ? ARES_RESULT_FAIL
-	    : ARES_RESULT_UNKNOWN;
-
-    g = string_fmt_append(g, "arc %d\n"
-			     "arc_policy %d json[%#Y ]\n",
-			  i,
-			  i == ARES_RESULT_PASS ? DMARC_ARC_POLICY_RESULT_PASS
-			  : i == ARES_RESULT_FAIL ? DMARC_ARC_POLICY_RESULT_FAIL
-			  : DMARC_ARC_POLICY_RESULT_UNUSED,
-			  g2
-			  );
-    }
-  else
-    string_fmt_append(g, "arc %d\narc_policy %d json:[]\n",
-			ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED);
-  }
-
-# else
-g = string_fmt_append(g, "arc %d\narc_policy %d json:[]\n",
-		      ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED);
-# endif
-#endif
-
-/* Write the contents to the history file */
-DEBUG(D_receive)
-  {
-  debug_printf_indent("DMARC logging history data for opendmarc reporting%s\n",
-	     host_checking ? " (not really)" : "");
-  debug_printf_indent("DMARC history data for debugging:\n");
-  expand_level++;
-    debug_printf_indent("%Y", g);
-  expand_level--;
-  }
-
-if (!host_checking)
-  {
-  ssize_t written_len = write_to_fd_buf(history_file_fd,
-				g->s,
-				gstring_length(g));
-  if (written_len == 0)
-    {
-    log_write(0, LOG_MAIN|LOG_PANIC, "failure to write to DMARC history file: %s",
-			   dmarc_history_file);
-    return DMARC_HIST_WRITE_ERR;
-    }
-  (void)close(history_file_fd);
-  }
-return DMARC_HIST_OK;
-}
-
-
-/* dmarc_process adds the envelope sender address to the existing
+/*API: dmarc_process adds the envelope sender address to the existing
 context (if any), retrieves the result, sets up expansion
 strings and evaluates the condition outcome.
 Called for the first ACL dmarc= condition. */
 
-static int
+int
 dmarc_process(void)
 {
-int sr = SPF_RESULT_INVALID, origin;	/* used in SPF section */
 int dmarc_spf_result;			/* stores spf into dmarc conn ctx */
 int tmp_ans, c;
 uschar * rr;
@@ -444,44 +156,44 @@ if (f.dmarc_disable_verify)
   return OK;
 
 /* Store the header From: sender domain for this part of DMARC.
-If there is no from_header struct, then it's likely this message
+If there is no from_header string, then it's likely this message
 is locally generated and relying on fixups to add it.  Just skip
 the entire DMARC system if we can't find a From: header....or if
 there was a previous error.  */
 
-if (!from_header)
+if (!dmarc_from_header)
   {
   DEBUG(D_receive) debug_printf_indent("DMARC: no From: header\n");
   dmarc_abort = TRUE;
   }
 else if (!dmarc_abort)
   {
+  const uschar * end_addr, * s;
   uschar * errormsg;
   int dummy, domain;
-  uschar * p;
-  uschar saveend;
 
   f.parse_allow_group = TRUE;
-  p = parse_find_address_end(from_header->text, FALSE);
-  saveend = *p; *p = '\0';
-  if ((header_from_sender = parse_extract_address(from_header->text, &errormsg,
+  end_addr = parse_find_address_end(dmarc_from_header, FALSE);
+  s = *end_addr
+      ? string_copyn(dmarc_from_header, end_addr - dmarc_from_header)
+      : dmarc_from_header;
+  if ((dmarc_header_from_sender = parse_extract_address(s, &errormsg,
 			      &dummy, &dummy, &domain, FALSE)))
-    header_from_sender += domain;
-  *p = saveend;
+    dmarc_header_from_sender += domain;
 
   /* The opendmarc library extracts the domain from the email address, but
   only try to store it if it's not empty.  Otherwise, skip out of DMARC. */
 
-  if (!header_from_sender || (strcmp( CCS header_from_sender, "") == 0))
+  if (!dmarc_header_from_sender || !*dmarc_header_from_sender)
     dmarc_abort = TRUE;
   libdm_status = dmarc_abort
     ? DMARC_PARSE_OKAY
-    : opendmarc_policy_store_from_domain(dmarc_pctx, header_from_sender);
+    : opendmarc_policy_store_from_domain(dmarc_pctx, dmarc_header_from_sender);
   if (libdm_status != DMARC_PARSE_OKAY)
     {
     log_write(0, LOG_MAIN|LOG_PANIC,
 	      "failure to store header From: in DMARC: %s, header was '%s'",
-	      opendmarc_policy_status_to_str(libdm_status), from_header->text);
+	      opendmarc_policy_status_to_str(libdm_status), dmarc_from_header);
     dmarc_abort = TRUE;
     }
   }
@@ -491,7 +203,8 @@ instead do this in the ACLs.  */
 
 if (!dmarc_abort && !sender_host_authenticated)
   {
-  uschar * dmarc_domain;
+  int sr = SPF_RESULT_INVALID, origin;
+  uschar * spf_human_readable = NULL, * spf_sender_domain;
   gstring * dkim_history_buffer = NULL;
   typedef const pdkim_signature * (*sigs_fn_t)(void);
 
@@ -511,14 +224,11 @@ if (!dmarc_abort && !sender_host_authenticated)
     /* No spf data means null envelope sender so generate a domain name
     from the sender_helo_name  */
 
-    if (!spf_sender_domain)
+    if (!spf_sender_domain || !*spf_sender_domain)
       {
       spf_sender_domain = sender_helo_name;
       log_write(0, LOG_MAIN, "DMARC using synthesized SPF sender domain = %s\n",
 			     spf_sender_domain);
-      DEBUG(D_receive)
-	debug_printf_indent("DMARC using synthesized SPF sender domain = %s\n",
-	  spf_sender_domain);
       }
     dmarc_spf_result = DMARC_POLICY_SPF_OUTCOME_NONE;
     dmarc_spf_ares_result = ARES_RESULT_UNKNOWN;
@@ -544,7 +254,7 @@ if (!dmarc_abort && !sender_host_authenticated)
     DEBUG(D_receive) debug_printf_indent("DMARC using SPF sender domain = %s\n",
 					spf_sender_domain);
     }
-  if (strcmp( CCS spf_sender_domain, "") == 0)
+  if (!*spf_sender_domain)
     dmarc_abort = TRUE;
   if (!dmarc_abort)
     {
@@ -610,24 +320,24 @@ The EDITME provides a DMARC_API variable */
   our dns access path is used for debug tracing and for the testsuite
   diversion. */
 
-  libdm_status = (rr = dmarc_dns_lookup(header_from_sender))
-    ? opendmarc_policy_store_dmarc(dmarc_pctx, rr, header_from_sender, NULL)
+  libdm_status = (rr = dmarc_dns_lookup(dmarc_header_from_sender))
+    ? opendmarc_policy_store_dmarc(dmarc_pctx, rr, dmarc_header_from_sender, NULL)
     : DMARC_DNS_ERROR_NO_RECORD;
   switch (libdm_status)
     {
     case DMARC_DNS_ERROR_NXDOMAIN:
     case DMARC_DNS_ERROR_NO_RECORD:
       DEBUG(D_receive)
-	debug_printf_indent("DMARC no record found for %s\n", header_from_sender);
+	debug_printf_indent("DMARC no record found for %s\n", dmarc_header_from_sender);
       has_dmarc_record = FALSE;
       break;
     case DMARC_PARSE_OKAY:
       DEBUG(D_receive)
-	debug_printf_indent("DMARC record found for %s\n", header_from_sender);
+	debug_printf_indent("DMARC record found for %s\n", dmarc_header_from_sender);
       break;
     case DMARC_PARSE_ERROR_BAD_VALUE:
       DEBUG(D_receive)
-	debug_printf_indent("DMARC record parse error for %s\n", header_from_sender);
+	debug_printf_indent("DMARC record parse error for %s\n", dmarc_header_from_sender);
       has_dmarc_record = FALSE;
       break;
     default:
@@ -635,7 +345,7 @@ The EDITME provides a DMARC_API variable */
       DEBUG(D_receive)
 	debug_printf_indent("DMARC skipping (%s), unsure what to do with %s",
 		      opendmarc_policy_status_to_str(libdm_status),
-		      from_header->text);
+		      dmarc_from_header);
       has_dmarc_record = FALSE;
       break;
     }
@@ -650,11 +360,10 @@ The EDITME provides a DMARC_API variable */
   /* Can't use exim's string manipulation functions so allocate memory
   for libopendmarc using its max hostname length definition. */
 
-  dmarc_domain = store_get(DMARC_MAXHOSTNAMELEN, GET_TAINTED);
+  dmarc_used_domain = store_get(DMARC_MAXHOSTNAMELEN, GET_TAINTED);
   libdm_status = opendmarc_policy_fetch_utilized_domain(dmarc_pctx,
-    dmarc_domain, DMARC_MAXHOSTNAMELEN-1);
-  store_release_above(dmarc_domain + Ustrlen(dmarc_domain)+1);
-  dmarc_used_domain = dmarc_domain;
+    dmarc_used_domain, DMARC_MAXHOSTNAMELEN-1);
+  store_release_above(dmarc_used_domain + Ustrlen(dmarc_used_domain)+1);
 
   if (libdm_status != DMARC_PARSE_OKAY)
     log_write(0, LOG_MAIN|LOG_PANIC,
@@ -662,61 +371,65 @@ The EDITME provides a DMARC_API variable */
       opendmarc_policy_status_to_str(libdm_status));
 
   dmarc_policy = libdm_status = opendmarc_get_policy_to_enforce(dmarc_pctx);
+
   switch(libdm_status)
     {
-    case DMARC_POLICY_ABSENT:     /* No DMARC record found */
+    case DMARC_POLICY_ABSENT:		/* No DMARC record found */
       dmarc_status = US"norecord";
       dmarc_pass_fail = US"none";
       dmarc_status_text = US"No DMARC record";
-      action = DMARC_RESULT_ACCEPT;
+      dmarc_action = DMARC_RESULT_ACCEPT;
       break;
-    case DMARC_FROM_DOMAIN_ABSENT:    /* No From: domain */
+    case DMARC_FROM_DOMAIN_ABSENT:	/* No From: domain */
       dmarc_status = US"nofrom";
       dmarc_pass_fail = US"temperror";
       dmarc_status_text = US"No From: domain found";
-      action = DMARC_RESULT_ACCEPT;
+      dmarc_action = DMARC_RESULT_ACCEPT;
       break;
-    case DMARC_POLICY_NONE:       /* Accept and report */
+    case DMARC_POLICY_NONE:		/* Accept and report */
       dmarc_status = US"none";
       dmarc_pass_fail = US"none";
       dmarc_status_text = US"None, Accept";
-      action = DMARC_RESULT_ACCEPT;
+      dmarc_action = DMARC_RESULT_ACCEPT;
       break;
-    case DMARC_POLICY_PASS:       /* Explicit accept */
+    case DMARC_POLICY_PASS:		/* Explicit accept */
       dmarc_status = US"accept";
       dmarc_pass_fail = US"pass";
       dmarc_status_text = US"Accept";
-      action = DMARC_RESULT_ACCEPT;
+      dmarc_action = DMARC_RESULT_ACCEPT;
       break;
-    case DMARC_POLICY_REJECT:       /* Explicit reject */
+    case DMARC_POLICY_REJECT:		/* Explicit reject */
       dmarc_status = US"reject";
       dmarc_pass_fail = US"fail";
       dmarc_status_text = US"Reject";
-      action = DMARC_RESULT_REJECT;
+      dmarc_action = DMARC_RESULT_REJECT;
       break;
-    case DMARC_POLICY_QUARANTINE:       /* Explicit quarantine */
+    case DMARC_POLICY_QUARANTINE:	/* Explicit quarantine */
       dmarc_status = US"quarantine";
       dmarc_pass_fail = US"fail";
       dmarc_status_text = US"Quarantine";
-      action = DMARC_RESULT_QUARANTINE;
+      dmarc_action = DMARC_RESULT_QUARANTINE;
       break;
     default:
       dmarc_status = US"temperror";
       dmarc_pass_fail = US"temperror";
       dmarc_status_text = US"Internal Policy Error";
-      action = DMARC_RESULT_TEMPFAIL;
+      dmarc_action = DMARC_RESULT_TEMPFAIL;
       break;
     }
 
-  libdm_status = opendmarc_policy_fetch_alignment(dmarc_pctx, &da, &sa);
+  libdm_status = opendmarc_policy_fetch_alignment(dmarc_pctx,
+		    &dmarc_dkim_alignment, &dmarc_spf_alignment);
   if (libdm_status != DMARC_PARSE_OKAY)
     log_write(0, LOG_MAIN|LOG_PANIC, "failure to read DMARC alignment: %s",
 			     opendmarc_policy_status_to_str(libdm_status));
 
   if (has_dmarc_record)
     {
-    dmarc_alignment_spf = sa == DMARC_POLICY_SPF_ALIGNMENT_PASS;
-    dmarc_alignment_dkim = da == DMARC_POLICY_DKIM_ALIGNMENT_PASS;
+    dmarc_alignment_spf =
+      dmarc_spf_alignment == DMARC_POLICY_SPF_ALIGNMENT_PASS;
+    dmarc_alignment_dkim =
+      dmarc_dkim_alignment == DMARC_POLICY_DKIM_ALIGNMENT_PASS;
 
     log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s "
 			   "spf_align=%s dkim_align=%s enforcement='%s'",
@@ -724,10 +437,18 @@ The EDITME provides a DMARC_API variable */
 			   dmarc_alignment_spf  ? "yes" : "no",
 			   dmarc_alignment_dkim ? "yes" : "no",
 			   dmarc_status_text);
-    history_file_status = dmarc_write_history_file(dkim_history_buffer);
+
+    dmarc_rua = USS opendmarc_policy_fetch_rua(dmarc_pctx, NULL, 0, 1);
+    opendmarc_policy_fetch_pct(dmarc_pctx, &dmarc_pct);
+    opendmarc_policy_fetch_adkim(dmarc_pctx, &dmarc_adkim);
+    opendmarc_policy_fetch_aspf(dmarc_pctx, &dmarc_aspf);
+    opendmarc_policy_fetch_p(dmarc_pctx, &dmarc_dom_policy);
+    opendmarc_policy_fetch_sp(dmarc_pctx, &dmarc_subdom_policy);
+    dmarc_write_history_file(dkim_history_buffer);
+
     /* Now get the forensic reporting addresses, if any */
     ruf = opendmarc_policy_fetch_ruf(dmarc_pctx, NULL, 0, 1);
-    dmarc_send_forensic_report(ruf);
+    dmarc_local_send_forensic_report(ruf);
     }
   }
 
@@ -746,7 +467,8 @@ dmarc_exim_expand_defaults(void)
 return f.dmarc_disable_verify ?  US"off" : US"none";
 }
 
-static const uschar *
+/*API*/
+const uschar *
 dmarc_exim_expand_query(void)
 {
 if (f.dmarc_disable_verify || !dmarc_pctx)
@@ -756,75 +478,6 @@ return dmarc_status;
 }
 
 
-static gstring *
-authres_dmarc(gstring * g)
-{
-if (f.dmarc_has_been_checked)
-  {
-  int start = 0;		/* Compiler quietening */
-  DEBUG(D_acl) start = gstring_length(g);
-  g = string_append(g, 2, US";\n\tdmarc=", dmarc_pass_fail);
-  if (header_from_sender)
-    g = string_append(g, 2, US" header.from=", header_from_sender);
-  DEBUG(D_acl) debug_printf_indent("DMARC:\tauthres '%.*s'\n",
-		  gstring_length(g) - start - 3, g->s + start + 3);
-  }
-else
-  DEBUG(D_acl) debug_printf_indent("DMARC:\tno authres\n");
-return g;
-}
-
-/******************************************************************************/
-/* Module API */
-
-static optionlist dmarc_options[] = {
-  { "dmarc_forensic_sender",    opt_stringptr,      {&dmarc_forensic_sender} },
-  { "dmarc_history_file",       opt_stringptr,      {&dmarc_history_file} },
-  { "dmarc_tld_file",           opt_stringptr,      {&dmarc_tld_file} },
-};
-
-static void * dmarc_functions[] = {
-  [DMARC_PROCESS] =	(void *) dmarc_process,
-  [DMARC_EXPAND_QUERY] = (void *) dmarc_exim_expand_query,
-  [DMARC_STORE_DATA] =	(void *) dmarc_store_data,
-};
-
-/* dmarc_forensic_sender is provided for visibility of the the option setting
-by moan_send_message. We do not document it as a config-visible $variable.
-We could provide it via a function but there's little advantage. */
-
-static var_entry dmarc_variables[] = {
-  { "dmarc_alignment_dkim",	vtype_bool,		&dmarc_alignment_dkim },
-  { "dmarc_alignment_spf",	vtype_bool,		&dmarc_alignment_spf },
-  { "dmarc_domain_policy",	vtype_stringptr,	&dmarc_domain_policy },
-  { "dmarc_forensic_sender",	vtype_stringptr,	&dmarc_forensic_sender},
-  { "dmarc_status",		vtype_stringptr,	&dmarc_status },
-  { "dmarc_status_text",	vtype_stringptr,	&dmarc_status_text },
-  { "dmarc_used_domain",	vtype_stringptr,	&dmarc_used_domain },
-};
-
-misc_module_info dmarc_module_info =
-{
-  .name =		US"dmarc",
-# ifdef DYNLOOKUP
-  .dyn_magic =		MISC_MODULE_MAGIC,
-# endif
-  .init =		dmarc_init,
-  .lib_vers_report =	dmarc_version_report,
-  .smtp_reset =		dmarc_smtp_reset,
-  .msg_init =		dmarc_msg_init,
-  .authres =		authres_dmarc,
-
-  .options =		dmarc_options,
-  .options_count =	nelem(dmarc_options),
-
-  .functions =		dmarc_functions,
-  .functions_count =	nelem(dmarc_functions),
-
-  .variables =		dmarc_variables,
-  .variables_count =	nelem(dmarc_variables),
-};
-
 # endif /* SUPPORT_SPF */
 #endif /* SUPPORT_DMARC */
 /* vi: aw ai sw=2
diff --git a/src/src/miscmods/dmarc.h b/src/src/miscmods/dmarc.h
index c1cafd0d1..6f8c456dd 100644
--- a/src/src/miscmods/dmarc.h
+++ b/src/src/miscmods/dmarc.h
@@ -2,8 +2,8 @@
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Experimental DMARC support.
-   Copyright (c) The Exim Maintainers 2021 - 2023
+/* DMARC support.
+   Copyright (c) The Exim Maintainers 2021 - 2025
    Copyright (c) Todd Lyons <tlyons@exim.org> 2012 - 2014
    License: GPL */
 /* SPDX-License-Identifier: GPL-2.0-or-later */
@@ -11,14 +11,54 @@
 /* Portions Copyright (c) 2012, 2013, The Trusted Domain Project;
    All rights reserved, licensed for use per LICENSE.opendmarc. */
 
-#ifdef SUPPORT_DMARC
+#ifdef EXPERIMENTAL_DMARC_NATIVE
+# define EXIM_HAVE_DMARC EXPERIMENTAL_DMARC_NATIVE
+# define DMARC_SUPPORTS_ARC
+
+/* from opendmarc/dmarc.h */
+# define DMARC_MAXHOSTNAMELEN    256
+
+# define DMARC_POLICY_ABSENT     14
+# define DMARC_POLICY_PASS       15
+# define DMARC_POLICY_REJECT     16
+# define DMARC_POLICY_QUARANTINE 17
+# define DMARC_POLICY_NONE       18
+# define DMARC_USED_POLICY_IS_P  19
+# define DMARC_USED_POLICY_IS_SP 20
+
+# define DMARC_POLICY_SPF_ORIGIN_MAILFROM       1
+# define DMARC_POLICY_SPF_ORIGIN_HELO           2
+
+# define DMARC_POLICY_SPF_OUTCOME_NONE          0
+# define DMARC_POLICY_SPF_OUTCOME_PASS          1
+# define DMARC_POLICY_SPF_OUTCOME_FAIL          2
+# define DMARC_POLICY_SPF_OUTCOME_TMPFAIL       3
+# define DMARC_POLICY_SPF_ALIGNMENT_PASS        4
+# define DMARC_POLICY_SPF_ALIGNMENT_FAIL        5
+
+# define DMARC_POLICY_DKIM_OUTCOME_NONE         0
+# define DMARC_POLICY_DKIM_OUTCOME_PASS         1
+# define DMARC_POLICY_DKIM_OUTCOME_FAIL         2
+# define DMARC_POLICY_DKIM_OUTCOME_TMPFAIL      3
+# define DMARC_POLICY_DKIM_ALIGNMENT_PASS       4
+# define DMARC_POLICY_DKIM_ALIGNMENT_FAIL       5
+
+
+
+#elif defined(SUPPORT_DMARC)
+# define EXIM_HAVE_DMARC SUPPORT_DMARC
+# if DMARC_API >= 100400
+#  define DMARC_SUPPORTS_ARC
+# endif
 
 # include <opendmarc/dmarc.h>
 # ifdef SUPPORT_SPF
 #  include <spf2/spf.h>
 # endif /* SUPPORT_SPF */
 
-#define DMARC_VERIFY_STATUS    1
+#endif /* SUPPORT_DMARC */
+
+
 
 #define DMARC_HIST_OK          1
 #define DMARC_HIST_DISABLED    2
@@ -50,8 +90,55 @@
 #define ARES_RESULT_UNKNOWN 11
 #define ARES_RESULT_DISCARD 12
 
+# define DMARC_RECORD_A_UNSPECIFIED	('\0')		/* adkim and aspf */
+# define DMARC_RECORD_A_STRICT		('s')		/* adkim and aspf */
+# define DMARC_RECORD_A_RELAXED		('r')		/* adkim and aspf */
+# define DMARC_RECORD_P_UNSPECIFIED	('\0')		/* p and sp */
+# define DMARC_RECORD_P_NONE		('n')		/* p and sp */
+# define DMARC_RECORD_P_QUARANTINE	('q')		/* p and sp */
+# define DMARC_RECORD_P_REJECT		('r')		/* p and sp */
+
+#ifndef DMARC_POLICY_SPF_ALIGNMENT_PASS
+/* From opendmarc/dmarc.h */
+# define DMARC_POLICY_SPF_ALIGNMENT_PASS 4
+# define DMARC_POLICY_SPF_ALIGNMENT_FAIL 5
+#endif
+
+#ifndef DMARC_POLICY_DKIM_ALIGNMENT_PASS
+/* From opendmarc/dmarc.h */
+# define DMARC_POLICY_DKIM_ALIGNMENT_PASS 4
+# define DMARC_POLICY_DKIM_ALIGNMENT_FAIL 5
+#endif
+
 #define	DMARC_ARC_POLICY_RESULT_PASS	0
 #define	DMARC_ARC_POLICY_RESULT_UNUSED	1
 #define	DMARC_ARC_POLICY_RESULT_FAIL	2
 
-#endif /* SUPPORT_DMARC */
+
+
+/* These live in dmarc_common.c */
+extern BOOL dmarc_abort;
+extern uschar * dmarc_header_from_sender;
+extern uschar * dmarc_pass_fail;
+extern const uschar * dmarc_from_header;
+extern const misc_module_info * dmarc_dkim_mod_info;
+extern const misc_module_info * dmarc_spf_mod_info;
+extern int dmarc_spf_ares_result;
+extern uschar ** dmarc_rua;		/* aggregate report addressees */
+extern int dmarc_pct;
+extern int dmarc_adkim;
+extern int dmarc_aspf;
+extern int dmarc_policy;
+extern int dmarc_dom_policy;
+extern int dmarc_subdom_policy;
+extern int dmarc_spf_alignment;
+extern int dmarc_dkim_alignment;
+extern int dmarc_action;
+extern uschar * dmarc_used_domain;
+extern const uschar * dmarc_domain_policy;
+extern BOOL dmarc_alignment_spf;
+extern BOOL dmarc_alignment_dkim;
+
+extern const uschar * dmarc_status;
+extern const uschar * dmarc_status_text;
+
diff --git a/src/src/miscmods/dmarc_api.h b/src/src/miscmods/dmarc_api.h
index 2f10463fc..54b1630a6 100644
--- a/src/src/miscmods/dmarc_api.h
+++ b/src/src/miscmods/dmarc_api.h
@@ -13,4 +13,4 @@
 
 #define	DMARC_PROCESS		0
 #define DMARC_EXPAND_QUERY	1
-#define DMARC_STORE_DATA	2
+#define DMARC_STORE_FROMHDR	2
diff --git a/src/src/miscmods/dmarc_common.c b/src/src/miscmods/dmarc_common.c
new file mode 100644
index 000000000..168e7da8e
--- /dev/null
+++ b/src/src/miscmods/dmarc_common.c
@@ -0,0 +1,546 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* DMARC support.
+   Copyright (c) The Exim Maintainers 2025
+   License: GPL */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "../exim.h"
+
+#ifdef EXIM_HAVE_DMARC
+
+// #  include "dmarc.h"
+// #  include "pdkim.h"
+
+extern BOOL		dmarc_local_init(void);
+extern void		dmarc_local_msg_init(void);
+extern gstring *	dmarc_version_report(gstring *);
+extern int		dmarc_process(void);
+extern const uschar *	dmarc_exim_expand_query(void);
+
+
+/* Other modules needed for services */
+const misc_module_info * dmarc_spf_mod_info;
+const misc_module_info * dmarc_dkim_mod_info;
+const misc_module_info * dmarc_arc_mod_info;
+
+/* Working data */
+BOOL		dmarc_abort;
+uschar *	dmarc_pass_fail;	/* for authres */
+const uschar *	dmarc_from_header;
+uschar *	dmarc_header_from_sender;
+
+/* results */
+int		dmarc_spf_ares_result;
+uschar **	dmarc_rua;		/* aggregate report addressees */
+int		dmarc_pct;		/* percentage */
+int		dmarc_adkim;		/* dkim policy */
+int		dmarc_aspf;		/* spf policy */
+int		dmarc_policy;		/* policy to enforce */
+int		dmarc_dom_policy;	/* (the p tag, as numeric) */
+int		dmarc_subdom_policy;	/* (the sp tag, as numeric) */
+int		dmarc_spf_alignment;
+int		dmarc_dkim_alignment;
+int		dmarc_action;
+
+
+/* $variables */
+BOOL	 dmarc_alignment_dkim	 = FALSE; /* Subtest result */
+BOOL	 dmarc_alignment_spf	 = FALSE; /* Subtest result */
+const uschar * dmarc_domain_policy = NULL; /* Declared policy of used domain */
+const uschar * dmarc_status;		/* One word value */
+const uschar * dmarc_status_text   = NULL; /* Human readable value */
+uschar * dmarc_used_domain;		/* Domain libopendmarc chose for DMARC policy lookup */
+
+/* options */
+uschar * dmarc_forensic_sender   = NULL; /* Set sender address for forensic reports */
+uschar * dmarc_history_file      = NULL; /* File to store dmarc results */
+uschar * dmarc_tld_file          = NULL; /* Mozilla TLDs text file */
+
+
+/*API:
+One-time initialisation for dmarc.  Ensure the spf module is available.
+*/
+
+static BOOL
+dmarc_init(void * dummy)
+{
+uschar * errstr;
+if (!(dmarc_spf_mod_info = misc_mod_find(US"spf", &errstr)))
+  {
+  log_write(0, LOG_MAIN|LOG_PANIC, "dmarc: %s", errstr);
+  return FALSE;
+  }
+
+if (!(dmarc_dkim_mod_info = misc_mod_find(US"dkim", &errstr)))
+  {
+  log_write(0, LOG_MAIN|LOG_PANIC, "dmarc: %s", errstr);
+  return FALSE;
+  }
+
+dmarc_arc_mod_info = misc_mod_findonly(US"arc");
+return dmarc_local_init();
+}
+
+
+
+/*API: dmarc_msg_init could set up a context that can be re-used for several
+messages on the same SMTP connection
+(that come from the same host with the same HELO string).
+However, we seem to only use it for one; we destroy some sort of context
+at the tail end of dmarc_process(). */
+
+static int
+dmarc_msg_init(void)
+{
+/* Set some sane defaults.  Also clears previous results when
+multiple messages in one connection. */
+
+f.dmarc_has_been_checked = FALSE;
+dmarc_from_header	 = NULL;
+dmarc_header_from_sender = NULL;
+dmarc_spf_ares_result  = ARES_RESULT_UNDEFINED;
+dmarc_status       = US"none";
+dmarc_abort        = FALSE;
+dmarc_pass_fail    = US"skipped";
+dmarc_used_domain  = US"";
+
+/* ACLs have "control=dmarc_disable_verify" */
+if (f.dmarc_disable_verify)
+  return OK;
+
+GET_OPTION("dmarc_tld_file");
+if (  !dmarc_tld_file
+   || !(dmarc_tld_file = expand_string(dmarc_tld_file))
+   || !*dmarc_tld_file)
+  {
+  DEBUG(D_receive) debug_printf_indent("DMARC: no dmarc_tld_file\n");
+  dmarc_abort = TRUE;
+  }
+else if (!sender_host_address)
+  {
+  DEBUG(D_receive) debug_printf_indent("DMARC: no sender_host_address\n");
+  dmarc_abort = TRUE;
+  }
+else
+  dmarc_local_msg_init();
+
+return OK;
+}
+
+
+/*API*/
+
+static void
+dmarc_smtp_reset(void)
+{
+f.dmarc_has_been_checked = f.dmarc_disable_verify =
+  f.dmarc_enable_forensic = FALSE;
+dmarc_domain_policy = dmarc_status = dmarc_status_text =
+  dmarc_used_domain = NULL;
+}
+
+
+/* API: dmarc_store_data stores the header data so that subsequent dmarc_process
+can access the data.  Cleared by msg_init.
+Called after the entire message has been received, with the From: header. */
+
+static void
+dmarc_store_fromhdr(const uschar * hdr)
+{
+/* No debug output because would change every test debug output */
+dmarc_from_header = hdr;
+}
+
+
+/* Accept an error_block struct, initialize if empty, parse to the
+end, and append the two strings passed to it.  Used for adding
+variable amounts of value:pair data to the forensic emails. */
+
+static error_block *
+add_to_eblock(error_block * eblock, const uschar * t1, const uschar * t2)
+{
+error_block * eb = store_malloc(sizeof(error_block));
+if (!eblock)
+  eblock = eb;
+else
+  {
+  /* Find the end of the eblock struct and point it at eb */
+  error_block * tmp = eblock;
+  while(tmp->next)
+    tmp = tmp->next;
+  tmp->next = eb;
+  }
+eb->text1 = t1;
+eb->text2 = t2;
+eb->next  = NULL;
+return eblock;
+}
+
+void
+dmarc_send_forensic_report(const uschar ** ruf)
+{
+error_block * eblock;
+
+/* Earlier ACL does not have *required* control=dmarc_enable_forensic */
+if (!f.dmarc_enable_forensic || !ruf)
+  return;
+
+eblock = add_to_eblock(NULL,
+	  string_sprintf("Subject: DMARC Forensic Report for %s from IP %s\n\n",
+			dmarc_used_domain, sender_host_address), NULL);
+eblock = add_to_eblock(eblock,
+	  US"A message claiming to be from you has failed the published DMARC\n"
+	  "policy for your domain.\n\n", NULL);
+
+eblock = add_to_eblock(eblock, US"Sender Domain", dmarc_header_from_sender);
+eblock = add_to_eblock(eblock, US"Sender IP Address", sender_host_address);
+eblock = add_to_eblock(eblock, US"Received Date", tod_stamp(tod_full));
+eblock = add_to_eblock(eblock, US"SPF Alignment",
+			       dmarc_alignment_spf ? US"yes" : US"no");
+eblock = add_to_eblock(eblock, US"DKIM Alignment",
+			       dmarc_alignment_dkim ? US"yes" : US"no");
+eblock = add_to_eblock(eblock, US"DMARC Results", dmarc_status_text);
+
+for (int c = 0; ruf[c]; c++)
+  {
+  uschar * recipient = string_copylc(ruf[c]);
+  if (Ustrncmp(recipient, "mailto:",7))
+    continue;
+  /* Move to first character past the colon */
+  recipient += 7;
+  DEBUG(D_receive)
+    debug_printf_indent("DMARC forensic report to %s%s\n", recipient,
+	 host_checking || f.running_in_test_harness ? " (not really)" : "");
+  if (host_checking || f.running_in_test_harness)
+    continue;
+
+  if (!moan_send_message(recipient, ERRMESS_DMARC_FORENSIC, eblock,
+			header_list, NULL, NULL))
+    log_write(0, LOG_MAIN|LOG_PANIC,
+      "failure to send DMARC forensic report to %s", recipient);
+  }
+}
+
+
+/* Look up a DNS dmarc record for the given domain.  Return it or NULL */
+
+const uschar *
+dmarc_dns_lookup(const uschar * dom)
+{
+dns_answer * dnsa = store_get_dns_answer();
+dns_scan dnss;
+const uschar * res = NULL;
+
+expand_level++;
+
+/* RFC 7489 6.6.1 :- policy record is at a "_dmarc" sub of the domain */
+
+if (dns_lookup(dnsa, string_sprintf("_dmarc.%s", dom), T_TXT, NULL)
+    == DNS_SUCCEED)
+  {
+/*XXX we lose track of temporary DNS failures */
+
+  for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
+       rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
+    {
+    const uschar * rdata = rr->data;
+    int len = rdata[0];
+
+    if (len > 511) len = 127;
+    rdata++;
+
+/* RFC 7489 6.6.1 :- policy record is a TXT record */
+/* RFC 7489 6.6.3 step 2: ignore records not starting "v=DMARC1;"
+   (also noted in 6.3 for the v tag) */
+
+    if (  rr->type == T_TXT && len > 9
+       && Ustrncmp(rdata, "v=DMARC1;", 9) == 0)
+      if (!res)
+	res = string_copyn_taint(rdata, len, GET_TAINTED);
+      else
+/* RFC 7489 6.6.3 step 5: multiple records are treated as no record */
+	{
+	DEBUG(D_receive) debug_printf_indent("DMARC: multiple rr\n");
+	res = NULL;
+	break;
+	}
+    }
+  }
+else
+  DEBUG(D_receive) debug_printf_indent("DMARC: no ret\n");
+
+expand_level--;
+store_free_dns_answer(dnsa);
+DEBUG(D_receive) debug_printf_indent("DMARC: rr %q\n", res);
+return res;
+}
+
+
+
+const uschar *
+dmarc_lookup_regdom(const uschar * dom)
+{
+int expand_setup = -1, partial, affixlen, starflags;
+const uschar * affix, * opts, * res;
+const lookup_info * li;
+void * handle;
+static const uschar * cached_key = NULL, * cached_res = NULL;
+
+DEBUG(D_receive) debug_printf_indent("DMARC: lookup regdom for %q\n", dom);
+
+if (cached_key && Ustrcmp(dom, cached_key) == 0)
+  {
+  res = cached_res;
+  DEBUG(D_receive) debug_printf_indent(" DMARC: cached value %q\n", res);
+  return res;
+  }
+
+expand_level++;
+res = NULL;
+if (!(li = search_findtype_partial(US"regdom", &partial, &affix, &affixlen,
+				  &starflags, &opts)))
+  {
+  DEBUG(D_receive) debug_printf_indent("DMARC: missing regdom lookup\n");
+  goto out;
+  }
+
+if (!(handle = search_open(dmarc_tld_file, li, 0, NULL, NULL)))
+  goto out;
+
+/*XXX should we handle a defer return?  cf. f.search_find_defer */
+
+res = search_find(handle, dmarc_tld_file, dom, partial, affix,
+		  affixlen, starflags, &expand_setup, opts);
+
+out:
+  cached_key = dom; cached_res = res;
+  expand_level--;
+  return res;
+}
+
+const uschar *
+dmarc_get_dns_policy_record(const uschar ** used_dom_p)
+{
+const uschar * s;
+
+DEBUG(D_receive) debug_printf_indent("DMARC: lookup policy record for %s\n",
+				      dmarc_header_from_sender);
+
+/* RFC 7489 6.6.3 step 1: DNS domain matching the 5322.From */
+
+if ((s= dmarc_dns_lookup(*used_dom_p = dmarc_header_from_sender)))
+  return s;
+
+/* RFC 7489 6.6.3 step 3: if no record, use the Organizational Domain */
+
+if (!(s = dmarc_lookup_regdom(dmarc_header_from_sender)))
+  return NULL;
+
+/* RFC 7489 6.6.3 step 3: if the Organizational Domain differs */
+
+if (Ustrcmp(s, dmarc_header_from_sender) == 0)
+  return NULL;
+
+return dmarc_dns_lookup(*used_dom_p = s);
+}
+
+
+void
+dmarc_write_history_file(const gstring * dkim_history_buffer)
+{
+int history_file_fd = -1;
+uschar * s;
+gstring * g;
+
+GET_OPTION("dmarc_history_file");
+if (!(s = dmarc_history_file) || !(s = expand_string(s)) || !*s)
+  {
+  DEBUG(D_receive) debug_printf_indent("DMARC history file not set\n");
+  return;
+  }
+if (!host_checking)	/* -bh mode: nothing written except debug */
+  if ((history_file_fd = log_open_as_exim(s)) < 0)
+    {
+    log_write(0, LOG_MAIN|LOG_PANIC,
+	      "failure to create DMARC history file: %s: %s",
+	      s, strerror(errno));
+    return;
+    }
+
+/* Generate the contents of the history file entry */
+
+g = string_fmt_append(NULL,
+  "job %s\n"
+  "reporter %s\n"
+  "received %ld\n"
+  "ipaddr %s\n"
+  "from %s\n"
+  "mfrom %s\n",
+  message_id, primary_hostname, time(NULL), sender_host_address,
+  dmarc_header_from_sender, expand_string(US"$sender_address_domain"));
+
+if (dmarc_spf_ares_result != ARES_RESULT_UNDEFINED)
+  g = string_fmt_append(g, "spf %d\n", dmarc_spf_ares_result);
+
+if (dkim_history_buffer)
+  g = string_fmt_append(g, "%Y", dkim_history_buffer);
+
+g = string_fmt_append(g, "pdomain %s\n"
+			  "policy %d\n",
+  dmarc_used_domain, dmarc_policy);
+
+if (dmarc_rua)
+  for (uschar ** ss = dmarc_rua; *ss; ss++)
+    g = string_fmt_append(g, "rua %s\n", *ss);
+else
+  g = string_catn(g, US"rua -\n", 6);
+
+/* policy tag values */
+g = string_fmt_append(g, "pct %d\n"
+			  "adkim %d\n"
+			  "aspf %d\n"
+			  "p %d\n"
+			  "sp %d\n",
+  dmarc_pct, dmarc_adkim, dmarc_aspf, dmarc_dom_policy, dmarc_subdom_policy);
+
+g = string_fmt_append(g, "align_dkim %d\n"
+			  "align_spf %d\n"
+			  "action %d\n",
+  dmarc_dkim_alignment, dmarc_spf_alignment, dmarc_action);
+
+#ifdef DMARC_SUPPORTS_ARC
+  {
+# ifdef EXPERIMENTAL_ARC
+  const uschar * s;
+  gstring * g2 = NULL;
+  typedef const uschar * (*fn_t)(gstring **);
+
+  if (  dmarc_arc_mod_info
+     && (s = (((fn_t *) dmarc_arc_mod_info->functions)[ARC_ARCSET_INFO]) (&g2)))
+    {
+    int i = Ustrcmp(s, "pass") == 0 ? ARES_RESULT_PASS
+	    : Ustrcmp(s, "fail") == 0 ? ARES_RESULT_FAIL
+	    : ARES_RESULT_UNKNOWN;
+
+    g = string_fmt_append(g, "arc %d\n"
+			     "arc_policy %d json[%#Y ]\n",
+			  i,
+			  i == ARES_RESULT_PASS ? DMARC_ARC_POLICY_RESULT_PASS
+			  : i == ARES_RESULT_FAIL ? DMARC_ARC_POLICY_RESULT_FAIL
+			  : DMARC_ARC_POLICY_RESULT_UNUSED,
+			  g2
+			  );
+    }
+  else
+
+# endif
+    g = string_fmt_append(g, "arc %d\narc_policy %d json:[]\n",
+		      ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED);
+  }
+#endif
+
+/* Write the contents to the history file */
+DEBUG(D_receive)
+  {
+  debug_printf_indent("DMARC history data for debugging:\n");
+  expand_level++;
+  debug_printf_indent("%Y", g);
+  expand_level--;
+  debug_printf_indent("DMARC logging history data for opendmarc reporting%s\n",
+	     host_checking ? " (not really)" : "");
+  }
+
+if (!host_checking)
+  {
+  ssize_t written_len = write_to_fd_buf(history_file_fd,
+				string_from_gstring(g), gstring_length(g));
+  if (written_len == 0)
+    {
+    log_write(0, LOG_MAIN|LOG_PANIC,
+	    "failure to write to DMARC history file: %s", dmarc_history_file);
+    (void)close(history_file_fd);
+    return;
+    }
+  (void)close(history_file_fd);
+  }
+return;
+}
+
+
+
+/*API*/
+static gstring *
+authres_dmarc(gstring * g)
+{
+if (f.dmarc_has_been_checked)
+  {
+  int start = 0;		/* Compiler quietening */
+  DEBUG(D_acl) start = gstring_length(g);
+  g = string_append(g, 2, US";\n\tdmarc=", dmarc_pass_fail);
+  if (dmarc_header_from_sender)
+    g = string_append(g, 2, US" header.from=", dmarc_header_from_sender);
+  DEBUG(D_acl) debug_printf_indent("DMARC:\tauthres '%.*s'\n",
+		  gstring_length(g) - start - 3, g->s + start + 3);
+  }
+else
+  DEBUG(D_acl) debug_printf_indent("DMARC:\tno authres\n");
+return g;
+}
+
+/******************************************************************************/
+/* Module API */
+
+static optionlist dmarc_options[] = {
+  { "dmarc_forensic_sender",    opt_stringptr,      {&dmarc_forensic_sender} },
+  { "dmarc_history_file",       opt_stringptr,      {&dmarc_history_file} },
+  { "dmarc_tld_file",           opt_stringptr,      {&dmarc_tld_file} },
+};
+
+static void * dmarc_functions[] = {
+  [DMARC_PROCESS] =	(void *) dmarc_process,
+  [DMARC_EXPAND_QUERY] = (void *) dmarc_exim_expand_query,
+  [DMARC_STORE_FROMHDR] = (void *) dmarc_store_fromhdr,
+};
+
+/* dmarc_forensic_sender is provided for visibility of the the option setting
+by moan_send_message. We do not document it as a config-visible $variable.
+We could provide it via a function but there's little advantage. */
+
+static var_entry dmarc_variables[] = {
+  { "dmarc_alignment_dkim",	vtype_bool,		&dmarc_alignment_dkim },
+  { "dmarc_alignment_spf",	vtype_bool,		&dmarc_alignment_spf },
+  { "dmarc_domain_policy",	vtype_stringptr,	&dmarc_domain_policy },
+  { "dmarc_forensic_sender",	vtype_stringptr,	&dmarc_forensic_sender},
+  { "dmarc_status",		vtype_stringptr,	&dmarc_status },
+  { "dmarc_status_text",	vtype_stringptr,	&dmarc_status_text },
+  { "dmarc_used_domain",	vtype_stringptr,	&dmarc_used_domain },
+};
+
+misc_module_info dmarc_module_info =
+{
+  .name =		US"dmarc",
+# ifdef DYNLOOKUP
+  .dyn_magic =		MISC_MODULE_MAGIC,
+# endif
+  .init =		dmarc_init,
+  .lib_vers_report =	dmarc_version_report,
+  .smtp_reset =		dmarc_smtp_reset,
+  .msg_init =		dmarc_msg_init,
+  .authres =		authres_dmarc,
+
+  .options =		dmarc_options,
+  .options_count =	nelem(dmarc_options),
+
+  .functions =		dmarc_functions,
+  .functions_count =	nelem(dmarc_functions),
+
+  .variables =		dmarc_variables,
+  .variables_count =	nelem(dmarc_variables),
+};
+
+#endif	/*EXIM_HAVE_DMARC*/
+/* vi: aw ai sw=2
+ */
diff --git a/src/src/miscmods/dmarc_native.c b/src/src/miscmods/dmarc_native.c
new file mode 100644
index 000000000..ce4aaee4f
--- /dev/null
+++ b/src/src/miscmods/dmarc_native.c
@@ -0,0 +1,707 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* DMARC support.
+   Copyright (c) The Exim Maintainers 2025
+   License: GPL */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "../exim.h"
+
+#ifdef SUPPORT_DMARC
+# error Build cannot support both libopendmarc and native DMARC modules
+#endif
+
+#ifdef EXPERIMENTAL_DMARC_NATIVE
+# ifndef EXIM_HAVE_SPF
+#  error SPF must also be enabled for DMARC
+# elif defined DISABLE_DKIM
+#  error DKIM must also be enabled for DMARC
+# elif !defined LOOKUP_PSL
+#  error PSL lookups must be enabled for DMARC
+# else
+
+#  include "../functions.h"
+#  include "pdkim.h"
+
+extern void		dmarc_send_forensic_report(const uschar **);
+extern const uschar *	dmarc_get_dns_policy_record(uschar **);
+extern void		dmarc_write_history_file(const gstring *);
+extern const uschar *	dmarc_lookup_regdom(const uschar *);
+
+
+static const pcre2_code * dmarc_regex_uri = NULL;
+static const pcre2_code * dmarc_regex_pct = NULL;
+static const pcre2_code * dmarc_regex_ri = NULL;
+static const pcre2_code * dmarc_regex_fo = NULL;
+
+BOOL
+dmarc_local_init(void)
+{
+if (!dmarc_regex_uri)
+  dmarc_regex_uri = regex_must_compile(US "^mailto:[^@]+@[^ !]+(?:[ !]|$)",
+							MCS_CACHEABLE, FALSE);
+if (!dmarc_regex_pct)
+  dmarc_regex_uri = regex_must_compile(US "^\\d{1,3}$",  MCS_CACHEABLE, FALSE);
+if (!dmarc_regex_ri)
+  dmarc_regex_ri =  regex_must_compile(US "^\\d{1,10}$", MCS_CACHEABLE, FALSE);
+if (!dmarc_regex_fo)
+  dmarc_regex_fo =  regex_must_compile(US "^[01ds]$",    MCS_CACHEABLE, FALSE);
+}
+
+
+#include "../version.h"
+
+gstring *
+dmarc_version_report(gstring * g)
+{
+/*
+return string_fmt_append(g, "Library version: dmarc: Compile: %d.%d.%d.%d\n",
+    (OPENDMARC_LIB_VERSION & 0xff000000) >> 24,
+    (OPENDMARC_LIB_VERSION & 0x00ff0000) >> 16,
+    (OPENDMARC_LIB_VERSION & 0x0000ff00) >> 8,
+    (OPENDMARC_LIB_VERSION & 0x000000ff));
+*/
+return string_fmt_append(g, "Library version: dmarc: Exim %s builtin\n",
+			    EXIM_VERSION_STR);
+}
+
+
+int
+dmarc_local_msg_init()
+{
+return OK;
+}
+
+
+/* Convert to comma-sep list to NULL-terminated array of pointers */
+static uschar **
+dmarc_clist_to_array(const uschar * list)
+{
+int cnt = 0, sep = ',';
+const uschar * s = list;
+uschar * buf = store_get(2, list), ** rarray;
+
+while (string_nextinlist(&s, &sep, buf, 1)) cnt++;	/* count the elements */
+rarray = store_get((cnt+1) * sizeof(*rarray), list);
+for (cnt = 0; rarray[cnt] = string_nextinlist(&list, &sep, NULL, 0); ) cnt++;
+return rarray;
+}
+
+
+static void
+dmarc_maybe_send_forensic(const uschar * ruf)
+{
+/* Earlier ACL does not have *required* control=dmarc_enable_forensic */
+if (!f.dmarc_enable_forensic)
+  return;
+
+/* RFC 7489 6.3 - ruf is optional */
+if (!ruf)
+  return;
+
+if (   dmarc_policy == DMARC_POLICY_REJECT
+    && dmarc_action == DMARC_RESULT_REJECT
+   ||  dmarc_policy == DMARC_POLICY_QUARANTINE
+    && dmarc_action == DMARC_RESULT_QUARANTINE
+   ||  dmarc_policy == DMARC_POLICY_NONE
+    && dmarc_action == DMARC_RESULT_REJECT
+   ||  dmarc_policy == DMARC_POLICY_NONE
+    && dmarc_action == DMARC_RESULT_QUARANTINE
+   )
+  {
+/* RFC 7489 6.3 - ruf is a comma-sep list */
+  /* Convert to NULL-terminated array of pointers */
+  const uschar ** rarray = CUSS dmarc_clist_to_array(ruf);
+  dmarc_send_forensic_report(rarray);
+  }
+}
+
+
+/******************************************************************************/
+/* Policy record parsing */
+
+/* Value verification routines: return boolean "good" */
+
+static BOOL dmarc_vfy_vmode(const uschar * val)
+{ return (*val == 's' || *val == 'r') && val[1] == '\0'; }
+static BOOL dmarc_vfy_policy(const uschar * val)
+{ return Ustrcmp(val, "none") == 0
+	|| Ustrcmp(val, "quarantine") == 0
+	|| Ustrcmp(val, "reject") == 0; }
+static BOOL dmarc_vfy_fbl(const uschar * val)
+{
+/* For now, permit a list starting with (a plausible) mailto URI */
+return regex_match(dmarc_regex_uri, val, -1, NULL);
+}
+
+static BOOL dmarc_tag_vfy_adkim(const uschar * val)
+{ return dmarc_vfy_vmode(val); }
+static BOOL dmarc_tag_vfy_aspf(const uschar * val)
+{ return dmarc_vfy_vmode(val); }
+static BOOL dmarc_tag_vfy_fo(const uschar * val)
+{ return regex_match(dmarc_regex_fo, val, -1, NULL); }
+static BOOL dmarc_tag_vfy_p(const uschar * val)
+{ return dmarc_vfy_policy(val); }
+static BOOL dmarc_tag_vfy_pct(const uschar * val)
+{ return regex_match(dmarc_regex_pct, val, -1, NULL); }
+static BOOL dmarc_tag_vfy_rf(const uschar * val)
+{ return Ustrcmp(val, "afrf") == 0; }
+static BOOL dmarc_tag_vfy_ri(const uschar * val)
+{ return regex_match(dmarc_regex_ri, val, -1, NULL); }
+static BOOL dmarc_tag_vfy_rua(const uschar * val)
+{ return dmarc_vfy_fbl(val); }
+static BOOL dmarc_tag_vfy_ruf(const uschar * val)
+{ return dmarc_vfy_fbl(val); }
+static BOOL dmarc_tag_vfy_sp(const uschar * val)
+{ return dmarc_vfy_policy(val); }
+static BOOL dmarc_tag_vfy_v(const uschar * val)
+{ return Ustrcmp(val, "DMARC1") == 0; }
+
+typedef struct dmarc_policy_record {
+  const uschar * adkim;
+  const uschar * aspf;
+  const uschar * fo;
+  const uschar * p;
+  const uschar * pct;
+  const uschar * rf;
+  const uschar * ri;
+  const uschar * rua;
+  const uschar * ruf;
+  const uschar * sp;
+  const uschar * v;
+} dmarc_policy_record;
+
+typedef struct tag {
+  const uschar * name;
+  unsigned	 offset;
+  BOOL		(*verify)(const uschar *);
+} tag;
+#define TAG(field) {.name = US mac_expanded_string(field), \
+		    .offset = offsetof(dmarc_policy_record, field), \
+		    .verify = dmarc_tag_vfy_ ## field }
+tag policy_tags[] = {
+  TAG(adkim),
+  TAG(aspf),
+  TAG(fo),
+  TAG(p),
+  TAG(pct),
+  TAG(rf),
+  TAG(ri),
+  TAG(rua),
+  TAG(ruf),
+  TAG(sp),
+  TAG(v),
+};
+#undef TAG
+
+/* Handle one potential tag
+Return: boolean success; else parsing error
+
+RFC 7489 6.3 :- unknown tags are ignored
+*/
+static int
+parse_tag(const uschar * tagrecord, dmarc_policy_record * prp)
+{
+const uschar * e = Ustrchr(tagrecord, '='), * s;
+
+/* RFC 6736 3.2 tagspec must have = */
+if (!*e)
+  return FALSE;
+
+/* RFC 6736 3.2 ignore whitespace between tag name and = */
+for (s = e; s > tagrecord && isspace(s[-1]); ) s--;
+
+/* RFC 6736 3.2 tag name at least 1 char */
+if (s == tagrecord)
+  return FALSE;
+
+/* search for tag name in our table of known ones */
+for (tag * ptp = policy_tags; ptp < policy_tags + nelem(policy_tags); ptp++)
+ {
+  if (  Ustrncmp(ptp->name, tagrecord, s - tagrecord) == 0
+     && Ustrlen(ptp->name) == s - tagrecord)
+
+    {			/* match; copy tag value to policy record struct */
+    const uschar ** vp = CUSS (US prp + ptp->offset);
+
+// debug_printf_indent("matched %q, off %u\n", tagrecord, ptp->offset);
+
+/* RFC 6736 3.2 ignore whitespace between = and value */
+    s = e + 1;
+    Uskip_whitespace(&s);
+
+    if (!ptp->verify(s)) DEBUG(D_receive)
+      debug_printf_indent("DMARC: bad value for tag %q: %q\n", ptp->name, s);
+    *vp = string_copy(s);
+    break;
+    }
+ }
+return TRUE;
+}
+
+static BOOL
+dmarc_local_parse_policy(const uschar * rr, dmarc_policy_record * prp)
+{
+/* RFC 6376 3.2 :- a taglist is a ;-sep list of tagspec */
+int sep = ';';
+
+/* RFC 6736 3.2 :- ignore whitespace preceding tag-name and after value */
+
+for (uschar * tagspec; tagspec = string_nextinlist(&rr, &sep, NULL, 0); )
+  if (!parse_tag(tagspec, prp))
+    return FALSE;
+
+return TRUE;
+}
+
+/******************************************************************************/
+
+static BOOL
+identifier_aligned(const uschar * a, const uschar * b, const uschar * mode)
+{
+BOOL res;
+
+/* RFC 7489 3.3.1 - In strict mode, only an exact match */
+
+if (*mode == 's')
+  res = Ustrcmp(a, b) == 0;
+
+/* RFC 7489 3.3.1 - In relaxed mode, the Organizational Domains of both */
+else
+  {
+  /* - if there is an exact match, the ODs will also match -
+  so check that first to save on regdom lookups. */
+
+  if (Ustrcmp(a, b) == 0)
+    res = TRUE;
+  else
+    {
+    a = dmarc_lookup_regdom(a);
+    b = dmarc_lookup_regdom(b);
+    res = a && b && Ustrcmp(a, b) == 0;
+    }
+  }
+DEBUG(D_receive)
+  if (res) debug_printf_indent("DMARC aligned(%s) %s %s\n", mode, a, b);
+return res;
+}
+
+
+/* API: dmarc_process adds the envelope sender address to the existing
+context (if any), retrieves the result, sets up expansion
+strings and evaluates the condition outcome.
+Called for the first ACL dmarc= condition. */
+
+int
+dmarc_process(void)
+{
+const uschar * rr;
+BOOL has_dmarc_record = TRUE;
+u_char ** ruf; /* forensic report addressees, if called for */
+
+dmarc_alignment_spf = dmarc_alignment_dkim = FALSE;
+dmarc_dkim_alignment = DMARC_POLICY_DKIM_ALIGNMENT_FAIL;
+dmarc_spf_alignment =  DMARC_POLICY_SPF_ALIGNMENT_FAIL;
+
+/* ACLs have "control=dmarc_disable_verify" */
+if (f.dmarc_disable_verify || dmarc_abort)
+  return OK;
+
+DEBUG(D_receive) { debug_printf_indent("DMARC: process\n"); expand_level++; }
+
+/* Store the header From: sender domain for this part of DMARC.
+If there is no from_header string, then it's likely this message
+is locally generated and relying on fixups to add it.  Just skip
+the entire DMARC system if we can't find a From: header....or if
+there was a previous error.  */
+
+if (!dmarc_from_header)
+  {
+  DEBUG(D_receive) debug_printf_indent("DMARC: no From: header\n");
+  dmarc_abort = TRUE;
+  }
+else
+  {
+/* RFC 7489 6.6.1 :- extract the domain from the 5322.From */
+  const uschar * end_addr, * s;
+  uschar * errormsg;
+  int dummy, domain;
+
+  f.parse_allow_group = TRUE;
+  end_addr = parse_find_address_end(dmarc_from_header, FALSE);
+  s = *end_addr
+      ? string_copyn(dmarc_from_header, end_addr - dmarc_from_header)
+      : dmarc_from_header;
+  if ((dmarc_header_from_sender = parse_extract_address(s, &errormsg,
+			      &dummy, &dummy, &domain, FALSE)))
+    dmarc_header_from_sender += domain;
+
+  /* Only use the domain if not empty.  Otherwise, skip out of DMARC. */
+
+  if (!dmarc_header_from_sender || !*dmarc_header_from_sender)
+    {
+    dmarc_status = US"nofrom";
+    dmarc_pass_fail = US"temperror";
+    dmarc_status_text = US"No From: domain found";
+    dmarc_action = DMARC_RESULT_ACCEPT;
+
+    dmarc_abort = TRUE;
+    }
+  }
+
+/* Skip DMARC if connection is SMTP Auth. Temporarily, admin should
+instead do this in the ACLs.  */
+
+if (!dmarc_abort && !sender_host_authenticated)
+  {
+/* RFC 7489 6.3 :- defaults for policy record tags */
+  dmarc_policy_record dmarc_parsed = {
+    .adkim =	US"r",
+    .aspf =	US"r",
+    .fo =	US"0",
+    .pct =	US"100",
+    .rf =	US"afrf",
+    .ri =	US"86400",
+    };
+
+  int sr = SPF_RESULT_INVALID, spf_origin;
+  uschar * spf_human_readable = NULL, * spf_sender_domain = NULL;
+  unsigned dkim_sig_count = 0;
+  gstring * dkim_history_buffer = NULL;
+  typedef const pdkim_signature * (*sigs_fn_t)(void);
+
+/* RFC 7489 6.6.2 step 2: DMARC policy record from DNS */
+  DEBUG(D_receive)
+    {
+    debug_printf_indent("DMARC: get policy record\n");
+    expand_level++;
+    }
+
+  /* uses $dmarc_header_from_sender */
+  if (!(rr = dmarc_get_dns_policy_record(&dmarc_used_domain)))
+	/*XXX want to handle nxdomain,temprror etc. here */
+    {
+    DEBUG(D_receive) debug_printf_indent("DMARC: no record found for %s\n",
+					  dmarc_header_from_sender);
+    dmarc_policy = DMARC_POLICY_ABSENT;
+    dmarc_status = US"norecord";
+    dmarc_pass_fail = US"none";
+    dmarc_status_text = US"No DMARC record";
+    dmarc_action = DMARC_RESULT_ACCEPT;
+
+    has_dmarc_record = FALSE;
+    }
+
+  else if (!dmarc_local_parse_policy(rr, &dmarc_parsed))
+    {
+    DEBUG(D_receive) debug_printf_indent("DMARC: invalid record found for %s\n",
+					  dmarc_header_from_sender);
+    dmarc_policy = DMARC_POLICY_ABSENT;
+    dmarc_status = US"norecord";
+    dmarc_pass_fail = US"none";
+    dmarc_status_text = US"No DMARC record";
+    dmarc_action = DMARC_RESULT_ACCEPT;
+
+    has_dmarc_record = FALSE;
+    goto out;
+    }
+
+/* RFC 7489 6.6.3 step 6: p/sp checks */
+    if (  !dmarc_parsed.p
+       || !dmarc_tag_vfy_p(dmarc_parsed.p)
+       || dmarc_parsed.sp && !dmarc_tag_vfy_sp(dmarc_parsed.sp)
+       )
+
+/* RFC 7489 6.6.3 step 6: if a valid rua, continue with p=none */
+/*XXX "at least one syntactically valid reporting URI" */
+      if (dmarc_parsed.rua && dmarc_tag_vfy_rua(dmarc_parsed.rua))
+	{
+	DEBUG(D_receive)
+	  debug_printf_indent("DMARC: invalid p or sp; continue for rua\n");
+	dmarc_parsed.p = US"none";
+	}
+      else
+	{
+	DEBUG(D_receive)
+	  debug_printf_indent("DMARC: invalid p or sp, and no rua. Abort.\n");
+	dmarc_abort = TRUE;
+	goto out;
+	}
+
+/* RFC 7489 6.6.2 step 3: Perform DKIM signature verification checks */
+  DEBUG(D_receive)
+    {
+    expand_level--;
+    debug_printf_indent("DMARC: process dkim results\n");
+    expand_level++;
+    }
+
+  /* Now we cycle through the dkim signature results and put into
+  the opendmarc context, further building the DMARC reply. */
+
+  if (has_dmarc_record)
+    for(const pdkim_signature * sig =
+	      (((sigs_fn_t *)dmarc_dkim_mod_info->functions)[DKIM_SIGS_LIST])();
+	sig; sig = sig->next)
+    {
+    int dkim_result, dkim_ares_result, vs, ves;
+
+    dkim_sig_count++;
+    vs  = sig->verify_status & ~PDKIM_VERIFY_POLICY;
+    ves = sig->verify_ext_status;
+    dkim_result = vs == PDKIM_VERIFY_PASS ? DMARC_POLICY_DKIM_OUTCOME_PASS :
+		  vs == PDKIM_VERIFY_FAIL ? DMARC_POLICY_DKIM_OUTCOME_FAIL :
+		  vs == PDKIM_VERIFY_INVALID ? DMARC_POLICY_DKIM_OUTCOME_TMPFAIL :
+		  DMARC_POLICY_DKIM_OUTCOME_NONE;
+
+    DEBUG(D_receive)
+      debug_printf_indent("DMARC: adding DKIM sender domain = %s\n",
+			  sig->domain);
+
+    /* Update the history buffer */
+
+    dkim_ares_result =
+      vs == PDKIM_VERIFY_PASS    ? ARES_RESULT_PASS :
+      vs == PDKIM_VERIFY_FAIL    ? ARES_RESULT_FAIL :
+      vs == PDKIM_VERIFY_NONE    ? ARES_RESULT_NONE :
+      vs == PDKIM_VERIFY_INVALID ?
+       ves == PDKIM_VERIFY_INVALID_PUBKEY_UNAVAILABLE ? ARES_RESULT_PERMERROR :
+       ves == PDKIM_VERIFY_INVALID_BUFFER_SIZE        ? ARES_RESULT_PERMERROR :
+       ves == PDKIM_VERIFY_INVALID_PUBKEY_DNSRECORD   ? ARES_RESULT_PERMERROR :
+       ves == PDKIM_VERIFY_INVALID_PUBKEY_IMPORT      ? ARES_RESULT_PERMERROR :
+       ARES_RESULT_UNKNOWN :
+      ARES_RESULT_UNKNOWN;
+
+    dkim_history_buffer = string_fmt_append(dkim_history_buffer,
+      "dkim %s %s %d\n", sig->domain, sig->selector, dkim_ares_result);
+
+    /* Evaluate the sig vs. dmarc requirements */
+
+/* RFC 7489 3.1.1 if any DKIM signature ... verifies. */
+    if (  !dmarc_alignment_dkim
+       && dkim_result == DMARC_POLICY_DKIM_OUTCOME_PASS
+
+/* RFC 7489 3.1.1 dkim alignment: d= tag in dkim sig */
+/* RFC 7489 3.1.1 dkim alignment: 5322.From domain */
+/* RFC 7489 6.3   adkim: DKIM Identifier Alignment mode */
+
+       && (dmarc_alignment_dkim = identifier_aligned(sig->domain,
+				  dmarc_header_from_sender, dmarc_parsed.adkim))
+       )
+      dmarc_dkim_alignment = DMARC_POLICY_DKIM_ALIGNMENT_PASS;
+    }
+  DEBUG(D_receive) debug_printf_indent("DMARC: %u dkim sig%s\n",
+				dkim_sig_count, dkim_sig_count == 1 ? "" : "s");
+
+  DEBUG(D_receive)
+    {
+    expand_level--;
+    debug_printf_indent("DMARC: process spf results\n");
+    expand_level++;
+    }
+
+/* RFC 7489 6.6.2 step 4: Perform SPF validation checks */
+
+  if (has_dmarc_record)
+    {
+    int spf_result;
+    typedef int (*fn_t)(uschar **);
+
+    /* Use the envelope sender domain for this part of DMARC */
+
+    spf_sender_domain = expand_string(US"$sender_address_domain");
+
+    if (dmarc_spf_mod_info)
+      sr = ((fn_t *) dmarc_spf_mod_info->functions)[SPF_GET_RESULTS]
+							  (&spf_human_readable);
+
+    if (sr == SPF_RESULT_INVALID)
+      {
+      /* No spf data means null envelope sender so generate a domain name
+      from the sender_helo_name  */
+
+      DEBUG(D_receive) debug_printf_indent("DMARC: spf result 'invalid'\n");
+
+      if (!spf_sender_domain || !*spf_sender_domain)
+	{
+	spf_sender_domain = sender_helo_name;
+	log_write(0, LOG_MAIN, "DMARC using synthesized SPF sender domain = %s\n",
+			       spf_sender_domain);
+	}
+      spf_result = DMARC_POLICY_SPF_OUTCOME_NONE;
+      dmarc_spf_ares_result = ARES_RESULT_UNKNOWN;
+      spf_origin = DMARC_POLICY_SPF_ORIGIN_HELO;
+      spf_human_readable = US"";
+      }
+    else
+      {
+      spf_result = sr == SPF_RESULT_NEUTRAL  ? DMARC_POLICY_SPF_OUTCOME_NONE :
+			 sr == SPF_RESULT_PASS     ? DMARC_POLICY_SPF_OUTCOME_PASS :
+			 sr == SPF_RESULT_FAIL     ? DMARC_POLICY_SPF_OUTCOME_FAIL :
+			 sr == SPF_RESULT_SOFTFAIL ? DMARC_POLICY_SPF_OUTCOME_TMPFAIL :
+			 DMARC_POLICY_SPF_OUTCOME_NONE;
+      dmarc_spf_ares_result = sr == SPF_RESULT_NEUTRAL   ? ARES_RESULT_NEUTRAL :
+			      sr == SPF_RESULT_PASS      ? ARES_RESULT_PASS :
+			      sr == SPF_RESULT_FAIL      ? ARES_RESULT_FAIL :
+			      sr == SPF_RESULT_SOFTFAIL  ? ARES_RESULT_SOFTFAIL :
+			      sr == SPF_RESULT_NONE      ? ARES_RESULT_NONE :
+			      sr == SPF_RESULT_TEMPERROR ? ARES_RESULT_TEMPERROR :
+			      sr == SPF_RESULT_PERMERROR ? ARES_RESULT_PERMERROR :
+			      ARES_RESULT_UNKNOWN;
+      /*XXX hmm, spf_origin never used? */
+      spf_origin = DMARC_POLICY_SPF_ORIGIN_MAILFROM;
+      DEBUG(D_receive)
+	debug_printf_indent("DMARC: using SPF sender domain = %s\n",
+					  spf_sender_domain);
+      }
+    if (!spf_sender_domain || !*spf_sender_domain)
+      dmarc_abort = TRUE;
+    if (!dmarc_abort)
+      {
+  /* RFC 7489 3.1.1 spf alignment: the SPF-authenticated domain */
+  /* RFC 7489 3.1.1 spf alignment: 5322.From domain */
+  /* RFC 7489 6.3   aspf: SPF Identifier Alignment mode */
+
+      if (  spf_result == DMARC_POLICY_SPF_OUTCOME_PASS
+	 && (dmarc_alignment_spf = identifier_aligned(spf_sender_domain,
+				  dmarc_header_from_sender, dmarc_parsed.aspf))
+	 )
+	dmarc_spf_alignment = DMARC_POLICY_SPF_ALIGNMENT_PASS;
+      }
+    }
+
+  DEBUG(D_receive)
+    {
+    expand_level--;
+    debug_printf_indent("DMARC: finished spf\n");
+    }
+
+  /* Store the policy string in an expandable variable. */
+
+/* RFC 7489 is unclear how to obtain the policy-string that is to be used.
+The decription of tags p & sp in 6.3 uses the term "domain queried". I assume
+that is the portion of the DNS lookup key *after* the prepended "_dmarc."
+which returned the DMARC RR being used (so it could be the Organizational
+Domain, per 6.6.3 bullet 3, rather than the 5322.From domain).
+
+Given that assumption: if dom-used != 5322.From.dom and there is an sp,
+use the sp.  Otherwise use the p. */
+
+  dmarc_domain_policy = dmarc_parsed.sp
+			&& dmarc_used_domain != dmarc_header_from_sender
+		      ? dmarc_parsed.sp : dmarc_parsed.p;
+
+/* RFC 7489 6.6.2 step 5 - if either the spf or dkim shows alignment, pass */
+  if (dmarc_alignment_spf || dmarc_alignment_dkim)
+    {							/* Explicit accept */
+    dmarc_policy = DMARC_POLICY_PASS;
+    dmarc_status = US"accept";
+    dmarc_pass_fail = US"pass";
+    dmarc_status_text = US"Accept";
+    dmarc_action = DMARC_RESULT_ACCEPT;
+    }
+
+/* RFC 7489 6.6.2 step 6 - dispose of no-alignment per discovered policy */
+  else
+    {
+    dmarc_status = dmarc_domain_policy;
+    if (Ustrcmp(dmarc_domain_policy, "none") == 0)
+      {							/* Accept and report */
+      dmarc_policy = DMARC_POLICY_NONE;
+      dmarc_pass_fail = US"none";
+      dmarc_status_text = US"None, Accept";
+      dmarc_action = DMARC_RESULT_ACCEPT;
+      }
+    else if (Ustrcmp(dmarc_domain_policy, "quarantine") == 0)
+      {							/* Explicit quarantine*/
+      dmarc_policy = DMARC_POLICY_QUARANTINE;
+      dmarc_pass_fail = US"fail";
+      dmarc_status_text = US"Quarantine";
+      dmarc_action = DMARC_RESULT_QUARANTINE;
+      }
+    else if (Ustrcmp(dmarc_domain_policy, "reject") == 0)
+      {							/* Explicit reject */
+      dmarc_policy = DMARC_POLICY_REJECT;
+      dmarc_pass_fail = US"fail";
+      dmarc_status_text = US"Reject";
+      dmarc_action = DMARC_RESULT_REJECT;
+      }
+    else	/* should never happen; tag values were validated */
+      {		/* could use similar for dns tmpfail */
+      dmarc_status = dmarc_pass_fail = US"temperror";
+      dmarc_status_text = US"Internal Policy Error";
+      dmarc_action = DMARC_RESULT_TEMPFAIL;
+      }
+    }
+
+  // the alignments would be results of the DMARC evaluation
+  // - we have them already, from the dkim & spf processing
+
+  if (has_dmarc_record && !dmarc_abort)
+    {
+    /* Log results.  Robably should have a log_selector to reduce noise. */
+
+    log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s "
+			   "spf_align=%s dkim_align=%s enforcement='%s'",
+			   spf_sender_domain, dmarc_used_domain,
+			   dmarc_alignment_spf  ? "yes" : "no",
+			   dmarc_alignment_dkim ? "yes" : "no",
+			   dmarc_status_text);
+
+
+    /* History file, for later aggregate reporting. */
+
+    dmarc_pct = atoi(CCS dmarc_parsed.pct);
+
+    dmarc_adkim = dmarc_parsed.adkim
+		? *dmarc_parsed.adkim : DMARC_RECORD_A_UNSPECIFIED;
+    dmarc_aspf = dmarc_parsed.aspf 
+		? *dmarc_parsed.aspf : DMARC_RECORD_A_UNSPECIFIED;
+    dmarc_dom_policy = dmarc_parsed.p
+		? *dmarc_parsed.p : DMARC_RECORD_P_UNSPECIFIED;
+    dmarc_subdom_policy = dmarc_parsed.sp
+		? *dmarc_parsed.sp : DMARC_RECORD_P_UNSPECIFIED;
+
+/* RFC 7489 6.3 - rua is a comma-sep list */
+    dmarc_rua = dmarc_clist_to_array(dmarc_parsed.rua);
+
+    dmarc_write_history_file(dkim_history_buffer);
+
+    /* Forensic reporting */
+
+    dmarc_maybe_send_forensic(dmarc_parsed.ruf);
+    }
+  }
+
+out:
+  DEBUG(D_receive)
+    {
+    expand_level--;
+    debug_printf_indent("DMARC: finished process, status %q\n", dmarc_status);
+    }
+  return OK;
+}
+
+static const uschar *
+dmarc_exim_expand_defaults(void)
+{
+return f.dmarc_disable_verify ?  US"off" : US"none";
+}
+
+/*API*/
+const uschar *
+dmarc_exim_expand_query(void)
+{
+if (f.dmarc_disable_verify )            // || !dmarc_pctx)
+  return dmarc_exim_expand_defaults();
+
+return dmarc_status;
+}
+
+
+# endif /* have SPF & DKIM */
+#endif /* EXPERIMENTAL_DMARC_NATIVE */
+/* vi: aw ai sw=2
+ */
diff --git a/src/src/miscmods/spf.c b/src/src/miscmods/spf.c
index 9d215f516..f73a8776b 100644
--- a/src/src/miscmods/spf.c
+++ b/src/src/miscmods/spf.c
@@ -84,18 +84,18 @@ SPF_dns_rr_t srr = {
   .source = spf_dns_server
 };
 
-DEBUG(D_receive) debug_printf("SPF_dns_exim_lookup '%s'\n", domain);
+DEBUG(D_receive)
+  { debug_printf_indent("SPF_dns_exim_lookup '%s'\n", domain); expand_level++; }
 
 /* Shortcircuit SPF RR lookups by returning NO_DATA.  They were obsoleted by
 RFC 6686/7208 years ago. see bug #1294 */
 
 if (rr_type == T_SPF)
   {
-  HDEBUG(D_host_lookup) debug_printf("faking NO_DATA for SPF RR(99) lookup\n");
+  HDEBUG(D_host_lookup)
+    debug_printf_indent("faking NO_DATA for SPF RR(99) lookup\n");
   srr.herrno = NO_DATA;
-  SPF_dns_rr_dup(&spfrr, &srr);
-  store_free_dns_answer(dnsa);
-  return spfrr;
+  goto out;
   }
 
 switch (dns_lookup(dnsa, US domain, rr_type, NULL))
@@ -115,11 +115,7 @@ switch (dns_lookup(dnsa, US domain, rr_type, NULL))
   }
 
 if (found == 0)
-  {
-  SPF_dns_rr_dup(&spfrr, &srr);
-  store_free_dns_answer(dnsa);
-  return spfrr;
-  }
+  goto out;
 
 srr.rr = store_malloc(sizeof(SPF_dns_rr_data_t) * found);
 
@@ -153,8 +149,8 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
 	if (rr->size < 1+6) continue;		/* min for version str */
 	if (strncmpic(rr->data+1, US SPF_VER_STR, 6) != 0)
 	  {
-	  HDEBUG(D_host_lookup) debug_printf("not an spf record: %.*s\n",
-	    (int) s[0], s+1);
+	  HDEBUG(D_host_lookup) debug_printf_indent("not an spf record: %.*s\n",
+						    (int) s[0], s+1);
 	  continue;
 	  }
 
@@ -170,7 +166,7 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
 	  continue;
 	gstring_release_unused(g);
 	s = string_copy_malloc(string_from_gstring(g));
-	DEBUG(D_receive) debug_printf("SPF_dns_exim_lookup '%s'\n", s);
+	DEBUG(D_receive) debug_printf_indent("SPF_dns_exim_lookup '%s'\n", s);
 	break;
 	}
 
@@ -192,10 +188,13 @@ empty ANSWER section. */
 if (!(srr.num_rr = found))
   srr.herrno = NO_DATA;
 
-/* spfrr->rr must have been malloc()d for this */
-SPF_dns_rr_dup(&spfrr, &srr);
-store_free_dns_answer(dnsa);
-return spfrr;
+out:
+  /* spfrr->rr must have been malloc()d for this */
+  SPF_dns_rr_dup(&spfrr, &srr);
+
+  DEBUG(D_receive) expand_level--;
+  store_free_dns_answer(dnsa);
+  return spfrr;
 }
 
 
@@ -205,7 +204,7 @@ SPF_dns_exim_new(int debug)
 {
 SPF_dns_server_t * spf_dns_server = store_malloc(sizeof(SPF_dns_server_t));
 
-/* DEBUG(D_receive) debug_printf("SPF_dns_exim_new\n"); */
+/* DEBUG(D_receive) debug_printf_indent("SPF_dns_exim_new\n"); */
 
 memset(spf_dns_server, 0, sizeof(SPF_dns_server_t));
 spf_dns_server->destroy      = NULL;
@@ -252,17 +251,17 @@ testsuite. */
 
 if (!(dc = SPF_dns_exim_new(debug)))
   {
-  DEBUG(D_receive) debug_printf("spf: SPF_dns_exim_new() failed\n");
+  DEBUG(D_receive) debug_printf_indent("SPF_dns_exim_new() failed\n");
   return FALSE;
   }
 if (!(dc = SPF_dns_cache_new(dc, NULL, debug, 8)))
   {
-  DEBUG(D_receive) debug_printf("spf: SPF_dns_cache_new() failed\n");
+  DEBUG(D_receive) debug_printf_indent("SPF_dns_cache_new() failed\n");
   return FALSE;
   }
 if (!(spf_server = SPF_server_new_dns(dc, debug)))
   {
-  DEBUG(D_receive) debug_printf("spf: SPF_server_new() failed.\n");
+  DEBUG(D_receive) debug_printf_indent("SPF_server_new() failed.\n");
   return FALSE;
   }
 
@@ -294,8 +293,8 @@ static int
 spf_conn_init(const uschar * spf_helo_domain, const uschar * spf_remote_addr,
   const uschar ** errstr)
 {
-DEBUG(D_receive)
-  debug_printf("spf_conn_init: %s %s\n", spf_helo_domain, spf_remote_addr);
+DEBUG(D_receive) debug_printf_indent("spf_conn_init: %s %s\n",
+				      spf_helo_domain, spf_remote_addr);
 
 if (!spf_server && !spf_init(NULL))
   {
@@ -305,8 +304,8 @@ if (!spf_server && !spf_init(NULL))
 
 if (SPF_server_set_rec_dom(spf_server, CS primary_hostname))
   {
-  DEBUG(D_receive) debug_printf("spf: SPF_server_set_rec_dom(%q) failed.\n",
-    primary_hostname);
+  DEBUG(D_receive) debug_printf_indent("SPF_server_set_rec_dom(%q) failed.\n",
+					primary_hostname);
   spf_server = NULL;
   *errstr = US"spf: setting host name";
   return FAIL;
@@ -319,7 +318,7 @@ if (  SPF_request_set_ipv4_str(spf_request, CCS spf_remote_addr)
    )
   {
   DEBUG(D_receive)
-    debug_printf("spf: SPF_request_set_ipv4_str() and "
+    debug_printf_indent("SPF_request_set_ipv4_str() and "
       "SPF_request_set_ipv6_str() failed [%s]\n", spf_remote_addr);
   spf_server = NULL;
   spf_request = NULL;
@@ -329,8 +328,8 @@ if (  SPF_request_set_ipv4_str(spf_request, CCS spf_remote_addr)
 
 if (SPF_request_set_helo_dom(spf_request, CCS spf_helo_domain))
   {
-  DEBUG(D_receive) debug_printf("spf: SPF_set_helo_dom(%q) failed.\n",
-    spf_helo_domain);
+  DEBUG(D_receive) debug_printf_indent("SPF_set_helo_dom(%q) failed.\n",
+				      spf_helo_domain);
   spf_server = NULL;
   spf_request = NULL;
   *errstr = US"spf: setting helo string";
@@ -352,14 +351,14 @@ static void
 spf_response_debug(SPF_response_t * spf_response)
 {
 if (SPF_response_messages(spf_response) == 0)
-  debug_printf(" (no errors)\n");
+  debug_printf_indent(" (no errors)\n");
 else for (int i = 0; i < SPF_response_messages(spf_response); i++)
   {
   SPF_error_t * err = SPF_response_message(spf_response, i);
-  debug_printf( "%s_msg = (%d) %s\n",
-		  (SPF_error_errorp(err) ? "warn" : "err"),
-		  SPF_error_code(err),
-		  SPF_error_message(err));
+  debug_printf_indent("%s_msg = (%d) %s\n",
+		      SPF_error_errorp(err) ? "warn" : "err",
+		      SPF_error_code(err),
+		      SPF_error_message(err));
   }
 }
 
@@ -375,11 +374,11 @@ spf_process(const uschar ** listptr, const uschar * spf_envelope_sender,
   int action)
 {
 int sep = 0;
-const uschar *list = *listptr;
-uschar *spf_result_id;
-int rc = SPF_RESULT_PERMERROR;
+const uschar * list = * listptr;
+uschar * spf_result_id;
+int rc = SPF_RESULT_PERMERROR, ret = OK;
 
-DEBUG(D_receive) debug_printf("spf_process\n");
+DEBUG(D_receive) { debug_printf_indent("SPF: process\n"); expand_level++; }
 
 if (!(spf_server && spf_request))
   /* no global context, assume temp error and skip to evaluation */
@@ -412,24 +411,32 @@ else
   }
 
 /* We got a result. Now see if we should return OK or FAIL for it */
-DEBUG(D_acl) debug_printf("SPF result is %s (%d)\n", SPF_strresult(rc), rc);
+DEBUG(D_acl)
+  debug_printf_indent("SPF: result is %s (%d)\n", SPF_strresult(rc), rc);
 
 if (action == SPF_PROCESS_GUESS && (!strcmp (SPF_strresult(rc), "none")))
-  return spf_process(listptr, spf_envelope_sender, SPF_PROCESS_FALLBACK);
+  ret = spf_process(listptr, spf_envelope_sender, SPF_PROCESS_FALLBACK);
 
-while ((spf_result_id = string_nextinlist(&list, &sep, NULL, 0)))
+else
   {
-  BOOL negate, result;
+  while ((spf_result_id = string_nextinlist(&list, &sep, NULL, 0)))
+    {
+    BOOL negate, result;
 
-  if ((negate = spf_result_id[0] == '!'))
-    spf_result_id++;
+    if ((negate = spf_result_id[0] == '!'))
+      spf_result_id++;
+
+    result = Ustrcmp(spf_result_id, spf_result_id_list[rc].name) == 0;
+    if (negate != result) goto out;
+    }
 
-  result = Ustrcmp(spf_result_id, spf_result_id_list[rc].name) == 0;
-  if (negate != result) return OK;
+  /* no match */
+  ret = FAIL;
   }
 
-/* no match */
-return FAIL;
+out:
+  DEBUG(D_receive) expand_level--;
+  return ret;
 }
 
 
@@ -457,11 +464,11 @@ if (spf_result)
       ? string_append(g, 2, US" smtp.helo=", s)
       : string_cat(g, US" smtp.mailfrom=<>");
     }
-  DEBUG(D_acl) debug_printf("SPF:\tauthres '%.*s'\n",
+  DEBUG(D_acl) debug_printf_indent("SPF:\tauthres '%.*s'\n",
 		  gstring_length(g) - start - 3, g->s + start + 3);
   }
 else
-  DEBUG(D_acl) debug_printf("SPF:\tno authres\n");
+  DEBUG(D_acl) debug_printf_indent("SPF:\tno authres\n");
 return g;
 }
 
@@ -478,7 +485,7 @@ if (spf_response)
   s = US spf_response->header_comment;
   }
 *human_readable_p = s ? string_copy(s) : US"";
-DEBUG(D_acl) debug_printf("SPF: %d '%s'\n", res, s);
+DEBUG(D_acl) debug_printf_indent(" SPF: %d '%s'\n", res, s);
 return res;
 }
 
diff --git a/src/src/miscmods/spf_perl.c b/src/src/miscmods/spf_perl.c
index e7bc1e331..33871c9aa 100644
--- a/src/src/miscmods/spf_perl.c
+++ b/src/src/miscmods/spf_perl.c
@@ -298,7 +298,7 @@ if (spf_result)
   }
 
 *human_readable_p = s ? string_copy(s) : US"";
-DEBUG(D_acl) debug_printf_indent("SPF: %d '%s'\n", res, s);
+DEBUG(D_acl) debug_printf_indent(" SPF: %d '%s'\n", res, s);
 return res;
 }
 
diff --git a/src/src/moan.c b/src/src/moan.c
index 5635f93e2..01bf4f578 100644
--- a/src/src/moan.c
+++ b/src/src/moan.c
@@ -167,7 +167,7 @@ int written = 0, fd, status, count = 0, size_limit = bounce_return_size_limit;
 FILE * fp;
 int pid;
 
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
 uschar * s, * s2;
 
 /* For DMARC if there is a specific sender set, expand the variable for the
@@ -206,7 +206,7 @@ fp = fdopen(fd, "wb");
 if (errors_reply_to) fprintf(fp, "Reply-To: %s\n", errors_reply_to);
 fprintf(fp, "Auto-Submitted: auto-replied\n");
 
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
 if (s)
   fprintf(fp, "From: %s\n", s);
 else
@@ -324,23 +324,16 @@ switch(ident)
   fprintf(fp, "\n");
   break;
 
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
   case ERRMESS_DMARC_FORENSIC:
     bounce_return_message = TRUE;
     bounce_return_body    = FALSE;
-    fprintf(fp, "Subject: DMARC Forensic Report for %s from IP %s\n\n",
-	  eblock ? eblock->text2 : US"Unknown",
-          sender_host_address);
-    fprintf(fp,
-      "A message claiming to be from you has failed the published DMARC\n"
-      "policy for your domain.\n\n");
-    while (eblock)
-      {
-      fprintf(fp, "  %s: %s\n", eblock->text1, eblock->text2);
-      count++;
-      eblock = eblock->next;
-      }
-  break;
+    for (; eblock; eblock = eblock->next)
+      if (eblock->text2)
+	fprintf(fp, "  %s: %s\n", eblock->text1, eblock->text2);
+      else
+	fprintf(fp, "%s", eblock->text1);
+    break;
 #endif
 
   default:
@@ -435,13 +428,13 @@ if (bounce_return_message)
       fputs(CS buf, fp);
       }
     }
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
   /* Overkill, but use exact test in case future code gets inserted */
-  else if (bounce_return_body && message_file == NULL)
+  else if (bounce_return_body && !message_file)
     {
     /*XXX limit line length here? */
     /* This doesn't print newlines, disable until can parse and fix
-     * output to be legible.  */
+    output to be legible.  */
     fprintf(fp, "%s", expand_string(US"$message_body"));
     }
 #endif
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 1fe2b2bfc..04cd5e68a 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -124,7 +124,7 @@ static optionlist optionlist_config[] = {
   { "dkim_verify_minimal",      opt_module,	 {US"dkim"} },
   { "dkim_verify_signers",      opt_module,	 {US"dkim"} },
 #endif
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
   { "dmarc_forensic_sender",    opt_module,	 {US"dmarc"} },
   { "dmarc_history_file",       opt_module,	 {US"dmarc"} },
   { "dmarc_tld_file",           opt_module,	 {US"dmarc"} },
diff --git a/src/src/receive.c b/src/src/receive.c
index cde09c40a..0f407ce93 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -16,7 +16,7 @@
 extern int dcc_ok;
 #endif
 
-#ifdef SUPPORT_DMARC
+#ifdef EXIM_HAVE_DMARC
 # include "miscmods/dmarc.h"
 #endif
 
@@ -1754,9 +1754,6 @@ BOOL date_header_exists = FALSE;
 /* Pointers to receive the addresses of headers whose contents we need. */
 
 header_line * from_header = NULL;
-#ifdef SUPPORT_DMARC
-header_line * dmarc_from_header = NULL;
-#endif
 header_line * subject_header = NULL, * msgid_header = NULL, * received_header;
 BOOL msgid_header_newly_created = FALSE;
 
@@ -2454,8 +2451,16 @@ for (header_line * h = header_list->next; h; h = h->next)
 
     case htype_from:
       h->type = htype_from;
-#ifdef SUPPORT_DMARC
-      if (!is_resent) dmarc_from_header = h;
+#ifdef EXIM_HAVE_DMARC
+      if (!is_resent && !f.dmarc_disable_verify)
+	{
+	misc_module_info * mi = misc_mod_findonly(US"dmarc");
+	if (mi)
+	  {
+	  typedef void (*fn_t)(const uschar *);
+	  (((fn_t *) mi->functions)[DMARC_STORE_FROMHDR]) (h->text);
+	  }
+	}
 #endif
       if (!resents_exist || is_resent)
 	{
@@ -3584,17 +3589,6 @@ else
       }
 #endif /* WITH_CONTENT_SCAN */
 
-#ifdef SUPPORT_DMARC
-    {
-    misc_module_info * mi = misc_mod_findonly(US"dmarc");
-    if (mi)
-      {
-      typedef int (*fn_t)(header_line *);
-      (((fn_t *) mi->functions)[DMARC_STORE_DATA]) (dmarc_from_header);
-      }
-    }
-#endif
-
 #ifndef DISABLE_PRDR
     if (prdr_requested && recipients_count > 1)
       {
diff --git a/src/src/regex_cache.c b/src/src/regex_cache.c
index b92556d92..713da90d5 100644
--- a/src/src/regex_cache.c
+++ b/src/src/regex_cache.c
@@ -142,7 +142,7 @@ size_t offset;
 const pcre2_code * yield;
 int old_pool = store_pool, err;
 
-/* Optionall, check the cache and return if found */
+/* Optionally, check the cache and return if found */
 
 if (  flags & MCS_CACHEABLE
    && (yield = regex_from_cache(pattern, caseless)))

commit 726e6f798bc2a18b019a23ba48ee6ddbf68ebf7f
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Nov 14 11:58:10 2025 +0000

    Build: fix spf dynamic-module build
    
    Broken-by: 998636a4f849

diff --git a/src/src/EDITME b/src/src/EDITME
index d38ac98e5..a2dddd780 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -406,7 +406,8 @@ TRANSPORT_SMTP=yes
 # for the specialist case of using the DNS as a general database facility (not
 # common).
 # If set to "2" instead of "yes" then the corresponding lookup will be
-# built as a module and must be installed into LOOKUP_MODULE_DIR. You need to
+# built as a module and left in the "dynlibs" directory under the build
+# directory. They must be installed into LOOKUP_MODULE_DIR. You need to
 # add -export-dynamic -rdynamic to EXTRALIBS. You may also need to add -ldl to
 # EXTRALIBS so that dlopen() is available to Exim. You need to define
 # LOOKUP_MODULE_DIR above so the exim binary actually loads dynamic lookup
diff --git a/src/src/lookups/spf.c b/src/src/lookups/spf.c
index ce314f7e5..bfce04fa7 100644
--- a/src/src/lookups/spf.c
+++ b/src/src/lookups/spf.c
@@ -86,11 +86,15 @@ return FAIL;
 gstring *
 spf_version_report(gstring * g)
 {
+#if EXIM_HAVE_SPF == 2
+return g;
+#else
 int maj, min, patch;
 
 SPF_get_lib_version(&maj, &min, &patch);
 return string_fmt_append(g, "Library version: SPF: Runtime: %d.%d.%d\n",
       maj, min, patch);
+#endif
 }
 
 

commit 03c4dd24276bfb43f5f803fa7c8cc83076f1c485
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Nov 14 12:11:10 2025 +0000

    LDAP: fix version report under dynamic build

diff --git a/src/src/lookups/ldap.c b/src/src/lookups/ldap.c
index 7d240309c..b73413a39 100644
--- a/src/src/lookups/ldap.c
+++ b/src/src/lookups/ldap.c
@@ -1576,7 +1576,7 @@ if (ldap_get_option(ld, LDAP_OPT_API_INFO, &info) == LDAP_OPT_SUCCESS)
   g = string_fmt_append(g, "                       Runtime %s %d\n",
       info.ldapai_vendor_name, info.ldapai_vendor_version);
 
-  for (char ** sp = info.ldapai_extensions; *sp; *sp++)
+  for (char ** sp = info.ldapai_extensions; *sp; sp++)
     ldap_memfree(*sp);
   ldap_memfree(info.ldapai_extensions);
   ldap_memfree(info.ldapai_vendor_name);

commit 11f6b4c7d34582a3b046345dd67d576b3fef920f
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Nov 14 14:17:14 2025 +0000

    DMARC: fix history-file ARC reporting, under dynamic build

diff --git a/src/src/miscmods/dmarc_common.c b/src/src/miscmods/dmarc_common.c
index 168e7da8e..059b31167 100644
--- a/src/src/miscmods/dmarc_common.c
+++ b/src/src/miscmods/dmarc_common.c
@@ -418,6 +418,9 @@ g = string_fmt_append(g, "align_dkim %d\n"
   gstring * g2 = NULL;
   typedef const uschar * (*fn_t)(gstring **);
 
+  if (!dmarc_arc_mod_info)
+    dmarc_arc_mod_info = misc_mod_findonly(US"arc");
+
   if (  dmarc_arc_mod_info
      && (s = (((fn_t *) dmarc_arc_mod_info->functions)[ARC_ARCSET_INFO]) (&g2)))
     {
@@ -437,7 +440,7 @@ g = string_fmt_append(g, "align_dkim %d\n"
   else
 
 # endif
-    g = string_fmt_append(g, "arc %d\narc_policy %d json:[]\n",
+    g = string_fmt_append(g, "arc %d\narc_policy %d json[ ]\n",
 		      ARES_RESULT_UNKNOWN, DMARC_ARC_POLICY_RESULT_UNUSED);
   }
 #endif

commit 68841125d8ba6168ae38de296273bdcd4df592c8
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 16 15:16:53 2025 +0000

        tidying

diff --git a/src/src/functions.h b/src/src/functions.h
index bd390cce6..73371cc7b 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -1080,7 +1080,7 @@ if (gstring_last_char(g) == c) gstring_trim(g, 1);
 static inline void
 gstring_reset(gstring * g)
 {
-g->ptr = 0;
+g->s[g->ptr = 0] = '\0';
 }
 
 
diff --git a/src/src/host.c b/src/src/host.c
index cfa2620df..28e08ee23 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -836,6 +836,7 @@ Returns:     pointer to character string
 uschar *
 host_ntoa(int type, const void * arg, uschar * buffer, int * portptr)
 {
+#define NTOA_BSIZE 46
 uschar * yield;
 
 /* The new world. It is annoying that we have to fish out the address from
@@ -845,7 +846,7 @@ function inet_ntoa() returns just uschar *, and some picky compilers insist
 on warning if one assigns a const uschar * to a uschar *. Hence the casts. */
 
 #if HAVE_IPV6
-uschar addr_buffer[46];
+uschar addr_buffer[NTOA_BSIZE];
 if (type < 0)
   {
   int family = ((struct sockaddr *)arg)->sa_family;
@@ -888,7 +889,7 @@ else
 
 /* If there is no buffer, put the string into some new store. */
 
-if (!buffer) buffer = store_get(46, GET_UNTAINTED);
+if (!buffer) buffer = store_get(NTOA_BSIZE, GET_UNTAINTED);
 
 /* Callers of this function with a non-NULL buffer must ensure that it is
 large enough to hold an IPv6 address, namely, at least 46 bytes. That's what
@@ -896,9 +897,10 @@ makes this use of strcpy() OK.
 If the library returned apparently an apparently tainted string, clean it;
 we trust IP addresses. */
 
-string_format_nt(buffer, 46, "%s", yield);
+string_format_nt(buffer, NTOA_BSIZE, "%s", yield);
 return buffer;
 }
+#undef NTOA_BSIZE
 
 
 
diff --git a/src/src/miscmods/dmarc_native.c b/src/src/miscmods/dmarc_native.c
index ce4aaee4f..617517d84 100644
--- a/src/src/miscmods/dmarc_native.c
+++ b/src/src/miscmods/dmarc_native.c
@@ -299,7 +299,6 @@ dmarc_process(void)
 {
 const uschar * rr;
 BOOL has_dmarc_record = TRUE;
-u_char ** ruf; /* forensic report addressees, if called for */
 
 dmarc_alignment_spf = dmarc_alignment_dkim = FALSE;
 dmarc_dkim_alignment = DMARC_POLICY_DKIM_ALIGNMENT_FAIL;
diff --git a/src/src/miscmods/pdkim/pdkim.c b/src/src/miscmods/pdkim/pdkim.c
index a43ea9bfb..a7ec5f511 100644
--- a/src/src/miscmods/pdkim/pdkim.c
+++ b/src/src/miscmods/pdkim/pdkim.c
@@ -1000,10 +1000,12 @@ else
       last_sig->next = sig;
       }
 
+    /* Check for too many signatures */
+
     if (dkim_collect_input && --dkim_collect_input == 0)
       {
       ctx->headers = pdkim_prepend_stringlist(ctx->headers, g->s);
-      g->s[g->ptr = 0] = '\0';
+      gstring_reset(g);
       return PDKIM_ERR_EXCESS_SIGS;
       }
     }
@@ -1013,7 +1015,7 @@ else
   }
 
 BAIL:
-g->s[g->ptr = 0] = '\0';	/* leave buffer for reuse */
+gstring_reset(g);	/* leave buffer for reuse */
 return PDKIM_OK;
 }
 

commit 3c24de41ada52816d2890033b99eda0543a06b06
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Nov 21 11:54:08 2025 +0000

    debug: indents

diff --git a/src/OS/os.c-Linux b/src/OS/os.c-Linux
index 34dd7c190..b7cd4e53a 100644
--- a/src/OS/os.c-Linux
+++ b/src/OS/os.c-Linux
@@ -140,7 +140,7 @@ while (fscanf(f, "%4s%4s%4s%4s%4s%4s%4s%4s %02x %02x %02x %02x %20s\n",
     }
 
   DEBUG(D_interface)
-    debug_printf("Actual local interface address is %s (%s)\n", last->address,
+    debug_printf_indent("Actual local interface address is %s (%s)\n", last->address,
       devname);
   }
 fclose(f);
diff --git a/src/OS/unsupported/os.c-IRIX b/src/OS/unsupported/os.c-IRIX
index 6f043d044..67725322b 100644
--- a/src/OS/unsupported/os.c-IRIX
+++ b/src/OS/unsupported/os.c-IRIX
@@ -106,7 +106,7 @@ for (nextaddr = buf; nextaddr < lim; nextaddr += ifm->ifm_msglen)
         last = next;
         }
 
-      DEBUG(D_interface) debug_printf("Actual local interface address is %s\n",
+      DEBUG(D_interface) debug_printf_indent("Actual local interface address is %s\n",
         last->address);
       }
     }
diff --git a/src/OS/unsupported/os.c-IRIX6 b/src/OS/unsupported/os.c-IRIX6
index 6f043d044..67725322b 100644
--- a/src/OS/unsupported/os.c-IRIX6
+++ b/src/OS/unsupported/os.c-IRIX6
@@ -106,7 +106,7 @@ for (nextaddr = buf; nextaddr < lim; nextaddr += ifm->ifm_msglen)
         last = next;
         }
 
-      DEBUG(D_interface) debug_printf("Actual local interface address is %s\n",
+      DEBUG(D_interface) debug_printf_indent("Actual local interface address is %s\n",
         last->address);
       }
     }
diff --git a/src/OS/unsupported/os.c-IRIX632 b/src/OS/unsupported/os.c-IRIX632
index 6f043d044..67725322b 100644
--- a/src/OS/unsupported/os.c-IRIX632
+++ b/src/OS/unsupported/os.c-IRIX632
@@ -106,7 +106,7 @@ for (nextaddr = buf; nextaddr < lim; nextaddr += ifm->ifm_msglen)
         last = next;
         }
 
-      DEBUG(D_interface) debug_printf("Actual local interface address is %s\n",
+      DEBUG(D_interface) debug_printf_indent("Actual local interface address is %s\n",
         last->address);
       }
     }
diff --git a/src/OS/unsupported/os.c-IRIX65 b/src/OS/unsupported/os.c-IRIX65
index 6f043d044..67725322b 100644
--- a/src/OS/unsupported/os.c-IRIX65
+++ b/src/OS/unsupported/os.c-IRIX65
@@ -106,7 +106,7 @@ for (nextaddr = buf; nextaddr < lim; nextaddr += ifm->ifm_msglen)
         last = next;
         }
 
-      DEBUG(D_interface) debug_printf("Actual local interface address is %s\n",
+      DEBUG(D_interface) debug_printf_indent("Actual local interface address is %s\n",
         last->address);
       }
     }
diff --git a/src/src/host.c b/src/src/host.c
index 28e08ee23..4c513e28c 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -413,7 +413,7 @@ else if (Ustrchr(h->name, ':') == p)
   h->name = string_copyn(h->name, p - h->name);
 else return PORT_NONE;
 
-DEBUG(D_route|D_host_lookup) debug_printf("host=%s port=%d\n", h->name, port);
+DEBUG(D_route|D_host_lookup) debug_printf_indent("host=%s port=%d\n", h->name, port);
 return port;
 }
 
@@ -792,7 +792,7 @@ if (!local_interface_data)
       local_interface_data = add_unique_interface(local_interface_data, ipa);
       DEBUG(D_interface)
         {
-        debug_printf("Configured local interface: address=%s", ipa->address);
+        debug_printf_indent("Configured local interface: address=%s", ipa->address);
         if (ipa->port != 0) debug_printf(" port=%d", ipa->port);
         debug_printf("\n");
         }
@@ -1408,7 +1408,7 @@ while (host != *lastptr)
       if (h->next->address != NULL &&
           Ustrcmp(h->next->address, host->address) == 0)
         {
-        DEBUG(D_host_lookup) debug_printf("duplicate IP address %s (MX=%d) "
+        DEBUG(D_host_lookup) debug_printf_indent("duplicate IP address %s (MX=%d) "
           "removed\n", host->address, h->next->mx);
         if (h->next == *lastptr) *lastptr = h;
         h->next = h->next->next;
@@ -1490,7 +1490,7 @@ if (  slow_lookup_log
 
 if (!hosts)
   {
-  HDEBUG(D_host_lookup) debug_printf("IP address lookup failed: h_errno=%d\n",
+  HDEBUG(D_host_lookup) debug_printf_indent("IP address lookup failed: h_errno=%d\n",
     h_errno);
   return (h_errno == TRY_AGAIN || h_errno == NO_RECOVERY) ? DEFER : FAIL;
   }
@@ -1602,7 +1602,7 @@ if (f.running_in_test_harness &&
     Ustrcmp(sender_host_address, "99.99.99.99") == 0)
   {
   HDEBUG(D_host_lookup)
-    debug_printf("Test harness: host name lookup returns DEFER\n");
+    debug_printf_indent("Test harness: host name lookup returns DEFER\n");
   host_lookup_deferred = TRUE;
   yield = DEFER;
   goto out;
@@ -1701,7 +1701,7 @@ while ((ordername = string_nextinlist(&list, &sep, NULL, 0)))
     if (rc == DNS_AGAIN)
       {
       HDEBUG(D_host_lookup)
-        debug_printf("IP address PTR lookup gave temporary error\n");
+        debug_printf_indent("IP address PTR lookup gave temporary error\n");
       host_lookup_deferred = TRUE;
       yield = DEFER;
       goto out;
@@ -1713,7 +1713,7 @@ while ((ordername = string_nextinlist(&list, &sep, NULL, 0)))
   else if (strcmpic(ordername, US"byaddr") == 0)
     {
     HDEBUG(D_host_lookup)
-      debug_printf("IP address lookup using gethostbyaddr()\n");
+      debug_printf_indent("IP address lookup using gethostbyaddr()\n");
     rc = host_name_lookup_byaddr();
     if (rc == DEFER)
       {
@@ -1786,27 +1786,27 @@ for (uschar * hname = sender_host_name; hname; hname = *aliases++)
     for (host_item * hh = &h; hh; hh = hh->next)
       if (host_is_in_net(hh->address, sender_host_address, 0))
         {
-        HDEBUG(D_host_lookup) debug_printf("  %s OK\n", hh->address);
+        HDEBUG(D_host_lookup) debug_printf_indent("  %s OK\n", hh->address);
         ok = TRUE;
         break;
         }
       else
-        HDEBUG(D_host_lookup) debug_printf("  %s\n", hh->address);
+        HDEBUG(D_host_lookup) debug_printf_indent("  %s\n", hh->address);
 
     if (!ok) HDEBUG(D_host_lookup)
-      debug_printf("no IP address for %s matched %s\n", hname,
+      debug_printf_indent("no IP address for %s matched %s\n", hname,
         sender_host_address);
     }
   else if (rc == HOST_FIND_AGAIN)
     {
-    HDEBUG(D_host_lookup) debug_printf("temporary error for host name lookup\n");
+    HDEBUG(D_host_lookup) debug_printf_indent("temporary error for host name lookup\n");
     host_lookup_deferred = TRUE;
     sender_host_name = NULL;
     yield = DEFER;
     goto out;
     }
   else
-    HDEBUG(D_host_lookup) debug_printf("no IP addresses found for %s\n", hname);
+    HDEBUG(D_host_lookup) debug_printf_indent("no IP addresses found for %s\n", hname);
 
   /* If this name is no good, and it's the sender name, set it null pro tem;
   if it's an alias, just remove it from the list. */
@@ -1835,7 +1835,7 @@ if (sender_host_name) { yield = OK; goto out; }
 /* We have failed to find an address that matches. */
 
 HDEBUG(D_host_lookup)
-  debug_printf("%s does not match any IP address for %s\n",
+  debug_printf_indent("%s does not match any IP address for %s\n",
     sender_host_address, save_hostname);
 
 /* This message must be in permanent store */
@@ -2051,7 +2051,7 @@ for (int i = 1; i <= times;
 	    text_address, NULL) == OK)
       {
       DEBUG(D_host_lookup)
-        debug_printf("ignored host %s [%s]\n", host->name, text_address);
+        debug_printf_indent("ignored host %s [%s]\n", host->name, text_address);
       continue;
       }
     #endif
@@ -2130,8 +2130,8 @@ yield = local_host_check?
 HDEBUG(D_host_lookup)
   {
   if (fully_qualified_name)
-    debug_printf("fully qualified name = %s\n", *fully_qualified_name);
-  debug_printf("%s looked up these IP addresses:\n",
+    debug_printf_indent("fully qualified name = %s\n", *fully_qualified_name);
+  debug_printf_indent("%s looked up these IP addresses:\n",
     #if HAVE_IPV6
       #if HAVE_GETIPNODEBYNAME
       "getipnodebyname"
@@ -2143,7 +2143,7 @@ HDEBUG(D_host_lookup)
     #endif
     );
   for (const host_item * h = host; h != last->next; h = h->next)
-    debug_printf("  name=%s address=%s\n", h->name,
+    debug_printf_indent("  name=%s address=%s\n", h->name,
       h->address ? h->address : US"<null>");
   }
 
@@ -2165,7 +2165,7 @@ RETURN_AGAIN:
   deliver_domain = save;
   if (rc == OK)
     {
-    DEBUG(D_host_lookup) debug_printf("%s is in dns_again_means_nonexist: "
+    DEBUG(D_host_lookup) debug_printf_indent("%s is in dns_again_means_nonexist: "
       "returning HOST_FIND_FAILED\n", host->name);
     return HOST_FIND_FAILED;
     }
@@ -2297,7 +2297,7 @@ for (; i >= 0; i--)
        && !dns_is_secure(dnsa)
        && dns_is_aa(dnsa)
        )
-      debug_printf("DNS lookup of %.256s (A/AAAA) requested AD, but got AA\n", host->name);
+      debug_printf_indent("DNS lookup of %.256s (A/AAAA) requested AD, but got AA\n", host->name);
 
   /* We want to return HOST_FIND_AGAIN if one of the A or AAAA lookups
   fails or times out, but not if another one succeeds. (In the early
@@ -2328,7 +2328,7 @@ for (; i >= 0; i--)
     {
     if (dns_is_secure(dnsa))
       {
-      DEBUG(D_host_lookup) debug_printf("%s A DNSSEC\n", host->name);
+      DEBUG(D_host_lookup) debug_printf_indent("%s A DNSSEC\n", host->name);
       if (host->dnssec_used == DS_UNK) /* set in host_find_bydns() */
 	host->dnssec_used = DS_YES;
       }
@@ -2337,13 +2337,13 @@ for (; i >= 0; i--)
       if (dnssec_require)
 	{
 	dnssec_fail = TRUE;
-	DEBUG(D_host_lookup) debug_printf("dnssec fail on %s for %.256s",
+	DEBUG(D_host_lookup) debug_printf_indent("dnssec fail on %s for %.256s",
 		i>0 ? "AAAA" : "A", host->name);
 	continue;
 	}
       if (host->dnssec_used == DS_YES) /* set in host_find_bydns() */
 	{
-	DEBUG(D_host_lookup) debug_printf("%s A cancel DNSSEC\n", host->name);
+	DEBUG(D_host_lookup) debug_printf_indent("%s A cancel DNSSEC\n", host->name);
 	host->dnssec_used = DS_NO;
 	lookup_dnssec_authenticated = US"no";
 	}
@@ -2364,7 +2364,7 @@ for (; i >= 0; i--)
     dns_address * da = dns_address_from_rr(dnsa, rr);
 
     DEBUG(D_host_lookup)
-      if (!da) debug_printf("no addresses extracted from A6 RR for %s\n",
+      if (!da) debug_printf_indent("no addresses extracted from A6 RR for %s\n",
 	  host->name);
 
     /* This loop runs only once for A and AAAA records, but may run
@@ -2378,7 +2378,7 @@ for (; i >= 0; i--)
 	      host->name, da->address, NULL) == OK)
 	{
 	DEBUG(D_host_lookup)
-	  debug_printf("ignored host %s [%s]\n", host->name, da->address);
+	  debug_printf_indent("ignored host %s [%s]\n", host->name, da->address);
 	continue;
 	}
       #endif
@@ -3198,7 +3198,7 @@ BOOL sec;
 rc = dns_lookup_timerwrap(dnsa, buffer, T_TLSA, &fullname);
 sec = dns_is_secure(dnsa);
 DEBUG(D_transport)
-  debug_printf("TLSA lookup ret %s %sDNSSEC\n", dns_rc_names[rc], sec ? "" : "not ");
+  debug_printf_indent("TLSA lookup ret %s %sDNSSEC\n", dns_rc_names[rc], sec ? "" : "not ");
 
 switch (rc)
   {
@@ -3221,7 +3221,7 @@ switch (rc)
 
 	    if (payload_length > MAX_TLSA_EXPANDED_SIZE)
 	      payload_length = MAX_TLSA_EXPANDED_SIZE;
-	    debug_printf(" %d %d %d %.*H\n",
+	    debug_printf_indent(" %d %d %d %.*H\n",
 			  usage, selector, matching_type, payload_length, p);
 	    }
 	}
diff --git a/src/src/os.c b/src/src/os.c
index 4a054ac50..4b9d2d1d5 100644
--- a/src/src/os.c
+++ b/src/src/os.c
@@ -528,7 +528,7 @@ for (struct ifaddrs * ifa = ifalist; ifa; ifa = ifa->ifa_next)
     last = next;
     }
 
-  DEBUG(D_interface) debug_printf("Actual local interface address is %s (%s)\n",
+  DEBUG(D_interface) debug_printf_indent("Actual local interface address is %s (%s)\n",
     last->address, ifa->ifa_name);
   }
 
@@ -761,7 +761,7 @@ for (char * cp = buf; cp < buf + ifc.V_ifc_len; cp += len)
     last = next;
     }
 
-  DEBUG(D_interface) debug_printf("Actual local interface address is %s (%s)\n",
+  DEBUG(D_interface) debug_printf_indent("Actual local interface address is %s (%s)\n",
     last->address, ifreq.V_ifr_name);
   }
 
diff --git a/src/src/routers/rf_lookup_hostlist.c b/src/src/routers/rf_lookup_hostlist.c
index 0a3a71ba9..59e0761ea 100644
--- a/src/src/routers/rf_lookup_hostlist.c
+++ b/src/src/routers/rf_lookup_hostlist.c
@@ -73,6 +73,7 @@ for (host_item * prev = NULL, * h = addr->host_list, *next_h; h; h = next_h)
 
   DEBUG(D_route|D_host_lookup)
     debug_printf_indent("finding IP address for %s\n", h->name);
+  expand_level++;
 
   /* Handle any port setting that may be on the name; it will be removed
   from the end of the name. */
@@ -164,12 +165,14 @@ for (host_item * prev = NULL, * h = addr->host_list, *next_h; h; h = next_h)
 
   if (rc == HOST_FIND_SECURITY)
     {
+    expand_level--;
     addr->message = string_sprintf("host lookup for %s done insecurely" , h->name);
     addr->basic_errno = ERRNO_DNSDEFER;
     return DEFER;
     }
   if (rc == HOST_FIND_AGAIN)
     {
+    expand_level--;
     if (rblock->pass_on_timeout)
       {
       DEBUG(D_route)
@@ -187,6 +190,7 @@ for (host_item * prev = NULL, * h = addr->host_list, *next_h; h; h = next_h)
 
   if (rc == HOST_FIND_FAILED)
     {
+    expand_level--;
     if (hff_code == hff_ignore)
       {
       if (prev == NULL) addr->host_list = next_h; else prev->next = next_h;
@@ -238,6 +242,7 @@ for (host_item * prev = NULL, * h = addr->host_list, *next_h; h; h = next_h)
         }
       prev->next = NULL;
       setflag(addr, af_local_host_removed);
+      expand_level--;
       break;
       }
     rc = rf_self_action(addr, h, rblock->self_code, rblock->self_rewrite,
@@ -245,6 +250,7 @@ for (host_item * prev = NULL, * h = addr->host_list, *next_h; h; h = next_h)
     if (rc != OK)
       {
       addr->host_list = NULL;   /* Kill the host list for */
+      expand_level--;
       return rc;                /* anything other than "send" */
       }
     self_send = TRUE;
@@ -255,6 +261,7 @@ for (host_item * prev = NULL, * h = addr->host_list, *next_h; h; h = next_h)
 
   prev = h;
   while (prev->next != next_h) prev = prev->next;
+  expand_level--;
   }
 
 return OK;
diff --git a/src/src/routers/rf_self_action.c b/src/src/routers/rf_self_action.c
index 75fbaa400..dba0774f8 100644
--- a/src/src/routers/rf_self_action.c
+++ b/src/src/routers/rf_self_action.c
@@ -95,25 +95,25 @@ switch (code)
 
   case self_reroute:
     DEBUG(D_route)
-      debug_printf("%s: %s: domain changed to %s\n", msg, addr->domain, new);
+      debug_printf_indent("%s: %s: domain changed to %s\n", msg, addr->domain, new);
     rf_change_domain(addr, new, rewrite, addr_new);
     return REROUTED;
 
   case self_send:
     DEBUG(D_route)
-      debug_printf("%s: %s: configured to try delivery anyway\n", msg, addr->domain);
+      debug_printf_indent("%s: %s: configured to try delivery anyway\n", msg, addr->domain);
     return OK;
 
   case self_pass:    /* This is soft failure; pass to next router */
     DEBUG(D_route)
-      debug_printf("%s: %s: passed to next router (self = pass)\n", msg, addr->domain);
+      debug_printf_indent("%s: %s: passed to next router (self = pass)\n", msg, addr->domain);
     addr->message = msg;
     addr->self_hostname = string_copy(host->name);
     return PASS;
 
   case self_fail:
     DEBUG(D_route)
-      debug_printf("%s: %s: address failed (self = fail)\n", msg, addr->domain);
+      debug_printf_indent("%s: %s: address failed (self = fail)\n", msg, addr->domain);
     addr->message = msg;
     setflag(addr, af_pass_message);
     return FAIL;

commit d2353fcd8f602a1f4fdeb31ae006e6b7bc46b349
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Nov 21 14:17:39 2025 +0000

    fix TLS held-open verify to delivery

diff --git a/src/src/daemon.c b/src/src/daemon.c
index a31ef1eb5..056bf6311 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -709,11 +709,8 @@ if (pid == 0)
         signal(SIGINT, SIG_DFL);
 
         if (geteuid() != root_uid && !deliver_drop_privilege)
-          {
-          signal(SIGALRM, SIG_DFL);
 	  delivery_re_exec(CEE_EXEC_PANIC);
           /* Control does not return here. */
-          }
 
         /* No need to re-exec; SIGALRM remains set to the default handler */
 
diff --git a/src/src/deliver.c b/src/src/deliver.c
index 3dc1aef73..c978e67b5 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -9011,7 +9011,6 @@ if (cutthrough.cctx.sock >= 0 && cutthrough.callout_hold_only)
   {
   int channel_fd = cutthrough.cctx.sock;
 
-  smtp_peer_options = cutthrough.peer_options;
   continue_sequence = 0;
 
 #ifndef DISABLE_TLS
@@ -9019,9 +9018,7 @@ if (cutthrough.cctx.sock >= 0 && cutthrough.callout_hold_only)
     {
     int pfd[2], pid;
 
-    smtp_peer_options |= OPTION_TLS;
-    sending_ip_address = cutthrough.snd_ip;
-    sending_port = cutthrough.snd_port;
+    cutthrough.peer_options |= OPTION_TLS;
 
     where = US"socketpair";
     if (socketpair(AF_UNIX, SOCK_STREAM, 0, pfd) != 0)
@@ -9034,7 +9031,7 @@ if (cutthrough.cctx.sock >= 0 && cutthrough.callout_hold_only)
 
     if (pid == 0)	/* child: will fork again to totally disconnect */
       {
-      smtp_proxy_tls(cutthrough.cctx.tls_ctx, big_buffer, big_buffer_size,
+      smtp_proxy_tls(&cutthrough.cctx, big_buffer, big_buffer_size,
 		      pfd, 5*60, cutthrough.host.name);
       /* does not return */
       }
@@ -9046,8 +9043,7 @@ if (cutthrough.cctx.sock >= 0 && cutthrough.callout_hold_only)
     }
 #endif
 
-  transport_do_pass_socket(cutthrough.transport, cutthrough.host.name,
-    cutthrough.host.address, cutthrough.host.port, message_id, channel_fd);
+  transport_do_pass_socket(message_id, channel_fd);
   }
 else
   {
diff --git a/src/src/exim.c b/src/src/exim.c
index c23eb868c..a50303049 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -405,13 +405,13 @@ Returns:   nothing
 */
 
 static void
-milliwait(struct itimerval *itval)
+milliwait(struct itimerval * itval)
 {
 sigset_t sigmask;
 sigset_t old_sigmask;
 int save_errno = errno;
 
-if (itval->it_value.tv_usec < 50 && itval->it_value.tv_sec == 0)
+if (itval->it_value.tv_usec < 50 && itval->it_value.tv_sec <= 0)
   return;
 (void)sigemptyset(&sigmask);                           /* Empty mask */
 (void)sigaddset(&sigmask, SIGALRM);                    /* Add SIGALRM */
@@ -3000,7 +3000,16 @@ on the second character (the one after '-'), to save some effort. */
 
       if (!continue_proxy_cipher)
 	if (getsockname(0, (struct sockaddr *)(&tmp_sock), &size) == 0)
-	  sending_ip_address = host_ntoa(-1, &tmp_sock, NULL, &sending_port);
+	  switch (tmp_sock.v0.sa_family)
+	    {
+	    case AF_INET:
+	    case AF_INET6:
+	      sending_ip_address =
+		    host_ntoa(-1, &tmp_sock, NULL, &sending_port);
+	      break;
+	    default:
+	      exim_fail("non-INET socket on stdin for -MC option");
+	    }
 	else
 	  exim_fail("getsockname() failed after -MC option: %s",
 	    strerror(errno));
diff --git a/src/src/functions.h b/src/src/functions.h
index 73371cc7b..937259334 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -551,7 +551,7 @@ extern void    smtp_log_no_mail(void);
 extern void    smtp_message_code(uschar **, int *, uschar **, uschar **, BOOL);
 extern void    smtp_notquit_exit(const uschar *, uschar *, const uschar *, ...);
 extern void    smtp_port_for_connect(host_item *, int);
-extern void    smtp_proxy_tls(void *, uschar *, size_t, int *, int, const uschar *) NORETURN;
+extern void    smtp_proxy_tls(client_conn_ctx *, uschar *, size_t, int *, int, const uschar *) NORETURN;
 extern BOOL    smtp_read_response(void *, uschar *, int, int, int);
 rmark	       smtp_reset(rmark);
 extern void    smtp_respond(uschar *, int, BOOL, uschar *);
@@ -662,8 +662,7 @@ extern uschar *tod_stamp(int);
 extern BOOL    transport_check_waiting(const uschar *, const uschar *, int, uschar *,
                  oicf, void*);
 extern uschar *transport_current_name(void);
-extern void    transport_do_pass_socket(const uschar *, const uschar *,
-		 const uschar *, int, uschar *, int);
+extern void    transport_do_pass_socket(uschar *, int);
 extern void    transport_init(void);
 extern const uschar *transport_rcpt_address(address_item *, BOOL);
 extern BOOL    transport_set_up_command(const uschar ***, const uschar *,
diff --git a/src/src/globals.c b/src/src/globals.c
index 36f364996..38fdbc7a3 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -665,14 +665,8 @@ open_db *continue_wait_db      = NULL;
 #endif
 
 uschar *csa_status             = NULL;
-cut_t   cutthrough = {
-  .callout_hold_only =	FALSE,				/* verify-only: normal delivery */
-  .delivery =		FALSE,				/* when to attempt */
-  .tpt_sender =		FALSE,				/* use tpt's sender */
-  .defer_pass =		FALSE,				/* on defer: spool locally */
-  .is_tls =		FALSE,				/* not a TLS conn yet */
+cut_t   cutthrough = {	/* All remaining items 0/FALSE/NULL */
   .cctx =		{.sock = -1},			/* open connection */
-  .nrcpt =		0,				/* number of addresses */
 };
 
 int	daemon_notifier_fd     = -1;
diff --git a/src/src/globals.h b/src/src/globals.h
index 9f4d04a2d..1c133f5ea 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -474,11 +474,14 @@ extern open_db *continue_wait_db;      /* Hintsdb for wait-transport */
 extern uschar *csa_status;             /* Client SMTP Authorization result */
 
 typedef struct {
-  unsigned     callout_hold_only:1;    /* Conn is only for verify callout */
-  unsigned     delivery:1;             /* When to attempt */
-  unsigned     tpt_sender:1;           /* Use tpt-defined sender */
-  unsigned     defer_pass:1;           /* Pass 4xx to caller rather than spooling */
-  unsigned     is_tls:1;	       /* Conn has TLS active */
+  BOOL		callout_hold_only:1;    /* Conn is only for verify callout */
+  BOOL		delivery:1;             /* When to attempt */
+  BOOL		tpt_sender:1;           /* Use tpt-defined sender */
+  BOOL		defer_pass:1;           /* Pass 4xx to caller rather than spooling */
+  BOOL		is_tls:1;	       /* Conn has TLS active */
+  BOOL		is_dane:1;
+  uschar *	sni;
+  uschar *	cipher;
   client_conn_ctx cctx;                /* Open connection */
   int          nrcpt;                  /* Count of addresses */
   uschar *     transport;	       /* Name of transport */
diff --git a/src/src/transport.c b/src/src/transport.c
index a59a5ae3d..5d3b8a732 100644
--- a/src/src/transport.c
+++ b/src/src/transport.c
@@ -2025,16 +2025,17 @@ retfalse:
 *    Deliver waiting message down same socket    *
 *************************************************/
 
-/* Just the regain-root-privilege exec portion */
+/* Just the regain-root-privilege exec portion.
+The sole caller is delivery_re_exec(). */
+
 void
-transport_do_pass_socket(const uschar * transport_name, const uschar * hostname,
-  const uschar * hostaddress, int hostport, uschar * id, int socket_fd)
+transport_do_pass_socket(uschar * id, int socket_fd)
 {
 int i = 14;
 const uschar **argv;
 
 #ifndef DISABLE_TLS
-if (smtp_peer_options & OPTION_TLS) i += 6;
+if (cutthrough.peer_options & OPTION_TLS) i += 6;
 #endif
 #ifndef DISABLE_ESMTP_LIMITS
 if (continue_limit_mail || continue_limit_rcpt || continue_limit_rcptdom)
@@ -2051,27 +2052,27 @@ but we have a number of extras that may be added. */
 argv = CUSS child_exec_exim(CEE_RETURN_ARGV, TRUE, &i, FALSE, 0);
 
 if (f.smtp_authenticated)			argv[i++] = US"-MCA";
-if (smtp_peer_options & OPTION_CHUNKING)	argv[i++] = US"-MCK";
-if (smtp_peer_options & OPTION_DSN)		argv[i++] = US"-MCD";
-if (smtp_peer_options & OPTION_PIPE)		argv[i++] = US"-MCP";
-if (smtp_peer_options & OPTION_SIZE)		argv[i++] = US"-MCS";
+if (cutthrough.peer_options & OPTION_CHUNKING)	argv[i++] = US"-MCK";
+if (cutthrough.peer_options & OPTION_DSN)	argv[i++] = US"-MCD";
+if (cutthrough.peer_options & OPTION_PIPE)	argv[i++] = US"-MCP";
+if (cutthrough.peer_options & OPTION_SIZE)	argv[i++] = US"-MCS";
 #ifndef DISABLE_TLS
-if (smtp_peer_options & OPTION_TLS)
-  if (tls_out.active.sock >= 0 || continue_proxy_cipher)
+if (cutthrough.peer_options & OPTION_TLS)
+  if (cutthrough.is_tls)
     {
     argv[i++] = US"-MCt";
-    argv[i++] = sending_ip_address;
-    argv[i++] = string_sprintf("%d", sending_port);
-    argv[i++] = tls_out.active.sock >= 0 ? tls_out.cipher : continue_proxy_cipher;
+    argv[i++] = cutthrough.snd_ip;
+    argv[i++] = string_sprintf("%d", cutthrough.snd_port);
+    argv[i++] = cutthrough.cipher;
 
-    if (tls_out.sni)
+    if (cutthrough.sni)
       {
       argv[i++] =
 #ifdef SUPPORT_DANE
-        tls_out.dane_verified ? US"-MCr" :
+        cutthrough.is_dane ? US"-MCr" :
 #endif
         US"-MCs";
-      argv[i++] = tls_out.sni;
+      argv[i++] = cutthrough.sni;
       }
     }
   else
@@ -2107,11 +2108,11 @@ if (proxy_session)
 #endif
 
 argv[i++] = US"-MC";
-argv[i++] = US transport_name;
-argv[i++] = US hostname;
-argv[i++] = US hostaddress;
-argv[i++] = string_sprintf("%d", hostport);
-argv[i++] = string_sprintf("%d", continue_sequence + 1);
+argv[i++] = US cutthrough.transport;
+argv[i++] = US cutthrough.host.name;
+argv[i++] = US cutthrough.host.address;
+argv[i++] = string_sprintf("%d", cutthrough.host.port);
+argv[i++] = string_sprintf("%d", continue_sequence + 1); /*XXX always 0+1 */
 argv[i++] = id;
 argv[i++] = NULL;
 
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index 36b6370fd..7160f0616 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -3881,7 +3881,7 @@ Do blocking full-size writes, and reads under a timeout.  Once both input
 channels are closed, exit the process.
 
 Arguments:
-  ct_ctx	tls context
+  ctx		client context
   buf		space to use for buffering
   bufsiz	size of buffer
   pfd		pipe filedescriptor array; [0] is comms to proxied process
@@ -3892,11 +3892,12 @@ Does not return.
 */
 
 void
-smtp_proxy_tls(void * ct_ctx, uschar * buf, size_t bsize, int * pfd,
+smtp_proxy_tls(client_conn_ctx * ctx, uschar * buf, size_t bsize, int * pfd,
   int timeout, const uschar * host)
 {
-struct pollfd p[2] = {{.fd = tls_out.active.sock, .events = POLLIN},
+struct pollfd p[2] = {{.fd = ctx->sock, .events = POLLIN},
 		      {.fd = pfd[0], .events = POLLIN}};
+void * tls_ctx = ctx->tls_ctx;
 int rc, i;
 BOOL send_tls_shutdown = TRUE;
 
@@ -3930,7 +3931,7 @@ do
 
     if (p[0].revents & POLLERR || p[1].revents & POLLERR)
       {
-      DEBUG(D_transport) debug_printf("select: exceptional cond on %s fd\n",
+      DEBUG(D_transport) debug_printf("poll: err cond on %s fd\n",
 	p[0].revents & POLLERR ? "tls" : "proxy");
       if (!(p[0].revents & POLLIN || p[1].events & POLLIN))
 	goto done;
@@ -3941,7 +3942,7 @@ do
 
   /* handle inbound data */
   if (p[0].revents & POLLIN)
-    if ((rc = tls_read(ct_ctx, buf, bsize)) <= 0)	/* Expect -1 for EOF; */
+    if ((rc = tls_read(tls_ctx, buf, bsize)) <= 0)	/* Expect -1 for EOF; */
       {				    /* that reaps the TLS Close Notify record */
       p[0].fd = -1;
       shutdown(pfd[0], SHUT_WR);
@@ -3965,19 +3966,19 @@ do
 # ifdef EXIM_TCP_CORK  /* Use _CORK to get TLS Close Notify in FIN segment */
       (void) setsockopt(tls_out.active.sock, IPPROTO_TCP, EXIM_TCP_CORK, US &on, sizeof(on));
 # endif
-      tls_shutdown_wr(ct_ctx);
+      tls_shutdown_wr(tls_ctx);
       send_tls_shutdown = FALSE;
       shutdown(tls_out.active.sock, SHUT_WR);
       }
     else
       for (int nbytes = 0; rc - nbytes > 0; nbytes += i)
-	if ((i = tls_write(ct_ctx, buf + nbytes, rc - nbytes, FALSE)) < 0)
+	if ((i = tls_write(tls_ctx, buf + nbytes, rc - nbytes, FALSE)) < 0)
 	  goto done;
   }
 while (p[0].fd >= 0 || p[1].fd >= 0);
 
 done:
-  if (send_tls_shutdown) tls_close(ct_ctx, TLS_SHUTDOWN_NOWAIT);
+  if (send_tls_shutdown) tls_close(tls_ctx, TLS_SHUTDOWN_NOWAIT);
   testharness_pause_ms(100);	/* let logging complete */
   exim_exit(EXIT_SUCCESS);
 }
@@ -5108,7 +5109,7 @@ if (sx->completed_addr && sx->ok && sx->send_quit)
 	      if (pid == 0)	/* child; fork again to disconnect totally */
 		{
 		/* does not return */
-		smtp_proxy_tls(sx->cctx.tls_ctx, sx->buffer, sizeof(sx->buffer),
+		smtp_proxy_tls(&sx->cctx, sx->buffer, sizeof(sx->buffer),
 			      pfd, ob->command_timeout, host->name);
 		}
 
diff --git a/src/src/verify.c b/src/src/verify.c
index 1fee97150..84134e49c 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -1157,7 +1157,12 @@ no_conn:
 	? "cutthrough delivery" : "potential further verifies and delivery");
 
       cutthrough.callout_hold_only = !cutthrough.delivery;
-      cutthrough.is_tls =	tls_out.active.sock >= 0;
+      if ((cutthrough.is_tls =	tls_out.active.sock >= 0))
+	{
+	cutthrough.is_dane =	tls_out.sni && tls_out.dane_verified;
+	cutthrough.sni =	tls_out.sni;
+	cutthrough.cipher =	tls_out.cipher;
+	}
       /* We assume no buffer in use in the outblock */
       cutthrough.cctx =		sx->cctx;
       cutthrough.nrcpt =	1;

commit 1fd121860e713f4d2783be54939b92a297a82dd4
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Nov 21 14:37:12 2025 +0000

    fix non-DANE build
    
    Broken-by: d2353fcd8f60

diff --git a/src/src/verify.c b/src/src/verify.c
index 84134e49c..18c53c1e8 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -1159,7 +1159,9 @@ no_conn:
       cutthrough.callout_hold_only = !cutthrough.delivery;
       if ((cutthrough.is_tls =	tls_out.active.sock >= 0))
 	{
+#ifdef SUPPORT_DANE
 	cutthrough.is_dane =	tls_out.sni && tls_out.dane_verified;
+#endif
 	cutthrough.sni =	tls_out.sni;
 	cutthrough.cipher =	tls_out.cipher;
 	}

commit 9ecbb33982299a8d83a72d4dcd951949e5cfb81e
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 23 10:35:12 2025 +0000

    Local-scan: bump API minor version
    
    Things like constification and ifdeffed-feature removal
    have, strictly, affected the API definition

diff --git a/src/src/exim.c b/src/src/exim.c
index a50303049..32ca1e1d2 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1125,6 +1125,9 @@ g = string_cat(g, US"Support for:");
 #ifdef WITH_CONTENT_SCAN
   g = string_cat(g, US" Content_Scanning");
 #endif
+#ifdef HAVE_LOCAL_SCAN
+  g = string_cat(g, US" Local_Scan");
+#endif
 #ifndef DISABLE_EXIM_FILTER
   g = string_cat(g, US" Exim_filter");
 #endif
@@ -1282,6 +1285,7 @@ DEBUG(D_any)
 #else
   g = string_cat(g, US"Compiler: <unknown>\n");
 #endif
+/*XXX Sun Studio compiler? */
 
 #if defined(__GLIBC__) && !defined(__UCLIBC__)
   g = string_fmt_append(g, "Library version: Glibc: Compile: %d.%d\n",
@@ -1344,6 +1348,10 @@ Currently they are output in misc_mod_add() */
   g = string_fmt_append(g, "TRUSTED_CONFIG_LIST: %q\n", TRUSTED_CONFIG_LIST);
 #else
   g = string_cat(g, US"TRUSTED_CONFIG_LIST unset\n");
+#endif
+#ifdef HAVE_LOCAL_SCAN
+  g = string_cat(g, US"Local-Scan API: "
+		    mac_expanded_string(LOCAL_SCAN_ABI_VERSION) "\n");
 #endif
   }
 
diff --git a/src/src/local_scan.h b/src/src/local_scan.h
index 355487b0d..915b12007 100644
--- a/src/src/local_scan.h
+++ b/src/src/local_scan.h
@@ -44,7 +44,7 @@ each time a new feature is added (in a way that doesn't break backward
 compatibility). */
 
 #define LOCAL_SCAN_ABI_VERSION_MAJOR 6
-#define LOCAL_SCAN_ABI_VERSION_MINOR 0
+#define LOCAL_SCAN_ABI_VERSION_MINOR 1
 #define LOCAL_SCAN_ABI_VERSION \
   LOCAL_SCAN_ABI_VERSION_MAJOR.LOCAL_SCAN_ABI_VERSION_MINOR
 

commit 66a452cb6128ddc0805eac37230afd77d6fb168c
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 23 14:18:54 2025 +0000

    Local-scan: fix API
    
    Broken-by: 7b4e2a15a529

diff --git a/src/src/functions.h b/src/src/functions.h
index 937259334..76bc24c1b 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -238,19 +238,6 @@ extern int     exp_bool(address_item *,
 extern BOOL    expand_check_condition(const uschar *, const uschar *, const uschar *);
 extern uschar *expand_file_big_buffer(const uschar *);
 
-extern const uschar *expand_string_2(const uschar *, BOOL *);
-
-static inline uschar * expand_nc_string(uschar * s)
-{ return US expand_string_2(s, NULL); }
-static inline const uschar * expand_c_string(const uschar * s)
-{ return expand_string_2(s, NULL); }
-
-/* A macro that picks which function to use depending on the type of the arg */
-#define expand_string(X) _Generic((X),     \
-	      uschar *:		expand_nc_string, \
-	      const uschar *:	expand_c_string   \
-	      )(X)
-
 extern BOOL   expand_string_nonempty(const uschar *);
 extern uschar *expand_getkeyed(const uschar *, const uschar *);
 
diff --git a/src/src/local_scan.h b/src/src/local_scan.h
index 915b12007..5aff615e7 100644
--- a/src/src/local_scan.h
+++ b/src/src/local_scan.h
@@ -193,7 +193,19 @@ extern BOOL    smtp_input;             /* TRUE if input is via SMTP */
 
 extern int     child_close(pid_t, int);
 extern void    debug_printf(const char *, ...) PRINTF_FUNCTION(1,2);
-extern uschar *expand_string(uschar *);
+
+extern const uschar * expand_string_2(const uschar *, BOOL *);
+static inline uschar * expand_nc_string(uschar * s)
+{ return US expand_string_2(s, NULL); }
+static inline const uschar * expand_c_string(const uschar * s)
+{ return expand_string_2(s, NULL); }
+
+/* A macro that picks which function to use depending on the type of the arg */
+#define expand_string(X) _Generic((X),     \
+	      uschar *:		expand_nc_string, \
+	      const uschar *:	expand_c_string   \
+	      )(X)
+
 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 *);
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index e6c9dbacc..674bdd785 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -1798,6 +1798,9 @@ deliver_host = deliver_host_address = NULL;	/* Can be set by ACL */
 #ifdef SUPPORT_SRS
  srs_recipient = NULL;
 #endif
+#ifdef HAVE_LOCAL_SCAN
+local_scan_data = NULL;
+#endif
 #ifdef WITH_CONTENT_SCAN
  regex_vars_clear();
  malware_name = NULL;

commit 5921ece54a48e3d773293b280722fb0e34a66539
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Nov 24 12:27:09 2025 +0000

    Testsuite: add testcases for dbmjz/dbmnz under sqlite

diff --git a/src/src/dbfn.c b/src/src/dbfn.c
index 30015fe73..7c2dbcce3 100644
--- a/src/src/dbfn.c
+++ b/src/src/dbfn.c
@@ -403,10 +403,7 @@ unsigned dlen;
 
 memcpy(key_copy, key, klen);
 
-/*XXX the %.*s will terminate early on a key with embedded NUL (legit for
-lookup dbmjz).  We do not have a convenient function; maybe extend
-string_printing2() ? */
-DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: key=%.*s\n", klen, key);
+DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: key=%.*W\n", klen, key);
 
 exim_datum_init(&key_datum);         /* Some DBM libraries require the datum */
 exim_datum_init(&result_datum);      /* to be cleared before use. */

commit 89c878362b94d35fa662e02a8834238b699762d7
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Nov 29 12:51:38 2025 +0000

    extend string-formatting facility

diff --git a/src/src/exim_dbmbuild.c b/src/src/exim_dbmbuild.c
index 99ea5c826..e3ef848d3 100644
--- a/src/src/exim_dbmbuild.c
+++ b/src/src/exim_dbmbuild.c
@@ -44,7 +44,7 @@ uschar *
 readconf_printtime(int t)
 { return NULL; }
 const uschar * expand_string_2(const uschar * string, BOOL * textonly_p)
-{return NULL; }
+{ return NULL; }
 void *
 store_get_3(int size, const void * proto_mem, const char *filename, int linenumber)
 { return NULL; }
@@ -55,9 +55,6 @@ 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/exim_dbutil.c b/src/src/exim_dbutil.c
index 5065b6e9b..30d446bad 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -62,9 +62,6 @@ uschar *
 readconf_printtime(int t)
 { return NULL; }
 const uschar * expand_string_2(const uschar * string, BOOL * textonly_p)
-{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,
diff --git a/src/src/string.c b/src/src/string.c
index 474f4a1bb..472008382 100644
--- a/src/src/string.c
+++ b/src/src/string.c
@@ -15,6 +15,11 @@ utilities and tests, and are cut out by the COMPILE_UTILITY macro. */
 #include <assert.h>
 
 
+#ifdef COMPILE_UTILITY
+BOOL print_topbitchars = FALSE;	/* referenced by string_printing3() */
+#endif
+
+
 #ifndef COMPILE_UTILITY
 /*************************************************
 *            Test for IP address                 *
@@ -310,7 +315,6 @@ return ch;
 
 
 
-#ifndef COMPILE_UTILITY
 /*************************************************
 *          Ensure string is printable            *
 *************************************************/
@@ -325,19 +329,20 @@ Arguments:
   flags		Bit 0: convert tabs.
 		Bit 1: convert spaces.
 		Bit 2: convert doublequotes.
+  len		if >= 0, max size of string
+		otherwise, NUL-terminated
 
 Returns:        string with non-printers encoded as printing sequences
 */
 
 const uschar *
-string_printing2(const uschar * s, int flags)
+string_printing3(const uschar * s, int flags, int len)
 {
-int nonprintcount = 0;
-int length = 0;
-const uschar *t = s;
-uschar *ss, *tt;
+int nonprintcount = 0, olen = 0;
+const uschar * t = s;
+uschar * ss, * tt;
 
-while (*t)
+for (int n = len; n != 0 && *t; n--)
   {
   int c = *t++;
   if (  !mac_isprint(c)
@@ -345,7 +350,7 @@ while (*t)
      || flags & SP_SPACE && c == ' '
      || flags & SP_DQUOTES && c == '"'
      ) nonprintcount++;
-  length++;
+  olen++;
   }
 
 if (nonprintcount == 0) return s;
@@ -353,13 +358,15 @@ if (nonprintcount == 0) return s;
 /* Get a new block of store guaranteed big enough to hold the
 expanded string. */
 
-tt = ss = store_get(length + nonprintcount * 3 + 1, s);
+tt = ss = store_get(olen + nonprintcount * 3 + 1, s);
 
 /* Copy everything, escaping non printers. */
 
 for (t = s; *t; )
   {
   int c = *t;
+  /*XXX does \ go through unchanged here?  Since we use it for escaping,
+  surely it should be doubled? */
   if (  mac_isprint(c)
      && (!(flags & SP_TAB) || c != '\t')
      && (!(flags & SP_SPACE) || c != ' ')
@@ -386,7 +393,12 @@ for (t = s; *t; )
 *tt = 0;
 return ss;
 }
-#endif  /* COMPILE_UTILITY */
+
+const uschar *
+string_printing2(const uschar * s, int flags)
+{
+return string_printing3(s, flags, -1);
+}
 
 /*************************************************
 *        Undo printing escapes in string         *
@@ -1215,6 +1227,7 @@ if (!store_extend(g->s, oldsize, g->size))
   g->s = store_newblock(g->s, g->size, p);
 }
 
+#endif	/*!COMPILE_UTILITY*/
 
 
 /*************************************************
@@ -1290,6 +1303,7 @@ return g;
 
 
 
+#ifndef COMPILE_UTILITY
 /*************************************************
 *        Append strings to another string        *
 *************************************************/
@@ -1410,15 +1424,15 @@ enum ltypes { L_NORMAL=1, L_SHORT=2, L_LONG=3, L_LONGLONG=4, L_LONGDOUBLE=5, L_S
 int width, precision, initial_off, lim, need;
 const char * fp = format;	/* Deliberately not unsigned */
 
-string_datestamp_offset = -1;	/* Datestamp not inserted */
-string_datestamp_length = 0;	/* Datestamp not inserted */
-string_datestamp_type = 0;	/* Datestamp not inserted */
-
 #ifdef COMPILE_UTILITY
 assert(!(flags & SVFMT_EXTEND));
 assert(g);
 #else
 
+string_datestamp_offset = -1;	/* Datestamp not inserted */
+string_datestamp_length = 0;	/* Datestamp not inserted */
+string_datestamp_type = 0;	/* Datestamp not inserted */
+
 /* Ensure we have a string, to save on checking later */
 if (!g) g = string_get(16);
 
@@ -1532,7 +1546,7 @@ while (*fp)
 	gp = CS g->s + g->ptr;
 	}
       strncpy(newformat, item_start, fp - item_start);
-      newformat[fp - item_start] = 0;
+      newformat[fp - item_start] = '\0';
 
       /* Short int is promoted to int when passing through ..., so we must use
       int for va_arg(). */
@@ -1566,7 +1580,7 @@ while (*fp)
       if ((ptr = va_arg(ap, void *)))
 	{
 	strncpy(newformat, item_start, fp - item_start);
-	newformat[fp - item_start] = 0;
+	newformat[fp - item_start] = '\0';
 	g->ptr += sprintf(gp, newformat, ptr);
 	}
       else
@@ -1595,7 +1609,7 @@ while (*fp)
 	gp = CS g->s + g->ptr;
 	}
       strncpy(newformat, item_start, fp - item_start);
-      newformat[fp-item_start] = 0;
+      newformat[fp-item_start] = '\0';
       if (length == L_LONGDOUBLE)
 	g->ptr += sprintf(gp, newformat, va_arg(ap, long double));
       else
@@ -1624,6 +1638,7 @@ while (*fp)
       g->s[g->ptr++] = (uschar) va_arg(ap, int);
       break;
 
+#ifndef COMPILE_UTILITY
     case 'D':                   /* Insert daily datestamp for log file names */
       s = CS tod_stamp(tod_log_datestamp_daily);
       string_datestamp_offset = g->ptr;		/* Passed back via global */
@@ -1639,6 +1654,7 @@ while (*fp)
       string_datestamp_type = tod_log_datestamp_monthly;
       slen = string_datestamp_length;
       goto INSERT_STRING;
+#endif
 
     case 'Y':			/* gstring pointer */
       {
@@ -1782,12 +1798,14 @@ while (*fp)
 	{ s = "<NULL>"; precision = slen = 6; }
       }
       goto INSERT_GSTRING;
+#endif
 
     case 'q':			/* string, to be wrapped in "" and with tab & " escaped */
       if ((s = va_arg(ap, char *)))
 	{
 	gstring * zg = string_catn(NULL, US"\"", 1);
-	zg = string_cat(zg, string_printing2(US s, SP_TAB | SP_DQUOTES));
+	zg = string_cat(zg,
+			string_printing3(US s, SP_TAB | SP_DQUOTES, precision));
 	zg = string_catn(zg, US"\"", 1);
 	s = CS zg->s; precision = slen = gstring_length(zg);
 	}
@@ -1795,7 +1813,6 @@ while (*fp)
 	{ s = "<NULL>"; precision = slen = 6; }
       goto INSERT_GSTRING;
 
-#endif
     case 's':
     case 'S':                   /* Forces *lower* case */
     case 'T':                   /* Forces *upper* case */
@@ -1876,7 +1893,7 @@ while (*fp)
 
     default:
       strncpy(newformat, item_start, fp - item_start);
-      newformat[fp-item_start] = 0;
+      newformat[fp-item_start] = '\0';
       log_write_die(0, LOG_MAIN, "string_format: unsupported type "
 	"in %q in %q", newformat, format);
       break;

commit e4e66eda1683cfd3a6658b39c21be91a7ca99df0
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Nov 29 14:01:04 2025 +0000

    local_scan: bump API version, for smtp_fflush()
    
    Broken-by: 9682a4923d1d

diff --git a/src/src/local_scan.h b/src/src/local_scan.h
index 5aff615e7..c9d179375 100644
--- a/src/src/local_scan.h
+++ b/src/src/local_scan.h
@@ -43,8 +43,8 @@ ABI is changed in a non backward compatible way. The minor number is increased
 each time a new feature is added (in a way that doesn't break backward
 compatibility). */
 
-#define LOCAL_SCAN_ABI_VERSION_MAJOR 6
-#define LOCAL_SCAN_ABI_VERSION_MINOR 1
+#define LOCAL_SCAN_ABI_VERSION_MAJOR 7
+#define LOCAL_SCAN_ABI_VERSION_MINOR 0
 #define LOCAL_SCAN_ABI_VERSION \
   LOCAL_SCAN_ABI_VERSION_MAJOR.LOCAL_SCAN_ABI_VERSION_MINOR
 

commit 34b791b30d336dd78db0ded8c9b9790fdfa8f3c9
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 16 15:23:56 2025 +0000

    fix build with perl-spf

diff --git a/src/scripts/lookups-Makefile b/src/scripts/lookups-Makefile
index 6555e820c..7aa186e2e 100755
--- a/src/scripts/lookups-Makefile
+++ b/src/scripts/lookups-Makefile
@@ -158,7 +158,7 @@ sed -n "1,/$tag_marker/p" < "$input"
 
 for name_mod in \
     CDB DBM:dbmdb DNSDB DSEARCH JSON LMDB LDAP LSEARCH MYSQL NIS NISPLUS \
-    NMH ORACLE PASSWD PGSQL PSL REDIS SQLITE SPF TESTDB WHOSON
+    NMH ORACLE PASSWD PGSQL PSL REDIS SQLITE SPF SPF_PERL:spf TESTDB WHOSON
 do
   emit_module_rule $name_mod
 done
diff --git a/src/src/miscmods/spf_perl.c b/src/src/miscmods/spf_perl.c
index 33871c9aa..3008facc8 100644
--- a/src/src/miscmods/spf_perl.c
+++ b/src/src/miscmods/spf_perl.c
@@ -127,7 +127,7 @@ spf_lib_version_report(gstring * g)
 {
 /*XXX Does Mail::SPF have a version? MetaCPAN says yes, but does not
 document a method that returns it. */
-return string_fmt_append(g, "Library_version: SPF: perl Mail::SPF\n");
+return string_cat(g, US"Library_version: SPF: perl Mail::SPF\n");
 }
 
 

commit 21811ff3fd95a5325c665cb28b1d65e612a179dc
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Nov 18 18:03:39 2025 +0000

    Testsuite: fix for perl-spf dynamic build

diff --git a/src/src/EDITME b/src/src/EDITME
index a2dddd780..1c9d834dd 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -412,6 +412,7 @@ TRANSPORT_SMTP=yes
 # EXTRALIBS so that dlopen() is available to Exim. You need to define
 # LOOKUP_MODULE_DIR above so the exim binary actually loads dynamic lookup
 # modules.
+#
 # Also, instead of adding all the libraries/includes to LOOKUP_INCLUDE and
 # LOOKUP_LIBS, add them to the respective LOOKUP_*_INCLUDE and LOOKUP_*_LIBS
 # (where * is the name as given here in this list). That ensures that only
@@ -1170,6 +1171,7 @@ ZCAT_COMMAND=/usr/bin/zcat
 # SUPPORT_SPF=yes
 # CFLAGS  += -I/usr/local/include
 # LDFLAGS += -lspf2
+# SUPPORT_SPF_LIBS= -lspf2
 
 
 #------------------------------------------------------------------------------
diff --git a/src/src/drtables.c b/src/src/drtables.c
index 9e25472d3..87ff8da61 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -185,9 +185,13 @@ if (info->magic != LOOKUP_MODULE_INFO_MAGIC)
   }
 
 addlookupmodule(info);
-EARLY_DEBUG(D_lookup, "Loaded %q (%d lookup type%s)\n",
+if (debug_startup)
+  { EARLY_DEBUG(D_lookup, "Loaded %q (%d lookup type%s)\n",
 				    name, info->lookupcount,
-				    info->lookupcount > 1 ? "s" : "");
+				    info->lookupcount > 1 ? "s" : ""); }
+else
+  DEBUG(D_lookup) debug_printf_indent("Loaded module %q\n", name);
+
 return TRUE;
 }
 
@@ -341,7 +345,8 @@ if ((mi = misc_mod_findonly(name))) return mi;
 #ifdef LOOKUP_MODULE_DIR
 return misc_mod_load(name, errstr);
 #else
-*errstr = string_sprintf("module '%s' not found", name);
+*errstr = string_sprintf("module %q not built-in, and"
+	"no setting for LOOKUP_MODULE_DIR", name);
 return NULL;
 #endif	/*LOOKUP_MODULE_DIR*/
 }
@@ -470,7 +475,7 @@ else
   closedir(dd);
   }
 
-EARLY_DEBUG(D_lookup, "Loaded %d lookup modules\n", countmodules);
+EARLY_DEBUG(D_lookup, "Loaded %d dynamic lookup modules\n", countmodules);
 #endif
 }
 
diff --git a/src/src/exim_dbmbuild.c b/src/src/exim_dbmbuild.c
index e3ef848d3..8cb06fefe 100644
--- a/src/src/exim_dbmbuild.c
+++ b/src/src/exim_dbmbuild.c
@@ -55,6 +55,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/lookups/json.c b/src/src/lookups/json.c
index 3e5f9609e..92da9b9cb 100644
--- a/src/src/lookups/json.c
+++ b/src/src/lookups/json.c
@@ -157,8 +157,6 @@ json_close(void *handle)
 
 /* See local README for interface description. */
 
-#include "../version.h"
-
 gstring *
 json_version_report(gstring * g)
 {
diff --git a/src/src/lookups/ldap.c b/src/src/lookups/ldap.c
index b73413a39..0a00633ab 100644
--- a/src/src/lookups/ldap.c
+++ b/src/src/lookups/ldap.c
@@ -1558,8 +1558,6 @@ return quoted;
 
 /* See local README for interface description. */
 
-#include "../version.h"
-
 gstring *
 ldap_version_report(gstring * g)
 {
diff --git a/src/src/lookups/lmdb.c b/src/src/lookups/lmdb.c
index c90632cee..dc8bec3ba 100644
--- a/src/src/lookups/lmdb.c
+++ b/src/src/lookups/lmdb.c
@@ -128,8 +128,6 @@ mdb_env_close(db_env);
 *         Version reporting entry point          *
 *************************************************/
 
-#include "../version.h"
-
 gstring *
 lmdb_version_report(gstring * g)
 {
diff --git a/src/src/lookups/mysql.c b/src/src/lookups/mysql.c
index 76954ca43..f28e9154f 100644
--- a/src/src/lookups/mysql.c
+++ b/src/src/lookups/mysql.c
@@ -467,8 +467,6 @@ return quoted;
 
 /* See local README for interface description. */
 
-#include "../version.h"
-
 gstring *
 mysql_version_report(gstring * g)
 {
diff --git a/src/src/lookups/pgsql.c b/src/src/lookups/pgsql.c
index 8ddf0e058..7071cb4ab 100644
--- a/src/src/lookups/pgsql.c
+++ b/src/src/lookups/pgsql.c
@@ -472,8 +472,6 @@ return quoted;
 
 /* See local README for interface description. */
 
-#include "../version.h"
-
 gstring *
 pgsql_version_report(gstring * g)
 {
diff --git a/src/src/lookups/redis.c b/src/src/lookups/redis.c
index a721d0605..61c264a73 100644
--- a/src/src/lookups/redis.c
+++ b/src/src/lookups/redis.c
@@ -428,7 +428,6 @@ return quoted;
 /*************************************************
 *         Version reporting entry point          *
 *************************************************/
-#include "../version.h"
 
 gstring *
 redis_version_report(gstring * g)
diff --git a/src/src/lookups/spf.c b/src/src/lookups/spf.c
index bfce04fa7..a8404d9bf 100644
--- a/src/src/lookups/spf.c
+++ b/src/src/lookups/spf.c
@@ -81,39 +81,33 @@ return FAIL;
 
 /* See local README for interface description. */
 
-#include "../version.h"
-
 gstring *
 spf_version_report(gstring * g)
 {
-#if EXIM_HAVE_SPF == 2
-return g;
-#else
-int maj, min, patch;
-
-SPF_get_lib_version(&maj, &min, &patch);
-return string_fmt_append(g, "Library version: SPF: Runtime: %d.%d.%d\n",
-      maj, min, patch);
-#endif
+uschar * dummy_errmsg;
+misc_module_info * mi = misc_mod_find(US"spf", &dummy_errmsg);
+return mi && mi->lib_vers_report ? mi->lib_vers_report(g) : g;
 }
 
 
+
 static lookup_info spf_lookup_info = {
-  .name = US"spf",			/* lookup name */
-  .type = 0,				/* not absfile, not query style */
-  .open = spf_open,			/* open function */
-  .check = NULL,			/* no check function */
-  .find = spf_find,			/* find function */
-  .close = spf_close,			/* close function */
-  .tidy = NULL,				/* no tidy function */
-  .quote = NULL,			/* no quoting function */
-  .version_report = spf_version_report             /* version reporting */
+  .name =	US"spf",		/* lookup name */
+  .type =	0,			/* not absfile, not query style */
+  .open =	spf_open,		/* open function */
+  .check =	NULL,			/* no check function */
+  .find =	spf_find,		/* find function */
+  .close =	spf_close,		/* close function */
+  .tidy =	NULL,			/* no tidy function */
+  .quote =	NULL,			/* no quoting function */
+  .version_report = spf_version_report	/* version reporting */
 };
 
-#ifdef notdef_DYNLOOKUP
-#define spf_lookup_module_info _lookup_module_info
+#ifdef DYNLOOKUP
+# define spf_lookup_module_info _lookup_module_info
 #endif
 
 static lookup_info *_lookup_list[] = { &spf_lookup_info };
-lookup_module_info spf_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+lookup_module_info spf_lookup_module_info =
+  { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
 
diff --git a/src/src/lookups/sqlite.c b/src/src/lookups/sqlite.c
index e358c9f84..eb23b8631 100644
--- a/src/src/lookups/sqlite.c
+++ b/src/src/lookups/sqlite.c
@@ -163,8 +163,6 @@ return quoted;
 
 /* See local README for interface description. */
 
-#include "../version.h"
-
 gstring *
 sqlite_version_report(gstring * g)
 {
diff --git a/src/src/lookups/whoson.c b/src/src/lookups/whoson.c
index f783a905a..dcd48db2d 100644
--- a/src/src/lookups/whoson.c
+++ b/src/src/lookups/whoson.c
@@ -63,8 +63,6 @@ switch (wso_query(CS query, CS buffer, sizeof(buffer)))
 
 /* See local README for interface description. */
 
-#include "../version.h"
-
 gstring *
 whoson_version_report(gstring * g)
 {
diff --git a/src/src/miscmods/perl.c b/src/src/miscmods/perl.c
index 2220c7dc9..41d946735 100644
--- a/src/src/miscmods/perl.c
+++ b/src/src/miscmods/perl.c
@@ -131,7 +131,8 @@ s = US SvPV(ST(0), len);
 log_write(0, LOG_MAIN, "%.*s", (int)len, s);
 }
 
-/* Do a DNS lookup using Exim's facilities.  Returns a scalar with the response packet. */
+/* Do a DNS lookup using Exim's facilities.
+Returns a scalar with the response packet; undef for DNS_FAIL or DNS_AGAIN. */
 
 XS(xs_dns_lookup)
 {
@@ -154,8 +155,10 @@ debug_printf_indent(" dnsa answer %p len %d\n", dnsa->answer, dnsa->answerlen);
 */
 
 ST(0) = sv_newmortal();
-sv_setpvn(ST(0), CCS dnsa->answer,
-		  (STRLEN) (dns_res == DNS_NODATA ? 0 : dnsa->answerlen));
+if (dns_res == DNS_AGAIN || dns_res == DNS_FAIL)
+  sv_setsv(ST(0), &PL_sv_undef);
+else
+  sv_setpvn(ST(0), CCS dnsa->answer, (STRLEN) dnsa->answerlen);
 XSRETURN(1);	/* ? needed because there are 2 arg, but 1 res? */
 
 store_free_dns_answer(dnsa);
@@ -222,9 +225,11 @@ errstr = exim_perl_add_codeblock(US
 	"\"TLSA\"  => 52,"
 	"\"SPF\"   => 99,"
 	"};"
-	"my $rrtype = $rr->{$rrtype_str};"		/*XXX only one rrtype per query...*/
+	/*XXX only one rrtype per query...*/
+	"my $rrtype = $rr->{$rrtype_str};"
         "my $dnsa = Exim::dns_lookup($dom, $rrtype);"
-	"my $res = Net::DNS::Packet->decode( \\$dnsa );"
+	"my $res;"
+	"my $res = Net::DNS::Packet->decode(\\$dnsa) unless (!defined($dnsa));"
 		/* "Exim::debug_write( $res->string . '\n' );" */
 	"return $res;"
   "}"
diff --git a/src/src/miscmods/spf_perl.c b/src/src/miscmods/spf_perl.c
index 3008facc8..17eb3ce35 100644
--- a/src/src/miscmods/spf_perl.c
+++ b/src/src/miscmods/spf_perl.c
@@ -212,7 +212,7 @@ else
   sep = '\n';
 
   spf_result = string_nextinlist(CUSS &res_list, &sep, NULL, 0);
-  DEBUG(D_acl) debug_printf_indent("SPF result is %s\n", spf_received);
+  DEBUG(D_acl) debug_printf_indent("MAIL::SPF result is %q\n", spf_received);
 
   spf_received = res_list;		/* remainder of the returned string */
 
@@ -329,9 +329,9 @@ spf_lookup_find(void * handle, const uschar * filename,
 {
 int res = FAIL;
 
-expand_level++;
 DEBUG(D_acl) debug_printf_indent("%s: mfrom:<%s> ip %q\n", __FUNCTION__,
 				  keystring, filename);
+expand_level++;
 
 if (setup_spf_perl_mi())
   if (!filename)
@@ -346,11 +346,12 @@ if (setup_spf_perl_mi())
       uschar * res_list = US string_from_gstring(g);
       int sep = '\n';
       *result = string_nextinlist(CUSS &res_list, &sep, NULL, 0);
-      DEBUG(D_acl) debug_printf_indent("SPF result is %s\n", *result);
+      DEBUG(D_acl) debug_printf_indent("MAIL::SPF result is %q\n", *result);
       res = OK;
       }
     }
 
+expand_level--;
 return res;
 }
 

commit 6096b50a869de851b710262dcc479f61e68c1bc7
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Nov 18 18:11:10 2025 +0000

    dmarc-native: fix rua/ruf verify

diff --git a/src/src/drtables.c b/src/src/drtables.c
index 87ff8da61..45b1ade16 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -346,7 +346,7 @@ if ((mi = misc_mod_findonly(name))) return mi;
 return misc_mod_load(name, errstr);
 #else
 *errstr = string_sprintf("module %q not built-in, and"
-	"no setting for LOOKUP_MODULE_DIR", name);
+	" no setting for LOOKUP_MODULE_DIR", name);
 return NULL;
 #endif	/*LOOKUP_MODULE_DIR*/
 }
diff --git a/src/src/miscmods/dmarc_common.c b/src/src/miscmods/dmarc_common.c
index 059b31167..d6e27c892 100644
--- a/src/src/miscmods/dmarc_common.c
+++ b/src/src/miscmods/dmarc_common.c
@@ -11,9 +11,6 @@
 
 #ifdef EXIM_HAVE_DMARC
 
-// #  include "dmarc.h"
-// #  include "pdkim.h"
-
 extern BOOL		dmarc_local_init(void);
 extern void		dmarc_local_msg_init(void);
 extern gstring *	dmarc_version_report(gstring *);
diff --git a/src/src/miscmods/dmarc_native.c b/src/src/miscmods/dmarc_native.c
index 617517d84..d073e4274 100644
--- a/src/src/miscmods/dmarc_native.c
+++ b/src/src/miscmods/dmarc_native.c
@@ -43,7 +43,7 @@ if (!dmarc_regex_uri)
   dmarc_regex_uri = regex_must_compile(US "^mailto:[^@]+@[^ !]+(?:[ !]|$)",
 							MCS_CACHEABLE, FALSE);
 if (!dmarc_regex_pct)
-  dmarc_regex_uri = regex_must_compile(US "^\\d{1,3}$",  MCS_CACHEABLE, FALSE);
+  dmarc_regex_pct = regex_must_compile(US "^\\d{1,3}$",  MCS_CACHEABLE, FALSE);
 if (!dmarc_regex_ri)
   dmarc_regex_ri =  regex_must_compile(US "^\\d{1,10}$", MCS_CACHEABLE, FALSE);
 if (!dmarc_regex_fo)
@@ -56,13 +56,6 @@ if (!dmarc_regex_fo)
 gstring *
 dmarc_version_report(gstring * g)
 {
-/*
-return string_fmt_append(g, "Library version: dmarc: Compile: %d.%d.%d.%d\n",
-    (OPENDMARC_LIB_VERSION & 0xff000000) >> 24,
-    (OPENDMARC_LIB_VERSION & 0x00ff0000) >> 16,
-    (OPENDMARC_LIB_VERSION & 0x0000ff00) >> 8,
-    (OPENDMARC_LIB_VERSION & 0x000000ff));
-*/
 return string_fmt_append(g, "Library version: dmarc: Exim %s builtin\n",
 			    EXIM_VERSION_STR);
 }

commit 2a87c1fb9d76cd00cc1815c9b56b8bea47e409e1
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 30 19:41:43 2025 +0000

    perl-spf: fix undef whine

diff --git a/src/src/miscmods/perl.c b/src/src/miscmods/perl.c
index 41d946735..1b111e711 100644
--- a/src/src/miscmods/perl.c
+++ b/src/src/miscmods/perl.c
@@ -77,6 +77,7 @@ if (SvTRUE(ERRSV))
   STRLEN len;
   s = US SvPV(ERRSV, len);
   s = string_copyn(s, (unsigned)len);
+  debug_printf_indent("adding perl codeblock: %s\n", s);
   }
 
 setlocale(LC_ALL, "C");		/* In case it got changed */
@@ -228,9 +229,13 @@ errstr = exim_perl_add_codeblock(US
 	/*XXX only one rrtype per query...*/
 	"my $rrtype = $rr->{$rrtype_str};"
         "my $dnsa = Exim::dns_lookup($dom, $rrtype);"
+
 	"my $res;"
-	"my $res = Net::DNS::Packet->decode(\\$dnsa) unless (!defined($dnsa));"
-		/* "Exim::debug_write( $res->string . '\n' );" */
+	"$res = new Net::DNS::Packet(\\$dnsa) if (defined($dnsa));"
+
+	/*XXX dumb, but at least clears the undef on errorstring */
+	"$self->errorstring(defined($dnsa) ? 'ok' : 'timeout');"
+
 	"return $res;"
   "}"
   "package MAIN;"
diff --git a/src/src/miscmods/spf_perl.c b/src/src/miscmods/spf_perl.c
index 17eb3ce35..4e854d7af 100644
--- a/src/src/miscmods/spf_perl.c
+++ b/src/src/miscmods/spf_perl.c
@@ -61,9 +61,10 @@ static const uschar spf_pl[] =
   "use Mail::SPF;"
   "sub my_spf_req {"
     "my ($mfrom, $conn_addr, $conn_helo) = @_;"
+    "my ($id, $sc) = ($mfrom ne '') ? ($mfrom, 'mfrom') : ($conn_helo, 'helo');"
     "my $request = Mail::SPF::Request->new("
-      "scope       => 'mfrom',"
-      "identity    => $mfrom,"
+      "scope       => $sc,"
+      "identity    => $id,"
       "ip_address  => $conn_addr,"
       "helo_identity => $conn_helo"
       ");"
@@ -80,21 +81,19 @@ spf lookup routine to it.  Safely does nothing if called again.
 */
 
 static const misc_module_info *
-setup_spf_perl_mi(void)
+setup_spf_perl_mi(uschar ** errstr)
 {
 typedef uschar * (*fn_t)(const uschar *);
 
 if (!spf_perl_mi)
   {
   if (!(spf_perl_mi = perl_startup(opt_perl_startup ? opt_perl_startup : US"")))
-    /* errstr = string_sprintf("spf: %s", expand_string_message); */
+    {
+    *errstr = string_sprintf("spf: %s", expand_string_message);
     return NULL;
+    }
 
-  /*XXX could return an error string here:
-  if ((errstr = (((fn_t *) spf_perl_mi->functions)[PERL_ADDBLOCK]) (spf_pl)))
-  */
-
-  if ((((fn_t *) spf_perl_mi->functions)[PERL_ADDBLOCK]) (spf_pl))
+  if ((*errstr = (((fn_t *) spf_perl_mi->functions)[PERL_ADDBLOCK]) (spf_pl)))
     return spf_perl_mi = NULL;
   }
 return spf_perl_mi;
@@ -187,14 +186,18 @@ spf_process(const uschar ** listptr, const uschar * spf_envelope_sender,
   int action)
 {
 int res = FAIL, sep;
+uschar * errstr;
 const uschar * arglist = *listptr;
 
 expand_level++;
 DEBUG(D_acl)
   debug_printf_indent("%s: mfrom:<%s>\n", __FUNCTION__, spf_envelope_sender);
 
-if (!setup_spf_perl_mi())
+if (!setup_spf_perl_mi(&errstr))
+  {
+  expand_level--;
   return FAIL;
+  }
 
 if (!(conn_helo && conn_addr))
   spf_result = US"permerror";
@@ -327,13 +330,14 @@ spf_lookup_find(void * handle, const uschar * filename,
   const uschar * keystring, int key_len, uschar ** result, uschar ** errmsg,
   uint * do_cache, const uschar * opts)
 {
+uschar * errstr;
 int res = FAIL;
 
 DEBUG(D_acl) debug_printf_indent("%s: mfrom:<%s> ip %q\n", __FUNCTION__,
 				  keystring, filename);
 expand_level++;
 
-if (setup_spf_perl_mi())
+if (setup_spf_perl_mi(&errstr))
   if (!filename)
     *result = US"permerror";
   else

commit 1b88acb34288b0858d0f26c1ab2ebc752d046595
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Dec 1 18:17:46 2025 +0000

    Fix nonstandard DNS server port number use from perl.  Bug 3177
    
    Broken-by: 040f2adb9003

diff --git a/src/src/miscmods/perl.c b/src/src/miscmods/perl.c
index 1b111e711..cee920c77 100644
--- a/src/src/miscmods/perl.c
+++ b/src/src/miscmods/perl.c
@@ -200,19 +200,26 @@ perl_parse(interp_perl, xs_init, argc, argv, 0);
 perl_run(interp_perl);
 
 /*********************************************************************/
+errstr = exim_perl_add_codeblock(US
+
 /* These lines by PH added to make "warn" output go to the Exim log; I
 hope this doesn't break anything. */
 
-errstr = exim_perl_add_codeblock(US
   "$SIG{__WARN__} = sub { my($s) = $_[0];"
   "$s =~ s/\\n$//;"
   "Exim::log_write($s) };"
 
-/* These lines added by JGH to route DNS queries via Exim's facilities */
+/* These lines added by JGH to route DNS queries via Exim's facilities.
+If a port was specified, we punt.
+*/
 
   "package Net::DNS::Resolver;"
   "sub send {"
-        "my ( $self, $dom, $rrtype_str ) = @_;"
+	"my $self = shift;"
+
+	"return $self->SUPER::send(@_) if ($self->{'port'} != 53);"
+
+        "my ( $dom, $rrtype_str ) = @_;"
 	"my $rr = {"
 	"\"A\"     => 1,"
 	"\"NS\"    => 2,"

commit 5f52bd307cccd1a840ab4e0b761add7b3d1946cd
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Dec 1 22:43:53 2025 +0000

    Build: Solaris compilers

diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index 30d446bad..42396fe8b 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -64,6 +64,9 @@ readconf_printtime(int t)
 const uschar * expand_string_2(const uschar * string, BOOL * textonly_p)
 { 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 29b229ef956c08381bb5514a4da3fecf89148441
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Dec 2 11:19:49 2025 +0000

    Build: GCC on Solaris

diff --git a/src/OS/os.h-SunOS5 b/src/OS/os.h-SunOS5
index f7ad50421..389b3403b 100644
--- a/src/OS/os.h-SunOS5
+++ b/src/OS/os.h-SunOS5
@@ -2,6 +2,13 @@
 /* Copyright (c) The Exim Maintainers 2021 - 2025 */
 /* SPDX-License-Identifier: GPL-2.0-or-later */
 
+/* The Sun Studio compilers define these, but GCC does not.
+Adjust if needed. */
+#if !defined(__SunOS_5_10) && !defined(__SunOS_5_11)
+# define __SunOS_5_11
+#endif
+
+
 #define CRYPT_H
 #define HAVE_MMAP
 #define HAVE_SYS_STATVFS_H
@@ -35,6 +42,11 @@ it seems. */
 
 /* default is non-const */
 #define ICONV_ARG2_TYPE const char **
+/* A GCC user reported that the following was needed.  Possibly using
+Glibc iconv?
+  #define ICONV_ARG2_TYPE char ** restrict
+A way of spotting the difference would be useful.
+*/
 
 #if _POSIX_C_SOURCE + 0 < 200112L
 # define MISSING_UNSETENV_3

commit 9536defc5cb25beeb00491f6b00c454aadfdb7a5
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Dec 2 22:59:04 2025 +0000

    SPF: logging

diff --git a/src/src/functions.h b/src/src/functions.h
index 76bc24c1b..873b8c8be 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -107,6 +107,7 @@ extern uschar *acl_standalone_setvar(const uschar *, BOOL);
 extern tree_node *acl_var_create(const uschar *);
 extern void    acl_var_write(uschar *, uschar *, void *);
 extern void    add_driver_info(driver_info **, const driver_info *, size_t);
+extern gstring * add_spf_info_for_log(gstring *);
 
 extern void    assert_no_variables(void *, int, const char *, int);
 extern void    atrn_handle_customer(void);
diff --git a/src/src/globals.c b/src/src/globals.c
index 38fdbc7a3..be5afe736 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -1021,6 +1021,10 @@ bit_table log_options[]        = { /* must be in alphabetical order,
   BIT_TABLE(L, smtp_no_mail),
   BIT_TABLE(L, smtp_protocol_error),
   BIT_TABLE(L, smtp_syntax_error),
+#ifdef EXIM_HAVE_SPF
+  BIT_TABLE(L, spf),
+  BIT_TABLE(L, spf_verbose),
+#endif
   BIT_TABLE(L, subject),
   BIT_TABLE(L, tls_certificate_verified),
   BIT_TABLE(L, tls_cipher),
diff --git a/src/src/macros.h b/src/src/macros.h
index c0900eee9..ad5d73267 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -453,7 +453,7 @@ enum logbit {
   LOG_BIT(dnslist_defer),
   LOG_BIT(etrn),
   LOG_BIT(host_lookup_failed),
-  LOG_BIT(lost_incoming_connection),
+  LOG_BIT(lost_incoming_connection),	/* 7 */
   LOG_BIT(queue_run),
   LOG_BIT(retry_defer),
   LOG_BIT(size_reject),
@@ -461,7 +461,7 @@ enum logbit {
   LOG_BIT(smtp_connection),
   LOG_BIT(smtp_incomplete_transaction),
   LOG_BIT(smtp_protocol_error),
-  LOG_BIT(smtp_syntax_error),
+  LOG_BIT(smtp_syntax_error),		/* 15 */
 
   Li_8bitmime = BITWORDSIZE,
   Li_acl_warn_skipped,
@@ -497,6 +497,8 @@ enum logbit {
   Li_smtp_confirmation,
   Li_smtp_mailauth,
   Li_smtp_no_mail,
+  Li_spf,
+  Li_spf_verbose,
   Li_subject,
   Li_tls_certificate_verified,
   Li_tls_cipher,
diff --git a/src/src/receive.c b/src/src/receive.c
index 0f407ce93..a0fcffd06 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -1361,6 +1361,21 @@ DEBUG(D_receive|D_acl) debug_printf_indent(">>\n");
 *       Add host information for log line        *
 *************************************************/
 
+gstring *
+add_spf_info_for_log(gstring * g)
+{
+#ifdef EXIM_HAVE_SPF
+if (LOGGING(spf_verbose))
+  {
+  const uschar * s = expand_string(CUS"$spf_result");
+  if (*s) g = string_append(g, 2, US" SPF=", s);
+  }
+else if (LOGGING(spf) && Ustrcmp(expand_string(CUS"$spf_result"), "pass") == 0)
+  g = string_catn(g, US" SPF", 4);
+#endif
+return g;
+}
+
 /* Called for acceptance and rejecting log lines. This adds information about
 the calling host to a string that is being built dynamically.
 
@@ -1407,6 +1422,8 @@ if (LOGGING(pipelining) && f.smtp_in_pipelining_advertised)
   if (!f.smtp_in_pipelining_used)
     g = string_catn(g, US"-", 1);
   }
+
+g = add_spf_info_for_log(g);
 return g;
 }
 
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 674bdd785..6cfc36f7a 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -3166,7 +3166,7 @@ switch (where)
 #ifdef WITH_CONTENT_SCAN
   case ACL_WHERE_MIME:
 #endif
-    sender_info = string_sprintf("F=<%s>%s%s%s%s ",
+    sender_info = string_sprintf(" F=<%s>%s%s%s%s",
       sender_address_unrewritten ? sender_address_unrewritten : sender_address,
       sender_host_authenticated ? US" A="                                    : US"",
       sender_host_authenticated ? sender_host_authenticated                  : US"",
@@ -3266,18 +3266,23 @@ is closing if required and return 2.  */
 if (log_reject_target)
   {
 #ifndef DISABLE_TLS
-  gstring * g = add_tls_info_for_log(NULL);
-  uschar * tls = string_from_gstring(g);
-  if (!tls) tls = US"";
+  gstring * tls = add_tls_info_for_log(NULL);
 #else
-  uschar * tls = US"";
+  gstring * tls = NULL;
 #endif
+#ifdef EXIM_HAVE_SPF
+  gstring * spf= add_spf_info_for_log(NULL);
+#else
+  gstring * spf = NULL;
+#endif
+
   log_write(where == ACL_WHERE_CONNECT ? L_connection_reject : 0,
-    log_reject_target, "%s%s%s %s%srejected %s%s",
+    log_reject_target, "%s%s%#Y%s%#Y %srejected %s%s",
     LOGGING(dnssec) && sender_host_dnssec ? US" DS" : US"",
     host_and_ident(TRUE),
     tls,
     sender_info,
+    spf,
     rc == FAIL ? US"" : US"temporarily ",
     what, log_msg);
   }

commit b8d16e2a4e8e2016e8cd03402705a6f47f1e2b5b
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Dec 3 12:16:59 2025 +0000

    SPF: support * in ACL condition

diff --git a/src/src/miscmods/spf.c b/src/src/miscmods/spf.c
index f73a8776b..4febe7f4e 100644
--- a/src/src/miscmods/spf.c
+++ b/src/src/miscmods/spf.c
@@ -373,10 +373,7 @@ static int
 spf_process(const uschar ** listptr, const uschar * spf_envelope_sender,
   int action)
 {
-int sep = 0;
-const uschar * list = * listptr;
-uschar * spf_result_id;
-int rc = SPF_RESULT_PERMERROR, ret = OK;
+int rc = SPF_RESULT_PERMERROR, ret;
 
 DEBUG(D_receive) { debug_printf_indent("SPF: process\n"); expand_level++; }
 
@@ -419,24 +416,13 @@ if (action == SPF_PROCESS_GUESS && (!strcmp (SPF_strresult(rc), "none")))
 
 else
   {
-  while ((spf_result_id = string_nextinlist(&list, &sep, NULL, 0)))
-    {
-    BOOL negate, result;
-
-    if ((negate = spf_result_id[0] == '!'))
-      spf_result_id++;
-
-    result = Ustrcmp(spf_result_id, spf_result_id_list[rc].name) == 0;
-    if (negate != result) goto out;
-    }
-
-  /* no match */
-  ret = FAIL;
+  const uschar * list = *listptr;
+  ret = match_isinlist(spf_result_id_list[rc].name, &list,
+                    0, NULL, NULL, MCL_STRING, TRUE, NULL);
   }
 
-out:
-  DEBUG(D_receive) expand_level--;
-  return ret;
+DEBUG(D_receive) expand_level--;
+return ret;
 }
 
 
diff --git a/src/src/miscmods/spf_perl.c b/src/src/miscmods/spf_perl.c
index 4e854d7af..55741eef0 100644
--- a/src/src/miscmods/spf_perl.c
+++ b/src/src/miscmods/spf_perl.c
@@ -35,9 +35,6 @@ static spf_result_id spf_result_id_list[] = {
   { US"permerror",	7 }  /* RFC 4408 defined */
 };
 
-const uschar * conn_helo = NULL;
-const uschar * conn_addr = NULL;
-
 uschar * spf_guess              = US"v=spf1 a/24 mx/24 ptr ?all";
 uschar * spf_header_comment     = NULL;
 uschar * spf_received           = NULL;
@@ -131,34 +128,6 @@ return string_cat(g, US"Library_version: SPF: perl Mail::SPF\n");
 
 
 
-/*API*/
-/* Set up a context that can be re-used for several
-   messages on the same SMTP connection (that come from the
-   same host with the same HELO string).
-
-We delay doing perl startup until spf processing time, as ACL might
-never need us on any given connection.
-
-Return: OK/FAIL
-*/
-
-static int
-spf_conn_init(const uschar * spf_helo_domain, const uschar * spf_remote_addr,
-  const uschar ** errstr)
-{
-DEBUG(D_receive) debug_printf_indent("spf_conn_init: helo:%s addr:%s\n",
-			      spf_helo_domain, spf_remote_addr);
-
-/* Copy the args to globals */
-
-conn_helo = spf_helo_domain;
-conn_addr = spf_remote_addr;
-
-return OK;
-}
-
-
-
 /*API*/
 static void
 spf_smtp_reset(void)
@@ -185,7 +154,7 @@ static int
 spf_process(const uschar ** listptr, const uschar * spf_envelope_sender,
   int action)
 {
-int res = FAIL, sep;
+int res, sep;
 uschar * errstr;
 const uschar * arglist = *listptr;
 
@@ -199,17 +168,18 @@ if (!setup_spf_perl_mi(&errstr))
   return FAIL;
   }
 
-if (!(conn_helo && conn_addr))
+if (!(sender_helo_name && sender_host_address))
   spf_result = US"permerror";
 
 else
   {
-  const uschar * argv[4] = {spf_envelope_sender, conn_addr, conn_helo, NULL};
+  const uschar * argv[4] = {spf_envelope_sender, sender_host_address,
+			    sender_helo_name, NULL};
   gstring * g;
   uschar * res_list, * s;
 
   if (!(g = call_my_spf_req(argv)))
-    goto out;
+    { res = FAIL; goto out; }
 
   res_list = US string_from_gstring(g);
   sep = '\n';
@@ -228,18 +198,8 @@ else
     }
   }
 
-sep = 0;
-for (uschar * ele; ele = string_nextinlist(&arglist, &sep, NULL, 0); )
-  {
-  BOOL negate, result;
-
-  if ((negate = *ele == '!'))
-    ele++;
-
-  result = Ustrcmp(ele, spf_result) == 0;
-  if (negate != result) { res = OK; break; }
-  }
-/* if the loop ran out of list, no match */
+res = match_isinlist(spf_result, &arglist,
+		    0, NULL, NULL, MCL_STRING, TRUE, NULL);
 
 out:
   expand_level--;
@@ -342,7 +302,7 @@ if (setup_spf_perl_mi(&errstr))
     *result = US"permerror";
   else
     {
-    const uschar * argv[4] = {keystring, filename, conn_helo, NULL};
+    const uschar * argv[4] = {keystring, filename, sender_helo_name, NULL};
     gstring * g;
 
     if ((g = call_my_spf_req(argv)))
@@ -393,7 +353,7 @@ misc_module_info spf_module_info =
   .dyn_magic =		MISC_MODULE_MAGIC,
 # endif
   .lib_vers_report =	spf_lib_version_report,
-  .conn_init =		spf_conn_init,
+  .conn_init =		NULL,
   .smtp_reset =		spf_smtp_reset,
   .authres =		authres_spf,
 

commit 266b6e714cf4a95d412a81197516e6dcb1454214
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Dec 3 14:10:50 2025 +0000

    Debug: redefine the early-debug macro to be also active after main-debug init

diff --git a/src/src/macros.h b/src/src/macros.h
index ad5d73267..0b69bb83f 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -106,11 +106,10 @@ don't make the file descriptors two-way. */
 #define HDEBUG(x)	if (host_checking || IS_DEBUG(x))
 
 #define EARLY_DEBUG(x, fmt, ...) \
-  if (debug_startup) \
-    if (debug_fd < 0) \
-      fprintf(stderr, "%s", string_sprintf(fmt, __VA_ARGS__)); \
-    else DEBUG(x) \
-      debug_printf_indent(fmt, __VA_ARGS__);
+  if (debug_fd >= 0) \
+    { DEBUG(x) debug_printf_indent(fmt, __VA_ARGS__); } \
+  else if (debug_startup) \
+    fprintf(stderr, "%s", string_sprintf(fmt, __VA_ARGS__));
 
 /* The default From: text for DSNs */
 
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 04cd5e68a..37d9c6b85 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -4592,7 +4592,7 @@ readconf_rest(void)
 {
 int had = 0;
 
-while(next_section[0] != 0)
+while(*next_section)
   {
   int bit;
   int first = 0;
@@ -4601,6 +4601,7 @@ while(next_section[0] != 0)
   int n = Ustrlen(next_section);
 
   EARLY_DEBUG(D_any, "%s: %s\n", __FUNCTION__, next_section);
+  expand_level++;
   if (tolower(next_section[n-1]) != 's') Ustrcpy(next_section+n, US"s");
 
   for (;;)
@@ -4630,6 +4631,7 @@ while(next_section[0] != 0)
     case 5: route_init(); break;
     case 6: transport_init(); break;
     }
+  expand_level--;
   }
 
 (void)fclose(config_file);

commit c5d27945075095ee56c92eee36a81aa239a53494
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Dec 3 15:06:10 2025 +0000

    DMARC: logging

diff --git a/src/src/functions.h b/src/src/functions.h
index 873b8c8be..a15369af9 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -20,7 +20,6 @@ are in in fact in separate headers. */
 
 
 #ifndef DISABLE_TLS
-extern gstring *	add_tls_info_for_log(gstring *);
 extern const char *	std_dh_prime_default(void);
 extern const char *	std_dh_prime_named(const uschar *);
 
@@ -106,8 +105,10 @@ extern uschar *acl_standalone_setvar(const uschar *, BOOL);
 
 extern tree_node *acl_var_create(const uschar *);
 extern void    acl_var_write(uschar *, uschar *, void *);
+extern gstring * add_dmarc_info_for_log(gstring *);
 extern void    add_driver_info(driver_info **, const driver_info *, size_t);
 extern gstring * add_spf_info_for_log(gstring *);
+extern gstring * add_tls_info_for_log(gstring *);
 
 extern void    assert_no_variables(void *, int, const char *, int);
 extern void    atrn_handle_customer(void);
diff --git a/src/src/globals.c b/src/src/globals.c
index be5afe736..610c72909 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -979,6 +979,10 @@ bit_table log_options[]        = { /* must be in alphabetical order,
 #ifndef DISABLE_DKIM
   BIT_TABLE(L, dkim),
   BIT_TABLE(L, dkim_verbose),
+#endif
+#ifdef EXIM_HAVE_DMARC
+  BIT_TABLE(L, dmarc),
+  BIT_TABLE(L, dmarc_verbose),
 #endif
   BIT_TABLE(L, dnslist_defer),
   BIT_TABLE(L, dnssec),
diff --git a/src/src/macros.h b/src/src/macros.h
index 0b69bb83f..6c7d8b085 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -470,6 +470,8 @@ enum logbit {
   Li_delivery_size,
   Li_dkim,
   Li_dkim_verbose,
+  Li_dmarc,
+  Li_dmarc_verbose,
   Li_dnssec,
   Li_ident_timeout,
   Li_incoming_interface,
diff --git a/src/src/miscmods/dmarc.c b/src/src/miscmods/dmarc.c
index be5d68c3a..8d840ae84 100644
--- a/src/src/miscmods/dmarc.c
+++ b/src/src/miscmods/dmarc.c
@@ -431,7 +431,8 @@ The EDITME provides a DMARC_API variable */
     dmarc_alignment_dkim =
       dmarc_dkim_alignment == DMARC_POLICY_DKIM_ALIGNMENT_PASS;
 
-    log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s "
+    if (LOGGING(dmarc_verbose))
+      log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s "
 			   "spf_align=%s dkim_align=%s enforcement='%s'",
 			   spf_sender_domain, dmarc_used_domain,
 			   dmarc_alignment_spf  ? "yes" : "no",
diff --git a/src/src/miscmods/dmarc_native.c b/src/src/miscmods/dmarc_native.c
index d073e4274..02eb5dbd3 100644
--- a/src/src/miscmods/dmarc_native.c
+++ b/src/src/miscmods/dmarc_native.c
@@ -628,14 +628,12 @@ use the sp.  Otherwise use the p. */
       }
     }
 
-  // the alignments would be results of the DMARC evaluation
-  // - we have them already, from the dkim & spf processing
-
   if (has_dmarc_record && !dmarc_abort)
     {
-    /* Log results.  Robably should have a log_selector to reduce noise. */
+    /* Log results. */
 
-    log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s "
+    if (LOGGING(dmarc_verbose))
+      log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s "
 			   "spf_align=%s dkim_align=%s enforcement='%s'",
 			   spf_sender_domain, dmarc_used_domain,
 			   dmarc_alignment_spf  ? "yes" : "no",
diff --git a/src/src/receive.c b/src/src/receive.c
index a0fcffd06..7827d0288 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -1376,6 +1376,21 @@ else if (LOGGING(spf) && Ustrcmp(expand_string(CUS"$spf_result"), "pass") == 0)
 return g;
 }
 
+gstring *
+add_dmarc_info_for_log(gstring * g)
+{
+#ifdef EXIM_HAVE_DMARC
+if (LOGGING(dmarc_verbose))
+  {
+  const uschar * s = expand_string(CUS"$dmarc_status");
+  if (*s) g = string_append(g, 2, US" DMARC=", s);
+  }
+else if (LOGGING(dmarc) && Ustrcmp(expand_string(CUS"$dmarc_status"), "accept") == 0)
+  g = string_catn(g, US" DMARC", 6);
+#endif
+return g;
+}
+
 /* Called for acceptance and rejecting log lines. This adds information about
 the calling host to a string that is being built dynamically.
 
@@ -4099,10 +4114,7 @@ if (message_reference)
   g = string_append(g, 2, US" R=", message_reference);
 
 g = add_host_info_for_log(g);
-
-#ifndef DISABLE_TLS
 g = add_tls_info_for_log(g);
-#endif
 
 if (sender_host_authenticated)
   {
@@ -4156,6 +4168,8 @@ if (LOGGING(dkim))
   }
 #endif
 
+g = add_dmarc_info_for_log(g);
+
 if (LOGGING(receive_time))
   {
   struct timeval diff = received_time_complete;
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 6cfc36f7a..d694c8d7a 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -1483,7 +1483,6 @@ return string_from_gstring(g);
 
 
 
-#ifndef DISABLE_TLS
 /* Append TLS-related information to a log line
 
 Arguments:
@@ -1494,13 +1493,14 @@ Returns:	Allocated string or NULL
 gstring *
 add_tls_info_for_log(gstring * g)
 {
+#ifndef DISABLE_TLS
 if (LOGGING(tls_cipher) && tls_in.cipher)
   {
   g = string_append(g, 2, US" X=", tls_in.cipher);
-#ifndef DISABLE_TLS_RESUME
+# ifndef DISABLE_TLS_RESUME
   if (LOGGING(tls_resumption) && tls_in.resumption & RESUME_USED)
     g = string_catn(g, US"*", 1);
-#endif
+# endif
   }
 if (LOGGING(tls_certificate_verified) && tls_in.peercert)
   g = string_append(g, 2, US" CV=", tls_in.certificate_verified? "yes":"no");
@@ -1509,8 +1509,8 @@ if (LOGGING(tls_peerdn) && tls_in.peerdn)
 if (LOGGING(tls_sni) && tls_in.sni)
   g = string_append(g, 2, US" SNI=", string_printing2(tls_in.sni, SP_TAB|SP_SPACE));
 return g;
-}
 #endif
+}
 
 
 
@@ -1547,7 +1547,6 @@ Returns:     nothing
 void
 smtp_log_no_mail(void)
 {
-uschar * s;
 gstring * g = NULL;
 
 if (smtp_mailcmd_count > 0 || !LOGGING(smtp_no_mail))
@@ -1559,17 +1558,12 @@ if (sender_host_authenticated)
   if (authenticated_id) g = string_append(g, 2, US":", authenticated_id);
   }
 
-#ifndef DISABLE_TLS
 g = add_tls_info_for_log(g);
-#endif
-
 g = s_connhad_log(g);
 
-if (!(s = string_from_gstring(g))) s = US"";
-
-log_write(0, LOG_MAIN, "no MAIL in %sSMTP connection from %s D=%s%s",
+log_write(0, LOG_MAIN, "no MAIL in %sSMTP connection from %s D=%s%#Y",
   f.tcp_in_fastopen ? f.tcp_in_fastopen_data ? US"TFO* " : US"TFO " : US"",
-  host_and_ident(FALSE), string_timesince(&smtp_connection_start), s);
+  host_and_ident(FALSE), string_timesince(&smtp_connection_start), g);
 }
 
 
@@ -2178,21 +2172,13 @@ static void
 log_connect_tls_drop(const uschar * what, const uschar * log_msg)
 {
 if (log_reject_target)
-  {
-#ifdef DISABLE_TLS
-  uschar * tls = NULL;
-#else
-  gstring * g = add_tls_info_for_log(NULL);
-  uschar * tls = string_from_gstring(g);
-#endif
   log_write(L_connection_reject,
-    log_reject_target, "%s%s%s dropped by %s%s%s",
+    log_reject_target, "%s%s%#Y dropped by %s%s%s",
     LOGGING(dnssec) && sender_host_dnssec ? US" DS" : US"",
     host_and_ident(TRUE),
-    tls ? tls : US"",
+    add_tls_info_for_log(NULL),
     what,
     log_msg ? US": " : US"", log_msg);
-  }
 }
 
 
@@ -3264,28 +3250,16 @@ the connection is not forcibly to be dropped, return 0. Otherwise, log why it
 is closing if required and return 2.  */
 
 if (log_reject_target)
-  {
-#ifndef DISABLE_TLS
-  gstring * tls = add_tls_info_for_log(NULL);
-#else
-  gstring * tls = NULL;
-#endif
-#ifdef EXIM_HAVE_SPF
-  gstring * spf= add_spf_info_for_log(NULL);
-#else
-  gstring * spf = NULL;
-#endif
-
   log_write(where == ACL_WHERE_CONNECT ? L_connection_reject : 0,
-    log_reject_target, "%s%s%#Y%s%#Y %srejected %s%s",
+    log_reject_target, "%s%s%#Y%s%#Y%#Y %srejected %s%s",
     LOGGING(dnssec) && sender_host_dnssec ? US" DS" : US"",
     host_and_ident(TRUE),
-    tls,
+    add_tls_info_for_log(NULL),
     sender_info,
-    spf,
+    add_spf_info_for_log(NULL),
+    add_dmarc_info_for_log(NULL),
     rc == FAIL ? US"" : US"temporarily ",
     what, log_msg);
-  }
 
 if (!drop) return 0;
 

commit c5007722d5f5e86953912ea75b13575577e4f98a
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Dec 4 22:22:17 2025 +0000

    Perl: tweak dns lookup fail cases

diff --git a/src/src/dns.c b/src/src/dns.c
index dd754f6c9..5ed896b38 100644
--- a/src/src/dns.c
+++ b/src/src/dns.c
@@ -717,7 +717,7 @@ the packet length if the packet header looks plausible.
 
 Return TRUE iff it seemed ok */
 
-static BOOL
+BOOL
 fake_dnsa_len_for_fail(dns_answer * dnsa, int type)
 {
 const HEADER * h = (const HEADER *)dnsa->answer;
diff --git a/src/src/functions.h b/src/src/functions.h
index a15369af9..49ed1064d 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -248,6 +248,7 @@ extern uschar *expand_string_copy(const uschar *);
 extern int_eximarith_t expand_string_integer(uschar *, BOOL);
 extern void    modify_variable(uschar *, void *);
 
+extern BOOL    fake_dnsa_len_for_fail(dns_answer *, int);
 extern BOOL    fd_ready(int, time_t);
 extern BOOL    filter_runtest(int, const uschar *, BOOL, BOOL);
 
diff --git a/src/src/lookups/Makefile b/src/src/lookups/Makefile
index 23347e83b..27ae3fe1e 100644
--- a/src/src/lookups/Makefile
+++ b/src/src/lookups/Makefile
@@ -40,7 +40,7 @@ avail_static_lookups.o: $(HDRS) avail_static_lookups.c
 lookups.a:       avail_static_lookups.o $(OBJS)
 		 @$(RM_COMMAND) -f lookups.a
 		 @echo "$(AR) lookups.a"
-		 @$(AR) lookups.a avail_static_lookups.o $(OBJS)
+		 $(FE)$(AR) lookups.a avail_static_lookups.o $(OBJS)
 		 $(RANLIB) $@
 
 .SUFFIXES:       .o .c .so
diff --git a/src/src/miscmods/perl.c b/src/src/miscmods/perl.c
index cee920c77..5877d3dea 100644
--- a/src/src/miscmods/perl.c
+++ b/src/src/miscmods/perl.c
@@ -158,6 +158,8 @@ debug_printf_indent(" dnsa answer %p len %d\n", dnsa->answer, dnsa->answerlen);
 ST(0) = sv_newmortal();
 if (dns_res == DNS_AGAIN || dns_res == DNS_FAIL)
   sv_setsv(ST(0), &PL_sv_undef);
+else if (dnsa->answerlen == -1 && !fake_dnsa_len_for_fail(dnsa, rrtype_int))
+  sv_setsv(ST(0), &PL_sv_undef);
 else
   sv_setpvn(ST(0), CCS dnsa->answer, (STRLEN) dnsa->answerlen);
 XSRETURN(1);	/* ? needed because there are 2 arg, but 1 res? */

commit 46846fc1f6254cb6a1ef14babc40fcae9f5baea1
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Dec 5 12:18:39 2025 +0000

    taint: better tracking for dns lookup results

diff --git a/src/src/acl.c b/src/src/acl.c
index be61fc953..b3c45668a 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -1435,7 +1435,7 @@ tree_node *t;
 const uschar *found;
 int priority, weight, port;
 dns_answer * dnsa;
-dns_scan dnss;
+dns_scan dnss = {0};
 dns_record *rr;
 int rc, type, yield;
 #define TARGET_SIZE 256
@@ -1584,7 +1584,7 @@ be authorized. (This is an odd configuration because weight=2 target=. is
 equivalent to weight=1, but we check for it in order to keep load off the
 root name servers.) Note that dn_expand() turns "." into "". */
 
-if (Ustrcmp(target, "") == 0)
+if (!*target)
   {
   yield = CSA_FAIL_NOADDR;
   goto out;
diff --git a/src/src/auths/dovecot.c b/src/src/auths/dovecot.c
index 118e95f29..c9fdc0ea9 100644
--- a/src/src/auths/dovecot.c
+++ b/src/src/auths/dovecot.c
@@ -298,7 +298,10 @@ if (ob->server_tls)
   {
   union sockaddr_46 interface_sock;
   EXIM_SOCKLEN_T size = sizeof(interface_sock);
+
+  /* Large (64k if DANE supported) */
   smtp_connect_args conn_args = { .host = &host };
+
   tls_support tls_dummy = { .sni = NULL };
   uschar * errstr;
 
diff --git a/src/src/dns.c b/src/src/dns.c
index 5ed896b38..6b527ef00 100644
--- a/src/src/dns.c
+++ b/src/src/dns.c
@@ -318,6 +318,20 @@ dnss_inc_aptr(const dns_answer * dnsa, dns_scan * dnss, unsigned delta)
 return dnsa_bad_ptr(dnsa, dnss->aptr += delta);
 }
 
+static int
+dns_rr_expand_taint(const uschar * msg, const uschar * eom,
+  const uschar * comp_dn, dns_scan * dnss)
+{
+uschar buf[DNS_MAXNAME];
+int ret;
+
+if ((ret = dn_expand(msg, eom, comp_dn,
+		    (DN_EXPAND_ARG4_TYPE) buf, sizeof(buf))) < 0)
+  return ret;
+dnss->srr.name = string_copy_taint(buf, GET_TAINTED);
+return ret;
+}
+
 /*************************************************
 *       Get next DNS record from answer block    *
 *************************************************/
@@ -339,6 +353,7 @@ dns_record *
 dns_next_rr(const dns_answer * dnsa, dns_scan * dnss, int reset)
 {
 const HEADER * h = (const HEADER *)dnsa->answer;
+const uschar * eom = dnsa->answer + dnsa->answerlen;
 int namelen;
 
 char * trace = NULL;
@@ -361,9 +376,9 @@ if (reset != RESET_NEXT)
   while (dnss->rrcount-- > 0)
     {
     TRACE trace = "Q-namelen";
-    namelen = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen,
-      dnss->aptr, (DN_EXPAND_ARG4_TYPE) &dnss->srr.name, DNS_MAXNAME);
-    if (namelen < 0) goto null_return;
+    /*TTT*/
+    if ((namelen = dns_rr_expand_taint(dnsa->answer, eom, dnss->aptr, dnss)) <0)
+      goto null_return;
     /* skip name & type & class */
     TRACE trace = "Q-skip";
     if (dnss_inc_aptr(dnsa, dnss, namelen+4)) goto null_return;
@@ -392,9 +407,10 @@ if (reset != RESET_NEXT)
     while (dnss->rrcount-- > 0)
       {
       TRACE trace = "A-namelen";
-      namelen = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen,
-        dnss->aptr, (DN_EXPAND_ARG4_TYPE) &dnss->srr.name, DNS_MAXNAME);
-      if (namelen < 0) goto null_return;
+      /*TTT*/
+      if (  (namelen = dns_rr_expand_taint(dnsa->answer, eom, dnss->aptr, dnss))
+	  < 0)
+	goto null_return;
 
       /* skip name, type, class & TTL */
       TRACE trace = "A-hdr";
@@ -426,9 +442,9 @@ if (dnss->rrcount-- <= 0) return NULL;
 (something safe). */
 
 TRACE trace = "R-namelen";
-namelen = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, dnss->aptr,
-  (DN_EXPAND_ARG4_TYPE) &dnss->srr.name, DNS_MAXNAME);
-if (namelen < 0) goto null_return;
+/*TTT*/
+if ((namelen = dns_rr_expand_taint(dnsa->answer, eom, dnss->aptr, dnss)) < 0)
+  goto null_return;
 
 /* Move the pointer past the name and fill in the rest of the data structure
 from the following bytes.  We seem to be assuming here that the RR blob passed
@@ -482,16 +498,16 @@ Scan the whole AUTHORITY section, since it may contain other records
 Return: name for the authority, in an allocated string, or NULL if none found */
 
 static const uschar *
-dns_extract_auth_name(const dns_answer * dnsa)	/* FIXME: const dns_answer */
+dns_extract_auth_name(const dns_answer * dnsa)
 {
-dns_scan dnss;
+dns_scan dnss = {0};
 const HEADER * h = (const HEADER *) dnsa->answer;
 
 if (h->nscount && h->aa)
   for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_AUTHORITY);
        rr; rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
     if (rr->type == (h->ancount ? T_NS : T_SOA))
-      return string_copy(rr->name);
+      return string_copy(rr->name);	/*TTT*/
 return NULL;
 }
 
@@ -750,7 +766,7 @@ bother doing a separate lookup; if not found return a forever TTL.
 time_t
 dns_expire_from_soa(dns_answer * dnsa, int type)
 {
-dns_scan dnss;
+dns_scan dnss = {0};
 
 if (fake_dnsa_len_for_fail(dnsa, type))
   for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_AUTHORITY);
@@ -1046,7 +1062,7 @@ for (int i = 0; i <= dns_cname_loops; i++)
   {
   uschar * data;
   dns_record cname_rr, type_rr;
-  dns_scan dnss;
+  dns_scan dnss = {0};
 
   /* DNS lookup failures get passed straight back. */
 
@@ -1087,7 +1103,7 @@ for (int i = 0; i <= dns_cname_loops; i++)
 	  )
 #endif
        )
-        *fully_qualified_name = string_copy_dnsdomain(rr_name);
+        *fully_qualified_name = string_copy_dnsdomain(rr_name);	/*TTT*/
     }
 
   /* If any data records of the correct type were found, we are done. */
@@ -1111,7 +1127,7 @@ for (int i = 0; i <= dns_cname_loops; i++)
     }
 
   /* DNS data comes from the outside, hence tainted */
-  data = store_get(256, GET_TAINTED);
+  data = store_get(256, GET_TAINTED);				/*TTT alloc*/
   if (dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen,
       cname_rr.data, (DN_EXPAND_ARG4_TYPE)data, 256) < 0)
     {
@@ -1120,6 +1136,13 @@ for (int i = 0; i <= dns_cname_loops; i++)
     }
   name = data;
 
+  for (int i = 256-50; i > 0; i--)	/* Usually much smaller, so find end */
+    if (!*data++)
+      {
+      store_release_above(data);	/* and release excess allocation */
+      break;
+      }
+
   if (!dns_is_secure(dnsa))
     secure_so_far = FALSE;
 
@@ -1239,8 +1262,8 @@ switch (type)
     uschar * namesuff, * tld;
     int priority, dummy_weight, port, limit, rc, i;
     BOOL ipv6;
-    dns_record *rr;
-    dns_scan dnss;
+    dns_record * rr;
+    dns_scan dnss = {0};
 
     DEBUG(D_dns) debug_printf_indent("CSA lookup of %s\n", name);
 
@@ -1336,7 +1359,7 @@ switch (type)
 	/* If it's making an interesting assertion, return this response. */
 	if (port & 1)
 	  {
-	  *fully_qualified_name = namesuff + 1;
+	  *fully_qualified_name = namesuff + 1;	/*TTT*/
 	  return DNS_SUCCEED;
 	  }
 	}
diff --git a/src/src/dnsbl.c b/src/src/dnsbl.c
index dc4a0bea0..ac3e017e9 100644
--- a/src/src/dnsbl.c
+++ b/src/src/dnsbl.c
@@ -70,7 +70,7 @@ one_check_dnsbl(uschar *domain, uschar *domain_txt, uschar *keydomain,
   int defer_return)
 {
 dns_answer * dnsa = store_get_dns_answer();
-dns_scan dnss;
+dns_scan dnss = {0};
 tree_node *t;
 dnsbl_cache_block *cb;
 int old_pool = store_pool;
@@ -363,6 +363,7 @@ if (cb->rc == DNS_SUCCEED)
 	  int len = (rr->data)[0];
 	  if (len > 511) len = 127;
 	  store_pool = POOL_PERM;
+	  /*TTT*/
 	  cb->text = string_copyn_taint(CUS (rr->data+1), len, GET_TAINTED);
 	  store_pool = old_pool;
 	  break;
diff --git a/src/src/functions.h b/src/src/functions.h
index 49ed1064d..ec13c544e 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -590,7 +590,7 @@ extern uschar *string_base62_32(unsigned long int);
 extern uschar *string_base62_64(unsigned long int);
 extern gstring *string_catn(gstring *, const uschar *, int) WARN_UNUSED_RESULT;
 extern int     string_compare_by_pointer(const void *, const void *);
-extern uschar *string_copy_dnsdomain(uschar *);
+extern uschar *string_copy_dnsdomain(const uschar *);
 extern uschar *string_copy_malloc(const uschar *);
 extern uschar *string_dequote(const uschar **);
 extern uschar *string_format_size(int, uschar *);
@@ -1145,8 +1145,9 @@ return item
 /* Use store_malloc for DNSA structs, and explicit frees. Using the same pool
 for them as the strings we proceed to copy from them meant they could not be
 released, hence blowing 64k for every DNS lookup. That mounted up. With malloc
-we do have to take care over marking tainted all copied strings.  A separate pool
-could be used and would handle that implicitly. */
+we do have to take care over marking tainted all copied strings.
+A separate pool could be used and could handle taint implicitly - but we would
+want to support independent free ops, not limited to stacked alloc/release */
 
 #define store_get_dns_answer() store_get_dns_answer_trc(CUS __FUNCTION__, __LINE__)
 
diff --git a/src/src/host.c b/src/src/host.c
index 4c513e28c..95694635a 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -180,7 +180,7 @@ uschar *adds;
 uschar **alist;
 struct hostent *yield;
 dns_answer * dnsa = store_get_dns_answer();
-dns_scan dnss;
+dns_scan dnss = {0};
 
 DEBUG(D_host_lookup)
   debug_printf_indent("using host_fake_gethostbyname for %s (%s)\n", name,
@@ -1587,7 +1587,7 @@ uschar **aliases;
 uschar *ordername;
 const uschar *list = host_lookup_order;
 dns_answer * dnsa = store_get_dns_answer();
-dns_scan dnss;
+dns_scan dnss = {0};
 
 sender_host_dnssec = host_lookup_deferred = host_lookup_failed = FALSE;
 
@@ -1662,6 +1662,7 @@ while ((ordername = string_nextinlist(&list, &sep, NULL, 0)))
         /* If an overlong response was received, the data will have been
         truncated and dn_expand may fail. */
 
+	/*TTT*/
         if (dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen,
              US rr->data, (DN_EXPAND_ARG4_TYPE)(s), ssize) < 0)
           {
@@ -2286,7 +2287,7 @@ for (; i >= 0; i--)
   int type = types[i];
   int randoffset = i == (whichrrs & HOST_FIND_IPV4_FIRST ? 1 : 0)
     ? 500 : 0;  /* Ensures v6/4 sort order */
-  dns_scan dnss;
+  dns_scan dnss = {0};
 
   int rc = dns_lookup_timerwrap(dnsa, host->name, type, fully_qualified_name);
   lookup_dnssec_authenticated = !dnssec_request ? NULL
@@ -2389,7 +2390,7 @@ for (; i >= 0; i--)
       if (thishostlast == NULL)
 	{
 	if (strcmpic(host->name, rr->name) != 0)
-	  host->name = string_copy_dnsdomain(rr->name);
+	  host->name = string_copy_dnsdomain(rr->name);	/*TTT*/
 	host->address = da->address;
 	host->sort_key = host->mx * 1000 + random_number(500) + randoffset;
 	host->status = hstatus_unknown;
@@ -2530,7 +2531,7 @@ BOOL srv_smtps = FALSE;
 #endif
 int rc = DNS_FAIL, ind_type = 0, yield;
 dns_answer * dnsa = store_get_dns_answer();
-dns_scan dnss;
+dns_scan dnss = {0};
 BOOL dnssec_require, dnssec_request;
 dnssec_status_t dnssec;
 
@@ -2784,7 +2785,7 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
   int port = PORT_NONE;		/* MX lookups get PORT_NONE */
   const uschar * s = rr->data;	/* MUST be unsigned for GETSHORT */
   host_item * next;
-  uschar data[256];
+  uschar data[256];	/*TTT*/
 
   if (rr_bad_size(rr, sizeof(uint16_t))) continue;
   GETSHORT(precedence, s);      /* Pointer s is advanced */
@@ -2811,8 +2812,10 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
 
   /* Get the name of the host pointed to. */
 
-  (void)dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, s,
-    (DN_EXPAND_ARG4_TYPE)data, sizeof(data));
+  /*TTT*/
+  if (dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, s,
+      (DN_EXPAND_ARG4_TYPE)data, sizeof(data)) < 0)
+    continue;
 
   /* Check that we haven't already got this host on the chain; if we have,
   keep only the lower precedence. This situation shouldn't occur, but you
@@ -2851,7 +2854,7 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
   before the first block, copy the first block's data to a new second block. */
 
   next = last ? store_get(sizeof(host_item), GET_UNTAINTED) : host;
-  next->name = string_copy_dnsdomain(data);
+  next->name = string_copy_dnsdomain(data);	/*TTT*/
   next->address = NULL;
   next->port = port;
   next->mx = precedence;
@@ -3210,7 +3213,7 @@ switch (rc)
       {
       DEBUG(D_transport)
 	{
-	dns_scan dnss;
+	dns_scan dnss = {0};
 	for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
 	     rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
 	  if (rr->type == T_TLSA && rr->size > 3)
diff --git a/src/src/lookups/dnsdb.c b/src/src/lookups/dnsdb.c
index 6e2be8ae1..80cf2e62a 100644
--- a/src/src/lookups/dnsdb.c
+++ b/src/src/lookups/dnsdb.c
@@ -134,22 +134,19 @@ dnsdb_find(void * handle, const uschar * filename, const uschar * keystring,
  int length, uschar ** result, uschar ** errmsg, uint * do_cache,
  const uschar * opts)
 {
-int rc;
-int sep = 0;
 int defer_mode = PASS, dnssec_mode = PASS;
-int save_retrans = dns_retrans, save_retry =   dns_retry;
-int type;
-int failrc = FAIL;
+int save_retrans = dns_retrans, save_retry = dns_retry;
+int sep = 0, rc, type, failrc = FAIL;
 const uschar * outsep = CUS"\n", * outsep2 = NULL;
 uschar * equals, * domain, * found;
 
-dns_answer * dnsa = store_get_dns_answer();
-dns_scan dnss;
+dns_answer * dnsa = store_get_dns_answer();	/*TTT alloc */
+dns_scan dnss = {0};
 
 /* Because we're working in the search pool, we try to reclaim as much
 store as possible later, so we preallocate the result here */
 
-gstring * yield = string_get(256);
+gstring * yield = string_get_tainted(256, GET_TAINTED);	/*TTT alloc*/
 
 /* If the string starts with '>' we change the output separator.
 If it's followed by ';' or ',' we set the TXT output separator. */
@@ -406,6 +403,7 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 	  remain = rr->size - ++data_offset;
 	  if (chunk_len > remain)
 	    chunk_len = remain;
+	  /*TTT*/
 	  yield = string_catn(yield, US ((rr->data) + data_offset), chunk_len);
 	  data_offset += chunk_len;
 
@@ -430,6 +428,7 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 
 	  if (payload_length > MAX_TLSA_EXPANDED_SIZE)
 	    payload_length = MAX_TLSA_EXPANDED_SIZE;
+	  /*TTT*/
 	  yield = string_fmt_append(yield, "%d%c%d%c%d%c%.*H",
 				      usage, *outsep2,
 				      selector, *outsep2,
@@ -439,9 +438,15 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
       else   /* T_CNAME, T_CSA, T_MX, T_MXH, T_NS, T_PTR, T_SOA, T_SRV */
         {
         int priority, weight, port;
-        uschar s[264];
         uschar * p = US rr->data;
 
+	/* NB: this memory is released implicitly by the call
+	gstring_release_unused(yield) below. We used to use a stack-auto, but
+	I want to track taint wherever possible. The dnsa is not (yet)
+	allocated using taint-marked memory. */
+#define LCL_BUF_SIZE 264
+	uschar * buf = store_get(LCL_BUF_SIZE, GET_TAINTED);
+
 	switch (type)
 	  {
 	  case T_MXH:
@@ -453,8 +458,7 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 	  case T_MX:
 	    if (rr_bad_size(rr, sizeof(uint16_t))) continue;
 	    GETSHORT(priority, p);
-	    sprintf(CS s, "%d%c", priority, *outsep2);
-	    yield = string_cat(yield, s);
+	    yield = string_fmt_append(yield, "%d%c", priority, *outsep2);
 	    break;
 
 	  case T_SRV:
@@ -462,9 +466,8 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 	    GETSHORT(priority, p);
 	    GETSHORT(weight, p);
 	    GETSHORT(port, p);
-	    sprintf(CS s, "%d%c%d%c%d%c", priority, *outsep2,
+	    yield = string_fmt_append(yield, "%d%c%d%c%d%c", priority, *outsep2,
 			      weight, *outsep2, port, *outsep2);
-	    yield = string_cat(yield, s);
 	    break;
 
 	  case T_CSA:
@@ -481,20 +484,18 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 	    authorization status in the weight field. */
 
 	    if (Ustrcmp(found, domain) != 0)
-	      {
-	      if (port & 1) *s = 'X';         /* explicit authorization required */
-	      else *s = '?';                  /* no subdomain assertions here */
-	      }
+	      yield = string_catn(yield, port & 1
+				  ? US"X " /* explicit authorization required */
+				  : US"? ", /* no subdomain assertions here */
+				  2);
+	    else if (weight > 3)
+	      continue;
 	    else
-	      {
-	      if (weight < 2) *s = 'N';       /* not authorized */
-	      else if (weight == 2) *s = 'Y'; /* authorized */
-	      else if (weight == 3) *s = '?'; /* unauthorizable */
-	      else continue;                  /* invalid */
-	      }
-
-	    s[1] = ' ';
-	    yield = string_catn(yield, s, 2);
+	      yield = string_catn(yield,
+				    weight < 2  ? US"N "  /* not authorized */
+				  : weight == 2 ? US"Y "  /* authorized */
+				  :		  US"? ", /* unauthorizable */
+				  2);
 	    break;
 
 	  default:
@@ -503,8 +504,9 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 
         /* GETSHORT() has advanced the pointer to the target domain. */
 
+	/*TTT*/
         rc = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, p,
-          (DN_EXPAND_ARG4_TYPE)s, sizeof(s));
+          (DN_EXPAND_ARG4_TYPE)buf, LCL_BUF_SIZE);
 
         /* If an overlong response was received, the data will have been
         truncated and dn_expand may fail. */
@@ -515,7 +517,7 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
             "domain=%s", dns_text_type(type), domain);
           break;
           }
-        else yield = string_cat(yield, s);
+        else yield = string_cat(yield, buf);
 
 	if (type == T_SOA && outsep2 != NULL)
 	  {
@@ -524,15 +526,16 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 	  p += rc;
 	  yield = string_catn(yield, outsep2, 1);
 
+	  /*TTT*/
 	  rc = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, p,
-	    (DN_EXPAND_ARG4_TYPE)s, sizeof(s));
+	    (DN_EXPAND_ARG4_TYPE)buf, LCL_BUF_SIZE);
 	  if (rc < 0)
 	    {
 	    log_write(0, LOG_MAIN, "responsible-mailbox truncated: type=%s "
 	      "domain=%s", dns_text_type(type), domain);
 	    break;
 	    }
-	  else yield = string_cat(yield, s);
+	  else yield = string_cat(yield, buf);
 
 	  p += rc;
 	  if (!rr_bad_increment(rr, p, 5 * sizeof(uint32_t)))
@@ -540,12 +543,12 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 	    GETLONG(serial, p); GETLONG(refresh, p);
 	    GETLONG(retry,  p); GETLONG(expire,  p); GETLONG(minimum, p);
 	    }
-	  sprintf(CS s, "%c%lu%c%lu%c%lu%c%lu%c%lu",
+	  yield = string_fmt_append(yield, "%c%lu%c%lu%c%lu%c%lu%c%lu",
 	    *outsep2, serial, *outsep2, refresh,
 	    *outsep2, retry,  *outsep2, expire,  *outsep2, minimum);
-	  yield = string_cat(yield, s);
 	  }
         }
+#undef LCL_BUF_SIZE
       }    /* Loop for list of returned records */
 
            /* Loop for set of A-lookup types */
@@ -564,7 +567,7 @@ dns_retrans = save_retrans;
 dns_retry = save_retry;
 dns_init(FALSE, FALSE, FALSE);	/* clear the dnssec bit for getaddrbyname */
 
-if (!yield || !yield->ptr)
+if (gstring_length(yield) == 0)
   rc = failrc;
 else
   {
diff --git a/src/src/miscmods/dkim.c b/src/src/miscmods/dkim.c
index ccb649ca1..4bd87bbf9 100644
--- a/src/src/miscmods/dkim.c
+++ b/src/src/miscmods/dkim.c
@@ -81,9 +81,9 @@ static uschar *
 dkim_exim_query_dns_txt(const uschar * name)
 {
 dns_answer * dnsa = store_get_dns_answer();
-dns_scan dnss;
+dns_scan dnss = {0};
 rmark reset_point = store_mark();
-gstring * g = string_get_tainted(256, GET_TAINTED);
+gstring * g = string_get_tainted(256, GET_TAINTED);	/*TTT alloc*/
 
 lookup_dnssec_authenticated = NULL;
 if (dns_lookup(dnsa, name, T_TXT, NULL) != DNS_SUCCEED)
@@ -100,6 +100,7 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
       {
       uschar len = rr->data[rr_offset++];
 
+      /*TTT*/
       g = string_catn(g, US(rr->data + rr_offset), len);
       if (g->ptr >= PDKIM_DNS_TXT_MAX_RECLEN)
 	goto bad;
diff --git a/src/src/miscmods/dmarc_common.c b/src/src/miscmods/dmarc_common.c
index d6e27c892..1669d8400 100644
--- a/src/src/miscmods/dmarc_common.c
+++ b/src/src/miscmods/dmarc_common.c
@@ -228,7 +228,7 @@ const uschar *
 dmarc_dns_lookup(const uschar * dom)
 {
 dns_answer * dnsa = store_get_dns_answer();
-dns_scan dnss;
+dns_scan dnss = {0};
 const uschar * res = NULL;
 
 expand_level++;
@@ -256,7 +256,7 @@ if (dns_lookup(dnsa, string_sprintf("_dmarc.%s", dom), T_TXT, NULL)
     if (  rr->type == T_TXT && len > 9
        && Ustrncmp(rdata, "v=DMARC1;", 9) == 0)
       if (!res)
-	res = string_copyn_taint(rdata, len, GET_TAINTED);
+	res = string_copyn_taint(rdata, len, GET_TAINTED);	/*XXX*/
       else
 /* RFC 7489 6.6.3 step 5: multiple records are treated as no record */
 	{
diff --git a/src/src/miscmods/spf.c b/src/src/miscmods/spf.c
index 4febe7f4e..d97995d56 100644
--- a/src/src/miscmods/spf.c
+++ b/src/src/miscmods/spf.c
@@ -67,7 +67,7 @@ SPF_dns_exim_lookup(SPF_dns_server_t *spf_dns_server,
   const char *domain, ns_type rr_type, int should_cache)
 {
 dns_answer * dnsa = store_get_dns_answer();
-dns_scan dnss;
+dns_scan dnss = {0};
 SPF_dns_rr_t * spfrr;
 unsigned found = 0;
 
@@ -134,9 +134,16 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
 	s += 2;	/* skip the MX precedence field */
       case T_PTR:
 	{
-	uschar * buf = store_malloc(256);
-	(void)dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, s,
-	  (DN_EXPAND_ARG4_TYPE)buf, 256);
+	/* We lose taint-tracking here, really just assuming the data
+	given to the spf library will never leak back out.  Not sure if
+	the lib assumes it can free this (it does for srr.rr) - meaning we
+	cannot use pool store. The use of malloc also for T_TXT implies so. */
+
+	uschar * buf = store_malloc(256);	/*TTT alloc*/
+	/*TTT*/
+	if (dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, s,
+	    (DN_EXPAND_ARG4_TYPE)buf, 256) < 0)
+	  continue;
 	s = buf;
 	break;
 	}
@@ -160,12 +167,13 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
 	  if (  !(chunk_len = s[offset++])
 	     || rr->size < offset + chunk_len	/* ignore bogus size chunks */
 	     ) break;
-	  g = string_catn(g, s+offset , chunk_len);
+	  g = string_catn(g, s+offset , chunk_len);	/*TTT*/
 	  }
 	if (!g)
 	  continue;
+	s = string_copy_malloc(string_from_gstring(g));	/*TTT*/
+	gstring_reset(g);
 	gstring_release_unused(g);
-	s = string_copy_malloc(string_from_gstring(g));
 	DEBUG(D_receive) debug_printf_indent("SPF_dns_exim_lookup '%s'\n", s);
 	break;
 	}
@@ -174,7 +182,7 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
       case T_AAAA:
       default:
 	{
-	uschar * buf = store_malloc(dnsa->answerlen + 1);
+	uschar * buf = store_malloc(dnsa->answerlen + 1);	/*TTT alloc*/
 	s = memcpy(buf, s, dnsa->answerlen + 1);
 	break;
 	}
diff --git a/src/src/store.c b/src/src/store.c
index 4daa92512..e265acd4c 100644
--- a/src/src/store.c
+++ b/src/src/store.c
@@ -1217,7 +1217,7 @@ return yield;
 }
 
 void *
-store_malloc_3(size_t size, const char *func, int linenumber)
+store_malloc_3(size_t size, const char * func, int linenumber)
 {
 if (n_nonpool_blocks++ > max_nonpool_blocks)
   max_nonpool_blocks = n_nonpool_blocks;
diff --git a/src/src/string.c b/src/src/string.c
index 472008382..4e122a97b 100644
--- a/src/src/string.c
+++ b/src/src/string.c
@@ -618,9 +618,10 @@ Returns:    copy of string in new store, de-escaped
 */
 
 uschar *
-string_copy_dnsdomain(uschar * s)
+string_copy_dnsdomain(const uschar * s)
 {
 uschar * yield;
+/*TTT*/
 uschar * ss = yield = store_get(Ustrlen(s) + 1, GET_TAINTED);	/* always treat as tainted */
 
 while (*s)
diff --git a/src/src/structs.h b/src/src/structs.h
index a6261afcb..22a4bba90 100644
--- a/src/src/structs.h
+++ b/src/src/structs.h
@@ -760,11 +760,11 @@ typedef struct search_cache {
 uncompressed, but the data pointer is into the raw data. */
 
 typedef struct {
-  uschar        name[DNS_MAXNAME];      /* domain name */
-  int           type;                   /* record type */
-  unsigned short ttl;		        /* time-to-live, seconds */
-  int           size;                   /* size of data */
-  const uschar *data;                   /* pointer to data */
+  uschar       *name;			/* domain name */
+  int           type;			/* record type */
+  unsigned short ttl;			/* time-to-live, seconds */
+  int           size;			/* size of data */
+  const uschar *data;			/* pointer to data */
 } dns_record;
 
 /* Structure for holding the result of a DNS query.  A touch over
@@ -826,6 +826,9 @@ typedef struct {
 
 #ifdef SUPPORT_DANE
   BOOL			dane:1;		/* TLSA says connection must do dane */
+  /*TTT alloc*/
+  /* Strictly, this should use tainted mem. Also, is rather large - 64k - so
+  perhaps should be allocated only when needed (for DANE)? */
   dns_answer		tlsa_dnsa;	/* strictly, this should use tainted mem */
 #endif
 } smtp_connect_args;
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index b78c33fcb..1610387d9 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -3397,7 +3397,7 @@ after verification is done.*/
 static BOOL
 dane_tlsa_load(exim_gnutls_state_st * state, const dns_answer * dnsa)
 {
-dns_scan dnss;
+dns_scan dnss = {0};
 int i;
 const char **	dane_data;
 int *		dane_data_len;
@@ -3416,7 +3416,7 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
     ) if (rr->type == T_TLSA && rr->size > 3)
   {
   const uschar * p = rr->data;
-/*XXX need somehow to mark rr and its data as tainted.  Doues this mean copying it? */
+/*XXX need somehow to mark rr and its data as tainted.  Doues this mean copying it? *//*TTT*/
   uint8_t usage = p[0], sel = p[1], type = p[2];
 
   DEBUG(D_tls)
@@ -3438,7 +3438,7 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
     }
 
   tls_out.tlsa_usage |= 1<<usage;
-  dane_data[i] = CS p;
+  dane_data[i] = CS p;			/*TTT*/
   dane_data_len[i++] = rr->size;
   }
 
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index eea1f15d3..40acee7a3 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -3940,7 +3940,7 @@ static int
 dane_tlsa_load(SSL * ssl, const host_item * host, const dns_answer * dnsa,
   uschar ** errstr)
 {
-dns_scan dnss;
+dns_scan dnss = {0};
 const char * hostnames[2] = { CS host->name, NULL };
 int found = 0;
 
@@ -3972,6 +3972,7 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
     }
 
   found++;
+  /*TTT*/
   switch (DANESSL_add_tlsa(ssl, usage, selector, mdname, p, rr->size - 3))
     {
     default:
diff --git a/src/src/tls.c b/src/src/tls.c
index dd82c7156..041cca5e9 100644
--- a/src/src/tls.c
+++ b/src/src/tls.c
@@ -879,7 +879,10 @@ tls_client_adjunct_start(host_item * host, client_conn_ctx * cctx,
 {
 union sockaddr_46 interface_sock;
 EXIM_SOCKLEN_T size = sizeof(interface_sock);
+
+/* Large (64k if DANE supported) */
 smtp_connect_args conn_args = {.host = host };
+
 tls_support tls_dummy = { .sni = NULL };
 uschar * errstr;
 
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index 7160f0616..1f2161ae2 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -4045,8 +4045,11 @@ int yield = OK;
 int save_errno;
 int rc;
 
-uschar *message = NULL;
-smtp_context * sx = store_get(sizeof(*sx), GET_TAINTED);	/* tainted, for the data buffers */
+uschar * message = NULL;
+
+/* Large (12k, 66k if DANE supported).  Tainted, for the data buffers */
+smtp_context * sx = store_get(sizeof(*sx), GET_TAINTED);
+
 BOOL pass_message = FALSE;
 #ifndef DISABLE_ESMTP_LIMITS
 BOOL mail_limit = FALSE;
@@ -5346,7 +5349,7 @@ smtp_transport_closedown(transport_instance * tblock)
 {
 const smtp_transport_options_block * ob = tblock->drinst.options_block;
 client_conn_ctx cctx;
-smtp_context sx = {0};
+smtp_context sx = {0};		/* Large (12k, 66k if DANE supported) */
 uschar buffer[256];
 uschar inbuffer[4096];
 uschar outbuffer[16];
@@ -5381,7 +5384,7 @@ int
 smtp_write_atrn(address_item * addr, cut_t * cutp)
 {
 const smtp_transport_options_block * ob = addr->transport->drinst.options_block;
-smtp_context sx = {0};
+smtp_context sx = {0};		/* Large (12k, 66k if DANE supported */
 uschar buffer[256];
 uschar inbuffer[4096];
 uschar outbuffer[256];
diff --git a/src/src/transports/smtp.h b/src/src/transports/smtp.h
index 49e3c8b96..506d6094a 100644
--- a/src/src/transports/smtp.h
+++ b/src/src/transports/smtp.h
@@ -144,7 +144,7 @@ typedef struct {
   const uschar *	from_addr;
   address_item *	addrlist;
 
-  smtp_connect_args	conn_args;
+  smtp_connect_args	conn_args;	/* Large (if DANE supported) */
   int			port;
 
   BOOL verify:1;
diff --git a/src/src/transports/smtp_socks.c b/src/src/transports/smtp_socks.c
index 436674e9b..c17b808e9 100644
--- a/src/src/transports/smtp_socks.c
+++ b/src/src/transports/smtp_socks.c
@@ -258,6 +258,8 @@ for(;;)
   {
   int idx;
   host_item proxy;
+
+  /* Large (64k if DANE supported) */
   smtp_connect_args proxy_sc = {.sock = -1};
 
   if ((idx = socks_get_proxy(proxies, nproxies)) < 0)
diff --git a/src/src/verify.c b/src/src/verify.c
index 18c53c1e8..6696ec4d9 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -16,7 +16,7 @@ caching was contributed by Kevin Fleming (but I hacked it around a bit). */
 
 #define CUTTHROUGH_CMD_TIMEOUT  30	/* timeout for cutthrough-routing calls */
 #define CUTTHROUGH_DATA_TIMEOUT 60	/* timeout for cutthrough-routing calls */
-static smtp_context ctctx;
+static smtp_context ctctx;		/* Large (12k, 66k if DANE supported */
 uschar ctbuffer[8192];
 
 
@@ -703,7 +703,8 @@ coding means skipping this whole loop and doing the append separately.  */
       log_write(0, LOG_MAIN|LOG_PANIC, "<%s>: %s", addr->address,
         addr->message);
 
-    if (!sx) sx = store_get(sizeof(*sx), GET_TAINTED);	/* tainted buffers */
+    /* Large (12k, 66k if DANE supported.  Tainted, for the receive buffers */
+    if (!sx) sx = store_get(sizeof(*sx), GET_TAINTED);
     memset(sx, 0, sizeof(*sx));
 
     sx->addrlist = sx->first_addr = addr;
@@ -1417,7 +1418,7 @@ Used for
 static uschar
 cutthrough_response(client_conn_ctx * cctx, char expect, uschar ** copy, int timeout)
 {
-smtp_context sx = {0};
+smtp_context sx = {0};		/* Large (12k, 66k if DANE supported) */
 uschar inbuffer[4096];
 uschar responsebuffer[4096];
 

commit 6b77712fcc9e4daf673751f0302c1ae003ed7dce
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Dec 7 13:17:41 2025 +0000

    DMARC: fix retval for module init

diff --git a/src/src/miscmods/dmarc.c b/src/src/miscmods/dmarc.c
index 8d840ae84..49ad795c2 100644
--- a/src/src/miscmods/dmarc.c
+++ b/src/src/miscmods/dmarc.c
@@ -63,9 +63,9 @@ extern uschar * dmarc_history_file;	/* File to store dmarc results */
 extern uschar * dmarc_tld_file;		/* Mozilla TLDs text file */
 
 
-void
+BOOL
 dmarc_local_init(void)
-{}
+{ return TRUE; }
 
 gstring *
 dmarc_version_report(gstring * g)
diff --git a/src/src/miscmods/dmarc_native.c b/src/src/miscmods/dmarc_native.c
index 02eb5dbd3..084adab28 100644
--- a/src/src/miscmods/dmarc_native.c
+++ b/src/src/miscmods/dmarc_native.c
@@ -48,6 +48,7 @@ if (!dmarc_regex_ri)
   dmarc_regex_ri =  regex_must_compile(US "^\\d{1,10}$", MCS_CACHEABLE, FALSE);
 if (!dmarc_regex_fo)
   dmarc_regex_fo =  regex_must_compile(US "^[01ds]$",    MCS_CACHEABLE, FALSE);
+return TRUE;
 }
 
 

commit 01c6fb5f07c0d7555e45ab0409dd8e551359dc2c
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Dec 7 14:16:30 2025 +0000

    tidying

diff --git a/src/src/exim.c b/src/src/exim.c
index 32ca1e1d2..d35168d60 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -988,13 +988,13 @@ has the effect of collapsing source routes.
 Arguments:
   s            the address string
   flags        flag bits for verify_address()
-  exit_value   to be set for failures
+  ret	       exit value so far
 
-Returns:       nothing
+Returns:       updated exit value
 */
 
-static void
-test_address(const uschar * s, int flags, int * exit_value)
+static int
+test_address(const uschar * s, int flags, int ret)
 {
 int start, end, domain;
 uschar * parse_error = NULL;
@@ -1003,16 +1003,17 @@ const uschar * address = parse_extract_address(s, &parse_error,
 if (!address)
   {
   fprintf(stdout, "syntax error: %s\n", parse_error);
-  *exit_value = 2;
+  ret = 2;
   }
 else
   {
   /* NB we lose stdio buffering here */
   int rc = verify_address(deliver_make_addr(address,TRUE), fileno(stdout),
     flags, -1, -1, -1, NULL, NULL, NULL);
-  if (rc == FAIL) *exit_value = 2;
-  else if (rc == DEFER && *exit_value == 0) *exit_value = 1;
+  if (rc == FAIL) ret = 2;
+  else if (rc == DEFER && ret == EXIT_SUCCESS) ret = EXIT_FAILURE;
   }
+return ret;
 }
 
 
@@ -5302,8 +5303,7 @@ stdin. Set debug_level to at least D_v to get full output for address testing.
 
 if (verify_address_mode || f.address_test_mode)
   {
-  int exit_value = 0;
-  int flags = vopt_qualify;
+  int exit_value = EXIT_SUCCESS, flags = vopt_qualify;
 
   if (verify_address_mode)
     {
@@ -5320,33 +5320,35 @@ if (verify_address_mode || f.address_test_mode)
     DEBUG(D_verify) debug_print_ids(US"Address testing:");
     }
 
-  if (recipients_arg < argc)
+  if (recipients_arg < argc)			/* addresses on cmdline */
     while (recipients_arg < argc)
       {
       /* Supplied addresses are tainted since they come from a user */
       uschar * s = string_copy_taint(
-	exim_str_fail_toolong(argv[recipients_arg++], EXIM_DISPLAYMAIL_MAX, "address verification"),
+	exim_str_fail_toolong(argv[recipients_arg++], EXIM_DISPLAYMAIL_MAX,
+			      "address verification"),
 	GET_TAINTED);
       while (*s)
         {
         BOOL finished = FALSE;
         uschar * ss = parse_find_address_end(s, FALSE);
         if (*ss == ',') *ss = 0; else finished = TRUE;
-        test_address(s, flags, &exit_value);
+        exit_value = test_address(s, flags, exit_value);
         s = ss;
         if (!finished)
           while (*++s == ',' || isspace(*s)) ;
         }
       }
 
-  else for (;;)
+  else for (;;)					/* addresses from stdin */
     {
     const uschar * s = get_stdinput(NULL, NULL);
     if (!s) break;
-    test_address(string_copy_taint(
-	exim_str_fail_toolong(s, EXIM_DISPLAYMAIL_MAX, "address verification (stdin)"),
+    exit_value = test_address(string_copy_taint(
+	exim_str_fail_toolong(s, EXIM_DISPLAYMAIL_MAX,
+			      "address verification (stdin)"),
 	GET_TAINTED),
-      flags, &exit_value);
+      flags, exit_value);
     }
 
   route_tidyup();
diff --git a/src/src/lookups/psl.c b/src/src/lookups/psl.c
index 9171192a3..3b2af489e 100644
--- a/src/src/lookups/psl.c
+++ b/src/src/lookups/psl.c
@@ -46,7 +46,7 @@ psl_gen_find(void * handle, const uschar * keystring,
   int length, uschar ** result, uschar ** errmsg, BOOL is_regdom)
 {
 uschar rulebuf[128], * res = NULL;
-const uschar * s, * k, * kmatch;
+const uschar * s, * k, * kmatch = NULL;
 unsigned res_label_cnt = 0, nlabels;
 BOOL key_utf8;
 
@@ -138,7 +138,7 @@ while ((s = US fgets(CS rulebuf, sizeof(rulebuf), handle)))
       kmatch = k;
       }
 
-    nonmatch:
+    nonmatch: ;
     }
   }
     /* kmatch is the key trail after the regdom. label */
diff --git a/src/src/miscmods/dmarc_native.c b/src/src/miscmods/dmarc_native.c
index 084adab28..a3434a146 100644
--- a/src/src/miscmods/dmarc_native.c
+++ b/src/src/miscmods/dmarc_native.c
@@ -359,7 +359,7 @@ if (!dmarc_abort && !sender_host_authenticated)
     .ri =	US"86400",
     };
 
-  int sr = SPF_RESULT_INVALID, spf_origin;
+  int sr = SPF_RESULT_INVALID /*, spf_origin*/;
   uschar * spf_human_readable = NULL, * spf_sender_domain = NULL;
   unsigned dkim_sig_count = 0;
   gstring * dkim_history_buffer = NULL;
@@ -525,7 +525,7 @@ if (!dmarc_abort && !sender_host_authenticated)
 	}
       spf_result = DMARC_POLICY_SPF_OUTCOME_NONE;
       dmarc_spf_ares_result = ARES_RESULT_UNKNOWN;
-      spf_origin = DMARC_POLICY_SPF_ORIGIN_HELO;
+      /* spf_origin = DMARC_POLICY_SPF_ORIGIN_HELO; */
       spf_human_readable = US"";
       }
     else
@@ -544,7 +544,7 @@ if (!dmarc_abort && !sender_host_authenticated)
 			      sr == SPF_RESULT_PERMERROR ? ARES_RESULT_PERMERROR :
 			      ARES_RESULT_UNKNOWN;
       /*XXX hmm, spf_origin never used? */
-      spf_origin = DMARC_POLICY_SPF_ORIGIN_MAILFROM;
+      /* spf_origin = DMARC_POLICY_SPF_ORIGIN_MAILFROM; */
       DEBUG(D_receive)
 	debug_printf_indent("DMARC: using SPF sender domain = %s\n",
 					  spf_sender_domain);
diff --git a/src/src/moan.c b/src/src/moan.c
index 01bf4f578..e5af5e511 100644
--- a/src/src/moan.c
+++ b/src/src/moan.c
@@ -666,7 +666,7 @@ void
 moan_smtp_batch(const uschar * cmd_buffer, const char * format, ...)
 {
 va_list ap;
-int yield = (receive_messagecount > 0)? 1 : 2;
+int yield = receive_messagecount > 0 ? EXIT_FAILURE : 2;
 
 DEBUG(D_any) debug_printf("Handling error in batched SMTP input\n");
 
diff --git a/src/src/string.c b/src/src/string.c
index 4e122a97b..6be0b7678 100644
--- a/src/src/string.c
+++ b/src/src/string.c
@@ -1835,9 +1835,9 @@ while (*fp)
 	else
 	  die_tainted(US"string_vformat", func, line);
 #endif
-
-    INSERT_STRING:              /* Come to from %D or %M above */
-
+#ifndef COMPILE_UTILITY
+    INSERT_STRING:              /* Come from %D or %M above */
+#endif
       {
       BOOL truncated = FALSE;
 

commit c038d63e56be080c1bf8043b2fc5c3aac4b57544
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Dec 8 11:37:18 2025 +0000

    DMARC: downgrade detailed result logging to debug

diff --git a/src/src/miscmods/dmarc.c b/src/src/miscmods/dmarc.c
index 49ad795c2..ce000ef8e 100644
--- a/src/src/miscmods/dmarc.c
+++ b/src/src/miscmods/dmarc.c
@@ -431,8 +431,8 @@ The EDITME provides a DMARC_API variable */
     dmarc_alignment_dkim =
       dmarc_dkim_alignment == DMARC_POLICY_DKIM_ALIGNMENT_PASS;
 
-    if (LOGGING(dmarc_verbose))
-      log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s "
+    DEBUG(D_receive)
+      debug_printf_indent("DMARC results: spf_domain=%s dmarc_domain=%s "
 			   "spf_align=%s dkim_align=%s enforcement='%s'",
 			   spf_sender_domain, dmarc_used_domain,
 			   dmarc_alignment_spf  ? "yes" : "no",
diff --git a/src/src/miscmods/dmarc_native.c b/src/src/miscmods/dmarc_native.c
index a3434a146..aa28df583 100644
--- a/src/src/miscmods/dmarc_native.c
+++ b/src/src/miscmods/dmarc_native.c
@@ -631,17 +631,14 @@ use the sp.  Otherwise use the p. */
 
   if (has_dmarc_record && !dmarc_abort)
     {
-    /* Log results. */
-
-    if (LOGGING(dmarc_verbose))
-      log_write(0, LOG_MAIN, "DMARC results: spf_domain=%s dmarc_domain=%s "
+    DEBUG(D_receive) 
+      debug_printf_indent("DMARC results: spf_domain=%s dmarc_domain=%s "
 			   "spf_align=%s dkim_align=%s enforcement='%s'",
 			   spf_sender_domain, dmarc_used_domain,
 			   dmarc_alignment_spf  ? "yes" : "no",
 			   dmarc_alignment_dkim ? "yes" : "no",
 			   dmarc_status_text);
 
-
     /* History file, for later aggregate reporting. */
 
     dmarc_pct = atoi(CCS dmarc_parsed.pct);

commit abef0531c0676680cab9b1fbd91da4482e35d45d
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Dec 8 16:02:57 2025 +0000

    DMARC: fix for empty env-from.  Bug 1994

diff --git a/src/src/expand.c b/src/src/expand.c
index 1b5980f5b..e222e8e75 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -719,6 +719,7 @@ static var_entry var_table[] = {
   { "spf_result",          vtype_module,	US"spf" },
   { "spf_result_guessed",  vtype_module,	US"spf" },
   { "spf_smtp_comment",    vtype_module,	US"spf" },
+  { "spf_used_domain",	   vtype_module,	US"spf" },
 #endif
   { "spool_directory",     vtype_stringptr,   &spool_directory },
   { "spool_inodes",        vtype_pinodes,     (void *)TRUE },
diff --git a/src/src/miscmods/dmarc.c b/src/src/miscmods/dmarc.c
index ce000ef8e..a18100c81 100644
--- a/src/src/miscmods/dmarc.c
+++ b/src/src/miscmods/dmarc.c
@@ -206,30 +206,19 @@ if (!dmarc_abort && !sender_host_authenticated)
   int sr = SPF_RESULT_INVALID, origin;
   uschar * spf_human_readable = NULL, * spf_sender_domain;
   gstring * dkim_history_buffer = NULL;
-  typedef const pdkim_signature * (*sigs_fn_t)(void);
+  typedef const pdkim_signature * (*dkim_sigs_fn_t)(void);
+  typedef int (*spf_fn_t)(uschar **);
 
-  /* Use the envelope sender domain for this part of DMARC */
+  if (dmarc_spf_mod_info)
+    sr = ((spf_fn_t *) dmarc_spf_mod_info->functions)[SPF_GET_RESULTS]
+							(&spf_human_readable);
 
-  spf_sender_domain = expand_string(US"$sender_address_domain");
-
-    {
-    typedef int (*fn_t)(uschar **);
-    if (dmarc_spf_mod_info)
-      sr = ((fn_t *) dmarc_spf_mod_info->functions)[SPF_GET_RESULTS]
-							  (&spf_human_readable);
-    }
+  spf_sender_domain = expand_string(US"$spf_used_domain");
 
   if (sr == SPF_RESULT_INVALID)
     {
-    /* No spf data means null envelope sender so generate a domain name
-    from the sender_helo_name  */
-
-    if (!spf_sender_domain || !*spf_sender_domain)
-      {
-      spf_sender_domain = sender_helo_name;
-      log_write(0, LOG_MAIN, "DMARC using synthesized SPF sender domain = %s\n",
-			     spf_sender_domain);
-      }
+    DEBUG(D_receive) debug_printf_indent("DMARC: spf result 'invalid'\n");
+
     dmarc_spf_result = DMARC_POLICY_SPF_OUTCOME_NONE;
     dmarc_spf_ares_result = ARES_RESULT_UNKNOWN;
     origin = DMARC_POLICY_SPF_ORIGIN_HELO;
@@ -237,11 +226,12 @@ if (!dmarc_abort && !sender_host_authenticated)
     }
   else
     {
-    dmarc_spf_result = sr == SPF_RESULT_NEUTRAL  ? DMARC_POLICY_SPF_OUTCOME_NONE :
-		       sr == SPF_RESULT_PASS     ? DMARC_POLICY_SPF_OUTCOME_PASS :
-		       sr == SPF_RESULT_FAIL     ? DMARC_POLICY_SPF_OUTCOME_FAIL :
-		       sr == SPF_RESULT_SOFTFAIL ? DMARC_POLICY_SPF_OUTCOME_TMPFAIL :
-		       DMARC_POLICY_SPF_OUTCOME_NONE;
+    dmarc_spf_result =
+      sr == SPF_RESULT_NEUTRAL  ? DMARC_POLICY_SPF_OUTCOME_NONE :
+      sr == SPF_RESULT_PASS     ? DMARC_POLICY_SPF_OUTCOME_PASS :
+      sr == SPF_RESULT_FAIL     ? DMARC_POLICY_SPF_OUTCOME_FAIL :
+      sr == SPF_RESULT_SOFTFAIL ? DMARC_POLICY_SPF_OUTCOME_TMPFAIL :
+      DMARC_POLICY_SPF_OUTCOME_NONE;
     dmarc_spf_ares_result = sr == SPF_RESULT_NEUTRAL   ? ARES_RESULT_NEUTRAL :
 			    sr == SPF_RESULT_PASS      ? ARES_RESULT_PASS :
 			    sr == SPF_RESULT_FAIL      ? ARES_RESULT_FAIL :
@@ -269,7 +259,7 @@ if (!dmarc_abort && !sender_host_authenticated)
   the opendmarc context, further building the DMARC reply. */
 
   for(const pdkim_signature * sig =
-	      (((sigs_fn_t *)dmarc_dkim_mod_info->functions)[DKIM_SIGS_LIST])();
+	(((dkim_sigs_fn_t *)dmarc_dkim_mod_info->functions)[DKIM_SIGS_LIST])();
       sig; sig = sig->next)
     {
     int dkim_result, dkim_ares_result, vs, ves;
diff --git a/src/src/miscmods/dmarc_native.c b/src/src/miscmods/dmarc_native.c
index aa28df583..f0c91868b 100644
--- a/src/src/miscmods/dmarc_native.c
+++ b/src/src/miscmods/dmarc_native.c
@@ -360,7 +360,8 @@ if (!dmarc_abort && !sender_host_authenticated)
     };
 
   int sr = SPF_RESULT_INVALID /*, spf_origin*/;
-  uschar * spf_human_readable = NULL, * spf_sender_domain = NULL;
+  uschar * spf_human_readable = NULL;
+  const uschar * spf_sender_domain = NULL;
   unsigned dkim_sig_count = 0;
   gstring * dkim_history_buffer = NULL;
   typedef const pdkim_signature * (*sigs_fn_t)(void);
@@ -502,30 +503,19 @@ if (!dmarc_abort && !sender_host_authenticated)
     int spf_result;
     typedef int (*fn_t)(uschar **);
 
-    /* Use the envelope sender domain for this part of DMARC */
-
-    spf_sender_domain = expand_string(US"$sender_address_domain");
-
     if (dmarc_spf_mod_info)
       sr = ((fn_t *) dmarc_spf_mod_info->functions)[SPF_GET_RESULTS]
 							  (&spf_human_readable);
 
+    spf_sender_domain = expand_string(CUS"$spf_used_domain");
+
     if (sr == SPF_RESULT_INVALID)
       {
-      /* No spf data means null envelope sender so generate a domain name
-      from the sender_helo_name  */
-
       DEBUG(D_receive) debug_printf_indent("DMARC: spf result 'invalid'\n");
 
-      if (!spf_sender_domain || !*spf_sender_domain)
-	{
-	spf_sender_domain = sender_helo_name;
-	log_write(0, LOG_MAIN, "DMARC using synthesized SPF sender domain = %s\n",
-			       spf_sender_domain);
-	}
       spf_result = DMARC_POLICY_SPF_OUTCOME_NONE;
       dmarc_spf_ares_result = ARES_RESULT_UNKNOWN;
-      /* spf_origin = DMARC_POLICY_SPF_ORIGIN_HELO; */
+      /* spf_origin = DMARC_POLICY_SPF_ORIGIN_HELO; not used, and we'd want to ask the spf impl really */
       spf_human_readable = US"";
       }
     else
diff --git a/src/src/miscmods/spf.c b/src/src/miscmods/spf.c
index d97995d56..a0cdb8dc6 100644
--- a/src/src/miscmods/spf.c
+++ b/src/src/miscmods/spf.c
@@ -42,7 +42,8 @@ uschar * spf_smtp_comment       = NULL;
 uschar * spf_smtp_comment_template
                     /* Used to be: "Please%_see%_http://www.open-spf.org/Why?id=%{S}&ip=%{C}&receiver=%{R}" */
 				= US"Please%_see%_http://www.open-spf.org/Why";
-BOOL    spf_result_guessed     = FALSE;
+BOOL    spf_result_guessed	= FALSE;
+const uschar * spf_used_domain	= NULL;
 
 
 
@@ -409,6 +410,9 @@ else
   spf_received           = US SPF_response_get_received_spf(spf_response);
   spf_result             = US SPF_strresult(SPF_response_result(spf_response));
   spf_smtp_comment       = US SPF_response_get_smtp_comment(spf_response);
+  spf_used_domain	 = sender_address && *sender_address
+			  ? expand_string(US"$sender_address_domain")
+			  : sender_helo_name;
 
   rc = SPF_response_result(spf_response);
 
@@ -595,6 +599,7 @@ static var_entry spf_variables[] = {
   { "spf_result",		vtype_stringptr,	&spf_result },
   { "spf_result_guessed",	vtype_bool,		&spf_result_guessed },
   { "spf_smtp_comment",		vtype_stringptr,	&spf_smtp_comment },
+  { "spf_used_domain",		vtype_stringptr,	&spf_used_domain },
 };
 
 misc_module_info spf_module_info =
diff --git a/src/src/miscmods/spf_perl.c b/src/src/miscmods/spf_perl.c
index 55741eef0..c30e0efe4 100644
--- a/src/src/miscmods/spf_perl.c
+++ b/src/src/miscmods/spf_perl.c
@@ -43,7 +43,8 @@ uschar * spf_smtp_comment       = NULL;
 uschar * spf_smtp_comment_template
                     /* Used to be: "Please%_see%_http://www.open-spf.org/Why?id=%{S}&ip=%{C}&receiver=%{R}" */
 				= US"Please%_see%_http://www.open-spf.org/Why";
-BOOL    spf_result_guessed     = FALSE;
+BOOL    spf_result_guessed	= FALSE;
+const uschar * spf_used_domain	= NULL;
 
 
 static const misc_module_info * spf_perl_mi = NULL;
@@ -196,6 +197,10 @@ else
     uschar * t = Ustrchr(s, ')');	/* grab a (comment) if there is one */
     if (t) spf_header_comment = string_copyn(s+1, t-s-1);
     }
+
+  spf_used_domain	 = sender_address && *sender_address
+			  ? expand_string(US"$sender_address_domain")
+			  : sender_helo_name;
   }
 
 res = match_isinlist(spf_result, &arglist,
@@ -344,6 +349,7 @@ static var_entry spf_variables[] = {
   { "spf_result",		vtype_stringptr,	&spf_result },
   { "spf_result_guessed",	vtype_bool,		&spf_result_guessed },
   { "spf_smtp_comment",		vtype_stringptr,	&spf_smtp_comment },
+  { "spf_used_domain",		vtype_stringptr,	&spf_used_domain },
 };
 
 misc_module_info spf_module_info =

commit b064cc0ef055fdcb91c971234b756e494f64e347
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Dec 9 10:59:13 2025 +0000

    DMARC: rework module API

diff --git a/src/src/acl.c b/src/src/acl.c
index b3c45668a..63478ca7d 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -3939,25 +3939,20 @@ for (; cb; cb = cb->next)
       /* See comment on ACLC_SPF wrt. coding issues */
       {
       misc_module_info * mi = misc_mod_find(US"dmarc", &log_message);
-      typedef const uschar * (*efn_t)(void);
-      const uschar * expanded_query;
+      typedef int (*pfn_t)(void);
+      typedef int (*mfn_t)(const uschar * const *);
 
       if (!mi)
-	{ rc = DEFER; break; }			/* shouldn't happen */
-
-      if (!f.dmarc_has_been_checked)		/* only once per message */
+	rc = DEFER;				/* shouldn't happen */
+      else
 	{
-	typedef int (*pfn_t)(void);
-	(void) (((pfn_t *) mi->functions)[DMARC_PROCESS]) ();
-	f.dmarc_has_been_checked = TRUE;
+	if (!f.dmarc_has_been_checked)		/* only once per message */
+	  {
+	  (void) (((pfn_t *) mi->functions)[DMARC_PROCESS]) ();
+	  f.dmarc_has_been_checked = TRUE;
+	  }
+	rc = (((mfn_t *) mi->functions)[DMARC_RESULT_INLIST]) (&arg);
 	}
-
-      /* used long way of dmarc_exim_expand_query() in case we need more
-      view into the process in the future. */
-
-      expanded_query = (((efn_t *) mi->functions)[DMARC_EXPAND_QUERY]) ();
-      rc = match_isinlist(expanded_query,
-			  &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
       }
       break;
 #endif
diff --git a/src/src/globals.c b/src/src/globals.c
index 610c72909..6c8e8ec05 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -838,6 +838,7 @@ volatile sig_atomic_t had_command_sigterm = 0;
 volatile sig_atomic_t had_data_timeout    = 0;
 volatile sig_atomic_t had_data_sigint     = 0;
 const uschar *headers_charset  = US HEADERS_CHARSET;
+const uschar *header_from      = NULL;		/* mainly for dmarc */
 int     header_insert_maxlen   = 64 * 1024;
 header_line  *header_last      = NULL;
 header_line  *header_list      = NULL;
diff --git a/src/src/globals.h b/src/src/globals.h
index 1c133f5ea..977107f58 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -633,6 +633,7 @@ extern volatile sig_atomic_t had_command_timeout;   /* Alarm sighandler called *
 extern volatile sig_atomic_t had_command_sigterm;   /* TERM  sighandler called */
 extern volatile sig_atomic_t had_data_timeout;      /* Alarm sighandler called */
 extern volatile sig_atomic_t had_data_sigint;       /* TERM/INT  sighandler called */
+extern const uschar *header_from;      /* Text of the From: header */
 extern int     header_insert_maxlen;   /* Max for inserting headers */
 extern int     header_maxsize;         /* Max total length for header */
 extern int     header_line_maxsize;    /* Max for an individual line */
diff --git a/src/src/miscmods/dmarc.c b/src/src/miscmods/dmarc.c
index a18100c81..5a5ea7398 100644
--- a/src/src/miscmods/dmarc.c
+++ b/src/src/miscmods/dmarc.c
@@ -161,7 +161,7 @@ is locally generated and relying on fixups to add it.  Just skip
 the entire DMARC system if we can't find a From: header....or if
 there was a previous error.  */
 
-if (!dmarc_from_header)
+if (!header_from)
   {
   DEBUG(D_receive) debug_printf_indent("DMARC: no From: header\n");
   dmarc_abort = TRUE;
@@ -173,10 +173,10 @@ else if (!dmarc_abort)
   int dummy, domain;
 
   f.parse_allow_group = TRUE;
-  end_addr = parse_find_address_end(dmarc_from_header, FALSE);
+  end_addr = parse_find_address_end(header_from, FALSE);
   s = *end_addr
-      ? string_copyn(dmarc_from_header, end_addr - dmarc_from_header)
-      : dmarc_from_header;
+      ? string_copyn(header_from, end_addr - header_from)
+      : header_from;
   if ((dmarc_header_from_sender = parse_extract_address(s, &errormsg,
 			      &dummy, &dummy, &domain, FALSE)))
     dmarc_header_from_sender += domain;
@@ -193,7 +193,7 @@ else if (!dmarc_abort)
     {
     log_write(0, LOG_MAIN|LOG_PANIC,
 	      "failure to store header From: in DMARC: %s, header was '%s'",
-	      opendmarc_policy_status_to_str(libdm_status), dmarc_from_header);
+	      opendmarc_policy_status_to_str(libdm_status), header_from);
     dmarc_abort = TRUE;
     }
   }
@@ -335,7 +335,7 @@ The EDITME provides a DMARC_API variable */
       DEBUG(D_receive)
 	debug_printf_indent("DMARC skipping (%s), unsure what to do with %s",
 		      opendmarc_policy_status_to_str(libdm_status),
-		      dmarc_from_header);
+		      header_from);
       has_dmarc_record = FALSE;
       break;
     }
@@ -458,14 +458,17 @@ dmarc_exim_expand_defaults(void)
 return f.dmarc_disable_verify ?  US"off" : US"none";
 }
 
-/*API*/
-const uschar *
-dmarc_exim_expand_query(void)
+/*API
+Check result against list.  Return OK/FAIL/DEFER.
+*/
+int
+dmarc_result_inlist(const uschar * const * listp)
 {
-if (f.dmarc_disable_verify || !dmarc_pctx)
-  return dmarc_exim_expand_defaults();
-
-return dmarc_status;
+const uschar * res =
+  f.dmarc_disable_verify || !dmarc_pctx
+  ? dmarc_exim_expand_defaults()
+  : dmarc_status;
+return match_isinlist(res, listp, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
 }
 
 
diff --git a/src/src/miscmods/dmarc.h b/src/src/miscmods/dmarc.h
index 6f8c456dd..fe3138cf2 100644
--- a/src/src/miscmods/dmarc.h
+++ b/src/src/miscmods/dmarc.h
@@ -120,7 +120,6 @@
 extern BOOL dmarc_abort;
 extern uschar * dmarc_header_from_sender;
 extern uschar * dmarc_pass_fail;
-extern const uschar * dmarc_from_header;
 extern const misc_module_info * dmarc_dkim_mod_info;
 extern const misc_module_info * dmarc_spf_mod_info;
 extern int dmarc_spf_ares_result;
diff --git a/src/src/miscmods/dmarc_api.h b/src/src/miscmods/dmarc_api.h
index 54b1630a6..e99b45804 100644
--- a/src/src/miscmods/dmarc_api.h
+++ b/src/src/miscmods/dmarc_api.h
@@ -12,5 +12,4 @@
 /* Function table entry numbers */
 
 #define	DMARC_PROCESS		0
-#define DMARC_EXPAND_QUERY	1
-#define DMARC_STORE_FROMHDR	2
+#define DMARC_RESULT_INLIST	1
diff --git a/src/src/miscmods/dmarc_common.c b/src/src/miscmods/dmarc_common.c
index 1669d8400..35b832c59 100644
--- a/src/src/miscmods/dmarc_common.c
+++ b/src/src/miscmods/dmarc_common.c
@@ -15,7 +15,7 @@ extern BOOL		dmarc_local_init(void);
 extern void		dmarc_local_msg_init(void);
 extern gstring *	dmarc_version_report(gstring *);
 extern int		dmarc_process(void);
-extern const uschar *	dmarc_exim_expand_query(void);
+extern const uschar *	dmarc_result_inlist(const uschar * const *);
 
 
 /* Other modules needed for services */
@@ -26,7 +26,6 @@ const misc_module_info * dmarc_arc_mod_info;
 /* Working data */
 BOOL		dmarc_abort;
 uschar *	dmarc_pass_fail;	/* for authres */
-const uschar *	dmarc_from_header;
 uschar *	dmarc_header_from_sender;
 
 /* results */
@@ -96,7 +95,6 @@ dmarc_msg_init(void)
 multiple messages in one connection. */
 
 f.dmarc_has_been_checked = FALSE;
-dmarc_from_header	 = NULL;
 dmarc_header_from_sender = NULL;
 dmarc_spf_ares_result  = ARES_RESULT_UNDEFINED;
 dmarc_status       = US"none";
@@ -140,18 +138,6 @@ dmarc_domain_policy = dmarc_status = dmarc_status_text =
 }
 
 
-/* API: dmarc_store_data stores the header data so that subsequent dmarc_process
-can access the data.  Cleared by msg_init.
-Called after the entire message has been received, with the From: header. */
-
-static void
-dmarc_store_fromhdr(const uschar * hdr)
-{
-/* No debug output because would change every test debug output */
-dmarc_from_header = hdr;
-}
-
-
 /* Accept an error_block struct, initialize if empty, parse to the
 end, and append the two strings passed to it.  Used for adding
 variable amounts of value:pair data to the forensic emails. */
@@ -501,8 +487,7 @@ static optionlist dmarc_options[] = {
 
 static void * dmarc_functions[] = {
   [DMARC_PROCESS] =	(void *) dmarc_process,
-  [DMARC_EXPAND_QUERY] = (void *) dmarc_exim_expand_query,
-  [DMARC_STORE_FROMHDR] = (void *) dmarc_store_fromhdr,
+  [DMARC_RESULT_INLIST] = (void *) dmarc_result_inlist,
 };
 
 /* dmarc_forensic_sender is provided for visibility of the the option setting
diff --git a/src/src/miscmods/dmarc_native.c b/src/src/miscmods/dmarc_native.c
index f0c91868b..96628dee1 100644
--- a/src/src/miscmods/dmarc_native.c
+++ b/src/src/miscmods/dmarc_native.c
@@ -310,7 +310,7 @@ is locally generated and relying on fixups to add it.  Just skip
 the entire DMARC system if we can't find a From: header....or if
 there was a previous error.  */
 
-if (!dmarc_from_header)
+if (!header_from)
   {
   DEBUG(D_receive) debug_printf_indent("DMARC: no From: header\n");
   dmarc_abort = TRUE;
@@ -323,10 +323,10 @@ else
   int dummy, domain;
 
   f.parse_allow_group = TRUE;
-  end_addr = parse_find_address_end(dmarc_from_header, FALSE);
+  end_addr = parse_find_address_end(header_from, FALSE);
   s = *end_addr
-      ? string_copyn(dmarc_from_header, end_addr - dmarc_from_header)
-      : dmarc_from_header;
+      ? string_copyn(header_from, end_addr - header_from)
+      : header_from;
   if ((dmarc_header_from_sender = parse_extract_address(s, &errormsg,
 			      &dummy, &dummy, &domain, FALSE)))
     dmarc_header_from_sender += domain;
@@ -668,14 +668,15 @@ dmarc_exim_expand_defaults(void)
 return f.dmarc_disable_verify ?  US"off" : US"none";
 }
 
-/*API*/
-const uschar *
-dmarc_exim_expand_query(void)
+/*API
+Check result against list.  Return OK/FAIL/DEFER.
+*/
+int
+dmarc_result_inlist(const uschar * const * listp)
 {
-if (f.dmarc_disable_verify )            // || !dmarc_pctx)
-  return dmarc_exim_expand_defaults();
-
-return dmarc_status;
+const uschar * res =
+  f.dmarc_disable_verify ? dmarc_exim_expand_defaults() : dmarc_status;
+return match_isinlist(res, listp, 0, NULL, NULL, MCL_STRING, TRUE, NULL);
 }
 
 
diff --git a/src/src/receive.c b/src/src/receive.c
index 7827d0288..f1833c5a0 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -1732,47 +1732,36 @@ not. */
 BOOL
 receive_msg(BOOL extract_recip)
 {
-int  rc = FAIL;
-int  msg_size = 0;
-int  process_info_len = Ustrlen(process_info);
+int  rc = FAIL, msg_size = 0, process_info_len = Ustrlen(process_info);
+int  header_size = 256, had_zero = 0, prevlines_length = 0, ptr = 0;
+
 int  error_rc = error_handling == ERRORS_SENDER
 	? errors_sender_rc : EXIT_FAILURE;
-int  header_size = 256;
-int  had_zero = 0;
-int  prevlines_length = 0;
 const int id_resolution = BASE_62 == 62 && !host_number_string ? 1
   : BASE_62 != 62 && host_number_string ? 4
   : 2;
 
-int ptr = 0;
-
-BOOL contains_resent_headers = FALSE;
-BOOL extracted_ignored = FALSE;
-BOOL first_line_ended_crlf = TRUE_UNSET;
-BOOL smtp_yield = TRUE;
-BOOL yield = FALSE;
+BOOL contains_resent_headers = FALSE, extracted_ignored = FALSE;
+BOOL first_line_ended_crlf = TRUE_UNSET, smtp_yield = TRUE, yield = FALSE;
 
 BOOL resents_exist = FALSE;
-uschar *resent_prefix = US"";
-uschar *blackholed_by = NULL;
-uschar *blackhole_log_msg = US"";
+uschar * resent_prefix = US"", * blackholed_by = NULL;
+uschar * blackhole_log_msg = US"";
 enum {NOT_TRIED, TMP_REJ, PERM_REJ, ACCEPTED} cutthrough_done = NOT_TRIED;
 
 flock_t lock_data;
-error_block *bad_addresses = NULL;
+error_block * bad_addresses = NULL;
 
-uschar *frozen_by = NULL;
-uschar *queued_by = NULL;
+uschar * frozen_by = NULL, * queued_by = NULL;
 
-uschar *errmsg;
+uschar * errmsg;
 rmark rcvd_log_reset_point;
 gstring * g;
 struct stat statbuf;
 
 /* Final message to give to SMTP caller, and messages from ACLs */
 
-uschar *smtp_reply = NULL;
-uschar *user_msg, *log_msg;
+uschar * smtp_reply = NULL,  * user_msg, * log_msg;
 
 /* Working header pointers */
 
@@ -1808,6 +1797,7 @@ search_tidyup();
 /* Extracting the recipient list from an input file is incompatible with
 cutthrough delivery with the no-spool option.  It shouldn't be possible
 to set up the combination, but just in case kill any ongoing connection. */
+
 if (extract_recip || !smtp_input)
   cancel_cutthrough_connection(TRUE, US"not smtp input");
 
@@ -1815,7 +1805,8 @@ if (extract_recip || !smtp_input)
 header. Temporarily mark it as "old", i.e. not to be used. We keep header_last
 pointing to the end of the chain to make adding headers simple. */
 
-received_header = header_list = header_last = store_get(sizeof(header_line), GET_UNTAINTED);
+received_header = header_list = header_last =
+  store_get(sizeof(header_line), GET_UNTAINTED);
 header_list->next = NULL;
 header_list->type = htype_old;
 header_list->text = NULL;
@@ -1842,11 +1833,7 @@ received_count = 1;            /* For the one we will add */
 
 if (thismessage_size_limit <= 0) thismessage_size_limit = INT_MAX;
 
-/* While reading the message, the following counts are computed. */
-
-message_linecount = body_linecount = body_zerocount =
-  max_received_linelength = 0;
-
+header_from = NULL;
 #ifdef WITH_CONTENT_SCAN
 /* reset non-per-part mime variables */
 mime_is_coverletter    = 0;
@@ -1857,6 +1844,11 @@ mime_part_count        = -1;
 if (misc_mod_msg_init() != OK)
   goto CONN_GONE;
 
+/* While reading the message, the following counts are computed. */
+
+message_linecount = body_linecount = body_zerocount =
+  max_received_linelength = 0;
+
 /* In SMTP sessions we may receive several messages in one connection. Before
 each subsequent one, we wait for the clock to tick at the level of message-id
 granularity.
@@ -2170,7 +2162,7 @@ OVERSIZE:
   beyond it. If it turns out to be a real header, internal binary zeros will
   be squashed later. */
 
-  next->text[ptr] = 0;
+  next->text[ptr] = '\0';
   next->slen = ptr;
   store_release_above(next->text + ptr + 1);
 
@@ -2483,17 +2475,7 @@ for (header_line * h = header_list->next; h; h = h->next)
 
     case htype_from:
       h->type = htype_from;
-#ifdef EXIM_HAVE_DMARC
-      if (!is_resent && !f.dmarc_disable_verify)
-	{
-	misc_module_info * mi = misc_mod_findonly(US"dmarc");
-	if (mi)
-	  {
-	  typedef void (*fn_t)(const uschar *);
-	  (((fn_t *) mi->functions)[DMARC_STORE_FROMHDR]) (h->text);
-	  }
-	}
-#endif
+      if (!is_resent) header_from = h->text;
       if (!resents_exist || is_resent)
 	{
 	from_header = h;

commit 6b109f69cfc8804105f373dd22936d7b9edd66f9
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Dec 13 11:58:46 2025 +0000

    Debug: tweaks

diff --git a/src/src/drtables.c b/src/src/drtables.c
index 45b1ade16..bf1c3e530 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -287,7 +287,7 @@ void * dl;
 struct misc_module_info * mi;
 const char * errormsg;
 
-EARLY_DEBUG(D_any, "loading module %q\n", name);
+EARLY_DEBUG(D_any, "Loading module %q\n", name);
 if (!(dl = mod_open(name, US"miscmod", errstr)))
   {
   if (errstr) EARLY_DEBUG(D_any, " mod_open: %s\n", *errstr);
@@ -313,7 +313,7 @@ if (mi->dyn_magic != MISC_MODULE_MAGIC)
   return FALSE;
   }
 
-EARLY_DEBUG(D_lookup, "loaded %q\n", name);
+EARLY_DEBUG(D_lookup, "Loaded module %q\n", name);
 misc_mod_add(mi);
 return mi;
 }
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 37d9c6b85..7840fd0ac 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -3853,7 +3853,7 @@ else
     if ((errormsg = dlerror()))
       {
       log_write(0, LOG_MAIN|LOG_PANIC,
-		"%s does not appear to be a %s module (%s)\n", fname, class, errormsg);
+	"%s does not appear to be a %s module (%s)\n", fname, class, errormsg);
       dlclose(dl);
       break;
       }
@@ -3864,7 +3864,8 @@ else
 	store_pool = POOL_PERM;
 	add_driver_info(info_anchor, di, size_of_info);
 	store_pool = old_pool;
-	DEBUG(D_any) debug_printf("Loaded %s %s\n", d->driver_name, class);
+	DEBUG(D_any)
+	  debug_printf("Loaded module %q (%s)\n", d->driver_name, class);
 	closedir(dd);
 	goto found;
 	}

commit acdc767008460bf5e86a1bf39b43e8da7bd6e90d
Author: Andreas Metzler <ametzler@bebt.de>
Date:   Sat Dec 13 14:52:14 2025 +0000

    Build: linker invocation ordering
    
    Apparently some linkers are sensitive to positioning
    of library args in the commandline

diff --git a/src/scripts/Configure-Makefile b/src/scripts/Configure-Makefile
index 70dc886ab..b38fcd360 100755
--- a/src/scripts/Configure-Makefile
+++ b/src/scripts/Configure-Makefile
@@ -313,7 +313,13 @@ fi
 cp ../src/lookups/Makefile $look_mf_pre
 EGREP="$egrep" ../scripts/lookups-Makefile
 
-# make the Makefiles for routers, transports, auths and miscmods
+# Make the Makefiles for routers, transports, auths and miscmods
+#
+# Columns in the here-file:
+#  1		"class"		the subdir name to work in
+#  2		"classdef"	is a prefix on the driver name, used for pulling
+#  				construction data from Local/Makefile
+#  (remainder)	"names"		a list of driver names, in caps
 # An _ prefix on a name means the control in Local/Makefile is DISABLED_<name>
 #
 while read class classdef names
diff --git a/src/src/auths/Makefile b/src/src/auths/Makefile
index 6eab96a66..44ca8d8fb 100644
--- a/src/src/auths/Makefile
+++ b/src/src/auths/Makefile
@@ -31,8 +31,8 @@ auths.a:	$(OBJ)
 
 SO_FLAGS = -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) $(DLFLAGS)
 .c.so:;		@echo "$(CC) -shared $*.c"
-		$(FE)$(CC) $(SO_FLAGS) $(AUTH_$*_INCLUDE) $(AUTH_$*_LIBS) \
-			$*.c -o $@
+		$(FE)$(CC) $(SO_FLAGS) $(AUTH_$*_INCLUDE) $*.c $(AUTH_$*_LIBS) \
+			-o $@
 
 
 $(OBJ) $(MOD): $(HDRS)
diff --git a/src/src/lookups/Makefile b/src/src/lookups/Makefile
index 27ae3fe1e..560a5cba5 100644
--- a/src/src/lookups/Makefile
+++ b/src/src/lookups/Makefile
@@ -48,7 +48,9 @@ lookups.a:       avail_static_lookups.o $(OBJS)
 		 $(FE)$(CC) -c $(CFLAGS) $(INCLUDE) $*.c
 
 .c.so:;          @echo "$(CC) -shared $*.c"
-		 $(FE)$(CC) $(LOOKUP_$*_INCLUDE) $(LOOKUP_$*_LIBS) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) $(DLFLAGS) $*.c -o $@
+		 $(FE)$(CC) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) \
+		 	$(LOOKUP_$*_INCLUDE) $(INCLUDE) $(DLFLAGS) $*.c \
+		 	$(LOOKUP_$*_LIBS) -o $@
 
 cdb.o cdb.so:           $(HDRS) cdb.c
 dbmdb.o dbmdb.so:       $(HDRS) dbmdb.c
diff --git a/src/src/miscmods/Makefile b/src/src/miscmods/Makefile
index e92c34941..2d44059ba 100644
--- a/src/src/miscmods/Makefile
+++ b/src/src/miscmods/Makefile
@@ -27,9 +27,9 @@ miscmods.a:	$(OBJ)
 		$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) $*.c
 
 .c.so:;         @echo "$(CC) -shared $*.c"
-		$(FE)$(CC) $(SUPPORT_$*_INCLUDE) $(SUPPORT_$*_LIBS) \
-			-DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) \
-			$(DLFLAGS) $*.c -o $@
+		$(FE)$(CC) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) \
+			$(SUPPORT_$*_INCLUDE) $(INCLUDE) $(DLFLAGS) $*.c \
+			$(SUPPORT_$*_LIBS) -o $@
 
 # Note that the sources from pdkim/ are linked into the build.../miscmods/ dir
 # by scripts/Makelinks.
@@ -68,10 +68,10 @@ dkim.o:
 # Similarly, we want a single .so for the dynamic-load module
 dkim.so:
 	@echo "$(CC) -shared dkim.c dkim_transport.c pdkim.c signing.c"
-	$(FE)$(CC) -DDYNLOOKUP $(CFLAGS_DYNAMIC) -o $@ \
-		$(SUPPORT_$*_INCLUDE) $(SUPPORT_$*_LIBS) \
-		$(CFLAGS) $(INCLUDE) $(TLS_INCLUDE) $(DLFLAGS) \
-		dkim.c dkim_transport.c pdkim.c signing.c
+	$(FE)$(CC) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) \
+		$(SUPPORT_$*_INCLUDE) $(INCLUDE) $(TLS_INCLUDE) $(DLFLAGS) \
+		dkim.c dkim_transport.c pdkim.c signing.c $(SUPPORT_$*_LIBS)  \
+		-o $@
 
 # spf_perl is special; Configure-Makefile asks for it but we actually
 # build resulting in a file named spf.so because the module-loading
@@ -81,9 +81,9 @@ dkim.so:
 
 spf_perl.so:
 	@echo "$(CC) -shared $*.c"
-	$(FE)$(CC) $(SUPPORT_SPF_PERL_INCLUDE) $(SUPPORT_SPF_PERL_LIBS) \
-			-DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) \
-			$(DLFLAGS) spf_perl.c -o spf.so
+	$(FE)$(CC) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) \
+		$(SUPPORT_SPF_PERL_INCLUDE) $(INCLUDE) $(DLFLAGS) \
+		spf_perl.c $(SUPPORT_SPF_PERL_LIBS) -o spf.so
 
 # DMARC
 dmarc.o dmarc_native.o:
@@ -96,10 +96,9 @@ dmarc.o dmarc_native.o:
 # dmarc_native is special in the same way as spf_perl
 dmarc.so dmarc_native.so:
 	@echo "$(CC) -shared $*.c dmarc_common.c"
-	$(FE)$(CC) -DDYNLOOKUP $(CFLAGS_DYNAMIC) -o dmarc.so \
-		$(SUPPORT_$*_INCLUDE) $(SUPPORT_$*_LIBS) \
-		$(CFLAGS) $(INCLUDE) $(TLS_INCLUDE) $(DLFLAGS) \
-		$*.c dmarc_common.c
+	$(FE)$(CC) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) \
+		$(SUPPORT_$*_INCLUDE) $(INCLUDE) $(TLS_INCLUDE) $(DLFLAGS) \
+		$*.c dmarc_common.c $(SUPPORT_$*_LIBS) -o dmarc.so
 
 # Compile instructions for static perl.o for when EXIM_PERL is set
 # Dynamic is managed all via scripts/Configure-Makefile
diff --git a/src/src/routers/Makefile b/src/src/routers/Makefile
index 1b2953d40..32515fcf8 100644
--- a/src/src/routers/Makefile
+++ b/src/src/routers/Makefile
@@ -26,7 +26,8 @@ routers.a:       $(OBJ)
 		 $(FE)$(CC) -c $(CFLAGS) $(INCLUDE) $*.c
 
 .c.so:;          @echo "$(CC) -shared $*.c"
-		 $(FE)$(CC) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) $(DLFLAGS) $*.c -o $@
+		 $(FE)$(CC) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) \
+			$(DLFLAGS) $*.c -o $@
 
 
 

commit d9081efaca19d0bef80c0dbc3ffa045207f3171e
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Dec 13 23:52:08 2025 +0000

    Re-order module init in "-bV" support

diff --git a/src/src/exim.c b/src/src/exim.c
index d35168d60..693301899 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1334,11 +1334,13 @@ Currently they are output in misc_mod_add() */
   show_string(is_stdout, g);
   g = NULL;
 
+  /* Has to be before the lookups as the spf lookup calls into the spf module */
+  init_misc_mod_list();
+
   init_lookup_list();
   tree_walk(lookups_tree, lookup_version_report_cb, &g);
   show_string(is_stdout, g);
   g = NULL;
-  init_misc_mod_list();
 
 #ifdef WHITELIST_D_MACROS
   g = string_fmt_append(g, "WHITELIST_D_MACROS: %q\n", WHITELIST_D_MACROS);

commit 4a11617127599c2a40552f5f7e9bf0e863f10e8d
Merge: d9081efac d46a67277
Author: Heiko Schlittermann (HS12-RIPE) <hs@schlittermann.de>
Date:   Tue Dec 16 12:55:32 2025 +0100

    Merge branch 'exim-4.99+fixes' (EXIM-Security-2025-12-09.1, CVE-2025-67896)
    
    * exim-4.99+fixes:
      doc: update about CVE-2025-67896
      fix: release process docbook2texi
      EXIM-Security-2025-12-09.1
      doc: update Changelog (relates #7)
      Testsuite: add testing for sqlite db external usage
      return scan result into key parameter
      move to blobs for keys as well as data. refactor sqlite3_ calls to reduce duplication
      new: xtextencode
      CVE-2025-30232
      Squashed from fix/cve-2025-26794 (fixes CVE-26794)

diff --git a/src/src/hintsdb.h b/src/src/hintsdb.h
index d2d30969b..9a65d8b19 100644
--- a/src/src/hintsdb.h
+++ b/src/src/hintsdb.h
@@ -67,6 +67,7 @@ required by Exim's process transitions)?
 
 /* Include file ordering problem */
 extern void    debug_printf_indent(const char *, ...) PRINTF_FUNCTION(1,2);
+static inline BOOL is_tainted(const void *);
 
 
 #ifdef USE_SQLITE
@@ -102,15 +103,8 @@ the default is the NDBM interface (which seems to be a wrapper for GDBM) */
 #endif /* !USE_GDBM */
 
 
-
-
-
-
 /* Wrappers for open/close with debug tracing */
 
-extern void debug_printf_indent(const char *, ...);
-static inline BOOL is_tainted(const void *);
-
 static inline EXIM_DB *
 exim_dbopen(const uschar * name, const uschar * dirname, int flags,
   unsigned mode)
diff --git a/src/src/hintsdb/hints_sqlite.h b/src/src/hintsdb/hints_sqlite.h
index 40d50b8c0..dd88dd98b 100644
--- a/src/src/hintsdb/hints_sqlite.h
+++ b/src/src/hintsdb/hints_sqlite.h
@@ -17,7 +17,7 @@ backend provider. */
 /* Basic DB type */
 # define EXIM_DB sqlite3
 
-# define EXIM_CURSOR int
+# define EXIM_CURSOR sqlite3_stmt
 
 # /* The datum type used for queries */
 # define EXIM_DATUM blob
@@ -25,12 +25,6 @@ backend provider. */
 /* Some text for messages */
 # define EXIM_DBTYPE "sqlite3"
 
-/* Utility functionss */
-
-extern uschar *xtextencode(const uschar *, int);
-extern int xtextdecode(const uschar *, uschar**);
-
-
 /* Access functions */
 
 static inline BOOL
@@ -54,10 +48,12 @@ if ((ret = sqlite3_open_v2(CCS name, &dbp, sflags, NULL)) == SQLITE_OK)
   sqlite3_busy_timeout(dbp, 5000);
   if (flags & O_CREAT)
     ret = sqlite3_exec(dbp,
-	    "CREATE TABLE IF NOT EXISTS tbl (ky TEXT PRIMARY KEY, dat BLOB);",
+	    "CREATE TABLE IF NOT EXISTS tblblob (ky BLOB PRIMARY KEY, dat BLOB);",
 	    NULL, NULL, NULL);
   if (ret != SQLITE_OK)
     sqlite3_close(dbp);
+  /* in case we are migrating, drop the old table, return code not needed */
+  (void) sqlite3_exec(dbp, "DROP TABLE IF EXISTS tbl;", NULL, NULL, NULL);
   }
 else DEBUG(D_hints_lookup)
   debug_printf_indent("sqlite_open(flags 0x%x mode %04o) %s\n",
@@ -82,148 +78,143 @@ sqlite3_close(dbp);
 return NULL;
 }
 
-/* EXIM_DBGET - returns TRUE if successful, FALSE otherwise */
-/* note we alloc'n'copy - the caller need not do so */
-/* result has a NUL appended, but the length is as per the DB */
-
-static inline BOOL
-exim_dbget__(EXIM_DB * dbp, const uschar * s, EXIM_DATUM * res)
+static inline sqlite3_stmt *
+exim_sqlbind_blob(EXIM_DB * dbp, sqlite3_stmt *stmt, int *bindcolp, EXIM_DATUM *data )
 {
-sqlite3_stmt * statement;
-int ret;
-
-res->len = (size_t) -1;
-/* DEBUG(D_hints_lookup) debug_printf_indent("exim_dbget__(%s)\n", s); */
-if ((ret = sqlite3_prepare_v2(dbp, CCS s, -1, &statement, NULL)) != SQLITE_OK)
+if (data && stmt)
   {
-  DEBUG(D_hints_lookup)
-    debug_printf_indent("prepare fail: %s\n", sqlite3_errmsg(dbp));
-  return FALSE;
-  }
-if (sqlite3_step(statement) != SQLITE_ROW)
-  {
-  /* DEBUG(D_hints_lookup)
-    debug_printf_indent("step fail: %s\n", sqlite3_errmsg(dbp)); */
-  sqlite3_finalize(statement);
-  return FALSE;
-  }
-
-res->len = sqlite3_column_bytes(statement, 0);
-# ifdef COMPILE_UTILITY
-if (!(res->data = malloc(res->len +1)))
-  { sqlite3_finalize(statement); return FALSE; }
-# else
-res->data = store_get(res->len +1, GET_TAINTED);
+  if (SQLITE_OK != sqlite3_bind_blob(stmt, *bindcolp, data->data, data->len, SQLITE_STATIC))
+    {
+# ifdef SQL_DEBUG
+    fprintf(stderr, EXIM_DBTYPE " bind to value %d: %s\n", *bindcolp, sqlite3_errmsg(dbp));
 # endif
-memcpy(res->data, sqlite3_column_blob(statement, 0), res->len);
-res->data[res->len] = '\0';
-/* DEBUG(D_hints_lookup) debug_printf_indent("res %d bytes: '%.*s'\n",
-				  (int)res->len, (int)res->len, res->data); */
-sqlite3_finalize(statement);
-return TRUE;
+    sqlite3_finalize(stmt);
+    stmt = NULL;
+    }
+  else
+    {
+    (*bindcolp)++;
+    }
+  }
+return stmt;
 }
 
-static inline BOOL
-exim_dbget(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * res)
+/* We use a common prepare/bind mechanism with optional value binding */
+static inline sqlite3_stmt *
+exim_sqlprep(EXIM_DB * dbp, const char *query, EXIM_DATUM *key, EXIM_DATUM *data )
 {
-# define FMT "SELECT dat FROM tbl WHERE ky = '%s';"
-uschar * encoded_key, * qry;
-BOOL ret;
+sqlite3_stmt * stmt = NULL; /* don't make it static, as it depends on the dbp */
+int bindcol = 1;
 
-# ifdef COMPILE_UTILITY
-if (!(encoded_key = xtextencode(key->data, key->len)))
-  return FALSE;
-# else
-encoded_key = xtextencode(key->data, key->len);
+if (SQLITE_OK != sqlite3_prepare_v2(dbp, query, strlen(query), &stmt, NULL))
+  {
+# ifdef SQL_DEBUG
+  fprintf(stderr, EXIM_DBTYPE " prepare %s: %s\n", query, sqlite3_errmsg(dbp));
 # endif
-/* DEBUG(D_hints_lookup) debug_printf_indent("exim_dbget(k len %d '%s')\n",
-				  (int)key->len, encoded_key); */
+  goto DONE;
+  }
 
-# ifdef COMPILE_UTILITY
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent("prepared SQL: %s\n", sqlite3_sql(stmt));
+# endif
+
+stmt = exim_sqlbind_blob(dbp, stmt, &bindcol, key);
+stmt = exim_sqlbind_blob(dbp, stmt, &bindcol, data);
+
+# ifdef SQL_DEBUG
+if (stmt)
   {
-  int i = snprintf(NULL, 0, FMT, encoded_key) + 1;
-  if (!(qry = malloc(i)))
-    return FALSE;
-  snprintf(CS qry, i, FMT, encoded_key);
-  ret = exim_dbget__(dbp, qry, res);
-  free(qry);
+  DEBUG(D_hints_lookup) debug_printf_indent("expanded SQL: %s\n", sqlite3_expanded_sql(stmt));
   }
-free(encoded_key);
-# else
-qry = string_sprintf(FMT, encoded_key);
-ret = exim_dbget__(dbp, qry, res);
 # endif
 
-return ret;
-# undef FMT
-}
+DONE:
 
-/* Note that we return claiming a duplicate record for any error.
-It seem not uncommon to get a "database is locked" error.
+return stmt;
+}
 
-Keys are stored xtext-encoded (which is mostly readable, for plaintext).
-Values are stored in a BLOB type in the DB, for which the SQL interface
-is hex-encoded. */
-# define EXIM_DBPUTB_OK  0
-# define EXIM_DBPUTB_DUP (-1)
 
 static inline int
-exim_s_dbp(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * data, const uschar * alt)
+exim_sqlstep(EXIM_DB * dbp, sqlite3_stmt * stmt, EXIM_DATUM *res )
 {
-int hlen = data->len * 2, off = 0, res;
-# define FMT "INSERT OR %s INTO tbl (ky,dat) VALUES ('%s', X'%.*s');"
-uschar * encoded_key, * qry;
+int rv = SQLITE_MISUSE;
+
+switch ((rv = sqlite3_step(stmt)))
+  {
+    case SQLITE_DONE: /* might want to call sqlite3_reset(stmt); here */
+                      goto DONE;
+    case SQLITE_ROW:  if (!res)
+                      {
+                        /* allow for fetch but didn't want data (existence check ?) */
+                        goto DONE;
+                      }
+                      res->len = sqlite3_column_bytes(stmt, 0);
 # ifdef COMPILE_UTILITY
-uschar * hex = malloc(hlen+1), dummy[1];
-if (!hex) return EXIM_DBPUTB_DUP;	/* best we can do */
+                      res->data = malloc(res->len +1);
+                      if (! res->data ) goto DONE;
 # else
-uschar * hex = store_get(hlen+1, data->data);
+                      res->data = store_get(res->len +1, GET_TAINTED);
+# endif
+                      memcpy(res->data, sqlite3_column_blob(stmt, 0), res->len);
+                      res->data[res->len] = '\0';
+                      goto DONE;
+    default:
+# ifdef SQL_DEBUG
+                      fprintf(stderr, EXIM_DBTYPE " step: %s: %s\n", sqlite3_expanded_sql(stmt), sqlite3_errmsg(dbp));
 # endif
+                      goto DONE;
+  }
 
-/* Encode the value for the SQL API */
+DONE:
+return rv;
+}
 
-for (const uschar * s = data->data, * t = s + data->len; s < t; s++, off += 2)
-  sprintf(CS hex + off, "%02X", *s);
 
-# ifdef COMPILE_UTILITY
-if (!(encoded_key = xtextencode(key->data, key->len)))
-  return EXIM_DBPUTB_DUP;
-res = snprintf(CS dummy, 0, FMT, alt, encoded_key, hlen, hex) +1;
-if (!(qry = malloc(res))) return EXIM_DBPUTB_DUP;
-snprintf(CS qry, res, FMT, alt, encoded_key, hlen, hex);
-DEBUG(D_hints_lookup) debug_printf_indent("exim_s_dbp(%s)\n", qry);
-
-res = sqlite3_exec(dbp, CS qry, NULL, NULL, NULL);
-free(qry);
-free(encoded_key);
-free(hex);
+/* simplest case when updating a single row or fetching a single row */
+static inline int
+exim_sqlprep_step(EXIM_DB * dbp, const char *query, EXIM_DATUM *key, EXIM_DATUM *data, EXIM_DATUM *res )
+{
+int more = SQLITE_ERROR;
+sqlite3_stmt * stmt = NULL; /* don't make it static, as it depends on the dbp */
 
-# else
-encoded_key = xtextencode(key->data, key->len);
-qry = string_sprintf(FMT, alt, encoded_key, hlen, hex);
-/* DEBUG(D_hints_lookup) debug_printf_indent("exim_s_dbp(%s)\n", qry); */
+if ((stmt = exim_sqlprep(dbp, query, key, data )))
+  {
+    more = exim_sqlstep(dbp, stmt, res );
+    sqlite3_finalize(stmt);
+    stmt = NULL;
+  }
+return more;
+}
 
-res = sqlite3_exec(dbp, CS qry, NULL, NULL, NULL);
-/* DEBUG(D_hints_lookup) debug_printf_indent("exim_s_dbp res %d\n", res); */
-# endif
+/* EXIM_DBGET - returns the value associated with the key. */
+static inline BOOL
+exim_dbget(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * res)
+{
+const char query[] = "SELECT dat FROM tblblob WHERE ky = ?";
+return (exim_sqlprep_step(dbp, query, key, NULL, res ) == SQLITE_ROW);
+}
 
-# ifdef COMPILE_UTILITY
-if (res != SQLITE_OK)
-  DEBUG(D_hints_lookup)
-    debug_printf_indent("sqlite3_exec: %s\n", sqlite3_errmsg(dbp));
-# endif
 
-return res == SQLITE_OK ? EXIM_DBPUTB_OK : EXIM_DBPUTB_DUP;
-# undef FMT
+/* Note that we return claiming a duplicate record for any error.
+ * It seem not uncommon to get a "database is locked" error.
+*/
+# define EXIM_DBPUTB_OK  0
+# define EXIM_DBPUTB_DUP (-1)
+
+static inline int
+exim_s_dbp(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * data, const char * sql)
+{
+return (SQLITE_DONE == exim_sqlprep_step(dbp, sql, key, data, NULL )) ? EXIM_DBPUTB_OK : EXIM_DBPUTB_DUP;
 }
 
 /* EXIM_DBPUT - returns nothing useful, assumes replace mode */
-
 static inline int
 exim_dbput(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * data)
 {
-/* DEBUG(D_hints_lookup) debug_printf_indent("exim_dbput()\n"); */
-(void) exim_s_dbp(dbp, key, data, US"REPLACE");
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent(EXIM_DBTYPE " put: key:%.*W data:%.*W\n", key->len, key->data, data->len, data->data );
+# endif
+(void) exim_s_dbp(dbp, key, data, "INSERT OR REPLACE INTO tblblob (ky, dat) VALUES(?, ?)");
 return 0;
 }
 
@@ -234,110 +225,52 @@ return 0;
 static inline int
 exim_dbputb(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * data)
 {
-return exim_s_dbp(dbp, key, data, US"ABORT");
+return exim_s_dbp(dbp, key, data, "INSERT OR ABORT INTO tblblob (ky, dat) VALUES(?, ?)");
 }
 
 /* EXIM_DBDEL */
 static inline int
 exim_dbdel(EXIM_DB * dbp, EXIM_DATUM * key)
 {
-# define FMT "DELETE FROM tbl WHERE ky = '%s';"
-uschar * encoded_key, * qry;
-int res;
-
-# ifdef COMPILE_UTILITY
-if (!(encoded_key = xtextencode(key->data, key->len)))
-  return EXIM_DBPUTB_DUP;
-res = snprintf(NULL, 0, FMT, encoded_key) +1;		/* res includes nul */
-if (!(qry = malloc(res))) return SQLITE_NOMEM;
-snprintf(CS qry, res, FMT, encoded_key);
-res = sqlite3_exec(dbp, CS qry, NULL, NULL, NULL);
-free(qry);
-
-# else
-encoded_key = xtextencode(key->data, key->len);
-qry = string_sprintf(FMT, encoded_key);
-res = sqlite3_exec(dbp, CS qry, NULL, NULL, NULL);
-
-# endif
-
-return res == SQLITE_OK ? EXIM_DBPUTB_OK : EXIM_DBPUTB_DUP;
-# undef FMT
+const char query[] = "DELETE FROM tblblob WHERE ky = ?";
+return (SQLITE_DONE == exim_sqlprep_step(dbp, query, key, NULL, NULL )) ? 0 : -1;
 }
 
-
 /* EXIM_DBCREATE_CURSOR - initialize for scanning operation */
-/* Cursors are inefficiently emulated by repeating searches */
 
 static inline EXIM_CURSOR *
 exim_dbcreate_cursor(EXIM_DB * dbp)
 {
-# ifdef COMPILE_UTILITY
-EXIM_CURSOR * c = malloc(sizeof(int));
-if (!c) return NULL;
-# else
-EXIM_CURSOR * c = store_malloc(sizeof(int));
+EXIM_CURSOR * cursor;
+
+cursor = exim_sqlprep(dbp, "SELECT ky FROM tblblob ORDER BY ky", NULL, NULL );
+if (!cursor) return NULL;
+# ifdef SQL_DEBUG
+DEBUG(D_hints_lookup) debug_printf_indent("prepared query: %s\n", sqlite3_sql(cursor));
 # endif
-*c = 0;
-return c;
+
+return cursor;
 }
 
 /* EXIM_DBSCAN */
-/* Note that we return the (next) key, not the record value.
-We allocate memory for the return. */
-
+/* Note that we return the (next) key into the key parameter, not the res parameter. */
 static inline BOOL
-exim_dbscan(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * res, BOOL first,
-  EXIM_CURSOR * cursor)
+exim_dbscan(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * res /* unused */, BOOL first /*unused*/, EXIM_CURSOR * cursor)
 {
-# define FMT "SELECT ky FROM tbl ORDER BY ky LIMIT 1 OFFSET %d;"
-uschar * qry;
-EXIM_DATUM encoded_key;
-BOOL ret;
-
-# ifdef COMPILE_UTILITY
-int i = snprintf(NULL, 0, FMT, *cursor)+1;
-
-if (!(qry = malloc(i))) return FALSE;
-snprintf(CS qry, i, FMT, *cursor);
-DEBUG(D_hints_lookup) debug_printf_indent("exim_dbscan(%s)\n", qry);
-ret = exim_dbget__(dbp, qry, &encoded_key);
-free(qry);
-
-# else	/*!COMPILE_UTILITY*/
-qry = string_sprintf(FMT, *cursor);
-DEBUG(D_hints_lookup) debug_printf_indent("exim_dbscan(%s)\n", qry);
-ret = exim_dbget__(dbp, qry, &encoded_key);
-
-# endif	/*COMPILE_UTILITY*/
-
-DEBUG(D_hints_lookup)
-  debug_printf_indent("exim_dbscan ret %c\n", ret ? 'T':'F');
-
-if (ret)
-  {
-  key->len = xtextdecode(encoded_key.data, &key->data);
-  *cursor = *cursor + 1;
-  }
-return ret;
-# undef FMT
+return (exim_sqlstep(dbp, cursor, key )==SQLITE_ROW);
 }
 
 /* EXIM_DBDELETE_CURSOR - terminate scanning operation. */
 static inline void
 exim_dbdelete_cursor(EXIM_CURSOR * cursor)
 {
-# ifdef COMPILE_UTILITY
-free(cursor);
-# else
-store_free(cursor);
-# endif
+if(cursor)
+  sqlite3_finalize(cursor);
 }
 
-
 /* EXIM_DBCLOSE */
 static inline void
-exim_dbclose_multi__(EXIM_DB * dbp)
+exim_dbclose_multi__(EXIM_DB *dbp)
 {
 sqlite3_close(dbp);
 }
@@ -359,19 +292,19 @@ exim_dbclose_multi__(dbp);
 static inline uschar *
 exim_datum_data_get(EXIM_DATUM * dp)
 { return US dp->data; }
-static inline void
+
+static void
 exim_datum_data_set(EXIM_DATUM * dp, void * s)
 { dp->data = s; }
- 
-static inline unsigned
+
+static unsigned
 exim_datum_size_get(EXIM_DATUM * dp)
 { return dp->len; }
+
 static inline void
 exim_datum_size_set(EXIM_DATUM * dp, unsigned n)
 { dp->len = n; }
 
-
-
 static inline void
 exim_datum_init(EXIM_DATUM * dp)
 { dp->data = NULL; }			/* compiler quietening */
@@ -386,7 +319,7 @@ exim_datum_free(EXIM_DATUM * dp)
 
 # define EXIM_DB_RLIMIT	150
 
-
 /* End of hints_sqlite.h */
 /* vi: aw ai sw=2
+ *
 */

commit 227e467d611d9c5bcb942f2b2635e4466a8c7fd6
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Dec 15 13:43:19 2025 +0000

    Fast-ramp: do not notify daemon during high load-avg

diff --git a/src/src/queue.c b/src/src/queue.c
index 97238ba1a..46f7f3d57 100644
--- a/src/src/queue.c
+++ b/src/src/queue.c
@@ -1695,6 +1695,13 @@ int fd;
 
 DEBUG(D_queue_run) debug_printf("%s: %s\n", __FUNCTION__, msgid);
 
+if (  deliver_queue_load_max >= 0
+   && os_getloadavg() > deliver_queue_load_max)
+  {
+  DEBUG(D_queue_run) debug_printf(" - avoided due to load-avg\n");
+  return;
+  }
+
 buf[0] = NOTIFY_MSG_QRUN;
 memcpy(buf+1, msgid, MESSAGE_ID_LENGTH+1);
 Ustrcpy(buf+1+MESSAGE_ID_LENGTH+1, queue_name);

commit d09946f296565ebdd640b07d9bee0b30283d186d
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Dec 15 15:32:12 2025 +0000

    Fix DISABLE_ESMTP_LIMITS build

diff --git a/src/src/globals.c b/src/src/globals.c
index 6c8e8ec05..6d2b27227 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -648,6 +648,7 @@ unsigned continue_flags	       = 0;
 unsigned continue_limit_mail   = 0;
 unsigned continue_limit_rcpt   = 0;
 unsigned continue_limit_rcptdom= 0;
+#endif
 int	continue_fd	       = -1;
 uschar *continue_proxy_cipher  = NULL;
 BOOL    continue_proxy_dane    = FALSE;
@@ -662,7 +663,6 @@ const uschar *continue_transport = NULL;
 open_db *continue_retry_db     = NULL;
 open_db *continue_wait_db      = NULL;
 #endif
-#endif
 
 uschar *csa_status             = NULL;
 cut_t   cutthrough = {	/* All remaining items 0/FALSE/NULL */
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index 1f2161ae2..8ea3d882a 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -4051,9 +4051,7 @@ uschar * message = NULL;
 smtp_context * sx = store_get(sizeof(*sx), GET_TAINTED);
 
 BOOL pass_message = FALSE;
-#ifndef DISABLE_ESMTP_LIMITS
 BOOL mail_limit = FALSE;
-#endif
 #ifdef SUPPORT_DANE
 BOOL dane_held;
 #endif
@@ -4971,7 +4969,6 @@ DEBUG(D_transport)
     sx->send_rset, f.continue_more, yield, sx->first_addr ? "not " : "");
 
 if (sx->completed_addr && sx->ok && sx->send_quit)
-#ifndef DISABLE_ESMTP_LIMITS
   if (mail_limit = continue_sequence >= sx->max_mail)
     {
     DEBUG(D_transport)
@@ -4984,7 +4981,6 @@ if (sx->completed_addr && sx->ok && sx->send_quit)
     process. */
     }
   else
-#endif
     {
     BOOL send_rst;
     smtp_compare_t t_compare =
@@ -5299,7 +5295,6 @@ if (dane_held)
   }
 #endif
 
-#ifndef DISABLE_ESMTP_LIMITS
 if (mail_limit && sx->first_addr)
   {
   /* Reset the sequence count since we closed the connection.  This is flagged
@@ -5309,7 +5304,6 @@ if (mail_limit && sx->first_addr)
   continue_sequence = 1;			/* for consistency */
   goto REPEAT_CONN;				/* open a fresh connection */
   }
-#endif
 
 OUT:
   smtp_debug_cmd_report();

commit 5103de3423da39e0edcbd556ab2633c8d5c09c5f
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Dec 17 15:56:32 2025 +0000

    dscp module

diff --git a/src/scripts/Configure-Makefile b/src/scripts/Configure-Makefile
index b38fcd360..9b766d6ee 100755
--- a/src/scripts/Configure-Makefile
+++ b/src/scripts/Configure-Makefile
@@ -332,7 +332,7 @@ done <<-END
  routers    ROUTER	ACCEPT DNSLOOKUP IPLITERAL IPLOOKUP MANUALROUTE QUERYPROGRAM REDIRECT
  transports TRANSPORT	APPENDFILE AUTOREPLY LMTP PIPE QUEUEFILE SMTP
  auths	    AUTH	CRAM_MD5 CYRUS_SASL DOVECOT EXTERNAL GSASL HEIMDAL_GSSAPI PLAINTEXT SPA TLS
- miscmods   SUPPORT	ARC _DKIM DMARC DMARC_NATIVE _EXIM_FILTER PAM PERL RADIUS _SIEVE_FILTER SPF SPF_PERL
+ miscmods   SUPPORT	ARC _DKIM DMARC DMARC_NATIVE DSCP _EXIM_FILTER PAM PERL RADIUS _SIEVE_FILTER SPF SPF_PERL
 END
 
 # See if there is a definition of EXIM_PERL in what we have built so far.
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index 517310c8a..35b58ec04 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -90,7 +90,7 @@ done
 cd ..
 
 # miscellaneous modules
-# Note that the file in the miscmods/pdkim/ source subdir get linked to the 
+# Note that the files in the miscmods/pdkim/ source subdir get linked to the 
 # destination miscmods/ dir
 d="miscmods"
 mkdir $d
@@ -102,6 +102,7 @@ for f in dummy.c \
 	pdkim/crypt_ver.h pdkim/pdkim.c pdkim/pdkim.h \
 	pdkim/pdkim_hash.h pdkim/signing.c pdkim/signing.h \
 	dmarc.c dmarc_common.c dmarc.h dmarc_api.h dmarc_native.c \
+	dscp.c dscp_api.h \
 	exim_filter.c exim_filter_api.h \
 	pam.c pam_api.h \
 	perl.c perl_api.h \
diff --git a/src/src/EDITME b/src/src/EDITME
index 1c9d834dd..98baae7d5 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -577,6 +577,9 @@ SUPPORT_DANE=yes
 
 # SUPPORT_SIEVE_FILTER=2
 
+# Uncomment the line to include DSCP support.  Set to 2 for a module build.
+# SUPPORT_DSCP
+
 #------------------------------------------------------------------------------
 # Compiling Exim with content scanning support: If you want to compile Exim
 # with support for message body content scanning, set WITH_CONTENT_SCAN to
diff --git a/src/src/acl.c b/src/src/acl.c
index 63478ca7d..bc4f6af6d 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -3498,44 +3498,18 @@ for (; cb; cb = cb->next)
 	  f.dmarc_enable_forensic = TRUE;
 	  break;
 #endif
-
+#ifdef SUPPORT_DSCP
 	case CONTROL_DSCP:
-	  if (*p == '/')
-	    {
-	    int af, socklevel, optname, value;
-	    /* If we are acting on stdin, the setsockopt may fail if stdin is not
-	    a socket; we can accept that, we'll just debug-log failures anyway. */
-	    if (smtp_in_fd < 0) return ERROR;
-	    if ((af = ip_get_address_family(smtp_in_fd)) < 0)
-	      {
-	      HDEBUG(D_acl)
-		debug_printf_indent("smtp input is probably not a socket [%s], not setting DSCP\n",
-		    strerror(errno));
-	      break;
-	      }
-	    if (dscp_lookup(p+1, af, &socklevel, &optname, &value))
-	      if (setsockopt(smtp_in_fd, socklevel, optname,
-			    &value, sizeof(value)) < 0)
-		{
-		HDEBUG(D_acl) debug_printf_indent("failed to set input DSCP[%s]: %s\n",
-		    p+1, strerror(errno));
-		}
-	      else
-		{
-		HDEBUG(D_acl) debug_printf_indent("set input DSCP to %q\n", p+1);
-		}
-	    else
-	      {
-	      *log_msgptr = string_sprintf("unrecognised DSCP value in \"control=%s\"", arg);
-	      return ERROR;
-	      }
-	    }
-	  else
-	    {
-	    *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg);
-	    return ERROR;
-	    }
+	  {
+	  misc_module_info * mi = misc_mod_find(US"dscp", &log_message);
+	  typedef uschar * (*fn_t)(const uschar *, const uschar *);
+	  if (!mi)
+	    rc = DEFER;
+	  else if ((*log_msgptr = ((fn_t *) mi->functions)[DSCP_ACL] (arg, p)))
+	    rc = ERROR;
 	  break;
+	  }
+#endif
 
 	case CONTROL_ERROR:
 	  return ERROR;
diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults
index 71146ae3b..ba9f50b01 100644
--- a/src/src/config.h.defaults
+++ b/src/src/config.h.defaults
@@ -155,6 +155,7 @@ Do not put spaces between # and the 'define'.
 #define SUPPORT_DMARC
 #define DMARC_API 100400
 #define DMARC_TLD_FILE "/etc/exim/opendmarc.tlds"
+#define SUPPORT_DSCP
 #define SUPPORT_I18N
 #define SUPPORT_I18N_2008
 #define SUPPORT_MAILDIR
diff --git a/src/src/drtables.c b/src/src/drtables.c
index bf1c3e530..e21d47047 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -356,7 +356,7 @@ return NULL;
 
 int
 misc_mod_conn_init(const uschar * sender_helo_name,
-  const uschar * sender_host_address, const uschar * * errstr)
+  const uschar * sender_host_address, const uschar ** errstr)
 {
 for (const misc_module_info * mi = misc_module_list; mi; mi = mi->next)
   if (mi->conn_init)
@@ -495,6 +495,9 @@ extern misc_module_info spf_module_info;
 #if defined(EXPERIMENTAL_ARC) && (!defined(SUPPORT_ARC) || SUPPORT_ARC!=2)
 extern misc_module_info arc_module_info;
 #endif
+#if defined(SUPPORT_DSCP) && SUPPORT_DSCP!=2
+extern misc_module_info dscp_module_info;
+#endif
 #if defined(RADIUS_CONFIG_FILE) && (!defined(SUPPORT_RADIUS) || SUPPORT_RADIUS!=2)
 extern misc_module_info radius_module_info;
 #endif
@@ -531,6 +534,9 @@ onetime = TRUE;
 /* dmarc depends on spf/dkim/arc so this add must go after, for the both-static case */
   misc_mod_add(&dmarc_module_info);
 #endif
+#if defined(SUPPORT_DSCP) && SUPPORT_DSCP!=2
+  misc_mod_add(&dscp_module_info);
+#endif
 #if defined(RADIUS_CONFIG_FILE) && (!defined(SUPPORT_RADIUS) || SUPPORT_RADIUS!=2)
   misc_mod_add(&radius_module_info);
 #endif
diff --git a/src/src/exim.c b/src/src/exim.c
index 693301899..383fe8247 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1186,6 +1186,9 @@ g = string_cat(g, US"Support for:");
 #ifndef DISABLE_DNSSEC
   g = string_cat(g, US" DNSSEC");
 #endif
+#ifdef SUPPORT_DSCP
+  g = string_cat(g, US" DSCP");
+#endif
 #ifndef DISABLE_ESMTP_LIMITS
   g = string_cat(g, US" ESMTP_Limits");
 #endif
@@ -1382,7 +1385,9 @@ switch(request)
 "If the string is not recognised, you'll get this help (on stderr).\n"
 "\n"
 "  exim -bI:help    this information\n"
+#ifdef SUPPORT_DSCP
 "  exim -bI:dscp    list of known dscp value keywords\n"
+#endif
 "  exim -bI:sieve   list of supported sieve extensions\n"
 );
     return;
@@ -1396,9 +1401,16 @@ switch(request)
       fprintf(stream, "Sieve filtering not available\n");
     }
     return;
+#ifdef SUPPORT_DSCP
   case CMDINFO_DSCP:
-    dscp_list_to_stream(stream);
+    {
+    uschar * dummy_errstr;
+    misc_module_info * mi = misc_mod_find(US"dscp", &dummy_errstr);
+    typedef void (*fn_t)(FILE *);
+    if (mi) ((fn_t *) mi->functions)[DSCP_KEYWORDS] (stream);
+    }
     return;
+#endif
   }
 }
 
@@ -2419,15 +2431,11 @@ on the second character (the one after '-'), to save some effort. */
 	    info_flag = CMDINFO_HELP;
 	    if (Ustrlen(p))
 	      if (strcmpic(p, CUS"sieve") == 0)
-		{
-		info_flag = CMDINFO_SIEVE;
-		info_stdout = TRUE;
-		}
+		{ info_flag = CMDINFO_SIEVE; info_stdout = TRUE; }
+#ifdef SUPPORT_DSCP
 	      else if (strcmpic(p, CUS"dscp") == 0)
-		{
-		info_flag = CMDINFO_DSCP;
-		info_stdout = TRUE;
-		}
+		{ info_flag = CMDINFO_DSCP; info_stdout = TRUE; }
+#endif
 	      else if (strcmpic(p, CUS"help") == 0)
 		info_stdout = TRUE;
 	    }
diff --git a/src/src/exim.h b/src/src/exim.h
index 538f354e2..07b017a01 100644
--- a/src/src/exim.h
+++ b/src/src/exim.h
@@ -558,6 +558,9 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly.
 #ifdef EXPERIMENTAL_ARC
 # include "miscmods/arc_api.h"
 #endif
+#ifdef SUPPORT_DSCP
+# include "miscmods/dscp_api.h"
+#endif
 #ifdef RADIUS_CONFIG_FILE
 # include "miscmods/radius_api.h"
 #endif
diff --git a/src/src/expand.c b/src/src/expand.c
index e222e8e75..b0500037d 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -8753,8 +8753,7 @@ uschar *
 expand_string_copy(const uschar * string)
 {
 const uschar * yield = expand_string(string);
-if (yield == string) yield = string_copy(string);
-return US yield;
+return yield == string ? string_copy(string) : US yield;
 }
 
 
diff --git a/src/src/functions.h b/src/src/functions.h
index ec13c544e..4e56c14a4 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -216,8 +216,6 @@ extern void    dns_pattern_init(void);
 extern int     dns_special_lookup(dns_answer *, const uschar *, int, const uschar **);
 extern dns_record *dns_next_rr(const dns_answer *, dns_scan *, int);
 extern uschar *dns_text_type(int);
-extern void    dscp_list_to_stream(FILE *);
-extern BOOL    dscp_lookup(const uschar *, int, int *, int *, int *);
 
 extern void    enq_end(uschar *);
 extern unsigned enq_start(uschar *, unsigned);
@@ -290,7 +288,6 @@ extern int     ip_bind(int, int, const uschar *, int);
 extern int     ip_connect(int, int, const uschar *, int, int, const blob *);
 extern int     ip_connectedsocket(int, const uschar *, int, int,
                  int, host_item *, uschar **, const blob *);
-extern int     ip_get_address_family(int);
 extern void    ip_keepalive(int, const uschar *, BOOL);
 extern int     ip_recv(client_conn_ctx *, uschar *, int, time_t);
 extern int     ip_socket(int, int);
diff --git a/src/src/ip.c b/src/src/ip.c
index f48fa82a9..80038e3ec 100644
--- a/src/src/ip.c
+++ b/src/src/ip.c
@@ -681,179 +681,6 @@ return -1;
 
 
 
-
-/*************************************************
-*    Lookup address family of potential socket   *
-*************************************************/
-
-/* Given a file-descriptor, check to see if it's a socket and, if so,
-return the address family; detects IPv4 vs IPv6.  If not a socket then
-return -1.
-
-The value 0 is typically AF_UNSPEC, which should not be seen on a connected
-fd.  If the return is -1, the errno will be from getsockname(); probably
-ENOTSOCK or ECONNRESET.
-
-Arguments:     socket-or-not fd
-Returns:       address family or -1
-*/
-
-int
-ip_get_address_family(int fd)
-{
-struct sockaddr_storage ss;
-socklen_t sslen = sizeof(ss);
-
-if (getsockname(fd, (struct sockaddr *) &ss, &sslen) < 0)
-  return -1;
-
-return (int) ss.ss_family;
-}
-
-
-
-
-/*************************************************
-*       Lookup DSCP settings for a socket        *
-*************************************************/
-
-struct dscp_name_tableentry {
-  const uschar *name;
-  int value;
-};
-/* Keep both of these tables sorted! */
-static struct dscp_name_tableentry dscp_table[] = {
-#ifdef IPTOS_DSCP_AF11
-    { CUS"af11", IPTOS_DSCP_AF11 },
-    { CUS"af12", IPTOS_DSCP_AF12 },
-    { CUS"af13", IPTOS_DSCP_AF13 },
-    { CUS"af21", IPTOS_DSCP_AF21 },
-    { CUS"af22", IPTOS_DSCP_AF22 },
-    { CUS"af23", IPTOS_DSCP_AF23 },
-    { CUS"af31", IPTOS_DSCP_AF31 },
-    { CUS"af32", IPTOS_DSCP_AF32 },
-    { CUS"af33", IPTOS_DSCP_AF33 },
-    { CUS"af41", IPTOS_DSCP_AF41 },
-    { CUS"af42", IPTOS_DSCP_AF42 },
-    { CUS"af43", IPTOS_DSCP_AF43 },
-    { CUS"ef", IPTOS_DSCP_EF },
-#endif
-#ifdef IPTOS_LOWCOST
-    { CUS"lowcost", IPTOS_LOWCOST },
-#endif
-    { CUS"lowdelay", IPTOS_LOWDELAY },
-#ifdef IPTOS_MINCOST
-    { CUS"mincost", IPTOS_MINCOST },
-#endif
-    { CUS"reliability", IPTOS_RELIABILITY },
-    { CUS"throughput", IPTOS_THROUGHPUT }
-};
-static int dscp_table_size =
-  sizeof(dscp_table) / sizeof(struct dscp_name_tableentry);
-
-/* DSCP values change by protocol family, and so do the options used for
-setsockopt(); this utility does all the lookups.  It takes an unexpanded
-option string, expands it, strips off affix whitespace, then checks if it's
-a number.  If all of what's left is a number, then that's how the option will
-be parsed and success/failure is a range check.  If it's not all a number,
-then it must be a supported keyword.
-
-Arguments:
-  dscp_name   a string, so far unvalidated
-  af          address_family in use
-  level       setsockopt level to use
-  optname     setsockopt name to use
-  dscp_value  value for dscp_name
-
-Returns: TRUE if okay to setsockopt(), else FALSE
-
-*level and *optname may be set even if FALSE is returned
-*/
-
-BOOL
-dscp_lookup(const uschar *dscp_name, int af,
-    int *level, int *optname, int *dscp_value)
-{
-uschar *dscp_lookup, *p;
-int first, last;
-long rawlong;
-
-if (af == AF_INET)
-  {
-  *level = IPPROTO_IP;
-  *optname = IP_TOS;
-  }
-#if HAVE_IPV6 && defined(IPV6_TCLASS)
-else if (af == AF_INET6)
-  {
-  *level = IPPROTO_IPV6;
-  *optname = IPV6_TCLASS;
-  }
-#endif
-else
-  {
-  DEBUG(D_transport)
-    debug_printf("Unhandled address family %d in dscp_lookup()\n", af);
-  return FALSE;
-  }
-if (!dscp_name)
-  {
-  DEBUG(D_transport)
-    debug_printf("[empty DSCP]\n");
-  return FALSE;
-  }
-dscp_lookup = expand_string(US dscp_name);
-if (dscp_lookup == NULL || *dscp_lookup == '\0')
-  return FALSE;
-
-p = dscp_lookup + Ustrlen(dscp_lookup) - 1;
-while (isspace(*p)) *p-- = '\0';
-while (isspace(*dscp_lookup) && dscp_lookup < p) dscp_lookup++;
-if (*dscp_lookup == '\0')
-  return FALSE;
-
-rawlong = Ustrtol(dscp_lookup, &p, 0);
-if (p != dscp_lookup && *p == '\0')
-  {
-  /* We have six bits available, which will end up shifted to fit in 0xFC mask.
-  RFC 2597 defines the values unshifted. */
-  if (rawlong < 0 || rawlong > 0x3F)
-    {
-    DEBUG(D_transport)
-      debug_printf("DSCP value %ld out of range, ignored.\n", rawlong);
-    return FALSE;
-    }
-  *dscp_value = rawlong << 2;
-  return TRUE;
-  }
-
-first = 0;
-last = dscp_table_size;
-while (last > first)
-  {
-  int middle = (first + last)/2;
-  int c = Ustrcmp(dscp_lookup, dscp_table[middle].name);
-  if (c == 0)
-    {
-    *dscp_value = dscp_table[middle].value;
-    return TRUE;
-    }
-  else if (c > 0)
-    first = middle + 1;
-  else
-    last = middle;
-  }
-return FALSE;
-}
-
-void
-dscp_list_to_stream(FILE *stream)
-{
-for (int i = 0; i < dscp_table_size; ++i)
-  fprintf(stream, "%s\n", dscp_table[i].name);
-}
-
-
 /* End of ip.c */
 /* vi: aw ai sw=2
 */
diff --git a/src/src/macro_predef.c b/src/src/macro_predef.c
index a8ebc40d4..4778ace64 100644
--- a/src/src/macro_predef.c
+++ b/src/src/macro_predef.c
@@ -212,6 +212,9 @@ due to conflicts with other common macros. */
 #ifdef EXPERIMENTAL_DCC
   builtin_macro_create(US"_HAVE_DCC");
 #endif
+#ifdef SUPPORT_DSCP
+  builtin_macro_create(US"_HAVE_DSCP");
+#endif
 #ifdef EXPERIMENTAL_DSN_INFO
   builtin_macro_create(US"_HAVE_DSN_INFO");
 #endif
diff --git a/src/src/miscmods/Makefile b/src/src/miscmods/Makefile
index 2d44059ba..e129fe8bc 100644
--- a/src/src/miscmods/Makefile
+++ b/src/src/miscmods/Makefile
@@ -41,6 +41,7 @@ dmarc.o dmarc.so:		$(HDRS) spf.h pdkim.h dmarc.h \
 				dmarc.c dmarc_common.c
 dmarc_native.o dmarc_native.so:	$(HDRS) spf.h pdkim.h dmarc.h \
 				dmarc_native.c dmarc_common.c
+dscp.o  dscp.so:		$(HDRS) dscp.c
 dummy.o:			dummy.c
 exim_filter.o exim_filter.so:	$(HDRS) exim_filter.c
 pam.o   pam.so:			$(HDRS) pam.c
diff --git a/src/src/miscmods/dscp.c b/src/src/miscmods/dscp.c
new file mode 100644
index 000000000..8c25833d9
--- /dev/null
+++ b/src/src/miscmods/dscp.c
@@ -0,0 +1,278 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* DSCP support for Exim
+   Copyright (c) The Exim Maintainers - 2025
+   License: GPL
+   SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "../exim.h"
+#if defined SUPPORT_DSCP
+
+# include "../functions.h"
+
+/******************************************************************************/
+/* Utility functions */
+
+/*************************************************
+*    Lookup address family of potential socket   *
+*************************************************/
+
+/* Given a file-descriptor, check to see if it's a socket and, if so,
+return the address family; detects IPv4 vs IPv6.  If not a socket then
+return -1.
+
+The value 0 is typically AF_UNSPEC, which should not be seen on a connected
+fd.  If the return is -1, the errno will be from getsockname(); probably
+ENOTSOCK or ECONNRESET.
+
+Arguments:     socket-or-not fd
+Returns:       address family or -1
+*/
+
+static int
+ip_get_address_family(int fd)
+{
+struct sockaddr_storage ss;
+socklen_t sslen = sizeof(ss);
+
+if (getsockname(fd, (struct sockaddr *) &ss, &sslen) < 0)
+  return -1;
+
+return (int) ss.ss_family;
+}
+
+
+
+/*************************************************
+*       Lookup DSCP settings for a socket        *
+*************************************************/
+
+struct dscp_name_tableentry {
+  const uschar *name;
+  int value;
+};
+/* Keep both of these tables sorted! */
+static struct dscp_name_tableentry dscp_table[] = {
+#ifdef IPTOS_DSCP_AF11
+    { CUS"af11", IPTOS_DSCP_AF11 },
+    { CUS"af12", IPTOS_DSCP_AF12 },
+    { CUS"af13", IPTOS_DSCP_AF13 },
+    { CUS"af21", IPTOS_DSCP_AF21 },
+    { CUS"af22", IPTOS_DSCP_AF22 },
+    { CUS"af23", IPTOS_DSCP_AF23 },
+    { CUS"af31", IPTOS_DSCP_AF31 },
+    { CUS"af32", IPTOS_DSCP_AF32 },
+    { CUS"af33", IPTOS_DSCP_AF33 },
+    { CUS"af41", IPTOS_DSCP_AF41 },
+    { CUS"af42", IPTOS_DSCP_AF42 },
+    { CUS"af43", IPTOS_DSCP_AF43 },
+    { CUS"ef", IPTOS_DSCP_EF },
+#endif
+#ifdef IPTOS_LOWCOST
+    { CUS"lowcost", IPTOS_LOWCOST },
+#endif
+    { CUS"lowdelay", IPTOS_LOWDELAY },
+#ifdef IPTOS_MINCOST
+    { CUS"mincost", IPTOS_MINCOST },
+#endif
+    { CUS"reliability", IPTOS_RELIABILITY },
+    { CUS"throughput", IPTOS_THROUGHPUT }
+};
+static int dscp_table_size =
+  sizeof(dscp_table) / sizeof(struct dscp_name_tableentry);
+
+/* DSCP values change by protocol family, and so do the options used for
+setsockopt(); this utility does all the lookups.  It takes an unexpanded
+option string, expands it, strips off affix whitespace, then checks if it's
+a number.  If all of what's left is a number, then that's how the option will
+be parsed and success/failure is a range check.  If it's not all a number,
+then it must be a supported keyword.
+
+Arguments:
+  dscp_name   a string, so far unvalidated
+  af          address_family in use
+  level       setsockopt level to use
+  optname     setsockopt name to use
+  dscp_value  value for dscp_name
+
+Returns: TRUE if okay to setsockopt(), else FALSE
+
+*level and *optname may be set even if FALSE is returned
+*/
+
+static BOOL
+dscp_lookup(const uschar * dscp_name, int af,
+    int * level, int * optname, int * dscp_value)
+{
+uschar * dscp_lookup, * p;
+int first, last;
+long rawlong;
+
+if (af == AF_INET)
+  { *level = IPPROTO_IP; *optname = IP_TOS; }
+#if HAVE_IPV6 && defined(IPV6_TCLASS)
+else if (af == AF_INET6)
+  { *level = IPPROTO_IPV6; *optname = IPV6_TCLASS; }
+#endif
+else
+  {
+  DEBUG(D_transport)
+    debug_printf("Unhandled address family %d in dscp_lookup()\n", af);
+  return FALSE;
+  }
+if (!dscp_name)
+  {
+  DEBUG(D_transport)
+    debug_printf("[empty DSCP]\n");
+  return FALSE;
+  }
+dscp_lookup = expand_string_copy(dscp_name);
+if (!dscp_lookup || !*dscp_lookup)
+  return FALSE;
+
+p = dscp_lookup + Ustrlen(dscp_lookup) - 1;
+while (isspace(*p)) *p-- = '\0';
+while (isspace(*dscp_lookup) && dscp_lookup < p) dscp_lookup++;
+if (*dscp_lookup == '\0')
+  return FALSE;
+
+rawlong = Ustrtol(dscp_lookup, &p, 0);
+if (p != dscp_lookup && *p == '\0')
+  {
+  /* We have six bits available, which will end up shifted to fit in 0xFC mask.
+  RFC 2597 defines the values unshifted. */
+  if (rawlong < 0 || rawlong > 0x3F)
+    {
+    DEBUG(D_transport)
+      debug_printf("DSCP value %ld out of range, ignored.\n", rawlong);
+    return FALSE;
+    }
+  *dscp_value = rawlong << 2;
+  return TRUE;
+  }
+
+first = 0;
+last = dscp_table_size;
+while (last > first)
+  {
+  int middle = (first + last)/2;
+  int c = Ustrcmp(dscp_lookup, dscp_table[middle].name);
+  if (c == 0)
+    {
+    *dscp_value = dscp_table[middle].value;
+    return TRUE;
+    }
+  else if (c > 0)
+    first = middle + 1;
+  else
+    last = middle;
+  }
+return FALSE;
+}
+
+/******************************************************************************/
+
+/*API
+Set DSCP on stdin.  Called from ACL control.
+Return error message on fail, NULL on ok.
+*/
+
+static uschar *
+dscp_acl(const uschar * control, const uschar * opt)
+{
+int af, socklevel, optname, value;
+
+if (*opt != '/')
+  return string_sprintf("syntax error in \"control=%s\"", control);
+
+/* If we are acting on stdin, the setsockopt may fail if stdin is
+not a socket; we can accept that, we'll just debug-log failures
+anyway. */
+if (smtp_in_fd < 0) return US"no stdin";
+if ((af = ip_get_address_family(smtp_in_fd)) < 0)
+  {
+  HDEBUG(D_acl) debug_printf_indent(
+    "smtp input is probably not a socket [%s], not setting DSCP\n",
+    strerror(errno));
+  return NULL;
+  }
+if (!dscp_lookup(++opt, af, &socklevel, &optname, &value))
+  return string_sprintf("unrecognised DSCP value in \"control=%s\"", control);
+
+value = setsockopt(smtp_in_fd, socklevel, optname,
+		    &value, sizeof(value));
+HDEBUG(D_acl)
+  if (value < 0)
+    debug_printf_indent("failed to set input DSCP[%s]: %s\n",
+      opt, strerror(errno));
+  else
+    debug_printf_indent("set input DSCP to %q\n", opt);
+return NULL;
+}
+
+
+/*API
+Set DSCP on given socket; called from smtp transport.
+Can silently fail.
+*/
+
+static void
+dscp_transport(int sock, const uschar * dscp_str, int host_af)
+{
+int dscp_value, dscp_level, dscp_option;
+
+if (  dscp_str
+   && dscp_lookup(dscp_str, host_af, &dscp_level, &dscp_option, &dscp_value)
+   )
+  {
+  HDEBUG(D_transport|D_acl|D_v)
+    debug_printf_indent("DSCP %q=%x ", dscp_str, dscp_value);
+
+  if (setsockopt(sock, dscp_level, dscp_option, &dscp_value, sizeof(dscp_value)) < 0)
+    HDEBUG(D_transport|D_acl|D_v)
+      debug_printf_indent("failed to set DSCP: %s ", strerror(errno));
+
+  /* If the kernel supports IPv4 and IPv6 on an IPv6 socket, we need to set the
+  option for both; ignore failures here */
+
+  if (  host_af == AF_INET6
+     && dscp_lookup(dscp_str, AF_INET, &dscp_level, &dscp_option, &dscp_value)
+     )
+    (void) setsockopt(sock, dscp_level, dscp_option, &dscp_value, sizeof(dscp_value));
+  }
+}
+
+
+/*API: output known DSCP names */
+static void
+dscp_keywords(FILE * stream)
+{
+for (int i = 0; i < dscp_table_size; ++i)
+  fprintf(stream, "%s\n", dscp_table[i].name);
+}
+
+/******************************************************************************/
+/* Module API */
+
+static void * dscp_functions[] = {
+  [DSCP_ACL] =		(void *) dscp_acl,
+  [DSCP_TRANSPORT] =	(void *) dscp_transport,
+  [DSCP_KEYWORDS] =	(void *) dscp_keywords,
+};
+
+misc_module_info dscp_module_info =
+{
+  .name =		US"dscp",
+# ifdef DYNLOOKUP
+  .dyn_magic =		MISC_MODULE_MAGIC,
+# endif
+  .functions =		dscp_functions,
+  .functions_count =	nelem(dscp_functions),
+};
+
+#endif /* SUPPORT_DSCP */
+/* vi: aw ai sw=2
+ */
diff --git a/src/src/miscmods/dscp_api.h b/src/src/miscmods/dscp_api.h
new file mode 100644
index 000000000..ce355319d
--- /dev/null
+++ b/src/src/miscmods/dscp_api.h
@@ -0,0 +1,16 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2025 */
+/* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/* API definitions for the arcmodule */
+
+
+/* Function table entry numbers */
+
+#define	DSCP_ACL		0
+#define	DSCP_TRANSPORT		1
+#define	DSCP_KEYWORDS		2
diff --git a/src/src/smtp_out.c b/src/src/smtp_out.c
index 465acb5ed..790a0c005 100644
--- a/src/src/smtp_out.c
+++ b/src/src/smtp_out.c
@@ -276,10 +276,7 @@ Update those with the state.  Return the fd, or -1 with errno set.
 int
 smtp_boundsock(smtp_connect_args * sc)
 {
-transport_instance * tb = sc->tblock;
-smtp_transport_options_block * ob = tb->drinst.options_block;
-const uschar * dscp = ob->dscp;
-int sock, dscp_value, dscp_level, dscp_option;
+int sock;
 
 if ((sock = ip_socket(SOCK_STREAM, sc->host_af)) < 0)
   return -1;
@@ -290,23 +287,23 @@ if (setsockopt(sock, IPPROTO_TCP, TCP_NODELAY, US &on, sizeof(on)))
   HDEBUG(D_transport|D_acl|D_v)
     debug_printf_indent("failed to set NODELAY: %s ", strerror(errno));
 
+#ifdef SUPPORT_DSCP
 /* Set DSCP value, if we can. For now, if we fail to set the value, we don't
 bomb out, just log it and continue in default traffic class. */
-
-GET_OPTION("dscp");
-if (dscp && dscp_lookup(dscp, sc->host_af, &dscp_level, &dscp_option, &dscp_value))
   {
-  HDEBUG(D_transport|D_acl|D_v)
-    debug_printf_indent("DSCP %q=%x ", dscp, dscp_value);
-  if (setsockopt(sock, dscp_level, dscp_option, &dscp_value, sizeof(dscp_value)) < 0)
-    HDEBUG(D_transport|D_acl|D_v)
-      debug_printf_indent("failed to set DSCP: %s ", strerror(errno));
-  /* If the kernel supports IPv4 and IPv6 on an IPv6 socket, we need to set the
-  option for both; ignore failures here */
-  if (sc->host_af == AF_INET6 &&
-      dscp_lookup(dscp, AF_INET, &dscp_level, &dscp_option, &dscp_value))
-    (void) setsockopt(sock, dscp_level, dscp_option, &dscp_value, sizeof(dscp_value));
+  transport_instance * tb = sc->tblock;
+  smtp_transport_options_block * ob = tb->drinst.options_block;
+  GET_OPTION("dscp");
+  if (ob->dscp)
+    {
+    uschar * dummy_errstr;
+    misc_module_info * mi = misc_mod_find(US"dscp", &dummy_errstr);
+    typedef void (*fn_t)(int, const uschar *, int);
+    if (mi)
+      ((fn_t *) mi->functions)[DSCP_TRANSPORT] (sock, ob->dscp, sc->host_af);
+    }
   }
+#endif
 
 /* Bind to a specific interface if requested. Caller must ensure the interface
 is the same type (IPv4 or IPv6) as the outgoing address. */

commit 732c68f17cb249e6d40727ab0a043ded0fed2247
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Dec 14 12:52:18 2025 +0000

    socks module

diff --git a/src/scripts/Configure-Makefile b/src/scripts/Configure-Makefile
index 9b766d6ee..82e93d2ad 100755
--- a/src/scripts/Configure-Makefile
+++ b/src/scripts/Configure-Makefile
@@ -332,7 +332,7 @@ done <<-END
  routers    ROUTER	ACCEPT DNSLOOKUP IPLITERAL IPLOOKUP MANUALROUTE QUERYPROGRAM REDIRECT
  transports TRANSPORT	APPENDFILE AUTOREPLY LMTP PIPE QUEUEFILE SMTP
  auths	    AUTH	CRAM_MD5 CYRUS_SASL DOVECOT EXTERNAL GSASL HEIMDAL_GSSAPI PLAINTEXT SPA TLS
- miscmods   SUPPORT	ARC _DKIM DMARC DMARC_NATIVE DSCP _EXIM_FILTER PAM PERL RADIUS _SIEVE_FILTER SPF SPF_PERL
+ miscmods   SUPPORT	ARC _DKIM DMARC DMARC_NATIVE DSCP _EXIM_FILTER PAM PERL RADIUS _SIEVE_FILTER SOCKS SPF SPF_PERL
 END
 
 # See if there is a definition of EXIM_PERL in what we have built so far.
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index 35b58ec04..1875c6963 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -66,7 +66,7 @@ cd $d
 # Makefile is generated
 for f in README appendfile.h appendfile.c autoreply.h \
   autoreply.c lmtp.h lmtp.c pipe.h pipe.c queuefile.c queuefile.h \
-  smtp.h smtp.c smtp_socks.c tf_maildir.c tf_maildir.h
+  smtp.h smtp.c tf_maildir.c tf_maildir.h
 do
   ln -s ../../src/$d/$f $f
 done
@@ -108,6 +108,7 @@ for f in dummy.c \
 	perl.c perl_api.h \
 	radius.c radius_api.h \
 	sieve_filter.c sieve_filter_api.h \
+	socks.c socks_api.h \
 	spf.c spf_perl.c spf.h spf_api.h
 do
   ln -s ../../src/$d/$f `basename $f`
diff --git a/src/src/EDITME b/src/src/EDITME
index 98baae7d5..695cceafd 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -1131,7 +1131,7 @@ ZCAT_COMMAND=/usr/bin/zcat
 # Proxying.
 #
 # If you may want to use outbound (client-side) proxying, using Socks5,
-# uncomment the line below.
+# uncomment the line below.  To build as a dynamic module set it to 2.
 
 # SUPPORT_SOCKS=yes
 
diff --git a/src/src/drtables.c b/src/src/drtables.c
index e21d47047..3c714ba24 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -513,6 +513,9 @@ extern misc_module_info exim_filter_module_info;
 #if !defined(DISABLE_SIEVE_FILTER) && (!defined(SUPPORT_SIEVE_FILTER) || SUPPORT_SIEVE_FILTER!=2)
 extern misc_module_info sieve_filter_module_info;
 #endif
+#if defined(SUPPORT_SOCKS) && SUPPORT_SOCKS!=2
+extern misc_module_info socks_module_info;
+#endif
 
 void
 init_misc_mod_list(void)
@@ -537,9 +540,6 @@ onetime = TRUE;
 #if defined(SUPPORT_DSCP) && SUPPORT_DSCP!=2
   misc_mod_add(&dscp_module_info);
 #endif
-#if defined(RADIUS_CONFIG_FILE) && (!defined(SUPPORT_RADIUS) || SUPPORT_RADIUS!=2)
-  misc_mod_add(&radius_module_info);
-#endif
 #if defined(SUPPORT_PAM) && SUPPORT_PAM!=2
   misc_mod_add(&pam_module_info);
 #endif
@@ -552,6 +552,12 @@ onetime = TRUE;
 #if !defined(DISABLE_SIEVE_FILTER) && (!defined(SUPPORT_SIEVE_FILTER) || SUPPORT_SIEVE_FILTER!=2)
   misc_mod_add(&sieve_filter_module_info);
 #endif
+#if defined(SUPPORT_SOCKS) && SUPPORT_SOCKS!=2
+  misc_mod_add(&socks_module_info);
+#endif
+#if defined(RADIUS_CONFIG_FILE) && (!defined(SUPPORT_RADIUS) || SUPPORT_RADIUS!=2)
+  misc_mod_add(&radius_module_info);
+#endif
 }
 
 
diff --git a/src/src/exim.c b/src/src/exim.c
index 383fe8247..e955d2488 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -3093,6 +3093,8 @@ on the second character (the one after '-'), to save some effort. */
 
 #ifdef SUPPORT_SOCKS
     /* -MCp: Socks proxy in use; nearside IP, port, external IP, port */
+    /*XXX Neatness might have this code in the socks module, but having to
+    load a dynamic version puely for this?  However, of logging moved there...*/
 	case 'p': proxy_session = TRUE;
 		  if (++i < argc)
 		    {
diff --git a/src/src/exim.h b/src/src/exim.h
index 07b017a01..4bd0a87e7 100644
--- a/src/src/exim.h
+++ b/src/src/exim.h
@@ -570,6 +570,9 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly.
 #ifdef EXIM_PERL
 # include "miscmods/perl_api.h"
 #endif
+#ifdef SUPPORT_SOCKS
+# include "miscmods/socks_api.h"
+#endif
 #include "miscmods/exim_filter_api.h"
 #include "miscmods/sieve_filter_api.h"
 
diff --git a/src/src/miscmods/Makefile b/src/src/miscmods/Makefile
index e129fe8bc..a5fbba441 100644
--- a/src/src/miscmods/Makefile
+++ b/src/src/miscmods/Makefile
@@ -48,6 +48,7 @@ pam.o   pam.so:			$(HDRS) pam.c
 perl.o perl.so:         	$(HDRS) perl.c
 radius.o radius.so:		$(HDRS) radius.c
 sieve_filter.o sieve_filter.so:	$(HDRS) sieve_filter.c
+socks.o socks.so:		$(HDRS) socks_api.h ../transports/smtp.h socks.c
 spf.o   spf.so:			$(HDRS) spf.h spf.c
 spf_perl.o spf_perl.so:		$(HDRS) spf.h spf_perl.c
 
diff --git a/src/src/transports/smtp_socks.c b/src/src/miscmods/socks.c
similarity index 94%
rename from src/src/transports/smtp_socks.c
rename to src/src/miscmods/socks.c
index c17b808e9..6694044da 100644
--- a/src/src/transports/smtp_socks.c
+++ b/src/src/miscmods/socks.c
@@ -10,15 +10,10 @@
 /* SOCKS version 5 proxy, client-mode */
 
 #include "../exim.h"
-#include "smtp.h"
+#include "../transports/smtp.h"
 
 #ifdef SUPPORT_SOCKS /* entire file */
 
-#ifndef nelem
-# define nelem(arr) (sizeof(arr)/sizeof(*arr))
-#endif
-
-
 /* Defaults */
 #define SOCKS_PORT	1080
 #define SOCKS_TIMEOUT	5
@@ -187,18 +182,18 @@ return -1;
 
 
 
-/* Make a connection via a socks proxy
+/*API: Make a connection via a socks proxy
 
 Arguments:
  sc		details for making connection: host, af, interface, transport
  early_data	data to send down the smtp channel (once proxied)
 
 Return value:
- 0 on success; -1 on failure, with errno set
+ connected file descriptor on success; -1 on failure, with errno set
 */
 
-int
-socks_sock_connect(smtp_connect_args * sc, const blob * early_data)
+static int
+socks_sock_connect(const smtp_connect_args * sc, const blob * early_data)
 {
 transport_instance * tb = sc->tblock;
 smtp_transport_options_block * ob = tb->drinst.options_block;
@@ -407,6 +402,24 @@ rcv_err:
   return -1;
 }
 
+/******************************************************************************/
+/* Module API */
+
+static void * socks_functions[] = {
+  [SOCKS_CONNECT] =	(void *) socks_sock_connect,
+};
+
+misc_module_info socks_module_info =
+{
+  .name =		US"socks",
+# ifdef DYNLOOKUP
+  .dyn_magic =		MISC_MODULE_MAGIC,
+# endif
+
+  .functions =		socks_functions,
+  .functions_count =	nelem(socks_functions),
+};
+
 #endif	/* entire file */
 /* vi: aw ai sw=2
 */
diff --git a/src/src/miscmods/socks_api.h b/src/src/miscmods/socks_api.h
new file mode 100644
index 000000000..6a083d3cb
--- /dev/null
+++ b/src/src/miscmods/socks_api.h
@@ -0,0 +1,14 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2025 */
+/* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/* API definitions for the socks module */
+
+
+/* Function table entry numbers */
+
+#define	SOCKS_CONNECT	0
diff --git a/src/src/smtp_out.c b/src/src/smtp_out.c
index 790a0c005..a5557d09e 100644
--- a/src/src/smtp_out.c
+++ b/src/src/smtp_out.c
@@ -520,7 +520,12 @@ if (ob->socks_proxy)
     return -1;
     }
   if (*ob->socks_proxy)
-    return socks_sock_connect(sc, early_data);
+    {
+    uschar * dummy_errmsg;
+    misc_module_info * mi = misc_mod_find(US"socks", &dummy_errmsg);
+    typedef int (*fn_t) (const smtp_connect_args *, const blob *);
+    return mi ? ((fn_t *) mi->functions)[SOCKS_CONNECT] (sc, early_data) : -1;
+    }
   }
 #endif
 
diff --git a/src/src/transports/Makefile b/src/src/transports/Makefile
index 89cadc568..761cf01c8 100644
--- a/src/src/transports/Makefile
+++ b/src/src/transports/Makefile
@@ -11,12 +11,12 @@
 
 # MAGIC-TAG-MODS-OBJ-RULES-GO-HERE
 
-OBJ += smtp_socks.o tf_maildir.o
+OBJ += tf_maildir.o
 
 all:	transports.a $(MODS)
 	$(FE):
 
-transports.a:    $(OBJ) smtp_socks.o tf_maildir.o
+transports.a:    $(OBJ) tf_maildir.o
 		 @$(RM_COMMAND) -f transports.a
 		 @echo "$(AR) transports.a"
 		 @$(AR) transports.a $(OBJ)
@@ -39,7 +39,6 @@ lmtp.o lmtp.so:			lmtp.c lmtp.h
 pipe.o pipe.so:			pipe.c pipe.h
 queuefile.o queuefile.so:	queuefile.c queuefile.h
 smtp.o:				smtp.c smtp.h
-smtp_socks.o:			smtp_socks.c smtp.h
 
 tf_maildir.o:			tf_maildir.c tf_maildir.h appendfile.h
 
diff --git a/src/src/transports/smtp.h b/src/src/transports/smtp.h
index 506d6094a..72b3b00ba 100644
--- a/src/src/transports/smtp.h
+++ b/src/src/transports/smtp.h
@@ -250,9 +250,4 @@ extern BOOL smtp_transport_entry(transport_instance *, address_item *);
 extern void smtp_transport_closedown(transport_instance *);
 
 
-
-#ifdef SUPPORT_SOCKS
-extern int     socks_sock_connect(smtp_connect_args *, const blob *);
-#endif
-
 /* End of transports/smtp.h */

commit ee143587852f7a6937a6997da730b4e02e427fc4
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Dec 14 18:23:37 2025 +0000

    xclient module

diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index f713ad3ca..9f26eff0f 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -541,8 +541,7 @@ OBJ_WITH_CONTENT_SCAN = malware.o mime.o regex.o spam.o spool_mbox.o
 OBJ_EXPERIMENTAL =	dane.o \
 			dcc.o \
 			imap_utf7.o \
-			utf8.o \
-			xclient.o
+			utf8.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.
@@ -939,7 +938,6 @@ dane.o:		$(HDRS) dane.c dane-openssl.c
 dcc.o:		$(HDRS) dcc.h dcc.c
 imap_utf7.o:	$(HDRS) imap_utf7.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/Configure-Makefile b/src/scripts/Configure-Makefile
index 82e93d2ad..9a7351482 100755
--- a/src/scripts/Configure-Makefile
+++ b/src/scripts/Configure-Makefile
@@ -332,7 +332,7 @@ done <<-END
  routers    ROUTER	ACCEPT DNSLOOKUP IPLITERAL IPLOOKUP MANUALROUTE QUERYPROGRAM REDIRECT
  transports TRANSPORT	APPENDFILE AUTOREPLY LMTP PIPE QUEUEFILE SMTP
  auths	    AUTH	CRAM_MD5 CYRUS_SASL DOVECOT EXTERNAL GSASL HEIMDAL_GSSAPI PLAINTEXT SPA TLS
- miscmods   SUPPORT	ARC _DKIM DMARC DMARC_NATIVE DSCP _EXIM_FILTER PAM PERL RADIUS _SIEVE_FILTER SOCKS SPF SPF_PERL
+ miscmods   SUPPORT	ARC _DKIM DMARC DMARC_NATIVE DSCP _EXIM_FILTER PAM PERL RADIUS _SIEVE_FILTER SOCKS SPF SPF_PERL XCLIENT
 END
 
 # See if there is a definition of EXIM_PERL in what we have built so far.
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index 1875c6963..642cd7302 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -109,7 +109,8 @@ for f in dummy.c \
 	radius.c radius_api.h \
 	sieve_filter.c sieve_filter_api.h \
 	socks.c socks_api.h \
-	spf.c spf_perl.c spf.h spf_api.h
+	spf.c spf_perl.c spf.h spf_api.h \
+	xclient.c xclient_api.h
 do
   ln -s ../../src/$d/$f `basename $f`
 done
@@ -158,7 +159,7 @@ done
 
 # EXPERIMENTAL_*
 for f in  dcc.c dcc.h dane.c dane-openssl.c \
-  danessl.h imap_utf7.c utf8.c xclient.c
+  danessl.h imap_utf7.c utf8.c
 do
   ln -s ../src/$f $f
 done
diff --git a/src/src/EDITME b/src/src/EDITME
index 695cceafd..9e5b25029 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -707,7 +707,8 @@ DISABLE_MAL_MKS=yes
 # Uncomment the following line to add SRV smtps support
 # EXPERIMENTAL_SRV_SMTPS=yes
 #
-# Uncomment the following line to add XCLIENT support
+# Uncomment the following line to add XCLIENT support.
+# Set to 2 for a dynamic-load module.
 # EXPERIMENTAL_XCLIENT=yes
 
 ###############################################################################
diff --git a/src/src/drtables.c b/src/src/drtables.c
index 3c714ba24..4df612c65 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -498,24 +498,28 @@ extern misc_module_info arc_module_info;
 #if defined(SUPPORT_DSCP) && SUPPORT_DSCP!=2
 extern misc_module_info dscp_module_info;
 #endif
-#if defined(RADIUS_CONFIG_FILE) && (!defined(SUPPORT_RADIUS) || SUPPORT_RADIUS!=2)
-extern misc_module_info radius_module_info;
-#endif
 #if defined(SUPPORT_PAM) && SUPPORT_PAM!=2
 extern misc_module_info pam_module_info;
 #endif
 #if defined(EXIM_PERL) && (!defined(SUPPORT_PERL) || SUPPORT_PERL!=2)
 extern misc_module_info perl_module_info;
 #endif
+#if defined(RADIUS_CONFIG_FILE) && (!defined(SUPPORT_RADIUS) || SUPPORT_RADIUS!=2)
+extern misc_module_info radius_module_info;
+#endif
+#if defined(SUPPORT_SOCKS) && SUPPORT_SOCKS!=2
+extern misc_module_info socks_module_info;
+#endif
+#if defined(EXPERIMENTAL_XCLIENT) && EXPERIMENTAL_XCLIENT!=2
+extern misc_module_info xclient_module_info;
+#endif
+
 #if !defined(DISABLE_EXIM_FILTER) && (!defined(SUPPORT_EXIM_FILTER) || SUPPORT_EXIM_FILTER!=2)
 extern misc_module_info exim_filter_module_info;
 #endif
 #if !defined(DISABLE_SIEVE_FILTER) && (!defined(SUPPORT_SIEVE_FILTER) || SUPPORT_SIEVE_FILTER!=2)
 extern misc_module_info sieve_filter_module_info;
 #endif
-#if defined(SUPPORT_SOCKS) && SUPPORT_SOCKS!=2
-extern misc_module_info socks_module_info;
-#endif
 
 void
 init_misc_mod_list(void)
@@ -543,6 +547,9 @@ onetime = TRUE;
 #if defined(SUPPORT_PAM) && SUPPORT_PAM!=2
   misc_mod_add(&pam_module_info);
 #endif
+#if defined(EXPERIMENTAL_XCLIENT) && EXPERIMENTAL_XCLIENT!=2
+  misc_mod_add(&xclient_module_info);
+#endif
 #if defined(EXIM_PERL) && (!defined(SUPPORT_PERL) || SUPPORT_PERL!=2)
   misc_mod_add(&perl_module_info);
 #endif
diff --git a/src/src/exim.h b/src/src/exim.h
index 4bd0a87e7..7b4270cad 100644
--- a/src/src/exim.h
+++ b/src/src/exim.h
@@ -561,9 +561,6 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly.
 #ifdef SUPPORT_DSCP
 # include "miscmods/dscp_api.h"
 #endif
-#ifdef RADIUS_CONFIG_FILE
-# include "miscmods/radius_api.h"
-#endif
 #ifdef SUPPORT_PAM
 # include "miscmods/pam_api.h"
 #endif
@@ -573,6 +570,12 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly.
 #ifdef SUPPORT_SOCKS
 # include "miscmods/socks_api.h"
 #endif
+#ifdef RADIUS_CONFIG_FILE
+# include "miscmods/radius_api.h"
+#endif
+#ifdef EXPERIMENTAL_XCLIENT
+# include "miscmods/xclient_api.h"
+#endif
 #include "miscmods/exim_filter_api.h"
 #include "miscmods/sieve_filter_api.h"
 
diff --git a/src/src/functions.h b/src/src/functions.h
index 4e56c14a4..f07799321 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -706,10 +706,6 @@ extern BOOL    write_chunk(transport_ctx *, const 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
 extern uschar *xtextencode(const uschar *, int);
 extern int     xtextdecode(const uschar *, uschar **);
 
diff --git a/src/src/miscmods/Makefile b/src/src/miscmods/Makefile
index a5fbba441..f731088ba 100644
--- a/src/src/miscmods/Makefile
+++ b/src/src/miscmods/Makefile
@@ -43,14 +43,15 @@ dmarc_native.o dmarc_native.so:	$(HDRS) spf.h pdkim.h dmarc.h \
 				dmarc_native.c dmarc_common.c
 dscp.o  dscp.so:		$(HDRS) dscp.c
 dummy.o:			dummy.c
-exim_filter.o exim_filter.so:	$(HDRS) exim_filter.c
-pam.o   pam.so:			$(HDRS) pam.c
-perl.o perl.so:         	$(HDRS) perl.c
-radius.o radius.so:		$(HDRS) radius.c
-sieve_filter.o sieve_filter.so:	$(HDRS) sieve_filter.c
-socks.o socks.so:		$(HDRS) socks_api.h ../transports/smtp.h socks.c
-spf.o   spf.so:			$(HDRS) spf.h spf.c
-spf_perl.o spf_perl.so:		$(HDRS) spf.h spf_perl.c
+exim_filter.o	exim_filter.so:	$(HDRS) exim_filter.c
+pam.o   	pam.so:		$(HDRS) pam.c
+perl.o		perl.so:       	$(HDRS) perl.c
+radius.o	radius.so:	$(HDRS) radius.c
+sieve_filter.o	sieve_filter.so: $(HDRS) sieve_filter.c
+socks.o		socks.so:	$(HDRS) socks_api.h ../transports/smtp.h socks.c
+spf.o   	spf.so:		$(HDRS) spf.h spf.c
+spf_perl.o	spf_perl.so:	$(HDRS) spf.h spf_perl.c
+xclient.o	xclient.so:	$(HDRS) xclient_api.h xclient.c
 
 
 
diff --git a/src/src/miscmods/xclient.c b/src/src/miscmods/xclient.c
new file mode 100644
index 000000000..77bf743a4
--- /dev/null
+++ b/src/src/miscmods/xclient.c
@@ -0,0 +1,356 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2023 - 2025 */
+/* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+#include "../exim.h"
+
+#ifdef EXPERIMENTAL_XCLIENT
+
+/* This is a proxy protocol.
+
+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(const uschar * code, const uschar * end, uschar ** ptr)
+{
+return 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
+  flagp		input: helo seen  output: fail is fatal
+
+Return: NULL on success, or error message
+*/
+
+# define XCLIENT_UNAVAIL     US"[UNAVAILABLE]"
+# define XCLIENT_TEMPUNAVAIL US"[TEMPUNAVAIL]"
+
+static uschar *
+xclient_smtp_command(const uschar * s, int * resp, BOOL * flagp)
+{
+const uschar * word = s;
+uschar * errmsg = NULL;
+enum {
+  XCLIENT_READ_COMMAND = 0,
+  XCLIENT_READ_VALUE,
+  XCLIENT_SKIP_SPACES
+} state = XCLIENT_SKIP_SPACES;
+enum xclient_cmd_e cmd;
+static BOOL xc_proxy_session = FALSE;
+
+if (  !*flagp
+   && verify_check_host(&hosts_require_helo) == OK)
+  {
+  *resp = 503;
+  *flagp= FALSE;
+  return US"no HELO/EHLO given";
+  }
+
+/* If already in a proxy session, do not re-check permission. */
+
+if (!xc_proxy_session && verify_check_host(&hosts_xclient) == FAIL)
+  {
+  *resp = 501;
+  *flagp= TRUE;
+  return US"XCLIENT command used when not advertised";
+  }
+
+if (sender_address)
+  {
+  *resp = 503;
+  *flagp= FALSE;
+  return US"mail transaction in progress";
+  }
+
+if (!*word)
+  {
+  errmsg = 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)
+	{
+	errmsg = 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)
+	{
+	errmsg = 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 = */
+      Uskip_nonwhite(&s);
+      len = s - word;
+
+      DEBUG(D_transport) debug_printf(" XCLIENT: \tvalue %.*s\n", len, word);
+      if (len == 0)
+	{ errmsg = 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)
+	{
+	errmsg = 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; errmsg = 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; errmsg = US"bad proto for XCLIENT"; goto fatal_501; }
+	  break;
+# endif
+	default: break;	/* stupid compiler silencing */
+	}
+      store_pool = old_pool;
+      state = XCLIENT_SKIP_SPACES;
+      break;
+      }
+
+    case XCLIENT_SKIP_SPACES:
+      Uskip_whitespace(&s);
+      state = XCLIENT_READ_COMMAND;
+      break;
+
+    default:
+      errmsg = US"unhandled XCLIENT parameter type";
+      goto fatal_501;
+    }
+
+if (!proxy_local_address)
+  { errmsg = US"missing ADDR for XCLIENT"; goto fatal_501; }
+if (!proxy_local_port)
+  { errmsg = US"missing PORT for XCLIENT"; goto fatal_501; }
+if (state != XCLIENT_SKIP_SPACES)
+  { errmsg = US"bad state parsing XCLIENT parameters"; goto fatal_501; }
+
+host_build_sender_fullhost();
+proxy_session = TRUE;		/* global */
+xc_proxy_session = TRUE;	/* local */
+*resp = 220;
+return NULL;
+
+fatal_501:
+  *flagp= TRUE;
+  *resp = 501;
+  return errmsg;
+}
+
+# undef XCLIENT_UNAVAIL
+# undef XCLIENT_TEMPUNAVAIL
+
+
+/*API*/
+static 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);
+}
+
+
+/*API
+Return: true if the cmd was good and the protocol started
+*/
+
+static int
+xclient_protocol_start(const uschar * smtp_cmd_data, int * donep,
+  BOOL helo_seen)
+{
+BOOL fatal = helo_seen;
+uschar * errmsg;
+int resp;
+
+if (!(errmsg = xclient_smtp_command(smtp_cmd_data, &resp, &fatal)))
+  {
+  /*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("%d XCLIENT success\r\n", SP_NO_MORE, resp);
+  return TRUE;
+  }
+else if (fatal)
+  *donep = synprot_error(L_smtp_syntax_error, resp, NULL, errmsg);
+else
+  {
+  smtp_printf("%d %s\r\n", SP_NO_MORE, resp, errmsg);
+  log_write(0, LOG_MAIN|LOG_REJECT, "rejected XCLIENT from %s: %s",
+    host_and_ident(FALSE), errmsg);
+  }
+  
+return FALSE;
+}
+
+/******************************************************************************/
+/* Module API */
+
+static void * xclient_functions[] = {
+  [XCLIENT_PROTO_ADVERTISE] =	(void *) xclient_smtp_advertise_str,
+  [XCLIENT_PROTO_START] =	(void *) xclient_protocol_start,
+};
+
+misc_module_info xclient_module_info =
+{
+  .name =		US"xclient",
+# ifdef DYNLOOKUP
+  .dyn_magic =		MISC_MODULE_MAGIC,
+# endif
+  .functions =		xclient_functions,
+  .functions_count =	nelem(xclient_functions),
+};
+#endif	/*EXPERIMENTAL_XCLIENT*/
+
+/* vi: aw ai sw=2
+*/
+/* End of xclient.c */
+
diff --git a/src/src/miscmods/xclient_api.h b/src/src/miscmods/xclient_api.h
new file mode 100644
index 000000000..e59296395
--- /dev/null
+++ b/src/src/miscmods/xclient_api.h
@@ -0,0 +1,15 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2025 */
+/* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/* API definitions for the proxy-protocol module */
+
+
+/* Function table entry numbers */
+
+#define	XCLIENT_PROTO_ADVERTISE	0
+#define	XCLIENT_PROTO_START	1
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index d694c8d7a..222fc5b93 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -3811,6 +3811,9 @@ smtp_setup_msg(void)
 int done = 0;
 BOOL toomany = FALSE, discarded = FALSE, last_was_rej_mail = FALSE,
     last_was_rcpt = FALSE;
+#ifdef EXPERIMENTAL_XCLIENT
+static misc_module_info * xclient_mi = NULL;
+#endif
 rmark reset_point = store_mark();
 
 DEBUG(D_receive) debug_printf("smtp_setup_msg entered\n");
@@ -4433,8 +4436,17 @@ while (done <= 0)
 #ifdef EXPERIMENTAL_XCLIENT
 	if (proxy_session || verify_check_host(&hosts_xclient) != FAIL)
 	  {
+	  uschar * dummy_errmsg;
+	  typedef gstring * (*fn_t) (gstring *);
+
+	  if (  !xclient_mi
+	     && !(xclient_mi = misc_mod_find(US"xclient", &dummy_errmsg)))
+	    {
+	    smtp_closedown(US"Temporary local problem - please try later");
+	    return FALSE;
+	    }
 	  g = string_catn(g, smtp_code, 3);
-	  g = xclient_smtp_advertise_str(g);
+	  g = ((fn_t *) xclient_mi->functions)[XCLIENT_PROTO_ADVERTISE] (g);
 	  }
 #endif
 #ifndef DISABLE_PRDR
@@ -4516,37 +4528,21 @@ while (done <= 0)
 
 #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", SP_NO_MORE, resp, errmsg);
-	  log_write(0, LOG_MAIN|LOG_REJECT, "rejected XCLIENT from %s: %s",
-	    host_and_ident(FALSE), errmsg);
-	  }
+      if (!xclient_mi)
+	done = synprot_error(L_smtp_syntax_error, 501, NULL,
+	  US"XCLIENT command used when not advertised");
       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", SP_NO_MORE, smtp_code);
+	typedef BOOL (*fn_t) (const uschar *, int *, BOOL);
+	BOOL started = ((fn_t *) xclient_mi->functions)[XCLIENT_PROTO_START]
+					  (smtp_cmd_data, &done, fl.helo_seen);
+	if (started)
+	  fl.helo_seen = FALSE;			/* Require another EHLO */
 	}
       break; /* XCLIENT */
-      }
 #endif
 
 

commit fda16457fbba6a11abdb09370ce21ea09f9e64f7
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Dec 14 15:32:48 2025 +0000

    proxy-protocol module

diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index 9f26eff0f..ff733b490 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -557,7 +557,7 @@ OBJ_EXIM = acl.o atrn.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 \
         filtertest.o globals.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 proxy.o queue.o \
+        os.o parse.o priv.o queue.o \
         rda.o readconf.o receive.o retry.o rewrite.o rfc2047.o regex_cache.o \
         route.o search.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 \
@@ -895,7 +895,6 @@ 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/Configure-Makefile b/src/scripts/Configure-Makefile
index 9a7351482..84a8abbb2 100755
--- a/src/scripts/Configure-Makefile
+++ b/src/scripts/Configure-Makefile
@@ -332,7 +332,7 @@ done <<-END
  routers    ROUTER	ACCEPT DNSLOOKUP IPLITERAL IPLOOKUP MANUALROUTE QUERYPROGRAM REDIRECT
  transports TRANSPORT	APPENDFILE AUTOREPLY LMTP PIPE QUEUEFILE SMTP
  auths	    AUTH	CRAM_MD5 CYRUS_SASL DOVECOT EXTERNAL GSASL HEIMDAL_GSSAPI PLAINTEXT SPA TLS
- miscmods   SUPPORT	ARC _DKIM DMARC DMARC_NATIVE DSCP _EXIM_FILTER PAM PERL RADIUS _SIEVE_FILTER SOCKS SPF SPF_PERL XCLIENT
+ miscmods   SUPPORT	ARC _DKIM DMARC DMARC_NATIVE DSCP _EXIM_FILTER PAM PERL PROXY RADIUS _SIEVE_FILTER SOCKS SPF SPF_PERL XCLIENT
 END
 
 # See if there is a definition of EXIM_PERL in what we have built so far.
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index 642cd7302..5219d3dbc 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -106,6 +106,7 @@ for f in dummy.c \
 	exim_filter.c exim_filter_api.h \
 	pam.c pam_api.h \
 	perl.c perl_api.h \
+	proxy.c proxy_api.h \
 	radius.c radius_api.h \
 	sieve_filter.c sieve_filter_api.h \
 	socks.c socks_api.h \
@@ -139,7 +140,7 @@ for f in blob.h dbfunctions.h exim.h functions.h globals.h \
   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 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 priv.c proxy.c queue.c rda.c readconf.c receive.c \
+  md5.c moan.c parse.c priv.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 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/EDITME b/src/src/EDITME
index 9e5b25029..8977f624b 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -1137,7 +1137,7 @@ ZCAT_COMMAND=/usr/bin/zcat
 # SUPPORT_SOCKS=yes
 
 # If you may want to use inbound (server-side) proxying, using Proxy Protocol,
-# uncomment the line below.
+# uncomment the line below.  To build as a dynamic module set it to 2.
 
 # SUPPORT_PROXY=yes
 
diff --git a/src/src/drtables.c b/src/src/drtables.c
index 4df612c65..833c352c0 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -504,6 +504,9 @@ extern misc_module_info pam_module_info;
 #if defined(EXIM_PERL) && (!defined(SUPPORT_PERL) || SUPPORT_PERL!=2)
 extern misc_module_info perl_module_info;
 #endif
+#if defined(SUPPORT_PROXY) && SUPPORT_PROXY!=2
+extern misc_module_info proxy_module_info;
+#endif
 #if defined(RADIUS_CONFIG_FILE) && (!defined(SUPPORT_RADIUS) || SUPPORT_RADIUS!=2)
 extern misc_module_info radius_module_info;
 #endif
@@ -553,6 +556,9 @@ onetime = TRUE;
 #if defined(EXIM_PERL) && (!defined(SUPPORT_PERL) || SUPPORT_PERL!=2)
   misc_mod_add(&perl_module_info);
 #endif
+#if defined(SUPPORT_PROXY) && SUPPORT_PROXY!=2
+  misc_mod_add(&proxy_module_info);
+#endif
 #if !defined(DISABLE_EXIM_FILTER) && (!defined(SUPPORT_EXIM_FILTER) || SUPPORT_EXIM_FILTER!=2)
   misc_mod_add(&exim_filter_module_info);
 #endif
diff --git a/src/src/exim.h b/src/src/exim.h
index 7b4270cad..70911b0f8 100644
--- a/src/src/exim.h
+++ b/src/src/exim.h
@@ -567,6 +567,9 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly.
 #ifdef EXIM_PERL
 # include "miscmods/perl_api.h"
 #endif
+#ifdef SUPPORT_PROXY
+# include "miscmods/proxy_api.h"
+#endif
 #ifdef SUPPORT_SOCKS
 # include "miscmods/socks_api.h"
 #endif
diff --git a/src/src/functions.h b/src/src/functions.h
index f07799321..b48103922 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -407,10 +407,6 @@ const misc_module_info * perl_startup(const uschar *);
 
 extern void priv_drop_temp(const uid_t, const gid_t);
 extern void priv_restore(void);
-#ifdef SUPPORT_PROXY
-extern BOOL	proxy_protocol_host(void);
-extern void	proxy_protocol_setup(void);
-#endif
 
 extern BOOL    queue_action(const uschar *, int, const uschar **, int, int);
 extern void    queue_check_only(void);
diff --git a/src/src/globals.c b/src/src/globals.c
index 6d2b27227..2d1354236 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -286,15 +286,15 @@ struct global_flags f =
 	.parse_allow_group      = FALSE,
 	.parse_found_group      = FALSE,
 	.pipelining_enable      = TRUE,
-#if defined(SUPPORT_PROXY) || defined(SUPPORT_SOCKS)
-	.proxy_session_failed   = FALSE,
-#endif
 
 	.queue_2stage           = FALSE,
 	.queue_only_policy      = FALSE,
 	.queue_run_local        = FALSE,
 	.queue_running          = FALSE,
 	.queue_smtp             = FALSE,
+#if defined(SUPPORT_PROXY)
+	.quit_cmd_only		= FALSE,
+#endif
 
 	.really_exim            = TRUE,
 	.receive_call_bombout   = FALSE,
diff --git a/src/src/globals.h b/src/src/globals.h
index 977107f58..52c46f169 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -256,15 +256,15 @@ extern struct global_flags {
  BOOL   parse_allow_group		:1; /* Allow group syntax */
  BOOL   parse_found_group		:1; /* In the middle of a group */
  BOOL   pipelining_enable		:1; /* As it says */
-#if defined(SUPPORT_PROXY) || defined(SUPPORT_SOCKS)
- BOOL   proxy_session_failed		:1; /* TRUE if required proxy negotiation failed */
-#endif
 
  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_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)*/
+#ifdef SUPPORT_PROXY
+ BOOL   quit_cmd_only			:1; /* TRUE if required proxy negotiation failed */
+#endif
 
  BOOL   really_exim			:1; /* FALSE in utilities */
  BOOL   receive_call_bombout		:1; /* Flag for crashing log */
diff --git a/src/src/miscmods/Makefile b/src/src/miscmods/Makefile
index f731088ba..1076c6daf 100644
--- a/src/src/miscmods/Makefile
+++ b/src/src/miscmods/Makefile
@@ -46,6 +46,7 @@ dummy.o:			dummy.c
 exim_filter.o	exim_filter.so:	$(HDRS) exim_filter.c
 pam.o   	pam.so:		$(HDRS) pam.c
 perl.o		perl.so:       	$(HDRS) perl.c
+proxy.o		proxy.so:	$(HDRS) proxy_api.h proxy.c
 radius.o	radius.so:	$(HDRS) radius.c
 sieve_filter.o	sieve_filter.so: $(HDRS) sieve_filter.c
 socks.o		socks.so:	$(HDRS) socks_api.h ../transports/smtp.h socks.c
diff --git a/src/src/proxy.c b/src/src/miscmods/proxy.c
similarity index 95%
rename from src/src/proxy.c
rename to src/src/miscmods/proxy.c
index c8230b009..320b57ea8 100644
--- a/src/src/proxy.c
+++ b/src/src/miscmods/proxy.c
@@ -11,9 +11,17 @@
 *            Proxy-Protocol support             *
 ************************************************/
 
-#include "exim.h"
+#include "../exim.h"
 
 #ifdef SUPPORT_PROXY
+
+
+static void
+command_timeout_handler(int sig)
+{
+had_command_timeout = sig;
+}
+
 /*************************************************
 *       Check if host is required proxy host     *
 *************************************************/
@@ -25,7 +33,7 @@ Arguments: none
 Returns:   boolean for Proxy Protocol needed
 */
 
-BOOL
+static BOOL
 proxy_protocol_host(void)
 {
 if (  sender_host_address
@@ -34,6 +42,7 @@ if (  sender_host_address
   {
   DEBUG(D_receive)
     debug_printf("Detected proxy protocol configured host\n");
+  /* having this set when we could still fail is ugly */
   proxy_session = TRUE;
   }
 return proxy_session;
@@ -114,6 +123,7 @@ debug_printf("PROXY<<%3.*H\n", (int)(end - start), buf + start);
 }
 
 
+/*API*/
 /*************************************************
 *         Setup host for proxy protocol          *
 *************************************************/
@@ -123,11 +133,11 @@ 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
+Returns:   TRUE iff success
 */
 
-void
-proxy_protocol_setup(void)
+static BOOL
+proxy_protocol(void)
 {
 union {
   struct {
@@ -201,6 +211,10 @@ const char v2sig[12] = "\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A";
 uschar * iptype;  /* To display debug info */
 BOOL yield = FALSE;
 
+if (!proxy_protocol_host())
+  return TRUE;
+
+os_non_restarting_signal(SIGALRM, command_timeout_handler);
 ALARM(proxy_protocol_timeout);
 
 do
@@ -496,6 +510,8 @@ done:
 should cause a synchronization failure */
 
 proxyfail:
+
+  ALARM(0);
   DEBUG(D_receive) if (had_command_timeout)
     debug_printf("Timeout while reading proxy header\n");
 
@@ -506,15 +522,29 @@ proxyfail:
     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;
+return yield;
 }
+
+
+/******************************************************************************/
+/* Module API */
+
+static void * proxy_functions[] = {
+  [PROXY_PROTO_START] =	(void *) proxy_protocol,
+};
+
+misc_module_info proxy_module_info =
+{
+  .name =		US"proxy",
+# ifdef DYNLOOKUP
+  .dyn_magic =		MISC_MODULE_MAGIC,
+# endif
+  .functions =		proxy_functions,
+  .functions_count =	nelem(proxy_functions),
+};
 #endif	/*SUPPORT_PROXY*/
 
 /* vi: aw ai sw=2
diff --git a/src/src/miscmods/proxy_api.h b/src/src/miscmods/proxy_api.h
new file mode 100644
index 000000000..4cd57908c
--- /dev/null
+++ b/src/src/miscmods/proxy_api.h
@@ -0,0 +1,14 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2025 */
+/* See the file NOTICE for conditions of use and distribution. */
+/* SPDX-License-Identifier: GPL-2.0-or-later */
+
+/* API definitions for the proxy-protocol module */
+
+
+/* Function table entry numbers */
+
+#define	PROXY_PROTO_START	0
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 222fc5b93..3ffb6fe8b 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -1308,8 +1308,8 @@ if required. */
 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 */
-  if (proxy_session && f.proxy_session_failed && p->cmd != QUIT_CMD)
+  /* If Proxy Protocol parsing failed, only allow QUIT command */
+  if (f.quit_cmd_only && p->cmd != QUIT_CMD)
     continue;
 #endif
   if (  p->len
@@ -1363,8 +1363,8 @@ 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 */
-if (proxy_session && f.proxy_session_failed)
+/* If Proxy Protocol parsing failed, only allow QUIT command */
+if (f.quit_cmd_only)
   return PROXY_FAIL_IGNORE_CMD;
 #endif
 
@@ -2668,11 +2668,20 @@ proxy_session = FALSE;
 /* If valid Proxy Protocol source is connecting, set up session.
 Failure will not allow any SMTP function other than QUIT. */
 
-f.proxy_session_failed = FALSE;
-if (proxy_protocol_host())
+f.quit_cmd_only = FALSE;
+if (hosts_proxy)
   {
-  os_non_restarting_signal(SIGALRM, command_timeout_handler);
-  proxy_protocol_setup();
+  uschar * dummy_errmsg;
+  misc_module_info * mi;
+  typedef BOOL (*fn_t) (void);
+
+  if (!(mi = misc_mod_find(US"proxy", &dummy_errmsg)))
+    {
+    smtp_closedown(US"Temporary local problem - please try later");
+    return FALSE;
+    }
+  if (!((fn_t *) mi->functions)[PROXY_PROTO_START] ())
+    f.quit_cmd_only = TRUE;
   }
 #endif
 

commit 53e771b0ddaa89477793c8955ed474acf42c32b5
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Dec 18 10:50:33 2025 +0000

    Docs: more notes on DBM lookups

diff --git a/src/src/dbfn.c b/src/src/dbfn.c
index 7c2dbcce3..3ed78901b 100644
--- a/src/src/dbfn.c
+++ b/src/src/dbfn.c
@@ -381,6 +381,16 @@ no guarantee of alignment. Since all the records used by Exim need
 to be properly aligned to pick out the timestamps, etc., we might as
 well do the copying centrally here.
 
+Callers:
+	dbfn_read_with_length		(below)
+	  dbfn_read			dbfunctions.h
+	   (assorted hintsdb uses)
+	  (assorted hintsdb uses)
+	  dbfn_read_enforce_length	(below)
+	    (assorted hintsdb uses)
+	  exim_dbutil.c			utils
+	lookups/dbmdb.c			lookup dbm
+
 Arguments:
   dbblock   a pointer to an open database block
   key       the key of the record to be read
diff --git a/src/src/hintsdb/hints_sqlite.h b/src/src/hintsdb/hints_sqlite.h
index dd88dd98b..1772a85c7 100644
--- a/src/src/hintsdb/hints_sqlite.h
+++ b/src/src/hintsdb/hints_sqlite.h
@@ -187,6 +187,11 @@ return more;
 }
 
 /* EXIM_DBGET - returns the value associated with the key. */
+/* Callers:
+	dbfn_read_klen			lookup-dbm, hints general
+	autoreply_transport_entry	autoreply tpt
+	dbfn_read_with_length		utils
+*/
 static inline BOOL
 exim_dbget(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * res)
 {
@@ -208,6 +213,12 @@ return (SQLITE_DONE == exim_sqlprep_step(dbp, sql, key, data, NULL )) ? EXIM_DBP
 }
 
 /* EXIM_DBPUT - returns nothing useful, assumes replace mode */
+/* Callers:
+	dbfn_write				hints general
+	autoreply_transport_entry		autoreply tpt
+	main(dbmbuild)				utils
+	dbfn_write				utils
+*/
 static inline int
 exim_dbput(EXIM_DB * dbp, EXIM_DATUM * key, EXIM_DATUM * data)
 {
@@ -229,6 +240,10 @@ return exim_s_dbp(dbp, key, data, "INSERT OR ABORT INTO tblblob (ky, dat) VALUES
 }
 
 /* EXIM_DBDEL */
+/* Callers
+	dbfn_delete			hints general
+	dnfn_delete			utils
+*/
 static inline int
 exim_dbdel(EXIM_DB * dbp, EXIM_DATUM * key)
 {

commit a51a01811d1b0984603662609a6b7f0845b34387
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Nov 28 12:45:10 2025 +0000

    exim_dumpdb for lookup-dbm files

diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index 42396fe8b..be1cf710e 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -30,25 +30,23 @@ There are a number of common subroutines, followed by three main programs,
 whose inclusion is controlled by -D on the compilation command. */
 
 
+#include <libgen.h>
 #include "exim.h"
 #include "hintsdb.h"
 
 
 /* Identifiers for the different database types. */
 
-#define type_retry     1
-#define type_wait      2
-#define type_misc      3
-#define type_callout   4
-#define type_ratelimit 5
-#define type_tls       6
-#define type_seen      7
-
+enum dbtype { type_retry = 1, type_wait, type_misc, type_callout,
+	      type_ratelimit, type_tls, type_seen, type_dbm
+};
 
 /* This is used by our cut-down dbfn_open(). */
 
-uschar *spool_directory;
+uschar * spool_directory = NULL;
+const uschar * lockfile_name = NULL;
 
+BOOL dbmdb = FALSE;
 BOOL keyonly = FALSE;
 BOOL utc = FALSE;
 
@@ -110,7 +108,15 @@ sigalrm_seen = 1;
 static void
 usage(const uschar * name, const uschar * options)
 {
-printf("Usage: exim_%s%s  <spool-directory> <database-name>\n", name, options);
+uschar * s = store_get(Ustrlen(options), options), * t = s;
+
+if (Ustrchr(options, 'L'))
+  printf("Usage: exim_%s -L <database-path>\n\n", name);
+
+/* drop the L from options */
+do { if (*options != 'L') *t++ = *options; } while (*options++);
+
+printf("Usage: exim_%s%s  <spool-directory> <database-name>\n", name, s);
 printf("  <database-name> = retry | misc | wait-<transport-name> | callout | ratelimit | tls | seen\n");
 exit(EXIT_FAILURE);
 }
@@ -146,6 +152,8 @@ second of them to be sure it is a known database name. */
 static int
 check_args(int argc, uschar **argv, const uschar * name, const uschar * options)
 {
+if (dbmdb && argc - optind == 1) return type_dbm;
+
 if (argc - optind == 2)
   {
   const uschar * aname = argv[optind + 1];
@@ -172,9 +180,10 @@ opterr = 0;
 while ((opt = getopt(argc, (char * const *)argv, CCS opts)) != -1)
   switch (opt)
   {
+  case 'L':	dbmdb = TRUE; break;
   case 'k':	keyonly = TRUE; break;
   case 'z':	utc = TRUE; break;
-  default:	usage(name, US" [-z] [-k]");
+  default:	usage(name, US" [-kLz]");
   }
 }
 
@@ -322,29 +331,40 @@ dbfn_open(const uschar * name, int flags, open_db * dbblock,
 {
 int rc;
 struct flock lock_data;
-uschar * dirname, * filename;
+uschar * dname, * filename;
 
 /* The first thing to do is to open a separate file on which to lock. This
 ensures that Exim has exclusive use of the database before it even tries to
 open it. If there is a database, there should be a lock file in existence;
-if no lockfile we infer there is no database and error out.  We open the
+if no lockfile and a create fails, we assume it was the spooldir perms (and
+we have no write perms) and there is no database - and error out.  We open the
 lockfile using the r/w mode requested for the DB, users lacking permission
 for the DB access mode will error out here. */
 
-if (  asprintf(CSS &dirname, "%s/db", spool_directory) < 0
-   || asprintf(CSS &filename, "%s/%s.lockfile", dirname, name) < 0)
+if (dbmdb)
+  {
+  if (asprintf(CSS &filename, "%s.lockfile", name) < 0)
+    return NULL;
+  dname = Ustrchr(name, '/')		/* path has dirs */
+    ? US dirname(CS string_copy(name))
+    : US ".";
+  }
+else if (  asprintf(CSS &dname, "%s/db", spool_directory) < 0
+	|| asprintf(CSS &filename, "%s/%s.lockfile", dname, name) < 0)
   return NULL;
 
 dbblock->readonly = (flags & O_ACCMODE) == O_RDONLY;
 dbblock->lockfd = -1;
 if (exim_lockfile_needed())
   {
-  if ((dbblock->lockfd = Uopen(filename, flags, 0)) < 0)
+  if ((dbblock->lockfd = Uopen(filename, dbmdb ? flags | O_CREAT : flags,
+			      0600)) < 0)
     {
     printf("** Failed to open database lock file %s: %s\n", filename,
       strerror(errno));
     return NULL;
     }
+  lockfile_name = string_copy(filename);
 
   /* Now we must get a lock on the opened lock file; do this with a blocking
   lock that times out. */
@@ -366,6 +386,7 @@ if (exim_lockfile_needed())
       filename,
       errno == ETIMEDOUT ? "timed out" : strerror(errno));
     (void)close(dbblock->lockfd);
+    unlink(CS filename);
     return NULL;
     }
 
@@ -373,11 +394,14 @@ if (exim_lockfile_needed())
   exclusive access to the database, so we can go ahead and open it. */
   }
 
-if (asprintf(CSS &filename, "%s/%s", dirname, name) < 0) return NULL;
+if ((dbmdb
+    ? asprintf(CSS &filename, "%s", name)
+    : asprintf(CSS &filename, "%s/%s", dname, name)
+    ) < 0) return NULL;
 
 if (!(dbblock->dbptr = dbblock->readonly && !exim_lockfile_needed()
-		      ? exim_dbopen_multi(filename, dirname, flags, 0)
-		      : exim_dbopen(filename, dirname, flags, 0)))
+		      ? exim_dbopen_multi(filename, dname, flags, 0)
+		      : exim_dbopen(filename, dname, flags, 0)))
   {
   printf("** Failed to open hintsdb file %s for %s: %s%s\n", filename,
     dbblock->readonly ? "reading" : "writing", strerror(errno),
@@ -388,6 +412,7 @@ if (!(dbblock->dbptr = dbblock->readonly && !exim_lockfile_needed()
 #endif
     );
   if (dbblock->lockfd >= 0) (void)close(dbblock->lockfd);
+  unlink(CCS lockfile_name);
   return NULL;
   }
 
@@ -590,26 +615,33 @@ return yield;
 *************************************************/
 
 int
-main(int argc, char **cargv)
+main(int argc, char ** cargv)
 {
-int dbdata_type = 0;
-int yield = 0;
+int dbdata_type = 0, yield = 0;
 open_db dbblock;
-open_db *dbm;
-EXIM_CURSOR *cursor;
-uschar **argv = USS cargv;
+open_db * dbm;
+EXIM_CURSOR * cursor;
+uschar ** argv = USS cargv;
+const uschar * file;
 uschar keybuffer[1024];
 
 store_init();
-options(argc, argv, US"dumpdb", US"kz");
+options(argc, argv, US"dumpdb", US"kLz");
 
 /* Check the arguments, and open the database */
 
-dbdata_type = check_args(argc, argv, US"dumpdb", US" [-z] [-k]");
+dbdata_type = check_args(argc, argv, US"dumpdb", US" [-kLz]");
 argc -= optind; argv += optind;
-spool_directory = argv[0];
 
-if (!(dbm = dbfn_open(argv[1], O_RDONLY, &dbblock, FALSE, TRUE)))
+if (dbmdb)
+  file = argv[0];
+else
+  {
+  spool_directory = argv[0];
+  file = argv[1];
+  }
+
+if (!(dbm = dbfn_open(file, O_RDONLY, &dbblock, FALSE, TRUE)))
   exit(EXIT_FAILURE);
 
 /* Scan the file, formatting the information for each entry. Note
@@ -772,11 +804,15 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 	seen = (dbdata_seen *)value;
 	printf("%s\t%s\n", keybuffer, print_time(seen->time_stamp));
 	break;
+
+      case type_dbm:
+	printf("%s\t%s\n", keybuffer, value);
       }
   store_reset(reset_point);
   }
 
 dbfn_close(dbm);
+unlink(CCS lockfile_name);
 return yield;
 }
 

commit e76fd60f5560db45225a4e2a35c5c4a27217464b
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Nov 29 12:56:09 2025 +0000

    exim_fixdb for lookup-dbm files

diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index ff733b490..9524dcef9 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -586,7 +586,7 @@ exim:   buildlookups buildauths buildrouters buildtransports buildmisc \
 
 # The utility for dumping the contents of an exim database
 
-OBJ_DUMPDB = exim_dumpdb.o util-os.o util-store.o util-xtextencode.o
+OBJ_DUMPDB = exim_dumpdb.o util-os.o util-store.o util-string.o
 
 exim_dumpdb: $(OBJ_DUMPDB)
 	@echo "$(LNCC) -o exim_dumpdb"
@@ -601,7 +601,7 @@ exim_dumpdb: $(OBJ_DUMPDB)
 
 # The utility for interrogating/fixing the contents of an exim database
 
-OBJ_FIXDB = exim_fixdb.o util-os.o util-store.o util-md5.o util-xtextencode.o
+OBJ_FIXDB = exim_fixdb.o util-os.o util-store.o util-md5.o
 
 exim_fixdb:  $(OBJ_FIXDB)
 	@echo "$(LNCC) -o exim_fixdb"
@@ -616,7 +616,7 @@ exim_fixdb:  $(OBJ_FIXDB)
 
 # The utility for tidying the contents of an exim database
 
-OBJ_TIDYDB = exim_tidydb.o util-os.o util-store.o util-xtextencode.o
+OBJ_TIDYDB = exim_tidydb.o util-os.o util-store.o
 
 exim_tidydb: $(OBJ_TIDYDB)
 	@echo "$(LNCC) -o exim_tidydb"
@@ -632,7 +632,7 @@ exim_tidydb: $(OBJ_TIDYDB)
 
 # The utility for building dbm files
 
-OBJ_DBMBUILD = exim_dbmbuild.o util-xtextencode.o
+OBJ_DBMBUILD = exim_dbmbuild.o
 
 exim_dbmbuild: $(OBJ_DBMBUILD)
 	@echo "$(LNCC) -o exim_dbmbuild"
@@ -682,7 +682,6 @@ OBJ_MONBIN = util-host_address.o \
 	     util-string.o \
 	     util-tod.o \
 	     util-tree.o \
-	     util-xtextencode.o \
 	     $(MONBIN)
 
 eximon.bin: $(EXIMON_EDITME) eximon $(OBJ_MONBIN) ../exim_monitor/em_version.c \
@@ -843,10 +842,6 @@ util-tree.o:   $(HDRS) tree.c
 	@echo "$(CC) -DCOMPILE_UTILITY tree.c"
 	$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) -DCOMPILE_UTILITY -o util-tree.o tree.c
 
-util-xtextencode.o:   $(HDRS) xtextencode.c
-	@echo "$(CC) -DCOMPILE_UTILITY xtextencode.c"
-	$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) -DCOMPILE_UTILITY -o util-xtextencode.o xtextencode.c
-
 util-os.o:       $(HDRS) os.c
 	@echo "$(CC) -DCOMPILE_UTILITY os.c"
 	$(FE)$(CC) -c $(CFLAGS) $(INCLUDE) \
diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index be1cf710e..26dca7dbd 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -52,7 +52,7 @@ BOOL utc = FALSE;
 
 
 /******************************************************************************/
-      /* dummies needed by Solaris build */
+	/* dummies needed by Solaris build */
 void
 millisleep(int msec)
 {}
@@ -61,20 +61,6 @@ readconf_printtime(int t)
 { return NULL; }
 const uschar * expand_string_2(const uschar * string, BOOL * textonly_p)
 { 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; }
-uschar *
-string_sprintf_trc(const char * fmt, const uschar * func, unsigned line, ...)
-{ return NULL; }
-BOOL
-string_format_trc(uschar * buf, int len, const uschar * func, unsigned line,
-  const char * fmt, ...)
-{ return FALSE; }
 const uschar * parse_find_address_end_gen(const uschar * s, BOOL b)
 {return NULL; }
 
@@ -442,7 +428,9 @@ else
   exim_dbclose(dbp->dbptr);
 
 if (dbp->lockfd >= 0)
-  (void) close(dbp->lockfd);
+  { (void) close(dbp->lockfd); dbp->lockfd = -1; }
+unlink(CCS lockfile_name);
+lockfile_name = NULL;
 }
 
 
@@ -516,15 +504,13 @@ Returns:    the yield of the underlying dbm or db "write" function. If this
 */
 
 int
-dbfn_write(open_db *dbblock, const uschar *key, void *ptr, int length)
+dbfn_write(open_db * dbblock, const uschar * key, void * ptr, int length)
 {
 EXIM_DATUM key_datum, value_datum;
-dbdata_generic *gptr = (dbdata_generic *)ptr;
 int klen = Ustrlen(key) + 1;
 uschar * key_copy = store_get(klen, key);
 
 memcpy(key_copy, key, klen);
-gptr->time_stamp = time(NULL);
 
 exim_datum_init(&key_datum);         /* Some DBM libraries require the datum */
 exim_datum_init(&value_datum);       /* to be cleared before use. */
@@ -738,9 +724,18 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 	break;
 
       case type_misc:
-	printf("%s %s\n", print_time(((dbdata_generic *)value)->time_stamp),
-	  keybuffer);
+	/* It might be nice to recognize the subtypes of "misc" DBs (by the
+	key prefix?) for better decode.  For now, dump the raw data. */
+
+	{
+	dbdata_generic * recp = (dbdata_generic *)value;
+	uschar * dp = US (recp + 1);
+	int dlen = length = sizeof(dbdata_generic);
+
+	printf("%s %s\n", print_time(recp->time_stamp), keybuffer);
+	printf("%s\n", string_sprintf("%.*q\n", dlen, dp));
 	break;
+	}
 
       case type_callout:
 	callout = (dbdata_callout_cache *)value;
@@ -806,13 +801,12 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 	break;
 
       case type_dbm:
-	printf("%s\t%s\n", keybuffer, value);
+	printf("%s\t%.*s\n", keybuffer, length, value);
       }
   store_reset(reset_point);
   }
 
 dbfn_close(dbm);
-unlink(CCS lockfile_name);
 return yield;
 }
 
@@ -860,22 +854,30 @@ int dbdata_type;
 uschar **argv = USS cargv;
 uschar buffer[256];
 uschar name[256];
+const uschar * file;
 rmark reset_point;
-uschar * aname;
 
 store_init();
-options(argc, argv, US"fixdb", US"z");
-name[0] = 0;  /* No name set */
+options(argc, argv, US"fixdb", US"Lz");
+*name = '\0';  /* No name set */
 
 /* Sort out the database type, verify what we are working on and then process
 user requests */
 
-dbdata_type = check_args(argc, argv, US"fixdb", US" [-z]");
+dbdata_type = check_args(argc, argv, US"fixdb", US" [-Lz]");
 argc -= optind; argv += optind;
-spool_directory = argv[0];
-aname = argv[1];
 
-printf("Modifying Exim hints database %s/db/%s\n", spool_directory, aname);
+if (dbmdb)
+  {
+  file = argv[0];
+  printf("Modifying Exim dbm database %s\n", file);
+  }
+else
+  {
+  spool_directory = argv[0];
+  file = argv[1];
+  printf("Modifying Exim hints database %s/db/%s\n", spool_directory, file);
+  }
 
 for(; (reset_point = store_mark()); store_reset(reset_point))
   {
@@ -888,9 +890,8 @@ for(; (reset_point = store_mark()); store_reset(reset_point))
   dbdata_ratelimit *ratelimit;
   dbdata_ratelimit_unique *rate_unique;
   dbdata_tls_session *session;
-  int oldlength;
-  uschar *t;
-  uschar field[256], value[256];
+  int oldlength, newlength;
+  uschar * t, field[256], value[256];
 
   printf("> ");
   if (Ufgets(buffer, 256, stdin) == NULL) break;
@@ -904,7 +905,7 @@ for(; (reset_point = store_mark()); store_reset(reset_point))
   if ((isdigit((uschar)buffer[0]) && (buffer[1] == ' ' || buffer[1] == '\0'))
        || Ustrcmp(buffer, "d") == 0)
     {
-    if (name[0] == 0)
+    if (!*name)
       {
       printf("No previous record name is set\n");
       continue;
@@ -913,167 +914,175 @@ for(; (reset_point = store_mark()); store_reset(reset_point))
     }
   else
     {
-    name[0] = 0;
+    *name = '\0';
     (void)sscanf(CS buffer, "%s %s %s", name, field, value);
     }
 
   /* Handle an update request */
 
-  if (field[0] != 0)
+  if (*field)
     {
     int verify = 1;
 
-    if (!(dbm = dbfn_open(aname, O_RDWR|O_CREAT, &dbblock, FALSE, TRUE)))
+    if (!(dbm = dbfn_open(file, O_RDWR|O_CREAT, &dbblock, FALSE, TRUE)))
       continue;
 
     if (Ustrcmp(field, "d") == 0)
       {
-      if (value[0] != 0) printf("unexpected value after \"d\"\n");
-        else printf("%s\n", (dbfn_delete(dbm, name) < 0)?
-          "not found" : "deleted");
+      if (*value)
+	printf("unexpected value after \"d\"\n");
+      else
+	printf("%s\n", dbfn_delete(dbm, name) < 0 ?  "not found" : "deleted");
       dbfn_close(dbm);
       continue;
       }
 
-    else if (isdigit((uschar)field[0]))
+    else if (dbdata_type == type_dbm || isdigit((uschar)field[0]))
       {
-      int fieldno = Uatoi(field);
-      if (value[0] == 0)
+      if (!*value)
         {
         printf("value missing\n");
         dbfn_close(dbm);
         continue;
         }
+      else if (!(record = dbfn_read_with_length(dbm, name, &oldlength)))
+	printf("not found\n");
       else
-        {
-        record = dbfn_read_with_length(dbm, name, &oldlength);
-        if (record == NULL) printf("not found\n"); else
-          {
-          time_t tt;
-          /*int length = 0;      Stops compiler warning */
+	{
+	int fieldno = Uatoi(field);
+	time_t tt;
+	/*int length = 0;      Stops compiler warning */
 
-          switch(dbdata_type)
-            {
-            case type_retry:
-	      retry = (dbdata_retry *)record;
-	      /* length = sizeof(dbdata_retry) + Ustrlen(retry->text); */
+	newlength = oldlength;
+	switch(dbdata_type)
+	  {
+	  case type_retry:
+	    retry = (dbdata_retry *)record;
+	    /* length = sizeof(dbdata_retry) + Ustrlen(retry->text); */
+
+	    switch(fieldno)
+	      {
+	      case 0: retry->basic_errno = Uatoi(value);
+		      break;
+	      case 1: retry->more_errno = Uatoi(value);
+		      break;
+	      case 2: if ((tt = read_time(value)) > 0) retry->first_failed = tt;
+		      else printf("bad time value\n");
+		      break;
+	      case 3: if ((tt = read_time(value)) > 0) retry->last_try = tt;
+		      else printf("bad time value\n");
+		      break;
+	      case 4: if ((tt = read_time(value)) > 0) retry->next_try = tt;
+		      else printf("bad time value\n");
+		      break;
+	      case 5: if (Ustrcmp(value, "yes") == 0) retry->expired = TRUE;
+		      else if (Ustrcmp(value, "no") == 0) retry->expired = FALSE;
+		      else printf("\"yes\" or \"no\" expected=n");
+		      break;
+	      default: printf("unknown field number\n");
+		       verify = 0;
+		       break;
+	      }
+	    break;
 
-	      switch(fieldno)
-		{
-		case 0: retry->basic_errno = Uatoi(value);
-			break;
-		case 1: retry->more_errno = Uatoi(value);
-			break;
-		case 2: if ((tt = read_time(value)) > 0) retry->first_failed = tt;
-			else printf("bad time value\n");
-			break;
-		case 3: if ((tt = read_time(value)) > 0) retry->last_try = tt;
-			else printf("bad time value\n");
-			break;
-		case 4: if ((tt = read_time(value)) > 0) retry->next_try = tt;
-			else printf("bad time value\n");
-			break;
-		case 5: if (Ustrcmp(value, "yes") == 0) retry->expired = TRUE;
-			else if (Ustrcmp(value, "no") == 0) retry->expired = FALSE;
-			else printf("\"yes\" or \"no\" expected=n");
-			break;
-		default: printf("unknown field number\n");
-			 verify = 0;
-			 break;
-		}
-	      break;
+	  case type_wait:
+	    printf("Can't change contents of wait database record\n");
+	    break;
 
-            case type_wait:
-	      printf("Can't change contents of wait database record\n");
-	      break;
+	  case type_misc:
+	    printf("Can't change contents of misc database record\n");
+	    break;
 
-            case type_misc:
-	      printf("Can't change contents of misc database record\n");
+	  case type_callout:
+	    callout = (dbdata_callout_cache *)record;
+	    /* length = sizeof(dbdata_callout_cache); */
+	    switch(fieldno)
+	      {
+	      case 0: callout->result = Uatoi(value);
+		      break;
+	      case 1: callout->postmaster_result = Uatoi(value);
+		      break;
+	      case 2: callout->random_result = Uatoi(value);
+		      break;
+	      default: printf("unknown field number\n");
+		       verify = 0;
+		       break;
+	      }
 	      break;
 
-            case type_callout:
-	      callout = (dbdata_callout_cache *)record;
-	      /* length = sizeof(dbdata_callout_cache); */
-	      switch(fieldno)
-		{
-		case 0: callout->result = Uatoi(value);
-			break;
-		case 1: callout->postmaster_result = Uatoi(value);
-			break;
-		case 2: callout->random_result = Uatoi(value);
+	  case type_ratelimit:
+	    ratelimit = (dbdata_ratelimit *)record;
+	    switch(fieldno)
+	      {
+	      case 0: if ((tt = read_time(value)) > 0) ratelimit->time_stamp = tt;
+		      else printf("bad time value\n");
+		      break;
+	      case 1: ratelimit->time_usec = Uatoi(value);
+		      break;
+	      case 2: ratelimit->rate = Ustrtod(value, NULL);
+		      break;
+	      case 3: if (Ustrstr(name, "/unique/") != NULL
+			  && oldlength >= sizeof(dbdata_ratelimit_unique))
+			{
+			rate_unique = (dbdata_ratelimit_unique *)record;
+			if ((tt = read_time(value)) > 0) rate_unique->bloom_epoch = tt;
+			  else printf("bad time value\n");
 			break;
-		default: printf("unknown field number\n");
-			 verify = 0;
-			 break;
-		}
-		break;
-
-            case type_ratelimit:
-	      ratelimit = (dbdata_ratelimit *)record;
-	      switch(fieldno)
-		{
-		case 0: if ((tt = read_time(value)) > 0) ratelimit->time_stamp = tt;
-			else printf("bad time value\n");
-			break;
-		case 1: ratelimit->time_usec = Uatoi(value);
-			break;
-		case 2: ratelimit->rate = Ustrtod(value, NULL);
-			break;
-		case 3: if (Ustrstr(name, "/unique/") != NULL
-			    && oldlength >= sizeof(dbdata_ratelimit_unique))
+			}
+		      /* else fall through */
+	      case 4:
+	      case 5: if (Ustrstr(name, "/unique/") != NULL
+			  && oldlength >= sizeof(dbdata_ratelimit_unique))
+			{
+			/* see acl.c */
+			BOOL seen;
+			unsigned hash, hinc;
+			uschar md5sum[16];
+			md5 md5info;
+			md5_start(&md5info);
+			md5_end(&md5info, value, Ustrlen(value), md5sum);
+			hash = md5sum[0] <<  0 | md5sum[1] <<  8
+			     | md5sum[2] << 16 | md5sum[3] << 24;
+			hinc = md5sum[4] <<  0 | md5sum[5] <<  8
+			     | md5sum[6] << 16 | md5sum[7] << 24;
+			rate_unique = (dbdata_ratelimit_unique *)record;
+			seen = TRUE;
+			for (unsigned n = 0; n < 8; n++, hash += hinc)
 			  {
-			  rate_unique = (dbdata_ratelimit_unique *)record;
-			  if ((tt = read_time(value)) > 0) rate_unique->bloom_epoch = tt;
-			    else printf("bad time value\n");
-			  break;
-			  }
-			/* else fall through */
-		case 4:
-		case 5: if (Ustrstr(name, "/unique/") != NULL
-			    && oldlength >= sizeof(dbdata_ratelimit_unique))
-			  {
-			  /* see acl.c */
-			  BOOL seen;
-			  unsigned hash, hinc;
-			  uschar md5sum[16];
-			  md5 md5info;
-			  md5_start(&md5info);
-			  md5_end(&md5info, value, Ustrlen(value), md5sum);
-			  hash = md5sum[0] <<  0 | md5sum[1] <<  8
-			       | md5sum[2] << 16 | md5sum[3] << 24;
-			  hinc = md5sum[4] <<  0 | md5sum[5] <<  8
-			       | md5sum[6] << 16 | md5sum[7] << 24;
-			  rate_unique = (dbdata_ratelimit_unique *)record;
-			  seen = TRUE;
-			  for (unsigned n = 0; n < 8; n++, hash += hinc)
+			  int bit = 1 << (hash % 8);
+			  int byte = (hash / 8) % rate_unique->bloom_size;
+			  if ((rate_unique->bloom[byte] & bit) == 0)
 			    {
-			    int bit = 1 << (hash % 8);
-			    int byte = (hash / 8) % rate_unique->bloom_size;
-			    if ((rate_unique->bloom[byte] & bit) == 0)
-			      {
-			      seen = FALSE;
-			      if (fieldno == 5) rate_unique->bloom[byte] |= bit;
-			      }
+			    seen = FALSE;
+			    if (fieldno == 5) rate_unique->bloom[byte] |= bit;
 			    }
-			  printf("%s %s\n",
-			    seen ? "seen" : fieldno == 5 ? "added" : "unseen", value);
-			  break;
 			  }
-			/* else fall through */
-		default: printf("unknown field number\n");
-			 verify = 0;
-			 break;
-		}
-	      break;
+			printf("%s %s\n",
+			  seen ? "seen" : fieldno == 5 ? "added" : "unseen", value);
+			break;
+			}
+		      /* else fall through */
+	      default: printf("unknown field number\n");
+		       verify = 0;
+		       break;
+	      }
+	    break;
 
-            case type_tls:
-	      printf("Can't change contents of tls database record\n");
-	      break;
-            }
+	  case type_tls:
+	    printf("Can't change contents of tls database record\n");
+	    break;
 
-          dbfn_write(dbm, name, record, oldlength);
-          }
-        }
+	  case type_dbm:
+	    record = value;
+	    newlength = Ustrlen(value);
+	    break;
+	  }
+
+	if (dbdata_type != type_dbm)
+	  ((dbdata_generic *)record)->time_stamp = time(NULL);
+
+	dbfn_write(dbm, name, record, newlength);
+	}
       }
 
     else
@@ -1092,18 +1101,21 @@ for(; (reset_point = store_mark()); store_reset(reset_point))
 
   /* Handle a read request, or verify after an update. */
 
-  if (!(dbm = dbfn_open(aname, O_RDONLY, &dbblock, FALSE, TRUE)))
+  if (!(dbm = dbfn_open(file, O_RDONLY, &dbblock, FALSE, TRUE)))
     continue;
 
   if (!(record = dbfn_read_with_length(dbm, name, &oldlength)))
     {
     printf("record %s not found\n", name);
-    name[0] = 0;
+    *name = '\0';
     }
   else
     {
     int count_bad = 0;
-    printf("%s\n", CS print_time(((dbdata_generic *)record)->time_stamp));
+
+    if (dbdata_type != type_dbm)
+      printf("%s\n", CS print_time(((dbdata_generic *)record)->time_stamp));
+
     switch(dbdata_type)
       {
       case type_retry:
@@ -1113,7 +1125,7 @@ for(; (reset_point = store_mark()); store_reset(reset_point))
 	printf("2 first failed: %s\n", print_time(retry->first_failed));
 	printf("3 last try:     %s\n", print_time(retry->last_try));
 	printf("4 next try:     %s\n", print_time(retry->next_try));
-	printf("5 expired:      %s\n", (retry->expired)? "yes" : "no");
+	printf("5 expired:      %s\n", retry->expired ? "yes" : "no");
 	break;
 
       case type_wait:
@@ -1185,6 +1197,9 @@ for(; (reset_point = store_mark()); store_reset(reset_point))
 	printf("0 time stamp:  %s\n", print_time(session->time_stamp));
 	printf("1 session: .%s\n", session->session);
 	break;
+
+      case type_dbm:
+	printf("0 value:  %.*s\n", oldlength, record);
       }
     }
 
@@ -1451,6 +1466,7 @@ for (; keychain && (reset_point = store_mark()); store_reset(reset_point))
     if (update)
       {
       printf("updated %s\n", key);
+      wait->time_stamp = time(NULL);
       dbfn_write(dbm, key, wait, sizeof(dbdata_wait) +
         wait->count * MESSAGE_ID_LENGTH);
       }
diff --git a/src/src/functions.h b/src/src/functions.h
index b48103922..bfe34f391 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -594,6 +594,7 @@ extern int     string_is_ip_addressX(const uschar *, int *, const uschar **);
 extern BOOL    string_is_utf8(const uschar *);
 #endif
 extern const uschar *string_printing2(const uschar *, int);
+extern const uschar *string_printing3(const uschar *, int, int);
 extern uschar *string_split_message(uschar *);
 extern uschar *string_unprinting(uschar *);
 #ifdef SUPPORT_I18N
diff --git a/src/src/hintsdb_structs.h b/src/src/hintsdb_structs.h
index 3eb8b3386..c77190f38 100644
--- a/src/src/hintsdb_structs.h
+++ b/src/src/hintsdb_structs.h
@@ -25,8 +25,10 @@ typedef struct {
 } open_db;
 
 
-/* Structures for records stored in exim database dbm files. They all
-start with the same fields, described in the generic type. */
+/* Structures for records stored in exim database hinsts files. They all
+start with the same fields, described in the generic type.
+DBM databases are used for hints files.
+*/
 
 
 typedef struct {

commit 73d3b73c090557b9f11cef78623cf11b81e9c7a9
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Nov 29 21:02:12 2025 +0000

    track tainted values through hintsdb storage

diff --git a/src/src/acl.c b/src/src/acl.c
index bc4f6af6d..7ec3a898c 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -2826,7 +2826,7 @@ if (!dbd)
   {
   HDEBUG(D_acl) debug_printf_indent("ratelimit initializing new key's rate data\n");
   dbd = &dbdb->dbd;
-  dbd->time_stamp = tv.tv_sec;
+  dbd->gen.time_stamp = tv.tv_sec;
   dbd->time_usec = tv.tv_usec;
   dbd->rate = count;
   }
@@ -2876,7 +2876,7 @@ else
 
   double this_time = (double)tv.tv_sec
                    + (double)tv.tv_usec / 1000000.0;
-  double prev_time = (double)dbd->time_stamp
+  double prev_time = (double)dbd->gen.time_stamp
                    + (double)dbd->time_usec / 1000000.0;
 
   /* We must avoid division by zero, and deal gracefully with the clock going
@@ -2893,7 +2893,7 @@ else
   using the smoothing factor a. In order to measure sized events, multiply the
   instantaneous rate by the count of bytes or recipients etc. */
 
-  dbd->time_stamp = tv.tv_sec;
+  dbd->gen.time_stamp = tv.tv_sec;
   dbd->time_usec = tv.tv_usec;
   dbd->rate = (1 - a) * count / i_over_p + a * dbd->rate;
 
@@ -3048,7 +3048,7 @@ dbd = dbfn_read_with_length(dbm, key, NULL);
 now = time(NULL);
 if (dbd)		/* an existing record */
   {
-  time_t diff = now - dbd->time_stamp;	/* time since the record was written */
+  time_t diff = now - dbd->gen.time_stamp;  /* time since record was written */
 
   if (before ? diff >= interval : diff < interval)
     yield = OK;
@@ -3057,13 +3057,13 @@ if (dbd)		/* an existing record */
     { HDEBUG(D_acl) debug_printf_indent("seen db not written (readonly)\n"); }
   else if (mode == SEEN_WRITE || !before)
     {
-    dbd->time_stamp = now;
+    dbd->gen.time_stamp = now;
     dbfn_write(dbm, key, dbd, sizeof(*dbd));
     HDEBUG(D_acl) debug_printf_indent("seen db written (update)\n");
     }
   else if (diff >= refresh)
     {
-    dbd->time_stamp = now - interval;
+    dbd->gen.time_stamp = now - interval;
     dbfn_write(dbm, key, dbd, sizeof(*dbd));
     HDEBUG(D_acl) debug_printf_indent("seen db written (refresh)\n");
     }
@@ -3072,7 +3072,7 @@ else
   {			/* No record found, yield always FAIL */
   if (mode != SEEN_READONLY)
     {
-    dbdata_seen d = {.time_stamp = now};
+    dbdata_seen d = {.gen = {.time_stamp = now}};
     dbfn_write(dbm, key, &d, sizeof(*dbd));
     HDEBUG(D_acl) debug_printf_indent("seen db written (create)\n");
     }
diff --git a/src/src/dbfn.c b/src/src/dbfn.c
index 3ed78901b..0a05524e1 100644
--- a/src/src/dbfn.c
+++ b/src/src/dbfn.c
@@ -410,6 +410,7 @@ void * yield;
 EXIM_DATUM key_datum, result_datum;
 uschar * key_copy = store_get(klen, key);
 unsigned dlen;
+BOOL tainted;
 
 memcpy(key_copy, key, klen);
 
@@ -426,13 +427,17 @@ if (!exim_dbget(dbblock->dbptr, &key_datum, &result_datum))
   return NULL;
   }
 
-/* Assume the data store could have been tainted.  Properly, we should
-store the taint status with the data. */
-
 dlen = exim_datum_size_get(&result_datum);
 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: size %u return\n", dlen);
 
-yield = store_get(dlen+1, hintsdb ? GET_TAINTED : GET_UNTAINTED);
+/* Hintsdb uses store the taint of the payload of the value in the value.
+For non-hints uses we have no provenance, so assume untainted */
+
+tainted = hintsdb
+  ? ((dbdata_generic *) exim_datum_data_get(&result_datum))->tainted
+  : FALSE;
+
+yield = store_get(dlen+1, tainted ? GET_TAINTED : GET_UNTAINTED);
 memcpy(yield, exim_datum_data_get(&result_datum), dlen);
 ((uschar *)yield)[dlen] = '\0';
 if (length) *length = dlen;
@@ -498,6 +503,8 @@ return NULL;
 *************************************************/
 
 /*
+All callers are writing a hintsdb record.
+
 Arguments:
   dbblock   a pointer to an open database block
   key       the key of the record to be written
@@ -509,7 +516,7 @@ Returns:    the yield of the underlying dbm or db "write" function. If this
 */
 
 int
-dbfn_write(open_db *dbblock, const uschar *key, void *ptr, int length)
+dbfn_write(open_db * dbblock, const uschar * key, void * ptr, int length)
 {
 EXIM_DATUM key_datum, value_datum;
 dbdata_generic *gptr = (dbdata_generic *)ptr;
@@ -518,6 +525,7 @@ uschar * key_copy = store_get(klen, key);
 
 memcpy(key_copy, key, klen);
 gptr->time_stamp = time(NULL);
+gptr->tainted = is_tainted(ptr);
 
 DEBUG(D_hints_lookup)
   debug_printf_indent("dbfn_write: key=%s datalen %d\n", key, length);
diff --git a/src/src/deliver.c b/src/src/deliver.c
index c978e67b5..cf3a41156 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -2932,7 +2932,7 @@ while (addr_local)
 	DEBUG(D_retry)
 	  {
 	  debug_printf("retry record exists: age=%s ",
-	    readconf_printtime(now - retry_record->time_stamp));
+	    readconf_printtime(now - retry_record->gen.time_stamp));
 	  debug_printf("(max %s)\n", readconf_printtime(retry_data_expire));
 	  debug_printf("  time to retry = %s expired = %d\n",
 	    readconf_printtime(retry_record->next_try - now),
@@ -2941,7 +2941,7 @@ while (addr_local)
 
 	if (f.queue_running && !f.deliver_force)
 	  {
-	  ok = (now - retry_record->time_stamp > retry_data_expire)
+	  ok = (now - retry_record->gen.time_stamp > retry_data_expire)
 	    || (now >= retry_record->next_try)
 	    || retry_record->expired;
 
@@ -7827,7 +7827,7 @@ while (addr_new)           /* Loop until all addresses dealt with */
       {
       domain_retry_record = dbfn_read(dbm_file, addr->domain_retry_key);
       if (  domain_retry_record
-	 && now - domain_retry_record->time_stamp > retry_data_expire
+	 && now - domain_retry_record->gen.time_stamp > retry_data_expire
 	 )
 	{
 	DEBUG(D_deliver|D_retry)
@@ -7837,7 +7837,7 @@ while (addr_new)           /* Loop until all addresses dealt with */
 
       address_retry_record = dbfn_read(dbm_file, addr->address_retry_key);
       if (  address_retry_record
-	 && now - address_retry_record->time_stamp > retry_data_expire
+	 && now - address_retry_record->gen.time_stamp > retry_data_expire
 	 )
 	{
 	DEBUG(D_deliver|D_retry)
@@ -7851,7 +7851,7 @@ while (addr_new)           /* Loop until all addresses dealt with */
 				  addr->address_retry_key, sender_address);
 	address_retry_record = dbfn_read(dbm_file, altkey);
 	if (  address_retry_record
-	   && now - address_retry_record->time_stamp > retry_data_expire)
+	   && now - address_retry_record->gen.time_stamp > retry_data_expire)
 	  {
 	  DEBUG(D_deliver|D_retry)
 	    debug_printf_indent("address<sender> retry record present but expired\n");
diff --git a/src/src/enq.c b/src/src/enq.c
index 8129b3b2b..372607daf 100644
--- a/src/src/enq.c
+++ b/src/src/enq.c
@@ -53,7 +53,7 @@ if (!(dbm_file = dbfn_open(US"misc", O_RDWR|O_CREAT, &dbblock, TRUE, TRUE)))
 proceed with the connection unless the record is very old. */
 
 serial_record = dbfn_read_enforce_length(dbm_file, key, sizeof(dbdata_serialize));
-if (serial_record && time(NULL) - serial_record->time_stamp < 6*60*60)
+if (serial_record && time(NULL) - serial_record->gen.time_stamp < 6*60*60)
   {
   if (serial_record->count >= lim)
     {
diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index 26dca7dbd..2c03be1c5 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -380,10 +380,7 @@ if (exim_lockfile_needed())
   exclusive access to the database, so we can go ahead and open it. */
   }
 
-if ((dbmdb
-    ? asprintf(CSS &filename, "%s", name)
-    : asprintf(CSS &filename, "%s/%s", dname, name)
-    ) < 0) return NULL;
+if (asprintf(CSS &filename, "%s", name) < 0) return NULL;
 
 if (!(dbblock->dbptr = dbblock->readonly && !exim_lockfile_needed()
 		      ? exim_dbopen_multi(filename, dname, flags, 0)
@@ -461,6 +458,7 @@ EXIM_DATUM key_datum, result_datum;
 int klen = Ustrlen(key) + 1;
 uschar * key_copy = store_get(klen, key);
 unsigned dlen;
+BOOL tainted;
 
 memcpy(key_copy, key, klen);
 
@@ -475,7 +473,15 @@ if (!exim_dbget(dbblock->dbptr, &key_datum, &result_datum)) return NULL;
 we should store the taint status along with the data. */
 
 dlen = exim_datum_size_get(&result_datum);
-yield = store_get(dlen, GET_TAINTED);
+
+/* Hintsdb uses store the taint of the payload of the value in the value.
+For non-hints uses we have no provenance, so assume untainted */
+
+tainted = dbmdb
+  ? FALSE
+  : ((dbdata_generic *) exim_datum_data_get(&result_datum))->tainted;
+
+yield = store_get(dlen, tainted ? GET_TAINTED : GET_UNTAINTED);
 memcpy(yield, exim_datum_data_get(&result_datum), dlen);
 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: size %u return\n", dlen);
 if (length) *length = dlen;
@@ -733,7 +739,33 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 	int dlen = length = sizeof(dbdata_generic);
 
 	printf("%s %s\n", print_time(recp->time_stamp), keybuffer);
-	printf("%s\n", string_sprintf("%.*q\n", dlen, dp));
+	if (  Ustrncmp(keybuffer, "etrn-", 5) == 0
+	   || Ustrncmp(keybuffer, "tpt-serialize-", 14) == 0
+	   || Ustrncmp(keybuffer, "host-serialize-", 15) == 0
+	   )
+	  printf("serialize %.*s: %d running\n",
+		  (int)(Ustrchr(keybuffer, '-') - keybuffer), keybuffer,
+		  ((dbdata_serialize *)recp)->count);
+	else
+	  {
+	  uschar * s = keybuffer + Ustrlen(keybuffer) - 5;
+	  if (s > keybuffer && Ustrcmp(s, ".EHLO") == 0)
+	    {
+	    ehlo_resp_precis * erp = &((dbdata_ehlo_resp *)recp)->data;
+
+	    printf("helo/ehlo %.*s clr %04x/%04x cry %04x/%04x",
+		    (int)(s - keybuffer), keybuffer,
+		    erp->cleartext_features, erp->cleartext_auths,
+		    erp->crypted_features,   erp->crypted_auths);
+
+	    if (sizeof(*erp) > 4 * sizeof(unsigned short))
+	      printf(" lim %05d/%05d/%05d",
+		erp->limit_mail, erp->limit_rcpt, erp->limit_rcptdom);
+	    putchar('\n');
+	    }
+	  else
+	    printf("%s\n", string_sprintf("%.*q\n", dlen, dp));
+	  }
 	break;
 	}
 
@@ -745,7 +777,7 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 	if (length == sizeof(dbdata_callout_cache_address))
 	  {
 	  printf("%s %s callout=%s\n",
-	    print_time(((dbdata_generic *)value)->time_stamp),
+	    print_time(callout->gen.time_stamp),
 	    keybuffer,
 	    print_cache(callout->result));
 	  }
@@ -755,7 +787,7 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 	else if (length == sizeof(dbdata_callout_cache))
 	  {
 	  printf("%s %s callout=%s postmaster=%s",
-	    print_time(((dbdata_generic *)value)->time_stamp),
+	    print_time(callout->gen.time_stamp),
 	    keybuffer,
 	    print_cache(callout->result),
 	    print_cache(callout->postmaster_result));
@@ -775,7 +807,7 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 	  ratelimit = (dbdata_ratelimit *)value;
 	  rate_unique = (dbdata_ratelimit_unique *)value;
 	  printf("%s.%06d rate: %10.3f epoch: %s size: %u key: %s\n",
-	    print_time(ratelimit->time_stamp),
+	    print_time(ratelimit->gen.time_stamp),
 	    ratelimit->time_usec, ratelimit->rate,
 	    print_time(rate_unique->bloom_epoch), rate_unique->bloom_size,
 	    keybuffer);
@@ -784,7 +816,7 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 	  {
 	  ratelimit = (dbdata_ratelimit *)value;
 	  printf("%s.%06d rate: %10.3f key: %s\n",
-	    print_time(ratelimit->time_stamp),
+	    print_time(ratelimit->gen.time_stamp),
 	    ratelimit->time_usec, ratelimit->rate,
 	    keybuffer);
 	  }
@@ -797,7 +829,7 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 
       case type_seen:
 	seen = (dbdata_seen *)value;
-	printf("%s\t%s\n", keybuffer, print_time(seen->time_stamp));
+	printf("%s\t%s\n", keybuffer, print_time(seen->gen.time_stamp));
 	break;
 
       case type_dbm:
@@ -1014,8 +1046,10 @@ for(; (reset_point = store_mark()); store_reset(reset_point))
 	    ratelimit = (dbdata_ratelimit *)record;
 	    switch(fieldno)
 	      {
-	      case 0: if ((tt = read_time(value)) > 0) ratelimit->time_stamp = tt;
-		      else printf("bad time value\n");
+	      case 0: if ((tt = read_time(value)) > 0)
+			ratelimit->gen.time_stamp = tt;
+		      else
+			printf("bad time value\n");
 		      break;
 	      case 1: ratelimit->time_usec = Uatoi(value);
 		      break;
@@ -1079,7 +1113,11 @@ for(; (reset_point = store_mark()); store_reset(reset_point))
 	  }
 
 	if (dbdata_type != type_dbm)
-	  ((dbdata_generic *)record)->time_stamp = time(NULL);
+	  {
+	  dbdata_generic * p = (dbdata_generic *)record;
+	  p->time_stamp = time(NULL);
+	  p->tainted = FALSE;
+	  }
 
 	dbfn_write(dbm, name, record, newlength);
 	}
@@ -1179,7 +1217,7 @@ for(; (reset_point = store_mark()); store_reset(reset_point))
 
       case type_ratelimit:
 	ratelimit = (dbdata_ratelimit *)record;
-	printf("0 time stamp:  %s\n", print_time(ratelimit->time_stamp));
+	printf("0 time stamp:  %s\n", print_time(ratelimit->gen.time_stamp));
 	printf("1 fract. time: .%06d\n", ratelimit->time_usec);
 	printf("2 sender rate: % .3f\n", ratelimit->rate);
 	if (Ustrstr(name, "/unique/") != NULL
@@ -1193,8 +1231,8 @@ for(; (reset_point = store_mark()); store_reset(reset_point))
 	break;
 
       case type_tls:
-	session = (dbdata_tls_session *)value;
-	printf("0 time stamp:  %s\n", print_time(session->time_stamp));
+	session = (dbdata_tls_session *)record;
+	printf("0 time stamp:  %s\n", print_time(session->gen.time_stamp));
 	printf("1 session: .%s\n", session->session);
 	break;
 
@@ -1360,10 +1398,10 @@ for (; keychain && (reset_point = store_mark()); store_reset(reset_point))
 
     /* Leave corrupt records alone */
 
-    if (wait->time_stamp > time(NULL))
+    if (wait->gen.time_stamp > time(NULL))
       {
       printf("**** Data for '%s' corrupted\n  time in future: %s\n",
-        key, print_time(((dbdata_generic *)value)->time_stamp));
+        key, print_time(wait->gen.time_stamp));
       continue;
       }
     if (wait->count > WAIT_NAME_MAX)
@@ -1381,7 +1419,7 @@ for (; keychain && (reset_point = store_mark()); store_reset(reset_point))
 
     /* Record over 1 year old; just remove it */
 
-    if (wait->time_stamp < time(NULL) - 365*24*60*60)
+    if (wait->gen.time_stamp < time(NULL) - 365*24*60*60)
       {
       dbfn_delete(dbm, key);
       printf("deleted %s (too old)\n", key);
@@ -1466,7 +1504,8 @@ for (; keychain && (reset_point = store_mark()); store_reset(reset_point))
     if (update)
       {
       printf("updated %s\n", key);
-      wait->time_stamp = time(NULL);
+      wait->gen.time_stamp = time(NULL);
+      /* leave the taint marker unchanged */
       dbfn_write(dbm, key, wait, sizeof(dbdata_wait) +
         wait->count * MESSAGE_ID_LENGTH);
       }
diff --git a/src/src/hintsdb_structs.h b/src/src/hintsdb_structs.h
index c77190f38..f4d72fdc9 100644
--- a/src/src/hintsdb_structs.h
+++ b/src/src/hintsdb_structs.h
@@ -32,7 +32,8 @@ DBM databases are used for hints files.
 
 
 typedef struct {
-  time_t time_stamp;      /* Timestamp of writing */
+  time_t time_stamp;	/* Timestamp of writing */
+  BOOL	 tainted;	/* metadata for the "value" part of the record */
 } dbdata_generic;
 
 
@@ -40,7 +41,7 @@ typedef struct {
 address. */
 
 typedef struct {
-  time_t time_stamp;
+  dbdata_generic gen;
   /*************/
   time_t first_failed;    /* Time of first failure */
   time_t last_try;        /* Time of last try */
@@ -72,7 +73,7 @@ handle the old type of record, we retain the old definition. The different
 kinds of record can be distinguished by their different lengths. */
 
 typedef struct {
-  time_t time_stamp;
+  dbdata_generic gen;
   /*************/
   int   result;
   int   postmaster_result; /* Postmaster is accepted */
@@ -80,7 +81,7 @@ typedef struct {
 } dbdata_callout_cache_obs;
 
 typedef struct {
-  time_t time_stamp;       /* Timestamp of last address check */
+  dbdata_generic gen;		/* Timestamp of last address check */
   /*************/
   int   result;            /* accept or reject */
 } dbdata_callout_cache_address;
@@ -90,7 +91,7 @@ last so that if somebody reverts to an older Exim, the new records will
 still make sense because they match the old layout. */
 
 typedef struct {
-  time_t time_stamp;       /* Time stamp of last connection */
+  dbdata_generic gen;		/* Time stamp of last connection */
   /*************/
   int   result;            /* Domain reject or accept */
   int   postmaster_result; /* Postmaster result */
@@ -103,7 +104,7 @@ typedef struct {
 host for a particular transport. */
 
 typedef struct {
-  time_t time_stamp;
+  dbdata_generic gen;
   /*************/
   int    count;           /* Count of message ids */
   int    sequence;        /* Sequence for continued records */
@@ -123,7 +124,7 @@ at present in use. The same structure is used for recording a running ETRN
 process. */
 
 typedef struct {
-  time_t time_stamp;
+  dbdata_generic gen;
   /*************/
   int    count;           /* Reserved for possible connection count */
 } dbdata_serialize;
@@ -133,7 +134,7 @@ typedef struct {
 ACL condition. */
 
 typedef struct {
-  time_t time_stamp;
+  dbdata_generic gen;
   /*************/
   int    time_usec;       /* Fractional part of time, from gettimeofday() */
   double rate;            /* Smoothed sending rate at that time */
@@ -151,7 +152,7 @@ typedef struct {
 
 /* For "seen" ACL condition */
 typedef struct {
-  time_t time_stamp;
+  dbdata_generic gen;
 } dbdata_seen;
 
 #ifndef DISABLE_PIPE_CONNECT
@@ -174,14 +175,14 @@ typedef struct {
 } ehlo_resp_precis;
 
 typedef struct {
-  time_t time_stamp;
+  dbdata_generic gen;
   /*************/
   ehlo_resp_precis data;
 } dbdata_ehlo_resp;
 #endif
 
 typedef struct {
-  time_t time_stamp;
+  dbdata_generic gen;
   /*************/
   uschar verify_override:1;
   uschar ocsp:3;
diff --git a/src/src/retry.c b/src/src/retry.c
index 24b2fa66a..515c94288 100644
--- a/src/src/retry.c
+++ b/src/src/retry.c
@@ -227,7 +227,7 @@ if (!host_retry_record)
   {
   DEBUG(D_transport|D_retry) debug_printf_indent("no host retry record\n");
   }
-else if (now - host_retry_record->time_stamp > retry_data_expire)
+else if (now - host_retry_record->gen.time_stamp > retry_data_expire)
   {
   host_retry_record = NULL;
   DEBUG(D_transport|D_retry) debug_printf_indent("host retry record too old\n");
@@ -237,7 +237,7 @@ if (!message_retry_record)
   {
   DEBUG(D_transport|D_retry) debug_printf_indent("no message retry record\n");
   }
-else if (now - message_retry_record->time_stamp > retry_data_expire)
+else if (now - message_retry_record->gen.time_stamp > retry_data_expire)
   {
   message_retry_record = NULL;
   DEBUG(D_transport|D_retry)
@@ -744,7 +744,7 @@ for (int i = 0; i < 3; i++)
         retry_record = dbfn_read_with_length(dbm_file, rti->key,
 					      &message_space);
         if (  retry_record
-	   && now - retry_record->time_stamp > retry_data_expire)
+	   && now - retry_record->gen.time_stamp > retry_data_expire)
           retry_record = NULL;
 
         if (!retry_record)
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 3ffb6fe8b..abc893ddc 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -5748,7 +5748,8 @@ while (done <= 0)
       /* Compute the serialization key for this command. We used (all the way
       back to 4.00) to include the given string as part of the key, but this
       opens a security hole for hintsdb types that use a command-string for
-      operations. All ETRN with the same command hash are serialized */
+      operations. So, use a hash of the string. All ETRN with the same command
+      hash are serialized */
 
       md5 hash;
       uschar *digest = store_get(16, GET_TAINTED);
@@ -5756,11 +5757,7 @@ while (done <= 0)
       md5_start(&hash);
       md5_end(&hash, smtp_cmd_argument, Ustrlen(smtp_cmd_argument), digest);
 
-      etrn_serialize_key = string_sprintf("etrn-" /* don't we have a function doing exactly this? */
-          "%02x%02x%02x%02x" "%02x%02x%02x%02x"   /* we have, since 2024-09-xx we can use %.16H */
-          "%02x%02x%02x%02x" "%02x%02x%02x%02x",
-          digest[0], digest[1], digest[2], digest[3], digest[4], digest[5], digest[6], digest[7],
-          digest[8], digest[9], digest[10], digest[11], digest[12], digest[13], digest[14], digest[15]);
+      etrn_serialize_key = string_sprintf("etrn-%.16H", digest);
 
       /* If a command has been specified for running as a result of ETRN, we
       permit any argument to ETRN. If not, only the # standard form is
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 40acee7a3..9d3f5af73 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -4035,7 +4035,7 @@ if (tlsp->host_resumable)
 #else			/* Use, fairly arbitrilarily, what we as server would */
 	  f.running_in_test_harness ? TESTSUITE_TICKET_LIFE : ssl_session_timeout;
 #endif
-	time_t now = time(NULL), expires = lifetime + dt->time_stamp;
+	time_t now = time(NULL), expires = lifetime + dt->gen.time_stamp;
 	if (expires < now)
 	  {
 	  DEBUG(D_tls) debug_printf("session expired (by " TIME_T_FMT "s from %lus)\n", now - expires, lifetime);
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index 8ea3d882a..80a70b490 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -992,7 +992,7 @@ if (  sx->early_pipe_active
       debug_printf("no ehlo-resp record!\n");
     else
       debug_printf("ehlo-resp record is %.0f seconds old\n",
-		    difftime(time(NULL), er->time_stamp));
+		    difftime(time(NULL), er->gen.time_stamp));
     }
 
   dbfn_delete(dbm_file, ehlo_resp_key);
@@ -1018,7 +1018,7 @@ else
     dbfn_close(dbm_file);
     DEBUG(D_transport) debug_printf("no ehlo-resp record\n");
     }
-  else if (time(NULL) - er->time_stamp > retry_data_expire)
+  else if (time(NULL) - er->gen.time_stamp > retry_data_expire)
     {
     DEBUG(D_transport) debug_printf("ehlo-resp record too old\n");
     dbfn_close(dbm_file);
diff --git a/src/src/verify.c b/src/src/verify.c
index 6696ec4d9..be9ea7fe4 100644
--- a/src/src/verify.c
+++ b/src/src/verify.c
@@ -63,7 +63,7 @@ negative = cache_record->result != ccache_accept ||
 expire = negative? negative_expire : positive_expire;
 now = time(NULL);
 
-if (now - cache_record->time_stamp > expire)
+if (now - cache_record->gen.time_stamp > expire)
   {
   HDEBUG(D_verify) debug_printf_indent("callout cache: %s record expired for %s\n", type, key);
   return NULL;
@@ -81,7 +81,7 @@ if (type[0] == 'd' && cache_record->result != ccache_reject)
     {
     dbdata_callout_cache * new = store_get(sizeof(dbdata_callout_cache), GET_UNTAINTED);
     memcpy(new, cache_record, length);
-    new->postmaster_stamp = new->random_stamp = new->time_stamp;
+    new->postmaster_stamp = new->random_stamp = new->gen.time_stamp;
     cache_record = new;
     }
 

commit de32296674ed4e57cb5a8efa3bb3f2c838d03d1a
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Nov 30 10:19:10 2025 +0000

    add version checking and grooming for hintsdb records

diff --git a/src/src/dbfn.c b/src/src/dbfn.c
index 0a05524e1..64807a7b8 100644
--- a/src/src/dbfn.c
+++ b/src/src/dbfn.c
@@ -17,25 +17,29 @@ with database files like $spooldirectory/db/<name> */
 
 
 /* Functions for accessing Exim's hints database, which consists of a number of
-different DBM files. This module does not contain code for reading DBM files
-for (e.g.) alias expansion. That is all contained within the general search
-functions. As Exim now has support for several DBM interfaces, all the relevant
+different DBM files.  Also used for the readonly access for dbm lookup
+expansions via the general search functions, for non-hints DBM file.
+
+As Exim now has support for several DBM interfaces, all the relevant
 functions are called as inlinable functions from an included file.
 
-All the data in Exim's database is in the nature of *hints*. Therefore it
+All the data in Exim's hists database is in the nature of *hints*. Therefore it
 doesn't matter if it gets destroyed by accident. These functions are not
 supposed to implement a "safe" database.
 
-Keys are passed in as C strings, and the terminating zero *is* used when
-building the dbm files. This just makes life easier when scanning the files
+For hints, keys are passed in as C strings - and the terminating zero *is* used
+when building the dbm files. This just makes life easier when scanning the files
 sequentially.
 
-Synchronization is required on the database files, and this is achieved by
-means of locking on independent lock files. (Earlier attempts to lock on the
-DBM files themselves were never completely successful.) Since callers may in
-general want to do more than one read or write while holding the lock, there
-are separate open and close functions. However, the calling modules should
-arrange to hold the locks for the bare minimum of time.
+For many of the DBM interfaces, synchronization is required on the database
+files; and this is achieved by means of locking on independent lock files.
+(Earlier attempts to lock on the DBM files themselves were never completely
+successful.) Since callers may in general want to do more than one read or write
+while holding the lock, there are separate open and close functions.
+However, the calling modules should arrange to hold the locks for the bare
+minimum of time.
+A predicate call is provided to tell if this locking is required, as opposed
+to being built-in to the DBM.
 
 API:
   exim_lockfile_needed			facilities predicate
@@ -62,6 +66,13 @@ Users:
   peer capability cache
   callout & quota cache
   DBM lookup type
+
+
+NOTE: the autoreply transport accesses a DBM database using
+exim_db{open,close,get,put} directly, not using this layer.
+
+The DBM interface is selected at build time from one of the
+files src/hintsdb/hints_*.h
 */
 
 
@@ -430,12 +441,30 @@ if (!exim_dbget(dbblock->dbptr, &key_datum, &result_datum))
 dlen = exim_datum_size_get(&result_datum);
 DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: size %u return\n", dlen);
 
-/* Hintsdb uses store the taint of the payload of the value in the value.
-For non-hints uses we have no provenance, so assume untainted */
+if (hintsdb)
+  {
+  dbdata_generic *  gp = (dbdata_generic *) exim_datum_data_get(&result_datum);
 
-tainted = hintsdb
-  ? ((dbdata_generic *) exim_datum_data_get(&result_datum))->tainted
-  : FALSE;
+  if (dlen < sizeof(dbdata_generic))
+    {
+    DEBUG(D_hints_lookup)
+      debug_printf_indent("dbfn_read: bad record size %u\n", dlen);
+    return NULL;
+    }
+  if (gp->version != HINTS_VERSION)
+    {
+    DEBUG(D_hints_lookup)
+      debug_printf_indent("dbfn_read: bad record version %u\n", gp->version);
+    return NULL;
+    }
+
+  /* Hintsdb uses store the taint of the payload of the value in the value.
+  For non-hints uses we have no provenance, so assume untainted */
+
+  tainted = gp->tainted;
+  }
+else
+  tainted = FALSE;
 
 yield = store_get(dlen+1, tainted ? GET_TAINTED : GET_UNTAINTED);
 memcpy(yield, exim_datum_data_get(&result_datum), dlen);
@@ -519,13 +548,14 @@ int
 dbfn_write(open_db * dbblock, const uschar * key, void * ptr, int length)
 {
 EXIM_DATUM key_datum, value_datum;
-dbdata_generic *gptr = (dbdata_generic *)ptr;
+dbdata_generic * gptr = (dbdata_generic *)ptr;
 int klen = Ustrlen(key) + 1;
 uschar * key_copy = store_get(klen, key);
 
 memcpy(key_copy, key, klen);
-gptr->time_stamp = time(NULL);
+gptr->version = HINTS_VERSION;
 gptr->tainted = is_tainted(ptr);
+gptr->time_stamp = time(NULL);
 
 DEBUG(D_hints_lookup)
   debug_printf_indent("dbfn_write: key=%s datalen %d\n", key, length);
diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index 2c03be1c5..f3842874f 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -380,7 +380,8 @@ if (exim_lockfile_needed())
   exclusive access to the database, so we can go ahead and open it. */
   }
 
-if (asprintf(CSS &filename, "%s", name) < 0) return NULL;
+/* Drop the ".lockfile" off the name */
+*Ustrrchr(filename, '.') = '\0';
 
 if (!(dbblock->dbptr = dbblock->readonly && !exim_lockfile_needed()
 		      ? exim_dbopen_multi(filename, dname, flags, 0)
@@ -447,7 +448,7 @@ Arguments:
   length    where to put the length (or NULL if length not wanted). Includes overhead.
 
 Returns: a pointer to the retrieved record, or
-         NULL if the record is not found
+         NULL if the record is not found or bad
 */
 
 void *
@@ -467,26 +468,42 @@ exim_datum_init(&result_datum);      /* to be cleared before use. */
 exim_datum_data_set(&key_datum, key_copy);
 exim_datum_size_set(&key_datum, klen);
 
-if (!exim_dbget(dbblock->dbptr, &key_datum, &result_datum)) return NULL;
-
-/* Assume for now that anything stored could have been tainted. Properly
-we should store the taint status along with the data. */
+if (!exim_dbget(dbblock->dbptr, &key_datum, &result_datum))
+  return NULL;
 
 dlen = exim_datum_size_get(&result_datum);
+DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: size %u return\n", dlen);
+if (length) *length = dlen;
 
 /* Hintsdb uses store the taint of the payload of the value in the value.
 For non-hints uses we have no provenance, so assume untainted */
 
-tainted = dbmdb
-  ? FALSE
-  : ((dbdata_generic *) exim_datum_data_get(&result_datum))->tainted;
+if (dbmdb)
+  tainted = FALSE;
+else
+  {
+  dbdata_generic * gp = (dbdata_generic *) exim_datum_data_get(&result_datum);
+  if (dlen < sizeof(dbdata_generic))
+    {
+    DEBUG(D_hints_lookup)
+      debug_printf_indent("dbfn_read: bad record size %u\n", dlen);
+    return NULL;
+    }
+  if (gp->version != HINTS_VERSION)
+    {
+    DEBUG(D_hints_lookup)
+      debug_printf_indent("dbfn_read: bad record version %u; deleting\n",
+			    gp->version);
+    return NULL;
+    }
+
+  tainted = gp->tainted;
+  }
 
 yield = store_get(dlen, tainted ? GET_TAINTED : GET_UNTAINTED);
 memcpy(yield, exim_datum_data_get(&result_datum), dlen);
-DEBUG(D_hints_lookup) debug_printf_indent("dbfn_read: size %u return\n", dlen);
-if (length) *length = dlen;
+exim_datum_free(&result_datum);		/* Some DBM libs require freeing */
 
-exim_datum_free(&result_datum);    /* Some DBM libs require freeing */
 return yield;
 }
 
@@ -726,13 +743,10 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 	  printf("%s ", name);
 	  t += MESSAGE_ID_LENGTH;
 	  }
-	printf("\n");
+	putchar('\n');
 	break;
 
       case type_misc:
-	/* It might be nice to recognize the subtypes of "misc" DBs (by the
-	key prefix?) for better decode.  For now, dump the raw data. */
-
 	{
 	dbdata_generic * recp = (dbdata_generic *)value;
 	uschar * dp = US (recp + 1);
@@ -1368,16 +1382,26 @@ the store each time round. */
 
 for (; keychain && (reset_point = store_mark()); store_reset(reset_point))
   {
-  dbdata_generic *value;
+  dbdata_generic * value;
+  int size = 0;
 
   key = keychain->key;
   keychain = keychain->next;
-  value = dbfn_read_with_length(dbm, key, NULL);
+  value = dbfn_read_with_length(dbm, key, &size);
 
   /* A continuation record may have been deleted or renamed already, so
-  non-existence is not serious. */
+  non-existence is not serious. Also, for a back-version record we get
+  a null value - so attempt a delete. */
 
-  if (!value) continue;
+  if (!value)
+    {
+    if (size)
+      {
+      printf(" deleted %s (bad record version)\n", key);
+      dbfn_delete(dbm, key);
+      }
+    continue;
+    }
 
   /* Delete if too old */
 
diff --git a/src/src/hintsdb_structs.h b/src/src/hintsdb_structs.h
index f4d72fdc9..fdca1f468 100644
--- a/src/src/hintsdb_structs.h
+++ b/src/src/hintsdb_structs.h
@@ -25,17 +25,20 @@ typedef struct {
 } open_db;
 
 
-/* Structures for records stored in exim database hinsts files. They all
+/* Structures for records stored in exim database hints files. They all
 start with the same fields, described in the generic type.
 DBM databases are used for hints files.
 */
 
 
 typedef struct {
-  time_t time_stamp;	/* Timestamp of writing */
+  unsigned version;
   BOOL	 tainted;	/* metadata for the "value" part of the record */
+  time_t time_stamp;	/* Timestamp of writing */
 } dbdata_generic;
 
+#define	HINTS_VERSION	2
+
 
 /* This structure keeps track of retry information for a host or a local
 address. */

commit 7873db47ac111972cc46b42e741fd437ec7a9869
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Dec 18 11:03:22 2025 +0000

    Add notes on alternates to prototype Makefile

diff --git a/src/src/EDITME b/src/src/EDITME
index 8977f624b..0edeedaa2 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -654,8 +654,8 @@ DISABLE_MAL_MKS=yes
 # Uncomment the following to remove the fast-ramp two-phase-queue-run support
 # DISABLE_QUEUE_RAMP=yes
 
-# Uncomment the following lines to add SRS (Sender Rewriting Scheme) support
-# using only native facilities.
+# Uncomment the following lines to add SRS (Sender Rewriting Scheme) support.
+# This needs no library.
 # SUPPORT_SRS=yes
 
 # Uncomment the following to remove support for the ESMTP extension "WELLKNOWN"
@@ -693,6 +693,9 @@ DISABLE_MAL_MKS=yes
 # 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
+#
+# As an alternative, see EXPERIMENTAL_DMARC_NATIVE
+# in doc-txt/experimental-spec.txt
 
 # Uncomment the following line to add ARC (Authenticated Received Chain)
 # support.  You must have SPF and DKIM support enabled also.
@@ -1177,6 +1180,8 @@ ZCAT_COMMAND=/usr/bin/zcat
 # LDFLAGS += -lspf2
 # SUPPORT_SPF_LIBS= -lspf2
 
+# As an alternative, see EXPERIMENTAL_SPF_PERL
+# in doc-txt/experimental-spec.txt
 
 #------------------------------------------------------------------------------
 # Support for authentication via Radius is also available. The Exim support,

commit dbb8fa7d6275bb2a21d9a7803b3ab902a2e15301
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Dec 18 13:30:02 2025 +0000

    Utils; fix exim_dumpdb build for non- pipe-connect

diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index f3842874f..3ef241a4e 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -763,6 +763,7 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 	else
 	  {
 	  uschar * s = keybuffer + Ustrlen(keybuffer) - 5;
+#ifndef DISABLE_PIPE_CONNECT
 	  if (s > keybuffer && Ustrcmp(s, ".EHLO") == 0)
 	    {
 	    ehlo_resp_precis * erp = &((dbdata_ehlo_resp *)recp)->data;
@@ -778,6 +779,7 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 	    putchar('\n');
 	    }
 	  else
+#endif
 	    printf("%s\n", string_sprintf("%.*q\n", dlen, dp));
 	  }
 	break;
@@ -847,7 +849,7 @@ for (uschar * key = dbfn_scan(dbm, TRUE, &cursor);
 	break;
 
       case type_dbm:
-	printf("%s\t%.*s\n", keybuffer, length, value);
+	printf("%s\t%.*s\n", keybuffer, length, CS value);
       }
   store_reset(reset_point);
   }

commit 0b98b3d655993f08104b9caf71be382cee57bd79
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Dec 18 13:35:42 2025 +0000

    Avoid function name conflict with Solaris libgen

diff --git a/src/src/acl.c b/src/src/acl.c
index 7ec3a898c..b236eb2de 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -4095,7 +4095,7 @@ for (; cb; cb = cb->next)
 
 #ifdef WITH_CONTENT_SCAN
     case ACLC_REGEX:
-      rc = regex(&arg, textonly);
+      rc = exim_regex(&arg, textonly);
       break;
 #endif
 
diff --git a/src/src/functions.h b/src/src/functions.h
index bfe34f391..1923ca5b6 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -446,7 +446,7 @@ extern int_eximarith_t receive_statvfs(BOOL, int *);
 extern void    receive_swallow_smtp(void);
 extern int     recv_fd_from_sock(int);
 #ifdef WITH_CONTENT_SCAN
-extern int     regex(const uschar **, BOOL);
+extern int     exim_regex(const uschar **, BOOL);
 extern void    regex_vars_clear(void);
 #endif
 extern void    regex_at_daemon(const uschar *);
diff --git a/src/src/regex.c b/src/src/regex.c
index ff52d349c..43f7a9403 100644
--- a/src/src/regex.c
+++ b/src/src/regex.c
@@ -113,7 +113,7 @@ for (int i = 0; i < REGEX_VARS; i++) regex_vars[i] = NULL;
 
 
 int
-regex(const uschar ** listptr, BOOL cacheable)
+exim_regex(const uschar ** listptr, BOOL cacheable)
 {
 unsigned long mbox_size;
 FILE * mbox_file;

commit 7b81731f51c53643c30d330596779a1c2f7eeb73
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Dec 18 16:04:24 2025 +0000

    Build: Solaris

diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index 9524dcef9..d0c00e431 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -601,7 +601,7 @@ exim_dumpdb: $(OBJ_DUMPDB)
 
 # The utility for interrogating/fixing the contents of an exim database
 
-OBJ_FIXDB = exim_fixdb.o util-os.o util-store.o util-md5.o
+OBJ_FIXDB = exim_fixdb.o util-os.o util-store.o util-string.o util-md5.o
 
 exim_fixdb:  $(OBJ_FIXDB)
 	@echo "$(LNCC) -o exim_fixdb"

commit fc6c642e1bfecab156b65c992697dbf1b3002b8b
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Dec 18 19:13:06 2025 +0000

    Build: Solaris

diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index d0c00e431..699f485c1 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -616,7 +616,7 @@ exim_fixdb:  $(OBJ_FIXDB)
 
 # The utility for tidying the contents of an exim database
 
-OBJ_TIDYDB = exim_tidydb.o util-os.o util-store.o
+OBJ_TIDYDB = exim_tidydb.o util-os.o util-store.o util-string.o
 
 exim_tidydb: $(OBJ_TIDYDB)
 	@echo "$(LNCC) -o exim_tidydb"

commit e46d2bb2bd33044b45cd9cc508a50f63d91aa0f8
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Dec 19 10:29:58 2025 +0000

    Utils: eliminate use of asprintf()

diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index 3ef241a4e..6ffcf764c 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -329,15 +329,16 @@ for the DB access mode will error out here. */
 
 if (dbmdb)
   {
-  if (asprintf(CSS &filename, "%s.lockfile", name) < 0)
-    return NULL;
+  filename = string_sprintf("%s.lockfile", name);
   dname = Ustrchr(name, '/')		/* path has dirs */
     ? US dirname(CS string_copy(name))
     : US ".";
   }
-else if (  asprintf(CSS &dname, "%s/db", spool_directory) < 0
-	|| asprintf(CSS &filename, "%s/%s.lockfile", dname, name) < 0)
-  return NULL;
+else
+  {
+  dname = string_sprintf("%s/db", spool_directory);
+  filename = string_sprintf("%s/%s.lockfile", dname, name);
+  }
 
 dbblock->readonly = (flags & O_ACCMODE) == O_RDONLY;
 dbblock->lockfd = -1;

commit cb28da1d541b29ce86ea862f00f6c278117cbcb9
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Dec 19 20:27:36 2025 +0000

    Tidying

diff --git a/src/src/exim.c b/src/src/exim.c
index e955d2488..d8851d782 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -85,7 +85,11 @@ function_store_nullfree(void * block, void * tag)
 *************************************************/
 
 enum commandline_info { CMDINFO_NONE=0,
-  CMDINFO_HELP, CMDINFO_SIEVE, CMDINFO_DSCP };
+  CMDINFO_HELP, CMDINFO_SIEVE,
+#ifdef SUPPORT_DSCP
+  CMDINFO_DSCP
+#endif
+};
 
 
 
diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index 6ffcf764c..b6977513b 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -1254,7 +1254,7 @@ for(; (reset_point = store_mark()); store_reset(reset_point))
 	break;
 
       case type_dbm:
-	printf("0 value:  %.*s\n", oldlength, record);
+	printf("0 value:  %.*s\n", oldlength, CS record);
       }
     }
 
diff --git a/src/src/miscmods/dmarc_common.c b/src/src/miscmods/dmarc_common.c
index 35b832c59..ab0027f17 100644
--- a/src/src/miscmods/dmarc_common.c
+++ b/src/src/miscmods/dmarc_common.c
@@ -15,7 +15,7 @@ extern BOOL		dmarc_local_init(void);
 extern void		dmarc_local_msg_init(void);
 extern gstring *	dmarc_version_report(gstring *);
 extern int		dmarc_process(void);
-extern const uschar *	dmarc_result_inlist(const uschar * const *);
+extern int		dmarc_result_inlist(const uschar * const *);
 
 
 /* Other modules needed for services */
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index 80a70b490..7274050f1 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -1126,8 +1126,6 @@ sx->pending_EHLO = FALSE;
 
 if (pending_BANNER)
   {
-  const uschar * s;
-
   DEBUG(D_transport) debug_printf("%s expect banner\n", __FUNCTION__);
   (*countp)--;
   if (!smtp_reap_banner(sx))
@@ -1140,9 +1138,12 @@ if (pending_BANNER)
 
 # if !defined(DISABLE_TLS) && !defined(DISABLE_TLS_RESUME)
   GET_OPTION("host_name_extract");
-  s = ((smtp_transport_options_block *)sx->conn_args.ob)->host_name_extract;
-  if (!s) s = HNE_DEFAULT;
-  ehlo_response_lbserver(sx, s);
+    {
+    const uschar * s;
+    s = ((smtp_transport_options_block *)sx->conn_args.ob)->host_name_extract;
+    if (!s) s = HNE_DEFAULT;
+    ehlo_response_lbserver(sx, s);
+    }
 # endif
   }
 
@@ -2677,8 +2678,6 @@ goto SEND_QUIT;
 #ifndef DISABLE_TLS
   if (sx->smtps)
     {
-    const uschar * s;
-
     smtp_peer_options |= OPTION_TLS;
     suppress_tls = FALSE;
     ob->tls_tempfail_tryclear = FALSE;
@@ -2690,8 +2689,11 @@ goto SEND_QUIT;
     force resumption attempts. */
 
     GET_OPTION("host_name_extract");
-    if (!(s = ob->host_name_extract)) s = US"never-LB";
-    ehlo_response_lbserver(sx, s);
+      {
+      const uschar * s;
+      if (!(s = ob->host_name_extract)) s = US"never-LB";
+      ehlo_response_lbserver(sx, s);
+      }
 # endif
     smtp_record_protocol_sequence(sx, US"s");
     goto TLS_NEGOTIATE;
@@ -2784,8 +2786,6 @@ goto SEND_QUIT;
     if (!sx->early_pipe_active)
 #endif
       {
-      const uschar * s;
-
       sx->peer_offered = ehlo_response(sx->buffer,
 	OPTION_TLS	/* others checked later */
 #ifndef DISABLE_PIPE_CONNECT
@@ -2824,8 +2824,11 @@ goto SEND_QUIT;
 #endif
 #if !defined(DISABLE_TLS) && !defined(DISABLE_TLS_RESUME)
       GET_OPTION("host_name_extract");
-      if (!(s = ob->host_name_extract)) s = HNE_DEFAULT;
-      ehlo_response_lbserver(sx, s);
+	{
+	const uschar * s;
+	if (!(s = ob->host_name_extract)) s = HNE_DEFAULT;
+	ehlo_response_lbserver(sx, s);
+	}
 #endif
       }
 

commit 8783c47fcae5a69aea91cb37da694070185569e5
Author: Andreas Metzler <ametzler@bebt.de>
Date:   Sun Dec 21 12:42:13 2025 +0000

    typo, c'n'p errors

diff --git a/src/src/EDITME b/src/src/EDITME
index 0edeedaa2..21d81f43c 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -320,8 +320,8 @@ SPOOL_DIRECTORY=/var/spool/exim
 # is historic).
 # You need to add -export-dynamic -rdynamic to EXTRALIBS. You may also need to
 # add -ldl to EXTRALIBS so that dlopen() is available to Exim. You need to
-# define CFLAGS_DYNAIC and LOOKUP_MODULE_DIR below so the builds are done right,
-# and so the exim binary actually loads dynamic lookup modules.
+# define CFLAGS_DYNAMIC and LOOKUP_MODULE_DIR below so the builds are done
+# right, and so the exim binary actually loads dynamic lookup modules.
 
 ROUTER_ACCEPT=yes
 ROUTER_DNSLOOKUP=yes
@@ -350,8 +350,8 @@ ROUTER_REDIRECT=yes
 # is historic).
 # You need to add -export-dynamic -rdynamic to EXTRALIBS. You may also need to
 # add -ldl to EXTRALIBS so that dlopen() is available to Exim. You need to
-# define CFLAGS_DYNAIC and LOOKUP_MODULE_DIR below so the builds are done right,
-# and so the exim binary actually loads dynamic lookup modules.
+# define CFLAGS_DYNAMIC and LOOKUP_MODULE_DIR below so the builds are done
+# right, and so the exim binary actually loads dynamic lookup modules.
 # The smtp transport cannot be built as a module.
 
 TRANSPORT_APPENDFILE=yes
@@ -874,7 +874,7 @@ FIXED_NEVER_USERS=root
 # right and so the exim binary actually loads dynamic lookup modules.
 #
 # Libraries being built as modules should be added to respective
-# LOOKUP_*_INCLUDE and LOOKUP_*_LIBS rather than the the ones for the
+# AUTH_*_INCLUDE and AUTH_*_LIBS rather than the the ones for the
 # core exim build.  This gets them linked with the module instead.
 # The heimdal does build but we have no test coverage so it is not know to work.
 

commit 91586f969ddddc4ff4339167c05141141a4b6272
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Dec 21 15:54:59 2025 +0000

    Cmdline option for list of dynamic-load modules

diff --git a/src/src/drtables.c b/src/src/drtables.c
index 833c352c0..1b9dc0c98 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -224,6 +224,7 @@ const pcre2_code * regex_islookupmod = regex_must_compile(
 if (!(dd = exim_opendir(CUS LOOKUP_MODULE_DIR)))
   g = string_cat(g, US"FAIL exim_opendir");
 else
+  {
   for (struct dirent * ent; ent = readdir(dd); )
     {
     void * dl;
@@ -246,6 +247,8 @@ else
       dlclose(dl);
       }
     }
+  closedir(dd);
+  }
 #endif	/*!LOOKUP_MODULE_DIR*/
 return g;
 }
@@ -574,5 +577,37 @@ onetime = TRUE;
 }
 
 
+void
+mod_names(FILE * stream)
+{
+#ifdef LOOKUP_MODULE_DIR
+DIR * dd;
+gstring * list = NULL;
+const pcre2_code * regex_ismodule = regex_must_compile(
+  US"^([a-z0-9]+)_(?:lookup|auth|router|transport|miscmod)\\."
+    DYNLIB_FN_EXT "$",
+  MCS_NOFLAGS, TRUE);
+
+if ((dd = exim_opendir(CUS LOOKUP_MODULE_DIR)))
+  {
+  for (struct dirent * ent; ent = readdir(dd); )
+    if (regex_match_and_setup(regex_ismodule, US ent->d_name, 0, 0))
+      list = string_append_listele(list, ' ', expand_nstring[1]);
+  closedir(dd);
+
+  if (list)
+    fprintf(stream, "Installed modules: %s\n", string_from_gstring(list));
+  else
+    fprintf(stream, "No modules are installed\n");
+  }
+else
+  fprintf(stream, "Modules directory not readable\n");
+
+#else
+fprintf(stream, "Loadable-module support is not available\n");
+#endif
+}
+
+
 #endif	/*!MACRO_PREDEF*/
 /* End of drtables.c */
diff --git a/src/src/exim.c b/src/src/exim.c
index d8851d782..055882ad0 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -85,7 +85,7 @@ function_store_nullfree(void * block, void * tag)
 *************************************************/
 
 enum commandline_info { CMDINFO_NONE=0,
-  CMDINFO_HELP, CMDINFO_SIEVE,
+  CMDINFO_HELP, CMDINFO_SIEVE, CMDINFO_MODULES,
 #ifdef SUPPORT_DSCP
   CMDINFO_DSCP
 #endif
@@ -1392,9 +1392,13 @@ switch(request)
 #ifdef SUPPORT_DSCP
 "  exim -bI:dscp    list of known dscp value keywords\n"
 #endif
+"  exim -bI:modules list of loadable modules\n"
 "  exim -bI:sieve   list of supported sieve extensions\n"
 );
     return;
+  case CMDINFO_MODULES:
+    mod_names(stream);
+    return;
   case CMDINFO_SIEVE:
     {
     const misc_module_info * mi;
@@ -2436,6 +2440,8 @@ on the second character (the one after '-'), to save some effort. */
 	    if (Ustrlen(p))
 	      if (strcmpic(p, CUS"sieve") == 0)
 		{ info_flag = CMDINFO_SIEVE; info_stdout = TRUE; }
+	      else if (strcmpic(p, CUS"modules") == 0)
+		{ info_flag = CMDINFO_MODULES; info_stdout = TRUE; }
 #ifdef SUPPORT_DSCP
 	      else if (strcmpic(p, CUS"dscp") == 0)
 		{ info_flag = CMDINFO_DSCP; info_stdout = TRUE; }
diff --git a/src/src/functions.h b/src/src/functions.h
index 1923ca5b6..fe9d718ea 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -376,6 +376,7 @@ extern void    moan_write_references(FILE *, uschar *);
 #ifdef LOOKUP_MODULE_DIR
 //extern void    mod_load_check(const uschar *);
 #endif
+extern void    mod_names(FILE *);
 extern FILE   *modefopen(const uschar *, const char *, mode_t);
 
 extern int     open_cutthrough_connection(address_item *, BOOL);

commit 6d75a89d2f506f88cfddf092440af5b2d54f07df
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Dec 21 18:01:26 2025 +0000

    Invalidate more variables at smtp-reset

diff --git a/src/src/deliver.c b/src/src/deliver.c
index cf3a41156..a984ee21e 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -174,8 +174,10 @@ deliver_set_expansions(address_item * addr)
 {
 if (!addr)
   {
-  const uschar ***p = address_expansions;
-  while (*p) **p++ = NULL;
+  for (const uschar ***p = address_expansions; *p; ) **p++ = NULL;
+  deliver_host_port = 0;
+  deliver_recipients = NULL;
+  router_var = NULL;
   return;
   }
 
diff --git a/src/src/globals.c b/src/src/globals.c
index 2d1354236..d04feb24f 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -188,6 +188,8 @@ when verifying one address while routing/verifying another. We have to have
 the size explicit, because it is referenced from more than one module. */
 
 const uschar **address_expansions[ADDRESS_EXPANSIONS_COUNT] = {
+  CUSS &address_file,
+  CUSS &address_pipe,
   CUSS &deliver_address_data,
   CUSS &deliver_domain,
   CUSS &deliver_domain_data,
@@ -198,12 +200,13 @@ const uschar **address_expansions[ADDRESS_EXPANSIONS_COUNT] = {
   CUSS &deliver_localpart_orig,
   CUSS &deliver_localpart_parent,
   CUSS &deliver_localpart_prefix,
+  CUSS &deliver_localpart_prefix_v,
   CUSS &deliver_localpart_suffix,
+  CUSS &deliver_localpart_suffix_v,
   CUSS (uschar **)(&deliver_recipients),
   CUSS &deliver_host,
+  CUSS &deliver_host_address,
   CUSS &deliver_home,
-  CUSS &address_file,
-  CUSS &address_pipe,
   CUSS &self_hostname,
   NULL };
 
diff --git a/src/src/log.c b/src/src/log.c
index 41f91a1ad..64587a9f2 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -1323,7 +1323,7 @@ va_end(ap);
 }
 
 /* As the above, but adds in LOG_PANIC_DIE.
-We have this as a wripper so that we can mark it as never returning,
+We have this as a wrapper so that we can mark it as never returning,
 for the benefit of static analysers. */
 
 void
diff --git a/src/src/macros.h b/src/src/macros.h
index 6c7d8b085..0070ef7ed 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -119,7 +119,7 @@ don't make the file descriptors two-way. */
 verifying. This has to be explicit because it is referenced in more than one
 source module. */
 
-#define ADDRESS_EXPANSIONS_COUNT 19
+#define ADDRESS_EXPANSIONS_COUNT 21
 
 /* The maximum permitted number of command-line (-D) macro definitions. We
 need a limit only to make it easier to generate argument vectors for re-exec
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index abc893ddc..da919048c 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -1736,6 +1736,8 @@ Returns:    a replacement reset point
 rmark
 smtp_reset(rmark reset_point)
 {
+deliver_set_expansions(NULL);
+
 recipients_list = NULL;
 rcpt_count = rcpt_defer_count = rcpt_fail_count =
   raw_recipients_count = recipients_count = recipients_list_max = 0;
@@ -1760,11 +1762,8 @@ f.active_local_from_check = local_from_check;		/* Can be set by ACL */
 f.active_local_sender_retain = local_sender_retain;	/* Can be set by ACL */
 sending_ip_address = NULL;
 return_path = sender_address = NULL;
-deliver_localpart_data = deliver_domain_data =
 recipient_data = sender_data = NULL;			/* Can be set by ACL */
 recipient_verify_failure = NULL;
-deliver_localpart_parent = deliver_localpart_orig = NULL;
-deliver_domain_parent = deliver_domain_orig = NULL;
 callout_address = NULL;
 submission_name = NULL;					/* Can be set by ACL */
 raw_sender = NULL;                  /* After SMTP rewrite, before qualifying */
@@ -1778,7 +1777,6 @@ dnslist_domain = dnslist_matched = NULL;
 
 dsn_ret = 0;
 dsn_envid = NULL;
-deliver_host = deliver_host_address = NULL;	/* Can be set by ACL */
 #ifndef DISABLE_TLS
  memset(&tls_out, 0, sizeof(tls_out));		/* Can be set by callout */
  tls_out.active.sock = -1;

commit bfd2759002fd2b5b376a1544e8fc59f313dc4e9c
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Dec 22 10:10:01 2025 +0000

    Disable -bI:sieve for sieve-less builds.  Bug 3184

diff --git a/src/src/EDITME b/src/src/EDITME
index 21d81f43c..f641a6699 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -566,7 +566,6 @@ SUPPORT_DANE=yes
 
 # SUPPORT_EXIM_FILTER=2
 
-#------------------------------------------------------------------------------
 # Compiling with support for Sieve filters is the default. To disable this
 # uncomment the line below.
 
@@ -577,6 +576,7 @@ SUPPORT_DANE=yes
 
 # SUPPORT_SIEVE_FILTER=2
 
+#------------------------------------------------------------------------------
 # Uncomment the line to include DSCP support.  Set to 2 for a module build.
 # SUPPORT_DSCP
 
diff --git a/src/src/drtables.c b/src/src/drtables.c
index 1b9dc0c98..29ae84bd6 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -348,8 +348,9 @@ if ((mi = misc_mod_findonly(name))) return mi;
 #ifdef LOOKUP_MODULE_DIR
 return misc_mod_load(name, errstr);
 #else
-*errstr = string_sprintf("module %q not built-in, and"
-	" no setting for LOOKUP_MODULE_DIR", name);
+if (errstr)
+  *errstr = string_sprintf("module %q not built-in, and"
+	  " no setting for LOOKUP_MODULE_DIR", name);
 return NULL;
 #endif	/*LOOKUP_MODULE_DIR*/
 }
diff --git a/src/src/exim.c b/src/src/exim.c
index 055882ad0..8c5089f7b 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -85,7 +85,10 @@ function_store_nullfree(void * block, void * tag)
 *************************************************/
 
 enum commandline_info { CMDINFO_NONE=0,
-  CMDINFO_HELP, CMDINFO_SIEVE, CMDINFO_MODULES,
+  CMDINFO_HELP, CMDINFO_MODULES,
+#ifndef DISABLE_SIEVE_FILTER
+  CMDINFO_SIEVE,
+#endif
 #ifdef SUPPORT_DSCP
   CMDINFO_DSCP
 #endif
@@ -1393,27 +1396,30 @@ switch(request)
 "  exim -bI:dscp    list of known dscp value keywords\n"
 #endif
 "  exim -bI:modules list of loadable modules\n"
+#ifndef DISABLE_SIEVE_FILTER
 "  exim -bI:sieve   list of supported sieve extensions\n"
+#endif
 );
     return;
   case CMDINFO_MODULES:
     mod_names(stream);
     return;
+#ifndef DISABLE_SIEVE_FILTER
   case CMDINFO_SIEVE:
     {
-    const misc_module_info * mi;
+    misc_module_info * mi = misc_mod_find(US"sieve_filter", NULL);
     typedef void (*fn_t)(FILE *);
-    if ((mi = misc_mod_find(US"sieve_filter", NULL)))
+    if (mi)
       (((fn_t *) mi->functions)[SIEVE_EXTENSIONS]) (stream);
     else
       fprintf(stream, "Sieve filtering not available\n");
     }
     return;
+#endif
 #ifdef SUPPORT_DSCP
   case CMDINFO_DSCP:
     {
-    uschar * dummy_errstr;
-    misc_module_info * mi = misc_mod_find(US"dscp", &dummy_errstr);
+    misc_module_info * mi = misc_mod_find(US"dscp", NULL);
     typedef void (*fn_t)(FILE *);
     if (mi) ((fn_t *) mi->functions)[DSCP_KEYWORDS] (stream);
     }
@@ -2438,10 +2444,12 @@ on the second character (the one after '-'), to save some effort. */
 	    const uschar * p = argrest+1;
 	    info_flag = CMDINFO_HELP;
 	    if (Ustrlen(p))
-	      if (strcmpic(p, CUS"sieve") == 0)
-		{ info_flag = CMDINFO_SIEVE; info_stdout = TRUE; }
-	      else if (strcmpic(p, CUS"modules") == 0)
+	      if (strcmpic(p, CUS"modules") == 0)
 		{ info_flag = CMDINFO_MODULES; info_stdout = TRUE; }
+#ifndef DISABLE_SIEVE_FILTER
+	      else if (strcmpic(p, CUS"sieve") == 0)
+		{ info_flag = CMDINFO_SIEVE; info_stdout = TRUE; }
+#endif
 #ifdef SUPPORT_DSCP
 	      else if (strcmpic(p, CUS"dscp") == 0)
 		{ info_flag = CMDINFO_DSCP; info_stdout = TRUE; }
diff --git a/src/src/expand.c b/src/src/expand.c
index b0500037d..85d27b534 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -2802,7 +2802,8 @@ switch(cond_type = identify_operator(&s, &opname))
     case ECOND_RADIUS:
 #ifdef RADIUS_CONFIG_FILE
       {
-      const misc_module_info * mi = misc_mod_find(US"radius", NULL);
+      uschar * dummy_errstr;
+      const misc_module_info * mi = misc_mod_find(US"radius", &dummy_errstr);
       typedef int (*fn_t)(const uschar *, uschar **);
       if (!mi)
 	goto COND_FAILED_NOT_COMPILED;
diff --git a/src/src/lookups/spf.c b/src/src/lookups/spf.c
index a8404d9bf..e41e1dcb0 100644
--- a/src/src/lookups/spf.c
+++ b/src/src/lookups/spf.c
@@ -84,8 +84,7 @@ return FAIL;
 gstring *
 spf_version_report(gstring * g)
 {
-uschar * dummy_errmsg;
-misc_module_info * mi = misc_mod_find(US"spf", &dummy_errmsg);
+misc_module_info * mi = misc_mod_find(US"spf", NULL);
 return mi && mi->lib_vers_report ? mi->lib_vers_report(g) : g;
 }
 
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index da919048c..643d88ca2 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -2669,11 +2669,10 @@ Failure will not allow any SMTP function other than QUIT. */
 f.quit_cmd_only = FALSE;
 if (hosts_proxy)
   {
-  uschar * dummy_errmsg;
   misc_module_info * mi;
   typedef BOOL (*fn_t) (void);
 
-  if (!(mi = misc_mod_find(US"proxy", &dummy_errmsg)))
+  if (!(mi = misc_mod_find(US"proxy", NULL)))
     {
     smtp_closedown(US"Temporary local problem - please try later");
     return FALSE;
@@ -4443,11 +4442,10 @@ while (done <= 0)
 #ifdef EXPERIMENTAL_XCLIENT
 	if (proxy_session || verify_check_host(&hosts_xclient) != FAIL)
 	  {
-	  uschar * dummy_errmsg;
 	  typedef gstring * (*fn_t) (gstring *);
 
 	  if (  !xclient_mi
-	     && !(xclient_mi = misc_mod_find(US"xclient", &dummy_errmsg)))
+	     && !(xclient_mi = misc_mod_find(US"xclient", NULL)))
 	    {
 	    smtp_closedown(US"Temporary local problem - please try later");
 	    return FALSE;
diff --git a/src/src/smtp_out.c b/src/src/smtp_out.c
index a5557d09e..2712ee0ce 100644
--- a/src/src/smtp_out.c
+++ b/src/src/smtp_out.c
@@ -296,8 +296,7 @@ bomb out, just log it and continue in default traffic class. */
   GET_OPTION("dscp");
   if (ob->dscp)
     {
-    uschar * dummy_errstr;
-    misc_module_info * mi = misc_mod_find(US"dscp", &dummy_errstr);
+    misc_module_info * mi = misc_mod_find(US"dscp", NULL);
     typedef void (*fn_t)(int, const uschar *, int);
     if (mi)
       ((fn_t *) mi->functions)[DSCP_TRANSPORT] (sock, ob->dscp, sc->host_af);
@@ -521,8 +520,7 @@ if (ob->socks_proxy)
     }
   if (*ob->socks_proxy)
     {
-    uschar * dummy_errmsg;
-    misc_module_info * mi = misc_mod_find(US"socks", &dummy_errmsg);
+    misc_module_info * mi = misc_mod_find(US"socks", NULL);
     typedef int (*fn_t) (const smtp_connect_args *, const blob *);
     return mi ? ((fn_t *) mi->functions)[SOCKS_CONNECT] (sc, early_data) : -1;
     }

commit 711a7387e0200c5dda3f9d0a744dae18afb80351
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Dec 23 13:07:35 2025 +0000

    Keep handle open for module dir

diff --git a/src/src/drtables.c b/src/src/drtables.c
index 29ae84bd6..ec9ca3c67 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -221,7 +221,7 @@ DIR * dd;
 const pcre2_code * regex_islookupmod = regex_must_compile(
   US"^([a-z0-9]+)_lookup\\." DYNLIB_FN_EXT "$", MCS_NOFLAGS, TRUE);
 
-if (!(dd = exim_opendir(CUS LOOKUP_MODULE_DIR)))
+if (!(dd = open_module_dir()))
   g = string_cat(g, US"FAIL exim_opendir");
 else
   {
@@ -247,7 +247,6 @@ else
       dlclose(dl);
       }
     }
-  closedir(dd);
   }
 #endif	/*!LOOKUP_MODULE_DIR*/
 return g;
@@ -448,7 +447,7 @@ DEBUG(D_lookup) debug_printf_indent("Total %d built-in lookups\n", lookup_list_c
 
 
 #ifdef LOOKUP_MODULE_DIR
-if (!(dd = exim_opendir(CUS LOOKUP_MODULE_DIR)))
+if (!(dd = open_module_dir()))
   {
   EARLY_DEBUG(D_lookup, "Couldn't open %s: not loading lookup modules\n", LOOKUP_MODULE_DIR);
   log_write(0, LOG_MAIN|LOG_PANIC,
@@ -476,7 +475,6 @@ else
 	log_write(0, LOG_MAIN|LOG_PANIC, "%s", errstr);
 	}
       }
-  closedir(dd);
   }
 
 EARLY_DEBUG(D_lookup, "Loaded %d dynamic lookup modules\n", countmodules);
@@ -589,12 +587,11 @@ const pcre2_code * regex_ismodule = regex_must_compile(
     DYNLIB_FN_EXT "$",
   MCS_NOFLAGS, TRUE);
 
-if ((dd = exim_opendir(CUS LOOKUP_MODULE_DIR)))
+if ((dd = open_module_dir()))
   {
   for (struct dirent * ent; ent = readdir(dd); )
     if (regex_match_and_setup(regex_ismodule, US ent->d_name, 0, 0))
       list = string_append_listele(list, ' ', expand_nstring[1]);
-  closedir(dd);
 
   if (list)
     fprintf(stream, "Installed modules: %s\n", string_from_gstring(list));
diff --git a/src/src/functions.h b/src/src/functions.h
index fe9d718ea..b2c2626ab 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -1327,7 +1327,7 @@ if (f.running_in_test_harness && f.testsuite_delays) millisleep(millisec);
 /* Taint-checked file opens. Return values/errno per open(2). */
 
 static inline int
-exim_open2(const char *pathname, int flags)
+exim_open2(const char * pathname, int flags)
 {
 if (!is_tainted(pathname)) return open(pathname, flags);
 log_write(0, LOG_MAIN|LOG_PANIC, "Tainted filename '%s'", pathname);
@@ -1344,7 +1344,7 @@ return -1;
 }
 #ifdef EXIM_HAVE_OPENAT
 static inline int
-exim_openat(int dirfd, const char *pathname, int flags)
+exim_openat(int dirfd, const char * pathname, int flags)
 {
 if (!is_tainted(pathname)) return openat(dirfd, pathname, flags);
 log_write(0, LOG_MAIN|LOG_PANIC, "Tainted filename '%s'", pathname);
@@ -1362,7 +1362,7 @@ return -1;
 #endif
 
 static inline FILE *
-exim_fopen(const char *pathname, const char *mode)
+exim_fopen(const char * pathname, const char * mode)
 {
 if (!is_tainted(pathname)) return fopen(pathname, mode);
 log_write(0, LOG_MAIN|LOG_PANIC, "Tainted filename '%s'", pathname);
@@ -1379,6 +1379,18 @@ errno = EACCES;
 return NULL;
 }
 
+#ifdef LOOKUP_MODULE_DIR
+static inline DIR *
+open_module_dir(void)
+{
+if (module_dir)
+  rewinddir(module_dir);
+else
+  module_dir = exim_opendir(CUS LOOKUP_MODULE_DIR);
+return module_dir;
+}
+#endif
+
 /******************************************************************************/
 # if !defined(COMPILE_UTILITY)
 
diff --git a/src/src/globals.c b/src/src/globals.c
index d04feb24f..e5de80ea4 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -1101,6 +1101,9 @@ int     mime_is_coverletter    = 0;
 int     mime_is_rfc822         = 0;
 int     mime_part_count        = -1;
 #endif
+#ifdef LOOKUP_MODULE_DIR
+DIR *	module_dir	       = NULL;
+#endif
 
 uid_t  *never_users            = NULL;
 uschar *notifier_socket        = US"$spool_directory/" NOTIFIER_SOCKET_NAME ;
diff --git a/src/src/globals.h b/src/src/globals.h
index 52c46f169..313062890 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -769,6 +769,9 @@ extern int     mime_is_coverletter;
 extern int     mime_is_rfc822;
 extern int     mime_part_count;
 #endif
+#ifdef LOOKUP_MODULE_DIR
+extern DIR *   module_dir;
+#endif
 
 extern BOOL    mua_wrapper;            /* TRUE when Exim is wrapping an MUA */
 
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 7840fd0ac..417a786e3 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -3819,7 +3819,7 @@ for (di = *info_anchor; di; di = di->next)
 #ifdef LOOKUP_MODULE_DIR
 /* Potentially a loadable module. Look for a file with the right name. */
 
-if (!(dd = exim_opendir(CUS LOOKUP_MODULE_DIR)))
+if (!(dd = open_module_dir()))
   log_write(0, LOG_MAIN|LOG_PANIC,
 	  "Couldn't open %s: not loading driver modules\n", LOOKUP_MODULE_DIR);
 else
@@ -3828,7 +3828,7 @@ else
 				  d->driver_name, class);
   const char * errormsg;
 
-  DEBUG(D_any) debug_printf("Loading %s %s driver from %s\n",
+  DEBUG(D_any) debug_printf("Loading %q %s driver from %s\n",
 			    d->driver_name, class, LOOKUP_MODULE_DIR);
 
   for(struct dirent * ent; ent = readdir(dd); ) if (Ustrcmp(ent->d_name, fname) == 0)
@@ -3866,7 +3866,6 @@ else
 	store_pool = old_pool;
 	DEBUG(D_any)
 	  debug_printf("Loaded module %q (%s)\n", d->driver_name, class);
-	closedir(dd);
 	goto found;
 	}
 
@@ -3876,7 +3875,6 @@ else
     dlclose(dl);
     break;
     }
-  closedir(dd);
   }
 #endif	/* LOOKUP_MODULE_DIR */
 

commit b3881e820e87ccaabfb2c4a08d3cd07974c59b75
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Dec 24 13:48:55 2025 +0000

    Move lookup options & variables from global to module

diff --git a/src/src/drtables.c b/src/src/drtables.c
index ec9ca3c67..20034e7ce 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -152,7 +152,7 @@ return dl;
 /* Try to load a lookup module with the given name.
 
 Arguments:
-    name		name of the lookup
+    name		name of the module
     errstr		if not NULL, place "open fail" error message here
 
 Return: boolean success
@@ -211,6 +211,15 @@ return TRUE;
 
 #endif	/*LOOKUP_MODULE_DIR*/
 
+const lookup_info *
+lookup_find(const uschar * name, uschar ** errstr)
+{
+const lookup_info * li;
+if ((li = lookup_findonly(name))) return li;
+lookup_mod_load(name, errstr);
+return lookup_findonly(name);
+}
+
 /* Look at all the lookup module files and add a name from each lookup type */
 
 gstring *
diff --git a/src/src/expand.c b/src/src/expand.c
index 85d27b534..ab0c0b37a 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -459,10 +459,10 @@ static var_entry var_table[] = {
   { "address_file",        vtype_stringptr,   &address_file },
   { "address_pipe",        vtype_stringptr,   &address_pipe },
 #ifdef EXPERIMENTAL_ARC
-  { "arc_domains",         vtype_module,	US"arc" },
-  { "arc_oldest_pass",     vtype_module,	US"arc" },
-  { "arc_state",           vtype_module,	US"arc" },
-  { "arc_state_reason",    vtype_module,	US"arc" },
+  { "arc_domains",         vtype_misc_module,	US"arc" },
+  { "arc_oldest_pass",     vtype_misc_module,	US"arc" },
+  { "arc_state",           vtype_misc_module,	US"arc" },
+  { "arc_state_reason",    vtype_misc_module,	US"arc" },
 #endif
   { "atrn_host",	   vtype_stringptr,   &atrn_host },
   { "atrn_mode",	   vtype_stringptr,   &atrn_mode },
@@ -491,36 +491,36 @@ static var_entry var_table[] = {
   { "dcc_result",          vtype_stringptr,   &dcc_result },
 #endif
 #ifndef DISABLE_DKIM
-  { "dkim_algo",           vtype_module,	US"dkim" },
-  { "dkim_bodylength",     vtype_module,	US"dkim" },
-  { "dkim_canon_body",     vtype_module,	US"dkim" },
-  { "dkim_canon_headers",  vtype_module,	US"dkim" },
-  { "dkim_copiedheaders",  vtype_module,	US"dkim" },
-  { "dkim_created",        vtype_module,	US"dkim" },
-  { "dkim_cur_signer",     vtype_module,	US"dkim" },
-  { "dkim_domain",         vtype_module,	US"dkim" },
-  { "dkim_expires",        vtype_module,	US"dkim" },
-  { "dkim_headernames",    vtype_module,	US"dkim" },
-  { "dkim_identity",       vtype_module,	US"dkim" },
-  { "dkim_key_granularity",vtype_module,	US"dkim" },
-  { "dkim_key_length",     vtype_module,	US"dkim" },
-  { "dkim_key_nosubdomains",vtype_module,	US"dkim" },
-  { "dkim_key_notes",      vtype_module,	US"dkim" },
-  { "dkim_key_srvtype",    vtype_module,	US"dkim" },
-  { "dkim_key_testing",    vtype_module,	US"dkim" },
-  { "dkim_selector",       vtype_module,	US"dkim" },
-  { "dkim_signers",        vtype_module,	US"dkim" },
-  { "dkim_verify_reason",  vtype_module,	US"dkim" },
-  { "dkim_verify_signers", vtype_module,	US"dkim" },
-  { "dkim_verify_status",  vtype_module,	US"dkim" },
+  { "dkim_algo",           vtype_misc_module,	US"dkim" },
+  { "dkim_bodylength",     vtype_misc_module,	US"dkim" },
+  { "dkim_canon_body",     vtype_misc_module,	US"dkim" },
+  { "dkim_canon_headers",  vtype_misc_module,	US"dkim" },
+  { "dkim_copiedheaders",  vtype_misc_module,	US"dkim" },
+  { "dkim_created",        vtype_misc_module,	US"dkim" },
+  { "dkim_cur_signer",     vtype_misc_module,	US"dkim" },
+  { "dkim_domain",         vtype_misc_module,	US"dkim" },
+  { "dkim_expires",        vtype_misc_module,	US"dkim" },
+  { "dkim_headernames",    vtype_misc_module,	US"dkim" },
+  { "dkim_identity",       vtype_misc_module,	US"dkim" },
+  { "dkim_key_granularity",vtype_misc_module,	US"dkim" },
+  { "dkim_key_length",     vtype_misc_module,	US"dkim" },
+  { "dkim_key_nosubdomains",vtype_misc_module,	US"dkim" },
+  { "dkim_key_notes",      vtype_misc_module,	US"dkim" },
+  { "dkim_key_srvtype",    vtype_misc_module,	US"dkim" },
+  { "dkim_key_testing",    vtype_misc_module,	US"dkim" },
+  { "dkim_selector",       vtype_misc_module,	US"dkim" },
+  { "dkim_signers",        vtype_misc_module,	US"dkim" },
+  { "dkim_verify_reason",  vtype_misc_module,	US"dkim" },
+  { "dkim_verify_signers", vtype_misc_module,	US"dkim" },
+  { "dkim_verify_status",  vtype_misc_module,	US"dkim" },
 #endif
 #ifdef EXIM_HAVE_DMARC
-  { "dmarc_alignment_dkim",vtype_module,	US"dmarc" },
-  { "dmarc_alignment_spf", vtype_module,	US"dmarc" },
-  { "dmarc_domain_policy", vtype_module,	US"dmarc" },
-  { "dmarc_status",        vtype_module,	US"dmarc" },
-  { "dmarc_status_text",   vtype_module,	US"dmarc" },
-  { "dmarc_used_domain",   vtype_module,	US"dmarc" },
+  { "dmarc_alignment_dkim",vtype_misc_module,	US"dmarc" },
+  { "dmarc_alignment_spf", vtype_misc_module,	US"dmarc" },
+  { "dmarc_domain_policy", vtype_misc_module,	US"dmarc" },
+  { "dmarc_status",        vtype_misc_module,	US"dmarc" },
+  { "dmarc_status_text",   vtype_misc_module,	US"dmarc" },
+  { "dmarc_used_domain",   vtype_misc_module,	US"dmarc" },
 #endif
   { "dnslist_domain",      vtype_stringptr,   &dnslist_domain },
   { "dnslist_matched",     vtype_stringptr,   &dnslist_matched },
@@ -554,7 +554,7 @@ static var_entry var_table[] = {
   { "interface_port",      vtype_int,         &interface_port },
   { "item",                vtype_stringptr,   &iterate_item },
 #ifdef LOOKUP_LDAP
-  { "ldap_dn",             vtype_stringptr,   &eldap_dn },
+  { "ldap_dn",             vtype_lookup_module, US"ldap" },
 #endif
   { "load_average",        vtype_load_avg,    NULL },
   { "local_part",          vtype_stringptr,   &deliver_localpart },
@@ -713,13 +713,13 @@ static var_entry var_table[] = {
   { "spam_score_int",      vtype_stringptr,   &spam_score_int },
 #endif
 #ifdef EXIM_HAVE_SPF
-  { "spf_guess",           vtype_module,	US"spf" },
-  { "spf_header_comment",  vtype_module,	US"spf" },
-  { "spf_received",        vtype_module,	US"spf" },
-  { "spf_result",          vtype_module,	US"spf" },
-  { "spf_result_guessed",  vtype_module,	US"spf" },
-  { "spf_smtp_comment",    vtype_module,	US"spf" },
-  { "spf_used_domain",	   vtype_module,	US"spf" },
+  { "spf_guess",           vtype_misc_module,	US"spf" },
+  { "spf_header_comment",  vtype_misc_module,	US"spf" },
+  { "spf_received",        vtype_misc_module,	US"spf" },
+  { "spf_result",          vtype_misc_module,	US"spf" },
+  { "spf_result_guessed",  vtype_misc_module,	US"spf" },
+  { "spf_smtp_comment",    vtype_misc_module,	US"spf" },
+  { "spf_used_domain",	   vtype_misc_module,	US"spf" },
 #endif
   { "spool_directory",     vtype_stringptr,   &spool_directory },
   { "spool_inodes",        vtype_pinodes,     (void *)TRUE },
@@ -2161,7 +2161,7 @@ switch (vp->type)
     }
 #endif
 
-  case vtype_module:
+  case vtype_misc_module:
     {
     uschar * errstr;
     misc_module_info * mi = misc_mod_find(val, &errstr);
diff --git a/src/src/functions.h b/src/src/functions.h
index b2c2626ab..57ae06c58 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -307,6 +307,8 @@ extern void    log_write_die(unsigned, int, const char * format, ...)
 
 extern const lookup_info * lookup_with_acq_num(unsigned);
 extern gstring *lookup_dynamic_supported(gstring *);
+extern const lookup_info * lookup_find(const uschar *, uschar **);
+extern const lookup_info * lookup_findonly(const uschar *);
 #ifdef LOOKUP_MODULE_DIR
 extern BOOL    lookup_one_mod_load(const uschar *, uschar **);
 #endif
diff --git a/src/src/globals.c b/src/src/globals.c
index e5de80ea4..4fe89f727 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -58,39 +58,6 @@ BOOL    opt_perl_taintmode     = FALSE;
 tree_node *dlobj_anchor        = NULL;
 #endif
 
-#ifdef LOOKUP_LDAP
-uschar *eldap_ca_cert_dir      = NULL;
-uschar *eldap_ca_cert_file     = NULL;
-uschar *eldap_cert_file        = NULL;
-uschar *eldap_cert_key         = NULL;
-uschar *eldap_cipher_suite     = NULL;
-uschar *eldap_default_servers  = NULL;
-uschar *eldap_require_cert     = NULL;
-int     eldap_version          = -1;
-BOOL    eldap_start_tls        = FALSE;
-#endif
-
-#ifdef LOOKUP_MYSQL
-uschar *mysql_servers          = NULL;
-#endif
-
-#ifdef LOOKUP_ORACLE
-uschar *oracle_servers         = NULL;
-#endif
-
-#ifdef LOOKUP_PGSQL
-uschar *pgsql_servers          = NULL;
-#endif
-
-#ifdef LOOKUP_REDIS
-uschar *redis_servers          = NULL;
-#endif
-
-#ifdef LOOKUP_SQLITE
-uschar *sqlite_dbfile	       = NULL;
-int     sqlite_lock_timeout    = 5;
-#endif
-
 #ifdef SUPPORT_MOVE_FROZEN_MESSAGES
 BOOL    move_frozen_messages   = FALSE;
 #endif
@@ -908,7 +875,6 @@ 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"
diff --git a/src/src/globals.h b/src/src/globals.h
index 313062890..f8cd0548b 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -40,39 +40,6 @@ extern BOOL    opt_perl_taintmode;     /* Enable taint mode in Perl */
 extern tree_node *dlobj_anchor;        /* Tree of dynamically-loaded objects */
 #endif
 
-#ifdef LOOKUP_LDAP
-extern uschar *eldap_ca_cert_dir;      /* Directory with CA certificates */
-extern uschar *eldap_ca_cert_file;     /* CA certificate file */
-extern uschar *eldap_cert_file;        /* Certificate file */
-extern uschar *eldap_cert_key;         /* Certificate key file */
-extern uschar *eldap_cipher_suite;     /* Allowed cipher suite */
-extern uschar *eldap_default_servers;  /* List of default servers */
-extern uschar *eldap_require_cert;     /* Peer certificate checking strategy */
-extern BOOL    eldap_start_tls;        /* Use STARTTLS */
-extern int     eldap_version;          /* LDAP version */
-#endif
-
-#ifdef LOOKUP_MYSQL
-extern uschar *mysql_servers;          /* List of servers and connect info */
-#endif
-
-#ifdef LOOKUP_ORACLE
-extern uschar *oracle_servers;         /* List of servers and connect info */
-#endif
-
-#ifdef LOOKUP_PGSQL
-extern uschar *pgsql_servers;          /* List of servers and connect info */
-#endif
-
-#ifdef LOOKUP_REDIS
-extern uschar *redis_servers;          /* List of servers and connect info */
-#endif
-
-#ifdef LOOKUP_SQLITE
-extern uschar *sqlite_dbfile;	       /* Filname for database */
-extern int     sqlite_lock_timeout;    /* Internal lock waiting timeout */
-#endif
-
 #ifdef SUPPORT_MOVE_FROZEN_MESSAGES
 extern BOOL    move_frozen_messages;   /* Get them out of the normal directory */
 #endif
@@ -677,7 +644,6 @@ extern int     journal_fd;             /* Fd for journal file */
 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 */
 #ifndef DISABLE_ESMTP_LIMITS
 extern uschar *limits_advertise_hosts; /* for banner/EHLO pipelining */
diff --git a/src/src/lookupapi.h b/src/src/lookupapi.h
index b8ec17880..84f150e03 100644
--- a/src/src/lookupapi.h
+++ b/src/src/lookupapi.h
@@ -50,15 +50,21 @@ typedef struct lookup_info {
     unsigned);			  /* lookup type index */
   gstring * (*version_report)(    /* diagnostic function */
     gstring *);                   /* string to appand to */
+
+  void *	options;
+  unsigned	options_count;
+  void *	variables;
+  unsigned	variables_count;
 } lookup_info;
 
 /* This magic number is used by the following lookup_module_info structure
    for checking API compatibility. It used to be equivalent to the string"LMM3" */
-#define LOOKUP_MODULE_INFO_MAGIC 0x4c4d4935
+#define LOOKUP_MODULE_INFO_MAGIC 0x4c4d4936
 /* Version 2 adds: version_report */
 /* Version 3 change: non/cache becomes TTL in seconds */
 /* Version 4 add: index on quoting function */
 /* Version 5 change: version report now adds to a gstring */
+/* Version 6 add: options */
 
 typedef struct lookup_module_info {
   uint		magic;
diff --git a/src/src/lookups/ldap.c b/src/src/lookups/ldap.c
index 0a00633ab..06b82aa4c 100644
--- a/src/src/lookups/ldap.c
+++ b/src/src/lookups/ldap.c
@@ -74,6 +74,19 @@ LDAP_LIB_OPENLDAP1
 SEARCH_LDAP_DN lookup). */
 
 
+static uschar * eldap_ca_cert_dir      = NULL; /* Directory with CA certificates */
+static uschar * eldap_ca_cert_file     = NULL; /* CA certificate file */
+static uschar * eldap_cert_file        = NULL; /* Certificate file */
+static uschar * eldap_cert_key         = NULL; /* Certificate key file */
+static uschar * eldap_cipher_suite     = NULL; /* Allowed cipher suite */
+static uschar * eldap_default_servers  = NULL; /* List of default servers */
+static uschar * eldap_require_cert     = NULL; /* Peer certificate checking strategy */
+static int      eldap_version          = -1;	/* Use STARTTLS */
+static BOOL     eldap_start_tls        = FALSE; /* LDAP version */
+
+static uschar * eldap_dn;			/* Where LDAP DNs are left */
+
+
 /* Structure and anchor for caching connections. */
 
 typedef struct ldap_connection {
@@ -87,7 +100,7 @@ typedef struct ldap_connection {
   LDAP *ld;
 } LDAP_CONNECTION;
 
-static LDAP_CONNECTION *ldap_connections = NULL;
+static LDAP_CONNECTION * ldap_connections = NULL;
 
 
 
@@ -1587,16 +1600,42 @@ return string_cat(g, "Library version: LDAP: (unknown)\n");
 }
 
 
+/******************************************************************************/
+/* Module API */
+
+static optionlist eldap_options[] = {
+  { "ldap_ca_cert_dir",         opt_stringptr,   {&eldap_ca_cert_dir} },
+  { "ldap_ca_cert_file",        opt_stringptr,   {&eldap_ca_cert_file} },
+  { "ldap_cert_file",           opt_stringptr,   {&eldap_cert_file} },
+  { "ldap_cert_key",            opt_stringptr,   {&eldap_cert_key} },
+  { "ldap_cipher_suite",        opt_stringptr,   {&eldap_cipher_suite} },
+  { "ldap_default_servers",     opt_stringptr,   {&eldap_default_servers} },
+  { "ldap_require_cert",        opt_stringptr,   {&eldap_require_cert} },
+  { "ldap_start_tls",           opt_bool,        {&eldap_start_tls} },
+  { "ldap_version",             opt_int,         {&eldap_version} },
+};
+
+static var_entry eldap_variables[] = {
+  { "ldap_dn",             vtype_stringptr,   &eldap_dn },
+};
+
 static lookup_info ldap_lookup_info = {
-  .name = US"ldap",			/* lookup name */
-  .type = lookup_querystyle,		/* query-style lookup */
-  .open = eldap_open,			/* open function */
-  .check = NULL,			/* check function */
-  .find = eldap_find,			/* find function */
-  .close = NULL,			/* no close function */
-  .tidy = eldap_tidy,			/* tidy function */
-  .quote = eldap_quote,			/* quoting function */
-  .version_report = ldap_version_report	/* version reporting */
+  .name =	US"ldap",			/* lookup name */
+  .type =	lookup_querystyle,		/* query-style lookup */
+  .open =	eldap_open,			/* open function */
+  .check =	NULL,				/* check function */
+  .find =	eldap_find,			/* find function */
+  .close =	NULL,				/* no close function */
+  .tidy =	eldap_tidy,			/* tidy function */
+  .quote =	eldap_quote,			/* quoting function */
+  .version_report = ldap_version_report,	/* version reporting */
+
+  /* This lookup name matches the modules name, so access options here */
+  .options =	eldap_options,
+  .options_count = nelem(eldap_options),
+
+  .variables =	eldap_variables,
+  .variables_count = nelem(eldap_variables),
 };
 
 static lookup_info ldapdn_lookup_info = {
diff --git a/src/src/lookups/mysql.c b/src/src/lookups/mysql.c
index f28e9154f..dca125188 100644
--- a/src/src/lookups/mysql.c
+++ b/src/src/lookups/mysql.c
@@ -64,6 +64,8 @@ with versions before 10.2, as they do not define there there specific symbols.
 #endif
 
 
+static uschar * mysql_servers = NULL;	/* List of servers and connect info */
+
 /* Structure and anchor for caching connections. */
 
 typedef struct mysql_connection {
@@ -478,18 +480,28 @@ g = string_fmt_append(g,
 return g;
 }
 
+/******************************************************************************/
+/* Module API */
+
+static optionlist mysql_glbl_options[] = {
+  { "mysql_servers",	opt_stringptr,	{&mysql_servers} }
+};
+
 /* These are the lookup_info blocks for this driver */
 
 static lookup_info mysql_lookup_info = {
-  .name = US"mysql",			/* lookup name */
-  .type = lookup_querystyle,		/* query-style lookup */
-  .open = mysql_open,			/* open function */
-  .check = NULL,			/* no check function */
-  .find = mysql_find,			/* find function */
-  .close = NULL,			/* no close function */
-  .tidy = mysql_tidy,			/* tidy function */
-  .quote = mysql_quote,			/* quoting function */
-  .version_report = mysql_version_report           /* version reporting */
+  .name =	US"mysql",			/* lookup name */
+  .type =	lookup_querystyle,		/* query-style lookup */
+  .open =	mysql_open,			/* open function */
+  .check =	NULL,				/* no check function */
+  .find =	mysql_find,			/* find function */
+  .close =	NULL,				/* no close function */
+  .tidy =	mysql_tidy,			/* tidy function */
+  .quote =	mysql_quote,			/* quoting function */
+  .version_report = mysql_version_report,	/* version reporting */
+
+  .options =	mysql_glbl_options,
+  .options_count = nelem(mysql_glbl_options),
 };
 
 #ifdef DYNLOOKUP
diff --git a/src/src/lookups/oracle.c b/src/src/lookups/oracle.c
index e4eedbe28..9472fc2ff 100644
--- a/src/src/lookups/oracle.c
+++ b/src/src/lookups/oracle.c
@@ -69,6 +69,11 @@ typedef struct Ora_Define {
   ub2   col_retlen, col_retcode;
 } Ora_Define;
 
+
+
+static uschar * oracle_servers = NULL;	/* List of servers and connect info */
+
+
 /* Structure and anchor for caching connections. */
 
 typedef struct oracle_connection {
@@ -604,16 +609,26 @@ return g;
 }
 
 
+/******************************************************************************/
+/* Module API */
+
+static optionlist oracle_options[] = {
+  { "oracle_servers",	opt_stringptr,	{&oracle_servers} }
+};
+
 static lookup_info _lookup_info = {
-  .name = US"oracle",			/* lookup name */
-  .type = lookup_querystyle,		/* query-style lookup */
-  .open = oracle_open,			/* open function */
-  .check = NULL,			/* check function */
-  .find = oracle_find,			/* find function */
-  .close = NULL,			/* no close function */
-  .tidy = oracle_tidy,			/* tidy function */
-  .quote = oracle_quote,		/* quoting function */
-  .version_report = oracle_version_report          /* version reporting */
+  .name =	US"oracle",			/* lookup name */
+  .type =	lookup_querystyle,		/* query-style lookup */
+  .open =	oracle_open,			/* open function */
+  .check =	NULL,				/* check function */
+  .find =	oracle_find,			/* find function */
+  .close =	NULL,				/* no close function */
+  .tidy =	oracle_tidy,			/* tidy function */
+  .quote =	oracle_quote,			/* quoting function */
+  .version_report = oracle_version_report,	/* version reporting */
+
+  .options =	oracle_options,
+  .options_count = nelem(oracle_options),
 };
 
 #ifdef DYNLOOKUP
diff --git a/src/src/lookups/pgsql.c b/src/src/lookups/pgsql.c
index 7071cb4ab..bef6c59e2 100644
--- a/src/src/lookups/pgsql.c
+++ b/src/src/lookups/pgsql.c
@@ -16,6 +16,9 @@ socket extension. */
 
 #include <libpq-fe.h>       /* The system header */
 
+
+static uschar * pgsql_servers = NULL;	/* List of servers and connect info */
+
 /* Structure and anchor for caching connections. */
 
 typedef struct pgsql_connection {
@@ -490,16 +493,26 @@ return g;
 }
 
 
+/******************************************************************************/
+/* Module API */
+
+static optionlist pgsql_options[] = {
+  { "pgsql_servers",	opt_stringptr,	{&pgsql_servers} }
+};
+
 static lookup_info _lookup_info = {
-  .name = US"pgsql",			/* lookup name */
-  .type = lookup_querystyle,		/* query-style lookup */
-  .open = pgsql_open,			/* open function */
-  .check = NULL,			/* no check function */
-  .find = pgsql_find,			/* find function */
-  .close = NULL,			/* no close function */
-  .tidy = pgsql_tidy,			/* tidy function */
-  .quote = pgsql_quote,			/* quoting function */
-  .version_report = pgsql_version_report           /* version reporting */
+  .name =	US"pgsql",			/* lookup name */
+  .type =	lookup_querystyle,		/* query-style lookup */
+  .open =	pgsql_open,			/* open function */
+  .check =	NULL,				/* no check function */
+  .find =	pgsql_find,			/* find function */
+  .close =	NULL,				/* no close function */
+  .tidy =	pgsql_tidy,			/* tidy function */
+  .quote =	pgsql_quote,			/* quoting function */
+  .version_report = pgsql_version_report,	/* version reporting */
+
+  .options =	pgsql_options,
+  .options_count = nelem(pgsql_options),
 };
 
 #ifdef DYNLOOKUP
diff --git a/src/src/lookups/redis.c b/src/src/lookups/redis.c
index 61c264a73..e88ded154 100644
--- a/src/src/lookups/redis.c
+++ b/src/src/lookups/redis.c
@@ -15,9 +15,7 @@
 
 #include <hiredis/hiredis.h>
 
-#ifndef nele
-# define nele(arr) (sizeof(arr) / sizeof(*arr))
-#endif
+static uschar * redis_servers = NULL;	/* List of servers and connect info */
 
 /* Structure and anchor for caching connections. */
 typedef struct redis_connection {
@@ -26,7 +24,7 @@ typedef struct redis_connection {
   redisContext    *handle;
 } redis_connection;
 
-static redis_connection *redis_connections = NULL;
+static redis_connection * redis_connections = NULL;
 
 
 static void *
@@ -210,7 +208,7 @@ if(sdata[1])
 
   Uskip_whitespace(&s);
 
-  for (i = 0; *s && i < nele(argv); i++)
+  for (i = 0; *s && i < nelem(argv); i++)
     {
     gstring * g;
 
@@ -438,17 +436,27 @@ return string_fmt_append(g, "Library version: REDIS: Compile: %d.%d.%d\n",
 
 
 
+/******************************************************************************/
+/* Module API */
+
+static optionlist redis_options[] = {
+  { "redis_servers",	opt_stringptr,	{&redis_servers} }
+};
+
 /* These are the lookup_info blocks for this driver */
 static lookup_info redis_lookup_info = {
-  .name = US"redis",			/* lookup name */
-  .type = lookup_querystyle,		/* query-style lookup */
-  .open = redis_open,			/* open function */
-  .check = NULL,			/* no check function */
-  .find = redis_find,			/* find function */
-  .close = NULL,			/* no close function */
-  .tidy = redis_tidy,			/* tidy function */
-  .quote = redis_quote,			/* quoting function */
-  .version_report = redis_version_report           /* version reporting */
+  .name =	US"redis",			/* lookup name */
+  .type =	lookup_querystyle,		/* query-style lookup */
+  .open =	redis_open,			/* open function */
+  .check =	NULL,				/* no check function */
+  .find =	redis_find,			/* find function */
+  .close =	NULL,				/* no close function */
+  .tidy =	redis_tidy,			/* tidy function */
+  .quote =	redis_quote,			/* quoting function */
+  .version_report = redis_version_report,	/*  version reporting */
+
+  .options =	redis_options,
+  .options_count = nelem(redis_options),
 };
 
 #ifdef DYNLOOKUP
diff --git a/src/src/lookups/sqlite.c b/src/src/lookups/sqlite.c
index eb23b8631..c91440e1e 100644
--- a/src/src/lookups/sqlite.c
+++ b/src/src/lookups/sqlite.c
@@ -12,6 +12,9 @@
 
 #include <sqlite3.h>
 
+static uschar * sqlite_dbfile = NULL;	/* Filename for database */
+static int	sqlite_lock_timeout = 5;/* Internal lock waiting timeout */
+
 
 /*************************************************
 *              Open entry point                  *
@@ -173,20 +176,31 @@ g = string_fmt_append(g,
 return g;
 }
 
+/******************************************************************************/
+/* Module API */
+
+static optionlist sqlite_options[] = {
+  { "sqlite_dbfile",            opt_stringptr,   {&sqlite_dbfile} },
+  { "sqlite_lock_timeout",      opt_int,         {&sqlite_lock_timeout} },
+};
+
 static lookup_info _lookup_info = {
-  .name = US"sqlite",			/* lookup name */
-  .type = lookup_absfilequery,		/* query-style lookup, starts with file name */
-  .open = sqlite_open,			/* open function */
-  .check = NULL,			/* no check function */
-  .find = sqlite_find,			/* find function */
-  .close = sqlite_close,		/* close function */
-  .tidy = NULL,				/* no tidy function */
-  .quote = sqlite_quote,		/* quoting function */
-  .version_report = sqlite_version_report          /* version reporting */
+  .name =	US"sqlite",			/* lookup name */
+  .type =	lookup_absfilequery,		/* query-style lookup, starts with file name */
+  .open =	sqlite_open,			/* open function */
+  .check =	NULL,				/* no check function */
+  .find =	sqlite_find,			/* find function */
+  .close =	sqlite_close,			/* close function */
+  .tidy =	NULL,				/* no tidy function */
+  .quote =	sqlite_quote,			/* quoting function */
+  .version_report = sqlite_version_report,	/* version reporting */
+
+  .options =	sqlite_options,
+  .options_count = nelem(sqlite_options),
 };
 
 #ifdef DYNLOOKUP
-#define sqlite_lookup_module_info _lookup_module_info
+# define sqlite_lookup_module_info _lookup_module_info
 #endif
 
 static lookup_info *_lookup_list[] = { &_lookup_info };
diff --git a/src/src/macros.h b/src/src/macros.h
index 0070ef7ed..f6b240322 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -707,7 +707,8 @@ can be easily tested as a group. That is the only use of opt_bool_last. */
 enum { opt_bit = 32, opt_bool_verify, opt_bool_set, opt_expand_bool,
   opt_bool_last,
   opt_rewrite, opt_timelist, opt_uid, opt_gid, opt_uidlist, opt_gidlist,
-  opt_expand_uid, opt_expand_gid, opt_func, opt_void, opt_module };
+  opt_expand_uid, opt_expand_gid, opt_func, opt_void,
+  opt_lookup_module, opt_misc_module };
 
 /* There's a high-ish bit which is used to flag duplicate options, kept
 for compatibility, which shouldn't be output. Also used for hidden options
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 417a786e3..46adac621 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -47,7 +47,7 @@ static optionlist optionlist_config[] = {
   { "acl_smtp_data_prdr",       opt_stringptr,   {&acl_smtp_data_prdr} },
 #endif
 #ifndef DISABLE_DKIM
-  { "acl_smtp_dkim",            opt_module,	 {US"dkim"} },
+  { "acl_smtp_dkim",            opt_misc_module, {US"dkim"} },
 #endif
   { "acl_smtp_etrn",            opt_stringptr,   {&acl_smtp_etrn} },
   { "acl_smtp_expn",            opt_stringptr,   {&acl_smtp_expn} },
@@ -118,16 +118,16 @@ static optionlist optionlist_config[] = {
 #endif
   { "disable_ipv6",             opt_bool,        {&disable_ipv6} },
 #ifndef DISABLE_DKIM
-  { "dkim_verify_hashes",       opt_module,	 {US"dkim"} },
-  { "dkim_verify_keytypes",     opt_module,	 {US"dkim"} },
-  { "dkim_verify_min_keysizes", opt_module,	 {US"dkim"} },
-  { "dkim_verify_minimal",      opt_module,	 {US"dkim"} },
-  { "dkim_verify_signers",      opt_module,	 {US"dkim"} },
+  { "dkim_verify_hashes",       opt_misc_module, {US"dkim"} },
+  { "dkim_verify_keytypes",     opt_misc_module, {US"dkim"} },
+  { "dkim_verify_min_keysizes", opt_misc_module, {US"dkim"} },
+  { "dkim_verify_minimal",      opt_misc_module, {US"dkim"} },
+  { "dkim_verify_signers",      opt_misc_module, {US"dkim"} },
 #endif
 #ifdef EXIM_HAVE_DMARC
-  { "dmarc_forensic_sender",    opt_module,	 {US"dmarc"} },
-  { "dmarc_history_file",       opt_module,	 {US"dmarc"} },
-  { "dmarc_tld_file",           opt_module,	 {US"dmarc"} },
+  { "dmarc_forensic_sender",    opt_misc_module, {US"dmarc"} },
+  { "dmarc_history_file",       opt_misc_module, {US"dmarc"} },
+  { "dmarc_tld_file",           opt_misc_module, {US"dmarc"} },
 #endif
   { "dns_again_means_nonexist", opt_stringptr,   {&dns_again_means_nonexist} },
   { "dns_check_names_pattern",  opt_stringptr,   {&check_dns_names_pattern} },
@@ -192,15 +192,15 @@ static optionlist optionlist_config[] = {
   { "keep_environment",         opt_stringptr,   {&keep_environment} },
   { "keep_malformed",           opt_time,        {&keep_malformed} },
 #ifdef LOOKUP_LDAP
-  { "ldap_ca_cert_dir",         opt_stringptr,   {&eldap_ca_cert_dir} },
-  { "ldap_ca_cert_file",        opt_stringptr,   {&eldap_ca_cert_file} },
-  { "ldap_cert_file",           opt_stringptr,   {&eldap_cert_file} },
-  { "ldap_cert_key",            opt_stringptr,   {&eldap_cert_key} },
-  { "ldap_cipher_suite",        opt_stringptr,   {&eldap_cipher_suite} },
-  { "ldap_default_servers",     opt_stringptr,   {&eldap_default_servers} },
-  { "ldap_require_cert",        opt_stringptr,   {&eldap_require_cert} },
-  { "ldap_start_tls",           opt_bool,        {&eldap_start_tls} },
-  { "ldap_version",             opt_int,         {&eldap_version} },
+  { "ldap_ca_cert_dir",         opt_lookup_module, {US"ldap"} },
+  { "ldap_ca_cert_file",        opt_lookup_module, {US"ldap"} },
+  { "ldap_cert_file",           opt_lookup_module, {US"ldap"} },
+  { "ldap_cert_key",            opt_lookup_module, {US"ldap"} },
+  { "ldap_cipher_suite",        opt_lookup_module, {US"ldap"} },
+  { "ldap_default_servers",     opt_lookup_module, {US"ldap"} },
+  { "ldap_require_cert",        opt_lookup_module, {US"ldap"} },
+  { "ldap_start_tls",           opt_lookup_module, {US"ldap"} },
+  { "ldap_version",             opt_lookup_module, {US"ldap"} },
 #endif
 #ifndef DISABLE_ESMTP_LIMITS
   { "limits_advertise_hosts", opt_stringptr, {&limits_advertise_hosts} },
@@ -231,7 +231,7 @@ static optionlist optionlist_config[] = {
 #endif
   { "mua_wrapper",              opt_bool,        {&mua_wrapper} },
 #ifdef LOOKUP_MYSQL
-  { "mysql_servers",            opt_stringptr,   {&mysql_servers} },
+  { "mysql_servers",            opt_lookup_module, {US"mysql"} },
 #endif
   { "never_users",              opt_uidlist,     {&never_users} },
   { "notifier_socket",          opt_stringptr,   {&notifier_socket} },
@@ -239,7 +239,7 @@ static optionlist optionlist_config[] = {
   { "openssl_options",          opt_stringptr,   {&openssl_options} },
 #endif
 #ifdef LOOKUP_ORACLE
-  { "oracle_servers",           opt_stringptr,   {&oracle_servers} },
+  { "oracle_servers",           opt_lookup_module, {US"oracle"} },
 #endif
   { "panic_coredump",           opt_bool,        {&panic_coredump} },
   { "percent_hack_domains",     opt_stringptr,   {&percent_hack_domains} },
@@ -249,7 +249,7 @@ static optionlist optionlist_config[] = {
   { "perl_taintmode",           opt_bool,        {&opt_perl_taintmode} },
 #endif
 #ifdef LOOKUP_PGSQL
-  { "pgsql_servers",            opt_stringptr,   {&pgsql_servers} },
+  { "pgsql_servers",            opt_lookup_module, {US"pgsql"} },
 #endif
   { "pid_file_path",            opt_stringptr,   {&pid_file_path} },
   { "pipelining_advertise_hosts", opt_stringptr, {&pipelining_advertise_hosts} },
@@ -290,7 +290,7 @@ static optionlist optionlist_config[] = {
   { "recipients_max",           opt_stringptr,   {&recipients_max} },
   { "recipients_max_reject",    opt_bool,        {&recipients_max_reject} },
 #ifdef LOOKUP_REDIS
-  { "redis_servers",            opt_stringptr,   {&redis_servers} },
+  { "redis_servers",            opt_lookup_module, {US"redis"} },
 #endif
   { "remote_max_parallel",      opt_int,         {&remote_max_parallel} },
   { "remote_sort_domains",      opt_stringptr,   {&remote_sort_domains} },
@@ -336,15 +336,15 @@ static optionlist optionlist_config[] = {
   { "spamd_address",            opt_stringptr,   {&spamd_address} },
 #endif
 #ifdef EXIM_HAVE_SPF
-  { "spf_guess",                opt_module,	 {US"spf"} },
-  { "spf_smtp_comment_template",opt_module,	 {US"spf"} },
+  { "spf_guess",                opt_misc_module, {US"spf"} },
+  { "spf_smtp_comment_template",opt_misc_module, {US"spf"} },
 #endif
   { "split_spool_directory",    opt_bool,        {&split_spool_directory} },
   { "spool_directory",          opt_stringptr,   {&spool_directory} },
   { "spool_wireformat",         opt_bool,        {&spool_wireformat} },
 #ifdef LOOKUP_SQLITE
-  { "sqlite_dbfile",            opt_stringptr,   {&sqlite_dbfile} },
-  { "sqlite_lock_timeout",      opt_int,         {&sqlite_lock_timeout} },
+  { "sqlite_dbfile",            opt_lookup_module, {US"sqlite"} },
+  { "sqlite_lock_timeout",      opt_lookup_module, {US"sqlite"} },
 #endif
   { "strict_acl_vars",          opt_bool,        {&strict_acl_vars} },
   { "strip_excess_angle_brackets", opt_bool,     {&strip_excess_angle_brackets} },
@@ -1762,13 +1762,10 @@ Returns:        TRUE if an option was read successfully,
 */
 
 static BOOL
-readconf_handle_option(uschar *buffer, optionlist *oltop, int last,
-  void *data_block, uschar *unknown_txt)
+readconf_handle_option(uschar * buffer, optionlist * oltop, int last,
+  void * data_block, uschar * unknown_txt)
 {
-int ptr;
-int offset = 0;
-int count, type, value;
-int issecure = 0;
+int ptr, offset = 0, count, type, value, issecure = 0;
 uid_t uid;
 gid_t gid;
 BOOL boolvalue = TRUE;
@@ -2507,10 +2504,23 @@ switch (type)
     ol->v.fn(name, s, 0);
     break;
 
-  case opt_module:
+  case opt_lookup_module:
     {
     uschar * errstr;
-    misc_module_info * mi = misc_mod_find(US ol->v.value, &errstr);
+    const lookup_info * li = lookup_find(US ol->v.value, &errstr);
+    if (!li)
+      log_write_die(0, LOG_CONFIG_IN,
+	"failed to find %s module for %s: %s", US ol->v.value, name, errstr);
+
+    oltop = li->options;
+    last = li->options_count;
+    goto sublist;
+    }
+
+  case opt_misc_module:
+    {
+    uschar * errstr;
+    const misc_module_info * mi = misc_mod_find(US ol->v.value, &errstr);
     if (!mi)
       log_write_die(0, LOG_CONFIG_IN,
 	"failed to find %s module for %s: %s", US ol->v.value, name, errstr);
diff --git a/src/src/search.c b/src/src/search.c
index e09f3ab4e..e81806379 100644
--- a/src/src/search.c
+++ b/src/src/search.c
@@ -54,8 +54,8 @@ static rmark search_reset_point = NULL;
 *      Validate a plain lookup type name         *
 *************************************************/
 
-static const lookup_info *
-internal_search_findtype(const uschar * name)
+const lookup_info *
+lookup_findonly(const uschar * name)
 {
 tree_node * t = tree_search(lookups_tree, name);
 return t ? t->data.ptr : NULL;
@@ -81,7 +81,7 @@ const lookup_info * li;
 
 if (name[len])
   name = string_copyn(name, len);
-if ((li = internal_search_findtype(name)))
+if ((li = lookup_findonly(name)))
   return li;
 
 #ifdef LOOKUP_MODULE_DIR
@@ -89,7 +89,7 @@ if ((li = internal_search_findtype(name)))
       debug_printf_indent("searchtype %s not initially found\n", name);
 
     if (lookup_one_mod_load(name, NULL))
-      if ((li = internal_search_findtype(name)))
+      if ((li = lookup_findonly(name)))
 	return li;
       else
 	{ DEBUG(D_lookup) debug_printf_indent("find retry failed\n"); }
diff --git a/src/src/structs.h b/src/src/structs.h
index 22a4bba90..52b540356 100644
--- a/src/src/structs.h
+++ b/src/src/structs.h
@@ -1012,7 +1012,8 @@ enum vtypes {
 #ifndef DISABLE_DKIM
   vtype_dkim,           /* Lookup of value in DKIM signature */
 #endif
-  vtype_module,		/* variable lives in a module; value is module name */
+  vtype_lookup_module,	/* variable lives in a module; value is module name */
+  vtype_misc_module,	/* variable lives in a module; value is module name */
 };
 
 /* Type for main variable table */

commit d6c8c56fd08000ea9da90d4cf506870a6a7e205b
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Dec 24 14:18:00 2025 +0000

    Docs: build note

diff --git a/src/src/EDITME b/src/src/EDITME
index f641a6699..ec187e919 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -751,6 +751,8 @@ DISABLE_MAL_MKS=yes
 # sqlite
 # USE_SQLITE = yes
 # DBMLIB = -lsqlite3
+#  If using Gnu Make this is usable:
+# DBMLIB = $(shell pkg-config --libs sqlite3)
 
 
 #------------------------------------------------------------------------------

commit 928e43f00d3b88e2684fdd530545932895ef684b
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Dec 24 14:51:36 2025 +0000

    Fix non-LOOKUP_MODULE_DIR build
    
    Broken-by: b3881e820e87

diff --git a/src/src/drtables.c b/src/src/drtables.c
index 20034e7ce..21c9feb60 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -214,9 +214,11 @@ return TRUE;
 const lookup_info *
 lookup_find(const uschar * name, uschar ** errstr)
 {
+#ifdef LOOKUP_MODULE_DIR
 const lookup_info * li;
 if ((li = lookup_findonly(name))) return li;
 lookup_mod_load(name, errstr);
+#endif
 return lookup_findonly(name);
 }
 

commit a87bd5b303e01724fa4cec576a5d824186a8074c
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Dec 25 09:16:09 2025 +0000

    daemon_modules_preload option

diff --git a/src/src/daemon.c b/src/src/daemon.c
index 056bf6311..e8432196c 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -1667,6 +1667,21 @@ return string_from_gstring(g);
 }
 
 
+
+static void
+daemon_preload_modules(void)
+{
+#ifdef LOOKUP_MODULE_DIR
+uschar namebuf[32];
+const uschar * list = daemon_modules_load, * ele;
+int sep = 0;
+
+while (ele = string_nextinlist(&list, &sep, namebuf, sizeof(namebuf)))
+  mod_load_anyclass(ele);
+#endif
+}
+
+
 /*************************************************
 *              Exim Daemon Mainline              *
 *************************************************/
@@ -1733,6 +1748,10 @@ debugging lines get the pid added. */
 
 DEBUG(D_any|D_v) debug_selector |= D_pid;
 
+/* Get any requested dynamic-load modules loaded */
+
+daemon_preload_modules();
+
 /* Allocate enough pollstructs for inetd mode plus the ancillary sockets;
 also used when there are no listen sockets. */
 
diff --git a/src/src/drtables.c b/src/src/drtables.c
index 21c9feb60..ec2c4c33a 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -209,6 +209,35 @@ if (!lookup_mod_load(name, errstr)) return FALSE;
 return TRUE;
 }
 
+
+/* Try to load a module of any class, given just the name.
+This is used for the daemon_modules_load option.
+Do not bother with class auth,router,transport - readconf will load if used.
+*/
+
+void
+mod_load_anyclass(const uschar * name)
+{
+DIR * dd;
+const pcre2_code * regex_class;
+
+/* Find the class for this name, by checking the lookup modules dir.
+We assume the .so files there are properly worded, and that the names
+are distinct. */
+
+if (!(dd = open_module_dir())) return;
+
+regex_class = regex_must_compile(
+  string_sprintf("^%s_(lookup|miscmod)\\." DYNLIB_FN_EXT "$", name),
+  MCS_NOFLAGS, TRUE);
+
+for (struct dirent * ent; ent = readdir(dd); )
+  if (regex_match_and_setup(regex_class, US ent->d_name, 0, 0))
+    if (Ustrcmp(expand_nstring[1], "miscmod") == 0)
+      (void) misc_mod_find(name, NULL);
+    else	/* assume "lookup" */
+      if (!tree_search(lookups_tree, name)) lookup_one_mod_load(name, NULL);
+}
 #endif	/*LOOKUP_MODULE_DIR*/
 
 const lookup_info *
@@ -303,7 +332,7 @@ const char * errormsg;
 EARLY_DEBUG(D_any, "Loading module %q\n", name);
 if (!(dl = mod_open(name, US"miscmod", errstr)))
   {
-  if (errstr) EARLY_DEBUG(D_any, " mod_open: %s\n", *errstr);
+  if (errstr && *errstr) EARLY_DEBUG(D_any, " mod_open: %s\n", *errstr);
   return NULL;
   }
 
diff --git a/src/src/exim.c b/src/src/exim.c
index 8c5089f7b..7e90e927c 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1756,10 +1756,6 @@ len = Ustrlen(big_buffer);
 
 (void) macros_expand(0, &len, &macro_found);
 
-#ifdef LOOKUP_MODULE_DIR
-//mod_load_check(big_buffer);
-#endif
-
 if (isupper(big_buffer[0]) && !(macro_found & MACRO_FIRST))
   {
   if (macro_read_assignment(big_buffer))
diff --git a/src/src/functions.h b/src/src/functions.h
index 57ae06c58..fd6cab0d5 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -376,7 +376,7 @@ extern BOOL    moan_to_sender(int, const error_block *, const header_line *,
 extern void    moan_write_from(FILE *);
 extern void    moan_write_references(FILE *, uschar *);
 #ifdef LOOKUP_MODULE_DIR
-//extern void    mod_load_check(const uschar *);
+extern void    mod_load_anyclass(const uschar *);
 #endif
 extern void    mod_names(FILE *);
 extern FILE   *modefopen(const uschar *, const char *, mode_t);
diff --git a/src/src/globals.c b/src/src/globals.c
index 4fe89f727..2b2270899 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -639,6 +639,7 @@ cut_t   cutthrough = {	/* All remaining items 0/FALSE/NULL */
   .cctx =		{.sock = -1},			/* open connection */
 };
 
+uschar *daemon_modules_load    = NULL;
 int	daemon_notifier_fd     = -1;
 uschar *daemon_smtp_port       = US"smtp";
 int     daemon_startup_retries = 9;
diff --git a/src/src/globals.h b/src/src/globals.h
index f8cd0548b..befaf308b 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -461,6 +461,7 @@ typedef struct {
 } cut_t;
 extern cut_t cutthrough;               /* Deliver-concurrently */
 
+extern uschar *daemon_modules_load;    /* Dyn-load modules to preload */
 extern int     daemon_notifier_fd;     /* Unix socket for notifications */
 extern uschar *daemon_smtp_port;       /* Can be a list of ports */
 extern int     daemon_startup_retries; /* Number of times to retry */
diff --git a/src/src/macro_predef.c b/src/src/macro_predef.c
index 4778ace64..59894cbb3 100644
--- a/src/src/macro_predef.c
+++ b/src/src/macro_predef.c
@@ -385,6 +385,7 @@ avail_list(drtable, tag, US"static", FALSE);
 avail_list(drtable, tag, US"dynamic", TRUE);
 }
 
+/* Make lists of the drivers (by class, and static/dynamic) we are building */
 static void
 avail(void)
 {
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 46adac621..356321765 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -98,6 +98,7 @@ static optionlist optionlist_config[] = {
   { "check_spool_space",        opt_Kint,        {&check_spool_space} },
   { "chunking_advertise_hosts", opt_stringptr,	 {&chunking_advertise_hosts} },
   { "commandline_checks_require_admin", opt_bool,{&commandline_checks_require_admin} },
+  { "daemon_modules_load",	opt_stringptr,   {&daemon_modules_load} },
   { "daemon_smtp_port",         opt_stringptr|opt_hidden, {&daemon_smtp_port} },
   { "daemon_smtp_ports",        opt_stringptr,   {&daemon_smtp_port} },
   { "daemon_startup_retries",   opt_int,         {&daemon_startup_retries} },
@@ -1290,12 +1291,6 @@ if (strncmpic(s, US"begin ", 6) == 0)
   return NULL;
   }
 
-#ifdef LOOKUP_MODULE_DIR
-/* Check for any required module load operations */
-
-//mod_load_check(s);
-#endif
-
 /* Return the first non-blank character. */
 
 return s;
@@ -4420,9 +4415,9 @@ store_pool = POOL_PERM;
   }
 store_pool = old_pool;
 
-/* Read the config file "authenticators" section, creating an auth instance list.
-For any yet-undiscovered driver, check for a loadable module and add it to
-those available. */
+/* Read the config file "authenticators" section, creating an auth instance
+list.  For any yet-undiscovered driver, check for a loadable module and add it
+to those available. */
 
 readconf_driver_init((driver_instance **)&auths,      /* chain anchor */
   (driver_info **)&auths_available,  /* available drivers */

commit 71f1ce17a7ceda7ec778b84188d74419ab082e1e
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Dec 26 14:10:30 2025 +0000

    constify

diff --git a/src/src/expand.c b/src/src/expand.c
index ab0c0b37a..23ae31baa 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -2227,8 +2227,9 @@ Returns:     -1 OK; string pointer updated, but in "skipping" mode
 */
 
 static int
-read_subs(uschar ** sub, int n, int m, const uschar ** sptr, esi_flags flags,
-  BOOL check_end, uschar * name, BOOL * resetok, unsigned * textonly_p)
+read_subs(const 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;
 unsigned textonly_l = 0;
@@ -2864,7 +2865,7 @@ 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,
+    switch(read_subs(CUSS 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 "
@@ -2913,7 +2914,7 @@ switch(cond_type = identify_operator(&s, &opname))
     goto COND_FAILED_NOT_COMPILED;
 #else
     {
-    uschar *sub[4];
+    const uschar * sub[4];
     Uskip_whitespace(&s);
     if (*s++ != '{') goto COND_FAILED_CURLY_START;	/* }-for-text-editors */
     switch(read_subs(sub, nelem(sub), 2, &s,
@@ -3521,15 +3522,15 @@ switch(cond_type = identify_operator(&s, &opname))
   case ECOND_BOOL:
   case ECOND_BOOL_LAX:
     {
-    uschar *sub_arg[1];
-    uschar *t, *t2;
+    uschar * sub_arg[1];
+    uschar * t, * t2;
     uschar *ourname;
     size_t len;
     BOOL boolvalue = FALSE;
 
     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,
+    switch(read_subs(CUSS sub_arg, 1, 1, &s,
 	    yield ? ESI_NOFLAGS : ESI_SKIPPING, FALSE, ourname, resetok, NULL))
       {
       case 1: expand_string_message = string_sprintf(
@@ -3590,7 +3591,7 @@ switch(cond_type = identify_operator(&s, &opname))
   case ECOND_INBOUND_SRS:
     /* ${if inbound_srs {local_part}{secret}  {yes}{no}} */
     {
-    uschar * sub[2];
+    const uschar * sub[2];
     const pcre2_code * re;
     pcre2_match_data * md;
     PCRE2_SIZE * ovec;
@@ -3622,7 +3623,7 @@ switch(cond_type = identify_operator(&s, &opname))
 
     if (sub[0][0] == '"')
       quoting = 1;
-    else for (uschar * s = sub[0]; *s; s++)
+    else for (const uschar * s = sub[0]; *s; s++)
       if (!isalnum(*s) && Ustrchr(".!#$%&'*+-/=?^_`{|}~", *s) == NULL)
 	{ quoting = 1; break; }
     if (quoting)
@@ -5023,7 +5024,7 @@ while (*s)	/* known to be untainted */
       uschar * user_msg;
       int rc;
 
-      switch(read_subs(sub, nelem(sub), 1, &s, flags, TRUE, name, &resetok, NULL))
+      switch(read_subs(CUSS sub, nelem(sub), 1, &s, flags, TRUE, name, &resetok, NULL))
         {
 	case -1: continue;		/* skipping */
         case 1: goto EXPAND_FAILED_CURLY;
@@ -5056,7 +5057,7 @@ while (*s)	/* known to be untainted */
     case EITEM_AUTHRESULTS:
       /* ${authresults {mysystemname}} */
       {
-      uschar * sub_arg[1];
+      const uschar * sub_arg[1];
 
       switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, flags, TRUE, name, &resetok, NULL))
         {
@@ -5131,8 +5132,7 @@ while (*s)	/* known to be untainted */
 #ifdef SUPPORT_I18N
     case EITEM_IMAPFOLDER:
       {				/* ${imapfolder {name}{sep}{specials}} */
-      uschar * sub_arg[3];
-      const uschar * encoded;
+      const uschar * sub_arg[3], * encoded;
 
       switch(read_subs(sub_arg, nelem(sub_arg), 1, &s, flags, TRUE, name, &resetok, NULL))
         {
@@ -5357,7 +5357,7 @@ while (*s)	/* known to be untainted */
 
 #else   /* EXIM_PERL */
       {
-      uschar * sub_arg[EXIM_PERL_MAX_ARGS + 2];
+      const uschar * sub_arg[EXIM_PERL_MAX_ARGS + 2];
       gstring * new_yield;
       const misc_module_info * mi;
 
@@ -5386,10 +5386,10 @@ while (*s)	/* known to be untainted */
       sub_arg[EXIM_PERL_MAX_ARGS + 1] = NULL;
 	{
 	typedef gstring * (*fn_t)
-			    (gstring *, uschar **, uschar *, const uschar **);
+			(gstring *, uschar **, const uschar *, const uschar **);
 	new_yield = (((fn_t *) mi->functions)[PERL_CAT])
 					      (yield, &expand_string_message,
-						sub_arg[0], CUSS sub_arg + 1);
+						sub_arg[0], sub_arg + 1);
 	}
 
       /* NULL yield indicates failure; if the message pointer has been set to
@@ -5425,7 +5425,7 @@ while (*s)	/* known to be untainted */
       uschar * sub_arg[3], * domain;
       const uschar * p;
 
-      switch(read_subs(sub_arg, 3, 2, &s, flags, TRUE, name, &resetok, NULL))
+      switch(read_subs(CUSS 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;
@@ -5477,7 +5477,7 @@ while (*s)	/* known to be untainted */
 
     case EITEM_PRVSCHECK:
       {
-      uschar * sub_arg[3], * p;
+      const uschar * sub_arg[3], * p;
       gstring * g;
       const pcre2_code * re;
 
@@ -5612,7 +5612,7 @@ while (*s)	/* known to be untainted */
     case EITEM_READFILE:
       {
       FILE * f;
-      uschar * sub_arg[2];
+      const uschar * sub_arg[2];
 
       if (expand_forbid & RDO_READFILE)
         {
@@ -5647,7 +5647,7 @@ while (*s)	/* known to be untainted */
     case EITEM_READSOCK:
       {
       const uschar * arg;
-      uschar * sub_arg[4];
+      const uschar * sub_arg[4];
 
       if (expand_forbid & RDO_READSOCK)
         {
@@ -5947,7 +5947,7 @@ while (*s)	/* known to be untainted */
       {
       int oldptr = gstring_length(yield);
       int o2m;
-      uschar * sub[3];
+      const uschar * sub[3];
 
       switch(read_subs(sub, 3, 3, &s, flags, TRUE, name, &resetok, NULL))
         {
@@ -5989,7 +5989,7 @@ while (*s)	/* known to be untainted */
       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, flags,
+      switch(read_subs(CUSS sub, item_type == EITEM_LENGTH ? 2:3, 2, &s, flags,
              TRUE, name, &resetok, NULL))
         {
 	case -1: continue;	/* skipping */
@@ -6056,14 +6056,15 @@ while (*s)	/* known to be untainted */
 
     case EITEM_HMAC:
       {
-      uschar * sub[3];
+      const uschar * sub[3];
       md5 md5_base;
       hctx sha1_ctx;
       void * use_base;
       int type;
       int hashlen;      /* Number of octets for the hash algorithm's output */
       int hashblocklen; /* Number of octets the hash algorithm processes */
-      uschar * keyptr, * p;
+      const uschar * keyptr;
+      uschar * p;
       unsigned int keylen;
 
       uschar keyhash[MAX_HASHLEN];
@@ -6162,7 +6163,7 @@ while (*s)	/* known to be untainted */
       int moffset, moffsetextra, slen;
       pcre2_match_data * md;
       int emptyopt;
-      uschar * subject, * sub[3];
+      const uschar * subject, * sub[3];
       int save_expand_nmax =
         save_expand_strings(save_expand_nstring, save_expand_nlength);
       unsigned sub_textonly = 0;
@@ -6617,7 +6618,7 @@ while (*s)	/* known to be untainted */
 
     case EITEM_LISTQUOTE:
       {
-      uschar * sub[2];
+      const uschar * sub[2];
       switch(read_subs(sub, 2, 2, &s, flags, TRUE, name, &resetok, NULL))
         {
 	case -1: continue;	/* skipping */
@@ -7130,7 +7131,7 @@ while (*s)	/* known to be untainted */
         goto EXPAND_FAILED;
         }
 
-      switch(read_subs(argv, EXPAND_DLFUNC_MAX_ARGS + 2, 2, &s, flags,
+      switch(read_subs(CUSS argv, EXPAND_DLFUNC_MAX_ARGS + 2, 2, &s, flags,
            TRUE, name, &resetok, NULL))
         {
 	case -1: continue;	/* skipping */
@@ -7235,7 +7236,7 @@ while (*s)	/* known to be untainted */
     case EITEM_SRS_ENCODE:
       /* ${srs_encode {secret} {return_path} {orig_domain}} */
       {
-      uschar * sub[3];
+      const uschar * sub[3];
       uschar cksum[4];
       gstring * g = NULL;
       BOOL quoted = FALSE;
diff --git a/src/src/functions.h b/src/src/functions.h
index fd6cab0d5..9d4201a7b 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -279,8 +279,8 @@ extern int     host_nmtoa(int, const int *, int, uschar *, int);
 extern uschar *host_ntoa(int, const void *, uschar *, int *);
 extern int     host_scan_for_local_hosts(host_item *, host_item **, BOOL *);
 
-extern uschar *imap_utf7_encode(uschar *, const uschar *,
-				 uschar, uschar *, uschar **);
+extern const uschar *imap_utf7_encode(const uschar *, const uschar *,
+				 uschar, const uschar *, uschar **);
 
 extern void    invert_address(uschar *, uschar *);
 extern int     ip_addr(void *, int, const uschar *, int);
diff --git a/src/src/imap_utf7.c b/src/src/imap_utf7.c
index 09a32e689..a1abec7a0 100644
--- a/src/src/imap_utf7.c
+++ b/src/src/imap_utf7.c
@@ -11,14 +11,14 @@
 
 #ifdef SUPPORT_I18N
 
-uschar *
-imap_utf7_encode(uschar *string, const uschar *charset, uschar sep,
-  uschar *specials, uschar **error)
+const uschar *
+imap_utf7_encode(const uschar * string, const uschar * charset, uschar sep,
+  const uschar * specials, uschar ** error)
 {
 static uschar encode_base64[64] =
   "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,";
 size_t slen;
-uschar *sptr;
+const uschar * sptr;
 gstring * yield = NULL;
 int i = 0;	/* compiler quietening */
 uschar c = 0;	/* compiler quietening */
@@ -26,7 +26,7 @@ BOOL base64mode = FALSE;
 BOOL lastsep = FALSE;
 uschar utf16buf[256];
 uschar *utf16ptr;
-uschar *s;
+const uschar * s;
 uschar outbuf[256];
 uschar *outptr = outbuf;
 #if HAVE_ICONV
diff --git a/src/src/miscmods/perl.c b/src/src/miscmods/perl.c
index 5877d3dea..c94836f0b 100644
--- a/src/src/miscmods/perl.c
+++ b/src/src/miscmods/perl.c
@@ -275,7 +275,7 @@ yield string */
 
 static gstring *
 call_perl_cat(gstring * yield, uschar ** errstrp,
-  uschar * name, const uschar ** arg)
+  const uschar * name, const uschar ** arg)
 {
 dSP;
 SV * sv;
@@ -294,7 +294,7 @@ SAVETMPS;
 PUSHMARK(SP);
 while (*arg) XPUSHs(newSVpv(CCS (*arg++), 0));
 PUTBACK;
-items = perl_call_pv(CS name, G_SCALAR|G_EVAL);
+items = perl_call_pv(CS string_copy(name), G_SCALAR|G_EVAL);
 items = items;	/* stupid compiler quietening */
 SPAGAIN;
 sv = POPs;

commit e38b380adcf53f4f9771d0e1444afb1f4b79a801
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Dec 26 14:25:51 2025 +0000

    func prototypes

diff --git a/src/exim_monitor/em_TextPop.c b/src/exim_monitor/em_TextPop.c
index 83f4cf514..c58d8bf71 100644
--- a/src/exim_monitor/em_TextPop.c
+++ b/src/exim_monitor/em_TextPop.c
@@ -136,11 +136,8 @@ static char search_text_trans[] =
 
 /* ARGSUSED */
 void
-_XawTextDoSearchAction(w, event, params, num_params)
-Widget w;
-XEvent *event;
-String * params;
-Cardinal * num_params;
+_XawTextDoSearchAction(Widget w, XEvent * event, String * params,
+  Cardinal * num_params)
 {
   TextWidget tw = (TextWidget) XtParent(XtParent(XtParent(w)));
   Boolean popdown = FALSE;
@@ -162,11 +159,8 @@ Cardinal * num_params;
 
 /* ARGSUSED */
 void
-_XawTextPopdownSearchAction(w, event, params, num_params)
-Widget w;
-XEvent *event;
-String * params;
-Cardinal * num_params;
+_XawTextPopdownSearchAction(Widget w, XEvent * event, String * params,
+  Cardinal * num_params)
 {
   TextWidget tw = (TextWidget) XtParent(XtParent(XtParent(w)));
 
@@ -183,10 +177,7 @@ Cardinal * num_params;
 
 /* ARGSUSED */
 static void
-PopdownSearch(w, closure, call_data)
-Widget w;
-XtPointer closure;
-XtPointer call_data;
+PopdownSearch(Widget w, XtPointer closure, XtPointer call_data)
 {
   struct SearchAndReplace * search = (struct SearchAndReplace *) closure;
 
@@ -204,10 +195,7 @@ XtPointer call_data;
 
 /* ARGSUSED */
 static void
-SearchButton(w, closure, call_data)
-Widget w;
-XtPointer closure;
-XtPointer call_data;
+SearchButton(Widget w, XtPointer closure, XtPointer call_data)
 {
   (void) DoSearch( (struct SearchAndReplace *) closure );
 }
@@ -235,11 +223,7 @@ XtPointer call_data;
 #define SEARCH_HEADER ("Text Widget - Search():")
 
 void
-_XawTextSearch(w, event, params, num_params)
-Widget w;
-XEvent *event;
-String * params;
-Cardinal * num_params;
+_XawTextSearch(Widget w, XEvent *event, String * params, Cardinal * num_params)
 {
   TextWidget ctx = (TextWidget)w;
   XawTextScanDirection dir;
@@ -337,9 +321,7 @@ replace_active = replace_active; /* PH - shuts compilers up */
  */
 
 static void
-AddSearchChildren(form, ptr, tw)
-Widget form, tw;
-char * ptr;
+AddSearchChildren(Widget form, char * ptr, Widget tw)
 {
   Arg args[10];
   Cardinal num_args;
@@ -455,8 +437,7 @@ char * ptr;
 
 /* ARGSUSED */
 static Boolean
-DoSearch(search)
-struct SearchAndReplace * search;
+DoSearch(struct SearchAndReplace * search)
 {
   char msg[BUFSIZ];
   Widget tw = XtParent(search->search_popup);
@@ -530,10 +511,7 @@ msg2 = msg2; /* PH - shuts compilers up */
  */
 
 static void
-SetResource(w, res_name, value)
-Widget w;
-char * res_name;
-XtArgVal value;
+SetResource(Widget w, char * res_name, XtArgVal value)
 {
   Arg args[1];
 
@@ -548,8 +526,7 @@ XtArgVal value;
  */
 
 static String
-GetString(text)
-Widget text;
+GetString(Widget text)
 {
   String string;
   Arg args[1];
@@ -570,9 +547,7 @@ Widget text;
  */
 
 static void
-CenterWidgetOnPoint(w, event)
-Widget w;
-XEvent *event;
+CenterWidgetOnPoint(Widget w, XEvent *event)
 {
   Arg args[3];
   Cardinal num_args;
@@ -668,8 +643,7 @@ CreateDialog(Widget parent, String ptr, String name,
   */
 
 static Widget
-GetShell(w)
-Widget w;
+GetShell(Widget w)
 {
     while ((w != NULL) && !XtIsShell(w))
 	w = XtParent(w);
@@ -677,14 +651,7 @@ Widget w;
     return (w);
 }
 
-/* Add proper prototype to keep IRIX 6 compiler happy. PH */
-
-static Boolean InParams(String, String *, Cardinal);
-
-static Boolean InParams(str, p, n)
-    String str;
-    String *p;
-    Cardinal n;
+static Boolean InParams(String str, String *p, Cardinal n)
 {
     int i;
     for (i=0; i < n; p++, i++)
@@ -694,11 +661,11 @@ static Boolean InParams(str, p, n)
 
 static char *WM_DELETE_WINDOW = "WM_DELETE_WINDOW";
 
-static void WMProtocols(w, event, params, num_params)
-    Widget w;		/* popup shell */
-    XEvent *event;
-    String *params;
-    Cardinal *num_params;
+static void WMProtocols(
+    Widget w,		/* popup shell */
+    XEvent *event,
+    String *params,
+    Cardinal *num_params)
 {
     Atom wm_delete_window;
     Atom wm_protocols;
@@ -730,8 +697,8 @@ static void WMProtocols(w, event, params, num_params)
     }
 }
 
-static void SetWMProtocolTranslations(w)
-    Widget	w;	/* realized popup shell */
+static void SetWMProtocolTranslations(
+    Widget	w)	/* realized popup shell */
 {
     int i;
     XtAppContext app_context;

commit 84030909c23fbdd6aabcd38b6853d6097f33982d
Author: Bernard Quatermass <toolsmith@quatermass.co.uk>
Date:   Fri Dec 26 15:29:40 2025 +0000

    last push to github


commit d06324cdd0ee9fc3180810bc5e818da715f0be23
Author: Bernard Quatermass <toolsmith@quatermass.co.uk>
Date:   Fri Dec 26 15:31:18 2025 +0000

    Readme.pod removed in favour of up-to-date README.md


commit af14bc8cf9b19e8a53131c209d6cff2b82d6b476
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sat Dec 27 15:47:45 2025 +0000

    string-handling: insert space for empty item when building stringlist

diff --git a/src/src/string.c b/src/src/string.c
index 6be0b7678..b37606cf3 100644
--- a/src/src/string.c
+++ b/src/src/string.c
@@ -1060,7 +1060,7 @@ return NULL;
 ************************************************/
 /* This function is used to build a list, returning an allocated null-terminated
 growable string. The given element has any embedded separator characters
-doubled.
+doubled. A zero-length element results in a single space addition.
 
 Despite having the same growable-string interface as string_cat() the list is
 always returned null-terminated.
@@ -1082,13 +1082,18 @@ uschar * sp;
 if (list && list->ptr)
   list = string_catn(list, &sep, 1);
 
-while((sp = Ustrchr(ele, sep)))
+if (!*ele)
+  list = string_catn(list, US" ", 1);
+else
   {
-  list = string_catn(list, ele, sp-ele+1);
-  list = string_catn(list, &sep, 1);
-  ele = sp+1;
+  while((sp = Ustrchr(ele, sep)))
+    {
+    list = string_catn(list, ele, sp-ele+1);
+    list = string_catn(list, &sep, 1);
+    ele = sp+1;
+    }
+  list = string_cat(list, ele);
   }
-list = string_cat(list, ele);
 (void) string_from_gstring(list);
 return list;
 }

commit 13835a3c1e057efad7da269c0f93bf2eac850205
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Dec 29 00:14:20 2025 +0000

    LDAP: support module load triggered by variable access

diff --git a/src/src/expand.c b/src/src/expand.c
index 23ae31baa..3c68ab2e9 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -2175,6 +2175,30 @@ switch (vp->type)
       "failed to find %s module for %s: %s", US val, name, errstr);
     return US"";
     }
+
+  case vtype_lookup_module:
+    {
+    uschar * errstr = NULL;
+    const tree_node * t = tree_search(lookups_tree, val);
+#ifdef LOOKUP_MODULE_DIR
+    if (!t)
+      {
+      lookup_one_mod_load(val, &errstr);
+      t = tree_search(lookups_tree, val);
+      }
+#endif
+    if (t)
+      {
+      const lookup_info * li = t->data.ptr;
+      table = li->variables;
+      table_count = li->variables_count;
+      goto sublist;
+      }
+    log_write(0, LOG_MAIN|LOG_PANIC,
+      "failed to find %s module for %s%s%s", US val, name,
+	errstr ? ": " : "", errstr);
+    return US"";
+    }
   }
 
 return NULL;  /* Unknown variable. Silences static checkers. */

commit 40d39e3522a6f66e21591341c33b33c05864287d
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Jan 9 17:01:11 2026 +0000

    tidying

diff --git a/src/src/log.c b/src/src/log.c
index 64587a9f2..40bdfcd0c 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -462,15 +462,14 @@ to create the file; if running as root, this must be done in a subprocess to
 avoid races.
 
 Arguments:
-  fd         where to return the resulting file descriptor
   type       lt_main, lt_reject, lt_panic, or lt_debug
   tag        optional tag to include in the name (only hooked up for debug)
 
-Returns:   nothing
+Returns:   fd
 */
 
-static void
-open_log(int * fd, int type, const uschar * tag)
+static int
+open_log(int type, const uschar * tag)
 {
 uid_t euid;
 BOOL ok, ok2;
@@ -555,8 +554,10 @@ if (!ok)
 
 /* We now have the file name. After a successful open, return. */
 
-if ((*fd = log_open_as_exim(buffer)) >= 0)
-  return;
+ {
+  int fd = log_open_as_exim(buffer);
+  if (fd >= 0) return fd;
+ }
 
 euid = geteuid();
 
@@ -567,10 +568,7 @@ just bombing out, force the log to stderr and carry on if stderr is available.
 */
 
 if (euid != root_uid && euid != exim_uid && log_stderr)
-  {
-  *fd = fileno(log_stderr);
-  return;
-  }
+  return fileno(log_stderr);
 
 /* Otherwise this is a disaster. This call is deliberately ONLY to the panic
 log. If possible, save a copy of the original line that was being logged. If we
@@ -1144,7 +1142,7 @@ if (  flags & LOG_MAIN
 
     if (mainlogfd < 0)
       {
-      open_log(&mainlogfd, lt_main, NULL);     /* No return on error */
+      mainlogfd = open_log(lt_main, NULL);     /* No return on error */
       if (fstat(mainlogfd, &statbuf) >= 0) mainlog_inode = statbuf.st_ino;
       }
 
@@ -1247,7 +1245,7 @@ if (flags & LOG_REJECT)
 
     if (rejectlogfd < 0)
       {
-      open_log(&rejectlogfd, lt_reject, NULL); /* No return on error */
+      rejectlogfd = open_log(lt_reject, NULL); /* No return on error */
       if (fstat(rejectlogfd, &statbuf) >= 0) rejectlog_inode = statbuf.st_ino;
       }
 
@@ -1280,7 +1278,7 @@ if (flags & LOG_PANIC)
   if (logging_mode & LOG_MODE_FILE)
     {
     panic_recurseflag = TRUE;
-    open_log(&paniclogfd, lt_panic, NULL);  /* Won't return on failure */
+    paniclogfd = open_log(lt_panic, NULL);  /* Won't return on failure */
     panic_recurseflag = FALSE;
 
     if (panic_save_buffer)
@@ -1560,13 +1558,12 @@ if (opts)
 
 /* When activating from a transport process we may never have logged at all
 resulting in certain setup not having been done.  Hack this for now so we
-do not segfault; note that nondefault log locations will not work */
+do not segfault; note that nondefault log locations (set via log_file_path)
+will not work for that case. */
 
 if (!*file_path) set_file_path();
 
-open_log(&debug_fd, lt_debug, tag_name);
-
-if (debug_fd != -1)
+if ((debug_fd = open_log(lt_debug, tag_name)) != -1)
   debug_file = fdopen(debug_fd, "w");
 else
   log_write(0, LOG_MAIN|LOG_PANIC, "unable to open debug log");

commit 4dd582c432c7f8817d1484b91bb5607851d85ebb
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Jan 13 14:57:07 2026 +0000

    Tidying: move ETRN handling out-of-line

diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 643d88ca2..bfcdb22af 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -3786,6 +3786,185 @@ if (!(s = expand_string(s)))
 return *s ? Uatoi(s) : 0;
 }
 
+
+
+static int
+etrn_handle(uschar ** user_msgp, uschar ** log_msgp)
+{
+int rc;
+uschar * etrn_command, * etrn_serialize_key = NULL;
+const uschar ** argv;
+void (*oldsignal)(int);
+pid_t pid;
+
+if (sender_address)
+  return synprot_error(L_smtp_protocol_error, 503, NULL,
+    US"ETRN is not permitted inside a transaction");
+
+log_write(L_etrn, LOG_MAIN, "ETRN %s received from %s", smtp_cmd_argument,
+  host_and_ident(FALSE));
+
+GET_OPTION("acl_smtp_etrn");
+if ((rc = acl_check(ACL_WHERE_ETRN, NULL, acl_smtp_etrn,
+	    user_msgp, log_msgp)) != OK)
+  return smtp_handle_acl_fail(ACL_WHERE_ETRN, rc, *user_msgp, *log_msgp);
+
+/* Compute the serialization key for this command. We used (all the way
+back to 4.00) to include the given string as part of the key, but this
+opens a security hole for hintsdb types that use a command-string for
+operations. So, use a hash of the string. All ETRN with the same command
+hash are serialized */
+
+if (smtp_etrn_serialize)
+  {
+  md5 hash;
+  uschar * digest = store_get(16, GET_TAINTED);
+
+  md5_start(&hash);
+  md5_end(&hash, smtp_cmd_argument, Ustrlen(smtp_cmd_argument), digest);
+
+  etrn_serialize_key = string_sprintf("etrn-%.16H", digest);
+  }
+
+/* If a command has been specified for running as a result of ETRN, we
+permit any argument to ETRN. If not, only the # standard form is
+permitted, since that is strictly the only kind of ETRN that can be
+implemented according to the RFC. */
+
+GET_OPTION("smtp_etrn_command");
+if (smtp_etrn_command)
+  {
+  uschar * error;
+  BOOL rc;
+  etrn_command = smtp_etrn_command;
+  deliver_domain = smtp_cmd_data;
+  rc = transport_set_up_command(&argv, smtp_etrn_command,
+		  TSUC_EXPAND_ARGS, 0, NULL, US"ETRN processing", &error);
+  deliver_domain = NULL;
+  if (!rc)
+    {
+    log_write(0, LOG_MAIN|LOG_PANIC, "failed to set up ETRN command: %s",
+      error);
+    smtp_printf("458 Internal failure\r\n", SP_NO_MORE);
+    return 0;
+    }
+  }
+
+/* Else set up to call Exim with the -R option. */
+
+else
+  {
+  if (*smtp_cmd_data++ != '#')
+    return synprot_error(L_smtp_syntax_error, 501, NULL,
+      US"argument must begin with #");
+
+  etrn_command = US"exim -R";
+  argv = CUSS child_exec_exim(CEE_RETURN_ARGV, TRUE, NULL, TRUE,
+    *queue_name ? 4 : 2,
+    US"-R", smtp_cmd_data,
+    US"-MCG", queue_name);
+  }
+
+/* If we are host-testing, don't actually do anything. */
+
+if (host_checking)
+  {
+  HDEBUG(D_any)
+    {
+    debug_printf("ETRN command is: %s\n", etrn_command);
+    debug_printf("ETRN command execution skipped\n");
+    }
+  if (*user_msgp)
+    smtp_user_msg(US"250", *user_msgp);
+  else
+    smtp_printf("250 OK\r\n", SP_NO_MORE);
+  return 0;
+  }
+
+
+/* If ETRN queue runs are to be serialized, check the database to
+ensure one isn't already running. */
+
+if (smtp_etrn_serialize && !enq_start(etrn_serialize_key, 1))
+  {
+  smtp_printf("458 Already processing %s\r\n", SP_NO_MORE, smtp_cmd_data);
+  return 0;
+  }
+
+/* Fork a child process and run the command. We don't want to have to
+wait for the process at any point, so set SIGCHLD to SIG_IGN before
+forking. It should be set that way anyway for external incoming SMTP,
+but we save and restore to be tidy. If serialization is required, we
+actually run the command in yet another process, so we can wait for it
+to complete and then remove the serialization lock. */
+
+oldsignal = signal(SIGCHLD, SIG_IGN);
+
+if ((pid = exim_fork(US"etrn-command")) == 0)
+  {
+  smtp_input = FALSE;       /* This process is not associated with the */
+  smtp_inout_close();	  /* SMTP call any more. */
+
+  signal(SIGCHLD, SIG_DFL);      /* Want to catch child */
+
+  /* If not serializing, do the exec right away. Otherwise, fork down
+  into another process. */
+
+  if (  !smtp_etrn_serialize
+     || (pid = exim_fork(US"etrn-serialised-command")) == 0)
+    {
+    DEBUG(D_exec) debug_print_argv(argv);
+    exim_nullstd();                   /* Ensure std{in,out,err} exist */
+    /* argv[0] should be untainted, from child_exec_exim() */
+    execv(CS argv[0], (char *const *)argv);
+    log_write_die(0, LOG_MAIN, "exec of %q (ETRN) failed: %s",
+      etrn_command, strerror(errno));
+    _exit(EXIT_FAILURE);         /* paranoia */
+    }
+
+  /* Obey this if smtp_serialize and the 2nd fork yielded non-zero. That
+  is, we are in the first subprocess, after forking again. All we can do
+  for a failing fork is to log it. Otherwise, wait for the 2nd process to
+  complete, before removing the serialization. */
+
+  if (pid < 0)
+    log_write(0, LOG_MAIN|LOG_PANIC, "2nd fork for serialized ETRN "
+      "failed: %s", strerror(errno));
+  else
+    {
+    int status;
+    DEBUG(D_any) debug_printf("waiting for serialized ETRN process %d\n",
+      (int)pid);
+    (void)wait(&status);
+    DEBUG(D_any) debug_printf("serialized ETRN process %d ended\n",
+      (int)pid);
+    }
+
+  if (smtp_etrn_serialize) enq_end(etrn_serialize_key);
+  exim_underbar_exit(EXIT_SUCCESS);
+  }
+
+/* Back in the top level SMTP process. Check that we started a subprocess
+and restore the signal state. */
+
+if (pid < 0)
+  {
+  log_write(0, LOG_MAIN|LOG_PANIC, "fork of process for ETRN failed: %s",
+    strerror(errno));
+  smtp_printf("458 Unable to fork process\r\n", SP_NO_MORE);
+  if (smtp_etrn_serialize) enq_end(etrn_serialize_key);
+  }
+else
+  if (*user_msgp)
+    smtp_user_msg(US"250", *user_msgp);
+  else
+    smtp_printf("250 OK\r\n", SP_NO_MORE);
+
+signal(SIGCHLD, oldsignal);
+return 0;
+}
+
+
 /*************************************************
 *       Initialize for SMTP incoming message     *
 *************************************************/
@@ -3870,14 +4049,10 @@ value. The values are 2 larger than the required yield of the function. */
 
 while (done <= 0)
   {
-  const uschar ** argv;
-  uschar * etrn_command, * etrn_serialize_key, * errmess;
-  uschar * log_msg, * smtp_code;
+  uschar * errmess, * log_msg, * smtp_code;
   uschar * user_msg = NULL, * recipient = NULL, * hello = NULL;
   uschar * s, * ss;
   BOOL was_rej_mail = FALSE, was_rcpt = FALSE;
-  void (*oldsignal)(int);
-  pid_t pid;
   int start, end, sender_domain, recipient_domain;
   int rc, c, dsn_flags;
   uschar * orcpt = NULL;
@@ -5723,176 +5898,9 @@ while (done <= 0)
 
     case ETRN_CMD:
       HAD(SCH_ETRN);
-      if (sender_address)
-	{
-	done = synprot_error(L_smtp_protocol_error, 503, NULL,
-	  US"ETRN is not permitted inside a transaction");
-	break;
-	}
-
-      log_write(L_etrn, LOG_MAIN, "ETRN %s received from %s", smtp_cmd_argument,
-	host_and_ident(FALSE));
-
-      GET_OPTION("acl_smtp_etrn");
-      if ((rc = acl_check(ACL_WHERE_ETRN, NULL, acl_smtp_etrn,
-		  &user_msg, &log_msg)) != OK)
-	{
-	done = smtp_handle_acl_fail(ACL_WHERE_ETRN, rc, user_msg, log_msg);
-	break;
-	}
-
-      /* Compute the serialization key for this command. We used (all the way
-      back to 4.00) to include the given string as part of the key, but this
-      opens a security hole for hintsdb types that use a command-string for
-      operations. So, use a hash of the string. All ETRN with the same command
-      hash are serialized */
-
-      md5 hash;
-      uschar *digest = store_get(16, GET_TAINTED);
-
-      md5_start(&hash);
-      md5_end(&hash, smtp_cmd_argument, Ustrlen(smtp_cmd_argument), digest);
-
-      etrn_serialize_key = string_sprintf("etrn-%.16H", digest);
-
-      /* If a command has been specified for running as a result of ETRN, we
-      permit any argument to ETRN. If not, only the # standard form is
-      permitted, since that is strictly the only kind of ETRN that can be
-      implemented according to the RFC. */
-
-      GET_OPTION("smtp_etrn_command");
-      if (smtp_etrn_command)
-	{
-	uschar * error;
-	BOOL rc;
-	etrn_command = smtp_etrn_command;
-	deliver_domain = smtp_cmd_data;
-	rc = transport_set_up_command(&argv, smtp_etrn_command,
-			TSUC_EXPAND_ARGS, 0, NULL, US"ETRN processing", &error);
-	deliver_domain = NULL;
-	if (!rc)
-	  {
-	  log_write(0, LOG_MAIN|LOG_PANIC, "failed to set up ETRN command: %s",
-	    error);
-	  smtp_printf("458 Internal failure\r\n", SP_NO_MORE);
-	  break;
-	  }
-	}
-
-      /* Else set up to call Exim with the -R option. */
-
-      else
-	{
-	if (*smtp_cmd_data++ != '#')
-	  {
-	  done = synprot_error(L_smtp_syntax_error, 501, NULL,
-	    US"argument must begin with #");
-	  break;
-	  }
-	etrn_command = US"exim -R";
-	argv = CUSS child_exec_exim(CEE_RETURN_ARGV, TRUE, NULL, TRUE,
-	  *queue_name ? 4 : 2,
-	  US"-R", smtp_cmd_data,
-	  US"-MCG", queue_name);
-	}
-
-      /* If we are host-testing, don't actually do anything. */
-
-      if (host_checking)
-	{
-	HDEBUG(D_any)
-	  {
-	  debug_printf("ETRN command is: %s\n", etrn_command);
-	  debug_printf("ETRN command execution skipped\n");
-	  }
-	if (user_msg == NULL) smtp_printf("250 OK\r\n", SP_NO_MORE);
-	  else smtp_user_msg(US"250", user_msg);
-	break;
-	}
-
-
-      /* If ETRN queue runs are to be serialized, check the database to
-      ensure one isn't already running. */
-
-      if (smtp_etrn_serialize && !enq_start(etrn_serialize_key, 1))
-	{
-	smtp_printf("458 Already processing %s\r\n", SP_NO_MORE, smtp_cmd_data);
-	break;
-	}
-
-      /* Fork a child process and run the command. We don't want to have to
-      wait for the process at any point, so set SIGCHLD to SIG_IGN before
-      forking. It should be set that way anyway for external incoming SMTP,
-      but we save and restore to be tidy. If serialization is required, we
-      actually run the command in yet another process, so we can wait for it
-      to complete and then remove the serialization lock. */
-
-      oldsignal = signal(SIGCHLD, SIG_IGN);
-
-      if ((pid = exim_fork(US"etrn-command")) == 0)
-	{
-	smtp_input = FALSE;       /* This process is not associated with the */
-	smtp_inout_close();	  /* SMTP call any more. */
-
-	signal(SIGCHLD, SIG_DFL);      /* Want to catch child */
-
-	/* If not serializing, do the exec right away. Otherwise, fork down
-	into another process. */
-
-	if (  !smtp_etrn_serialize
-	   || (pid = exim_fork(US"etrn-serialised-command")) == 0)
-	  {
-	  DEBUG(D_exec) debug_print_argv(argv);
-	  exim_nullstd();                   /* Ensure std{in,out,err} exist */
-	  /* argv[0] should be untainted, from child_exec_exim() */
-	  execv(CS argv[0], (char *const *)argv);
-	  log_write_die(0, LOG_MAIN, "exec of %q (ETRN) failed: %s",
-	    etrn_command, strerror(errno));
-	  _exit(EXIT_FAILURE);         /* paranoia */
-	  }
-
-	/* Obey this if smtp_serialize and the 2nd fork yielded non-zero. That
-	is, we are in the first subprocess, after forking again. All we can do
-	for a failing fork is to log it. Otherwise, wait for the 2nd process to
-	complete, before removing the serialization. */
-
-	if (pid < 0)
-	  log_write(0, LOG_MAIN|LOG_PANIC, "2nd fork for serialized ETRN "
-	    "failed: %s", strerror(errno));
-	else
-	  {
-	  int status;
-	  DEBUG(D_any) debug_printf("waiting for serialized ETRN process %d\n",
-	    (int)pid);
-	  (void)wait(&status);
-	  DEBUG(D_any) debug_printf("serialized ETRN process %d ended\n",
-	    (int)pid);
-	  }
-
-	enq_end(etrn_serialize_key);
-	exim_underbar_exit(EXIT_SUCCESS);
-	}
-
-      /* Back in the top level SMTP process. Check that we started a subprocess
-      and restore the signal state. */
-
-      if (pid < 0)
-	{
-	log_write(0, LOG_MAIN|LOG_PANIC, "fork of process for ETRN failed: %s",
-	  strerror(errno));
-	smtp_printf("458 Unable to fork process\r\n", SP_NO_MORE);
-	if (smtp_etrn_serialize) enq_end(etrn_serialize_key);
-	}
-      else
-	if (!user_msg)
-	  smtp_printf("250 OK\r\n", SP_NO_MORE);
-	else
-	  smtp_user_msg(US"250", user_msg);
-
-      signal(SIGCHLD, oldsignal);
+      done = etrn_handle(&user_msg, &log_msg);
       break;
 
-
     case BADARG_CMD:
       done = synprot_error(L_smtp_syntax_error, 501, NULL,
 	US"unexpected argument data");

commit 7a7e1513914f2ded45e5e51426d77ec78b4f6d54
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Jan 13 21:20:07 2026 +0000

    tidying

diff --git a/src/exim_monitor/em_globals.c b/src/exim_monitor/em_globals.c
index 5627a2277..45dc6f298 100644
--- a/src/exim_monitor/em_globals.c
+++ b/src/exim_monitor/em_globals.c
@@ -131,11 +131,6 @@ time_t  deliver_frozen_at      = 0;
 BOOL    deliver_manual_thaw    = FALSE;
 
 #ifndef DISABLE_DKIM
-uschar *dkim_cur_signer          = NULL;
-uschar *dkim_signers             = NULL;
-uschar *dkim_signing_domain      = NULL;
-uschar *dkim_signing_selector    = NULL;
-uschar *dkim_verify_signers      = US"$dkim_signers";
 unsigned dkim_collect_input      = 0;
 BOOL    dkim_disable_verify      = FALSE;
 #endif
diff --git a/src/src/spool_in.c b/src/src/spool_in.c
index b475c3955..2dde341f0 100644
--- a/src/src/spool_in.c
+++ b/src/src/spool_in.c
@@ -266,7 +266,6 @@ tree_nonrecipients = NULL;
 #ifndef DISABLE_DKIM
 f.dkim_disable_verify = FALSE;
 # ifdef COMPILE_UTILITY
-dkim_signers = NULL;
 dkim_collect_input = 0;
 #else
   {

commit f92a63ebf6bcf991a09a0429808c61e2407bb10f
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Jan 16 20:01:33 2026 +0000

    DNS: fix memory leak

diff --git a/src/src/host.c b/src/src/host.c
index 95694635a..130334793 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -19,6 +19,11 @@ of Exim. */
 #include "exim.h"
 
 
+static int
+host_find_bydns_internal(host_item *, const uschar *,
+  int, const uschar *, const uschar *, const uschar *, const dnssec_domains *,
+  const uschar **, BOOL *, dns_answer *);
+
 /* Static variable for preserving the list of interface addresses in case it is
 used more than once. */
 
@@ -1770,8 +1775,9 @@ for (uschar * hname = sender_host_name; hname; hname = *aliases++)
   dnssec_domains d =
     { .request = sender_host_dnssec ? US"*" : NULL, .require = NULL };
 
-  if (  (rc = host_find_bydns(&h, NULL, HOST_FIND_BY_A | HOST_FIND_BY_AAAA,
-	  NULL, NULL, NULL, &d, NULL, NULL)) == HOST_FOUND
+  if (  (rc = host_find_bydns_internal(&h, NULL,
+	  HOST_FIND_BY_A | HOST_FIND_BY_AAAA,
+	  NULL, NULL, NULL, &d, NULL, NULL, dnsa)) == HOST_FOUND
      || rc == HOST_FOUND_LOCAL
      )
     {
@@ -1851,6 +1857,7 @@ yield = FAIL;
 
 out:
   expand_level--;
+  store_free_dns_answer(dnsa);
   return yield;
 }
 
@@ -2198,6 +2205,7 @@ function as it may be called to set the addresses of hosts taken from MX
 records.
 
 Arguments:
+  dnsa			a dns_answer area to use (avoid alloc another)
   host                  points to the host item we're filling in
   lastptr               points to pointer to last host item in a chain of
                           host items (may be updated if host is last and gets
@@ -2219,7 +2227,7 @@ Returns:       HOST_FIND_FAILED     couldn't find A record
 */
 
 static int
-set_address_from_dns(host_item *host, host_item **lastptr,
+set_address_from_dns(dns_answer * dnsa, host_item *host, host_item **lastptr,
   const uschar *ignore_target_hosts, BOOL allow_ip,
   const uschar **fully_qualified_name,
   BOOL dnssec_request, BOOL dnssec_require, int whichrrs)
@@ -2228,7 +2236,6 @@ host_item * thishostlast = NULL;    /* Indicates not yet filled in anything */
 BOOL v6_find_again = FALSE;
 BOOL dnssec_fail = FALSE;
 int i;
-dns_answer * dnsa;
 
 #ifndef DISABLE_TLS
 /* Copy the host name at this point to the value which is used for
@@ -2254,8 +2261,6 @@ if (allow_ip && string_is_ip_address(host->name, NULL) != 0)
   return HOST_FOUND;
   }
 
-dnsa = store_get_dns_answer();
-
 /* On an IPv6 system, unless IPv6 is disabled, go round the loop up to twice,
 looking for AAAA records the first time. However, unless doing standalone
 testing, we force an IPv4 lookup if the domain matches dns_ipv4_lookup global.
@@ -2467,7 +2472,6 @@ i = host->address
   : HOST_IGNORED;
 
 out:
-  store_free_dns_answer(dnsa);
   return i;
 }
 
@@ -2518,19 +2522,19 @@ Returns:                HOST_FIND_FAILED  Failed to find the host or domain;
                                           an address of the local host
 */
 
-int
-host_find_bydns(host_item * host, const uschar * ignore_target_hosts,
+static int
+host_find_bydns_internal(host_item * host, const uschar * ignore_target_hosts,
   int whichrrs,
   const uschar * srv_svclist, const uschar * srv_fail_domains,
   const uschar * mx_fail_domains, const dnssec_domains * dnssec_d,
-  const uschar ** fully_qualified_name, BOOL * removed)
+  const uschar ** fully_qualified_name, BOOL * removed,
+  dns_answer * dnsa)
 {
 host_item * h, * last;
 #ifdef EXPERIMENTAL_SRV_SMTPS
 BOOL srv_smtps = FALSE;
 #endif
 int rc = DNS_FAIL, ind_type = 0, yield;
-dns_answer * dnsa = store_get_dns_answer();
 dns_scan dnss = {0};
 BOOL dnssec_require, dnssec_request;
 dnssec_status_t dnssec;
@@ -2726,7 +2730,7 @@ if (rc != DNS_SUCCEED)
   host->tls_needs = SRV_TLS_UNK;
 #endif
   lookup_dnssec_authenticated = NULL;
-  rc = set_address_from_dns(host, &last, ignore_target_hosts, FALSE,
+  rc = set_address_from_dns(dnsa, host, &last, ignore_target_hosts, FALSE,
     fully_qualified_name, dnssec_request, dnssec_require, whichrrs);
 
   /* If one or more address records have been found, check that none of them
@@ -3047,7 +3051,7 @@ for (h = host; h != last->next; h = h->next)
   {
   if (h->address) continue;  /* Inserted by a multihomed host */
 
-  rc = set_address_from_dns(h, &last, ignore_target_hosts, allow_mx_to_ip,
+  rc = set_address_from_dns(dnsa, h, &last, ignore_target_hosts, allow_mx_to_ip,
     NULL, dnssec_request, dnssec_require,
     whichrrs & HOST_FIND_IPV4_ONLY
     ?  HOST_FIND_BY_A  :  HOST_FIND_BY_A | HOST_FIND_BY_AAAA);
@@ -3172,6 +3176,20 @@ DEBUG(D_host_lookup)
 out:
 
 dns_init(FALSE, FALSE, FALSE);	/* clear the dnssec bit for getaddrbyname */
+return yield;
+}
+
+int
+host_find_bydns(host_item * host, const uschar * ignore_target_hosts,
+  int whichrrs,
+  const uschar * srv_svclist, const uschar * srv_fail_domains,
+  const uschar * mx_fail_domains, const dnssec_domains * dnssec_d,
+  const uschar ** fully_qualified_name, BOOL * removed)
+{
+dns_answer * dnsa = store_get_dns_answer();
+int yield = host_find_bydns_internal(host, ignore_target_hosts, whichrrs,
+  srv_svclist, srv_fail_domains, mx_fail_domains, dnssec_d,
+  fully_qualified_name, removed, dnsa);
 store_free_dns_answer(dnsa);
 return yield;
 }

commit 7d5f543c395691cdffdba8013ee4684834f6cb01
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Jan 19 00:57:15 2026 +0000

    DMARC (native): ignore tags failing verification

diff --git a/src/src/miscmods/dmarc_native.c b/src/src/miscmods/dmarc_native.c
index 96628dee1..9fa4a1924 100644
--- a/src/src/miscmods/dmarc_native.c
+++ b/src/src/miscmods/dmarc_native.c
@@ -192,24 +192,30 @@ tag policy_tags[] = {
 
 /* Handle one potential tag
 Return: boolean success; else parsing error
-
-RFC 7489 6.3 :- unknown tags are ignored
 */
-static int
+static BOOL
 parse_tag(const uschar * tagrecord, dmarc_policy_record * prp)
 {
 const uschar * e = Ustrchr(tagrecord, '='), * s;
 
 /* RFC 6736 3.2 tagspec must have = */
-if (!*e)
+if (!e)
+  {
+  DEBUG(D_receive)
+    debug_printf_indent("DMARC: missing '=' for tag in %q\n", tagrecord);
   return FALSE;
+  }
 
 /* RFC 6736 3.2 ignore whitespace between tag name and = */
 for (s = e; s > tagrecord && isspace(s[-1]); ) s--;
 
 /* RFC 6736 3.2 tag name at least 1 char */
 if (s == tagrecord)
+  {
+  DEBUG(D_receive)
+    debug_printf_indent("DMARC: missing tag name in %q\n", tagrecord);
   return FALSE;
+  }
 
 /* search for tag name in our table of known ones */
 for (tag * ptp = policy_tags; ptp < policy_tags + nelem(policy_tags); ptp++)
@@ -226,13 +232,20 @@ for (tag * ptp = policy_tags; ptp < policy_tags + nelem(policy_tags); ptp++)
     s = e + 1;
     Uskip_whitespace(&s);
 
-    if (!ptp->verify(s)) DEBUG(D_receive)
+    if (ptp->verify(s))
+      {
+      *vp = string_copy(s);
+      return TRUE;
+      }
+    DEBUG(D_receive)
       debug_printf_indent("DMARC: bad value for tag %q: %q\n", ptp->name, s);
-    *vp = string_copy(s);
-    break;
+    return FALSE;
     }
  }
-return TRUE;
+
+DEBUG(D_receive)
+  debug_printf_indent("DMARC: no recognised tag in %q\n", tagrecord);
+return FALSE;
 }
 
 static BOOL
@@ -242,10 +255,10 @@ dmarc_local_parse_policy(const uschar * rr, dmarc_policy_record * prp)
 int sep = ';';
 
 /* RFC 6736 3.2 :- ignore whitespace preceding tag-name and after value */
+/* RFC 7489 6.3 :- syntax errors and unknown tags are ignored */
 
 for (uschar * tagspec; tagspec = string_nextinlist(&rr, &sep, NULL, 0); )
-  if (!parse_tag(tagspec, prp))
-    return FALSE;
+  (void) parse_tag(tagspec, prp);
 
 return TRUE;
 }

commit bdb65dd6fd1af3f7d186adc05bfe27f11b1403d4
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Jan 19 14:52:58 2026 +0000

    Debug: add "macro" channel

diff --git a/src/src/buildconfig.c b/src/src/buildconfig.c
index b365643bb..081d1041b 100644
--- a/src/src/buildconfig.c
+++ b/src/src/buildconfig.c
@@ -207,6 +207,9 @@ else
   fprintf(new, "#define SSIZE_T_FMT  \"%%d\"\n");
 #endif
 
+fprintf(new, "#define EXIM_ULONG_BITS	%u\n",
+		sizeof(unsigned long) == 8 ? 64 : 32);
+
 /* Now search the makefile for certain settings */
 
 if (!(base = fopen("Makefile", "rb")))
diff --git a/src/src/daemon.c b/src/src/daemon.c
index e8432196c..a37cc7cc6 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -188,7 +188,7 @@ pid_t pid;
 union sockaddr_46 interface_sockaddr;
 EXIM_SOCKLEN_T ifsize = sizeof(interface_sockaddr);
 int max_for_this_host = 0;
-int save_log_selector = *log_selector;
+unsigned long save_log_selector = *log_selector;
 gstring * whofrom;
 
 rmark reset_point = store_mark();
diff --git a/src/src/exim.c b/src/src/exim.c
index 7e90e927c..e11250cc0 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -2822,7 +2822,7 @@ on the second character (the one after '-'), to save some effort. */
       /* Use an intermediate variable so that we don't set debugging while
       decoding the debugging bits. */
 
-      unsigned int selector = D_default;
+      unsigned long selector = D_default;
       debug_selector = 0;
       debug_file = NULL;
       if (*argrest == 'd')
diff --git a/src/src/exim_dbmbuild.c b/src/src/exim_dbmbuild.c
index 8cb06fefe..c6c884125 100644
--- a/src/src/exim_dbmbuild.c
+++ b/src/src/exim_dbmbuild.c
@@ -77,7 +77,7 @@ const uschar * parse_find_address_end_gen(const uschar * s, BOOL b)
 
 
 struct global_flags	f;
-unsigned int		log_selector[1];
+unsigned long		log_selector[1];
 uschar *		queue_name;
 BOOL			split_spool_directory;
 
@@ -98,7 +98,7 @@ const uschar *hex_digits = CUS"0123456789abcdef";
 *   Debug output   *
 *******************/
 
-unsigned int debug_selector = 0;	/* set -1 for debugging */
+unsigned long debug_selector = 0;	/* set -1 for debugging */
 
 void
 debug_printf(const char * fmt, ...)
diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index b6977513b..2955861fa 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -65,7 +65,7 @@ const uschar * parse_find_address_end_gen(const uschar * s, BOOL b)
 {return NULL; }
 
 struct global_flags	f;
-unsigned int		log_selector[1];
+unsigned long		log_selector[1];
 uschar *		queue_name;
 BOOL			split_spool_directory;
 
@@ -113,7 +113,7 @@ exit(EXIT_FAILURE);
 *   Debug output   *
 *******************/
 
-unsigned int debug_selector = 0;	/* set -1 for debugging */
+unsigned long debug_selector = 0;	/* set -1 for debugging */
 
 void
 debug_printf(const char * fmt, ...)
diff --git a/src/src/functions.h b/src/src/functions.h
index 9d4201a7b..677972e01 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -139,8 +139,8 @@ extern BOOL    bdat_hasc(void);
 extern int     bdat_ungetc(int);
 extern void    bdat_flush_data(void);
 
-extern void    bits_clear(unsigned int *, size_t, int *);
-extern void    bits_set(unsigned int *, size_t, int *);
+extern void    bits_clear(unsigned long *, size_t, int *);
+extern void    bits_set(unsigned long *, size_t, int *);
 
 extern void    cancel_cutthrough_connection(BOOL, const uschar *);
 extern gstring *cat_file(FILE *, gstring *, const uschar *);
@@ -188,7 +188,7 @@ extern void    debug_pretrigger_discard(void);
 extern void    debug_print_socket(int);
 extern void    debug_trigger_fire(void);
 
-extern void    decode_bits(unsigned int *, size_t, int *,
+extern void    decode_bits(unsigned long *, size_t, int *,
 	           const uschar *, bit_table *, int, uschar *, int);
 extern void    delete_pid_file(void);
 extern void    deliver_local(address_item *, BOOL);
diff --git a/src/src/globals.c b/src/src/globals.c
index 2b2270899..90906e923 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -657,6 +657,7 @@ FILE   *debug_file             = NULL;
 int     debug_notall[]         = {
   Di_memory,
   Di_noutf8,
+  Di_macro,
   -1
 };
 bit_table debug_options[]      = { /* must be in alphabetical order and use
@@ -678,6 +679,7 @@ bit_table debug_options[]      = { /* must be in alphabetical order and use
   BIT_TABLE(D, load),
   BIT_TABLE(D, local_scan),
   BIT_TABLE(D, lookup),
+  BIT_TABLE(D, macro),
   BIT_TABLE(D, memory),
   BIT_TABLE(D, noutf8),
   BIT_TABLE(D, pid),
@@ -698,7 +700,7 @@ int      debug_options_count	= nelem(debug_options);
 uschar   debuglog_name[LOG_NAME_SIZE] = {0};
 unsigned debug_pretrigger_bsize	= 0;
 uschar * debug_pretrigger_buf	= NULL;
-unsigned int debug_selector	= 0;
+unsigned long debug_selector	= 0;
 BOOL	 debug_startup		= FALSE;
 
 int     delay_warning[DELAY_WARNING_SIZE] = { DELAY_WARNING_SIZE, 1, 24*60*60 };
@@ -1013,7 +1015,7 @@ int     log_options_count      = nelem(log_options);
 
 const uschar *log_ports	       = NULL;
 int     log_reject_target      = 0;
-unsigned int log_selector[log_selector_size]; /* initialized in main() */
+unsigned long log_selector[log_selector_size]; /* initialized in main() */
 uschar *log_selector_string    = NULL;
 FILE   *log_stderr             = NULL;
 uschar *login_sender_address   = NULL;
diff --git a/src/src/globals.h b/src/src/globals.h
index befaf308b..5402fb59e 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -673,7 +673,7 @@ extern int     log_notall[];           /* Log options excluded from +all */
 extern bit_table log_options[];        /* Table of options */
 extern int     log_options_count;      /* Size of table */
 extern int     log_reject_target;      /* Target log for ACL rejections */
-extern unsigned int log_selector[];    /* Bit map of logging options */
+extern unsigned long log_selector[];   /* Bit map of logging options */
 extern uschar *log_selector_string;    /* As supplied in the config */
 extern FILE   *log_stderr;             /* Copy of stderr for log use, or NULL */
 extern BOOL    log_timezone;           /* TRUE to include the timezone in log lines */
diff --git a/src/src/local_scan.h b/src/src/local_scan.h
index c9d179375..6e63672eb 100644
--- a/src/src/local_scan.h
+++ b/src/src/local_scan.h
@@ -44,7 +44,7 @@ each time a new feature is added (in a way that doesn't break backward
 compatibility). */
 
 #define LOCAL_SCAN_ABI_VERSION_MAJOR 7
-#define LOCAL_SCAN_ABI_VERSION_MINOR 0
+#define LOCAL_SCAN_ABI_VERSION_MINOR 1
 #define LOCAL_SCAN_ABI_VERSION \
   LOCAL_SCAN_ABI_VERSION_MAJOR.LOCAL_SCAN_ABI_VERSION_MINOR
 
@@ -165,7 +165,7 @@ typedef struct recipient_item {
 
 /* Global variables that are documented as visible in the function. */
 
-extern unsigned int debug_selector;    /* Debugging bits */
+extern unsigned long debug_selector;   /* Debugging bits */
 
 extern int     body_linecount;         /* Line count in body */
 extern int     body_zerocount;         /* Binary zero count in body */
diff --git a/src/src/log.c b/src/src/log.c
index 40bdfcd0c..0220b91bb 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -1366,14 +1366,14 @@ Arguments:
 */
 
 void
-bits_clear(unsigned int *selector, size_t selsize, int *bits)
+bits_clear(unsigned long * selector, size_t selsize, int * bits)
 {
 for(; *bits != -1; ++bits)
   BIT_CLEAR(selector, selsize, *bits);
 }
 
 void
-bits_set(unsigned int *selector, size_t selsize, int *bits)
+bits_set(unsigned long * selector, size_t selsize, int * bits)
 {
 for(; *bits != -1; ++bits)
   BIT_SET(selector, selsize, *bits);
@@ -1403,7 +1403,7 @@ we treat it as an unknown option: error message to stderr and die.
 Arguments:
   selector       address of the bit string
   selsize        number of words in the bit string
-  notall         list of bits to exclude from "all"
+  notall         list of bit-numbers to exclude from "all"
   string         the configured string
   options        the table of option names
   count          size of table
@@ -1414,11 +1414,12 @@ Returns:         nothing on success - bomb out on failure
 */
 
 void
-decode_bits(unsigned int * selector, size_t selsize, int * notall,
+decode_bits(unsigned long * selector, size_t selsize, int * notall,
   const uschar * string, bit_table * options, int count, uschar * which,
   int flags)
 {
-uschar *errmsg;
+uschar * errmsg;
+
 if (!string) return;
 
 if (*string == '=')
diff --git a/src/src/macros.h b/src/src/macros.h
index f6b240322..a1a9cd6c1 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -101,7 +101,7 @@ don't make the file descriptors two-way. */
 /* Debugging control */
 
 #define LOG_NAME_SIZE 256
-#define IS_DEBUG(x)	(debug_selector & (x))
+#define IS_DEBUG(x)	(debug_selector & (x ? x : D_any))
 #define DEBUG(x)	if (IS_DEBUG(x))
 #define HDEBUG(x)	if (host_checking || IS_DEBUG(x))
 
@@ -327,17 +327,21 @@ for having to swallow the rest of an SMTP message is whether the value is
 
 /* Bit masks for debug and log selectors */
 
-/* Assume words are 32 bits wide. Tiny waste of space on 64 bit
+/* Assume words are at least 32 bits wide. Tiny waste of space on 64 bit
 platforms, but this ensures bit vectors always work the same way. */
-#define BITWORDSIZE 32
+#ifdef EXIM_ULONG_BITS
+  #define BITWORDSIZE EXIM_ULONG_BITS
+#else
+  #define BITWORDSIZE 64
+#endif
 
 /* This macro is for single-word bit vectors: the debug selector,
 and the first word of the log selector. */
 #define BIT(n) (1UL << (n))
 
 /* And these are for multi-word vectors. */
-#define BITWORD(n) (      (n) / BITWORDSIZE)
-#define BITMASK(n) (1U << (n) % BITWORDSIZE)
+#define BITWORD(n) (       (n) / BITWORDSIZE)
+#define BITMASK(n) (1UL << (n) % BITWORDSIZE)
 
 #define BIT_CLEAR(s,z,n) ((s)[BITWORD(n)] &= ~BITMASK(n))
 #define BIT_SET(s,z,n)   ((s)[BITWORD(n)] |=  BITMASK(n))
@@ -362,9 +366,13 @@ These must match the debug_options table in globals.c .
 
 Exim's code assumes in a number of places that the debug_selector is one
 word, and this is exposed in the local_scan ABI. The D_v and D_local_scan bit
-masks are part of the local_scan API so are #defined in local_scan.h */
+masks are part of the local_scan API so are #defined in local_scan.h .
+
+Thanks to the "one word", debug bits beyond 31 are not available on 32b-int
+systems, and coding must account for that. */
 
-#define DEBUG_BIT(name) Di_##name = IOTA(Di_iota), D_##name = (int)BIT(Di_##name)
+#define DEBUG_BIT(name) Di_##name = IOTA(Di_iota), D_##name = (unsigned long)BIT(Di_##name)
+#define DEBUG_Z_BIT(name) Di_##name = 0, D_##name = 0
 
 enum {
   Di_all        = -1,
@@ -402,8 +410,14 @@ enum {
   DEBUG_BIT(transport),
   DEBUG_BIT(uid),
   DEBUG_BIT(verify),		/* 31 */
+#if EXIM_ULONG_BITS > 32
+  DEBUG_BIT(macro),		/* 33 */
+#else
+  DEBUG_Z_BIT(macro),
+#endif
 };
 
+
 /* Multi-bit debug masks */
 
 #define D_all                        0xffffffff
diff --git a/src/src/os.c b/src/src/os.c
index 4b9d2d1d5..44a53a575 100644
--- a/src/src/os.c
+++ b/src/src/os.c
@@ -12,8 +12,8 @@
 # include <stdio.h>
 # include <time.h>
 #else
-# define IS_DEBUG(x) (debug_selector & (x))
-# define DEBUG(x) if (IS_DEBUG(x))
+# define IS_DEBUG(x) (debug_selector & (x ? x : D_any))
+# define DEBUG(x) if (IS_DEBUG(x))	/* for cppcheck */
 #endif
 
 #ifndef CS
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 356321765..e95a2aed3 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -786,7 +786,7 @@ macro_create(const uschar * name, const uschar * val, BOOL command_line)
 {
 macro_item * m = store_get(sizeof(macro_item), GET_UNTAINTED);
 
-EARLY_DEBUG(D_any, "%s: '%s' '%s'\n", __FUNCTION__, name, val);
+EARLY_DEBUG(D_macro, "%s: '%s' '%s'\n", __FUNCTION__, name, val);
 m->next = NULL;
 m->command_line = command_line;
 m->namelen = Ustrlen(name);
@@ -979,11 +979,11 @@ if (*s) for (macro_item * m = *s == '_' ? macros : macros_user; m; m = m->next)
     {
     int moveby;
 
-    DEBUG(D_any)
+    DEBUG(D_macro)
       if (f.expansion_test)
 	printf("macro '%s' -> '%s'\n", m->name, m->replacement);
       else
-	EARLY_DEBUG(D_any, "%s: matched '%s' in '%.*s'\n", __FUNCTION__,
+	EARLY_DEBUG(D_macro, "%s: matched '%s' in '%.*s'\n", __FUNCTION__,
 	  m->name, (int) Ustrlen(ss)-1, ss);
 
     /* Expand the buffer if necessary */

commit 6f268ee4c14cdde79b29809ef25179fb6b60689b
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Jan 20 18:48:15 2026 +0000

    Debug: ensure extanded channels propagated
    
    Broken-by: bdb65dd6fd1a

diff --git a/src/src/child.c b/src/src/child.c
index 359b791e8..05f87d856 100644
--- a/src/src/child.c
+++ b/src/src/child.c
@@ -107,7 +107,7 @@ if (!minimal)
     {
     if (debug_selector != 0)
       {
-      argv[n++] = string_sprintf("-d=0x%x", debug_selector);
+      argv[n++] = string_sprintf("-d=0x%lx", debug_selector);
       if (debug_fd > 2)
 	{
 	int flags = fcntl(debug_fd, F_GETFD);
diff --git a/src/src/daemon.c b/src/src/daemon.c
index a37cc7cc6..607913f6c 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -365,9 +365,8 @@ if (pid == 0)
   {
   int queue_only_reason = 0;
   int old_pool = store_pool;
-  int save_debug_selector = debug_selector;
-  BOOL local_queue_only;
-  BOOL session_local_queue_only;
+  unsigned long save_debug_selector = debug_selector;
+  BOOL local_queue_only,  session_local_queue_only;
 #ifdef SA_NOCLDWAIT
   struct sigaction act;
 #endif
diff --git a/src/src/environment.c b/src/src/environment.c
index 9cbd352ec..4b8041f9f 100644
--- a/src/src/environment.c
+++ b/src/src/environment.c
@@ -42,7 +42,7 @@ if (!keep_environment || !*keep_environment)
 else if (Ustrcmp(keep_environment, "*") != 0)
   {
   rmark reset_point = store_mark();
-  unsigned deb = debug_selector;
+  unsigned long deb = debug_selector;
   BOOL hc = host_checking;
   debug_selector = 0;			/* quieten this clearout */
   host_checking = FALSE;
diff --git a/src/src/exim.c b/src/src/exim.c
index e11250cc0..6924b4b14 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -4464,10 +4464,10 @@ a debugging feature for finding out what arguments certain MUAs actually use.
 Don't attempt it if logging is disabled, or if listing variables or if
 verifying/testing addresses or expansions. */
 
-if (  (debug_selector & D_any  ||  LOGGING(arguments))
+if (  (IS_DEBUG(D_any)  ||  LOGGING(arguments))
    && f.really_exim && !list_options && !checking)
   {
-  uschar *p = big_buffer;
+  uschar * p = big_buffer;
   Ustrcpy(p, US"cwd= (failed)");
 
   if (!initial_cwd)
diff --git a/src/src/rewrite.c b/src/src/rewrite.c
index 97c811f41..cf65fc242 100644
--- a/src/src/rewrite.c
+++ b/src/src/rewrite.c
@@ -241,7 +241,7 @@ for (rewrite_rule * rule = rewrite_rules;
 
   /* We have a validly rewritten address */
 
-  if (LOGGING(address_rewrite) || debug_selector & D_rewrite)
+  if (LOGGING(address_rewrite) || IS_DEBUG(D_rewrite))
     {
     const uschar * where = CUS"?";
 
diff --git a/src/src/spool_in.c b/src/src/spool_in.c
index 2dde341f0..35fa5089b 100644
--- a/src/src/spool_in.c
+++ b/src/src/spool_in.c
@@ -620,7 +620,7 @@ for (;;)
       dsn_envid = string_copy_taint(var + 10, proto_mem);
 #ifndef COMPILE_UTILITY
     else if (Ustrncmp(p, "ebug_selector ", 14) == 0)
-      debug_selector = strtol(CS var + 15, NULL, 0);
+      debug_selector = strtoul(CS var + 15, NULL, 0);
     else if (Ustrncmp(p, "ebuglog_name ", 13) == 0)
       debug_logging_from_spool(var + 14);
 #endif
diff --git a/src/src/spool_out.c b/src/src/spool_out.c
index 91b776df2..243db106d 100644
--- a/src/src/spool_out.c
+++ b/src/src/spool_out.c
@@ -232,7 +232,7 @@ tree_walk(acl_var_m, &acl_var_write, fp);
 
 if (*debuglog_name)
   {
-  fprintf(fp, "-debug_selector 0x%x\n", debug_selector);
+  fprintf(fp, "-debug_selector 0x%lx\n", debug_selector);
   fprintf(fp, "-debuglog_name %s\n", debuglog_name);
   }
 

commit c052ac7dc3c71e083317d8bcd234fa32a0f964ab
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Jan 20 20:20:27 2026 +0000

    Debug: add "regex" channel

diff --git a/src/src/globals.c b/src/src/globals.c
index 90906e923..c4f5064e7 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -655,9 +655,10 @@ uschar *dccifd_options         = US"header";
 int     debug_fd               = -1;
 FILE   *debug_file             = NULL;
 int     debug_notall[]         = {
+  Di_macro,
   Di_memory,
   Di_noutf8,
-  Di_macro,
+  Di_regex,
   -1
 };
 bit_table debug_options[]      = { /* must be in alphabetical order and use
@@ -686,6 +687,7 @@ bit_table debug_options[]      = { /* must be in alphabetical order and use
   BIT_TABLE(D, process_info),
   BIT_TABLE(D, queue_run),
   BIT_TABLE(D, receive),
+  BIT_TABLE(D, regex),
   BIT_TABLE(D, resolver),
   BIT_TABLE(D, retry),
   BIT_TABLE(D, rewrite),
diff --git a/src/src/macros.h b/src/src/macros.h
index a1a9cd6c1..8ab066fac 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -412,8 +412,10 @@ enum {
   DEBUG_BIT(verify),		/* 31 */
 #if EXIM_ULONG_BITS > 32
   DEBUG_BIT(macro),		/* 33 */
+  DEBUG_BIT(regex),
 #else
   DEBUG_Z_BIT(macro),
+  DEBUG_Z_BIT(regex),
 #endif
 };
 
diff --git a/src/src/regex_cache.c b/src/src/regex_cache.c
index 713da90d5..ab26b8a96 100644
--- a/src/src/regex_cache.c
+++ b/src/src/regex_cache.c
@@ -52,7 +52,7 @@ int rlen = sizeof(re_req) + klen;
 re_req * req;
 int fd, old_pool = store_pool;
 
-DEBUG(D_expand|D_lists)
+DEBUG(D_regex)
   debug_printf_indent("sending RE '%s' to daemon\n", key);
 
 store_pool = POOL_MAIN;
@@ -68,11 +68,11 @@ if ((fd = socket(AF_UNIX, SOCK_DGRAM, 0)) >= 0)
   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(D_regex)
       debug_printf("%s: sendto %s\n", __FUNCTION__, strerror(errno));
   close(fd);
   }
-else DEBUG(D_queue_run) debug_printf(" socket: %s\n", strerror(errno));
+else DEBUG(D_regex) debug_printf(" socket: %s\n", strerror(errno));
 }
 
 
@@ -81,7 +81,7 @@ 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(D_regex)
   debug_printf_indent("compiled %sRE '%s' %sfound in local cache\n",
 		      caseless ? "caseless " : "", key, node ? "" : "not ");
 
@@ -99,8 +99,8 @@ 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(D_regex) debug_printf_indent("duplicate key!\n"); }
+else DEBUG(D_regex)
   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
@@ -204,7 +204,7 @@ if (  flags & MCS_CACHEABLE
    && (yield = regex_from_cache(key, caseless)))
   return yield;
 
-DEBUG(D_expand|D_lists) debug_printf_indent("compiling %sRE '%s'\n",
+DEBUG(D_regex) debug_printf_indent("compiling %sRE '%s'\n",
 				caseless ? "caseless " : "", pattern);
 
 store_pool = POOL_PERM;

commit 2d4c73da7de2c0bfc47d5321f158f6620c008b7a
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Wed Jan 21 13:21:27 2026 +0000

    More coverage in "-bV" datatype size diagnostics

diff --git a/src/src/exim.c b/src/src/exim.c
index 6924b4b14..489aa3bd6 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -1277,7 +1277,9 @@ if (fixed_never_users[0] > 0)
 
 g = string_fmt_append(g, "Configure owner: %d:%d\n", config_uid, config_gid);
 
-g = string_fmt_append(g, "Size of off_t: " SIZE_T_FMT "\n", sizeof(off_t));
+g = string_fmt_append(g, "Size of off_t:" SIZE_T_FMT
+      " ptr:" SIZE_T_FMT " long:" SIZE_T_FMT " int:" SIZE_T_FMT "\n",
+      sizeof(off_t), sizeof(void *), sizeof(long), sizeof(int));
 
 /* Everything else is details which are only worth reporting when debugging.
 Perhaps the tls_version_report should move into this too. */
diff --git a/src/src/globals.c b/src/src/globals.c
index c4f5064e7..142cdde5e 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -1453,8 +1453,8 @@ uschar *uucp_from_sender       = US"$1";
 
 uschar *verify_mode	       = NULL;
 uschar *version_copyright      =
- US"Copyright (c) University of Cambridge, 1995 - 2018\n"
-   "(c) The Exim Maintainers and contributors in ACKNOWLEDGMENTS file, 2007 - 2025";
+ US"Copyright (c) The Exim Maintainers and contributors in ACKNOWLEDGMENTS file, 2007 - 2026\n"
+ "Copyright (c) University of Cambridge, 1995 - 2018\n";
 uschar *version_date           = US"?";
 uschar *version_cnumber        = US"????";
 uschar *version_string         = US"?";

commit 3ec1c65df8dd1cedb3f77666107a860000dd280b
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Jan 22 18:21:00 2026 +0000

    tidying

diff --git a/src/src/log.c b/src/src/log.c
index 0220b91bb..39ff04378 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -763,27 +763,22 @@ static void
 set_file_path(void)
 {
 int sep = ':';              /* Fixed separator - outside use */
-uschar *t;
-const uschar *tt = US LOG_FILE_PATH;
+const uschar * t, * tt = US LOG_FILE_PATH;
 while ((t = string_nextinlist(&tt, &sep, log_buffer, LOG_BUFFER_SIZE)))
-  {
-  if (Ustrcmp(t, "syslog") == 0 || t[0] == 0) continue;
-  file_path = string_copy(t);
-  break;
-  }
+  if (Ustrcmp(t, "syslog") != 0 && *t)
+    { file_path = string_copy(t); break; }
 }
 
 
 /* Close mainlog, unless we do not see a chance to open the file mainlog later
 again.  This will happen if we log from a transport process (which has dropped
 privs); something we traditionally avoid, but the introduction of taint-tracking
-and resulting detection of errors is makinng harder. */
+and resulting detection of errors is making harder. */
 
 void
 mainlog_close(void)
 {
-if (mainlogfd < 0
-   || !(geteuid() == 0 || geteuid() == exim_uid))
+if (mainlogfd < 0 || !(geteuid() == 0 || geteuid() == exim_uid))
   return;
 (void)close(mainlogfd);
 mainlogfd = -1;
@@ -852,8 +847,7 @@ log_vwrite(unsigned int selector, int flags, const char * format, va_list ap)
 {
 int paniclogfd;
 ssize_t written_len;
-gstring gs = { .size = LOG_BUFFER_SIZE-2, .ptr = 0, .s = log_buffer };
-gstring * g = &gs;
+gstring gs = { .size = LOG_BUFFER_SIZE-2 }, * g = &gs;
 
 /* If panic_recurseflag is set, we have failed to open the panic log. This is
 the ultimate disaster. First try to write the message to a debug file and/or
@@ -881,6 +875,7 @@ if (!log_buffer)
     fprintf(stderr, "exim: failed to get store for log buffer\n");
     exim_exit(EXIT_FAILURE);
     }
+gs.s = log_buffer;
 
 /* If we haven't already done so, inspect the setting of log_file_path to
 determine whether to log to files and/or to syslog. Bits in logging_mode

commit c33264393a1c70c92a1fccdfa0640988da26351e
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Jan 22 19:30:43 2026 +0000

    Debug: use defined-width type for bitmap

diff --git a/src/exim_monitor/em_main.c b/src/exim_monitor/em_main.c
index 143de870d..d85cefdba 100644
--- a/src/exim_monitor/em_main.c
+++ b/src/exim_monitor/em_main.c
@@ -168,7 +168,7 @@ Returns:    nothing
 */
 
 void
-log_write(unsigned int selector, int flags, const char *format, ...)
+log_write(bitmask_word_t selector, int flags, const char *format, ...)
 {
 va_list ap;
 va_start(ap, format);
@@ -179,7 +179,7 @@ va_end(ap);
 
 
 void
-log_write_die(unsigned int selector, int flags, const char *format, ...)
+log_write_die(bitmask_word_t selector, int flags, const char *format, ...)
 {
 va_list ap;
 va_start(ap, format);
diff --git a/src/src/buildconfig.c b/src/src/buildconfig.c
index 081d1041b..b365643bb 100644
--- a/src/src/buildconfig.c
+++ b/src/src/buildconfig.c
@@ -207,9 +207,6 @@ else
   fprintf(new, "#define SSIZE_T_FMT  \"%%d\"\n");
 #endif
 
-fprintf(new, "#define EXIM_ULONG_BITS	%u\n",
-		sizeof(unsigned long) == 8 ? 64 : 32);
-
 /* Now search the makefile for certain settings */
 
 if (!(base = fopen("Makefile", "rb")))
diff --git a/src/src/child.c b/src/src/child.c
index 05f87d856..de08095af 100644
--- a/src/src/child.c
+++ b/src/src/child.c
@@ -107,7 +107,7 @@ if (!minimal)
     {
     if (debug_selector != 0)
       {
-      argv[n++] = string_sprintf("-d=0x%lx", debug_selector);
+      argv[n++] = string_sprintf("-d=0x" PR_EXIM_BITMASK, debug_selector);
       if (debug_fd > 2)
 	{
 	int flags = fcntl(debug_fd, F_GETFD);
diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults
index ba9f50b01..e99b10c16 100644
--- a/src/src/config.h.defaults
+++ b/src/src/config.h.defaults
@@ -247,4 +247,11 @@ for EXIM_ARITH_MAX and _MIN in OS/oh.h-FOO */
 #define SC_EXIM_ARITH "%" SCNi64		/* scanf incl. 0x prefix */
 #define SC_EXIM_DEC   "%" SCNd64		/* scanf decimal */
 
+/* Word size for debug and logging channel bitmaps.
+Change as above (but expect debugging to become odd). */
+#define bitmask_word_t		uint64_t
+#define EXIM_BITMAP_WORD_BITS	64
+#define PR_EXIM_BITMASK		"%" PRIx64
+#define SC_EXIM_BITMASK		"%" SCNx64
+
 /* End of config.h.defaults */
diff --git a/src/src/daemon.c b/src/src/daemon.c
index 607913f6c..bce9bb9f4 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -188,7 +188,7 @@ pid_t pid;
 union sockaddr_46 interface_sockaddr;
 EXIM_SOCKLEN_T ifsize = sizeof(interface_sockaddr);
 int max_for_this_host = 0;
-unsigned long save_log_selector = *log_selector;
+bitmask_word_t save_log_selector = *log_selector;
 gstring * whofrom;
 
 rmark reset_point = store_mark();
@@ -365,7 +365,7 @@ if (pid == 0)
   {
   int queue_only_reason = 0;
   int old_pool = store_pool;
-  unsigned long save_debug_selector = debug_selector;
+  bitmask_word_t save_debug_selector = debug_selector;
   BOOL local_queue_only,  session_local_queue_only;
 #ifdef SA_NOCLDWAIT
   struct sigaction act;
diff --git a/src/src/environment.c b/src/src/environment.c
index 4b8041f9f..58af6b6c3 100644
--- a/src/src/environment.c
+++ b/src/src/environment.c
@@ -42,7 +42,7 @@ if (!keep_environment || !*keep_environment)
 else if (Ustrcmp(keep_environment, "*") != 0)
   {
   rmark reset_point = store_mark();
-  unsigned long deb = debug_selector;
+  bitmask_word_t deb = debug_selector;
   BOOL hc = host_checking;
   debug_selector = 0;			/* quieten this clearout */
   host_checking = FALSE;
diff --git a/src/src/exim.c b/src/src/exim.c
index 489aa3bd6..5a756cc4d 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -3982,8 +3982,9 @@ if (debug_selector != 0)
   testharness_pause_ms(100);   /* lets caller finish */
   if (debug_selector != D_v)    /* -v only doesn't show this */
     {
-    debug_printf("Exim version %s uid=%ld gid=%ld pid=%d D=%x\n",
-      version_string, (long int)real_uid, (long int)real_gid, (int)getpid(),
+    debug_printf("Exim version %s uid=%ld gid=%ld pid=" PID_T_FMT
+      " D=0x" PR_EXIM_BITMASK "\n",
+      version_string, (long int)real_uid, (long int)real_gid, getpid(),
       debug_selector);
     if (!version_printed)
       show_whats_supported(FALSE);
@@ -4290,7 +4291,7 @@ DEBUG(D_any)
   debug_printf("configuration file is %s\n", config_main_filename);
   debug_printf("log selectors =");
   for (int i = 0; i < log_selector_size; i++)
-    debug_printf(" %08x", log_selector[i]);
+    debug_printf(" 0x" PR_EXIM_BITMASK, log_selector[i]);
   debug_printf("\n");
   }
 
diff --git a/src/src/exim_dbmbuild.c b/src/src/exim_dbmbuild.c
index c6c884125..1ffedfd95 100644
--- a/src/src/exim_dbmbuild.c
+++ b/src/src/exim_dbmbuild.c
@@ -69,7 +69,7 @@ string_format_trc(uschar * buf, int len, const uschar * func, unsigned line,
   const char * fmt, ...)
 { return FALSE; }
 void
-log_write(unsigned int selector, int flags, const char *format, ...)
+log_write(bitmask_word_t selector, int flags, const char *format, ...)
 { }
 const uschar * parse_find_address_end_gen(const uschar * s, BOOL b)
 {return NULL; }
@@ -77,7 +77,7 @@ const uschar * parse_find_address_end_gen(const uschar * s, BOOL b)
 
 
 struct global_flags	f;
-unsigned long		log_selector[1];
+bitmask_word_t		log_selector[1];
 uschar *		queue_name;
 BOOL			split_spool_directory;
 
@@ -98,7 +98,7 @@ const uschar *hex_digits = CUS"0123456789abcdef";
 *   Debug output   *
 *******************/
 
-unsigned long debug_selector = 0;	/* set -1 for debugging */
+bitmask_word_t debug_selector = 0;	/* set -1 for debugging */
 
 void
 debug_printf(const char * fmt, ...)
diff --git a/src/src/exim_dbutil.c b/src/src/exim_dbutil.c
index 2955861fa..04b3023b7 100644
--- a/src/src/exim_dbutil.c
+++ b/src/src/exim_dbutil.c
@@ -65,7 +65,7 @@ const uschar * parse_find_address_end_gen(const uschar * s, BOOL b)
 {return NULL; }
 
 struct global_flags	f;
-unsigned long		log_selector[1];
+bitmask_word_t		log_selector[1];
 uschar *		queue_name;
 BOOL			split_spool_directory;
 
@@ -113,7 +113,7 @@ exit(EXIT_FAILURE);
 *   Debug output   *
 *******************/
 
-unsigned long debug_selector = 0;	/* set -1 for debugging */
+bitmask_word_t debug_selector = 0;	/* set -1 for debugging */
 
 void
 debug_printf(const char * fmt, ...)
@@ -195,7 +195,7 @@ Returns:    nothing
 */
 
 void
-log_write(unsigned int selector, int flags, const char *format, ...)
+log_write(bitmask_word_t selector, int flags, const char *format, ...)
 {
 va_list ap;
 va_start(ap, format);
@@ -205,7 +205,7 @@ va_end(ap);
 }
 
 void
-log_write_die(unsigned int selector, int flags, const char *format, ...)
+log_write_die(bitmask_word_t selector, int flags, const char *format, ...)
 {
 va_list ap;
 va_start(ap, format);
diff --git a/src/src/expand.c b/src/src/expand.c
index 3c68ab2e9..5e46f7cde 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -9135,7 +9135,7 @@ for (int i = 1; i < argc; i++)
     argv[i]++;
     }
   if (isdigit(argv[i][0]))
-    debug_selector = Ustrtol(argv[i], NULL, 0);
+    debug_selector = Ustrtoul(argv[i], NULL, 0);
   else
     if (Ustrspn(argv[i], "abcdefghijklmnopqrtsuvwxyz0123456789-.:/") ==
         Ustrlen(argv[i]))
diff --git a/src/src/functions.h b/src/src/functions.h
index 677972e01..0981f126b 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -302,7 +302,7 @@ extern const uschar *local_part_quote(const uschar *);
 extern void    log_close_all(void);
 extern int     log_open_as_exim(const uschar * const);
 extern gstring *log_portnum(gstring *, int);
-extern void    log_write_die(unsigned, int, const char * format, ...)
+extern void    log_write_die(bitmask_word_t, int, const char * format, ...)
 		PRINTF_FUNCTION(3,4) NORETURN;
 
 extern const lookup_info * lookup_with_acq_num(unsigned);
diff --git a/src/src/globals.c b/src/src/globals.c
index 142cdde5e..58dc8bffd 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -702,7 +702,7 @@ int      debug_options_count	= nelem(debug_options);
 uschar   debuglog_name[LOG_NAME_SIZE] = {0};
 unsigned debug_pretrigger_bsize	= 0;
 uschar * debug_pretrigger_buf	= NULL;
-unsigned long debug_selector	= 0;
+bitmask_word_t debug_selector	= 0;
 BOOL	 debug_startup		= FALSE;
 
 int     delay_warning[DELAY_WARNING_SIZE] = { DELAY_WARNING_SIZE, 1, 24*60*60 };
@@ -1017,7 +1017,7 @@ int     log_options_count      = nelem(log_options);
 
 const uschar *log_ports	       = NULL;
 int     log_reject_target      = 0;
-unsigned long log_selector[log_selector_size]; /* initialized in main() */
+bitmask_word_t log_selector[log_selector_size]; /* initialized in main() */
 uschar *log_selector_string    = NULL;
 FILE   *log_stderr             = NULL;
 uschar *login_sender_address   = NULL;
diff --git a/src/src/globals.h b/src/src/globals.h
index 5402fb59e..dca2d0b17 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -673,7 +673,7 @@ extern int     log_notall[];           /* Log options excluded from +all */
 extern bit_table log_options[];        /* Table of options */
 extern int     log_options_count;      /* Size of table */
 extern int     log_reject_target;      /* Target log for ACL rejections */
-extern unsigned long log_selector[];   /* Bit map of logging options */
+extern bitmask_word_t log_selector[];  /* Bit map of logging options */
 extern uschar *log_selector_string;    /* As supplied in the config */
 extern FILE   *log_stderr;             /* Copy of stderr for log use, or NULL */
 extern BOOL    log_timezone;           /* TRUE to include the timezone in log lines */
diff --git a/src/src/local_scan.h b/src/src/local_scan.h
index 6e63672eb..ad2601c38 100644
--- a/src/src/local_scan.h
+++ b/src/src/local_scan.h
@@ -29,6 +29,7 @@ store.c
 settings, and the store functions. */
 
 #include <stdarg.h>
+#include <stdint.h>
 #include <sys/types.h>
 #include "config.h"
 #include "mytypes.h"
@@ -165,7 +166,7 @@ typedef struct recipient_item {
 
 /* Global variables that are documented as visible in the function. */
 
-extern unsigned long debug_selector;   /* Debugging bits */
+extern bitmask_word_t debug_selector;   /* Debugging bits */
 
 extern int     body_linecount;         /* Line count in body */
 extern int     body_zerocount;         /* Binary zero count in body */
@@ -211,7 +212,7 @@ extern void    header_add_at_position(BOOL, uschar *, BOOL, int, const char *, .
 extern void    header_remove(int, const uschar *);
 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 void    log_write(bitmask_word_t, int, const char *format, ...) PRINTF_FUNCTION(3,4);
 extern int     lss_b64decode(uschar *, uschar **);
 extern uschar *lss_b64encode(uschar *, int);
 extern int     lss_match_domain(uschar *, uschar *);
diff --git a/src/src/log.c b/src/src/log.c
index 39ff04378..46d8873da 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -843,7 +843,7 @@ Returns:    nothing
 */
 
 static void
-log_vwrite(unsigned int selector, int flags, const char * format, va_list ap)
+log_vwrite(bitmask_word_t selector, int flags, const char * format, va_list ap)
 {
 int paniclogfd;
 ssize_t written_len;
@@ -1307,7 +1307,7 @@ if (flags & LOG_PANIC)
 /* The public interface */
 
 void
-log_write(unsigned int selector, int flags, const char * format, ...)
+log_write(bitmask_word_t selector, int flags, const char * format, ...)
 {
 va_list ap;
 va_start(ap, format);
@@ -1320,7 +1320,7 @@ We have this as a wrapper so that we can mark it as never returning,
 for the benefit of static analysers. */
 
 void
-log_write_die(unsigned int selector, int flags, const char * format, ...)
+log_write_die(bitmask_word_t selector, int flags, const char * format, ...)
 {
 va_list ap;
 va_start(ap, format);
@@ -1361,14 +1361,14 @@ Arguments:
 */
 
 void
-bits_clear(unsigned long * selector, size_t selsize, int * bits)
+bits_clear(bitmask_word_t * selector, size_t selsize, int * bits)
 {
 for(; *bits != -1; ++bits)
   BIT_CLEAR(selector, selsize, *bits);
 }
 
 void
-bits_set(unsigned long * selector, size_t selsize, int * bits)
+bits_set(bitmask_word_t * selector, size_t selsize, int * bits)
 {
 for(; *bits != -1; ++bits)
   BIT_SET(selector, selsize, *bits);
@@ -1409,7 +1409,7 @@ Returns:         nothing on success - bomb out on failure
 */
 
 void
-decode_bits(unsigned long * selector, size_t selsize, int * notall,
+decode_bits(bitmask_word_t * selector, size_t selsize, int * notall,
   const uschar * string, bit_table * options, int count, uschar * which,
   int flags)
 {
@@ -1419,12 +1419,14 @@ if (!string) return;
 
 if (*string == '=')
   {
-  char *end;    /* Not uschar */
+  int n;
   memset(selector, 0, sizeof(*selector)*selsize);
-  *selector = strtoul(CCS string+1, &end, 0);
-  if (!*end) return;
-  errmsg = string_sprintf("malformed numeric %s_selector setting: %s", which,
-    string);
+  if (  sscanf(CCS string+1, SC_EXIM_BITMASK "%n", selector, &n) == 1
+     && !string[1+n])
+    return;
+
+  errmsg = string_sprintf("malformed numeric %s_selector setting: %q (n %d, fmt %q)", which,
+    string, n, SC_EXIM_BITMASK);
   goto ERROR_RETURN;
   }
 
@@ -1487,7 +1489,7 @@ else for(;;)
   if (start >= end)
     {
     errmsg = string_sprintf("unknown %s_selector setting: %c%.*s", which,
-      adding? '+' : '-', len, s);
+      adding ? '+' : '-', len, s);
     goto ERROR_RETURN;
     }
   }    /* Loop for selector names */
diff --git a/src/src/macros.h b/src/src/macros.h
index 8ab066fac..ea5ca9277 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -329,10 +329,10 @@ for having to swallow the rest of an SMTP message is whether the value is
 
 /* Assume words are at least 32 bits wide. Tiny waste of space on 64 bit
 platforms, but this ensures bit vectors always work the same way. */
-#ifdef EXIM_ULONG_BITS
-  #define BITWORDSIZE EXIM_ULONG_BITS
+#ifdef EXIM_BITMAP_WORD_BITS
+# define BITWORDSIZE EXIM_BITMAP_WORD_BITS
 #else
-  #define BITWORDSIZE 64
+# define BITWORDSIZE 64
 #endif
 
 /* This macro is for single-word bit vectors: the debug selector,
@@ -410,7 +410,7 @@ enum {
   DEBUG_BIT(transport),
   DEBUG_BIT(uid),
   DEBUG_BIT(verify),		/* 31 */
-#if EXIM_ULONG_BITS > 32
+#if BITWORDSIZE > 32
   DEBUG_BIT(macro),		/* 33 */
   DEBUG_BIT(regex),
 #else
diff --git a/src/src/spool_in.c b/src/src/spool_in.c
index 35fa5089b..9231a302e 100644
--- a/src/src/spool_in.c
+++ b/src/src/spool_in.c
@@ -620,7 +620,7 @@ for (;;)
       dsn_envid = string_copy_taint(var + 10, proto_mem);
 #ifndef COMPILE_UTILITY
     else if (Ustrncmp(p, "ebug_selector ", 14) == 0)
-      debug_selector = strtoul(CS var + 15, NULL, 0);
+      sscanf(CS var + 15, SC_EXIM_BITMASK, &debug_selector);
     else if (Ustrncmp(p, "ebuglog_name ", 13) == 0)
       debug_logging_from_spool(var + 14);
 #endif
diff --git a/src/src/spool_out.c b/src/src/spool_out.c
index 243db106d..016f7cdd4 100644
--- a/src/src/spool_out.c
+++ b/src/src/spool_out.c
@@ -232,7 +232,7 @@ tree_walk(acl_var_m, &acl_var_write, fp);
 
 if (*debuglog_name)
   {
-  fprintf(fp, "-debug_selector 0x%lx\n", debug_selector);
+  fprintf(fp, "-debug_selector 0x" PR_EXIM_BITMASK "\n", debug_selector);
   fprintf(fp, "-debuglog_name %s\n", debuglog_name);
   }
 

commit d61563f31ad33c93eae8e1c448475783c437f6e8
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Jan 22 23:13:10 2026 +0000

    Tidying: pid string formatting

diff --git a/src/OS/unsupported/os.c-cygwin b/src/OS/unsupported/os.c-cygwin
index 56085b857..d9e54d6a8 100644
--- a/src/OS/unsupported/os.c-cygwin
+++ b/src/OS/unsupported/os.c-cygwin
@@ -55,7 +55,7 @@ int cygwin_setuid(uid_t uid )
   if (fakesetugid == 0) { 
     res = setuid(uid);
     if (cygwin_debug)
-      fprintf(stderr, "setuid %u %u %d pid: %d\n",
+      fprintf(stderr, "setuid %u %u %d pid: " PID_T_FMT "\n",
               uid, getuid(),res, getpid());
   }
   return res;
@@ -68,7 +68,7 @@ int cygwin_setgid(gid_t gid )
   if (fakesetugid == 0) { 
     res = setgid(gid);
     if (cygwin_debug)
-      fprintf(stderr, "setgid %u %u %d pid: %d\n",
+      fprintf(stderr, "setgid %u %u %d pid: " PID_T_FMT "\n",
               gid, getgid(), res, getpid());
   }
   return res;
diff --git a/src/src/auths/auth-spa.c b/src/src/auths/auth-spa.c
index ee69330dd..db62c6859 100644
--- a/src/src/auths/auth-spa.c
+++ b/src/src/auths/auth-spa.c
@@ -1407,7 +1407,7 @@ spa_build_auth_challenge(SPAAuthRequest * request, SPAAuthChallenge * challenge)
 {
 char chalstr[8];
 int i;
-int p = (int)getpid();
+pid_t p = getpid();
 int random_seed = (int)time(NULL) ^ ((p << 16) | p);
 
 /* Ensure challenge data is cleared, in case it isn't all used. This
diff --git a/src/src/auths/cram_md5.c b/src/src/auths/cram_md5.c
index 44b05465c..1532e2af6 100644
--- a/src/src/auths/cram_md5.c
+++ b/src/src/auths/cram_md5.c
@@ -173,8 +173,8 @@ int
 auth_cram_md5_server(auth_instance * ablock, uschar * data)
 {
 auth_cram_md5_options_block * ob = ablock->drinst.options_block;
-uschar * challenge = string_sprintf("<%d.%ld@%s>", getpid(),
-    (long int) time(NULL), primary_hostname);
+uschar * challenge = string_sprintf("<" PID_T_FMT ".%ld@%s>",
+    getpid(), (long int) time(NULL), primary_hostname);
 uschar * clear, * secret;
 uschar digest[16];
 int i, rc, len;
diff --git a/src/src/auths/dovecot.c b/src/src/auths/dovecot.c
index c9fdc0ea9..73322a6cd 100644
--- a/src/src/auths/dovecot.c
+++ b/src/src/auths/dovecot.c
@@ -441,7 +441,7 @@ Subsequently, the command was modified to add "secured" and "valid-client-
 cert" when relevant.
 ****************************************************************************/
 
-auth_command = string_sprintf("CPID\t%d\n"
+auth_command = string_sprintf("CPID\t" PID_T_FMT "\n"
        "AUTH\t%d\t%s\tservice=smtp\t%srip=%s\tlip=%s\tnologin\tresp=%s\n",
        getpid(), crequid,
        ablock->public_name, auth_extra_data, sender_host_address,
diff --git a/src/src/daemon.c b/src/src/daemon.c
index bce9bb9f4..d3e84a567 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -493,8 +493,8 @@ if (pid == 0)
   debug_selector = save_debug_selector;
 
   DEBUG(D_any)
-    debug_printf("Process %d is handling incoming connection from %s\n",
-      (int)getpid(), sender_fullhost);
+    debug_printf("Process " PID_T_FMT " is handling incoming connection"
+      " from %s\n", getpid(), sender_fullhost);
 
   /* Now disable debugging permanently if it's required only for the daemon
   process. */
@@ -538,7 +538,7 @@ if (pid == 0)
     reset_point = store_mark();   /* Save current store high water point */
 
     DEBUG(D_any)
-      debug_printf("Process %d is ready for new message\n", (int)getpid());
+      debug_printf("Process " PID_T_FMT " is ready for new message\n", getpid());
 
     /* Smtp_setup_msg() returns 0 on QUIT or if the call is from an
     unacceptable host or if an ACL "drop" command was triggered, -1 on
@@ -971,7 +971,7 @@ static BOOL
 operate_on_pid_file(const enum pid_op operation, const pid_t pid)
 {
 char pid_line[sizeof(int) * 3 + 2];
-const int pid_len = snprintf(pid_line, sizeof(pid_line), "%ld\n", (long)pid);
+const int pid_len = snprintf(pid_line, sizeof(pid_line), PID_T_FMT "\n", pid);
 BOOL lines_match = FALSE;
 uschar * path, * base, * dir;
 
@@ -1144,9 +1144,10 @@ 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());
+  + snprintf(sup->sun_path+1, sizeof(sup->sun_path)-1,
+	      "exim_" PID_T_FMT, getpid());
 #else
-*sname = string_sprintf("%s/p_%d", spool_directory, getpid());
+*sname = string_sprintf("%s/p_" PID_T_FMT, spool_directory, getpid());
 return offsetof(struct sockaddr_un, sun_path)
   + snprintf(sup->sun_path, sizeof(sup->sun_path), "%s", CS *sname);
 #endif
@@ -2373,7 +2374,9 @@ if (f.running_in_test_harness || write_pid)
      || real_uid == root_uid
      || (real_uid == exim_uid && !override_pid_file_path)) ? PID_WRITE : PID_CHECK;
   if (!operate_on_pid_file(operation, getpid()))
-    DEBUG(D_any) debug_printf("%s pid file %s: %s\n", (operation == PID_WRITE) ? "write" : "check", pid_file_path, strerror(errno));
+    DEBUG(D_any) debug_printf("%s pid file %s: %s\n",
+			      operation == PID_WRITE ? "write" : "check",
+			      pid_file_path, strerror(errno));
   }
 
 /* Set up the handler for SIGHUP, which causes a restart of the daemon. */
@@ -2437,8 +2440,8 @@ if (f.inetd_wait_mode)
   else
     sprintf(CS p, "with no wait timeout");
 
-  log_write(0, LOG_MAIN,
-    "exim %s daemon started: pid=%ld, launched with listening socket, %s",
+  log_write(0, LOG_MAIN, "exim %s daemon started: pid=" PID_T_FMT
+			  ", launched with listening socket, %s",
     version_string, getpid(), big_buffer);
   set_process_info("daemon(%s): pre-listening socket", version_string);
 
@@ -2544,7 +2547,7 @@ else if (f.daemon_listen)
     }
 
   log_write(0, LOG_MAIN,
-    "exim %s daemon started: pid=%ld, %s, listening for %s",
+    "exim %s daemon started: pid=" PID_T_FMT ", %s, listening for %s",
     version_string, getpid(), qinfo, big_buffer);
   set_process_info("daemon(%s): %s, listening for %s",
     version_string, qinfo, big_buffer);
@@ -2554,7 +2557,7 @@ else	/* no listening sockets, only queue-runs */
   {
   const uschar * s = describe_queue_runners();
   log_write(0, LOG_MAIN,
-    "exim %s daemon started: pid=%ld, %s, not listening for SMTP",
+    "exim %s daemon started: pid=" PID_T_FMT ", %s, not listening for SMTP",
     version_string, getpid(), s);
   set_process_info("daemon(%s): %s, not listening", version_string, s);
   }
@@ -2859,7 +2862,7 @@ for (;;)
 
   if (sighup_seen)
     {
-    log_write(0, LOG_MAIN, "pid %ld: SIGHUP received: re-exec daemon",
+    log_write(0, LOG_MAIN, "pid " PID_T_FMT ": SIGHUP received: re-exec daemon",
       getpid());
     close_daemon_sockets(daemon_notifier_fd, fd_polls, listen_socket_count);
     unlink_notifier_socket();
@@ -2868,7 +2871,7 @@ for (;;)
     sighup_argv[0] = exim_path;
     exim_nullstd();
     execv(CS exim_path, (char *const *)sighup_argv);
-    log_write_die(0, LOG_MAIN, "pid %ld: exec of %s failed: %s",
+    log_write_die(0, LOG_MAIN, "pid " PID_T_FMT ": exec of %s failed: %s",
       getpid(), exim_path, strerror(errno));
     /*NOTREACHED*/
     }
diff --git a/src/src/deliver.c b/src/src/deliver.c
index a984ee21e..44e163fa4 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -2240,7 +2240,7 @@ if (  !shadowing
 
   addr->return_filename =
     spool_fname(US"msglog", message_subdir, message_id,
-      string_sprintf("-%ld-%d", (long)getpid(), return_count++));
+      string_sprintf("-" PID_T_FMT "-%d", getpid(), return_count++));
 
   if ((addr->return_file = open_msglog_file(addr->return_filename, 0400, &error)) < 0)
     {
@@ -5919,9 +5919,10 @@ if (!(bounce_recipient = addr_failed->prop.errors_address))
 /* Make a subprocess to send a message, using its stdin */
 
 if ((pid = child_open_exim(&fd, US"bounce-message")) < 0)
-  log_write_die(0, LOG_MAIN, "Process %ld (parent %ld) failed to "
+  log_write_die(0, LOG_MAIN,
+    "Process " PID_T_FMT " (parent " PID_T_FMT ") failed to "
     "create child process to send failure message: %s",
-    (long)getpid(), (long)getppid(), strerror(errno));
+    getpid(), getppid(), strerror(errno));
 
 /* Creation of child succeeded */
 
@@ -6577,9 +6578,10 @@ if (addr_senddsn)
 
   if (pid < 0)  /* Creation of child failed */
     {
-    log_write_die(0, LOG_MAIN, "Process %ld (parent %ld) failed to "
+    log_write_die(0, LOG_MAIN,
+      "Process " PID_T_FMT " (parent " PID_T_FMT ") failed to "
       "create child process to send success-dsn message: %s",
-      (long)getpid(), (long)getppid(), strerror(errno));
+      getpid(), getppid(), strerror(errno));
 
     DEBUG(D_deliver) debug_printf("DSN: child_open_exim failed\n");
     }
diff --git a/src/src/exim.c b/src/src/exim.c
index 5a756cc4d..a87219920 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -224,7 +224,7 @@ int len;
 uschar * s;
 va_list ap;
 
-g = string_fmt_append(&gs, "%5d ", (int)getpid());
+g = string_fmt_append(&gs, PID_T_FMT " ", getpid());
 len = gstring_length(g);
 va_start(ap, format);
 if (!string_vformat(g, 0, format, ap))
@@ -803,8 +803,8 @@ DEBUG(D_uid)
   {
   int group_count, save_errno;
   gid_t group_list[EXIM_GROUPLIST_SIZE];
-  debug_printf("changed uid/gid: %s\n  uid=%ld gid=%ld pid=%ld\n", msg,
-    (long int)geteuid(), (long int)getegid(), (long int)getpid());
+  debug_printf("changed uid/gid: %s\n  uid=%ld gid=%ld pid=" PID_T_FMT "\n",
+    msg, (long int)geteuid(), (long int)getegid(), getpid());
   group_count = getgroups(nelem(group_list), group_list);
   save_errno = errno;
   debug_printf("  auxiliary group list:");
@@ -840,9 +840,9 @@ smtp_fflush(SFF_NO_UNCORK);
 search_tidyup();
 store_exit();
 DEBUG(D_any)
-  debug_printf(">>>>>>>>>>>>>>>> Exim pid=%d (%s) terminating with rc=%d "
+  debug_printf(">>>>>>>>>>>>>>>> Exim pid=" PID_T_FMT " (%s) terminating with rc=%d "
     ">>>>>>>>>>>>>>>>\n",
-    (int)getpid(), process_purpose, rc);
+    getpid(), process_purpose, rc);
 exit(rc);
 }
 
@@ -852,9 +852,9 @@ exim_underbar_exit(int rc)
 {
 store_exit();
 DEBUG(D_any)
-  debug_printf(">>>>>>>>>>>>>>>> Exim pid=%d (%s) terminating with rc=%d "
+  debug_printf(">>>>>>>>>>>>>>>> Exim pid=" PID_T_FMT " (%s) terminating with rc=%d "
     ">>>>>>>>>>>>>>>>\n",
-    (int)getpid(), process_purpose, rc);
+    getpid(), process_purpose, rc);
 _exit(rc);
 }
 
diff --git a/src/src/exim_lock.c b/src/src/exim_lock.c
index f42da23a8..1797fa98b 100644
--- a/src/src/exim_lock.c
+++ b/src/src/exim_lock.c
@@ -305,7 +305,7 @@ if (use_lockfile)
 
   /* Presumably, this must match appendfile.c */
   sprintf(hitchname, "%s.%s.%08x.%08x", lockname, primary_hostname,
-    (unsigned int)now, (unsigned int)getpid());
+    (unsigned int)now, (unsigned long)getpid());
 
   if (verbose)
     printf("exim_lock: lockname =  %s\n           hitchname = %s\n", lockname,
diff --git a/src/src/expand.c b/src/src/expand.c
index 5e46f7cde..097e4aa5e 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -2004,7 +2004,7 @@ switch (vp->type)
     return (s = *((uschar **)(val))) ? s : US"";
 
   case vtype_pid:
-    sprintf(CS var_buffer, "%d", (int)getpid()); /* pid */
+    sprintf(CS var_buffer, PID_T_FMT, getpid()); /* pid */
     return var_buffer;
 
   case vtype_load_avg:
diff --git a/src/src/functions.h b/src/src/functions.h
index 0981f126b..916c1a329 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -1311,8 +1311,8 @@ report_time_since(const struct timeval * t0, const uschar * where)
 # ifdef MEASURE_TIMING
 struct timeval diff;
 timesince(&diff, t0);
-fprintf(stderr, "%d %s:\t%ld.%06ld\n",
-       (uint)getpid(), where, (long)diff.tv_sec, (long)diff.tv_usec);
+fprintf(stderr, PID_T_FMT " %s:\t%ld.%06ld\n",
+       getpid(), where, (long)diff.tv_sec, (long)diff.tv_usec);
 # endif
 }
 
@@ -1405,7 +1405,7 @@ the generator for it at any time. */
 static inline void
 set_connection_id(void)
 {
-connection_id = string_sprintf("%lu", (u_long)getpid());
+connection_id = string_sprintf(PID_T_FMT, getpid());
 }
 
 
diff --git a/src/src/host.c b/src/src/host.c
index 130334793..1bafda996 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -94,7 +94,7 @@ if (random_seed == 0)
     random_seed = 42;
   else
     {
-    int p = (int)getpid();
+    pid_t p = getpid();
     random_seed = (int)time(NULL) ^ ((p << 16) | p);
     }
 random_seed = 1103515245 * random_seed + 12345;
diff --git a/src/src/log.c b/src/src/log.c
index 46d8873da..e4fd3aa7f 100644
--- a/src/src/log.c
+++ b/src/src/log.c
@@ -1023,7 +1023,7 @@ string_fmt_append_noextend(&gs, "%s ", tod_stamp(tod_log));
 if (LOGGING(pid))
   {
   if (!syslog_pid) pid_position[0] = gstring_length(g);	/* remember begin … */
-  string_fmt_append_noextend(g, "[%ld] ", (long)getpid());
+  string_fmt_append_noextend(g, "[" PID_T_FMT "] ", getpid());
   if (!syslog_pid) pid_position[1] = gstring_length(g);	/*  … and end+1 of the PID */
   }
 
diff --git a/src/src/miscmods/dkim_transport.c b/src/src/miscmods/dkim_transport.c
index 5bce81ad1..1e9a1153c 100644
--- a/src/src/miscmods/dkim_transport.c
+++ b/src/src/miscmods/dkim_transport.c
@@ -264,7 +264,7 @@ off_t k_file_size;
 const uschar * errstr;
 
 dkim_spool_name = spool_fname(US"input", message_subdir, message_id,
-		    string_sprintf("-%d-K", (int)getpid()));
+		    string_sprintf("-" PID_T_FMT "-K", getpid()));
 
 DEBUG(D_transport) debug_printf("dkim signing via file %s\n", dkim_spool_name);
 
diff --git a/src/src/transports/appendfile.c b/src/src/transports/appendfile.c
index cad14b889..59af24ced 100644
--- a/src/src/transports/appendfile.c
+++ b/src/src/transports/appendfile.c
@@ -1564,7 +1564,7 @@ if (!isdirectory)
     /* cf. exim_lock.c */
     lockname = string_sprintf("%s.lock", filename);
     hitchname = string_sprintf( "%s.%s.%08x.%08x", lockname, primary_hostname,
-      (unsigned int)(time(NULL)), (unsigned int)getpid());
+      (unsigned int)(time(NULL)), (unsigned long)getpid());
 
     DEBUG(D_transport) debug_printf("lock name: %s\nhitch name: %s\n", lockname,
       hitchname);
@@ -2419,8 +2419,8 @@ else
     {
     DEBUG(D_transport)
       debug_printf("delivering to new file in %s\n", path);
-    filename = dataname =
-      string_sprintf("%s/temp.%d.%s", path, (int)getpid(), primary_hostname);
+    filename = dataname = string_sprintf("%s/temp." PID_T_FMT ".%s",
+					path, getpid(), primary_hostname);
     fd = Uopen(filename, O_WRONLY|O_CREAT, mode);
     if (fd < 0 &&                                 /* failed to open, and */
         (errno != ENOENT ||                       /* either not non-exist */
diff --git a/src/src/transports/tf_maildir.c b/src/src/transports/tf_maildir.c
index 60716f1a8..fdbe01e71 100644
--- a/src/src/transports/tf_maildir.c
+++ b/src/src/transports/tf_maildir.c
@@ -527,8 +527,8 @@ else
     FALSE);
 
   (void)gettimeofday(&tv, NULL);
-  tempname = string_sprintf("%s/tmp/" TIME_T_FMT ".H%luP%lu.%s",
-    path, tv.tv_sec, tv.tv_usec, (long unsigned) getpid(), primary_hostname);
+  tempname = string_sprintf("%s/tmp/" TIME_T_FMT ".H%luP" PID_T_FMT ".%s",
+    path, tv.tv_sec, tv.tv_usec, getpid(), primary_hostname);
 
   fd = Uopen(tempname, O_RDWR|O_CREAT|O_EXCL, ob->mode ? ob->mode : 0600);
   if (fd >= 0)

commit c4b6cd19106c7866a0446815a54e8551141512ed
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Jan 23 14:38:26 2026 +0000

    Debug: wordsize mods follow-up
    
    Broken-by: c33264393a1c

diff --git a/src/exim_monitor/em_version.c b/src/exim_monitor/em_version.c
index 15b16c10f..aac3788cd 100644
--- a/src/exim_monitor/em_version.c
+++ b/src/exim_monitor/em_version.c
@@ -9,12 +9,13 @@
 
 #define EM_VERSION_C
 
+#include <string.h>
+#include <stdint.h>
+#include <stdlib.h>
 #include "mytypes.h"
 #include "store.h"
 #include "path_max.h"
 #include "macros.h"
-#include <string.h>
-#include <stdlib.h>
 
 #include "version.h"
 
diff --git a/src/src/functions.h b/src/src/functions.h
index 916c1a329..3614ebfca 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -140,7 +140,7 @@ extern int     bdat_ungetc(int);
 extern void    bdat_flush_data(void);
 
 extern void    bits_clear(unsigned long *, size_t, int *);
-extern void    bits_set(unsigned long *, size_t, int *);
+extern void    bits_set(bitmask_word_t *, size_t, int *);
 
 extern void    cancel_cutthrough_connection(BOOL, const uschar *);
 extern gstring *cat_file(FILE *, gstring *, const uschar *);
diff --git a/src/src/macros.h b/src/src/macros.h
index ea5ca9277..0efe34cc0 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -371,7 +371,16 @@ masks are part of the local_scan API so are #defined in local_scan.h .
 Thanks to the "one word", debug bits beyond 31 are not available on 32b-int
 systems, and coding must account for that. */
 
-#define DEBUG_BIT(name) Di_##name = IOTA(Di_iota), D_##name = (unsigned long)BIT(Di_##name)
+#ifndef bitmask_word_t
+# define bitmask_word_t          uint64_t
+#endif
+
+#define BITMASK_IDX_TO_BIT(idx) ((bitmask_word_t)1 << (idx))
+#define BIT_TABLE_BIT(class, name) \
+			class##i_##name = IOTA(class##i_iota), \
+			class##_##name = BITMASK_IDX_TO_BIT(class##i_##name)
+
+#define DEBUG_BIT(name) BIT_TABLE_BIT(D, name)
 #define DEBUG_Z_BIT(name) Di_##name = 0, D_##name = 0
 
 enum {
@@ -455,7 +464,7 @@ and are only ever tested independently, so they do not need bit mask
 declarations. The Li_all value is recognized specially by decode_bits().
 Add also to log_options[] when creating new ones. */
 
-#define LOG_BIT(name) Li_##name = IOTA(Li_iota), L_##name = BIT(Li_##name)
+#define LOG_BIT(name) BIT_TABLE_BIT(L, name)
 
 enum logbit {
   Li_all = -1,

commit c58cf949706491e185303796a0193e74188ae035
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Fri Jan 23 18:25:03 2026 +0000

    Fix header collapse in auto-generated bounce message.  Bug 3192

diff --git a/src/src/deliver.c b/src/src/deliver.c
index 44e163fa4..af10ff8f9 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -6110,7 +6110,7 @@ wording. */
     else
       fprintf(fp,
 	"The following text was generated during the delivery "
-	"attempt%s:\n", (filecount > 1)? "s" : "");
+	"attempt%s:\n", filecount > 1 ? "s" : "");
 
     for (address_item * addr = msgchain; addr; addr = nextaddr)
       {
diff --git a/src/src/routers/manualroute.c b/src/src/routers/manualroute.c
index 589db7ead..3ddde3e56 100644
--- a/src/src/routers/manualroute.c
+++ b/src/src/routers/manualroute.c
@@ -263,7 +263,7 @@ if (ob->route_list)
     {
     int rc;
 
-    DEBUG(D_route) debug_printf("route_item = %s\n", route_item);
+    DEBUG(D_route) debug_printf_indent("route_item = %s\n", route_item);
     if (!parse_route_item(route_item, &domain, &hostlist, &options))
       continue;     /* Ignore blank items */
 
@@ -276,10 +276,7 @@ if (ob->route_list)
     /* If there was a problem doing the check, defer */
 
     if (rc == DEFER)
-      {
-      addr->message = US"lookup defer in route_list";
-      return DEFER;
-      }
+      { addr->message = US"lookup defer in route_list"; return DEFER; }
     }
 
   if (!route_item) return DECLINE;  /* No pattern in the list matched */
diff --git a/src/src/transport.c b/src/src/transport.c
index 5d3b8a732..457245cd0 100644
--- a/src/src/transport.c
+++ b/src/src/transport.c
@@ -834,8 +834,13 @@ for (header_line * h = header_list; h; h = h->next) if (h->type != htype_old)
 		  tblock->rewrite_existflags, FALSE)))
 	{
 	len = hh->slen;
-	if (tctx->options & topt_truncate_headers && len > 998) len = 998;
-	if (!sendfn(tctx, hh->text, len)) return FALSE;
+	if (tctx->options & topt_truncate_headers && len > 998)
+	  {
+	  if (  !sendfn(tctx, hh->text, 991)
+	     || !sendfn(tctx, US"<trunc>\n", 8)) return FALSE;
+	  }
+	else
+	  if (!sendfn(tctx, hh->text, len)) return FALSE;
 	store_reset(reset_point);
 	continue;     /* With the next header line */
 	}
@@ -844,8 +849,13 @@ 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 */
 
     len = h->slen;
-    if (tctx->options & topt_truncate_headers && len > 998) len = 998;
-    if (!sendfn(tctx, h->text, len)) return FALSE;
+    if (tctx->options & topt_truncate_headers && len > 998)
+      {
+      if (  !sendfn(tctx, h->text, 991)
+	 || !sendfn(tctx, US"<trunc>\n", 8)) return FALSE;
+      }
+    else
+      if (!sendfn(tctx, h->text, len)) return FALSE;
     }
 
   /* Header removed */

commit c1bdd01d925954872fa7a7ffea10d3d86965a6a0
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Jan 25 14:27:55 2026 +0000

    tidying

diff --git a/src/src/dnsbl.c b/src/src/dnsbl.c
index ac3e017e9..8a3d02184 100644
--- a/src/src/dnsbl.c
+++ b/src/src/dnsbl.c
@@ -65,8 +65,8 @@ Returns:         OK if lookup succeeded
 */
 
 static int
-one_check_dnsbl(uschar *domain, uschar *domain_txt, uschar *keydomain,
-  uschar *prepend, uschar *iplist, BOOL bitmask, int match_type,
+one_check_dnsbl(uschar * domain, uschar * domain_txt, uschar * keydomain,
+  uschar * prepend, uschar * iplist, BOOL bitmask, int match_type,
   int defer_return)
 {
 dns_answer * dnsa = store_get_dns_answer();
@@ -202,17 +202,17 @@ list (introduced by "&"), or a negative bitmask list (introduced by "!&").*/
 
 if (cb->rc == DNS_SUCCEED)
   {
-  dns_address * da = NULL;
-  uschar *addlist = cb->rhs->address;
+  gstring * addlist = NULL;
 
   /* For A and AAAA records, there may be multiple addresses from multiple
   records. For A6 records (currently not expected to be used) there may be
   multiple addresses from a single record. */
 
-  for (da = cb->rhs->next; da; da = da->next)
-    addlist = string_sprintf("%s, %s", addlist, da->address);
+  for (dns_address * da = cb->rhs; da; da = da->next)
+    addlist = string_append2_listele_n(addlist, US", ",
+					da->address, Ustrlen(da->address));
 
-  HDEBUG(D_dnsbl) debug_printf("DNS lookup for %s succeeded (yielding %s)\n",
+  HDEBUG(D_dnsbl) debug_printf("DNS lookup for %s succeeded (yielding %Y)\n",
     query, addlist);
 
   /* Address list check; this can be either for equality, or via a bitmask.
@@ -220,6 +220,7 @@ if (cb->rc == DNS_SUCCEED)
 
   if (iplist)
     {
+    dns_address * da;
     for (da = cb->rhs; da; da = da->next)
       {
       int ipsep = ',';
@@ -315,7 +316,7 @@ if (cb->rc == DNS_SUCCEED)
   else
     {
     BOOL ok = FALSE;
-    for (da = cb->rhs; da; da = da->next)
+    for (dns_address * da = cb->rhs; da; da = da->next)
       {
       int address[4];
 
@@ -371,9 +372,9 @@ if (cb->rc == DNS_SUCCEED)
     }
 
   /* $dnslist_* likely apply to the conn not a message, so we want them not
-  destroyed by reset_store() between messages */
+  destroyed by reset_store() between messages. Use the perm pool. */
 
-  dnslist_value = addlist ? string_copy_perm(addlist, FALSE) : NULL;
+  dnslist_value = addlist ? string_copy_perm(addlist->s, FALSE) : NULL;
   dnslist_text = cb->text ? string_copy_perm(cb->text, FALSE) : NULL;
   yield = OK;
   goto out;
@@ -503,10 +504,7 @@ while ((domain = string_nextinlist(&list, &sep, NULL, 0)))
   int rc;
   BOOL bitmask = FALSE;
   int match_type = 0;
-  uschar *domain_txt;
-  uschar *comma;
-  uschar *iplist;
-  uschar *key;
+  uschar * domain_txt, * comma, * iplist, * key;
 
   HDEBUG(D_dnsbl) debug_printf("dnslists check: %s\n", domain);
 

commit 57ef5eda41a8c3b3c037d1075f5f6506cd82daed
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Jan 26 10:51:11 2026 +0000

        Debug: wordsize mods follow-up
    
        Broken-by: c33264393a1c

diff --git a/src/src/functions.h b/src/src/functions.h
index 3614ebfca..27d1d2504 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -188,7 +188,7 @@ extern void    debug_pretrigger_discard(void);
 extern void    debug_print_socket(int);
 extern void    debug_trigger_fire(void);
 
-extern void    decode_bits(unsigned long *, size_t, int *,
+extern void    decode_bits(bitmask_word_t *, size_t, int *,
 	           const uschar *, bit_table *, int, uschar *, int);
 extern void    delete_pid_file(void);
 extern void    deliver_local(address_item *, BOOL);
diff --git a/src/src/macros.h b/src/src/macros.h
index 0efe34cc0..8600f5a94 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -337,11 +337,11 @@ platforms, but this ensures bit vectors always work the same way. */
 
 /* This macro is for single-word bit vectors: the debug selector,
 and the first word of the log selector. */
-#define BIT(n) (1UL << (n))
+#define BIT(n) ((bitmask_word_t)1 << (n))
 
 /* And these are for multi-word vectors. */
-#define BITWORD(n) (       (n) / BITWORDSIZE)
-#define BITMASK(n) (1UL << (n) % BITWORDSIZE)
+#define BITWORD(n) (    (n) / BITWORDSIZE)
+#define BITMASK(n) (BIT((n) % BITWORDSIZE))
 
 #define BIT_CLEAR(s,z,n) ((s)[BITWORD(n)] &= ~BITMASK(n))
 #define BIT_SET(s,z,n)   ((s)[BITWORD(n)] |=  BITMASK(n))

commit 8fdbbec22de1bdb8f7cc60c13798050edcafe696
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Mon Jan 26 10:52:12 2026 +0000

        Taint: track the dns answer buffers as being tainted

diff --git a/src/src/dns.c b/src/src/dns.c
index 6b527ef00..15f4c8cd0 100644
--- a/src/src/dns.c
+++ b/src/src/dns.c
@@ -376,7 +376,6 @@ if (reset != RESET_NEXT)
   while (dnss->rrcount-- > 0)
     {
     TRACE trace = "Q-namelen";
-    /*TTT*/
     if ((namelen = dns_rr_expand_taint(dnsa->answer, eom, dnss->aptr, dnss)) <0)
       goto null_return;
     /* skip name & type & class */
@@ -407,7 +406,6 @@ if (reset != RESET_NEXT)
     while (dnss->rrcount-- > 0)
       {
       TRACE trace = "A-namelen";
-      /*TTT*/
       if (  (namelen = dns_rr_expand_taint(dnsa->answer, eom, dnss->aptr, dnss))
 	  < 0)
 	goto null_return;
@@ -442,7 +440,6 @@ if (dnss->rrcount-- <= 0) return NULL;
 (something safe). */
 
 TRACE trace = "R-namelen";
-/*TTT*/
 if ((namelen = dns_rr_expand_taint(dnsa->answer, eom, dnss->aptr, dnss)) < 0)
   goto null_return;
 
@@ -507,7 +504,7 @@ if (h->nscount && h->aa)
   for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_AUTHORITY);
        rr; rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
     if (rr->type == (h->ancount ? T_NS : T_SOA))
-      return string_copy(rr->name);	/*TTT*/
+      return string_copy(rr->name);
 return NULL;
 }
 
@@ -1061,7 +1058,7 @@ former will work. */
 for (int i = 0; i <= dns_cname_loops; i++)
   {
   uschar * data;
-  dns_record cname_rr, type_rr;
+  dns_record cname_rr = {0}, type_rr = {0};
   dns_scan dnss = {0};
 
   /* DNS lookup failures get passed straight back. */
@@ -1075,7 +1072,6 @@ for (int i = 0; i <= dns_cname_loops; i++)
   contents of any rr blocks returned by dns_next_rr() as they use the same
   area in the dnsa block. */
 
-  cname_rr.data = type_rr.data = NULL;
   for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
        rr; rr = dns_next_rr(dnsa, &dnss, RESET_NEXT))
     if (rr->type == type)
@@ -1103,7 +1099,7 @@ for (int i = 0; i <= dns_cname_loops; i++)
 	  )
 #endif
        )
-        *fully_qualified_name = string_copy_dnsdomain(rr_name);	/*TTT*/
+        *fully_qualified_name = string_copy_dnsdomain(rr_name);
     }
 
   /* If any data records of the correct type were found, we are done. */
@@ -1127,7 +1123,7 @@ for (int i = 0; i <= dns_cname_loops; i++)
     }
 
   /* DNS data comes from the outside, hence tainted */
-  data = store_get(256, GET_TAINTED);				/*TTT alloc*/
+  data = store_get(256, GET_TAINTED);
   if (dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen,
       cname_rr.data, (DN_EXPAND_ARG4_TYPE)data, 256) < 0)
     {
@@ -1359,7 +1355,7 @@ switch (type)
 	/* If it's making an interesting assertion, return this response. */
 	if (port & 1)
 	  {
-	  *fully_qualified_name = namesuff + 1;	/*TTT*/
+	  *fully_qualified_name = namesuff + 1;
 	  return DNS_SUCCEED;
 	  }
 	}
diff --git a/src/src/dnsbl.c b/src/src/dnsbl.c
index 8a3d02184..e12a120fa 100644
--- a/src/src/dnsbl.c
+++ b/src/src/dnsbl.c
@@ -364,8 +364,7 @@ if (cb->rc == DNS_SUCCEED)
 	  int len = (rr->data)[0];
 	  if (len > 511) len = 127;
 	  store_pool = POOL_PERM;
-	  /*TTT*/
-	  cb->text = string_copyn_taint(CUS (rr->data+1), len, GET_TAINTED);
+	  cb->text = string_copyn(CUS (rr->data+1), len);
 	  store_pool = old_pool;
 	  break;
 	  }
diff --git a/src/src/functions.h b/src/src/functions.h
index 27d1d2504..c95ea2dc4 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -1138,26 +1138,14 @@ return item
 /* Use store_malloc for DNSA structs, and explicit frees. Using the same pool
 for them as the strings we proceed to copy from them meant they could not be
 released, hence blowing 64k for every DNS lookup. That mounted up. With malloc
-we do have to take care over marking tainted all copied strings.
-A separate pool could be used and could handle taint implicitly - but we would
-want to support independent free ops, not limited to stacked alloc/release */
+we do have to special-case taint checking (see store.c). */
 
+extern dns_answer * store_get_dns_answer_trc(const uschar *, unsigned);
 #define store_get_dns_answer() store_get_dns_answer_trc(CUS __FUNCTION__, __LINE__)
 
-static inline dns_answer *
-store_get_dns_answer_trc(const uschar * func, unsigned line)
-{
-return store_malloc_3(sizeof(dns_answer), CCS func, line);
-}
-
+extern void store_free_dns_answer_trc(dns_answer *, const uschar *, unsigned);
 #define store_free_dns_answer(dnsa) store_free_dns_answer_trc(dnsa, CUS __FUNCTION__, __LINE__)
 
-static inline void
-store_free_dns_answer_trc(dns_answer * dnsa, const uschar * func, unsigned line)
-{
-store_free_3(dnsa, CCS func, line);
-}
-
 
 /* Check for an RR being large enough.  Return TRUE iff bad. */
 static inline BOOL
diff --git a/src/src/host.c b/src/src/host.c
index 1bafda996..c4ca666cf 100644
--- a/src/src/host.c
+++ b/src/src/host.c
@@ -1667,7 +1667,6 @@ while ((ordername = string_nextinlist(&list, &sep, NULL, 0)))
         /* If an overlong response was received, the data will have been
         truncated and dn_expand may fail. */
 
-	/*TTT*/
         if (dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen,
              US rr->data, (DN_EXPAND_ARG4_TYPE)(s), ssize) < 0)
           {
@@ -2395,7 +2394,7 @@ for (; i >= 0; i--)
       if (thishostlast == NULL)
 	{
 	if (strcmpic(host->name, rr->name) != 0)
-	  host->name = string_copy_dnsdomain(rr->name);	/*TTT*/
+	  host->name = string_copy_dnsdomain(rr->name);
 	host->address = da->address;
 	host->sort_key = host->mx * 1000 + random_number(500) + randoffset;
 	host->status = hstatus_unknown;
@@ -2789,7 +2788,10 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
   int port = PORT_NONE;		/* MX lookups get PORT_NONE */
   const uschar * s = rr->data;	/* MUST be unsigned for GETSHORT */
   host_item * next;
-  uschar data[256];	/*TTT*/
+
+  /* Properly, should be tainted. string_copy_dnsdomain() call below must be
+  the only outside use if data placed here by dn_expand(). */
+  uschar data[256];
 
   if (rr_bad_size(rr, sizeof(uint16_t))) continue;
   GETSHORT(precedence, s);      /* Pointer s is advanced */
@@ -2816,7 +2818,6 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
 
   /* Get the name of the host pointed to. */
 
-  /*TTT*/
   if (dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, s,
       (DN_EXPAND_ARG4_TYPE)data, sizeof(data)) < 0)
     continue;
@@ -2858,7 +2859,7 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
   before the first block, copy the first block's data to a new second block. */
 
   next = last ? store_get(sizeof(host_item), GET_UNTAINTED) : host;
-  next->name = string_copy_dnsdomain(data);	/*TTT*/
+  next->name = string_copy_dnsdomain(data);
   next->address = NULL;
   next->port = port;
   next->mx = precedence;
diff --git a/src/src/lookups/dnsdb.c b/src/src/lookups/dnsdb.c
index 80cf2e62a..a17264501 100644
--- a/src/src/lookups/dnsdb.c
+++ b/src/src/lookups/dnsdb.c
@@ -140,13 +140,13 @@ int sep = 0, rc, type, failrc = FAIL;
 const uschar * outsep = CUS"\n", * outsep2 = NULL;
 uschar * equals, * domain, * found;
 
-dns_answer * dnsa = store_get_dns_answer();	/*TTT alloc */
+dns_answer * dnsa = store_get_dns_answer();
 dns_scan dnss = {0};
 
 /* Because we're working in the search pool, we try to reclaim as much
 store as possible later, so we preallocate the result here */
 
-gstring * yield = string_get_tainted(256, GET_TAINTED);	/*TTT alloc*/
+gstring * yield = string_get_tainted(256, GET_TAINTED);
 
 /* If the string starts with '>' we change the output separator.
 If it's followed by ';' or ',' we set the TXT output separator. */
@@ -403,7 +403,6 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 	  remain = rr->size - ++data_offset;
 	  if (chunk_len > remain)
 	    chunk_len = remain;
-	  /*TTT*/
 	  yield = string_catn(yield, US ((rr->data) + data_offset), chunk_len);
 	  data_offset += chunk_len;
 
@@ -428,7 +427,6 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 
 	  if (payload_length > MAX_TLSA_EXPANDED_SIZE)
 	    payload_length = MAX_TLSA_EXPANDED_SIZE;
-	  /*TTT*/
 	  yield = string_fmt_append(yield, "%d%c%d%c%d%c%.*H",
 				      usage, *outsep2,
 				      selector, *outsep2,
@@ -442,8 +440,7 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 
 	/* NB: this memory is released implicitly by the call
 	gstring_release_unused(yield) below. We used to use a stack-auto, but
-	I want to track taint wherever possible. The dnsa is not (yet)
-	allocated using taint-marked memory. */
+	I want to track taint wherever possible. */
 #define LCL_BUF_SIZE 264
 	uschar * buf = store_get(LCL_BUF_SIZE, GET_TAINTED);
 
@@ -504,7 +501,6 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 
         /* GETSHORT() has advanced the pointer to the target domain. */
 
-	/*TTT*/
         rc = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, p,
           (DN_EXPAND_ARG4_TYPE)buf, LCL_BUF_SIZE);
 
@@ -526,7 +522,6 @@ while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
 	  p += rc;
 	  yield = string_catn(yield, outsep2, 1);
 
-	  /*TTT*/
 	  rc = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, p,
 	    (DN_EXPAND_ARG4_TYPE)buf, LCL_BUF_SIZE);
 	  if (rc < 0)
diff --git a/src/src/miscmods/dkim.c b/src/src/miscmods/dkim.c
index 4bd87bbf9..d6858a00c 100644
--- a/src/src/miscmods/dkim.c
+++ b/src/src/miscmods/dkim.c
@@ -83,7 +83,7 @@ dkim_exim_query_dns_txt(const uschar * name)
 dns_answer * dnsa = store_get_dns_answer();
 dns_scan dnss = {0};
 rmark reset_point = store_mark();
-gstring * g = string_get_tainted(256, GET_TAINTED);	/*TTT alloc*/
+gstring * g = string_get_tainted(256, GET_TAINTED);
 
 lookup_dnssec_authenticated = NULL;
 if (dns_lookup(dnsa, name, T_TXT, NULL) != DNS_SUCCEED)
@@ -100,7 +100,6 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS);
       {
       uschar len = rr->data[rr_offset++];
 
-      /*TTT*/
       g = string_catn(g, US(rr->data + rr_offset), len);
       if (g->ptr >= PDKIM_DNS_TXT_MAX_RECLEN)
 	goto bad;
diff --git a/src/src/miscmods/spf.c b/src/src/miscmods/spf.c
index a0cdb8dc6..bc4131071 100644
--- a/src/src/miscmods/spf.c
+++ b/src/src/miscmods/spf.c
@@ -140,8 +140,7 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
 	the lib assumes it can free this (it does for srr.rr) - meaning we
 	cannot use pool store. The use of malloc also for T_TXT implies so. */
 
-	uschar * buf = store_malloc(256);	/*TTT alloc*/
-	/*TTT*/
+	uschar * buf = store_malloc(256);
 	if (dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, s,
 	    (DN_EXPAND_ARG4_TYPE)buf, 256) < 0)
 	  continue;
@@ -168,11 +167,12 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
 	  if (  !(chunk_len = s[offset++])
 	     || rr->size < offset + chunk_len	/* ignore bogus size chunks */
 	     ) break;
-	  g = string_catn(g, s+offset , chunk_len);	/*TTT*/
+	  g = string_catn(g, s+offset , chunk_len);
 	  }
 	if (!g)
 	  continue;
-	s = string_copy_malloc(string_from_gstring(g));	/*TTT*/
+	/* Again, we lose taint-tracking here */
+	s = string_copy_malloc(string_from_gstring(g));
 	gstring_reset(g);
 	gstring_release_unused(g);
 	DEBUG(D_receive) debug_printf_indent("SPF_dns_exim_lookup '%s'\n", s);
@@ -183,7 +183,8 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
       case T_AAAA:
       default:
 	{
-	uschar * buf = store_malloc(dnsa->answerlen + 1);	/*TTT alloc*/
+	uschar * buf = store_malloc(dnsa->answerlen + 1);
+	/* Again, we lose taint-tracking here */
 	s = memcpy(buf, s, dnsa->answerlen + 1);
 	break;
 	}
diff --git a/src/src/store.c b/src/src/store.c
index e265acd4c..6478dc3e7 100644
--- a/src/src/store.c
+++ b/src/src/store.c
@@ -210,12 +210,17 @@ static const uschar * poolclass[N_PAIRED_POOLS] = {
 [POOL_TAINT_SEARCH] =	US"tainted",
 [POOL_TAINT_MESSAGE] =	US"tainted",
 };
+
+
+static dns_answer * dnsa_tainted = NULL;
+
 #endif
 
 
 static void * internal_store_malloc(size_t, const char *, int);
 static void   internal_store_free(void *, const char *, int linenumber);
 
+
 /******************************************************************************/
 
 static void
@@ -285,6 +290,17 @@ log_write_die(0, LOG_MAIN,
 return NULL;
 }
 
+
+/******************************************************************************/
+static BOOL
+is_tainted_dnsa(const void * p)
+{
+#ifndef COMPILE_UTILITY
+for (dns_answer * dnsa = dnsa_tainted; dnsa; dnsa = dnsa->next)
+  if (CS p >= CS dnsa && CS p < CS(dnsa+1)) return TRUE;
+#endif
+return FALSE;
+}
 /******************************************************************************/
 /* Test if a pointer refers to tainted memory.
 
@@ -321,7 +337,7 @@ for (quoted_pooldesc * qp = quoted_pools; qp; qp = qp->next)
   for (b = qp->pool.chainbase; b; b = b->next)
     if (is_pointer_in_block(b, p)) return TRUE;
 
-return FALSE;
+return is_tainted_dnsa(p);
 }
 
 
@@ -1258,6 +1274,29 @@ n_nonpool_blocks--;
 internal_store_free(block, func, linenumber);
 }
 
+/******************************************************************************/
+#ifndef COMPILE_UTILITY
+/* Block-handling for dns answers - a bit over 64k each.
+We maintain a list for taint-tracking; these are from the outside so always
+tainted. Expect less than 3, so a linked-list is fine. */
+
+dns_answer *
+store_get_dns_answer_trc(const uschar * func, unsigned line)
+{
+dns_answer * dnsa = store_malloc_3(sizeof(dns_answer), CCS func, line);
+dnsa->next = dnsa_tainted;
+return dnsa_tainted = dnsa;
+}
+
+void
+store_free_dns_answer_trc(dns_answer * dnsa, const uschar * func, unsigned line)
+{
+for (dns_answer ** dp = &dnsa_tainted; *dp; dp = &((*dp)->next))
+  if (*dp == dnsa) { *dp = (*dp)->next; break; }
+store_free_3(dnsa, CCS func, line);
+}
+#endif	/*COMPILE_UTILITY*/
+
 /******************************************************************************/
 /* Stats output on process exit */
 void
diff --git a/src/src/string.c b/src/src/string.c
index b37606cf3..5e66e0732 100644
--- a/src/src/string.c
+++ b/src/src/string.c
@@ -613,6 +613,8 @@ allow_utf8_domains is set true and UTF-8 characters are used in domain
 names. Backslash can also be used to escape other characters, though we
 shouldn't come across them in domain names.
 
+We always return tainted mem, as some callers use stack-auto source buffers.
+
 Argument:   the domain name string
 Returns:    copy of string in new store, de-escaped
 */
@@ -621,7 +623,6 @@ uschar *
 string_copy_dnsdomain(const uschar * s)
 {
 uschar * yield;
-/*TTT*/
 uschar * ss = yield = store_get(Ustrlen(s) + 1, GET_TAINTED);	/* always treat as tainted */
 
 while (*s)
diff --git a/src/src/structs.h b/src/src/structs.h
index 52b540356..20438bb47 100644
--- a/src/src/structs.h
+++ b/src/src/structs.h
@@ -770,7 +770,8 @@ typedef struct {
 /* Structure for holding the result of a DNS query.  A touch over
 64k big, so take care to release as soon as possible. */
 
-typedef struct {
+typedef struct dns_answer {
+  struct dns_answer * next;
   int     answerlen;              /* length of the answer */
   uschar  answer[NS_MAXMSG];      /* the answer itself */
 } dns_answer;
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index 1610387d9..17ee21ef0 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -3416,7 +3416,10 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
     ) if (rr->type == T_TLSA && rr->size > 3)
   {
   const uschar * p = rr->data;
-/*XXX need somehow to mark rr and its data as tainted.  Doues this mean copying it? *//*TTT*/
+/*XXX need somehow to mark rr and its data as tainted.
+Doues this mean copying it?  That could actually be preferable, as the 64k
+of dns_annwer could be alloc/free within this func. */
+/*TTT*/
   uint8_t usage = p[0], sel = p[1], type = p[2];
 
   DEBUG(D_tls)

commit b7dda913d7f81dece907c536587b54a16b253e4a
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Sun Jan 25 23:20:23 2026 +0000

    DANE: separately alloc the dns answer buffer for TLSA

diff --git a/src/src/structs.h b/src/src/structs.h
index 20438bb47..ba88f487b 100644
--- a/src/src/structs.h
+++ b/src/src/structs.h
@@ -827,10 +827,7 @@ typedef struct {
 
 #ifdef SUPPORT_DANE
   BOOL			dane:1;		/* TLSA says connection must do dane */
-  /*TTT alloc*/
-  /* Strictly, this should use tainted mem. Also, is rather large - 64k - so
-  perhaps should be allocated only when needed (for DANE)? */
-  dns_answer		tlsa_dnsa;	/* strictly, this should use tainted mem */
+  dns_answer *		tlsa_dnsa;
 #endif
 } smtp_connect_args;
 
diff --git a/src/src/tls-gnu.c b/src/src/tls-gnu.c
index 17ee21ef0..04b439950 100644
--- a/src/src/tls-gnu.c
+++ b/src/src/tls-gnu.c
@@ -3388,7 +3388,7 @@ if (verify_check_given_host(CUSS &ob->tls_verify_cert_hostnames, host) == OK)
 
 #ifdef SUPPORT_DANE
 /* Given our list of RRs from the TLSA lookup, build a lookup block in
-GnuTLS-DANE's preferred format.  Hang it on the state str for later
+GnuTLS-DANE's preferred format.  Hang it on the state struct for later
 use in DANE verification.
 
 We point at the dnsa data not copy it, so it must remain valid until
@@ -3416,10 +3416,6 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
     ) if (rr->type == T_TLSA && rr->size > 3)
   {
   const uschar * p = rr->data;
-/*XXX need somehow to mark rr and its data as tainted.
-Doues this mean copying it?  That could actually be preferable, as the 64k
-of dns_annwer could be alloc/free within this func. */
-/*TTT*/
   uint8_t usage = p[0], sel = p[1], type = p[2];
 
   DEBUG(D_tls)
@@ -3441,7 +3437,7 @@ of dns_annwer could be alloc/free within this func. */
     }
 
   tls_out.tlsa_usage |= 1<<usage;
-  dane_data[i] = CS p;			/*TTT*/
+  dane_data[i] = CS p;
   dane_data_len[i++] = rr->size;
   }
 
@@ -3721,7 +3717,7 @@ set but both tls_verify_hosts and tls_try_verify_hosts are unset. Check only
 the specified host patterns if one of them is defined */
 
 #ifdef SUPPORT_DANE
-if (conn_args->dane && dane_tlsa_load(state, &conn_args->tlsa_dnsa))
+if (conn_args->dane && dane_tlsa_load(state, conn_args->tlsa_dnsa))
   {
   DEBUG(D_tls)
     debug_printf("TLS: server certificate DANE required\n");
diff --git a/src/src/tls-openssl.c b/src/src/tls-openssl.c
index 9d3f5af73..72c5c7a46 100644
--- a/src/src/tls-openssl.c
+++ b/src/src/tls-openssl.c
@@ -3972,7 +3972,6 @@ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
     }
 
   found++;
-  /*TTT*/
   switch (DANESSL_add_tlsa(ssl, usage, selector, mdname, p, rr->size - 3))
     {
     default:
@@ -4424,7 +4423,7 @@ if (tlsp->sni)
 
 #ifdef SUPPORT_DANE
 if (conn_args->dane)
-  if (dane_tlsa_load(exim_client_ctx->ssl, host, &conn_args->tlsa_dnsa, errstr) != OK)
+  if (dane_tlsa_load(exim_client_ctx->ssl, host, conn_args->tlsa_dnsa, errstr) != OK)
     return FALSE;
 #endif
 
diff --git a/src/src/tls.c b/src/src/tls.c
index 041cca5e9..5c349c2f4 100644
--- a/src/src/tls.c
+++ b/src/src/tls.c
@@ -850,7 +850,7 @@ exim_sha_init(h, HASH_SHA1);
 exim_sha_update_string(h, conn_args->host_lbserver);
 # ifdef SUPPORT_DANE
 if (conn_args->dane)
-  exim_sha_update(h,  CUS &conn_args->tlsa_dnsa, sizeof(dns_answer));
+  exim_sha_update(h,  CUS conn_args->tlsa_dnsa, sizeof(dns_answer));
 # endif
 exim_sha_update_string(h, conn_args->host->address);
 exim_sha_update(h,   CUS &conn_args->host->port, sizeof(conn_args->host->port));
diff --git a/src/src/transports/smtp.c b/src/src/transports/smtp.c
index 7274050f1..836831a49 100644
--- a/src/src/transports/smtp.c
+++ b/src/src/transports/smtp.c
@@ -2160,7 +2160,11 @@ int rc;
 if(  sx->dane_required
   || verify_check_given_host(CUSS &ob->hosts_try_dane, sx->conn_args.host) == OK
   )
-  switch (rc = tlsa_lookup(sx->conn_args.host, &sx->conn_args.tlsa_dnsa, sx->dane_required))
+  {
+  if (!sx->conn_args.tlsa_dnsa)
+    sx->conn_args.tlsa_dnsa = store_get_dns_answer();
+  switch (rc = tlsa_lookup(sx->conn_args.host,
+			    sx->conn_args.tlsa_dnsa, sx->dane_required))
     {
     case OK:		sx->conn_args.dane = TRUE;
 #ifdef EXPERIMENTAL_SRV_SMTPS
@@ -2182,6 +2186,7 @@ if(  sx->dane_required
 # endif
 			return rc;
     }
+  }
 return OK;
 }
 #endif
@@ -4050,7 +4055,7 @@ int rc;
 
 uschar * message = NULL;
 
-/* Large (12k, 66k if DANE supported).  Tainted, for the data buffers */
+/* Large (12k).  Tainted, for the data buffers */
 smtp_context * sx = store_get(sizeof(*sx), GET_TAINTED);
 
 BOOL pass_message = FALSE;

commit 697303f0af9f46062d3ebc3b3c336903ef8447cb
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Thu Jan 15 12:17:45 2026 +0000

    tidying

diff --git a/src/src/functions.h b/src/src/functions.h
index c95ea2dc4..2d14d2a58 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -139,7 +139,7 @@ extern BOOL    bdat_hasc(void);
 extern int     bdat_ungetc(int);
 extern void    bdat_flush_data(void);
 
-extern void    bits_clear(unsigned long *, size_t, int *);
+extern void    bits_clear(bitmask_word_t *, size_t, int *);
 extern void    bits_set(bitmask_word_t *, size_t, int *);
 
 extern void    cancel_cutthrough_connection(BOOL, const uschar *);
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index bfcdb22af..d1885069b 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -1423,6 +1423,11 @@ for (;;) switch(smtp_read_command(FALSE, GETC_BUFFER_UNLIMITED))
     smtp_printf("250 Reset OK\r\n", SP_NO_MORE);
     break;
 
+  case BADARG_CMD:
+    if (synprot_error(L_smtp_syntax_error, 501, NULL,
+      US"unexpected argument data") > 0) return;
+    break;
+
   default:
     smtp_printf("421 %s\r\n", SP_NO_MORE, message);
     break;
@@ -2888,15 +2893,16 @@ int
 synprot_error(int type, int code, uschar *data, uschar *errmess)
 {
 int yield = -1;
+BOOL syntax_error = type == L_smtp_syntax_error;
 
 #ifndef DISABLE_EVENT
 event_raise(event_action,
-  L_smtp_syntax_error ? US"smtp:fail:syntax" : US"smtp:fail:protocol",
+  syntax_error ? US"smtp:fail:syntax" : US"smtp:fail:protocol",
   errmess, NULL);
 #endif
 
 log_write(type, LOG_MAIN, "SMTP %s error in %q %s %s",
-  type == L_smtp_syntax_error ? "syntax" : "protocol",
+  syntax_error ? "syntax" : "protocol",
   string_printing(smtp_cmd_buffer), host_and_ident(TRUE), errmess);
 
 GET_OPTION("smtp_max_synprot_errors");
@@ -4051,7 +4057,7 @@ while (done <= 0)
   {
   uschar * errmess, * log_msg, * smtp_code;
   uschar * user_msg = NULL, * recipient = NULL, * hello = NULL;
-  uschar * s, * ss;
+  uschar * s;
   BOOL was_rej_mail = FALSE, was_rcpt = FALSE;
   int start, end, sender_domain, recipient_domain;
   int rc, c, dsn_flags;
@@ -4079,9 +4085,10 @@ while (done <= 0)
 	  done = smtp_handle_acl_fail(ACL_WHERE_AUTH, rc, user_msg, log_msg);
 	else
 	  {
-	  smtp_cmd_data = NULL;
+	  uschar * dummy_errmsg;
 
-	  if (smtp_in_auth(au, &s, &ss) == OK)
+	  smtp_cmd_data = NULL;
+	  if (smtp_in_auth(au, &s, &dummy_errmsg) == OK)
 	    { DEBUG(D_auth) debug_printf("tls auth succeeded\n"); }
 	  else
 	    {

commit 02b8b183a16a5bc7e10eb45cfb454002f63400c7
Author: Jeremy Harris <jgh146exb@wizmail.org>
Date:   Tue Jan 27 00:20:57 2026 +0000

    Build: Gnu/HURD types
    
    Broken-by: c33264393a1c

diff --git a/src/OS/os.h-GNU b/src/OS/os.h-GNU
index 2a335bc27..3aba3def5 100644
--- a/src/OS/os.h-GNU
+++ b/src/OS/os.h-GNU
@@ -1,5 +1,5 @@
 /* Exim: OS-specific C header file for GNU/Hurd */
-/* Copyright (c) The Exim Maintainers 2020 - 2025 */
+/* Copyright (c) The Exim Maintainers 2020 - 2026 */
 /* SPDX-License-Identifier: GPL-2.0-or-later */
 
 #include <features.h>
@@ -29,4 +29,9 @@ as well as any supplementary groups*/
 # define EXIM_HAVE_OPENAT
 #endif
 
+#ifdef PID_T_FMT
+# undef PID_T_FMT
+#endif
+#define PID_T_FMT "%d"
+
 /* End */
diff --git a/src/src/exim.c b/src/src/exim.c
index a87219920..c42bb60d8 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -2824,7 +2824,7 @@ on the second character (the one after '-'), to save some effort. */
       /* Use an intermediate variable so that we don't set debugging while
       decoding the debugging bits. */
 
-      unsigned long selector = D_default;
+      bitmask_word_t selector = D_default;
       debug_selector = 0;
       debug_file = NULL;
       if (*argrest == 'd')
