Skip to content

Instantly share code, notes, and snippets.

@ninapavlich
Last active January 24, 2024 08:33
Show Gist options
  • Save ninapavlich/bcb4aa6c9bdedbdbbbbc4f9fbb4a1bb8 to your computer and use it in GitHub Desktop.
Save ninapavlich/bcb4aa6c9bdedbdbbbbc4f9fbb4a1bb8 to your computer and use it in GitHub Desktop.
Convert HTML emails with python
from bs4 import BeautifulSoup
import os
import re
import requests
import urlparse
import smtplib
from smtplib import SMTP
from smtplib import SMTP_SSL
from smtplib import SMTPAuthenticationError
from smtplib import SMTPException
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.MIMEImage import MIMEImage
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, default_site_url, 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,
unicode(message_html),
from_email,
from_name,
default_site_url,
smtp_settings,
verify_ssl
)
@classmethod
def send_rendered_message(cls, recipient_email, subject, message_no_tags, message_html, from_email, from_name, default_site_url, 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,
default_site_url,
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, error) as e:
raise EAException("Error connecting to SMTP host: %s" % (e))
except SMTPAuthenticationError as e:
raise EAException("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]
elif isinstance(recipient_email, unicode):
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, default_site_url, 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, default_site_url, 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, default_site_url, verify_ssl=True):
"""Attach MIME / CID Images to email"""
if cid_images and len(cid_images) > 0:
msg.mixed_subtype = 'related'
for image in cid_images:
try:
mime_image = EmailSender.convert_image_to_cid(
image['src'], image['cid_id'], default_site_url, verify_ssl)
if mime_image:
msg.attach(mime_image)
except Exception, 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, default_site_url, 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:
image_src = EmailSender.normalize_image_url(
image_src, default_site_url)
path = urlparse.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, e:
print u"ERROR creating mime_image %s[%s] %s" % (cid_id, image_src, str(e))
return None
@classmethod
def normalize_image_url(cls, url, default_site_url):
if '//' not in url.lower():
url = u"%s%s" % (default_site_url, url)
return url
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment