1/*++
2/* NAME
3/*	dict_memcache 3
4/* SUMMARY
5/*	dictionary interface to memcaches
6/* SYNOPSIS
7/*	#include <dict_memcache.h>
8/*
9/*	DICT	*dict_memcache_open(name, open_flags, dict_flags)
10/*	const char *name;
11/*	int	open_flags;
12/*	int	dict_flags;
13/* DESCRIPTION
14/*	dict_memcache_open() opens a memcache, providing
15/*	a dictionary interface for Postfix key->value mappings.
16/*	The result is a pointer to the installed dictionary.
17/*
18/*	Configuration parameters are described in memcache_table(5).
19/*
20/*	Arguments:
21/* .IP name
22/*	The path to the Postfix memcache configuration file.
23/* .IP open_flags
24/*	O_RDONLY or O_RDWR. This function ignores flags that don't
25/*	specify a read, write or append mode.
26/* .IP dict_flags
27/*	See dict_open(3).
28/* SEE ALSO
29/*	dict(3) generic dictionary manager
30/* HISTORY
31/* .ad
32/* .fi
33/*	The first memcache client for Postfix was written by Omar
34/*	Kilani, and was based on libmemcache.  The current
35/*	implementation implements the memcache protocol directly,
36/*	and bears no resemblance to earlier work.
37/* AUTHOR(S)
38/*	Wietse Venema
39/*	IBM T.J. Watson Research
40/*	P.O. Box 704
41/*	Yorktown Heights, NY 10598, USA
42/*--*/
43
44/* System library. */
45
46#include <sys_defs.h>
47#include <errno.h>
48#include <string.h>
49#include <ctype.h>
50#include <stdio.h>			/* XXX sscanf() */
51
52/* Utility library. */
53
54#include <msg.h>
55#include <mymalloc.h>
56#include <dict.h>
57#include <vstring.h>
58#include <stringops.h>
59#include <auto_clnt.h>
60#include <vstream.h>
61
62/* Global library. */
63
64#include <cfg_parser.h>
65#include <db_common.h>
66#include <memcache_proto.h>
67
68/* Application-specific. */
69
70#include <dict_memcache.h>
71
72 /*
73  * Structure of one memcache dictionary handle.
74  */
75typedef struct {
76    DICT    dict;			/* parent class */
77    CFG_PARSER *parser;			/* common parameter parser */
78    void   *dbc_ctxt;			/* db_common context */
79    char   *key_format;			/* query key translation */
80    int     timeout;			/* client timeout */
81    int     mc_ttl;			/* memcache update expiration */
82    int     mc_flags;			/* memcache update flags */
83    int     err_pause;			/* delay between errors */
84    int     max_tries;			/* number of tries */
85    int     max_line;			/* reply line limit */
86    int     max_data;			/* reply data limit */
87    char   *memcache;			/* memcache server spec */
88    AUTO_CLNT *clnt;			/* memcache client stream */
89    VSTRING *clnt_buf;			/* memcache client buffer */
90    VSTRING *key_buf;			/* lookup key */
91    VSTRING *res_buf;			/* lookup result */
92    int     error;			/* memcache dict_errno */
93    DICT   *backup;			/* persistent backup */
94} DICT_MC;
95
96 /*
97  * Memcache option defaults and names.
98  */
99#define DICT_MC_DEF_HOST	"localhost"
100#define DICT_MC_DEF_PORT	"11211"
101#define DICT_MC_DEF_MEMCACHE	"inet:" DICT_MC_DEF_HOST ":" DICT_MC_DEF_PORT
102#define DICT_MC_DEF_KEY_FMT	"%s"
103#define DICT_MC_DEF_MC_TTL	3600
104#define DICT_MC_DEF_MC_TIMEOUT	2
105#define DICT_MC_DEF_MC_FLAGS	0
106#define DICT_MC_DEF_MAX_TRY	2
107#define DICT_MC_DEF_MAX_LINE	1024
108#define DICT_MC_DEF_MAX_DATA	10240
109#define DICT_MC_DEF_ERR_PAUSE	1
110
111#define DICT_MC_NAME_MEMCACHE	"memcache"
112#define DICT_MC_NAME_BACKUP	"backup"
113#define DICT_MC_NAME_KEY_FMT	"key_format"
114#define DICT_MC_NAME_MC_TTL	"ttl"
115#define DICT_MC_NAME_MC_TIMEOUT	"timeout"
116#define DICT_MC_NAME_MC_FLAGS	"flags"
117#define DICT_MC_NAME_MAX_TRY	"max_try"
118#define DICT_MC_NAME_MAX_LINE	"line_size_limit"
119#define DICT_MC_NAME_MAX_DATA	"data_size_limit"
120#define DICT_MC_NAME_ERR_PAUSE	"retry_pause"
121
122 /*
123  * SLMs.
124  */
125#define STR(x)	vstring_str(x)
126#define LEN(x)	VSTRING_LEN(x)
127
128/*#define msg_verbose 1*/
129
130/* dict_memcache_set - set memcache key/value */
131
132static int dict_memcache_set(DICT_MC *dict_mc, const char *value, int ttl)
133{
134    VSTREAM *fp;
135    int     count;
136    int     data_len = strlen(value);
137
138    /*
139     * Return a permanent error if we can't store this data. This results in
140     * loss of information.
141     */
142    if (data_len > dict_mc->max_data) {
143	msg_warn("database %s:%s: data for key %s is too long (%s=%d) "
144		 "-- not stored", DICT_TYPE_MEMCACHE, dict_mc->dict.name,
145		 STR(dict_mc->key_buf), DICT_MC_NAME_MAX_DATA,
146		 dict_mc->max_data);
147	/* Not stored! */
148	DICT_ERR_VAL_RETURN(dict_mc, DICT_ERR_NONE, DICT_STAT_FAIL);
149    }
150    for (count = 0; count < dict_mc->max_tries; count++) {
151	if (count > 0)
152	    sleep(dict_mc->err_pause);
153	if ((fp = auto_clnt_access(dict_mc->clnt)) == 0) {
154	    break;
155	} else if (memcache_printf(fp, "set %s %d %d %ld",
156		STR(dict_mc->key_buf), dict_mc->mc_flags, ttl, data_len) < 0
157		   || memcache_fwrite(fp, value, strlen(value)) < 0
158		   || memcache_get(fp, dict_mc->clnt_buf,
159				   dict_mc->max_line) < 0) {
160	    if (count > 0)
161		msg_warn(errno ? "database %s:%s: I/O error: %m" :
162			 "database %s:%s: I/O error",
163			 DICT_TYPE_MEMCACHE, dict_mc->dict.name);
164	} else if (strcmp(STR(dict_mc->clnt_buf), "STORED") != 0) {
165	    if (count > 0)
166		msg_warn("database %s:%s: update failed: %.30s",
167			 DICT_TYPE_MEMCACHE, dict_mc->dict.name,
168			 STR(dict_mc->clnt_buf));
169	} else {
170	    /* Victory! */
171	    DICT_ERR_VAL_RETURN(dict_mc, DICT_ERR_NONE, DICT_STAT_SUCCESS);
172	}
173	auto_clnt_recover(dict_mc->clnt);
174    }
175    DICT_ERR_VAL_RETURN(dict_mc, DICT_ERR_RETRY, DICT_STAT_ERROR);
176}
177
178/* dict_memcache_get - get memcache key/value */
179
180static const char *dict_memcache_get(DICT_MC *dict_mc)
181{
182    VSTREAM *fp;
183    long    todo;
184    int     count;
185
186    for (count = 0; count < dict_mc->max_tries; count++) {
187	if (count > 0)
188	    sleep(dict_mc->err_pause);
189	if ((fp = auto_clnt_access(dict_mc->clnt)) == 0) {
190	    break;
191	} else if (memcache_printf(fp, "get %s", STR(dict_mc->key_buf)) < 0
192	    || memcache_get(fp, dict_mc->clnt_buf, dict_mc->max_line) < 0) {
193	    if (count > 0)
194		msg_warn(errno ? "database %s:%s: I/O error: %m" :
195			 "database %s:%s: I/O error",
196			 DICT_TYPE_MEMCACHE, dict_mc->dict.name);
197	} else if (strcmp(STR(dict_mc->clnt_buf), "END") == 0) {
198	    /* Not found. */
199	    DICT_ERR_VAL_RETURN(dict_mc, DICT_ERR_NONE, (char *) 0);
200	} else if (sscanf(STR(dict_mc->clnt_buf),
201			  "VALUE %*s %*s %ld", &todo) != 1
202		   || todo < 0 || todo > dict_mc->max_data) {
203	    if (count > 0)
204		msg_warn("%s: unexpected memcache server reply: %.30s",
205			 dict_mc->dict.name, STR(dict_mc->clnt_buf));
206	} else if (memcache_fread(fp, dict_mc->res_buf, todo) < 0) {
207	    if (count > 0)
208		msg_warn("%s: EOF receiving memcache server reply",
209			 dict_mc->dict.name);
210	} else {
211	    /* Victory! */
212	    if (memcache_get(fp, dict_mc->clnt_buf, dict_mc->max_line) < 0
213		|| strcmp(STR(dict_mc->clnt_buf), "END") != 0)
214		auto_clnt_recover(dict_mc->clnt);
215	    DICT_ERR_VAL_RETURN(dict_mc, DICT_ERR_NONE, STR(dict_mc->res_buf));
216	}
217	auto_clnt_recover(dict_mc->clnt);
218    }
219    DICT_ERR_VAL_RETURN(dict_mc, DICT_ERR_RETRY, (char *) 0);
220}
221
222/* dict_memcache_del - delete memcache key/value */
223
224static int dict_memcache_del(DICT_MC *dict_mc)
225{
226    VSTREAM *fp;
227    int     count;
228
229    for (count = 0; count < dict_mc->max_tries; count++) {
230	if (count > 0)
231	    sleep(dict_mc->err_pause);
232	if ((fp = auto_clnt_access(dict_mc->clnt)) == 0) {
233	    break;
234	} else if (memcache_printf(fp, "delete %s", STR(dict_mc->key_buf)) < 0
235	    || memcache_get(fp, dict_mc->clnt_buf, dict_mc->max_line) < 0) {
236	    if (count > 0)
237		msg_warn(errno ? "database %s:%s: I/O error: %m" :
238			 "database %s:%s: I/O error",
239			 DICT_TYPE_MEMCACHE, dict_mc->dict.name);
240	} else if (strcmp(STR(dict_mc->clnt_buf), "DELETED") == 0) {
241	    /* Victory! */
242	    DICT_ERR_VAL_RETURN(dict_mc, DICT_ERR_NONE, DICT_STAT_SUCCESS);
243	} else if (strcmp(STR(dict_mc->clnt_buf), "NOT_FOUND") == 0) {
244	    /* Not found! */
245	    DICT_ERR_VAL_RETURN(dict_mc, DICT_ERR_NONE, DICT_STAT_FAIL);
246	} else {
247	    if (count > 0)
248		msg_warn("database %s:%s: delete failed: %.30s",
249			 DICT_TYPE_MEMCACHE, dict_mc->dict.name,
250			 STR(dict_mc->clnt_buf));
251	}
252	auto_clnt_recover(dict_mc->clnt);
253    }
254    DICT_ERR_VAL_RETURN(dict_mc, DICT_ERR_RETRY, DICT_STAT_ERROR);
255}
256
257/* dict_memcache_prepare_key - prepare lookup key */
258
259static int dict_memcache_prepare_key(DICT_MC *dict_mc, const char *name)
260{
261
262    /*
263     * Optionally case-fold the search string.
264     */
265    if (dict_mc->dict.flags & DICT_FLAG_FOLD_FIX) {
266	if (dict_mc->dict.fold_buf == 0)
267	    dict_mc->dict.fold_buf = vstring_alloc(10);
268	vstring_strcpy(dict_mc->dict.fold_buf, name);
269	name = lowercase(STR(dict_mc->dict.fold_buf));
270    }
271
272    /*
273     * Optionally expand the query key format.
274     */
275#define DICT_MC_NO_KEY		(0)
276#define DICT_MC_NO_QUOTING	((void (*)(DICT *, const char *, VSTRING *)) 0)
277
278    if (dict_mc->key_format != 0
279	&& strcmp(dict_mc->key_format, DICT_MC_DEF_KEY_FMT) != 0) {
280	VSTRING_RESET(dict_mc->key_buf);
281	if (db_common_expand(dict_mc->dbc_ctxt, dict_mc->key_format,
282			     name, DICT_MC_NO_KEY, dict_mc->key_buf,
283			     DICT_MC_NO_QUOTING) == 0)
284	    return (0);
285    } else {
286	vstring_strcpy(dict_mc->key_buf, name);
287    }
288
289    /*
290     * The length indicates whether the expansion is empty or not.
291     */
292    return (LEN(dict_mc->key_buf));
293}
294
295/* dict_memcache_valid_key - validate key */
296
297static int dict_memcache_valid_key(DICT_MC *dict_mc,
298				           const char *name,
299				           const char *operation,
300				        void (*log_func) (const char *,...))
301{
302    unsigned char *cp;
303    int     rc;
304
305#define DICT_MC_SKIP(why) do { \
306	if (msg_verbose || log_func != msg_info) \
307	    log_func("%s: skipping %s for name \"%s\": %s", \
308		     dict_mc->dict.name, operation, name, (why)); \
309	DICT_ERR_VAL_RETURN(dict_mc, DICT_ERR_NONE, 0); \
310    } while (0)
311
312    if (*name == 0)
313	DICT_MC_SKIP("empty lookup key");
314    if ((rc = db_common_check_domain(dict_mc->dbc_ctxt, name)) == 0)
315	DICT_MC_SKIP("domain mismatch");
316    if (rc < 0)
317	DICT_ERR_VAL_RETURN(dict_mc, rc, 0);
318    if (dict_memcache_prepare_key(dict_mc, name) == 0)
319	DICT_MC_SKIP("empty lookup key expansion");
320    for (cp = (unsigned char *) STR(dict_mc->key_buf); *cp; cp++)
321	if (isascii(*cp) && isspace(*cp))
322	    DICT_MC_SKIP("name contains space");
323
324    DICT_ERR_VAL_RETURN(dict_mc, DICT_ERR_NONE, 1);
325}
326
327/* dict_memcache_update - update memcache */
328
329static int dict_memcache_update(DICT *dict, const char *name,
330				        const char *value)
331{
332    const char *myname = "dict_memcache_update";
333    DICT_MC *dict_mc = (DICT_MC *) dict;
334    DICT   *backup = dict_mc->backup;
335    int     upd_res;
336
337    /*
338     * Skip updates with an inapplicable key, noisily. This results in loss
339     * of information.
340     */
341    if (dict_memcache_valid_key(dict_mc, name, "update", msg_warn) == 0)
342	DICT_ERR_VAL_RETURN(dict, dict_mc->error, DICT_STAT_FAIL);
343
344    /*
345     * Update the memcache first.
346     */
347    upd_res = dict_memcache_set(dict_mc, value, dict_mc->mc_ttl);
348    dict->error = dict_mc->error;
349
350    /*
351     * Update the backup database last.
352     */
353    if (backup) {
354	upd_res = backup->update(backup, name, value);
355	dict->error = backup->error;
356    }
357    if (msg_verbose)
358	msg_info("%s: %s: update key \"%s\"(%s) => \"%s\" %s",
359		 myname, dict_mc->dict.name, name, STR(dict_mc->key_buf),
360		 value, dict_mc->error ? "(memcache error)" : (backup
361		       && backup->error) ? "(backup error)" : "(no error)");
362
363    return (upd_res);
364}
365
366/* dict_memcache_lookup - lookup memcache */
367
368static const char *dict_memcache_lookup(DICT *dict, const char *name)
369{
370    const char *myname = "dict_memcache_lookup";
371    DICT_MC *dict_mc = (DICT_MC *) dict;
372    DICT   *backup = dict_mc->backup;
373    const char *retval;
374
375    /*
376     * Skip lookups with an inapplicable key, silently. This is just asking
377     * for information that cannot exist.
378     */
379    if (dict_memcache_valid_key(dict_mc, name, "lookup", msg_info) == 0)
380	DICT_ERR_VAL_RETURN(dict, dict_mc->error, (char *) 0);
381
382    /*
383     * Search the memcache first.
384     */
385    retval = dict_memcache_get(dict_mc);
386    dict->error = dict_mc->error;
387
388    /*
389     * Search the backup database last. Update the memcache if the data is
390     * found.
391     */
392    if (backup) {
393	backup->error = 0;
394	if (retval == 0) {
395	    retval = backup->lookup(backup, name);
396	    dict->error = backup->error;
397	    /* Update the cache. */
398	    if (retval != 0)
399		dict_memcache_set(dict_mc, retval, dict_mc->mc_ttl);
400	}
401    }
402    if (msg_verbose)
403	msg_info("%s: %s: key \"%s\"(%s) => %s",
404		 myname, dict_mc->dict.name, name, STR(dict_mc->key_buf),
405		 retval ? retval : dict_mc->error ? "(memcache error)" :
406	      (backup && backup->error) ? "(backup error)" : "(not found)");
407
408    return (retval);
409}
410
411/* dict_memcache_delete - delete memcache entry */
412
413static int dict_memcache_delete(DICT *dict, const char *name)
414{
415    const char *myname = "dict_memcache_delete";
416    DICT_MC *dict_mc = (DICT_MC *) dict;
417    DICT   *backup = dict_mc->backup;
418    int     del_res;
419
420    /*
421     * Skip lookups with an inapplicable key, noisily. This is just deleting
422     * information that cannot exist.
423     */
424    if (dict_memcache_valid_key(dict_mc, name, "delete", msg_info) == 0)
425	DICT_ERR_VAL_RETURN(dict, dict_mc->error, dict_mc->error ?
426			    DICT_STAT_ERROR : DICT_STAT_FAIL);
427
428    /*
429     * Update the memcache first.
430     */
431    del_res = dict_memcache_del(dict_mc);
432    dict->error = dict_mc->error;
433
434    /*
435     * Update the persistent database last.
436     */
437    if (backup) {
438	del_res = backup->delete(backup, name);
439	dict->error = backup->error;
440    }
441    if (msg_verbose)
442	msg_info("%s: %s: delete key \"%s\"(%s) => %s",
443		 myname, dict_mc->dict.name, name, STR(dict_mc->key_buf),
444		 dict_mc->error ? "(memcache error)" : (backup
445		       && backup->error) ? "(backup error)" : "(no error)");
446
447    return (del_res);
448}
449
450/* dict_memcache_sequence - first/next lookup */
451
452static int dict_memcache_sequence(DICT *dict, int function, const char **key,
453				          const char **value)
454{
455    const char *myname = "dict_memcache_sequence";
456    DICT_MC *dict_mc = (DICT_MC *) dict;
457    DICT   *backup = dict_mc->backup;
458    int     seq_res;
459
460    if (backup == 0) {
461	msg_warn("database %s:%s: first/next support requires backup database",
462		 DICT_TYPE_MEMCACHE, dict_mc->dict.name);
463	DICT_ERR_VAL_RETURN(dict, DICT_ERR_NONE, DICT_STAT_FAIL);
464    } else {
465	seq_res = backup->sequence(backup, function, key, value);
466	if (msg_verbose)
467	    msg_info("%s: %s: key \"%s\" => %s",
468		     myname, dict_mc->dict.name, *key ? *key : "(not found)",
469		     *value ? *value : backup->error ? "(backup error)" :
470		     "(not found)");
471	DICT_ERR_VAL_RETURN(dict, backup->error, seq_res);
472    }
473}
474
475/* dict_memcache_close - close memcache */
476
477static void dict_memcache_close(DICT *dict)
478{
479    DICT_MC *dict_mc = (DICT_MC *) dict;
480
481    cfg_parser_free(dict_mc->parser);
482    db_common_free_ctx(dict_mc->dbc_ctxt);
483    if (dict_mc->key_format)
484	myfree(dict_mc->key_format);
485    myfree(dict_mc->memcache);
486    auto_clnt_free(dict_mc->clnt);
487    vstring_free(dict_mc->clnt_buf);
488    vstring_free(dict_mc->key_buf);
489    vstring_free(dict_mc->res_buf);
490    if (dict->fold_buf)
491	vstring_free(dict->fold_buf);
492    if (dict_mc->backup)
493	dict_close(dict_mc->backup);
494    dict_free(dict);
495}
496
497/* dict_memcache_open - open memcache */
498
499DICT   *dict_memcache_open(const char *name, int open_flags, int dict_flags)
500{
501    DICT_MC *dict_mc;
502    char   *backup;
503    CFG_PARSER *parser;
504
505    /*
506     * Sanity checks.
507     */
508    if (dict_flags & DICT_FLAG_NO_UNAUTH)
509	return (dict_surrogate(DICT_TYPE_MEMCACHE, name, open_flags, dict_flags,
510		     "%s:%s map is not allowed for security-sensitive data",
511			       DICT_TYPE_MEMCACHE, name));
512    open_flags &= (O_RDONLY | O_RDWR | O_WRONLY | O_APPEND);
513    if (open_flags != O_RDONLY && open_flags != O_RDWR)
514	return (dict_surrogate(DICT_TYPE_MEMCACHE, name, open_flags, dict_flags,
515			"%s:%s map requires O_RDONLY or O_RDWR access mode",
516			       DICT_TYPE_MEMCACHE, name));
517
518    /*
519     * Open the configuration file.
520     */
521    if ((parser = cfg_parser_alloc(name)) == 0)
522	return (dict_surrogate(DICT_TYPE_MEMCACHE, name, open_flags, dict_flags,
523			       "open %s: %m", name));
524
525    /*
526     * Create the dictionary object.
527     */
528    dict_mc = (DICT_MC *) dict_alloc(DICT_TYPE_MEMCACHE, name,
529				     sizeof(*dict_mc));
530    dict_mc->dict.lookup = dict_memcache_lookup;
531    if (open_flags == O_RDWR) {
532	dict_mc->dict.update = dict_memcache_update;
533	dict_mc->dict.delete = dict_memcache_delete;
534    }
535    dict_mc->dict.sequence = dict_memcache_sequence;
536    dict_mc->dict.close = dict_memcache_close;
537    dict_mc->dict.flags = dict_flags;
538    dict_mc->key_buf = vstring_alloc(10);
539    dict_mc->res_buf = vstring_alloc(10);
540
541    /*
542     * Parse the configuration file.
543     */
544    dict_mc->parser = parser;
545    dict_mc->key_format = cfg_get_str(dict_mc->parser, DICT_MC_NAME_KEY_FMT,
546				      DICT_MC_DEF_KEY_FMT, 0, 0);
547    dict_mc->timeout = cfg_get_int(dict_mc->parser, DICT_MC_NAME_MC_TIMEOUT,
548				   DICT_MC_DEF_MC_TIMEOUT, 0, 0);
549    dict_mc->mc_ttl = cfg_get_int(dict_mc->parser, DICT_MC_NAME_MC_TTL,
550				  DICT_MC_DEF_MC_TTL, 0, 0);
551    dict_mc->mc_flags = cfg_get_int(dict_mc->parser, DICT_MC_NAME_MC_FLAGS,
552				    DICT_MC_DEF_MC_FLAGS, 0, 0);
553    dict_mc->err_pause = cfg_get_int(dict_mc->parser, DICT_MC_NAME_ERR_PAUSE,
554				     DICT_MC_DEF_ERR_PAUSE, 1, 0);
555    dict_mc->max_tries = cfg_get_int(dict_mc->parser, DICT_MC_NAME_MAX_TRY,
556				     DICT_MC_DEF_MAX_TRY, 1, 0);
557    dict_mc->max_line = cfg_get_int(dict_mc->parser, DICT_MC_NAME_MAX_LINE,
558				    DICT_MC_DEF_MAX_LINE, 1, 0);
559    dict_mc->max_data = cfg_get_int(dict_mc->parser, DICT_MC_NAME_MAX_DATA,
560				    DICT_MC_DEF_MAX_DATA, 1, 0);
561    dict_mc->memcache = cfg_get_str(dict_mc->parser, DICT_MC_NAME_MEMCACHE,
562				    DICT_MC_DEF_MEMCACHE, 0, 0);
563
564    /*
565     * Initialize the memcache client.
566     */
567    dict_mc->clnt = auto_clnt_create(dict_mc->memcache, dict_mc->timeout, 0, 0);
568    dict_mc->clnt_buf = vstring_alloc(100);
569
570    /*
571     * Open the optional backup database.
572     */
573    backup = cfg_get_str(dict_mc->parser, DICT_MC_NAME_BACKUP,
574			 (char *) 0, 0, 0);
575    if (backup) {
576	dict_mc->backup = dict_open(backup, open_flags, dict_flags);
577	myfree(backup);
578    } else
579	dict_mc->backup = 0;
580
581    /*
582     * Parse templates and common database parameters. Maps that use
583     * substring keys should only be used with the full input key.
584     */
585    dict_mc->dbc_ctxt = 0;
586    db_common_parse(&dict_mc->dict, &dict_mc->dbc_ctxt,
587		    dict_mc->key_format, 1);
588    db_common_parse_domain(dict_mc->parser, dict_mc->dbc_ctxt);
589    if (db_common_dict_partial(dict_mc->dbc_ctxt))
590	/* Breaks recipient delimiters */
591	dict_mc->dict.flags |= DICT_FLAG_PATTERN;
592    else
593	dict_mc->dict.flags |= DICT_FLAG_FIXED;
594
595    dict_mc->dict.flags |= DICT_FLAG_MULTI_WRITER;
596
597    return (&dict_mc->dict);
598}
599