Skip to content

Commit 7a2843b

Browse files
Address review comments for login_plain
* Use an empty SASL authorization identity (RFC 4616): the previous code sent the user name as authzid, requesting proxy authorization, which many servers reject. * Note that PLAIN is only base64-encoded and requires a TLS connection. * Cite RFC 4616 for the mechanism; RFC 6855 only for UTF-8 support. * Fix the versionadded marker and the documented parameter name. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 7de847c commit 7a2843b

5 files changed

Lines changed: 62 additions & 38 deletions

File tree

Doc/library/imaplib.rst

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -437,18 +437,20 @@ An :class:`IMAP4` instance has the following methods:
437437
An :exc:`IMAP4.error` is raised if MD5 support is not available.
438438

439439

440-
.. method:: IMAP4.login_plain(login, password)
440+
.. method:: IMAP4.login_plain(user, password)
441441

442-
Authenticate using PLAIN SASL mechanism.
442+
Authenticate using the ``PLAIN`` SASL mechanism (:rfc:`4616`).
443443

444-
This is a plain-text authentication mechanism that can be used
445-
instead of :meth:`IMAP4.login()` when UTF-8 support is required.
446-
See :RFC:`6855`, page 5.
444+
This is a plaintext authentication mechanism that can be used instead
445+
of :meth:`login` when UTF-8 support is required (see :rfc:`6855`).
446+
Since the credentials are only base64-encoded, not encrypted, this
447+
method should only be used over a TLS-protected connection, such as
448+
:class:`IMAP4_SSL` or after :meth:`starttls`.
447449

448-
It will only work if the server ``CAPABILITY`` response includes
449-
the phrase ``AUTH=PLAIN``.
450+
It will only work if the server ``CAPABILITY`` response includes
451+
the phrase ``AUTH=PLAIN``.
450452

451-
.. versionadded:: 3.11
453+
.. versionadded:: next
452454

453455

454456
.. method:: IMAP4.logout()

Lib/imaplib.py

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -740,15 +740,27 @@ def login(self, user, password):
740740

741741

742742
def login_plain(self, user, password):
743-
"""Authenticate using PLAIN SASL mechanism.
743+
"""Authenticate using the PLAIN SASL mechanism (RFC 4616).
744744
745-
This is a plain-text authentication mechanism that can be used
746-
instead of login() when UTF-8 support is required.
745+
This is a plaintext authentication mechanism that can be used
746+
instead of login() when UTF-8 support is required. Since the
747+
credentials are only base64-encoded, not encrypted, it should
748+
only be used over a TLS-protected connection.
749+
750+
'user' and 'password' can be strings (encoded to UTF-8) or
751+
bytes-like objects.
747752
"""
748-
return self.authenticate(
749-
"PLAIN",
750-
lambda _: "{0}\x00{0}\x00{1}".format(user, password).encode()
751-
)
753+
if isinstance(user, str):
754+
user = user.encode('utf-8')
755+
if isinstance(password, str):
756+
password = password.encode('utf-8')
757+
if b'\0' in user or b'\0' in password:
758+
raise ValueError("NUL is not allowed in user name or password")
759+
# An empty authorization identity (RFC 4616) makes the server
760+
# derive it from the authentication identity; a non-empty one
761+
# requests proxy authorization and is often rejected.
762+
response = b'\0' + bytes(user) + b'\0' + bytes(password)
763+
return self.authenticate("PLAIN", lambda _: response)
752764

753765

754766
def login_cram_md5(self, user, password):

Lib/test/test_imaplib.py

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,15 @@ def cmd_AUTHENTICATE(self, tag, args):
420420
self._send_tagged(tag, 'NO', 'No access')
421421

422422

423+
class AuthHandler_PLAIN(SimpleIMAPHandler):
424+
capabilities = 'LOGINDISABLED AUTH=PLAIN'
425+
def cmd_AUTHENTICATE(self, tag, args):
426+
self.server.auth_args = args
427+
self._send_textline('+')
428+
self.server.response = yield
429+
self._send_tagged(tag, 'OK', 'Logged in.')
430+
431+
423432
class NewIMAPTestsMixin:
424433
client = None
425434

@@ -693,34 +702,33 @@ def test_login_cram_md5_blocked(self):
693702
client.login_cram_md5("tim", b"tanstaaftanstaaf")
694703

695704
def test_login_plain_ascii(self):
696-
class AuthHandler(SimpleIMAPHandler):
697-
capabilities = 'LOGINDISABLED AUTH=PLAIN'
698-
def cmd_AUTHENTICATE(self, tag, args):
699-
self._send_textline('+')
700-
r = yield
701-
if r == b'cHJlbQBwcmVtAHBhc3M=\r\n':
702-
self._send_tagged(tag, 'OK', 'Logged in.')
703-
else:
704-
self._send_tagged(tag, 'NO', 'No access')
705-
client, _ = self._setup(AuthHandler)
706-
self.assertTrue('AUTH=PLAIN' in client.capabilities)
705+
client, server = self._setup(AuthHandler_PLAIN)
706+
self.assertIn('AUTH=PLAIN', client.capabilities)
707707
ret, _ = client.login_plain("prem", "pass")
708708
self.assertEqual(ret, "OK")
709+
self.assertEqual(server.auth_args, ['PLAIN'])
710+
self.assertEqual(server.response, b'AHByZW0AcGFzcw==\r\n')
709711

710712
def test_login_plain_utf8(self):
711-
class AuthHandler(SimpleIMAPHandler):
712-
capabilities = 'LOGINDISABLED AUTH=PLAIN'
713-
def cmd_AUTHENTICATE(self, tag, args):
714-
self._send_textline('+')
715-
r = yield
716-
if r == b'cHLEmW0AcHLEmW0AxbzDs8WCxIc=\r\n':
717-
self._send_tagged(tag, 'OK', 'Logged in.')
718-
else:
719-
self._send_tagged(tag, 'NO', 'No access')
720-
client, _ = self._setup(AuthHandler)
721-
self.assertTrue('AUTH=PLAIN' in client.capabilities)
713+
client, server = self._setup(AuthHandler_PLAIN)
714+
self.assertIn('AUTH=PLAIN', client.capabilities)
722715
ret, _ = client.login_plain("pręm", "żółć")
723716
self.assertEqual(ret, "OK")
717+
self.assertEqual(server.response, b'AHByxJltAMW8w7PFgsSH\r\n')
718+
719+
def test_login_plain_bytes(self):
720+
# bytes are accepted and sent verbatim (not re-encoded).
721+
client, server = self._setup(AuthHandler_PLAIN)
722+
ret, _ = client.login_plain(b'pr\xc4\x99m', b'\xc5\xbc\xc3\xb3\xc5\x82\xc4\x87')
723+
self.assertEqual(ret, "OK")
724+
self.assertEqual(server.response, b'AHByxJltAMW8w7PFgsSH\r\n')
725+
726+
def test_login_plain_nul(self):
727+
client, _ = self._setup(AuthHandler_PLAIN)
728+
with self.assertRaises(ValueError):
729+
client.login_plain('user\0', 'pass')
730+
with self.assertRaises(ValueError):
731+
client.login_plain('user', b'pa\0ss')
724732

725733
def test_aborted_authentication(self):
726734
class MyServer(SimpleIMAPHandler):

Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-45706.XG7aHz.rst

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Add :meth:`imaplib.IMAP4.login_plain`, which authenticates using the
2+
``PLAIN`` SASL mechanism (:rfc:`4616`). Unlike :meth:`~imaplib.IMAP4.login`,
3+
it supports non-ASCII user names and passwords.

0 commit comments

Comments
 (0)