diff --git a/Lib/smtplib.py b/Lib/smtplib.py index 84d6d858e7dec1..4316df93bbca5d 100644 --- a/Lib/smtplib.py +++ b/Lib/smtplib.py @@ -54,7 +54,7 @@ __all__ = ["SMTPException", "SMTPNotSupportedError", "SMTPServerDisconnected", "SMTPResponseException", "SMTPSenderRefused", "SMTPRecipientsRefused", "SMTPDataError", - "SMTPConnectError", "SMTPHeloError", "SMTPAuthenticationError", + "SMTPConnectError", "SMTPHeloError", "SMTPAuthenticationError", "SMTPAuthHashUnsupportedError", "quoteaddr", "quotedata", "SMTP"] SMTP_PORT = 25 @@ -141,6 +141,10 @@ class SMTPAuthenticationError(SMTPResponseException): combination provided. """ +class SMTPAuthHashUnsupportedError(SMTPException): + """Raised when the authentication mechanism uses a hash algorithm unsupported by the system.""" + + def quoteaddr(addrstring): """Quote a subset of the email addresses defined by RFC 821. @@ -665,8 +669,11 @@ def auth_cram_md5(self, challenge=None): # CRAM-MD5 does not support initial-response. if challenge is None: return None - return self.user + " " + hmac.HMAC( - self.password.encode('ascii'), challenge, 'md5').hexdigest() + try: + return self.user + " " + hmac.HMAC( + self.password.encode('ascii'), challenge, 'md5').hexdigest() + except ValueError as e: + raise SMTPAuthHashUnsupportedError(f'CRAM-MD5 failed: {e}') from e def auth_plain(self, challenge=None): """ Authobject to use with PLAIN authentication. Requires self.user and @@ -743,6 +750,12 @@ def login(self, user, password, *, initial_response_ok=True): return (code, resp) except SMTPAuthenticationError as e: last_exception = e + except SMTPAuthHashUnsupportedError as e: + # Some environments (e.g., FIPS) disable certain hashing algorithms like MD5, + # which are required by CRAM-MD5. This raises a SMTPAuthHashUnsupportedError when trying to use HMAC. + # If this happens, we catch the exception and continue trying the next auth method. + last_exception = e + continue # We could not login successfully. Return result of last attempt. raise last_exception diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py index 4c9fc14bd43f54..acbd1a1db24652 100644 --- a/Lib/test/test_smtplib.py +++ b/Lib/test/test_smtplib.py @@ -23,7 +23,7 @@ from test.support import threading_helper from test.support import asyncore from test.support import smtpd -from unittest.mock import Mock +from unittest.mock import Mock, patch support.requires_working_socket(module=True) @@ -1570,5 +1570,60 @@ def testAUTH_PLAIN_initial_response_auth(self): self.assertEqual(code, 235) +class TestSMTPLoginValueError(unittest.TestCase): + def broken_hmac(*args, **kwargs): + raise smtplib.SMTPAuthHashUnsupportedError("CRAM-MD5 failed: [digital envelope routines] unsupported") + + def test_login_raises_valueerror_when_cram_md5_fails(self): + with patch("hmac.HMAC", self.broken_hmac): + class FakeSMTP(smtplib.SMTP): + def __init__(self): + super().__init__(host='', port=0) + self.esmtp_features = {"auth": "CRAM-MD5"} + self._host = "localhost" + + def ehlo_or_helo_if_needed(self): + pass + + def has_extn(self, ext): + return ext.lower() == "auth" + + def docmd(self, *args, **kwargs): + # Retorna uma challenge base64 vĂ¡lida + return 334, b"Y2hhbGxlbmdl" + + smtp = FakeSMTP() + with self.assertRaises(smtplib.SMTPAuthHashUnsupportedError) as ctx: + smtp.login("user", "pass") + self.assertIn("unsupported", str(ctx.exception).lower()) + + def test_login_fallbacks_when_cram_md5_raises_valueerror(self): + with patch("hmac.HMAC", self.broken_hmac): + class FakeSMTP(smtplib.SMTP): + def __init__(self): + super().__init__(host='', port=0) + self.esmtp_features = {"auth": "CRAM-MD5 LOGIN"} + self._host = "localhost" + + def ehlo_or_helo_if_needed(self): + pass + + def has_extn(self, ext): + return ext.lower() == "auth" + + def docmd(self, *args, **kwargs): + if args[0] == "AUTH" and args[1].startswith("CRAM-MD5"): + return 334, b"Y2hhbGxlbmdl" # base64('challenge') + return 235, b"Authentication successful" + + def auth_login(self, challenge=None): + return "login response" + + smtp = FakeSMTP() + code, resp = smtp.login("user", "pass") + self.assertEqual(code, 235) + self.assertEqual(resp, b"Authentication successful") + + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2025-07-01-21-37-17.gh-issue-136134.NhFpu3.rst b/Misc/NEWS.d/next/Library/2025-07-01-21-37-17.gh-issue-136134.NhFpu3.rst new file mode 100644 index 00000000000000..e53bbafd092277 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-07-01-21-37-17.gh-issue-136134.NhFpu3.rst @@ -0,0 +1,2 @@ +Make smtplib fallback to next auth method if CRAM-MD5 fails due to +unsupported hash (e.g. FIPS). Contributed by Raphael Rodrigues.