Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 8308444

Browse files
committed
python#24218: Add SMTPUTF8 support to send_message.
Reviewed by Maciej Szulik.
1 parent 740d613 commit 8308444

4 files changed

Lines changed: 86 additions & 8 deletions

File tree

Doc/library/smtplib.rst

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -467,7 +467,7 @@ An :class:`SMTP` instance has the following methods:
467467

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

487492
.. versionadded:: 3.2
488493

494+
.. versionadded:: 3.5
495+
Support for internationalized addresses (``SMTPUTF8``).
496+
489497

490498
.. method:: SMTP.quit()
491499

Doc/whatsnew/3.5.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -557,8 +557,10 @@ smtplib
557557
:class:`smtplib.SMTP`. (Contributed by Gavin Chappell and Maciej Szulik in
558558
:issue:`16914`.)
559559

560-
* :mod:`smtplib` now support :rfc:`6531` (SMTPUTF8). (Contributed by
561-
Milan Oberkirch and R. David Murray in :issue:`22027`.)
560+
* :mod:`smtplib` now supports :rfc:`6531` (SMTPUTF8) in both the
561+
:meth:`~smtplib.SMTP.sendmail` and :meth:`~smtplib.SMTP.send_message`
562+
commands. (Contributed by Milan Oberkirch and R. David Murray in
563+
:issue:`22027`.)
562564

563565
sndhdr
564566
------

Lib/smtplib.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -872,7 +872,13 @@ def send_message(self, msg, from_addr=None, to_addrs=None,
872872
to_addr, any Bcc field (or Resent-Bcc field, when the Message is a
873873
resent) of the Message object won't be transmitted. The Message
874874
object is then serialized using email.generator.BytesGenerator and
875-
sendmail is called to transmit the message.
875+
sendmail is called to transmit the message. If the sender or any of
876+
the recipient addresses contain non-ASCII and the server advertises the
877+
SMTPUTF8 capability, the policy is cloned with utf8 set to True for the
878+
serialization, and SMTPUTF8 and BODY=8BITMIME are asserted on the send.
879+
If the server does not support SMTPUTF8, an SMPTNotSupported error is
880+
raised. Otherwise the generator is called without modifying the
881+
policy.
876882
877883
"""
878884
# 'Resent-Date' is a mandatory field if the Message is resent (RFC 2822
@@ -885,6 +891,7 @@ def send_message(self, msg, from_addr=None, to_addrs=None,
885891
# option allowing the user to enable the heuristics. (It should be
886892
# possible to guess correctly almost all of the time.)
887893

894+
self.ehlo_or_helo_if_needed()
888895
resent = msg.get_all('Resent-Date')
889896
if resent is None:
890897
header_prefix = ''
@@ -900,14 +907,30 @@ def send_message(self, msg, from_addr=None, to_addrs=None,
900907
if to_addrs is None:
901908
addr_fields = [f for f in (msg[header_prefix + 'To'],
902909
msg[header_prefix + 'Bcc'],
903-
msg[header_prefix + 'Cc']) if f is not None]
910+
msg[header_prefix + 'Cc'])
911+
if f is not None]
904912
to_addrs = [a[1] for a in email.utils.getaddresses(addr_fields)]
905913
# Make a local copy so we can delete the bcc headers.
906914
msg_copy = copy.copy(msg)
907915
del msg_copy['Bcc']
908916
del msg_copy['Resent-Bcc']
917+
international = False
918+
try:
919+
''.join([from_addr, *to_addrs]).encode('ascii')
920+
except UnicodeEncodeError:
921+
if not self.has_extn('smtputf8'):
922+
raise SMTPNotSupportedError(
923+
"One or more source or delivery addresses require"
924+
" internationalized email support, but the server"
925+
" does not advertise the required SMTPUTF8 capability")
926+
international = True
909927
with io.BytesIO() as bytesmsg:
910-
g = email.generator.BytesGenerator(bytesmsg)
928+
if international:
929+
g = email.generator.BytesGenerator(
930+
bytesmsg, policy=msg.policy.clone(utf8=True))
931+
mail_options += ['SMTPUTF8', 'BODY=8BITMIME']
932+
else:
933+
g = email.generator.BytesGenerator(bytesmsg)
911934
g.flatten(msg_copy, linesep='\r\n')
912935
flatmsg = bytesmsg.getvalue()
913936
return self.sendmail(from_addr, to_addrs, flatmsg, mail_options,

Lib/test/test_smtplib.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncore
22
import email.mime.text
3+
from email.message import EmailMessage
34
import email.utils
45
import socket
56
import smtpd
@@ -10,7 +11,7 @@
1011
import time
1112
import select
1213
import errno
13-
import base64
14+
import textwrap
1415

1516
import unittest
1617
from test import support, mock_socket
@@ -1029,6 +1030,8 @@ def process_message(self, peer, mailfrom, rcpttos, data, mail_options=None,
10291030
@unittest.skipUnless(threading, 'Threading required for this test.')
10301031
class SMTPUTF8SimTests(unittest.TestCase):
10311032

1033+
maxDiff = None
1034+
10321035
def setUp(self):
10331036
self.real_getfqdn = socket.getfqdn
10341037
socket.getfqdn = mock_socket.getfqdn
@@ -1096,6 +1099,48 @@ def test_send_unicode_with_SMTPUTF8_via_low_level_API(self):
10961099
self.assertIn('SMTPUTF8', self.serv.last_mail_options)
10971100
self.assertEqual(self.serv.last_rcpt_options, [])
10981101

1102+
def test_send_message_uses_smtputf8_if_addrs_non_ascii(self):
1103+
msg = EmailMessage()
1104+
msg['From'] = "Páolo <fő[email protected]>"
1105+
msg['To'] = 'Dinsdale'
1106+
msg['Subject'] = 'Nudge nudge, wink, wink \u1F609'
1107+
# XXX I don't know why I need two \n's here, but this is an existing
1108+
# bug (if it is one) and not a problem with the new functionality.
1109+
msg.set_content("oh là là, know what I mean, know what I mean?\n\n")
1110+
# XXX smtpd converts received /r/n to /n, so we can't easily test that
1111+
# we are successfully sending /r/n :(.
1112+
expected = textwrap.dedent("""\
1113+
From: Páolo <fő[email protected]>
1114+
To: Dinsdale
1115+
Subject: Nudge nudge, wink, wink \u1F609
1116+
Content-Type: text/plain; charset="utf-8"
1117+
Content-Transfer-Encoding: 8bit
1118+
MIME-Version: 1.0
1119+
1120+
oh là là, know what I mean, know what I mean?
1121+
""")
1122+
smtp = smtplib.SMTP(
1123+
HOST, self.port, local_hostname='localhost', timeout=3)
1124+
self.addCleanup(smtp.close)
1125+
self.assertEqual(smtp.send_message(msg), {})
1126+
self.assertEqual(self.serv.last_mailfrom, 'fő[email protected]')
1127+
self.assertEqual(self.serv.last_rcpttos, ['Dinsdale'])
1128+
self.assertEqual(self.serv.last_message.decode(), expected)
1129+
self.assertIn('BODY=8BITMIME', self.serv.last_mail_options)
1130+
self.assertIn('SMTPUTF8', self.serv.last_mail_options)
1131+
self.assertEqual(self.serv.last_rcpt_options, [])
1132+
1133+
def test_send_message_error_on_non_ascii_addrs_if_no_smtputf8(self):
1134+
msg = EmailMessage()
1135+
msg['From'] = "Páolo <fő[email protected]>"
1136+
msg['To'] = 'Dinsdale'
1137+
msg['Subject'] = 'Nudge nudge, wink, wink \u1F609'
1138+
smtp = smtplib.SMTP(
1139+
HOST, self.port, local_hostname='localhost', timeout=3)
1140+
self.addCleanup(smtp.close)
1141+
self.assertRaises(smtplib.SMTPNotSupportedError,
1142+
smtp.send_message(msg))
1143+
10991144

11001145
@support.reap_threads
11011146
def test_main(verbose=None):

0 commit comments

Comments
 (0)