[exim-cvs] spf dynamic module

Top Page
Delete this message
Reply to this message
Author: Exim Git Commits Mailing List
Date:  
To: exim-cvs
Subject: [exim-cvs] spf dynamic module
Gitweb: https://git.exim.org/exim.git/commitdiff/cccb2c06bbb96fa98edf6867f0eba4059aa757a0
Commit:     cccb2c06bbb96fa98edf6867f0eba4059aa757a0
Parent:     a0ecb20496a00e26cf7345a75cc1137eb3ac0709
Author:     Jeremy Harris <jgh146exb@???>
AuthorDate: Wed Aug 28 00:16:43 2024 +0100
Committer:  Jeremy Harris <jgh146exb@???>
CommitDate: Wed Aug 28 11:17:07 2024 +0100


    spf dynamic module
---
 doc/doc-docbook/spec.xfpt      |   6 ++
 doc/doc-txt/NewStuff           |   6 +-
 src/OS/Makefile-Base           |  21 +++--
 src/scripts/Configure-Makefile |   1 +
 src/scripts/MakeLinks          |  11 +++
 src/scripts/lookups-Makefile   |   4 +-
 src/src/EDITME                 |   9 +-
 src/src/acl.c                  |  90 +++++++++++++++++---
 src/src/daemon.c               |   4 +-
 src/src/dmarc.c                |  37 ++++++--
 src/src/dmarc.h                |   1 +
 src/src/drtables.c             | 153 ++++++++++++++++++++++++++++-----
 src/src/exim.c                 |  13 ++-
 src/src/exim.h                 |   2 +-
 src/src/expand.c               | 107 ++++++++++--------------
 src/src/functions.h            |   5 +-
 src/src/globals.c              |  14 ----
 src/src/globals.h              |  10 ---
 src/src/lookups/Makefile       |   2 -
 src/src/lookups/spf.c          |  92 +++++---------------
 src/src/macros.h               |   2 +-
 src/src/miscmods/Makefile      |  31 +++++++
 src/src/{ => miscmods}/spf.c   | 186 ++++++++++++++++++++++++++++++++++++++---
 src/src/{ => miscmods}/spf.h   |   7 --
 src/src/readconf.c             |  20 ++++-
 src/src/receive.c              |   2 +-
 src/src/smtp_in.c              |  28 ++++++-
 src/src/structs.h              |  69 +++++++++++++++
 test/runtest                   |  15 ++--
 test/stderr/2610               |   2 -
 30 files changed, 697 insertions(+), 253 deletions(-)


