Merge pull request #35103 from Z4nzu/fix/email-validation-rfc5322-27337
feat(utils): replace custom email parsing with RFC-compliant validation
This commit is contained in:
commit
4751bc06d7
2 changed files with 61 additions and 31 deletions
|
|
@ -577,6 +577,39 @@ class TestValidationUtils(IntegrationTestCase):
|
|||
"erp+Job%20Applicant=JA00004@frappe.com",
|
||||
)
|
||||
|
||||
# RFC 5322 format - Display name with comma (main bug fix)
|
||||
self.assertEqual(
|
||||
validate_email_address('"Lastname, Firstname" <test@example.com>'), "test@example.com"
|
||||
)
|
||||
self.assertEqual(validate_email_address('"Doe, John" <john.doe@example.com>'), "john.doe@example.com")
|
||||
|
||||
# RFC 5322 format - Display name without comma
|
||||
self.assertEqual(validate_email_address("Test User <test@example.com>"), "test@example.com")
|
||||
|
||||
# RFC 5322 format - Multiple emails
|
||||
self.assertEqual(
|
||||
validate_email_address('"Last, First" <test1@example.com>, "Another, Name" <test2@example.com>'),
|
||||
"test1@example.com, test2@example.com",
|
||||
)
|
||||
|
||||
# RFC 5322 format - Mixed with plain emails
|
||||
self.assertEqual(
|
||||
validate_email_address("Test User <test@example.com>, plain@example.com"),
|
||||
"test@example.com, plain@example.com",
|
||||
)
|
||||
|
||||
# Emails with newlines
|
||||
self.assertEqual(
|
||||
validate_email_address("test1@example.com\ntest2@example.com"),
|
||||
"test1@example.com, test2@example.com",
|
||||
)
|
||||
|
||||
# Undisclosed recipients should be filtered
|
||||
self.assertEqual(validate_email_address("undisclosed-recipients:;"), "")
|
||||
self.assertEqual(
|
||||
validate_email_address("test@example.com, undisclosed-recipients:;"), "test@example.com"
|
||||
)
|
||||
|
||||
def test_valid_phone(self):
|
||||
valid_phones = ["+91 1234567890", ""]
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ from collections.abc import (
|
|||
Sequence,
|
||||
)
|
||||
from email.header import decode_header, make_header
|
||||
from email.utils import formataddr, parseaddr
|
||||
from email.utils import formataddr, getaddresses, parseaddr
|
||||
from typing import Any, Generic, TypeAlias, TypedDict
|
||||
|
||||
import orjson
|
||||
|
|
@ -180,45 +180,42 @@ def validate_name(name, throw=False):
|
|||
|
||||
def validate_email_address(email_str, throw=False):
|
||||
"""Validates the email string"""
|
||||
email = email_str = (email_str or "").strip()
|
||||
|
||||
def _check(e):
|
||||
_valid = True
|
||||
if not e:
|
||||
_valid = False
|
||||
email_str = (email_str or "").strip()
|
||||
out = []
|
||||
|
||||
if "undisclosed-recipient" in e:
|
||||
return False
|
||||
# Replace newlines with commas so getaddresses can handle them
|
||||
# getaddresses expects comma-separated values
|
||||
email_str = email_str.replace("\n", ",").replace("\r", ",")
|
||||
|
||||
elif " " in e and "<" not in e:
|
||||
# example: "test@example.com test2@example.com" will return "test@example.comtest2" after parseaddr!!!
|
||||
_valid = False
|
||||
# Parse using stdlib (handles commas in display names correctly)
|
||||
addresses = getaddresses([email_str])
|
||||
|
||||
else:
|
||||
email_id = extract_email_id(e)
|
||||
match = EMAIL_MATCH_PATTERN.match(email_id) if email_id else None
|
||||
|
||||
if not match:
|
||||
_valid = False
|
||||
|
||||
if not _valid:
|
||||
for name, addr in addresses:
|
||||
if not addr:
|
||||
if throw:
|
||||
invalid_email = frappe.utils.escape_html(e)
|
||||
frappe.throw(
|
||||
frappe._("{0} is not a valid Email Address").format(invalid_email),
|
||||
frappe._("{0} is not a valid Email Address").format(
|
||||
frappe.utils.escape_html(name or email_str)
|
||||
),
|
||||
frappe.InvalidEmailAddressError,
|
||||
)
|
||||
return None
|
||||
else:
|
||||
return email_id
|
||||
|
||||
out = []
|
||||
for e in email_str.split(","):
|
||||
if not e:
|
||||
continue
|
||||
email = _check(e.strip())
|
||||
if email:
|
||||
out.append(email)
|
||||
|
||||
# Skip undisclosed recipients
|
||||
if "undisclosed-recipient" in addr:
|
||||
continue
|
||||
|
||||
match = EMAIL_MATCH_PATTERN.match(addr)
|
||||
if not match:
|
||||
if throw:
|
||||
frappe.throw(
|
||||
frappe._("{0} is not a valid Email Address").format(frappe.utils.escape_html(addr)),
|
||||
frappe.InvalidEmailAddressError,
|
||||
)
|
||||
continue
|
||||
|
||||
out.append(addr)
|
||||
|
||||
return ", ".join(out)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue