# -*- coding: utf-8 -*-
# mailbox_indexer.py
# Copyright (C) 2014 LEAP
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
"""
.. :py:module::mailbox_indexer
Local tables to store the message Unique Identifiers for a given mailbox.
"""
import re
import uuid
from leap.bitmask.mail.constants import METAMSGID_RE
def _maybe_first_query_item(thing):
"""
Return the first item the returned query result, or None
if empty.
"""
try:
return thing[0][0]
except (TypeError, IndexError):
return None
class WrongMetaDocIDError(Exception):
pass
def sanitize(mailbox_uuid):
return mailbox_uuid.replace("-", "_")
def check_good_uuid(mailbox_uuid):
"""
Check that the passed mailbox identifier is a valid UUID.
:param mailbox_uuid: the uuid to check
:type mailbox_uuid: str
:return: None
:raises: AssertionError if a wrong uuid was passed.
"""
try:
uuid.UUID(str(mailbox_uuid))
except (AttributeError, ValueError):
raise AssertionError(
"the mbox_id is not a valid uuid: %s" % mailbox_uuid)
class MailboxIndexer(object):
"""
This class contains the commands needed to create, modify and alter the
local-only UID tables for a given mailbox.
Its purpouse is to keep a local-only index with the messages in each
mailbox, mainly to satisfy the demands of the IMAP specification, but
useful too for any effective listing of the messages in a mailbox.
Since the incoming mail can be processed at any time in any replica, it's
preferred not to attempt to maintain a global chronological global index.
These indexes are Message Attributes needed for the IMAP specification (rfc
3501), although they can be useful for other non-imap store
implementations.
"""
# The uids are expected to be 32-bits values, but the ROWIDs in sqlite
# are 64-bit values. I *don't* think it really matters for any
# practical use, but it's good to remember we've got that difference going
# on.
store = None
table_preffix = "leapmail_uid_"
def __init__(self, store):
self.store = store
def _query(self, *args, **kw):
assert self.store is not None
return self.store.raw_sqlcipher_query(*args, **kw)
def _operation(self, *args, **kw):
assert self.store is not None
return self.store.raw_sqlcipher_operation(*args, **kw)
def create_table(self, mailbox_uuid):
"""
Create the UID table for a given mailbox.
:param mailbox: the mailbox identifier.
:type mailbox: str
:rtype: Deferred
"""
check_good_uuid(mailbox_uuid)
sql = ("CREATE TABLE if not exists {preffix}{name}( "
"uid INTEGER PRIMARY KEY AUTOINCREMENT, "
"hash TEXT UNIQUE NOT NULL)".format(
preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
return self._operation(sql)
def delete_table(self, mailbox_uuid):
"""
Delete the UID table for a given mailbox.
:param mailbox: the mailbox name
:type mailbox: str
:rtype: Deferred
"""
check_good_uuid(mailbox_uuid)
sql = ("DROP TABLE if exists {preffix}{name}".format(
preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
return self._operation(sql)
def insert_doc(self, mailbox_uuid, doc_id):
"""
Insert the doc_id for a MetaMsg in the UID table for a given mailbox.
The doc_id must be in the format:
M--
:param mailbox: the mailbox name
:type mailbox: str
:param doc_id: the doc_id for the MetaMsg
:type doc_id: str
:return: a deferred that will fire with the uid of the newly inserted
document.
:rtype: Deferred
"""
check_good_uuid(mailbox_uuid)
assert doc_id
mailbox_uuid = mailbox_uuid.replace('-', '_')
if not re.findall(METAMSGID_RE.format(mbox_uuid=mailbox_uuid), doc_id):
raise WrongMetaDocIDError("Wrong format for the MetaMsg doc_id")
def get_rowid(result):
return _maybe_first_query_item(result)
sql = ("INSERT INTO {preffix}{name} VALUES ("
"NULL, ?)".format(
preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
values = (doc_id,)
sql_last = ("SELECT MAX(rowid) FROM {preffix}{name} "
"LIMIT 1;").format(
preffix=self.table_preffix, name=sanitize(mailbox_uuid))
d = self._operation(sql, values)
d.addCallback(lambda _: self._query(sql_last))
d.addCallback(get_rowid)
d.addErrback(lambda f: f.printTraceback())
return d
def delete_doc_by_uid(self, mailbox_uuid, uid):
"""
Delete the entry for a MetaMsg in the UID table for a given mailbox.
:param mailbox_uuid: the mailbox uuid
:type mailbox: str
:param uid: the UID of the message.
:type uid: int
:rtype: Deferred
"""
check_good_uuid(mailbox_uuid)
assert uid
sql = ("DELETE FROM {preffix}{name} "
"WHERE uid=?".format(
preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
values = (uid,)
return self._query(sql, values)
def delete_doc_by_hash(self, mailbox_uuid, doc_id):
"""
Delete the entry for a MetaMsg in the UID table for a given mailbox.
The doc_id must be in the format:
M--
:param mailbox_uuid: the mailbox uuid
:type mailbox: str
:param doc_id: the doc_id for the MetaMsg
:type doc_id: str
:return: a deferred that will fire when the deletion has succed.
:rtype: Deferred
"""
check_good_uuid(mailbox_uuid)
assert doc_id
sql = ("DELETE FROM {preffix}{name} "
"WHERE hash=?".format(
preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
values = (doc_id,)
return self._query(sql, values)
def get_doc_id_from_uid(self, mailbox_uuid, uid):
"""
Get the doc_id for a MetaMsg in the UID table for a given mailbox.
:param mailbox_uuid: the mailbox uuid
:type mailbox: str
:param uid: the uid for the MetaMsg for this mailbox
:type uid: int
:rtype: Deferred
"""
check_good_uuid(mailbox_uuid)
mailbox_uuid = mailbox_uuid.replace('-', '_')
def get_hash(result):
return _maybe_first_query_item(result)
sql = ("SELECT hash from {preffix}{name} "
"WHERE uid=?".format(
preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
values = (uid,)
d = self._query(sql, values)
d.addCallback(get_hash)
return d
def get_uid_from_doc_id(self, mailbox_uuid, doc_id):
check_good_uuid(mailbox_uuid)
mailbox_uuid = mailbox_uuid.replace('-', '_')
def get_uid(result):
return _maybe_first_query_item(result)
sql = ("SELECT uid from {preffix}{name} "
"WHERE hash=?".format(
preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
values = (doc_id,)
d = self._query(sql, values)
d.addCallback(get_uid)
return d
def get_doc_ids_from_uids(self, mailbox_uuid, uids):
# For IMAP relative numbering /sequences.
# XXX dereference the range (n,*)
raise NotImplementedError()
def count(self, mailbox_uuid):
"""
Get the number of entries in the UID table for a given mailbox.
:param mailbox_uuid: the mailbox uuid
:type mailbox_uuid: str
:return: a deferred that will fire with an integer returning the count.
:rtype: Deferred
"""
check_good_uuid(mailbox_uuid)
def get_count(result):
return _maybe_first_query_item(result)
sql = ("SELECT Count(*) FROM {preffix}{name};".format(
preffix=self.table_preffix, name=sanitize(mailbox_uuid)))
d = self._query(sql)
d.addCallback(get_count)
d.addErrback(lambda _: 0)
return d
def get_next_uid(self, mailbox_uuid):
"""
Get the next integer beyond the highest UID count for a given mailbox.
This is expected by the IMAP implementation. There are no guarantees
that a document to be inserted in the future gets the returned UID: the
only thing that can be assured is that it will be equal or greater than
the value returned.
:param mailbox_uuid: the mailbox uuid
:type mailbox: str
:return: a deferred that will fire with an integer returning the next
uid.
:rtype: Deferred
"""
check_good_uuid(mailbox_uuid)
d = self.get_last_uid(mailbox_uuid)
d.addCallback(lambda uid: uid + 1)
return d
def get_last_uid(self, mailbox_uuid):
"""
Get the highest UID for a given mailbox.
"""
check_good_uuid(mailbox_uuid)
sql = ("SELECT MAX(rowid) FROM {preffix}{name} "
"LIMIT 1;").format(
preffix=self.table_preffix, name=sanitize(mailbox_uuid))
def getit(result):
rowid = _maybe_first_query_item(result)
if not rowid:
rowid = 0
return rowid
d = self._query(sql)
d.addCallback(getit)
return d
def all_uid_iter(self, mailbox_uuid):
"""
Get a sequence of all the uids in this mailbox.
:param mailbox_uuid: the mailbox uuid
:type mailbox_uuid: str
"""
check_good_uuid(mailbox_uuid)
sql = ("SELECT uid from {preffix}{name} ").format(
preffix=self.table_preffix, name=sanitize(mailbox_uuid))
def get_results(result):
return [x[0] for x in result]
d = self._query(sql)
d.addCallback(get_results)
return d