[exim-cvs] Bugzilla 1217: Experimental Redis lookup

Góra strony
Delete this message
Reply to this message
Autor: Exim Git Commits Mailing List
Data:  
Dla: exim-cvs
Temat: [exim-cvs] Bugzilla 1217: Experimental Redis lookup
Gitweb: http://git.exim.org/exim.git/commitdiff/9bdd29ad5c5d394229753b114f6f288893f616f4
Commit:     9bdd29ad5c5d394229753b114f6f288893f616f4
Parent:     9cb1785a68901ce990c7581bd465d0931edf166e
Author:     Todd Lyons <tlyons@???>
AuthorDate: Tue Oct 1 09:24:19 2013 -0700
Committer:  Todd Lyons <tlyons@???>
CommitDate: Tue Oct 1 09:32:24 2013 -0700


    Bugzilla 1217: Experimental Redis lookup


    Add want_experimental() test in the script to create the lookups
      Makefile to ease detection of requested Experimental features, and
      simplify the #ifdef guards in the redis.c.
---
 doc/doc-txt/ChangeLog             |    8 +-
 doc/doc-txt/experimental-spec.txt |   85 +++++++++
 src/scripts/MakeLinks             |    1 +
 src/scripts/lookups-Makefile      |   17 ++
 src/src/EDITME                    |    7 +
 src/src/config.h.defaults         |    1 +
 src/src/deliver.c                 |    3 +
 src/src/drtables.c                |    7 +
 src/src/exim.c                    |    3 +
 src/src/expand.c                  |    3 +
 src/src/globals.c                 |    4 +
 src/src/globals.h                 |    4 +
 src/src/lookups/Makefile          |    2 +
 src/src/lookups/redis.c           |  349 +++++++++++++++++++++++++++++++++++++
 src/src/readconf.c                |    3 +
 src/src/route.c                   |    3 +
 16 files changed, 499 insertions(+), 1 deletions(-)


diff --git a/doc/doc-txt/ChangeLog b/doc/doc-txt/ChangeLog
index 0f603e4..f5fd6d6 100644
--- a/doc/doc-txt/ChangeLog
+++ b/doc/doc-txt/ChangeLog
@@ -237,7 +237,13 @@ TL/12 Enhanced documentation in the ratelimit.pl script provided in

 TL/13 Bug 1301 - Imported transport SQL logging patch from Axel Rau
       renamed to Transport Post Delivery Action by Jeremy Harris, as
-            EXPERIMENTAL_TPDA.
+      EXPERIMENTAL_TPDA.
+
+TL/14 Bugzilla 1217 - Redis lookup support has been added. It is only enabled
+      when Exim is compiled with EXPERIMENTAL_REDIS. A new config variable
+      redis_servers = needs to be configured which will be used by the redis
+      lookup.  Patch from Warren Baker, of The Packet Hub.
+



 Exim version 4.80.1
diff --git a/doc/doc-txt/experimental-spec.txt b/doc/doc-txt/experimental-spec.txt
index 271ab0b..a5024d8 100644
--- a/doc/doc-txt/experimental-spec.txt
+++ b/doc/doc-txt/experimental-spec.txt
@@ -931,6 +931,91 @@ ${lookup mysql {insert into delivlog set \
     deliverrstr = '${quote_mysql:$tpda_defer_errstr}' \
     }}


