]> git.ipfire.org Git - thirdparty/Python/cpython.git/commitdiff
gh-153502: Add uid parameter to imaplib command methods (GH-153508)
authorSerhiy Storchaka <storchaka@gmail.com>
Fri, 10 Jul 2026 12:08:11 +0000 (15:08 +0300)
committerGitHub <noreply@github.com>
Fri, 10 Jul 2026 12:08:11 +0000 (15:08 +0300)
Add a keyword-only uid=False argument to IMAP4.copy(), move(), fetch(),
store(), search(), sort(), thread() and expunge(), selecting the UID
variant of the command as a shorthand for uid().  expunge() also gains
an optional message_set argument, required for UID EXPUNGE.

Co-authored-by: Claude Opus 4.8 <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-10-18-30-00.gh-issue-153502.uIdKw0.rst [new file with mode: 0644]

index 6872ba0d6cbfcb00fe879f8e32e6990819d1949f..3f26243f77dd5aa7699a2be7c3685846dce84067 100644 (file)
@@ -273,10 +273,16 @@ An :class:`IMAP4` instance has the following methods:
    mailbox. This is the recommended command before ``LOGOUT``.
 
 
-.. method:: IMAP4.copy(message_set, new_mailbox)
+.. method:: IMAP4.copy(message_set, new_mailbox, *, uid=False)
 
    Copy *message_set* messages onto end of *new_mailbox*.
 
+   If *uid* is true, *message_set* is a set of UIDs and the ``UID COPY``
+   command is used instead of ``COPY``.
+
+   .. versionchanged:: next
+      Added the *uid* parameter.
+
 
 .. method:: IMAP4.create(mailbox)
 
@@ -303,19 +309,33 @@ An :class:`IMAP4` instance has the following methods:
       The :meth:`enable` method itself, and :RFC:`6855` support.
 
 
-.. method:: IMAP4.expunge()
+.. method:: IMAP4.expunge(message_set=None, *, uid=False)
 
    Permanently remove deleted items from selected mailbox. Generates an ``EXPUNGE``
    response for each deleted message. Returned data contains a list of ``EXPUNGE``
    message numbers in order received.
 
+   If *uid* is true, the ``UID EXPUNGE`` command (:rfc:`4315`) is used to remove
+   only the messages that both are marked as deleted and have a UID in
+   *message_set*.  *message_set* is required in this case, and must be omitted
+   otherwise.
+
+   .. versionchanged:: next
+      Added the *message_set* and *uid* parameters.
+
 
-.. method:: IMAP4.fetch(message_set, message_parts)
+.. method:: IMAP4.fetch(message_set, message_parts, *, uid=False)
 
    Fetch (parts of) messages.  *message_parts* should be a string of message part
    names enclosed within parentheses, eg: ``"(UID BODY[TEXT])"``.  Returned data
    are tuples of message part envelope and data.
 
+   If *uid* is true, *message_set* is a set of UIDs and the message numbers in
+   the response are UIDs (``UID FETCH``).
+
+   .. versionchanged:: next
+      Added the *uid* parameter.
+
 
 .. method:: IMAP4.getacl(mailbox)
 
@@ -495,12 +515,15 @@ An :class:`IMAP4` instance has the following methods:
    Returned data are tuples of message part envelope and data.
 
 
-.. method:: IMAP4.move(message_set, new_mailbox)
+.. method:: IMAP4.move(message_set, new_mailbox, *, uid=False)
 
    Move *message_set* messages onto end of *new_mailbox*.
 
    The server must support the ``MOVE`` capability (:rfc:`6851`).
 
+   If *uid* is true, *message_set* is a set of UIDs and the ``UID MOVE``
+   command is used instead of ``MOVE``.
+
    .. versionadded:: next
 
 
@@ -575,7 +598,7 @@ An :class:`IMAP4` instance has the following methods:
    code, instead of the usual type.
 
 
-.. method:: IMAP4.search(charset, criterion[, ...])
+.. method:: IMAP4.search(charset, criterion[, ...], *, uid=False)
 
    Search mailbox for matching messages.  *charset* may be ``None``, in which case
    no ``CHARSET`` will be specified in the request to the server.  The IMAP
@@ -584,6 +607,9 @@ An :class:`IMAP4` instance has the following methods:
    the ``UTF8=ACCEPT`` capability was enabled using the :meth:`enable`
    command.
 
