def _fix_eols(data):
return re.sub(r'(?:\r\n|\n|\r(?!\n))', CRLF, data)
+
+try:
+ hmac.digest(b'', b'', 'md5')
+except ValueError:
+ _have_cram_md5_support = False
+else:
+ _have_cram_md5_support = True
+
+
try:
import ssl
except ImportError:
# 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()
+ if not _have_cram_md5_support:
+ raise SMTPException("CRAM-MD5 is not supported")
+ password = self.password.encode('ascii')
+ authcode = hmac.HMAC(password, challenge, 'md5')
+ return f"{self.user} {authcode.hexdigest()}"
def auth_plain(self, challenge=None):
""" Authobject to use with PLAIN authentication. Requires self.user and
advertised_authlist = self.esmtp_features["auth"].split()
# Authentication methods we can handle in our preferred order:
- preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']
-
- # We try the supported authentications in our preferred order, if
+ if _have_cram_md5_support:
+ preferred_auths = ['CRAM-MD5', 'PLAIN', 'LOGIN']
+ else:
+ preferred_auths = ['PLAIN', 'LOGIN']
# the server supports them.
authlist = [auth for auth in preferred_auths
if auth in advertised_authlist]
import threading
import unittest
+import unittest.mock as mock
from test import support, mock_socket
from test.support import hashlib_helper
from test.support import socket_helper
except ValueError as e:
self.push('535 Splitting response {!r} into user and password '
'failed: {}'.format(logpass, e))
- return False
- valid_hashed_pass = hmac.HMAC(
- sim_auth[1].encode('ascii'),
- self._decode_base64(sim_cram_md5_challenge).encode('ascii'),
- 'md5').hexdigest()
+ return
+ pwd = sim_auth[1].encode('ascii')
+ msg = self._decode_base64(sim_cram_md5_challenge).encode('ascii')
+ try:
+ valid_hashed_pass = hmac.HMAC(pwd, msg, 'md5').hexdigest()
+ except ValueError:
+ self.push('504 CRAM-MD5 is not supported')
+ return
self._authenticated(user, hashed_pass == valid_hashed_pass)
# end AUTH related stuff.
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()
+ @hashlib_helper.block_algorithm('md5')
+ @mock.patch("smtplib._have_cram_md5_support", False)
+ def testAUTH_CRAM_MD5_blocked(self):
+ # CRAM-MD5 is the only "known" method by the server,
+ # but it is not supported by the client. In particular,
+ # no challenge will ever be sent.
+ self.serv.add_feature("AUTH CRAM-MD5")
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
+ timeout=support.LOOPBACK_TIMEOUT)
+ self.addCleanup(smtp.close)
+ msg = re.escape("No suitable authentication method found.")
+ with self.assertRaisesRegex(smtplib.SMTPException, msg):
+ smtp.login(sim_auth[0], sim_auth[1])
+
+ @hashlib_helper.block_algorithm('md5')
+ @mock.patch("smtplib._have_cram_md5_support", False)
+ def testAUTH_CRAM_MD5_blocked_and_fallback(self):
+ # Test that PLAIN is tried after CRAM-MD5 failed
+ self.serv.add_feature("AUTH CRAM-MD5 PLAIN")
+ smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
+ timeout=support.LOOPBACK_TIMEOUT)
+ self.addCleanup(smtp.close)
+ with (
+ mock.patch.object(smtp, "auth_cram_md5") as smtp_auth_cram_md5,
+ mock.patch.object(
+ smtp, "auth_plain", wraps=smtp.auth_plain
+ ) as smtp_auth_plain
+ ):
+ resp = smtp.login(sim_auth[0], sim_auth[1])
+ smtp_auth_plain.assert_called_once()
+ smtp_auth_cram_md5.assert_not_called()
+ self.assertEqual(resp, (235, b'Authentication Succeeded'))
+
@hashlib_helper.requires_hashdigest('md5', openssl=True)
def testAUTH_multiple(self):
# Test that multiple authentication methods are tried.