X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/ff790e47f2de6f4d6d48148e1d5a67da8e93c446..e2803e40f432c44b56967261357dc38a33616a35:/src/src/expand.c diff --git a/src/src/expand.c b/src/src/expand.c index ca340bb1b..cd84294ae 100644 --- a/src/src/expand.c +++ b/src/src/expand.c @@ -1,10 +1,10 @@ -/* $Cambridge: exim/src/src/expand.c,v 1.35 2005/06/22 15:44:38 ph10 Exp $ */ +/* $Cambridge: exim/src/src/expand.c,v 1.96 2008/08/07 11:05:03 fanf2 Exp $ */ /************************************************* * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) University of Cambridge 1995 - 2005 */ +/* Copyright (c) University of Cambridge 1995 - 2007 */ /* See the file NOTICE for conditions of use and distribution. */ @@ -13,12 +13,20 @@ #include "exim.h" +/* Recursively called function */ + +static uschar *expand_string_internal(uschar *, BOOL, uschar **, BOOL); + #ifdef STAND_ALONE #ifndef SUPPORT_CRYPTEQ #define SUPPORT_CRYPTEQ #endif #endif +#ifdef LOOKUP_LDAP +#include "lookups/ldap.h" +#endif + #ifdef SUPPORT_CRYPTEQ #ifdef CRYPT_H #include @@ -28,15 +36,63 @@ extern char* crypt16(char*, char*); #endif #endif -#ifdef LOOKUP_LDAP -#include "lookups/ldap.h" -#endif - - - -/* Recursively called function */ +/* The handling of crypt16() is a mess. I will record below the analysis of the +mess that was sent to me. We decided, however, to make changing this very low +priority, because in practice people are moving away from the crypt() +algorithms nowadays, so it doesn't seem worth it. + + +There is an algorithm named "crypt16" in Ultrix and Tru64. It crypts +the first 8 characters of the password using a 20-round version of crypt +(standard crypt does 25 rounds). It then crypts the next 8 characters, +or an empty block if the password is less than 9 characters, using a +20-round version of crypt and the same salt as was used for the first +block. Charaters after the first 16 are ignored. It always generates +a 16-byte hash, which is expressed together with the salt as a string +of 24 base 64 digits. Here are some links to peruse: + + http://cvs.pld.org.pl/pam/pamcrypt/crypt16.c?rev=1.2 + http://seclists.org/bugtraq/1999/Mar/0076.html + +There's a different algorithm named "bigcrypt" in HP-UX, Digital Unix, +and OSF/1. This is the same as the standard crypt if given a password +of 8 characters or less. If given more, it first does the same as crypt +using the first 8 characters, then crypts the next 8 (the 9th to 16th) +using as salt the first two base 64 digits from the first hash block. +If the password is more than 16 characters then it crypts the 17th to 24th +characters using as salt the first two base 64 digits from the second hash +block. And so on: I've seen references to it cutting off the password at +40 characters (5 blocks), 80 (10 blocks), or 128 (16 blocks). Some links: + + http://cvs.pld.org.pl/pam/pamcrypt/bigcrypt.c?rev=1.2 + http://seclists.org/bugtraq/1999/Mar/0109.html + http://h30097.www3.hp.com/docs/base_doc/DOCUMENTATION/HTML/AA-Q0R2D- + TET1_html/sec.c222.html#no_id_208 + +Exim has something it calls "crypt16". It will either use a native +crypt16 or its own implementation. A native crypt16 will presumably +be the one that I called "crypt16" above. The internal "crypt16" +function, however, is a two-block-maximum implementation of what I called +"bigcrypt". The documentation matches the internal code. + +I suspect that whoever did the "crypt16" stuff for Exim didn't realise +that crypt16 and bigcrypt were different things. + +Exim uses the LDAP-style scheme identifier "{crypt16}" to refer +to whatever it is using under that name. This unfortunately sets a +precedent for using "{crypt16}" to identify two incompatible algorithms +whose output can't be distinguished. With "{crypt16}" thus rendered +ambiguous, I suggest you deprecate it and invent two new identifiers +for the two algorithms. + +Both crypt16 and bigcrypt are very poor algorithms, btw. Hashing parts +of the password separately means they can be cracked separately, so +the double-length hash only doubles the cracking effort instead of +squaring it. I recommend salted SHA-1 ({SSHA}), or the Blowfish-based +bcrypt ({CRYPT}$2a$). + +*/ -static uschar *expand_string_internal(uschar *, BOOL, uschar **, BOOL); @@ -50,17 +106,20 @@ alphabetical order. */ static uschar *item_table[] = { US"dlfunc", US"extract", + US"filter", US"hash", US"hmac", US"if", US"length", US"lookup", + US"map", US"nhash", US"perl", US"prvs", US"prvscheck", US"readfile", US"readsocket", + US"reduce", US"run", US"sg", US"substr", @@ -69,17 +128,20 @@ static uschar *item_table[] = { enum { EITEM_DLFUNC, EITEM_EXTRACT, + EITEM_FILTER, EITEM_HASH, EITEM_HMAC, EITEM_IF, EITEM_LENGTH, EITEM_LOOKUP, + EITEM_MAP, EITEM_NHASH, EITEM_PERL, EITEM_PRVS, EITEM_PRVSCHECK, EITEM_READFILE, EITEM_READSOCK, + EITEM_REDUCE, EITEM_RUN, EITEM_SG, EITEM_SUBSTR, @@ -94,16 +156,19 @@ static uschar *op_table_underscore[] = { US"from_utf8", US"local_part", US"quote_local_part", + US"time_eval", US"time_interval"}; enum { EOP_FROM_UTF8, EOP_LOCAL_PART, EOP_QUOTE_LOCAL_PART, + EOP_TIME_EVAL, EOP_TIME_INTERVAL }; static uschar *op_table_main[] = { US"address", + US"addresses", US"base62", US"base62d", US"domain", @@ -123,6 +188,7 @@ static uschar *op_table_main[] = { US"nhash", US"quote", US"rfc2047", + US"rfc2047d", US"rxquote", US"s", US"sha1", @@ -134,6 +200,7 @@ static uschar *op_table_main[] = { enum { EOP_ADDRESS = sizeof(op_table_underscore)/sizeof(uschar *), + EOP_ADDRESSES, EOP_BASE62, EOP_BASE62D, EOP_DOMAIN, @@ -153,6 +220,7 @@ enum { EOP_NHASH, EOP_QUOTE, EOP_RFC2047, + EOP_RFC2047D, EOP_RXQUOTE, EOP_S, EOP_SHA1, @@ -180,6 +248,8 @@ static uschar *cond_table[] = { US"eqi", US"exists", US"first_delivery", + US"forall", + US"forany", US"ge", US"gei", US"gt", @@ -219,6 +289,8 @@ enum { ECOND_STR_EQI, ECOND_EXISTS, ECOND_FIRST_DELIVERY, + ECOND_FORALL, + ECOND_FORANY, ECOND_STR_GE, ECOND_STR_GEI, ECOND_STR_GT, @@ -272,11 +344,13 @@ enum { vtype_stringptr, /* value is address of pointer to string */ vtype_msgbody, /* as stringptr, but read when first required */ vtype_msgbody_end, /* ditto, the end of the message */ - vtype_msgheaders, /* the message's headers */ + vtype_msgheaders, /* the message's headers, processed */ + vtype_msgheaders_raw, /* the message's headers, unprocessed */ vtype_localpart, /* extract local part from string */ vtype_domain, /* extract domain from string */ vtype_recipients, /* extract recipients from recipients list */ - /* (enabled only during system filtering */ + /* (available only in system filters, ACLs, and */ + /* local_scan()) */ vtype_todbsdin, /* value not used; generate BSD inbox tod */ vtype_tode, /* value not used; generate tod in epoch format */ vtype_todf, /* value not used; generate full tod */ @@ -298,26 +372,8 @@ enum { /* This table must be kept in alphabetical order. */ static var_entry var_table[] = { - { "acl_c0", vtype_stringptr, &acl_var[0] }, - { "acl_c1", vtype_stringptr, &acl_var[1] }, - { "acl_c2", vtype_stringptr, &acl_var[2] }, - { "acl_c3", vtype_stringptr, &acl_var[3] }, - { "acl_c4", vtype_stringptr, &acl_var[4] }, - { "acl_c5", vtype_stringptr, &acl_var[5] }, - { "acl_c6", vtype_stringptr, &acl_var[6] }, - { "acl_c7", vtype_stringptr, &acl_var[7] }, - { "acl_c8", vtype_stringptr, &acl_var[8] }, - { "acl_c9", vtype_stringptr, &acl_var[9] }, - { "acl_m0", vtype_stringptr, &acl_var[10] }, - { "acl_m1", vtype_stringptr, &acl_var[11] }, - { "acl_m2", vtype_stringptr, &acl_var[12] }, - { "acl_m3", vtype_stringptr, &acl_var[13] }, - { "acl_m4", vtype_stringptr, &acl_var[14] }, - { "acl_m5", vtype_stringptr, &acl_var[15] }, - { "acl_m6", vtype_stringptr, &acl_var[16] }, - { "acl_m7", vtype_stringptr, &acl_var[17] }, - { "acl_m8", vtype_stringptr, &acl_var[18] }, - { "acl_m9", vtype_stringptr, &acl_var[19] }, + /* WARNING: Do not invent variables whose names start acl_c or acl_m because + they will be confused with user-creatable ACL variables. */ { "acl_verify_message", vtype_stringptr, &acl_verify_message }, { "address_data", vtype_stringptr, &deliver_address_data }, { "address_file", vtype_stringptr, &address_file }, @@ -340,6 +396,10 @@ static var_entry var_table[] = { { "compile_date", vtype_stringptr, &version_date }, { "compile_number", vtype_stringptr, &version_cnumber }, { "csa_status", vtype_stringptr, &csa_status }, +#ifdef EXPERIMENTAL_DCC + { "dcc_header", vtype_stringptr, &dcc_header }, + { "dcc_result", vtype_stringptr, &dcc_result }, +#endif #ifdef WITH_OLD_DEMIME { "demime_errorlevel", vtype_int, &demime_errorlevel }, { "demime_reason", vtype_stringptr, &demime_reason }, @@ -356,8 +416,13 @@ static var_entry var_table[] = { { "dk_signsall", vtype_dk_verify, NULL }, { "dk_status", vtype_dk_verify, NULL }, { "dk_testing", vtype_dk_verify, NULL }, +#endif +#ifdef EXPERIMENTAL_DKIM + { "dkim_domain", vtype_stringptr, &dkim_signing_domain }, + { "dkim_selector", vtype_stringptr, &dkim_signing_selector }, #endif { "dnslist_domain", vtype_stringptr, &dnslist_domain }, + { "dnslist_matched", vtype_stringptr, &dnslist_matched }, { "dnslist_text", vtype_stringptr, &dnslist_text }, { "dnslist_value", vtype_stringptr, &dnslist_value }, { "domain", vtype_stringptr, &deliver_domain }, @@ -377,6 +442,7 @@ static var_entry var_table[] = { { "inode", vtype_ino, &deliver_inode }, { "interface_address", vtype_stringptr, &interface_address }, { "interface_port", vtype_int, &interface_port }, + { "item", vtype_stringptr, &iterate_item }, #ifdef LOOKUP_LDAP { "ldap_dn", vtype_stringptr, &eldap_dn }, #endif @@ -395,11 +461,14 @@ static var_entry var_table[] = { #ifdef WITH_CONTENT_SCAN { "malware_name", vtype_stringptr, &malware_name }, #endif + { "max_received_linelength", vtype_int, &max_received_linelength }, { "message_age", vtype_int, &message_age }, { "message_body", vtype_msgbody, &message_body }, { "message_body_end", vtype_msgbody_end, &message_body_end }, { "message_body_size", vtype_int, &message_body_size }, + { "message_exim_id", vtype_stringptr, &message_id }, { "message_headers", vtype_msgheaders, NULL }, + { "message_headers_raw", vtype_msgheaders_raw, NULL }, { "message_id", vtype_stringptr, &message_id }, { "message_linecount", vtype_int, &message_linecount }, { "message_size", vtype_int, &message_size }, @@ -449,6 +518,8 @@ static var_entry var_table[] = { { "rcpt_fail_count", vtype_int, &rcpt_fail_count }, { "received_count", vtype_int, &received_count }, { "received_for", vtype_stringptr, &received_for }, + { "received_ip_address", vtype_stringptr, &interface_address }, + { "received_port", vtype_int, &interface_port }, { "received_protocol", vtype_stringptr, &received_protocol }, { "received_time", vtype_int, &received_time }, { "recipient_data", vtype_stringptr, &recipient_data }, @@ -480,8 +551,13 @@ static var_entry var_table[] = { { "sender_rate_period", vtype_stringptr, &sender_rate_period }, { "sender_rcvhost", vtype_stringptr, &sender_rcvhost }, { "sender_verify_failure",vtype_stringptr, &sender_verify_failure }, + { "sending_ip_address", vtype_stringptr, &sending_ip_address }, + { "sending_port", vtype_int, &sending_port }, { "smtp_active_hostname", vtype_stringptr, &smtp_active_hostname }, - { "smtp_command_argument", vtype_stringptr, &smtp_command_argument }, + { "smtp_command", vtype_stringptr, &smtp_cmd_buffer }, + { "smtp_command_argument", vtype_stringptr, &smtp_cmd_argument }, + { "smtp_count_at_connection_start", vtype_int, &smtp_accept_count }, + { "smtp_notquit_reason", vtype_stringptr, &smtp_notquit_reason }, { "sn0", vtype_filter_int, &filter_sn[0] }, { "sn1", vtype_filter_int, &filter_sn[1] }, { "sn2", vtype_filter_int, &filter_sn[2] }, @@ -499,6 +575,7 @@ static var_entry var_table[] = { { "spam_score_int", vtype_stringptr, &spam_score_int }, #endif #ifdef EXPERIMENTAL_SPF + { "spf_guess", vtype_stringptr, &spf_guess }, { "spf_header_comment", vtype_stringptr, &spf_header_comment }, { "spf_received", vtype_stringptr, &spf_received }, { "spf_result", vtype_stringptr, &spf_result }, @@ -1092,7 +1169,8 @@ Arguments: newsize return the size of memory block that was obtained; may be NULL if exists_only is TRUE want_raw TRUE if called for $rh_ or $rheader_ variables; no processing, - other than concatenating, will be done on the header + other than concatenating, will be done on the header. Also used + for $message_headers_raw. charset name of charset to translate MIME words to; used only if want_raw is false; if NULL, no translation is done (this is used for $bh_ and $bheader_) @@ -1135,6 +1213,12 @@ for (i = 0; i < 2; i++) while (isspace(*t)) t++; /* remove leading white space */ ilen = h->slen - (t - h->text); /* length to insert */ + /* Unless wanted raw, remove trailing whitespace, including the + newline. */ + + if (!want_raw) + while (ilen > 0 && isspace(t[ilen-1])) ilen--; + /* Set comma = 1 if handling a single header and it's one of those that contains an address list, except when asked for raw headers. Only need to do this once. */ @@ -1146,7 +1230,7 @@ for (i = 0; i < 2; i++) /* First pass - compute total store needed; second pass - compute total store used, including this header. */ - size += ilen + comma; + size += ilen + comma + 1; /* +1 for the newline */ /* Second pass - concatentate the data, up to a maximum. Note that the loop stops when size hits the limit. */ @@ -1155,14 +1239,19 @@ for (i = 0; i < 2; i++) { if (size > header_insert_maxlen) { - ilen -= size - header_insert_maxlen; + ilen -= size - header_insert_maxlen - 1; comma = 0; } Ustrncpy(ptr, t, ilen); ptr += ilen; - if (comma != 0 && ilen > 0) + + /* For a non-raw header, put in the comma if needed, then add + back the newline we removed above, provided there was some text in + the header. */ + + if (!want_raw && ilen > 0) { - ptr[-1] = ','; + if (comma != 0) *ptr++ = ','; *ptr++ = '\n'; } } @@ -1170,8 +1259,9 @@ for (i = 0; i < 2; i++) } } - /* At end of first pass, truncate size if necessary, and get the buffer - to hold the data, returning the buffer size. */ + /* At end of first pass, return NULL if no header found. Then truncate size + if necessary, and get the buffer to hold the data, returning the buffer size. + */ if (i == 0) { @@ -1182,10 +1272,6 @@ for (i = 0; i < 2; i++) } } -/* Remove a redundant added comma if present */ - -if (comma != 0 && ptr > yield) ptr -= 2; - /* That's all we do for raw header expansion. */ if (want_raw) @@ -1193,17 +1279,19 @@ if (want_raw) *ptr = 0; } -/* Otherwise, we remove trailing whitespace, including newlines. Then we do RFC -2047 decoding, translating the charset if requested. The rfc2047_decode2() +/* Otherwise, remove a final newline and a redundant added comma. Then we do +RFC 2047 decoding, translating the charset if requested. The rfc2047_decode2() function can return an error with decoded data if the charset translation fails. If decoding fails, it returns NULL. */ else { uschar *decoded, *error; - while (ptr > yield && isspace(ptr[-1])) ptr--; + if (ptr > yield && ptr[-1] == '\n') ptr--; + if (ptr > yield && comma != 0 && ptr[-1] == ',') ptr--; *ptr = 0; - decoded = rfc2047_decode2(yield, TRUE, charset, '?', NULL, newsize, &error); + decoded = rfc2047_decode2(yield, check_rfc2047_length, charset, '?', NULL, + newsize, &error); if (error != NULL) { DEBUG(D_any) debug_printf("*** error in RFC 2047 decoding: %s\n" @@ -1246,6 +1334,37 @@ find_variable(uschar *name, BOOL exists_only, BOOL skipping, int *newsize) int first = 0; int last = var_table_size; +/* Handle ACL variables, whose names are of the form acl_cxxx or acl_mxxx. +Originally, xxx had to be a number in the range 0-9 (later 0-19), but from +release 4.64 onwards arbitrary names are permitted, as long as the first 5 +characters are acl_c or acl_m and the sixth is either a digit or an underscore +(this gave backwards compatibility at the changeover). There may be built-in +variables whose names start acl_ but they should never start in this way. This +slightly messy specification is a consequence of the history, needless to say. + +If an ACL variable does not exist, treat it as empty, unless strict_acl_vars is +set, in which case give an error. */ + +if ((Ustrncmp(name, "acl_c", 5) == 0 || Ustrncmp(name, "acl_m", 5) == 0) && + !isalpha(name[5])) + { + tree_node *node = + tree_search((name[4] == 'c')? acl_var_c : acl_var_m, name + 4); + return (node == NULL)? (strict_acl_vars? NULL : US"") : node->data.ptr; + } + +/* Handle $auth variables. */ + +if (Ustrncmp(name, "auth", 4) == 0) + { + uschar *endptr; + int n = Ustrtoul(name + 4, &endptr, 10); + if (*endptr == 0 && n != 0 && n <= AUTH_VARS) + return (auth_vars[n-1] == NULL)? US"" : auth_vars[n-1]; + } + +/* For all other variables, search the table */ + while (last > first) { uschar *s, *domain; @@ -1257,7 +1376,7 @@ while (last > first) if (c < 0) { last = middle; continue; } /* Found an existing variable. If in skipping state, the value isn't needed, - and we want to avoid processing (such as looking up up the host name). */ + and we want to avoid processing (such as looking up the host name). */ if (skipping) return US""; @@ -1337,7 +1456,7 @@ while (last > first) return var_buffer; case vtype_load_avg: - sprintf(CS var_buffer, "%d", os_getloadavg()); /* load_average */ + sprintf(CS var_buffer, "%d", OS_GETLOADAVG()); /* load_average */ return var_buffer; case vtype_host_lookup: /* Lookup if not done so */ @@ -1367,6 +1486,9 @@ while (last > first) case vtype_msgheaders: return find_header(NULL, exists_only, newsize, FALSE, NULL); + case vtype_msgheaders_raw: + return find_header(NULL, exists_only, newsize, TRUE, NULL); + case vtype_msgbody: /* Pointer to msgbody string */ case vtype_msgbody_end: /* Ditto, the end of the msg */ ss = (uschar **)(var_table[middle].value); @@ -1393,9 +1515,15 @@ while (last > first) if (len > 0) { body[len] = 0; - while (len > 0) + if (message_body_newlines) /* Separate loops for efficiency */ { - if (body[--len] == '\n' || body[len] == 0) body[len] = ' '; + while (len > 0) + { if (body[--len] == 0) body[len] = ' '; } + } + else + { + while (len > 0) + { if (body[--len] == '\n' || body[len] == 0) body[len] = ' '; } } } } @@ -1423,10 +1551,22 @@ while (last > first) return tod_stamp(tod_log_datestamp); case vtype_reply: /* Get reply address */ - s = find_header(US"reply-to:", exists_only, newsize, FALSE, + s = find_header(US"reply-to:", exists_only, newsize, TRUE, headers_charset); + if (s != NULL) while (isspace(*s)) s++; if (s == NULL || *s == 0) - s = find_header(US"from:", exists_only, newsize, FALSE, headers_charset); + { + *newsize = 0; /* For the *s==0 case */ + s = find_header(US"from:", exists_only, newsize, TRUE, headers_charset); + } + if (s != NULL) + { + uschar *t; + while (isspace(*s)) s++; + for (t = s; *t != 0; t++) if (*t == '\n') *t = ' '; + while (t > s && isspace(t[-1])) t--; + *t = 0; + } return (s == NULL)? US"" : s; /* A recipients list is available only during system message filtering, @@ -1536,6 +1676,33 @@ return 0; +/************************************************* +* Elaborate message for bad variable * +*************************************************/ + +/* For the "unknown variable" message, take a look at the variable's name, and +give additional information about possible ACL variables. The extra information +is added on to expand_string_message. + +Argument: the name of the variable +Returns: nothing +*/ + +static void +check_variable_error_message(uschar *name) +{ +if (Ustrncmp(name, "acl_", 4) == 0) + expand_string_message = string_sprintf("%s (%s)", expand_string_message, + (name[4] == 'c' || name[4] == 'm')? + (isalpha(name[5])? + US"6th character of a user-defined ACL variable must be a digit or underscore" : + US"strict_acl_vars is set" /* Syntax is OK, it has to be this */ + ) : + US"user-defined ACL variables must start acl_c or acl_m"); +} + + + /************************************************* * Read and evaluate a condition * *************************************************/ @@ -1617,7 +1784,9 @@ switch(cond_type) s = read_name(name, 256, s+1, US"_"); - /* Test for a header's existence */ + /* Test for a header's existence. If the name contains a closing brace + character, this may be a user error where the terminating colon has been + omitted. Set a flag to adjust a subsequent error message in this case. */ if (Ustrncmp(name, "h_", 2) == 0 || Ustrncmp(name, "rh_", 3) == 0 || @@ -1627,6 +1796,7 @@ switch(cond_type) Ustrncmp(name, "bheader_", 8) == 0) { s = read_header_name(name, 256, s); + if (Ustrchr(name, '}') != NULL) malformed_header = TRUE; if (yield != NULL) *yield = (find_header(name, TRUE, NULL, FALSE, NULL) != NULL) == testfor; } @@ -1642,6 +1812,7 @@ switch(cond_type) expand_string_message = (name[0] == 0)? string_sprintf("variable name omitted after \"def:\"") : string_sprintf("unknown variable \"%s\" after \"def:\"", name); + check_variable_error_message(name); return NULL; } if (yield != NULL) *yield = (value[0] != 0) == testfor; @@ -1707,7 +1878,7 @@ switch(cond_type) case ECOND_ISIP4: case ECOND_ISIP6: rc = string_is_ip_address(sub[0], NULL); - *yield = ((cond_type == ECOND_ISIP)? (rc > 0) : + *yield = ((cond_type == ECOND_ISIP)? (rc != 0) : (cond_type == ECOND_ISIP4)? (rc == 4) : (rc == 6)) == testfor; break; @@ -1852,26 +2023,18 @@ switch(cond_type) conditions that compare numbers do not start with a letter. This just saves checking for them individually. */ - if (!isalpha(name[0])) + if (!isalpha(name[0]) && yield != NULL) { - uschar *endptr; - num[i] = (int)Ustrtol((const uschar *)sub[i], &endptr, 10); - if (tolower(*endptr) == 'k') + if (sub[i][0] == 0) { - num[i] *= 1024; - endptr++; + num[i] = 0; + DEBUG(D_expand) + debug_printf("empty string cast to zero for numerical comparison\n"); } - else if (tolower(*endptr) == 'm') + else { - num[i] *= 1024*1024; - endptr++; - } - while (isspace(*endptr)) endptr++; - if (*endptr != 0) - { - expand_string_message = string_sprintf("\"%s\" is not a number", - sub[i]); - return NULL; + num[i] = expand_string_integer(sub[i], FALSE); + if (expand_string_message != NULL) return NULL; } } } @@ -1967,7 +2130,7 @@ switch(cond_type) goto MATCHED_SOMETHING; case ECOND_MATCH_IP: /* Match IP address in a host list */ - if (sub[0][0] != 0 && string_is_ip_address(sub[0], NULL) <= 0) + if (sub[0][0] != 0 && string_is_ip_address(sub[0], NULL) == 0) { expand_string_message = string_sprintf("\"%s\" is not an IP address", sub[0]); @@ -2218,6 +2381,68 @@ switch(cond_type) return ++s; + /* forall/forany: iterates a condition with different values */ + + case ECOND_FORALL: + case ECOND_FORANY: + { + int sep = 0; + uschar *save_iterate_item = iterate_item; + + while (isspace(*s)) s++; + if (*s++ != '{') goto COND_FAILED_CURLY_START; + sub[0] = expand_string_internal(s, TRUE, &s, (yield == NULL)); + if (sub[0] == NULL) return NULL; + if (*s++ != '}') goto COND_FAILED_CURLY_END; + + while (isspace(*s)) s++; + if (*s++ != '{') goto COND_FAILED_CURLY_START; + + sub[1] = s; + + /* Call eval_condition once, with result discarded (as if scanning a + "false" part). This allows us to find the end of the condition, because if + the list it empty, we won't actually evaluate the condition for real. */ + + s = eval_condition(sub[1], NULL); + if (s == NULL) + { + expand_string_message = string_sprintf("%s inside \"%s\" condition", + expand_string_message, name); + return NULL; + } + while (isspace(*s)) s++; + + if (*s++ != '}') + { + expand_string_message = string_sprintf("missing } at end of condition " + "inside \"%s\"", name); + return NULL; + } + + if (yield != NULL) *yield = !testfor; + while ((iterate_item = string_nextinlist(&sub[0], &sep, NULL, 0)) != NULL) + { + DEBUG(D_expand) debug_printf("%s: $item = \"%s\"\n", name, iterate_item); + if (eval_condition(sub[1], &tempcond) == NULL) + { + expand_string_message = string_sprintf("%s inside \"%s\" condition", + expand_string_message, name); + iterate_item = save_iterate_item; + return NULL; + } + DEBUG(D_expand) debug_printf("%s: condition evaluated to %s\n", name, + tempcond? "true":"false"); + + if (yield != NULL) *yield = (tempcond == testfor); + if (tempcond == (cond_type == ECOND_FORANY)) break; + } + + iterate_item = save_iterate_item; + return s; + } + + /* Unknown condition */ default: @@ -2529,8 +2754,8 @@ Returns: pointer to string containing the last three static uschar * prvs_daystamp(int day_offset) { -uschar *days = store_get(16); -(void)string_format(days, 16, TIME_T_FMT, +uschar *days = store_get(32); /* Need at least 24 for cases */ +(void)string_format(days, 32, TIME_T_FMT, /* where TIME_T_FMT is %lld */ (time(NULL) + day_offset*86400)/86400); return (Ustrlen(days) >= 3) ? &days[Ustrlen(days)-3] : US"100"; } @@ -2660,63 +2885,53 @@ return yield; * Evaluate numeric expression * *************************************************/ -/* This is a set of mutually recursive functions that evaluate a simple -arithmetic expression involving only + - * / and parentheses. The only one that -is called from elsewhere is eval_expr, whose interface is: +/* This is a set of mutually recursive functions that evaluate an arithmetic +expression involving + - * / % & | ^ ~ << >> and parentheses. The only one of +these functions that is called from elsewhere is eval_expr, whose interface is: Arguments: - sptr pointer to the pointer to the string - gets updated - decimal TRUE if numbers are to be assumed decimal - error pointer to where to put an error message - must be NULL on input - endket TRUE if ')' must terminate - FALSE for external call + sptr pointer to the pointer to the string - gets updated + decimal TRUE if numbers are to be assumed decimal + error pointer to where to put an error message - must be NULL on input + endket TRUE if ')' must terminate - FALSE for external call - -Returns: on success: the value of the expression, with *error still NULL - on failure: an undefined value, with *error = a message +Returns: on success: the value of the expression, with *error still NULL + on failure: an undefined value, with *error = a message */ -static int eval_sumterm(uschar **, BOOL, uschar **); +static int eval_op_or(uschar **, BOOL, uschar **); + static int eval_expr(uschar **sptr, BOOL decimal, uschar **error, BOOL endket) { uschar *s = *sptr; -int x = eval_sumterm(&s, decimal, error); +int x = eval_op_or(&s, decimal, error); if (*error == NULL) { - while (*s == '+' || *s == '-') + if (endket) { - int op = *s++; - int y = eval_sumterm(&s, decimal, error); - if (*error != NULL) break; - if (op == '+') x += y; else x -= y; - } - if (*error == NULL) - { - if (endket) - { - if (*s != ')') - *error = US"expecting closing parenthesis"; - else - while (isspace(*(++s))); - } - else if (*s != 0) *error = US"expecting + or -"; + if (*s != ')') + *error = US"expecting closing parenthesis"; + else + while (isspace(*(++s))); } + else if (*s != 0) *error = US"expecting operator"; } - *sptr = s; return x; } + static int -eval_term(uschar **sptr, BOOL decimal, uschar **error) +eval_number(uschar **sptr, BOOL decimal, uschar **error) { register int c; int n; uschar *s = *sptr; while (isspace(*s)) s++; c = *s; -if (isdigit(c) || ((c == '-' || c == '+') && isdigit(s[1]))) +if (isdigit(c)) { int count; (void)sscanf(CS s, (decimal? "%d%n" : "%i%n"), &n, &count); @@ -2739,18 +2954,82 @@ else return n; } -static int eval_sumterm(uschar **sptr, BOOL decimal, uschar **error) + +static int eval_op_unary(uschar **sptr, BOOL decimal, uschar **error) +{ +uschar *s = *sptr; +int x; +while (isspace(*s)) s++; +if (*s == '+' || *s == '-' || *s == '~') + { + int op = *s++; + x = eval_op_unary(&s, decimal, error); + if (op == '-') x = -x; + else if (op == '~') x = ~x; + } +else + { + x = eval_number(&s, decimal, error); + } +*sptr = s; +return x; +} + + +static int eval_op_mult(uschar **sptr, BOOL decimal, uschar **error) +{ +uschar *s = *sptr; +int x = eval_op_unary(&s, decimal, error); +if (*error == NULL) + { + while (*s == '*' || *s == '/' || *s == '%') + { + int op = *s++; + int y = eval_op_unary(&s, decimal, error); + if (*error != NULL) break; + if (op == '*') x *= y; + else if (op == '/') x /= y; + else x %= y; + } + } +*sptr = s; +return x; +} + + +static int eval_op_sum(uschar **sptr, BOOL decimal, uschar **error) +{ +uschar *s = *sptr; +int x = eval_op_mult(&s, decimal, error); +if (*error == NULL) + { + while (*s == '+' || *s == '-') + { + int op = *s++; + int y = eval_op_mult(&s, decimal, error); + if (*error != NULL) break; + if (op == '+') x += y; else x -= y; + } + } +*sptr = s; +return x; +} + + +static int eval_op_shift(uschar **sptr, BOOL decimal, uschar **error) { uschar *s = *sptr; -int x = eval_term(&s, decimal, error); +int x = eval_op_sum(&s, decimal, error); if (*error == NULL) { - while (*s == '*' || *s == '/') + while ((*s == '<' || *s == '>') && s[1] == s[0]) { + int y; int op = *s++; - int y = eval_term(&s, decimal, error); + s++; + y = eval_op_sum(&s, decimal, error); if (*error != NULL) break; - if (op == '*') x *= y; else x /= y; + if (op == '<') x <<= y; else x >>= y; } } *sptr = s; @@ -2758,6 +3037,65 @@ return x; } +static int eval_op_and(uschar **sptr, BOOL decimal, uschar **error) +{ +uschar *s = *sptr; +int x = eval_op_shift(&s, decimal, error); +if (*error == NULL) + { + while (*s == '&') + { + int y; + s++; + y = eval_op_shift(&s, decimal, error); + if (*error != NULL) break; + x &= y; + } + } +*sptr = s; +return x; +} + + +static int eval_op_xor(uschar **sptr, BOOL decimal, uschar **error) +{ +uschar *s = *sptr; +int x = eval_op_and(&s, decimal, error); +if (*error == NULL) + { + while (*s == '^') + { + int y; + s++; + y = eval_op_and(&s, decimal, error); + if (*error != NULL) break; + x ^= y; + } + } +*sptr = s; +return x; +} + + +static int eval_op_or(uschar **sptr, BOOL decimal, uschar **error) +{ +uschar *s = *sptr; +int x = eval_op_xor(&s, decimal, error); +if (*error == NULL) + { + while (*s == '|') + { + int y; + s++; + y = eval_op_xor(&s, decimal, error); + if (*error != NULL) break; + x |= y; + } + } +*sptr = s; +return x; +} + /************************************************* @@ -2798,6 +3136,12 @@ we reset the store before processing it; if the result is in fresh store, we use that without copying. This is helpful for expanding strings like $message_headers which can get very long. +There's a problem if a ${dlfunc item has side-effects that cause allocation, +since resetting the store at the end of the expansion will free store that was +allocated by the plugin code as well as the slop after the expanded string. So +we skip any resets if ${dlfunc has been used. This is an unfortunate +consequence of string expansion becoming too powerful. + Arguments: string the string to be expanded ket_ends true if expansion is to stop at } @@ -2823,6 +3167,7 @@ uschar *yield = store_get(size); uschar *s = string; uschar *save_expand_nstring[EXPAND_MAXN+1]; int save_expand_nlength[EXPAND_MAXN+1]; +BOOL resetok = TRUE; expand_string_forcedfail = FALSE; expand_string_message = US""; @@ -2895,7 +3240,7 @@ while (*s != 0) if (ptr == 0 && yield != NULL) { - store_reset(yield); + if (resetok) store_reset(yield); yield = NULL; size = 0; } @@ -2915,7 +3260,7 @@ while (*s != 0) value = find_header(name, FALSE, &newsize, want_raw, charset); /* If we didn't find the header, and the header contains a closing brace - characters, this may be a user error where the terminating colon + character, this may be a user error where the terminating colon has been omitted. Set a flag to adjust the error message in this case. But there is no error here - nothing gets inserted. */ @@ -2935,6 +3280,7 @@ while (*s != 0) { expand_string_message = string_sprintf("unknown variable name \"%s\"", name); + check_variable_error_message(name); goto EXPAND_FAILED; } } @@ -3125,7 +3471,7 @@ while (*s != 0) /* Check that a key was provided for those lookup types that need it, and was not supplied for those that use the query style. */ - if (!mac_islookup(stype, lookup_querystyle)) + if (!mac_islookup(stype, lookup_querystyle|lookup_absfilequery)) { if (key == NULL) { @@ -3145,7 +3491,9 @@ while (*s != 0) } /* Get the next string in brackets and expand it. It is the file name for - single-key+file lookups, and the whole query otherwise. */ + single-key+file lookups, and the whole query otherwise. In the case of + queries that also require a file name (e.g. sqlite), the file name comes + first. */ if (*s != '{') goto EXPAND_FAILED_CURLY; filename = expand_string_internal(s+1, TRUE, &s, skipping); @@ -3154,12 +3502,30 @@ while (*s != 0) while (isspace(*s)) s++; /* If this isn't a single-key+file lookup, re-arrange the variables - to be appropriate for the search_ functions. */ + to be appropriate for the search_ functions. For query-style lookups, + there is just a "key", and no file name. For the special query-style + + file types, the query (i.e. "key") starts with a file name. */ if (key == NULL) { + while (isspace(*filename)) filename++; key = filename; - filename = NULL; + + if (mac_islookup(stype, lookup_querystyle)) + { + filename = NULL; + } + else + { + if (*filename != '/') + { + expand_string_message = string_sprintf( + "absolute file name expected for \"%s\" lookup", name); + goto EXPAND_FAILED; + } + while (*key != 0 && !isspace(*key)) key++; + if (*key != 0) *key++ = 0; + } } /* If skipping, don't do the next bit - just lookup_value == NULL, as if @@ -3333,15 +3699,24 @@ while (*s != 0) domain = Ustrrchr(sub_arg[0],'@'); if ( (domain == NULL) || (domain == sub_arg[0]) || (Ustrlen(domain) == 1) ) { - expand_string_message = US"first parameter must be a qualified email address"; + expand_string_message = US"prvs first argument must be a qualified email address"; + goto EXPAND_FAILED; + } + + /* Calculate the hash. The second argument must be a single-digit + key number, or unset. */ + + if (sub_arg[2] != NULL && + (!isdigit(sub_arg[2][0]) || sub_arg[2][1] != 0)) + { + expand_string_message = US"prvs second argument must be a single digit"; goto EXPAND_FAILED; } - /* Calculate the hash */ p = prvs_hmac_sha1(sub_arg[0],sub_arg[1],sub_arg[2],prvs_daystamp(7)); if (p == NULL) { - expand_string_message = US"hmac-sha1 conversion failed"; + expand_string_message = US"prvs hmac-sha1 conversion failed"; goto EXPAND_FAILED; } @@ -3349,11 +3724,11 @@ while (*s != 0) *domain++ = '\0'; yield = string_cat(yield,&size,&ptr,US"prvs=",5); - string_cat(yield,&size,&ptr,sub_arg[0],Ustrlen(sub_arg[0])); - string_cat(yield,&size,&ptr,US"/",1); string_cat(yield,&size,&ptr,(sub_arg[2] != NULL) ? sub_arg[2] : US"0", 1); string_cat(yield,&size,&ptr,prvs_daystamp(7),3); string_cat(yield,&size,&ptr,p,6); + string_cat(yield,&size,&ptr,US"=",1); + string_cat(yield,&size,&ptr,sub_arg[0],Ustrlen(sub_arg[0])); string_cat(yield,&size,&ptr,US"@",1); string_cat(yield,&size,&ptr,domain,Ustrlen(domain)); @@ -3368,32 +3743,38 @@ while (*s != 0) int mysize = 0, myptr = 0; const pcre *re; uschar *p; - /* Ugliness: We want to expand parameter 1 first, then set + + /* TF: Ugliness: We want to expand parameter 1 first, then set up expansion variables that are used in the expansion of parameter 2. So we clone the string for the first - expansion, where we only expand paramter 1. */ - uschar *s_backup = string_copy(s); + expansion, where we only expand parameter 1. + + PH: Actually, that isn't necessary. The read_subs() function is + designed to work this way for the ${if and ${lookup expansions. I've + tidied the code. + */ /* Reset expansion variables */ prvscheck_result = NULL; prvscheck_address = NULL; prvscheck_keynum = NULL; - switch(read_subs(sub_arg, 1, 1, &s_backup, skipping, FALSE, US"prvs")) + switch(read_subs(sub_arg, 1, 1, &s, skipping, FALSE, US"prvs")) { case 1: goto EXPAND_FAILED_CURLY; case 2: case 3: goto EXPAND_FAILED; } - re = regex_must_compile(US"^prvs\\=(.+)\\/([0-9])([0-9]{3})([A-F0-9]{6})\\@(.+)$", + re = regex_must_compile(US"^prvs\\=([0-9])([0-9]{3})([A-F0-9]{6})\\=(.+)\\@(.+)$", TRUE,FALSE); - if (regex_match_and_setup(re,sub_arg[0],0,-1)) { - uschar *local_part = string_copyn(expand_nstring[1],expand_nlength[1]); - uschar *key_num = string_copyn(expand_nstring[2],expand_nlength[2]); - uschar *daystamp = string_copyn(expand_nstring[3],expand_nlength[3]); - uschar *hash = string_copyn(expand_nstring[4],expand_nlength[4]); + if (regex_match_and_setup(re,sub_arg[0],0,-1)) + { + uschar *local_part = string_copyn(expand_nstring[4],expand_nlength[4]); + uschar *key_num = string_copyn(expand_nstring[1],expand_nlength[1]); + uschar *daystamp = string_copyn(expand_nstring[2],expand_nlength[2]); + uschar *hash = string_copyn(expand_nstring[3],expand_nlength[3]); uschar *domain = string_copyn(expand_nstring[5],expand_nlength[5]); DEBUG(D_expand) debug_printf("prvscheck localpart: %s\n", local_part); @@ -3409,21 +3790,19 @@ while (*s != 0) prvscheck_address[myptr] = '\0'; prvscheck_keynum = string_copy(key_num); - /* Now re-expand all arguments in the usual manner */ - switch(read_subs(sub_arg, 3, 3, &s, skipping, TRUE, US"prvs")) + /* Now expand the second argument */ + switch(read_subs(sub_arg, 1, 1, &s, skipping, FALSE, US"prvs")) { case 1: goto EXPAND_FAILED_CURLY; case 2: case 3: goto EXPAND_FAILED; } - if (*sub_arg[2] == '\0') - yield = string_cat(yield,&size,&ptr,prvscheck_address,Ustrlen(prvscheck_address)); - else - yield = string_cat(yield,&size,&ptr,sub_arg[2],Ustrlen(sub_arg[2])); - /* Now we have the key and can check the address. */ - p = prvs_hmac_sha1(prvscheck_address, sub_arg[1], prvscheck_keynum, daystamp); + + p = prvs_hmac_sha1(prvscheck_address, sub_arg[0], prvscheck_keynum, + daystamp); + if (p == NULL) { expand_string_message = US"hmac-sha1 conversion failed"; @@ -3432,6 +3811,7 @@ while (*s != 0) DEBUG(D_expand) debug_printf("prvscheck: received hash is %s\n", hash); DEBUG(D_expand) debug_printf("prvscheck: own hash is %s\n", p); + if (Ustrcmp(p,hash) == 0) { /* Success, valid BATV address. Now check the expiry date. */ @@ -3445,7 +3825,7 @@ while (*s != 0) Adjust "inow" accordingly. */ if ( (iexpire < 7) && (inow >= 993) ) inow = 0; - if (iexpire > inow) + if (iexpire >= inow) { prvscheck_result = US"1"; DEBUG(D_expand) debug_printf("prvscheck: success, $pvrs_result set to 1\n"); @@ -3461,12 +3841,35 @@ while (*s != 0) prvscheck_result = NULL; DEBUG(D_expand) debug_printf("prvscheck: hash failure, $pvrs_result unset\n"); } - } + + /* Now expand the final argument. We leave this till now so that + it can include $prvscheck_result. */ + + switch(read_subs(sub_arg, 1, 0, &s, skipping, TRUE, US"prvs")) + { + case 1: goto EXPAND_FAILED_CURLY; + case 2: + case 3: goto EXPAND_FAILED; + } + + if (sub_arg[0] == NULL || *sub_arg[0] == '\0') + yield = string_cat(yield,&size,&ptr,prvscheck_address,Ustrlen(prvscheck_address)); + else + yield = string_cat(yield,&size,&ptr,sub_arg[0],Ustrlen(sub_arg[0])); + + /* Reset the "internal" variables afterwards, because they are in + dynamic store that will be reclaimed if the expansion succeeded. */ + + prvscheck_address = NULL; + prvscheck_keynum = NULL; + } else { /* Does not look like a prvs encoded address, return the empty string. - We need to make sure all subs are expanded first. */ - switch(read_subs(sub_arg, 3, 3, &s, skipping, TRUE, US"prvs")) + We need to make sure all subs are expanded first, so as to skip over + the entire item. */ + + switch(read_subs(sub_arg, 2, 1, &s, skipping, TRUE, US"prvs")) { case 1: goto EXPAND_FAILED_CURLY; case 2: @@ -3511,7 +3914,7 @@ while (*s != 0) } yield = cat_file(f, yield, &size, &ptr, sub_arg[1]); - fclose(f); + (void)fclose(f); continue; } @@ -3557,28 +3960,160 @@ while (*s != 0) } else sub_arg[3] = NULL; /* No eol if no timeout */ - /* If skipping, we don't actually do anything */ + /* If skipping, we don't actually do anything. Otherwise, arrange to + connect to either an IP or a Unix socket. */ if (!skipping) { - /* Make a connection to the socket */ + /* Handle an IP (internet) domain */ - if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) + if (Ustrncmp(sub_arg[0], "inet:", 5) == 0) { - expand_string_message = string_sprintf("failed to create socket: %s", - strerror(errno)); - goto SOCK_FAIL; + BOOL connected = FALSE; + int namelen, port; + host_item shost; + host_item *h; + uschar *server_name = sub_arg[0] + 5; + uschar *port_name = Ustrrchr(server_name, ':'); + + /* Sort out the port */ + + if (port_name == NULL) + { + expand_string_message = + string_sprintf("missing port for readsocket %s", sub_arg[0]); + goto EXPAND_FAILED; + } + *port_name++ = 0; /* Terminate server name */ + + if (isdigit(*port_name)) + { + uschar *end; + port = Ustrtol(port_name, &end, 0); + if (end != port_name + Ustrlen(port_name)) + { + expand_string_message = + string_sprintf("invalid port number %s", port_name); + goto EXPAND_FAILED; + } + } + else + { + struct servent *service_info = getservbyname(CS port_name, "tcp"); + if (service_info == NULL) + { + expand_string_message = string_sprintf("unknown port \"%s\"", + port_name); + goto EXPAND_FAILED; + } + port = ntohs(service_info->s_port); + } + + /* Sort out the server. */ + + shost.next = NULL; + shost.address = NULL; + shost.port = port; + shost.mx = -1; + + namelen = Ustrlen(server_name); + + /* Anything enclosed in [] must be an IP address. */ + + if (server_name[0] == '[' && + server_name[namelen - 1] == ']') + { + server_name[namelen - 1] = 0; + server_name++; + if (string_is_ip_address(server_name, NULL) == 0) + { + expand_string_message = + string_sprintf("malformed IP address \"%s\"", server_name); + goto EXPAND_FAILED; + } + shost.name = shost.address = server_name; + } + + /* Otherwise check for an unadorned IP address */ + + else if (string_is_ip_address(server_name, NULL) != 0) + shost.name = shost.address = server_name; + + /* Otherwise lookup IP address(es) from the name */ + + else + { + shost.name = server_name; + if (host_find_byname(&shost, NULL, HOST_FIND_QUALIFY_SINGLE, NULL, + FALSE) != HOST_FOUND) + { + expand_string_message = + string_sprintf("no IP address found for host %s", shost.name); + goto EXPAND_FAILED; + } + } + + /* Try to connect to the server - test each IP till one works */ + + for (h = &shost; h != NULL; h = h->next) + { + int af = (Ustrchr(h->address, ':') != 0)? AF_INET6 : AF_INET; + if ((fd = ip_socket(SOCK_STREAM, af)) == -1) + { + expand_string_message = string_sprintf("failed to create socket: " + "%s", strerror(errno)); + goto SOCK_FAIL; + } + + if (ip_connect(fd, af, h->address, port, timeout) == 0) + { + connected = TRUE; + break; + } + } + + if (!connected) + { + expand_string_message = string_sprintf("failed to connect to " + "socket %s: couldn't connect to any host", sub_arg[0], + strerror(errno)); + goto SOCK_FAIL; + } } - sockun.sun_family = AF_UNIX; - sprintf(sockun.sun_path, "%.*s", (int)(sizeof(sockun.sun_path)-1), - sub_arg[0]); - if(connect(fd, (struct sockaddr *)(&sockun), sizeof(sockun)) == -1) + /* Handle a Unix domain socket */ + + else { - expand_string_message = string_sprintf("failed to connect to socket " - "%s: %s", sub_arg[0], strerror(errno)); - goto SOCK_FAIL; + int rc; + if ((fd = socket(PF_UNIX, SOCK_STREAM, 0)) == -1) + { + expand_string_message = string_sprintf("failed to create socket: %s", + strerror(errno)); + goto SOCK_FAIL; + } + + sockun.sun_family = AF_UNIX; + sprintf(sockun.sun_path, "%.*s", (int)(sizeof(sockun.sun_path)-1), + sub_arg[0]); + + sigalrm_seen = FALSE; + alarm(timeout); + rc = connect(fd, (struct sockaddr *)(&sockun), sizeof(sockun)); + alarm(0); + if (sigalrm_seen) + { + expand_string_message = US "socket connect timed out"; + goto SOCK_FAIL; + } + if (rc < 0) + { + expand_string_message = string_sprintf("failed to connect to socket " + "%s: %s", sub_arg[0], strerror(errno)); + goto SOCK_FAIL; + } } + DEBUG(D_expand) debug_printf("connected to socket %s\n", sub_arg[0]); /* Write the request string, if not empty */ @@ -3596,6 +4131,14 @@ while (*s != 0) } } + /* Shut down the sending side of the socket. This helps some servers to + recognise that it is their turn to do some work. Just in case some + system doesn't have this function, make it conditional. */ + + #ifdef SHUT_WR + shutdown(fd, SHUT_WR); + #endif + /* Now we need to read from the socket, under a timeout. The function that reads a file can be used. */ @@ -3604,7 +4147,7 @@ while (*s != 0) alarm(timeout); yield = cat_file(f, yield, &size, &ptr, sub_arg[3]); alarm(0); - fclose(f); + (void)fclose(f); /* After a timeout, we restore the pointer in the result, that is, make sure we add nothing from the socket. */ @@ -3612,7 +4155,7 @@ while (*s != 0) if (sigalrm_seen) { ptr = save_ptr; - expand_string_message = US"socket read timed out"; + expand_string_message = US "socket read timed out"; goto SOCK_FAIL; } } @@ -3701,7 +4244,7 @@ while (*s != 0) /* Nothing is written to the standard input. */ - close(fd_in); + (void)close(fd_in); /* Wait for the process to finish, applying the timeout, and inspect its return code for serious disasters. Simple non-zero returns are passed on. @@ -3732,7 +4275,7 @@ while (*s != 0) f = fdopen(fd_out, "rb"); lookup_value = NULL; lookup_value = cat_file(f, lookup_value, &lsize, &lptr, NULL); - fclose(f); + (void)fclose(f); } /* Process the yes/no strings; $value may be useful in both cases */ @@ -4116,7 +4659,7 @@ while (*s != 0) while (len > 0 && isspace(p[len-1])) len--; p[len] = 0; - if (*p == 0) + if (*p == 0 && !skipping) { expand_string_message = US"first argument of \"extract\" must " "not be empty"; @@ -4173,6 +4716,184 @@ while (*s != 0) } + /* Handle list operations */ + + case EITEM_FILTER: + case EITEM_MAP: + case EITEM_REDUCE: + { + int sep = 0; + int save_ptr = ptr; + uschar outsep[2] = { '\0', '\0' }; + uschar *list, *expr, *temp; + uschar *save_iterate_item = iterate_item; + uschar *save_lookup_value = lookup_value; + + while (isspace(*s)) s++; + if (*s++ != '{') goto EXPAND_FAILED_CURLY; + + list = expand_string_internal(s, TRUE, &s, skipping); + if (list == NULL) goto EXPAND_FAILED; + if (*s++ != '}') goto EXPAND_FAILED_CURLY; + + if (item_type == EITEM_REDUCE) + { + while (isspace(*s)) s++; + if (*s++ != '{') goto EXPAND_FAILED_CURLY; + temp = expand_string_internal(s, TRUE, &s, skipping); + if (temp == NULL) goto EXPAND_FAILED; + lookup_value = temp; + if (*s++ != '}') goto EXPAND_FAILED_CURLY; + } + + while (isspace(*s)) s++; + if (*s++ != '{') goto EXPAND_FAILED_CURLY; + + expr = s; + + /* For EITEM_FILTER, call eval_condition once, with result discarded (as + if scanning a "false" part). This allows us to find the end of the + condition, because if the list is empty, we won't actually evaluate the + condition for real. For EITEM_MAP and EITEM_REDUCE, do the same, using + the normal internal expansion function. */ + + if (item_type == EITEM_FILTER) + { + temp = eval_condition(expr, NULL); + if (temp != NULL) s = temp; + } + else + { + temp = expand_string_internal(s, TRUE, &s, TRUE); + } + + if (temp == NULL) + { + expand_string_message = string_sprintf("%s inside \"%s\" item", + expand_string_message, name); + goto EXPAND_FAILED; + } + + while (isspace(*s)) s++; + if (*s++ != '}') + { + expand_string_message = string_sprintf("missing } at end of condition " + "or expression inside \"%s\"", name); + goto EXPAND_FAILED; + } + + while (isspace(*s)) s++; + if (*s++ != '}') + { + expand_string_message = string_sprintf("missing } at end of \"%s\"", + name); + goto EXPAND_FAILED; + } + + /* If we are skipping, we can now just move on to the next item. When + processing for real, we perform the iteration. */ + + if (skipping) continue; + while ((iterate_item = string_nextinlist(&list, &sep, NULL, 0)) != NULL) + { + *outsep = (uschar)sep; /* Separator as a string */ + + DEBUG(D_expand) debug_printf("%s: $item = \"%s\"\n", name, iterate_item); + + if (item_type == EITEM_FILTER) + { + BOOL condresult; + if (eval_condition(expr, &condresult) == NULL) + { + iterate_item = save_iterate_item; + lookup_value = save_lookup_value; + expand_string_message = string_sprintf("%s inside \"%s\" condition", + expand_string_message, name); + goto EXPAND_FAILED; + } + DEBUG(D_expand) debug_printf("%s: condition is %s\n", name, + condresult? "true":"false"); + if (condresult) + temp = iterate_item; /* TRUE => include this item */ + else + continue; /* FALSE => skip this item */ + } + + /* EITEM_MAP and EITEM_REDUCE */ + + else + { + temp = expand_string_internal(expr, TRUE, NULL, skipping); + if (temp == NULL) + { + iterate_item = save_iterate_item; + expand_string_message = string_sprintf("%s inside \"%s\" item", + expand_string_message, name); + goto EXPAND_FAILED; + } + if (item_type == EITEM_REDUCE) + { + lookup_value = temp; /* Update the value of $value */ + continue; /* and continue the iteration */ + } + } + + /* We reach here for FILTER if the condition is true, always for MAP, + and never for REDUCE. The value in "temp" is to be added to the output + list that is being created, ensuring that any occurrences of the + separator character are doubled. Unless we are dealing with the first + item of the output list, add in a space if the new item begins with the + separator character, or is an empty string. */ + + if (ptr != save_ptr && (temp[0] == *outsep || temp[0] == 0)) + yield = string_cat(yield, &size, &ptr, US" ", 1); + + /* Add the string in "temp" to the output list that we are building, + This is done in chunks by searching for the separator character. */ + + for (;;) + { + size_t seglen = Ustrcspn(temp, outsep); + yield = string_cat(yield, &size, &ptr, temp, seglen + 1); + + /* If we got to the end of the string we output one character + too many; backup and end the loop. Otherwise arrange to double the + separator. */ + + if (temp[seglen] == '\0') { ptr--; break; } + yield = string_cat(yield, &size, &ptr, outsep, 1); + temp += seglen + 1; + } + + /* Output a separator after the string: we will remove the redundant + final one at the end. */ + + yield = string_cat(yield, &size, &ptr, outsep, 1); + } /* End of iteration over the list loop */ + + /* REDUCE has generated no output above: output the final value of + $value. */ + + if (item_type == EITEM_REDUCE) + { + yield = string_cat(yield, &size, &ptr, lookup_value, + Ustrlen(lookup_value)); + lookup_value = save_lookup_value; /* Restore $value */ + } + + /* FILTER and MAP generate lists: if they have generated anything, remove + the redundant final separator. Even though an empty item at the end of a + list does not count, this is tidier. */ + + else if (ptr != save_ptr) ptr--; + + /* Restore preserved $item */ + + iterate_item = save_iterate_item; + continue; + } + + /* If ${dlfunc support is configured, handle calling dynamically-loaded functions, unless locked out at this time. Syntax is ${dlfunc{file}{func}} or ${dlfunc{file}{func}{arg}} or ${dlfunc{file}{func}{arg1}{arg2}} or up to @@ -4249,8 +4970,10 @@ while (*s != 0) returns OK, we have a replacement string; if it returns DEFER then expansion has failed in a non-forced manner; if it returns FAIL then failure was forced; if it returns ERROR or any other value there's a - problem, so panic slightly. */ + problem, so panic slightly. In any case, assume that the function has + side-effects on the store that must be preserved. */ + resetok = FALSE; result = NULL; for (argc = 0; argv[argc] != NULL; argc++); status = func(&result, argc - 2, &argv[2]); @@ -4331,6 +5054,8 @@ while (*s != 0) continue; } + /* Note that for Darwin and Cygwin, BASE_62 actually has the value 36 */ + case EOP_BASE62D: { uschar buf[16]; @@ -4342,10 +5067,11 @@ while (*s != 0) if (t == NULL) { expand_string_message = string_sprintf("argument for base62d " - "operator is \"%s\", which is not a base 62 number", sub); + "operator is \"%s\", which is not a base %d number", sub, + BASE_62); goto EXPAND_FAILED; } - n = n * 62 + (t - base62_chars); + n = n * BASE_62 + (t - base62_chars); } (void)sprintf(CS buf, "%ld", n); yield = string_cat(yield, &size, &ptr, buf, Ustrlen(buf)); @@ -4530,6 +5256,69 @@ while (*s != 0) continue; } + case EOP_ADDRESSES: + { + uschar outsep[2] = { ':', '\0' }; + uschar *address, *error; + int save_ptr = ptr; + int start, end, domain; /* Not really used */ + + while (isspace(*sub)) sub++; + if (*sub == '>') { *outsep = *++sub; ++sub; } + parse_allow_group = TRUE; + + for (;;) + { + uschar *p = parse_find_address_end(sub, FALSE); + uschar saveend = *p; + *p = '\0'; + address = parse_extract_address(sub, &error, &start, &end, &domain, + FALSE); + *p = saveend; + + /* Add the address to the output list that we are building. This is + done in chunks by searching for the separator character. At the + start, unless we are dealing with the first address of the output + list, add in a space if the new address begins with the separator + character, or is an empty string. */ + + if (address != NULL) + { + if (ptr != save_ptr && address[0] == *outsep) + yield = string_cat(yield, &size, &ptr, US" ", 1); + + for (;;) + { + size_t seglen = Ustrcspn(address, outsep); + yield = string_cat(yield, &size, &ptr, address, seglen + 1); + + /* If we got to the end of the string we output one character + too many. */ + + if (address[seglen] == '\0') { ptr--; break; } + yield = string_cat(yield, &size, &ptr, outsep, 1); + address += seglen + 1; + } + + /* Output a separator after the string: we will remove the + redundant final one at the end. */ + + yield = string_cat(yield, &size, &ptr, outsep, 1); + } + + if (saveend == '\0') break; + sub = p + 1; + } + + /* If we have generated anything, remove the redundant final + separator. */ + + if (ptr != save_ptr) ptr--; + parse_allow_group = FALSE; + continue; + } + + /* quote puts a string in quotes if it is empty or contains anything other than alphamerics, underscore, dot, or hyphen. @@ -4636,11 +5425,28 @@ while (*s != 0) { uschar buffer[2048]; uschar *string = parse_quote_2047(sub, Ustrlen(sub), headers_charset, - buffer, sizeof(buffer)); + buffer, sizeof(buffer), FALSE); yield = string_cat(yield, &size, &ptr, string, Ustrlen(string)); continue; } + /* RFC 2047 decode */ + + case EOP_RFC2047D: + { + int len; + uschar *error; + uschar *decoded = rfc2047_decode(sub, check_rfc2047_length, + headers_charset, '?', &len, &error); + if (error != NULL) + { + expand_string_message = error; + goto EXPAND_FAILED; + } + yield = string_cat(yield, &size, &ptr, decoded, len); + continue; + } + /* from_utf8 converts UTF-8 to 8859-1, turning non-existent chars into underscores */ @@ -4689,6 +5495,20 @@ while (*s != 0) /* Handle time period formating */ + case EOP_TIME_EVAL: + { + int n = readconf_readtime(sub, 0, FALSE); + if (n < 0) + { + expand_string_message = string_sprintf("string \"%s\" is not an " + "Exim time interval in \"%s\" operator", sub, name); + goto EXPAND_FAILED; + } + sprintf(CS var_buffer, "%d", n); + yield = string_cat(yield, &size, &ptr, var_buffer, Ustrlen(var_buffer)); + continue; + } + case EOP_TIME_INTERVAL: { int n; @@ -4825,6 +5645,12 @@ while (*s != 0) mode_t mode; struct stat st; + if ((expand_forbid & RDO_EXISTS) != 0) + { + expand_string_message = US"Use of the stat() expansion is not permitted"; + goto EXPAND_FAILED; + } + if (stat(CS sub, &st) < 0) { expand_string_message = string_sprintf("stat(%s) failed: %s", @@ -4885,7 +5711,7 @@ while (*s != 0) int newsize = 0; if (ptr == 0) { - store_reset(yield); + if (resetok) store_reset(yield); yield = NULL; size = 0; } @@ -4894,6 +5720,7 @@ while (*s != 0) { expand_string_message = string_sprintf("unknown variable in \"${%s}\"", name); + check_variable_error_message(name); goto EXPAND_FAILED; } len = Ustrlen(value); @@ -4939,7 +5766,7 @@ if (left != NULL) *left = s; In many cases the final string will be the first one that was got and so there will be optimal store usage. */ -store_reset(yield + ptr + 1); +if (resetok) store_reset(yield + ptr + 1); DEBUG(D_expand) { debug_printf("expanding: %.*s\n result: %s\n", (int)(s - string), string, @@ -5019,22 +5846,26 @@ return yield; /* Expand a string, and convert the result into an integer. -Argument: the string to be expanded +Arguments: + string the string to be expanded + isplus TRUE if a non-negative number is expected Returns: the integer value, or -1 for an expansion error ) in both cases, message in -2 for an integer interpretation error ) expand_string_message - + expand_string_message is set NULL for an OK integer */ int -expand_string_integer(uschar *string) +expand_string_integer(uschar *string, BOOL isplus) { long int value; uschar *s = expand_string(string); uschar *msg = US"invalid integer \"%s\""; uschar *endptr; +/* If expansion failed, expand_string_message will be set. */ + if (s == NULL) return -1; /* On an overflow, strtol() returns LONG_MAX or LONG_MIN, and sets errno @@ -5042,12 +5873,17 @@ to ERANGE. When there isn't an overflow, errno is not changed, at least on some systems, so we set it zero ourselves. */ errno = 0; -value = strtol(CS s, CSS &endptr, 0); +expand_string_message = NULL; /* Indicates no error */ +value = strtol(CS s, CSS &endptr, 10); if (endptr == s) { msg = US"integer expected but \"%s\" found"; } +else if (value < 0 && isplus) + { + msg = US"non-negative integer expected but \"%s\" found"; + } else { /* Ensure we can cast this down to an int */