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]

Reply via email to