Skip to content

bpo-39329: Add timeout parameter for LMTP #17998

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jan 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Doc/library/smtplib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:

Expand Down
3 changes: 3 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------

Expand Down
8 changes: 6 additions & 2 deletions Lib/smtplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
75 changes: 45 additions & 30 deletions Lib/test/test_smtplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -75,86 +75,101 @@ def testQuoteData(self):
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")
self.assertIsNone(mock_socket.getdefaulttimeout())
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()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
:class:`~smtplib.LMTP` constructor now has an optional *timeout* parameter.
Patch by Dong-hee Na.