Skip to content

Commit c77adec

Browse files
committed
gh-73936: Add support for unicode in smtplib's logins/passwords.
Formally speaking, this also requires support for SASLprep, RFC 4013. MongoDB very kindly contributes its SASLprep implementation.
1 parent 2564f42 commit c77adec

File tree

4 files changed

+53
-41
lines changed

4 files changed

+53
-41
lines changed

Lib/smtplib.py

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
import datetime
5454
import sys
5555
from email.base64mime import body_encode as encode_base64
56+
from saslprep import saslprep
5657

5758
__all__ = ["SMTPException", "SMTPNotSupportedError", "SMTPServerDisconnected", "SMTPResponseException",
5859
"SMTPSenderRefused", "SMTPRecipientsRefused", "SMTPDataError",
@@ -638,7 +639,7 @@ def auth(self, mechanism, authobject, *, initial_response_ok=True):
638639
mechanism = mechanism.upper()
639640
initial_response = (authobject() if initial_response_ok else None)
640641
if initial_response is not None:
641-
response = encode_base64(initial_response.encode('ascii'), eol='')
642+
response = encode_base64(initial_response.encode('utf-8'), eol='')
642643
(code, resp) = self.docmd("AUTH", mechanism + " " + response)
643644
self._auth_challenge_count = 1
644645
else:
@@ -649,7 +650,7 @@ def auth(self, mechanism, authobject, *, initial_response_ok=True):
649650
self._auth_challenge_count += 1
650651
challenge = base64.decodebytes(resp)
651652
response = encode_base64(
652-
authobject(challenge).encode('ascii'), eol='')
653+
authobject(challenge).encode('utf-8'), eol='')
653654
(code, resp) = self.docmd(response)
654655
# If server keeps sending challenges, something is wrong.
655656
if self._auth_challenge_count > _MAXCHALLENGE:
@@ -667,21 +668,21 @@ def auth_cram_md5(self, challenge=None):
667668
# CRAM-MD5 does not support initial-response.
668669
if challenge is None:
669670
return None
670-
return self.user + " " + hmac.HMAC(
671-
self.password.encode('ascii'), challenge, 'md5').hexdigest()
671+
return saslprep(self.user) + " " + hmac.HMAC(
672+
saslprep(self.password).encode('utf-8'), challenge, 'md5').hexdigest()
672673

673674
def auth_plain(self, challenge=None):
674675
""" Authobject to use with PLAIN authentication. Requires self.user and
675676
self.password to be set."""
676-
return "\0%s\0%s" % (self.user, self.password)
677+
return "\0%s\0%s" % (saslprep(self.user), saslprep(self.password))
677678

678679
def auth_login(self, challenge=None):
679680
""" Authobject to use with LOGIN authentication. Requires self.user and
680681
self.password to be set."""
681682
if challenge is None or self._auth_challenge_count < 2:
682-
return self.user
683+
return saslprep(self.user)
683684
else:
684-
return self.password
685+
return saslprep(self.password)
685686

