X-Git-Url: https://git.exim.org/exim.git/blobdiff_plain/8768d5483a5894400ae1f70cda1beb44ed9b087c..HEAD:/src/src/retry.c diff --git a/src/src/retry.c b/src/src/retry.c index a040c3310..6e4a3459d 100644 --- a/src/src/retry.c +++ b/src/src/retry.c @@ -2,8 +2,10 @@ * Exim - an Internet mail transport agent * *************************************************/ +/* Copyright (c) The Exim Maintainers 2020 - 2024 */ /* Copyright (c) University of Cambridge 1995 - 2018 */ /* See the file NOTICE for conditions of use and distribution. */ +/* SPDX-License-Identifier: GPL-2.0-or-later */ /* Functions concerned with retrying unsuccessful deliveries. */ @@ -29,32 +31,31 @@ Returns: TRUE if the ultimate timeout has been reached */ BOOL -retry_ultimate_address_timeout(uschar *retry_key, const uschar *domain, +retry_ultimate_address_timeout(const uschar * retry_key, const uschar *domain, dbdata_retry *retry_record, time_t now) { BOOL address_timeout; +retry_config * retry; 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); + debug_printf(" now=" TIME_T_FMT " first_failed=" TIME_T_FMT + " next_try=" TIME_T_FMT " expired=%c\n", + now, retry_record->first_failed, + retry_record->next_try, retry_record->expired ? 'T' : 'F'); } -retry_config *retry = - retry_find_config(retry_key+2, domain, +retry = retry_find_config(retry_key+2, domain, retry_record->basic_errno, retry_record->more_errno); -if (retry != NULL && retry->rules != NULL) +if (retry && retry->rules) { retry_rule *last_rule; - for (last_rule = retry->rules; - last_rule->next != NULL; - last_rule = last_rule->next); + for (last_rule = retry->rules; last_rule->next; last_rule = last_rule->next) ; DEBUG(D_retry) - debug_printf(" received_time=%d diff=%d timeout=%d\n", - (int)received_time.tv_sec, (int)(now - received_time.tv_sec), last_rule->timeout); + debug_printf(" received_time=" TIME_T_FMT " diff=%d timeout=%d\n", + received_time.tv_sec, (int)(now - received_time.tv_sec), last_rule->timeout); address_timeout = (now - received_time.tv_sec > last_rule->timeout); } else @@ -74,6 +75,29 @@ return address_timeout; +const uschar * +retry_host_key_build(const host_item * host, BOOL incl_ip, + const uschar * portstring) +{ +const uschar * s = host->name; +gstring * g = string_is_ip_address(s, NULL) + ? string_fmt_append(NULL, "T:[%s]", s) /* wrap a name which is a bare ip */ + : string_fmt_append(NULL, "T:%s", s); + +s = host->address; +if (incl_ip) + g = Ustrchr(s, ':') + ? string_fmt_append(g, ":[%s]", s) /* wrap an ipv6 */ + : string_fmt_append(g, ":%s", s); + +if (portstring) + g = string_cat(g, portstring); + +gstring_release_unused(g); +return string_from_gstring(g); +} + + /************************************************* * Set status of a host+address item * *************************************************/ @@ -123,34 +147,35 @@ Returns: TRUE if the host has expired but is usable because BOOL retry_check_address(const uschar *domain, host_item *host, uschar *portstring, - BOOL include_ip_address, uschar **retry_host_key, uschar **retry_message_key) + BOOL include_ip_address, + const uschar **retry_host_key, const uschar **retry_message_key) { BOOL yield = FALSE; time_t now = time(NULL); -uschar *host_key, *message_key; -open_db dbblock; -open_db *dbm_file; -tree_node *node; -dbdata_retry *host_retry_record, *message_retry_record; +const uschar * host_key, * message_key; +open_db dbblock, * dbm_file = NULL; +tree_node * node; +dbdata_retry * host_retry_record, * message_retry_record; *retry_host_key = *retry_message_key = NULL; -DEBUG(D_transport|D_retry) debug_printf("checking status of %s\n", host->name); - /* Do nothing if status already set; otherwise initialize status as usable. */ if (host->status != hstatus_unknown) return FALSE; host->status = hstatus_usable; -/* Generate the host key for the unusable tree and the retry database. Ensure -host names are lower cased (that's what %S does). */ - -host_key = include_ip_address? - string_sprintf("T:%S:%s%s", host->name, host->address, portstring) : - string_sprintf("T:%S%s", host->name, portstring); +DEBUG(D_transport|D_retry) + { + debug_printf_indent("checking retry status of %s\n", host->name); + acl_level++; + } -/* Generate the message-specific key */ +/* Generate the host key for the unusable tree and the retry database. Ensure +host names are lower cased (that's what %S does). +Generate the message-specific key too. +Be sure to maintain lack-of-spaces in retry keys; exinext depends on it. */ +host_key = retry_host_key_build(host, include_ip_address, portstring); message_key = string_sprintf("%s:%s", host_key, message_id); /* Search the tree of unusable IP addresses. This is filled in when deliveries @@ -161,46 +186,62 @@ the retry database when it is updated). */ if ((node = tree_search(tree_unusable, host_key))) { - DEBUG(D_transport|D_retry) debug_printf("found in tree of unusables\n"); - host->status = (node->data.val > 255)? - hstatus_unusable_expired : hstatus_unusable; + DEBUG(D_transport|D_retry) + debug_printf_indent("found in tree of unusables\n"); + host->status = node->data.val > 255 + ? hstatus_unusable_expired : hstatus_unusable; host->why = node->data.val & 255; - return FALSE; + goto out; } /* Open the retry database, giving up if there isn't one. Otherwise, search for the retry records, and then close the database again. */ -if (!(dbm_file = dbfn_open(US"retry", O_RDONLY, &dbblock, FALSE))) +if (!continue_retry_db) + dbm_file = dbfn_open(US"retry", O_RDONLY, &dbblock, FALSE, TRUE); +else if (continue_retry_db != (open_db *)-1) + { + DEBUG(D_hints_lookup) + debug_printf_indent(" using cached retry hintsdb handle\n"); + dbm_file = continue_retry_db; + } +else DEBUG(D_hints_lookup) + debug_printf_indent(" using cached retry hintsdb nonpresence\n"); + +if (!dbm_file) { DEBUG(D_deliver|D_retry|D_hints_lookup) - debug_printf("no retry data available\n"); - return FALSE; + debug_printf_indent("no retry data available\n"); + goto out; } host_retry_record = dbfn_read(dbm_file, host_key); message_retry_record = dbfn_read(dbm_file, message_key); -dbfn_close(dbm_file); +if (!continue_retry_db) + dbfn_close(dbm_file); +else + DEBUG(D_hints_lookup) debug_printf_indent("retaining retry hintsdb handle\n"); /* Ignore the data if it is too old - too long since it was written */ if (!host_retry_record) { - DEBUG(D_transport|D_retry) debug_printf("no host retry record\n"); + DEBUG(D_transport|D_retry) debug_printf_indent("no host retry record\n"); } else if (now - host_retry_record->time_stamp > retry_data_expire) { host_retry_record = NULL; - DEBUG(D_transport|D_retry) debug_printf("host retry record too old\n"); + DEBUG(D_transport|D_retry) debug_printf_indent("host retry record too old\n"); } if (!message_retry_record) { - DEBUG(D_transport|D_retry) debug_printf("no message retry record\n"); + DEBUG(D_transport|D_retry) debug_printf_indent("no message retry record\n"); } else if (now - message_retry_record->time_stamp > retry_data_expire) { message_retry_record = NULL; - DEBUG(D_transport|D_retry) debug_printf("message retry record too old\n"); + DEBUG(D_transport|D_retry) + debug_printf_indent("message retry record too old\n"); } /* If there's a host-specific retry record, check for reaching the retry @@ -222,7 +263,7 @@ if (host_retry_record) if (!host_retry_record->expired && retry_ultimate_address_timeout(host_key, domain, host_retry_record, now)) - return FALSE; + goto out; /* We have not hit the ultimate address timeout; host is unusable. */ @@ -230,7 +271,7 @@ if (host_retry_record) hstatus_unusable_expired : hstatus_unusable; host->why = hwhy_retry; host->last_try = host_retry_record->last_try; - return FALSE; + goto out; } /* Host is usable; set return TRUE if expired. */ @@ -253,10 +294,12 @@ if (message_retry_record) host->status = hstatus_unusable; host->why = hwhy_retry; } - return FALSE; + yield = FALSE; goto out; } } +out: +DEBUG(D_transport|D_retry) acl_level--; return yield; } @@ -290,9 +333,9 @@ Returns: nothing */ void -retry_add_item(address_item *addr, uschar *key, int flags) +retry_add_item(address_item * addr, const uschar * key, int flags) { -retry_item *rti = store_get(sizeof(retry_item)); +retry_item * rti = store_get(sizeof(retry_item), GET_UNTAINTED); host_item * host = addr->host_used; rti->next = addr->retries; @@ -308,7 +351,9 @@ rti->flags = flags; DEBUG(D_transport|D_retry) { int letter = rti->more_errno & 255; - debug_printf("added retry item for %s: errno=%d more_errno=", rti->key, + debug_printf("added retry %sitem for %s: errno=%d more_errno=", + flags & rf_delete ? "delete-" : "", + rti->key, rti->basic_errno); if (letter == 'A' || letter == 'M') debug_printf("%d,%c", (rti->more_errno >> 8) & 255, letter); @@ -343,11 +388,11 @@ Returns: pointer to retry rule, or NULL */ retry_config * -retry_find_config(const uschar *key, const uschar *alternate, int basic_errno, +retry_find_config(const uschar * key, const uschar * alternate, int basic_errno, int more_errno) { -const uschar *colon = Ustrchr(key, ':'); -retry_config *yield; +const uschar * colon = Ustrchr(key, ':'); +retry_config * yield; /* If there's a colon in the key, there are two possibilities: @@ -355,7 +400,8 @@ retry_config *yield; hostname:ip+port - In this case, we copy the host name. + In this case, we copy the host name (which could be an [ip], including + being an [ipv6], and we drop the []). (2) This is a key for a pipe, file, or autoreply delivery, in the format @@ -369,6 +415,8 @@ retry_config *yield; if (colon) key = isalnum(*key) ? string_copyn(key, colon-key) /* the hostname */ + : *key == '[' + ? string_copyn(key+1, Ustrchr(key, ']')-1-key) /* the ip */ : Ustrrchr(key, ':') + 1; /* Take from the last colon */ /* Sort out the keys */ @@ -501,6 +549,7 @@ return yield; /* Update the retry data for any directing/routing/transporting that was deferred, or delete it for those that succeeded after a previous defer. This is done all in one go to minimize opening/closing/locking of the database file. +Called (only) from deliver_message(). Note that, because SMTP delivery involves a list of destinations to try, there may be defer-type retry information for some of them even when the message was @@ -518,30 +567,31 @@ Returns: nothing */ void -retry_update(address_item **addr_defer, address_item **addr_failed, - address_item **addr_succeed) +retry_update(address_item ** addr_defer, address_item ** addr_failed, + address_item ** addr_succeed) { -open_db dbblock; -open_db *dbm_file = NULL; +open_db dbblock, * dbm_file = NULL; time_t now = time(NULL); -int i; -DEBUG(D_retry) debug_printf("Processing retry items\n"); +DEBUG(D_retry) { debug_printf_indent("Processing retry items\n"); acl_level++; } /* Three-times loop to handle succeeded, failed, and deferred addresses. Deferred addresses must be handled after failed ones, because some may be moved to the failed chain if they have timed out. */ -for (i = 0; i < 3; i++) +for (int i = 0; i < 3; i++) { - address_item *endaddr, *addr; - address_item *last_first = NULL; - address_item **paddr = i==0 ? addr_succeed : - i==1 ? addr_failed : addr_defer; - address_item **saved_paddr = NULL; + address_item * endaddr, *addr; + address_item * last_first = NULL; + address_item ** paddr = i==0 ? addr_succeed : i==1 ? addr_failed : addr_defer; + address_item ** saved_paddr = NULL; - DEBUG(D_retry) debug_printf("%s addresses:\n", - i == 0 ? "Succeeded" : i == 1 ? "Failed" : "Deferred"); + DEBUG(D_retry) + { + debug_printf_indent("%s addresses:\n", + i == 0 ? "Succeeded" : i == 1 ? "Failed" : "Deferred"); + acl_level++; + } /* Loop for each address on the chain. For deferred addresses, the whole address times out unless one of its retry addresses has a retry rule that @@ -556,19 +606,21 @@ for (i = 0; i < 3; i++) while ((endaddr = *paddr)) { BOOL timed_out = FALSE; - retry_item *rti; for (addr = endaddr; addr; addr = addr->parent) { - int update_count = 0; - int timedout_count = 0; + int update_count = 0, timedout_count = 0; - DEBUG(D_retry) debug_printf(" %s%s\n", addr->address, - addr->retries ? "" : ": no retry items"); + DEBUG(D_retry) + { + debug_printf_indent("%s%s\n", addr->address, + addr->retries ? "" : ": no retry items"); + acl_level++; + } /* Loop for each retry item. */ - for (rti = addr->retries; rti; rti = rti->next) + for (retry_item * rti = addr->retries; rti; rti = rti->next) { uschar *message; int message_length, message_space, failing_interval, next_try; @@ -583,14 +635,20 @@ for (i = 0; i < 3; i++) reached their retry next try time. */ if (!dbm_file) - dbm_file = dbfn_open(US"retry", O_RDWR, &dbblock, TRUE); - - if (!dbm_file) - { - DEBUG(D_deliver|D_retry|D_hints_lookup) - debug_printf("retry database not available for updating\n"); - return; - } + if (continue_retry_db && continue_retry_db != (open_db *)-1) + { + DEBUG(D_hints_lookup) + debug_printf_indent("using cached retry hintsdb handle\n"); + dbm_file = continue_retry_db; + } + else if (!(dbm_file = exim_lockfile_needed() + ? dbfn_open(US"retry", O_RDWR|O_CREAT, &dbblock, TRUE, TRUE) + : dbfn_open_multi(US"retry", O_RDWR|O_CREAT, &dbblock))) + { + DEBUG(D_deliver|D_retry|D_hints_lookup) + debug_printf_indent("retry db not available for updating\n"); + return; + } /* If there are no deferred addresses, that is, if this message is completing, and the retry item is for a message-specific SMTP error, @@ -610,7 +668,7 @@ for (i = 0; i < 3; i++) { (void)dbfn_delete(dbm_file, rti->key); DEBUG(D_retry) - debug_printf("deleted retry information for %s\n", rti->key); + debug_printf_indent("deleted retry information for %s\n", rti->key); continue; } @@ -630,7 +688,7 @@ for (i = 0; i < 3; i++) rti->flags & rf_host ? addr->domain : NULL, rti->basic_errno, rti->more_errno))) { - DEBUG(D_retry) debug_printf("No configured retry item for %s%s%s\n", + DEBUG(D_retry) debug_printf_indent("No configured retry item for %s%s%s\n", rti->key, rti->flags & rf_host ? US" or " : US"", rti->flags & rf_host ? addr->domain : US""); @@ -640,11 +698,11 @@ for (i = 0; i < 3; i++) DEBUG(D_retry) if (rti->flags & rf_host) - debug_printf("retry for %s (%s) = %s %d %d\n", rti->key, + debug_printf_indent("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 %d %d\n", rti->key, retry->pattern, + debug_printf_indent("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 @@ -657,19 +715,33 @@ for (i = 0; i < 3; i++) ? US string_printing(rti->message) : US"unknown error"; message_length = Ustrlen(message); - if (message_length > 150) message_length = 150; + if (message_length > EXIM_DB_RLIMIT) + { + DEBUG(D_retry) + debug_printf_indent("truncating message from %u to %u bytes\n", + message_length, EXIM_DB_RLIMIT); + message_length = EXIM_DB_RLIMIT; + } + + /* For a transaction-capable DB, open one for the read,write + sequence used for this retry record */ + + if (!exim_lockfile_needed()) + dbfn_transaction_start(dbm_file); /* Read a retry record from the database or construct a new one. Ignore an old one if it is too old since it was last updated. */ - retry_record = dbfn_read(dbm_file, rti->key); + retry_record = dbfn_read_with_length(dbm_file, rti->key, + &message_space); if ( retry_record && now - retry_record->time_stamp > retry_data_expire) retry_record = NULL; if (!retry_record) { - retry_record = store_get(sizeof(dbdata_retry) + message_length); + retry_record = store_get(sizeof(dbdata_retry) + message_length, + message); message_space = message_length; retry_record->first_failed = now; retry_record->last_try = now; @@ -677,12 +749,12 @@ for (i = 0; i < 3; i++) retry_record->expired = FALSE; retry_record->text[0] = 0; /* just in case */ } - else message_space = Ustrlen(retry_record->text); + else message_space -= sizeof(dbdata_retry); /* 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", + DEBUG(D_retry) debug_printf_indent("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 @@ -764,7 +836,7 @@ for (i = 0; i < 3; i++) ; if (now - received_time.tv_sec > last_rule->timeout) { - DEBUG(D_retry) debug_printf("on queue longer than maximum retry\n"); + DEBUG(D_retry) debug_printf_indent("on queue longer than maximum retry\n"); timedout_count++; rule = NULL; } @@ -808,15 +880,17 @@ for (i = 0; i < 3; i++) if (next_try - now > retry_interval_max) next_try = now + retry_interval_max; - /* If the new message length is greater than the previous one, we - have to copy the record first. */ + /* If the new message length is greater than the previous one, we have + to copy the record first. If we're using an old one, the read used + tainted memory so we're ok to write into it. */ - if (message_length > message_space) - { - dbdata_retry *newr = store_get(sizeof(dbdata_retry) + message_length); - memcpy(newr, retry_record, sizeof(dbdata_retry)); - retry_record = newr; - } + if (message_length > message_space) + { + dbdata_retry * newr = + store_get(sizeof(dbdata_retry) + message_length, message); + memcpy(newr, retry_record, sizeof(dbdata_retry)); + retry_record = newr; + } /* Set up the retry record; message_length may be less than the string length for very long error strings. */ @@ -826,16 +900,16 @@ for (i = 0; i < 3; i++) retry_record->basic_errno = rti->basic_errno; retry_record->more_errno = rti->more_errno; Ustrncpy(retry_record->text, message, message_length); - retry_record->text[message_length] = 0; + retry_record->text[message_length] = 0; /* nul-term string in db */ DEBUG(D_retry) { int letter = retry_record->more_errno & 255; - debug_printf("Writing retry data for %s\n", rti->key); - debug_printf(" first failed=%d last try=%d next try=%d expired=%d\n", + debug_printf_indent("Writing retry data for %s\n", rti->key); + debug_printf_indent(" first failed=%d last try=%d next try=%d expired=%d\n", (int)retry_record->first_failed, (int)retry_record->last_try, (int)retry_record->next_try, retry_record->expired); - debug_printf(" errno=%d more_errno=", retry_record->basic_errno); + debug_printf_indent(" errno=%d more_errno=", retry_record->basic_errno); if (letter == 'A' || letter == 'M') debug_printf("%d,%c", (retry_record->more_errno >> 8) & 255, letter); @@ -844,9 +918,14 @@ for (i = 0; i < 3; i++) debug_printf(" %s\n", retry_record->text); } - (void)dbfn_write(dbm_file, rti->key, retry_record, - sizeof(dbdata_retry) + message_length); + if (dbfn_write(dbm_file, rti->key, retry_record, + sizeof(dbdata_retry) + message_length) != 0) + DEBUG(D_retry) debug_printf_indent("retry record write failed\n"); + + if (!exim_lockfile_needed()) + dbfn_transaction_commit(dbm_file); } /* Loop for each retry item */ + DEBUG(D_retry) acl_level--; /* If all the non-delete retry items are timed out, the address is timed out, provided that we didn't skip any hosts because their retry @@ -855,12 +934,12 @@ for (i = 0; i < 3; i++) if (update_count > 0 && update_count == timedout_count) if (!testflag(endaddr, af_retry_skipped)) { - DEBUG(D_retry) debug_printf("timed out: all retries expired\n"); + DEBUG(D_retry) debug_printf_indent("timed out: all retries expired\n"); timed_out = TRUE; } else DEBUG(D_retry) - debug_printf("timed out but some hosts were skipped\n"); + debug_printf_indent("timed out but some hosts were skipped\n"); } /* Loop for an address and its parents */ /* If this is a deferred address, and retry processing was requested by @@ -888,16 +967,17 @@ for (i = 0; i < 3; i++) for (;; addr = addr->next) { 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); + addr->message = addr->message + ? string_sprintf("%s: retry timeout exceeded", addr->message) + : US"retry timeout exceeded"; + addr->user_message = addr->user_message + ? string_sprintf("%s: retry timeout exceeded", addr->user_message) + : US"retry timeout exceeded"; log_write(0, LOG_MAIN, "** %s%s%s%s: retry timeout exceeded", addr->address, - (addr->parent == NULL)? US"" : US" <", - (addr->parent == NULL)? US"" : addr->parent->address, - (addr->parent == NULL)? US"" : US">"); + addr->parent ? US" <" : US"", + addr->parent ? addr->parent->address : US"", + addr->parent ? US">" : US""); if (addr == endaddr) break; } @@ -920,13 +1000,24 @@ for (i = 0; i < 3; i++) paddr = &(endaddr->next); /* Advance to next address */ } /* Loop for all addresses */ + DEBUG(D_retry) acl_level--; } /* Loop for succeed, fail, defer */ /* Close and unlock the database */ -if (dbm_file) dbfn_close(dbm_file); +if (dbm_file) + if (dbm_file != continue_retry_db) + if (exim_lockfile_needed()) + dbfn_close(dbm_file); + else + dbfn_close_multi(dbm_file); + else DEBUG(D_hints_lookup) + debug_printf_indent("retaining retry hintsdb handle\n"); -DEBUG(D_retry) debug_printf("end of retry processing\n"); +DEBUG(D_retry) + { acl_level--; debug_printf_indent("end of retry processing\n"); } } /* End of retry.c */ +/* vi: aw ai sw=2 +*/