diff options
Diffstat (limited to 'src/smtpd/smtpd_check.c')
-rw-r--r-- | src/smtpd/smtpd_check.c | 6289 |
1 files changed, 6289 insertions, 0 deletions
diff --git a/src/smtpd/smtpd_check.c b/src/smtpd/smtpd_check.c new file mode 100644 index 0000000..31ed00f --- /dev/null +++ b/src/smtpd/smtpd_check.c @@ -0,0 +1,6289 @@ +/*++ +/* NAME +/* smtpd_check 3 +/* SUMMARY +/* SMTP client request filtering +/* SYNOPSIS +/* #include "smtpd.h" +/* #include "smtpd_check.h" +/* +/* void smtpd_check_init() +/* +/* int smtpd_check_addr(sender, address, smtputf8) +/* const char *sender; +/* const char *address; +/* int smtputf8; +/* +/* char *smtpd_check_rewrite(state) +/* SMTPD_STATE *state; +/* +/* char *smtpd_check_client(state) +/* SMTPD_STATE *state; +/* +/* char *smtpd_check_helo(state, helohost) +/* SMTPD_STATE *state; +/* char *helohost; +/* +/* char *smtpd_check_mail(state, sender) +/* SMTPD_STATE *state; +/* char *sender; +/* +/* char *smtpd_check_rcpt(state, recipient) +/* SMTPD_STATE *state; +/* char *recipient; +/* +/* char *smtpd_check_etrn(state, destination) +/* SMTPD_STATE *state; +/* char *destination; +/* +/* char *smtpd_check_data(state) +/* SMTPD_STATE *state; +/* +/* char *smtpd_check_eod(state) +/* SMTPD_STATE *state; +/* +/* char *smtpd_check_size(state, size) +/* SMTPD_STATE *state; +/* off_t size; +/* +/* char *smtpd_check_queue(state) +/* SMTPD_STATE *state; +/* DESCRIPTION +/* This module implements additional checks on SMTP client requests. +/* A client request is validated in the context of the session state. +/* The result is either an error response (including the numerical +/* code) or the result is a null pointer in case of success. +/* +/* smtpd_check_init() initializes. This function should be called +/* once during the process life time. +/* +/* smtpd_check_addr() sanity checks an email address and returns +/* non-zero in case of badness. The sender argument provides sender +/* context for address resolution and caching, or a null pointer +/* if information is unavailable. +/* +/* smtpd_check_rewrite() should be called before opening a queue +/* file or proxy connection, in order to establish the proper +/* header address rewriting context. +/* +/* Each of the following routines scrutinizes the argument passed to +/* an SMTP command such as HELO, MAIL FROM, RCPT TO, or scrutinizes +/* the initial client connection request. The administrator can +/* specify what restrictions apply. +/* +/* Restrictions are specified via configuration parameters named +/* \fIsmtpd_{client,helo,sender,recipient}_restrictions.\fR Each +/* configuration parameter specifies a list of zero or more +/* restrictions that are applied in the order as specified. +/* .PP +/* smtpd_check_client() validates the client host name or address. +/* Relevant configuration parameters: +/* .IP smtpd_client_restrictions +/* Restrictions on the names or addresses of clients that may connect +/* to this SMTP server. +/* .PP +/* smtpd_check_helo() validates the hostname provided with the +/* HELO/EHLO commands. Relevant configuration parameters: +/* .IP smtpd_helo_restrictions +/* Restrictions on the hostname that is sent with the HELO/EHLO +/* command. +/* .PP +/* smtpd_check_mail() validates the sender address provided with +/* a MAIL FROM request. Relevant configuration parameters: +/* .IP smtpd_sender_restrictions +/* Restrictions on the sender address that is sent with the MAIL FROM +/* command. +/* .PP +/* smtpd_check_rcpt() validates the recipient address provided +/* with an RCPT TO request. Relevant configuration parameters: +/* .IP smtpd_recipient_restrictions +/* Restrictions on the recipient address that is sent with the RCPT +/* TO command. +/* .IP local_recipient_maps +/* Tables of user names (not addresses) that exist in $mydestination. +/* Mail for local users not in these tables is rejected. +/* .PP +/* smtpd_check_etrn() validates the domain name provided with the +/* ETRN command, and other client-provided information. Relevant +/* configuration parameters: +/* .IP smtpd_etrn_restrictions +/* Restrictions on the hostname that is sent with the HELO/EHLO +/* command. +/* .PP +/* smtpd_check_size() checks if a message with the given size can +/* be received (zero means that the message size is unknown). The +/* message is rejected when +/* the message size exceeds the non-zero bound specified with the +/* \fImessage_size_limit\fR configuration parameter. This is a +/* permanent error. +/* +/* smtpd_check_queue() checks the available queue file system +/* space. The message is rejected when: +/* .IP \(bu +/* The available queue file system space is less than the amount +/* specified with the \fImin_queue_free\fR configuration parameter. +/* This is a temporary error. +/* .IP \(bu +/* The available queue file system space is less than twice the +/* message size limit. This is a temporary error. +/* .PP +/* smtpd_check_data() enforces generic restrictions after the +/* client has sent the DATA command. +/* +/* smtpd_check_eod() enforces generic restrictions after the +/* client has sent the END-OF-DATA command. +/* +/* Arguments: +/* .IP name +/* The client hostname, or \fIunknown\fR. +/* .IP addr +/* The client address. +/* .IP helohost +/* The hostname given with the HELO command. +/* .IP sender +/* The sender address given with the MAIL FROM command. +/* .IP recipient +/* The recipient address given with the RCPT TO or VRFY command. +/* .IP size +/* The message size given with the MAIL FROM command (zero if unknown). +/* BUGS +/* Policies like these should not be hard-coded in C, but should +/* be user-programmable instead. +/* SEE ALSO +/* namadr_list(3) host access control +/* domain_list(3) domain access control +/* fsspace(3) free file system space +/* LICENSE +/* .ad +/* .fi +/* The Secure Mailer license must be distributed with this software. +/* AUTHOR(S) +/* Wietse Venema +/* IBM T.J. Watson Research +/* P.O. Box 704 +/* Yorktown Heights, NY 10598, USA +/* +/* Wietse Venema +/* Google, Inc. +/* 111 8th Avenue +/* New York, NY 10011, USA +/* +/* TLS support originally by: +/* Lutz Jaenicke +/* BTU Cottbus +/* Allgemeine Elektrotechnik +/* Universitaetsplatz 3-4 +/* D-03044 Cottbus, Germany +/*--*/ + +/* System library. */ + +#include <sys_defs.h> +#include <sys/socket.h> +#include <netinet/in.h> +#include <arpa/inet.h> +#include <string.h> +#include <ctype.h> +#include <stdarg.h> +#include <netdb.h> +#include <setjmp.h> +#include <stdlib.h> +#include <unistd.h> +#include <errno.h> + +#ifdef STRCASECMP_IN_STRINGS_H +#include <strings.h> +#endif + +/* Utility library. */ + +#include <msg.h> +#include <vstring.h> +#include <split_at.h> +#include <fsspace.h> +#include <stringops.h> +#include <valid_hostname.h> +#include <argv.h> +#include <mymalloc.h> +#include <dict.h> +#include <htable.h> +#include <ctable.h> +#include <mac_expand.h> +#include <attr_clnt.h> +#include <myaddrinfo.h> +#include <inet_proto.h> +#include <ip_match.h> +#include <valid_utf8_hostname.h> +#include <midna_domain.h> +#include <mynetworks.h> + +/* DNS library. */ + +#include <dns.h> + +/* Global library. */ + +#include <string_list.h> +#include <namadr_list.h> +#include <domain_list.h> +#include <mail_params.h> +#include <resolve_clnt.h> +#include <mail_error.h> +#include <resolve_local.h> +#include <own_inet_addr.h> +#include <mail_conf.h> +#include <maps.h> +#include <mail_addr_find.h> +#include <match_parent_style.h> +#include <strip_addr.h> +#include <cleanup_user.h> +#include <record.h> +#include <rec_type.h> +#include <mail_proto.h> +#include <mail_addr.h> +#include <verify_clnt.h> +#include <input_transp.h> +#include <is_header.h> +#include <valid_mailhost_addr.h> +#include <dsn_util.h> +#include <conv_time.h> +#include <xtext.h> +#include <smtp_stream.h> +#include <attr_override.h> + +/* Application-specific. */ + +#include "smtpd.h" +#include "smtpd_sasl_glue.h" +#include "smtpd_check.h" +#include "smtpd_dsn_fix.h" +#include "smtpd_resolve.h" +#include "smtpd_expand.h" + + /* + * Eject seat in case of parsing problems. + */ +static jmp_buf smtpd_check_buf; + + /* + * Results of restrictions. Errors are negative; see dict.h. + */ +#define SMTPD_CHECK_DUNNO 0 /* indifferent */ +#define SMTPD_CHECK_OK 1 /* explicitly permit */ +#define SMTPD_CHECK_REJECT 2 /* explicitly reject */ + + /* + * Intermediate results. These are static to avoid unnecessary stress on the + * memory manager routines. + */ +static VSTRING *error_text; +static CTABLE *smtpd_rbl_cache; +static CTABLE *smtpd_rbl_byte_cache; + + /* + * Pre-opened SMTP recipient maps so we can reject mail for unknown users. + * XXX This does not belong here and will eventually become part of the + * trivial-rewrite resolver. + */ +static MAPS *local_rcpt_maps; +static MAPS *send_canon_maps; +static MAPS *rcpt_canon_maps; +static MAPS *canonical_maps; +static MAPS *virt_alias_maps; +static MAPS *virt_mailbox_maps; +static MAPS *relay_rcpt_maps; + +#ifdef TEST + +static STRING_LIST *virt_alias_doms; +static STRING_LIST *virt_mailbox_doms; + +#endif + + /* + * Response templates for various rbl domains. + */ +static MAPS *rbl_reply_maps; + + /* + * Pre-opened sender to login name mapping. + */ +static MAPS *smtpd_sender_login_maps; + + /* + * Pre-opened access control lists. + */ +static DOMAIN_LIST *relay_domains; +static NAMADR_LIST *mynetworks_curr; +static NAMADR_LIST *mynetworks_new; +static NAMADR_LIST *perm_mx_networks; + +#ifdef USE_TLS +static MAPS *relay_ccerts; + +#endif + + /* + * How to do parent domain wildcard matching, if any. + */ +static int access_parent_style; + + /* + * Pre-parsed restriction lists. + */ +static ARGV *client_restrctions; +static ARGV *helo_restrctions; +static ARGV *mail_restrctions; +static ARGV *relay_restrctions; +static ARGV *fake_relay_restrctions; +static ARGV *rcpt_restrctions; +static ARGV *etrn_restrctions; +static ARGV *data_restrctions; +static ARGV *eod_restrictions; + +static HTABLE *smtpd_rest_classes; +static HTABLE *policy_clnt_table; +static HTABLE *map_command_table; + +static ARGV *local_rewrite_clients; + + /* + * The routine that recursively applies restrictions. + */ +static int generic_checks(SMTPD_STATE *, ARGV *, const char *, const char *, const char *); + + /* + * Recipient table check. + */ +static int check_sender_rcpt_maps(SMTPD_STATE *, const char *); +static int check_recipient_rcpt_maps(SMTPD_STATE *, const char *); +static int check_rcpt_maps(SMTPD_STATE *, const char *, const char *, + const char *); + + /* + * Tempfail actions; + */ +static int unk_name_tf_act; +static int unk_addr_tf_act; +static int unv_rcpt_tf_act; +static int unv_from_tf_act; + + /* + * Optional permit logging. + */ +static STRING_LIST *smtpd_acl_perm_log; + + /* + * YASLM. + */ +#define STR vstring_str +#define CONST_STR(x) ((const char *) vstring_str(x)) +#define UPDATE_STRING(ptr,val) { if (ptr) myfree(ptr); ptr = mystrdup(val); } + + /* + * If some decision can't be made due to a temporary error, then change + * other decisions into deferrals. + * + * XXX Deferrals can be postponed only with restrictions that are based on + * client-specified information: this restricts their use to parameters + * given in HELO, MAIL FROM, RCPT TO commands. + * + * XXX Deferrals must not be postponed after client hostname lookup failure. + * The reason is that the effect of access tables may depend on whether a + * client hostname is available or not. Thus, the reject_unknown_client + * restriction must defer immediately when lookup fails, otherwise incorrect + * results happen with: + * + * reject_unknown_client, hostname-based white-list, reject + * + * XXX With warn_if_reject, don't raise the defer_if_permit flag when a + * reject-style restriction fails. Instead, log the warning for the + * resulting defer message. + * + * XXX With warn_if_reject, do raise the defer_if_reject flag when a + * permit-style restriction fails. Otherwise, we could reject legitimate + * mail. + */ +static int PRINTFLIKE(5, 6) defer_if(SMTPD_DEFER *, int, int, const char *, const char *,...); +static int PRINTFLIKE(5, 6) smtpd_check_reject(SMTPD_STATE *, int, int, const char *, const char *,...); + +#define DEFER_IF_REJECT2(state, class, code, dsn, fmt, a1, a2) \ + defer_if(&(state)->defer_if_reject, (class), (code), (dsn), (fmt), (a1), (a2)) +#define DEFER_IF_REJECT3(state, class, code, dsn, fmt, a1, a2, a3) \ + defer_if(&(state)->defer_if_reject, (class), (code), (dsn), (fmt), (a1), (a2), (a3)) +#define DEFER_IF_REJECT4(state, class, code, dsn, fmt, a1, a2, a3, a4) \ + defer_if(&(state)->defer_if_reject, (class), (code), (dsn), (fmt), (a1), (a2), (a3), (a4)) + + /* + * The following choose between DEFER_IF_PERMIT (only if warn_if_reject is + * turned off) and plain DEFER. See tempfail_actions[] below for the mapping + * from names to numeric action code. + */ +#define DEFER_ALL_ACT 0 +#define DEFER_IF_PERMIT_ACT 1 + +#define DEFER_IF_PERMIT2(type, state, class, code, dsn, fmt, a1, a2) \ + (((state)->warn_if_reject == 0 && (type) != 0) ? \ + defer_if(&(state)->defer_if_permit, (class), (code), (dsn), (fmt), (a1), (a2)) \ + : \ + smtpd_check_reject((state), (class), (code), (dsn), (fmt), (a1), (a2))) +#define DEFER_IF_PERMIT3(type, state, class, code, dsn, fmt, a1, a2, a3) \ + (((state)->warn_if_reject == 0 && (type) != 0) ? \ + defer_if(&(state)->defer_if_permit, (class), (code), (dsn), (fmt), (a1), (a2), (a3)) \ + : \ + smtpd_check_reject((state), (class), (code), (dsn), (fmt), (a1), (a2), (a3))) +#define DEFER_IF_PERMIT4(type, state, class, code, dsn, fmt, a1, a2, a3, a4) \ + (((state)->warn_if_reject == 0 && (type) != 0) ? \ + defer_if(&(state)->defer_if_permit, (class), (code), (dsn), (fmt), (a1), (a2), (a3), (a4)) \ + : \ + smtpd_check_reject((state), (class), (code), (dsn), (fmt), (a1), (a2), (a3), (a4))) + + /* + * Cached RBL lookup state. + */ +typedef struct { + char *txt; /* TXT content or NULL */ + DNS_RR *a; /* A records */ +} SMTPD_RBL_STATE; + +static void *rbl_pagein(const char *, void *); +static void rbl_pageout(void *, void *); +static void *rbl_byte_pagein(const char *, void *); +static void rbl_byte_pageout(void *, void *); + + /* + * Context for RBL $name expansion. + */ +typedef struct { + SMTPD_STATE *state; /* general state */ + char *domain; /* query domain */ + const char *what; /* rejected value */ + const char *class; /* name of rejected value */ + const char *txt; /* randomly selected trimmed TXT rr */ +} SMTPD_RBL_EXPAND_CONTEXT; + + /* + * Multiplication factor for free space check. Free space must be at least + * smtpd_space_multf * message_size_limit. + */ +double smtpd_space_multf = 1.5; + + /* + * SMTPD policy client. Most attributes are ATTR_CLNT attributes. + */ +typedef struct { + ATTR_CLNT *client; /* client handle */ + char *def_action; /* default action */ + char *policy_context; /* context of policy request */ +} SMTPD_POLICY_CLNT; + + /* + * Table-driven parsing of main.cf parameter overrides for specific policy + * clients. We derive the override names from the corresponding main.cf + * parameter names by skipping the redundant "smtpd_policy_service_" prefix. + */ +static ATTR_OVER_TIME time_table[] = { + 21 + (const char *) VAR_SMTPD_POLICY_TMOUT, DEF_SMTPD_POLICY_TMOUT, 0, 1, 0, + 21 + (const char *) VAR_SMTPD_POLICY_IDLE, DEF_SMTPD_POLICY_IDLE, 0, 1, 0, + 21 + (const char *) VAR_SMTPD_POLICY_TTL, DEF_SMTPD_POLICY_TTL, 0, 1, 0, + 21 + (const char *) VAR_SMTPD_POLICY_TRY_DELAY, DEF_SMTPD_POLICY_TRY_DELAY, 0, 1, 0, + 0, +}; +static ATTR_OVER_INT int_table[] = { + 21 + (const char *) VAR_SMTPD_POLICY_REQ_LIMIT, 0, 0, 0, + 21 + (const char *) VAR_SMTPD_POLICY_TRY_LIMIT, 0, 1, 0, + 0, +}; +static ATTR_OVER_STR str_table[] = { + 21 + (const char *) VAR_SMTPD_POLICY_DEF_ACTION, 0, 1, 0, + 21 + (const char *) VAR_SMTPD_POLICY_CONTEXT, 0, 1, 0, + 0, +}; + +#define link_override_table_to_variable(table, var) \ + do { table[var##_offset].target = &var; } while (0) + +#define smtpd_policy_tmout_offset 0 +#define smtpd_policy_idle_offset 1 +#define smtpd_policy_ttl_offset 2 +#define smtpd_policy_try_delay_offset 3 + +#define smtpd_policy_req_limit_offset 0 +#define smtpd_policy_try_limit_offset 1 + +#define smtpd_policy_def_action_offset 0 +#define smtpd_policy_context_offset 1 + +/* policy_client_register - register policy service endpoint */ + +static void policy_client_register(const char *name) +{ + static const char myname[] = "policy_client_register"; + SMTPD_POLICY_CLNT *policy_client; + char *saved_name = 0; + const char *policy_name = 0; + char *cp; + const char *sep = CHARS_COMMA_SP; + const char *parens = CHARS_BRACE; + char *err; + + if (policy_clnt_table == 0) + policy_clnt_table = htable_create(1); + + if (htable_find(policy_clnt_table, name) == 0) { + + /* + * Allow per-service overrides for main.cf global settings. + */ + int smtpd_policy_tmout = var_smtpd_policy_tmout; + int smtpd_policy_idle = var_smtpd_policy_idle; + int smtpd_policy_ttl = var_smtpd_policy_ttl; + int smtpd_policy_try_delay = var_smtpd_policy_try_delay; + int smtpd_policy_req_limit = var_smtpd_policy_req_limit; + int smtpd_policy_try_limit = var_smtpd_policy_try_limit; + const char *smtpd_policy_def_action = var_smtpd_policy_def_action; + const char *smtpd_policy_context = var_smtpd_policy_context; + + link_override_table_to_variable(time_table, smtpd_policy_tmout); + link_override_table_to_variable(time_table, smtpd_policy_idle); + link_override_table_to_variable(time_table, smtpd_policy_ttl); + link_override_table_to_variable(time_table, smtpd_policy_try_delay); + link_override_table_to_variable(int_table, smtpd_policy_req_limit); + link_override_table_to_variable(int_table, smtpd_policy_try_limit); + link_override_table_to_variable(str_table, smtpd_policy_def_action); + link_override_table_to_variable(str_table, smtpd_policy_context); + + if (*name == parens[0]) { + cp = saved_name = mystrdup(name); + if ((err = extpar(&cp, parens, EXTPAR_FLAG_NONE)) != 0) + msg_fatal("policy service syntax error: %s", cp); + if ((policy_name = mystrtok(&cp, sep)) == 0) + msg_fatal("empty policy service: \"%s\"", name); + attr_override(cp, sep, parens, + CA_ATTR_OVER_TIME_TABLE(time_table), + CA_ATTR_OVER_INT_TABLE(int_table), + CA_ATTR_OVER_STR_TABLE(str_table), + CA_ATTR_OVER_END); + } else { + policy_name = name; + } + if (msg_verbose) + msg_info("%s: name=\"%s\" default_action=\"%s\" max_idle=%d " + "max_ttl=%d request_limit=%d retry_delay=%d " + "timeout=%d try_limit=%d policy_context=\"%s\"", + myname, policy_name, smtpd_policy_def_action, + smtpd_policy_idle, smtpd_policy_ttl, + smtpd_policy_req_limit, smtpd_policy_try_delay, + smtpd_policy_tmout, smtpd_policy_try_limit, + smtpd_policy_context); + + /* + * Create the client. + */ + policy_client = (SMTPD_POLICY_CLNT *) mymalloc(sizeof(*policy_client)); + policy_client->client = attr_clnt_create(policy_name, + smtpd_policy_tmout, + smtpd_policy_idle, + smtpd_policy_ttl); + + attr_clnt_control(policy_client->client, + ATTR_CLNT_CTL_REQ_LIMIT, smtpd_policy_req_limit, + ATTR_CLNT_CTL_TRY_LIMIT, smtpd_policy_try_limit, + ATTR_CLNT_CTL_TRY_DELAY, smtpd_policy_try_delay, + ATTR_CLNT_CTL_END); + policy_client->def_action = mystrdup(smtpd_policy_def_action); + policy_client->policy_context = mystrdup(smtpd_policy_context); + htable_enter(policy_clnt_table, name, (void *) policy_client); + if (saved_name) + myfree(saved_name); + } +} + +/* command_map_register - register access table for maps lookup */ + +static void command_map_register(const char *name) +{ + MAPS *maps; + + if (map_command_table == 0) + map_command_table = htable_create(1); + + if (htable_find(map_command_table, name) == 0) { + maps = maps_create(name, name, DICT_FLAG_LOCK + | DICT_FLAG_FOLD_FIX + | DICT_FLAG_UTF8_REQUEST); + (void) htable_enter(map_command_table, name, (void *) maps); + } +} + +/* smtpd_check_parse - pre-parse restrictions */ + +static ARGV *smtpd_check_parse(int flags, const char *checks) +{ + char *saved_checks = mystrdup(checks); + ARGV *argv = argv_alloc(1); + char *bp = saved_checks; + char *name; + char *last = 0; + + /* + * Pre-parse the restriction list, and open any dictionaries that we + * encounter. Dictionaries must be opened before entering the chroot + * jail. + */ +#define SMTPD_CHECK_PARSE_POLICY (1<<0) +#define SMTPD_CHECK_PARSE_MAPS (1<<1) +#define SMTPD_CHECK_PARSE_ALL (~0) + + while ((name = mystrtokq(&bp, CHARS_COMMA_SP, CHARS_BRACE)) != 0) { + argv_add(argv, name, (char *) 0); + if ((flags & SMTPD_CHECK_PARSE_POLICY) + && last && strcasecmp(last, CHECK_POLICY_SERVICE) == 0) + policy_client_register(name); + else if ((flags & SMTPD_CHECK_PARSE_MAPS) && strchr(name, ':') != 0) { + command_map_register(name); + } + last = name; + } + argv_terminate(argv); + + /* + * Cleanup. + */ + myfree(saved_checks); + return (argv); +} + +#ifndef TEST + +/* has_required - make sure required restriction is present */ + +static int has_required(ARGV *restrictions, const char **required) +{ + char **rest; + const char **reqd; + ARGV *expansion; + + /* + * Recursively check list membership. + */ + for (rest = restrictions->argv; *rest; rest++) { + if (strcasecmp(*rest, WARN_IF_REJECT) == 0 && rest[1] != 0) { + rest += 1; + continue; + } + if (strcasecmp(*rest, PERMIT_ALL) == 0) { + if (rest[1] != 0) + msg_warn("restriction `%s' after `%s' is ignored", + rest[1], rest[0]); + return (0); + } + for (reqd = required; *reqd; reqd++) + if (strcasecmp(*rest, *reqd) == 0) + return (1); + /* XXX This lookup operation should not be case-sensitive. */ + if ((expansion = (ARGV *) htable_find(smtpd_rest_classes, *rest)) != 0) + if (has_required(expansion, required)) + return (1); + } + return (0); +} + +/* fail_required - handle failure to use required restriction */ + +static void fail_required(const char *name, const char **required) +{ + const char *myname = "fail_required"; + const char **reqd; + VSTRING *example; + + /* + * Sanity check. + */ + if (required[0] == 0) + msg_panic("%s: null required list", myname); + + /* + * Go bust. + */ + example = vstring_alloc(10); + for (reqd = required; *reqd; reqd++) + vstring_sprintf_append(example, "%s%s", *reqd, + reqd[1] == 0 ? "" : reqd[2] == 0 ? " or " : ", "); + msg_fatal("in parameter %s, specify at least one working instance of: %s", + name, STR(example)); +} + +#endif + +/* smtpd_check_init - initialize once during process lifetime */ + +void smtpd_check_init(void) +{ + char *saved_classes; + const char *name; + const char *value; + char *cp; + +#ifndef TEST + static const char *rcpt_required[] = { + REJECT_UNAUTH_DEST, + DEFER_UNAUTH_DEST, + REJECT_ALL, + DEFER_ALL, + DEFER_IF_PERMIT, + CHECK_RELAY_DOMAINS, + 0, + }; + +#endif + static NAME_CODE tempfail_actions[] = { + DEFER_ALL, DEFER_ALL_ACT, + DEFER_IF_PERMIT, DEFER_IF_PERMIT_ACT, + 0, -1, + }; + + /* + * Pre-open access control lists before going to jail. + */ + mynetworks_curr = + namadr_list_init(VAR_MYNETWORKS, MATCH_FLAG_RETURN + | match_parent_style(VAR_MYNETWORKS), var_mynetworks); + mynetworks_new = + namadr_list_init(VAR_MYNETWORKS, MATCH_FLAG_RETURN + | match_parent_style(VAR_MYNETWORKS), mynetworks_host()); + relay_domains = + domain_list_init(VAR_RELAY_DOMAINS, + match_parent_style(VAR_RELAY_DOMAINS), + var_relay_domains); + perm_mx_networks = + namadr_list_init(VAR_PERM_MX_NETWORKS, MATCH_FLAG_RETURN + | match_parent_style(VAR_PERM_MX_NETWORKS), + var_perm_mx_networks); +#ifdef USE_TLS + relay_ccerts = maps_create(VAR_RELAY_CCERTS, var_smtpd_relay_ccerts, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX); +#endif + + /* + * Pre-parse and pre-open the recipient maps. + */ + local_rcpt_maps = maps_create(VAR_LOCAL_RCPT_MAPS, var_local_rcpt_maps, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX + | DICT_FLAG_UTF8_REQUEST); + send_canon_maps = maps_create(VAR_SEND_CANON_MAPS, var_send_canon_maps, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX + | DICT_FLAG_UTF8_REQUEST); + rcpt_canon_maps = maps_create(VAR_RCPT_CANON_MAPS, var_rcpt_canon_maps, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX + | DICT_FLAG_UTF8_REQUEST); + canonical_maps = maps_create(VAR_CANONICAL_MAPS, var_canonical_maps, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX + | DICT_FLAG_UTF8_REQUEST); + virt_alias_maps = maps_create(VAR_VIRT_ALIAS_MAPS, var_virt_alias_maps, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX + | DICT_FLAG_UTF8_REQUEST); + virt_mailbox_maps = maps_create(VAR_VIRT_MAILBOX_MAPS, + var_virt_mailbox_maps, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX + | DICT_FLAG_UTF8_REQUEST); + relay_rcpt_maps = maps_create(VAR_RELAY_RCPT_MAPS, var_relay_rcpt_maps, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX + | DICT_FLAG_UTF8_REQUEST); + +#ifdef TEST + virt_alias_doms = string_list_init(VAR_VIRT_ALIAS_DOMS, MATCH_FLAG_NONE, + var_virt_alias_doms); + virt_mailbox_doms = string_list_init(VAR_VIRT_MAILBOX_DOMS, MATCH_FLAG_NONE, + var_virt_mailbox_doms); +#endif + + access_parent_style = match_parent_style(SMTPD_ACCESS_MAPS); + + /* + * Templates for RBL rejection replies. + */ + rbl_reply_maps = maps_create(VAR_RBL_REPLY_MAPS, var_rbl_reply_maps, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX + | DICT_FLAG_UTF8_REQUEST); + + /* + * Sender to login name mapping. + */ + smtpd_sender_login_maps = maps_create(VAR_SMTPD_SND_AUTH_MAPS, + var_smtpd_snd_auth_maps, + DICT_FLAG_LOCK | DICT_FLAG_FOLD_FIX + | DICT_FLAG_UTF8_REQUEST); + + /* + * error_text is used for returning error responses. + */ + error_text = vstring_alloc(10); + + /* + * Initialize the resolved address cache. Note: the cache persists across + * SMTP sessions so we cannot make it dependent on session state. + */ + smtpd_resolve_init(100); + + /* + * Initialize the RBL lookup cache. Note: the cache persists across SMTP + * sessions so we cannot make it dependent on session state. + */ + smtpd_rbl_cache = ctable_create(100, rbl_pagein, rbl_pageout, (void *) 0); + smtpd_rbl_byte_cache = ctable_create(1000, rbl_byte_pagein, + rbl_byte_pageout, (void *) 0); + + /* + * Pre-parse the restriction lists. At the same time, pre-open tables + * before going to jail. + */ + client_restrctions = smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, + var_client_checks); + helo_restrctions = smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, + var_helo_checks); + mail_restrctions = smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, + var_mail_checks); + relay_restrctions = smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, + var_relay_checks); + if (warn_compat_break_relay_restrictions) + fake_relay_restrctions = smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, + FAKE_RELAY_CHECKS); + rcpt_restrctions = smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, + var_rcpt_checks); + etrn_restrctions = smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, + var_etrn_checks); + data_restrctions = smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, + var_data_checks); + eod_restrictions = smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, + var_eod_checks); + + /* + * Parse the pre-defined restriction classes. + */ + smtpd_rest_classes = htable_create(1); + if (*var_rest_classes) { + cp = saved_classes = mystrdup(var_rest_classes); + while ((name = mystrtok(&cp, CHARS_COMMA_SP)) != 0) { + if ((value = mail_conf_lookup_eval(name)) == 0 || *value == 0) + msg_fatal("restriction class `%s' needs a definition", name); + /* XXX This store operation should not be case-sensitive. */ + htable_enter(smtpd_rest_classes, name, + (void *) smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, + value)); + } + myfree(saved_classes); + } + + /* + * This is the place to specify definitions for complex restrictions such + * as check_relay_domains in terms of more elementary restrictions. + */ +#if 0 + htable_enter(smtpd_rest_classes, "check_relay_domains", + smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, + "permit_mydomain reject_unauth_destination")); +#endif + htable_enter(smtpd_rest_classes, REJECT_SENDER_LOGIN_MISMATCH, + (void *) smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, + REJECT_AUTH_SENDER_LOGIN_MISMATCH + " " REJECT_UNAUTH_SENDER_LOGIN_MISMATCH)); + + /* + * People screw up the relay restrictions too often. Require that they + * list at least one restriction that rejects mail by default. We allow + * relay restrictions to be empty for sites that require backwards + * compatibility. + */ +#ifndef TEST + if (!has_required(rcpt_restrctions, rcpt_required) + && !has_required(relay_restrctions, rcpt_required)) + fail_required(VAR_RELAY_CHECKS " or " VAR_RCPT_CHECKS, rcpt_required); +#endif + + /* + * Local rewrite policy. + */ + local_rewrite_clients = smtpd_check_parse(SMTPD_CHECK_PARSE_MAPS, + var_local_rwr_clients); + + /* + * Tempfail_actions. + * + * XXX This name-to-number mapping should be encapsulated in a separate + * mail_conf_name_code.c module. + */ + if ((unk_name_tf_act = name_code(tempfail_actions, NAME_CODE_FLAG_NONE, + var_unk_name_tf_act)) < 0) + msg_fatal("bad configuration: %s = %s", + VAR_UNK_NAME_TF_ACT, var_unk_name_tf_act); + if ((unk_addr_tf_act = name_code(tempfail_actions, NAME_CODE_FLAG_NONE, + var_unk_addr_tf_act)) < 0) + msg_fatal("bad configuration: %s = %s", + VAR_UNK_ADDR_TF_ACT, var_unk_addr_tf_act); + if ((unv_rcpt_tf_act = name_code(tempfail_actions, NAME_CODE_FLAG_NONE, + var_unv_rcpt_tf_act)) < 0) + msg_fatal("bad configuration: %s = %s", + VAR_UNV_RCPT_TF_ACT, var_unv_rcpt_tf_act); + if ((unv_from_tf_act = name_code(tempfail_actions, NAME_CODE_FLAG_NONE, + var_unv_from_tf_act)) < 0) + msg_fatal("bad configuration: %s = %s", + VAR_UNV_FROM_TF_ACT, var_unv_from_tf_act); + if (msg_verbose) { + msg_info("%s = %s", VAR_UNK_NAME_TF_ACT, tempfail_actions[unk_name_tf_act].name); + msg_info("%s = %s", VAR_UNK_ADDR_TF_ACT, tempfail_actions[unk_addr_tf_act].name); + msg_info("%s = %s", VAR_UNV_RCPT_TF_ACT, tempfail_actions[unv_rcpt_tf_act].name); + msg_info("%s = %s", VAR_UNV_FROM_TF_ACT, tempfail_actions[unv_from_tf_act].name); + } + + /* + * Optional permit logging. + */ + smtpd_acl_perm_log = string_list_init(VAR_SMTPD_ACL_PERM_LOG, + MATCH_FLAG_RETURN, + var_smtpd_acl_perm_log); +} + +/* log_whatsup - log as much context as we have */ + +static void log_whatsup(SMTPD_STATE *state, const char *whatsup, + const char *text) +{ + VSTRING *buf = vstring_alloc(100); + + vstring_sprintf(buf, "%s: %s: %s from %s: %s;", + state->queue_id ? state->queue_id : "NOQUEUE", + whatsup, state->where, state->namaddr, text); + if (state->sender) + vstring_sprintf_append(buf, " from=<%s>", state->sender); + if (state->recipient) + vstring_sprintf_append(buf, " to=<%s>", state->recipient); + if (state->protocol) + vstring_sprintf_append(buf, " proto=%s", state->protocol); + if (state->helo_name) + vstring_sprintf_append(buf, " helo=<%s>", state->helo_name); + msg_info("%s", STR(buf)); + vstring_free(buf); +} + +/* smtpd_acl_permit - permit request with optional logging */ + +static int PRINTFLIKE(5, 6) smtpd_acl_permit(SMTPD_STATE *state, + const char *action, + const char *reply_class, + const char *reply_name, + const char *format,...) +{ + const char myname[] = "smtpd_acl_permit"; + va_list ap; + const char *whatsup; + +#ifdef notdef +#define NO_PRINT_ARGS "" +#else +#define NO_PRINT_ARGS "%s", "" +#endif + + /* + * First, find out if (and how) this permit action should be logged. + */ + if (msg_verbose) + msg_info("%s: checking %s settings", myname, VAR_SMTPD_ACL_PERM_LOG); + + if (state->defer_if_permit.active) { + /* This action is overruled. Do not log. */ + whatsup = 0; + } else if (string_list_match(smtpd_acl_perm_log, action) != 0) { + /* This is not a test. Logging is enabled. */ + whatsup = "permit"; + } else { + /* This is not a test. Logging is disabled. */ + whatsup = 0; + } + if (whatsup != 0) { + vstring_sprintf(error_text, "action=%s for %s=%s", + action, reply_class, reply_name); + if (format && *format) { + vstring_strcat(error_text, " "); + va_start(ap, format); + vstring_vsprintf_append(error_text, format, ap); + va_end(ap); + } + log_whatsup(state, whatsup, STR(error_text)); + } else { + if (msg_verbose) + msg_info("%s: %s: no match", myname, VAR_SMTPD_ACL_PERM_LOG); + } + return (SMTPD_CHECK_OK); +} + +/* smtpd_check_reject - do the boring things that must be done */ + +static int smtpd_check_reject(SMTPD_STATE *state, int error_class, + int code, const char *dsn, + const char *format,...) +{ + va_list ap; + int warn_if_reject; + const char *whatsup; + + /* + * Do not reject mail if we were asked to warn only. However, + * configuration/software/data errors cannot be converted into warnings. + */ + if (state->warn_if_reject && error_class != MAIL_ERROR_SOFTWARE + && error_class != MAIL_ERROR_RESOURCE + && error_class != MAIL_ERROR_DATA) { + warn_if_reject = 1; + whatsup = "reject_warning"; + } else { + warn_if_reject = 0; + whatsup = "reject"; + } + + /* + * Update the error class mask, and format the response. XXX What about + * multi-line responses? For now we cheat and send whitespace. + * + * Format the response before complaining about configuration errors, so + * that we can show the error in context. + */ + state->error_mask |= error_class; + vstring_sprintf(error_text, "%d %s ", code, dsn); + va_start(ap, format); + vstring_vsprintf_append(error_text, format, ap); + va_end(ap); + + /* + * Validate the response, that is, the response must begin with a + * three-digit status code, and the first digit must be 4 or 5. If the + * response is bad, log a warning and send a generic response instead. + */ + if (code < 400 || code > 599) { + msg_warn("SMTP reply code configuration error: %s", STR(error_text)); + vstring_strcpy(error_text, "450 4.7.1 Service unavailable"); + } + if (!dsn_valid(STR(error_text) + 4)) { + msg_warn("DSN detail code configuration error: %s", STR(error_text)); + vstring_strcpy(error_text, "450 4.7.1 Service unavailable"); + } + + /* + * Ensure RFC compliance. We could do this inside smtpd_chat_reply() and + * switch to multi-line for long replies. + */ + vstring_truncate(error_text, 510); + printable(STR(error_text), ' '); + + /* + * Force this rejection into deferral because of some earlier temporary + * error that may have prevented us from accepting mail, and report the + * earlier problem instead. + */ + if (!warn_if_reject && state->defer_if_reject.active && STR(error_text)[0] == '5') { + state->warn_if_reject = state->defer_if_reject.active = 0; + return (smtpd_check_reject(state, state->defer_if_reject.class, + state->defer_if_reject.code, + STR(state->defer_if_reject.dsn), + "%s", STR(state->defer_if_reject.reason))); + } + + /* + * Soft bounce safety net. + * + * XXX The code below also appears in the Postfix SMTP server reply output + * routine. It is duplicated here in order to avoid discrepancies between + * the reply codes that are shown in "reject" logging and the reply codes + * that are actually sent to the SMTP client. + * + * Implementing the soft_bounce safety net in the SMTP server reply output + * routine has the advantage that it covers all 5xx replies, including + * SMTP protocol or syntax errors, which makes soft_bounce great for + * non-destructive tests (especially by people who are paranoid about + * losing mail). + * + * We could eliminate the code duplication and implement the soft_bounce + * safety net only in the code below. But then the safety net would cover + * the UCE restrictions only. This would be at odds with documentation + * which says soft_bounce changes all 5xx replies into 4xx ones. + */ + if (var_soft_bounce && STR(error_text)[0] == '5') + STR(error_text)[0] = '4'; + + /* + * In any case, enforce consistency between the SMTP code and DSN code. + * SMTP has the higher precedence since it came here first. + */ + STR(error_text)[4] = STR(error_text)[0]; + + /* + * Log what is happening. When the sysadmin discards policy violation + * postmaster notices, this may be the only trace left that service was + * rejected. Print the request, client name/address, and response. + */ + log_whatsup(state, whatsup, STR(error_text)); + + return (warn_if_reject ? 0 : SMTPD_CHECK_REJECT); +} + +/* defer_if - prepare to change our mind */ + +static int defer_if(SMTPD_DEFER *defer, int error_class, + int code, const char *dsn, + const char *fmt,...) +{ + va_list ap; + + /* + * Keep the first reason for this type of deferral, to minimize + * confusion. + */ + if (defer->active == 0) { + defer->active = 1; + defer->class = error_class; + defer->code = code; + if (defer->dsn == 0) + defer->dsn = vstring_alloc(10); + vstring_strcpy(defer->dsn, dsn); + if (defer->reason == 0) + defer->reason = vstring_alloc(10); + va_start(ap, fmt); + vstring_vsprintf(defer->reason, fmt, ap); + va_end(ap); + } + return (SMTPD_CHECK_DUNNO); +} + +/* reject_dict_retry - reject with temporary failure if dict lookup fails */ + +static NORETURN reject_dict_retry(SMTPD_STATE *state, const char *reply_name) +{ + longjmp(smtpd_check_buf, smtpd_check_reject(state, MAIL_ERROR_DATA, + 451, "4.3.0", + "<%s>: Temporary lookup failure", + reply_name)); +} + +/* reject_server_error - reject with temporary failure after non-dict error */ + +static NORETURN reject_server_error(SMTPD_STATE *state) +{ + longjmp(smtpd_check_buf, smtpd_check_reject(state, MAIL_ERROR_SOFTWARE, + 451, "4.3.5", + "Server configuration error")); +} + +/* check_mail_addr_find - reject with temporary failure if dict lookup fails */ + +static const char *check_mail_addr_find(SMTPD_STATE *state, + const char *reply_name, + MAPS *maps, const char *key, + char **ext) +{ + const char *result; + + if ((result = mail_addr_find(maps, key, ext)) != 0 || maps->error == 0) + return (result); + if (maps->error == DICT_ERR_RETRY) + /* Warning is already logged. */ + reject_dict_retry(state, reply_name); + else + reject_server_error(state); +} + +/* reject_unknown_reverse_name - fail if reverse client hostname is unknown */ + +static int reject_unknown_reverse_name(SMTPD_STATE *state) +{ + const char *myname = "reject_unknown_reverse_name"; + + if (msg_verbose) + msg_info("%s: %s", myname, state->reverse_name); + + if (state->reverse_name_status != SMTPD_PEER_CODE_OK) + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, + state->reverse_name_status == SMTPD_PEER_CODE_PERM ? + var_unk_client_code : 450, "4.7.1", + "Client host rejected: cannot find your reverse hostname, [%s]", + state->addr)); + return (SMTPD_CHECK_DUNNO); +} + +/* reject_unknown_client - fail if client hostname is unknown */ + +static int reject_unknown_client(SMTPD_STATE *state) +{ + const char *myname = "reject_unknown_client"; + + if (msg_verbose) + msg_info("%s: %s %s", myname, state->name, state->addr); + + /* RFC 7372: Email Authentication Status Codes. */ + if (state->name_status != SMTPD_PEER_CODE_OK) + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, + state->name_status >= SMTPD_PEER_CODE_PERM ? + var_unk_client_code : 450, "4.7.25", + "Client host rejected: cannot find your hostname, [%s]", + state->addr)); + return (SMTPD_CHECK_DUNNO); +} + +/* reject_plaintext_session - fail if session is not encrypted */ + +static int reject_plaintext_session(SMTPD_STATE *state) +{ + const char *myname = "reject_plaintext_session"; + + if (msg_verbose) + msg_info("%s: %s %s", myname, state->name, state->addr); + +#ifdef USE_TLS + if (state->tls_context == 0) +#endif + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_plaintext_code, "4.7.1", + "Session encryption is required")); + return (SMTPD_CHECK_DUNNO); +} + +/* permit_inet_interfaces - succeed if client my own address */ + +static int permit_inet_interfaces(SMTPD_STATE *state) +{ + const char *myname = "permit_inet_interfaces"; + + if (msg_verbose) + msg_info("%s: %s %s", myname, state->name, state->addr); + + if (own_inet_addr((struct sockaddr *) &(state->sockaddr))) + /* Permit logging in generic_checks() only. */ + return (SMTPD_CHECK_OK); + return (SMTPD_CHECK_DUNNO); +} + +/* permit_mynetworks - succeed if client is in a trusted network */ + +static int permit_mynetworks(SMTPD_STATE *state) +{ + const char *myname = "permit_mynetworks"; + + if (msg_verbose) + msg_info("%s: %s %s", myname, state->name, state->addr); + + if (namadr_list_match(mynetworks_curr, state->name, state->addr)) { + if (warn_compat_break_mynetworks_style + && !namadr_list_match(mynetworks_new, state->name, state->addr)) + msg_info("using backwards-compatible default setting " + VAR_MYNETWORKS_STYLE "=%s to permit request from " + "client \"%s\"", var_mynetworks_style, state->namaddr); + /* Permit logging in generic_checks() only. */ + return (SMTPD_CHECK_OK); + } else if (mynetworks_curr->error == 0) + return (SMTPD_CHECK_DUNNO); + else + return (mynetworks_curr->error); +} + +/* dup_if_truncate - save hostname and truncate if it ends in dot */ + +static char *dup_if_truncate(char *name) +{ + ssize_t len; + char *result; + + /* + * Truncate hostnames ending in dot but not dot-dot. + * + * XXX This should not be distributed all over the code. Problem is, + * addresses can enter the system via multiple paths: networks, local + * forward/alias/include files, even as the result of address rewriting. + */ + if ((len = strlen(name)) > 1 + && name[len - 1] == '.' + && name[len - 2] != '.') { + result = mystrndup(name, len - 1); + } else + result = name; + return (result); +} + +/* reject_invalid_hostaddr - fail if host address is incorrect */ + +static int reject_invalid_hostaddr(SMTPD_STATE *state, char *addr, + char *reply_name, char *reply_class) +{ + const char *myname = "reject_invalid_hostaddr"; + ssize_t len; + char *test_addr; + int stat; + + if (msg_verbose) + msg_info("%s: %s", myname, addr); + + if (addr[0] == '[' && (len = strlen(addr)) > 2 && addr[len - 1] == ']') { + test_addr = mystrndup(addr + 1, len - 2); + } else + test_addr = addr; + + /* + * Validate the address. + */ + if (!valid_mailhost_addr(test_addr, DONT_GRIPE)) + stat = smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_bad_name_code, "5.5.2", + "<%s>: %s rejected: invalid ip address", + reply_name, reply_class); + else + stat = SMTPD_CHECK_DUNNO; + + /* + * Cleanup. + */ + if (test_addr != addr) + myfree(test_addr); + + return (stat); +} + +/* reject_invalid_hostname - fail if host/domain syntax is incorrect */ + +static int reject_invalid_hostname(SMTPD_STATE *state, char *name, + char *reply_name, char *reply_class) +{ + const char *myname = "reject_invalid_hostname"; + char *test_name; + int stat; + + if (msg_verbose) + msg_info("%s: %s", myname, name); + + /* + * Truncate hostnames ending in dot but not dot-dot. + */ + test_name = dup_if_truncate(name); + + /* + * Validate the HELO/EHLO hostname. Fix 20140706: EAI not allowed here. + */ + if (!valid_hostname(test_name, DONT_GRIPE) + && !valid_hostaddr(test_name, DONT_GRIPE)) /* XXX back compat */ + stat = smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_bad_name_code, "5.5.2", + "<%s>: %s rejected: Invalid name", + reply_name, reply_class); + else + stat = SMTPD_CHECK_DUNNO; + + /* + * Cleanup. + */ + if (test_name != name) + myfree(test_name); + + return (stat); +} + +/* reject_non_fqdn_hostname - fail if host name is not in fqdn form */ + +static int reject_non_fqdn_hostname(SMTPD_STATE *state, char *name, + char *reply_name, char *reply_class) +{ + const char *myname = "reject_non_fqdn_hostname"; + char *test_name; + int stat; + + if (msg_verbose) + msg_info("%s: %s", myname, name); + + /* + * Truncate hostnames ending in dot but not dot-dot. + */ + test_name = dup_if_truncate(name); + + /* + * Validate the hostname. For backwards compatibility, permit non-ASCII + * names only when the client requested SMTPUTF8 support. + */ + if (valid_utf8_hostname(state->flags & SMTPD_FLAG_SMTPUTF8, + test_name, DONT_GRIPE) == 0 || strchr(test_name, '.') == 0) + stat = smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_non_fqdn_code, "5.5.2", + "<%s>: %s rejected: need fully-qualified hostname", + reply_name, reply_class); + else + stat = SMTPD_CHECK_DUNNO; + + /* + * Cleanup. + */ + if (test_name != name) + myfree(test_name); + + return (stat); +} + +/* reject_unknown_hostname - fail if name has no A, AAAA or MX record */ + +static int reject_unknown_hostname(SMTPD_STATE *state, char *name, + char *reply_name, char *reply_class) +{ + const char *myname = "reject_unknown_hostname"; + int dns_status; + DNS_RR *dummy; + + if (msg_verbose) + msg_info("%s: %s", myname, name); + +#ifdef T_AAAA +#define RR_ADDR_TYPES T_A, T_AAAA +#else +#define RR_ADDR_TYPES T_A +#endif + + dns_status = dns_lookup_l(name, 0, &dummy, (VSTRING *) 0, + (VSTRING *) 0, DNS_REQ_FLAG_STOP_OK, + RR_ADDR_TYPES, T_MX, 0); + if (dummy) + dns_rr_free(dummy); + /* Allow MTA names to have nullMX records. */ + if (dns_status != DNS_OK && dns_status != DNS_NULLMX) { + if (dns_status == DNS_POLICY) { + msg_warn("%s: address or MX lookup error: %s", + name, "DNS reply filter drops all results"); + return (SMTPD_CHECK_DUNNO); + } + if (dns_status != DNS_RETRY) + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_unk_name_code, "4.7.1", + "<%s>: %s rejected: %s", + reply_name, reply_class, + dns_status == DNS_INVAL ? + "Malformed DNS server reply" : + "Host not found")); + else + return (DEFER_IF_PERMIT2(unk_name_tf_act, state, MAIL_ERROR_POLICY, + 450, "4.7.1", + "<%s>: %s rejected: Host not found", + reply_name, reply_class)); + } + return (SMTPD_CHECK_DUNNO); +} + +/* reject_unknown_mailhost - fail if name has no A, AAAA or MX record */ + +static int reject_unknown_mailhost(SMTPD_STATE *state, const char *name, + const char *reply_name, const char *reply_class) +{ + const char *myname = "reject_unknown_mailhost"; + int dns_status; + DNS_RR *dummy; + const char *aname; + + if (msg_verbose) + msg_info("%s: %s", myname, name); + + /* + * Fix 20140924: convert domain to ASCII. + */ +#ifndef NO_EAI + if (!allascii(name) && (aname = midna_domain_to_ascii(name)) != 0) { + if (msg_verbose) + msg_info("%s asciified to %s", name, aname); + name = aname; + } +#endif + +#define MAILHOST_LOOKUP_FLAGS \ + (DNS_REQ_FLAG_STOP_OK | DNS_REQ_FLAG_STOP_INVAL | \ + DNS_REQ_FLAG_STOP_NULLMX | DNS_REQ_FLAG_STOP_MX_POLICY) + + dns_status = dns_lookup_l(name, 0, &dummy, (VSTRING *) 0, + (VSTRING *) 0, MAILHOST_LOOKUP_FLAGS, + T_MX, RR_ADDR_TYPES, 0); + if (dummy) + dns_rr_free(dummy); + if (dns_status != DNS_OK) { /* incl. DNS_INVAL */ + if (dns_status == DNS_POLICY) { + msg_warn("%s: MX or address lookup error: %s", + name, "DNS reply filter drops all results"); + return (SMTPD_CHECK_DUNNO); + } + if (dns_status == DNS_NULLMX) + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, + strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + 550 : 556, + strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + "4.7.27" : "4.1.10", + "<%s>: %s rejected: Domain %s " + "does not accept mail (nullMX)", + reply_name, reply_class, name)); + if (dns_status != DNS_RETRY) + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_unk_addr_code, + strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + "4.1.8" : "4.1.2", + "<%s>: %s rejected: %s", + reply_name, reply_class, + dns_status == DNS_INVAL ? + "Malformed DNS server reply" : + "Domain not found")); + else + return (DEFER_IF_PERMIT2(unk_addr_tf_act, state, MAIL_ERROR_POLICY, + 450, strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + "4.1.8" : "4.1.2", + "<%s>: %s rejected: Domain not found", + reply_name, reply_class)); + } + return (SMTPD_CHECK_DUNNO); +} + +static int permit_auth_destination(SMTPD_STATE *state, char *recipient); + +/* permit_tls_clientcerts - OK/DUNNO for message relaying, or set dict_errno */ + +static int permit_tls_clientcerts(SMTPD_STATE *state, int permit_all_certs) +{ +#ifdef USE_TLS + const char *found = 0; + + if (!state->tls_context) + return SMTPD_CHECK_DUNNO; + + if (TLS_CERT_IS_TRUSTED(state->tls_context) && permit_all_certs) { + if (msg_verbose) + msg_info("Relaying allowed for all verified client certificates"); + /* Permit logging in generic_checks() only. */ + return (SMTPD_CHECK_OK); + } + + /* + * When directly checking the fingerprint, it is OK if the issuing CA is + * not trusted. + */ + if (TLS_CERT_IS_PRESENT(state->tls_context)) { + int i; + char *prints[2]; + + prints[0] = state->tls_context->peer_cert_fprint; + prints[1] = state->tls_context->peer_pkey_fprint; + + /* After lookup error, leave relay_ccerts->error at non-zero value. */ + for (i = 0; i < 2; ++i) { + found = maps_find(relay_ccerts, prints[i], DICT_FLAG_NONE); + if (found != 0) { + if (msg_verbose) + msg_info("Relaying allowed for certified client: %s", found); + /* Permit logging in generic_checks() only. */ + return (SMTPD_CHECK_OK); + } else if (relay_ccerts->error != 0) { + msg_warn("relay_clientcerts: lookup error for fingerprint '%s', " + "pkey fingerprint %s", prints[0], prints[1]); + return (relay_ccerts->error); + } + } + if (msg_verbose) + msg_info("relay_clientcerts: No match for fingerprint '%s', " + "pkey fingerprint %s", prints[0], prints[1]); + } +#endif + return (SMTPD_CHECK_DUNNO); +} + +/* check_relay_domains - OK/FAIL for message relaying */ + +static int check_relay_domains(SMTPD_STATE *state, char *recipient, + char *reply_name, char *reply_class) +{ + const char *myname = "check_relay_domains"; + +#if 1 + static int once; + + if (once == 0) { + once = 1; + msg_warn("support for restriction \"%s\" will be removed from %s; " + "use \"%s\" instead", + CHECK_RELAY_DOMAINS, var_mail_name, REJECT_UNAUTH_DEST); + } +#endif + + if (msg_verbose) + msg_info("%s: %s", myname, recipient); + + /* + * Permit if the client matches the relay_domains list. + */ + if (domain_list_match(relay_domains, state->name)) { + if (warn_compat_break_relay_domains) + msg_info("using backwards-compatible default setting " + VAR_RELAY_DOMAINS "=$mydestination to permit " + "request from client \"%s\"", state->name); + return (SMTPD_CHECK_OK); + } + + /* + * Permit authorized destinations. + */ + if (permit_auth_destination(state, recipient) == SMTPD_CHECK_OK) + return (SMTPD_CHECK_OK); + + /* + * Deny relaying between sites that both are not in relay_domains. + */ + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_relay_code, "5.7.1", + "<%s>: %s rejected: Relay access denied", + reply_name, reply_class)); +} + +/* permit_auth_destination - OK for message relaying */ + +static int permit_auth_destination(SMTPD_STATE *state, char *recipient) +{ + const char *myname = "permit_auth_destination"; + const RESOLVE_REPLY *reply; + const char *domain; + + if (msg_verbose) + msg_info("%s: %s", myname, recipient); + + /* + * Resolve the address. + */ + reply = smtpd_resolve_addr(state->sender, recipient); + if (reply->flags & RESOLVE_FLAG_FAIL) + reject_dict_retry(state, recipient); + + /* + * Handle special case that is not supposed to happen. + */ + if ((domain = strrchr(CONST_STR(reply->recipient), '@')) == 0) + return (SMTPD_CHECK_OK); + domain += 1; + + /* + * Skip source-routed non-local or virtual mail (uncertain destination). + */ + if (var_allow_untrust_route == 0 && (reply->flags & RESOLVE_FLAG_ROUTED)) + return (SMTPD_CHECK_DUNNO); + + /* + * Permit final delivery: the destination matches mydestination, + * virtual_alias_domains, or virtual_mailbox_domains. + */ + if (reply->flags & RESOLVE_CLASS_FINAL) + return (SMTPD_CHECK_OK); + + /* + * Permit if the destination matches the relay_domains list. + */ + if (reply->flags & RESOLVE_CLASS_RELAY) { + if (warn_compat_break_relay_domains) + msg_info("using backwards-compatible default setting " + VAR_RELAY_DOMAINS "=$mydestination to accept mail " + "for domain \"%s\"", domain); + return (SMTPD_CHECK_OK); + } + + /* + * Skip when not matched + */ + return (SMTPD_CHECK_DUNNO); +} + +/* reject_unauth_destination - FAIL for message relaying */ + +static int reject_unauth_destination(SMTPD_STATE *state, char *recipient, + int reply_code, const char *reply_dsn) +{ + const char *myname = "reject_unauth_destination"; + + if (msg_verbose) + msg_info("%s: %s", myname, recipient); + + /* + * Skip authorized destination. + */ + if (permit_auth_destination(state, recipient) == SMTPD_CHECK_OK) + return (SMTPD_CHECK_DUNNO); + + /* + * Reject relaying to sites that are not listed in relay_domains. + */ + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, + reply_code, reply_dsn, + "<%s>: Relay access denied", + recipient)); +} + +/* reject_unauth_pipelining - reject improper use of SMTP command pipelining */ + +static int reject_unauth_pipelining(SMTPD_STATE *state, + const char *reply_name, const char *reply_class) +{ + const char *myname = "reject_unauth_pipelining"; + + if (msg_verbose) + msg_info("%s: %s", myname, state->where); + + if (state->flags & SMTPD_FLAG_ILL_PIPELINING) + return (smtpd_check_reject(state, MAIL_ERROR_PROTOCOL, + 503, "5.5.0", + "<%s>: %s rejected: Improper use of SMTP command pipelining", + reply_name, reply_class)); + + return (SMTPD_CHECK_DUNNO); +} + +/* all_auth_mx_addr - match host addresses against permit_mx_backup_networks */ + +static int all_auth_mx_addr(SMTPD_STATE *state, char *host, + const char *reply_name, const char *reply_class) +{ + const char *myname = "all_auth_mx_addr"; + MAI_HOSTADDR_STR hostaddr; + DNS_RR *rr; + DNS_RR *addr_list; + int dns_status; + + if (msg_verbose) + msg_info("%s: host %s", myname, host); + + /* + * If we can't lookup the host, defer. + */ +#define NOPE 0 +#define YUP 1 + + /* + * Verify that all host addresses are within permit_mx_backup_networks. + */ + dns_status = dns_lookup_v(host, 0, &addr_list, (VSTRING *) 0, (VSTRING *) 0, + DNS_REQ_FLAG_NONE, inet_proto_info()->dns_atype_list); + /* DNS_NULLMX is not applicable here. */ + if (dns_status != DNS_OK) { /* incl. DNS_INVAL */ + DEFER_IF_REJECT4(state, MAIL_ERROR_POLICY, + 450, "4.4.4", + "<%s>: %s rejected: Unable to look up host " + "%s as mail exchanger: %s", + reply_name, reply_class, host, + dns_status == DNS_POLICY ? + "DNS reply filter policy" : dns_strerror(h_errno)); + return (NOPE); + } + for (rr = addr_list; rr != 0; rr = rr->next) { + if (dns_rr_to_pa(rr, &hostaddr) == 0) { + msg_warn("%s: skipping record type %s for host %s: %m", + myname, dns_strtype(rr->type), host); + continue; + } + if (msg_verbose) + msg_info("%s: checking: %s", myname, hostaddr.buf); + + if (!namadr_list_match(perm_mx_networks, host, hostaddr.buf)) { + if (perm_mx_networks->error == 0) { + + /* + * Reject: at least one IP address is not listed in + * permit_mx_backup_networks. + */ + if (msg_verbose) + msg_info("%s: address %s for %s does not match %s", + myname, hostaddr.buf, host, VAR_PERM_MX_NETWORKS); + } else { + msg_warn("%s: %s lookup error for address %s for %s", + myname, VAR_PERM_MX_NETWORKS, hostaddr.buf, host); + DEFER_IF_REJECT3(state, MAIL_ERROR_POLICY, + 450, "4.4.4", + "<%s>: %s rejected: Unable to verify host %s as mail exchanger", + reply_name, reply_class, host); + } + dns_rr_free(addr_list); + return (NOPE); + } + } + dns_rr_free(addr_list); + return (YUP); +} + +/* has_my_addr - see if this host name lists one of my network addresses */ + +static int has_my_addr(SMTPD_STATE *state, const char *host, + const char *reply_name, const char *reply_class) +{ + const char *myname = "has_my_addr"; + struct addrinfo *res; + struct addrinfo *res0; + int aierr; + MAI_HOSTADDR_STR hostaddr; + INET_PROTO_INFO *proto_info = inet_proto_info(); + + if (msg_verbose) + msg_info("%s: host %s", myname, host); + + /* + * If we can't lookup the host, defer rather than reject. + */ +#define YUP 1 +#define NOPE 0 + + aierr = hostname_to_sockaddr(host, (char *) 0, 0, &res0); + if (aierr) { + DEFER_IF_REJECT4(state, MAIL_ERROR_POLICY, + 450, "4.4.4", + "<%s>: %s rejected: Unable to look up mail exchanger host %s: %s", + reply_name, reply_class, host, MAI_STRERROR(aierr)); + return (NOPE); + } +#define HAS_MY_ADDR_RETURN(x) { freeaddrinfo(res0); return (x); } + + for (res = res0; res != 0; res = res->ai_next) { + if (strchr((char *) proto_info->sa_family_list, res->ai_family) == 0) { + if (msg_verbose) + msg_info("skipping address family %d for host %s", + res->ai_family, host); + continue; + } + if (msg_verbose) { + SOCKADDR_TO_HOSTADDR(res->ai_addr, res->ai_addrlen, + &hostaddr, (MAI_SERVPORT_STR *) 0, 0); + msg_info("%s: addr %s", myname, hostaddr.buf); + } + if (own_inet_addr(res->ai_addr)) + HAS_MY_ADDR_RETURN(YUP); + if (proxy_inet_addr(res->ai_addr)) + HAS_MY_ADDR_RETURN(YUP); + } + if (msg_verbose) + msg_info("%s: host %s: no match", myname, host); + + HAS_MY_ADDR_RETURN(NOPE); +} + +/* i_am_mx - is this machine listed as MX relay */ + +static int i_am_mx(SMTPD_STATE *state, DNS_RR *mx_list, + const char *reply_name, const char *reply_class) +{ + const char *myname = "i_am_mx"; + DNS_RR *mx; + + /* + * Compare hostnames first. Only if no name match is found, go through + * the trouble of host address lookups. + */ + for (mx = mx_list; mx != 0; mx = mx->next) { + if (msg_verbose) + msg_info("%s: resolve hostname: %s", myname, (char *) mx->data); + if (resolve_local((char *) mx->data) > 0) + return (YUP); + /* if no match or error, match interface addresses instead. */ + } + + /* + * Argh. Do further DNS lookups and match interface addresses. + */ + for (mx = mx_list; mx != 0; mx = mx->next) { + if (msg_verbose) + msg_info("%s: address lookup: %s", myname, (char *) mx->data); + if (has_my_addr(state, (char *) mx->data, reply_name, reply_class)) + return (YUP); + } + + /* + * This machine is not listed as MX relay. + */ + if (msg_verbose) + msg_info("%s: I am not listed as MX relay", myname); + return (NOPE); +} + +/* permit_mx_primary - authorize primary MX relays */ + +static int permit_mx_primary(SMTPD_STATE *state, DNS_RR *mx_list, + const char *reply_name, const char *reply_class) +{ + const char *myname = "permit_mx_primary"; + DNS_RR *mx; + + if (msg_verbose) + msg_info("%s", myname); + + /* + * See if each best MX host has all IP addresses in + * permit_mx_backup_networks. + */ + for (mx = mx_list; mx != 0; mx = mx->next) { + if (!all_auth_mx_addr(state, (char *) mx->data, reply_name, reply_class)) + return (NOPE); + } + + /* + * All IP addresses of the best MX hosts are within + * permit_mx_backup_networks. + */ + return (YUP); +} + +/* permit_mx_backup - permit use of me as MX backup for recipient domain */ + +static int permit_mx_backup(SMTPD_STATE *state, const char *recipient, + const char *reply_name, const char *reply_class) +{ + const char *myname = "permit_mx_backup"; + const RESOLVE_REPLY *reply; + const char *domain; + const char *adomain; + DNS_RR *mx_list; + DNS_RR *middle; + DNS_RR *rest; + int dns_status; + + if (msg_verbose) + msg_info("%s: %s", myname, recipient); + + /* + * Resolve the address. + */ + reply = smtpd_resolve_addr(state->sender, recipient); + if (reply->flags & RESOLVE_FLAG_FAIL) + reject_dict_retry(state, recipient); + + /* + * For backwards compatibility, emulate permit_auth_destination. However, + * old permit_mx_backup implementations allow source routing with local + * address class. + */ + if ((domain = strrchr(CONST_STR(reply->recipient), '@')) == 0) + return (SMTPD_CHECK_OK); + domain += 1; +#if 0 + if (reply->flags & RESOLVE_CLASS_LOCAL) + return (SMTPD_CHECK_OK); +#endif + if (var_allow_untrust_route == 0 && (reply->flags & RESOLVE_FLAG_ROUTED)) + return (SMTPD_CHECK_DUNNO); + if (reply->flags & RESOLVE_CLASS_FINAL) + return (SMTPD_CHECK_OK); + if (reply->flags & RESOLVE_CLASS_RELAY) { + if (warn_compat_break_relay_domains) + msg_info("using backwards-compatible default setting " + VAR_RELAY_DOMAINS "=$mydestination to accept mail " + "for domain \"%s\"", domain); + return (SMTPD_CHECK_OK); + } + if (msg_verbose) + msg_info("%s: not local: %s", myname, recipient); + + /* + * Skip numerical forms that didn't match the local system. + */ + if (domain[0] == '[' && domain[strlen(domain) - 1] == ']') + return (SMTPD_CHECK_DUNNO); + + /* + * Fix 20140924: convert domain to ASCII. + */ +#ifndef NO_EAI + if (!allascii(domain) && (adomain = midna_domain_to_ascii(domain)) != 0) { + if (msg_verbose) + msg_info("%s asciified to %s", domain, adomain); + domain = adomain; + } +#endif + + /* + * Look up the list of MX host names for this domain. If no MX host is + * found, perhaps it is a CNAME for the local machine. Clients aren't + * supposed to send CNAMEs in SMTP commands, but it happens anyway. If we + * can't look up the destination, play safe and turn reject into defer. + */ + dns_status = dns_lookup(domain, T_MX, 0, &mx_list, + (VSTRING *) 0, (VSTRING *) 0); +#if 0 + if (dns_status == DNS_NOTFOUND) + return (has_my_addr(state, domain, reply_name, reply_class) ? + SMTPD_CHECK_OK : SMTPD_CHECK_DUNNO); +#endif + if (dns_status != DNS_OK) { /* incl. DNS_INVAL */ + /* We don't special-case DNS_NULLMX. */ + if (dns_status == DNS_RETRY || dns_status == DNS_POLICY) + DEFER_IF_REJECT3(state, MAIL_ERROR_POLICY, + 450, "4.4.4", + "<%s>: %s rejected: Unable to look up mail " + "exchanger information: %s", + reply_name, reply_class, dns_status == DNS_POLICY ? + "DNS reply filter policy" : dns_strerror(h_errno)); + return (SMTPD_CHECK_DUNNO); + } + + /* + * Separate MX list into primaries and backups. + */ + mx_list = dns_rr_sort(mx_list, dns_rr_compare_pref_any); + for (middle = mx_list; /* see below */ ; middle = rest) { + rest = middle->next; + if (rest == 0) + break; + if (rest->pref != mx_list->pref) { + middle->next = 0; + break; + } + } + /* postcondition: middle->next = 0, rest may be 0. */ + +#define PERMIT_MX_BACKUP_RETURN(x) do { \ + middle->next = rest; \ + dns_rr_free(mx_list); \ + return (x); \ + } while (0) + + /* + * First, see if we match any of the primary MX servers. + */ + if (i_am_mx(state, mx_list, reply_name, reply_class)) + PERMIT_MX_BACKUP_RETURN(SMTPD_CHECK_DUNNO); + + /* + * Then, see if we match any of the backup MX servers. + */ + if (rest == 0 || !i_am_mx(state, rest, reply_name, reply_class)) + PERMIT_MX_BACKUP_RETURN(SMTPD_CHECK_DUNNO); + + /* + * Optionally, see if the primary MX hosts are in a restricted list of + * networks. + */ + if (*var_perm_mx_networks + && !permit_mx_primary(state, mx_list, reply_name, reply_class)) + PERMIT_MX_BACKUP_RETURN(SMTPD_CHECK_DUNNO); + + /* + * The destination passed all requirements. + */ + PERMIT_MX_BACKUP_RETURN(SMTPD_CHECK_OK); +} + +/* reject_non_fqdn_address - fail if address is not in fqdn form */ + +static int reject_non_fqdn_address(SMTPD_STATE *state, char *addr, + char *reply_name, char *reply_class) +{ + const char *myname = "reject_non_fqdn_address"; + char *domain; + char *test_dom; + int stat; + + if (msg_verbose) + msg_info("%s: %s", myname, addr); + + /* + * Locate the domain information. + */ + if ((domain = strrchr(addr, '@')) != 0) + domain++; + else + domain = ""; + + /* + * Skip forms that we can't handle yet. + */ + if (domain[0] == '[' && domain[strlen(domain) - 1] == ']') + return (SMTPD_CHECK_DUNNO); + + /* + * Truncate names ending in dot but not dot-dot. + */ + test_dom = dup_if_truncate(domain); + + /* + * Validate the domain. For backwards compatibility, permit non-ASCII + * names only when the client requested SMTPUTF8 support. + */ + if (!*test_dom || !valid_utf8_hostname(state->flags & SMTPD_FLAG_SMTPUTF8, + test_dom, DONT_GRIPE) || !strchr(test_dom, '.')) + stat = smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_non_fqdn_code, "4.5.2", + "<%s>: %s rejected: need fully-qualified address", + reply_name, reply_class); + else + stat = SMTPD_CHECK_DUNNO; + + /* + * Cleanup. + */ + if (test_dom != domain) + myfree(test_dom); + + return (stat); +} + +/* reject_unknown_address - fail if address does not resolve */ + +static int reject_unknown_address(SMTPD_STATE *state, const char *addr, + const char *reply_name, const char *reply_class) +{ + const char *myname = "reject_unknown_address"; + const RESOLVE_REPLY *reply; + const char *domain; + + if (msg_verbose) + msg_info("%s: %s", myname, addr); + + /* + * Resolve the address. + */ + reply = smtpd_resolve_addr(strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + state->recipient : state->sender, addr); + if (reply->flags & RESOLVE_FLAG_FAIL) + reject_dict_retry(state, addr); + + /* + * Skip local destinations and non-DNS forms. + */ + if ((domain = strrchr(CONST_STR(reply->recipient), '@')) == 0) + return (SMTPD_CHECK_DUNNO); + domain += 1; + if (reply->flags & RESOLVE_CLASS_FINAL) + return (SMTPD_CHECK_DUNNO); + if (domain[0] == '[' && domain[strlen(domain) - 1] == ']') + return (SMTPD_CHECK_DUNNO); + + /* + * Look up the name in the DNS. + */ + return (reject_unknown_mailhost(state, domain, reply_name, reply_class)); +} + +/* reject_unverified_address - fail if address bounces */ + +static int reject_unverified_address(SMTPD_STATE *state, const char *addr, + const char *reply_name, const char *reply_class, + int unv_addr_dcode, int unv_addr_rcode, + int unv_addr_tf_act, + const char *alt_reply) +{ + const char *myname = "reject_unverified_address"; + VSTRING *why = vstring_alloc(10); + int rqst_status = SMTPD_CHECK_DUNNO; + int rcpt_status; + int verify_status; + int count; + int reject_code = 0; + + if (msg_verbose) + msg_info("%s: %s", myname, addr); + + /* + * Verify the address. Don't waste too much of their or our time. + */ + for (count = 0; /* see below */ ; /* see below */ ) { + verify_status = verify_clnt_query(addr, &rcpt_status, why); + if (verify_status != VRFY_STAT_OK || rcpt_status != DEL_RCPT_STAT_TODO) + break; + if (++count >= var_verify_poll_count) + break; + sleep(var_verify_poll_delay); + } + if (verify_status != VRFY_STAT_OK) { + msg_warn("%s service failure", var_verify_service); + rqst_status = + DEFER_IF_PERMIT2(unv_addr_tf_act, state, MAIL_ERROR_POLICY, + 450, strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + SND_DSN : "4.1.1", + "<%s>: %s rejected: address verification problem", + reply_name, reply_class); + } else { + switch (rcpt_status) { + default: + msg_warn("unknown address verification status %d", rcpt_status); + break; + case DEL_RCPT_STAT_TODO: + case DEL_RCPT_STAT_DEFER: + reject_code = unv_addr_dcode; + break; + case DEL_RCPT_STAT_OK: + break; + case DEL_RCPT_STAT_BOUNCE: + reject_code = unv_addr_rcode; + break; + } + if (reject_code >= 400 && *alt_reply) + vstring_strcpy(why, alt_reply); + switch (reject_code / 100) { + case 2: + break; + case 4: + rqst_status = + DEFER_IF_PERMIT3(unv_addr_tf_act, state, MAIL_ERROR_POLICY, + reject_code, + strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + SND_DSN : "4.1.1", + "<%s>: %s rejected: unverified address: %.250s", + reply_name, reply_class, STR(why)); + break; + default: + if (reject_code != 0) + rqst_status = + smtpd_check_reject(state, MAIL_ERROR_POLICY, + reject_code, + strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + SND_DSN : "4.1.1", + "<%s>: %s rejected: undeliverable address: %s", + reply_name, reply_class, STR(why)); + break; + } + } + vstring_free(why); + return (rqst_status); +} + +/* can_delegate_action - can we delegate this to the cleanup server */ + +#ifndef TEST + +static int not_in_client_helo(SMTPD_STATE *, const char *, const char *, const char *); + +static int can_delegate_action(SMTPD_STATE *state, const char *table, + const char *action, const char *reply_class) +{ + + /* + * If we're not using the cleanup server, then there is no way that we + * can support actions such as FILTER or HOLD that are delegated to the + * cleanup server. + */ + if (USE_SMTPD_PROXY(state)) { + msg_warn("access table %s: with %s specified, action %s is unavailable", + table, VAR_SMTPD_PROXY_FILT, action); + return (0); + } + + /* + * ETRN does not receive mail so we can't store queue file records. + */ + if (strcmp(state->where, SMTPD_CMD_ETRN) == 0) { + msg_warn("access table %s: action %s is unavailable in %s", + table, action, VAR_ETRN_CHECKS); + return (0); + } + return (not_in_client_helo(state, table, action, reply_class)); +} + +/* not_in_client_helo - not in client or helo restriction context */ + +static int not_in_client_helo(SMTPD_STATE *state, const char *table, + const char *action, + const char *unused_reply_class) +{ + + /* + * If delay_reject=no, then client and helo restrictions take effect + * immediately, outside any particular mail transaction context. For + * example, rejecting HELO does not affect subsequent mail deliveries. + * Thus, if delay_reject=no, client and helo actions such as FILTER or + * HOLD also should not affect subsequent mail deliveries. Hmm... + * + * XXX If the MAIL FROM command is rejected then we have to reset access map + * side effects such as FILTER. + */ + if (state->sender == 0) { + msg_warn("access table %s: with %s=%s, " + "action %s is always skipped in %s or %s restrictions", + table, VAR_SMTPD_DELAY_REJECT, CONFIG_BOOL_NO, + action, SMTPD_NAME_CLIENT, SMTPD_NAME_HELO); + /* XXX What about ETRN? */ + return (0); + } + return (1); +} + +#endif + +/* check_table_result - translate table lookup result into pass/reject */ + +static int check_table_result(SMTPD_STATE *state, const char *table, + const char *value, const char *datum, + const char *reply_name, + const char *reply_class, + const char *def_acl) +{ + const char *myname = "check_table_result"; + int code; + ARGV *restrictions; + jmp_buf savebuf; + int status; + const char *cmd_text; + int cmd_len; + static char def_dsn[] = "5.7.1"; + DSN_SPLIT dp; + static VSTRING *buf; + +#ifdef DELAY_ACTION + int defer_delay; + +#endif + + if (buf == 0) + buf = vstring_alloc(10); + + /* + * Parse into command and text. Do not change the input. + */ + cmd_text = value + strcspn(value, " \t"); + cmd_len = cmd_text - value; + vstring_strncpy(buf, value, cmd_len); + while (*cmd_text && ISSPACE(*cmd_text)) + cmd_text++; + + if (msg_verbose) + msg_info("%s: %s %s %s", myname, table, value, datum); + +#define STREQUAL(x,y,l) (strncasecmp((x), (y), (l)) == 0 && (y)[l] == 0) + + /* + * DUNNO means skip this table. Silently ignore optional text. + */ + if (STREQUAL(value, "DUNNO", cmd_len)) + return (SMTPD_CHECK_DUNNO); + + /* + * REJECT means NO. Use optional text or generate a generic error + * response. + */ + if (STREQUAL(value, "REJECT", cmd_len)) { + dsn_split(&dp, "5.7.1", cmd_text); + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_map_reject_code, + smtpd_dsn_fix(DSN_STATUS(dp.dsn), + reply_class), + "<%s>: %s rejected: %s", + reply_name, reply_class, + *dp.text ? dp.text : "Access denied")); + } + + /* + * DEFER means "try again". Use optional text or generate a generic error + * response. + */ + if (STREQUAL(value, "DEFER", cmd_len)) { + dsn_split(&dp, "4.7.1", cmd_text); + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_map_defer_code, + smtpd_dsn_fix(DSN_STATUS(dp.dsn), + reply_class), + "<%s>: %s rejected: %s", + reply_name, reply_class, + *dp.text ? dp.text : "Access denied")); + } +#ifndef SHUT_RDWR +#define SHUT_RDWR 2 +#endif + + /* + * HANGUP. Text is optional. Drop the connection without sending any + * reply. + * + * Note: this is an unsupported test feature. No attempt is made to maintain + * compatibility between successive versions. + */ + if (STREQUAL(value, "HANGUP", cmd_len)) { + shutdown(vstream_fileno(state->client), SHUT_RDWR); + log_whatsup(state, "hangup", cmd_text); + vstream_longjmp(state->client, SMTP_ERR_QUIET); + } + + /* + * INFO. Text is optional. + */ + if (STREQUAL(value, "INFO", cmd_len)) { + log_whatsup(state, "info", cmd_text); + return (SMTPD_CHECK_DUNNO); + } + + /* + * WARN. Text is optional. + */ + if (STREQUAL(value, "WARN", cmd_len)) { + log_whatsup(state, "warn", cmd_text); + return (SMTPD_CHECK_DUNNO); + } + + /* + * FILTER means deliver to content filter. But we may still change our + * mind, and reject/discard the message for other reasons. + */ + if (STREQUAL(value, "FILTER", cmd_len)) { +#ifndef TEST + if (can_delegate_action(state, table, "FILTER", reply_class) == 0) + return (SMTPD_CHECK_DUNNO); +#endif + if (*cmd_text == 0) { + msg_warn("access table %s entry \"%s\" has FILTER entry without value", + table, datum); + return (SMTPD_CHECK_DUNNO); + } else if (strchr(cmd_text, ':') == 0) { + msg_warn("access table %s entry \"%s\" requires transport:destination", + table, datum); + return (SMTPD_CHECK_DUNNO); + } else { + vstring_sprintf(error_text, "<%s>: %s triggers FILTER %s", + reply_name, reply_class, cmd_text); + log_whatsup(state, "filter", STR(error_text)); +#ifndef TEST + UPDATE_STRING(state->saved_filter, cmd_text); +#endif + return (SMTPD_CHECK_DUNNO); + } + } + + /* + * HOLD means deliver later. But we may still change our mind, and + * reject/discard the message for other reasons. + */ + if (STREQUAL(value, "HOLD", cmd_len)) { +#ifndef TEST + if (can_delegate_action(state, table, "HOLD", reply_class) == 0 + || (state->saved_flags & CLEANUP_FLAG_HOLD)) + return (SMTPD_CHECK_DUNNO); +#endif + vstring_sprintf(error_text, "<%s>: %s %s", reply_name, reply_class, + *cmd_text ? cmd_text : "triggers HOLD action"); + log_whatsup(state, "hold", STR(error_text)); +#ifndef TEST + state->saved_flags |= CLEANUP_FLAG_HOLD; +#endif + return (SMTPD_CHECK_DUNNO); + } + + /* + * DELAY means deliver later. But we may still change our mind, and + * reject/discard the message for other reasons. + * + * This feature is deleted because it has too many problems. 1) It does not + * work on some remote file systems; 2) mail will be delivered anyway + * with "sendmail -q" etc.; 3) while the mail is queued it bogs down the + * deferred queue scan with huge amounts of useless disk I/O operations. + */ +#ifdef DELAY_ACTION + if (STREQUAL(value, "DELAY", cmd_len)) { +#ifndef TEST + if (can_delegate_action(state, table, "DELAY", reply_class) == 0) + return (SMTPD_CHECK_DUNNO); +#endif + if (*cmd_text == 0) { + msg_warn("access table %s entry \"%s\" has DELAY entry without value", + table, datum); + return (SMTPD_CHECK_DUNNO); + } + if (conv_time(cmd_text, &defer_delay, 's') == 0) { + msg_warn("access table %s entry \"%s\" has invalid DELAY argument \"%s\"", + table, datum, cmd_text); + return (SMTPD_CHECK_DUNNO); + } + vstring_sprintf(error_text, "<%s>: %s %s", reply_name, reply_class, + *cmd_text ? cmd_text : "triggers DELAY action"); + log_whatsup(state, "delay", STR(error_text)); +#ifndef TEST + state->saved_delay = defer_delay; +#endif + return (SMTPD_CHECK_DUNNO); + } +#endif + + /* + * DISCARD means silently discard and claim successful delivery. + */ + if (STREQUAL(value, "DISCARD", cmd_len)) { +#ifndef TEST + if (can_delegate_action(state, table, "DISCARD", reply_class) == 0) + return (SMTPD_CHECK_DUNNO); +#endif + vstring_sprintf(error_text, "<%s>: %s %s", reply_name, reply_class, + *cmd_text ? cmd_text : "triggers DISCARD action"); + log_whatsup(state, "discard", STR(error_text)); +#ifndef TEST + state->saved_flags |= CLEANUP_FLAG_DISCARD; + state->discard = 1; +#endif + return (smtpd_acl_permit(state, STR(buf), reply_class, reply_name, + "from %s", table)); + } + + /* + * REDIRECT means deliver to designated recipient. But we may still + * change our mind, and reject/discard the message for other reasons. + */ + if (STREQUAL(value, "REDIRECT", cmd_len)) { +#ifndef TEST + if (can_delegate_action(state, table, "REDIRECT", reply_class) == 0) + return (SMTPD_CHECK_DUNNO); +#endif + if (strchr(cmd_text, '@') == 0) { + msg_warn("access table %s entry \"%s\" requires user@domain target", + table, datum); + return (SMTPD_CHECK_DUNNO); + } else { + vstring_sprintf(error_text, "<%s>: %s triggers REDIRECT %s", + reply_name, reply_class, cmd_text); + log_whatsup(state, "redirect", STR(error_text)); +#ifndef TEST + UPDATE_STRING(state->saved_redirect, cmd_text); +#endif + return (SMTPD_CHECK_DUNNO); + } + } + + /* + * BCC means deliver to designated recipient. But we may still change our + * mind, and reject/discard the message for other reasons. + */ + if (STREQUAL(value, "BCC", cmd_len)) { +#ifndef TEST + if (can_delegate_action(state, table, "BCC", reply_class) == 0) + return (SMTPD_CHECK_DUNNO); +#endif + if (strchr(cmd_text, '@') == 0) { + msg_warn("access table %s entry \"%s\" requires user@domain target", + table, datum); + return (SMTPD_CHECK_DUNNO); + } else { + vstring_sprintf(error_text, "<%s>: %s triggers BCC %s", + reply_name, reply_class, cmd_text); + log_whatsup(state, "bcc", STR(error_text)); +#ifndef TEST + if (state->saved_bcc == 0) + state->saved_bcc = argv_alloc(1); + argv_add(state->saved_bcc, cmd_text, (char *) 0); +#endif + return (SMTPD_CHECK_DUNNO); + } + } + + /* + * DEFER_IF_PERMIT changes "permit" into "maybe". Use optional text or + * generate a generic error response. + */ + if (STREQUAL(value, DEFER_IF_PERMIT, cmd_len)) { + dsn_split(&dp, "4.7.1", cmd_text); + return (DEFER_IF_PERMIT3(DEFER_IF_PERMIT_ACT, state, MAIL_ERROR_POLICY, + var_map_defer_code, + smtpd_dsn_fix(DSN_STATUS(dp.dsn), reply_class), + "<%s>: %s rejected: %s", + reply_name, reply_class, + *dp.text ? dp.text : "Service unavailable")); + } + + /* + * DEFER_IF_REJECT changes "reject" into "maybe". Use optional text or + * generate a generic error response. + */ + if (STREQUAL(value, DEFER_IF_REJECT, cmd_len)) { + dsn_split(&dp, "4.7.1", cmd_text); + DEFER_IF_REJECT3(state, MAIL_ERROR_POLICY, + var_map_defer_code, + smtpd_dsn_fix(DSN_STATUS(dp.dsn), reply_class), + "<%s>: %s rejected: %s", + reply_name, reply_class, + *dp.text ? dp.text : "Service unavailable"); + return (SMTPD_CHECK_DUNNO); + } + + /* + * PREPEND prepends the specified message header text. + */ + if (STREQUAL(value, "PREPEND", cmd_len)) { +#ifndef TEST + /* XXX what about ETRN. */ + if (not_in_client_helo(state, table, "PREPEND", reply_class) == 0) + return (SMTPD_CHECK_DUNNO); +#endif + if (strcmp(state->where, SMTPD_AFTER_EOM) == 0) { + msg_warn("access table %s: action PREPEND must be used before %s", + table, VAR_EOD_CHECKS); + return (SMTPD_CHECK_DUNNO); + } + if (*cmd_text == 0 || is_header(cmd_text) == 0) { + msg_warn("access table %s entry \"%s\" requires header: text", + table, datum); + return (SMTPD_CHECK_DUNNO); + } else { + if (state->prepend == 0) + state->prepend = argv_alloc(1); + argv_add(state->prepend, cmd_text, (char *) 0); + return (SMTPD_CHECK_DUNNO); + } + } + + /* + * All-numeric result probably means OK - some out-of-band authentication + * mechanism uses this as time stamp. + */ + if (alldig(value)) + return (smtpd_acl_permit(state, STR(buf), reply_class, reply_name, + "from %s", table)); + + /* + * 4xx or 5xx means NO as well. smtpd_check_reject() will validate the + * response status code. + * + * If the caller specifies an RFC 3463 enhanced status code, put it + * immediately after the SMTP status code as described in RFC 2034. + */ + if (cmd_len == 3 && *cmd_text + && (value[0] == '4' || value[0] == '5') + && ISDIGIT(value[1]) && ISDIGIT(value[2])) { + code = atoi(value); + def_dsn[0] = value[0]; + dsn_split(&dp, def_dsn, cmd_text); + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, + code, + smtpd_dsn_fix(DSN_STATUS(dp.dsn), + reply_class), + "<%s>: %s rejected: %s", + reply_name, reply_class, + *dp.text ? dp.text : "Access denied")); + } + + /* + * OK or RELAY means YES. Ignore trailing text. + */ + if (STREQUAL(value, "OK", cmd_len) || STREQUAL(value, "RELAY", cmd_len)) + return (smtpd_acl_permit(state, STR(buf), reply_class, reply_name, + "from %s", table)); + + /* + * Unfortunately, maps must be declared ahead of time so they can be + * opened before we go to jail. We could insist that the RHS can only + * contain a pre-defined restriction class name, but that would be too + * restrictive. Instead we warn if an access table references any map. + * + * XXX Don't use passwd files or address rewriting maps as access tables. + */ + if (strchr(value, ':') != 0) { + msg_warn("access table %s has entry with lookup table: %s", + table, value); + msg_warn("do not specify lookup tables inside SMTPD access maps"); + msg_warn("define a restriction class and specify its name instead."); + reject_server_error(state); + } + + /* + * Don't get carried away with recursion. + */ + if (state->recursion > 100) { + msg_warn("access table %s entry %s causes unreasonable recursion", + table, value); + reject_server_error(state); + } + + /* + * Recursively evaluate the restrictions given in the right-hand side. In + * the dark ages, an empty right-hand side meant OK. Make some + * discouraging comments. + * + * XXX Jump some hoops to avoid a minute memory leak in case of a file + * configuration error. + */ +#define ADDROF(x) ((char *) &(x)) + + restrictions = argv_splitq(value, CHARS_COMMA_SP, CHARS_BRACE); + memcpy(ADDROF(savebuf), ADDROF(smtpd_check_buf), sizeof(savebuf)); + status = setjmp(smtpd_check_buf); + if (status != 0) { + argv_free(restrictions); + memcpy(ADDROF(smtpd_check_buf), ADDROF(savebuf), + sizeof(smtpd_check_buf)); + longjmp(smtpd_check_buf, status); + } + if (restrictions->argc == 0) { + msg_warn("access table %s entry %s has empty value", + table, value); + status = SMTPD_CHECK_OK; + } else { + status = generic_checks(state, restrictions, reply_name, + reply_class, def_acl); + } + argv_free(restrictions); + memcpy(ADDROF(smtpd_check_buf), ADDROF(savebuf), sizeof(smtpd_check_buf)); + return (status); +} + +/* check_access - table lookup without substring magic */ + +static int check_access(SMTPD_STATE *state, const char *table, const char *name, + int flags, int *found, const char *reply_name, + const char *reply_class, const char *def_acl) +{ + const char *myname = "check_access"; + const char *value; + MAPS *maps; + +#define CHK_ACCESS_RETURN(x,y) \ + { *found = y; return(x); } +#define FULL 0 +#define PARTIAL DICT_FLAG_FIXED +#define FOUND 1 +#define MISSED 0 + + if (msg_verbose) + msg_info("%s: %s", myname, name); + + if ((maps = (MAPS *) htable_find(map_command_table, table)) == 0) { + msg_warn("%s: unexpected dictionary: %s", myname, table); + value = "451 4.3.5 Server configuration error"; + CHK_ACCESS_RETURN(check_table_result(state, table, value, name, + reply_name, reply_class, + def_acl), FOUND); + } + if ((value = maps_find(maps, name, flags)) != 0) + CHK_ACCESS_RETURN(check_table_result(state, table, value, name, + reply_name, reply_class, + def_acl), FOUND); + if (maps->error != 0) { + /* Warning is already logged. */ + value = "451 4.3.5 Server configuration error"; + CHK_ACCESS_RETURN(check_table_result(state, table, value, name, + reply_name, reply_class, + def_acl), FOUND); + } + CHK_ACCESS_RETURN(SMTPD_CHECK_DUNNO, MISSED); +} + +/* check_domain_access - domainname-based table lookup */ + +static int check_domain_access(SMTPD_STATE *state, const char *table, + const char *domain, int flags, + int *found, const char *reply_name, + const char *reply_class, + const char *def_acl) +{ + const char *myname = "check_domain_access"; + const char *name; + const char *next; + const char *value; + MAPS *maps; + int maybe_numerical = 1; + + if (msg_verbose) + msg_info("%s: %s", myname, domain); + + /* + * Try the name and its parent domains. Including top-level domains. + * + * Helo names can end in ".". The test below avoids lookups of the empty + * key, because Berkeley DB cannot deal with it. [Victor Duchovni, Morgan + * Stanley]. + * + * TODO(wietse) move to mail_domain_find library module. + */ +#define CHK_DOMAIN_RETURN(x,y) { *found = y; return(x); } + + if ((maps = (MAPS *) htable_find(map_command_table, table)) == 0) { + msg_warn("%s: unexpected dictionary: %s", myname, table); + value = "451 4.3.5 Server configuration error"; + CHK_DOMAIN_RETURN(check_table_result(state, table, value, + domain, reply_name, reply_class, + def_acl), FOUND); + } + for (name = domain; *name != 0; name = next) { + if ((value = maps_find(maps, name, flags)) != 0) + CHK_DOMAIN_RETURN(check_table_result(state, table, value, + domain, reply_name, reply_class, + def_acl), FOUND); + if (maps->error != 0) { + /* Warning is already logged. */ + value = "451 4.3.5 Server configuration error"; + CHK_DOMAIN_RETURN(check_table_result(state, table, value, + domain, reply_name, reply_class, + def_acl), FOUND); + } + /* Don't apply subdomain magic to numerical hostnames. */ + if (maybe_numerical + && (maybe_numerical = valid_hostaddr(domain, DONT_GRIPE)) != 0) + break; + if ((next = strchr(name + 1, '.')) == 0) + break; + if (access_parent_style == MATCH_FLAG_PARENT) + next += 1; + flags = PARTIAL; + } + CHK_DOMAIN_RETURN(SMTPD_CHECK_DUNNO, MISSED); +} + +/* check_addr_access - address-based table lookup */ + +static int check_addr_access(SMTPD_STATE *state, const char *table, + const char *address, int flags, + int *found, const char *reply_name, + const char *reply_class, + const char *def_acl) +{ + const char *myname = "check_addr_access"; + char *addr; + const char *value; + MAPS *maps; + int delim; + + if (msg_verbose) + msg_info("%s: %s", myname, address); + + /* + * Try the address and its parent networks. + * + * TODO(wietse) move to mail_ipaddr_find library module. + */ +#define CHK_ADDR_RETURN(x,y) { *found = y; return(x); } + + addr = STR(vstring_strcpy(error_text, address)); +#ifdef HAS_IPV6 + if (strchr(addr, ':') != 0) + delim = ':'; + else +#endif + delim = '.'; + + if ((maps = (MAPS *) htable_find(map_command_table, table)) == 0) { + msg_warn("%s: unexpected dictionary: %s", myname, table); + value = "451 4.3.5 Server configuration error"; + CHK_ADDR_RETURN(check_table_result(state, table, value, address, + reply_name, reply_class, + def_acl), FOUND); + } + do { + if ((value = maps_find(maps, addr, flags)) != 0) + CHK_ADDR_RETURN(check_table_result(state, table, value, address, + reply_name, reply_class, + def_acl), FOUND); + if (maps->error != 0) { + /* Warning is already logged. */ + value = "451 4.3.5 Server configuration error"; + CHK_ADDR_RETURN(check_table_result(state, table, value, address, + reply_name, reply_class, + def_acl), FOUND); + } + flags = PARTIAL; + } while (split_at_right(addr, delim)); + + CHK_ADDR_RETURN(SMTPD_CHECK_DUNNO, MISSED); +} + +/* check_namadr_access - OK/FAIL based on host name/address lookup */ + +static int check_namadr_access(SMTPD_STATE *state, const char *table, + const char *name, const char *addr, + int flags, int *found, + const char *reply_name, + const char *reply_class, + const char *def_acl) +{ + const char *myname = "check_namadr_access"; + int status; + + if (msg_verbose) + msg_info("%s: name %s addr %s", myname, name, addr); + + /* + * Look up the host name, or parent domains thereof. XXX A domain + * wildcard may pre-empt a more specific address table entry. + */ + if ((status = check_domain_access(state, table, name, flags, + found, reply_name, reply_class, + def_acl)) != 0 || *found) + return (status); + + /* + * Look up the network address, or parent networks thereof. + */ + if ((status = check_addr_access(state, table, addr, flags, + found, reply_name, reply_class, + def_acl)) != 0 || *found) + return (status); + + /* + * Undecided when the host was not found. + */ + return (SMTPD_CHECK_DUNNO); +} + +/* check_server_access - access control by server host name or address */ + +static int check_server_access(SMTPD_STATE *state, const char *table, + const char *name, + int type, + const char *reply_name, + const char *reply_class, + const char *def_acl) +{ + const char *myname = "check_server_access"; + const char *domain; + const char *adomain; + int dns_status; + DNS_RR *server_list; + DNS_RR *server; + int found = 0; + MAI_HOSTADDR_STR addr_string; + int aierr; + struct addrinfo *res0; + struct addrinfo *res; + int status; + INET_PROTO_INFO *proto_info; + + /* + * Sanity check. + */ + if (type != T_MX && type != T_NS && type != T_A +#ifdef HAS_IPV6 + && type != T_AAAA +#endif + ) + msg_panic("%s: unexpected resource type \"%s\" in request", + myname, dns_strtype(type)); + + if (msg_verbose) + msg_info("%s: %s %s", myname, dns_strtype(type), name); + + /* + * Skip over local-part. + */ + if ((domain = strrchr(name, '@')) != 0) + domain += 1; + else + domain = name; + + /* + * Treat an address literal as its own MX server, just like we treat a + * name without MX record as its own MX server. There is, however, no + * applicable NS server equivalent. + */ + if (*domain == '[') { + char *saved_addr; + const char *bare_addr; + ssize_t len; + + if (type != T_A && type != T_MX) + return (SMTPD_CHECK_DUNNO); + len = strlen(domain); + if (domain[len - 1] != ']') + return (SMTPD_CHECK_DUNNO); + /* Memory leak alert: no early returns after this point. */ + saved_addr = mystrndup(domain + 1, len - 2); + if ((bare_addr = valid_mailhost_addr(saved_addr, DONT_GRIPE)) == 0) + status = SMTPD_CHECK_DUNNO; + else + status = check_addr_access(state, table, bare_addr, FULL, + &found, reply_name, reply_class, + def_acl); + myfree(saved_addr); + return (status); + } + + /* + * Fix 20140924: convert domain to ASCII. + */ +#ifndef NO_EAI + if (!allascii(domain) && (adomain = midna_domain_to_ascii(domain)) != 0) { + if (msg_verbose) + msg_info("%s asciified to %s", domain, adomain); + domain = adomain; + } +#endif + + /* + * If the request is type A or AAAA, fabricate an MX record that points + * to the domain name itself, and skip name-based access control. + * + * If the domain name does not exist then we apply no restriction. + * + * If the domain name exists but no MX record exists, fabricate an MX record + * that points to the domain name itself. + * + * If the domain name exists but no NS record exists, look up parent domain + * NS records. + * + * XXX 20150707 Work around broken DNS servers that reply with NXDOMAIN + * instead of "no data". + */ + if (type == T_A +#ifdef HAS_IPV6 + || type == T_AAAA +#endif + ) { + server_list = dns_rr_create(domain, domain, T_MX, C_IN, 0, 0, + domain, strlen(domain) + 1); + } else { + dns_status = dns_lookup(domain, type, 0, &server_list, + (VSTRING *) 0, (VSTRING *) 0); + if (dns_status == DNS_NULLMX) + return (SMTPD_CHECK_DUNNO); + if (dns_status == DNS_NOTFOUND /* Not: h_errno == NO_DATA */ ) { + if (type == T_MX) { + server_list = dns_rr_create(domain, domain, type, C_IN, 0, 0, + domain, strlen(domain) + 1); + dns_status = DNS_OK; + } else if (type == T_NS /* && h_errno == NO_DATA */ ) { + while ((domain = strchr(domain, '.')) != 0 && domain[1]) { + domain += 1; + dns_status = dns_lookup(domain, type, 0, &server_list, + (VSTRING *) 0, (VSTRING *) 0); + if (dns_status != DNS_NOTFOUND /* || h_errno != NO_DATA */ ) + break; + } + } + } + if (dns_status != DNS_OK) { + msg_warn("Unable to look up %s host for %s: %s", dns_strtype(type), + domain && domain[1] ? domain : name, dns_status == DNS_POLICY ? + "DNS reply filter policy" : dns_strerror(h_errno)); + return (SMTPD_CHECK_DUNNO); + } + } + + /* + * No bare returns after this point or we have a memory leak. + */ +#define CHECK_SERVER_RETURN(x) { dns_rr_free(server_list); return(x); } + + /* + * Check the hostnames first, then the addresses. + */ + proto_info = inet_proto_info(); + for (server = server_list; server != 0; server = server->next) { + if (msg_verbose) + msg_info("%s: %s hostname check: %s", + myname, dns_strtype(type), (char *) server->data); + if (valid_hostaddr((char *) server->data, DONT_GRIPE)) { + if ((status = check_addr_access(state, table, (char *) server->data, + FULL, &found, reply_name, reply_class, + def_acl)) != 0 || found) + CHECK_SERVER_RETURN(status); + continue; + } + if (type != T_A && type != T_AAAA + && ((status = check_domain_access(state, table, (char *) server->data, + FULL, &found, reply_name, reply_class, + def_acl)) != 0 || found)) + CHECK_SERVER_RETURN(status); + if ((aierr = hostname_to_sockaddr((char *) server->data, + (char *) 0, 0, &res0)) != 0) { + if (type != T_A && type != T_AAAA) + msg_warn("Unable to look up %s host %s for %s %s: %s", + dns_strtype(type), (char *) server->data, + reply_class, reply_name, MAI_STRERROR(aierr)); + continue; + } + /* Now we must also free the addrinfo result. */ + if (msg_verbose) + msg_info("%s: %s host address check: %s", + myname, dns_strtype(type), (char *) server->data); + for (res = res0; res != 0; res = res->ai_next) { + if (strchr((char *) proto_info->sa_family_list, res->ai_family) == 0) { + if (msg_verbose) + msg_info("skipping address family %d for host %s", + res->ai_family, server->data); + continue; + } + SOCKADDR_TO_HOSTADDR(res->ai_addr, res->ai_addrlen, + &addr_string, (MAI_SERVPORT_STR *) 0, 0); + status = check_addr_access(state, table, addr_string.buf, FULL, + &found, reply_name, reply_class, + def_acl); + if (status != 0 || found) { + freeaddrinfo(res0); /* 200412 */ + CHECK_SERVER_RETURN(status); + } + } + freeaddrinfo(res0); /* 200412 */ + } + CHECK_SERVER_RETURN(SMTPD_CHECK_DUNNO); +} + +/* check_ccert_access - access for TLS clients by certificate fingerprint */ + + +static int check_ccert_access(SMTPD_STATE *state, const char *table, + const char *def_acl) +{ + int result = SMTPD_CHECK_DUNNO; + +#ifdef USE_TLS + const char *myname = "check_ccert_access"; + int found; + + /* + * When directly checking the fingerprint, it is OK if the issuing CA is + * not trusted. + */ + if (TLS_CERT_IS_PRESENT(state->tls_context)) { + int i; + char *prints[2]; + + prints[0] = state->tls_context->peer_cert_fprint; + prints[1] = state->tls_context->peer_pkey_fprint; + + for (i = 0; i < 2; ++i) { + if (msg_verbose) + msg_info("%s: %s", myname, prints[i]); + + /* + * Regexp tables don't make sense for certificate fingerprints. + * That may be so, but we can't ignore the entire + * check_ccert_access request without logging a warning. + * + * Log the peer CommonName when access is denied. Non-printable + * characters will be neutered by smtpd_check_reject(). The SMTP + * client name and address are always syslogged as part of a + * "reject" event. + */ + result = check_access(state, table, prints[i], + DICT_FLAG_NONE, &found, + state->tls_context->peer_CN, + SMTPD_NAME_CCERT, def_acl); + if (result != SMTPD_CHECK_DUNNO) + break; + } + } +#endif + return (result); +} + +/* check_sasl_access - access by SASL user name */ + +#ifdef USE_SASL_AUTH + +static int check_sasl_access(SMTPD_STATE *state, const char *table, + const char *def_acl) +{ + int result; + int unused_found; + char *sane_username = printable(mystrdup(state->sasl_username), '_'); + + result = check_access(state, table, state->sasl_username, + DICT_FLAG_NONE, &unused_found, sane_username, + SMTPD_NAME_SASL_USER, def_acl); + myfree(sane_username); + return (result); +} + +#endif + +/* check_mail_access - OK/FAIL based on mail address lookup */ + +static int check_mail_access(SMTPD_STATE *state, const char *table, + const char *addr, int *found, + const char *reply_name, + const char *reply_class, + const char *def_acl) +{ + const char *myname = "check_mail_access"; + const RESOLVE_REPLY *reply; + const char *value; + int lookup_strategy; + int status; + MAPS *maps; + + if (msg_verbose) + msg_info("%s: %s", myname, addr); + + /* + * Resolve the address. + */ + reply = smtpd_resolve_addr(strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + state->recipient : state->sender, addr); + if (reply->flags & RESOLVE_FLAG_FAIL) + reject_dict_retry(state, addr); + + /* + * Garbage in, garbage out. Every address from rewrite_clnt_internal() + * and from resolve_clnt_query() must be fully qualified. + */ + if (strrchr(CONST_STR(reply->recipient), '@') == 0) { + msg_warn("%s: no @domain in address: %s", myname, + CONST_STR(reply->recipient)); + return (0); + } + + /* + * Source-routed (non-local or virtual) recipient addresses are too + * suspicious for returning an "OK" result. The complicated expression + * below was brought to you by the keyboard of Victor Duchovni, Morgan + * Stanley and hacked up a bit by Wietse. + */ +#define SUSPICIOUS(reply, reply_class) \ + (var_allow_untrust_route == 0 \ + && (reply->flags & RESOLVE_FLAG_ROUTED) \ + && strcmp(reply_class, SMTPD_NAME_RECIPIENT) == 0) + + /* + * Look up user+foo@domain if the address has an extension, user@domain + * otherwise. + */ + lookup_strategy = MA_FIND_FULL | MA_FIND_NOEXT | MA_FIND_DOMAIN + | MA_FIND_LOCALPART_AT + | (access_parent_style == MATCH_FLAG_PARENT ? + MA_FIND_PDMS : MA_FIND_PDDMDS); + + if ((maps = (MAPS *) htable_find(map_command_table, table)) == 0) { + msg_warn("%s: unexpected dictionary: %s", myname, table); + value = "451 4.3.5 Server configuration error"; + return (check_table_result(state, table, value, + CONST_STR(reply->recipient), + reply_name, reply_class, + def_acl)); + } + if ((value = mail_addr_find_strategy(maps, CONST_STR(reply->recipient), + (char **) 0, lookup_strategy)) != 0) { + *found = 1; + status = check_table_result(state, table, value, + CONST_STR(reply->recipient), + reply_name, reply_class, def_acl); + return (status == SMTPD_CHECK_OK && SUSPICIOUS(reply, reply_class) ? + SMTPD_CHECK_DUNNO : status); + } else if (maps->error != 0) { + /* Warning is already logged. */ + value = "451 4.3.5 Server configuration error"; + return (check_table_result(state, table, value, + CONST_STR(reply->recipient), + reply_name, reply_class, + def_acl)); + } + + /* + * Undecided when no match found. + */ + return (SMTPD_CHECK_DUNNO); +} + +/* Support for different DNSXL lookup results. */ + +static SMTPD_RBL_STATE dnsxl_stat_soft[1]; + +#define SMTPD_DNSXL_STAT_SOFT(dnsxl_res) ((dnsxl_res) == dnsxl_stat_soft) +#define SMTPD_DNXSL_STAT_HARD(dnsxl_res) ((dnsxl_res) == 0) +#define SMTPD_DNSXL_STAT_OK(dnsxl_res) \ + !(SMTPD_DNXSL_STAT_HARD(dnsxl_res) || SMTPD_DNSXL_STAT_SOFT(dnsxl_res)) + +/* rbl_pagein - look up an RBL lookup result */ + +static void *rbl_pagein(const char *query, void *unused_context) +{ + DNS_RR *txt_list; + VSTRING *why; + int dns_status; + SMTPD_RBL_STATE *rbl = 0; + DNS_RR *addr_list; + DNS_RR *rr; + DNS_RR *next; + VSTRING *buf; + int space_left; + + /* + * Do the query. If the DNS lookup produces no definitive reply, give the + * requestor the benefit of the doubt. We can't block all email simply + * because an RBL server is unavailable. + * + * Don't do this for AAAA records. Yet. + */ + why = vstring_alloc(10); + dns_status = dns_lookup(query, T_A, 0, &addr_list, (VSTRING *) 0, why); + if (dns_status != DNS_OK && dns_status != DNS_NOTFOUND) { + msg_warn("%s: RBL lookup error: %s", query, STR(why)); + rbl = dnsxl_stat_soft; + } + vstring_free(why); + if (dns_status != DNS_OK) + return ((void *) rbl); + + /* + * Save the result. Yes, we cache negative results as well as positive + * results. Concatenate multiple TXT records, up to some limit. + */ +#define RBL_TXT_LIMIT 500 + + rbl = (SMTPD_RBL_STATE *) mymalloc(sizeof(*rbl)); + dns_status = dns_lookup(query, T_TXT, 0, &txt_list, + (VSTRING *) 0, (VSTRING *) 0); + if (dns_status == DNS_OK) { + buf = vstring_alloc(1); + space_left = RBL_TXT_LIMIT; + for (rr = txt_list; rr != 0 && space_left > 0; rr = next) { + vstring_strncat(buf, rr->data, (int) rr->data_len > space_left ? + space_left : rr->data_len); + space_left = RBL_TXT_LIMIT - VSTRING_LEN(buf); + next = rr->next; + if (next && space_left > 3) { + vstring_strcat(buf, " / "); + space_left -= 3; + } + } + rbl->txt = vstring_export(buf); + dns_rr_free(txt_list); + } else { + if (dns_status == DNS_POLICY) + msg_warn("%s: TXT lookup error: %s", + query, "DNS reply filter drops all results"); + rbl->txt = 0; + } + rbl->a = addr_list; + return ((void *) rbl); +} + +/* rbl_pageout - discard an RBL lookup result */ + +static void rbl_pageout(void *data, void *unused_context) +{ + SMTPD_RBL_STATE *rbl = (SMTPD_RBL_STATE *) data; + + if (SMTPD_DNSXL_STAT_OK(rbl)) { + if (rbl->txt) + myfree(rbl->txt); + if (rbl->a) + dns_rr_free(rbl->a); + myfree((void *) rbl); + } +} + +/* rbl_byte_pagein - parse RBL reply pattern, save byte codes */ + +static void *rbl_byte_pagein(const char *query, void *unused_context) +{ + VSTRING *byte_codes = vstring_alloc(100); + char *saved_query = mystrdup(query); + char *saved_byte_codes; + char *err; + + if ((err = ip_match_parse(byte_codes, saved_query)) != 0) + msg_fatal("RBL reply error: %s", err); + saved_byte_codes = ip_match_save(byte_codes); + myfree(saved_query); + vstring_free(byte_codes); + return (saved_byte_codes); +} + +/* rbl_byte_pageout - discard parsed RBL reply byte codes */ + +static void rbl_byte_pageout(void *data, void *unused_context) +{ + myfree(data); +} + +/* rbl_expand_lookup - RBL specific $name expansion */ + +static const char *rbl_expand_lookup(const char *name, int mode, + void *context) +{ + SMTPD_RBL_EXPAND_CONTEXT *rbl_exp = (SMTPD_RBL_EXPAND_CONTEXT *) context; + SMTPD_STATE *state = rbl_exp->state; + +#define STREQ(x,y) (*(x) == *(y) && strcmp((x), (y)) == 0) + + if (state->expand_buf == 0) + state->expand_buf = vstring_alloc(10); + + if (msg_verbose > 1) + msg_info("rbl_expand_lookup: ${%s}", name); + + /* + * Be sure to return NULL only for non-existent names. + */ + if (STREQ(name, MAIL_ATTR_RBL_CODE)) { + vstring_sprintf(state->expand_buf, "%d", var_maps_rbl_code); + return (STR(state->expand_buf)); + } else if (STREQ(name, MAIL_ATTR_RBL_DOMAIN)) { + return (rbl_exp->domain); + } else if (STREQ(name, MAIL_ATTR_RBL_REASON)) { + return (rbl_exp->txt); + } else if (STREQ(name, MAIL_ATTR_RBL_TXT)) {/* LaMont compat */ + return (rbl_exp->txt); + } else if (STREQ(name, MAIL_ATTR_RBL_WHAT)) { + return (rbl_exp->what); + } else if (STREQ(name, MAIL_ATTR_RBL_CLASS)) { + return (rbl_exp->class); + } else { + return (smtpd_expand_lookup(name, mode, (void *) state)); + } +} + +/* rbl_reject_reply - format reply after RBL reject */ + +static int rbl_reject_reply(SMTPD_STATE *state, const SMTPD_RBL_STATE *rbl, + const char *rbl_domain, + const char *what, + const char *reply_class) +{ + const char *myname = "rbl_reject_reply"; + VSTRING *why = 0; + const char *template = 0; + SMTPD_RBL_EXPAND_CONTEXT rbl_exp; + int result; + DSN_SPLIT dp; + int code; + + /* + * Use the server-specific reply template or use the default one. + */ + if (*var_rbl_reply_maps) { + template = maps_find(rbl_reply_maps, rbl_domain, DICT_FLAG_NONE); + if (rbl_reply_maps->error) + reject_server_error(state); + } + why = vstring_alloc(100); + rbl_exp.state = state; + rbl_exp.domain = mystrdup(rbl_domain); + (void) split_at(rbl_exp.domain, '='); + rbl_exp.what = what; + rbl_exp.class = reply_class; + rbl_exp.txt = (rbl->txt == 0 ? "" : rbl->txt); + + for (;;) { + if (template == 0) + template = var_def_rbl_reply; + if (mac_expand(why, template, MAC_EXP_FLAG_NONE, + STR(smtpd_expand_filter), rbl_expand_lookup, + (void *) &rbl_exp) == 0) + break; + if (template == var_def_rbl_reply) + msg_fatal("%s: bad default rbl reply template: %s", + myname, var_def_rbl_reply); + msg_warn("%s: bad rbl reply template for domain %s: %s", + myname, rbl_domain, template); + template = 0; /* pretend not found */ + } + + /* + * XXX Impedance mis-match. + * + * Validate the response, that is, the response must begin with a + * three-digit status code, and the first digit must be 4 or 5. If the + * response is bad, log a warning and send a generic response instead. + */ + if ((STR(why)[0] != '4' && STR(why)[0] != '5') + || !ISDIGIT(STR(why)[1]) || !ISDIGIT(STR(why)[2]) + || STR(why)[3] != ' ') { + msg_warn("rbl response code configuration error: %s", STR(why)); + result = smtpd_check_reject(state, MAIL_ERROR_POLICY, + 450, "4.7.1", "Service unavailable"); + } else { + code = atoi(STR(why)); + dsn_split(&dp, "4.7.1", STR(why) + 4); + result = smtpd_check_reject(state, MAIL_ERROR_POLICY, + code, + smtpd_dsn_fix(DSN_STATUS(dp.dsn), + reply_class), + "%s", *dp.text ? + dp.text : "Service unavailable"); + } + + /* + * Clean up. + */ + myfree(rbl_exp.domain); + vstring_free(why); + + return (result); +} + +/* rbl_match_addr - match address list */ + +static int rbl_match_addr(SMTPD_RBL_STATE *rbl, const char *byte_codes) +{ + const char *myname = "rbl_match_addr"; + DNS_RR *rr; + + for (rr = rbl->a; rr != 0; rr = rr->next) { + if (rr->type == T_A) { + if (ip_match_execute(byte_codes, rr->data)) + return (1); + } else { + msg_warn("%s: skipping record type %s for query %s", + myname, dns_strtype(rr->type), rr->qname); + } + } + return (0); +} + +/* find_dnsxl_addr - look up address in DNSXL */ + +static const SMTPD_RBL_STATE *find_dnsxl_addr(SMTPD_STATE *state, + const char *rbl_domain, + const char *addr) +{ + const char *myname = "find_dnsxl_addr"; + ARGV *octets; + VSTRING *query; + int i; + SMTPD_RBL_STATE *rbl; + const char *reply_addr; + const char *byte_codes; + struct addrinfo *res; + unsigned char *ipv6_addr; + + query = vstring_alloc(100); + + /* + * Reverse the client IPV6 address, represented as 32 hexadecimal + * nibbles. We use the binary address to avoid tricky code. Asking for an + * AAAA record makes no sense here. Just like with IPv4 we use the lookup + * result as a bit mask, not as an IP address. + */ +#ifdef HAS_IPV6 + if (valid_ipv6_hostaddr(addr, DONT_GRIPE)) { + if (hostaddr_to_sockaddr(addr, (char *) 0, 0, &res) != 0 + || res->ai_family != PF_INET6) + msg_fatal("%s: unable to convert address %s", myname, addr); + ipv6_addr = (unsigned char *) &SOCK_ADDR_IN6_ADDR(res->ai_addr); + for (i = sizeof(SOCK_ADDR_IN6_ADDR(res->ai_addr)) - 1; i >= 0; i--) + vstring_sprintf_append(query, "%x.%x.", + ipv6_addr[i] & 0xf, ipv6_addr[i] >> 4); + freeaddrinfo(res); + } else +#endif + + /* + * Reverse the client IPV4 address, represented as four decimal octet + * values. We use the textual address for convenience. + */ + { + octets = argv_split(addr, "."); + for (i = octets->argc - 1; i >= 0; i--) { + vstring_strcat(query, octets->argv[i]); + vstring_strcat(query, "."); + } + argv_free(octets); + } + + /* + * Tack on the RBL domain name and query the DNS for an A record. + */ + vstring_strcat(query, rbl_domain); + reply_addr = split_at(STR(query), '='); + rbl = (SMTPD_RBL_STATE *) ctable_locate(smtpd_rbl_cache, STR(query)); + if (reply_addr != 0) + byte_codes = ctable_locate(smtpd_rbl_byte_cache, reply_addr); + + /* + * If the record exists, match the result address. + */ + if (SMTPD_DNSXL_STAT_OK(rbl) && reply_addr != 0 + && !rbl_match_addr(rbl, byte_codes)) + rbl = 0; + vstring_free(query); + return (rbl); +} + +/* reject_rbl_addr - reject address in real-time blackhole list */ + +static int reject_rbl_addr(SMTPD_STATE *state, const char *rbl_domain, + const char *addr, const char *reply_class) +{ + const char *myname = "reject_rbl_addr"; + const SMTPD_RBL_STATE *rbl; + + if (msg_verbose) + msg_info("%s: %s %s", myname, reply_class, addr); + + rbl = find_dnsxl_addr(state, rbl_domain, addr); + if (!SMTPD_DNSXL_STAT_OK(rbl)) { + return (SMTPD_CHECK_DUNNO); + } else { + return (rbl_reject_reply(state, rbl, rbl_domain, addr, reply_class)); + } +} + +/* permit_dnswl_addr - permit address in DNSWL */ + +static int permit_dnswl_addr(SMTPD_STATE *state, const char *dnswl_domain, + const char *addr, const char *reply_class) +{ + const char *myname = "permit_dnswl_addr"; + const SMTPD_RBL_STATE *dnswl_result; + + if (msg_verbose) + msg_info("%s: %s", myname, addr); + + /* Safety: don't whitelist unauthorized recipients. */ + if (strcmp(state->where, SMTPD_CMD_RCPT) == 0 && state->recipient != 0 + && permit_auth_destination(state, state->recipient) != SMTPD_CHECK_OK) + return (SMTPD_CHECK_DUNNO); + + dnswl_result = find_dnsxl_addr(state, dnswl_domain, addr); + if (SMTPD_DNXSL_STAT_HARD(dnswl_result)) { + return (SMTPD_CHECK_DUNNO); + } else if (SMTPD_DNSXL_STAT_SOFT(dnswl_result)) { + /* XXX: Make configurable as dnswl_tempfail_action. */ + DEFER_IF_REJECT3(state, MAIL_ERROR_POLICY, + 450, "4.7.1", + "<%s>: %s rejected: %s", + addr, reply_class, + "Service unavailable"); + return (SMTPD_CHECK_DUNNO); + } else if (SMTPD_DNSXL_STAT_OK(dnswl_result)) { + return (SMTPD_CHECK_OK); + } else { + /* Future proofing, in case find_dnsxl_addr() result is changed. */ + msg_panic("%s: find_dnsxl_addr API failure", myname); + } +} + +/* find_dnsxl_domain - reject if domain in real-time blackhole list */ + +static const SMTPD_RBL_STATE *find_dnsxl_domain(SMTPD_STATE *state, + const char *rbl_domain, const char *what) +{ + VSTRING *query; + SMTPD_RBL_STATE *rbl; + const char *domain; + const char *reply_addr; + const char *byte_codes; + const char *suffix; + const char *adomain; + + /* + * Extract the domain, tack on the RBL domain name and query the DNS for + * an A record. + */ + if ((domain = strrchr(what, '@')) != 0) { + domain += 1; + if (domain[0] == '[') + return (SMTPD_CHECK_DUNNO); + } else + domain = what; + + /* + * XXX Some Spamhaus RHSBL rejects lookups with "No IP queries" even if + * the name has an alphanumerical prefix. We play safe, and skip both + * RHSBL and RHSWL queries for names ending in a numerical suffix. + */ + if (domain[0] == 0) + return (SMTPD_CHECK_DUNNO); + suffix = strrchr(domain, '.'); + if (alldig(suffix == 0 ? domain : suffix + 1)) + return (SMTPD_CHECK_DUNNO); + + /* + * Fix 20140706: convert domain to ASCII. + */ +#ifndef NO_EAI + if (!allascii(domain) && (adomain = midna_domain_to_ascii(domain)) != 0) { + if (msg_verbose) + msg_info("%s asciified to %s", domain, adomain); + domain = adomain; + } +#endif + if (domain[0] == 0 || valid_hostname(domain, DONT_GRIPE) == 0) + return (SMTPD_CHECK_DUNNO); + + query = vstring_alloc(100); + vstring_sprintf(query, "%s.%s", domain, rbl_domain); + reply_addr = split_at(STR(query), '='); + rbl = (SMTPD_RBL_STATE *) ctable_locate(smtpd_rbl_cache, STR(query)); + if (reply_addr != 0) + byte_codes = ctable_locate(smtpd_rbl_byte_cache, reply_addr); + + /* + * If the record exists, match the result address. + */ + if (SMTPD_DNSXL_STAT_OK(rbl) && reply_addr != 0 + && !rbl_match_addr(rbl, byte_codes)) + rbl = 0; + vstring_free(query); + return (rbl); +} + +/* reject_rbl_domain - reject if domain in real-time blackhole list */ + +static int reject_rbl_domain(SMTPD_STATE *state, const char *rbl_domain, + const char *what, const char *reply_class) +{ + const char *myname = "reject_rbl_domain"; + const SMTPD_RBL_STATE *rbl; + + if (msg_verbose) + msg_info("%s: %s %s", myname, rbl_domain, what); + + rbl = find_dnsxl_domain(state, rbl_domain, what); + if (!SMTPD_DNSXL_STAT_OK(rbl)) { + return (SMTPD_CHECK_DUNNO); + } else { + return (rbl_reject_reply(state, rbl, rbl_domain, what, reply_class)); + } +} + +/* permit_dnswl_domain - permit domain in DNSWL */ + +static int permit_dnswl_domain(SMTPD_STATE *state, const char *dnswl_domain, + const char *what, const char *reply_class) +{ + const char *myname = "permit_dnswl_domain"; + const SMTPD_RBL_STATE *dnswl_result; + + if (msg_verbose) + msg_info("%s: %s", myname, what); + + /* Safety: don't whitelist unauthorized recipients. */ + if (strcmp(state->where, SMTPD_CMD_RCPT) == 0 && state->recipient != 0 + && permit_auth_destination(state, state->recipient) != SMTPD_CHECK_OK) + return (SMTPD_CHECK_DUNNO); + + dnswl_result = find_dnsxl_domain(state, dnswl_domain, what); + if (SMTPD_DNXSL_STAT_HARD(dnswl_result)) { + return (SMTPD_CHECK_DUNNO); + } else if (SMTPD_DNSXL_STAT_SOFT(dnswl_result)) { + /* XXX: Make configurable as rhswl_tempfail_action. */ + DEFER_IF_REJECT3(state, MAIL_ERROR_POLICY, + 450, "4.7.1", + "<%s>: %s rejected: %s", + what, reply_class, + "Service unavailable"); + return (SMTPD_CHECK_DUNNO); + } else if (SMTPD_DNSXL_STAT_OK(dnswl_result)) { + return (SMTPD_CHECK_OK); + } else { + /* Future proofing, in case find_dnsxl_addr() result is changed. */ + msg_panic("%s: find_dnsxl_addr API failure", myname); + } +} + +/* reject_maps_rbl - reject if client address in real-time blackhole list */ + +static int reject_maps_rbl(SMTPD_STATE *state) +{ + const char *myname = "reject_maps_rbl"; + char *saved_domains = mystrdup(var_maps_rbl_domains); + char *bp = saved_domains; + char *rbl_domain; + int result = SMTPD_CHECK_DUNNO; + static int warned; + + if (msg_verbose) + msg_info("%s: %s", myname, state->addr); + + if (warned == 0) { + warned++; + msg_warn("support for restriction \"%s\" will be removed from %s; " + "use \"%s domain-name\" instead", + REJECT_MAPS_RBL, var_mail_name, REJECT_RBL_CLIENT); + } + while ((rbl_domain = mystrtok(&bp, CHARS_COMMA_SP)) != 0) { + result = reject_rbl_addr(state, rbl_domain, state->addr, + SMTPD_NAME_CLIENT); + if (result != SMTPD_CHECK_DUNNO) + break; + } + + /* + * Clean up. + */ + myfree(saved_domains); + + return (result); +} + +#ifdef USE_SASL_AUTH + +/* reject_auth_sender_login_mismatch - logged in client must own sender address */ + +static int reject_auth_sender_login_mismatch(SMTPD_STATE *state, const char *sender, int allow_unknown_sender) +{ + const RESOLVE_REPLY *reply; + const char *owners; + char *saved_owners; + char *cp; + char *name; + int found = 0; + +#define ALLOW_UNKNOWN_SENDER 1 +#define FORBID_UNKNOWN_SENDER 0 + + /* + * Reject if the client is logged in and does not own the sender address. + */ + if (smtpd_sender_login_maps && state->sasl_username) { + reply = smtpd_resolve_addr(state->recipient, sender); + if (reply->flags & RESOLVE_FLAG_FAIL) + reject_dict_retry(state, sender); + if ((owners = check_mail_addr_find(state, sender, smtpd_sender_login_maps, + STR(reply->recipient), (char **) 0)) != 0) { + cp = saved_owners = mystrdup(owners); + while ((name = mystrtok(&cp, CHARS_COMMA_SP)) != 0) { + if (strcasecmp_utf8(state->sasl_username, name) == 0) { + found = 1; + break; + } + } + myfree(saved_owners); + } else if (allow_unknown_sender) + return (SMTPD_CHECK_DUNNO); + if (!found) + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, 553, "5.7.1", + "<%s>: Sender address rejected: not owned by user %s", + sender, state->sasl_username)); + } + return (SMTPD_CHECK_DUNNO); +} + +/* reject_unauth_sender_login_mismatch - sender requires client is logged in */ + +static int reject_unauth_sender_login_mismatch(SMTPD_STATE *state, const char *sender) +{ + const RESOLVE_REPLY *reply; + + /* + * Reject if the client is not logged in and the sender address has an + * owner. + */ + if (smtpd_sender_login_maps && !state->sasl_username) { + reply = smtpd_resolve_addr(state->recipient, sender); + if (reply->flags & RESOLVE_FLAG_FAIL) + reject_dict_retry(state, sender); + if (check_mail_addr_find(state, sender, smtpd_sender_login_maps, + STR(reply->recipient), (char **) 0) != 0) + return (smtpd_check_reject(state, MAIL_ERROR_POLICY, 553, "5.7.1", + "<%s>: Sender address rejected: not logged in", sender)); + } + return (SMTPD_CHECK_DUNNO); +} + +#endif + +/* valid_utf8_action - validate UTF-8 policy server response */ + +static int valid_utf8_action(const char *server, const char *action) +{ + int retval; + + if ((retval = valid_utf8_string(action, strlen(action))) == 0) + msg_warn("malformed UTF-8 in policy server %s response: \"%s\"", + server, action); + return (retval); +} + +/* check_policy_service - check delegated policy service */ + +static int check_policy_service(SMTPD_STATE *state, const char *server, + const char *reply_name, const char *reply_class, + const char *def_acl) +{ + static VSTRING *action = 0; + SMTPD_POLICY_CLNT *policy_clnt; + +#ifdef USE_TLS + VSTRING *subject_buf; + VSTRING *issuer_buf; + const char *subject; + const char *issuer; + +#endif + int ret; + + /* + * Sanity check. + */ + if (!policy_clnt_table + || (policy_clnt = (SMTPD_POLICY_CLNT *) + htable_find(policy_clnt_table, server)) == 0) + msg_panic("check_policy_service: no client endpoint for server %s", + server); + + /* + * Initialize. + */ + if (action == 0) + action = vstring_alloc(10); + +#ifdef USE_TLS +#define ENCODE_CN(coded_CN, coded_CN_buf, CN) do { \ + if (!TLS_CERT_IS_TRUSTED(state->tls_context) || *(CN) == 0) { \ + coded_CN_buf = 0; \ + coded_CN = ""; \ + } else { \ + coded_CN_buf = vstring_alloc(strlen(CN) + 1); \ + xtext_quote(coded_CN_buf, CN, ""); \ + coded_CN = STR(coded_CN_buf); \ + } \ + } while (0); + + ENCODE_CN(subject, subject_buf, state->tls_context->peer_CN); + ENCODE_CN(issuer, issuer_buf, state->tls_context->issuer_CN); +#endif + + if (attr_clnt_request(policy_clnt->client, + ATTR_FLAG_NONE, /* Query attributes. */ + SEND_ATTR_STR(MAIL_ATTR_REQ, "smtpd_access_policy"), + SEND_ATTR_STR(MAIL_ATTR_PROTO_STATE, + STREQ(state->where, SMTPD_CMD_BDAT) ? + SMTPD_CMD_DATA : state->where), + SEND_ATTR_STR(MAIL_ATTR_ACT_PROTO_NAME, state->protocol), + SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_ADDR, state->addr), + SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_NAME, state->name), + SEND_ATTR_STR(MAIL_ATTR_ACT_CLIENT_PORT, state->port), + SEND_ATTR_STR(MAIL_ATTR_ACT_REVERSE_CLIENT_NAME, + state->reverse_name), + SEND_ATTR_STR(MAIL_ATTR_ACT_SERVER_ADDR, + state->dest_addr), + SEND_ATTR_STR(MAIL_ATTR_ACT_SERVER_PORT, + state->dest_port), + SEND_ATTR_STR(MAIL_ATTR_ACT_HELO_NAME, + state->helo_name ? state->helo_name : ""), + SEND_ATTR_STR(MAIL_ATTR_SENDER, + state->sender ? state->sender : ""), + SEND_ATTR_STR(MAIL_ATTR_RECIP, + state->recipient ? state->recipient : ""), + SEND_ATTR_INT(MAIL_ATTR_RCPT_COUNT, + ((strcasecmp(state->where, SMTPD_CMD_DATA) == 0) || + (strcasecmp(state->where, SMTPD_CMD_BDAT) == 0) || + (strcasecmp(state->where, SMTPD_AFTER_EOM) == 0)) ? + state->rcpt_count : 0), + SEND_ATTR_STR(MAIL_ATTR_QUEUEID, + state->queue_id ? state->queue_id : ""), + SEND_ATTR_STR(MAIL_ATTR_INSTANCE, + STR(state->instance)), + SEND_ATTR_LONG(MAIL_ATTR_SIZE, + (unsigned long) (state->act_size > 0 ? + state->act_size : state->msg_size)), + SEND_ATTR_STR(MAIL_ATTR_ETRN_DOMAIN, + state->etrn_name ? state->etrn_name : ""), + SEND_ATTR_STR(MAIL_ATTR_STRESS, var_stress), +#ifdef USE_SASL_AUTH + SEND_ATTR_STR(MAIL_ATTR_SASL_METHOD, + state->sasl_method ? state->sasl_method : ""), + SEND_ATTR_STR(MAIL_ATTR_SASL_USERNAME, + state->sasl_username ? state->sasl_username : ""), + SEND_ATTR_STR(MAIL_ATTR_SASL_SENDER, + state->sasl_sender ? state->sasl_sender : ""), +#endif +#ifdef USE_TLS +#define IF_ENCRYPTED(x, y) ((state->tls_context && ((x) != 0)) ? (x) : (y)) + SEND_ATTR_STR(MAIL_ATTR_CCERT_SUBJECT, subject), + SEND_ATTR_STR(MAIL_ATTR_CCERT_ISSUER, issuer), + + /* + * When directly checking the fingerprint, it is OK if the issuing CA is + * not trusted. + */ + SEND_ATTR_STR(MAIL_ATTR_CCERT_CERT_FPRINT, + IF_ENCRYPTED(state->tls_context->peer_cert_fprint, "")), + SEND_ATTR_STR(MAIL_ATTR_CCERT_PKEY_FPRINT, + IF_ENCRYPTED(state->tls_context->peer_pkey_fprint, "")), + SEND_ATTR_STR(MAIL_ATTR_CRYPTO_PROTOCOL, + IF_ENCRYPTED(state->tls_context->protocol, "")), + SEND_ATTR_STR(MAIL_ATTR_CRYPTO_CIPHER, + IF_ENCRYPTED(state->tls_context->cipher_name, "")), + SEND_ATTR_INT(MAIL_ATTR_CRYPTO_KEYSIZE, + IF_ENCRYPTED(state->tls_context->cipher_usebits, 0)), +#endif + SEND_ATTR_STR(MAIL_ATTR_POL_CONTEXT, + policy_clnt->policy_context), + ATTR_TYPE_END, + ATTR_FLAG_MISSING, /* Reply attributes. */ + RECV_ATTR_STR(MAIL_ATTR_ACTION, action), + ATTR_TYPE_END) != 1 + || (var_smtputf8_enable && valid_utf8_action(server, STR(action)) == 0)) { + NOCLOBBER static int nesting_level = 0; + jmp_buf savebuf; + int status; + + /* + * Safety to prevent recursive execution of the default action. + */ + nesting_level += 1; + memcpy(ADDROF(savebuf), ADDROF(smtpd_check_buf), sizeof(savebuf)); + status = setjmp(smtpd_check_buf); + if (status != 0) { + nesting_level -= 1; + memcpy(ADDROF(smtpd_check_buf), ADDROF(savebuf), + sizeof(smtpd_check_buf)); + longjmp(smtpd_check_buf, status); + } + ret = check_table_result(state, server, nesting_level == 1 ? + policy_clnt->def_action : + DEF_SMTPD_POLICY_DEF_ACTION, + "policy query", reply_name, + reply_class, def_acl); + nesting_level -= 1; + memcpy(ADDROF(smtpd_check_buf), ADDROF(savebuf), + sizeof(smtpd_check_buf)); + } else { + + /* + * XXX This produces bogus error messages when the reply is + * malformed. + */ + ret = check_table_result(state, server, STR(action), + "policy query", reply_name, + reply_class, def_acl); + } +#ifdef USE_TLS + if (subject_buf) + vstring_free(subject_buf); + if (issuer_buf) + vstring_free(issuer_buf); +#endif + return (ret); +} + +/* is_map_command - restriction has form: check_xxx_access type:name */ + +static int is_map_command(SMTPD_STATE *state, const char *name, + const char *command, char ***argp) +{ + + /* + * This is a three-valued function: (a) this is not a check_xxx_access + * command, (b) this is a malformed check_xxx_access command, (c) this is + * a well-formed check_xxx_access command. That's too clumsy for function + * result values, so we use regular returns for (a) and (c), and use long + * jumps for the error case (b). + */ + if (strcasecmp(name, command) != 0) { + return (0); + } else if (*(*argp + 1) == 0 || strchr(*(*argp += 1), ':') == 0) { + msg_warn("restriction %s: bad argument \"%s\": need maptype:mapname", + command, **argp); + reject_server_error(state); + } else { + return (1); + } +} + +/* forbid_whitelist - disallow whitelisting */ + +static void forbid_whitelist(SMTPD_STATE *state, const char *name, + int status, const char *target) +{ + if (state->discard == 0 && status == SMTPD_CHECK_OK) { + msg_warn("restriction %s returns OK for %s", name, target); + msg_warn("this is not allowed for security reasons"); + msg_warn("use DUNNO instead of OK if you want to make an exception"); + reject_server_error(state); + } +} + +/* generic_checks - generic restrictions */ + +static int generic_checks(SMTPD_STATE *state, ARGV *restrictions, + const char *reply_name, + const char *reply_class, + const char *def_acl) +{ + const char *myname = "generic_checks"; + char **cpp; + const char *name; + int status = 0; + ARGV *list; + int found; + int saved_recursion = state->recursion++; + + if (msg_verbose) + msg_info(">>> START %s RESTRICTIONS <<<", reply_class); + + for (cpp = restrictions->argv; (name = *cpp) != 0; cpp++) { + + if (state->discard != 0) + break; + + if (msg_verbose) + msg_info("%s: name=%s", myname, name); + + /* + * Pseudo restrictions. + */ + if (strcasecmp(name, WARN_IF_REJECT) == 0) { + if (state->warn_if_reject == 0) + state->warn_if_reject = state->recursion; + continue; + } + + /* + * Spoof the is_map_command() routine, so that we do not have to make + * special cases for the implicit short-hand access map notation. + */ +#define NO_DEF_ACL 0 + + if (strchr(name, ':') != 0) { + if (def_acl == NO_DEF_ACL) { + msg_warn("specify one of (%s, %s, %s, %s, %s, %s) before %s restriction \"%s\"", + CHECK_CLIENT_ACL, CHECK_REVERSE_CLIENT_ACL, CHECK_HELO_ACL, CHECK_SENDER_ACL, + CHECK_RECIP_ACL, CHECK_ETRN_ACL, reply_class, name); + reject_server_error(state); + } + name = def_acl; + cpp -= 1; + } + + /* + * Generic restrictions. + */ + if (strcasecmp(name, PERMIT_ALL) == 0) { + status = smtpd_acl_permit(state, name, reply_class, + reply_name, NO_PRINT_ARGS); + if (status == SMTPD_CHECK_OK && cpp[1] != 0) + msg_warn("restriction `%s' after `%s' is ignored", + cpp[1], PERMIT_ALL); + } else if (strcasecmp(name, DEFER_ALL) == 0) { + status = smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_defer_code, "4.3.2", + "<%s>: %s rejected: Try again later", + reply_name, reply_class); + if (cpp[1] != 0 && state->warn_if_reject == 0) + msg_warn("restriction `%s' after `%s' is ignored", + cpp[1], DEFER_ALL); + } else if (strcasecmp(name, REJECT_ALL) == 0) { + status = smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_reject_code, "5.7.1", + "<%s>: %s rejected: Access denied", + reply_name, reply_class); + if (cpp[1] != 0 && state->warn_if_reject == 0) + msg_warn("restriction `%s' after `%s' is ignored", + cpp[1], REJECT_ALL); + } else if (strcasecmp(name, REJECT_UNAUTH_PIPE) == 0) { + status = reject_unauth_pipelining(state, reply_name, reply_class); + } else if (strcasecmp(name, CHECK_POLICY_SERVICE) == 0) { + if (cpp[1] == 0 || strchr(cpp[1], ':') == 0) { + msg_warn("restriction %s must be followed by transport:server", + CHECK_POLICY_SERVICE); + reject_server_error(state); + } else + status = check_policy_service(state, *++cpp, reply_name, + reply_class, def_acl); + } else if (strcasecmp(name, DEFER_IF_PERMIT) == 0) { + status = DEFER_IF_PERMIT2(DEFER_IF_PERMIT_ACT, + state, MAIL_ERROR_POLICY, + 450, "4.7.0", + "<%s>: %s rejected: defer_if_permit requested", + reply_name, reply_class); + } else if (strcasecmp(name, DEFER_IF_REJECT) == 0) { + DEFER_IF_REJECT2(state, MAIL_ERROR_POLICY, + 450, "4.7.0", + "<%s>: %s rejected: defer_if_reject requested", + reply_name, reply_class); + } else if (strcasecmp(name, SLEEP) == 0) { + if (cpp[1] == 0 || alldig(cpp[1]) == 0) { + msg_warn("restriction %s must be followed by number", SLEEP); + reject_server_error(state); + } else + sleep(atoi(*++cpp)); + } else if (strcasecmp(name, REJECT_PLAINTEXT_SESSION) == 0) { + status = reject_plaintext_session(state); + } + + /* + * Client name/address restrictions. + */ + else if (strcasecmp(name, REJECT_UNKNOWN_CLIENT_HOSTNAME) == 0 + || strcasecmp(name, REJECT_UNKNOWN_CLIENT) == 0) { + status = reject_unknown_client(state); + } else if (strcasecmp(name, REJECT_UNKNOWN_REVERSE_HOSTNAME) == 0) { + status = reject_unknown_reverse_name(state); + } else if (strcasecmp(name, PERMIT_INET_INTERFACES) == 0) { + status = permit_inet_interfaces(state); + if (status == SMTPD_CHECK_OK) + status = smtpd_acl_permit(state, name, SMTPD_NAME_CLIENT, + state->namaddr, NO_PRINT_ARGS); + } else if (strcasecmp(name, PERMIT_MYNETWORKS) == 0) { + status = permit_mynetworks(state); + if (status == SMTPD_CHECK_OK) + status = smtpd_acl_permit(state, name, SMTPD_NAME_CLIENT, + state->namaddr, NO_PRINT_ARGS); + } else if (is_map_command(state, name, CHECK_CLIENT_ACL, &cpp)) { + status = check_namadr_access(state, *cpp, state->name, state->addr, + FULL, &found, state->namaddr, + SMTPD_NAME_CLIENT, def_acl); + } else if (is_map_command(state, name, CHECK_REVERSE_CLIENT_ACL, &cpp)) { + status = check_namadr_access(state, *cpp, state->reverse_name, state->addr, + FULL, &found, state->reverse_name, + SMTPD_NAME_REV_CLIENT, def_acl); + forbid_whitelist(state, name, status, state->reverse_name); + } else if (strcasecmp(name, REJECT_MAPS_RBL) == 0) { + status = reject_maps_rbl(state); + } else if (strcasecmp(name, REJECT_RBL_CLIENT) == 0 + || strcasecmp(name, REJECT_RBL) == 0) { + if (cpp[1] == 0) + msg_warn("restriction %s requires domain name argument", name); + else + status = reject_rbl_addr(state, *(cpp += 1), state->addr, + SMTPD_NAME_CLIENT); + } else if (strcasecmp(name, PERMIT_DNSWL_CLIENT) == 0) { + if (cpp[1] == 0) + msg_warn("restriction %s requires domain name argument", name); + else { + status = permit_dnswl_addr(state, *(cpp += 1), state->addr, + SMTPD_NAME_CLIENT); + if (status == SMTPD_CHECK_OK) + status = smtpd_acl_permit(state, name, SMTPD_NAME_CLIENT, + state->namaddr, NO_PRINT_ARGS); + } + } else if (strcasecmp(name, REJECT_RHSBL_CLIENT) == 0) { + if (cpp[1] == 0) + msg_warn("restriction %s requires domain name argument", + name); + else { + cpp += 1; + if (strcasecmp(state->name, "unknown") != 0) + status = reject_rbl_domain(state, *cpp, state->name, + SMTPD_NAME_CLIENT); + } + } else if (strcasecmp(name, PERMIT_RHSWL_CLIENT) == 0) { + if (cpp[1] == 0) + msg_warn("restriction %s requires domain name argument", + name); + else { + cpp += 1; + if (strcasecmp(state->name, "unknown") != 0) { + status = permit_dnswl_domain(state, *cpp, state->name, + SMTPD_NAME_CLIENT); + if (status == SMTPD_CHECK_OK) + status = smtpd_acl_permit(state, name, + SMTPD_NAME_CLIENT, state->namaddr, NO_PRINT_ARGS); + } + } + } else if (strcasecmp(name, REJECT_RHSBL_REVERSE_CLIENT) == 0) { + if (cpp[1] == 0) + msg_warn("restriction %s requires domain name argument", + name); + else { + cpp += 1; + if (strcasecmp(state->reverse_name, "unknown") != 0) + status = reject_rbl_domain(state, *cpp, state->reverse_name, + SMTPD_NAME_REV_CLIENT); + } + } else if (is_map_command(state, name, CHECK_CCERT_ACL, &cpp)) { + status = check_ccert_access(state, *cpp, def_acl); + } else if (is_map_command(state, name, CHECK_SASL_ACL, &cpp)) { +#ifdef USE_SASL_AUTH + if (var_smtpd_sasl_enable) { + if (state->sasl_username && state->sasl_username[0]) + status = check_sasl_access(state, *cpp, def_acl); + } else +#endif + msg_warn("restriction `%s' ignored: no SASL support", name); + } else if (is_map_command(state, name, CHECK_CLIENT_NS_ACL, &cpp)) { + if (strcasecmp(state->name, "unknown") != 0) { + status = check_server_access(state, *cpp, state->name, + T_NS, state->namaddr, + SMTPD_NAME_CLIENT, def_acl); + forbid_whitelist(state, name, status, state->name); + } + } else if (is_map_command(state, name, CHECK_CLIENT_MX_ACL, &cpp)) { + if (strcasecmp(state->name, "unknown") != 0) { + status = check_server_access(state, *cpp, state->name, + T_MX, state->namaddr, + SMTPD_NAME_CLIENT, def_acl); + forbid_whitelist(state, name, status, state->name); + } + } else if (is_map_command(state, name, CHECK_CLIENT_A_ACL, &cpp)) { + if (strcasecmp(state->name, "unknown") != 0) { + status = check_server_access(state, *cpp, state->name, + T_A, state->namaddr, + SMTPD_NAME_CLIENT, def_acl); + forbid_whitelist(state, name, status, state->name); + } + } else if (is_map_command(state, name, CHECK_REVERSE_CLIENT_NS_ACL, &cpp)) { + if (strcasecmp(state->reverse_name, "unknown") != 0) { + status = check_server_access(state, *cpp, state->reverse_name, + T_NS, state->reverse_name, + SMTPD_NAME_REV_CLIENT, def_acl); + forbid_whitelist(state, name, status, state->reverse_name); + } + } else if (is_map_command(state, name, CHECK_REVERSE_CLIENT_MX_ACL, &cpp)) { + if (strcasecmp(state->reverse_name, "unknown") != 0) { + status = check_server_access(state, *cpp, state->reverse_name, + T_MX, state->reverse_name, + SMTPD_NAME_REV_CLIENT, def_acl); + forbid_whitelist(state, name, status, state->reverse_name); + } + } else if (is_map_command(state, name, CHECK_REVERSE_CLIENT_A_ACL, &cpp)) { + if (strcasecmp(state->reverse_name, "unknown") != 0) { + status = check_server_access(state, *cpp, state->reverse_name, + T_A, state->reverse_name, + SMTPD_NAME_REV_CLIENT, def_acl); + forbid_whitelist(state, name, status, state->reverse_name); + } + } + + /* + * HELO/EHLO parameter restrictions. + */ + else if (is_map_command(state, name, CHECK_HELO_ACL, &cpp)) { + if (state->helo_name) + status = check_domain_access(state, *cpp, state->helo_name, + FULL, &found, state->helo_name, + SMTPD_NAME_HELO, def_acl); + } else if (strcasecmp(name, REJECT_INVALID_HELO_HOSTNAME) == 0 + || strcasecmp(name, REJECT_INVALID_HOSTNAME) == 0) { + if (state->helo_name) { + if (*state->helo_name != '[') + status = reject_invalid_hostname(state, state->helo_name, + state->helo_name, SMTPD_NAME_HELO); + else + status = reject_invalid_hostaddr(state, state->helo_name, + state->helo_name, SMTPD_NAME_HELO); + } + } else if (strcasecmp(name, REJECT_UNKNOWN_HELO_HOSTNAME) == 0 + || strcasecmp(name, REJECT_UNKNOWN_HOSTNAME) == 0) { + if (state->helo_name) { + if (*state->helo_name != '[') + status = reject_unknown_hostname(state, state->helo_name, + state->helo_name, SMTPD_NAME_HELO); + else + status = reject_invalid_hostaddr(state, state->helo_name, + state->helo_name, SMTPD_NAME_HELO); + } + } else if (strcasecmp(name, PERMIT_NAKED_IP_ADDR) == 0) { + msg_warn("restriction %s is deprecated. Use %s or %s instead", + PERMIT_NAKED_IP_ADDR, PERMIT_MYNETWORKS, PERMIT_SASL_AUTH); + if (state->helo_name) { + if (state->helo_name[strspn(state->helo_name, "0123456789.:")] == 0 + && (status = reject_invalid_hostaddr(state, state->helo_name, + state->helo_name, SMTPD_NAME_HELO)) == 0) + status = smtpd_acl_permit(state, name, SMTPD_NAME_HELO, + state->helo_name, NO_PRINT_ARGS); + } + } else if (is_map_command(state, name, CHECK_HELO_NS_ACL, &cpp)) { + if (state->helo_name) { + status = check_server_access(state, *cpp, state->helo_name, + T_NS, state->helo_name, + SMTPD_NAME_HELO, def_acl); + forbid_whitelist(state, name, status, state->helo_name); + } + } else if (is_map_command(state, name, CHECK_HELO_MX_ACL, &cpp)) { + if (state->helo_name) { + status = check_server_access(state, *cpp, state->helo_name, + T_MX, state->helo_name, + SMTPD_NAME_HELO, def_acl); + forbid_whitelist(state, name, status, state->helo_name); + } + } else if (is_map_command(state, name, CHECK_HELO_A_ACL, &cpp)) { + if (state->helo_name) { + status = check_server_access(state, *cpp, state->helo_name, + T_A, state->helo_name, + SMTPD_NAME_HELO, def_acl); + forbid_whitelist(state, name, status, state->helo_name); + } + } else if (strcasecmp(name, REJECT_NON_FQDN_HELO_HOSTNAME) == 0 + || strcasecmp(name, REJECT_NON_FQDN_HOSTNAME) == 0) { + if (state->helo_name) { + if (*state->helo_name != '[') + status = reject_non_fqdn_hostname(state, state->helo_name, + state->helo_name, SMTPD_NAME_HELO); + else + status = reject_invalid_hostaddr(state, state->helo_name, + state->helo_name, SMTPD_NAME_HELO); + } + } else if (strcasecmp(name, REJECT_RHSBL_HELO) == 0) { + if (cpp[1] == 0) + msg_warn("restriction %s requires domain name argument", + name); + else { + cpp += 1; + if (state->helo_name) + status = reject_rbl_domain(state, *cpp, state->helo_name, + SMTPD_NAME_HELO); + } + } + + /* + * Sender mail address restrictions. + */ + else if (is_map_command(state, name, CHECK_SENDER_ACL, &cpp)) { + if (state->sender && *state->sender) + status = check_mail_access(state, *cpp, state->sender, + &found, state->sender, + SMTPD_NAME_SENDER, def_acl); + if (state->sender && !*state->sender) + status = check_access(state, *cpp, var_smtpd_null_key, FULL, + &found, state->sender, + SMTPD_NAME_SENDER, def_acl); + } else if (strcasecmp(name, REJECT_UNKNOWN_ADDRESS) == 0) { + if (state->sender && *state->sender) + status = reject_unknown_address(state, state->sender, + state->sender, SMTPD_NAME_SENDER); + } else if (strcasecmp(name, REJECT_UNKNOWN_SENDDOM) == 0) { + if (state->sender && *state->sender) + status = reject_unknown_address(state, state->sender, + state->sender, SMTPD_NAME_SENDER); + } else if (strcasecmp(name, REJECT_UNVERIFIED_SENDER) == 0) { + if (state->sender && *state->sender) + status = reject_unverified_address(state, state->sender, + state->sender, SMTPD_NAME_SENDER, + var_unv_from_dcode, var_unv_from_rcode, + unv_from_tf_act, + var_unv_from_why); + } else if (strcasecmp(name, REJECT_NON_FQDN_SENDER) == 0) { + if (state->sender && *state->sender) + status = reject_non_fqdn_address(state, state->sender, + state->sender, SMTPD_NAME_SENDER); + } else if (strcasecmp(name, REJECT_AUTH_SENDER_LOGIN_MISMATCH) == 0) { +#ifdef USE_SASL_AUTH + if (var_smtpd_sasl_enable) { + if (state->sender && *state->sender) + status = reject_auth_sender_login_mismatch(state, + state->sender, FORBID_UNKNOWN_SENDER); + } else +#endif + msg_warn("restriction `%s' ignored: no SASL support", name); + } else if (strcasecmp(name, REJECT_KNOWN_SENDER_LOGIN_MISMATCH) == 0) { +#ifdef USE_SASL_AUTH + if (var_smtpd_sasl_enable) { + if (state->sender && *state->sender) { + if (state->sasl_username) + status = reject_auth_sender_login_mismatch(state, + state->sender, ALLOW_UNKNOWN_SENDER); + else + status = reject_unauth_sender_login_mismatch(state, state->sender); + } + } else +#endif + msg_warn("restriction `%s' ignored: no SASL support", name); + } else if (strcasecmp(name, REJECT_UNAUTH_SENDER_LOGIN_MISMATCH) == 0) { +#ifdef USE_SASL_AUTH + if (var_smtpd_sasl_enable) { + if (state->sender && *state->sender) + status = reject_unauth_sender_login_mismatch(state, state->sender); + } else +#endif + msg_warn("restriction `%s' ignored: no SASL support", name); + } else if (is_map_command(state, name, CHECK_SENDER_NS_ACL, &cpp)) { + if (state->sender && *state->sender) { + status = check_server_access(state, *cpp, state->sender, + T_NS, state->sender, + SMTPD_NAME_SENDER, def_acl); + forbid_whitelist(state, name, status, state->sender); + } + } else if (is_map_command(state, name, CHECK_SENDER_MX_ACL, &cpp)) { + if (state->sender && *state->sender) { + status = check_server_access(state, *cpp, state->sender, + T_MX, state->sender, + SMTPD_NAME_SENDER, def_acl); + forbid_whitelist(state, name, status, state->sender); + } + } else if (is_map_command(state, name, CHECK_SENDER_A_ACL, &cpp)) { + if (state->sender && *state->sender) { + status = check_server_access(state, *cpp, state->sender, + T_A, state->sender, + SMTPD_NAME_SENDER, def_acl); + forbid_whitelist(state, name, status, state->sender); + } + } else if (strcasecmp(name, REJECT_RHSBL_SENDER) == 0) { + if (cpp[1] == 0) + msg_warn("restriction %s requires domain name argument", name); + else { + cpp += 1; + if (state->sender && *state->sender) + status = reject_rbl_domain(state, *cpp, state->sender, + SMTPD_NAME_SENDER); + } + } else if (strcasecmp(name, REJECT_UNLISTED_SENDER) == 0) { + if (state->sender && *state->sender) + status = check_sender_rcpt_maps(state, state->sender); + } + + /* + * Recipient mail address restrictions. + */ + else if (is_map_command(state, name, CHECK_RECIP_ACL, &cpp)) { + if (state->recipient) + status = check_mail_access(state, *cpp, state->recipient, + &found, state->recipient, + SMTPD_NAME_RECIPIENT, def_acl); + } else if (strcasecmp(name, PERMIT_MX_BACKUP) == 0) { + if (state->recipient) { + status = permit_mx_backup(state, state->recipient, + state->recipient, SMTPD_NAME_RECIPIENT); + if (status == SMTPD_CHECK_OK) + status = smtpd_acl_permit(state, name, SMTPD_NAME_RECIPIENT, + state->recipient, NO_PRINT_ARGS); + } + } else if (strcasecmp(name, PERMIT_AUTH_DEST) == 0) { + if (state->recipient) { + status = permit_auth_destination(state, state->recipient); + if (status == SMTPD_CHECK_OK) + status = smtpd_acl_permit(state, name, SMTPD_NAME_RECIPIENT, + state->recipient, NO_PRINT_ARGS); + } + } else if (strcasecmp(name, REJECT_UNAUTH_DEST) == 0) { + if (state->recipient) + status = reject_unauth_destination(state, state->recipient, + var_relay_code, "5.7.1"); + } else if (strcasecmp(name, DEFER_UNAUTH_DEST) == 0) { + if (state->recipient) + status = reject_unauth_destination(state, state->recipient, + var_relay_code - 100, "4.7.1"); + } else if (strcasecmp(name, CHECK_RELAY_DOMAINS) == 0) { + if (state->recipient) + status = check_relay_domains(state, state->recipient, + state->recipient, SMTPD_NAME_RECIPIENT); + if (status == SMTPD_CHECK_OK) + status = smtpd_acl_permit(state, name, SMTPD_NAME_RECIPIENT, + state->recipient, NO_PRINT_ARGS); + if (cpp[1] != 0 && state->warn_if_reject == 0) + msg_warn("restriction `%s' after `%s' is ignored", + cpp[1], CHECK_RELAY_DOMAINS); + } else if (strcasecmp(name, PERMIT_SASL_AUTH) == 0) { +#ifdef USE_SASL_AUTH + if (smtpd_sasl_is_active(state)) { + status = permit_sasl_auth(state, + SMTPD_CHECK_OK, SMTPD_CHECK_DUNNO); + if (status == SMTPD_CHECK_OK) + status = smtpd_acl_permit(state, name, SMTPD_NAME_CLIENT, + state->namaddr, NO_PRINT_ARGS); + } +#endif + } else if (strcasecmp(name, PERMIT_TLS_ALL_CLIENTCERTS) == 0) { + status = permit_tls_clientcerts(state, 1); + if (status == SMTPD_CHECK_OK) + status = smtpd_acl_permit(state, name, SMTPD_NAME_CLIENT, + state->namaddr, NO_PRINT_ARGS); + } else if (strcasecmp(name, PERMIT_TLS_CLIENTCERTS) == 0) { + status = permit_tls_clientcerts(state, 0); + if (status == SMTPD_CHECK_OK) + status = smtpd_acl_permit(state, name, SMTPD_NAME_CLIENT, + state->namaddr, NO_PRINT_ARGS); + } else if (strcasecmp(name, REJECT_UNKNOWN_RCPTDOM) == 0) { + if (state->recipient) + status = reject_unknown_address(state, state->recipient, + state->recipient, SMTPD_NAME_RECIPIENT); + } else if (strcasecmp(name, REJECT_NON_FQDN_RCPT) == 0) { + if (state->recipient) + status = reject_non_fqdn_address(state, state->recipient, + state->recipient, SMTPD_NAME_RECIPIENT); + } else if (is_map_command(state, name, CHECK_RECIP_NS_ACL, &cpp)) { + if (state->recipient && *state->recipient) { + status = check_server_access(state, *cpp, state->recipient, + T_NS, state->recipient, + SMTPD_NAME_RECIPIENT, def_acl); + forbid_whitelist(state, name, status, state->recipient); + } + } else if (is_map_command(state, name, CHECK_RECIP_MX_ACL, &cpp)) { + if (state->recipient && *state->recipient) { + status = check_server_access(state, *cpp, state->recipient, + T_MX, state->recipient, + SMTPD_NAME_RECIPIENT, def_acl); + forbid_whitelist(state, name, status, state->recipient); + } + } else if (is_map_command(state, name, CHECK_RECIP_A_ACL, &cpp)) { + if (state->recipient && *state->recipient) { + status = check_server_access(state, *cpp, state->recipient, + T_A, state->recipient, + SMTPD_NAME_RECIPIENT, def_acl); + forbid_whitelist(state, name, status, state->recipient); + } + } else if (strcasecmp(name, REJECT_RHSBL_RECIPIENT) == 0) { + if (cpp[1] == 0) + msg_warn("restriction %s requires domain name argument", name); + else { + cpp += 1; + if (state->recipient) + status = reject_rbl_domain(state, *cpp, state->recipient, + SMTPD_NAME_RECIPIENT); + } + } else if (strcasecmp(name, CHECK_RCPT_MAPS) == 0 + || strcasecmp(name, REJECT_UNLISTED_RCPT) == 0) { + if (state->recipient && *state->recipient) + status = check_recipient_rcpt_maps(state, state->recipient); + } else if (strcasecmp(name, REJECT_MUL_RCPT_BOUNCE) == 0) { + if (state->sender && *state->sender == 0 && state->rcpt_count + > (strcmp(state->where, SMTPD_CMD_RCPT) != 0)) + status = smtpd_check_reject(state, MAIL_ERROR_POLICY, + var_mul_rcpt_code, "5.5.3", + "<%s>: %s rejected: Multi-recipient bounce", + reply_name, reply_class); + } else if (strcasecmp(name, REJECT_UNVERIFIED_RECIP) == 0) { + if (state->recipient && *state->recipient) + status = reject_unverified_address(state, state->recipient, + state->recipient, SMTPD_NAME_RECIPIENT, + var_unv_rcpt_dcode, var_unv_rcpt_rcode, + unv_rcpt_tf_act, + var_unv_rcpt_why); + } + + /* + * ETRN domain name restrictions. + */ + else if (is_map_command(state, name, CHECK_ETRN_ACL, &cpp)) { + if (state->etrn_name) + status = check_domain_access(state, *cpp, state->etrn_name, + FULL, &found, state->etrn_name, + SMTPD_NAME_ETRN, def_acl); + } + + /* + * User-defined restriction class. + */ + else if ((list = (ARGV *) htable_find(smtpd_rest_classes, name)) != 0) { + status = generic_checks(state, list, reply_name, + reply_class, def_acl); + } + + /* + * Error: undefined restriction name. + */ + else { + msg_warn("unknown smtpd restriction: \"%s\"", name); + reject_server_error(state); + } + if (msg_verbose) + msg_info("%s: name=%s status=%d", myname, name, status); + + if (status < 0) { + if (status == DICT_ERR_RETRY) + reject_dict_retry(state, reply_name); + else + reject_server_error(state); + } + if (state->warn_if_reject >= state->recursion) + state->warn_if_reject = 0; + + if (status != 0) + break; + + if (state->defer_if_permit.active && state->defer_if_reject.active) + break; + } + if (msg_verbose) + msg_info(">>> END %s RESTRICTIONS <<<", reply_class); + + state->recursion = saved_recursion; + + /* In case the list terminated with one or more warn_if_mumble. */ + if (state->warn_if_reject >= state->recursion) + state->warn_if_reject = 0; + + return (status); +} + +/* smtpd_check_addr - address sanity check */ + +int smtpd_check_addr(const char *sender, const char *addr, int smtputf8) +{ + const RESOLVE_REPLY *resolve_reply; + const char *myname = "smtpd_check_addr"; + const char *domain; + + if (msg_verbose) + msg_info("%s: addr=%s", myname, addr); + + /* + * Catch syntax errors early on if we can, but be prepared to re-compute + * the result later when the cache fills up with lots of recipients, at + * which time errors can still happen. + */ + if (addr == 0 || *addr == 0) + return (0); + resolve_reply = smtpd_resolve_addr(sender, addr); + if (resolve_reply->flags & RESOLVE_FLAG_ERROR) + return (-1); + + /* + * Backwards compatibility: if the client does not request SMTPUTF8 + * support, then behave like Postfix < 3.0 trivial-rewrite, and don't + * allow non-ASCII email domains. Historically, Postfix does not reject + * UTF8 etc. in the address localpart. + */ + if (smtputf8 == 0 + && (domain = strrchr(STR(resolve_reply->recipient), '@')) != 0 + && *(domain += 1) != 0 && !allascii(domain)) + return (-1); + + return (0); +} + +/* smtpd_check_rewrite - choose address qualification context */ + +char *smtpd_check_rewrite(SMTPD_STATE *state) +{ + const char *myname = "smtpd_check_rewrite"; + int status; + char **cpp; + MAPS *maps; + char *name; + + /* + * We don't use generic_checks() because it produces results that aren't + * applicable such as DEFER or REJECT. + */ + for (cpp = local_rewrite_clients->argv; *cpp != 0; cpp++) { + if (msg_verbose) + msg_info("%s: trying: %s", myname, *cpp); + status = SMTPD_CHECK_DUNNO; + if (strchr(name = *cpp, ':') != 0) { + name = CHECK_ADDR_MAP; + cpp -= 1; + } + if (strcasecmp(name, PERMIT_INET_INTERFACES) == 0) { + status = permit_inet_interfaces(state); + } else if (strcasecmp(name, PERMIT_MYNETWORKS) == 0) { + status = permit_mynetworks(state); + } else if (is_map_command(state, name, CHECK_ADDR_MAP, &cpp)) { + if ((maps = (MAPS *) htable_find(map_command_table, *cpp)) == 0) + msg_panic("%s: dictionary not found: %s", myname, *cpp); + if (maps_find(maps, state->addr, 0) != 0) + status = SMTPD_CHECK_OK; + else if (maps->error != 0) { + /* Warning is already logged. */ + status = maps->error; + } + } else if (strcasecmp(name, PERMIT_SASL_AUTH) == 0) { +#ifdef USE_SASL_AUTH + if (smtpd_sasl_is_active(state)) + status = permit_sasl_auth(state, SMTPD_CHECK_OK, + SMTPD_CHECK_DUNNO); +#endif + } else if (strcasecmp(name, PERMIT_TLS_ALL_CLIENTCERTS) == 0) { + status = permit_tls_clientcerts(state, 1); + } else if (strcasecmp(name, PERMIT_TLS_CLIENTCERTS) == 0) { + status = permit_tls_clientcerts(state, 0); + } else { + msg_warn("parameter %s: invalid request: %s", + VAR_LOC_RWR_CLIENTS, name); + continue; + } + if (status < 0) { + if (status == DICT_ERR_RETRY) { + state->error_mask |= MAIL_ERROR_RESOURCE; + log_whatsup(state, "reject", + "451 4.3.0 Temporary lookup error"); + return ("451 4.3.0 Temporary lookup error"); + } else { + state->error_mask |= MAIL_ERROR_SOFTWARE; + log_whatsup(state, "reject", + "451 4.3.5 Server configuration error"); + return ("451 4.3.5 Server configuration error"); + } + } + if (status == SMTPD_CHECK_OK) { + state->rewrite_context = MAIL_ATTR_RWR_LOCAL; + return (0); + } + } + state->rewrite_context = MAIL_ATTR_RWR_REMOTE; + return (0); +} + +/* smtpd_check_client - validate client name or address */ + +char *smtpd_check_client(SMTPD_STATE *state) +{ + int status; + + /* + * Initialize. + */ + if (state->name == 0 || state->addr == 0) + return (0); + +#define SMTPD_CHECK_RESET() { \ + state->recursion = 0; \ + state->warn_if_reject = 0; \ + state->defer_if_reject.active = 0; \ + } + + /* + * Reset the defer_if_permit flag. + */ + state->defer_if_permit.active = 0; + + /* + * Apply restrictions in the order as specified. + */ + SMTPD_CHECK_RESET(); + status = setjmp(smtpd_check_buf); + if (status == 0 && client_restrctions->argc) + status = generic_checks(state, client_restrctions, state->namaddr, + SMTPD_NAME_CLIENT, CHECK_CLIENT_ACL); + state->defer_if_permit_client = state->defer_if_permit.active; + + return (status == SMTPD_CHECK_REJECT ? STR(error_text) : 0); +} + +/* smtpd_check_helo - validate HELO hostname */ + +char *smtpd_check_helo(SMTPD_STATE *state, char *helohost) +{ + int status; + char *saved_helo; + + /* + * Initialize. + */ + if (helohost == 0) + return (0); + + /* + * Minor kluge so that we can delegate work to the generic routine and so + * that we can syslog the recipient with the reject messages. + */ +#define SMTPD_CHECK_PUSH(backup, current, new) { \ + backup = current; \ + current = (new ? mystrdup(new) : 0); \ + } + +#define SMTPD_CHECK_POP(current, backup) { \ + if (current) myfree(current); \ + current = backup; \ + } + + SMTPD_CHECK_PUSH(saved_helo, state->helo_name, helohost); + +#define SMTPD_CHECK_HELO_RETURN(x) { \ + SMTPD_CHECK_POP(state->helo_name, saved_helo); \ + return (x); \ + } + + /* + * Restore the defer_if_permit flag to its value before HELO/EHLO, and do + * not set the flag when it was already raised by a previous protocol + * stage. + */ + state->defer_if_permit.active = state->defer_if_permit_client; + + /* + * Apply restrictions in the order as specified. + */ + SMTPD_CHECK_RESET(); + status = setjmp(smtpd_check_buf); + if (status == 0 && helo_restrctions->argc) + status = generic_checks(state, helo_restrctions, state->helo_name, + SMTPD_NAME_HELO, CHECK_HELO_ACL); + state->defer_if_permit_helo = state->defer_if_permit.active; + + SMTPD_CHECK_HELO_RETURN(status == SMTPD_CHECK_REJECT ? STR(error_text) : 0); +} + +/* smtpd_check_mail - validate sender address, driver */ + +char *smtpd_check_mail(SMTPD_STATE *state, char *sender) +{ + int status; + char *saved_sender; + + /* + * Initialize. + */ + if (sender == 0) + return (0); + + /* + * Minor kluge so that we can delegate work to the generic routine and so + * that we can syslog the recipient with the reject messages. + */ + SMTPD_CHECK_PUSH(saved_sender, state->sender, sender); + +#define SMTPD_CHECK_MAIL_RETURN(x) { \ + SMTPD_CHECK_POP(state->sender, saved_sender); \ + return (x); \ + } + + /* + * Restore the defer_if_permit flag to its value before MAIL FROM, and do + * not set the flag when it was already raised by a previous protocol + * stage. The client may skip the helo/ehlo. + */ + state->defer_if_permit.active = state->defer_if_permit_client + | state->defer_if_permit_helo; + state->sender_rcptmap_checked = 0; + + /* + * Apply restrictions in the order as specified. + */ + SMTPD_CHECK_RESET(); + status = setjmp(smtpd_check_buf); + if (status == 0 && mail_restrctions->argc) + status = generic_checks(state, mail_restrctions, sender, + SMTPD_NAME_SENDER, CHECK_SENDER_ACL); + state->defer_if_permit_sender = state->defer_if_permit.active; + + /* + * If the "reject_unlisted_sender" restriction still needs to be applied, + * validate the sender here. + */ + if (var_smtpd_rej_unl_from + && status != SMTPD_CHECK_REJECT && state->sender_rcptmap_checked == 0 + && state->discard == 0 && *sender) + status = check_sender_rcpt_maps(state, sender); + + SMTPD_CHECK_MAIL_RETURN(status == SMTPD_CHECK_REJECT ? STR(error_text) : 0); +} + +/* smtpd_check_rcpt - validate recipient address, driver */ + +char *smtpd_check_rcpt(SMTPD_STATE *state, char *recipient) +{ + int status; + char *saved_recipient; + char *err; + ARGV *restrctions[2]; + int n; + + /* + * Initialize. + */ + if (recipient == 0) + return (0); + + /* + * XXX 2821: Section 3.6 requires that "postmaster" be accepted even when + * specified without a fully qualified domain name. + */ + if (strcasecmp(recipient, "postmaster") == 0) + return (0); + + /* + * Minor kluge so that we can delegate work to the generic routine and so + * that we can syslog the recipient with the reject messages. + */ + SMTPD_CHECK_PUSH(saved_recipient, state->recipient, recipient); + +#define SMTPD_CHECK_RCPT_RETURN(x) { \ + SMTPD_CHECK_POP(state->recipient, saved_recipient); \ + return (x); \ + } + + /* + * The "check_recipient_maps" restriction is relevant only when + * responding to RCPT TO or VRFY. + */ + state->recipient_rcptmap_checked = 0; + + /* + * Apply delayed restrictions. + */ + if (var_smtpd_delay_reject) + if ((err = smtpd_check_client(state)) != 0 + || (err = smtpd_check_helo(state, state->helo_name)) != 0 + || (err = smtpd_check_mail(state, state->sender)) != 0) + SMTPD_CHECK_RCPT_RETURN(err); + + /* + * Restore the defer_if_permit flag to its value before RCPT TO, and do + * not set the flag when it was already raised by a previous protocol + * stage. + */ + state->defer_if_permit.active = state->defer_if_permit_sender; + + /* + * Apply restrictions in the order as specified. We allow relay + * restrictions to be empty, for sites that require backwards + * compatibility. + * + * If compatibility_level < 1 and smtpd_relay_restrictions is left at its + * default value, find out if the new smtpd_relay_restrictions default + * value would block the request, without logging REJECT messages. + * Approach: evaluate fake relay restrictions (permit_mynetworks, + * permit_sasl_authenticated, permit_auth_destination) and log a warning + * if the result is DUNNO instead of OK, i.e. a reject_unauth_destinatin + * at the end would have blocked the request. + */ + SMTPD_CHECK_RESET(); + restrctions[0] = rcpt_restrctions; + restrctions[1] = warn_compat_break_relay_restrictions ? + fake_relay_restrctions : relay_restrctions; + for (n = 0; n < 2; n++) { + status = setjmp(smtpd_check_buf); + if (status == 0 && restrctions[n]->argc) + status = generic_checks(state, restrctions[n], + recipient, SMTPD_NAME_RECIPIENT, CHECK_RECIP_ACL); + if (n == 1 && warn_compat_break_relay_restrictions + && status == SMTPD_CHECK_DUNNO) { + msg_info("using backwards-compatible default setting \"" + VAR_RELAY_CHECKS " = (empty)\" to avoid \"Relay " + "access denied\" error for recipient \"%s\" from " + "client \"%s\"", state->recipient, state->namaddr); + } + if (status == SMTPD_CHECK_REJECT) + break; + } + + /* + * Force permission into deferral when some earlier temporary error may + * have prevented us from rejecting mail, and report the earlier problem. + */ + if (status != SMTPD_CHECK_REJECT && state->defer_if_permit.active) + status = smtpd_check_reject(state, state->defer_if_permit.class, + state->defer_if_permit.code, + STR(state->defer_if_permit.dsn), + "%s", STR(state->defer_if_permit.reason)); + + /* + * If the "reject_unlisted_recipient" restriction still needs to be + * applied, validate the recipient here. + */ + if (var_smtpd_rej_unl_rcpt + && status != SMTPD_CHECK_REJECT + && state->recipient_rcptmap_checked == 0 + && state->discard == 0) + status = check_recipient_rcpt_maps(state, recipient); + + SMTPD_CHECK_RCPT_RETURN(status == SMTPD_CHECK_REJECT ? STR(error_text) : 0); +} + +/* smtpd_check_etrn - validate ETRN request */ + +char *smtpd_check_etrn(SMTPD_STATE *state, char *domain) +{ + int status; + char *saved_etrn_name; + char *err; + + /* + * Initialize. + */ + if (domain == 0) + return (0); + + /* + * Minor kluge so that we can delegate work to the generic routine and so + * that we can syslog the recipient with the reject messages. + */ + SMTPD_CHECK_PUSH(saved_etrn_name, state->etrn_name, domain); + +#define SMTPD_CHECK_ETRN_RETURN(x) { \ + SMTPD_CHECK_POP(state->etrn_name, saved_etrn_name); \ + return (x); \ + } + + /* + * Apply delayed restrictions. + */ + if (var_smtpd_delay_reject) + if ((err = smtpd_check_client(state)) != 0 + || (err = smtpd_check_helo(state, state->helo_name)) != 0) + SMTPD_CHECK_ETRN_RETURN(err); + + /* + * Restore the defer_if_permit flag to its value before ETRN, and do not + * set the flag when it was already raised by a previous protocol stage. + * The client may skip the helo/ehlo. + */ + state->defer_if_permit.active = state->defer_if_permit_client + | state->defer_if_permit_helo; + + /* + * Apply restrictions in the order as specified. + */ + SMTPD_CHECK_RESET(); + status = setjmp(smtpd_check_buf); + if (status == 0 && etrn_restrctions->argc) + status = generic_checks(state, etrn_restrctions, domain, + SMTPD_NAME_ETRN, CHECK_ETRN_ACL); + + /* + * Force permission into deferral when some earlier temporary error may + * have prevented us from rejecting mail, and report the earlier problem. + */ + if (status != SMTPD_CHECK_REJECT && state->defer_if_permit.active) + status = smtpd_check_reject(state, state->defer_if_permit.class, + state->defer_if_permit.code, + STR(state->defer_if_permit.dsn), + "%s", STR(state->defer_if_permit.reason)); + + SMTPD_CHECK_ETRN_RETURN(status == SMTPD_CHECK_REJECT ? STR(error_text) : 0); +} + +/* check_recipient_rcpt_maps - generic_checks() recipient table check */ + +static int check_recipient_rcpt_maps(SMTPD_STATE *state, const char *recipient) +{ + + /* + * Duplicate suppression. There's an implicit check_recipient_maps + * restriction at the end of all recipient restrictions. + */ + if (smtpd_input_transp_mask & INPUT_TRANSP_UNKNOWN_RCPT) + return (0); + if (state->recipient_rcptmap_checked == 1) + return (0); + if (state->warn_if_reject == 0) + /* We really validate the recipient address. */ + state->recipient_rcptmap_checked = 1; + return (check_rcpt_maps(state, state->sender, recipient, + SMTPD_NAME_RECIPIENT)); +} + +/* check_sender_rcpt_maps - generic_checks() sender table check */ + +static int check_sender_rcpt_maps(SMTPD_STATE *state, const char *sender) +{ + + /* + * Duplicate suppression. There's an implicit check_sender_maps + * restriction at the end of all sender restrictions. + */ + if (smtpd_input_transp_mask & INPUT_TRANSP_UNKNOWN_RCPT) + return (0); + if (state->sender_rcptmap_checked == 1) + return (0); + if (state->warn_if_reject == 0) + /* We really validate the sender address. */ + state->sender_rcptmap_checked = 1; + return (check_rcpt_maps(state, state->recipient, sender, + SMTPD_NAME_SENDER)); +} + +/* check_rcpt_maps - generic_checks() interface for recipient table check */ + +static int check_rcpt_maps(SMTPD_STATE *state, const char *sender, + const char *recipient, + const char *reply_class) +{ + const RESOLVE_REPLY *reply; + DSN_SPLIT dp; + + if (msg_verbose) + msg_info(">>> CHECKING %s VALIDATION MAPS <<<", reply_class); + + /* + * Resolve the address. + */ + reply = smtpd_resolve_addr(sender, recipient); + if (reply->flags & RESOLVE_FLAG_FAIL) + reject_dict_retry(state, recipient); + + /* + * Make complex expressions more readable? + */ +#define MATCH(map, rcpt) \ + check_mail_addr_find(state, recipient, map, rcpt, (char **) 0) + +#define NOMATCH(map, rcpt) (MATCH(map, rcpt) == 0) + + /* + * XXX We assume the recipient address is OK if it matches a canonical + * map or virtual alias map. Eventually, the address resolver should give + * us the final resolved recipient address, and the SMTP server should + * write the final resolved recipient address to the output record + * stream. See also the next comment block on recipients in virtual alias + * domains. + */ + if (MATCH(rcpt_canon_maps, CONST_STR(reply->recipient)) + || (strcmp(reply_class, SMTPD_NAME_SENDER) == 0 + && MATCH(send_canon_maps, CONST_STR(reply->recipient))) + || MATCH(canonical_maps, CONST_STR(reply->recipient)) + || MATCH(virt_alias_maps, CONST_STR(reply->recipient))) + return (0); + + /* + * At this point, anything that resolves to the error mailer is known to + * be undeliverable. + * + * XXX Until the address resolver does final address resolution, known and + * unknown recipients in virtual alias domains will both resolve to + * "error:user unknown". + */ + if (strcmp(STR(reply->transport), MAIL_SERVICE_ERROR) == 0) { + dsn_split(&dp, strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + "5.1.0" : "5.1.1", STR(reply->nexthop)); + return (smtpd_check_reject(state, MAIL_ERROR_BOUNCE, + (reply->flags & RESOLVE_CLASS_ALIAS) ? + var_virt_alias_code : 550, + smtpd_dsn_fix(DSN_STATUS(dp.dsn), + reply_class), + "<%s>: %s rejected: %s", + recipient, reply_class, + dp.text)); + } + if (strcmp(STR(reply->transport), MAIL_SERVICE_RETRY) == 0) { + dsn_split(&dp, strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + "4.1.0" : "4.1.1", STR(reply->nexthop)); + return (smtpd_check_reject(state, MAIL_ERROR_BOUNCE, 450, + smtpd_dsn_fix(DSN_STATUS(dp.dsn), + reply_class), + "<%s>: %s rejected: %s", + recipient, reply_class, + dp.text)); + } + + /* + * Search the recipient lookup tables of the respective address class. + * + * XXX Use the less expensive maps_find() (built-in case folding) instead of + * the baroque mail_addr_find(). But then we have to strip the domain and + * deal with address extensions ourselves. + * + * XXX But that would break sites that use the virtual delivery agent for + * local delivery, because the virtual delivery agent requires + * user@domain style addresses in its user database. + */ +#define MATCH_LEFT(l, r, n) \ + (strncasecmp_utf8((l), (r), (n)) == 0 && (r)[n] == '@') + + switch (reply->flags & RESOLVE_CLASS_MASK) { + + /* + * Reject mail to unknown addresses in local domains (domains that + * match $mydestination or ${proxy,inet}_interfaces). + */ + case RESOLVE_CLASS_LOCAL: + if (*var_local_rcpt_maps + /* Generated by bounce, absorbed by qmgr. */ + && !MATCH_LEFT(var_double_bounce_sender, CONST_STR(reply->recipient), + strlen(var_double_bounce_sender)) + /* Absorbed by qmgr. */ + && !MATCH_LEFT(MAIL_ADDR_POSTMASTER, CONST_STR(reply->recipient), + strlen(MAIL_ADDR_POSTMASTER)) + /* Generated by bounce. */ + && !MATCH_LEFT(MAIL_ADDR_MAIL_DAEMON, CONST_STR(reply->recipient), + strlen(MAIL_ADDR_MAIL_DAEMON)) + && NOMATCH(local_rcpt_maps, CONST_STR(reply->recipient))) + return (smtpd_check_reject(state, MAIL_ERROR_BOUNCE, + var_local_rcpt_code, + strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + "5.1.0" : "5.1.1", + "<%s>: %s rejected: User unknown%s", + recipient, reply_class, + var_show_unk_rcpt_table ? + " in local recipient table" : "")); + break; + + /* + * Reject mail to unknown addresses in virtual mailbox domains. + */ + case RESOLVE_CLASS_VIRTUAL: + if (*var_virt_mailbox_maps + && NOMATCH(virt_mailbox_maps, CONST_STR(reply->recipient))) + return (smtpd_check_reject(state, MAIL_ERROR_BOUNCE, + var_virt_mailbox_code, + strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + "5.1.0" : "5.1.1", + "<%s>: %s rejected: User unknown%s", + recipient, reply_class, + var_show_unk_rcpt_table ? + " in virtual mailbox table" : "")); + break; + + /* + * Reject mail to unknown addresses in relay domains. + */ + case RESOLVE_CLASS_RELAY: + if (*var_relay_rcpt_maps + && NOMATCH(relay_rcpt_maps, CONST_STR(reply->recipient))) + return (smtpd_check_reject(state, MAIL_ERROR_BOUNCE, + var_relay_rcpt_code, + strcmp(reply_class, SMTPD_NAME_SENDER) == 0 ? + "5.1.0" : "5.1.1", + "<%s>: %s rejected: User unknown%s", + recipient, reply_class, + var_show_unk_rcpt_table ? + " in relay recipient table" : "")); + if (warn_compat_break_relay_domains) + msg_info("using backwards-compatible default setting " + VAR_RELAY_DOMAINS "=$mydestination to accept mail " + "for address \"%s\"", recipient); + break; + } + + /* + * Accept all other addresses - including addresses that passed the above + * tests because of some table lookup problem. + */ + return (0); +} + +/* smtpd_check_size - check optional SIZE parameter value */ + +char *smtpd_check_size(SMTPD_STATE *state, off_t size) +{ + int status; + + /* + * Return here in case of serious trouble. + */ + SMTPD_CHECK_RESET(); + if ((status = setjmp(smtpd_check_buf)) != 0) + return (status == SMTPD_CHECK_REJECT ? STR(error_text) : 0); + + /* + * Check against file size limit. + */ + if (var_message_limit > 0 && size > var_message_limit) { + (void) smtpd_check_reject(state, MAIL_ERROR_POLICY, + 552, "5.3.4", + "Message size exceeds fixed limit"); + return (STR(error_text)); + } + return (0); +} + +/* smtpd_check_queue - check queue space */ + +char *smtpd_check_queue(SMTPD_STATE *state) +{ + const char *myname = "smtpd_check_queue"; + struct fsspace fsbuf; + int status; + + /* + * Return here in case of serious trouble. + */ + SMTPD_CHECK_RESET(); + if ((status = setjmp(smtpd_check_buf)) != 0) + return (status == SMTPD_CHECK_REJECT ? STR(error_text) : 0); + + /* + * Avoid overflow/underflow when comparing message size against available + * space. + */ +#define BLOCKS(x) ((x) / fsbuf.block_size) + + fsspace(".", &fsbuf); + if (msg_verbose) + msg_info("%s: blocks %lu avail %lu min_free %lu msg_size_limit %lu", + myname, + (unsigned long) fsbuf.block_size, + (unsigned long) fsbuf.block_free, + (unsigned long) var_queue_minfree, + (unsigned long) var_message_limit); + if (BLOCKS(var_queue_minfree) >= fsbuf.block_free + || BLOCKS(var_message_limit) >= fsbuf.block_free / smtpd_space_multf) { + (void) smtpd_check_reject(state, MAIL_ERROR_RESOURCE, + 452, "4.3.1", + "Insufficient system storage"); + msg_warn("not enough free space in mail queue: %lu bytes < " + "%g*message size limit", + (unsigned long) fsbuf.block_free * fsbuf.block_size, + smtpd_space_multf); + return (STR(error_text)); + } + return (0); +} + +/* smtpd_check_data - check DATA command */ + +char *smtpd_check_data(SMTPD_STATE *state) +{ + int status; + char *NOCLOBBER saved_recipient; + + /* + * Minor kluge so that we can delegate work to the generic routine. We + * provide no recipient information in the case of multiple recipients, + * This restriction applies to all recipients alike, and logging only one + * of them would be misleading. + */ + if (state->rcpt_count > 1) { + saved_recipient = state->recipient; + state->recipient = 0; + } + + /* + * Reset the defer_if_permit flag. This is necessary when some recipients + * were accepted but the last one was rejected. + */ + state->defer_if_permit.active = 0; + + /* + * Apply restrictions in the order as specified. + * + * XXX We cannot specify a default target for a bare access map. + */ + SMTPD_CHECK_RESET(); + status = setjmp(smtpd_check_buf); + if (status == 0 && data_restrctions->argc) + status = generic_checks(state, data_restrctions, + SMTPD_CMD_DATA, SMTPD_NAME_DATA, NO_DEF_ACL); + + /* + * Force permission into deferral when some earlier temporary error may + * have prevented us from rejecting mail, and report the earlier problem. + */ + if (status != SMTPD_CHECK_REJECT && state->defer_if_permit.active) + status = smtpd_check_reject(state, state->defer_if_permit.class, + state->defer_if_permit.code, + STR(state->defer_if_permit.dsn), + "%s", STR(state->defer_if_permit.reason)); + + if (state->rcpt_count > 1) + state->recipient = saved_recipient; + + return (status == SMTPD_CHECK_REJECT ? STR(error_text) : 0); +} + +/* smtpd_check_eod - check end-of-data command */ + +char *smtpd_check_eod(SMTPD_STATE *state) +{ + int status; + char *NOCLOBBER saved_recipient; + + /* + * Minor kluge so that we can delegate work to the generic routine. We + * provide no recipient information in the case of multiple recipients, + * This restriction applies to all recipients alike, and logging only one + * of them would be misleading. + */ + if (state->rcpt_count > 1) { + saved_recipient = state->recipient; + state->recipient = 0; + } + + /* + * Reset the defer_if_permit flag. This is necessary when some recipients + * were accepted but the last one was rejected. + */ + state->defer_if_permit.active = 0; + + /* + * Apply restrictions in the order as specified. + * + * XXX We cannot specify a default target for a bare access map. + */ + SMTPD_CHECK_RESET(); + status = setjmp(smtpd_check_buf); + if (status == 0 && eod_restrictions->argc) + status = generic_checks(state, eod_restrictions, + SMTPD_CMD_EOD, SMTPD_NAME_EOD, NO_DEF_ACL); + + /* + * Force permission into deferral when some earlier temporary error may + * have prevented us from rejecting mail, and report the earlier problem. + */ + if (status != SMTPD_CHECK_REJECT && state->defer_if_permit.active) + status = smtpd_check_reject(state, state->defer_if_permit.class, + state->defer_if_permit.code, + STR(state->defer_if_permit.dsn), + "%s", STR(state->defer_if_permit.reason)); + + if (state->rcpt_count > 1) + state->recipient = saved_recipient; + + return (status == SMTPD_CHECK_REJECT ? STR(error_text) : 0); +} + +#ifdef TEST + + /* + * Test program to try out all these restrictions without having to go live. + * This is not entirely stand-alone, as it requires access to the Postfix + * rewrite/resolve service. This is just for testing code, not for debugging + * configuration files. + */ +#include <stdlib.h> + +#include <msg_vstream.h> +#include <vstring_vstream.h> + +#include <mail_conf.h> +#include <rewrite_clnt.h> +#include <dns.h> + +#include <smtpd_chat.h> + +int smtpd_input_transp_mask; + + /* + * Dummies. These are never set. + */ +char *var_client_checks = ""; +char *var_helo_checks = ""; +char *var_mail_checks = ""; +char *var_relay_checks = ""; +char *var_rcpt_checks = ""; +char *var_etrn_checks = ""; +char *var_data_checks = ""; +char *var_eod_checks = ""; +char *var_relay_domains = ""; +char *var_smtpd_uproxy_proto = ""; +int var_smtpd_uproxy_tmout = 0; + +#ifdef USE_TLS +char *var_relay_ccerts = ""; + +#endif +char *var_mynetworks = ""; +char *var_notify_classes = ""; +char *var_smtpd_policy_def_action = ""; +char *var_smtpd_policy_context = ""; + + /* + * String-valued configuration parameters. + */ +char *var_maps_rbl_domains; +char *var_myorigin; +char *var_mydest; +char *var_inet_interfaces; +char *var_proxy_interfaces; +char *var_rcpt_delim; +char *var_rest_classes; +char *var_alias_maps; +char *var_send_canon_maps; +char *var_rcpt_canon_maps; +char *var_canonical_maps; +char *var_virt_alias_maps; +char *var_virt_alias_doms; +char *var_virt_mailbox_maps; +char *var_virt_mailbox_doms; +char *var_local_rcpt_maps; +char *var_perm_mx_networks; +char *var_par_dom_match; +char *var_smtpd_null_key; +char *var_smtpd_snd_auth_maps; +char *var_double_bounce_sender; +char *var_rbl_reply_maps; +char *var_smtpd_exp_filter; +char *var_def_rbl_reply; +char *var_relay_rcpt_maps; +char *var_verify_sender; +char *var_smtpd_sasl_opts; +char *var_local_rwr_clients; +char *var_smtpd_relay_ccerts; +char *var_unv_from_why; +char *var_unv_rcpt_why; +char *var_stress; +char *var_unk_name_tf_act; +char *var_unk_addr_tf_act; +char *var_unv_rcpt_tf_act; +char *var_unv_from_tf_act; +char *var_smtpd_acl_perm_log; + +typedef struct { + char *name; + char *defval; + char **target; +} STRING_TABLE; + +#undef DEF_VIRT_ALIAS_MAPS +#define DEF_VIRT_ALIAS_MAPS "" + +#undef DEF_LOCAL_RCPT_MAPS +#define DEF_LOCAL_RCPT_MAPS "" + +static const STRING_TABLE string_table[] = { + VAR_MAPS_RBL_DOMAINS, DEF_MAPS_RBL_DOMAINS, &var_maps_rbl_domains, + VAR_MYORIGIN, DEF_MYORIGIN, &var_myorigin, + VAR_MYDEST, DEF_MYDEST, &var_mydest, + VAR_INET_INTERFACES, DEF_INET_INTERFACES, &var_inet_interfaces, + VAR_PROXY_INTERFACES, DEF_PROXY_INTERFACES, &var_proxy_interfaces, + VAR_RCPT_DELIM, DEF_RCPT_DELIM, &var_rcpt_delim, + VAR_REST_CLASSES, DEF_REST_CLASSES, &var_rest_classes, + VAR_ALIAS_MAPS, DEF_ALIAS_MAPS, &var_alias_maps, + VAR_SEND_CANON_MAPS, DEF_SEND_CANON_MAPS, &var_send_canon_maps, + VAR_RCPT_CANON_MAPS, DEF_RCPT_CANON_MAPS, &var_rcpt_canon_maps, + VAR_CANONICAL_MAPS, DEF_CANONICAL_MAPS, &var_canonical_maps, + VAR_VIRT_ALIAS_MAPS, DEF_VIRT_ALIAS_MAPS, &var_virt_alias_maps, + VAR_VIRT_ALIAS_DOMS, DEF_VIRT_ALIAS_DOMS, &var_virt_alias_doms, + VAR_VIRT_MAILBOX_MAPS, DEF_VIRT_MAILBOX_MAPS, &var_virt_mailbox_maps, + VAR_VIRT_MAILBOX_DOMS, DEF_VIRT_MAILBOX_DOMS, &var_virt_mailbox_doms, + VAR_LOCAL_RCPT_MAPS, DEF_LOCAL_RCPT_MAPS, &var_local_rcpt_maps, + VAR_PERM_MX_NETWORKS, DEF_PERM_MX_NETWORKS, &var_perm_mx_networks, + VAR_PAR_DOM_MATCH, DEF_PAR_DOM_MATCH, &var_par_dom_match, + VAR_SMTPD_SND_AUTH_MAPS, DEF_SMTPD_SND_AUTH_MAPS, &var_smtpd_snd_auth_maps, + VAR_SMTPD_NULL_KEY, DEF_SMTPD_NULL_KEY, &var_smtpd_null_key, + VAR_DOUBLE_BOUNCE, DEF_DOUBLE_BOUNCE, &var_double_bounce_sender, + VAR_RBL_REPLY_MAPS, DEF_RBL_REPLY_MAPS, &var_rbl_reply_maps, + VAR_SMTPD_EXP_FILTER, DEF_SMTPD_EXP_FILTER, &var_smtpd_exp_filter, + VAR_DEF_RBL_REPLY, DEF_DEF_RBL_REPLY, &var_def_rbl_reply, + VAR_RELAY_RCPT_MAPS, DEF_RELAY_RCPT_MAPS, &var_relay_rcpt_maps, + VAR_VERIFY_SENDER, DEF_VERIFY_SENDER, &var_verify_sender, + VAR_MAIL_NAME, DEF_MAIL_NAME, &var_mail_name, + VAR_SMTPD_SASL_OPTS, DEF_SMTPD_SASL_OPTS, &var_smtpd_sasl_opts, + VAR_LOC_RWR_CLIENTS, DEF_LOC_RWR_CLIENTS, &var_local_rwr_clients, + VAR_RELAY_CCERTS, DEF_RELAY_CCERTS, &var_smtpd_relay_ccerts, + VAR_UNV_FROM_WHY, DEF_UNV_FROM_WHY, &var_unv_from_why, + VAR_UNV_RCPT_WHY, DEF_UNV_RCPT_WHY, &var_unv_rcpt_why, + VAR_STRESS, DEF_STRESS, &var_stress, + /* XXX Can't use ``$name'' type default values below. */ + VAR_UNK_NAME_TF_ACT, DEF_REJECT_TMPF_ACT, &var_unk_name_tf_act, + VAR_UNK_ADDR_TF_ACT, DEF_REJECT_TMPF_ACT, &var_unk_addr_tf_act, + VAR_UNV_RCPT_TF_ACT, DEF_REJECT_TMPF_ACT, &var_unv_rcpt_tf_act, + VAR_UNV_FROM_TF_ACT, DEF_REJECT_TMPF_ACT, &var_unv_from_tf_act, + /* XXX Can't use ``$name'' type default values above. */ + VAR_SMTPD_ACL_PERM_LOG, DEF_SMTPD_ACL_PERM_LOG, &var_smtpd_acl_perm_log, + VAR_SMTPD_DNS_RE_FILTER, DEF_SMTPD_DNS_RE_FILTER, &var_smtpd_dns_re_filter, + 0, +}; + +/* string_init - initialize string parameters */ + +static void string_init(void) +{ + const STRING_TABLE *sp; + + for (sp = string_table; sp->name; sp++) + sp->target[0] = mystrdup(sp->defval); +} + +/* string_update - update string parameter */ + +static int string_update(char **argv) +{ + const STRING_TABLE *sp; + + for (sp = string_table; sp->name; sp++) { + if (strcasecmp(argv[0], sp->name) == 0) { + myfree(sp->target[0]); + sp->target[0] = mystrdup(argv[1]); + return (1); + } + } + return (0); +} + + /* + * Integer parameters. + */ +int var_queue_minfree; /* XXX use off_t */ +typedef struct { + char *name; + int defval; + int *target; +} INT_TABLE; + +int var_unk_client_code; +int var_bad_name_code; +int var_unk_name_code; +int var_unk_addr_code; +int var_relay_code; +int var_maps_rbl_code; +int var_map_reject_code; +int var_map_defer_code; +int var_reject_code; +int var_defer_code; +int var_non_fqdn_code; +int var_smtpd_delay_reject; +int var_allow_untrust_route; +int var_mul_rcpt_code; +int var_unv_from_rcode; +int var_unv_from_dcode; +int var_unv_rcpt_rcode; +int var_unv_rcpt_dcode; +int var_local_rcpt_code; +int var_relay_rcpt_code; +int var_virt_mailbox_code; +int var_virt_alias_code; +int var_show_unk_rcpt_table; +int var_verify_poll_count; +int var_verify_poll_delay; +int var_smtpd_policy_tmout; +int var_smtpd_policy_idle; +int var_smtpd_policy_ttl; +int var_smtpd_policy_req_limit; +int var_smtpd_policy_try_limit; +int var_smtpd_policy_try_delay; +int var_smtpd_rej_unl_from; +int var_smtpd_rej_unl_rcpt; +int var_plaintext_code; +bool var_smtpd_peername_lookup; +bool var_smtpd_client_port_log; +char *var_smtpd_dns_re_filter; + +#define int_table test_int_table + +static const INT_TABLE int_table[] = { + "msg_verbose", 0, &msg_verbose, + VAR_UNK_CLIENT_CODE, DEF_UNK_CLIENT_CODE, &var_unk_client_code, + VAR_BAD_NAME_CODE, DEF_BAD_NAME_CODE, &var_bad_name_code, + VAR_UNK_NAME_CODE, DEF_UNK_NAME_CODE, &var_unk_name_code, + VAR_UNK_ADDR_CODE, DEF_UNK_ADDR_CODE, &var_unk_addr_code, + VAR_RELAY_CODE, DEF_RELAY_CODE, &var_relay_code, + VAR_MAPS_RBL_CODE, DEF_MAPS_RBL_CODE, &var_maps_rbl_code, + VAR_MAP_REJECT_CODE, DEF_MAP_REJECT_CODE, &var_map_reject_code, + VAR_MAP_DEFER_CODE, DEF_MAP_DEFER_CODE, &var_map_defer_code, + VAR_REJECT_CODE, DEF_REJECT_CODE, &var_reject_code, + VAR_DEFER_CODE, DEF_DEFER_CODE, &var_defer_code, + VAR_NON_FQDN_CODE, DEF_NON_FQDN_CODE, &var_non_fqdn_code, + VAR_SMTPD_DELAY_REJECT, DEF_SMTPD_DELAY_REJECT, &var_smtpd_delay_reject, + VAR_ALLOW_UNTRUST_ROUTE, DEF_ALLOW_UNTRUST_ROUTE, &var_allow_untrust_route, + VAR_MUL_RCPT_CODE, DEF_MUL_RCPT_CODE, &var_mul_rcpt_code, + VAR_UNV_FROM_RCODE, DEF_UNV_FROM_RCODE, &var_unv_from_rcode, + VAR_UNV_FROM_DCODE, DEF_UNV_FROM_DCODE, &var_unv_from_dcode, + VAR_UNV_RCPT_RCODE, DEF_UNV_RCPT_RCODE, &var_unv_rcpt_rcode, + VAR_UNV_RCPT_DCODE, DEF_UNV_RCPT_DCODE, &var_unv_rcpt_dcode, + VAR_LOCAL_RCPT_CODE, DEF_LOCAL_RCPT_CODE, &var_local_rcpt_code, + VAR_RELAY_RCPT_CODE, DEF_RELAY_RCPT_CODE, &var_relay_rcpt_code, + VAR_VIRT_ALIAS_CODE, DEF_VIRT_ALIAS_CODE, &var_virt_alias_code, + VAR_VIRT_MAILBOX_CODE, DEF_VIRT_MAILBOX_CODE, &var_virt_mailbox_code, + VAR_SHOW_UNK_RCPT_TABLE, DEF_SHOW_UNK_RCPT_TABLE, &var_show_unk_rcpt_table, + VAR_VERIFY_POLL_COUNT, 3, &var_verify_poll_count, + VAR_SMTPD_REJ_UNL_FROM, DEF_SMTPD_REJ_UNL_FROM, &var_smtpd_rej_unl_from, + VAR_SMTPD_REJ_UNL_RCPT, DEF_SMTPD_REJ_UNL_RCPT, &var_smtpd_rej_unl_rcpt, + VAR_PLAINTEXT_CODE, DEF_PLAINTEXT_CODE, &var_plaintext_code, + VAR_SMTPD_PEERNAME_LOOKUP, DEF_SMTPD_PEERNAME_LOOKUP, &var_smtpd_peername_lookup, + VAR_SMTPD_CLIENT_PORT_LOG, DEF_SMTPD_CLIENT_PORT_LOG, &var_smtpd_client_port_log, + 0, +}; + +/* int_init - initialize int parameters */ + +static void int_init(void) +{ + const INT_TABLE *sp; + + for (sp = int_table; sp->name; sp++) + sp->target[0] = sp->defval; +} + +/* int_update - update int parameter */ + +static int int_update(char **argv) +{ + const INT_TABLE *ip; + + for (ip = int_table; ip->name; ip++) { + if (strcasecmp(argv[0], ip->name) == 0) { + if (!ISDIGIT(*argv[1])) + msg_fatal("bad number: %s %s", ip->name, argv[1]); + ip->target[0] = atoi(argv[1]); + return (1); + } + } + return (0); +} + + /* + * Restrictions. + */ +typedef struct { + char *name; + ARGV **target; +} REST_TABLE; + +static const REST_TABLE rest_table[] = { + "client_restrictions", &client_restrctions, + "helo_restrictions", &helo_restrctions, + "sender_restrictions", &mail_restrctions, + "relay_restrictions", &relay_restrctions, + "recipient_restrictions", &rcpt_restrctions, + "etrn_restrictions", &etrn_restrctions, + 0, +}; + +/* rest_update - update restriction */ + +static int rest_update(char **argv) +{ + const REST_TABLE *rp; + + for (rp = rest_table; rp->name; rp++) { + if (strcasecmp(rp->name, argv[0]) == 0) { + argv_free(rp->target[0]); + rp->target[0] = smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, argv[1]); + return (1); + } + } + return (0); +} + +/* rest_class - (re)define a restriction class */ + +static void rest_class(char *class) +{ + char *cp = class; + char *name; + HTABLE_INFO *entry; + + if (smtpd_rest_classes == 0) + smtpd_rest_classes = htable_create(1); + + if ((name = mystrtok(&cp, CHARS_COMMA_SP)) == 0) + msg_panic("rest_class: null class name"); + if ((entry = htable_locate(smtpd_rest_classes, name)) != 0) + argv_free((ARGV *) entry->value); + else + entry = htable_enter(smtpd_rest_classes, name, (void *) 0); + entry->value = (void *) smtpd_check_parse(SMTPD_CHECK_PARSE_ALL, cp); +} + +/* resolve_clnt_init - initialize reply */ + +void resolve_clnt_init(RESOLVE_REPLY *reply) +{ + reply->flags = 0; + reply->transport = vstring_alloc(100); + reply->nexthop = vstring_alloc(100); + reply->recipient = vstring_alloc(100); +} + +void resolve_clnt_free(RESOLVE_REPLY *reply) +{ + vstring_free(reply->transport); + vstring_free(reply->nexthop); + vstring_free(reply->recipient); +} + +bool var_smtpd_sasl_enable = 0; + +#ifdef USE_SASL_AUTH + +/* smtpd_sasl_activate - stub */ + +void smtpd_sasl_activate(SMTPD_STATE *state, const char *opts_name, + const char *opts_var) +{ + msg_panic("smtpd_sasl_activate was called"); +} + +/* smtpd_sasl_deactivate - stub */ + +void smtpd_sasl_deactivate(SMTPD_STATE *state) +{ + msg_panic("smtpd_sasl_deactivate was called"); +} + +/* permit_sasl_auth - stub */ + +int permit_sasl_auth(SMTPD_STATE *state, int ifyes, int ifnot) +{ + return (ifnot); +} + +/* smtpd_sasl_state_init - the real deal */ + +void smtpd_sasl_state_init(SMTPD_STATE *state) +{ + state->sasl_username = 0; + state->sasl_method = 0; + state->sasl_sender = 0; +} + +#endif + +/* verify_clnt_query - stub */ + +int verify_clnt_query(const char *addr, int *addr_status, VSTRING *why) +{ + *addr_status = DEL_RCPT_STAT_OK; + return (VRFY_STAT_OK); +} + +/* rewrite_clnt_internal - stub */ + +VSTRING *rewrite_clnt_internal(const char *context, const char *addr, + VSTRING *result) +{ + if (addr == STR(result)) + msg_panic("rewrite_clnt_internal: result clobbers input"); + if (*addr && strchr(addr, '@') == 0) + msg_fatal("%s: address rewriting is disabled", addr); + vstring_strcpy(result, addr); + return (result); +} + +/* resolve_clnt_query - stub */ + +void resolve_clnt(const char *class, const char *unused_sender, const char *addr, + RESOLVE_REPLY *reply) +{ + const char *domain; + int rc; + + if (addr == CONST_STR(reply->recipient)) + msg_panic("resolve_clnt_query: result clobbers input"); + if (strchr(addr, '%')) + msg_fatal("%s: address rewriting is disabled", addr); + if ((domain = strrchr(addr, '@')) == 0) + msg_fatal("%s: unqualified address", addr); + domain += 1; + if ((rc = resolve_local(domain)) > 0) { + reply->flags = RESOLVE_CLASS_LOCAL; + vstring_strcpy(reply->transport, MAIL_SERVICE_LOCAL); + vstring_strcpy(reply->nexthop, domain); + } else if (rc < 0) { + reply->flags = RESOLVE_FLAG_FAIL; + } else if (string_list_match(virt_alias_doms, domain)) { + reply->flags = RESOLVE_CLASS_ALIAS; + vstring_strcpy(reply->transport, MAIL_SERVICE_ERROR); + vstring_strcpy(reply->nexthop, "user unknown"); + } else if (virt_alias_doms->error) { + reply->flags = RESOLVE_FLAG_FAIL; + } else if (string_list_match(virt_mailbox_doms, domain)) { + reply->flags = RESOLVE_CLASS_VIRTUAL; + vstring_strcpy(reply->transport, MAIL_SERVICE_VIRTUAL); + vstring_strcpy(reply->nexthop, domain); + } else if (virt_mailbox_doms->error) { + reply->flags = RESOLVE_FLAG_FAIL; + } else if (domain_list_match(relay_domains, domain)) { + reply->flags = RESOLVE_CLASS_RELAY; + vstring_strcpy(reply->transport, MAIL_SERVICE_RELAY); + vstring_strcpy(reply->nexthop, domain); + } else if (relay_domains->error) { + reply->flags = RESOLVE_FLAG_FAIL; + } else { + reply->flags = RESOLVE_CLASS_DEFAULT; + vstring_strcpy(reply->transport, MAIL_SERVICE_SMTP); + vstring_strcpy(reply->nexthop, domain); + } + vstring_strcpy(reply->recipient, addr); +} + +/* smtpd_chat_reset - stub */ + +void smtpd_chat_reset(SMTPD_STATE *unused_state) +{ +} + +/* usage - scream and terminate */ + +static NORETURN usage(char *myname) +{ + msg_fatal("usage: %s", myname); +} + +int main(int argc, char **argv) +{ + VSTRING *buf = vstring_alloc(100); + SMTPD_STATE state; + ARGV *args; + char *bp; + char *resp; + char *addr; + + /* + * Initialization. Use dummies for client information. + */ + msg_vstream_init(argv[0], VSTREAM_ERR); + if (argc != 1) + usage(argv[0]); + string_init(); + int_init(); + smtpd_check_init(); + smtpd_expand_init(); + (void) inet_proto_init(argv[0], INET_PROTO_NAME_IPV4); + smtpd_state_init(&state, VSTREAM_IN, "smtpd"); + state.queue_id = "<queue id>"; + + /* + * Main loop: update config parameters or test the client, helo, sender + * and recipient restrictions. + */ + while (vstring_fgets_nonl(buf, VSTREAM_IN) != 0) { + + /* + * Tokenize the command. Note, the comma is not a separator, so that + * restriction lists can be entered as comma-separated lists. + */ + bp = STR(buf); + if (!isatty(0)) { + vstream_printf(">>> %s\n", bp); + vstream_fflush(VSTREAM_OUT); + } + if (*bp == '#') + continue; + + if (*bp == '!') { + vstream_printf("exit %d\n", system(bp + 1)); + continue; + } + args = argv_splitq(bp, CHARS_SPACE, CHARS_BRACE); + + /* + * Recognize the command. + */ + resp = "bad command"; + switch (args->argc) { + + /* + * Emtpy line. + */ + case 0: + argv_free(args); + continue; + + /* + * Special case: rewrite context. + */ + case 1: + if (strcasecmp(args->argv[0], "rewrite") == 0) { + resp = smtpd_check_rewrite(&state); + break; + } + + /* + * Other parameter-less commands. + */ + if (strcasecmp(args->argv[0], "flush_dnsxl_cache") == 0) { + if (smtpd_rbl_cache) { + ctable_free(smtpd_rbl_cache); + ctable_free(smtpd_rbl_byte_cache); + } + smtpd_rbl_cache = ctable_create(100, rbl_pagein, + rbl_pageout, (void *) 0); + smtpd_rbl_byte_cache = ctable_create(1000, rbl_byte_pagein, + rbl_byte_pageout, (void *) 0); + resp = 0; + break; + } + + /* + * Special case: client identity. + */ + case 4: + case 3: + if (strcasecmp(args->argv[0], "client") == 0) { + state.where = SMTPD_AFTER_CONNECT; + UPDATE_STRING(state.name, args->argv[1]); + UPDATE_STRING(state.reverse_name, args->argv[1]); + UPDATE_STRING(state.addr, args->argv[2]); + if (args->argc == 4) + state.name_status = + state.reverse_name_status = + atoi(args->argv[3]); + else if (strcmp(state.name, "unknown") == 0) + state.name_status = + state.reverse_name_status = + SMTPD_PEER_CODE_TEMP; + else + state.name_status = + state.reverse_name_status = + SMTPD_PEER_CODE_OK; + if (state.namaddr) + myfree(state.namaddr); + state.namaddr = concatenate(state.name, "[", state.addr, + "]", (char *) 0); + resp = smtpd_check_client(&state); + } + break; + + /* + * Try config settings. + */ +#define UPDATE_MAPS(ptr, var, val, lock) \ + { if (ptr) maps_free(ptr); ptr = maps_create(var, val, lock); } + +#define UPDATE_LIST(ptr, var, val) \ + { if (ptr) string_list_free(ptr); \ + ptr = string_list_init(var, MATCH_FLAG_NONE, val); } + + case 2: + if (strcasecmp(args->argv[0], VAR_MYDEST) == 0) { + UPDATE_STRING(var_mydest, args->argv[1]); + resolve_local_init(); + smtpd_resolve_init(100); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_VIRT_ALIAS_MAPS) == 0) { + UPDATE_STRING(var_virt_alias_maps, args->argv[1]); + UPDATE_MAPS(virt_alias_maps, VAR_VIRT_ALIAS_MAPS, + var_virt_alias_maps, DICT_FLAG_LOCK + | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_VIRT_ALIAS_DOMS) == 0) { + UPDATE_STRING(var_virt_alias_doms, args->argv[1]); + UPDATE_LIST(virt_alias_doms, VAR_VIRT_ALIAS_DOMS, + var_virt_alias_doms); + smtpd_resolve_init(100); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_VIRT_MAILBOX_MAPS) == 0) { + UPDATE_STRING(var_virt_mailbox_maps, args->argv[1]); + UPDATE_MAPS(virt_mailbox_maps, VAR_VIRT_MAILBOX_MAPS, + var_virt_mailbox_maps, DICT_FLAG_LOCK + | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_VIRT_MAILBOX_DOMS) == 0) { + UPDATE_STRING(var_virt_mailbox_doms, args->argv[1]); + UPDATE_LIST(virt_mailbox_doms, VAR_VIRT_MAILBOX_DOMS, + var_virt_mailbox_doms); + smtpd_resolve_init(100); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_LOCAL_RCPT_MAPS) == 0) { + UPDATE_STRING(var_local_rcpt_maps, args->argv[1]); + UPDATE_MAPS(local_rcpt_maps, VAR_LOCAL_RCPT_MAPS, + var_local_rcpt_maps, DICT_FLAG_LOCK + | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_RELAY_RCPT_MAPS) == 0) { + UPDATE_STRING(var_relay_rcpt_maps, args->argv[1]); + UPDATE_MAPS(relay_rcpt_maps, VAR_RELAY_RCPT_MAPS, + var_relay_rcpt_maps, DICT_FLAG_LOCK + | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_CANONICAL_MAPS) == 0) { + UPDATE_STRING(var_canonical_maps, args->argv[1]); + UPDATE_MAPS(canonical_maps, VAR_CANONICAL_MAPS, + var_canonical_maps, DICT_FLAG_LOCK + | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_SEND_CANON_MAPS) == 0) { + UPDATE_STRING(var_send_canon_maps, args->argv[1]); + UPDATE_MAPS(send_canon_maps, VAR_SEND_CANON_MAPS, + var_send_canon_maps, DICT_FLAG_LOCK + | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_RCPT_CANON_MAPS) == 0) { + UPDATE_STRING(var_rcpt_canon_maps, args->argv[1]); + UPDATE_MAPS(rcpt_canon_maps, VAR_RCPT_CANON_MAPS, + var_rcpt_canon_maps, DICT_FLAG_LOCK + | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_RBL_REPLY_MAPS) == 0) { + UPDATE_STRING(var_rbl_reply_maps, args->argv[1]); + UPDATE_MAPS(rbl_reply_maps, VAR_RBL_REPLY_MAPS, + var_rbl_reply_maps, DICT_FLAG_LOCK + | DICT_FLAG_FOLD_FIX | DICT_FLAG_UTF8_REQUEST); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_MYNETWORKS) == 0) { + /* NOT: UPDATE_STRING */ + namadr_list_free(mynetworks_curr); + mynetworks_curr = + namadr_list_init(VAR_MYNETWORKS, MATCH_FLAG_RETURN + | match_parent_style(VAR_MYNETWORKS), + args->argv[1]); + smtpd_resolve_init(100); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_RELAY_DOMAINS) == 0) { + /* NOT: UPDATE_STRING */ + domain_list_free(relay_domains); + relay_domains = + domain_list_init(VAR_RELAY_DOMAINS, + match_parent_style(VAR_RELAY_DOMAINS), + args->argv[1]); + smtpd_resolve_init(100); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_PERM_MX_NETWORKS) == 0) { + UPDATE_STRING(var_perm_mx_networks, args->argv[1]); + domain_list_free(perm_mx_networks); + perm_mx_networks = + namadr_list_init(VAR_PERM_MX_NETWORKS, MATCH_FLAG_RETURN + | match_parent_style(VAR_PERM_MX_NETWORKS), + args->argv[1]); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_SMTPD_DNS_RE_FILTER) == 0) { + /* NOT: UPDATE_STRING */ + dns_rr_filter_compile(VAR_SMTPD_DNS_RE_FILTER, args->argv[1]); + resp = 0; + break; + } +#ifdef USE_TLS + if (strcasecmp(args->argv[0], VAR_RELAY_CCERTS) == 0) { + UPDATE_STRING(var_smtpd_relay_ccerts, args->argv[1]); + UPDATE_MAPS(relay_ccerts, VAR_RELAY_CCERTS, + var_smtpd_relay_ccerts, DICT_FLAG_LOCK + | DICT_FLAG_FOLD_FIX); + resp = 0; + } +#endif + if (strcasecmp(args->argv[0], "restriction_class") == 0) { + rest_class(args->argv[1]); + resp = 0; + break; + } + if (strcasecmp(args->argv[0], VAR_LOC_RWR_CLIENTS) == 0) { + UPDATE_STRING(var_local_rwr_clients, args->argv[1]); + argv_free(local_rewrite_clients); + local_rewrite_clients = smtpd_check_parse(SMTPD_CHECK_PARSE_MAPS, + var_local_rwr_clients); + } + if (int_update(args->argv) + || string_update(args->argv) + || rest_update(args->argv)) { + resp = 0; + break; + } + + /* + * Try restrictions. + */ +#define TRIM_ADDR(src, res) { \ + if (*(res = src) == '<') { \ + res += strlen(res) - 1; \ + if (*res == '>') \ + *res = 0; \ + res = src + 1; \ + } \ + } + + if (strcasecmp(args->argv[0], "helo") == 0) { + state.where = "HELO"; + resp = smtpd_check_helo(&state, args->argv[1]); + UPDATE_STRING(state.helo_name, args->argv[1]); + } else if (strcasecmp(args->argv[0], "mail") == 0) { + state.where = "MAIL"; + TRIM_ADDR(args->argv[1], addr); + UPDATE_STRING(state.sender, addr); + resp = smtpd_check_mail(&state, addr); + } else if (strcasecmp(args->argv[0], "rcpt") == 0) { + state.where = "RCPT"; + TRIM_ADDR(args->argv[1], addr); + resp = smtpd_check_rcpt(&state, addr); +#ifdef USE_TLS + } else if (strcasecmp(args->argv[0], "fingerprint") == 0) { + if (state.tls_context == 0) { + state.tls_context = + (TLS_SESS_STATE *) mymalloc(sizeof(*state.tls_context)); + memset((void *) state.tls_context, 0, + sizeof(*state.tls_context)); + state.tls_context->peer_cert_fprint = + state.tls_context->peer_pkey_fprint = 0; + } + state.tls_context->peer_status |= TLS_CERT_FLAG_PRESENT; + UPDATE_STRING(state.tls_context->peer_cert_fprint, + args->argv[1]); + state.tls_context->peer_pkey_fprint = + state.tls_context->peer_cert_fprint; + resp = "OK"; + break; +#endif + } + break; + + /* + * Show commands. + */ + default: + if (strcasecmp(args->argv[0], "check_rewrite") == 0) { + smtpd_check_rewrite(&state); + resp = state.rewrite_context; + break; + } + resp = "Commands...\n\ + client <name> <address> [<code>]\n\ + helo <hostname>\n\ + sender <address>\n\ + recipient <address>\n\ + check_rewrite\n\ + msg_verbose <level>\n\ + client_restrictions <restrictions>\n\ + helo_restrictions <restrictions>\n\ + sender_restrictions <restrictions>\n\ + recipient_restrictions <restrictions>\n\ + restriction_class name,<restrictions>\n\ + flush_dnsxl_cache\n\ + \n\ + Note: no address rewriting \n"; + break; + } + vstream_printf("%s\n", resp ? resp : "OK"); + vstream_fflush(VSTREAM_OUT); + argv_free(args); + } + vstring_free(buf); + smtpd_state_reset(&state); +#define FREE_STRING(s) { if (s) myfree(s); } + FREE_STRING(state.helo_name); + FREE_STRING(state.sender); +#ifdef USE_TLS + if (state.tls_context) { + FREE_STRING(state.tls_context->peer_cert_fprint); + myfree((void *) state.tls_context); + } +#endif + exit(0); +} + +#endif |