Skip to content

Commit 2eab1cb

Browse files
gh-153502: Add uid parameter to imaplib command methods
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>
1 parent 9324f84 commit 2eab1cb

5 files changed

Lines changed: 227 additions & 32 deletions

File tree

Doc/library/imaplib.rst

Lines changed: 54 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,16 @@ An :class:`IMAP4` instance has the following methods:
273273
mailbox. This is the recommended command before ``LOGOUT``.
274274

275275

276-
.. method:: IMAP4.copy(message_set, new_mailbox)
276+
.. method:: IMAP4.copy(message_set, new_mailbox, *, uid=False)
277277

278278
Copy *message_set* messages onto end of *new_mailbox*.
279279

280+
If *uid* is true, *message_set* is a set of UIDs and the ``UID COPY``
281+
command is used instead of ``COPY``.
282+
283+
.. versionchanged:: next
284+
Added the *uid* parameter.
285+
280286

281287
.. method:: IMAP4.create(mailbox)
282288

@@ -303,19 +309,33 @@ An :class:`IMAP4` instance has the following methods:
303309
The :meth:`enable` method itself, and :RFC:`6855` support.
304310

305311

306-
.. method:: IMAP4.expunge()
312+
.. method:: IMAP4.expunge(message_set=None, *, uid=False)
307313

308314
Permanently remove deleted items from selected mailbox. Generates an ``EXPUNGE``
309315
response for each deleted message. Returned data contains a list of ``EXPUNGE``
310316
message numbers in order received.
311317

318+
If *uid* is true, the ``UID EXPUNGE`` command (:rfc:`4315`) is used to remove
319+
only the messages that both are marked as deleted and have a UID in
320+
*message_set*. *message_set* is required in this case, and must be omitted
321+
otherwise.
322+
323+
.. versionchanged:: next
324+
Added the *message_set* and *uid* parameters.
325+
312326

313-
.. method:: IMAP4.fetch(message_set, message_parts)
327+
.. method:: IMAP4.fetch(message_set, message_parts, *, uid=False)
314328

315329
Fetch (parts of) messages. *message_parts* should be a string of message part
316330
names enclosed within parentheses, eg: ``"(UID BODY[TEXT])"``. Returned data
317331
are tuples of message part envelope and data.
318332

333+
If *uid* is true, *message_set* is a set of UIDs and the message numbers in
334+
the response are UIDs (``UID FETCH``).
335+
336+
.. versionchanged:: next
337+
Added the *uid* parameter.
338+
319339

320340
.. method:: IMAP4.getacl(mailbox)
321341

@@ -495,12 +515,15 @@ An :class:`IMAP4` instance has the following methods:
495515
Returned data are tuples of message part envelope and data.
496516

497517

498-
.. method:: IMAP4.move(message_set, new_mailbox)
518+
.. method:: IMAP4.move(message_set, new_mailbox, *, uid=False)
499519

500520
Move *message_set* messages onto end of *new_mailbox*.
501521

502522
The server must support the ``MOVE`` capability (:rfc:`6851`).
503523

524+
If *uid* is true, *message_set* is a set of UIDs and the ``UID MOVE``
525+
command is used instead of ``MOVE``.
526+
504527
.. versionadded:: next
505528

506529

@@ -575,7 +598,7 @@ An :class:`IMAP4` instance has the following methods:
575598
code, instead of the usual type.
576599

577600

578-
.. method:: IMAP4.search(charset, criterion[, ...])
601+
.. method:: IMAP4.search(charset, criterion[, ...], *, uid=False)
579602

580603
Search mailbox for matching messages. *charset* may be ``None``, in which case
581604
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:
584607
the ``UTF8=ACCEPT`` capability was enabled using the :meth:`enable`
585608
command.
586609

610+
If *uid* is true, the message numbers in the response are UIDs
611+
(``UID SEARCH``).
612+
587613
Example::
588614

589615
# M is a connected IMAP4 instance...
@@ -592,6 +618,9 @@ An :class:`IMAP4` instance has the following methods:
592618
# or:
593619
typ, msgnums = M.search(None, '(FROM "LDJ")')
594620

621+
.. versionchanged:: next
622+
Added the *uid* parameter.
623+
595624

596625
.. method:: IMAP4.select(mailbox='INBOX', readonly=False)
597626

@@ -636,7 +665,7 @@ An :class:`IMAP4` instance has the following methods:
636665
Returns socket instance used to connect to server.
637666

638667

639-
.. method:: IMAP4.sort(sort_criteria, charset, search_criterion[, ...])
668+
.. method:: IMAP4.sort(sort_criteria, charset, search_criterion[, ...], *, uid=False)
640669

641670
The ``sort`` command is a variant of ``search`` with sorting semantics for the
642671
results. Returned data contains a space separated list of matching message
@@ -651,8 +680,13 @@ An :class:`IMAP4` instance has the following methods:
651680
the interpretation of strings in the searching criteria. It then returns the
652681
numbers of matching messages.
653682

683+
If *uid* is true, the message numbers in the response are UIDs (``UID SORT``).
684+
654685
This is an ``IMAP4rev1`` extension command.
655686

687+
.. versionchanged:: next
688+
Added the *uid* parameter.
689+
656690

657691
.. method:: IMAP4.starttls(ssl_context=None)
658692

@@ -681,12 +715,15 @@ An :class:`IMAP4` instance has the following methods:
681715
Request named status conditions for *mailbox*.
682716

683717

684-
.. method:: IMAP4.store(message_set, command, flag_list)
718+
.. method:: IMAP4.store(message_set, command, flag_list, *, uid=False)
685719

686720
Alters flag dispositions for messages in mailbox. *command* is specified by
687721
section 6.4.6 of :rfc:`3501` as being one of "FLAGS", "+FLAGS", or "-FLAGS",
688722
optionally with a suffix of ".SILENT".
689723

724+
If *uid* is true, *message_set* is a set of UIDs and the ``UID STORE``
725+
command is used instead of ``STORE``.
726+
690727
For example, to set the delete flag on all messages::
691728

692729
typ, data = M.search(None, 'ALL')
@@ -706,12 +743,15 @@ An :class:`IMAP4` instance has the following methods:
706743
Python 3.6, handles them if they are sent from the server, since this
707744
improves real-world compatibility.
708745

746+
.. versionchanged:: next
747+
Added the *uid* parameter.
748+
709749
.. method:: IMAP4.subscribe(mailbox)
710750

711751
Subscribe to new mailbox.
712752

713753

714-
.. method:: IMAP4.thread(threading_algorithm, charset, search_criterion[, ...])
754+
.. method:: IMAP4.thread(threading_algorithm, charset, search_criterion[, ...], *, uid=False)
715755

716756
The ``thread`` command is a variant of ``search`` with threading semantics for
717757
the results. Returned data contains a space separated list of thread members.
@@ -729,8 +769,14 @@ An :class:`IMAP4` instance has the following methods:
729769
returns the matching messages threaded according to the specified threading
730770
algorithm.
731771

772+
If *uid* is true, the message numbers in the response are UIDs
773+
(``UID THREAD``).
774+
732775
This is an ``IMAP4rev1`` extension command.
733776

777+
.. versionchanged:: next
778+
Added the *uid* parameter.
779+
734780

735781
.. method:: IMAP4.uid(command, arg[, ...])
736782

Doc/whatsnew/3.16.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,15 @@ imaplib
267267
as ordinary :class:`str`.
268268
(Contributed by Serhiy Storchaka in :gh:`49555`.)
269269

270+
* The :meth:`~imaplib.IMAP4.copy`, :meth:`~imaplib.IMAP4.move`,
271+
:meth:`~imaplib.IMAP4.fetch`, :meth:`~imaplib.IMAP4.store`,
272+
:meth:`~imaplib.IMAP4.search`, :meth:`~imaplib.IMAP4.sort`,
273+
:meth:`~imaplib.IMAP4.thread` and :meth:`~imaplib.IMAP4.expunge` methods
274+
now accept a keyword-only *uid* argument that selects the corresponding
275+
``UID`` command, as a more convenient alternative to
276+
:meth:`~imaplib.IMAP4.uid`.
277+
(Contributed by Serhiy Storchaka in :gh:`153502`.)
278+
270279

271280
ipaddress
272281
---------

Lib/imaplib.py

Lines changed: 76 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -590,13 +590,17 @@ def close(self):
590590
return typ, dat
591591

592592

