Skip to content

Commit

Permalink
pythongh-90890: New methods to access mailbox.Maildir message info an…
Browse files Browse the repository at this point in the history
…d flags (python#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>
  • Loading branch information
gildea and gpshead authored Nov 11, 2023
1 parent fa84e5f commit 38035fe
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 1 deletion.
104 changes: 103 additions & 1 deletion Doc/library/mailbox.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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>`_
Expand Down Expand Up @@ -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``.

Expand Down
50 changes: 50 additions & 0 deletions Lib/mailbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,56 @@ def get_file(self, key):
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()
Expand Down
86 changes: 86 additions & 0 deletions Lib/test/test_mailbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -847,6 +847,92 @@ def test_lock_unlock(self):
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.
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -630,6 +630,7 @@ Dinu Gherman
Subhendu Ghosh
Jonathan Giddy
Johannes Gijsbers
Stephen Gildea
Michael Gilfix
Julian Gindi
Yannick Gingras
Expand Down
Original file line number Diff line number Diff line change
@@ -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.

0 comments on commit 38035fe

Please sign in to comment.