Example #1
0
int     smtp_sasl_passwd_lookup(SMTP_SESSION *session)
{
    const char *myname = "smtp_sasl_passwd_lookup";
    SMTP_STATE *state = session->state;
    const char *value;
    char   *passwd;

    /*
     * Sanity check.
     */
    if (smtp_sasl_passwd_map == 0)
	msg_panic("%s: passwd map not initialized", myname);

    /*
     * Look up the per-server password information. Try the hostname first,
     * then try the destination.
     * 
     * XXX Instead of using nexthop (the intended destination) we use dest
     * (either the intended destination, or a fall-back destination).
     * 
     * XXX SASL authentication currently depends on the host/domain but not on
     * the TCP port. If the port is not :25, we should append it to the table
     * lookup key. Code for this was briefly introduced into 2.2 snapshots,
     * but didn't canonicalize the TCP port, and did not append the port to
     * the MX hostname.
     */
    smtp_sasl_passwd_map->error = 0;
    if (((state->misc_flags & SMTP_MISC_FLAG_USE_LMTP) == 0
	 && var_smtp_sender_auth && state->request->sender[0]
	 && (value = mail_addr_find(smtp_sasl_passwd_map,
				 state->request->sender, (char **) 0)) != 0)
	|| (smtp_sasl_passwd_map->error == 0
	    && (value = maps_find(smtp_sasl_passwd_map,
				  session->host, 0)) != 0)
	|| (smtp_sasl_passwd_map->error == 0
	    && (value = maps_find(smtp_sasl_passwd_map,
				  session->dest, 0)) != 0)) {
	if (session->sasl_username)
	    myfree(session->sasl_username);
	session->sasl_username = mystrdup(value);
	passwd = split_at(session->sasl_username, ':');
	if (session->sasl_passwd)
	    myfree(session->sasl_passwd);
	session->sasl_passwd = mystrdup(passwd ? passwd : "");
	if (msg_verbose)
	    msg_info("%s: host `%s' user `%s' pass `%s'",
		     myname, session->host,
		     session->sasl_username, session->sasl_passwd);
	return (1);
    } else if (smtp_sasl_passwd_map->error) {
	msg_warn("%s: %s lookup error",
		  state->request->queue_id, smtp_sasl_passwd_map->title);
	vstream_longjmp(session->stream, SMTP_ERR_DATA);
    } else {
	if (msg_verbose)
	    msg_info("%s: no auth info found (sender=`%s', host=`%s')",
		     myname, state->request->sender, session->host);
	return (0);
    }
}
Example #2
0
static const char *find_addr(MAPS *path, const char *address, int flags,
	             int with_domain, int query_form, VSTRING *ext_addr_buf)
{
    const char *result;

#define SANS_DOMAIN	0
#define WITH_DOMAIN	1

    switch (query_form) {

	/*
	 * Query with external-form (quoted) address. The code looks a bit
	 * unusual to emphasize the symmetry with the other cases.
	 */
    case MA_FORM_EXTERNAL:
    case MA_FORM_EXTERNAL_FIRST:
	quote_822_local_flags(ext_addr_buf, address,
			      with_domain ? QUOTE_FLAG_DEFAULT :
			    QUOTE_FLAG_DEFAULT | QUOTE_FLAG_BARE_LOCALPART);
	result = maps_find(path, STR(ext_addr_buf), flags);
	if (result != 0 || path->error != 0
	    || query_form != MA_FORM_EXTERNAL_FIRST
	    || strcmp(address, STR(ext_addr_buf)) == 0)
	    break;
	result = maps_find(path, address, flags);
	break;

	/*
	 * Query with internal-form (unquoted) address. The code looks a bit
	 * unusual to emphasize the symmetry with the other cases.
	 */
    case MA_FORM_INTERNAL:
    case MA_FORM_INTERNAL_FIRST:
	result = maps_find(path, address, flags);
	if (result != 0 || path->error != 0
	    || query_form != MA_FORM_INTERNAL_FIRST)
	    break;
	quote_822_local_flags(ext_addr_buf, address,
			      with_domain ? QUOTE_FLAG_DEFAULT :
			    QUOTE_FLAG_DEFAULT | QUOTE_FLAG_BARE_LOCALPART);
	if (strcmp(address, STR(ext_addr_buf)) == 0)
	    break;
	result = maps_find(path, STR(ext_addr_buf), flags);
	break;

	/*
	 * Can't happen.
	 */
    default:
	msg_panic("mail_addr_find: bad query_form: %d", query_form);
    }
    return (result);
}
Example #3
0
int     main(int argc, char **argv)
{
    VSTRING *buf = vstring_alloc(100);
    MAPS   *maps;
    const char *result;

    if (argc != 2)
	msg_fatal("usage: %s maps", argv[0]);
    msg_verbose = 2;
    maps = maps_create("whatever", argv[1], DICT_FLAG_LOCK);

    while (vstring_fgets_nonl(buf, VSTREAM_IN)) {
	if ((result = maps_find(maps, vstring_str(buf), 0)) != 0) {
	    vstream_printf("%s\n", result);
	} else if (dict_errno != 0) {
	    msg_fatal("lookup error: %m");
	} else {
	    vstream_printf("not found\n");
	}
	vstream_fflush(VSTREAM_OUT);
    }
    maps_free(maps);
    vstring_free(buf);
    return (0);
}
Example #4
0
char   *hbc_header_checks(void *context, HBC_CHECKS *hbc, int header_class,
			          const HEADER_OPTS *hdr_opts,
			          VSTRING *header, off_t offset)
{
    const char *myname = "hbc_header_checks";
    const char *action;
    HBC_MAP_INFO *mp;

    if (msg_verbose)
	msg_info("%s: '%.30s'", myname, STR(header));

    /*
     * XXX This is for compatibility with the cleanup(8) server.
     */
    if (hdr_opts && (hdr_opts->flags & HDR_OPT_MIME))
	header_class = MIME_HDR_MULTIPART;

    mp = hbc->map_info + HBC_HEADER_INDEX(header_class);

    if (mp->maps != 0 && (action = maps_find(mp->maps, STR(header), 0)) != 0) {
	return (hbc_action(context, hbc->call_backs,
			   mp->map_class, HBC_CTXT_HEADER, action,
			   STR(header), LEN(header), offset));
    } else if (mp->maps && mp->maps->error) {
	return (HBC_CHECKS_STAT_ERROR);
    } else {
	return (STR(header));
    }
}
Example #5
0
static void tls_site_lookup(int *site_level, const char *site_name,
			            const char *site_class)
{
    const char *lookup;

    /*
     * Look up a non-default policy. In case of multiple lookup results, the
     * precedence order is a permutation of the TLS enforcement level order:
     * VERIFY, ENCRYPT, NONE, MAY, NOTFOUND. I.e. we override MAY with a more
     * specific policy including NONE, otherwise we choose the stronger
     * enforcement level.
     */
    if ((lookup = maps_find(tls_per_site, site_name, 0)) != 0) {
	if (!strcasecmp(lookup, "NONE")) {
	    /* NONE overrides MAY or NOTFOUND. */
	    if (*site_level <= TLS_LEV_MAY)
		*site_level = TLS_LEV_NONE;
	} else if (!strcasecmp(lookup, "MAY")) {
	    /* MAY overrides NOTFOUND but not NONE. */
	    if (*site_level < TLS_LEV_NONE)
		*site_level = TLS_LEV_MAY;
	} else if (!strcasecmp(lookup, "MUST_NOPEERMATCH")) {
	    if (*site_level < TLS_LEV_ENCRYPT)
		*site_level = TLS_LEV_ENCRYPT;
	} else if (!strcasecmp(lookup, "MUST")) {
	    if (*site_level < TLS_LEV_VERIFY)
		*site_level = TLS_LEV_VERIFY;
	} else {
	    msg_warn("Table %s: ignoring unknown TLS policy '%s' for %s %s",
		     var_smtp_tls_per_site, lookup, site_class, site_name);
	}
    } else if (tls_per_site->error) {
	msg_fatal("%s lookup error for %s", tls_per_site->title, site_name);
    }
}
Example #6
0
static int find_transport_entry(TRANSPORT_INFO *tp, const char *key,
				        const char *rcpt_domain, int flags,
				        VSTRING *channel, VSTRING *nexthop)
{
    char   *saved_value;
    const char *host;
    const char *value;

#define FOUND		1
#define NOTFOUND	0

    /*
     * Look up an entry with extreme prejudice.
     * 
     * XXX Should report lookup failure status to caller instead of aborting.
     */
    if ((value = maps_find(tp->transport_path, key, flags)) == 0)
	return (NOTFOUND);

    /*
     * It would be great if we could specify a recipient address in the
     * lookup result. Unfortunately, we cannot simply run the result through
     * a parser that recognizes "transport:user@domain" because the lookup
     * result can have arbitrary content (especially in the case of the error
     * mailer).
     */
    else {
	saved_value = mystrdup(value);
	host = split_at(saved_value, ':');
	update_entry(saved_value, host ? host : "", rcpt_domain,
		     channel, nexthop);
	myfree(saved_value);
	return (FOUND);
    }
}
static void tls_site_lookup(SMTP_TLS_POLICY *tls, int *site_level,
		              const char *site_name, const char *site_class)
{
    const char *lookup;

    /*
     * Look up a non-default policy. In case of multiple lookup results, the
     * precedence order is a permutation of the TLS enforcement level order:
     * VERIFY, ENCRYPT, NONE, MAY, NOTFOUND. I.e. we override MAY with a more
     * specific policy including NONE, otherwise we choose the stronger
     * enforcement level.
     */
    if ((lookup = maps_find(tls_per_site, site_name, 0)) != 0) {
	if (!strcasecmp(lookup, "NONE")) {
	    /* NONE overrides MAY or NOTFOUND. */
	    if (*site_level <= TLS_LEV_MAY)
		*site_level = TLS_LEV_NONE;
	} else if (!strcasecmp(lookup, "MAY")) {
	    /* MAY overrides NOTFOUND but not NONE. */
	    if (*site_level < TLS_LEV_NONE)
		*site_level = TLS_LEV_MAY;
	} else if (!strcasecmp(lookup, "MUST_NOPEERMATCH")) {
	    if (*site_level < TLS_LEV_ENCRYPT)
		*site_level = TLS_LEV_ENCRYPT;
	} else if (!strcasecmp(lookup, "MUST")) {
	    if (*site_level < TLS_LEV_VERIFY)
		*site_level = TLS_LEV_VERIFY;
	} else {
	    msg_warn("%s: unknown TLS policy '%s' for %s %s",
		     tls_per_site->title, lookup, site_class, site_name);
	    MARK_INVALID(tls->why, site_level);
	    return;
	}
    } else if (tls_per_site->error) {
	msg_warn("%s: %s \"%s\": per-site table lookup error",
		 tls_per_site->title, site_class, site_name);
	dsb_simple(tls->why, "4.3.0", "Temporary lookup error");
	*site_level = TLS_LEV_INVALID;
	return;
    }
    return;
}
Example #8
0
char   *hbc_body_checks(void *context, HBC_CHECKS *hbc, const char *line,
			        ssize_t len, off_t offset)
{
    const char *myname = "hbc_body_checks";
    const char *action;
    HBC_MAP_INFO *mp;

    if (msg_verbose)
	msg_info("%s: '%.30s'", myname, line);

    mp = hbc->map_info;

    if ((action = maps_find(mp->maps, line, 0)) != 0) {
	return (hbc_action(context, hbc->call_backs,
			   mp->map_class, HBC_CTXT_BODY, action,
			   line, len, offset));
    } else if (mp->maps->error) {
	return (HBC_CHECKS_STAT_ERROR);
    } else {
	return ((char *) line);
    }
}
Example #9
0
int     deliver_unknown(LOCAL_STATE state, USER_ATTR usr_attr)
{
    const char *myname = "deliver_unknown";
    int     status;
    VSTRING *expand_luser;
    static MAPS *transp_maps;
    const char *map_transport;

    /*
     * Make verbose logging easier to understand.
     */
    state.level++;
    if (msg_verbose)
	MSG_LOG_STATE(myname, state);

    /*
     * DUPLICATE/LOOP ELIMINATION
     * 
     * Don't deliver the same user twice.
     */
    if (been_here(state.dup_filter, "%s %s", myname, state.msg_attr.local))
	return (0);

    /*
     * The fall-back transport specifies a delivery machanism that handles
     * users not found in the aliases or UNIX passwd databases.
     */
    if (*var_fbck_transp_maps && transp_maps == 0)
	transp_maps = maps_create(VAR_FBCK_TRANSP_MAPS, var_fbck_transp_maps,
				  DICT_FLAG_LOCK | DICT_FLAG_NO_REGSUB);
    /* The -1 is a hint for the down-stream deliver_completed() function. */
    if (transp_maps
	&& (map_transport = maps_find(transp_maps, state.msg_attr.user,
				      DICT_FLAG_NONE)) != 0) {
	state.msg_attr.rcpt.offset = -1L;
	return (deliver_pass(MAIL_CLASS_PRIVATE, map_transport,
			     state.request, &state.msg_attr.rcpt));
    } else if (transp_maps && transp_maps->error != 0) {
	/* Details in the logfile. */
	dsb_simple(state.msg_attr.why, "4.3.0", "table lookup failure");
	return (defer_append(BOUNCE_FLAGS(state.request),
			     BOUNCE_ATTR(state.msg_attr)));
    }
    if (*var_fallback_transport) {
	state.msg_attr.rcpt.offset = -1L;
	return (deliver_pass(MAIL_CLASS_PRIVATE, var_fallback_transport,
			     state.request, &state.msg_attr.rcpt));
    }

    /*
     * Subject the luser_relay address to $name expansion, disable
     * propagation of unmatched address extension, and re-inject the address
     * into the delivery machinery. Do not give special treatment to "|stuff"
     * or /stuff.
     */
    if (*var_luser_relay) {
	state.msg_attr.unmatched = 0;
	expand_luser = vstring_alloc(100);
	local_expand(expand_luser, var_luser_relay, &state, &usr_attr, (char *) 0);
	status = deliver_resolve_addr(state, usr_attr, STR(expand_luser));
	vstring_free(expand_luser);
	return (status);
    }

    /*
     * If no alias was found for a required reserved name, toss the message
     * into the bit bucket, and issue a warning instead.
     */
#define STREQ(x,y) (strcasecmp(x,y) == 0)

    if (STREQ(state.msg_attr.local, MAIL_ADDR_MAIL_DAEMON)
	|| STREQ(state.msg_attr.local, MAIL_ADDR_POSTMASTER)) {
	msg_warn("required alias not found: %s", state.msg_attr.local);
	dsb_simple(state.msg_attr.why, "2.0.0", "discarded");
	return (sent(BOUNCE_FLAGS(state.request), SENT_ATTR(state.msg_attr)));
    }

    /*
     * Bounce the message when no luser relay is specified.
     */
    dsb_simple(state.msg_attr.why, "5.1.1",
	       "unknown user: \"%s\"", state.msg_attr.local);
    return (bounce_append(BOUNCE_FLAGS(state.request),
			  BOUNCE_ATTR(state.msg_attr)));
}
Example #10
0
int     deliver_alias(LOCAL_STATE state, USER_ATTR usr_attr,
		              char *name, int *statusp)
{
    char   *myname = "deliver_alias";
    const char *alias_result;
    char   *expansion;
    char   *owner;
    char  **cpp;
    uid_t   alias_uid;
    struct mypasswd *alias_pwd;
    VSTRING *canon_owner;
    DICT   *dict;
    const char *owner_rhs;		/* owner alias, RHS */
    int     alias_count;

    /*
     * Make verbose logging easier to understand.
     */
    state.level++;
    if (msg_verbose)
	MSG_LOG_STATE(myname, state);

    /*
     * DUPLICATE/LOOP ELIMINATION
     * 
     * We cannot do duplicate elimination here. Sendmail compatibility requires
     * that we allow multiple deliveries to the same alias, even recursively!
     * For example, we must deliver to mailbox any messags that are addressed
     * to the alias of a user that lists that same alias in her own .forward
     * file. Yuck! This is just an example of some really perverse semantics
     * that people will expect Postfix to implement just like sendmail.
     * 
     * We can recognize one special case: when an alias includes its own name,
     * deliver to the user instead, just like sendmail. Otherwise, we just
     * bail out when nesting reaches some unreasonable depth, and blame it on
     * a possible alias loop.
     */
    if (state.msg_attr.exp_from != 0
	&& strcasecmp(state.msg_attr.exp_from, name) == 0)
	return (NO);
    if (state.level > 100) {
	msg_warn("possible alias database loop for %s", name);
	*statusp = bounce_append(BOUNCE_FLAGS(state.request),
				 BOUNCE_ATTR(state.msg_attr),
			       "possible alias database loop for %s", name);
	return (YES);
    }
    state.msg_attr.exp_from = name;

    /*
     * There are a bunch of roles that we're trying to keep track of.
     * 
     * First, there's the issue of whose rights should be used when delivering
     * to "|command" or to /file/name. With alias databases, the rights are
     * those of who owns the alias, i.e. the database owner. With aliases
     * owned by root, a default user is used instead. When an alias with
     * default rights references an include file owned by an ordinary user,
     * we must use the rights of the include file owner, otherwise the
     * include file owner could take control of the default account.
     * 
     * Secondly, there's the question of who to notify of delivery problems.
     * With aliases that have an owner- alias, the latter is used to set the
     * sender and owner attributes. Otherwise, the owner attribute is reset
     * (the alias is globally visible and could be sent to by anyone).
     */
    for (cpp = alias_maps->argv->argv; *cpp; cpp++) {
	if ((dict = dict_handle(*cpp)) == 0)
	    msg_panic("%s: dictionary not found: %s", myname, *cpp);
	if ((alias_result = dict_get(dict, name)) != 0) {
	    if (msg_verbose)
		msg_info("%s: %s: %s = %s", myname, *cpp, name, alias_result);

	    /*
	     * Don't expand a verify-only request.
	     */
	    if (state.request->flags & DEL_REQ_FLAG_VERIFY) {
		*statusp = sent(BOUNCE_FLAGS(state.request),
				SENT_ATTR(state.msg_attr),
				"aliased to %s", alias_result);
		return (YES);
	    }

	    /*
	     * DELIVERY POLICY
	     * 
	     * Update the expansion type attribute, so we can decide if
	     * deliveries to |command and /file/name are allowed at all.
	     */
	    state.msg_attr.exp_type = EXPAND_TYPE_ALIAS;

	    /*
	     * DELIVERY RIGHTS
	     * 
	     * What rights to use for |command and /file/name deliveries? The
	     * command and file code will use default rights when the alias
	     * database is owned by root, otherwise it will use the rights of
	     * the alias database owner.
	     */
	    if ((alias_uid = dict_owner(*cpp)) == 0) {
		alias_pwd = 0;
		RESET_USER_ATTR(usr_attr, state.level);
	    } else {
		if ((alias_pwd = mypwuid(alias_uid)) == 0) {
		    msg_warn("cannot find alias database owner for %s", *cpp);
		    *statusp = defer_append(BOUNCE_FLAGS(state.request),
					    BOUNCE_ATTR(state.msg_attr),
					"cannot find alias database owner");
		    return (YES);
		}
		SET_USER_ATTR(usr_attr, alias_pwd, state.level);
	    }

	    /*
	     * WHERE TO REPORT DELIVERY PROBLEMS.
	     * 
	     * Use the owner- alias if one is specified, otherwise reset the
	     * owner attribute and use the include file ownership if we can.
	     * Save the dict_lookup() result before something clobbers it.
	     * 
	     * Don't match aliases that are based on regexps.
	     */
#define STR(x)	vstring_str(x)
#define OWNER_ASSIGN(own) \
	    (own = (var_ownreq_special == 0 ? 0 : \
	    concatenate("owner-", name, (char *) 0)))

	    expansion = mystrdup(alias_result);
	    if (OWNER_ASSIGN(owner) != 0
		&& (owner_rhs = maps_find(alias_maps, owner, DICT_FLAG_NONE)) != 0) {
		canon_owner = canon_addr_internal(vstring_alloc(10),
				     var_exp_own_alias ? owner_rhs : owner);
		SET_OWNER_ATTR(state.msg_attr, STR(canon_owner), state.level);
	    } else {
		canon_owner = 0;
		RESET_OWNER_ATTR(state.msg_attr, state.level);
	    }

	    /*
	     * EXTERNAL LOOP CONTROL
	     * 
	     * Set the delivered message attribute to the recipient, so that
	     * this message will list the correct forwarding address.
	     */
	    state.msg_attr.delivered = state.msg_attr.recipient;

	    /*
	     * Deliver.
	     */
	    alias_count = 0;
	    *statusp =
		(dict_errno ?
		 defer_append(BOUNCE_FLAGS(state.request),
			      BOUNCE_ATTR(state.msg_attr),
			      "alias database unavailable") :
	    deliver_token_string(state, usr_attr, expansion, &alias_count));
#if 0
	    if (var_ownreq_special
		&& strncmp("owner-", state.msg_attr.sender, 6) != 0
		&& alias_count > 10)
		msg_warn("mailing list \"%s\" needs an \"owner-%s\" alias",
			 name, name);
#endif
	    if (alias_count < 1) {
		msg_warn("no recipient in alias lookup result for %s", name);
		*statusp = defer_append(BOUNCE_FLAGS(state.request),
					BOUNCE_ATTR(state.msg_attr),
					"alias database unavailable");
	    }
	    myfree(expansion);
	    if (owner)
		myfree(owner);
	    if (canon_owner)
		vstring_free(canon_owner);
	    if (alias_pwd)
		mypwfree(alias_pwd);
	    return (YES);
	}

	/*
	 * If the alias database was inaccessible for some reason, defer
	 * further delivery for the current top-level recipient.
	 */
	if (dict_errno != 0) {
	    *statusp = defer_append(BOUNCE_FLAGS(state.request),
				    BOUNCE_ATTR(state.msg_attr),
				    "alias database unavailable");
	    return (YES);
	} else {
	    if (msg_verbose)
		msg_info("%s: %s: %s not found", myname, *cpp, name);
	}
    }

    /*
     * Try delivery to a local user instead.
     */
    return (NO);
}
Example #11
0
static int tls_policy_lookup_one(SMTP_SESSION *session, int *site_level,
				         const char *site_name,
				         const char *site_class)
{
    const char *lookup;
    char   *policy;
    char   *saved_policy;
    char   *tok;
    const char *err;
    char   *name;
    char   *val;
    static VSTRING *cbuf;

#undef FREE_RETURN
#define FREE_RETURN(x) do { myfree(saved_policy); return (x); } while (0)

    if ((lookup = maps_find(tls_policy, site_name, 0)) == 0) {
	if (tls_policy->error) {
	    msg_fatal("%s: %s lookup error for %s",
		      session->state->request->queue_id,
		      tls_policy->title, site_name);
	    /* XXX session->stream has no longjmp context yet. */
	}
	return (0);
    }
    if (cbuf == 0)
	cbuf = vstring_alloc(10);

#define WHERE \
    vstring_str(vstring_sprintf(cbuf, "TLS policy table, %s \"%s\"", \
		site_class, site_name))

    saved_policy = policy = mystrdup(lookup);

    if ((tok = mystrtok(&policy, "\t\n\r ,")) == 0) {
	msg_warn("%s: invalid empty policy", WHERE);
	*site_level = TLS_LEV_INVALID;
	FREE_RETURN(1);				/* No further lookups */
    }
    *site_level = tls_level_lookup(tok);
    if (*site_level == TLS_LEV_INVALID) {
	/* tls_level_lookup() logs no warning. */
	msg_warn("%s: invalid security level \"%s\"", WHERE, tok);
	FREE_RETURN(1);				/* No further lookups */
    }

    /*
     * Warn about ignored attributes when TLS is disabled.
     */
    if (*site_level < TLS_LEV_MAY) {
	while ((tok = mystrtok(&policy, "\t\n\r ,")) != 0)
	    msg_warn("%s: ignoring attribute \"%s\" with TLS disabled",
		     WHERE, tok);
	FREE_RETURN(1);
    }

    /*
     * Errors in attributes may have security consequences, don't ignore
     * errors that can degrade security.
     */
    while ((tok = mystrtok(&policy, "\t\n\r ,")) != 0) {
	if ((err = split_nameval(tok, &name, &val)) != 0) {
	    *site_level = TLS_LEV_INVALID;
	    msg_warn("%s: malformed attribute/value pair \"%s\": %s",
		     WHERE, tok, err);
	    break;
	}
	/* Only one instance per policy. */
	if (!strcasecmp(name, "ciphers")) {
	    if (*val == 0) {
		msg_warn("%s: attribute \"%s\" has empty value", WHERE, name);
		*site_level = TLS_LEV_INVALID;
		break;
	    }
	    if (session->tls_grade) {
		msg_warn("%s: attribute \"%s\" is specified multiple times",
			 WHERE, name);
		*site_level = TLS_LEV_INVALID;
		break;
	    }
	    session->tls_grade = mystrdup(val);
	    continue;
	}
	/* Only one instance per policy. */
	if (!strcasecmp(name, "protocols")) {
	    if (session->tls_protocols) {
		msg_warn("%s: attribute \"%s\" is specified multiple times",
			 WHERE, name);
		*site_level = TLS_LEV_INVALID;
		break;
	    }
	    session->tls_protocols = mystrdup(val);
	    continue;
	}
	/* Multiple instance(s) per policy. */
	if (!strcasecmp(name, "match")) {
	    char   *delim = *site_level == TLS_LEV_FPRINT ? "|" : ":";

	    if (*site_level <= TLS_LEV_ENCRYPT) {
		msg_warn("%s: attribute \"%s\" invalid at security level \"%s\"",
			 WHERE, name, policy_name(*site_level));
		*site_level = TLS_LEV_INVALID;
		break;
	    }
	    if (*val == 0) {
		msg_warn("%s: attribute \"%s\" has empty value", WHERE, name);
		*site_level = TLS_LEV_INVALID;
		break;
	    }
	    if (session->tls_matchargv == 0)
		session->tls_matchargv = argv_split(val, delim);
	    else
		argv_split_append(session->tls_matchargv, val, delim);
	    continue;
	}
	/* Only one instance per policy. */
	if (!strcasecmp(name, "exclude")) {
	    if (session->tls_exclusions) {
		msg_warn("%s: attribute \"%s\" is specified multiple times",
			 WHERE, name);
		*site_level = TLS_LEV_INVALID;
		break;
	    }
	    session->tls_exclusions = vstring_strcpy(vstring_alloc(10), val);
	    continue;
	} else {
	    msg_warn("%s: invalid attribute name: \"%s\"", WHERE, name);
	    *site_level = TLS_LEV_INVALID;
	    break;
	}
    }
    FREE_RETURN(1);
}
Example #12
0
int     deliver_alias(LOCAL_STATE state, USER_ATTR usr_attr,
		              char *name, int *statusp)
{
    const char *myname = "deliver_alias";
    const char *alias_result;
    char   *saved_alias_result;
    char   *owner;
    char  **cpp;
    uid_t   alias_uid;
    struct mypasswd *alias_pwd;
    VSTRING *canon_owner;
    DICT   *dict;
    const char *owner_rhs;		/* owner alias, RHS */
    int     alias_count;
    int     dsn_notify;
    char   *dsn_envid;
    int     dsn_ret;
    const char *dsn_orcpt;

    /*
     * Make verbose logging easier to understand.
     */
    state.level++;
    if (msg_verbose)
	MSG_LOG_STATE(myname, state);

    /*
     * DUPLICATE/LOOP ELIMINATION
     * 
     * We cannot do duplicate elimination here. Sendmail compatibility requires
     * that we allow multiple deliveries to the same alias, even recursively!
     * For example, we must deliver to mailbox any messags that are addressed
     * to the alias of a user that lists that same alias in her own .forward
     * file. Yuck! This is just an example of some really perverse semantics
     * that people will expect Postfix to implement just like sendmail.
     * 
     * We can recognize one special case: when an alias includes its own name,
     * deliver to the user instead, just like sendmail. Otherwise, we just
     * bail out when nesting reaches some unreasonable depth, and blame it on
     * a possible alias loop.
     */
    if (state.msg_attr.exp_from != 0
	&& strcasecmp(state.msg_attr.exp_from, name) == 0)
	return (NO);
    if (state.level > 100) {
	msg_warn("alias database loop for %s", name);
	dsb_simple(state.msg_attr.why, "5.4.6",
		   "alias database loop for %s", name);
	*statusp = bounce_append(BOUNCE_FLAGS(state.request),
				 BOUNCE_ATTR(state.msg_attr));
	return (YES);
    }
    state.msg_attr.exp_from = name;

    /*
     * There are a bunch of roles that we're trying to keep track of.
     * 
     * First, there's the issue of whose rights should be used when delivering
     * to "|command" or to /file/name. With alias databases, the rights are
     * those of who owns the alias, i.e. the database owner. With aliases
     * owned by root, a default user is used instead. When an alias with
     * default rights references an include file owned by an ordinary user,
     * we must use the rights of the include file owner, otherwise the
     * include file owner could take control of the default account.
     * 
     * Secondly, there's the question of who to notify of delivery problems.
     * With aliases that have an owner- alias, the latter is used to set the
     * sender and owner attributes. Otherwise, the owner attribute is reset
     * (the alias is globally visible and could be sent to by anyone).
     */
    for (cpp = alias_maps->argv->argv; *cpp; cpp++) {
	if ((dict = dict_handle(*cpp)) == 0)
	    msg_panic("%s: dictionary not found: %s", myname, *cpp);
	if ((alias_result = dict_get(dict, name)) != 0) {
	    if (msg_verbose)
		msg_info("%s: %s: %s = %s", myname, *cpp, name, alias_result);

	    /*
	     * Don't expand a verify-only request.
	     */
	    if (state.request->flags & DEL_REQ_FLAG_MTA_VRFY) {
		dsb_simple(state.msg_attr.why, "2.0.0",
			   "aliased to %s", alias_result);
		*statusp = sent(BOUNCE_FLAGS(state.request),
				SENT_ATTR(state.msg_attr));
		return (YES);
	    }

	    /*
	     * DELIVERY POLICY
	     * 
	     * Update the expansion type attribute, so we can decide if
	     * deliveries to |command and /file/name are allowed at all.
	     */
	    state.msg_attr.exp_type = EXPAND_TYPE_ALIAS;

	    /*
	     * DELIVERY RIGHTS
	     * 
	     * What rights to use for |command and /file/name deliveries? The
	     * command and file code will use default rights when the alias
	     * database is owned by root, otherwise it will use the rights of
	     * the alias database owner.
	     */
	    if ((alias_uid = dict_owner(*cpp)) == 0) {
		alias_pwd = 0;
		RESET_USER_ATTR(usr_attr, state.level);
	    } else {
		if ((alias_pwd = mypwuid(alias_uid)) == 0) {
		    msg_warn("cannot find alias database owner for %s", *cpp);
		    dsb_simple(state.msg_attr.why, "4.3.0",
			       "cannot find alias database owner");
		    *statusp = defer_append(BOUNCE_FLAGS(state.request),
					    BOUNCE_ATTR(state.msg_attr));
		    return (YES);
		}
		SET_USER_ATTR(usr_attr, alias_pwd, state.level);
	    }

	    /*
	     * WHERE TO REPORT DELIVERY PROBLEMS.
	     * 
	     * Use the owner- alias if one is specified, otherwise reset the
	     * owner attribute and use the include file ownership if we can.
	     * Save the dict_lookup() result before something clobbers it.
	     * 
	     * Don't match aliases that are based on regexps.
	     */
#define OWNER_ASSIGN(own) \
	    (own = (var_ownreq_special == 0 ? 0 : \
	    concatenate("owner-", name, (char *) 0)))

	    saved_alias_result = mystrdup(alias_result);
	    if (OWNER_ASSIGN(owner) != 0
		&& (owner_rhs = maps_find(alias_maps, owner, DICT_FLAG_NONE)) != 0) {
		canon_owner = canon_addr_internal(vstring_alloc(10),
				     var_exp_own_alias ? owner_rhs : owner);
		/* Set envelope sender and owner attribute. */
		SET_OWNER_ATTR(state.msg_attr, STR(canon_owner), state.level);
	    } else {
		canon_owner = 0;
		/* Note: this does not reset the envelope sender. */
		if (var_reset_owner_attr)
		    RESET_OWNER_ATTR(state.msg_attr, state.level);
	    }

	    /*
	     * EXTERNAL LOOP CONTROL
	     * 
	     * Set the delivered message attribute to the recipient, so that
	     * this message will list the correct forwarding address.
	     */
	    if (var_frozen_delivered == 0)
		state.msg_attr.delivered = state.msg_attr.rcpt.address;

	    /*
	     * Deliver.
	     */
	    alias_count = 0;
	    if (dict_errno != 0) {
		dsb_simple(state.msg_attr.why, "4.3.0",
			   "alias database unavailable");
		*statusp = defer_append(BOUNCE_FLAGS(state.request),
					BOUNCE_ATTR(state.msg_attr));
	    } else {

		/*
		 * XXX DSN
		 * 
		 * When delivering to a mailing list (i.e. the envelope sender
		 * is replaced) the ENVID, NOTIFY, RET, and ORCPT parameters
		 * which accompany the redistributed message MUST NOT be
		 * derived from those of the original message.
		 * 
		 * When delivering to an alias (i.e. the envelope sender is not
		 * replaced) any ENVID, RET, or ORCPT parameters are
		 * propagated to all forwarding addresses associated with
		 * that alias.  The NOTIFY parameter is propagated to the
		 * forwarding addresses, except that any SUCCESS keyword is
		 * removed.
		 */
#define DSN_SAVE_UPDATE(saved, old, new) do { \
	saved = old; \
	old = new; \
    } while (0)

		DSN_SAVE_UPDATE(dsn_notify, state.msg_attr.rcpt.dsn_notify,
				dsn_notify == DSN_NOTIFY_SUCCESS ?
				DSN_NOTIFY_NEVER :
				dsn_notify & ~DSN_NOTIFY_SUCCESS);
		if (canon_owner != 0) {
		    DSN_SAVE_UPDATE(dsn_envid, state.msg_attr.dsn_envid, "");
		    DSN_SAVE_UPDATE(dsn_ret, state.msg_attr.dsn_ret, 0);
		    DSN_SAVE_UPDATE(dsn_orcpt, state.msg_attr.rcpt.dsn_orcpt, "");
		    state.msg_attr.rcpt.orig_addr = "";
		}
		*statusp =
		    deliver_token_string(state, usr_attr, saved_alias_result,
					 &alias_count);
#if 0
		if (var_ownreq_special
		    && strncmp("owner-", state.msg_attr.sender, 6) != 0
		    && alias_count > 10)
		    msg_warn("mailing list \"%s\" needs an \"owner-%s\" alias",
			     name, name);
#endif
		if (alias_count < 1) {
		    msg_warn("no recipient in alias lookup result for %s", name);
		    dsb_simple(state.msg_attr.why, "4.3.0",
			       "alias database unavailable");
		    *statusp = defer_append(BOUNCE_FLAGS(state.request),
					    BOUNCE_ATTR(state.msg_attr));
		} else {

		    /*
		     * XXX DSN
		     * 
		     * When delivering to a mailing list (i.e. the envelope
		     * sender address is replaced) and NOTIFY=SUCCESS was
		     * specified, report a DSN of "delivered".
		     * 
		     * When delivering to an alias (i.e. the envelope sender
		     * address is not replaced) and NOTIFY=SUCCESS was
		     * specified, report a DSN of "expanded".
		     */
		    if (dsn_notify & DSN_NOTIFY_SUCCESS) {
			state.msg_attr.rcpt.dsn_notify = dsn_notify;
			if (canon_owner != 0) {
			    state.msg_attr.dsn_envid = dsn_envid;
			    state.msg_attr.dsn_ret = dsn_ret;
			    state.msg_attr.rcpt.dsn_orcpt = dsn_orcpt;
			}
			dsb_update(state.msg_attr.why, "2.0.0", canon_owner ?
				   "delivered" : "expanded",
				   DSB_SKIP_RMTA, DSB_SKIP_REPLY,
				   "alias expanded");
			(void) trace_append(BOUNCE_FLAG_NONE,
					    SENT_ATTR(state.msg_attr));
		    }
		}
	    }
	    myfree(saved_alias_result);
	    if (owner)
		myfree(owner);
	    if (canon_owner)
		vstring_free(canon_owner);
	    if (alias_pwd)
		mypwfree(alias_pwd);
	    return (YES);
	}

	/*
	 * If the alias database was inaccessible for some reason, defer
	 * further delivery for the current top-level recipient.
	 */
	if (dict_errno != 0) {
	    dsb_simple(state.msg_attr.why, "4.3.0",
		       "alias database unavailable");
	    *statusp = defer_append(BOUNCE_FLAGS(state.request),
				    BOUNCE_ATTR(state.msg_attr));
	    return (YES);
	} else {
	    if (msg_verbose)
		msg_info("%s: %s: %s not found", myname, *cpp, name);
	}
    }

    /*
     * Try delivery to a local user instead.
     */
    return (NO);
}
Example #13
0
int     deliver_mailbox(LOCAL_STATE state, USER_ATTR usr_attr, int *statusp)
{
    const char *myname = "deliver_mailbox";
    int     status;
    struct mypasswd *mbox_pwd;
    char   *path;
    static MAPS *transp_maps;
    const char *map_transport;
    static MAPS *cmd_maps;
    const char *map_command;

    /*
     * Make verbose logging easier to understand.
     */
    state.level++;
    if (msg_verbose)
	MSG_LOG_STATE(myname, state);

    /*
     * DUPLICATE ELIMINATION
     * 
     * Don't come here more than once, whether or not the recipient exists.
     */
    if (been_here(state.dup_filter, "mailbox %s", state.msg_attr.local))
	return (YES);

    /*
     * Delegate mailbox delivery to another message transport.
     */
    if (*var_mbox_transp_maps && transp_maps == 0)
	transp_maps = maps_create(VAR_MBOX_TRANSP_MAPS, var_mbox_transp_maps,
				  DICT_FLAG_LOCK | DICT_FLAG_NO_REGSUB);
    /* The -1 is a hint for the down-stream deliver_completed() function. */
    if (transp_maps
	&& (map_transport = maps_find(transp_maps, state.msg_attr.user,
				      DICT_FLAG_NONE)) != 0) {
	state.msg_attr.rcpt.offset = -1L;
	*statusp = deliver_pass(MAIL_CLASS_PRIVATE, map_transport,
				state.request, &state.msg_attr.rcpt);
	return (YES);
    } else if (transp_maps && transp_maps->error != 0) {
	/* Details in the logfile. */
	dsb_simple(state.msg_attr.why, "4.3.0", "table lookup failure");
	*statusp = defer_append(BOUNCE_FLAGS(state.request),
				BOUNCE_ATTR(state.msg_attr));
	return (YES);
    }
    if (*var_mailbox_transport) {
	state.msg_attr.rcpt.offset = -1L;
	*statusp = deliver_pass(MAIL_CLASS_PRIVATE, var_mailbox_transport,
				state.request, &state.msg_attr.rcpt);
	return (YES);
    }

    /*
     * Skip delivery when this recipient does not exist.
     */
    if ((errno = mypwnam_err(state.msg_attr.user, &mbox_pwd)) != 0) {
	msg_warn("error looking up passwd info for %s: %m",
		 state.msg_attr.user);
	dsb_simple(state.msg_attr.why, "4.0.0", "user lookup error");
	*statusp = defer_append(BOUNCE_FLAGS(state.request),
				BOUNCE_ATTR(state.msg_attr));
	return (YES);
    }
    if (mbox_pwd == 0)
	return (NO);

    /*
     * No early returns or we have a memory leak.
     */

    /*
     * DELIVERY RIGHTS
     * 
     * Use the rights of the recipient user.
     */
    SET_USER_ATTR(usr_attr, mbox_pwd, state.level);

    /*
     * Deliver to mailbox, maildir or to external command.
     */
#define LAST_CHAR(s) (s[strlen(s) - 1])

    if (*var_mailbox_cmd_maps && cmd_maps == 0)
	cmd_maps = maps_create(VAR_MAILBOX_CMD_MAPS, var_mailbox_cmd_maps,
			       DICT_FLAG_LOCK | DICT_FLAG_PARANOID);

    if (cmd_maps && (map_command = maps_find(cmd_maps, state.msg_attr.user,
				    DICT_FLAG_NONE)) != 0) {
	status = deliver_command(state, usr_attr, map_command);
    } else if (cmd_maps && cmd_maps->error != 0) {
	/* Details in the logfile. */
	dsb_simple(state.msg_attr.why, "4.3.0", "table lookup failure");
	status = defer_append(BOUNCE_FLAGS(state.request),
			      BOUNCE_ATTR(state.msg_attr));
    } else if (*var_mailbox_command) {
	status = deliver_command(state, usr_attr, var_mailbox_command);
    } else if (*var_home_mailbox && LAST_CHAR(var_home_mailbox) == '/') {
	path = concatenate(usr_attr.home, "/", var_home_mailbox, (char *) 0);
	status = deliver_maildir(state, usr_attr, path);
	myfree(path);
    } else if (*var_mail_spool_dir && LAST_CHAR(var_mail_spool_dir) == '/') {
	path = concatenate(var_mail_spool_dir, state.msg_attr.user,
			   "/", (char *) 0);
	status = deliver_maildir(state, usr_attr, path);
	myfree(path);
    } else
	status = deliver_mailbox_file(state, usr_attr);

    /*
     * Cleanup.
     */
    mypwfree(mbox_pwd);
    *statusp = status;
    return (YES);
}
static void tls_policy_lookup_one(SMTP_TLS_POLICY *tls, int *site_level,
				          const char *site_name,
				          const char *site_class)
{
    const char *lookup;
    char   *policy;
    char   *saved_policy;
    char   *tok;
    const char *err;
    char   *name;
    char   *val;
    static VSTRING *cbuf;

#undef FREE_RETURN
#define FREE_RETURN do { myfree(saved_policy); return; } while (0)

#define INVALID_RETURN(why, levelp) do { \
	    MARK_INVALID((why), (levelp)); FREE_RETURN; } while (0)

