X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/8e669ac162fe3b1040297f1d021de10778dce9d9..f68fe5f62128effcce35efca90d74bc6df066765:/src/src/acl.c diff --git a/src/src/acl.c b/src/src/acl.c index 7b176b690..c1eebf655 100644 --- a/src/src/acl.c +++ b/src/src/acl.c @@ -1,10 +1,8 @@ -/* $Cambridge: exim/src/src/acl.c,v 1.19 2005/02/17 11:58:25 ph10 Exp $ */ - /************************************************* * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) University of Cambridge 1995 - 2005 */ +/* Copyright (c) University of Cambridge 1995 - 2012 */ /* See the file NOTICE for conditions of use and distribution. */ /* Code for handling Access Control Lists (ACLs) */ @@ -27,18 +25,37 @@ static uschar *verbs[] = { US"accept", US"defer", US"deny", US"discard", US"drop", US"require", US"warn" }; -/* For each verb, the condition for which "message" is used */ - -static int msgcond[] = { FAIL, OK, OK, FAIL, OK, FAIL, OK }; +/* For each verb, the conditions for which "message" or "log_message" are used +are held as a bitmap. This is to avoid expanding the strings unnecessarily. For +"accept", the FAIL case is used only after "endpass", but that is selected in +the code. */ + +static int msgcond[] = { + (1<u.varnumber = s[5] - '0'; - if (s[4] == 'm') cond->u.varnumber += ACL_C_MAX; - s += 6; + while (*endptr != 0 && *endptr != '=' && !isspace(*endptr)) + { + if (!isalnum(*endptr) && *endptr != '_') + { + *error = string_sprintf("invalid character \"%c\" in variable name " + "in ACL modifier \"set %s\"", *endptr, s); + return NULL; + } + endptr++; + } + + cond->u.varname = string_copyn(s + 4, endptr - s - 4); + s = endptr; while (isspace(*s)) s++; } @@ -669,6 +939,181 @@ 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); + +/* Ignore any leading newlines */ +while (*hstring == '\n') hstring++, hlen--; + +/* An empty string does nothing; ensure exactly one final newline. */ +if (hlen <= 0) return; +if (hstring[--hlen] != '\n') hstring = string_sprintf("%s\n", hstring); +else while(hstring[--hlen] == '\n') hstring[hlen+1] = '\0'; + +/* 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: ", (int) (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; + } +} + + + +/************************************************* +* 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(uschar *hnames) +{ +if (*hnames != 0) + { + if (acl_removed_headers == NULL) + acl_removed_headers = hnames; + else + acl_removed_headers = string_sprintf("%s : %s", acl_removed_headers, hnames); + } +} + + + /************************************************* * Handle warnings * *************************************************/ @@ -677,6 +1122,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 @@ -688,8 +1136,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; @@ -706,8 +1152,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; @@ -739,106 +1185,17 @@ 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; - - /* Add a final newline if not present */ +/* 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. */ - text = ((user_message)[hlen-1] == '\n')? user_message : - string_sprintf("%s\n", user_message); +setup_header(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; - } - } -} - - - -/************************************************* -* Verify and check reverse DNS * -*************************************************/ +/************************************************* +* Verify and check reverse DNS * +*************************************************/ /* Called from acl_verify() below. We look up the host name(s) of the client IP address if this has not yet been done. The host_name_lookup() function checks @@ -894,10 +1251,362 @@ 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) * *************************************************/ +enum { VERIFY_REV_HOST_LKUP, VERIFY_CERT, VERIFY_HELO, VERIFY_CSA, VERIFY_HDR_SYNTAX, + VERIFY_NOT_BLIND, VERIFY_HDR_SNDR, VERIFY_SNDR, VERIFY_RCPT + }; +typedef struct { + uschar * name; + int value; + unsigned where_allowed; /* bitmap */ + BOOL no_options; /* Never has /option(s) following */ + 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"certificate", VERIFY_CERT, ~0, TRUE, 0 }, + { US"helo", VERIFY_HELO, ~0, TRUE, 0 }, + { US"csa", VERIFY_CSA, ~0, FALSE, 0 }, + { US"header_syntax", VERIFY_HDR_SYNTAX, (1<alt_opt_sep ? strncmpic(ss, vp->name, vp->alt_opt_sep) == 0 + : strcmpic (ss, vp->name) == 0) + break; +if ((char *)vp >= (char *)verify_type_list + sizeof(verify_type_list)) + goto BAD_VERIFY; + +if (vp->no_options && slash != NULL) { - if (sender_host_address == NULL) return OK; - return acl_verify_reverse(user_msgptr, log_msgptr); + *log_msgptr = string_sprintf("unexpected '/' found in \"%s\" " + "(this verify item has no options)", arg); + return ERROR; } - -/* 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 (strcmpic(ss, US"certificate") == 0) +if (!(vp->where_allowed & (1<name, acl_wherenames[where]); + return ERROR; } +switch(vp->value) + { + case VERIFY_REV_HOST_LKUP: + if (sender_host_address == NULL) return OK; + return acl_verify_reverse(user_msgptr, log_msgptr); + + 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.) */ -/* We can test the result of optional HELO verification */ + if (tls_in.certificate_verified) return OK; + *user_msgptr = US"no verified certificate"; + return FAIL; -if (strcmpic(ss, US"helo") == 0) return helo_verified? OK : FAIL; + case VERIFY_HELO: + /* We can test the result of optional HELO verification that might have + occurred earlier. If not, we can attempt the verification now. */ -/* Handle header verification options - permitted only after DATA or a non-SMTP -message. */ + if (!helo_verified && !helo_verify_failed) smtp_verify_helo(); + return helo_verified? OK : FAIL; -if (strncmpic(ss, US"header_", 7) == 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; - } + case VERIFY_CSA: + /* Do Client SMTP Authorization checks in a separate function, and turn the + result code into user-friendly strings. */ - /* 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). */ + 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]; - if (strcmpic(ss+7, US"syntax") == 0) - { - int rc = verify_check_headers(log_msgptr); + case VERIFY_HDR_SYNTAX: + /* 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). */ + + 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 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 if (strcmpic(ss+7, US"sender") == 0) verify_header_sender = TRUE; + 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:. */ - /* Unknown verify argument starting with "header_" */ + rc = verify_check_notblind(); + if (rc != OK) + { + *log_msgptr = string_sprintf("bcc recipient detected"); + if (smtp_return_error_details) + *user_msgptr = string_sprintf("Rejected after DATA: %s", *log_msgptr); + } + return rc; - else goto BAD_VERIFY; - } + /* 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. */ -/* Otherwise, first item in verify argument must be "sender" or "recipient". -In the case of a sender, this can optionally be followed by an address to use -in place of the actual sender (rare special-case requirement). */ + case VERIFY_HDR_SNDR: + verify_header_sender = TRUE; + break; -else if (strncmpic(ss, US"sender", 6) == 0) - { - uschar *s = ss + 6; - if (where > ACL_WHERE_NOTSMTP) + case VERIFY_SNDR: + /* In the case of a sender, this can optionally be followed by an address to use + in place of the actual sender (rare special-case requirement). */ { - *log_msgptr = string_sprintf("cannot verify sender in ACL for %s " - "(only possible for MAIL, RCPT, PREDATA, or DATA)", - acl_wherenames[where]); - return ERROR; - } - if (*s == 0) - verify_sender_address = sender_address; - else - { - while (isspace(*s)) s++; - if (*s++ != '=') goto BAD_VERIFY; - while (isspace(*s)) s++; - verify_sender_address = string_copy(s); - } - } -else - { - if (strcmpic(ss, US"recipient") != 0) goto BAD_VERIFY; - if (addr == NULL) - { - *log_msgptr = string_sprintf("cannot verify recipient in ACL for %s " - "(only possible for RCPT)", acl_wherenames[where]); - return ERROR; + uschar *s = ss + 6; + if (*s == 0) + verify_sender_address = sender_address; + else + { + while (isspace(*s)) s++; + if (*s++ != '=') goto BAD_VERIFY; + while (isspace(*s)) s++; + verify_sender_address = string_copy(s); + } } + break; + + case VERIFY_RCPT: + break; } -/* 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 */ @@ -1072,107 +1808,60 @@ while ((ss = string_nextinlist(&list, &sep, big_buffer, big_buffer_size)) uschar buffer[256]; while (isspace(*ss)) ss++; - /* This callout option handling code has become a mess as new options - have been added in an ad hoc manner. It should be tidied up into some - kind of table-driven thing. */ - while ((opt = string_nextinlist(&ss, &optsep, buffer, sizeof(buffer))) != NULL) { - if (strcmpic(opt, US"defer_ok") == 0) callout_defer_ok = TRUE; - else if (strcmpic(opt, US"no_cache") == 0) - verify_options |= vopt_callout_no_cache; - else if (strcmpic(opt, US"random") == 0) - verify_options |= vopt_callout_random; - else if (strcmpic(opt, US"use_sender") == 0) - verify_options |= vopt_callout_recipsender; - 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 (strncmpic(opt, US"mailfrom", 8) == 0) - { - if (!verify_header_sender) - { - *log_msgptr = string_sprintf("\"mailfrom\" is allowed as a " - "callout option only for verify=header_sender (detected in ACL " - "condition \"%s\")", arg); - return ERROR; - } - opt += 8; - while (isspace(*opt)) opt++; - if (*opt++ != '=') - { - *log_msgptr = string_sprintf("'=' expected after " - "\"mailfrom\" in ACL condition \"%s\"", arg); - return ERROR; - } - while (isspace(*opt)) opt++; - se_mailfrom = string_copy(opt); - } + callout_opt_t * op; + double period = 1.0F; - else if (strncmpic(opt, US"postmaster_mailfrom", 19) == 0) - { - opt += 19; - while (isspace(*opt)) opt++; - if (*opt++ != '=') - { - *log_msgptr = string_sprintf("'=' expected after " - "\"postmaster_mailfrom\" in ACL condition \"%s\"", arg); - return ERROR; - } - while (isspace(*opt)) opt++; - pm_mailfrom = string_copy(opt); - } + for (op= callout_opt_list; op->name; op++) + if (strncmpic(opt, op->name, Ustrlen(op->name)) == 0) + break; - else if (strncmpic(opt, US"maxwait", 7) == 0) - { - opt += 7; - while (isspace(*opt)) opt++; - if (*opt++ != '=') - { - *log_msgptr = string_sprintf("'=' expected after \"maxwait\" in " - "ACL condition \"%s\"", arg); - return ERROR; - } - while (isspace(*opt)) opt++; - callout_overall = readconf_readtime(opt, 0, FALSE); - if (callout_overall < 0) - { - *log_msgptr = string_sprintf("bad time value in ACL condition " - "\"verify %s\"", arg); - return ERROR; - } - } - else if (strncmpic(opt, US"connect", 7) == 0) - { - opt += 7; + verify_options |= op->flag; + if (op->has_option) + { + opt += Ustrlen(op->name); while (isspace(*opt)) opt++; if (*opt++ != '=') { *log_msgptr = string_sprintf("'=' expected after " - "\"callout_overaall\" in ACL condition \"%s\"", arg); + "\"%s\" in ACL verify condition \"%s\"", op->name, arg); return ERROR; } while (isspace(*opt)) opt++; - callout_connect = readconf_readtime(opt, 0, FALSE); - if (callout_connect < 0) + } + if (op->timeval) + { + period = readconf_readtime(opt, 0, FALSE); + if (period < 0) { *log_msgptr = string_sprintf("bad time value in ACL condition " "\"verify %s\"", arg); return ERROR; } - } - else /* Plain time is callout connect/command timeout */ - { - callout = readconf_readtime(opt, 0, FALSE); - if (callout < 0) - { - *log_msgptr = string_sprintf("bad time value in ACL condition " - "\"verify %s\"", arg); - return ERROR; - } - } + } + + switch(op->value) + { + case CALLOUT_DEFER_OK: callout_defer_ok = TRUE; break; + case CALLOUT_POSTMASTER: pm_mailfrom = US""; break; + case CALLOUT_FULLPOSTMASTER: pm_mailfrom = US""; break; + case CALLOUT_MAILFROM: + if (!verify_header_sender) + { + *log_msgptr = string_sprintf("\"mailfrom\" is allowed as a " + "callout option only for verify=header_sender (detected in ACL " + "condition \"%s\")", arg); + return ERROR; + } + se_mailfrom = string_copy(opt); + break; + case CALLOUT_POSTMASTER_MAILFROM: pm_mailfrom = string_copy(opt); break; + case CALLOUT_MAXWAIT: callout_overall = period; break; + case CALLOUT_CONNECT: callout_connect = period; break; + case CALLOUT_TIME: callout = period; break; + } } } else @@ -1293,6 +1982,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. */ @@ -1344,6 +2036,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. */ @@ -1352,10 +2047,13 @@ else callout_overall, callout_connect, se_mailfrom, pm_mailfrom, NULL); HDEBUG(D_acl) debug_printf("----------- end verify ------------\n"); + *basic_errno = addr2.basic_errno; *log_msgptr = addr2.message; *user_msgptr = (addr2.user_message != NULL)? addr2.user_message : addr2.message; - *basic_errno = addr2.basic_errno; + + /* Allow details for temporary error if the address is so flagged. */ + if (testflag((&addr2), af_pass_message)) acl_temp_details = TRUE; /* Make $address_data visible */ deliver_address_data = addr2.p.address_data; @@ -1408,7 +2106,8 @@ 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; } @@ -1452,8 +2151,577 @@ if (d >= controls_list + sizeof(controls_list)/sizeof(control_def) || return CONTROL_ERROR; } -*pptr = arg + len; -return d->value; +*pptr = arg + len; +return d->value; +} + + + + +/************************************************* +* Return a ratelimit error * +*************************************************/ + +/* Called from acl_ratelimit() below + +Arguments: + log_msgptr for error messages + format format string + ... supplementary arguments + ss ratelimit option name + where ACL_WHERE_xxxx indicating which ACL this is + +Returns: ERROR +*/ + +static int +ratelimit_error(uschar **log_msgptr, const char *format, ...) +{ +va_list ap; +uschar buffer[STRING_SPRINTF_BUFFER_SIZE]; +va_start(ap, format); +if (!string_vformat(buffer, sizeof(buffer), format, ap)) + log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "string_sprintf expansion was longer than " SIZE_T_FMT, sizeof(buffer)); +va_end(ap); +*log_msgptr = string_sprintf( + "error in arguments to \"ratelimit\" condition: %s", buffer); +return ERROR; +} + + + + +/************************************************* +* 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, count; +uschar *ss; +uschar *key = NULL; +uschar *unique = NULL; +int sep = '/'; +BOOL leaky = FALSE, strict = FALSE, readonly = FALSE; +BOOL noupdate = FALSE, badacl = FALSE; +int mode = RATE_PER_WHAT; +int old_pool, rc; +tree_node **anchor, *t; +open_db dbblock, *dbm; +int dbdb_size; +dbdata_ratelimit *dbd; +dbdata_ratelimit_unique *dbdb; +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 / 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') + return ratelimit_error(log_msgptr, + "\"%s\" is not a positive number", sender_rate_limit); + +/* Second is the rate measurement period / 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) + return ratelimit_error(log_msgptr, + "\"%s\" is not a time value", sender_rate_period); + +/* By default we are counting one of something, but the per_rcpt, +per_byte, and count options can change this. */ + +count = 1.0; + +/* Parse the other options. */ + +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"noupdate") == 0) noupdate = TRUE; + else if (strcmpic(ss, US"readonly") == 0) readonly = TRUE; + else if (strcmpic(ss, US"per_cmd") == 0) RATE_SET(mode, PER_CMD); + else if (strcmpic(ss, US"per_conn") == 0) + { + RATE_SET(mode, PER_CONN); + if (where == ACL_WHERE_NOTSMTP || where == ACL_WHERE_NOTSMTP_START) + badacl = TRUE; + } + else if (strcmpic(ss, US"per_mail") == 0) + { + RATE_SET(mode, PER_MAIL); + if (where > ACL_WHERE_NOTSMTP) badacl = TRUE; + } + else if (strcmpic(ss, US"per_rcpt") == 0) + { + /* If we are running in the RCPT ACL, then we'll count the recipients + one by one, but if we are running when we have accumulated the whole + list then we'll add them all in one batch. */ + if (where == ACL_WHERE_RCPT) + RATE_SET(mode, PER_RCPT); + else if (where >= ACL_WHERE_PREDATA && where <= ACL_WHERE_NOTSMTP) + RATE_SET(mode, PER_ALLRCPTS), count = (double)recipients_count; + else if (where == ACL_WHERE_MAIL || where > ACL_WHERE_NOTSMTP) + RATE_SET(mode, PER_RCPT), badacl = TRUE; + } + else if (strcmpic(ss, US"per_byte") == 0) + { + /* If we have not yet received the message data and there was no SIZE + declaration on the MAIL comand, then it's safe to just use a value of + zero and let the recorded rate decay as if nothing happened. */ + RATE_SET(mode, PER_MAIL); + if (where > ACL_WHERE_NOTSMTP) badacl = TRUE; + else count = message_size < 0 ? 0.0 : (double)message_size; + } + else if (strcmpic(ss, US"per_addr") == 0) + { + RATE_SET(mode, PER_RCPT); + if (where != ACL_WHERE_RCPT) badacl = TRUE, unique = US"*"; + else unique = string_sprintf("%s@%s", deliver_localpart, deliver_domain); + } + else if (strncmpic(ss, US"count=", 6) == 0) + { + uschar *e; + count = Ustrtod(ss+6, &e); + if (count < 0.0 || *e != '\0') + return ratelimit_error(log_msgptr, + "\"%s\" is not a positive number", ss); + } + else if (strncmpic(ss, US"unique=", 7) == 0) + unique = string_copy(ss + 7); + else if (key == NULL) + key = string_copy(ss); + else + key = string_sprintf("%s/%s", key, ss); + } + +/* Sanity check. When the badacl flag is set the update mode must either +be readonly (which is the default if it is omitted) or, for backwards +compatibility, a combination of noupdate and strict or leaky. */ + +if (mode == RATE_PER_CLASH) + return ratelimit_error(log_msgptr, "conflicting per_* options"); +if (leaky + strict + readonly > 1) + return ratelimit_error(log_msgptr, "conflicting update modes"); +if (badacl && (leaky || strict) && !noupdate) + return ratelimit_error(log_msgptr, + "\"%s\" must not have /leaky or /strict option in %s ACL", + ratelimit_option_string[mode], acl_wherenames[where]); + +/* Set the default values of any unset options. In readonly mode we +perform the rate computation without any increment so that its value +decays to eventually allow over-limit senders through. */ + +if (noupdate) readonly = TRUE, leaky = strict = FALSE; +if (badacl) readonly = TRUE; +if (readonly) count = 0.0; +if (!strict && !readonly) leaky = TRUE; +if (mode == RATE_PER_WHAT) mode = RATE_PER_MAIL; + +/* Create the lookup key. If there is no explicit key, use sender_host_address. +If there is no sender_host_address (e.g. -bs or acl_not_smtp) then we simply +omit it. The smoothing constant (sender_rate_period) and the per_xxx options +are added to the key because they alter the meaning of the stored data. */ + +if (key == NULL) + key = (sender_host_address == NULL)? US"" : sender_host_address; + +key = string_sprintf("%s/%s/%s%s", + sender_rate_period, + ratelimit_option_string[mode], + unique == NULL ? "" : "unique/", + key); + +HDEBUG(D_acl) + debug_printf("ratelimit condition count=%.0f %.1f/%s\n", count, limit, 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. In readonly mode we only remember the +result for the rest of this command in case a later command changes it. After +this bit of logic the code is independent of the per_* mode. */ + +old_pool = store_pool; + +if (readonly) + anchor = &ratelimiters_cmd; +else switch(mode) { +case RATE_PER_CONN: + anchor = &ratelimiters_conn; + store_pool = POOL_PERM; + break; +case RATE_PER_BYTE: +case RATE_PER_MAIL: +case RATE_PER_ALLRCPTS: + anchor = &ratelimiters_mail; + break; +case RATE_PER_ADDR: +case RATE_PER_CMD: +case RATE_PER_RCPT: + anchor = &ratelimiters_cmd; + break; +default: + anchor = NULL; /* silence an "unused" complaint */ + log_write(0, LOG_MAIN|LOG_PANIC_DIE, + "internal ACL error: unknown ratelimit mode %d", mode); + break; +} + +t = tree_search(*anchor, key); +if (t != NULL) + { + dbd = t->data.ptr; + /* The following few lines duplicate some of the code below. */ + rc = (dbd->rate < limit)? FAIL : 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, which will be updated and written back if required. */ + +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; + } +dbdb = dbfn_read_with_length(dbm, key, &dbdb_size); +dbd = NULL; + +gettimeofday(&tv, NULL); + +if (dbdb != NULL) + { + /* Locate the basic ratelimit block inside the DB data. */ + HDEBUG(D_acl) debug_printf("ratelimit found key in database\n"); + dbd = &dbdb->dbd; + + /* Forget the old Bloom filter if it is too old, so that we count each + repeating event once per period. We don't simply clear and re-use the old + filter because we want its size to change if the limit changes. Note that + we keep the dbd pointer for copying the rate into the new data block. */ + + if(unique != NULL && tv.tv_sec > dbdb->bloom_epoch + period) + { + HDEBUG(D_acl) debug_printf("ratelimit discarding old Bloom filter\n"); + dbdb = NULL; + } + + /* Sanity check. */ + + if(unique != NULL && dbdb_size < sizeof(*dbdb)) + { + HDEBUG(D_acl) debug_printf("ratelimit discarding undersize Bloom filter\n"); + dbdb = NULL; + } + } + +/* Allocate a new data block if the database lookup failed +or the Bloom filter passed its age limit. */ + +if (dbdb == NULL) + { + if (unique == NULL) + { + /* No Bloom filter. This basic ratelimit block is initialized below. */ + HDEBUG(D_acl) debug_printf("ratelimit creating new rate data block\n"); + dbdb_size = sizeof(*dbd); + dbdb = store_get(dbdb_size); + } + else + { + int extra; + HDEBUG(D_acl) debug_printf("ratelimit creating new Bloom filter\n"); + + /* See the long comment below for an explanation of the magic number 2. + The filter has a minimum size in case the rate limit is very small; + this is determined by the definition of dbdata_ratelimit_unique. */ + + extra = (int)limit * 2 - sizeof(dbdb->bloom); + if (extra < 0) extra = 0; + dbdb_size = sizeof(*dbdb) + extra; + dbdb = store_get(dbdb_size); + dbdb->bloom_epoch = tv.tv_sec; + dbdb->bloom_size = sizeof(dbdb->bloom) + extra; + memset(dbdb->bloom, 0, dbdb->bloom_size); + + /* Preserve any basic ratelimit data (which is our longer-term memory) + by copying it from the discarded block. */ + + if (dbd != NULL) + { + dbdb->dbd = *dbd; + dbd = &dbdb->dbd; + } + } + } + +/* If we are counting unique events, find out if this event is new or not. +If the client repeats the event during the current period then it should be +counted. We skip this code in readonly mode for efficiency, because any +changes to the filter will be discarded and because count is already set to +zero. */ + +if (unique != NULL && !readonly) + { + /* We identify unique events using a Bloom filter. (You can find my + notes on Bloom filters at http://fanf.livejournal.com/81696.html) + With the per_addr option, an "event" is a recipient address, though the + user can use the unique option to define their own events. We only count + an event if we have not seen it before. + + We size the filter according to the rate limit, which (in leaky mode) + is the limit on the population of the filter. We allow 16 bits of space + per entry (see the construction code above) and we set (up to) 8 of them + when inserting an element (see the loop below). The probability of a false + positive (an event we have not seen before but which we fail to count) is + + size = limit * 16 + numhash = 8 + allzero = exp(-numhash * pop / size) + = exp(-0.5 * pop / limit) + fpr = pow(1 - allzero, numhash) + + For senders at the limit the fpr is 0.06% or 1 in 1700 + and for senders at half the limit it is 0.0006% or 1 in 170000 + + In strict mode the Bloom filter can fill up beyond the normal limit, in + which case the false positive rate will rise. This means that the + measured rate for very fast senders can bogusly drop off after a while. + + At twice the limit, the fpr is 2.5% or 1 in 40 + At four times the limit, it is 31% or 1 in 3.2 + + It takes ln(pop/limit) periods for an over-limit burst of pop events to + decay below the limit, and if this is more than one then the Bloom filter + will be discarded before the decay gets that far. The false positive rate + at this threshold is 9.3% or 1 in 10.7. */ + + BOOL seen; + unsigned n, hash, hinc; + uschar md5sum[16]; + md5 md5info; + + /* Instead of using eight independent hash values, we combine two values + using the formula h1 + n * h2. This does not harm the Bloom filter's + performance, and means the amount of hash we need is independent of the + number of bits we set in the filter. */ + + md5_start(&md5info); + md5_end(&md5info, unique, Ustrlen(unique), md5sum); + hash = md5sum[0] | md5sum[1] << 8 | md5sum[2] << 16 | md5sum[3] << 24; + hinc = md5sum[4] | md5sum[5] << 8 | md5sum[6] << 16 | md5sum[7] << 24; + + /* Scan the bits corresponding to this event. A zero bit means we have + not seen it before. Ensure all bits are set to record this event. */ + + HDEBUG(D_acl) debug_printf("ratelimit checking uniqueness of %s\n", unique); + + seen = TRUE; + for (n = 0; n < 8; n++, hash += hinc) + { + int bit = 1 << (hash % 8); + int byte = (hash / 8) % dbdb->bloom_size; + if ((dbdb->bloom[byte] & bit) == 0) + { + dbdb->bloom[byte] |= bit; + seen = FALSE; + } + } + + /* If this event has occurred before, do not count it. */ + + if (seen) + { + HDEBUG(D_acl) debug_printf("ratelimit event found in Bloom filter\n"); + count = 0.0; + } + else + HDEBUG(D_acl) debug_printf("ratelimit event added to Bloom filter\n"); + } + +/* If there was no previous ratelimit data block for this key, initialize +the new one, otherwise update the block from the database. The initial rate +is what would be computed by the code below for an infinite interval. */ + +if (dbd == NULL) + { + HDEBUG(D_acl) debug_printf("ratelimit initializing new key's rate data\n"); + dbd = &dbdb->dbd; + dbd->time_stamp = tv.tv_sec; + dbd->time_usec = tv.tv_usec; + dbd->rate = count; + } +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); + + /* Combine the instantaneous rate (period / interval) with the previous rate + using the smoothing factor a. In order to measure sized events, multiply the + instantaneous rate by the count of bytes or recipients etc. */ + + dbd->time_stamp = tv.tv_sec; + dbd->time_usec = tv.tv_usec; + dbd->rate = (1 - a) * count / i_over_p + a * dbd->rate; + + /* When events are very widely spaced the computed rate tends towards zero. + Although this is accurate it turns out not to be useful for our purposes, + especially when the first event after a long silence is the start of a spam + run. A more useful model is that the rate for an isolated event should be the + size of the event per the period size, ignoring the lack of events outside + the current period and regardless of where the event falls in the period. So, + if the interval was so long that the calculated rate is unhelpfully small, we + re-intialize the rate. In the absence of higher-rate bursts, the condition + below is true if the interval is greater than the period. */ + + if (dbd->rate < count) dbd->rate = count; + } + +/* Clients sending at the limit are considered to be over the limit. +This matters for edge cases such as a limit of zero, when the client +should be completely blocked. */ + +rc = (dbd->rate < limit)? FAIL : 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 readonly is set, +neither leaky nor strict are set, so we do not do any updates. */ + +if ((rc == FAIL && leaky) || strict) + { + dbfn_write(dbm, key, dbdb, dbdb_size); + HDEBUG(D_acl) debug_printf("ratelimit db updated\n"); + } +else + { + HDEBUG(D_acl) debug_printf("ratelimit db not updated: %s\n", + readonly? "readonly mode" : "over the limit, but leaky"); + } + +dbfn_close(dbm); + +/* Store the result in the tree for future reference. */ + +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; } @@ -1494,7 +2762,9 @@ acl_check_condition(int verb, acl_condition_block *cb, int where, { uschar *user_message = NULL; uschar *log_message = NULL; -uschar *p; +uschar *debug_tag = NULL; +uschar *debug_opts = NULL; +uschar *p = NULL; int rc = OK; #ifdef WITH_CONTENT_SCAN int sep = '/'; @@ -1557,11 +2827,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; - debug_printf("acl_%c%d ", t, n); - lhswidth += 7; + debug_printf("acl_%s ", cb->u.varname); + lhswidth += 5 + Ustrlen(cb->u.varname); } debug_printf("= %s\n", cb->arg); @@ -1586,18 +2853,22 @@ 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. */ 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: @@ -1606,7 +2877,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; @@ -1615,9 +2886,12 @@ for (; cb != NULL; cb = cb->next) store_pool = old_pool; } break; -#endif + #endif case ACLC_CONDITION: + /* 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 */ rc = (Uatoi(arg) == 0)? FAIL : OK; else @@ -1629,6 +2903,9 @@ for (; cb != NULL; cb = cb->next) *log_msgptr = string_sprintf("invalid \"condition\" value \"%s\"", arg); break; + case ACLC_CONTINUE: /* Always succeeds */ + break; + case ACLC_CONTROL: control_type = decode_control(arg, &p, where, log_msgptr); @@ -1643,11 +2920,61 @@ 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 + #endif + + #ifndef DISABLE_DKIM + case CONTROL_DKIM_VERIFY: + dkim_disable_verify = 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; @@ -1668,35 +2995,59 @@ 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_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_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: @@ -1704,6 +3055,7 @@ for (; cb != NULL; cb = cb->next) break; case CONTROL_SUBMISSION: + originator_name = US""; submission_mode = TRUE; while (*p == '/') { @@ -1717,7 +3069,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; @@ -1728,14 +3090,73 @@ for (; cb != NULL; cb = cb->next) 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; + break; + + case CONTROL_CUTTHROUGH_DELIVERY: + if (deliver_freeze) + { + *log_msgptr = string_sprintf("\"control=%s\" on frozen item", arg); + return ERROR; + } + if (queue_only_policy) + { + *log_msgptr = string_sprintf("\"control=%s\" on queue-only item", arg); + return ERROR; + } + cutthrough_delivery = TRUE; + break; } break; -#ifdef WITH_CONTENT_SCAN + #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); + /* 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)) + != NULL) { + if (strcmpic(ss, US"defer_ok") == 0 && rc == DEFER) + { + /* FAIL so that the message is passed to the next ACL */ + rc = FAIL; + } + } + } + break; + #endif + + #ifdef WITH_CONTENT_SCAN case ACLC_DECODE: rc = mime_decode(&arg); break; -#endif + #endif case ACLC_DELAY: { @@ -1755,19 +3176,55 @@ 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 1: If ever this state of affairs changes, remember that we may be + dealing with stdin/stdout here, in addition to TCP/IP connections. + Also, delays may be specified for non-SMTP input, where smtp_out and + smtp_in will be NULL. Whatever is done must work in all cases. + + NOTE 2: The added feature of flushing the output before a delay must + apply only to SMTP input. Hence the test for smtp_out being non-NULL. + */ + else { + if (smtp_out != NULL && !disable_delay_flush) mac_smtp_fflush(); while (delay > 0) delay = sleep(delay); } } } break; -#ifdef WITH_OLD_DEMIME + #ifdef WITH_OLD_DEMIME case ACLC_DEMIME: rc = demime(&arg); break; -#endif + #endif + + #ifndef DISABLE_DKIM + case ACLC_DKIM_SIGNER: + if (dkim_cur_signer != NULL) + rc = match_isinlist(dkim_cur_signer, + &arg,0,NULL,NULL,MCL_STRING,TRUE,NULL); + else + rc = FAIL; + break; + + case ACLC_DKIM_STATUS: + rc = match_isinlist(dkim_exim_expand_query(DKIM_VERIFY_STATUS), + &arg,0,NULL,NULL,MCL_STRING,TRUE,NULL); + break; + #endif case ACLC_DNSLISTS: rc = verify_check_dnsbl(&arg); @@ -1785,11 +3242,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; @@ -1818,6 +3275,29 @@ for (; cb != NULL; cb = cb->next) &deliver_localpart_data); break; + case ACLC_LOG_REJECT_TARGET: + { + int logbits = 0; + int sep = 0; + uschar *s = arg; + uschar *ss; + while ((ss = string_nextinlist(&s, &sep, big_buffer, big_buffer_size)) + != NULL) + { + if (Ustrcmp(ss, "main") == 0) logbits |= LOG_MAIN; + else if (Ustrcmp(ss, "panic") == 0) logbits |= LOG_PANIC; + else if (Ustrcmp(ss, "reject") == 0) logbits |= LOG_REJECT; + else + { + logbits |= LOG_MAIN|LOG_REJECT; + log_write(0, LOG_MAIN|LOG_PANIC, "unknown log name \"%s\" in " + "\"log_reject_target\" in %s ACL", ss, acl_wherenames[where]); + } + } + log_reject_target = logbits; + } + break; + case ACLC_LOGWRITE: { int logbits = 0; @@ -1844,15 +3324,17 @@ for (; cb != NULL; cb = cb->next) s++; } while (isspace(*s)) s++; + + if (logbits == 0) logbits = LOG_MAIN; log_write(0, logbits, "%s", string_printing(s)); } break; -#ifdef WITH_CONTENT_SCAN + #ifdef WITH_CONTENT_SCAN case ACLC_MALWARE: { - /* Seperate the regular expression and any optional parameters. */ + /* 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); @@ -1869,20 +3351,28 @@ 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 + + case ACLC_REMOVE_HEADER: + setup_remove_header(arg); break; -#endif case ACLC_SENDER_DOMAINS: { @@ -1904,13 +3394,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; - acl_var[cb->u.varnumber] = string_copy(arg); + if (cb->u.varname[0] == 'c') store_pool = POOL_PERM; + acl_var_create(cb->u.varname)->data.ptr = 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. */ @@ -1928,20 +3418,26 @@ for (; cb != NULL; cb = cb->next) } } break; -#endif + #endif -#ifdef EXPERIMENTAL_SPF + #ifdef EXPERIMENTAL_SPF case ACLC_SPF: - rc = spf_process(&arg, sender_address); + rc = spf_process(&arg, sender_address, SPF_PROCESS_NORMAL); break; -#endif + case ACLC_SPF_GUESS: + rc = spf_process(&arg, sender_address, SPF_PROCESS_GUESS); + break; + #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; @@ -1964,13 +3460,8 @@ for (; cb != NULL; cb = cb->next) /* If the result is the one for which "message" and/or "log_message" are used, -handle the values of these options. Most verbs have but a single return for -which the messages are relevant, but for "discard", it's useful to have the log -message both when it succeeds and when it fails. Also, for an "accept" that -appears in a QUIT ACL, we want to handle the user message. Since only "accept" -and "warn" are permitted in that ACL, we don't need to test the verb. - -These modifiers act in different ways: +handle the values of these modifiers. If there isn't a log message set, we make +it the same as the user message. "message" is a user message that will be included in an SMTP response. Unless it is empty, it overrides any previously set user message. @@ -1978,23 +3469,29 @@ it is empty, it overrides any previously set user message. "log_message" is a non-user message, and it adds to any existing non-user message that is already set. -If there isn't a log message set, we make it the same as the user message. */ +Most verbs have but a single return for which the messages are relevant, but +for "discard", it's useful to have the log message both when it succeeds and +when it fails. For "accept", the message is used in the OK case if there is no +"endpass", but (for backwards compatibility) in the FAIL case if "endpass" is +present. */ + +if (*epp && rc == OK) user_message = NULL; -if (((rc == FAIL_DROP)? FAIL : rc) == msgcond[verb] || - (verb == ACL_DISCARD && rc == OK) || - (where == ACL_WHERE_QUIT)) +if (((1<verb != ACL_ACCEPT && acl->verb != ACL_WARN) { - *log_msgptr = string_sprintf("\"%s\" is not allowed in a QUIT ACL", + *log_msgptr = string_sprintf("\"%s\" is not allowed in a QUIT or not-QUIT ACL", verbs[acl->verb]); return ERROR; } @@ -2331,7 +3828,7 @@ while (acl != NULL) switch (cond) { case DEFER: - HDEBUG(D_acl) debug_printf("%s: condition test deferred\n", verbs[acl->verb]); + HDEBUG(D_acl) debug_printf("%s: condition test deferred in %s\n", verbs[acl->verb], acl_name); if (basic_errno != ERRNO_CALLOUTDEFER) { if (search_error_message != NULL && *search_error_message != 0) @@ -2347,29 +3844,29 @@ while (acl != NULL) default: /* Paranoia */ case ERROR: - HDEBUG(D_acl) debug_printf("%s: condition test error\n", verbs[acl->verb]); + HDEBUG(D_acl) debug_printf("%s: condition test error in %s\n", verbs[acl->verb], acl_name); return ERROR; case OK: - HDEBUG(D_acl) debug_printf("%s: condition test succeeded\n", - verbs[acl->verb]); + HDEBUG(D_acl) debug_printf("%s: condition test succeeded in %s\n", + verbs[acl->verb], acl_name); break; case FAIL: - HDEBUG(D_acl) debug_printf("%s: condition test failed\n", verbs[acl->verb]); + HDEBUG(D_acl) debug_printf("%s: condition test failed in %s\n", verbs[acl->verb], acl_name); break; /* DISCARD and DROP can happen only from a nested ACL condition, and DISCARD can happen only for an "accept" or "discard" verb. */ case DISCARD: - HDEBUG(D_acl) debug_printf("%s: condition test yielded \"discard\"\n", - verbs[acl->verb]); + HDEBUG(D_acl) debug_printf("%s: condition test yielded \"discard\" in %s\n", + verbs[acl->verb], acl_name); break; case FAIL_DROP: - HDEBUG(D_acl) debug_printf("%s: condition test yielded \"drop\"\n", - verbs[acl->verb]); + HDEBUG(D_acl) debug_printf("%s: condition test yielded \"drop\" in %s\n", + verbs[acl->verb], acl_name); break; } @@ -2420,10 +3917,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; @@ -2445,17 +3943,109 @@ 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, 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 *recipient, uschar *s, uschar **user_msgptr, + uschar **log_msgptr) +{ +int rc; +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 = recipient; + if (deliver_split_address(addr) == DEFER) + { + *log_msgptr = US"defer in percent_hack_domains check"; + return DEFER; + } + deliver_domain = addr->domain; + deliver_localpart = addr->local_part; + } + +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. 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) @@ -2467,23 +4057,26 @@ 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 *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; +log_reject_target = LOG_MAIN|LOG_REJECT; 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"; @@ -2492,16 +4085,57 @@ if (where == ACL_WHERE_RCPT) deliver_domain = addr->domain; deliver_localpart = addr->local_part; } -else - { - addr = NULL; - smtp_command_argument = data_string; - } +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. -smtp_command_argument = deliver_domain = - deliver_localpart = deliver_address_data = sender_address_data = NULL; +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: + if( rcpt_count > 1 ) + cancel_cutthrough_connection("more than one recipient"); + else if (rc == OK && cutthrough_delivery && cutthrough_fd < 0) + 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; /* A DISCARD response is permitted only for message ACLs, excluding the PREDATA ACL, which is really in the middle of an SMTP command. */ @@ -2526,53 +4160,73 @@ if (rc == FAIL_DROP && where == ACL_WHERE_MAILAUTH) return ERROR; } -/* Before giving an error response, take a look at the length of any user -message, and split it up into multiple lines if possible. */ +/* Before giving a response, take a look at the length of any user message, and +split it up into multiple lines if possible. */ -if (rc != OK && *user_msgptr != NULL && Ustrlen(*user_msgptr) > 75) - { - uschar *s = *user_msgptr = string_copy(*user_msgptr); - uschar *ss = s; +*user_msgptr = string_split_message(*user_msgptr); +if (fake_response != OK) + fake_response_text = string_split_message(fake_response_text); - for (;;) - { - int i = 0; - while (i < 75 && *ss != 0 && *ss != '\n') ss++, i++; - if (*ss == 0) break; - if (*ss == '\n') - s = ++ss; - else - { - uschar *t = ss + 1; - uschar *tt = NULL; - while (--t > s + 35) - { - if (*t == ' ') - { - if (t[-1] == ':') { tt = t; break; } - if (tt == NULL) tt = t; - } - } +return rc; +} - if (tt == NULL) /* Can't split behind - try ahead */ - { - t = ss + 1; - while (*t != 0) - { - if (*t == ' ' || *t == '\n') - { tt = t; break; } - t++; - } - } - if (tt == NULL) break; /* Can't find anywhere to split */ - *tt = '\n'; - s = ss = tt+1; - } - } +/************************************************* +* Create ACL variable * +*************************************************/ + +/* Create an ACL variable or reuse an existing one. ACL variables are in a +binary tree (see tree.c) with acl_var_c and acl_var_m as root nodes. + +Argument: + name pointer to the variable's name, starting with c or m + +Returns the pointer to variable's tree node +*/ + +tree_node * +acl_var_create(uschar *name) +{ +tree_node *node, **root; +root = (name[0] == 'c')? &acl_var_c : &acl_var_m; +node = tree_search(*root, name); +if (node == NULL) + { + node = store_get(sizeof(tree_node) + Ustrlen(name)); + Ustrcpy(node->name, name); + (void)tree_insertnode(root, node); } +node->data.ptr = NULL; +return node; +} -return rc; + + +/************************************************* +* Write an ACL variable in spool format * +*************************************************/ + +/* This function is used as a callback for tree_walk when writing variables to +the spool file. To retain spool file compatibility, what is written is -aclc or +-aclm followed by the rest of the name and the data length, space separated, +then the value itself, starting on a new line, and terminated by an additional +newline. When we had only numbered ACL variables, the first line might look +like this: "-aclc 5 20". Now it might be "-aclc foo 20" for the variable called +acl_cfoo. + +Arguments: + name of the variable + value of the variable + ctx FILE pointer (as a void pointer) + +Returns: nothing +*/ + +void +acl_var_write(uschar *name, uschar *value, void *ctx) +{ +FILE *f = (FILE *)ctx; +fprintf(f, "-acl%c %s %d\n%s\n", name[0], name+1, Ustrlen(value), value); } /* End of acl.c */