From 9ef151e72668ef72c6d423c09252a56f72e87149 Mon Sep 17 00:00:00 2001 From: Serhiy Storchaka Date: Sun, 5 Jul 2026 18:40:44 +0300 Subject: [PATCH] gh-98092: Add imaplib.IMAP4.id method 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 | 8 +++++++ 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, 64 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 c58c957d03c1d84..450c22a0b3a3054 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 8219700cc1e8385..036d6c38c542269 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -228,6 +228,14 @@ io (Contributed by Marcel Martin in :gh:`90533`.) +imaplib +------- + +* Add the :meth:`~imaplib.IMAP4.id` method, + a wrapper for the ``ID`` command (:rfc:`2971`). + (Contributed by Serhiy Storchaka in :gh:`98092`.) + + logging ------- diff --git a/Lib/imaplib.py b/Lib/imaplib.py index a871e509b0d82c6..144585ba1c6a53e 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'), @@ -695,6 +696,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 cdc2f9c8d852322..3dae0339f2e2666 100644 --- a/Lib/test/test_imaplib.py +++ b/Lib/test/test_imaplib.py @@ -1672,6 +1672,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`).