#define WHERE \
    STR(vstring_sprintf(cbuf, "%s, %s \"%s\"", \
		tls_policy->title, site_class, site_name))

    if (cbuf == 0)
	cbuf = vstring_alloc(10);

    if ((lookup = maps_find(tls_policy, site_name, 0)) == 0) {
	if (tls_policy->error) {
	    msg_warn("%s: policy table lookup error", WHERE);
	    MARK_INVALID(tls->why, site_level);
	}
	return;
    }
    saved_policy = policy = mystrdup(lookup);

    if ((tok = mystrtok(&policy, "\t\n\r ,")) == 0) {
	msg_warn("%s: invalid empty policy", WHERE);
	INVALID_RETURN(tls->why, site_level);
    }
    *site_level = tls_level_lookup(tok);
    if (*site_level == TLS_LEV_INVALID) {
	/* tls_level_lookup() logs no warning. */
	msg_warn("%s: invalid security level \"%s\"", WHERE, tok);
	INVALID_RETURN(tls->why, site_level);
    }

    /*
     * Warn about ignored attributes when TLS is disabled.
     */
    if (*site_level < TLS_LEV_MAY) {
	while ((tok = mystrtok(&policy, "\t\n\r ,")) != 0)
	    msg_warn("%s: ignoring attribute \"%s\" with TLS disabled",
		     WHERE, tok);
	FREE_RETURN;
    }

    /*
     * Errors in attributes may have security consequences, don't ignore
     * errors that can degrade security.
     */
    while ((tok = mystrtok(&policy, "\t\n\r ,")) != 0) {
	if ((err = split_nameval(tok, &name, &val)) != 0) {
	    msg_warn("%s: malformed attribute/value pair \"%s\": %s",
		     WHERE, tok, err);
	    INVALID_RETURN(tls->why, site_level);
	}
	/* Only one instance per policy. */
	if (!strcasecmp(name, "ciphers")) {
	    if (*val == 0) {
		msg_warn("%s: attribute \"%s\" has empty value", WHERE, name);
		INVALID_RETURN(tls->why, site_level);
	    }
	    if (tls->grade) {
		msg_warn("%s: attribute \"%s\" is specified multiple times",
			 WHERE, name);
		INVALID_RETURN(tls->why, site_level);
	    }
	    tls->grade = mystrdup(val);
	    continue;
	}
	/* Only one instance per policy. */
	if (!strcasecmp(name, "protocols")) {
	    if (tls->protocols) {
		msg_warn("%s: attribute \"%s\" is specified multiple times",
			 WHERE, name);
		INVALID_RETURN(tls->why, site_level);
	    }
	    tls->protocols = mystrdup(val);
	    continue;
	}
	/* Multiple instances per policy. */
	if (!strcasecmp(name, "match")) {
	    if (*val == 0) {
		msg_warn("%s: attribute \"%s\" has empty value", WHERE, name);
		INVALID_RETURN(tls->why, site_level);
	    }
	    switch (*site_level) {
	    default:
		msg_warn("%s: attribute \"%s\" invalid at security level "
			 "\"%s\"", WHERE, name, policy_name(*site_level));
		INVALID_RETURN(tls->why, site_level);
		break;
	    case TLS_LEV_FPRINT:
		if (!tls->dane)
		    tls->dane = tls_dane_alloc();
		tls_dane_add_ee_digests(tls->dane,
					var_smtp_tls_fpt_dgst, val, "|");
		break;
	    case TLS_LEV_VERIFY:
	    case TLS_LEV_SECURE:
		if (tls->matchargv == 0)
		    tls->matchargv = argv_split(val, ":");
		else
		    argv_split_append(tls->matchargv, val, ":");
		break;
	    }
	    continue;
	}
	/* Only one instance per policy. */
	if (!strcasecmp(name, "exclude")) {
	    if (tls->exclusions) {
		msg_warn("%s: attribute \"%s\" is specified multiple times",
			 WHERE, name);
		INVALID_RETURN(tls->why, site_level);
	    }
	    tls->exclusions = vstring_strcpy(vstring_alloc(10), val);
	    continue;
	}
	/* Multiple instances per policy. */
	if (!strcasecmp(name, "tafile")) {
	    /* Only makes sense if we're using CA-based trust */
	    if (!TLS_MUST_PKIX(*site_level)) {
		msg_warn("%s: attribute \"%s\" invalid at security level"
			 " \"%s\"", WHERE, name, policy_name(*site_level));
		INVALID_RETURN(tls->why, site_level);
	    }
	    if (*val == 0) {
		msg_warn("%s: attribute \"%s\" has empty value", WHERE, name);
		INVALID_RETURN(tls->why, site_level);
	    }
	    if (!tls->dane)
		tls->dane = tls_dane_alloc();
	    if (!tls_dane_load_trustfile(tls->dane, val)) {
		INVALID_RETURN(tls->why, site_level);
	    }
	    continue;
	}
	msg_warn("%s: invalid attribute name: \"%s\"", WHERE, name);
	INVALID_RETURN(tls->why, site_level);
    }

    FREE_RETURN;
}
Example #15
0
const char *mail_addr_find(MAPS *path, const char *address, char **extp)
{
    const char *myname = "mail_addr_find";
    const char *result;
    char   *ratsign = 0;
    char   *full_key;
    char   *bare_key;
    char   *saved_ext;
    int     rc = 0;

    /*
     * Initialize.
     */
    full_key = mystrdup(address);
    if (*var_rcpt_delim == 0) {
	bare_key = saved_ext = 0;
    } else {
	bare_key = strip_addr(full_key, &saved_ext, *var_rcpt_delim);
    }

    /*
     * Try user+foo@domain and user@domain.
     * 
     * Specify what keys are partial or full, to avoid matching partial
     * addresses with regular expressions.
     */
#define FULL	0
#define PARTIAL	DICT_FLAG_FIXED

    if ((result = maps_find(path, full_key, FULL)) == 0 && path->error == 0
      && bare_key != 0 && (result = maps_find(path, bare_key, PARTIAL)) != 0
	&& extp != 0) {
	*extp = saved_ext;
	saved_ext = 0;
    }

    /*
     * Try user+foo@$myorigin, user+foo@$mydestination or
     * user+foo@[${proxy,inet}_interfaces]. Then try with +foo stripped off.
     */
    if (result == 0 && path->error == 0
	&& (ratsign = strrchr(full_key, '@')) != 0
	&& (strcasecmp(ratsign + 1, var_myorigin) == 0
	    || (rc = resolve_local(ratsign + 1)) > 0)) {
	*ratsign = 0;
	result = maps_find(path, full_key, PARTIAL);
	if (result == 0 && path->error == 0 && bare_key != 0) {
	    if ((ratsign = strrchr(bare_key, '@')) == 0)
		msg_panic("%s: bare key botch", myname);
	    *ratsign = 0;
	    if ((result = maps_find(path, bare_key, PARTIAL)) != 0 && extp != 0) {
		*extp = saved_ext;
		saved_ext = 0;
	    }
	}
	*ratsign = '@';
    } else if (rc < 0)
	path->error = rc;

    /*
     * Try @domain.
     */
    if (result == 0 && path->error == 0 && ratsign)
	result = maps_find(path, ratsign, PARTIAL);

    /*
     * Clean up.
     */
    if (msg_verbose)
	msg_info("%s: %s -> %s", myname, address,
		 result ? result :
		 path->error ? "(try again)" :
		 "(not found)");
    myfree(full_key);
    if (bare_key)
	myfree(bare_key);
    if (saved_ext)
	myfree(saved_ext);

    return (result);
}
Example #16
0
const char *mail_addr_find_opt(MAPS *path, const char *address, char **extp,
			               int in_form, int query_form,
			               int out_form, int strategy)
{
    const char *myname = "mail_addr_find";
    VSTRING *ext_addr_buf = 0;
    VSTRING *int_addr_buf = 0;
    const char *int_addr;
    static VSTRING *int_result = 0;
    const char *result;
    char   *ratsign = 0;
    char   *int_full_key;
    char   *int_bare_key;
    char   *saved_ext;
    int     rc = 0;

    /*
     * Optionally convert the address from external form.
     */
    if (in_form == MA_FORM_EXTERNAL) {
	int_addr_buf = vstring_alloc(100);
	unquote_822_local(int_addr_buf, address);
	int_addr = STR(int_addr_buf);
    } else {
	int_addr = address;
    }
    if (query_form == MA_FORM_EXTERNAL_FIRST
	|| query_form == MA_FORM_EXTERNAL)
	ext_addr_buf = vstring_alloc(100);

    /*
     * Initialize.
     */
    int_full_key = mystrdup(int_addr);
    if (*var_rcpt_delim == 0 || (strategy & MA_FIND_NOEXT) == 0) {
	int_bare_key = saved_ext = 0;
    } else {
	/* XXX This could be done after user+foo@domain fails. */
	int_bare_key =
	    strip_addr_internal(int_full_key, &saved_ext, var_rcpt_delim);
    }

    /*
     * Try user+foo@domain and user@domain.
     */
    if ((strategy & MA_FIND_FULL) != 0) {
	result = find_addr(path, int_full_key, FULL, WITH_DOMAIN,
			   query_form, ext_addr_buf);
    } else {
	result = 0;
	path->error = 0;
    }

    if (result == 0 && path->error == 0 && int_bare_key != 0
	&& (result = find_addr(path, int_bare_key, PARTIAL, WITH_DOMAIN,
			       query_form, ext_addr_buf)) != 0
	&& extp != 0) {
	*extp = saved_ext;
	saved_ext = 0;
    }

    /*
     * Try user+foo if the domain matches user+foo@$myorigin,
     * user+foo@$mydestination or user+foo@[${proxy,inet}_interfaces]. Then
     * try with +foo stripped off.
     */
    if (result == 0 && path->error == 0
	&& (ratsign = strrchr(int_full_key, '@')) != 0
	&& (strategy & (MA_FIND_LOCALPART_IF_LOCAL
			| MA_FIND_LOCALPART_AT_IF_LOCAL)) != 0) {
	if (strcasecmp_utf8(ratsign + 1, var_myorigin) == 0
	    || (rc = resolve_local(ratsign + 1)) > 0) {
	    if ((strategy & MA_FIND_LOCALPART_IF_LOCAL) != 0)
		result = find_local(path, ratsign, 0, int_full_key,
				 int_bare_key, query_form, extp, &saved_ext,
				    ext_addr_buf);
	    if (result == 0 && path->error == 0
		&& (strategy & MA_FIND_LOCALPART_AT_IF_LOCAL) != 0)
		result = find_local(path, ratsign, 1, int_full_key,
				 int_bare_key, query_form, extp, &saved_ext,
				    ext_addr_buf);
	} else if (rc < 0)
	    path->error = rc;
    }

    /*
     * Try @domain.
     */
    if (result == 0 && path->error == 0 && ratsign != 0
	&& (strategy & MA_FIND_AT_DOMAIN) != 0)
	result = maps_find(path, ratsign, PARTIAL);

    /*
     * Try domain (optionally, subdomains).
     */
    if (result == 0 && path->error == 0 && ratsign != 0
	&& (strategy & MA_FIND_DOMAIN) != 0) {
	const char *name;
	const char *next;

	if ((strategy & MA_FIND_PDMS) && (strategy & MA_FIND_PDDMDS))
	    msg_warn("mail_addr_find_opt: do not specify both "
		     "MA_FIND_PDMS and MA_FIND_PDDMDS");
	for (name = ratsign + 1; *name != 0; name = next) {
	    if ((result = maps_find(path, name, PARTIAL)) != 0
		|| path->error != 0
		|| (strategy & (MA_FIND_PDMS | MA_FIND_PDDMDS)) == 0
		|| (next = strchr(name + 1, '.')) == 0)
		break;
	    if ((strategy & MA_FIND_PDDMDS) == 0)
		next++;
	}
    }

    /*
     * Try localpart@ even if the domain is not local.
     */
    if ((strategy & MA_FIND_LOCALPART_AT) != 0 \
	&&result == 0 && path->error == 0)
	result = find_local(path, ratsign, 1, int_full_key,
			    int_bare_key, query_form, extp, &saved_ext,
			    ext_addr_buf);

    /*
     * Optionally convert the result to internal form. The lookup result is
     * supposed to be one external-form email address.
     */
    if (result != 0 && out_form == MA_FORM_INTERNAL) {
	if (int_result == 0)
	    int_result = vstring_alloc(100);
	unquote_822_local(int_result, result);
	result = STR(int_result);
    }

    /*
     * Clean up.
     */
    if (msg_verbose)
	msg_info("%s: %s -> %s", myname, address,
		 result ? result :
		 path->error ? "(try again)" :
		 "(not found)");
    myfree(int_full_key);
    if (int_bare_key)
	myfree(int_bare_key);
    if (saved_ext)
	myfree(saved_ext);
    if (int_addr_buf)
	vstring_free(int_addr_buf);
    if (ext_addr_buf)
	vstring_free(ext_addr_buf);
    return (result);
}