Beispiel #1
0
    def condstore_refresh_flags(self, crispin_client):
        new_highestmodseq = crispin_client.conn.folder_status(
            self.folder_name, ['HIGHESTMODSEQ'])['HIGHESTMODSEQ']
        # Ensure that we have an initial highestmodseq value stored before we
        # begin polling for changes.
        if self.highestmodseq is None:
            self.highestmodseq = new_highestmodseq

        if new_highestmodseq == self.highestmodseq:
            # Don't need to do anything if the highestmodseq hasn't
            # changed.
            return
        elif new_highestmodseq < self.highestmodseq:
            # This should really never happen, but if it does, handle it.
            log.warning('got server highestmodseq less than saved '
                        'highestmodseq',
                        new_highestmodseq=new_highestmodseq,
                        saved_highestmodseq=self.highestmodseq)
            return

        # Highestmodseq has changed, update accordingly.
        crispin_client.select_folder(self.folder_name, self.uidvalidity_cb)
        changed_flags = crispin_client.condstore_changed_flags(
            self.highestmodseq)
        remote_uids = crispin_client.all_uids()
        with session_scope() as db_session:
            common.update_metadata(self.account_id, self.folder_id,
                                   changed_flags, db_session)
            local_uids = common.local_uids(self.account_id, db_session,
                                           self.folder_id)
            expunged_uids = set(local_uids).difference(remote_uids)
            common.remove_deleted_uids(self.account_id, self.folder_id,
                                       expunged_uids, db_session)
            db_session.commit()
        self.highestmodseq = new_highestmodseq
Beispiel #2
0
 def resync_uids_impl(self):
     # First, let's check if the UIVDALIDITY change was spurious, if
     # it is, just discard it and go on.
     with self.conn_pool.get() as crispin_client:
         crispin_client.select_folder(self.folder_name, lambda *args: True)
         remote_uidvalidity = crispin_client.selected_uidvalidity
         remote_uidnext = crispin_client.selected_uidnext
         if remote_uidvalidity <= self.uidvalidity:
             log.debug('UIDVALIDITY unchanged')
             return
     # Otherwise, if the UIDVALIDITY really has changed, discard all saved
     # UIDs for the folder, mark associated messages for garbage-collection,
     # and return to the 'initial' state to resync.
     # This will cause message and threads to be deleted and recreated, but
     # uidinvalidity is sufficiently rare that this tradeoff is acceptable.
     with session_scope(self.namespace_id) as db_session:
         invalid_uids = {
             uid
             for uid, in db_session.query(ImapUid.msg_uid).filter_by(
                 account_id=self.account_id, folder_id=self.folder_id)
         }
         common.remove_deleted_uids(self.account_id, self.folder_id,
                                    invalid_uids, db_session)
     self.uidvalidity = remote_uidvalidity
     self.highestmodseq = None
     self.uidnext = remote_uidnext
Beispiel #3
0
    def refresh_flags_impl(self, crispin_client, max_uids):
        crispin_client.select_folder(self.folder_name, self.uidvalidity_cb)
        with session_scope(self.namespace_id) as db_session:
            local_uids = common.local_uids(account_id=self.account_id,
                                           session=db_session,
                                           folder_id=self.folder_id,
                                           limit=max_uids)

        flags = crispin_client.flags(local_uids)
        if (max_uids in self.flags_fetch_results and
                self.flags_fetch_results[max_uids] == (local_uids, flags)):
            # If the flags fetch response is exactly the same as the last one
            # we got, then we don't need to persist any changes.
            log.debug('Unchanged flags refresh response, '
                      'not persisting changes', max_uids=max_uids)
            return
        log.debug('Changed flags refresh response, persisting changes',
                  max_uids=max_uids)
        expunged_uids = set(local_uids).difference(flags.keys())
        common.remove_deleted_uids(self.account_id, self.folder_id,
                                   expunged_uids)
        with session_scope(self.namespace_id) as db_session:
            common.update_metadata(self.account_id, self.folder_id,
                                   self.folder_role, flags, db_session)
        self.flags_fetch_results[max_uids] = (local_uids, flags)
