bpo-39329: Add timeout parameter for smtplib.LMTP constructor (GH-17998)

This commit is contained in:
Dong-hee Na 2020-01-15 06:42:09 +09:00 committed by Victor Stinner
parent 7d6378051f
commit 65a5ce247f
5 changed files with 61 additions and 33 deletions

View File

@ -115,7 +115,8 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions).
If the *timeout* parameter is set to be zero, it will raise a If the *timeout* parameter is set to be zero, it will raise a
:class:`ValueError` to prevent the creation of a non-blocking socket :class:`ValueError` to prevent the creation of a non-blocking socket
.. class:: LMTP(host='', port=LMTP_PORT, local_hostname=None, source_address=None) .. class:: LMTP(host='', port=LMTP_PORT, local_hostname=None,
source_address=None[, timeout])
The LMTP protocol, which is very similar to ESMTP, is heavily based on the The LMTP protocol, which is very similar to ESMTP, is heavily based on the
standard SMTP client. It's common to use Unix sockets for LMTP, so our standard SMTP client. It's common to use Unix sockets for LMTP, so our
@ -128,6 +129,9 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions).
Unix socket, LMTP generally don't support or require any authentication, but Unix socket, LMTP generally don't support or require any authentication, but
your mileage might vary. your mileage might vary.
.. versionchanged:: 3.9
The optional *timeout* parameter was added.
A nice selection of exceptions is defined as well: A nice selection of exceptions is defined as well:

View File

@ -270,6 +270,9 @@ smtplib
if the given timeout for their constructor is zero to prevent the creation of if the given timeout for their constructor is zero to prevent the creation of
a non-blocking socket. (Contributed by Dong-hee Na in :issue:`39259`.) a non-blocking socket. (Contributed by Dong-hee Na in :issue:`39259`.)
:class:`~smtplib.LMTP` constructor now has an optional *timeout* parameter.
(Contributed by Dong-hee Na in :issue:`39329`.)
signal signal
------ ------

View File

@ -1066,19 +1066,23 @@ class LMTP(SMTP):
ehlo_msg = "lhlo" ehlo_msg = "lhlo"
def __init__(self, host='', port=LMTP_PORT, local_hostname=None, def __init__(self, host='', port=LMTP_PORT, local_hostname=None,
source_address=None): source_address=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT):
"""Initialize a new instance.""" """Initialize a new instance."""
super().__init__(host, port, local_hostname=local_hostname, super().__init__(host, port, local_hostname=local_hostname,
source_address=source_address) source_address=source_address, timeout=timeout)
def connect(self, host='localhost', port=0, source_address=None): def connect(self, host='localhost', port=0, source_address=None):
"""Connect to the LMTP daemon, on either a Unix or a TCP socket.""" """Connect to the LMTP daemon, on either a Unix or a TCP socket."""
if host[0] != '/': if host[0] != '/':
return super().connect(host, port, source_address=source_address) return super().connect(host, port, source_address=source_address)
if self.timeout is not None and not self.timeout:
raise ValueError('Non-blocking socket (timeout=0) is not supported')
# Handle Unix-domain sockets. # Handle Unix-domain sockets.
try: try:
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.settimeout(self.timeout)
self.file = None self.file = None
self.sock.connect(host) self.sock.connect(host)
except OSError: except OSError:

View File

