Index: Doc/library/smtplib.rst =================================================================== --- Doc/library/smtplib.rst (revision 88674) +++ Doc/library/smtplib.rst (working copy) @@ -20,7 +20,7 @@ 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]]) 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,11 @@ 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. Index: Lib/smtplib.py =================================================================== --- Lib/smtplib.py (revision 88674) +++ Lib/smtplib.py (working copy) @@ -233,7 +233,8 @@ does_esmtp = 0 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 @@ -241,12 +242,17 @@ 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) like socket.create_connection, + for the socket to bind to as its source address before connecting. + If host or port are '' or 0 respectively the OS default behavior will + be used. """ self.timeout = timeout self.esmtp_features = {} self.default_port = SMTP_PORT + self.source_address = source_address if host: (code, msg) = self.connect(host, port) if code != 220: @@ -282,10 +288,11 @@ # 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 @@ -296,6 +303,7 @@ 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: @@ -806,16 +814,19 @@ """ def __init__(self, host='', port=0, local_hostname=None, keyfile=None, certfile=None, - timeout=socket._GLOBAL_DEFAULT_TIMEOUT): + timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=('', 0)): self.keyfile = keyfile self.certfile = certfile - SMTP.__init__(self, host, port, local_hostname, timeout) + SMTP.__init__(self, host, port, local_hostname, timeout, + source_address) self.default_port = SMTP_SSL_PORT 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) new_socket = ssl.wrap_socket(new_socket, self.keyfile, self.certfile) self.file = SSLFakeFile(new_socket) return new_socket Index: Lib/test/test_smtplib.py =================================================================== --- Lib/test/test_smtplib.py (revision 88674) +++ Lib/test/test_smtplib.py (working copy) @@ -72,6 +72,12 @@ 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, ('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 +212,12 @@ 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)) + smtp.quit() + def testNOOP(self): smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3) expected = (250, b'Ok') Index: Lib/test/mock_socket.py =================================================================== --- Lib/test/mock_socket.py (revision 88674) +++ Lib/test/mock_socket.py (working copy) @@ -106,7 +106,8 @@ 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: