# -*- coding: utf-8 -*- # account.py # Copyright (C) 2013 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 . """ Soledad Backed Account. """ import copy import time from twisted.mail import imap4 from zope.interface import implements from leap.common.check import leap_assert, leap_assert_type from leap.mail.imap.index import IndexedDB from leap.mail.imap.fields import WithMsgFields from leap.mail.imap.parser import MBoxParser from leap.mail.imap.mailbox import SoledadMailbox from leap.soledad.client import Soledad ####################################### # Soledad Account ####################################### class SoledadBackedAccount(WithMsgFields, IndexedDB, MBoxParser): """ An implementation of IAccount and INamespacePresenteer that is backed by Soledad Encrypted Documents. """ implements(imap4.IAccount, imap4.INamespacePresenter) _soledad = None selected = None closed = False def __init__(self, account_name, soledad=None): """ Creates a SoledadAccountIndex that keeps track of the mailboxes and subscriptions handled by this account. :param acct_name: The name of the account (user id). :type acct_name: str :param soledad: a Soledad instance. :param soledad: Soledad """ leap_assert(soledad, "Need a soledad instance to initialize") leap_assert_type(soledad, Soledad) # XXX SHOULD assert too that the name matches the user/uuid with which # soledad has been initialized. self._account_name = self._parse_mailbox_name(account_name) self._soledad = soledad self.initialize_db() # every user should have the right to an inbox folder # at least, so let's make one! if not self.mailboxes: self.addMailbox(self.INBOX_NAME) def _get_empty_mailbox(self): """ Returns an empty mailbox. :rtype: dict """ return copy.deepcopy(self.EMPTY_MBOX) def _get_mailbox_by_name(self, name): """ Return an mbox document by name. :param name: the name of the mailbox :type name: str :rtype: SoledadDocument """ doc = self._soledad.get_from_index( self.TYPE_MBOX_IDX, self.MBOX_KEY, self._parse_mailbox_name(name)) return doc[0] if doc else None @property def mailboxes(self): """ A list of the current mailboxes for this account. """ return [doc.content[self.MBOX_KEY] for doc in self._soledad.get_from_index( self.TYPE_IDX, self.MBOX_KEY)] @property def subscriptions(self): """ A list of the current subscriptions for this account. """ return [doc.content[self.MBOX_KEY] for doc in self._soledad.get_from_index( self.TYPE_SUBS_IDX, self.MBOX_KEY, '1')] def getMailbox(self, name): """ Returns a Mailbox with that name, without selecting it. :param name: name of the mailbox :type name: str :returns: a a SoledadMailbox instance :rtype: SoledadMailbox """ name = self._parse_mailbox_name(name) if name not in self.mailboxes: raise imap4.MailboxException("No such mailbox") return SoledadMailbox(name, soledad=self._soledad) ## ## IAccount ## def addMailbox(self, name, creation_ts=None): """ Add a mailbox to the account. :param name: the name of the mailbox :type name: str :param creation_ts: an optional creation timestamp to be used as mailbox id. A timestamp will be used if no one is provided. :type creation_ts: int :returns: True if successful :rtype: bool """ name = self._parse_mailbox_name(name) if name in self.mailboxes: raise imap4.MailboxCollision, name if not creation_ts: # by default, we pass an int value # taken from the current time # we make sure to take enough decimals to get a unique # mailbox-uidvalidity. creation_ts = int(time.time() * 10E2) mbox = self._get_empty_mailbox() mbox[self.MBOX_KEY] = name mbox[self.CREATED_KEY] = creation_ts doc = self._soledad.create_doc(mbox) return bool(doc) def create(self, pathspec): """ Create a new mailbox from the given hierarchical name. :param pathspec: The full hierarchical name of a new mailbox to create. If any of the inferior hierarchical names to this one do not exist, they are created as well. :type pathspec: str :return: A true value if the creation succeeds. :rtype: bool :raise MailboxException: Raised if this mailbox cannot be added. """ # TODO raise MailboxException paths = filter( None, self._parse_mailbox_name(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 select(self, name, readwrite=1): """ Selects a mailbox. :param name: the mailbox to select :type name: str :param readwrite: 1 for readwrite permissions. :type readwrite: int :rtype: bool """ name = self._parse_mailbox_name(name) if name not in self.mailboxes: return None self.selected = name return SoledadMailbox( name, rw=readwrite, soledad=self._soledad) def delete(self, name, force=False): """ Deletes a mailbox. Right now it does not purge the messages, but just removes the mailbox name from the mailboxes list!!! :param name: the mailbox to be deleted :type name: str :param force: if True, it will not check for noselect flag or inferior names. use with care. :type force: bool """ name = self._parse_mailbox_name(name) if not name in self.mailboxes: raise imap4.MailboxException("No such mailbox") mbox = self.getMailbox(name) if force is False: # See if this box is flagged \Noselect # XXX use mbox.flags instead? if self.NOSELECT_FLAG in mbox.getFlags(): # Check for hierarchically inferior mailboxes with this one # as part of their root. for others in self.mailboxes: if others != name and others.startswith(name): raise imap4.MailboxException, ( "Hierarchically inferior mailboxes " "exist and \\Noselect is set") mbox.destroy() # XXX FIXME --- not honoring the inferior names... # if there are no hierarchically inferior names, we will # delete it from our ken. #if self._inferiorNames(name) > 1: # ??! -- can this be rite? #self._index.removeMailbox(name) def rename(self, oldname, newname): """ Renames a mailbox. :param oldname: old name of the mailbox :type oldname: str :param newname: new name of the mailbox :type newname: str """ oldname = self._parse_mailbox_name(oldname) newname = self._parse_mailbox_name(newname) 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: mbox = self._get_mailbox_by_name(old) mbox.content[self.MBOX_KEY] = new self._soledad.put_doc(mbox) # XXX ---- FIXME!!!! ------------------------------------ # until here we just renamed the index... # We have to rename also the occurrence of this # mailbox on ALL the messages that are contained in it!!! # ... we maybe could use a reference to the doc_id # in each msg, instead of the "mbox" field in msgs # ------------------------------------------------------- def _inferiorNames(self, name): """ Return hierarchically inferior mailboxes. :param name: name of the mailbox :rtype: list """ # XXX use wildcard query instead inferiors = [] for infname in self.mailboxes: if infname.startswith(name): inferiors.append(infname) return inferiors def isSubscribed(self, name): """ Returns True if user is subscribed to this mailbox. :param name: the mailbox to be checked. :type name: str :rtype: bool """ mbox = self._get_mailbox_by_name(name) return mbox.content.get('subscribed', False) def _set_subscription(self, name, value): """ Sets the subscription value for a given mailbox :param name: the mailbox :type name: str :param value: the boolean value :type value: bool """ # maybe we should store subscriptions in another # document... if not name in self.mailboxes: self.addMailbox(name) mbox = self._get_mailbox_by_name(name) if mbox: mbox.content[self.SUBSCRIBED_KEY] = value self._soledad.put_doc(mbox) def subscribe(self, name): """ Subscribe to this mailbox :param name: name of the mailbox :type name: str """ name = self._parse_mailbox_name(name) if name not in self.subscriptions: self._set_subscription(name, True) def unsubscribe(self, name): """ Unsubscribe from this mailbox :param name: name of the mailbox :type name: str """ name = self._parse_mailbox_name(name) if name not in self.subscriptions: raise imap4.MailboxException, "Not currently subscribed to " + name self._set_subscription(name, False) def listMailboxes(self, ref, wildcard): """ List the mailboxes. from rfc 3501: returns a subset of names from the complete set of all names available to the client. Zero or more untagged LIST replies are returned, containing the name attributes, hierarchy delimiter, and name. :param ref: reference name :type ref: str :param wildcard: mailbox name with possible wildcards :type wildcard: str """ # XXX use wildcard in index query ref = self._inferiorNames( self._parse_mailbox_name(ref)) wildcard = imap4.wildcardToRegexp(wildcard, '/') return [(i, self.getMailbox(i)) for i in ref if wildcard.match(i)] ## ## INamespacePresenter ## def getPersonalNamespaces(self): return [["", "/"]] def getSharedNamespaces(self): return None def getOtherNamespaces(self): return None # extra, for convenience def deleteAllMessages(self, iknowhatiamdoing=False): """ Deletes all messages from all mailboxes. Danger! high voltage! :param iknowhatiamdoing: confirmation parameter, needs to be True to proceed. """ if iknowhatiamdoing is True: for mbox in self.mailboxes: self.delete(mbox, force=True) def __repr__(self): """ Representation string for this object. """ return "" % self._account_name