fix: SMTP envelope recipients split on commas inside display names (#1464)
This commit is contained in:
@@ -322,6 +322,20 @@ def _apply_odysseus_headers(msg, kind: str | None = None, ref_id: str | None = N
|
|||||||
msg["X-Odysseus-Ref"] = re.sub(r"[^A-Za-z0-9_.:-]", "-", ref_id)[:128]
|
msg["X-Odysseus-Ref"] = re.sub(r"[^A-Za-z0-9_.:-]", "-", ref_id)[:128]
|
||||||
|
|
||||||
|
|
||||||
|
def _envelope_recipients(*fields: str) -> list:
|
||||||
|
"""Extract bare SMTP envelope addresses from one or more To/Cc/Bcc header
|
||||||
|
strings. A naive `field.split(",")` corrupts display names that contain a
|
||||||
|
comma (e.g. `"Smith, John" <john@corp.com>`, the canonical Outlook form):
|
||||||
|
it splits into `"Smith` and `John" <john@corp.com>`, breaking delivery.
|
||||||
|
email.utils.getaddresses parses the address grammar correctly."""
|
||||||
|
out = []
|
||||||
|
for _name, addr in email.utils.getaddresses([f for f in fields if f]):
|
||||||
|
addr = (addr or "").strip()
|
||||||
|
if addr:
|
||||||
|
out.append(addr)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def _md_to_email_html(text: str) -> str:
|
def _md_to_email_html(text: str) -> str:
|
||||||
"""Render the compose markdown body to a SAFE HTML fragment for the email's
|
"""Render the compose markdown body to a SAFE HTML fragment for the email's
|
||||||
text/html part. Everything is HTML-escaped FIRST (so a pasted <script> /
|
text/html part. Everything is HTML-escaped FIRST (so a pasted <script> /
|
||||||
@@ -1943,11 +1957,7 @@ def setup_email_routes():
|
|||||||
outer.attach(body_container)
|
outer.attach(body_container)
|
||||||
_attach_compose_uploads(outer, attachments)
|
_attach_compose_uploads(outer, attachments)
|
||||||
|
|
||||||
recipients = [r.strip() for r in to.split(",") if r.strip()]
|
recipients = _envelope_recipients(to, cc, bcc)
|
||||||
if cc:
|
|
||||||
recipients.extend([r.strip() for r in cc.split(",") if r.strip()])
|
|
||||||
if bcc:
|
|
||||||
recipients.extend([r.strip() for r in bcc.split(",") if r.strip()])
|
|
||||||
|
|
||||||
_send_smtp_message(cfg, cfg["from_address"], recipients, outer.as_string())
|
_send_smtp_message(cfg, cfg["from_address"], recipients, outer.as_string())
|
||||||
|
|
||||||
@@ -2161,12 +2171,9 @@ def setup_email_routes():
|
|||||||
outer.attach(body_container)
|
outer.attach(body_container)
|
||||||
_attach_compose_uploads(outer, req.attachments)
|
_attach_compose_uploads(outer, req.attachments)
|
||||||
|
|
||||||
# Build recipient list
|
# Build recipient list (parse the address grammar so display names with
|
||||||
recipients = [r.strip() for r in req.to.split(",") if r.strip()]
|
# commas don't get split into broken envelope addresses)
|
||||||
if req.cc:
|
recipients = _envelope_recipients(req.to, req.cc, req.bcc)
|
||||||
recipients.extend([r.strip() for r in req.cc.split(",") if r.strip()])
|
|
||||||
if req.bcc:
|
|
||||||
recipients.extend([r.strip() for r in req.bcc.split(",") if r.strip()])
|
|
||||||
|
|
||||||
# Serialize what the background task needs so the request object can be GC'd
|
# Serialize what the background task needs so the request object can be GC'd
|
||||||
outer_bytes = outer.as_bytes()
|
outer_bytes = outer.as_bytes()
|
||||||
|
|||||||
26
tests/test_email_envelope_recipients.py
Normal file
26
tests/test_email_envelope_recipients.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"""Regression: SMTP envelope recipients must be parsed, not split on bare commas.
|
||||||
|
|
||||||
|
The send paths built the RCPT TO list with `field.split(",")`, which corrupts a
|
||||||
|
display name containing a comma (e.g. `"Smith, John" <john@corp.com>`, the common
|
||||||
|
Outlook / corporate address-book form): it splits into `"Smith` and
|
||||||
|
`John" <john@corp.com>`, so the broken fragments are handed to smtp.sendmail and
|
||||||
|
delivery fails. `_envelope_recipients` uses email.utils.getaddresses instead.
|
||||||
|
"""
|
||||||
|
import routes.email_routes as email_routes
|
||||||
|
|
||||||
|
|
||||||
|
def test_display_name_with_comma_yields_one_address():
|
||||||
|
assert email_routes._envelope_recipients('"Smith, John" <john@corp.com>') == ["john@corp.com"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_multiple_plain_addresses():
|
||||||
|
assert email_routes._envelope_recipients("a@x.com, b@y.com") == ["a@x.com", "b@y.com"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_to_cc_bcc_combined_and_none_safe():
|
||||||
|
got = email_routes._envelope_recipients('"Doe, Jane" <jane@x.com>, bob@y.com', None, "carol@z.com")
|
||||||
|
assert got == ["jane@x.com", "bob@y.com", "carol@z.com"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_and_none_fields():
|
||||||
|
assert email_routes._envelope_recipients("", None) == []
|
||||||
Reference in New Issue
Block a user