Skip to content

Commit 456f3a7

Browse files
serhiy-storchakaclaudebitdancer
authored
gh-153521: Support structured arguments in imaplib commands (GH-153522)
Command methods now accept a structured *message_set* (an integer, or a sequence of integers, (start, stop) ranges and range objects) and lists of flags or other atoms in place of preformatted parenthesized strings. The search, fetch, sort, thread and uid methods gain a keyword-only *params* argument that substitutes and quotes '?' placeholders in their value-bearing arguments, in the manner of sqlite3 parameter substitution. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: R. David Murray <rdmurray@bitdance.com>
1 parent 190d2ff commit 456f3a7

5 files changed

Lines changed: 451 additions & 37 deletions

File tree

Doc/library/imaplib.rst

Lines changed: 101 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,10 @@ enclosed with either parentheses or double quotes) each string is quoted.
188188
However, the *password* argument to the ``LOGIN`` command is always quoted. If
189189
you want to avoid having an argument string quoted (eg: the *flags* argument to
190190
``STORE``) then enclose the string in parentheses (eg: ``r'(\Deleted)'``).
191-
In general, pass arguments unquoted and let the module quote them as needed.
192-
An argument that is already enclosed in double quotes is left unchanged,
193-
so that code which quotes arguments itself keeps working.
191+
Or you can quote the string yourself;
192+
an argument that is already enclosed in double quotes is left unchanged.
193+
In general, however, it is better to pass arguments unquoted
194+
and let the module quote them as needed.
194195

195196
Mailbox names are encoded as modified UTF-7 (:rfc:`3501`, section 5.1.3),
196197
so a mailbox name containing non-ASCII characters can be passed as an
@@ -211,11 +212,70 @@ or mandated results from the command. Each *data* is either a ``bytes``, or a
211212
tuple. If a tuple, then the first part is the header of the response, and the
212213
second part contains the data (ie: 'literal' value).
213214

214-
The *message_set* options to commands below is a string specifying one or more
215-
messages to be acted upon. It may be a simple message number (``'1'``), a range
216-
of message numbers (``'2:4'``), or a group of non-contiguous ranges separated by
217-
commas (``'1:3,6:9'``). A range can contain an asterisk to indicate an infinite
218-
upper bound (``'3:*'``).
215+
The *message_set* options to the commands below can be a string specifying
216+
one or more messages to be acted upon.
217+
It may be a simple message number (``'1'``),
218+
a range of message numbers (``'2:4'``),
219+
or a group of non-contiguous ranges separated by commas (``'1:3,6:9'``).
220+
A range can contain an asterisk
221+
to indicate an infinite upper bound (``'3:*'``).
222+
223+
Alternatively it can be specified using integers and :class:`range` objects.
224+
It may be a single message number or a sequence.
225+
The sequence items may be integers, ``(start, stop)`` tuples
226+
(where ``None`` or ``'*'`` stands for the last message),
227+
or :class:`range` objects.
228+
For example, ``[1, (3, 5), 8]`` and ``[range(1, 6), 8]``
229+
are both equivalent to ``'1,3:5,8'``.
230+
231+
.. versionchanged:: next
232+
Added support for the structured *message_set*.
233+
234+
Command arguments that are parenthesized lists of atoms ---
235+
such as the *flag_list* argument of :meth:`~IMAP4.store` and the *flags*
236+
argument of :meth:`~IMAP4.append`,
237+
the *names* argument of :meth:`~IMAP4.status`,
238+
the *sort_criteria* argument of :meth:`~IMAP4.sort`,
239+
or the *message_parts* argument of :meth:`~IMAP4.fetch` ---
240+
can be passed as a sequence of strings instead of a single preformatted string.
241+
For example, ``[r'\Seen', r'\Answered']``
242+
is equivalent to ``(\Seen \Answered)``.
243+
244+
.. versionchanged:: next
245+
Added support for passing these arguments as a sequence.
246+
247+
.. _imap4-params:
248+
249+
The value-bearing arguments of the search and fetch commands
250+
can be quoted by hand, but this is error prone.
251+
Instead, they may contain ``?`` placeholders that are substituted, and quoted
252+
as required, from a *params* keyword argument,
253+
in the manner of :mod:`sqlite3` parameter substitution::
254+
255+
# SEARCH FROM me@example.com SUBJECT "trip report"
256+
M.search(None, 'FROM ? SUBJECT ?', params=['me@example.com', 'trip report'])
257+
258+
# FETCH 1:5 (FLAGS BODY[HEADER.FIELDS (DATE FROM)])
259+
M.fetch('1:5', 'FLAGS BODY[HEADER.FIELDS ?]', params=[['DATE', 'FROM']])
260+
261+
The placeholders are:
262+
263+
* ``?`` --- an ``astring``: a string (which will be quoted if necessary),
264+
an integer, or a list of integers and/or strings
265+
(which will be sent as a parenthesized list);
266+
* ``?f`` --- a flag or a list of flags, sent verbatim without quoting;
267+
* ``?s`` --- a *message_set* in the structured form described above.
268+
269+
``??`` stands for a literal ``?``.
270+
271+
Substitution is only performed when *params* is given;
272+
if no *params* are given, an argument containing a literal ``?`` is unchanged.
273+
The *params* keyword is accepted by :meth:`~IMAP4.search`,
274+
:meth:`~IMAP4.fetch`, :meth:`~IMAP4.sort`, :meth:`~IMAP4.thread` and
275+
:meth:`~IMAP4.uid`.
276+
277+
.. versionadded:: next
278+
The *params* keyword argument.
219279