+
+Redis Lookup
+--------------------------------------------------------------
+
+Redis is open source advanced key-value data store. This document
+does not explain the fundamentals, you should read and understand how
+it works by visiting the website at http://www.redis.io/.
+
+Redis lookup support is added via the hiredis library.  Visit:
+
+  https://github.com/redis/hiredis
+
+to obtain a copy, or find it in your operating systems package repository.
+If building from source, this description assumes that headers will be in
+/usr/local/include, and that the libraries are in /usr/local/lib.
+
+1. In order to build exim with Redis lookup support add
+
+EXPERIMENTAL_REDIS=yes
+
+to your Local/Makefile. (Re-)build/install exim. exim -d should show
+Experimental_Redis in the line "Support for:".
+
+EXPERIMENTAL_REDIS=yes
+LDFLAGS += -lhiredis
+# CFLAGS += -I/usr/local/include
+# LDFLAGS += -L/usr/local/lib
+
+The first line sets the feature to include the correct code, and
+the second line says to link the hiredis libraries into the
+exim binary.  The commented out lines should be uncommented if you
+built hiredis from source and installed in the default location.
+Adjust the paths if you installed them elsewhere, but you do not
+need to uncomment them if an rpm (or you) installed them in the
+package controlled locations (/usr/include and /usr/lib).
+
+
+2. Use the following global settings to configure Redis lookup support:
+
+Required:
+redis_servers       This option provides a list of Redis servers
+                    and associated connection data, to be used in
+                    conjunction with redis lookups. The option is
+                    only available if Exim is configured with Redis
+                    support.
+
+For example:
+
+redis_servers = 127.0.0.1/10/ - using database 10 with no password
+redis_servers = 127.0.0.1//password - to make use of the default database of 0 with a password
+redis_servers = 127.0.0.1// - for default database of 0 with no password
+
+3. Once you have the Redis servers defined you can then make use of the
+experimental Redis lookup by specifying ${lookup redis{}} in a lookup query.
+
+4. Example usage:
+
+(Host List)
+hostlist relay_from_ips = <\n ${lookup redis{SMEMBERS relay_from_ips}}
+
+Where relay_from_ips is a Redis set which contains entries such as "192.168.0.0/24" "10.0.0.0/8" and so on.
+The result set is returned as
+192.168.0.0/24
+10.0.0.0/8
+..
+.
+
+(Domain list)
+domainlist virtual_domains = ${lookup redis {HGET $domain domain}}
+
+Where $domain is a hash which includes the key 'domain' and the value '$domain'.
+
+(Adding or updating an existing key)
+set acl_c_spammer = ${if eq{${lookup redis{SPAMMER_SET}}}{OK}}
+
+Where SPAMMER_SET is a macro and it is defined as
+
+"SET SPAMMER <some_value>"
+
+(Getting a value from Redis)
+
+set acl_c_spam_host = ${lookup redis{GET...}}
+
+
+
 --------------------------------------------------------------
 End of file
 --------------------------------------------------------------
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index a9abdab..2eb8a96 100755
--- a/src/scripts/MakeLinks
+++ b/src/scripts/MakeLinks
@@ -38,6 +38,7 @@ ln -s ../../src/lookups/ldap.h           ldap.h
 ln -s ../../src/lookups/ldap.c           ldap.c
 ln -s ../../src/lookups/lsearch.c        lsearch.c
 ln -s ../../src/lookups/mysql.c          mysql.c
+ln -s ../../src/lookups/redis.c          redis.c
 ln -s ../../src/lookups/nis.c            nis.c
 ln -s ../../src/lookups/nisplus.c        nisplus.c
 ln -s ../../src/lookups/oracle.c         oracle.c
diff --git a/src/scripts/lookups-Makefile b/src/scripts/lookups-Makefile
index e7aeaa0..51fbd94 100755
--- a/src/scripts/lookups-Makefile
+++ b/src/scripts/lookups-Makefile
@@ -76,6 +76,15 @@ want_at_all() {
   grep -q "^[ $tab]*$re" "$defs_source"
 }


+# Adapted want_at_all above to work for EXPERIMENTAL features
+want_experimental() {
+ local want_name="$1"
+ local re="EXPERIMENTAL_${want_name}[ $tab]*=[ $tab]*."
+ env | grep -q "^$re"
+ if [ $? -eq 0 ]; then return 0; fi
+ grep -q "^[ $tab]*$re" "$defs_source"
+}
+
# The values of these variables will be emitted into the Makefile.

MODS=""
@@ -139,6 +148,14 @@ fi

OBJ="${OBJ} spf.o"

+# Because the variable is EXPERIMENTAL_REDIS and not LOOKUP_REDIS we
+# use a different function to check for EXPERIMENTAL_* features
+# requested. Don't use the SPF method with dummy functions above.
+if want_experimental REDIS
+then
+ OBJ="${OBJ} redis.o"
+fi
+
echo "MODS = $MODS"
echo "OBJ = $OBJ"

diff --git a/src/src/EDITME b/src/src/EDITME
index 1db70f7..f44a1e3 100644
--- a/src/src/EDITME
+++ b/src/src/EDITME
@@ -473,6 +473,13 @@ EXIM_MONITOR=eximon.bin
# eg. for logging to a database.
# EXPERIMENTAL_TPDA=yes

