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

Reply via email to