3

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?

guettli
  • 25,042
  • 81
  • 346
  • 663
  • Does this help ? : http://stackoverflow.com/a/20717538/2286762 – Kishan Mehta Nov 03 '16 at 05:41
  • @soupboy no, it does not help. The question you reference does only solve one way of attaching a file. – guettli Nov 03 '16 at 13:50
  • but inline image part you were able to implement right? Can you show your code where you are facing the problem. – Kishan Mehta Nov 04 '16 at 05:20
  • I love down-votes.... if I know why it was done. Please tell me, I am curious. – guettli Nov 09 '16 at 08:45
  • i was expecting more code and error tracebacks rather than theoretical question. – Kishan Mehta Nov 09 '16 at 10:00
  • 1
    @soupboy when I was younger, I did coding even if I had no definition of "done". I am 40 years old now, and take pyCharm only if I have a definition of done/ready. I tried to find an explanation how to structure a mail which uses both (inline attachments via cid:logo.png and downloadable attachments). I could not find a guideline. That's why I did no coding, that's why this question does not contain a traceback :-) You find code and a nice ascii art structure in the answer ... – guettli Nov 09 '16 at 10:50

1 Answers1

25

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 :-)

guettli
  • 25,042
  • 81
  • 346
  • 663
  • 3
    This graphical representation helped me understand the structure in better way. Thanks for that. When using SMIME, I observed _multipart/mixed_ is followed by _application/pkcs7-signature_ part and _multipart/signed_ holding both of these. Something like: _multipart/signed [ multipart/mixed, application/pkcs7-signature]_ – ramtech Jul 21 '17 at 13:12
  • I'm trying to find documentation on that email structure with no luck. This should be described as part of email protocol but I've never seen this well described. How did you come-up with that email structure? – Mike Oct 05 '22 at 15:25
  • @Mike at 2016 we developed a web based mail user agent as part of this product: https://www.tbz-pariv.de/produkte/modwork. We analyzed many mails. – guettli Oct 06 '22 at 07:19
  • 1
    This is something unbelievable that we must reverse engineer quite tricky piece of software and it is hard to find detailed documentation on so widely used stuff... Your post is VERY useful, thanks a lot for sharing – Mike Oct 06 '22 at 07:51