1"""eMail."""
2
3# standard
4import re
5
6# local
7from .hostname import hostname
8from .utils import validator
9
10
11@validator
12def email(
13 value: str,
14 /,
15 *,
16 ipv6_address: bool = False,
17 ipv4_address: bool = False,
18 simple_host: bool = False,
19 rfc_1034: bool = False,
20 rfc_2782: bool = False,
21):
22 """Validate an email address.
23
24 This was inspired from [Django's email validator][1].
25 Also ref: [RFC 1034][2], [RFC 5321][3] and [RFC 5322][4].
26
27 [1]: https://github.com/django/django/blob/main/django/core/validators.py#L174
28 [2]: https://www.rfc-editor.org/rfc/rfc1034
29 [3]: https://www.rfc-editor.org/rfc/rfc5321
30 [4]: https://www.rfc-editor.org/rfc/rfc5322
31
32 Examples:
33 >>> email('someone@example.com')
34 True
35 >>> email('bogus@@')
36 ValidationError(func=email, args={'value': 'bogus@@'})
37
38 Args:
39 value:
40 eMail string to validate.
41 ipv6_address:
42 When the domain part is an IPv6 address.
43 ipv4_address:
44 When the domain part is an IPv4 address.
45 simple_host:
46 When the domain part is a simple hostname.
47 rfc_1034:
48 Allow trailing dot in domain name.
49 Ref: [RFC 1034](https://www.rfc-editor.org/rfc/rfc1034).
50 rfc_2782:
51 Domain name is of type service record.
52 Ref: [RFC 2782](https://www.rfc-editor.org/rfc/rfc2782).
53
54 Returns:
55 (Literal[True]): If `value` is a valid eMail.
56 (ValidationError): If `value` is an invalid eMail.
57 """
58 if not value or value.count("@") != 1:
59 return False
60
61 username_part, domain_part = value.rsplit("@", 1)
62
63 if len(username_part) > 64 or len(domain_part) > 253:
64 # ref: RFC 1034 and 5231
65 return False
66
67 if ipv6_address or ipv4_address:
68 if domain_part.startswith("[") and domain_part.endswith("]"):
69 # ref: RFC 5321
70 domain_part = domain_part.lstrip("[").rstrip("]")
71 else:
72 return False
73
74 return (
75 bool(
76 hostname(
77 domain_part,
78 skip_ipv6_addr=not ipv6_address,
79 skip_ipv4_addr=not ipv4_address,
80 may_have_port=False,
81 maybe_simple=simple_host,
82 rfc_1034=rfc_1034,
83 rfc_2782=rfc_2782,
84 )
85 )
86 if re.match(
87 # extended latin
88 r"(^[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF]"
89 # dot-atom
90 + r"|[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF0-9a-z!#$%&'*+/=?^_`{}|~\-]+"
91 + r"(\.[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF0-9a-z!#$%&'*+/=?^_`{}|~\-]+)*$"
92 # quoted-string
93 + r'|^"('
94 + r"[\u0100-\u017F\u0180-\u024F\u00A0-\u00FF\001-\010\013\014\016-\037"
95 + r"!#-\[\]-\177]|\\[\011.]"
96 + r')*")$',
97 username_part,
98 re.IGNORECASE,
99 )
100 else False
101 )