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. */
