X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/3ecab1575ef1f45a5e7cd3c48cd937ffa8eb0ad9..f69979cfecf29a4910b5750cad41d21a5418c6c7:/src/src/acl.c diff --git a/src/src/acl.c b/src/src/acl.c index 5b5390d8d..69db5cda7 100644 --- a/src/src/acl.c +++ b/src/src/acl.c @@ -2,7 +2,7 @@ * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) University of Cambridge 1995 - 2012 */ +/* Copyright (c) University of Cambridge 1995 - 2014 */ /* See the file NOTICE for conditions of use and distribution. */ /* Code for handling Access Control Lists (ACLs) */ @@ -21,8 +21,13 @@ enum { ACL_ACCEPT, ACL_DEFER, ACL_DENY, ACL_DISCARD, ACL_DROP, ACL_REQUIRE, /* ACL verbs */ -static uschar *verbs[] = - { US"accept", US"defer", US"deny", US"discard", US"drop", US"require", +static uschar *verbs[] = { + US"accept", + US"defer", + US"deny", + US"discard", + US"drop", + US"require", US"warn" }; /* For each verb, the conditions for which "message" or "log_message" are used @@ -66,6 +71,9 @@ enum { ACLC_ACL, #ifndef DISABLE_DKIM ACLC_DKIM_SIGNER, ACLC_DKIM_STATUS, +#endif +#ifdef EXPERIMENTAL_DMARC + ACLC_DMARC_STATUS, #endif ACLC_DNSLISTS, ACLC_DOMAINS, @@ -88,6 +96,7 @@ enum { ACLC_ACL, #ifdef WITH_CONTENT_SCAN ACLC_REGEX, #endif + ACLC_REMOVE_HEADER, ACLC_SENDER_DOMAINS, ACLC_SENDERS, ACLC_SET, @@ -98,6 +107,7 @@ enum { ACLC_ACL, ACLC_SPF, ACLC_SPF_GUESS, #endif + ACLC_UDPSEND, ACLC_VERIFY }; /* ACL conditions/modifiers: "delay", "control", "continue", "endpass", @@ -128,6 +138,9 @@ static uschar *conditions[] = { #ifndef DISABLE_DKIM US"dkim_signers", US"dkim_status", +#endif +#ifdef EXPERIMENTAL_DMARC + US"dmarc_status", #endif US"dnslists", US"domains", @@ -150,6 +163,7 @@ static uschar *conditions[] = { #ifdef WITH_CONTENT_SCAN US"regex", #endif + US"remove_header", US"sender_domains", US"senders", US"set", #ifdef WITH_CONTENT_SCAN US"spam", @@ -158,6 +172,7 @@ static uschar *conditions[] = { US"spf", US"spf_guess", #endif + US"udpsend", US"verify" }; @@ -173,9 +188,15 @@ enum { #ifndef DISABLE_DKIM CONTROL_DKIM_VERIFY, #endif + #ifdef EXPERIMENTAL_DMARC + CONTROL_DMARC_VERIFY, + CONTROL_DMARC_FORENSIC, + #endif + CONTROL_DSCP, CONTROL_ERROR, CONTROL_CASEFUL_LOCAL_PART, CONTROL_CASELOWER_LOCAL_PART, + CONTROL_CUTTHROUGH_DELIVERY, CONTROL_ENFORCE_SYNC, CONTROL_NO_ENFORCE_SYNC, CONTROL_FREEZE, @@ -207,9 +228,15 @@ static uschar *controls[] = { #ifndef DISABLE_DKIM US"dkim_disable_verify", #endif + #ifdef EXPERIMENTAL_DMARC + US"dmarc_disable_verify", + US"dmarc_enable_forensic", + #endif + US"dscp", US"error", US"caseful_local_part", US"caselower_local_part", + US"cutthrough_delivery", US"enforce_sync", US"no_enforce_sync", US"freeze", @@ -232,7 +259,7 @@ at the outer level. In the other cases, expansion already occurs in the checking functions. */ static uschar cond_expand_at_top[] = { - TRUE, /* acl */ + FALSE, /* acl */ TRUE, /* add_header */ FALSE, /* authenticated */ #ifdef EXPERIMENTAL_BRIGHTMAIL @@ -254,6 +281,9 @@ static uschar cond_expand_at_top[] = { #ifndef DISABLE_DKIM TRUE, /* dkim_signers */ TRUE, /* dkim_status */ +#endif +#ifdef EXPERIMENTAL_DMARC + TRUE, /* dmarc_status */ #endif TRUE, /* dnslists */ FALSE, /* domains */ @@ -276,6 +306,7 @@ static uschar cond_expand_at_top[] = { #ifdef WITH_CONTENT_SCAN TRUE, /* regex */ #endif + TRUE, /* remove_header */ FALSE, /* sender_domains */ FALSE, /* senders */ TRUE, /* set */ @@ -286,6 +317,7 @@ static uschar cond_expand_at_top[] = { TRUE, /* spf */ TRUE, /* spf_guess */ #endif + TRUE, /* udpsend */ TRUE /* verify */ }; @@ -314,6 +346,9 @@ static uschar cond_modifiers[] = { #ifndef DISABLE_DKIM FALSE, /* dkim_signers */ FALSE, /* dkim_status */ +#endif +#ifdef EXPERIMENTAL_DMARC + FALSE, /* dmarc_status */ #endif FALSE, /* dnslists */ FALSE, /* domains */ @@ -336,6 +371,7 @@ static uschar cond_modifiers[] = { #ifdef WITH_CONTENT_SCAN FALSE, /* regex */ #endif + TRUE, /* remove_header */ FALSE, /* sender_domains */ FALSE, /* senders */ TRUE, /* set */ @@ -346,6 +382,7 @@ static uschar cond_modifiers[] = { FALSE, /* spf */ FALSE, /* spf_guess */ #endif + TRUE, /* udpsend */ FALSE /* verify */ }; @@ -360,6 +397,9 @@ static unsigned int cond_forbids[] = { (unsigned int) ~((1<text, s, hlen) == 0) break; + if (Ustrncmp((*hptr)->text, hdr, hlen) == 0) break; hptr = &((*hptr)->next); } @@ -1008,7 +1140,7 @@ for (p = q = hstring; *p != 0; ) if (*hptr == NULL) { header_line *h = store_get(sizeof(header_line)); - h->text = s; + h->text = hdr; h->next = NULL; h->type = newtype; h->slen = hlen; @@ -1024,6 +1156,66 @@ for (p = q = hstring; *p != 0; ) +/************************************************* +* List the added header lines * +*************************************************/ +uschar * +fn_hdrs_added(void) +{ +uschar * ret = NULL; +header_line * h = acl_added_headers; +uschar * s; +uschar * cp; +int size = 0; +int ptr = 0; + +if (!h) return NULL; + +do + { + s = h->text; + while ((cp = Ustrchr(s, '\n')) != NULL) + { + if (cp[1] == '\0') break; + + /* contains embedded newline; needs doubling */ + ret = string_cat(ret, &size, &ptr, s, cp-s+1); + ret = string_cat(ret, &size, &ptr, US"\n", 1); + s = cp+1; + } + /* last bit of header */ + + ret = string_cat(ret, &size, &ptr, s, cp-s+1); /* newline-sep list */ + } +while((h = h->next)); + +ret[ptr-1] = '\0'; /* overwrite last newline */ +return ret; +} + + +/************************************************* +* Set up removed header line(s) * +*************************************************/ + +/* This function is called by the remove_header modifier. The argument is +treated as a sequence of header names which are added to a colon separated +list, provided there isn't an identical one already there. + +Argument: string of header names +Returns: nothing +*/ + +static void +setup_remove_header(const uschar *hnames) +{ +if (*hnames != 0) + acl_removed_headers = acl_removed_headers + ? string_sprintf("%s : %s", acl_removed_headers, hnames) + : string_copy(hnames); +} + + /************************************************* * Handle warnings * @@ -1259,10 +1451,11 @@ Returns: CSA_UNKNOWN no valid CSA record found */ static int -acl_verify_csa(uschar *domain) +acl_verify_csa(const uschar *domain) { tree_node *t; -uschar *found, *p; +const uschar *found; +uschar *p; int priority, weight, port; dns_answer dnsa; dns_scan dnss; @@ -1285,7 +1478,7 @@ containing a keyword and a colon before the actual IP address. */ if (domain[0] == '[') { - uschar *start = Ustrchr(domain, ':'); + const uschar *start = Ustrchr(domain, ':'); if (start == NULL) start = domain; domain = string_copyn(start + 1, Ustrlen(start) - 2); } @@ -1365,7 +1558,7 @@ for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS); assertion: legitimate SMTP clients are all explicitly authorized with CSA SRV records of their own. */ - if (found != domain) + if (Ustrcmp(found, domain) != 0) { if (port & 1) return t->data.val = CSA_FAIL_EXPLICIT; @@ -1429,6 +1622,7 @@ else DNS_LOOKUP_AGAIN: #endif +lookup_dnssec_authenticated = NULL; switch (dns_lookup(&dnsa, target, type, NULL)) { /* If something bad happened (most commonly DNS_AGAIN), defer. */ @@ -1465,7 +1659,8 @@ switch (dns_lookup(&dnsa, target, type, NULL)) *************************************************/ enum { VERIFY_REV_HOST_LKUP, VERIFY_CERT, VERIFY_HELO, VERIFY_CSA, VERIFY_HDR_SYNTAX, - VERIFY_NOT_BLIND, VERIFY_HDR_SNDR, VERIFY_SNDR, VERIFY_RCPT + VERIFY_NOT_BLIND, VERIFY_HDR_SNDR, VERIFY_SNDR, VERIFY_RCPT, + VERIFY_HDR_NAMES_ASCII }; typedef struct { uschar * name; @@ -1475,7 +1670,7 @@ typedef struct { unsigned alt_opt_sep; /* >0 Non-/ option separator (custom parser) */ } verify_type_t; static verify_type_t verify_type_list[] = { - { US"reverse_host_lookup", VERIFY_REV_HOST_LKUP, ~0, TRUE, 0 }, + { US"reverse_host_lookup", VERIFY_REV_HOST_LKUP, ~0, FALSE, 0 }, { US"certificate", VERIFY_CERT, ~0, TRUE, 0 }, { US"helo", VERIFY_HELO, ~0, TRUE, 0 }, { US"csa", VERIFY_CSA, ~0, FALSE, 0 }, @@ -1485,7 +1680,8 @@ static verify_type_t verify_type_list[] = { { US"sender", VERIFY_SNDR, (1<value) { case VERIFY_REV_HOST_LKUP: if (sender_host_address == NULL) return OK; - return acl_verify_reverse(user_msgptr, log_msgptr); + if ((rc = acl_verify_reverse(user_msgptr, log_msgptr)) == DEFER) + while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size))) + if (strcmpic(ss, US"defer_ok") == 0) + return OK; + return rc; case VERIFY_CERT: /* TLS certificate verification is done at STARTTLS time; here we just test whether it was successful or not. (This is for optional verification; for mandatory verification, the connection doesn't last this long.) */ - if (tls_certificate_verified) return OK; + if (tls_in.certificate_verified) return OK; *user_msgptr = US"no verified certificate"; return FAIL; @@ -1635,6 +1835,15 @@ switch(vp->value) *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr); return rc; + case VERIFY_HDR_NAMES_ASCII: + /* Check that all header names are true 7 bit strings + See RFC 5322, 2.2. and RFC 6532, 3. */ + + rc = verify_check_header_names_ascii(log_msgptr); + if (rc != OK && smtp_return_error_details && *log_msgptr != NULL) + *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr); + return rc; + case VERIFY_NOT_BLIND: /* Check that no recipient of this message is "blind", that is, every envelope recipient must be mentioned in either To: or Cc:. */ @@ -1714,12 +1923,13 @@ while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size)) while (isspace(*ss)) ss++; if (*ss++ == '=') { + const uschar * sublist = ss; int optsep = ','; uschar *opt; uschar buffer[256]; - while (isspace(*ss)) ss++; + while (isspace(*sublist)) sublist++; - while ((opt = string_nextinlist(&ss, &optsep, buffer, sizeof(buffer))) + while ((opt = string_nextinlist(&sublist, &optsep, buffer, sizeof(buffer))) != NULL) { callout_opt_t * op; @@ -2017,8 +2227,8 @@ return rc; BAD_VERIFY: *log_msgptr = string_sprintf("expected \"sender[=address]\", \"recipient\", " - "\"helo\", \"header_syntax\", \"header_sender\" or " - "\"reverse_host_lookup\" at start of ACL condition " + "\"helo\", \"header_syntax\", \"header_sender\", \"header_names_ascii\" " + "or \"reverse_host_lookup\" at start of ACL condition " "\"verify %s\"", arg); return ERROR; } @@ -2042,7 +2252,7 @@ Returns: CONTROL_xxx value */ static int -decode_control(uschar *arg, uschar **pptr, int where, uschar **log_msgptr) +decode_control(const uschar *arg, const uschar **pptr, int where, uschar **log_msgptr) { int len; control_def *d; @@ -2127,7 +2337,7 @@ Returns: OK - Sender's rate is above limit */ static int -acl_ratelimit(uschar *arg, int where, uschar **log_msgptr) +acl_ratelimit(const uschar *arg, int where, uschar **log_msgptr) { double limit, period, count; uschar *ss; @@ -2155,7 +2365,10 @@ rate measurement as opposed to rate limiting. */ sender_rate_limit = string_nextinlist(&arg, &sep, NULL, 0); if (sender_rate_limit == NULL) + { limit = -1.0; + ss = NULL; /* compiler quietening */ + } else { limit = Ustrtod(sender_rate_limit, &ss); @@ -2637,6 +2850,110 @@ return rc; +/************************************************* +* The udpsend ACL modifier * +*************************************************/ + +/* Called by acl_check_condition() below. + +Arguments: + arg the option string for udpsend= + log_msgptr for error messages + +Returns: OK - Completed. + DEFER - Problem with DNS lookup. + ERROR - Syntax error in options. +*/ + +static int +acl_udpsend(const uschar *arg, uschar **log_msgptr) +{ +int sep = 0; +uschar *hostname; +uschar *portstr; +uschar *portend; +host_item *h; +int portnum; +int len; +int r, s; +uschar * errstr; + +hostname = string_nextinlist(&arg, &sep, NULL, 0); +portstr = string_nextinlist(&arg, &sep, NULL, 0); + +if (hostname == NULL) + { + *log_msgptr = US"missing destination host in \"udpsend\" modifier"; + return ERROR; + } +if (portstr == NULL) + { + *log_msgptr = US"missing destination port in \"udpsend\" modifier"; + return ERROR; + } +if (arg == NULL) + { + *log_msgptr = US"missing datagram payload in \"udpsend\" modifier"; + return ERROR; + } +portnum = Ustrtol(portstr, &portend, 10); +if (*portend != '\0') + { + *log_msgptr = US"bad destination port in \"udpsend\" modifier"; + return ERROR; + } + +/* Make a single-item host list. */ +h = store_get(sizeof(host_item)); +memset(h, 0, sizeof(host_item)); +h->name = hostname; +h->port = portnum; +h->mx = MX_NONE; + +if (string_is_ip_address(hostname, NULL)) + h->address = hostname, r = HOST_FOUND; +else + r = host_find_byname(h, NULL, 0, NULL, FALSE); +if (r == HOST_FIND_FAILED || r == HOST_FIND_AGAIN) + { + *log_msgptr = US"DNS lookup failed in \"udpsend\" modifier"; + return DEFER; + } + +HDEBUG(D_acl) + debug_printf("udpsend [%s]:%d %s\n", h->address, portnum, arg); + +r = s = ip_connectedsocket(SOCK_DGRAM, h->address, portnum, portnum, + 1, NULL, &errstr); +if (r < 0) goto defer; +len = Ustrlen(arg); +r = send(s, arg, len, 0); +if (r < 0) + { + errstr = US strerror(errno); + close(s); + goto defer; + } +close(s); +if (r < len) + { + *log_msgptr = + string_sprintf("\"udpsend\" truncated from %d to %d octets", len, r); + return DEFER; + } + +HDEBUG(D_acl) + debug_printf("udpsend %d bytes\n", r); + +return OK; + +defer: +*log_msgptr = string_sprintf("\"udpsend\" failed: %s", errstr); +return DEFER; +} + + + /************************************************* * Handle conditions/modifiers on an ACL item * *************************************************/ @@ -2675,15 +2992,14 @@ uschar *user_message = NULL; uschar *log_message = NULL; uschar *debug_tag = NULL; uschar *debug_opts = NULL; -uschar *p = NULL; int rc = OK; #ifdef WITH_CONTENT_SCAN -int sep = '/'; +int sep = -'/'; #endif for (; cb != NULL; cb = cb->next) { - uschar *arg; + const uschar *arg; int control_type; /* The message and log_message items set up messages to be used in @@ -2691,12 +3007,14 @@ for (; cb != NULL; cb = cb->next) if (cb->type == ACLC_MESSAGE) { + HDEBUG(D_acl) debug_printf(" message: %s\n", cb->arg); user_message = cb->arg; continue; } if (cb->type == ACLC_LOG_MESSAGE) { + HDEBUG(D_acl) debug_printf("l_message: %s\n", cb->arg); log_message = cb->arg; continue; } @@ -2772,14 +3090,14 @@ for (; cb != NULL; cb = cb->next) "discard" verb. */ case ACLC_ACL: - rc = acl_check_internal(where, addr, arg, level+1, user_msgptr, log_msgptr); - if (rc == DISCARD && verb != ACL_ACCEPT && verb != ACL_DISCARD) - { - *log_msgptr = string_sprintf("nested ACL returned \"discard\" for " - "\"%s\" command (only allowed with \"accept\" or \"discard\")", - verbs[verb]); - return ERROR; - } + rc = acl_check_wargs(where, addr, arg, level+1, user_msgptr, log_msgptr); + if (rc == DISCARD && verb != ACL_ACCEPT && verb != ACL_DISCARD) + { + *log_msgptr = string_sprintf("nested ACL returned \"discard\" for " + "\"%s\" command (only allowed with \"accept\" or \"discard\")", + verbs[verb]); + return ERROR; + } break; case ACLC_AUTHENTICATED: @@ -2803,7 +3121,9 @@ for (; cb != NULL; cb = cb->next) /* The true/false parsing here should be kept in sync with that used in expand.c when dealing with ECOND_BOOL so that we don't have too many different definitions of what can be a boolean. */ - if (Ustrspn(arg, "0123456789") == Ustrlen(arg)) /* Digits, or empty */ + if (*arg == '-' + ? Ustrspn(arg+1, "0123456789") == Ustrlen(arg+1) /* Negative number */ + : Ustrspn(arg, "0123456789") == Ustrlen(arg)) /* Digits, or empty */ rc = (Uatoi(arg) == 0)? FAIL : OK; else rc = (strcmpic(arg, US"no") == 0 || @@ -2818,186 +3138,272 @@ for (; cb != NULL; cb = cb->next) break; case ACLC_CONTROL: - control_type = decode_control(arg, &p, where, log_msgptr); - - /* Check if this control makes sense at this time */ - - if ((control_forbids[control_type] & (1 << where)) != 0) { - *log_msgptr = string_sprintf("cannot use \"control=%s\" in %s ACL", - controls[control_type], acl_wherenames[where]); - return ERROR; - } - - switch(control_type) - { - case CONTROL_AUTH_UNADVERTISED: - allow_auth_unadvertised = TRUE; - break; - - #ifdef EXPERIMENTAL_BRIGHTMAIL - case CONTROL_BMI_RUN: - bmi_run = 1; - break; - #endif - - #ifndef DISABLE_DKIM - case CONTROL_DKIM_VERIFY: - dkim_disable_verify = TRUE; - break; - #endif - - case CONTROL_ERROR: - return ERROR; - - case CONTROL_CASEFUL_LOCAL_PART: - deliver_localpart = addr->cc_local_part; - break; - - case CONTROL_CASELOWER_LOCAL_PART: - deliver_localpart = addr->lc_local_part; - break; - - case CONTROL_ENFORCE_SYNC: - smtp_enforce_sync = TRUE; - break; - - case CONTROL_NO_ENFORCE_SYNC: - smtp_enforce_sync = FALSE; - break; - - #ifdef WITH_CONTENT_SCAN - case CONTROL_NO_MBOX_UNSPOOL: - no_mbox_unspool = TRUE; - break; - #endif - - case CONTROL_NO_MULTILINE: - no_multiline_responses = TRUE; - break; - - case CONTROL_NO_PIPELINING: - pipelining_enable = FALSE; - break; - - case CONTROL_NO_DELAY_FLUSH: - disable_delay_flush = TRUE; - break; - - case CONTROL_NO_CALLOUT_FLUSH: - disable_callout_flush = TRUE; - break; - - case CONTROL_FAKEDEFER: - case CONTROL_FAKEREJECT: - fake_response = (control_type == CONTROL_FAKEDEFER) ? DEFER : FAIL; - if (*p == '/') - { - uschar *pp = p + 1; - while (*pp != 0) pp++; - fake_response_text = expand_string(string_copyn(p+1, pp-p-1)); - p = pp; - } - else - { - /* Explicitly reset to default string */ - fake_response_text = US"Your message has been rejected but is being kept for evaluation.\nIf it was a legitimate message, it may still be delivered to the target recipient(s)."; - } - break; - - case CONTROL_FREEZE: - deliver_freeze = TRUE; - deliver_frozen_at = time(NULL); - freeze_tell = freeze_tell_config; /* Reset to configured value */ - if (Ustrncmp(p, "/no_tell", 8) == 0) - { - p += 8; - freeze_tell = NULL; - } - if (*p != 0) - { - *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg); - return ERROR; - } - break; - - case CONTROL_QUEUE_ONLY: - queue_only_policy = TRUE; - break; - - case CONTROL_SUBMISSION: - originator_name = US""; - submission_mode = TRUE; - while (*p == '/') - { - if (Ustrncmp(p, "/sender_retain", 14) == 0) - { - p += 14; - active_local_sender_retain = TRUE; - active_local_from_check = FALSE; - } - else if (Ustrncmp(p, "/domain=", 8) == 0) - { - uschar *pp = p + 8; - while (*pp != 0 && *pp != '/') pp++; - submission_domain = string_copyn(p+8, pp-p-8); - p = pp; - } - /* The name= option must be last, because it swallows the rest of - the string. */ - else if (Ustrncmp(p, "/name=", 6) == 0) - { - uschar *pp = p + 6; - while (*pp != 0) pp++; - submission_name = string_copy(parse_fix_phrase(p+6, pp-p-6, - big_buffer, big_buffer_size)); - p = pp; - } - else break; - } - if (*p != 0) - { - *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg); - return ERROR; - } - break; - - case CONTROL_DEBUG: - while (*p == '/') - { - if (Ustrncmp(p, "/tag=", 5) == 0) - { - uschar *pp = p + 5; - while (*pp != '\0' && *pp != '/') pp++; - debug_tag = string_copyn(p+5, pp-p-5); - p = pp; - } - else if (Ustrncmp(p, "/opts=", 6) == 0) - { - uschar *pp = p + 6; - while (*pp != '\0' && *pp != '/') pp++; - debug_opts = string_copyn(p+6, pp-p-6); - p = pp; - } - } - debug_logging_activate(debug_tag, debug_opts); - break; - - case CONTROL_SUPPRESS_LOCAL_FIXUPS: - suppress_local_fixups = TRUE; + const uschar *p = NULL; + control_type = decode_control(arg, &p, where, log_msgptr); + + /* Check if this control makes sense at this time */ + + if ((control_forbids[control_type] & (1 << where)) != 0) + { + *log_msgptr = string_sprintf("cannot use \"control=%s\" in %s ACL", + controls[control_type], acl_wherenames[where]); + return ERROR; + } + + switch(control_type) + { + case CONTROL_AUTH_UNADVERTISED: + allow_auth_unadvertised = TRUE; + break; + + #ifdef EXPERIMENTAL_BRIGHTMAIL + case CONTROL_BMI_RUN: + bmi_run = 1; + break; + #endif + + #ifndef DISABLE_DKIM + case CONTROL_DKIM_VERIFY: + dkim_disable_verify = TRUE; + #ifdef EXPERIMENTAL_DMARC + /* Since DKIM was blocked, skip DMARC too */ + dmarc_disable_verify = TRUE; + dmarc_enable_forensic = FALSE; + #endif + break; + #endif + + #ifdef EXPERIMENTAL_DMARC + case CONTROL_DMARC_VERIFY: + dmarc_disable_verify = TRUE; + break; + + case CONTROL_DMARC_FORENSIC: + dmarc_enable_forensic = TRUE; + break; + #endif + + case CONTROL_DSCP: + if (*p == '/') + { + int fd, af, level, optname, value; + /* If we are acting on stdin, the setsockopt may fail if stdin is not + a socket; we can accept that, we'll just debug-log failures anyway. */ + fd = fileno(smtp_in); + af = ip_get_address_family(fd); + if (af < 0) + { + HDEBUG(D_acl) + debug_printf("smtp input is probably not a socket [%s], not setting DSCP\n", + strerror(errno)); + break; + } + if (dscp_lookup(p+1, af, &level, &optname, &value)) + { + if (setsockopt(fd, level, optname, &value, sizeof(value)) < 0) + { + HDEBUG(D_acl) debug_printf("failed to set input DSCP[%s]: %s\n", + p+1, strerror(errno)); + } + else + { + HDEBUG(D_acl) debug_printf("set input DSCP to \"%s\"\n", p+1); + } + } + else + { + *log_msgptr = string_sprintf("unrecognised DSCP value in \"control=%s\"", arg); + return ERROR; + } + } + else + { + *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg); + return ERROR; + } + break; + + case CONTROL_ERROR: + return ERROR; + + case CONTROL_CASEFUL_LOCAL_PART: + deliver_localpart = addr->cc_local_part; + break; + + case CONTROL_CASELOWER_LOCAL_PART: + deliver_localpart = addr->lc_local_part; + break; + + case CONTROL_ENFORCE_SYNC: + smtp_enforce_sync = TRUE; + break; + + case CONTROL_NO_ENFORCE_SYNC: + smtp_enforce_sync = FALSE; + break; + + #ifdef WITH_CONTENT_SCAN + case CONTROL_NO_MBOX_UNSPOOL: + no_mbox_unspool = TRUE; + break; + #endif + + case CONTROL_NO_MULTILINE: + no_multiline_responses = TRUE; + break; + + case CONTROL_NO_PIPELINING: + pipelining_enable = FALSE; + break; + + case CONTROL_NO_DELAY_FLUSH: + disable_delay_flush = TRUE; + break; + + case CONTROL_NO_CALLOUT_FLUSH: + disable_callout_flush = TRUE; + break; + + case CONTROL_FAKEREJECT: + cancel_cutthrough_connection("fakereject"); + case CONTROL_FAKEDEFER: + fake_response = (control_type == CONTROL_FAKEDEFER) ? DEFER : FAIL; + if (*p == '/') + { + const uschar *pp = p + 1; + while (*pp != 0) pp++; + fake_response_text = expand_string(string_copyn(p+1, pp-p-1)); + p = pp; + } + else + { + /* Explicitly reset to default string */ + fake_response_text = US"Your message has been rejected but is being kept for evaluation.\nIf it was a legitimate message, it may still be delivered to the target recipient(s)."; + } + break; + + case CONTROL_FREEZE: + deliver_freeze = TRUE; + deliver_frozen_at = time(NULL); + freeze_tell = freeze_tell_config; /* Reset to configured value */ + if (Ustrncmp(p, "/no_tell", 8) == 0) + { + p += 8; + freeze_tell = NULL; + } + if (*p != 0) + { + *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg); + return ERROR; + } + cancel_cutthrough_connection("item frozen"); + break; + + case CONTROL_QUEUE_ONLY: + queue_only_policy = TRUE; + cancel_cutthrough_connection("queueing forced"); + break; + + case CONTROL_SUBMISSION: + originator_name = US""; + submission_mode = TRUE; + while (*p == '/') + { + if (Ustrncmp(p, "/sender_retain", 14) == 0) + { + p += 14; + active_local_sender_retain = TRUE; + active_local_from_check = FALSE; + } + else if (Ustrncmp(p, "/domain=", 8) == 0) + { + const uschar *pp = p + 8; + while (*pp != 0 && *pp != '/') pp++; + submission_domain = string_copyn(p+8, pp-p-8); + p = pp; + } + /* The name= option must be last, because it swallows the rest of + the string. */ + else if (Ustrncmp(p, "/name=", 6) == 0) + { + const uschar *pp = p + 6; + while (*pp != 0) pp++; + submission_name = string_copy(parse_fix_phrase(p+6, pp-p-6, + big_buffer, big_buffer_size)); + p = pp; + } + else break; + } + if (*p != 0) + { + *log_msgptr = string_sprintf("syntax error in \"control=%s\"", arg); + return ERROR; + } + break; + + case CONTROL_DEBUG: + while (*p == '/') + { + if (Ustrncmp(p, "/tag=", 5) == 0) + { + const uschar *pp = p + 5; + while (*pp != '\0' && *pp != '/') pp++; + debug_tag = string_copyn(p+5, pp-p-5); + p = pp; + } + else if (Ustrncmp(p, "/opts=", 6) == 0) + { + const uschar *pp = p + 6; + while (*pp != '\0' && *pp != '/') pp++; + debug_opts = string_copyn(p+6, pp-p-6); + p = pp; + } + } + debug_logging_activate(debug_tag, debug_opts); + break; + + case CONTROL_SUPPRESS_LOCAL_FIXUPS: + suppress_local_fixups = TRUE; + break; + + case CONTROL_CUTTHROUGH_DELIVERY: + if (prdr_requested) + /* Too hard to think about for now. We might in future cutthrough + the case where both sides handle prdr and this-node prdr acl + is "accept" */ + *log_msgptr = string_sprintf("PRDR on %s reception\n", arg); + else + { + if (deliver_freeze) + *log_msgptr = US"frozen"; + else if (queue_only_policy) + *log_msgptr = US"queue-only"; + else if (fake_response == FAIL) + *log_msgptr = US"fakereject"; + else + { + if (rcpt_count == 1) cutthrough.delivery = TRUE; + break; + } + *log_msgptr = string_sprintf("\"control=%s\" on %s item", + arg, *log_msgptr); + } + return ERROR; + } break; } - break; #ifdef EXPERIMENTAL_DCC case ACLC_DCC: { /* Seperate the regular expression and any optional parameters. */ - uschar *ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size); + const uschar * list = arg; + uschar *ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size); /* Run the dcc backend. */ rc = dcc_process(&ss); /* Modify return code based upon the existance of options. */ - while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size)) + while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size)) != NULL) { if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER) { @@ -3083,13 +3489,25 @@ for (; cb != NULL; cb = cb->next) break; #endif + #ifdef EXPERIMENTAL_DMARC + case ACLC_DMARC_STATUS: + if (!dmarc_has_been_checked) + dmarc_process(); + dmarc_has_been_checked = TRUE; + /* used long way of dmarc_exim_expand_query() in case we need more + * view into the process in the future. */ + rc = match_isinlist(dmarc_exim_expand_query(DMARC_VERIFY_STATUS), + &arg,0,NULL,NULL,MCL_STRING,TRUE,NULL); + break; + #endif + case ACLC_DNSLISTS: rc = verify_check_dnsbl(&arg); break; case ACLC_DOMAINS: rc = match_isinlist(addr->domain, &arg, 0, &domainlist_anchor, - addr->domain_cache, MCL_DOMAIN, TRUE, &deliver_domain_data); + addr->domain_cache, MCL_DOMAIN, TRUE, CUSS &deliver_domain_data); break; /* The value in tls_cipher is the full cipher name, for example, @@ -3099,11 +3517,11 @@ for (; cb != NULL; cb = cb->next) writing is poorly documented. */ case ACLC_ENCRYPTED: - if (tls_cipher == NULL) rc = FAIL; else + if (tls_in.cipher == NULL) rc = FAIL; else { uschar *endcipher = NULL; - uschar *cipher = Ustrchr(tls_cipher, ':'); - if (cipher == NULL) cipher = tls_cipher; else + uschar *cipher = Ustrchr(tls_in.cipher, ':'); + if (cipher == NULL) cipher = tls_in.cipher; else { endcipher = Ustrchr(++cipher, ':'); if (endcipher != NULL) *endcipher = 0; @@ -3122,21 +3540,22 @@ for (; cb != NULL; cb = cb->next) case ACLC_HOSTS: rc = verify_check_this_host(&arg, sender_host_cache, NULL, - (sender_host_address == NULL)? US"" : sender_host_address, &host_data); + (sender_host_address == NULL)? US"" : sender_host_address, + CUSS &host_data); if (host_data != NULL) host_data = string_copy_malloc(host_data); break; case ACLC_LOCAL_PARTS: rc = match_isinlist(addr->cc_local_part, &arg, 0, &localpartlist_anchor, addr->localpart_cache, MCL_LOCALPART, TRUE, - &deliver_localpart_data); + CUSS &deliver_localpart_data); break; case ACLC_LOG_REJECT_TARGET: { int logbits = 0; int sep = 0; - uschar *s = arg; + const uschar *s = arg; uschar *ss; while ((ss = string_nextinlist(&s, &sep, big_buffer, big_buffer_size)) != NULL) @@ -3158,7 +3577,7 @@ for (; cb != NULL; cb = cb->next) case ACLC_LOGWRITE: { int logbits = 0; - uschar *s = arg; + const uschar *s = arg; if (*s == ':') { s++; @@ -3189,21 +3608,29 @@ for (; cb != NULL; cb = cb->next) break; #ifdef WITH_CONTENT_SCAN - case ACLC_MALWARE: + case ACLC_MALWARE: /* Run the malware backend. */ { /* Separate the regular expression and any optional parameters. */ - uschar *ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size); - /* Run the malware backend. */ - rc = malware(&ss); - /* Modify return code based upon the existance of options. */ - while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size)) - != NULL) { - if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER) - { - /* FAIL so that the message is passed to the next ACL */ - rc = FAIL; - } - } + const uschar * list = arg; + uschar *ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size); + uschar *opt; + BOOL defer_ok = FALSE; + int timeout = 0; + + while ((opt = string_nextinlist(&list, &sep, NULL, 0))) + if (strcmpic(opt, US"defer_ok") == 0) + defer_ok = TRUE; + else if ( strncmpic(opt, US"tmo=", 4) == 0 + && (timeout = readconf_readtime(opt+4, '\0', FALSE)) < 0 + ) + { + *log_msgptr = string_sprintf("bad timeout value in '%s'", opt); + return ERROR; + } + + rc = malware(ss, timeout); + if (rc == DEFER && defer_ok) + rc = FAIL; /* FAIL so that the message is passed to the next ACL */ } break; @@ -3217,8 +3644,8 @@ for (; cb != NULL; cb = cb->next) break; case ACLC_RECIPIENTS: - rc = match_address_list(addr->address, TRUE, TRUE, &arg, NULL, -1, 0, - &recipient_data); + rc = match_address_list((const uschar *)addr->address, TRUE, TRUE, &arg, NULL, -1, 0, + CUSS &recipient_data); break; #ifdef WITH_CONTENT_SCAN @@ -3227,6 +3654,10 @@ for (; cb != NULL; cb = cb->next) break; #endif + case ACLC_REMOVE_HEADER: + setup_remove_header(arg); + break; + case ACLC_SENDER_DOMAINS: { uschar *sdomain; @@ -3238,8 +3669,8 @@ for (; cb != NULL; cb = cb->next) break; case ACLC_SENDERS: - rc = match_address_list(sender_address, TRUE, TRUE, &arg, - sender_address_cache, -1, 0, &sender_data); + rc = match_address_list((const uschar *)sender_address, TRUE, TRUE, &arg, + sender_address_cache, -1, 0, CUSS &sender_data); break; /* Connection variables must persist forever */ @@ -3257,11 +3688,12 @@ for (; cb != NULL; cb = cb->next) case ACLC_SPAM: { /* Seperate the regular expression and any optional parameters. */ - uschar *ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size); + const uschar * list = arg; + uschar *ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size); /* Run the spam backend. */ - rc = spam(&ss); + rc = spam(CUSS &ss); /* Modify return code based upon the existance of options. */ - while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size)) + while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size)) != NULL) { if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER) { @@ -3282,6 +3714,10 @@ for (; cb != NULL; cb = cb->next) break; #endif + case ACLC_UDPSEND: + rc = acl_udpsend(arg, log_msgptr); + break; + /* If the verb is WARN, discard any user message from verification, because such messages are SMTP responses, not header additions. The latter come only from explicit "message" modifiers. However, put the user message into @@ -3730,7 +4166,11 @@ while (acl != NULL) switch(acl->verb) { case ACL_ACCEPT: - if (cond == OK || cond == DISCARD) return cond; + if (cond == OK || cond == DISCARD) + { + HDEBUG(D_acl) debug_printf("end of %s: ACCEPT\n", acl_name); + return cond; + } if (endpass_seen) { HDEBUG(D_acl) debug_printf("accept: endpass encountered - denying access\n"); @@ -3741,17 +4181,26 @@ while (acl != NULL) case ACL_DEFER: if (cond == OK) { + HDEBUG(D_acl) debug_printf("end of %s: DEFER\n", acl_name); acl_temp_details = TRUE; return DEFER; } break; case ACL_DENY: - if (cond == OK) return FAIL; + if (cond == OK) + { + HDEBUG(D_acl) debug_printf("end of %s: DENY\n", acl_name); + return FAIL; + } break; case ACL_DISCARD: - if (cond == OK || cond == DISCARD) return DISCARD; + if (cond == OK || cond == DISCARD) + { + HDEBUG(D_acl) debug_printf("end of %s: DISCARD\n", acl_name); + return DISCARD; + } if (endpass_seen) { HDEBUG(D_acl) debug_printf("discard: endpass encountered - denying access\n"); @@ -3760,11 +4209,19 @@ while (acl != NULL) break; case ACL_DROP: - if (cond == OK) return FAIL_DROP; + if (cond == OK) + { + HDEBUG(D_acl) debug_printf("end of %s: DROP\n", acl_name); + return FAIL_DROP; + } break; case ACL_REQUIRE: - if (cond != OK) return cond; + if (cond != OK) + { + HDEBUG(D_acl) debug_printf("end of %s: not OK\n", acl_name); + return cond; + } break; case ACL_WARN: @@ -3796,10 +4253,97 @@ return FAIL; } + + +/* Same args as acl_check_internal() above, but the string s is +the name of an ACL followed optionally by up to 9 space-separated arguments. +The name and args are separately expanded. Args go into $acl_arg globals. */ +static int +acl_check_wargs(int where, address_item *addr, const uschar *s, int level, + uschar **user_msgptr, uschar **log_msgptr) +{ +uschar * tmp; +uschar * tmp_arg[9]; /* must match acl_arg[] */ +uschar * sav_arg[9]; /* must match acl_arg[] */ +int sav_narg; +uschar * name; +int i; +int ret; + +if (!(tmp = string_dequote(&s)) || !(name = expand_string(tmp))) + goto bad; + +for (i = 0; i < 9; i++) + { + while (*s && isspace(*s)) s++; + if (!*s) break; + if (!(tmp = string_dequote(&s)) || !(tmp_arg[i] = expand_string(tmp))) + { + tmp = name; + goto bad; + } + } + +sav_narg = acl_narg; +acl_narg = i; +for (i = 0; i < acl_narg; i++) + { + sav_arg[i] = acl_arg[i]; + acl_arg[i] = tmp_arg[i]; + } +while (i < 9) + { + sav_arg[i] = acl_arg[i]; + acl_arg[i++] = NULL; + } + +ret = acl_check_internal(where, addr, name, level, user_msgptr, log_msgptr); + +acl_narg = sav_narg; +for (i = 0; i < 9; i++) acl_arg[i] = sav_arg[i]; +return ret; + +bad: +if (expand_string_forcedfail) return ERROR; +*log_msgptr = string_sprintf("failed to expand ACL string \"%s\": %s", + tmp, expand_string_message); +return search_find_defer?DEFER:ERROR; +} + + + /************************************************* * Check access using an ACL * *************************************************/ +/* Alternate interface for ACL, used by expansions */ +int +acl_eval(int where, uschar *s, uschar **user_msgptr, uschar **log_msgptr) +{ +address_item adb; +address_item *addr = NULL; + +*user_msgptr = *log_msgptr = NULL; +sender_verified_failed = NULL; +ratelimiters_cmd = NULL; +log_reject_target = LOG_MAIN|LOG_REJECT; + +if (where == ACL_WHERE_RCPT) + { + adb = address_defaults; + addr = &adb; + addr->address = expand_string(US"$local_part@$domain"); + addr->domain = deliver_domain; + addr->local_part = deliver_localpart; + addr->cc_local_part = deliver_localpart; + addr->lc_local_part = deliver_localpart; + } + +return acl_check_internal(where, addr, s, 0, user_msgptr, log_msgptr); +} + + + /* This is the external interface for ACL checks. It sets up an address and the expansions for $domain and $local_part when called after RCPT, then calls acl_check_internal() to do the actual work. @@ -3818,6 +4362,7 @@ Returns: OK access is granted by an ACCEPT verb DEFER can't tell at the moment ERROR disaster */ +int acl_where = ACL_WHERE_UNKNOWN; int acl_check(int where, uschar *recipient, uschar *s, uschar **user_msgptr, @@ -3832,7 +4377,11 @@ sender_verified_failed = NULL; ratelimiters_cmd = NULL; log_reject_target = LOG_MAIN|LOG_REJECT; +#ifndef DISABLE_PRDR +if (where == ACL_WHERE_RCPT || where == ACL_WHERE_PRDR) +#else if (where == ACL_WHERE_RCPT) +#endif { adb = address_defaults; addr = &adb; @@ -3846,7 +4395,54 @@ if (where == ACL_WHERE_RCPT) deliver_localpart = addr->local_part; } +acl_where = where; rc = acl_check_internal(where, addr, s, 0, user_msgptr, log_msgptr); +acl_where = ACL_WHERE_UNKNOWN; + +/* Cutthrough - if requested, +and WHERE_RCPT and not yet opened conn as result of recipient-verify, +and rcpt acl returned accept, +and first recipient (cancel on any subsequents) +open one now and run it up to RCPT acceptance. +A failed verify should cancel cutthrough request. + +Initial implementation: dual-write to spool. +Assume the rxd datastream is now being copied byte-for-byte to an open cutthrough connection. + +Cease cutthrough copy on rxd final dot; do not send one. + +On a data acl, if not accept and a cutthrough conn is open, hard-close it (no SMTP niceness). + +On data acl accept, terminate the dataphase on an open cutthrough conn. If accepted or +perm-rejected, reflect that to the original sender - and dump the spooled copy. +If temp-reject, close the conn (and keep the spooled copy). +If conn-failure, no action (and keep the spooled copy). +*/ +switch (where) +{ +case ACL_WHERE_RCPT: +#ifndef DISABLE_PRDR +case ACL_WHERE_PRDR: +#endif + if (rc == OK && cutthrough.delivery && rcpt_count > cutthrough.nrcpt) + open_cutthrough_connection(addr); + break; + +case ACL_WHERE_PREDATA: + if( rc == OK ) + cutthrough_predata(); + else + cancel_cutthrough_connection("predata acl not ok"); + break; + +case ACL_WHERE_QUIT: +case ACL_WHERE_NOTQUIT: + cancel_cutthrough_connection("quit or notquit"); + break; + +default: + break; +} deliver_domain = deliver_localpart = deliver_address_data = sender_address_data = NULL; @@ -3885,7 +4481,6 @@ return rc; } - /************************************************* * Create ACL variable * *************************************************/ @@ -3944,4 +4539,6 @@ FILE *f = (FILE *)ctx; fprintf(f, "-acl%c %s %d\n%s\n", name[0], name+1, Ustrlen(value), value); } +/* vi: aw ai sw=2 +*/ /* End of acl.c */