686687
def login(self, user, password, *, initial_response_ok=True):
687688
"""Log in on an SMTP server that requires authentication.

Lib/test/test_saslprep.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,8 @@
1313
# limitations under the License.
1414

1515
import sys
16-
17-
sys.path[0:0] = [""]
18-
19-
from test import unittest
20-
21-
from pymongo.saslprep import saslprep
16+
import unittest
17+
from saslprep import saslprep
2218

2319

2420
class TestSASLprep(unittest.TestCase):

Lib/test/test_smtplib.py

Lines changed: 40 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import email.mime.text
33
from email.message import EmailMessage
44
from email.base64mime import body_encode as encode_base64
5+
from saslprep import saslprep
56
import email.utils
67
import hashlib
78
import hmac
@@ -808,6 +809,10 @@ def testLineTooLong(self):
808809
}
809810

810811
sim_auth = ('Mr.A@somewhere.com', 'somepassword')
812+
sim_auths = {
813+
'Mr.A@somewhere.com' : 'somepassword',
814+
'भारत@भारत' : 'भारत@',
815+
'Mr.C@somewhere.com' : 'IX'}
811816
sim_cram_md5_challenge = ('PENCeUxFREJoU0NnbmhNWitOMjNGNn'
812817
'dAZWx3b29kLmlubm9zb2Z0LmNvbT4=')
813818
sim_lists = {'list-1':['Mr.A@somewhere.com','Mrs.C@somewhereesle.com'],
@@ -895,7 +900,7 @@ def _auth_plain(self, arg=None):
895900
self.push('535 Splitting response {!r} into user and password'
896901
' failed: {}'.format(logpass, e))
897902
return
898-
self._authenticated(user, password == sim_auth[1])
903+
self._authenticated(user, saslprep(password) == sim_auths[user])
899904

900905
def _auth_login(self, arg=None):
901906
if arg is None:
@@ -907,7 +912,8 @@ def _auth_login(self, arg=None):
907912
self.push('334 UGFzc3dvcmQ6')
908913
else:
909914
password = self._decode_base64(arg)
910-
self._authenticated(self._auth_login_user, password == sim_auth[1])
915+
self._authenticated(self._auth_login_user,
916+
saslprep(password) == sim_auths[self._auth_login_user])
911917
del self._auth_login_user
912918

913919
def _auth_buggy(self, arg=None):
@@ -927,8 +933,8 @@ def _auth_cram_md5(self, arg=None):
927933
'failed: {}'.format(logpass, e))
928934
return False
929935
valid_hashed_pass = hmac.HMAC(
930-
sim_auth[1].encode('ascii'),
931-
self._decode_base64(sim_cram_md5_challenge).encode('ascii'),
936+
saslprep(sim_auths[user].encode('utf-8')),
937+
self._decode_base64(sim_cram_md5_challenge).encode('utf-8'),
932938
'md5').hexdigest()
933939
self._authenticated(user, hashed_pass == valid_hashed_pass)
934940
# end AUTH related stuff.
@@ -1117,30 +1123,41 @@ def testEXPN(self):
11171123
self.assertEqual(smtp.expn(u), expected_unknown)
11181124
smtp.quit()
11191125

1126+
def helpAUTH_x(self, feature):
1127+
self.serv.add_feature(feature)
1128+
for username in sim_auths.keys():
1129+
with self.subTest(username=username):
1130+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
1131+
resp = smtp.login(username, sim_auths[username])
1132+
self.assertEqual(resp, (235, b'Authentication Succeeded'))
1133+
smtp.close()
1134+
with self.subTest(username=username):
1135+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
1136+
with self.assertRaises(smtplib.SMTPAuthenticationError):
1137+
smtp.login(username, "No" + sim_auths[username])
1138+
smtp.close()
1139+
with self.subTest(username=username):
1140+
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=15)
1141+
resp = smtp.login(username, sim_auths[username] + u"\u00AD")
1142+
self.assertEqual(resp, (235, b'Authentication Succeeded'))
1143+
smtp.close()
1144+
11201145
def testAUTH_PLAIN(self):
1121-
self.serv.add_feature("AUTH PLAIN")
1122-
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
1123-
timeout=support.LOOPBACK_TIMEOUT)
1124-
resp = smtp.login(sim_auth[0], sim_auth[1])
1125-
self.assertEqual(resp, (235, b'Authentication Succeeded'))
1126-
smtp.close()
1146+
self.helpAUTH_x("AUTH PLAIN")
11271147

11281148
def testAUTH_LOGIN(self):
1129-
self.serv.add_feature("AUTH LOGIN")
1130-
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
1131-
timeout=support.LOOPBACK_TIMEOUT)
1132-
resp = smtp.login(sim_auth[0], sim_auth[1])
1133-
self.assertEqual(resp, (235, b'Authentication Succeeded'))
1134-
smtp.close()
1149+
self.helpAUTH_x("AUTH LOGIN")
11351150

11361151
def testAUTH_LOGIN_initial_response_ok(self):
11371152
self.serv.add_feature("AUTH LOGIN")
1138-
with smtplib.SMTP(HOST, self.port, local_hostname='localhost',
1139-
timeout=support.LOOPBACK_TIMEOUT) as smtp:
1140-
smtp.user, smtp.password = sim_auth
1141-
smtp.ehlo("test_auth_login")
1142-
resp = smtp.auth("LOGIN", smtp.auth_login, initial_response_ok=True)
1143-
self.assertEqual(resp, (235, b'Authentication Succeeded'))
1153+
for username in sim_auths.keys():
1154+
with smtplib.SMTP(HOST, self.port, local_hostname='localhost',
1155+
timeout=support.LOOPBACK_TIMEOUT) as smtp:
1156+
smtp.user = username
1157+
smtp.password = sim_auths[username]
1158+
smtp.ehlo("test_auth_login")
1159+
resp = smtp.auth("LOGIN", smtp.auth_login, initial_response_ok=True)
1160+
self.assertEqual(resp, (235, b'Authentication Succeeded'))
11441161

11451162
def testAUTH_LOGIN_initial_response_notok(self):
11461163
self.serv.add_feature("AUTH LOGIN")
@@ -1183,12 +1200,7 @@ def testAUTH_CRAM_MD5(self):
11831200
@hashlib_helper.requires_hashdigest('md5', openssl=True)
11841201
def testAUTH_multiple(self):
11851202
# Test that multiple authentication methods are tried.
1186-
self.serv.add_feature("AUTH BOGUS PLAIN LOGIN CRAM-MD5")
1187-
smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost',
1188-
timeout=support.LOOPBACK_TIMEOUT)
1189-
resp = smtp.login(sim_auth[0], sim_auth[1])
1190-
self.assertEqual(resp, (235, b'Authentication Succeeded'))
1191-
smtp.close()
1203+
self.helpAUTH_x("AUTH BOGUS PLAIN LOGIN CRAM-MD5")
11921204

11931205
def test_auth_function(self):
11941206
supported = {'PLAIN', 'LOGIN'}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Added support for unicode logins/passwords for SMTP authentication.
2+
3+
This includes SASLprep support, graciously contributed by MongoDB.com.

0 commit comments

Comments
 (0)