-
-
Save paulonteri/4985ea177849913448831d9a5f68b322 to your computer and use it in GitHub Desktop.
Convert HTML emails with python
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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