+   If *uid* is true, the message numbers in the response are UIDs
+   (``UID SEARCH``).
+
    Example::
 
       # M is a connected IMAP4 instance...
@@ -592,6 +618,9 @@ An :class:`IMAP4` instance has the following methods:
       # or:
       typ, msgnums = M.search(None, '(FROM "LDJ")')
 
+   .. versionchanged:: next
+      Added the *uid* parameter.
+
 
 .. method:: IMAP4.select(mailbox='INBOX', readonly=False)
 
@@ -636,7 +665,7 @@ An :class:`IMAP4` instance has the following methods:
    Returns socket instance used to connect to server.
 
 
-.. method:: IMAP4.sort(sort_criteria, charset, search_criterion[, ...])
+.. method:: IMAP4.sort(sort_criteria, charset, search_criterion[, ...], *, uid=False)
 
    The ``sort`` command is a variant of ``search`` with sorting semantics for the
    results.  Returned data contains a space separated list of matching message
@@ -651,8 +680,13 @@ An :class:`IMAP4` instance has the following methods:
    the interpretation of strings in the searching criteria.  It then returns the
    numbers of matching messages.
 
+   If *uid* is true, the message numbers in the response are UIDs (``UID SORT``).
+
    This is an ``IMAP4rev1`` extension command.
 
+   .. versionchanged:: next
+      Added the *uid* parameter.
+
 
 .. method:: IMAP4.starttls(ssl_context=None)
 
@@ -681,12 +715,15 @@ An :class:`IMAP4` instance has the following methods:
    Request named status conditions for *mailbox*.
 
 
-.. method:: IMAP4.store(message_set, command, flag_list)
+.. method:: IMAP4.store(message_set, command, flag_list, *, uid=False)
 
    Alters flag dispositions for messages in mailbox.  *command* is specified by
    section 6.4.6 of :rfc:`3501` as being one of "FLAGS", "+FLAGS", or "-FLAGS",
    optionally with a suffix of ".SILENT".
 
+   If *uid* is true, *message_set* is a set of UIDs and the ``UID STORE``
+   command is used instead of ``STORE``.
+
    For example, to set the delete flag on all messages::
 
       typ, data = M.search(None, 'ALL')
@@ -706,12 +743,15 @@ An :class:`IMAP4` instance has the following methods:
       Python 3.6, handles them if they are sent from the server, since this
       improves real-world compatibility.
 
+   .. versionchanged:: next
+      Added the *uid* parameter.
+
 .. method:: IMAP4.subscribe(mailbox)
 
    Subscribe to new mailbox.
 
 
-.. method:: IMAP4.thread(threading_algorithm, charset, search_criterion[, ...])
+.. method:: IMAP4.thread(threading_algorithm, charset, search_criterion[, ...], *, uid=False)
 
    The ``thread`` command is a variant of ``search`` with threading semantics for
    the results.  Returned data contains a space separated list of thread members.
@@ -729,8 +769,14 @@ An :class:`IMAP4` instance has the following methods:
    returns the matching messages threaded according to the specified threading
    algorithm.
 
+   If *uid* is true, the message numbers in the response are UIDs
+   (``UID THREAD``).
+
    This is an ``IMAP4rev1`` extension command.
 
+   .. versionchanged:: next
+      Added the *uid* parameter.
+
 
 .. method:: IMAP4.uid(command, arg[, ...])
 
index c36535236471708d76270c54cd50efd84695862a..5e8abc7034bea00c0bd826ebbc2b9f6601f20cae 100644 (file)
@@ -267,6 +267,15 @@ imaplib
   as ordinary :class:`str`.
   (Contributed by Serhiy Storchaka in :gh:`49555`.)
 
+* The :meth:`~imaplib.IMAP4.copy`, :meth:`~imaplib.IMAP4.move`,
+  :meth:`~imaplib.IMAP4.fetch`, :meth:`~imaplib.IMAP4.store`,
+  :meth:`~imaplib.IMAP4.search`, :meth:`~imaplib.IMAP4.sort`,
+  :meth:`~imaplib.IMAP4.thread` and :meth:`~imaplib.IMAP4.expunge` methods
+  now accept a keyword-only *uid* argument that selects the corresponding
+  ``UID`` command, as a more convenient alternative to
+  :meth:`~imaplib.IMAP4.uid`.
+  (Contributed by Serhiy Storchaka in :gh:`153502`.)
+
 
 ipaddress
 ---------
