diff --git a/mistral/engine/actions/actions.py b/mistral/engine/actions/actions.py index 15f5b191a..18f91d302 100644 --- a/mistral/engine/actions/actions.py +++ b/mistral/engine/actions/actions.py @@ -16,6 +16,9 @@ from amqplib import client_0_8 as amqp import requests +#TODO(dzimine):separate actions across different files/modules +import smtplib +from email.mime.text import MIMEText from mistral.openstack.common import log as logging @@ -40,6 +43,7 @@ class BaseAction(object): class RestAction(BaseAction): + def __init__(self, action_type, action_name, url, params={}, method="GET", headers={}, data={}): super(RestAction, self).__init__(action_type, action_name) @@ -110,3 +114,49 @@ class OsloRPCAction(BaseAction): def callback(self, msg): #TODO (nmakhotkin) set status self.status = None + + +class SendEmailAction(BaseAction): + def __init__(self, action_type, action_name, params, settings): + super(SendEmailAction, self).__init__(action_type, action_name) + #TODO(dzimine): validate parameters + + # Task invocation parameters. + self.to = ', '.join(params['to']) + self.subject = params['subject'] + self.body = params['body'] + + # Action provider settings. + self.smtp_server = settings['smtp_server'] + self.sender = settings['from'] + self.password = settings['password'] \ + if 'password' in settings else None + + def run(self): + LOG.info("Sending email message " + "[from=%s, to=%s, subject=%s, using smtp=%s, body=%s...]" % + (self.sender, self.to, self.subject, + self.smtp_server, self.body[:128])) + + #TODO(dzimine): handle utf-8, http://stackoverflow.com/a/14506784 + message = MIMEText(self.body) + message['Subject'] = self.subject + message['From'] = self.sender + message['To'] = self.to + try: + s = smtplib.SMTP(self.smtp_server) + if self.password is not None: + # Sequence to request TLS connection and log in (RFC-2487). + s.ehlo() + s.starttls() + s.ehlo() + s.login(self.sender, self.password) + + s.sendmail(from_addr=self.sender, + to_addrs=self.to, + msg=message.as_string()) + except (smtplib.SMTPException, IOError) as e: + LOG.error("Error sending email message: %s" % e) + #NOTE(DZ): Raise Misral exception instead re-throwing SMTP? + # For now just logging the error here and re-thorw the original + raise diff --git a/mistral/tests/unit/engine/actions/test_send_email_action.py b/mistral/tests/unit/engine/actions/test_send_email_action.py new file mode 100644 index 000000000..e3fa36d15 --- /dev/null +++ b/mistral/tests/unit/engine/actions/test_send_email_action.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +# +# Copyright 2013 - StackStorm, Inc. +# +# 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 mistral.engine.actions import actions +from email.parser import Parser + +import unittest2 +from mock import patch, call + + +#TODO(dzimine): replace local definitions on next step +# from mistral.engine.actions import action_types +ACTION_TYPE = "MISTRAL_SEND_EMAIL" +ACTION_NAME = "TEMPORARY" + +''' +To try against a real SNMP server: + +1) set LOCAL_SMPTD = True + run debug snmpd on the local machine: + `sudo python -m smtpd -c DebuggingServer -n localhost:25` + Debugging server doesn't support password. + +2) set REMOTE_SMPT = True + use external SNMP (like gmail), change the configuration, + provide actual username and password + self.settings = { + 'host': 'smtp.gmail.com:587', + 'from': "youraccount@gmail.com", + 'password': "secret" + } + +''' +LOCAL_SMTPD = False +REMOTE_SMTP = False + + +class SendEmailActionTest(unittest2.TestCase): + + def setUp(self): + self.params = { + 'to': ["dz@example.com, deg@example.com", "xyz@example.com"], + 'subject': "Multi word subject с русскими буквами", + 'body': "short multiline\nbody\nc русскими буквами", + } + self.settings = { + 'smtp_server': 'mail.example.com:25', + 'from': "bot@example.com", + } + self.to_addrs = ', '.join(self.params['to']) + + @unittest2.skipIf(not LOCAL_SMTPD, "Setup local smtpd to run it") + def test_send_email_real(self): + action = actions.SendEmailAction( + ACTION_TYPE, ACTION_NAME, self.params, self.settings) + action.run() + + @unittest2.skipIf(not REMOTE_SMTP, "Configure Remote SMTP to run it") + def test_with_password_real(self): + self.params['to'] = ["dz@stackstorm.com"] + self.settings = { + 'smtp_server': 'smtp.gmail.com:587', + 'from': "username@gmail.com", + } + self.settings['password'] = "secret" + action = actions.SendEmailAction( + ACTION_TYPE, ACTION_NAME, self.params, self.settings) + action.run() + + @patch('smtplib.SMTP') + def test_send_email(self, smtp): + action = actions.SendEmailAction( + ACTION_TYPE, ACTION_NAME, self.params, self.settings) + action.run() + smtp.assert_called_once_with(self.settings['smtp_server']) + sendmail = smtp.return_value.sendmail + self.assertTrue(sendmail.called, "should call sendmail") + self.assertEqual( + sendmail.call_args[1]['from_addr'], self.settings['from']) + self.assertEqual( + sendmail.call_args[1]['to_addrs'], self.to_addrs) + message = Parser().parsestr(sendmail.call_args[1]['msg']) + self.assertEqual(message['from'], self.settings['from']) + self.assertEqual(message['to'], self.to_addrs) + self.assertEqual(message['subject'], self.params['subject']) + self.assertEqual(message.get_payload(), self.params['body']) + + @patch('smtplib.SMTP') + def test_with_password(self, smtp): + self.settings['password'] = "secret" + action = actions.SendEmailAction( + ACTION_TYPE, ACTION_NAME, self.params, self.settings) + action.run() + smtpmock = smtp.return_value + calls = [call.ehlo(), call.starttls(), call.ehlo(), + call.login(self.settings['from'], self.settings['password'])] + smtpmock.assert_has_calls(calls) + self.assertTrue(smtpmock.sendmail.called, "should call sendmail") + + @patch('mistral.engine.actions.actions.LOG') + def test_exception(self, log): + self.params['smtp_server'] = "wrong host" + action = actions.SendEmailAction( + ACTION_TYPE, ACTION_NAME, self.params, self.settings) + try: + action.run() + except IOError: + pass + else: + self.assertFalse("Must throw exception") + + self.assertTrue(log.error.called)