X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/fb2274d4a2c4398a497fbec5cacebaab7d20a127..45b915963e2e3721fc65c7c3f50f2f65f5c54d1b:/src/src/acl.c diff --git a/src/src/acl.c b/src/src/acl.c index 439e9d424..086fa68fd 100644 --- a/src/src/acl.c +++ b/src/src/acl.c @@ -1,10 +1,10 @@ -/* $Cambridge: exim/src/src/acl.c,v 1.20 2005/03/08 15:32:02 tom Exp $ */ +/* $Cambridge: exim/src/src/acl.c,v 1.62 2006/06/28 16:00:23 ph10 Exp $ */ /************************************************* * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) University of Cambridge 1995 - 2005 */ +/* Copyright (c) University of Cambridge 1995 - 2006 */ /* See the file NOTICE for conditions of use and distribution. */ /* Code for handling Access Control Lists (ACLs) */ @@ -32,13 +32,17 @@ static uschar *verbs[] = static int msgcond[] = { FAIL, OK, OK, FAIL, OK, FAIL, OK }; /* ACL condition and modifier codes - keep in step with the table that -follows. */ +follows, and the cond_expand_at_top and uschar cond_modifiers tables lower +down. */ -enum { ACLC_ACL, ACLC_AUTHENTICATED, +enum { ACLC_ACL, + ACLC_ADD_HEADER, + ACLC_AUTHENTICATED, #ifdef EXPERIMENTAL_BRIGHTMAIL ACLC_BMI_OPTIN, #endif -ACLC_CONDITION, ACLC_CONTROL, + ACLC_CONDITION, + ACLC_CONTROL, #ifdef WITH_CONTENT_SCAN ACLC_DECODE, #endif @@ -54,8 +58,14 @@ ACLC_CONDITION, ACLC_CONTROL, ACLC_DK_SENDERS, ACLC_DK_STATUS, #endif - ACLC_DNSLISTS, ACLC_DOMAINS, ACLC_ENCRYPTED, ACLC_ENDPASS, - ACLC_HOSTS, ACLC_LOCAL_PARTS, ACLC_LOG_MESSAGE, ACLC_LOGWRITE, + ACLC_DNSLISTS, + ACLC_DOMAINS, + ACLC_ENCRYPTED, + ACLC_ENDPASS, + ACLC_HOSTS, + ACLC_LOCAL_PARTS, + ACLC_LOG_MESSAGE, + ACLC_LOGWRITE, #ifdef WITH_CONTENT_SCAN ACLC_MALWARE, #endif @@ -63,11 +73,14 @@ ACLC_CONDITION, ACLC_CONTROL, #ifdef WITH_CONTENT_SCAN ACLC_MIME_REGEX, #endif + ACLC_RATELIMIT, ACLC_RECIPIENTS, #ifdef WITH_CONTENT_SCAN ACLC_REGEX, #endif - ACLC_SENDER_DOMAINS, ACLC_SENDERS, ACLC_SET, + ACLC_SENDER_DOMAINS, + ACLC_SENDERS, + ACLC_SET, #ifdef WITH_CONTENT_SCAN ACLC_SPAM, #endif @@ -80,7 +93,10 @@ ACLC_CONDITION, ACLC_CONTROL, "log_message", "logwrite", and "set" are modifiers that look like conditions but always return TRUE. They are used for their side effects. */ -static uschar *conditions[] = { US"acl", US"authenticated", +static uschar *conditions[] = { + US"acl", + US"add_header", + US"authenticated", #ifdef EXPERIMENTAL_BRIGHTMAIL US"bmi_optin", #endif @@ -110,6 +126,7 @@ static uschar *conditions[] = { US"acl", US"authenticated", #ifdef WITH_CONTENT_SCAN US"mime_regex", #endif + US"ratelimit", US"recipients", #ifdef WITH_CONTENT_SCAN US"regex", @@ -123,11 +140,62 @@ static uschar *conditions[] = { US"acl", US"authenticated", #endif US"verify" }; -/* ACL control names */ -static uschar *controls[] = { US"error", US"caseful_local_part", - US"caselower_local_part", US"enforce_sync", US"no_enforce_sync", US"freeze", - US"queue_only", US"submission", US"no_multiline"}; +/* Return values from decode_control(); keep in step with the table of names +that follows! */ + +enum { + CONTROL_AUTH_UNADVERTISED, + #ifdef EXPERIMENTAL_BRIGHTMAIL + CONTROL_BMI_RUN, + #endif + #ifdef EXPERIMENTAL_DOMAINKEYS + CONTROL_DK_VERIFY, + #endif + CONTROL_ERROR, + CONTROL_CASEFUL_LOCAL_PART, + CONTROL_CASELOWER_LOCAL_PART, + CONTROL_ENFORCE_SYNC, + CONTROL_NO_ENFORCE_SYNC, + CONTROL_FREEZE, + CONTROL_QUEUE_ONLY, + CONTROL_SUBMISSION, + CONTROL_SUPPRESS_LOCAL_FIXUPS, + #ifdef WITH_CONTENT_SCAN + CONTROL_NO_MBOX_UNSPOOL, + #endif + CONTROL_FAKEDEFER, + CONTROL_FAKEREJECT, + CONTROL_NO_MULTILINE +}; + +/* ACL control names; keep in step with the table above! This list is used for +turning ids into names. The actual list of recognized names is in the variable +control_def controls_list[] below. The fact that there are two lists is a mess +and should be tidied up. */ + +static uschar *controls[] = { + US"allow_auth_unadvertised", + #ifdef EXPERIMENTAL_BRIGHTMAIL + US"bmi_run", + #endif + #ifdef EXPERIMENTAL_DOMAINKEYS + US"dk_verify", + #endif + US"error", + US"caseful_local_part", + US"caselower_local_part", + US"enforce_sync", + US"no_enforce_sync", + US"freeze", + US"queue_only", + US"submission", + US"suppress_local_fixups", + #ifdef WITH_CONTENT_SCAN + US"no_mbox_unspool", + #endif + US"no_multiline" +}; /* Flags to indicate for which conditions /modifiers a string expansion is done at the outer level. In the other cases, expansion already occurs in the @@ -135,6 +203,7 @@ checking functions. */ static uschar cond_expand_at_top[] = { TRUE, /* acl */ + TRUE, /* add_header */ FALSE, /* authenticated */ #ifdef EXPERIMENTAL_BRIGHTMAIL TRUE, /* bmi_optin */ @@ -149,12 +218,12 @@ static uschar cond_expand_at_top[] = { TRUE, /* demime */ #endif #ifdef EXPERIMENTAL_DOMAINKEYS - TRUE, - TRUE, - TRUE, - TRUE, - TRUE, - TRUE, + TRUE, /* dk_domain_source */ + TRUE, /* dk_policy */ + TRUE, /* dk_sender_domains */ + TRUE, /* dk_sender_local_parts */ + TRUE, /* dk_senders */ + TRUE, /* dk_status */ #endif TRUE, /* dnslists */ FALSE, /* domains */ @@ -171,6 +240,7 @@ static uschar cond_expand_at_top[] = { #ifdef WITH_CONTENT_SCAN TRUE, /* mime_regex */ #endif + TRUE, /* ratelimit */ FALSE, /* recipients */ #ifdef WITH_CONTENT_SCAN TRUE, /* regex */ @@ -191,6 +261,7 @@ static uschar cond_expand_at_top[] = { static uschar cond_modifiers[] = { FALSE, /* acl */ + TRUE, /* add_header */ FALSE, /* authenticated */ #ifdef EXPERIMENTAL_BRIGHTMAIL TRUE, /* bmi_optin */ @@ -205,12 +276,12 @@ static uschar cond_modifiers[] = { FALSE, /* demime */ #endif #ifdef EXPERIMENTAL_DOMAINKEYS - FALSE, - FALSE, - FALSE, - FALSE, - FALSE, - FALSE, + FALSE, /* dk_domain_source */ + FALSE, /* dk_policy */ + FALSE, /* dk_sender_domains */ + FALSE, /* dk_sender_local_parts */ + FALSE, /* dk_senders */ + FALSE, /* dk_status */ #endif FALSE, /* dnslists */ FALSE, /* domains */ @@ -227,6 +298,7 @@ static uschar cond_modifiers[] = { #ifdef WITH_CONTENT_SCAN FALSE, /* mime_regex */ #endif + FALSE, /* ratelimit */ FALSE, /* recipients */ #ifdef WITH_CONTENT_SCAN FALSE, /* regex */ @@ -244,171 +316,147 @@ static uschar cond_modifiers[] = { }; /* Bit map vector of which conditions are not allowed at certain times. For -each condition, there's a bitmap of dis-allowed times. */ +each condition, there's a bitmap of dis-allowed times. For some, it is easier +to specify the negation of a small number of allowed times. */ static unsigned int cond_forbids[] = { 0, /* acl */ - (1<= max) { - *error = string_sprintf("unrecognized name after \"set\" in ACL " - "modifier \"set %s\"", s); + BAD_ACL_VAR: + *error = string_sprintf("syntax error or unrecognized name after " + "\"set\" in ACL modifier \"set %s\"", s); return NULL; } - cond->u.varnumber = s[5] - '0'; - if (s[4] == 'm') cond->u.varnumber += ACL_C_MAX; - s += 6; + cond->u.varnumber = n + offset; + s = endptr; while (isspace(*s)) s++; } @@ -760,6 +860,115 @@ return yield; +/************************************************* +* Set up added header line(s) * +*************************************************/ + +/* This function is called by the add_header modifier, and also from acl_warn() +to implement the now-deprecated way of adding header lines using "message" on a +"warn" verb. The argument is treated as a sequence of header lines which are +added to a chain, provided there isn't an identical one already there. + +Argument: string of header lines +Returns: nothing +*/ + +static void +setup_header(uschar *hstring) +{ +uschar *p, *q; +int hlen = Ustrlen(hstring); + +/* An empty string does nothing; otherwise add a final newline if necessary. */ + +if (hlen <= 0) return; +if (hstring[hlen-1] != '\n') hstring = string_sprintf("%s\n", hstring); + +/* Loop for multiple header lines, taking care about continuations */ + +for (p = q = hstring; *p != 0; ) + { + uschar *s; + int newtype = htype_add_bot; + header_line **hptr = &acl_added_headers; + + /* Find next header line within the string */ + + for (;;) + { + q = Ustrchr(q, '\n'); + if (*(++q) != ' ' && *q != '\t') break; + } + + /* If the line starts with a colon, interpret the instruction for where to + add it. This temporarily sets up a new type. */ + + if (*p == ':') + { + if (strncmpic(p, US":after_received:", 16) == 0) + { + newtype = htype_add_rec; + p += 16; + } + else if (strncmpic(p, US":at_start_rfc:", 14) == 0) + { + newtype = htype_add_rfc; + p += 14; + } + else if (strncmpic(p, US":at_start:", 10) == 0) + { + newtype = htype_add_top; + p += 10; + } + else if (strncmpic(p, US":at_end:", 8) == 0) + { + newtype = htype_add_bot; + p += 8; + } + while (*p == ' ' || *p == '\t') p++; + } + + /* See if this line starts with a header name, and if not, add X-ACL-Warn: + to the front of it. */ + + for (s = p; s < q - 1; s++) + { + if (*s == ':' || !isgraph(*s)) break; + } + + s = string_sprintf("%s%.*s", (*s == ':')? "" : "X-ACL-Warn: ", q - p, p); + hlen = Ustrlen(s); + + /* See if this line has already been added */ + + while (*hptr != NULL) + { + if (Ustrncmp((*hptr)->text, s, hlen) == 0) break; + hptr = &((*hptr)->next); + } + + /* Add if not previously present */ + + if (*hptr == NULL) + { + header_line *h = store_get(sizeof(header_line)); + h->text = s; + h->next = NULL; + h->type = newtype; + h->slen = hlen; + *hptr = h; + hptr = &(h->next); + } + + /* Advance for next header line within the string */ + + p = q; + } +} + + + + /************************************************* * Handle warnings * *************************************************/ @@ -768,6 +977,9 @@ return yield; the message's headers, and/or writes information to the log. In each case, this only happens once (per message for headers, per connection for log). +** NOTE: The header adding action using the "message" setting is historic, and +its use is now deprecated. The new add_header modifier should be used instead. + Arguments: where ACL_WHERE_xxxx indicating which ACL this is user_message message for adding to headers @@ -779,8 +991,6 @@ Returns: nothing static void acl_warn(int where, uschar *user_message, uschar *log_message) { -int hlen; - if (log_message != NULL && log_message != user_message) { uschar *text; @@ -797,8 +1007,8 @@ if (log_message != NULL && log_message != user_message) strcmpic(log_message, US"sender verify failed") == 0) text = string_sprintf("%s: %s", text, sender_verified_failed->message); - /* Search previously logged warnings. They are kept in malloc store so they - can be freed at the start of a new message. */ + /* Search previously logged warnings. They are kept in malloc + store so they can be freed at the start of a new message. */ for (logged = acl_warn_logged; logged != NULL; logged = logged->next) if (Ustrcmp(logged->text, text) == 0) break; @@ -830,99 +1040,10 @@ if (where > ACL_WHERE_NOTSMTP) return; } -/* Treat the user message as a sequence of one or more header lines. */ - -hlen = Ustrlen(user_message); -if (hlen > 0) - { - uschar *text, *p, *q; +/* The code for setting up header lines is now abstracted into a separate +function so that it can be used for the add_header modifier as well. */ - /* Add a final newline if not present */ - - text = ((user_message)[hlen-1] == '\n')? user_message : - string_sprintf("%s\n", user_message); - - /* Loop for multiple header lines, taking care about continuations */ - - for (p = q = text; *p != 0; ) - { - uschar *s; - int newtype = htype_add_bot; - header_line **hptr = &acl_warn_headers; - - /* Find next header line within the string */ - - for (;;) - { - q = Ustrchr(q, '\n'); - if (*(++q) != ' ' && *q != '\t') break; - } - - /* If the line starts with a colon, interpret the instruction for where to - add it. This temporarily sets up a new type. */ - - if (*p == ':') - { - if (strncmpic(p, US":after_received:", 16) == 0) - { - newtype = htype_add_rec; - p += 16; - } - else if (strncmpic(p, US":at_start_rfc:", 14) == 0) - { - newtype = htype_add_rfc; - p += 14; - } - else if (strncmpic(p, US":at_start:", 10) == 0) - { - newtype = htype_add_top; - p += 10; - } - else if (strncmpic(p, US":at_end:", 8) == 0) - { - newtype = htype_add_bot; - p += 8; - } - while (*p == ' ' || *p == '\t') p++; - } - - /* See if this line starts with a header name, and if not, add X-ACL-Warn: - to the front of it. */ - - for (s = p; s < q - 1; s++) - { - if (*s == ':' || !isgraph(*s)) break; - } - - s = string_sprintf("%s%.*s", (*s == ':')? "" : "X-ACL-Warn: ", q - p, p); - hlen = Ustrlen(s); - - /* See if this line has already been added */ - - while (*hptr != NULL) - { - if (Ustrncmp((*hptr)->text, s, hlen) == 0) break; - hptr = &((*hptr)->next); - } - - /* Add if not previously present */ - - if (*hptr == NULL) - { - header_line *h = store_get(sizeof(header_line)); - h->text = s; - h->next = NULL; - h->type = newtype; - h->slen = hlen; - *hptr = h; - hptr = &(h->next); - } - - /* Advance for next header line within the string */ - - p = q; - } - } +setup_header(user_message); } @@ -985,6 +1106,304 @@ return OK; +/************************************************* +* Check client IP address matches CSA target * +*************************************************/ + +/* Called from acl_verify_csa() below. This routine scans a section of a DNS +response for address records belonging to the CSA target hostname. The section +is specified by the reset argument, either RESET_ADDITIONAL or RESET_ANSWERS. +If one of the addresses matches the client's IP address, then the client is +authorized by CSA. If there are target IP addresses but none of them match +then the client is using an unauthorized IP address. If there are no target IP +addresses then the client cannot be using an authorized IP address. (This is +an odd configuration - why didn't the SRV record have a weight of 1 instead?) + +Arguments: + dnsa the DNS answer block + dnss a DNS scan block for us to use + reset option specifing what portion to scan, as described above + target the target hostname to use for matching RR names + +Returns: CSA_OK successfully authorized + CSA_FAIL_MISMATCH addresses found but none matched + CSA_FAIL_NOADDR no target addresses found +*/ + +static int +acl_verify_csa_address(dns_answer *dnsa, dns_scan *dnss, int reset, + uschar *target) +{ +dns_record *rr; +dns_address *da; + +BOOL target_found = FALSE; + +for (rr = dns_next_rr(dnsa, dnss, reset); + rr != NULL; + rr = dns_next_rr(dnsa, dnss, RESET_NEXT)) + { + /* Check this is an address RR for the target hostname. */ + + if (rr->type != T_A + #if HAVE_IPV6 + && rr->type != T_AAAA + #ifdef SUPPORT_A6 + && rr->type != T_A6 + #endif + #endif + ) continue; + + if (strcmpic(target, rr->name) != 0) continue; + + target_found = TRUE; + + /* Turn the target address RR into a list of textual IP addresses and scan + the list. There may be more than one if it is an A6 RR. */ + + for (da = dns_address_from_rr(dnsa, rr); da != NULL; da = da->next) + { + /* If the client IP address matches the target IP address, it's good! */ + + DEBUG(D_acl) debug_printf("CSA target address is %s\n", da->address); + + if (strcmpic(sender_host_address, da->address) == 0) return CSA_OK; + } + } + +/* If we found some target addresses but none of them matched, the client is +using an unauthorized IP address, otherwise the target has no authorized IP +addresses. */ + +if (target_found) return CSA_FAIL_MISMATCH; +else return CSA_FAIL_NOADDR; +} + + + +/************************************************* +* Verify Client SMTP Authorization * +*************************************************/ + +/* Called from acl_verify() below. This routine calls dns_lookup_special() +to find the CSA SRV record corresponding to the domain argument, or +$sender_helo_name if no argument is provided. It then checks that the +client is authorized, and that its IP address corresponds to the SRV +target's address by calling acl_verify_csa_address() above. The address +should have been returned in the DNS response's ADDITIONAL section, but if +not we perform another DNS lookup to get it. + +Arguments: + domain pointer to optional parameter following verify = csa + +Returns: CSA_UNKNOWN no valid CSA record found + CSA_OK successfully authorized + CSA_FAIL_* client is definitely not authorized + CSA_DEFER_* there was a DNS problem +*/ + +static int +acl_verify_csa(uschar *domain) +{ +tree_node *t; +uschar *found, *p; +int priority, weight, port; +dns_answer dnsa; +dns_scan dnss; +dns_record *rr; +int rc, type; +uschar target[256]; + +/* Work out the domain we are using for the CSA lookup. The default is the +client's HELO domain. If the client has not said HELO, use its IP address +instead. If it's a local client (exim -bs), CSA isn't applicable. */ + +while (isspace(*domain) && *domain != '\0') ++domain; +if (*domain == '\0') domain = sender_helo_name; +if (domain == NULL) domain = sender_host_address; +if (sender_host_address == NULL) return CSA_UNKNOWN; + +/* If we have an address literal, strip off the framing ready for turning it +into a domain. The framing consists of matched square brackets possibly +containing a keyword and a colon before the actual IP address. */ + +if (domain[0] == '[') + { + uschar *start = Ustrchr(domain, ':'); + if (start == NULL) start = domain; + domain = string_copyn(start + 1, Ustrlen(start) - 2); + } + +/* Turn domains that look like bare IP addresses into domains in the reverse +DNS. This code also deals with address literals and $sender_host_address. It's +not quite kosher to treat bare domains such as EHLO 192.0.2.57 the same as +address literals, but it's probably the most friendly thing to do. This is an +extension to CSA, so we allow it to be turned off for proper conformance. */ + +if (string_is_ip_address(domain, NULL) != 0) + { + if (!dns_csa_use_reverse) return CSA_UNKNOWN; + dns_build_reverse(domain, target); + domain = target; + } + +/* Find out if we've already done the CSA check for this domain. If we have, +return the same result again. Otherwise build a new cached result structure +for this domain. The name is filled in now, and the value is filled in when +we return from this function. */ + +t = tree_search(csa_cache, domain); +if (t != NULL) return t->data.val; + +t = store_get_perm(sizeof(tree_node) + Ustrlen(domain)); +Ustrcpy(t->name, domain); +(void)tree_insertnode(&csa_cache, t); + +/* Now we are ready to do the actual DNS lookup(s). */ + +found = domain; +switch (dns_special_lookup(&dnsa, domain, T_CSA, &found)) + { + /* If something bad happened (most commonly DNS_AGAIN), defer. */ + + default: + return t->data.val = CSA_DEFER_SRV; + + /* If we found nothing, the client's authorization is unknown. */ + + case DNS_NOMATCH: + case DNS_NODATA: + return t->data.val = CSA_UNKNOWN; + + /* We got something! Go on to look at the reply in more detail. */ + + case DNS_SUCCEED: + break; + } + +/* Scan the reply for well-formed CSA SRV records. */ + +for (rr = dns_next_rr(&dnsa, &dnss, RESET_ANSWERS); + rr != NULL; + rr = dns_next_rr(&dnsa, &dnss, RESET_NEXT)) + { + if (rr->type != T_SRV) continue; + + /* Extract the numerical SRV fields (p is incremented) */ + + p = rr->data; + GETSHORT(priority, p); + GETSHORT(weight, p); + GETSHORT(port, p); + + DEBUG(D_acl) + debug_printf("CSA priority=%d weight=%d port=%d\n", priority, weight, port); + + /* Check the CSA version number */ + + if (priority != 1) continue; + + /* If the domain does not have a CSA SRV record of its own (i.e. the domain + found by dns_special_lookup() is a parent of the one we asked for), we check + the subdomain assertions in the port field. At the moment there's only one + assertion: legitimate SMTP clients are all explicitly authorized with CSA + SRV records of their own. */ + + if (found != domain) + { + if (port & 1) + return t->data.val = CSA_FAIL_EXPLICIT; + else + return t->data.val = CSA_UNKNOWN; + } + + /* This CSA SRV record refers directly to our domain, so we check the value + in the weight field to work out the domain's authorization. 0 and 1 are + unauthorized; 3 means the client is authorized but we can't check the IP + address in order to authenticate it, so we treat it as unknown; values + greater than 3 are undefined. */ + + if (weight < 2) return t->data.val = CSA_FAIL_DOMAIN; + + if (weight > 2) continue; + + /* Weight == 2, which means the domain is authorized. We must check that the + client's IP address is listed as one of the SRV target addresses. Save the + target hostname then break to scan the additional data for its addresses. */ + + (void)dn_expand(dnsa.answer, dnsa.answer + dnsa.answerlen, p, + (DN_EXPAND_ARG4_TYPE)target, sizeof(target)); + + DEBUG(D_acl) debug_printf("CSA target is %s\n", target); + + break; + } + +/* If we didn't break the loop then no appropriate records were found. */ + +if (rr == NULL) return t->data.val = CSA_UNKNOWN; + +/* Do not check addresses if the target is ".", in accordance with RFC 2782. +A target of "." indicates there are no valid addresses, so the client cannot +be authorized. (This is an odd configuration because weight=2 target=. is +equivalent to weight=1, but we check for it in order to keep load off the +root name servers.) Note that dn_expand() turns "." into "". */ + +if (Ustrcmp(target, "") == 0) return t->data.val = CSA_FAIL_NOADDR; + +/* Scan the additional section of the CSA SRV reply for addresses belonging +to the target. If the name server didn't return any additional data (e.g. +because it does not fully support SRV records), we need to do another lookup +to obtain the target addresses; otherwise we have a definitive result. */ + +rc = acl_verify_csa_address(&dnsa, &dnss, RESET_ADDITIONAL, target); +if (rc != CSA_FAIL_NOADDR) return t->data.val = rc; + +/* The DNS lookup type corresponds to the IP version used by the client. */ + +#if HAVE_IPV6 +if (Ustrchr(sender_host_address, ':') != NULL) + type = T_AAAA; +else +#endif /* HAVE_IPV6 */ + type = T_A; + + +#if HAVE_IPV6 && defined(SUPPORT_A6) +DNS_LOOKUP_AGAIN: +#endif + +switch (dns_lookup(&dnsa, target, type, NULL)) + { + /* If something bad happened (most commonly DNS_AGAIN), defer. */ + + default: + return t->data.val = CSA_DEFER_ADDR; + + /* If the query succeeded, scan the addresses and return the result. */ + + case DNS_SUCCEED: + rc = acl_verify_csa_address(&dnsa, &dnss, RESET_ANSWERS, target); + if (rc != CSA_FAIL_NOADDR) return t->data.val = rc; + /* else fall through */ + + /* If the target has no IP addresses, the client cannot have an authorized + IP address. However, if the target site uses A6 records (not AAAA records) + we have to do yet another lookup in order to check them. */ + + case DNS_NOMATCH: + case DNS_NODATA: + + #if HAVE_IPV6 && defined(SUPPORT_A6) + if (type == T_AAAA) { type = T_A6; goto DNS_LOOKUP_AGAIN; } + #endif + + return t->data.val = CSA_FAIL_NOADDR; + } +} + + + /************************************************* * Handle verification (address & other) * *************************************************/ @@ -1022,10 +1441,18 @@ BOOL verify_header_sender = FALSE; BOOL defer_ok = FALSE; BOOL callout_defer_ok = FALSE; BOOL no_details = FALSE; +BOOL success_on_redirect = FALSE; address_item *sender_vaddr = NULL; uschar *verify_sender_address = NULL; uschar *pm_mailfrom = NULL; uschar *se_mailfrom = NULL; + +/* Some of the verify items have slash-separated options; some do not. Diagnose +an error if options are given for items that don't expect them. This code has +now got very message. Refactoring to use a table would be a good idea one day. +*/ + +uschar *slash = Ustrchr(arg, '/'); uschar *list = arg; uschar *ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size); @@ -1035,6 +1462,7 @@ if (ss == NULL) goto BAD_VERIFY; if (strcmpic(ss, US"reverse_host_lookup") == 0) { + if (slash != NULL) goto NO_OPTIONS; if (sender_host_address == NULL) return OK; return acl_verify_reverse(user_msgptr, log_msgptr); } @@ -1045,49 +1473,80 @@ mandatory verification, the connection doesn't last this long.) */ if (strcmpic(ss, US"certificate") == 0) { + if (slash != NULL) goto NO_OPTIONS; if (tls_certificate_verified) return OK; *user_msgptr = US"no verified certificate"; return FAIL; } -/* We can test the result of optional HELO verification */ +/* We can test the result of optional HELO verification that might have +occurred earlier. If not, we can attempt the verification now. */ -if (strcmpic(ss, US"helo") == 0) return helo_verified? OK : FAIL; +if (strcmpic(ss, US"helo") == 0) + { + if (slash != NULL) goto NO_OPTIONS; + if (!helo_verified && !helo_verify_failed) smtp_verify_helo(); + return helo_verified? OK : FAIL; + } -/* Handle header verification options - permitted only after DATA or a non-SMTP -message. */ +/* Do Client SMTP Authorization checks in a separate function, and turn the +result code into user-friendly strings. */ -if (strncmpic(ss, US"header_", 7) == 0) +if (strcmpic(ss, US"csa") == 0) { - if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP) - { - *log_msgptr = string_sprintf("cannot check header contents in ACL for %s " - "(only possible in ACL for DATA)", acl_wherenames[where]); - return ERROR; - } + rc = acl_verify_csa(list); + *log_msgptr = *user_msgptr = string_sprintf("client SMTP authorization %s", + csa_reason_string[rc]); + csa_status = csa_status_string[rc]; + DEBUG(D_acl) debug_printf("CSA result %s\n", csa_status); + return csa_return_code[rc]; + } + +/* Check that all relevant header lines have the correct syntax. If there is +a syntax error, we return details of the error to the sender if configured to +send out full details. (But a "message" setting on the ACL can override, as +always). */ + +if (strcmpic(ss, US"header_syntax") == 0) + { + if (slash != NULL) goto NO_OPTIONS; + if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP) goto WRONG_ACL; + rc = verify_check_headers(log_msgptr); + if (rc != OK && smtp_return_error_details && *log_msgptr != NULL) + *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr); + return rc; + } - /* Check that all relevant header lines have the correct syntax. If there is - a syntax error, we return details of the error to the sender if configured to - send out full details. (But a "message" setting on the ACL can override, as - always). */ +/* Check that no recipient of this message is "blind", that is, every envelope +recipient must be mentioned in either To: or Cc:. */ - if (strcmpic(ss+7, US"syntax") == 0) +if (strcmpic(ss, US"not_blind") == 0) + { + if (slash != NULL) goto NO_OPTIONS; + if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP) goto WRONG_ACL; + rc = verify_check_notblind(); + if (rc != OK) { - int rc = verify_check_headers(log_msgptr); - if (rc != OK && smtp_return_error_details && *log_msgptr != NULL) + *log_msgptr = string_sprintf("bcc recipient detected"); + if (smtp_return_error_details) *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr); - return rc; } + return rc; + } - /* Check that there is at least one verifiable sender address in the relevant - header lines. This can be followed by callout and defer options, just like - sender and recipient. */ +/* The remaining verification tests check recipient and sender addresses, +either from the envelope or from the header. There are a number of +slash-separated options that are common to all of them. */ - else if (strcmpic(ss+7, US"sender") == 0) verify_header_sender = TRUE; - /* Unknown verify argument starting with "header_" */ +/* Check that there is at least one verifiable sender address in the relevant +header lines. This can be followed by callout and defer options, just like +sender and recipient. */ - else goto BAD_VERIFY; +if (strcmpic(ss, US"header_sender") == 0) + { + if (where != ACL_WHERE_DATA && where != ACL_WHERE_NOTSMTP) goto WRONG_ACL; + verify_header_sender = TRUE; } /* Otherwise, first item in verify argument must be "sender" or "recipient". @@ -1125,13 +1584,15 @@ else } } -/* Remaining items are optional */ +/* Remaining items are optional; they apply to sender and recipient +verification, including "header sender" verification. */ while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size)) != NULL) { if (strcmpic(ss, US"defer_ok") == 0) defer_ok = TRUE; else if (strcmpic(ss, US"no_details") == 0) no_details = TRUE; + else if (strcmpic(ss, US"success_on_redirect") == 0) success_on_redirect = TRUE; /* These two old options are left for backwards compatibility */ @@ -1180,6 +1641,11 @@ while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size)) else if (strcmpic(opt, US"use_postmaster") == 0) verify_options |= vopt_callout_recippmaster; else if (strcmpic(opt, US"postmaster") == 0) pm_mailfrom = US""; + else if (strcmpic(opt, US"fullpostmaster") == 0) + { + pm_mailfrom = US""; + verify_options |= vopt_callout_fullpm; + } else if (strncmpic(opt, US"mailfrom", 8) == 0) { @@ -1384,6 +1850,9 @@ else if (verify_sender_address != NULL) else verify_options |= vopt_fake_sender; + if (success_on_redirect) + verify_options |= vopt_success_on_redirect; + /* The recipient, qualify, and expn options are never set in verify_options. */ @@ -1435,6 +1904,9 @@ else { address_item addr2; + if (success_on_redirect) + verify_options |= vopt_success_on_redirect; + /* We must use a copy of the address for verification, because it might get rewritten. */ @@ -1499,9 +1971,24 @@ return rc; BAD_VERIFY: *log_msgptr = string_sprintf("expected \"sender[=address]\", \"recipient\", " - "\"header_syntax\" or \"header_sender\" at start of ACL condition " + "\"helo\", \"header_syntax\", \"header_sender\" or " + "\"reverse_host_lookup\" at start of ACL condition " "\"verify %s\"", arg); return ERROR; + +/* Options supplied when not allowed come here */ + +NO_OPTIONS: +*log_msgptr = string_sprintf("unexpected '/' found in \"%s\" " + "(this verify item has no options)", arg); +return ERROR; + +/* Calls in the wrong ACL come here */ + +WRONG_ACL: +*log_msgptr = string_sprintf("cannot check header contents in ACL for %s " + "(only possible in ACL for DATA)", acl_wherenames[where]); +return ERROR; } @@ -1549,6 +2036,297 @@ return d->value; +/************************************************* +* Handle rate limiting * +*************************************************/ + +/* Called by acl_check_condition() below to calculate the result +of the ACL ratelimit condition. + +Note that the return value might be slightly unexpected: if the +sender's rate is above the limit then the result is OK. This is +similar to the dnslists condition, and is so that you can write +ACL clauses like: defer ratelimit = 15 / 1h + +Arguments: + arg the option string for ratelimit= + where ACL_WHERE_xxxx indicating which ACL this is + log_msgptr for error messages + +Returns: OK - Sender's rate is above limit + FAIL - Sender's rate is below limit + DEFER - Problem opening ratelimit database + ERROR - Syntax error in options. +*/ + +static int +acl_ratelimit(uschar *arg, int where, uschar **log_msgptr) +{ +double limit, period; +uschar *ss, *key; +int sep = '/'; +BOOL have_key = FALSE, leaky = FALSE, strict = FALSE; +BOOL per_byte = FALSE, per_cmd = FALSE, per_conn = FALSE, per_mail = FALSE; +int old_pool, rc; +tree_node **anchor, *t; +open_db dbblock, *dbm; +dbdata_ratelimit *dbd; +struct timeval tv; + +/* Parse the first two options and record their values in expansion +variables. These variables allow the configuration to have informative +error messages based on rate limits obtained from a table lookup. */ + +/* First is the maximum number of messages per period and maximum burst +size, which must be greater than or equal to zero. Zero is useful for +rate measurement as opposed to rate limiting. */ + +sender_rate_limit = string_nextinlist(&arg, &sep, NULL, 0); +if (sender_rate_limit == NULL) + limit = -1.0; +else + { + limit = Ustrtod(sender_rate_limit, &ss); + if (tolower(*ss) == 'k') { limit *= 1024.0; ss++; } + else if (tolower(*ss) == 'm') { limit *= 1024.0*1024.0; ss++; } + else if (tolower(*ss) == 'g') { limit *= 1024.0*1024.0*1024.0; ss++; } + } +if (limit < 0.0 || *ss != 0) + { + *log_msgptr = string_sprintf("syntax error in argument for " + "\"ratelimit\" condition: \"%s\" is not a positive number", + sender_rate_limit); + return ERROR; + } + +/* We use the rest of the argument list following the limit as the +lookup key, because it doesn't make sense to use the same stored data +if the period or options are different. */ + +key = arg; + +/* Second is the rate measurement period and exponential smoothing time +constant. This must be strictly greater than zero, because zero leads to +run-time division errors. */ + +sender_rate_period = string_nextinlist(&arg, &sep, NULL, 0); +if (sender_rate_period == NULL) period = -1.0; +else period = readconf_readtime(sender_rate_period, 0, FALSE); +if (period <= 0.0) + { + *log_msgptr = string_sprintf("syntax error in argument for " + "\"ratelimit\" condition: \"%s\" is not a time value", + sender_rate_period); + return ERROR; + } + +/* Parse the other options. Should we check if the per_* options are being +used in ACLs where they don't make sense, e.g. per_mail in the connect ACL? */ + +while ((ss = string_nextinlist(&arg, &sep, big_buffer, big_buffer_size)) + != NULL) + { + if (strcmpic(ss, US"leaky") == 0) leaky = TRUE; + else if (strcmpic(ss, US"strict") == 0) strict = TRUE; + else if (strcmpic(ss, US"per_byte") == 0) per_byte = TRUE; + else if (strcmpic(ss, US"per_cmd") == 0) per_cmd = TRUE; + else if (strcmpic(ss, US"per_conn") == 0) per_conn = TRUE; + else if (strcmpic(ss, US"per_mail") == 0) per_mail = TRUE; + else if (strcmpic(ss, US"per_rcpt") == 0) per_cmd = TRUE; /* alias */ + else have_key = TRUE; + } +if (leaky + strict > 1 || per_byte + per_cmd + per_conn + per_mail > 1) + { + *log_msgptr = US"conflicting options for \"ratelimit\" condition"; + return ERROR; + } + +/* Default option values */ +if (!strict) leaky = TRUE; +if (!per_byte && !per_cmd && !per_conn) per_mail = TRUE; + +/* If there is no explicit key, use the sender_host_address. If there is no +sender_host_address (e.g. -bs or acl_not_smtp) then we simply omit it. */ + +if (!have_key && sender_host_address != NULL) + key = string_sprintf("%s / %s", key, sender_host_address); + +HDEBUG(D_acl) debug_printf("ratelimit condition limit=%.0f period=%.0f key=%s\n", + limit, period, key); + +/* See if we have already computed the rate by looking in the relevant tree. For +per-connection rate limiting, store tree nodes and dbdata in the permanent pool +so that they survive across resets. */ + +anchor = NULL; +old_pool = store_pool; + +if (per_conn) + { + anchor = &ratelimiters_conn; + store_pool = POOL_PERM; + } +else if (per_mail || per_byte) + anchor = &ratelimiters_mail; +else if (per_cmd) + anchor = &ratelimiters_cmd; + +if (anchor != NULL && (t = tree_search(*anchor, key)) != NULL) + { + dbd = t->data.ptr; + /* The following few lines duplicate some of the code below. */ + if (dbd->rate < limit) rc = FAIL; + else rc = OK; + store_pool = old_pool; + sender_rate = string_sprintf("%.1f", dbd->rate); + HDEBUG(D_acl) + debug_printf("ratelimit found pre-computed rate %s\n", sender_rate); + return rc; + } + +/* We aren't using a pre-computed rate, so get a previously recorded +rate from the database, update it, and write it back. If there's no +previous rate for this key, create one. */ + +dbm = dbfn_open(US"ratelimit", O_RDWR, &dbblock, TRUE); +if (dbm == NULL) + { + store_pool = old_pool; + sender_rate = NULL; + HDEBUG(D_acl) debug_printf("ratelimit database not available\n"); + *log_msgptr = US"ratelimit database not available"; + return DEFER; + } +dbd = dbfn_read(dbm, key); + +gettimeofday(&tv, NULL); + +if (dbd == NULL) + { + HDEBUG(D_acl) debug_printf("ratelimit initializing new key's data\n"); + dbd = store_get(sizeof(dbdata_ratelimit)); + dbd->time_stamp = tv.tv_sec; + dbd->time_usec = tv.tv_usec; + dbd->rate = 0.0; + } +else + { + /* The smoothed rate is computed using an exponentially weighted moving + average adjusted for variable sampling intervals. The standard EWMA for + a fixed sampling interval is: f'(t) = (1 - a) * f(t) + a * f'(t - 1) + where f() is the measured value and f'() is the smoothed value. + + Old data decays out of the smoothed value exponentially, such that data n + samples old is multiplied by a^n. The exponential decay time constant p + is defined such that data p samples old is multiplied by 1/e, which means + that a = exp(-1/p). We can maintain the same time constant for a variable + sampling interval i by using a = exp(-i/p). + + The rate we are measuring is messages per period, suitable for directly + comparing with the limit. The average rate between now and the previous + message is period / interval, which we feed into the EWMA as the sample. + + It turns out that the number of messages required for the smoothed rate + to reach the limit when they are sent in a burst is equal to the limit. + This can be seen by analysing the value of the smoothed rate after N + messages sent at even intervals. Let k = (1 - a) * p/i + + rate_1 = (1 - a) * p/i + a * rate_0 + = k + a * rate_0 + rate_2 = k + a * rate_1 + = k + a * k + a^2 * rate_0 + rate_3 = k + a * k + a^2 * k + a^3 * rate_0 + rate_N = rate_0 * a^N + k * SUM(x=0..N-1)(a^x) + = rate_0 * a^N + k * (1 - a^N) / (1 - a) + = rate_0 * a^N + p/i * (1 - a^N) + + When N is large, a^N -> 0 so rate_N -> p/i as desired. + + rate_N = p/i + (rate_0 - p/i) * a^N + a^N = (rate_N - p/i) / (rate_0 - p/i) + N * -i/p = log((rate_N - p/i) / (rate_0 - p/i)) + N = p/i * log((rate_0 - p/i) / (rate_N - p/i)) + + Numerical analysis of the above equation, setting the computed rate to + increase from rate_0 = 0 to rate_N = limit, shows that for large sending + rates, p/i, the number of messages N = limit. So limit serves as both the + maximum rate measured in messages per period, and the maximum number of + messages that can be sent in a fast burst. */ + + double this_time = (double)tv.tv_sec + + (double)tv.tv_usec / 1000000.0; + double prev_time = (double)dbd->time_stamp + + (double)dbd->time_usec / 1000000.0; + + /* We must avoid division by zero, and deal gracefully with the clock going + backwards. If we blunder ahead when time is in reverse then the computed + rate will be bogus. To be safe we clamp interval to a very small number. */ + + double interval = this_time - prev_time <= 0.0 ? 1e-9 + : this_time - prev_time; + + double i_over_p = interval / period; + double a = exp(-i_over_p); + + dbd->time_stamp = tv.tv_sec; + dbd->time_usec = tv.tv_usec; + + /* If we are measuring the rate in bytes per period, multiply the + measured rate by the message size. If we don't know the message size + then it's safe to just use a value of zero and let the recorded rate + decay as if nothing happened. */ + + if (per_byte) + dbd->rate = (message_size < 0 ? 0.0 : (double)message_size) + * (1 - a) / i_over_p + a * dbd->rate; + else if (per_cmd && where == ACL_WHERE_NOTSMTP) + dbd->rate = (double)recipients_count + * (1 - a) / i_over_p + a * dbd->rate; + else + dbd->rate = (1 - a) / i_over_p + a * dbd->rate; + } + +/* Clients sending at the limit are considered to be over the limit. This +matters for edge cases such the first message sent by a client (which gets +the initial rate of 0.0) when the rate limit is zero (i.e. the client should +be completely blocked). */ + +if (dbd->rate < limit) rc = FAIL; + else rc = OK; + +/* Update the state if the rate is low or if we are being strict. If we +are in leaky mode and the sender's rate is too high, we do not update +the recorded rate in order to avoid an over-aggressive sender's retry +rate preventing them from getting any email through. */ + +if (rc == FAIL || !leaky) + dbfn_write(dbm, key, dbd, sizeof(dbdata_ratelimit)); +dbfn_close(dbm); + +/* Store the result in the tree for future reference, if necessary. */ + +if (anchor != NULL) + { + t = store_get(sizeof(tree_node) + Ustrlen(key)); + t->data.ptr = dbd; + Ustrcpy(t->name, key); + (void)tree_insertnode(anchor, t); + } + +/* We create the formatted version of the sender's rate very late in +order to ensure that it is done using the correct storage pool. */ + +store_pool = old_pool; +sender_rate = string_sprintf("%.1f", dbd->rate); + +HDEBUG(D_acl) + debug_printf("ratelimit computed rate %s\n", sender_rate); + +return rc; +} + + + /************************************************* * Handle conditions/modifiers on an ACL item * *************************************************/ @@ -1649,8 +2427,8 @@ for (; cb != NULL; cb = cb->next) if (cb->type == ACLC_SET) { int n = cb->u.varnumber; - int t = (n < ACL_C_MAX)? 'c' : 'm'; - if (n >= ACL_C_MAX) n -= ACL_C_MAX; + int t = (n < ACL_CVARS)? 'c' : 'm'; + if (n >= ACL_CVARS) n -= ACL_CVARS; debug_printf("acl_%c%d ", t, n); lhswidth += 7; } @@ -1677,6 +2455,10 @@ for (; cb != NULL; cb = cb->next) switch(cb->type) { + case ACLC_ADD_HEADER: + setup_header(arg); + break; + /* A nested ACL that returns "discard" makes sense only for an "accept" or "discard" verb. */ @@ -1697,7 +2479,7 @@ for (; cb != NULL; cb = cb->next) TRUE, NULL); break; -#ifdef EXPERIMENTAL_BRIGHTMAIL + #ifdef EXPERIMENTAL_BRIGHTMAIL case ACLC_BMI_OPTIN: { int old_pool = store_pool; @@ -1706,7 +2488,7 @@ for (; cb != NULL; cb = cb->next) store_pool = old_pool; } break; -#endif + #endif case ACLC_CONDITION: if (Ustrspn(arg, "0123456789") == Ustrlen(arg)) /* Digits, or empty */ @@ -1734,16 +2516,22 @@ for (; cb != NULL; cb = cb->next) switch(control_type) { -#ifdef EXPERIMENTAL_BRIGHTMAIL + case CONTROL_AUTH_UNADVERTISED: + allow_auth_unadvertised = TRUE; + break; + + #ifdef EXPERIMENTAL_BRIGHTMAIL case CONTROL_BMI_RUN: bmi_run = 1; break; -#endif -#ifdef EXPERIMENTAL_DOMAINKEYS + #endif + + #ifdef EXPERIMENTAL_DOMAINKEYS case CONTROL_DK_VERIFY: dk_do_verify = 1; break; -#endif + #endif + case CONTROL_ERROR: return ERROR; @@ -1763,35 +2551,47 @@ for (; cb != NULL; cb = cb->next) smtp_enforce_sync = FALSE; break; -#ifdef WITH_CONTENT_SCAN + #ifdef WITH_CONTENT_SCAN case CONTROL_NO_MBOX_UNSPOOL: no_mbox_unspool = TRUE; break; -#endif + #endif case CONTROL_NO_MULTILINE: no_multiline_responses = TRUE; break; + case CONTROL_FAKEDEFER: case CONTROL_FAKEREJECT: - fake_reject = TRUE; + fake_response = (control_type == CONTROL_FAKEDEFER) ? DEFER : FAIL; if (*p == '/') { uschar *pp = p + 1; while (*pp != 0) pp++; - fake_reject_text = expand_string(string_copyn(p+1, pp-p)); + fake_response_text = expand_string(string_copyn(p+1, pp-p-1)); p = pp; } else { /* Explicitly reset to default string */ - fake_reject_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)."; + 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: @@ -1799,6 +2599,7 @@ for (; cb != NULL; cb = cb->next) break; case CONTROL_SUBMISSION: + originator_name = US""; submission_mode = TRUE; while (*p == '/') { @@ -1812,7 +2613,17 @@ for (; cb != NULL; cb = cb->next) { uschar *pp = p + 8; while (*pp != 0 && *pp != '/') pp++; - submission_domain = string_copyn(p+8, pp-p); + 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; @@ -1823,14 +2634,18 @@ for (; cb != NULL; cb = cb->next) return ERROR; } break; + + case CONTROL_SUPPRESS_LOCAL_FIXUPS: + suppress_local_fixups = TRUE; + break; } break; -#ifdef WITH_CONTENT_SCAN + #ifdef WITH_CONTENT_SCAN case ACLC_DECODE: rc = mime_decode(&arg); break; -#endif + #endif case ACLC_DELAY: { @@ -1850,6 +2665,22 @@ for (; cb != NULL; cb = cb->next) HDEBUG(D_acl) debug_printf("delay skipped in -bh checking mode\n"); } + + /* It appears to be impossible to detect that a TCP/IP connection has + gone away without reading from it. This means that we cannot shorten + the delay below if the client goes away, because we cannot discover + that the client has closed its end of the connection. (The connection + is actually in a half-closed state, waiting for the server to close its + end.) It would be nice to be able to detect this state, so that the + Exim process is not held up unnecessarily. However, it seems that we + can't. The poll() function does not do the right thing, and in any case + it is not always available. + + NOTE: If ever this state of affairs changes, remember that we may be + dealing with stdin/stdout here, in addition to TCP/IP connections. + Whatever is done must work in both cases. To detected the stdin/stdout + case, check for smtp_in or smtp_out being NULL. */ + else { while (delay > 0) delay = sleep(delay); @@ -1858,14 +2689,14 @@ for (; cb != NULL; cb = cb->next) } break; -#ifdef WITH_OLD_DEMIME + #ifdef WITH_OLD_DEMIME case ACLC_DEMIME: rc = demime(&arg); break; -#endif + #endif -#ifdef EXPERIMENTAL_DOMAINKEYS - case ACLC_DK_DOMAIN_SOURCE: + #ifdef EXPERIMENTAL_DOMAINKEYS + case ACLC_DK_DOMAIN_SOURCE: if (dk_verify_block == NULL) { rc = FAIL; break; }; /* check header source of domain against given string */ switch (dk_verify_block->address_source) { @@ -1881,9 +2712,10 @@ for (; cb != NULL; cb = cb->next) rc = match_isinlist(US"none", &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL); break; - } - break; - case ACLC_DK_POLICY: + } + break; + + case ACLC_DK_POLICY: if (dk_verify_block == NULL) { rc = FAIL; break; }; /* check policy against given string, default FAIL */ rc = FAIL; @@ -1893,28 +2725,32 @@ for (; cb != NULL; cb = cb->next) if (dk_verify_block->testing) rc = match_isinlist(US"testing", &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL); - break; - case ACLC_DK_SENDER_DOMAINS: + break; + + case ACLC_DK_SENDER_DOMAINS: if (dk_verify_block == NULL) { rc = FAIL; break; }; if (dk_verify_block->domain != NULL) rc = match_isinlist(dk_verify_block->domain, &arg, 0, &domainlist_anchor, NULL, MCL_DOMAIN, TRUE, NULL); else rc = FAIL; - break; - case ACLC_DK_SENDER_LOCAL_PARTS: + break; + + case ACLC_DK_SENDER_LOCAL_PARTS: if (dk_verify_block == NULL) { rc = FAIL; break; }; if (dk_verify_block->local_part != NULL) rc = match_isinlist(dk_verify_block->local_part, &arg, 0, &localpartlist_anchor, NULL, MCL_LOCALPART, TRUE, NULL); else rc = FAIL; - break; - case ACLC_DK_SENDERS: + break; + + case ACLC_DK_SENDERS: if (dk_verify_block == NULL) { rc = FAIL; break; }; if (dk_verify_block->address != NULL) rc = match_address_list(dk_verify_block->address, TRUE, TRUE, &arg, NULL, -1, 0, NULL); else rc = FAIL; - break; - case ACLC_DK_STATUS: + break; + + case ACLC_DK_STATUS: if (dk_verify_block == NULL) { rc = FAIL; break; }; if (dk_verify_block->result > 0) { switch(dk_verify_block->result) { @@ -1946,10 +2782,10 @@ for (; cb != NULL; cb = cb->next) rc = match_isinlist(US"bad", &arg, 0, NULL, NULL, MCL_STRING, TRUE, NULL); break; + } } - } - break; -#endif + break; + #endif case ACLC_DNSLISTS: rc = verify_check_dnsbl(&arg); @@ -2031,7 +2867,7 @@ for (; cb != NULL; cb = cb->next) } break; -#ifdef WITH_CONTENT_SCAN + #ifdef WITH_CONTENT_SCAN case ACLC_MALWARE: { /* Seperate the regular expression and any optional parameters. */ @@ -2051,20 +2887,24 @@ for (; cb != NULL; cb = cb->next) break; case ACLC_MIME_REGEX: - rc = mime_regex(&arg); + rc = mime_regex(&arg); + break; + #endif + + case ACLC_RATELIMIT: + rc = acl_ratelimit(arg, where, log_msgptr); break; -#endif case ACLC_RECIPIENTS: rc = match_address_list(addr->address, TRUE, TRUE, &arg, NULL, -1, 0, &recipient_data); break; -#ifdef WITH_CONTENT_SCAN - case ACLC_REGEX: - rc = regex(&arg); + #ifdef WITH_CONTENT_SCAN + case ACLC_REGEX: + rc = regex(&arg); break; -#endif + #endif case ACLC_SENDER_DOMAINS: { @@ -2086,13 +2926,13 @@ for (; cb != NULL; cb = cb->next) case ACLC_SET: { int old_pool = store_pool; - if (cb->u.varnumber < ACL_C_MAX) store_pool = POOL_PERM; + if (cb->u.varnumber < ACL_CVARS) store_pool = POOL_PERM; acl_var[cb->u.varnumber] = string_copy(arg); store_pool = old_pool; } break; -#ifdef WITH_CONTENT_SCAN + #ifdef WITH_CONTENT_SCAN case ACLC_SPAM: { /* Seperate the regular expression and any optional parameters. */ @@ -2110,20 +2950,23 @@ for (; cb != NULL; cb = cb->next) } } break; -#endif + #endif -#ifdef EXPERIMENTAL_SPF + #ifdef EXPERIMENTAL_SPF case ACLC_SPF: rc = spf_process(&arg, sender_address); break; -#endif + #endif /* 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. */ + only from explicit "message" modifiers. However, put the user message into + $acl_verify_message so it can be used in subsequent conditions or modifiers + (until something changes it). */ case ACLC_VERIFY: rc = acl_verify(where, addr, arg, user_msgptr, log_msgptr, basic_errno); + acl_verify_message = *user_msgptr; if (verb == ACL_WARN) *user_msgptr = NULL; break; @@ -2451,7 +3294,7 @@ if (Ustrchr(ss, ' ') == NULL) return ERROR; } acl_text[statbuf.st_size] = 0; - close(fd); + (void)close(fd); acl_name = string_sprintf("ACL \"%s\"", ss); HDEBUG(D_acl) debug_printf("read ACL from file %s\n", ss); @@ -2602,10 +3445,11 @@ while (acl != NULL) case ACL_WARN: if (cond == OK) acl_warn(where, *user_msgptr, *log_msgptr); - else if (cond == DEFER) - acl_warn(where, NULL, string_sprintf("ACL \"warn\" statement skipped: " - "condition test deferred: %s", - (*log_msgptr == NULL)? US"" : *log_msgptr)); + else if (cond == DEFER && (log_extra_selector & LX_acl_warn_skipped) != 0) + log_write(0, LOG_MAIN, "%s Warning: ACL \"warn\" statement skipped: " + "condition test deferred%s%s", host_and_ident(TRUE), + (*log_msgptr == NULL)? US"" : US": ", + (*log_msgptr == NULL)? US"" : *log_msgptr); *log_msgptr = *user_msgptr = NULL; /* In case implicit DENY follows */ break; @@ -2637,7 +3481,7 @@ acl_check_internal() to do the actual work. Arguments: where ACL_WHERE_xxxx indicating where called from - data_string RCPT address, or SMTP command argument, or NULL + recipient RCPT address for RCPT check, else NULL s the input string; NULL is the same as an empty ACL => DENY user_msgptr where to put a user error (for SMTP response) log_msgptr where to put a logging message (not for SMTP response) @@ -2651,21 +3495,22 @@ Returns: OK access is granted by an ACCEPT verb */ int -acl_check(int where, uschar *data_string, uschar *s, uschar **user_msgptr, +acl_check(int where, uschar *recipient, uschar *s, uschar **user_msgptr, uschar **log_msgptr) { int rc; address_item adb; -address_item *addr; +address_item *addr = NULL; *user_msgptr = *log_msgptr = NULL; sender_verified_failed = NULL; +ratelimiters_cmd = NULL; if (where == ACL_WHERE_RCPT) { adb = address_defaults; addr = &adb; - addr->address = data_string; + addr->address = recipient; if (deliver_split_address(addr) == DEFER) { *log_msgptr = US"defer in percent_hack_domains check"; @@ -2674,16 +3519,11 @@ if (where == ACL_WHERE_RCPT) deliver_domain = addr->domain; deliver_localpart = addr->local_part; } -else - { - addr = NULL; - smtp_command_argument = data_string; - } rc = acl_check_internal(where, addr, s, 0, user_msgptr, log_msgptr); -smtp_command_argument = deliver_domain = - deliver_localpart = deliver_address_data = sender_address_data = NULL; +deliver_domain = deliver_localpart = deliver_address_data = + sender_address_data = NULL; /* A DISCARD response is permitted only for message ACLs, excluding the PREDATA ACL, which is really in the middle of an SMTP command. */