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:
parent
33bcd64679
commit
3c430ef0a2
@ -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*.
|
||||
|
@ -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]
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -0,0 +1,4 @@
|
||||
---
|
||||
features:
|
||||
- |
|
||||
Improves std.email action with cc, bcc and html formatting.
|
Loading…
Reference in New Issue
Block a user