https://github.com/python/cpython/commit/93b1a886343e4424114541d1c24ae2e01867af95
commit: 93b1a886343e4424114541d1c24ae2e01867af95
branch: main
author: Przemysław Buczkowski <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-05T21:26:44Z
summary:
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.
files:
A Misc/NEWS.d/next/Library/2026-07-05-21-40-00.gh-issue-89869.XG7aHz.rst
M Doc/library/imaplib.rst
M Doc/whatsnew/3.16.rst
M Lib/imaplib.py
M Lib/test/test_imaplib.py
M Misc/ACKS
diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst
index 0684820ccc6916..53934dc698f19e 100644
--- a/Doc/library/imaplib.rst
+++ b/Doc/library/imaplib.rst
@@ -443,13 +443,30 @@ An :class:`IMAP4` instance has the following methods:
.. method:: IMAP4.login_cram_md5(user, password)
Force use of ``CRAM-MD5`` authentication when identifying the client to
protect
- the password. Will only work if the server ``CAPABILITY`` response
includes the
- phrase ``AUTH=CRAM-MD5``.
+ the password. It will only work if the server ``CAPABILITY`` response
includes
+ the phrase ``AUTH=CRAM-MD5``.
.. versionchanged:: 3.15
An :exc:`IMAP4.error` is raised if MD5 support is not available.
+.. method:: IMAP4.login_plain(user, password)
+
+ Authenticate using the ``PLAIN`` SASL mechanism (:rfc:`4616`).
+
+ This is a plaintext authentication mechanism that can be used instead
+ of :meth:`login` when UTF-8 support is required (see :rfc:`6855`).
+ Since the credentials are only base64-encoded, not encrypted, this
+ method should only be used over a TLS-protected connection, such as
+ :class:`IMAP4_SSL` or after :meth:`starttls`.
+
+ It will only work if the server supports the ``PLAIN`` mechanism,
+ which it need not advertise as ``AUTH=PLAIN`` in its ``CAPABILITY``
+ response.
+
+ .. versionadded:: next
+
+
.. method:: IMAP4.logout()
Shutdown connection to server. Returns server ``BYE`` response.
diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst
index e8c530e19d2b53..e8c75d2f571061 100644
--- a/Doc/whatsnew/3.16.rst
+++ b/Doc/whatsnew/3.16.rst
@@ -239,6 +239,11 @@ imaplib
a wrapper for the ``MOVE`` command (:rfc:`6851`).
(Contributed by Serhiy Storchaka in :gh:`77508`.)
+* Add the :meth:`~imaplib.IMAP4.login_plain` method, which authenticates
+ using the ``PLAIN`` SASL mechanism (:rfc:`4616`) and supports non-ASCII
+ user names and passwords.
+ (Contributed by Przemysław Buczkowski and Serhiy Storchaka in :gh:`89869`.)
+
logging
-------
diff --git a/Lib/imaplib.py b/Lib/imaplib.py
index 40d2b7a309b640..b1cf8872314d97 100644
--- a/Lib/imaplib.py
+++ b/Lib/imaplib.py
@@ -762,6 +762,30 @@ def login(self, user, password):
return typ, dat
+ def login_plain(self, user, password):
+ """Authenticate using the PLAIN SASL mechanism (RFC 4616).
+
+ This is a plaintext authentication mechanism that can be used
+ instead of login() when UTF-8 support is required. Since the
+ credentials are only base64-encoded, not encrypted, it should
+ only be used over a TLS-protected connection.
+
+ 'user' and 'password' can be strings (encoded to UTF-8) or
+ bytes-like objects.
+ """
+ if isinstance(user, str):
+ user = user.encode('utf-8')
+ if isinstance(password, str):
+ password = password.encode('utf-8')
+ if b'\0' in user or b'\0' in password:
+ raise ValueError("NUL is not allowed in user name or password")
+ # An empty authorization identity (RFC 4616) makes the server
+ # derive it from the authentication identity; a non-empty one
+ # requests proxy authorization and is often rejected.
+ response = b'\0' + bytes(user) + b'\0' + bytes(password)
+ return self.authenticate("PLAIN", lambda _: response)
+
+
def login_cram_md5(self, user, password):
""" Force use of CRAM-MD5 authentication.
diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py
index aba3f5e44f2566..a2ddbbf9121808 100644
--- a/Lib/test/test_imaplib.py
+++ b/Lib/test/test_imaplib.py
@@ -420,6 +420,15 @@ def cmd_AUTHENTICATE(self, tag, args):
self._send_tagged(tag, 'NO', 'No access')
+class AuthHandler_PLAIN(SimpleIMAPHandler):
+ capabilities = 'LOGINDISABLED AUTH=PLAIN'
+ def cmd_AUTHENTICATE(self, tag, args):
+ self.server.auth_args = args
+ self._send_textline('+')
+ self.server.response = yield
+ self._send_tagged(tag, 'OK', 'Logged in.')
+
+
class NewIMAPTestsMixin:
client = None
@@ -692,6 +701,35 @@ def test_login_cram_md5_blocked(self):
with self.assertRaisesRegex(imaplib.IMAP4.error, msg):
client.login_cram_md5("tim", b"tanstaaftanstaaf")
+ def test_login_plain_ascii(self):
+ client, server = self._setup(AuthHandler_PLAIN)
+ self.assertIn('AUTH=PLAIN', client.capabilities)
+ ret, _ = client.login_plain("prem", "pass")
+ self.assertEqual(ret, "OK")
+ self.assertEqual(server.auth_args, ['PLAIN'])
+ self.assertEqual(server.response, b'AHByZW0AcGFzcw==\r\n')
+
+ def test_login_plain_utf8(self):
+ client, server = self._setup(AuthHandler_PLAIN)
+ self.assertIn('AUTH=PLAIN', client.capabilities)
+ ret, _ = client.login_plain("pręm", "żółć")
+ self.assertEqual(ret, "OK")
+ self.assertEqual(server.response, b'AHByxJltAMW8w7PFgsSH\r\n')
+
+ def test_login_plain_bytes(self):
+ # bytes are accepted and sent verbatim (not re-encoded).
+ client, server = self._setup(AuthHandler_PLAIN)
+ ret, _ = client.login_plain(b'pr\xc4\x99m',
b'\xc5\xbc\xc3\xb3\xc5\x82\xc4\x87')
+ self.assertEqual(ret, "OK")
+ self.assertEqual(server.response, b'AHByxJltAMW8w7PFgsSH\r\n')
+
+ def test_login_plain_nul(self):
+ client, _ = self._setup(AuthHandler_PLAIN)
+ with self.assertRaises(ValueError):
+ client.login_plain('user\0', 'pass')
+ with self.assertRaises(ValueError):
+ client.login_plain('user', b'pa\0ss')
+
def test_aborted_authentication(self):
class MyServer(SimpleIMAPHandler):
def cmd_AUTHENTICATE(self, tag, args):
diff --git a/Misc/ACKS b/Misc/ACKS
index 1f2049da56c2da..68fed1f68de593 100644
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -260,6 +260,7 @@ Stan Bubrouski
Brandt Bucher
Curtis Bucher
Colm Buckley
+Przemysław Buczkowski
Erik de Bueger
Jan-Hein Bührman
Marc Bürg
diff --git
a/Misc/NEWS.d/next/Library/2026-07-05-21-40-00.gh-issue-89869.XG7aHz.rst
b/Misc/NEWS.d/next/Library/2026-07-05-21-40-00.gh-issue-89869.XG7aHz.rst
new file mode 100644
index 00000000000000..4bf319f7f68152
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-07-05-21-40-00.gh-issue-89869.XG7aHz.rst
@@ -0,0 +1,3 @@
+Add :meth:`imaplib.IMAP4.login_plain`, which authenticates using the
+``PLAIN`` SASL mechanism (:rfc:`4616`). Unlike :meth:`~imaplib.IMAP4.login`,
+it supports non-ASCII user names and passwords.
_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]