From 65a5ce247f177c4c52cfd104d9df0c2f3b1c91f0 Mon Sep 17 00:00:00 2001 From: Dong-hee Na Date: Wed, 15 Jan 2020 06:42:09 +0900 Subject: [PATCH] bpo-39329: Add timeout parameter for smtplib.LMTP constructor (GH-17998) --- Doc/library/smtplib.rst | 6 +- Doc/whatsnew/3.9.rst | 3 + Lib/smtplib.py | 8 +- Lib/test/test_smtplib.py | 75 +++++++++++-------- .../2020-01-14-22-16-07.bpo-39329.6OKGBn.rst | 2 + 5 files changed, 61 insertions(+), 33 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2020-01-14-22-16-07.bpo-39329.6OKGBn.rst diff --git a/Doc/library/smtplib.rst b/Doc/library/smtplib.rst index f6ac123823b..a88e358eae5 100644 --- a/Doc/library/smtplib.rst +++ b/Doc/library/smtplib.rst @@ -115,7 +115,8 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions). 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:: 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 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 your mileage might vary. + .. versionchanged:: 3.9 + The optional *timeout* parameter was added. + A nice selection of exceptions is defined as well: diff --git a/Doc/whatsnew/3.9.rst b/Doc/whatsnew/3.9.rst index c9499999920..451902ab1db 100644 --- a/Doc/whatsnew/3.9.rst +++ b/Doc/whatsnew/3.9.rst @@ -270,6 +270,9 @@ smtplib 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`.) +:class:`~smtplib.LMTP` constructor now has an optional *timeout* parameter. +(Contributed by Dong-hee Na in :issue:`39329`.) + signal ------ diff --git a/Lib/smtplib.py b/Lib/smtplib.py index 4d5cdb5ac0a..7808ba01cba 100755 --- a/Lib/smtplib.py +++ b/Lib/smtplib.py @@ -1066,19 +1066,23 @@ class LMTP(SMTP): ehlo_msg = "lhlo" 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.""" 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): """Connect to the LMTP daemon, on either a Unix or a TCP socket.""" if host[0] != '/': 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. try: self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.sock.settimeout(self.timeout) self.file = None self.sock.connect(host) except OSError: diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py index cc5c4b13464..067c01c10c1 100644 --- a/Lib/test/test_smtplib.py +++ b/Lib/test/test_smtplib.py @@ -56,7 +56,7 @@ def server(evt, buf, serv): serv.close() evt.set() -class GeneralTests(unittest.TestCase): +class GeneralTests: def setUp(self): smtplib.socket = mock_socket @@ -75,29 +75,29 @@ class GeneralTests(unittest.TestCase): def testBasic1(self): mock_socket.reply_with(b"220 Hola mundo") # connects - smtp = smtplib.SMTP(HOST, self.port) - smtp.close() + client = self.client(HOST, self.port) + client.close() def testSourceAddress(self): mock_socket.reply_with(b"220 Hola mundo") # connects - smtp = smtplib.SMTP(HOST, self.port, - source_address=('127.0.0.1',19876)) - self.assertEqual(smtp.source_address, ('127.0.0.1', 19876)) - smtp.close() + client = self.client(HOST, self.port, + source_address=('127.0.0.1',19876)) + self.assertEqual(client.source_address, ('127.0.0.1', 19876)) + client.close() def testBasic2(self): mock_socket.reply_with(b"220 Hola mundo") # connects, include port in host name - smtp = smtplib.SMTP("%s:%s" % (HOST, self.port)) - smtp.close() + client = self.client("%s:%s" % (HOST, self.port)) + client.close() def testLocalHostName(self): mock_socket.reply_with(b"220 Hola mundo") # check that supplied local_hostname is used - smtp = smtplib.SMTP(HOST, self.port, local_hostname="testhost") - self.assertEqual(smtp.local_hostname, "testhost") - smtp.close() + client = self.client(HOST, self.port, local_hostname="testhost") + self.assertEqual(client.local_hostname, "testhost") + client.close() def testTimeoutDefault(self): mock_socket.reply_with(b"220 Hola mundo") @@ -105,56 +105,71 @@ class GeneralTests(unittest.TestCase): mock_socket.setdefaulttimeout(30) self.assertEqual(mock_socket.getdefaulttimeout(), 30) try: - smtp = smtplib.SMTP(HOST, self.port) + client = self.client(HOST, self.port) finally: mock_socket.setdefaulttimeout(None) - self.assertEqual(smtp.sock.gettimeout(), 30) - smtp.close() + self.assertEqual(client.sock.gettimeout(), 30) + client.close() def testTimeoutNone(self): mock_socket.reply_with(b"220 Hola mundo") self.assertIsNone(socket.getdefaulttimeout()) socket.setdefaulttimeout(30) try: - smtp = smtplib.SMTP(HOST, self.port, timeout=None) + client = self.client(HOST, self.port, timeout=None) finally: socket.setdefaulttimeout(None) - self.assertIsNone(smtp.sock.gettimeout()) - smtp.close() + self.assertIsNone(client.sock.gettimeout()) + client.close() def testTimeoutZero(self): mock_socket.reply_with(b"220 Hola mundo") with self.assertRaises(ValueError): - smtplib.SMTP(HOST, self.port, timeout=0) + self.client(HOST, self.port, timeout=0) def testTimeoutValue(self): mock_socket.reply_with(b"220 Hola mundo") - smtp = smtplib.SMTP(HOST, self.port, timeout=30) - self.assertEqual(smtp.sock.gettimeout(), 30) - smtp.close() + client = self.client(HOST, self.port, timeout=30) + self.assertEqual(client.sock.gettimeout(), 30) + client.close() def test_debuglevel(self): mock_socket.reply_with(b"220 Hello world") - smtp = smtplib.SMTP() - smtp.set_debuglevel(1) + client = self.client() + client.set_debuglevel(1) with support.captured_stderr() as stderr: - smtp.connect(HOST, self.port) - smtp.close() + client.connect(HOST, self.port) + client.close() expected = re.compile(r"^connect:", re.MULTILINE) self.assertRegex(stderr.getvalue(), expected) def test_debuglevel_2(self): mock_socket.reply_with(b"220 Hello world") - smtp = smtplib.SMTP() - smtp.set_debuglevel(2) + client = self.client() + client.set_debuglevel(2) with support.captured_stderr() as stderr: - smtp.connect(HOST, self.port) - smtp.close() + client.connect(HOST, self.port) + client.close() expected = re.compile(r"^\d{2}:\d{2}:\d{2}\.\d{6} connect: ", re.MULTILINE) 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 def debugging_server(serv, serv_evt, client_evt): serv_evt.set() diff --git a/Misc/NEWS.d/next/Library/2020-01-14-22-16-07.bpo-39329.6OKGBn.rst b/Misc/NEWS.d/next/Library/2020-01-14-22-16-07.bpo-39329.6OKGBn.rst new file mode 100644 index 00000000000..1e3da4618b4 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2020-01-14-22-16-07.bpo-39329.6OKGBn.rst @@ -0,0 +1,2 @@ +:class:`~smtplib.LMTP` constructor now has an optional *timeout* parameter. +Patch by Dong-hee Na.