Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 48 additions & 7 deletions homeassistant/components/notify/smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
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 (
Expand All @@ -26,10 +26,12 @@
_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
Expand All @@ -47,6 +49,7 @@
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,
})

Expand All @@ -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():
Expand All @@ -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
Expand All @@ -84,6 +88,8 @@ def __init__(self, server, port, timeout, sender, starttls, username,
self.username = username
self.password = password
self.recipients = recipients
self._sender_name = sender_name
self._timeout = timeout
self.debug = debug
self.tries = 2

Expand Down Expand Up @@ -128,19 +134,28 @@ def send_message(self, message="", **kwargs):
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()
Expand All @@ -155,12 +170,16 @@ def _send_email(self, msg):
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()


Expand Down Expand Up @@ -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
26 changes: 24 additions & 2 deletions tests/components/notify/test_smtp.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ def setUp(self): # pylint: disable=invalid-name
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."""
Expand All @@ -38,7 +39,7 @@ def test_text_email(self, mock_make_msgid):
'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'
Expand All @@ -52,3 +53,24 @@ def test_mixed_email(self, mock_make_msgid):
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)