diff --git a/contrib/testing/README.md b/contrib/testing/README.md
index 86d2cb2..5aa9876 100644
--- a/contrib/testing/README.md
+++ b/contrib/testing/README.md
@@ -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
diff --git a/contrib/testing/smtp-stress-timeout-check.py b/contrib/testing/smtp-stress-timeout-check.py
new file mode 100755
index 0000000..625ef79
--- /dev/null
+++ b/contrib/testing/smtp-stress-timeout-check.py
@@ -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)
diff --git a/docs/release-testing-0.7.md b/docs/release-testing-0.7.md
index 3ef414b..eee481a 100644
--- a/docs/release-testing-0.7.md
+++ b/docs/release-testing-0.7.md
@@ -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 | | | |
diff --git a/docs/smtp.md b/docs/smtp.md
index 21156ee..493c9f2 100644
--- a/docs/smtp.md
+++ b/docs/smtp.md
@@ -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
diff --git a/include/connio.h b/include/connio.h
index 2f9e6e8..51ea344 100644
--- a/include/connio.h
+++ b/include/connio.h
@@ -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);
diff --git a/src/agents/smtp/CMakeLists.txt b/src/agents/smtp/CMakeLists.txt
index 279016f..de81836 100644
--- a/src/agents/smtp/CMakeLists.txt
+++ b/src/agents/smtp/CMakeLists.txt
@@ -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
diff --git a/src/agents/smtp/load-policy.c b/src/agents/smtp/load-policy.c
new file mode 100644
index 0000000..0a1deff
--- /dev/null
+++ b/src/agents/smtp/load-policy.c
@@ -0,0 +1,46 @@
+/****************************************************************************
+ *
+ * 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.
+ *
+ ****************************************************************************/
+
+#include "load-policy.h"
+
+#include
+
+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;
+}
diff --git a/src/agents/smtp/load-policy.h b/src/agents/smtp/load-policy.h
new file mode 100644
index 0000000..0867f75
--- /dev/null
+++ b/src/agents/smtp/load-policy.h
@@ -0,0 +1,32 @@
+/****************************************************************************
+ *
+ * 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.
+ *
+ ****************************************************************************/
+
+#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
diff --git a/src/agents/smtp/smtpd.c b/src/agents/smtp/smtpd.c
index a7e33f6..c82d40a 100644
--- a/src/agents/smtp/smtpd.c
+++ b/src/agents/smtp/smtpd.c
@@ -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;
}
diff --git a/src/agents/smtp/tests/load-policy-test.c b/src/agents/smtp/tests/load-policy-test.c
new file mode 100644
index 0000000..7c62ba9
--- /dev/null
+++ b/src/agents/smtp/tests/load-policy-test.c
@@ -0,0 +1,41 @@
+/****************************************************************************
+ *
+ * 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.
+ *
+ ****************************************************************************/
+
+#include "load-policy.h"
+
+#include
+
+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;
+}
diff --git a/src/apps/config/config/smtp b/src/apps/config/config/smtp
index 8a0b7c8..8004a47 100644
--- a/src/apps/config/config/smtp
+++ b/src/apps/config/config/smtp
@@ -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,
diff --git a/src/apps/config/tests/test_configuration.py b/src/apps/config/tests/test_configuration.py
index 39b019a..b369a3e 100644
--- a/src/apps/config/tests/test_configuration.py
+++ b/src/apps/config/tests/test_configuration.py
@@ -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(
diff --git a/src/libs/connio/connio.c b/src/libs/connio/connio.c
index 21ca691..92e4f5b 100644
--- a/src/libs/connio/connio.c
+++ b/src/libs/connio/connio.c
@@ -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)
{
diff --git a/src/libs/connio/tests/default-timeout-test.c b/src/libs/connio/tests/default-timeout-test.c
index fc3d56e..0bda4e8 100644
--- a/src/libs/connio/tests/default-timeout-test.c
+++ b/src/libs/connio/tests/default-timeout-test.c
@@ -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;
diff --git a/src/libs/python/bongo/configuration/model.py b/src/libs/python/bongo/configuration/model.py
index 4b08bb8..a3b72a4 100644
--- a/src/libs/python/bongo/configuration/model.py
+++ b/src/libs/python/bongo/configuration/model.py
@@ -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"):