Beispiel #4
0
    def initial_sync_impl(self, crispin_client):
        # We wrap the block in a try/finally because the greenlets like
        # change_poller need to be killed when this greenlet is interrupted
        change_poller = None
        try:
            remote_uids = sorted(crispin_client.all_uids(), key=int)
            with self.syncmanager_lock:
                with session_scope(self.namespace_id) as db_session:
                    local_uids = common.local_uids(
                        self.account_id, db_session, self.folder_id
                    )
                common.remove_deleted_uids(
                    self.account_id, self.folder_id, set(local_uids) - set(remote_uids)
                )
                unknown_uids = set(remote_uids) - local_uids
                with session_scope(self.namespace_id) as db_session:
                    self.update_uid_counts(
                        db_session,
                        remote_uid_count=len(remote_uids),
                        download_uid_count=len(unknown_uids),
                    )

            change_poller = gevent.spawn(self.poll_for_changes)
            bind_context(change_poller, "changepoller", self.account_id, self.folder_id)

            if self.is_all_mail(crispin_client):
                # Prioritize UIDs for messages in the inbox folder.
                if len(remote_uids) < 1e6:
                    inbox_uids = set(
                        crispin_client.search_uids(["X-GM-LABELS", "inbox"])
                    )
                else:
                    # The search above is really slow (times out) on really
                    # large mailboxes, so bound the search to messages within
                    # the past month in order to get anywhere.
                    since = datetime.utcnow() - timedelta(days=30)
                    inbox_uids = set(
                        crispin_client.search_uids(
                            ["X-GM-LABELS", "inbox", "SINCE", since]
                        )
                    )

                uids_to_download = sorted(unknown_uids - inbox_uids) + sorted(
                    unknown_uids & inbox_uids
                )
            else:
                uids_to_download = sorted(unknown_uids)

            for uids in chunk(reversed(uids_to_download), 1024):
                g_metadata = crispin_client.g_metadata(uids)
                # UIDs might have been expunged since sync started, in which
                # case the g_metadata call above will return nothing.
                # They may also have been preemptively downloaded by thread
                # expansion. We can omit such UIDs.
                uids = [u for u in uids if u in g_metadata and u not in self.saved_uids]
                self.batch_download_uids(crispin_client, uids, g_metadata)
        finally:
            if change_poller is not None:
                # schedule change_poller to die
                gevent.kill(change_poller)
def test_deleting_from_a_message_with_multiple_uids(db, default_account,
                                                    message, thread):
    """Check that deleting a imapuid from a message with
    multiple uids doesn't mark the message for deletion."""
    inbox_folder = Folder.find_or_create(db.session, default_account, "inbox",
                                         "inbox")
    sent_folder = Folder.find_or_create(db.session, default_account, "sent",
                                        "sent")

    add_fake_imapuid(db.session, default_account.id, message, sent_folder,
                     1337)
    add_fake_imapuid(db.session, default_account.id, message, inbox_folder,
                     2222)

    assert len(message.imapuids) == 2

    remove_deleted_uids(default_account.id, inbox_folder.id, [2222])
    db.session.expire_all()

    assert (
        message.deleted_at is None
    ), "The associated message should not have been marked for deletion."

    assert len(
        message.imapuids) == 1, "The message should have only one imapuid."
Beispiel #6
0
 def resync_uids_impl(self):
     # First, let's check if the UIVDALIDITY change was spurious, if
     # it is, just discard it and go on.
     with self.conn_pool.get() as crispin_client:
         crispin_client.select_folder(self.folder_name, lambda *args: True)
         remote_uidvalidity = crispin_client.selected_uidvalidity
         remote_uidnext = crispin_client.selected_uidnext
         if remote_uidvalidity <= self.uidvalidity:
             log.debug('UIDVALIDITY unchanged')
             return
     # Otherwise, if the UIDVALIDITY really has changed, discard all saved
     # UIDs for the folder, mark associated messages for garbage-collection,
     # and return to the 'initial' state to resync.
     # This will cause message and threads to be deleted and recreated, but
     # uidinvalidity is sufficiently rare that this tradeoff is acceptable.
     with session_scope(self.namespace_id) as db_session:
         invalid_uids = {
             uid for uid, in db_session.query(ImapUid.msg_uid).
             filter_by(account_id=self.account_id,
                       folder_id=self.folder_id)
         }
         common.remove_deleted_uids(self.account_id, self.folder_id,
                                    invalid_uids, db_session)
     self.uidvalidity = remote_uidvalidity
     self.highestmodseq = None
     self.uidnext = remote_uidnext
def test_drafts_deleted_synchronously(db, default_account, thread, message,
                                      imapuid, folder):
    message.is_draft = True
    msg_uid = imapuid.msg_uid
    remove_deleted_uids(default_account.id, db.session, [msg_uid], folder.id)
    db.session.expire_all()
    assert inspect(message).deleted
    assert inspect(thread).deleted
def test_drafts_deleted_synchronously(db, default_account, thread, message,
                                      imapuid, folder):
    message.is_draft = True
    msg_uid = imapuid.msg_uid
    remove_deleted_uids(default_account.id, folder.id, [msg_uid], db.session)
    db.session.expire_all()
    assert inspect(message).deleted
    assert inspect(thread).deleted
def test_messages_deleted_asynchronously(db, default_account, thread, message,
                                         imapuid, folder):
    msg_uid = imapuid.msg_uid
    update_metadata(default_account.id, folder.id,
                    {msg_uid: GmailFlags((), ('label', ))}, db.session)
    assert 'label' in [cat.display_name for cat in message.categories]
    remove_deleted_uids(default_account.id, folder.id, [msg_uid], db.session)
    assert abs((message.deleted_at - datetime.utcnow()).total_seconds()) < 2
    # Check that message categories do get updated synchronously.
    assert 'label' not in [cat.display_name for cat in message.categories]
