]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-98092: Add imaplib.IMAP4.id method (GH-153136)
authorSerhiy Storchaka <storchaka@gmail.com>
Sun, 5 Jul 2026 17:32:32 +0000 (20:32 +0300)
committerGitHub <noreply@github.com>
Sun, 5 Jul 2026 17:32:32 +0000 (17:32 +0000)
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 <noreply@anthropic.com>
Doc/library/imaplib.rst
Doc/whatsnew/3.16.rst
Lib/imaplib.py
Lib/test/test_imaplib.py
Misc/NEWS.d/next/Library/2026-07-05-20-15-00.gh-issue-98092.iDcmd1.rst [new file with mode: 0644]

index 6b7c02f54e90af2eb09a84ab2d59704d8dfa75fd..0684820ccc6916c6cc5d4c623ba676d9c3f83059 100644 (file)
@@ -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
index cf105a26d98d45df31e5bc7ec9100be53b14b6e2..e8c530e19d2b53be7af871ef7bce4f32bdf9b3b1 100644 (file)
@@ -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`.)
index adfd8afb9c053bb47cab705fc4d0e997db8e53c6..40d2b7a309b640b8d7c8c768c373477ca3587102 100644 (file)
@@ -73,6 +73,7 @@ Commands = {
         '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 @@ class IMAP4:
         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.
index 046d28f4d30c8a47c3ad890b0c7368cb8a64988f..aba3f5e44f2566806be9c79f1e8ff3ac99b76945 100644 (file)
@@ -1701,6 +1701,24 @@ class NewIMAPTestsMixin:
         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 (file)
index 0000000..9854e99
--- /dev/null
@@ -0,0 +1,2 @@
+Add :meth:`imaplib.IMAP4.id`, a wrapper for the IMAP ``ID`` command
+(:rfc:`2971`).