Send email action, step 1
Basic send-email functionality and unit tests See comments in test_send_email_action on how to run with real smtpd. Implements: blueprint mistral-std-send-email-action Change-Id: Ib1d460d26ce54f4ab85d34b18947d858ab471dfd
This commit is contained in:
parent
842d75713a
commit
9bca04c79e
mistral
@ -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
|
||||
|
125
mistral/tests/unit/engine/actions/test_send_email_action.py
Normal file
125
mistral/tests/unit/engine/actions/test_send_email_action.py
Normal file
@ -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)
|
Loading…
x
Reference in New Issue
Block a user