def test_messages_deleted_asynchronously(db, default_account, thread, message,
                                         imapuid, folder):
    msg_uid = imapuid.msg_uid
    update_metadata(default_account.id, db.session, folder.name, folder.id,
                    [msg_uid], {msg_uid: GmailFlags((), ('label',))})
    assert 'label' in [cat.display_name for cat in message.categories]
    remove_deleted_uids(default_account.id, db.session, [msg_uid], folder.id)
    assert abs((message.deleted_at - datetime.utcnow()).total_seconds()) < 2
    # Check that message categories do get updated synchronously.
    assert 'label' not in [cat.display_name for cat in message.categories]
Beispiel #11
0
    def initial_sync_impl(self, crispin_client):
        # We wrap the block in a try/finally because the greenlets like
        # change_poller need to be killed when this greenlet is interrupted
        change_poller = None
        try:
            remote_uids = sorted(crispin_client.all_uids(), key=int)
            with self.syncmanager_lock:
                with session_scope(self.namespace_id) as db_session:
                    local_uids = common.local_uids(self.account_id, db_session,
                                                   self.folder_id)
                common.remove_deleted_uids(
                    self.account_id, self.folder_id,
                    set(local_uids) - set(remote_uids))
                unknown_uids = set(remote_uids) - local_uids
                with session_scope(self.namespace_id) as db_session:
                    self.update_uid_counts(
                        db_session, remote_uid_count=len(remote_uids),
                        download_uid_count=len(unknown_uids))

            change_poller = spawn(self.poll_for_changes)
            bind_context(change_poller, 'changepoller', self.account_id,
                         self.folder_id)

            if self.is_all_mail(crispin_client):
                # Prioritize UIDs for messages in the inbox folder.
                if len(remote_uids) < 1e6:
                    inbox_uids = set(
                        crispin_client.search_uids(['X-GM-LABELS', 'inbox']))
                else:
                    # The search above is really slow (times out) on really
                    # large mailboxes, so bound the search to messages within
                    # the past month in order to get anywhere.
                    since = datetime.utcnow() - timedelta(days=30)
                    inbox_uids = set(crispin_client.search_uids([
                        'X-GM-LABELS', 'inbox',
                        'SINCE', since]))

                uids_to_download = (sorted(unknown_uids - inbox_uids) +
                                    sorted(unknown_uids & inbox_uids))
            else:
                uids_to_download = sorted(unknown_uids)

            for uids in chunk(reversed(uids_to_download), 1024):
                g_metadata = crispin_client.g_metadata(uids)
                # UIDs might have been expunged since sync started, in which
                # case the g_metadata call above will return nothing.
                # They may also have been preemptively downloaded by thread
                # expansion. We can omit such UIDs.
                uids = [u for u in uids if u in g_metadata and u not in
                        self.saved_uids]
                self.batch_download_uids(crispin_client, uids, g_metadata)
        finally:
            if change_poller is not None:
                # schedule change_poller to die
                kill(change_poller)
Beispiel #12
0
def test_only_uids_deleted_synchronously(db, default_account,
                                         default_namespace, thread, message,
                                         imapuid, folder):
    msg_uid = imapuid.msg_uid
    update_metadata(default_account.id, db.session, folder.name, folder.id,
                    [msg_uid], {msg_uid: GmailFlags((), ('label', ))})
    assert 'label' in [t.name for t in thread.tags]
    remove_deleted_uids(default_account.id, db.session, [msg_uid], folder.id)
    assert abs((message.deleted_at - datetime.utcnow()).total_seconds()) < 1
    # Check that thread tags do get updated synchronously.
    assert 'label' not in [t.name for t in thread.tags]
def test_only_uids_deleted_synchronously(db, default_account,
                                         default_namespace, thread, message,
                                         imapuid, folder):
    msg_uid = imapuid.msg_uid
    update_metadata(default_account.id, db.session, folder.name, folder.id,
                    [msg_uid], {msg_uid: GmailFlags((), ('label',))})
    assert 'label' in [t.name for t in thread.tags]
    remove_deleted_uids(default_account.id, db.session, [msg_uid], folder.id)
    assert abs((message.deleted_at - datetime.utcnow()).total_seconds()) < 2
    # Check that thread tags do get updated synchronously.
    assert 'label' not in [t.name for t in thread.tags]
Beispiel #14
0
def test_drafts_deleted_synchronously(db, default_account, thread, message,
                                      imapuid, folder):
    message.is_draft = True
    db.session.commit()
    msg_uid = imapuid.msg_uid
    remove_deleted_uids(default_account.id, folder.id, [msg_uid])
    db.session.expire_all()
    with pytest.raises(ObjectDeletedError):
        message.id
    with pytest.raises(ObjectDeletedError):
        thread.id
