Skip to content

Commit

Permalink
Introduce new driver 'mail' for sending emails (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
maretskiy authored and boris-42 committed Dec 31, 2016
1 parent 8c12288 commit d13f484
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 0 deletions.
6 changes: 6 additions & 0 deletions etc/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@
"dummyerr": {
"dummy_err": {},
"dummy_err_explained": {}
},
"mail": {
"mail": {
"sender_domain": "seecloud-notify.example.org",
"recipients": ["yourname@mail.example.org"]
}
}
}
}
81 changes: 81 additions & 0 deletions notify/drivers/mail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Copyright 2016: Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from email.mime import text as mime_text
import logging
import smtplib

from notify import driver

LOG = logging.getLogger(__name__)
LOG.setLevel(logging.INFO)


class Driver(driver.Driver):
"""Mail notification driver."""

CONFIG_SCHEMA = {
"$schema": "http://json-schema.org/draft-04/schema",
"type": "object",
"properties": {
"sender_domain": {"type": "string"},
"recipients": {"type": "array", "minItems": 1},
"smtp_host": {"type": "string"},
"smtp_port": {"type": "integer"},
"mimetype": {"enum": ["plain", "html"]},
},
"required": ["sender_domain"],
"additionalProperties": False
}

def __init__(self, config):
super(Driver, self).__init__(config)
self._sender_domain = self.config["sender_domain"]
self._recipients = self.config["recipients"]
self._smtp_host = self.config.get("smtp_host", "localhost")
self._smtp_port = self.config.get("smtp_port")
self._mime = self.config.get("mimetype", "plain")

def _sanitize_name(self, name):
sanitized_name = ""
for c in name.lower().replace("_", "-"):
if c.isalnum() or c in "-.":
sanitized_name += c
return sanitized_name

def notify(self, payload):
subject = "{}: {}".format(payload["who"], payload["what"])
if payload.get("affected_hosts"):
subject += " ({})".format(",".join(payload["affected_hosts"]))

sender = "{}@{}".format(self._sanitize_name(payload["region"]),
self._sender_domain)

msg = mime_text.MIMEText(payload["description"], self._mime)
msg["Subject"] = subject
msg["From"] = sender
msg["To"] = self._recipients[0]
smtp = smtplib.SMTP(host=self._smtp_host, port=self._smtp_port)
try:
fails = smtp.sendmail(sender, self._recipients, msg.as_string())
for recipient, err in fails.items():
LOG.error("Fail to notify {} via email: {}", recipient, err)
# NOTE(maretskiy): True is returned in case of non-empty `fails',
# because smtp.sendmail returns if there is at least one
# recipient successfully got a messag.
# But in case of total failure it raises some exception.
finally:
smtp.quit()
return True
8 changes: 8 additions & 0 deletions tests/tools/notify_mail.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
curl -XPOST -H 'Content-Type: application/json' http://localhost:5000/api/v1/notify/mail -d '
{
"region": "sender-username",
"description": "This is a test message that sent via mail notification driver!",
"severity": "INFO",
"who": "John Doe",
"what": "This is a subject of test message"
}'
97 changes: 97 additions & 0 deletions tests/unit/drivers/test_mail.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Copyright 2016: Mirantis Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

import mock

from notify import driver
from notify.drivers import mail
from tests.unit import test


class MailDriverTestCase(test.TestCase):

def test___init__(self):
self.assertRaises(KeyError, mail.Driver, {})
self.assertRaises(KeyError, mail.Driver, {"sender_domain": "foo"})
self.assertRaises(KeyError, mail.Driver, {"recipients": ["bar"]})
drv = mail.Driver({"sender_domain": "foo", "recipients": ["bar"]})
self.assertIsInstance(drv, driver.Driver)

def _driver(self, **config):
config.setdefault("sender_domain", "foo_domain")
config.setdefault("recipients", ["foo@example.org"])
return mail.Driver(config)

def _payload(self):
return {"description": "Message body",
"region": "fooenv42",
"severity": "INFO",
"what": "Foo subject",
"who": "John Doe"}

def test_sanitize_name(self):
drv = self._driver()
self.assertEqual("foo123", drv._sanitize_name("foo123"))
self.assertEqual("foo-12-3.bar",
drv._sanitize_name(" foo- 1+2_3 . bar "))

@mock.patch("notify.drivers.mail.smtplib")
@mock.patch("notify.drivers.mail.mime_text.MIMEText")
@mock.patch("notify.drivers.mail.LOG")
def test_notify(self, mock_log, mock_mimetext, mock_smtplib):
mock_mimetext.return_value.as_string.return_value = "message body"
mock_smtp = mock.Mock()
mock_smtp.sendmail.return_value = {}
mock_smtplib.SMTP.return_value = mock_smtp
drv = self._driver()
self.assertTrue(drv.notify(self._payload()))

calls = [mock.call("Subject", "John Doe: Foo subject"),
mock.call("From", "fooenv42@foo_domain"),
mock.call("To", "foo@example.org")]
self.assertEqual(calls,
mock_mimetext.return_value.__setitem__.mock_calls)
mock_mimetext.assert_called_once_with("Message body", "plain")
mock_smtplib.SMTP.assert_called_once_with(host="localhost", port=None)
mock_smtp.sendmail.assert_called_once_with(
"fooenv42@foo_domain", ["foo@example.org"], "message body")
mock_smtp.quit.assert_called_once_with()
self.assertFalse(mock_log.error.called)

@mock.patch("notify.drivers.mail.smtplib")
@mock.patch("notify.drivers.mail.mime_text.MIMEText")
@mock.patch("notify.drivers.mail.LOG")
def test_notify_some_fails(self, mock_log, mock_mimetext, mock_smtplib):
mock_mimetext.return_value.as_string.return_value = "message body"
mock_smtp = mock.Mock()
mock_smtp.sendmail.return_value = {"foo": "error details"}
mock_smtplib.SMTP.return_value = mock_smtp
drv = self._driver()
payload = self._payload()
payload["affected_hosts"] = ["srv1", "srv2"]
self.assertTrue(drv.notify(payload))

calls = [mock.call("Subject", "John Doe: Foo subject (srv1,srv2)"),
mock.call("From", "fooenv42@foo_domain"),
mock.call("To", "foo@example.org")]
self.assertEqual(calls,
mock_mimetext.return_value.__setitem__.mock_calls)
mock_mimetext.assert_called_once_with("Message body", "plain")
mock_smtplib.SMTP.assert_called_once_with(host="localhost", port=None)
mock_smtp.sendmail.assert_called_once_with(
"fooenv42@foo_domain", ["foo@example.org"], "message body")
mock_smtp.quit.assert_called_once_with()
mock_log.error.assert_called_once_with(
"Fail to notify {} via email: {}", "foo", "error details")

0 comments on commit d13f484

Please sign in to comment.