From 28a8c1819a8d94620801c6c79c30a3a89a00fb89 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 5 Jul 2026 20:32:32 +0300 Subject: [PATCH 1/3] gh-98092: Add imaplib.IMAP4.id method (GH-153136) Add a wrapper for the IMAP ID command (RFC 2971). It takes a mapping of field names to values and returns the server identification information from the untagged ID response. Co-authored-by: Claude Fable 5 --- Doc/library/imaplib.rst | 13 +++++++++++ Doc/whatsnew/3.16.rst | 4 ++++ Lib/imaplib.py | 23 +++++++++++++++++++ Lib/test/test_imaplib.py | 18 +++++++++++++++ ...6-07-05-20-15-00.gh-issue-98092.iDcmd1.rst | 2 ++ 5 files changed, 60 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-05-20-15-00.gh-issue-98092.iDcmd1.rst diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst index 6b7c02f54e90af2..0684820ccc6916c 100644 --- a/Doc/library/imaplib.rst +++ b/Doc/library/imaplib.rst @@ -328,6 +328,19 @@ An :class:`IMAP4` instance has the following methods: of the IMAP4 QUOTA extension defined in rfc2087. +.. method:: IMAP4.id(fields=None) + + Send client identification information to the server + and return the identification information sent back by the server + (the ``ID`` command, defined in :rfc:`2971`). + *fields* is a mapping of field names to values + (for example, ``{'name': 'myclient', 'version': '1.0'}``); + a value can be ``None``. + The server must support the ``ID`` capability. + + .. versionadded:: next + + .. method:: IMAP4.idle(duration=None) Return an :class:`!Idler`: an iterable context manager implementing the diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index cf105a26d98d45d..e8c530e19d2b53b 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -231,6 +231,10 @@ io imaplib ------- +* Add the :meth:`~imaplib.IMAP4.id` method, + a wrapper for the ``ID`` command (:rfc:`2971`). + (Contributed by Serhiy Storchaka in :gh:`98092`.) + * Add the :meth:`~imaplib.IMAP4.move` method, a wrapper for the ``MOVE`` command (:rfc:`6851`). (Contributed by Serhiy Storchaka in :gh:`77508`.) diff --git a/Lib/imaplib.py b/Lib/imaplib.py index adfd8afb9c053bb..40d2b7a309b640b 100644 --- a/Lib/imaplib.py +++ b/Lib/imaplib.py @@ -73,6 +73,7 @@ 'GETANNOTATION':('AUTH', 'SELECTED'), 'GETQUOTA': ('AUTH', 'SELECTED'), 'GETQUOTAROOT': ('AUTH', 'SELECTED'), + 'ID': ('NONAUTH', 'AUTH', 'SELECTED', 'LOGOUT'), 'IDLE': ('AUTH', 'SELECTED'), 'MYRIGHTS': ('AUTH', 'SELECTED'), 'LIST': ('AUTH', 'SELECTED'), @@ -697,6 +698,28 @@ def getquotaroot(self, mailbox): return typ, [quotaroot, quota] + def id(self, fields=None): + """Send client identification information to the server. + + (typ, [data]) = .id(fields) + + 'fields' is a mapping of field names to values; a value can be + None. 'data' is the identification information sent back by + the server, in the same parenthesized list form. + """ + name = 'ID' + if fields: + items = [] + for field, value in fields.items(): + items.append(self._quote(field)) + items.append(b'NIL' if value is None else self._quote(value)) + arg = b'(' + b' '.join(items) + b')' + else: + arg = 'NIL' + typ, dat = self._simple_command(name, arg) + return self._untagged_response(typ, dat, name) + + def idle(self, duration=None): """Return an iterable IDLE context manager producing untagged responses. If the argument is not None, limit iteration to 'duration' seconds. diff --git a/Lib/test/test_imaplib.py b/Lib/test/test_imaplib.py index 046d28f4d30c8a4..aba3f5e44f25668 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -1701,6 +1701,24 @@ def test_getquotaroot(self): self.assertEqual(typ, 'OK') self.assertEqual(server.args, ['"New folder"']) + def test_id(self): + client, server = self._setup(make_simple_handler('ID', + ['* ID ("name" "Cyrus" "version" "1.5")'])) + typ, data = client.id({'name': 'imaplib', 'version': '3.16'}) + self.assertEqual(typ, 'OK') + self.assertEqual(data, [b'("name" "Cyrus" "version" "1.5")']) + self.assertEqual(server.args, ['("name" "imaplib" "version" "3.16")']) + + typ, data = client.id() + self.assertEqual(typ, 'OK') + self.assertEqual(server.args, ['NIL']) + + # Fields and values are quoted strings; a None value is sent + # as NIL. + typ, data = client.id({'name': 'my "client"', 'os': None}) + self.assertEqual(typ, 'OK') + self.assertEqual(server.args, [r'("name" "my \"client\"" "os" NIL)']) + def test_setquota(self): client, server = self._setup(make_simple_handler('SETQUOTA', ['* QUOTA "" (STORAGE 512)'])) diff --git a/Misc/NEWS.d/next/Library/2026-07-05-20-15-00.gh-issue-98092.iDcmd1.rst b/Misc/NEWS.d/next/Library/2026-07-05-20-15-00.gh-issue-98092.iDcmd1.rst new file mode 100644 index 000000000000000..9854e996a942a53 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-05-20-15-00.gh-issue-98092.iDcmd1.rst @@ -0,0 +1,2 @@ +Add :meth:`imaplib.IMAP4.id`, a wrapper for the IMAP ``ID`` command +(:rfc:`2971`). From d733b104d53e96584a6881d2772df65ad82573a0 Mon Sep 17 00:00:00 2001 From: Vineet Kumar <108144301+whyvineet@users.noreply.github.com> Date: Mon, 6 Jul 2026 00:32:58 +0530 Subject: [PATCH 2/3] gh-153141: Fix mutable default argument in _SharedMemoryTracker.__init__ (GH-153142) Fix default argument for segment_names in _SharedMemoryTracker constructor to not use a mutable list. --- Lib/multiprocessing/managers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/multiprocessing/managers.py b/Lib/multiprocessing/managers.py index 91bcf243e78e5b8..11c6e21de90e693 100644 --- a/Lib/multiprocessing/managers.py +++ b/Lib/multiprocessing/managers.py @@ -1294,9 +1294,9 @@ class SyncManager(BaseManager): class _SharedMemoryTracker: "Manages one or more shared memory segments." - def __init__(self, name, segment_names=[]): + def __init__(self, name, segment_names=None): self.shared_memory_context_name = name - self.segment_names = segment_names + self.segment_names = [] if segment_names is None else segment_names def register_segment(self, segment_name): "Adds the supplied shared memory block name to tracker." From 93b1a886343e4424114541d1c24ae2e01867af95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Przemys=C5=82aw=20Buczkowski?= Date: Mon, 6 Jul 2026 06:26:44 +0900 Subject: [PATCH 3/3] 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. --- Doc/library/imaplib.rst | 21 +++++++++- Doc/whatsnew/3.16.rst | 5 +++ Lib/imaplib.py | 24 ++++++++++++ Lib/test/test_imaplib.py | 38 +++++++++++++++++++ Misc/ACKS | 1 + ...6-07-05-21-40-00.gh-issue-89869.XG7aHz.rst | 3 ++ 6 files changed, 90 insertions(+), 2 deletions(-) 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 0684820ccc6916c..53934dc698f19e0 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 e8c530e19d2b53b..e8c75d2f571061a 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 40d2b7a309b640b..b1cf8872314d977 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 aba3f5e44f25668..a2ddbbf91218083 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 1f2049da56c2da7..68fed1f68de5933 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 000000000000000..4bf319f7f681525 --- /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.