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:
@@ -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)
|
||||
Reference in New Issue
Block a user