From 3c430ef0a2ae54ae7e42fead98015f6a1d2740e9 Mon Sep 17 00:00:00 2001 From: Jose Castro Leon Date: Tue, 24 Jul 2018 15:10:19 +0200 Subject: [PATCH] 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 --- doc/source/user/wf_lang_v2.rst | 3 + mistral/actions/std_actions.py | 38 +++- .../unit/actions/test_std_email_action.py | 183 ++++++++++++++++-- .../unit/services/test_action_manager.py | 5 +- ...rove_std_html_action-eca10df5bf934be8.yaml | 4 + 5 files changed, 210 insertions(+), 23 deletions(-) create mode 100644 releasenotes/notes/improve_std_html_action-eca10df5bf934be8.yaml diff --git a/doc/source/user/wf_lang_v2.rst b/doc/source/user/wf_lang_v2.rst index 09b990974..73b6f2157 100644 --- a/doc/source/user/wf_lang_v2.rst +++ b/doc/source/user/wf_lang_v2.rst @@ -1056,8 +1056,11 @@ std.email Sends an email message via SMTP protocol. - **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*. - **body** - Text containing message body. *Optional*. +- **html_body** - Text containing the message in HTML format. *Optional*. - **from_addr** - Sender email address. *Required*. - **smtp_server** - SMTP server host name. *Required*. - **smtp_password** - SMTP server password. *Required*. diff --git a/mistral/actions/std_actions.py b/mistral/actions/std_actions.py index 91527e5c9..eab06e66b 100644 --- a/mistral/actions/std_actions.py +++ b/mistral/actions/std_actions.py @@ -14,6 +14,7 @@ # limitations under the License. from email import header +from email.mime import multipart from email.mime import text import json import smtplib @@ -277,15 +278,19 @@ class MistralHTTPAction(HTTPAction): class SendEmailAction(actions.Action): - def __init__(self, from_addr, to_addrs, smtp_server, - smtp_password=None, subject=None, body=None): + def __init__(self, from_addr, to_addrs, smtp_server, cc_addrs=None, + bcc_addrs=None, smtp_password=None, subject=None, body=None, + html_body=None): super(SendEmailAction, self).__init__() # TODO(dzimine): validate parameters # Task invocation parameters. self.to = to_addrs + self.cc = cc_addrs or [] + self.bcc = bcc_addrs or [] self.subject = subject or "" self.body = body or "" + self.html_body = html_body # Action provider settings. self.smtp_server = smtp_server @@ -295,19 +300,35 @@ class SendEmailAction(actions.Action): def run(self, context): LOG.info( "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.to, + self.cc, + self.bcc, self.subject, self.smtp_server, self.body[:128] ) - - message = text.MIMEText(self.body, _charset='utf-8') + if not self.html_body: + 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['From'] = self.sender message['To'] = ', '.join(self.to) + if self.cc: + message['cc'] = ', '.join(self.cc) + + rcpt = self.cc + self.bcc + self.to + try: s = smtplib.SMTP(self.smtp_server) @@ -319,7 +340,7 @@ class SendEmailAction(actions.Action): s.login(self.sender, self.password) s.sendmail(from_addr=self.sender, - to_addrs=self.to, + to_addrs=rcpt, msg=message.as_string()) except (smtplib.SMTPException, IOError) as e: raise exc.ActionException("Failed to send an email message: %s" @@ -330,9 +351,12 @@ class SendEmailAction(actions.Action): # to return a result. LOG.info( "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.to, + self.cc, + self.bcc, self.subject, self.smtp_server, self.body[:128] diff --git a/mistral/tests/unit/actions/test_std_email_action.py b/mistral/tests/unit/actions/test_std_email_action.py index 6eee21a95..020d22dce 100644 --- a/mistral/tests/unit/actions/test_std_email_action.py +++ b/mistral/tests/unit/actions/test_std_email_action.py @@ -54,8 +54,11 @@ class SendEmailActionTest(base.BaseTest): super(SendEmailActionTest, self).setUp() self.to_addrs = ["dz@example.com", "deg@example.com", "xyz@example.com"] + self.cc_addrs = ['copy@example.com'] + self.bcc_addrs = ['hidden_copy@example.com'] self.subject = "Multi word subject с русскими буквами" self.body = "short multiline\nbody\nc русскими буквами" + self.html_body = 'HTML body' self.smtp_server = 'mail.example.com:25' 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") def test_send_email_real(self): action = std.SendEmailAction( - self.from_addr, self.to_addrs, - self.smtp_server, None, self.subject, self.body + from_addr=self.from_addr, + to_addrs=self.to_addrs, + smtp_server=self.smtp_server, + smtp_password=None, + subject=self.subject, + body=self.body ) action.run(self.ctx) @@ -79,8 +86,12 @@ class SendEmailActionTest(base.BaseTest): self.smtp_password = 'secret' action = std.SendEmailAction( - self.from_addr, self.to_addrs, - self.smtp_server, self.smtp_password, self.subject, self.body + from_addr=self.from_addr, + 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) @@ -89,8 +100,12 @@ class SendEmailActionTest(base.BaseTest): def test_with_mutli_to_addrs(self, smtp): smtp_password = "secret" action = std.SendEmailAction( - self.from_addr, self.to_addrs, - self.smtp_server, smtp_password, self.subject, self.body + from_addr=self.from_addr, + to_addrs=self.to_addrs, + smtp_server=self.smtp_server, + smtp_password=smtp_password, + subject=self.subject, + body=self.body ) action.run(self.ctx) @@ -100,16 +115,24 @@ class SendEmailActionTest(base.BaseTest): smtp_password = "secret" action = std.SendEmailAction( - self.from_addr, to_addr, - self.smtp_server, smtp_password, self.subject, self.body + from_addr=self.from_addr, + to_addrs=to_addr, + smtp_server=self.smtp_server, + smtp_password=smtp_password, + subject=self.subject, + body=self.body ) action.run(self.ctx) @mock.patch('smtplib.SMTP') def test_send_email(self, smtp): action = std.SendEmailAction( - self.from_addr, self.to_addrs, - self.smtp_server, None, self.subject, self.body + from_addr=self.from_addr, + to_addrs=self.to_addrs, + smtp_server=self.smtp_server, + smtp_password=None, + subject=self.subject, + body=self.body ) action.run(self.ctx) @@ -149,13 +172,141 @@ class SendEmailActionTest(base.BaseTest): 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') def test_with_password(self, smtp): self.smtp_password = "secret" action = std.SendEmailAction( - self.from_addr, self.to_addrs, - self.smtp_server, self.smtp_password, self.subject, self.body + from_addr=self.from_addr, + 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) @@ -173,8 +324,12 @@ class SendEmailActionTest(base.BaseTest): self.smtp_server = "wrong host" action = std.SendEmailAction( - self.from_addr, self.to_addrs, - self.smtp_server, None, self.subject, self.body + from_addr=self.from_addr, + to_addrs=self.to_addrs, + smtp_server=self.smtp_server, + smtp_password=None, + subject=self.subject, + body=self.body ) try: diff --git a/mistral/tests/unit/services/test_action_manager.py b/mistral/tests/unit/services/test_action_manager.py index 2996df933..198134238 100644 --- a/mistral/tests/unit/services/test_action_manager.py +++ b/mistral/tests/unit/services/test_action_manager.py @@ -31,8 +31,9 @@ class ActionManagerTest(base.DbTestCase): self.assertEqual(http_action_input, std_http.input) std_email_input = ( - "from_addr, to_addrs, smtp_server, " - "smtp_password=null, subject=null, body=null" + "from_addr, to_addrs, smtp_server, cc_addrs=null, " + "bcc_addrs=null, smtp_password=null, subject=null, body=null, " + "html_body=null" ) self.assertEqual(std_email_input, std_email.input) diff --git a/releasenotes/notes/improve_std_html_action-eca10df5bf934be8.yaml b/releasenotes/notes/improve_std_html_action-eca10df5bf934be8.yaml new file mode 100644 index 000000000..6d8724d3b --- /dev/null +++ b/releasenotes/notes/improve_std_html_action-eca10df5bf934be8.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Improves std.email action with cc, bcc and html formatting.