diff --git a/server/mergin/auth/forms.py b/server/mergin/auth/forms.py index b45638eb..f7403bb8 100644 --- a/server/mergin/auth/forms.py +++ b/server/mergin/auth/forms.py @@ -1,6 +1,7 @@ # Copyright (C) Lutra Consulting Limited # # SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-MerginMaps-Commercial + import re import safe from flask_wtf import FlaskForm @@ -48,18 +49,22 @@ class ExtendedEmail(Email): 1. spaces, 2. special characters ,:;()<>[]\" 3, multiple @ symbols, - 4, leading, trailing, or consecutive dots in the local part - 5, invalid domain part - missing top level domain (user@example), consecutive dots - Custom check for additional invalid characters disallows |'— because they make our email sending service to fail + 4, leading, trailing, or consecutive dots in the local part, + 5, invalid domain part - missing top level domain (user@example), consecutive dots, + The extended validation checks email addresses using the regex provided by Brevo, + so that we stay consistent with their validation rules and avoid API failures. """ def __call__(self, form, field): super().__call__(form, field) - if re.search(r"[|'—]", field.data): - raise ValidationError( - f"Email address '{field.data}' contains an invalid character." - ) + email = field.data.strip() + + pattern = r"^[\x60#&*\/=?^{!}~'+\w-]+(\.[\x60#&*\/=?^{!}~'+\w-]+)*\.?@([_a-zA-Z0-9-]+(\.[_a-zA-Z0-9-]+)*\.)[a-zA-Z0-9-]*[a-zA-Z0-9]{2,}$" + email_regexp = re.compile(pattern, re.IGNORECASE) + + if not email_regexp.match(email): + raise ValidationError(f"Email address '{email}' is invalid.") class PasswordValidator: diff --git a/server/mergin/tests/test_auth.py b/server/mergin/tests/test_auth.py index d53b01bc..69ba37c6 100644 --- a/server/mergin/tests/test_auth.py +++ b/server/mergin/tests/test_auth.py @@ -125,9 +125,14 @@ def test_logout(client): 400, ), # tests with upper case, but email already exists (" mergin@mergin.com ", "#pwd123", 400), # invalid password - ("verylonglonglonglonglonglonglongemail@example.com", "#pwd1234", 201), + ( + "verylonglonglonglonglonglonglongemail@lutra-consulting.co.uk", + "#pwd1234", + 201, + ), # long local part, second-level domain, dash in domain ("us.er@mergin.com", "#pwd1234", 201), # dot is allowed ("us er@mergin.com", "#pwd1234", 400), # space is disallowed + ("test@gmaiñ.com", "#pwd1234", 400), # non-ASCII character in the domain ] @@ -936,15 +941,16 @@ def test_server_usage(client): ("日人日本人", True), # non-ascii character ("usér", True), # non-ascii character ("user\\", False), # disallowed character - ("user\260", True), # non-ascii character (°) + ("user\260", False), # not letter character (°) ("user|", False), # vertical bar ("us er", False), # space in the middle ("us,er", False), # comma ("us—er", False), # dash - ("us'er", False), # apostrophe + ("us´er", False), # acute accent (" user", True), # starting with space (will be stripped) ("us.er", True), # dot in the middle (".user", False), # starting with dot + ("us-er", True), # hyphen ]