#24218: Add SMTPUTF8 support to send_message.

Reviewed by Maciej Szulik.
This commit is contained in:
R David Murray 2015-05-17 19:27:22 -04:00
parent 740d6134f1
commit 8308444eef
4 changed files with 86 additions and 8 deletions

View File

@ -467,7 +467,7 @@ An :class:`SMTP` instance has the following methods:
If *from_addr* is ``None`` or *to_addrs* is ``None``, ``send_message`` fills If *from_addr* is ``None`` or *to_addrs* is ``None``, ``send_message`` fills
those arguments with addresses extracted from the headers of *msg* as those arguments with addresses extracted from the headers of *msg* as
specified in :rfc:`2822`\: *from_addr* is set to the :mailheader:`Sender` specified in :rfc:`5322`\: *from_addr* is set to the :mailheader:`Sender`
field if it is present, and otherwise to the :mailheader:`From` field. field if it is present, and otherwise to the :mailheader:`From` field.
*to_adresses* combines the values (if any) of the :mailheader:`To`, *to_adresses* combines the values (if any) of the :mailheader:`To`,
:mailheader:`Cc`, and :mailheader:`Bcc` fields from *msg*. If exactly one :mailheader:`Cc`, and :mailheader:`Bcc` fields from *msg*. If exactly one
@ -482,10 +482,18 @@ An :class:`SMTP` instance has the following methods:
calls :meth:`sendmail` to transmit the resulting message. Regardless of the calls :meth:`sendmail` to transmit the resulting message. Regardless of the
values of *from_addr* and *to_addrs*, ``send_message`` does not transmit any values of *from_addr* and *to_addrs*, ``send_message`` does not transmit any
:mailheader:`Bcc` or :mailheader:`Resent-Bcc` headers that may appear :mailheader:`Bcc` or :mailheader:`Resent-Bcc` headers that may appear
in *msg*. in *msg*. If any of the addresses in *from_addr* and *to_addrs* contain
non-ASCII characters and the server does not advertise ``SMTPUTF8`` support,
an :exc:`SMTPNotSupported` error is raised. Otherwise the ``Message`` is
serialized with a clone of its :mod:`~email.policy` with the
:attr:`~email.policy.EmailPolicy.utf8` attribute set to ``True``, and
``SMTPUTF8`` and ``BODY=8BITMIME`` are added to *mail_options*.
.. versionadded:: 3.2 .. versionadded:: 3.2
.. versionadded:: 3.5
Support for internationalized addresses (``SMTPUTF8``).
.. method:: SMTP.quit() .. method:: SMTP.quit()

View File

@ -557,8 +557,10 @@ smtplib
:class:`smtplib.SMTP`. (Contributed by Gavin Chappell and Maciej Szulik in :class:`smtplib.SMTP`. (Contributed by Gavin Chappell and Maciej Szulik in
:issue:`16914`.) :issue:`16914`.)
* :mod:`smtplib` now support :rfc:`6531` (SMTPUTF8). (Contributed by * :mod:`smtplib` now supports :rfc:`6531` (SMTPUTF8) in both the
Milan Oberkirch and R. David Murray in :issue:`22027`.) :meth:`~smtplib.SMTP.sendmail` and :meth:`~smtplib.SMTP.send_message`
commands. (Contributed by Milan Oberkirch and R. David Murray in
:issue:`22027`.)
sndhdr sndhdr
------ ------

View File

