Index: Lib/smtplib.py =================================================================== --- Lib/smtplib.py (revision 88500) +++ Lib/smtplib.py (working copy) @@ -232,7 +232,8 @@ does_esmtp = 0 def __init__(self, host='', port=0, local_hostname=None, - timeout=socket._GLOBAL_DEFAULT_TIMEOUT): + timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_ip='', + source_port=0): """Initialize a new instance. If specified, `host' is the name of the remote host to which to @@ -242,12 +243,18 @@ `local_hostname` is used as the FQDN of the local host. By default, the local hostname is found using socket.getfqdn(). + If `source_ip' and/or `source_port' are specified, the socket will bind + to this source IP address and/or port number before connecting. This is + useful in a system with multiple interfaces. When ommited, the behavior + will be according to the OS defaults. """ self.timeout = timeout self.esmtp_features = {} self.default_port = SMTP_PORT + self.source_ip = source_ip + self.source_port = source_port if host: - (code, msg) = self.connect(host, port) + (code, msg) = self.connect(host, port, source_ip, source_port) if code != 220: raise SMTPConnectError(code, msg) if local_hostname is not None: @@ -277,13 +284,15 @@ """ self.debuglevel = debuglevel - def _get_socket(self, host, port, timeout): + def _get_socket(self, host, port, timeout, source_ip='', source_port=0): # 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) + if self.debuglevel > 0: print('connect: to', (host, port), 'from' + (source_ip, source_port), file=stderr) + return socket.create_connection((host, port), timeout, + (source_ip, source_port)) - def connect(self, host='localhost', port = 0): + def connect(self, host='localhost', port = 0, source_ip='', source_port=0): """Connect to a host on a given port. If the hostname ends with a colon (`:') followed by a number, and @@ -302,8 +311,11 @@ except ValueError: raise socket.error("nonnumeric port") if not port: port = self.default_port + if self.source_ip and not source_ip: source_ip = self.source_ip + if self.source_port and not source_port: source_port = self.source_port if self.debuglevel > 0: print('connect:', (host, port), file=stderr) - self.sock = self._get_socket(host, port, self.timeout) + self.sock = self._get_socket(host, port, self.timeout, source_ip, + source_port) (code, msg) = self.getreply() if self.debuglevel > 0: print("connect:", msg, file=stderr) return (code, msg) @@ -799,15 +811,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_ip='', source_port=0): self.keyfile = keyfile self.certfile = certfile - SMTP.__init__(self, host, port, local_hostname, timeout) + SMTP.__init__(self, host, port, local_hostname, timeout, source_ip, + source_port) 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) + def _get_socket(self, host, port, timeout, source_ip='', source_port=0): + if self.debuglevel > 0: print('connect: to', (host, port), 'from' + (source_ip, source_port), file=stderr) + new_socket = socket.create_connection((host, port), timeout, + (source_ip, source_port)) 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 88500) +++ Lib/test/test_smtplib.py (working copy) @@ -78,6 +78,12 @@ smtp = smtplib.SMTP("%s:%s" % (HOST, self.port)) smtp.close() + def testSourceIPPort(self): + mock_socket.reply_with(b"220 Hola mundo") + smtp = smtplib.SMTP(HOST, self.port, source_ip='127.0.0.1', + source_port=19876) + smtp.close() + def testLocalHostName(self): mock_socket.reply_with(b"220 Hola mundo") # check that supplied local_hostname is used @@ -206,6 +212,12 @@ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3) smtp.quit() + def testSourceIPPort(self): + # connect + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3, + source_ip='127.0.0.1', source_port=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 88500) +++ 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: