Wire IDNA2008 canonicalization as an unconditional, no-instance ctest

Split out of smtp-idna-check.py's verify_installed_python_paths(): pure
source-tree Python (bongo.configuration.model.canonicalize_domains,
bongo.domain.domain_to_ascii, bongo_web.compose.build_message) that never
touched the network or needed a live instance in the first place, unlike
the rest of that script. Registered unconditionally next to
c-literal-length-check.py, so it runs even without
BONGO_CTEST_LIVE_INSTANCE.

smtp-idna-check.py's remaining live-routing assertions (local Unicode-
domain delivery, direct-MX + relay routing to Unicode targets with
canonical-A-label DNS/envelope verification, invalid-A-label rejection)
still need real root + systemd, and don't fit either shared fixture
cleanly: use_relay_host is a global SMTP-wide toggle, so permanently
enabling it for a live_instance/netns_instance relay test would break
every other already-wired test relying on direct MX delivery on that same
instance. Left for a dedicated instance if pursued further.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mario Fetka
2026-08-02 15:45:59 +02:00
parent 66d36d6353
commit 706fcab428
2 changed files with 122 additions and 0 deletions
+10
View File
@@ -5,6 +5,16 @@
add_test(NAME c-literal-length
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/c-literal-length-check.py")
# idna-canonicalization-check.py is pure source-tree Python (bongo.
# configuration.model, bongo.domain, bongo_web.compose) -- no live
# instance, no build, no BONGO_CTEST_LIVE_INSTANCE needed, so it's
# registered unconditionally too. Split out of smtp-idna-check.py's
# verify_installed_python_paths(), which never touched the network in the
# first place; the rest of that script (full end-to-end SMTP routing)
# stays real-root/systemd-tied and unwired.
add_test(NAME idna-canonicalization
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/idna-canonicalization-check.py")
# Wires contrib/testing's live/integration test scripts into ctest, driven
# by an isolated, disposable Bongo instance -- see live-instance-fixture.py
# and the BONGO_CTEST_LIVE_INSTANCE option in the top-level CMakeLists.txt.
@@ -0,0 +1,112 @@
#!/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 Bongo's Python-side IDNA2008 (non-transitional) domain
canonicalization, with no live instance or build required -- pure source-
tree Python, the same as c-literal-length-check.py. Split out of
smtp-idna-check.py's verify_installed_python_paths(): the rest of that
script drives full end-to-end SMTP routing (still real-root/systemd-tied,
see smtp-outbound-opportunistic-tls-check.py's docstring for why), but
this specific check never touched the network in the first place.
RFC 8264/UTS #46 non-transitional processing means "faß" (German sharp s)
canonicalizes to "xn--fa-hia" (an actual IDNA2008 A-label), not the
transitional "fass" a pre-2008 implementation would produce -- confirmed
against dns.domain_to_ascii() and every config field
canonicalize_domains() touches, plus bongo_web.compose.build_message()'s
outbound recipient normalization.
"""
from __future__ import annotations
import copy
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(REPO_ROOT / "src" / "libs" / "python"))
sys.path.insert(0, str(REPO_ROOT / "src" / "www"))
LOCAL_UNICODE_DOMAIN = "bücher.smtp26.test"
LOCAL_ASCII_DOMAIN = "xn--bcher-kva.smtp26.test"
REMOTE_UNICODE_DOMAIN = "faß.smtp26.test"
REMOTE_ASCII_DOMAIN = "xn--fa-hia.smtp26.test"
RELAY_UNICODE_HOST = "relay.bücher.smtp26.test"
RELAY_ASCII_HOST = "relay.xn--bcher-kva.smtp26.test"
class IdnaCheckError(RuntimeError):
"""Raised when Bongo's Python IDNA canonicalization is not IDNA2008."""
def main() -> int:
from bongo.configuration.model import canonicalize_domains
from bongo.domain import domain_to_ascii
from bongo_web.compose import build_message
if domain_to_ascii("faß.de") != "xn--fa-hia.de":
raise IdnaCheckError(
"domain_to_ascii used transitional IDNA mapping "
f"(got {domain_to_ascii('faß.de')!r}, expected 'xn--fa-hia.de')"
)
configurations = {
"global": {"hostname": f"mail.{LOCAL_UNICODE_DOMAIN}"},
"queue": {
"domains": [LOCAL_UNICODE_DOMAIN],
"hosteddomains": [LOCAL_UNICODE_DOMAIN],
},
"smtp": {
"relay_host": RELAY_UNICODE_HOST,
"lmtp_transports": [
f"{LOCAL_UNICODE_DOMAIN}=lmtp.{LOCAL_UNICODE_DOMAIN}:24"
],
},
"web": {},
}
normalized = canonicalize_domains(copy.deepcopy(configurations))
if normalized["global"]["hostname"] != f"mail.{LOCAL_ASCII_DOMAIN}":
raise IdnaCheckError(
f"global hostname not canonicalized: {normalized['global']['hostname']!r}"
)
if normalized["queue"]["domains"] != [LOCAL_ASCII_DOMAIN]:
raise IdnaCheckError(
f"hosted domain not canonicalized: {normalized['queue']['domains']!r}"
)
if normalized["smtp"]["relay_host"] != RELAY_ASCII_HOST:
raise IdnaCheckError(
f"relay host not canonicalized: {normalized['smtp']['relay_host']!r}"
)
expected_lmtp = f"{LOCAL_ASCII_DOMAIN}=lmtp.{LOCAL_ASCII_DOMAIN}:24"
if normalized["smtp"]["lmtp_transports"] != [expected_lmtp]:
raise IdnaCheckError(
"LMTP transport not canonicalized: "
f"{normalized['smtp']['lmtp_transports']!r}"
)
_message, recipients = build_message(
"Sender@bücher.example",
{"to": f"Capture@{REMOTE_UNICODE_DOMAIN}", "text": "idna-canonicalization-check"},
)
if recipients != [f"Capture@{REMOTE_ASCII_DOMAIN}"]:
raise IdnaCheckError(
f"Web compose did not use the canonical A-label: {recipients!r}"
)
print(
"IDNA-CANONICALIZATION PASS "
"idna=idna2008/nontransitional "
"config=hostname/hosted/relay/lmtp "
"web-compose=alabel"
)
return 0
if __name__ == "__main__":
try:
raise SystemExit(main())
except IdnaCheckError as error:
print(f"idna-canonicalization-check: {error}", file=sys.stderr)
raise SystemExit(1)