593-
def copy(self, message_set, new_mailbox):
593+
def copy(self, message_set, new_mailbox, *, uid=False):
594594
"""Copy 'message_set' messages onto end of 'new_mailbox'.
595595
596596
(typ, [data]) = <instance>.copy(message_set, new_mailbox)
597+
598+
If 'uid' is true, 'message_set' is a set of UIDs (UID COPY).
597599
"""
598-
return self._simple_command('COPY', self._sequence_set(message_set),
599-
self._mailbox(new_mailbox))
600+
args = (self._sequence_set(message_set), self._mailbox(new_mailbox))
601+
if uid:
602+
return self._simple_command('UID', self._atom('COPY'), *args)
603+
return self._simple_command('COPY', *args)
600604

601605

602606
def create(self, mailbox):
@@ -634,21 +638,34 @@ def enable(self, capability):
634638
self._mode_utf8()
635639
return typ, data
636640

637-
def expunge(self):
641+
def expunge(self, message_set=None, *, uid=False):
638642
"""Permanently remove deleted items from selected mailbox.
639643
640644
Generates 'EXPUNGE' response for each deleted message.
641645
642646
(typ, [data]) = <instance>.expunge()
643647
644648
'data' is list of 'EXPUNGE'd message numbers in order received.
649+
650+
If 'uid' is true, only messages with a UID in 'message_set' are
651+
removed (UID EXPUNGE, RFC 4315); 'message_set' is then required and
652+
must be omitted otherwise.
645653
"""
646654
name = 'EXPUNGE'
647-
typ, dat = self._simple_command(name)
655+
if uid:
656+
if message_set is None:
657+
raise self.error('UID EXPUNGE requires a message set')
658+
typ, dat = self._simple_command('UID', self._atom(name),
659+
self._sequence_set(message_set))
660+
else:
661+
if message_set is not None:
662+
raise self.error('EXPUNGE takes no message set; '
663+
'use uid=True for UID EXPUNGE')
664+
typ, dat = self._simple_command(name)
648665
return self._untagged_response(typ, dat, name)
649666

650667

651-
def fetch(self, message_set, message_parts):
668+
def fetch(self, message_set, message_parts, *, uid=False):
652669
"""Fetch (parts of) messages.
653670
654671
(typ, [data, ...]) = <instance>.fetch(message_set, message_parts)
@@ -657,10 +674,17 @@ def fetch(self, message_set, message_parts):
657674
enclosed in parentheses, eg: "(UID BODY[TEXT])".
658675
659676
'data' are tuples of message part envelope and data.
677+
678+
If 'uid' is true, 'message_set' is a set of UIDs and the message
679+
numbers in the response are UIDs (UID FETCH).
660680
"""
661681
name = 'FETCH'
662-
typ, dat = self._simple_command(name, self._sequence_set(message_set),
663-
self._fetch_parts(message_parts))
682+
args = (self._sequence_set(message_set),
683+
self._fetch_parts(message_parts))
684+
if uid:
685+
typ, dat = self._simple_command('UID', self._atom(name), *args)
686+
else:
687+
typ, dat = self._simple_command(name, *args)
664688
return self._untagged_response(typ, dat, name)
665689

666690

@@ -842,13 +866,17 @@ def lsub(self, directory='', pattern='*'):
842866
self._list_mailbox(pattern))
843867
return self._untagged_response(typ, dat, name)
844868

845-
def move(self, message_set, new_mailbox):
869+
def move(self, message_set, new_mailbox, *, uid=False):
846870
"""Move 'message_set' messages onto end of 'new_mailbox'.
847871
848872
(typ, [data]) = <instance>.move(message_set, new_mailbox)
873+
874+
If 'uid' is true, 'message_set' is a set of UIDs (UID MOVE).
849875
"""
850-
return self._simple_command('MOVE', self._sequence_set(message_set),
851-
self._mailbox(new_mailbox))
876+
args = (self._sequence_set(message_set), self._mailbox(new_mailbox))
877+
if uid:
878+
return self._simple_command('UID', self._atom('MOVE'), *args)
879+
return self._simple_command('MOVE', *args)
852880

