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