#10321: Add support for sending binary DATA and Message objects to smtplib
This commit is contained in:
parent
a563392f92
commit
7dff9e08fb
@ -27,5 +27,5 @@ for file in pngfiles:
|
|||||||
|
|
||||||
# Send the email via our own SMTP server.
|
# Send the email via our own SMTP server.
|
||||||
s = smtplib.SMTP()
|
s = smtplib.SMTP()
|
||||||
s.sendmail(me, family, msg.as_string())
|
s.sendmail(msg)
|
||||||
s.quit()
|
s.quit()
|
||||||
|
@ -17,8 +17,7 @@ msg['Subject'] = 'The contents of %s' % textfile
|
|||||||
msg['From'] = me
|
msg['From'] = me
|
||||||
msg['To'] = you
|
msg['To'] = you
|
||||||
|
|
||||||
# Send the message via our own SMTP server, but don't include the
|
# Send the message via our own SMTP server.
|
||||||
# envelope header.
|
|
||||||
s = smtplib.SMTP()
|
s = smtplib.SMTP()
|
||||||
s.sendmail(me, [you], msg.as_string())
|
s.sendmail(msg)
|
||||||
s.quit()
|
s.quit()
|
||||||
|
@ -274,9 +274,14 @@ An :class:`SMTP` instance has the following methods:
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
The *from_addr* and *to_addrs* parameters are used to construct the message
|
The *from_addr* and *to_addrs* parameters are used to construct the message
|
||||||
envelope used by the transport agents. The :class:`SMTP` does not modify the
|
envelope used by the transport agents. ``sendmail`` does not modify the
|
||||||
message headers in any way.
|
message headers in any way.
|
||||||
|
|
||||||
|
msg may be a string containing characters in the ASCII range, or a byte
|
||||||
|
string. A string is encoded to bytes using the ascii codec, and lone ``\r``
|
||||||
|
and ``\n`` characters are converted to ``\r\n`` characters. A byte string
|
||||||
|
is not modified.
|
||||||
|
|
||||||
If there has been no previous ``EHLO`` or ``HELO`` command this session, this
|
If there has been no previous ``EHLO`` or ``HELO`` command this session, this
|
||||||
method tries ESMTP ``EHLO`` first. If the server does ESMTP, message size and
|
method tries ESMTP ``EHLO`` first. If the server does ESMTP, message size and
|
||||||
each of the specified options will be passed to it (if the option is in the
|
each of the specified options will be passed to it (if the option is in the
|
||||||
@ -311,6 +316,27 @@ An :class:`SMTP` instance has the following methods:
|
|||||||
Unless otherwise noted, the connection will be open even after an exception is
|
Unless otherwise noted, the connection will be open even after an exception is
|
||||||
raised.
|
raised.
|
||||||
|
|
||||||
|
.. versionchanged:: 3.2 *msg* may be a byte string.
|
||||||
|
|
||||||
|
|
||||||
|
.. method:: SMTP.send_message(msg, from_addr=None, to_addrs=None, mail_options=[], rcpt_options=[])
|
||||||
|
|
||||||
|
This is a convenience method for calling :meth:`sendmail` with the message
|
||||||
|
represented by an :class:`email.message.Message` object. The arguments have
|
||||||
|
the same meaning as for :meth:`sendmail`, except that *msg* is a ``Message``
|
||||||
|
object.
|
||||||
|
|
||||||
|
If *from_addr* is ``None``, ``send_message`` sets its value to the value of
|
||||||
|
the :mailheader:`From` header from *msg*. If *to_addrs* is ``None``,
|
||||||
|
``send_message`` combines the values (if any) of the :mailheader:`To`,
|
||||||
|
:mailheader:`CC`, and :mailheader:`Bcc` fields from *msg*. Regardless of
|
||||||
|
the values of *from_addr* and *to_addrs*, ``send_message`` deletes any Bcc
|
||||||
|
field from *msg*. It then serializes *msg* using
|
||||||
|
:class:`~email.generator.BytesGenerator` with ``\r\n`` as the *linesep*, and
|
||||||
|
calls :meth:`sendmail` to transmit the resulting message.
|
||||||
|
|
||||||
|
.. versionadded:: 3.2
|
||||||
|
|
||||||
|
|
||||||
.. method:: SMTP.quit()
|
.. method:: SMTP.quit()
|
||||||
|
|
||||||
@ -366,5 +392,5 @@ example doesn't do any processing of the :rfc:`822` headers. In particular, the
|
|||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
In general, you will want to use the :mod:`email` package's features to
|
In general, you will want to use the :mod:`email` package's features to
|
||||||
construct an email message, which you can then convert to a string and send
|
construct an email message, which you can then send
|
||||||
via :meth:`sendmail`; see :ref:`email-examples`.
|
via :meth:`~smtplib.SMTP.send_message`; see :ref:`email-examples`.
|
||||||
|
@ -540,6 +540,14 @@ New, Improved, and Deprecated Modules
|
|||||||
|
|
||||||
(Contributed by Neil Schemenauer and Nick Coghlan; :issue:`5178`.)
|
(Contributed by Neil Schemenauer and Nick Coghlan; :issue:`5178`.)
|
||||||
|
|
||||||
|
* The :mod:`smtplib` :class:`~smtplib.SMTP` class now accepts a byte string
|
||||||
|
for the *msg* argument to the :meth:`~smtplib.SMTP.sendmail` method,
|
||||||
|
and a new method, :meth:`~smtplib.SMTP.send_message` accepts a
|
||||||
|
:class:`~email.message.Message` object and can optionally obtain the
|
||||||
|
*from_addr* and *to_addrs* addresses directly from the object.
|
||||||
|
|
||||||
|
(Contributed by R. David Murray, :issue:`10321`.)
|
||||||
|
|
||||||
|
|
||||||
Multi-threading
|
Multi-threading
|
||||||
===============
|
===============
|
||||||
|
@ -42,8 +42,11 @@ Example:
|
|||||||
# This was modified from the Python 1.5 library HTTP lib.
|
# This was modified from the Python 1.5 library HTTP lib.
|
||||||
|
|
||||||
import socket
|
import socket
|
||||||
|
import io
|
||||||
import re
|
import re
|
||||||
import email.utils
|
import email.utils
|
||||||
|
import email.message
|
||||||
|
import email.generator
|
||||||
import base64
|
import base64
|
||||||
import hmac
|
import hmac
|
||||||
from email.base64mime import body_encode as encode_base64
|
from email.base64mime import body_encode as encode_base64
|
||||||
@ -57,6 +60,7 @@ __all__ = ["SMTPException","SMTPServerDisconnected","SMTPResponseException",
|
|||||||
SMTP_PORT = 25
|
SMTP_PORT = 25
|
||||||
SMTP_SSL_PORT = 465
|
SMTP_SSL_PORT = 465
|
||||||
CRLF="\r\n"
|
CRLF="\r\n"
|
||||||
|
bCRLF=b"\r\n"
|
||||||
|
|
||||||
OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I)
|
OLDSTYLE_AUTH = re.compile(r"auth=(.*)", re.I)
|
||||||
|
|
||||||
@ -147,6 +151,7 @@ def quoteaddr(addr):
|
|||||||
else:
|
else:
|
||||||
return "<%s>" % m
|
return "<%s>" % m
|
||||||
|
|
||||||
|
# Legacy method kept for backward compatibility.
|
||||||
def quotedata(data):
|
def quotedata(data):
|
||||||
"""Quote data for email.
|
"""Quote data for email.
|
||||||
|
|
||||||
@ -156,6 +161,12 @@ def quotedata(data):
|
|||||||
return re.sub(r'(?m)^\.', '..',
|
return re.sub(r'(?m)^\.', '..',
|
||||||
re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data))
|
re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data))
|
||||||
|
|
||||||
|
def _quote_periods(bindata):
|
||||||
|
return re.sub(br'(?m)^\.', '..', bindata)
|
||||||
|
|
||||||
|
def _fix_eols(data):
|
||||||
|
return re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
import ssl
|
import ssl
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -469,7 +480,9 @@ class SMTP:
|
|||||||
Automatically quotes lines beginning with a period per rfc821.
|
Automatically quotes lines beginning with a period per rfc821.
|
||||||
Raises SMTPDataError if there is an unexpected reply to the
|
Raises SMTPDataError if there is an unexpected reply to the
|
||||||
DATA command; the return value from this method is the final
|
DATA command; the return value from this method is the final
|
||||||
response code received when the all data is sent.
|
response code received when the all data is sent. If msg
|
||||||
|
is a string, lone '\r' and '\n' characters are converted to
|
||||||
|
'\r\n' characters. If msg is bytes, it is transmitted as is.
|
||||||
"""
|
"""
|
||||||
self.putcmd("data")
|
self.putcmd("data")
|
||||||
(code,repl)=self.getreply()
|
(code,repl)=self.getreply()
|
||||||
@ -477,10 +490,12 @@ class SMTP:
|
|||||||
if code != 354:
|
if code != 354:
|
||||||
raise SMTPDataError(code,repl)
|
raise SMTPDataError(code,repl)
|
||||||
else:
|
else:
|
||||||
q = quotedata(msg)
|
if isinstance(msg, str):
|
||||||
if q[-2:] != CRLF:
|
msg = _fix_eols(msg).encode('ascii')
|
||||||
q = q + CRLF
|
q = _quote_periods(msg)
|
||||||
q = q + "." + CRLF
|
if q[-2:] != bCRLF:
|
||||||
|
q = q + bCRLF
|
||||||
|
q = q + b"." + bCRLF
|
||||||
self.send(q)
|
self.send(q)
|
||||||
(code,msg)=self.getreply()
|
(code,msg)=self.getreply()
|
||||||
if self.debuglevel >0 : print("data:", (code,msg), file=stderr)
|
if self.debuglevel >0 : print("data:", (code,msg), file=stderr)
|
||||||
@ -648,6 +663,10 @@ class SMTP:
|
|||||||
- rcpt_options : List of ESMTP options (such as DSN commands) for
|
- rcpt_options : List of ESMTP options (such as DSN commands) for
|
||||||
all the rcpt commands.
|
all the rcpt commands.
|
||||||
|
|
||||||
|
msg may be a string containing characters in the ASCII range, or a byte
|
||||||
|
string. A string is encoded to bytes using the ascii codec, and lone
|
||||||
|
\r and \n characters are converted to \r\n characters.
|
||||||
|
|
||||||
If there has been no previous EHLO or HELO command this session, this
|
If there has been no previous EHLO or HELO command this session, this
|
||||||
method tries ESMTP EHLO first. If the server does ESMTP, message size
|
method tries ESMTP EHLO first. If the server does ESMTP, message size
|
||||||
and each of the specified options will be passed to it. If EHLO
|
and each of the specified options will be passed to it. If EHLO
|
||||||
@ -693,6 +712,8 @@ class SMTP:
|
|||||||
"""
|
"""
|
||||||
self.ehlo_or_helo_if_needed()
|
self.ehlo_or_helo_if_needed()
|
||||||
esmtp_opts = []
|
esmtp_opts = []
|
||||||
|
if isinstance(msg, str):
|
||||||
|
msg = _fix_eols(msg).encode('ascii')
|
||||||
if self.does_esmtp:
|
if self.does_esmtp:
|
||||||
# Hmmm? what's this? -ddm
|
# Hmmm? what's this? -ddm
|
||||||
# self.esmtp_features['7bit']=""
|
# self.esmtp_features['7bit']=""
|
||||||
@ -700,7 +721,6 @@ class SMTP:
|
|||||||
esmtp_opts.append("size=%d" % len(msg))
|
esmtp_opts.append("size=%d" % len(msg))
|
||||||
for option in mail_options:
|
for option in mail_options:
|
||||||
esmtp_opts.append(option)
|
esmtp_opts.append(option)
|
||||||
|
|
||||||
(code,resp) = self.mail(from_addr, esmtp_opts)
|
(code,resp) = self.mail(from_addr, esmtp_opts)
|
||||||
if code != 250:
|
if code != 250:
|
||||||
self.rset()
|
self.rset()
|
||||||
@ -723,6 +743,33 @@ class SMTP:
|
|||||||
#if we got here then somebody got our mail
|
#if we got here then somebody got our mail
|
||||||
return senderrs
|
return senderrs
|
||||||
|
|
||||||
|
def send_message(self, msg, from_addr=None, to_addrs=None,
|
||||||
|
mail_options=[], rcpt_options={}):
|
||||||
|
"""Converts message to a bytestring and passes it to sendmail.
|
||||||
|
|
||||||
|
The arguments are as for sendmail, except that msg is an
|
||||||
|
email.message.Message object. If from_addr is None, the from_addr is
|
||||||
|
taken from the 'From' header of the Message. If to_addrs is None, its
|
||||||
|
value is composed from the addresses listed in the 'To', 'CC', and
|
||||||
|
'Bcc' fields. Regardless of the values of from_addr and to_addr, any
|
||||||
|
Bcc field in the Message object is deleted. The Message object is then
|
||||||
|
serialized using email.generator.BytesGenerator and sendmail is called
|
||||||
|
to transmit the message.
|
||||||
|
"""
|
||||||
|
if from_addr is None:
|
||||||
|
from_addr = msg['From']
|
||||||
|
if to_addrs is None:
|
||||||
|
addr_fields = [f for f in (msg['To'], msg['Bcc'], msg['CC'])
|
||||||
|
if f is not None]
|
||||||
|
to_addrs = [a[1] for a in email.utils.getaddresses(addr_fields)]
|
||||||
|
del msg['Bcc']
|
||||||
|
with io.BytesIO() as bytesmsg:
|
||||||
|
g = email.generator.BytesGenerator(bytesmsg)
|
||||||
|
g.flatten(msg, linesep='\r\n')
|
||||||
|
flatmsg = bytesmsg.getvalue()
|
||||||
|
return self.sendmail(from_addr, to_addrs, flatmsg, mail_options,
|
||||||
|
rcpt_options)
|
||||||
|
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""Close the connection to the SMTP server."""
|
"""Close the connection to the SMTP server."""
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import asyncore
|
import asyncore
|
||||||
|
import email.mime.text
|
||||||
import email.utils
|
import email.utils
|
||||||
import socket
|
import socket
|
||||||
import smtpd
|
import smtpd
|
||||||
import smtplib
|
import smtplib
|
||||||
import io
|
import io
|
||||||
|
import re
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import select
|
import select
|
||||||
@ -57,6 +59,13 @@ class GeneralTests(unittest.TestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
smtplib.socket = socket
|
smtplib.socket = socket
|
||||||
|
|
||||||
|
# This method is no longer used but is retained for backward compatibility,
|
||||||
|
# so test to make sure it still works.
|
||||||
|
def testQuoteData(self):
|
||||||
|
teststr = "abc\n.jkl\rfoo\r\n..blue"
|
||||||
|
expected = "abc\r\n..jkl\r\nfoo\r\n...blue"
|
||||||
|
self.assertEqual(expected, smtplib.quotedata(teststr))
|
||||||
|
|
||||||
def testBasic1(self):
|
def testBasic1(self):
|
||||||
mock_socket.reply_with(b"220 Hola mundo")
|
mock_socket.reply_with(b"220 Hola mundo")
|
||||||
# connects
|
# connects
|
||||||
@ -150,6 +159,8 @@ MSG_END = '------------ END MESSAGE ------------\n'
|
|||||||
@unittest.skipUnless(threading, 'Threading required for this test.')
|
@unittest.skipUnless(threading, 'Threading required for this test.')
|
||||||
class DebuggingServerTests(unittest.TestCase):
|
class DebuggingServerTests(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
|
||||||
@ -161,6 +172,9 @@ class DebuggingServerTests(unittest.TestCase):
|
|||||||
self._threads = support.threading_setup()
|
self._threads = support.threading_setup()
|
||||||
self.serv_evt = threading.Event()
|
self.serv_evt = threading.Event()
|
||||||
self.client_evt = threading.Event()
|
self.client_evt = threading.Event()
|
||||||
|
# Capture SMTPChannel debug output
|
||||||
|
self.old_DEBUGSTREAM = smtpd.DEBUGSTREAM
|
||||||
|
smtpd.DEBUGSTREAM = io.StringIO()
|
||||||
# Pick a random unused port by passing 0 for the port number
|
# Pick a random unused port by passing 0 for the port number
|
||||||
self.serv = smtpd.DebuggingServer((HOST, 0), ('nowhere', -1))
|
self.serv = smtpd.DebuggingServer((HOST, 0), ('nowhere', -1))
|
||||||
# Keep a note of what port was assigned
|
# Keep a note of what port was assigned
|
||||||
@ -183,6 +197,9 @@ class DebuggingServerTests(unittest.TestCase):
|
|||||||
support.threading_cleanup(*self._threads)
|
support.threading_cleanup(*self._threads)
|
||||||
# restore sys.stdout
|
# restore sys.stdout
|
||||||
sys.stdout = self.old_stdout
|
sys.stdout = self.old_stdout
|
||||||
|
# restore DEBUGSTREAM
|
||||||
|
smtpd.DEBUGSTREAM.close()
|
||||||
|
smtpd.DEBUGSTREAM = self.old_DEBUGSTREAM
|
||||||
|
|
||||||
def testBasic(self):
|
def testBasic(self):
|
||||||
# connect
|
# connect
|
||||||
@ -247,6 +264,95 @@ class DebuggingServerTests(unittest.TestCase):
|
|||||||
mexpect = '%s%s\n%s' % (MSG_BEGIN, m, MSG_END)
|
mexpect = '%s%s\n%s' % (MSG_BEGIN, m, MSG_END)
|
||||||
self.assertEqual(self.output.getvalue(), mexpect)
|
self.assertEqual(self.output.getvalue(), mexpect)
|
||||||
|
|
||||||
|
def testSendBinary(self):
|
||||||
|
m = b'A test message'
|
||||||
|
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
|
||||||
|
smtp.sendmail('John', 'Sally', m)
|
||||||
|
# XXX (see comment in testSend)
|
||||||
|
time.sleep(0.01)
|
||||||
|
smtp.quit()
|
||||||
|
|
||||||
|
self.client_evt.set()
|
||||||
|
self.serv_evt.wait()
|
||||||
|
self.output.flush()
|
||||||
|
mexpect = '%s%s\n%s' % (MSG_BEGIN, m.decode('ascii'), MSG_END)
|
||||||
|
self.assertEqual(self.output.getvalue(), mexpect)
|
||||||
|
|
||||||
|
def testSendMessage(self):
|
||||||
|
m = email.mime.text.MIMEText('A test message')
|
||||||
|
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
|
||||||
|
smtp.send_message(m, from_addr='John', to_addrs='Sally')
|
||||||
|
# XXX (see comment in testSend)
|
||||||
|
time.sleep(0.01)
|
||||||
|
smtp.quit()
|
||||||
|
|
||||||
|
self.client_evt.set()
|
||||||
|
self.serv_evt.wait()
|
||||||
|
self.output.flush()
|
||||||
|
# Add the X-Peer header that DebuggingServer adds
|
||||||
|
# XXX: I'm not sure hardcoding this IP will work on linux-vserver.
|
||||||
|
m['X-Peer'] = '127.0.0.1'
|
||||||
|
mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END)
|
||||||
|
self.assertEqual(self.output.getvalue(), mexpect)
|
||||||
|
|
||||||
|
def testSendMessageWithAddresses(self):
|
||||||
|
m = email.mime.text.MIMEText('A test message')
|
||||||
|
m['From'] = 'foo@bar.com'
|
||||||
|
m['To'] = 'John'
|
||||||
|
m['CC'] = 'Sally, Fred'
|
||||||
|
m['Bcc'] = 'John Root <root@localhost>, "Dinsdale" <warped@silly.walks.com>'
|
||||||
|
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
|
||||||
|
smtp.send_message(m)
|
||||||
|
# XXX (see comment in testSend)
|
||||||
|
time.sleep(0.01)
|
||||||
|
smtp.quit()
|
||||||
|
|
||||||
|
self.client_evt.set()
|
||||||
|
self.serv_evt.wait()
|
||||||
|
self.output.flush()
|
||||||
|
# Add the X-Peer header that DebuggingServer adds
|
||||||
|
# XXX: I'm not sure hardcoding this IP will work on linux-vserver.
|
||||||
|
m['X-Peer'] = '127.0.0.1'
|
||||||
|
# The Bcc header is deleted before serialization.
|
||||||
|
del m['Bcc']
|
||||||
|
mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END)
|
||||||
|
self.assertEqual(self.output.getvalue(), mexpect)
|
||||||
|
debugout = smtpd.DEBUGSTREAM.getvalue()
|
||||||
|
sender = re.compile("^sender: foo@bar.com$", re.MULTILINE)
|
||||||
|
self.assertRegexpMatches(debugout, sender)
|
||||||
|
for addr in ('John', 'Sally', 'Fred', 'root@localhost',
|
||||||
|
'warped@silly.walks.com'):
|
||||||
|
to_addr = re.compile(r"^recips: .*'{}'.*$".format(addr),
|
||||||
|
re.MULTILINE)
|
||||||
|
self.assertRegexpMatches(debugout, to_addr)
|
||||||
|
|
||||||
|
def testSendMessageWithSomeAddresses(self):
|
||||||
|
# Make sure nothing breaks if not all of the three 'to' headers exist
|
||||||
|
m = email.mime.text.MIMEText('A test message')
|
||||||
|
m['From'] = 'foo@bar.com'
|
||||||
|
m['To'] = 'John, Dinsdale'
|
||||||
|
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
|
||||||
|
smtp.send_message(m)
|
||||||
|
# XXX (see comment in testSend)
|
||||||
|
time.sleep(0.01)
|
||||||
|
smtp.quit()
|
||||||
|
|
||||||
|
self.client_evt.set()
|
||||||
|
self.serv_evt.wait()
|
||||||
|
self.output.flush()
|
||||||
|
# Add the X-Peer header that DebuggingServer adds
|
||||||
|
# XXX: I'm not sure hardcoding this IP will work on linux-vserver.
|
||||||
|
m['X-Peer'] = '127.0.0.1'
|
||||||
|
mexpect = '%s%s\n%s' % (MSG_BEGIN, m.as_string(), MSG_END)
|
||||||
|
self.assertEqual(self.output.getvalue(), mexpect)
|
||||||
|
debugout = smtpd.DEBUGSTREAM.getvalue()
|
||||||
|
sender = re.compile("^sender: foo@bar.com$", re.MULTILINE)
|
||||||
|
self.assertRegexpMatches(debugout, sender)
|
||||||
|
for addr in ('John', 'Dinsdale'):
|
||||||
|
to_addr = re.compile(r"^recips: .*'{}'.*$".format(addr),
|
||||||
|
re.MULTILINE)
|
||||||
|
self.assertRegexpMatches(debugout, to_addr)
|
||||||
|
|
||||||
|
|
||||||
class NonConnectingTests(unittest.TestCase):
|
class NonConnectingTests(unittest.TestCase):
|
||||||
|
|
||||||
|
@ -60,6 +60,9 @@ Core and Builtins
|
|||||||
Library
|
Library
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
- Issue #10321: Added support for binary data to smtplib.SMTP.sendmail,
|
||||||
|
and a new method send_message to send an email.message.Message object.
|
||||||
|
|
||||||
- Issue #6011: sysconfig and distutils.sysconfig use the surrogateescape error
|
- Issue #6011: sysconfig and distutils.sysconfig use the surrogateescape error
|
||||||
handler to parse the Makefile file. Avoid a UnicodeDecodeError if the source
|
handler to parse the Makefile file. Avoid a UnicodeDecodeError if the source
|
||||||
code directory name contains a non-ASCII character and the locale encoding is
|
code directory name contains a non-ASCII character and the locale encoding is
|
||||||
|
Loading…
x
Reference in New Issue
Block a user