summaryrefslogtreecommitdiffstats
path: root/src/lookups/mysql.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/lookups/mysql.c')
-rw-r--r--src/lookups/mysql.c503
1 files changed, 503 insertions, 0 deletions
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 */