xref: /netbsd-src/external/ibm-public/postfix/dist/src/global/dict_mysql.c (revision 1b9578b8c2c1f848eeb16dabbfd7d1f0d9fdefbd)
1 /*	$NetBSD: dict_mysql.c,v 1.1.1.2 2011/03/02 19:32:14 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_errno=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
269      * input 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     PLMYSQL *pldb = dict_mysql->pldb;
293     MYSQL_RES *query_res;
294     MYSQL_ROW row;
295     static VSTRING *result;
296     static VSTRING *query;
297     int     i;
298     int     j;
299     int     numrows;
300     int     expansion;
301     const char *r;
302     db_quote_callback_t quote_func = dict_mysql_quote;
303 
304     dict_errno = 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
318      * addresses in domains on the list. This can significantly reduce
319      * the load on the server.
320      */
321     if (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 
327 #define INIT_VSTR(buf, len) do { \
328 	if (buf == 0) \
329 	    buf = vstring_alloc(len); \
330 	VSTRING_RESET(buf); \
331 	VSTRING_TERMINATE(buf); \
332     } while (0)
333 
334     INIT_VSTR(query, 10);
335 
336     /*
337      * Suppress the lookup if the query expansion is empty
338      *
339      * This initial expansion is outside the context of any
340      * specific host connection, we just want to check the
341      * key pre-requisites, so when quoting happens separately
342      * for each connection, we don't bother with quoting...
343      */
344 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
345     quote_func = 0;
346 #endif
347     if (!db_common_expand(dict_mysql->ctx, dict_mysql->query,
348     			  name, 0, query, quote_func))
349         return (0);
350 
351     /* do the query - set dict_errno & cleanup if there's an error */
352     if ((query_res = plmysql_query(dict_mysql, name, query,
353 				   dict_mysql->dbname,
354 				   dict_mysql->username,
355 				   dict_mysql->password)) == 0) {
356 	dict_errno = DICT_ERR_RETRY;
357 	return (0);
358     }
359 
360     numrows = mysql_num_rows(query_res);
361     if (msg_verbose)
362 	msg_info("%s: retrieved %d rows", myname, numrows);
363     if (numrows == 0) {
364 	mysql_free_result(query_res);
365 	return 0;
366     }
367 
368     INIT_VSTR(result, 10);
369 
370     for (expansion = i = 0; i < numrows && dict_errno == 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_errno = DICT_ERR_RETRY;
380 		break;
381 	    }
382 	}
383     }
384     mysql_free_result(query_res);
385     r = vstring_str(result);
386     return ((dict_errno == 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.
451      * "count" is a safety net, in case the loop takes more than
452      * RETRY_CONN_INTV and the dead hosts are no longer 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 	 * The active host is used to escape strings in the
504 	 * context of the active connection's character encoding.
505 	 */
506 	dict_mysql->active_host = host;
507 	VSTRING_RESET(query);
508 	VSTRING_TERMINATE(query);
509 	db_common_expand(dict_mysql->ctx, dict_mysql->query,
510 			 name, 0, query, dict_mysql_quote);
511 	dict_mysql->active_host = 0;
512 #endif
513 
514 	if (!(mysql_query(host->db, vstring_str(query)))) {
515 	    if ((res = mysql_store_result(host->db)) == 0) {
516 		msg_warn("mysql query failed: %s", mysql_error(host->db));
517 		plmysql_down_host(host);
518 	    } else {
519 		if (msg_verbose)
520 		    msg_info("dict_mysql: successful query from host %s", host->hostname);
521 		event_request_timer(dict_mysql_event, (char *) host, IDLE_CONN_INTV);
522 		break;
523 	    }
524 	} else {
525 	    msg_warn("mysql query failed: %s", mysql_error(host->db));
526 	    plmysql_down_host(host);
527 	}
528     }
529 
530     return res;
531 }
532 
533 /*
534  * plmysql_connect_single -
535  * used to reconnect to a single database when one is down or none is
536  * connected yet. Log all errors and set the stat field of host accordingly
537  */
538 static void plmysql_connect_single(HOST *host, char *dbname, char *username, char *password)
539 {
540     if ((host->db = mysql_init(NULL)) == NULL)
541 	msg_fatal("dict_mysql: insufficient memory");
542     if (mysql_real_connect(host->db,
543 			   (host->type == TYPEINET ? host->name : 0),
544 			   username,
545 			   password,
546 			   dbname,
547 			   host->port,
548 			   (host->type == TYPEUNIX ? host->name : 0),
549 			   0)) {
550 	if (msg_verbose)
551 	    msg_info("dict_mysql: successful connection to host %s",
552 		     host->hostname);
553 	host->stat = STATACTIVE;
554     } else {
555 	msg_warn("connect to mysql server %s: %s",
556 		 host->hostname, mysql_error(host->db));
557 	plmysql_down_host(host);
558     }
559 }
560 
561 /* plmysql_close_host - close an established MySQL connection */
562 static void plmysql_close_host(HOST *host)
563 {
564     mysql_close(host->db);
565     host->db = 0;
566     host->stat = STATUNTRIED;
567 }
568 
569 /*
570  * plmysql_down_host - close a failed connection AND set a "stay away from
571  * this host" timer
572  */
573 static void plmysql_down_host(HOST *host)
574 {
575     mysql_close(host->db);
576     host->db = 0;
577     host->ts = time((time_t *) 0) + RETRY_CONN_INTV;
578     host->stat = STATFAIL;
579     event_cancel_timer(dict_mysql_event, (char *) host);
580 }
581 
582 /* mysql_parse_config - parse mysql configuration file */
583 
584 static void mysql_parse_config(DICT_MYSQL *dict_mysql, const char *mysqlcf)
585 {
586     const char *myname = "mysqlname_parse";
587     CFG_PARSER *p;
588     VSTRING *buf;
589     int     i;
590     char   *hosts;
591 
592     p = dict_mysql->parser = cfg_parser_alloc(mysqlcf);
593     dict_mysql->username = cfg_get_str(p, "user", "", 0, 0);
594     dict_mysql->password = cfg_get_str(p, "password", "", 0, 0);
595     dict_mysql->dbname = cfg_get_str(p, "dbname", "", 1, 0);
596     dict_mysql->result_format = cfg_get_str(p, "result_format", "%s", 1, 0);
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          * No query specified -- fallback to building it from components
607          * (old style "select %s from %s where %s")
608          */
609 	buf = vstring_alloc(64);
610 	db_common_sql_build_query(buf, p);
611 	dict_mysql->query = vstring_export(buf);
612     }
613 
614     /*
615      * Must parse all templates before we can use db_common_expand()
616      */
617     dict_mysql->ctx = 0;
618     (void) db_common_parse(&dict_mysql->dict, &dict_mysql->ctx,
619 			   dict_mysql->query, 1);
620     (void) db_common_parse(0, &dict_mysql->ctx, dict_mysql->result_format, 0);
621     db_common_parse_domain(p, dict_mysql->ctx);
622 
623     /*
624      * Maps that use substring keys should only be used with the full
625      * input key.
626      */
627     if (db_common_dict_partial(dict_mysql->ctx))
628 	dict_mysql->dict.flags |= DICT_FLAG_PATTERN;
629     else
630 	dict_mysql->dict.flags |= DICT_FLAG_FIXED;
631     if (dict_mysql->dict.flags & DICT_FLAG_FOLD_FIX)
632 	dict_mysql->dict.fold_buf = vstring_alloc(10);
633 
634     hosts = cfg_get_str(p, "hosts", "", 0, 0);
635 
636     dict_mysql->hosts = argv_split(hosts, " ,\t\r\n");
637     if (dict_mysql->hosts->argc == 0) {
638 	argv_add(dict_mysql->hosts, "localhost", ARGV_END);
639 	argv_terminate(dict_mysql->hosts);
640 	if (msg_verbose)
641 	    msg_info("%s: %s: no hostnames specified, defaulting to '%s'",
642 		     myname, mysqlcf, dict_mysql->hosts->argv[0]);
643     }
644     myfree(hosts);
645 }
646 
647 /* dict_mysql_open - open MYSQL data base */
648 
649 DICT   *dict_mysql_open(const char *name, int open_flags, int dict_flags)
650 {
651     DICT_MYSQL *dict_mysql;
652 
653     /*
654      * Sanity checks.
655      */
656     if (open_flags != O_RDONLY)
657 	msg_fatal("%s:%s map requires O_RDONLY access mode",
658 		  DICT_TYPE_MYSQL, name);
659 
660     dict_mysql = (DICT_MYSQL *) dict_alloc(DICT_TYPE_MYSQL, name,
661 					   sizeof(DICT_MYSQL));
662     dict_mysql->dict.lookup = dict_mysql_lookup;
663     dict_mysql->dict.close = dict_mysql_close;
664     dict_mysql->dict.flags = dict_flags;
665     mysql_parse_config(dict_mysql, name);
666 #if defined(MYSQL_VERSION_ID) && MYSQL_VERSION_ID >= 40000
667     dict_mysql->active_host = 0;
668 #endif
669     dict_mysql->pldb = plmysql_init(dict_mysql->hosts);
670     if (dict_mysql->pldb == NULL)
671 	msg_fatal("couldn't intialize pldb!\n");
672     return (DICT_DEBUG (&dict_mysql->dict));
673 }
674 
675 /*
676  * plmysql_init - initalize a MYSQL database.
677  *		    Return NULL on failure, or a PLMYSQL * on success.
678  */
679 static PLMYSQL *plmysql_init(ARGV *hosts)
680 {
681     PLMYSQL *PLDB;
682     int     i;
683 
684     if ((PLDB = (PLMYSQL *) mymalloc(sizeof(PLMYSQL))) == 0)
685 	msg_fatal("mymalloc of pldb failed");
686 
687     PLDB->len_hosts = hosts->argc;
688     if ((PLDB->db_hosts = (HOST **) mymalloc(sizeof(HOST *) * hosts->argc)) == 0)
689 	return (0);
690     for (i = 0; i < hosts->argc; i++)
691 	PLDB->db_hosts[i] = host_init(hosts->argv[i]);
692 
693     return PLDB;
694 }
695 
696 
697 /* host_init - initialize HOST structure */
698 static HOST *host_init(const char *hostname)
699 {
700     const char *myname = "mysql host_init";
701     HOST   *host = (HOST *) mymalloc(sizeof(HOST));
702     const char *d = hostname;
703     char   *s;
704 
705     host->db = 0;
706     host->hostname = mystrdup(hostname);
707     host->port = 0;
708     host->stat = STATUNTRIED;
709     host->ts = 0;
710 
711     /*
712      * Ad-hoc parsing code. Expect "unix:pathname" or "inet:host:port", where
713      * both "inet:" and ":port" are optional.
714      */
715     if (strncmp(d, "unix:", 5) == 0) {
716 	d += 5;
717 	host->type = TYPEUNIX;
718     } else {
719 	if (strncmp(d, "inet:", 5) == 0)
720 	    d += 5;
721 	host->type = TYPEINET;
722     }
723     host->name = mystrdup(d);
724     if ((s = split_at_right(host->name, ':')) != 0)
725 	host->port = ntohs(find_inet_port(s, "tcp"));
726     if (strcasecmp(host->name, "localhost") == 0) {
727 	/* The MySQL way: this will actually connect over the UNIX socket */
728 	myfree(host->name);
729 	host->name = 0;
730 	host->type = TYPEUNIX;
731     }
732 
733     if (msg_verbose > 1)
734 	msg_info("%s: host=%s, port=%d, type=%s", myname,
735 		 host->name ? host->name : "localhost",
736 		 host->port, host->type == TYPEUNIX ? "unix" : "inet");
737     return host;
738 }
739 
740 /* dict_mysql_close - close MYSQL database */
741 
742 static void dict_mysql_close(DICT *dict)
743 {
744     int     i;
745     DICT_MYSQL *dict_mysql = (DICT_MYSQL *) dict;
746 
747     plmysql_dealloc(dict_mysql->pldb);
748     cfg_parser_free(dict_mysql->parser);
749     myfree(dict_mysql->username);
750     myfree(dict_mysql->password);
751     myfree(dict_mysql->dbname);
752     myfree(dict_mysql->query);
753     myfree(dict_mysql->result_format);
754     if (dict_mysql->hosts)
755     	argv_free(dict_mysql->hosts);
756     if (dict_mysql->ctx)
757 	db_common_free_ctx(dict_mysql->ctx);
758     if (dict->fold_buf)
759 	vstring_free(dict->fold_buf);
760     dict_free(dict);
761 }
762 
763 /* plmysql_dealloc - free memory associated with PLMYSQL close databases */
764 static void plmysql_dealloc(PLMYSQL *PLDB)
765 {
766     int     i;
767 
768     for (i = 0; i < PLDB->len_hosts; i++) {
769 	event_cancel_timer(dict_mysql_event, (char *) (PLDB->db_hosts[i]));
770 	if (PLDB->db_hosts[i]->db)
771 	    mysql_close(PLDB->db_hosts[i]->db);
772 	myfree(PLDB->db_hosts[i]->hostname);
773 	if (PLDB->db_hosts[i]->name)
774 	    myfree(PLDB->db_hosts[i]->name);
775 	myfree((char *) PLDB->db_hosts[i]);
776     }
777     myfree((char *) PLDB->db_hosts);
778     myfree((char *) (PLDB));
779 }
780 
781 #endif
782