Beispiel #15
0
    def refresh_flags_impl(self, crispin_client, max_uids):
        crispin_client.select_folder(self.folder_name, self.uidvalidity_cb)
        with session_scope(self.namespace_id) as db_session:
            local_uids = common.local_uids(
                account_id=self.account_id, session=db_session, folder_id=self.folder_id, limit=max_uids
            )

        flags = crispin_client.flags(local_uids)
        expunged_uids = set(local_uids).difference(flags.keys())
        common.remove_deleted_uids(self.account_id, self.folder_id, expunged_uids)
        with session_scope(self.namespace_id) as db_session:
            common.update_metadata(self.account_id, self.folder_id, flags, db_session)
Beispiel #16
0
def test_deletion_deferred_with_longer_ttl(db, default_account,
                                           default_namespace, message, thread,
                                           folder, imapuid):
    msg_uid = imapuid.msg_uid
    handler = DeleteHandler(account_id=default_account.id,
                            namespace_id=default_namespace.id,
                            uid_accessor=lambda m: m.imapuids,
                            message_ttl=1)
    remove_deleted_uids(default_account.id, db.session, [msg_uid], folder.id)
    handler.check()
    # Would raise ObjectDeletedError if objects were deleted
    message.id
    thread.id
Beispiel #17
0
def test_deletion_deferred_with_longer_ttl(db, default_account,
                                           default_namespace, message, thread,
                                           folder, imapuid):
    msg_uid = imapuid.msg_uid
    handler = DeleteHandler(account_id=default_account.id,
                            namespace_id=default_namespace.id,
                            uid_accessor=lambda m: m.imapuids,
                            message_ttl=1)
    remove_deleted_uids(default_account.id, db.session, [msg_uid], folder.id)
    handler.check()
    # Would raise ObjectDeletedError if objects were deleted
    message.id
    thread.id
Beispiel #18
0
def test_deletion_with_short_ttl(db, default_account, default_namespace,
                                 message, thread, folder, imapuid):
    msg_uid = imapuid.msg_uid
    handler = DeleteHandler(account_id=default_account.id,
                            namespace_id=default_namespace.id,
                            uid_accessor=lambda m: m.imapuids,
                            message_ttl=0)
    remove_deleted_uids(default_account.id, db.session, [msg_uid], folder.id)
    handler.check()
    # Check that objects were actually deleted
    with pytest.raises(ObjectDeletedError):
        message.id
    with pytest.raises(ObjectDeletedError):
        thread.id
Beispiel #19
0
def test_deletion_with_short_ttl(db, default_account, default_namespace,
                                 message, thread, folder, imapuid):
    msg_uid = imapuid.msg_uid
    handler = DeleteHandler(account_id=default_account.id,
                            namespace_id=default_namespace.id,
                            uid_accessor=lambda m: m.imapuids,
                            message_ttl=0)
    remove_deleted_uids(default_account.id, db.session, [msg_uid], folder.id)
    handler.check()
    # Check that objects were actually deleted
    with pytest.raises(ObjectDeletedError):
        message.id
    with pytest.raises(ObjectDeletedError):
        thread.id
Beispiel #20
0
    def initial_sync_impl(self, crispin_client):
        # We wrap the block in a try/finally because the change_poller greenlet
        # needs to be killed when this greenlet is interrupted
        change_poller = None
        try:
            assert crispin_client.selected_folder_name == self.folder_name
            remote_uids = crispin_client.all_uids()
            with self.syncmanager_lock:
                with session_scope(self.namespace_id) as db_session:
                    local_uids = common.local_uids(self.account_id, db_session,
                                                   self.folder_id)
                common.remove_deleted_uids(
                    self.account_id,
                    self.folder_id,
                    set(local_uids).difference(remote_uids),
                )

            new_uids = set(remote_uids).difference(local_uids)
            with session_scope(self.namespace_id) as db_session:
                account = db_session.query(Account).get(self.account_id)
                throttled = account.throttled
                self.update_uid_counts(
                    db_session,
                    remote_uid_count=len(remote_uids),
                    # This is the initial size of our download_queue
                    download_uid_count=len(new_uids),
                )

            change_poller = gevent.spawn(self.poll_for_changes)
            bind_context(change_poller, "changepoller", self.account_id,
                         self.folder_id)
            uids = sorted(new_uids, reverse=True)
            count = 0
            for uid in uids:
                # The speedup from batching appears to be less clear for
                # non-Gmail accounts, so for now just download one-at-a-time.
                self.download_and_commit_uids(crispin_client, [uid])
                self.heartbeat_status.publish()
                count += 1
                if throttled and count >= THROTTLE_COUNT:
                    # Throttled accounts' folders sync at a rate of
                    # 1 message/ minute, after the first approx. THROTTLE_COUNT
                    # messages per folder are synced.
                    # Note this is an approx. limit since we use the #(uids),
                    # not the #(messages).
                    gevent.sleep(THROTTLE_WAIT)
        finally:
            if change_poller is not None:
                # schedule change_poller to die
                gevent.kill(change_poller)