+# Uncomment the following line to add Redis lookup support
+# You need to have hiredis installed on your system (https://github.com/redis/hiredis).
+# Depending on where it is installed you may have to edit the CFLAGS and LDFLAGS lines.
+# EXPERIMENTAL_REDIS=yes
+# CFLAGS += -I/usr/local/include
+# LDFLAGS += -lhiredis
+

 ###############################################################################
 #                 THESE ARE THINGS YOU MIGHT WANT TO SPECIFY                  #
diff --git a/src/src/config.h.defaults b/src/src/config.h.defaults
index bf7ac63..19bc1b1 100644
--- a/src/src/config.h.defaults
+++ b/src/src/config.h.defaults
@@ -168,6 +168,7 @@ it's a default value. */
 #define EXPERIMENTAL_DMARC
 #define EXPERIMENTAL_OCSP
 #define EXPERIMENTAL_PRDR
+#define EXPERIMENTAL_REDIS
 #define EXPERIMENTAL_SPF
 #define EXPERIMENTAL_SRS
 #define EXPERIMENTAL_TPDA
diff --git a/src/src/deliver.c b/src/src/deliver.c
index bc6a69f..8e1d177 100644
--- a/src/src/deliver.c
+++ b/src/src/deliver.c
@@ -945,6 +945,9 @@ if (addr->message != NULL)
   if (((Ustrstr(addr->message, "failed to expand") != NULL) || (Ustrstr(addr->message, "expansion of ") != NULL)) &&
       (Ustrstr(addr->message, "mysql") != NULL ||
        Ustrstr(addr->message, "pgsql") != NULL ||
+#ifdef EXPERIMENTAL_REDIS
+       Ustrstr(addr->message, "redis") != NULL ||
+#endif
        Ustrstr(addr->message, "sqlite") != NULL ||
        Ustrstr(addr->message, "ldap:") != NULL ||
        Ustrstr(addr->message, "ldapdn:") != NULL ||
diff --git a/src/src/drtables.c b/src/src/drtables.c
index c1332ed..699f327 100644
--- a/src/src/drtables.c
+++ b/src/src/drtables.c
@@ -447,6 +447,9 @@ extern lookup_module_info sqlite_lookup_module_info;
 #ifdef EXPERIMENTAL_SPF
 extern lookup_module_info spf_lookup_module_info;
 #endif
+#ifdef EXPERIMENTAL_REDIS
+extern lookup_module_info redis_lookup_module_info;
+#endif
 #if defined(LOOKUP_PGSQL) && LOOKUP_PGSQL!=2
 extern lookup_module_info pgsql_lookup_module_info;
 #endif
@@ -555,6 +558,10 @@ void init_lookup_list(void)
   addlookupmodule(NULL, &pgsql_lookup_module_info);
 #endif


+#ifdef EXPERIMENTAL_REDIS
+ addlookupmodule(NULL, &redis_lookup_module_info);
+#endif
+
#ifdef EXPERIMENTAL_SPF
addlookupmodule(NULL, &spf_lookup_module_info);
#endif
diff --git a/src/src/exim.c b/src/src/exim.c
index c5053ba..a715c0b 100644
--- a/src/src/exim.c
+++ b/src/src/exim.c
@@ -828,6 +828,9 @@ fprintf(f, "Support for:");
#ifdef EXPERIMENTAL_TPDA
fprintf(f, " Experimental_TPDA");
#endif
+#ifdef EXPERIMENTAL_REDIS
+ fprintf(f, " Experimental_Redis");
+#endif
fprintf(f, "\n");

 fprintf(f, "Lookups (built-in):");
diff --git a/src/src/expand.c b/src/src/expand.c
index a22ee2a..5a764d3 100644
--- a/src/src/expand.c
+++ b/src/src/expand.c
@@ -6644,6 +6644,9 @@ for (i = 1; i < argc; i++)
       #ifdef LOOKUP_PGSQL
       pgsql_servers = argv[i];
       #endif
+      #ifdef EXPERIMENTAL_REDIS
+      redis_servers = argv[i];
+      #endif
       }
   #ifdef EXIM_PERL
   else opt_perl_startup = argv[i];
diff --git a/src/src/globals.c b/src/src/globals.c
index d4589cd..1dfd23c 100644
--- a/src/src/globals.c
+++ b/src/src/globals.c
@@ -83,6 +83,10 @@ uschar *oracle_servers         = NULL;
 uschar *pgsql_servers          = NULL;
 #endif


+#ifdef EXPERIMENTAL_REDIS
+uschar *redis_servers          = NULL;
+#endif
+
 #ifdef LOOKUP_SQLITE
 int     sqlite_lock_timeout    = 5;
 #endif
diff --git a/src/src/globals.h b/src/src/globals.h
index 104b5fa..4acc7f8 100644
--- a/src/src/globals.h
+++ b/src/src/globals.h
@@ -62,6 +62,10 @@ extern uschar *oracle_servers;         /* List of servers and connect info */
 extern uschar *pgsql_servers;          /* List of servers and connect info */
 #endif


+#ifdef EXPERIMENTAL_REDIS
+extern uschar *redis_servers;          /* List of servers and connect info */
+#endif
+
 #ifdef LOOKUP_SQLITE
 extern int     sqlite_lock_timeout;    /* Internal lock waiting timeout */
 #endif
diff --git a/src/src/lookups/Makefile b/src/src/lookups/Makefile
index 035f6f2..6ba0cb1 100644
--- a/src/src/lookups/Makefile
+++ b/src/src/lookups/Makefile
@@ -41,6 +41,7 @@ nisplus.o:       $(PHDRS) nisplus.c
 oracle.o:        $(PHDRS) oracle.c
 passwd.o:        $(PHDRS) passwd.c
 pgsql.o:         $(PHDRS) pgsql.c
+redis.o:         $(PHDRS) redis.c
 spf.o:           $(PHDRS) spf.c
 sqlite.o:        $(PHDRS) sqlite.c
 testdb.o:        $(PHDRS) testdb.c
@@ -59,6 +60,7 @@ nisplus.so:       $(PHDRS) nisplus.c
 oracle.so:        $(PHDRS) oracle.c
 passwd.so:        $(PHDRS) passwd.c
 pgsql.so:         $(PHDRS) pgsql.c
+redis.so:         $(PHDRS) redis.c
 spf.so:           $(PHDRS) spf.c
 sqlite.so:        $(PHDRS) sqlite.c
 testdb.so:        $(PHDRS) testdb.c
diff --git a/src/src/lookups/redis.c b/src/src/lookups/redis.c
new file mode 100644
index 0000000..87cc9fd
--- /dev/null
+++ b/src/src/lookups/redis.c
@@ -0,0 +1,349 @@
+/*************************************************
+*     Exim - an Internet mail transport agent    *
+*************************************************/
+
+/* Copyright (c) University of Cambridge 1995 - 2009 */
+/* See the file NOTICE for conditions of use and distribution. */
+
+#include "../exim.h"
+
+#ifdef EXPERIMENTAL_REDIS
+
+#include "lf_functions.h"
+
+#include <hiredis/hiredis.h>
+
+/* 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(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) != NULL) {
+               redis_connections = cn->next;
+               DEBUG(D_lookup) debug_printf("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
+ *
+ *     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(uschar *command, uschar *server, uschar **resultptr,
+  uschar **errmsg, BOOL *defer_break, BOOL *do_cache)
+{
+       redisContext *redis_handle = NULL;        /* Keep compilers happy */
+       redisReply *redis_reply = NULL;
+       redisReply *entry = NULL;
+       redisReply *tentry = NULL;
+       redis_connection *cn;
+       int ssize = 0;
+       int offset = 0;
+       int yield = DEFER;
+       int i, j;
+       uschar *result = NULL;
+       uschar *server_copy = NULL;
+       uschar *tmp, *ttmp;
+       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 (i = 2; i > 0; i--) {
+                        uschar *pp = Ustrrchr(server, '/');
+                        if (pp == NULL) {
+                                *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 != NULL; cn = cn->next) {
+               if (Ustrcmp(cn->server, server_copy) == 0) {
+                       redis_handle = cn->handle;
+                       break;
+               }
+       }
+
+       if (cn == NULL) {
+               uschar *p;
+               uschar *socket = NULL;
+               int port = 0;
+               /* int redis_err = REDIS_OK; */
+
+               if ((p = Ustrchr(sdata[0], '(')) != NULL) {
+                       *p++ = 0;
+                       socket = p;
+                       while (*p != 0 && *p != ')')
+                               p++;
+                       *p = 0;
+               }
+
+               if ((p = Ustrchr(sdata[0], ':')) != NULL) {
+                       *p++ = 0;
+                       port = Uatoi(p);
+               } else {
+                       port = Uatoi("6379");
+               }
+
+               if (Ustrchr(server, '/') != NULL) {
+                       *errmsg = string_sprintf("unexpected slash in Redis server hostname: %s", sdata[0]);
+                       *defer_break = TRUE;
+                       return DEFER;
+               }
+
+               DEBUG(D_lookup)
+               debug_printf("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 ? */
+               if (socket != NULL)
+                       redis_handle = redisConnectUnix(CCS socket);
+               else
+                       redis_handle = redisConnect(CCS server, port);
+               if (redis_handle == NULL) {
+                       *errmsg = string_sprintf("REDIS connection failed");
+                       *defer_break = FALSE;
+                       goto REDIS_EXIT;
+               }
+
+               /* Add the connection to the cache */
+               cn = store_get(sizeof(redis_connection));
+               cn->server = server_copy;
+               cn->handle = redis_handle;
+               cn->next = redis_connections;
+               redis_connections = cn;
+       } else {
+               DEBUG(D_lookup)
+               debug_printf("REDIS using cached connection for %s\n", server_copy);
+       }
+
+       /* Authenticate if there is a password */
+       if(sdata[2] != NULL) {
+               if ((redis_reply = redisCommand(redis_handle, "AUTH %s", sdata[2])) == NULL) {
+                       *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] != NULL) {
+               if ((redis_reply = redisCommand(redis_handle, "SELECT %s", sdata[1])) == NULL) {
+                       *errmsg = string_sprintf("REDIS: Selecting database=%s failed: %s\n", sdata[1], redis_handle->errstr);
+                       *defer_break = FALSE;
+                       goto REDIS_EXIT;
+               } else {
+                       DEBUG(D_lookup) debug_printf("REDIS: Selecting database=%s\n", sdata[1]);
+               }
+       }
+
+       /* Run the command */
+       if ((redis_reply = redisCommand(redis_handle, CS command)) == 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);
+               *defer_break = FALSE;
+               *do_cache = FALSE;
+               goto REDIS_EXIT;
+               /* NOTREACHED */
+
+               break;
+       case REDIS_REPLY_NIL:
+               DEBUG(D_lookup) debug_printf("REDIS: query was not one that returned any data\n");
+               result = string_sprintf("");
+               *do_cache = FALSE;
+               goto REDIS_EXIT;
+               /* NOTREACHED */
+
+               break;
+       case REDIS_REPLY_INTEGER:
+               ttmp = (redis_reply->integer == 1) ? US"true" : US"false";
+               result = string_cat(result, &ssize, &offset, US ttmp, Ustrlen(ttmp));
+               break;
+       case REDIS_REPLY_STRING:
+       case REDIS_REPLY_STATUS:
+               result = string_cat(result, &ssize, &offset, 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 (i = 0; i < redis_reply->elements; i++) {
+                       entry = redis_reply->element[i];
+
+                       if (result != NULL)
+                               result = string_cat(result, &ssize, &offset, US"\n", 1);
+
+                       switch (entry->type) {
+                       case REDIS_REPLY_INTEGER:
+                               tmp = string_sprintf("%d", entry->integer);
+                               result = string_cat(result, &ssize, &offset, US tmp, Ustrlen(tmp));
+                               break;
+                       case REDIS_REPLY_STRING:
+                               result = string_cat(result, &ssize, &offset, US entry->str, entry->len);
+                               break;
+                       case REDIS_REPLY_ARRAY:
+                               for (j = 0; j < entry->elements; j++) {
+                                       tentry = entry->element[j];
+
+                                       if (result != NULL)
+                                               result = string_cat(result, &ssize, &offset, US"\n", 1);
+
+                                       switch (tentry->type) {
+                                       case REDIS_REPLY_INTEGER:
+                                               ttmp = string_sprintf("%d", tentry->integer);
+                                               result = string_cat(result, &ssize, &offset, US ttmp, Ustrlen(ttmp));
+                                               break;
+                                       case REDIS_REPLY_STRING:
+                                               result = string_cat(result, &ssize, &offset, US tentry->str, tentry->len);
+                                               break;
+                                       case REDIS_REPLY_ARRAY:
+                                               DEBUG(D_lookup) debug_printf("REDIS: result has nesting of arrays which is not supported. Ignoring!\n");
+                                               break;
+                                       default:
+                                               DEBUG(D_lookup) debug_printf("REDIS: result has unsupported type. Ignoring!\n");
+                                               break;
+                                       }
+                               }
+                               break;
+                       default:
+                               DEBUG(D_lookup) debug_printf("REDIS: query returned unsupported type\n");
+                               break;
+                       }
+               }
+               break;
+       }
+
+
+       if (result == NULL) {
+               yield = FAIL;
+               *errmsg = US"REDIS: no data found";
+       } else {
+               result[offset] = 0;
+               store_reset(result + offset + 1);
+       }
+
+    REDIS_EXIT:
+       /* Free store for any result that was got; don't close the connection, as it is cached. */
+       if (redis_reply != NULL)
+               freeReplyObject(redis_reply);
+
+       /* Non-NULL result indicates a sucessful result */
+       if (result != NULL) {
+               *resultptr = result;
+               return OK;
+       } else {
+               DEBUG(D_lookup) debug_printf("%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)), uschar *filename __attribute__((unused)),
+           uschar *command, int length, uschar **result, uschar **errmsg, BOOL *do_cache)
+{
+       return lf_sqlperform(US"Redis", US"redis_servers", redis_servers, command,
+         result, errmsg, do_cache, perform_redis_search);
+}
+
+/*************************************************
+*         Version reporting entry point          *
+*************************************************/
+#include "../version.h"
+
+void
+redis_version_report(FILE *f)
+{
+       fprintf(f, "Library version: REDIS: Compile: %d [%d]\n",
+               HIREDIS_MAJOR, HIREDIS_MINOR);
+#ifdef DYNLOOKUP
+       fprintf(f, "                        Exim version %s\n", EXIM_VERSION_STR);
+#endif
+}
+
+/* These are the lookup_info blocks for this driver */
+static lookup_info redis_lookup_info = {
+  US"redis",                     /* lookup name */
+  lookup_querystyle,             /* query-style lookup */
+  redis_open,                    /* open function */
+  NULL,                          /* no check function */
+  redis_find,                    /* find function */
+  NULL,                          /* no close function */
+  redis_tidy,                    /* tidy function */
+  NULL,                                /* quoting function */
+  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 /* EXPERIMENTAL_REDIS */
+/* End of lookups/redis.c */
diff --git a/src/src/readconf.c b/src/src/readconf.c
index 218eff7..6b0f3aa 100644
--- a/src/src/readconf.c
+++ b/src/src/readconf.c
@@ -350,6 +350,9 @@ static optionlist optionlist_config[] = {
   { "recipient_unqualified_hosts", opt_stringptr, &recipient_unqualified_hosts },
   { "recipients_max",           opt_int,         &recipients_max },
   { "recipients_max_reject",    opt_bool,        &recipients_max_reject },
+#ifdef EXPERIMENTAL_REDIS
+  { "redis_servers",            opt_stringptr,   &redis_servers },
+#endif
   { "remote_max_parallel",      opt_int,         &remote_max_parallel },
   { "remote_sort_domains",      opt_stringptr,   &remote_sort_domains },
   { "retry_data_expire",        opt_time,        &retry_data_expire },
diff --git a/src/src/route.c b/src/src/route.c
index 2fee382..f8f3b86 100644
--- a/src/src/route.c
+++ b/src/src/route.c
@@ -1961,6 +1961,9 @@ if (yield == DEFER) {
     (
       Ustrstr(addr->message, "mysql") != NULL ||
       Ustrstr(addr->message, "pgsql") != NULL ||
+#ifdef EXPERIMENTAL_REDIS
+      Ustrstr(addr->message, "redis") != NULL ||
+#endif
       Ustrstr(addr->message, "sqlite") != NULL ||
       Ustrstr(addr->message, "ldap:") != NULL ||
       Ustrstr(addr->message, "ldapdn:") != NULL ||