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