fix queue_name tracking across exec
[exim.git] / src / src / retry.c
index 9c90f6699affcad795beafeaf21d87dce35da546..0099b6e6ad00c343741655532fd86db53dafdf64 100644 (file)
@@ -1,10 +1,8 @@
-/* $Cambridge: exim/src/src/retry.c,v 1.2 2005/01/04 10:00:42 ph10 Exp $ */
-
 /*************************************************
 *     Exim - an Internet mail transport agent    *
 *************************************************/
 
-/* Copyright (c) University of Cambridge 1995 - 2005 */
+/* Copyright (c) University of Cambridge 1995 - 2015 */
 /* See the file NOTICE for conditions of use and distribution. */
 
 /* Functions concerned with retrying unsuccessful deliveries. */
 *************************************************/
 
 /* This function tests whether a message has been on the queue longer than
-the maximum retry time for a particular host.
+the maximum retry time for a particular host or address.
 
 Arguments:
-  host_key      the key to look up a host retry rule
+  retry_key     the key to look up a retry rule
   domain        the domain to look up a domain retry rule
-  basic_errno   a specific error number, or zero if none
-  more_errno    additional data for the error
+  retry_record  contains error information for finding rule
   now           the time
 
 Returns:        TRUE if the ultimate timeout has been reached
 */
 
-static BOOL
-ultimate_address_timeout(uschar *host_key, uschar *domain, int basic_errno,
-  int more_errno, time_t now)
+BOOL
+retry_ultimate_address_timeout(uschar *retry_key, const uschar *domain,
+  dbdata_retry *retry_record, time_t now)
 {
-BOOL address_timeout = TRUE;   /* no rule => timed out */
+BOOL address_timeout;
+
+DEBUG(D_retry)
+  {
+  debug_printf("retry time not reached: checking ultimate address timeout\n");
+  debug_printf("  now=%d first_failed=%d next_try=%d expired=%d\n",
+    (int)now, (int)retry_record->first_failed,
+    (int)retry_record->next_try, retry_record->expired);
+  }
 
 retry_config *retry =
-  retry_find_config(host_key+2, domain, basic_errno, more_errno);
+  retry_find_config(retry_key+2, domain,
+    retry_record->basic_errno, retry_record->more_errno);
 
 if (retry != NULL && retry->rules != NULL)
   {
@@ -46,8 +52,22 @@ if (retry != NULL && retry->rules != NULL)
   for (last_rule = retry->rules;
        last_rule->next != NULL;
        last_rule = last_rule->next);
+  DEBUG(D_retry)
+    debug_printf("  received_time=%d diff=%d timeout=%d\n",
+      received_time, (int)(now - received_time), last_rule->timeout);
   address_timeout = (now - received_time > last_rule->timeout);
   }
+else
+  {
+  DEBUG(D_retry)
+    debug_printf("no retry rule found: assume timed out\n");
+  address_timeout = TRUE;
+  }
+
+DEBUG(D_retry)
+  if (address_timeout)
+    debug_printf("on queue longer than maximum retry for address - "
+      "allowing delivery\n");
 
 return address_timeout;
 }
@@ -102,7 +122,7 @@ Returns:    TRUE if the host has expired but is usable because
 */
 
 BOOL
