8000 gh-55454: Add IMAP4 IDLE support to imaplib · python/cpython@c7c78e4 · GitHub
[go: up one dir, main page]

Skip to content
8000

Commit c7c78e4

Browse files
committed
gh-55454: Add IMAP4 IDLE support to imaplib
This extends imaplib with support for the rfc2177 IMAP IDLE command, as requested in #55454. It allows events to be pushed to a client as they occur, rather than having to continually poll for mailbox changes. The interface is a new idle() method, which returns an iterable context manager. Entering the context starts IDLE mode, during which events (untagged responses) can be retrieved using the iteration protocol. Exiting the context sends DONE to the server, ending IDLE mode. An optional time limit for the IDLE session is supported, for use with servers that impose an inactivity timeout. The context manager also offers a burst() method, designed for programs wishing to process events in batch rather than one at a time. Notable differences from other implementations: - It's an extension to imaplib, rather than a replacement. - It doesn't introduce additional threads. - It doesn't impose new requirements on the use of imaplib's existing methods. - It passes the unit tests in CPython's test/test_imaplib.py module (and adds new ones). - It works on Windows, Linux, and other unix-like systems. - It makes IDLE available on all of imaplib's client variants (including IMAP4_stream). - The interface is pythonic and easy to use. Caveats: - Due to a Windows limitation, the special case of IMAP4_stream running on Windows lacks a duration/timeout feature. (This is the stdin/stdout pipe connection variant; timeouts work fine for socket-based connections, even on Windows.) I have documented it where appropriate. - The file-like imaplib instance attributes are changed from buffered to unbuffered mode. This could potentially break any client code that uses those objects directly without expecting partial reads/writes. However, these attributes are undocumented. As such, I think (and PEP 8 confirms) that they are fair game for changes. https://peps.python.org/pep-0008/#public-and-internal-interfaces Usage examples: #55454 (comment) Original discussion: https://discuss.python.org/t/gauging-interest-in-my-imap4-idle-implementation-for-imaplib/59272 Earlier requests and suggestions: #55454 https://mail.python.org/archives/list/python-ideas@python.org/thread/C4TVEYL5IBESQQPPS5GBR7WFBXCLQMZ2/
1 parent 1cac090 commit c7c78e4

File tree

6 files changed

+507
-20
lines changed

6 files changed

+507
-20
lines changed

Doc/library/imaplib.rst

Lines changed: 100 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
.. changes for IMAP4_SSL by Tino Lange <Tino.Lange@isg.de>, March 2002
1111
.. changes for IMAP4_stream by Piers Lauder <piers@communitysolutions.com.au>,
1212
November 2002
13+
.. changes for IDLE by Forest <forestix@nom.one> August 2024
1314
1415
**Source code:** :source:`Lib/imaplib.py`
1516

@@ -187,7 +188,7 @@ However, the *password* argument to the ``LOGIN`` command is always quoted. If
187188
you want to avoid having an argument string quoted (eg: the *flags* argument to
188189
``STORE``) then enclose the string in parentheses (eg: ``r'(\Deleted)'``).
189190

190-
Each command returns a tuple: ``(type, [data, ...])`` where *type* is usually
191+
Most commands return a tuple: ``(type, [data, ...])`` where *type* is usually
191192
``'OK'`` or ``'NO'``, and *data* is either the text from the command response,
192193
or mandated results from the command. Each *data* is either a ``bytes``, or a
193194
tuple. If a tuple, then the first part is the header of the response, and the
@@ -307,6 +308,48 @@ An :class:`IMAP4` instance has the following methods:
307308
of the IMAP4 QUOTA extension defined in rfc2087.
308309

309310

