emails: Truncate overly-long From fields for RFC compatibility.

Amazon SES has a limit on the size of address fields, and rejects
emails with too-long "From" combinations of name and address. This
limit is set to 320 bytes and comes from an RFC limitation on the
size of addresses. This RFC standard states that an email address
should not be composed of a local part (before the '@') longer than
64 bytes and a domain part (after the '@') longer than 255 bytes.
It is possible that Amazon SES misinterprets this limitation as it
checks the length of the combination of the name and the email
address of the sender.

To ensure that this problem is not encountered in the send_email
module of Zulip the length of this combination is now checked
against this limit and the from_name field is removed to only
keep the from_address field when it is necessary in order to
stay below 320 bytes.

If the from_address field alone is longer than 320 bytes the
sending process will raise an SMTPDataError exception.

Tests for this new check are added to the backend test suite in
order to test if build_email correctly outputs an email with filled
from_name and from_address fields when the total length is lower
than 320 bytes and that it correctly throws the from_name field
away when necessary.

Fixes: #17558.
This commit is contained in:
Cyril Pletinckx
2021-04-03 11:58:16 +02:00
committed by Tim Abbott
parent 4f38da5ce7
commit b7fa41601d
2 changed files with 68 additions and 1 deletions

View File

@@ -11,6 +11,7 @@ from typing import Any, Dict, List, Mapping, Optional, Tuple
import orjson
from django.conf import settings
from django.core.mail import EmailMultiAlternatives
from django.core.mail.message import sanitize_address
from django.core.management import CommandError
from django.db import transaction
from django.template import loader
@@ -148,8 +149,15 @@ def build_email(
if from_address == FromAddress.support_placeholder:
from_address = FromAddress.SUPPORT
# Set the "From" that is displayed separately from the envelope-from
# Set the "From" that is displayed separately from the envelope-from.
extra_headers["From"] = str(Address(display_name=from_name, addr_spec=from_address))
# Check ASCII encoding length. Amazon SES rejects emails with
# From names longer than 320 characters (which appears to be a
# misinterpretation of the RFC); in that case we drop the name
# from the From line, under the theory that it's better to send
# the email with a simplified From field than not.
if len(sanitize_address(extra_headers["From"], "utf-8")) > 320:
extra_headers["From"] = str(Address(addr_spec=from_address))
reply_to = None
if reply_to_email is not None:

View File

@@ -0,0 +1,59 @@
from django.core.mail.message import sanitize_address
from zerver.lib.send_email import FromAddress, build_email
from zerver.lib.test_classes import ZulipTestCase
OVERLY_LONG_NAME = "Z̷̧̙̯͙̠͇̰̲̞̙͆́͐̅̌͐̔͑̚u̷̼͎̹̻̻̣̞͈̙͛͑̽̉̾̀̅̌͜͠͞ļ̛̫̻̫̰̪̩̠̣̼̏̅́͌̊͞į̴̛̛̩̜̜͕̘̂̑̀̈p̡̛͈͖͓̟͍̿͒̍̽͐͆͂̀ͅ A̰͉̹̅̽̑̕͜͟͡c̷͚̙̘̦̞̫̭͗̋͋̾̑͆̒͟͞c̵̗̹̣̲͚̳̳̮͋̈́̾̉̂͝ͅo̠̣̻̭̰͐́͛̄̂̿̏͊u̴̱̜̯̭̞̠͋͛͐̍̄n̸̡̘̦͕͓̬͌̂̎͊͐̎͌̕ť̮͎̯͎̣̙̺͚̱̌̀́̔͢͝ S͇̯̯̙̳̝͆̊̀͒͛̕ę̛̘̬̺͎͎́̔̊̀͂̓̆̕͢ͅc̨͎̼̯̩̽͒̀̏̄̌̚u̷͉̗͕̼̮͎̬͓͋̃̀͂̈̂̈͊͛ř̶̡͔̺̱̹͓̺́̃̑̉͡͞ͅi̶̺̭͈̬̞̓̒̃͆̅̿̀̄́t͔̹̪͔̥̣̙̍̍̍̉̑̏͑́̌ͅŷ̧̗͈͚̥̗͚͊͑̀͢͜͡"
class TestBuildEmail(ZulipTestCase):
def test_build_SES_compatible_From_field(self) -> None:
hamlet = self.example_user("hamlet")
from_name = FromAddress.security_email_from_name(language="en")
mail = build_email(
"zerver/emails/password_reset",
to_emails=[hamlet],
from_name=from_name,
from_address=FromAddress.NOREPLY,
language="en",
)
self.assertEqual(
mail.extra_headers["From"], "{} <{}>".format(from_name, FromAddress.NOREPLY)
)
def test_build_SES_compatible_From_field_limit(self) -> None:
hamlet = self.example_user("hamlet")
limit_length_name = "a" * (320 - len(sanitize_address(FromAddress.NOREPLY, "utf-8")) - 3)
mail = build_email(
"zerver/emails/password_reset",
to_emails=[hamlet],
from_name=limit_length_name,
from_address=FromAddress.NOREPLY,
language="en",
)
self.assertEqual(
mail.extra_headers["From"], "{} <{}>".format(limit_length_name, FromAddress.NOREPLY)
)
def test_build_SES_incompatible_From_field(self) -> None:
hamlet = self.example_user("hamlet")
mail = build_email(
"zerver/emails/password_reset",
to_emails=[hamlet],
from_name=OVERLY_LONG_NAME,
from_address=FromAddress.NOREPLY,
language="en",
)
self.assertEqual(mail.extra_headers["From"], FromAddress.NOREPLY)
def test_build_SES_incompatible_From_field_limit(self) -> None:
hamlet = self.example_user("hamlet")
limit_length_name = "a" * (321 - len(sanitize_address(FromAddress.NOREPLY, "utf-8")) - 3)
mail = build_email(
"zerver/emails/password_reset",
to_emails=[hamlet],
from_name=limit_length_name,
from_address=FromAddress.NOREPLY,
language="en",
)
self.assertEqual(mail.extra_headers["From"], FromAddress.NOREPLY)