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.