220280
An :class:`IMAP4` instance has the following methods:
221281

@@ -324,7 +384,7 @@ An :class:`IMAP4` instance has the following methods:
324384
Added the *message_set* and *uid* parameters.
325385

326386

327-
.. method:: IMAP4.fetch(message_set, message_parts, *, uid=False)
387+
.. method:: IMAP4.fetch(message_set, message_parts, *, uid=False, params=None)
328388

329389
Fetch (parts of) messages. *message_parts* should be a string of message part
330390
names enclosed within parentheses, eg: ``"(UID BODY[TEXT])"``. Returned data
@@ -333,8 +393,11 @@ An :class:`IMAP4` instance has the following methods:
333393
If *uid* is true, *message_set* is a set of UIDs and the message numbers in
334394
the response are UIDs (``UID FETCH``).
335395

396+
If *params* is given, ``?`` placeholders in *message_parts* are substituted
397+
with the quoted parameters (see :ref:`the placeholders <imap4-params>`).
398+
336399
.. versionchanged:: next
337-
Added the *uid* parameter.
400+
Added the *params* and *uid* parameters.
338401

339402

340403
.. method:: IMAP4.getacl(mailbox)
@@ -598,7 +661,7 @@ An :class:`IMAP4` instance has the following methods:
598661
code, instead of the usual type.
599662

600663

601-
.. method:: IMAP4.search(charset, criterion[, ...], *, uid=False)
664+
.. method:: IMAP4.search(charset, criterion[, ...], *, uid=False, params=None)
602665

603666
Search mailbox for matching messages. *charset* may be ``None``, in which case
604667
no ``CHARSET`` will be specified in the request to the server. The IMAP
@@ -617,18 +680,22 @@ An :class:`IMAP4` instance has the following methods:
617680
When *charset* is ``None`` (as it must be under ``UTF8=ACCEPT``),
618681
the criterion is sent using the connection's encoding instead.
619682

683+
If *params* is given, ``?`` placeholders in the criteria are substituted
684+
with the quoted parameters (see :ref:`the placeholders <imap4-params>`).
685+
620686
Example::
621687

622688
# M is a connected IMAP4 instance...
623-
typ, msgnums = M.search(None, 'FROM', '"LDJ"')
689+
typ, msgnums = M.search(None, 'FROM', '"John Smith"')
624690

625691
# or:
626-
typ, msgnums = M.search(None, '(FROM "LDJ")')
692+
typ, msgnums = M.search(None, '(FROM "John Smith")')
627693

628-
.. versionchanged:: next
629-
Added the *uid* parameter.
694+
# or, letting the module quote the value (this is recommended):
695+
typ, msgnums = M.search(None, 'FROM ?', params=['John Smith'])
630696

631697
.. versionchanged:: next
698+
Added the *params* and *uid* parameters.
632699
``str`` search criteria are encoded to *charset*.
633700

634701

@@ -675,7 +742,7 @@ An :class:`IMAP4` instance has the following methods:
675742
Returns socket instance used to connect to server.
676743

677744

678-
.. method:: IMAP4.sort(sort_criteria, charset, search_criterion[, ...], *, uid=False)
745+
.. method:: IMAP4.sort(sort_criteria, charset, search_criterion[, ...], *, uid=False, params=None)
679746

