Skip to content

Instantly share code, notes, and snippets.

@paulonteri
Forked from ninapavlich/email_sender.py
Last active December 14, 2021 14:57
Show Gist options
  • Save paulonteri/4985ea177849913448831d9a5f68b322 to your computer and use it in GitHub Desktop.
Save paulonteri/4985ea177849913448831d9a5f68b322 to your computer and use it in GitHub Desktop.
Convert HTML emails with python
import os
import re
from email.mime.image import MIMEImage
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from smtplib import SMTP, SMTP_SSL, SMTPAuthenticationError, SMTPException
from urllib.parse import urlparse
import requests
from bs4 import BeautifulSoup
from jinja2 import Template
class EmailSender(object):
@classmethod
def send_email_message(cls, recipient_email, from_email, from_name, subject_template, message_template,
message_context, smtp_settings, verify_ssl=True):
message_context['recipient_email'] = recipient_email
# -- Render the Subject of the Email
subject_template = Template(subject_template)
subject = subject_template.render(message_context)
# subject = ' '.join(subject.split()) # Remove whitespace, newlines, etc
# -- Render the Body of the Email
message_template = Template(message_template)
message_html = message_template.render(message_context)
message_no_tags = EmailSender.prepare_body_html(message_html)
# -- Send Message
EmailSender._send_rendered_message(
recipient_email,
subject,
message_no_tags,
(message_html),
from_email,
from_name,
smtp_settings,
verify_ssl
)
@classmethod
def _send_rendered_message(cls, recipient_email, subject, message_no_tags, message_html, from_email, from_name,
smtp_settings, verify_ssl=True):
email_headers = {}
# -- Create Message
msg = EmailSender.create_email(
EmailSender._get_formatted_recipient(recipient_email),
EmailSender._get_formatted_sender(from_email, from_name),
subject,
message_no_tags,
message_html,
verify_ssl
)
# -- Send Message
try:
if smtp_settings['smtp_ssl']:
if smtp_settings['smtp_port']:
smtp = SMTP_SSL(
smtp_settings['smtp_host'], smtp_settings['smtp_port'])
else:
smtp = SMTP_SSL(smtp_settings['smtp_host'])
else:
if smtp_settings['smtp_port']:
smtp = SMTP(smtp_settings['smtp_host'],
smtp_settings['smtp_port'])
else:
smtp = SMTP(smtp_settings['smtp_host'])
smtp.ehlo()
if smtp.has_extn('STARTTLS'):
smtp.starttls()
smtp.login(smtp_settings['smtp_user'],
smtp_settings['smtp_password'])
except SMTPException as e:
raise Exception("Error connecting to SMTP host: %s" % (e))
except SMTPAuthenticationError as e:
raise Exception("SMTP username/password rejected: %s" % (e))
smtp.sendmail(from_email, recipient_email, msg.as_string())
smtp.close()
@classmethod
def _get_formatted_sender(cls, from_email, from_name):
return "%s <%s>" % (from_name, from_email)
@classmethod
def _get_formatted_recipient(cls, recipient_email):
"""Ensure Email Address is Returned in a List"""
if isinstance(recipient_email, str):
recipient_email = [recipient_email]
return ",".join(recipient_email)
@classmethod
def prepare_body_html(cls, body_html):
"""Strips HTML from Email for Text Only"""
p = re.compile(r'<.*?>')
return p.sub('', body_html)
@classmethod
def create_email(cls, recipient_email, from_email, subject, message, message_html,
verify_ssl=True):
# Create the root message and fill in the from, to, and subject headers
msg = MIMEMultipart('related')
msg['Subject'] = subject
msg['From'] = from_email
msg['To'] = recipient_email
msg.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')
msg.attach(msgAlternative)
msgText = MIMEText(message)
msgAlternative.attach(msgText)
# We reference the image in the IMG SRC attribute by the ID we give it
# below
# -- Replace images with CID paths
message_with_images_prepared, cid_images = EmailSender._replace_images_with_cid_paths(
message_html)
# -- Attach CID images
EmailSender._attach_cid_images(
msg, cid_images, verify_ssl)
# -- Attach HTML Message
msgText = MIMEText(message_with_images_prepared,
"html")
msgAlternative.attach(msgText)
return msg
@classmethod
def _replace_images_with_cid_paths(cls, body_html):
"""Parse the message HTML and identify images"""
if body_html:
email = BeautifulSoup(body_html, "html5lib")
image_counter = 1
cid_images = []
for image in email.findAll('img'):
cid_id = "image_%s" % (image_counter)
image_counter = image_counter + 1
original_image_src = image['src']
image['src'] = "cid:%s" % (cid_id)
cid_images.append({
'src': original_image_src,
'cid_id': cid_id
})
return (email.prettify(), cid_images)
else:
return (body_html, [])
@classmethod
def _attach_cid_images(cls, msg, cid_images, verify_ssl=True):
"""Attach MIME / CID Images to email"""
if cid_images and len(cid_images) > 0:
print("Attach MIME / CID Images to email")
msg.mixed_subtype = 'related'
for image in cid_images:
try:
mime_image = EmailSender._convert_image_to_cid(
image['src'], image['cid_id'], verify_ssl)
if mime_image:
msg.attach(mime_image)
except Exception as e:
print(u"ERROR attacing CID image %s[%s] %s" % (
image['cid_id'], image['src'], str(e)))
@classmethod
def _convert_image_to_cid(cls, image_src, cid_id, verify_ssl=True):
"""Turn image path into a MIMEImage"""
try:
if 'data:image/png;base64,' in image_src.lower():
mime_image = MIMEImage(image_src, _subtype="png")
else:
path = urlparse(image_src).path
guess_subtype = os.path.splitext(path)[1][1:]
response = requests.get(image_src, verify=verify_ssl)
mime_image = MIMEImage(
response.content, _subtype=guess_subtype)
# Define the image's ID as referenced above
mime_image.add_header('Content-ID', '<%s>' % (cid_id))
return mime_image
except Exception as e:
print(u"ERROR creating mime_image %s[%s] %s" % (
cid_id, image_src, str(e)))
return None
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment