Kaydet (Commit) 7dff9e08 authored tarafından R. David Murray's avatar R. David Murray

#10321: Add support for sending binary DATA and Message objects to smtplib

üst a563392f
...@@ -27,5 +27,5 @@ for file in pngfiles: ...@@ -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 ...@@ -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: ...@@ -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: ...@@ -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 ...@@ -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 ...@@ -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: ...@@ -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", ...@@ -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): ...@@ -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): ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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: ...@@ -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."""
......
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): ...@@ -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' ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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 ...@@ -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
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment