summaryrefslogtreecommitdiffstats
path: root/src/lookups
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/lookups/Makefile76
-rw-r--r--src/lookups/README181
-rw-r--r--src/lookups/cdb.c490
-rw-r--r--src/lookups/dbmdb.c284
-rw-r--r--src/lookups/dnsdb.c614
-rw-r--r--src/lookups/dsearch.c190
-rw-r--r--src/lookups/ibase.c561
-rw-r--r--src/lookups/json.c188
-rw-r--r--src/lookups/ldap.c1613
-rw-r--r--src/lookups/ldap.h13
-rw-r--r--src/lookups/lf_check_file.c113
-rw-r--r--src/lookups/lf_functions.h20
-rw-r--r--src/lookups/lf_quote.c63
-rw-r--r--src/lookups/lf_sqlperform.c175
-rw-r--r--src/lookups/lmdb.c162
-rw-r--r--src/lookups/lsearch.c487
-rw-r--r--src/lookups/mysql.c503
-rw-r--r--src/lookups/nis.c141
-rw-r--r--src/lookups/nisplus.c294
-rw-r--r--src/lookups/oracle.c628
-rw-r--r--src/lookups/passwd.c85
-rw-r--r--src/lookups/pgsql.c506
-rw-r--r--src/lookups/readsock.c341
-rw-r--r--src/lookups/redis.c471
-rw-r--r--src/lookups/spf.c159
-rw-r--r--src/lookups/sqlite.c200
-rw-r--r--src/lookups/testdb.c100
-rw-r--r--src/lookups/whoson.c98
28 files changed, 8756 insertions, 0 deletions
diff --git a/src/lookups/Makefile b/src/lookups/Makefile
new file mode 100644
index 0000000..19585bf
--- /dev/null
+++ b/src/lookups/Makefile
@@ -0,0 +1,76 @@
+# Make file for building Exim's lookup modules.
+# This is called from the main make file, after cd'ing
+# to the lookups subdirectory.
+#
+# Copyright (c) The Exim Maintainers 2021
+
+# 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.
+
+# MAGIC-TAG-MODS-OBJ-RULES-GO-HERE
+
+
+all: lookups.a lf_quote.o lf_check_file.o lf_sqlperform.o $(MODS)
+
+lookups.a: $(OBJ)
+ @$(RM_COMMAND) -f lookups.a
+ @echo "$(AR) lookups.a"
+ @$(AR) lookups.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) $(LOOKUP_$*_INCLUDE) $(LOOKUP_$*_LIBS) -DDYNLOOKUP $(CFLAGS_DYNAMIC) $(CFLAGS) $(INCLUDE) $(DLFLAGS) $*.c -o $@
+
+lf_check_file.o: $(HDRS) lf_check_file.c lf_functions.h
+lf_quote.o: $(HDRS) lf_quote.c lf_functions.h
+lf_sqlperform.o: $(HDRS) lf_sqlperform.c lf_functions.h
+
+cdb.o: $(HDRS) cdb.c
+dbmdb.o: $(HDRS) dbmdb.c
+dnsdb.o: $(HDRS) dnsdb.c
+dsearch.o: $(HDRS) dsearch.c
+ibase.o: $(HDRS) ibase.c
+ldap.o: $(HDRS) ldap.c
+lmdb.o: $(HDRS) lmdb.c
+json.o: $(HDRS) json.c
+lsearch.o: $(HDRS) lsearch.c
+mysql.o: $(HDRS) mysql.c
+nis.o: $(HDRS) nis.c
+nisplus.o: $(HDRS) nisplus.c
+oracle.o: $(HDRS) oracle.c
+passwd.o: $(HDRS) passwd.c
+pgsql.o: $(HDRS) pgsql.c
+readsock.o: $(HDRS) readsock.c
+redis.o: $(HDRS) redis.c
+spf.o: $(HDRS) spf.c
+sqlite.o: $(HDRS) sqlite.c
+testdb.o: $(HDRS) testdb.c
+whoson.o: $(HDRS) whoson.c
+
+cdb.so: $(HDRS) cdb.c
+dbmdb.so: $(HDRS) dbmdb.c
+dnsdb.so: $(HDRS) dnsdb.c
+dsearch.so: $(HDRS) dsearch.c
+ibase.so: $(HDRS) ibase.c
+json.so: $(HDRS) json.c
+ldap.so: $(HDRS) ldap.c
+lmdb.so: $(HDRS) lmdb.c
+lsearch.so: $(HDRS) lsearch.c
+mysql.so: $(HDRS) mysql.c
+nis.so: $(HDRS) nis.c
+nisplus.so: $(HDRS) nisplus.c
+oracle.so: $(HDRS) oracle.c
+passwd.so: $(HDRS) passwd.c
+pgsql.so: $(HDRS) pgsql.c
+redis.so: $(HDRS) redis.c
+spf.so: $(HDRS) spf.c
+sqlite.so: $(HDRS) sqlite.c
+testdb.so: $(HDRS) testdb.c
+whoson.so: $(HDRS) whoson.c
+
+# End
diff --git a/src/lookups/README b/src/lookups/README
new file mode 100644
index 0000000..2e87eda
--- /dev/null
+++ b/src/lookups/README
@@ -0,0 +1,181 @@
+LOOKUPS
+-------
+
+Each lookup type is implemented by 6 functions, xxx_open(), xxx_check(),
+xxx_find(), xxx_close(), xxx_tidy(), and xxx_quote(), where xxx is the name of
+the lookup type (e.g. lsearch, dbm, or whatever). In addition, there is
+a version reporting function used to trace compile-vs-runtime conflicts and
+to help administrators ensure that the modules from the correct build are
+in use by the main binary.
+
+The xxx_check(), xxx_close(), xxx_tidy(), and xxx_quote() functions need not
+exist. There is a table in drtables.c which links the lookup names to the
+various sets of functions, with NULL entries for any that don't exist. When
+the code for a lookup type is omitted from the binary, all its entries are
+NULL.
+
+One of the fields in the table contains flags describing the kind of lookup.
+These are
+
+ lookup_querystyle
+
+This is a "query style" lookup without a file name, as opposed to the "single
+key" style, where the key is associated with a "file name".
+
+ lookup_absfile
+
+For single key lookups, this means that the file name must be an absolute path.
+It is set for lsearch and dbm, but not for NIS, for example.
+
+ lookup_absfilequery
+
+This is a query-style lookup that must start with an absolute file name. For
+example, the sqlite lookup is of this type.
+
+When a single-key or absfilequery lookup file is opened, the handle returned by
+the xxx_open() function is saved, along with the file name and lookup type, in
+a tree. Traditionally, lookup_querystyle does not use this (just returning a
+dummy value, and doing the "open" work in the xxx_find() routine); but this is
+not enforced by the framework.
+
+The xxx_close() function is not called when the first lookup is completed. If
+there are subsequent lookups of the same type that quote the same file name,
+xxx_open() isn't called; instead the cached handle is re-used.
+
+Exim calls the function search_tidyup() at strategic points in its processing
+(e.g. after all routing and directing has been done) and this function walks
+the tree and calls the xxx_close() functions for all the cached handles.
+
+Query-style lookups don't have the concept of an open file that can be cached
+this way. Of course, the local code for the lookup can manage its own caching
+information in any way it pleases. This means that the xxx_close()
+function, even it it exists, is never called. However, if an xxx_tidy()
+function exists, it is called once whenever Exim calls search_tidyup().
+
+A single-key lookup type may also have an xxx_tidy() function, which is called
+by search_tidyup() after all cached handles have been closed via the
+xxx_close() function.
+
+The lookup functions are wrapped into a special store pool (POOL_SEARCH). You
+can safely use store_get to allocate store for your handle caching. The store
+will be reset after all xxx_tidy() functions are called.
+
+The function interfaces are as follows:
+
+
+xxx_open()
+----------
+
+This function is called to initiate the lookup. For things that involve files
+it should do a real open; for other kinds of lookup it may do nothing at all.
+The arguments are:
+
+ uschar *filename the name of the "file" to open, for non-query-style
+ lookups; NULL for query-style lookups
+ uschar **errmsg where to put an error message if there is a problem
+
+The yield of xxx_open() is a void * value representing the open file or
+database. For real files is is normally the FILE or DBM value. For other
+kinds of lookup, if there is no natural value to use, (-1) is recommended.
+The value should not be NULL (or 0) as that is taken to indicate failure of
+the xxx_open() function. For single-key lookups, the handle is cached along
+with the filename and type, and may be used for several lookups.
+
+
+xxx_check()
+-----------
+
+If this function exists, it is called after a successful open to check on the
+ownership and mode of the file(s). The arguments are:
+
+ void *handle the handle passed back from xxx_open()
+ uschar *filename the filename passed to xxx_open()
+ int modemask mode bits that must not be set
+ int *owners permitted owners of the file(s)
+ int *owngroups permitted group owners of the file(s)
+ uschar **errmsg where to put an error message if there is a problem
+
+In the owners and owngroups vectors, the first element is the count of the
+remaining elements. There is a common function that can be called to check
+a file:
+
+int search_check_file(int fd, char *filename, int modemask, int *owners,
+ int *owngroups, char *type, char **errmsg);
+
+If fd is >= 0, it is checked using fstat(), and filename is used only in
+error messages. If fd is < 0 then filename is checked using stat(). The yield
+is zero if all is well, +1 if the mode or uid or gid is wrong, or -1 if the
+stat() fails.
+
+The yield of xxx_check() is TRUE if all is well, FALSE otherwise. The
+function should not close the file(s) on failure. That is done from outside
+by calling the xxx_close() function.
+
+
+xxx_find()
+----------
+
+This is called to search an open file/database. The result is OK, FAIL, or
+DEFER. The arguments are:
+
+ void *handle the handle passed back from xxx_open()
+ uschar *filename the filename passed to xxx_open() (NULL for querystyle)
+ uschar *keyquery the key to look up, or query to process, zero-terminated
+ int length the length of the key
+ uschar **result point to the yield, in dynamic store, on success
+ uschar **errmsg where to put an error message on failure;
+ this is initially set to "", and should be left
+ as that for a standard "entry not found" error
+ uint *do_cache the lookup should set this to 0 when it changes data.
+ This is MAXINT by default. When set to 0 the cache tree
+ of the current search handle will be cleaned and the
+ current result will NOT be cached. Currently the mysql
+ and pgsql lookups use this when UPDATE/INSERT queries are
+ executed.
+ If set to a nonzero number of seconds, the cached value
+ becomes unusable after this time. Currently the dnsdb
+ lookup uses this to support the TTL value.
+ uschar *opts options, a comma-separated list of tagged values for
+ modifying the search operation
+
+Even though the key is zero-terminated, the length is passed because in the
+common case it has been computed already and is often needed.
+
+
+xxx_close()
+-----------
+
+This is called for single-key lookups when a file is finished with. There is no
+yield, and the only argument is the handle that was passed back from
+xxx_open(). It is not called for query style lookups.
+
+
+xxx_tidy()
+----------
+
+This function is called once at the end of search_tidyup() for every lookup
+type for which it exists.
+
+
+xxx_quote()
+-----------
+
+This is called by the string expansion code for expansions of the form
+${quote_xxx:<string>}, if it exists. If not, the expansion code makes no change
+to the string. The function must apply any quoting rules that are specific to
+the lookup, and return a pointer to the revised string. If quoting is not
+needed, it can return its single argument, which is a uschar *. This function
+does NOT use the POOL_SEARCH store, because it's usually never called from any
+lookup code.
+
+xxx_version_report()
+--------------------
+
+This is called to report diagnostic information to a file stream.
+Typically it would report both compile-time and run-time version information.
+The arguments are:
+
+ FILE *stream where to fprintf() the data to
+
+
+****
diff --git a/src/lookups/cdb.c b/src/lookups/cdb.c
new file mode 100644
index 0000000..966078f
--- /dev/null
+++ b/src/lookups/cdb.c
@@ -0,0 +1,490 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/*
+ * Exim - CDB database lookup module
+ * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ *
+ * Copyright (c) The Exim Maintainers 2020 - 2022
+ * Copyright (c) 1998 Nigel Metheringham, Planet Online Ltd
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * --------------------------------------------------------------
+ * Modified by PH for Exim 4:
+ * Changed over to using unsigned chars
+ * Makes use of lf_check_file() for file checking
+ * --------------------------------------------------------------
+ * Modified by The Exim Maintainers 2015:
+ * const propagation
+ * --------------------------------------------------------------
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
+ * 02111-1307, USA.
+ *
+ *
+ * This code implements Dan Bernstein's Constant DataBase (cdb) spec.
+ * Information, the spec and sample code for cdb can be obtained from
+ * http://www.pobox.com/~djb/cdb.html
+ *
+ * This implementation borrows some code from Dan Bernstein's
+ * implementation (which has no license restrictions applied to it).
+ * This (read-only) implementation is completely contained within
+ * cdb.[ch] it does *not* link against an external cdb library.
+ *
+ *
+ * There are 2 variants included within this code. One uses MMAP and
+ * should give better performance especially for multiple lookups on a
+ * modern machine. The other is the default implementation which is
+ * used in the case where the MMAP fails or if MMAP was not compiled
+ * in. this implementation is the same as the original reference cdb
+ * implementation. The MMAP version is compiled in if the HAVE_MMAP
+ * preprocessor define is defined - this should be set in the system
+ * specific os.h file.
+ *
+ */
+
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+#ifdef HAVE_MMAP
+# include <sys/mman.h>
+/* Not all implementations declare MAP_FAILED */
+# ifndef MAP_FAILED
+# define MAP_FAILED ((void *) -1)
+# endif /* MAP_FAILED */
+#endif /* HAVE_MMAP */
+
+
+#define CDB_HASH_SPLIT 256 /* num pieces the hash table is split into */
+#define CDB_HASH_MASK 255 /* mask to and off split value */
+#define CDB_HASH_ENTRY 8 /* how big each offset it */
+#define CDB_HASH_TABLE (CDB_HASH_SPLIT * CDB_HASH_ENTRY)
+
+/* State information for cdb databases that are open NB while the db
+ * is open its contents will not change (cdb dbs are normally updated
+ * atomically by renaming). However the lifetime of one of these
+ * state structures should be limited - ie a long running daemon
+ * that opens one may hit problems....
+ */
+
+struct cdb_state {
+ int fileno;
+ off_t filelen;
+ uschar *cdb_map;
+ uschar *cdb_offsets;
+};
+
+/* 32 bit unsigned type - this is an int on all modern machines */
+typedef unsigned int uint32;
+
+/*
+ * cdb_hash()
+ * Internal function to make hash value */
+
+static uint32
+cdb_hash(const uschar *buf, unsigned int len)
+{
+ uint32 h;
+
+ h = 5381;
+ while (len) {
+ --len;
+ h += (h << 5);
+ h ^= (uint32) *buf++;
+ }
+ return h;
+}
+
+/*
+ * cdb_bread()
+ * Internal function to read len bytes from disk, coping with oddities */
+
+static int
+cdb_bread(int fd,
+ uschar *buf,
+ int len)
+{
+ int r;
+ while (len > 0) {
+ do
+ r = Uread(fd,buf,len);
+ while ((r == -1) && (errno == EINTR));
+ if (r == -1) return -1;
+ if (r == 0) { errno = EIO; return -1; }
+ buf += r;
+ len -= r;
+ }
+ return 0;
+}
+
+/*
+ * cdb_bread()
+ * Internal function to parse 4 byte number (endian independent) */
+
+static uint32
+cdb_unpack(uschar *buf)
+{
+uint32 num;
+num = buf[3]; num <<= 8;
+num += buf[2]; num <<= 8;
+num += buf[1]; num <<= 8;
+num += buf[0];
+return num;
+}
+
+static void cdb_close(void *handle);
+
+static void *
+cdb_open(const uschar * filename, uschar ** errmsg)
+{
+int fileno;
+struct cdb_state *cdbp;
+struct stat statbuf;
+void * mapbuf;
+
+if ((fileno = Uopen(filename, O_RDONLY, 0)) < 0)
+ {
+ *errmsg = string_open_failed("%s for cdb lookup", filename);
+ return NULL;
+ }
+
+if (fstat(fileno, &statbuf) != 0)
+ {
+ *errmsg = string_open_failed("fstat(%s) failed - cannot do cdb lookup",
+ filename);
+ return NULL;
+ }
+
+/* If this is a valid file, then it *must* be at least
+CDB_HASH_TABLE bytes long */
+
+if (statbuf.st_size < CDB_HASH_TABLE)
+ {
+ *errmsg = string_open_failed("%s too short for cdb lookup", filename);
+ return NULL;
+ }
+
+/* Having got a file open we need the structure to put things in */
+cdbp = store_get(sizeof(struct cdb_state), GET_UNTAINTED);
+/* store_get() does not return if memory was not available... */
+/* preload the structure.... */
+cdbp->fileno = fileno;
+cdbp->filelen = statbuf.st_size;
+cdbp->cdb_map = NULL;
+cdbp->cdb_offsets = NULL;
+
+/* if we are allowed to we use mmap here.... */
+#ifdef HAVE_MMAP
+if ((mapbuf = mmap(NULL, statbuf.st_size, PROT_READ, MAP_SHARED, fileno, 0))
+ != MAP_FAILED)
+ {
+ /* We have an mmap-ed section. Now we can just use it */
+ cdbp->cdb_map = mapbuf;
+ /* The offsets can be set to the same value since they should
+ * effectively be cached as well
+ */
+ cdbp->cdb_offsets = mapbuf;
+
+ /* Now return the state struct */
+ return(cdbp);
+ }
+
+/* If we got here the map failed. Basically we can ignore this since we fall
+back to slower methods.... However lets debug log it... */
+
+DEBUG(D_lookup) debug_printf_indent("cdb mmap failed - %d\n", errno);
+#endif /* HAVE_MMAP */
+
+/* In this case we have either not got MMAP allowed, or it failed */
+
+/* get a buffer to stash the basic offsets in - this should speed
+things up a lot - especially on multiple lookups */
+
+cdbp->cdb_offsets = store_get(CDB_HASH_TABLE, GET_UNTAINTED);
+
+/* now fill the buffer up... */
+
+if (cdb_bread(fileno, cdbp->cdb_offsets, CDB_HASH_TABLE) == -1)
+ {
+ /* read of hash table failed, oh dear, oh..... time to give up I think....
+ call the close routine (deallocs the memory), and return NULL */
+
+ *errmsg = string_open_failed("cannot read header from %s for cdb lookup",
+ filename);
+ cdb_close(cdbp);
+ return NULL;
+ }
+
+/* Everything else done - return the cache structure */
+return cdbp;
+}
+
+
+
+/*************************************************
+* Check entry point *
+*************************************************/
+
+static BOOL
+cdb_check(void * handle, const uschar * filename, int modemask,
+ uid_t * owners, gid_t * owngroups, uschar ** errmsg)
+{
+struct cdb_state * cdbp = handle;
+return lf_check_file(cdbp->fileno, filename, S_IFREG, modemask,
+ owners, owngroups, "cdb", errmsg) == 0;
+}
+
+
+
+/*************************************************
+* Find entry point *
+*************************************************/
+
+static int
+cdb_find(void * handle, const uschar * filename, const uschar * keystring,
+ int key_len, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+struct cdb_state * cdbp = handle;
+uint32 item_key_len,
+item_dat_len,
+key_hash,
+item_hash,
+item_posn,
+cur_offset,
+end_offset,
+hash_offset_entry,
+hash_offset,
+hash_offlen,
+hash_slotnm;
+
+key_hash = cdb_hash(keystring, key_len);
+
+hash_offset_entry = CDB_HASH_ENTRY * (key_hash & CDB_HASH_MASK);
+hash_offset = cdb_unpack(cdbp->cdb_offsets + hash_offset_entry);
+hash_offlen = cdb_unpack(cdbp->cdb_offsets + hash_offset_entry + 4);
+
+/* If the offset length is zero this key cannot be in the file */
+
+if (hash_offlen == 0)
+ return FAIL;
+
+hash_slotnm = (key_hash >> 8) % hash_offlen;
+
+/* check to ensure that the file is not corrupt
+ * if the hash_offset + (hash_offlen * CDB_HASH_ENTRY) is longer
+ * than the file, then we have problems.... */
+
+if ((hash_offset + (hash_offlen * CDB_HASH_ENTRY)) > cdbp->filelen)
+ {
+ *errmsg = string_sprintf("cdb: corrupt cdb file %s (too short)",
+ filename);
+ DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
+ return DEFER;
+ }
+
+cur_offset = hash_offset + (hash_slotnm * CDB_HASH_ENTRY);
+end_offset = hash_offset + (hash_offlen * CDB_HASH_ENTRY);
+
+/* if we are allowed to we use mmap here.... */
+
+#ifdef HAVE_MMAP
+/* make sure the mmap was OK */
+if (cdbp->cdb_map != NULL)
+ {
+ uschar * cur_pos = cur_offset + cdbp->cdb_map;
+ uschar * end_pos = end_offset + cdbp->cdb_map;
+
+ for (int loop = 0; (loop < hash_offlen); ++loop)
+ {
+ item_hash = cdb_unpack(cur_pos);
+ cur_pos += 4;
+ item_posn = cdb_unpack(cur_pos);
+ cur_pos += 4;
+
+ /* if the position is zero then we have a definite miss */
+
+ if (item_posn == 0)
+ return FAIL;
+
+ if (item_hash == key_hash)
+ { /* matching hash value */
+ uschar * item_ptr = cdbp->cdb_map + item_posn;
+
+ item_key_len = cdb_unpack(item_ptr);
+ item_ptr += 4;
+ item_dat_len = cdb_unpack(item_ptr);
+ item_ptr += 4;
+
+ /* check key length matches */
+
+ if (item_key_len == key_len)
+ {
+ /* finally check if key matches */
+ if (Ustrncmp(keystring, item_ptr, key_len) == 0)
+ {
+ /* we have a match.... * make item_ptr point to data */
+
+ item_ptr += item_key_len;
+
+ /* ... and the returned result. Assume it is not
+ tainted, lacking any way of telling. */
+
+ *result = store_get(item_dat_len + 1, GET_UNTAINTED);
+ memcpy(*result, item_ptr, item_dat_len);
+ (*result)[item_dat_len] = 0;
+ return OK;
+ }
+ }
+ }
+ /* handle warp round of table */
+ if (cur_pos == end_pos)
+ cur_pos = cdbp->cdb_map + hash_offset;
+ }
+ /* looks like we failed... */
+ return FAIL;
+ }
+
+#endif /* HAVE_MMAP */
+
+for (int loop = 0; (loop < hash_offlen); ++loop)
+ {
+ uschar packbuf[8];
+
+ if (lseek(cdbp->fileno, (off_t) cur_offset, SEEK_SET) == -1) return DEFER;
+ if (cdb_bread(cdbp->fileno, packbuf, 8) == -1) return DEFER;
+
+ item_hash = cdb_unpack(packbuf);
+ item_posn = cdb_unpack(packbuf + 4);
+
+ /* if the position is zero then we have a definite miss */
+
+ if (item_posn == 0)
+ return FAIL;
+
+ if (item_hash == key_hash)
+ { /* matching hash value */
+ if (lseek(cdbp->fileno, (off_t) item_posn, SEEK_SET) == -1) return DEFER;
+ if (cdb_bread(cdbp->fileno, packbuf, 8) == -1) return DEFER;
+
+ item_key_len = cdb_unpack(packbuf);
+
+ /* check key length matches */
+
+ if (item_key_len == key_len)
+ { /* finally check if key matches */
+ rmark reset_point = store_mark();
+ uschar * item_key = store_get(key_len, GET_TAINTED); /* keys liable to be tainted */
+
+ if (cdb_bread(cdbp->fileno, item_key, key_len) == -1) return DEFER;
+ if (Ustrncmp(keystring, item_key, key_len) == 0)
+ {
+ /* Reclaim some store */
+ store_reset(reset_point);
+
+ /* matches - get data length */
+ item_dat_len = cdb_unpack(packbuf + 4);
+
+ /* then we build a new result string. We know we have enough
+ memory so disable Coverity errors about the tainted item_dat_ken */
+
+ *result = store_get(item_dat_len + 1, GET_UNTAINTED);
+ /* coverity[tainted_data] */
+ if (cdb_bread(cdbp->fileno, *result, item_dat_len) == -1)
+ return DEFER;
+
+ /* coverity[tainted_data] */
+ (*result)[item_dat_len] = 0;
+ return OK;
+ }
+ /* Reclaim some store */
+ store_reset(reset_point);
+ }
+ }
+ cur_offset += 8;
+
+ /* handle warp round of table */
+ if (cur_offset == end_offset)
+ cur_offset = hash_offset;
+ }
+return FAIL;
+}
+
+
+
+/*************************************************
+* Close entry point *
+*************************************************/
+
+/* See local README for interface description */
+
+static void
+cdb_close(void *handle)
+{
+struct cdb_state * cdbp = handle;
+
+#ifdef HAVE_MMAP
+if (cdbp->cdb_map)
+ {
+ munmap(CS cdbp->cdb_map, cdbp->filelen);
+ if (cdbp->cdb_map == cdbp->cdb_offsets)
+ cdbp->cdb_offsets = NULL;
+ }
+#endif /* HAVE_MMAP */
+
+(void)close(cdbp->fileno);
+}
+
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+cdb_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: CDB: Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+
+lookup_info cdb_lookup_info = {
+ .name = US"cdb", /* lookup name */
+ .type = lookup_absfile, /* absolute file name */
+ .open = cdb_open, /* open function */
+ .check = cdb_check, /* check function */
+ .find = cdb_find, /* find function */
+ .close = cdb_close, /* close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = cdb_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+#define cdb_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &cdb_lookup_info };
+lookup_module_info cdb_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* End of lookups/cdb.c */
diff --git a/src/lookups/dbmdb.c b/src/lookups/dbmdb.c
new file mode 100644
index 0000000..32514af
--- /dev/null
+++ b/src/lookups/dbmdb.c
@@ -0,0 +1,284 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description */
+
+static void *
+dbmdb_open(const uschar * filename, uschar ** errmsg)
+{
+uschar * dirname = string_copy(filename);
+uschar * s;
+EXIM_DB * yield = NULL;
+
+if ((s = Ustrrchr(dirname, '/'))) *s = '\0';
+if (!(yield = exim_dbopen(filename, dirname, O_RDONLY, 0)))
+ *errmsg = string_open_failed("%s as a %s file", filename, EXIM_DBTYPE);
+return yield;
+}
+
+
+
+/*************************************************
+* Check entry point *
+*************************************************/
+
+/* This needs to know more about the underlying files than is good for it!
+We need to know what the real file names are in order to check the owners and
+modes. If USE_DB is set, we know it is Berkeley DB, which uses an unmodified
+file name. If USE_TDB or USE_GDBM is set, we know it is tdb or gdbm, which do
+the same. Otherwise, for safety, we have to check for x.db or x.dir and x.pag.
+*/
+
+static BOOL
+dbmdb_check(void *handle, const uschar *filename, int modemask, uid_t *owners,
+ gid_t *owngroups, uschar **errmsg)
+{
+int rc;
+
+#if defined(USE_DB) || defined(USE_TDB) || defined(USE_GDBM)
+rc = lf_check_file(-1, filename, S_IFREG, modemask, owners, owngroups,
+ "dbm", errmsg);
+#else
+ {
+ uschar filebuffer[256];
+ (void)sprintf(CS filebuffer, "%.250s.db", filename);
+ rc = lf_check_file(-1, filebuffer, S_IFREG, modemask, owners, owngroups,
+ "dbm", errmsg);
+ if (rc < 0) /* stat() failed */
+ {
+ (void)sprintf(CS filebuffer, "%.250s.dir", filename);
+ rc = lf_check_file(-1, filebuffer, S_IFREG, modemask, owners, owngroups,
+ "dbm", errmsg);
+ if (rc == 0) /* x.dir was OK */
+ {
+ (void)sprintf(CS filebuffer, "%.250s.pag", filename);
+ rc = lf_check_file(-1, filebuffer, S_IFREG, modemask, owners, owngroups,
+ "dbm", errmsg);
+ }
+ }
+ }
+#endif
+
+return rc == 0;
+}
+
+
+
+/*************************************************
+* Find entry point *
+*************************************************/
+
+/* See local README for interface description. This function adds 1 to
+the keylength in order to include the terminating zero. */
+
+static int
+dbmdb_find(void * handle, const uschar * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+EXIM_DB *d = (EXIM_DB *)handle;
+EXIM_DATUM key, data;
+
+exim_datum_init(&key); /* Some DBM libraries require datums to */
+exim_datum_init(&data); /* be cleared before use. */
+length++;
+exim_datum_data_set(&key,
+ memcpy(store_get(length, keystring), keystring, length)); /* key can have embedded NUL */
+exim_datum_size_set(&key, length);
+
+if (exim_dbget(d, &key, &data))
+ {
+ *result = string_copyn(exim_datum_data_get(&data), exim_datum_size_get(&data));
+ exim_datum_free(&data); /* Some DBM libraries need a free() call */
+ return OK;
+ }
+return FAIL;
+}
+
+
+
+/*************************************************
+* Find entry point - no zero on key *
+*************************************************/
+
+/* See local README for interface description */
+
+static int
+dbmnz_find(void * handle, const uschar * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+return dbmdb_find(handle, filename, keystring, length-1, result, errmsg,
+ do_cache, opts);
+}
+
+
+
+/*************************************************
+* Find entry point - zero-joined list key *
+*************************************************/
+
+/*
+ * The parameter passed as a key is a list in normal Exim list syntax.
+ * The elements of that list are joined together on NUL, with no trailing
+ * NUL, to form the key.
+ */
+
+static int
+dbmjz_find(void * handle, const uschar * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+uschar *key_item, *key_buffer, *key_p;
+const uschar *key_elems = keystring;
+int buflen, bufleft, key_item_len, sep = 0;
+
+/* To a first approximation, the size of the lookup key needs to be about,
+or less than, the length of the delimited list passed in + 1. */
+
+buflen = length + 3;
+key_buffer = store_get(buflen, keystring);
+
+key_buffer[0] = '\0';
+
+key_p = key_buffer;
+bufleft = buflen;
+
+/* In all cases of an empty list item, we can set 1 and advance by 1 and then
+pick up the trailing NUL from the previous list item, EXCEPT when at the
+beginning of the output string, in which case we need to supply that NUL
+ourselves. */
+while ((key_item = string_nextinlist(&key_elems, &sep, key_p, bufleft)) != NULL)
+ {
+ key_item_len = Ustrlen(key_item) + 1;
+ if (key_item_len == 1)
+ {
+ key_p[0] = '\0';
+ if (key_p == key_buffer)
+ {
+ key_p[1] = '\0';
+ key_item_len += 1;
+ }
+ }
+
+ bufleft -= key_item_len;
+ if (bufleft <= 0)
+ {
+ /* The string_nextinlist() will stop at buffer size, but we should always
+ have at least 1 character extra, so some assumption has failed. */
+ *errmsg = string_copy(US"Ran out of buffer space for joining elements");
+ return DEFER;
+ }
+ key_p += key_item_len;
+ }
+
+if (key_p == key_buffer)
+ {
+ *errmsg = string_copy(US"empty list key");
+ return FAIL;
+ }
+
+/* We do not pass in the final NULL; if needed, the list should include an
+empty element to put one in. Boundary: key length 1, is a NULL */
+key_item_len = key_p - key_buffer - 1;
+
+DEBUG(D_lookup) debug_printf_indent("NUL-joined key length: %d\n", key_item_len);
+
+/* beware that dbmdb_find() adds 1 to length to get back terminating NUL, so
+because we've calculated the real length, we need to subtract one more here */
+
+return dbmdb_find(handle, filename, key_buffer, key_item_len - 1,
+ result, errmsg, do_cache, opts);
+}
+
+
+
+/*************************************************
+* Close entry point *
+*************************************************/
+
+/* See local README for interface description */
+
+void
+static dbmdb_close(void *handle)
+{
+exim_dbclose((EXIM_DB *)handle);
+}
+
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+dbm_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: DBM: Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+
+lookup_info dbm_lookup_info = {
+ .name = US"dbm", /* lookup name */
+ .type = lookup_absfile, /* uses absolute file name */
+ .open = dbmdb_open, /* open function */
+ .check = dbmdb_check, /* check function */
+ .find = dbmdb_find, /* find function */
+ .close = dbmdb_close, /* close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = dbm_version_report /* version reporting */
+};
+
+lookup_info dbmz_lookup_info = {
+ .name = US"dbmnz", /* lookup name */
+ .type = lookup_absfile, /* uses absolute file name */
+ .open = dbmdb_open, /* sic */ /* open function */
+ .check = dbmdb_check, /* sic */ /* check function */
+ .find = dbmnz_find, /* find function */
+ .close = dbmdb_close, /* sic */ /* close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = NULL /* no version reporting (redundant) */
+};
+
+lookup_info dbmjz_lookup_info = {
+ .name = US"dbmjz", /* lookup name */
+ .type = lookup_absfile, /* uses absolute file name */
+ .open = dbmdb_open, /* sic */ /* open function */
+ .check = dbmdb_check, /* sic */ /* check function */
+ .find = dbmjz_find, /* find function */
+ .close = dbmdb_close, /* sic */ /* close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = NULL /* no version reporting (redundant) */
+};
+
+#ifdef DYNLOOKUP
+#define dbmdb_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &dbm_lookup_info, &dbmz_lookup_info, &dbmjz_lookup_info };
+lookup_module_info dbmdb_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 3 };
+
+/* End of lookups/dbmdb.c */
diff --git a/src/lookups/dnsdb.c b/src/lookups/dnsdb.c
new file mode 100644
index 0000000..355be1b
--- /dev/null
+++ b/src/lookups/dnsdb.c
@@ -0,0 +1,614 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+
+
+/* Ancient systems (e.g. SunOS4) don't appear to have T_TXT defined in their
+header files. */
+
+#ifndef T_TXT
+# define T_TXT 16
+#endif
+
+/* Many systems do not have T_SPF. */
+#ifndef T_SPF
+# define T_SPF 99
+#endif
+
+/* New TLSA record for DANE */
+#ifndef T_TLSA
+# define T_TLSA 52
+#endif
+
+/* Table of recognized DNS record types and their integer values. */
+
+static const char *type_names[] = {
+ "a",
+#if HAVE_IPV6
+ "a+",
+ "aaaa",
+#endif
+ "cname",
+ "csa",
+ "mx",
+ "mxh",
+ "ns",
+ "ptr",
+ "soa",
+ "spf",
+ "srv",
+ "tlsa",
+ "txt",
+ "zns"
+};
+
+static int type_values[] = {
+ T_A,
+#if HAVE_IPV6
+ T_ADDRESSES, /* Private type for AAAA + A */
+ T_AAAA,
+#endif
+ T_CNAME,
+ T_CSA, /* Private type for "Client SMTP Authorization". */
+ T_MX,
+ T_MXH, /* Private type for "MX hostnames" */
+ T_NS,
+ T_PTR,
+ T_SOA,
+ T_SPF,
+ T_SRV,
+ T_TLSA,
+ T_TXT,
+ T_ZNS /* Private type for "zone nameservers" */
+};
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static void *
+dnsdb_open(const uschar * filename, uschar **errmsg)
+{
+return (void *)(-1); /* Any non-0 value */
+}
+
+
+
+/*************************************************
+* Find entry point for dnsdb *
+*************************************************/
+
+/* See local README for interface description. The query in the "keystring" may
+consist of a number of parts.
+
+(a) If the first significant character is '>' then the next character is the
+separator character that is used when multiple records are found. The default
+separator is newline.
+
+(b) If the next character is ',' then the next character is the separator
+character used for multiple items of text in "TXT" records. Alternatively,
+if the next character is ';' then these multiple items are concatenated with
+no separator. With neither of these options specified, only the first item
+is output. Similarly for "SPF" records, but the default for joining multiple
+items in one SPF record is the empty string, for direct concatenation.
+
+(c) Options, all comma-terminated, in any order. Any unrecognised option
+terminates option processing. Recognised options are:
+
+- 'defer_FOO': set the defer behaviour to FOO. The possible behaviours are:
+'strict', where any defer causes the whole lookup to defer; 'lax', where a defer
+causes the whole lookup to defer only if none of the DNS queries succeeds; and
+'never', where all defers are as if the lookup failed. The default is 'lax'.
+
+- 'dnssec_FOO', with 'strict', 'lax' (default), and 'never'. The meanings are
+require, try and don't-try dnssec respectively.
+
+- 'retrans_VAL', set the timeout value. VAL is an Exim time specification
+(eg "5s"). The default is set by the main configuration option 'dns_retrans'.
+
+- 'retry_VAL', set the retry count on timeouts. VAL is an integer. The
+default is set by the main configuration option "dns_retry".
+
+(d) If the next sequence of characters is a sequence of letters and digits
+followed by '=', it is interpreted as the name of the DNS record type. The
+default is "TXT".
+
+(e) Then there follows list of domain names. This is a generalized Exim list,
+which may start with '<' in order to set a specific separator. The default
+separator, as always, is colon. */
+
+static int
+dnsdb_find(void * handle, const uschar * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+int rc;
+int sep = 0;
+int defer_mode = PASS;
+int dnssec_mode = PASS;
+int save_retrans = dns_retrans;
+int save_retry = dns_retry;
+int type;
+int failrc = FAIL;
+const uschar *outsep = CUS"\n";
+const uschar *outsep2 = NULL;
+uschar *equals, *domain, *found;
+
+dns_answer * dnsa = store_get_dns_answer();
+dns_scan dnss;
+
+/* Because we're working in the search pool, we try to reclaim as much
+store as possible later, so we preallocate the result here */
+
+gstring * yield = string_get(256);
+
+/* If the string starts with '>' we change the output separator.
+If it's followed by ';' or ',' we set the TXT output separator. */
+
+while (isspace(*keystring)) keystring++;
+if (*keystring == '>')
+ {
+ outsep = keystring + 1;
+ keystring += 2;
+ if (*keystring == ',')
+ {
+ outsep2 = keystring + 1;
+ keystring += 2;
+ }
+ else if (*keystring == ';')
+ {
+ outsep2 = US"";
+ keystring++;
+ }
+ while (isspace(*keystring)) keystring++;
+ }
+
+/* Check for a modifier keyword. */
+
+for (;;)
+ {
+ if (strncmpic(keystring, US"defer_", 6) == 0)
+ {
+ keystring += 6;
+ if (strncmpic(keystring, US"strict", 6) == 0)
+ { defer_mode = DEFER; keystring += 6; }
+ else if (strncmpic(keystring, US"lax", 3) == 0)
+ { defer_mode = PASS; keystring += 3; }
+ else if (strncmpic(keystring, US"never", 5) == 0)
+ { defer_mode = OK; keystring += 5; }
+ else
+ {
+ *errmsg = US"unsupported dnsdb defer behaviour";
+ rc = DEFER;
+ goto out;
+ }
+ }
+ else if (strncmpic(keystring, US"dnssec_", 7) == 0)
+ {
+ keystring += 7;
+ if (strncmpic(keystring, US"strict", 6) == 0)
+ { dnssec_mode = DEFER; keystring += 6; }
+ else if (strncmpic(keystring, US"lax", 3) == 0)
+ { dnssec_mode = PASS; keystring += 3; }
+ else if (strncmpic(keystring, US"never", 5) == 0)
+ { dnssec_mode = OK; keystring += 5; }
+ else
+ {
+ *errmsg = US"unsupported dnsdb dnssec behaviour";
+ rc = DEFER;
+ goto out;
+ }
+ }
+ else if (strncmpic(keystring, US"retrans_", 8) == 0)
+ {
+ int timeout_sec;
+ if ((timeout_sec = readconf_readtime(keystring += 8, ',', FALSE)) <= 0)
+ {
+ *errmsg = US"unsupported dnsdb timeout value";
+ rc = DEFER;
+ goto out;
+ }
+ dns_retrans = timeout_sec;
+ while (*keystring != ',') keystring++;
+ }
+ else if (strncmpic(keystring, US"retry_", 6) == 0)
+ {
+ int retries;
+ if ((retries = (int)strtol(CCS keystring + 6, CSS &keystring, 0)) < 0)
+ {
+ *errmsg = US"unsupported dnsdb retry count";
+ rc = DEFER;
+ goto out;
+ }
+ dns_retry = retries;
+ }
+ else
+ break;
+
+ while (isspace(*keystring)) keystring++;
+ if (*keystring++ != ',')
+ {
+ *errmsg = US"dnsdb modifier syntax error";
+ rc = DEFER;
+ goto out;
+ }
+ while (isspace(*keystring)) keystring++;
+ }
+
+/* Figure out the "type" value if it is not T_TXT.
+If the keystring contains an = this must be preceded by a valid type name. */
+
+type = T_TXT;
+if ((equals = Ustrchr(keystring, '=')) != NULL)
+ {
+ int i, len;
+ uschar *tend = equals;
+
+ while (tend > keystring && isspace(tend[-1])) tend--;
+ len = tend - keystring;
+
+ for (i = 0; i < nelem(type_names); i++)
+ if (len == Ustrlen(type_names[i]) &&
+ strncmpic(keystring, US type_names[i], len) == 0)
+ {
+ type = type_values[i];
+ break;
+ }
+
+ if (i >= nelem(type_names))
+ {
+ *errmsg = US"unsupported DNS record type";
+ rc = DEFER;
+ goto out;
+ }
+
+ keystring = equals + 1;
+ while (isspace(*keystring)) keystring++;
+ }
+
+/* Initialize the resolver in case this is the first time it has been used. */
+
+dns_init(FALSE, FALSE, dnssec_mode != OK);
+
+/* The remainder of the string must be a list of domains. As long as the lookup
+for at least one of them succeeds, we return success. Failure means that none
+of them were found.
+
+The original implementation did not support a list of domains. Adding the list
+feature is compatible, except in one case: when PTR records are being looked up
+for a single IPv6 address. Fortunately, we can hack in a compatibility feature
+here: If the type is PTR and no list separator is specified, and the entire
+remaining string is valid as an IP address, set an impossible separator so that
+it is treated as one item. */
+
+if (type == T_PTR && keystring[0] != '<' &&
+ string_is_ip_address(keystring, NULL) != 0)
+ sep = -1;
+
+/* SPF strings should be concatenated without a separator, thus make
+it the default if not defined (see RFC 4408 section 3.1.3).
+Multiple SPF records are forbidden (section 3.1.2) but are currently
+not handled specially, thus they are concatenated with \n by default.
+MX priority and value are space-separated by default.
+SRV and TLSA record parts are space-separated by default. */
+
+if (!outsep2) switch(type)
+ {
+ case T_SPF: outsep2 = US""; break;
+ case T_SRV: case T_MX: case T_TLSA: outsep2 = US" "; break;
+ }
+
+/* Now scan the list and do a lookup for each item */
+
+while ((domain = string_nextinlist(&keystring, &sep, NULL, 0)))
+ {
+ int searchtype = type == T_CSA ? T_SRV : /* record type we want */
+ type == T_MXH ? T_MX :
+ type == T_ZNS ? T_NS : type;
+
+ /* If the type is PTR or CSA, we have to construct the relevant magic lookup
+ key if the original is an IP address (some experimental protocols are using
+ PTR records for different purposes where the key string is a host name, and
+ Exim's extended CSA can be keyed by domains or IP addresses). This code for
+ doing the reversal is now in a separate function. */
+
+ if ((type == T_PTR || type == T_CSA) &&
+ string_is_ip_address(domain, NULL) != 0)
+ domain = dns_build_reverse(domain);
+
+ do
+ {
+ DEBUG(D_lookup) debug_printf_indent("dnsdb key: %s\n", domain);
+
+ /* Do the lookup and sort out the result. There are four special types that
+ are handled specially: T_CSA, T_ZNS, T_ADDRESSES and T_MXH.
+ The first two are handled in a special lookup function so that the facility
+ could be used from other parts of the Exim code. T_ADDRESSES is handled by looping
+ over the types of A lookup. T_MXH affects only what happens later on in
+ this function, but for tidiness it is handled by the "special". If the
+ lookup fails, continue with the next domain. In the case of DEFER, adjust
+ the final "nothing found" result, but carry on to the next domain. */
+
+ found = domain;
+#if HAVE_IPV6
+ if (type == T_ADDRESSES) /* NB cannot happen unless HAVE_IPV6 */
+ {
+ if (searchtype == T_ADDRESSES) searchtype = T_AAAA;
+ else if (searchtype == T_AAAA) searchtype = T_A;
+ rc = dns_special_lookup(dnsa, domain, searchtype, CUSS &found);
+ }
+ else
+#endif
+ rc = dns_special_lookup(dnsa, domain, type, CUSS &found);
+
+ lookup_dnssec_authenticated = dnssec_mode==OK ? NULL
+ : dns_is_secure(dnsa) ? US"yes" : US"no";
+
+ if (rc == DNS_NOMATCH || rc == DNS_NODATA) continue;
+ if ( rc != DNS_SUCCEED
+ || (dnssec_mode == DEFER && !dns_is_secure(dnsa))
+ )
+ {
+ if (defer_mode == DEFER)
+ {
+ dns_retrans = save_retrans;
+ dns_retry = save_retry;
+ dns_init(FALSE, FALSE, FALSE); /* clr dnssec bit */
+ rc = DEFER; /* always defer */
+ goto out;
+ }
+ if (defer_mode == PASS) failrc = DEFER; /* defer only if all do */
+ continue; /* treat defer as fail */
+ }
+
+
+ /* Search the returned records */
+
+ for (dns_record * rr = dns_next_rr(dnsa, &dnss, RESET_ANSWERS); rr;
+ rr = dns_next_rr(dnsa, &dnss, RESET_NEXT)) if (rr->type == searchtype)
+ {
+ if (*do_cache > rr->ttl)
+ *do_cache = rr->ttl;
+
+ if (type == T_A || type == T_AAAA || type == T_ADDRESSES)
+ {
+ for (dns_address * da = dns_address_from_rr(dnsa, rr); da; da = da->next)
+ {
+ if (yield->ptr) yield = string_catn(yield, outsep, 1);
+ yield = string_cat(yield, da->address);
+ }
+ continue;
+ }
+
+ /* Other kinds of record just have one piece of data each, but there may be
+ several of them, of course. */
+
+ if (yield->ptr) yield = string_catn(yield, outsep, 1);
+
+ if (type == T_TXT || type == T_SPF)
+ {
+ if (outsep2 == NULL) /* output only the first item of data */
+ yield = string_catn(yield, US (rr->data+1), (rr->data)[0]);
+ else
+ {
+ /* output all items */
+ int data_offset = 0;
+ while (data_offset < rr->size)
+ {
+ uschar chunk_len = (rr->data)[data_offset++];
+ if (outsep2[0] != '\0' && data_offset != 1)
+ yield = string_catn(yield, outsep2, 1);
+ yield = string_catn(yield, US ((rr->data)+data_offset), chunk_len);
+ data_offset += chunk_len;
+ }
+ }
+ }
+ else if (type == T_TLSA)
+ {
+ uint8_t usage, selector, matching_type;
+ uint16_t payload_length;
+ uschar s[MAX_TLSA_EXPANDED_SIZE];
+ uschar * sp = s;
+ uschar * p = US rr->data;
+
+ usage = *p++;
+ selector = *p++;
+ matching_type = *p++;
+ /* What's left after removing the first 3 bytes above */
+ payload_length = rr->size - 3;
+ sp += sprintf(CS s, "%d%c%d%c%d%c", usage, *outsep2,
+ selector, *outsep2, matching_type, *outsep2);
+ /* Now append the cert/identifier, one hex char at a time */
+ while (payload_length-- > 0 && sp-s < (MAX_TLSA_EXPANDED_SIZE - 4))
+ sp += sprintf(CS sp, "%02x", *p++);
+
+ yield = string_cat(yield, s);
+ }
+ else /* T_CNAME, T_CSA, T_MX, T_MXH, T_NS, T_PTR, T_SOA, T_SRV */
+ {
+ int priority, weight, port;
+ uschar s[264];
+ uschar * p = US rr->data;
+
+ switch (type)
+ {
+ case T_MXH:
+ /* mxh ignores the priority number and includes only the hostnames */
+ GETSHORT(priority, p);
+ break;
+
+ case T_MX:
+ GETSHORT(priority, p);
+ sprintf(CS s, "%d%c", priority, *outsep2);
+ yield = string_cat(yield, s);
+ break;
+
+ case T_SRV:
+ GETSHORT(priority, p);
+ GETSHORT(weight, p);
+ GETSHORT(port, p);
+ sprintf(CS s, "%d%c%d%c%d%c", priority, *outsep2,
+ weight, *outsep2, port, *outsep2);
+ yield = string_cat(yield, s);
+ break;
+
+ case T_CSA:
+ /* See acl_verify_csa() for more comments about CSA. */
+ GETSHORT(priority, p);
+ GETSHORT(weight, p);
+ GETSHORT(port, p);
+
+ if (priority != 1) continue; /* CSA version must be 1 */
+
+ /* If the CSA record we found is not the one we asked for, analyse
+ the subdomain assertions in the port field, else analyse the direct
+ authorization status in the weight field. */
+
+ if (Ustrcmp(found, domain) != 0)
+ {
+ if (port & 1) *s = 'X'; /* explicit authorization required */
+ else *s = '?'; /* no subdomain assertions here */
+ }
+ else
+ {
+ if (weight < 2) *s = 'N'; /* not authorized */
+ else if (weight == 2) *s = 'Y'; /* authorized */
+ else if (weight == 3) *s = '?'; /* unauthorizable */
+ else continue; /* invalid */
+ }
+
+ s[1] = ' ';
+ yield = string_catn(yield, s, 2);
+ break;
+
+ default:
+ break;
+ }
+
+ /* GETSHORT() has advanced the pointer to the target domain. */
+
+ rc = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, p,
+ (DN_EXPAND_ARG4_TYPE)s, sizeof(s));
+
+ /* If an overlong response was received, the data will have been
+ truncated and dn_expand may fail. */
+
+ if (rc < 0)
+ {
+ log_write(0, LOG_MAIN, "host name alias list truncated: type=%s "
+ "domain=%s", dns_text_type(type), domain);
+ break;
+ }
+ else yield = string_cat(yield, s);
+
+ if (type == T_SOA && outsep2 != NULL)
+ {
+ unsigned long serial, refresh, retry, expire, minimum;
+
+ p += rc;
+ yield = string_catn(yield, outsep2, 1);
+
+ rc = dn_expand(dnsa->answer, dnsa->answer + dnsa->answerlen, p,
+ (DN_EXPAND_ARG4_TYPE)s, sizeof(s));
+ if (rc < 0)
+ {
+ log_write(0, LOG_MAIN, "responsible-mailbox truncated: type=%s "
+ "domain=%s", dns_text_type(type), domain);
+ break;
+ }
+ else yield = string_cat(yield, s);
+
+ p += rc;
+ GETLONG(serial, p); GETLONG(refresh, p);
+ GETLONG(retry, p); GETLONG(expire, p); GETLONG(minimum, p);
+ sprintf(CS s, "%c%lu%c%lu%c%lu%c%lu%c%lu",
+ *outsep2, serial, *outsep2, refresh,
+ *outsep2, retry, *outsep2, expire, *outsep2, minimum);
+ yield = string_cat(yield, s);
+ }
+ }
+ } /* Loop for list of returned records */
+
+ /* Loop for set of A-lookup types */
+ } while (type == T_ADDRESSES && searchtype != T_A);
+
+ } /* Loop for list of domains */
+
+/* Reclaim unused memory */
+
+gstring_release_unused(yield);
+
+/* If yield NULL we have not found anything. Otherwise, insert the terminating
+zero and return the result. */
+
+dns_retrans = save_retrans;
+dns_retry = save_retry;
+dns_init(FALSE, FALSE, FALSE); /* clear the dnssec bit for getaddrbyname */
+
+if (!yield || !yield->ptr)
+ rc = failrc;
+else
+ {
+ *result = string_from_gstring(yield);
+ rc = OK;
+ }
+
+out:
+
+store_free_dns_answer(dnsa);
+return rc;
+}
+
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+dnsdb_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: DNSDB: Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+
+static lookup_info _lookup_info = {
+ .name = US"dnsdb", /* lookup name */
+ .type = lookup_querystyle, /* query style */
+ .open = dnsdb_open, /* open function */
+ .check = NULL, /* check function */
+ .find = dnsdb_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = dnsdb_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+#define dnsdb_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &_lookup_info };
+lookup_module_info dnsdb_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* vi: aw ai sw=2
+*/
+/* End of lookups/dnsdb.c */
diff --git a/src/lookups/dsearch.c b/src/lookups/dsearch.c
new file mode 100644
index 0000000..a769102
--- /dev/null
+++ b/src/lookups/dsearch.c
@@ -0,0 +1,190 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2015 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* The idea for this code came from Matthew Byng-Maddick, but his original has
+been heavily reworked a lot for Exim 4 (and it now uses stat() (more precisely:
+lstat()) rather than a directory scan). */
+
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description. We open the directory to test
+whether it exists and whether it is searchable. However, we don't need to keep
+it open, because the "search" can be done by a call to lstat() rather than
+actually scanning through the list of files. */
+
+static void *
+dsearch_open(const uschar * dirname, uschar ** errmsg)
+{
+DIR * dp = exim_opendir(dirname);
+if (!dp)
+ {
+ *errmsg = string_open_failed("%s for directory search", dirname);
+ return NULL;
+ }
+closedir(dp);
+return (void *)(-1);
+}
+
+
+/*************************************************
+* Check entry point *
+*************************************************/
+
+/* The handle will always be (void *)(-1), but don't try casting it to an
+integer as this gives warnings on 64-bit systems. */
+
+static BOOL
+dsearch_check(void * handle, const uschar * filename, int modemask,
+ uid_t * owners, gid_t * owngroups, uschar ** errmsg)
+{
+handle = handle;
+if (*filename == '/')
+ return lf_check_file(-1, filename, S_IFDIR, modemask, owners, owngroups,
+ "dsearch", errmsg) == 0;
+*errmsg = string_sprintf("dirname '%s' for dsearch is not absolute", filename);
+return FALSE;
+}
+
+
+/*************************************************
+* Find entry point *
+*************************************************/
+
+#define RET_FULL BIT(0)
+#define FILTER_TYPE BIT(1)
+#define FILTER_ALL BIT(1)
+#define FILTER_FILE BIT(2)
+#define FILTER_DIR BIT(3)
+#define FILTER_SUBDIR BIT(4)
+
+/* See local README for interface description. We use lstat() instead of
+scanning the directory, as it is hopefully faster to let the OS do the scanning
+for us. */
+
+static int
+dsearch_find(void * handle, const uschar * dirname, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+struct stat statbuf;
+int save_errno;
+uschar * filename;
+unsigned flags = 0;
+
+if (Ustrchr(keystring, '/') != 0)
+ {
+ *errmsg = string_sprintf("key for dsearch lookup contains a slash: %s",
+ keystring);
+ return DEFER;
+ }
+
+if (opts)
+ {
+ int sep = ',';
+ uschar * ele;
+
+ while ((ele = string_nextinlist(&opts, &sep, NULL, 0)))
+ if (Ustrcmp(ele, "ret=full") == 0)
+ flags |= RET_FULL;
+ else if (Ustrncmp(ele, "filter=", 7) == 0)
+ {
+ ele += 7;
+ if (Ustrcmp(ele, "file") == 0)
+ flags |= FILTER_TYPE | FILTER_FILE;
+ else if (Ustrcmp(ele, "dir") == 0)
+ flags |= FILTER_TYPE | FILTER_DIR;
+ else if (Ustrcmp(ele, "subdir") == 0)
+ flags |= FILTER_TYPE | FILTER_SUBDIR; /* like dir but not "." or ".." */
+ }
+ }
+
+filename = string_sprintf("%s/%s", dirname, keystring);
+if ( Ulstat(filename, &statbuf) >= 0
+ && ( !(flags & FILTER_TYPE)
+ || (flags & FILTER_FILE && S_ISREG(statbuf.st_mode))
+ || ( flags & (FILTER_DIR | FILTER_SUBDIR)
+ && S_ISDIR(statbuf.st_mode)
+ && ( flags & FILTER_DIR
+ || keystring[0] != '.'
+ || keystring[1] && keystring[1] != '.'
+ ) ) ) )
+ {
+ /* Since the filename exists in the filesystem, we can return a
+ non-tainted result. */
+ *result = string_copy_taint(flags & RET_FULL ? filename : keystring, GET_UNTAINTED);
+ return OK;
+ }
+
+if (errno == ENOENT || errno == 0) return FAIL;
+
+save_errno = errno;
+*errmsg = string_sprintf("%s: lstat: %s", filename, strerror(errno));
+errno = save_errno;
+return DEFER;
+}
+
+
+/*************************************************
+* Close entry point *
+*************************************************/
+
+/* See local README for interface description */
+
+void
+static dsearch_close(void *handle)
+{
+handle = handle; /* Avoid compiler warning */
+}
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+dsearch_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: dsearch: Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+
+static lookup_info _lookup_info = {
+ .name = US"dsearch", /* lookup name */
+ .type = lookup_absfile, /* uses absolute file name */
+ .open = dsearch_open, /* open function */
+ .check = dsearch_check, /* check function */
+ .find = dsearch_find, /* find function */
+ .close = dsearch_close, /* close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = dsearch_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+#define dsearch_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &_lookup_info };
+lookup_module_info dsearch_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* End of lookups/dsearch.c */
diff --git a/src/lookups/ibase.c b/src/lookups/ibase.c
new file mode 100644
index 0000000..c4fff71
--- /dev/null
+++ b/src/lookups/ibase.c
@@ -0,0 +1,561 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* 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;
+
+
+
+/*************************************************
+* 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) != NULL) {
+ 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(char *buffer, int buffer_size, XSQLVAR * var)
+{
+ if (buffer_size < var->sqllen)
+ return 0;
+
+ switch (var->sqltype & ~1) {
+ case SQL_VARYING:
+ strncpy(buffer, &var->sqldata[2], *(short *) var->sqldata);
+ return *(short *) var->sqldata;
+ case SQL_TEXT:
+ strncpy(buffer, var->sqldata, var->sqllen);
+ return var->sqllen;
+ case SQL_SHORT:
+ return sprintf(buffer, "%d", *(short *) var->sqldata);
+ case SQL_LONG:
+ return sprintf(buffer, "%ld", *(ISC_LONG *) var->sqldata);
+#ifdef SQL_INT64
+ case SQL_INT64:
+ return sprintf(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(uschar * query, uschar * server, uschar ** resultptr,
+ uschar ** errmsg, BOOL * defer_break)
+{
+isc_stmt_handle stmth = NULL;
+XSQLDA *out_sqlda;
+XSQLVAR *var;
+int i;
+rmark reset_point;
+
+char buffer[256];
+ISC_STATUS status[20], *statusp = status;
+
+gstring * result;
+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 == NULL)
+ {
+ *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 != NULL; 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), 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 = NULL;
+ cn->transh = NULL;
+ cn->next = ibase_connections;
+ ibase_connections = cn;
+ }
+
+/* If no cached connection, we must set one up. */
+
+if (cn->dbh == NULL || cn->transh == NULL)
+ {
+ char *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++ = strlen(sdata[1]);
+ for (char * p = sdata[1]; *p;)
+ *dpb++ = *p++;
+ *dpb++ = isc_dpb_password;
+ *dpb++ = strlen(sdata[2]);
+ for (char * 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, sdata[0], &cn->dbh, dpb_length, buffer))
+ {
+ isc_interprete(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(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(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, query, 1, out_sqlda))
+ {
+ isc_interprete(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(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(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(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_cat(result, US "\\", 1);
+ result = string_cat(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, uschar * query, int length,
+ uschar ** result, uschar ** errmsg, uint * do_cache, const uschar * opts)
+{
+int sep = 0;
+uschar *server;
+uschar *list = ibase_servers;
+
+DEBUG(D_lookup) debug_printf_indent("Interbase query: %s\n", query);
+
+while ((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, uschar * opt, unsigned idx)
+{
+int c;
+int count = 0;
+uschar * t = s, * quoted;
+
+if (opt)
+ return NULL; /* No options recognized */
+
+while ((c = *t++))
+ if (c == '\'') count++;
+
+t = quoted = store_get_quoted(Ustrlen(s) + count + 1, s, idx);
+
+while ((c = *s++))
+ if (c == '\'') { *t++ = '\''; *t++ = '\''; }
+ else *t++ = c;
+
+*t = 0;
+return 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/lookups/json.c b/src/lookups/json.c
new file mode 100644
index 0000000..c9abf8c
--- /dev/null
+++ b/src/lookups/json.c
@@ -0,0 +1,188 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2021 - 2022 */
+/* Copyright (c) Jeremy Harris 2019 - 2020 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+#include "lf_functions.h"
+#include <jansson.h>
+
+
+
+/* All use of allocations will be done against the POOL_SEARCH memory,
+which is freed once by search_tidyup(). Make the free call a dummy.
+This burns some 300kB in handling a 37kB JSON file, for the benefit of
+a fast free. The alternative of staying with malloc is nearly as bad,
+eyeballing the activity there are 20% the number of free vs. alloc
+calls (before the big bunch at the end).
+
+Assume that the file is trusted, so no tainting */
+
+static void *
+json_malloc(size_t nbytes)
+{
+void * p = store_get((int)nbytes, GET_UNTAINTED);
+/* debug_printf("%s %d: %p\n", __FUNCTION__, (int)nbytes, p); */
+return p;
+}
+static void
+json_free(void * p)
+{
+/* debug_printf("%s: %p\n", __FUNCTION__, p); */
+}
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description */
+
+static void *
+json_open(const uschar * filename, uschar ** errmsg)
+{
+FILE * f;
+
+json_set_alloc_funcs(json_malloc, json_free);
+
+if (!(f = Ufopen(filename, "rb")))
+ *errmsg = string_open_failed("%s for json search", filename);
+return f;
+}
+
+
+
+/*************************************************
+* Check entry point *
+*************************************************/
+
+static BOOL
+json_check(void *handle, const uschar *filename, int modemask, uid_t *owners,
+ gid_t *owngroups, uschar **errmsg)
+{
+return lf_check_file(fileno((FILE *)handle), filename, S_IFREG, modemask,
+ owners, owngroups, "json", errmsg) == 0;
+}
+
+
+
+/*************************************************
+* Find entry point for lsearch *
+*************************************************/
+
+/* See local README for interface description */
+
+static int
+json_find(void * handle, const uschar * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+FILE * f = handle;
+json_t * j, * j0;
+json_error_t jerr;
+uschar * key;
+int sep = 0;
+
+rewind(f);
+if (!(j = json_loadf(f, 0, &jerr)))
+ {
+ *errmsg = string_sprintf("json error on open: %.*s\n",
+ JSON_ERROR_TEXT_LENGTH, jerr.text);
+ return FAIL;
+ }
+j0 = j;
+
+for (int k = 1; (key = string_nextinlist(&keystring, &sep, NULL, 0)); k++)
+ {
+ BOOL numeric = TRUE;
+ for (uschar * s = key; *s; s++) if (!isdigit(*s)) { numeric = FALSE; break; }
+
+ if (!(j = numeric
+ ? json_array_get(j, (size_t) strtoul(CS key, NULL, 10))
+ : json_object_get(j, CCS key)
+ ) )
+ {
+ DEBUG(D_lookup) debug_printf_indent("%s, for key %d: '%s'\n",
+ numeric
+ ? US"bad index, or not json array"
+ : US"no such key, or not json object",
+ k, key);
+ json_decref(j0);
+ return FAIL;
+ }
+ }
+
+switch (json_typeof(j))
+ {
+ case JSON_STRING:
+ *result = string_copyn(CUS json_string_value(j), json_string_length(j));
+ break;
+ case JSON_INTEGER:
+ *result = string_sprintf("%" JSON_INTEGER_FORMAT, json_integer_value(j));
+ break;
+ case JSON_REAL:
+ *result = string_sprintf("%f", json_real_value(j));
+ break;
+ case JSON_TRUE: *result = US"true"; break;
+ case JSON_FALSE: *result = US"false"; break;
+ case JSON_NULL: *result = NULL; break;
+ default: *result = US json_dumps(j, 0); break;
+ }
+json_decref(j0);
+return OK;
+}
+
+
+
+/*************************************************
+* Close entry point *
+*************************************************/
+
+/* See local README for interface description */
+
+static void
+json_close(void *handle)
+{
+(void)fclose((FILE *)handle);
+}
+
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+json_version_report(gstring * g)
+{
+return string_fmt_append(g, "Library version: json: Jansonn version %s\n", JANSSON_VERSION);
+}
+
+
+static lookup_info json_lookup_info = {
+ .name = US"json", /* lookup name */
+ .type = lookup_absfile, /* uses absolute file name */
+ .open = json_open, /* open function */
+ .check = json_check, /* check function */
+ .find = json_find, /* find function */
+ .close = json_close, /* close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = json_version_report /* version reporting */
+};
+
+
+#ifdef DYNLOOKUP
+#define json_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &json_lookup_info };
+lookup_module_info json_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* End of lookups/json.c */
diff --git a/src/lookups/ldap.c b/src/lookups/ldap.c
new file mode 100644
index 0000000..9751fa3
--- /dev/null
+++ b/src/lookups/ldap.c
@@ -0,0 +1,1613 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* Many thanks to Stuart Lynne for contributing the original code for this
+driver. Further contributions from Michael Haardt, Brian Candler, Barry
+Pederson, Peter Savitch and Christian Kellner. Particular thanks to Brian for
+researching how to handle the different kinds of error. */
+
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+
+/* Include LDAP headers. The code below uses some "old" LDAP interfaces that
+are deprecated in OpenLDAP. I don't know their status in other LDAP
+implementations. LDAP_DEPRECATED causes their prototypes to be defined in
+ldap.h. */
+
+#define LDAP_DEPRECATED 1
+
+#include <lber.h>
+#include <ldap.h>
+
+
+/* Annoyingly, the different LDAP libraries handle errors in different ways,
+and some other things too. There doesn't seem to be an automatic way of
+distinguishing between them. Local/Makefile should contain a setting of
+LDAP_LIB_TYPE, which in turn causes appropriate macros to be defined for the
+different kinds. Those that matter are:
+
+LDAP_LIB_NETSCAPE
+LDAP_LIB_SOLARIS with synonym LDAP_LIB_SOLARIS7
+LDAP_LIB_OPENLDAP2
+
+These others may be defined, but are in fact the default, so are not tested:
+
+LDAP_LIB_UMICHIGAN
+LDAP_LIB_OPENLDAP1
+*/
+
+#if defined(LDAP_LIB_SOLARIS7) && ! defined(LDAP_LIB_SOLARIS)
+#define LDAP_LIB_SOLARIS
+#endif
+
+
+/* Just in case LDAP_NO_LIMIT is not defined by some of these libraries. */
+
+#ifndef LDAP_NO_LIMIT
+#define LDAP_NO_LIMIT 0
+#endif
+
+
+/* Just in case LDAP_DEREF_NEVER is not defined */
+
+#ifndef LDAP_DEREF_NEVER
+#define LDAP_DEREF_NEVER 0
+#endif
+
+
+/* Four types of LDAP search are implemented */
+
+#define SEARCH_LDAP_MULTIPLE 0 /* Get attributes from multiple entries */
+#define SEARCH_LDAP_SINGLE 1 /* Get attributes from one entry only */
+#define SEARCH_LDAP_DN 2 /* Get just the DN from one entry */
+#define SEARCH_LDAP_AUTH 3 /* Just checking for authentication */
+
+/* In all 4 cases, the DN is left in $ldap_dn (which post-dates the
+SEARCH_LDAP_DN lookup). */
+
+
+/* Structure and anchor for caching connections. */
+
+typedef struct ldap_connection {
+ struct ldap_connection *next;
+ uschar *host;
+ uschar *user;
+ uschar *password;
+ BOOL bound;
+ int port;
+ BOOL is_start_tls_called;
+ LDAP *ld;
+} LDAP_CONNECTION;
+
+static LDAP_CONNECTION *ldap_connections = NULL;
+
+
+
+/*************************************************
+* Internal search function *
+*************************************************/
+
+/* This is the function that actually does the work. It is called (indirectly
+via control_ldap_search) from eldap_find(), eldapauth_find(), eldapdn_find(),
+and eldapm_find(), with a difference in the "search_type" argument.
+
+The case of eldapauth_find() is special in that all it does is do
+authentication, returning OK or FAIL as appropriate. This isn't used as a
+lookup. Instead, it is called from expand.c as an expansion condition test.
+
+The DN from a successful lookup is placed in $ldap_dn. This feature postdates
+the provision of the SEARCH_LDAP_DN facility for returning just the DN as the
+data.
+
+Arguments:
+ ldap_url the URL to be looked up
+ server server host name, when URL contains none
+ s_port server port, used when URL contains no name
+ search_type SEARCH_LDAP_MULTIPLE allows values from multiple entries
+ SEARCH_LDAP_SINGLE allows values from one entry only
+ SEARCH_LDAP_DN gets the DN from one entry
+ res set to point at the result (not used for ldapauth)
+ errmsg set to point a message if result is not OK
+ defer_break set TRUE if no more servers to be tried after a DEFER
+ user user name for authentication, or NULL
+ password password for authentication, or NULL
+ sizelimit max number of entries returned, or 0 for no limit
+ timelimit max time to wait, or 0 for no limit
+ tcplimit max time for network activity, e.g. connect, or 0 for OS default
+ deference the dereference option, which is one of
+ LDAP_DEREF_{NEVER,SEARCHING,FINDING,ALWAYS}
+ referrals the referral option, which is LDAP_OPT_ON or LDAP_OPT_OFF
+
+Returns: OK or FAIL or DEFER
+ FAIL is given only if a lookup was performed successfully, but
+ returned no data.
+*/
+
+static int
+perform_ldap_search(const uschar *ldap_url, uschar *server, int s_port,
+ int search_type, uschar **res, uschar **errmsg, BOOL *defer_break,
+ uschar *user, uschar *password, int sizelimit, int timelimit, int tcplimit,
+ int dereference, void *referrals)
+{
+LDAPURLDesc *ludp = NULL;
+LDAPMessage *result = NULL;
+BerElement *ber;
+LDAP_CONNECTION *lcp;
+
+struct timeval timeout;
+struct timeval *timeoutptr = NULL;
+
+gstring * data = NULL;
+uschar *dn = NULL;
+uschar *host;
+uschar **values;
+uschar **firstval;
+uschar porttext[16];
+
+uschar *error1 = NULL; /* string representation of errcode (static) */
+uschar *error2 = NULL; /* error message from the server */
+uschar *matched = NULL; /* partially matched DN */
+
+int attrs_requested = 0;
+int error_yield = DEFER;
+int msgid;
+int rc, ldap_rc, ldap_parse_rc;
+int port;
+int rescount = 0;
+BOOL attribute_found = FALSE;
+BOOL ldapi = FALSE;
+
+DEBUG(D_lookup) debug_printf_indent("perform_ldap_search:"
+ " ldap%s URL = \"%s\" server=%s port=%d "
+ "sizelimit=%d timelimit=%d tcplimit=%d\n",
+ search_type == SEARCH_LDAP_MULTIPLE ? "m" :
+ search_type == SEARCH_LDAP_DN ? "dn" :
+ search_type == SEARCH_LDAP_AUTH ? "auth" : "",
+ ldap_url, server, s_port, sizelimit, timelimit, tcplimit);
+
+/* Check if LDAP thinks the URL is a valid LDAP URL. We assume that if the LDAP
+library that is in use doesn't recognize, say, "ldapi", it will barf here. */
+
+if (!ldap_is_ldap_url(CS ldap_url))
+ {
+ *errmsg = string_sprintf("ldap_is_ldap_url: not an LDAP url \"%s\"\n",
+ ldap_url);
+ goto RETURN_ERROR_BREAK;
+ }
+
+/* Parse the URL */
+
+if ((rc = ldap_url_parse(CS ldap_url, &ludp)) != 0)
+ {
+ *errmsg = string_sprintf("ldap_url_parse: (error %d) parsing \"%s\"\n", rc,
+ ldap_url);
+ goto RETURN_ERROR_BREAK;
+ }
+
+/* If the host name is empty, take it from the separate argument, if one is
+given. OpenLDAP 2.0.6 sets an unset hostname to "" rather than empty, but
+expects NULL later in ldap_init() to mean "default", annoyingly. In OpenLDAP
+2.0.11 this has changed (it uses NULL). */
+
+if ((!ludp->lud_host || !ludp->lud_host[0]) && server)
+ {
+ host = server;
+ port = s_port;
+ }
+else
+ {
+ host = US ludp->lud_host;
+ if (host && !host[0]) host = NULL;
+ port = ludp->lud_port;
+ }
+
+DEBUG(D_lookup) debug_printf_indent("after ldap_url_parse: host=%s port=%d\n",
+ host, port);
+
+if (port == 0) port = LDAP_PORT; /* Default if none given */
+sprintf(CS porttext, ":%d", port); /* For messages */
+
+/* If the "host name" is actually a path, we are going to connect using a Unix
+socket, regardless of whether "ldapi" was actually specified or not. This means
+that a Unix socket can be declared in eldap_default_servers, and "traditional"
+LDAP queries using just "ldap" can be used ("ldaps" is similarly overridden).
+The path may start with "/" or it may already be escaped as "%2F" if it was
+actually declared that way in eldap_default_servers. (I did it that way the
+first time.) If the host name is not a path, the use of "ldapi" causes an
+error, except in the default case. (But lud_scheme doesn't seem to exist in
+older libraries.) */
+
+if (host)
+ {
+ if ((host[0] == '/' || Ustrncmp(host, "%2F", 3) == 0))
+ {
+ ldapi = TRUE;
+ porttext[0] = 0; /* Remove port from messages */
+ }
+
+#if defined LDAP_LIB_OPENLDAP2
+ else if (strncmp(ludp->lud_scheme, "ldapi", 5) == 0)
+ {
+ *errmsg = string_sprintf("ldapi requires an absolute path (\"%s\" given)",
+ host);
+ goto RETURN_ERROR;
+ }
+#endif
+ }
+
+/* Count the attributes; we need this later to tell us how to format results */
+
+for (uschar ** attrp = USS ludp->lud_attrs; attrp && *attrp; attrp++)
+ attrs_requested++;
+
+/* See if we can find a cached connection to this host. The port is not
+relevant for ldapi. The host name pointer is set to NULL if no host was given
+(implying the library default), rather than to the empty string. Note that in
+this case, there is no difference between ldap and ldapi. */
+
+for (lcp = ldap_connections; lcp; lcp = lcp->next)
+ {
+ if ((host == NULL) != (lcp->host == NULL) ||
+ (host != NULL && strcmpic(lcp->host, host) != 0))
+ continue;
+ if (ldapi || port == lcp->port) break;
+ }
+
+/* Use this network timeout in any requests. */
+
+if (tcplimit > 0)
+ {
+ timeout.tv_sec = tcplimit;
+ timeout.tv_usec = 0;
+ timeoutptr = &timeout;
+ }
+
+/* If no cached connection found, we must open a connection to the server. If
+the server name is actually an absolute path, we set ldapi=TRUE above. This
+requests connection via a Unix socket. However, as far as I know, only OpenLDAP
+supports the use of sockets, and the use of ldap_initialize(). */
+
+if (!lcp)
+ {
+ LDAP *ld;
+
+#ifdef LDAP_OPT_X_TLS_NEWCTX
+ int am_server = 0;
+ LDAP *ldsetctx;
+#else
+ LDAP *ldsetctx = NULL;
+#endif
+
+
+ /* --------------------------- OpenLDAP ------------------------ */
+
+ /* There seems to be a preference under OpenLDAP for ldap_initialize()
+ instead of ldap_init(), though I have as yet been unable to find
+ documentation that says this. (OpenLDAP documentation is sparse to
+ non-existent). So we handle OpenLDAP differently here. Also, support for
+ ldapi seems to be OpenLDAP-only at present. */
+
+#ifdef LDAP_LIB_OPENLDAP2
+
+ /* We now need an empty string for the default host. Get some store in which
+ to build a URL for ldap_initialize(). In the ldapi case, it can't be bigger
+ than (9 + 3*Ustrlen(shost)), whereas in the other cases it can't be bigger
+ than the host name + "ldaps:///" plus : and a port number, say 20 + the
+ length of the host name. What we get should accommodate both, easily. */
+
+ uschar * shost = host ? host : US"";
+ rmark reset_point = store_mark();
+ gstring * g;
+
+ /* Handle connection via Unix socket ("ldapi"). We build a basic LDAP URI to
+ contain the path name, with slashes escaped as %2F. */
+
+ if (ldapi)
+ {
+ g = string_catn(NULL, US"ldapi://", 8);
+ for (uschar ch; (ch = *shost); shost++)
+ g = ch == '/' ? string_catn(g, US"%2F", 3) : string_catn(g, shost, 1);
+ }
+
+ /* This is not an ldapi call. Just build a URI with the protocol type, host
+ name, and port. */
+
+ else
+ {
+ uschar * init_ptr = Ustrchr(ldap_url, '/');
+ g = string_catn(NULL, ldap_url, init_ptr - ldap_url);
+ g = string_fmt_append(g, "//%s:%d/", shost, port);
+ }
+ string_from_gstring(g);
+
+ /* Call ldap_initialize() and check the result */
+
+ DEBUG(D_lookup) debug_printf_indent("ldap_initialize with URL %s\n", g->s);
+ if ((rc = ldap_initialize(&ld, CS g->s)) != LDAP_SUCCESS)
+ {
+ *errmsg = string_sprintf("ldap_initialize: (error %d) URL \"%s\"\n",
+ rc, g->s);
+ goto RETURN_ERROR;
+ }
+ store_reset(reset_point); /* Might as well save memory when we can */
+
+
+ /* ------------------------- Not OpenLDAP ---------------------- */
+
+ /* For libraries other than OpenLDAP, use ldap_init(). */
+
+#else /* LDAP_LIB_OPENLDAP2 */
+ ld = ldap_init(CS host, port);
+#endif /* LDAP_LIB_OPENLDAP2 */
+
+ /* -------------------------------------------------------------- */
+
+
+ /* Handle failure to initialize */
+
+ if (!ld)
+ {
+ *errmsg = string_sprintf("failed to initialize for LDAP server %s%s - %s",
+ host, porttext, strerror(errno));
+ goto RETURN_ERROR;
+ }
+
+#ifdef LDAP_OPT_X_TLS_NEWCTX
+ ldsetctx = ld;
+#endif
+
+ /* Set the TCP connect time limit if available. This is something that is
+ in Netscape SDK v4.1; I don't know about other libraries. */
+
+#ifdef LDAP_X_OPT_CONNECT_TIMEOUT
+ if (tcplimit > 0)
+ {
+ int timeout1000 = tcplimit*1000;
+ ldap_set_option(ld, LDAP_X_OPT_CONNECT_TIMEOUT, (void *)&timeout1000);
+ }
+ else
+ {
+ int notimeout = LDAP_X_IO_TIMEOUT_NO_TIMEOUT;
+ ldap_set_option(ld, LDAP_X_OPT_CONNECT_TIMEOUT, (void *)&notimeout);
+ }
+#endif
+
+ /* Set the TCP connect timeout. This works with OpenLDAP 2.2.14. */
+
+#ifdef LDAP_OPT_NETWORK_TIMEOUT
+ if (tcplimit > 0)
+ ldap_set_option(ld, LDAP_OPT_NETWORK_TIMEOUT, (void *)timeoutptr);
+#endif
+
+ /* I could not get TLS to work until I set the version to 3. That version
+ seems to be the default nowadays. The RFC is dated 1997, so I would hope
+ that all the LDAP libraries support it. Therefore, if eldap_version hasn't
+ been set, go for v3 if we can. */
+
+ if (eldap_version < 0)
+ {
+#ifdef LDAP_VERSION3
+ eldap_version = LDAP_VERSION3;
+#else
+ eldap_version = 2;
+#endif
+ }
+
+#ifdef LDAP_OPT_PROTOCOL_VERSION
+ ldap_set_option(ld, LDAP_OPT_PROTOCOL_VERSION, (void *)&eldap_version);
+#endif
+
+ DEBUG(D_lookup) debug_printf_indent("initialized for LDAP (v%d) server %s%s\n",
+ eldap_version, host, porttext);
+
+ /* If not using ldapi and TLS is available, set appropriate TLS options: hard
+ for "ldaps" and soft otherwise. */
+
+#ifdef LDAP_OPT_X_TLS
+ if (!ldapi)
+ {
+ int tls_option;
+# ifdef LDAP_OPT_X_TLS_REQUIRE_CERT
+ if (eldap_require_cert)
+ {
+ tls_option =
+ Ustrcmp(eldap_require_cert, "hard") == 0 ? LDAP_OPT_X_TLS_HARD
+ : Ustrcmp(eldap_require_cert, "demand") == 0 ? LDAP_OPT_X_TLS_DEMAND
+ : Ustrcmp(eldap_require_cert, "allow") == 0 ? LDAP_OPT_X_TLS_ALLOW
+ : Ustrcmp(eldap_require_cert, "try") == 0 ? LDAP_OPT_X_TLS_TRY
+ : LDAP_OPT_X_TLS_NEVER;
+
+ DEBUG(D_lookup) debug_printf_indent(
+ "Require certificate overrides LDAP_OPT_X_TLS option (%d)\n",
+ tls_option);
+ }
+ else
+# endif /* LDAP_OPT_X_TLS_REQUIRE_CERT */
+ if (strncmp(ludp->lud_scheme, "ldaps", 5) == 0)
+ {
+ tls_option = LDAP_OPT_X_TLS_HARD;
+ DEBUG(D_lookup)
+ debug_printf_indent("LDAP_OPT_X_TLS_HARD set due to ldaps:// URI\n");
+ }
+ else
+ {
+ tls_option = LDAP_OPT_X_TLS_TRY;
+ DEBUG(D_lookup)
+ debug_printf_indent("LDAP_OPT_X_TLS_TRY set due to ldap:// URI\n");
+ }
+ ldap_set_option(ld, LDAP_OPT_X_TLS, (void *)&tls_option);
+ }
+#endif /* LDAP_OPT_X_TLS */
+
+#ifdef LDAP_OPT_X_TLS_CACERTFILE
+ if (eldap_ca_cert_file)
+ ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_CACERTFILE, eldap_ca_cert_file);
+#endif
+#ifdef LDAP_OPT_X_TLS_CACERTDIR
+ if (eldap_ca_cert_dir)
+ ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_CACERTDIR, eldap_ca_cert_dir);
+#endif
+#ifdef LDAP_OPT_X_TLS_CERTFILE
+ if (eldap_cert_file)
+ ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_CERTFILE, eldap_cert_file);
+#endif
+#ifdef LDAP_OPT_X_TLS_KEYFILE
+ if (eldap_cert_key)
+ ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_KEYFILE, eldap_cert_key);
+#endif
+#ifdef LDAP_OPT_X_TLS_CIPHER_SUITE
+ if (eldap_cipher_suite)
+ ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_CIPHER_SUITE, eldap_cipher_suite);
+#endif
+#ifdef LDAP_OPT_X_TLS_REQUIRE_CERT
+ if (eldap_require_cert)
+ {
+ int cert_option =
+ Ustrcmp(eldap_require_cert, "hard") == 0 ? LDAP_OPT_X_TLS_HARD
+ : Ustrcmp(eldap_require_cert, "demand") == 0 ? LDAP_OPT_X_TLS_DEMAND
+ : Ustrcmp(eldap_require_cert, "allow") == 0 ? LDAP_OPT_X_TLS_ALLOW
+ : Ustrcmp(eldap_require_cert, "try") == 0 ? LDAP_OPT_X_TLS_TRY
+ : LDAP_OPT_X_TLS_NEVER;
+
+ /* This ldap handle is set at compile time based on client libs. Older
+ * versions want it to be global and newer versions can force a reload
+ * of the TLS context (to reload these settings we are changing from the
+ * default that loaded at instantiation). */
+ rc = ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_REQUIRE_CERT, &cert_option);
+ if (rc)
+ DEBUG(D_lookup)
+ debug_printf_indent("Unable to set TLS require cert_option(%d) globally: %s\n",
+ cert_option, ldap_err2string(rc));
+ }
+#endif
+#ifdef LDAP_OPT_X_TLS_NEWCTX
+ if ((rc = ldap_set_option(ldsetctx, LDAP_OPT_X_TLS_NEWCTX, &am_server)))
+ DEBUG(D_lookup)
+ debug_printf_indent("Unable to reload TLS context %d: %s\n",
+ rc, ldap_err2string(rc));
+ #endif
+
+ /* Now add this connection to the chain of cached connections */
+
+ lcp = store_get(sizeof(LDAP_CONNECTION), GET_UNTAINTED);
+ lcp->host = host ? string_copy(host) : NULL;
+ lcp->bound = FALSE;
+ lcp->user = NULL;
+ lcp->password = NULL;
+ lcp->port = port;
+ lcp->ld = ld;
+ lcp->next = ldap_connections;
+ lcp->is_start_tls_called = FALSE;
+ ldap_connections = lcp;
+ }
+
+/* Found cached connection */
+
+else
+ DEBUG(D_lookup)
+ debug_printf_indent("re-using cached connection to LDAP server %s%s\n",
+ host, porttext);
+
+/* Bind with the user/password supplied, or an anonymous bind if these values
+are NULL, unless a cached connection is already bound with the same values. */
+
+if ( !lcp->bound
+ || !lcp->user && user
+ || lcp->user && !user
+ || lcp->user && user && Ustrcmp(lcp->user, user) != 0
+ || !lcp->password && password
+ || lcp->password && !password
+ || lcp->password && password && Ustrcmp(lcp->password, password) != 0
+ )
+ {
+ DEBUG(D_lookup) debug_printf_indent("%sbinding with user=%s password=%s\n",
+ lcp->bound ? "re-" : "", user, password);
+
+ if (eldap_start_tls && !lcp->is_start_tls_called && !ldapi)
+ {
+#if defined(LDAP_OPT_X_TLS) && !defined(LDAP_LIB_SOLARIS)
+ /* The Oracle LDAP libraries (LDAP_LIB_TYPE=SOLARIS) don't support this.
+ * Note: moreover, they appear to now define LDAP_OPT_X_TLS and still not
+ * export an ldap_start_tls_s symbol.
+ */
+ if ( (rc = ldap_start_tls_s(lcp->ld, NULL, NULL)) != LDAP_SUCCESS)
+ {
+ *errmsg = string_sprintf("failed to initiate TLS processing on an "
+ "LDAP session to server %s%s - ldap_start_tls_s() returned %d:"
+ " %s", host, porttext, rc, ldap_err2string(rc));
+ goto RETURN_ERROR;
+ }
+ lcp->is_start_tls_called = TRUE;
+#else
+ DEBUG(D_lookup) debug_printf_indent("TLS initiation not supported with this Exim"
+ " and your LDAP library.\n");
+#endif
+ }
+ if ((msgid = ldap_bind(lcp->ld, CS user, CS password, LDAP_AUTH_SIMPLE))
+ == -1)
+ {
+ *errmsg = string_sprintf("failed to bind the LDAP connection to server "
+ "%s%s - ldap_bind() returned -1", host, porttext);
+ goto RETURN_ERROR;
+ }
+
+ if ((rc = ldap_result(lcp->ld, msgid, 1, timeoutptr, &result)) <= 0)
+ {
+ *errmsg = string_sprintf("failed to bind the LDAP connection to server "
+ "%s%s - LDAP error: %s", host, porttext,
+ rc == -1 ? "result retrieval failed" : "timeout" );
+ result = NULL;
+ goto RETURN_ERROR;
+ }
+
+ rc = ldap_result2error(lcp->ld, result, 0);
+
+ /* Invalid credentials when just checking credentials returns FAIL. This
+ stops any further servers being tried. */
+
+ if (search_type == SEARCH_LDAP_AUTH && rc == LDAP_INVALID_CREDENTIALS)
+ {
+ DEBUG(D_lookup)
+ debug_printf_indent("Invalid credentials: ldapauth returns FAIL\n");
+ error_yield = FAIL;
+ goto RETURN_ERROR_NOMSG;
+ }
+
+ /* Otherwise we have a problem that doesn't stop further servers from being
+ tried. */
+
+ if (rc != LDAP_SUCCESS)
+ {
+ *errmsg = string_sprintf("failed to bind the LDAP connection to server "
+ "%s%s - LDAP error %d: %s", host, porttext, rc, ldap_err2string(rc));
+ goto RETURN_ERROR;
+ }
+
+ /* Successful bind */
+
+ lcp->bound = TRUE;
+ lcp->user = !user ? NULL : string_copy(user);
+ lcp->password = !password ? NULL : string_copy(password);
+
+ ldap_msgfree(result);
+ result = NULL;
+ }
+
+/* If we are just checking credentials, return OK. */
+
+if (search_type == SEARCH_LDAP_AUTH)
+ {
+ DEBUG(D_lookup) debug_printf_indent("Bind succeeded: ldapauth returns OK\n");
+ goto RETURN_OK;
+ }
+
+/* Before doing the search, set the time and size limits (if given). Here again
+the different implementations of LDAP have chosen to do things differently. */
+
+#if defined(LDAP_OPT_SIZELIMIT)
+ldap_set_option(lcp->ld, LDAP_OPT_SIZELIMIT, (void *)&sizelimit);
+ldap_set_option(lcp->ld, LDAP_OPT_TIMELIMIT, (void *)&timelimit);
+#else
+lcp->ld->ld_sizelimit = sizelimit;
+lcp->ld->ld_timelimit = timelimit;
+#endif
+
+/* Similarly for dereferencing aliases. Don't know if this is possible on
+an LDAP library without LDAP_OPT_DEREF. */
+
+#if defined(LDAP_OPT_DEREF)
+ldap_set_option(lcp->ld, LDAP_OPT_DEREF, (void *)&dereference);
+#endif
+
+/* Similarly for the referral setting; should the library follow referrals that
+the LDAP server returns? The conditional is just in case someone uses a library
+without it. */
+
+#if defined(LDAP_OPT_REFERRALS)
+ldap_set_option(lcp->ld, LDAP_OPT_REFERRALS, referrals);
+#endif
+
+/* Start the search on the server. */
+
+DEBUG(D_lookup) debug_printf_indent("Start search\n");
+
+msgid = ldap_search(lcp->ld, ludp->lud_dn, ludp->lud_scope, ludp->lud_filter,
+ ludp->lud_attrs, 0);
+
+if (msgid == -1)
+ {
+#if defined LDAP_LIB_SOLARIS || defined LDAP_LIB_OPENLDAP2
+ int err;
+ ldap_get_option(lcp->ld, LDAP_OPT_ERROR_NUMBER, &err);
+ *errmsg = string_sprintf("ldap_search failed: %d, %s", err,
+ ldap_err2string(err));
+#else
+ *errmsg = string_sprintf("ldap_search failed");
+#endif
+
+ goto RETURN_ERROR;
+ }
+
+/* Loop to pick up results as they come in, setting a timeout if one was
+given. */
+
+while ((rc = ldap_result(lcp->ld, msgid, 0, timeoutptr, &result)) ==
+ LDAP_RES_SEARCH_ENTRY)
+ {
+ LDAPMessage *e;
+ int valuecount; /* We can see an attr spread across several
+ entries. If B is derived from A and we request
+ A and the directory contains both, A and B,
+ then we get two entries, one for A and one for B.
+ Here we just count the values per entry */
+
+ DEBUG(D_lookup) debug_printf_indent("LDAP result loop\n");
+
+ for(e = ldap_first_entry(lcp->ld, result), valuecount = 0;
+ e;
+ e = ldap_next_entry(lcp->ld, e))
+ {
+ uschar *new_dn;
+ BOOL insert_space = FALSE;
+
+ DEBUG(D_lookup) debug_printf_indent("LDAP entry loop\n");
+
+ rescount++; /* Count results */
+
+ /* Results for multiple entries values are separated by newlines. */
+
+ if (data) data = string_catn(data, US"\n", 1);
+
+ /* Get the DN from the last result. */
+
+ if ((new_dn = US ldap_get_dn(lcp->ld, e)))
+ {
+ if (dn)
+ {
+#if defined LDAP_LIB_NETSCAPE || defined LDAP_LIB_OPENLDAP2
+ ldap_memfree(dn);
+#else /* OPENLDAP 1, UMich, Solaris */
+ free(dn);
+#endif
+ }
+ /* Save for later */
+ dn = new_dn;
+ }
+
+ /* If the data we want is actually the DN rather than any attribute values,
+ (an "ldapdn" search) add it to the data string. If there are multiple
+ entries, the DNs will be concatenated, but we test for this case below, as
+ for SEARCH_LDAP_SINGLE, and give an error. */
+
+ if (search_type == SEARCH_LDAP_DN) /* Do not amalgamate these into one */
+ { /* condition, because of the else */
+ if (new_dn) /* below, that's for the first only */
+ {
+ data = string_cat(data, new_dn);
+ (void) string_from_gstring(data);
+ attribute_found = TRUE;
+ }
+ }
+
+ /* Otherwise, loop through the entry, grabbing attribute values. If there's
+ only one attribute being retrieved, no attribute name is given, and the
+ result is not quoted. Multiple values are separated by (comma).
+ If more than one attribute is being retrieved, the data is given as a
+ sequence of name=value pairs, separated by (space), with the value always in quotes.
+ If there are multiple values, they are given within the quotes, comma separated. */
+
+ else for (uschar * attr = US ldap_first_attribute(lcp->ld, e, &ber);
+ attr; attr = US ldap_next_attribute(lcp->ld, e, ber))
+ {
+ DEBUG(D_lookup) debug_printf_indent("LDAP attr loop\n");
+
+ /* In case of attrs_requested == 1 we just count the values, in all other cases
+ (0, >1) we count the values per attribute */
+ if (attrs_requested != 1) valuecount = 0;
+
+ if (attr[0] != 0)
+ {
+ /* Get array of values for this attribute. */
+
+ if ((firstval = values = USS ldap_get_values(lcp->ld, e, CS attr)))
+ {
+ if (attrs_requested != 1)
+ {
+ if (insert_space)
+ data = string_catn(data, US" ", 1);
+ else
+ insert_space = TRUE;
+ data = string_cat(data, attr);
+ data = string_catn(data, US"=\"", 2);
+ }
+
+ while (*values)
+ {
+ uschar *value = *values;
+ int len = Ustrlen(value);
+ ++valuecount;
+
+ DEBUG(D_lookup) debug_printf_indent("LDAP value loop %s:%s\n", attr, value);
+
+ /* In case we requested one attribute only but got several times
+ into that attr loop, we need to append the additional values.
+ (This may happen if you derive attributeTypes B and C from A and
+ then query for A.) In all other cases we detect the different
+ attribute and append only every non first value. */
+
+ if (data && valuecount > 1)
+ data = string_catn(data, US",", 1);
+
+ /* For multiple attributes, the data is in quotes. We must escape
+ internal quotes, backslashes, newlines, and must double commas. */
+
+ if (attrs_requested != 1)
+ for (int j = 0; j < len; j++)
+ {
+ if (value[j] == '\n')
+ data = string_catn(data, US"\\n", 2);
+ else if (value[j] == ',')
+ data = string_catn(data, US",,", 2);
+ else
+ {
+ if (value[j] == '\"' || value[j] == '\\')
+ data = string_catn(data, US"\\", 1);
+ data = string_catn(data, value+j, 1);
+ }
+ }
+
+ /* For single attributes, just double commas */
+
+ else
+ for (int j = 0; j < len; j++)
+ if (value[j] == ',')
+ data = string_catn(data, US",,", 2);
+ else
+ data = string_catn(data, value+j, 1);
+
+
+ /* Move on to the next value */
+
+ values++;
+ attribute_found = TRUE;
+ }
+
+ /* Closing quote at the end of the data for a named attribute. */
+
+ if (attrs_requested != 1)
+ data = string_catn(data, US"\"", 1);
+
+ /* Free the values */
+
+ ldap_value_free(CSS firstval);
+ }
+ }
+
+#if defined LDAP_LIB_NETSCAPE || defined LDAP_LIB_OPENLDAP2
+
+ /* Netscape and OpenLDAP2 LDAP's attrs are dynamically allocated and need
+ to be freed. UMich LDAP stores them in static storage and does not require
+ this. */
+
+ ldap_memfree(attr);
+#endif
+ } /* End "for" loop for extracting attributes from an entry */
+ } /* End "for" loop for extracting entries from a result */
+
+ /* Free the result */
+
+ ldap_msgfree(result);
+ result = NULL;
+ } /* End "while" loop for multiple results */
+
+/* Terminate the dynamic string that we have built and reclaim unused store.
+In the odd case of a single attribute with zero-length value, allocate
+an empty string. */
+
+if (!data) data = string_get(1);
+(void) string_from_gstring(data);
+gstring_release_unused(data);
+
+/* Copy the last dn into eldap_dn */
+
+if (dn)
+ {
+ eldap_dn = string_copy(dn);
+#if defined LDAP_LIB_NETSCAPE || defined LDAP_LIB_OPENLDAP2
+ ldap_memfree(dn);
+#else /* OPENLDAP 1, UMich, Solaris */
+ free(dn);
+#endif
+ }
+
+DEBUG(D_lookup) debug_printf_indent("search ended by ldap_result yielding %d\n",rc);
+
+if (rc == 0)
+ {
+ *errmsg = US"ldap_result timed out";
+ goto RETURN_ERROR;
+ }
+
+/* A return code of -1 seems to mean "ldap_result failed internally or couldn't
+provide you with a message". Other error states seem to exist where
+ldap_result() didn't give us any message from the server at all, leaving result
+set to NULL. Apparently, "the error parameters of the LDAP session handle will
+be set accordingly". That's the best we can do to retrieve an error status; we
+can't use functions like ldap_result2error because they parse a message from
+the server, which we didn't get.
+
+Annoyingly, the different implementations of LDAP have gone for different
+methods of handling error codes and generating error messages. */
+
+if (rc == -1 || !result)
+ {
+ int err;
+ DEBUG(D_lookup) debug_printf_indent("ldap_result failed\n");
+
+#if defined LDAP_LIB_SOLARIS || defined LDAP_LIB_OPENLDAP2
+ ldap_get_option(lcp->ld, LDAP_OPT_ERROR_NUMBER, &err);
+ *errmsg = string_sprintf("ldap_result failed: %d, %s",
+ err, ldap_err2string(err));
+
+#elif defined LDAP_LIB_NETSCAPE
+ /* Dubious (surely 'matched' is spurious here?) */
+ (void)ldap_get_lderrno(lcp->ld, &matched, &error1);
+ *errmsg = string_sprintf("ldap_result failed: %s (%s)", error1, matched);
+
+#else /* UMich LDAP aka OpenLDAP 1.x */
+ *errmsg = string_sprintf("ldap_result failed: %d, %s",
+ lcp->ld->ld_errno, ldap_err2string(lcp->ld->ld_errno));
+#endif
+
+ goto RETURN_ERROR;
+ }
+
+/* A return code that isn't -1 doesn't necessarily mean there were no problems
+with the search. The message must be an LDAP_RES_SEARCH_RESULT or
+LDAP_RES_SEARCH_REFERENCE or else it's something we can't handle. Some versions
+of LDAP do not define LDAP_RES_SEARCH_REFERENCE (LDAP v1 is one, it seems). So
+we don't provide that functionality when we can't. :-) */
+
+if (rc != LDAP_RES_SEARCH_RESULT
+#ifdef LDAP_RES_SEARCH_REFERENCE
+ && rc != LDAP_RES_SEARCH_REFERENCE
+#endif
+ )
+ {
+ *errmsg = string_sprintf("ldap_result returned unexpected code %d", rc);
+ goto RETURN_ERROR;
+ }
+
+/* We have a result message from the server. This doesn't yet mean all is well.
+We need to parse the message to find out exactly what's happened. */
+
+#if defined LDAP_LIB_SOLARIS || defined LDAP_LIB_OPENLDAP2
+ ldap_rc = rc;
+ ldap_parse_rc = ldap_parse_result(lcp->ld, result, &rc, CSS &matched,
+ CSS &error2, NULL, NULL, 0);
+ DEBUG(D_lookup) debug_printf_indent("ldap_parse_result: %d\n", ldap_parse_rc);
+ if (ldap_parse_rc < 0 &&
+ (ldap_parse_rc != LDAP_NO_RESULTS_RETURNED
+ #ifdef LDAP_RES_SEARCH_REFERENCE
+ || ldap_rc != LDAP_RES_SEARCH_REFERENCE
+ #endif
+ ))
+ {
+ *errmsg = string_sprintf("ldap_parse_result failed %d", ldap_parse_rc);
+ goto RETURN_ERROR;
+ }
+ error1 = US ldap_err2string(rc);
+
+#elif defined LDAP_LIB_NETSCAPE
+ /* Dubious (it doesn't reference 'result' at all!) */
+ rc = ldap_get_lderrno(lcp->ld, &matched, &error1);
+
+#else /* UMich LDAP aka OpenLDAP 1.x */
+ rc = ldap_result2error(lcp->ld, result, 0);
+ error1 = ldap_err2string(rc);
+ error2 = lcp->ld->ld_error;
+ matched = lcp->ld->ld_matched;
+#endif
+
+/* Process the status as follows:
+
+ (1) If we get LDAP_SIZELIMIT_EXCEEDED, just carry on, to return the
+ truncated result list.
+
+ (2) If we get LDAP_RES_SEARCH_REFERENCE, also just carry on. This was a
+ submitted patch that is reported to "do the right thing" with Solaris
+ LDAP libraries. (The problem it addresses apparently does not occur with
+ Open LDAP.)
+
+ (3) The range of errors defined by LDAP_NAME_ERROR generally mean "that
+ object does not, or cannot, exist in the database". For those cases we
+ fail the lookup.
+
+ (4) All other non-successes here are treated as some kind of problem with
+ the lookup, so return DEFER (which is the default in error_yield).
+*/
+
+DEBUG(D_lookup) debug_printf_indent("ldap_parse_result yielded %d: %s\n",
+ rc, ldap_err2string(rc));
+
+if (rc != LDAP_SUCCESS && rc != LDAP_SIZELIMIT_EXCEEDED
+ #ifdef LDAP_RES_SEARCH_REFERENCE
+ && rc != LDAP_RES_SEARCH_REFERENCE
+ #endif
+ )
+ {
+ *errmsg = string_sprintf("LDAP search failed - error %d: %s%s%s%s%s",
+ rc,
+ error1 ? error1 : US"",
+ error2 && error2[0] ? US"/" : US"",
+ error2 ? error2 : US"",
+ matched && matched[0] ? US"/" : US"",
+ matched ? matched : US"");
+
+#if defined LDAP_NAME_ERROR
+ if (LDAP_NAME_ERROR(rc))
+#elif defined NAME_ERROR /* OPENLDAP1 calls it this */
+ if (NAME_ERROR(rc))
+#else
+ if (rc == LDAP_NO_SUCH_OBJECT)
+#endif
+
+ {
+ DEBUG(D_lookup) debug_printf_indent("lookup failure forced\n");
+ error_yield = FAIL;
+ }
+ goto RETURN_ERROR;
+ }
+
+/* The search succeeded. Check if we have too many results */
+
+if (search_type != SEARCH_LDAP_MULTIPLE && rescount > 1)
+ {
+ *errmsg = string_sprintf("LDAP search: more than one entry (%d) was returned "
+ "(filter not specific enough?)", rescount);
+ goto RETURN_ERROR_BREAK;
+ }
+
+/* Check if we have too few (zero) entries */
+
+if (rescount < 1)
+ {
+ *errmsg = US"LDAP search: no results";
+ error_yield = FAIL;
+ goto RETURN_ERROR_BREAK;
+ }
+
+/* If an entry was found, but it had no attributes, we behave as if no entries
+were found, that is, the lookup failed. */
+
+if (!attribute_found)
+ {
+ *errmsg = US"LDAP search: found no attributes";
+ error_yield = FAIL;
+ goto RETURN_ERROR;
+ }
+
+/* Otherwise, it's all worked */
+
+DEBUG(D_lookup) debug_printf_indent("LDAP search: returning: %s\n", data->s);
+*res = data->s;
+
+RETURN_OK:
+if (result) ldap_msgfree(result);
+ldap_free_urldesc(ludp);
+return OK;
+
+/* Error returns */
+
+RETURN_ERROR_BREAK:
+*defer_break = TRUE;
+
+RETURN_ERROR:
+DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
+
+RETURN_ERROR_NOMSG:
+if (result) ldap_msgfree(result);
+if (ludp) ldap_free_urldesc(ludp);
+
+#if defined LDAP_LIB_OPENLDAP2
+ if (error2) ldap_memfree(error2);
+ if (matched) ldap_memfree(matched);
+#endif
+
+return error_yield;
+}
+
+
+
+/*************************************************
+* Internal search control function *
+*************************************************/
+
+/* This function is called from eldap_find(), eldapauth_find(), eldapdn_find(),
+and eldapm_find() with a difference in the "search_type" argument. It controls
+calls to perform_ldap_search() which actually does the work. We call that
+repeatedly for certain types of defer in the case when the URL contains no host
+name and eldap_default_servers is set to a list of servers to try. This gives
+more control than just passing over a list of hosts to ldap_open() because it
+handles other kinds of defer as well as just a failure to open. Note that the
+URL is defined to contain either zero or one "hostport" only.
+
+Parameter data in addition to the URL can be passed as preceding text in the
+string, as items of the form XXX=yyy. The URL itself can be detected because it
+must begin "ldapx://", where x is empty, s, or i.
+
+Arguments:
+ ldap_url the URL to be looked up, optionally preceded by other parameter
+ settings
+ search_type SEARCH_LDAP_MULTIPLE allows values from multiple entries
+ SEARCH_LDAP_SINGLE allows values from one entry only
+ SEARCH_LDAP_DN gets the DN from one entry
+ res set to point at the result
+ errmsg set to point a message if result is not OK
+
+Returns: OK or FAIL or DEFER
+*/
+
+static int
+control_ldap_search(const uschar *ldap_url, int search_type, uschar **res,
+ uschar **errmsg)
+{
+BOOL defer_break = FALSE;
+int timelimit = LDAP_NO_LIMIT;
+int sizelimit = LDAP_NO_LIMIT;
+int tcplimit = 0;
+int sep = 0;
+int dereference = LDAP_DEREF_NEVER;
+void* referrals = LDAP_OPT_ON;
+const uschar *url = ldap_url;
+const uschar *p;
+uschar *user = NULL;
+uschar *password = NULL;
+uschar *local_servers = NULL;
+const uschar *list;
+
+while (isspace(*url)) url++;
+
+/* Until the string begins "ldap", search for the other parameter settings that
+are recognized. They are of the form NAME=VALUE, with the value being
+optionally double-quoted. There must still be a space after it, however. No
+NAME has the value "ldap". */
+
+while (strncmpic(url, US"ldap", 4) != 0)
+ {
+ const uschar *name = url;
+ while (*url && *url != '=') url++;
+ if (*url == '=')
+ {
+ int namelen;
+ uschar *value;
+ namelen = ++url - name;
+ value = string_dequote(&url);
+ if (isspace(*url))
+ {
+ if (strncmpic(name, US"USER=", namelen) == 0) user = value;
+ else if (strncmpic(name, US"PASS=", namelen) == 0) password = value;
+ else if (strncmpic(name, US"SIZE=", namelen) == 0) sizelimit = Uatoi(value);
+ else if (strncmpic(name, US"TIME=", namelen) == 0) timelimit = Uatoi(value);
+ else if (strncmpic(name, US"CONNECT=", namelen) == 0) tcplimit = Uatoi(value);
+ else if (strncmpic(name, US"NETTIME=", namelen) == 0) tcplimit = Uatoi(value);
+ else if (strncmpic(name, US"SERVERS=", namelen) == 0) local_servers = value;
+
+ /* Don't know if all LDAP libraries have LDAP_OPT_DEREF */
+
+ #ifdef LDAP_OPT_DEREF
+ else if (strncmpic(name, US"DEREFERENCE=", namelen) == 0)
+ {
+ if (strcmpic(value, US"never") == 0) dereference = LDAP_DEREF_NEVER;
+ else if (strcmpic(value, US"searching") == 0)
+ dereference = LDAP_DEREF_SEARCHING;
+ else if (strcmpic(value, US"finding") == 0)
+ dereference = LDAP_DEREF_FINDING;
+ if (strcmpic(value, US"always") == 0) dereference = LDAP_DEREF_ALWAYS;
+ }
+ #else
+ else if (strncmpic(name, US"DEREFERENCE=", namelen) == 0)
+ {
+ *errmsg = string_sprintf("LDAP_OP_DEREF not defined in this LDAP "
+ "library - cannot use \"dereference\"");
+ DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
+ return DEFER;
+ }
+ #endif
+
+ #ifdef LDAP_OPT_REFERRALS
+ else if (strncmpic(name, US"REFERRALS=", namelen) == 0)
+ {
+ if (strcmpic(value, US"follow") == 0) referrals = LDAP_OPT_ON;
+ else if (strcmpic(value, US"nofollow") == 0) referrals = LDAP_OPT_OFF;
+ else
+ {
+ *errmsg = US"LDAP option REFERRALS is not \"follow\" or \"nofollow\"";
+ DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
+ return DEFER;
+ }
+ }
+ #else
+ else if (strncmpic(name, US"REFERRALS=", namelen) == 0)
+ {
+ *errmsg = string_sprintf("LDAP_OP_REFERRALS not defined in this LDAP "
+ "library - cannot use \"referrals\"");
+ DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
+ return DEFER;
+ }
+ #endif
+
+ else
+ {
+ *errmsg =
+ string_sprintf("unknown parameter \"%.*s\" precedes LDAP URL",
+ namelen, name);
+ DEBUG(D_lookup) debug_printf_indent("LDAP query error: %s\n", *errmsg);
+ return DEFER;
+ }
+ while (isspace(*url)) url++;
+ continue;
+ }
+ }
+ *errmsg = US"malformed parameter setting precedes LDAP URL";
+ DEBUG(D_lookup) debug_printf_indent("LDAP query error: %s\n", *errmsg);
+ return DEFER;
+ }
+
+/* If user is set, de-URL-quote it. Some LDAP libraries do this for themselves,
+but it seems that not all behave like this. The DN for the user is often the
+result of ${quote_ldap_dn:...} quoting, which does apply URL quoting, because
+that is needed when the DN is used as a base DN in a query. Sigh. This is all
+far too complicated. */
+
+if (user)
+ {
+ uschar *t = user;
+ for (uschar * s = user; *s != 0; s++)
+ {
+ int c, d;
+ if (*s == '%' && isxdigit(c=s[1]) && isxdigit(d=s[2]))
+ {
+ c = tolower(c);
+ d = tolower(d);
+ *t++ =
+ (((c >= 'a')? (10 + c - 'a') : c - '0') << 4) |
+ ((d >= 'a')? (10 + d - 'a') : d - '0');
+ s += 2;
+ }
+ else *t++ = *s;
+ }
+ *t = 0;
+ }
+
+DEBUG(D_lookup)
+ debug_printf_indent("LDAP parameters: user=%s pass=%s size=%d time=%d connect=%d "
+ "dereference=%d referrals=%s\n", user, password, sizelimit, timelimit,
+ tcplimit, dereference, referrals == LDAP_OPT_ON ? "on" : "off");
+
+/* If the request is just to check authentication, some credentials must
+be given. The password must not be empty because LDAP binds with an empty
+password are considered anonymous, and will succeed on most installations. */
+
+if (search_type == SEARCH_LDAP_AUTH)
+ {
+ if (!user || !password)
+ {
+ *errmsg = US"ldapauth lookups must specify the username and password";
+ return DEFER;
+ }
+ if (!*password)
+ {
+ DEBUG(D_lookup) debug_printf_indent("Empty password: ldapauth returns FAIL\n");
+ return FAIL;
+ }
+ }
+
+/* Check for valid ldap url starters */
+
+p = url + 4;
+if (tolower(*p) == 's' || tolower(*p) == 'i') p++;
+if (Ustrncmp(p, "://", 3) != 0)
+ {
+ *errmsg = string_sprintf("LDAP URL does not start with \"ldap://\", "
+ "\"ldaps://\", or \"ldapi://\" (it starts with \"%.16s...\")", url);
+ DEBUG(D_lookup) debug_printf_indent("LDAP query error: %s\n", *errmsg);
+ return DEFER;
+ }
+
+/* No default servers, or URL contains a server name: just one attempt */
+
+if (!eldap_default_servers && !local_servers || p[3] != '/')
+ return perform_ldap_search(url, NULL, 0, search_type, res, errmsg,
+ &defer_break, user, password, sizelimit, timelimit, tcplimit, dereference,
+ referrals);
+
+/* Loop through the servers until OK or FAIL. Use local_servers list
+if defined in the lookup, otherwise use the global default list */
+
+list = local_servers ? local_servers : eldap_default_servers;
+for (uschar * server; server = string_nextinlist(&list, &sep, NULL, 0); )
+ {
+ int rc, port = 0;
+ uschar *colon = Ustrchr(server, ':');
+ if (colon)
+ {
+ *colon = 0;
+ port = Uatoi(colon+1);
+ }
+ rc = perform_ldap_search(url, server, port, search_type, res, errmsg,
+ &defer_break, user, password, sizelimit, timelimit, tcplimit, dereference,
+ referrals);
+ if (rc != DEFER || defer_break) return rc;
+ }
+
+return DEFER;
+}
+
+
+
+/*************************************************
+* Find entry point *
+*************************************************/
+
+/* See local README for interface description. The different kinds of search
+are handled by a common function, with a flag to differentiate between them.
+The handle and filename arguments are not used. */
+
+static int
+eldap_find(void * handle, const uschar * filename, const uschar * ldap_url,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+return(control_ldap_search(ldap_url, SEARCH_LDAP_SINGLE, result, errmsg));
+}
+
+static int
+eldapm_find(void * handle, const uschar * filename, const uschar * ldap_url,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+return(control_ldap_search(ldap_url, SEARCH_LDAP_MULTIPLE, result, errmsg));
+}
+
+static int
+eldapdn_find(void * handle, const uschar * filename, const uschar * ldap_url,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+return(control_ldap_search(ldap_url, SEARCH_LDAP_DN, result, errmsg));
+}
+
+int
+eldapauth_find(void * handle, const uschar * filename, const uschar * ldap_url,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache)
+{
+return(control_ldap_search(ldap_url, SEARCH_LDAP_AUTH, result, errmsg));
+}
+
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static void *
+eldap_open(const uschar * filename, uschar ** errmsg)
+{
+return (void *)(1); /* Just return something non-null */
+}
+
+
+
+/*************************************************
+* Tidy entry point *
+*************************************************/
+
+/* See local README for interface description.
+Make sure that eldap_dn does not refer to reclaimed or worse, freed store */
+
+static void
+eldap_tidy(void)
+{
+eldap_dn = NULL;
+
+for (LDAP_CONNECTION *lcp; lcp = ldap_connections; ldap_connections = lcp->next)
+ {
+ DEBUG(D_lookup) debug_printf_indent("unbind LDAP connection to %s:%d\n",
+ lcp->host, lcp->port);
+ if(lcp->bound) ldap_unbind(lcp->ld);
+ }
+}
+
+
+
+/*************************************************
+* Quote entry point *
+*************************************************/
+
+/* LDAP quoting is unbelievably messy. For a start, two different levels of
+quoting have to be done: LDAP quoting, and URL quoting. The current
+specification is the result of a suggestion by Brian Candler. It recognizes
+two separate cases:
+
+(1) For text that appears in a search filter, the following escapes are
+ required (see RFC 2254):
+
+ * -> \2A
+ ( -> \28
+ ) -> \29
+ \ -> \5C
+ NULL -> \00
+
+ Then the entire filter text must be URL-escaped. This kind of quoting is
+ implemented by ${quote_ldap:....}. Note that we can never have a NULL
+ in the input string, because that's a terminator.
+
+(2) For a DN that is part of a URL (i.e. the base DN), the characters
+
+ , + " \ < > ;
+
+ must be quoted by backslashing. See RFC 2253. Leading and trailing spaces
+ must be escaped, as must a leading #. Then the string must be URL-quoted.
+ This type of quoting is implemented by ${quote_ldap_dn:....}.
+
+For URL quoting, the only characters that need not be quoted are the
+alphamerics and
+
+ ! $ ' ( ) * + - . _
+
+All the others must be hexified and preceded by %. This includes the
+backslashes used for LDAP quoting.
+
+For a DN that is given in the USER parameter for authentication, we need the
+same initial quoting as (2) but in this case, the result must NOT be
+URL-escaped, because it isn't a URL. The way this is handled is by
+de-URL-quoting the text when processing the USER parameter in
+control_ldap_search() above. That means that the same quote operator can be
+used. This has the additional advantage that spaces in the DN won't cause
+parsing problems. For example:
+
+ USER=cn=${quote_ldap_dn:$1},%20dc=example,%20dc=com
+
+should be safe if there are spaces in $1.
+
+
+Arguments:
+ s the string to be quoted
+ opt additional option text or NULL if none
+ only "dn" is recognized
+ idx lookup type index
+
+Returns: the processed string or NULL for a bad option
+*/
+
+
+
+/* The characters in this string, together with alphanumerics, never need
+quoting in any way. */
+
+#define ALWAYS_LITERAL "!$'-._"
+
+/* The special characters in this string do not need to be URL-quoted. The set
+is a bit larger than the general literals. */
+
+#define URL_NONQUOTE ALWAYS_LITERAL "()*+"
+
+/* The following macros define the characters that are quoted by quote_ldap and
+quote_ldap_dn, respectively. */
+
+#define LDAP_QUOTE "*()\\"
+#define LDAP_DN_QUOTE ",+\"\\<>;"
+
+
+
+static uschar *
+eldap_quote(uschar * s, uschar * opt, unsigned idx)
+{
+int c, count = 0, len = 0;
+BOOL dn = FALSE;
+uschar * t = s, * quoted;
+
+/* Test for a DN quotation. */
+
+if (opt)
+ {
+ if (Ustrcmp(opt, "dn") != 0) return NULL; /* No others recognized */
+ dn = TRUE;
+ }
+
+/* Compute how much extra store we need for the string. This doesn't have to be
+exact as long as it isn't an underestimate. The worst case is the addition of 5
+extra bytes for a single character. This occurs for certain characters in DNs,
+where, for example, < turns into %5C%3C. For simplicity, we just add 5 for each
+possibly escaped character. The really fast way would be just to test for
+non-alphanumerics, but it is probably better to spot a few others that are
+never escaped, because if there are no specials at all, we can avoid copying
+the string.
+XXX No longer true; we always copy, to support quoted-enforcement */
+
+while ((c = *t++))
+ {
+ len++;
+ if (!isalnum(c) && Ustrchr(ALWAYS_LITERAL, c) == NULL) count += 5;
+ }
+/*if (count == 0) return s;*/
+
+/* Get sufficient store to hold the quoted string */
+
+t = quoted = store_get_quoted(len + count + 1, s, idx);
+
+/* Handle plain quote_ldap */
+
+if (!dn)
+ {
+ while ((c = *s++))
+ {
+ if (!isalnum(c))
+ {
+ if (Ustrchr(LDAP_QUOTE, c) != NULL)
+ {
+ sprintf(CS t, "%%5C%02X", c); /* e.g. * => %5C2A */
+ t += 5;
+ continue;
+ }
+ if (Ustrchr(URL_NONQUOTE, c) == NULL) /* e.g. ] => %5D */
+ {
+ sprintf(CS t, "%%%02X", c);
+ t += 3;
+ continue;
+ }
+ }
+ *t++ = c; /* unquoted character */
+ }
+ }
+
+/* Handle quote_ldap_dn */
+
+else
+ {
+ uschar * ss = s + len;
+
+ /* Find the last char before any trailing spaces */
+
+ while (ss > s && ss[-1] == ' ') ss--;
+
+ /* Quote leading spaces and sharps */
+
+ for (; s < ss; s++)
+ {
+ if (*s != ' ' && *s != '#') break;
+ sprintf(CS t, "%%5C%%%02X", *s);
+ t += 6;
+ }
+
+ /* Handle the rest of the string, up to the trailing spaces */
+
+ while (s < ss)
+ {
+ c = *s++;
+ if (!isalnum(c))
+ {
+ if (Ustrchr(LDAP_DN_QUOTE, c) != NULL)
+ {
+ Ustrncpy(t, US"%5C", 3); /* insert \ where needed */
+ t += 3; /* fall through to check URL */
+ }
+ if (Ustrchr(URL_NONQUOTE, c) == NULL) /* e.g. ] => %5D */
+ {
+ sprintf(CS t, "%%%02X", c);
+ t += 3;
+ continue;
+ }
+ }
+ *t++ = c; /* unquoted character, or non-URL quoted after %5C */
+ }
+
+ /* Handle the trailing spaces */
+
+ while (*ss++ != 0)
+ {
+ Ustrncpy(t, US"%5C%20", 6);
+ t += 6;
+ }
+ }
+
+/* Terminate the new string and return */
+
+*t = 0;
+return quoted;
+}
+
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+ldap_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: LDAP: Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+
+static lookup_info ldap_lookup_info = {
+ .name = US"ldap", /* lookup name */
+ .type = lookup_querystyle, /* query-style lookup */
+ .open = eldap_open, /* open function */
+ .check = NULL, /* check function */
+ .find = eldap_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = eldap_tidy, /* tidy function */
+ .quote = eldap_quote, /* quoting function */
+ .version_report = ldap_version_report /* version reporting */
+};
+
+static lookup_info ldapdn_lookup_info = {
+ .name = US"ldapdn", /* lookup name */
+ .type = lookup_querystyle, /* query-style lookup */
+ .open = eldap_open, /* sic */ /* open function */
+ .check = NULL, /* check function */
+ .find = eldapdn_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = eldap_tidy, /* sic */ /* tidy function */
+ .quote = eldap_quote, /* sic */ /* quoting function */
+ .version_report = NULL /* no version reporting (redundant) */
+};
+
+static lookup_info ldapm_lookup_info = {
+ .name = US"ldapm", /* lookup name */
+ .type = lookup_querystyle, /* query-style lookup */
+ .open = eldap_open, /* sic */ /* open function */
+ .check = NULL, /* check function */
+ .find = eldapm_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = eldap_tidy, /* sic */ /* tidy function */
+ .quote = eldap_quote, /* sic */ /* quoting function */
+ .version_report = NULL /* no version reporting (redundant) */
+};
+
+#ifdef DYNLOOKUP
+#define ldap_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &ldap_lookup_info, &ldapdn_lookup_info, &ldapm_lookup_info };
+lookup_module_info ldap_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 3 };
+
+/* End of lookups/ldap.c */
diff --git a/src/lookups/ldap.h b/src/lookups/ldap.h
new file mode 100644
index 0000000..ddfda85
--- /dev/null
+++ b/src/lookups/ldap.h
@@ -0,0 +1,13 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) University of Cambridge 1995 - 2015 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* Header for eldapauth_find */
+
+extern int eldapauth_find(void *, uschar *, const uschar *, int, uschar **,
+ uschar **, BOOL *);
+
+/* End of lookups/ldap.h */
diff --git a/src/lookups/lf_check_file.c b/src/lookups/lf_check_file.c
new file mode 100644
index 0000000..7f0f128
--- /dev/null
+++ b/src/lookups/lf_check_file.c
@@ -0,0 +1,113 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) University of Cambridge 1995 - 2009 */
+/* Copyright (c) The Exim Maintainers 2020 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+
+
+/*************************************************
+* Check a file's credentials *
+*************************************************/
+
+/* fstat can normally be expected to work on an open file, but there are some
+NFS states where it may not.
+
+Arguments:
+ fd an open file descriptor or -1
+ filename a file name if fd is -1
+ s_type type of file (S_IFREG or S_IFDIR)
+ modemask a mask specifying mode bits that must *not* be set
+ owners NULL or a list of of allowable uids, count in the first item
+ owngroups NULL or a list of allowable gids, count in the first item
+ type name of lookup type for putting in error message
+ errmsg where to put an error message
+
+Returns: -1 stat() or fstat() failed
+ 0 OK
+ +1 something didn't match
+
+Side effect: sets errno to ERRNO_BADUGID, ERRNO_NOTREGULAR or ERRNO_BADMODE for
+ bad uid/gid, not a regular file, or bad mode; otherwise leaves it
+ to what fstat set it to.
+*/
+
+int
+lf_check_file(int fd, const uschar * filename, int s_type, int modemask,
+ uid_t * owners, gid_t * owngroups, const char * type, uschar ** errmsg)
+{
+struct stat statbuf;
+
+if ((fd >= 0 && fstat(fd, &statbuf) != 0) ||
+ (fd < 0 && Ustat(filename, &statbuf) != 0))
+ {
+ int save_errno = errno;
+ *errmsg = string_sprintf("%s: stat failed", filename);
+ errno = save_errno;
+ return -1;
+ }
+
+if ((statbuf.st_mode & S_IFMT) != s_type)
+ {
+ if (s_type == S_IFREG)
+ {
+ *errmsg = string_sprintf("%s is not a regular file (%s lookup)",
+ filename, type);
+ errno = ERRNO_NOTREGULAR;
+ }
+ else
+ {
+ *errmsg = string_sprintf("%s is not a directory (%s lookup)",
+ filename, type);
+ errno = ERRNO_NOTDIRECTORY;
+ }
+ return +1;
+ }
+
+if ((statbuf.st_mode & modemask) != 0)
+ {
+ *errmsg = string_sprintf("%s (%s lookup): file mode %.4o should not contain "
+ "%.4o", filename, type, statbuf.st_mode & 07777,
+ statbuf.st_mode & modemask);
+ errno = ERRNO_BADMODE;
+ return +1;
+ }
+
+if (owners != NULL)
+ {
+ BOOL uid_ok = FALSE;
+ for (int i = 1; i <= (int)owners[0]; i++)
+ if (owners[i] == statbuf.st_uid) { uid_ok = TRUE; break; }
+ if (!uid_ok)
+ {
+ *errmsg = string_sprintf("%s (%s lookup): file has wrong owner", filename,
+ type);
+ errno = ERRNO_BADUGID;
+ return +1;
+ }
+ }
+
+if (owngroups != NULL)
+ {
+ BOOL gid_ok = FALSE;
+ for (int i = 1; i <= (int)owngroups[0]; i++)
+ if (owngroups[i] == statbuf.st_gid) { gid_ok = TRUE; break; }
+ if (!gid_ok)
+ {
+ *errmsg = string_sprintf("%s (%s lookup): file has wrong group", filename,
+ type);
+ errno = ERRNO_BADUGID;
+ return +1;
+ }
+ }
+
+return 0;
+}
+
+/* End of lf_check_file.c */
diff --git a/src/lookups/lf_functions.h b/src/lookups/lf_functions.h
new file mode 100644
index 0000000..fd9eb30
--- /dev/null
+++ b/src/lookups/lf_functions.h
@@ -0,0 +1,20 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* Copyright (c) The Exim Maintainers 2020 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* Header for the functions that are shared by the lookups */
+
+extern int lf_check_file(int, const uschar *, int, int, uid_t *, gid_t *,
+ const char *, uschar **);
+extern gstring *lf_quote(uschar *, uschar *, int, gstring *);
+extern int lf_sqlperform(const uschar *, const uschar *, const uschar *,
+ const uschar *, uschar **,
+ uschar **, uint *, const uschar *,
+ int(*)(const uschar *, uschar *, uschar **,
+ uschar **, BOOL *, uint *, const uschar *));
+
+/* End of lf_functions.h */
diff --git a/src/lookups/lf_quote.c b/src/lookups/lf_quote.c
new file mode 100644
index 0000000..6f4143d
--- /dev/null
+++ b/src/lookups/lf_quote.c
@@ -0,0 +1,63 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+
+/*************************************************
+* Add string to result, quoting if necessary *
+*************************************************/
+
+/* This function is called by some lookups that create name=value result
+strings, to handle the quoting of the data. It adds "name=" to the result,
+followed by appropriately quoted data, followed by a single space.
+
+Arguments:
+ name the field name
+ value the data value
+ vlength the data length
+ result the result expanding-string
+
+Returns: the result pointer (possibly updated)
+*/
+
+gstring *
+lf_quote(uschar *name, uschar *value, int vlength, gstring * result)
+{
+result = string_append(result, 2, name, US"=");
+
+/* NULL is handled as an empty string */
+
+if (!value)
+ {
+ value = US"";
+ vlength = 0;
+ }
+
+/* Quote the value if it is empty, contains white space, or starts with a quote
+character. */
+
+if (value[0] == 0 || Ustrpbrk(value, " \t\n\r") != NULL || value[0] == '\"')
+ {
+ result = string_catn(result, US"\"", 1);
+ for (int j = 0; j < vlength; j++)
+ {
+ if (value[j] == '\"' || value[j] == '\\')
+ result = string_catn(result, US"\\", 1);
+ result = string_catn(result, US value+j, 1);
+ }
+ result = string_catn(result, US"\"", 1);
+ }
+else
+ result = string_catn(result, US value, vlength);
+
+return string_catn(result, US" ", 1);
+}
+
+/* End of lf_quote.c */
diff --git a/src/lookups/lf_sqlperform.c b/src/lookups/lf_sqlperform.c
new file mode 100644
index 0000000..ce6f163
--- /dev/null
+++ b/src/lookups/lf_sqlperform.c
@@ -0,0 +1,175 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+
+
+/*************************************************
+* Call SQL server(s) to run an actual query *
+*************************************************/
+
+/* All the SQL lookups are of the same form, with a list of servers to try
+until one can be accessed. It is now also possible to provide the server data
+as part of the query. This function manages server selection and looping; each
+lookup has its own function for actually performing the lookup.
+
+Arguments:
+ name the lookup name, e.g. "MySQL"
+ optionname the name of the servers option, e.g. "mysql_servers"
+ optserverlist the value of the servers option
+ query the query
+ result where to pass back the result
+ errmsg where to pass back an error message
+ do_cache to be set zero if data is changed
+ func the lookup function to call
+
+Returns: the return from the lookup function, or DEFER
+*/
+
+int
+lf_sqlperform(const uschar *name, const uschar *optionname,
+ const uschar *optserverlist, const uschar *query,
+ uschar **result, uschar **errmsg, uint *do_cache, const uschar * opts,
+ int(*fn)(const uschar *, uschar *, uschar **, uschar **, BOOL *, uint *, const uschar *))
+{
+int rc;
+uschar *server;
+BOOL defer_break = FALSE;
+
+DEBUG(D_lookup) debug_printf_indent("%s query: \"%s\" opts '%s'\n", name, query, opts);
+
+/* Handle queries that do have server information at the start. */
+
+if (Ustrncmp(query, "servers", 7) == 0)
+ {
+ int qsep = 0;
+ const uschar *s, *ss;
+ const uschar *qserverlist;
+ uschar *qserver;
+
+ s = query + 7;
+ skip_whitespace(&s);
+ if (*s++ != '=')
+ {
+ *errmsg = string_sprintf("missing = after \"servers\" in %s lookup", name);
+ return DEFER;
+ }
+ skip_whitespace(&s);
+
+ ss = Ustrchr(s, ';');
+ if (!ss)
+ {
+ *errmsg = string_sprintf("missing ; after \"servers=\" in %s lookup",
+ name);
+ return DEFER;
+ }
+
+ if (ss == s)
+ {
+ *errmsg = string_sprintf("\"servers=\" defines no servers in \"%s\"",
+ query);
+ return DEFER;
+ }
+
+ qserverlist = string_sprintf("%.*s", (int)(ss - s), s);
+
+ while ((qserver = string_nextinlist(&qserverlist, &qsep, NULL, 0)))
+ {
+ if (Ustrchr(qserver, '/'))
+ server = qserver;
+ else
+ {
+ int len = Ustrlen(qserver);
+ const uschar * serverlist = optserverlist;
+
+ for (int sep = 0; server = string_nextinlist(&serverlist, &sep, NULL, 0);)
+ if (Ustrncmp(server, qserver, len) == 0 && server[len] == '/')
+ break;
+
+ if (!server)
+ {
+ *errmsg = string_sprintf("%s server \"%s\" not found in %s", name,
+ qserver, optionname);
+ return DEFER;
+ }
+ }
+
+ if (is_tainted(server))
+ {
+ *errmsg = string_sprintf("%s server \"%s\" is tainted", name, server);
+ return DEFER;
+ }
+
+ rc = (*fn)(ss+1, server, result, errmsg, &defer_break, do_cache, opts);
+ if (rc != DEFER || defer_break) return rc;
+ }
+ }
+
+/* Handle queries that do not have server information at the start. */
+
+else
+ {
+ const uschar * serverlist = NULL;
+
+ /* If options are present, scan for a server definition. Default to
+ the "optserverlist" srgument. */
+
+ if (opts)
+ {
+ uschar * ele;
+ for (int sep = ','; ele = string_nextinlist(&opts, &sep, NULL, 0); )
+ if (Ustrncmp(ele, "servers=", 8) == 0)
+ { serverlist = ele + 8; break; }
+ }
+
+ if (!serverlist)
+ serverlist = optserverlist;
+ if (!serverlist)
+ *errmsg = string_sprintf("no %s servers defined (%s option)", name,
+ optionname);
+ else
+ for (int d = 0; (server = string_nextinlist(&serverlist, &d, NULL, 0)); )
+ {
+ /* If not a full spec assume from options; scan main list for matching
+ hostname */
+
+ if (!Ustrchr(server, '/'))
+ {
+ int len = Ustrlen(server);
+ const uschar * slist = optserverlist;
+ uschar * ele;
+ for (int sep = 0; ele = string_nextinlist(&slist, &sep, NULL, 0); )
+ if (Ustrncmp(ele, server, len) == 0 && ele[len] == '/')
+ break;
+ if (!ele)
+ {
+ *errmsg = string_sprintf("%s server \"%s\" not found in %s", name,
+ server, optionname);
+ return DEFER;
+ }
+ server = ele;
+ }
+
+ if (is_tainted(server))
+ {
+ *errmsg = string_sprintf("%s server \"%s\" is tainted", name, server);
+ return DEFER;
+ }
+
+ rc = (*fn)(query, server, result, errmsg, &defer_break, do_cache, opts);
+ if (rc != DEFER || defer_break) return rc;
+ }
+ }
+
+return DEFER;
+}
+
+/* End of lf_sqlperform.c */
diff --git a/src/lookups/lmdb.c b/src/lookups/lmdb.c
new file mode 100644
index 0000000..a32c7f7
--- /dev/null
+++ b/src/lookups/lmdb.c
@@ -0,0 +1,162 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 2016 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+
+#ifdef LOOKUP_LMDB
+
+#include <lmdb.h>
+
+typedef struct lmdbstrct
+{
+MDB_txn *txn;
+MDB_dbi db_dbi;
+} Lmdbstrct;
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+static void *
+lmdb_open(const uschar * filename, uschar ** errmsg)
+{
+MDB_env * db_env = NULL;
+Lmdbstrct * lmdb_p;
+int ret, save_errno;
+const uschar * errstr;
+
+lmdb_p = store_get(sizeof(Lmdbstrct), GET_UNTAINTED);
+lmdb_p->txn = NULL;
+
+if ((ret = mdb_env_create(&db_env)))
+ {
+ errstr = US"create environment";
+ goto bad;
+ }
+
+if ((ret = mdb_env_open(db_env, CS filename, MDB_NOSUBDIR|MDB_RDONLY, 0660)))
+ {
+ errstr = string_sprintf("open environment with %s", filename);
+ goto bad;
+ }
+
+if ((ret = mdb_txn_begin(db_env, NULL, MDB_RDONLY, &lmdb_p->txn)))
+ {
+ errstr = US"start transaction";
+ goto bad;
+ }
+
+if ((ret = mdb_open(lmdb_p->txn, NULL, 0, &lmdb_p->db_dbi)))
+ {
+ errstr = US"open database";
+ goto bad;
+ }
+
+return lmdb_p;
+
+bad:
+ save_errno = errno;
+ if (lmdb_p->txn) mdb_txn_abort(lmdb_p->txn);
+ if (db_env) mdb_env_close(db_env);
+ *errmsg = string_sprintf("LMDB: Unable to %s: %s", errstr, mdb_strerror(ret));
+ errno = save_errno;
+ return NULL;
+}
+
+
+/*************************************************
+* Find entry point *
+*************************************************/
+
+static int
+lmdb_find(void * handle, const uschar * filename,
+ const uschar * keystring, int length, uschar ** result, uschar ** errmsg,
+ uint * do_cache, const uschar * opts)
+{
+int ret;
+MDB_val dbkey, data;
+Lmdbstrct * lmdb_p = handle;
+
+dbkey.mv_data = CS keystring;
+dbkey.mv_size = length;
+
+DEBUG(D_lookup) debug_printf_indent("LMDB: lookup key: %s\n", CS keystring);
+
+if ((ret = mdb_get(lmdb_p->txn, lmdb_p->db_dbi, &dbkey, &data)) == 0)
+ {
+ *result = string_copyn(US data.mv_data, data.mv_size);
+ DEBUG(D_lookup) debug_printf_indent("LMDB: lookup result: %s\n", *result);
+ return OK;
+ }
+else if (ret == MDB_NOTFOUND)
+ {
+ *errmsg = US"LMDB: lookup, no data found";
+ DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
+ return FAIL;
+ }
+else
+ {
+ *errmsg = string_sprintf("LMDB: lookup error: %s", mdb_strerror(ret));
+ DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
+ return DEFER;
+ }
+}
+
+
+/*************************************************
+* Close entry point *
+*************************************************/
+
+static void
+lmdb_close(void * handle)
+{
+Lmdbstrct * lmdb_p = handle;
+MDB_env * db_env = mdb_txn_env(lmdb_p->txn);
+mdb_txn_abort(lmdb_p->txn);
+mdb_env_close(db_env);
+}
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+#include "../version.h"
+
+gstring *
+lmdb_version_report(gstring * g)
+{
+g = string_fmt_append(g, "Library version: LMDB: Compile: %d.%d.%d\n",
+ MDB_VERSION_MAJOR, MDB_VERSION_MINOR, MDB_VERSION_PATCH);
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, " Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+static lookup_info lmdb_lookup_info = {
+ .name = US"lmdb", /* lookup name */
+ .type = lookup_absfile, /* query-style lookup */
+ .open = lmdb_open, /* open function */
+ .check = NULL, /* no check function */
+ .find = lmdb_find, /* find function */
+ .close = lmdb_close, /* close function */
+ .tidy = NULL, /* tidy function */
+ .quote = NULL, /* quoting function */
+ .version_report = lmdb_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+# define lmdb_lookup_module_info _lookup_module_info
+#endif /* DYNLOOKUP */
+
+static lookup_info *_lookup_list[] = { &lmdb_lookup_info };
+lookup_module_info lmdb_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+#endif /* LOOKUP_LMDB */
diff --git a/src/lookups/lsearch.c b/src/lookups/lsearch.c
new file mode 100644
index 0000000..dcfdec9
--- /dev/null
+++ b/src/lookups/lsearch.c
@@ -0,0 +1,487 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+/* Codes for the different kinds of lsearch that are supported */
+
+enum {
+ LSEARCH_PLAIN, /* Literal keys */
+ LSEARCH_WILD, /* Wild card keys, expanded */
+ LSEARCH_NWILD, /* Wild card keys, not expanded */
+ LSEARCH_IP /* IP addresses and networks */
+};
+
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description */
+
+static void *
+lsearch_open(const uschar * filename, uschar ** errmsg)
+{
+FILE * f = Ufopen(filename, "rb");
+if (!f)
+ *errmsg = string_open_failed("%s for linear search", filename);
+return f;
+}
+
+
+
+/*************************************************
+* Check entry point *
+*************************************************/
+
+static BOOL
+lsearch_check(void *handle, const uschar *filename, int modemask, uid_t *owners,
+ gid_t *owngroups, uschar **errmsg)
+{
+return lf_check_file(fileno((FILE *)handle), filename, S_IFREG, modemask,
+ owners, owngroups, "lsearch", errmsg) == 0;
+}
+
+
+
+/*************************************************
+* Internal function for the various lsearches *
+*************************************************/
+
+/* See local README for interface description, plus:
+
+Extra argument:
+
+ type one of the values LSEARCH_PLAIN, LSEARCH_WILD, LSEARCH_NWILD, or
+ LSEARCH_IP
+
+There is some messy logic in here to cope with very long data lines that do not
+fit into the fixed sized buffer. Most of the time this will never be exercised,
+but people do occasionally do weird things. */
+
+static int
+internal_lsearch_find(void * handle, const uschar * filename,
+ const uschar * keystring, int length, uschar ** result, uschar ** errmsg,
+ int type, const uschar * opts)
+{
+FILE *f = handle;
+BOOL ret_full = FALSE;
+int old_pool = store_pool;
+rmark reset_point = NULL;
+uschar buffer[4096];
+
+if (opts)
+ {
+ int sep = ',';
+ uschar * ele;
+
+ while ((ele = string_nextinlist(&opts, &sep, NULL, 0)))
+ if (Ustrcmp(ele, "ret=full") == 0)
+ { ret_full = TRUE; break; }
+ }
+
+/* Wildcard searches may use up some store, because of expansions. We don't
+want them to fill up our search store. What we do is set the pool to the main
+pool and get a point to reset to later. Wildcard searches could also issue
+lookups, but internal_search_find will take care of that, and the cache will be
+safely stored in the search pool again. */
+
+if (type == LSEARCH_WILD || type == LSEARCH_NWILD)
+ {
+ store_pool = POOL_MAIN;
+ reset_point = store_mark();
+ }
+
+rewind(f);
+for (BOOL this_is_eol, last_was_eol = TRUE;
+ Ufgets(buffer, sizeof(buffer), f) != NULL;
+ last_was_eol = this_is_eol)
+ {
+ int p = Ustrlen(buffer);
+ int linekeylength;
+ BOOL this_is_comment;
+ gstring * yield;
+ uschar *s = buffer;
+
+ /* Check whether this the final segment of a line. If it follows an
+ incomplete part-line, skip it. */
+
+ this_is_eol = p > 0 && buffer[p-1] == '\n';
+ if (!last_was_eol) continue;
+
+ /* We now have the start of a physical line. If this is a final line segment,
+ remove trailing white space. */
+
+ if (this_is_eol)
+ {
+ while (p > 0 && isspace((uschar)buffer[p-1])) p--;
+ buffer[p] = 0;
+ }
+
+ /* If the buffer is empty it might be (a) a complete empty line, or (b) the
+ start of a line that begins with so much white space that it doesn't all fit
+ in the buffer. In both cases we want to skip the entire physical line.
+
+ If the buffer begins with # it is a comment line; if it begins with white
+ space it is a logical continuation; again, we want to skip the entire
+ physical line. */
+
+ if (buffer[0] == 0 || buffer[0] == '#' || isspace(buffer[0])) continue;
+
+ /* We assume that they key will fit in the buffer. If the key starts with ",
+ read it as a quoted string. We don't use string_dequote() because that uses
+ new store for the result, and we may be doing this many times in a long file.
+ We know that the dequoted string must be shorter than the original, because
+ we are removing the quotes, and also any escape sequences always turn two or
+ more characters into one character. Therefore, we can store the new string in
+ the same buffer. */
+
+ if (*s == '\"')
+ {
+ uschar *t = s++;
+ while (*s && *s != '\"')
+ {
+ *t++ = *s == '\\' ? string_interpret_escape(CUSS &s) : *s;
+ s++;
+ }
+ linekeylength = t - buffer;
+ if (*s) s++; /* Past terminating " */
+ if (ret_full)
+ memmove(t, s, Ustrlen(s)+1); /* copy the rest of line also */
+ }
+
+ /* Otherwise it is terminated by a colon or white space */
+
+ else
+ {
+ while (*s && *s != ':' && !isspace(*s)) s++;
+ linekeylength = s - buffer;
+ }
+
+ /* The matching test depends on which kind of lsearch we are doing */
+
+ switch(type)
+ {
+ /* A plain lsearch treats each key as a literal */
+
+ case LSEARCH_PLAIN:
+ if (linekeylength != length || strncmpic(buffer, keystring, length) != 0)
+ continue;
+ break; /* Key matched */
+
+ /* A wild lsearch treats each key as a possible wildcarded string; no
+ expansion is done for nwildlsearch. */
+
+ case LSEARCH_WILD:
+ case LSEARCH_NWILD:
+ {
+ int rc;
+ int save = buffer[linekeylength];
+ const uschar *list = buffer;
+ buffer[linekeylength] = 0;
+ rc = match_isinlist(keystring,
+ &list,
+ UCHAR_MAX+1, /* Single-item list */
+ NULL, /* No anchor */
+ NULL, /* No caching */
+ MCL_STRING + (type == LSEARCH_WILD ? 0 : MCL_NOEXPAND),
+ TRUE, /* Caseless */
+ NULL);
+ buffer[linekeylength] = save;
+ if (rc == FAIL) continue;
+ if (rc == DEFER) return DEFER;
+ }
+
+ /* The key has matched. If the search involved a regular expression, it
+ might have caused numerical variables to be set. However, their values will
+ be in the wrong storage pool for external use. Copying them to the standard
+ pool is not feasible because of the caching of lookup results - a repeated
+ lookup will not match the regular expression again. Therefore, we drop
+ all numeric variables at this point. */
+
+ expand_nmax = -1;
+ break;
+
+ /* Compare an ip address against a list of network/ip addresses. We have to
+ allow for the "*" case specially. */
+
+ case LSEARCH_IP:
+ if (linekeylength == 1 && buffer[0] == '*')
+ {
+ if (length != 1 || keystring[0] != '*') continue;
+ }
+ else if (length == 1 && keystring[0] == '*') continue;
+ else
+ {
+ int maskoffset;
+ int save = buffer[linekeylength];
+ buffer[linekeylength] = 0;
+ if (string_is_ip_address(buffer, &maskoffset) == 0 ||
+ !host_is_in_net(keystring, buffer, maskoffset)) continue;
+ buffer[linekeylength] = save;
+ }
+ break; /* Key matched */
+ }
+
+ /* The key has matched. Skip spaces after the key, and allow an optional
+ colon after the spaces. This is an odd specification, but it's for
+ compatibility. */
+
+ if (!ret_full)
+ if (Uskip_whitespace(&s) == ':')
+ {
+ s++;
+ Uskip_whitespace(&s);
+ }
+
+ /* Reset dynamic store, if we need to, and revert to the search pool */
+
+ if (reset_point)
+ {
+ reset_point = store_reset(reset_point);
+ store_pool = old_pool;
+ }
+
+ /* Now we want to build the result string to contain the data. There can be
+ two kinds of continuation: (a) the physical line may not all have fitted into
+ the buffer, and (b) there may be logical continuation lines, for which we
+ must convert all leading white space into a single blank.
+
+ Initialize, and copy the first segment of data. */
+
+ this_is_comment = FALSE;
+ yield = string_get(100);
+ if (ret_full)
+ yield = string_cat(yield, buffer);
+ else if (*s)
+ yield = string_cat(yield, s);
+
+ /* Now handle continuations */
+
+ for (last_was_eol = this_is_eol;
+ Ufgets(buffer, sizeof(buffer), f) != NULL;
+ last_was_eol = this_is_eol)
+ {
+ s = buffer;
+ p = Ustrlen(buffer);
+ this_is_eol = p > 0 && buffer[p-1] == '\n';
+
+ /* Remove trailing white space from a physical line end */
+
+ if (this_is_eol)
+ {
+ while (p > 0 && isspace((uschar)buffer[p-1])) p--;
+ buffer[p] = 0;
+ }
+
+ /* If this is not a physical line continuation, skip it entirely if it's
+ empty or starts with #. Otherwise, break the loop if it doesn't start with
+ white space. Otherwise, replace leading white space with a single blank. */
+
+ if (last_was_eol)
+ {
+ this_is_comment = (this_is_comment || (buffer[0] == 0 || buffer[0] == '#'));
+ if (this_is_comment) continue;
+ if (!isspace((uschar)buffer[0])) break;
+ while (isspace((uschar)*s)) s++;
+ *(--s) = ' ';
+ }
+ if (this_is_comment) continue;
+
+ /* Join a physical or logical line continuation onto the result string. */
+
+ yield = string_cat(yield, s);
+ }
+
+ gstring_release_unused(yield);
+ *result = string_from_gstring(yield);
+ return OK;
+ }
+
+/* Reset dynamic store, if we need to */
+
+if (reset_point)
+ {
+ store_reset(reset_point);
+ store_pool = old_pool;
+ }
+
+return FAIL;
+}
+
+
+/*************************************************
+* Find entry point for lsearch *
+*************************************************/
+
+/* See local README for interface description */
+
+static int
+lsearch_find(void * handle, const uschar * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+return internal_lsearch_find(handle, filename, keystring, length, result,
+ errmsg, LSEARCH_PLAIN, opts);
+}
+
+
+
+/*************************************************
+* Find entry point for wildlsearch *
+*************************************************/
+
+/* See local README for interface description */
+
+static int
+wildlsearch_find(void * handle, const uschar * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+return internal_lsearch_find(handle, filename, keystring, length, result,
+ errmsg, LSEARCH_WILD, opts);
+}
+
+
+
+/*************************************************
+* Find entry point for nwildlsearch *
+*************************************************/
+
+/* See local README for interface description */
+
+static int
+nwildlsearch_find(void * handle, const uschar * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+return internal_lsearch_find(handle, filename, keystring, length, result,
+ errmsg, LSEARCH_NWILD, opts);
+}
+
+
+
+
+/*************************************************
+* Find entry point for iplsearch *
+*************************************************/
+
+/* See local README for interface description */
+
+static int
+iplsearch_find(void * handle, uschar const * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+if ((length == 1 && keystring[0] == '*') ||
+ string_is_ip_address(keystring, NULL) != 0)
+ return internal_lsearch_find(handle, filename, keystring, length, result,
+ errmsg, LSEARCH_IP, opts);
+
+*errmsg = string_sprintf("\"%s\" is not a valid iplsearch key (an IP "
+"address, with optional CIDR mask, is wanted): "
+"in a host list, use net-iplsearch as the search type", keystring);
+return DEFER;
+}
+
+
+
+
+/*************************************************
+* Close entry point *
+*************************************************/
+
+/* See local README for interface description */
+
+static void
+lsearch_close(void *handle)
+{
+(void)fclose((FILE *)handle);
+}
+
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+lsearch_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: lsearch: Exim version %s\n", EXIM_VERSION_STR));
+#endif
+return g;
+}
+
+
+static lookup_info iplsearch_lookup_info = {
+ .name = US"iplsearch", /* lookup name */
+ .type = lookup_absfile, /* uses absolute file name */
+ .open = lsearch_open, /* open function */
+ .check = lsearch_check, /* check function */
+ .find = iplsearch_find, /* find function */
+ .close = lsearch_close, /* close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = NULL /* no version reporting (redundant) */
+};
+
+static lookup_info lsearch_lookup_info = {
+ .name = US"lsearch", /* lookup name */
+ .type = lookup_absfile, /* uses absolute file name */
+ .open = lsearch_open, /* open function */
+ .check = lsearch_check, /* check function */
+ .find = lsearch_find, /* find function */
+ .close = lsearch_close, /* close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = lsearch_version_report /* version reporting */
+};
+
+static lookup_info nwildlsearch_lookup_info = {
+ .name = US"nwildlsearch", /* lookup name */
+ .type = lookup_absfile, /* uses absolute file name */
+ .open = lsearch_open, /* open function */
+ .check = lsearch_check, /* check function */
+ .find = nwildlsearch_find, /* find function */
+ .close = lsearch_close, /* close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = NULL /* no version reporting (redundant) */
+};
+
+static lookup_info wildlsearch_lookup_info = {
+ .name = US"wildlsearch", /* lookup name */
+ .type = lookup_absfile, /* uses absolute file name */
+ .open = lsearch_open, /* open function */
+ .check = lsearch_check, /* check function */
+ .find = wildlsearch_find, /* find function */
+ .close = lsearch_close, /* close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = NULL /* no version reporting (redundant) */
+};
+
+#ifdef DYNLOOKUP
+#define lsearch_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &iplsearch_lookup_info,
+ &lsearch_lookup_info,
+ &nwildlsearch_lookup_info,
+ &wildlsearch_lookup_info };
+lookup_module_info lsearch_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 4 };
+
+/* End of lookups/lsearch.c */
diff --git a/src/lookups/mysql.c b/src/lookups/mysql.c
new file mode 100644
index 0000000..78b8c2b
--- /dev/null
+++ b/src/lookups/mysql.c
@@ -0,0 +1,503 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* Thanks to Paul Kelly for contributing the original code for these
+functions. */
+
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+#include <mysql.h> /* The system header */
+
+/* We define symbols for *_VERSION_ID (numeric), *_VERSION_STR (char*)
+and *_BASE_STR (char*). It's a bit of guesswork. Especially for mariadb
+with versions before 10.2, as they do not define there there specific symbols.
+*/
+
+/* Newer (>= 10.2) MariaDB */
+#if defined MARIADB_VERSION_ID
+#define EXIM_MxSQL_VERSION_ID MARIADB_VERSION_ID
+
+/* MySQL defines MYSQL_VERSION_ID, and MariaDB does so */
+/* https://dev.mysql.com/doc/refman/5.7/en/c-api-server-client-versions.html */
+#elif defined LIBMYSQL_VERSION_ID
+#define EXIM_MxSQL_VERSION_ID LIBMYSQL_VERSION_ID
+#elif defined MYSQL_VERSION_ID
+#define EXIM_MxSQL_VERSION_ID MYSQL_VERSION_ID
+
+#else
+#define EXIM_MYSQL_VERSION_ID 0
+#endif
+
+/* Newer (>= 10.2) MariaDB */
+#ifdef MARIADB_CLIENT_VERSION_STR
+#define EXIM_MxSQL_VERSION_STR MARIADB_CLIENT_VERSION_STR
+
+/* Mysql uses MYSQL_SERVER_VERSION */
+#elif defined LIBMYSQL_VERSION
+#define EXIM_MxSQL_VERSION_STR LIBMYSQL_VERSION
+#elif defined MYSQL_SERVER_VERSION
+#define EXIM_MxSQL_VERSION_STR MYSQL_SERVER_VERSION
+
+#else
+#define EXIM_MxSQL_VERSION_STR "unknown"
+#endif
+
+#if defined MARIADB_BASE_VERSION
+#define EXIM_MxSQL_BASE_STR MARIADB_BASE_VERSION
+
+#elif defined MARIADB_PACKAGE_VERSION
+#define EXIM_MxSQL_BASE_STR "mariadb"
+
+#elif defined MYSQL_BASE_VERSION
+#define EXIM_MxSQL_BASE_STR MYSQL_BASE_VERSION
+
+#else
+#define EXIM_MxSQL_BASE_STR "n.A."
+#endif
+
+
+/* Structure and anchor for caching connections. */
+
+typedef struct mysql_connection {
+ struct mysql_connection *next;
+ uschar *server;
+ MYSQL *handle;
+} mysql_connection;
+
+static mysql_connection *mysql_connections = NULL;
+
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static void *
+mysql_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
+mysql_tidy(void)
+{
+mysql_connection *cn;
+while ((cn = mysql_connections) != NULL)
+ {
+ mysql_connections = cn->next;
+ DEBUG(D_lookup) debug_printf_indent("close MYSQL connection: %s\n", cn->server);
+ mysql_close(cn->handle);
+ }
+}
+
+
+
+/*************************************************
+* 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
+ do_cache set zero if data is changed
+ opts options
+
+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_mysql_search(const uschar *query, uschar *server, uschar **resultptr,
+ uschar **errmsg, BOOL *defer_break, uint *do_cache, const uschar * opts)
+{
+MYSQL *mysql_handle = NULL; /* Keep compilers happy */
+MYSQL_RES *mysql_result = NULL;
+MYSQL_ROW mysql_row_data;
+MYSQL_FIELD *fields;
+
+int i;
+int yield = DEFER;
+unsigned int num_fields;
+gstring * result = NULL;
+mysql_connection *cn;
+uschar *server_copy = NULL;
+uschar *sdata[4];
+
+/* 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 = 3; i > 0; i--)
+ {
+ uschar *pp = Ustrrchr(server, '/');
+ if (!pp)
+ {
+ *errmsg = string_sprintf("incomplete MySQL server data: %s",
+ (i == 3)? server : server_copy);
+ *defer_break = TRUE;
+ return DEFER;
+ }
+ *pp++ = 0;
+ sdata[i] = pp;
+ if (i == 3) 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 = mysql_connections; cn; cn = cn->next)
+ if (Ustrcmp(cn->server, server_copy) == 0)
+ { mysql_handle = cn->handle; break; }
+
+/* If no cached connection, we must set one up. Mysql allows for a host name
+and port to be specified. It also allows the name of a Unix socket to be used.
+Unfortunately, this contains slashes, but its use is expected to be rare, so
+the rather cumbersome syntax shouldn't inconvenience too many people. We use
+this: host:port(socket)[group] where all the parts are optional.
+The "group" parameter specifies an option group from a MySQL option file. */
+
+if (!cn)
+ {
+ uschar *p;
+ uschar *socket = NULL;
+ int port = 0;
+ uschar *group = US"exim";
+
+ if ((p = Ustrchr(sdata[0], '[')))
+ {
+ *p++ = 0;
+ group = p;
+ while (*p && *p != ']') p++;
+ *p = 0;
+ }
+
+ if ((p = Ustrchr(sdata[0], '(')))
+ {
+ *p++ = 0;
+ socket = p;
+ while (*p && *p != ')') p++;
+ *p = 0;
+ }
+
+ if ((p = Ustrchr(sdata[0], ':')))
+ {
+ *p++ = 0;
+ port = Uatoi(p);
+ }
+
+ if (Ustrchr(sdata[0], '/'))
+ {
+ *errmsg = string_sprintf("unexpected slash in MySQL server hostname: %s",
+ sdata[0]);
+ *defer_break = TRUE;
+ return DEFER;
+ }
+
+ /* If the database is the empty string, set it NULL - the query must then
+ define it. */
+
+ if (sdata[1][0] == 0) sdata[1] = NULL;
+
+ DEBUG(D_lookup)
+ debug_printf_indent("MYSQL new connection: host=%s port=%d socket=%s "
+ "database=%s user=%s\n", sdata[0], port, socket, sdata[1], sdata[2]);
+
+ /* Get store for a new handle, initialize it, and connect to the server */
+
+ mysql_handle = store_get(sizeof(MYSQL), GET_UNTAINTED);
+ mysql_init(mysql_handle);
+ mysql_options(mysql_handle, MYSQL_READ_DEFAULT_GROUP, CS group);
+ if (mysql_real_connect(mysql_handle,
+ /* host user passwd database */
+ CS sdata[0], CS sdata[2], CS sdata[3], CS sdata[1],
+ port, CS socket, CLIENT_MULTI_RESULTS) == NULL)
+ {
+ *errmsg = string_sprintf("MYSQL connection failed: %s",
+ mysql_error(mysql_handle));
+ *defer_break = FALSE;
+ goto MYSQL_EXIT;
+ }
+
+ /* Add the connection to the cache */
+
+ cn = store_get(sizeof(mysql_connection), GET_UNTAINTED);
+ cn->server = server_copy;
+ cn->handle = mysql_handle;
+ cn->next = mysql_connections;
+ mysql_connections = cn;
+ }
+
+/* Else use a previously cached connection */
+
+else
+ {
+ DEBUG(D_lookup)
+ debug_printf_indent("MYSQL using cached connection for %s\n", server_copy);
+ }
+
+/* Run the query */
+
+if (mysql_query(mysql_handle, CS query) != 0)
+ {
+ *errmsg = string_sprintf("MYSQL: query failed: %s\n",
+ mysql_error(mysql_handle));
+ *defer_break = FALSE;
+ goto MYSQL_EXIT;
+ }
+
+/* Pick up the result. If the query was not of the type that returns data,
+namely INSERT, UPDATE, or DELETE, an error occurs here. However, this situation
+can be detected by calling mysql_field_count(). If its result is zero, no data
+was expected (this is all explained clearly in the MySQL manual). In this case,
+we return the number of rows affected by the command. In this event, we do NOT
+want to cache the result; also the whole cache for the handle must be cleaned
+up. Setting do_cache zero requests this. */
+
+if (!(mysql_result = mysql_use_result(mysql_handle)))
+ {
+ if (mysql_field_count(mysql_handle) == 0)
+ {
+ DEBUG(D_lookup) debug_printf_indent("MYSQL: query was not one that returns data\n");
+ result = string_cat(result,
+ string_sprintf("%lld", mysql_affected_rows(mysql_handle)));
+ *do_cache = 0;
+ goto MYSQL_EXIT;
+ }
+ *errmsg = string_sprintf("MYSQL: lookup result failed: %s\n",
+ mysql_error(mysql_handle));
+ *defer_break = FALSE;
+ goto MYSQL_EXIT;
+ }
+
+/* Find the number of fields returned. If this is one, we don't add field
+names to the data. Otherwise we do. */
+
+num_fields = mysql_num_fields(mysql_result);
+
+/* Get the fields and construct the result string. If there is more than one
+row, we insert '\n' between them. */
+
+fields = mysql_fetch_fields(mysql_result);
+
+while ((mysql_row_data = mysql_fetch_row(mysql_result)))
+ {
+ unsigned long * lengths = mysql_fetch_lengths(mysql_result);
+
+ if (result)
+ result = string_catn(result, US"\n", 1);
+
+ if (num_fields != 1)
+ for (int i = 0; i < num_fields; i++)
+ result = lf_quote(US fields[i].name, US mysql_row_data[i], lengths[i],
+ result);
+
+ else if (mysql_row_data[0] != NULL) /* NULL value yields nothing */
+ result = lengths[0] == 0 && !result
+ ? string_get(1) /* for 0-len string result ensure non-null gstring */
+ : string_catn(result, US mysql_row_data[0], lengths[0]);
+ }
+
+/* more results? -1 = no, >0 = error, 0 = yes (keep looping)
+ This is needed because of the CLIENT_MULTI_RESULTS on mysql_real_connect(),
+ we don't expect any more results. */
+
+while((i = mysql_next_result(mysql_handle)) >= 0)
+ if(i != 0)
+ {
+ *errmsg = string_sprintf(
+ "MYSQL: lookup result error when checking for more results: %s\n",
+ mysql_error(mysql_handle));
+ goto MYSQL_EXIT;
+ }
+ else /* just ignore more results */
+ DEBUG(D_lookup) debug_printf_indent("MYSQL: got unexpected more results\n");
+
+/* 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"MYSQL: no data found";
+ }
+
+/* Get here by goto from various error checks and from the case where no data
+was read (e.g. an update query). */
+
+MYSQL_EXIT:
+
+/* Free mysal store for any result that was got; don't close the connection, as
+it is cached. */
+
+if (mysql_result) mysql_free_result(mysql_result);
+
+/* Non-NULL result indicates a successful result */
+
+if (result)
+ {
+ *resultptr = string_from_gstring(result);
+ gstring_release_unused(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. The code to loop through a list of servers while the
+query is deferred with a retryable error is now in a separate function that is
+shared with other SQL lookups. */
+
+static int
+mysql_find(void * handle, const uschar * filename, const uschar * query,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+return lf_sqlperform(US"MySQL", US"mysql_servers", mysql_servers, query,
+ result, errmsg, do_cache, opts, perform_mysql_search);
+}
+
+
+
+/*************************************************
+* 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 *
+mysql_quote(uschar * s, uschar * opt, unsigned idx)
+{
+int c, count = 0;
+uschar * t = s, * quoted;
+
+if (opt) return NULL; /* No options recognized */
+
+while ((c = *t++))
+ if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL) count++;
+
+/* Old code: if (count == 0) return s;
+Now always allocate and copy, to track the quoted status. */
+
+t = quoted = store_get_quoted(Ustrlen(s) + count + 1, s, idx);
+
+while ((c = *s++))
+ {
+ if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL)
+ {
+ *t++ = '\\';
+ switch(c)
+ {
+ case '\n': *t++ = 'n'; break;
+ case '\t': *t++ = 't'; break;
+ case '\r': *t++ = 'r'; break;
+ case '\b': *t++ = 'b'; break;
+ default: *t++ = c; break;
+ }
+ }
+ else *t++ = c;
+ }
+
+*t = 0;
+return quoted;
+}
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+mysql_version_report(gstring * g)
+{
+g = string_fmt_append(g,
+ "Library version: MySQL: Compile: %lu %s [%s]\n"
+ " Runtime: %lu %s\n",
+ (long)EXIM_MxSQL_VERSION_ID, EXIM_MxSQL_VERSION_STR, EXIM_MxSQL_BASE_STR,
+ mysql_get_client_version(), mysql_get_client_info());
+#ifdef DYNLOOKUP
+g = string_fmt_append(g,
+ " Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+/* These are the lookup_info blocks for this driver */
+
+static lookup_info mysql_lookup_info = {
+ .name = US"mysql", /* lookup name */
+ .type = lookup_querystyle, /* query-style lookup */
+ .open = mysql_open, /* open function */
+ .check = NULL, /* no check function */
+ .find = mysql_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = mysql_tidy, /* tidy function */
+ .quote = mysql_quote, /* quoting function */
+ .version_report = mysql_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+#define mysql_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &mysql_lookup_info };
+lookup_module_info mysql_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* End of lookups/mysql.c */
diff --git a/src/lookups/nis.c b/src/lookups/nis.c
new file mode 100644
index 0000000..0024f44
--- /dev/null
+++ b/src/lookups/nis.c
@@ -0,0 +1,141 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2015 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+#include <rpcsvc/ypclnt.h>
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description. This serves for both
+the "nis" and "nis0" lookup types. */
+
+static void *
+nis_open(const uschar * filename, uschar ** errmsg)
+{
+char *nis_domain;
+if (yp_get_default_domain(&nis_domain) != 0)
+ {
+ *errmsg = US"failed to get default NIS domain";
+ return NULL;
+ }
+return nis_domain;
+}
+
+
+
+/*************************************************
+* Find entry point for nis *
+*************************************************/
+
+/* See local README for interface description. A separate function is used
+for nis0 because they are so short it isn't worth trying to use any common
+code. */
+
+static int
+nis_find(void * handle, const uschar * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+int rc;
+uschar *nis_data;
+int nis_data_length;
+do_cache = do_cache; /* Placate picky compilers */
+if ((rc = yp_match(CCS handle, CCS filename, CCS keystring, length,
+ CSS &nis_data, &nis_data_length)) == 0)
+ {
+ *result = string_copy(nis_data);
+ (*result)[nis_data_length] = 0; /* remove final '\n' */
+ return OK;
+ }
+return (rc == YPERR_KEY || rc == YPERR_MAP)? FAIL : DEFER;
+}
+
+
+
+/*************************************************
+* Find entry point for nis0 *
+*************************************************/
+
+/* See local README for interface description. */
+
+static int
+nis0_find(void * handle, const uschar * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+int rc;
+uschar *nis_data;
+int nis_data_length;
+do_cache = do_cache; /* Placate picky compilers */
+if ((rc = yp_match(CCS handle, CCS filename, CCS keystring, length + 1,
+ CSS &nis_data, &nis_data_length)) == 0)
+ {
+ *result = string_copy(nis_data);
+ (*result)[nis_data_length] = 0; /* remove final '\n' */
+ return OK;
+ }
+return (rc == YPERR_KEY || rc == YPERR_MAP)? FAIL : DEFER;
+}
+
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+nis_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: NIS: Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+
+static lookup_info nis_lookup_info = {
+ .name = US"nis", /* lookup name */
+ .type = 0, /* not abs file, not query style*/
+ .open = nis_open, /* open function */
+ .check = NULL, /* check function */
+ .find = nis_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = nis_version_report /* version reporting */
+};
+
+static lookup_info nis0_lookup_info = {
+ .name = US"nis0", /* lookup name */
+ .type = 0, /* not absfile, not query style */
+ .open = nis_open, /* sic */ /* open function */
+ .check = NULL, /* check function */
+ .find = nis0_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = NULL /* no version reporting (redundant) */
+};
+
+#ifdef DYNLOOKUP
+#define nis_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &nis_lookup_info, &nis0_lookup_info };
+lookup_module_info nis_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 2 };
+
+/* End of lookups/nis.c */
diff --git a/src/lookups/nisplus.c b/src/lookups/nisplus.c
new file mode 100644
index 0000000..d9f3f7d
--- /dev/null
+++ b/src/lookups/nisplus.c
@@ -0,0 +1,294 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+#include <rpcsvc/nis.h>
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static void *
+nisplus_open(const uschar * filename, uschar ** errmsg)
+{
+return (void *)(1); /* Just return something non-null */
+}
+
+
+
+/*************************************************
+* Find entry point *
+*************************************************/
+
+/* See local README for interface description. The format of queries for a
+NIS+ search is
+
+ [field=value,...],table-name
+or
+ [field=value,...],table-name:result-field-name
+
+in other words, a normal NIS+ "indexed name", with an optional result field
+name tagged on the end after a colon. If there is no result-field name, the
+yield is the concatenation of all the fields, preceded by their names and an
+equals sign. */
+
+static int
+nisplus_find(void * handle, const uschar * filename, const uschar * query,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+int error_error = FAIL;
+const uschar * field_name = NULL;
+nis_result *nrt = NULL;
+nis_result *nre = NULL;
+nis_object *tno, *eno;
+struct entry_obj *eo;
+struct table_obj *ta;
+const uschar * p = query + length;
+gstring * yield = NULL;
+
+do_cache = do_cache; /* Placate picky compilers */
+
+/* Search backwards for a colon to see if a result field name
+has been given. */
+
+while (p > query && p[-1] != ':') p--;
+
+if (p > query) /* get the query without the result-field */
+ {
+ uint len = p-1 - query;
+ field_name = p;
+ query = string_copyn(query, len);
+ p = query + len;
+ }
+else
+ p = query + length;
+
+/* Now search backwards to find the comma that starts the
+table name. */
+
+while (p > query && p[-1] != ',') p--;
+if (p <= query)
+ {
+ *errmsg = US"NIS+ query malformed";
+ error_error = DEFER;
+ goto NISPLUS_EXIT;
+ }
+
+/* Look up the data for the table, in order to get the field names,
+check that we got back a table, and set up pointers so the field
+names can be scanned. */
+
+nrt = nis_lookup(CS p, EXPAND_NAME | NO_CACHE);
+if (nrt->status != NIS_SUCCESS)
+ {
+ *errmsg = string_sprintf("NIS+ error accessing %s table: %s", p,
+ nis_sperrno(nrt->status));
+ if (nrt->status != NIS_NOTFOUND && nrt->status != NIS_NOSUCHTABLE)
+ error_error = DEFER;
+ goto NISPLUS_EXIT;
+ }
+tno = nrt->objects.objects_val;
+if (tno->zo_data.zo_type != TABLE_OBJ)
+ {
+ *errmsg = string_sprintf("NIS+ error: %s is not a table", p);
+ goto NISPLUS_EXIT;
+ }
+ta = &tno->zo_data.objdata_u.ta_data;
+
+/* Now look up the entry in the table, check that we got precisely one
+object and that it is a table entry. */
+
+nre = nis_list(CS query, EXPAND_NAME, NULL, NULL);
+if (nre->status != NIS_SUCCESS)
+ {
+ *errmsg = string_sprintf("NIS+ error accessing entry %s: %s",
+ query, nis_sperrno(nre->status));
+ goto NISPLUS_EXIT;
+ }
+if (nre->objects.objects_len > 1)
+ {
+ *errmsg = string_sprintf("NIS+ returned more than one object for %s",
+ query);
+ goto NISPLUS_EXIT;
+ }
+else if (nre->objects.objects_len < 1)
+ {
+ *errmsg = string_sprintf("NIS+ returned no data for %s", query);
+ goto NISPLUS_EXIT;
+ }
+eno = nre->objects.objects_val;
+if (eno->zo_data.zo_type != ENTRY_OBJ)
+ {
+ *errmsg = string_sprintf("NIS+ error: %s is not an entry", query);
+ goto NISPLUS_EXIT;
+ }
+
+/* Scan the columns in the entry and in the table. If a result field
+was given, look for that field; otherwise concatenate all the fields
+with their names. */
+
+eo = &(eno->zo_data.objdata_u.en_data);
+for (int i = 0; i < eo->en_cols.en_cols_len; i++)
+ {
+ table_col *tc = ta->ta_cols.ta_cols_val + i;
+ entry_col *ec = eo->en_cols.en_cols_val + i;
+ int len = ec->ec_value.ec_value_len;
+ uschar *value = US ec->ec_value.ec_value_val;
+
+ /* The value may be NULL for a zero-length field. Turn this into an
+ empty string for consistency. Remove trailing whitespace and zero
+ bytes. */
+
+ if (!value) value = US"";
+ else
+ while (len > 0 && (value[len-1] == 0 || isspace(value[len-1])))
+ len--;
+
+ /* Concatenate all fields if no specific one selected */
+
+ if (!field_name)
+ {
+ yield = string_cat (yield, US tc->tc_name);
+ yield = string_catn(yield, US"=", 1);
+
+ /* Quote the value if it contains spaces or is empty */
+
+ if (value[0] == 0 || Ustrchr(value, ' ') != NULL)
+ {
+ yield = string_catn(yield, US"\"", 1);
+ for (int j = 0; j < len; j++)
+ {
+ if (value[j] == '\"' || value[j] == '\\')
+ yield = string_catn(yield, US"\\", 1);
+ yield = string_catn(yield, value+j, 1);
+ }
+ yield = string_catn(yield, US"\"", 1);
+ }
+ else
+ yield = string_catn(yield, value, len);
+
+ yield = string_catn(yield, US" ", 1);
+ }
+
+ /* When the specified field is found, grab its data and finish */
+
+ else if (Ustrcmp(field_name, tc->tc_name) == 0)
+ {
+ yield = string_catn(yield, value, len);
+ goto NISPLUS_EXIT;
+ }
+ }
+
+/* Error if a field name was specified and we didn't find it; if no
+field name, ensure the concatenated data is zero-terminated. */
+
+if (field_name)
+ *errmsg = string_sprintf("NIS+ field %s not found for %s", field_name,
+ query);
+else
+ gstring_release_unused(yield);
+
+/* Free result store before finishing. */
+
+NISPLUS_EXIT:
+if (nrt) nis_freeresult(nrt);
+if (nre) nis_freeresult(nre);
+
+if (yield)
+ {
+ *result = string_from_gstring(yield);
+ return OK;
+ }
+
+return error_error; /* FAIL or DEFER */
+}
+
+
+
+/*************************************************
+* Quote entry point *
+*************************************************/
+
+/* The only quoting that is necessary for NIS+ is to double any doublequote
+characters. No options are recognized.
+
+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 *
+nisplus_quote(uschar * s, uschar * opt, unsigned idx)
+{
+int count = 0;
+uschar * quoted, * t = s;
+
+if (opt) return NULL; /* No options recognized */
+
+while (*t) if (*t++ == '\"') count++;
+
+t = quoted = store_get_quoted(Ustrlen(s) + count + 1, s, idx);
+
+while (*s)
+ {
+ *t++ = *s;
+ if (*s++ == '\"') *t++ = '\"';
+ }
+
+*t = 0;
+return quoted;
+}
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+nisplus_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: NIS+: Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+
+static lookup_info _lookup_info = {
+ .name = US"nisplus", /* lookup name */
+ .type = lookup_querystyle, /* query-style lookup */
+ .open = nisplus_open, /* open function */
+ .check = NULL, /* check function */
+ .find = nisplus_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = nisplus_quote, /* quoting function */
+ .version_report = nisplus_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+#define nisplus_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &_lookup_info };
+lookup_module_info nisplus_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* End of lookups/nisplus.c */
diff --git a/src/lookups/oracle.c b/src/lookups/oracle.c
new file mode 100644
index 0000000..d32b5e4
--- /dev/null
+++ b/src/lookups/oracle.c
@@ -0,0 +1,628 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2015 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* Interface to an Oracle database. This code was originally supplied by
+Paul Kelly, but I have hacked it around for various reasons, and tried to add
+some comments from my position of Oracle ignorance. */
+
+
+#include "../exim.h"
+
+
+/* The Oracle system headers */
+
+#include <oratypes.h>
+#include <ocidfn.h>
+#include <ocikpr.h>
+
+#define PARSE_NO_DEFER 0 /* parse straight away */
+#define PARSE_V7_LNG 2
+#define MAX_ITEM_BUFFER_SIZE 1024 /* largest size of a cell of data */
+#define MAX_SELECT_LIST_SIZE 32 /* maximum number of columns (not rows!) */
+
+/* Paul's comment on this was "change this to 512 for 64bit cpu", but I don't
+understand why. The Oracle manual just asks for 256 bytes.
+
+That was years ago. Jin Choi suggested (March 2007) that this change should
+be made in the source, as at worst it wastes 256 bytes, and it saves people
+having to discover about this for themselves as more and more systems are
+64-bit. So I have changed 256 to 512. */
+
+#define HDA_SIZE 512
+
+/* Internal/external datatype codes */
+
+#define NUMBER_TYPE 2
+#define INT_TYPE 3
+#define FLOAT_TYPE 4
+#define STRING_TYPE 5
+#define ROWID_TYPE 11
+#define DATE_TYPE 12
+
+/* ORACLE error codes used in demonstration programs */
+
+#define VAR_NOT_IN_LIST 1007
+#define NO_DATA_FOUND 1403
+
+typedef struct Ora_Describe {
+ sb4 dbsize;
+ sb2 dbtype;
+ sb1 buf[MAX_ITEM_BUFFER_SIZE];
+ sb4 buflen;
+ sb4 dsize;
+ sb2 precision;
+ sb2 scale;
+ sb2 nullok;
+} Ora_Describe;
+
+typedef struct Ora_Define {
+ ub1 buf[MAX_ITEM_BUFFER_SIZE];
+ float flt_buf;
+ sword int_buf;
+ sb2 indp;
+ ub2 col_retlen, col_retcode;
+} Ora_Define;
+
+/* Structure and anchor for caching connections. */
+
+typedef struct oracle_connection {
+ struct oracle_connection *next;
+ uschar *server;
+ struct cda_def *handle;
+ void *hda_mem;
+} oracle_connection;
+
+static oracle_connection *oracle_connections = NULL;
+
+
+
+
+
+/*************************************************
+* Set up message after error *
+*************************************************/
+
+/* Sets up a message from a local string plus whatever Oracle gives.
+
+Arguments:
+ oracle_handle the handle of the connection
+ rc the return code
+ msg local text message
+*/
+
+static uschar *
+oracle_error(struct cda_def *oracle_handle, int rc, uschar *msg)
+{
+uschar tmp[1024];
+oerhms(oracle_handle, rc, tmp, sizeof(tmp));
+return string_sprintf("ORACLE %s: %s", msg, tmp);
+}
+
+
+
+/*************************************************
+* Describe and define the select list items *
+*************************************************/
+
+/* Figures out sizes, types, and numbers.
+
+Arguments:
+ cda the connection
+ def
+ desc descriptions put here
+
+Returns: number of fields
+*/
+
+static sword
+describe_define(Cda_Def *cda, Ora_Define *def, Ora_Describe *desc)
+{
+sword col, deflen, deftyp;
+static ub1 *defptr;
+static sword numwidth = 8;
+
+/* Describe the select-list items. */
+
+for (col = 0; col < MAX_SELECT_LIST_SIZE; col++)
+ {
+ desc[col].buflen = MAX_ITEM_BUFFER_SIZE;
+
+ if (odescr(cda, col + 1, &desc[col].dbsize,
+ &desc[col].dbtype, &desc[col].buf[0],
+ &desc[col].buflen, &desc[col].dsize,
+ &desc[col].precision, &desc[col].scale,
+ &desc[col].nullok) != 0)
+ {
+ /* Break on end of select list. */
+ if (cda->rc == VAR_NOT_IN_LIST) break; else return -1;
+ }
+
+ /* Adjust sizes and types for display, handling NUMBER with scale as float. */
+
+ if (desc[col].dbtype == NUMBER_TYPE)
+ {
+ desc[col].dbsize = numwidth;
+ if (desc[col].scale != 0)
+ {
+ defptr = (ub1 *)&def[col].flt_buf;
+ deflen = (sword) sizeof(float);
+ deftyp = FLOAT_TYPE;
+ desc[col].dbtype = FLOAT_TYPE;
+ }
+ else
+ {
+ defptr = (ub1 *)&def[col].int_buf;
+ deflen = (sword) sizeof(sword);
+ deftyp = INT_TYPE;
+ desc[col].dbtype = INT_TYPE;
+ }
+ }
+ else
+ {
+ if (desc[col].dbtype == DATE_TYPE)
+ desc[col].dbsize = 9;
+ if (desc[col].dbtype == ROWID_TYPE)
+ desc[col].dbsize = 18;
+ defptr = def[col].buf;
+ deflen = desc[col].dbsize > MAX_ITEM_BUFFER_SIZE ?
+ MAX_ITEM_BUFFER_SIZE : desc[col].dbsize + 1;
+ deftyp = STRING_TYPE;
+ desc[col].dbtype = STRING_TYPE;
+ }
+
+ /* Define an output variable */
+
+ if (odefin(cda, col + 1,
+ defptr, deflen, deftyp,
+ -1, &def[col].indp, (text *) 0, -1, -1,
+ &def[col].col_retlen,
+ &def[col].col_retcode) != 0)
+ return -1;
+ } /* Loop for each column */
+
+return col;
+}
+
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static void *
+oracle_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
+oracle_tidy(void)
+{
+oracle_connection *cn;
+while ((cn = oracle_connections) != NULL)
+ {
+ oracle_connections = cn->next;
+ DEBUG(D_lookup) debug_printf_indent("close ORACLE connection: %s\n", cn->server);
+ ologof(cn->handle);
+ }
+}
+
+
+
+/*************************************************
+* 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", for compatibility
+with MySQL and pgsql, but at present, the dbname is not used. This string is in
+a nextinlist temporary buffer, so can be overwritten.
+
+Returns: OK, FAIL, or DEFER
+*/
+
+static int
+perform_oracle_search(uschar *query, uschar *server, uschar **resultptr,
+ uschar **errmsg, BOOL *defer_break)
+{
+Cda_Def *cda = NULL;
+struct cda_def *oracle_handle = NULL;
+Ora_Describe *desc = NULL;
+Ora_Define *def = NULL;
+void *hda = NULL;
+
+int yield = DEFER;
+unsigned int num_fields = 0;
+gstring * result = NULL;
+oracle_connection *cn = NULL;
+uschar *server_copy = NULL;
+uschar *sdata[4];
+
+/* 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 = 3; i > 0; i--)
+ {
+ uschar *pp = Ustrrchr(server, '/');
+ if (pp == NULL)
+ {
+ *errmsg = string_sprintf("incomplete ORACLE server data: %s", server);
+ *defer_break = TRUE;
+ return DEFER;
+ }
+ *pp++ = 0;
+ sdata[i] = pp;
+ if (i == 3) server_copy = string_copy(server); /* sans password */
+ }
+sdata[0] = server; /* What's left at the start */
+
+/* If the database is the empty string, set it NULL - the query must then
+define it. */
+
+if (sdata[1][0] == 0) sdata[1] = NULL;
+
+/* See if we have a cached connection to the server */
+
+for (cn = oracle_connections; cn; cn = cn->next)
+ if (strcmp(cn->server, server_copy) == 0)
+ {
+ oracle_handle = cn->handle;
+ hda = cn->hda_mem;
+ break;
+ }
+
+/* If no cached connection, we must set one up */
+
+if (!cn)
+ {
+ DEBUG(D_lookup) debug_printf_indent("ORACLE new connection: host=%s database=%s "
+ "user=%s\n", sdata[0], sdata[1], sdata[2]);
+
+ /* Get store for a new connection, initialize it, and connect to the server */
+
+ oracle_handle = store_get(sizeof(struct cda_def), GET_UNTAINTED);
+ hda = store_get(HDA_SIZE, GET_UNTAINTED);
+ memset(hda,'\0',HDA_SIZE);
+
+ /*
+ * Perform a default (blocking) login
+ *
+ * sdata[0] = tnsname (service name - typically host name)
+ * sdata[1] = dbname - not used at present
+ * sdata[2] = username
+ * sdata[3] = passwd
+ */
+
+ if(olog(oracle_handle, hda, sdata[2], -1, sdata[3], -1, sdata[0], -1,
+ (ub4)OCI_LM_DEF) != 0)
+ {
+ *errmsg = oracle_error(oracle_handle, oracle_handle->rc,
+ US"connection failed");
+ *defer_break = FALSE;
+ goto ORACLE_EXIT_NO_VALS;
+ }
+
+ /* Add the connection to the cache */
+
+ cn = store_get(sizeof(oracle_connection), GET_UNTAINTED);
+ cn->server = server_copy;
+ cn->handle = oracle_handle;
+ cn->next = oracle_connections;
+ cn->hda_mem = hda;
+ oracle_connections = cn;
+ }
+
+/* Else use a previously cached connection - we can write to the server string
+to obliterate the password because it is in a nextinlist temporary buffer. */
+
+else
+ {
+ DEBUG(D_lookup)
+ debug_printf_indent("ORACLE using cached connection for %s\n", server_copy);
+ }
+
+/* We have a connection. Open a cursor and run the query */
+
+cda = store_get(sizeof(Cda_Def), GET_UNTAINTED);
+
+if (oopen(cda, oracle_handle, (text *)0, -1, -1, (text *)0, -1) != 0)
+ {
+ *errmsg = oracle_error(oracle_handle, cda->rc, "failed to open cursor");
+ *defer_break = FALSE;
+ goto ORACLE_EXIT_NO_VALS;
+ }
+
+if (oparse(cda, (text *)query, (sb4) -1,
+ (sword)PARSE_NO_DEFER, (ub4)PARSE_V7_LNG) != 0)
+ {
+ *errmsg = oracle_error(oracle_handle, cda->rc, "query failed");
+ *defer_break = FALSE;
+ oclose(cda);
+ goto ORACLE_EXIT_NO_VALS;
+ }
+
+/* Find the number of fields returned and sort out their types. If the number
+is one, we don't add field names to the data. Otherwise we do. */
+
+def = store_get(sizeof(Ora_Define)*MAX_SELECT_LIST_SIZE, GET_UNTAINTED);
+desc = store_get(sizeof(Ora_Describe)*MAX_SELECT_LIST_SIZE, GET_UNTAINTED);
+
+if ((num_fields = describe_define(cda,def,desc)) == -1)
+ {
+ *errmsg = oracle_error(oracle_handle, cda->rc, "describe_define failed");
+ *defer_break = FALSE;
+ goto ORACLE_EXIT;
+ }
+
+if (oexec(cda)!=0)
+ {
+ *errmsg = oracle_error(oracle_handle, cda->rc, "oexec failed");
+ *defer_break = FALSE;
+ goto ORACLE_EXIT;
+ }
+
+/* Get the fields and construct the result string. If there is more than one
+row, we insert '\n' between them. */
+
+while (cda->rc != NO_DATA_FOUND) /* Loop for each row */
+ {
+ ofetch(cda);
+ if(cda->rc == NO_DATA_FOUND) break;
+
+ if (result) result = string_catn(result, "\n", 1);
+
+ /* Single field - just add on the data */
+
+ if (num_fields == 1)
+ result = string_catn(result, def[0].buf, def[0].col_retlen);
+
+ /* Multiple fields - precede by file name, removing {lead,trail}ing WS */
+
+ else for (int i = 0; i < num_fields; i++)
+ {
+ int slen;
+ uschar *s = US desc[i].buf;
+
+ while (*s != 0 && isspace(*s)) s++;
+ slen = Ustrlen(s);
+ while (slen > 0 && isspace(s[slen-1])) slen--;
+ result = string_catn(result, s, slen);
+ result = string_catn(result, US"=", 1);
+
+ /* int and float type won't ever need escaping. Otherwise, quote the value
+ if it contains spaces or is empty. */
+
+ if (desc[i].dbtype != INT_TYPE && desc[i].dbtype != FLOAT_TYPE &&
+ (def[i].buf[0] == 0 || strchr(def[i].buf, ' ') != NULL))
+ {
+ result = string_catn(result, "\"", 1);
+ for (int j = 0; j < def[i].col_retlen; j++)
+ {
+ if (def[i].buf[j] == '\"' || def[i].buf[j] == '\\')
+ result = string_catn(result, "\\", 1);
+ result = string_catn(result, def[i].buf+j, 1);
+ }
+ result = string_catn(result, "\"", 1);
+ }
+
+ else switch(desc[i].dbtype)
+ {
+ case INT_TYPE:
+ result = string_cat(result, string_sprintf("%d", def[i].int_buf));
+ break;
+
+ case FLOAT_TYPE:
+ result = string_cat(result, string_sprintf("%f", def[i].flt_buf));
+ break;
+
+ case STRING_TYPE:
+ result = string_catn(result, def[i].buf, def[i].col_retlen);
+ break;
+
+ default:
+ *errmsg = string_sprintf("ORACLE: unknown field type %d", desc[i].dbtype);
+ *defer_break = FALSE;
+ result = NULL;
+ goto ORACLE_EXIT;
+ }
+
+ result = string_catn(result, " ", 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 = "ORACLE: no data found";
+ }
+else
+ gstring_release_unused(result);
+
+/* Get here by goto from various error checks. */
+
+ORACLE_EXIT:
+
+/* Close the cursor; don't close the connection, as it is cached. */
+
+oclose(cda);
+
+ORACLE_EXIT_NO_VALS:
+
+/* 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
+oracle_find(void * handle, const uschar * filename, uschar * query, int length,
+ uschar ** result, uschar ** errmsg, uint * do_cache, const uschar * opts)
+{
+int sep = 0;
+uschar *server;
+uschar *list = oracle_servers;
+
+do_cache = do_cache; /* Placate picky compilers */
+
+DEBUG(D_lookup) debug_printf_indent("ORACLE query: %s\n", query);
+
+while ((server = string_nextinlist(&list, &sep, NULL, 0)))
+ {
+ BOOL defer_break;
+ int rc = perform_oracle_search(query, server, result, errmsg, &defer_break);
+ if (rc != DEFER || defer_break) return rc;
+ }
+
+if (!oracle_servers)
+ *errmsg = "no ORACLE servers defined (oracle_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 are 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.
+
+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 *
+oracle_quote(uschar * s, uschar * opt, unsigned idx)
+{
+int c, count = 0;
+uschar * t = s, * quoted;
+
+if (opt) return NULL; /* No options are recognized */
+
+while ((c = *t++))
+ if (strchr("\n\t\r\b\'\"\\", c) != NULL) count++;
+
+t = quoted = store_get_quoted((int)Ustrlen(s) + count + 1, s, idx);
+
+while ((c = *s++))
+ {
+ if (strchr("\n\t\r\b\'\"\\", c) != NULL)
+ {
+ *t++ = '\\';
+ switch(c)
+ {
+ case '\n': *t++ = 'n';
+ break;
+ case '\t': *t++ = 't';
+ break;
+ case '\r': *t++ = 'r';
+ break;
+ case '\b': *t++ = 'b';
+ break;
+ default: *t++ = c;
+ break;
+ }
+ }
+ else *t++ = c;
+ }
+
+*t = 0;
+return quoted;
+}
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+oracle_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: Oracle: Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+
+static lookup_info _lookup_info = {
+ .name = US"oracle", /* lookup name */
+ .type = lookup_querystyle, /* query-style lookup */
+ .open = oracle_open, /* open function */
+ .check = NULL, /* check function */
+ .find = oracle_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = oracle_tidy, /* tidy function */
+ .quote = oracle_quote, /* quoting function */
+ .version_report = oracle_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+#define oracle_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &_lookup_info };
+lookup_module_info oracle_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* End of lookups/oracle.c */
diff --git a/src/lookups/passwd.c b/src/lookups/passwd.c
new file mode 100644
index 0000000..eaf78b2
--- /dev/null
+++ b/src/lookups/passwd.c
@@ -0,0 +1,85 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2015 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description */
+
+static void *
+passwd_open(const uschar * filename, uschar ** errmsg)
+{
+return (void *)(-1); /* Just return something non-null */
+}
+
+
+
+
+/*************************************************
+* Find entry point for passwd *
+*************************************************/
+
+/* See local README for interface description */
+
+static int
+passwd_find(void * handle, const uschar * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+struct passwd *pw;
+
+if (!route_finduser(keystring, &pw, NULL)) return FAIL;
+*result = string_sprintf("*:%d:%d:%s:%s:%s", (int)pw->pw_uid, (int)pw->pw_gid,
+ pw->pw_gecos, pw->pw_dir, pw->pw_shell);
+return OK;
+}
+
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+passwd_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: passwd: Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+static lookup_info _lookup_info = {
+ .name = US"passwd", /* lookup name */
+ .type = lookup_querystyle, /* query-style lookup */
+ .open = passwd_open, /* open function */
+ .check = NULL, /* no check function */
+ .find = passwd_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = passwd_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+#define passwd_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &_lookup_info };
+lookup_module_info passwd_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* End of lookups/passwd.c */
diff --git a/src/lookups/pgsql.c b/src/lookups/pgsql.c
new file mode 100644
index 0000000..4bb693a
--- /dev/null
+++ b/src/lookups/pgsql.c
@@ -0,0 +1,506 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* Thanks to Petr Cech for contributing the original code for these
+functions. Thanks to Joachim Wieland for the initial patch for the Unix domain
+socket extension. */
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+#include <libpq-fe.h> /* The system header */
+
+/* Structure and anchor for caching connections. */
+
+typedef struct pgsql_connection {
+ struct pgsql_connection *next;
+ uschar *server;
+ PGconn *handle;
+} pgsql_connection;
+
+static pgsql_connection *pgsql_connections = NULL;
+
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static void *
+pgsql_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
+pgsql_tidy(void)
+{
+pgsql_connection *cn;
+while ((cn = pgsql_connections) != NULL)
+ {
+ pgsql_connections = cn->next;
+ DEBUG(D_lookup) debug_printf_indent("close PGSQL connection: %s\n", cn->server);
+ PQfinish(cn->handle);
+ }
+}
+
+
+/*************************************************
+* Notice processor function for pgsql *
+*************************************************/
+
+/* This function is passed to pgsql below, and called for any PostgreSQL
+"notices". By default they are written to stderr, which is undesirable.
+
+Arguments:
+ arg an opaque user cookie (not used)
+ message the notice
+
+Returns: nothing
+*/
+
+static void
+notice_processor(void *arg, const char *message)
+{
+arg = arg; /* Keep compiler happy */
+DEBUG(D_lookup) debug_printf_indent("PGSQL: %s\n", message);
+}
+
+
+
+/*************************************************
+* Internal search function *
+*************************************************/
+
+/* This function is called from the find entry point to do the search for a
+single server. The server string is of the form "server/dbname/user/password".
+
+PostgreSQL supports connections through Unix domain sockets. This is usually
+faster and costs less cpu time than a TCP/IP connection. However it can only be
+used if the mail server runs on the same machine as the database server. A
+configuration line for PostgreSQL via Unix domain sockets looks like this:
+
+hide pgsql_servers = (/tmp/.s.PGSQL.5432)/db/user/password[:<nextserver>]
+
+We enclose the path name in parentheses so that its slashes aren't visually
+confused with the delimiters for the other pgsql_server settings.
+
+For TCP/IP connections, the server is a host name and optional port (with a
+colon separator).
+
+NOTE:
+ 1) All three '/' must be present.
+ 2) If host is omitted the local unix socket is used.
+
+Arguments:
+ query the query string
+ server the server string; this is in dynamic memory and can be updated
+ resultptr where to store the result
+ errmsg where to point an error message
+ defer_break set TRUE if no more servers are to be tried after DEFER
+ do_cache set FALSE if data is changed
+ opts options list
+
+Returns: OK, FAIL, or DEFER
+*/
+
+static int
+perform_pgsql_search(const uschar *query, uschar *server, uschar **resultptr,
+ uschar **errmsg, BOOL *defer_break, uint *do_cache, const uschar * opts)
+{
+PGconn *pg_conn = NULL;
+PGresult *pg_result = NULL;
+
+gstring * result = NULL;
+int yield = DEFER;
+unsigned int num_fields, num_tuples;
+pgsql_connection *cn;
+rmark reset_point = store_mark();
+uschar *server_copy = NULL;
+uschar *sdata[3];
+
+/* Disaggregate the parameters from the server argument. The order is host or
+path, 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 pgSQL server data: %s",
+ (i == 2)? server : server_copy);
+ *defer_break = TRUE;
+ return DEFER;
+ }
+ *pp++ = 0;
+ sdata[i] = pp;
+ if (i == 2) server_copy = string_copy(server); /* sans password */
+ }
+
+/* The total server string has now been truncated so that what is left at the
+start is the identification of the server (host or path). See if we have a
+cached connection to the server. */
+
+for (cn = pgsql_connections; cn; cn = cn->next)
+ if (Ustrcmp(cn->server, server_copy) == 0)
+ {
+ pg_conn = cn->handle;
+ break;
+ }
+
+/* If there is no cached connection, we must set one up. */
+
+if (!cn)
+ {
+ uschar *port = US"";
+
+ /* For a Unix domain socket connection, the path is in parentheses */
+
+ if (*server == '(')
+ {
+ uschar *last_slash, *last_dot, *p;
+
+ p = ++server;
+ while (*p && *p != ')') p++;
+ *p = 0;
+
+ last_slash = Ustrrchr(server, '/');
+ last_dot = Ustrrchr(server, '.');
+
+ DEBUG(D_lookup) debug_printf_indent("PGSQL new connection: socket=%s "
+ "database=%s user=%s\n", server, sdata[0], sdata[1]);
+
+ /* A valid socket name looks like this: /var/run/postgresql/.s.PGSQL.5432
+ We have to call PQsetdbLogin with '/var/run/postgresql' as the hostname
+ argument and put '5432' into the port variable. */
+
+ if (!last_slash || !last_dot)
+ {
+ *errmsg = string_sprintf("PGSQL invalid filename for socket: %s", server);
+ *defer_break = TRUE;
+ return DEFER;
+ }
+
+ /* Terminate the path name and set up the port: we'll have something like
+ server = "/var/run/postgresql" and port = "5432". */
+
+ *last_slash = 0;
+ port = last_dot + 1;
+ }
+
+ /* Host connection; sort out the port */
+
+ else
+ {
+ uschar *p;
+ if ((p = Ustrchr(server, ':')))
+ {
+ *p++ = 0;
+ port = p;
+ }
+
+ if (Ustrchr(server, '/'))
+ {
+ *errmsg = string_sprintf("unexpected slash in pgSQL server hostname: %s",
+ server);
+ *defer_break = TRUE;
+ return DEFER;
+ }
+
+ DEBUG(D_lookup) debug_printf_indent("PGSQL new connection: host=%s port=%s "
+ "database=%s user=%s\n", server, port, sdata[0], sdata[1]);
+ }
+
+ /* If the database is the empty string, set it NULL - the query must then
+ define it. */
+
+ if (sdata[0][0] == 0) sdata[0] = NULL;
+
+ /* Get store for a new handle, initialize it, and connect to the server */
+
+ pg_conn=PQsetdbLogin(
+ /* host port options tty database user passwd */
+ CS server, CS port, NULL, NULL, CS sdata[0], CS sdata[1], CS sdata[2]);
+
+ if(PQstatus(pg_conn) == CONNECTION_BAD)
+ {
+ reset_point = store_reset(reset_point);
+ *errmsg = string_sprintf("PGSQL connection failed: %s",
+ PQerrorMessage(pg_conn));
+ PQfinish(pg_conn);
+ goto PGSQL_EXIT;
+ }
+
+ /* Set the client encoding to SQL_ASCII, which means that the server will
+ not try to interpret the query as being in any fancy encoding such as UTF-8
+ or other multibyte code that might cause problems with escaping. */
+
+ PQsetClientEncoding(pg_conn, "SQL_ASCII");
+
+ /* Set the notice processor to prevent notices from being written to stderr
+ (which is what the default does). Our function (above) just produces debug
+ output. */
+
+ PQsetNoticeProcessor(pg_conn, notice_processor, NULL);
+
+ /* Add the connection to the cache */
+
+ cn = store_get(sizeof(pgsql_connection), GET_UNTAINTED);
+ cn->server = server_copy;
+ cn->handle = pg_conn;
+ cn->next = pgsql_connections;
+ pgsql_connections = cn;
+ }
+
+/* Else use a previously cached connection */
+
+else
+ {
+ DEBUG(D_lookup) debug_printf_indent("PGSQL using cached connection for %s\n",
+ server_copy);
+ }
+
+/* Run the query */
+
+pg_result = PQexec(pg_conn, CS query);
+switch(PQresultStatus(pg_result))
+ {
+ case PGRES_EMPTY_QUERY:
+ case PGRES_COMMAND_OK:
+ /* The command was successful but did not return any data since it was
+ not SELECT but either an INSERT, UPDATE or DELETE statement. Tell the
+ high level code to not cache this query, and clean the current cache for
+ this handle by setting *do_cache zero. */
+
+ result = string_cat(result, US PQcmdTuples(pg_result));
+ *do_cache = 0;
+ DEBUG(D_lookup) debug_printf_indent("PGSQL: command does not return any data "
+ "but was successful. Rows affected: %s\n", string_from_gstring(result));
+ break;
+
+ case PGRES_TUPLES_OK:
+ break;
+
+ default:
+ /* This was the original code:
+ *errmsg = string_sprintf("PGSQL: query failed: %s\n",
+ PQresultErrorMessage(pg_result));
+ This was suggested by a user:
+ */
+
+ *errmsg = string_sprintf("PGSQL: query failed: %s (%s) (%s)\n",
+ PQresultErrorMessage(pg_result),
+ PQresStatus(PQresultStatus(pg_result)), query);
+ goto PGSQL_EXIT;
+ }
+
+/* Result is in pg_result. Find the number of fields returned. If this is one,
+we don't add field names to the data. Otherwise we do. If the query did not
+return anything we skip the for loop; this also applies to the case
+PGRES_COMMAND_OK. */
+
+num_fields = PQnfields(pg_result);
+num_tuples = PQntuples(pg_result);
+
+/* Get the fields and construct the result string. If there is more than one
+row, we insert '\n' between them. */
+
+for (int i = 0; i < num_tuples; i++)
+ {
+ if (result)
+ result = string_catn(result, US"\n", 1);
+
+ if (num_fields == 1)
+ result = string_catn(result,
+ US PQgetvalue(pg_result, i, 0), PQgetlength(pg_result, i, 0));
+ else
+ for (int j = 0; j < num_fields; j++)
+ {
+ uschar *tmp = US PQgetvalue(pg_result, i, j);
+ result = lf_quote(US PQfname(pg_result, j), tmp, Ustrlen(tmp), result);
+ }
+ if (!result) result = string_get(1);
+ }
+
+/* If result is NULL then no data has been found and so we return FAIL. */
+
+if (!result)
+ {
+ yield = FAIL;
+ *errmsg = US"PGSQL: no data found";
+ }
+
+/* Get here by goto from various error checks. */
+
+PGSQL_EXIT:
+
+/* Free store for any result that was got; don't close the connection, as
+it is cached. */
+
+if (pg_result) PQclear(pg_result);
+
+/* Non-NULL result indicates a successful result */
+
+if (result)
+ {
+ gstring_release_unused(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. The code to loop through a list of servers while the
+query is deferred with a retryable error is now in a separate function that is
+shared with other SQL lookups. */
+
+static int
+pgsql_find(void * handle, const uschar * filename, const uschar * query,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+return lf_sqlperform(US"PostgreSQL", US"pgsql_servers", pgsql_servers, query,
+ result, errmsg, do_cache, opts, perform_pgsql_search);
+}
+
+
+
+/*************************************************
+* Quote entry point *
+*************************************************/
+
+/* The characters that always need to be quoted (with backslash) are newline,
+tab, carriage return, backspace, backslash itself, and the quote characters.
+
+The original code quoted single quotes as \' which is documented as valid in
+the O'Reilly book "Practical PostgreSQL" (first edition) as an alternative to
+the SQL standard '' way of representing a single quote as data. However, in
+June 2006 there was some security issue with using \' and so this has been
+changed.
+
+[Note: There is a function called PQescapeStringConn() that quotes strings.
+This cannot be used because it needs a PGconn argument (the connection handle).
+Why, I don't know. Seems odd for just string escaping...]
+
+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 *
+pgsql_quote(uschar * s, uschar * opt, unsigned idx)
+{
+int count = 0, c;
+uschar * t = s, * quoted;
+
+if (opt) return NULL; /* No options recognized */
+
+while ((c = *t++))
+ if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL) count++;
+
+t = quoted = store_get_quoted(Ustrlen(s) + count + 1, s, idx);
+
+while ((c = *s++))
+ {
+ if (c == '\'')
+ {
+ *t++ = '\'';
+ *t++ = '\'';
+ }
+ else if (Ustrchr("\n\t\r\b\"\\", c) != NULL)
+ {
+ *t++ = '\\';
+ switch(c)
+ {
+ case '\n': *t++ = 'n'; break;
+ case '\t': *t++ = 't'; break;
+ case '\r': *t++ = 'r'; break;
+ case '\b': *t++ = 'b'; break;
+ default: *t++ = c; break;
+ }
+ }
+ else *t++ = c;
+ }
+
+*t = 0;
+return quoted;
+}
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+pgsql_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: PostgreSQL: Exim version %s\n", EXIM_VERSION_STR);
+#endif
+
+/* Version reporting: there appears to be no available information about
+the client library in libpq-fe.h; once you have a connection object, you
+can access the server version and the chosen protocol version, but those
+aren't really what we want. It might make sense to debug_printf those
+when the connection is established though? */
+
+return g;
+}
+
+
+static lookup_info _lookup_info = {
+ .name = US"pgsql", /* lookup name */
+ .type = lookup_querystyle, /* query-style lookup */
+ .open = pgsql_open, /* open function */
+ .check = NULL, /* no check function */
+ .find = pgsql_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = pgsql_tidy, /* tidy function */
+ .quote = pgsql_quote, /* quoting function */
+ .version_report = pgsql_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+#define pgsql_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &_lookup_info };
+lookup_module_info pgsql_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* End of lookups/pgsql.c */
diff --git a/src/lookups/readsock.c b/src/lookups/readsock.c
new file mode 100644
index 0000000..22179c9
--- /dev/null
+++ b/src/lookups/readsock.c
@@ -0,0 +1,341 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2021 - 2022 */
+/* Copyright (c) Jeremy Harris 2020 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+
+static int
+internal_readsock_open(client_conn_ctx * cctx, const uschar * sspec,
+ int timeout, BOOL do_tls, uschar ** errmsg)
+{
+const uschar * server_name;
+host_item host;
+
+if (Ustrncmp(sspec, "inet:", 5) == 0)
+ {
+ int port;
+ uschar * port_name;
+
+ DEBUG(D_lookup)
+ debug_printf_indent(" new inet socket needed for readsocket\n");
+
+ server_name = sspec + 5;
+ port_name = Ustrrchr(server_name, ':');
+
+ /* Sort out the port */
+
+ if (!port_name)
+ {
+ /* expand_string_message results in an EXPAND_FAIL, from our
+ only caller. Lack of it gets a SOCK_FAIL; we feed back via errmsg
+ for that, which gets copied to search_error_message. */
+
+ expand_string_message =
+ string_sprintf("missing port for readsocket %s", sspec);
+ return FAIL;
+ }
+ *port_name++ = 0; /* Terminate server name */
+
+ if (isdigit(*port_name))
+ {
+ uschar *end;
+ port = Ustrtol(port_name, &end, 0);
+ if (end != port_name + Ustrlen(port_name))
+ {
+ expand_string_message =
+ string_sprintf("invalid port number %s", port_name);
+ return FAIL;
+ }
+ }
+ else
+ {
+ struct servent *service_info = getservbyname(CS port_name, "tcp");
+ if (!service_info)
+ {
+ expand_string_message = string_sprintf("unknown port \"%s\"",
+ port_name);
+ return FAIL;
+ }
+ port = ntohs(service_info->s_port);
+ }
+
+ /* Not having the request-string here in the open routine means
+ that we cannot do TFO; a pity */
+
+ cctx->sock = ip_connectedsocket(SOCK_STREAM, server_name, port, port,
+ timeout, &host, errmsg, NULL);
+ callout_address = NULL;
+ if (cctx->sock < 0)
+ return FAIL;
+ }
+
+else
+ {
+ struct sockaddr_un sockun; /* don't call this "sun" ! */
+ int rc;
+
+ DEBUG(D_lookup)
+ debug_printf_indent(" new unix socket needed for readsocket\n");
+
+ if ((cctx->sock = socket(PF_UNIX, SOCK_STREAM, 0)) == -1)
+ {
+ *errmsg = string_sprintf("failed to create socket: %s", strerror(errno));
+ return FAIL;
+ }
+
+ sockun.sun_family = AF_UNIX;
+ sprintf(sockun.sun_path, "%.*s", (int)(sizeof(sockun.sun_path)-1),
+ sspec);
+ server_name = US sockun.sun_path;
+
+ sigalrm_seen = FALSE;
+ ALARM(timeout);
+ rc = connect(cctx->sock, (struct sockaddr *) &sockun, sizeof(sockun));
+ ALARM_CLR(0);
+ if (sigalrm_seen)
+ {
+ *errmsg = US "socket connect timed out";
+ goto bad;
+ }
+ if (rc < 0)
+ {
+ *errmsg = string_sprintf("failed to connect to socket "
+ "%s: %s", sspec, strerror(errno));
+ goto bad;
+ }
+ host.name = server_name;
+ host.address = US"";
+ }
+
+#ifndef DISABLE_TLS
+if (do_tls)
+ {
+ union sockaddr_46 interface_sock;
+ EXIM_SOCKLEN_T size = sizeof(interface_sock);
+ smtp_connect_args conn_args = {.host = &host };
+ tls_support tls_dummy = { .sni = NULL };
+ uschar * errstr;
+
+ if (getsockname(cctx->sock, (struct sockaddr *) &interface_sock, &size) == 0)
+ conn_args.sending_ip_address = host_ntoa(-1, &interface_sock, NULL, NULL);
+ else
+ {
+ *errmsg = string_sprintf("getsockname failed: %s", strerror(errno));
+ goto bad;
+ }
+
+ if (!tls_client_start(cctx, &conn_args, NULL, &tls_dummy, &errstr))
+ {
+ *errmsg = string_sprintf("TLS connect failed: %s", errstr);
+ goto bad;
+ }
+ }
+#endif
+
+DEBUG(D_expand|D_lookup) debug_printf_indent(" connected to socket %s\n", sspec);
+return OK;
+
+bad:
+ close(cctx->sock);
+ return FAIL;
+}
+
+/* All use of allocations will be done against the POOL_SEARCH memory,
+which is freed once by search_tidyup(). */
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description */
+/* We just create a placeholder record with a closed socket, so
+that connection cacheing at the framework layer works. */
+
+static void *
+readsock_open(const uschar * filename, uschar ** errmsg)
+{
+client_conn_ctx * cctx = store_get(sizeof(*cctx), GET_UNTAINTED);
+cctx->sock = -1;
+cctx->tls_ctx = NULL;
+DEBUG(D_lookup) debug_printf_indent("readsock: allocated context\n");
+return cctx;
+}
+
+
+
+
+
+/*************************************************
+* Find entry point for lsearch *
+*************************************************/
+
+/* See local README for interface description */
+
+static int
+readsock_find(void * handle, const uschar * filename, const uschar * keystring,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+client_conn_ctx * cctx = handle;
+int sep = ',';
+struct {
+ BOOL do_shutdown:1;
+ BOOL do_tls:1;
+ BOOL cache:1;
+} lf = {.do_shutdown = TRUE};
+uschar * eol = NULL;
+int timeout = 5;
+gstring * yield;
+int ret = DEFER;
+
+DEBUG(D_lookup)
+ debug_printf_indent("readsock: file=\"%s\" key=\"%s\" len=%d opts=\"%s\"\n",
+ filename, keystring, length, opts);
+
+/* Parse options */
+
+if (opts) for (uschar * s; s = string_nextinlist(&opts, &sep, NULL, 0); )
+ if (Ustrncmp(s, "timeout=", 8) == 0)
+ timeout = readconf_readtime(s + 8, 0, FALSE);
+ else if (Ustrncmp(s, "shutdown=", 9) == 0)
+ lf.do_shutdown = Ustrcmp(s + 9, "no") != 0;
+#ifndef DISABLE_TLS
+ else if (Ustrncmp(s, "tls=", 4) == 0 && Ustrcmp(s + 4, US"no") != 0)
+ lf.do_tls = TRUE;
+#endif
+ else if (Ustrncmp(s, "eol=", 4) == 0)
+ eol = string_unprinting(s + 4);
+ else if (Ustrcmp(s, "cache=yes") == 0)
+ lf.cache = TRUE;
+ else if (Ustrcmp(s, "send=no") == 0)
+ length = 0;
+
+if (!filename) return FAIL; /* Server spec is required */
+
+/* Open the socket, if not cached */
+
+if (cctx->sock == -1)
+ if (internal_readsock_open(cctx, filename, timeout, lf.do_tls, errmsg) != OK)
+ return ret;
+
+testharness_pause_ms(100); /* Allow sequencing of test actions */
+
+/* Write the request string, if not empty or already done */
+
+if (length)
+ {
+ if ((
+#ifndef DISABLE_TLS
+ cctx->tls_ctx ? tls_write(cctx->tls_ctx, keystring, length, FALSE) :
+#endif
+ write(cctx->sock, keystring, length)) != length)
+ {
+ *errmsg = string_sprintf("request write to socket "
+ "failed: %s", strerror(errno));
+ goto out;
+ }
+ }
+
+/* Shut down the sending side of the socket. This helps some servers to
+recognise that it is their turn to do some work. Just in case some
+system doesn't have this function, make it conditional. */
+
+#ifdef SHUT_WR
+if (!cctx->tls_ctx && lf.do_shutdown)
+ shutdown(cctx->sock, SHUT_WR);
+#endif
+
+testharness_pause_ms(100);
+
+/* Now we need to read from the socket, under a timeout. The function
+that reads a file can be used. If we're using a stdio buffered read,
+and might need later write ops on the socket, the stdio must be in
+writable mode or the underlying socket goes non-writable. */
+
+sigalrm_seen = FALSE;
+#ifdef DISABLE_TLS
+if (TRUE)
+#else
+if (!cctx->tls_ctx)
+#endif
+ {
+ FILE * fp = fdopen(cctx->sock, "rb");
+ if (!fp)
+ {
+ log_write(0, LOG_MAIN|LOG_PANIC, "readsock fdopen: %s\n", strerror(errno));
+ goto out;
+ }
+ ALARM(timeout);
+ yield = cat_file(fp, NULL, eol);
+ }
+else
+ {
+ ALARM(timeout);
+ yield = cat_file_tls(cctx->tls_ctx, NULL, eol);
+ }
+
+ALARM_CLR(0);
+
+if (sigalrm_seen)
+ { *errmsg = US "socket read timed out"; goto out; }
+
+*result = yield ? string_from_gstring(yield) : US"";
+ret = OK;
+if (!lf.cache) *do_cache = 0;
+
+out:
+
+(void) close(cctx->sock);
+cctx->sock = -1;
+return ret;
+}
+
+
+
+/*************************************************
+* Close entry point *
+*************************************************/
+
+/* See local README for interface description */
+
+static void
+readsock_close(void * handle)
+{
+client_conn_ctx * cctx = handle;
+if (cctx->sock < 0) return;
+#ifndef DISABLE_TLS
+if (cctx->tls_ctx) tls_close(cctx->tls_ctx, TRUE);
+#endif
+close(cctx->sock);
+cctx->sock = -1;
+}
+
+
+
+static lookup_info readsock_lookup_info = {
+ .name = US"readsock", /* lookup name */
+ .type = lookup_querystyle,
+ .open = readsock_open, /* open function */
+ .check = NULL,
+ .find = readsock_find, /* find function */
+ .close = readsock_close,
+ .tidy = NULL,
+ .quote = NULL, /* no quoting function */
+ .version_report = NULL
+};
+
+
+#ifdef DYNLOOKUP
+#define readsock_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &readsock_lookup_info };
+lookup_module_info readsock_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* End of lookups/readsock.c */
diff --git a/src/lookups/redis.c b/src/lookups/redis.c
new file mode 100644
index 0000000..9c8559c
--- /dev/null
+++ b/src/lookups/redis.c
@@ -0,0 +1,471 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+
+#ifdef LOOKUP_REDIS
+
+#include "lf_functions.h"
+
+#include <hiredis/hiredis.h>
+
+#ifndef nele
+# define nele(arr) (sizeof(arr) / sizeof(*arr))
+#endif
+
+/* Structure and anchor for caching connections. */
+typedef struct redis_connection {
+ struct redis_connection *next;
+ uschar *server;
+ redisContext *handle;
+} redis_connection;
+
+static redis_connection *redis_connections = NULL;
+
+
+static void *
+redis_open(const uschar * filename, uschar ** errmsg)
+{
+return (void *)(1);
+}
+
+
+void
+redis_tidy(void)
+{
+redis_connection *cn;
+
+/* XXX: Not sure how often this is called!
+ Guess its called after every lookup which probably would mean to just
+ not use the _tidy() function at all and leave with exim exiting to
+ GC connections! */
+
+while ((cn = redis_connections))
+ {
+ redis_connections = cn->next;
+ DEBUG(D_lookup) debug_printf_indent("close REDIS connection: %s\n", cn->server);
+ redisFree(cn->handle);
+ }
+}
+
+
+/* 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
+ do_cache set false if data is changed
+ opts options
+
+ The server string is of the form "host/dbnumber/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_redis_search(const uschar *command, uschar *server, uschar **resultptr,
+ uschar **errmsg, BOOL *defer_break, uint *do_cache, const uschar * opts)
+{
+redisContext *redis_handle = NULL; /* Keep compilers happy */
+redisReply *redis_reply = NULL;
+redisReply *entry = NULL;
+redisReply *tentry = NULL;
+redis_connection *cn;
+int yield = DEFER;
+int i, j;
+gstring * result = NULL;
+uschar *server_copy = NULL;
+uschar *sdata[3];
+
+/* Disaggregate the parameters from the server argument.
+The order is host:port(socket)
+We can write to the string, since it is in a nextinlist temporary buffer.
+This copy is also used for debugging output. */
+
+memset(sdata, 0, sizeof(sdata)) /* Set all to NULL */;
+for (int i = 2; i > 0; i--)
+ {
+ uschar *pp = Ustrrchr(server, '/');
+
+ if (!pp)
+ {
+ *errmsg = string_sprintf("incomplete Redis server data: %s",
+ i == 2 ? 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 */
+
+/* If the database or password is an empty string, set it NULL */
+if (sdata[1][0] == 0) sdata[1] = NULL;
+if (sdata[2][0] == 0) sdata[2] = NULL;
+
+/* See if we have a cached connection to the server */
+
+for (cn = redis_connections; cn; cn = cn->next)
+ if (Ustrcmp(cn->server, server_copy) == 0)
+ {
+ redis_handle = cn->handle;
+ break;
+ }
+
+if (!cn)
+ {
+ uschar *p;
+ uschar *socket = NULL;
+ int port = 0;
+ /* int redis_err = REDIS_OK; */
+
+ if ((p = Ustrchr(sdata[0], '(')))
+ {
+ *p++ = 0;
+ socket = p;
+ while (*p != 0 && *p != ')') p++;
+ *p = 0;
+ }
+
+ if ((p = Ustrchr(sdata[0], ':')))
+ {
+ *p++ = 0;
+ port = Uatoi(p);
+ }
+ else
+ port = Uatoi("6379");
+
+ if (Ustrchr(server, '/'))
+ {
+ *errmsg = string_sprintf("unexpected slash in Redis server hostname: %s",
+ sdata[0]);
+ *defer_break = TRUE;
+ return DEFER;
+ }
+
+ DEBUG(D_lookup)
+ debug_printf_indent("REDIS new connection: host=%s port=%d socket=%s database=%s\n",
+ sdata[0], port, socket, sdata[1]);
+
+ /* Get store for a new handle, initialize it, and connect to the server */
+ /* XXX: Use timeouts ? */
+ redis_handle =
+ socket ? redisConnectUnix(CCS socket) : redisConnect(CCS server, port);
+ if (!redis_handle)
+ {
+ *errmsg = US"REDIS connection failed";
+ *defer_break = FALSE;
+ goto REDIS_EXIT;
+ }
+
+ /* Add the connection to the cache */
+ cn = store_get(sizeof(redis_connection), GET_UNTAINTED);
+ cn->server = server_copy;
+ cn->handle = redis_handle;
+ cn->next = redis_connections;
+ redis_connections = cn;
+ }
+else
+ {
+ DEBUG(D_lookup)
+ debug_printf_indent("REDIS using cached connection for %s\n", server_copy);
+}
+
+/* Authenticate if there is a password */
+if(sdata[2])
+ if (!(redis_reply = redisCommand(redis_handle, "AUTH %s", sdata[2])))
+ {
+ *errmsg = string_sprintf("REDIS Authentication failed: %s\n", redis_handle->errstr);
+ *defer_break = FALSE;
+ goto REDIS_EXIT;
+ }
+
+/* Select the database if there is a dbnumber passed */
+if(sdata[1])
+ {
+ if (!(redis_reply = redisCommand(redis_handle, "SELECT %s", sdata[1])))
+ {
+ *errmsg = string_sprintf("REDIS: Selecting database=%s failed: %s\n", sdata[1], redis_handle->errstr);
+ *defer_break = FALSE;
+ goto REDIS_EXIT;
+ }
+ DEBUG(D_lookup) debug_printf_indent("REDIS: Selecting database=%s\n", sdata[1]);
+ }
+
+/* split string on whitespace into argv */
+ {
+ uschar * argv[32];
+ const uschar * s = command;
+ int siz, ptr, i;
+ uschar c;
+
+ while (isspace(*s)) s++;
+
+ for (i = 0; *s && i < nele(argv); i++)
+ {
+ gstring * g;
+
+ for (g = NULL; (c = *s) && !isspace(c); s++)
+ if (c != '\\' || *++s) /* backslash protects next char */
+ g = string_catn(g, s, 1);
+ argv[i] = string_from_gstring(g);
+
+ DEBUG(D_lookup) debug_printf_indent("REDIS: argv[%d] '%s'\n", i, argv[i]);
+ while (isspace(*s)) s++;
+ }
+
+ /* Run the command. We use the argv form rather than plain as that parses
+ into args by whitespace yet has no escaping mechanism. */
+
+ if (!(redis_reply = redisCommandArgv(redis_handle, i, CCSS argv, NULL)))
+ {
+ *errmsg = string_sprintf("REDIS: query failed: %s\n", redis_handle->errstr);
+ *defer_break = FALSE;
+ goto REDIS_EXIT;
+ }
+ }
+
+switch (redis_reply->type)
+ {
+ case REDIS_REPLY_ERROR:
+ *errmsg = string_sprintf("REDIS: lookup result failed: %s\n", redis_reply->str);
+
+ /* trap MOVED cluster responses and follow them */
+ if (Ustrncmp(redis_reply->str, "MOVED", 5) == 0)
+ {
+ DEBUG(D_lookup)
+ debug_printf_indent("REDIS: cluster redirect %s\n", redis_reply->str);
+ /* follow redirect
+ This is cheating, we simply set defer_break = FALSE to move on to
+ the next server in the redis_servers list */
+ *defer_break = FALSE;
+ return DEFER;
+ } else {
+ *defer_break = TRUE;
+ }
+ *do_cache = 0;
+ goto REDIS_EXIT;
+ /* NOTREACHED */
+
+ case REDIS_REPLY_NIL:
+ DEBUG(D_lookup)
+ debug_printf_indent("REDIS: query was not one that returned any data\n");
+ result = string_catn(result, US"", 1);
+ *do_cache = 0;
+ goto REDIS_EXIT;
+ /* NOTREACHED */
+
+ case REDIS_REPLY_INTEGER:
+ result = string_cat(result, redis_reply->integer != 0 ? US"true" : US"false");
+ break;
+
+ case REDIS_REPLY_STRING:
+ case REDIS_REPLY_STATUS:
+ result = string_catn(result, US redis_reply->str, redis_reply->len);
+ break;
+
+ case REDIS_REPLY_ARRAY:
+
+ /* NOTE: For now support 1 nested array result. If needed a limitless
+ result can be parsed */
+
+ for (int i = 0; i < redis_reply->elements; i++)
+ {
+ entry = redis_reply->element[i];
+
+ if (result)
+ result = string_catn(result, US"\n", 1);
+
+ switch (entry->type)
+ {
+ case REDIS_REPLY_INTEGER:
+ result = string_fmt_append(result, "%d", entry->integer);
+ break;
+ case REDIS_REPLY_STRING:
+ result = string_catn(result, US entry->str, entry->len);
+ break;
+ case REDIS_REPLY_ARRAY:
+ for (int j = 0; j < entry->elements; j++)
+ {
+ tentry = entry->element[j];
+
+ if (result)
+ result = string_catn(result, US"\n", 1);
+
+ switch (tentry->type)
+ {
+ case REDIS_REPLY_INTEGER:
+ result = string_fmt_append(result, "%d", tentry->integer);
+ break;
+ case REDIS_REPLY_STRING:
+ result = string_catn(result, US tentry->str, tentry->len);
+ break;
+ case REDIS_REPLY_ARRAY:
+ DEBUG(D_lookup)
+ debug_printf_indent("REDIS: result has nesting of arrays which"
+ " is not supported. Ignoring!\n");
+ break;
+ default:
+ DEBUG(D_lookup) debug_printf_indent(
+ "REDIS: result has unsupported type. Ignoring!\n");
+ break;
+ }
+ }
+ break;
+ default:
+ DEBUG(D_lookup) debug_printf_indent("REDIS: query returned unsupported type\n");
+ break;
+ }
+ }
+ break;
+ }
+
+
+if (result)
+ gstring_release_unused(result);
+else
+ {
+ yield = FAIL;
+ *errmsg = US"REDIS: no data found";
+ }
+
+REDIS_EXIT:
+
+/* Free store for any result that was got; don't close the connection,
+as it is cached. */
+
+if (redis_reply) freeReplyObject(redis_reply);
+
+/* 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);
+ /* NOTE: Required to close connection since it needs to be reopened */
+ return yield; /* FAIL or DEFER */
+ }
+}
+
+
+
+/*************************************************
+* Find entry point *
+*************************************************/
+/*
+ * See local README for interface description. The handle and filename
+ * arguments are not used. The code to loop through a list of servers while the
+ * query is deferred with a retryable error is now in a separate function that is
+ * shared with other noSQL lookups.
+ */
+
+static int
+redis_find(void * handle __attribute__((unused)),
+ const uschar * filename __attribute__((unused)),
+ const uschar * command, int length, uschar ** result, uschar ** errmsg,
+ uint * do_cache, const uschar * opts)
+{
+return lf_sqlperform(US"Redis", US"redis_servers", redis_servers, command,
+ result, errmsg, do_cache, opts, perform_redis_search);
+}
+
+
+
+/*************************************************
+* Quote entry point *
+*************************************************/
+
+/* Prefix any whitespace, or backslash, with a backslash.
+This is not a Redis thing but instead to let the argv splitting
+we do to split on whitespace, yet provide means for getting
+whitespace into an argument.
+
+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 *
+redis_quote(uschar * s, uschar * opt, unsigned idx)
+{
+int c, count = 0;
+uschar * t = s, * quoted;
+
+if (opt) return NULL; /* No options recognized */
+
+while ((c = *t++))
+ if (isspace(c) || c == '\\') count++;
+
+t = quoted = store_get_quoted(Ustrlen(s) + count + 1, s, idx);
+
+while ((c = *s++))
+ {
+ if (isspace(c) || c == '\\') *t++ = '\\';
+ *t++ = c;
+ }
+
+*t = 0;
+return quoted;
+}
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+#include "../version.h"
+
+gstring *
+redis_version_report(gstring * g)
+{
+g = string_fmt_append(g,
+ "Library version: REDIS: Compile: %d [%d]\n", HIREDIS_MAJOR, HIREDIS_MINOR);
+#ifdef DYNLOOKUP
+g = string_fmt_append(g,
+ " Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+
+
+/* These are the lookup_info blocks for this driver */
+static lookup_info redis_lookup_info = {
+ .name = US"redis", /* lookup name */
+ .type = lookup_querystyle, /* query-style lookup */
+ .open = redis_open, /* open function */
+ .check = NULL, /* no check function */
+ .find = redis_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = redis_tidy, /* tidy function */
+ .quote = redis_quote, /* quoting function */
+ .version_report = redis_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+# define redis_lookup_module_info _lookup_module_info
+#endif /* DYNLOOKUP */
+
+static lookup_info *_lookup_list[] = { &redis_lookup_info };
+lookup_module_info redis_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+#endif /* LOOKUP_REDIS */
+/* End of lookups/redis.c */
diff --git a/src/lookups/spf.c b/src/lookups/spf.c
new file mode 100644
index 0000000..78d954c
--- /dev/null
+++ b/src/lookups/spf.c
@@ -0,0 +1,159 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Exim - SPF lookup module using libspf2
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Copyright (c) The Exim Maintainers 2020 - 2022
+Copyright (c) 2005 Chris Webb, Arachsys Internet Services Ltd
+
+This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+*/
+
+#include "../exim.h"
+
+#ifndef SUPPORT_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"
+#if !defined(HAVE_NS_TYPE) && defined(NS_INADDRSZ)
+# define HAVE_NS_TYPE
+#endif
+#include <spf2/spf.h>
+#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)
+ {
+ *errmsg = US"SPF_dns_exim_nnew() failed";
+ return NULL;
+ }
+return (void *) spf_server;
+}
+
+
+static void
+spf_close(void *handle)
+{
+SPF_server_t *spf_server = handle;
+if (spf_server) SPF_server_free(spf_server);
+}
+
+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
+ {
+ 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;
+}
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+spf_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: SPF: Exim version %s\n", EXIM_VERSION_STR));
+#endif
+return g;
+}
+
+
+static lookup_info _lookup_info = {
+ .name = US"spf", /* lookup name */
+ .type = 0, /* not absfile, not query style */
+ .open = spf_open, /* open function */
+ .check = NULL, /* no check function */
+ .find = spf_find, /* find function */
+ .close = spf_close, /* close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = spf_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+#define spf_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &_lookup_info };
+lookup_module_info spf_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+#endif /* SUPPORT_SPF */
diff --git a/src/lookups/sqlite.c b/src/lookups/sqlite.c
new file mode 100644
index 0000000..9080ae7
--- /dev/null
+++ b/src/lookups/sqlite.c
@@ -0,0 +1,200 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2018 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+#include <sqlite3.h>
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static void *
+sqlite_open(const uschar * filename, uschar ** errmsg)
+{
+sqlite3 *db = NULL;
+int ret;
+
+if (!filename || !*filename)
+ {
+ DEBUG(D_lookup) debug_printf_indent("Using sqlite_dbfile: %s\n", sqlite_dbfile);
+ filename = sqlite_dbfile;
+ }
+if (!filename || *filename != '/')
+ *errmsg = US"absolute file name expected for \"sqlite\" lookup";
+else if ((ret = sqlite3_open(CCS filename, &db)) != 0)
+ {
+ *errmsg = (void *)sqlite3_errmsg(db);
+ sqlite3_close(db);
+ db = NULL;
+ DEBUG(D_lookup) debug_printf_indent("Error opening database: %s\n", *errmsg);
+ }
+
+if (db)
+ sqlite3_busy_timeout(db, 1000 * sqlite_lock_timeout);
+return db;
+}
+
+
+/*************************************************
+* Find entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static int
+sqlite_callback(void *arg, int argc, char **argv, char **azColName)
+{
+gstring * res = *(gstring **)arg;
+
+/* For second and subsequent results, insert \n */
+
+if (res)
+ res = string_catn(res, US"\n", 1);
+
+if (argc > 1)
+ {
+ /* For multiple fields, include the field name too */
+ for (int i = 0; i < argc; i++)
+ {
+ uschar * value = US(argv[i] ? argv[i] : "<NULL>");
+ res = lf_quote(US azColName[i], value, Ustrlen(value), res);
+ }
+ }
+
+else
+ res = string_cat(res, argv[0] ? US argv[0] : US "<NULL>");
+
+/* always return a non-null gstring, even for a zero-length string result */
+*(gstring **)arg = res ? res : string_get(1);
+return 0;
+}
+
+
+static int
+sqlite_find(void * handle, const uschar * filename, const uschar * query,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+int ret;
+gstring * res = NULL;
+
+ret = sqlite3_exec(handle, CS query, sqlite_callback, &res, CSS errmsg);
+if (ret != SQLITE_OK)
+ {
+ debug_printf_indent("sqlite3_exec failed: %s\n", *errmsg);
+ return FAIL;
+ }
+
+if (!res) *do_cache = 0; /* on fail, wipe cache */
+
+*result = string_from_gstring(res);
+return OK;
+}
+
+
+
+/*************************************************
+* Close entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static void sqlite_close(void *handle)
+{
+sqlite3_close(handle);
+}
+
+
+
+/*************************************************
+* Quote entry point *
+*************************************************/
+
+/* From what I have found so far, the only character that needs to be quoted
+for sqlite is the single quote, and it is quoted by doubling.
+
+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 *
+sqlite_quote(uschar * s, uschar * opt, unsigned idx)
+{
+int c, count = 0;
+uschar * t = s, * quoted;
+
+if (opt) return NULL; /* No options recognized */
+
+while ((c = *t++)) if (c == '\'') count++;
+count += t - s;
+
+t = quoted = store_get_quoted(count + 1, s, idx);
+
+while ((c = *s++))
+ {
+ if (c == '\'') *t++ = '\'';
+ *t++ = c;
+ }
+
+*t = 0;
+return quoted;
+}
+
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+sqlite_version_report(gstring * g)
+{
+g = string_fmt_append(g,
+ "Library version: SQLite: Compile: %s\n"
+ " Runtime: %s\n",
+ SQLITE_VERSION, sqlite3_libversion());
+#ifdef DYNLOOKUP
+g = string_fmt_append(g,
+ " Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+static lookup_info _lookup_info = {
+ .name = US"sqlite", /* lookup name */
+ .type = lookup_absfilequery, /* query-style lookup, starts with file name */
+ .open = sqlite_open, /* open function */
+ .check = NULL, /* no check function */
+ .find = sqlite_find, /* find function */
+ .close = sqlite_close, /* close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = sqlite_quote, /* quoting function */
+ .version_report = sqlite_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+#define sqlite_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &_lookup_info };
+lookup_module_info sqlite_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* End of lookups/sqlite.c */
diff --git a/src/lookups/testdb.c b/src/lookups/testdb.c
new file mode 100644
index 0000000..4824161
--- /dev/null
+++ b/src/lookups/testdb.c
@@ -0,0 +1,100 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2015 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+#include "lf_functions.h"
+
+
+/* These are not real lookup functions; they are just a way of testing the
+rest of Exim by providing an easy way of specifying particular yields from
+the find function. */
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static void *
+testdb_open(const uschar * filename, uschar ** errmsg)
+{
+return (void *)(1); /* Just return something non-null */
+}
+
+
+
+/*************************************************
+* Find entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static int
+testdb_find(void * handle, const uschar * filename, const uschar * query,
+ int length, uschar ** result, uschar ** errmsg, uint * do_cache,
+ const uschar * opts)
+{
+if (Ustrcmp(query, "fail") == 0)
+ {
+ *errmsg = US"testdb lookup forced FAIL";
+ DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
+ return FAIL;
+ }
+if (Ustrcmp(query, "defer") == 0)
+ {
+ *errmsg = US"testdb lookup forced DEFER";
+ DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
+ return DEFER;
+ }
+
+if (Ustrcmp(query, "nocache") == 0) *do_cache = 0;
+
+*result = string_copy(query);
+return OK;
+}
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+testdb_version_report(gstring * g)
+{
+#ifdef DYNLOOKUP
+g = string_fmt_append(g, "Library version: TestDB: Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+
+static lookup_info _lookup_info = {
+ .name = US"testdb", /* lookup name */
+ .type = lookup_querystyle, /* query-style lookup */
+ .open = testdb_open, /* open function */
+ .check = NULL, /* check function */
+ .find = testdb_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = testdb_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+#define testdb_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &_lookup_info };
+lookup_module_info testdb_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* End of lookups/testdb.c */
diff --git a/src/lookups/whoson.c b/src/lookups/whoson.c
new file mode 100644
index 0000000..990703f
--- /dev/null
+++ b/src/lookups/whoson.c
@@ -0,0 +1,98 @@
+/*************************************************
+* Exim - an Internet mail transport agent *
+*************************************************/
+
+/* Copyright (c) The Exim Maintainers 2020 - 2022 */
+/* Copyright (c) University of Cambridge 1995 - 2015 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+/* This code originally came from Robert Wal. */
+
+#include "../exim.h"
+
+
+#include <whoson.h> /* Public header */
+
+
+/*************************************************
+* Open entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static void *
+whoson_open(const uschar * filename, uschar ** errmsg)
+{
+return (void *)(1); /* Just return something non-null */
+}
+
+
+/*************************************************
+* Find entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+static int
+whoson_find(void * handle, const uschar * filename, uschar * query, int length,
+ uschar ** result, uschar ** errmsg, uint * do_cache, const uschar * opts)
+{
+uschar buffer[80];
+
+switch (wso_query(CS query, CS buffer, sizeof(buffer)))
+ {
+ case 0:
+ *result = string_copy(buffer); /* IP in database; return name of user */
+ return OK;
+
+ case +1:
+ return FAIL; /* IP not in database */
+
+ default:
+ *errmsg = string_sprintf("WHOSON: failed to complete: %s", buffer);
+ return DEFER;
+ }
+}
+
+
+
+/*************************************************
+* Version reporting entry point *
+*************************************************/
+
+/* See local README for interface description. */
+
+#include "../version.h"
+
+gstring *
+whoson_version_report(gstring * g)
+{
+g = string_fmt_append(g,
+ "Library version: Whoson: Runtime: %s\n", wso_version());
+#ifdef DYNLOOKUP
+g = string_fmt_append(g,
+ " Exim version %s\n", EXIM_VERSION_STR);
+#endif
+return g;
+}
+
+static lookup_info _lookup_info = {
+ .name = US"whoson", /* lookup name */
+ .type = lookup_querystyle, /* query-style lookup */
+ .open = whoson_open, /* open function */
+ .check = NULL, /* check function */
+ .find = whoson_find, /* find function */
+ .close = NULL, /* no close function */
+ .tidy = NULL, /* no tidy function */
+ .quote = NULL, /* no quoting function */
+ .version_report = whoson_version_report /* version reporting */
+};
+
+#ifdef DYNLOOKUP
+#define whoson_lookup_module_info _lookup_module_info
+#endif
+
+static lookup_info *_lookup_list[] = { &_lookup_info };
+lookup_module_info whoson_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
+
+/* End of lookups/whoson.c */