@ -872,7 +872,13 @@ class SMTP:
to_addr, any Bcc field (or Resent-Bcc field, when the Message is a to_addr, any Bcc field (or Resent-Bcc field, when the Message is a
resent) of the Message object won't be transmitted. The Message resent) of the Message object won't be transmitted. The Message
object is then serialized using email.generator.BytesGenerator and object is then serialized using email.generator.BytesGenerator and
sendmail is called to transmit the message. sendmail is called to transmit the message. If the sender or any of
the recipient addresses contain non-ASCII and the server advertises the
SMTPUTF8 capability, the policy is cloned with utf8 set to True for the
serialization, and SMTPUTF8 and BODY=8BITMIME are asserted on the send.
If the server does not support SMTPUTF8, an SMPTNotSupported error is
raised. Otherwise the generator is called without modifying the
policy.
""" """
# 'Resent-Date' is a mandatory field if the Message is resent (RFC 2822 # 'Resent-Date' is a mandatory field if the Message is resent (RFC 2822
@ -885,6 +891,7 @@ class SMTP:
# option allowing the user to enable the heuristics. (It should be # option allowing the user to enable the heuristics. (It should be
# possible to guess correctly almost all of the time.) # possible to guess correctly almost all of the time.)
self.ehlo_or_helo_if_needed()
resent = msg.get_all('Resent-Date') resent = msg.get_all('Resent-Date')
if resent is None: if resent is None:
header_prefix = '' header_prefix = ''
@ -900,13 +907,29 @@ class SMTP:
if to_addrs is None: if to_addrs is None:
addr_fields = [f for f in (msg[header_prefix + 'To'], addr_fields = [f for f in (msg[header_prefix + 'To'],
msg[header_prefix + 'Bcc'], msg[header_prefix + 'Bcc'],
msg[header_prefix + 'Cc']) if f is not None] msg[header_prefix + 'Cc'])
if f is not None]
to_addrs = [a[1] for a in email.utils.getaddresses(addr_fields)] to_addrs = [a[1] for a in email.utils.getaddresses(addr_fields)]
# Make a local copy so we can delete the bcc headers. # Make a local copy so we can delete the bcc headers.
msg_copy = copy.copy(msg) msg_copy = copy.copy(msg)
del msg_copy['Bcc'] del msg_copy['Bcc']
del msg_copy['Resent-Bcc'] del msg_copy['Resent-Bcc']
international = False
try:
''.join([from_addr, *to_addrs]).encode('ascii')
except UnicodeEncodeError:
if not self.has_extn('smtputf8'):
raise SMTPNotSupportedError(
"One or more source or delivery addresses require"
" internationalized email support, but the server"
" does not advertise the required SMTPUTF8 capability")
international = True
with io.BytesIO() as bytesmsg: with io.BytesIO() as bytesmsg:
if international:
g = email.generator.BytesGenerator(
bytesmsg, policy=msg.policy.clone(utf8=True))
mail_options += ['SMTPUTF8', 'BODY=8BITMIME']
else:
g = email.generator.BytesGenerator(bytesmsg) g = email.generator.BytesGenerator(bytesmsg)
g.flatten(msg_copy, linesep='\r\n') g.flatten(msg_copy, linesep='\r\n')
flatmsg = bytesmsg.getvalue() flatmsg = bytesmsg.getvalue()

View File

@ -1,5 +1,6 @@
import asyncore import asyncore
import email.mime.text import email.mime.text
from email.message import EmailMessage
import email.utils import email.utils
import socket import socket
import smtpd import smtpd
@ -10,7 +11,7 @@ import sys
import time import time
import select import select
import errno import errno
import base64 import textwrap
import unittest import unittest
from test import support, mock_socket from test import support, mock_socket
@ -1029,6 +1030,8 @@ class SimSMTPUTF8Server(SimSMTPServer):
@unittest.skipUnless(threading, 'Threading required for this test.') @unittest.skipUnless(threading, 'Threading required for this test.')
class SMTPUTF8SimTests(unittest.TestCase): class SMTPUTF8SimTests(unittest.TestCase):
maxDiff = None
def setUp(self): def setUp(self):
self.real_getfqdn = socket.getfqdn self.real_getfqdn = socket.getfqdn
socket.getfqdn = mock_socket.getfqdn socket.getfqdn = mock_socket.getfqdn
@ -1096,6 +1099,48 @@ class SMTPUTF8SimTests(unittest.TestCase):
self.assertIn('SMTPUTF8', self.serv.last_mail_options) self.assertIn('SMTPUTF8', self.serv.last_mail_options)
self.assertEqual(self.serv.last_rcpt_options, []) self.assertEqual(self.serv.last_rcpt_options, [])
def test_send_message_uses_smtputf8_if_addrs_non_ascii(self):
msg = EmailMessage()
msg['From'] = "Páolo <főo@bar.com>"
msg['To'] = 'Dinsdale'
msg['Subject'] = 'Nudge nudge, wink, wink \u1F609'
# XXX I don't know why I need two \n's here, but this is an existing
# bug (if it is one) and not a problem with the new functionality.
msg.set_content("oh là là, know what I mean, know what I mean?\n\n")
# XXX smtpd converts received /r/n to /n, so we can't easily test that
# we are successfully sending /r/n :(.
expected = textwrap.dedent("""\
From: Páolo <főo@bar.com>
To: Dinsdale
Subject: Nudge nudge, wink, wink \u1F609
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit
MIME-Version: 1.0
oh , know what I mean, know what I mean?
""")
smtp = smtplib.SMTP(
HOST, self.port, local_hostname='localhost', timeout=3)
self.addCleanup(smtp.close)
self.assertEqual(smtp.send_message(msg), {})
self.assertEqual(self.serv.last_mailfrom, 'főo@bar.com')
self.assertEqual(self.serv.last_rcpttos, ['Dinsdale'])
self.assertEqual(self.serv.last_message.decode(), expected)
self.assertIn('BODY=8BITMIME', self.serv.last_mail_options)
self.assertIn('SMTPUTF8', self.serv.last_mail_options)
self.assertEqual(self.serv.last_rcpt_options, [])
def test_send_message_error_on_non_ascii_addrs_if_no_smtputf8(self):
msg = EmailMessage()
msg['From'] = "Páolo <főo@bar.com>"
msg['To'] = 'Dinsdale'
msg['Subject'] = 'Nudge nudge, wink, wink \u1F609'
smtp = smtplib.SMTP(
HOST, self.port, local_hostname='localhost', timeout=3)
self.addCleanup(smtp.close)
self.assertRaises(smtplib.SMTPNotSupportedError,
smtp.send_message(msg))
@support.reap_threads @support.reap_threads
def test_main(verbose=None): def test_main(verbose=None):