SMTP notify enhancements: full HTML emails and custom product_name in email headers (#7533)

* SMTP notify enhancements: HTML emails and customization options

- Send full HTML emails, with or without inline attached images.
- Custom `timeout`.
- Custom `product_name` identifier for the `FROM` and `X-MAILER` email headers.
- New HTML email test

* `sender_name` instead of product_name

 - Change `sender_name` instead of `product_name`.
 - No changes in `X-Mailer` header.
 - `From` header as always unless you define the new `sender_name` parameter.
This commit is contained in:
Eugenio Panadero 2017-05-15 09:23:57 +02:00 committed by Paulus Schoutsen
parent 36d7fe72eb
commit d0304198de
2 changed files with 72 additions and 9 deletions

View File

@ -9,9 +9,9 @@ import smtplib
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.mime.image import MIMEImage
import email.utils
from email.mime.application import MIMEApplication
import email.utils
import os
import voluptuous as vol
from homeassistant.components.notify import (
@ -26,10 +26,12 @@ import homeassistant.util.dt as dt_util
_LOGGER = logging.getLogger(__name__)
ATTR_IMAGES = 'images' # optional embedded image file attachments
ATTR_HTML = 'html'
CONF_STARTTLS = 'starttls'
CONF_DEBUG = 'debug'
CONF_SERVER = 'server'
CONF_SENDER_NAME = 'sender_name'
DEFAULT_HOST = 'localhost'
DEFAULT_PORT = 25
@ -47,6 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
vol.Optional(CONF_USERNAME): cv.string,
vol.Optional(CONF_PASSWORD): cv.string,
vol.Optional(CONF_SENDER_NAME): cv.string,
vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean,
})
@ -62,6 +65,7 @@ def get_service(hass, config, discovery_info=None):
config.get(CONF_USERNAME),
config.get(CONF_PASSWORD),
config.get(CONF_RECIPIENT),
config.get(CONF_SENDER_NAME),
config.get(CONF_DEBUG))
if mail_service.connection_is_valid():
@ -74,7 +78,7 @@ class MailNotificationService(BaseNotificationService):
"""Implement the notification service for E-Mail messages."""
def __init__(self, server, port, timeout, sender, starttls, username,
password, recipients, debug):
password, recipients, sender_name, debug):
"""Initialize the service."""
self._server = server
self._port = port
@ -84,6 +88,8 @@ class MailNotificationService(BaseNotificationService):
self.username = username
self.password = password
self.recipients = recipients
self._sender_name = sender_name
self._timeout = timeout
self.debug = debug
self.tries = 2
@ -128,19 +134,28 @@ class MailNotificationService(BaseNotificationService):
Build and send a message to a user.
Will send plain text normally, or will build a multipart HTML message
with inline image attachments if images config is defined.
with inline image attachments if images config is defined, or will
build a multipart HTML if html config is defined.
"""
subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
data = kwargs.get(ATTR_DATA)
if data:
msg = _build_multipart_msg(message, images=data.get(ATTR_IMAGES))
if ATTR_HTML in data:
msg = _build_html_msg(message, data[ATTR_HTML],
images=data.get(ATTR_IMAGES))
else:
msg = _build_multipart_msg(message,
images=data.get(ATTR_IMAGES))
else:
msg = _build_text_msg(message)
msg['Subject'] = subject
msg['To'] = ','.join(self.recipients)
msg['From'] = self._sender
if self._sender_name:
msg['From'] = '{} <{}>'.format(self._sender_name, self._sender)
else:
msg['From'] = self._sender
msg['X-Mailer'] = 'HomeAssistant'
msg['Date'] = email.utils.format_datetime(dt_util.now())
msg['Message-Id'] = email.utils.make_msgid()
@ -155,12 +170,16 @@ class MailNotificationService(BaseNotificationService):
mail.sendmail(self._sender, self.recipients,
msg.as_string())
break
except smtplib.SMTPServerDisconnected:
_LOGGER.warning(
"SMTPServerDisconnected sending mail: retrying connection")
mail.quit()
mail = self.connect()
except smtplib.SMTPException:
_LOGGER.warning(
"SMTPException sending mail: retrying connection")
mail.quit()
mail = self.connect()
mail.quit()
@ -204,3 +223,25 @@ def _build_multipart_msg(message, images):
body_html = MIMEText(''.join(body_text), 'html')
msg_alt.attach(body_html)
return msg
def _build_html_msg(text, html, images):
"""Build Multipart message with in-line images and rich html (UTF-8)."""
_LOGGER.debug("Building html rich email")
msg = MIMEMultipart('related')
alternative = MIMEMultipart('alternative')
alternative.attach(MIMEText(text, _charset='utf-8'))
alternative.attach(MIMEText(html, ATTR_HTML, _charset='utf-8'))
msg.attach(alternative)
for atch_num, atch_name in enumerate(images):
name = os.path.basename(atch_name)
try:
with open(atch_name, 'rb') as attachment_file:
attachment = MIMEImage(attachment_file.read(), filename=name)
msg.attach(attachment)
attachment.add_header('Content-ID', '<{}>'.format(name))
except FileNotFoundError:
_LOGGER.warning('Attachment %s [#%s] not found. Skipping',
atch_name, atch_num)
return msg

View File

@ -23,7 +23,8 @@ class TestNotifySmtp(unittest.TestCase):
self.hass = get_test_home_assistant()
self.mailer = MockSMTP('localhost', 25, 5, 'test@test.com', 1,
'testuser', 'testpass',
['recip1@example.com', 'testrecip@test.com'], 0)
['recip1@example.com', 'testrecip@test.com'],
'HomeAssistant', 0)
def tearDown(self): # pylint: disable=invalid-name
""""Stop down everything that was started."""
@ -38,7 +39,7 @@ class TestNotifySmtp(unittest.TestCase):
'Content-Transfer-Encoding: 7bit\n'
'Subject: Home Assistant\n'
'To: recip1@example.com,testrecip@test.com\n'
'From: test@test.com\n'
'From: HomeAssistant <test@test.com>\n'
'X-Mailer: HomeAssistant\n'
'Date: [^\n]+\n'
'Message-Id: <[^@]+@[^>]+>\n'
@ -52,3 +53,24 @@ class TestNotifySmtp(unittest.TestCase):
msg = self.mailer.send_message('Test msg',
data={'images': ['test.jpg']})
self.assertTrue('Content-Type: multipart/related' in msg)
@patch('email.utils.make_msgid', return_value='<mock@mock>')
def test_html_email(self, mock_make_msgid):
"""Test build of html email behavior."""
html = '''
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head><meta charset="UTF-8"></head>
<body>
<div>
<h1>Intruder alert at apartment!!</h1>
</div>
<div>
<img alt="test.jpg" src="cid:test.jpg"/>
</div>
</body>
</html>'''
msg = self.mailer.send_message('Test msg',
data={'html': html,
'images': ['test.jpg']})
self.assertTrue('Content-Type: multipart/related' in msg)