Beispiel #21
0
    def condstore_refresh_flags(self, crispin_client):
        new_highestmodseq = crispin_client.conn.folder_status(
            self.folder_name, ['HIGHESTMODSEQ'])['HIGHESTMODSEQ']
        # Ensure that we have an initial highestmodseq value stored before we
        # begin polling for changes.
        if self.highestmodseq is None:
            self.highestmodseq = new_highestmodseq

        if new_highestmodseq == self.highestmodseq:
            # Don't need to do anything if the highestmodseq hasn't
            # changed.
            return
        elif new_highestmodseq < self.highestmodseq:
            # This should really never happen, but if it does, handle it.
            log.warning(
                'got server highestmodseq less than saved '
                'highestmodseq',
                new_highestmodseq=new_highestmodseq,
                saved_highestmodseq=self.highestmodseq)
            return

        # Highestmodseq has changed, update accordingly.
        crispin_client.select_folder(self.folder_name, self.uidvalidity_cb)
        changed_flags = crispin_client.condstore_changed_flags(
            self.highestmodseq)
        remote_uids = crispin_client.all_uids()
        with session_scope(self.namespace_id) as db_session:
            common.update_metadata(self.account_id, self.folder_id,
                                   changed_flags, db_session)
            local_uids = common.local_uids(self.account_id, db_session,
                                           self.folder_id)
            expunged_uids = set(local_uids).difference(remote_uids)

        if expunged_uids:
            # If new UIDs have appeared since we last checked in
            # get_new_uids, save them first. We want to always have the
            # latest UIDs before expunging anything, in order to properly
            # capture draft revisions.
            with session_scope(self.namespace_id) as db_session:
                lastseenuid = common.lastseenuid(self.account_id, db_session,
                                                 self.folder_id)
            if remote_uids and lastseenuid < max(remote_uids):
                log.info('Downloading new UIDs before expunging')
                self.get_new_uids(crispin_client)
            with session_scope(self.namespace_id) as db_session:
                common.remove_deleted_uids(self.account_id, self.folder_id,
                                           expunged_uids, db_session)
                db_session.commit()
        self.highestmodseq = new_highestmodseq
Beispiel #22
0
    def refresh_flags_impl(self, crispin_client, max_uids):
        crispin_client.select_folder(self.folder_name, self.uidvalidity_cb)
        with session_scope(self.namespace_id) as db_session:
            local_uids = common.local_uids(account_id=self.account_id,
                                           session=db_session,
                                           folder_id=self.folder_id,
                                           limit=max_uids)

        flags = crispin_client.flags(local_uids)
        expunged_uids = set(local_uids).difference(flags.keys())
        with session_scope(self.namespace_id) as db_session:
            common.remove_deleted_uids(self.account_id, self.folder_id,
                                       expunged_uids, db_session)
            common.update_metadata(self.account_id, self.folder_id, flags,
                                   db_session)
Beispiel #23
0
    def condstore_refresh_flags(self, crispin_client):
        new_highestmodseq = crispin_client.conn.folder_status(
            self.folder_name, ['HIGHESTMODSEQ'])['HIGHESTMODSEQ']
        # Ensure that we have an initial highestmodseq value stored before we
        # begin polling for changes.
        if self.highestmodseq is None:
            self.highestmodseq = new_highestmodseq

        if new_highestmodseq == self.highestmodseq:
            # Don't need to do anything if the highestmodseq hasn't
            # changed.
            return
        elif new_highestmodseq < self.highestmodseq:
            # This should really never happen, but if it does, handle it.
            log.warning('got server highestmodseq less than saved '
                        'highestmodseq',
                        new_highestmodseq=new_highestmodseq,
                        saved_highestmodseq=self.highestmodseq)
            return

        # Highestmodseq has changed, update accordingly.
        crispin_client.select_folder(self.folder_name, self.uidvalidity_cb)
        changed_flags = crispin_client.condstore_changed_flags(
            self.highestmodseq)
        remote_uids = crispin_client.all_uids()
        with session_scope(self.namespace_id) as db_session:
            common.update_metadata(self.account_id, self.folder_id,
                                   changed_flags, db_session)
            local_uids = common.local_uids(self.account_id, db_session,
                                           self.folder_id)
            expunged_uids = set(local_uids).difference(remote_uids)

        if expunged_uids:
            # If new UIDs have appeared since we last checked in
            # get_new_uids, save them first. We want to always have the
            # latest UIDs before expunging anything, in order to properly
            # capture draft revisions.
            with session_scope(self.namespace_id) as db_session:
                lastseenuid = common.lastseenuid(self.account_id, db_session,
                                                 self.folder_id)
            if remote_uids and lastseenuid < max(remote_uids):
                log.info('Downloading new UIDs before expunging')
                self.get_new_uids(crispin_client)
            with session_scope(self.namespace_id) as db_session:
                common.remove_deleted_uids(self.account_id, self.folder_id,
                                           expunged_uids, db_session)
                db_session.commit()
        self.highestmodseq = new_highestmodseq