diff --git a/doc/doc-docbook/spec.xfpt b/doc/doc-docbook/spec.xfpt
index 600eee4af..428cbc079 100644
--- a/doc/doc-docbook/spec.xfpt
+++ b/doc/doc-docbook/spec.xfpt
@@ -42314,6 +42314,12 @@ This includes retransmissions done by traditional forwarders.
SPF verification support is built into Exim if SUPPORT_SPF=yes is set in
&_Local/Makefile_&. The support uses the &_libspf2_& library
&url(https://www.libspf2.org/).
+.new
+.cindex "dynamic modules"
+The support can be built as a dynamic-load module if desired;
+see the comments in that Makefile.
+.wen
+
There is no Exim involvement in the transmission of messages;
publishing certain DNS records is all that is required.

diff --git a/doc/doc-txt/NewStuff b/doc/doc-txt/NewStuff
index 9a34f8ac2..640bd58cd 100644
--- a/doc/doc-txt/NewStuff
+++ b/doc/doc-txt/NewStuff
@@ -14,9 +14,9 @@ Version 4.98

3. Events smtp:fail:protocol and smtp:fail:syntax

- 4. JSON and LDAP lookup support, all the router and authenticator drivers,
-    and all the transport drivers except smtp, can now be built as loadable
-    modules
+ 4. JSON and LDAP lookup support, SPF support, all the router and authenticator
+    drivers, and all the transport drivers except smtp, can now be built as
+    loadable modules


Version 4.98
------------
diff --git a/src/OS/Makefile-Base b/src/OS/Makefile-Base
index 2e8728ae2..caae1e536 100644
--- a/src/OS/Makefile-Base
+++ b/src/OS/Makefile-Base
@@ -245,7 +245,7 @@ macro.c: macro_predef

 .PHONY: all config utils \
     buildauths buildlookups buildpdkim buildrouters \
-        buildtransports dynmodules checklocalmake clean
+        buildtransports buildmisc dynmodules checklocalmake clean



 utils: $(EXIM_MONITOR) exicyclog exinext exiwhat \
@@ -501,7 +501,6 @@ OBJ_EXPERIMENTAL =    arc.o \
             dcc.o \
             dmarc.o \
             imap_utf7.o \
-            spf.o \
             utf8.o \
             xclient.o


@@ -529,12 +528,12 @@ OBJ_EXIM = acl.o base64.o child.o crypt16.o daemon.o dbfn.o debug.o deliver.o \
         $(OBJ_EXPERIMENTAL)


 exim:   buildlookups buildauths pdkim/pdkim.a \
-        buildrouters buildtransports \
+        buildrouters buildtransports buildmisc \
         $(OBJ_EXIM) version.o
     @echo "$(LNCC) -o exim"
     $(FE)$(PURIFY) $(LNCC) -o exim $(LFLAGS) $(OBJ_EXIM) version.o \
       routers/routers.a transports/transports.a lookups/lookups.a \
-      auths/auths.a pdkim/pdkim.a \
+      auths/auths.a pdkim/pdkim.a miscmods/miscmods.a \
       $(LIBRESOLV) $(LIBS) $(LIBS_EXIM) $(IPV6_LIBS) $(EXTRALIBS) \
       $(EXTRALIBS_EXIM) $(DBMLIB) $(LOOKUP_LIBS) $(AUTH_LIBS) \
       $(PERL_LIBS) $(TLS_LIBS) $(PCRE_LIBS) $(LDFLAGS)
@@ -904,7 +903,6 @@ dane.o:        $(HDRS) dane.c dane-openssl.c
 dcc.o:        $(HDRS) dcc.h dcc.c
 dmarc.o:    $(HDRS) pdkim/pdkim.h dmarc.h dmarc.c
 imap_utf7.o:    $(HDRS) imap_utf7.c
-spf.o:        $(HDRS) spf.h spf.c
 utf8.o:        $(HDRS) utf8.c
 xclient.o:    $(HDRS) xclient.c


@@ -1018,10 +1016,11 @@ $(MONBIN): $(HDRS)

# Copies of modules built as dynamic-load libraries

-dynmodules: buildlookups buildrouters buildtransports buildauths
+dynmodules: buildlookups buildrouters buildtransports buildauths \
+        buildmisc
     rm -fr dynmodules
     mkdir dynmodules
-    for d in lookup router transport auth; do \
+    for d in lookup router transport auth miscmod; do \
       for f in $${d}s/*.so; do \
         [ -e $$f ] && ln $$f dynmodules/`basename $$f .so`_$$d.so; \
       done; \
@@ -1073,6 +1072,14 @@ pdkim/pdkim.a: config
        INCLUDE="$(INCLUDE) $(IPV6_INCLUDE) $(TLS_INCLUDE)"
      @echo " "


+buildmisc: config
+     @cd miscmods && $(MAKE) SHELL=$(SHELL) AR="$(AR)" $(MFLAGS) \
+       CC="$(CC)" CFLAGS="$(CFLAGS)" \
+         CFLAGS_DYNAMIC="$(CFLAGS_DYNAMIC)" HDRS="../version.h $(PHDRS)" \
+         FE="$(FE)" RANLIB="$(RANLIB)" RM_COMMAND="$(RM_COMMAND)" \
+         INCLUDE="$(INCLUDE) $(IPV6_INCLUDE)"
+     @echo " "
+
 # The "clean", "install", and "makefile" targets just pass themselves back to
 # the main Exim makefile. These targets will be obeyed only if "make" is obeyed
 # for them in the build directory.
diff --git a/src/scripts/Configure-Makefile b/src/scripts/Configure-Makefile
index 9179392f3..427ce0cb7 100755
--- a/src/scripts/Configure-Makefile
+++ b/src/scripts/Configure-Makefile
@@ -301,6 +301,7 @@ done <<-END
  routers    ROUTER    ACCEPT DNSLOOKUP IPLITERAL IPLOOKUP MANUALROUTE QUERYPROGRAM REDIRECT
  transports TRANSPORT    APPENDFILE AUTOREPLY LMTP PIPE QUEUEFILE SMTP
  auths        AUTH    CRAM_MD5 CYRUS_SASL DOVECOT EXTERNAL GSASL HEIMDAL_GSSAPI PLAINTEXT SPA TLS
+ miscmods   SUPPORT    SPF
 END


# See if there is a definition of EXIM_PERL in what we have built so far.
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index 76859ce9a..09d18b63c 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -89,6 +89,17 @@ do
done
cd ..

+# miscellaneous modules
+d="miscmods"
+mkdir $d
+cd $d
+# Makefile is generated
+for f in spf.c spf.h
+do
+ ln -s ../../src/$d/$f $f
+done
+cd ..
+
# and the hintsdb implementations
d="hintsdb"
mkdir $d
diff --git a/src/scripts/lookups-Makefile b/src/scripts/lookups-Makefile
index 40cca603f..188c22d14 100755
--- a/src/scripts/lookups-Makefile
+++ b/src/scripts/lookups-Makefile
@@ -169,8 +169,8 @@ do
emit_module_rule $name_mod
done

-# Because the variable is EXPERIMENTAL_SPF and not LOOKUP_SPF we
-# always include spf.o and compile a dummy if EXPERIMENTAL_SPF is not
+# 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"
diff --git a/src/src/EDITME b/src/src/EDITME
index 7d0b07331..e507ab3cd 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -832,8 +832,8 @@ FIXED_NEVER_USERS=root
# is historic).
# You need to add -export-dynamic -rdynamic to EXTRALIBS. You may also need to
# add -ldl to EXTRALIBS so that dlopen() is available to Exim. You need to
-# define CFLAGS_DYNAIC and LOOKUP_MODULE_DIR below so the builds are done right,
-# and so the exim binary actually loads dynamic lookup modules.
+# define CFLAGS_DYNAMIC and LOOKUP_MODULE_DIR below so the builds are done
+# right and so the exim binary actually loads dynamic lookup modules.
#
# Libraries being built as modules should be added to respective
# LOOKUP_*_INCLUDE and LOOKUP_*_LIBS rather than the the ones for the
@@ -1119,6 +1119,11 @@ ZCAT_COMMAND=/usr/bin/zcat
# Uncomment the following lines to add SPF support. You need to have libspf2
# installed on your system (www.libspf2.org). Depending on where it is installed
# you may have to edit the CFLAGS and LDFLAGS lines.
+#
+# If set to "2" instead of "yes" then the support will be
+# built as a module and must be installed into LOOKUP_MODULE_DIR (the name
+# is historic). The same rules as for other module builds apply; use
+# SUPPORT_SPF_{INCLUDE,LIBS}.

 # SUPPORT_SPF=yes
 # CFLAGS  += -I/usr/local/include
diff --git a/src/src/acl.c b/src/src/acl.c
index 533dcd60a..e285da65c 100644
--- a/src/src/acl.c
+++ b/src/src/acl.c
@@ -129,14 +129,16 @@ being the prefix of another; the binary-search in the list will go wrong. */
 typedef struct condition_def {
   uschar    *name;


+  /* Flags for actions or checks to do during readconf for this condition */
   unsigned    flags;
 #define ACD_EXP        BIT(0)    /* do expansion at outer level*/
 #define ACD_MOD        BIT(1)    /* is a modifier */
+#define ACD_LOAD    BIT(2)    /* supported by a dynamic-load module */


-/* Bit map vector of which conditions and modifiers are not allowed at certain
-times. For each condition and modifier, there's a bitmap of dis-allowed times.
-For some, it is easier to specify the negation of a small number of allowed
-times. */
+  /* Bit map vector of which conditions and modifiers are not allowed at certain
+  times. For each condition and modifier, there's a bitmap of dis-allowed times.
+  For some, it is easier to specify the negation of a small number of allowed
+  times. */
   unsigned    forbids;
 #define FORBIDDEN(times)    (times)
 #define PERMITTED(times)    ((unsigned) ~(times))
@@ -339,14 +341,22 @@ static condition_def conditions[] = {
   },
 #endif
 #ifdef SUPPORT_SPF
-  [ACLC_SPF] =            { US"spf",        ACD_EXP,
+  [ACLC_SPF] =            { US"spf",
+# if SUPPORT_SPF==2
+                  ACD_LOAD |
+# endif
+                  ACD_EXP,
                   FORBIDDEN(ACL_BIT_AUTH | ACL_BIT_CONNECT |
                     ACL_BIT_HELO | ACL_BIT_MAILAUTH |
                     ACL_BIT_ETRN | ACL_BIT_EXPN |
                     ACL_BIT_STARTTLS | ACL_BIT_VRFY |
                     ACL_BIT_NOTSMTP | ACL_BIT_NOTSMTP_START),
   },
-  [ACLC_SPF_GUESS] =        { US"spf_guess",    ACD_EXP,
+  [ACLC_SPF_GUESS] =        { US"spf_guess",
+# if SUPPORT_SPF==2
+                  ACD_LOAD |
+# endif
+                  ACD_EXP,
                   FORBIDDEN(ACL_BIT_AUTH | ACL_BIT_CONNECT |
                     ACL_BIT_HELO | ACL_BIT_MAILAUTH |
                     ACL_BIT_ETRN | ACL_BIT_EXPN |
@@ -383,6 +393,25 @@ for (condition_def * c = conditions; c < conditions + nelem(conditions); c++)


#ifndef MACRO_PREDEF

+# ifdef LOOKUP_MODULE_DIR
+typedef struct condition_module {
+  const uschar *    mod_name;    /* module for the givien conditions */
+  misc_module_info *    info;        /* NULL when not loaded */
+  const int *        conditions;    /* array of ACLC_*, -1 terminated */
+} condition_module;
+
+#  if SUPPORT_SPF==2
+static int spf_condx[] = { ACLC_SPF, ACLC_SPF_GUESS, -1 };
+#  endif
+
+static condition_module condition_modules[] = {
+#  if SUPPORT_SPF==2
+  {.mod_name = US"spf", .conditions = spf_condx},
+#  endif
+};
+
+# endif
+
 /* Return values from decode_control() */


enum {
@@ -936,7 +965,7 @@ while ((s = (*func)()))

/* The modifiers may not be negated */

-  if (negated && conditions[c].flags & ACD_MOD )
+  if (negated && conditions[c].flags & ACD_MOD)
     {
     *error = string_sprintf("ACL error: negation is not allowed with "
       "\"%s\"", conditions[c].name);
@@ -954,6 +983,34 @@ while ((s = (*func)()))
     return NULL;
     }


+#ifdef LOOKUP_MODULE_DIR
+  if (conditions[c].flags & ACD_LOAD)
+    {                /* a loadable module supports this condition */
+    condition_module * cm;
+    uschar * s = NULL;
+
+    for (cm = condition_modules;
+        cm < condition_modules + nelem(condition_modules); cm++)
+      for (const int * cond = cm->conditions; *cond != -1; cond++)
+    if (*cond == c) goto found;
+    found:
+
+    if (cm >= condition_modules + nelem(condition_modules))
+      {                        /* shouldn't happen */
+      *error = string_sprintf("ACL error: failed to locate support for '%s'",
+                  conditions[c].name);
+      return NULL;
+      }
+    if (  !cm->info                /* module not loaded */
+       && !(cm->info = misc_mod_find(cm->mod_name, &s)))
+      {
+      *error = string_sprintf("ACL error: failed to find module for '%s': %s",
+                  conditions[c].name, s);
+      return NULL;
+      }
+    }
+#endif
+
   cond = store_get(sizeof(acl_condition_block), GET_UNTAINTED);
   cond->next = NULL;
   cond->type = c;
@@ -4127,12 +4184,23 @@ for (; cb; cb = cb->next)


 #ifdef SUPPORT_SPF
     case ACLC_SPF:
-      rc = spf_process(&arg, sender_address, SPF_PROCESS_NORMAL);
-      break;
-
     case ACLC_SPF_GUESS:
-      rc = spf_process(&arg, sender_address, SPF_PROCESS_GUESS);
+      /* Hardwire the offset of the function in the module functions table
+      for now.  Work out a more general mech later. */
+      {
+      misc_module_info * mi = misc_mod_find(US"spf", &log_message);
+      typedef int (*fn_t)(const uschar **, const uschar *, int);
+      fn_t fn;
+
+      if (!mi)
+    { rc = DEFER; break; }            /* shouldn't happen */
+
+      fn = ((fn_t *) mi->functions)[1];
+
+      rc = fn(&arg, sender_address,
+          cb->type == ACLC_SPF ? SPF_PROCESS_NORMAL : SPF_PROCESS_GUESS);
       break;
+      }
 #endif


     case ACLC_UDPSEND:
diff --git a/src/src/daemon.c b/src/src/daemon.c
index 4088cb532..49bf74a11 100644
--- a/src/src/daemon.c
+++ b/src/src/daemon.c
@@ -2578,8 +2578,8 @@ smtp_deliver_init();    /* Used for callouts */
 #ifdef WITH_CONTENT_SCAN
 malware_init();
 #endif
-#ifdef SUPPORT_SPF
-spf_init();
+#ifdef SUPPORT_DMARC
+dmarc_init();
 #endif
 #ifndef DISABLE_TLS
 tls_daemon_init();
diff --git a/src/src/dmarc.c b/src/src/dmarc.c
index 28fce0624..664daa737 100644
--- a/src/src/dmarc.c
+++ b/src/src/dmarc.c
@@ -31,7 +31,9 @@ OPENDMARC_STATUS_T  da, sa, action;
 BOOL dmarc_abort  = FALSE;
 uschar *dmarc_pass_fail = US"skipped";
 header_line *from_header   = NULL;
-extern SPF_response_t   *spf_response;
+
+misc_module_info * spf_mod_info;
+SPF_response_t   *spf_response_p;
 int dmarc_spf_ares_result  = 0;
 uschar *spf_sender_domain  = NULL;
 uschar *spf_human_readable = NULL;
@@ -53,6 +55,16 @@ static dmarc_exim_p dmarc_policy_description[] = {
 };



+int
+dmarc_init(void)
+{
+uschar * errstr;
+if (!(spf_mod_info = misc_mod_find(US"spf", &errstr)))
+  log_write(0, LOG_MAIN|LOG_PANIC_DIE,
+        "dmarc: failed to find SPF module: %s", errstr);
+return TRUE;
+}
+
 gstring *
 dmarc_version_report(gstring * g)
 {
@@ -86,12 +98,12 @@ eb->next  = NULL;
 return eblock;
 }


-/* dmarc_init sets up a context that can be re-used for several
+/* dmarc_conn_init sets up a context that can be re-used for several
messages on the same SMTP connection (that come from the
same host with the same HELO string) */

 int
-dmarc_init(void)
+dmarc_conn_init(void)
 {
 int *netmask   = NULL;   /* Ignored */
 int is_ipv6    = 0;
@@ -106,11 +118,12 @@ dmarc_pass_fail    = US"skipped";
 dmarc_used_domain  = US"";
 f.dmarc_has_been_checked = FALSE;
 header_from_sender = NULL;
+spf_response_p       = NULL;
 spf_sender_domain  = NULL;
 spf_human_readable = NULL;


/* ACLs have "control=dmarc_disable_verify" */
-if (f.dmarc_disable_verify == TRUE)
+if (f.dmarc_disable_verify)
return OK;

(void) memset(&dmarc_ctx, '\0', sizeof dmarc_ctx);
@@ -274,7 +287,7 @@ g = string_fmt_append(NULL,
message_id, primary_hostname, time(NULL), sender_host_address,
header_from_sender, expand_string(US"$sender_address_domain"));

-if (spf_response)
+if (spf_response_p)
g = string_fmt_append(g, "spf %d\n", dmarc_spf_ares_result);

if (dkim_history_buffer)
@@ -418,7 +431,15 @@ if (!dmarc_abort && !sender_host_authenticated)
/* Use the envelope sender domain for this part of DMARC */

   spf_sender_domain = expand_string(US"$sender_address_domain");
-  if (!spf_response)
+
+    {
+    misc_module_info * mi = misc_mod_findonly(US"spf");
+    typedef SPF_response_t * (*fn_t)(void);
+    if (mi)
+      spf_response_p = ((fn_t *) mi->functions)[3]();    /* spf_get_response */
+    }
+
+  if (!spf_response_p)
     {
     /* No spf data means null envelope sender so generate a domain name
     from the sender_helo_name  */
@@ -439,7 +460,7 @@ if (!dmarc_abort && !sender_host_authenticated)
     }
   else
     {
-    sr = spf_response->result;
+    sr = spf_response_p->result;
     dmarc_spf_result = sr == SPF_RESULT_NEUTRAL  ? DMARC_POLICY_SPF_OUTCOME_NONE :
                sr == SPF_RESULT_PASS     ? DMARC_POLICY_SPF_OUTCOME_PASS :
                sr == SPF_RESULT_FAIL     ? DMARC_POLICY_SPF_OUTCOME_FAIL :
@@ -454,7 +475,7 @@ if (!dmarc_abort && !sender_host_authenticated)
                 sr == SPF_RESULT_PERMERROR ? ARES_RESULT_PERMERROR :
                 ARES_RESULT_UNKNOWN;
     origin = DMARC_POLICY_SPF_ORIGIN_MAILFROM;
-    spf_human_readable = US spf_response->header_comment;
+    spf_human_readable = US spf_response_p->header_comment;
     DEBUG(D_receive)
       debug_printf_indent("DMARC using SPF sender domain = %s\n", spf_sender_domain);
     }
diff --git a/src/src/dmarc.h b/src/src/dmarc.h
index fa366dd06..dcf289f2d 100644
--- a/src/src/dmarc.h
+++ b/src/src/dmarc.h
@@ -21,6 +21,7 @@
 /* prototypes */
 gstring * dmarc_version_report(gstring *);
 int dmarc_init(void);
+int dmarc_conn_init(void);
 int dmarc_store_data(header_line *);
 int dmarc_process(void);
 uschar *dmarc_exim_expand_query(int);
diff --git a/src/src/drtables.c b/src/src/drtables.c
index a144085c5..35a376dd1 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -325,7 +325,7 @@ extern lookup_module_info redis_lookup_module_info;
 extern lookup_module_info lmdb_lookup_module_info;
 #endif
 #if defined(SUPPORT_SPF)
-extern lookup_module_info spf_lookup_module_info;
+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;
@@ -341,6 +341,31 @@ extern lookup_module_info readsock_lookup_module_info;



 #ifdef LOOKUP_MODULE_DIR
+static void *
+mod_open(const uschar * name, const uschar * class, uschar ** errstr)
+{
+const uschar * path = string_sprintf(
+  LOOKUP_MODULE_DIR "/%s_%s." DYNLIB_FN_EXT, name, class);
+void * dl;
+if (!(dl = dlopen(CS path, RTLD_NOW)))
+  {
+  if (errstr)
+    *errstr = string_sprintf("Error loading %s: %s", name, dlerror());
+  else
+    (void) dlerror();        /* clear out error state */
+  return NULL;
+  }
+
+/* FreeBSD nsdispatch() can trigger dlerror() errors about
+_nss_cache_cycle_prevention_function; we need to clear the dlerror()
+state before calling dlsym(), so that any error afterwards only comes
+from dlsym().  */
+
+(void) dlerror();
+return dl;
+}
+
+
 /* Try to load a lookup module with the given name.


Arguments:
@@ -353,27 +378,12 @@ Return: boolean success
static BOOL
lookup_mod_load(const uschar * name, uschar ** errstr)
{
-const uschar * path = string_sprintf(
- LOOKUP_MODULE_DIR "/%s_lookup." DYNLIB_FN_EXT, name);
void * dl;
struct lookup_module_info * info;
const char * errormsg;

-if (!(dl = dlopen(CS path, RTLD_NOW)))
-  {
-  if (errstr)
-    *errstr = string_sprintf("Error loading %s: %s", name, dlerror());
-  else
-    (void) dlerror();        /* clear out error state */
+if (!(dl = mod_open(name, US"lookup", errstr)))
   return FALSE;
-  }
-
-/* FreeBSD nsdispatch() can trigger dlerror() errors about
-_nss_cache_cycle_prevention_function; we need to clear the dlerror()
-state before calling dlsym(), so that any error afterwards only comes
-from dlsym().  */
-
-errormsg = dlerror();


info = (struct lookup_module_info *) dlsym(dl, "_lookup_module_info");
if ((errormsg = dlerror()))
@@ -416,6 +426,89 @@ return TRUE;



+misc_module_info * misc_module_list = NULL;
+
+static void
+misc_mod_add(misc_module_info * mi)
+{
+if (mi->init) mi->init(mi);
+DEBUG(D_lookup) if (mi->lib_vers_report)
+  debug_printf_indent("%Y\n", mi->lib_vers_report(NULL));
+
+mi->next = misc_module_list;
+misc_module_list = mi;
+}
+
+
+#ifdef LOOKUP_MODULE_DIR
+
+/* Load a "misc" module, and add to list */
+
+static misc_module_info *
+misc_mod_load(const uschar * name, uschar ** errstr)
+{
+void * dl;
+struct misc_module_info * mi;
+const char * errormsg;
+
+DEBUG(D_any) debug_printf_indent("loading module '%s'\n", name);
+if (!(dl = mod_open(name, US"miscmod", errstr)))
+  return NULL;
+
+mi = (struct misc_module_info *) dlsym(dl,
+                    CS string_sprintf("%s_module_info", name));
+if ((errormsg = dlerror()))
+  {
+  fprintf(stderr, "%s does not appear to be an spf module (%s)\n", name, errormsg);
+  log_write(0, LOG_MAIN|LOG_PANIC, "%s does not appear to be an spf module (%s)", name, errormsg);
+  dlclose(dl);
+  return NULL;
+  }
+if (mi->dyn_magic != MISC_MODULE_MAGIC)
+  {
+  fprintf(stderr, "Module %s is not compatible with this version of Exim\n", name);
+  log_write(0, LOG_MAIN|LOG_PANIC, "Module %s is not compatible with this version of Exim", name);
+  dlclose(dl);
+  return FALSE;
+  }
+
+DEBUG(D_lookup) debug_printf_indent("Loaded \"%s\"\n", name);
+misc_mod_add(mi);
+return mi;
+}
+
+#endif    /*LOOKUP_MODULE_DIR*/
+
+
+/* Find a "misc" module by name, if loaded.
+For now use a linear search down a linked list.  If the number of
+modules gets large, we might consider a tree.
+*/
+
+misc_module_info *
+misc_mod_findonly(const uschar * name)
+{
+for (misc_module_info * mi = misc_module_list; mi; mi = mi->next)
+  if (Ustrcmp(name, mi->name) == 0)
+    return mi;
+}
+
+/* Find a "misc" module, possibly already loaded, by name. */
+
+misc_module_info *
+misc_mod_find(const uschar * name, uschar ** errstr)
+{
+misc_module_info * mi;
+if ((mi = misc_mod_findonly(name))) return mi;
+#ifdef LOOKUP_MODULE_DIR
+return misc_mod_load(name, errstr);
+#else
+return NULL;
+#endif    /*LOOKUP_MODULE_DIR*/
+}
+
+
+



void
@@ -495,10 +588,6 @@ addlookupmodule(&redis_lookup_module_info);
addlookupmodule(&lmdb_lookup_module_info);
#endif

-#ifdef SUPPORT_SPF
-addlookupmodule(&spf_lookup_module_info);
-#endif
-
#if defined(LOOKUP_SQLITE) && LOOKUP_SQLITE!=2
addlookupmodule(&sqlite_lookup_module_info);
#endif
@@ -511,6 +600,14 @@ addlookupmodule(&testdb_lookup_module_info);
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. */
+
+#if defined(SUPPORT_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. */
@@ -558,8 +655,22 @@ else

DEBUG(D_lookup) debug_printf("Loaded %d lookup modules\n", countmodules);
#endif
+}


+#if defined(SUPPORT_SPF) && SUPPORT_SPF!=2
+extern misc_module_info spf_module_info;
+#endif
+
+void
+init_misc_mod_list(void)
+{
+static BOOL onetime = FALSE;
+if (onetime) return;
+#if defined(SUPPORT_SPF) && SUPPORT_SPF!=2
+misc_mod_add(&spf_module_info);
+#endif
+onetime = TRUE;
}


diff --git a/src/src/exim.c b/src/src/exim.c
index de1f48434..ecc25d6bc 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -33,6 +33,7 @@ Also a few functions that don't naturally fit elsewhere. */
#endif

extern void init_lookup_list(void);
+extern void init_misc_mod_list(void);



@@ -1155,6 +1156,13 @@ gstring * b = NULL, * d = NULL;
d = string_cat(d, US" redis");
# endif
#endif
+#ifdef SUPPORT_SPF
+# if SUPPORT_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");
@@ -1390,9 +1398,6 @@ DEBUG(D_any)
#ifdef SUPPORT_DMARC
g = dmarc_version_report(g);
#endif
-#ifdef SUPPORT_SPF
- g = spf_lib_version_report(g);
-#endif

show_string(is_stdout, g);
g = NULL;
@@ -1428,6 +1433,7 @@ DEBUG(D_any)
tree_walk(lookups_tree, lookup_version_report_cb, &g);
show_string(is_stdout, g);
g = NULL;
+ init_misc_mod_list();

#ifdef WHITELIST_D_MACROS
g = string_fmt_append(g, "WHITELIST_D_MACROS: \"%s\"\n", WHITELIST_D_MACROS);
@@ -4204,6 +4210,7 @@ is equivalent to the ability to modify a setuid binary!

This needs to happen before we read the main configuration. */
init_lookup_list();
+init_misc_mod_list();

/*XXX this excrescence could move to the testsuite standard config setup file */
#ifdef SUPPORT_I18N
diff --git a/src/src/exim.h b/src/src/exim.h
index c4d80c694..470adb351 100644
--- a/src/src/exim.h
+++ b/src/src/exim.h
@@ -543,7 +543,7 @@ config.h, mytypes.h, and store.h, so we don't need to mention them explicitly.
# include "bmi_spam.h"
#endif
#ifdef SUPPORT_SPF
-# include "spf.h"
+# include "miscmods/spf.h"
#endif
#ifndef DISABLE_DKIM
# include "dkim.h"
diff --git a/src/src/expand.c b/src/src/expand.c
index 08d72f213..b3a1575a7 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -421,51 +421,6 @@ enum {
};


-/* Types of table entry */
-
-enum vtypes {
-  vtype_int,            /* value is address of int */
-  vtype_filter_int,     /* ditto, but recognized only when filtering */
-  vtype_ino,            /* value is address of ino_t (not always an int) */
-  vtype_uid,            /* value is address of uid_t (not always an int) */
-  vtype_gid,            /* value is address of gid_t (not always an int) */
-  vtype_bool,           /* value is address of bool */
-  vtype_stringptr,      /* value is address of pointer to string */
-  vtype_msgbody,        /* as stringptr, but read when first required */
-  vtype_msgbody_end,    /* ditto, the end of the message */
-  vtype_msgheaders,     /* the message's headers, processed */
-  vtype_msgheaders_raw, /* the message's headers, unprocessed */
-  vtype_localpart,      /* extract local part from string */
-  vtype_domain,         /* extract domain from string */
-  vtype_string_func,    /* value is string returned by given function */
-  vtype_todbsdin,       /* value not used; generate BSD inbox tod */
-  vtype_tode,           /* value not used; generate tod in epoch format */
-  vtype_todel,          /* value not used; generate tod in epoch/usec format */
-  vtype_todf,           /* value not used; generate full tod */
-  vtype_todl,           /* value not used; generate log tod */
-  vtype_todlf,          /* value not used; generate log file datestamp tod */
-  vtype_todzone,        /* value not used; generate time zone only */
-  vtype_todzulu,        /* value not used; generate zulu tod */
-  vtype_reply,          /* value not used; get reply from headers */
-  vtype_pid,            /* value not used; result is pid */
-  vtype_host_lookup,    /* value not used; get host name */
-  vtype_load_avg,       /* value not used; result is int from os_getloadavg */
-  vtype_pspace,         /* partition space; value is T/F for spool/log */
-  vtype_pinodes,        /* partition inodes; value is T/F for spool/log */
-  vtype_cert        /* SSL certificate */
-#ifndef DISABLE_DKIM
-  ,vtype_dkim           /* Lookup of value in DKIM signature */
-#endif
-};
-
-/* Type for main variable table */
-
-typedef struct {
-  const char *name;
-  enum vtypes type;
-  void       *value;
-} var_entry;
-
 /* Type for entries pointing to address/length pairs. Not currently
 in use. */


@@ -754,12 +709,12 @@ static var_entry var_table[] = {
   { "spam_score_int",      vtype_stringptr,   &spam_score_int },
 #endif
 #ifdef SUPPORT_SPF
-  { "spf_guess",           vtype_stringptr,   &spf_guess },
-  { "spf_header_comment",  vtype_stringptr,   &spf_header_comment },
-  { "spf_received",        vtype_stringptr,   &spf_received },
-  { "spf_result",          vtype_stringptr,   &spf_result },
-  { "spf_result_guessed",  vtype_bool,        &spf_result_guessed },
-  { "spf_smtp_comment",    vtype_stringptr,   &spf_smtp_comment },
+  { "spf_guess",           vtype_module,    US"spf" },
+  { "spf_header_comment",  vtype_module,    US"spf" },
+  { "spf_received",        vtype_module,    US"spf" },
+  { "spf_result",          vtype_module,    US"spf" },
+  { "spf_result_guessed",  vtype_module,    US"spf" },
+  { "spf_smtp_comment",    vtype_module,    US"spf" },
 #endif
   { "spool_directory",     vtype_stringptr,   &spool_directory },
   { "spool_inodes",        vtype_pinodes,     (void *)TRUE },
@@ -1281,19 +1236,19 @@ return NULL;



static var_entry *
-find_var_ent(uschar * name)
+find_var_ent(uschar * name, var_entry * table, unsigned nent)
{
int first = 0;
-int last = nelem(var_table);
+int last = nent;

while (last > first)
{
int middle = (first + last)/2;
- int c = Ustrcmp(name, var_table[middle].name);
+ int c = Ustrcmp(name, table[middle].name);

if (c > 0) { first = middle + 1; continue; }
if (c < 0) { last = middle; continue; }
- return &var_table[middle];
+ return &table[middle];
}
return NULL;
}
@@ -1420,7 +1375,7 @@ expand_getcertele(uschar * field, uschar * certvar)
{
var_entry * vp;

-if (!(vp = find_var_ent(certvar)))
+if (!(vp = find_var_ent(certvar, var_table, nelem(var_table))))
   {
   expand_string_message =
     string_sprintf("no variable named \"%s\"", certvar);
@@ -1935,9 +1890,11 @@ static const uschar *
 find_variable(uschar * name, esi_flags flags, int * newsize)
 {
 var_entry * vp;
-uschar *s, *domain;
-uschar **ss;
+uschar * s, * domain;
+uschar ** ss;
 void * val;
+var_entry * table = var_table;
+unsigned table_count = nelem(var_table);


/* Handle ACL variables, whose names are of the form acl_cxxx or acl_mxxx.
Originally, xxx had to be a number in the range 0-9 (later 0-19), but from
@@ -1982,9 +1939,11 @@ else if (Ustrncmp(name, "regex", 5) == 0)
}
#endif

+sublist:
+
/* For all other variables, search the table */

-if (!(vp = find_var_ent(name)))
+if (!(vp = find_var_ent(name, table, table_count)))
   return NULL;          /* Unknown variable name */


 /* Found an existing variable. If in skipping state, the value isn't needed,
@@ -2175,6 +2134,20 @@ switch (vp->type)
     return dkim_exim_expand_query((int)(long)val);
 #endif


+  case vtype_module:
+    {
+    uschar * errstr;
+    misc_module_info * mi = misc_mod_find(val, &errstr);
+    if (mi)
+      {
+      table = mi->variables;
+      table_count = mi->variables_count;
+      goto sublist;
+      }
+    log_write(0, LOG_MAIN|LOG_PANIC,
+      "failed to find %s module for %s: %s", US val, name, errstr);
+    return US"";
+    }
   }


 return NULL;  /* Unknown variable. Silences static checkers. */
@@ -2187,7 +2160,8 @@ void
 modify_variable(uschar *name, void * value)
 {
 var_entry * vp;
-if ((vp = find_var_ent(name))) vp->value = value;
+if ((vp = find_var_ent(name, var_table, nelem(var_table))))
+  vp->value = value;
 return;          /* Unknown variable name, fail silently */
 }


@@ -4909,7 +4883,15 @@ while (*s)
       yield = authres_iprev(yield);
       yield = authres_smtpauth(yield);
 #ifdef SUPPORT_SPF
-      yield = authres_spf(yield);
+    {
+    misc_module_info * mi = misc_mod_findonly(US"spf");
+    if (mi)
+      {
+      typedef gstring * (*fn_t)(gstring *);
+      fn_t fn = ((fn_t *) mi->functions)[2];    /* authres_spf */
+      yield = fn(yield);
+      }
+    }
 #endif
 #ifndef DISABLE_DKIM
       yield = authres_dkim(yield);
@@ -7225,7 +7207,8 @@ NOT_ITEM: ;
           string_sprintf("missing '}' closing cert arg of %s", name);
         goto EXPAND_FAILED_CURLY;
         }
-      if ((vp = find_var_ent(sub)) && vp->type == vtype_cert)
+      if (  (vp = find_var_ent(sub, var_table, nelem(var_table)))
+         && vp->type == vtype_cert)
         {
         s = s1+1;
         break;
diff --git a/src/src/functions.h b/src/src/functions.h
index be60d5fc9..aaec6461f 100644
--- a/src/src/functions.h
+++ b/src/src/functions.h
@@ -151,9 +151,6 @@ extern gstring *authres_dkim(gstring *);
 extern gstring *authres_dmarc(gstring *);
 #endif
 extern gstring *authres_smtpauth(gstring *);
-#ifdef SUPPORT_SPF
-extern gstring *authres_spf(gstring *);
-#endif


 extern uschar *b64encode(const uschar *, int);
 extern uschar *b64encode_taint(const uschar *, int, const void *);
@@ -380,6 +377,8 @@ extern ssize_t mime_decode_base64(FILE *, FILE *, uschar *);
 extern int     mime_regex(const uschar **, BOOL);
 extern void    mime_set_anomaly(int);
 #endif
+extern misc_module_info * misc_mod_find(const uschar * modname, uschar **);
+extern misc_module_info * misc_mod_findonly(const uschar * modname);
 extern uschar *moan_check_errorcopy(const uschar *);
 extern BOOL    moan_skipped_syntax_errors(uschar *, error_block *, uschar *,
                  BOOL, uschar *);
diff --git a/src/src/globals.c b/src/src/globals.c
index 9efc389a6..f2287d41c 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -411,9 +411,6 @@ BOOL    smtp_enforce_sync      = TRUE;
 BOOL    smtp_etrn_serialize    = TRUE;
 BOOL    smtp_input             = FALSE;
 BOOL    smtp_return_error_details = FALSE;
-#ifdef SUPPORT_SPF
-BOOL    spf_result_guessed     = FALSE;
-#endif
 BOOL    split_spool_directory  = FALSE;
 BOOL    spool_wireformat       = FALSE;
 BOOL    strict_acl_vars        = FALSE;
@@ -1501,17 +1498,6 @@ uschar *spam_action            = NULL;
 uschar *spam_score             = NULL;
 uschar *spam_score_int         = NULL;
 #endif
-#ifdef SUPPORT_SPF
-uschar *spf_guess              = US"v=spf1 a/24 mx/24 ptr ?all";
-uschar *spf_header_comment     = NULL;
-uschar *spf_received           = NULL;
-uschar *spf_result             = NULL;
-uschar *spf_smtp_comment       = NULL;
-uschar *spf_smtp_comment_template
-                    /* Used to be: "Please%_see%_http://www.open-spf.org/Why?id=%{S}&ip=%{C}&receiver=%{R}" */
-                               = US"Please%_see%_http://www.open-spf.org/Why";
-
-#endif


 FILE   *spool_data_file           = NULL;
 uschar *spool_directory        = US SPOOL_DIRECTORY
diff --git a/src/src/globals.h b/src/src/globals.h
index a542a179f..9b30e502c 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -1049,16 +1049,6 @@ extern uschar *spam_action;            /* the spamd recommended-action */
 extern uschar *spam_score;             /* the spam score (float) */
 extern uschar *spam_score_int;         /* spam_score * 10 (int) */
 #endif
-#ifdef SUPPORT_SPF
-extern uschar *spf_guess;              /* spf best-guess record */
-extern uschar *spf_header_comment;     /* spf header comment */
-extern uschar *spf_received;           /* Received-SPF: header */
-extern uschar *spf_result;             /* spf result in string form */
-extern BOOL    spf_result_guessed;     /* spf result is of best-guess operation */
-extern uschar *spf_smtp_comment;       /* spf comment to include in SMTP reply */
-extern uschar *spf_smtp_comment_template;
-                                       /* template to construct the spf comment by libspf2 */
-#endif
 extern BOOL    split_spool_directory;  /* TRUE to use multiple subdirs */
 extern FILE   *spool_data_file;           /* handle for -D file */
 extern uschar *spool_directory;        /* Name of spool directory */
diff --git a/src/src/lookups/Makefile b/src/src/lookups/Makefile
index 5193520f9..2bfd691a1 100644
--- a/src/src/lookups/Makefile
+++ b/src/src/lookups/Makefile
@@ -10,8 +10,6 @@


# MAGIC-TAG-MODS-OBJ-RULES-GO-HERE

-OBJ += spf.o
-
 all:             lookups.a $(MODS)


 lookups.a:       $(OBJ)
diff --git a/src/src/lookups/spf.c b/src/src/lookups/spf.c
index a1052d7fc..4e0824911 100644
--- a/src/src/lookups/spf.c
+++ b/src/src/lookups/spf.c
@@ -31,92 +31,46 @@ static void dummy(int x) { dummy2(x-1); }
 #include <spf2/spf_dns_resolv.h>
 #include <spf2/spf_dns_cache.h>


-extern SPF_dns_server_t * SPF_dns_exim_new(int);
-

 static void *
 spf_open(const uschar * filename, uschar ** errmsg)
 {
-SPF_dns_server_t * dc;
-SPF_server_t *spf_server = NULL;
-int debug = 0;
-
-DEBUG(D_lookup) debug = 1;
-
-if ((dc = SPF_dns_exim_new(debug)))
-  if ((dc = SPF_dns_cache_new(dc, NULL, debug, 8)))
-    spf_server = SPF_server_new_dns(dc, debug);
-
-if (!spf_server)
+misc_module_info * mi = misc_mod_find(US"spf", errmsg);
+if (mi)
   {
-  *errmsg = US"SPF_dns_exim_nnew() failed";
-  return NULL;
+  typedef void * (*fn_t)(const uschar *, uschar **);
+  return (((fn_t *) mi->functions)[5]) (filename, errmsg);
   }
-return (void *) spf_server;
+return NULL;
 }



static void
-spf_close(void *handle)
+spf_close(void * handle)
{
-SPF_server_t *spf_server = handle;
-if (spf_server) SPF_server_free(spf_server);
+misc_module_info * mi = misc_mod_find(US"spf", NULL);
+if (mi)
+ {
+ typedef void (*fn_t)(void *);
+ return (((fn_t *) mi->functions)[6]) (handle);
+ }
}

+
 static int
 spf_find(void * handle, const uschar * filename, const uschar * keystring,
   int key_len, uschar ** result, uschar ** errmsg, uint * do_cache,
   const uschar * opts)
 {
-SPF_server_t *spf_server = handle;
-SPF_request_t *spf_request;
-SPF_response_t *spf_response = NULL;
-
-if (!(spf_request = SPF_request_new(spf_server)))
-  {
-  *errmsg = US"SPF_request_new() failed";
-  return FAIL;
-  }
-
-#if HAVE_IPV6
-switch (string_is_ip_address(filename, NULL))
-#else
-switch (4)
-#endif
+misc_module_info * mi = misc_mod_find(US"spf", errmsg);
+if (mi)
   {
-  case 4:
-    if (!SPF_request_set_ipv4_str(spf_request, CS filename))
-      break;
-    *errmsg = string_sprintf("invalid IPv4 address '%s'", filename);
-    return FAIL;
-#if HAVE_IPV6
-
-  case 6:
-    if (!SPF_request_set_ipv6_str(spf_request, CS filename))
-      break;
-    *errmsg = string_sprintf("invalid IPv6 address '%s'", filename);
-    return FAIL;
-
-  default:
-    *errmsg = string_sprintf("invalid IP address '%s'", filename);
-    return FAIL;
-#endif
+  typedef int (*fn_t) (void *, const uschar *, const uschar *,
+              int, uschar **, uschar **, uint *, const uschar *);
+  return (((fn_t *) mi->functions)[7]) (handle, filename, keystring, key_len,
+                      result, errmsg, do_cache, opts);
   }
-
-if (SPF_request_set_env_from(spf_request, CS keystring))
-    {
-  *errmsg = string_sprintf("invalid envelope from address '%s'", keystring);
-  return FAIL;
-}
-
-SPF_request_query_mailfrom(spf_request, &spf_response);
-*result = string_copy(US SPF_strresult(SPF_response_result(spf_response)));
-
-DEBUG(D_lookup) spf_response_debug(spf_response);
-
-SPF_response_free(spf_response);
-SPF_request_free(spf_request);
-return OK;
+return FAIL;
 }



@@ -138,7 +92,7 @@ return g;
}


-static lookup_info _lookup_info = {
+static lookup_info spf_lookup_info = {
   .name = US"spf",            /* lookup name */
   .type = 0,                /* not absfile, not query style */
   .open = spf_open,            /* open function */
@@ -150,11 +104,11 @@ static lookup_info _lookup_info = {
   .version_report = spf_version_report             /* version reporting */
 };


-#ifdef DYNLOOKUP
+#ifdef notdef_DYNLOOKUP
#define spf_lookup_module_info _lookup_module_info
#endif

-static lookup_info *_lookup_list[] = { &_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 /* SUPPORT_SPF */
diff --git a/src/src/macros.h b/src/src/macros.h
index 3bfcf51d5..7bcc7cd04 100644
--- a/src/src/macros.h
+++ b/src/src/macros.h
@@ -704,7 +704,7 @@ can be easily tested as a group. That is the only use of opt_bool_last. */
enum { opt_bit = 32, opt_bool_verify, opt_bool_set, opt_expand_bool,
opt_bool_last,
opt_rewrite, opt_timelist, opt_uid, opt_gid, opt_uidlist, opt_gidlist,
- opt_expand_uid, opt_expand_gid, opt_func, opt_void };
+ opt_expand_uid, opt_expand_gid, opt_func, opt_void, opt_module };

 /* There's a high-ish bit which is used to flag duplicate options, kept
 for compatibility, which shouldn't be output. Also used for hidden options
diff --git a/src/src/miscmods/Makefile b/src/src/miscmods/Makefile
new file mode 100644
index 000000000..59bf29836
--- /dev/null
+++ b/src/src/miscmods/Makefile
@@ -0,0 +1,31 @@
+# Make file for building Exim's lookup modules.
+# This is called from the main make file, after cd'ing
+# to the misc_modulessubdirectory.
+#
+# Copyright (c) The Exim Maintainers 2024
+
+# nb: at build time, the version of this file used will have had some
+#     extra variable definitions and prepended to it and module build rules
+#     interpolated below. This is done by scripts/lookups-Makefile.
+
+# MAGIC-TAG-MODS-OBJ-RULES-GO-HERE
+
+
+all:        miscmods.a $(MODS)
+
+miscmods.a:    $(OBJ)
+        @$(RM_COMMAND) -f miscmods.a
+        @echo "$(AR) miscmods.a"
+        @$(AR) miscmods.a $(OBJ)
+        $(RANLIB) $@
+
+.SUFFIXES:      .o .c .so
+.c.o:;          @echo "$(CC) $*.c"
+        $(FE)$(CC) -c $(CFLAGS) $(INCLUDE) $*.c
+
+.c.so:;         @echo "$(CC) -shared $*.c"
+        $(FE)$(CC) $(SUPPORT_$*_INCLUDE) $(SUPPORT_$*_LIBS) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) $(DLFLAGS) $*.c -o $@
+
+spf.o spf.so:    $(HDRS) spf.h spf.c
+
+# End
diff --git a/src/src/spf.c b/src/src/miscmods/spf.c
similarity index 71%
rename from src/src/spf.c
rename to src/src/miscmods/spf.c
index 9abe18fc3..a7b6c6a8d 100644
--- a/src/src/spf.c
+++ b/src/src/miscmods/spf.c
@@ -11,7 +11,7 @@


/* Code for calling spf checks via libspf-alt. Called from acl.c. */

-#include "exim.h"
+#include "../exim.h"
#ifdef SUPPORT_SPF

/* must be kept in numeric order */
@@ -34,8 +34,20 @@ SPF_response_t *spf_response_2mx = NULL;

SPF_dns_rr_t * spf_nxdomain = NULL;

+uschar * spf_guess              = US"v=spf1 a/24 mx/24 ptr ?all";
+uschar * spf_header_comment     = NULL;
+uschar * spf_received           = NULL;
+uschar * spf_result             = NULL;
+uschar * spf_smtp_comment       = NULL;
+uschar * spf_smtp_comment_template
+                    /* Used to be: "Please%_see%_http://www.open-spf.org/Why?id=%{S}&ip=%{C}&receiver=%{R}" */
+                = US"Please%_see%_http://www.open-spf.org/Why";
+BOOL    spf_result_guessed     = FALSE;


-gstring *
+
+
+
+static gstring *
spf_lib_version_report(gstring * g)
{
int maj, min, patch;
@@ -188,12 +200,12 @@ return spfrr;



-SPF_dns_server_t *
+static SPF_dns_server_t *
SPF_dns_exim_new(int debug)
{
SPF_dns_server_t * spf_dns_server = store_malloc(sizeof(SPF_dns_server_t));

-DEBUG(D_receive) debug_printf("SPF_dns_exim_new\n");
+/* DEBUG(D_receive) debug_printf("SPF_dns_exim_new\n"); */

 memset(spf_dns_server, 0, sizeof(SPF_dns_server_t));
 spf_dns_server->destroy      = NULL;
@@ -225,8 +237,8 @@ return spf_dns_server;
    Return: Boolean success.
 */


-BOOL
-spf_init(void)
+static BOOL
+spf_init(void * dummy_ctx)
{
SPF_dns_server_t * dc;
int debug = 0;
@@ -278,13 +290,13 @@ return TRUE;
Return: Boolean success
*/

-BOOL
+static BOOL
spf_conn_init(uschar * spf_helo_domain, uschar * spf_remote_addr)
{
DEBUG(D_receive)
debug_printf("spf_conn_init: %s %s\n", spf_helo_domain, spf_remote_addr);

-if (!spf_server && !spf_init()) return FALSE;
+if (!spf_server && !spf_init(NULL)) return FALSE;

if (SPF_server_set_rec_dom(spf_server, CS primary_hostname))
{
@@ -320,8 +332,15 @@ if (SPF_request_set_helo_dom(spf_request, CS spf_helo_domain))
return TRUE;
}

+static void
+spf_smtp_reset(void)
+{
+spf_header_comment = spf_received = spf_result = spf_smtp_comment = NULL;
+spf_result_guessed = FALSE;
+}
+

-void
+static void
spf_response_debug(SPF_response_t * spf_response)
{
if (SPF_response_messages(spf_response) == 0)
@@ -343,7 +362,7 @@ else for (int i = 0; i < SPF_response_messages(spf_response); i++)

Return: OK/FAIL */

-int
+static int
spf_process(const uschar ** listptr, const uschar * spf_envelope_sender,
int action)
{
@@ -407,7 +426,7 @@ return FAIL;



-gstring *
+static gstring *
authres_spf(gstring * g)
{
uschar * s;
@@ -439,4 +458,149 @@ return g;
}


+/* Ugly; used only by dmarc (peeking into our data!)
+Exposure of values as $variables might be better? */
+
+static SPF_response_t *
+spf_get_response(void)
+{
+return spf_response;
+}
+
+/******************************************************************************/
+/* Lookup support */
+
+static void *
+spf_lookup_open(const uschar * filename, uschar ** errmsg)
+{
+SPF_dns_server_t * dc;
+SPF_server_t * spf_server = NULL;
+int debug = 0;
+
+DEBUG(D_lookup) debug = 1;
+
+if ((dc = SPF_dns_exim_new(debug)))
+  if ((dc = SPF_dns_cache_new(dc, NULL, debug, 8)))
+    spf_server = SPF_server_new_dns(dc, debug);
+
+if (!spf_server)
+  {
+  *errmsg = US"SPF_dns_exim_nnew() failed";
+  return NULL;
+  }
+return (void *) spf_server;
+}
+
+static void
+spf_lookup_close(void * handle)
+{
+SPF_server_t * spf_server = handle;
+if (spf_server) SPF_server_free(spf_server);
+}
+
+static int
+spf_lookup_find(void * handle, const uschar * filename,
+  const uschar * keystring, int key_len, uschar ** result, uschar ** errmsg,
+  uint * do_cache, const uschar * opts)
+{
+SPF_server_t *spf_server = handle;
+SPF_request_t *spf_request;
+SPF_response_t *spf_response = NULL;
+
+if (!(spf_request = SPF_request_new(spf_server)))
+  {
+  *errmsg = US"SPF_request_new() failed";
+  return FAIL;
+  }
+
+#if HAVE_IPV6
+switch (string_is_ip_address(filename, NULL))
+#else
+switch (4)
+#endif
+  {
+  case 4:
+    if (!SPF_request_set_ipv4_str(spf_request, CS filename))
+      break;
+    *errmsg = string_sprintf("invalid IPv4 address '%s'", filename);
+    return FAIL;
+#if HAVE_IPV6
+
+  case 6:
+    if (!SPF_request_set_ipv6_str(spf_request, CS filename))
+      break;
+    *errmsg = string_sprintf("invalid IPv6 address '%s'", filename);
+    return FAIL;
+
+  default:
+    *errmsg = string_sprintf("invalid IP address '%s'", filename);
+    return FAIL;
 #endif
+  }
+
+if (SPF_request_set_env_from(spf_request, CS keystring))
+    {
+  *errmsg = string_sprintf("invalid envelope from address '%s'", keystring);
+  return FAIL;
+}
+
+SPF_request_query_mailfrom(spf_request, &spf_response);
+*result = string_copy(US SPF_strresult(SPF_response_result(spf_response)));
+
+DEBUG(D_lookup) spf_response_debug(spf_response);
+
+SPF_response_free(spf_response);
+SPF_request_free(spf_request);
+return OK;
+}
+
+
+/******************************************************************************/
+/* Module API */
+
+static optionlist spf_options[] = {
+  { "spf_guess",                opt_stringptr,   {&spf_guess} },
+  { "spf_smtp_comment_template",opt_stringptr,   {&spf_smtp_comment_template} },
+};
+
+static void * spf_functions[] = {
+  spf_conn_init,
+  spf_process,
+  authres_spf,
+  spf_get_response,        /* ugly; for dmarc */
+  spf_smtp_reset,
+  
+  spf_lookup_open,
+  spf_lookup_close,
+  spf_lookup_find,
+};
+
+static var_entry spf_variables[] = {
+  { "spf_guess",        vtype_stringptr,    &spf_guess },
+  { "spf_header_comment",    vtype_stringptr,    &spf_header_comment },
+  { "spf_received",        vtype_stringptr,    &spf_received },
+  { "spf_result",        vtype_stringptr,    &spf_result },
+  { "spf_result_guessed",    vtype_bool,        &spf_result_guessed },
+  { "spf_smtp_comment",        vtype_stringptr,    &spf_smtp_comment },
+};
+
+misc_module_info spf_module_info =
+{
+  .name =        US"spf",
+# if SUPPORT_SPF==2
+  .dyn_magic =        MISC_MODULE_MAGIC,
+# endif
+  .init =        spf_init,
+  .lib_vers_report =    spf_lib_version_report,
+
+  .options =        spf_options,
+  .options_count =    nelem(spf_options),
+
+  .functions =        spf_functions,
+  .functions_count =    nelem(spf_functions),
+
+  .variables =        spf_variables,
+  .variables_count =    nelem(spf_variables),
+};
+
+#endif    /* almost all the file */
diff --git a/src/src/spf.h b/src/src/miscmods/spf.h
similarity index 77%
rename from src/src/spf.h
rename to src/src/miscmods/spf.h
index 7e9a6ca2a..679eab44b 100644
--- a/src/src/spf.h
+++ b/src/src/miscmods/spf.h
@@ -25,13 +25,6 @@ typedef struct spf_result_id {
   int    value;
 } spf_result_id;


-/* prototypes */
-gstring * spf_lib_version_report(gstring *);
-BOOL spf_init(void);
-BOOL spf_conn_init(uschar *, uschar *);
-int  spf_process(const uschar **, const uschar *, int);
-void spf_response_debug(SPF_response_t *);
-
 #define SPF_PROCESS_NORMAL  0
 #define SPF_PROCESS_GUESS   1
 #define SPF_PROCESS_FALLBACK    2
diff --git a/src/src/readconf.c b/src/src/readconf.c
index a090cfb30..1fe6b7341 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -344,8 +344,8 @@ static optionlist optionlist_config[] = {
   { "spamd_address",            opt_stringptr,   {&spamd_address} },
 #endif
 #ifdef SUPPORT_SPF
-  { "spf_guess",                opt_stringptr,   {&spf_guess} },
-  { "spf_smtp_comment_template",opt_stringptr,   {&spf_smtp_comment_template} },
+  { "spf_guess",                opt_module,     {US"spf"} },
+  { "spf_smtp_comment_template",opt_module,     {US"spf"} },
 #endif
   { "split_spool_directory",    opt_bool,        {&split_spool_directory} },
   { "spool_directory",          opt_stringptr,   {&spool_directory} },
@@ -1764,6 +1764,8 @@ if (Ustrncmp(name, "not_", 4) == 0)
   offset = 4;
   }


+sublist:
+
/* Search the list for the given name. A non-existent name, or an option that
is set twice, is a disaster. */

@@ -2443,6 +2445,20 @@ switch (type)
   case opt_func:
     ol->v.fn(name, s, 0);
     break;
+
+  case opt_module:
+    {
+    uschar * errstr;
+    misc_module_info * mi = misc_mod_find(US ol->v.value, &errstr);
+    if (!mi)
+      log_write(0, LOG_PANIC_DIE|LOG_CONFIG_IN,
+    "failed to find %s module for %s: %s", US ol->v.value, name, errstr);
+
+debug_printf("hunting for option %s in module %s\n", name, mi->name);
+    oltop = mi->options;
+    last = mi->options_count;
+    goto sublist;
+    }
   }


return TRUE;
diff --git a/src/src/receive.c b/src/src/receive.c
index 1ee02c5b7..336b37410 100644
--- a/src/src/receive.c
+++ b/src/src/receive.c
@@ -1836,7 +1836,7 @@ if (smtp_input && !smtp_batched_input && !f.dkim_disable_verify)
#endif

 #ifdef SUPPORT_DMARC
-if (sender_host_address) dmarc_init();    /* initialize libopendmarc */
+if (sender_host_address) dmarc_conn_init();    /* initialize libopendmarc */
 #endif


 /* In SMTP sessions we may receive several messages in one connection. Before
diff --git a/src/src/smtp_in.c b/src/src/smtp_in.c
index 7c2b4a292..f9bd3ece8 100644
--- a/src/src/smtp_in.c
+++ b/src/src/smtp_in.c
@@ -1682,8 +1682,14 @@ bmi_verdicts = NULL;
 #endif
 dnslist_domain = dnslist_matched = NULL;
 #ifdef SUPPORT_SPF
-spf_header_comment = spf_received = spf_result = spf_smtp_comment = NULL;
-spf_result_guessed = FALSE;
+  {
+  misc_module_info * mi = misc_mod_findonly(US"spf");
+  if (mi)
+    {
+    typedef void (*fn_t)(void);
+    (((fn_t *) mi->functions)[4])();    /* spf_smtp_reset*/
+    }
+  }
 #endif
 #ifndef DISABLE_DKIM
 dkim_cur_signer = dkim_signers =
@@ -4020,8 +4026,22 @@ while (done <= 0)
     }


 #ifdef SUPPORT_SPF
-      /* set up SPF context */
-      spf_conn_init(sender_helo_name, sender_host_address);
+      /* If we have an spf module, set up SPF context */
+      {
+      misc_module_info * mi = misc_mod_findonly(US"spf");
+      if (mi)
+    {
+    /* We have hardwired function-call numbers, and also prototypes for the
+    functions.  We could do a function name table search for the number
+    but I can't see how to deal with prototypes.  Is a K&R non-prototyped
+    function still usable with today's compilers? */
+
+    typedef BOOL (*fn_t)(uschar *, uschar *);
+    fn_t fn = ((fn_t *) mi->functions)[0];    /* spf_conn_init */
+
+    (void) fn(sender_helo_name, sender_host_address);
+    }
+      }
 #endif


       /* Apply an ACL check if one is defined; afterwards, recheck
diff --git a/src/src/structs.h b/src/src/structs.h
index 9492dbac2..2c8c77c43 100644
--- a/src/src/structs.h
+++ b/src/src/structs.h
@@ -965,4 +965,73 @@ typedef struct qrunner {
   BOOL queue_2stage :1;
 } qrunner;


+
+/* Types of variable table entry */
+
+enum vtypes {
+  vtype_int,            /* value is address of int */
+  vtype_filter_int,     /* ditto, but recognized only when filtering */
+  vtype_ino,            /* value is address of ino_t (not always an int) */
+  vtype_uid,            /* value is address of uid_t (not always an int) */
+  vtype_gid,            /* value is address of gid_t (not always an int) */
+  vtype_bool,           /* value is address of bool */
+  vtype_stringptr,      /* value is address of pointer to string */
+  vtype_msgbody,        /* as stringptr, but read when first required */
+  vtype_msgbody_end,    /* ditto, the end of the message */
+  vtype_msgheaders,     /* the message's headers, processed */
+  vtype_msgheaders_raw, /* the message's headers, unprocessed */
+  vtype_localpart,      /* extract local part from string */
+  vtype_domain,         /* extract domain from string */
+  vtype_string_func,    /* value is string returned by given function */
+  vtype_todbsdin,       /* value not used; generate BSD inbox tod */
+  vtype_tode,           /* value not used; generate tod in epoch format */
+  vtype_todel,          /* value not used; generate tod in epoch/usec format */
+  vtype_todf,           /* value not used; generate full tod */
+  vtype_todl,           /* value not used; generate log tod */
+  vtype_todlf,          /* value not used; generate log file datestamp tod */
+  vtype_todzone,        /* value not used; generate time zone only */
+  vtype_todzulu,        /* value not used; generate zulu tod */
+  vtype_reply,          /* value not used; get reply from headers */
+  vtype_pid,            /* value not used; result is pid */
+  vtype_host_lookup,    /* value not used; get host name */
+  vtype_load_avg,       /* value not used; result is int from os_getloadavg */
+  vtype_pspace,         /* partition space; value is T/F for spool/log */
+  vtype_pinodes,        /* partition inodes; value is T/F for spool/log */
+  vtype_cert,        /* SSL certificate */
+#ifndef DISABLE_DKIM
+  vtype_dkim,           /* Lookup of value in DKIM signature */
+#endif
+  vtype_module,        /* variable lives in a module; value is module name */
+};
+
+/* Type for main variable table */
+
+typedef struct {
+  const char *name;
+  enum vtypes type;
+  void       *value;
+} var_entry;
+
+
+
+/* dynamic-load module info */
+
+typedef struct misc_module_info {
+  struct misc_module_info * next;
+
+  const uschar * name;
+  unsigned    dyn_magic;
+  BOOL        (*init)(void *);    /* arg is the misc_module_info ptr */
+  gstring *    (*lib_vers_report)(gstring *);    /* underlying library */
+
+  void *    options;
+  unsigned    options_count;
+  void *    functions;
+  unsigned    functions_count;
+  void *    variables;
+  unsigned    variables_count;
+} misc_module_info;
+
+#define MISC_MODULE_MAGIC    0x4d4d4d31    /* MMM1 */
+
 /* End of structs.h */
diff --git a/test/runtest b/test/runtest
index 3e10a5ab7..dcf6d76b2 100755
--- a/test/runtest
+++ b/test/runtest
@@ -383,7 +383,7 @@ $date = "\\d{2}-\\w{3}-\\d{4}\\s\\d{2}:\\d{2}:\\d{2}";


# Debug time & pid

-$time_pid = "(?:\\d{2}:\\d{2}:\\d{2}\\s+\\d+\\s)";
+$time_pid = "(?:(?:\\d{2}:\\d{2}:\\d{2}\\s+)?\\d+\\s)";

 # Pattern for matching pids at start of stderr lines; initially something
 # that won't match.
@@ -1315,11 +1315,14 @@ RESET_AFTER_EXTRA_LINE_READ:
     # different libraries will have different numbers (possibly 0) of follow-up
     # lines, indenting with more data
     if (/^$time_pid?Library version:/) {
-      while (1) {
+      $_ = <IN>;
+      if (/^$time_pid?\s/) {
     $_ = <IN>;
-    next if /^$time_pid?\s/;
-    goto RESET_AFTER_EXTRA_LINE_READ;
+    if (/^$time_pid?\s/) {
+      $_ = <IN>;
+    }
       }
+      goto RESET_AFTER_EXTRA_LINE_READ;
     }


     # drop other build-time controls emitted for debugging
@@ -1556,8 +1559,10 @@ RESET_AFTER_EXTRA_LINE_READ:
     next if /^DKIM >> Body data for hash, canonicalized/;


     # Not all platforms build with SPF enabled
-    next if /(^spf_conn_init|^SPF_dns_exim_new|spf_compile\.c)/;
+    next if /(^$time_pid?spf_conn_init|spf_compile\.c)/;
     next if /try option spf_smtp_comment_template$/;
+    next if /loading module 'spf'$/;
+    next if /^Loaded "spf"$/;


     # Not all platforms have sendfile support
     next if /^cannot use sendfile for body: no support$/;
diff --git a/test/stderr/2610 b/test/stderr/2610
index c762e0340..564fb7b11 100644
--- a/test/stderr/2610
+++ b/test/stderr/2610
@@ -304,8 +304,6 @@ close MYSQL connection: 127.0.0.1:PORT_N/test/root
 01:01:01 p1235  sender_fullhost = (test) [10.0.0.0]
 01:01:01 p1235  sender_rcvhost = [10.0.0.0] (helo=test)
 01:01:01 p1235  set_process_info: pppp handling incoming connection from (test) [10.0.0.0]
-01:01:01 p1235  spf_conn_init: test 10.0.0.0
-01:01:01 p1235  SPF_dns_exim_new
 01:01:01 p1235  try option acl_smtp_helo
 01:01:01 p1235  SMTP>> 250 myhost.test.ex Hello test [10.0.0.0]
 01:01:01 p1235  SMTP<< mail from:<a@b>


--
## subscription configuration (requires account):
## https://lists.exim.org/mailman3/postorius/lists/exim-cvs.lists.exim.org/
## unsubscribe (doesn't require an account):
## exim-cvs-unsubscribe@???
## Exim details at http://www.exim.org/
## Please use the Wiki with this list - http://wiki.exim.org/