summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rwxr-xr-xsrc/scripts/MakeLinks1
-rwxr-xr-xsrc/scripts/lookups-Makefile17
-rw-r--r--src/src/EDITME7
-rw-r--r--src/src/config.h.defaults1
-rw-r--r--src/src/deliver.c3
-rw-r--r--src/src/drtables.c7
-rw-r--r--src/src/exim.c3
-rw-r--r--src/src/expand.c3
-rw-r--r--src/src/globals.c4
-rw-r--r--src/src/globals.h4
-rw-r--r--src/src/lookups/Makefile2
-rw-r--r--src/src/lookups/redis.c349
-rw-r--r--src/src/readconf.c3
-rw-r--r--src/src/route.c3
14 files changed, 407 insertions, 0 deletions
diff --git a/src/scripts/MakeLinks b/src/scripts/MakeLinks
index a9abdab25..2eb8a967e 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 e7aeaa08a..51fbd944b 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 1db70f715..f44a1e3a5 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 bf7ac63fb..19bc1b180 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 bc6a69fbf..8e1d17793 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 c1332ed0b..699f32762 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 c5053ba7c..a715c0b39 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 a22ee2af4..5a764d3df 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 d4589cd18..1dfd23ce2 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 104b5fa7a..4acc7f8c2 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 035f6f23f..6ba0cb169 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 000000000..87cc9fd1a
--- /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 218eff704..6b0f3aaf7 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 2fee38271..f8f3b86a5 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 ||