Home | History | Annotate | Line # | Download | only in postalias
      1 /*	$NetBSD: postalias.c,v 1.7 2026/05/09 18:49:18 christos Exp $	*/
      2 
      3 /*++
      4 /* NAME
      5 /*	postalias 1
      6 /* SUMMARY
      7 /*	Postfix alias database maintenance
      8 /* SYNOPSIS
      9 /* .fi
     10 /*	\fBpostalias\fR [\fB-Nfijnoprsuvw\fR] [\fB-c \fIconfig_dir\fR]
     11 /*	[\fB-d \fIkey\fR] [\fB-q \fIkey\fR]
     12 /*		[\fIfile_type\fR:]\fIfile_name\fR ...
     13 /* DESCRIPTION
     14 /*	The \fBpostalias\fR(1) command creates or queries one or more Postfix
     15 /*	alias databases, or updates an existing one. The input and output
     16 /*	file formats are expected to be compatible with Sendmail version 8,
     17 /*	and are expected to be suitable for use as NIS alias maps.
     18 /*
     19 /*	If the result files do not exist they will be created with the
     20 /*	same group and other read permissions as their source file.
     21 /*
     22 /*	While a database update is in progress, signal delivery is
     23 /*	postponed, and an exclusive, advisory, lock is placed on the
     24 /*	entire database, in order to avoid surprises in spectator
     25 /*	processes.
     26 /*
     27 /*	The format of Postfix alias input files is described in
     28 /*	\fBaliases\fR(5).
     29 /*
     30 /*	By default the lookup key is mapped to lowercase to make
     31 /*	the lookups case insensitive; as of Postfix 2.3 this case
     32 /*	folding happens only with tables whose lookup keys are
     33 /*	fixed-case strings such as btree:, dbm: or hash:. With
     34 /*	earlier versions, the lookup key is folded even with tables
     35 /*	where a lookup field can match both upper and lower case
     36 /*	text, such as regexp: and pcre:. This resulted in loss of
     37 /*	information with $\fInumber\fR substitutions.
     38 /*
     39 /*	Options:
     40 /* .IP "\fB-c \fIconfig_dir\fR"
     41 /*	Read the \fBmain.cf\fR configuration file in the named directory
     42 /*	instead of the default configuration directory.
     43 /* .IP "\fB-d \fIkey\fR"
     44 /*	Search the specified maps for \fIkey\fR and remove one entry per map.
     45 /*	The exit status is zero when the requested information was found.
     46 /*
     47 /*	If a key value of \fB-\fR is specified, the program reads key
     48 /*	values from the standard input stream. The exit status is zero
     49 /*	when at least one of the requested keys was found.
     50 /* .IP \fB-f\fR
     51 /*	Do not fold the lookup key to lower case while creating or querying
     52 /*	a table.
     53 /*
     54 /*	With Postfix version 2.3 and later, this option has no
     55 /*	effect for regular expression tables. There, case folding
     56 /*	is controlled by appending a flag to a pattern.
     57 /* .IP \fB-i\fR
     58 /*	Incremental mode. Read entries from standard input and do not
     59 /*	truncate an existing database. By default, \fBpostalias\fR(1) creates
     60 /*	a new database from the entries in \fIfile_name\fR.
     61 /* .IP \fB-j\fR
     62 /*	JSON output. Format the output from \fB-q\fR and \fB-s\fR
     63 /*	as one \fB{"\fIkey\fB": "\fIvalue\fB"}\fR object per line.
     64 /* .sp
     65 /*	This feature is available in Postfix version 3.11 and later.
     66 /* .IP \fB-N\fR
     67 /*	Include the terminating null character that terminates lookup keys
     68 /*	and values. By default, \fBpostalias\fR(1) does whatever
     69 /*	is the default for
     70 /*	the host operating system.
     71 /* .IP \fB-n\fR
     72 /*	Don't include the terminating null character that terminates lookup
     73 /*	keys and values. By default, \fBpostalias\fR(1) does whatever
     74 /*	is the default for
     75 /*	the host operating system.
     76 /* .IP \fB-o\fR
     77 /*	Do not release root privileges when processing a non-root
     78 /*	input file. By default, \fBpostalias\fR(1) drops root privileges
     79 /*	and runs as the source file owner instead.
     80 /* .IP \fB-p\fR
     81 /*	Do not inherit the file access permissions from the input file
     82 /*	when creating a new file.  Instead, create a new file with default
     83 /*	access permissions (mode 0644).
     84 /* .IP "\fB-q \fIkey\fR"
     85 /*	Search the specified maps for \fIkey\fR and write the first value
     86 /*	found to the standard output stream. The exit status is zero
     87 /*	when the requested information was found.
     88 /*
     89 /*	Note: this performs a single query with the key as specified,
     90 /*	and does not make iterative queries with substrings of the
     91 /*	key as described in the aliases(5) manual page.
     92 /*
     93 /*	If a key value of \fB-\fR is specified, the program reads key
     94 /*	values from the standard input stream and writes one line of
     95 /*	\fIkey: value\fR output for each key that was found. The exit
     96 /*	status is zero when at least one of the requested keys was found.
     97 /* .IP \fB-r\fR
     98 /*	When updating a table, do not complain about attempts to update
     99 /*	existing entries, and make those updates anyway.
    100 /* .IP \fB-s\fR
    101 /*	Retrieve all database elements, and write one line of
    102 /*	\fIkey: value\fR output for each element. The elements are
    103 /*	printed in database order, which is not necessarily the same
    104 /*	as the original input order.
    105 /*	This feature is available in Postfix version 2.2 and later,
    106 /*	and is not available for all database types.
    107 /* .IP \fB-u\fR
    108 /*	Disable UTF-8 support. UTF-8 support is enabled by default
    109 /*	when "smtputf8_enable = yes". It requires that keys and
    110 /*	values are valid UTF-8 strings.
    111 /* .IP \fB-v\fR
    112 /*	Enable verbose logging for debugging purposes. Multiple \fB-v\fR
    113 /*	options make the software increasingly verbose.
    114 /* .IP \fB-w\fR
    115 /*	When updating a table, do not complain about attempts to update
    116 /*	existing entries, and ignore those attempts.
    117 /* .PP
    118 /*	Arguments:
    119 /* .IP \fIfile_type\fR
    120 /*	The database type. To find out what types are supported, use
    121 /*	the "\fBpostconf -m\fR" command.
    122 /*
    123 /*	When no \fIfile_type\fR is specified, the software uses the database
    124 /*	type specified via the \fBdefault_database_type\fR configuration
    125 /*	parameter.
    126 /*	The default value for this parameter depends on the host environment.
    127 /*
    128 /*	The \fBpostalias\fR(1) command can query any supported file type,
    129 /*	but it can create only the following file types:
    130 /* .RS
    131 /* .IP \fBbtree\fR
    132 /*	The output is a btree file, named \fIfile_name\fB.db\fR.
    133 /*	This is available on systems with support for \fBdb\fR databases.
    134 /* .IP \fBcdb\fR
    135 /*	The output is one file named \fIfile_name\fB.cdb\fR.
    136 /*	This is available on systems with support for \fBcdb\fR databases.
    137 /* .IP \fBdbm\fR
    138 /*	The output consists of two files, named \fIfile_name\fB.pag\fR and
    139 /*	\fIfile_name\fB.dir\fR.
    140 /*	This is available on systems with support for \fBdbm\fR databases.
    141 /* .IP \fBfail\fR
    142 /*	A table that reliably fails all requests. The lookup table
    143 /*	name is used for logging only. This table exists to simplify
    144 /*	Postfix error tests.
    145 /* .IP \fBhash\fR
    146 /*	The output is a hashed file, named \fIfile_name\fB.db\fR.
    147 /*	This is available on systems with support for \fBdb\fR databases.
    148 /* .IP \fBlmdb\fR
    149 /*	The output is a btree-based file, named \fIfile_name\fB.lmdb\fR.
    150 /*	\fBlmdb\fR supports concurrent writes and reads from different
    151 /*	processes, unlike other supported file-based tables.
    152 /*	This is available on systems with support for \fBlmdb\fR databases.
    153 /* .IP \fBsdbm\fR
    154 /*	The output consists of two files, named \fIfile_name\fB.pag\fR and
    155 /*	\fIfile_name\fB.dir\fR.
    156 /*	This is available on systems with support for \fBsdbm\fR databases.
    157 /* .RE
    158 /* .IP \fIfile_name\fR
    159 /*	The name of the alias database source file when creating a database.
    160 /* DIAGNOSTICS
    161 /*	Problems are logged to the standard error stream and to
    162 /*	\fBsyslogd\fR(8) or \fBpostlogd\fR(8). No output means that
    163 /*	no problems were detected. Duplicate entries are skipped and are
    164 /*	flagged with a warning.
    165 /*
    166 /*	\fBpostalias\fR(1) terminates with zero exit status in case of success
    167 /*	(including successful "\fBpostalias -q\fR" lookup) and terminates
    168 /*	with non-zero exit status in case of failure.
    169 /* ENVIRONMENT
    170 /* .ad
    171 /* .fi
    172 /* .IP \fBMAIL_CONFIG\fR
    173 /*	Directory with Postfix configuration files.
    174 /* .IP \fBMAIL_VERBOSE\fR
    175 /*	Enable verbose logging for debugging purposes.
    176 /* CONFIGURATION PARAMETERS
    177 /* .ad
    178 /* .fi
    179 /*	The following \fBmain.cf\fR parameters are especially relevant to
    180 /*	this program.
    181 /*
    182 /*	The text below provides only a parameter summary. See
    183 /*	\fBpostconf\fR(5) for more details including examples.
    184 /* .IP "\fBalias_database (see 'postconf -d' output)\fR"
    185 /*	The alias databases for \fBlocal\fR(8) delivery that are updated with
    186 /*	"\fBnewaliases\fR" or with "\fBsendmail -bi\fR".
    187 /* .IP "\fBconfig_directory (see 'postconf -d' output)\fR"
    188 /*	The default location of the Postfix main.cf and master.cf
    189 /*	configuration files.
    190 /* .IP "\fBberkeley_db_create_buffer_size (16777216)\fR"
    191 /*	The per-table I/O buffer size for programs that create Berkeley DB
    192 /*	hash or btree tables.
    193 /* .IP "\fBberkeley_db_read_buffer_size (131072)\fR"
    194 /*	The per-table I/O buffer size for programs that read Berkeley DB
    195 /*	hash or btree tables.
    196 /* .IP "\fBdefault_database_type (see 'postconf -d' output)\fR"
    197 /*	The default database type for use in \fBnewaliases\fR(1), \fBpostalias\fR(1)
    198 /*	and \fBpostmap\fR(1) commands.
    199 /* .IP "\fBimport_environment (see 'postconf -d' output)\fR"
    200 /*	The list of environment variables that a privileged Postfix
    201 /*	process will import from a non-Postfix parent process, or name=value
    202 /*	environment overrides.
    203 /* .IP "\fBsmtputf8_enable (yes)\fR"
    204 /*	Enable preliminary SMTPUTF8 support for the protocols described
    205 /*	in RFC 6531, RFC 6532, and RFC 6533.
    206 /* .IP "\fBsyslog_facility (mail)\fR"
    207 /*	The syslog facility of Postfix logging.
    208 /* .IP "\fBsyslog_name (see 'postconf -d' output)\fR"
    209 /*	A prefix that is prepended to the process name in syslog
    210 /*	records, so that, for example, "smtpd" becomes "prefix/smtpd".
    211 /* .PP
    212 /*	Available in Postfix 2.11 and later:
    213 /* .IP "\fBlmdb_map_size (16777216)\fR"
    214 /*	The initial OpenLDAP LMDB database size limit in bytes.
    215 /* STANDARDS
    216 /*	RFC 822 (ARPA Internet Text Messages)
    217 /* SEE ALSO
    218 /*	aliases(5), format of alias database input file.
    219 /*	local(8), Postfix local delivery agent.
    220 /*	postconf(1), supported database types
    221 /*	postconf(5), configuration parameters
    222 /*	postmap(1), create/update/query lookup tables
    223 /*	newaliases(1), Sendmail compatibility interface.
    224 /*	postlogd(8), Postfix logging
    225 /*	syslogd(8), system logging
    226 /* README FILES
    227 /* .ad
    228 /* .fi
    229 /*	Use "\fBpostconf readme_directory\fR" or
    230 /*	"\fBpostconf html_directory\fR" to locate this information.
    231 /* .na
    232 /* .nf
    233 /*	DATABASE_README, Postfix lookup table overview
    234 /* LICENSE
    235 /* .ad
    236 /* .fi
    237 /*	The Secure Mailer license must be distributed with this software.
    238 /* AUTHOR(S)
    239 /*	Wietse Venema
    240 /*	IBM T.J. Watson Research
    241 /*	P.O. Box 704
    242 /*	Yorktown Heights, NY 10598, USA
    243 /*
    244 /*	Wietse Venema
    245 /*	Google, Inc.
    246 /*	111 8th Avenue
    247 /*	New York, NY 10011, USA
    248 /*--*/
    249 
    250 /* System library. */
    251 
    252 #include <sys_defs.h>
    253 #include <sys/stat.h>
    254 #include <stdlib.h>
    255 #include <unistd.h>
    256 #include <fcntl.h>
    257 #include <ctype.h>
    258 #include <string.h>
    259 #include <inttypes.h>
    260 
    261 /* Utility library. */
    262 
    263 #include <msg.h>
    264 #include <mymalloc.h>
    265 #include <vstring.h>
    266 #include <vstream.h>
    267 #include <msg_vstream.h>
    268 #include <readlline.h>
    269 #include <stringops.h>
    270 #include <split_at.h>
    271 #include <vstring_vstream.h>
    272 #include <set_eugid.h>
    273 #include <warn_stat.h>
    274 #include <clean_env.h>
    275 #include <dict_db.h>
    276 
    277 /* Global library. */
    278 
    279 #include <tok822.h>
    280 #include <mail_conf.h>
    281 #include <mail_dict.h>
    282 #include <mail_params.h>
    283 #include <mail_version.h>
    284 #include <mail_task.h>
    285 #include <dict_proxy.h>
    286 #include <mail_parm_split.h>
    287 #include <maillog_client.h>
    288 
    289 /* Application-specific. */
    290 
    291 #define STR	vstring_str
    292 #define LEN	VSTRING_LEN
    293 
    294 #define POSTALIAS_FLAG_AS_OWNER	(1<<0)	/* open dest as owner of source */
    295 #define POSTALIAS_FLAG_SAVE_PERM	(1<<1)	/* copy access permission
    296 						 * from source */
    297 
    298  /*
    299   * Global state.
    300   */
    301 int     json_output;
    302 VSTRING *json_key_buf;
    303 VSTRING *json_val_buf;
    304 
    305 /* postalias - create or update alias database */
    306 
    307 static void postalias(char *map_type, char *path_name, int postalias_flags,
    308 		              int open_flags, int dict_flags)
    309 {
    310     VSTREAM *NOCLOBBER source_fp;
    311     VSTRING *line_buffer;
    312     MKMAP  *mkmap;
    313     int     lineno;
    314     int     last_line;
    315     VSTRING *key_buffer;
    316     VSTRING *value_buffer;
    317     TOK822 *tok_list;
    318     TOK822 *key_list;
    319     TOK822 *colon;
    320     TOK822 *value_list;
    321     struct stat st;
    322     mode_t  saved_mask;
    323 
    324     /*
    325      * Initialize.
    326      */
    327     line_buffer = vstring_alloc(100);
    328     key_buffer = vstring_alloc(100);
    329     value_buffer = vstring_alloc(100);
    330     if ((open_flags & O_TRUNC) == 0) {
    331 	/* Incremental mode. */
    332 	source_fp = VSTREAM_IN;
    333 	vstream_control(source_fp, CA_VSTREAM_CTL_PATH("stdin"), CA_VSTREAM_CTL_END);
    334     } else {
    335 	/* Create database. */
    336 	if (strcmp(map_type, DICT_TYPE_PROXY) == 0)
    337 	    msg_fatal("can't create maps via the proxy service");
    338 	dict_flags |= DICT_FLAG_BULK_UPDATE;
    339 	if ((source_fp = vstream_fopen(path_name, O_RDONLY, 0)) == 0)
    340 	    msg_fatal("open %s: %m", path_name);
    341     }
    342     if (fstat(vstream_fileno(source_fp), &st) < 0)
    343 	msg_fatal("fstat %s: %m", path_name);
    344 
    345     /*
    346      * Turn off group/other read permissions as indicated in the source file.
    347      */
    348     if ((postalias_flags & POSTALIAS_FLAG_SAVE_PERM) && S_ISREG(st.st_mode))
    349 	saved_mask = umask(022 | (~st.st_mode & 077));
    350 
    351     /*
    352      * If running as root, run as the owner of the source file, so that the
    353      * result shows proper ownership, and so that a bug in postalias does not
    354      * allow privilege escalation.
    355      */
    356     if ((postalias_flags & POSTALIAS_FLAG_AS_OWNER) && getuid() == 0
    357 	&& (st.st_uid != geteuid() || st.st_gid != getegid()))
    358 	set_eugid(st.st_uid, st.st_gid);
    359 
    360     /*
    361      * Override the default per-table cache size for DB map (re)builds. We
    362      * can't do this in the mkmap* functions because those don't have access
    363      * to Postfix parameter settings.
    364      *
    365      * db_cache_size" is defined in util/dict_open.c and defaults to 128kB,
    366      * which works well for the lookup code.
    367      *
    368      * We use a larger per-table cache when building ".db" files. For "hash"
    369      * files performance degrades rapidly unless the memory pool is O(file
    370      * size).
    371      *
    372      * For "btree" files performance is good with sorted input even for small
    373      * memory pools, but with random input degrades rapidly unless the memory
    374      * pool is O(file size).
    375      */
    376     dict_db_cache_size = var_db_create_buf;
    377 
    378     /*
    379      * Open the database, create it when it does not exist, truncate it when
    380      * it does exist, and lock out any spectators.
    381      */
    382     mkmap = mkmap_open(map_type, path_name, open_flags, dict_flags);
    383 
    384     /*
    385      * And restore the umask, in case it matters.
    386      */
    387     if ((postalias_flags & POSTALIAS_FLAG_SAVE_PERM) && S_ISREG(st.st_mode))
    388 	umask(saved_mask);
    389 
    390     /*
    391      * Trap "exceptions" so that we can restart a bulk-mode update after a
    392      * recoverable error.
    393      */
    394     for (;;) {
    395 	if (dict_isjmp(mkmap->dict) != 0
    396 	    && dict_setjmp(mkmap->dict) != 0
    397 	    && vstream_fseek(source_fp, SEEK_SET, 0) < 0)
    398 	    msg_fatal("seek %s: %m", VSTREAM_PATH(source_fp));
    399 
    400 	/*
    401 	 * Add records to the database.
    402 	 */
    403 	last_line = 0;
    404 	while (readllines(line_buffer, source_fp, &last_line, &lineno)) {
    405 
    406 	    /*
    407 	     * First some UTF-8 checks sans casefolding.
    408 	     */
    409 	    if ((mkmap->dict->flags & DICT_FLAG_UTF8_ACTIVE)
    410 		&& !allascii(STR(line_buffer))
    411 		&& !valid_utf8_stringz(STR(line_buffer))) {
    412 		msg_warn("%s, line %d: non-UTF-8 input \"%s\""
    413 			 " -- ignoring this line",
    414 			 VSTREAM_PATH(source_fp), lineno, STR(line_buffer));
    415 		continue;
    416 	    }
    417 
    418 	    /*
    419 	     * Tokenize the input, so that we do the right thing when a
    420 	     * quoted localpart contains special characters such as "@", ":"
    421 	     * and so on.
    422 	     */
    423 	    if ((tok_list = tok822_scan(STR(line_buffer), (TOK822 **) 0)) == 0)
    424 		continue;
    425 
    426 	    /*
    427 	     * Enforce the key:value format. Disallow missing keys,
    428 	     * multi-address keys, or missing values. In order to specify an
    429 	     * empty string or value, enclose it in double quotes.
    430 	     */
    431 	    if ((colon = tok822_find_type(tok_list, ':')) == 0
    432 		|| colon->prev == 0 || colon->next == 0
    433 		|| tok822_rfind_type(colon, ',')) {
    434 		msg_warn("%s, line %d: need name:value pair",
    435 			 VSTREAM_PATH(source_fp), lineno);
    436 		tok822_free_tree(tok_list);
    437 		continue;
    438 	    }
    439 
    440 	    /*
    441 	     * Key must be local. XXX We should use the Postfix rewriting and
    442 	     * resolving services to handle all address forms correctly.
    443 	     * However, we can't count on the mail system being up when the
    444 	     * alias database is being built, so we're guessing a bit.
    445 	     */
    446 	    if (tok822_rfind_type(colon, '@') || tok822_rfind_type(colon, '%')) {
    447 		msg_warn("%s, line %d: name must be local and have no domain",
    448 			 VSTREAM_PATH(source_fp), lineno);
    449 		tok822_free_tree(tok_list);
    450 		continue;
    451 	    }
    452 
    453 	    /*
    454 	     * Split the input into key and value parts, and convert from
    455 	     * token representation back to string representation. Convert
    456 	     * the key to internal (unquoted) form, because the resolver
    457 	     * produces addresses in internal form. Convert the value to
    458 	     * external (quoted) form, because it will have to be re-parsed
    459 	     * upon lookup. Discard the token representation when done.
    460 	     */
    461 	    key_list = tok_list;
    462 	    tok_list = 0;
    463 	    value_list = tok822_cut_after(colon);
    464 	    tok822_unlink(colon);
    465 	    tok822_free(colon);
    466 
    467 	    tok822_internalize(key_buffer, key_list, TOK822_STR_DEFL);
    468 	    tok822_free_tree(key_list);
    469 
    470 	    tok822_externalize(value_buffer, value_list, TOK822_STR_DEFL);
    471 	    tok822_free_tree(value_list);
    472 
    473 	    /*
    474 	     * Store the value under a case-insensitive key.
    475 	     */
    476 	    mkmap_append(mkmap, STR(key_buffer), STR(value_buffer));
    477 	    if (mkmap->dict->error)
    478 		msg_fatal("table %s:%s: write error: %m",
    479 			  mkmap->dict->type, mkmap->dict->name);
    480 	}
    481 	break;
    482     }
    483 
    484     /*
    485      * Update or append sendmail and NIS signatures.
    486      */
    487     if ((open_flags & O_TRUNC) == 0)
    488 	mkmap->dict->flags |= DICT_FLAG_DUP_REPLACE;
    489 
    490     /*
    491      * Sendmail compatibility: add the @:@ signature to indicate that the
    492      * database is complete. This might be needed by NIS clients running
    493      * sendmail.
    494      */
    495     mkmap_append(mkmap, "@", "@");
    496     if (mkmap->dict->error)
    497 	msg_fatal("table %s:%s: write error: %m",
    498 		  mkmap->dict->type, mkmap->dict->name);
    499 
    500     /*
    501      * NIS compatibility: add time and master info. Unlike other information,
    502      * this information MUST be written without a trailing null appended to
    503      * key or value.
    504      */
    505 #if (defined(HAS_NIS) || defined(HAS_NISPLUS))
    506     mkmap->dict->flags &= ~DICT_FLAG_TRY1NULL;
    507     mkmap->dict->flags |= DICT_FLAG_TRY0NULL;
    508     vstring_sprintf(value_buffer, "%010" PRId64, (int64_t) time((time_t *) 0));
    509     mkmap->dict->flags &= ~DICT_FLAG_FOLD_FIX;
    510     mkmap_append(mkmap, "YP_LAST_MODIFIED", STR(value_buffer));
    511     mkmap_append(mkmap, "YP_MASTER_NAME", var_myhostname);
    512 #endif
    513 
    514     /*
    515      * Close the alias database, and release the lock.
    516      */
    517     mkmap_close(mkmap);
    518 
    519     /*
    520      * Cleanup. We're about to terminate, but it is a good sanity check.
    521      */
    522     vstring_free(value_buffer);
    523     vstring_free(key_buffer);
    524     vstring_free(line_buffer);
    525     if (source_fp != VSTREAM_IN)
    526 	vstream_fclose(source_fp);
    527 }
    528 
    529 /* postalias_queries - apply multiple requests from stdin */
    530 
    531 static int postalias_queries(VSTREAM *in, char **maps, const int map_count,
    532 			             const int dict_flags)
    533 {
    534     int     found = 0;
    535     VSTRING *keybuf = vstring_alloc(100);
    536     DICT  **dicts;
    537     const char *map_name;
    538     const char *value;
    539     int     n;
    540 
    541     /*
    542      * Sanity check.
    543      */
    544     if (map_count <= 0)
    545 	msg_panic("postalias_queries: bad map count");
    546 
    547     /*
    548      * Prepare to open maps lazily.
    549      */
    550     dicts = (DICT **) mymalloc(sizeof(*dicts) * map_count);
    551     for (n = 0; n < map_count; n++)
    552 	dicts[n] = 0;
    553 
    554     /*
    555      * Perform all queries. Open maps on the fly, to avoid opening
    556      * unnecessary maps.
    557      */
    558     while (vstring_get_nonl(keybuf, in) != VSTREAM_EOF) {
    559 	for (n = 0; n < map_count; n++) {
    560 	    if (dicts[n] == 0)
    561 		dicts[n] = ((map_name = split_at(maps[n], ':')) != 0 ?
    562 		       dict_open3(maps[n], map_name, O_RDONLY, dict_flags) :
    563 		    dict_open3(var_db_type, maps[n], O_RDONLY, dict_flags));
    564 	    if ((value = dict_get(dicts[n], STR(keybuf))) != 0) {
    565 		if (*value == 0) {
    566 		    msg_warn("table %s:%s: key %s: empty string result is not allowed",
    567 			     dicts[n]->type, dicts[n]->name, STR(keybuf));
    568 		    msg_warn("table %s:%s should return NO RESULT in case of NOT FOUND",
    569 			     dicts[n]->type, dicts[n]->name);
    570 		}
    571 		if (json_output == 0)
    572 		    vstream_printf("%s:	%s\n", STR(keybuf), value);
    573 		else
    574 		    vstream_printf("{\"%s\": \"%s\"}\n",
    575 			      quote_for_json(json_key_buf, STR(keybuf), -1),
    576 				   quote_for_json(json_val_buf, value, -1));
    577 		found = 1;
    578 		break;
    579 	    }
    580 	    if (dicts[n]->error)
    581 		msg_fatal("table %s:%s: query error: %m",
    582 			  dicts[n]->type, dicts[n]->name);
    583 	}
    584     }
    585     if (found)
    586 	vstream_fflush(VSTREAM_OUT);
    587 
    588     /*
    589      * Cleanup.
    590      */
    591     for (n = 0; n < map_count; n++)
    592 	if (dicts[n])
    593 	    dict_close(dicts[n]);
    594     myfree((void *) dicts);
    595     vstring_free(keybuf);
    596 
    597     return (found);
    598 }
    599 
    600 /* postalias_query - query a map and print the result to stdout */
    601 
    602 static int postalias_query(const char *map_type, const char *map_name,
    603 			           const char *key, int dict_flags)
    604 {
    605     DICT   *dict;
    606     const char *value;
    607 
    608     dict = dict_open3(map_type, map_name, O_RDONLY, dict_flags);
    609     if ((value = dict_get(dict, key)) != 0) {
    610 	if (*value == 0) {
    611 	    msg_warn("table %s:%s: key %s: empty string result is not allowed",
    612 		     map_type, map_name, key);
    613 	    msg_warn("table %s:%s should return NO RESULT in case of NOT FOUND",
    614 		     map_type, map_name);
    615 	}
    616 	if (json_output == 0)
    617 	    vstream_printf("%s\n", value);
    618 	else
    619 	    vstream_printf("{\"%s\": \"%s\"}\n",
    620 			   quote_for_json(json_key_buf, key, -1),
    621 			   quote_for_json(json_val_buf, value, -1));
    622     }
    623     if (dict->error)
    624 	msg_fatal("table %s:%s: query error: %m", dict->type, dict->name);
    625     vstream_fflush(VSTREAM_OUT);
    626     dict_close(dict);
    627     return (value != 0);
    628 }
    629 
    630 /* postalias_deletes - apply multiple requests from stdin */
    631 
    632 static int postalias_deletes(VSTREAM *in, char **maps, const int map_count,
    633 			             int dict_flags)
    634 {
    635     int     found = 0;
    636     VSTRING *keybuf = vstring_alloc(100);
    637     DICT  **dicts;
    638     const char *map_name;
    639     int     n;
    640     int     open_flags;
    641 
    642     /*
    643      * Sanity check.
    644      */
    645     if (map_count <= 0)
    646 	msg_panic("postalias_deletes: bad map count");
    647 
    648     /*
    649      * Open maps ahead of time.
    650      */
    651     dicts = (DICT **) mymalloc(sizeof(*dicts) * map_count);
    652     for (n = 0; n < map_count; n++) {
    653 	map_name = split_at(maps[n], ':');
    654 	if (map_name && strcmp(maps[n], DICT_TYPE_PROXY) == 0)
    655 	    open_flags = O_RDWR | O_CREAT;	/* XXX */
    656 	else
    657 	    open_flags = O_RDWR;
    658 	dicts[n] = (map_name != 0 ?
    659 		    dict_open3(maps[n], map_name, open_flags, dict_flags) :
    660 		  dict_open3(var_db_type, maps[n], open_flags, dict_flags));
    661     }
    662 
    663     /*
    664      * Perform all requests.
    665      */
    666     while (vstring_get_nonl(keybuf, in) != VSTREAM_EOF) {
    667 	for (n = 0; n < map_count; n++) {
    668 	    found |= (dict_del(dicts[n], STR(keybuf)) == 0);
    669 	    if (dicts[n]->error)
    670 		msg_fatal("table %s:%s: delete error: %m",
    671 			  dicts[n]->type, dicts[n]->name);
    672 	}
    673     }
    674 
    675     /*
    676      * Cleanup.
    677      */
    678     for (n = 0; n < map_count; n++)
    679 	if (dicts[n])
    680 	    dict_close(dicts[n]);
    681     myfree((void *) dicts);
    682     vstring_free(keybuf);
    683 
    684     return (found);
    685 }
    686 
    687 /* postalias_delete - delete a key value pair from a map */
    688 
    689 static int postalias_delete(const char *map_type, const char *map_name,
    690 			            const char *key, int dict_flags)
    691 {
    692     DICT   *dict;
    693     int     status;
    694     int     open_flags;
    695 
    696     if (strcmp(map_type, DICT_TYPE_PROXY) == 0)
    697 	open_flags = O_RDWR | O_CREAT;		/* XXX */
    698     else
    699 	open_flags = O_RDWR;
    700     dict = dict_open3(map_type, map_name, open_flags, dict_flags);
    701     status = dict_del(dict, key);
    702     if (dict->error)
    703 	msg_fatal("table %s:%s: delete error: %m", dict->type, dict->name);
    704     dict_close(dict);
    705     return (status == 0);
    706 }
    707 
    708 /* postalias_seq - print all map entries to stdout */
    709 
    710 static void postalias_seq(const char *map_type, const char *map_name,
    711 			          int dict_flags)
    712 {
    713     DICT   *dict;
    714     const char *key;
    715     const char *value;
    716     int     func;
    717 
    718     dict = dict_open3(map_type, map_name, O_RDONLY, dict_flags);
    719     for (func = DICT_SEQ_FUN_FIRST; /* void */ ; func = DICT_SEQ_FUN_NEXT) {
    720 	if (dict_seq(dict, func, &key, &value) != 0)
    721 	    break;
    722 	if (*key == 0) {
    723 	    msg_warn("table %s:%s: empty lookup key value is not allowed",
    724 		     map_type, map_name);
    725 	} else if (*value == 0) {
    726 	    msg_warn("table %s:%s: key %s: empty string result is not allowed",
    727 		     map_type, map_name, key);
    728 	    msg_warn("table %s:%s should return NO RESULT in case of NOT FOUND",
    729 		     map_type, map_name);
    730 	}
    731 	if (json_output == 0)
    732 	    vstream_printf("%s:	%s\n", key, value);
    733 	else
    734 	    vstream_printf("{\"%s\": \"%s\"}\n",
    735 			   quote_for_json(json_key_buf, key, -1),
    736 			   quote_for_json(json_val_buf, value, -1));
    737     }
    738     if (dict->error)
    739 	msg_fatal("table %s:%s: sequence error: %m", dict->type, dict->name);
    740     vstream_fflush(VSTREAM_OUT);
    741     dict_close(dict);
    742 }
    743 
    744 /* usage - explain */
    745 
    746 static NORETURN usage(char *myname)
    747 {
    748     msg_fatal("usage: %s [-Nfinoprsuvw] [-c config_dir] [-d key] [-q key] [map_type:]file...",
    749 	      myname);
    750 }
    751 
    752 MAIL_VERSION_STAMP_DECLARE;
    753 
    754 int     main(int argc, char **argv)
    755 {
    756     char   *path_name;
    757     int     ch;
    758     int     fd;
    759     char   *slash;
    760     struct stat st;
    761     int     postalias_flags = POSTALIAS_FLAG_AS_OWNER | POSTALIAS_FLAG_SAVE_PERM;
    762     int     open_flags = O_RDWR | O_CREAT | O_TRUNC;
    763     int     dict_flags = (DICT_FLAG_DUP_WARN | DICT_FLAG_FOLD_FIX
    764 			  | DICT_FLAG_UTF8_REQUEST);
    765     int     update = 0;
    766     char   *query = 0;
    767     char   *delkey = 0;
    768     int     sequence = 0;
    769     int     found;
    770     ARGV   *import_env;
    771 
    772     /*
    773      * Fingerprint executables and core dumps.
    774      */
    775     MAIL_VERSION_STAMP_ALLOCATE;
    776 
    777     /*
    778      * Be consistent with file permissions.
    779      */
    780     umask(022);
    781 
    782     /*
    783      * To minimize confusion, make sure that the standard file descriptors
    784      * are open before opening anything else. XXX Work around for 44BSD where
    785      * fstat can return EBADF on an open file descriptor.
    786      */
    787     for (fd = 0; fd < 3; fd++)
    788 	if (fstat(fd, &st) == -1
    789 	    && (close(fd), open("/dev/null", O_RDWR, 0)) != fd)
    790 	    msg_fatal("open /dev/null: %m");
    791 
    792     /*
    793      * Process environment options as early as we can. We are not set-uid,
    794      * and we are supposed to be running in a controlled environment.
    795      */
    796     if (getenv(CONF_ENV_VERB))
    797 	msg_verbose = 1;
    798 
    799     /*
    800      * Initialize. Set up logging. Read the global configuration file after
    801      * parsing command-line arguments.
    802      */
    803     if ((slash = strrchr(argv[0], '/')) != 0 && slash[1])
    804 	argv[0] = slash + 1;
    805     msg_vstream_init(argv[0], VSTREAM_ERR);
    806     maillog_client_init(mail_task(argv[0]), MAILLOG_CLIENT_FLAG_NONE);
    807 
    808     /*
    809      * Check the Postfix library version as soon as we enable logging.
    810      */
    811     MAIL_VERSION_CHECK;
    812 
    813     /*
    814      * Parse JCL.
    815      */
    816     while ((ch = GETOPT(argc, argv, "Nc:d:fijnopq:rsuvw")) > 0) {
    817 	switch (ch) {
    818 	default:
    819 	    usage(argv[0]);
    820 	    break;
    821 	case 'N':
    822 	    dict_flags |= DICT_FLAG_TRY1NULL;
    823 	    dict_flags &= ~DICT_FLAG_TRY0NULL;
    824 	    break;
    825 	case 'c':
    826 	    if (setenv(CONF_ENV_PATH, optarg, 1) < 0)
    827 		msg_fatal("out of memory");
    828 	    break;
    829 	case 'd':
    830 	    if (update || sequence || query || delkey)
    831 		msg_fatal("specify only one of -d -i -q or -s");
    832 	    delkey = optarg;
    833 	    break;
    834 	case 'f':
    835 	    dict_flags &= ~DICT_FLAG_FOLD_FIX;
    836 	    break;
    837 	case 'i':
    838 	    if (update || sequence || query || delkey)
    839 		msg_fatal("specify only one of -d -i -q or -s");
    840 	    update = 1;
    841 	    open_flags &= ~O_TRUNC;
    842 	    break;
    843 	case 'j':
    844 	    if (json_output == 0) {
    845 		json_output = 1;
    846 		json_key_buf = vstring_alloc(100);
    847 		json_val_buf = vstring_alloc(100);
    848 	    }
    849 	    break;
    850 	case 'n':
    851 	    dict_flags |= DICT_FLAG_TRY0NULL;
    852 	    dict_flags &= ~DICT_FLAG_TRY1NULL;
    853 	    break;
    854 	case 'o':
    855 	    postalias_flags &= ~POSTALIAS_FLAG_AS_OWNER;
    856 	    break;
    857 	case 'p':
    858 	    postalias_flags &= ~POSTALIAS_FLAG_SAVE_PERM;
    859 	    break;
    860 	case 'q':
    861 	    if (update || sequence || query || delkey)
    862 		msg_fatal("specify only one of -d -i -q or -s");
    863 	    query = optarg;
    864 	    break;
    865 	case 'r':
    866 	    dict_flags &= ~(DICT_FLAG_DUP_WARN | DICT_FLAG_DUP_IGNORE);
    867 	    dict_flags |= DICT_FLAG_DUP_REPLACE;
    868 	    break;
    869 	case 's':
    870 	    if (update || sequence || query || delkey)
    871 		msg_fatal("specify only one of -d -i -q or -s");
    872 	    sequence = 1;
    873 	    break;
    874 	case 'u':
    875 	    dict_flags &= ~DICT_FLAG_UTF8_REQUEST;
    876 	    break;
    877 	case 'v':
    878 	    msg_verbose++;
    879 	    break;
    880 	case 'w':
    881 	    dict_flags &= ~(DICT_FLAG_DUP_WARN | DICT_FLAG_DUP_REPLACE);
    882 	    dict_flags |= DICT_FLAG_DUP_IGNORE;
    883 	    break;
    884 	}
    885     }
    886     if (json_output && !(sequence || query))
    887 	msg_fatal("option -j requires -q or -s");
    888     mail_conf_read();
    889     /* Enforce consistent operation of different Postfix parts. */
    890     import_env = mail_parm_split(VAR_IMPORT_ENVIRON, var_import_environ);
    891     update_env(import_env->argv);
    892     argv_free(import_env);
    893     /* Re-evaluate mail_task() after reading main.cf. */
    894     maillog_client_init(mail_task(argv[0]), MAILLOG_CLIENT_FLAG_NONE);
    895     mail_dict_init();
    896 
    897     /*
    898      * Use the map type specified by the user, or fall back to a default
    899      * database type.
    900      */
    901     if (delkey) {				/* remove entry */
    902 	if (optind + 1 > argc)
    903 	    usage(argv[0]);
    904 	if (strcmp(delkey, "-") == 0)
    905 	    exit(postalias_deletes(VSTREAM_IN, argv + optind, argc - optind,
    906 				   dict_flags | DICT_FLAG_LOCK) == 0);
    907 	found = 0;
    908 	while (optind < argc) {
    909 	    if ((path_name = split_at(argv[optind], ':')) != 0) {
    910 		found |= postalias_delete(argv[optind], path_name, delkey,
    911 					  dict_flags | DICT_FLAG_LOCK);
    912 	    } else {
    913 		found |= postalias_delete(var_db_type, argv[optind], delkey,
    914 					  dict_flags | DICT_FLAG_LOCK);
    915 	    }
    916 	    optind++;
    917 	}
    918 	exit(found ? 0 : 1);
    919     } else if (query) {				/* query map(s) */
    920 	if (optind + 1 > argc)
    921 	    usage(argv[0]);
    922 	if (strcmp(query, "-") == 0)
    923 	    exit(postalias_queries(VSTREAM_IN, argv + optind, argc - optind,
    924 				   dict_flags | DICT_FLAG_LOCK) == 0);
    925 	while (optind < argc) {
    926 	    if ((path_name = split_at(argv[optind], ':')) != 0) {
    927 		found = postalias_query(argv[optind], path_name, query,
    928 					dict_flags | DICT_FLAG_LOCK);
    929 	    } else {
    930 		found = postalias_query(var_db_type, argv[optind], query,
    931 					dict_flags | DICT_FLAG_LOCK);
    932 	    }
    933 	    if (found)
    934 		exit(0);
    935 	    optind++;
    936 	}
    937 	exit(1);
    938     } else if (sequence) {
    939 	while (optind < argc) {
    940 	    if ((path_name = split_at(argv[optind], ':')) != 0) {
    941 		postalias_seq(argv[optind], path_name,
    942 			      dict_flags | DICT_FLAG_LOCK);
    943 	    } else {
    944 		postalias_seq(var_db_type, argv[optind],
    945 			      dict_flags | DICT_FLAG_LOCK);
    946 	    }
    947 	    exit(0);
    948 	}
    949 	exit(1);
    950     } else {					/* create/update map(s) */
    951 	if (optind + 1 > argc)
    952 	    usage(argv[0]);
    953 	while (optind < argc) {
    954 	    if ((path_name = split_at(argv[optind], ':')) != 0) {
    955 		postalias(argv[optind], path_name, postalias_flags,
    956 			  open_flags, dict_flags);
    957 	    } else {
    958 		postalias(var_db_type, argv[optind], postalias_flags,
    959 			  open_flags, dict_flags);
    960 	    }
    961 	    optind++;
    962 	}
    963 	exit(0);
    964     }
    965 }
    966