diff --git a/aiosmtpd/controller.py b/aiosmtpd/controller.py index 5e07eb4b..03e139fc 100644 --- a/aiosmtpd/controller.py +++ b/aiosmtpd/controller.py @@ -440,6 +440,10 @@ def _trigger_server(self): # At this point, if self.hostname is Falsy, it most likely is "" (bind to all # addresses). In such case, it should be safe to connect to localhost) hostname = self.hostname or self._localhost + # If port is 0, we need to get the port that the OS assigned so that we + # can connect. + if self.port == 0: + self.port = self.server.sockets[0].getsockname()[1] with ExitStack() as stk: s = stk.enter_context(create_connection((hostname, self.port), 1.0)) if self.ssl_context: diff --git a/aiosmtpd/docs/NEWS.rst b/aiosmtpd/docs/NEWS.rst index cf7251e0..da587cef 100644 --- a/aiosmtpd/docs/NEWS.rst +++ b/aiosmtpd/docs/NEWS.rst @@ -14,6 +14,7 @@ Fixed/Improved -------------- * All Controllers now have more rationale design, as they are now composited from a Base + a Mixin * A whole bunch of annotations +* Allow using port=0 with TCP controllers to use an OS-assigned port (Closes #276). 1.4.4.post2 (2023-01-19) diff --git a/aiosmtpd/tests/test_server.py b/aiosmtpd/tests/test_server.py index 656e963a..6cdcb23b 100644 --- a/aiosmtpd/tests/test_server.py +++ b/aiosmtpd/tests/test_server.py @@ -297,6 +297,15 @@ def test_hostname_none(self): finally: cont.stop() + def test_port_zero(self): + cont = Controller(Sink(), port=0) + try: + cont.start() + # Ensure port on controller has been populated with the OS-assigned port + assert cont.port != 0 + finally: + cont.stop() + def test_testconn_raises(self, mocker: MockFixture): mocker.patch("socket.socket.recv", side_effect=RuntimeError("MockError")) cont = Controller(Sink(), hostname="")