Skip to content

Commit 38035fe

Browse files
gildeagpshead
andauthored
gh-90890: New methods to access mailbox.Maildir message info and flags (#103905)
New methods to access mailbox.Maildir message info and flags: get_info, set_info, get_flags, set_flags, add_flag, remove_flag. These methods speed up accessing a message's info and/or flags and are useful when it is not necessary to access the message's contents, as when iterating over a Maildir to find messages with specific flags. --------- * Add more str type checking * modernize to f-strings instead of % Co-authored-by: Gregory P. Smith <greg@krypto.org>
1 parent fa84e5f commit 38035fe

File tree

5 files changed

+247
-1
lines changed

5 files changed

+247
-1
lines changed

Doc/library/mailbox.rst

+103-1
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,108 @@ Supported mailbox formats are Maildir, mbox, MH, Babyl, and MMDF.
424424
remove the underlying message while the returned file remains open.
425425

426426

427+
.. method:: get_flags(key)
428+
429+
Return as a string the flags that are set on the message
430+
corresponding to *key*.
431+
This is the same as ``get_message(key).get_flags()`` but much
432+
faster, because it does not open the message file.
433+
Use this method when iterating over the keys to determine which
434+
messages are interesting to get.
435+
436+
If you do have a :class:`MaildirMessage` object, use
437+
its :meth:`~MaildirMessage.get_flags` method instead, because
438+
changes made by the message's :meth:`~MaildirMessage.set_flags`,
439+
:meth:`~MaildirMessage.add_flag` and :meth:`~MaildirMessage.remove_flag`
440+
methods are not reflected here until the mailbox's
441+
:meth:`__setitem__` method is called.
442+
443+
.. versionadded:: 3.13
444+
445+
446+
.. method:: set_flags(key, flags)
447+
448+
On the message corresponding to *key*, set the flags specified
449+
by *flags* and unset all others.
450+
Calling ``some_mailbox.set_flags(key, flags)`` is similar to ::
451+
452+
one_message = some_mailbox.get_message(key)
453+
one_message.set_flags(flags)
454+
some_mailbox[key] = one_message
455+
456+
but faster, because it does not open the message file.
457+
458+
If you do have a :class:`MaildirMessage` object, use
459+
its :meth:`~MaildirMessage.set_flags` method instead, because
460+
changes made with this mailbox method will not be visible to the
461+
message object's method, :meth:`~MaildirMessage.get_flags`.
462+
463+
.. versionadded:: 3.13
464+
465+
466+
.. method:: add_flag(key, flag)
467+
468+
On the message corresponding to *key*, set the flags specified
469+
by *flag* without changing other flags. To add more than one
470+
flag at a time, *flag* may be a string of more than one character.
471+
472+
Considerations for using this method versus the message object's
473+
:meth:`~MaildirMessage.add_flag` method are similar to
474+
those for :meth:`set_flags`; see the discussion there.
475+
476+
.. versionadded:: 3.13
477+
478+
479+
.. method:: remove_flag(key, flag)
480+
481+
On the message corresponding to *key*, unset the flags specified
482+
by *flag* without changing other flags. To remove more than one
483+
flag at a time, *flag* may be a string of more than one character.
484+
485+
Considerations for using this method versus the message object's
486+
:meth:`~MaildirMessage.remove_flag` method are similar to
487+
those for :meth:`set_flags`; see the discussion there.
488+
489+
.. versionadded:: 3.13
490+
491+
492+
.. method:: get_info(key)
493+
494+
Return a string containing the info for the message
495+
corresponding to *key*.
496+
This is the same as ``get_message(key).get_info()`` but much
497+
faster, because it does not open the message file.
498+
Use this method when iterating over the keys to determine which
499+
messages are interesting to get.
500+
501+
If you do have a :class:`MaildirMessage` object, use
502+
its :meth:`~MaildirMessage.get_info` method instead, because
503+
changes made by the message's :meth:`~MaildirMessage.set_info` method
504+
are not reflected here until the mailbox's :meth:`__setitem__` method
505+
is called.
506+
507+
.. versionadded:: 3.13
508+
509+
510+
.. method:: set_info(key, info)
511+
512+
Set the info of the message corresponding to *key* to *info*.
513+
Calling ``some_mailbox.set_info(key, flags)`` is similar to ::
514+
515+
one_message = some_mailbox.get_message(key)
516+
one_message.set_info(info)
517+
some_mailbox[key] = one_message
518+
519+
but faster, because it does not open the message file.
520+
521+
If you do have a :class:`MaildirMessage` object, use
522+
its :meth:`~MaildirMessage.set_info` method instead, because
523+
changes made with this mailbox method will not be visible to the
524+
message object's method, :meth:`~MaildirMessage.get_info`.
525+
526+
.. versionadded:: 3.13
527+
528+
427529
.. seealso::
428530

429531
`maildir man page from Courier <https://www.courier-mta.org/maildir.html>`_
@@ -838,7 +940,7 @@ Supported mailbox formats are Maildir, mbox, MH, Babyl, and MMDF.
838940
.. note::
839941

840942
A message is typically moved from :file:`new` to :file:`cur` after its
841-
mailbox has been accessed, whether or not the message is has been
943+
mailbox has been accessed, whether or not the message has been
842944
read. A message ``msg`` has been read if ``"S" in msg.get_flags()`` is
843945
``True``.
844946

Lib/mailbox.py

+50
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,56 @@ def get_file(self, key):
395395
f = open(os.path.join(self._path, self._lookup(key)), 'rb')
396396
return _ProxyFile(f)
397397

398+
def get_info(self, key):
399+
"""Get the keyed message's "info" as a string."""
400+
subpath = self._lookup(key)
401+
if self.colon in subpath:
402+
return subpath.split(self.colon)[-1]
403+
return ''
404+
405+
def set_info(self, key, info: str):
406+
"""Set the keyed message's "info" string."""
407+
if not isinstance(info, str):
408+
raise TypeError(f'info must be a string: {type(info)}')
409+
old_subpath = self._lookup(key)
410+
new_subpath = old_subpath.split(self.colon)[0]
411+
if info:
412+
new_subpath += self.colon + info
413+
if new_subpath == old_subpath:
414+
return
415+
old_path = os.path.join(self._path, old_subpath)
416+
new_path = os.path.join(self._path, new_subpath)
417+
os.rename(old_path, new_path)
418+
self._toc[key] = new_subpath
419+
420+
def get_flags(self, key):
421+
"""Return as a string the standard flags that are set on the keyed message."""
422+
info = self.get_info(key)
423+
if info.startswith('2,'):
424+
return info[2:]
425+
return ''
426+
427+
def set_flags(self, key, flags: str):
428+
"""Set the given flags and unset all others on the keyed message."""
429+
if not isinstance(flags, str):
430+
raise TypeError(f'flags must be a string: {type(flags)}')
431+
# TODO: check if flags are valid standard flag characters?
432+
self.set_info(key, '2,' + ''.join(sorted(set(flags))))
433+
434+
def add_flag(self, key, flag: str):
435+
"""Set the given flag(s) without changing others on the keyed message."""
436+
if not isinstance(flag, str):
437+
raise TypeError(f'flag must be a string: {type(flag)}')
438+
# TODO: check that flag is a valid standard flag character?
439+
self.set_flags(key, ''.join(set(self.get_flags(key)) | set(flag)))
440+
441+
def remove_flag(self, key, flag: str):
442+
"""Unset the given string flag(s) without changing others on the keyed message."""
443+
if not isinstance(flag, str):
444+
raise TypeError(f'flag must be a string: {type(flag)}')
445+
if self.get_flags(key):
446+
self.set_flags(key, ''.join(set(self.get_flags(key)) - set(flag)))
447+
398448
def iterkeys(self):
399449
"""Return an iterator over keys."""
400450
self._refresh()

Lib/test/test_mailbox.py

+86
Original file line numberDiff line numberDiff line change
@@ -847,6 +847,92 @@ def test_lock_unlock(self):
847847
self._box.lock()
848848
self._box.unlock()
849849

850+
def test_get_info(self):
851+
# Test getting message info from Maildir, not the message.
852+
msg = mailbox.MaildirMessage(self._template % 0)
853+
key = self._box.add(msg)
854+
self.assertEqual(self._box.get_info(key), '')
855+
msg.set_info('OurTestInfo')
856+
self._box[key] = msg
857+
self.assertEqual(self._box.get_info(key), 'OurTestInfo')
858+
859+
def test_set_info(self):
860+
# Test setting message info from Maildir, not the message.
861+
# This should immediately rename the message file.
862+
msg = mailbox.MaildirMessage(self._template % 0)
863+
key = self._box.add(msg)
864+
def check_info(oldinfo, newinfo):
865+
oldfilename = os.path.join(self._box._path, self._box._lookup(key))
866+
newsubpath = self._box._lookup(key).split(self._box.colon)[0]
867+
if newinfo:
868+
newsubpath += self._box.colon + newinfo
869+
newfilename = os.path.join(self._box._path, newsubpath)
870+
# assert initial conditions
871+
self.assertEqual(self._box.get_info(key), oldinfo)
872+
if not oldinfo:
873+
self.assertNotIn(self._box._lookup(key), self._box.colon)
874+
self.assertTrue(os.path.exists(oldfilename))
875+
if oldinfo != newinfo:
876+
self.assertFalse(os.path.exists(newfilename))
877+
# do the rename
878+
self._box.set_info(key, newinfo)
879+
# assert post conditions
880+
if not newinfo:
881+
self.assertNotIn(self._box._lookup(key), self._box.colon)
882+
if oldinfo != newinfo:
883+
self.assertFalse(os.path.exists(oldfilename))
884+
self.assertTrue(os.path.exists(newfilename))
885+
self.assertEqual(self._box.get_info(key), newinfo)
886+
# none -> has info
887+
check_info('', 'info1')
888+
# has info -> same info
889+
check_info('info1', 'info1')
890+
# has info -> different info
891+
check_info('info1', 'info2')
892+
# has info -> none
893+
check_info('info2', '')
894+
# none -> none
895+
check_info('', '')
896+
897+
def test_get_flags(self):
898+
# Test getting message flags from Maildir, not the message.
899+
msg = mailbox.MaildirMessage(self._template % 0)
900+
key = self._box.add(msg)
901+
self.assertEqual(self._box.get_flags(key), '')
902+
msg.set_flags('T')
903+
self._box[key] = msg
904+
self.assertEqual(self._box.get_flags(key), 'T')
905+
906+
def test_set_flags(self):
907+
msg = mailbox.MaildirMessage(self._template % 0)
908+
key = self._box.add(msg)
909+
self.assertEqual(self._box.get_flags(key), '')
910+
self._box.set_flags(key, 'S')
911+
self.assertEqual(self._box.get_flags(key), 'S')
912+
913+
def test_add_flag(self):
914+
msg = mailbox.MaildirMessage(self._template % 0)
915+
key = self._box.add(msg)
916+
self.assertEqual(self._box.get_flags(key), '')
917+
self._box.add_flag(key, 'B')
918+
self.assertEqual(self._box.get_flags(key), 'B')
919+
self._box.add_flag(key, 'B')
920+
self.assertEqual(self._box.get_flags(key), 'B')
921+
self._box.add_flag(key, 'AC')
922+
self.assertEqual(self._box.get_flags(key), 'ABC')
923+
924+
def test_remove_flag(self):
925+
msg = mailbox.MaildirMessage(self._template % 0)
926+
key = self._box.add(msg)
927+
self._box.set_flags(key, 'abc')
928+
self.assertEqual(self._box.get_flags(key), 'abc')
929+
self._box.remove_flag(key, 'b')
930+
self.assertEqual(self._box.get_flags(key), 'ac')
931+
self._box.remove_flag(key, 'b')
932+
self.assertEqual(self._box.get_flags(key), 'ac')
933+
self._box.remove_flag(key, 'ac')
934+
self.assertEqual(self._box.get_flags(key), '')
935+
850936
def test_folder (self):
851937
# Test for bug #1569790: verify that folders returned by .get_folder()
852938
# use the same factory function.

Misc/ACKS

+1
Original file line numberDiff line numberDiff line change
@@ -630,6 +630,7 @@ Dinu Gherman
630630
Subhendu Ghosh
631631
Jonathan Giddy
632632
Johannes Gijsbers
633+
Stephen Gildea
633634
Michael Gilfix
634635
Julian Gindi
635636
Yannick Gingras
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
New methods :meth:`mailbox.Maildir.get_info`,
2+
:meth:`mailbox.Maildir.set_info`, :meth:`mailbox.Maildir.get_flags`,
3+
:meth:`mailbox.Maildir.set_flags`, :meth:`mailbox.Maildir.add_flag`,
4+
:meth:`mailbox.Maildir.remove_flag`. These methods speed up accessing a
5+
message's info and/or flags and are useful when it is not necessary to
6+
access the message's contents, as when iterating over a Maildir to find
7+
messages with specific flags.

0 commit comments

Comments
 (0)