Browse Source

Merge "Improve std.email action"

Zuul 8 months ago
parent
commit
bef9aa5bc3

+ 3
- 0
doc/source/user/wf_lang_v2.rst View File

@@ -1056,8 +1056,11 @@ std.email
1056 1056
 Sends an email message via SMTP protocol.
1057 1057
 
1058 1058
 -  **to_addrs** - Comma separated list of recipients. *Required*.
1059
+-  **cc_addrs** - Comma separated list of CC recipients. *Optional*.
1060
+-  **bcc_addrs** - Comma separated list of BCC recipients. *Optional*.
1059 1061
 -  **subject** - Subject of the message. *Optional*.
1060 1062
 -  **body** - Text containing message body. *Optional*.
1063
+-  **html_body** - Text containing the message in HTML format. *Optional*.
1061 1064
 -  **from_addr** - Sender email address. *Required*.
1062 1065
 -  **smtp_server** - SMTP server host name. *Required*.
1063 1066
 -  **smtp_password** - SMTP server password. *Required*.

+ 31
- 7
mistral/actions/std_actions.py View File

@@ -14,6 +14,7 @@
14 14
 #    limitations under the License.
15 15
 
16 16
 from email import header
17
+from email.mime import multipart
17 18
 from email.mime import text
18 19
 import json
19 20
 import smtplib
@@ -277,15 +278,19 @@ class MistralHTTPAction(HTTPAction):
277 278
 
278 279
 
279 280
 class SendEmailAction(actions.Action):
280
-    def __init__(self, from_addr, to_addrs, smtp_server,
281
-                 smtp_password=None, subject=None, body=None):
281
+    def __init__(self, from_addr, to_addrs, smtp_server, cc_addrs=None,
282
+                 bcc_addrs=None, smtp_password=None, subject=None, body=None,
283
+                 html_body=None):
282 284
         super(SendEmailAction, self).__init__()
283 285
         # TODO(dzimine): validate parameters
284 286
 
285 287
         # Task invocation parameters.
286 288
         self.to = to_addrs
289
+        self.cc = cc_addrs or []
290
+        self.bcc = bcc_addrs or []
287 291
         self.subject = subject or "<No subject>"
288 292
         self.body = body or "<No body>"
293
+        self.html_body = html_body
289 294
 
290 295
         # Action provider settings.
291 296
         self.smtp_server = smtp_server
@@ -295,19 +300,35 @@ class SendEmailAction(actions.Action):
295 300
     def run(self, context):
296 301
         LOG.info(
297 302
             "Sending email message "
298
-            "[from=%s, to=%s, subject=%s, using smtp=%s, body=%s...]",
303
+            "[from=%s, to=%s, cc=%s, bcc=%s, subject=%s, using smtp=%s, "
304
+            "body=%s...]",
299 305
             self.sender,
300 306
             self.to,
307
+            self.cc,
308
+            self.bcc,
301 309
             self.subject,
302 310
             self.smtp_server,
303 311
             self.body[:128]
304 312
         )
305
-
306
-        message = text.MIMEText(self.body, _charset='utf-8')
313
+        if not self.html_body:
314
+            message = text.MIMEText(self.body, _charset='utf-8')
315
+        else:
316
+            message = multipart.MIMEMultipart('alternative')
317
+            message.attach(text.MIMEText(self.body,
318
+                                         'plain',
319
+                                         _charset='utf-8'))
320
+            message.attach(text.MIMEText(self.html_body,
321
+                                         'html',
322
+                                         _charset='utf-8'))
307 323
         message['Subject'] = header.Header(self.subject, 'utf-8')
308 324
         message['From'] = self.sender
309 325
         message['To'] = ', '.join(self.to)
310 326
 
327
+        if self.cc:
328
+            message['cc'] = ', '.join(self.cc)
329
+
330
+        rcpt = self.cc + self.bcc + self.to
331
+
311 332
         try:
312 333
             s = smtplib.SMTP(self.smtp_server)
313 334
 
@@ -319,7 +340,7 @@ class SendEmailAction(actions.Action):
319 340
                 s.login(self.sender, self.password)
