+/* Check the callout cache.
+Options * pm_mailfrom may be modified by cache partial results.
+
+Return: TRUE if result found
+*/
+
+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 options = *opt_ptr;
+open_db dbblock;
+open_db *dbm_file = NULL;
+
+/* Open the callout cache database, it it exists, for reading only at this
+stage, unless caching has been disabled. */
+
+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)))
+ {
+ HDEBUG(D_verify) debug_printf("callout cache: not available\n");
+ }
+else
+ {
+ /* 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. */
+
+ if (cache_record)
+ {
+ /* In most cases, if an early command (up to and including MAIL FROM:<>)
+ was rejected, there is no point carrying on. The callout fails. However, if
+ we are doing a recipient verification with use_sender or use_postmaster
+ set, a previous failure of MAIL FROM:<> doesn't count, because this time we
+ will be using a non-empty sender. We have to remember this situation so as
+ not to disturb the cached domain value if this whole verification succeeds
+ (we don't want it turning into "accept"). */
+
+ *old_domain_res = cache_record->result;
+
+ if ( cache_record->result == ccache_reject
+ || *from_address == 0 && cache_record->result == ccache_reject_mfnull)
+ {
+ HDEBUG(D_verify)
+ 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;
+ *failure_ptr = US"mail";
+ dbfn_close(dbm_file);
+ return TRUE;
+ }
+
+ /* If a previous check on a "random" local part was accepted, we assume
+ that the server does not do any checking on local parts. There is therefore
+ no point in doing the callout, because it will always be successful. If a
+ random check previously failed, arrange not to do it again, but preserve
+ the data in the new record. If a random check is required but hasn't been
+ done, skip the remaining cache processing. */
+
+ if (options & vopt_callout_random) switch(cache_record->random_result)
+ {
+ case ccache_accept:
+ HDEBUG(D_verify)
+ debug_printf("callout cache: domain accepts random addresses\n");
+ *failure_ptr = US"random";
+ 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");
+ *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");
+ dbfn_close(dbm_file);
+ return FALSE;
+ }
+
+ /* If a postmaster check is requested, but there was a previous failure,
+ there is again no point in carrying on. If a postmaster check is required,
+ but has not been done before, we are going to have to do a callout, so skip
+ remaining cache processing. */
+
+ 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:<postmaster@domain>\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:<postmaster@domain> (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
+ that the value in the cache record is preserved (with its old timestamp).
+ */
+
+ HDEBUG(D_verify) debug_printf("callout cache: domain accepts RCPT "
+ "TO:<postmaster@domain>\n");
+ *pm_ptr = NULL;
+ new_domain_record->postmaster_result = ccache_accept;
+ new_domain_record->postmaster_stamp = cache_record->postmaster_stamp;
+ }
+ }
+
+ /* We can't give a result based on information about the domain. See if there
+ is an unexpired cache record for this specific address (combined with the
+ sender address if we are doing a recipient callout with a non-empty sender).
+ */
+
+ 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->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;
+ }
+
+ /* Close the cache database while we actually do the callout for real. */
+
+ dbfn_close(dbm_file);
+ 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);
+
+ smtp_port_for_connect(host, port);
+
+ if ( ( interface == cutthrough.interface
+ || ( interface
+ && cutthrough.interface
+ && Ustrcmp(interface, cutthrough.interface) == 0
+ ) )
+ && host->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.cctx, '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;
+}
+
+