From 58e0345a036591cf012b1d16ef7e91c62200bd65 Mon Sep 17 00:00:00 2001 From: Windson yang Date: Sun, 26 Aug 2018 15:25:21 +0800 Subject: [PATCH 1/3] bpo-29750: support non-ASCII passwords in smtplib --- Lib/smtplib.py | 7 ++-- Lib/test/test_smtplib.py | 85 ++++++++++++++++++++++++++-------------- 2 files changed, 59 insertions(+), 33 deletions(-) diff --git a/Lib/smtplib.py b/Lib/smtplib.py index b679875fd2c539..d64a107f309a85 100755 --- a/Lib/smtplib.py +++ b/Lib/smtplib.py @@ -417,6 +417,7 @@ def getreply(self): def docmd(self, cmd, args=""): """Send a command, and return its response code.""" + self.putcmd(cmd, args) return self.getreply() @@ -627,7 +628,7 @@ 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) @@ -635,7 +636,7 @@ def auth(self, mechanism, authobject, *, initial_response_ok=True): 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) @@ -648,7 +649,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 diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py index 495764f9aca902..948affc751f8ca 100644 --- a/Lib/test/test_smtplib.py +++ b/Lib/test/test_smtplib.py @@ -631,7 +631,11 @@ def testLineTooLong(self): 'Mrs.C@somewhereesle.com':'Ruth C', } -sim_auth = ('Mr.A@somewhere.com', 'somepassword') +sim_auth_latin = ('Mr.A@somewhere.com', 'somepassword') +sim_auth_nonlatin = ('Ms.D@example.com', 'p\u03B1sswor\u03B4') +sim_auth_nonvalid = ('Ms.E@example.com', '\ud800\ud800') +sim_auth_lists = [sim_auth_latin, sim_auth_nonlatin] + sim_cram_md5_challenge = ('PENCeUxFREJoU0NnbmhNWitOMjNGNn' 'dAZWx3b29kLmlubm9zb2Z0LmNvbT4=') sim_lists = {'list-1':['Mr.A@somewhere.com','Mrs.C@somewhereesle.com'], @@ -706,7 +710,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: @@ -719,7 +723,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 == self.sim_auth[1]) def _auth_login(self, arg=None): if arg is None: @@ -731,7 +735,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 == self.sim_auth[1]) del self._auth_login_user def _auth_cram_md5(self, arg=None): @@ -746,8 +750,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'), + self.sim_auth[1].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. @@ -920,7 +924,7 @@ 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' @@ -928,34 +932,53 @@ def testEXPN(self): self.assertEqual(smtp.expn(u), expected_unknown) smtp.quit() - def testAUTH_PLAIN(self): + def testAUTH_PLAIN_nonvalid(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(sim_auth_nonvalid[0], sim_auth_nonvalid[1]) smtp.close() + def testAUTH_PLAIN(self): + self.serv.add_feature("AUTH PLAIN") + for sim_auth in sim_auth_lists: + with self.subTest(sim_auth=sim_auth): + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15) + SimSMTPChannel.sim_auth = sim_auth + resp = smtp.login(SimSMTPChannel.sim_auth[0], SimSMTPChannel.sim_auth[1]) + 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 sim_auth in sim_auth_lists: + with self.subTest(sim_auth=sim_auth): + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15) + SimSMTPChannel.sim_auth = sim_auth + resp = smtp.login(sim_auth[0], sim_auth[1]) + self.assertEqual(resp, (235, b'Authentication Succeeded')) + 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 sim_auth in sim_auth_lists: + with self.subTest(sim_auth=sim_auth): + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15) + SimSMTPChannel.sim_auth = sim_auth + resp = smtp.login(sim_auth[0], sim_auth[1]) + 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 sim_auth in sim_auth_lists: + with self.subTest(sim_auth=sim_auth): + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15) + SimSMTPChannel.sim_auth = sim_auth + resp = smtp.login(sim_auth[0], sim_auth[1]) + self.assertEqual(resp, (235, b'Authentication Succeeded')) + smtp.close() def test_auth_function(self): supported = {'CRAM-MD5', 'PLAIN', 'LOGIN'} @@ -963,14 +986,16 @@ def test_auth_function(self): 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 sim_auth in sim_auth_lists: + smtp = smtplib.SMTP(HOST, self.port, + local_hostname='localhost', timeout=15) + smtp.ehlo('foo') + SimSMTPChannel.sim_auth = sim_auth + 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() def test_quit_resets_greeting(self): smtp = smtplib.SMTP(HOST, self.port, From 0b087279520a233553ffa764fcf2bc3286478429 Mon Sep 17 00:00:00 2001 From: Windsooon Date: Thu, 1 Aug 2019 11:57:31 +0800 Subject: [PATCH 2/3] fix tests based on review --- Lib/smtplib.py | 1 - Lib/test/test_smtplib.py | 64 ++++++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/Lib/smtplib.py b/Lib/smtplib.py index d64a107f309a85..5f98befe3571b4 100755 --- a/Lib/smtplib.py +++ b/Lib/smtplib.py @@ -417,7 +417,6 @@ def getreply(self): def docmd(self, cmd, args=""): """Send a command, and return its response code.""" - self.putcmd(cmd, args) return self.getreply() diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py index 948affc751f8ca..380578c3942e1a 100644 --- a/Lib/test/test_smtplib.py +++ b/Lib/test/test_smtplib.py @@ -631,10 +631,12 @@ def testLineTooLong(self): 'Mrs.C@somewhereesle.com':'Ruth C', } -sim_auth_latin = ('Mr.A@somewhere.com', 'somepassword') -sim_auth_nonlatin = ('Ms.D@example.com', 'p\u03B1sswor\u03B4') -sim_auth_nonvalid = ('Ms.E@example.com', '\ud800\ud800') -sim_auth_lists = [sim_auth_latin, sim_auth_nonlatin] +# '密码' 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=') @@ -723,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 == self.sim_auth[1]) + self._authenticated(user, password == valid_sim_auths[user]) def _auth_login(self, arg=None): if arg is None: @@ -735,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 == self.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): @@ -750,7 +752,7 @@ def _auth_cram_md5(self, arg=None): 'failed: {}'.format(logpass, e)) return False valid_hashed_pass = hmac.HMAC( - self.sim_auth[1].encode('utf-8'), + 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) @@ -932,51 +934,56 @@ def testEXPN(self): self.assertEqual(smtp.expn(u), expected_unknown) smtp.quit() - def testAUTH_PLAIN_nonvalid(self): + def testAUTH_PLAIN_UnicodeEncodeError(self): self.serv.add_feature("AUTH PLAIN") smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15) with self.assertRaises(UnicodeEncodeError): - resp = smtp.login(sim_auth_nonvalid[0], sim_auth_nonvalid[1]) + resp = smtp.login(invalid_sim_auth[0], invalid_sim_auth[1]) smtp.close() def testAUTH_PLAIN(self): self.serv.add_feature("AUTH PLAIN") - for sim_auth in sim_auth_lists: - with self.subTest(sim_auth=sim_auth): + 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) - SimSMTPChannel.sim_auth = sim_auth - resp = smtp.login(SimSMTPChannel.sim_auth[0], SimSMTPChannel.sim_auth[1]) + resp = smtp.login(username, password) self.assertEqual(resp, (235, b'Authentication Succeeded')) smtp.close() def testAUTH_LOGIN(self): self.serv.add_feature("AUTH LOGIN") - for sim_auth in sim_auth_lists: - with self.subTest(sim_auth=sim_auth): + 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) - SimSMTPChannel.sim_auth = sim_auth - resp = smtp.login(sim_auth[0], sim_auth[1]) + 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") - for sim_auth in sim_auth_lists: - with self.subTest(sim_auth=sim_auth): + 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) - SimSMTPChannel.sim_auth = sim_auth - resp = smtp.login(sim_auth[0], sim_auth[1]) + 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") - for sim_auth in sim_auth_lists: - with self.subTest(sim_auth=sim_auth): + 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) - SimSMTPChannel.sim_auth = sim_auth - resp = smtp.login(sim_auth[0], sim_auth[1]) + resp = smtp.login(username, password) self.assertEqual(resp, (235, b'Authentication Succeeded')) smtp.close() @@ -985,13 +992,12 @@ def test_auth_function(self): for mechanism in supported: self.serv.add_feature("AUTH {}".format(mechanism)) for mechanism in supported: - with self.subTest(mechanism=mechanism): - for sim_auth in sim_auth_lists: + 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') - SimSMTPChannel.sim_auth = sim_auth - smtp.user, smtp.password = sim_auth[0], sim_auth[1] + 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')) From 7d12a7ff10a07ea3154450eb728d265ae2ecdc30 Mon Sep 17 00:00:00 2001 From: "blurb-it[bot]" <43283697+blurb-it[bot]@users.noreply.github.com> Date: Thu, 1 Aug 2019 04:13:27 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=93=9C=F0=9F=A4=96=20Added=20by=20blu?= =?UTF-8?q?rb=5Fit.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../NEWS.d/next/Library/2019-08-01-04-13-26.bpo-29750.jJGCMv.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 Misc/NEWS.d/next/Library/2019-08-01-04-13-26.bpo-29750.jJGCMv.rst diff --git a/Misc/NEWS.d/next/Library/2019-08-01-04-13-26.bpo-29750.jJGCMv.rst b/Misc/NEWS.d/next/Library/2019-08-01-04-13-26.bpo-29750.jJGCMv.rst new file mode 100644 index 00000000000000..2aad1420201699 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-08-01-04-13-26.bpo-29750.jJGCMv.rst @@ -0,0 +1 @@ +Let smtplib support non-ASCII passwords \ No newline at end of file