Add durable queue retry backoff
Debian Trixie package bundle / packages (push) Successful in 21m55s

This commit is contained in:
Mario Fetka
2026-07-27 08:59:22 +02:00
parent 9acc289023
commit 8725cfbd7c
15 changed files with 443 additions and 38 deletions
+4 -3
View File
@@ -83,12 +83,13 @@ one-off files in `/tmp`:
delivery when STARTTLS works, plaintext delivery when it is not advertised,
and a fresh one-time plaintext reconnect after a failed opportunistic TLS
handshake. The fourth MX initially refuses connections; its message must
remain in Queue 7 and be delivered exactly once after the MX becomes
remain in Queue 7 with a persisted initial retry state and be delivered
exactly once by the automatic exponential-backoff path after the MX becomes
reachable. Every captured external message must also contain exactly one
Bongo DKIM signature with the configured domain and selector. To free
standard MX port 25, it temporarily moves Bongo's public listener to port
10025. It restores the exact SMTP configuration, resolver file, Queue
fixtures, and original Bongo service state.
10025. It restores the exact SMTP and Queue configurations, resolver file,
Queue fixtures, and original Bongo service state.
- `smtp-required-tls-check.py` uses isolated direct-MX fixtures to prove that
globally required TLS never falls back after absent or failed STARTTLS. A
second phase injects RFC 8689 `REQUIRETLS` over an encrypted trusted
@@ -118,25 +118,33 @@ def service_active() -> bool:
).returncode == 0
def read_smtp_configuration() -> dict:
completed = run([ADMIN, "__config-read", "smtp"])
def read_configuration(name: str) -> dict:
completed = run([ADMIN, "__config-read", name])
try:
configuration = json.loads(completed.stdout)
except json.JSONDecodeError as error:
raise SMTP07Error(
"bongo-admin returned invalid SMTP configuration") from error
f"bongo-admin returned invalid {name} configuration") from error
if not isinstance(configuration, dict):
raise SMTP07Error("SMTP configuration is not an object")
raise SMTP07Error(f"{name} configuration is not an object")
return configuration
def replace_smtp_configuration(configuration: dict) -> None:
def replace_configuration(name: str, configuration: dict) -> None:
run(
[ADMIN, "__config-replace", "smtp"],
[ADMIN, "__config-replace", name],
input_text=json.dumps(configuration, separators=(",", ":")),
)
def read_smtp_configuration() -> dict:
return read_configuration("smtp")
def replace_smtp_configuration(configuration: dict) -> None:
replace_configuration("smtp", configuration)
def wait_for_bongo() -> None:
deadline = time.monotonic() + 30
while time.monotonic() < deadline:
@@ -216,23 +224,30 @@ def wait_for_queued(marker: str) -> str:
f"direct-MX outage did not retain message {marker}")
def retry_when_idle(identifier: str) -> None:
def wait_for_retry_schedule(identifier: str) -> None:
queue_number, entry = identifier.split("-", 1)
control = Path("/var/lib/bongo/spool") / f"c{entry}.{queue_number}"
deadline = time.monotonic() + TIMEOUT
while time.monotonic() < deadline:
completed = queue("retry", identifier, check=False)
if completed.returncode == 0:
try:
lines = control.read_text(
encoding="ascii", errors="strict").splitlines()
attempts = [
int(line[1:]) for line in lines
if re.fullmatch(r"Y[0-9]+", line)
]
delay = control.stat().st_mtime - time.time()
except (FileNotFoundError, ValueError):
time.sleep(0.1)
continue
if attempts == [1] and 0 < delay <= 5:
return
if identifier not in queue_ids():
return
detail = f"{completed.stdout}\n{completed.stderr}".lower()
if "already being processed" not in detail:
if attempts and attempts != [1]:
raise SMTP07Error(
f"cannot retry direct-MX queue entry {identifier}: "
f"{completed.stderr.strip() or completed.stdout.strip()}"
)
time.sleep(0.2)
f"unexpected retry state for {identifier}: {attempts}")
time.sleep(0.1)
raise SMTP07Error(
f"direct-MX queue entry {identifier} remained busy")
f"queue entry {identifier} has no persisted initial retry schedule")
def wait_for_queue_removal(identifier: str) -> None:
@@ -817,6 +832,7 @@ def perform_test() -> None:
if not originally_active:
raise SMTP07Error("bongo.service must be active before this live test")
smtp_configuration = read_smtp_configuration()
queue_configuration = read_configuration("queue")
configured_host = str(
smtp_configuration.get("internal_relay_bind_address", "")
).strip()
@@ -850,8 +866,13 @@ def perform_test() -> None:
raise SMTP07Error("SMTP-07 requires a configured DKIM selector")
test_smtp_configuration = copy.deepcopy(smtp_configuration)
test_smtp_configuration["port"] = TEMPORARY_PUBLIC_PORT
test_queue_configuration = copy.deepcopy(queue_configuration)
test_queue_configuration["queueinterval"] = 1
test_queue_configuration["retry_initial_seconds"] = 2
test_queue_configuration["retry_maximum_seconds"] = 4
resolver_changed = False
smtp_changed = False
queue_changed = False
servers: list[socketserver.BaseServer] = []
queue_clean = False
connections: dict[str, int] = {}
@@ -865,6 +886,8 @@ def perform_test() -> None:
try:
replace_smtp_configuration(test_smtp_configuration)
smtp_changed = True
replace_configuration("queue", test_queue_configuration)
queue_changed = True
run([SYSTEMCTL, "stop", "bongo.service"])
save_resolver(original)
atomic_write(
@@ -901,6 +924,7 @@ def perform_test() -> None:
outage_marker = f"{TOKEN}-{outage_route.mode}"
submit(outage_route)
outage_queue_id = wait_for_queued(outage_marker)
wait_for_retry_schedule(outage_queue_id)
outage_capture = SMTPCapture(outage_route)
outage_server = ThreadingTCPServer(
@@ -911,7 +935,6 @@ def perform_test() -> None:
threading.Thread(
target=outage_server.serve_forever, daemon=True
).start()
retry_when_idle(outage_queue_id)
encrypted, message = wait_for_marker(
outage_capture, outage_marker
)
@@ -948,6 +971,13 @@ def perform_test() -> None:
except (OSError, SMTP07Error) as error:
restoration_errors.append(
f"SMTP configuration restore failed: {error}")
if queue_changed:
try:
replace_configuration("queue", queue_configuration)
queue_changed = False
except (OSError, SMTP07Error) as error:
restoration_errors.append(
f"Queue configuration restore failed: {error}")
if run(
[SYSTEMCTL, "stop", "bongo.service"], check=False
).returncode != 0:
@@ -989,13 +1019,15 @@ def perform_test() -> None:
raise SMTP07Error("resolver configuration was not restored exactly")
if read_smtp_configuration() != smtp_configuration:
raise SMTP07Error("SMTP configuration was not restored exactly")
if read_configuration("queue") != queue_configuration:
raise SMTP07Error("Queue configuration was not restored exactly")
print(
"SMTP-07 PASS "
f"starttls=tls/connections:{connections['tls']} "
f"plain=plaintext/connections:{connections['plain']} "
"handshake-failure=plaintext-reconnect/"
f"connections:{connections['fallback']} "
"internet-outage=retained/recovered "
"internet-outage=retained/automatic-backoff/recovered "
"dkim=verified "
"duplicates=no "
f"queue-clean={'yes' if queue_clean else 'no'} "
+18
View File
@@ -32,6 +32,24 @@ consuming the filesystem's final space. Per-user Store quota is separate: a
full mailbox defers or rejects that recipient without corrupting other
recipient state.
Temporary delivery failures use one persistent exponential retry policy,
regardless of whether the next hop is a directly resolved Internet MX, a
configured relayhost, or an internal LMTP destination. The retry counter is
part of the durable Queue envelope and the next eligible attempt survives a
daemon or host restart. `retry_initial_seconds` defaults to 300 seconds and
the delay doubles up to `retry_maximum_seconds`, which defaults to 4000
seconds. `queueinterval` controls how often the monitor looks for eligible
work; it does not shorten a message's persisted backoff. An administrator can
request an immediate attempt with `bongo-queuetool retry` or `flush`.
Normal messages expire after `queuetimeout`, while delivery-status messages
and other null-envelope-sender traffic expire after
`bounce_queue_timeout`. Both defaults are 432000 seconds (five days). On
normal-message expiry Bongo uses its existing RFC delivery-status path to
return a failure for the recipients still pending. An expired null-sender
message is discarded after the configured bounce lifetime, preventing a
bounce loop. Held queue entries do not run or expire until released.
Collector backpressure is also separate from the byte reserve. The
`collector` document limits globally pending and per-user pending external
messages. Queue counts only entries carrying its durable collected-message
+13 -1
View File
@@ -2358,13 +2358,15 @@ queries to the required `ns_c_in` class. This fixes direct MX/A resolution and
the TXT lookup path shared by mail policy features.
The reusable `smtp-outbound-opportunistic-tls-check.py` installs a temporary
loopback DNS resolver and publishes three isolated MX routes:
loopback DNS resolver and publishes four isolated MX routes:
- a server which advertises STARTTLS and completes a TLS 1.2-or-newer
handshake;
- a server which does not advertise STARTTLS;
- a server which advertises STARTTLS, accepts the command, then aborts the TLS
handshake.
- an initially unreachable MX which becomes available only after Bongo has
durably retained the original Queue 7 entry.
```sh
sudo ./contrib/testing/smtp-outbound-opportunistic-tls-check.py --live
@@ -2399,6 +2401,16 @@ opportunistic-handshake-fallback deliveries and required exactly one
SMTP-07 PASS starttls=tls/connections:1 plain=plaintext/connections:1 handshake-failure=plaintext-reconnect/connections:2 dkim=verified duplicates=no queue-clean=yes resolver-restored=yes config-restored=yes service-restored=active
```
The outage extension was then run against installed source commit
`9acc2890`. No server listened on the fourth MX address during the first
delivery attempt. Bongo retained one stable Queue 7 entry, the fixture brought
the same MX online, and an explicit retry delivered the original message
exactly once with a valid DKIM signature:
```text
SMTP-07 PASS starttls=tls/connections:1 plain=plaintext/connections:1 handshake-failure=plaintext-reconnect/connections:2 internet-outage=retained/recovered dkim=verified duplicates=no queue-clean=yes resolver-restored=yes config-restored=yes service-restored=active
```
## SMTP-08
Result: **PASS**
+2
View File
@@ -418,6 +418,7 @@ typedef int NMAPDeliveryCode;
#define QUEUE_ADDRESS 'A'
#define QUEUE_THIRD_PARTY 'T'
#define QUEUE_CLIENT_METADATA 'P'
#define QUEUE_RETRY_STATE 'Y'
/* Queue prefixes (String) */
#define QUEUES_ID "I"
@@ -432,6 +433,7 @@ typedef int NMAPDeliveryCode;
#define QUEUES_ADDRESS "A"
#define QUEUES_THIRD_PARTY "T"
#define QUEUES_CLIENT_METADATA "P"
#define QUEUES_RETRY_STATE "Y"
/* Result codes */
#define NMAP_READY 1000
+10
View File
@@ -9,6 +9,7 @@ add_executable(bongoqueue
queued.c
mime.c
queue.c
retry-policy.c
spool-file.c
store-delivery-format.c
)
@@ -86,4 +87,13 @@ if(BUILD_TESTING)
${CMAKE_CURRENT_SOURCE_DIR}
)
add_test(NAME queue-spool-file COMMAND queue-spool-file-test)
add_executable(queue-retry-policy-test
tests/retry-policy-test.c
retry-policy.c
)
target_include_directories(queue-retry-policy-test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
add_test(NAME queue-retry-policy COMMAND queue-retry-policy-test)
endif()
+22 -1
View File
@@ -42,7 +42,10 @@
#define STACKSPACE_Q (1024*80)
#define STACKSPACE_S (1024*80)
#define DEFAULT_MAX_LINGER (4 * 24 * 60 * 60)
#define DEFAULT_MAX_LINGER (5 * 24 * 60 * 60)
#define DEFAULT_BOUNCE_MAX_LINGER (5 * 24 * 60 * 60)
#define DEFAULT_RETRY_INITIAL (5 * 60)
#define DEFAULT_RETRY_MAXIMUM 4000
#define QLIMIT_CONCURRENT 250
#define QLIMIT_SEQUENTIAL 500
@@ -91,11 +94,14 @@ static BongoConfigItem QueueConfig[] = {
BONGO_CONFIG_INT("o:queuetuning_load_low/i", &Conf.loadMonitorLow),
BONGO_CONFIG_INT("o:queuetuning_trigger/i", &Conf.limitTrigger),
BONGO_CONFIG_INT("o:queuetimeout/i", &Conf.maxLinger),
BONGO_CONFIG_INT("o:bounce_queue_timeout/i", &Conf.bounceMaxLinger),
{ BONGO_JSON_STRING, "o:quotamessage/s", &Conf.quotaMessage },
BONGO_CONFIG_INT("o:port/i", &Agent.agent.port),
{ BONGO_JSON_BOOL, "o:forwardundeliverable_enabled/b", &Conf.forwardUndeliverableEnabled },
{ BONGO_JSON_STRING, "o:forwardundeliverable_to/s", &Conf.forwardUndeliverableAddress },
BONGO_CONFIG_INT("o:queueinterval/i", &Conf.queueInterval),
BONGO_CONFIG_INT("o:retry_initial_seconds/i", &Conf.retryInitial),
BONGO_CONFIG_INT("o:retry_maximum_seconds/i", &Conf.retryMaximum),
BONGO_CONFIG_INT("o:minimumfreespace/i", &Conf.minimumFree),
{ BONGO_JSON_ARRAY, "o:trustedhosts/a", &trustedHostsConfig },
{ BONGO_JSON_BOOL, "o:rtsantispamconfig_enabled/b", &Conf.bounceBlockSpam },
@@ -126,6 +132,21 @@ ReadConfiguration (BOOL *recover)
if (!ReadBongoConfiguration(GlobalConfig, "global")) {
return FALSE;
}
/*
* A pre-0.7 Store document does not contain the retry fields. Keep the
* daemon safe until the configuration editor merges the current template
* and synchronizes it back to Store.
*/
if (Conf.maxLinger <= 0)
Conf.maxLinger = DEFAULT_MAX_LINGER;
if (Conf.bounceMaxLinger <= 0)
Conf.bounceMaxLinger = DEFAULT_BOUNCE_MAX_LINGER;
if (Conf.retryInitial <= 0)
Conf.retryInitial = DEFAULT_RETRY_INITIAL;
if (Conf.retryMaximum < Conf.retryInitial)
Conf.retryMaximum = Conf.retryInitial > DEFAULT_RETRY_MAXIMUM ?
Conf.retryInitial : DEFAULT_RETRY_MAXIMUM;
*recover = MsgGetRecoveryFlag("queue");
+3
View File
@@ -63,8 +63,11 @@ typedef struct _QueueConfiguration {
char deferEnd[7];
time_t maxLinger;
time_t bounceMaxLinger;
time_t queueInterval;
time_t retryInitial;
time_t retryMaximum;
/* Forward Undeliverable */
BOOL forwardUndeliverableEnabled;
+123 -11
View File
@@ -26,8 +26,10 @@
#include <dirent.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <stdint.h>
#include <sys/stat.h>
#include <utime.h>
#include <unistd.h>
#include "queue.h"
@@ -37,6 +39,7 @@
#include "messages.h"
#include "dsn-format.h"
#include "envelope-format.h"
#include "retry-policy.h"
#include "spool-file.h"
#include "store-delivery-format.h"
#include "delivery-policy.h"
@@ -112,6 +115,82 @@ int IntCmp(const void *key1, const void *key2);
static int HandleDSN(FILE *data, FILE *control);
static BOOL ProcessQueueEntry(char *entryIn);
static BOOL
ScheduleQueueRetry(const char *entry, int queue)
{
char controlPath[XPL_MAX_PATH + 1];
char workPath[XPL_MAX_PATH + 1];
char line[CONN_BUFSIZE + 1];
FILE *control = NULL;
FILE *work = NULL;
unsigned long attempt = 0;
unsigned long parsedAttempt;
uint64_t delay;
time_t now;
struct utimbuf timestamps;
BOOL result = FALSE;
if (!QueueFormat(controlPath, "%s/c%s.%03d", Conf.spoolPath, entry,
queue) ||
!QueueFormat(workPath, "%s/w%s.%03d", Conf.spoolPath, entry, queue))
return FALSE;
control = fopen(controlPath, "rb");
work = fopen(workPath, "wb");
if (!control || !work)
goto cleanup;
while (fgets(line, sizeof(line), control)) {
if (BongoQueueParseRetryState(line, &parsedAttempt)) {
if (parsedAttempt > attempt)
attempt = parsedAttempt;
continue;
}
if (BongoQueueFileWrite(work, line, strlen(line)) != 0)
goto cleanup;
}
if (ferror(control))
goto cleanup;
if (fclose(control) != 0) {
control = NULL;
goto cleanup;
}
control = NULL;
if (attempt != ULONG_MAX)
attempt++;
if (BongoQueueFilePrintf(work, QUEUES_RETRY_STATE"%lu\r\n",
attempt) != 0 ||
BongoQueueFileCommit(&work, workPath, controlPath) != 0)
goto cleanup;
delay = BongoQueueRetryDelay((uint64_t)Conf.retryInitial,
(uint64_t)Conf.retryMaximum, attempt);
now = time(NULL);
timestamps.actime = now;
timestamps.modtime = now + (time_t)delay;
if (utime(controlPath, &timestamps) != 0) {
Log(LOG_WARNING,
"Could not persist retry time for queue entry %03d-%s: %s",
queue, entry, strerror(errno));
goto cleanup;
}
Log(LOG_NOTICE,
"Deferred queue entry %03d-%s for %" PRIu64
" seconds after attempt %lu",
queue, entry, delay, attempt);
result = TRUE;
cleanup:
if (control)
fclose(control);
if (work)
fclose(work);
if (!result)
unlink(workPath);
return result;
}
static void
StartImportedQueueEntry(unsigned long id)
{
@@ -888,6 +967,8 @@ ProcessQueueEntry(char *entryIn)
char entry[15];
BOOL keep = TRUE;
BOOL bounce = FALSE;
BOOL nullSender = FALSE;
BOOL dateFound = FALSE;
time_t date;
struct sockaddr_in saddr;
struct stat sb;
@@ -940,14 +1021,28 @@ StartOver:
}
if (fh) {
if (fgets(line, CONN_BUFSIZE, fh) == NULL) {
date = 0;
dateFound = FALSE;
nullSender = FALSE;
while (fgets(line, CONN_BUFSIZE, fh) != NULL) {
if (line[0] == QUEUE_DATE) {
date = (time_t)strtoimax(line + 1, NULL, 10);
dateFound = TRUE;
} else if (line[0] == QUEUE_FROM) {
nullSender = line[1] == '-' &&
(line[2] == ' ' || line[2] == '\r' ||
line[2] == '\n' || line[2] == '\0');
}
if (dateFound && line[0] == QUEUE_FROM)
break;
}
if (!dateFound) {
Log(LOG_ERROR, "Unable to read queue control file '%s': %s",
path, ferror(fh) ? strerror(errno) : "unexpected end of file");
FCLOSE_CHECK(fh);
ProcessQueueEntryCleanUp(entryID, report);
return(FALSE);
}
date = atoi(line + 1);
FCLOSE_CHECK(fh);
} else {
ProcessQueueEntryCleanUp(entryID, report);
@@ -1256,7 +1351,9 @@ StartOver:
/* It's in the deliver queue, check if it's local and deliver it, if not, hand it off to the agents!*/
/* By way of design,this function also removes all entries w/o any receiver from the queue */
case Q_OUTGOING: {
if (date < time(NULL) - Conf.maxLinger) {
time_t lifetime = BongoQueueDeliveryLifetime(
Conf.maxLinger, Conf.bounceMaxLinger, nullSender);
if (date < time(NULL) - lifetime) {
/* We move it to the Q_RTS queue and the linger code there will bounce it for us */
QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue);
QueueFormat(path2, "%s/c%s.%03d", Conf.spoolPath, entry, Q_RTS);
@@ -1754,7 +1851,7 @@ StartOver:
entry, Q_DELIVER);
RENAME_CHECK(path, Path2);
}
Queue.restartNeeded = TRUE;
ScheduleQueueRetry(entry, Q_DELIVER);
ProcessQueueEntryCleanUp(entryID, report);
return(TRUE);
}
@@ -2083,7 +2180,9 @@ StartOver:
}
case Q_RTS: {
if (date < time(NULL) - Conf.maxLinger) {
time_t lifetime = BongoQueueDeliveryLifetime(
Conf.maxLinger, Conf.bounceMaxLinger, nullSender);
if (date < time(NULL) - lifetime) {
if ((handle = QDBHandleAlloc()) != NULL) {
QDBRemoveID(handle, entryID);
@@ -2202,6 +2301,9 @@ StartOver:
}
}
if (queue == Q_OUTGOING && keep)
ScheduleQueueRetry(entry, Q_OUTGOING);
ProcessQueueEntryCleanUp(entryID, report);
return(TRUE);
@@ -2857,10 +2959,9 @@ CheckQueue(void *queueIn)
flushing = Queue.flushNeeded;
Queue.restartNeeded = Queue.flushNeeded = FALSE;
now = time(NULL);
if (flushing) {
Log(LOG_INFO, "Flushing the queue.");
} else {
now = time(NULL) - Conf.queueInterval + 60;
}
Log(LOG_DEBUG, "Restarting the queue");
@@ -2892,10 +2993,21 @@ CheckQueue(void *queueIn)
continue;
}
/* skip recently-touched messages that are in the outgoing
* queue (queue 7) */
if (!flushing && path[2] == '7' && (XplCalendarTime(dirEntry->d_cdatetime)+600) > (unsigned long)now) {
continue;
/*
* A retained local or remote delivery has a future mtime
* recording its next eligible retry. QFLUSH intentionally
* overrides that schedule for an administrator-requested run.
*/
if (!flushing &&
(atoi(path) == Q_DELIVER || atoi(path) == Q_OUTGOING)) {
struct stat retryStat;
char retryPath[XPL_MAX_PATH + 1];
QueueFormat(retryPath, "%s/%s", Conf.spoolPath,
dirEntry->d_nameDOS);
if (stat(retryPath, &retryStat) == 0 &&
retryStat.st_mtime > now)
continue;
}
/* wait for an empty queue slot */
+76
View File
@@ -0,0 +1,76 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
*
* To contact Novell about this file by physical or electronic mail, you
* may find current contact information at www.novell.com.
* </Novell-copyright>
****************************************************************************/
#include "retry-policy.h"
#include <nmap.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
uint64_t
BongoQueueRetryDelay(uint64_t initial_seconds, uint64_t maximum_seconds,
unsigned long attempt)
{
uint64_t delay;
if (initial_seconds == 0 || maximum_seconds < initial_seconds)
return 0;
delay = initial_seconds;
while (attempt > 1 && delay < maximum_seconds) {
if (delay > maximum_seconds / 2) {
delay = maximum_seconds;
break;
}
delay *= 2;
attempt--;
}
return delay > maximum_seconds ? maximum_seconds : delay;
}
int
BongoQueueParseRetryState(const char *line, unsigned long *attempt)
{
char *end;
unsigned long parsed;
if (!line || !attempt || line[0] != QUEUE_RETRY_STATE ||
!isdigit((unsigned char)line[1]))
return 0;
errno = 0;
parsed = strtoul(line + 1, &end, 10);
if (errno == ERANGE || end == line + 1 ||
(*end == '\r' && (end[1] != '\n' || end[2] != '\0')) ||
(*end == '\n' && end[1] != '\0') ||
(*end != '\0' && *end != '\r' && *end != '\n'))
return 0;
*attempt = parsed;
return 1;
}
time_t
BongoQueueDeliveryLifetime(time_t message_lifetime, time_t bounce_lifetime,
int null_sender)
{
return null_sender ? bounce_lifetime : message_lifetime;
}
+36
View File
@@ -0,0 +1,36 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
*
* To contact Novell about this file by physical or electronic mail, you
* may find current contact information at www.novell.com.
* </Novell-copyright>
****************************************************************************/
#ifndef BONGO_QUEUE_RETRY_POLICY_H
#define BONGO_QUEUE_RETRY_POLICY_H
#include <stdint.h>
#include <time.h>
uint64_t BongoQueueRetryDelay(uint64_t initial_seconds,
uint64_t maximum_seconds,
unsigned long attempt);
int BongoQueueParseRetryState(const char *line, unsigned long *attempt);
time_t BongoQueueDeliveryLifetime(time_t message_lifetime,
time_t bounce_lifetime,
int null_sender);
#endif
@@ -0,0 +1,53 @@
/****************************************************************************
* <Novell-copyright>
* Copyright (c) 2001 Novell, Inc. All Rights Reserved.
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of version 2 of the GNU General Public License
* as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, contact Novell, Inc.
*
* To contact Novell about this file by physical or electronic mail, you
* may find current contact information at www.novell.com.
* </Novell-copyright>
****************************************************************************/
#include "retry-policy.h"
#include <assert.h>
#include <stdint.h>
int
main(void)
{
unsigned long attempt = 0;
assert(BongoQueueRetryDelay(300, 4000, 1) == 300);
assert(BongoQueueRetryDelay(300, 4000, 2) == 600);
assert(BongoQueueRetryDelay(300, 4000, 3) == 1200);
assert(BongoQueueRetryDelay(300, 4000, 4) == 2400);
assert(BongoQueueRetryDelay(300, 4000, 5) == 4000);
assert(BongoQueueRetryDelay(300, 4000, 1000000) == 4000);
assert(BongoQueueRetryDelay(0, 4000, 1) == 0);
assert(BongoQueueRetryDelay(4001, 4000, 1) == 0);
assert(BongoQueueParseRetryState("Y1", &attempt) && attempt == 1);
assert(BongoQueueParseRetryState("Y42\r\n", &attempt) && attempt == 42);
assert(!BongoQueueParseRetryState("Y", &attempt));
assert(!BongoQueueParseRetryState("Y-1", &attempt));
assert(!BongoQueueParseRetryState("Y1 trailing", &attempt));
assert(!BongoQueueParseRetryState("Y1\rtrailing", &attempt));
assert(!BongoQueueParseRetryState("Y1\ntrailing", &attempt));
assert(!BongoQueueParseRetryState("X1", &attempt));
assert(BongoQueueDeliveryLifetime(432000, 432000, 0) == 432000);
assert(BongoQueueDeliveryLifetime(432000, 86400, 1) == 86400);
return 0;
}
+4 -1
View File
@@ -11,12 +11,15 @@
"queuetuning_load_high" : 0,
"queuetuning_load_low" : 0,
"queuetuning_trigger" : 0,
"queuetimeout" : 345600,
"queuetimeout" : 432000,
"bounce_queue_timeout" : 432000,
"quotamessage" : "",
"port" : 8670,
"forwardundeliverable_enabled" : false,
"forwardundeliverable_to" : "",
"queueinterval" : 10,
"retry_initial_seconds" : 300,
"retry_maximum_seconds" : 4000,
"minimumfreespace" : 5242880,
"trustedhosts" : [],
"hosteddomains" : [],
@@ -162,6 +162,25 @@ class ConfigurationModelTest(unittest.TestCase):
for issue in errors(validate_all(
configs, defaults, check_files=False))))
def test_queue_retry_delays_are_positive_and_ordered(self):
defaults = templates()
configs = templates()
queue = configs["queue"]
queue["retry_initial_seconds"] = 600
queue["retry_maximum_seconds"] = 300
problems = errors(validate_all(configs, defaults, check_files=False))
self.assertTrue(any(
issue.document == "queue" and
issue.path == "retry_maximum_seconds"
for issue in problems))
queue["retry_maximum_seconds"] = 1200
self.assertFalse(any(
issue.document == "queue" and
issue.path.startswith("retry_")
for issue in errors(validate_all(
configs, defaults, check_files=False))))
def test_smtp_socket_timeout_is_bounded(self):
defaults = templates()
for value in (0, 3601):
+8 -1
View File
@@ -505,9 +505,16 @@ def _validate_queue(config: Mapping[str, Any], issues: list[Issue]) -> None:
_validate_domains(issues, "queue", "domains", config.get("domains"), required=True)
_validate_domains(issues, "queue", "hosteddomains", config.get("hosteddomains"))
_port(issues, "queue", config, "port")
for key in ("queueinterval", "queuetimeout", "minimumfreespace",
for key in ("queueinterval", "queuetimeout", "bounce_queue_timeout",
"retry_initial_seconds", "retry_maximum_seconds",
"minimumfreespace",
"queuetuning_concurrent", "queuetuning_sequential"):
_positive(issues, "queue", config, key)
initial = config.get("retry_initial_seconds")
maximum = config.get("retry_maximum_seconds")
if isinstance(initial, int) and isinstance(maximum, int) and maximum < initial:
_issue(issues, "queue", "retry_maximum_seconds",
"maximum retry delay must not be shorter than the initial delay")
if config.get("forwardundeliverable_enabled") and not config.get("forwardundeliverable_to"):
_issue(issues, "queue", "forwardundeliverable_to",
"a destination is required when forwarding is enabled")