From: Todd Lyons Date: Tue, 1 Oct 2013 16:24:19 +0000 (-0700) Subject: Bugzilla 1217: Experimental Redis lookup X-Git-Tag: exim-4_82_RC2~2 X-Git-Url: https://git.exim.org/exim.git/commitdiff_plain/9bdd29ad5c5d394229753b114f6f288893f616f4?hp=9cb1785a68901ce990c7581bd465d0931edf166e 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. --- diff --git a/doc/doc-txt/ChangeLog b/doc/doc-txt/ChangeLog index 0f603e4eb..f5fd6d6d2 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 271ab0ba0..a5024d827 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 " + +(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 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 + +/* 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 ||