From 01330bd5e3acbdeb1307ee5324f3ed1b2045587d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Buczkowski?= Date: Thu, 4 Nov 2021 00:10:11 +0000 Subject: [PATCH 1/4] bpo-45706: Add imaplib.IMAP4.login_plain Adds authentication using PLAIN SASL mechanism. This is a plain-text authentication mechanism which can be used instead of :meth:`IMAP4.login()` when UTF-8 support is required. --- Doc/library/imaplib.rst | 18 +++++++++-- Lib/imaplib.py | 12 ++++++++ Lib/test/test_imaplib.py | 30 +++++++++++++++++++ Misc/ACKS | 1 + .../2021-11-04-00-09-09.bpo-45706.XG7aHz.rst | 1 + 5 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-45706.XG7aHz.rst diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index 65681ec093598c..b33621b086d283 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -330,8 +330,22 @@ 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``. + + +.. method:: IMAP4.login_plain(login, password) + + Authenticate using PLAIN SASL mechanism. + + This is a plain-text authentication mechanism that can be used + instead of :meth:`IMAP4.login()` when UTF-8 support is required. + See :RFC:`6855`, page 5. + + It will only work if the server ``CAPABILITY`` response includes + the phrase ``AUTH=PLAIN``. + + .. versionadded:: 3.11 .. method:: IMAP4.logout() diff --git a/Lib/imaplib.py b/Lib/imaplib.py index fa4c0f8f62361a..e9d90f831dd3b7 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -614,6 +614,18 @@ def login(self, user, password): return typ, dat + def login_plain(self, user, password): + """Authenticate using PLAIN SASL mechanism. + + This is a plain-text authentication mechanism that can be used + instead of login() when UTF-8 support is required. + """ + return self.authenticate( + "PLAIN", + lambda _: "{0}\x00{0}\x00{1}".format(user, password).encode() + ) + + 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 c2b935f58164e5..da4b74c8e33cf8 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -423,6 +423,36 @@ def cmd_AUTHENTICATE(self, tag, args): ret, _ = client.login_cram_md5("tim", "tanstaaftanstaaf") self.assertEqual(ret, "OK") + def test_login_plain_ascii(self): + class AuthHandler(SimpleIMAPHandler): + capabilities = 'LOGINDISABLED AUTH=PLAIN' + def cmd_AUTHENTICATE(self, tag, args): + self._send_textline('+') + r = yield + if r == b'cHJlbQBwcmVtAHBhc3M=\r\n': + self._send_tagged(tag, 'OK', 'Logged in.') + else: + self._send_tagged(tag, 'NO', 'No access') + client, _ = self._setup(AuthHandler) + self.assertTrue('AUTH=PLAIN' in client.capabilities) + ret, _ = client.login_plain("prem", "pass") + self.assertEqual(ret, "OK") + + def test_login_plain_utf8(self): + class AuthHandler(SimpleIMAPHandler): + capabilities = 'LOGINDISABLED AUTH=PLAIN' + def cmd_AUTHENTICATE(self, tag, args): + self._send_textline('+') + r = yield + if r == b'cHLEmW0AcHLEmW0AxbzDs8WCxIc=\r\n': + self._send_tagged(tag, 'OK', 'Logged in.') + else: + self._send_tagged(tag, 'NO', 'No access') + client, _ = self._setup(AuthHandler) + self.assertTrue('AUTH=PLAIN' in client.capabilities) + ret, _ = client.login_plain("pręm", "żółć") + self.assertEqual(ret, "OK") + def test_aborted_authentication(self): class MyServer(SimpleIMAPHandler): def cmd_AUTHENTICATE(self, tag, args): diff --git a/Misc/ACKS b/Misc/ACKS index 204293fa50d9c0..34c969fe6824be 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -248,6 +248,7 @@ Stan Bubrouski Brandt Bucher Curtis Bucher Colm Buckley +Przemysław Buczkowski Erik de Bueger Jan-Hein Bührman Lars Buitinck diff --git a/Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-45706.XG7aHz.rst b/Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-45706.XG7aHz.rst new file mode 100644 index 00000000000000..7b2e7bffe531a7 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-45706.XG7aHz.rst @@ -0,0 +1 @@ +Add :meth:`imaplib.IMAP4.login_plain`. From 7de847c9a97b04434d72c5538d5b97d8d63f26fe Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 5 Jul 2026 20:32:36 +0300 Subject: [PATCH 2/4] Fix misplaced versionchanged note after the merge The 'MD5 support is not available' note belongs to login_cram_md5, but merging placed it after login_plain, which was inserted between them. Co-Authored-By: Claude Fable 5 --- Doc/library/imaplib.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index 35974012f3b007..d52be5446b19d4 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -433,6 +433,9 @@ An :class:`IMAP4` instance has the following methods: 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(login, password) @@ -447,9 +450,6 @@ An :class:`IMAP4` instance has the following methods: .. versionadded:: 3.11 - .. versionchanged:: 3.15 - An :exc:`IMAP4.error` is raised if MD5 support is not available. - .. method:: IMAP4.logout() From 7a2843bdbcb651d7f8235a8d0eb0245090c9b026 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 5 Jul 2026 20:35:07 +0300 Subject: [PATCH 3/4] 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 --- Doc/library/imaplib.rst | 18 ++++--- Lib/imaplib.py | 26 +++++++--- Lib/test/test_imaplib.py | 52 +++++++++++-------- .../2021-11-04-00-09-09.bpo-45706.XG7aHz.rst | 1 - ...6-07-05-21-40-00.gh-issue-89869.XG7aHz.rst | 3 ++ 5 files changed, 62 insertions(+), 38 deletions(-) delete mode 100644 Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-45706.XG7aHz.rst create mode 100644 Misc/NEWS.d/next/Library/2026-07-05-21-40-00.gh-issue-89869.XG7aHz.rst diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index d52be5446b19d4..05e8e352763af8 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -437,18 +437,20 @@ An :class:`IMAP4` instance has the following methods: An :exc:`IMAP4.error` is raised if MD5 support is not available. -.. method:: IMAP4.login_plain(login, password) +.. method:: IMAP4.login_plain(user, password) - Authenticate using PLAIN SASL mechanism. + Authenticate using the ``PLAIN`` SASL mechanism (:rfc:`4616`). - This is a plain-text authentication mechanism that can be used - instead of :meth:`IMAP4.login()` when UTF-8 support is required. - See :RFC:`6855`, page 5. + 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 ``CAPABILITY`` response includes - the phrase ``AUTH=PLAIN``. + It will only work if the server ``CAPABILITY`` response includes + the phrase ``AUTH=PLAIN``. - .. versionadded:: 3.11 + .. versionadded:: next .. method:: IMAP4.logout() diff --git a/Lib/imaplib.py b/Lib/imaplib.py index 59e1bd6436fc24..a8189f02947d74 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -740,15 +740,27 @@ def login(self, user, password): def login_plain(self, user, password): - """Authenticate using PLAIN SASL mechanism. + """Authenticate using the PLAIN SASL mechanism (RFC 4616). - This is a plain-text authentication mechanism that can be used - instead of login() when UTF-8 support is required. + 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. """ - return self.authenticate( - "PLAIN", - lambda _: "{0}\x00{0}\x00{1}".format(user, password).encode() - ) + 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): diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index 3f1128d89dcb66..b0f14fb848c26c 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 @@ -693,34 +702,33 @@ def test_login_cram_md5_blocked(self): client.login_cram_md5("tim", b"tanstaaftanstaaf") def test_login_plain_ascii(self): - class AuthHandler(SimpleIMAPHandler): - capabilities = 'LOGINDISABLED AUTH=PLAIN' - def cmd_AUTHENTICATE(self, tag, args): - self._send_textline('+') - r = yield - if r == b'cHJlbQBwcmVtAHBhc3M=\r\n': - self._send_tagged(tag, 'OK', 'Logged in.') - else: - self._send_tagged(tag, 'NO', 'No access') - client, _ = self._setup(AuthHandler) - self.assertTrue('AUTH=PLAIN' in client.capabilities) + 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): - class AuthHandler(SimpleIMAPHandler): - capabilities = 'LOGINDISABLED AUTH=PLAIN' - def cmd_AUTHENTICATE(self, tag, args): - self._send_textline('+') - r = yield - if r == b'cHLEmW0AcHLEmW0AxbzDs8WCxIc=\r\n': - self._send_tagged(tag, 'OK', 'Logged in.') - else: - self._send_tagged(tag, 'NO', 'No access') - client, _ = self._setup(AuthHandler) - self.assertTrue('AUTH=PLAIN' in client.capabilities) + 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): diff --git a/Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-45706.XG7aHz.rst b/Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-45706.XG7aHz.rst deleted file mode 100644 index 7b2e7bffe531a7..00000000000000 --- a/Misc/NEWS.d/next/Library/2021-11-04-00-09-09.bpo-45706.XG7aHz.rst +++ /dev/null @@ -1 +0,0 @@ -Add :meth:`imaplib.IMAP4.login_plain`. 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. From c0c2c29c238385808d60146176d7884d713b037a Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 5 Jul 2026 23:04:13 +0300 Subject: [PATCH 4/4] Soften the AUTH=PLAIN capability note and add a What's New entry RFC 3501 makes the PLAIN mechanism mandatory, so a server may support it without advertising AUTH=PLAIN in its CAPABILITY response. Co-Authored-By: Claude Fable 5 --- Doc/library/imaplib.rst | 5 +++-- Doc/whatsnew/3.16.rst | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index 05e8e352763af8..3f95be31f4eed9 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -447,8 +447,9 @@ An :class:`IMAP4` instance has the following methods: 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 ``CAPABILITY`` response includes - the phrase ``AUTH=PLAIN``. + 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 diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index cf105a26d98d45..89ea6ff43e8cbd 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -235,6 +235,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 -------