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:
parent
36d7fe72eb
commit
d0304198de
2 changed files with 72 additions and 9 deletions
|
@ -9,9 +9,9 @@ import smtplib
|
||||||
from email.mime.multipart import MIMEMultipart
|
from email.mime.multipart import MIMEMultipart
|
||||||
from email.mime.text import MIMEText
|
from email.mime.text import MIMEText
|
||||||
from email.mime.image import MIMEImage
|
from email.mime.image import MIMEImage
|
||||||
import email.utils
|
|
||||||
from email.mime.application import MIMEApplication
|
from email.mime.application import MIMEApplication
|
||||||
|
import email.utils
|
||||||
|
import os
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.notify import (
|
from homeassistant.components.notify import (
|
||||||
|
@ -26,10 +26,12 @@ import homeassistant.util.dt as dt_util
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_IMAGES = 'images' # optional embedded image file attachments
|
ATTR_IMAGES = 'images' # optional embedded image file attachments
|
||||||
|
ATTR_HTML = 'html'
|
||||||
|
|
||||||
CONF_STARTTLS = 'starttls'
|
CONF_STARTTLS = 'starttls'
|
||||||
CONF_DEBUG = 'debug'
|
CONF_DEBUG = 'debug'
|
||||||
CONF_SERVER = 'server'
|
CONF_SERVER = 'server'
|
||||||
|
CONF_SENDER_NAME = 'sender_name'
|
||||||
|
|
||||||
DEFAULT_HOST = 'localhost'
|
DEFAULT_HOST = 'localhost'
|
||||||
DEFAULT_PORT = 25
|
DEFAULT_PORT = 25
|
||||||
|
@ -47,6 +49,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||||
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
|
vol.Optional(CONF_STARTTLS, default=DEFAULT_STARTTLS): cv.boolean,
|
||||||
vol.Optional(CONF_USERNAME): cv.string,
|
vol.Optional(CONF_USERNAME): cv.string,
|
||||||
vol.Optional(CONF_PASSWORD): cv.string,
|
vol.Optional(CONF_PASSWORD): cv.string,
|
||||||
|
vol.Optional(CONF_SENDER_NAME): cv.string,
|
||||||
vol.Optional(CONF_DEBUG, default=DEFAULT_DEBUG): cv.boolean,
|
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_USERNAME),
|
||||||
config.get(CONF_PASSWORD),
|
config.get(CONF_PASSWORD),
|
||||||
config.get(CONF_RECIPIENT),
|
config.get(CONF_RECIPIENT),
|
||||||
|
config.get(CONF_SENDER_NAME),
|
||||||
config.get(CONF_DEBUG))
|
config.get(CONF_DEBUG))
|
||||||
|
|
||||||
if mail_service.connection_is_valid():
|
if mail_service.connection_is_valid():
|
||||||
|
@ -74,7 +78,7 @@ class MailNotificationService(BaseNotificationService):
|
||||||
"""Implement the notification service for E-Mail messages."""
|
"""Implement the notification service for E-Mail messages."""
|
||||||
|
|
||||||
def __init__(self, server, port, timeout, sender, starttls, username,
|
def __init__(self, server, port, timeout, sender, starttls, username,
|
||||||
password, recipients, debug):
|
password, recipients, sender_name, debug):
|
||||||
"""Initialize the service."""
|
"""Initialize the service."""
|
||||||
self._server = server
|
self._server = server
|
||||||
self._port = port
|
self._port = port
|
||||||
|
@ -84,6 +88,8 @@ class MailNotificationService(BaseNotificationService):
|
||||||
self.username = username
|
self.username = username
|
||||||
self.password = password
|
self.password = password
|
||||||
self.recipients = recipients
|
self.recipients = recipients
|
||||||
|
self._sender_name = sender_name
|
||||||
|
self._timeout = timeout
|
||||||
self.debug = debug
|
self.debug = debug
|
||||||
self.tries = 2
|
self.tries = 2
|
||||||
|
|
||||||
|
@ -128,18 +134,27 @@ class MailNotificationService(BaseNotificationService):
|
||||||
Build and send a message to a user.
|
Build and send a message to a user.
|
||||||
|
|
||||||
Will send plain text normally, or will build a multipart HTML message
|
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)
|
subject = kwargs.get(ATTR_TITLE, ATTR_TITLE_DEFAULT)
|
||||||
data = kwargs.get(ATTR_DATA)
|
data = kwargs.get(ATTR_DATA)
|
||||||
|
|
||||||
if 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:
|
else:
|
||||||
msg = _build_text_msg(message)
|
msg = _build_text_msg(message)
|
||||||
|
|
||||||
msg['Subject'] = subject
|
msg['Subject'] = subject
|
||||||
msg['To'] = ','.join(self.recipients)
|
msg['To'] = ','.join(self.recipients)
|
||||||
|
if self._sender_name:
|
||||||
|
msg['From'] = '{} <{}>'.format(self._sender_name, self._sender)
|
||||||
|
else:
|
||||||
msg['From'] = self._sender
|
msg['From'] = self._sender
|
||||||
msg['X-Mailer'] = 'HomeAssistant'
|
msg['X-Mailer'] = 'HomeAssistant'
|
||||||
msg['Date'] = email.utils.format_datetime(dt_util.now())
|
msg['Date'] = email.utils.format_datetime(dt_util.now())
|
||||||
|
@ -155,12 +170,16 @@ class MailNotificationService(BaseNotificationService):
|
||||||
mail.sendmail(self._sender, self.recipients,
|
mail.sendmail(self._sender, self.recipients,
|
||||||
msg.as_string())
|
msg.as_string())
|
||||||
break
|
break
|
||||||
|
except smtplib.SMTPServerDisconnected:
|
||||||
|
_LOGGER.warning(
|
||||||
|
"SMTPServerDisconnected sending mail: retrying connection")
|
||||||
|
mail.quit()
|
||||||
|
mail = self.connect()
|
||||||
except smtplib.SMTPException:
|
except smtplib.SMTPException:
|
||||||
_LOGGER.warning(
|
_LOGGER.warning(
|
||||||
"SMTPException sending mail: retrying connection")
|
"SMTPException sending mail: retrying connection")
|
||||||
mail.quit()
|
mail.quit()
|
||||||
mail = self.connect()
|
mail = self.connect()
|
||||||
|
|
||||||
mail.quit()
|
mail.quit()
|
||||||
|
|
||||||
|
|
||||||
|
@ -204,3 +223,25 @@ def _build_multipart_msg(message, images):
|
||||||
body_html = MIMEText(''.join(body_text), 'html')
|
body_html = MIMEText(''.join(body_text), 'html')
|
||||||
msg_alt.attach(body_html)
|
msg_alt.attach(body_html)
|
||||||
return msg
|
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
|
||||||
|
|
|
@ -23,7 +23,8 @@ class TestNotifySmtp(unittest.TestCase):
|
||||||
self.hass = get_test_home_assistant()
|
self.hass = get_test_home_assistant()
|
||||||
self.mailer = MockSMTP('localhost', 25, 5, 'test@test.com', 1,
|
self.mailer = MockSMTP('localhost', 25, 5, 'test@test.com', 1,
|
||||||
'testuser', 'testpass',
|
'testuser', 'testpass',
|
||||||
['recip1@example.com', 'testrecip@test.com'], 0)
|
['recip1@example.com', 'testrecip@test.com'],
|
||||||
|
'HomeAssistant', 0)
|
||||||
|
|
||||||
def tearDown(self): # pylint: disable=invalid-name
|
def tearDown(self): # pylint: disable=invalid-name
|
||||||
""""Stop down everything that was started."""
|
""""Stop down everything that was started."""
|
||||||
|
@ -38,7 +39,7 @@ class TestNotifySmtp(unittest.TestCase):
|
||||||
'Content-Transfer-Encoding: 7bit\n'
|
'Content-Transfer-Encoding: 7bit\n'
|
||||||
'Subject: Home Assistant\n'
|
'Subject: Home Assistant\n'
|
||||||
'To: recip1@example.com,testrecip@test.com\n'
|
'To: recip1@example.com,testrecip@test.com\n'
|
||||||
'From: test@test.com\n'
|
'From: HomeAssistant <test@test.com>\n'
|
||||||
'X-Mailer: HomeAssistant\n'
|
'X-Mailer: HomeAssistant\n'
|
||||||
'Date: [^\n]+\n'
|
'Date: [^\n]+\n'
|
||||||
'Message-Id: <[^@]+@[^>]+>\n'
|
'Message-Id: <[^@]+@[^>]+>\n'
|
||||||
|
@ -52,3 +53,24 @@ class TestNotifySmtp(unittest.TestCase):
|
||||||
msg = self.mailer.send_message('Test msg',
|
msg = self.mailer.send_message('Test msg',
|
||||||
data={'images': ['test.jpg']})
|
data={'images': ['test.jpg']})
|
||||||
self.assertTrue('Content-Type: multipart/related' in msg)
|
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)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue