Skip to content

Commit f1d84c0

Browse files
committed
bpo-32814: Handle 8BITMIME availabilty in smtplib.SMTP.send_messageIf the policy requests an 8bit content transfer encoding, than we willcheck for the 8BITMIME extension and use it if available. Otherwise wewill use a 7bit content transfer encoding since the server doesn'tsupport 8bit messages.
1 parent c9265c1 commit f1d84c0

5 files changed

Lines changed: 127 additions & 12 deletions

File tree

Doc/library/smtplib.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,12 +514,20 @@ An :class:`SMTP` instance has the following methods:
514514
serialized with a clone of its :mod:`~email.policy` with the
515515
:attr:`~email.policy.EmailPolicy.utf8` attribute set to ``True``, and
516516
``SMTPUTF8`` and ``BODY=8BITMIME`` are added to *mail_options*.
517+
If the ``Message`` policy requests an ``'8bit'``
518+
:attr:`~email.policy.EmailPolicy.cte_type` and the server supports
519+
``8BITMIME``, the message will be sent using it. Otherwise the policy
520+
will be cloned with ``cte_type`` set to ``'7bit'`` and the message sent
521+
using it.
517522

518523
.. versionadded:: 3.2
519524

520525
.. versionadded:: 3.5
521526
Support for internationalized addresses (``SMTPUTF8``).
522527

528+
.. versionadded:: 3.8
529+
Handling ``8BITMIME`` (:rfc:`6152`) availability automatically.
530+
523531

524532
.. method:: SMTP.quit()
525533

Lib/smtplib.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -906,8 +906,11 @@ def send_message(self, msg, from_addr=None, to_addrs=None,
906906
SMTPUTF8 capability, the policy is cloned with utf8 set to True for the
907907
serialization, and SMTPUTF8 and BODY=8BITMIME are asserted on the send.
908908
If the server does not support SMTPUTF8, an SMTPNotSupported error is
909-
raised. Otherwise the generator is called without modifying the
910-
policy.
909+
raised. If the policy requests an '8bit' cte and the server supports
910+
8BITMIME, the message will be sent using it. Otherwise the policy
911+
will be cloned with cte_type set to '7bit'. If we didn't clone the
912+
policy as previously described, the generator is called with the
913+
original policy.
911914
912915
"""
913916
# 'Resent-Date' is a mandatory field if the Message is resent (RFC 2822
@@ -942,9 +945,10 @@ def send_message(self, msg, from_addr=None, to_addrs=None,
942945
to_addrs = [a[1] for a in email.utils.getaddresses(addr_fields)]
943946
# Make a local copy so we can delete the bcc headers.
944947
msg_copy = copy.copy(msg)
948+
policy = msg.policy.clone()
949+
mail_options = mail_options[:]
945950
del msg_copy['Bcc']
946951
del msg_copy['Resent-Bcc']
947-
international = False
948952
try:
949953
''.join([from_addr, *to_addrs]).encode('ascii')
950954
except UnicodeEncodeError:
@@ -953,14 +957,16 @@ def send_message(self, msg, from_addr=None, to_addrs=None,
953957
"One or more source or delivery addresses require"
954958
" internationalized email support, but the server"
955959
" does not advertise the required SMTPUTF8 capability")
956-
international = True
957-
with io.BytesIO() as bytesmsg:
958-
if international:
959-
g = email.generator.BytesGenerator(
960-
bytesmsg, policy=msg.policy.clone(utf8=True))
961-
mail_options += ['SMTPUTF8', 'BODY=8BITMIME']
960+
policy = policy.clone(utf8=True)
961+
mail_options += ['SMTPUTF8', 'BODY=8BITMIME']
962+
if policy.cte_type == '8bit':
963+
if self.has_extn('8bitmime'):
964+
if 'BODY=8BITMIME' not in mail_options:
965+
mail_options.append('BODY=8BITMIME')
962966
else:
963-
g = email.generator.BytesGenerator(bytesmsg)
967+
policy = policy.clone(cte_type='7bit')
968+
with io.BytesIO() as bytesmsg:
969+
g = email.generator.BytesGenerator(bytesmsg, policy=policy)
964970
g.flatten(msg_copy, linesep='\r\n')
965971
flatmsg = bytesmsg.getvalue()
966972
return self.sendmail(from_addr, to_addrs, flatmsg, mail_options,

Lib/test/test_logging.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -995,7 +995,7 @@ def test_basic(self):
995995
self.assertEqual(mailfrom, 'me')
996996
self.assertEqual(rcpttos, ['you'])
997997
self.assertIn('\nSubject: Log\n', data)
998-
self.assertTrue(data.endswith('\n\nHello \u2713'))
998+
self.assertTrue(data.endswith('\n\nSGVsbG8g4pyTCg=='))
999999
h.close()
10001000

10011001
def process_message(self, *args):

Lib/test/test_smtplib.py

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import asyncore
22
import base64
33
import email.mime.text
4+
from email import policy
45
from email.message import EmailMessage
56
from email.base64mime import body_encode as encode_base64
67
import email.utils
@@ -1219,6 +1220,8 @@ def test_send_message_uses_smtputf8_if_addrs_non_ascii(self):
12191220
self.assertEqual(self.serv.last_rcpt_options, [])
12201221

12211222
def test_send_message_error_on_non_ascii_addrs_if_no_smtputf8(self):
1223+
self.serv.enable_SMTPUTF8 = False
1224+
self.serv._extra_features = []
12221225
msg = EmailMessage()
12231226
msg['From'] = "Páolo <főo@bar.com>"
12241227
msg['To'] = 'Dinsdale'
@@ -1227,7 +1230,103 @@ def test_send_message_error_on_non_ascii_addrs_if_no_smtputf8(self):
12271230
HOST, self.port, local_hostname='localhost', timeout=3)
12281231
self.addCleanup(smtp.close)
12291232
self.assertRaises(smtplib.SMTPNotSupportedError,
1230-
smtp.send_message(msg))
1233+
lambda: smtp.send_message(msg))
1234+
1235+
def test_send_message_uses_8bitmime_if_cte_type_8bitmime(self):
1236+
msg = EmailMessage()
1237+
msg['From'] = 'Dinsdale <foo@bar.com>'
1238+
msg['To'] = 'Dinsdale'
1239+
msg['Subject'] = 'Nudge nudge, wink, wink \u1F609'
1240+
# XXX I don't know why I need two \n's here, but this is an existing
1241+
# bug (if it is one) and not a problem with the new functionality.
1242+
msg.set_content("oh là là, know what I mean, know what I mean?\n\n")
1243+
# XXX smtpd converts received /r/n to /n, so we can't easily test that
1244+
# we are successfully sending /r/n :(.
1245+
expected = textwrap.dedent("""\
1246+
From: Dinsdale <foo@bar.com>
1247+
To: Dinsdale
1248+
Subject: Nudge nudge, wink, wink =?utf-8?q?=E1=BD=A09?=
1249+
Content-Type: text/plain; charset="utf-8"
1250+
Content-Transfer-Encoding: 8bit
1251+
MIME-Version: 1.0
1252+
1253+
oh là là, know what I mean, know what I mean?
1254+
""")
1255+
smtp = smtplib.SMTP(
1256+
HOST, self.port, local_hostname='localhost', timeout=3)
1257+
self.addCleanup(smtp.close)
1258+
self.assertEqual(smtp.send_message(msg), {})
1259+
self.assertEqual(self.serv.last_mailfrom, 'foo@bar.com')
1260+
self.assertEqual(self.serv.last_rcpttos, ['Dinsdale'])
1261+
self.assertEqual(self.serv.last_message.decode(), expected)
1262+
self.assertIn('BODY=8BITMIME', self.serv.last_mail_options)
1263+
self.assertNotIn('SMTPUTF8', self.serv.last_mail_options)
1264+
self.assertEqual(self.serv.last_rcpt_options, [])
1265+
1266+
def test_send_message_uses_7bit_if_requested_even_with_8bitmime(self):
1267+
msg = EmailMessage(policy=policy.default.clone(cte_type='7bit'))
1268+
msg['From'] = 'Dinsdale <foo@bar.com>'
1269+
msg['To'] = 'Dinsdale'
1270+
msg['Subject'] = 'Nudge nudge, wink, wink \u1F609'
1271+
# XXX I don't know why I need two \n's here, but this is an existing
1272+
# bug (if it is one) and not a problem with the new functionality.
1273+
msg.set_content("oh là là, know what I mean, know what I mean?\n\n")
1274+
# XXX smtpd converts received /r/n to /n, so we can't easily test that
1275+
# we are successfully sending /r/n :(.
1276+
# XXX This uses quoted-printable but the case below happens to end up
1277+
# with base64...
1278+
expected = textwrap.dedent("""\
1279+
From: Dinsdale <foo@bar.com>
1280+
To: Dinsdale
1281+
Subject: Nudge nudge, wink, wink =?utf-8?q?=E1=BD=A09?=
1282+
Content-Type: text/plain; charset="utf-8"
1283+
Content-Transfer-Encoding: quoted-printable
1284+
MIME-Version: 1.0
1285+
1286+
oh l=C3=A0 l=C3=A0, know what I mean, know what I mean?
1287+
""")
1288+
smtp = smtplib.SMTP(
1289+
HOST, self.port, local_hostname='localhost', timeout=3)
1290+
self.addCleanup(smtp.close)
1291+
self.assertEqual(smtp.send_message(msg), {})
1292+
self.assertEqual(self.serv.last_mailfrom, 'foo@bar.com')
1293+
self.assertEqual(self.serv.last_rcpttos, ['Dinsdale'])
1294+
self.assertEqual(self.serv.last_message.decode(), expected)
1295+
self.assertNotIn('BODY=8BITMIME', self.serv.last_mail_options)
1296+
self.assertNotIn('SMTPUTF8', self.serv.last_mail_options)
1297+
self.assertEqual(self.serv.last_rcpt_options, [])
1298+
1299+
def test_send_message_uses_7bit_if_cte_type_8bitmime_but_no_8bitmime(self):
1300+
self.serv.enable_SMTPUTF8 = False
1301+
self.serv._extra_features = []
1302+
msg = EmailMessage()
1303+
msg['From'] = 'Dinsdale <foo@bar.com>'
1304+
msg['To'] = 'Dinsdale'
1305+
msg['Subject'] = 'Nudge nudge, wink, wink \u1F609'
1306+
# XXX I don't know why I need two \n's here, but this is an existing
1307+
# bug (if it is one) and not a problem with the new functionality.
1308+
msg.set_content("oh là là, know what I mean, know what I mean?\n\n")
1309+
# XXX smtpd converts received /r/n to /n, so we can't easily test that
1310+
# we are successfully sending /r/n :(.
1311+
expected = textwrap.dedent("""\
1312+
From: Dinsdale <foo@bar.com>
1313+
To: Dinsdale
1314+
Subject: Nudge nudge, wink, wink =?utf-8?q?=E1=BD=A09?=
1315+
Content-Type: text/plain; charset="utf-8"
1316+
Content-Transfer-Encoding: base64
1317+
MIME-Version: 1.0
1318+
1319+
b2ggbMOgIGzDoCwga25vdyB3aGF0IEkgbWVhbiwga25vdyB3aGF0IEkgbWVhbj8KCg==""")
1320+
smtp = smtplib.SMTP(
1321+
HOST, self.port, local_hostname='localhost', timeout=3)
1322+
self.addCleanup(smtp.close)
1323+
self.assertEqual(smtp.send_message(msg), {})
1324+
self.assertEqual(self.serv.last_mailfrom, 'foo@bar.com')
1325+
self.assertEqual(self.serv.last_rcpttos, ['Dinsdale'])
1326+
self.assertEqual(self.serv.last_message.decode(), expected)
1327+
self.assertNotIn('BODY=8BITMIME', self.serv.last_mail_options)
1328+
self.assertNotIn('SMTPUTF8', self.serv.last_mail_options)
1329+
self.assertEqual(self.serv.last_rcpt_options, [])
12311330

12321331

12331332
EXPECTED_RESPONSE = encode_base64(b'\0psu\0doesnotexist', eol='')
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Handle ``8BITMIME`` (RFC6152) availability automatically in
2+
``smtplib.SMTP.send_message``. Patch by Segev Finer.

0 commit comments

Comments
 (0)