Repository: incubator-airflow Updated Branches: refs/heads/master 29ae02a07 -> c0cf73d27
[AIRFLOW-1914] Add other charset support to email utils The built-in email utils does not support multibyte string content, for example, Japanese or emojis. The fix is to add mime_charset parameter to allow for other values such as `utf-8`. Closes #3308 from wolfier/AIRFLOW-1914 Project: http://git-wip-us.apache.org/repos/asf/incubator-airflow/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-airflow/commit/c0cf73d2 Tree: http://git-wip-us.apache.org/repos/asf/incubator-airflow/tree/c0cf73d2 Diff: http://git-wip-us.apache.org/repos/asf/incubator-airflow/diff/c0cf73d2 Branch: refs/heads/master Commit: c0cf73d27bb764e53f3b61ba66d1d370326e466c Parents: 29ae02a Author: Alan Ma <a...@pandora.com> Authored: Sun May 6 11:16:55 2018 +0200 Committer: Fokko Driesprong <fokkodriespr...@godatadriven.com> Committed: Sun May 6 11:16:55 2018 +0200 ---------------------------------------------------------------------- airflow/operators/email_operator.py | 15 ++++++++++++--- airflow/utils/email.py | 17 +++++++++-------- tests/core.py | 17 +++++++++++++---- 3 files changed, 34 insertions(+), 15 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/c0cf73d2/airflow/operators/email_operator.py ---------------------------------------------------------------------- diff --git a/airflow/operators/email_operator.py b/airflow/operators/email_operator.py index 1087bac..69ed285 100644 --- a/airflow/operators/email_operator.py +++ b/airflow/operators/email_operator.py @@ -7,9 +7,9 @@ # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -39,6 +39,11 @@ class EmailOperator(BaseOperator): :type cc: list or string (comma or semicolon delimited) :param bcc: list of recipients to be added in BCC field :type bcc: list or string (comma or semicolon delimited) + :param mime_subtype: MIME sub content type + :type mime_subtype: string + :param mime_charset: character set parameter added to the Content-Type + header. + :type mime_charset: string """ template_fields = ('to', 'subject', 'html_content') @@ -55,6 +60,7 @@ class EmailOperator(BaseOperator): cc=None, bcc=None, mime_subtype='mixed', + mime_charset='us_ascii', *args, **kwargs): super(EmailOperator, self).__init__(*args, **kwargs) self.to = to @@ -64,6 +70,9 @@ class EmailOperator(BaseOperator): self.cc = cc self.bcc = bcc self.mime_subtype = mime_subtype + self.mime_charset = mime_charset def execute(self, context): - send_email(self.to, self.subject, self.html_content, files=self.files, cc=self.cc, bcc=self.bcc, mime_subtype=self.mime_subtype) + send_email(self.to, self.subject, self.html_content, + files=self.files, cc=self.cc, bcc=self.bcc, + mime_subtype=self.mime_subtype, mine_charset=self.mime_charset) http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/c0cf73d2/airflow/utils/email.py ---------------------------------------------------------------------- diff --git a/airflow/utils/email.py b/airflow/utils/email.py index afe3f28..b37e3d4 100644 --- a/airflow/utils/email.py +++ b/airflow/utils/email.py @@ -7,9 +7,9 @@ # to you under the Apache License, Version 2.0 (the # "License"); you may not use this file except in compliance # with the License. You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, # software distributed under the License is distributed on an # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY @@ -39,9 +39,9 @@ from airflow.exceptions import AirflowConfigException from airflow.utils.log.logging_mixin import LoggingMixin -def send_email(to, subject, html_content, files=None, - dryrun=False, cc=None, bcc=None, - mime_subtype='mixed', **kwargs): +def send_email(to, subject, html_content, + files=None, dryrun=False, cc=None, bcc=None, + mime_subtype='mixed', mime_charset='us-ascii', **kwargs): """ Send email using backend specified in EMAIL_BACKEND. """ @@ -50,12 +50,13 @@ def send_email(to, subject, html_content, files=None, backend = getattr(module, attr) return backend(to, subject, html_content, files=files, dryrun=dryrun, cc=cc, bcc=bcc, - mime_subtype=mime_subtype, **kwargs) + mime_subtype=mime_subtype, mime_charset=mime_charset, **kwargs) def send_email_smtp(to, subject, html_content, files=None, dryrun=False, cc=None, bcc=None, - mime_subtype='mixed', **kwargs): + mime_subtype='mixed', mime_charset='us-ascii', + **kwargs): """ Send an email with html content @@ -81,7 +82,7 @@ def send_email_smtp(to, subject, html_content, files=None, recipients = recipients + bcc msg['Date'] = formatdate(localtime=True) - mime_text = MIMEText(html_content, 'html') + mime_text = MIMEText(html_content, 'html', mime_charset) msg.attach(mime_text) for fname in files or []: http://git-wip-us.apache.org/repos/asf/incubator-airflow/blob/c0cf73d2/tests/core.py ---------------------------------------------------------------------- diff --git a/tests/core.py b/tests/core.py index 4cece16..5ab2e94 100644 --- a/tests/core.py +++ b/tests/core.py @@ -37,6 +37,7 @@ from datetime import timedelta from dateutil.relativedelta import relativedelta from email.mime.application import MIMEApplication from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText from freezegun import freeze_time from numpy.testing import assert_array_almost_equal from six.moves.urllib.parse import urlencode @@ -2409,8 +2410,7 @@ class EmailTest(unittest.TestCase): utils.email.send_email('to', 'subject', 'content') send_email_test.assert_called_with( 'to', 'subject', 'content', files=None, dryrun=False, - cc=None, bcc=None, mime_subtype='mixed' - ) + cc=None, bcc=None, mime_charset='us-ascii', mime_subtype='mixed') self.assertFalse(mock_send_email.called) @@ -2432,12 +2432,21 @@ class EmailSmtpTest(unittest.TestCase): self.assertEqual('subject', msg['Subject']) self.assertEqual(configuration.conf.get('smtp', 'SMTP_MAIL_FROM'), msg['From']) self.assertEqual(2, len(msg.get_payload())) - self.assertEqual(u'attachment; filename="' + os.path.basename(attachment.name) + '"', - msg.get_payload()[-1].get(u'Content-Disposition')) + filename = u'attachment; filename="' + os.path.basename(attachment.name) + '"' + self.assertEqual(filename, msg.get_payload()[-1].get(u'Content-Disposition')) mimeapp = MIMEApplication('attachment') self.assertEqual(mimeapp.get_payload(), msg.get_payload()[-1].get_payload()) @mock.patch('airflow.utils.email.send_MIME_email') + def test_send_smtp_with_multibyte_content(self, mock_send_mime): + utils.email.send_email_smtp('to', 'subject', 'ð¥', mime_charset='utf-8') + self.assertTrue(mock_send_mime.called) + call_args = mock_send_mime.call_args[0] + msg = call_args[2] + mimetext = MIMEText('ð¥', 'mixed', 'utf-8') + self.assertEqual(mimetext.get_payload(), msg.get_payload()[0].get_payload()) + + @mock.patch('airflow.utils.email.send_MIME_email') def test_send_bcc_smtp(self, mock_send_mime): attachment = tempfile.NamedTemporaryFile() attachment.write(b'attachment')