Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Doc/library/imaplib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,15 @@ An :class:`IMAP4` instance has the following methods:

Unsubscribe from old mailbox.

.. method:: IMAP4.unselect()

:meth:`imaplib.IMAP4.unselect` frees server's resources associated with the
selected mailbox and returns the server to the authenticated
state. This command performs the same actions as :meth:`imaplib.IMAP4.close`, except
that no messages are permanently removed from the currently
selected mailbox.

.. versionadded:: 3.9

.. method:: IMAP4.xatom(name[, ...])

Expand Down
7 changes: 7 additions & 0 deletions Doc/whatsnew/3.9.rst
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,13 @@ with this change. The overridden methods of :class:`~imaplib.IMAP4_SSL` and
:class:`~imaplib.IMAP4_stream` were applied to this change.
(Contributed by Dong-hee Na in :issue:`38615`.)

:meth:`imaplib.IMAP4.unselect` is added.
:meth:`imaplib.IMAP4.unselect` frees server's resources associated with the
selected mailbox and returns the server to the authenticated
state. This command performs the same actions as :meth:`imaplib.IMAP4.close`, except
that no messages are permanently removed from the currently
selected mailbox. (Contributed by Dong-hee Na in :issue:`40375`.)

importlib
---------

Expand Down
17 changes: 17 additions & 0 deletions Lib/imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@
'THREAD': ('SELECTED',),
'UID': ('SELECTED',),
'UNSUBSCRIBE': ('AUTH', 'SELECTED'),
'UNSELECT': ('SELECTED',),
Comment thread
corona10 marked this conversation as resolved.
}

# Patterns to match server responses
Expand Down Expand Up @@ -902,6 +903,22 @@ def unsubscribe(self, mailbox):
return self._simple_command('UNSUBSCRIBE', mailbox)


def unselect(self):
"""Free server's resources associated with the selected mailbox
and returns the server to the authenticated state.
This command performs the same actions as CLOSE, except
that no messages are permanently removed from the currently
selected mailbox.

(typ, [data]) = <instance>.unselect()
"""
try:
typ, data = self._simple_command('UNSELECT')
finally:
self.state = 'AUTH'
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that you copied this code from the close() command. Is it correct to reset to AUTH state if _simple_command() failed with a socket error or timeout error? I don't think that it's correct to reset to AUTH state. Maybe we should even ensure that the IMAP replied "OK" in typ, since very old server may fail if they don't implement the command.

In case of doubt, I'm ok to leave the code as it it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's correct enough to return to AUTH state in all cases.

Although really, I think the entire design of the client trying to track the server state is a design error. The commands should just pass to the server, and if the server thinks the connection isn't in the correct state let it generate an error.

But it's likely not worth changing this. The differences between SELECT and AUTH are small, and if you issued an UNSELECT then I think assuming you'll issue a SELECT command before trying another SELECT-state-only command are pretty high.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I wrote, I'm fine with leaving the code as it is. Fixing imaplib design can be addressed in a separated PR if someone considers that the design should/can be enhanced.

return typ, data


def xatom(self, name, *args):
"""Allow simple extension commands
notified by server in CAPABILITY response.
Expand Down
25 changes: 25 additions & 0 deletions Lib/test/test_imaplib.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ class SimpleIMAPHandler(socketserver.StreamRequestHandler):

def setup(self):
super().setup()
self.server.is_selected = False
self.server.logged = None

def _send(self, message):
Expand Down Expand Up @@ -190,6 +191,18 @@ def cmd_LOGIN(self, tag, args):
self.server.logged = args[0]
self._send_tagged(tag, 'OK', 'LOGIN completed')

def cmd_SELECT(self, tag, args):
self.server.is_selected = True
self._send_line(b'* 2 EXISTS')
self._send_tagged(tag, 'OK', '[READ-WRITE] SELECT completed.')

def cmd_UNSELECT(self, tag, args):
if self.server.is_selected:
self.server.is_selected = False
self._send_tagged(tag, 'OK', 'Returned to authenticated state. (Success)')
else:
self._send_tagged(tag, 'BAD', 'No mailbox selected')


class NewIMAPTestsMixin():
client = None
Expand Down Expand Up @@ -511,6 +524,18 @@ def cmd_LSUB(self, tag, args):
self.assertEqual(typ, 'OK')
self.assertEqual(data[0], b'() "." directoryA')

def test_unselect(self):
client, _ = self._setup(SimpleIMAPHandler)
client.login('user', 'pass')
typ, data = client.select()
self.assertEqual(typ, 'OK')
self.assertEqual(data[0], b'2')

typ, data = client.unselect()
self.assertEqual(typ, 'OK')
self.assertEqual(data[0], b'Returned to authenticated state. (Success)')
self.assertEqual(client.state, 'AUTH')


class NewIMAPTests(NewIMAPTestsMixin, unittest.TestCase):
imap_class = imaplib.IMAP4
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
:meth:`imaplib.IMAP4.unselect` is added. Patch by Dong-hee Na.