In this post, I will walk through sending HTML formatted emails with inline images and attachments.
I was really excited to learn about this functionality in Python because I have been frustrated that I haven’t been able to find a tool that does the following and that doesn’t cost tens of thousands of dollars:
- Send bulk emails with the option of having multiple recipients per email
- Connect to a database to generate custom charts and include them as inline images in the body of the email
Showing an interesting chart created with MatPlotLib in the body of an email creates a more engaging message, in my opinion. Requiring someone to open your attachment is an added layer of friction and the attachment oftentimes missed.
This posts uses Gmail to send emails and will accomplish the following:
- Send an HTML formatted email
- Include inline images in the body of an email
- Include a plain text backup (in case the recipient can’t view the HTML version)
- Include images as attachments (in case the recipient can’t view the HTML version)
- Loop through a list of email contacts
- Loop through a list of parameters and generate a customized chart using MatPlotLib for each recipient
HTML and Plain Text Email with Two or More Attachments with Inline Images
There are multiple parts to the code so I’ll break it down and include the full code at the end.
First, we define the path to our target contacts and our target images. Both of these variables are lists so include as many contacts or image paths as you would like.
contacts = ['email@test.com']
image_paths = ["/path_to_image1/image1.png","/path_to_image2/image2.png"]
Next, we’ll define the email parameters, minus the body. The msg['To']
variable will depend on if you have one or more than one email address. Leveraging the python .join
function, we can turn the list of two or more items into a string to pass into the msg['To']
field.
msg = EmailMessage()
msg['Subject'] = 'Test Email'
msg['From'] = email
msg['To'] = contacts # Use this for delivery to one contact. Un comment the next line if you have multiple recipients in your email
#msg['To'] = (", ").join(contacts) #If you have multiple contacts, you can use this To field
In case the HTML we write in the next section doesn’t show up on the recipient’s email, we should include a plain text back up. Just write your message with this code. If you want to have multiple lines in your string, then use \n
.
#Plain text email content
msg.set_content('This is a plain text email')
The next section is where you will write the body of an email in an HTML format. Including the images is slightly different than what you would do if this were straight-up HTML. Instead of passing in the path to the file, we need to create a CID Image. A CID image is how we imbed the image into the actual email body. So it will appear like this: <img src="cid:image1"><br>
. We will define what image1, image2, etc. are in the next section.
text_part = msg.iter_parts()
text_part
msg.add_alternative("""\
<!DOCTYPE html>
<html>
<body>
<p>Hi Paul,</p>
<p>If you are seeing this, it means that you have received my email. Check out these images!</p>
<p>Best,</p>
<p>Paul</p>
<img src="cid:image1" ><br>
<img src="cid:image2" ><br>
</body>
</html>
""", subtype='html')
This next code block, will loop through the image files defined earlier and create the CID image id.
counter = 1
for fp in image_paths:
fp = open(fp, 'rb')
msgImage = MIMEImage(fp.read())
fp.close()
# Define the image's ID as referenced above
msgImage.add_header('Content-ID', '<image'+str(counter)+'>')
msg.attach(msgImage)
counter += 1
I like including the images as attachments as well in case the email is delivered as plain text, or they don’t appear in the body for some reason. In either case, I don’t think it hurts to include them. This code loops through each image and includes it as an attachment to the email.
#iterating through images to add as attachment
for f in image_paths:
attachment = MIMEApplication(open(f, "rb").read(), _subtype="txt")
attachment.add_header('Content-Disposition','attachment', filename=f)
msg.attach(attachment)
The last bit of code connects to the Gmail server and sends the message! You will need to input your Gmail Email and your Password. I highly recommend using environment variables instead of placing your credentials directly in your code. If your Gmail uses two-factor authentication, then you will need to use a Google App Password instead of your regular password.
with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp:
smtp.login(email, password)
smtp.send_message(msg)
Here is the full code:
import os import smtplib import imghdr from email.message import EmailMessage from email.mime.multipart import MIMEMultipart #pip install email-to from email.mime.text import MIMEText from email.mime.image import MIMEImage email = os.environ.get('email_address') password = os.environ.get('gmail_python_app_password') contacts = ['email@test.com'] #add one or multiple contacts here image_paths = ["/path_to_image1/image1.png","/path_to_image2/image2.png"] msg = EmailMessage() msg['Subject'] = 'Test Email' msg['From'] = email msg['To'] = contacts # Use this for delivery to one contact. Un comment the next line if you have multiple recipients in your email #msg['To'] = (", ").join(contacts) #If you have multiple contacts, you can use this To field #Plain text email content msg.set_content('This is a plain text email') #HTML Body text_part = msg.iter_parts() text_part msg.add_alternative("""\ <!DOCTYPE html> <html> <body> <p>Hi Paul,</p> <p>If you are seeing this, it means that you have received my email. Check out these images!</p> <p>Best,</p> <p>Paul</p> <img src="cid:image1" ><br> <img src="cid:image2" ><br> </body> </html> """, subtype='html') #iterating through the image files for HTML body counter = 1 for fp in image_paths: fp = open(fp, 'rb') msgImage = MIMEImage(fp.read()) fp.close() # Define the image's ID as referenced above msgImage.add_header('Content-ID', '<image'+str(counter)+'>') msg.attach(msgImage) counter += 1 #iterating through images to add as attachment for f in image_paths: attachment = MIMEApplication(open(f, "rb").read(), _subtype="txt") attachment.add_header('Content-Disposition','attachment', filename=f) msg.attach(attachment) with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp: smtp.login(email, password) smtp.send_message(msg)
Here is how an email would appear the recipients inbox:
Iterating Through a List of Contacts
If you have a list of contacts you want to iterate through, then you can turn the above code into a for loop and iterate through the contacts list.
You can also connect to a database and iterate through the contacts to generate a chart to include in the email.
Other ways to send Emails
Through this project, I also created a handful of variations of the code for other use cases. What helped me a lot was Corey Shafers YouTube guide – How to Send Emails Using Python – Plain Text, Adding Attachments, HTML Emails, and More
Plain Text email with an attachment
contacts = ['hello@123.com', 'test@email.com'] with open("/path_to_image/image.png", 'rb') as f: file_data = f.read() file_type = imghdr.what(f.name) file_name = f.name msg = EmailMessage() msg['Subject'] = 'Hello!' msg['From'] = email msg['To'] = (", ").join(contacts) msg.set_content('This is a plain text email') #body of the email. Use Triple quotes to run multiple lines msg.add_attachment(file_data, maintype='image', subtype=file_type, filename = file_name) with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp: smtp.login(email, password) smtp.send_message(msg)
HTML and Plain Text Email with One Attachment
contact = 'email@test.com' image_path = "/path_to_image/image.png" with open(image_path, 'rb') as f: file_data = f.read() file_type = imghdr.what(f.name) file_name = f.name msg = EmailMessage() msg['Subject'] = 'Hello!' msg['From'] = email msg['To'] = contact #msg['To'] = (", ").join(contacts) #use this if you want to have multiple recipients in the same email msg.set_content('This is a plain text email') msg.add_attachment(file_data, maintype='image', subtype=file_type, filename = file_name) text_part, attachment_part = msg.iter_parts() text_part.add_alternative("""\ <!DOCTYPE html> <html> <body> <p>Hi Paul,</p> <p>Check out this image.</p> <p>Thanks,</p> <p>Paul</p> <img src="cid:image1" ><br> </body> </html> """, subtype='html') fp = open(image_path, 'rb') msgImage = MIMEImage(fp.read()) fp.close() # Define the image's ID as referenced above msgImage.add_header('Content-ID', '<image1>') msg.attach(msgImage) with smtplib.SMTP_SSL('smtp.gmail.com', 465) as smtp: smtp.login(email, password) smtp.send_message(msg)