311+
.. method:: IMAP4.idle([dur])
312+
313+
Return an iterable context manager implementing the ``IDLE`` command
314+
as defined in :rfc:`2177`.
315+
316+
The optional *dur* argument specifies a maximum duration (in seconds) to
317+
keep idling. It defaults to ``None``, meaning no time limit.
318+
To avoid inactivity timeouts on servers that impose them, callers are
319+
advised to keep this <= 29 minutes. See the note below regarding
320+
:class:`IMAP4_stream` on Windows.
321+
322+
The context manager sends the ``IDLE`` command upon entry, produces
323+
responses via iteration, and sends ``DONE`` upon exit.
324+
It represents responses as ``(type, datum)`` tuples, rather than the
325+
``(type, [data, ...])`` tuples returned by other methods, because only
326+
one response is represented at a time.
327+
328+
Example::
329+
330+
with M.idle(dur=29*60) as idler:
331+
for response in idler:
332+
typ, datum = response
333+
print(typ, datum)
334+
335+
It is also possible to process a burst of responses all at once instead
336+
of one at a time. See `IDLE Context Manager`_ for details.
337+
338+
Responses produced by the iterator are not added to the internal
339+
cache for retrieval by :meth:`IMAP4.response`.
340+
341+
.. note::
342+
343+
Windows :class:`IMAP4_stream` connections have no way to accurately
344+
respect *dur*, since Windows ``select()`` only works on sockets.
345+
However, if the server regularly sends status messages during ``IDLE``,
346+
they will wake our selector and keep iteration from blocking for long.
347+
Dovecot's ``imap_idle_notify_interval`` is two minutes by default.
348+
Assuming that's typical of IMAP servers, subtracting it from the 29
349+
minutes needed to avoid server inactivity timeouts would make 27
350+
minutes a sensible value for *dur* in this situation.
351+
352+
310353
.. method:: IMAP4.list([directory[, pattern]])
311354

312355
List mailbox names in *directory* matching *pattern*. *directory* defaults to
@@ -612,6 +655,62 @@ The following attributes are defined on instances of :class:`IMAP4`:
612655
.. versionadded:: 3.5
613656

614657

658+
.. _idle context manager:
659+
660+
IDLE Context Manager
661+
--------------------
662+
663+
The object returned by :meth:`IMAP4.idle` implements the context management
664+
protocol for the :keyword:`with` statement, and the :term:`iterator` protocol
665+
for retrieving untagged responses while the context is active.
666+
It also has the following method:
667+
668+
.. method:: IdleContextManager.burst([interval])
669+
670+
Yield a burst of responses no more than *interval* seconds apart.
671+
672+
This generator retrieves the next response along with any
673+
immediately available subsequent responses (e.g. a rapid series of
674+
``EXPUNGE`` responses after a bulk delete) so they can be efficiently
675+
processed as a batch instead of one at a time.
676+
677+
The optional *interval* argument specifies a time limit (in seconds)
678+
for each response after the first. It defaults to 0.1 seconds.
679+
(The ``IDLE`` context's maximum duration is respected when waiting for the
680+
first response.)
681+
682+
Represents responses as ``(type, datum)`` tuples, just as when
683+
iterating directly on the context manager.
684+
685+
Example::
686+
687+
with M.idle() as idler:
688+
689+
# get the next response and any others following by < 0.1 seconds
690+
batch = list(idler.burst())
691+
692+
print(f'processing {len(batch)} responses...')
693+
for typ, datum in batch:
694+
print(typ, datum)
695+
696+
Produces no responses and returns immediately if the ``IDLE`` context's
697+
maximum duration (the *dur* argument to :meth:`IMAP4.idle`) has elapsed.
698+
Callers should plan accordingly if using this method in a loop.
699+
700+
.. note::
701+
702+
Windows :class:`IMAP4_stream` connections will ignore the *interval*
703+
argument, yielding endless responses and blocking indefinitely for each
704+
one, since Windows ``select()`` only works on sockets. It is therefore
705+
advised not to use this method with an :class:`IMAP4_stream` connection
706+
on Windows.
707+
708+
.. note::
709+
710+
The context manager's type name is not part of its public interface,
711+
and is subject to change.
712+
713+
615714
.. _imap4-example:
616715

617716
IMAP4 Example

Doc/whatsnew/3.14.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,12 @@ Added support for converting any objects that have the
117117
:meth:`!as_integer_ratio` method to a :class:`~fractions.Fraction`.
118118
(Contributed by Serhiy Storchaka in :gh:`82017`.)
119119

120+
imaplib
121+
-------
122+
123+
* Add :meth:`~imaplib.IMAP4.idle`, implementing the ``IDLE`` command
124+
as defined in :rfc:`2177`. (Contributed by Forest in :gh:`55454`.)
125+
120126
json
121127
----
122128

0 commit comments

Comments
 (0)
0