320 341
 
321 342
             s.sendmail(from_addr=self.sender,
322
-                       to_addrs=self.to,
343
+                       to_addrs=rcpt,
323 344
                        msg=message.as_string())
324 345
         except (smtplib.SMTPException, IOError) as e:
325 346
             raise exc.ActionException("Failed to send an email message: %s"
@@ -330,9 +351,12 @@ class SendEmailAction(actions.Action):
330 351
         # to return a result.
331 352
         LOG.info(
332 353
             "Sending email message "
333
-            "[from=%s, to=%s, subject=%s, using smtp=%s, body=%s...]",
354
+            "[from=%s, to=%s, cc=%s, bcc=%s, subject=%s, using smtp=%s, "
355
+            "body=%s...]",
334 356
             self.sender,
335 357
             self.to,
358
+            self.cc,
359
+            self.bcc,
336 360
             self.subject,
337 361
             self.smtp_server,
338 362
             self.body[:128]

+ 169
- 14
mistral/tests/unit/actions/test_std_email_action.py View File

@@ -54,8 +54,11 @@ class SendEmailActionTest(base.BaseTest):
54 54
         super(SendEmailActionTest, self).setUp()
55 55
         self.to_addrs = ["dz@example.com", "deg@example.com",
56 56
                          "xyz@example.com"]
57
+        self.cc_addrs = ['copy@example.com']
58
+        self.bcc_addrs = ['hidden_copy@example.com']
57 59
         self.subject = "Multi word subject с русскими буквами"
58 60
         self.body = "short multiline\nbody\nc русскими буквами"
61
+        self.html_body = '<html><body><b>HTML</b> body</body></html>'
59 62
 
60 63
         self.smtp_server = 'mail.example.com:25'
61 64
         self.from_addr = "bot@example.com"
@@ -66,8 +69,12 @@ class SendEmailActionTest(base.BaseTest):
66 69
     @testtools.skipIf(not LOCAL_SMTPD, "Setup local smtpd to run it")
67 70
     def test_send_email_real(self):
68 71
         action = std.SendEmailAction(
69
-            self.from_addr, self.to_addrs,
70
-            self.smtp_server, None, self.subject, self.body
72
+            from_addr=self.from_addr,
73
+            to_addrs=self.to_addrs,
74
+            smtp_server=self.smtp_server,
75
+            smtp_password=None,
76
+            subject=self.subject,
77
+            body=self.body
71 78
         )
72 79
         action.run(self.ctx)
73 80
 
@@ -79,8 +86,12 @@ class SendEmailActionTest(base.BaseTest):
79 86
         self.smtp_password = 'secret'
80 87
 
81 88
         action = std.SendEmailAction(
82
-            self.from_addr, self.to_addrs,
83
-            self.smtp_server, self.smtp_password, self.subject, self.body
89
+            from_addr=self.from_addr,
90
+            to_addrs=self.to_addrs,
91
+            smtp_server=self.smtp_server,
92
+            smtp_password=self.smtp_password,
93
+            subject=self.subject,
94
+            body=self.body
84 95
         )
85 96
 
86 97
         action.run(self.ctx)
@@ -89,8 +100,12 @@ class SendEmailActionTest(base.BaseTest):
89 100
     def test_with_mutli_to_addrs(self, smtp):
90 101
         smtp_password = "secret"
91 102
         action = std.SendEmailAction(
92
-            self.from_addr, self.to_addrs,
93
-            self.smtp_server, smtp_password, self.subject, self.body
103
+            from_addr=self.from_addr,
104
+            to_addrs=self.to_addrs,
105
+            smtp_server=self.smtp_server,
106
+            smtp_password=smtp_password,
107
+            subject=self.subject,
108
+            body=self.body
94 109
         )
95 110
         action.run(self.ctx)
96 111
 
@@ -100,16 +115,24 @@ class SendEmailActionTest(base.BaseTest):
100 115
         smtp_password = "secret"
101 116
 
102 117
         action = std.SendEmailAction(
103
-            self.from_addr, to_addr,
104
-            self.smtp_server, smtp_password, self.subject, self.body
118
+            from_addr=self.from_addr,
119
+            to_addrs=to_addr,
120
+            smtp_server=self.smtp_server,
121
+            smtp_password=smtp_password,
122
+            subject=self.subject,
123
+            body=self.body
105 124
         )
106 125
         action.run(self.ctx)
107 126
 
108 127
     @mock.patch('smtplib.SMTP')
109 128
     def test_send_email(self, smtp):
110 129
         action = std.SendEmailAction(
111
-            self.from_addr, self.to_addrs,
112
-            self.smtp_server, None, self.subject, self.body
130
+            from_addr=self.from_addr,
131
+            to_addrs=self.to_addrs,
132
+            smtp_server=self.smtp_server,
133
+            smtp_password=None,
134
+            subject=self.subject,
135
+            body=self.body
113 136
         )
114 137
 
115 138
         action.run(self.ctx)
@@ -149,13 +172,141 @@ class SendEmailActionTest(base.BaseTest):
149 172
                 base64.b64decode(message.get_payload()).decode('utf-8')
150 173
             )
151 174
 
175
+    @mock.patch('smtplib.SMTP')
176
+    def test_send_email_with_cc(self, smtp):
177
+        to_addrs = self.cc_addrs + self.to_addrs
178
+        cc_addrs_str = ", ".join(self.cc_addrs)
179
+
180
+        action = std.SendEmailAction(
181
+            from_addr=self.from_addr,
182
+            to_addrs=self.to_addrs,
183
+            cc_addrs=self.cc_addrs,
184
+            smtp_server=self.smtp_server,
185
+            smtp_password=None,
186
+            subject=self.subject,
187
+            body=self.body
188
+        )
189
+
190
+        action.run(self.ctx)
191
+
192
+        smtp.assert_called_once_with(self.smtp_server)
193
+
194
+        sendmail = smtp.return_value.sendmail
195
+
196
+        self.assertTrue(sendmail.called, "should call sendmail")
197
+        self.assertEqual(
198
+            self.from_addr, sendmail.call_args[1]['from_addr'])
199
+        self.assertEqual(
200
+            to_addrs, sendmail.call_args[1]['to_addrs'])
201
+
202
+        message = parser.Parser().parsestr(sendmail.call_args[1]['msg'])
203
+
204
+        self.assertEqual(self.from_addr, message['from'])
205
+        self.assertEqual(self.to_addrs_str, message['to'])
206
+        self.assertEqual(cc_addrs_str, message['cc'])
207
+
208
+    @mock.patch('smtplib.SMTP')
209
+    def test_send_email_with_bcc(self, smtp):
210
+        to_addrs = self.bcc_addrs + self.to_addrs
211
+        action = std.SendEmailAction(
212
+            from_addr=self.from_addr,
213
+            to_addrs=self.to_addrs,
214
+            bcc_addrs=self.bcc_addrs,
215
+            smtp_server=self.smtp_server,
216
+            smtp_password=None,
217
+            subject=self.subject,
218
+            body=self.body
219
+        )
220
+
221
+        action.run(self.ctx)
222
+
223
+        smtp.assert_called_once_with(self.smtp_server)
224
+
225
+        sendmail = smtp.return_value.sendmail
226
+
227
+        self.assertTrue(sendmail.called, "should call sendmail")
228
+        self.assertEqual(
229
+            self.from_addr, sendmail.call_args[1]['from_addr'])
230
+        self.assertEqual(
231
+            to_addrs, sendmail.call_args[1]['to_addrs'])
232
+
233
+        message = parser.Parser().parsestr(sendmail.call_args[1]['msg'])
234
+
235
+        self.assertEqual(self.from_addr, message['from'])
236
+        self.assertEqual(self.to_addrs_str, message['to'])
237
+
238
+    @mock.patch('smtplib.SMTP')
239
+    def test_send_email_html(self, smtp):
240
+        action = std.SendEmailAction(
241
+            from_addr=self.from_addr,
242
+            to_addrs=self.to_addrs,
243
+            smtp_server=self.smtp_server,
244
+            smtp_password=None,
245
+            subject=self.subject,
246
+            body=self.body,
247
+            html_body=self.html_body
248
+        )
249
+
250
+        action.run(self.ctx)
251
+
252
+        smtp.assert_called_once_with(self.smtp_server)
253
+
254
+        sendmail = smtp.return_value.sendmail
255
+
256
+        self.assertTrue(sendmail.called, "should call sendmail")
257
+        self.assertEqual(
258
+            self.from_addr, sendmail.call_args[1]['from_addr'])
259
+        self.assertEqual(
260
+            self.to_addrs, sendmail.call_args[1]['to_addrs'])
261
+
262
+        message = parser.Parser().parsestr(sendmail.call_args[1]['msg'])
263
+
264
+        self.assertEqual(self.from_addr, message['from'])
265
+        self.assertEqual(self.to_addrs_str, message['to'])
266
+        if six.PY3:
267
+            self.assertEqual(
268
+                self.subject,
269
+                decode_header(message['subject'])[0][0].decode('utf-8')
270
+            )
271
+        else:
272
+            self.assertEqual(
273
+                self.subject.decode('utf-8'),
274
+                decode_header(message['subject'])[0][0].decode('utf-8')
275
+            )
276
+        body_payload = message.get_payload(0).get_payload()
277
+        if six.PY3:
278
+            self.assertEqual(
279
+                self.body,
280
+                base64.b64decode(body_payload).decode('utf-8')
281
+            )
282
+        else:
283
+            self.assertEqual(
284
+                self.body.decode('utf-8'),
285
+                base64.b64decode(body_payload).decode('utf-8')
286
+            )
287
+        html_body_payload = message.get_payload(1).get_payload()
288
+        if six.PY3:
289
+            self.assertEqual(
290
+                self.html_body,
291
+                base64.b64decode(html_body_payload).decode('utf-8')
292
+            )
293
+        else:
294
+            self.assertEqual(
295
+                self.html_body.decode('utf-8'),
296
+                base64.b64decode(html_body_payload).decode('utf-8')
297
+            )
298
+
152 299
     @mock.patch('smtplib.SMTP')
153 300
     def test_with_password(self, smtp):
154 301
         self.smtp_password = "secret"
155 302
 
156 303
         action = std.SendEmailAction(
157
-            self.from_addr, self.to_addrs,
158
-            self.smtp_server, self.smtp_password, self.subject, self.body
304
+            from_addr=self.from_addr,
305
+            to_addrs=self.to_addrs,
306
+            smtp_server=self.smtp_server,
307
+            smtp_password=self.smtp_password,
308
+            subject=self.subject,
309
+            body=self.body
159 310
         )
160 311
 
161 312
         action.run(self.ctx)
@@ -173,8 +324,12 @@ class SendEmailActionTest(base.BaseTest):
173 324
         self.smtp_server = "wrong host"
174 325
 
175 326
         action = std.SendEmailAction(
176
-            self.from_addr, self.to_addrs,
177
-            self.smtp_server, None, self.subject, self.body
327
+            from_addr=self.from_addr,
328
+            to_addrs=self.to_addrs,
329
+            smtp_server=self.smtp_server,
330
+            smtp_password=None,
331
+            subject=self.subject,
332
+            body=self.body
178 333
         )
179 334
 
180 335
         try:

+ 3
- 2
mistral/tests/unit/services/test_action_manager.py View File

@@ -31,8 +31,9 @@ class ActionManagerTest(base.DbTestCase):
31 31
         self.assertEqual(http_action_input, std_http.input)
32 32
 
33 33
         std_email_input = (
34
-            "from_addr, to_addrs, smtp_server, "
35
-            "smtp_password=null, subject=null, body=null"
34
+            "from_addr, to_addrs, smtp_server, cc_addrs=null, "
35
+            "bcc_addrs=null, smtp_password=null, subject=null, body=null, "
36
+            "html_body=null"
36 37
         )
37 38
 
38 39
         self.assertEqual(std_email_input, std_email.input)

+ 4
- 0
releasenotes/notes/improve_std_html_action-eca10df5bf934be8.yaml View File

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

Loading…
Cancel
Save