Improve std.email action

Adds support for cc and bcc addresses to send mails as copy to
administrators and also html formatting. If the html body is specified
the mail will be sent as multipart.

Closes-Bug: #1783349

Change-Id: I2b90354c33052c4b7ae3a98a08e7df1055524a25
This commit is contained in:
Jose Castro Leon 2018-07-24 15:10:19 +02:00
parent 33bcd64679
commit 3c430ef0a2
5 changed files with 210 additions and 23 deletions

View File

@ -1056,8 +1056,11 @@ std.email
Sends an email message via SMTP protocol. Sends an email message via SMTP protocol.
- **to_addrs** - Comma separated list of recipients. *Required*. - **to_addrs** - Comma separated list of recipients. *Required*.
- **cc_addrs** - Comma separated list of CC recipients. *Optional*.
- **bcc_addrs** - Comma separated list of BCC recipients. *Optional*.
- **subject** - Subject of the message. *Optional*. - **subject** - Subject of the message. *Optional*.
- **body** - Text containing message body. *Optional*. - **body** - Text containing message body. *Optional*.
- **html_body** - Text containing the message in HTML format. *Optional*.
- **from_addr** - Sender email address. *Required*. - **from_addr** - Sender email address. *Required*.
- **smtp_server** - SMTP server host name. *Required*. - **smtp_server** - SMTP server host name. *Required*.
- **smtp_password** - SMTP server password. *Required*. - **smtp_password** - SMTP server password. *Required*.

View File

