Commit 3d23fd64 authored by Senthil Kumaran's avatar Senthil Kumaran

Fix closes Issue11281 - smtplib.STMP gets source_address parameter, which adds...

Fix closes Issue11281 - smtplib.STMP gets source_address parameter, which adds the ability to bind to specific source address on a machine with multiple interfaces. Patch by Paulo Scardine.
parent f83e4acb
...@@ -20,7 +20,7 @@ details of SMTP and ESMTP operation, consult :rfc:`821` (Simple Mail Transfer ...@@ -20,7 +20,7 @@ details of SMTP and ESMTP operation, consult :rfc:`821` (Simple Mail Transfer
Protocol) and :rfc:`1869` (SMTP Service Extensions). Protocol) and :rfc:`1869` (SMTP Service Extensions).
.. class:: SMTP(host='', port=0, local_hostname=None[, timeout]) .. class:: SMTP(host='', port=0, local_hostname=None[, timeout], source_address=None)
A :class:`SMTP` instance encapsulates an SMTP connection. It has methods A :class:`SMTP` instance encapsulates an SMTP connection. It has methods
that support a full repertoire of SMTP and ESMTP operations. If the optional that support a full repertoire of SMTP and ESMTP operations. If the optional
...@@ -29,7 +29,12 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions). ...@@ -29,7 +29,12 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions).
raised if the specified host doesn't respond correctly. The optional raised if the specified host doesn't respond correctly. The optional
*timeout* parameter specifies a timeout in seconds for blocking operations *timeout* parameter specifies a timeout in seconds for blocking operations
like the connection attempt (if not specified, the global default timeout like the connection attempt (if not specified, the global default timeout
setting will be used). setting will be used). The optional source_address parameter allows to bind to some
specific source address in a machine with multiple network interfaces,
and/or to some specific source tcp port. It takes a 2-tuple (host, port),
for the socket to bind to as its source address before connecting. If
ommited (or if host or port are '' and/or 0 respectively) the OS default
behavior will be used.
For normal use, you should only require the initialization/connect, For normal use, you should only require the initialization/connect,
:meth:`sendmail`, and :meth:`quit` methods. An example is included below. :meth:`sendmail`, and :meth:`quit` methods. An example is included below.
...@@ -48,8 +53,10 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions). ...@@ -48,8 +53,10 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions).
.. versionchanged:: 3.3 .. versionchanged:: 3.3
Support for the :keyword:`with` statement was added. Support for the :keyword:`with` statement was added.
.. versionadded:: 3.3
source_address parameter.
.. class:: SMTP_SSL(host='', port=0, local_hostname=None, keyfile=None, certfile=None[, timeout], context=None) .. class:: SMTP_SSL(host='', port=0, local_hostname=None, keyfile=None, certfile=None[, timeout], context=None, source_address=None)
A :class:`SMTP_SSL` instance behaves exactly the same as instances of A :class:`SMTP_SSL` instance behaves exactly the same as instances of
:class:`SMTP`. :class:`SMTP_SSL` should be used for situations where SSL is :class:`SMTP`. :class:`SMTP_SSL` should be used for situations where SSL is
...@@ -62,18 +69,28 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions). ...@@ -62,18 +69,28 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions).
keyfile and certfile must be None. The optional *timeout* keyfile and certfile must be None. The optional *timeout*
parameter specifies a timeout in seconds for blocking operations like the parameter specifies a timeout in seconds for blocking operations like the
connection attempt (if not specified, the global default timeout setting connection attempt (if not specified, the global default timeout setting
will be used). will be used). The optional source_address parameter allows to bind to some
specific source address in a machine with multiple network interfaces,
and/or to some specific source tcp port. It takes a 2-tuple (host, port),
for the socket to bind to as its source address before connecting. If
ommited (or if host or port are '' and/or 0 respectively) the OS default
behavior will be used.
.. versionchanged:: 3.3 .. versionchanged:: 3.3
*context* was added. *context* was added.
.. versionadded:: 3.3
source_address parameter.
.. class:: LMTP(host='', port=LMTP_PORT, local_hostname=None)
.. class:: LMTP(host='', port=LMTP_PORT, local_hostname=None, source_address=None)
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 :meth:`connect` standard SMTP client. It's common to use Unix sockets for LMTP, so our
method must support that as well as a regular host:port server. To specify a :meth:`connect` method must support that as well as a regular host:port
Unix socket, you must use an absolute path for *host*, starting with a '/'. server. The optional parameters local_hostname and source_address has the
same meaning as that of SMTP client.To specify a Unix socket, you must use
an absolute path for *host*, starting with a '/'.
Authentication is supported, using the regular SMTP mechanism. When using a Unix Authentication is supported, using the regular SMTP mechanism. When using a Unix
socket, LMTP generally don't support or require any authentication, but your socket, LMTP generally don't support or require any authentication, but your
......
...@@ -215,7 +215,8 @@ class SMTP: ...@@ -215,7 +215,8 @@ class SMTP:
default_port = SMTP_PORT default_port = SMTP_PORT
def __init__(self, host='', port=0, local_hostname=None, def __init__(self, host='', port=0, local_hostname=None,
timeout=socket._GLOBAL_DEFAULT_TIMEOUT): timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
source_address=None):
"""Initialize a new instance. """Initialize a new instance.
If specified, `host' is the name of the remote host to which to If specified, `host' is the name of the remote host to which to
...@@ -223,11 +224,16 @@ class SMTP: ...@@ -223,11 +224,16 @@ class SMTP:
By default, smtplib.SMTP_PORT is used. An SMTPConnectError is raised By default, smtplib.SMTP_PORT is used. An SMTPConnectError is raised
if the specified `host' doesn't respond correctly. If specified, if the specified `host' doesn't respond correctly. If specified,
`local_hostname` is used as the FQDN of the local host. By default, `local_hostname` is used as the FQDN of the local host. By default,
the local hostname is found using socket.getfqdn(). the local hostname is found using socket.getfqdn(). The
`source_address` parameter takes a 2-tuple (host, port) for the socket
to bind to as its source address before connecting. If the host is ''
and port is 0, the OS default behavior will be used.
""" """
self.timeout = timeout self.timeout = timeout
self.esmtp_features = {} self.esmtp_features = {}
self.source_address = source_address
if host: if host:
(code, msg) = self.connect(host, port) (code, msg) = self.connect(host, port)
if code != 220: if code != 220:
...@@ -276,10 +282,11 @@ class SMTP: ...@@ -276,10 +282,11 @@ class SMTP:
# This makes it simpler for SMTP_SSL to use the SMTP connect code # This makes it simpler for SMTP_SSL to use the SMTP connect code
# and just alter the socket connection bit. # and just alter the socket connection bit.
if self.debuglevel > 0: if self.debuglevel > 0:
print('connect:', (host, port), file=stderr) print('connect: to', (host, port), self.source_address, file=stderr)
return socket.create_connection((host, port), timeout) return socket.create_connection((host, port), timeout,
self.source_address)
def connect(self, host='localhost', port=0): def connect(self, host='localhost', port=0, source_address=None):
"""Connect to a host on a given port. """Connect to a host on a given port.
If the hostname ends with a colon (`:') followed by a number, and If the hostname ends with a colon (`:') followed by a number, and
...@@ -290,6 +297,7 @@ class SMTP: ...@@ -290,6 +297,7 @@ class SMTP:
specified during instantiation. specified during instantiation.
""" """
if source_address: self.source_address = source_address
if not port and (host.find(':') == host.rfind(':')): if not port and (host.find(':') == host.rfind(':')):
i = host.rfind(':') i = host.rfind(':')
if i >= 0: if i >= 0:
...@@ -829,7 +837,8 @@ if _have_ssl: ...@@ -829,7 +837,8 @@ if _have_ssl:
""" This is a subclass derived from SMTP that connects over an SSL encrypted """ This is a subclass derived from SMTP that connects over an SSL encrypted
socket (to use this class you need a socket module that was compiled with SSL socket (to use this class you need a socket module that was compiled with SSL
support). If host is not specified, '' (the local host) is used. If port is support). If host is not specified, '' (the local host) is used. If port is
omitted, the standard SMTP-over-SSL port (465) is used. keyfile and certfile omitted, the standard SMTP-over-SSL port (465) is used. The optional
source_address takes a two-tuple (host,port) for socket to bind to. keyfile and certfile
are also optional - they can contain a PEM formatted private key and are also optional - they can contain a PEM formatted private key and
certificate chain file for the SSL connection. context also optional, can contain certificate chain file for the SSL connection. context also optional, can contain
a SSLContext, and is an alternative to keyfile and certfile; If it is specified both a SSLContext, and is an alternative to keyfile and certfile; If it is specified both
...@@ -840,7 +849,8 @@ if _have_ssl: ...@@ -840,7 +849,8 @@ if _have_ssl:
def __init__(self, host='', port=0, local_hostname=None, def __init__(self, host='', port=0, local_hostname=None,
keyfile=None, certfile=None, keyfile=None, certfile=None,
timeout=socket._GLOBAL_DEFAULT_TIMEOUT, context=None): timeout=socket._GLOBAL_DEFAULT_TIMEOUT,
source_address=None, context=None):
if context is not None and keyfile is not None: if context is not None and keyfile is not None:
raise ValueError("context and keyfile arguments are mutually " raise ValueError("context and keyfile arguments are mutually "
"exclusive") "exclusive")
...@@ -850,12 +860,14 @@ if _have_ssl: ...@@ -850,12 +860,14 @@ if _have_ssl:
self.keyfile = keyfile self.keyfile = keyfile
self.certfile = certfile self.certfile = certfile
self.context = context self.context = context
SMTP.__init__(self, host, port, local_hostname, timeout) SMTP.__init__(self, host, port, local_hostname, timeout,
source_address)
def _get_socket(self, host, port, timeout): def _get_socket(self, host, port, timeout):
if self.debuglevel > 0: if self.debuglevel > 0:
print('connect:', (host, port), file=stderr) print('connect:', (host, port), file=stderr)
new_socket = socket.create_connection((host, port), timeout) new_socket = socket.create_connection((host, port), timeout,
self.source_address)
if self.context is not None: if self.context is not None:
new_socket = self.context.wrap_socket(new_socket) new_socket = self.context.wrap_socket(new_socket)
else: else:
...@@ -884,14 +896,16 @@ class LMTP(SMTP): ...@@ -884,14 +896,16 @@ 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):
"""Initialize a new instance.""" """Initialize a new instance."""
SMTP.__init__(self, host, port, local_hostname) SMTP.__init__(self, host, port, local_hostname = local_hostname,
source_address = source_address)
def connect(self, host='localhost', port=0): 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 SMTP.connect(self, host, port) return SMTP.connect(self, host, port, source_address = source_address)
# Handle Unix-domain sockets. # Handle Unix-domain sockets.
try: try:
......
...@@ -106,7 +106,8 @@ def socket(family=None, type=None, proto=None): ...@@ -106,7 +106,8 @@ def socket(family=None, type=None, proto=None):
return MockSocket() return MockSocket()
def create_connection(address, timeout=socket_module._GLOBAL_DEFAULT_TIMEOUT): def create_connection(address, timeout=socket_module._GLOBAL_DEFAULT_TIMEOUT,
source_address=None):
try: try:
int_port = int(address[1]) int_port = int(address[1])
except ValueError: except ValueError:
......
...@@ -72,6 +72,14 @@ class GeneralTests(unittest.TestCase): ...@@ -72,6 +72,14 @@ class GeneralTests(unittest.TestCase):
smtp = smtplib.SMTP(HOST, self.port) smtp = smtplib.SMTP(HOST, self.port)
smtp.close() smtp.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()
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
...@@ -206,6 +214,15 @@ class DebuggingServerTests(unittest.TestCase): ...@@ -206,6 +214,15 @@ class DebuggingServerTests(unittest.TestCase):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3) smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
smtp.quit() smtp.quit()
def testSourceAddress(self):
# connect
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3,
source_address=('127.0.0.1', 19876))
self.assertEqual(smtp.source_address, ('127.0.0.1', 19876))
self.assertEqual(smtp.local_hostname, 'localhost')
print(dir(smtp))
smtp.quit()
def testNOOP(self): def testNOOP(self):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3) smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
expected = (250, b'Ok') expected = (250, b'Ok')
......
...@@ -246,6 +246,10 @@ Core and Builtins ...@@ -246,6 +246,10 @@ Core and Builtins
Library Library
------- -------
- Issue #11281: smtplib.STMP gets source_address parameter, which adds the
ability to bind to specific source address on a machine with multiple
interfaces. Patch by Paulo Scardine.
- Issue #12464: tempfile.TemporaryDirectory.cleanup() should not follow - Issue #12464: tempfile.TemporaryDirectory.cleanup() should not follow
symlinks: fix it. Patch by Petri Lehtinen. symlinks: fix it. Patch by Petri Lehtinen.
......
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