xref: /netbsd-src/external/ibm-public/postfix/dist/src/global/dict_mysql.c (revision daf6c4152fcddc27c445489775ed1f66ab4ea9a9)
1 /*	$NetBSD: dict_mysql.c,v 1.1.1.1 2009/06/23 10:08:45 tron Exp $	*/
2 
3 /*++
4 /* NAME
5 /*	dict_mysql 3
6 /* SUMMARY
7 /*	dictionary manager interface to MySQL databases
8 /* SYNOPSIS
9 /*	#include <dict_mysql.h>
10 /*
11 /*	DICT	*dict_mysql_open(name, open_flags, dict_flags)
12 /*	const char *name;
13 /*	int	open_flags;
14 /*	int	dict_flags;
15 /* DESCRIPTION
16 /*	dict_mysql_open() creates a dictionary of type 'mysql'.  This
17 /*	dictionary is an interface for the postfix key->value mappings
18 /*	to mysql.  The result is a pointer to the installed dictionary,
19 /*	or a null pointer in case of problems.
20 /*
21 /*	The mysql dictionary can manage multiple connections to different
22 /*	sql servers on different hosts.  It assumes that the underlying data
23 /*	on each host is identical (mirrored) and maintains one connection
24 /*	at any given time.  If any connection fails,  any other available
25 /*	ones will be opened and used.  The intent of this feature is to eliminate
26 /*	a single point of failure for mail systems that would otherwise rely
27 /*	on a single mysql server.
28 /* .PP
29 /*	Arguments:
30 /* .IP name
31 /*	Either the path to the MySQL configuration file (if it starts
32 /*	with '/' or '.'), or the prefix which will be used to obtain
33 /*	main.cf configuration parameters for this search.
34 /*
35 /*	In the first case, the configuration parameters below are
36 /*	specified in the file as \fIname\fR=\fBvalue\fR pairs.
37 /*
38 /*	In the second case, the configuration parameters are
39 /*	prefixed with the value of \fIname\fR and an underscore,
40 /*	and they are specified in main.cf.  For example, if this
41 /*	value is \fImysqlsource\fR, the parameters would look like
42 /*	\fImysqlsource_user\fR, \fImysqlsource_table\fR, and so on.
43 /*
44 /* .IP other_name
45 /*	reference for outside use.
46 /* .IP open_flags
47 /*	Must be O_RDONLY.
48 /* .IP dict_flags
49 /*	See dict_open(3).
50 /* .PP
51 /*	Configuration parameters:
52 /*
53 /*	The parameters encodes a number of pieces of information:
54 /*	username, password, databasename, table, select_field,
55 /*	where_field, and hosts:
56 /* .IP \fIuser\fR
57 /* 	Username for connecting to the database.
58 /* .IP \fIpassword\fR
59 /*	Password for the above.
60 /* .IP \fIdbname\fR
61 /*	Name of the database.
62 /* .IP \fIdomain\fR
63 /*      List of domains the queries should be restricted to.  If
64 /*      specified, only FQDN addresses whose domain parts matching this
65 /*      list will be queried against the SQL database.  Lookups for
66 /*      partial addresses are also supressed.  This can significantly
67 /*      reduce the query load on the server.
68 /* .IP \fIquery\fR
69 /*      Query template, before the query is actually issued, variable
70 /*	substitutions are performed. See mysql_table(5) for details. If
71 /*	No query is specified, the legacy variables \fItable\fR,
72 /*	\fIselect_field\fR, \fIwhere_field\fR and \fIadditional_conditions\fR
73 /*	are used to construct the query template.
74 /* .IP \fIresult_format\fR
75 /*      The format used to expand results from queries.  Substitutions
76 /*      are performed as described in mysql_table(5). Defaults to returning
77 /*	the lookup result unchanged.
78 /* .IP expansion_limit
79 /*	Limit (if any) on the total number of lookup result values. Lookups which
80 /*	exceed the limit fail with dict_errno=DICT_ERR_RETRY. Note that each
81 /*	non-empty (and non-NULL) column of a multi-column result row counts as
82 /*	one result.
83 /* .IP \fItable\fR
84 /*	When \fIquery\fR is not set, name of the table used to construct the
85 /*	query string. This provides compatibility with older releases.
86 /* .IP \fIselect_field\fR
87 /*	When \fIquery\fR is not set, name of the result field used to
88 /*	construct the query string. This provides compatibility with older
89 /*	releases.
90 /* .IP \fIwhere_field\fR
91 /*	When \fIquery\fR is not set, name of the where clause field used to
92 /*	construct the query string. This provides compatibility with older
93 /*	releases.
94 /* .IP \fIadditional_conditions\fR
95 /*	When \fIquery\fR is not set, additional where clause conditions used
96 /*	to construct the query string. This provides compatibility with older
97 /*	releases.
98 /* .IP \fIhosts\fR
99 /*	List of hosts to connect to.
100 /* .PP
101 /*	For example, if you want the map to reference databases of
102 /*	the name "your_db" and execute a query like this: select
103 /*	forw_addr from aliases where alias like '<some username>'
104 /*	against any database called "vmailer_info" located on hosts
105 /*	host1.some.domain and host2.some.domain, logging in as user
106 /*	"vmailer" and password "passwd" then the configuration file
107 /*	should read:
108 /* .PP
109 /*	\fIuser\fR = \fBvmailer\fR
110 /* .br
111 /*	\fIpassword\fR = \fBpasswd\fR
112 /* .br
113 /*	\fIdbname\fR = \fBvmailer_info\fR
114 /* .br
115 /*	\fItable\fR = \fBaliases\fR
116 /* .br
117 /*	\fIselect_field\fR = \fBforw_addr\fR
118 /* .br
119 /*	\fIwhere_field\fR = \fBalias\fR
120 /* .br
121 /*	\fIhosts\fR = \fBhost1.some.domain\fR \fBhost2.some.domain\fR
122 /* .IP \fIadditional_conditions\fR
123 /*      Backward compatibility when \fIquery\fR is not set, additional
124 /*	conditions to the WHERE clause.
125 /* .IP \fIhosts\fR
126 /*	List of hosts to connect to.
127 /* .PP
128 /*	For example, if you want the map to reference databases of
129 /*	the name "your_db" and execute a query like this: select
130 /*	forw_addr from aliases where alias like '<some username>'
131 /*	against any database called "vmailer_info" located on hosts
132 /*	host1.some.domain and host2.some.domain, logging in as user
133 /*	"vmailer" and password "passwd" then the configuration file
134 /*	should read:
135 /* .PP
136 /*	\fIuser\fR = \fBvmailer\fR
137 /* .br
138 /*	\fIpassword\fR = \fBpasswd\fR
139 /* .br
140 /*	\fIdbname\fR = \fBvmailer_info\fR
141 /* .br
142 /*	\fItable\fR = \fBaliases\fR
143 /* .br
144 /*	\fIselect_field\fR = \fBforw_addr\fR
145 /* .br
146 /*	\fIwhere_field\fR = \fBalias\fR
147 /* .br
148 /*	\fIhosts\fR = \fBhost1.some.domain\fR \fBhost2.some.domain\fR
149 /* .PP
150 /* SEE ALSO
151 /*	dict(3) generic dictionary manager
152 /* AUTHOR(S)
153 /*	Scott Cotton
154 /*	IC Group, Inc.
155 /*	scott@icgroup.com
156 /*
157 /*	Joshua Marcus
158 /*	IC Group, Inc.
159 /*	josh@icgroup.com
160 /*--*/
161 
162 /* System library. */
163 #include "sys_defs.h"
164 
165 #ifdef HAS_MYSQL
166 #include <sys/socket.h>
167 #include <netinet/in.h>
168 #include <arpa/inet.h>
169 #include <netdb.h>
170 #include <stdio.h>
171 #include <string.h>
172 #include <stdlib.h>
173 #include <syslog.h>
174 #include <time.h>
175 #include <mysql.h>
176 
177 #ifdef STRCASECMP_IN_STRINGS_H
178 #include <strings.h>
179 #endif
180 
181 /* Utility library. */
182 
183 #include "dict.h"
184 #include "msg.h"
185 #include "mymalloc.h"
186 #include "argv.h"
187 #include "vstring.h"
188 #include "split_at.h"
189 #include "find_inet.h"
190 #include "myrand.h"
191 #include "events.h"
192 #include "stringops.h"
193 
194 /* Global library. */
195 
196 #include "cfg_parser.h"
197 #include "db_common.h"
198 
199 /* Application-specific. */
200 
201 #include "dict_mysql.h"
202 
203 /* need some structs to help organize things */
204 typedef struct {
205     MYSQL  *db;
206     char   *hostname;
207     char   *name;
208     unsigned port;
209     unsigned type;			/* TYPEUNIX | TYPEINET */
210     unsigned stat;			/* STATUNTRIED | STATFAIL | STATCUR */
211     time_t  ts;				/* used for attempting reconnection
212 					 * every so often if a host is down */
213 } HOST;
214 
215 typedef struct {
216     int     len_hosts;			/* number of hosts */
217     HOST  **db_hosts;			/* the hosts on which the databases
218 					 * reside */
219 } PLMYSQL;
220 
221 typedef struct {
222     DICT    dict;
223     CFG_PARSER *parser;
224     char   *query;
225     char   *result_format;
226     void   *ctx;
227     int     expansion_limit;
228     char   *username;
229     char   *password;
230     char   *dbname;
231     ARGV   *hosts;
232     PLMYSQL *pldb;
233 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
234     HOST   *active_host;
235 #endif
236 } DICT_MYSQL;
237 
238 #define STATACTIVE			(1<<0)
239 #define STATFAIL			(1<<1)
240 #define STATUNTRIED			(1<<2)
241 
242 #define TYPEUNIX			(1<<0)
243 #define TYPEINET			(1<<1)
244 
245 #define RETRY_CONN_MAX			100
246 #define RETRY_CONN_INTV			60		/* 1 minute */
247 #define IDLE_CONN_INTV			60		/* 1 minute */
248 
249 /* internal function declarations */
250 static PLMYSQL *plmysql_init(ARGV *);
251 static MYSQL_RES *plmysql_query(DICT_MYSQL *, const char *, VSTRING *, char *,
252 				char *, char *);
253 static void plmysql_dealloc(PLMYSQL *);
254 static void plmysql_close_host(HOST *);
255 static void plmysql_down_host(HOST *);
256 static void plmysql_connect_single(HOST *, char *, char *, char *);
257 static const char *dict_mysql_lookup(DICT *, const char *);
258 DICT   *dict_mysql_open(const char *, int, int);
259 static void dict_mysql_close(DICT *);
260 static void mysql_parse_config(DICT_MYSQL *, const char *);
261 static HOST *host_init(const char *);
262 
263 /* dict_mysql_quote - escape SQL metacharacters in input string */
264 
265 static void dict_mysql_quote(DICT *dict, const char *name, VSTRING *result)
266 {
267     DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict;
268     int len = strlen(name);
269     int buflen = 2*len + 1;
270 
271     /*
272      * We won't get integer overflows in 2*len + 1, because Postfix
273      * input keys have reasonable size limits, better safe than sorry.
274      */
275     if (buflen < len)
276 	msg_panic("dict_mysql_quote: integer overflow in 2*%d+1", len);
277     VSTRING_SPACE(result, buflen);
278 
279 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
280     if (dict_mysql->active_host)
281 	mysql_real_escape_string(dict_mysql->active_host->db,
282 				 vstring_end(result), name, len);
283     else
284 #endif
285 	mysql_escape_string(vstring_end(result), name, len);
286 
287     VSTRING_SKIP(result);
288 }
289 
290 /* dict_mysql_lookup - find database entry */
291 
292 static const char *dict_mysql_lookup(DICT *dict, const char *name)
293 {
294     const char *myname = "dict_mysql_lookup";
295     DICT_MYSQL *dict_mysql = (DICT_MYSQL *)dict;
296     PLMYSQL *pldb = dict_mysql->pldb;
297     MYSQL_RES *query_res;
298     MYSQL_ROW row;
299     static VSTRING *result;
300     static VSTRING *query;
301     int     i;
302     int     j;
303     int     numrows;
304     int     expansion;
305     const char *r;
306     db_quote_callback_t quote_func = dict_mysql_quote;
307 
308     dict_errno = 0;
309 
310     /*
311      * Optionally fold the key.
312      */
313     if (dict->flags & DICT_FLAG_FOLD_FIX) {
314 	if (dict->fold_buf == 0)
315 	    dict->fold_buf = vstring_alloc(10);
316 	vstring_strcpy(dict->fold_buf, name);
317 	name = lowercase(vstring_str(dict->fold_buf));
318     }
319 
320     /*
321      * If there is a domain list for this map, then only search for
322      * addresses in domains on the list. This can significantly reduce
323      * the load on the server.
324      */
325     if (db_common_check_domain(dict_mysql->ctx, name) == 0) {
326         if (msg_verbose)
327 	    msg_info("%s: Skipping lookup of '%s'", myname, name);
328         return (0);
329     }
330 
331 #define INIT_VSTR(buf, len) do { \
332 	if (buf == 0) \
333 	    buf = vstring_alloc(len); \
334 	VSTRING_RESET(buf); \
335 	VSTRING_TERMINATE(buf); \
336     } while (0)
337 
338     INIT_VSTR(query, 10);
339 
340     /*
341      * Suppress the lookup if the query expansion is empty
342      *
343      * This initial expansion is outside the context of any
344      * specific host connection, we just want to check the
345      * key pre-requisites, so when quoting happens separately
346      * for each connection, we don't bother with quoting...
347      */
348 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
349     quote_func = 0;
350 #endif
351     if (!db_common_expand(dict_mysql->ctx, dict_mysql->query,
352     			  name, 0, query, quote_func))
353         return (0);
354 
355     /* do the query - set dict_errno & cleanup if there's an error */
356     if ((query_res = plmysql_query(dict_mysql, name, query,
357 				   dict_mysql->dbname,
358 				   dict_mysql->username,
359 				   dict_mysql->password)) == 0) {
360 	dict_errno = DICT_ERR_RETRY;
361 	return (0);
362     }
363 
364     numrows = mysql_num_rows(query_res);
365     if (msg_verbose)
366 	msg_info("%s: retrieved %d rows", myname, numrows);
367     if (numrows == 0) {
368 	mysql_free_result(query_res);
369 	return 0;
370     }
371 
372     INIT_VSTR(result, 10);
373 
374     for (expansion = i = 0; i < numrows && dict_errno == 0; i++) {
375 	row = mysql_fetch_row(query_res);
376 	for (j = 0; j < mysql_num_fields(query_res); j++) {
377 	    if (db_common_expand(dict_mysql->ctx, dict_mysql->result_format,
378 	    			 row[j], name, result, 0)
379 		&& dict_mysql->expansion_limit > 0
380 		&& ++expansion > dict_mysql->expansion_limit) {
381 		msg_warn("%s: %s: Expansion limit exceeded for key: '%s'",
382 			 myname, dict_mysql->parser->name, name);
383 		dict_errno = DICT_ERR_RETRY;
384 		break;
385 	    }
386 	}
387     }
388     mysql_free_result(query_res);
389     r = vstring_str(result);
390     return ((dict_errno == 0 && *r) ? r : 0);
391 }
392 
393 /* dict_mysql_check_stat - check the status of a host */
394 
395 static int dict_mysql_check_stat(HOST *host, unsigned stat, unsigned type,
396 				 time_t t)
397 {
398     if ((host->stat & stat) && (!type || host->type & type)) {
399 	/* try not to hammer the dead hosts too often */
400 	if (host->stat == STATFAIL && host->ts > 0 && host->ts >= t)
401 	    return 0;
402 	return 1;
403     }
404     return 0;
405 }
406 
407 /* dict_mysql_find_host - find a host with the given status */
408 
409 static HOST *dict_mysql_find_host(PLMYSQL *PLDB, unsigned stat, unsigned type)
410 {
411     time_t  t;
412     int     count = 0;
413     int     idx;
414     int     i;
415 
416     t = time((time_t *) 0);
417     for (i = 0; i < PLDB->len_hosts; i++) {
418 	if (dict_mysql_check_stat(PLDB->db_hosts[i], stat, type, t))
419 	    count++;
420     }
421 
422     if (count) {
423 	idx = (count > 1) ?
424 	    1 + count * (double) myrand() / (1.0 + RAND_MAX) : 1;
425 
426 	for (i = 0; i < PLDB->len_hosts; i++) {
427 	    if (dict_mysql_check_stat(PLDB->db_hosts[i], stat, type, t) &&
428 		--idx == 0)
429 		return PLDB->db_hosts[i];
430 	}
431     }
432     return 0;
433 }
434 
435 /* dict_mysql_get_active - get an active connection */
436 
437 static HOST *dict_mysql_get_active(PLMYSQL *PLDB, char *dbname,
438 				   char *username, char *password)
439 {
440     const char *myname = "dict_mysql_get_active";
441     HOST   *host;
442     int     count = RETRY_CONN_MAX;
443 
444     /* Try the active connections first; prefer the ones to UNIX sockets. */
445     if ((host = dict_mysql_find_host(PLDB, STATACTIVE, TYPEUNIX)) != NULL ||
446 	(host = dict_mysql_find_host(PLDB, STATACTIVE, TYPEINET)) != NULL) {
447 	if (msg_verbose)
448 	    msg_info("%s: found active connection to host %s", myname,
449 		     host->hostname);
450 	return host;
451     }
452 
453     /*
454      * Try the remaining hosts.
455      * "count" is a safety net, in case the loop takes more than
456      * RETRY_CONN_INTV and the dead hosts are no longer skipped.
457      */
458     while (--count > 0 &&
459 	   ((host = dict_mysql_find_host(PLDB, STATUNTRIED | STATFAIL,
460 					 TYPEUNIX)) != NULL ||
461 	   (host = dict_mysql_find_host(PLDB, STATUNTRIED | STATFAIL,
462 					TYPEINET)) != NULL)) {
463 	if (msg_verbose)
464 	    msg_info("%s: attempting to connect to host %s", myname,
465 		     host->hostname);
466 	plmysql_connect_single(host, dbname, username, password);
467 	if (host->stat == STATACTIVE)
468 	    return host;
469     }
470 
471     /* bad news... */
472     return 0;
473 }
474 
475 /* dict_mysql_event - callback: close idle connections */
476 
477 static void dict_mysql_event(int unused_event, char *context)
478 {
479     HOST   *host = (HOST *) context;
480 
481     if (host->db)
482 	plmysql_close_host(host);
483 }
484 
485 /*
486  * plmysql_query - process a MySQL query.  Return MYSQL_RES* on success.
487  *			On failure, log failure and try other db instances.
488  *			on failure of all db instances, return 0;
489  *			close unnecessary active connections
490  */
491 
492 static MYSQL_RES *plmysql_query(DICT_MYSQL *dict_mysql,
493 				        const char *name,
494 					VSTRING *query,
495 				        char *dbname,
496 				        char *username,
497 				        char *password)
498 {
499     PLMYSQL *PLDB = dict_mysql->pldb;
500     HOST   *host;
501     MYSQL_RES *res = 0;
502 
503     while ((host = dict_mysql_get_active(PLDB, dbname, username, password)) != NULL) {
504 
505 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
506 	/*
507 	 * The active host is used to escape strings in the
508 	 * context of the active connection's character encoding.
509 	 */
510 	dict_mysql->active_host = host;
511 	VSTRING_RESET(query);
512 	VSTRING_TERMINATE(query);
513 	db_common_expand(dict_mysql->ctx, dict_mysql->query,
514 			 name, 0, query, dict_mysql_quote);
515 	dict_mysql->active_host = 0;
516 #endif
517 
518 	if (!(mysql_query(host->db, vstring_str(query)))) {
519 	    if ((res = mysql_store_result(host->db)) == 0) {
520 		msg_warn("mysql query failed: %s", mysql_error(host->db));
521 		plmysql_down_host(host);
522 	    } else {
523 		if (msg_verbose)
524 		    msg_info("dict_mysql: successful query from host %s", host->hostname);
525 		event_request_timer(dict_mysql_event, (char *) host, IDLE_CONN_INTV);
526 		break;
527 	    }
528 	} else {
529 	    msg_warn("mysql query failed: %s", mysql_error(host->db));
530 	    plmysql_down_host(host);
531 	}
532     }
533 
534     return res;
535 }
536 
537 /*
538  * plmysql_connect_single -
539  * used to reconnect to a single database when one is down or none is
540  * connected yet. Log all errors and set the stat field of host accordingly
541  */
542 static void plmysql_connect_single(HOST *host, char *dbname, char *username, char *password)
543 {
544     if ((host->db = mysql_init(NULL)) == NULL)
545 	msg_fatal("dict_mysql: insufficient memory");
546     if (mysql_real_connect(host->db,
547 			   (host->type == TYPEINET ? host->name : 0),
548 			   username,
549 			   password,
550 			   dbname,
551 			   host->port,
552 			   (host->type == TYPEUNIX ? host->name : 0),
553 			   0)) {
554 	if (msg_verbose)
555 	    msg_info("dict_mysql: successful connection to host %s",
556 		     host->hostname);
557 	host->stat = STATACTIVE;
558     } else {
559 	msg_warn("connect to mysql server %s: %s",
560 		 host->hostname, mysql_error(host->db));
561 	plmysql_down_host(host);
562     }
563 }
564 
565 /* plmysql_close_host - close an established MySQL connection */
566 static void plmysql_close_host(HOST *host)
567 {
568     mysql_close(host->db);
569     host->db = 0;
570     host->stat = STATUNTRIED;
571 }
572 
573 /*
574  * plmysql_down_host - close a failed connection AND set a "stay away from
575  * this host" timer
576  */
577 static void plmysql_down_host(HOST *host)
578 {
579     mysql_close(host->db);
580     host->db = 0;
581     host->ts = time((time_t *) 0) + RETRY_CONN_INTV;
582     host->stat = STATFAIL;
583     event_cancel_timer(dict_mysql_event, (char *) host);
584 }
585 
586 /* mysql_parse_config - parse mysql configuration file */
587 
588 static void mysql_parse_config(DICT_MYSQL *dict_mysql, const char *mysqlcf)
589 {
590     const char *myname = "mysqlname_parse";
591     CFG_PARSER *p;
592     VSTRING *buf;
593     int     i;
594     char   *hosts;
595 
596     p = dict_mysql->parser = cfg_parser_alloc(mysqlcf);
597     dict_mysql->username = cfg_get_str(p, "user", "", 0, 0);
598     dict_mysql->password = cfg_get_str(p, "password", "", 0, 0);
599     dict_mysql->dbname = cfg_get_str(p, "dbname", "", 1, 0);
600     dict_mysql->result_format = cfg_get_str(p, "result_format", "%s", 1, 0);
601     /*
602      * XXX: The default should be non-zero for safety, but that is not
603      * backwards compatible.
604      */
605     dict_mysql->expansion_limit = cfg_get_int(dict_mysql->parser,
606 					      "expansion_limit", 0, 0, 0);
607 
608     if ((dict_mysql->query = cfg_get_str(p, "query", NULL, 0, 0)) == 0) {
609         /*
610          * No query specified -- fallback to building it from components
611          * (old style "select %s from %s where %s")
612          */
613 	buf = vstring_alloc(64);
614 	db_common_sql_build_query(buf, p);
615 	dict_mysql->query = vstring_export(buf);
616     }
617 
618     /*
619      * Must parse all templates before we can use db_common_expand()
620      */
621     dict_mysql->ctx = 0;
622     (void) db_common_parse(&dict_mysql->dict, &dict_mysql->ctx,
623 			   dict_mysql->query, 1);
624     (void) db_common_parse(0, &dict_mysql->ctx, dict_mysql->result_format, 0);
625     db_common_parse_domain(p, dict_mysql->ctx);
626 
627     /*
628      * Maps that use substring keys should only be used with the full
629      * input key.
630      */
631     if (db_common_dict_partial(dict_mysql->ctx))
632 	dict_mysql->dict.flags |= DICT_FLAG_PATTERN;
633     else
634 	dict_mysql->dict.flags |= DICT_FLAG_FIXED;
635     if (dict_mysql->dict.flags & DICT_FLAG_FOLD_FIX)
636 	dict_mysql->dict.fold_buf = vstring_alloc(10);
637 
638     hosts = cfg_get_str(p, "hosts", "", 0, 0);
639 
640     dict_mysql->hosts = argv_split(hosts, " ,\t\r\n");
641     if (dict_mysql->hosts->argc == 0) {
642 	argv_add(dict_mysql->hosts, "localhost", ARGV_END);
643 	argv_terminate(dict_mysql->hosts);
644 	if (msg_verbose)
645 	    msg_info("%s: %s: no hostnames specified, defaulting to '%s'",
646 		     myname, mysqlcf, dict_mysql->hosts->argv[0]);
647     }
648     myfree(hosts);
649 }
650 
651 /* dict_mysql_open - open MYSQL data base */
652 
653 DICT   *dict_mysql_open(const char *name, int open_flags, int dict_flags)
654 {
655     DICT_MYSQL *dict_mysql;
656 
657     /*
658      * Sanity checks.
659      */
660     if (open_flags != O_RDONLY)
661 	msg_fatal("%s:%s map requires O_RDONLY access mode",
662 		  DICT_TYPE_MYSQL, name);
663 
664     dict_mysql = (DICT_MYSQL *) dict_alloc(DICT_TYPE_MYSQL, name,
665 					   sizeof(DICT_MYSQL));
666     dict_mysql->dict.lookup = dict_mysql_lookup;
667     dict_mysql->dict.close = dict_mysql_close;
668     dict_mysql->dict.flags = dict_flags;
669     mysql_parse_config(dict_mysql, name);
670 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
671     dict_mysql->active_host = 0;
672 #endif
673     dict_mysql->pldb = plmysql_init(dict_mysql->hosts);
674     if (dict_mysql->pldb == NULL)
675 	msg_fatal("couldn't intialize pldb!\n");
676     return (DICT_DEBUG (&dict_mysql->dict));
677 }
678 
679 /*
680  * plmysql_init - initalize a MYSQL database.
681  *		    Return NULL on failure, or a PLMYSQL * on success.
682  */
683 static PLMYSQL *plmysql_init(ARGV *hosts)
684 {
685     PLMYSQL *PLDB;
686     int     i;
687 
688     if ((PLDB = (PLMYSQL *) mymalloc(sizeof(PLMYSQL))) == 0)
689 	msg_fatal("mymalloc of pldb failed");
690 
691     PLDB->len_hosts = hosts->argc;
692     if ((PLDB->db_hosts = (HOST **) mymalloc(sizeof(HOST *) * hosts->argc)) == 0)
693 	return (0);
694     for (i = 0; i < hosts->argc; i++)
695 	PLDB->db_hosts[i] = host_init(hosts->argv[i]);
696 
697     return PLDB;
698 }
699 
700 
701 /* host_init - initialize HOST structure */
702 static HOST *host_init(const char *hostname)
703 {
704     const char *myname = "mysql host_init";
705     HOST   *host = (HOST *) mymalloc(sizeof(HOST));
706     const char *d = hostname;
707     char   *s;
708 
709     host->db = 0;
710     host->hostname = mystrdup(hostname);
711     host->port = 0;
712     host->stat = STATUNTRIED;
713     host->ts = 0;
714 
715     /*
716      * Ad-hoc parsing code. Expect "unix:pathname" or "inet:host:port", where
717      * both "inet:" and ":port" are optional.
718      */
719     if (strncmp(d, "unix:", 5) == 0) {
720 	d += 5;
721 	host->type = TYPEUNIX;
722     } else {
723 	if (strncmp(d, "inet:", 5) == 0)
724 	    d += 5;
725 	host->type = TYPEINET;
726     }
727     host->name = mystrdup(d);
728     if ((s = split_at_right(host->name, ':')) != 0)
729 	host->port = ntohs(find_inet_port(s, "tcp"));
730     if (strcasecmp(host->name, "localhost") == 0) {
731 	/* The MySQL way: this will actually connect over the UNIX socket */
732 	myfree(host->name);
733 	host->name = 0;
734 	host->type = TYPEUNIX;
735     }
736 
737     if (msg_verbose > 1)
738 	msg_info("%s: host=%s, port=%d, type=%s", myname,
739 		 host->name ? host->name : "localhost",
740 		 host->port, host->type == TYPEUNIX ? "unix" : "inet");
741     return host;
742 }
743 
744 /* dict_mysql_close - close MYSQL database */
745 
746 static void dict_mysql_close(DICT *dict)
747 {
748     int     i;
749     DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict;
750 
751     plmysql_dealloc(dict_mysql->pldb);
752     cfg_parser_free(dict_mysql->parser);
753     myfree(dict_mysql->username);
754     myfree(dict_mysql->password);
755     myfree(dict_mysql->dbname);
756     myfree(dict_mysql->query);
757     myfree(dict_mysql->result_format);
758     if (dict_mysql->hosts)
759     	argv_free(dict_mysql->hosts);
760     if (dict_mysql->ctx)
761 	db_common_free_ctx(dict_mysql->ctx);
762     if (dict->fold_buf)
763 	vstring_free(dict->fold_buf);
764     dict_free(dict);
765 }
766 
767 /* plmysql_dealloc - free memory associated with PLMYSQL close databases */
768 static void plmysql_dealloc(PLMYSQL *PLDB)
769 {
770     int     i;
771 
772     for (i = 0; i < PLDB->len_hosts; i++) {
773 	event_cancel_timer(dict_mysql_event, (char *) (PLDB->db_hosts[i]));
774 	if (PLDB->db_hosts[i]->db)
775 	    mysql_close(PLDB->db_hosts[i]->db);
776 	myfree(PLDB->db_hosts[i]->hostname);
777 	if (PLDB->db_hosts[i]->name)
778 	    myfree(PLDB->db_hosts[i]->name);
779 	myfree((char *) PLDB->db_hosts[i]);
780     }
781     myfree((char *) PLDB->db_hosts);
782     myfree((char *) (PLDB));
783 }
784 
785 #endif
786