]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
bpo-45706: Add imaplib.IMAP4.login_plain (GH-29398)
authorPrzemysław Buczkowski <prem@prem.moe>
Sun, 5 Jul 2026 21:26:44 +0000 (06:26 +0900)
committerGitHub <noreply@github.com>
Sun, 5 Jul 2026 21:26:44 +0000 (21:26 +0000)
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.

Doc/library/imaplib.rst
Doc/whatsnew/3.16.rst
Lib/imaplib.py
Lib/test/test_imaplib.py
Misc/ACKS
Misc/NEWS.d/next/Library/2026-07-05-21-40-00.gh-issue-89869.XG7aHz.rst [new file with mode: 0644]

index 0684820ccc6916c6cc5d4c623ba676d9c3f83059..53934dc698f19e0c0e8ff0cf926ea3769c4ae3d0 100644 (file)
@@ -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.
index e8c530e19d2b53be7af871ef7bce4f32bdf9b3b1..e8c75d2f571061aa51278356674fa8a407337f9a 100644 (file)
@@ -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
 -------
index 40d2b7a309b640b8d7c8c768c373477ca3587102..b1cf8872314d9776d973f65852638ba43e3574d2 100644 (file)
@@ -762,6 +762,30 @@ class IMAP4:
         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.
 
index aba3f5e44f2566806be9c79f1e8ff3ac99b76945..a2ddbbf91218083d329b985b863d45d17aab8c09 100644 (file)
@@ -420,6 +420,15 @@ class AuthHandler_CRAM_MD5(SimpleIMAPHandler):
             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 @@ class NewIMAPTestsMixin:
         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):
index 1f2049da56c2da7f2412b8ad0516a91a7265b832..68fed1f68de5933ea19c31634ab15b8f4c7d4c9d 100644 (file)
--- 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 (file)
index 0000000..4bf319f
--- /dev/null
@@ -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.