Test byte-exact SMTP 8BITMIME delivery
This commit is contained in:
@@ -36,6 +36,11 @@ one-off files in `/tmp`:
|
||||
command, and commands already buffered after the message terminator. The
|
||||
accepted message must reach Store exactly once and all marked fixtures are
|
||||
removed afterward.
|
||||
- `smtp-8bitmime-check.py` requires `8BITMIME` after EHLO, rejects its use
|
||||
after HELO and an invalid BODY value, then submits an ISO-8859-1 message
|
||||
containing deliberately non-UTF-8 high octets and a dot-stuffed line. The
|
||||
bytes following Bongo's added transport headers must match the submitted
|
||||
message exactly in Store.
|
||||
- `smtp-submission-auth-check.py` exercises port 587 STARTTLS and port 465
|
||||
implicit TLS directly, including the shared GSASL PLAIN and LOGIN
|
||||
mechanisms, pre-TLS AUTH rejection, invalid credentials, and repeated
|
||||
|
||||
Executable
+166
@@ -0,0 +1,166 @@
|
||||
#!/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 byte-exact inbound SMTP 8BITMIME delivery."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import time
|
||||
|
||||
|
||||
TOKEN = f"smtp13-{os.getpid()}-{int(time.time())}"
|
||||
TOKEN_PREFIX = b"smtp13-"
|
||||
TIMEOUT = float(os.environ.get("BONGO_TEST_TIMEOUT", "30"))
|
||||
ALLOW_LIVE = os.environ.get("BONGO_ALLOW_LIVE_SMTP_TEST") == "1"
|
||||
|
||||
|
||||
class SMTP13Error(RuntimeError):
|
||||
"""Raised when an 8BITMIME transaction changes message bytes."""
|
||||
|
||||
|
||||
def load_helpers():
|
||||
path = Path(__file__).with_name("smtp-pipelining-check.py")
|
||||
specification = importlib.util.spec_from_file_location(
|
||||
"bongo_smtp13_helpers", path
|
||||
)
|
||||
if specification is None or specification.loader is None:
|
||||
raise SMTP13Error(f"cannot load SMTP test helpers from {path}")
|
||||
module = importlib.util.module_from_spec(specification)
|
||||
sys.modules[specification.name] = module
|
||||
specification.loader.exec_module(module)
|
||||
module.TOKEN_PREFIX = TOKEN_PREFIX
|
||||
return module
|
||||
|
||||
|
||||
HELPERS = load_helpers()
|
||||
|
||||
|
||||
def desired_message() -> bytes:
|
||||
headers = (
|
||||
f"From: SMTP-13 <sender@example.test>\r\n"
|
||||
f"To: {HELPERS.USER}@{HELPERS.DOMAIN}\r\n"
|
||||
f"Subject: SMTP-13 byte preservation {TOKEN}\r\n"
|
||||
f"Message-ID: <{TOKEN}@{HELPERS.DOMAIN}>\r\n"
|
||||
f"X-Bongo-Test: {TOKEN}\r\n"
|
||||
"MIME-Version: 1.0\r\n"
|
||||
"Content-Type: text/plain; charset=iso-8859-1\r\n"
|
||||
"Content-Transfer-Encoding: 8bit\r\n"
|
||||
"\r\n"
|
||||
).encode("ascii")
|
||||
body = (
|
||||
b"Gr\xfc\xdfe aus dem 8-Bit-Test.\r\n"
|
||||
b".Diese Zeile beginnt mit einem Punkt.\r\n"
|
||||
b"Alle hohen Oktette bleiben erhalten: \x80\xa0\xc4\xe4\xfe\xff\r\n"
|
||||
)
|
||||
return headers + body
|
||||
|
||||
|
||||
def dot_stuff(message: bytes) -> bytes:
|
||||
if message.startswith(b"."):
|
||||
message = b"." + message
|
||||
return message.replace(b"\r\n.", b"\r\n..")
|
||||
|
||||
|
||||
def wait_for_message(store) -> tuple[str, bytes]:
|
||||
deadline = time.monotonic() + TIMEOUT
|
||||
while time.monotonic() < deadline:
|
||||
identifiers = HELPERS.store_token_documents(store)
|
||||
if len(identifiers) > 1:
|
||||
raise SMTP13Error(
|
||||
f"8BITMIME message delivered more than once: {identifiers!r}"
|
||||
)
|
||||
if identifiers:
|
||||
return identifiers[0], store.Read(identifiers[0])
|
||||
time.sleep(0.25)
|
||||
raise SMTP13Error("8BITMIME message did not reach the Store")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not ALLOW_LIVE:
|
||||
raise SMTP13Error(
|
||||
"set BONGO_ALLOW_LIVE_SMTP_TEST=1 for the live delivery test"
|
||||
)
|
||||
if not HELPERS.PASSWORD:
|
||||
raise SMTP13Error("BONGO_TEST_PASSWORD is required")
|
||||
|
||||
expected = desired_message()
|
||||
store = HELPERS.open_store()
|
||||
try:
|
||||
HELPERS.cleanup(store)
|
||||
client = HELPERS.SMTPConnection()
|
||||
try:
|
||||
client.require(220, "greeting")
|
||||
client.command("HELO smtp13-client.example", 250)
|
||||
client.command(
|
||||
"MAIL FROM:<sender@example.test> BODY=8BITMIME", 555
|
||||
)
|
||||
|
||||
capabilities = client.command(
|
||||
"EHLO smtp13-client.example", 250
|
||||
)
|
||||
if b"8BITMIME" not in capabilities:
|
||||
raise SMTP13Error(
|
||||
f"EHLO did not advertise 8BITMIME: {capabilities!r}"
|
||||
)
|
||||
client.command(
|
||||
"MAIL FROM:<sender@example.test> BODY=8BIT", 555
|
||||
)
|
||||
client.command(
|
||||
"MAIL FROM:<sender@example.test> BODY=8BITMIME", 250
|
||||
)
|
||||
client.command(
|
||||
f"RCPT TO:<{HELPERS.USER}@{HELPERS.DOMAIN}>", 250
|
||||
)
|
||||
client.command("DATA", 354)
|
||||
client.socket.sendall(dot_stuff(expected) + b".\r\n")
|
||||
client.require(250, "8BITMIME DATA completion")
|
||||
client.command("QUIT", 221)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
identifier, stored = wait_for_message(store)
|
||||
if not stored.endswith(expected):
|
||||
common = 0
|
||||
maximum = min(len(stored), len(expected))
|
||||
while common < maximum and stored[-maximum + common] == expected[common]:
|
||||
common += 1
|
||||
raise SMTP13Error(
|
||||
"stored message does not end in the exact submitted bytes "
|
||||
f"(stored={len(stored)}, submitted={len(expected)}, "
|
||||
f"matching-prefix={common})"
|
||||
)
|
||||
if b"\x80\xa0\xc4\xe4\xfe\xff\r\n" not in stored:
|
||||
raise SMTP13Error("stored message omitted high-bit octets")
|
||||
if b"\r\n.Diese Zeile" not in stored:
|
||||
raise SMTP13Error("SMTP dot transparency changed the leading dot")
|
||||
|
||||
store.Delete(identifier)
|
||||
HELPERS.cleanup(store)
|
||||
if HELPERS.store_token_documents(store) or HELPERS.queue_token_ids():
|
||||
raise SMTP13Error("test content remains after cleanup")
|
||||
finally:
|
||||
try:
|
||||
HELPERS.cleanup(store)
|
||||
finally:
|
||||
store.Quit()
|
||||
|
||||
print(
|
||||
"SMTP-13 PASS advertised=yes helo-parameter=555 "
|
||||
"invalid-body=555 body-8bitmime=250 high-octets=exact "
|
||||
"dot-transparency=exact duplicate=no queue-clean=yes store-clean=yes"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (OSError, SMTP13Error, ValueError) as error:
|
||||
print(f"SMTP-13 FAIL: {type(error).__name__}: {error}")
|
||||
raise SystemExit(1)
|
||||
@@ -140,7 +140,7 @@ inputs and outputs is absent from the ZIP.
|
||||
| SMTP-10 | Open-relay tests reject unauthenticated/untrusted third-party relay | [PASS](test-evidence/0.7-r1.md#smtp-10) | | |
|
||||
| SMTP-11 | `SIZE` exact limit, over-limit, absent size, and disk-space responses | [PASS](test-evidence/0.7-r1.md#smtp-11) | | |
|
||||
| SMTP-12 | `PIPELINING` ordering and error recovery | [PASS](test-evidence/0.7-r1.md#smtp-12) | | |
|
||||
| SMTP-13 | `8BITMIME` remains byte-correct | | | |
|
||||
| SMTP-13 | `8BITMIME` remains byte-correct | [PASS](test-evidence/0.7-r1.md#smtp-13) | | |
|
||||
| SMTP-14 | DSN `RET`, `ENVID`, `NOTIFY`, and `ORCPT` cases | | | |
|
||||
| SMTP-15 | `CHUNKING`/bounded `BDAT`, including zero/last and malformed chunks | | | |
|
||||
| SMTP-16 | `BINARYMIME` remains unadvertised and rejected | | | |
|
||||
|
||||
@@ -2603,6 +2603,42 @@ No configuration or service state changed. The fixture removed the delivered
|
||||
Store object and any uniquely marked interrupted-run entries; the final Queue
|
||||
and Store checks were empty.
|
||||
|
||||
## SMTP-13
|
||||
|
||||
Result: **PASS**
|
||||
|
||||
The reusable `smtp-8bitmime-check.py` first used classic HELO and verified
|
||||
that the ESMTP-only `BODY=8BITMIME` parameter received `555`. After EHLO it
|
||||
required the advertised `8BITMIME` capability, rejected the invalid
|
||||
`BODY=8BIT` value with `555`, and accepted `BODY=8BITMIME`.
|
||||
|
||||
The test message used ASCII headers declaring `charset=iso-8859-1` and
|
||||
`Content-Transfer-Encoding: 8bit`. Its body deliberately contained the raw
|
||||
octet sequence `80 a0 c4 e4 fe ff`, including bytes which are not a valid
|
||||
standalone UTF-8 sequence. Another body line began with a dot and was
|
||||
dot-stuffed only on the SMTP wire.
|
||||
|
||||
After local Queue-to-Store delivery, the fixture allowed Bongo's expected
|
||||
transport headers before the submitted message but required every submitted
|
||||
byte from `From:` through the final CRLF to be an exact suffix of the stored
|
||||
object. The high-octet sequence was unchanged and dot transparency produced
|
||||
exactly one leading dot.
|
||||
|
||||
```sh
|
||||
export BONGO_ALLOW_LIVE_SMTP_TEST=1
|
||||
export BONGO_TEST_PASSWORD='the-disposable-test-password'
|
||||
./contrib/testing/smtp-8bitmime-check.py
|
||||
```
|
||||
|
||||
The installed `d782d593` debug build reported on 2026-07-27:
|
||||
|
||||
```text
|
||||
SMTP-13 PASS advertised=yes helo-parameter=555 invalid-body=555 body-8bitmime=250 high-octets=exact dot-transparency=exact duplicate=no queue-clean=yes store-clean=yes
|
||||
```
|
||||
|
||||
No configuration or service state changed. The delivered Store object and
|
||||
all uniquely marked interrupted-run Queue or Store fixtures were removed.
|
||||
|
||||
## SMTP-20
|
||||
|
||||
Result: **PASS**
|
||||
|
||||
Reference in New Issue
Block a user