Repository: scrapy/scrapy
Pull request #1662 (2016-01-06 21:27:49)

[MRG+1] MailSender.send: allow passing a charset.
Resolves Issue #348.

The default behavior remains the same as before, but it is now possible to send utf-8 emails.

codecov-io: ## [Current coverage][1] is `82.93%`
> Merging **#1662** into **master** will not affect coverage as of [`1e2d29f`][3]



[1]: https://codecov.io/github/scrapy/scrapy?ref=1e2d29f39c89f4959390b490c27987042f9f65e6
[2]: https://codecov.io/github/scrapy/scrapy/features/suggestions?ref=1e2d29f39c89f4959390b490c27987042f9f65e6
[3]: https://codecov.io/github/scrapy/scrapy/commit/1e2d29f39c89f4959390b490c27987042f9f65e6
[4]: https://codecov.io/github/scrapy/scrapy/compare/95e8ff8ba1dff3ec045dce931b6ea4314e887399...1e2d29f39c89f4959390b490c27987042f9f65e6

> Powered by [Codecov](https://codecov.io). Updated on successful CI builds.

Patch:
diff --git a/docs/topics/email.rst b/docs/topics/email.rst
index 789fbd4fb..96487d865 100644
--- a/docs/topics/email.rst
+++ b/docs/topics/email.rst
@@ -76,7 +76,7 @@ uses `Twisted non-blocking IO`_, like the rest of the framework.
         :param settings: the e-mail recipients
         :type settings: :class:`scrapy.settings.Settings` object
 
-    .. method:: send(to, subject, body, cc=None, attachs=(), mimetype='text/plain')
+    .. method:: send(to, subject, body, cc=None, attachs=(), mimetype='text/plain', charset=None)
 
         Send email to the given recipients.
 
@@ -102,6 +102,9 @@ uses `Twisted non-blocking IO`_, like the rest of the framework.
         :param mimetype: the MIME type of the e-mail
         :type mimetype: str
 
+        :param charset: the character encoding to use for the e-mail contents
+        :type charset: str
+
 
 .. _topics-email-settings:
 
diff --git a/scrapy/mail.py b/scrapy/mail.py
index ad8ecbe13..c6339f25b 100644
--- a/scrapy/mail.py
+++ b/scrapy/mail.py
@@ -43,7 +43,7 @@ class MailSender(object):
             settings['MAIL_PASS'], settings.getint('MAIL_PORT'),
             settings.getbool('MAIL_TLS'), settings.getbool('MAIL_SSL'))
 
-    def send(self, to, subject, body, cc=None, attachs=(), mimetype='text/plain', _callback=None):
+    def send(self, to, subject, body, cc=None, attachs=(), mimetype='text/plain', charset=None, _callback=None):
         if attachs:
             msg = MIMEMultipart()
         else:
@@ -57,8 +57,11 @@ class MailSender(object):
             rcpts.extend(cc)
             msg['Cc'] = COMMASPACE.join(cc)
 
+        if charset:
+            msg.set_charset(charset)
+
         if attachs:
-            msg.attach(MIMEText(body))
+            msg.attach(MIMEText(body, 'plain', charset or 'us-ascii'))
             for attach_name, mimetype, f in attachs:
                 part = MIMEBase(*mimetype.split('/'))
                 part.set_payload(f.read())


Test patch:
diff --git a/tests/test_mail.py b/tests/test_mail.py
index 25dd35099..bd7e49621 100644
--- a/tests/test_mail.py
+++ b/tests/test_mail.py
@@ -1,5 +1,8 @@
+# coding=utf-8
+
 import unittest
 from io import BytesIO
+from email.charset import Charset
 
 from scrapy.mail import MailSender
 
@@ -54,11 +57,58 @@ class MailSenderTest(unittest.TestCase):
 
         text, attach = payload
         self.assertEqual(text.get_payload(decode=True), b'body')
+        self.assertEqual(text.get_charset(), Charset('us-ascii'))
         self.assertEqual(attach.get_payload(decode=True), b'content')
 
     def _catch_mail_sent(self, **kwargs):
         self.catched_msg = dict(**kwargs)
 
+    def test_send_utf8(self):
+        subject = u'sübjèçt'
+        body = u'bödÿ-àéïöñß'
+        mailsender = MailSender(debug=True)
+        mailsender.send(to=['test@scrapy.org'], subject=subject, body=body,
+                        charset='utf-8', _callback=self._catch_mail_sent)
+
+        assert self.catched_msg
+        self.assertEqual(self.catched_msg['subject'], subject)
+        self.assertEqual(self.catched_msg['body'], body)
+
+        msg = self.catched_msg['msg']
+        self.assertEqual(msg['subject'], subject)
+        self.assertEqual(msg.get_payload(), body)
+        self.assertEqual(msg.get_charset(), Charset('utf-8'))
+        self.assertEqual(msg.get('Content-Type'), 'text/plain; charset="utf-8"')
+
+    def test_send_attach_utf8(self):
+        subject = u'sübjèçt'
+        body = u'bödÿ-àéïöñß'
+        attach = BytesIO()
+        attach.write(body.encode('utf-8'))
+        attach.seek(0)
+        attachs = [('attachment', 'text/plain', attach)]
+
+        mailsender = MailSender(debug=True)
+        mailsender.send(to=['test@scrapy.org'], subject=subject, body=body,
+                        attachs=attachs, charset='utf-8', _callback=self._catch_mail_sent)
+
+        assert self.catched_msg
+        self.assertEqual(self.catched_msg['subject'], subject)
+        self.assertEqual(self.catched_msg['body'], body)
+
+        msg = self.catched_msg['msg']
+        self.assertEqual(msg['subject'], subject)
+        self.assertEqual(msg.get_charset(), Charset('utf-8'))
+        self.assertEqual(msg.get('Content-Type'), 'multipart/mixed; charset="utf-8"')
+
+        payload = msg.get_payload()
+        assert isinstance(payload, list)
+        self.assertEqual(len(payload), 2)
+
+        text, attach = payload
+        self.assertEqual(text.get_payload(decode=True).decode('utf-8'), body)
+        self.assertEqual(text.get_charset(), Charset('utf-8'))
+        self.assertEqual(attach.get_payload(decode=True).decode('utf-8'), body)
 
 if __name__ == "__main__":
     unittest.main()