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