X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/02b41d7106c67bbe862d6a44373034684a03c00e..687a382d6fd27a355a551d8cfc64e1791f1fa4a7:/src/src/verify.c diff --git a/src/src/verify.c b/src/src/verify.c index 4af863c0b..6a50af506 100644 --- a/src/src/verify.c +++ b/src/src/verify.c @@ -2,7 +2,7 @@ * Exim - an Internet mail transport agent * *************************************************/ -/* Copyright (c) University of Cambridge 1995 - 2016 */ +/* Copyright (c) University of Cambridge 1995 - 2017 */ /* See the file NOTICE for conditions of use and distribution. */ /* Functions concerned with verifying things. The original code for callout @@ -39,7 +39,7 @@ static tree_node *dnsbl_cache = NULL; #define MT_NOT 1 #define MT_ALL 2 -static uschar cutthrough_response(char, uschar **, int); +static uschar cutthrough_response(int, char, uschar **, int); @@ -119,97 +119,21 @@ return cache_record; -/************************************************* -* Do callout verification for an address * -*************************************************/ - -/* This function is called from verify_address() when the address has routed to -a host list, and a callout has been requested. Callouts are expensive; that is -why a cache is used to improve the efficiency. - -Arguments: - addr the address that's been routed - host_list the list of hosts to try - tf the transport feedback block - - ifstring "interface" option from transport, or NULL - portstring "port" option from transport, or NULL - protocolstring "protocol" option from transport, or NULL - callout the per-command callout timeout - callout_overall the overall callout timeout (if < 0 use 4*callout) - callout_connect the callout connection timeout (if < 0 use callout) - options the verification options - these bits are used: - vopt_is_recipient => this is a recipient address - vopt_callout_no_cache => don't use callout cache - vopt_callout_fullpm => if postmaster check, do full one - vopt_callout_random => do the "random" thing - vopt_callout_recipsender => use real sender for recipient - vopt_callout_recippmaster => use postmaster for recipient - se_mailfrom MAIL FROM address for sender verify; NULL => "" - pm_mailfrom if non-NULL, do the postmaster check with this sender +/* Check the callout cache. +Options * pm_mailfrom may be modified by cache partial results. -Returns: OK/FAIL/DEFER +Return: TRUE if result found */ -static int -do_callout(address_item *addr, host_item *host_list, transport_feedback *tf, - int callout, int callout_overall, int callout_connect, int options, - uschar *se_mailfrom, uschar *pm_mailfrom) +static BOOL +cached_callout_lookup(address_item * addr, uschar * address_key, + uschar * from_address, int * opt_ptr, uschar ** pm_ptr, + int * yield, uschar ** failure_ptr, + dbdata_callout_cache * new_domain_record, int * old_domain_res) { -int yield = OK; -int old_domain_cache_result = ccache_accept; -BOOL done = FALSE; -uschar *address_key; -uschar *from_address; -uschar *random_local_part = NULL; -const uschar *save_deliver_domain = deliver_domain; -uschar **failure_ptr = options & vopt_is_recipient - ? &recipient_verify_failure : &sender_verify_failure; +int options = *opt_ptr; open_db dbblock; open_db *dbm_file = NULL; -dbdata_callout_cache new_domain_record; -dbdata_callout_cache_address new_address_record; -host_item *host; -time_t callout_start_time; - -new_domain_record.result = ccache_unknown; -new_domain_record.postmaster_result = ccache_unknown; -new_domain_record.random_result = ccache_unknown; - -memset(&new_address_record, 0, sizeof(new_address_record)); - -/* For a recipient callout, the key used for the address cache record must -include the sender address if we are using the real sender in the callout, -because that may influence the result of the callout. */ - -address_key = addr->address; -from_address = US""; - -if (options & vopt_is_recipient) - { - if (options & vopt_callout_recipsender) - { - address_key = string_sprintf("%s/<%s>", addr->address, sender_address); - from_address = sender_address; - if (cutthrough.delivery) options |= vopt_callout_no_cache; - } - else if (options & vopt_callout_recippmaster) - { - address_key = string_sprintf("%s/", addr->address, - qualify_domain_sender); - from_address = string_sprintf("postmaster@%s", qualify_domain_sender); - } - } - -/* For a sender callout, we must adjust the key if the mailfrom address is not -empty. */ - -else - { - from_address = (se_mailfrom == NULL)? US"" : se_mailfrom; - if (from_address[0] != 0) - address_key = string_sprintf("%s/<%s>", addr->address, from_address); - } /* Open the callout cache database, it it exists, for reading only at this stage, unless caching has been disabled. */ @@ -218,21 +142,19 @@ if (options & vopt_callout_no_cache) { HDEBUG(D_verify) debug_printf("callout cache: disabled by no_cache\n"); } -else if ((dbm_file = dbfn_open(US"callout", O_RDWR, &dbblock, FALSE)) == NULL) +else if (!(dbm_file = dbfn_open(US"callout", O_RDWR, &dbblock, FALSE))) { HDEBUG(D_verify) debug_printf("callout cache: not available\n"); } - -/* If a cache database is available see if we can avoid the need to do an -actual callout by making use of previously-obtained data. */ - -if (dbm_file) +else { - dbdata_callout_cache_address *cache_address_record; - dbdata_callout_cache *cache_record = get_callout_cache_record(dbm_file, - addr->domain, US"domain", - callout_cache_domain_positive_expire, - callout_cache_domain_negative_expire); + /* If a cache database is available see if we can avoid the need to do an + actual callout by making use of previously-obtained data. */ + + dbdata_callout_cache_address * cache_address_record; + dbdata_callout_cache * cache_record = get_callout_cache_record(dbm_file, + addr->domain, US"domain", + callout_cache_domain_positive_expire, callout_cache_domain_negative_expire); /* If an unexpired cache record was found for this domain, see if the callout process can be short-circuited. */ @@ -247,20 +169,21 @@ if (dbm_file) not to disturb the cached domain value if this whole verification succeeds (we don't want it turning into "accept"). */ - old_domain_cache_result = cache_record->result; + *old_domain_res = cache_record->result; - if (cache_record->result == ccache_reject || - (*from_address == 0 && cache_record->result == ccache_reject_mfnull)) + if ( cache_record->result == ccache_reject + || *from_address == 0 && cache_record->result == ccache_reject_mfnull) { setflag(addr, af_verify_nsfail); HDEBUG(D_verify) - debug_printf("callout cache: domain gave initial rejection, or " - "does not accept HELO or MAIL FROM:<>\n"); + debug_printf("callout cache: domain gave initial rejection, or " + "does not accept HELO or MAIL FROM:<>\n"); setflag(addr, af_verify_nsfail); addr->user_message = US"(result of an earlier callout reused)."; - yield = FAIL; + *yield = FAIL; *failure_ptr = US"mail"; - goto END_CALLOUT; + dbfn_close(dbm_file); + return TRUE; } /* If a previous check on a "random" local part was accepted, we assume @@ -275,21 +198,23 @@ if (dbm_file) case ccache_accept: HDEBUG(D_verify) debug_printf("callout cache: domain accepts random addresses\n"); - goto END_CALLOUT; /* Default yield is OK */ + dbfn_close(dbm_file); + return TRUE; /* Default yield is OK */ case ccache_reject: HDEBUG(D_verify) debug_printf("callout cache: domain rejects random addresses\n"); - options &= ~vopt_callout_random; - new_domain_record.random_result = ccache_reject; - new_domain_record.random_stamp = cache_record->random_stamp; + *opt_ptr = options & ~vopt_callout_random; + new_domain_record->random_result = ccache_reject; + new_domain_record->random_stamp = cache_record->random_stamp; break; default: HDEBUG(D_verify) debug_printf("callout cache: need to check random address handling " "(not cached or cache expired)\n"); - goto END_CACHE; + dbfn_close(dbm_file); + return FALSE; } /* If a postmaster check is requested, but there was a previous failure, @@ -297,27 +222,29 @@ if (dbm_file) but has not been done before, we are going to have to do a callout, so skip remaining cache processing. */ - if (pm_mailfrom) + if (*pm_ptr) { if (cache_record->postmaster_result == ccache_reject) - { - setflag(addr, af_verify_pmfail); - HDEBUG(D_verify) - debug_printf("callout cache: domain does not accept " - "RCPT TO:\n"); - yield = FAIL; - *failure_ptr = US"postmaster"; - setflag(addr, af_verify_pmfail); - addr->user_message = US"(result of earlier verification reused)."; - goto END_CALLOUT; - } + { + setflag(addr, af_verify_pmfail); + HDEBUG(D_verify) + debug_printf("callout cache: domain does not accept " + "RCPT TO:\n"); + *yield = FAIL; + *failure_ptr = US"postmaster"; + setflag(addr, af_verify_pmfail); + addr->user_message = US"(result of earlier verification reused)."; + dbfn_close(dbm_file); + return TRUE; + } if (cache_record->postmaster_result == ccache_unknown) - { - HDEBUG(D_verify) - debug_printf("callout cache: need to check RCPT " - "TO: (not cached or cache expired)\n"); - goto END_CACHE; - } + { + HDEBUG(D_verify) + debug_printf("callout cache: need to check RCPT " + "TO: (not cached or cache expired)\n"); + dbfn_close(dbm_file); + return FALSE; + } /* If cache says OK, set pm_mailfrom NULL to prevent a redundant postmaster check if the address itself has to be checked. Also ensure @@ -325,10 +252,10 @@ if (dbm_file) */ HDEBUG(D_verify) debug_printf("callout cache: domain accepts RCPT " - "TO:\n"); - pm_mailfrom = NULL; - new_domain_record.postmaster_result = ccache_accept; - new_domain_record.postmaster_stamp = cache_record->postmaster_stamp; + "TO:\n"); + *pm_ptr = NULL; + new_domain_record->postmaster_result = ccache_accept; + new_domain_record->postmaster_stamp = cache_record->postmaster_stamp; } } @@ -337,35 +264,302 @@ if (dbm_file) sender address if we are doing a recipient callout with a non-empty sender). */ - cache_address_record = (dbdata_callout_cache_address *) - get_callout_cache_record(dbm_file, - address_key, US"address", - callout_cache_positive_expire, - callout_cache_negative_expire); + if (!(cache_address_record = (dbdata_callout_cache_address *) + get_callout_cache_record(dbm_file, address_key, US"address", + callout_cache_positive_expire, callout_cache_negative_expire))) + { + dbfn_close(dbm_file); + return FALSE; + } - if (cache_address_record) + if (cache_address_record->result == ccache_accept) { - if (cache_address_record->result == ccache_accept) - { - HDEBUG(D_verify) - debug_printf("callout cache: address record is positive\n"); - } - else - { - HDEBUG(D_verify) - debug_printf("callout cache: address record is negative\n"); - addr->user_message = US"Previous (cached) callout verification failure"; - *failure_ptr = US"recipient"; - yield = FAIL; - } - goto END_CALLOUT; + HDEBUG(D_verify) + debug_printf("callout cache: address record is positive\n"); + } + else + { + HDEBUG(D_verify) + debug_printf("callout cache: address record is negative\n"); + addr->user_message = US"Previous (cached) callout verification failure"; + *failure_ptr = US"recipient"; + *yield = FAIL; } /* Close the cache database while we actually do the callout for real. */ - END_CACHE: dbfn_close(dbm_file); - dbm_file = NULL; + return TRUE; + } +return FALSE; +} + + +/* Write results to callout cache +*/ +static void +cache_callout_write(dbdata_callout_cache * dom_rec, const uschar * domain, + int done, dbdata_callout_cache_address * addr_rec, uschar * address_key) +{ +open_db dbblock; +open_db *dbm_file = NULL; + +/* If we get here with done == TRUE, a successful callout happened, and yield +will be set OK or FAIL according to the response to the RCPT command. +Otherwise, we looped through the hosts but couldn't complete the business. +However, there may be domain-specific information to cache in both cases. + +The value of the result field in the new_domain record is ccache_unknown if +there was an error before or with MAIL FROM:, and errno was not zero, +implying some kind of I/O error. We don't want to write the cache in that case. +Otherwise the value is ccache_accept, ccache_reject, or ccache_reject_mfnull. */ + +if (dom_rec->result != ccache_unknown) + if (!(dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE))) + { + HDEBUG(D_verify) debug_printf("callout cache: not available\n"); + } + else + { + (void)dbfn_write(dbm_file, domain, dom_rec, + (int)sizeof(dbdata_callout_cache)); + HDEBUG(D_verify) debug_printf("wrote callout cache domain record for %s:\n" + " result=%d postmaster=%d random=%d\n", + domain, + dom_rec->result, + dom_rec->postmaster_result, + dom_rec->random_result); + } + +/* If a definite result was obtained for the callout, cache it unless caching +is disabled. */ + +if (done && addr_rec->result != ccache_unknown) + { + if (!dbm_file) + dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE); + if (!dbm_file) + { + HDEBUG(D_verify) debug_printf("no callout cache available\n"); + } + else + { + (void)dbfn_write(dbm_file, address_key, addr_rec, + (int)sizeof(dbdata_callout_cache_address)); + HDEBUG(D_verify) debug_printf("wrote %s callout cache address record for %s\n", + addr_rec->result == ccache_accept ? "positive" : "negative", + address_key); + } + } + +if (dbm_file) dbfn_close(dbm_file); +} + + +/* Cutthrough-multi. If the existing cached cutthrough connection matches +the one we would make for a subsequent recipient, use it. Send the RCPT TO +and check the result, nonpipelined as it may be wanted immediately for +recipient-verification. + +It seems simpler to deal with this case separately from the main callout loop. +We will need to remember it has sent, or not, so that rcpt-acl tail code +can do it there for the non-rcpt-verify case. For this we keep an addresscount. + +Return: TRUE for a definitive result for the recipient +*/ +static int +cutthrough_multi(address_item * addr, host_item * host_list, + transport_feedback * tf, int * yield) +{ +BOOL done = FALSE; +host_item * host; + +if (addr->transport == cutthrough.addr.transport) + for (host = host_list; host; host = host->next) + if (Ustrcmp(host->address, cutthrough.host.address) == 0) + { + int host_af; + uschar *interface = NULL; /* Outgoing interface to use; NULL => any */ + int port = 25; + + deliver_host = host->name; + deliver_host_address = host->address; + deliver_host_port = host->port; + deliver_domain = addr->domain; + transport_name = addr->transport->name; + + host_af = Ustrchr(host->address, ':') ? AF_INET6 : AF_INET; + + if (!smtp_get_interface(tf->interface, host_af, addr, &interface, + US"callout") || + !smtp_get_port(tf->port, addr, &port, US"callout")) + log_write(0, LOG_MAIN|LOG_PANIC, "<%s>: %s", addr->address, + addr->message); + + if ( ( interface == cutthrough.interface + || ( interface + && cutthrough.interface + && Ustrcmp(interface, cutthrough.interface) == 0 + ) ) + && port == cutthrough.host.port + ) + { + uschar * resp = NULL; + + /* Match! Send the RCPT TO, set done from the response */ + done = + smtp_write_command(&ctblock, SCMD_FLUSH, "RCPT TO:<%.1000s>\r\n", + transport_rcpt_address(addr, + addr->transport->rcpt_include_affixes)) >= 0 && + cutthrough_response(cutthrough.fd, '2', &resp, CUTTHROUGH_DATA_TIMEOUT) == '2'; + + /* This would go horribly wrong if a callout fail was ignored by ACL. + We punt by abandoning cutthrough on a reject, like the + first-rcpt does. */ + + if (done) + { + address_item * na = store_get(sizeof(address_item)); + *na = cutthrough.addr; + cutthrough.addr = *addr; + cutthrough.addr.host_used = &cutthrough.host; + cutthrough.addr.next = na; + + cutthrough.nrcpt++; + } + else + { + cancel_cutthrough_connection(TRUE, US"recipient rejected"); + if (!resp || errno == ETIMEDOUT) + { + HDEBUG(D_verify) debug_printf("SMTP timeout\n"); + } + else if (errno == 0) + { + if (*resp == 0) + Ustrcpy(resp, US"connection dropped"); + + addr->message = + string_sprintf("response to \"%s\" was: %s", + big_buffer, string_printing(resp)); + + addr->user_message = + string_sprintf("Callout verification failed:\n%s", resp); + + /* Hard rejection ends the process */ + + if (resp[0] == '5') /* Address rejected */ + { + *yield = FAIL; + done = TRUE; + } + } + } + } + break; /* host_list */ + } +if (!done) + cancel_cutthrough_connection(TRUE, US"incompatible connection"); +return done; +} + + +/************************************************* +* Do callout verification for an address * +*************************************************/ + +/* This function is called from verify_address() when the address has routed to +a host list, and a callout has been requested. Callouts are expensive; that is +why a cache is used to improve the efficiency. + +Arguments: + addr the address that's been routed + host_list the list of hosts to try + tf the transport feedback block + + ifstring "interface" option from transport, or NULL + portstring "port" option from transport, or NULL + protocolstring "protocol" option from transport, or NULL + callout the per-command callout timeout + callout_overall the overall callout timeout (if < 0 use 4*callout) + callout_connect the callout connection timeout (if < 0 use callout) + options the verification options - these bits are used: + vopt_is_recipient => this is a recipient address + vopt_callout_no_cache => don't use callout cache + vopt_callout_fullpm => if postmaster check, do full one + vopt_callout_random => do the "random" thing + vopt_callout_recipsender => use real sender for recipient + vopt_callout_recippmaster => use postmaster for recipient + vopt_callout_hold => lazy close connection + se_mailfrom MAIL FROM address for sender verify; NULL => "" + pm_mailfrom if non-NULL, do the postmaster check with this sender + +Returns: OK/FAIL/DEFER +*/ + +static int +do_callout(address_item *addr, host_item *host_list, transport_feedback *tf, + int callout, int callout_overall, int callout_connect, int options, + uschar *se_mailfrom, uschar *pm_mailfrom) +{ +int yield = OK; +int old_domain_cache_result = ccache_accept; +BOOL done = FALSE; +uschar *address_key; +uschar *from_address; +uschar *random_local_part = NULL; +const uschar *save_deliver_domain = deliver_domain; +uschar **failure_ptr = options & vopt_is_recipient + ? &recipient_verify_failure : &sender_verify_failure; +dbdata_callout_cache new_domain_record; +dbdata_callout_cache_address new_address_record; +time_t callout_start_time; + +new_domain_record.result = ccache_unknown; +new_domain_record.postmaster_result = ccache_unknown; +new_domain_record.random_result = ccache_unknown; + +memset(&new_address_record, 0, sizeof(new_address_record)); + +/* For a recipient callout, the key used for the address cache record must +include the sender address if we are using the real sender in the callout, +because that may influence the result of the callout. */ + +if (options & vopt_is_recipient) + if (options & vopt_callout_recipsender) + { + from_address = sender_address; + address_key = string_sprintf("%s/<%s>", addr->address, sender_address); + if (cutthrough.delivery) options |= vopt_callout_no_cache; + } + else if (options & vopt_callout_recippmaster) + { + from_address = string_sprintf("postmaster@%s", qualify_domain_sender); + address_key = string_sprintf("%s/", addr->address, + qualify_domain_sender); + } + else + { + from_address = US""; + address_key = addr->address; + } + +/* For a sender callout, we must adjust the key if the mailfrom address is not +empty. */ + +else + { + from_address = se_mailfrom ? se_mailfrom : US""; + address_key = *from_address + ? string_sprintf("%s/<%s>", addr->address, from_address) : addr->address; + } + +if (cached_callout_lookup(addr, address_key, from_address, + &options, &pm_mailfrom, &yield, failure_ptr, + &new_domain_record, &old_domain_cache_result)) + { + cancel_cutthrough_connection(TRUE, US"cache-hit"); + goto END_CALLOUT; } if (!addr->transport) @@ -379,6 +573,7 @@ else { smtp_transport_options_block *ob = (smtp_transport_options_block *)addr->transport->options_block; + host_item * host; /* The information wasn't available in the cache, so we have to do a real callout and save the result in the cache for next time, unless no_cache is set, @@ -386,7 +581,7 @@ else with a random local part, ensure that such a local part is available. If not, log the fact, but carry on without randomising. */ - if (options & vopt_callout_random && callout_random_local_part != NULL) + if (options & vopt_callout_random && callout_random_local_part) if (!(random_local_part = expand_string(callout_random_local_part))) log_write(0, LOG_MAIN|LOG_PANIC, "failed to expand " "callout_random_local_part: %s", expand_string_message); @@ -406,14 +601,13 @@ else if (smtp_out && !disable_callout_flush) mac_smtp_fflush(); + clearflag(addr, af_verify_pmfail); /* postmaster callout flag */ + clearflag(addr, af_verify_nsfail); /* null sender callout flag */ + /* cutthrough-multi: if a nonfirst rcpt has the same routing as the first, and we are holding a cutthrough conn open, we can just append the rcpt to that conn for verification purposes (and later delivery also). Simplest -coding means skipping this whole loop and doing the append separately. - -We will need to remember it has been appended so that rcpt-acl tail code -can do it there for the non-rcpt-verify case. For this we keep an addresscount. -*/ +coding means skipping this whole loop and doing the append separately. */ /* Can we re-use an open cutthrough connection? */ if ( cutthrough.fd >= 0 @@ -422,99 +616,10 @@ can do it there for the non-rcpt-verify case. For this we keep an addresscount. && !random_local_part && !pm_mailfrom ) - { - if (addr->transport == cutthrough.addr.transport) - for (host = host_list; host; host = host->next) - if (Ustrcmp(host->address, cutthrough.host.address) == 0) - { - int host_af; - uschar *interface = NULL; /* Outgoing interface to use; NULL => any */ - int port = 25; - - deliver_host = host->name; - deliver_host_address = host->address; - deliver_host_port = host->port; - deliver_domain = addr->domain; - transport_name = addr->transport->name; - - host_af = (Ustrchr(host->address, ':') == NULL)? AF_INET:AF_INET6; - - if (!smtp_get_interface(tf->interface, host_af, addr, &interface, - US"callout") || - !smtp_get_port(tf->port, addr, &port, US"callout")) - log_write(0, LOG_MAIN|LOG_PANIC, "<%s>: %s", addr->address, - addr->message); - - if ( ( interface == cutthrough.interface - || ( interface - && cutthrough.interface - && Ustrcmp(interface, cutthrough.interface) == 0 - ) ) - && port == cutthrough.host.port - ) - { - uschar * resp = NULL; - - /* Match! Send the RCPT TO, append the addr, set done */ - done = - smtp_write_command(&ctblock, FALSE, "RCPT TO:<%.1000s>\r\n", - transport_rcpt_address(addr, - (addr->transport == NULL)? FALSE : - addr->transport->rcpt_include_affixes)) >= 0 && - cutthrough_response('2', &resp, CUTTHROUGH_DATA_TIMEOUT) == '2'; - - /* This would go horribly wrong if a callout fail was ignored by ACL. - We punt by abandoning cutthrough on a reject, like the - first-rcpt does. */ - - if (done) - { - address_item * na = store_get(sizeof(address_item)); - *na = cutthrough.addr; - cutthrough.addr = *addr; - cutthrough.addr.host_used = &cutthrough.host; - cutthrough.addr.next = na; - - cutthrough.nrcpt++; - } - else - { - cancel_cutthrough_connection("recipient rejected"); - if (!resp || errno == ETIMEDOUT) - { - HDEBUG(D_verify) debug_printf("SMTP timeout\n"); - } - else if (errno == 0) - { - if (*resp == 0) - Ustrcpy(resp, US"connection dropped"); - - addr->message = - string_sprintf("response to \"%s\" from %s [%s] was: %s", - big_buffer, host->name, host->address, - string_printing(resp)); - - addr->user_message = - string_sprintf("Callout verification failed:\n%s", resp); - - /* Hard rejection ends the process */ - - if (resp[0] == '5') /* Address rejected */ - { - yield = FAIL; - done = TRUE; - } - } - } - } - break; /* host_list */ - } - if (!done) - cancel_cutthrough_connection("incompatible connection"); - } + done = cutthrough_multi(addr, host_list, tf, &yield); - /* Now make connections to the hosts and do real callouts. The list of hosts - is passed in as an argument. */ + /* If we did not use a cached connection, make connections to the hosts + and do real callouts. The list of hosts is passed in as an argument. */ for (host = host_list; host && !done; host = host->next) { @@ -522,12 +627,6 @@ can do it there for the non-rcpt-verify case. For this we keep an addresscount. int port = 25; uschar *interface = NULL; /* Outgoing interface to use; NULL => any */ smtp_context sx; - uschar responsebuffer[4096]; - - clearflag(addr, af_verify_pmfail); /* postmaster callout flag */ - clearflag(addr, af_verify_nsfail); /* null sender callout flag */ - - /* Skip this host if we don't have an IP address for it. */ if (!host->address) { @@ -546,7 +645,7 @@ can do it there for the non-rcpt-verify case. For this we keep an addresscount. /* Set IPv4 or IPv6 */ - host_af = Ustrchr(host->address, ':') == NULL ? AF_INET : AF_INET6; + host_af = Ustrchr(host->address, ':') ? AF_INET6 : AF_INET; /* Expand and interpret the interface and port strings. The latter will not be used if there is a host-specific port (e.g. from a manualroute router). @@ -574,6 +673,7 @@ can do it there for the non-rcpt-verify case. For this we keep an addresscount. sx.interface = interface; sx.helo_data = tf->helo_data; sx.tblock = addr->transport; + sx.verify = TRUE; tls_retry_connection: /* Set the address state so that errors are recorded in it */ @@ -586,21 +686,23 @@ tls_retry_connection: SMTP command to send. If we tried TLS but it failed, try again without if permitted */ - if ( (yield = smtp_setup_conn(&sx, FALSE, TRUE)) == DEFER + yield = smtp_setup_conn(&sx, FALSE); +#ifdef SUPPORT_TLS + if ( yield == DEFER && addr->basic_errno == ERRNO_TLSFAILURE && ob->tls_tempfail_tryclear && verify_check_given_host(&ob->hosts_require_tls, host) != OK ) { - log_write(0, LOG_MAIN, "TLS session failure:" - " callout unencrypted to %s [%s] (not in hosts_require_tls)", - host->name, host->address); - yield = smtp_setup_conn(&sx, TRUE, TRUE); + log_write(0, LOG_MAIN, + "%s: callout unencrypted to %s [%s] (not in hosts_require_tls)", + addr->message, host->name, host->address); + addr->transport_return = PENDING_DEFER; + yield = smtp_setup_conn(&sx, TRUE); } +#endif if (yield != OK) { - if (addr->message) addr->message = string_sprintf("%s [%s] %s", - host->name, host->address, addr->message); errno = addr->basic_errno; transport_name = NULL; deliver_host = deliver_host_address = NULL; @@ -626,66 +728,21 @@ tls_retry_connection: addr->authenticator = client_authenticator; addr->auth_id = client_authenticated_id; - /* Build a mail-AUTH string (re-using responsebuffer for convenience */ - - done = - !smtp_mail_auth_str(responsebuffer, sizeof(responsebuffer), addr, ob) - && ( - (addr->auth_sndr = client_authenticated_sender), - - /* Send the MAIL command */ - - (smtp_write_command(&sx.outblock, FALSE, -#ifdef SUPPORT_I18N - addr->prop.utf8_msg && !addr->prop.utf8_downcvt - ? "MAIL FROM:<%s>%s%s SMTPUTF8\r\n" - : -#endif - "MAIL FROM:<%s>%s%s\r\n", - from_address, - responsebuffer, - options & vopt_is_recipient && sx.peer_offered & PEER_OFFERED_SIZE - ? string_sprintf(" SIZE=%d", message_size + ob->size_addition) - : US"" - - ) >= 0) - ) - - && smtp_read_response(&sx.inblock, responsebuffer, sizeof(responsebuffer), - '2', callout); + sx.from_addr = from_address; + sx.first_addr = sx.sync_addr = addr; + sx.ok = FALSE; /*XXX these 3 last might not be needed for verify? */ + sx.send_rset = TRUE; + sx.completed_addr = FALSE; - deliver_host = deliver_host_address = NULL; - deliver_domain = save_deliver_domain; + new_domain_record.result = old_domain_cache_result == ccache_reject_mfnull + ? ccache_reject_mfnull : ccache_accept; - /* If the host does not accept MAIL FROM:<>, arrange to cache this - information, but again, don't record anything for an I/O error or a defer. Do - not cache rejections of MAIL when a non-empty sender has been used, because - that blocks the whole domain for all senders. */ + /* Do the random local part check first. Temporarily replace the recipient + with the "random" value */ - if (!done) - { - *failure_ptr = US"mail"; /* At or before MAIL */ - if (errno == 0 && responsebuffer[0] == '5') - { - setflag(addr, af_verify_nsfail); - if (from_address[0] == 0) - new_domain_record.result = ccache_reject_mfnull; - } - } - - /* Otherwise, proceed to check a "random" address (if required), then the - given address, and the postmaster address (if required). Between each check, - issue RSET, because some servers accept only one recipient after MAIL - FROM:<>. - - Before doing this, set the result in the domain cache record to "accept", - unless its previous value was ccache_reject_mfnull. In that case, the domain - rejects MAIL FROM:<> and we want to continue to remember that. When that is - the case, we have got here only in the case of a recipient verification with - a non-null sender. */ - - else + if (random_local_part) { + uschar * main_address = addr->address; const uschar * rcpt_domain = addr->domain; #ifdef SUPPORT_I18N @@ -703,180 +760,188 @@ tls_retry_connection: } #endif - new_domain_record.result = - (old_domain_cache_result == ccache_reject_mfnull)? - ccache_reject_mfnull: ccache_accept; - - /* Do the random local part check first */ - - if (random_local_part != NULL) - { - uschar randombuffer[1024]; - BOOL random_ok = - smtp_write_command(&sx.outblock, FALSE, - "RCPT TO:<%.1000s@%.1000s>\r\n", random_local_part, - rcpt_domain) >= 0 && - smtp_read_response(&sx.inblock, randombuffer, - sizeof(randombuffer), '2', callout); - - /* Remember when we last did a random test */ - - new_domain_record.random_stamp = time(NULL); - - /* If accepted, we aren't going to do any further tests below. */ - - if (random_ok) - new_domain_record.random_result = ccache_accept; - - /* Otherwise, cache a real negative response, and get back to the right - state to send RCPT. Unless there's some problem such as a dropped - connection, we expect to succeed, because the commands succeeded above. - However, some servers drop the connection after responding to an - invalid recipient, so on (any) error we drop and remake the connection. - */ - - else if (errno == 0) - { - /* This would be ok for 1st rcpt a cutthrough, but no way to - handle a subsequent. So refuse to support any */ - cancel_cutthrough_connection("random-recipient"); - - if (randombuffer[0] == '5') - new_domain_record.random_result = ccache_reject; + /* This would be ok for 1st rcpt of a cutthrough (the case handled here; + subsequents are done in cutthrough_multi()), but no way to + handle a subsequent because of the RSET vaporising the MAIL FROM. + So refuse to support any. Most cutthrough use will not involve + random_local_part, so no loss. */ + cancel_cutthrough_connection(TRUE, US"random-recipient"); + + addr->address = string_sprintf("%s@%.1000s", + random_local_part, rcpt_domain); + done = FALSE; + + /* If accepted, we aren't going to do any further tests below. + Otherwise, cache a real negative response, and get back to the right + state to send RCPT. Unless there's some problem such as a dropped + connection, we expect to succeed, because the commands succeeded above. + However, some servers drop the connection after responding to an + invalid recipient, so on (any) error we drop and remake the connection. + XXX We don't care about that for postmaster_full. Should we? + + XXX could we add another flag to the context, and have the common + code emit the RSET too? Even pipelined after the RCPT... + Then the main-verify call could use it if there's to be a subsequent + postmaster-verify. + The sync_responses() would need to be taught about it and we'd + need another return code filtering out to here. + */ - done = - smtp_write_command(&sx.outblock, FALSE, "RSET\r\n") >= 0 && - smtp_read_response(&sx.inblock, responsebuffer, sizeof(responsebuffer), - '2', callout) && + /* Remember when we last did a random test */ + new_domain_record.random_stamp = time(NULL); - smtp_write_command(&sx.outblock, FALSE, -#ifdef SUPPORT_I18N - addr->prop.utf8_msg && !addr->prop.utf8_downcvt - ? "MAIL FROM:<%s> SMTPUTF8\r\n" - : -#endif - "MAIL FROM:<%s>\r\n", - from_address) >= 0 && - smtp_read_response(&sx.inblock, responsebuffer, sizeof(responsebuffer), - '2', callout); + if (smtp_write_mail_and_rcpt_cmds(&sx, &yield) == 0) + switch(addr->transport_return) + { + case PENDING_OK: + new_domain_record.random_result = ccache_accept; + break; + case FAIL: + new_domain_record.random_result = ccache_reject; + + /* Between each check, issue RSET, because some servers accept only + one recipient after MAIL FROM:<>. + XXX We don't care about that for postmaster_full. Should we? */ + + if ((done = + smtp_write_command(&sx.outblock, SCMD_FLUSH, "RSET\r\n") >= 0 && + smtp_read_response(&sx.inblock, sx.buffer, sizeof(sx.buffer), + '2', callout))) + break; - if (!done) - { HDEBUG(D_acl|D_v) - debug_printf("problem after random/rset/mfrom; reopen conn\n"); + debug_printf_indent("problem after random/rset/mfrom; reopen conn\n"); random_local_part = NULL; #ifdef SUPPORT_TLS tls_close(FALSE, TRUE); #endif - HDEBUG(D_transport|D_acl|D_v) debug_printf(" SMTP(close)>>\n"); + HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP(close)>>\n"); (void)close(sx.inblock.sock); + sx.inblock.sock = sx.outblock.sock = -1; #ifndef DISABLE_EVENT (void) event_raise(addr->transport->event_action, US"tcp:close", NULL); #endif + addr->address = main_address; + addr->transport_return = PENDING_DEFER; + sx.first_addr = sx.sync_addr = addr; + sx.ok = FALSE; + sx.send_rset = TRUE; + sx.completed_addr = FALSE; goto tls_retry_connection; - } - } - else done = FALSE; /* Some timeout/connection problem */ - } /* Random check */ + } - /* If the host is accepting all local parts, as determined by the "random" - check, we don't need to waste time doing any further checking. */ + /* Re-setup for main verify, or for the error message when failing */ + addr->address = main_address; + addr->transport_return = PENDING_DEFER; + sx.first_addr = sx.sync_addr = addr; + sx.ok = FALSE; + sx.send_rset = TRUE; + sx.completed_addr = FALSE; + } + else + done = TRUE; - if (new_domain_record.random_result != ccache_accept && done) - { - /* Get the rcpt_include_affixes flag from the transport if there is one, - but assume FALSE if there is not. */ + /* Main verify. If the host is accepting all local parts, as determined + by the "random" check, we don't need to waste time doing any further + checking. */ - uschar * rcpt = transport_rcpt_address(addr, - addr->transport ? addr->transport->rcpt_include_affixes : FALSE); + if (done) + { + done = FALSE; + switch(smtp_write_mail_and_rcpt_cmds(&sx, &yield)) + { + case 0: switch(addr->transport_return) /* ok so far */ + { + case PENDING_OK: done = TRUE; + new_address_record.result = ccache_accept; + break; + case FAIL: done = TRUE; + yield = FAIL; + *failure_ptr = US"recipient"; + new_address_record.result = ccache_reject; + break; + default: break; + } + break; + + case -1: /* MAIL response error */ + *failure_ptr = US"mail"; + if (errno == 0 && sx.buffer[0] == '5') + { + setflag(addr, af_verify_nsfail); + if (from_address[0] == 0) + new_domain_record.result = ccache_reject_mfnull; + } + break; + /* non-MAIL read i/o error */ + /* non-MAIL response timeout */ + /* internal error; channel still usable */ + default: break; /* transmit failed */ + } + } -#ifdef SUPPORT_I18N - /*XXX should the conversion be moved into transport_rcpt_address() ? */ - if ( testflag(addr, af_utf8_downcvt) - && !(rcpt = string_address_utf8_to_alabel(rcpt, NULL)) - ) - { - errno = ERRNO_EXPANDFAIL; - *failure_ptr = US"recipient"; - done = FALSE; - } - else -#endif + addr->auth_sndr = client_authenticated_sender; - done = - smtp_write_command(&sx.outblock, FALSE, "RCPT TO:<%.1000s>\r\n", - rcpt) >= 0 && - smtp_read_response(&sx.inblock, responsebuffer, sizeof(responsebuffer), - '2', callout); + deliver_host = deliver_host_address = NULL; + deliver_domain = save_deliver_domain; - if (done) - new_address_record.result = ccache_accept; - else if (errno == 0 && responsebuffer[0] == '5') - { - *failure_ptr = US"recipient"; - new_address_record.result = ccache_reject; - } + /* Do postmaster check if requested; if a full check is required, we + check for RCPT TO: (no domain) in accordance with RFC 821. */ - /* Do postmaster check if requested; if a full check is required, we - check for RCPT TO: (no domain) in accordance with RFC 821. */ + if (done && pm_mailfrom) + { + /* Could possibly shift before main verify, just above, and be ok + for cutthrough. But no way to handle a subsequent rcpt, so just + refuse any */ + cancel_cutthrough_connection(TRUE, US"postmaster verify"); + HDEBUG(D_acl|D_v) debug_printf_indent("Cutthrough cancelled by presence of postmaster verify\n"); - if (done && pm_mailfrom) - { - /* Could possibly shift before main verify, just above, and be ok - for cutthrough. But no way to handle a subsequent rcpt, so just - refuse any */ - cancel_cutthrough_connection("postmaster verify"); - HDEBUG(D_acl|D_v) debug_printf("Cutthrough cancelled by presence of postmaster verify\n"); - - done = - smtp_write_command(&sx.outblock, FALSE, "RSET\r\n") >= 0 && - smtp_read_response(&sx.inblock, responsebuffer, - sizeof(responsebuffer), '2', callout) && - - smtp_write_command(&sx.outblock, FALSE, - "MAIL FROM:<%s>\r\n", pm_mailfrom) >= 0 && - smtp_read_response(&sx.inblock, responsebuffer, - sizeof(responsebuffer), '2', callout) && - - /* First try using the current domain */ - - (( - smtp_write_command(&sx.outblock, FALSE, - "RCPT TO:\r\n", rcpt_domain) >= 0 && - smtp_read_response(&sx.inblock, responsebuffer, - sizeof(responsebuffer), '2', callout) - ) - - || - - /* If that doesn't work, and a full check is requested, - try without the domain. */ - - ( - (options & vopt_callout_fullpm) != 0 && - smtp_write_command(&sx.outblock, FALSE, - "RCPT TO:\r\n") >= 0 && - smtp_read_response(&sx.inblock, responsebuffer, - sizeof(responsebuffer), '2', callout) - )); - - /* Sort out the cache record */ - - new_domain_record.postmaster_stamp = time(NULL); - - if (done) - new_domain_record.postmaster_result = ccache_accept; - else if (errno == 0 && responsebuffer[0] == '5') - { - *failure_ptr = US"postmaster"; - setflag(addr, af_verify_pmfail); - new_domain_record.postmaster_result = ccache_reject; - } - } - } /* Random not accepted */ - } /* MAIL FROM: accepted */ + done = smtp_write_command(&sx.outblock, SCMD_FLUSH, "RSET\r\n") >= 0 + && smtp_read_response(&sx.inblock, sx.buffer, + sizeof(sx.buffer), '2', callout); + + if (done) + { + uschar * main_address = addr->address; + + /*XXX oops, affixes */ + addr->address = string_sprintf("postmaster@%.1000s", addr->domain); + addr->transport_return = PENDING_DEFER; + + sx.from_addr = pm_mailfrom; + sx.first_addr = sx.sync_addr = addr; + sx.ok = FALSE; + sx.send_rset = TRUE; + sx.completed_addr = FALSE; + + if( smtp_write_mail_and_rcpt_cmds(&sx, &yield) == 0 + && addr->transport_return == PENDING_OK + ) + done = TRUE; + else + done = (options & vopt_callout_fullpm) != 0 + && smtp_write_command(&sx.outblock, SCMD_FLUSH, + "RCPT TO:\r\n") >= 0 + && smtp_read_response(&sx.inblock, sx.buffer, + sizeof(sx.buffer), '2', callout); + + /* Sort out the cache record */ + new_domain_record.postmaster_stamp = time(NULL); + + if (done) + new_domain_record.postmaster_result = ccache_accept; + else if (errno == 0 && sx.buffer[0] == '5') + { + *failure_ptr = US"postmaster"; + setflag(addr, af_verify_pmfail); + new_domain_record.postmaster_result = ccache_reject; + } + + addr->address = main_address; + } + } /* For any failure of the main check, other than a negative response, we just close the connection and carry on. We can identify a negative response by the fact that errno is zero. For I/O errors it will be non-zero @@ -888,7 +953,7 @@ tls_retry_connection: is not to be widely broadcast. */ no_conn: - if (!done) switch(errno) + switch(errno) { case ETIMEDOUT: HDEBUG(D_verify) debug_printf("SMTP timeout\n"); @@ -901,8 +966,7 @@ no_conn: extern int acl_where; /* src/acl.c */ errno = 0; addr->message = string_sprintf( - "response to \"EHLO\" from %s [%s] did not include SMTPUTF8", - host->name, host->address); + "response to \"EHLO\" did not include SMTPUTF8"); addr->user_message = acl_where == ACL_WHERE_RCPT ? US"533 no support for internationalised mailbox name" : US"550 mailbox unavailable"; @@ -916,21 +980,25 @@ no_conn: break; case 0: - if (*responsebuffer == 0) Ustrcpy(responsebuffer, US"connection dropped"); + if (*sx.buffer == 0) Ustrcpy(sx.buffer, US"connection dropped"); - addr->message = - string_sprintf("response to \"%s\" from %s [%s] was: %s", - big_buffer, host->name, host->address, - string_printing(responsebuffer)); + /*XXX test here is ugly; seem to have a split of responsibility for + building this message. Need to reationalise. Where is it done + before here, and when not? + Not == 5xx resp to MAIL on main-verify + */ + if (!addr->message) addr->message = + string_sprintf("response to \"%s\" was: %s", + big_buffer, string_printing(sx.buffer)); addr->user_message = options & vopt_is_recipient - ? string_sprintf("Callout verification failed:\n%s", responsebuffer) + ? string_sprintf("Callout verification failed:\n%s", sx.buffer) : string_sprintf("Called: %s\nSent: %s\nResponse: %s", - host->address, big_buffer, responsebuffer); + host->address, big_buffer, sx.buffer); /* Hard rejection ends the process */ - if (responsebuffer[0] == '5') /* Address rejected */ + if (sx.buffer[0] == '5') /* Address rejected */ { yield = FAIL; done = TRUE; @@ -942,8 +1010,10 @@ no_conn: /* Cutthrough - on a successful connect and recipient-verify with use-sender and we are 1st rcpt and have no cutthrough conn so far - here is where we want to leave the conn open */ - if ( cutthrough.delivery + here is where we want to leave the conn open. Ditto for a lazy-close + verify. */ + + if ( (cutthrough.delivery || options & vopt_callout_hold) && rcpt_count == 1 && done && yield == OK @@ -955,14 +1025,29 @@ no_conn: && !sx.lmtp ) { - HDEBUG(D_acl|D_v) debug_printf("holding verify callout open for cutthrough delivery\n"); - - cutthrough.fd = sx.outblock.sock; /* We assume no buffer in use in the outblock */ - cutthrough.nrcpt = 1; - cutthrough.interface = interface; - cutthrough.host = *host; - cutthrough.addr = *addr; /* Save the address_item for later logging */ - cutthrough.addr.next = NULL; + HDEBUG(D_acl|D_v) debug_printf_indent("holding verify callout open for %s\n", + cutthrough.delivery + ? "cutthrough delivery" : "potential further verifies and delivery"); + + cutthrough.callout_hold_only = !cutthrough.delivery; + cutthrough.is_tls = tls_out.active >= 0; + cutthrough.fd = sx.outblock.sock; /* We assume no buffer in use in the outblock */ + cutthrough.nrcpt = 1; + cutthrough.transport = addr->transport->name; + cutthrough.interface = interface; + cutthrough.snd_port = sending_port; + cutthrough.peer_options = smtp_peer_options; + cutthrough.host = *host; + { + int oldpool = store_pool; + store_pool = POOL_PERM; + cutthrough.snd_ip = string_copy(sending_ip_address); + cutthrough.host.name = string_copy(host->name); + cutthrough.host.address = string_copy(host->address); + store_pool = oldpool; + } + cutthrough.addr = *addr; /* Save the address_item for later logging */ + cutthrough.addr.next = NULL; cutthrough.addr.host_used = &cutthrough.host; if (addr->parent) *(cutthrough.addr.parent = store_get(sizeof(address_item))) = @@ -975,15 +1060,15 @@ no_conn: } else { - /* Ensure no cutthrough on multiple address verifies */ + /* Ensure no cutthrough on multiple verifies that were incompatible */ if (options & vopt_callout_recipsender) - cancel_cutthrough_connection("not usable for cutthrough"); + cancel_cutthrough_connection(TRUE, US"not usable for cutthrough"); if (sx.send_quit) { - (void) smtp_write_command(&sx.outblock, FALSE, "QUIT\r\n"); + (void) smtp_write_command(&sx.outblock, SCMD_FLUSH, "QUIT\r\n"); /* Wait a short time for response, and discard it */ - smtp_read_response(&sx.inblock, responsebuffer, sizeof(responsebuffer), + smtp_read_response(&sx.inblock, sx.buffer, sizeof(sx.buffer), '2', 1); } @@ -992,7 +1077,7 @@ no_conn: #ifdef SUPPORT_TLS tls_close(FALSE, TRUE); #endif - HDEBUG(D_transport|D_acl|D_v) debug_printf(" SMTP(close)>>\n"); + HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP(close)>>\n"); (void)close(sx.inblock.sock); sx.inblock.sock = sx.outblock.sock = -1; #ifndef DISABLE_EVENT @@ -1001,70 +1086,26 @@ no_conn: } } + if (!done || yield != OK) + addr->message = string_sprintf("%s [%s] : %s", host->name, host->address, + addr->message); } /* Loop through all hosts, while !done */ } /* If we get here with done == TRUE, a successful callout happened, and yield will be set OK or FAIL according to the response to the RCPT command. Otherwise, we looped through the hosts but couldn't complete the business. -However, there may be domain-specific information to cache in both cases. +However, there may be domain-specific information to cache in both cases. */ -The value of the result field in the new_domain record is ccache_unknown if -there was an error before or with MAIL FROM:, and errno was not zero, -implying some kind of I/O error. We don't want to write the cache in that case. -Otherwise the value is ccache_accept, ccache_reject, or ccache_reject_mfnull. */ - -if ( !(options & vopt_callout_no_cache) - && new_domain_record.result != ccache_unknown) - { - if ((dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE)) - == NULL) - { - HDEBUG(D_verify) debug_printf("callout cache: not available\n"); - } - else - { - (void)dbfn_write(dbm_file, addr->domain, &new_domain_record, - (int)sizeof(dbdata_callout_cache)); - HDEBUG(D_verify) debug_printf("wrote callout cache domain record for %s:\n" - " result=%d postmaster=%d random=%d\n", - addr->domain, - new_domain_record.result, - new_domain_record.postmaster_result, - new_domain_record.random_result); - } - } - -/* If a definite result was obtained for the callout, cache it unless caching -is disabled. */ - -if (done) - { - if ( !(options & vopt_callout_no_cache) - && new_address_record.result != ccache_unknown) - { - if (!dbm_file) - dbm_file = dbfn_open(US"callout", O_RDWR|O_CREAT, &dbblock, FALSE); - if (!dbm_file) - { - HDEBUG(D_verify) debug_printf("no callout cache available\n"); - } - else - { - (void)dbfn_write(dbm_file, address_key, &new_address_record, - (int)sizeof(dbdata_callout_cache_address)); - HDEBUG(D_verify) debug_printf("wrote %s callout cache address record for %s\n", - new_address_record.result == ccache_accept ? "positive" : "negative", - address_key); - } - } - } /* done */ +if (!(options & vopt_callout_no_cache)) + cache_callout_write(&new_domain_record, addr->domain, + done, &new_address_record, address_key); /* Failure to connect to any host, or any response other than 2xx or 5xx is a temporary error. If there was only one host, and a response was received, leave it alone if supplying details. Otherwise, give a generic response. */ -else /* !done */ +if (!done) { uschar * dullmsg = string_sprintf("Could not complete %s verify callout", options & vopt_is_recipient ? "recipient" : "sender"); @@ -1093,7 +1134,6 @@ else /* !done */ /* Come here from within the cache-reading code on fast-track exit. */ END_CALLOUT: -if (dbm_file) dbfn_close(dbm_file); tls_modify_variables(&tls_in); return yield; } @@ -1114,7 +1154,7 @@ int rc; get rewritten. */ addr2 = *addr; -HDEBUG(D_acl) debug_printf("----------- %s cutthrough setup ------------\n", +HDEBUG(D_acl) debug_printf_indent("----------- %s cutthrough setup ------------\n", rcpt_count > 1 ? "more" : "start"); rc = verify_address(&addr2, NULL, vopt_is_recipient | vopt_callout_recipsender | vopt_callout_no_cache, @@ -1122,7 +1162,7 @@ rc = verify_address(&addr2, NULL, NULL, NULL, NULL); addr->message = addr2.message; addr->user_message = addr2.user_message; -HDEBUG(D_acl) debug_printf("----------- end cutthrough setup ------------\n"); +HDEBUG(D_acl) debug_printf_indent("----------- end cutthrough setup ------------\n"); return rc; } @@ -1147,7 +1187,7 @@ if( return TRUE; } -HDEBUG(D_transport|D_acl) debug_printf("cutthrough_send failed: %s\n", strerror(errno)); +HDEBUG(D_transport|D_acl) debug_printf_indent("cutthrough_send failed: %s\n", strerror(errno)); return FALSE; } @@ -1168,20 +1208,27 @@ return TRUE; } /* Buffered output of counted data block. Return boolean success */ -BOOL +static BOOL cutthrough_puts(uschar * cp, int n) { if (cutthrough.fd < 0) return TRUE; if (_cutthrough_puts(cp, n)) return TRUE; -cancel_cutthrough_connection("transmit failed"); +cancel_cutthrough_connection(TRUE, US"transmit failed"); return FALSE; } +void +cutthrough_data_puts(uschar * cp, int n) +{ +if (cutthrough.delivery) (void) cutthrough_puts(cp, n); +return; +} + static BOOL _cutthrough_flush_send(void) { -int n= ctblock.ptr-ctblock.buffer; +int n = ctblock.ptr - ctblock.buffer; if(n>0) if(!cutthrough_send(n)) @@ -1195,21 +1242,28 @@ BOOL cutthrough_flush_send(void) { if (_cutthrough_flush_send()) return TRUE; -cancel_cutthrough_connection("transmit failed"); +cancel_cutthrough_connection(TRUE, US"transmit failed"); return FALSE; } -BOOL +static BOOL cutthrough_put_nl(void) { return cutthrough_puts(US"\r\n", 2); } +void +cutthrough_data_put_nl(void) +{ +cutthrough_data_puts(US"\r\n", 2); +} + + /* Get and check response from cutthrough target */ static uschar -cutthrough_response(char expect, uschar ** copy, int timeout) +cutthrough_response(int fd, char expect, uschar ** copy, int timeout) { smtp_inblock inblock; uschar inbuffer[4096]; @@ -1219,12 +1273,12 @@ inblock.buffer = inbuffer; inblock.buffersize = sizeof(inbuffer); inblock.ptr = inbuffer; inblock.ptrend = inbuffer; -inblock.sock = cutthrough.fd; +inblock.sock = fd; /* this relies on (inblock.sock == tls_out.active) */ if(!smtp_read_response(&inblock, responsebuffer, sizeof(responsebuffer), expect, timeout)) - cancel_cutthrough_connection("target timeout on read"); + cancel_cutthrough_connection(TRUE, US"target timeout on read"); -if(copy != NULL) +if(copy) { uschar * cp; *copy = cp = string_copy(responsebuffer); @@ -1242,21 +1296,21 @@ return responsebuffer[0]; BOOL cutthrough_predata(void) { -if(cutthrough.fd < 0) +if(cutthrough.fd < 0 || cutthrough.callout_hold_only) return FALSE; -HDEBUG(D_transport|D_acl|D_v) debug_printf(" SMTP>> DATA\n"); +HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP>> DATA\n"); cutthrough_puts(US"DATA\r\n", 6); cutthrough_flush_send(); /* Assume nothing buffered. If it was it gets ignored. */ -return cutthrough_response('3', NULL, CUTTHROUGH_DATA_TIMEOUT) == '3'; +return cutthrough_response(cutthrough.fd, '3', NULL, CUTTHROUGH_DATA_TIMEOUT) == '3'; } -/* fd and tctx args only to match write_chunk() */ +/* tctx arg only to match write_chunk() */ static BOOL -cutthrough_write_chunk(int fd, transport_ctx * tctx, uschar * s, int len) +cutthrough_write_chunk(transport_ctx * tctx, uschar * s, int len) { uschar * s2; while(s && (s2 = Ustrchr(s, '\n'))) @@ -1277,61 +1331,75 @@ cutthrough_headers_send(void) { transport_ctx tctx; -if(cutthrough.fd < 0) +if(cutthrough.fd < 0 || cutthrough.callout_hold_only) return FALSE; /* We share a routine with the mainline transport to handle header add/remove/rewrites, but having a separate buffered-output function (for now) */ -HDEBUG(D_acl) debug_printf("----------- start cutthrough headers send -----------\n"); +HDEBUG(D_acl) debug_printf_indent("----------- start cutthrough headers send -----------\n"); +tctx.u.fd = cutthrough.fd; tctx.tblock = cutthrough.addr.transport; tctx.addr = &cutthrough.addr; tctx.check_string = US"."; tctx.escape_string = US".."; +/*XXX check under spool_files_wireformat. Might be irrelevant */ tctx.options = topt_use_crlf; -if (!transport_headers_send(cutthrough.fd, &tctx, &cutthrough_write_chunk)) +if (!transport_headers_send(&tctx, &cutthrough_write_chunk)) return FALSE; -HDEBUG(D_acl) debug_printf("----------- done cutthrough headers send ------------\n"); +HDEBUG(D_acl) debug_printf_indent("----------- done cutthrough headers send ------------\n"); return TRUE; } static void -close_cutthrough_connection(const char * why) +close_cutthrough_connection(const uschar * why) { -if(cutthrough.fd >= 0) +int fd = cutthrough.fd; +if(fd >= 0) { /* We could be sending this after a bunch of data, but that is ok as the only way to cancel the transfer in dataphase is to drop the tcp conn before the final dot. */ ctblock.ptr = ctbuffer; - HDEBUG(D_transport|D_acl|D_v) debug_printf(" SMTP>> QUIT\n"); + HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP>> QUIT\n"); _cutthrough_puts(US"QUIT\r\n", 6); /* avoid recursion */ _cutthrough_flush_send(); + cutthrough.fd = -1; /* avoid recursion via read timeout */ /* Wait a short time for response, and discard it */ - cutthrough_response('2', NULL, 1); + cutthrough_response(fd, '2', NULL, 1); - #ifdef SUPPORT_TLS +#ifdef SUPPORT_TLS tls_close(FALSE, TRUE); - #endif - HDEBUG(D_transport|D_acl|D_v) debug_printf(" SMTP(close)>>\n"); - (void)close(cutthrough.fd); - cutthrough.fd = -1; - HDEBUG(D_acl) debug_printf("----------- cutthrough shutdown (%s) ------------\n", why); +#endif + HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP(close)>>\n"); + (void)close(fd); + HDEBUG(D_acl) debug_printf_indent("----------- cutthrough shutdown (%s) ------------\n", why); } ctblock.ptr = ctbuffer; } void -cancel_cutthrough_connection(const char * why) +cancel_cutthrough_connection(BOOL close_noncutthrough_verifies, const uschar * why) +{ +if (cutthrough.delivery || close_noncutthrough_verifies) + close_cutthrough_connection(why); +cutthrough.delivery = cutthrough.callout_hold_only = FALSE; +} + + +void +release_cutthrough_connection(const uschar * why) { -close_cutthrough_connection(why); -cutthrough.delivery = FALSE; +if (cutthrough.fd < 0) return; +HDEBUG(D_acl) debug_printf_indent("release cutthrough conn: %s\n", why); +cutthrough.fd = -1; +cutthrough.delivery = cutthrough.callout_hold_only = FALSE; } @@ -1347,7 +1415,7 @@ cutthrough_finaldot(void) { uschar res; address_item * addr; -HDEBUG(D_transport|D_acl|D_v) debug_printf(" SMTP>> .\n"); +HDEBUG(D_transport|D_acl|D_v) debug_printf_indent(" SMTP>> .\n"); /* Assume data finshed with new-line */ if( !cutthrough_puts(US".", 1) @@ -1356,7 +1424,7 @@ if( !cutthrough_puts(US".", 1) ) return cutthrough.addr.message; -res = cutthrough_response('2', &cutthrough.addr.message, CUTTHROUGH_DATA_TIMEOUT); +res = cutthrough_response(cutthrough.fd, '2', &cutthrough.addr.message, CUTTHROUGH_DATA_TIMEOUT); for (addr = &cutthrough.addr; addr; addr = addr->next) { addr->message = cutthrough.addr.message; @@ -1364,7 +1432,7 @@ for (addr = &cutthrough.addr; addr; addr = addr->next) { case '2': delivery_log(LOG_MAIN, addr, (int)'>', NULL); - close_cutthrough_connection("delivered"); + close_cutthrough_connection(US"delivered"); break; case '4': @@ -1787,7 +1855,7 @@ while (addr_new) dnssec_domains = &ob->dnssec; } - (void)host_find_bydns(host, NULL, flags, NULL, NULL, NULL, + (void) host_find_bydns(host, NULL, flags, NULL, NULL, NULL, dnssec_domains, NULL, NULL); } } @@ -1863,7 +1931,7 @@ while (addr_new) } respond_printf(f, "%s\n", cr); } - cancel_cutthrough_connection("routing hard fail"); + cancel_cutthrough_connection(TRUE, US"routing hard fail"); if (!full_info) { @@ -1902,7 +1970,7 @@ while (addr_new) } respond_printf(f, "%s\n", cr); } - cancel_cutthrough_connection("routing soft fail"); + cancel_cutthrough_connection(TRUE, US"routing soft fail"); if (!full_info) { @@ -1975,7 +2043,7 @@ while (addr_new) /* If stopped because more than one new address, cannot cutthrough */ if (addr_new && addr_new->next) - cancel_cutthrough_connection("multiple addresses from routing"); + cancel_cutthrough_connection(TRUE, US"multiple addresses from routing"); yield = OK; goto out; @@ -2225,18 +2293,16 @@ verify_check_header_names_ascii(uschar **msgptr) header_line *h; uschar *colon, *s; -for (h = header_list; h != NULL; h = h->next) +for (h = header_list; h; h = h->next) { - colon = Ustrchr(h->text, ':'); - for(s = h->text; s < colon; s++) - { - if ((*s < 33) || (*s > 126)) - { - *msgptr = string_sprintf("Invalid character in header \"%.*s\" found", - colon - h->text, h->text); - return FAIL; - } - } + colon = Ustrchr(h->text, ':'); + for(s = h->text; s < colon; s++) + if ((*s < 33) || (*s > 126)) + { + *msgptr = string_sprintf("Invalid character in header \"%.*s\" found", + colon - h->text, h->text); + return FAIL; + } } return OK; }