Split out callout-cache handling from do_callout()
[exim.git] / src / src / verify.c
index 4af863c0bc80e7cf6fc7642ada24280551caef5b..def9b31a8c4b299272c3cbc1d5388b66beb0a36f 100644 (file)
@@ -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/<postmaster@%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:<postmaster@domain>\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:<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");
-        goto END_CACHE;
-        }
+       {
+       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
@@ -325,10 +252,10 @@ if (dbm_file)
       */
 
       HDEBUG(D_verify) debug_printf("callout cache: domain accepts RCPT "
-        "TO:<postmaster@domain>\n");
-      pm_mailfrom = NULL;
-      new_domain_record.postmaster_result = ccache_accept;
-      new_domain_record.postmaster_stamp = cache_record->postmaster_stamp;
+       "TO:<postmaster@domain>\n");
+      *pm_ptr = NULL;
+      new_domain_record->postmaster_result = ccache_accept;
+      new_domain_record->postmaster_stamp = cache_record->postmaster_stamp;
       }
     }
 
@@ -337,36 +264,192 @@ 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);
+}
+
+
+/*************************************************
+*      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
+
+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;
+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. */
+
+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/<postmaster@%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))
+  goto END_CALLOUT;
 
 if (!addr->transport)
   {
@@ -386,7 +469,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);
@@ -703,9 +786,8 @@ tls_retry_connection:
        }
 #endif
 
-      new_domain_record.result =
-        (old_domain_cache_result == ccache_reject_mfnull)?
-          ccache_reject_mfnull: ccache_accept;
+      new_domain_record.result = old_domain_cache_result == ccache_reject_mfnull
+       ? ccache_reject_mfnull : ccache_accept;
 
       /* Do the random local part check first */
 
@@ -1007,64 +1089,17 @@ no_conn:
 /* 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 +1128,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;
 }