853881
def myrights(self, mailbox):
854882
"""Show my ACLs for a mailbox (i.e. the rights that I have on mailbox).
@@ -913,22 +941,27 @@ def rename(self, oldmailbox, newmailbox):
913941
self._mailbox(newmailbox))
914942

915943

916-
def search(self, charset, *criteria):
944+
def search(self, charset, *criteria, uid=False):
917945
"""Search mailbox for matching messages.
918946
919947
(typ, [data]) = <instance>.search(charset, criterion, ...)
920948
921949
'data' is space separated list of matching message numbers.
922950
If UTF8 is enabled, charset MUST be None.
951+
If 'uid' is true, the message numbers in the response are UIDs
952+
(UID SEARCH).
923953
"""
924954
name = 'SEARCH'
925955
if charset is not None:
926956
if self.utf8_enabled:
927957
raise IMAP4.error("Non-None charset not valid in UTF8 mode")
928-
typ, dat = self._simple_command(name,
929-
'CHARSET', self._astring(charset), *criteria)
958+
args = ('CHARSET', self._astring(charset), *criteria)
930959
else:
931-
typ, dat = self._simple_command(name, *criteria)
960+
args = criteria
961+
if uid:
962+
typ, dat = self._simple_command('UID', self._atom(name), *args)
963+
else:
964+
typ, dat = self._simple_command(name, *args)
932965
return self._untagged_response(typ, dat, name)
933966

934967

@@ -991,18 +1024,25 @@ def setquota(self, root, limits):
9911024
return self._untagged_response(typ, dat, 'QUOTA')
9921025

9931026

994-
def sort(self, sort_criteria, charset, *search_criteria):
1027+
def sort(self, sort_criteria, charset, *search_criteria, uid=False):
9951028
"""IMAP4rev1 extension SORT command.
9961029
9971030
(typ, [data]) = <instance>.sort(sort_criteria, charset, search_criteria, ...)
1031+
1032+
If 'uid' is true, the message numbers in the response are UIDs
1033+
(UID SORT).
9981034
"""
9991035
name = 'SORT'
10001036
#if not name in self.capabilities: # Let the server decide!
10011037
# raise self.error('unimplemented extension command: %s' % name)
10021038
sort_criteria = self._set_quote(sort_criteria)
10031039
if charset is not None:
10041040
charset = self._astring(charset)
1005-
typ, dat = self._simple_command(name, sort_criteria, charset, *search_criteria)
1041+
args = (sort_criteria, charset, *search_criteria)
1042+
if uid:
1043+
typ, dat = self._simple_command('UID', self._atom(name), *args)
1044+
else:
1045+
typ, dat = self._simple_command(name, *args)
10061046
return self._untagged_response(typ, dat, name)
10071047

10081048

@@ -1043,14 +1083,20 @@ def status(self, mailbox, names):
10431083
return self._untagged_response(typ, dat, name)
10441084

10451085

1046-
def store(self, message_set, command, flags):
1086+
def store(self, message_set, command, flags, *, uid=False):
10471087
"""Alters flag dispositions for messages in mailbox.
10481088
10491089
(typ, [data]) = <instance>.store(message_set, command, flags)
1090+
1091+
If 'uid' is true, 'message_set' is a set of UIDs (UID STORE).
10501092
"""
1051-
flags = self._set_quote(flags)
1052-
typ, dat = self._simple_command('STORE', self._sequence_set(message_set),
1053-
command, flags)
1093+
name = 'STORE'
1094+
args = (self._sequence_set(message_set), command,
1095+
self._set_quote(flags))
1096+
if uid:
1097+
typ, dat = self._simple_command('UID', self._atom(name), *args)
1098+
else:
1099+
typ, dat = self._simple_command(name, *args)
10541100
return self._untagged_response(typ, dat, 'FETCH')
10551101

10561102

@@ -1062,16 +1108,22 @@ def subscribe(self, mailbox):
10621108
return self._simple_command('SUBSCRIBE', self._mailbox(mailbox))
10631109

10641110

1065-
def thread(self, threading_algorithm, charset, *search_criteria):
1111+
def thread(self, threading_algorithm, charset, *search_criteria, uid=False):
10661112
"""IMAPrev1 extension THREAD command.
10671113
10681114
(type, [data]) = <instance>.thread(threading_algorithm, charset, search_criteria, ...)
1115+
1116+
If 'uid' is true, the message numbers in the response are UIDs
1117+
(UID THREAD).
10691118
"""
10701119
name = 'THREAD'
10711120
if charset is not None:
10721121
charset = self._astring(charset)
1073-
typ, dat = self._simple_command(name, self._atom(threading_algorithm),
1074-
charset, *search_criteria)
1122+
args = (self._atom(threading_algorithm), charset, *search_criteria)
1123+
if uid:
1124+
typ, dat = self._simple_command('UID', self._atom(name), *args)
1125+
else:
1126+
typ, dat = self._simple_command(name, *args)
10751127
return self._untagged_response(typ, dat, name)
10761128

10771129

0 commit comments

Comments
 (0)