Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-29750: support non-ASCII passwords in smtplib #15064

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions Lib/smtplib.py
Expand Up @@ -627,15 +627,15 @@ def auth(self, mechanism, authobject, *, initial_response_ok=True):
mechanism = mechanism.upper()
initial_response = (authobject() if initial_response_ok else None)
if initial_response is not None:
response = encode_base64(initial_response.encode('ascii'), eol='')
response = encode_base64(initial_response.encode('utf-8'), eol='')
(code, resp) = self.docmd("AUTH", mechanism + " " + response)
else:
(code, resp) = self.docmd("AUTH", mechanism)
# If server responds with a challenge, send the response.
if code == 334:
challenge = base64.decodebytes(resp)
response = encode_base64(
authobject(challenge).encode('ascii'), eol='')
authobject(challenge).encode('utf-8'), eol='')
(code, resp) = self.docmd(response)
if code in (235, 503):
return (code, resp)
Expand All @@ -648,7 +648,7 @@ def auth_cram_md5(self, challenge=None):
if challenge is None:
return None
return self.user + " " + hmac.HMAC(
self.password.encode('ascii'), challenge, 'md5').hexdigest()
self.password.encode('utf-8'), challenge, 'md5').hexdigest()

def auth_plain(self, challenge=None):
""" Authobject to use with PLAIN authentication. Requires self.user and
Expand Down
93 changes: 62 additions & 31 deletions Lib/test/test_smtplib.py
Expand Up @@ -631,7 +631,13 @@ def testLineTooLong(self):
'Mrs.C@somewhereesle.com':'Ruth C',
}

sim_auth = ('Mr.A@somewhere.com', 'somepassword')
# '密码' means password in Chinese.
valid_sim_auths = {
'Mr.A@somewhere.com': 'somepassword',
'Ms.D@example.com': '密码'}

invalid_sim_auth = ('Ms.E@example.com', '\ud800\ud800')

sim_cram_md5_challenge = ('PENCeUxFREJoU0NnbmhNWitOMjNGNn'
'dAZWx3b29kLmlubm9zb2Z0LmNvbT4=')
sim_lists = {'list-1':['Mr.A@somewhere.com','Mrs.C@somewhereesle.com'],
Expand Down Expand Up @@ -706,7 +712,7 @@ def _authenticated(self, user, valid):
self.smtp_state = self.COMMAND

def _decode_base64(self, string):
return base64.decodebytes(string.encode('ascii')).decode('utf-8')
return base64.decodebytes(string.encode('utf-8')).decode('utf-8')

def _auth_plain(self, arg=None):
if arg is None:
Expand All @@ -719,7 +725,7 @@ def _auth_plain(self, arg=None):
self.push('535 Splitting response {!r} into user and password'
' failed: {}'.format(logpass, e))
return
self._authenticated(user, password == sim_auth[1])
self._authenticated(user, password == valid_sim_auths[user])

def _auth_login(self, arg=None):
if arg is None:
Expand All @@ -731,7 +737,7 @@ def _auth_login(self, arg=None):
self.push('334 UGFzc3dvcmQ6')
else:
password = self._decode_base64(arg)
self._authenticated(self._auth_login_user, password == sim_auth[1])
self._authenticated(self._auth_login_user, password == valid_sim_auths[self._auth_login_user])
del self._auth_login_user

def _auth_cram_md5(self, arg=None):
Expand All @@ -746,8 +752,8 @@ def _auth_cram_md5(self, arg=None):
'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'),
valid_sim_auths[user].encode('utf-8'),
self._decode_base64(sim_cram_md5_challenge).encode('utf-8'),
'md5').hexdigest()
self._authenticated(user, hashed_pass == valid_hashed_pass)
# end AUTH related stuff.
Expand Down Expand Up @@ -920,57 +926,82 @@ def testEXPN(self):
users = []
for m in members:
users.append('%s %s' % (sim_users[m], smtplib.quoteaddr(m)))
expected_known = (250, bytes('\n'.join(users), "ascii"))
expected_known = (250, bytes('\n'.join(users), "utf-8"))
self.assertEqual(smtp.expn(listname), expected_known)

u = 'PSU-Members-List'
expected_unknown = (550, b'No access for you!')
self.assertEqual(smtp.expn(u), expected_unknown)
smtp.quit()

def testAUTH_PLAIN(self):
def testAUTH_PLAIN_UnicodeEncodeError(self):
self.serv.add_feature("AUTH PLAIN")
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
resp = smtp.login(sim_auth[0], sim_auth[1])
self.assertEqual(resp, (235, b'Authentication Succeeded'))
with self.assertRaises(UnicodeEncodeError):
resp = smtp.login(invalid_sim_auth[0], invalid_sim_auth[1])
smtp.close()

def testAUTH_PLAIN(self):
self.serv.add_feature("AUTH PLAIN")
for username, password in valid_sim_auths.items():
with self.subTest(username=username, password=password):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
resp = smtp.login(username, password)
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()

def testAUTH_LOGIN(self):
self.serv.add_feature("AUTH LOGIN")
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
resp = smtp.login(sim_auth[0], sim_auth[1])
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()
for username, password in valid_sim_auths.items():
with self.subTest(username=username, password=password):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
resp = smtp.login(username, password)
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()

def testAUTH_LOGIN_wrong_password(self):
self.serv.add_feature("AUTH LOGIN")
for username, password in valid_sim_auths.items():
with self.subTest(username=username, password=password):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
with self.assertRaises(smtplib.SMTPAuthenticationError):
resp = smtp.login(username, 'random')
smtp.close()

def testAUTH_CRAM_MD5(self):
self.serv.add_feature("AUTH CRAM-MD5")
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
resp = smtp.login(sim_auth[0], sim_auth[1])
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()
for username, password in valid_sim_auths.items():
with self.subTest(username=username, password=password):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
resp = smtp.login(username, password)
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()

def testAUTH_multiple(self):
# Test that multiple authentication methods are tried.
self.serv.add_feature("AUTH BOGUS PLAIN LOGIN CRAM-MD5")
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
resp = smtp.login(sim_auth[0], sim_auth[1])
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()
for username, password in valid_sim_auths.items():
with self.subTest(username=username, password=password):
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
resp = smtp.login(username, password)
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()

def test_auth_function(self):
supported = {'CRAM-MD5', 'PLAIN', 'LOGIN'}
for mechanism in supported:
self.serv.add_feature("AUTH {}".format(mechanism))
for mechanism in supported:
with self.subTest(mechanism=mechanism):
smtp = smtplib.SMTP(HOST, self.port,
local_hostname='localhost', timeout=15)
smtp.ehlo('foo')
smtp.user, smtp.password = sim_auth[0], sim_auth[1]
method = 'auth_' + mechanism.lower().replace('-', '_')
resp = smtp.auth(mechanism, getattr(smtp, method))
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()
for username, password in valid_sim_auths.items():
with self.subTest(mechanism=mechanism):
smtp = smtplib.SMTP(HOST, self.port,
local_hostname='localhost', timeout=15)
smtp.ehlo('foo')
smtp.user, smtp.password = username, password
method = 'auth_' + mechanism.lower().replace('-', '_')
resp = smtp.auth(mechanism, getattr(smtp, method))
self.assertEqual(resp, (235, b'Authentication Succeeded'))
smtp.close()

def test_quit_resets_greeting(self):
smtp = smtplib.SMTP(HOST, self.port,
Expand Down
@@ -0,0 +1 @@
Let smtplib support non-ASCII passwords