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)
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)
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
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
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...
# or:
typ, msgnums = M.search(None, '(FROM "LDJ")')
+ .. versionchanged:: next
+ Added the *uid* parameter.
+
.. method:: IMAP4.select(mailbox='INBOX', readonly=False)
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
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)
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')
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.
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[, ...])
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
---------
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):
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.
(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)
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)
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).
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)
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!
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)
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')
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)
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')
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')
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))',
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
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")']))
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))
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))
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')
--- /dev/null
+: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`.