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.
- **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*.

View File

@ -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 "<No subject>"
self.body = body or "<No body>"
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]

View File

@ -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><b>HTML</b> body</body></html>'
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:

View File

@ -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)

View File

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