-retry_check_address(uschar *domain, host_item *host, uschar *portstring,
+retry_check_address(const uschar *domain, host_item *host, uschar *portstring,
   BOOL include_ip_address, uschar **retry_host_key, uschar **retry_message_key)
 {
 BOOL yield = FALSE;
@@ -200,19 +220,10 @@ if (host_retry_record != NULL)
 
   if (now < host_retry_record->next_try && !deliver_force)
     {
-    DEBUG(D_transport|D_retry)
-      debug_printf("host retry time not reached: checking ultimate address "
-        "timeout\n");
-
     if (!host_retry_record->expired &&
-        ultimate_address_timeout(host_key, domain,
-          host_retry_record->basic_errno, host_retry_record->more_errno, now))
-      {
-      DEBUG(D_transport|D_retry)
-        debug_printf("on queue longer than maximum retry for "
-          "address - allowing delivery\n");
+        retry_ultimate_address_timeout(host_key, domain,
+          host_retry_record, now))
       return FALSE;
-      }
 
     /* We have not hit the ultimate address timeout; host is unusable. */
 
@@ -237,20 +248,12 @@ if (message_retry_record != NULL)
   *retry_message_key = message_key;
   if (now < message_retry_record->next_try && !deliver_force)
     {
-    DEBUG(D_transport|D_retry)
-      debug_printf("host+message retry time not reached: checking ultimate "
-        "address timeout\n");
-    if (!ultimate_address_timeout(host_key, domain, 0, 0, now))
+    if (!retry_ultimate_address_timeout(host_key, domain,
+        message_retry_record, now))
       {
       host->status = hstatus_unusable;
       host->why = hwhy_retry;
       }
-    else
-      {
-      DEBUG(D_transport|D_retry)
-        debug_printf("on queue longer than maximum retry for "
-          "address - allowing delivery\n");
-      }
     return FALSE;
     }
   }
@@ -291,12 +294,15 @@ void
 retry_add_item(address_item *addr, uschar *key, int flags)
 {
 retry_item *rti = store_get(sizeof(retry_item));
+host_item * host = addr->host_used;
 rti->next = addr->retries;
 addr->retries = rti;
 rti->key = key;
 rti->basic_errno = addr->basic_errno;
 rti->more_errno = addr->more_errno;
-rti->message = addr->message;
+rti->message = host
+  ? string_sprintf("H=%s [%s]: %s", host->name, host->address, addr->message)
+  : addr->message;
 rti->flags = flags;
 
 DEBUG(D_transport|D_retry)
@@ -337,39 +343,45 @@ Returns:       pointer to retry rule, or NULL
 */
 
 retry_config *
-retry_find_config(uschar *key, uschar *alternate, int basic_errno,
+retry_find_config(const uschar *key, const uschar *alternate, int basic_errno,
   int more_errno)
 {
-int replace;
-uschar *use_key, *use_alternate;
-uschar *colon = Ustrchr(key, ':');
+const uschar *colon = Ustrchr(key, ':');
 retry_config *yield;
 
-/* If there's a colon in the key, temporarily replace it with
-a zero to terminate the string there. */
+/* If there's a colon in the key, there are two possibilities:
 
-if (colon != NULL)
-  {
-  replace = ':';
-  }
-else
-  {
-  colon = key + Ustrlen(key);
-  replace = 0;
-  }
-*colon = 0;
+(1) This is a key for a host, ip address, and possibly port, in the format
+
+      hostname:ip+port
+
+    In this case, we copy the host name.
+
+(2) This is a key for a pipe, file, or autoreply delivery, in the format
+
+      pipe-or-file-or-auto:x@y
+
+    where x@y is the original address that provoked the delivery. The pipe or
+    file or auto will start with | or / or >, whereas a host name will start
+    with a letter or a digit. In this case we want to use the original address
+    to search for a retry rule. */
+
+if (colon)
+  key = isalnum(*key)
+    ? string_copyn(key, colon-key)     /* the hostname */
+    : Ustrrchr(key, ':') + 1;          /* Take from the last colon */
 
 /* Sort out the keys */
 
-use_key = (Ustrchr(key, '@') != NULL)? key : string_sprintf("*@%s", key);
-use_alternate = (alternate == NULL)? NULL : string_sprintf("*@%s", alternate);
+if (!Ustrchr(key, '@')) key = string_sprintf("*@%s", key);
+if (alternate)    alternate = string_sprintf("*@%s", alternate);
 
 /* Scan the configured retry items. */
 
 for (yield = retries; yield != NULL; yield = yield->next)
   {
-  uschar *plist = yield->pattern;
-  uschar *slist = yield->senders;
+  const uschar *plist = yield->pattern;
+  const uschar *slist = yield->senders;
 
   /* If a specific error is set for this item, check that we are handling that
   specific error, and if so, check any additional error information if
@@ -390,20 +402,31 @@ for (yield = retries; yield != NULL; yield = yield->next)
         continue;
       }
 
-    /* Handle 4xx responses to RCPT. The code that was received is in the 2nd
-    least significant byte of more_errno (with 400 subtracted). The required
-    value is coded in the 2nd least significant byte of the yield->more_errno
-    field as follows:
+    /* The TLSREQUIRED error also covers TLSFAILURE. These are subtly different
+    errors, but not worth separating at this level. */
+
+    else if (yield->basic_errno == ERRNO_TLSREQUIRED)
+      {
+      if (basic_errno != ERRNO_TLSREQUIRED && basic_errno != ERRNO_TLSFAILURE)
+        continue;
+      }
+
+    /* Handle 4xx responses to MAIL, RCPT, or DATA. The code that was received
+    is in the 2nd least significant byte of more_errno (with 400 subtracted).
+    The required value is coded in the 2nd least significant byte of the
+    yield->more_errno field as follows:
 
       255     => any 4xx code
       >= 100  => the decade must match the value less 100
       < 100   => the exact value must match
     */
 
-    else if (yield->basic_errno == ERRNO_RCPT4XX)
+    else if (yield->basic_errno == ERRNO_MAIL4XX ||
+             yield->basic_errno == ERRNO_RCPT4XX ||
+             yield->basic_errno == ERRNO_DATA4XX)
       {
       int wanted;
-      if (basic_errno != ERRNO_RCPT4XX) continue;
+      if (basic_errno != yield->basic_errno) continue;
       wanted = (yield->more_errno >> 8) & 255;
       if (wanted != 255)
         {
@@ -457,15 +480,14 @@ for (yield = retries; yield != NULL; yield = yield->next)
   /* Check for a match between the address list item at the start of this retry
   rule and either the main or alternate keys. */
 
-  if (match_address_list(use_key, TRUE, TRUE, &plist, NULL, -1, UCHAR_MAX+1,
+  if (match_address_list(key, TRUE, TRUE, &plist, NULL, -1, UCHAR_MAX+1,
         NULL) == OK ||
-     (use_alternate != NULL &&
-      match_address_list(use_alternate, TRUE, TRUE, &plist, NULL, -1,
+     (alternate != NULL &&
+      match_address_list(alternate, TRUE, TRUE, &plist, NULL, -1,
         UCHAR_MAX+1, NULL) == OK))
     break;
   }
 
-*colon = replace;
 return yield;
 }
 
@@ -619,10 +641,12 @@ for (i = 0; i < 3; i++)
         DEBUG(D_retry)
           {
           if ((rti->flags & rf_host) != 0)
-            debug_printf("retry for %s (%s) = %s\n", rti->key,
-              addr->domain, retry->pattern);
+            debug_printf("retry for %s (%s) = %s %d %d\n", rti->key,
+              addr->domain, retry->pattern, retry->basic_errno,
+              retry->more_errno);
           else
-            debug_printf("retry for %s = %s\n", rti->key, retry->pattern);
+            debug_printf("retry for %s = %s %d %d\n", rti->key, retry->pattern,
+              retry->basic_errno, retry->more_errno);
           }
 
         /* Set up the message for the database retry record. Because DBM
@@ -631,7 +655,7 @@ for (i = 0; i < 3; i++)
 
         message = (rti->basic_errno > 0)? US strerror(rti->basic_errno) :
           (rti->message == NULL)?
-          US"unknown error" : string_printing(rti->message);
+          US"unknown error" : US string_printing(rti->message);
         message_length = Ustrlen(message);
         if (message_length > 150) message_length = 150;
 
@@ -658,6 +682,17 @@ for (i = 0; i < 3; i++)
         /* Compute how long this destination has been failing */
 
         failing_interval = now - retry_record->first_failed;
+        DEBUG(D_retry) debug_printf("failing_interval=%d message_age=%d\n",
+          failing_interval, message_age);
+
+        /* For a non-host error, if the message has been on the queue longer
+        than the recorded time of failure, use the message's age instead. This
+        can happen when some messages can be delivered and others cannot; a
+        successful delivery will reset the first_failed time, and this can lead
+        to a failing message being retried too often. */
+
+        if ((rti->flags & rf_host) == 0 && message_age > failing_interval)
+          failing_interval = message_age;
 
         /* Search for the current retry rule. The cutoff time of the
         last rule is handled differently to the others. The rule continues
@@ -712,7 +747,14 @@ for (i = 0; i < 3; i++)
 
         This implements "timeout this rule if EITHER the host (or routing or
         directing) has been failing for more than the maximum time, OR if the
-        message has been on the queue for more than the maximum time." */
+        message has been on the queue for more than the maximum time."
+
+        February 2006: It is possible that this code is no longer needed
+        following the change to the retry calculation to use the message age if
+        it is larger than the time since first failure. It may be that the
+        expired flag is always set when the other conditions are met. However,
+        this is a small bit of code, and it does no harm to leave it in place,
+        just in case. */
 
         if (received_time <= retry_record->first_failed &&
             addr == endaddr && !retry_record->expired && rule != NULL)
@@ -739,15 +781,25 @@ for (i = 0; i < 3; i++)
         if (rule == NULL) next_try = now; else
           {
           if (rule->rule == 'F') next_try = now + rule->p1;
-          else  /* assume rule = 'G' */
+          else  /* rule = 'G' or 'H' */
             {
             int last_predicted_gap =
               retry_record->next_try - retry_record->last_try;
             int last_actual_gap = now - retry_record->last_try;
             int lastgap = (last_predicted_gap < last_actual_gap)?
               last_predicted_gap : last_actual_gap;
-            next_try = now + ((lastgap < rule->p1)? rule->p1 :
-               (lastgap * rule->p2)/1000);
+            int next_gap = (lastgap * rule->p2)/1000;
+            if (rule->rule == 'G')
+              {
+              next_try = now + ((lastgap < rule->p1)? rule->p1 : next_gap);
+              }
+            else  /* The 'H' rule */
+              {
+              next_try = now + rule->p1;
+              if (next_gap > rule->p1)
+                next_try += random_number(next_gap - rule->p1)/2 +
+                  (next_gap - rule->p1)/2;
+              }
             }
           }
 
@@ -842,6 +894,9 @@ for (i = 0; i < 3; i++)
           setflag(addr, af_retry_timedout);
           addr->message = (addr->message == NULL)? US"retry timeout exceeded" :
             string_sprintf("%s: retry timeout exceeded", addr->message);
+          addr->user_message = (addr->user_message == NULL)?
+            US"retry timeout exceeded" :
+            string_sprintf("%s: retry timeout exceeded", addr->user_message);
           log_write(0, LOG_MAIN, "** %s%s%s%s: retry timeout exceeded",
             addr->address,
            (addr->parent == NULL)? US"" : US" <",