This is an automated email from the ASF dual-hosted git repository. sbp pushed a commit to branch sbp in repository https://gitbox.apache.org/repos/asf/tooling-trusted-releases.git
commit 7ff06690a990a05e2757f8166e0ed14955078e19 Author: Sean B. Palmer <[email protected]> AuthorDate: Wed Mar 11 17:51:41 2026 +0000 Fix and improve email validation --- atr/admin/__init__.py | 4 +++- atr/form.py | 12 ++++++++++++ atr/tasks/message.py | 8 +++++--- tests/unit/test_message.py | 3 ++- 4 files changed, 22 insertions(+), 5 deletions(-) diff --git a/atr/admin/__init__.py b/atr/admin/__init__.py index 3fe6e7dc..7538287f 100644 --- a/atr/admin/__init__.py +++ b/atr/admin/__init__.py @@ -89,7 +89,9 @@ class DeleteReleaseForm(form.Form): class LdapLookupForm(form.Form): uid: str = form.label("ASF UID (optional)", "Enter ASF UID, e.g. johnsmith, or * for all") - email: str = form.label("Email address (optional)", "Enter email address, e.g. [email protected]") + email: form.OptionalEmail = form.label( + "Email address (optional)", "Enter email address, e.g. [email protected]", widget=form.Widget.EMAIL + ) class RevokeUserTokensForm(form.Form): diff --git a/atr/form.py b/atr/form.py index 2ddcca51..8463512e 100644 --- a/atr/form.py +++ b/atr/form.py @@ -504,6 +504,18 @@ Bool = Annotated[ Email = pydantic.EmailStr +def _empty_to_none(v: object) -> object: + if isinstance(v, str) and (not v): + return None + return v + + +OptionalEmail = Annotated[ + pydantic.EmailStr | None, + functional_validators.BeforeValidator(_empty_to_none), +] + + class Enum[EnumType: enum.Enum]: # These exist for type checkers - at runtime, the actual type is the enum name: str diff --git a/atr/tasks/message.py b/atr/tasks/message.py index 17b03145..2cf33f04 100644 --- a/atr/tasks/message.py +++ b/atr/tasks/message.py @@ -15,6 +15,8 @@ # specific language governing permissions and limitations # under the License. +import pydantic + import atr.ldap as ldap import atr.log as log import atr.mail as mail @@ -27,8 +29,8 @@ import atr.tasks.checks as checks class Send(schema.Strict): """Arguments for the task to send an email.""" - email_sender: str = schema.description("The email address of the sender") - email_recipient: str = schema.description("The email address of the recipient") + email_sender: pydantic.EmailStr = schema.description("The email address of the sender") + email_recipient: pydantic.EmailStr = schema.description("The email address of the recipient") subject: str = schema.description("The subject of the email") body: str = schema.description("The body of the email") in_reply_to: str | None = schema.description("The message ID of the email to reply to") @@ -55,7 +57,7 @@ async def send(args: Send) -> results.Results | None: raise SendError(f"Email account {args.email_sender} is banned") recipient_domain = args.email_recipient.split("@")[-1] - sending_to_self = recipient_domain == f"{sender_asf_uid}@apache.org" + sending_to_self = args.email_recipient == f"{sender_asf_uid}@apache.org" # audit_guidance this application intentionally allows users to send messages to committees they are not a part of sending_to_committee = recipient_domain.endswith(".apache.org") if not (sending_to_self or sending_to_committee): diff --git a/tests/unit/test_message.py b/tests/unit/test_message.py index 048d4538..6f742dac 100644 --- a/tests/unit/test_message.py +++ b/tests/unit/test_message.py @@ -21,6 +21,7 @@ import contextlib import unittest.mock as mock from typing import TYPE_CHECKING +import pydantic import pytest import atr.ldap as ldap @@ -54,7 +55,7 @@ async def test_send_rejects_bare_invalid_asf_id(monkeypatch: "MonkeyPatch") -> N """Test that a bare ASF UID (no @) not found in LDAP raises SendError.""" monkeypatch.setattr("atr.tasks.message.ldap.account_lookup", mock.AsyncMock(return_value=None)) - with pytest.raises(message.SendError, match=r"Invalid email account"): + with pytest.raises(pydantic.ValidationError, match=r"not a valid email address"): await message.send(_send_args(email_sender="nosuchuser")) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
