https://github.com/python/cpython/commit/28a8c1819a8d94620801c6c79c30a3a89a00fb89
commit: 28a8c1819a8d94620801c6c79c30a3a89a00fb89
branch: main
author: Serhiy Storchaka <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-07-05T17:32:32Z
summary:

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 <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-07-05-20-15-00.gh-issue-98092.iDcmd1.rst
M Doc/library/imaplib.rst
M Doc/whatsnew/3.16.rst
M Lib/imaplib.py
M Lib/test/test_imaplib.py

diff --git a/Doc/library/imaplib.rst b/Doc/library/imaplib.rst
index 6b7c02f54e90af..0684820ccc6916 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 cf105a26d98d45..e8c530e19d2b53 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 adfd8afb9c053b..40d2b7a309b640 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]) = <instance>.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 046d28f4d30c8a..aba3f5e44f2566 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 00000000000000..9854e996a942a5
--- /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`).

_______________________________________________
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