4,410 bytes · the Python implementation · view raw
from typing import Optional
# Every character class is hand-rolled rather than expressed as a regular# expression. The Rust sibling has no regex crate available, and the only way# to be sure three implementations agree on an edge case is for all three to# make the same decision in the same place.#: RFC 5321 caps the local part at 64 octets.
_MAX_LOCAL = 64#: RFC 5321 caps a forward path at 256 octets including the angle brackets.
_MAX_TOTAL = 254#: RFC 1035 caps a DNS label at 63 octets.
_MAX_LABEL = 63#: The "atext" specials from RFC 5322, plus the dot handled separately below.
_LOCAL_SPECIALS = "!#$%&'*+-/=?^_`{|}~"def _is_digit(ch: str) -> bool:
return"0" <= ch <= "9"def _is_letter(ch: str) -> bool:
return ("a" <= ch <= "z") or ("A" <= ch <= "Z")
def _is_letter_or_digit(ch: str) -> bool:
return _is_letter(ch) or _is_digit(ch)
def _is_local_char(ch: str) -> bool:
return _is_letter_or_digit(ch) or ch in _LOCAL_SPECIALS
def is_email(value: str) -> bool:
"""Is this a plausible email address? This is a deliberate, documented subset of RFC 5322, not an implementation of it. The full grammar admits comments, folded whitespace, quoted strings with embedded spaces and bracketed IP literals; almost nothing downstream of a signup form can handle those, and accepting them would let addresses through that the mail stack then rejects. The only true validation of an email address is sending mail to it and seeing the recipient act on it. Use this to catch typos at the keyboard, then confirm by email. Never use it to decide that an address is real. """ifnot isinstance(value, str):
returnFalseif len(value) == 0or len(value) > _MAX_TOTAL:
returnFalse# Exactly one @: the last-@ split used by lenient parsers quietly accepts# "a@b@c", which no MTA will route.
at = -1for i, ch in enumerate(value):
if ch == "@":
if at != -1:
returnFalse
at = i
if at <= 0or at == len(value) - 1:
returnFalsereturn _is_local_part(value[:at]) and _is_domain(value[at + 1 :])
def _is_local_part(local: str) -> bool:
if len(local) == 0or len(local) > _MAX_LOCAL:
returnFalse# A dot is a separator between atoms, so it cannot lead, trail or double up.if local[0] == "."or local[-1] == ".":
returnFalsefor i, ch in enumerate(local):
if ch == ".":
if local[i - 1] == ".":
returnFalsecontinueifnot _is_local_char(ch):
returnFalsereturnTruedef _is_domain(domain: str) -> bool:
# The total-length cap already bounds this, but stating the domain limit# separately keeps the rule readable and survives a change to the total.if len(domain) == 0or len(domain) > _MAX_TOTAL - 2:
returnFalse
labels = domain.split(".")
# At least one dot. A bare "localhost" is a valid host but not an address# anyone outside that machine can deliver to, and a signup form is asking# for the latter.if len(labels) < 2:
returnFalsefor label in labels:
if len(label) == 0or len(label) > _MAX_LABEL:
returnFalseif label[0] == "-"or label[-1] == "-":
returnFalsefor ch in label:
ifnot _is_letter_or_digit(ch) and ch != "-":
returnFalse# The top-level label must be two or more letters. This is what rejects# "user@example.123" and the bracketed-IP form, and it is the rule most# likely to need revisiting: it also rejects punycode-free internationalised# TLDs written in their native script.
tld = labels[-1]
if len(tld) < 2:
returnFalsefor ch in tld:
ifnot _is_letter(ch):
returnFalsereturnTruedef email_domain(value: str) -> Optional[str]:
"""The domain half of an address, lowercased, or None if the address is not one this capability accepts. Domains are case-insensitive; local parts are not, so this deliberately only normalises the half where doing so is safe. """ifnot is_email(value):
returnNone
domain = value[value.index("@") + 1 :]
out = []
for ch in domain:
out.append(chr(ord(ch) + 32) if"A" <= ch <= "Z"else ch)
return"".join(out)