Search code examples
pythondjangoemailmime

Create HTML Mail with inline Image and PDF Attachment


I want to write a HTML mail in Python/Django containing these parts:

  • HTML linking to logo.png
  • logo.png which should be displayed inline (not as attachment) in the mail user agent
  • info.pdf which should be displayed as attachment
  • Text which should be displayed if the mail user agent can't display HTML.

I followed this blog post.

Result:

  • The HTML and the inline image works
  • but the info.pdf file gets treated like the inline logo.png, and some mail user agent don't show it :-(

How to create both ways (download (info.pdf) and inline (logo.png)) in one mail in python?


Solution

  • I reverse engineered that this structure gets used in practice:

    +-------------------------------------------------------+
    | multipart/mixed                                       |
    |                                                       |
    |  +-------------------------------------------------+  |
    |  |   multipart/related                             |  |
    |  |                                                 |  |
    |  |  +-------------------------------------------+  |  |
    |  |  | multipart/alternative                     |  |  |
    |  |  |                                           |  |  |
    |  |  |  +-------------------------------------+  |  |  |
    |  |  |  | text can contain [cid:logo.png]     |  |  |  |
    |  |  |  +-------------------------------------+  |  |  |
    |  |  |                                           |  |  |
    |  |  |  +-------------------------------------+  |  |  |
    |  |  |  | html can contain src="cid:logo.png" |  |  |  |
    |  |  |  +-------------------------------------+  |  |  |
    |  |  |                                           |  |  |
    |  |  +-------------------------------------------+  |  |
    |  |                                                 |  |
    |  |  +-------------------------------------------+  |  |
    |  |  | image logo.png  "inline" attachment       |  |  |
    |  |  +-------------------------------------------+  |  |
    |  |                                                 |  |
    |  +-------------------------------------------------+  |
    |                                                       |
    |  +-------------------------------------------------+  |
    |  | pdf ("download" attachment, not inline)         |  |
    |  +-------------------------------------------------+  |
    |                                                       |
    +-------------------------------------------------------+
    

    Unfortunately I only found this complicated solution:

    from django.core.mail.message import EmailMessage
    
    
    def create_email(subject='', body='', from_email=None, to=None, bcc=None,
                     connection=None, attachments=[], headers=None,
                     cc=None, reply_to=None, html_body='', html_inline_attachments=[]):
        message = _create_email(subject=subject, body=body, from_email=from_email, to=to, bcc=bcc,
                                connection=connection, headers=headers, cc=cc, reply_to=reply_to,
                                html_body=html_body, html_inline_attachments=html_inline_attachments)
    
        for attachment in attachments:
            if isinstance(attachment, basestring):
                message.attach_file(attachment)
                continue
            message.attach(attachment)
    
        return message
    
    
    def _create_email(subject='', body='', from_email=None, to=None, bcc=None,
                      connection=None, headers=None,
                      cc=None, reply_to=None, html_body='', html_inline_attachments=[]):
        if not (body or html_body):
            raise ValueError('Missing body or html_body!')
    
        for address, type, name in [
            (from_email, basestring, 'from_email'),
            (to, list, 'to'),
            (cc, list, 'cc'),
            (bcc, list, 'bcc')]:
            if address and not isinstance(address, type):
                raise ValueError('"{}" must be a list! ({})'.format(name, address))
    
        if body and not html_body:
            if html_inline_attachments:
                raise ValueError('"html_body" is missing!')
            return EmailMessage(subject=subject, body=body, from_email=from_email, to=to, bcc=bcc,
                                connection=connection, headers=headers, cc=cc,
                                reply_to=reply_to)
    
        if not body:
            body = html_to_text(html_body)
        msg = EmailMessage(subject=subject, from_email=from_email, to=to, bcc=bcc,
                           connection=connection, headers=headers, cc=cc, reply_to=reply_to)
        alternative = MIMEMultipart('alternative')
        alternative.attach(MIMEText(body.encode('utf8'), 'plain', 'utf8'))
        alternative.attach(MIMEText(html_body.encode('utf8'), 'html', 'utf8'))
        related = MIMEMultipart('related')
        related.attach(alternative)
        for inline in html_inline_attachments:
            inline_attachment = msg._create_attachment(os.path.basename(inline), open(inline).read())
            inline_attachment.add_header('Content-Disposition', 'inline')
            inline_attachment.add_header('Content-ID', os.path.basename(inline))
            related.attach(inline_attachment)
        msg.attach(related)
        return msg
    

    If someone has a simpler solution, please let me know :-)