@ -56,7 +56,7 @@ def server(evt, buf, serv):
serv.close() serv.close()
evt.set() evt.set()
class GeneralTests(unittest.TestCase): class GeneralTests:
def setUp(self): def setUp(self):
smtplib.socket = mock_socket smtplib.socket = mock_socket
@ -75,29 +75,29 @@ class GeneralTests(unittest.TestCase):
def testBasic1(self): def testBasic1(self):
mock_socket.reply_with(b"220 Hola mundo") mock_socket.reply_with(b"220 Hola mundo")
# connects # connects
smtp = smtplib.SMTP(HOST, self.port) client = self.client(HOST, self.port)
smtp.close() client.close()
def testSourceAddress(self): def testSourceAddress(self):
mock_socket.reply_with(b"220 Hola mundo") mock_socket.reply_with(b"220 Hola mundo")
# connects # connects
smtp = smtplib.SMTP(HOST, self.port, client = self.client(HOST, self.port,
source_address=('127.0.0.1',19876)) source_address=('127.0.0.1',19876))
self.assertEqual(smtp.source_address, ('127.0.0.1', 19876)) self.assertEqual(client.source_address, ('127.0.0.1', 19876))
smtp.close() client.close()
def testBasic2(self): def testBasic2(self):
mock_socket.reply_with(b"220 Hola mundo") mock_socket.reply_with(b"220 Hola mundo")
# connects, include port in host name # connects, include port in host name
smtp = smtplib.SMTP("%s:%s" % (HOST, self.port)) client = self.client("%s:%s" % (HOST, self.port))
smtp.close() client.close()
def testLocalHostName(self): def testLocalHostName(self):
mock_socket.reply_with(b"220 Hola mundo") mock_socket.reply_with(b"220 Hola mundo")
# check that supplied local_hostname is used # check that supplied local_hostname is used
smtp = smtplib.SMTP(HOST, self.port, local_hostname="testhost") client = self.client(HOST, self.port, local_hostname="testhost")
self.assertEqual(smtp.local_hostname, "testhost") self.assertEqual(client.local_hostname, "testhost")
smtp.close() client.close()
def testTimeoutDefault(self): def testTimeoutDefault(self):
mock_socket.reply_with(b"220 Hola mundo") mock_socket.reply_with(b"220 Hola mundo")
@ -105,56 +105,71 @@ class GeneralTests(unittest.TestCase):
mock_socket.setdefaulttimeout(30) mock_socket.setdefaulttimeout(30)
self.assertEqual(mock_socket.getdefaulttimeout(), 30) self.assertEqual(mock_socket.getdefaulttimeout(), 30)
try: try:
smtp = smtplib.SMTP(HOST, self.port) client = self.client(HOST, self.port)
finally: finally:
mock_socket.setdefaulttimeout(None) mock_socket.setdefaulttimeout(None)
self.assertEqual(smtp.sock.gettimeout(), 30) self.assertEqual(client.sock.gettimeout(), 30)
smtp.close() client.close()
def testTimeoutNone(self): def testTimeoutNone(self):
mock_socket.reply_with(b"220 Hola mundo") mock_socket.reply_with(b"220 Hola mundo")
self.assertIsNone(socket.getdefaulttimeout()) self.assertIsNone(socket.getdefaulttimeout())
socket.setdefaulttimeout(30) socket.setdefaulttimeout(30)
try: try:
smtp = smtplib.SMTP(HOST, self.port, timeout=None) client = self.client(HOST, self.port, timeout=None)
finally: finally:
socket.setdefaulttimeout(None) socket.setdefaulttimeout(None)
self.assertIsNone(smtp.sock.gettimeout()) self.assertIsNone(client.sock.gettimeout())
smtp.close() client.close()
def testTimeoutZero(self): def testTimeoutZero(self):
mock_socket.reply_with(b"220 Hola mundo") mock_socket.reply_with(b"220 Hola mundo")
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
smtplib.SMTP(HOST, self.port, timeout=0) self.client(HOST, self.port, timeout=0)
def testTimeoutValue(self): def testTimeoutValue(self):
mock_socket.reply_with(b"220 Hola mundo") mock_socket.reply_with(b"220 Hola mundo")
smtp = smtplib.SMTP(HOST, self.port, timeout=30) client = self.client(HOST, self.port, timeout=30)
self.assertEqual(smtp.sock.gettimeout(), 30) self.assertEqual(client.sock.gettimeout(), 30)
smtp.close() client.close()
def test_debuglevel(self): def test_debuglevel(self):
mock_socket.reply_with(b"220 Hello world") mock_socket.reply_with(b"220 Hello world")
smtp = smtplib.SMTP() client = self.client()
smtp.set_debuglevel(1) client.set_debuglevel(1)
with support.captured_stderr() as stderr: with support.captured_stderr() as stderr:
smtp.connect(HOST, self.port) client.connect(HOST, self.port)
smtp.close() client.close()
expected = re.compile(r"^connect:", re.MULTILINE) expected = re.compile(r"^connect:", re.MULTILINE)
self.assertRegex(stderr.getvalue(), expected) self.assertRegex(stderr.getvalue(), expected)
def test_debuglevel_2(self): def test_debuglevel_2(self):
mock_socket.reply_with(b"220 Hello world") mock_socket.reply_with(b"220 Hello world")
smtp = smtplib.SMTP() client = self.client()
smtp.set_debuglevel(2) client.set_debuglevel(2)
with support.captured_stderr() as stderr: with support.captured_stderr() as stderr:
smtp.connect(HOST, self.port) client.connect(HOST, self.port)
smtp.close() client.close()
expected = re.compile(r"^\d{2}:\d{2}:\d{2}\.\d{6} connect: ", expected = re.compile(r"^\d{2}:\d{2}:\d{2}\.\d{6} connect: ",
re.MULTILINE) re.MULTILINE)
self.assertRegex(stderr.getvalue(), expected) self.assertRegex(stderr.getvalue(), expected)
class SMTPGeneralTests(GeneralTests, unittest.TestCase):
client = smtplib.SMTP
class LMTPGeneralTests(GeneralTests, unittest.TestCase):
client = smtplib.LMTP
def testTimeoutZero(self):
super().testTimeoutZero()
local_host = '/some/local/lmtp/delivery/program'
with self.assertRaises(ValueError):
self.client(local_host, timeout=0)
# Test server thread using the specified SMTP server class # Test server thread using the specified SMTP server class
def debugging_server(serv, serv_evt, client_evt): def debugging_server(serv, serv_evt, client_evt):
serv_evt.set() serv_evt.set()

View File

@ -0,0 +1,2 @@
:class:`~smtplib.LMTP` constructor now has an optional *timeout* parameter.
Patch by Dong-hee Na.