Repository: pretix/pretix

Issue:
Use proper MIME structure
Since #1358, we've been sending emails with a MIME structure like this:

```
multipart/related
   multipart/alternative
      text/plain
      text/html
   image/png (images for HTML content)
   application/pdf (tickets)
   …
```

Unfortunately, this broke attachments on Apple devices entirely: iOS Mail no longer showed any attachments. As a quick fix, in ca0c0f4ae, I changed ``multipart/related`` to ``multipart/mixed``. This has the upside of showing all attachments again, but the downside of showing the PNG images as attachments as well.

The correct structure would look like this:

```
multipart/mixed
   multipart/alternative
      text/plain
      multipart/related
         text/html
         image/png (images for HTML content)
   application/pdf (tickets)
   …
```

I verified that this yields the correct results on iOS. What's left to do is changing our code to generate this structure and verify that it works as intended in at least GMail and Outlook.


Discussion:
@raphaelm , Can I take this issue ?
@Abhi-khandelwal Feel free! It will require some deep-diving into Django's email code, though, I'm afraid.

Resolving patch:
diff --git a/src/pretix/base/services/mail.py b/src/pretix/base/services/mail.py
--- a/src/pretix/base/services/mail.py
+++ b/src/pretix/base/services/mail.py
@@ -14,7 +14,10 @@
 from bs4 import BeautifulSoup
 from celery import chain
 from django.conf import settings
-from django.core.mail import EmailMultiAlternatives, get_connection
+from django.core.mail import (
+    EmailMultiAlternatives, SafeMIMEMultipart, get_connection,
+)
+from django.core.mail.message import SafeMIMEText
 from django.template.loader import get_template
 from django.utils.translation import pgettext, ugettext as _
 from django_scopes import scope, scopes_disabled
@@ -232,16 +235,32 @@ def mail(email: str, subject: str, template: Union[str, LazyI18nString],
         chain(*task_chain).apply_async()
 
 
+class CustomEmail(EmailMultiAlternatives):
+    def _create_mime_attachment(self, content, mimetype):
+        """
+        Convert the content, mimetype pair into a MIME attachment object.
+
+        If the mimetype is message/rfc822, content may be an
+        email.Message or EmailMessage object, as well as a str.
+        """
+        basetype, subtype = mimetype.split('/', 1)
+        if basetype == 'multipart' and isinstance(content, SafeMIMEMultipart):
+            return content
+        return super()._create_mime_attachment(content, mimetype)
+
+
 @app.task(base=TransactionAwareTask, bind=True)
 def mail_send_task(self, *args, to: List[str], subject: str, body: str, html: str, sender: str,
                    event: int=None, position: int=None, headers: dict=None, bcc: List[str]=None,
                    invoices: List[int]=None, order: int=None, attach_tickets=False, user=None,
                    attach_ical=False) -> bool:
-    email = EmailMultiAlternatives(subject, body, sender, to=to, bcc=bcc, headers=headers)
+    email = CustomEmail(subject, body, sender, to=to, bcc=bcc, headers=headers)
     if html is not None:
+        html_message = SafeMIMEMultipart(_subtype='related', encoding=settings.DEFAULT_CHARSET)
         html_with_cid, cid_images = replace_images_with_cid_paths(html)
-        email = attach_cid_images(email, cid_images, verify_ssl=True)
-        email.attach_alternative(html_with_cid, "text/html")
+        html_message.attach(SafeMIMEText(html_with_cid, 'html', settings.DEFAULT_CHARSET))
+        attach_cid_images(html_message, cid_images, verify_ssl=True)
+        email.attach_alternative(html_message, "multipart/related")
 
     if user:
         user = User.objects.get(pk=user)
@@ -409,8 +428,6 @@ def attach_cid_images(msg, cid_images, verify_ssl=True):
             except:
                 logger.exception("ERROR attaching CID image %s[%s]" % (cid, image))
 
-    return msg
-
 
 def encoder_linelength(msg):
     """


Test patch: