diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index cf211c75..ee9ed9aa 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -13,8 +13,8 @@ # limitations under the License. import math +import re import typing -from email.utils import parseaddr from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_address, ip_network from urllib import parse as urlparse @@ -23,6 +23,11 @@ from protovalidate.internal import string_format +# See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address +_email_regex = re.compile( + r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" +) + def _validate_hostname(host): if not host: @@ -49,23 +54,6 @@ def _validate_hostname(host): return not all_digits -def validate_email(addr): - parts = parseaddr(addr) - if addr != parts[1]: - return False - - addr = parts[1] - if len(addr) > 254: - return False - - parts = addr.split("@") - if len(parts) != 2: - return False - if len(parts[0]) > 64: - return False - return _validate_hostname(parts[1]) - - def validate_host_and_port(string: str, *, port_required: bool) -> bool: if not string: return False @@ -157,10 +145,19 @@ def is_ip_prefix(val: celtypes.Value, *args) -> celpy.Result: def is_email(string: celtypes.Value) -> celpy.Result: + """Returns true if the string is an email address, for example "foo@example.com". + + Conforms to the definition for a valid email address from the HTML standard. + Note that this standard willfully deviates from RFC 5322, which allows many + unexpected forms of email addresses and will easily match a typographical + error. + """ + if not isinstance(string, celtypes.StringType): msg = "invalid argument, expected string" raise celpy.CELEvalError(msg) - return celtypes.BoolType(validate_email(string)) + m = _email_regex.match(string) is not None + return celtypes.BoolType(m) def is_uri(string: celtypes.Value) -> celpy.Result: diff --git a/tests/conformance/nonconforming.yaml b/tests/conformance/nonconforming.yaml index 531cf553..9fc8ea2f 100644 --- a/tests/conformance/nonconforming.yaml +++ b/tests/conformance/nonconforming.yaml @@ -8,46 +8,6 @@ standard_constraints/well_known_types/timestamp: - gte_lte/invalid/above - lte/invalid -library/is_email: - - invalid/left_side_empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/non_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"ยต@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/quoted-string/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"\"foo bar\"@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/quoted-string/b - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"\"foo..bar\"@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/trailing_dot - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"foo@example.com."} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - valid/exhaust_atext - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'*+-/=?^_`{|}~@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # message: "" - # for_key: false - - valid/label_all_digits - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"foo@0.1.2.3.4.5.6.7.8.9"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # message: "" - # for_key: false library/is_host_and_port: - port_required/false/invalid/port_number_sign # input: [type.googleapis.com/buf.validate.conformance.cases.IsHostAndPort]:{val:"example.com:+0"}