diff --git a/newsfragments/3555.bugfix.rst b/newsfragments/3555.bugfix.rst new file mode 100644 index 00000000000..aa35edf2b34 --- /dev/null +++ b/newsfragments/3555.bugfix.rst @@ -0,0 +1 @@ +Fix email validation in the application for some special cases diff --git a/parsec/core/gui/validators.py b/parsec/core/gui/validators.py index b763d93bdb6..fddeb3242bf 100644 --- a/parsec/core/gui/validators.py +++ b/parsec/core/gui/validators.py @@ -113,16 +113,35 @@ def validate(self, string: str, pos: int) -> tuple[QValidator.State, str, int]: # HumanHandle raises the same ValueError if either email or label are incorrect. # We trick it by using an email we know will be valid, so that the only ValueError # that can be raised will be because of an incorrect label. - HumanHandle(email="a@b.c", label=string) + HumanHandle(email="local@domain.com", label=string) return QValidator.Acceptable, string, pos except ValueError: return QValidator.Invalid, string, pos class EmailValidator(QRegularExpressionValidator): - # We don't use the HumanHandle to validate the email because it's way too permissive. def __init__(self) -> None: - super().__init__(QRegularExpression(r"^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$")) + # Note: this regex is very approximative, but it's used for two reasons: + # - to get a sensible `QValidator.Intermediate` status + # - to ban weird-but-valid email addresses such as `example@example.com#` and `a@b.c` + # However, it might not be able to accurately detect invalid email addresses. + # For instance, it will falsely report `example@-example.com` as a valid email. + # It will also report email with excessively long domain names as valid, although they are not. + # Ultimately, the actual validation is performed by ANDing the results of the regex + # and the `HumanHandle` constructor, which in turn uses the `email_address_parser` crate. + email_regex = r"^([a-zA-Z0-9_%+-]+\.)*([a-zA-Z0-9_%+-]+)@([a-zA-Z0-9-]+\.)+([a-zA-Z]{2,})$" + super().__init__(QRegularExpression(email_regex)) + + def validate(self, input: str, pos: int) -> tuple[QValidator.State, str, int]: + status, string, pos = super().validate(input, pos) + if status != QValidator.Acceptable: + return status, string, pos + try: + HumanHandle(email=input, label="Some Label") + except ValueError: + return QValidator.Invalid, string, pos + else: + return QValidator.Acceptable, string, pos class WorkspaceNameValidator(QValidator): diff --git a/tests/core/gui/test_validators.py b/tests/core/gui/test_validators.py index a896ae2b1de..216b3714123 100644 --- a/tests/core/gui/test_validators.py +++ b/tests/core/gui/test_validators.py @@ -49,6 +49,22 @@ def test_email_validator(qtbot, core_config): assert not le.is_input_valid() assert le.property("validity") == QtGui.QValidator.Invalid + le.clear() + qtbot.keyClicks(le, "example.") + qtbot.wait_until(lambda: le.text() == "example.") + assert not le.is_input_valid() + assert le.property("validity") == QtGui.QValidator.Intermediate + + qtbot.keyClicks(le, "@") + qtbot.wait_until(lambda: le.text() == "example.@") + assert not le.is_input_valid() + assert le.property("validity") == QtGui.QValidator.Invalid + + qtbot.keyClicks(le, "example.com") + qtbot.wait_until(lambda: le.text() == "example.@example.com") + assert not le.is_input_valid() + assert le.property("validity") == QtGui.QValidator.Invalid + @pytest.mark.gui def test_organization_validator(qtbot, core_config):