Sending Multipart html emails which contain embedded images
Asked Answered
D

4

103

I've been playing around with the email module in python but I want to be able to know how to embed images which are included in the html.

So for example if the body is something like

<img src="../path/image.png"></img>

I would like to embed image.png into the email, and the src attribute should be replaced with content-id. Does anybody know how to do this?

Disafforest answered 28/5, 2009 at 13:45 Comment(0)
A
93

For Python versions 3.4 and above.

The accepted answer is excellent, but only suitable for older Python versions (2.x and 3.3). I think it needs an update.

Here's how you can do it in newer Python versions (3.4 and above):

from email.message import EmailMessage
from email.utils import make_msgid
import mimetypes

msg = EmailMessage()

# generic email headers
msg['Subject'] = 'Hello there'
msg['From'] = 'ABCD <[email protected]>'
msg['To'] = 'PQRS <[email protected]>'

# set the plain text body
msg.set_content('This is a plain text body.')

# now create a Content-ID for the image
image_cid = make_msgid(domain='xyz.com')
# if `domain` argument isn't provided, it will 
# use your computer's name

# set an alternative html body
msg.add_alternative("""\
<html>
    <body>
        <p>This is an HTML body.<br>
           It also has an image.
        </p>
        <img src="cid:{image_cid}">
    </body>
</html>
""".format(image_cid=image_cid[1:-1]), subtype='html')
# image_cid looks like <[email protected]>
# to use it as the img src, we don't need `<` or `>`
# so we use [1:-1] to strip them off


# now open the image and attach it to the email
with open('path/to/image.jpg', 'rb') as img:

    # know the Content-Type of the image
    maintype, subtype = mimetypes.guess_type(img.name)[0].split('/')

    # attach it
    msg.get_payload()[1].add_related(img.read(), 
                                         maintype=maintype, 
                                         subtype=subtype, 
                                         cid=image_cid)


