"""Internationalization primitives for the autonomous engine. Covers the two i18n hard requirements from the architecture spec: 7.1 Punycode / IDN domains — a Unicode domain (e.g. bücher.example) must be normalized to Punycode (RFC 3492, "xn--…") before MX resolution and SMTP probing. The `email-validator` library already does this at Layer 1, but every other layer (DoH resolver, DNS cache keys, SMTP dialogue) must ALSO receive the ASCII form, so the conversion lives here as the single source. 7.2 EAI / SMTPUTF8 local parts — a non-ASCII local part (utf8@bücher.example) can only be delivered when the receiving MX advertises SMTPUTF8. Without an SMTP dialogue we cannot observe the advertisement, so the engine must NOT claim a definitive verdict for these addresses: it refuses the probe deterministically and reports `unsupported_eai` (honest unknown, never a fake invalid). """ from __future__ import annotations import idna EAI_REASON = "unsupported_eai" def to_punycode(domain: str) -> str: """Return the ASCII (Punycode) form of a domain, best-effort. Already-ASCII domains pass through unchanged (fast path). A domain that cannot be encoded (malformed Unicode, over-long label) is returned as-is — DNS will then fail naturally and the verdict degrades honestly. """ if not domain: return domain try: domain.encode("ascii") return domain # pure ASCII — nothing to do except UnicodeEncodeError: pass try: return idna.encode(domain, uts46=True).decode("ascii") except Exception: # Last resort: Python's builtin IDNA (RFC 3490 without uts46 mapping) try: return domain.encode("idna").decode("ascii") except Exception: return domain def domain_is_unicode(domain: str) -> bool: """True when the domain needs Punycode (contains non-ASCII labels).""" try: domain.encode("ascii") return False except UnicodeEncodeError: return True def local_part_is_ascii(local_part: str) -> bool: """True when the local part is plain ASCII (no EAI/SMTPUTF8 needed).""" try: (local_part or "").encode("ascii") return True except UnicodeEncodeError: return False def eai_gate(local_part: str) -> str | None: """Deterministic EAI gate for the mailbox-probe layers. Returns None when the local part is ASCII and probing may proceed. Returns the machine reason ("unsupported_eai") when the address requires SMTPUTF8 and this deployment cannot honestly verify it. """ if local_part_is_ascii(local_part): return None return EAI_REASON