index d8acf085e7a128614a0b77a61c4e5dbc25656392..4e7619b61e89cd30f340870a76044cd94e387dc2 100644 (file)
@@ -590,13 +590,17 @@ class IMAP4:
         return typ, dat
 
 
-    def copy(self, message_set, new_mailbox):
+    def copy(self, message_set, new_mailbox, *, uid=False):
         """Copy 'message_set' messages onto end of 'new_mailbox'.
 
         (typ, [data]) = <instance>.copy(message_set, new_mailbox)
+
+        If 'uid' is true, 'message_set' is a set of UIDs (UID COPY).
         """
-        return self._simple_command('COPY', self._sequence_set(message_set),
-                                    self._mailbox(new_mailbox))
+        args = (self._sequence_set(message_set), self._mailbox(new_mailbox))
+        if uid:
+            return self._simple_command('UID', self._atom('COPY'), *args)
+        return self._simple_command('COPY', *args)
 
 
     def create(self, mailbox):
@@ -634,7 +638,7 @@ class IMAP4:
             self._mode_utf8()
         return typ, data
 
-    def expunge(self):
+    def expunge(self, message_set=None, *, uid=False):
         """Permanently remove deleted items from selected mailbox.
 
         Generates 'EXPUNGE' response for each deleted message.
@@ -642,13 +646,26 @@ class IMAP4:
         (typ, [data]) = <instance>.expunge()
 
         'data' is list of 'EXPUNGE'd message numbers in order received.
+
+        If 'uid' is true, only messages with a UID in 'message_set' are
+        removed (UID EXPUNGE, RFC 4315); 'message_set' is then required and
+        must be omitted otherwise.
         """
         name = 'EXPUNGE'
-        typ, dat = self._simple_command(name)
+        if uid:
+            if message_set is None:
+                raise self.error('UID EXPUNGE requires a message set')
+            typ, dat = self._simple_command('UID', self._atom(name),
+                                            self._sequence_set(message_set))
+        else:
+            if message_set is not None:
+                raise self.error('EXPUNGE takes no message set; '
+                                 'use uid=True for UID EXPUNGE')
+            typ, dat = self._simple_command(name)
         return self._untagged_response(typ, dat, name)
 
 
-    def fetch(self, message_set, message_parts):
+    def fetch(self, message_set, message_parts, *, uid=False):
         """Fetch (parts of) messages.
 
         (typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
@@ -657,10 +674,17 @@ class IMAP4:
         enclosed in parentheses, eg: "(UID BODY[TEXT])".
 
         'data' are tuples of message part envelope and data.
+
+        If 'uid' is true, 'message_set' is a set of UIDs and the message
+        numbers in the response are UIDs (UID FETCH).
         """
         name = 'FETCH'
-        typ, dat = self._simple_command(name, self._sequence_set(message_set),
-                                        self._fetch_parts(message_parts))
+        args = (self._sequence_set(message_set),
+                self._fetch_parts(message_parts))
+        if uid:
+            typ, dat = self._simple_command('UID', self._atom(name), *args)
+        else:
+            typ, dat = self._simple_command(name, *args)
         return self._untagged_response(typ, dat, name)
 
 
@@ -842,13 +866,17 @@ class IMAP4:
                                         self._list_mailbox(pattern))
         return self._untagged_response(typ, dat, name)
 
-    def move(self, message_set, new_mailbox):
+    def move(self, message_set, new_mailbox, *, uid=False):
         """Move 'message_set' messages onto end of 'new_mailbox'.
 
         (typ, [data]) = <instance>.move(message_set, new_mailbox)
+
+        If 'uid' is true, 'message_set' is a set of UIDs (UID MOVE).
         """
-        return self._simple_command('MOVE', self._sequence_set(message_set),
-                                    self._mailbox(new_mailbox))
+        args = (self._sequence_set(message_set), self._mailbox(new_mailbox))
+        if uid:
+            return self._simple_command('UID', self._atom('MOVE'), *args)
+        return self._simple_command('MOVE', *args)
 
     def myrights(self, mailbox):
         """Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
@@ -913,22 +941,27 @@ class IMAP4:
                                     self._mailbox(newmailbox))
 
 
-    def search(self, charset, *criteria):
+    def search(self, charset, *criteria, uid=False):
         """Search mailbox for matching messages.
 
         (typ, [data]) = <instance>.search(charset, criterion, ...)
 
         'data' is space separated list of matching message numbers.
         If UTF8 is enabled, charset MUST be None.