Beispiel #24
0
    def initial_sync_impl(self, crispin_client):
        # We wrap the block in a try/finally because the change_poller greenlet
        # needs to be killed when this greenlet is interrupted
        change_poller = None
        try:
            assert crispin_client.selected_folder_name == self.folder_name
            remote_uids = crispin_client.all_uids()
            with self.syncmanager_lock:
                with session_scope(self.namespace_id) as db_session:
                    local_uids = common.local_uids(self.account_id, db_session,
                                                   self.folder_id)
                    common.remove_deleted_uids(
                        self.account_id, self.folder_id,
                        set(local_uids).difference(remote_uids),
                        db_session)

            new_uids = set(remote_uids).difference(local_uids)
            with session_scope(self.namespace_id) as db_session:
                account = db_session.query(Account).get(self.account_id)
                throttled = account.throttled
                self.update_uid_counts(
                    db_session,
                    remote_uid_count=len(remote_uids),
                    # This is the initial size of our download_queue
                    download_uid_count=len(new_uids))

            change_poller = spawn(self.poll_for_changes)
            bind_context(change_poller, 'changepoller', self.account_id,
                         self.folder_id)
            uids = sorted(new_uids, reverse=True)
            count = 0
            for uid in uids:
                # The speedup from batching appears to be less clear for
                # non-Gmail accounts, so for now just download one-at-a-time.
                self.download_and_commit_uids(crispin_client, [uid])
                self.heartbeat_status.publish()
                count += 1
                if throttled and count >= THROTTLE_COUNT:
                    # Throttled accounts' folders sync at a rate of
                    # 1 message/ minute, after the first approx. THROTTLE_COUNT
                    # messages per folder are synced.
                    # Note this is an approx. limit since we use the #(uids),
                    # not the #(messages).
                    sleep(THROTTLE_WAIT)
        finally:
            if change_poller is not None:
                # schedule change_poller to die
                kill(change_poller)
Beispiel #25
0
    def remove_deleted_uids(self, db_session, local_uids, remote_uids):
        """ Remove imapuid entries that no longer exist on the remote.

        Works as follows:
            1. Do a LIST on the current folder to see what messages are on the
                server.
            2. Compare to message uids stored locally.
            3. Purge uids we have locally but not on the server. Ignore
               remote uids that aren't saved locally.

        Make SURE to be holding `syncmanager_lock` when calling this function;
        we do not grab it here to allow callers to lock higher level
        functionality.  """
        to_delete = set(local_uids) - set(remote_uids)
        common.remove_deleted_uids(self.account_id, db_session, to_delete,
                                   self.folder_id)
Beispiel #26
0
    def remove_deleted_uids(self, db_session, local_uids, remote_uids):
        """ Remove imapuid entries that no longer exist on the remote.

        Works as follows:
            1. Do a LIST on the current folder to see what messages are on the
                server.
            2. Compare to message uids stored locally.
            3. Purge uids we have locally but not on the server. Ignore
               remote uids that aren't saved locally.

        Make SURE to be holding `syncmanager_lock` when calling this function;
        we do not grab it here to allow callers to lock higher level
        functionality.  """
        to_delete = set(local_uids) - set(remote_uids)
        common.remove_deleted_uids(self.account_id, db_session, to_delete,
                                   self.folder_id)
def test_messages_deleted_asynchronously(db, default_account, thread, message,
                                         imapuid, folder):
    msg_uid = imapuid.msg_uid
    update_metadata(
        default_account.id,
        folder.id,
        folder.canonical_name,
        {msg_uid: GmailFlags((), ("label", ), None)},
        db.session,
    )
    assert "label" in [cat.display_name for cat in message.categories]
    remove_deleted_uids(default_account.id, folder.id, [msg_uid])
    db.session.expire_all()
    assert abs((message.deleted_at - datetime.utcnow()).total_seconds()) < 2
    # Check that message categories do get updated synchronously.
    assert "label" not in [cat.display_name for cat in message.categories]
Beispiel #28
0
def test_threads_only_deleted_when_no_messages_left(db, default_account,
                                                    default_namespace, message,
                                                    thread, folder, imapuid):
    msg_uid = imapuid.msg_uid
    handler = DeleteHandler(account_id=default_account.id,
                            namespace_id=default_namespace.id,
                            uid_accessor=lambda m: m.imapuids,
                            message_ttl=0)
    # Add another message onto the thread
    add_fake_message(db.session, default_namespace.id, thread)
    remove_deleted_uids(default_account.id, db.session, [msg_uid], folder.id)
    handler.check()
    # Check that the orphaned message was deleted.
    with pytest.raises(ObjectDeletedError):
        message.id
    # Would raise ObjectDeletedError if thread was deleted.
    thread.id
