Copyright updates:
[exim.git] / src / src / lookups / mysql.c
CommitLineData
0756eb3c
PH
1/*************************************************
2* Exim - an Internet mail transport agent *
3*************************************************/
4
f9ba5e22 5/* Copyright (c) University of Cambridge 1995 - 2018 */
1e1ddfac 6/* Copyright (c) The Exim Maintainers 2020 */
0756eb3c
PH
7/* See the file NOTICE for conditions of use and distribution. */
8
9/* Thanks to Paul Kelly for contributing the original code for these
10functions. */
11
12
13#include "../exim.h"
14#include "lf_functions.h"
0756eb3c
PH
15
16#include <mysql.h> /* The system header */
31beb797
HSHR
17
18/* We define symbols for *_VERSION_ID (numeric), *_VERSION_STR (char*)
19and *_BASE_STR (char*). It's a bit of guesswork. Especially for mariadb
20with versions before 10.2, as they do not define there there specific symbols.
21*/
22
8b6c5715 23/* Newer (>= 10.2) MariaDB */
31beb797
HSHR
24#if defined MARIADB_VERSION_ID
25#define EXIM_MxSQL_VERSION_ID MARIADB_VERSION_ID
26
8b6c5715
JH
27/* MySQL defines MYSQL_VERSION_ID, and MariaDB does so */
28/* https://dev.mysql.com/doc/refman/5.7/en/c-api-server-client-versions.html */
31beb797
HSHR
29#elif defined LIBMYSQL_VERSION_ID
30#define EXIM_MxSQL_VERSION_ID LIBMYSQL_VERSION_ID
31#elif defined MYSQL_VERSION_ID
32#define EXIM_MxSQL_VERSION_ID MYSQL_VERSION_ID
33
34#else
35#define EXIM_MYSQL_VERSION_ID 0
36#endif
37
8b6c5715 38/* Newer (>= 10.2) MariaDB */
31beb797
HSHR
39#ifdef MARIADB_CLIENT_VERSION_STR
40#define EXIM_MxSQL_VERSION_STR MARIADB_CLIENT_VERSION_STR
41
8b6c5715 42/* Mysql uses MYSQL_SERVER_VERSION */
31beb797
HSHR
43#elif defined LIBMYSQL_VERSION
44#define EXIM_MxSQL_VERSION_STR LIBMYSQL_VERSION
45#elif defined MYSQL_SERVER_VERSION
46#define EXIM_MxSQL_VERSION_STR MYSQL_SERVER_VERSION
47
48#else
eb002283 49#define EXIM_MxSQL_VERSION_STR "unknown"
31beb797
HSHR
50#endif
51
52#if defined MARIADB_BASE_VERSION
53#define EXIM_MxSQL_BASE_STR MARIADB_BASE_VERSION
54
55#elif defined MARIADB_PACKAGE_VERSION
56#define EXIM_MxSQL_BASE_STR "mariadb"
57
58#elif defined MYSQL_BASE_VERSION
59#define EXIM_MxSQL_BASE_STR MYSQL_BASE_VERSION
60
61#else
62#define EXIM_MxSQL_BASE_STR "n.A."
63#endif
0756eb3c
PH
64
65
66/* Structure and anchor for caching connections. */
67
68typedef struct mysql_connection {
69 struct mysql_connection *next;
70 uschar *server;
71 MYSQL *handle;
72} mysql_connection;
73
74static mysql_connection *mysql_connections = NULL;
75
76
77
78/*************************************************
79* Open entry point *
80*************************************************/
81
82/* See local README for interface description. */
83
e6d225ae 84static void *
d447dbd1 85mysql_open(const uschar * filename, uschar ** errmsg)
0756eb3c
PH
86{
87return (void *)(1); /* Just return something non-null */
88}
89
90
91
92/*************************************************
93* Tidy entry point *
94*************************************************/
95
96/* See local README for interface description. */
97
e6d225ae 98static void
0756eb3c
PH
99mysql_tidy(void)
100{
101mysql_connection *cn;
102while ((cn = mysql_connections) != NULL)
103 {
104 mysql_connections = cn->next;
42c7f0b4 105 DEBUG(D_lookup) debug_printf_indent("close MYSQL connection: %s\n", cn->server);
0756eb3c
PH
106 mysql_close(cn->handle);
107 }
108}
109
110
111
112/*************************************************
113* Internal search function *
114*************************************************/
115
116/* This function is called from the find entry point to do the search for a
117single server.
118
119Arguments:
120 query the query string
121 server the server string
122 resultptr where to store the result
123 errmsg where to point an error message
124 defer_break TRUE if no more servers are to be tried after DEFER
14b3c5bc 125 do_cache set zero if data is changed
0b4dfe7a 126 opts options
0756eb3c
PH
127
128The server string is of the form "host/dbname/user/password". The host can be
129host:port. This string is in a nextinlist temporary buffer, so can be
130overwritten.
131
132Returns: OK, FAIL, or DEFER
133*/
134
135static int
55414b25 136perform_mysql_search(const uschar *query, uschar *server, uschar **resultptr,
0b4dfe7a 137 uschar **errmsg, BOOL *defer_break, uint *do_cache, const uschar * opts)
0756eb3c
PH
138{
139MYSQL *mysql_handle = NULL; /* Keep compilers happy */
140MYSQL_RES *mysql_result = NULL;
141MYSQL_ROW mysql_row_data;
142MYSQL_FIELD *fields;
143
144int i;
0756eb3c
PH
145int yield = DEFER;
146unsigned int num_fields;
acec9514 147gstring * result = NULL;
0756eb3c
PH
148mysql_connection *cn;
149uschar *server_copy = NULL;
150uschar *sdata[4];
151
152/* Disaggregate the parameters from the server argument. The order is host,
153database, user, password. We can write to the string, since it is in a
154nextinlist temporary buffer. The copy of the string that is used for caching
155has the password removed. This copy is also used for debugging output. */
156
d7978c0f 157for (int i = 3; i > 0; i--)
0756eb3c
PH
158 {
159 uschar *pp = Ustrrchr(server, '/');
0b4dfe7a 160 if (!pp)
0756eb3c
PH
161 {
162 *errmsg = string_sprintf("incomplete MySQL server data: %s",
163 (i == 3)? server : server_copy);
164 *defer_break = TRUE;
165 return DEFER;
166 }
167 *pp++ = 0;
168 sdata[i] = pp;
169 if (i == 3) server_copy = string_copy(server); /* sans password */
170 }
171sdata[0] = server; /* What's left at the start */
172
173/* See if we have a cached connection to the server */
174
a159f203 175for (cn = mysql_connections; cn; cn = cn->next)
0756eb3c 176 if (Ustrcmp(cn->server, server_copy) == 0)
0b4dfe7a 177 { mysql_handle = cn->handle; break; }
0756eb3c
PH
178
179/* If no cached connection, we must set one up. Mysql allows for a host name
180and port to be specified. It also allows the name of a Unix socket to be used.
181Unfortunately, this contains slashes, but its use is expected to be rare, so
182the rather cumbersome syntax shouldn't inconvenience too many people. We use
a159f203
JH
183this: host:port(socket)[group] where all the parts are optional.
184The "group" parameter specifies an option group from a MySQL option file. */
0756eb3c 185
a159f203 186if (!cn)
0756eb3c
PH
187 {
188 uschar *p;
189 uschar *socket = NULL;
190 int port = 0;
a159f203
JH
191 uschar *group = US"exim";
192
193 if ((p = Ustrchr(sdata[0], '[')))
194 {
195 *p++ = 0;
196 group = p;
197 while (*p && *p != ']') p++;
198 *p = 0;
199 }
0756eb3c 200
a159f203 201 if ((p = Ustrchr(sdata[0], '(')))
0756eb3c
PH
202 {
203 *p++ = 0;
204 socket = p;
a159f203 205 while (*p && *p != ')') p++;
0756eb3c
PH
206 *p = 0;
207 }
208
a159f203 209 if ((p = Ustrchr(sdata[0], ':')))
0756eb3c
PH
210 {
211 *p++ = 0;
212 port = Uatoi(p);
213 }
214
a159f203 215 if (Ustrchr(sdata[0], '/'))
0756eb3c
PH
216 {
217 *errmsg = string_sprintf("unexpected slash in MySQL server hostname: %s",
218 sdata[0]);
219 *defer_break = TRUE;
220 return DEFER;
221 }
222
223 /* If the database is the empty string, set it NULL - the query must then
224 define it. */
225
226 if (sdata[1][0] == 0) sdata[1] = NULL;
227
228 DEBUG(D_lookup)
42c7f0b4 229 debug_printf_indent("MYSQL new connection: host=%s port=%d socket=%s "
0756eb3c
PH
230 "database=%s user=%s\n", sdata[0], port, socket, sdata[1], sdata[2]);
231
232 /* Get store for a new handle, initialize it, and connect to the server */
233
f3ebb786 234 mysql_handle = store_get(sizeof(MYSQL), FALSE);
0756eb3c 235 mysql_init(mysql_handle);
a159f203 236 mysql_options(mysql_handle, MYSQL_READ_DEFAULT_GROUP, CS group);
0756eb3c
PH
237 if (mysql_real_connect(mysql_handle,
238 /* host user passwd database */
239 CS sdata[0], CS sdata[2], CS sdata[3], CS sdata[1],
f6efe9ce 240 port, CS socket, CLIENT_MULTI_RESULTS) == NULL)
0756eb3c
PH
241 {
242 *errmsg = string_sprintf("MYSQL connection failed: %s",
243 mysql_error(mysql_handle));
244 *defer_break = FALSE;
245 goto MYSQL_EXIT;
246 }
247
248 /* Add the connection to the cache */
249
f3ebb786 250 cn = store_get(sizeof(mysql_connection), FALSE);
0756eb3c
PH
251 cn->server = server_copy;
252 cn->handle = mysql_handle;
253 cn->next = mysql_connections;
254 mysql_connections = cn;
255 }
256
257/* Else use a previously cached connection */
258
259else
260 {
261 DEBUG(D_lookup)
42c7f0b4 262 debug_printf_indent("MYSQL using cached connection for %s\n", server_copy);
0756eb3c
PH
263 }
264
265/* Run the query */
266
267if (mysql_query(mysql_handle, CS query) != 0)
268 {
269 *errmsg = string_sprintf("MYSQL: query failed: %s\n",
270 mysql_error(mysql_handle));
271 *defer_break = FALSE;
272 goto MYSQL_EXIT;
273 }
274
275/* Pick up the result. If the query was not of the type that returns data,
276namely INSERT, UPDATE, or DELETE, an error occurs here. However, this situation
277can be detected by calling mysql_field_count(). If its result is zero, no data
278was expected (this is all explained clearly in the MySQL manual). In this case,
279we return the number of rows affected by the command. In this event, we do NOT
280want to cache the result; also the whole cache for the handle must be cleaned
14b3c5bc 281up. Setting do_cache zero requests this. */
0756eb3c 282
ba0e37b1 283if (!(mysql_result = mysql_use_result(mysql_handle)))
0756eb3c 284 {
0b4dfe7a 285 if (mysql_field_count(mysql_handle) == 0)
0756eb3c 286 {
42c7f0b4 287 DEBUG(D_lookup) debug_printf_indent("MYSQL: query was not one that returns data\n");
acec9514
JH
288 result = string_cat(result,
289 string_sprintf("%d", mysql_affected_rows(mysql_handle)));
14b3c5bc 290 *do_cache = 0;
0756eb3c
PH
291 goto MYSQL_EXIT;
292 }
293 *errmsg = string_sprintf("MYSQL: lookup result failed: %s\n",
294 mysql_error(mysql_handle));
295 *defer_break = FALSE;
296 goto MYSQL_EXIT;
297 }
298
299/* Find the number of fields returned. If this is one, we don't add field
300names to the data. Otherwise we do. */
301
302num_fields = mysql_num_fields(mysql_result);
303
304/* Get the fields and construct the result string. If there is more than one
305row, we insert '\n' between them. */
306
307fields = mysql_fetch_fields(mysql_result);
308
acec9514 309while ((mysql_row_data = mysql_fetch_row(mysql_result)))
0756eb3c
PH
310 {
311 unsigned long *lengths = mysql_fetch_lengths(mysql_result);
312
acec9514
JH
313 if (result)
314 result = string_catn(result, US"\n", 1);
0756eb3c 315
ba0e37b1 316 if (num_fields != 1)
d7978c0f 317 for (int i = 0; i < num_fields; i++)
ba0e37b1
JH
318 result = lf_quote(US fields[i].name, US mysql_row_data[i], lengths[i],
319 result);
0756eb3c 320
ba0e37b1
JH
321 else if (mysql_row_data[0] != NULL) /* NULL value yields nothing */
322 result = string_catn(result, US mysql_row_data[0], lengths[0]);
0756eb3c
PH
323 }
324
f6efe9ce
NM
325/* more results? -1 = no, >0 = error, 0 = yes (keep looping)
326 This is needed because of the CLIENT_MULTI_RESULTS on mysql_real_connect(),
327 we don't expect any more results. */
328
ba0e37b1 329while((i = mysql_next_result(mysql_handle)) >= 0)
0b4dfe7a 330 if(i != 0)
ba0e37b1 331 {
0b4dfe7a
JH
332 *errmsg = string_sprintf(
333 "MYSQL: lookup result error when checking for more results: %s\n",
334 mysql_error(mysql_handle));
335 goto MYSQL_EXIT;
ba0e37b1 336 }
0b4dfe7a
JH
337 else /* just ignore more results */
338 DEBUG(D_lookup) debug_printf_indent("MYSQL: got unexpected more results\n");
f6efe9ce 339
0756eb3c
PH
340/* If result is NULL then no data has been found and so we return FAIL.
341Otherwise, we must terminate the string which has been built; string_cat()
342always leaves enough room for a terminating zero. */
343
acec9514 344if (!result)
0756eb3c
PH
345 {
346 yield = FAIL;
347 *errmsg = US"MYSQL: no data found";
348 }
0756eb3c
PH
349
350/* Get here by goto from various error checks and from the case where no data
351was read (e.g. an update query). */
352
353MYSQL_EXIT:
354
355/* Free mysal store for any result that was got; don't close the connection, as
356it is cached. */
357
acec9514 358if (mysql_result) mysql_free_result(mysql_result);
0756eb3c 359
4c04137d 360/* Non-NULL result indicates a successful result */
0756eb3c 361
acec9514 362if (result)
0756eb3c 363 {
ba0e37b1 364 *resultptr = string_from_gstring(result);
f3ebb786 365 gstring_release_unused(result);
0756eb3c
PH
366 return OK;
367 }
368else
369 {
42c7f0b4 370 DEBUG(D_lookup) debug_printf_indent("%s\n", *errmsg);
0756eb3c
PH
371 return yield; /* FAIL or DEFER */
372 }
373}
374
375
376
377
378/*************************************************
379* Find entry point *
380*************************************************/
381
382/* See local README for interface description. The handle and filename
b7670459
PH
383arguments are not used. The code to loop through a list of servers while the
384query is deferred with a retryable error is now in a separate function that is
385shared with other SQL lookups. */
0756eb3c 386
e6d225ae 387static int
d447dbd1 388mysql_find(void * handle, const uschar * filename, const uschar * query,
67a57a5a
JH
389 int length, uschar ** result, uschar ** errmsg, uint * do_cache,
390 const uschar * opts)
0756eb3c 391{
b7670459 392return lf_sqlperform(US"MySQL", US"mysql_servers", mysql_servers, query,
0b4dfe7a 393 result, errmsg, do_cache, opts, perform_mysql_search);
0756eb3c
PH
394}
395
396
397
398/*************************************************
399* Quote entry point *
400*************************************************/
401
402/* The only characters that need to be quoted (with backslash) are newline,
403tab, carriage return, backspace, backslash itself, and the quote characters.
404Percent, and underscore and not escaped. They are only special in contexts
405where they can be wild cards, and this isn't usually the case for data inserted
406from messages, since that isn't likely to be treated as a pattern of any kind.
407Sadly, MySQL doesn't seem to behave like other programs. If you use something
408like "where id="ab\%cd" it does not treat the string as "ab%cd". So you really
409can't quote "on spec".
410
411Arguments:
412 s the string to be quoted
413 opt additional option text or NULL if none
414
415Returns: the processed string or NULL for a bad option
416*/
417
e6d225ae 418static uschar *
0756eb3c
PH
419mysql_quote(uschar *s, uschar *opt)
420{
421register int c;
422int count = 0;
423uschar *t = s;
424uschar *quoted;
425
426if (opt != NULL) return NULL; /* No options recognized */
427
428while ((c = *t++) != 0)
429 if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL) count++;
430
431if (count == 0) return s;
f3ebb786 432t = quoted = store_get(Ustrlen(s) + count + 1, is_tainted(s));
0756eb3c
PH
433
434while ((c = *s++) != 0)
435 {
436 if (Ustrchr("\n\t\r\b\'\"\\", c) != NULL)
437 {
438 *t++ = '\\';
439 switch(c)
440 {
441 case '\n': *t++ = 'n';
442 break;
443 case '\t': *t++ = 't';
444 break;
445 case '\r': *t++ = 'r';
446 break;
447 case '\b': *t++ = 'b';
448 break;
449 default: *t++ = c;
450 break;
451 }
452 }
453 else *t++ = c;
454 }
455
456*t = 0;
457return quoted;
458}
459
6545de78
PP
460
461/*************************************************
462* Version reporting entry point *
463*************************************************/
464
465/* See local README for interface description. */
466
467#include "../version.h"
468
469void
470mysql_version_report(FILE *f)
471{
31beb797
HSHR
472fprintf(f, "Library version: MySQL: Compile: %lu %s [%s]\n"
473 " Runtime: %lu %s\n",
474 (long)EXIM_MxSQL_VERSION_ID, EXIM_MxSQL_VERSION_STR, EXIM_MxSQL_BASE_STR,
475 mysql_get_client_version(), mysql_get_client_info());
6545de78
PP
476#ifdef DYNLOOKUP
477fprintf(f, " Exim version %s\n", EXIM_VERSION_STR);
478#endif
479}
480
e6d225ae
DW
481/* These are the lookup_info blocks for this driver */
482
483static lookup_info mysql_lookup_info = {
9f400174
JH
484 .name = US"mysql", /* lookup name */
485 .type = lookup_querystyle, /* query-style lookup */
486 .open = mysql_open, /* open function */
487 .check = NULL, /* no check function */
488 .find = mysql_find, /* find function */
489 .close = NULL, /* no close function */
490 .tidy = mysql_tidy, /* tidy function */
491 .quote = mysql_quote, /* quoting function */
492 .version_report = mysql_version_report /* version reporting */
e6d225ae
DW
493};
494
495#ifdef DYNLOOKUP
496#define mysql_lookup_module_info _lookup_module_info
497#endif
498
499static lookup_info *_lookup_list[] = { &mysql_lookup_info };
500lookup_module_info mysql_lookup_module_info = { LOOKUP_MODULE_INFO_MAGIC, _lookup_list, 1 };
0756eb3c
PH
501
502/* End of lookups/mysql.c */