diff --git a/Lib/logging/handlers.py b/Lib/logging/handlers.py index fb6b5f3b411b227..c4d48d62c22af0d 100644 --- a/Lib/logging/handlers.py +++ b/Lib/logging/handlers.py @@ -1121,35 +1121,38 @@ def emit(self, record): if not port: port = smtplib.SMTP_PORT smtp = smtplib.SMTP(self.mailhost, port, timeout=self.timeout) - msg = EmailMessage() - msg['From'] = self.fromaddr - msg['To'] = ','.join(self.toaddrs) - msg['Subject'] = self.getSubject(record) - msg['Date'] = email.utils.localtime() - msg.set_content(self.format(record)) - if self.username: - if self.secure is not None: - import ssl - - try: - keyfile = self.secure[0] - except IndexError: - keyfile = None - - try: - certfile = self.secure[1] - except IndexError: - certfile = None - - context = ssl._create_stdlib_context( - certfile=certfile, keyfile=keyfile - ) - smtp.ehlo() - smtp.starttls(context=context) - smtp.ehlo() - smtp.login(self.username, self.password) - smtp.send_message(msg) - smtp.quit() + try: + msg = EmailMessage() + msg['From'] = self.fromaddr + msg['To'] = ','.join(self.toaddrs) + msg['Subject'] = self.getSubject(record) + msg['Date'] = email.utils.localtime() + msg.set_content(self.format(record)) + if self.username: + if self.secure is not None: + import ssl + + try: + keyfile = self.secure[0] + except IndexError: + keyfile = None + + try: + certfile = self.secure[1] + except IndexError: + certfile = None + + context = ssl._create_stdlib_context( + certfile=certfile, keyfile=keyfile + ) + smtp.ehlo() + smtp.starttls(context=context) + smtp.ehlo() + smtp.login(self.username, self.password) + smtp.send_message(msg) + smtp.quit() + finally: + smtp.close() except Exception: self.handleError(record) diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 7cd0df3ea0b62d2..b20b20ed59c844b 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -1175,6 +1175,19 @@ def process_message(self, *args): self.messages.append(args) self.handled.set() + @patch('smtplib.SMTP') + def test_connection_closed_on_error(self, mock_smtp): + # gh-155946: the connection must be closed even if sending fails. + instance_mock_smtp = mock_smtp.return_value + instance_mock_smtp.send_message.side_effect = OSError('sending failed') + h = logging.handlers.SMTPHandler('localhost', 'me', 'you', 'Log') + h.handleError = Mock() + r = logging.makeLogRecord({'msg': 'Hello'}) + h.emit(r) + instance_mock_smtp.close.assert_called() + h.handleError.assert_called_with(r) + h.close() + class MemoryHandlerTest(BaseTest): """Tests for the MemoryHandler.""" diff --git a/Misc/NEWS.d/next/Library/2026-08-17-21-15-00.gh-issue-155946.Xq3mLp.rst b/Misc/NEWS.d/next/Library/2026-08-17-21-15-00.gh-issue-155946.Xq3mLp.rst new file mode 100644 index 000000000000000..649817dabfbe84b --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-17-21-15-00.gh-issue-155946.Xq3mLp.rst @@ -0,0 +1,3 @@ +Fix a connection leak in :class:`logging.handlers.SMTPHandler`: ``emit()`` +now closes the SMTP connection when sending the record fails, instead of +leaving the cleanup to the garbage collector.