Beispiel #29
0
def test_threads_only_deleted_when_no_messages_left(db, default_account,
                                                    default_namespace, message,
                                                    thread, folder, imapuid):
    msg_uid = imapuid.msg_uid
    handler = DeleteHandler(account_id=default_account.id,
                            namespace_id=default_namespace.id,
                            uid_accessor=lambda m: m.imapuids,
                            message_ttl=0)
    # Add another message onto the thread
    add_fake_message(db.session, default_namespace.id, thread)
    remove_deleted_uids(default_account.id, db.session, [msg_uid], folder.id)
    handler.check()
    # Check that the orphaned message was deleted.
    with pytest.raises(ObjectDeletedError):
        message.id
    # Would raise ObjectDeletedError if thread was deleted.
    thread.id
Beispiel #30
0
    def refresh_flags_impl(self, crispin_client, max_uids):
        crispin_client.select_folder(self.folder_name, self.uidvalidity_cb)

        # Check for any deleted messages.
        remote_uids = crispin_client.all_uids()
        with session_scope(self.namespace_id) as db_session:
            local_uids = common.local_uids(self.account_id, db_session,
                                           self.folder_id)
            expunged_uids = set(local_uids).difference(remote_uids)
        if expunged_uids:
            with self.syncmanager_lock:
                common.remove_deleted_uids(self.account_id, self.folder_id,
                                           expunged_uids)

        # Get recent UIDs to monitor for flag changes.
        with session_scope(self.namespace_id) as db_session:
            local_uids = common.local_uids(
                account_id=self.account_id,
                session=db_session,
                folder_id=self.folder_id,
                limit=max_uids,
            )

        flags = crispin_client.flags(local_uids)
        if max_uids in self.flags_fetch_results and self.flags_fetch_results[
                max_uids] == (local_uids, flags):
            # If the flags fetch response is exactly the same as the last one
            # we got, then we don't need to persist any changes.

            # Stopped logging this to reduce overall logging volume
            # log.debug('Unchanged flags refresh response, '
            #          'not persisting changes', max_uids=max_uids)
            return
        log.debug("Changed flags refresh response, persisting changes",
                  max_uids=max_uids)
        expunged_uids = set(local_uids).difference(flags.keys())
        with self.syncmanager_lock:
            common.remove_deleted_uids(self.account_id, self.folder_id,
                                       expunged_uids)
        with self.syncmanager_lock, session_scope(
                self.namespace_id) as db_session:
            common.update_metadata(self.account_id, self.folder_id,
                                   self.folder_role, flags, db_session)
        self.flags_fetch_results[max_uids] = (local_uids, flags)
def test_deleting_from_a_message_with_multiple_uids(db, default_account,
                                                    message, thread):
    """Check that deleting a imapuid from a message with
    multiple uids doesn't mark the message for deletion."""
    inbox_folder = default_account.inbox_folder
    sent_folder = default_account.sent_folder

    add_fake_imapuid(db.session, default_account.id, message, sent_folder,
                     1337)
    add_fake_imapuid(db.session, default_account.id, message, inbox_folder,
                     2222)

    assert len(message.imapuids) == 2

    remove_deleted_uids(default_account.id, db.session, [2222],
                        inbox_folder.id)

    assert message.deleted_at is None, \
        "The associated message should not have been marked for deletion."

    assert len(message.imapuids) == 1, \
        "The message should have only one imapuid."
Beispiel #32
0
def test_deleting_from_a_message_with_multiple_uids(db, default_account,
                                                    message, thread):
    """Check that deleting a imapuid from a message with
    multiple uids doesn't mark the message for deletion."""
    inbox_folder = default_account.inbox_folder
    sent_folder = default_account.sent_folder

    add_fake_imapuid(db.session, default_account.id, message, sent_folder,
                     1337)
    add_fake_imapuid(db.session, default_account.id, message, inbox_folder,
                     2222)

    assert len(message.imapuids) == 2

    remove_deleted_uids(default_account.id, db.session, [2222],
                        inbox_folder.id)

    assert message.deleted_at is None, \
        "The associated message should not have been marked for deletion."

    assert len(message.imapuids) == 1, \
        "The message should have only one imapuid."
Beispiel #33
0
    def initial_sync_impl(self, crispin_client):
        # We wrap the block in a try/finally because the change_poller greenlet
        # needs to be killed when this greenlet is interrupted
        change_poller = None
        try:
            assert crispin_client.selected_folder_name == self.folder_name
            remote_uids = crispin_client.all_uids()
            with self.syncmanager_lock:
                with session_scope() as db_session:
                    local_uids = common.local_uids(self.account_id, db_session,
                                                   self.folder_id)
                    common.remove_deleted_uids(
                        self.account_id, self.folder_id,
                        set(local_uids).difference(remote_uids),
                        db_session)

            new_uids = set(remote_uids).difference(local_uids)
            with session_scope() as db_session:
                self.update_uid_counts(
                    db_session,
                    remote_uid_count=len(remote_uids),
                    # This is the initial size of our download_queue
                    download_uid_count=len(new_uids))

            change_poller = spawn(self.poll_for_changes)
            bind_context(change_poller, 'changepoller', self.account_id,
                         self.folder_id)
            uids = sorted(new_uids, reverse=True)
            for uid in uids:
                # The speedup from batching appears to be less clear for
                # non-Gmail accounts, so for now just download one-at-a-time.
                self.download_and_commit_uids(crispin_client, [uid])
                self.heartbeat_status.publish()

        finally:
            if change_poller is not None:
                # schedule change_poller to die
                kill(change_poller)
