Skip to content

Commit 93b1a88

Browse files
authored
bpo-45706: Add imaplib.IMAP4.login_plain (GH-29398)
Adds authentication using PLAIN SASL mechanism. This is a plain-text authentication mechanism which can be used instead of IMAP4.login() when UTF-8 support is required.
1 parent d733b10 commit 93b1a88

6 files changed

Lines changed: 90 additions & 2 deletions

File tree

Doc/library/imaplib.rst

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,13 +443,30 @@ An :class:`IMAP4` instance has the following methods:
443443
.. method:: IMAP4.login_cram_md5(user, password)
444444

445445
Force use of ``CRAM-MD5`` authentication when identifying the client to protect
446-
the password. Will only work if the server ``CAPABILITY`` response includes the
447-
phrase ``AUTH=CRAM-MD5``.
446+
the password. It will only work if the server ``CAPABILITY`` response includes
447+
the phrase ``AUTH=CRAM-MD5``.
448448

449449
.. versionchanged:: 3.15
450450
An :exc:`IMAP4.error` is raised if MD5 support is not available.
451451

452452

453+
.. method:: IMAP4.login_plain(user, password)
454+
455+
Authenticate using the ``PLAIN`` SASL mechanism (:rfc:`4616`).
456+
457+
This is a plaintext authentication mechanism that can be used instead
458+
of :meth:`login` when UTF-8 support is required (see :rfc:`6855`).
459+
Since the credentials are only base64-encoded, not encrypted, this
460+
method should only be used over a TLS-protected connection, such as
461+
:class:`IMAP4_SSL` or after :meth:`starttls`.
462+
463+
It will only work if the server supports the ``PLAIN`` mechanism,
464+
which it need not advertise as ``AUTH=PLAIN`` in its ``CAPABILITY``
465+
response.
466+
467+
.. versionadded:: next
468+
469+
453470
.. method:: IMAP4.logout()
454471

455472
Shutdown connection to server. Returns server ``BYE`` response.

Doc/whatsnew/3.16.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,11 @@ imaplib
239239
a wrapper for the ``MOVE`` command (:rfc:`6851`).
240240
(Contributed by Serhiy Storchaka in :gh:`77508`.)
241241

242+
* Add the :meth:`~imaplib.IMAP4.login_plain` method, which authenticates
243+
using the ``PLAIN`` SASL mechanism (:rfc:`4616`) and supports non-ASCII
244+
user names and passwords.
245+
(Contributed by Przemysław Buczkowski and Serhiy Storchaka in :gh:`89869`.)
246+
242247

243248
logging
244249
-------

Lib/imaplib.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,30 @@ def login(self, user, password):
762762
return typ, dat
763763

764764

765+
def login_plain(self, user, password):
766+
"""Authenticate using the PLAIN SASL mechanism (RFC 4616).
767+
768+
This is a plaintext authentication mechanism that can be used
769+
instead of login() when UTF-8 support is required. Since the
770+
credentials are only base64-encoded, not encrypted, it should
771+
only be used over a TLS-protected connection.
772+
773+
'user' and 'password' can be strings (encoded to UTF-8) or
774+
bytes-like objects.
775+
"""
776+
if isinstance(user, str):
777+
user = user.encode('utf-8')
778+
if isinstance(password, str):
779+
password = password.encode('utf-8')
780+
if b'\0' in user or b'\0' in password:
781+
raise ValueError("NUL is not allowed in user name or password")
782+
# An empty authorization identity (RFC 4616) makes the server
783+
# derive it from the authentication identity; a non-empty one
784+
# requests proxy authorization and is often rejected.
785+
response = b'\0' + bytes(user) + b'\0' + bytes(password)
786+
return self.authenticate("PLAIN", lambda _: response)
787+
788+
765789
def login_cram_md5(self, user, password):
766790
""" Force use of CRAM-MD5 authentication.
767791

Lib/test/test_imaplib.py

Lines changed: 38 additions & 0 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

@@ -692,6 +701,35 @@ def test_login_cram_md5_blocked(self):
692701
with self.assertRaisesRegex(imaplib.IMAP4.error, msg):
693702
client.login_cram_md5("tim", b"tanstaaftanstaaf")
694703

704+
def test_login_plain_ascii(self):
705+
client, server = self._setup(AuthHandler_PLAIN)
706+
self.assertIn('AUTH=PLAIN', client.capabilities)
707+
ret, _ = client.login_plain("prem", "pass")
708+
self.assertEqual(ret, "OK")
709+
self.assertEqual(server.auth_args, ['PLAIN'])
710+
self.assertEqual(server.response, b'AHByZW0AcGFzcw==\r\n')
711+
712+
def test_login_plain_utf8(self):
713+
client, server = self._setup(AuthHandler_PLAIN)
714+
self.assertIn('AUTH=PLAIN', client.capabilities)
715+
ret, _ = client.login_plain("pręm", "żółć")
716+
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')
732+
695733
def test_aborted_authentication(self):
696734
class MyServer(SimpleIMAPHandler):
697735
def cmd_AUTHENTICATE(self, tag, args):

Misc/ACKS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,7 @@ Stan Bubrouski
260260
Brandt Bucher
261261
Curtis Bucher
262262
Colm Buckley
263+
Przemysław Buczkowski
263264
Erik de Bueger
264265
Jan-Hein Bührman
265266
Marc Bürg
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)