+        If 'uid' is true, the message numbers in the response are UIDs
+        (UID SEARCH).
         """
         name = 'SEARCH'
         if charset is not None:
             if self.utf8_enabled:
                 raise IMAP4.error("Non-None charset not valid in UTF8 mode")
-            typ, dat = self._simple_command(name,
-                    'CHARSET', self._astring(charset), *criteria)
+            args = ('CHARSET', self._astring(charset), *criteria)
         else:
-            typ, dat = self._simple_command(name, *criteria)
+            args = criteria
+        if uid:
+            typ, dat = self._simple_command('UID', self._atom(name), *args)
+        else:
+            typ, dat = self._simple_command(name, *args)
         return self._untagged_response(typ, dat, name)
 
 
@@ -991,10 +1024,13 @@ class IMAP4:
         return self._untagged_response(typ, dat, 'QUOTA')
 
 
-    def sort(self, sort_criteria, charset, *search_criteria):
+    def sort(self, sort_criteria, charset, *search_criteria, uid=False):
         """IMAP4rev1 extension SORT command.
 
         (typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
+
+        If 'uid' is true, the message numbers in the response are UIDs
+        (UID SORT).
         """
         name = 'SORT'
         #if not name in self.capabilities:      # Let the server decide!
@@ -1002,7 +1038,11 @@ class IMAP4:
         sort_criteria = self._set_quote(sort_criteria)
         if charset is not None:
             charset = self._astring(charset)
-        typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
+        args = (sort_criteria, charset, *search_criteria)
+        if uid:
+            typ, dat = self._simple_command('UID', self._atom(name), *args)
+        else:
+            typ, dat = self._simple_command(name, *args)
         return self._untagged_response(typ, dat, name)
 
 
@@ -1043,14 +1083,20 @@ class IMAP4:
         return self._untagged_response(typ, dat, name)
 
 
-    def store(self, message_set, command, flags):
+    def store(self, message_set, command, flags, *, uid=False):
         """Alters flag dispositions for messages in mailbox.
 
         (typ, [data]) = <instance>.store(message_set, command, flags)
+
+        If 'uid' is true, 'message_set' is a set of UIDs (UID STORE).
         """
-        flags = self._set_quote(flags)
-        typ, dat = self._simple_command('STORE', self._sequence_set(message_set),
-                                        command, flags)
+        name = 'STORE'
+        args = (self._sequence_set(message_set), command,
+                self._set_quote(flags))
+        if uid:
+            typ, dat = self._simple_command('UID', self._atom(name), *args)
+        else:
+            typ, dat = self._simple_command(name, *args)
         return self._untagged_response(typ, dat, 'FETCH')
 
 
@@ -1062,16 +1108,22 @@ class IMAP4:
         return self._simple_command('SUBSCRIBE', self._mailbox(mailbox))
 
 
-    def thread(self, threading_algorithm, charset, *search_criteria):
+    def thread(self, threading_algorithm, charset, *search_criteria, uid=False):
         """IMAPrev1 extension THREAD command.
 
         (type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)
+
+        If 'uid' is true, the message numbers in the response are UIDs
+        (UID THREAD).
         """
         name = 'THREAD'
         if charset is not None:
             charset = self._astring(charset)
-        typ, dat = self._simple_command(name, self._atom(threading_algorithm),
-                                        charset, *search_criteria)
+        args = (self._atom(threading_algorithm), charset, *search_criteria)
+        if uid:
+            typ, dat = self._simple_command('UID', self._atom(name), *args)
+        else:
+            typ, dat = self._simple_command(name, *args)
         return self._untagged_response(typ, dat, name)
 
 
index 16a96381992b6849fd77f38512dd9657665bb816..2413a8728ec72be9603a641856700e2e5296705c 100644 (file)
@@ -1201,6 +1201,25 @@ class NewIMAPTestsMixin:
         self.assertEqual(typ, 'OK')
         self.assertEqual(data, [b'3', b'3', b'5', b'8'])
 
+        # A message set is only accepted with uid=True (UID EXPUNGE).
+        with self.assertRaises(imaplib.IMAP4.error):
+            client.expunge('3:8')
+
+    def test_uid_expunge(self):
+        client, server = self._setup(make_simple_handler('UID',
+            ['* 3 EXPUNGE', '* 3 EXPUNGE', '* 5 EXPUNGE', '* 8 EXPUNGE'],
+            'UID EXPUNGE completed'))
+        client.login('user', 'pass')
+        client.select()
+        typ, data = client.expunge('3:8', uid=True)
+        self.assertEqual(typ, 'OK')
+        self.assertEqual(data, [b'3', b'3', b'5', b'8'])
+        self.assertEqual(server.args, ['EXPUNGE', '3:8'])
+
+        # UID EXPUNGE requires a message set.
+        with self.assertRaises(imaplib.IMAP4.error):
+            client.expunge(uid=True)
+
     def test_close(self):
         client, server = self._setup(make_simple_handler('CLOSE'))
         client.login('user', 'pass')
@@ -1308,6 +1327,12 @@ class NewIMAPTestsMixin:
         self.assertEqual(data, [None])
         self.assertEqual(server.args, ['COPY', '4827313:4828442', '"New folder"'])
 
+        # The uid=True keyword is a shorthand for uid('COPY', ...).
+        typ, data = client.copy('4827313:4828442', 'MEETING', uid=True)
+        self.assertEqual(typ, 'OK')
+        self.assertEqual(data, [b'UID COPY completed'])
+        self.assertEqual(server.args, ['COPY', '4827313:4828442', 'MEETING'])
+
     def test_move(self):
         client, server = self._setup(make_simple_handler('MOVE'))
         client.login('user', 'pass')
@@ -1337,6 +1362,12 @@ class NewIMAPTestsMixin:
         self.assertEqual(data, [None])
         self.assertEqual(server.args, ['MOVE', '4827313:4828442', '"New folder"'])
 
+        # The uid=True keyword is a shorthand for uid('MOVE', ...).
+        typ, data = client.move('4827313:4828442', 'MEETING', uid=True)
+        self.assertEqual(typ, 'OK')
+        self.assertEqual(data, [b'UID MOVE completed'])
+        self.assertEqual(server.args, ['MOVE', '4827313:4828442', 'MEETING'])
+
     def test_store(self):
         client, server = self._setup(make_simple_handler('STORE', [
             r'* 2 FETCH (FLAGS (\Deleted \Seen))',
@@ -1379,6 +1410,17 @@ class NewIMAPTestsMixin:
         self.assertEqual(typ, 'OK')
         self.assertEqual(server.args, ['STORE', '4827313:4828442', '+FLAGS', r'(\Deleted)'])
 
+        # The uid=True keyword is a shorthand for uid('STORE', ...).
+        typ, data = client.store('4827313:4828442', '+FLAGS', r'(\Deleted)',
+                                 uid=True)
+        self.assertEqual(typ, 'OK')
+        self.assertEqual(data, [
+            br'23 (FLAGS (\Deleted \Seen) UID 4827313)',
+            br'24 (FLAGS (\Deleted) UID 4827943)',
+            br'25 (FLAGS (\Deleted \Flagged \Seen) UID 4828442)',
+        ])
+        self.assertEqual(server.args, ['STORE', '4827313:4828442', '+FLAGS', r'(\Deleted)'])
+
     def test_fetch(self):
         # The handler expands the requested sequence set and answers for
         # exactly those messages, so the test exercises the round trip of
@@ -1461,6 +1503,16 @@ class NewIMAPTestsMixin:
         self.assertEqual(typ, 'OK')
         self.assertEqual(server.args, ['FETCH', '4827313:4828442', 'ALL'])
 
+        # The uid=True keyword is a shorthand for uid('FETCH', ...).
+        typ, data = client.fetch('4827313:4828442', '(FLAGS)', uid=True)
+        self.assertEqual(typ, 'OK')
+        self.assertEqual(data, [
+            br'23 (FLAGS (\Seen) UID 4827313)',
+            br'24 (FLAGS (\Seen) UID 4827943)',
+            br'25 (FLAGS (\Seen) UID 4828442)',
+        ])
+        self.assertEqual(server.args, ['FETCH', '4827313:4828442', '(FLAGS)'])
+
     def test_partial(self):
         client, server = self._setup(make_simple_handler('PARTIAL',
             ['* 1 FETCH (RFC822.TEXT<0.10> "0123456789")']))
@@ -1526,6 +1578,21 @@ class NewIMAPTestsMixin:
         self.assertEqual(typ, 'OK')
         self.assertEqual(server.args, ['SEARCH', 'CHARSET', '"NF_Z_62-010_(1973)"', 'TEXT', 'XXXXXX'])
 
+        # The uid=True keyword is a shorthand for uid('SEARCH', ...).
+        response[:] = ['* SEARCH 2 84 882']
+        typ, data = client.search(None, 'FLAGGED', 'SINCE', '1-Feb-1994',
+                                  'NOT', 'FROM', '"Smith"', uid=True)
+        self.assertEqual(typ, 'OK')
+        self.assertEqual(data, [b'2 84 882'])
+        self.assertEqual(server.args,
+                ['SEARCH', 'FLAGGED', 'SINCE', '1-Feb-1994', 'NOT', 'FROM', '"Smith"'])
+
+        response[:] = ['* SEARCH 43']
+        typ, data = client.search('UTF-8', 'TEXT', 'XXXXXX', uid=True)
+        self.assertEqual(typ, 'OK')
+        self.assertEqual(data, [b'43'])
+        self.assertEqual(server.args, ['SEARCH', 'CHARSET', 'UTF-8', 'TEXT', 'XXXXXX'])
+
     def test_sort(self):
         response = []
         client, server = self._setup(make_simple_handler('SORT', response))
@@ -1581,6 +1648,13 @@ class NewIMAPTestsMixin:
         self.assertEqual(typ, 'OK')
         self.assertEqual(server.args, ['SORT', '(SUBJECT)', '"NF_Z_62-010_(1973)"', 'TEXT', '"not in mailbox"'])
 
+        # The uid=True keyword is a shorthand for uid('SORT', ...).
+        response[:] = ['* SORT 2 84 882']
+        typ, data = client.sort('(SUBJECT)', 'UTF-8', 'SINCE', '1-Feb-1994', uid=True)
+        self.assertEqual(typ, 'OK')
+        self.assertEqual(data, [br'2 84 882'])
+        self.assertEqual(server.args, ['SORT', '(SUBJECT)', 'UTF-8', 'SINCE', '1-Feb-1994'])
+
     def test_thread(self):
         response = []
         client, server = self._setup(make_simple_handler('THREAD', response))
@@ -1664,6 +1738,14 @@ class NewIMAPTestsMixin:
         self.assertEqual(typ, 'OK')
         self.assertEqual(server.args, ['THREAD', 'ORDEREDSUBJECT', '"NF_Z_62-010_(1973)"', 'TEXT', '"gewp"'])
 
+        # The uid=True keyword is a shorthand for uid('THREAD', ...).
+        response[:] = ['* THREAD (166)(167)(168)']
+        typ, data = client.thread('ORDEREDSUBJECT', 'UTF-8', 'SINCE', '5-MAR-2000',
+                                  uid=True)
+        self.assertEqual(typ, 'OK')
+        self.assertEqual(data, [b'(166)(167)(168)'])
+        self.assertEqual(server.args, ['THREAD', 'ORDEREDSUBJECT', 'UTF-8', 'SINCE', '5-MAR-2000'])
+
     def test_delete(self):
         client, server = self._setup(make_simple_handler('DELETE'))
         client.login('user', 'pass')
diff --git a/Misc/NEWS.d/next/Library/2026-07-10-18-30-00.gh-issue-153502.uIdKw0.rst b/Misc/NEWS.d/next/Library/2026-07-10-18-30-00.gh-issue-153502.uIdKw0.rst
new file mode 100644 (file)
index 0000000..7717c84
--- /dev/null
@@ -0,0 +1,6 @@
+:meth:`imaplib.IMAP4.copy`, :meth:`~imaplib.IMAP4.move`,
+:meth:`~imaplib.IMAP4.fetch`, :meth:`~imaplib.IMAP4.store`,
+:meth:`~imaplib.IMAP4.search`, :meth:`~imaplib.IMAP4.sort`,
+:meth:`~imaplib.IMAP4.thread` and :meth:`~imaplib.IMAP4.expunge` now accept a
+keyword-only *uid* argument that selects the corresponding ``UID`` command, as
+a more convenient alternative to :meth:`~imaplib.IMAP4.uid`.