summaryrefslogtreecommitdiff
path: root/src/leap/mail/imap/server.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/mail/imap/server.py')
-rw-r--r--src/leap/mail/imap/server.py558
1 files changed, 558 insertions, 0 deletions
diff --git a/src/leap/mail/imap/server.py b/src/leap/mail/imap/server.py
new file mode 100644
index 0000000..4e9c22c
--- /dev/null
+++ b/src/leap/mail/imap/server.py
@@ -0,0 +1,558 @@
+import copy
+
+from zope.interface import implements
+
+from twisted.mail import imap4
+from twisted.internet import defer
+
+#from twisted import cred
+
+import u1db
+
+
+# TODO delete this SimpleMailbox
+class SimpleMailbox:
+ """
+ A simple Mailbox for reference
+ We don't intend to use this, only for debugging purposes
+ until we stabilize unittests with SoledadMailbox
+ """
+ implements(imap4.IMailboxInfo, imap4.IMailbox, imap4.ICloseableMailbox)
+
+ flags = ('\\Flag1', 'Flag2', '\\AnotherSysFlag', 'LastFlag')
+ messages = []
+ mUID = 0
+ rw = 1
+ closed = False
+
+ def __init__(self):
+ self.listeners = []
+ self.addListener = self.listeners.append
+ self.removeListener = self.listeners.remove
+
+ def getFlags(self):
+ return self.flags
+
+ def getUIDValidity(self):
+ return 42
+
+ def getUIDNext(self):
+ return len(self.messages) + 1
+
+ def getMessageCount(self):
+ return 9
+
+ def getRecentCount(self):
+ return 3
+
+ def getUnseenCount(self):
+ return 4
+
+ def isWriteable(self):
+ return self.rw
+
+ def destroy(self):
+ pass
+
+ def getHierarchicalDelimiter(self):
+ return '/'
+
+ def requestStatus(self, names):
+ r = {}
+ if 'MESSAGES' in names:
+ r['MESSAGES'] = self.getMessageCount()
+ if 'RECENT' in names:
+ r['RECENT'] = self.getRecentCount()
+ if 'UIDNEXT' in names:
+ r['UIDNEXT'] = self.getMessageCount() + 1
+ if 'UIDVALIDITY' in names:
+ r['UIDVALIDITY'] = self.getUID()
+ if 'UNSEEN' in names:
+ r['UNSEEN'] = self.getUnseenCount()
+ return defer.succeed(r)
+
+ def addMessage(self, message, flags, date=None):
+ self.messages.append((message, flags, date, self.mUID))
+ self.mUID += 1
+ return defer.succeed(None)
+
+ def expunge(self):
+ delete = []
+ for i in self.messages:
+ if '\\Deleted' in i[1]:
+ delete.append(i)
+ for i in delete:
+ self.messages.remove(i)
+ return [i[3] for i in delete]
+
+ def close(self):
+ self.closed = True
+
+
+###################################
+# SoledadAccount Index
+###################################
+
+class MissingIndexError(Exception):
+ """raises when tried to access a non existent index document"""
+
+
+class BadIndexError(Exception):
+ """raises when index is malformed or has the wrong cardinality"""
+
+
+EMPTY_INDEXDOC = {"is_index": True, "mailboxes": [], "subscriptions": []}
+get_empty_indexdoc = lambda: copy.deepcopy(EMPTY_INDEXDOC)
+
+
+class SoledadAccountIndex(object):
+ """
+ Index for the Soledad Account
+ keeps track of mailboxes and subscriptions
+ """
+ _index = None
+
+ def __init__(self, soledad=None):
+ self._soledad = soledad
+ self._db = soledad._db
+ self._initialize_db()
+
+ def _initialize_db(self):
+ """initialize the database"""
+ db_indexes = dict(self._soledad._db.list_indexes())
+ name, expression = "isindex", ["bool(is_index)"]
+ if name not in db_indexes:
+ self._soledad._db.create_index(name, *expression)
+ try:
+ self._index = self._get_index_doc()
+ except MissingIndexError:
+ print "no index!!! creating..."
+ self._create_index_doc()
+
+ def _create_index_doc(self):
+ """creates an empty index document"""
+ indexdoc = get_empty_indexdoc()
+ self._index = self._soledad.create_doc(
+ indexdoc)
+
+ def _get_index_doc(self):
+ """gets index document"""
+ indexdoc = self._db.get_from_index("isindex", "*")
+ if not indexdoc:
+ raise MissingIndexError
+ if len(indexdoc) > 1:
+ raise BadIndexError
+ return indexdoc[0]
+
+ def _update_index_doc(self):
+ """updates index document"""
+ self._db.put_doc(self._index)
+
+ # setters and getters for the index document
+
+ def _get_mailboxes(self):
+ """Get mailboxes associated with this account."""
+ return self._index.content.setdefault('mailboxes', [])
+
+ def _set_mailboxes(self, mailboxes):
+ """Set mailboxes associated with this account."""
+ self._index.content['mailboxes'] = list(set(mailboxes))
+ self._update_index_doc()
+
+ mailboxes = property(
+ _get_mailboxes, _set_mailboxes, doc="Account mailboxes.")
+
+ def _get_subscriptions(self):
+ """Get subscriptions associated with this account."""
+ return self._index.content.setdefault('subscriptions', [])
+
+ def _set_subscriptions(self, subscriptions):
+ """Set subscriptions associated with this account."""
+ self._index.content['subscriptions'] = list(set(subscriptions))
+ self._update_index_doc()
+
+ subscriptions = property(
+ _get_subscriptions, _set_subscriptions, doc="Account subscriptions.")
+
+ def addMailbox(self, name):
+ """add a mailbox to the mailboxes list."""
+ name = name.upper()
+ self.mailboxes.append(name)
+ self._update_index_doc()
+
+ def removeMailbox(self, name):
+ """remove a mailbox from the mailboxes list."""
+ self.mailboxes.remove(name)
+ self._update_index_doc()
+
+ def addSubscription(self, name):
+ """add a subscription to the subscriptions list."""
+ name = name.upper()
+ self.subscriptions.append(name)
+ self._update_index_doc()
+
+ def removeSubscription(self, name):
+ """remove a subscription from the subscriptions list."""
+ self.subscriptions.remove(name)
+ self._update_index_doc()
+
+
+#######################################
+# Soledad Account
+#######################################
+
+class SoledadBackedAccount(object):
+
+ implements(imap4.IAccount, imap4.INamespacePresenter)
+
+ #mailboxes = None
+ #subscriptions = None
+
+ top_id = 0 # XXX move top_id to _index
+ _soledad = None
+ _db = None
+
+ def __init__(self, name, soledad=None):
+ self.name = name
+ self._soledad = soledad
+ self._db = soledad._db
+ self._index = SoledadAccountIndex(soledad=soledad)
+
+ #self.mailboxes = {}
+ #self.subscriptions = []
+
+ def allocateID(self):
+ id = self.top_id # XXX move to index !!!
+ self.top_id += 1
+ return id
+
+ @property
+ def mailboxes(self):
+ return self._index.mailboxes
+
+ @property
+ def subscriptions(self):
+ return self._index.subscriptions
+
+ ##
+ ## IAccount
+ ##
+
+ def addMailbox(self, name, mbox=None):
+ name = name.upper()
+ if name in self.mailboxes:
+ raise imap4.MailboxCollision, name
+ if mbox is None:
+ mbox = self._emptyMailbox(name, self.allocateID())
+ self._index.addMailbox(name)
+ return 1
+
+ def create(self, pathspec):
+ paths = filter(None, pathspec.split('/'))
+ for accum in range(1, len(paths)):
+ try:
+ self.addMailbox('/'.join(paths[:accum]))
+ except imap4.MailboxCollision:
+ pass
+ try:
+ self.addMailbox('/'.join(paths))
+ except imap4.MailboxCollision:
+ if not pathspec.endswith('/'):
+ return False
+ return True
+
+ def _emptyMailbox(self, name, id):
+ # XXX implement!!!
+ raise NotImplementedError
+
+ def select(self, name, readwrite=1):
+ return self.mailboxes.get(name.upper())
+
+ def delete(self, name):
+ name = name.upper()
+ # See if this mailbox exists at all
+ mbox = self.mailboxes.get(name)
+ if not mbox:
+ raise imap4.MailboxException("No such mailbox")
+ # See if this box is flagged \Noselect
+ if r'\Noselect' in mbox.getFlags():
+ # Check for hierarchically inferior mailboxes with this one
+ # as part of their root.
+ for others in self.mailboxes.keys():
+ if others != name and others.startswith(name):
+ raise imap4.MailboxException, (
+ "Hierarchically inferior mailboxes "
+ "exist and \\Noselect is set")
+ mbox.destroy()
+
+ # iff there are no hierarchically inferior names, we will
+ # delete it from our ken.
+ if self._inferiorNames(name) > 1:
+ del self.mailboxes[name]
+
+ def rename(self, oldname, newname):
+ oldname = oldname.upper()
+ newname = newname.upper()
+ if oldname not in self.mailboxes:
+ raise imap4.NoSuchMailbox, oldname
+
+ inferiors = self._inferiorNames(oldname)
+ inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors]
+
+ for (old, new) in inferiors:
+ if new in self.mailboxes:
+ raise imap4.MailboxCollision, new
+
+ for (old, new) in inferiors:
+ self.mailboxes[new] = self.mailboxes[old]
+ del self.mailboxes[old]
+
+ def _inferiorNames(self, name):
+ inferiors = []
+ for infname in self.mailboxes.keys():
+ if infname.startswith(name):
+ inferiors.append(infname)
+ return inferiors
+
+ def isSubscribed(self, name):
+ return name.upper() in self.subscriptions
+
+ def subscribe(self, name):
+ name = name.upper()
+ if name not in self.subscriptions:
+ self._index.addSubscription(name)
+
+ def unsubscribe(self, name):
+ name = name.upper()
+ if name not in self.subscriptions:
+ raise imap4.MailboxException, "Not currently subscribed to " + name
+ self._index.removeSubscription(name)
+
+ def listMailboxes(self, ref, wildcard):
+ ref = self._inferiorNames(ref.upper())
+ wildcard = imap4.wildcardToRegexp(wildcard, '/')
+ return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)]
+
+ ##
+ ## INamespacePresenter
+ ##
+
+ def getPersonalNamespaces(self):
+ return [["", "/"]]
+
+ def getSharedNamespaces(self):
+ return None
+
+ def getOtherNamespaces(self):
+ return None
+
+#######################################
+# Soledad Message, MessageCollection
+# and Mailbox
+#######################################
+
+FLAGS_INDEX = 'flags'
+SEEN_INDEX = 'seen'
+INDEXES = {FLAGS_INDEX: ['flags'],
+ SEEN_INDEX: ['bool(seen)'],
+}
+
+
+class Message(u1db.Document):
+ """A rfc822 message item."""
+ # XXX TODO use email module
+
+ def _get_subject(self):
+ """Get the message title."""
+ return self.content.get('subject')
+
+ def _set_subject(self, subject):
+ """Set the message title."""
+ self.content['subject'] = subject
+
+ subject = property(_get_subject, _set_subject,
+ doc="Subject of the message.")
+
+ def _get_seen(self):
+ """Get the seen status of the message."""
+ return self.content.get('seen', False)
+
+ def _set_seen(self, value):
+ """Set the seen status."""
+ self.content['seen'] = value
+
+ seen = property(_get_seen, _set_seen, doc="Seen flag.")
+
+ def _get_flags(self):
+ """Get flags associated with the message."""
+ return self.content.setdefault('flags', [])
+
+ def _set_flags(self, flags):
+ """Set flags associated with the message."""
+ self.content['flags'] = list(set(flags))
+
+ flags = property(_get_flags, _set_flags, doc="Message flags.")
+
+EMPTY_MSG = {
+ "subject": "",
+ "seen": False,
+ "flags": [],
+ "mailbox": "",
+}
+get_empty_msg = lambda: copy.deepcopy(EMPTY_MSG)
+
+
+class MessageCollection(object):
+ """
+ A collection of messages
+ """
+
+ def __init__(self, mbox=None, db=None):
+ assert mbox
+ self.db = db
+ self.initialize_db()
+
+ def initialize_db(self):
+ """Initialize the database."""
+ # Ask the database for currently existing indexes.
+ db_indexes = dict(self.db.list_indexes())
+ # Loop through the indexes we expect to find.
+ for name, expression in INDEXES.items():
+ print 'name is', name
+ if name not in db_indexes:
+ # The index does not yet exist.
+ print 'creating index'
+ self.db.create_index(name, *expression)
+ continue
+
+ if expression == db_indexes[name]:
+ print 'expression up to date'
+ # The index exists and is up to date.
+ continue
+ # The index exists but the definition is not what expected, so we
+ # delete it and add the proper index expression.
+ print 'deleting index'
+ self.db.delete_index(name)
+ self.db.create_index(name, *expression)
+
+ def add_msg(self, subject=None, flags=None):
+ """Create a new message document."""
+ if flags is None:
+ flags = []
+ content = get_empty_msg()
+ if subject or flags:
+ content['subject'] = subject
+ content['flags'] = flags
+ # Store the document in the database. Since we did not set a document
+ # id, the database will store it as a new document, and generate
+ # a valid id.
+ return self.db.create_doc(content)
+
+ def get_all(self):
+ """Get all messages"""
+ return self.db.get_from_index(SEEN_INDEX, "*")
+
+ def get_unseen(self):
+ """Get only unseen messages"""
+ return self.db.get_from_index(SEEN_INDEX, "0")
+
+ def count(self):
+ return len(self.get_all())
+
+
+class SoledadMailbox:
+ """
+ A Soledad-backed IMAP mailbox
+ """
+
+ implements(imap4.IMailboxInfo, imap4.IMailbox, imap4.ICloseableMailbox)
+
+ flags = ('\\Seen', '\\Answered', '\\Flagged',
+ '\\Deleted', '\\Draft', '\\Recent', 'List')
+
+ #messages = []
+ messages = None
+ mUID = 0
+ rw = 1
+ closed = False
+
+ def __init__(self, mbox, soledad=None):
+ # XXX sanity check:
+ #soledad is not None and isinstance(SQLCipherDatabase, soldad._db)
+ self.listeners = []
+ self.addListener = self.listeners.append
+ self.removeListener = self.listeners.remove
+ self._soledad = soledad
+ if soledad:
+ self.messages = MessageCollection(
+ mbox=mbox, db=soledad._db)
+
+ def getFlags(self):
+ return self.messages.db.get_index_keys(FLAGS_INDEX)
+
+ def getUIDValidity(self):
+ return 42
+
+ def getUIDNext(self):
+ return self.messages.count() + 1
+
+ def getMessageCount(self):
+ return self.messages.count()
+
+ def getUnseenCount(self):
+ return len(self.messages.get_unseen())
+
+ def getRecentCount(self):
+ # XXX
+ return 3
+
+ def isWriteable(self):
+ return self.rw
+
+ def destroy(self):
+ pass
+
+ def getHierarchicalDelimiter(self):
+ return '/'
+
+ def requestStatus(self, names):
+ r = {}
+ if 'MESSAGES' in names:
+ r['MESSAGES'] = self.getMessageCount()
+ if 'RECENT' in names:
+ r['RECENT'] = self.getRecentCount()
+ if 'UIDNEXT' in names:
+ r['UIDNEXT'] = self.getMessageCount() + 1
+ if 'UIDVALIDITY' in names:
+ r['UIDVALIDITY'] = self.getUID()
+ if 'UNSEEN' in names:
+ r['UNSEEN'] = self.getUnseenCount()
+ return defer.succeed(r)
+
+ def addMessage(self, message, flags, date=None):
+ # self.messages.add_msg((msg, flags, date, self.mUID))
+ #self.messages.append((message, flags, date, self.mUID))
+ # XXX CHANGE-ME
+ self.messages.add_msg(subject=message, flags=flags, date=date)
+ self.mUID += 1
+ return defer.succeed(None)
+
+ def deleteAllDocs(self):
+ """deletes all docs"""
+ docs = self.messages.db.get_all_docs()[1]
+ for doc in docs:
+ self.messages.db.delete_doc(doc)
+
+ def expunge(self):
+ """deletes all messages flagged \\Deleted"""
+ # XXX FIXME!
+ delete = []
+ for i in self.messages:
+ if '\\Deleted' in i[1]:
+ delete.append(i)
+ for i in delete:
+ self.messages.remove(i)
+ return [i[3] for i in delete]
+
+ def close(self):
+ self.closed = True