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.

This commit is contained in:
Senthil Kumaran 2011-07-30 10:56:50 +08:00
parent f83e4acbae
commit 3d23fd6493
5 changed files with 75 additions and 22 deletions

View File

@ -20,7 +20,7 @@ details of SMTP and ESMTP operation, consult :rfc:`821` (Simple Mail Transfer
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
that support a full repertoire of SMTP and ESMTP operations. If the optional
@ -29,7 +29,12 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions).
raised if the specified host doesn't respond correctly. The optional
*timeout* parameter specifies a timeout in seconds for blocking operations
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,
:meth:`sendmail`, and :meth:`quit` methods. An example is included below.
@ -48,8 +53,10 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions).
.. versionchanged:: 3.3
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
:class:`SMTP`. :class:`SMTP_SSL` should be used for situations where SSL is
@ -62,18 +69,28 @@ Protocol) and :rfc:`1869` (SMTP Service Extensions).
keyfile and certfile must be None. The optional *timeout*
parameter specifies a timeout in seconds for blocking operations like the
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
*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
standard SMTP client. It's common to use Unix sockets for LMTP, so our :meth:`connect`
method must support that as well as a regular host:port server. To specify a
Unix socket, you must use an absolute path for *host*, starting with a '/'.
standard SMTP client. It's common to use Unix sockets for LMTP, so our
:meth:`connect` method must support that as well as a regular host:port
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
socket, LMTP generally don't support or require any authentication, but your

View File

@ -215,7 +215,8 @@ class SMTP:
default_port = SMTP_PORT
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.
If specified, `host' is the name of the remote host to which to
@ -223,11 +224,16 @@ class SMTP:
By default, smtplib.SMTP_PORT is used. An SMTPConnectError is raised
if the specified `host' doesn't respond correctly. If specified,
`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.esmtp_features = {}
self.source_address = source_address
if host:
(code, msg) = self.connect(host, port)
if code != 220:
@ -276,10 +282,11 @@ class SMTP:
# This makes it simpler for SMTP_SSL to use the SMTP connect code
# and just alter the socket connection bit.
if self.debuglevel > 0:
print('connect:', (host, port), file=stderr)
return socket.create_connection((host, port), timeout)
print('connect: to', (host, port), self.source_address, file=stderr)
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.
If the hostname ends with a colon (`:') followed by a number, and
@ -290,6 +297,7 @@ class SMTP:
specified during instantiation.
"""
if source_address: self.source_address = source_address
if not port and (host.find(':') == host.rfind(':')):
i = host.rfind(':')
if i >= 0:
@ -829,7 +837,8 @@ if _have_ssl:
""" 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
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
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
@ -840,7 +849,8 @@ if _have_ssl:
def __init__(self, host='', port=0, local_hostname=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:
raise ValueError("context and keyfile arguments are mutually "
"exclusive")
@ -850,12 +860,14 @@ if _have_ssl:
self.keyfile = keyfile
self.certfile = certfile
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):
if self.debuglevel > 0:
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:
new_socket = self.context.wrap_socket(new_socket)
else:
@ -884,14 +896,16 @@ class LMTP(SMTP):
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."""
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."""
if host[0] != '/':
return SMTP.connect(self, host, port)
return SMTP.connect(self, host, port, source_address = source_address)
# Handle Unix-domain sockets.
try:

View File

@ -106,7 +106,8 @@ def socket(family=None, type=None, proto=None):
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:
int_port = int(address[1])
except ValueError:

View File

@ -72,6 +72,14 @@ class GeneralTests(unittest.TestCase):
smtp = smtplib.SMTP(HOST, self.port)
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):
mock_socket.reply_with(b"220 Hola mundo")
# connects, include port in host name
@ -206,6 +214,15 @@ class DebuggingServerTests(unittest.TestCase):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
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):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3)
expected = (250, b'Ok')

View File

@ -246,6 +246,10 @@ Core and Builtins
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
symlinks: fix it. Patch by Petri Lehtinen.