Add load-dependent SMTP timeouts
Debian Trixie package bundle / packages (push) Failing after 13m7s

This commit is contained in:
Mario Fetka
2026-07-24 13:24:08 +02:00
parent eea5e634b7
commit 54eee9efaf
15 changed files with 526 additions and 1 deletions
+5
View File
@@ -49,6 +49,11 @@ one-off files in `/tmp`:
proves that slow manual `EHLO`, `NOOP`, and `RSET` input resets the timeout
after each complete command. It restores the SMTP configuration and active
service before exiting.
- `smtp-stress-timeout-check.py` uses only two simultaneous connections to
trigger the configurable load threshold. It proves that the first
connection keeps the normal timeout while newly accepted inbound,
trusted-device, and submission connections receive the shorter overload
timeout, then restores the exact configuration and active service.
- `sendmail-local-submission-check.py` runs as an unprivileged Unix account
and verifies both `/usr/sbin/sendmail` and GNU `mail` through Bongo's
restricted, rate-limited local SMTP submission path. It confirms that no
+281
View File
@@ -0,0 +1,281 @@
#!/usr/bin/env python3
# This program is free software, licensed under the terms of the GNU GPL.
# See the Bongo COPYING file for full details.
# Copyright (c) 2026 Bongo Project contributors
"""Verify load-dependent SMTP timeouts with two real connections."""
from __future__ import annotations
import copy
import json
import os
import select
import socket
import subprocess
import time
ADMIN = os.environ.get("BONGO_TEST_ADMIN", "/usr/bin/bongo-admin")
SYSTEMCTL = os.environ.get("BONGO_TEST_SYSTEMCTL", "/usr/bin/systemctl")
INBOUND_HOST = os.environ.get("BONGO_TEST_INBOUND_HOST", "127.0.0.1")
INBOUND_PORT = int(os.environ.get("BONGO_TEST_INBOUND_PORT", "25"))
INTERNAL_HOST = os.environ.get(
"BONGO_TEST_INTERNAL_HOST", "172.16.11.190")
INTERNAL_PORT = int(os.environ.get("BONGO_TEST_INTERNAL_PORT", "26"))
SUBMISSION_HOST = os.environ.get(
"BONGO_TEST_SUBMISSION_HOST", "127.0.0.1")
SUBMISSION_PORT = int(os.environ.get("BONGO_TEST_SUBMISSION_PORT", "587"))
NORMAL_TIMEOUT = int(os.environ.get("BONGO_TEST_NORMAL_TIMEOUT", "5"))
STRESS_TIMEOUT = int(os.environ.get("BONGO_TEST_STRESS_TIMEOUT", "2"))
STARTUP_GRACE = float(os.environ.get("BONGO_TEST_STARTUP_GRACE", "2"))
ALLOW_LIVE = os.environ.get("BONGO_ALLOW_LIVE_SMTP_TEST") == "1"
class StressTimeoutError(RuntimeError):
"""Raised when SMTP does not apply its load-dependent timeout."""
def run(arguments: list[str], *, input_text: str | None = None) -> str:
completed = subprocess.run(
arguments,
input=input_text,
capture_output=True,
text=True,
check=False,
)
if completed.returncode:
raise StressTimeoutError(
f"{' '.join(arguments)} failed: "
f"{completed.stderr.strip() or completed.stdout.strip()}")
return completed.stdout
def admin(*arguments: str, input_text: str | None = None) -> str:
return run(
["sudo", "-n", ADMIN, *arguments],
input_text=input_text,
)
def read_configuration() -> dict:
try:
configuration = json.loads(admin("__config-read", "smtp"))
except json.JSONDecodeError as error:
raise StressTimeoutError(
"bongo-admin returned invalid SMTP configuration") from error
if not isinstance(configuration, dict):
raise StressTimeoutError("SMTP configuration is not an object")
return configuration
def replace_configuration(configuration: dict) -> None:
admin(
"__config-replace",
"smtp",
input_text=json.dumps(configuration, separators=(",", ":")),
)
def read_greeting(connection: socket.socket, name: str) -> None:
data = bytearray()
while b"\n" not in data and len(data) <= 8192:
chunk = connection.recv(4096)
if not chunk:
raise StressTimeoutError(
f"{name} closed before the SMTP greeting")
data.extend(chunk)
if not bytes(data).startswith(b"220 "):
raise StressTimeoutError(
f"{name} returned an invalid SMTP greeting: {bytes(data)!r}")
def open_connection(host: str, port: int, name: str) -> socket.socket:
connection = socket.create_connection((host, port), timeout=10)
connection.settimeout(10)
try:
read_greeting(connection, name)
except BaseException:
connection.close()
raise
return connection
def wait_closed(
connection: socket.socket,
started: float,
expected: int,
name: str,
) -> float:
deadline = started + expected + 5
received = bytearray()
while time.monotonic() < deadline:
connection.settimeout(max(0.1, deadline - time.monotonic()))
try:
chunk = connection.recv(4096)
except socket.timeout:
break
if not chunk:
elapsed = time.monotonic() - started
if elapsed < max(0.5, expected * 0.65):
raise StressTimeoutError(
f"{name} closed too early after {elapsed:.2f}s")
return elapsed
received.extend(chunk)
raise StressTimeoutError(
f"{name} remained open beyond {expected + 5}s; "
f"unexpected data={bytes(received)!r}")
def assert_still_open(connection: socket.socket, name: str) -> None:
readable, _, _ = select.select([connection], [], [], 0)
if not readable:
return
data = connection.recv(4096)
if not data:
raise StressTimeoutError(
f"{name} normal connection received the stress timeout")
raise StressTimeoutError(
f"{name} normal connection returned unexpected data: {data!r}")
def wait_ready(host: str, port: int, name: str) -> None:
deadline = time.monotonic() + 30
while time.monotonic() < deadline:
connection: socket.socket | None = None
try:
connection = open_connection(host, port, name)
connection.sendall(b"QUIT\r\n")
reply = connection.recv(4096)
if not reply.startswith(b"221 "):
raise OSError("SMTP did not accept readiness QUIT")
return
except (OSError, StressTimeoutError):
time.sleep(0.1)
finally:
if connection is not None:
connection.close()
raise StressTimeoutError(
f"{name} did not become ready at {host}:{port}")
def restart_bongo() -> None:
run(["sudo", "-n", SYSTEMCTL, "restart", "bongo.service"])
time.sleep(STARTUP_GRACE)
for name, host, port in (
("inbound", INBOUND_HOST, INBOUND_PORT),
("internal", INTERNAL_HOST, INTERNAL_PORT),
("submission", SUBMISSION_HOST, SUBMISSION_PORT),
):
wait_ready(host, port, name)
if run(
["sudo", "-n", SYSTEMCTL, "is-active", "bongo.service"]
).strip() != "active":
raise StressTimeoutError("bongo.service is not active")
def check_endpoint(name: str, host: str, port: int) -> tuple[float, float]:
normal: socket.socket | None = None
stressed: socket.socket | None = None
try:
normal_started = time.monotonic()
normal = open_connection(host, port, f"{name} normal")
time.sleep(0.25)
stress_started = time.monotonic()
stressed = open_connection(host, port, f"{name} stressed")
stress_elapsed = wait_closed(
stressed, stress_started, STRESS_TIMEOUT, f"{name} stressed")
stressed.close()
stressed = None
assert_still_open(normal, f"{name} normal")
normal_elapsed = wait_closed(
normal, normal_started, NORMAL_TIMEOUT, f"{name} normal")
normal.close()
normal = None
if stress_elapsed >= normal_elapsed:
raise StressTimeoutError(
f"{name} stress timeout was not shorter than normal")
return normal_elapsed, stress_elapsed
finally:
if stressed is not None:
stressed.close()
if normal is not None:
normal.close()
def main() -> int:
if not ALLOW_LIVE:
raise StressTimeoutError(
"set BONGO_ALLOW_LIVE_SMTP_TEST=1 for the live stress test")
if not 1 <= STRESS_TIMEOUT < NORMAL_TIMEOUT <= 15:
raise StressTimeoutError(
"require 1 <= stress timeout < normal timeout <= 15 seconds")
original = read_configuration()
test_configuration = copy.deepcopy(original)
test_configuration.update({
"socket_timeout": NORMAL_TIMEOUT,
"stress_socket_timeout": STRESS_TIMEOUT,
"stress_load_percent": 100,
"max_thread_load": 2,
})
results: list[str] = []
test_error: BaseException | None = None
restore_error: BaseException | None = None
try:
replace_configuration(test_configuration)
restart_bongo()
for name, host, port in (
("inbound", INBOUND_HOST, INBOUND_PORT),
("internal", INTERNAL_HOST, INTERNAL_PORT),
("submission", SUBMISSION_HOST, SUBMISSION_PORT),
):
normal, stressed = check_endpoint(name, host, port)
results.append(
f"{name}=normal:{normal:.2f}s,stress:{stressed:.2f}s")
except BaseException as error:
test_error = error
finally:
try:
replace_configuration(original)
restart_bongo()
if read_configuration() != original:
raise StressTimeoutError(
"SMTP configuration was not restored exactly")
except BaseException as error:
restore_error = error
if restore_error is not None:
if test_error is not None:
raise StressTimeoutError(
f"test failed ({test_error}) and restoration also failed "
f"({restore_error})")
raise StressTimeoutError(
f"failed to restore SMTP configuration: {restore_error}")
if test_error is not None:
raise test_error
print(
"SMTP-33 PASS "
f"maximum-connections=2 stress-threshold=100% "
f"normal-timeout={NORMAL_TIMEOUT}s stress-timeout={STRESS_TIMEOUT}s "
f"sessions={';'.join(results)} "
"config-restored=yes service=active"
)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except (
OSError,
StressTimeoutError,
subprocess.SubprocessError,
ValueError,
) as error:
print(f"SMTP-33 FAIL: {type(error).__name__}: {error}")
raise SystemExit(1)
+1
View File
@@ -132,6 +132,7 @@ inputs and outputs is absent from the ZIP.
| SMTP-30 | curl, swaks, Xeams, GNU Mailutils, and paced Telnet delivery over authenticated submission and unauthenticated port 25/26 | [PASS](test-evidence/0.7-r1.md#smtp-30) | | |
| SMTP-31 | Unprivileged sendmail-compatible local submission without the Bongo system credential | [PASS](test-evidence/0.7-r1.md#smtp-31) | | |
| SMTP-32 | Telnet activity resets the configured per-operation timeout; abandoned port 25/26 sessions close | [PASS](test-evidence/0.7-r1.md#smtp-32) | | |
| SMTP-33 | Load threshold gives newly accepted inbound, trusted-device, and submission sessions the short stress timeout | | | |
| SMTP-07 | Internet delivery prefers STARTTLS and follows opportunistic fallback | | | |
| SMTP-08 | Required TLS and `REQUIRETLS` defer rather than downgrade | | | |
| SMTP-27 | DANE takes precedence; MTA-STS enforce/testing/cache and MX wildcard policy | | | |
+8
View File
@@ -41,6 +41,14 @@ worker indefinitely. The default is 300 seconds, matching Postfix's normal
`smtpd_timeout`; values from 1 through 3600 seconds are accepted. Bongo rejects
zero rather than treating it as an unlimited connection.
When the projected number of active SMTP connection threads reaches
`stress_load_percent` of `max_thread_load`, a newly accepted connection uses
`stress_socket_timeout` instead. The defaults are 80 percent and 10 seconds,
following Postfix's normal-versus-overload timeout model. Existing sessions
retain the timeout selected when they were accepted, while new sessions
return to the normal value automatically as load falls. The shorter policy
also covers the PROXY header and TLS negotiation performed after accept.
## Trusted downstream proxy metadata
Bongo can preserve the original SMTP client's connection address when it
+1
View File
@@ -251,6 +251,7 @@ Connection *ConnAddressPoolConnect(AddressPool *pool, unsigned long timeOut);
BOOL ConnStartup(unsigned long TimeOut);
BOOL ConnSetDefaultTimeout(unsigned long TimeOut);
BOOL ConnSetTimeout(Connection *Conn, unsigned long TimeOut);
void ConnShutdown(void);
void ConnSSLContextFree(bongo_ssl_context *Context);
+10
View File
@@ -5,6 +5,7 @@ add_executable(bongosmtp
internal-relay.c
capabilities.c
auth-results.c
load-policy.c
protocol.c
proxy.c
)
@@ -76,6 +77,15 @@ if(BUILD_TESTING)
)
add_test(NAME smtp-protocol COMMAND smtp-protocol-test)
add_executable(smtp-load-policy-test
tests/load-policy-test.c
load-policy.c
)
target_include_directories(smtp-load-policy-test PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
)
add_test(NAME smtp-load-policy COMMAND smtp-load-policy-test)
add_executable(smtp-internal-relay-test
tests/internal-relay-test.c
internal-relay.c
+46
View File
@@ -0,0 +1,46 @@
/****************************************************************************
* <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 "load-policy.h"
#include <stdint.h>
unsigned long
SMTPConnectionTimeoutForLoad(
unsigned int activeConnections,
unsigned int maximumConnections,
unsigned int stressLoadPercent,
unsigned long normalTimeout,
unsigned long stressTimeout)
{
uint64_t projected;
uint64_t threshold;
if (maximumConnections == 0U || stressLoadPercent == 0U ||
stressLoadPercent > 100U) {
return normalTimeout;
}
projected = (uint64_t)activeConnections + 1U;
threshold = ((uint64_t)maximumConnections * stressLoadPercent + 99U) /
100U;
return projected >= threshold ? stressTimeout : normalTimeout;
}
+32
View File
@@ -0,0 +1,32 @@
/****************************************************************************
* <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_SMTP_LOAD_POLICY_H
#define BONGO_SMTP_LOAD_POLICY_H
unsigned long SMTPConnectionTimeoutForLoad(
unsigned int activeConnections,
unsigned int maximumConnections,
unsigned int stressLoadPercent,
unsigned long normalTimeout,
unsigned long stressTimeout);
#endif
+43 -1
View File
@@ -54,6 +54,7 @@
#include "auth-results.h"
#include "external_accounts.h"
#include "capabilities.h"
#include "load-policy.h"
#include "protocol.h"
struct {
@@ -99,6 +100,8 @@ struct {
int max_null_sender;
int max_mx_servers;
int socket_timeout;
int stress_socket_timeout;
int stress_load_percent;
BOOL spf_verify;
BOOL dkim_verify;
BOOL dmarc_verify;
@@ -107,7 +110,11 @@ struct {
char *srs_domain;
char *srs_secret_file;
GArray *lmtp_transports;
} SMTP;
} SMTP = {
.socket_timeout = 300,
.stress_socket_timeout = 10,
.stress_load_percent = 80
};
static BongoConfigItem LMTPTransportList = {
BONGO_JSON_STRING, NULL, &SMTP.lmtp_transports
@@ -166,6 +173,8 @@ static BongoConfigItem SMTPConfig[] = {
BONGO_CONFIG_INT("o:max_flood_count/i", &SMTP.max_flood_count), // UNUSED ?
BONGO_CONFIG_INT("o:max_null_sender/i", &SMTP.max_null_sender),
BONGO_CONFIG_INT("o:socket_timeout/i", &SMTP.socket_timeout),
BONGO_CONFIG_INT("o:stress_socket_timeout/i", &SMTP.stress_socket_timeout),
BONGO_CONFIG_INT("o:stress_load_percent/i", &SMTP.stress_load_percent),
BONGO_CONFIG_INT("o:max_mx_servers/i", &SMTP.max_mx_servers),
{ BONGO_JSON_BOOL, "o:relay_local_mail/b", &SMTP.relay_local },
{ BONGO_JSON_BOOL, "o:require_auth/b", &SMTP.require_auth },
@@ -4948,6 +4957,21 @@ SMTPListenerReady(Connection *listener)
return (descriptor.revents & POLLIN) ? 1 : 0;
}
static void
SMTPApplyConnectionTimeout(Connection *connection)
{
unsigned long timeout = SMTPConnectionTimeoutForLoad(
(unsigned int)XplSafeRead(SMTPConnThreads),
(unsigned int)SMTP.max_thread_load,
(unsigned int)SMTP.stress_load_percent,
(unsigned long)SMTP.socket_timeout,
(unsigned long)SMTP.stress_socket_timeout);
if (!ConnSetTimeout(connection, timeout)) {
Log(LOG_ERROR, "Could not apply SMTP connection timeout");
}
}
static int
ServerSocketInit (void)
{
@@ -5063,6 +5087,7 @@ SMTPSubmissionServer(void *ignored)
if (!SMTP_IS_EXITING() && errno == EINTR) continue;
break;
}
SMTPApplyConnectionTimeout(conn);
if (!SMTPApplyProxyProtocol(conn, "SMTP submission")) {
ConnClose(conn);
ConnFree(conn);
@@ -5150,6 +5175,7 @@ SMTPInternalServer(void *ignored)
if (!SMTP_IS_EXITING() && errno == EINTR) continue;
break;
}
SMTPApplyConnectionTimeout(conn);
if (!SMTPApplyProxyProtocol(conn, "internal SMTP")) {
ConnClose(conn);
ConnFree(conn);
@@ -5246,6 +5272,7 @@ SMTPServer (void *ignored)
break;
}
if (ConnAccept(SMTPServerConnection, &conn) != -1) {
SMTPApplyConnectionTimeout(conn);
if (!SMTPApplyProxyProtocol(conn, "SMTP")) {
ConnClose(conn);
ConnFree(conn);
@@ -5427,6 +5454,7 @@ SMTPSSLServer (void *ignored)
break;
}
if (ConnAccept(SMTPServerConnectionSSL, &conn) != -1) {
SMTPApplyConnectionTimeout(conn);
if (!SMTPApplyProxyProtocol(conn, "SMTPS")) {
ConnClose(conn);
ConnFree(conn);
@@ -5634,6 +5662,20 @@ ValidateListenerConfiguration(void)
Log(LOG_ERROR, "SMTP socket timeout must be between 1 and 3600 seconds");
return FALSE;
}
if (SMTP.stress_socket_timeout < 1 ||
SMTP.stress_socket_timeout > SMTP.socket_timeout) {
Log(LOG_ERROR,
"SMTP stress socket timeout must be between 1 and the normal timeout");
return FALSE;
}
if (SMTP.stress_load_percent < 1 || SMTP.stress_load_percent > 100) {
Log(LOG_ERROR, "SMTP stress load percentage must be between 1 and 100");
return FALSE;
}
if (SMTP.max_thread_load < 1) {
Log(LOG_ERROR, "SMTP maximum thread load must be greater than zero");
return FALSE;
}
return TRUE;
}
+41
View File
@@ -0,0 +1,41 @@
/****************************************************************************
* <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 "load-policy.h"
#include <assert.h>
int
main(void)
{
assert(SMTPConnectionTimeoutForLoad(0, 10000, 80, 300, 10) == 300);
assert(SMTPConnectionTimeoutForLoad(7998, 10000, 80, 300, 10) == 300);
assert(SMTPConnectionTimeoutForLoad(7999, 10000, 80, 300, 10) == 10);
assert(SMTPConnectionTimeoutForLoad(9999, 10000, 80, 300, 10) == 10);
assert(SMTPConnectionTimeoutForLoad(0, 2, 100, 5, 2) == 5);
assert(SMTPConnectionTimeoutForLoad(1, 2, 100, 5, 2) == 2);
assert(SMTPConnectionTimeoutForLoad(100, 0, 80, 300, 10) == 300);
assert(SMTPConnectionTimeoutForLoad(100, 100, 0, 300, 10) == 300);
assert(SMTPConnectionTimeoutForLoad(100, 100, 101, 300, 10) == 300);
return 0;
}
+2
View File
@@ -47,6 +47,8 @@
"max_flood_count": 1000,
"max_null_sender": 10,
"socket_timeout": 300,
"stress_socket_timeout": 10,
"stress_load_percent": 80,
"max_mx_servers": 20,
"require_auth": false,
"spf_verify": true,
@@ -131,6 +131,34 @@ class ConfigurationModelTest(unittest.TestCase):
for issue in errors(validate_all(
configs, defaults, check_files=False))))
def test_smtp_stress_timeout_policy_is_bounded(self):
defaults = templates()
for key, value in (
("stress_socket_timeout", 0),
("stress_socket_timeout", 3601),
("stress_load_percent", 0),
("stress_load_percent", 101),
("max_thread_load", 0),
):
with self.subTest(key=key, value=value):
configs = templates()
configs["smtp"][key] = value
problems = errors(validate_all(
configs, defaults, check_files=False))
self.assertTrue(any(
issue.document == "smtp" and issue.path == key
for issue in problems))
configs = templates()
configs["smtp"]["socket_timeout"] = 5
configs["smtp"]["stress_socket_timeout"] = 6
problems = errors(validate_all(
configs, defaults, check_files=False))
self.assertTrue(any(
issue.document == "smtp" and
issue.path == "stress_socket_timeout"
for issue in problems))
def test_safe_reverse_proxy_scenario(self):
defaults = templates()
scenario = Scenario(
+13
View File
@@ -150,6 +150,19 @@ ConnSetDefaultTimeout(unsigned long TimeOut)
return TRUE;
}
BOOL
ConnSetTimeout(Connection *Conn, unsigned long TimeOut)
{
if (Conn == NULL || TimeOut == 0 ||
TimeOut > (unsigned long)(INT_MAX / 1000)) {
return FALSE;
}
Conn->receive.timeOut = TimeOut;
Conn->send.timeOut = TimeOut;
return TRUE;
}
void
ConnSSLContextFree(bongo_ssl_context *context)
{
@@ -48,6 +48,11 @@ main(void)
assert(after->send.timeOut == 3);
assert(before->receive.timeOut == 600);
assert(before->send.timeOut == 600);
assert(!ConnSetTimeout(NULL, 2));
assert(!ConnSetTimeout(before, 0));
assert(ConnSetTimeout(before, 2));
assert(before->receive.timeOut == 2);
assert(before->send.timeOut == 2);
ConnShutdown();
return 0;
@@ -578,7 +578,17 @@ def _validate_smtp(config: Mapping[str, Any], issues: list[Issue], check_files:
_issue(issues, "smtp", "proxy_protocol_networks",
"enabled PROXY protocol requires trusted proxy networks")
_positive(issues, "smtp", config, "max_thread_load", 100000)
_positive(issues, "smtp", config, "socket_timeout", 3600)
_positive(issues, "smtp", config, "stress_socket_timeout", 3600)
_positive(issues, "smtp", config, "stress_load_percent", 100)
normal_timeout = config.get("socket_timeout")
stress_timeout = config.get("stress_socket_timeout")
if (isinstance(normal_timeout, int) and
isinstance(stress_timeout, int) and
stress_timeout > normal_timeout):
_issue(issues, "smtp", "stress_socket_timeout",
"stress timeout must not exceed the normal socket timeout")
for key in ("xclient_trusted_destinations", "xforward_trusted_destinations",
"proxy_header_trusted_destinations"):