diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py index 3b1b13f9d0..3765180876 100644 --- a/src/pretix/base/services/mail.py +++ b/src/pretix/base/services/mail.py @@ -54,7 +54,7 @@ from celery.exceptions import MaxRetriesExceededError from django.conf import settings from django.core.files.storage import default_storage from django.core.mail import EmailMultiAlternatives, SafeMIMEMultipart -from django.core.mail.message import SafeMIMEText +from django.core.mail.message import SafeMIMEText, utf8_charset_qp from django.db import connection, transaction from django.db.models import Q from django.dispatch import receiver @@ -380,6 +380,8 @@ def mail(email: Union[str, Sequence[str]], subject: Union[str, FormattedString], class CustomEmail(EmailMultiAlternatives): + encoding = utf8_charset_qp + def _create_mime_attachment(self, content, mimetype): """ Convert the content, mimetype pair into a MIME attachment object. @@ -449,9 +451,9 @@ def mail_send_task(self, **kwargs) -> bool: # Rewrite all tags from real URLs or data URLs to inline attachments referred to by content ID if outgoing_mail.body_html is not None: - html_message = SafeMIMEMultipart(_subtype='related', encoding=settings.DEFAULT_CHARSET) + html_message = SafeMIMEMultipart(_subtype='related') html_with_cid, cid_images = replace_images_with_cid_paths(outgoing_mail.body_html) - html_message.attach(SafeMIMEText(html_with_cid, 'html', settings.DEFAULT_CHARSET)) + html_message.attach(SafeMIMEText(html_with_cid, 'html', utf8_charset_qp)) attach_cid_images(html_message, cid_images, verify_ssl=True) email.attach_alternative(html_message, "multipart/related") diff --git a/src/tests/base/test_mail.py b/src/tests/base/test_mail.py index 9a5a9e66a4..e685fdedca 100644 --- a/src/tests/base/test_mail.py +++ b/src/tests/base/test_mail.py @@ -34,6 +34,7 @@ import datetime import os +import quopri import re import socket from contextlib import contextmanager @@ -316,8 +317,10 @@ def _extract_html(mail): if "multipart/related" in mimetype: for sp in content._payload: if isinstance(sp, MIMEText): - return sp._payload - break + if sp.get('content-transfer-encoding').startswith('quoted-printable'): + return quopri.decodestring(sp.get_payload()).decode() + else: + return sp.get_payload() elif "text/html" in mimetype: return content @@ -711,3 +714,29 @@ def test_send_mail_private_ip(res, use_ssl, allow_private_networks, env): assert m.status == OutgoingMail.STATUS_SENT else: assert m.status == OutgoingMail.STATUS_FAILED + + +@pytest.mark.django_db +def test_mail_encoding(env, mailoutbox): + m = OutgoingMail.objects.create( + to=['recipient@example.com'], + subject='Test', + body_plain='Test öäü testtesttest', + body_html='Test ÖÄÜ', + sender='sender@example.com', + ) + assert m.status == OutgoingMail.STATUS_QUEUED + mail_send_task.apply(kwargs={ + 'outgoing_mail': m.pk, + }, max_retries=0) + m.refresh_from_db() + assert m.status == OutgoingMail.STATUS_SENT + + assert len(mailoutbox) == 1 + mail_plain = mailoutbox[0].message().as_string() + + assert "ä" not in mail_plain + assert "Ä" not in mail_plain + assert "Test =C3=B6=C3=A4=C3=BC" in mail_plain + assert "Test =C3=96=C3=84=C3=9C" in mail_plain + assert max(len(x) for x in mail_plain.splitlines()) <= 76