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>
This commit is contained in:
Stephen Gildea 2023-11-11 09:41:33 -08:00 committed by GitHub
parent fa84e5fe0a
commit 38035fed9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 247 additions and 1 deletions

View File

@ -424,6 +424,108 @@ Supported mailbox formats are Maildir, mbox, MH, Babyl, and MMDF.
remove the underlying message while the returned file remains open.
.. method:: get_flags(key)
Return as a string the flags that are set on the message
corresponding to *key*.
This is the same as ``get_message(key).get_flags()`` but much
faster, because it does not open the message file.
Use this method when iterating over the keys to determine which
messages are interesting to get.
If you do have a :class:`MaildirMessage` object, use
its :meth:`~MaildirMessage.get_flags` method instead, because
changes made by the message's :meth:`~MaildirMessage.set_flags`,
:meth:`~MaildirMessage.add_flag` and :meth:`~MaildirMessage.remove_flag`
methods are not reflected here until the mailbox's
:meth:`__setitem__` method is called.
.. versionadded:: 3.13
.. method:: set_flags(key, flags)
On the message corresponding to *key*, set the flags specified
by *flags* and unset all others.
Calling ``some_mailbox.set_flags(key, flags)`` is similar to ::
one_message = some_mailbox.get_message(key)
one_message.set_flags(flags)
some_mailbox[key] = one_message
but faster, because it does not open the message file.
If you do have a :class:`MaildirMessage` object, use
its :meth:`~MaildirMessage.set_flags` method instead, because
changes made with this mailbox method will not be visible to the
message object's method, :meth:`~MaildirMessage.get_flags`.
.. versionadded:: 3.13
.. method:: add_flag(key, flag)
On the message corresponding to *key*, set the flags specified
by *flag* without changing other flags. To add more than one
flag at a time, *flag* may be a string of more than one character.
Considerations for using this method versus the message object's
:meth:`~MaildirMessage.add_flag` method are similar to
those for :meth:`set_flags`; see the discussion there.
.. versionadded:: 3.13
.. method:: remove_flag(key, flag)
On the message corresponding to *key*, unset the flags specified
by *flag* without changing other flags. To remove more than one
flag at a time, *flag* may be a string of more than one character.
Considerations for using this method versus the message object's
:meth:`~MaildirMessage.remove_flag` method are similar to
those for :meth:`set_flags`; see the discussion there.
.. versionadded:: 3.13
.. method:: get_info(key)
Return a string containing the info for the message
corresponding to *key*.
This is the same as ``get_message(key).get_info()`` but much
faster, because it does not open the message file.
Use this method when iterating over the keys to determine which
messages are interesting to get.
If you do have a :class:`MaildirMessage` object, use
its :meth:`~MaildirMessage.get_info` method instead, because
changes made by the message's :meth:`~MaildirMessage.set_info` method
are not reflected here until the mailbox's :meth:`__setitem__` method
is called.
.. versionadded:: 3.13
.. method:: set_info(key, info)
Set the info of the message corresponding to *key* to *info*.
Calling ``some_mailbox.set_info(key, flags)`` is similar to ::
one_message = some_mailbox.get_message(key)
one_message.set_info(info)
some_mailbox[key] = one_message
but faster, because it does not open the message file.
If you do have a :class:`MaildirMessage` object, use
its :meth:`~MaildirMessage.set_info` method instead, because
changes made with this mailbox method will not be visible to the
message object's method, :meth:`~MaildirMessage.get_info`.
.. versionadded:: 3.13
.. seealso::
`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.
.. note::
A message is typically moved from :file:`new` to :file:`cur` after its
mailbox has been accessed, whether or not the message is has been
mailbox has been accessed, whether or not the message has been
read. A message ``msg`` has been read if ``"S" in msg.get_flags()`` is
``True``.

View File

@ -395,6 +395,56 @@ class Maildir(Mailbox):
f = open(os.path.join(self._path, self._lookup(key)), 'rb')
return _ProxyFile(f)
def get_info(self, key):
"""Get the keyed message's "info" as a string."""
subpath = self._lookup(key)
if self.colon in subpath:
return subpath.split(self.colon)[-1]
return ''
def set_info(self, key, info: str):
"""Set the keyed message's "info" string."""
if not isinstance(info, str):
raise TypeError(f'info must be a string: {type(info)}')
old_subpath = self._lookup(key)
new_subpath = old_subpath.split(self.colon)[0]
if info:
new_subpath += self.colon + info
if new_subpath == old_subpath:
return
old_path = os.path.join(self._path, old_subpath)
new_path = os.path.join(self._path, new_subpath)
os.rename(old_path, new_path)
self._toc[key] = new_subpath
def get_flags(self, key):
"""Return as a string the standard flags that are set on the keyed message."""
info = self.get_info(key)
if info.startswith('2,'):
return info[2:]
return ''
def set_flags(self, key, flags: str):
"""Set the given flags and unset all others on the keyed message."""
if not isinstance(flags, str):
raise TypeError(f'flags must be a string: {type(flags)}')
# TODO: check if flags are valid standard flag characters?
self.set_info(key, '2,' + ''.join(sorted(set(flags))))
def add_flag(self, key, flag: str):
"""Set the given flag(s) without changing others on the keyed message."""
if not isinstance(flag, str):
raise TypeError(f'flag must be a string: {type(flag)}')
# TODO: check that flag is a valid standard flag character?
self.set_flags(key, ''.join(set(self.get_flags(key)) | set(flag)))
def remove_flag(self, key, flag: str):
"""Unset the given string flag(s) without changing others on the keyed message."""
if not isinstance(flag, str):
raise TypeError(f'flag must be a string: {type(flag)}')
if self.get_flags(key):
self.set_flags(key, ''.join(set(self.get_flags(key)) - set(flag)))
def iterkeys(self):
"""Return an iterator over keys."""
self._refresh()

View File

@ -847,6 +847,92 @@ class TestMaildir(TestMailbox, unittest.TestCase):
self._box.lock()
self._box.unlock()
def test_get_info(self):
# Test getting message info from Maildir, not the message.
msg = mailbox.MaildirMessage(self._template % 0)
key = self._box.add(msg)
self.assertEqual(self._box.get_info(key), '')
msg.set_info('OurTestInfo')
self._box[key] = msg
self.assertEqual(self._box.get_info(key), 'OurTestInfo')
def test_set_info(self):
# Test setting message info from Maildir, not the message.
# This should immediately rename the message file.
msg = mailbox.MaildirMessage(self._template % 0)
key = self._box.add(msg)
def check_info(oldinfo, newinfo):
oldfilename = os.path.join(self._box._path, self._box._lookup(key))
newsubpath = self._box._lookup(key).split(self._box.colon)[0]
if newinfo:
newsubpath += self._box.colon + newinfo
newfilename = os.path.join(self._box._path, newsubpath)
# assert initial conditions
self.assertEqual(self._box.get_info(key), oldinfo)
if not oldinfo:
self.assertNotIn(self._box._lookup(key), self._box.colon)
self.assertTrue(os.path.exists(oldfilename))
if oldinfo != newinfo:
self.assertFalse(os.path.exists(newfilename))
# do the rename
self._box.set_info(key, newinfo)
# assert post conditions
if not newinfo:
self.assertNotIn(self._box._lookup(key), self._box.colon)
if oldinfo != newinfo:
self.assertFalse(os.path.exists(oldfilename))
self.assertTrue(os.path.exists(newfilename))
self.assertEqual(self._box.get_info(key), newinfo)
# none -> has info
check_info('', 'info1')
# has info -> same info
check_info('info1', 'info1')
# has info -> different info
check_info('info1', 'info2')
# has info -> none
check_info('info2', '')
# none -> none
check_info('', '')
def test_get_flags(self):
# Test getting message flags from Maildir, not the message.
msg = mailbox.MaildirMessage(self._template % 0)
key = self._box.add(msg)
self.assertEqual(self._box.get_flags(key), '')
msg.set_flags('T')
self._box[key] = msg
self.assertEqual(self._box.get_flags(key), 'T')
def test_set_flags(self):
msg = mailbox.MaildirMessage(self._template % 0)
key = self._box.add(msg)
self.assertEqual(self._box.get_flags(key), '')
self._box.set_flags(key, 'S')
self.assertEqual(self._box.get_flags(key), 'S')
def test_add_flag(self):
msg = mailbox.MaildirMessage(self._template % 0)
key = self._box.add(msg)
self.assertEqual(self._box.get_flags(key), '')
self._box.add_flag(key, 'B')
self.assertEqual(self._box.get_flags(key), 'B')
self._box.add_flag(key, 'B')
self.assertEqual(self._box.get_flags(key), 'B')
self._box.add_flag(key, 'AC')
self.assertEqual(self._box.get_flags(key), 'ABC')
def test_remove_flag(self):
msg = mailbox.MaildirMessage(self._template % 0)
key = self._box.add(msg)
self._box.set_flags(key, 'abc')
self.assertEqual(self._box.get_flags(key), 'abc')
self._box.remove_flag(key, 'b')
self.assertEqual(self._box.get_flags(key), 'ac')
self._box.remove_flag(key, 'b')
self.assertEqual(self._box.get_flags(key), 'ac')
self._box.remove_flag(key, 'ac')
self.assertEqual(self._box.get_flags(key), '')
def test_folder (self):
# Test for bug #1569790: verify that folders returned by .get_folder()
# use the same factory function.

View File

@ -630,6 +630,7 @@ Dinu Gherman
Subhendu Ghosh
Jonathan Giddy
Johannes Gijsbers
Stephen Gildea
Michael Gilfix
Julian Gindi
Yannick Gingras

View File

@ -0,0 +1,7 @@
New methods :meth:`mailbox.Maildir.get_info`,
:meth:`mailbox.Maildir.set_info`, :meth:`mailbox.Maildir.get_flags`,
:meth:`mailbox.Maildir.set_flags`, :meth:`mailbox.Maildir.add_flag`,
:meth:`mailbox.Maildir.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.