# -*- 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 logging
import os
import time
from twisted.mail import imap4
from twisted.python import log
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
logger = logging.getLogger(__name__)
PROFILE_CMD = os.environ.get('LEAP_PROFILE_IMAPCMD', False)
if PROFILE_CMD:
def _debugProfiling(result, cmdname, start):
took = (time.time() - start) * 1000
log.msg("CMD " + cmdname + " TOOK: " + str(took) + " msec")
return result
#######################################
# Soledad Account
#######################################
# TODO change name to LeapIMAPAccount, since we're using
# the memstore.
# IndexedDB should also not be here anymore.
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, memstore=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.
:type soledad: Soledad
:param memstore: a MemoryStore instance.
:type memstore: MemoryStore
"""
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.
# XXX ??? why is this parsing mailbox name??? it's account...
# userid? homogenize.
self._account_name = self._parse_mailbox_name(account_name)
self._soledad = soledad
self._memstore = memstore
self.__mailboxes = set([])
self.initialize_db()
# every user should have the right to an inbox folder
# at least, so let's make one!
self._load_mailboxes()
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
"""
# XXX use soledadstore instead ...;
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 self.__mailboxes
def _load_mailboxes(self):
self.__mailboxes.update(
[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: %r" % name)
return SoledadMailbox(name, self._soledad,
memstore=self._memstore)
##
## 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(repr(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)
self._load_mailboxes()
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
self._load_mailboxes()
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: SoledadMailbox
"""
if PROFILE_CMD:
start = time.time()
name = self._parse_mailbox_name(name)
if name not in self.mailboxes:
logger.warning("No such mailbox!")
return None
self.selected = name
sm = SoledadMailbox(
name, self._soledad, self._memstore, readwrite)
if PROFILE_CMD:
_debugProfiling(None, "SELECT", start)
return sm
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: %r" % name)
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()
self._load_mailboxes()
# 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(repr(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(repr(new))
for (old, new) in inferiors:
mbox = self._get_mailbox_by_name(old)
mbox.content[self.MBOX_KEY] = new
self._soledad.put_doc(mbox)
self._load_mailboxes()
# 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 %r" % 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