680747
The ``sort`` command is a variant of ``search`` with sorting semantics for the
681748
results. Returned data contains a space separated list of matching message
@@ -696,12 +763,13 @@ An :class:`IMAP4` instance has the following methods:
696763
a *search_criterion* passed as :class:`str` is encoded to *charset*;
697764
pass :class:`bytes` to send one already encoded.
698765

699-
This is an ``IMAP4rev1`` extension command.
766+
If *params* is given, ``?`` placeholders in the search criteria are
767+
substituted with the quoted parameters (see :ref:`the placeholders <imap4-params>`).
700768

701-
.. versionchanged:: next
702-
Added the *uid* parameter.
769+
This is an ``IMAP4rev1`` extension command.
703770

704771
.. versionchanged:: next
772+
Added the *params* and *uid* parameters.
705773
``str`` search criteria are encoded to *charset*.
706774

707775

@@ -745,7 +813,7 @@ An :class:`IMAP4` instance has the following methods:
745813

746814
typ, data = M.search(None, 'ALL')
747815
for num in data[0].split():
748-
M.store(num, '+FLAGS', '\\Deleted')
816+
M.store(num, '+FLAGS', r'\Deleted')
749817
M.expunge()
750818

751819
.. note::
@@ -768,7 +836,7 @@ An :class:`IMAP4` instance has the following methods:
768836
Subscribe to new mailbox.
769837

770838

771-
.. method:: IMAP4.thread(threading_algorithm, charset, search_criterion[, ...], *, uid=False)
839+
.. method:: IMAP4.thread(threading_algorithm, charset, search_criterion[, ...], *, uid=False, params=None)
772840

773841
The ``thread`` command is a variant of ``search`` with threading semantics for
774842
the results. Returned data contains a space separated list of thread members.
@@ -793,22 +861,30 @@ An :class:`IMAP4` instance has the following methods:
793861
a *search_criterion* passed as :class:`str` is encoded to *charset*;
794862
pass :class:`bytes` to send one already encoded.
795863

796-
This is an ``IMAP4rev1`` extension command.
864+
If *params* is given, ``?`` placeholders in the search criteria are
865+
substituted with the quoted parameters (see :ref:`the placeholders <imap4-params>`).
797866

798-
.. versionchanged:: next
799-
Added the *uid* parameter.
867+
This is an ``IMAP4rev1`` extension command.
800868

801869
.. versionchanged:: next
870+
Added the *params* and *uid* parameters.
802871
``str`` search criteria are encoded to *charset*.
803872

804873

805-
.. method:: IMAP4.uid(command, arg[, ...])
874+
.. method:: IMAP4.uid(command, arg[, ...], *, params=None)
806875

807876
Execute command args with messages identified by UID, rather than message
808877
number. Returns response appropriate to command. At least one argument must be
809878
supplied; if none are provided, the server will return an error and an exception
810879
will be raised.
811880

881+
If *params* is given, ``?`` placeholders in the ``SEARCH``, ``SORT`` and
882+
``THREAD`` criteria or in the ``FETCH`` parts are substituted with the quoted
883+
parameters (see :ref:`the placeholders <imap4-params>`).
884+
885+
.. versionchanged:: next
886+
Added the *params* parameter.
887+
812888

813889
.. method:: IMAP4.unsubscribe(mailbox)
814890

Doc/whatsnew/3.16.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,17 @@ imaplib
290290
the criteria are sent using the connection encoding instead.
291291
(Contributed by Serhiy Storchaka in :gh:`153494`.)
292292

293+
* Command methods now accept structured arguments,
294+
so the module takes care of quoting instead of the caller.
295+
A *message_set* and lists of flags or other atoms
296+
can be passed as sequences instead of preformatted strings,
297+
and the :meth:`~imaplib.IMAP4.search`, :meth:`~imaplib.IMAP4.fetch`,
298+
:meth:`~imaplib.IMAP4.sort`, :meth:`~imaplib.IMAP4.thread` and
299+
:meth:`~imaplib.IMAP4.uid` methods accept a *params* keyword argument
300+
that substitutes and quotes ``?`` placeholders,
301+
in the manner of :mod:`sqlite3` parameter substitution.
302+
(Contributed by Serhiy Storchaka in :gh:`153521`.)
303+
293304

294305
ipaddress
295306
---------

0 commit comments

Comments
 (0)