Beispiel #34
0
    def initial_sync_impl(self, crispin_client):
        # We wrap the block in a try/finally because the change_poller greenlet
        # needs to be killed when this greenlet is interrupted
        change_poller = None
        try:
            assert crispin_client.selected_folder_name == self.folder_name
            remote_uids = crispin_client.all_uids()
            with self.syncmanager_lock:
                with session_scope() as db_session:
                    local_uids = common.local_uids(self.account_id, db_session,
                                                   self.folder_id)
                    common.remove_deleted_uids(
                        self.account_id, self.folder_id,
                        set(local_uids).difference(remote_uids), db_session)

            new_uids = set(remote_uids).difference(local_uids)
            with session_scope() as db_session:
                self.update_uid_counts(
                    db_session,
                    remote_uid_count=len(remote_uids),
                    # This is the initial size of our download_queue
                    download_uid_count=len(new_uids))

            change_poller = spawn(self.poll_for_changes)
            bind_context(change_poller, 'changepoller', self.account_id,
                         self.folder_id)
            uids = sorted(new_uids, reverse=True)
            for uid in uids:
                # The speedup from batching appears to be less clear for
                # non-Gmail accounts, so for now just download one-at-a-time.
                self.download_and_commit_uids(crispin_client, [uid])
                self.heartbeat_status.publish()

        finally:
            if change_poller is not None:
                # schedule change_poller to die
                kill(change_poller)
Beispiel #35
0
    def condstore_refresh_flags(self, crispin_client):
        new_highestmodseq = crispin_client.conn.folder_status(
            self.folder_name, ['HIGHESTMODSEQ'])['HIGHESTMODSEQ']
        # Ensure that we have an initial highestmodseq value stored before we
        # begin polling for changes.
        if self.highestmodseq is None:
            self.highestmodseq = new_highestmodseq

        if new_highestmodseq == self.highestmodseq:
            # Don't need to do anything if the highestmodseq hasn't
            # changed.
            return
        elif new_highestmodseq < self.highestmodseq:
            # This should really never happen, but if it does, handle it.
            log.warning('got server highestmodseq less than saved '
                        'highestmodseq',
                        new_highestmodseq=new_highestmodseq,
                        saved_highestmodseq=self.highestmodseq)
            return

        log.info('HIGHESTMODSEQ has changed, getting changed UIDs',
                 new_highestmodseq=new_highestmodseq,
                 saved_highestmodseq=self.highestmodseq)
        crispin_client.select_folder(self.folder_name, self.uidvalidity_cb)
        changed_flags = crispin_client.condstore_changed_flags(
            self.highestmodseq)
        remote_uids = crispin_client.all_uids()

        # In order to be able to sync changes to tens of thousands of flags at
        # once, we commit updates in batches. We do this in ascending order by
        # modseq and periodically "checkpoint" our saved highestmodseq. (It's
        # safe to checkpoint *because* we go in ascending order by modseq.)
        # That way if the process gets restarted halfway through this refresh,
        # we don't have to completely start over. It's also slow to load many
        # objects into the SQLAlchemy session and then issue lots of commits;
        # we avoid that by batching.
        flag_batches = chunk(
            sorted(changed_flags.items(), key=lambda (k, v): v.modseq),
            CONDSTORE_FLAGS_REFRESH_BATCH_SIZE)
        for flag_batch in flag_batches:
            with session_scope(self.namespace_id) as db_session:
                common.update_metadata(self.account_id, self.folder_id,
                                       self.folder_role, dict(flag_batch),
                                       db_session)
            if len(flag_batch) == CONDSTORE_FLAGS_REFRESH_BATCH_SIZE:
                interim_highestmodseq = max(v.modseq for k, v in flag_batch)
                self.highestmodseq = interim_highestmodseq

        with session_scope(self.namespace_id) as db_session:
            local_uids = common.local_uids(self.account_id, db_session,
                                           self.folder_id)
            expunged_uids = set(local_uids).difference(remote_uids)

        if expunged_uids:
            # If new UIDs have appeared since we last checked in
            # get_new_uids, save them first. We want to always have the
            # latest UIDs before expunging anything, in order to properly
            # capture draft revisions.
            with session_scope(self.namespace_id) as db_session:
                lastseenuid = common.lastseenuid(self.account_id, db_session,
                                                 self.folder_id)
            if remote_uids and lastseenuid < max(remote_uids):
                log.info('Downloading new UIDs before expunging')
                self.get_new_uids(crispin_client)
            common.remove_deleted_uids(self.account_id, self.folder_id,
                                       expunged_uids)
        self.highestmodseq = new_highestmodseq