diff --git a/pymap/msgtracker.py b/pymap/msgtracker.py new file mode 100644 index 00000000..d16dab36 --- /dev/null +++ b/pymap/msgtracker.py @@ -0,0 +1,66 @@ +# Copyright (c) 2014 Ian C. Good +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# + +import asyncio + +__all__ = ['MessageTracker'] + + +class MessageTracker(object): + + def __init__(self): + super().__init__() + self.messages = {} + self.exists = 0 + self.recent = 0 + + @asyncio.coroutine + def update(self, messages): + """Given a list of messages, return a new tracker and a dictionary of + external updates that need to be returned in untagged FETCH responses. + + :param list messages: :class:`~pymap.interfaces.MessageInterface + objects fetched from the backend. + :returns: Two-tuple of the :class:`MessageTracker` object and details + about what has changed since the last :meth:`.update`. + + """ + ret = {} + if self.exists != len(messages): + ret['exists'] = self.exists = len(messages) + uid_flags = {msg.uid: (msg.seq, (yield from msg.fetch_flags())) + for msg in messages} + recent = len([True for _, flags in uid_flags.values() + if br'\Recent' in flags]) + if self.recent != recent: + ret['recent'] = self.recent = recent + before_uids = frozenset(self.messages.keys()) + after_uids = frozenset(uid_flags.keys()) + ret['expunge'] = [self.messages[uid][0] + for uid in (before_uids - after_uids)] + ret['flags'] = [] + for uid in before_uids & after_uids: + before_flags = frozenset(self.messages[uid][1]) + after_flags = frozenset(uid_flags[uid][1]) + if before_flags != after_flags: + seq = uid_flags[uid][0] + ret['flags'].append((seq, after_flags)) + return ret diff --git a/pymap/state.py b/pymap/state.py index f1fb6054..a40276c3 100644 --- a/pymap/state.py +++ b/pymap/state.py @@ -61,6 +61,7 @@ def __init__(self, transport, backend): self.backend = backend self.session = None self.selected = None + self.tracker = None self.capability = Capability([]) @asyncio.coroutine @@ -113,7 +114,7 @@ def do_select(self, cmd): except MailboxNotFound: return ResponseNo(cmd.tag, b'Mailbox does not exist.') self.selected = mbx - yield from mbx.poll() + self.selected_tacker = yield from mbx.init() code, data = self._get_mailbox_response_data(mbx) resp = ResponseOk(cmd.tag, b'Selected mailbox.', code) for data_part in data: @@ -127,7 +128,7 @@ def do_examine(self, cmd): except MailboxNotFound: return ResponseNo(cmd.tag, b'Mailbox does not exist.') self.selected = mbx - yield from mbx.poll() + self.selected_tacker = yield from mbx.init() code, data = self._get_mailbox_response_data(mbx, True) resp = ResponseOk(cmd.tag, b'Examined mailbox.', code) for data_part in data: @@ -176,7 +177,7 @@ def do_status(self, cmd): mbx = yield from self.session.get_mailbox(cmd.mailbox) except MailboxNotFound: return ResponseNo(cmd.tag, b'Mailbox does not exist.') - yield from mbx.poll() + yield from mbx.init() resp = ResponseOk(cmd.tag, b'STATUS completed.') status_list = List([]) for status_item in cmd.status_list: @@ -254,6 +255,7 @@ def do_close(self, cmd): except MailboxReadOnly: pass self.selected = None + self.tracker = None return ResponseOk(cmd.tag, b'CLOSE completed.') @asyncio.coroutine @@ -373,7 +375,7 @@ def do_logout(self, cmd): @asyncio.coroutine def _check_mailbox_updates(self, cmd, resp): send_expunge = not getattr(cmd, 'no_expunge_response', False) - updates = yield from self.selected.poll() + self.tracker, updates = yield from self.selected.poll(self.tracker) if 'exists' in updates: resp.add_data(ExistsResponse(updates['exists'])) if 'recent' in updates: @@ -381,10 +383,10 @@ def _check_mailbox_updates(self, cmd, resp): if 'expunge' in updates and send_expunge: for seq in updates['expunge']: resp.add_data(ExpungeResponse(seq)) - if 'fetch' in updates: - for msg in updates['fetch']: - fetch_data = yield from msg.fetch([self.flags_attr]) - resp.add_data(FetchResponse(msg.seq, fetch_data)) + if 'flags' in updates: + for seq, flags in updates['flags']: + fetch_data = {self.flags_attr: List(flags)} + resp.add_data(FetchResponse(seq, fetch_data)) @asyncio.coroutine def do_command(self, cmd):