# the message is ready now
# you can write it to a file
# or send it using smtplib
Adamis answered 4/3, 2018 at 17:32 Comment(4)
there is very similar example (2nd from the bottom) @ email.examplesIneludible
Any way to reproduce this with msg = MIMEMultipart() ?Tetralogy
@Tetralogy The .add_alternative() method automatically converts it to MIMEMultipart('alternative').Andino
Any idea how to make the attached image appear as something other than "noname" with this solution? All of the solutions for this go via the attach method (e.g. #77656978).Waitress
H
191

Here is an example I found.

Recipe 473810: Send an HTML email with embedded image and plain text alternate:

HTML is the method of choice for those wishing to send emails with rich text, layout and graphics. Often it is desirable to embed the graphics within the message so recipients can display the message directly, without further downloads.

Some mail agents don't support HTML or their users prefer to receive plain text messages. Senders of HTML messages should include a plain text message as an alternate for these users.

This recipe sends a short HTML message with a single embedded image and an alternate plain text message.

# Send an HTML email with an embedded image and a plain text message for
# email clients that don't want to display the HTML.

from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.MIMEImage import MIMEImage

# Define these once; use them twice!
strFrom = '[email protected]'
strTo = '[email protected]'

# Create the root message and fill in the from, to, and subject headers
msgRoot = MIMEMultipart('related')
msgRoot['Subject'] = 'test message'
msgRoot['From'] = strFrom
msgRoot['To'] = strTo
msgRoot.preamble = 'This is a multi-part message in MIME format.'

# Encapsulate the plain and HTML versions of the message body in an
# 'alternative' part, so message agents can decide which they want to display.
msgAlternative = MIMEMultipart('alternative')
msgRoot.attach(msgAlternative)

msgText = MIMEText('This is the alternative plain text message.')
msgAlternative.attach(msgText)

# We reference the image in the IMG SRC attribute by the ID we give it below
msgText = MIMEText('<b>Some <i>HTML</i> text</b> and an image.<br><img src="cid:image1"><br>Nifty!', 'html')
msgAlternative.attach(msgText)

# This example assumes the image is in the current directory
fp = open('test.jpg', 'rb')
msgImage = MIMEImage(fp.read())
fp.close()

# Define the image's ID as referenced above
msgImage.add_header('Content-ID', '<image1>')
msgRoot.attach(msgImage)

# Send the email (this example assumes SMTP authentication is required)
import smtplib
smtp = smtplib.SMTP()
smtp.connect('smtp.example.com')
smtp.login('exampleuser', 'examplepass')
smtp.sendmail(strFrom, strTo, msgRoot.as_string())
smtp.quit()
Harkness answered 28/5, 2009 at 13:48 Comment(11)
Much thanks, I've tried many solution, This is the one that works perfect!Collation
@Andrew Hare: There are two multipart views (the two msgText instances). I don't see the content-type specified for each. How would the receiving system know which to render for the Html one?Mezcaline
I added msgText.replace_header('Content-Type','text/html') for the second msgText instance.Mezcaline
Ftr: the second argument to the MIMEText constructor is the subtype (defaults to plain, is 'html' for the second instance).Rego
I tired the same code for sending multiple embedded images in the content, but the first image gets embedded and the rest doesn't gets embedded, how can we embed multiple images?Voltmeter
You should typically not mess with the MIME preamble. The one scenario where overriding the default preamble minght make sense is if you expect your recipients to not understand the English default message (but even then you only have access to the basic ASCII character repertoire). It is only likely to be seen by people who are stuck on email software from the previous millennium anyway.Kwashiorkor
It worked for me in python 3.7.2, but I had to write imports differently: from email.mime.text import MIMEText from email.mime.image import MIMEImage from email.mime.multipart import MIMEMultipartRemex
The recommended answer works perfectly but just make sure that you **don't have spaces in cid:image1 ** while making your HTML template. it should be exactly this :- <img src="cid:image1"> not <img src="cid: image1"> or <img src="cid : image1"> otherwise it won't work. Atleast it didn't work in my case when I was using it with jinja template. @AndrewHare if this is correct please update your answer.Pinnate
Special characters finally worked on python 3.7 adding encode('utf-8') to msgRoot at sendmail: smtp.sendmail(strFrom, strTo, msgRoot.as_string().encode('utf-8'))Acrodont
I keep getting 2 images, the actual image (automatically added to the bottom of the email... for some reason) and a little missing image icon above it (that I can only assume is the string with the <img src="cid:image1">. How are you ONLY rendering the <img src="cid:image1"> that's in the given html string and what is 'image1' in this context? A file name like my_image.jpg or something else?Marchpane
I tried running this example, but I am getting a ton of import errors.Noninterference
A
93

For Python versions 3.4 and above.

The accepted answer is excellent, but only suitable for older Python versions (2.x and 3.3). I think it needs an update.

Here's how you can do it in newer Python versions (3.4 and above):

from email.message import EmailMessage
from email.utils import make_msgid
import mimetypes

msg = EmailMessage()

# generic email headers
msg['Subject'] = 'Hello there'
msg['From'] = 'ABCD <[email protected]>'
msg['To'] = 'PQRS <[email protected]>'

# set the plain text body
msg.set_content('This is a plain text body.')

# now create a Content-ID for the image
image_cid = make_msgid(domain='xyz.com')
# if `domain` argument isn't provided, it will 
# use your computer's name

# set an alternative html body
msg.add_alternative("""\
<html>
    <body>
        <p>This is an HTML body.<br>
           It also has an image.
        </p>
        <img src="cid:{image_cid}">
    </body>
</html>
""".format(image_cid=image_cid[1:-1]), subtype='html')
# image_cid looks like <[email protected]>
# to use it as the img src, we don't need `<` or `>`
# so we use [1:-1] to strip them off


# now open the image and attach it to the email
with open('path/to/image.jpg', 'rb') as img:

    # know the Content-Type of the image
    maintype, subtype = mimetypes.guess_type(img.name)[0].split('/')

    # attach it
    msg.get_payload()[1].add_related(img.read(), 
                                         maintype=maintype, 
                                         subtype=subtype, 
                                         cid=image_cid)


# the message is ready now
# you can write it to a file
# or send it using smtplib
Adamis answered 4/3, 2018 at 17:32 Comment(4)
there is very similar example (2nd from the bottom) @ email.examplesIneludible
Any way to reproduce this with msg = MIMEMultipart() ?Tetralogy
@Tetralogy The .add_alternative() method automatically converts it to MIMEMultipart('alternative').Andino
Any idea how to make the attached image appear as something other than "noname" with this solution? All of the solutions for this go via the attach method (e.g. #77656978).Waitress
G
9

I realized how painful some of the things are with SMTP and email libraries and I thought I have to do something with it. I made a library that makes embedding images to HTML way easier:

from redmail import EmailSender
email = EmailSender(host="<SMTP HOST>", port=0)

email.send(
    sender="[email protected]",
    receivers=["[email protected]"]
    subject="An email with image",
    html="""
        <h1>Look at this:</h1>
        {{ my_image }}
    """, 
    body_images={
        "my_image": "path/to/image.png"
    }
)

Sorry for promotion but I think it's pretty awesome. You can supply the image as Matplotlib Figure, Pillow Image or just as bytes if your image is in those formats. It uses Jinja for templating.

If you need to control the size of the image, you can also do this:

email.send(
    sender="[email protected]",
    receivers=["[email protected]"]
    subject="An email with image",
    html="""
        <h1>Look at this:</h1>
        <img src="{{ my_image.src }}" width=200 height=300>
    """, 
    body_images={
        "my_image": "path/to/image.png"
    }
)

You can just pip install it:

pip install redmail

It's (hopefully) all you need for email sending (has a lot more) and it is well tested. I also wrote extensive documentation: https://red-mail.readthedocs.io/en/latest/ and source code is found here.

Groom answered 3/1, 2022 at 19:36 Comment(0)
H
-5

Code working

    att = MIMEImage(imgData)
    att.add_header('Content-ID', f'<image{i}.{imgType}>')
    att.add_header('X-Attachment-Id', f'image{i}.{imgType}')
    att['Content-Disposition'] = f'inline; filename=image{i}.{imgType}'
    msg.attach(att)
Helmer answered 3/2, 2020 at 19:23 Comment(1)
Hi! Thanks for sharing answer. It might be useful if you also will add some explanations about the code above. Aslo in OP's code I do not see imgType variable definition so your code will raise an exception.Noshow

© 2022 - 2024 — McMap. All rights reserved.