@ -14,6 +14,7 @@
# limitations under the License. # limitations under the License.
from email import header from email import header
from email.mime import multipart
from email.mime import text from email.mime import text
import json import json
import smtplib import smtplib
@ -277,15 +278,19 @@ class MistralHTTPAction(HTTPAction):
class SendEmailAction(actions.Action): class SendEmailAction(actions.Action):
def __init__(self, from_addr, to_addrs, smtp_server, def __init__(self, from_addr, to_addrs, smtp_server, cc_addrs=None,
smtp_password=None, subject=None, body=None): bcc_addrs=None, smtp_password=None, subject=None, body=None,
html_body=None):
super(SendEmailAction, self).__init__() super(SendEmailAction, self).__init__()
# TODO(dzimine): validate parameters # TODO(dzimine): validate parameters
# Task invocation parameters. # Task invocation parameters.
self.to = to_addrs self.to = to_addrs
self.cc = cc_addrs or []
self.bcc = bcc_addrs or []
self.subject = subject or "<No subject>" self.subject = subject or "<No subject>"
self.body = body or "<No body>" self.body = body or "<No body>"
self.html_body = html_body
# Action provider settings. # Action provider settings.
self.smtp_server = smtp_server self.smtp_server = smtp_server
@ -295,19 +300,35 @@ class SendEmailAction(actions.Action):
def run(self, context): def run(self, context):
LOG.info( LOG.info(
"Sending email message " "Sending email message "
"[from=%s, to=%s, subject=%s, using smtp=%s, body=%s...]", "[from=%s, to=%s, cc=%s, bcc=%s, subject=%s, using smtp=%s, "
"body=%s...]",
self.sender, self.sender,
self.to, self.to,
self.cc,
self.bcc,
self.subject, self.subject,
self.smtp_server, self.smtp_server,
self.body[:128] self.body[:128]
) )
if not self.html_body:
message = text.MIMEText(self.body, _charset='utf-8') message = text.MIMEText(self.body, _charset='utf-8')
else:
message = multipart.MIMEMultipart('alternative')
message.attach(text.MIMEText(self.body,
'plain',
_charset='utf-8'))
message.attach(text.MIMEText(self.html_body,
'html',
_charset='utf-8'))
message['Subject'] = header.Header(self.subject, 'utf-8') message['Subject'] = header.Header(self.subject, 'utf-8')
message['From'] = self.sender message['From'] = self.sender
message['To'] = ', '.join(self.to) message['To'] = ', '.join(self.to)
if self.cc:
message['cc'] = ', '.join(self.cc)
rcpt = self.cc + self.bcc + self.to
try: try:
s = smtplib.SMTP(self.smtp_server) s = smtplib.SMTP(self.smtp_server)
@ -319,7 +340,7 @@ class SendEmailAction(actions.Action):
s.login(self.sender, self.password) s.login(self.sender, self.password)
s.sendmail(from_addr=self.sender, s.sendmail(from_addr=self.sender,
to_addrs=self.to, to_addrs=rcpt,
msg=message.as_string()) msg=message.as_string())
except (smtplib.SMTPException, IOError) as e: except (smtplib.SMTPException, IOError) as e:
raise exc.ActionException("Failed to send an email message: %s" raise exc.ActionException("Failed to send an email message: %s"
@ -330,9 +351,12 @@ class SendEmailAction(actions.Action):
# to return a result. # to return a result.
LOG.info( LOG.info(
"Sending email message " "Sending email message "
"[from=%s, to=%s, subject=%s, using smtp=%s, body=%s...]", "[from=%s, to=%s, cc=%s, bcc=%s, subject=%s, using smtp=%s, "
"body=%s...]",
self.sender, self.sender,
self.to, self.to,
self.cc,
self.bcc,
self.subject, self.subject,
self.smtp_server, self.smtp_server,
self.body[:128] self.body[:128]

View File

@ -54,8 +54,11 @@ class SendEmailActionTest(base.BaseTest):
super(SendEmailActionTest, self).setUp() super(SendEmailActionTest, self).setUp()
self.to_addrs = ["dz@example.com", "deg@example.com", self.to_addrs = ["dz@example.com", "deg@example.com",
"xyz@example.com"] "xyz@example.com"]
self.cc_addrs = ['copy@example.com']
self.bcc_addrs = ['hidden_copy@example.com']
self.subject = "Multi word subject с русскими буквами" self.subject = "Multi word subject с русскими буквами"
self.body = "short multiline\nbody\nc русскими буквами" self.body = "short multiline\nbody\nc русскими буквами"
self.html_body = '<html><body><b>HTML</b> body</body></html>'
self.smtp_server = 'mail.example.com:25' self.smtp_server = 'mail.example.com:25'
self.from_addr = "bot@example.com" self.from_addr = "bot@example.com"
@ -66,8 +69,12 @@ class SendEmailActionTest(base.BaseTest):
@testtools.skipIf(not LOCAL_SMTPD, "Setup local smtpd to run it") @testtools.skipIf(not LOCAL_SMTPD, "Setup local smtpd to run it")
def test_send_email_real(self): def test_send_email_real(self):
action = std.SendEmailAction( action = std.SendEmailAction(
self.from_addr, self.to_addrs, from_addr=self.from_addr,
self.smtp_server, None, self.subject, self.body to_addrs=self.to_addrs,
smtp_server=self.smtp_server,
smtp_password=None,
subject=self.subject,
body=self.body
) )
action.run(self.ctx) action.run(self.ctx)
@ -79,8 +86,12 @@ class SendEmailActionTest(base.BaseTest):
self.smtp_password = 'secret' self.smtp_password = 'secret'
action = std.SendEmailAction( action = std.SendEmailAction(
self.from_addr, self.to_addrs, from_addr=self.from_addr,
self.smtp_server, self.smtp_password, self.subject, self.body to_addrs=self.to_addrs,
smtp_server=self.smtp_server,
smtp_password=self.smtp_password,
subject=self.subject,
body=self.body
) )
action.run(self.ctx) action.run(self.ctx)
@ -89,8 +100,12 @@ class SendEmailActionTest(base.BaseTest):
def test_with_mutli_to_addrs(self, smtp): def test_with_mutli_to_addrs(self, smtp):
smtp_password = "secret" smtp_password = "secret"
action = std.SendEmailAction( action = std.SendEmailAction(
self.from_addr, self.to_addrs, from_addr=self.from_addr,
self.smtp_server, smtp_password, self.subject, self.body to_addrs=self.to_addrs,
smtp_server=self.smtp_server,
smtp_password=smtp_password,
subject=self.subject,
body=self.body
) )
action.run(self.ctx) action.run(self.ctx)
@ -100,16 +115,24 @@ class SendEmailActionTest(base.BaseTest):
smtp_password = "secret" smtp_password = "secret"
action = std.SendEmailAction( action = std.SendEmailAction(
self.from_addr, to_addr, from_addr=self.from_addr,
self.smtp_server, smtp_password, self.subject, self.body to_addrs=to_addr,
smtp_server=self.smtp_server,
smtp_password=smtp_password,
subject=self.subject,
body=self.body
) )
action.run(self.ctx) action.run(self.ctx)
@mock.patch('smtplib.SMTP') @mock.patch('smtplib.SMTP')
def test_send_email(self, smtp): def test_send_email(self, smtp):
action = std.SendEmailAction( action = std.SendEmailAction(
self.from_addr, self.to_addrs, from_addr=self.from_addr,
self.smtp_server, None, self.subject, self.body to_addrs=self.to_addrs,
smtp_server=self.smtp_server,
smtp_password=None,
subject=self.subject,
body=self.body
) )
action.run(self.ctx) action.run(self.ctx)
@ -149,13 +172,141 @@ class SendEmailActionTest(base.BaseTest):
base64.b64decode(message.get_payload()).decode('utf-8') base64.b64decode(message.get_payload()).decode('utf-8')
) )
@mock.patch('smtplib.SMTP')
def test_send_email_with_cc(self, smtp):
to_addrs = self.cc_addrs + self.to_addrs
cc_addrs_str = ", ".join(self.cc_addrs)
action = std.SendEmailAction(
from_addr=self.from_addr,
to_addrs=self.to_addrs,
cc_addrs=self.cc_addrs,
smtp_server=self.smtp_server,
smtp_password=None,
subject=self.subject,
body=self.body
)
action.run(self.ctx)
smtp.assert_called_once_with(self.smtp_server)
sendmail = smtp.return_value.sendmail
self.assertTrue(sendmail.called, "should call sendmail")
self.assertEqual(
self.from_addr, sendmail.call_args[1]['from_addr'])
self.assertEqual(
to_addrs, sendmail.call_args[1]['to_addrs'])
message = parser.Parser().parsestr(sendmail.call_args[1]['msg'])
self.assertEqual(self.from_addr, message['from'])
self.assertEqual(self.to_addrs_str, message['to'])
self.assertEqual(cc_addrs_str, message['cc'])
@mock.patch('smtplib.SMTP')
def test_send_email_with_bcc(self, smtp):
to_addrs = self.bcc_addrs + self.to_addrs
action = std.SendEmailAction(
from_addr=self.from_addr,
to_addrs=self.to_addrs,
bcc_addrs=self.bcc_addrs,
smtp_server=self.smtp_server,
smtp_password=None,
subject=self.subject,
body=self.body
)
action.run(self.ctx)
smtp.assert_called_once_with(self.smtp_server)
sendmail = smtp.return_value.sendmail
self.assertTrue(sendmail.called, "should call sendmail")
self.assertEqual(
self.from_addr, sendmail.call_args[1]['from_addr'])
self.assertEqual(
to_addrs, sendmail.call_args[1]['to_addrs'])
message = parser.Parser().parsestr(sendmail.call_args[1]['msg'])
self.assertEqual(self.from_addr, message['from'])
self.assertEqual(self.to_addrs_str, message['to'])
@mock.patch('smtplib.SMTP')
def test_send_email_html(self, smtp):
action = std.SendEmailAction(
from_addr=self.from_addr,
to_addrs=self.to_addrs,
smtp_server=self.smtp_server,
smtp_password=None,
subject=self.subject,
body=self.body,
html_body=self.html_body
)
action.run(self.ctx)
smtp.assert_called_once_with(self.smtp_server)
sendmail = smtp.return_value.sendmail
self.assertTrue(sendmail.called, "should call sendmail")
self.assertEqual(
self.from_addr, sendmail.call_args[1]['from_addr'])
self.assertEqual(
self.to_addrs, sendmail.call_args[1]['to_addrs'])
message = parser.Parser().parsestr(sendmail.call_args[1]['msg'])
self.assertEqual(self.from_addr, message['from'])
self.assertEqual(self.to_addrs_str, message['to'])
if six.PY3:
self.assertEqual(
self.subject,
decode_header(message['subject'])[0][0].decode('utf-8')
)
else:
self.assertEqual(
self.subject.decode('utf-8'),
decode_header(message['subject'])[0][0].decode('utf-8')
)
body_payload = message.get_payload(0).get_payload()
if six.PY3:
self.assertEqual(
self.body,
base64.b64decode(body_payload).decode('utf-8')
)
else:
self.assertEqual(
self.body.decode('utf-8'),
base64.b64decode(body_payload).decode('utf-8')
)
html_body_payload = message.get_payload(1).get_payload()
if six.PY3:
self.assertEqual(
self.html_body,
base64.b64decode(html_body_payload).decode('utf-8')
)
else:
self.assertEqual(
self.html_body.decode('utf-8'),
base64.b64decode(html_body_payload).decode('utf-8')
)
@mock.patch('smtplib.SMTP') @mock.patch('smtplib.SMTP')
def test_with_password(self, smtp): def test_with_password(self, smtp):
self.smtp_password = "secret" self.smtp_password = "secret"
action = std.SendEmailAction( action = std.SendEmailAction(
self.from_addr, self.to_addrs, from_addr=self.from_addr,
self.smtp_server, self.smtp_password, self.subject, self.body to_addrs=self.to_addrs,
smtp_server=self.smtp_server,
smtp_password=self.smtp_password,
subject=self.subject,
body=self.body
) )
action.run(self.ctx) action.run(self.ctx)
@ -173,8 +324,12 @@ class SendEmailActionTest(base.BaseTest):
self.smtp_server = "wrong host" self.smtp_server = "wrong host"
action = std.SendEmailAction( action = std.SendEmailAction(
self.from_addr, self.to_addrs, from_addr=self.from_addr,
self.smtp_server, None, self.subject, self.body to_addrs=self.to_addrs,
smtp_server=self.smtp_server,
smtp_password=None,
subject=self.subject,
body=self.body
) )
try: try:

View File

@ -31,8 +31,9 @@ class ActionManagerTest(base.DbTestCase):
self.assertEqual(http_action_input, std_http.input) self.assertEqual(http_action_input, std_http.input)
std_email_input = ( std_email_input = (
"from_addr, to_addrs, smtp_server, " "from_addr, to_addrs, smtp_server, cc_addrs=null, "
"smtp_password=null, subject=null, body=null" "bcc_addrs=null, smtp_password=null, subject=null, body=null, "
"html_body=null"
) )
self.assertEqual(std_email_input, std_email.input) self.assertEqual(std_email_input, std_email.input)

View File

@ -0,0 +1,4 @@
---
features:
- |
Improves std.email action with cc, bcc and html formatting.