X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/ff790e47f2de6f4d6d48148e1d5a67da8e93c446..4d8d62b965c7379e85aeb6e43534c89d06099ad2:/src/src/expand.c diff --git a/src/src/expand.c b/src/src/expand.c index ca340bb1b..edef45dba 100644 --- a/src/src/expand.c +++ b/src/src/expand.c @@ -1,10 +1,8 @@ -/* $Cambridge: exim/src/src/expand.c,v 1.35 2005/06/22 15:44:38 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. */ @@ -13,12 +11,20 @@ #include "exim.h" +/* Recursively called function */ + +static uschar *expand_string_internal(uschar *, BOOL, uschar **, BOOL, 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 +34,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); @@ -48,38 +102,46 @@ static uschar *expand_string_internal(uschar *, BOOL, uschar **, BOOL); alphabetical order. */ static uschar *item_table[] = { + US"acl", 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", US"tr" }; enum { + EITEM_ACL, 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,21 @@ static uschar *op_table_underscore[] = { US"from_utf8", US"local_part", US"quote_local_part", + US"reverse_ip", + US"time_eval", US"time_interval"}; enum { EOP_FROM_UTF8, EOP_LOCAL_PART, EOP_QUOTE_LOCAL_PART, + EOP_REVERSE_IP, + EOP_TIME_EVAL, EOP_TIME_INTERVAL }; static uschar *op_table_main[] = { US"address", + US"addresses", US"base62", US"base62d", US"domain", @@ -114,15 +181,20 @@ static uschar *op_table_main[] = { US"h", US"hash", US"hex2b64", + US"hexquote", US"l", US"lc", US"length", + US"listcount", + US"listnamed", US"mask", US"md5", US"nh", US"nhash", US"quote", + US"randint", US"rfc2047", + US"rfc2047d", US"rxquote", US"s", US"sha1", @@ -134,6 +206,7 @@ static uschar *op_table_main[] = { enum { EOP_ADDRESS = sizeof(op_table_underscore)/sizeof(uschar *), + EOP_ADDRESSES, EOP_BASE62, EOP_BASE62D, EOP_DOMAIN, @@ -144,15 +217,20 @@ enum { EOP_H, EOP_HASH, EOP_HEX2B64, + EOP_HEXQUOTE, EOP_L, EOP_LC, EOP_LENGTH, + EOP_LISTCOUNT, + EOP_LISTNAMED, EOP_MASK, EOP_MD5, EOP_NH, EOP_NHASH, EOP_QUOTE, + EOP_RANDINT, EOP_RFC2047, + EOP_RFC2047D, EOP_RXQUOTE, EOP_S, EOP_SHA1, @@ -173,17 +251,24 @@ static uschar *cond_table[] = { US"==", /* Backward compatibility */ US">", US">=", + US"acl", US"and", + US"bool", + US"bool_lax", US"crypteq", US"def", US"eq", US"eqi", US"exists", US"first_delivery", + US"forall", + US"forany", US"ge", US"gei", US"gt", US"gti", + US"inlist", + US"inlisti", US"isip", US"isip4", US"isip6", @@ -212,17 +297,24 @@ enum { ECOND_NUM_EE, ECOND_NUM_G, ECOND_NUM_GE, + ECOND_ACL, ECOND_AND, + ECOND_BOOL, + ECOND_BOOL_LAX, ECOND_CRYPTEQ, ECOND_DEF, ECOND_STR_EQ, ECOND_STR_EQI, ECOND_EXISTS, ECOND_FIRST_DELIVERY, + ECOND_FORALL, + ECOND_FORANY, ECOND_STR_GE, ECOND_STR_GEI, ECOND_STR_GT, ECOND_STR_GTI, + ECOND_INLIST, + ECOND_INLISTI, ECOND_ISIP, ECOND_ISIP4, ECOND_ISIP6, @@ -248,9 +340,9 @@ enum { /* Type for main variable table */ typedef struct { - char *name; - int type; - void *value; + const char *name; + int type; + void *value; } var_entry; /* Type for entries pointing to address/length pairs. Not currently @@ -269,16 +361,18 @@ enum { vtype_ino, /* value is address of ino_t (not always an int) */ vtype_uid, /* value is address of uid_t (not always an int) */ vtype_gid, /* value is address of gid_t (not always an int) */ + vtype_bool, /* value is address of bool */ 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 */ + vtype_string_func, /* value is string returned by given function */ vtype_todbsdin, /* value not used; generate BSD inbox tod */ vtype_tode, /* value not used; generate tod in epoch format */ + vtype_todel, /* value not used; generate tod in epoch/usec format */ vtype_todf, /* value not used; generate full tod */ vtype_todl, /* value not used; generate log tod */ vtype_todlf, /* value not used; generate log file datestamp tod */ @@ -290,41 +384,39 @@ enum { vtype_load_avg, /* value not used; result is int from os_getloadavg */ vtype_pspace, /* partition space; value is T/F for spool/log */ vtype_pinodes /* partition inodes; value is T/F for spool/log */ -#ifdef EXPERIMENTAL_DOMAINKEYS - ,vtype_dk_verify /* Serve request out of DomainKeys verification structure */ -#endif + #ifndef DISABLE_DKIM + ,vtype_dkim /* Lookup of value in DKIM signature */ + #endif }; +static uschar * fn_recipients(void); + /* 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_arg1", vtype_stringptr, &acl_arg[0] }, + { "acl_arg2", vtype_stringptr, &acl_arg[1] }, + { "acl_arg3", vtype_stringptr, &acl_arg[2] }, + { "acl_arg4", vtype_stringptr, &acl_arg[3] }, + { "acl_arg5", vtype_stringptr, &acl_arg[4] }, + { "acl_arg6", vtype_stringptr, &acl_arg[5] }, + { "acl_arg7", vtype_stringptr, &acl_arg[6] }, + { "acl_arg8", vtype_stringptr, &acl_arg[7] }, + { "acl_arg9", vtype_stringptr, &acl_arg[8] }, + { "acl_narg", vtype_int, &acl_narg }, { "acl_verify_message", vtype_stringptr, &acl_verify_message }, { "address_data", vtype_stringptr, &deliver_address_data }, { "address_file", vtype_stringptr, &address_file }, { "address_pipe", vtype_stringptr, &address_pipe }, + { "authenticated_fail_id",vtype_stringptr, &authenticated_fail_id }, { "authenticated_id", vtype_stringptr, &authenticated_id }, { "authenticated_sender",vtype_stringptr, &authenticated_sender }, { "authentication_failed",vtype_int, &authentication_failed }, +#ifdef WITH_CONTENT_SCAN + { "av_failed", vtype_int, &av_failed }, +#endif #ifdef EXPERIMENTAL_BRIGHTMAIL { "bmi_alt_location", vtype_stringptr, &bmi_alt_location }, { "bmi_base64_tracker_verdict", vtype_stringptr, &bmi_base64_tracker_verdict }, @@ -340,24 +432,44 @@ 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 }, #endif -#ifdef EXPERIMENTAL_DOMAINKEYS - { "dk_domain", vtype_stringptr, &dk_signing_domain }, - { "dk_is_signed", vtype_dk_verify, NULL }, - { "dk_result", vtype_dk_verify, NULL }, - { "dk_selector", vtype_stringptr, &dk_signing_selector }, - { "dk_sender", vtype_dk_verify, NULL }, - { "dk_sender_domain", vtype_dk_verify, NULL }, - { "dk_sender_local_part",vtype_dk_verify, NULL }, - { "dk_sender_source", vtype_dk_verify, NULL }, - { "dk_signsall", vtype_dk_verify, NULL }, - { "dk_status", vtype_dk_verify, NULL }, - { "dk_testing", vtype_dk_verify, NULL }, +#ifndef DISABLE_DKIM + { "dkim_algo", vtype_dkim, (void *)DKIM_ALGO }, + { "dkim_bodylength", vtype_dkim, (void *)DKIM_BODYLENGTH }, + { "dkim_canon_body", vtype_dkim, (void *)DKIM_CANON_BODY }, + { "dkim_canon_headers", vtype_dkim, (void *)DKIM_CANON_HEADERS }, + { "dkim_copiedheaders", vtype_dkim, (void *)DKIM_COPIEDHEADERS }, + { "dkim_created", vtype_dkim, (void *)DKIM_CREATED }, + { "dkim_cur_signer", vtype_stringptr, &dkim_cur_signer }, + { "dkim_domain", vtype_stringptr, &dkim_signing_domain }, + { "dkim_expires", vtype_dkim, (void *)DKIM_EXPIRES }, + { "dkim_headernames", vtype_dkim, (void *)DKIM_HEADERNAMES }, + { "dkim_identity", vtype_dkim, (void *)DKIM_IDENTITY }, + { "dkim_key_granularity",vtype_dkim, (void *)DKIM_KEY_GRANULARITY }, + { "dkim_key_nosubdomains",vtype_dkim, (void *)DKIM_NOSUBDOMAINS }, + { "dkim_key_notes", vtype_dkim, (void *)DKIM_KEY_NOTES }, + { "dkim_key_srvtype", vtype_dkim, (void *)DKIM_KEY_SRVTYPE }, + { "dkim_key_testing", vtype_dkim, (void *)DKIM_KEY_TESTING }, + { "dkim_selector", vtype_stringptr, &dkim_signing_selector }, + { "dkim_signers", vtype_stringptr, &dkim_signers }, + { "dkim_verify_reason", vtype_dkim, (void *)DKIM_VERIFY_REASON }, + { "dkim_verify_status", vtype_dkim, (void *)DKIM_VERIFY_STATUS}, +#endif +#ifdef EXPERIMENTAL_DMARC + { "dmarc_ar_header", vtype_stringptr, &dmarc_ar_header }, + { "dmarc_status", vtype_stringptr, &dmarc_status }, + { "dmarc_status_text", vtype_stringptr, &dmarc_status_text }, + { "dmarc_used_domain", vtype_stringptr, &dmarc_used_domain }, #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 }, @@ -368,6 +480,7 @@ static var_entry var_table[] = { #ifdef WITH_OLD_DEMIME { "found_extension", vtype_stringptr, &found_extension }, #endif + { "headers_added", vtype_string_func, &fn_hdrs_added }, { "home", vtype_stringptr, &deliver_home }, { "host", vtype_stringptr, &deliver_host }, { "host_address", vtype_stringptr, &deliver_host_address }, @@ -377,6 +490,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 +509,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,11 +566,13 @@ 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 }, { "recipient_verify_failure",vtype_stringptr,&recipient_verify_failure }, - { "recipients", vtype_recipients, NULL }, + { "recipients", vtype_string_func, &fn_recipients }, { "recipients_count", vtype_int, &recipients_count }, #ifdef WITH_CONTENT_SCAN { "regex_match_string", vtype_stringptr, ®ex_match_string }, @@ -461,6 +580,7 @@ static var_entry var_table[] = { { "reply_address", vtype_reply, NULL }, { "return_path", vtype_stringptr, &return_path }, { "return_size_limit", vtype_int, &bounce_return_size_limit }, + { "router_name", vtype_stringptr, &router_name }, { "runrc", vtype_int, &runrc }, { "self_hostname", vtype_stringptr, &self_hostname }, { "sender_address", vtype_stringptr, &sender_address }, @@ -472,6 +592,7 @@ static var_entry var_table[] = { { "sender_helo_name", vtype_stringptr, &sender_helo_name }, { "sender_host_address", vtype_stringptr, &sender_host_address }, { "sender_host_authenticated",vtype_stringptr, &sender_host_authenticated }, + { "sender_host_dnssec", vtype_bool, &sender_host_dnssec }, { "sender_host_name", vtype_host_lookup, NULL }, { "sender_host_port", vtype_int, &sender_host_port }, { "sender_ident", vtype_stringptr, &sender_ident }, @@ -480,8 +601,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 +625,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 }, @@ -516,16 +643,41 @@ static var_entry var_table[] = { { "srs_status", vtype_stringptr, &srs_status }, #endif { "thisaddress", vtype_stringptr, &filter_thisaddress }, - { "tls_certificate_verified", vtype_int, &tls_certificate_verified }, - { "tls_cipher", vtype_stringptr, &tls_cipher }, - { "tls_peerdn", vtype_stringptr, &tls_peerdn }, + + /* The non-(in,out) variables are now deprecated */ + { "tls_bits", vtype_int, &tls_in.bits }, + { "tls_certificate_verified", vtype_int, &tls_in.certificate_verified }, + { "tls_cipher", vtype_stringptr, &tls_in.cipher }, + + { "tls_in_bits", vtype_int, &tls_in.bits }, + { "tls_in_certificate_verified", vtype_int, &tls_in.certificate_verified }, + { "tls_in_cipher", vtype_stringptr, &tls_in.cipher }, + { "tls_in_peerdn", vtype_stringptr, &tls_in.peerdn }, +#if defined(SUPPORT_TLS) && !defined(USE_GNUTLS) + { "tls_in_sni", vtype_stringptr, &tls_in.sni }, +#endif + { "tls_out_bits", vtype_int, &tls_out.bits }, + { "tls_out_certificate_verified", vtype_int,&tls_out.certificate_verified }, + { "tls_out_cipher", vtype_stringptr, &tls_out.cipher }, + { "tls_out_peerdn", vtype_stringptr, &tls_out.peerdn }, +#if defined(SUPPORT_TLS) && !defined(USE_GNUTLS) + { "tls_out_sni", vtype_stringptr, &tls_out.sni }, +#endif + + { "tls_peerdn", vtype_stringptr, &tls_in.peerdn }, /* mind the alphabetical order! */ +#if defined(SUPPORT_TLS) && !defined(USE_GNUTLS) + { "tls_sni", vtype_stringptr, &tls_in.sni }, /* mind the alphabetical order! */ +#endif + { "tod_bsdinbox", vtype_todbsdin, NULL }, { "tod_epoch", vtype_tode, NULL }, + { "tod_epoch_l", vtype_todel, NULL }, { "tod_full", vtype_todf, NULL }, { "tod_log", vtype_todl, NULL }, { "tod_logfile", vtype_todlf, NULL }, { "tod_zone", vtype_todzone, NULL }, { "tod_zulu", vtype_todzulu, NULL }, + { "transport_name", vtype_stringptr, &transport_name }, { "value", vtype_stringptr, &lookup_value }, { "version_number", vtype_stringptr, &version_string }, { "warn_message_delay", vtype_stringptr, &warnmsg_delay }, @@ -542,9 +694,9 @@ static BOOL malformed_header; /* For textual hashes */ -static char *hashcodes = "abcdefghijklmnopqrtsuvwxyz" - "ABCDEFGHIJKLMNOPQRSTUVWXYZ" - "0123456789"; +static const char *hashcodes = "abcdefghijklmnopqrtsuvwxyz" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ" + "0123456789"; enum { HMAC_MD5, HMAC_SHA1 }; @@ -643,8 +795,13 @@ return -1; /* This function is called to expand a string, and test the result for a "true" or "false" value. Failure of the expansion yields FALSE; logged unless it was a -forced fail or lookup defer. All store used by the function can be released on -exit. +forced fail or lookup defer. + +We used to release all store used, but this is not not safe due +to ${dlfunc } and ${acl }. In any case expand_string_internal() +is reasonably careful to release what it can. + +The actual false-value tests should be replicated for ECOND_BOOL_LAX. Arguments: condition the condition string @@ -658,7 +815,6 @@ BOOL expand_check_condition(uschar *condition, uschar *m1, uschar *m2) { int rc; -void *reset_point = store_get(0); uschar *ss = expand_string(condition); if (ss == NULL) { @@ -669,12 +825,86 @@ if (ss == NULL) } rc = ss[0] != 0 && Ustrcmp(ss, "0") != 0 && strcmpic(ss, US"no") != 0 && strcmpic(ss, US"false") != 0; -store_reset(reset_point); return rc; } + +/************************************************* +* Pseudo-random number generation * +*************************************************/ + +/* Pseudo-random number generation. The result is not "expected" to be +cryptographically strong but not so weak that someone will shoot themselves +in the foot using it as a nonce in some email header scheme or whatever +weirdness they'll twist this into. The result should ideally handle fork(). + +However, if we're stuck unable to provide this, then we'll fall back to +appallingly bad randomness. + +If SUPPORT_TLS is defined then this will not be used except as an emergency +fallback. + +Arguments: + max range maximum +Returns a random number in range [0, max-1] +*/ + +#ifdef SUPPORT_TLS +# define vaguely_random_number vaguely_random_number_fallback +#endif +int +vaguely_random_number(int max) +{ +#ifdef SUPPORT_TLS +# undef vaguely_random_number +#endif + static pid_t pid = 0; + pid_t p2; +#if defined(HAVE_SRANDOM) && !defined(HAVE_SRANDOMDEV) + struct timeval tv; +#endif + + p2 = getpid(); + if (p2 != pid) + { + if (pid != 0) + { + +#ifdef HAVE_ARC4RANDOM + /* cryptographically strong randomness, common on *BSD platforms, not + so much elsewhere. Alas. */ + arc4random_stir(); +#elif defined(HAVE_SRANDOM) || defined(HAVE_SRANDOMDEV) +#ifdef HAVE_SRANDOMDEV + /* uses random(4) for seeding */ + srandomdev(); +#else + gettimeofday(&tv, NULL); + srandom(tv.tv_sec | tv.tv_usec | getpid()); +#endif +#else + /* Poor randomness and no seeding here */ +#endif + + } + pid = p2; + } + +#ifdef HAVE_ARC4RANDOM + return arc4random() % max; +#elif defined(HAVE_SRANDOM) || defined(HAVE_SRANDOMDEV) + return random() % max; +#else + /* This one returns a 16-bit number, definitely not crypto-strong */ + return random_number(max); +#endif +} + + + + /************************************************* * Pick out a name from a string * *************************************************/ @@ -1092,7 +1322,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 +1366,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 +1383,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 +1392,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 +1412,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 +1425,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 +1432,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" @@ -1218,6 +1459,34 @@ return yield; +/************************************************* +* Return list of recipients * +*************************************************/ +/* A recipients list is available only during system message filtering, +during ACL processing after DATA, and while expanding pipe commands +generated from a system filter, but not elsewhere. */ + +static uschar * +fn_recipients(void) +{ +if (!enable_dollar_recipients) return NULL; else + { + int size = 128; + int ptr = 0; + int i; + uschar * s = store_get(size); + for (i = 0; i < recipients_count; i++) + { + if (i != 0) s = string_cat(s, &size, &ptr, US", ", 2); + s = string_cat(s, &size, &ptr, recipients_list[i].address, + Ustrlen(recipients_list[i].address)); + } + s[ptr] = 0; /* string_cat() leaves room */ + return s; + } +} + + /************************************************* * Find value of a variable * *************************************************/ @@ -1246,6 +1515,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,57 +1557,12 @@ 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""; switch (var_table[middle].type) { -#ifdef EXPERIMENTAL_DOMAINKEYS - - case vtype_dk_verify: - if (dk_verify_block == NULL) return US""; - s = NULL; - if (Ustrcmp(var_table[middle].name, "dk_result") == 0) - s = dk_verify_block->result_string; - if (Ustrcmp(var_table[middle].name, "dk_sender") == 0) - s = dk_verify_block->address; - if (Ustrcmp(var_table[middle].name, "dk_sender_domain") == 0) - s = dk_verify_block->domain; - if (Ustrcmp(var_table[middle].name, "dk_sender_local_part") == 0) - s = dk_verify_block->local_part; - - if (Ustrcmp(var_table[middle].name, "dk_sender_source") == 0) - switch(dk_verify_block->address_source) { - case DK_EXIM_ADDRESS_NONE: s = US"0"; break; - case DK_EXIM_ADDRESS_FROM_FROM: s = US"from"; break; - case DK_EXIM_ADDRESS_FROM_SENDER: s = US"sender"; break; - } - - if (Ustrcmp(var_table[middle].name, "dk_status") == 0) - switch(dk_verify_block->result) { - case DK_EXIM_RESULT_ERR: s = US"error"; break; - case DK_EXIM_RESULT_BAD_FORMAT: s = US"bad format"; break; - case DK_EXIM_RESULT_NO_KEY: s = US"no key"; break; - case DK_EXIM_RESULT_NO_SIGNATURE: s = US"no signature"; break; - case DK_EXIM_RESULT_REVOKED: s = US"revoked"; break; - case DK_EXIM_RESULT_NON_PARTICIPANT: s = US"non-participant"; break; - case DK_EXIM_RESULT_GOOD: s = US"good"; break; - case DK_EXIM_RESULT_BAD: s = US"bad"; break; - } - - if (Ustrcmp(var_table[middle].name, "dk_signsall") == 0) - s = (dk_verify_block->signsall)? US"1" : US"0"; - - if (Ustrcmp(var_table[middle].name, "dk_testing") == 0) - s = (dk_verify_block->testing)? US"1" : US"0"; - - if (Ustrcmp(var_table[middle].name, "dk_is_signed") == 0) - s = (dk_verify_block->is_signed)? US"1" : US"0"; - - return (s == NULL)? US"" : s; -#endif - case vtype_filter_int: if (!filter_running) return NULL; /* Fall through */ @@ -1328,6 +1583,10 @@ while (last > first) sprintf(CS var_buffer, "%ld", (long int)(*(uid_t *)(var_table[middle].value))); /* uid */ return var_buffer; + case vtype_bool: + sprintf(CS var_buffer, "%s", *(BOOL *)(var_table[middle].value) ? "yes" : "no"); /* bool */ + return var_buffer; + case vtype_stringptr: /* Pointer to string */ s = *((uschar **)(var_table[middle].value)); return (s == NULL)? US"" : s; @@ -1337,7 +1596,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 */ @@ -1352,8 +1611,8 @@ while (last > first) domain = Ustrrchr(s, '@'); if (domain == NULL) return s; if (domain - s > sizeof(var_buffer) - 1) - log_write(0, LOG_MAIN|LOG_PANIC_DIE, "local part longer than %d in " - "string expansion", sizeof(var_buffer)); + log_write(0, LOG_MAIN|LOG_PANIC_DIE, "local part longer than " SIZE_T_FMT + " in string expansion", sizeof(var_buffer)); Ustrncpy(var_buffer, s, domain - s); var_buffer[domain - s] = 0; return var_buffer; @@ -1367,6 +1626,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 +1655,15 @@ while (last > first) if (len > 0) { body[len] = 0; - while (len > 0) + if (message_body_newlines) /* Separate loops for efficiency */ + { + while (len > 0) + { if (body[--len] == 0) body[len] = ' '; } + } + else { - if (body[--len] == '\n' || body[len] == 0) body[len] = ' '; + while (len > 0) + { if (body[--len] == '\n' || body[len] == 0) body[len] = ' '; } } } } @@ -1407,6 +1675,9 @@ while (last > first) case vtype_tode: /* Unix epoch time of day */ return tod_stamp(tod_epoch); + case vtype_todel: /* Unix epoch/usec time of day */ + return tod_stamp(tod_epoch_l); + case vtype_todf: /* Full time of day */ return tod_stamp(tod_full); @@ -1420,35 +1691,32 @@ while (last > first) return tod_stamp(tod_zulu); case vtype_todlf: /* Log file datestamp tod */ - return tod_stamp(tod_log_datestamp); + return tod_stamp(tod_log_datestamp_daily); 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, - during ACL processing after DATA, and while expanding pipe commands - generated from a system filter, but not elsewhere. */ - - case vtype_recipients: - if (!enable_dollar_recipients) return NULL; else + case vtype_string_func: { - int size = 128; - int ptr = 0; - int i; - s = store_get(size); - for (i = 0; i < recipients_count; i++) - { - if (i != 0) s = string_cat(s, &size, &ptr, US", ", 2); - s = string_cat(s, &size, &ptr, recipients_list[i].address, - Ustrlen(recipients_list[i].address)); - } - s[ptr] = 0; /* string_cat() leaves room */ + uschar * (*fn)() = var_table[middle].value; + return fn(); } - return s; case vtype_pspace: { @@ -1465,6 +1733,12 @@ while (last > first) sprintf(CS var_buffer, "%d", inodes); } return var_buffer; + + #ifndef DISABLE_DKIM + case vtype_dkim: + return dkim_exim_expand_query((int)(long)var_table[middle].value); + #endif + } } @@ -1474,6 +1748,31 @@ return NULL; /* Unknown variable name */ +void +modify_variable(uschar *name, void * value) +{ +int first = 0; +int last = var_table_size; + +while (last > first) + { + int middle = (first + last)/2; + int c = Ustrcmp(name, var_table[middle].name); + + if (c > 0) { first = middle + 1; continue; } + if (c < 0) { last = middle; continue; } + + /* Found an existing variable; change the item it refers to */ + var_table[middle].value = value; + return; + } +return; /* Unknown variable name, fail silently */ +} + + + + + /************************************************* * Read and expand substrings * *************************************************/ @@ -1513,7 +1812,7 @@ for (i = 0; i < n; i++) sub[i] = NULL; break; } - sub[i] = expand_string_internal(s+1, TRUE, &s, skipping); + sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE); if (sub[i] == NULL) return 3; if (*s++ != '}') return 1; while (isspace(*s)) s++; @@ -1536,6 +1835,85 @@ 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"); +} + + + +/* +Load args from sub array to globals, and call acl_check(). +Sub array will be corrupted on return. + +Returns: OK access is granted by an ACCEPT verb + DISCARD access is granted by a DISCARD verb + FAIL access is denied + FAIL_DROP access is denied; drop the connection + DEFER can't tell at the moment + ERROR disaster +*/ +static int +eval_acl(uschar ** sub, int nsub, uschar ** user_msgp) +{ +int i; +uschar *tmp; +int sav_narg = acl_narg; +int ret; +extern int acl_where; + +if(--nsub > sizeof(acl_arg)/sizeof(*acl_arg)) nsub = sizeof(acl_arg)/sizeof(*acl_arg); +for (i = 0; i < nsub && sub[i+1]; i++) + { + tmp = acl_arg[i]; + acl_arg[i] = sub[i+1]; /* place callers args in the globals */ + sub[i+1] = tmp; /* stash the old args using our caller's storage */ + } +acl_narg = i; +while (i < nsub) + { + sub[i+1] = acl_arg[i]; + acl_arg[i++] = NULL; + } + +DEBUG(D_expand) + debug_printf("expanding: acl: %s arg: %s%s\n", + sub[0], + acl_narg>0 ? acl_arg[0] : US"", + acl_narg>1 ? " +more" : ""); + +ret = acl_eval(acl_where, sub[0], user_msgp, &tmp); + +for (i = 0; i < nsub; i++) + acl_arg[i] = sub[i+1]; /* restore old args */ +acl_narg = sav_narg; + +return ret; +} + + + + /************************************************* * Read and evaluate a condition * *************************************************/ @@ -1558,11 +1936,12 @@ eval_condition(uschar *s, BOOL *yield) BOOL testfor = TRUE; BOOL tempcond, combined_cond; BOOL *subcondptr; +BOOL sub2_honour_dollar = TRUE; int i, rc, cond_type, roffset; -int num[2]; +int_eximarith_t num[2]; struct stat statbuf; uschar name[256]; -uschar *sub[4]; +uschar *sub[10]; const pcre *re; const uschar *rerror; @@ -1617,7 +1996,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 +2008,8 @@ switch(cond_type) Ustrncmp(name, "bheader_", 8) == 0) { s = read_header_name(name, 256, s); + /* {-for-text-editors */ + if (Ustrchr(name, '}') != NULL) malformed_header = TRUE; if (yield != NULL) *yield = (find_header(name, TRUE, NULL, FALSE, NULL) != NULL) == testfor; } @@ -1642,6 +2025,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; @@ -1684,10 +2068,11 @@ switch(cond_type) case ECOND_PWCHECK: while (isspace(*s)) s++; - if (*s != '{') goto COND_FAILED_CURLY_START; + if (*s != '{') goto COND_FAILED_CURLY_START; /* }-for-text-editors */ - sub[0] = expand_string_internal(s+1, TRUE, &s, yield == NULL); + sub[0] = expand_string_internal(s+1, TRUE, &s, yield == NULL, TRUE); if (sub[0] == NULL) return NULL; + /* {-for-text-editors */ if (*s++ != '}') goto COND_FAILED_CURLY_END; if (yield == NULL) return s; /* No need to run the test if skipping */ @@ -1707,7 +2092,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; @@ -1763,19 +2148,71 @@ switch(cond_type) return s; + /* call ACL (in a conditional context). Accept true, deny false. + Defer is a forced-fail. Anything set by message= goes to $value. + Up to ten parameters are used; we use the braces round the name+args + like the saslauthd condition does, to permit a variable number of args. + See also the expansion-item version EITEM_ACL and the traditional + acl modifier ACLC_ACL. + */ + + case ECOND_ACL: + /* ${if acl {{name}{arg1}{arg2}...} {yes}{no}} */ + { + uschar *user_msg; + BOOL cond = FALSE; + int size = 0; + int ptr = 0; + + while (isspace(*s)) s++; + if (*s++ != '{') goto COND_FAILED_CURLY_START; /*}*/ + + switch(read_subs(sub, sizeof(sub)/sizeof(*sub), 1, + &s, yield == NULL, TRUE, US"acl")) + { + case 1: expand_string_message = US"too few arguments or bracketing " + "error for acl"; + case 2: + case 3: return NULL; + } + + if (yield != NULL) switch(eval_acl(sub, sizeof(sub)/sizeof(*sub), &user_msg)) + { + case OK: + cond = TRUE; + case FAIL: + lookup_value = NULL; + if (user_msg) + { + lookup_value = string_cat(NULL, &size, &ptr, user_msg, Ustrlen(user_msg)); + lookup_value[ptr] = '\0'; + } + *yield = cond == testfor; + break; + + case DEFER: + expand_string_forcedfail = TRUE; + default: + expand_string_message = string_sprintf("error from acl \"%s\"", sub[0]); + return NULL; + } + return s; + } + + /* saslauthd: does Cyrus saslauthd authentication. Four parameters are used: ${if saslauthd {{username}{password}{service}{realm}} {yes}[no}} However, the last two are optional. That is why the whole set is enclosed - in their own set or braces. */ + in their own set of braces. */ case ECOND_SASLAUTHD: #ifndef CYRUS_SASLAUTHD_SOCKET goto COND_FAILED_NOT_COMPILED; #else while (isspace(*s)) s++; - if (*s++ != '{') goto COND_FAILED_CURLY_START; + if (*s++ != '{') goto COND_FAILED_CURLY_START; /* }-for-text-editors */ switch(read_subs(sub, 4, 2, &s, yield == NULL, TRUE, US"saslauthd")) { case 1: expand_string_message = US"too few arguments or bracketing " @@ -1799,22 +2236,30 @@ switch(cond_type) /* symbolic operators for numeric and string comparison, and a number of other operators, all requiring two arguments. + crypteq: encrypts plaintext and compares against an encrypted text, + using crypt(), crypt16(), MD5 or SHA-1 + inlist/inlisti: checks if first argument is in the list of the second match: does a regular expression match and sets up the numerical variables if it succeeds match_address: matches in an address list match_domain: matches in a domain list match_ip: matches a host list that is restricted to IP addresses match_local_part: matches in a local part list - crypteq: encrypts plaintext and compares against an encrypted text, - using crypt(), crypt16(), MD5 or SHA-1 */ - case ECOND_MATCH: case ECOND_MATCH_ADDRESS: case ECOND_MATCH_DOMAIN: case ECOND_MATCH_IP: case ECOND_MATCH_LOCAL_PART: +#ifndef EXPAND_LISTMATCH_RHS + sub2_honour_dollar = FALSE; +#endif + /* FALLTHROUGH */ + case ECOND_CRYPTEQ: + case ECOND_INLIST: + case ECOND_INLISTI: + case ECOND_MATCH: case ECOND_NUM_L: /* Numerical comparisons */ case ECOND_NUM_LE: @@ -1836,6 +2281,13 @@ switch(cond_type) for (i = 0; i < 2; i++) { + /* Sometimes, we don't expand substrings; too many insecure configurations + created using match_address{}{} and friends, where the second param + includes information from untrustworthy sources. */ + BOOL honour_dollar = TRUE; + if ((i > 0) && !sub2_honour_dollar) + honour_dollar = FALSE; + while (isspace(*s)) s++; if (*s != '{') { @@ -1844,7 +2296,8 @@ switch(cond_type) "after \"%s\"", name); return NULL; } - sub[i] = expand_string_internal(s+1, TRUE, &s, yield == NULL); + sub[i] = expand_string_internal(s+1, TRUE, &s, yield == NULL, + honour_dollar); if (sub[i] == NULL) return NULL; if (*s++ != '}') goto COND_FAILED_CURLY_END; @@ -1852,26 +2305,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') - { - num[i] *= 1024*1024; - endptr++; - } - while (isspace(*endptr)) endptr++; - if (*endptr != 0) + else { - 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; } } } @@ -1886,63 +2331,63 @@ switch(cond_type) { case ECOND_NUM_E: case ECOND_NUM_EE: - *yield = (num[0] == num[1]) == testfor; + tempcond = (num[0] == num[1]); break; case ECOND_NUM_G: - *yield = (num[0] > num[1]) == testfor; + tempcond = (num[0] > num[1]); break; case ECOND_NUM_GE: - *yield = (num[0] >= num[1]) == testfor; + tempcond = (num[0] >= num[1]); break; case ECOND_NUM_L: - *yield = (num[0] < num[1]) == testfor; + tempcond = (num[0] < num[1]); break; case ECOND_NUM_LE: - *yield = (num[0] <= num[1]) == testfor; + tempcond = (num[0] <= num[1]); break; case ECOND_STR_LT: - *yield = (Ustrcmp(sub[0], sub[1]) < 0) == testfor; + tempcond = (Ustrcmp(sub[0], sub[1]) < 0); break; case ECOND_STR_LTI: - *yield = (strcmpic(sub[0], sub[1]) < 0) == testfor; + tempcond = (strcmpic(sub[0], sub[1]) < 0); break; case ECOND_STR_LE: - *yield = (Ustrcmp(sub[0], sub[1]) <= 0) == testfor; + tempcond = (Ustrcmp(sub[0], sub[1]) <= 0); break; case ECOND_STR_LEI: - *yield = (strcmpic(sub[0], sub[1]) <= 0) == testfor; + tempcond = (strcmpic(sub[0], sub[1]) <= 0); break; case ECOND_STR_EQ: - *yield = (Ustrcmp(sub[0], sub[1]) == 0) == testfor; + tempcond = (Ustrcmp(sub[0], sub[1]) == 0); break; case ECOND_STR_EQI: - *yield = (strcmpic(sub[0], sub[1]) == 0) == testfor; + tempcond = (strcmpic(sub[0], sub[1]) == 0); break; case ECOND_STR_GT: - *yield = (Ustrcmp(sub[0], sub[1]) > 0) == testfor; + tempcond = (Ustrcmp(sub[0], sub[1]) > 0); break; case ECOND_STR_GTI: - *yield = (strcmpic(sub[0], sub[1]) > 0) == testfor; + tempcond = (strcmpic(sub[0], sub[1]) > 0); break; case ECOND_STR_GE: - *yield = (Ustrcmp(sub[0], sub[1]) >= 0) == testfor; + tempcond = (Ustrcmp(sub[0], sub[1]) >= 0); break; case ECOND_STR_GEI: - *yield = (strcmpic(sub[0], sub[1]) >= 0) == testfor; + tempcond = (strcmpic(sub[0], sub[1]) >= 0); break; case ECOND_MATCH: /* Regular expression match */ @@ -1954,7 +2399,7 @@ switch(cond_type) "\"%s\": %s at offset %d", sub[1], rerror, roffset); return NULL; } - *yield = regex_match_and_setup(re, sub[0], 0, -1) == testfor; + tempcond = regex_match_and_setup(re, sub[0], 0, -1); break; case ECOND_MATCH_ADDRESS: /* Match in an address list */ @@ -1967,7 +2412,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]); @@ -2010,11 +2455,11 @@ switch(cond_type) switch(rc) { case OK: - *yield = testfor; + tempcond = TRUE; break; case FAIL: - *yield = !testfor; + tempcond = FALSE; break; case DEFER: @@ -2028,6 +2473,7 @@ switch(cond_type) /* Various "encrypted" comparisons. If the second string starts with "{" then an encryption type is given. Default to crypt() or crypt16() (build-time choice). */ + /* }-for-text-editors */ case ECOND_CRYPTEQ: #ifndef SUPPORT_CRYPTEQ @@ -2052,7 +2498,7 @@ switch(cond_type) uschar *coded = auth_b64encode((uschar *)digest, 16); DEBUG(D_auth) debug_printf("crypteq: using MD5+B64 hashing\n" " subject=%s\n crypted=%s\n", coded, sub[1]+5); - *yield = (Ustrcmp(coded, sub[1]+5) == 0) == testfor; + tempcond = (Ustrcmp(coded, sub[1]+5) == 0); } else if (sublen == 32) { @@ -2062,13 +2508,13 @@ switch(cond_type) coded[32] = 0; DEBUG(D_auth) debug_printf("crypteq: using MD5+hex hashing\n" " subject=%s\n crypted=%s\n", coded, sub[1]+5); - *yield = (strcmpic(coded, sub[1]+5) == 0) == testfor; + tempcond = (strcmpic(coded, sub[1]+5) == 0); } else { DEBUG(D_auth) debug_printf("crypteq: length for MD5 not 24 or 32: " "fail\n crypted=%s\n", sub[1]+5); - *yield = !testfor; + tempcond = FALSE; } } @@ -2090,7 +2536,7 @@ switch(cond_type) uschar *coded = auth_b64encode((uschar *)digest, 20); DEBUG(D_auth) debug_printf("crypteq: using SHA1+B64 hashing\n" " subject=%s\n crypted=%s\n", coded, sub[1]+6); - *yield = (Ustrcmp(coded, sub[1]+6) == 0) == testfor; + tempcond = (Ustrcmp(coded, sub[1]+6) == 0); } else if (sublen == 40) { @@ -2100,17 +2546,18 @@ switch(cond_type) coded[40] = 0; DEBUG(D_auth) debug_printf("crypteq: using SHA1+hex hashing\n" " subject=%s\n crypted=%s\n", coded, sub[1]+6); - *yield = (strcmpic(coded, sub[1]+6) == 0) == testfor; + tempcond = (strcmpic(coded, sub[1]+6) == 0); } else { DEBUG(D_auth) debug_printf("crypteq: length for SHA-1 not 28 or 40: " "fail\n crypted=%s\n", sub[1]+6); - *yield = !testfor; + tempcond = FALSE; } } else /* {crypt} or {crypt16} and non-{ at start */ + /* }-for-text-editors */ { int which = 0; uschar *coded; @@ -2125,7 +2572,7 @@ switch(cond_type) sub[1] += 9; which = 2; } - else if (sub[1][0] == '{') + else if (sub[1][0] == '{') /* }-for-text-editors */ { expand_string_message = string_sprintf("unknown encryption mechanism " "in \"%s\"", sub[1]); @@ -2152,13 +2599,37 @@ switch(cond_type) salt), force failure. Otherwise we get false positives: with an empty string the yield of crypt() is an empty string! */ - *yield = (Ustrlen(sub[1]) < 2)? !testfor : - (Ustrcmp(coded, sub[1]) == 0) == testfor; + tempcond = (Ustrlen(sub[1]) < 2)? FALSE : + (Ustrcmp(coded, sub[1]) == 0); } break; #endif /* SUPPORT_CRYPTEQ */ + + case ECOND_INLIST: + case ECOND_INLISTI: + { + int sep = 0; + uschar *save_iterate_item = iterate_item; + int (*compare)(const uschar *, const uschar *); + + tempcond = FALSE; + if (cond_type == ECOND_INLISTI) + compare = strcmpic; + else + compare = (int (*)(const uschar *, const uschar *)) strcmp; + + while ((iterate_item = string_nextinlist(&sub[1], &sep, NULL, 0)) != NULL) + if (compare(sub[0], iterate_item) == 0) + { + tempcond = TRUE; + break; + } + iterate_item = save_iterate_item; + } + } /* Switch for comparison conditions */ + *yield = tempcond == testfor; return s; /* End of comparison conditions */ @@ -2170,13 +2641,14 @@ switch(cond_type) combined_cond = (cond_type == ECOND_AND); while (isspace(*s)) s++; - if (*s++ != '{') goto COND_FAILED_CURLY_START; + if (*s++ != '{') goto COND_FAILED_CURLY_START; /* }-for-text-editors */ for (;;) { while (isspace(*s)) s++; + /* {-for-text-editors */ if (*s == '}') break; - if (*s != '{') + if (*s != '{') /* }-for-text-editors */ { expand_string_message = string_sprintf("each subcondition " "inside an \"%s{...}\" condition must be in its own {}", name); @@ -2192,8 +2664,10 @@ switch(cond_type) } while (isspace(*s)) s++; + /* {-for-text-editors */ if (*s++ != '}') { + /* {-for-text-editors */ expand_string_message = string_sprintf("missing } at end of condition " "inside \"%s\" group", name); return NULL; @@ -2218,41 +2692,180 @@ switch(cond_type) return ++s; - /* Unknown condition */ + /* forall/forany: iterates a condition with different values */ - default: - expand_string_message = string_sprintf("unknown condition \"%s\"", name); - return NULL; - } /* End switch on condition type */ + case ECOND_FORALL: + case ECOND_FORANY: + { + int sep = 0; + uschar *save_iterate_item = iterate_item; -/* Missing braces at start and end of data */ + while (isspace(*s)) s++; + if (*s++ != '{') goto COND_FAILED_CURLY_START; /* }-for-text-editors */ + sub[0] = expand_string_internal(s, TRUE, &s, (yield == NULL), TRUE); + if (sub[0] == NULL) return NULL; + /* {-for-text-editors */ + if (*s++ != '}') goto COND_FAILED_CURLY_END; -COND_FAILED_CURLY_START: -expand_string_message = string_sprintf("missing { after \"%s\"", name); -return NULL; + while (isspace(*s)) s++; + if (*s++ != '{') goto COND_FAILED_CURLY_START; /* }-for-text-editors */ -COND_FAILED_CURLY_END: -expand_string_message = string_sprintf("missing } at end of \"%s\" condition", - name); -return NULL; + sub[1] = s; -/* A condition requires code that is not compiled */ + /* 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. */ -#if !defined(SUPPORT_PAM) || !defined(RADIUS_CONFIG_FILE) || \ - !defined(LOOKUP_LDAP) || !defined(CYRUS_PWCHECK_SOCKET) || \ - !defined(SUPPORT_CRYPTEQ) || !defined(CYRUS_SASLAUTHD_SOCKET) -COND_FAILED_NOT_COMPILED: -expand_string_message = string_sprintf("support for \"%s\" not compiled", - name); -return NULL; -#endif -} + 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++; + /* {-for-text-editors */ + if (*s++ != '}') + { + /* {-for-text-editors */ + 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; + } -/************************************************* -* Save numerical variables * + iterate_item = save_iterate_item; + return s; + } + + + /* The bool{} expansion condition maps a string to boolean. + The values supported should match those supported by the ACL condition + (acl.c, ACLC_CONDITION) so that we keep to a minimum the different ideas + of true/false. Note that Router "condition" rules have a different + interpretation, where general data can be used and only a few values + map to FALSE. + Note that readconf.c boolean matching, for boolean configuration options, + only matches true/yes/false/no. + The bool_lax{} condition matches the Router logic, which is much more + liberal. */ + case ECOND_BOOL: + case ECOND_BOOL_LAX: + { + uschar *sub_arg[1]; + uschar *t, *t2; + uschar *ourname; + size_t len; + BOOL boolvalue = FALSE; + while (isspace(*s)) s++; + if (*s != '{') goto COND_FAILED_CURLY_START; /* }-for-text-editors */ + ourname = cond_type == ECOND_BOOL_LAX ? US"bool_lax" : US"bool"; + switch(read_subs(sub_arg, 1, 1, &s, yield == NULL, FALSE, ourname)) + { + case 1: expand_string_message = string_sprintf( + "too few arguments or bracketing error for %s", + ourname); + /*FALLTHROUGH*/ + case 2: + case 3: return NULL; + } + t = sub_arg[0]; + while (isspace(*t)) t++; + len = Ustrlen(t); + if (len) + { + /* trailing whitespace: seems like a good idea to ignore it too */ + t2 = t + len - 1; + while (isspace(*t2)) t2--; + if (t2 != (t + len)) + { + *++t2 = '\0'; + len = t2 - t; + } + } + DEBUG(D_expand) + debug_printf("considering %s: %s\n", ourname, len ? t : US""); + /* logic for the lax case from expand_check_condition(), which also does + expands, and the logic is both short and stable enough that there should + be no maintenance burden from replicating it. */ + if (len == 0) + boolvalue = FALSE; + else if (Ustrspn(t, "0123456789") == len) + { + boolvalue = (Uatoi(t) == 0) ? FALSE : TRUE; + /* expand_check_condition only does a literal string "0" check */ + if ((cond_type == ECOND_BOOL_LAX) && (len > 1)) + boolvalue = TRUE; + } + else if (strcmpic(t, US"true") == 0 || strcmpic(t, US"yes") == 0) + boolvalue = TRUE; + else if (strcmpic(t, US"false") == 0 || strcmpic(t, US"no") == 0) + boolvalue = FALSE; + else if (cond_type == ECOND_BOOL_LAX) + boolvalue = TRUE; + else + { + expand_string_message = string_sprintf("unrecognised boolean " + "value \"%s\"", t); + return NULL; + } + if (yield != NULL) *yield = (boolvalue == testfor); + return s; + } + + /* Unknown condition */ + + default: + expand_string_message = string_sprintf("unknown condition \"%s\"", name); + return NULL; + } /* End switch on condition type */ + +/* Missing braces at start and end of data */ + +COND_FAILED_CURLY_START: +expand_string_message = string_sprintf("missing { after \"%s\"", name); +return NULL; + +COND_FAILED_CURLY_END: +expand_string_message = string_sprintf("missing } at end of \"%s\" condition", + name); +return NULL; + +/* A condition requires code that is not compiled */ + +#if !defined(SUPPORT_PAM) || !defined(RADIUS_CONFIG_FILE) || \ + !defined(LOOKUP_LDAP) || !defined(CYRUS_PWCHECK_SOCKET) || \ + !defined(SUPPORT_CRYPTEQ) || !defined(CYRUS_SASLAUTHD_SOCKET) +COND_FAILED_NOT_COMPILED: +expand_string_message = string_sprintf("support for \"%s\" not compiled", + name); +return NULL; +#endif +} + + + + +/************************************************* +* Save numerical variables * *************************************************/ /* This function is called from items such as "if" that want to preserve and @@ -2374,7 +2987,7 @@ if (*s++ != '{') goto FAILED_CURLY; want this string. Set skipping in the call in the fail case (this will always be the case if we were already skipping). */ -sub1 = expand_string_internal(s, TRUE, &s, !yes); +sub1 = expand_string_internal(s, TRUE, &s, !yes, TRUE); if (sub1 == NULL && (yes || !expand_string_forcedfail)) goto FAILED; expand_string_forcedfail = FALSE; if (*s++ != '}') goto FAILED_CURLY; @@ -2399,7 +3012,7 @@ already skipping. */ while (isspace(*s)) s++; if (*s == '{') { - sub2 = expand_string_internal(s+1, TRUE, &s, yes || skipping); + sub2 = expand_string_internal(s+1, TRUE, &s, yes || skipping, TRUE); if (sub2 == NULL && (!yes || !expand_string_forcedfail)) goto FAILED; expand_string_forcedfail = FALSE; if (*s++ != '}') goto FAILED_CURLY; @@ -2529,8 +3142,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,69 +3273,64 @@ 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_eximarith_t eval_op_or(uschar **, BOOL, uschar **); -static int + +static int_eximarith_t eval_expr(uschar **sptr, BOOL decimal, uschar **error, BOOL endket) { uschar *s = *sptr; -int x = eval_sumterm(&s, decimal, error); +int_eximarith_t x = eval_op_or(&s, decimal, error); if (*error == NULL) { - while (*s == '+' || *s == '-') - { - 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 (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) + +static int_eximarith_t +eval_number(uschar **sptr, BOOL decimal, uschar **error) { register int c; -int n; +int_eximarith_t 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); + (void)sscanf(CS s, (decimal? SC_EXIM_DEC "%n" : SC_EXIM_ARITH "%n"), &n, &count); s += count; - if (tolower(*s) == 'k') { n *= 1024; s++; } - else if (tolower(*s) == 'm') { n *= 1024*1024; s++; } + switch (tolower(*s)) + { + default: break; + case 'k': n *= 1024; s++; break; + case 'm': n *= 1024*1024; s++; break; + case 'g': n *= 1024*1024*1024; s++; break; + } while (isspace (*s)) s++; } else if (c == '(') @@ -2739,18 +3347,82 @@ else return n; } -static int eval_sumterm(uschar **sptr, BOOL decimal, uschar **error) + +static int_eximarith_t +eval_op_unary(uschar **sptr, BOOL decimal, uschar **error) { uschar *s = *sptr; -int x = eval_term(&s, decimal, error); +int_eximarith_t 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_eximarith_t +eval_op_mult(uschar **sptr, BOOL decimal, uschar **error) +{ +uschar *s = *sptr; +int_eximarith_t x = eval_op_unary(&s, decimal, error); if (*error == NULL) { - while (*s == '*' || *s == '/') + while (*s == '*' || *s == '/' || *s == '%') { int op = *s++; - int y = eval_term(&s, decimal, error); + int_eximarith_t y = eval_op_unary(&s, decimal, error); if (*error != NULL) break; - if (op == '*') x *= y; else x /= y; + /* SIGFPE both on div/mod by zero and on INT_MIN / -1, which would give + * a value of INT_MAX+1. Note that INT_MIN * -1 gives INT_MIN for me, which + * is a bug somewhere in [gcc 4.2.1, FreeBSD, amd64]. In fact, -N*-M where + * -N*M is INT_MIN will yielf INT_MIN. + * Since we don't support floating point, this is somewhat simpler. + * Ideally, we'd return an error, but since we overflow for all other + * arithmetic, consistency suggests otherwise, but what's the correct value + * to use? There is none. + * The C standard guarantees overflow for unsigned arithmetic but signed + * overflow invokes undefined behaviour; in practice, this is overflow + * except for converting INT_MIN to INT_MAX+1. We also can't guarantee + * that long/longlong larger than int are available, or we could just work + * with larger types. We should consider whether to guarantee 32bit eval + * and 64-bit working variables, with errors returned. For now ... + * So, the only SIGFPEs occur with a non-shrinking div/mod, thus -1; we + * can just let the other invalid results occur otherwise, as they have + * until now. For this one case, we can coerce. + */ + if (y == -1 && x == EXIM_ARITH_MIN && op != '*') + { + DEBUG(D_expand) + debug_printf("Integer exception dodging: " PR_EXIM_ARITH "%c-1 coerced to " PR_EXIM_ARITH "\n", + EXIM_ARITH_MIN, op, EXIM_ARITH_MAX); + x = EXIM_ARITH_MAX; + continue; + } + if (op == '*') + x *= y; + else + { + if (y == 0) + { + *error = (op == '/') ? US"divide by zero" : US"modulo by zero"; + x = 0; + break; + } + if (op == '/') + x /= y; + else + x %= y; + } } } *sptr = s; @@ -2758,6 +3430,110 @@ return x; } +static int_eximarith_t +eval_op_sum(uschar **sptr, BOOL decimal, uschar **error) +{ +uschar *s = *sptr; +int_eximarith_t x = eval_op_mult(&s, decimal, error); +if (*error == NULL) + { + while (*s == '+' || *s == '-') + { + int op = *s++; + int_eximarith_t y = eval_op_mult(&s, decimal, error); + if (*error != NULL) break; + if (op == '+') x += y; else x -= y; + } + } +*sptr = s; +return x; +} + + +static int_eximarith_t +eval_op_shift(uschar **sptr, BOOL decimal, uschar **error) +{ +uschar *s = *sptr; +int_eximarith_t x = eval_op_sum(&s, decimal, error); +if (*error == NULL) + { + while ((*s == '<' || *s == '>') && s[1] == s[0]) + { + int_eximarith_t y; + int op = *s++; + s++; + y = eval_op_sum(&s, decimal, error); + if (*error != NULL) break; + if (op == '<') x <<= y; else x >>= y; + } + } +*sptr = s; +return x; +} + + +static int_eximarith_t +eval_op_and(uschar **sptr, BOOL decimal, uschar **error) +{ +uschar *s = *sptr; +int_eximarith_t x = eval_op_shift(&s, decimal, error); +if (*error == NULL) + { + while (*s == '&') + { + int_eximarith_t y; + s++; + y = eval_op_shift(&s, decimal, error); + if (*error != NULL) break; + x &= y; + } + } +*sptr = s; +return x; +} + + +static int_eximarith_t +eval_op_xor(uschar **sptr, BOOL decimal, uschar **error) +{ +uschar *s = *sptr; +int_eximarith_t x = eval_op_and(&s, decimal, error); +if (*error == NULL) + { + while (*s == '^') + { + int_eximarith_t y; + s++; + y = eval_op_and(&s, decimal, error); + if (*error != NULL) break; + x ^= y; + } + } +*sptr = s; +return x; +} + + +static int_eximarith_t +eval_op_or(uschar **sptr, BOOL decimal, uschar **error) +{ +uschar *s = *sptr; +int_eximarith_t x = eval_op_xor(&s, decimal, error); +if (*error == NULL) + { + while (*s == '|') + { + int_eximarith_t y; + s++; + y = eval_op_xor(&s, decimal, error); + if (*error != NULL) break; + x |= y; + } + } +*sptr = s; +return x; +} + /************************************************* @@ -2798,6 +3574,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. The same applies for ${acl. 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 } @@ -2805,6 +3587,8 @@ Arguments: expansion is placed here (typically used with ket_ends) skipping TRUE for recursive calls when the value isn't actually going to be used (to allow for optimisation) + honour_dollar TRUE if $ is to be expanded, + FALSE if it's just another character Returns: NULL if expansion fails: expand_string_forcedfail is set TRUE if failure was forced @@ -2814,7 +3598,7 @@ Returns: NULL if expansion fails: static uschar * expand_string_internal(uschar *string, BOOL ket_ends, uschar **left, - BOOL skipping) + BOOL skipping, BOOL honour_dollar) { int ptr = 0; int size = Ustrlen(string)+ 64; @@ -2823,6 +3607,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""; @@ -2869,7 +3654,7 @@ while (*s != 0) if (ket_ends && *s == '}') break; - if (*s != '$') + if (*s != '$' || !honour_dollar) { yield = string_cat(yield, &size, &ptr, s++, 1); continue; @@ -2895,7 +3680,7 @@ while (*s != 0) if (ptr == 0 && yield != NULL) { - store_reset(yield); + if (resetok) store_reset(yield); yield = NULL; size = 0; } @@ -2915,7 +3700,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 +3720,7 @@ while (*s != 0) { expand_string_message = string_sprintf("unknown variable name \"%s\"", name); + check_variable_error_message(name); goto EXPAND_FAILED; } } @@ -3008,6 +3794,46 @@ while (*s != 0) switch(item_type) { + /* Call an ACL from an expansion. We feed data in via $acl_arg1 - $acl_arg9. + If the ACL returns accept or reject we return content set by "message =" + There is currently no limit on recursion; this would have us call + acl_check_internal() directly and get a current level from somewhere. + See also the acl expansion condition ECOND_ACL and the traditional + acl modifier ACLC_ACL. + Assume that the function has side-effects on the store that must be preserved. + */ + + case EITEM_ACL: + /* ${acl {name} {arg1}{arg2}...} */ + { + uschar *sub[10]; /* name + arg1-arg9 (which must match number of acl_arg[]) */ + uschar *user_msg; + + switch(read_subs(sub, 10, 1, &s, skipping, TRUE, US"acl")) + { + case 1: goto EXPAND_FAILED_CURLY; + case 2: + case 3: goto EXPAND_FAILED; + } + if (skipping) continue; + + resetok = FALSE; + switch(eval_acl(sub, sizeof(sub)/sizeof(*sub), &user_msg)) + { + case OK: + case FAIL: + if (user_msg) + yield = string_cat(yield, &size, &ptr, user_msg, Ustrlen(user_msg)); + continue; + + case DEFER: + expand_string_forcedfail = TRUE; + default: + expand_string_message = string_sprintf("error from acl \"%s\"", sub[0]); + goto EXPAND_FAILED; + } + } + /* Handle conditionals - preserve the values of the numerical expansion variables in case they get changed by a regular expression match in the condition. If not, they retain their external settings. At the end @@ -3084,7 +3910,7 @@ while (*s != 0) while (isspace(*s)) s++; if (*s == '{') { - key = expand_string_internal(s+1, TRUE, &s, skipping); + key = expand_string_internal(s+1, TRUE, &s, skipping, TRUE); if (key == NULL) goto EXPAND_FAILED; if (*s++ != '}') goto EXPAND_FAILED_CURLY; while (isspace(*s)) s++; @@ -3125,7 +3951,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,21 +3971,41 @@ 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); + filename = expand_string_internal(s+1, TRUE, &s, skipping, TRUE); if (filename == NULL) goto EXPAND_FAILED; if (*s++ != '}') goto EXPAND_FAILED_CURLY; 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 @@ -3190,8 +4036,8 @@ while (*s != 0) if (search_find_defer) { expand_string_message = - string_sprintf("lookup of \"%s\" gave DEFER: %s", key, - search_error_message); + string_sprintf("lookup of \"%s\" gave DEFER: %s", + string_printing2(key, FALSE), search_error_message); goto EXPAND_FAILED; } if (expand_setup > 0) expand_nmax = expand_setup; @@ -3333,15 +4179,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 +4204,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 +4223,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 +4270,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 +4291,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 +4305,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 +4321,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 +4394,7 @@ while (*s != 0) } yield = cat_file(f, yield, &size, &ptr, sub_arg[1]); - fclose(f); + (void)fclose(f); continue; } @@ -3557,28 +4440,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 +4611,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 +4627,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 +4635,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; } } @@ -3622,7 +4645,7 @@ while (*s != 0) if (*s == '{') { - if (expand_string_internal(s+1, TRUE, &s, TRUE) == NULL) + if (expand_string_internal(s+1, TRUE, &s, TRUE, TRUE) == NULL) goto EXPAND_FAILED; if (*s++ != '}') goto EXPAND_FAILED_CURLY; while (isspace(*s)) s++; @@ -3637,7 +4660,7 @@ while (*s != 0) SOCK_FAIL: if (*s != '{') goto EXPAND_FAILED; DEBUG(D_any) debug_printf("%s\n", expand_string_message); - arg = expand_string_internal(s+1, TRUE, &s, FALSE); + arg = expand_string_internal(s+1, TRUE, &s, FALSE, TRUE); if (arg == NULL) goto EXPAND_FAILED; yield = string_cat(yield, &size, &ptr, arg, Ustrlen(arg)); if (*s++ != '}') goto EXPAND_FAILED_CURLY; @@ -3666,7 +4689,7 @@ while (*s != 0) while (isspace(*s)) s++; if (*s != '{') goto EXPAND_FAILED_CURLY; - arg = expand_string_internal(s+1, TRUE, &s, skipping); + arg = expand_string_internal(s+1, TRUE, &s, skipping, TRUE); if (arg == NULL) goto EXPAND_FAILED; while (isspace(*s)) s++; if (*s++ != '}') goto EXPAND_FAILED_CURLY; @@ -3701,15 +4724,26 @@ while (*s != 0) /* Nothing is written to the standard input. */ - close(fd_in); + (void)close(fd_in); + + /* Read the pipe to get the command's output into $value (which is kept + in lookup_value). Read during execution, so that if the output exceeds + the OS pipe buffer limit, we don't block forever. */ + + f = fdopen(fd_out, "rb"); + sigalrm_seen = FALSE; + alarm(60); + lookup_value = cat_file(f, lookup_value, &lsize, &lptr, NULL); + alarm(0); + (void)fclose(f); /* Wait for the process to finish, applying the timeout, and inspect its return code for serious disasters. Simple non-zero returns are passed on. */ - if ((runrc = child_close(pid, 60)) < 0) + if (sigalrm_seen == TRUE || (runrc = child_close(pid, 30)) < 0) { - if (runrc == -256) + if (sigalrm_seen == TRUE || runrc == -256) { expand_string_message = string_sprintf("command timed out"); killpg(pid, SIGKILL); /* Kill the whole process group */ @@ -3725,14 +4759,6 @@ while (*s != 0) goto EXPAND_FAILED; } - - /* Read the pipe to get the command's output into $value (which is kept - in lookup_value). */ - - f = fdopen(fd_out, "rb"); - lookup_value = NULL; - lookup_value = cat_file(f, lookup_value, &lsize, &lptr, NULL); - fclose(f); } /* Process the yes/no strings; $value may be useful in both cases */ @@ -4094,7 +5120,7 @@ while (*s != 0) while (isspace(*s)) s++; if (*s == '{') { - sub[i] = expand_string_internal(s+1, TRUE, &s, skipping); + sub[i] = expand_string_internal(s+1, TRUE, &s, skipping, TRUE); if (sub[i] == NULL) goto EXPAND_FAILED; if (*s++ != '}') goto EXPAND_FAILED_CURLY; @@ -4116,7 +5142,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 +5199,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, TRUE); + 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, TRUE); + 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, 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, TRUE); + 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 +5453,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]); @@ -4282,7 +5488,7 @@ while (*s != 0) { int c; uschar *arg = NULL; - uschar *sub = expand_string_internal(s+1, TRUE, &s, skipping); + uschar *sub = expand_string_internal(s+1, TRUE, &s, skipping, TRUE); if (sub == NULL) goto EXPAND_FAILED; s++; @@ -4331,6 +5537,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 +5550,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)); @@ -4354,7 +5563,7 @@ while (*s != 0) case EOP_EXPAND: { - uschar *expanded = expand_string_internal(sub, FALSE, NULL, skipping); + uschar *expanded = expand_string_internal(sub, FALSE, NULL, skipping, TRUE); if (expanded == NULL) { expand_string_message = @@ -4458,6 +5667,122 @@ while (*s != 0) continue; } + /* Convert octets outside 0x21..0x7E to \xXX form */ + + case EOP_HEXQUOTE: + { + uschar *t = sub - 1; + while (*(++t) != 0) + { + if (*t < 0x21 || 0x7E < *t) + yield = string_cat(yield, &size, &ptr, + string_sprintf("\\x%02x", *t), 4); + else + yield = string_cat(yield, &size, &ptr, t, 1); + } + continue; + } + + /* count the number of list elements */ + + case EOP_LISTCOUNT: + { + int cnt = 0; + int sep = 0; + uschar * cp; + uschar buffer[256]; + + while (string_nextinlist(&sub, &sep, buffer, sizeof(buffer)) != NULL) cnt++; + cp = string_sprintf("%d", cnt); + yield = string_cat(yield, &size, &ptr, cp, Ustrlen(cp)); + continue; + } + + /* expand a named list given the name */ + /* handles nested named lists; requotes as colon-sep list */ + + case EOP_LISTNAMED: + { + tree_node *t = NULL; + uschar * list; + int sep = 0; + uschar * item; + uschar * suffix = US""; + BOOL needsep = FALSE; + uschar buffer[256]; + + if (*sub == '+') sub++; + if (arg == NULL) /* no-argument version */ + { + if (!(t = tree_search(addresslist_anchor, sub)) && + !(t = tree_search(domainlist_anchor, sub)) && + !(t = tree_search(hostlist_anchor, sub))) + t = tree_search(localpartlist_anchor, sub); + } + else switch(*arg) /* specific list-type version */ + { + case 'a': t = tree_search(addresslist_anchor, sub); suffix = US"_a"; break; + case 'd': t = tree_search(domainlist_anchor, sub); suffix = US"_d"; break; + case 'h': t = tree_search(hostlist_anchor, sub); suffix = US"_h"; break; + case 'l': t = tree_search(localpartlist_anchor, sub); suffix = US"_l"; break; + default: + expand_string_message = string_sprintf("bad suffix on \"list\" operator"); + goto EXPAND_FAILED; + } + + if(!t) + { + expand_string_message = string_sprintf("\"%s\" is not a %snamed list", + sub, !arg?"" + : *arg=='a'?"address " + : *arg=='d'?"domain " + : *arg=='h'?"host " + : *arg=='l'?"localpart " + : 0); + goto EXPAND_FAILED; + } + + list = ((namedlist_block *)(t->data.ptr))->string; + + while ((item = string_nextinlist(&list, &sep, buffer, sizeof(buffer))) != NULL) + { + uschar * buf = US" : "; + if (needsep) + yield = string_cat(yield, &size, &ptr, buf, 3); + else + needsep = TRUE; + + if (*item == '+') /* list item is itself a named list */ + { + uschar * sub = string_sprintf("${listnamed%s:%s}", suffix, item); + item = expand_string_internal(sub, FALSE, NULL, FALSE, TRUE); + } + else if (sep != ':') /* item from non-colon-sep list, re-quote for colon list-separator */ + { + char * cp; + char tok[3]; + tok[0] = sep; tok[1] = ':'; tok[2] = 0; + while ((cp= strpbrk((const char *)item, tok))) + { + yield = string_cat(yield, &size, &ptr, item, cp-(char *)item); + if (*cp++ == ':') /* colon in a non-colon-sep list item, needs doubling */ + { + yield = string_cat(yield, &size, &ptr, US"::", 2); + item = (uschar *)cp; + } + else /* sep in item; should already be doubled; emit once */ + { + yield = string_cat(yield, &size, &ptr, (uschar *)tok, 1); + if (*cp == sep) cp++; + item = (uschar *)cp; + } + } + } + yield = string_cat(yield, &size, &ptr, item, Ustrlen(item)); + } + continue; + } + /* mask applies a mask to an IP address; for example the result of ${mask:131.111.10.206/28} is 131.111.10.192/28. */ @@ -4530,6 +5855,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. @@ -4598,8 +5986,8 @@ while (*s != 0) goto EXPAND_FAILED; } - if (lookup_list[n].quote != NULL) - sub = (lookup_list[n].quote)(sub, opt); + if (lookup_list[n]->quote != NULL) + sub = (lookup_list[n]->quote)(sub, opt); else if (opt != NULL) sub = NULL; if (sub == NULL) @@ -4636,11 +6024,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 */ @@ -4674,7 +6079,7 @@ while (*s != 0) { uschar *save_sub = sub; uschar *error = NULL; - int n = eval_expr(&sub, (c == EOP_EVAL10), &error, FALSE); + int_eximarith_t n = eval_expr(&sub, (c == EOP_EVAL10), &error, FALSE); if (error != NULL) { expand_string_message = string_sprintf("error in expression " @@ -4682,13 +6087,27 @@ while (*s != 0) save_sub); goto EXPAND_FAILED; } - sprintf(CS var_buffer, "%d", n); + sprintf(CS var_buffer, PR_EXIM_ARITH, n); yield = string_cat(yield, &size, &ptr, var_buffer, Ustrlen(var_buffer)); continue; } /* 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 +6244,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", @@ -4863,6 +6288,40 @@ while (*s != 0) continue; } + /* vaguely random number less than N */ + + case EOP_RANDINT: + { + int_eximarith_t max; + uschar *s; + + max = expand_string_integer(sub, TRUE); + if (expand_string_message != NULL) + goto EXPAND_FAILED; + s = string_sprintf("%d", vaguely_random_number((int)max)); + yield = string_cat(yield, &size, &ptr, s, Ustrlen(s)); + continue; + } + + /* Reverse IP, including IPv6 to dotted-nibble */ + + case EOP_REVERSE_IP: + { + int family, maskptr; + uschar reversed[128]; + + family = string_is_ip_address(sub, &maskptr); + if (family == 0) + { + expand_string_message = string_sprintf( + "reverse_ip() not given an IP address [%s]", sub); + goto EXPAND_FAILED; + } + invert_address(reversed, sub); + yield = string_cat(yield, &size, &ptr, reversed, Ustrlen(reversed)); + continue; + } + /* Unknown operator */ default: @@ -4885,7 +6344,7 @@ while (*s != 0) int newsize = 0; if (ptr == 0) { - store_reset(yield); + if (resetok) store_reset(yield); yield = NULL; size = 0; } @@ -4894,6 +6353,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 +6399,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, @@ -4987,7 +6447,7 @@ expand_string(uschar *string) search_find_defer = FALSE; malformed_header = FALSE; return (Ustrpbrk(string, "$\\") == NULL)? string : - expand_string_internal(string, FALSE, NULL, FALSE); + expand_string_internal(string, FALSE, NULL, FALSE, TRUE); } @@ -5019,22 +6479,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) +int_eximarith_t +expand_string_integer(uschar *string, BOOL isplus) { -long int value; +int_eximarith_t 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,39 +6506,62 @@ 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 */ + +/* Before Exim 4.64, strings consisting entirely of whitespace compared +equal to 0. Unfortunately, people actually relied upon that, so preserve +the behaviour explicitly. Stripping leading whitespace is a harmless +noop change since strtol skips it anyway (provided that there is a number +to find at all). */ +if (isspace(*s)) + { + while (isspace(*s)) ++s; + if (*s == '\0') + { + DEBUG(D_expand) + debug_printf("treating blank string as number 0\n"); + return 0; + } + } + +value = strtoll(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 */ - if (value > INT_MAX || value < INT_MIN) errno = ERANGE; - - if (errno != ERANGE) + switch (tolower(*endptr)) { - if (tolower(*endptr) == 'k') - { - if (value > INT_MAX/1024 || value < INT_MIN/1024) errno = ERANGE; - else value *= 1024; + default: + break; + case 'k': + if (value > EXIM_ARITH_MAX/1024 || value < EXIM_ARITH_MIN/1024) errno = ERANGE; + else value *= 1024; endptr++; - } - else if (tolower(*endptr) == 'm') - { - if (value > INT_MAX/(1024*1024) || value < INT_MIN/(1024*1024)) - errno = ERANGE; + break; + case 'm': + if (value > EXIM_ARITH_MAX/(1024*1024) || value < EXIM_ARITH_MIN/(1024*1024)) errno = ERANGE; else value *= 1024*1024; endptr++; - } + break; + case 'g': + if (value > EXIM_ARITH_MAX/(1024*1024*1024) || value < EXIM_ARITH_MIN/(1024*1024*1024)) errno = ERANGE; + else value *= 1024*1024*1024; + endptr++; + break; } if (errno == ERANGE) msg = US"absolute value of integer \"%s\" is too large (overflow)"; else { while (isspace(*endptr)) endptr++; - if (*endptr == 0) return (int)value; + if (*endptr == 0) return value; } }