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]

Reply via email to