diff --git a/homeassistant/components/notify/smtp.py b/homeassistant/components/notify/smtp.py index 1a2e1bf5b4e..d66d024e111 100644 --- a/homeassistant/components/notify/smtp.py +++ b/homeassistant/components/notify/smtp.py @@ -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 diff --git a/tests/components/notify/test_smtp.py b/tests/components/notify/test_smtp.py index 016c6a5d1f4..127eecae2b7 100644 --- a/tests/components/notify/test_smtp.py +++ b/tests/components/notify/test_smtp.py @@ -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 \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='') + def test_html_email(self, mock_make_msgid): + """Test build of html email behavior.""" + html = ''' + + + + +
+

Intruder alert at apartment!!

+
+
+ test.jpg +
+ + ''' + msg = self.mailer.send_message('Test msg', + data={'html': html, + 'images': ['test.jpg']}) + self.assertTrue('Content-Type: multipart/related' in msg)