Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion fastapi_mail/fastmail.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ async def _template_message_builder(
async def _sender(self, message: MessageSchema) -> Union[EmailStr, str]:
sender = message.from_email or self.config.MAIL_FROM
if (from_name := message.from_name or self.config.MAIL_FROM_NAME) is not None:
return formataddr((from_name, sender))
return formataddr((from_name, sender), charset="utf-8")
return sender

async def send_message(
Expand Down
14 changes: 9 additions & 5 deletions fastapi_mail/msg.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@
from email.mime.base import MIMEBase
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formatdate, make_msgid
from email.utils import formataddr, formatdate, make_msgid
from typing import Any, Union

from .schemas import MessageType, MultipartSubtypeEnum

PY3 = sys.version_info[0] == 3


def _format_address(recipient: Any) -> str:
return formataddr((recipient.name, str(recipient.email)), charset="utf-8")


class MailMsg:
"""
Mail message parameters
Expand Down Expand Up @@ -144,21 +148,21 @@ async def _message(self, sender: str) -> Union[EmailMessage, Message]:

self.message["Date"] = formatdate(time.time(), localtime=True)
self.message["Message-ID"] = self.msgId
self.message["To"] = ", ".join(str(recipient) for recipient in self.recipients)
self.message["To"] = ", ".join(_format_address(r) for r in self.recipients)
self.message["From"] = sender

if self.subject:
self.message["Subject"] = self.subject

if self.cc:
self.message["Cc"] = ", ".join(str(recipient) for recipient in self.cc)
self.message["Cc"] = ", ".join(_format_address(r) for r in self.cc)

if self.bcc:
self.message["Bcc"] = ", ".join(str(recipient) for recipient in self.bcc)
self.message["Bcc"] = ", ".join(_format_address(r) for r in self.bcc)

if self.reply_to:
self.message["Reply-To"] = ", ".join(
str(recipient) for recipient in self.reply_to
_format_address(r) for r in self.reply_to
)

if self.attachments:
Expand Down
44 changes: 44 additions & 0 deletions tests/test_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -307,3 +307,47 @@ async def test_name_email_message_headers():
assert msg_object["Cc"] == "CC User <cc@example.com>"
assert msg_object["Bcc"] == "BCC User <bcc@example.com>"
assert msg_object["Reply-To"] == "Reply User <reply@example.com>"


@pytest.mark.asyncio
async def test_non_ascii_names_are_rfc2047_encoded():
"""Recipient display names with non-ASCII characters must be RFC 2047
encoded so the resulting headers are 7-bit-clean. Without encoding, the
raw UTF-8 reaches the SMTP server, which on some providers (e.g.
Office 365) strips it and rejects the message with
`550 5.2.254 InvalidRecipientsException`."""
from email.header import decode_header
from email.utils import getaddresses

message = MessageSchema(
subject="test subject",
recipients=["Lukas Böhm <lboehm@example.com>"],
cc=["René Méndez <rmendez@example.com>"],
bcc=["Straße Müller <strasse@example.com>"],
reply_to=["Renée Doe <reply@example.com>"],
body="test",
subtype=MessageType.plain,
)

msg = MailMsg(message)
msg_object = await msg._message("sender@example.com")

def _decode(header_value):
name, addr = getaddresses([header_value])[0]
parts = decode_header(name)
return (
"".join(
chunk.decode(charset or "ascii") if isinstance(chunk, bytes) else chunk
for chunk, charset in parts
),
addr,
)

for header in ("To", "Cc", "Bcc", "Reply-To"):
raw = msg_object[header].encode("ascii") # must not raise
assert b"=?utf-8?" in raw, f"{header} is not RFC 2047 encoded: {raw!r}"

assert _decode(msg_object["To"]) == ("Lukas Böhm", "lboehm@example.com")
assert _decode(msg_object["Cc"]) == ("René Méndez", "rmendez@example.com")
assert _decode(msg_object["Bcc"]) == ("Straße Müller", "strasse@example.com")
assert _decode(msg_object["Reply-To"]) == ("Renée Doe", "reply@example.com")