diff options
Diffstat (limited to 'mail/src/leap')
83 files changed, 0 insertions, 17339 deletions
diff --git a/mail/src/leap/__init__.py b/mail/src/leap/__init__.py deleted file mode 100644 index f48ad10..0000000 --- a/mail/src/leap/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# See http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages -try: - __import__('pkg_resources').declare_namespace(__name__) -except ImportError: - from pkgutil import extend_path - __path__ = extend_path(__path__, __name__) diff --git a/mail/src/leap/mail/__init__.py b/mail/src/leap/mail/__init__.py deleted file mode 100644 index 4b25fe6..0000000 --- a/mail/src/leap/mail/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# -*- coding: utf-8 -*- -# __init__.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 <http://www.gnu.org/licenses/>. - - -""" -Client mail bits. -""" - - -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions diff --git a/mail/src/leap/mail/_version.py b/mail/src/leap/mail/_version.py deleted file mode 100644 index 954f488..0000000 --- a/mail/src/leap/mail/_version.py +++ /dev/null @@ -1,484 +0,0 @@ - -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.16 (https://github.com/warner/python-versioneer) - -"""Git implementation of _version.py.""" - -import errno -import os -import re -import subprocess -import sys - - -def get_keywords(): - """Get the keywords needed to look up the version information.""" - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "$Format:%d$" - git_full = "$Format:%H$" - keywords = {"refnames": git_refnames, "full": git_full} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_config(): - """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "pep440" - cfg.tag_prefix = "" - cfg.parentdir_prefix = "None" - cfg.versionfile_source = "src/leap/mail/_version.py" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False): - """Call the given command(s).""" - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - return None - return stdout - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes - both the project name and a version string. - """ - dirname = os.path.basename(root) - if not dirname.startswith(parentdir_prefix): - if verbose: - print("guessing rootdir is '%s', but '%s' doesn't start with " - "prefix '%s'" % (root, dirname, parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None} - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%s', no digits" % ",".join(refs-tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None - } - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags"} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - if not os.path.exists(os.path.join(root, ".git")): - if verbose: - print("no .git in %s" % root) - raise NotThisMethod("no .git directory") - - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - return pieces - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post.devDISTANCE - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Eexceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"]} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None} - - -def get_versions(): - """Get version information or return default if unable to do so.""" - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree"} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version"} diff --git a/mail/src/leap/mail/adaptors/__init__.py b/mail/src/leap/mail/adaptors/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/mail/src/leap/mail/adaptors/__init__.py +++ /dev/null diff --git a/mail/src/leap/mail/adaptors/models.py b/mail/src/leap/mail/adaptors/models.py deleted file mode 100644 index 49460f7..0000000 --- a/mail/src/leap/mail/adaptors/models.py +++ /dev/null @@ -1,123 +0,0 @@ -# -*- coding: utf-8 -*- -# models.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 <http://www.gnu.org/licenses/>. -""" -Generic Models to be used by the Document Adaptors. -""" -import copy - - -class SerializableModel(object): - """ - A Generic document model, that can be serialized into a dictionary. - - Subclasses of this `SerializableModel` are meant to be added as class - attributes of classes inheriting from DocumentWrapper. - - A subclass __meta__ of this SerializableModel might exist, and contain info - relative to particularities of this model. - - For instance, the use of `__meta__.index` marks the existence of a primary - index in the model, which will be used to do unique queries (in which case - all the other indexed fields in the underlying document will be filled with - the default info contained in the model definition). - """ - - @classmethod - def serialize(klass): - """ - Get a dictionary representation of the public attributes in the model - class. To avoid collisions with builtin functions, any occurrence of an - attribute ended in '_' (like 'type_') will be normalized by removing - the trailing underscore. - - This classmethod is used from within the serialized method of a - DocumentWrapper instance: it provides defaults for the - empty document. - """ - assert isinstance(klass, type) - return _normalize_dict(klass.__dict__) - - -class DocumentWrapper(object): - """ - A Wrapper object that can be manipulated, passed around, and serialized in - a format that the store understands. - It is related to a SerializableModel, which must be specified as the - ``model`` class attribute. The instance of this DocumentWrapper will not - allow any other *public* attributes than those defined in the corresponding - model. - """ - # TODO we could do some very basic type checking here - # TODO set a dirty flag (on __setattr__, whenever the value is != from - # before) - # TODO we could enforce the existence of a correct "model" attribute - # in some other way (other than in the initializer) - - def __init__(self, **kwargs): - if not getattr(self, 'model', None): - raise RuntimeError( - 'DocumentWrapper class needs a model attribute') - - defaults = self.model.serialize() - - if kwargs: - values = copy.deepcopy(defaults) - values.update(_normalize_dict(kwargs)) - else: - values = defaults - - for k, v in values.items(): - k = k.replace('-', '_') - setattr(self, k, v) - - def __setattr__(self, attr, value): - normalized = _normalize_dict(self.model.__dict__) - if not attr.startswith('_') and attr not in normalized: - raise RuntimeError( - "Cannot set attribute because it's not defined " - "in the model %s: %s" % (self.__class__, attr)) - object.__setattr__(self, attr, value) - - def serialize(self): - return _normalize_dict(self.__dict__) - - def create(self): - raise NotImplementedError() - - def update(self): - raise NotImplementedError() - - def delete(self): - raise NotImplementedError() - - @classmethod - def get_or_create(self): - raise NotImplementedError() - - @classmethod - def get_all(self): - raise NotImplementedError() - - -def _normalize_dict(_dict): - items = _dict.items() - items = filter(lambda (k, v): not callable(v), items) - items = filter(lambda (k, v): not k.startswith('_'), items) - items = [(k, v) if not k.endswith('_') else (k[:-1], v) - for (k, v) in items] - items = [(k.replace('-', '_'), v) for (k, v) in items] - return dict(items) diff --git a/mail/src/leap/mail/adaptors/soledad.py b/mail/src/leap/mail/adaptors/soledad.py deleted file mode 100644 index ca8f741..0000000 --- a/mail/src/leap/mail/adaptors/soledad.py +++ /dev/null @@ -1,1268 +0,0 @@ -# soledad.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 <http://www.gnu.org/licenses/>. -""" -Soledadad MailAdaptor module. -""" -import logging -import re - -from collections import defaultdict -from email import message_from_string - -from twisted.internet import defer -from twisted.python import log -from zope.interface import implements -from leap.soledad.common import l2db - -from leap.common.check import leap_assert, leap_assert_type - -from leap.mail import constants -from leap.mail import walk -from leap.mail.adaptors import soledad_indexes as indexes -from leap.mail.constants import INBOX_NAME -from leap.mail.adaptors import models -from leap.mail.imap.mailbox import normalize_mailbox -from leap.mail.utils import lowerdict, first -from leap.mail.utils import stringify_parts_map -from leap.mail.interfaces import IMailAdaptor, IMessageWrapper - -from leap.soledad.common.document import SoledadDocument - - -logger = logging.getLogger(__name__) - -# TODO -# [ ] Convenience function to create mail specifying subject, date, etc? - - -_MSGID_PATTERN = r"""<([\w@.]+)>""" -_MSGID_RE = re.compile(_MSGID_PATTERN) - - -class DuplicatedDocumentError(Exception): - """ - Raised when a duplicated document is detected. - """ - pass - - -def cleanup_deferred_locks(): - """ - Need to use this from within trial to cleanup the reactor before - each run. - """ - SoledadDocumentWrapper._k_locks = defaultdict(defer.DeferredLock) - - -class SoledadDocumentWrapper(models.DocumentWrapper): - """ - A Wrapper object that can be manipulated, passed around, and serialized in - a format that the Soledad Store understands. - - It ensures atomicity of the document operations on creation, update and - deletion. - """ - # TODO we could also use a _dirty flag (in models) - # TODO add a get_count() method ??? -- that is extended over l2db. - - # We keep a dictionary with DeferredLocks, that will be - # unique to every subclass of SoledadDocumentWrapper. - _k_locks = defaultdict(defer.DeferredLock) - - @classmethod - def _get_klass_lock(cls): - """ - Get a DeferredLock that is unique for this subclass name. - Used to lock the access to indexes in the `get_or_create` call - for a particular DocumentWrapper. - """ - return cls._k_locks[cls.__name__] - - def __init__(self, doc_id=None, future_doc_id=None, **kwargs): - self._doc_id = doc_id - self._future_doc_id = future_doc_id - self._lock = defer.DeferredLock() - super(SoledadDocumentWrapper, self).__init__(**kwargs) - - @property - def doc_id(self): - return self._doc_id - - @property - def future_doc_id(self): - return self._future_doc_id - - def set_future_doc_id(self, doc_id): - self._future_doc_id = doc_id - - def create(self, store, is_copy=False): - """ - Create the documents for this wrapper. - Since this method will not check for duplication, the - responsibility of avoiding duplicates is left to the caller. - - You might be interested in using `get_or_create` classmethod - instead (that's the preferred way of creating documents from - the wrapper object). - - :return: a deferred that will fire when the underlying - Soledad document has been created. - :rtype: Deferred - """ - leap_assert(self._doc_id is None, - "This document already has a doc_id!") - - def update_doc_id(doc): - self._doc_id = doc.doc_id - self.set_future_doc_id(None) - return doc - - def update_wrapper(failure): - # In the case of some copies (for instance, from one folder to - # another and back to the original folder), the document that we - # want to insert already exists. In this case, putting it - # and overwriting the document with that doc_id is the right thing - # to do. - failure.trap(l2db.errors.RevisionConflict) - self._doc_id = self.future_doc_id - self._future_doc_id = None - return self.update(store) - - if self.future_doc_id is None: - d = store.create_doc(self.serialize()) - else: - d = store.create_doc(self.serialize(), - doc_id=self.future_doc_id) - d.addCallback(update_doc_id) - - if is_copy: - d.addErrback(update_wrapper) - else: - d.addErrback(self._catch_revision_conflict, self.future_doc_id) - return d - - def update(self, store): - """ - Update the documents for this wrapper. - - :return: a deferred that will fire when the underlying - Soledad document has been updated. - :rtype: Deferred - """ - # the deferred lock guards against revision conflicts - return self._lock.run(self._update, store) - - def _update(self, store): - leap_assert(self._doc_id is not None, - "Need to create doc before updating") - - def update_and_put_doc(doc): - doc.content.update(self.serialize()) - d = store.put_doc(doc) - d.addErrback(self._catch_revision_conflict, doc.doc_id) - return d - - d = store.get_doc(self._doc_id) - d.addCallback(update_and_put_doc) - return d - - def _catch_revision_conflict(self, failure, doc_id): - # XXX We can have some RevisionConflicts if we try - # to put the docs that are already there. - # This can happen right now when creating/saving the cdocs - # during a copy. Instead of catching and ignoring this - # error, we should mark them in the copy so there is no attempt to - # create/update them. - failure.trap(l2db.errors.RevisionConflict) - logger.debug("Got conflict while putting %s" % doc_id) - - def delete(self, store): - """ - Delete the documents for this wrapper. - - :return: a deferred that will fire when the underlying - Soledad document has been deleted. - :rtype: Deferred - """ - # the deferred lock guards against conflicts while updating - return self._lock.run(self._delete, store) - - def _delete(self, store): - leap_assert(self._doc_id is not None, - "Need to create doc before deleting") - # XXX might want to flag this DocumentWrapper to avoid - # updating it by mistake. This could go in models.DocumentWrapper - - def delete_doc(doc): - return store.delete_doc(doc) - - d = store.get_doc(self._doc_id) - d.addCallback(delete_doc) - return d - - @classmethod - def get_or_create(cls, store, index, value): - """ - Get a unique DocumentWrapper by index, or create a new one if the - matching query does not exist. - - :param index: the primary index for the model. - :type index: str - :param value: the value to query the primary index. - :type value: str - - :return: a deferred that will be fired with the SoledadDocumentWrapper - matching the index query, either existing or just created. - :rtype: Deferred - """ - return cls._get_klass_lock().run( - cls._get_or_create, store, index, value) - - @classmethod - def _get_or_create(cls, store, index, value): - # TODO shorten this method. - assert store is not None - assert index is not None - assert value is not None - - def get_main_index(): - try: - return cls.model.__meta__.index - except AttributeError: - raise RuntimeError("The model is badly defined") - - # TODO separate into another method? - def try_to_get_doc_from_index(indexes): - values = [] - idx_def = dict(indexes)[index] - if len(idx_def) == 1: - values = [value] - else: - main_index = get_main_index() - fields = cls.model.serialize() - for field in idx_def: - if field == main_index: - values.append(value) - else: - values.append(fields[field]) - d = store.get_from_index(index, *values) - return d - - def get_first_doc_if_any(docs): - if not docs: - return None - if len(docs) > 1: - raise DuplicatedDocumentError - return docs[0] - - def wrap_existing_or_create_new(doc): - if doc: - return cls(doc_id=doc.doc_id, **doc.content) - else: - return create_and_wrap_new_doc() - - def create_and_wrap_new_doc(): - # XXX use closure to store indexes instead of - # querying for them again. - d = store.list_indexes() - d.addCallback(get_wrapper_instance_from_index) - d.addCallback(return_wrapper_when_created) - return d - - def get_wrapper_instance_from_index(indexes): - init_values = {} - idx_def = dict(indexes)[index] - if len(idx_def) == 1: - init_value = {idx_def[0]: value} - return cls(**init_value) - main_index = get_main_index() - fields = cls.model.serialize() - for field in idx_def: - if field == main_index: - init_values[field] = value - else: - init_values[field] = fields[field] - return cls(**init_values) - - def return_wrapper_when_created(wrapper): - d = wrapper.create(store) - d.addCallback(lambda doc: wrapper) - return d - - d = store.list_indexes() - d.addCallback(try_to_get_doc_from_index) - d.addCallback(get_first_doc_if_any) - d.addCallback(wrap_existing_or_create_new) - return d - - @classmethod - def get_all(cls, store): - """ - Get a collection of wrappers around all the documents belonging - to this kind. - - For this to work, the model.__meta__ needs to include a tuple with - the index to be used for listing purposes, and which is the field to be - used to query the index. - - Note that this method only supports indexes of a single field at the - moment. It also might be too expensive to return all the documents - matching the query, so handle with care. - - class __meta__(object): - index = "name" - list_index = ("by-type", "type_") - - :return: a deferred that will be fired with an iterable containing - as many SoledadDocumentWrapper are matching the index defined - in the model as the `list_index`. - :rtype: Deferred - """ - # TODO LIST (get_all) - # [ ] extend support to indexes with n-ples - # [ ] benchmark the cost of querying and returning indexes in a big - # database. This might badly need pagination before being put to - # serious use. - return cls._get_klass_lock().run(cls._get_all, store) - - @classmethod - def _get_all(cls, store): - try: - list_index, list_attr = cls.model.__meta__.list_index - except AttributeError: - raise RuntimeError("The model is badly defined: no list_index") - try: - index_value = getattr(cls.model, list_attr) - except AttributeError: - raise RuntimeError("The model is badly defined: " - "no attribute matching list_index") - - def wrap_docs(docs): - return (cls(doc_id=doc.doc_id, **doc.content) for doc in docs) - - d = store.get_from_index(list_index, index_value) - d.addCallback(wrap_docs) - return d - - def __repr__(self): - try: - idx = getattr(self, self.model.__meta__.index) - except AttributeError: - idx = "" - return "<%s: %s (%s)>" % (self.__class__.__name__, - idx, self._doc_id) - - -# -# Message documents -# - -class FlagsDocWrapper(SoledadDocumentWrapper): - - class model(models.SerializableModel): - type_ = "flags" - chash = "" - - mbox_uuid = "" - seen = False - deleted = False - recent = False - flags = [] - tags = [] - size = 0 - multi = False - - class __meta__(object): - index = "mbox" - - def set_mbox_uuid(self, mbox_uuid): - # XXX raise error if already created, should use copy instead - mbox_uuid = mbox_uuid.replace('-', '_') - new_id = constants.FDOCID.format(mbox_uuid=mbox_uuid, chash=self.chash) - self._future_doc_id = new_id - self.mbox_uuid = mbox_uuid - - def get_flags(self): - """ - Get the flags for this message (as a tuple of strings, not unicode). - """ - return map(str, self.flags) - - -class HeaderDocWrapper(SoledadDocumentWrapper): - - class model(models.SerializableModel): - type_ = "head" - chash = "" - - date = "" - subject = "" - headers = {} - part_map = {} - body = "" # link to phash of body - msgid = "" - multi = False - - class __meta__(object): - index = "chash" - - -class ContentDocWrapper(SoledadDocumentWrapper): - - class model(models.SerializableModel): - type_ = "cnt" - phash = "" - - ctype = "" # XXX index by ctype too? - lkf = [] # XXX not implemented yet! - raw = "" - - content_disposition = "" - content_transfer_encoding = "" - content_type = "" - - class __meta__(object): - index = "phash" - - -class MetaMsgDocWrapper(SoledadDocumentWrapper): - - class model(models.SerializableModel): - type_ = "meta" - fdoc = "" - hdoc = "" - cdocs = [] - - def set_mbox_uuid(self, mbox_uuid): - # XXX raise error if already created, should use copy instead - mbox_uuid = mbox_uuid.replace('-', '_') - chash = re.findall(constants.FDOCID_CHASH_RE, self.fdoc)[0] - new_id = constants.METAMSGID.format(mbox_uuid=mbox_uuid, chash=chash) - new_fdoc_id = constants.FDOCID.format(mbox_uuid=mbox_uuid, chash=chash) - self._future_doc_id = new_id - self.fdoc = new_fdoc_id - - -class MessageWrapper(object): - - # This could benefit of a DeferredLock to create/update all the - # documents at the same time maybe, and defend against concurrent updates? - - implements(IMessageWrapper) - - def __init__(self, mdoc, fdoc, hdoc, cdocs=None, is_copy=False): - """ - Need at least a metamsg-document, a flag-document and a header-document - to instantiate a MessageWrapper. Content-documents can be retrieved - lazily. - - cdocs, if any, should be a dictionary in which the keys are ascending - integers, beginning at one, and the values are dictionaries with the - content of the content-docs. - - is_copy, if set to True, will only attempt to create mdoc and fdoc - (because hdoc and cdocs are supposed to exist already) - """ - self._is_copy = is_copy - - def get_doc_wrapper(doc, cls): - if isinstance(doc, SoledadDocument): - doc_id = doc.doc_id - doc = doc.content - else: - doc_id = None - if not doc: - doc = {} - return cls(doc_id=doc_id, **doc) - - self.mdoc = get_doc_wrapper(mdoc, MetaMsgDocWrapper) - - self.fdoc = get_doc_wrapper(fdoc, FlagsDocWrapper) - self.fdoc.set_future_doc_id(self.mdoc.fdoc) - - self.hdoc = get_doc_wrapper(hdoc, HeaderDocWrapper) - self.hdoc.set_future_doc_id(self.mdoc.hdoc) - - if cdocs is None: - cdocs = {} - cdocs_keys = cdocs.keys() - assert sorted(cdocs_keys) == range(1, len(cdocs_keys) + 1) - self.cdocs = dict([ - (key, get_doc_wrapper(doc, ContentDocWrapper)) - for (key, doc) in cdocs.items()]) - for doc_id, cdoc in zip(self.mdoc.cdocs, self.cdocs.values()): - if cdoc.raw == "": - log.msg("Empty raw field in cdoc %s" % doc_id) - cdoc.set_future_doc_id(doc_id) - - def create(self, store, notify_just_mdoc=False, pending_inserts_dict=None): - """ - Create all the parts for this message in the store. - - :param store: an instance of Soledad - - :param notify_just_mdoc: - if set to True, this method will return *only* the deferred - corresponding to the creation of the meta-message document. - Be warned that in that case there will be no record of failures - when creating the other part-documents. - - Otherwise, this method will return a deferred that will wait for - the creation of all the part documents. - - Setting this flag to True is mostly a convenient workaround for the - fact that massive serial appends will take too much time, and in - most of the cases the MUA will only switch to the mailbox where the - appends have happened after a certain time, which in most of the - times will be enough to have all the queued insert operations - finished. - :type notify_just_mdoc: bool - :param pending_inserts_dict: - a dictionary with the pending inserts ids. - :type pending_inserts_dict: dict - - :return: a deferred whose callback will be called when either all the - part documents have been written, or just the metamsg-doc, - depending on the value of the notify_just_mdoc flag - :rtype: defer.Deferred - """ - if pending_inserts_dict is None: - pending_inserts_dict = {} - - leap_assert(self.cdocs, - "Need non empty cdocs to create the " - "MessageWrapper documents") - leap_assert(self.mdoc.doc_id is None, - "Cannot create: mdoc has a doc_id") - leap_assert(self.fdoc.doc_id is None, - "Cannot create: fdoc has a doc_id") - - def unblock_pending_insert(result): - if pending_inserts_dict: - ci_headers = lowerdict(self.hdoc.headers) - msgid = ci_headers.get('message-id', None) - try: - d = pending_inserts_dict[msgid] - d.callback(msgid) - except KeyError: - pass - return result - - # TODO check that the doc_ids in the mdoc are coherent - self.d = [] - - mdoc_created = self.mdoc.create(store, is_copy=self._is_copy) - fdoc_created = self.fdoc.create(store, is_copy=self._is_copy) - - mdoc_created.addErrback(lambda f: log.err(f)) - fdoc_created.addErrback(lambda f: log.err(f)) - - self.d.append(mdoc_created) - self.d.append(fdoc_created) - - if not self._is_copy: - if self.hdoc.doc_id is None: - self.d.append(self.hdoc.create(store)) - for cdoc in self.cdocs.values(): - if cdoc.doc_id is not None: - # we could be just linking to an existing - # content-doc. - continue - self.d.append(cdoc.create(store)) - - def log_all_inserted(result): - log.msg("All parts inserted for msg!") - return result - - self.all_inserted_d = defer.gatherResults(self.d, consumeErrors=True) - self.all_inserted_d.addCallback(log_all_inserted) - self.all_inserted_d.addCallback(unblock_pending_insert) - self.all_inserted_d.addErrback(lambda failure: log.err(failure)) - - if notify_just_mdoc: - return mdoc_created - else: - return self.all_inserted_d - - def update(self, store): - """ - Update the only mutable parts, which are within the flags document. - """ - return self.fdoc.update(store) - - def delete(self, store): - # TODO - # Eventually this would have to do the duplicate search or send for the - # garbage collector. At least mdoc and t the mdoc and fdoc can be - # unlinked. - d = [] - if self.mdoc.doc_id: - d.append(self.mdoc.delete(store)) - d.append(self.fdoc.delete(store)) - return defer.gatherResults(d) - - def copy(self, store, new_mbox_uuid): - """ - Return a copy of this MessageWrapper in a new mailbox. - - :param store: an instance of Soledad, or anything that behaves alike. - :param new_mbox_uuid: the uuid of the mailbox where we are copying this - message to. - :type new_mbox_uuid: str - :rtype: MessageWrapper - """ - new_mdoc = self.mdoc.serialize() - new_fdoc = self.fdoc.serialize() - - # the future doc_ids is properly set because we modified - # the pointers in mdoc, which has precedence. - new_wrapper = MessageWrapper(new_mdoc, new_fdoc, None, None, - is_copy=True) - new_wrapper.hdoc = self.hdoc - new_wrapper.cdocs = self.cdocs - new_wrapper.set_mbox_uuid(new_mbox_uuid) - - # XXX could flag so that it only creates mdoc/fdoc... - - d = new_wrapper.create(store) - d.addCallback(lambda result: new_wrapper) - d.addErrback(lambda failure: log.err(failure)) - return d - - def set_mbox_uuid(self, mbox_uuid): - """ - Set the mailbox for this wrapper. - This method should only be used before the Documents for the - MessageWrapper have been created, will raise otherwise. - """ - mbox_uuid = mbox_uuid.replace('-', '_') - self.mdoc.set_mbox_uuid(mbox_uuid) - self.fdoc.set_mbox_uuid(mbox_uuid) - - def set_flags(self, flags): - # TODO serialize the get + update - if flags is None: - flags = tuple() - leap_assert_type(flags, tuple) - self.fdoc.flags = list(flags) - self.fdoc.deleted = "\\Deleted" in flags - self.fdoc.seen = "\\Seen" in flags - self.fdoc.recent = "\\Recent" in flags - - def set_tags(self, tags): - # TODO serialize the get + update - if tags is None: - tags = tuple() - leap_assert_type(tags, tuple) - self.fdoc.tags = list(tags) - - def set_date(self, date): - # XXX assert valid date format - self.hdoc.date = date - - def get_subpart_dict(self, index): - """ - :param index: the part to lookup, 1-indexed - :type index: int - :rtype: dict - """ - return self.hdoc.part_map[str(index)] - - def get_subpart_indexes(self): - return self.hdoc.part_map.keys() - - def get_body(self, store): - """ - :rtype: deferred - """ - body_phash = self.hdoc.body - if body_phash: - d = store.get_doc('C-' + body_phash) - d.addCallback(lambda doc: ContentDocWrapper(**doc.content)) - return d - elif self.cdocs: - return self.cdocs[1] - else: - return '' - -# -# Mailboxes -# - - -class MailboxWrapper(SoledadDocumentWrapper): - - class model(models.SerializableModel): - type_ = "mbox" - mbox = INBOX_NAME - uuid = None - flags = [] - recent = [] - created = 1 - closed = False - subscribed = False - - class __meta__(object): - index = "mbox" - list_index = (indexes.TYPE_IDX, 'type_') - - -# -# Soledad Adaptor -# - -class SoledadIndexMixin(object): - """ - This will need a class attribute `indexes`, that is a dictionary containing - the index definitions for the underlying l2db store underlying soledad. - - It needs to be in the following format: - {'index-name': ['field1', 'field2']} - - You can also add a class attribute `wait_for_indexes` to any class - inheriting from this Mixin, that should be a list of strings representing - the methods that need to wait until the indexes have been initialized - before being able to work properly. - """ - # TODO move this mixin to soledad itself - # so that each application can pass a set of indexes for their data model. - - # TODO could have a wrapper class for indexes, supporting introspection - # and __getattr__ - - # TODO make this an interface? - - indexes = {} - wait_for_indexes = [] - store_ready = False - - def initialize_store(self, store): - """ - Initialize the indexes in the database. - - :param store: store - :returns: a Deferred that will fire when the store is correctly - initialized. - :rtype: deferred - """ - # TODO I think we *should* get another deferredLock in here, but - # global to the soledad namespace, to protect from several points - # initializing soledad indexes at the same time. - self._wait_for_indexes() - - d = self._init_indexes(store) - d.addCallback(self._restore_waiting_methods) - return d - - def _init_indexes(self, store): - """ - Initialize the database indexes. - """ - leap_assert(store, "Cannot init indexes with null soledad") - leap_assert_type(self.indexes, dict) - - def _create_index(name, expression): - return store.create_index(name, *expression) - - def init_idexes(indexes): - deferreds = [] - db_indexes = dict(indexes) - # Loop through the indexes we expect to find. - for name, expression in self.indexes.items(): - if name not in db_indexes: - # The index does not yet exist. - d = _create_index(name, expression) - deferreds.append(d) - elif expression != db_indexes[name]: - # The index exists but the definition is not what expected, - # so we delete it and add the proper index expression. - d = store.delete_index(name) - d.addCallback( - lambda _: _create_index(name, *expression)) - deferreds.append(d) - return defer.gatherResults(deferreds, consumeErrors=True) - - def store_ready(whatever): - self.store_ready = True - return whatever - - self.deferred_indexes = store.list_indexes() - self.deferred_indexes.addCallback(init_idexes) - self.deferred_indexes.addCallback(store_ready) - return self.deferred_indexes - - def _wait_for_indexes(self): - """ - Make the marked methods to wait for the indexes to be ready. - Heavily based on - http://blogs.fluidinfo.com/terry/2009/05/11/a-mixin-class-allowing-python-__init__-methods-to-work-with-twisted-deferreds/ - - :param methods: methods that need to wait for the indexes to be ready - :type methods: tuple(str) - """ - leap_assert_type(self.wait_for_indexes, list) - methods = self.wait_for_indexes - - self.waiting = [] - self.stored = {} - - def makeWrapper(method): - def wrapper(*args, **kw): - d = defer.Deferred() - d.addCallback(lambda _: self.stored[method](*args, **kw)) - self.waiting.append(d) - return d - return wrapper - - for method in methods: - self.stored[method] = getattr(self, method) - setattr(self, method, makeWrapper(method)) - - def _restore_waiting_methods(self, _): - for method in self.stored: - setattr(self, method, self.stored[method]) - for d in self.waiting: - d.callback(None) - - -class SoledadMailAdaptor(SoledadIndexMixin): - - implements(IMailAdaptor) - store = None - - indexes = indexes.MAIL_INDEXES - wait_for_indexes = ['get_or_create_mbox', 'update_mbox', 'get_all_mboxes'] - - mboxwrapper_klass = MailboxWrapper - - def __init__(self): - SoledadIndexMixin.__init__(self) - - # Message handling - - def get_msg_from_string(self, MessageClass, raw_msg): - """ - Get an instance of a MessageClass initialized with a MessageWrapper - that contains all the parts obtained from parsing the raw string for - the message. - - :param MessageClass: any Message class that can be initialized passing - an instance of an IMessageWrapper implementor. - :type MessageClass: type - :param raw_msg: a string containing the raw email message. - :type raw_msg: str - :rtype: MessageClass instance. - """ - assert(MessageClass is not None) - mdoc, fdoc, hdoc, cdocs = _split_into_parts(raw_msg) - return self.get_msg_from_docs( - MessageClass, mdoc, fdoc, hdoc, cdocs) - - def get_msg_from_docs(self, MessageClass, mdoc, fdoc, hdoc, cdocs=None, - uid=None): - """ - Get an instance of a MessageClass initialized with a MessageWrapper - that contains the passed part documents. - - This is not the recommended way of obtaining a message, unless you know - how to take care of ensuring the internal consistency between the part - documents, or unless you are glueing together the part documents that - have been previously generated by `get_msg_from_string`. - - :param MessageClass: any Message class that can be initialized passing - an instance of an IMessageWrapper implementor. - :type MessageClass: type - :param fdoc: a dictionary containing values from which a - FlagsDocWrapper can be initialized - :type fdoc: dict - :param hdoc: a dictionary containing values from which a - HeaderDocWrapper can be initialized - :type hdoc: dict - :param cdocs: None, or a dictionary mapping integers (1-indexed) to - dicts from where a ContentDocWrapper can be initialized. - :type cdocs: dict, or None - - :rtype: MessageClass instance. - """ - assert(MessageClass is not None) - return MessageClass(MessageWrapper(mdoc, fdoc, hdoc, cdocs), uid=uid) - - def get_msg_from_mdoc_id(self, MessageClass, store, mdoc_id, - uid=None, get_cdocs=False): - - def wrap_meta_doc(doc): - cls = MetaMsgDocWrapper - return cls(doc_id=doc.doc_id, **doc.content) - - def get_part_docs_from_mdoc_wrapper(wrapper): - d_docs = [] - d_docs.append(store.get_doc(wrapper.fdoc)) - d_docs.append(store.get_doc(wrapper.hdoc)) - for cdoc in wrapper.cdocs: - d_docs.append(store.get_doc(cdoc)) - - def add_mdoc(doc_list): - return [wrapper.serialize()] + doc_list - - d = defer.gatherResults(d_docs) - d.addCallback(add_mdoc) - return d - - def get_parts_doc_from_mdoc_id(): - mbox = re.findall(constants.METAMSGID_MBOX_RE, mdoc_id)[0] - chash = re.findall(constants.METAMSGID_CHASH_RE, mdoc_id)[0] - - def _get_fdoc_id_from_mdoc_id(): - return constants.FDOCID.format(mbox_uuid=mbox, chash=chash) - - def _get_hdoc_id_from_mdoc_id(): - return constants.HDOCID.format(mbox_uuid=mbox, chash=chash) - - d_docs = [] - fdoc_id = _get_fdoc_id_from_mdoc_id() - hdoc_id = _get_hdoc_id_from_mdoc_id() - - d_docs.append(store.get_doc(mdoc_id)) - d_docs.append(store.get_doc(fdoc_id)) - d_docs.append(store.get_doc(hdoc_id)) - - d = defer.gatherResults(d_docs) - return d - - def _err_log_failure_part_docs(failure): - # See https://leap.se/code/issues/7495. - # This avoids blocks, but the real cause still needs to be - # isolated (0.9.0rc3) -- kali - log.msg("BUG ---------------------------------------------------") - log.msg("BUG: Error while retrieving part docs for mdoc id %s" % - mdoc_id) - log.err(failure) - log.msg("BUG (please report above info) ------------------------") - return [] - - def _err_log_cannot_find_msg(failure): - log.msg("BUG: Error while getting msg (uid=%s)" % uid) - return None - - if get_cdocs: - d = store.get_doc(mdoc_id) - d.addCallback(wrap_meta_doc) - d.addCallback(get_part_docs_from_mdoc_wrapper) - d.addErrback(_err_log_failure_part_docs) - - else: - d = get_parts_doc_from_mdoc_id() - - d.addCallback(self._get_msg_from_variable_doc_list, - msg_class=MessageClass, uid=uid) - d.addErrback(_err_log_cannot_find_msg) - return d - - def _get_msg_from_variable_doc_list(self, doc_list, msg_class, uid=None): - if len(doc_list) == 3: - mdoc, fdoc, hdoc = doc_list - cdocs = None - elif len(doc_list) > 3: - # XXX is this case used? - mdoc, fdoc, hdoc = doc_list[:3] - cdocs = dict(enumerate(doc_list[3:], 1)) - return self.get_msg_from_docs( - msg_class, mdoc, fdoc, hdoc, cdocs, uid=uid) - - def get_flags_from_mdoc_id(self, store, mdoc_id): - """ - # XXX stuff here... - """ - mbox = re.findall(constants.METAMSGID_MBOX_RE, mdoc_id)[0] - chash = re.findall(constants.METAMSGID_CHASH_RE, mdoc_id)[0] - - def _get_fdoc_id_from_mdoc_id(): - return constants.FDOCID.format(mbox_uuid=mbox, chash=chash) - - fdoc_id = _get_fdoc_id_from_mdoc_id() - - def wrap_fdoc(doc): - if not doc: - return - cls = FlagsDocWrapper - return cls(doc_id=doc.doc_id, **doc.content) - - def get_flags(fdoc_wrapper): - if not fdoc_wrapper: - return [] - return fdoc_wrapper.get_flags() - - d = store.get_doc(fdoc_id) - d.addCallback(wrap_fdoc) - d.addCallback(get_flags) - return d - - def create_msg(self, store, msg): - """ - :param store: an instance of soledad, or anything that behaves alike - :param msg: a Message object. - - :return: a Deferred that is fired when all the underlying documents - have been created. - :rtype: defer.Deferred - """ - wrapper = msg.get_wrapper() - return wrapper.create(store) - - def update_msg(self, store, msg): - """ - :param msg: a Message object. - :param store: an instance of soledad, or anything that behaves alike - :return: a Deferred that is fired when all the underlying documents - have been updated (actually, it's only the fdoc that's allowed - to update). - :rtype: defer.Deferred - """ - wrapper = msg.get_wrapper() - return wrapper.update(store) - - # batch deletion - - def del_all_flagged_messages(self, store, mbox_uuid): - """ - Delete all messages flagged as deleted. - """ - def err(failure): - log.err(failure) - - def delete_fdoc_and_mdoc_flagged(fdocs): - # low level here, not using the wrappers... - # get meta doc ids from the flag doc ids - fdoc_ids = [doc.doc_id for doc in fdocs] - mdoc_ids = map(lambda s: "M" + s[1:], fdoc_ids) - - def delete_all_docs(mdocs, fdocs): - mdocs = list(mdocs) - doc_ids = [m.doc_id for m in mdocs] - _d = [] - docs = mdocs + fdocs - for doc in docs: - _d.append(store.delete_doc(doc)) - d = defer.gatherResults(_d) - # return the mdocs ids only - d.addCallback(lambda _: doc_ids) - return d - - d = store.get_docs(mdoc_ids) - d.addCallback(delete_all_docs, fdocs) - d.addErrback(err) - return d - - type_ = FlagsDocWrapper.model.type_ - uuid = mbox_uuid.replace('-', '_') - deleted_index = indexes.TYPE_MBOX_DEL_IDX - - d = store.get_from_index(deleted_index, type_, uuid, "1") - d.addCallbacks(delete_fdoc_and_mdoc_flagged, err) - return d - - # count messages - - def get_count_unseen(self, store, mbox_uuid): - """ - Get the number of unseen messages for a given mailbox. - - :param store: instance of Soledad. - :param mbox_uuid: the uuid for this mailbox. - :rtype: int - """ - type_ = FlagsDocWrapper.model.type_ - uuid = mbox_uuid.replace('-', '_') - - unseen_index = indexes.TYPE_MBOX_SEEN_IDX - - d = store.get_count_from_index(unseen_index, type_, uuid, "0") - d.addErrback(self._errback) - return d - - def get_count_recent(self, store, mbox_uuid): - """ - Get the number of recent messages for a given mailbox. - - :param store: instance of Soledad. - :param mbox_uuid: the uuid for this mailbox. - :rtype: int - """ - type_ = FlagsDocWrapper.model.type_ - uuid = mbox_uuid.replace('-', '_') - - recent_index = indexes.TYPE_MBOX_RECENT_IDX - - d = store.get_count_from_index(recent_index, type_, uuid, "1") - d.addErrback(self._errback) - return d - - # search api - - def get_mdoc_id_from_msgid(self, store, mbox_uuid, msgid): - """ - Get the UID for a message with the passed msgid (the one in the headers - msg-id). - This is used by the MUA to retrieve the recently saved draft. - """ - type_ = HeaderDocWrapper.model.type_ - uuid = mbox_uuid.replace('-', '_') - - msgid_index = indexes.TYPE_MSGID_IDX - - def get_mdoc_id(hdoc): - if not hdoc: - log.msg("Could not find a HDOC with MSGID %s" % msgid) - return None - hdoc = hdoc[0] - mdoc_id = hdoc.doc_id.replace("H-", "M-%s-" % uuid) - return mdoc_id - - d = store.get_from_index(msgid_index, type_, msgid) - d.addCallback(get_mdoc_id) - return d - - # Mailbox handling - - def get_or_create_mbox(self, store, name): - """ - Get the mailbox with the given name, or create one if it does not - exist. - - :param store: instance of Soledad - :param name: the name of the mailbox - :type name: str - """ - index = indexes.TYPE_MBOX_IDX - mbox = normalize_mailbox(name) - return MailboxWrapper.get_or_create(store, index, mbox) - - def update_mbox(self, store, mbox_wrapper): - """ - Update the documents for a given mailbox. - :param mbox_wrapper: MailboxWrapper instance - :type mbox_wrapper: MailboxWrapper - :return: a Deferred that will be fired when the mailbox documents - have been updated. - :rtype: defer.Deferred - """ - leap_assert_type(mbox_wrapper, SoledadDocumentWrapper) - return mbox_wrapper.update(store) - - def delete_mbox(self, store, mbox_wrapper): - leap_assert_type(mbox_wrapper, SoledadDocumentWrapper) - return mbox_wrapper.delete(store) - - def get_all_mboxes(self, store): - """ - Retrieve a list with wrappers for all the mailboxes. - - :return: a deferred that will be fired with a list of all the - MailboxWrappers found. - :rtype: defer.Deferred - """ - return MailboxWrapper.get_all(store) - - def _errback(self, failure): - log.err(failure) - - -def _split_into_parts(raw): - # TODO signal that we can delete the original message!----- - # when all the processing is done. - # TODO add the linked-from info ! - # TODO add reference to the original message? - # TODO populate Default FLAGS/TAGS (unseen?) - # TODO seed propely the content_docs with defaults?? - - msg, chash, multi = _parse_msg(raw) - size = len(msg.as_string()) - - parts_map = walk.get_tree(msg) - cdocs_list = list(walk.get_raw_docs(msg)) - cdocs_phashes = [c['phash'] for c in cdocs_list] - body_phash = walk.get_body_phash(msg) - - mdoc = _build_meta_doc(chash, cdocs_phashes) - fdoc = _build_flags_doc(chash, size, multi) - hdoc = _build_headers_doc(msg, chash, body_phash, parts_map) - - # The MessageWrapper expects a dict, one-indexed - cdocs = dict(enumerate(cdocs_list, 1)) - - return mdoc, fdoc, hdoc, cdocs - - -def _parse_msg(raw): - msg = message_from_string(raw) - chash = walk.get_hash(raw) - multi = msg.is_multipart() - return msg, chash, multi - - -def _build_meta_doc(chash, cdocs_phashes): - _mdoc = MetaMsgDocWrapper() - # FIXME passing the inbox name because we don't have the uuid at this - # point. - - _mdoc.fdoc = constants.FDOCID.format(mbox_uuid=INBOX_NAME, chash=chash) - _mdoc.hdoc = constants.HDOCID.format(chash=chash) - _mdoc.cdocs = [constants.CDOCID.format(phash=p) for p in cdocs_phashes] - - return _mdoc.serialize() - - -def _build_flags_doc(chash, size, multi): - _fdoc = FlagsDocWrapper(chash=chash, size=size, multi=multi) - return _fdoc.serialize() - - -def _build_headers_doc(msg, chash, body_phash, parts_map): - """ - Assemble a headers document from the original parsed message, the - content-hash, and the parts map. - - It takes into account possibly repeated headers. - """ - headers = defaultdict(list) - for k, v in msg.items(): - headers[k].append(v) - # "fix" for repeated headers (as in "Received:" - for k, v in headers.items(): - newline = "\n%s: " % (k.lower(),) - headers[k] = newline.join(v) - - lower_headers = lowerdict(dict(headers)) - msgid = first(_MSGID_RE.findall( - lower_headers.get('message-id', ''))) - - _hdoc = HeaderDocWrapper( - chash=chash, headers=headers, body=body_phash, - msgid=msgid) - - def copy_attr(headers, key, doc): - if key in headers: - setattr(doc, key, headers[key]) - - copy_attr(lower_headers, "subject", _hdoc) - copy_attr(lower_headers, "date", _hdoc) - - hdoc = _hdoc.serialize() - # add some of the attr from the parts map to header doc - for key in parts_map: - if key in ('body', 'multi', 'part_map'): - hdoc[key] = parts_map[key] - return stringify_parts_map(hdoc) diff --git a/mail/src/leap/mail/adaptors/soledad_indexes.py b/mail/src/leap/mail/adaptors/soledad_indexes.py deleted file mode 100644 index eec7d28..0000000 --- a/mail/src/leap/mail/adaptors/soledad_indexes.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -# soledad_indexes.py -# Copyright (C) 2013, 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 <http://www.gnu.org/licenses/>. -""" -Soledad Indexes for Mail Documents. -""" - -# TODO -# [ ] hide most of the constants here - -# Document Type, for indexing - -TYPE = "type" -MBOX = "mbox" -MBOX_UUID = "mbox_uuid" -FLAGS = "flags" -HEADERS = "head" -CONTENT = "cnt" -RECENT = "rct" -HDOCS_SET = "hdocset" - -INCOMING_KEY = "incoming" -ERROR_DECRYPTING_KEY = "errdecr" - -# indexing keys -CONTENT_HASH = "chash" -PAYLOAD_HASH = "phash" -MSGID = "msgid" -UID = "uid" - - -# Index types -# -------------- - -TYPE_IDX = 'by-type' -TYPE_MBOX_IDX = 'by-type-and-mbox' -TYPE_MBOX_UUID_IDX = 'by-type-and-mbox-uuid' -TYPE_SUBS_IDX = 'by-type-and-subscribed' -TYPE_MSGID_IDX = 'by-type-and-message-id' -TYPE_MBOX_SEEN_IDX = 'by-type-and-mbox-and-seen' -TYPE_MBOX_RECENT_IDX = 'by-type-and-mbox-and-recent' -TYPE_MBOX_DEL_IDX = 'by-type-and-mbox-and-deleted' -TYPE_MBOX_C_HASH_IDX = 'by-type-and-mbox-and-contenthash' -TYPE_C_HASH_IDX = 'by-type-and-contenthash' -TYPE_C_HASH_PART_IDX = 'by-type-and-contenthash-and-partnumber' -TYPE_P_HASH_IDX = 'by-type-and-payloadhash' - -# Soledad index for incoming mail, without decrypting errors. -# and the backward-compatible index, will be deprecated at 0.7 -JUST_MAIL_IDX = "just-mail" -JUST_MAIL_COMPAT_IDX = "just-mail-compat" - - -# TODO -# it would be nice to measure the cost of indexing -# by many fields. - -# TODO -# make the indexes dict more readable! - -MAIL_INDEXES = { - # generic - TYPE_IDX: [TYPE], - TYPE_MBOX_IDX: [TYPE, MBOX], - TYPE_MBOX_UUID_IDX: [TYPE, MBOX_UUID], - - # XXX deprecate 0.4.0 - # TYPE_MBOX_UID_IDX: [TYPE, MBOX, UID], - - # mailboxes - TYPE_SUBS_IDX: [TYPE, 'bool(subscribed)'], - - # fdocs uniqueness - TYPE_MBOX_C_HASH_IDX: [TYPE, MBOX, CONTENT_HASH], - - # headers doc - search by msgid. - TYPE_MSGID_IDX: [TYPE, MSGID], - - # content, headers doc - TYPE_C_HASH_IDX: [TYPE, CONTENT_HASH], - - # attachment payload dedup - TYPE_P_HASH_IDX: [TYPE, PAYLOAD_HASH], - - # messages - TYPE_MBOX_SEEN_IDX: [TYPE, MBOX_UUID, 'bool(seen)'], - TYPE_MBOX_RECENT_IDX: [TYPE, MBOX_UUID, 'bool(recent)'], - TYPE_MBOX_DEL_IDX: [TYPE, MBOX_UUID, 'bool(deleted)'], - - # incoming queue - JUST_MAIL_IDX: ["bool(%s)" % (INCOMING_KEY,), - "bool(%s)" % (ERROR_DECRYPTING_KEY,)], -} diff --git a/mail/src/leap/mail/adaptors/tests/rfc822.message b/mail/src/leap/mail/adaptors/tests/rfc822.message deleted file mode 120000 index b19cc28..0000000 --- a/mail/src/leap/mail/adaptors/tests/rfc822.message +++ /dev/null @@ -1 +0,0 @@ -../../tests/rfc822.message
\ No newline at end of file diff --git a/mail/src/leap/mail/adaptors/tests/test_models.py b/mail/src/leap/mail/adaptors/tests/test_models.py deleted file mode 100644 index b82cfad..0000000 --- a/mail/src/leap/mail/adaptors/tests/test_models.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -# test_models.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 <http://www.gnu.org/licenses/>. -""" -Tests for the leap.mail.adaptors.models module. -""" -from twisted.trial import unittest - -from leap.mail.adaptors import models - - -class SerializableModelsTestCase(unittest.TestCase): - - def test_good_serialized_model(self): - - class M(models.SerializableModel): - foo = 42 - bar = 33 - baaz_ = None - _nope = 0 - __nope = 0 - - def not_today(self): - pass - - class IgnoreMe(object): - pass - - def killmeplease(x): - return x - - serialized = M.serialize() - expected = {'foo': 42, 'bar': 33, 'baaz': None} - self.assertEqual(serialized, expected) - - -class DocumentWrapperTestCase(unittest.TestCase): - - def test_wrapper_defaults(self): - - class Wrapper(models.DocumentWrapper): - class model(models.SerializableModel): - foo = 42 - bar = 11 - - wrapper = Wrapper() - wrapper._ignored = True - serialized = wrapper.serialize() - expected = {'foo': 42, 'bar': 11} - self.assertEqual(serialized, expected) - - def test_initialized_wrapper(self): - - class Wrapper(models.DocumentWrapper): - class model(models.SerializableModel): - foo = 42 - bar_ = 11 - - wrapper = Wrapper(foo=0, bar=-1) - serialized = wrapper.serialize() - expected = {'foo': 0, 'bar': -1} - self.assertEqual(serialized, expected) - - wrapper.foo = 23 - serialized = wrapper.serialize() - expected = {'foo': 23, 'bar': -1} - self.assertEqual(serialized, expected) - - wrapper = Wrapper(foo=0) - serialized = wrapper.serialize() - expected = {'foo': 0, 'bar': 11} - self.assertEqual(serialized, expected) - - def test_invalid_initialized_wrapper(self): - - class Wrapper(models.DocumentWrapper): - class model(models.SerializableModel): - foo = 42 - - def getwrapper(): - return Wrapper(bar=1) - self.assertRaises(RuntimeError, getwrapper) - - def test_no_model_wrapper(self): - - class Wrapper(models.DocumentWrapper): - pass - - def getwrapper(): - w = Wrapper() - w.foo = None - - self.assertRaises(RuntimeError, getwrapper) diff --git a/mail/src/leap/mail/adaptors/tests/test_soledad_adaptor.py b/mail/src/leap/mail/adaptors/tests/test_soledad_adaptor.py deleted file mode 100644 index 73eaf16..0000000 --- a/mail/src/leap/mail/adaptors/tests/test_soledad_adaptor.py +++ /dev/null @@ -1,529 +0,0 @@ -# -*- coding: utf-8 -*- -# test_soledad_adaptor.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 <http://www.gnu.org/licenses/>. -""" -Tests for the Soledad Adaptor module - leap.mail.adaptors.soledad -""" -import os -from functools import partial - -from twisted.internet import defer - -from leap.mail.adaptors import models -from leap.mail.adaptors.soledad import SoledadDocumentWrapper -from leap.mail.adaptors.soledad import SoledadIndexMixin -from leap.mail.adaptors.soledad import SoledadMailAdaptor -from leap.mail.testing.common import SoledadTestMixin - -from email.MIMEMultipart import MIMEMultipart -from email.mime.text import MIMEText - -# DEBUG -# import logging -# logging.basicConfig(level=logging.DEBUG) - - -class CounterWrapper(SoledadDocumentWrapper): - class model(models.SerializableModel): - counter = 0 - flag = None - - -class CharacterWrapper(SoledadDocumentWrapper): - class model(models.SerializableModel): - name = "" - age = 20 - - -class ActorWrapper(SoledadDocumentWrapper): - class model(models.SerializableModel): - type_ = "actor" - name = None - - class __meta__(object): - index = "name" - list_index = ("by-type", "type_") - - -class TestAdaptor(SoledadIndexMixin): - indexes = {'by-name': ['name'], - 'by-type-and-name': ['type', 'name'], - 'by-type': ['type']} - - -class SoledadDocWrapperTestCase(SoledadTestMixin): - """ - Tests for the SoledadDocumentWrapper. - """ - def assert_num_docs(self, num, docs): - self.assertEqual(len(docs[1]), num) - - def test_create_single(self): - - store = self._soledad - wrapper = CounterWrapper() - - def assert_one_doc(docs): - self.assertEqual(docs[0], 1) - - d = wrapper.create(store) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(assert_one_doc) - return d - - def test_create_many(self): - - store = self._soledad - w1 = CounterWrapper() - w2 = CounterWrapper(counter=1) - w3 = CounterWrapper(counter=2) - w4 = CounterWrapper(counter=3) - w5 = CounterWrapper(counter=4) - - d1 = [w1.create(store), - w2.create(store), - w3.create(store), - w4.create(store), - w5.create(store)] - - d = defer.gatherResults(d1) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 5)) - return d - - def test_multiple_updates(self): - - store = self._soledad - wrapper = CounterWrapper(counter=1) - MAX = 100 - - def assert_doc_id(doc): - self.assertTrue(wrapper._doc_id is not None) - return doc - - def assert_counter_initial_ok(doc): - self.assertEqual(wrapper.counter, 1) - - def increment_counter(ignored): - d1 = [] - - def record_revision(revision): - rev = int(revision.split(':')[1]) - self.results.append(rev) - - for i in list(range(MAX)): - wrapper.counter += 1 - wrapper.flag = i % 2 == 0 - d = wrapper.update(store) - d.addCallback(record_revision) - d1.append(d) - - return defer.gatherResults(d1) - - def assert_counter_final_ok(doc): - self.assertEqual(doc.content['counter'], MAX + 1) - self.assertEqual(doc.content['flag'], False) - - def assert_results_ordered_list(ignored): - self.assertEqual(self.results, sorted(range(2, MAX + 2))) - - d = wrapper.create(store) - d.addCallback(assert_doc_id) - d.addCallback(assert_counter_initial_ok) - d.addCallback(increment_counter) - d.addCallback(lambda _: store.get_doc(wrapper._doc_id)) - d.addCallback(assert_counter_final_ok) - d.addCallback(assert_results_ordered_list) - return d - - def test_delete(self): - adaptor = TestAdaptor() - store = self._soledad - - wrapper_list = [] - - def get_or_create_bob(ignored): - def add_to_list(wrapper): - wrapper_list.append(wrapper) - return wrapper - wrapper = CharacterWrapper.get_or_create( - store, 'by-name', 'bob') - wrapper.addCallback(add_to_list) - return wrapper - - def delete_bob(ignored): - wrapper = wrapper_list[0] - return wrapper.delete(store) - - d = adaptor.initialize_store(store) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 0)) - - # this should create bob document - d.addCallback(get_or_create_bob) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 1)) - - d.addCallback(delete_bob) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 0)) - return d - - def test_get_or_create(self): - adaptor = TestAdaptor() - store = self._soledad - - def get_or_create_bob(ignored): - wrapper = CharacterWrapper.get_or_create( - store, 'by-name', 'bob') - return wrapper - - d = adaptor.initialize_store(store) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 0)) - - # this should create bob document - d.addCallback(get_or_create_bob) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 1)) - - # this should get us bob document - d.addCallback(get_or_create_bob) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 1)) - return d - - def test_get_or_create_multi_index(self): - adaptor = TestAdaptor() - store = self._soledad - - def get_or_create_actor_harry(ignored): - wrapper = ActorWrapper.get_or_create( - store, 'by-type-and-name', 'harrison') - return wrapper - - def create_director_harry(ignored): - wrapper = ActorWrapper(name="harrison", type="director") - return wrapper.create(store) - - d = adaptor.initialize_store(store) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 0)) - - # this should create harrison document - d.addCallback(get_or_create_actor_harry) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 1)) - - # this should get us harrison document - d.addCallback(get_or_create_actor_harry) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 1)) - - # create director harry, should create new doc - d.addCallback(create_director_harry) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 2)) - - # this should get us harrison document, still 2 docs - d.addCallback(get_or_create_actor_harry) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 2)) - return d - - def test_get_all(self): - adaptor = TestAdaptor() - store = self._soledad - actor_names = ["harry", "carrie", "mark", "david"] - - def create_some_actors(ignored): - deferreds = [] - for name in actor_names: - dw = ActorWrapper.get_or_create( - store, 'by-type-and-name', name) - deferreds.append(dw) - return defer.gatherResults(deferreds) - - d = adaptor.initialize_store(store) - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 0)) - - d.addCallback(create_some_actors) - - d.addCallback(lambda _: store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 4)) - - def assert_actor_list_is_expected(res): - got = set([actor.name for actor in res]) - expected = set(actor_names) - self.assertEqual(got, expected) - - d.addCallback(lambda _: ActorWrapper.get_all(store)) - d.addCallback(assert_actor_list_is_expected) - return d - -HERE = os.path.split(os.path.abspath(__file__))[0] - - -class MessageClass(object): - def __init__(self, wrapper, uid): - self.wrapper = wrapper - self.uid = uid - - def get_wrapper(self): - return self.wrapper - - -class SoledadMailAdaptorTestCase(SoledadTestMixin): - """ - Tests for the SoledadMailAdaptor. - """ - - def get_adaptor(self): - adaptor = SoledadMailAdaptor() - adaptor.store = self._soledad - return adaptor - - def assert_num_docs(self, num, docs): - self.assertEqual(len(docs[1]), num) - - def test_mail_adaptor_init(self): - adaptor = self.get_adaptor() - self.assertTrue(isinstance(adaptor.indexes, dict)) - self.assertTrue(len(adaptor.indexes) != 0) - - # Messages - - def test_get_msg_from_string(self): - adaptor = self.get_adaptor() - - with open(os.path.join(HERE, "rfc822.message")) as f: - raw = f.read() - - msg = adaptor.get_msg_from_string(MessageClass, raw) - - chash = ("D27B2771C0DCCDCB468EE65A4540438" - "09DBD11588E87E951545BE0CBC321C308") - phash = ("64934534C1C80E0D4FA04BE1CCBA104" - "F07BCA5F469C86E2C0ABE1D41310B7299") - subject = ("[Twisted-commits] rebuild now works on " - "python versions from 2.2.0 and up.") - self.assertTrue(msg.wrapper.fdoc is not None) - self.assertTrue(msg.wrapper.hdoc is not None) - self.assertTrue(msg.wrapper.cdocs is not None) - self.assertEquals(len(msg.wrapper.cdocs), 1) - self.assertEquals(msg.wrapper.fdoc.chash, chash) - self.assertEquals(msg.wrapper.fdoc.size, 3837) - self.assertEquals(msg.wrapper.hdoc.chash, chash) - self.assertEqual(dict(msg.wrapper.hdoc.headers)['Subject'], - subject) - self.assertEqual(msg.wrapper.hdoc.subject, subject) - self.assertEqual(msg.wrapper.cdocs[1].phash, phash) - - def test_get_msg_from_string_multipart(self): - msg = MIMEMultipart() - msg['Subject'] = 'Test multipart mail' - msg.attach(MIMEText(u'a utf8 message', _charset='utf-8')) - adaptor = self.get_adaptor() - - msg = adaptor.get_msg_from_string(MessageClass, msg.as_string()) - - self.assertEqual( - 'base64', msg.wrapper.cdocs[1].content_transfer_encoding) - self.assertEqual( - 'text/plain', msg.wrapper.cdocs[1].content_type) - self.assertEqual( - 'YSB1dGY4IG1lc3NhZ2U=\n', msg.wrapper.cdocs[1].raw) - - def test_get_msg_from_docs(self): - adaptor = self.get_adaptor() - mdoc = dict( - fdoc="F-Foobox-deadbeef", - hdoc="H-deadbeef", - cdocs=["C-deadabad"]) - fdoc = dict( - mbox_uuid="Foobox", - flags=('\Seen', '\Nice'), - tags=('Personal', 'TODO'), - seen=False, deleted=False, - recent=False, multi=False) - hdoc = dict( - chash="deadbeef", - subject="Test Msg") - cdocs = { - 1: dict( - raw='This is a test message')} - - msg = adaptor.get_msg_from_docs( - MessageClass, mdoc, fdoc, hdoc, cdocs=cdocs) - self.assertEqual(msg.wrapper.fdoc.flags, - ('\Seen', '\Nice')) - self.assertEqual(msg.wrapper.fdoc.tags, - ('Personal', 'TODO')) - self.assertEqual(msg.wrapper.fdoc.mbox_uuid, "Foobox") - self.assertEqual(msg.wrapper.hdoc.multi, False) - self.assertEqual(msg.wrapper.hdoc.subject, - "Test Msg") - self.assertEqual(msg.wrapper.cdocs[1].raw, - "This is a test message") - - def test_get_msg_from_metamsg_doc_id(self): - # TODO complete-me! - pass - - test_get_msg_from_metamsg_doc_id.skip = "Not yet implemented" - - def test_create_msg(self): - adaptor = self.get_adaptor() - - with open(os.path.join(HERE, "rfc822.message")) as f: - raw = f.read() - msg = adaptor.get_msg_from_string(MessageClass, raw) - - def check_create_result(created): - # that's one mdoc, one hdoc, one fdoc, one cdoc - self.assertEqual(len(created), 4) - for doc in created: - self.assertTrue( - doc.__class__.__name__, - "SoledadDocument") - - d = adaptor.create_msg(adaptor.store, msg) - d.addCallback(check_create_result) - return d - - def test_update_msg(self): - adaptor = self.get_adaptor() - with open(os.path.join(HERE, "rfc822.message")) as f: - raw = f.read() - - def assert_msg_has_doc_id(ignored, msg): - wrapper = msg.get_wrapper() - self.assertTrue(wrapper.fdoc.doc_id is not None) - - def assert_msg_has_no_flags(ignored, msg): - wrapper = msg.get_wrapper() - self.assertEqual(wrapper.fdoc.flags, []) - - def update_msg_flags(ignored, msg): - wrapper = msg.get_wrapper() - wrapper.fdoc.flags = ["This", "That"] - return wrapper.update(adaptor.store) - - def assert_msg_has_flags(ignored, msg): - wrapper = msg.get_wrapper() - self.assertEqual(wrapper.fdoc.flags, ["This", "That"]) - - def get_fdoc_and_check_flags(ignored): - def assert_doc_has_flags(doc): - self.assertEqual(doc.content['flags'], - ['This', 'That']) - wrapper = msg.get_wrapper() - d = adaptor.store.get_doc(wrapper.fdoc.doc_id) - d.addCallback(assert_doc_has_flags) - return d - - msg = adaptor.get_msg_from_string(MessageClass, raw) - d = adaptor.create_msg(adaptor.store, msg) - d.addCallback(lambda _: adaptor.store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 4)) - d.addCallback(assert_msg_has_doc_id, msg) - d.addCallback(assert_msg_has_no_flags, msg) - - # update it! - d.addCallback(update_msg_flags, msg) - d.addCallback(assert_msg_has_flags, msg) - d.addCallback(get_fdoc_and_check_flags) - return d - - # Mailboxes - - def test_get_or_create_mbox(self): - adaptor = self.get_adaptor() - - def get_or_create_mbox(ignored): - d = adaptor.get_or_create_mbox(adaptor.store, "Trash") - return d - - def assert_good_doc(mbox_wrapper): - self.assertTrue(mbox_wrapper.doc_id is not None) - self.assertEqual(mbox_wrapper.mbox, "Trash") - self.assertEqual(mbox_wrapper.type, "mbox") - self.assertEqual(mbox_wrapper.closed, False) - self.assertEqual(mbox_wrapper.subscribed, False) - - d = adaptor.initialize_store(adaptor.store) - d.addCallback(get_or_create_mbox) - d.addCallback(assert_good_doc) - d.addCallback(lambda _: adaptor.store.get_all_docs()) - d.addCallback(partial(self.assert_num_docs, 1)) - return d - - def test_update_mbox(self): - adaptor = self.get_adaptor() - - wrapper_ref = [] - - def get_or_create_mbox(ignored): - d = adaptor.get_or_create_mbox(adaptor.store, "Trash") - return d - - def update_wrapper(wrapper, wrapper_ref): - wrapper_ref.append(wrapper) - wrapper.subscribed = True - wrapper.closed = True - d = adaptor.update_mbox(adaptor.store, wrapper) - return d - - def get_mbox_doc_and_check_flags(res, wrapper_ref): - wrapper = wrapper_ref[0] - - def assert_doc_has_flags(doc): - self.assertEqual(doc.content['subscribed'], True) - self.assertEqual(doc.content['closed'], True) - d = adaptor.store.get_doc(wrapper.doc_id) - d.addCallback(assert_doc_has_flags) - return d - - d = adaptor.initialize_store(adaptor.store) - d.addCallback(get_or_create_mbox) - d.addCallback(update_wrapper, wrapper_ref) - d.addCallback(get_mbox_doc_and_check_flags, wrapper_ref) - return d - - def test_get_all_mboxes(self): - adaptor = self.get_adaptor() - mboxes = ("Sent", "Trash", "Personal", "ListFoo") - - def get_or_create_mboxes(ignored): - d = [] - for mbox in mboxes: - d.append(adaptor.get_or_create_mbox( - adaptor.store, mbox)) - return defer.gatherResults(d) - - def get_all_mboxes(ignored): - return adaptor.get_all_mboxes(adaptor.store) - - def assert_mboxes_match_expected(wrappers): - names = [m.mbox for m in wrappers] - self.assertEqual(set(names), set(mboxes)) - - d = adaptor.initialize_store(adaptor.store) - d.addCallback(get_or_create_mboxes) - d.addCallback(get_all_mboxes) - d.addCallback(assert_mboxes_match_expected) - return d diff --git a/mail/src/leap/mail/constants.py b/mail/src/leap/mail/constants.py deleted file mode 100644 index 4ef42cb..0000000 --- a/mail/src/leap/mail/constants.py +++ /dev/null @@ -1,52 +0,0 @@ -# *- coding: utf-8 -*- -# constants.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 <http://www.gnu.org/licenses/>. -""" -Constants for leap.mail. -""" - -INBOX_NAME = "INBOX" - -# Regular expressions for the identifiers to be used in the Message Data Layer. - -METAMSGID = "M-{mbox_uuid}-{chash}" -METAMSGID_RE = "M\-{mbox_uuid}\-[0-9a-fA-F]+" -METAMSGID_CHASH_RE = "M\-\w+\-([0-9a-fA-F]+)" -METAMSGID_MBOX_RE = "M\-(\w+)\-[0-9a-fA-F]+" - -FDOCID = "F-{mbox_uuid}-{chash}" -FDOCID_RE = "F\-{mbox_uuid}\-[0-9a-fA-F]+" -FDOCID_CHASH_RE = "F\-\w+\-([0-9a-fA-F]+)" - -HDOCID = "H-{chash}" -HDOCID_RE = "H\-[0-9a-fA-F]+" - -CDOCID = "C-{phash}" -CDOCID_RE = "C\-[0-9a-fA-F]+" - - -class MessageFlags(object): - """ - Flags used in Message and Mailbox. - """ - SEEN_FLAG = "\\Seen" - RECENT_FLAG = "\\Recent" - ANSWERED_FLAG = "\\Answered" - FLAGGED_FLAG = "\\Flagged" # yo dawg - DELETED_FLAG = "\\Deleted" - DRAFT_FLAG = "\\Draft" - NOSELECT_FLAG = "\\Noselect" - LIST_FLAG = "List" # is this OK? (no \. ie, no system flag) diff --git a/mail/src/leap/mail/cred.py b/mail/src/leap/mail/cred.py deleted file mode 100644 index 7eab1f0..0000000 --- a/mail/src/leap/mail/cred.py +++ /dev/null @@ -1,80 +0,0 @@ -# -*- coding: utf-8 -*- -# cred.py -# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>. -""" -Credentials handling. -""" - -from zope.interface import implementer -from twisted.cred.checkers import ICredentialsChecker -from twisted.cred.credentials import IUsernamePassword -from twisted.cred.error import UnauthorizedLogin -from twisted.internet import defer - - -@implementer(ICredentialsChecker) -class LocalSoledadTokenChecker(object): - - """ - A Credentials Checker for a LocalSoledad store. - - It checks that: - - 1) The Local SoledadStorage has been correctly unlocked for the given - user. This currently means that the right passphrase has been passed - to the Local SoledadStorage. - - 2) The password passed in the credentials matches whatever token has - been stored in the local encrypted SoledadStorage, associated to the - Protocol that is requesting the authentication. - """ - - credentialInterfaces = (IUsernamePassword,) - service = None - - def __init__(self, soledad_sessions): - """ - :param soledad_sessions: a dict-like object, containing instances - of a Store (soledad instances), indexed by - userid. - """ - self._soledad_sessions = soledad_sessions - - def requestAvatarId(self, credentials): - if self.service is None: - raise NotImplementedError( - "this checker has not defined its service name") - username, password = credentials.username, credentials.password - d = self.checkSoledadToken(username, password, self.service) - d.addErrback(lambda f: defer.fail(UnauthorizedLogin())) - return d - - def checkSoledadToken(self, username, password, service): - soledad = self._soledad_sessions.get(username) - if not soledad: - return defer.fail(Exception("No soledad")) - - def match_token(token): - if token is None: - raise RuntimeError('no token') - if token == password: - return username - else: - raise RuntimeError('bad token') - - d = soledad.get_or_create_service_token(service) - d.addCallback(match_token) - return d diff --git a/mail/src/leap/mail/decorators.py b/mail/src/leap/mail/decorators.py deleted file mode 100644 index 5105de9..0000000 --- a/mail/src/leap/mail/decorators.py +++ /dev/null @@ -1,149 +0,0 @@ -# -*- coding: utf-8 -*- -# decorators.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 <http://www.gnu.org/licenses/>. -""" -Useful decorators for mail package. -""" -import logging -import os - -from functools import wraps - -from twisted.internet.threads import deferToThread - - -logger = logging.getLogger(__name__) - - -# TODO -# Should write a helper to be able to pass a timeout argument. -# See this answer: http://stackoverflow.com/a/19019648/1157664 -# And the notes by glyph and jpcalderone - -def deferred_to_thread(f): - """ - Decorator, for deferring methods to Threads. - - It will do a deferToThread of the decorated method - unless the environment variable LEAPMAIL_DEBUG is set. - - It uses a descriptor to delay the definition of the - method wrapper. - """ - class descript(object): - """ - The class to be used as decorator. - - It takes any method as the passed object. - """ - - def __init__(self, f): - """ - Initializes the decorator object. - - :param f: the decorated function - :type f: callable - """ - self.f = f - - def __get__(self, instance, klass): - """ - Descriptor implementation. - - At creation time, the decorated `method` is unbound. - - It will dispatch the make_unbound method if we still do not - have an instance available, and the make_bound method when the - method has already been bound to the instance. - - :param instance: the instance of the class, or None if not exist. - :type instance: instantiated class or None. - """ - if instance is None: - # Class method was requested - return self.make_unbound(klass) - return self.make_bound(instance) - - def _errback(self, failure): - """ - Errorback that logs the exception catched. - - :param failure: a twisted failure - :type failure: Failure - """ - logger.warning('Error in method: %s' % (self.f.__name__)) - logger.exception(failure.getTraceback()) - - def make_unbound(self, klass): - """ - Return a wrapped function with the unbound call, during the - early access to the decortad method. This gets passed - only the class (not the instance since it does not yet exist). - - :param klass: the class to which the still unbound method belongs - :type klass: type - """ - - @wraps(self.f) - def wrapper(*args, **kwargs): - """ - We're temporarily wrapping the decorated method, but this - should not be called, since our application should use - the bound-wrapped method after this decorator class has been - used. - - This documentation will vanish at runtime. - """ - raise TypeError( - 'unbound method {}() must be called with {} instance ' - 'as first argument (got nothing instead)'.format( - self.f.__name__, - klass.__name__) - ) - return wrapper - - def make_bound(self, instance): - """ - Return a function that wraps the bound method call, - after we are able to access the instance object. - - :param instance: an instance of the class the decorated method, - now bound, belongs to. - :type instance: object - """ - - @wraps(self.f) - def wrapper(*args, **kwargs): - """ - Do a proper function wrapper that defers the decorated method - call to a separated thread if the LEAPMAIL_DEBUG - environment variable is set. - - This documentation will vanish at runtime. - """ - if not os.environ.get('LEAPMAIL_DEBUG'): - d = deferToThread(self.f, instance, *args, **kwargs) - d.addErrback(self._errback) - return d - else: - return self.f(instance, *args, **kwargs) - - # This instance does not need the descriptor anymore, - # let it find the wrapper directly next time: - setattr(instance, self.f.__name__, wrapper) - return wrapper - - return descript(f) diff --git a/mail/src/leap/mail/errors.py b/mail/src/leap/mail/errors.py deleted file mode 100644 index 2f18e87..0000000 --- a/mail/src/leap/mail/errors.py +++ /dev/null @@ -1,27 +0,0 @@ -# -*- coding: utf-8 -*- -# errors.py -# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>. -""" -Exceptions for leap.mail -""" - - -class AuthenticationError(Exception): - pass - - -class ConfigurationError(Exception): - pass diff --git a/mail/src/leap/mail/generator.py b/mail/src/leap/mail/generator.py deleted file mode 100644 index bb3f26e..0000000 --- a/mail/src/leap/mail/generator.py +++ /dev/null @@ -1,23 +0,0 @@ -from email.generator import Generator as EmailGenerator - - -class Generator(EmailGenerator): - """ - Generates output from a Message object tree, keeping signatures. - - This code was extracted from Mailman.Generator.Generator, version 2.1.4: - - Most other Generator will be created not setting the foldheader flag, - as we do not overwrite clone(). The original clone() does not - set foldheaders. - - So you need to set foldheaders if you want the toplevel to fold headers - - TODO: Python 3.3 is patched against this problems. See issue 1590744 on - python bug tracker. - """ - def _write_headers(self, msg): - for h, v in msg.items(): - print >> self._fp, '%s:' % h, - print >> self._fp, v - print >> self._fp diff --git a/mail/src/leap/mail/imap/__init__.py b/mail/src/leap/mail/imap/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/mail/src/leap/mail/imap/__init__.py +++ /dev/null diff --git a/mail/src/leap/mail/imap/account.py b/mail/src/leap/mail/imap/account.py deleted file mode 100644 index e795c1b..0000000 --- a/mail/src/leap/mail/imap/account.py +++ /dev/null @@ -1,498 +0,0 @@ -# -*- coding: utf-8 -*- -# account.py -# Copyright (C) 2013-2015 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 <http://www.gnu.org/licenses/>. -""" -Soledad Backed IMAP Account. -""" -import logging -import os -import time -from functools import partial - -from twisted.internet import defer -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.constants import MessageFlags -from leap.mail.mail import Account -from leap.mail.imap.mailbox import IMAPMailbox, normalize_mailbox -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 IMAP Account -####################################### - - -class IMAPAccount(object): - """ - An implementation of an imap4 Account - that is backed by Soledad Encrypted Documents. - """ - - implements(imap4.IAccount, imap4.INamespacePresenter) - - selected = None - - def __init__(self, store, user_id, d=defer.Deferred()): - """ - Keeps track of the mailboxes and subscriptions handled by this account. - - The account is not ready to be used, since the store needs to be - initialized and we also need to do some initialization routines. - You can either pass a deferred to this constructor, or use - `callWhenReady` method. - - :param store: a Soledad instance. - :type store: Soledad - - :param user_id: The identifier of the user this account belongs to - (user id, in the form user@provider). - :type user_id: str - - - :param d: a deferred that will be fired with this IMAPAccount instance - when the account is ready to be used. - :type d: defer.Deferred - """ - leap_assert(store, "Need a store instance to initialize") - leap_assert_type(store, Soledad) - - # TODO assert too that the name matches the user/uuid with which - # soledad has been initialized. Although afaik soledad doesn't know - # about user_id, only the client backend. - - self.user_id = user_id - self.account = Account( - store, user_id, ready_cb=lambda: d.callback(self)) - - def end_session(self): - """ - Used to mark when the session has closed, and we should not allow any - more commands from the client. - - Right now it's called from the client backend. - """ - # TODO move its use to the service shutdown in leap.mail - self.account.end_session() - - @property - def session_ended(self): - return self.account.session_ended - - def callWhenReady(self, cb, *args, **kw): - """ - Execute callback when the account is ready to be used. - XXX note that this callback will be called with a first ignored - parameter. - """ - # TODO ignore the first parameter and change tests accordingly. - d = self.account.callWhenReady(cb, *args, **kw) - return d - - def getMailbox(self, name): - """ - Return a Mailbox with that name, without selecting it. - - :param name: name of the mailbox - :type name: str - - :returns: an IMAPMailbox instance - :rtype: IMAPMailbox - """ - name = normalize_mailbox(name) - - def check_it_exists(mailboxes): - if name not in mailboxes: - raise imap4.MailboxException("No such mailbox: %r" % name) - return True - - d = self.account.list_all_mailbox_names() - d.addCallback(check_it_exists) - d.addCallback(lambda _: self.account.get_collection_by_mailbox(name)) - d.addCallback(self._return_mailbox_from_collection) - return d - - def _return_mailbox_from_collection(self, collection, readwrite=1): - if collection is None: - return None - mbox = IMAPMailbox(collection, rw=readwrite) - return mbox - - # - # 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: a Deferred that will contain the document if successful. - :rtype: defer.Deferred - """ - name = normalize_mailbox(name) - - # FIXME --- return failure instead of AssertionError - # See AccountTestCase... - leap_assert(name, "Need a mailbox name to create a mailbox") - - def check_it_does_not_exist(mailboxes): - if name in mailboxes: - raise imap4.MailboxCollision, repr(name) - return mailboxes - - d = self.account.list_all_mailbox_names() - d.addCallback(check_it_does_not_exist) - d.addCallback(lambda _: self.account.add_mailbox( - name, creation_ts=creation_ts)) - d.addCallback(lambda _: self.account.get_collection_by_mailbox(name)) - d.addCallback(self._return_mailbox_from_collection) - return d - - 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 deferred that will fire with a true value if the creation - succeeds. The deferred might fail with a MailboxException - if the mailbox cannot be added. - :rtype: Deferred - - """ - def pass_on_collision(failure): - failure.trap(imap4.MailboxCollision) - return True - - def handle_collision(failure): - failure.trap(imap4.MailboxCollision) - if not pathspec.endswith('/'): - return defer.succeed(False) - else: - return defer.succeed(True) - - def all_good(result): - return all(result) - - paths = filter(None, normalize_mailbox(pathspec).split('/')) - subs = [] - sep = '/' - - for accum in range(1, len(paths)): - partial_path = sep.join(paths[:accum]) - d = self.addMailbox(partial_path) - d.addErrback(pass_on_collision) - subs.append(d) - - df = self.addMailbox(sep.join(paths)) - df.addErrback(handle_collision) - subs.append(df) - - d1 = defer.gatherResults(subs) - d1.addCallback(all_good) - return d1 - - 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: IMAPMailbox - """ - name = normalize_mailbox(name) - - def check_it_exists(mailboxes): - if name not in mailboxes: - logger.warning("SELECT: No such mailbox!") - return None - return name - - def set_selected(_): - self.selected = name - - def get_collection(name): - if name is None: - return None - return self.account.get_collection_by_mailbox(name) - - d = self.account.list_all_mailbox_names() - d.addCallback(check_it_exists) - d.addCallback(get_collection) - d.addCallback(partial( - self._return_mailbox_from_collection, readwrite=readwrite)) - return d - - def delete(self, name, force=False): - """ - Deletes a mailbox. - - :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 - :rtype: Deferred - """ - name = normalize_mailbox(name) - _mboxes = None - - def check_it_exists(mailboxes): - global _mboxes - _mboxes = mailboxes - if name not in mailboxes: - raise imap4.MailboxException("No such mailbox: %r" % name) - - def get_mailbox(_): - return self.getMailbox(name) - - def destroy_mailbox(mbox): - return mbox.destroy() - - def check_can_be_deleted(mbox): - global _mboxes - # See if this box is flagged \Noselect - mbox_flags = mbox.getFlags() - if MessageFlags.NOSELECT_FLAG in mbox_flags: - # Check for hierarchically inferior mailboxes with this one - # as part of their root. - for others in _mboxes: - if others != name and others.startswith(name): - raise imap4.MailboxException( - "Hierarchically inferior mailboxes " - "exist and \\Noselect is set") - return mbox - - d = self.account.list_all_mailbox_names() - d.addCallback(check_it_exists) - d.addCallback(get_mailbox) - if not force: - d.addCallback(check_can_be_deleted) - d.addCallback(destroy_mailbox) - return d - - # FIXME --- not honoring the inferior names... - # if there are no hierarchically inferior names, we will - # delete it from our ken. - # XXX is this right? - # if self._inferiorNames(name) > 1: - # 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 = normalize_mailbox(oldname) - newname = normalize_mailbox(newname) - - def rename_inferiors((inferiors, mailboxes)): - rename_deferreds = [] - inferiors = [ - (o, o.replace(oldname, newname, 1)) for o in inferiors] - - for (old, new) in inferiors: - if new in mailboxes: - raise imap4.MailboxCollision(repr(new)) - - for (old, new) in inferiors: - d = self.account.rename_mailbox(old, new) - rename_deferreds.append(d) - - d1 = defer.gatherResults(rename_deferreds, consumeErrors=True) - return d1 - - d1 = self._inferiorNames(oldname) - d2 = self.account.list_all_mailbox_names() - - d = defer.gatherResults([d1, d2]) - d.addCallback(rename_inferiors) - return d - - def _inferiorNames(self, name): - """ - Return hierarchically inferior mailboxes. - - :param name: name of the mailbox - :rtype: list - """ - # XXX use wildcard query instead - def filter_inferiors(mailboxes): - inferiors = [] - for infname in mailboxes: - if infname.startswith(name): - inferiors.append(infname) - return inferiors - - d = self.account.list_all_mailbox_names() - d.addCallback(filter_inferiors) - return d - - 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 - """ - wildcard = imap4.wildcardToRegexp(wildcard, '/') - - def get_list(mboxes, mboxes_names): - return zip(mboxes_names, mboxes) - - def filter_inferiors(ref): - mboxes = [mbox for mbox in ref if wildcard.match(mbox)] - mbox_d = defer.gatherResults([self.getMailbox(m) for m in mboxes]) - - mbox_d.addCallback(get_list, mboxes) - return mbox_d - - d = self._inferiorNames(normalize_mailbox(ref)) - d.addCallback(filter_inferiors) - return d - - # - # The rest of the methods are specific for leap.mail.imap.account.Account - # - - def isSubscribed(self, name): - """ - Returns True if user is subscribed to this mailbox. - - :param name: the mailbox to be checked. - :type name: str - - :rtype: Deferred (will fire with bool) - """ - name = normalize_mailbox(name) - - def get_subscribed(mbox): - return mbox.collection.get_mbox_attr("subscribed") - - d = self.getMailbox(name) - d.addCallback(get_subscribed) - return d - - def subscribe(self, name): - """ - Subscribe to this mailbox if not already subscribed. - - :param name: name of the mailbox - :type name: str - :rtype: Deferred - """ - name = normalize_mailbox(name) - - def set_subscribed(mbox): - return mbox.collection.set_mbox_attr("subscribed", True) - - d = self.getMailbox(name) - d.addCallback(set_subscribed) - return d - - def unsubscribe(self, name): - """ - Unsubscribe from this mailbox - - :param name: name of the mailbox - :type name: str - :rtype: Deferred - """ - # TODO should raise MailboxException if attempted to unsubscribe - # from a mailbox that is not currently subscribed. - # TODO factor out with subscribe method. - name = normalize_mailbox(name) - - def set_unsubscribed(mbox): - return mbox.collection.set_mbox_attr("subscribed", False) - - d = self.getMailbox(name) - d.addCallback(set_unsubscribed) - return d - - def getSubscriptions(self): - def get_subscribed(mailboxes): - return [x.mbox for x in mailboxes if x.subscribed] - - d = self.account.get_all_mailboxes() - d.addCallback(get_subscribed) - return d - - # - # INamespacePresenter - # - - def getPersonalNamespaces(self): - return [["", "/"]] - - def getSharedNamespaces(self): - return None - - def getOtherNamespaces(self): - return None - - def __repr__(self): - """ - Representation string for this object. - """ - return "<IMAPAccount (%s)>" % self.user_id diff --git a/mail/src/leap/mail/imap/mailbox.py b/mail/src/leap/mail/imap/mailbox.py deleted file mode 100644 index e70a1d8..0000000 --- a/mail/src/leap/mail/imap/mailbox.py +++ /dev/null @@ -1,970 +0,0 @@ -# *- coding: utf-8 -*- -# mailbox.py -# Copyright (C) 2013-2015 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 <http://www.gnu.org/licenses/>. -""" -IMAP Mailbox. -""" -import re -import logging -import os -import cStringIO -import StringIO -import time - -from collections import defaultdict -from email.utils import formatdate - -from twisted.internet import defer -from twisted.internet import reactor -from twisted.python import log - -from twisted.mail import imap4 -from zope.interface import implements - -from leap.common.check import leap_assert -from leap.common.check import leap_assert_type -from leap.mail.constants import INBOX_NAME, MessageFlags -from leap.mail.imap.messages import IMAPMessage - -logger = logging.getLogger(__name__) - -# TODO LIST -# [ ] Restore profile_cmd instrumentation -# [ ] finish the implementation of IMailboxListener -# [ ] implement the rest of ISearchableMailbox - - -""" -If the environment variable `LEAP_SKIPNOTIFY` is set, we avoid -notifying clients of new messages. Use during stress tests. -""" -NOTIFY_NEW = not os.environ.get('LEAP_SKIPNOTIFY', False) -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 - - def do_profile_cmd(d, name): - """ - Add the profiling debug to the passed callback. - :param d: deferred - :param name: name of the command - :type name: str - """ - d.addCallback(_debugProfiling, name, time.time()) - d.addErrback(lambda f: log.msg(f.getTraceback())) - -INIT_FLAGS = (MessageFlags.SEEN_FLAG, MessageFlags.ANSWERED_FLAG, - MessageFlags.FLAGGED_FLAG, MessageFlags.DELETED_FLAG, - MessageFlags.DRAFT_FLAG, MessageFlags.RECENT_FLAG, - MessageFlags.LIST_FLAG) - - -def make_collection_listener(mailbox): - """ - Wrap a mailbox in a class that can be hashed according to the mailbox name. - - This means that dicts or sets will use this new equality rule, so we won't - collect multiple instances of the same mailbox in collections like the - MessageCollection set where we keep track of listeners. - """ - - class HashableMailbox(object): - - def __init__(self, mbox): - self.mbox = mbox - - # See #8083, pixelated adaptor seems to be misusing this class. - self.mailbox_name = self.mbox.mbox_name - - def __hash__(self): - return hash(self.mbox.mbox_name) - - def __eq__(self, other): - return self.mbox.mbox_name == other.mbox.mbox_name - - def notify_new(self): - self.mbox.notify_new() - - return HashableMailbox(mailbox) - - -class IMAPMailbox(object): - """ - A Soledad-backed IMAP mailbox. - - Implements the high-level method needed for the Mailbox interfaces. - The low-level database methods are contained in the generic - MessageCollection class. We receive an instance of it and it is made - accessible in the `collection` attribute. - """ - implements( - imap4.IMailbox, - imap4.IMailboxInfo, - imap4.ISearchableMailbox, - # XXX I think we do not need to implement CloseableMailbox, do we? - # We could remove ourselves from the collectionListener, although I - # think it simply will be garbage collected. - # imap4.ICloseableMailbox - imap4.IMessageCopier) - - init_flags = INIT_FLAGS - - CMD_MSG = "MESSAGES" - CMD_RECENT = "RECENT" - CMD_UIDNEXT = "UIDNEXT" - CMD_UIDVALIDITY = "UIDVALIDITY" - CMD_UNSEEN = "UNSEEN" - - # TODO we should turn this into a datastructure with limited capacity - _listeners = defaultdict(set) - - def __init__(self, collection, rw=1): - """ - :param collection: instance of MessageCollection - :type collection: MessageCollection - - :param rw: read-and-write flag for this mailbox - :type rw: int - """ - self.rw = rw - self._uidvalidity = None - self.collection = collection - self.collection.addListener(make_collection_listener(self)) - - @property - def mbox_name(self): - return self.collection.mbox_name - - @property - def listeners(self): - """ - Returns listeners for this mbox. - - The server itself is a listener to the mailbox. - so we can notify it (and should!) after changes in flags - and number of messages. - - :rtype: set - """ - return self._listeners[self.mbox_name] - - def get_imap_message(self, message): - d = defer.Deferred() - IMAPMessage(message, store=self.collection.store, d=d) - return d - - # FIXME this grows too crazily when many instances are fired, like - # during imaptest stress testing. Should have a queue of limited size - # instead. - - def addListener(self, listener): - """ - Add a listener to the listeners queue. - The server adds itself as a listener when there is a SELECT, - so it can send EXIST commands. - - :param listener: listener to add - :type listener: an object that implements IMailboxListener - """ - if not NOTIFY_NEW: - return - - listeners = self.listeners - logger.debug('adding mailbox listener: %s. Total: %s' % ( - listener, len(listeners))) - listeners.add(listener) - - def removeListener(self, listener): - """ - Remove a listener from the listeners queue. - - :param listener: listener to remove - :type listener: an object that implements IMailboxListener - """ - self.listeners.remove(listener) - - def getFlags(self): - """ - Returns the flags defined for this mailbox. - - :returns: tuple of flags for this mailbox - :rtype: tuple of str - """ - flags = self.collection.mbox_wrapper.flags - if not flags: - flags = self.init_flags - flags_str = map(str, flags) - return flags_str - - def setFlags(self, flags): - """ - Sets flags for this mailbox. - - :param flags: a tuple with the flags - :type flags: tuple of str - """ - # XXX this is setting (overriding) old flags. - # Better pass a mode flag - leap_assert(isinstance(flags, tuple), - "flags expected to be a tuple") - return self.collection.set_mbox_attr("flags", flags) - - def getUIDValidity(self): - """ - Return the unique validity identifier for this mailbox. - - :return: unique validity identifier - :rtype: int - """ - return self.collection.get_mbox_attr("created") - - def getUID(self, message_number): - """ - Return the UID of a message in the mailbox - - .. note:: this implementation does not make much sense RIGHT NOW, - but in the future will be useful to get absolute UIDs from - message sequence numbers. - - - :param message: the message sequence number. - :type message: int - - :rtype: int - :return: the UID of the message. - - """ - # TODO support relative sequences. The (imap) message should - # receive a sequence number attribute: a deferred is not expected - return message_number - - def getUIDNext(self): - """ - Return the likely UID for the next message added to this - mailbox. Currently it returns the higher UID incremented by - one. - - :return: deferred with int - :rtype: Deferred - """ - d = self.collection.get_uid_next() - return d - - def getMessageCount(self): - """ - Returns the total count of messages in this mailbox. - - :return: deferred with int - :rtype: Deferred - """ - return self.collection.count() - - def getUnseenCount(self): - """ - Returns the number of messages with the 'Unseen' flag. - - :return: count of messages flagged `unseen` - :rtype: int - """ - return self.collection.count_unseen() - - def getRecentCount(self): - """ - Returns the number of messages with the 'Recent' flag. - - :return: count of messages flagged `recent` - :rtype: int - """ - return self.collection.count_recent() - - def isWriteable(self): - """ - Get the read/write status of the mailbox. - - :return: 1 if mailbox is read-writeable, 0 otherwise. - :rtype: int - """ - # XXX We don't need to store it in the mbox doc, do we? - # return int(self.collection.get_mbox_attr('rw')) - return self.rw - - def getHierarchicalDelimiter(self): - """ - Returns the character used to delimite hierarchies in mailboxes. - - :rtype: str - """ - return '/' - - def requestStatus(self, names): - """ - Handles a status request by gathering the output of the different - status commands. - - :param names: a list of strings containing the status commands - :type names: iter - """ - r = {} - maybe = defer.maybeDeferred - if self.CMD_MSG in names: - r[self.CMD_MSG] = maybe(self.getMessageCount) - if self.CMD_RECENT in names: - r[self.CMD_RECENT] = maybe(self.getRecentCount) - if self.CMD_UIDNEXT in names: - r[self.CMD_UIDNEXT] = maybe(self.getUIDNext) - if self.CMD_UIDVALIDITY in names: - r[self.CMD_UIDVALIDITY] = maybe(self.getUIDValidity) - if self.CMD_UNSEEN in names: - r[self.CMD_UNSEEN] = maybe(self.getUnseenCount) - - def as_a_dict(values): - return dict(zip(r.keys(), values)) - - d = defer.gatherResults(r.values()) - d.addCallback(as_a_dict) - return d - - def addMessage(self, message, flags, date=None, notify_just_mdoc=True): - """ - Adds a message to this mailbox. - - :param message: the raw message - :type message: str - - :param flags: flag list - :type flags: list of str - - :param date: timestamp - :type date: str, or None - - :param notify_just_mdoc: - boolean passed to the wrapper.create method, to indicate whether - we're insterested in being notified right after the mdoc has been - written (as it's the first doc to be written, and quite small, this - is faster, though potentially unsafe). - Setting it to True improves a *lot* the responsiveness of the - APPENDS: we just need to be notified when the mdoc is saved, and - let's just expect that the other parts are doing just fine. This - will not catch any errors when the inserts of the other parts - fail, but on the other hand allows us to return very quickly, - which seems a good compromise given that we have to serialize the - appends. - However, some operations like the saving of drafts need to wait for - all the parts to be saved, so if some heuristics are met down in - the call chain a Draft message will unconditionally set this flag - to False, and therefore ignoring the setting of this flag here. - :type notify_just_mdoc: bool - - :return: a deferred that will be triggered with the UID of the added - message. - """ - # TODO should raise ReadOnlyMailbox if not rw. - # TODO have a look at the cases for internal date in the rfc - # XXX we could treat the message as an IMessage from here - - # TODO change notify_just_mdoc to something more meaningful, like - # fast_insert_notify? - - # TODO notify_just_mdoc *sometimes* make the append tests fail. - # have to find a better solution for this. A workaround could probably - # be to have a list of the ongoing deferreds related to append, so that - # we queue for later all the requests having to do with these. - - # A better solution will probably involve implementing MULTIAPPEND - # extension or patching imap server to support pipelining. - - if isinstance(message, (cStringIO.OutputType, StringIO.StringIO)): - message = message.getvalue() - - leap_assert_type(message, basestring) - - if flags is None: - flags = tuple() - else: - flags = tuple(str(flag) for flag in flags) - - if date is None: - date = formatdate(time.time()) - - d = self.collection.add_msg(message, flags, date=date, - notify_just_mdoc=notify_just_mdoc) - d.addErrback(lambda failure: log.err(failure)) - return d - - def notify_new(self, *args): - """ - Notify of new messages to all the listeners. - - This will be called indirectly by the underlying collection, that will - notify this IMAPMailbox whenever there are changes in the number of - messages in the collection, since we have added ourselves to the - collection listeners. - - :param args: ignored. - """ - if not NOTIFY_NEW: - return - - def cbNotifyNew(result): - exists, recent = result - for listener in self.listeners: - listener.newMessages(exists, recent) - - d = self._get_notify_count() - d.addCallback(cbNotifyNew) - d.addCallback(self.collection.cb_signal_unread_to_ui) - d.addErrback(lambda failure: log.err(failure)) - - def _get_notify_count(self): - """ - Get message count and recent count for this mailbox. - - :return: a deferred that will fire with a tuple, with number of - messages and number of recent messages. - :rtype: Deferred - """ - # XXX this is way too expensive in cases like multiple APPENDS. - # We should have a way of keep a cache or do a self-increment for that - # kind of calls. - d_exists = defer.maybeDeferred(self.getMessageCount) - d_recent = defer.maybeDeferred(self.getRecentCount) - d_list = [d_exists, d_recent] - - def log_num_msg(result): - exists, recent = tuple(result) - logger.debug("NOTIFY (%r): there are %s messages, %s recent" % ( - self.mbox_name, exists, recent)) - return result - - d = defer.gatherResults(d_list) - d.addCallback(log_num_msg) - return d - - # commands, do not rename methods - - def destroy(self): - """ - Called before this mailbox is permanently deleted. - - Should cleanup resources, and set the \\Noselect flag - on the mailbox. - - """ - # XXX this will overwrite all the existing flags - # should better simply addFlag - self.setFlags((MessageFlags.NOSELECT_FLAG,)) - - def remove_mbox(_): - uuid = self.collection.mbox_uuid - d = self.collection.mbox_wrapper.delete(self.collection.store) - d.addCallback( - lambda _: self.collection.mbox_indexer.delete_table(uuid)) - return d - - d = self.deleteAllDocs() - d.addCallback(remove_mbox) - return d - - def expunge(self): - """ - Remove all messages flagged \\Deleted - """ - if not self.isWriteable(): - raise imap4.ReadOnlyMailbox - return self.collection.delete_all_flagged() - - def _get_message_fun(self, uid): - """ - Return the proper method to get a message for this mailbox, depending - on the passed uid flag. - - :param uid: If true, the IDs specified in the query are UIDs; - otherwise they are message sequence IDs. - :type uid: bool - :rtype: callable - """ - get_message_fun = [ - self.collection.get_message_by_sequence_number, - self.collection.get_message_by_uid][uid] - return get_message_fun - - def _get_messages_range(self, messages_asked, uid=True): - - def get_range(messages_asked): - return self._filter_msg_seq(messages_asked) - - d = self._bound_seq(messages_asked, uid) - if uid: - d.addCallback(get_range) - d.addErrback(lambda f: log.err(f)) - return d - - def _bound_seq(self, messages_asked, uid): - """ - Put an upper bound to a messages sequence if this is open. - - :param messages_asked: IDs of the messages. - :type messages_asked: MessageSet - :return: a Deferred that will fire with a MessageSet - """ - - def set_last_uid(last_uid): - messages_asked.last = last_uid - return messages_asked - - def set_last_seq(all_uid): - messages_asked.last = len(all_uid) - return messages_asked - - if not messages_asked.last: - try: - iter(messages_asked) - except TypeError: - # looks like we cannot iterate - if uid: - d = self.collection.get_last_uid() - d.addCallback(set_last_uid) - else: - d = self.collection.all_uid_iter() - d.addCallback(set_last_seq) - return d - return defer.succeed(messages_asked) - - def _filter_msg_seq(self, messages_asked): - """ - Filter a message sequence returning only the ones that do exist in the - collection. - - :param messages_asked: IDs of the messages. - :type messages_asked: MessageSet - :rtype: set - """ - # TODO we could pass the asked sequence to the indexer - # all_uid_iter, and bound the sql query instead. - def filter_by_asked(all_msg_uid): - set_asked = set(messages_asked) - set_exist = set(all_msg_uid) - return set_asked.intersection(set_exist) - - d = self.collection.all_uid_iter() - d.addCallback(filter_by_asked) - return d - - def fetch(self, messages_asked, uid): - """ - Retrieve one or more messages in this mailbox. - - from rfc 3501: The data items to be fetched can be either a single atom - or a parenthesized list. - - :param messages_asked: IDs of the messages to retrieve information - about - :type messages_asked: MessageSet - - :param uid: If true, the IDs are UIDs. They are message sequence IDs - otherwise. - :type uid: bool - - :rtype: deferred with a generator that yields... - """ - get_msg_fun = self._get_message_fun(uid) - getimapmsg = self.get_imap_message - - def get_imap_messages_for_range(msg_range): - - def _get_imap_msg(messages): - d_imapmsg = [] - # just in case we got bad data in here - for msg in filter(None, messages): - d_imapmsg.append(getimapmsg(msg)) - return defer.gatherResults(d_imapmsg, consumeErrors=True) - - def _zip_msgid(imap_messages): - zipped = zip( - list(msg_range), imap_messages) - return (item for item in zipped) - - # XXX not called?? - def _unset_recent(sequence): - reactor.callLater(0, self.unset_recent_flags, sequence) - return sequence - - d_msg = [] - for msgid in msg_range: - # XXX We want cdocs because we "probably" are asked for the - # body. We should be smarter at do_FETCH and pass a parameter - # to this method in order not to prefetch cdocs if they're not - # going to be used. - d_msg.append(get_msg_fun(msgid, get_cdocs=True)) - - d = defer.gatherResults(d_msg, consumeErrors=True) - d.addCallback(_get_imap_msg) - d.addCallback(_zip_msgid) - d.addErrback(lambda failure: log.err(failure)) - return d - - d = self._get_messages_range(messages_asked, uid) - d.addCallback(get_imap_messages_for_range) - d.addErrback(lambda failure: log.err(failure)) - return d - - def fetch_flags(self, messages_asked, uid): - """ - A fast method to fetch all flags, tricking just the - needed subset of the MIME interface that's needed to satisfy - a generic FLAGS query. - - Given how LEAP Mail is supposed to work without local cache, - this query is going to be quite common, and also we expect - it to be in the form 1:* at the beginning of a session, so - it's not bad to fetch all the FLAGS docs at once. - - :param messages_asked: IDs of the messages to retrieve information - about - :type messages_asked: MessageSet - - :param uid: If 1, the IDs are UIDs. They are message sequence IDs - otherwise. - :type uid: int - - :return: A tuple of two-tuples of message sequence numbers and - flagsPart, which is a only a partial implementation of - MessagePart. - :rtype: tuple - """ - # is_sequence = True if uid == 0 else False - # XXX FIXME ----------------------------------------------------- - # imap/tests, or muas like mutt, it will choke until we implement - # sequence numbers. This is an easy hack meanwhile. - is_sequence = False - # --------------------------------------------------------------- - - if is_sequence: - raise NotImplementedError( - "FETCH FLAGS NOT IMPLEMENTED FOR MESSAGE SEQUENCE NUMBERS YET") - - d = defer.Deferred() - reactor.callLater(0, self._do_fetch_flags, messages_asked, uid, d) - if PROFILE_CMD: - do_profile_cmd(d, "FETCH-ALL-FLAGS") - return d - - def _do_fetch_flags(self, messages_asked, uid, d): - """ - :param messages_asked: IDs of the messages to retrieve information - about - :type messages_asked: MessageSet - - :param uid: If 1, the IDs are UIDs. They are message sequence IDs - otherwise. - :type uid: int - :param d: deferred whose callback will be called with result. - :type d: Deferred - - :rtype: A generator that yields two-tuples of message sequence numbers - and flagsPart - """ - class flagsPart(object): - def __init__(self, uid, flags): - self.uid = uid - self.flags = flags - - def getUID(self): - return self.uid - - def getFlags(self): - return map(str, self.flags) - - def pack_flags(result): - _uid, _flags = result - return _uid, flagsPart(_uid, _flags) - - def get_flags_for_seq(sequence): - d_all_flags = [] - for msgid in sequence: - # TODO implement sequence numbers here too - d_flags_per_uid = self.collection.get_flags_by_uid(msgid) - d_flags_per_uid.addCallback(pack_flags) - d_all_flags.append(d_flags_per_uid) - gotflags = defer.gatherResults(d_all_flags) - gotflags.addCallback(get_uid_flag_generator) - return gotflags - - def get_uid_flag_generator(result): - generator = (item for item in result) - d.callback(generator) - - d_seq = self._get_messages_range(messages_asked, uid) - d_seq.addCallback(get_flags_for_seq) - return d_seq - - @defer.inlineCallbacks - def fetch_headers(self, messages_asked, uid): - """ - A fast method to fetch all headers, tricking just the - needed subset of the MIME interface that's needed to satisfy - a generic HEADERS query. - - Given how LEAP Mail is supposed to work without local cache, - this query is going to be quite common, and also we expect - it to be in the form 1:* at the beginning of a session, so - **MAYBE** it's not too bad to fetch all the HEADERS docs at once. - - :param messages_asked: IDs of the messages to retrieve information - about - :type messages_asked: MessageSet - - :param uid: If true, the IDs are UIDs. They are message sequence IDs - otherwise. - :type uid: bool - - :return: A tuple of two-tuples of message sequence numbers and - headersPart, which is a only a partial implementation of - MessagePart. - :rtype: tuple - """ - # TODO implement sequences - is_sequence = True if uid == 0 else False - if is_sequence: - raise NotImplementedError( - "FETCH HEADERS NOT IMPLEMENTED FOR SEQUENCE NUMBER YET") - - class headersPart(object): - def __init__(self, uid, headers): - self.uid = uid - self.headers = headers - - def getUID(self): - return self.uid - - def getHeaders(self, _): - return dict( - (str(key), str(value)) - for key, value in - self.headers.items()) - - messages_asked = yield self._bound_seq(messages_asked, uid) - seq_messg = yield self._filter_msg_seq(messages_asked) - - result = [] - for msgid in seq_messg: - msg = yield self.collection.get_message_by_uid(msgid) - headers = headersPart(msgid, msg.get_headers()) - result.append((msgid, headers)) - defer.returnValue(iter(result)) - - def store(self, messages_asked, flags, mode, uid): - """ - Sets the flags of one or more messages. - - :param messages: The identifiers of the messages to set the flags - :type messages: A MessageSet object with the list of messages requested - - :param flags: The flags to set, unset, or add. - :type flags: sequence of str - - :param mode: If mode is -1, these flags should be removed from the - specified messages. If mode is 1, these flags should be - added to the specified messages. If mode is 0, all - existing flags should be cleared and these flags should be - added. - :type mode: -1, 0, or 1 - - :param uid: If true, the IDs specified in the query are UIDs; - otherwise they are message sequence IDs. - :type uid: bool - - :return: A deferred, that will be called with a dict mapping message - sequence numbers to sequences of str representing the flags - set on the message after this operation has been performed. - :rtype: deferred - - :raise ReadOnlyMailbox: Raised if this mailbox is not open for - read-write. - """ - if not self.isWriteable(): - log.msg('read only mailbox!') - raise imap4.ReadOnlyMailbox - - d = defer.Deferred() - reactor.callLater(0, self._do_store, messages_asked, flags, - mode, uid, d) - if PROFILE_CMD: - do_profile_cmd(d, "STORE") - - d.addCallback(self.collection.cb_signal_unread_to_ui) - d.addErrback(lambda f: log.err(f)) - return d - - def _do_store(self, messages_asked, flags, mode, uid, observer): - """ - Helper method, invoke set_flags method in the IMAPMessageCollection. - - See the documentation for the `store` method for the parameters. - - :param observer: a deferred that will be called with the dictionary - mapping UIDs to flags after the operation has been - done. - :type observer: deferred - """ - # TODO we should prevent client from setting Recent flag - get_msg_fun = self._get_message_fun(uid) - leap_assert(not isinstance(flags, basestring), - "flags cannot be a string") - flags = tuple(flags) - - def set_flags_for_seq(sequence): - def return_result_dict(list_of_flags): - result = dict(zip(list(sequence), list_of_flags)) - observer.callback(result) - return result - - d_all_set = [] - for msgid in sequence: - d = get_msg_fun(msgid) - d.addCallback(lambda msg: self.collection.update_flags( - msg, flags, mode)) - d_all_set.append(d) - got_flags_setted = defer.gatherResults(d_all_set) - got_flags_setted.addCallback(return_result_dict) - return got_flags_setted - - d_seq = self._get_messages_range(messages_asked, uid) - d_seq.addCallback(set_flags_for_seq) - return d_seq - - # ISearchableMailbox - - def search(self, query, uid): - """ - Search for messages that meet the given query criteria. - - Warning: this is half-baked, and it might give problems since - it offers the SearchableInterface. - We'll be implementing it asap. - - :param query: The search criteria - :type query: list - - :param uid: If true, the IDs specified in the query are UIDs; - otherwise they are message sequence IDs. - :type uid: bool - - :return: A list of message sequence numbers or message UIDs which - match the search criteria or a C{Deferred} whose callback - will be invoked with such a list. - :rtype: C{list} or C{Deferred} - """ - # TODO see if we can raise w/o interrupting flow - # :raise IllegalQueryError: Raised when query is not valid. - # example query: - # ['UNDELETED', 'HEADER', 'Message-ID', - # XXX fixme, does not exist - # '52D44F11.9060107@dev.bitmask.net'] - - # TODO hardcoding for now! -- we'll support generic queries later on - # but doing a quickfix for avoiding duplicate saves in the draft - # folder. # See issue #4209 - - if len(query) > 2: - if query[1] == 'HEADER' and query[2].lower() == "message-id": - msgid = str(query[3]).strip() - logger.debug("Searching for %s" % (msgid,)) - - d = self.collection.get_uid_from_msgid(str(msgid)) - d.addCallback(lambda result: [result]) - return d - - # nothing implemented for any other query - logger.warning("Cannot process query: %s" % (query,)) - return [] - - # IMessageCopier - - def copy(self, message): - """ - Copy the given message object into this mailbox. - - :param message: an IMessage implementor - :type message: LeapMessage - :return: a deferred that will be fired with the message - uid when the copy succeed. - :rtype: Deferred - """ - # if PROFILE_CMD: - # do_profile_cmd(d, "COPY") - - # A better place for this would be the COPY/APPEND dispatcher - # in server.py, but qtreactor hangs when I do that, so this seems - # to work fine for now. - # d.addCallback(lambda r: self.reactor.callLater(0, self.notify_new)) - # deferLater(self.reactor, 0, self._do_copy, message, d) - # return d - - d = self.collection.copy_msg(message.message, - self.collection.mbox_uuid) - return d - - # convenience fun - - def deleteAllDocs(self): - """ - Delete all docs in this mailbox - """ - # FIXME not implemented - return self.collection.delete_all_docs() - - def unset_recent_flags(self, uid_seq): - """ - Unset Recent flag for a sequence of UIDs. - """ - # FIXME not implemented - return self.collection.unset_recent_flags(uid_seq) - - def __repr__(self): - """ - Representation string for this mailbox. - """ - return u"<IMAPMailbox: mbox '%s' (%s)>" % ( - self.mbox_name, self.collection.count()) - - -_INBOX_RE = re.compile(INBOX_NAME, re.IGNORECASE) - - -def normalize_mailbox(name): - """ - Return a normalized representation of the mailbox ``name``. - - This method ensures that an eventual initial 'inbox' part of a - mailbox name is made uppercase. - - :param name: the name of the mailbox - :type name: unicode - - :rtype: unicode - """ - # XXX maybe it would make sense to normalize common folders too: - # trash, sent, drafts, etc... - if _INBOX_RE.match(name): - # ensure inital INBOX is uppercase - return INBOX_NAME + name[len(INBOX_NAME):] - return name diff --git a/mail/src/leap/mail/imap/messages.py b/mail/src/leap/mail/imap/messages.py deleted file mode 100644 index d1c7b93..0000000 --- a/mail/src/leap/mail/imap/messages.py +++ /dev/null @@ -1,254 +0,0 @@ -# -*- coding: utf-8 -*- -# imap/messages.py -# Copyright (C) 2013-2015 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 <http://www.gnu.org/licenses/>. -""" -IMAPMessage implementation. -""" -import logging -from twisted.mail import imap4 -from twisted.internet import defer -from zope.interface import implements - -from leap.mail.utils import find_charset, CaseInsensitiveDict - - -logger = logging.getLogger(__name__) - -# TODO -# [ ] Add ref to incoming message during add_msg. - - -class IMAPMessage(object): - """ - The main representation of a message as seen by the IMAP Server. - This class implements the semantics specific to IMAP specification. - """ - implements(imap4.IMessage) - - def __init__(self, message, prefetch_body=True, - store=None, d=defer.Deferred()): - """ - Get an IMAPMessage. A mail.Message is needed, since many of the methods - are proxied to that object. - - - If you do not need to prefetch the body of the message, you can set - `prefetch_body` to False, but the current imap server implementation - expect the getBodyFile method to return inmediately. - - When the prefetch_body option is used, a deferred is also expected as a - parameter, and this will fire when the deferred initialization has - taken place, with this instance of IMAPMessage as a parameter. - - :param message: the abstract message - :type message: mail.Message - :param prefetch_body: Whether to prefetch the content doc for the body. - :type prefetch_body: bool - :param store: an instance of soledad, or anything that behaves like it. - :param d: an optional deferred, that will be fired with the instance of - the IMAPMessage being initialized - :type d: defer.Deferred - """ - # TODO substitute the use of the deferred initialization by a factory - # function, maybe. - - self.message = message - self.__body_fd = None - self.store = store - if prefetch_body: - gotbody = self.__prefetch_body_file() - gotbody.addCallback(lambda _: d.callback(self)) - - # IMessage implementation - - def getUID(self): - """ - Retrieve the unique identifier associated with this Message. - - :return: uid for this message - :rtype: int - """ - return self.message.get_uid() - - def getFlags(self): - """ - Retrieve the flags associated with this Message. - - :return: The flags, represented as strings - :rtype: tuple - """ - return self.message.get_flags() - - def getInternalDate(self): - """ - Retrieve the date internally associated with this message - - According to the spec, this is NOT the date and time in the - RFC-822 header, but rather a date and time that reflects when the - message was received. - - * In SMTP, date and time of final delivery. - * In COPY, internal date/time of the source message. - * In APPEND, date/time specified. - - :return: An RFC822-formatted date string. - :rtype: str - """ - return self.message.get_internal_date() - - # - # IMessagePart - # - - def getBodyFile(self, store=None): - """ - Retrieve a file object containing only the body of this message. - - :return: file-like object opened for reading - :rtype: a deferred that will fire with a StringIO object. - """ - if self.__body_fd is not None: - fd = self.__body_fd - fd.seek(0) - return fd - - if store is None: - store = self.store - return self.message.get_body_file(store) - - def getSize(self): - """ - Return the total size, in octets, of this message. - - :return: size of the message, in octets - :rtype: int - """ - return self.message.get_size() - - def getHeaders(self, negate, *names): - """ - Retrieve a group of message headers. - - :param names: The names of the headers to retrieve or omit. - :type names: tuple of str - - :param negate: If True, indicates that the headers listed in names - should be omitted from the return value, rather - than included. - :type negate: bool - - :return: A mapping of header field names to header field values - :rtype: dict - """ - headers = self.message.get_headers() - return _format_headers(headers, negate, *names) - - def isMultipart(self): - """ - Return True if this message is multipart. - """ - return self.message.is_multipart() - - def getSubPart(self, part): - """ - Retrieve a MIME submessage - - :type part: C{int} - :param part: The number of the part to retrieve, indexed from 0. - :raise IndexError: Raised if the specified part does not exist. - :raise TypeError: Raised if this message is not multipart. - :rtype: Any object implementing C{IMessagePart}. - :return: The specified sub-part. - """ - subpart = self.message.get_subpart(part + 1) - return IMAPMessagePart(subpart) - - def __prefetch_body_file(self): - def assign_body_fd(fd): - self.__body_fd = fd - return fd - d = self.getBodyFile() - d.addCallback(assign_body_fd) - return d - - -class IMAPMessagePart(object): - - def __init__(self, message_part): - self.message_part = message_part - - def getBodyFile(self, store=None): - return self.message_part.get_body_file() - - def getSize(self): - return self.message_part.get_size() - - def getHeaders(self, negate, *names): - headers = self.message_part.get_headers() - return _format_headers(headers, negate, *names) - - def isMultipart(self): - return self.message_part.is_multipart() - - def getSubPart(self, part): - subpart = self.message_part.get_subpart(part + 1) - return IMAPMessagePart(subpart) - - -def _format_headers(headers, negate, *names): - # current server impl. expects content-type to be present, so if for - # some reason we do not have headers, we have to return at least that - # one - if not headers: - logger.warning("No headers found") - return {str('content-type'): str('')} - - names = map(lambda s: s.upper(), names) - - if negate: - def cond(key): - return key.upper() not in names - else: - def cond(key): - return key.upper() in names - - if isinstance(headers, list): - headers = dict(headers) - - # default to most likely standard - charset = find_charset(headers, "utf-8") - - # We will return a copy of the headers dictionary that - # will allow case-insensitive lookups. In some parts of the twisted imap - # server code the keys are expected to be in lower case, and in this way - # we avoid having to convert them. - - _headers = CaseInsensitiveDict() - for key, value in headers.items(): - if not isinstance(key, str): - key = key.encode(charset, 'replace') - if not isinstance(value, str): - value = value.encode(charset, 'replace') - - if value.endswith(";"): - # bastards - value = value[:-1] - - # filter original dict by negate-condition - if cond(key): - _headers[key] = value - - return _headers diff --git a/mail/src/leap/mail/imap/server.py b/mail/src/leap/mail/imap/server.py deleted file mode 100644 index 5a63af0..0000000 --- a/mail/src/leap/mail/imap/server.py +++ /dev/null @@ -1,693 +0,0 @@ -# -*- coding: utf-8 -*- -# server.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 <http://www.gnu.org/licenses/>. -""" -LEAP IMAP4 Server Implementation. -""" -import StringIO -from copy import copy - -from twisted.internet.defer import maybeDeferred -from twisted.mail import imap4 -from twisted.python import log - -# imports for LITERAL+ patch -from twisted.internet import defer, interfaces -from twisted.mail.imap4 import IllegalClientResponse -from twisted.mail.imap4 import LiteralString, LiteralFile - -from leap.common.events import emit_async, catalog - - -def _getContentType(msg): - """ - Return a two-tuple of the main and subtype of the given message. - """ - attrs = None - mm = msg.getHeaders(False, 'content-type').get('content-type', None) - if mm: - mm = ''.join(mm.splitlines()) - mimetype = mm.split(';') - if mimetype: - type = mimetype[0].split('/', 1) - if len(type) == 1: - major = type[0] - minor = None - elif len(type) == 2: - major, minor = type - else: - major = minor = None - # XXX patched --------------------------------------------- - attrs = dict(x.strip().split('=', 1) for x in mimetype[1:]) - # XXX patched --------------------------------------------- - else: - major = minor = None - else: - major = minor = None - return major, minor, attrs - -# Monkey-patch _getContentType to avoid bug that passes lower-case boundary in -# BODYSTRUCTURE response. -imap4._getContentType = _getContentType - - -class LEAPIMAPServer(imap4.IMAP4Server): - """ - An IMAP4 Server with a LEAP Storage Backend. - """ - - ############################################################# - # - # Twisted imap4 patch to workaround bad mime rendering in TB. - # See https://leap.se/code/issues/6773 - # and https://bugzilla.mozilla.org/show_bug.cgi?id=149771 - # Still unclear if this is a thunderbird bug. - # TODO send this patch upstream - # - ############################################################# - - def spew_body(self, part, id, msg, _w=None, _f=None): - if _w is None: - _w = self.transport.write - for p in part.part: - if msg.isMultipart(): - msg = msg.getSubPart(p) - elif p > 0: - # Non-multipart messages have an implicit first part but no - # other parts - reject any request for any other part. - raise TypeError("Requested subpart of non-multipart message") - - if part.header: - hdrs = msg.getHeaders(part.header.negate, *part.header.fields) - hdrs = imap4._formatHeaders(hdrs) - # PATCHED ########################################## - _w(str(part) + ' ' + imap4._literal(hdrs + "\r\n")) - # PATCHED ########################################## - elif part.text: - _w(str(part) + ' ') - _f() - return imap4.FileProducer( - msg.getBodyFile() - ).beginProducing(self.transport) - elif part.mime: - hdrs = imap4._formatHeaders(msg.getHeaders(True)) - - # PATCHED ########################################## - _w(str(part) + ' ' + imap4._literal(hdrs + "\r\n")) - # END PATCHED ###################################### - - elif part.empty: - _w(str(part) + ' ') - _f() - if part.part: - # PATCHED ############################################# - # implement partial FETCH - # TODO implement boundary checks - # TODO see if there's a more efficient way, without - # copying the original content into a new buffer. - fd = msg.getBodyFile() - begin = getattr(part, "partialBegin", None) - _len = getattr(part, "partialLength", None) - if begin is not None and _len is not None: - _fd = StringIO.StringIO() - fd.seek(part.partialBegin) - _fd.write(fd.read(part.partialLength)) - _fd.seek(0) - else: - _fd = fd - return imap4.FileProducer( - _fd - # END PATCHED #########################3 - ).beginProducing(self.transport) - else: - mf = imap4.IMessageFile(msg, None) - if mf is not None: - return imap4.FileProducer( - mf.open()).beginProducing(self.transport) - return imap4.MessageProducer( - msg, None, self._scheduler).beginProducing(self.transport) - - else: - _w('BODY ' + - imap4.collapseNestedLists([imap4.getBodyStructure(msg)])) - - ################################################################## - # - # END Twisted imap4 patch to workaround bad mime rendering in TB. - # #6773 - # - ################################################################## - - def lineReceived(self, line): - """ - Attempt to parse a single line from the server. - - :param line: the line from the server, without the line delimiter. - :type line: str - """ - if "login" in line.lower(): - # avoid to log the pass, even though we are using a dummy auth - # by now. - msg = line[:7] + " [...]" - else: - msg = copy(line) - log.msg('rcv (%s): %s' % (self.state, msg)) - imap4.IMAP4Server.lineReceived(self, line) - - def close_server_connection(self): - """ - Send a BYE command so that the MUA at least knows that we're closing - the connection. - """ - self.sendLine( - '* BYE LEAP IMAP Proxy is shutting down; ' - 'so long and thanks for all the fish') - self.transport.loseConnection() - if self.mbox: - self.mbox.removeListener(self) - self.mbox = None - self.state = 'unauth' - - def do_FETCH(self, tag, messages, query, uid=0): - """ - Overwritten fetch dispatcher to use the fast fetch_flags - method - """ - if not query: - self.sendPositiveResponse(tag, 'FETCH complete') - return - - cbFetch = self._IMAP4Server__cbFetch - ebFetch = self._IMAP4Server__ebFetch - - if len(query) == 1 and str(query[0]) == "flags": - self._oldTimeout = self.setTimeout(None) - # no need to call iter, we get a generator - maybeDeferred( - self.mbox.fetch_flags, messages, uid=uid - ).addCallback( - cbFetch, tag, query, uid - ).addErrback(ebFetch, tag) - - elif len(query) == 1 and str(query[0]) == "rfc822.header": - self._oldTimeout = self.setTimeout(None) - # no need to call iter, we get a generator - maybeDeferred( - self.mbox.fetch_headers, messages, uid=uid - ).addCallback( - cbFetch, tag, query, uid - ).addErrback(ebFetch, tag) - else: - self._oldTimeout = self.setTimeout(None) - # no need to call iter, we get a generator - maybeDeferred( - self.mbox.fetch, messages, uid=uid - ).addCallback( - cbFetch, tag, query, uid - ).addErrback( - ebFetch, tag) - - select_FETCH = (do_FETCH, imap4.IMAP4Server.arg_seqset, - imap4.IMAP4Server.arg_fetchatt) - - def _cbSelectWork(self, mbox, cmdName, tag): - """ - Callback for selectWork - - * patched to avoid conformance errors due to incomplete UIDVALIDITY - line. - * patched to accept deferreds for messagecount and recent count - """ - if mbox is None: - self.sendNegativeResponse(tag, 'No such mailbox') - return - if '\\noselect' in [s.lower() for s in mbox.getFlags()]: - self.sendNegativeResponse(tag, 'Mailbox cannot be selected') - return - - d1 = defer.maybeDeferred(mbox.getMessageCount) - d2 = defer.maybeDeferred(mbox.getRecentCount) - return defer.gatherResults([d1, d2]).addCallback( - self.__cbSelectWork, mbox, cmdName, tag) - - def __cbSelectWork(self, ((msg_count, recent_count)), mbox, cmdName, tag): - flags = mbox.getFlags() - self.sendUntaggedResponse('FLAGS (%s)' % ' '.join(flags)) - - # Patched ------------------------------------------------------- - # accept deferreds for the count - self.sendUntaggedResponse(str(msg_count) + ' EXISTS') - self.sendUntaggedResponse(str(recent_count) + ' RECENT') - # ---------------------------------------------------------------- - - # Patched ------------------------------------------------------- - # imaptest was complaining about the incomplete line, we're adding - # "UIDs valid" here. - self.sendPositiveResponse( - None, '[UIDVALIDITY %d] UIDs valid' % mbox.getUIDValidity()) - # ---------------------------------------------------------------- - - s = mbox.isWriteable() and 'READ-WRITE' or 'READ-ONLY' - mbox.addListener(self) - self.sendPositiveResponse(tag, '[%s] %s successful' % (s, cmdName)) - self.state = 'select' - self.mbox = mbox - - def checkpoint(self): - """ - Called when the client issues a CHECK command. - - This should perform any checkpoint operations required by the server. - It may be a long running operation, but may not block. If it returns - a deferred, the client will only be informed of success (or failure) - when the deferred's callback (or errback) is invoked. - """ - # TODO implement a collection of ongoing deferreds? - return None - - ############################################################# - # - # Twisted imap4 patch to support LITERAL+ extension - # TODO send this patch upstream asap! - # - ############################################################# - - def capabilities(self): - cap = {'AUTH': self.challengers.keys()} - if self.ctx and self.canStartTLS: - t = self.transport - ti = interfaces.ISSLTransport - if not self.startedTLS and ti(t, None) is None: - cap['LOGINDISABLED'] = None - cap['STARTTLS'] = None - cap['NAMESPACE'] = None - cap['IDLE'] = None - # patched ############ - cap['LITERAL+'] = None - ###################### - return cap - - def _stringLiteral(self, size, literal_plus=False): - if size > self._literalStringLimit: - raise IllegalClientResponse( - "Literal too long! I accept at most %d octets" % - (self._literalStringLimit,)) - d = defer.Deferred() - self.parseState = 'pending' - self._pendingLiteral = LiteralString(size, d) - # Patched ########################################################### - if not literal_plus: - self.sendContinuationRequest('Ready for %d octets of text' % size) - ##################################################################### - self.setRawMode() - return d - - def _fileLiteral(self, size, literal_plus=False): - d = defer.Deferred() - self.parseState = 'pending' - self._pendingLiteral = LiteralFile(size, d) - if not literal_plus: - self.sendContinuationRequest('Ready for %d octets of data' % size) - self.setRawMode() - return d - - def arg_astring(self, line): - """ - Parse an astring from the line, return (arg, rest), possibly - via a deferred (to handle literals) - """ - line = line.strip() - if not line: - raise IllegalClientResponse("Missing argument") - d = None - arg, rest = None, None - if line[0] == '"': - try: - spam, arg, rest = line.split('"', 2) - rest = rest[1:] # Strip space - except ValueError: - raise IllegalClientResponse("Unmatched quotes") - elif line[0] == '{': - # literal - if line[-1] != '}': - raise IllegalClientResponse("Malformed literal") - - # Patched ################ - if line[-2] == "+": - literalPlus = True - size_end = -2 - else: - literalPlus = False - size_end = -1 - - try: - size = int(line[1:size_end]) - except ValueError: - raise IllegalClientResponse( - "Bad literal size: " + line[1:size_end]) - d = self._stringLiteral(size, literalPlus) - ########################## - else: - arg = line.split(' ', 1) - if len(arg) == 1: - arg.append('') - arg, rest = arg - return d or (arg, rest) - - def arg_literal(self, line): - """ - Parse a literal from the line - """ - if not line: - raise IllegalClientResponse("Missing argument") - - if line[0] != '{': - raise IllegalClientResponse("Missing literal") - - if line[-1] != '}': - raise IllegalClientResponse("Malformed literal") - - # Patched ################## - if line[-2] == "+": - literalPlus = True - size_end = -2 - else: - literalPlus = False - size_end = -1 - - try: - size = int(line[1:size_end]) - except ValueError: - raise IllegalClientResponse( - "Bad literal size: " + line[1:size_end]) - - return self._fileLiteral(size, literalPlus) - ############################# - - # --------------------------------- isSubscribed patch - # TODO -- send patch upstream. - # There is a bug in twisted implementation: - # in cbListWork, it's assumed that account.isSubscribed IS a callable, - # although in the interface documentation it's stated that it can be - # a deferred. - - def _listWork(self, tag, ref, mbox, sub, cmdName): - mbox = self._parseMbox(mbox) - mailboxes = maybeDeferred(self.account.listMailboxes, ref, mbox) - mailboxes.addCallback(self._cbSubscribed) - mailboxes.addCallback( - self._cbListWork, tag, sub, cmdName, - ).addErrback(self._ebListWork, tag) - - def _cbSubscribed(self, mailboxes): - subscribed = [ - maybeDeferred(self.account.isSubscribed, name) - for (name, box) in mailboxes] - - def get_mailboxes_and_subs(result): - subscribed = [i[0] for i, yes in zip(mailboxes, result) if yes] - return mailboxes, subscribed - - d = defer.gatherResults(subscribed) - d.addCallback(get_mailboxes_and_subs) - return d - - def _cbListWork(self, mailboxes_subscribed, tag, sub, cmdName): - mailboxes, subscribed = mailboxes_subscribed - - for (name, box) in mailboxes: - if not sub or name in subscribed: - flags = box.getFlags() - delim = box.getHierarchicalDelimiter() - resp = (imap4.DontQuoteMe(cmdName), - map(imap4.DontQuoteMe, flags), - delim, name.encode('imap4-utf-7')) - self.sendUntaggedResponse( - imap4.collapseNestedLists(resp)) - self.sendPositiveResponse(tag, '%s completed' % (cmdName,)) - # -------------------- end isSubscribed patch ----------- - - # TODO subscribe method had also to be changed to accomodate deferred - def do_SUBSCRIBE(self, tag, name): - name = self._parseMbox(name) - - def _subscribeCb(_): - self.sendPositiveResponse(tag, 'Subscribed') - - def _subscribeEb(failure): - m = failure.value - log.err() - if failure.check(imap4.MailboxException): - self.sendNegativeResponse(tag, str(m)) - else: - self.sendBadResponse( - tag, - "Server error encountered while subscribing to mailbox") - - d = self.account.subscribe(name) - d.addCallbacks(_subscribeCb, _subscribeEb) - return d - - auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring) - select_SUBSCRIBE = auth_SUBSCRIBE - - def do_UNSUBSCRIBE(self, tag, name): - # unsubscribe method had also to be changed to accomodate - # deferred - name = self._parseMbox(name) - - def _unsubscribeCb(_): - self.sendPositiveResponse(tag, 'Unsubscribed') - - def _unsubscribeEb(failure): - m = failure.value - log.err() - if failure.check(imap4.MailboxException): - self.sendNegativeResponse(tag, str(m)) - else: - self.sendBadResponse( - tag, - "Server error encountered while unsubscribing " - "from mailbox") - - d = self.account.unsubscribe(name) - d.addCallbacks(_unsubscribeCb, _unsubscribeEb) - return d - - auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring) - select_UNSUBSCRIBE = auth_UNSUBSCRIBE - - def do_RENAME(self, tag, oldname, newname): - oldname, newname = [self._parseMbox(n) for n in oldname, newname] - if oldname.lower() == 'inbox' or newname.lower() == 'inbox': - self.sendNegativeResponse( - tag, - 'You cannot rename the inbox, or ' - 'rename another mailbox to inbox.') - return - - def _renameCb(_): - self.sendPositiveResponse(tag, 'Mailbox renamed') - - def _renameEb(failure): - m = failure.value - if failure.check(TypeError): - self.sendBadResponse(tag, 'Invalid command syntax') - elif failure.check(imap4.MailboxException): - self.sendNegativeResponse(tag, str(m)) - else: - log.err() - self.sendBadResponse( - tag, - "Server error encountered while " - "renaming mailbox") - - d = self.account.rename(oldname, newname) - d.addCallbacks(_renameCb, _renameEb) - return d - - auth_RENAME = (do_RENAME, arg_astring, arg_astring) - select_RENAME = auth_RENAME - - def do_CREATE(self, tag, name): - name = self._parseMbox(name) - - def _createCb(result): - if result: - self.sendPositiveResponse(tag, 'Mailbox created') - else: - self.sendNegativeResponse(tag, 'Mailbox not created') - - def _createEb(failure): - c = failure.value - if failure.check(imap4.MailboxException): - self.sendNegativeResponse(tag, str(c)) - else: - log.err() - self.sendBadResponse( - tag, "Server error encountered while creating mailbox") - - d = self.account.create(name) - d.addCallbacks(_createCb, _createEb) - return d - - auth_CREATE = (do_CREATE, arg_astring) - select_CREATE = auth_CREATE - - def do_DELETE(self, tag, name): - name = self._parseMbox(name) - if name.lower() == 'inbox': - self.sendNegativeResponse(tag, 'You cannot delete the inbox') - return - - def _deleteCb(result): - self.sendPositiveResponse(tag, 'Mailbox deleted') - - def _deleteEb(failure): - m = failure.value - if failure.check(imap4.MailboxException): - self.sendNegativeResponse(tag, str(m)) - else: - print "SERVER: other error" - log.err() - self.sendBadResponse( - tag, - "Server error encountered while deleting mailbox") - - d = self.account.delete(name) - d.addCallbacks(_deleteCb, _deleteEb) - return d - - auth_DELETE = (do_DELETE, arg_astring) - select_DELETE = auth_DELETE - - # ----------------------------------------------------------------------- - # Patched just to allow __cbAppend to receive a deferred from messageCount - # TODO format and send upstream. - def do_APPEND(self, tag, mailbox, flags, date, message): - mailbox = self._parseMbox(mailbox) - maybeDeferred(self.account.select, mailbox).addCallback( - self._cbAppendGotMailbox, tag, flags, date, message).addErrback( - self._ebAppendGotMailbox, tag) - - def __ebAppend(self, failure, tag): - self.sendBadResponse(tag, 'APPEND failed: ' + str(failure.value)) - - def _cbAppendGotMailbox(self, mbox, tag, flags, date, message): - if not mbox: - self.sendNegativeResponse(tag, '[TRYCREATE] No such mailbox') - return - - d = mbox.addMessage(message, flags, date) - d.addCallback(self.__cbAppend, tag, mbox) - d.addErrback(self.__ebAppend, tag) - - def _ebAppendGotMailbox(self, failure, tag): - self.sendBadResponse( - tag, "Server error encountered while opening mailbox.") - log.err(failure) - - def __cbAppend(self, result, tag, mbox): - - # XXX patched --------------------------------- - def send_response(count): - self.sendUntaggedResponse('%d EXISTS' % count) - self.sendPositiveResponse(tag, 'APPEND complete') - - d = mbox.getMessageCount() - d.addCallback(send_response) - return d - # XXX patched --------------------------------- - # ----------------------------------------------------------------------- - - auth_APPEND = (do_APPEND, arg_astring, imap4.IMAP4Server.opt_plist, - imap4.IMAP4Server.opt_datetime, arg_literal) - select_APPEND = auth_APPEND - - # Need to override the command table after patching - # arg_astring and arg_literal, except on the methods that we are already - # overriding. - - # TODO -------------------------------------------- - # Check if we really need to override these - # methods, or we can monkeypatch. - # do_DELETE = imap4.IMAP4Server.do_DELETE - # do_CREATE = imap4.IMAP4Server.do_CREATE - # do_RENAME = imap4.IMAP4Server.do_RENAME - # do_SUBSCRIBE = imap4.IMAP4Server.do_SUBSCRIBE - # do_UNSUBSCRIBE = imap4.IMAP4Server.do_UNSUBSCRIBE - # do_APPEND = imap4.IMAP4Server.do_APPEND - # ------------------------------------------------- - do_LOGIN = imap4.IMAP4Server.do_LOGIN - do_STATUS = imap4.IMAP4Server.do_STATUS - do_COPY = imap4.IMAP4Server.do_COPY - - _selectWork = imap4.IMAP4Server._selectWork - - arg_plist = imap4.IMAP4Server.arg_plist - arg_seqset = imap4.IMAP4Server.arg_seqset - opt_plist = imap4.IMAP4Server.opt_plist - opt_datetime = imap4.IMAP4Server.opt_datetime - - unauth_LOGIN = (do_LOGIN, arg_astring, arg_astring) - - auth_SELECT = (_selectWork, arg_astring, 1, 'SELECT') - select_SELECT = auth_SELECT - - auth_CREATE = (do_CREATE, arg_astring) - select_CREATE = auth_CREATE - - auth_EXAMINE = (_selectWork, arg_astring, 0, 'EXAMINE') - select_EXAMINE = auth_EXAMINE - - # TODO ----------------------------------------------- - # re-add if we stop overriding DELETE - # auth_DELETE = (do_DELETE, arg_astring) - # select_DELETE = auth_DELETE - # auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime, - # arg_literal) - # select_APPEND = auth_APPEND - - # ---------------------------------------------------- - - auth_RENAME = (do_RENAME, arg_astring, arg_astring) - select_RENAME = auth_RENAME - - auth_SUBSCRIBE = (do_SUBSCRIBE, arg_astring) - select_SUBSCRIBE = auth_SUBSCRIBE - - auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_astring) - select_UNSUBSCRIBE = auth_UNSUBSCRIBE - - auth_LIST = (_listWork, arg_astring, arg_astring, 0, 'LIST') - select_LIST = auth_LIST - - auth_LSUB = (_listWork, arg_astring, arg_astring, 1, 'LSUB') - select_LSUB = auth_LSUB - - auth_STATUS = (do_STATUS, arg_astring, arg_plist) - select_STATUS = auth_STATUS - - select_COPY = (do_COPY, arg_seqset, arg_astring) - - ############################################################# - # END of Twisted imap4 patch to support LITERAL+ extension - ############################################################# - - def authenticateLogin(self, user, passwd): - result = imap4.IMAP4Server.authenticateLogin(self, user, passwd) - emit_async(catalog.IMAP_CLIENT_LOGIN, str(user)) - return result diff --git a/mail/src/leap/mail/imap/service/README.rst b/mail/src/leap/mail/imap/service/README.rst deleted file mode 100644 index 2cca9b3..0000000 --- a/mail/src/leap/mail/imap/service/README.rst +++ /dev/null @@ -1,39 +0,0 @@ -testing the service -=================== - -Run the twisted service:: - - twistd -n -y imap-server.tac - -And use offlineimap for tests:: - - offlineimap -c LEAPofflineimapRC-tests - -minimal offlineimap configuration ---------------------------------- - -[general] -accounts = leap-local - -[Account leap-local] -localrepository = LocalLeap -remoterepository = RemoteLeap - -[Repository LocalLeap] -type = Maildir -localfolders = ~/LEAPMail/Mail - -[Repository RemoteLeap] -type = IMAP -ssl = no -remotehost = localhost -remoteport = 9930 -remoteuser = user -remotepass = pass - -debugging ---------- - -Use ngrep to obtain logs of the sequences:: - - sudo ngrep -d lo -W byline port 9930 diff --git a/mail/src/leap/mail/imap/service/__init__.py b/mail/src/leap/mail/imap/service/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/mail/src/leap/mail/imap/service/__init__.py +++ /dev/null diff --git a/mail/src/leap/mail/imap/service/imap-server.tac b/mail/src/leap/mail/imap/service/imap-server.tac deleted file mode 100644 index c4d602d..0000000 --- a/mail/src/leap/mail/imap/service/imap-server.tac +++ /dev/null @@ -1,145 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -# imap-server.tac -# Copyright (C) 2013,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 <http://www.gnu.org/licenses/>. -""" -TAC file for initialization of the imap service using twistd. - -Use this for debugging and testing the imap server using a native reactor. - -For now, and for debugging/testing purposes, you need -to pass a config file with the following structure: - -[leap_mail] -userid = 'user@provider' -uuid = 'deadbeefdeadabad' -passwd = 'supersecret' # optional, will get prompted if not found. -""" - -# TODO -- this .tac file should be deprecated in favor of bitmask.core.bitmaskd - -import ConfigParser -import getpass -import os -import sys - -from leap.keymanager import KeyManager -from leap.mail.imap.service import imap -from leap.soledad.client import Soledad - -from twisted.application import service, internet - - -# TODO should get this initializers from some authoritative mocked source -# We might want to put them the soledad itself. - -def initialize_soledad(uuid, email, passwd, - secrets, localdb, - gnupg_home, tempdir): - """ - Initializes soledad by hand - - :param email: ID for the user - :param gnupg_home: path to home used by gnupg - :param tempdir: path to temporal dir - :rtype: Soledad instance - """ - server_url = "http://provider" - cert_file = "" - - soledad = Soledad( - uuid, - passwd, - secrets, - localdb, - server_url, - cert_file, - syncable=False) - - return soledad - -###################################################################### -# Remember to set your config files, see module documentation above! -###################################################################### - -print "[+] Running LEAP IMAP Service" - - -bmconf = os.environ.get("LEAP_MAIL_CONFIG", "") -if not bmconf: - print ("[-] Please set LEAP_MAIL_CONFIG environment variable " - "pointing to your config.") - sys.exit(1) - -SECTION = "leap_mail" -cp = ConfigParser.ConfigParser() -cp.read(bmconf) - -userid = cp.get(SECTION, "userid") -uuid = cp.get(SECTION, "uuid") -passwd = unicode(cp.get(SECTION, "passwd")) - -# XXX get this right from the environment variable !!! -port = 1984 - -if not userid or not uuid: - print "[-] Config file missing userid or uuid field" - sys.exit(1) - -if not passwd: - passwd = unicode(getpass.getpass("Soledad passphrase: ")) - - -secrets = os.path.expanduser("~/.config/leap/soledad/%s.secret" % (uuid,)) -localdb = os.path.expanduser("~/.config/leap/soledad/%s.db" % (uuid,)) - -# XXX Is this really used? Should point it to user var dirs defined in xdg? -gnupg_home = "/tmp/" -tempdir = "/tmp/" - -################################################### - -# Ad-hoc soledad/keymanager initialization. - -print "[~] user:", userid -soledad = initialize_soledad(uuid, userid, passwd, secrets, - localdb, gnupg_home, tempdir, userid=userid) -km_args = (userid, "https://localhost", soledad) -km_kwargs = { - "token": "", - "ca_cert_path": "", - "api_uri": "", - "api_version": "", - "uid": uuid, - "gpgbinary": "/usr/bin/gpg" -} -keymanager = KeyManager(*km_args, **km_kwargs) - -################################################## - -# Ok, let's expose the application object for the twistd application -# framework to pick up from here... - - -def getIMAPService(): - soledad_sessions = {userid: soledad} - factory = imap.LeapIMAPFactory(soledad_sessions) - return internet.TCPServer(port, factory, interface="localhost") - - -application = service.Application("LEAP IMAP Application") -service = getIMAPService() -service.setServiceParent(application) diff --git a/mail/src/leap/mail/imap/service/imap.py b/mail/src/leap/mail/imap/service/imap.py deleted file mode 100644 index 4663854..0000000 --- a/mail/src/leap/mail/imap/service/imap.py +++ /dev/null @@ -1,208 +0,0 @@ -# -*- coding: utf-8 -*- -# imap.py -# Copyright (C) 2013-2015 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 <http://www.gnu.org/licenses/>. -""" -IMAP Service Initialization. -""" -import logging -import os - -from collections import defaultdict - -from twisted.cred.portal import Portal, IRealm -from twisted.mail.imap4 import IAccount -from twisted.internet import defer -from twisted.internet import reactor -from twisted.internet.error import CannotListenError -from twisted.internet.protocol import ServerFactory -from twisted.python import log -from zope.interface import implementer - -from leap.common.events import emit_async, catalog -from leap.mail.cred import LocalSoledadTokenChecker -from leap.mail.imap.account import IMAPAccount -from leap.mail.imap.server import LEAPIMAPServer - -# TODO: leave only an implementor of IService in here - -logger = logging.getLogger(__name__) - -DO_MANHOLE = os.environ.get("LEAP_MAIL_MANHOLE", None) -if DO_MANHOLE: - from leap.mail.imap.service import manhole - -# The default port in which imap service will run - -IMAP_PORT = 1984 - -# -# Credentials Handling -# - - -@implementer(IRealm) -class LocalSoledadIMAPRealm(object): - - _encoding = 'utf-8' - - def __init__(self, soledad_sessions): - """ - :param soledad_sessions: a dict-like object, containing instances - of a Store (soledad instances), indexed by - userid. - """ - self._soledad_sessions = soledad_sessions - - def requestAvatar(self, avatarId, mind, *interfaces): - if isinstance(avatarId, str): - avatarId = avatarId.decode(self._encoding) - - def gotSoledad(soledad): - for iface in interfaces: - if iface is IAccount: - avatar = IMAPAccount(soledad, avatarId) - return (IAccount, avatar, - getattr(avatar, 'logout', lambda: None)) - raise NotImplementedError(self, interfaces) - - return self.lookupSoledadInstance(avatarId).addCallback(gotSoledad) - - def lookupSoledadInstance(self, userid): - soledad = self._soledad_sessions[userid] - # XXX this should return the instance after whenReady callback - return defer.succeed(soledad) - - -class IMAPTokenChecker(LocalSoledadTokenChecker): - """A credentials checker that will lookup a token for the IMAP service. - For now it will be using the same identifier than SMTPTokenChecker""" - - service = 'mail_auth' - - -class LocalSoledadIMAPServer(LEAPIMAPServer): - - """ - An IMAP Server that authenticates against a LocalSoledad store. - """ - - def __init__(self, soledad_sessions, *args, **kw): - - LEAPIMAPServer.__init__(self, *args, **kw) - - realm = LocalSoledadIMAPRealm(soledad_sessions) - portal = Portal(realm) - checker = IMAPTokenChecker(soledad_sessions) - self.checker = checker - self.portal = portal - portal.registerChecker(checker) - - -class LeapIMAPFactory(ServerFactory): - - """ - Factory for a IMAP4 server with soledad remote sync and gpg-decryption - capabilities. - """ - - protocol = LocalSoledadIMAPServer - - def __init__(self, soledad_sessions): - """ - Initializes the server factory. - - :param soledad_sessions: a dict-like object, containing instances - of a Store (soledad instances), indexed by - userid. - """ - self._soledad_sessions = soledad_sessions - self._connections = defaultdict() - - def buildProtocol(self, addr): - """ - Return a protocol suitable for the job. - - :param addr: remote ip address - :type addr: str - """ - # TODO should reject anything from addr != localhost, - # just in case. - log.msg("Building protocol for connection %s" % addr) - imapProtocol = self.protocol(self._soledad_sessions) - self._connections[addr] = imapProtocol - return imapProtocol - - def stopFactory(self): - # say bye! - for conn, proto in self._connections.items(): - log.msg("Closing connections for %s" % conn) - proto.close_server_connection() - - def doStop(self): - """ - Stops imap service (fetcher, factory and port). - """ - return ServerFactory.doStop(self) - - -def run_service(soledad_sessions, port=IMAP_PORT): - """ - Main entry point to run the service from the client. - - :param soledad_sessions: a dict-like object, containing instances - of a Store (soledad instances), indexed by userid. - - :returns: the port as returned by the reactor when starts listening, and - the factory for the protocol. - :rtype: tuple - """ - factory = LeapIMAPFactory(soledad_sessions) - - try: - interface = "localhost" - # don't bind just to localhost if we are running on docker since we - # won't be able to access imap from the host - if os.environ.get("LEAP_DOCKERIZED"): - interface = '' - - # TODO use Endpoints !!! - tport = reactor.listenTCP(port, factory, - interface=interface) - except CannotListenError: - logger.error("IMAP Service failed to start: " - "cannot listen in port %s" % (port,)) - except Exception as exc: - logger.error("Error launching IMAP service: %r" % (exc,)) - else: - # all good. - - if DO_MANHOLE: - # TODO get pass from env var.too. - manhole_factory = manhole.getManholeFactory( - {'f': factory, - 'gm': factory.theAccount.getMailbox}, - "boss", "leap") - # TODO use Endpoints !!! - reactor.listenTCP(manhole.MANHOLE_PORT, manhole_factory, - interface="127.0.0.1") - logger.debug("IMAP4 Server is RUNNING in port %s" % (port,)) - emit_async(catalog.IMAP_SERVICE_STARTED, str(port)) - - # FIXME -- change service signature - return tport, factory - - # not ok, signal error. - emit_async(catalog.IMAP_SERVICE_FAILED_TO_START, str(port)) diff --git a/mail/src/leap/mail/imap/service/manhole.py b/mail/src/leap/mail/imap/service/manhole.py deleted file mode 100644 index c83ae89..0000000 --- a/mail/src/leap/mail/imap/service/manhole.py +++ /dev/null @@ -1,130 +0,0 @@ -# -*- coding: utf-8 -*- -# manhole.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 <http://www.gnu.org/licenses/>. -""" -Utilities for enabling the manhole administrative interface into the -LEAP Mail application. -""" -MANHOLE_PORT = 2222 - - -def getManholeFactory(namespace, user, secret): - """ - Get an administrative manhole into the application. - - :param namespace: the namespace to show in the manhole - :type namespace: dict - :param user: the user to authenticate into the administrative shell. - :type user: str - :param secret: pass for this manhole - :type secret: str - """ - import string - - from twisted.cred.portal import Portal - from twisted.conch import manhole, manhole_ssh - from twisted.conch.insults import insults - from twisted.cred.checkers import ( - InMemoryUsernamePasswordDatabaseDontUse as MemoryDB) - - from rlcompleter import Completer - - class EnhancedColoredManhole(manhole.ColoredManhole): - """ - A Manhole with some primitive autocomplete support. - """ - # TODO use introspection to make life easier - - def find_common(self, l): - """ - find common parts in thelist items - ex: 'ab' for ['abcd','abce','abf'] - requires an ordered list - """ - if len(l) == 1: - return l[0] - - init = l[0] - for item in l[1:]: - for i, (x, y) in enumerate(zip(init, item)): - if x != y: - init = "".join(init[:i]) - break - - if not init: - return None - return init - - def handle_TAB(self): - """ - Trap the TAB keystroke. - """ - necessarypart = "".join(self.lineBuffer).split(' ')[-1] - completer = Completer(globals()) - if completer.complete(necessarypart, 0): - matches = list(set(completer.matches)) # has multiples - - if len(matches) == 1: - length = len(necessarypart) - self.lineBuffer = self.lineBuffer[:-length] - self.lineBuffer.extend(matches[0]) - self.lineBufferIndex = len(self.lineBuffer) - else: - matches.sort() - commons = self.find_common(matches) - if commons: - length = len(necessarypart) - self.lineBuffer = self.lineBuffer[:-length] - self.lineBuffer.extend(commons) - self.lineBufferIndex = len(self.lineBuffer) - - self.terminal.nextLine() - while matches: - matches, part = matches[4:], matches[:4] - for item in part: - self.terminal.write('%s' % item.ljust(30)) - self.terminal.write('\n') - self.terminal.nextLine() - - self.terminal.eraseLine() - self.terminal.cursorBackward(self.lineBufferIndex + 5) - self.terminal.write("%s %s" % ( - self.ps[self.pn], "".join(self.lineBuffer))) - - def keystrokeReceived(self, keyID, modifier): - """ - Act upon any keystroke received. - """ - self.keyHandlers.update({'\b': self.handle_BACKSPACE}) - m = self.keyHandlers.get(keyID) - if m is not None: - m() - elif keyID in string.printable: - self.characterReceived(keyID, False) - - sshRealm = manhole_ssh.TerminalRealm() - - def chainedProtocolFactory(): - return insults.ServerProtocol(EnhancedColoredManhole, namespace) - - sshRealm = manhole_ssh.TerminalRealm() - sshRealm.chainedProtocolFactory = chainedProtocolFactory - - portal = Portal( - sshRealm, [MemoryDB(**{user: secret})]) - - f = manhole_ssh.ConchFactory(portal) - return f diff --git a/mail/src/leap/mail/imap/service/notes.txt b/mail/src/leap/mail/imap/service/notes.txt deleted file mode 100644 index 623e122..0000000 --- a/mail/src/leap/mail/imap/service/notes.txt +++ /dev/null @@ -1,81 +0,0 @@ -T 127.0.0.1:9930 -> 127.0.0.1:42866 [AP] -* OK [CAPABILITY IMAP4rev1 IDLE NAMESPACE] Twisted IMAP4rev1 Ready. - -## -T 127.0.0.1:42866 -> 127.0.0.1:9930 [AP] -NCLJ1 CAPABILITY. - -## -T 127.0.0.1:9930 -> 127.0.0.1:42866 [AP] -* CAPABILITY IMAP4rev1 IDLE NAMESPACE. -NCLJ1 OK CAPABILITY completed. - -## -T 127.0.0.1:42866 -> 127.0.0.1:9930 [AP] -NCLJ2 LOGIN user "pass". - -# -T 127.0.0.1:9930 -> 127.0.0.1:42866 [AP] -NCLJ2 OK LOGIN succeeded. - -## -T 127.0.0.1:42866 -> 127.0.0.1:9930 [AP] -NCLJ3 CAPABILITY. - -# -T 127.0.0.1:9930 -> 127.0.0.1:42866 [AP] -* CAPABILITY IMAP4rev1 IDLE NAMESPACE. -NCLJ3 OK CAPABILITY completed. - -# -T 127.0.0.1:42866 -> 127.0.0.1:9930 [AP] -NCLJ4 LIST "" "". - -## -T 127.0.0.1:9930 -> 127.0.0.1:42866 [AP] -* LIST (\Seen \Answered \Flagged \Deleted \Draft \Recent List) "/" "INBOX". -NCLJ4 OK LIST completed. - -# -T 127.0.0.1:42866 -> 127.0.0.1:9930 [AP] -NCLJ5 LIST "" "*". - -## -T 127.0.0.1:9930 -> 127.0.0.1:42866 [AP] -* LIST (\Seen \Answered \Flagged \Deleted \Draft \Recent List) "/" "INBOX". -NCLJ5 OK LIST completed. - -# -T 127.0.0.1:42866 -> 127.0.0.1:9930 [AP] -NCLJ6 SELECT INBOX. - -# -T 127.0.0.1:9930 -> 127.0.0.1:42866 [AP] -* 0 EXISTS. -* 3 RECENT. -* FLAGS (\Seen \Answered \Flagged \Deleted \Draft \Recent List). -* OK [UIDVALIDITY 42]. -NCLJ6 OK [READ-WRITE] SELECT successful. - -# -T 127.0.0.1:42866 -> 127.0.0.1:9930 [AP] -NCLJ7 EXAMINE INBOX. - -## -T 127.0.0.1:9930 -> 127.0.0.1:42866 [AP] -* 0 EXISTS. -* 3 RECENT. -* FLAGS (\Seen \Answered \Flagged \Deleted \Draft \Recent List). -* OK [UIDVALIDITY 42]. -NCLJ7 OK [READ-ONLY] EXAMINE successful. - -# -T 127.0.0.1:42866 -> 127.0.0.1:9930 [AP] -NCLJ8 LOGOUT. - -## -T 127.0.0.1:9930 -> 127.0.0.1:42866 [AP] -* BYE Nice talking to you. -NCLJ8 OK LOGOUT successful. - - diff --git a/mail/src/leap/mail/imap/service/rfc822.message b/mail/src/leap/mail/imap/service/rfc822.message deleted file mode 100644 index ee97ab9..0000000 --- a/mail/src/leap/mail/imap/service/rfc822.message +++ /dev/null @@ -1,86 +0,0 @@ -Return-Path: <twisted-commits-admin@twistedmatrix.com> -Delivered-To: exarkun@meson.dyndns.org -Received: from localhost [127.0.0.1] - by localhost with POP3 (fetchmail-6.2.1) - for exarkun@localhost (single-drop); Thu, 20 Mar 2003 14:50:20 -0500 (EST) -Received: from pyramid.twistedmatrix.com (adsl-64-123-27-105.dsl.austtx.swbell.net [64.123.27.105]) - by intarweb.us (Postfix) with ESMTP id 4A4A513EA4 - for <exarkun@meson.dyndns.org>; Thu, 20 Mar 2003 14:49:27 -0500 (EST) -Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com) - by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian)) - id 18w648-0007Vl-00; Thu, 20 Mar 2003 13:51:04 -0600 -Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian)) - id 18w63j-0007VK-00 - for <twisted-commits@twistedmatrix.com>; Thu, 20 Mar 2003 13:50:39 -0600 -To: twisted-commits@twistedmatrix.com -From: etrepum CVS <etrepum@twistedmatrix.com> -Reply-To: twisted-python@twistedmatrix.com -X-Mailer: CVSToys -Message-Id: <E18w63j-0007VK-00@pyramid.twistedmatrix.com> -Subject: [Twisted-commits] rebuild now works on python versions from 2.2.0 and up. -Sender: twisted-commits-admin@twistedmatrix.com -Errors-To: twisted-commits-admin@twistedmatrix.com -X-BeenThere: twisted-commits@twistedmatrix.com -X-Mailman-Version: 2.0.11 -Precedence: bulk -List-Help: <mailto:twisted-commits-request@twistedmatrix.com?subject=help> -List-Post: <mailto:twisted-commits@twistedmatrix.com> -List-Subscribe: <http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits>, - <mailto:twisted-commits-request@twistedmatrix.com?subject=subscribe> -List-Id: <twisted-commits.twistedmatrix.com> -List-Unsubscribe: <http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits>, - <mailto:twisted-commits-request@twistedmatrix.com?subject=unsubscribe> -List-Archive: <http://twistedmatrix.com/pipermail/twisted-commits/> -Date: Thu, 20 Mar 2003 13:50:39 -0600 - -Modified files: -Twisted/twisted/python/rebuild.py 1.19 1.20 - -Log message: -rebuild now works on python versions from 2.2.0 and up. - - -ViewCVS links: -http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/twisted/python/rebuild.py.diff?r1=text&tr1=1.19&r2=text&tr2=1.20&cvsroot=Twisted - -Index: Twisted/twisted/python/rebuild.py -diff -u Twisted/twisted/python/rebuild.py:1.19 Twisted/twisted/python/rebuild.py:1.20 ---- Twisted/twisted/python/rebuild.py:1.19 Fri Jan 17 13:50:49 2003 -+++ Twisted/twisted/python/rebuild.py Thu Mar 20 11:50:08 2003 -@@ -206,15 +206,27 @@ - clazz.__dict__.clear() - clazz.__getattr__ = __getattr__ - clazz.__module__ = module.__name__ -+ if newclasses: -+ import gc -+ if (2, 2, 0) <= sys.version_info[:3] < (2, 2, 2): -+ hasBrokenRebuild = 1 -+ gc_objects = gc.get_objects() -+ else: -+ hasBrokenRebuild = 0 - for nclass in newclasses: - ga = getattr(module, nclass.__name__) - if ga is nclass: - log.msg("WARNING: new-class %s not replaced by reload!" % reflect.qual(nclass)) - else: -- import gc -- for r in gc.get_referrers(nclass): -- if isinstance(r, nclass): -+ if hasBrokenRebuild: -+ for r in gc_objects: -+ if not getattr(r, '__class__', None) is nclass: -+ continue - r.__class__ = ga -+ else: -+ for r in gc.get_referrers(nclass): -+ if getattr(r, '__class__', None) is nclass: -+ r.__class__ = ga - if doLog: - log.msg('') - log.msg(' (fixing %s): ' % str(module.__name__)) - - -_______________________________________________ -Twisted-commits mailing list -Twisted-commits@twistedmatrix.com -http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits diff --git a/mail/src/leap/mail/imap/tests/.gitignore b/mail/src/leap/mail/imap/tests/.gitignore deleted file mode 100644 index 60baa9c..0000000 --- a/mail/src/leap/mail/imap/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -data/* diff --git a/mail/src/leap/mail/imap/tests/getmail b/mail/src/leap/mail/imap/tests/getmail deleted file mode 100755 index dd3fa0b..0000000 --- a/mail/src/leap/mail/imap/tests/getmail +++ /dev/null @@ -1,344 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) Twisted Matrix Laboratories. -# See LICENSE in twisted for details. - -# Modifications by LEAP Developers 2014 to fit -# Bitmask configuration settings. -""" -Simple IMAP4 client which displays the subjects of all messages in a -particular mailbox. -""" - -import os -import sys - -from twisted.internet import protocol -from twisted.internet import ssl -from twisted.internet import defer -from twisted.internet import stdio -from twisted.mail import imap4 -from twisted.protocols import basic -from twisted.python import log - -# Global options stored here from main -_opts = {} - - -class TrivialPrompter(basic.LineReceiver): - from os import linesep as delimiter - - promptDeferred = None - - def prompt(self, msg): - assert self.promptDeferred is None - self.display(msg) - self.promptDeferred = defer.Deferred() - return self.promptDeferred - - def display(self, msg): - self.transport.write(msg) - - def lineReceived(self, line): - if self.promptDeferred is None: - return - d, self.promptDeferred = self.promptDeferred, None - d.callback(line) - - -class SimpleIMAP4Client(imap4.IMAP4Client): - """ - A client with callbacks for greeting messages from an IMAP server. - """ - greetDeferred = None - - def serverGreeting(self, caps): - self.serverCapabilities = caps - if self.greetDeferred is not None: - d, self.greetDeferred = self.greetDeferred, None - d.callback(self) - - -class SimpleIMAP4ClientFactory(protocol.ClientFactory): - usedUp = False - - protocol = SimpleIMAP4Client - - def __init__(self, username, onConn): - self.ctx = ssl.ClientContextFactory() - - self.username = username - self.onConn = onConn - - def buildProtocol(self, addr): - """ - Initiate the protocol instance. Since we are building a simple IMAP - client, we don't bother checking what capabilities the server has. We - just add all the authenticators twisted.mail has. - """ - assert not self.usedUp - self.usedUp = True - - p = self.protocol(self.ctx) - p.factory = self - p.greetDeferred = self.onConn - - p.registerAuthenticator(imap4.PLAINAuthenticator(self.username)) - p.registerAuthenticator(imap4.LOGINAuthenticator(self.username)) - p.registerAuthenticator( - imap4.CramMD5ClientAuthenticator(self.username)) - - return p - - def clientConnectionFailed(self, connector, reason): - d, self.onConn = self.onConn, None - d.errback(reason) - - -def cbServerGreeting(proto, username, password): - """ - Initial callback - invoked after the server sends us its greet message. - """ - # Hook up stdio - tp = TrivialPrompter() - stdio.StandardIO(tp) - - # And make it easily accessible - proto.prompt = tp.prompt - proto.display = tp.display - - # Try to authenticate securely - return proto.authenticate( - password).addCallback( - cbAuthentication, - proto).addErrback( - ebAuthentication, proto, username, password - ) - - -def ebConnection(reason): - """ - Fallback error-handler. If anything goes wrong, log it and quit. - """ - log.startLogging(sys.stdout) - log.err(reason) - return reason - - -def cbAuthentication(result, proto): - """ - Callback after authentication has succeeded. - - Lists a bunch of mailboxes. - """ - return proto.list("", "*" - ).addCallback(cbMailboxList, proto - ) - - -def ebAuthentication(failure, proto, username, password): - """ - Errback invoked when authentication fails. - - If it failed because no SASL mechanisms match, offer the user the choice - of logging in insecurely. - - If you are trying to connect to your Gmail account, you will be here! - """ - failure.trap(imap4.NoSupportedAuthentication) - return InsecureLogin(proto, username, password) - - -def InsecureLogin(proto, username, password): - """ - insecure-login. - """ - return proto.login(username, password - ).addCallback(cbAuthentication, proto - ) - - -def cbMailboxList(result, proto): - """ - Callback invoked when a list of mailboxes has been retrieved. - If we have a selected mailbox in the global options, we directly pick it. - Otherwise, we offer a prompt to let user choose one. - """ - all_mbox_list = [e[2] for e in result] - s = '\n'.join(['%d. %s' % (n + 1, m) for (n, m) in zip(range(len(all_mbox_list)), all_mbox_list)]) - if not s: - return defer.fail(Exception("No mailboxes exist on server!")) - - selected_mailbox = _opts.get('mailbox') - - if not selected_mailbox: - return proto.prompt(s + "\nWhich mailbox? [1] " - ).addCallback(cbPickMailbox, proto, all_mbox_list - ) - else: - mboxes_lower = map(lambda s: s.lower(), all_mbox_list) - index = mboxes_lower.index(selected_mailbox.lower()) + 1 - return cbPickMailbox(index, proto, all_mbox_list) - - -def cbPickMailbox(result, proto, mboxes): - """ - When the user selects a mailbox, "examine" it. - """ - mbox = mboxes[int(result or '1') - 1] - return proto.examine(mbox - ).addCallback(cbExamineMbox, proto - ) - - -def cbExamineMbox(result, proto): - """ - Callback invoked when examine command completes. - - Retrieve the subject header of every message in the mailbox. - """ - return proto.fetchSpecific('1:*', - headerType='HEADER.FIELDS', - headerArgs=['SUBJECT'], - ).addCallback(cbFetch, proto, - ) - - -def cbFetch(result, proto): - """ - Display a listing of the messages in the mailbox, based on the collected - headers. - """ - selected_subject = _opts.get('subject', None) - index = None - - if result: - keys = result.keys() - keys.sort() - - if selected_subject: - for k in keys: - # remove 'Subject: ' preffix plus eol - subject = result[k][0][2][9:].rstrip('\r\n') - if subject.lower() == selected_subject.lower(): - index = k - break - else: - for k in keys: - proto.display('%s %s' % (k, result[k][0][2])) - else: - print "Hey, an empty mailbox!" - - if not index: - return proto.prompt("\nWhich message? [1] (Q quits) " - ).addCallback(cbPickMessage, proto) - else: - return cbPickMessage(index, proto) - - -def cbPickMessage(result, proto): - """ - Pick a message. - """ - if result == "Q": - print "Bye!" - return proto.logout() - - return proto.fetchSpecific( - '%s' % result, - headerType='', - headerArgs=['BODY.PEEK[]'], - ).addCallback(cbShowmessage, proto) - - -def cbShowmessage(result, proto): - """ - Display message. - """ - if result: - keys = result.keys() - keys.sort() - for k in keys: - proto.display('%s %s' % (k, result[k][0][2])) - else: - print "Hey, an empty message!" - - return proto.logout() - - -def cbClose(result): - """ - Close the connection when we finish everything. - """ - from twisted.internet import reactor - reactor.stop() - - -def main(): - import argparse - import ConfigParser - import sys - from twisted.internet import reactor - - description = ( - 'Get messages from a LEAP IMAP Proxy.\nThis is a ' - 'debugging tool, do not use this to retrieve any sensitive ' - 'information, or we will send ninjas to your house!') - epilog = ( - 'In case you want to automate the usage of this utility ' - 'you can place your credentials in a file pointed by ' - 'BITMASK_CREDENTIALS. You need to have a [Credentials] ' - 'section, with username=<user@provider> and password fields') - - parser = argparse.ArgumentParser(description=description, epilog=epilog) - credentials = os.environ.get('BITMASK_CREDENTIALS') - - if credentials: - try: - config = ConfigParser.ConfigParser() - config.read(credentials) - username = config.get('Credentials', 'username') - password = config.get('Credentials', 'password') - except Exception, e: - print "Error reading credentials file: {0}".format(e) - sys.exit() - else: - parser.add_argument('username', type=str) - parser.add_argument('password', type=str) - - parser.add_argument('--mailbox', dest='mailbox', default=None, - help='Which mailbox to retrieve. Empty for interactive prompt.') - parser.add_argument('--subject', dest='subject', default=None, - help='A subject for retrieve a mail that matches. Empty for interactive prompt.') - - ns = parser.parse_args() - - if not credentials: - username = ns.username - password = ns.password - - _opts['mailbox'] = ns.mailbox - _opts['subject'] = ns.subject - - hostname = "localhost" - port = "1984" - - onConn = defer.Deferred( - ).addCallback(cbServerGreeting, username, password - ).addErrback(ebConnection - ).addBoth(cbClose) - - factory = SimpleIMAP4ClientFactory(username, onConn) - - if port == '993': - reactor.connectSSL( - hostname, int(port), factory, ssl.ClientContextFactory()) - else: - if not port: - port = 143 - reactor.connectTCP(hostname, int(port), factory) - reactor.run() - - -if __name__ == '__main__': - main() diff --git a/mail/src/leap/mail/imap/tests/imapclient.py b/mail/src/leap/mail/imap/tests/imapclient.py deleted file mode 100755 index c353cee..0000000 --- a/mail/src/leap/mail/imap/tests/imapclient.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python - -# Copyright (c) Twisted Matrix Laboratories. -# See LICENSE for details. - - -""" -Simple IMAP4 client which connects to our custome -IMAP4 server: imapserver.py. -""" - -import sys - -from twisted.internet import protocol -from twisted.internet import defer -from twisted.internet import stdio -from twisted.mail import imap4 -from twisted.protocols import basic -from twisted.python import util -from twisted.python import log - - -class TrivialPrompter(basic.LineReceiver): - # from os import linesep as delimiter - - promptDeferred = None - - def prompt(self, msg): - assert self.promptDeferred is None - self.display(msg) - self.promptDeferred = defer.Deferred() - return self.promptDeferred - - def display(self, msg): - self.transport.write(msg) - - def lineReceived(self, line): - if self.promptDeferred is None: - return - d, self.promptDeferred = self.promptDeferred, None - d.callback(line) - - -class SimpleIMAP4Client(imap4.IMAP4Client): - - """ - Add callbacks when the client receives greeting messages from - an IMAP server. - """ - greetDeferred = None - - def serverGreeting(self, caps): - self.serverCapabilities = caps - if self.greetDeferred is not None: - d, self.greetDeferred = self.greetDeferred, None - d.callback(self) - - -class SimpleIMAP4ClientFactory(protocol.ClientFactory): - usedUp = False - protocol = SimpleIMAP4Client - - def __init__(self, username, onConn): - self.username = username - self.onConn = onConn - - def buildProtocol(self, addr): - assert not self.usedUp - self.usedUp = True - - p = self.protocol() - p.factory = self - p.greetDeferred = self.onConn - - p.registerAuthenticator(imap4.PLAINAuthenticator(self.username)) - p.registerAuthenticator(imap4.LOGINAuthenticator(self.username)) - p.registerAuthenticator( - imap4.CramMD5ClientAuthenticator(self.username)) - - return p - - def clientConnectionFailed(self, connector, reason): - d, self.onConn = self.onConn, None - d.errback(reason) - - -def cbServerGreeting(proto, username, password): - """ - Initial callback - invoked after the server sends us its greet message. - """ - # Hook up stdio - tp = TrivialPrompter() - stdio.StandardIO(tp) - - # And make it easily accessible - proto.prompt = tp.prompt - proto.display = tp.display - - # Try to authenticate securely - return proto.authenticate( - password).addCallback( - cbAuthentication, proto).addErrback( - ebAuthentication, proto, username, password) - - -def ebConnection(reason): - """ - Fallback error-handler. If anything goes wrong, log it and quit. - """ - log.startLogging(sys.stdout) - log.err(reason) - return reason - - -def cbAuthentication(result, proto): - """ - Callback after authentication has succeeded. - List a bunch of mailboxes. - """ - return proto.list("", "*" - ).addCallback(cbMailboxList, proto - ) - - -def ebAuthentication(failure, proto, username, password): - """ - Errback invoked when authentication fails. - If it failed because no SASL mechanisms match, offer the user the choice - of logging in insecurely. - If you are trying to connect to your Gmail account, you will be here! - """ - failure.trap(imap4.NoSupportedAuthentication) - return proto.prompt( - "No secure authentication available. Login insecurely? (y/N) " - ).addCallback(cbInsecureLogin, proto, username, password - ) - - -def cbInsecureLogin(result, proto, username, password): - """ - Callback for "insecure-login" prompt. - """ - if result.lower() == "y": - # If they said yes, do it. - return proto.login(username, password - ).addCallback(cbAuthentication, proto - ) - return defer.fail(Exception("Login failed for security reasons.")) - - -def cbMailboxList(result, proto): - """ - Callback invoked when a list of mailboxes has been retrieved. - """ - result = [e[2] for e in result] - s = '\n'.join( - ['%d. %s' % (n + 1, m) for (n, m) in zip(range(len(result)), result)]) - if not s: - return defer.fail(Exception("No mailboxes exist on server!")) - return proto.prompt(s + "\nWhich mailbox? [1] " - ).addCallback(cbPickMailbox, proto, result - ) - - -def cbPickMailbox(result, proto, mboxes): - """ - When the user selects a mailbox, "examine" it. - """ - mbox = mboxes[int(result or '1') - 1] - return proto.status(mbox, 'MESSAGES', 'UNSEEN' - ).addCallback(cbMboxStatus, proto) - - -def cbMboxStatus(result, proto): - print "You have %s messages (%s unseen)!" % ( - result['MESSAGES'], result['UNSEEN']) - return proto.logout() - - -def cbClose(result): - """ - Close the connection when we finish everything. - """ - from twisted.internet import reactor - reactor.stop() - - -def main(): - hostname = raw_input('IMAP4 Server Hostname: ') - port = raw_input('IMAP4 Server Port (the default is 143): ') - username = raw_input('IMAP4 Username: ') - password = util.getPassword('IMAP4 Password: ') - - onConn = defer.Deferred( - ).addCallback(cbServerGreeting, username, password - ).addErrback(ebConnection - ).addBoth(cbClose) - - factory = SimpleIMAP4ClientFactory(username, onConn) - - from twisted.internet import reactor - conn = reactor.connectTCP(hostname, int(port), factory) - reactor.run() - - -if __name__ == '__main__': - main() diff --git a/mail/src/leap/mail/imap/tests/regressions_mime_struct b/mail/src/leap/mail/imap/tests/regressions_mime_struct deleted file mode 100755 index 0332664..0000000 --- a/mail/src/leap/mail/imap/tests/regressions_mime_struct +++ /dev/null @@ -1,461 +0,0 @@ -#!/usr/bin/env python - -# -*- coding: utf-8 -*- -# regression_mime_struct -# Copyright (C) 2014 LEAP -# Copyright (c) Twisted Matrix Laboratories. -# -# 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 <http://www.gnu.org/licenses/>. -""" -Simple Regression Tests for checking MIME struct handling using IMAP4 client. - -Iterates trough all mails under a given folder and tries to APPEND them to -the server being tested. After FETCHING the pushed message, it compares -the received version with the one that was saved, and exits with an error -code if they do not match. -""" -import os -import StringIO -import sys - -from email.parser import Parser - -from twisted.internet import protocol -from twisted.internet import ssl -from twisted.internet import defer -from twisted.internet import stdio -from twisted.mail import imap4 -from twisted.protocols import basic -from twisted.python import log - - -REGRESSIONS_FOLDER = os.environ.get( - "REGRESSIONS_FOLDER", "regressions_test") -print "[+] Using regressions folder:", REGRESSIONS_FOLDER - -parser = Parser() - - -def get_msg_parts(raw): - """ - Return a representation of the parts of a message suitable for - comparison. - - :param raw: string for the message - :type raw: str - """ - m = parser.parsestr(raw) - return [dict(part.items()) - if part.is_multipart() - else part.get_payload() - for part in m.walk()] - - -def compare_msg_parts(a, b): - """ - Compare two sequences of parts of messages. - - :param a: part sequence for message a - :param b: part sequence for message b - - :return: True if both message sequences are equivalent. - :rtype: bool - """ - # XXX This could be smarter and show the differences in the - # different parts when/where they differ. - #import pprint; pprint.pprint(a[0]) - #import pprint; pprint.pprint(b[0]) - - def lowerkey(d): - return dict((k.lower(), v.replace('\r', '')) - for k, v in d.iteritems()) - - def eq(x, y): - # For dicts, we compare a variation with their keys - # in lowercase, and \r removed from their values - if all(map(lambda i: isinstance(i, dict), (x, y))): - x, y = map(lowerkey, (x, y)) - return x == y - - compare_vector = map(lambda tup: eq(tup[0], tup[1]), zip(a, b)) - all_match = all(compare_vector) - - if not all_match: - print "PARTS MISMATCH!" - print "vector: ", compare_vector - index = compare_vector.index(False) - from pprint import pprint - print "Expected:" - pprint(a[index]) - print ("***") - print "Found:" - pprint(b[index]) - print - - return all_match - - -def get_fd(string): - """ - Return a file descriptor with the passed string - as content. - """ - fd = StringIO.StringIO() - fd.write(string) - fd.seek(0) - return fd - - -class TrivialPrompter(basic.LineReceiver): - promptDeferred = None - - def prompt(self, msg): - assert self.promptDeferred is None - self.display(msg) - self.promptDeferred = defer.Deferred() - return self.promptDeferred - - def display(self, msg): - self.transport.write(msg) - - def lineReceived(self, line): - if self.promptDeferred is None: - return - d, self.promptDeferred = self.promptDeferred, None - d.callback(line) - - -class SimpleIMAP4Client(imap4.IMAP4Client): - """ - A client with callbacks for greeting messages from an IMAP server. - """ - greetDeferred = None - - def serverGreeting(self, caps): - self.serverCapabilities = caps - if self.greetDeferred is not None: - d, self.greetDeferred = self.greetDeferred, None - d.callback(self) - - -class SimpleIMAP4ClientFactory(protocol.ClientFactory): - usedUp = False - protocol = SimpleIMAP4Client - - def __init__(self, username, onConn): - self.ctx = ssl.ClientContextFactory() - - self.username = username - self.onConn = onConn - - def buildProtocol(self, addr): - """ - Initiate the protocol instance. Since we are building a simple IMAP - client, we don't bother checking what capabilities the server has. We - just add all the authenticators twisted.mail has. Note: Gmail no - longer uses any of the methods below, it's been using XOAUTH since - 2010. - """ - assert not self.usedUp - self.usedUp = True - - p = self.protocol(self.ctx) - p.factory = self - p.greetDeferred = self.onConn - - p.registerAuthenticator(imap4.PLAINAuthenticator(self.username)) - p.registerAuthenticator(imap4.LOGINAuthenticator(self.username)) - p.registerAuthenticator( - imap4.CramMD5ClientAuthenticator(self.username)) - - return p - - def clientConnectionFailed(self, connector, reason): - d, self.onConn = self.onConn, None - d.errback(reason) - - -def cbServerGreeting(proto, username, password): - """ - Initial callback - invoked after the server sends us its greet message. - """ - # Hook up stdio - tp = TrivialPrompter() - stdio.StandardIO(tp) - - # And make it easily accessible - proto.prompt = tp.prompt - proto.display = tp.display - - # Try to authenticate securely - return proto.authenticate( - password).addCallback( - cbAuthentication, - proto).addErrback( - ebAuthentication, proto, username, password - ) - - -def ebConnection(reason): - """ - Fallback error-handler. If anything goes wrong, log it and quit. - """ - log.startLogging(sys.stdout) - log.err(reason) - return reason - - -def cbAuthentication(result, proto): - """ - Callback after authentication has succeeded. - - Lists a bunch of mailboxes. - """ - return proto.select( - REGRESSIONS_FOLDER - ).addCallback( - cbSelectMbox, proto - ).addErrback( - ebSelectMbox, proto, REGRESSIONS_FOLDER) - - -def ebAuthentication(failure, proto, username, password): - """ - Errback invoked when authentication fails. - - If it failed because no SASL mechanisms match, offer the user the choice - of logging in insecurely. - - If you are trying to connect to your Gmail account, you will be here! - """ - failure.trap(imap4.NoSupportedAuthentication) - return InsecureLogin(proto, username, password) - - -def InsecureLogin(proto, username, password): - """ - Raise insecure-login error. - """ - return proto.login( - username, password - ).addCallback( - cbAuthentication, proto) - - -def cbSelectMbox(result, proto): - """ - Callback invoked when select command finishes successfully. - - If any message is in the test folder, it will flag them as deleted and - expunge. - If no messages found, it will start with the APPEND tests. - """ - print "SELECT: %s EXISTS " % result.get("EXISTS", "??") - - if result["EXISTS"] != 0: - # Flag as deleted, expunge, and do an examine again. - print "There is mail here, will delete..." - return cbDeleteAndExpungeTestFolder(proto) - - else: - return cbAppendNextMessage(proto) - - -def ebSelectMbox(failure, proto, folder): - """ - Errback invoked when the examine command fails. - - Creates the folder. - """ - log.err(failure) - log.msg("Folder %r does not exist. Creating..." % (folder,)) - return proto.create(folder).addCallback(cbAuthentication, proto) - - -def ebExpunge(failure): - log.err(failure) - - -def cbDeleteAndExpungeTestFolder(proto): - """ - Callback invoked fom cbExamineMbox when the number of messages in the - mailbox is not zero. It flags all messages as deleted and expunge the - mailbox. - """ - return proto.setFlags( - "1:*", ("\\Deleted",) - ).addCallback( - lambda r: proto.expunge() - ).addCallback( - cbExpunge, proto - ).addErrback( - ebExpunge) - - -def cbExpunge(result, proto): - return proto.select( - REGRESSIONS_FOLDER - ).addCallback( - cbSelectMbox, proto - ).addErrback(ebSettingDeleted, proto) - - -def ebSettingDeleted(failure, proto): - """ - Report errors during deletion of messages in the mailbox. - """ - print failure.getTraceback() - - -def cbAppendNextMessage(proto): - """ - Appends the next message in the global queue to the test folder. - """ - # 1. Get the next test message from global tuple. - try: - next_sample = SAMPLES.pop() - except IndexError: - # we're done! - return proto.logout() - - print "\nAPPEND %s" % (next_sample,) - raw = open(next_sample).read() - msg = get_fd(raw) - return proto.append( - REGRESSIONS_FOLDER, msg - ).addCallback( - lambda r: proto.select(REGRESSIONS_FOLDER) - ).addCallback( - cbAppend, proto, raw - ).addErrback( - ebAppend, proto, raw) - - -def cbAppend(result, proto, orig_msg): - """ - Fetches the message right after an append. - """ - # XXX keep account of highest UID - uid = "1:*" - - return proto.fetchSpecific( - '%s' % uid, - headerType='', - headerArgs=['BODY.PEEK[]'], - ).addCallback( - cbCompareMessage, proto, orig_msg - ).addErrback(ebAppend, proto, orig_msg) - - -def ebAppend(failure, proto, raw): - """ - Errorback for the append operation - """ - print "ERROR WHILE APPENDING!" - print failure.getTraceback() - - -def cbPickMessage(result, proto): - """ - Pick a message. - """ - return proto.fetchSpecific( - '%s' % result, - headerType='', - headerArgs=['BODY.PEEK[]'], - ).addCallback(cbCompareMessage, proto) - - -def cbCompareMessage(result, proto, raw): - """ - Display message and compare it with the original one. - """ - parts_orig = get_msg_parts(raw) - - if result: - keys = result.keys() - keys.sort() - else: - print "[-] GOT NO RESULT" - return proto.logout() - - latest = max(keys) - - fetched_msg = result[latest][0][2] - parts_fetched = get_msg_parts(fetched_msg) - - equal = compare_msg_parts( - parts_orig, - parts_fetched) - - if equal: - print "[+] MESSAGES MATCH" - return cbAppendNextMessage(proto) - else: - print "[-] ERROR: MESSAGES DO NOT MATCH !!!" - print " ABORTING COMPARISON..." - # FIXME logout and print the subject ... - return proto.logout() - - -def cbClose(result): - """ - Close the connection when we finish everything. - """ - from twisted.internet import reactor - reactor.stop() - - -def main(): - import glob - import sys - - if len(sys.argv) != 4: - print "Usage: regressions <user> <pass> <samples-folder>" - sys.exit() - - hostname = "localhost" - port = "1984" - username = sys.argv[1] - password = sys.argv[2] - - samplesdir = sys.argv[3] - - if not os.path.isdir(samplesdir): - print ("Could not find samples folder! " - "Make sure of copying mail_breaker contents there.") - sys.exit() - - samples = glob.glob(samplesdir + '/*') - - global SAMPLES - SAMPLES = [] - SAMPLES += samples - - onConn = defer.Deferred( - ).addCallback( - cbServerGreeting, username, password - ).addErrback( - ebConnection - ).addBoth(cbClose) - - factory = SimpleIMAP4ClientFactory(username, onConn) - - from twisted.internet import reactor - reactor.connectTCP(hostname, int(port), factory) - reactor.run() - - -if __name__ == '__main__': - main() diff --git a/mail/src/leap/mail/imap/tests/rfc822.message b/mail/src/leap/mail/imap/tests/rfc822.message deleted file mode 120000 index b19cc28..0000000 --- a/mail/src/leap/mail/imap/tests/rfc822.message +++ /dev/null @@ -1 +0,0 @@ -../../tests/rfc822.message
\ No newline at end of file diff --git a/mail/src/leap/mail/imap/tests/rfc822.multi-minimal.message b/mail/src/leap/mail/imap/tests/rfc822.multi-minimal.message deleted file mode 120000 index e0aa678..0000000 --- a/mail/src/leap/mail/imap/tests/rfc822.multi-minimal.message +++ /dev/null @@ -1 +0,0 @@ -../../tests/rfc822.multi-minimal.message
\ No newline at end of file diff --git a/mail/src/leap/mail/imap/tests/rfc822.multi-nested.message b/mail/src/leap/mail/imap/tests/rfc822.multi-nested.message deleted file mode 120000 index 306d0de..0000000 --- a/mail/src/leap/mail/imap/tests/rfc822.multi-nested.message +++ /dev/null @@ -1 +0,0 @@ -../../tests/rfc822.multi-nested.message
\ No newline at end of file diff --git a/mail/src/leap/mail/imap/tests/rfc822.multi-signed.message b/mail/src/leap/mail/imap/tests/rfc822.multi-signed.message deleted file mode 120000 index 4172244..0000000 --- a/mail/src/leap/mail/imap/tests/rfc822.multi-signed.message +++ /dev/null @@ -1 +0,0 @@ -../../tests/rfc822.multi-signed.message
\ No newline at end of file diff --git a/mail/src/leap/mail/imap/tests/rfc822.multi.message b/mail/src/leap/mail/imap/tests/rfc822.multi.message deleted file mode 120000 index 62057d2..0000000 --- a/mail/src/leap/mail/imap/tests/rfc822.multi.message +++ /dev/null @@ -1 +0,0 @@ -../../tests/rfc822.multi.message
\ No newline at end of file diff --git a/mail/src/leap/mail/imap/tests/rfc822.plain.message b/mail/src/leap/mail/imap/tests/rfc822.plain.message deleted file mode 120000 index 5bab0e8..0000000 --- a/mail/src/leap/mail/imap/tests/rfc822.plain.message +++ /dev/null @@ -1 +0,0 @@ -../../tests/rfc822.plain.message
\ No newline at end of file diff --git a/mail/src/leap/mail/imap/tests/stress_tests_imap.zsh b/mail/src/leap/mail/imap/tests/stress_tests_imap.zsh deleted file mode 100755 index 544faca..0000000 --- a/mail/src/leap/mail/imap/tests/stress_tests_imap.zsh +++ /dev/null @@ -1,178 +0,0 @@ -#!/bin/zsh -# BATCH STRESS TEST FOR IMAP ---------------------- -# http://imgs.xkcd.com/comics/science.jpg -# -# Run imaptest against a LEAP IMAP server -# for a fixed period of time, and collect output. -# -# Author: Kali Kaneko -# Date: 2014 01 26 -# -# To run, you need to have `imaptest` in your path. -# See: -# http://www.imapwiki.org/ImapTest/Installation -# -# For the tests, I'm using a 10MB file sample that -# can be downloaded from: -# http://www.dovecot.org/tmp/dovecot-crlf -# -# Want to contribute to benchmarking? -# -# 1. Create a pristine account in a bitmask provider. -# -# 2. Launch your bitmask client, with different flags -# if you desire. -# -# For example to try the nosync flag in sqlite: -# -# LEAP_SQLITE_NOSYNC=1 bitmask --debug -N --offline -l /tmp/leap.log -# -# 3. Run at several points in time (ie: just after -# launching the bitmask client. one minute after, -# ten minutes after) -# -# mkdir data -# cd data -# ../leap_tests_imap.zsh | tee sqlite_nosync_run2.log -# -# 4. Submit your results to: kali at leap dot se -# together with the logs of the bitmask run. -# -# Please provide also details about your system, and -# the type of hard disk setup you are running against. -# - -# ------------------------------------------------ -# Edit these variables if you are too lazy to pass -# the user and mbox as parameters. Like me. - -USER="test_f14@dev.bitmask.net" -MBOX="~/leap/imaptest/data/dovecot-crlf" - -HOST="localhost" -PORT="1984" - -# in case you have it aliased -GREP="/bin/grep" -IMAPTEST="imaptest" - -# ----------------------------------------------- -# -# These should be kept constant across benchmarking -# runs across different machines, for comparability. - -DURATION=200 -NUM_MSG=200 - - -# TODO add another function, and a cli flag, to be able -# to take several aggretates spaced in time, along a period -# of several minutes. - -imaptest_cmd() { - stdbuf -o0 ${IMAPTEST} user=${USER} pass=1234 host=${HOST} \ - port=${PORT} mbox=${MBOX} clients=1 msgs=${NUM_MSG} \ - no_pipelining 2>/dev/null -} - -stress_imap() { - mkfifo imap_pipe - cat imap_pipe | tee output & - imaptest_cmd >> imap_pipe -} - -wait_and_kill() { - while : - do - sleep $DURATION - pkill -2 imaptest - rm imap_pipe - break - done -} - -print_results() { - sleep 1 - echo - echo - echo "AGGREGATED RESULTS" - echo "----------------------" - echo "\tavg\tstdev" - $GREP "avg" ./output | sed -e 's/^ *//g' -e 's/ *$//g' | \ - gawk ' -function avg(data, count) { - sum=0; - for( x=0; x <= count-1; x++) { - sum += data[x]; - } - return sum/count; -} -function std_dev(data, count) { - sum=0; - for( x=0; x <= count-1; x++) { - sum += data[x]; - } - average = sum/count; - - sumsq=0; - for( x=0; x <= count-1; x++) { - sumsq += (data[x] - average)^2; - } - return sqrt(sumsq/count); -} -BEGIN { - cnt = 0 -} END { - -printf("LOGI:\t%04.2lf\t%04.2f\n", avg(array[1], NR), std_dev(array[1], NR)); -printf("LIST:\t%04.2lf\t%04.2f\n", avg(array[2], NR), std_dev(array[2], NR)); -printf("STAT:\t%04.2lf\t%04.2f\n", avg(array[3], NR), std_dev(array[3], NR)); -printf("SELE:\t%04.2lf\t%04.2f\n", avg(array[4], NR), std_dev(array[4], NR)); -printf("FETC:\t%04.2lf\t%04.2f\n", avg(array[5], NR), std_dev(array[5], NR)); -printf("FET2:\t%04.2lf\t%04.2f\n", avg(array[6], NR), std_dev(array[6], NR)); -printf("STOR:\t%04.2lf\t%04.2f\n", avg(array[7], NR), std_dev(array[7], NR)); -printf("DELE:\t%04.2lf\t%04.2f\n", avg(array[8], NR), std_dev(array[8], NR)); -printf("EXPU:\t%04.2lf\t%04.2f\n", avg(array[9], NR), std_dev(array[9], NR)); -printf("APPE:\t%04.2lf\t%04.2f\n", avg(array[10], NR), std_dev(array[10], NR)); -printf("LOGO:\t%04.2lf\t%04.2f\n", avg(array[11], NR), std_dev(array[11], NR)); - -print "" -print "TOT samples", NR; -} -{ - it = cnt++; - array[1][it] = $1; - array[2][it] = $2; - array[3][it] = $3; - array[4][it] = $4; - array[5][it] = $5; - array[6][it] = $6; - array[7][it] = $7; - array[8][it] = $8; - array[9][it] = $9; - array[10][it] = $10; - array[11][it] = $11; -}' -} - - -{ test $1 = "--help" } && { - echo "Usage: $0 [user@provider] [/path/to/sample.mbox]" - exit 0 -} - -# If the first parameter is passed, take it as the user -{ test $1 } && { - USER=$1 -} - -# If the second parameter is passed, take it as the mbox -{ test $2 } && { - MBOX=$2 -} - -echo "[+] LEAP IMAP TESTS" -echo "[+] Running imaptest for $DURATION seconds with $NUM_MSG messages" -wait_and_kill & -stress_imap -print_results diff --git a/mail/src/leap/mail/imap/tests/test_imap.py b/mail/src/leap/mail/imap/tests/test_imap.py deleted file mode 100644 index 9cca17f..0000000 --- a/mail/src/leap/mail/imap/tests/test_imap.py +++ /dev/null @@ -1,1060 +0,0 @@ -# -*- coding: utf-8 -*- -# test_imap.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 <http://www.gnu.org/licenses/>. -""" -Test case for leap.email.imap.server -TestCases taken from twisted tests and modified to make them work -against our implementation of the IMAPAccount. - -@authors: Kali Kaneko, <kali@leap.se> -XXX add authors from the original twisted tests. - -@license: GPLv3, see included LICENSE file -""" -# XXX review license of the original tests!!! -import os -import string -import types - - -from twisted.mail import imap4 -from twisted.internet import defer -from twisted.python import util -from twisted.python import failure - -from twisted import cred - -from leap.mail.imap.mailbox import IMAPMailbox -from leap.mail.imap.messages import CaseInsensitiveDict -from leap.mail.testing.imap import IMAP4HelperMixin - - -TEST_USER = "testuser@leap.se" -TEST_PASSWD = "1234" - - -def strip(f): - return lambda result, f=f: f() - - -def sortNest(l): - l = l[:] - l.sort() - for i in range(len(l)): - if isinstance(l[i], types.ListType): - l[i] = sortNest(l[i]) - elif isinstance(l[i], types.TupleType): - l[i] = tuple(sortNest(list(l[i]))) - return l - - -class TestRealm: - """ - A minimal auth realm for testing purposes only - """ - theAccount = None - - def requestAvatar(self, avatarId, mind, *interfaces): - return imap4.IAccount, self.theAccount, lambda: None - -# -# TestCases -# - -# DEBUG --- -# from twisted.internet.base import DelayedCall -# DelayedCall.debug = True - - -class LEAPIMAP4ServerTestCase(IMAP4HelperMixin): - - """ - Tests for the generic behavior of the LEAPIMAP4Server - which, right now, it's just implemented in this test file as - LEAPIMAPServer. We will move the implementation, together with - authentication bits, to leap.mail.imap.server so it can be instantiated - from the tac file. - - Right now this TestCase tries to mimmick as close as possible the - organization from the twisted.mail.imap tests so we can achieve - a complete implementation. The order in which they appear reflect - the intended order of implementation. - """ - - # - # mailboxes operations - # - - def testCreate(self): - """ - Test whether we can create mailboxes - """ - succeed = ('testbox', 'test/box', 'test/', 'test/box/box', 'foobox') - fail = ('testbox', 'test/box') - acc = self.server.theAccount - - def cb(): - self.result.append(1) - - def eb(failure): - self.result.append(0) - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def create(): - create_deferreds = [] - for name in succeed + fail: - d = self.client.create(name) - d.addCallback(strip(cb)).addErrback(eb) - create_deferreds.append(d) - dd = defer.gatherResults(create_deferreds) - dd.addCallbacks(self._cbStopClient, self._ebGeneral) - return dd - - self.result = [] - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(create)) - d2 = self.loopback() - d = defer.gatherResults([d1, d2], consumeErrors=True) - d.addCallback(lambda _: acc.account.list_all_mailbox_names()) - return d.addCallback(self._cbTestCreate, succeed, fail) - - def _cbTestCreate(self, mailboxes, succeed, fail): - self.assertEqual(self.result, [1] * len(succeed) + [0] * len(fail)) - - answers = ([u'INBOX', u'testbox', u'test/box', u'test', - u'test/box/box', 'foobox']) - self.assertEqual(sorted(mailboxes), sorted([a for a in answers])) - - def testDelete(self): - """ - Test whether we can delete mailboxes - """ - def add_mailbox(): - return self.server.theAccount.addMailbox('test-delete/me') - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def delete(): - return self.client.delete('test-delete/me') - - acc = self.server.theAccount.account - - d1 = self.connected.addCallback(add_mailbox) - d1.addCallback(strip(login)) - d1.addCallbacks(strip(delete), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: acc.list_all_mailbox_names()) - d.addCallback(lambda mboxes: self.assertEqual( - mboxes, ['INBOX'])) - return d - - def testIllegalInboxDelete(self): - """ - Test what happens if we try to delete the user Inbox. - We expect that operation to fail. - """ - self.stashed = None - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def delete(): - return self.client.delete('inbox') - - def stash(result): - self.stashed = result - - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(delete), self._ebGeneral) - d1.addBoth(stash) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: self.failUnless(isinstance(self.stashed, - failure.Failure))) - return d - - def testNonExistentDelete(self): - """ - Test what happens if we try to delete a non-existent mailbox. - We expect an error raised stating 'No such mailbox' - """ - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def delete(): - return self.client.delete('delete/me') - self.failure = failure - - def deleteFailed(failure): - self.failure = failure - - self.failure = None - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(delete)).addErrback(deleteFailed) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: self.assertTrue( - str(self.failure.value).startswith('No such mailbox'))) - return d - - def testIllegalDelete(self): - """ - Try deleting a mailbox with sub-folders, and \NoSelect flag set. - An exception is expected. - """ - acc = self.server.theAccount - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def create_mailboxes(): - d1 = acc.addMailbox('delete') - d2 = acc.addMailbox('delete/me') - d = defer.gatherResults([d1, d2]) - return d - - def get_noselect_mailbox(mboxes): - mbox = mboxes[0] - return mbox.setFlags((r'\Noselect',)) - - def delete_mbox(ignored): - return self.client.delete('delete') - - def deleteFailed(failure): - self.failure = failure - - self.failure = None - - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(create_mailboxes)) - d1.addCallback(get_noselect_mailbox) - - d1.addCallback(delete_mbox).addErrback(deleteFailed) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - expected = ("Hierarchically inferior mailboxes exist " - "and \\Noselect is set") - d.addCallback(lambda _: - self.assertTrue(self.failure is not None)) - d.addCallback(lambda _: - self.assertEqual(str(self.failure.value), expected)) - return d - - # FIXME --- this test sometimes FAILS (timing issue). - # Some of the deferreds used in the rename op is not waiting for the - # operations properly - def testRename(self): - """ - Test whether we can rename a mailbox - """ - def create_mbox(): - return self.server.theAccount.addMailbox('oldmbox') - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def rename(): - return self.client.rename('oldmbox', 'newname') - - d1 = self.connected.addCallback(strip(create_mbox)) - d1.addCallback(strip(login)) - d1.addCallbacks(strip(rename), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: - self.server.theAccount.account.list_all_mailbox_names()) - d.addCallback(lambda mboxes: - self.assertItemsEqual(mboxes, ['INBOX', 'newname'])) - return d - - def testIllegalInboxRename(self): - """ - Try to rename inbox. We expect it to fail. Then it would be not - an inbox anymore, would it? - """ - self.stashed = None - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def rename(): - return self.client.rename('inbox', 'frotz') - - def stash(stuff): - self.stashed = stuff - - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(rename), self._ebGeneral) - d1.addBoth(stash) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: - self.failUnless(isinstance( - self.stashed, failure.Failure))) - return d - - def testHierarchicalRename(self): - """ - Try to rename hierarchical mailboxes - """ - acc = self.server.theAccount - - def add_mailboxes(): - return defer.gatherResults([ - acc.addMailbox('oldmbox/m1'), - acc.addMailbox('oldmbox/m2')]) - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def rename(): - return self.client.rename('oldmbox', 'newname') - - d1 = self.connected.addCallback(strip(add_mailboxes)) - d1.addCallback(strip(login)) - d1.addCallbacks(strip(rename), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: acc.account.list_all_mailbox_names()) - return d.addCallback(self._cbTestHierarchicalRename) - - def _cbTestHierarchicalRename(self, mailboxes): - expected = ['INBOX', 'newname/m1', 'newname/m2'] - self.assertEqual(sorted(mailboxes), sorted([s for s in expected])) - - def testSubscribe(self): - """ - Test whether we can mark a mailbox as subscribed to - """ - acc = self.server.theAccount - - def add_mailbox(): - return acc.addMailbox('this/mbox') - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def subscribe(): - return self.client.subscribe('this/mbox') - - def get_subscriptions(ignored): - return self.server.theAccount.getSubscriptions() - - d1 = self.connected.addCallback(strip(add_mailbox)) - d1.addCallback(strip(login)) - d1.addCallbacks(strip(subscribe), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(get_subscriptions) - d.addCallback(lambda subscriptions: - self.assertEqual(subscriptions, - ['this/mbox'])) - return d - - def testUnsubscribe(self): - """ - Test whether we can unsubscribe from a set of mailboxes - """ - acc = self.server.theAccount - - def add_mailboxes(): - return defer.gatherResults([ - acc.addMailbox('this/mbox'), - acc.addMailbox('that/mbox')]) - - def dc1(): - return acc.subscribe('this/mbox') - - def dc2(): - return acc.subscribe('that/mbox') - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def unsubscribe(): - return self.client.unsubscribe('this/mbox') - - def get_subscriptions(ignored): - return acc.getSubscriptions() - - d1 = self.connected.addCallback(strip(add_mailboxes)) - d1.addCallback(strip(login)) - d1.addCallback(strip(dc1)) - d1.addCallback(strip(dc2)) - d1.addCallbacks(strip(unsubscribe), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(get_subscriptions) - d.addCallback(lambda subscriptions: - self.assertEqual(subscriptions, - ['that/mbox'])) - return d - - def testSelect(self): - """ - Try to select a mailbox - """ - mbox_name = "TESTMAILBOXSELECT" - self.selectedArgs = None - - acc = self.server.theAccount - - def add_mailbox(): - return acc.addMailbox(mbox_name, creation_ts=42) - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def select(): - def selected(args): - self.selectedArgs = args - self._cbStopClient(None) - d = self.client.select(mbox_name) - d.addCallback(selected) - return d - - d1 = self.connected.addCallback(strip(add_mailbox)) - d1.addCallback(strip(login)) - d1.addCallback(strip(select)) - # d1.addErrback(self._ebGeneral) - - d2 = self.loopback() - - d = defer.gatherResults([d1, d2]) - d.addCallback(self._cbTestSelect) - return d - - def _cbTestSelect(self, ignored): - self.assertTrue(self.selectedArgs is not None) - - self.assertEqual(self.selectedArgs, { - 'EXISTS': 0, 'RECENT': 0, 'UIDVALIDITY': 42, - 'FLAGS': ('\\Seen', '\\Answered', '\\Flagged', - '\\Deleted', '\\Draft', '\\Recent', 'List'), - 'READ-WRITE': True - }) - - # - # capabilities - # - - def testCapability(self): - caps = {} - - def getCaps(): - def gotCaps(c): - caps.update(c) - self.server.transport.loseConnection() - return self.client.getCapabilities().addCallback(gotCaps) - - d1 = self.connected - d1.addCallback( - strip(getCaps)).addErrback(self._ebGeneral) - - d = defer.gatherResults([self.loopback(), d1]) - expected = {'IMAP4rev1': None, 'NAMESPACE': None, 'LITERAL+': None, - 'IDLE': None} - d.addCallback(lambda _: self.assertEqual(expected, caps)) - return d - - def testCapabilityWithAuth(self): - caps = {} - self.server.challengers[ - 'CRAM-MD5'] = cred.credentials.CramMD5Credentials - - def getCaps(): - def gotCaps(c): - caps.update(c) - self.server.transport.loseConnection() - return self.client.getCapabilities().addCallback(gotCaps) - d1 = self.connected.addCallback( - strip(getCaps)).addErrback(self._ebGeneral) - - d = defer.gatherResults([self.loopback(), d1]) - - expCap = {'IMAP4rev1': None, 'NAMESPACE': None, - 'IDLE': None, 'LITERAL+': None, - 'AUTH': ['CRAM-MD5']} - - d.addCallback(lambda _: self.assertEqual(expCap, caps)) - return d - - # - # authentication - # - - def testLogout(self): - """ - Test log out - """ - self.loggedOut = 0 - - def logout(): - def setLoggedOut(): - self.loggedOut = 1 - self.client.logout().addCallback(strip(setLoggedOut)) - self.connected.addCallback(strip(logout)).addErrback(self._ebGeneral) - d = self.loopback() - return d.addCallback(lambda _: self.assertEqual(self.loggedOut, 1)) - - def testNoop(self): - """ - Test noop command - """ - self.responses = None - - def noop(): - def setResponses(responses): - self.responses = responses - self.server.transport.loseConnection() - self.client.noop().addCallback(setResponses) - self.connected.addCallback(strip(noop)).addErrback(self._ebGeneral) - d = self.loopback() - return d.addCallback(lambda _: self.assertEqual(self.responses, [])) - - def testLogin(self): - """ - Test login - """ - def login(): - d = self.client.login(TEST_USER, TEST_PASSWD) - d.addCallback(self._cbStopClient) - d1 = self.connected.addCallback( - strip(login)).addErrback(self._ebGeneral) - d = defer.gatherResults([d1, self.loopback()]) - return d.addCallback(self._cbTestLogin) - - def _cbTestLogin(self, ignored): - self.assertEqual(self.server.state, 'auth') - - def testFailedLogin(self): - """ - Test bad login - """ - def login(): - d = self.client.login("bad_user@leap.se", TEST_PASSWD) - d.addBoth(self._cbStopClient) - - d1 = self.connected.addCallback( - strip(login)).addErrback(self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - return d.addCallback(self._cbTestFailedLogin) - - def _cbTestFailedLogin(self, ignored): - self.assertEqual(self.server.state, 'unauth') - self.assertEqual(self.server.account, None) - - def testLoginRequiringQuoting(self): - """ - Test login requiring quoting - """ - self.server.checker.userid = '{test}user@leap.se' - self.server.checker.password = '{test}password' - - def login(): - d = self.client.login('{test}user@leap.se', '{test}password') - d.addBoth(self._cbStopClient) - - d1 = self.connected.addCallback( - strip(login)).addErrback(self._ebGeneral) - d = defer.gatherResults([self.loopback(), d1]) - return d.addCallback(self._cbTestLoginRequiringQuoting) - - def _cbTestLoginRequiringQuoting(self, ignored): - self.assertEqual(self.server.state, 'auth') - - # - # Inspection - # - - def testNamespace(self): - """ - Test retrieving namespace - """ - self.namespaceArgs = None - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def namespace(): - def gotNamespace(args): - self.namespaceArgs = args - self._cbStopClient(None) - return self.client.namespace().addCallback(gotNamespace) - - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(namespace)) - d1.addErrback(self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: self.assertEqual(self.namespaceArgs, - [[['', '/']], [], []])) - return d - - def testExamine(self): - """ - L{IMAP4Client.examine} issues an I{EXAMINE} command to the server and - returns a L{Deferred} which fires with a C{dict} with as many of the - following keys as the server includes in its response: C{'FLAGS'}, - C{'EXISTS'}, C{'RECENT'}, C{'UNSEEN'}, C{'READ-WRITE'}, C{'READ-ONLY'}, - C{'UIDVALIDITY'}, and C{'PERMANENTFLAGS'}. - - Unfortunately the server doesn't generate all of these so it's hard to - test the client's handling of them here. See - L{IMAP4ClientExamineTests} below. - - See U{RFC 3501<http://www.faqs.org/rfcs/rfc3501.html>}, section 6.3.2, - for details. - """ - # TODO implement the IMAP4ClientExamineTests testcase. - mbox_name = "test_mailbox_e" - acc = self.server.theAccount - self.examinedArgs = None - - def add_mailbox(): - return acc.addMailbox(mbox_name, creation_ts=42) - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def examine(): - def examined(args): - self.examinedArgs = args - self._cbStopClient(None) - d = self.client.examine(mbox_name) - d.addCallback(examined) - return d - - d1 = self.connected.addCallback(strip(add_mailbox)) - d1.addCallback(strip(login)) - d1.addCallback(strip(examine)) - d1.addErrback(self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - return d.addCallback(self._cbTestExamine) - - def _cbTestExamine(self, ignored): - self.assertEqual(self.examinedArgs, { - 'EXISTS': 0, 'RECENT': 0, 'UIDVALIDITY': 42, - 'FLAGS': ('\\Seen', '\\Answered', '\\Flagged', - '\\Deleted', '\\Draft', '\\Recent', 'List'), - 'READ-WRITE': False}) - - def _listSetup(self, f, f2=None): - - acc = self.server.theAccount - - def dc1(): - return acc.addMailbox('root_subthing', creation_ts=42) - - def dc2(): - return acc.addMailbox('root_another_thing', creation_ts=42) - - def dc3(): - return acc.addMailbox('non_root_subthing', creation_ts=42) - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def listed(answers): - self.listed = answers - - self.listed = None - d1 = self.connected.addCallback(strip(login)) - d1.addCallback(strip(dc1)) - d1.addCallback(strip(dc2)) - d1.addCallback(strip(dc3)) - - if f2 is not None: - d1.addCallback(f2) - - d1.addCallbacks(strip(f), self._ebGeneral) - d1.addCallbacks(listed, self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - return defer.gatherResults([d1, d2]).addCallback(lambda _: self.listed) - - def testList(self): - """ - Test List command - """ - def list(): - return self.client.list('root', '%') - - d = self._listSetup(list) - d.addCallback(lambda listed: self.assertEqual( - sortNest(listed), - sortNest([ - (IMAPMailbox.init_flags, "/", "root_subthing"), - (IMAPMailbox.init_flags, "/", "root_another_thing") - ]) - )) - return d - - def testLSub(self): - """ - Test LSub command - """ - acc = self.server.theAccount - - def subs_mailbox(): - # why not client.subscribe instead? - return acc.subscribe('root_subthing') - - def lsub(): - return self.client.lsub('root', '%') - - d = self._listSetup(lsub, strip(subs_mailbox)) - d.addCallback(self.assertEqual, - [(IMAPMailbox.init_flags, "/", "root_subthing")]) - return d - - def testStatus(self): - """ - Test Status command - """ - acc = self.server.theAccount - - def add_mailbox(): - return acc.addMailbox('root_subthings') - - # XXX FIXME ---- should populate this a little bit, - # with unseen etc... - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def status(): - return self.client.status( - 'root_subthings', 'MESSAGES', 'UIDNEXT', 'UNSEEN') - - def statused(result): - self.statused = result - - self.statused = None - - d1 = self.connected.addCallback(strip(add_mailbox)) - d1.addCallback(strip(login)) - d1.addCallbacks(strip(status), self._ebGeneral) - d1.addCallbacks(statused, self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: self.assertEqual( - self.statused, - {'MESSAGES': 0, 'UIDNEXT': '1', 'UNSEEN': 0} - )) - return d - - def testFailedStatus(self): - """ - Test failed status command with a non-existent mailbox - """ - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def status(): - return self.client.status( - 'root/nonexistent', 'MESSAGES', 'UIDNEXT', 'UNSEEN') - - def statused(result): - self.statused = result - - def failed(failure): - self.failure = failure - - self.statused = self.failure = None - d1 = self.connected.addCallback(strip(login)) - d1.addCallbacks(strip(status), self._ebGeneral) - d1.addCallbacks(statused, failed) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - return defer.gatherResults([d1, d2]).addCallback( - self._cbTestFailedStatus) - - def _cbTestFailedStatus(self, ignored): - self.assertEqual( - self.statused, None - ) - self.assertEqual( - self.failure.value.args, - ('Could not open mailbox',) - ) - - # - # messages - # - - def testFullAppend(self): - """ - Test appending a full message to the mailbox - """ - infile = util.sibpath(__file__, 'rfc822.message') - message = open(infile) - acc = self.server.theAccount - mailbox_name = "appendmbox/subthing" - - def add_mailbox(): - return acc.addMailbox(mailbox_name) - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def append(): - return self.client.append( - mailbox_name, message, - ('\\SEEN', '\\DELETED'), - 'Tue, 17 Jun 2003 11:22:16 -0600 (MDT)', - ) - - d1 = self.connected.addCallback(strip(add_mailbox)) - d1.addCallback(strip(login)) - d1.addCallbacks(strip(append), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - - d.addCallback(lambda _: acc.getMailbox(mailbox_name)) - d.addCallback(lambda mb: mb.fetch(imap4.MessageSet(start=1), True)) - return d.addCallback(self._cbTestFullAppend, infile) - - def _cbTestFullAppend(self, fetched, infile): - fetched = list(fetched) - self.assertTrue(len(fetched) == 1) - self.assertTrue(len(fetched[0]) == 2) - uid, msg = fetched[0] - parsed = self.parser.parse(open(infile)) - expected_body = parsed.get_payload() - expected_headers = CaseInsensitiveDict(parsed.items()) - - def assert_flags(flags): - self.assertEqual( - set(('\\SEEN', '\\DELETED')), - set(flags)) - - def assert_date(date): - self.assertEqual( - 'Tue, 17 Jun 2003 11:22:16 -0600 (MDT)', - date) - - def assert_body(body): - gotbody = body.read() - self.assertEqual(expected_body, gotbody) - - def assert_headers(headers): - self.assertItemsEqual(map(string.lower, expected_headers), headers) - - d = defer.maybeDeferred(msg.getFlags) - d.addCallback(assert_flags) - - d.addCallback(lambda _: defer.maybeDeferred(msg.getInternalDate)) - d.addCallback(assert_date) - - d.addCallback( - lambda _: defer.maybeDeferred( - msg.getBodyFile, self._soledad)) - d.addCallback(assert_body) - - d.addCallback(lambda _: defer.maybeDeferred(msg.getHeaders, True)) - d.addCallback(assert_headers) - - return d - - def testPartialAppend(self): - """ - Test partially appending a message to the mailbox - """ - # TODO this test sometimes will fail because of the notify_just_mdoc - infile = util.sibpath(__file__, 'rfc822.message') - - acc = self.server.theAccount - - def add_mailbox(): - return acc.addMailbox('PARTIAL/SUBTHING') - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def append(): - message = file(infile) - return self.client.sendCommand( - imap4.Command( - 'APPEND', - 'PARTIAL/SUBTHING (\\SEEN) "Right now" ' - '{%d}' % os.path.getsize(infile), - (), self.client._IMAP4Client__cbContinueAppend, message - ) - ) - d1 = self.connected.addCallback(strip(add_mailbox)) - d1.addCallback(strip(login)) - d1.addCallbacks(strip(append), self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - - d.addCallback(lambda _: acc.getMailbox("PARTIAL/SUBTHING")) - d.addCallback(lambda mb: mb.fetch(imap4.MessageSet(start=1), True)) - return d.addCallback( - self._cbTestPartialAppend, infile) - - def _cbTestPartialAppend(self, fetched, infile): - fetched = list(fetched) - self.assertTrue(len(fetched) == 1) - self.assertTrue(len(fetched[0]) == 2) - uid, msg = fetched[0] - parsed = self.parser.parse(open(infile)) - expected_body = parsed.get_payload() - - def assert_flags(flags): - self.assertEqual( - set((['\\SEEN'])), set(flags)) - - def assert_body(body): - gotbody = body.read() - self.assertEqual(expected_body, gotbody) - - d = defer.maybeDeferred(msg.getFlags) - d.addCallback(assert_flags) - - d.addCallback(lambda _: defer.maybeDeferred(msg.getBodyFile)) - d.addCallback(assert_body) - return d - - def testCheck(self): - """ - Test check command - """ - def add_mailbox(): - return self.server.theAccount.addMailbox('root/subthing') - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def select(): - return self.client.select('root/subthing') - - def check(): - return self.client.check() - - d = self.connected.addCallbacks( - strip(add_mailbox), self._ebGeneral) - d.addCallbacks(lambda _: login(), self._ebGeneral) - d.addCallbacks(strip(select), self._ebGeneral) - d.addCallbacks(strip(check), self._ebGeneral) - d.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - return defer.gatherResults([d, d2]) - - # Okay, that was much fun indeed - - def testExpunge(self): - """ - Test expunge command - """ - acc = self.server.theAccount - mailbox_name = 'mailboxexpunge' - - def add_mailbox(): - return acc.addMailbox(mailbox_name) - - def login(): - return self.client.login(TEST_USER, TEST_PASSWD) - - def select(): - return self.client.select(mailbox_name) - - def save_mailbox(mailbox): - self.mailbox = mailbox - - def get_mailbox(): - d = acc.getMailbox(mailbox_name) - d.addCallback(save_mailbox) - return d - - def add_messages(): - d = self.mailbox.addMessage( - 'test 1', flags=('\\Deleted', 'AnotherFlag'), - notify_just_mdoc=False) - d.addCallback(lambda _: self.mailbox.addMessage( - 'test 2', flags=('AnotherFlag',), - notify_just_mdoc=False)) - d.addCallback(lambda _: self.mailbox.addMessage( - 'test 3', flags=('\\Deleted',), - notify_just_mdoc=False)) - return d - - def expunge(): - return self.client.expunge() - - def expunged(results): - self.failIf(self.server.mbox is None) - self.results = results - - self.results = None - d1 = self.connected.addCallback(strip(add_mailbox)) - d1.addCallback(strip(login)) - d1.addCallback(strip(get_mailbox)) - d1.addCallbacks(strip(add_messages), self._ebGeneral) - d1.addCallbacks(strip(select), self._ebGeneral) - d1.addCallbacks(strip(expunge), self._ebGeneral) - d1.addCallbacks(expunged, self._ebGeneral) - d1.addCallbacks(self._cbStopClient, self._ebGeneral) - d2 = self.loopback() - d = defer.gatherResults([d1, d2]) - d.addCallback(lambda _: self.mailbox.getMessageCount()) - return d.addCallback(self._cbTestExpunge) - - def _cbTestExpunge(self, count): - # we only left 1 mssage with no deleted flag - self.assertEqual(count, 1) - # the uids of the deleted messages - self.assertItemsEqual(self.results, [1, 3]) - - -class AccountTestCase(IMAP4HelperMixin): - """ - Test the Account. - """ - def _create_empty_mailbox(self): - return self.server.theAccount.addMailbox('') - - def _create_one_mailbox(self): - return self.server.theAccount.addMailbox('one') - - def test_illegalMailboxCreate(self): - self.assertRaises(AssertionError, self._create_empty_mailbox) - - -class IMAP4ServerSearchTestCase(IMAP4HelperMixin): - """ - Tests for the behavior of the search_* functions in L{imap5.IMAP4Server}. - """ - # XXX coming soon to your screens! - pass diff --git a/mail/src/leap/mail/imap/tests/walktree.py b/mail/src/leap/mail/imap/tests/walktree.py deleted file mode 100644 index f259a55..0000000 --- a/mail/src/leap/mail/imap/tests/walktree.py +++ /dev/null @@ -1,127 +0,0 @@ -# -*- coding: utf-8 -*- -# walktree.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 <http://www.gnu.org/licenses/>. -""" -Tests for the walktree module. -""" -import os -import sys -import pprint -from email import parser - -from leap.mail import walk as W - -DEBUG = os.environ.get("BITMASK_MAIL_DEBUG") - - -p = parser.Parser() - -# TODO pass an argument of the type of message - -################################################## -# Input from hell - -if len(sys.argv) > 1: - FILENAME = sys.argv[1] -else: - FILENAME = "rfc822.multi-signed.message" - -""" -FILENAME = "rfc822.plain.message" -FILENAME = "rfc822.multi-minimal.message" -""" - -msg = p.parse(open(FILENAME)) -DO_CHECK = False -################################################# - -parts = W.get_parts(msg) - -if DEBUG: - def trim(item): - item = item[:10] - [trim(part["phash"]) for part in parts if part.get('phash', None)] - -raw_docs = list(W.get_raw_docs(msg, parts)) - -body_phash_fun = [W.get_body_phash_simple, - W.get_body_phash_multi][int(msg.is_multipart())] -body_phash = body_phash_fun(W.get_payloads(msg)) -parts_map = W.walk_msg_tree(parts, body_phash=body_phash) - - -# TODO add missing headers! -expected = { - 'body': '1ddfa80485', - 'multi': True, - 'part_map': { - 1: { - 'headers': {'Content-Disposition': 'inline', - 'Content-Type': 'multipart/mixed; ' - 'boundary="z0eOaCaDLjvTGF2l"'}, - 'multi': True, - 'part_map': {1: {'ctype': 'text/plain', - 'headers': [ - ('Content-Type', - 'text/plain; charset=utf-8'), - ('Content-Disposition', - 'inline'), - ('Content-Transfer-Encoding', - 'quoted-printable')], - 'multi': False, - 'parts': 1, - 'phash': '1ddfa80485', - 'size': 206}, - 2: {'ctype': 'text/plain', - 'headers': [('Content-Type', - 'text/plain; charset=us-ascii'), - ('Content-Disposition', - 'attachment; ' - 'filename="attach.txt"')], - 'multi': False, - 'parts': 1, - 'phash': '7a94e4d769', - 'size': 133}, - 3: {'ctype': 'application/octet-stream', - 'headers': [('Content-Type', - 'application/octet-stream'), - ('Content-Disposition', - 'attachment; filename="hack.ico"'), - ('Content-Transfer-Encoding', - 'base64')], - 'multi': False, - 'parts': 1, - 'phash': 'c42cccebbd', - 'size': 12736}}}, - 2: {'ctype': 'application/pgp-signature', - 'headers': [('Content-Type', 'application/pgp-signature')], - 'multi': False, - 'parts': 1, - 'phash': '8f49fbf749', - 'size': 877}}} - -if DEBUG and DO_CHECK: - # TODO turn this into a proper unittest - assert(parts_map == expected) - print "Structure: OK" - - -print -print "RAW DOCS" -pprint.pprint(raw_docs) -print -print "PARTS MAP" -pprint.pprint(parts_map) diff --git a/mail/src/leap/mail/incoming/__init__.py b/mail/src/leap/mail/incoming/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/mail/src/leap/mail/incoming/__init__.py +++ /dev/null diff --git a/mail/src/leap/mail/incoming/service.py b/mail/src/leap/mail/incoming/service.py deleted file mode 100644 index 1e20862..0000000 --- a/mail/src/leap/mail/incoming/service.py +++ /dev/null @@ -1,844 +0,0 @@ -# -*- coding: utf-8 -*- -# service.py -# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>. -""" -Incoming mail fetcher. -""" -import copy -import logging -import shlex -import time -import warnings - -from email.parser import Parser -from email.utils import parseaddr -from email.utils import formatdate -from StringIO import StringIO -from urlparse import urlparse - -from twisted.application.service import Service -from twisted.python import log -from twisted.python.failure import Failure -from twisted.internet import defer, reactor -from twisted.internet.task import LoopingCall -from twisted.internet.task import deferLater - -from leap.common.events import emit_async, catalog -from leap.common.check import leap_assert, leap_assert_type -from leap.common.mail import get_email_charset -from leap.keymanager import errors as keymanager_errors -from leap.mail.adaptors import soledad_indexes as fields -from leap.mail.generator import Generator -from leap.mail.utils import json_loads, empty -from leap.soledad.client import Soledad -from leap.soledad.common.crypto import ENC_SCHEME_KEY, ENC_JSON_KEY -from leap.soledad.common.errors import InvalidAuthTokenError - - -logger = logging.getLogger(__name__) - -MULTIPART_ENCRYPTED = "multipart/encrypted" -MULTIPART_SIGNED = "multipart/signed" -PGP_BEGIN = "-----BEGIN PGP MESSAGE-----" -PGP_END = "-----END PGP MESSAGE-----" - -# The period between succesive checks of the incoming mail -# queue (in seconds) -INCOMING_CHECK_PERIOD = 60 - - -class MalformedMessage(Exception): - """ - Raised when a given message is not well formed. - """ - pass - - -class IncomingMail(Service): - """ - Fetches and process mail from the incoming pool. - - This object implements IService interface, has public methods - startService and stopService that will actually initiate a - LoopingCall with check_period recurrency. - The LoopingCall itself will invoke the fetch method each time - that the check_period expires. - - This loop will sync the soledad db with the remote server and - process all the documents found tagged as incoming mail. - """ - # TODO implements IService? - - name = "IncomingMail" - - RECENT_FLAG = "\\Recent" - CONTENT_KEY = "content" - - LEAP_SIGNATURE_HEADER = 'X-Leap-Signature' - LEAP_ENCRYPTION_HEADER = 'X-Leap-Encryption' - """ - Header added to messages when they are decrypted by the fetcher, - which states the validity of an eventual signature that might be included - in the encrypted blob. - """ - LEAP_SIGNATURE_VALID = 'valid' - LEAP_SIGNATURE_INVALID = 'invalid' - LEAP_SIGNATURE_COULD_NOT_VERIFY = 'could not verify' - - LEAP_ENCRYPTION_DECRYPTED = 'decrypted' - - def __init__(self, keymanager, soledad, inbox, userid, - check_period=INCOMING_CHECK_PERIOD): - - """ - Initialize IncomingMail.. - - :param keymanager: a keymanager instance - :type keymanager: keymanager.KeyManager - - :param soledad: a soledad instance - :type soledad: Soledad - - :param inbox: the collection for the inbox where the new emails will be - stored - :type inbox: MessageCollection - - :param check_period: the period to fetch new mail, in seconds. - :type check_period: int - """ - leap_assert(keymanager, "need a keymanager to initialize") - leap_assert_type(soledad, Soledad) - leap_assert(check_period, "need a period to check incoming mail") - leap_assert_type(check_period, int) - leap_assert(userid, "need a userid to initialize") - - self._keymanager = keymanager - self._soledad = soledad - self._inbox_collection = inbox - self._userid = userid - - self._listeners = [] - self._loop = None - self._check_period = check_period - - # initialize a mail parser only once - self._parser = Parser() - - def add_listener(self, listener): - """ - Add a listener to inbox insertions. - - This listener function will be called for each message added to the - inbox with its uid as parameter. This function should not be blocking - or it will block the incoming queue. - - :param listener: the listener function - :type listener: callable - """ - self._listeners.append(listener) - - # - # Public API: fetch, start_loop, stop. - # - - def fetch(self): - """ - Fetch incoming mail, to be called periodically. - - Calls a deferred that will execute the fetch callback. - """ - def _sync_errback(failure): - log.err(failure) - - def syncSoledadCallback(_): - # XXX this should be moved to adaptors - d = self._soledad.get_from_index( - fields.JUST_MAIL_IDX, "1", "0") - d.addCallback(self._process_doclist) - d.addErrback(_sync_errback) - return d - - logger.debug("fetching mail for: %s %s" % ( - self._soledad.uuid, self._userid)) - d = self._sync_soledad() - d.addCallbacks(syncSoledadCallback, self._errback) - d.addCallbacks(self._signal_fetch_to_ui, self._errback) - return d - - def startService(self): - """ - Starts a loop to fetch mail. - - :returns: A Deferred whose callback will be invoked with - the LoopingCall instance when loop.stop is called, or - whose errback will be invoked when the function raises an - exception or returned a deferred that has its errback - invoked. - """ - Service.startService(self) - if self._loop is None: - self._loop = LoopingCall(self.fetch) - stop_deferred = self._loop.start(self._check_period) - return stop_deferred - else: - logger.warning("Tried to start an already running fetching loop.") - return defer.fail(Failure('Already running loop.')) - - def stopService(self): - """ - Stops the loop that fetches mail. - """ - if self._loop and self._loop.running is True: - self._loop.stop() - self._loop = None - Service.stopService(self) - - # - # Private methods. - # - - # synchronize incoming mail - - def _errback(self, failure): - log.err(failure) - - def _sync_soledad(self): - """ - Synchronize with remote soledad. - - :returns: a list of LeapDocuments, or None. - :rtype: iterable or None - """ - def _log_synced(result): - log.msg('FETCH soledad SYNCED.') - return result - - def _signal_invalid_auth(failure): - failure.trap(InvalidAuthTokenError) - # if the token is invalid, send an event so the GUI can - # disable mail and show an error message. - emit_async(catalog.SOLEDAD_INVALID_AUTH_TOKEN, self._userid) - - log.msg('FETCH: syncing soledad...') - d = self._soledad.sync() - d.addCallbacks(_log_synced, _signal_invalid_auth) - return d - - def _signal_fetch_to_ui(self, doclist): - """ - Send leap events to ui. - - :param doclist: iterable with msg documents. - :type doclist: iterable. - :returns: doclist - :rtype: iterable - """ - if doclist: - fetched_ts = time.mktime(time.gmtime()) - num_mails = len(doclist) if doclist is not None else 0 - if num_mails != 0: - log.msg("there are %s mails" % (num_mails,)) - emit_async(catalog.MAIL_FETCHED_INCOMING, self._userid, - str(num_mails), str(fetched_ts)) - return doclist - - def _signal_unread_to_ui(self, *args): - """ - Sends unread event to ui. - """ - emit_async(catalog.MAIL_UNREAD_MESSAGES, self._userid, - str(self._inbox_collection.count_unseen())) - - # process incoming mail. - - def _process_doclist(self, doclist): - """ - Iterates through the doclist, checks if each doc - looks like a message, and yields a deferred that will decrypt and - process the message. - - :param doclist: iterable with msg documents. - :type doclist: iterable. - :returns: a list of deferreds for individual messages. - """ - log.msg('processing doclist') - if not doclist: - logger.debug("no docs found") - return - num_mails = len(doclist) - - deferreds = [] - for index, doc in enumerate(doclist): - logger.debug("processing doc %d of %d" % (index + 1, num_mails)) - emit_async(catalog.MAIL_MSG_PROCESSING, self._userid, - str(index), str(num_mails)) - - keys = doc.content.keys() - - # TODO Compatibility check with the index in pre-0.6 mx - # that does not write the ERROR_DECRYPTING_KEY - # This should be removed in 0.7 - - has_errors = doc.content.get(fields.ERROR_DECRYPTING_KEY, None) - - if has_errors is None: - warnings.warn("JUST_MAIL_COMPAT_IDX will be deprecated!", - DeprecationWarning) - - if has_errors: - logger.debug("skipping msg with decrypting errors...") - elif self._is_msg(keys): - # TODO this pipeline is a bit obscure! - d = self._decrypt_doc(doc) - d.addCallback(self._maybe_extract_keys) - d.addCallbacks(self._add_message_locally, self._errback) - deferreds.append(d) - - d = defer.gatherResults(deferreds, consumeErrors=True) - d.addCallback(lambda _: doclist) - return d - - # - # operations on individual messages - # - - def _decrypt_doc(self, doc): - """ - Decrypt the contents of a document. - - :param doc: A document containing an encrypted message. - :type doc: SoledadDocument - - :return: A Deferred that will be fired with the document and the - decrypted message. - :rtype: SoledadDocument, str - """ - log.msg('decrypting msg') - - def process_decrypted(res): - if isinstance(res, tuple): - decrdata, _ = res - success = True - else: - decrdata = "" - success = False - - emit_async(catalog.MAIL_MSG_DECRYPTED, self._userid, - "1" if success else "0") - return self._process_decrypted_doc(doc, decrdata) - - d = self._keymanager.decrypt(doc.content[ENC_JSON_KEY], self._userid) - d.addErrback(self._errback) - d.addCallback(process_decrypted) - d.addCallback(lambda data: (doc, data)) - return d - - def _process_decrypted_doc(self, doc, data): - """ - Process a document containing a succesfully decrypted message. - - :param doc: the incoming message - :type doc: SoledadDocument - :param data: the json-encoded, decrypted content of the incoming - message - :type data: str - - :return: a Deferred that will be fired with an str of the proccessed - data. - :rtype: Deferred - """ - log.msg('processing decrypted doc') - - # XXX turn this into an errBack for each one of - # the deferreds that would process an individual document - try: - msg = json_loads(data) - except UnicodeError as exc: - logger.error("Error while decrypting %s" % (doc.doc_id,)) - logger.exception(exc) - - # we flag the message as "with decrypting errors", - # to avoid further decryption attempts during sync - # cycles until we're prepared to deal with that. - # What is the same, when Ivan deals with it... - # A new decrypting attempt event could be triggered by a - # future a library upgrade, or a cli flag to the client, - # we just `defer` that for now... :) - doc.content[fields.ERROR_DECRYPTING_KEY] = True - deferLater(reactor, 0, self._update_incoming_message, doc) - - # FIXME this is just a dirty hack to delay the proper - # deferred organization here... - # and remember, boys, do not do this at home. - return [] - - if not isinstance(msg, dict): - defer.returnValue(False) - if not msg.get(fields.INCOMING_KEY, False): - defer.returnValue(False) - - # ok, this is an incoming message - rawmsg = msg.get(self.CONTENT_KEY, None) - if rawmsg is None: - return "" - return self._maybe_decrypt_msg(rawmsg) - - def _update_incoming_message(self, doc): - """ - Do a put for a soledad document. This probably has been called only - in the case that we've needed to update the ERROR_DECRYPTING_KEY - flag in an incoming message, to get it out of the decrypting queue. - - :param doc: the SoledadDocument to update - :type doc: SoledadDocument - """ - log.msg("Updating Incoming MSG: SoledadDoc %s" % (doc.doc_id)) - return self._soledad.put_doc(doc) - - def _delete_incoming_message(self, doc): - """ - Delete document. - - :param doc: the SoledadDocument to delete - :type doc: SoledadDocument - """ - log.msg("Deleting Incoming message: %s" % (doc.doc_id,)) - return self._soledad.delete_doc(doc) - - def _maybe_decrypt_msg(self, data): - """ - Tries to decrypt a gpg message if data looks like one. - - :param data: the text to be decrypted. - :type data: str - - :return: a Deferred that will be fired with an str of data, possibly - decrypted. - :rtype: Deferred - """ - leap_assert_type(data, str) - log.msg('maybe decrypting doc') - - # parse the original message - encoding = get_email_charset(data) - msg = self._parser.parsestr(data) - - fromHeader = msg.get('from', None) - senderAddress = None - - if (fromHeader is not None and - (msg.get_content_type() == MULTIPART_ENCRYPTED or - msg.get_content_type() == MULTIPART_SIGNED)): - senderAddress = parseaddr(fromHeader)[1] - - def add_leap_header(ret): - decrmsg, signkey = ret - if (senderAddress is None or signkey is None or - isinstance(signkey, keymanager_errors.KeyNotFound)): - decrmsg.add_header( - self.LEAP_SIGNATURE_HEADER, - self.LEAP_SIGNATURE_COULD_NOT_VERIFY) - elif isinstance(signkey, keymanager_errors.InvalidSignature): - decrmsg.add_header( - self.LEAP_SIGNATURE_HEADER, - self.LEAP_SIGNATURE_INVALID) - else: - self._add_verified_signature_header(decrmsg, - signkey.fingerprint) - return decrmsg.as_string() - - if msg.get_content_type() == MULTIPART_ENCRYPTED: - d = self._decrypt_multipart_encrypted_msg( - msg, encoding, senderAddress) - elif msg.get_content_type() == MULTIPART_SIGNED: - d = self._verify_signature_not_encrypted_msg(msg, senderAddress) - else: - d = self._maybe_decrypt_inline_encrypted_msg( - msg, encoding, senderAddress) - d.addCallback(add_leap_header) - return d - - def _add_verified_signature_header(self, decrmsg, fingerprint): - decrmsg.add_header( - self.LEAP_SIGNATURE_HEADER, - self.LEAP_SIGNATURE_VALID, - pubkey=fingerprint) - - def _add_decrypted_header(self, msg): - msg.add_header(self.LEAP_ENCRYPTION_HEADER, - self.LEAP_ENCRYPTION_DECRYPTED) - - def _decrypt_multipart_encrypted_msg(self, msg, encoding, senderAddress): - """ - Decrypt a message with content-type 'multipart/encrypted'. - - :param msg: The original encrypted message. - :type msg: Message - :param encoding: The encoding of the email message. - :type encoding: str - :param senderAddress: The email address of the sender of the message. - :type senderAddress: str - - :return: A Deferred that will be fired with a tuple containing a - decrypted Message and the signing OpenPGPKey if the signature - is valid or InvalidSignature or KeyNotFound. - :rtype: Deferred - """ - log.msg('decrypting multipart encrypted msg') - msg = copy.deepcopy(msg) - self._msg_multipart_sanity_check(msg) - - # parse message and get encrypted content - pgpencmsg = msg.get_payload()[1] - encdata = pgpencmsg.get_payload() - - # decrypt or fail gracefully - def build_msg(res): - decrdata, signkey = res - - decrmsg = self._parser.parsestr(decrdata) - # remove original message's multipart/encrypted content-type - del(msg['content-type']) - - # replace headers back in original message - for hkey, hval in decrmsg.items(): - try: - # this will raise KeyError if header is not present - msg.replace_header(hkey, hval) - except KeyError: - msg[hkey] = hval - - # all ok, replace payload by unencrypted payload - msg.set_payload(decrmsg.get_payload()) - self._add_decrypted_header(msg) - return (msg, signkey) - - def verify_signature_after_decrypt_an_email(res): - decrdata, signkey = res - if decrdata.get_content_type() == MULTIPART_SIGNED: - res = self._verify_signature_not_encrypted_msg(decrdata, - senderAddress) - return res - - d = self._keymanager.decrypt( - encdata, self._userid, verify=senderAddress) - d.addCallbacks(build_msg, self._decryption_error, errbackArgs=(msg,)) - d.addCallbacks(verify_signature_after_decrypt_an_email) - return d - - def _maybe_decrypt_inline_encrypted_msg(self, origmsg, encoding, - senderAddress): - """ - Possibly decrypt an inline OpenPGP encrypted message. - - :param origmsg: The original, possibly encrypted message. - :type origmsg: Message - :param encoding: The encoding of the email message. - :type encoding: str - :param senderAddress: The email address of the sender of the message. - :type senderAddress: str - - :return: A Deferred that will be fired with a tuple containing a - decrypted Message and the signing OpenPGPKey if the signature - is valid or InvalidSignature or KeyNotFound. - :rtype: Deferred - """ - log.msg('maybe decrypting inline encrypted msg') - - data = self._serialize_msg(origmsg) - - def decrypted_data(res): - decrdata, signkey = res - replaced_data = data.replace(pgp_message, decrdata) - self._add_decrypted_header(origmsg) - return replaced_data, signkey - - def encode_and_return(res): - data, signkey = res - if isinstance(data, unicode): - data = data.encode(encoding, 'replace') - return (self._parser.parsestr(data), signkey) - - # handle exactly one inline PGP message - if PGP_BEGIN in data: - begin = data.find(PGP_BEGIN) - end = data.find(PGP_END) - pgp_message = data[begin:end + len(PGP_END)] - d = self._keymanager.decrypt( - pgp_message, self._userid, verify=senderAddress) - d.addCallbacks(decrypted_data, self._decryption_error, - errbackArgs=(data,)) - else: - d = defer.succeed((data, None)) - d.addCallback(encode_and_return) - return d - - def _verify_signature_not_encrypted_msg(self, origmsg, sender_address): - """ - Possibly decrypt an inline OpenPGP encrypted message. - - :param origmsg: The original, possibly encrypted message. - :type origmsg: Message - :param sender_address: The email address of the sender of the message. - :type sender_address: str - - :return: A Deferred that will be fired with a tuple containing a - signed Message and the signing OpenPGPKey if the signature - is valid or InvalidSignature. - :rtype: Deferred - """ - msg = copy.deepcopy(origmsg) - data = self._serialize_msg(msg.get_payload(0)) - detached_sig = self._extract_signature(msg) - d = self._keymanager.verify(data, sender_address, detached_sig) - - d.addCallback(lambda sign_key: (msg, sign_key)) - d.addErrback(lambda _: (msg, keymanager_errors.InvalidSignature())) - return d - - def _serialize_msg(self, origmsg): - buf = StringIO() - g = Generator(buf) - g.flatten(origmsg) - return buf.getvalue() - - def _extract_signature(self, msg): - body = msg.get_payload(0).get_payload() - - if isinstance(body, str): - body = msg.get_payload(0) - - detached_sig = msg.get_payload(1).get_payload() - msg.set_payload(body) - return detached_sig - - def _decryption_error(self, failure, msg): - """ - Check for known decryption errors - """ - if failure.check(keymanager_errors.DecryptError): - logger.warning('Failed to decrypt encrypted message (%s). ' - 'Storing message without modifications.' - % str(failure.value)) - return (msg, None) - elif failure.check(keymanager_errors.KeyNotFound): - logger.error('Failed to find private key for decryption (%s). ' - 'Storing message without modifications.' - % str(failure.value)) - return (msg, None) - else: - return failure - - @defer.inlineCallbacks - def _maybe_extract_keys(self, msgtuple): - """ - Retrieve attached keys to the mesage and parse message headers for an - *OpenPGP* header as described on the `IETF draft - <http://tools.ietf.org/html/draft-josefsson-openpgp-mailnews-header-06>` - only urls with https and the same hostname than the email are supported - for security reasons. - - :param msgtuple: a tuple consisting of a SoledadDocument - instance containing the incoming message - and data, the json-encoded, decrypted content of the - incoming message - :type msgtuple: (SoledadDocument, str) - - :return: A Deferred that will be fired with msgtuple when key - extraction finishes - :rtype: Deferred - """ - OpenPGP_HEADER = 'OpenPGP' - doc, data = msgtuple - - # XXX the parsing of the message is done in mailbox.addMessage, maybe - # we should do it in this module so we don't need to parse it again - # here - msg = self._parser.parsestr(data) - _, fromAddress = parseaddr(msg['from']) - - valid_attachment = False - if msg.is_multipart(): - valid_attachment = yield self._maybe_extract_attached_key( - msg.get_payload(), fromAddress) - - if not valid_attachment: - header = msg.get(OpenPGP_HEADER, None) - if header is not None: - yield self._maybe_extract_openpgp_header(header, fromAddress) - - defer.returnValue(msgtuple) - - def _maybe_extract_openpgp_header(self, header, address): - """ - Import keys from the OpenPGP header - - :param header: OpenPGP header string - :type header: str - :param address: email address in the from header - :type address: str - - :return: A Deferred that will be fired when header extraction is done - :rtype: Deferred - """ - d = defer.succeed(None) - fields = dict([f.strip(' ').split('=') for f in header.split(';')]) - if 'url' in fields: - url = shlex.split(fields['url'])[0] # remove quotations - urlparts = urlparse(url) - addressHostname = address.split('@')[1] - if ( - urlparts.scheme == 'https' and - urlparts.hostname == addressHostname - ): - def fetch_error(failure): - if failure.check(keymanager_errors.KeyNotFound): - logger.warning("Url from OpenPGP header %s failed" - % (url,)) - elif failure.check(keymanager_errors.KeyAttributesDiffer): - logger.warning("Key from OpenPGP header url %s didn't " - "match the from address %s" - % (url, address)) - else: - return failure - - d = self._keymanager.fetch_key(address, url) - d.addCallback( - lambda _: - logger.info("Imported key from header %s" % (url,))) - d.addErrback(fetch_error) - else: - logger.debug("No valid url on OpenPGP header %s" % (url,)) - else: - logger.debug("There is no url on the OpenPGP header: %s" - % (header,)) - return d - - def _maybe_extract_attached_key(self, attachments, address): - """ - Import keys from the attachments - - :param attachments: email attachment list - :type attachments: list(email.Message) - :param address: email address in the from header - :type address: str - - :return: A Deferred that will be fired when all the keys are stored - with a boolean: True if there was a valid key attached, or - False otherwise. - :rtype: Deferred - """ - MIME_KEY = "application/pgp-keys" - - def log_key_added(ignored): - logger.debug('Added key found in attachment for %s' % address) - return True - - def failed_put_key(failure): - logger.info("An error has ocurred adding attached key for %s: %s" - % (address, failure.getErrorMessage())) - return False - - deferreds = [] - for attachment in attachments: - if MIME_KEY == attachment.get_content_type(): - d = self._keymanager.put_raw_key( - attachment.get_payload(decode=True), address=address) - d.addCallbacks(log_key_added, failed_put_key) - deferreds.append(d) - d = defer.gatherResults(deferreds) - d.addCallback(lambda result: any(result)) - return d - - def _add_message_locally(self, msgtuple): - """ - Adds a message to local inbox and delete it from the incoming db - in soledad. - - :param msgtuple: a tuple consisting of a SoledadDocument - instance containing the incoming message - and data, the json-encoded, decrypted content of the - incoming message - :type msgtuple: (SoledadDocument, str) - - :return: A Deferred that will be fired when the messages is stored - :rtype: Defferred - """ - doc, raw_data = msgtuple - insertion_date = formatdate(time.time()) - log.msg('adding message %s to local db' % (doc.doc_id,)) - - def msgSavedCallback(result): - if empty(result): - return - - for listener in self._listeners: - listener(result) - - def signal_deleted(doc_id): - emit_async(catalog.MAIL_MSG_DELETED_INCOMING, - self._userid) - return doc_id - - emit_async(catalog.MAIL_MSG_SAVED_LOCALLY, self._userid) - d = self._delete_incoming_message(doc) - d.addCallback(signal_deleted) - return d - - d = self._inbox_collection.add_msg( - raw_data, (self.RECENT_FLAG,), date=insertion_date, - notify_just_mdoc=True) - d.addCallbacks(msgSavedCallback, self._errback) - return d - - # - # helpers - # - - def _msg_multipart_sanity_check(self, msg): - """ - Performs a sanity check against a multipart encrypted msg - - :param msg: The original encrypted message. - :type msg: Message - """ - # sanity check - payload = msg.get_payload() - if len(payload) != 2: - raise MalformedMessage( - 'Multipart/encrypted messages should have exactly 2 body ' - 'parts (instead of %d).' % len(payload)) - if payload[0].get_content_type() != 'application/pgp-encrypted': - raise MalformedMessage( - "Multipart/encrypted messages' first body part should " - "have content type equal to 'application/pgp-encrypted' " - "(instead of %s)." % payload[0].get_content_type()) - if payload[1].get_content_type() != 'application/octet-stream': - raise MalformedMessage( - "Multipart/encrypted messages' second body part should " - "have content type equal to 'octet-stream' (instead of " - "%s)." % payload[1].get_content_type()) - - def _is_msg(self, keys): - """ - Checks if the keys of a dictionary match the signature - of the document type we use for messages. - - :param keys: iterable containing the strings to match. - :type keys: iterable of strings. - :rtype: bool - """ - return ENC_SCHEME_KEY in keys and ENC_JSON_KEY in keys diff --git a/mail/src/leap/mail/incoming/tests/rfc822.multi-encrypt-signed.message b/mail/src/leap/mail/incoming/tests/rfc822.multi-encrypt-signed.message deleted file mode 100644 index 98304f2..0000000 --- a/mail/src/leap/mail/incoming/tests/rfc822.multi-encrypt-signed.message +++ /dev/null @@ -1,61 +0,0 @@ -Content-Type: multipart/encrypted; - boundary="Apple-Mail=_C01A1464-6C43-43BF-8F62-157335B7E25B"; - protocol="application/pgp-encrypted"; -Subject: Enc signed -Mime-Version: 1.0 (Mac OS X Mail 9.3 \(3124\)) -From: Leap Test Key <leap@leap.se> -Date: Tue, 24 May 2016 11:47:24 -0300 -Content-Description: OpenPGP encrypted message -To: leap@leap.se - -This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156) ---Apple-Mail=_C01A1464-6C43-43BF-8F62-157335B7E25B -Content-Type: application/pgp-encrypted -Content-Description: PGP/MIME Versions Identification - ---Apple-Mail=_C01A1464-6C43-43BF-8F62-157335B7E25B -Content-Disposition: inline; - filename=encrypted.asc -Content-Type: application/octet-stream; - name=encrypted.asc -Content-Description: OpenPGP encrypted message - ------BEGIN PGP MESSAGE----- -Version: GnuPG v2 - -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 -=tEyc ------END PGP MESSAGE----- - ---Apple-Mail=_C01A1464-6C43-43BF-8F62-157335B7E25B--
\ No newline at end of file diff --git a/mail/src/leap/mail/incoming/tests/test_incoming_mail.py b/mail/src/leap/mail/incoming/tests/test_incoming_mail.py deleted file mode 100644 index 29422ec..0000000 --- a/mail/src/leap/mail/incoming/tests/test_incoming_mail.py +++ /dev/null @@ -1,391 +0,0 @@ -# -*- coding: utf-8 -*- -# test_incoming_mail.py -# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>. -""" -Test case for leap.mail.incoming.service - -@authors: Ruben Pollan, <meskio@sindominio.net> - -@license: GPLv3, see included LICENSE file -""" - -import json -import os -import tempfile -import uuid - -from email.mime.application import MIMEApplication -from email.mime.multipart import MIMEMultipart -from email.parser import Parser -from mock import Mock - -from twisted.internet import defer -from twisted.python import log - -from leap.keymanager.errors import KeyAddressMismatch -from leap.mail.adaptors import soledad_indexes as fields -from leap.mail.adaptors.soledad import cleanup_deferred_locks -from leap.mail.adaptors.soledad import SoledadMailAdaptor -from leap.mail.mail import MessageCollection -from leap.mail.mailbox_indexer import MailboxIndexer - -from leap.mail.incoming.service import IncomingMail -from leap.mail.rfc3156 import MultipartEncrypted, PGPEncrypted -from leap.mail.testing import KeyManagerWithSoledadTestCase -from leap.mail.testing import ADDRESS, ADDRESS_2 -from leap.soledad.common.document import SoledadDocument -from leap.soledad.common.crypto import ( - EncryptionSchemes, - ENC_JSON_KEY, - ENC_SCHEME_KEY, -) - -HERE = os.path.split(os.path.abspath(__file__))[0] - -# TODO: add some tests for encrypted, unencrypted, signed and unsgined messages - - -class IncomingMailTestCase(KeyManagerWithSoledadTestCase): - """ - Tests for the incoming mail parser - """ - NICKSERVER = "http://domain" - BODY = """ -Governments of the Industrial World, you weary giants of flesh and steel, I -come from Cyberspace, the new home of Mind. On behalf of the future, I ask -you of the past to leave us alone. You are not welcome among us. You have -no sovereignty where we gather. - """ - EMAIL = """from: Test from SomeDomain <%(from)s> -to: %(to)s -subject: independence of cyberspace - -%(body)s - """ % { - "from": ADDRESS_2, - "to": ADDRESS, - "body": BODY - } - - def setUp(self): - cleanup_deferred_locks() - try: - del self._soledad - del self.km - except AttributeError: - pass - - # pytest handles correctly the setupEnv for the class, - # but trial ignores it. - if not getattr(self, 'tempdir', None): - self.tempdir = tempfile.mkdtemp() - - def getCollection(_): - adaptor = SoledadMailAdaptor() - store = self._soledad - adaptor.store = store - mbox_indexer = MailboxIndexer(store) - mbox_name = "INBOX" - mbox_uuid = str(uuid.uuid4()) - - def get_collection_from_mbox_wrapper(wrapper): - wrapper.uuid = mbox_uuid - return MessageCollection( - adaptor, store, - mbox_indexer=mbox_indexer, mbox_wrapper=wrapper) - - d = adaptor.initialize_store(store) - d.addCallback(lambda _: mbox_indexer.create_table(mbox_uuid)) - d.addCallback( - lambda _: adaptor.get_or_create_mbox(store, mbox_name)) - d.addCallback(get_collection_from_mbox_wrapper) - return d - - def setUpFetcher(inbox_collection): - self.fetcher = IncomingMail( - self.km, - self._soledad, - inbox_collection, - ADDRESS) - - # The messages don't exist on soledad will fail on deletion - self.fetcher._delete_incoming_message = Mock( - return_value=defer.succeed(None)) - - d = KeyManagerWithSoledadTestCase.setUp(self) - d.addCallback(getCollection) - d.addCallback(setUpFetcher) - d.addErrback(log.err) - return d - - def tearDown(self): - d = KeyManagerWithSoledadTestCase.tearDown(self) - return d - - def testExtractOpenPGPHeader(self): - """ - Test the OpenPGP header key extraction - """ - KEYURL = "https://leap.se/key.txt" - OpenPGP = "id=12345678; url=\"%s\"; preference=signencrypt" % (KEYURL,) - - message = Parser().parsestr(self.EMAIL) - message.add_header("OpenPGP", OpenPGP) - self.fetcher._keymanager.fetch_key = Mock( - return_value=defer.succeed(None)) - - def fetch_key_called(ret): - self.fetcher._keymanager.fetch_key.assert_called_once_with( - ADDRESS_2, KEYURL) - - d = self._create_incoming_email(message.as_string()) - d.addCallback( - lambda email: - self._mock_soledad_get_from_index(fields.JUST_MAIL_IDX, [email])) - d.addCallback(lambda _: self.fetcher.fetch()) - d.addCallback(fetch_key_called) - return d - - def testExtractOpenPGPHeaderInvalidUrl(self): - """ - Test the OpenPGP header key extraction - """ - KEYURL = "https://someotherdomain.com/key.txt" - OpenPGP = "id=12345678; url=\"%s\"; preference=signencrypt" % (KEYURL,) - - message = Parser().parsestr(self.EMAIL) - message.add_header("OpenPGP", OpenPGP) - self.fetcher._keymanager.fetch_key = Mock() - - def fetch_key_called(ret): - self.assertFalse(self.fetcher._keymanager.fetch_key.called) - - d = self._create_incoming_email(message.as_string()) - d.addCallback( - lambda email: - self._mock_soledad_get_from_index(fields.JUST_MAIL_IDX, [email])) - d.addCallback(lambda _: self.fetcher.fetch()) - d.addCallback(fetch_key_called) - return d - - def testExtractAttachedKey(self): - KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." - - message = MIMEMultipart() - message.add_header("from", ADDRESS_2) - key = MIMEApplication("", "pgp-keys") - key.set_payload(KEY) - message.attach(key) - self.fetcher._keymanager.put_raw_key = Mock( - return_value=defer.succeed(None)) - - def put_raw_key_called(_): - self.fetcher._keymanager.put_raw_key.assert_called_once_with( - KEY, address=ADDRESS_2) - - d = self._do_fetch(message.as_string()) - d.addCallback(put_raw_key_called) - return d - - def testExtractInvalidAttachedKey(self): - KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." - - message = MIMEMultipart() - message.add_header("from", ADDRESS_2) - key = MIMEApplication("", "pgp-keys") - key.set_payload(KEY) - message.attach(key) - self.fetcher._keymanager.put_raw_key = Mock( - return_value=defer.fail(KeyAddressMismatch())) - - def put_raw_key_called(_): - self.fetcher._keymanager.put_raw_key.assert_called_once_with( - KEY, address=ADDRESS_2) - - d = self._do_fetch(message.as_string()) - d.addCallback(put_raw_key_called) - d.addErrback(log.err) - return d - - def testExtractAttachedKeyAndNotOpenPGPHeader(self): - KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." - KEYURL = "https://leap.se/key.txt" - OpenPGP = "id=12345678; url=\"%s\"; preference=signencrypt" % (KEYURL,) - - message = MIMEMultipart() - message.add_header("from", ADDRESS_2) - message.add_header("OpenPGP", OpenPGP) - key = MIMEApplication("", "pgp-keys") - key.set_payload(KEY) - message.attach(key) - - self.fetcher._keymanager.put_raw_key = Mock( - return_value=defer.succeed(None)) - self.fetcher._keymanager.fetch_key = Mock() - - def put_raw_key_called(_): - self.fetcher._keymanager.put_raw_key.assert_called_once_with( - KEY, address=ADDRESS_2) - self.assertFalse(self.fetcher._keymanager.fetch_key.called) - - d = self._do_fetch(message.as_string()) - d.addCallback(put_raw_key_called) - return d - - def testExtractOpenPGPHeaderIfInvalidAttachedKey(self): - KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..." - KEYURL = "https://leap.se/key.txt" - OpenPGP = "id=12345678; url=\"%s\"; preference=signencrypt" % (KEYURL,) - - message = MIMEMultipart() - message.add_header("from", ADDRESS_2) - message.add_header("OpenPGP", OpenPGP) - key = MIMEApplication("", "pgp-keys") - key.set_payload(KEY) - message.attach(key) - - self.fetcher._keymanager.put_raw_key = Mock( - return_value=defer.fail(KeyAddressMismatch())) - self.fetcher._keymanager.fetch_key = Mock() - - def put_raw_key_called(_): - self.fetcher._keymanager.put_raw_key.assert_called_once_with( - KEY, address=ADDRESS_2) - self.fetcher._keymanager.fetch_key.assert_called_once_with( - ADDRESS_2, KEYURL) - - d = self._do_fetch(message.as_string()) - d.addCallback(put_raw_key_called) - return d - - def testAddDecryptedHeader(self): - class DummyMsg(): - - def __init__(self): - self.headers = {} - - def add_header(self, k, v): - self.headers[k] = v - - msg = DummyMsg() - self.fetcher._add_decrypted_header(msg) - - self.assertEquals(msg.headers['X-Leap-Encryption'], 'decrypted') - - def testDecryptEmail(self): - - self.fetcher._decryption_error = Mock() - self.fetcher._add_decrypted_header = Mock() - - def create_encrypted_message(encstr): - message = Parser().parsestr(self.EMAIL) - newmsg = MultipartEncrypted('application/pgp-encrypted') - for hkey, hval in message.items(): - newmsg.add_header(hkey, hval) - - encmsg = MIMEApplication( - encstr, _subtype='octet-stream', _encoder=lambda x: x) - encmsg.add_header('content-disposition', 'attachment', - filename='msg.asc') - # create meta message - metamsg = PGPEncrypted() - metamsg.add_header('Content-Disposition', 'attachment') - # attach pgp message parts to new message - newmsg.attach(metamsg) - newmsg.attach(encmsg) - return newmsg - - def decryption_error_not_called(_): - self.assertFalse(self.fetcher._decryption_error.called, - "There was some errors with decryption") - - def add_decrypted_header_called(_): - self.assertTrue(self.fetcher._add_decrypted_header.called, - "There was some errors with decryption") - - d = self.km.encrypt(self.EMAIL, ADDRESS, sign=ADDRESS_2) - d.addCallback(create_encrypted_message) - d.addCallback( - lambda message: - self._do_fetch(message.as_string())) - d.addCallback(decryption_error_not_called) - d.addCallback(add_decrypted_header_called) - return d - - def testValidateSignatureFromEncryptedEmailFromAppleMail(self): - enc_signed_file = os.path.join( - HERE, 'rfc822.multi-encrypt-signed.message') - self.fetcher._add_verified_signature_header = Mock() - - def add_verified_signature_header_called(_): - self.assertTrue(self.fetcher._add_verified_signature_header.called, - "There was some errors verifying signature") - - with open(enc_signed_file) as f: - enc_signed_raw = f.read() - - d = self._do_fetch(enc_signed_raw) - d.addCallback(add_verified_signature_header_called) - return d - - def testListener(self): - self.called = False - - def listener(uid): - self.called = True - - def listener_called(_): - self.assertTrue(self.called) - - self.fetcher.add_listener(listener) - d = self._do_fetch(self.EMAIL) - d.addCallback(listener_called) - return d - - def _do_fetch(self, message): - d = self._create_incoming_email(message) - d.addCallback( - lambda email: - self._mock_soledad_get_from_index(fields.JUST_MAIL_IDX, [email])) - d.addCallback(lambda _: self.fetcher.fetch()) - return d - - def _create_incoming_email(self, email_str): - email = SoledadDocument() - data = json.dumps( - {"incoming": True, "content": email_str}, - ensure_ascii=False) - - def set_email_content(encr_data): - email.content = { - fields.INCOMING_KEY: True, - fields.ERROR_DECRYPTING_KEY: False, - ENC_SCHEME_KEY: EncryptionSchemes.PUBKEY, - ENC_JSON_KEY: encr_data - } - return email - d = self.km.encrypt(data, ADDRESS, fetch_remote=False) - d.addCallback(set_email_content) - return d - - def _mock_soledad_get_from_index(self, index_name, value): - get_from_index = self._soledad.get_from_index - - def soledad_mock(idx_name, *key_values): - if index_name == idx_name: - return defer.succeed(value) - return get_from_index(idx_name, *key_values) - self.fetcher._soledad.get_from_index = Mock(side_effect=soledad_mock) diff --git a/mail/src/leap/mail/interfaces.py b/mail/src/leap/mail/interfaces.py deleted file mode 100644 index 10f5123..0000000 --- a/mail/src/leap/mail/interfaces.py +++ /dev/null @@ -1,215 +0,0 @@ -# -*- coding: utf-8 -*- -# interfaces.py -# Copyright (C) 2014,2015 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 <http://www.gnu.org/licenses/>. -""" -Interfaces for the leap.mail module. -""" -from zope.interface import Interface, Attribute - - -class IMessageWrapper(Interface): - """ - I know how to access the different parts into which a given message is - splitted into. - - :ivar fdoc: dict with flag document. - :ivar hdoc: dict with flag document. - :ivar cdocs: dict with content-documents, one-indexed. - """ - - fdoc = Attribute('A dictionaly-like containing the flags document ' - '(mutable)') - hdoc = Attribute('A dictionary-like containing the headers document ' - '(immutable)') - cdocs = Attribute('A dictionary with the content-docs, one-indexed') - - def create(self, store, notify_just_mdoc=False, pending_inserts_dict={}): - """ - Create the underlying wrapper. - """ - - def update(self, store): - """ - Update the only mutable parts, which are within the flags document. - """ - - def delete(self, store): - """ - Delete the parts for this wrapper that are not referenced from anywhere - else. - """ - - def copy(self, store, new_mbox_uuid): - """ - Return a copy of this IMessageWrapper in a new mailbox. - """ - - def set_mbox_uuid(self, mbox_uuid): - """ - Set the mailbox for this wrapper. - """ - - def set_flags(self, flags): - """ - """ - - def set_tags(self, tags): - """ - """ - - def set_date(self, date): - """ - """ - - def get_subpart_dict(self, index): - """ - :param index: the part to lookup, 1-indexed - """ - - def get_subpart_indexes(self): - """ - """ - - def get_body(self, store): - """ - """ - - -# TODO -- split into smaller interfaces? separate mailbox interface at least? - -class IMailAdaptor(Interface): - """ - I know how to store the standard representation for messages and mailboxes, - and how to update the relevant mutable parts when needed. - """ - - def initialize_store(self, store): - """ - Performs whatever initialization is needed before the store can be - used (creating indexes, sanity checks, etc). - - :param store: store - :returns: a Deferred that will fire when the store is correctly - initialized. - :rtype: deferred - """ - - def get_msg_from_string(self, MessageClass, raw_msg): - """ - Get an instance of a MessageClass initialized with a MessageWrapper - that contains all the parts obtained from parsing the raw string for - the message. - - :param MessageClass: an implementor of IMessage - :type raw_msg: str - :rtype: implementor of leap.mail.IMessage - """ - - def get_msg_from_docs(self, MessageClass, mdoc, fdoc, hdoc, cdocs=None, - uid=None): - """ - Get an instance of a MessageClass initialized with a MessageWrapper - that contains the passed part documents. - - This is not the recommended way of obtaining a message, unless you know - how to take care of ensuring the internal consistency between the part - documents, or unless you are glueing together the part documents that - have been previously generated by `get_msg_from_string`. - """ - - def get_flags_from_mdoc_id(self, store, mdoc_id): - """ - """ - - def create_msg(self, store, msg): - """ - :param store: an instance of soledad, or anything that behaves alike - :param msg: a Message object. - - :return: a Deferred that is fired when all the underlying documents - have been created. - :rtype: defer.Deferred - """ - - def update_msg(self, store, msg): - """ - :param msg: a Message object. - :param store: an instance of soledad, or anything that behaves alike - :return: a Deferred that is fired when all the underlying documents - have been updated (actually, it's only the fdoc that's allowed - to update). - :rtype: defer.Deferred - """ - - def get_count_unseen(self, store, mbox_uuid): - """ - Get the number of unseen messages for a given mailbox. - - :param store: instance of Soledad. - :param mbox_uuid: the uuid for this mailbox. - :rtype: int - """ - - def get_count_recent(self, store, mbox_uuid): - """ - Get the number of recent messages for a given mailbox. - - :param store: instance of Soledad. - :param mbox_uuid: the uuid for this mailbox. - :rtype: int - """ - - def get_mdoc_id_from_msgid(self, store, mbox_uuid, msgid): - """ - Get the UID for a message with the passed msgid (the one in the headers - msg-id). - This is used by the MUA to retrieve the recently saved draft. - """ - - # mbox handling - - def get_or_create_mbox(self, store, name): - """ - Get the mailbox with the given name, or create one if it does not - exist. - - :param store: instance of Soledad - :param name: the name of the mailbox - :type name: str - """ - - def update_mbox(self, store, mbox_wrapper): - """ - Update the documents for a given mailbox. - :param mbox_wrapper: MailboxWrapper instance - :type mbox_wrapper: MailboxWrapper - :return: a Deferred that will be fired when the mailbox documents - have been updated. - :rtype: defer.Deferred - """ - - def delete_mbox(self, store, mbox_wrapper): - """ - """ - - def get_all_mboxes(self, store): - """ - Retrieve a list with wrappers for all the mailboxes. - - :return: a deferred that will be fired with a list of all the - MailboxWrappers found. - :rtype: defer.Deferred - """ diff --git a/mail/src/leap/mail/load_tests.py b/mail/src/leap/mail/load_tests.py deleted file mode 100644 index be65b8d..0000000 --- a/mail/src/leap/mail/load_tests.py +++ /dev/null @@ -1,29 +0,0 @@ -# -*- coding: utf-8 -*- -# tests.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 <http://www.gnu.org/licenses/>. -""" -Provide a function for loading tests. -""" -import unittest - - -def load_tests(): - suite = unittest.TestSuite() - for test in unittest.defaultTestLoader.discover( - './src/leap/mail/', - top_level_dir='./src/'): - suite.addTest(test) - return suite diff --git a/mail/src/leap/mail/mail.py b/mail/src/leap/mail/mail.py deleted file mode 100644 index 2fde3a1..0000000 --- a/mail/src/leap/mail/mail.py +++ /dev/null @@ -1,1070 +0,0 @@ -# -*- coding: utf-8 -*- -# mail.py -# Copyright (C) 2014,2015 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 <http://www.gnu.org/licenses/>. -""" -Generic Access to Mail objects. - -This module holds the public LEAP Mail API, which should be viewed as the main -entry point for message and account manipulation, in a protocol-agnostic way. - -In the future, pluggable transports will expose this generic API. -""" -import itertools -import uuid -import logging -import StringIO -import time -import weakref - -from collections import defaultdict - -from twisted.internet import defer -from twisted.python import log - -from leap.common.check import leap_assert_type -from leap.common.events import emit_async, catalog - -from leap.mail.adaptors.soledad import SoledadMailAdaptor -from leap.mail.constants import INBOX_NAME -from leap.mail.constants import MessageFlags -from leap.mail.mailbox_indexer import MailboxIndexer -from leap.mail.plugins import soledad_sync_hooks -from leap.mail.utils import find_charset, CaseInsensitiveDict -from leap.mail.utils import lowerdict - -logger = logging.getLogger(name=__name__) - - -# TODO LIST -# [ ] Probably change the name of this module to "api" or "account", mail is -# too generic (there's also IncomingMail, and OutgoingMail -# [ ] Profile add_msg. - -def _get_mdoc_id(mbox, chash): - """ - Get the doc_id for the metamsg document. - """ - return "M+{mbox}+{chash}".format(mbox=mbox, chash=chash) - - -def _write_and_rewind(payload): - fd = StringIO.StringIO() - fd.write(payload) - fd.seek(0) - return fd - - -def _encode_payload(payload, ctype=""): - """ - Properly encode an unicode payload (which can be string or unicode) as a - string. - - :param payload: the payload to encode. currently soledad returns unicode - strings. - :type payload: basestring - :param ctype: optional, the content of the content-type header for this - payload. - :type ctype: str - :rtype: str - """ - # TODO Related, it's proposed that we're able to pass - # the encoding to the soledad documents. Better to store the charset there? - # FIXME ----------------------------------------------- - # this need a dedicated test-suite - charset = find_charset(ctype) - - # XXX get from mail headers if not multipart! - # Beware also that we should pass the proper encoding to - # soledad when it's creating the documents. - # if not charset: - # charset = get_email_charset(payload) - # ----------------------------------------------------- - - if not charset: - charset = "utf-8" - - try: - if isinstance(payload, unicode): - payload = payload.encode(charset) - except UnicodeError as exc: - logger.error( - "Unicode error, using 'replace'. {0!r}".format(exc)) - payload = payload.encode(charset, 'replace') - return payload - - -def _unpack_headers(headers_dict): - """ - Take a "packed" dict containing headers (with repeated keys represented as - line breaks inside each value, preceded by the header key) and return a - list of tuples in which each repeated key has a different tuple. - """ - headers_l = headers_dict.items() - for i, (k, v) in enumerate(headers_l): - splitted = v.split(k.lower() + ": ") - if len(splitted) != 1: - inner = zip( - itertools.cycle([k]), - map(lambda l: l.rstrip('\n'), splitted)) - headers_l = headers_l[:i] + inner + headers_l[i + 1:] - return headers_l - - -class MessagePart(object): - # TODO This class should be better abstracted from the data model. - # TODO support arbitrarily nested multiparts (right now we only support - # the trivial case) - """ - Represents a part of a multipart MIME Message. - """ - - def __init__(self, part_map, cdocs=None, nested=False): - """ - :param part_map: a dictionary mapping the subparts for - this MessagePart (1-indexed). - :type part_map: dict - - The format for the part_map is as follows: - - {u'ctype': u'text/plain', - u'headers': [[u'Content-Type', u'text/plain; charset="utf-8"'], - [u'Content-Transfer-Encoding', u'8bit']], - u'multi': False, - u'parts': 1, - u'phash': u'02D82B29F6BB0C8612D1C', - u'size': 132} - - :param cdocs: optional, a reference to the top-level dict of wrappers - for content-docs (1-indexed). - """ - if cdocs is None: - cdocs = {} - self._pmap = part_map - self._cdocs = cdocs - self._nested = nested - - def get_size(self): - """ - Size of the body, in octets. - """ - total = self._pmap['size'] - _h = self.get_headers() - headers = len( - '\n'.join(["%s: %s" % (k, v) for k, v in dict(_h).items()])) - # have to subtract 2 blank lines - return total - headers - 2 - - def get_body_file(self): - payload = "" - pmap = self._pmap - - multi = pmap.get('multi') - if not multi: - payload = self._get_payload(pmap.get('phash')) - if payload: - payload = _encode_payload(payload) - - return _write_and_rewind(payload) - - def get_headers(self): - return CaseInsensitiveDict(self._pmap.get("headers", [])) - - def is_multipart(self): - return self._pmap.get("multi", False) - - def get_subpart(self, part): - if not self.is_multipart(): - raise TypeError - sub_pmap = self._pmap.get("part_map", {}) - - try: - part_map = sub_pmap[str(part)] - except KeyError: - log.msg("getSubpart for %s: KeyError" % (part,)) - raise IndexError - return MessagePart(part_map, cdocs=self._cdocs, nested=True) - - def _get_payload(self, phash): - for cdocw in self._cdocs.values(): - if cdocw.phash == phash: - return cdocw.raw - return "" - - -class Message(object): - """ - Represents a single message, and gives access to all its attributes. - """ - - def __init__(self, wrapper, uid=None): - """ - :param wrapper: an instance of an implementor of IMessageWrapper - :param uid: - :type uid: int - """ - self._wrapper = wrapper - self._uid = uid - - def get_wrapper(self): - """ - Get the wrapper for this message. - """ - return self._wrapper - - def get_uid(self): - """ - Get the (optional) UID. - """ - return self._uid - - # imap.IMessage methods - - def get_flags(self): - """ - Get flags for this message. - :rtype: tuple - """ - return self._wrapper.fdoc.get_flags() - - def get_internal_date(self): - """ - Retrieve the date internally associated with this message - - According to the spec, this is NOT the date and time in the - RFC-822 header, but rather a date and time that reflects when the - message was received. - - * In SMTP, date and time of final delivery. - * In COPY, internal date/time of the source message. - * In APPEND, date/time specified. - - :return: An RFC822-formatted date string. - :rtype: str - """ - return self._wrapper.hdoc.date - - # imap.IMessageParts - - def get_headers(self): - """ - Get the raw headers document. - """ - return CaseInsensitiveDict(self._wrapper.hdoc.headers) - - def get_body_file(self, store): - """ - Get a file descriptor with the body content. - """ - def write_and_rewind_if_found(cdoc): - payload = cdoc.raw if cdoc else "" - # XXX pass ctype from headers if not multipart? - if payload: - payload = _encode_payload(payload, ctype=cdoc.content_type) - return _write_and_rewind(payload) - - d = defer.maybeDeferred(self._wrapper.get_body, store) - d.addCallback(write_and_rewind_if_found) - return d - - def get_size(self): - """ - Size of the whole message, in octets (including headers). - """ - total = self._wrapper.fdoc.size - return total - - def is_multipart(self): - """ - Return True if this message is multipart. - """ - return self._wrapper.fdoc.multi - - def get_subpart(self, part): - """ - :param part: The number of the part to retrieve, indexed from 1. - :type part: int - :rtype: MessagePart - """ - if not self.is_multipart(): - raise TypeError - try: - subpart_dict = self._wrapper.get_subpart_dict(part) - except KeyError: - raise IndexError - - return MessagePart( - subpart_dict, cdocs=self._wrapper.cdocs) - - # Custom methods. - - def get_tags(self): - """ - Get the tags for this message. - """ - return tuple(self._wrapper.fdoc.tags) - - -class Flagsmode(object): - """ - Modes for setting the flags/tags. - """ - APPEND = 1 - REMOVE = -1 - SET = 0 - - -class MessageCollection(object): - """ - A generic collection of messages. It can be messages sharing the same - mailbox, tag, the result of a given query, or just a bunch of ids for - master documents. - - Since LEAP Mail is primarily oriented to store mail in Soledad, the default - (and, so far, only) implementation of the store is contained in the - Soledad Mail Adaptor, which is passed to every collection on creation by - the root Account object. If you need to use a different adaptor, change the - adaptor class attribute in your Account object. - - Store is a reference to a particular instance of the message store (soledad - instance or proxy, for instance). - """ - - # TODO LIST - # [ ] look at IMessageSet methods - # [ ] make constructor with a per-instance deferredLock to use on - # creation/deletion? - # [ ] instead of a mailbox, we could pass an arbitrary container with - # pointers to different doc_ids (type: foo) - # [ ] To guarantee synchronicity of the documents sent together during a - # sync, we could get hold of a deferredLock that inhibits - # synchronization while we are updating (think more about this!) - # [ ] review the serveral count_ methods. I think it's better to patch - # server to accept deferreds. - # [ ] Use inheritance for the mailbox-collection instead of handling the - # special cases everywhere? - # [ ] or maybe a mailbox_only decorator... - - # Account should provide an adaptor instance when creating this collection. - adaptor = None - store = None - messageklass = Message - - _pending_inserts = dict() - - def __init__(self, adaptor, store, mbox_indexer=None, mbox_wrapper=None): - """ - Constructor for a MessageCollection. - """ - self.adaptor = adaptor - self.store = store - - # XXX think about what to do when there is no mbox passed to - # the initialization. We could still get the MetaMsg by index, instead - # of by doc_id. See get_message_by_content_hash - self.mbox_indexer = mbox_indexer - self.mbox_wrapper = mbox_wrapper - self._listeners = set([]) - - def is_mailbox_collection(self): - """ - Return True if this collection represents a Mailbox. - :rtype: bool - """ - return bool(self.mbox_wrapper) - - @property - def mbox_name(self): - # TODO raise instead? - if self.mbox_wrapper is None: - return None - return self.mbox_wrapper.mbox - - @property - def mbox_uuid(self): - # TODO raise instead? - if self.mbox_wrapper is None: - return None - return self.mbox_wrapper.uuid - - def get_mbox_attr(self, attr): - if self.mbox_wrapper is None: - raise RuntimeError("This is not a mailbox collection") - return getattr(self.mbox_wrapper, attr) - - def set_mbox_attr(self, attr, value): - if self.mbox_wrapper is None: - raise RuntimeError("This is not a mailbox collection") - setattr(self.mbox_wrapper, attr, value) - return self.mbox_wrapper.update(self.store) - - # Get messages - - def get_message_by_content_hash(self, chash, get_cdocs=False): - """ - Retrieve a message by its content hash. - :rtype: Deferred - """ - if not self.is_mailbox_collection(): - # TODO instead of getting the metamsg by chash, in this case we - # should query by (meta) index or use the internal collection of - # pointers-to-docs. - raise NotImplementedError() - - metamsg_id = _get_mdoc_id(self.mbox_name, chash) - - return self.adaptor.get_msg_from_mdoc_id( - self.messageklass, self.store, - metamsg_id, get_cdocs=get_cdocs) - - def get_message_by_sequence_number(self, msn, get_cdocs=False): - """ - Retrieve a message by its Message Sequence Number. - :rtype: Deferred - """ - def get_uid_for_msn(all_uid): - return all_uid[msn - 1] - d = self.all_uid_iter() - d.addCallback(get_uid_for_msn) - d.addCallback( - lambda uid: self.get_message_by_uid( - uid, get_cdocs=get_cdocs)) - d.addErrback(lambda f: log.err(f)) - return d - - def get_message_by_uid(self, uid, absolute=True, get_cdocs=False): - """ - Retrieve a message by its Unique Identifier. - - If this is a Mailbox collection, that is the message UID, unique for a - given mailbox, or a relative sequence number depending on the absolute - flag. For now, only absolute identifiers are supported. - :rtype: Deferred - """ - # TODO deprecate absolute flag, it doesn't make sense UID and - # !absolute. use _by_sequence_number instead. - if not absolute: - raise NotImplementedError("Does not support relative ids yet") - - get_doc_fun = self.mbox_indexer.get_doc_id_from_uid - - def get_msg_from_mdoc_id(doc_id): - if doc_id is None: - return None - return self.adaptor.get_msg_from_mdoc_id( - self.messageklass, self.store, - doc_id, uid=uid, get_cdocs=get_cdocs) - - def cleanup_and_get_doc_after_pending_insert(result): - for key in result: - self._pending_inserts.pop(key, None) - return get_doc_fun(self.mbox_uuid, uid) - - if not self._pending_inserts: - d = get_doc_fun(self.mbox_uuid, uid) - else: - d = defer.gatherResults(self._pending_inserts.values()) - d.addCallback(cleanup_and_get_doc_after_pending_insert) - d.addCallback(get_msg_from_mdoc_id) - return d - - def get_flags_by_uid(self, uid, absolute=True): - # TODO use sequence numbers - if not absolute: - raise NotImplementedError("Does not support relative ids yet") - - def get_flags_from_mdoc_id(doc_id): - if doc_id is None: # XXX needed? or bug? - return None - return self.adaptor.get_flags_from_mdoc_id( - self.store, doc_id) - - def wrap_in_tuple(flags): - return (uid, flags) - - d = self.mbox_indexer.get_doc_id_from_uid(self.mbox_uuid, uid) - d.addCallback(get_flags_from_mdoc_id) - d.addCallback(wrap_in_tuple) - return d - - def count(self): - """ - Count the messages in this collection. - :return: a Deferred that will fire with the integer for the count. - :rtype: Deferred - """ - if not self.is_mailbox_collection(): - raise NotImplementedError() - - d = self.mbox_indexer.count(self.mbox_uuid) - return d - - def count_recent(self): - """ - Count the recent messages in this collection. - :return: a Deferred that will fire with the integer for the count. - :rtype: Deferred - """ - if not self.is_mailbox_collection(): - raise NotImplementedError() - return self.adaptor.get_count_recent(self.store, self.mbox_uuid) - - def count_unseen(self): - """ - Count the unseen messages in this collection. - :return: a Deferred that will fire with the integer for the count. - :rtype: Deferred - """ - if not self.is_mailbox_collection(): - raise NotImplementedError() - return self.adaptor.get_count_unseen(self.store, self.mbox_uuid) - - def get_uid_next(self): - """ - Get the next integer beyond the highest UID count for this mailbox. - - :return: a Deferred that will fire with the integer for the next uid. - :rtype: Deferred - """ - return self.mbox_indexer.get_next_uid(self.mbox_uuid) - - def get_last_uid(self): - """ - Get the last UID for this mailbox. - """ - return self.mbox_indexer.get_last_uid(self.mbox_uuid) - - def all_uid_iter(self): - """ - Iterator through all the uids for this collection. - """ - return self.mbox_indexer.all_uid_iter(self.mbox_uuid) - - def get_uid_from_msgid(self, msgid): - """ - Return the UID(s) of the matching msg-ids for this mailbox collection. - """ - if not self.is_mailbox_collection(): - raise NotImplementedError() - - def get_uid(mdoc_id): - if not mdoc_id: - return None - d = self.mbox_indexer.get_uid_from_doc_id( - self.mbox_uuid, mdoc_id) - return d - - d = self.adaptor.get_mdoc_id_from_msgid( - self.store, self.mbox_uuid, msgid) - d.addCallback(get_uid) - return d - - # Manipulate messages - - def add_msg(self, raw_msg, flags=tuple(), tags=tuple(), date="", - notify_just_mdoc=False): - """ - Add a message to this collection. - - :param raw_msg: the raw message - :param flags: tuple of flags for this message - :param tags: tuple of tags for this message - :param date: - formatted date, it will be used to retrieve the internal - date for this message. According to the spec, this is NOT the date - and time in the RFC-822 header, but rather a date and time that - reflects when the message was received. - :type date: str - :param notify_just_mdoc: - boolean passed to the wrapper.create method, to indicate whether - we're insterested in being notified right after the mdoc has been - written (as it's the first doc to be written, and quite small, this - is faster, though potentially unsafe), or on the contrary we want - to wait untill all the parts have been written. - Used by the imap mailbox implementation to get faster responses. - This will be ignored (and set to False) if a heuristic for a Draft - message is met, which currently is a specific mozilla header. - :type notify_just_mdoc: bool - - :returns: a deferred that will fire with the UID of the inserted - message. - :rtype: deferred - """ - # TODO watch out if the use of this method in IMAP COPY/APPEND is - # passing the right date. - # XXX mdoc ref is a leaky abstraction here. generalize. - leap_assert_type(flags, tuple) - leap_assert_type(date, str) - - msg = self.adaptor.get_msg_from_string(Message, raw_msg) - wrapper = msg.get_wrapper() - - headers = lowerdict(msg.get_headers()) - moz_draft_hdr = "X-Mozilla-Draft-Info" - if moz_draft_hdr.lower() in headers: - log.msg("Setting fast notify to False, Draft detected") - notify_just_mdoc = False - - if notify_just_mdoc: - msgid = headers.get('message-id') - if msgid: - self._pending_inserts[msgid] = defer.Deferred() - - if not self.is_mailbox_collection(): - raise NotImplementedError() - - else: - mbox_id = self.mbox_uuid - wrapper.set_mbox_uuid(mbox_id) - wrapper.set_flags(flags) - wrapper.set_tags(tags) - wrapper.set_date(date) - - def insert_mdoc_id(_, wrapper): - doc_id = wrapper.mdoc.doc_id - if not doc_id: - # --- BUG ----------------------------------------- - # XXX watch out, sometimes mdoc doesn't have doc_id - # but it has future_id. Should be solved already. - logger.error("BUG: (please report) Null doc_id for " - "document %s" % - (wrapper.mdoc.serialize(),)) - return defer.succeed("mdoc_id not inserted") - # XXX BUG ----------------------------------------- - - # XXX BUG sometimes the table is not yet created, - # so workaround is to make sure we always check for it before - # inserting the doc. I should debug into the real cause. - d = self.mbox_indexer.create_table(self.mbox_uuid) - d.addBoth(lambda _: self.mbox_indexer.insert_doc( - self.mbox_uuid, doc_id)) - return d - - d = wrapper.create( - self.store, - notify_just_mdoc=notify_just_mdoc, - pending_inserts_dict=self._pending_inserts) - d.addCallback(insert_mdoc_id, wrapper) - d.addCallback(self.cb_signal_unread_to_ui) - d.addCallback(self.notify_new_to_listeners) - d.addErrback(lambda failure: log.err(failure)) - - return d - - # Listeners - - def addListener(self, listener): - self._listeners.add(listener) - - def removeListener(self, listener): - self._listeners.remove(listener) - - def notify_new_to_listeners(self, result): - for listener in self._listeners: - listener.notify_new() - return result - - def cb_signal_unread_to_ui(self, result): - """ - Sends an unread event to ui, passing *only* the number of unread - messages if *this* is the inbox. This event is catched, for instance, - in the Bitmask client that displays a message with the number of unread - mails in the INBOX. - - Used as a callback in several commands. - - :param result: ignored - """ - # TODO it might make sense to modify the event so that - # it receives both the mailbox name AND the number of unread messages. - if self.mbox_name.lower() == "inbox": - d = defer.maybeDeferred(self.count_unseen) - d.addCallback(self.__cb_signal_unread_to_ui) - return result - - def __cb_signal_unread_to_ui(self, unseen): - """ - Send the unread signal to UI. - :param unseen: number of unseen messages. - :type unseen: int - """ - emit_async(catalog.MAIL_UNREAD_MESSAGES, self.store.uuid, str(unseen)) - - def copy_msg(self, msg, new_mbox_uuid): - """ - Copy the message to another collection. (it only makes sense for - mailbox collections) - """ - # TODO should CHECK first if the mdoc is present in the mailbox - # WITH a Deleted flag... and just simply remove the flag... - # Another option is to delete the previous mdoc if it already exists - # (so we get a new UID) - - if not self.is_mailbox_collection(): - raise NotImplementedError() - - def delete_mdoc_entry_and_insert(failure, mbox_uuid, doc_id): - d = self.mbox_indexer.delete_doc_by_hash(mbox_uuid, doc_id) - d.addCallback(lambda _: self.mbox_indexer.insert_doc( - new_mbox_uuid, doc_id)) - return d - - def insert_copied_mdoc_id(wrapper_new_msg): - # XXX FIXME -- since this is already saved, the future_doc_id - # should be already copied into the doc_id! - # Investigate why we are not receiving the already saved doc_id - doc_id = wrapper_new_msg.mdoc.doc_id - if not doc_id: - doc_id = wrapper_new_msg.mdoc._future_doc_id - - def insert_conditionally(uid, mbox_uuid, doc_id): - indexer = self.mbox_indexer - if uid: - d = indexer.delete_doc_by_hash(mbox_uuid, doc_id) - d.addCallback(lambda _: indexer.insert_doc( - new_mbox_uuid, doc_id)) - return d - else: - d = indexer.insert_doc(mbox_uuid, doc_id) - return d - - def log_result(result): - return result - - def insert_doc(_, mbox_uuid, doc_id): - d = self.mbox_indexer.get_uid_from_doc_id(mbox_uuid, doc_id) - d.addCallback(insert_conditionally, mbox_uuid, doc_id) - d.addErrback(lambda err: log.failure(err)) - d.addCallback(log_result) - return d - - d = self.mbox_indexer.create_table(new_mbox_uuid) - d.addBoth(insert_doc, new_mbox_uuid, doc_id) - return d - - wrapper = msg.get_wrapper() - - d = wrapper.copy(self.store, new_mbox_uuid) - d.addCallback(insert_copied_mdoc_id) - d.addCallback(self.notify_new_to_listeners) - return d - - def delete_msg(self, msg): - """ - Delete this message. - """ - wrapper = msg.get_wrapper() - - def delete_mdoc_id(_, wrapper): - doc_id = wrapper.mdoc.doc_id - return self.mbox_indexer.delete_doc_by_hash( - self.mbox_uuid, doc_id) - d = wrapper.delete(self.store) - d.addCallback(delete_mdoc_id, wrapper) - return d - - def delete_all_flagged(self): - """ - Delete all messages flagged as \\Deleted. - Used from IMAPMailbox.expunge() - """ - def get_uid_list(hashes): - d = [] - for h in hashes: - d.append(self.mbox_indexer.get_uid_from_doc_id( - self.mbox_uuid, h)) - return defer.gatherResults(d), hashes - - def delete_uid_entries((uids, hashes)): - d = [] - for h in hashes: - d.append(self.mbox_indexer.delete_doc_by_hash( - self.mbox_uuid, h)) - - def return_uids_when_deleted(ignored): - return uids - - all_deleted = defer.gatherResults(d).addCallback( - return_uids_when_deleted) - return all_deleted - - mdocs_deleted = self.adaptor.del_all_flagged_messages( - self.store, self.mbox_uuid) - mdocs_deleted.addCallback(get_uid_list) - mdocs_deleted.addCallback(delete_uid_entries) - mdocs_deleted.addErrback(lambda f: log.err(f)) - return mdocs_deleted - - # TODO should add a delete-by-uid to collection? - - def delete_all_docs(self): - def del_all_uid(uid_list): - deferreds = [] - for uid in uid_list: - d = self.get_message_by_uid(uid) - d.addCallback(lambda msg: msg.delete()) - deferreds.append(d) - return defer.gatherResults(deferreds) - - d = self.all_uid_iter() - d.addCallback(del_all_uid) - return d - - def update_flags(self, msg, flags, mode): - """ - Update flags for a given message. - """ - wrapper = msg.get_wrapper() - current = wrapper.fdoc.flags - newflags = map(str, self._update_flags_or_tags(current, flags, mode)) - wrapper.fdoc.flags = newflags - - wrapper.fdoc.seen = MessageFlags.SEEN_FLAG in newflags - wrapper.fdoc.deleted = MessageFlags.DELETED_FLAG in newflags - - d = self.adaptor.update_msg(self.store, msg) - d.addCallback(lambda _: newflags) - return d - - def update_tags(self, msg, tags, mode): - """ - Update tags for a given message. - """ - wrapper = msg.get_wrapper() - current = wrapper.fdoc.tags - newtags = self._update_flags_or_tags(current, tags, mode) - - wrapper.fdoc.tags = newtags - d = self.adaptor.update_msg(self.store, msg) - d.addCallback(newtags) - return d - - def _update_flags_or_tags(self, old, new, mode): - if mode == Flagsmode.APPEND: - final = list((set(tuple(old) + new))) - elif mode == Flagsmode.REMOVE: - final = list(set(old).difference(set(new))) - elif mode == Flagsmode.SET: - final = new - return final - - -class Account(object): - """ - Account is the top level abstraction to access collections of messages - associated with a LEAP Mail Account. - - It primarily handles creation and access of Mailboxes, which will be the - basic collection handled by traditional MUAs, but it can also handle other - types of Collections (tag based, for instance). - - leap.mail.imap.IMAPAccount partially proxies methods in this - class. - """ - - # Adaptor is passed to the returned MessageCollections, so if you want to - # use a different adaptor this is the place to change it, by subclassing - # the Account class. - - adaptor_class = SoledadMailAdaptor - - # this is a defaultdict, indexed by userid, that returns a - # WeakValueDictionary mapping to collection instances so that we always - # return a reference to them instead of creating new ones. however, - # being a dictionary of weakrefs values, they automagically vanish - # from the dict when no hard refs is left to them (so they can be - # garbage collected) this is important because the different wrappers - # rely on several kinds of deferredlocks that are kept as class or - # instance variables. - - # We need it to be a class property because we create more than one Account - # object in the current usage pattern (ie, one in the mail service, and - # another one in the IncomingMailService). When we move to a proper service - # tree we can let it be an instance attribute. - _collection_mapping = defaultdict(weakref.WeakValueDictionary) - - def __init__(self, store, user_id, ready_cb=None): - self.store = store - self.user_id = user_id - self.adaptor = self.adaptor_class() - - self.mbox_indexer = MailboxIndexer(self.store) - - # This flag is only used from the imap service for the moment. - # In the future, we should prevent any public method to continue if - # this is set to True. Also, it would be good to plug to the - # authentication layer. - self.session_ended = False - - self.deferred_initialization = defer.Deferred() - self._ready_cb = ready_cb - - self._init_d = self._initialize_storage() - self._initialize_sync_hooks() - - def _initialize_storage(self): - - def add_mailbox_if_none(mboxes): - if not mboxes: - return self.add_mailbox(INBOX_NAME) - - def finish_initialization(result): - self.deferred_initialization.callback(None) - if self._ready_cb is not None: - self._ready_cb() - - d = self.adaptor.initialize_store(self.store) - d.addCallback(lambda _: self.list_all_mailbox_names()) - d.addCallback(add_mailbox_if_none) - d.addCallback(finish_initialization) - return d - - def callWhenReady(self, cb, *args, **kw): - """ - Execute the callback when the initialization of the Account is ready. - Note that the callback will receive a first meaningless parameter. - """ - # TODO this should ignore the first parameter explicitely - # lambda _: cb(*args, **kw) - self.deferred_initialization.addCallback(cb, *args, **kw) - return self.deferred_initialization - - # Sync hooks - - def _initialize_sync_hooks(self): - soledad_sync_hooks.post_sync_uid_reindexer.set_account(self) - - def _teardown_sync_hooks(self): - soledad_sync_hooks.post_sync_uid_reindexer.set_account(None) - - # - # Public API Starts - # - - def list_all_mailbox_names(self): - - def filter_names(mboxes): - return [m.mbox for m in mboxes] - - d = self.get_all_mailboxes() - d.addCallback(filter_names) - return d - - def get_all_mailboxes(self): - d = self.adaptor.get_all_mboxes(self.store) - return d - - def add_mailbox(self, name, creation_ts=None): - - if creation_ts is None: - # 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) - - def set_creation_ts(wrapper): - wrapper.created = creation_ts - d = wrapper.update(self.store) - d.addCallback(lambda _: wrapper) - return d - - def create_uuid(wrapper): - if not wrapper.uuid: - wrapper.uuid = str(uuid.uuid4()) - d = wrapper.update(self.store) - d.addCallback(lambda _: wrapper) - return d - return wrapper - - def create_uid_table_cb(wrapper): - d = self.mbox_indexer.create_table(wrapper.uuid) - d.addCallback(lambda _: wrapper) - return d - - d = self.adaptor.get_or_create_mbox(self.store, name) - d.addCallback(set_creation_ts) - d.addCallback(create_uuid) - d.addCallback(create_uid_table_cb) - return d - - def delete_mailbox(self, name): - - def delete_uid_table_cb(wrapper): - d = self.mbox_indexer.delete_table(wrapper.uuid) - d.addCallback(lambda _: wrapper) - return d - - d = self.adaptor.get_or_create_mbox(self.store, name) - d.addCallback(delete_uid_table_cb) - d.addCallback( - lambda wrapper: self.adaptor.delete_mbox(self.store, wrapper)) - return d - - def rename_mailbox(self, oldname, newname): - - def _rename_mbox(wrapper): - wrapper.mbox = newname - d = wrapper.update(self.store) - d.addCallback(lambda result: wrapper) - return d - - d = self.adaptor.get_or_create_mbox(self.store, oldname) - d.addCallback(_rename_mbox) - return d - - # Get Collections - - def get_collection_by_mailbox(self, name): - """ - :rtype: deferred - :return: a deferred that will fire with a MessageCollection - """ - collection = self._collection_mapping[self.user_id].get( - name, None) - if collection: - return defer.succeed(collection) - - # imap select will use this, passing the collection to SoledadMailbox - def get_collection_for_mailbox(mbox_wrapper): - collection = MessageCollection( - self.adaptor, self.store, self.mbox_indexer, mbox_wrapper) - self._collection_mapping[self.user_id][name] = collection - return collection - - d = self.adaptor.get_or_create_mbox(self.store, name) - d.addCallback(get_collection_for_mailbox) - return d - - def get_collection_by_docs(self, docs): - """ - :rtype: MessageCollection - """ - # get a collection of docs by a list of doc_id - # get.docs(...) --> it should be a generator. does it behave in the - # threadpool? - raise NotImplementedError() - - def get_collection_by_tag(self, tag): - """ - :rtype: MessageCollection - """ - raise NotImplementedError() - - # Session handling - - def end_session(self): - self._teardown_sync_hooks() - self.session_ended = True diff --git a/mail/src/leap/mail/mailbox_indexer.py b/mail/src/leap/mail/mailbox_indexer.py deleted file mode 100644 index c49f808..0000000 --- a/mail/src/leap/mail/mailbox_indexer.py +++ /dev/null @@ -1,327 +0,0 @@ -# -*- 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 <http://www.gnu.org/licenses/>. -""" -.. :py:module::mailbox_indexer - -Local tables to store the message Unique Identifiers for a given mailbox. -""" -import re -import uuid - -from leap.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-<mailbox>-<content-hash-of-the-message> - - :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-<mailbox_uuid>-<content-hash-of-the-message> - - :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 diff --git a/mail/src/leap/mail/outgoing/__init__.py b/mail/src/leap/mail/outgoing/__init__.py deleted file mode 100644 index e69de29..0000000 --- a/mail/src/leap/mail/outgoing/__init__.py +++ /dev/null diff --git a/mail/src/leap/mail/outgoing/service.py b/mail/src/leap/mail/outgoing/service.py deleted file mode 100644 index 8b02f2e..0000000 --- a/mail/src/leap/mail/outgoing/service.py +++ /dev/null @@ -1,518 +0,0 @@ -# -*- coding: utf-8 -*- -# outgoing/service.py -# Copyright (C) 2013-2015 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 <http://www.gnu.org/licenses/>. - -""" -OutgoingMail module. - -The OutgoingMail class allows to send mail, and encrypts/signs it if needed. -""" - -import os.path -import re -from StringIO import StringIO -from copy import deepcopy -from email.parser import Parser -from email.mime.application import MIMEApplication -from email.mime.multipart import MIMEMultipart -from email.mime.text import MIMEText - -from OpenSSL import SSL - -from twisted.mail import smtp -from twisted.internet import reactor -from twisted.internet import defer -from twisted.protocols.amp import ssl -from twisted.python import log - -from leap.common.check import leap_assert_type, leap_assert -from leap.common.events import emit_async, catalog -from leap.keymanager.errors import KeyNotFound, KeyAddressMismatch -from leap.mail import __version__ -from leap.mail import errors -from leap.mail.utils import validate_address -from leap.mail.rfc3156 import MultipartEncrypted -from leap.mail.rfc3156 import MultipartSigned -from leap.mail.rfc3156 import encode_base64_rec -from leap.mail.rfc3156 import RFC3156CompliantGenerator -from leap.mail.rfc3156 import PGPSignature -from leap.mail.rfc3156 import PGPEncrypted - -# TODO -# [ ] rename this module to something else, service should be the implementor -# of IService - - -class SSLContextFactory(ssl.ClientContextFactory): - def __init__(self, cert, key): - self.cert = cert - self.key = key - - def getContext(self): - # FIXME -- we should use sslv23 to allow for tlsv1.2 - # and, if possible, explicitely disable sslv3 clientside. - # Servers should avoid sslv3 - self.method = SSL.TLSv1_METHOD # SSLv23_METHOD - ctx = ssl.ClientContextFactory.getContext(self) - ctx.use_certificate_file(self.cert) - ctx.use_privatekey_file(self.key) - return ctx - - -def outgoingFactory(userid, keymanager, opts, check_cert=True, bouncer=None): - - cert = unicode(opts.cert) - key = unicode(opts.key) - hostname = str(opts.hostname) - port = opts.port - - if check_cert: - if not os.path.isfile(cert): - raise errors.ConfigurationError( - 'No valid SMTP certificate could be found for %s!' % userid) - - return OutgoingMail( - str(userid), keymanager, cert, key, hostname, port, - bouncer) - - -class OutgoingMail(object): - """ - Sends Outgoing Mail, encrypting and signing if needed. - """ - - def __init__(self, from_address, keymanager, cert, key, host, port, - bouncer=None): - """ - Initialize the outgoing mail service. - - :param from_address: The sender address. - :type from_address: str - :param keymanager: A KeyManager for retrieving recipient's keys. - :type keymanager: leap.common.keymanager.KeyManager - :param cert: The client certificate for SSL authentication. - :type cert: str - :param key: The client private key for SSL authentication. - :type key: str - :param host: The hostname of the remote SMTP server. - :type host: str - :param port: The port of the remote SMTP server. - :type port: int - """ - - # assert params - leap_assert_type(from_address, str) - leap_assert('@' in from_address) - - # XXX it can be a zope.proxy too - # leap_assert_type(keymanager, KeyManager) - - leap_assert_type(host, str) - leap_assert(host != '') - leap_assert_type(port, int) - leap_assert(port is not 0) - leap_assert_type(cert, unicode) - leap_assert(cert != '') - leap_assert_type(key, unicode) - leap_assert(key != '') - - self._port = port - self._host = host - self._key = key - self._cert = cert - self._from_address = from_address - self._keymanager = keymanager - self._bouncer = bouncer - - def send_message(self, raw, recipient): - """ - Sends a message to a recipient. Maybe encrypts and signs. - - :param raw: The raw message - :type raw: str - :param recipient: The recipient for the message - :type recipient: smtp.User - :return: a deferred which delivers the message when fired - """ - d = self._maybe_encrypt_and_sign(raw, recipient) - d.addCallback(self._route_msg, raw) - d.addErrback(self.sendError, raw) - return d - - def sendSuccess(self, smtp_sender_result): - """ - Callback for a successful send. - - :param smtp_sender_result: The result from the ESMTPSender from - _route_msg - :type smtp_sender_result: tuple(int, list(tuple)) - """ - dest_addrstr = smtp_sender_result[1][0][0] - fromaddr = self._from_address - log.msg('Message sent from %s to %s' % (fromaddr, dest_addrstr)) - emit_async(catalog.SMTP_SEND_MESSAGE_SUCCESS, - fromaddr, dest_addrstr) - - def sendError(self, failure, origmsg): - """ - Callback for an unsuccessfull send. - - :param failure: The result from the last errback. - :type failure: anything - :param origmsg: the original, unencrypted, raw message, to be passed to - the bouncer. - :type origmsg: str - """ - # XXX: need to get the address from the original message to send signal - # emit_async(catalog.SMTP_SEND_MESSAGE_ERROR, self._from_address, - # self._user.dest.addrstr) - - # TODO when we implement outgoing queues/long-term-retries, we could - # examine the error *here* and delay the notification if it's just a - # temporal error. We might want to notify the permanent errors - # differently. - - err = failure.value - log.err(err) - - if self._bouncer: - self._bouncer.bounce_message( - err.message, to=self._from_address, - orig=origmsg) - else: - raise err - - def _route_msg(self, encrypt_and_sign_result, raw): - """ - Sends the msg using the ESMTPSenderFactory. - - :param encrypt_and_sign_result: A tuple containing the 'maybe' - encrypted message and the recipient - :type encrypt_and_sign_result: tuple - """ - message, recipient = encrypt_and_sign_result - log.msg("Connecting to SMTP server %s:%s" % (self._host, self._port)) - msg = message.as_string(False) - - # we construct a defer to pass to the ESMTPSenderFactory - d = defer.Deferred() - d.addCallback(self.sendSuccess) - d.addErrback(self.sendError, raw) - # we don't pass an ssl context factory to the ESMTPSenderFactory - # because ssl will be handled by reactor.connectSSL() below. - factory = smtp.ESMTPSenderFactory( - "", # username is blank, no client auth here - "", # password is blank, no client auth here - self._from_address, - recipient.dest.addrstr, - StringIO(msg), - d, - heloFallback=True, - requireAuthentication=False, - requireTransportSecurity=True) - factory.domain = bytes('leap.mail-' + __version__) - emit_async(catalog.SMTP_SEND_MESSAGE_START, - self._from_address, recipient.dest.addrstr) - reactor.connectSSL( - self._host, self._port, factory, - contextFactory=SSLContextFactory(self._cert, self._key)) - - def _maybe_encrypt_and_sign(self, raw, recipient, fetch_remote=True): - """ - Attempt to encrypt and sign the outgoing message. - - The behaviour of this method depends on: - - 1. the original message's content-type, and - 2. the availability of the recipient's public key. - - If the original message's content-type is "multipart/encrypted", then - the original message is not altered. For any other content-type, the - method attempts to fetch the recipient's public key. If the - recipient's public key is available, the message is encrypted and - signed; otherwise it is only signed. - - Note that, if the C{encrypted_only} configuration is set to True and - the recipient's public key is not available, then the recipient - address would have been rejected in SMTPDelivery.validateTo(). - - The following table summarizes the overall behaviour of the gateway: - - +---------------------------------------------------+----------------+ - | content-type | rcpt pubkey | enforce encr. | action | - +---------------------+-------------+---------------+----------------+ - | multipart/encrypted | any | any | pass | - | other | available | any | encrypt + sign | - | other | unavailable | yes | reject | - | other | unavailable | no | sign | - +---------------------+-------------+---------------+----------------+ - - :param raw: The raw message - :type raw: str - :param recipient: The recipient for the message - :type: recipient: smtp.User - - :return: A Deferred that will be fired with a MIMEMultipart message - and the original recipient Message - :rtype: Deferred - """ - # pass if the original message's content-type is "multipart/encrypted" - origmsg = Parser().parsestr(raw) - - if origmsg.get_content_type() == 'multipart/encrypted': - return defer.succeed((origmsg, recipient)) - - from_address = validate_address(self._from_address) - username, domain = from_address.split('@') - to_address = validate_address(recipient.dest.addrstr) - - def maybe_encrypt_and_sign(message): - d = self._encrypt_and_sign( - message, to_address, from_address, - fetch_remote=fetch_remote) - d.addCallbacks(signal_encrypt_sign, - if_key_not_found_send_unencrypted, - errbackArgs=(message,)) - return d - - def signal_encrypt_sign(newmsg): - emit_async(catalog.SMTP_END_ENCRYPT_AND_SIGN, - self._from_address, - "%s,%s" % (self._from_address, to_address)) - return newmsg, recipient - - def if_key_not_found_send_unencrypted(failure, message): - failure.trap(KeyNotFound, KeyAddressMismatch) - - log.msg('Will send unencrypted message to %s.' % to_address) - emit_async(catalog.SMTP_START_SIGN, self._from_address, to_address) - d = self._sign(message, from_address) - d.addCallback(signal_sign) - return d - - def signal_sign(newmsg): - emit_async(catalog.SMTP_END_SIGN, self._from_address) - return newmsg, recipient - - log.msg("Will encrypt the message with %s and sign with %s." - % (to_address, from_address)) - emit_async(catalog.SMTP_START_ENCRYPT_AND_SIGN, - self._from_address, - "%s,%s" % (self._from_address, to_address)) - d = self._maybe_attach_key(origmsg, from_address, to_address) - d.addCallback(maybe_encrypt_and_sign) - return d - - def _maybe_attach_key(self, origmsg, from_address, to_address): - filename = "%s-email-key.asc" % (from_address,) - - def attach_if_address_hasnt_encrypted(to_key): - # if the sign_used flag is true that means that we got an encrypted - # email from this address, because we conly check signatures on - # encrypted emails. In this case we don't attach. - # XXX: this might not be true some time in the future - if to_key.sign_used: - return origmsg - return get_key_and_attach(None) - - def get_key_and_attach(_): - d = self._keymanager.get_key(from_address, fetch_remote=False) - d.addCallback(attach_key) - return d - - def attach_key(from_key): - msg = origmsg - if not origmsg.is_multipart(): - msg = MIMEMultipart() - for h, v in origmsg.items(): - msg.add_header(h, v) - msg.attach(MIMEText(origmsg.get_payload())) - - keymsg = MIMEApplication(from_key.key_data, _subtype='pgp-keys', - _encoder=lambda x: x) - keymsg.add_header('content-disposition', 'attachment', - filename=filename) - msg.attach(keymsg) - return msg - - d = self._keymanager.get_key(to_address, fetch_remote=False) - d.addCallbacks(attach_if_address_hasnt_encrypted, get_key_and_attach) - d.addErrback(lambda _: origmsg) - return d - - def _encrypt_and_sign(self, origmsg, encrypt_address, sign_address, - fetch_remote=True): - """ - Create an RFC 3156 compliang PGP encrypted and signed message using - C{encrypt_address} to encrypt and C{sign_address} to sign. - - :param origmsg: The original message - :type origmsg: email.message.Message - :param encrypt_address: The address used to encrypt the message. - :type encrypt_address: str - :param sign_address: The address used to sign the message. - :type sign_address: str - - :return: A Deferred with the MultipartEncrypted message - :rtype: Deferred - """ - # create new multipart/encrypted message with 'pgp-encrypted' protocol - - def encrypt(res): - newmsg, origmsg = res - d = self._keymanager.encrypt( - origmsg.as_string(unixfrom=False), - encrypt_address, sign=sign_address, - fetch_remote=fetch_remote) - d.addCallback(lambda encstr: (newmsg, encstr)) - return d - - def create_encrypted_message(res): - newmsg, encstr = res - encmsg = MIMEApplication( - encstr, _subtype='octet-stream', _encoder=lambda x: x) - encmsg.add_header('content-disposition', 'attachment', - filename='msg.asc') - # create meta message - metamsg = PGPEncrypted() - metamsg.add_header('Content-Disposition', 'attachment') - # attach pgp message parts to new message - newmsg.attach(metamsg) - newmsg.attach(encmsg) - return newmsg - - d = self._fix_headers( - origmsg, - MultipartEncrypted('application/pgp-encrypted'), - sign_address) - d.addCallback(encrypt) - d.addCallback(create_encrypted_message) - return d - - def _sign(self, origmsg, sign_address): - """ - Create an RFC 3156 compliant PGP signed MIME message using - C{sign_address}. - - :param origmsg: The original message - :type origmsg: email.message.Message - :param sign_address: The address used to sign the message. - :type sign_address: str - - :return: A Deferred with the MultipartSigned message. - :rtype: Deferred - """ - # apply base64 content-transfer-encoding - encode_base64_rec(origmsg) - # get message text with headers and replace \n for \r\n - fp = StringIO() - g = RFC3156CompliantGenerator( - fp, mangle_from_=False, maxheaderlen=76) - g.flatten(origmsg) - msgtext = re.sub('\r?\n', '\r\n', fp.getvalue()) - # make sure signed message ends with \r\n as per OpenPGP stantard. - if origmsg.is_multipart(): - if not msgtext.endswith("\r\n"): - msgtext += "\r\n" - - def create_signed_message(res): - (msg, _), signature = res - sigmsg = PGPSignature(signature) - # attach original message and signature to new message - msg.attach(origmsg) - msg.attach(sigmsg) - return msg - - dh = self._fix_headers( - origmsg, - MultipartSigned('application/pgp-signature', 'pgp-sha512'), - sign_address) - ds = self._keymanager.sign( - msgtext, sign_address, digest_algo='SHA512', - clearsign=False, detach=True, binary=False) - d = defer.gatherResults([dh, ds]) - d.addCallback(create_signed_message) - return d - - def _fix_headers(self, msg, newmsg, sign_address): - """ - Move some headers from C{origmsg} to C{newmsg}, delete unwanted - headers from C{origmsg} and add new headers to C{newms}. - - Outgoing messages are either encrypted and signed or just signed - before being sent. Because of that, they are packed inside new - messages and some manipulation has to be made on their headers. - - Allowed headers for passing through: - - - From - - Date - - To - - Subject - - Reply-To - - References - - In-Reply-To - - Cc - - Headers to be added: - - - Message-ID (i.e. should not use origmsg's Message-Id) - - Received (this is added automatically by twisted smtp API) - - OpenPGP (see #4447) - - Headers to be deleted: - - - User-Agent - - :param msg: The original message. - :type msg: email.message.Message - :param newmsg: The new message being created. - :type newmsg: email.message.Message - :param sign_address: The address used to sign C{newmsg} - :type sign_address: str - - :return: A Deferred with a touple: - (new Message with the unencrypted headers, - original Message with headers removed) - :rtype: Deferred - """ - origmsg = deepcopy(msg) - # move headers from origmsg to newmsg - headers = origmsg.items() - passthrough = [ - 'from', 'date', 'to', 'subject', 'reply-to', 'references', - 'in-reply-to', 'cc' - ] - headers = filter(lambda x: x[0].lower() in passthrough, headers) - for hkey, hval in headers: - newmsg.add_header(hkey, hval) - del (origmsg[hkey]) - # add a new message-id to newmsg - newmsg.add_header('Message-Id', smtp.messageid()) - # delete user-agent from origmsg - del (origmsg['user-agent']) - - def add_openpgp_header(signkey): - username, domain = sign_address.split('@') - newmsg.add_header( - 'OpenPGP', 'id=%s' % signkey.fingerprint, - url='https://%s/key/%s' % (domain, username), - preference='signencrypt') - return newmsg, origmsg - - d = self._keymanager.get_key(sign_address, private=True) - d.addCallback(add_openpgp_header) - return d diff --git a/mail/src/leap/mail/outgoing/tests/test_outgoing.py b/mail/src/leap/mail/outgoing/tests/test_outgoing.py deleted file mode 100644 index dd053c1..0000000 --- a/mail/src/leap/mail/outgoing/tests/test_outgoing.py +++ /dev/null @@ -1,263 +0,0 @@ -# -*- coding: utf-8 -*- -# test_gateway.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 <http://www.gnu.org/licenses/>. - - -""" -SMTP gateway tests. -""" -import re -from copy import deepcopy -from StringIO import StringIO -from email.parser import Parser -from datetime import datetime -from twisted.internet.defer import fail -from twisted.mail.smtp import User -from twisted.python import log - -from mock import Mock - -from leap.mail.rfc3156 import RFC3156CompliantGenerator -from leap.mail.outgoing.service import OutgoingMail -from leap.mail.testing import ADDRESS, ADDRESS_2, PUBLIC_KEY_2 -from leap.mail.testing import KeyManagerWithSoledadTestCase -from leap.mail.testing.smtp import getSMTPFactory -from leap.keymanager import errors - - -BEGIN_PUBLIC_KEY = "-----BEGIN PGP PUBLIC KEY BLOCK-----" - -TEST_USER = u'anotheruser@leap.se' - - -class TestOutgoingMail(KeyManagerWithSoledadTestCase): - EMAIL_DATA = ['HELO gateway.leap.se', - 'MAIL FROM: <%s>' % ADDRESS_2, - 'RCPT TO: <%s>' % ADDRESS, - 'DATA', - 'From: User <%s>' % ADDRESS_2, - 'To: Leap <%s>' % ADDRESS, - 'Date: ' + datetime.now().strftime('%c'), - 'Subject: test message', - '', - 'This is a secret message.', - 'Yours,', - 'A.', - '', - '.', - 'QUIT'] - - def setUp(self): - self.lines = [line for line in self.EMAIL_DATA[4:12]] - self.lines.append('') # add a trailing newline - self.raw = '\r\n'.join(self.lines) - self.expected_body = '\r\n'.join(self.EMAIL_DATA[9:12]) + "\r\n" - self.fromAddr = ADDRESS_2 - - class opts: - cert = u'/tmp/cert' - key = u'/tmp/cert' - hostname = 'remote' - port = 666 - self.opts = opts - - def init_outgoing_and_proto(_): - self.outgoing_mail = OutgoingMail( - self.fromAddr, self.km, opts.cert, - opts.key, opts.hostname, opts.port) - - user = TEST_USER - - # TODO -- this shouldn't need SMTP to be tested!? or does it? - self.proto = getSMTPFactory( - {user: None}, {user: self.km}, {user: None}) - self.dest = User(ADDRESS, 'gateway.leap.se', self.proto, ADDRESS_2) - - d = KeyManagerWithSoledadTestCase.setUp(self) - d.addCallback(init_outgoing_and_proto) - return d - - def test_message_encrypt(self): - """ - Test if message gets encrypted to destination email. - """ - def check_decryption(res): - decrypted, _ = res - self.assertEqual( - '\n' + self.expected_body, - decrypted, - 'Decrypted text differs from plaintext.') - - d = self._set_sign_used(ADDRESS) - d.addCallback( - lambda _: - self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest)) - d.addCallback(self._assert_encrypted) - d.addCallback(lambda message: self.km.decrypt( - message.get_payload(1).get_payload(), ADDRESS)) - d.addCallback(check_decryption) - return d - - def test_message_encrypt_sign(self): - """ - Test if message gets encrypted to destination email and signed with - sender key. - '""" - def check_decryption_and_verify(res): - decrypted, signkey = res - self.assertEqual( - '\n' + self.expected_body, - decrypted, - 'Decrypted text differs from plaintext.') - self.assertTrue(ADDRESS_2 in signkey.address, - "Verification failed") - - d = self._set_sign_used(ADDRESS) - d.addCallback( - lambda _: - self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest)) - d.addCallback(self._assert_encrypted) - d.addCallback(lambda message: self.km.decrypt( - message.get_payload(1).get_payload(), ADDRESS, verify=ADDRESS_2)) - d.addCallback(check_decryption_and_verify) - return d - - def test_message_sign(self): - """ - Test if message is signed with sender key. - """ - # mock the key fetching - self.km._fetch_keys_from_server = Mock( - return_value=fail(errors.KeyNotFound())) - recipient = User('ihavenopubkey@nonleap.se', - 'gateway.leap.se', self.proto, ADDRESS) - self.outgoing_mail = OutgoingMail( - self.fromAddr, self.km, self.opts.cert, self.opts.key, - self.opts.hostname, self.opts.port) - - def check_signed(res): - message, _ = res - self.assertTrue('Content-Type' in message) - self.assertEqual('multipart/signed', message.get_content_type()) - self.assertEqual('application/pgp-signature', - message.get_param('protocol')) - self.assertEqual('pgp-sha512', message.get_param('micalg')) - # assert content of message - body = (message.get_payload(0) - .get_payload(0) - .get_payload(decode=True)) - self.assertEqual(self.expected_body, - body) - # assert content of signature - self.assertTrue( - message.get_payload(1).get_payload().startswith( - '-----BEGIN PGP SIGNATURE-----\n'), - 'Message does not start with signature header.') - self.assertTrue( - message.get_payload(1).get_payload().endswith( - '-----END PGP SIGNATURE-----\n'), - 'Message does not end with signature footer.') - return message - - def verify(message): - # replace EOL before verifying (according to rfc3156) - fp = StringIO() - g = RFC3156CompliantGenerator( - fp, mangle_from_=False, maxheaderlen=76) - g.flatten(message.get_payload(0)) - signed_text = re.sub('\r?\n', '\r\n', - fp.getvalue()) - - def assert_verify(key): - self.assertTrue(ADDRESS_2 in key.address, - 'Signature could not be verified.') - - d = self.km.verify( - signed_text, ADDRESS_2, - detached_sig=message.get_payload(1).get_payload()) - d.addCallback(assert_verify) - return d - - d = self.outgoing_mail._maybe_encrypt_and_sign(self.raw, recipient) - d.addCallback(check_signed) - d.addCallback(verify) - return d - - def test_attach_key(self): - d = self.outgoing_mail._maybe_encrypt_and_sign(self.raw, self.dest) - d.addCallback(self._assert_encrypted) - d.addCallback(self._check_headers, self.lines[:4]) - d.addCallback(lambda message: self.km.decrypt( - message.get_payload(1).get_payload(), ADDRESS)) - d.addCallback(lambda (decrypted, _): - self._check_key_attachment(Parser().parsestr(decrypted))) - return d - - def test_attach_key_not_known(self): - unknown_address = "someunknownaddress@somewhere.com" - lines = deepcopy(self.lines) - lines[1] = "To: <%s>" % (unknown_address,) - raw = '\r\n'.join(lines) - dest = User(unknown_address, 'gateway.leap.se', self.proto, ADDRESS_2) - - d = self.outgoing_mail._maybe_encrypt_and_sign( - raw, dest, fetch_remote=False) - d.addCallback(lambda (message, _): - self._check_headers(message, lines[:4])) - d.addCallback(self._check_key_attachment) - d.addErrback(log.err) - return d - - def _check_headers(self, message, headers): - msgstr = message.as_string(unixfrom=False) - for header in headers: - self.assertTrue(header in msgstr, - "Missing header: %s" % (header,)) - return message - - def _check_key_attachment(self, message): - for payload in message.get_payload(): - if payload.is_multipart(): - return self._check_key_attachment(payload) - if 'application/pgp-keys' == payload.get_content_type(): - keylines = PUBLIC_KEY_2.split('\n') - key = BEGIN_PUBLIC_KEY + '\n\n' + '\n'.join(keylines[4:-1]) - self.assertTrue(key in payload.get_payload(decode=True), - "Key attachment don't match") - return - self.fail("No public key attachment found") - - def _set_sign_used(self, address): - def set_sign(key): - key.sign_used = True - return self.km.put_key(key) - - d = self.km.get_key(address, fetch_remote=False) - d.addCallback(set_sign) - return d - - def _assert_encrypted(self, res): - message, _ = res - self.assertTrue('Content-Type' in message) - self.assertEqual('multipart/encrypted', message.get_content_type()) - self.assertEqual('application/pgp-encrypted', - message.get_param('protocol')) - self.assertEqual(2, len(message.get_payload())) - self.assertEqual('application/pgp-encrypted', - message.get_payload(0).get_content_type()) - self.assertEqual('application/octet-stream', - message.get_payload(1).get_content_type()) - return message diff --git a/mail/src/leap/mail/plugins/__init__.py b/mail/src/leap/mail/plugins/__init__.py deleted file mode 100644 index ddb8691..0000000 --- a/mail/src/leap/mail/plugins/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from twisted.plugin import pluginPackagePaths -__path__.extend(pluginPackagePaths(__name__)) -__all__ = [] diff --git a/mail/src/leap/mail/plugins/soledad_sync_hooks.py b/mail/src/leap/mail/plugins/soledad_sync_hooks.py deleted file mode 100644 index 9d48126..0000000 --- a/mail/src/leap/mail/plugins/soledad_sync_hooks.py +++ /dev/null @@ -1,19 +0,0 @@ -# -*- coding: utf-8 -*- -# soledad_sync_hooks.py -# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>. - -from leap.mail.sync_hooks import MailProcessingPostSyncHook -post_sync_uid_reindexer = MailProcessingPostSyncHook() diff --git a/mail/src/leap/mail/rfc3156.py b/mail/src/leap/mail/rfc3156.py deleted file mode 100644 index 7d7bc0f..0000000 --- a/mail/src/leap/mail/rfc3156.py +++ /dev/null @@ -1,390 +0,0 @@ -# -*- coding: utf-8 -*- -# rfc3156.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 <http://www.gnu.org/licenses/>. - -""" -Implements RFC 3156: MIME Security with OpenPGP. -""" - -import base64 -from StringIO import StringIO - -from twisted.python import log -from email.mime.application import MIMEApplication -from email.mime.multipart import MIMEMultipart -from email import errors -from email.generator import ( - Generator, - fcre, - NL, - _make_boundary, -) - - -# -# A generator that solves http://bugs.python.org/issue14983 -# - -class RFC3156CompliantGenerator(Generator): - """ - An email generator that addresses Python's issue #14983 for multipart - messages. - - This is just a copy of email.generator.Generator which fixes the following - bug: http://bugs.python.org/issue14983 - """ - - def _handle_multipart(self, msg): - """ - A multipart handling implementation that addresses issue #14983. - - This is just a copy of the parent's method which fixes the following - bug: http://bugs.python.org/issue14983 (see the line marked with - "(***)"). - - :param msg: The multipart message to be handled. - :type msg: email.message.Message - """ - # The trick here is to write out each part separately, merge them all - # together, and then make sure that the boundary we've chosen isn't - # present in the payload. - msgtexts = [] - subparts = msg.get_payload() - if subparts is None: - subparts = [] - elif isinstance(subparts, basestring): - # e.g. a non-strict parse of a message with no starting boundary. - self._fp.write(subparts) - return - elif not isinstance(subparts, list): - # Scalar payload - subparts = [subparts] - for part in subparts: - s = StringIO() - g = self.clone(s) - g.flatten(part, unixfrom=False) - msgtexts.append(s.getvalue()) - # BAW: What about boundaries that are wrapped in double-quotes? - boundary = msg.get_boundary() - if not boundary: - # Create a boundary that doesn't appear in any of the - # message texts. - alltext = NL.join(msgtexts) - boundary = _make_boundary(alltext) - msg.set_boundary(boundary) - # If there's a preamble, write it out, with a trailing CRLF - if msg.preamble is not None: - preamble = msg.preamble - if self._mangle_from_: - preamble = fcre.sub('>From ', msg.preamble) - self._fp.write(preamble + '\n') - # dash-boundary transport-padding CRLF - self._fp.write('--' + boundary + '\n') - # body-part - if msgtexts: - self._fp.write(msgtexts.pop(0)) - # *encapsulation - # --> delimiter transport-padding - # --> CRLF body-part - for body_part in msgtexts: - # delimiter transport-padding CRLF - self._fp.write('\n--' + boundary + '\n') - # body-part - self._fp.write(body_part) - # close-delimiter transport-padding - self._fp.write('\n--' + boundary + '--' + '\n') # (***) Solve #14983 - if msg.epilogue is not None: - self._fp.write('\n') - epilogue = msg.epilogue - if self._mangle_from_: - epilogue = fcre.sub('>From ', msg.epilogue) - self._fp.write(epilogue) - - -# -# Base64 encoding: these are almost the same as python's email.encoder -# solution, but a bit modified. -# - -def _bencode(s): - """ - Encode C{s} in base64. - - :param s: The string to be encoded. - :type s: str - """ - # We can't quite use base64.encodestring() since it tacks on a "courtesy - # newline". Blech! - if not s: - return s - value = base64.encodestring(s) - return value[:-1] - - -def encode_base64(msg): - """ - Encode a non-multipart message's payload in Base64 (in place). - - This method modifies the message contents in place and adds or replaces an - appropriate Content-Transfer-Encoding header. - - :param msg: The non-multipart message to be encoded. - :type msg: email.message.Message - """ - encoding = msg.get('Content-Transfer-Encoding', None) - if encoding is not None: - encoding = encoding.lower() - # XXX Python's email module can only decode quoted-printable, base64 and - # uuencoded data, so we might have to implement other decoding schemes in - # order to support RFC 3156 properly and correctly calculate signatures - # for multipart attachments (eg. 7bit or 8bit encoded attachments). For - # now, if content is already encoded as base64 or if it is encoded with - # some unknown encoding, we just pass. - if encoding in [None, 'quoted-printable', 'x-uuencode', 'uue', 'x-uue']: - orig = msg.get_payload(decode=True) - encdata = _bencode(orig) - msg.set_payload(encdata) - # replace or set the Content-Transfer-Encoding header. - try: - msg.replace_header('Content-Transfer-Encoding', 'base64') - except KeyError: - msg['Content-Transfer-Encoding'] = 'base64' - elif encoding is not 'base64': - log.err('Unknown content-transfer-encoding: %s' % encoding) - - -def encode_base64_rec(msg): - """ - Encode (possibly multipart) messages in base64 (in place). - - This method modifies the message contents in place. - - :param msg: The non-multipart message to be encoded. - :type msg: email.message.Message - """ - if not msg.is_multipart(): - encode_base64(msg) - else: - for sub in msg.get_payload(): - encode_base64_rec(sub) - - -# -# RFC 1847: multipart/signed and multipart/encrypted -# - -class MultipartSigned(MIMEMultipart): - """ - Multipart/Signed MIME message according to RFC 1847. - - 2.1. Definition of Multipart/Signed - - (1) MIME type name: multipart - (2) MIME subtype name: signed - (3) Required parameters: boundary, protocol, and micalg - (4) Optional parameters: none - (5) Security considerations: Must be treated as opaque while in - transit - - The multipart/signed content type contains exactly two body parts. - The first body part is the body part over which the digital signature - was created, including its MIME headers. The second body part - contains the control information necessary to verify the digital - signature. The first body part may contain any valid MIME content - type, labeled accordingly. The second body part is labeled according - to the value of the protocol parameter. - - When the OpenPGP digital signature is generated: - - (1) The data to be signed MUST first be converted to its content- - type specific canonical form. For text/plain, this means - conversion to an appropriate character set and conversion of - line endings to the canonical <CR><LF> sequence. - - (2) An appropriate Content-Transfer-Encoding is then applied; see - section 3. In particular, line endings in the encoded data - MUST use the canonical <CR><LF> sequence where appropriate - (note that the canonical line ending may or may not be present - on the last line of encoded data and MUST NOT be included in - the signature if absent). - - (3) MIME content headers are then added to the body, each ending - with the canonical <CR><LF> sequence. - - (4) As described in section 3 of this document, any trailing - whitespace MUST then be removed from the signed material. - - (5) As described in [2], the digital signature MUST be calculated - over both the data to be signed and its set of content headers. - - (6) The signature MUST be generated detached from the signed data - so that the process does not alter the signed data in any way. - """ - - def __init__(self, protocol, micalg, boundary=None, _subparts=None): - """ - Initialize the multipart/signed message. - - :param boundary: the multipart boundary string. By default it is - calculated as needed. - :type boundary: str - :param _subparts: a sequence of initial subparts for the payload. It - must be an iterable object, such as a list. You can always - attach new subparts to the message by using the attach() method. - :type _subparts: iterable - """ - MIMEMultipart.__init__( - self, _subtype='signed', boundary=boundary, - _subparts=_subparts) - self.set_param('protocol', protocol) - self.set_param('micalg', micalg) - - def attach(self, payload): - """ - Add the C{payload} to the current payload list. - - Also prevent from adding payloads with wrong Content-Type and from - exceeding a maximum of 2 payloads. - - :param payload: The payload to be attached. - :type payload: email.message.Message - """ - # second payload's content type must be equal to the protocol - # parameter given on object creation - if len(self.get_payload()) == 1: - if payload.get_content_type() != self.get_param('protocol'): - raise errors.MultipartConversionError( - 'Wrong content type %s.' % payload.get_content_type) - # prevent from adding more payloads - if len(self._payload) == 2: - raise errors.MultipartConversionError( - 'Cannot have more than two subparts.') - MIMEMultipart.attach(self, payload) - - -class MultipartEncrypted(MIMEMultipart): - """ - Multipart/encrypted MIME message according to RFC 1847. - - 2.2. Definition of Multipart/Encrypted - - (1) MIME type name: multipart - (2) MIME subtype name: encrypted - (3) Required parameters: boundary, protocol - (4) Optional parameters: none - (5) Security considerations: none - - The multipart/encrypted content type contains exactly two body parts. - The first body part contains the control information necessary to - decrypt the data in the second body part and is labeled according to - the value of the protocol parameter. The second body part contains - the data which was encrypted and is always labeled - application/octet-stream. - """ - - def __init__(self, protocol, boundary=None, _subparts=None): - """ - :param protocol: The encryption protocol to be added as a parameter to - the Content-Type header. - :type protocol: str - :param boundary: the multipart boundary string. By default it is - calculated as needed. - :type boundary: str - :param _subparts: a sequence of initial subparts for the payload. It - must be an iterable object, such as a list. You can always - attach new subparts to the message by using the attach() method. - :type _subparts: iterable - """ - MIMEMultipart.__init__( - self, _subtype='encrypted', boundary=boundary, - _subparts=_subparts) - self.set_param('protocol', protocol) - - def attach(self, payload): - """ - Add the C{payload} to the current payload list. - - Also prevent from adding payloads with wrong Content-Type and from - exceeding a maximum of 2 payloads. - - :param payload: The payload to be attached. - :type payload: email.message.Message - """ - # first payload's content type must be equal to the protocol parameter - # given on object creation - if len(self._payload) == 0: - if payload.get_content_type() != self.get_param('protocol'): - raise errors.MultipartConversionError( - 'Wrong content type.') - # second payload is always application/octet-stream - if len(self._payload) == 1: - if payload.get_content_type() != 'application/octet-stream': - raise errors.MultipartConversionError( - 'Wrong content type %s.' % payload.get_content_type) - # prevent from adding more payloads - if len(self._payload) == 2: - raise errors.MultipartConversionError( - 'Cannot have more than two subparts.') - MIMEMultipart.attach(self, payload) - - -# -# RFC 3156: application/pgp-encrypted, application/pgp-signed and -# application-pgp-signature. -# - -class PGPEncrypted(MIMEApplication): - """ - Application/pgp-encrypted MIME media type according to RFC 3156. - - * MIME media type name: application - * MIME subtype name: pgp-encrypted - * Required parameters: none - * Optional parameters: none - """ - - def __init__(self, version=1): - data = "Version: %d" % version - MIMEApplication.__init__(self, data, 'pgp-encrypted') - - -class PGPSignature(MIMEApplication): - """ - Application/pgp-signature MIME media type according to RFC 3156. - - * MIME media type name: application - * MIME subtype name: pgp-signature - * Required parameters: none - * Optional parameters: none - """ - def __init__(self, _data, name='signature.asc'): - MIMEApplication.__init__(self, _data, 'pgp-signature', - _encoder=lambda x: x, name=name) - self.add_header('Content-Description', 'OpenPGP Digital Signature') - - -class PGPKeys(MIMEApplication): - """ - Application/pgp-keys MIME media type according to RFC 3156. - - * MIME media type name: application - * MIME subtype name: pgp-keys - * Required parameters: none - * Optional parameters: none - """ - - def __init__(self, _data): - MIMEApplication.__init__(self, _data, 'pgp-keys') diff --git a/mail/src/leap/mail/size.py b/mail/src/leap/mail/size.py deleted file mode 100644 index c9eaabd..0000000 --- a/mail/src/leap/mail/size.py +++ /dev/null @@ -1,57 +0,0 @@ -# -*- coding: utf-8 -*- -# size.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 <http://www.gnu.org/licenses/>. -""" -Recursively get size of objects. -""" -from gc import collect -from itertools import chain -from sys import getsizeof - - -def _get_size(item, seen): - known_types = {dict: lambda d: chain.from_iterable(d.items())} - default_size = getsizeof(0) - - def size_walk(item): - if id(item) in seen: - return 0 - seen.add(id(item)) - s = getsizeof(item, default_size) - for _type, fun in known_types.iteritems(): - if isinstance(item, _type): - s += sum(map(size_walk, fun(item))) - break - return s - - return size_walk(item) - - -def get_size(item): - """ - Return the cumulative size of a given object. - - Currently it supports only dictionaries, and seemingly leaks - some memory, so use with care. - - :param item: the item which size wants to be computed - :rtype: int - """ - seen = set() - size = _get_size(item, seen) - del seen - collect() - return size diff --git a/mail/src/leap/mail/smtp/README.rst b/mail/src/leap/mail/smtp/README.rst deleted file mode 100644 index 1d3a903..0000000 --- a/mail/src/leap/mail/smtp/README.rst +++ /dev/null @@ -1,44 +0,0 @@ -Leap SMTP Gateway -================= - -The Bitmask Client runs a thin SMTP gateway on the user's device, which -intends to encrypt and sign outgoing messages to achieve point to point -encryption. - -The gateway is bound to localhost and the user's MUA should be configured to -send messages to it. After doing its thing, the gateway will relay the -messages to the remote SMTP server. - -Outgoing mail workflow: - - * SMTP gateway receives a message from the MUA. - - * SMTP gateway queries Key Manager for the user's private key. - - * For each recipient (including addresses in "To", "Cc" anc "Bcc" fields), - the following happens: - - - The recipient's address is validated against RFC2822. - - - An attempt is made to fetch the recipient's public PGP key. - - - If key is not found: - - - If the gateway is configured to only send encrypted messages the - recipient is rejected. - - - Otherwise, the message is signed and sent as plain text. - - - If the key is found, the message is encrypted to the recipient and - signed with the sender's private PGP key. - - * Finally, one message for each recipient is gatewayed to provider's SMTP - server. - - -Running tests -------------- - -Tests are run using Twisted's Trial API, like this:: - - python setup.py test -s leap.mail.gateway.tests diff --git a/mail/src/leap/mail/smtp/__init__.py b/mail/src/leap/mail/smtp/__init__.py deleted file mode 100644 index 9fab70a..0000000 --- a/mail/src/leap/mail/smtp/__init__.py +++ /dev/null @@ -1,73 +0,0 @@ -# -*- coding: utf-8 -*- -# __init__.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 <http://www.gnu.org/licenses/>. - -""" -SMTP gateway helper function. -""" -import logging -import os - -from twisted.internet import reactor -from twisted.internet.error import CannotListenError - -from leap.common.events import emit_async, catalog - -from leap.mail.smtp.gateway import SMTPFactory - -logger = logging.getLogger(__name__) - - -SMTP_PORT = 2013 - - -def run_service(soledad_sessions, keymanager_sessions, sendmail_opts, - port=SMTP_PORT): - """ - Main entry point to run the service from the client. - - :param soledad_sessions: a dict-like object, containing instances - of a Store (soledad instances), indexed by userid. - :param keymanager_sessions: a dict-like object, containing instances - of Keymanager, indexed by userid. - :param sendmail_opts: a dict-like object of sendmailOptions. - - :returns: the port as returned by the reactor when starts listening, and - the factory for the protocol. - :rtype: tuple - """ - factory = SMTPFactory(soledad_sessions, keymanager_sessions, - sendmail_opts) - - try: - interface = "localhost" - # don't bind just to localhost if we are running on docker since we - # won't be able to access smtp from the host - if os.environ.get("LEAP_DOCKERIZED"): - interface = '' - - # TODO Use Endpoints instead -------------------------------- - tport = reactor.listenTCP(port, factory, interface=interface) - emit_async(catalog.SMTP_SERVICE_STARTED, str(port)) - - return factory, tport - except CannotListenError: - logger.error("STMP Service failed to start: " - "cannot listen in port %s" % port) - emit_async(catalog.SMTP_SERVICE_FAILED_TO_START, str(port)) - except Exception as exc: - logger.error("Unhandled error while launching smtp gateway service") - logger.exception(exc) diff --git a/mail/src/leap/mail/smtp/bounces.py b/mail/src/leap/mail/smtp/bounces.py deleted file mode 100644 index 7a4674b..0000000 --- a/mail/src/leap/mail/smtp/bounces.py +++ /dev/null @@ -1,90 +0,0 @@ -# -*- coding: utf-8 -*- -# bounces.py -# Copyright (C) 2016 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 <http://www.gnu.org/licenses/>. -""" -Deliver bounces to the user Inbox. -""" -import time -from email.message import Message -from email.utils import formatdate - -from leap.mail.constants import INBOX_NAME -from leap.mail.mail import Account - - -# TODO implement localization for this template. - -BOUNCE_TEMPLATE = """This is your local Bitmask Mail Agent running at localhost. - -I'm sorry to have to inform you that your message could not be delivered to one -or more recipients. - -The reasons I got for the error are: - -{raw_error} - -If the problem persists and it's not a network connectivity issue, you might -want to contact your provider ({provider}) with this information (remove any -sensitive data before). - ---- Original message (*before* it was encrypted by bitmask) below ----: - -{orig}""" - - -class Bouncer(object): - """ - Implements a mechanism to deliver bounces to user inbox. - """ - # TODO this should follow RFC 6522, and compose a correct multipart - # attaching the report and the original message. Leaving this for a future - # iteration. - - def __init__(self, inbox_collection): - self._inbox_collection = inbox_collection - - def bounce_message(self, error_data, to, date=None, orig=''): - if not date: - date = formatdate(time.time()) - - raw_data = self._format_msg(error_data, to, date, orig) - d = self._inbox_collection.add_msg( - raw_data, ('\\Recent',), date=date) - return d - - def _format_msg(self, error_data, to, date, orig): - provider = to.split('@')[1] - - msg = Message() - msg.add_header( - 'From', 'bitmask-bouncer@localhost (Bitmask Local Agent)') - msg.add_header('To', to) - msg.add_header('Subject', 'Undelivered Message') - msg.add_header('Date', date) - msg.set_payload(BOUNCE_TEMPLATE.format( - raw_error=error_data, - provider=provider, - orig=orig)) - - return msg.as_string() - - -def bouncerFactory(soledad): - user_id = soledad.uuid - acc = Account(soledad, user_id) - d = acc.callWhenReady(lambda _: acc.get_collection_by_mailbox(INBOX_NAME)) - d.addCallback(lambda inbox: Bouncer(inbox)) - return d diff --git a/mail/src/leap/mail/smtp/gateway.py b/mail/src/leap/mail/smtp/gateway.py deleted file mode 100644 index e49bbe8..0000000 --- a/mail/src/leap/mail/smtp/gateway.py +++ /dev/null @@ -1,413 +0,0 @@ -# -*- coding: utf-8 -*- -# gateway.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 <http://www.gnu.org/licenses/>. -""" -LEAP SMTP encrypted gateway. - -The following classes comprise the SMTP gateway service: - - * SMTPFactory - A twisted.internet.protocol.ServerFactory that provides - the SMTPDelivery protocol. - - * SMTPDelivery - A twisted.mail.smtp.IMessageDelivery implementation. It - knows how to validate sender and receiver of messages and it generates - an EncryptedMessage for each recipient. - - * EncryptedMessage - An implementation of twisted.mail.smtp.IMessage that - knows how to encrypt/sign itself before sending. -""" -from email.Header import Header - -from zope.interface import implements -from zope.interface import implementer - -from twisted.cred.portal import Portal, IRealm -from twisted.mail import smtp -from twisted.mail.imap4 import LOGINCredentials, PLAINCredentials -from twisted.internet import defer, protocol -from twisted.python import log - -from leap.common.check import leap_assert_type -from leap.common.events import emit_async, catalog -from leap.mail import errors -from leap.mail.cred import LocalSoledadTokenChecker -from leap.mail.utils import validate_address -from leap.mail.rfc3156 import RFC3156CompliantGenerator -from leap.mail.outgoing.service import outgoingFactory -from leap.mail.smtp.bounces import bouncerFactory -from leap.keymanager.errors import KeyNotFound - -# replace email generator with a RFC 3156 compliant one. -from email import generator - -generator.Generator = RFC3156CompliantGenerator - - -LOCAL_FQDN = "bitmask.local" - - -@implementer(IRealm) -class LocalSMTPRealm(object): - - _encoding = 'utf-8' - - def __init__(self, keymanager_sessions, soledad_sessions, sendmail_opts, - encrypted_only=False): - """ - :param keymanager_sessions: a dict-like object, containing instances - of a Keymanager objects, indexed by - userid. - """ - self._keymanager_sessions = keymanager_sessions - self._soledad_sessions = soledad_sessions - self._sendmail_opts = sendmail_opts - self.encrypted_only = encrypted_only - - def requestAvatar(self, avatarId, mind, *interfaces): - - if isinstance(avatarId, str): - avatarId = avatarId.decode(self._encoding) - - def gotKeymanagerAndSoledad(result): - keymanager, soledad = result - d = bouncerFactory(soledad) - d.addCallback(lambda bouncer: (keymanager, soledad, bouncer)) - return d - - def getMessageDelivery(result): - keymanager, soledad, bouncer = result - # TODO use IMessageDeliveryFactory instead ? - # it could reuse the connections. - if smtp.IMessageDelivery in interfaces: - userid = avatarId - opts = self.getSendingOpts(userid) - - outgoing = outgoingFactory( - userid, keymanager, opts, bouncer=bouncer) - avatar = SMTPDelivery(userid, keymanager, self.encrypted_only, - outgoing) - - return (smtp.IMessageDelivery, avatar, - getattr(avatar, 'logout', lambda: None)) - - raise NotImplementedError(self, interfaces) - - d1 = self.lookupKeymanagerInstance(avatarId) - d2 = self.lookupSoledadInstance(avatarId) - d = defer.gatherResults([d1, d2]) - d.addCallback(gotKeymanagerAndSoledad) - d.addCallback(getMessageDelivery) - return d - - def lookupKeymanagerInstance(self, userid): - try: - keymanager = self._keymanager_sessions[userid] - except: - raise errors.AuthenticationError( - 'No keymanager session found for user %s. Is it authenticated?' - % userid) - # XXX this should return the instance after whenReady callback - return defer.succeed(keymanager) - - def lookupSoledadInstance(self, userid): - try: - soledad = self._soledad_sessions[userid] - except: - raise errors.AuthenticationError( - 'No soledad session found for user %s. Is it authenticated?' - % userid) - # XXX this should return the instance after whenReady callback - return defer.succeed(soledad) - - def getSendingOpts(self, userid): - try: - opts = self._sendmail_opts[userid] - except KeyError: - raise errors.ConfigurationError( - 'No sendingMail options found for user %s' % userid) - return opts - - -class SMTPTokenChecker(LocalSoledadTokenChecker): - """A credentials checker that will lookup a token for the SMTP service. - For now it will be using the same identifier than IMAPTokenChecker""" - - service = 'mail_auth' - - # TODO besides checking for token credential, - # we could also verify the certificate here. - - -class LEAPInitMixin(object): - - """ - A Mixin that takes care of initialization of all the data needed to access - LEAP sessions. - """ - def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts, - encrypted_only=False): - realm = LocalSMTPRealm( - keymanager_sessions, soledad_sessions, sendmail_opts, - encrypted_only) - portal = Portal(realm) - - checker = SMTPTokenChecker(soledad_sessions) - self.checker = checker - self.portal = portal - portal.registerChecker(checker) - - -class LocalSMTPServer(smtp.ESMTP, LEAPInitMixin): - """ - The Production ESMTP Server: Authentication Needed. - Authenticates against SMTP Token stored in Local Soledad instance. - The Realm will produce a Delivery Object that handles encryption/signing. - """ - - # TODO: implement Queue using twisted.mail.mail.MailService - - def __init__(self, soledads, keyms, sendmailopts, *args, **kw): - encrypted_only = kw.pop('encrypted_only', False) - - LEAPInitMixin.__init__(self, soledads, keyms, sendmailopts, - encrypted_only) - smtp.ESMTP.__init__(self, *args, **kw) - - -# TODO implement retries -- see smtp.SenderMixin -class SMTPFactory(protocol.ServerFactory): - """ - Factory for an SMTP server with encrypted gatewaying capabilities. - """ - - protocol = LocalSMTPServer - domain = LOCAL_FQDN - timeout = 600 - encrypted_only = False - - def __init__(self, soledad_sessions, keymanager_sessions, sendmail_opts, - deferred=None, retries=3): - - self._soledad_sessions = soledad_sessions - self._keymanager_sessions = keymanager_sessions - self._sendmail_opts = sendmail_opts - - def buildProtocol(self, addr): - p = self.protocol( - self._soledad_sessions, self._keymanager_sessions, - self._sendmail_opts, encrypted_only=self.encrypted_only) - p.factory = self - p.host = LOCAL_FQDN - p.challengers = {"LOGIN": LOGINCredentials, "PLAIN": PLAINCredentials} - return p - - -# -# SMTPDelivery -# - -@implementer(smtp.IMessageDelivery) -class SMTPDelivery(object): - """ - Validate email addresses and handle message delivery. - """ - - def __init__(self, userid, keymanager, encrypted_only, outgoing_mail): - """ - Initialize the SMTP delivery object. - - :param userid: The user currently logged in - :type userid: unicode - :param keymanager: A Key Manager from where to get recipients' public - keys. - :param encrypted_only: Whether the SMTP gateway should send unencrypted - mail or not. - :type encrypted_only: bool - :param outgoing_mail: The outgoing mail to send the message - :type outgoing_mail: leap.mail.outgoing.service.OutgoingMail - """ - self._userid = userid - self._outgoing_mail = outgoing_mail - self._km = keymanager - self._encrypted_only = encrypted_only - self._origin = None - - def receivedHeader(self, helo, origin, recipients): - """ - Generate the 'Received:' header for a message. - - :param helo: The argument to the HELO command and the client's IP - address. - :type helo: (str, str) - :param origin: The address the message is from. - :type origin: twisted.mail.smtp.Address - :param recipients: A list of the addresses for which this message is - bound. - :type: list of twisted.mail.smtp.User - - @return: The full "Received" header string. - :type: str - """ - myHostname, clientIP = helo - headerValue = "by bitmask.local from %s with ESMTP ; %s" % ( - clientIP, smtp.rfc822date()) - # email.Header.Header used for automatic wrapping of long lines - return "Received: %s" % Header(s=headerValue, header_name='Received') - - def validateTo(self, user): - """ - Validate the address of a recipient of the message, possibly - rejecting it if the recipient key is not available. - - This method is called once for each recipient, i.e. for each SMTP - protocol line beginning with "RCPT TO:", which includes all addresses - in "To", "Cc" and "Bcc" MUA fields. - - The recipient's address is validated against the RFC 2822 definition. - If self._encrypted_only is True and no key is found for a recipient, - then that recipient is rejected. - - The method returns an encrypted message object that is able to send - itself to the user's address. - - :param user: The user whose address we wish to validate. - :type: twisted.mail.smtp.User - - @return: A callable which takes no arguments and returns an - encryptedMessage. - @rtype: no-argument callable - - @raise SMTPBadRcpt: Raised if messages to the address are not to be - accepted. - """ - # try to find recipient's public key - address = validate_address(user.dest.addrstr) - - # verify if recipient key is available in keyring - def found(_): - log.msg("Accepting mail for %s..." % user.dest.addrstr) - emit_async(catalog.SMTP_RECIPIENT_ACCEPTED_ENCRYPTED, - self._userid, user.dest.addrstr) - - def not_found(failure): - failure.trap(KeyNotFound) - - # if key was not found, check config to see if will send anyway - if self._encrypted_only: - emit_async(catalog.SMTP_RECIPIENT_REJECTED, self._userid, - user.dest.addrstr) - raise smtp.SMTPBadRcpt(user.dest.addrstr) - log.msg("Warning: will send an unencrypted message (because " - "encrypted_only' is set to False).") - emit_async( - catalog.SMTP_RECIPIENT_ACCEPTED_UNENCRYPTED, - self._userid, user.dest.addrstr) - - def encrypt_func(_): - return lambda: EncryptedMessage(user, self._outgoing_mail) - - d = self._km.get_key(address) - d.addCallbacks(found, not_found) - d.addCallback(encrypt_func) - return d - - def validateFrom(self, helo, origin): - """ - Validate the address from which the message originates. - - :param helo: The argument to the HELO command and the client's IP - address. - :type: (str, str) - :param origin: The address the message is from. - :type origin: twisted.mail.smtp.Address - - @return: origin or a Deferred whose callback will be passed origin. - @rtype: Deferred or Address - - @raise twisted.mail.smtp.SMTPBadSender: Raised if messages from this - address are not to be accepted. - """ - # accept mail from anywhere. To reject an address, raise - # smtp.SMTPBadSender here. - if str(origin) != str(self._userid): - log.msg("Rejecting sender {0}, expected {1}".format(origin, - self._userid)) - raise smtp.SMTPBadSender(origin) - self._origin = origin - return origin - - -# -# EncryptedMessage -# - -class EncryptedMessage(object): - """ - Receive plaintext from client, encrypt it and send message to a - recipient. - """ - implements(smtp.IMessage) - - def __init__(self, user, outgoing_mail): - """ - Initialize the encrypted message. - - :param user: The recipient of this message. - :type user: twisted.mail.smtp.User - :param outgoing_mail: The outgoing mail to send the message - :type outgoing_mail: leap.mail.outgoing.service.OutgoingMail - """ - # assert params - leap_assert_type(user, smtp.User) - - self._user = user - self._lines = [] - self._outgoing_mail = outgoing_mail - - def lineReceived(self, line): - """ - Handle another line. - - :param line: The received line. - :type line: str - """ - self._lines.append(line) - - def eomReceived(self): - """ - Handle end of message. - - This method will encrypt and send the message. - - :returns: a deferred - """ - log.msg("Message data complete.") - self._lines.append('') # add a trailing newline - raw_mail = '\r\n'.join(self._lines) - - return self._outgoing_mail.send_message(raw_mail, self._user) - - def connectionLost(self): - """ - Log an error when the connection is lost. - """ - log.msg("Connection lost unexpectedly!") - log.err() - emit_async(catalog.SMTP_CONNECTION_LOST, self._userid, - self._user.dest.addrstr) - # unexpected loss of connection; don't save - - self._lines = [] diff --git a/mail/src/leap/mail/smtp/tests/185CA770.key b/mail/src/leap/mail/smtp/tests/185CA770.key deleted file mode 100644 index 587b416..0000000 --- a/mail/src/leap/mail/smtp/tests/185CA770.key +++ /dev/null @@ -1,79 +0,0 @@ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -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 -=6HcJ ------END PGP PRIVATE KEY BLOCK----- diff --git a/mail/src/leap/mail/smtp/tests/185CA770.pub b/mail/src/leap/mail/smtp/tests/185CA770.pub deleted file mode 100644 index 38af19f..0000000 --- a/mail/src/leap/mail/smtp/tests/185CA770.pub +++ /dev/null @@ -1,52 +0,0 @@ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -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 -q3iu -=RChS ------END PGP PUBLIC KEY BLOCK----- diff --git a/mail/src/leap/mail/smtp/tests/cert/server.crt b/mail/src/leap/mail/smtp/tests/cert/server.crt deleted file mode 100644 index a27391c..0000000 --- a/mail/src/leap/mail/smtp/tests/cert/server.crt +++ /dev/null @@ -1,29 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFBjCCAu4CCQCWn3oMoQrDJTANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJV -UzETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0 -cyBQdHkgTHRkMB4XDTEzMTAyMzE0NDUwNFoXDTE2MDcxOTE0NDUwNFowRTELMAkG -A1UEBhMCVVMxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0 -IFdpZGdpdHMgUHR5IEx0ZDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB -APexTvEvG7cSmZdAERHt9TB11cSor54Y/F7NmYMdSOJNi4Y0kwkSslpdfipi+mt/ -NFg/uGKi1mcgvuXdVbVPZ9rCgVpIzMncO8RAP7a5+I2zKUzqMCCbLH16sYpo/rDk -VQ5V15TwLsTzOFGG8Cgp68TR8zHuZ4Edf2zMGC1IaiJ6W38LTnJgsowYOCFDAF3z -L36kxMO5gNGEUYV6tjltx+rAcXka3po+xiAgvW6q65UUgDHcIdEGG2dc9bkxxPl7 -RkprF2RwwADNzYS7Tn+Hpmjy06pfYZHNME+Iw515bCRF3GQFUU4BpGnY7EO+h4P9 -Kb1h948gUT9/oswXG+q2Kwk8AoggMJkUOWDFiCa5UjW1GBoxxb7VtZ+QTJXxlFWc -M2VzT7M/HX+P4b05vY4MXJjxPAFKrAGS7J8DKW8WJNUnXa9XSDBHg5qijDzZ/zGm -HTdG6iADnJLmOHBQgFQ12a/n9mYV2GPVC6FlgDzG9f0/SUPBUCafyWYz1LwKY4VM -2NLx/iwYMQsNIMSZQfNmufNDBr70+BShe3ZpbmKB/J33d87AuJd2HjnsThTEAAr+ -6CejyYmwFutoDUCF8IaKGJEp7OGP2//ub4nt5WwW8DYLRi8EqtzEnxPo5ZiayHMY -GHR1jpX1O5JVJFUE79bZCFFHKmtJc4kVZS4m4rTLsk83AgMBAAEwDQYJKoZIhvcN -AQEFBQADggIBAEt4PIRqVuALQSdgZ+GiZYuvEVjxoDVtMSc/ym93Gi8R7DDivFH9 -4suQc5QUiuEF8lpEtkmh+PZ+oFdQkjhBH80h7p4BUSyBy5Yi6dy7ATTlBAqwzCYZ -4wzHeJzu1SI6FinZLksoULbcw04n410aGHkLa6I9O3vCC4kXSnBlwU1sUsJphxM2 -3pkHBpvv79XYf5kFqZPzF16aO7rxFuVvqgXLyzwuyP9kH5zMA21Kioxs/pNyg1lm -5h0VinpHLPse+4tYih1L1WLMpEZiSwZgFhoRtlcdIVXokZPaX4G2EkdrMmSQruWg -Uz8Av6LEYHmRfbYwYM2kEX/+AF8thpTQDbvxjqYk5oyGX4wpKGpih1ac/jYu3O8B -VLhbxZlBYcLxCqqNsGJrWaiHj2Jf4GhUB0O9hXfaZDMqEGXT9GzOz0yF6b3pDQVy -H0lKIBb+kQzB/jhZKu4vrTAowXtt/av5d7D+rpAU1SxfUhBOPNSRoJUI5NSBbokp -a7u4azdB2IQETX3d2rhDk09EbG1XmMi5Vg1oa8nxfMOWXZnDMusJoZClKjrthmwd -rtR5et44XYhX6p217RBkYMDOVFT7aZpu4SaFeqZIuarVYodSmgXToOFXPsrLppRQ -adOT0FpU64RPNrQz5NF1bSIjqrHSaRVacf8yr7qqxNnpMsrtkDJzsMBz ------END CERTIFICATE----- diff --git a/mail/src/leap/mail/smtp/tests/cert/server.key b/mail/src/leap/mail/smtp/tests/cert/server.key deleted file mode 100644 index 197a449..0000000 --- a/mail/src/leap/mail/smtp/tests/cert/server.key +++ /dev/null @@ -1,51 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -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 ------END RSA PRIVATE KEY----- diff --git a/mail/src/leap/mail/smtp/tests/mail.txt b/mail/src/leap/mail/smtp/tests/mail.txt deleted file mode 100644 index 9542047..0000000 --- a/mail/src/leap/mail/smtp/tests/mail.txt +++ /dev/null @@ -1,10 +0,0 @@ -HELO drebs@riseup.net -MAIL FROM: drebs@riseup.net -RCPT TO: drebs@riseup.net -RCPT TO: drebs@leap.se -DATA -Subject: leap test - -Hello world! -. -QUIT diff --git a/mail/src/leap/mail/smtp/tests/test_gateway.py b/mail/src/leap/mail/smtp/tests/test_gateway.py deleted file mode 100644 index 9d88afb..0000000 --- a/mail/src/leap/mail/smtp/tests/test_gateway.py +++ /dev/null @@ -1,181 +0,0 @@ -# -*- coding: utf-8 -*- -# test_gateway.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 <http://www.gnu.org/licenses/>. - -""" -SMTP gateway tests. -""" -import re -import tempfile -from datetime import datetime - -from twisted.internet import reactor -from twisted.internet.defer import inlineCallbacks, fail, succeed, Deferred -from twisted.test import proto_helpers - -from mock import Mock - -from leap.keymanager import openpgp, errors -from leap.mail.testing import KeyManagerWithSoledadTestCase -from leap.mail.testing import ADDRESS, ADDRESS_2 -from leap.mail.testing.smtp import getSMTPFactory, TEST_USER - - -# some regexps -IP_REGEX = "(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}" + \ - "([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])" -HOSTNAME_REGEX = "(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*" + \ - "([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])" -IP_OR_HOST_REGEX = '(' + IP_REGEX + '|' + HOSTNAME_REGEX + ')' - - -class TestSmtpGateway(KeyManagerWithSoledadTestCase): - - EMAIL_DATA = ['HELO gateway.leap.se', - 'MAIL FROM: <%s>' % ADDRESS_2, - 'RCPT TO: <%s>' % ADDRESS, - 'DATA', - 'From: User <%s>' % ADDRESS_2, - 'To: Leap <%s>' % ADDRESS, - 'Date: ' + datetime.now().strftime('%c'), - 'Subject: test message', - '', - 'This is a secret message.', - 'Yours,', - 'A.', - '', - '.', - 'QUIT'] - - def setUp(self): - # pytest handles correctly the setupEnv for the class, - # but trial ignores it. - if not getattr(self, 'tempdir', None): - self.tempdir = tempfile.mkdtemp() - return KeyManagerWithSoledadTestCase.setUp(self) - - def tearDown(self): - return KeyManagerWithSoledadTestCase.tearDown(self) - - def assertMatch(self, string, pattern, msg=None): - if not re.match(pattern, string): - msg = self._formatMessage(msg, '"%s" does not match pattern "%s".' - % (string, pattern)) - raise self.failureException(msg) - - @inlineCallbacks - def test_gateway_accepts_valid_email(self): - """ - Test if SMTP server responds correctly for valid interaction. - """ - - SMTP_ANSWERS = ['220 ' + IP_OR_HOST_REGEX + - ' NO UCE NO UBE NO RELAY PROBES', - '250 ' + IP_OR_HOST_REGEX + ' Hello ' + - IP_OR_HOST_REGEX + ', nice to meet you', - '250 Sender address accepted', - '250 Recipient address accepted', - '354 Continue'] - - user = TEST_USER - proto = getSMTPFactory({user: None}, {user: self.km}, {user: None}) - transport = proto_helpers.StringTransport() - proto.makeConnection(transport) - reply = "" - for i, line in enumerate(self.EMAIL_DATA): - reply += yield self.getReply(line + '\r\n', proto, transport) - self.assertMatch(reply, '\r\n'.join(SMTP_ANSWERS), - 'Did not get expected answer from gateway.') - proto.setTimeout(None) - - @inlineCallbacks - def test_missing_key_rejects_address(self): - """ - Test if server rejects to send unencrypted when 'encrypted_only' is - True. - """ - # remove key from key manager - pubkey = yield self.km.get_key(ADDRESS) - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.delete_key(pubkey) - # mock the key fetching - self.km._fetch_keys_from_server = Mock( - return_value=fail(errors.KeyNotFound())) - user = TEST_USER - proto = getSMTPFactory( - {user: None}, {user: self.km}, {user: None}, - encrypted_only=True) - transport = proto_helpers.StringTransport() - proto.makeConnection(transport) - yield self.getReply(self.EMAIL_DATA[0] + '\r\n', proto, transport) - yield self.getReply(self.EMAIL_DATA[1] + '\r\n', proto, transport) - reply = yield self.getReply(self.EMAIL_DATA[2] + '\r\n', - proto, transport) - # ensure the address was rejected - self.assertEqual( - '550 Cannot receive for specified address\r\n', - reply, - 'Address should have been rejected with appropriate message.') - proto.setTimeout(None) - - @inlineCallbacks - def test_missing_key_accepts_address(self): - """ - Test if server accepts to send unencrypted when 'encrypted_only' is - False. - """ - # remove key from key manager - pubkey = yield self.km.get_key(ADDRESS) - pgp = openpgp.OpenPGPScheme( - self._soledad, gpgbinary=self.gpg_binary_path) - yield pgp.delete_key(pubkey) - # mock the key fetching - self.km._fetch_keys_from_server = Mock( - return_value=fail(errors.KeyNotFound())) - user = TEST_USER - proto = getSMTPFactory({user: None}, {user: self.km}, {user: None}) - transport = proto_helpers.StringTransport() - proto.makeConnection(transport) - yield self.getReply(self.EMAIL_DATA[0] + '\r\n', proto, transport) - yield self.getReply(self.EMAIL_DATA[1] + '\r\n', proto, transport) - reply = yield self.getReply(self.EMAIL_DATA[2] + '\r\n', - proto, transport) - # ensure the address was accepted - self.assertEqual( - '250 Recipient address accepted\r\n', - reply, - 'Address should have been accepted with appropriate message.') - proto.setTimeout(None) - - def getReply(self, line, proto, transport): - proto.lineReceived(line) - - if line[:4] not in ['HELO', 'MAIL', 'RCPT', 'DATA']: - return succeed("") - - def check_transport(_): - reply = transport.value() - if reply: - transport.clear() - return succeed(reply) - - d = Deferred() - d.addCallback(check_transport) - reactor.callLater(0, lambda: d.callback(None)) - return d - - return check_transport(None) diff --git a/mail/src/leap/mail/sync_hooks.py b/mail/src/leap/mail/sync_hooks.py deleted file mode 100644 index 8efbb7c..0000000 --- a/mail/src/leap/mail/sync_hooks.py +++ /dev/null @@ -1,120 +0,0 @@ -# -*- coding: utf-8 -*- -# sync_hooks.py -# Copyright (C) 2015 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 <http://www.gnu.org/licenses/>. -""" -Soledad PostSync Hooks. - -Process every new document of interest after every soledad synchronization, -using the hooks that soledad exposes via plugins. -""" -import logging - -from re import compile as regex_compile - -from zope.interface import implements -from twisted.internet import defer -from twisted.plugin import IPlugin -from twisted.python import log - -from leap.soledad.client.interfaces import ISoledadPostSyncPlugin -from leap.mail import constants - -logger = logging.getLogger(__name__) - - -def _get_doc_type_preffix(s): - return s[:2] - - -class MailProcessingPostSyncHook(object): - implements(IPlugin, ISoledadPostSyncPlugin) - - META_DOC_PREFFIX = _get_doc_type_preffix(constants.METAMSGID) - watched_doc_types = (META_DOC_PREFFIX, ) - - _account = None - _pending_docs = [] - _processing_deferreds = [] - - def process_received_docs(self, doc_id_list): - if self._has_configured_account(): - process_fun = self._make_uid_index - else: - self._processing_deferreds = [] - process_fun = self._queue_doc_id - - for doc_id in doc_id_list: - if _get_doc_type_preffix(doc_id) in self.watched_doc_types: - log.msg("Mail post-sync hook: processing %s" % doc_id) - process_fun(doc_id) - - return defer.gatherResults(self._processing_deferreds) - - def set_account(self, account): - self._account = account - if account: - self._process_queued_docs() - - def _has_configured_account(self): - return self._account is not None - - def _queue_doc_id(self, doc_id): - self._pending_docs.append(doc_id) - - def _make_uid_index(self, mdoc_id): - indexer = self._account.mbox_indexer - mbox_uuid = _get_mbox_uuid(mdoc_id) - if mbox_uuid: - chash = _get_chash_from_mdoc(mdoc_id) - logger.debug("Making index table for %s:%s" % (mbox_uuid, chash)) - index_docid = constants.METAMSGID.format( - mbox_uuid=mbox_uuid.replace('-', '_'), - chash=chash) - # XXX could avoid creating table if I track which ones I already - # have seen -- but make sure *it's already created* before - # inserting the index entry!. - d = indexer.create_table(mbox_uuid) - d.addBoth(lambda _: indexer.insert_doc(mbox_uuid, index_docid)) - self._processing_deferreds.append(d) - - def _process_queued_docs(self): - assert(self._has_configured_account()) - pending = self._pending_docs - log.msg("Mail post-sync hook: processing queued docs") - - def remove_pending_docs(res): - self._pending_docs = [] - return res - - d = self.process_received_docs(pending) - d.addCallback(remove_pending_docs) - return d - - -_mbox_uuid_regex = regex_compile(constants.METAMSGID_MBOX_RE) -_mdoc_chash_regex = regex_compile(constants.METAMSGID_CHASH_RE) - - -def _get_mbox_uuid(doc_id): - matches = _mbox_uuid_regex.findall(doc_id) - if matches: - return matches[0].replace('_', '-') - - -def _get_chash_from_mdoc(doc_id): - matches = _mdoc_chash_regex.findall(doc_id) - if matches: - return matches[0] diff --git a/mail/src/leap/mail/testing/__init__.py b/mail/src/leap/mail/testing/__init__.py deleted file mode 100644 index 8822a5c..0000000 --- a/mail/src/leap/mail/testing/__init__.py +++ /dev/null @@ -1,353 +0,0 @@ -# -*- coding: utf-8 -*- -# __init__.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 <http://www.gnu.org/licenses/>. -""" -Base classes and keys for leap.mail tests. -""" -import os -import distutils.spawn -from mock import Mock -from twisted.internet.defer import gatherResults -from twisted.trial import unittest -from twisted.internet import defer - - -from leap.soledad.client import Soledad -from leap.keymanager import KeyManager - - -from leap.common.testing.basetest import BaseLeapTest - -ADDRESS = 'leap@leap.se' -ADDRESS_2 = 'anotheruser@leap.se' - - -class defaultMockSharedDB(object): - get_doc = Mock(return_value=None) - put_doc = Mock(side_effect=None) - open = Mock(return_value=None) - close = Mock(return_value=None) - syncable = True - - def __call__(self): - return self - - -class KeyManagerWithSoledadTestCase(unittest.TestCase, BaseLeapTest): - - def setUp(self): - self.gpg_binary_path = self._find_gpg() - - self._soledad = Soledad( - u"leap@leap.se", - u"123456", - secrets_path=self.tempdir + "/secret.gpg", - local_db_path=self.tempdir + "/soledad.u1db", - server_url='', - cert_file=None, - auth_token=None, - shared_db=defaultMockSharedDB(), - syncable=False) - - self.km = self._key_manager() - - class Response(object): - code = 200 - phrase = '' - - def deliverBody(self, x): - return '' - - self.km._async_client_pinned.request = Mock( - return_value=defer.succeed(Response())) - - d1 = self.km.put_raw_key(PRIVATE_KEY, ADDRESS) - d2 = self.km.put_raw_key(PRIVATE_KEY_2, ADDRESS_2) - return gatherResults([d1, d2]) - - def tearDown(self): - km = self._key_manager() - # wait for the indexes to be ready for the tear down - d = km._openpgp.deferred_init - d.addCallback(lambda _: self.delete_all_keys(km)) - d.addCallback(lambda _: self._soledad.close()) - return d - - def delete_all_keys(self, km): - def delete_keys(keys): - deferreds = [] - for key in keys: - d = km._openpgp.delete_key(key) - deferreds.append(d) - return gatherResults(deferreds) - - def check_deleted(_, private): - d = km.get_all_keys(private=private) - d.addCallback(lambda keys: self.assertEqual(keys, [])) - return d - - deferreds = [] - for private in [True, False]: - d = km.get_all_keys(private=private) - d.addCallback(delete_keys) - d.addCallback(check_deleted, private) - deferreds.append(d) - return gatherResults(deferreds) - - def _key_manager(self, user=ADDRESS, url='', token=None, - ca_cert_path=None): - return KeyManager(user, url, self._soledad, token=token, - gpgbinary=self.gpg_binary_path, - ca_cert_path=ca_cert_path) - - def _find_gpg(self): - gpg_path = distutils.spawn.find_executable('gpg') - if gpg_path is not None: - return os.path.realpath(gpg_path) - else: - return "/usr/bin/gpg" - - def get_public_binary_key(self): - with open(PATH + '/fixtures/public_key.bin', 'r') as binary_public_key: - return binary_public_key.read() - - def get_private_binary_key(self): - with open( - PATH + '/fixtures/private_key.bin', 'r') as binary_private_key: - return binary_private_key.read() - - -# key 24D18DDF: public key "Leap Test Key <leap@leap.se>" -KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" -PUBLIC_KEY = """ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -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 -=MuOY ------END PGP PUBLIC KEY BLOCK----- -""" -PRIVATE_KEY = """ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -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 -=JTFu ------END PGP PRIVATE KEY BLOCK----- -""" - -# key 7FEE575A: public key "anotheruser <anotheruser@leap.se>" -PUBLIC_KEY_2 = """ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -mI0EUYwJXgEEAMbTKHuPJ5/Gk34l9Z06f+0WCXTDXdte1UBoDtZ1erAbudgC4MOR -gquKqoj3Hhw0/ILqJ88GcOJmKK/bEoIAuKaqlzDF7UAYpOsPZZYmtRfPC2pTCnXq -Z1vdeqLwTbUspqXflkCkFtfhGKMq5rH8GV5a3tXZkRWZhdNwhVXZagC3ABEBAAG0 -IWFub3RoZXJ1c2VyIDxhbm90aGVydXNlckBsZWFwLnNlPoi4BBMBAgAiBQJRjAle -AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRB/nfpof+5XWotuA/4tLN4E -gUr7IfLy2HkHAxzw7A4rqfMN92DIM9mZrDGaWRrOn3aVF7VU1UG7MDkHfPvp/cFw -ezoCw4s4IoHVc/pVlOkcHSyt4/Rfh248tYEJmFCJXGHpkK83VIKYJAithNccJ6Q4 -JE/o06Mtf4uh/cA1HUL4a4ceqUhtpLJULLeKo7iNBFGMCV4BBADsyQI7GR0wSAxz -VayLjuPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQt -Z/hwcLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63 -yuRe94WenT1RJd6xU1aaUff4rKizuQARAQABiJ8EGAECAAkFAlGMCV4CGwwACgkQ -f536aH/uV1rPZQQAqCzRysOlu8ez7PuiBD4SebgRqWlxa1TF1ujzfLmuPivROZ2X -Kw5aQstxgGSjoB7tac49s0huh4X8XK+BtJBfU84JS8Jc2satlfwoyZ35LH6sDZck -I+RS/3we6zpMfHs3vvp9xgca6ZupQxivGtxlJs294TpJorx+mFFqbV17AzQ= -=Thdu ------END PGP PUBLIC KEY BLOCK----- -""" - -PRIVATE_KEY_2 = """ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -lQHYBFGMCV4BBADG0yh7jyefxpN+JfWdOn/tFgl0w13bXtVAaA7WdXqwG7nYAuDD -kYKriqqI9x4cNPyC6ifPBnDiZiiv2xKCALimqpcwxe1AGKTrD2WWJrUXzwtqUwp1 -6mdb3Xqi8E21LKal35ZApBbX4RijKuax/BleWt7V2ZEVmYXTcIVV2WoAtwARAQAB -AAP7BLuSAx7tOohnimEs74ks8l/L6dOcsFQZj2bqs4AoY3jFe7bV0tHr4llypb/8 -H3/DYvpf6DWnCjyUS1tTnXSW8JXtx01BUKaAufSmMNg9blKV6GGHlT/Whe9uVyks -7XHk/+9mebVMNJ/kNlqq2k+uWqJohzC8WWLRK+d1tBeqDsECANZmzltPaqUsGV5X -C3zszE3tUBgptV/mKnBtopKi+VH+t7K6fudGcG+bAcZDUoH/QVde52mIIjjIdLje -uajJuHUCAO1mqh+vPoGv4eBLV7iBo3XrunyGXiys4a39eomhxTy3YktQanjjx+ty -GltAGCs5PbWGO6/IRjjvd46wh53kzvsCAO0J97gsWhzLuFnkxFAJSPk7RRlyl7lI -1XS/x0Og6j9XHCyY1OYkfBm0to3UlCfkgirzCYlTYObCofzdKFIPDmSqHbQhYW5v -dGhlcnVzZXIgPGFub3RoZXJ1c2VyQGxlYXAuc2U+iLgEEwECACIFAlGMCV4CGwMG -CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEH+d+mh/7ldai24D/i0s3gSBSvsh -8vLYeQcDHPDsDiup8w33YMgz2ZmsMZpZGs6fdpUXtVTVQbswOQd8++n9wXB7OgLD -izgigdVz+lWU6RwdLK3j9F+Hbjy1gQmYUIlcYemQrzdUgpgkCK2E1xwnpDgkT+jT -oy1/i6H9wDUdQvhrhx6pSG2kslQst4qjnQHYBFGMCV4BBADsyQI7GR0wSAxzVayL -juPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQtZ/hw -cLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63yuRe -94WenT1RJd6xU1aaUff4rKizuQARAQABAAP9EyElqJ3dq3EErXwwT4mMnbd1SrVC -rUJrNWQZL59mm5oigS00uIyR0SvusOr+UzTtd8ysRuwHy5d/LAZsbjQStaOMBILx -77TJveOel0a1QK0YSMF2ywZMCKvquvjli4hAtWYz/EwfuzQN3t23jc5ny+GqmqD2 -3FUxLJosFUfLNmECAO9KhVmJi+L9dswIs+2Dkjd1eiRQzNOEVffvYkGYZyKxNiXF -UA5kvyZcB4iAN9sWCybE4WHZ9jd4myGB0MPDGxkCAP1RsXJbbuD6zS7BXe5gwunO -2q4q7ptdSl/sJYQuTe1KNP5d/uGsvlcFfsYjpsopasPjFBIncc/2QThMKlhoEaEB -/0mVAxpT6SrEvUbJ18z7kna24SgMPr3OnPMxPGfvNLJY/Xv/A17YfoqjmByCvsKE -JCDjopXtmbcrZyoEZbEht9mko4ifBBgBAgAJBQJRjAleAhsMAAoJEH+d+mh/7lda -z2UEAKgs0crDpbvHs+z7ogQ+Enm4EalpcWtUxdbo83y5rj4r0TmdlysOWkLLcYBk -o6Ae7WnOPbNIboeF/FyvgbSQX1POCUvCXNrGrZX8KMmd+Sx+rA2XJCPkUv98Hus6 -THx7N776fcYHGumbqUMYrxrcZSbNveE6SaK8fphRam1dewM0 -=a5gs ------END PGP PRIVATE KEY BLOCK----- -""" diff --git a/mail/src/leap/mail/testing/__init__.py~ b/mail/src/leap/mail/testing/__init__.py~ deleted file mode 100644 index ffaadd8..0000000 --- a/mail/src/leap/mail/testing/__init__.py~ +++ /dev/null @@ -1,358 +0,0 @@ -# -*- coding: utf-8 -*- -# __init__.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 <http://www.gnu.org/licenses/>. -""" -Base classes and keys for leap.mail tests. -""" -import os -import distutils.spawn -from mock import Mock -from twisted.internet.defer import gatherResults, succeed -from twisted.trial import unittest -from twisted.web.client import Response -from twisted.internet import defer -from twisted.python import log - - -from leap.soledad.client import Soledad -from leap.keymanager import KeyManager - - -from leap.common.testing.basetest import BaseLeapTest - -ADDRESS = 'leap@leap.se' -ADDRESS_2 = 'anotheruser@leap.se' - -class defaultMockSharedDB(object): - get_doc = Mock(return_value=None) - put_doc = Mock(side_effect=None) - open = Mock(return_value=None) - close = Mock(return_value=None) - syncable = True - - def __call__(self): - return self - - -class KeyManagerWithSoledadTestCase(unittest.TestCase, BaseLeapTest): - - def setUp(self): - self.gpg_binary_path = self._find_gpg() - - self._soledad = Soledad( - u"leap@leap.se", - u"123456", - secrets_path=self.tempdir + "/secret.gpg", - local_db_path=self.tempdir + "/soledad.u1db", - server_url='', - cert_file=None, - auth_token=None, - shared_db=defaultMockSharedDB(), - syncable=False) - - self.km = self._key_manager() - - class Response(object): - code = 200 - phrase = '' - def deliverBody(self, x): - return - - # XXX why the fuck is this needed? ------------------------ - self.km._async_client_pinned.request = Mock( - return_value=defer.succeed(Response())) - #self.km._async_client.request = Mock(return_value='') - #self.km._async_client_pinned.request = Mock( - #return_value='') - # ------------------------------------------------------- - - d1 = self.km.put_raw_key(PRIVATE_KEY, ADDRESS) - d2 = self.km.put_raw_key(PRIVATE_KEY_2, ADDRESS_2) - return gatherResults([d1, d2]) - - def tearDown(self): - km = self._key_manager() - # wait for the indexes to be ready for the tear down - d = km._openpgp.deferred_init - d.addCallback(lambda _: self.delete_all_keys(km)) - d.addCallback(lambda _: self._soledad.close()) - return d - - def delete_all_keys(self, km): - def delete_keys(keys): - deferreds = [] - for key in keys: - d = km._openpgp.delete_key(key) - deferreds.append(d) - return gatherResults(deferreds) - - def check_deleted(_, private): - d = km.get_all_keys(private=private) - d.addCallback(lambda keys: self.assertEqual(keys, [])) - return d - - deferreds = [] - for private in [True, False]: - d = km.get_all_keys(private=private) - d.addCallback(delete_keys) - d.addCallback(check_deleted, private) - deferreds.append(d) - return gatherResults(deferreds) - - def _key_manager(self, user=ADDRESS, url='', token=None, - ca_cert_path=None): - return KeyManager(user, url, self._soledad, token=token, - gpgbinary=self.gpg_binary_path, - ca_cert_path=ca_cert_path) - - def _find_gpg(self): - gpg_path = distutils.spawn.find_executable('gpg') - if gpg_path is not None: - return os.path.realpath(gpg_path) - else: - return "/usr/bin/gpg" - - def get_public_binary_key(self): - with open(PATH + '/fixtures/public_key.bin', 'r') as binary_public_key: - return binary_public_key.read() - - def get_private_binary_key(self): - with open( - PATH + '/fixtures/private_key.bin', 'r') as binary_private_key: - return binary_private_key.read() - - -# key 24D18DDF: public key "Leap Test Key <leap@leap.se>" -KEY_FINGERPRINT = "E36E738D69173C13D709E44F2F455E2824D18DDF" -PUBLIC_KEY = """ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -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 -=MuOY ------END PGP PUBLIC KEY BLOCK----- -""" -PRIVATE_KEY = """ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -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 -=JTFu ------END PGP PRIVATE KEY BLOCK----- -""" - -# key 7FEE575A: public key "anotheruser <anotheruser@leap.se>" -PUBLIC_KEY_2 = """ ------BEGIN PGP PUBLIC KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -mI0EUYwJXgEEAMbTKHuPJ5/Gk34l9Z06f+0WCXTDXdte1UBoDtZ1erAbudgC4MOR -gquKqoj3Hhw0/ILqJ88GcOJmKK/bEoIAuKaqlzDF7UAYpOsPZZYmtRfPC2pTCnXq -Z1vdeqLwTbUspqXflkCkFtfhGKMq5rH8GV5a3tXZkRWZhdNwhVXZagC3ABEBAAG0 -IWFub3RoZXJ1c2VyIDxhbm90aGVydXNlckBsZWFwLnNlPoi4BBMBAgAiBQJRjAle -AhsDBgsJCAcDAgYVCAIJCgsEFgIDAQIeAQIXgAAKCRB/nfpof+5XWotuA/4tLN4E -gUr7IfLy2HkHAxzw7A4rqfMN92DIM9mZrDGaWRrOn3aVF7VU1UG7MDkHfPvp/cFw -ezoCw4s4IoHVc/pVlOkcHSyt4/Rfh248tYEJmFCJXGHpkK83VIKYJAithNccJ6Q4 -JE/o06Mtf4uh/cA1HUL4a4ceqUhtpLJULLeKo7iNBFGMCV4BBADsyQI7GR0wSAxz -VayLjuPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQt -Z/hwcLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63 -yuRe94WenT1RJd6xU1aaUff4rKizuQARAQABiJ8EGAECAAkFAlGMCV4CGwwACgkQ -f536aH/uV1rPZQQAqCzRysOlu8ez7PuiBD4SebgRqWlxa1TF1ujzfLmuPivROZ2X -Kw5aQstxgGSjoB7tac49s0huh4X8XK+BtJBfU84JS8Jc2satlfwoyZ35LH6sDZck -I+RS/3we6zpMfHs3vvp9xgca6ZupQxivGtxlJs294TpJorx+mFFqbV17AzQ= -=Thdu ------END PGP PUBLIC KEY BLOCK----- -""" - -PRIVATE_KEY_2 = """ ------BEGIN PGP PRIVATE KEY BLOCK----- -Version: GnuPG v1.4.10 (GNU/Linux) - -lQHYBFGMCV4BBADG0yh7jyefxpN+JfWdOn/tFgl0w13bXtVAaA7WdXqwG7nYAuDD -kYKriqqI9x4cNPyC6ifPBnDiZiiv2xKCALimqpcwxe1AGKTrD2WWJrUXzwtqUwp1 -6mdb3Xqi8E21LKal35ZApBbX4RijKuax/BleWt7V2ZEVmYXTcIVV2WoAtwARAQAB -AAP7BLuSAx7tOohnimEs74ks8l/L6dOcsFQZj2bqs4AoY3jFe7bV0tHr4llypb/8 -H3/DYvpf6DWnCjyUS1tTnXSW8JXtx01BUKaAufSmMNg9blKV6GGHlT/Whe9uVyks -7XHk/+9mebVMNJ/kNlqq2k+uWqJohzC8WWLRK+d1tBeqDsECANZmzltPaqUsGV5X -C3zszE3tUBgptV/mKnBtopKi+VH+t7K6fudGcG+bAcZDUoH/QVde52mIIjjIdLje -uajJuHUCAO1mqh+vPoGv4eBLV7iBo3XrunyGXiys4a39eomhxTy3YktQanjjx+ty -GltAGCs5PbWGO6/IRjjvd46wh53kzvsCAO0J97gsWhzLuFnkxFAJSPk7RRlyl7lI -1XS/x0Og6j9XHCyY1OYkfBm0to3UlCfkgirzCYlTYObCofzdKFIPDmSqHbQhYW5v -dGhlcnVzZXIgPGFub3RoZXJ1c2VyQGxlYXAuc2U+iLgEEwECACIFAlGMCV4CGwMG -CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEH+d+mh/7ldai24D/i0s3gSBSvsh -8vLYeQcDHPDsDiup8w33YMgz2ZmsMZpZGs6fdpUXtVTVQbswOQd8++n9wXB7OgLD -izgigdVz+lWU6RwdLK3j9F+Hbjy1gQmYUIlcYemQrzdUgpgkCK2E1xwnpDgkT+jT -oy1/i6H9wDUdQvhrhx6pSG2kslQst4qjnQHYBFGMCV4BBADsyQI7GR0wSAxzVayL -juPzgT+bjbFeymIhjuxKIEwnIKwYkovztW+4bbOcQs785k3Lp6RzvigTpQQtZ/hw -cLOqZbZw8t/24+D+Pq9mMP2uUvCFFqLlVvA6D3vKSQ/XNN+YB919WQ04jh63yuRe -94WenT1RJd6xU1aaUff4rKizuQARAQABAAP9EyElqJ3dq3EErXwwT4mMnbd1SrVC -rUJrNWQZL59mm5oigS00uIyR0SvusOr+UzTtd8ysRuwHy5d/LAZsbjQStaOMBILx -77TJveOel0a1QK0YSMF2ywZMCKvquvjli4hAtWYz/EwfuzQN3t23jc5ny+GqmqD2 -3FUxLJosFUfLNmECAO9KhVmJi+L9dswIs+2Dkjd1eiRQzNOEVffvYkGYZyKxNiXF -UA5kvyZcB4iAN9sWCybE4WHZ9jd4myGB0MPDGxkCAP1RsXJbbuD6zS7BXe5gwunO -2q4q7ptdSl/sJYQuTe1KNP5d/uGsvlcFfsYjpsopasPjFBIncc/2QThMKlhoEaEB -/0mVAxpT6SrEvUbJ18z7kna24SgMPr3OnPMxPGfvNLJY/Xv/A17YfoqjmByCvsKE -JCDjopXtmbcrZyoEZbEht9mko4ifBBgBAgAJBQJRjAleAhsMAAoJEH+d+mh/7lda -z2UEAKgs0crDpbvHs+z7ogQ+Enm4EalpcWtUxdbo83y5rj4r0TmdlysOWkLLcYBk -o6Ae7WnOPbNIboeF/FyvgbSQX1POCUvCXNrGrZX8KMmd+Sx+rA2XJCPkUv98Hus6 -THx7N776fcYHGumbqUMYrxrcZSbNveE6SaK8fphRam1dewM0 -=a5gs ------END PGP PRIVATE KEY BLOCK----- -""" diff --git a/mail/src/leap/mail/testing/common.py b/mail/src/leap/mail/testing/common.py deleted file mode 100644 index 1bf1de2..0000000 --- a/mail/src/leap/mail/testing/common.py +++ /dev/null @@ -1,109 +0,0 @@ -# -*- coding: utf-8 -*- -# common.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 <http://www.gnu.org/licenses/>. -""" -Common utilities for testing Soledad. -""" -import os -import tempfile - -from twisted.internet import defer -from twisted.trial import unittest - -from leap.common.testing.basetest import BaseLeapTest -from leap.soledad.client import Soledad - -# TODO move to common module, or Soledad itself -# XXX remove duplication - -TEST_USER = "testuser@leap.se" -TEST_PASSWD = "1234" - - -def _initialize_soledad(email, gnupg_home, tempdir): - """ - Initializes soledad by hand - - :param email: ID for the user - :param gnupg_home: path to home used by gnupg - :param tempdir: path to temporal dir - :rtype: Soledad instance - """ - - uuid = "foobar-uuid" - passphrase = u"verysecretpassphrase" - secret_path = os.path.join(tempdir, "secret.gpg") - local_db_path = os.path.join(tempdir, "soledad.u1db") - server_url = "https://provider" - cert_file = "" - - soledad = Soledad( - uuid, - passphrase, - secret_path, - local_db_path, - server_url, - cert_file, - syncable=False) - - return soledad - - -class SoledadTestMixin(unittest.TestCase, BaseLeapTest): - """ - It is **VERY** important that this base is added *AFTER* unittest.TestCase - """ - - def setUp(self): - self.results = [] - self.setUpEnv() - - # pytest handles correctly the setupEnv for the class, - # but trial ignores it. - if not getattr(self, 'tempdir', None): - self.tempdir = tempfile.gettempdir() - - # Soledad: config info - self.gnupg_home = "%s/gnupg" % self.tempdir - self.email = 'leap@leap.se' - - # initialize soledad by hand so we can control keys - self._soledad = _initialize_soledad( - self.email, - self.gnupg_home, - self.tempdir) - - return defer.succeed(True) - - def tearDown(self): - """ - tearDown method called after each test. - """ - self.results = [] - try: - self._soledad.close() - except Exception: - print "ERROR WHILE CLOSING SOLEDAD" - # logging.exception(exc) - self.tearDownEnv() - - @classmethod - def setUpClass(self): - pass - - @classmethod - def tearDownClass(self): - pass diff --git a/mail/src/leap/mail/testing/imap.py b/mail/src/leap/mail/testing/imap.py deleted file mode 100644 index 72acbf2..0000000 --- a/mail/src/leap/mail/testing/imap.py +++ /dev/null @@ -1,186 +0,0 @@ -# -*- coding: utf-8 -*- -# utils.py -# Copyright (C) 2014, 2015 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 <http://www.gnu.org/licenses/>. -""" -Common utilities for testing Soledad IMAP Server. -""" -from email import parser - -from mock import Mock -from twisted.cred.checkers import ICredentialsChecker -from twisted.cred.credentials import IUsernamePassword -from twisted.cred.error import UnauthorizedLogin -from twisted.cred.portal import Portal, IRealm -from twisted.mail import imap4 -from twisted.internet import defer -from twisted.protocols import loopback -from twisted.python import log -from zope.interface import implementer - -from leap.mail.adaptors import soledad as soledad_adaptor -from leap.mail.imap.account import IMAPAccount -from leap.mail.imap.server import LEAPIMAPServer -from leap.mail.testing.common import SoledadTestMixin - -TEST_USER = "testuser@leap.se" -TEST_PASSWD = "1234" - - -# -# Simple IMAP4 Client for testing -# - -class SimpleClient(imap4.IMAP4Client): - """ - A Simple IMAP4 Client to test our - Soledad-LEAPServer - """ - - def __init__(self, deferred, contextFactory=None): - imap4.IMAP4Client.__init__(self, contextFactory) - self.deferred = deferred - self.events = [] - - def serverGreeting(self, caps): - self.deferred.callback(None) - - def modeChanged(self, writeable): - self.events.append(['modeChanged', writeable]) - self.transport.loseConnection() - - def flagsChanged(self, newFlags): - self.events.append(['flagsChanged', newFlags]) - self.transport.loseConnection() - - def newMessages(self, exists, recent): - self.events.append(['newMessages', exists, recent]) - self.transport.loseConnection() - -# -# Dummy credentials for tests -# - - -@implementer(IRealm) -class TestRealm(object): - - def __init__(self, account): - self._account = account - - def requestAvatar(self, avatarId, mind, *interfaces): - avatar = self._account - return (imap4.IAccount, avatar, - getattr(avatar, 'logout', lambda: None)) - - -@implementer(ICredentialsChecker) -class TestCredentialsChecker(object): - - credentialInterfaces = (IUsernamePassword,) - - userid = TEST_USER - password = TEST_PASSWD - - def requestAvatarId(self, credentials): - username, password = credentials.username, credentials.password - d = self.checkTestCredentials(username, password) - d.addErrback(lambda f: defer.fail(UnauthorizedLogin())) - return d - - def checkTestCredentials(self, username, password): - if username == self.userid and password == self.password: - return defer.succeed(username) - else: - return defer.fail(Exception("Wrong credentials")) - - -class TestSoledadIMAPServer(LEAPIMAPServer): - - def __init__(self, account, *args, **kw): - - LEAPIMAPServer.__init__(self, *args, **kw) - - realm = TestRealm(account) - portal = Portal(realm) - checker = TestCredentialsChecker() - self.checker = checker - self.portal = portal - portal.registerChecker(checker) - - -class IMAP4HelperMixin(SoledadTestMixin): - """ - MixIn containing several utilities to be shared across - different TestCases - """ - serverCTX = None - clientCTX = None - - def setUp(self): - - soledad_adaptor.cleanup_deferred_locks() - - USERID = TEST_USER - - def setup_server(account): - self.server = TestSoledadIMAPServer( - account=account, - contextFactory=self.serverCTX) - self.server.theAccount = account - - d_server_ready = defer.Deferred() - self.client = SimpleClient( - d_server_ready, contextFactory=self.clientCTX) - self.connected = d_server_ready - - def setup_account(_): - self.parser = parser.Parser() - - # XXX this should be fixed in soledad. - # Soledad sync makes trial block forever. The sync it's mocked to - # fix this problem. _mock_soledad_get_from_index can be used from - # the tests to provide documents. - # TODO see here, possibly related? - # -- http://www.pythoneye.com/83_20424875/ - self._soledad.sync = Mock() - - d = defer.Deferred() - self.acc = IMAPAccount(self._soledad, USERID, d=d) - return d - - d = super(IMAP4HelperMixin, self).setUp() - d.addCallback(setup_account) - d.addCallback(setup_server) - return d - - def tearDown(self): - SoledadTestMixin.tearDown(self) - del self._soledad - del self.client - del self.server - del self.connected - - def _cbStopClient(self, ignore): - self.client.transport.loseConnection() - - def _ebGeneral(self, failure): - self.client.transport.loseConnection() - self.server.transport.loseConnection() - if hasattr(self, 'function'): - log.err(failure, "Problem with %r" % (self.function,)) - - def loopback(self): - return loopback.loopbackAsync(self.server, self.client) diff --git a/mail/src/leap/mail/testing/smtp.py b/mail/src/leap/mail/testing/smtp.py deleted file mode 100644 index d8690f1..0000000 --- a/mail/src/leap/mail/testing/smtp.py +++ /dev/null @@ -1,51 +0,0 @@ -from twisted.mail import smtp - -from leap.mail.smtp.gateway import SMTPFactory, LOCAL_FQDN -from leap.mail.smtp.gateway import SMTPDelivery -from leap.mail.outgoing.service import outgoingFactory - -TEST_USER = u'anotheruser@leap.se' - - -class UnauthenticatedSMTPServer(smtp.SMTP): - - encrypted_only = False - - def __init__(self, soledads, keyms, opts, encrypted_only=False): - smtp.SMTP.__init__(self) - - userid = TEST_USER - keym = keyms[userid] - - class Opts: - cert = '/tmp/cert' - key = '/tmp/cert' - hostname = 'remote' - port = 666 - - outgoing = outgoingFactory( - userid, keym, Opts, check_cert=False) - avatar = SMTPDelivery(userid, keym, encrypted_only, outgoing) - self.delivery = avatar - - def validateFrom(self, helo, origin): - return origin - - -class UnauthenticatedSMTPFactory(SMTPFactory): - """ - A Factory that produces a SMTP server that does not authenticate user. - Only for tests! - """ - protocol = UnauthenticatedSMTPServer - domain = LOCAL_FQDN - encrypted_only = False - - -def getSMTPFactory(soledad_s, keymanager_s, sendmail_opts, - encrypted_only=False): - factory = UnauthenticatedSMTPFactory - factory.encrypted_only = encrypted_only - proto = factory( - soledad_s, keymanager_s, sendmail_opts).buildProtocol(('127.0.0.1', 0)) - return proto diff --git a/mail/src/leap/mail/tests/rfc822.bounce.message b/mail/src/leap/mail/tests/rfc822.bounce.message deleted file mode 100644 index 7a51ac0..0000000 --- a/mail/src/leap/mail/tests/rfc822.bounce.message +++ /dev/null @@ -1,152 +0,0 @@ -Return-Path: <> -X-Original-To: yoyo@dev.pixelated-project.org -Delivered-To: a6973ec1af0a6d1e2a1e4db4ff85f6c2@deliver.local -Received: by dev1.dev.pixelated-project.org (Postfix) - id 92CEA83164; Thu, 16 Jun 2016 14:53:34 +0200 (CEST) -Date: Thu, 16 Jun 2016 14:53:34 +0200 (CEST) -From: MAILER-DAEMON@dev1.dev.pixelated-project.org (Mail Delivery System) -Subject: Undelivered Mail Returned to Sender -To: yoyo@dev.pixelated-project.org -Auto-Submitted: auto-replied -MIME-Version: 1.0 -Content-Type: multipart/report; report-type=delivery-status; - boundary="8F60183010.1466081614/dev1.dev.pixelated-project.org" -Message-Id: <20160616125334.92CEA83164@dev1.dev.pixelated-project.org> - -This is a MIME-encapsulated message. - ---8F60183010.1466081614/dev1.dev.pixelated-project.org -Content-Description: Notification -Content-Type: text/plain; charset=us-ascii - -This is the mail system at host dev1.dev.pixelated-project.org. - -I'm sorry to have to inform you that your message could not -be delivered to one or more recipients. It's attached below. - -For further assistance, please send mail to postmaster. - -If you do so, please include this problem report. You can -delete your own text from the attached returned message. - - The mail system - -<nobody@leap.se>: host caribou.leap.se[176.53.69.122] said: 550 5.1.1 - <nobody@leap.se>: Recipient address rejected: User unknown in virtual alias - table (in reply to RCPT TO command) - ---8F60183010.1466081614/dev1.dev.pixelated-project.org -Content-Description: Delivery report -Content-Type: message/delivery-status - -Reporting-MTA: dns; dev1.dev.pixelated-project.org -X-Postfix-Queue-ID: 8F60183010 -X-Postfix-Sender: rfc822; yoyo@dev.pixelated-project.org -Arrival-Date: Thu, 16 Jun 2016 14:53:33 +0200 (CEST) - -Final-Recipient: rfc822; nobody@leap.se -Original-Recipient: rfc822;nobody@leap.se -Action: failed -Status: 5.1.1 -Remote-MTA: dns; caribou.leap.se -Diagnostic-Code: smtp; 550 5.1.1 <nobody@leap.se>: Recipient address rejected: - User unknown in virtual alias table - ---8F60183010.1466081614/dev1.dev.pixelated-project.org -Content-Description: Undelivered Message -Content-Type: message/rfc822 - -Return-Path: <yoyo@dev.pixelated-project.org> -Received: from leap.mail-0.4.0rc1+111.g736ea86 (localhost [127.0.0.1]) - (using TLSv1 with cipher ECDHE-RSA-AES128-SHA (128/128 bits)) - (Client CN "yoyo@dev.pixelated-project.org", Issuer "Pixelated Project Root CA (client certificates only!)" (verified OK)) - by dev1.dev.pixelated-project.org (Postfix) with ESMTPS id 8F60183010 - for <nobody@leap.se>; Thu, 16 Jun 2016 14:53:33 +0200 (CEST) -MIME-Version: 1.0 -Content-Type: multipart/signed; protocol="application/pgp-signature"; - micalg="pgp-sha512"; boundary="===============7598747164910592838==" -To: nobody@leap.se -Subject: vrgg -From: yoyo@dev.pixelated-project.org -Date: Thu, 16 Jun 2016 13:53:32 -0000 -Message-Id: <20160616125332.16961.677041909.5@dev1.dev.pixelated-project.org> -OpenPGP: id=CB546109E857BC34DFF2BCB3288870B39C400C24; - url="https://dev.pixelated-project.org/key/yoyo"; preference="signencrypt" - ---===============7598747164910592838== -Content-Type: multipart/mixed; boundary="===============3737055506052708210==" -MIME-Version: 1.0 -To: nobody@leap.se -Subject: vrgg -From: yoyo@dev.pixelated-project.org -Date: Thu, 16 Jun 2016 13:53:32 -0000 - ---===============3737055506052708210== -Content-Type: text/plain; charset="utf-8" -MIME-Version: 1.0 -Content-Transfer-Encoding: base64 - - ---===============3737055506052708210== -Content-Type: application/pgp-keys -MIME-Version: 1.0 -content-disposition: attachment; filename="yoyo@dev.pixelated-project.org-email-key.asc" -Content-Transfer-Encoding: base64 - -LS0tLS1CRUdJTiBQR1AgUFVCTElDIEtFWSBCTE9DSy0tLS0tCgptUUlOQkZkZ01BZ0JFQURIWWpU -T20wcTdOT0lYVUpoTmlHVXg2S05OZ1M0Q0I2VlMvbGtab2UvYjZuRjdCSENmCkFnRVkxeFlxMkIv -MzA3YzBtNTZWMEZvOWt2ZmZCUWhQckU5WG9rckI5blRlN1RsSDZUNTdiV09LSWMyMHhNSy8KSlVU -djZ3UEpybjdLN0VyNEdxbzdrUmpWcFVBcWlBbGFxMkhVYllGd2NEMnBIb0VENmU2L01CZDBVUTFX -b2s4QQpPNURDc2ZmeWhBZ0NFU1poK2w2VHlsVEJXYTJDTmJvUTl0SWtPZ0ZWTk9kTW9uWkxoTk1N -Y0tIeU54dmF5bUdCCjhjQlRISVE2UWhGRThvR2JDRTdvczdZWWhyTmNmcUsyMzJJQllzTHNXN3Vk -QmdwRTA0YkpwQWlvbW1zTHBCYmwKV0pCSjdqeEhwWmhJR3JGL1ltejNsSXpkbm9Mb3BSSWJyS0pC -MmxaVDhIUHBlTVVJdVE2eHErd3RhQXFJVzlPTgo5U29uZWYyVU5BL3VseW1LeDRkOFhxbEwxY3hE -aDFQU1E5YVlPcVg0RDlrMklmOXZmR2hET0xVMzR2Y2VFOC8vCnM1WGdTY2ZFbHg2SWlEVWZHdGx2 -aE5zQUM4TmhhUU1sOHJjUXVoRDA2RFdvSUowMVhkeFJVM2JSVVZkc0I1NWMKcXRWSHJMbVBVb256 -NU13MGFURzlTZzZudUlQcU1QOVNKRlBzbVpzR3ZYVnZWbCtSNzl1SFBlc25yWkoyTjZqOQpNaUth -S045NFBhL1dJUnRoYWdzVnpHeHNtd2orTVZCRkZKRmh0TUtnNlFzYUsvbzRLNGJFR1ZLdWNXQk1i -MnNxCldmd0o0SndTcHcrOHgyS3p6aXhWTllTZXhRdm9oMkc3RDRmRXdISDJzazNST3k3dTlldjhs -bEVqUFFBUkFRQUIKdEQ5NWIzbHZRR1JsZGk1d2FYaGxiR0YwWldRdGNISnZhbVZqZEM1dmNtY2dQ -SGx2ZVc5QVpHVjJMbkJwZUdWcwpZWFJsWkMxd2NtOXFaV04wTG05eVp6NkpBajRFRXdFQ0FDZ0ZB -bGRnTUFnQ0d5OEZDUUhnTUZnR0N3a0lCd01DCkJoVUlBZ2tLQ3dRV0FnTUJBaDRCQWhlQUFBb0pF -Q2lJY0xPY1FBd2s4djBQL2o2MmNyNjRUMlZPMVNKdHp1RlEKWjVpeVJsVFVHSGN2NW5hQjlUSDdI -VVB3cTVwekZiTkg5SnhNRjVFRWtvZjdvV0hWeldWVTFBM1NDdzVNZ2FFbwppWTk5ZFBGNzdHazJ4 -ZEczNXZlWmIwWkg2WkVLdks1S042VXBucG5IeStxaVZVc1FLcE9DdUZKNkF0UlVEOTRJClJ2YnUv -S1hsMHdORDlzVXFlYkJZN1BBSlRNY1RjLzVEdWpIT1Erd3VlSkFtaFZZbEozVnpZK1lBS2t5U05B -QVoKZ3VVenNyUm5xQWU5SmU5TGgrcERpcVpHT2tEK1Z3b2kvRlVPQXJwbWFnNzZONTVjR3hiK2VG -QUlzRHYrM1NNOQpjUDFyQkFON2lEaGgvdkdJeHgzMFlrYUlpMmpmcXg3VXUydnNwSXh6K0NsWWdi -dm1wZm1CWmFqVzYzR0FsK3YvCngrby92eFZmVTMraTZ3alFjRS8vRTBTR2pvY3lQdUw0ZTZLNERy -S3k2SHQycjBQckdHVFZ0dUZPaWU2dnVzbVcKL09sdVB1dGszU3o1S1BmRDFpRXBobmpPQ0pNRkZx -Z2xRM1pPa3MweG00WGdwWW1ycnpQcXc1WWlzK1NEVjhobwp6anlrSzRWUlcrcC9IcUVzU29GQm5a -MG5XSmg2Q1pZOExIeVNiMVJwaFlMRFpWd21JRXd1OW12Vm1ISVIyWUZVCllNZEx4UExiOFZNei9t -QWpMb2Q0OGNSSzdSTzBSZ1RoMTUyK0VieXRGR3k5Y2tiS3VzRmJzVTFCQjN2MFJyUlUKenozTTcx -T3hjcFhVQ0tpWlI0MEVYZnErSnVtZVFudm1wSWdZdUNaQkh5MzJwQUJuOHNDdUlrMStyQnp4bXdt -bgp0WGh0K0RvNlExYXYyVjZYR00xV2xoKzEKPU8zaHEKLS0tLS1FTkQgUEdQIFBVQkxJQyBLRVkg -QkxPQ0stLS0tLQo= ---===============3737055506052708210==-- - ---===============7598747164910592838== -Content-Type: application/pgp-signature; name="signature.asc" -MIME-Version: 1.0 -Content-Description: OpenPGP Digital Signature - ------BEGIN PGP SIGNATURE----- - -iQIcBAABCgAGBQJXYqFNAAoJECiIcLOcQAwkDEIQAL67/XJXDv+lusoy18jr7Ony -WQEP0pIRLp4GywGpH3dAITFAkuamO4VX3QEdVGjOHNoaT8VkSVWf9mnsYLl+Mh2v -1OIwMv0u8WyVtrcxyXijIznnJv8X1RgyCzpUJcmOh04VZcDyxKbnFHWSDMfJ4Jtq -qnXDONcfEeT8pwrGjP5qzTgcF/irG3w5svyQjEtj6kycddYtqUc9Hx3cMaRIzsHg -kuUzznSzU/6P0Z345q/kXyYvU9rlcsP9vogrsqL2ueLwYSipxUJQUrRWG82FYoCo -PAKNdGIt0xl2gEW+xWZkJqFarPiUFCx//+bVBelKrqj6rjwbj+E7mHJW318JYVHQ -en3Smv7pEWlT4hZHXnoe8ng6TAvKzQjf7/bUxq2JpKSycp2hDO3Qz3Tv+kc+jC/r -5UDWe/flR+syq8lAQTRSn6057g3BgDG2RtAwsjedg1aTFSrljSxbKlK4vsj5Muek -Olq9+MUdMFSE3Jj/JC2COcS3rlt/Qt+JLDYXKahU3CodaSgF2dobikDe1bW0/QNS -7O4Ng2PK0pA416RCFRUgPXerUnMGiWAiq7BoRHeym9y7fkHYhIYGpPVKXJ6t67y5 -JjvuzwfwG8SZTp4Wy2pg1Mr6znm6uVBxUDxTHyP3BjciI1zpEigOIg9UwJ9nCDxL -uUGz4VqipNKbkpRkjLLW -=3IaF ------END PGP SIGNATURE----- - ---===============7598747164910592838==-- - ---8F60183010.1466081614/dev1.dev.pixelated-project.org-- diff --git a/mail/src/leap/mail/tests/rfc822.message b/mail/src/leap/mail/tests/rfc822.message deleted file mode 100644 index ee97ab9..0000000 --- a/mail/src/leap/mail/tests/rfc822.message +++ /dev/null @@ -1,86 +0,0 @@ -Return-Path: <twisted-commits-admin@twistedmatrix.com> -Delivered-To: exarkun@meson.dyndns.org -Received: from localhost [127.0.0.1] - by localhost with POP3 (fetchmail-6.2.1) - for exarkun@localhost (single-drop); Thu, 20 Mar 2003 14:50:20 -0500 (EST) -Received: from pyramid.twistedmatrix.com (adsl-64-123-27-105.dsl.austtx.swbell.net [64.123.27.105]) - by intarweb.us (Postfix) with ESMTP id 4A4A513EA4 - for <exarkun@meson.dyndns.org>; Thu, 20 Mar 2003 14:49:27 -0500 (EST) -Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com) - by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian)) - id 18w648-0007Vl-00; Thu, 20 Mar 2003 13:51:04 -0600 -Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian)) - id 18w63j-0007VK-00 - for <twisted-commits@twistedmatrix.com>; Thu, 20 Mar 2003 13:50:39 -0600 -To: twisted-commits@twistedmatrix.com -From: etrepum CVS <etrepum@twistedmatrix.com> -Reply-To: twisted-python@twistedmatrix.com -X-Mailer: CVSToys -Message-Id: <E18w63j-0007VK-00@pyramid.twistedmatrix.com> -Subject: [Twisted-commits] rebuild now works on python versions from 2.2.0 and up. -Sender: twisted-commits-admin@twistedmatrix.com -Errors-To: twisted-commits-admin@twistedmatrix.com -X-BeenThere: twisted-commits@twistedmatrix.com -X-Mailman-Version: 2.0.11 -Precedence: bulk -List-Help: <mailto:twisted-commits-request@twistedmatrix.com?subject=help> -List-Post: <mailto:twisted-commits@twistedmatrix.com> -List-Subscribe: <http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits>, - <mailto:twisted-commits-request@twistedmatrix.com?subject=subscribe> -List-Id: <twisted-commits.twistedmatrix.com> -List-Unsubscribe: <http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits>, - <mailto:twisted-commits-request@twistedmatrix.com?subject=unsubscribe> -List-Archive: <http://twistedmatrix.com/pipermail/twisted-commits/> -Date: Thu, 20 Mar 2003 13:50:39 -0600 - -Modified files: -Twisted/twisted/python/rebuild.py 1.19 1.20 - -Log message: -rebuild now works on python versions from 2.2.0 and up. - - -ViewCVS links: -http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/twisted/python/rebuild.py.diff?r1=text&tr1=1.19&r2=text&tr2=1.20&cvsroot=Twisted - -Index: Twisted/twisted/python/rebuild.py -diff -u Twisted/twisted/python/rebuild.py:1.19 Twisted/twisted/python/rebuild.py:1.20 ---- Twisted/twisted/python/rebuild.py:1.19 Fri Jan 17 13:50:49 2003 -+++ Twisted/twisted/python/rebuild.py Thu Mar 20 11:50:08 2003 -@@ -206,15 +206,27 @@ - clazz.__dict__.clear() - clazz.__getattr__ = __getattr__ - clazz.__module__ = module.__name__ -+ if newclasses: -+ import gc -+ if (2, 2, 0) <= sys.version_info[:3] < (2, 2, 2): -+ hasBrokenRebuild = 1 -+ gc_objects = gc.get_objects() -+ else: -+ hasBrokenRebuild = 0 - for nclass in newclasses: - ga = getattr(module, nclass.__name__) - if ga is nclass: - log.msg("WARNING: new-class %s not replaced by reload!" % reflect.qual(nclass)) - else: -- import gc -- for r in gc.get_referrers(nclass): -- if isinstance(r, nclass): -+ if hasBrokenRebuild: -+ for r in gc_objects: -+ if not getattr(r, '__class__', None) is nclass: -+ continue - r.__class__ = ga -+ else: -+ for r in gc.get_referrers(nclass): -+ if getattr(r, '__class__', None) is nclass: -+ r.__class__ = ga - if doLog: - log.msg('') - log.msg(' (fixing %s): ' % str(module.__name__)) - - -_______________________________________________ -Twisted-commits mailing list -Twisted-commits@twistedmatrix.com -http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits diff --git a/mail/src/leap/mail/tests/rfc822.multi-minimal.message b/mail/src/leap/mail/tests/rfc822.multi-minimal.message deleted file mode 100644 index 582297c..0000000 --- a/mail/src/leap/mail/tests/rfc822.multi-minimal.message +++ /dev/null @@ -1,16 +0,0 @@ -Content-Type: multipart/mixed; boundary="===============6203542367371144092==" -MIME-Version: 1.0 -Subject: [TEST] 010 - Inceptos cum lorem risus congue -From: testmailbitmaskspam@gmail.com -To: test_c5@dev.bitmask.net - ---===============6203542367371144092== -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -Howdy from python! -The subject: [TEST] 010 - Inceptos cum lorem risus congue -Current date & time: Wed Jan 8 16:36:21 2014 -Trying to attach: [] ---===============6203542367371144092==-- diff --git a/mail/src/leap/mail/tests/rfc822.multi-nested.message b/mail/src/leap/mail/tests/rfc822.multi-nested.message deleted file mode 100644 index 694bef5..0000000 --- a/mail/src/leap/mail/tests/rfc822.multi-nested.message +++ /dev/null @@ -1,619 +0,0 @@ -From: TEST <test.bitmask@example.com> -Content-Type: multipart/alternative; - boundary="Apple-Mail=_F4EF9C8E-2E66-4FC6-8840-F435ADBED5C8" -X-Smtp-Server: smtp.example.com:test.bitmask -Subject: test simple attachment -X-Universally-Unique-Identifier: 0ea1b4b2-cdb8-43c3-b54c-dc88a19c6e0a -Date: Wed, 8 Jul 2015 04:25:56 +0900 -Message-Id: <47278179-628A-43F5-95C9-BC7E1753C521@example.com> -To: test_alpha14_001@dev.bitmask.net -Mime-Version: 1.0 (Apple Message framework v1251.1) - - ---Apple-Mail=_F4EF9C8E-2E66-4FC6-8840-F435ADBED5C8 -Content-Transfer-Encoding: 7bit -Content-Type: text/plain; - charset=us-ascii - -this is a simple attachment ---Apple-Mail=_F4EF9C8E-2E66-4FC6-8840-F435ADBED5C8 -Content-Type: multipart/related; - type="text/html"; - boundary="Apple-Mail=_C7D5288F-B043-4A7F-AF3F-1EDF1A78438B" - - ---Apple-Mail=_C7D5288F-B043-4A7F-AF3F-1EDF1A78438B -Content-Transfer-Encoding: 7bit -Content-Type: text/html; - charset=us-ascii - -<html><head></head><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; ">this is a simple attachment<img height="286" width="300" apple-width="yes" apple-height="yes" id="fd3d0c89-709d-419f-b293-a6827f75c8d4" src="cid:163B7957-4342-485F-8FD6-D46A4A53A2C1"></body></html> ---Apple-Mail=_C7D5288F-B043-4A7F-AF3F-1EDF1A78438B -Content-Transfer-Encoding: base64 -Content-Disposition: inline; - filename="saing_ergol.jpg" -Content-Type: image/jpg; - x-mac-hide-extension=yes; - x-unix-mode=0600; - name="saint_ergol.jpg" -Content-Id: <163B7957-4342-485F-8FD6-D46A4A53A2C1> - -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 -64D/2Q== - ---Apple-Mail=_C7D5288F-B043-4A7F-AF3F-1EDF1A78438B-- - ---Apple-Mail=_F4EF9C8E-2E66-4FC6-8840-F435ADBED5C8-- diff --git a/mail/src/leap/mail/tests/rfc822.multi-signed.message b/mail/src/leap/mail/tests/rfc822.multi-signed.message deleted file mode 100644 index 9907c2d..0000000 --- a/mail/src/leap/mail/tests/rfc822.multi-signed.message +++ /dev/null @@ -1,238 +0,0 @@ -Date: Mon, 6 Jan 2014 04:40:47 -0400 -From: Kali Kaneko <kali@leap.se> -To: penguin@example.com -Subject: signed message -Message-ID: <20140106084047.GA21317@samsara.lan> -MIME-Version: 1.0 -Content-Type: multipart/signed; micalg=pgp-sha1; - protocol="application/pgp-signature"; boundary="z9ECzHErBrwFF8sy" -Content-Disposition: inline -User-Agent: Mutt/1.5.21 (2012-12-30) - - ---z9ECzHErBrwFF8sy -Content-Type: multipart/mixed; boundary="z0eOaCaDLjvTGF2l" -Content-Disposition: inline - - ---z0eOaCaDLjvTGF2l -Content-Type: text/plain; charset=utf-8 -Content-Disposition: inline -Content-Transfer-Encoding: quoted-printable - -This is an example of a signed message, -with attachments. - - ---=20 -Nihil sine chao! =E2=88=B4 - ---z0eOaCaDLjvTGF2l -Content-Type: text/plain; charset=us-ascii -Content-Disposition: attachment; filename="attach.txt" - -this is attachment in plain text. - ---z0eOaCaDLjvTGF2l -Content-Type: application/octet-stream -Content-Disposition: attachment; filename="hack.ico" -Content-Transfer-Encoding: base64 - -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 -AAAAEgAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8Af39/AL+/vwA/Pz8A39/fAF9fXwCfn58A -Hx8fAO/v7wAvLy8Ab29vAI+PjwAPDw8A0NDQALCwsABQUFAA9/f3ABcXFwDn5+cAJycnAMjI -yABHR0cAqKioAGdnZwCXl5cAd3d3AIeHhwAHBwcA2NjYALi4uABXV1cANTU1ADo6OgD7+/sA -CwsLAPPz8wATExMA6+vrABsbGwDj4+MAIyMjANTU1AArKysAzMzMAMTExABLS0sAtLS0AKys -rABbW1sApKSkAGNjYwCbm5sAa2trAJOTkwBzc3MAi4uLAHt7ewCDg4MAAwMDANzc3AAyMjIA -vLy8AFNTUwD9/f0ABQUFAPn5+QAJCQkADQ0NAPHx8QDt7e0AFRUVAOnp6QAZGRkA5eXlAB0d -HQDh4eEAISEhACUlJQDa2toAKSkpANbW1gDS0tIAysrKADw8PADGxsYAwsLCAEVFRQBJSUkA -urq6ALa2tgCysrIArq6uAFlZWQCqqqoAXV1dAKampgBlZWUAoqKiAJ2dnQBtbW0AmZmZAHFx -cQCVlZUAeXl5AH19fQCJiYkAhYWFAAEBAQACAgIABAQEAP7+/gAGBgYA/Pz8AAgICAD6+voA -CgoKAPj4+AAMDAwA9vb2APT09AASEhIA8vLyABQUFADu7u4AFhYWAOzs7AAYGBgA6urqAOjo -6AAeHh4AICAgAOTk5AAiIiIA4uLiACQkJADg4OAAJiYmAN7e3gDd3d0AKCgoANvb2wAqKioA -2dnZACwsLADX19cALi4uANXV1QAxMTEA09PTADMzMwDR0dEANDQ0AM3NzQA5OTkAy8vLADs7 -OwDJyckAPT09AMfHxwBAQEAAxcXFAMPDwwDBwcEAwMDAAL6+vgBKSkoAvb29ALu7uwC5ubkA -UVFRALe3twBSUlIAtbW1AFRUVACzs7MAVlZWAFhYWABaWloAra2tAFxcXACrq6sAXl5eAKmp -qQCnp6cAZGRkAKOjowChoaEAaGhoAKCgoACenp4AnJycAG5ubgCampoAcHBwAJiYmABycnIA -lpaWAJSUlAB2dnYAkpKSAHh4eACQkJAAenp6AI6OjgB8fHwAjIyMAIiIiACCgoIAhISEAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAC1WlpaWlpaWlpaWlpaWlpaWlpaHjAHr6taWlpaWlpaWlpa -WlpaWlpaq68HMB5aWlpap6KlWzBaA6KoWlpaWlq1WgEBAQEBAQEBAQEBAQEBAQEBAQGXNkUB -AQEBAQEBAQEBAQEBAQEBAQFFNpcBAQEBASg4EI6HPa5lfgEBAQEBWloBAQEBAQEBAQEBAQEB -AQEBAQEBlzZFAQEBAQEBAQEBAQEBAQEBAQEBRTaXAQEBETpEAAAAAAAAAH/FbwEBAVpaAQEB -AQEBAQEBAQEBAQEBAQEBAZc2RQEBAQEBAQEBAQEBAQEBAQEBAUU2lwEBhFQAAAAAAAAAAAAA -ALJCAQFaWgEBAQEBAQEBAQEBAQEBAQEBAQGXNkUBAQEBAQEBAQEBAQEBAQEBAQFFNpcBeJoA -AAAAAAAAAAAAAAAAMQEBWloBAQEBAQEBAQEBAQEBAQEBAQEBlzZFAQEBAQEBAQEBAQEBAQEB -AQEBRTZSATUAAAAAAAAAAAAAAAAAAABnAVpaAQEBAQEBAQEBAQEBAQEBAQEBAZc2RQEBAQEB -AQEBAQEBAQEBAQEBAUU2Tx1wAAAAAAAAAAAAAAAAAAAAgkaoWgEBAQEBAQEBAQEBAQEBAQEB -AQGXNkUBAQEBAQEBAQEBAQEBAQEBAQFFNgVrAAAAAAAAAAAAAAAAAAAAAABioloBAQEBAQEB -AQEBAQEBAQEBAQEBlzZFAQEBAQEBAQEBAQEBAQEBAQEBRWcqngAAAAAAAAAAAAAAAAAAAAAA -tANaAQEBAQEBAQEBAQEBAQEBAQEBAZc2RQEBAQEBAQEBAQEBAQEBAQEBAUXDpIcAAAAAAAAA -AAAAAAAAAAAAAJRaWgEBAQEBAQEBAQEBAQEBAQEBAQGXNkUBAQEBAQEBAQEBAQEBAQEBAQFF -wa9HAAAAAAAAAAAAAAAAAAAAAABOMFoBAQEBAQEBAQEBAQEBAQEBAQEBlzZFAQEBAQEBAQEB -AQEBAQEBAQEBRWVZggAAAAAAAAAAAAAAAAAAAAAAjltaAQEBAQEBAQEBAQEBAQEBAQEBAZc2 -RQEBAQEBAQEBAQEBAQEBAQEBAUXFmZYAAAAAAAAAAAAAAAAAAAAAAKqlWgEBAQEBAQEBAQEB -AQEBAQEBAQGXNkUBAQEBAQEBAQEBAQEBAQEBAQFFNorHAAAAAAAAAAAAAAAAAAAAAABloloB -AQEBAQEBAQEBAQEBAQEBAQEBlzZFAQEBAQEBAQEBAQEBAQEBAQEBRTY8UwAAAAAAAAAAAAAA -AAAAAAASEz5aAQEBAQEBAQEBAQEBAQEBAQEBAZc2RQEBAQEBAQEBAQEBAQEBAQEBAUU2lQFd -AAAAAAAAAAAAAAAAAAAA0AFaWgEBAQEBAQEBAQEBAQEBAQEBAQGXNkUBAQEBAQEBAQEBAQEB -AQEBAQFFNpcBhoUAAAAAAAAAAAAAAAAAVxEBWloBAQEBAQEBAQEBAQEBAQEBAQEBlzZFAQEB -AQEBAQEBAQEBAQEBAQEBRTaXAQGXTQAAAAAAAAAAAAAAnCgBAVpaAQEBAQEBAQEBAQEBAQEB -AQEBAZc2RQEBAQEBAQEBAQEBAQEBAQEBAUU2lwEBASiwAAAAAAAAAAAcwncBAQFaWgEBAQEB -AQEBAQEBAQEBAQEBAQGXNkUBAQEBAQEBAQEBAQEBAQEBAQFFNpcBAQEBASy8khINgiFojQEB -AQEBWjCVl5eXl5eXl5dSUpeXl5eXl5eTHsWdlZeXl5eXl5eXl5eXl5eXl5eVncUek5eXl1I8 -ipsvs6iVBU9Sl5eXlTAHNjY2NjY2Zb1ivbtiY2c2NjY2NsVlxjY2NjY2NjY2NjY2NjY2NjY2 -NsZlxTY2NjY2xr8yFxcXusHGNjY2NjYHW3hFRUURAY8HC7Jh0ahFb3pFRRGdxkp4RUVFRUVF -RUVFRUVFRUVFRXhKxp0RRUVFIkKhDLkxwMiXInNFRUV4W1oBAQEBCcclAAAAAAAAnK0BAQEB -lzZFAQEBAQEBAQEBAQEBAQEBAQEBRTaXAQEBAQ4ucAAAAAAAdAaNAQEBAVpaAQEBpYMAAAAA -AAAAAAAAGHUBAZc2RQEBAQEBAQEBAQEBAQEBAQEBAUU2lwEBAWtwAAAAAAAAAAAADboBAQFa -WgEBHnIAAAAAAAAAAAAAAACxcwGXNkUBAQEBAQEBAQEBAQEBAQEBAQFFNpcBAcQAAAAAAAAA -AAAAAABtwQEBWloBiCcAAAAAAAAAAAAAAAAAAM0BUjZFAQEBAQEBAQEBAQEBAQEBAQEBRTaX -AbsAAAAAAAAAAAAAAAAAAHCiAVpaAQYAAAAAAAAAAAAAAAAAAAAck082RQEBAQEBAQEBAQEB -AQEBAQEBAUU2UUVLAAAAAAAAAAAAAAAAAAAAIQEePkoNAAAAAAAAAAAAAAAAAAAAAMCLxkUB -AQEBAQEBAQEBAQEBAQEBAQFFNgViAAAAAAAAAAAAAAAAAAAAAACppKK9AAAAAAAAAAAAAAAA -AAAAAACQnxlFAQEBAQEBAQEBAQEBAQEBAQEBRcZPrAAAAAAAAAAAAAAAAAAAAAAAZqOjCwAA -AAAAAAAAAAAAAAAAAAAAQ7i/RQEBAQEBAQEBAQEBAQEBAQEBAUUZVSsAAAAAAAAAAAAAAAAA -AAAAAFRZpT8AAAAAAAAAAAAAAAAAAAAAAADKvkUBAQEBAQEBAQEBAQEBAQEBAQFFZVpJAAAA -AAAAAAAAAAAAAAAAAAAUXKU/AAAAAAAAAAAAAAAAAAAAAAAAyr5FAQEBAQEBAQEBAQEBAQEB -AQEBRWVaSQAAAAAAAAAAAAAAAAAAAAAAFFyjCwAAAAAAAAAAAAAAAAAAAAAAdl40RQEBAQEB -AQEBAQEBAQEBAQEBAUUZVSsAAAAAAAAAAAAAAAAAAAAAAKCoVrcAAAAAAAAAAAAAAAAAAAAA -ACCZxUUBAQEBAQEBAQEBAQEBAQEBAQFFxo1fAAAAAAAAAAAAAAAAAAAAAABpVqh+fQAAAAAA -AAAAAAAAAAAAAADRijZFAQEBAQEBAQEBAQEBAQEBAQEBRTaKXAAAAAAAAAAAAAAAAAAAAAA7 -LANaAWgAAAAAAAAAAAAAAAAAAABJSJE2RQEBAQEBAQEBAQEBAQEBAQEBAUU2KgEKAAAAAAAA -AAAAAAAAAAAAHwGrWgF8kAAAAAAAAAAAAAAAAAAAZQGXNkUBAQEBAQEBAQEBAQEBAQEBAQFF -NpcBHm0AAAAAAAAAAAAAAAAAEk8BWloBAZVLAAAAAAAAAAAAAAAANwEBlzZFAQEBAQEBAQEB -AQEBAQEBAQEBRTaXAQHFAAAAAAAAAAAAAAAAQx4BAVpaAQEBj1QAAAAAAAAAAAByGQEBAZc2 -RQEBAQEBAQEBAQEBAQEBAQEBAUU2lwEBARcSAAAAAAAAAAAAjJkBAQFaWgEBAQFxuphuAAAA -ABK8jwEBAQGXNkUBAQEBAQEBAQEBAQEBAQEBAQFFNpcBAQEBSMlLAAAAAG0rDEUBAQEBWlt4 -RUVFeAFFLWU6DC8FcXNFRUURncZKeEVFRUVFRUVFRUVFRUVFRUV4SsadEUVFRXUBhC8MOmWi -JgF3RUVFeFsHNjY2NjY2Z7+9Yru+wzY2NjY2NsVlxjY2NjY2NsU0vr6/wzY2NjY2NsZlxTY2 -NjY2NmUytbO3Yhk2NjY2NjYHMJWXl5eXl5eXl5eXl5eXl5eXl5MexZ2Vl5eXHQWdXgwMYKKK -T5eXl5WdxR6Tl5eXKgWVrWfOvquPipWXl5eVMFoBAQEBAQEBAQEBAQEBAQEBAQEBlzZFAQEB -AYE5kHYAAEMpvJEBAQEBRTaXAQEBAXFiBEcAAG4Spi8BAQEBAVpaAQEBAQEBAQEBAQEBAQEB -AQEBAZc2RQEBAcF7AAAAAAAAAABBaUIBAUU2lwEBAZsgAAAAAAAAAAAAFooBAQFaWgEBAQEB -AQEBAQEBAQEBAQEBAQGXNkUBAQsAAAAAAAAAAAAAAACxcwFFNpcBAQ92AAAAAAAAAAAAAABN -UQEBWloBAQEBAQEBAQEBAQEBAQEBAQEBlzZFAcwAAAAAAAAAAAAAAAAAABgBejaXAZd5AAAA -AAAAAAAAAAAAAImAAVpaAQEBAQEBAQEBAQEBAQEBAQEBAZc2c1JDAAAAAAAAAAAAAAAAAAAA -W3E2KgGeAAAAAAAAAAAAAAAAAAAAMwGrWgEBAQEBAQEBAQEBAQEBAQEBAQGXNm9kAAAAAAAA -AAAAAAAAAAAAAAQJZ4ukAAAAAAAAAAAAAAAAAAAAAHKVpVoBAQEBAQEBAQEBAQEBAQEBAQEB -l8OGKQAAAAAAAAAAAAAAAAAAAAAcor+LNQAAAAAAAAAAAAAAAAAAAAAAaqJaAQEBAQEBAQEB -AQEBAQEBAQEBAZdjHmwAAAAAAAAAAAAAAAAAAAAAAM8ymT0AAAAAAAAAAAAAAAAAAAAAAFg+ -WgEBAQEBAQEBAQEBAQEBAQEBAQGXvWUAAAAAAAAAAAAAAAAAAAAAAABhuFmCAAAAAAAAAAAA -AAAAAAAAAACOW1oBAQEBAQEBAQEBAQEBAQEBAQEBl7vOAAAAAAAAAAAAAAAAAAAAAAAAtGCv -RwAAAAAAAAAAAAAAAAAAAAAATjBaAQEBAQEBAQEBAQEBAQEBAQEBAZcHYgAAAAAAAAAAAAAA -AAAAAAAAAAu4pIcAAAAAAAAAAAAAAAAAAAAAAD1aWgEBAQEBAQEBAQEBAQEBAQEBAQGXNBUj -AAAAAAAAAAAAAAAAAAAAAAAyvSpXAAAAAAAAAAAAAAAAAAAAAAAYpFoBAQEBAQEBAQEBAQEB -AQEBAQEBl2ckVAAAAAAAAAAAAAAAAAAAAACDiMMFzAAAAAAAAAAAAAAAAAAAAAAAr6NaAQEB -AQEBAQEBAQEBAQEBAQEBAZc2b7sAAAAAAAAAAAAAAAAAAAAAaW82HRMlAAAAAAAAAAAAAAAA -AAAAlECpWgEBAQEBAQEBAQEBAQEBAQEBAQGXNngBBAAAAAAAAAAAAAAAAAAAKUZ3NpcBzwAA -AAAAAAAAAAAAAAAAAA8BWloBAQEBAQEBAQEBAQEBAQEBAQEBlzZFAZGCAAAAAAAAAAAAAAAA -dC0BRTaXAXGwAAAAAAAAAAAAAAAAAAIBAVpaAQEBAQEBAQEBAQEBAQEBAQEBAZc2RQEBlY4A -AAAAAAAAAAAACD4BAUU2lwEBd7YAAAAAAAAAAAAAbmtvAQFaWgEBAQEBAQEBAQEBAQEBAQEB -AQGXNkUBAQEJyw0AAAAAAAB0M0wBAQFFNpcBAQEBF1AAAAAAAAAAVD4BAQEBWloBAQEBAQEB -AQEBAQEBAQEBAQEBlzZFAQEBAQETB7ymprxliwEBAQEBRTaXAQEBAQF1qxqsV7QbVXEBAQEB -AVq1WlpaWlpaWlpaWlpaWlpaWlpaHjAHr6taWlpaPqKkPj6kLadaWlpaq68HMB5aWlpaqaNW -pz4DLaQeWlpaWlq1AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= - ---z0eOaCaDLjvTGF2l-- - ---z9ECzHErBrwFF8sy -Content-Type: application/pgp-signature - ------BEGIN PGP SIGNATURE----- -Version: GnuPG v1.4.15 (GNU/Linux) - -iQIcBAEBAgAGBQJSymwPAAoJECNji/csWTvBhtcP/2AKF0uk6ljrfMWhNBSFwDqv -kYng3slREnF/pxnIGOpR2GAxPBPjRipZOuUU8QL+pXBwk5kWzb9RYpr26xMYWRtl -vXdVbob5NolNEYrqTkkQ1kejERQGFyescsUJDcEDXJl024czKWbxHTYYN4vlYJMK -PZ5mPSdADFn970PnVXfNix3Rjvv7SFQGammDBGjQzyROkoiDKPZcomp6dzm6zEXC -w8i42WfHU8GkyVVNvXZI52Xw3LUXiXsJ58B1V1O5U42facepG6S+S0DC/PWptqPw -sAM9/YGkvBNWrsJA/BavXPRLE1gVpu+hZZEsOqRvs244k7JTrVo54xDbdeOT2nTr -BDk4e88vmCVKGgE9MZjDbjgOHDZhmsxNQm4DBGRH2huF0noUc/8Sm4KhSO49S2mN -QjIT5QrPerQNiP5QtShHZRJX7ElXYZWX1SG/c9jQjfd0W1XK/cGtwClICe+lpprt -mLC2607yalbRhCxV9bQlVUnd2tY3NY4UgIKgCEiEwb1hf/k9jQDvpk16VuNWSZQJ -jFeg9F2WdNjQMp79cyvnayyhjS9o/K2LbSIgJi7KdlQcVZ/2DQfbMjCwByR7P9g8 -gcAKh8V7E6IpAu1mnvs4FDagipppK6hOTRj2s/I3xZzneprSK1WaVro/8LAWZe9X -sSdfcAhT7Tno7PB/Acoh -=+okv ------END PGP SIGNATURE----- - ---z9ECzHErBrwFF8sy-- diff --git a/mail/src/leap/mail/tests/rfc822.multi.message b/mail/src/leap/mail/tests/rfc822.multi.message deleted file mode 100644 index 30f74e5..0000000 --- a/mail/src/leap/mail/tests/rfc822.multi.message +++ /dev/null @@ -1,96 +0,0 @@ -Date: Fri, 19 May 2000 09:55:48 -0400 (EDT)
-From: Doug Sauder <doug@penguin.example.com>
-To: Joe Blow <blow@example.com>
-Subject: Test message from PINE
-Message-ID: <Pine.LNX.4.21.0005190951410.8452-102000@penguin.example.com>
-MIME-Version: 1.0
-Content-Type: MULTIPART/MIXED; BOUNDARY="-1463757054-952513540-958744548=:8452"
-
- This message is in MIME format. The first part should be readable text,
- while the remaining parts are likely unreadable without MIME-aware tools.
- Send mail to mime@docserver.cac.washington.edu for more info.
-
----1463757054-952513540-958744548=:8452
-Content-Type: TEXT/PLAIN; charset=US-ASCII
-
-This is a test message from PINE MUA.
-
-
----1463757054-952513540-958744548=:8452
-Content-Type: APPLICATION/octet-stream; name="redball.png"
-Content-Transfer-Encoding: BASE64
-Content-ID: <Pine.LNX.4.21.0005190955480.8452@penguin.example.com>
-Content-Description: A PNG graphic file
-Content-Disposition: attachment; filename="redball.png"
-
-iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8A
-AAABAAALAAAVAAAaAAAXAAARAAAKAAADAAAcAAAyAABEAABNAABIAAA9AAAj
-AAAWAAAmAABhAAB7AACGAACHAAB9AAB0AABgAAA5AAAUAAAGAAAnAABLAABv
-AACQAAClAAC7AAC/AACrAAChAACMAABzAABbAAAuAAAIAABMAAB3AACZAAC0
-GRnKODjVPT3bKSndBQW4AACoAAB5AAAxAAAYAAAEAABFAACaAAC7JCTRYWHf
-hITmf3/mVlbqHx/SAAC5AACjAABdAABCAAAoAAAJAABnAAC6Dw/QVFTek5Pl
-rKzpmZntZWXvJSXXAADBAACxAACcAABtAABTAAA2AAAbAAAFAABKAACBAADL
-ICDdZ2fonJzrpqbtiorvUVHvFBTRAADDAAC2AAB4AABeAABAAAAiAABXAACS
-AADCAADaGxvoVVXseHjveHjvV1fvJibhAADOAAC3AACnAACVAABHAAArAAAP
-AACdAADFAADhBQXrKCjvPDzvNTXvGxvjAADQAADJAAC1AACXAACEAABsAABP
-AAASAAACAABiAADpAADvAgLnAADYAADLAAC6AACwAABwAAATAAAkAABYAADI
-AADTAADNAACzAACDAABuAAAeAAB+AADAAACkAACNAAB/AABpAABQAAAwAACR
-AACpAAC8AACqAACbAABlAABJAAAqAAAOAAA0AACsAACvAACtAACmAACJAAB6
-AABrAABaAAA+AAApAABqAACCAACfAACeAACWAACPAAB8AAAZAAAHAABVAACO
-AACKAAA4AAAQAAA/AAByAACAAABcAAA3AAAsAABmAABDAABWAAAgAAAzAAA8
-AAA6AAAfAAAMAAAdAAANAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD8
-LtlFAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu
-MT1evmgAAAIISURBVHicY2CAg/8QwIABmJhZWFnZ2Dk4MaU5uLh5eHn5+LkF
-BDlQJf8zC/EIi4iKiUtI8koJScsgyf5nlpWTV1BUUlZRVVPX4NFk1UJIyghp
-6+jq6RsYGhmbKJgK85mZW8Dk/rNaSlhZ29ja2Ts4Ojkr6Li4urFDNf53N/Ow
-8vTy9vH18w8IDAoWDQkNC4+ASP5ni4wKio6JjYtPSExKTnFWSE1LF4A69n9G
-ZlZ2Tm5efkFhUXFySWlZlEd5RSVY7j+TkGRVdU1tXX1DY1Ozcktpa1t7h2Yn
-OAj+d7l1tyo79vT29SdNSJ44SbFVdHIo9xSIHNPUaWqTpifNSJrZnK00S0U1
-a/acUG5piNz/uXLzVJ2qm6dXz584S2WB1cJFi5cshZr539xVftnyFKUVTi2T
-VjqvyhJLXb1m7TqoHPt6F/HW0g0bN63crGqVtWXrtu07BJihcsw71+zanRW8
-Z89eq337RQ/Ip60xO3gIElX/LbikDm8T36KwbNmRo7O3zpHkPSZwHBqL//8f
-lz1x2OOkyKJTi7aqbzutfUZI2gIuF8F2lr/D5dw2+fZdwpl8YVOlI+CJ4/9/
-joOyYed5QzMvhGqnm2V0WiClm///D0lfXHtJ6vLlK9w7rx7vQk5SQJbFtSms
-1y9evXid7QZacgOxmSxktNzdtSwwU+J/VICaCPFIYU3XAJhIOtjf5sfyAAAA
-JXRFWHRDb21tZW50AGNsaXAyZ2lmIHYuMC42IGJ5IFl2ZXMgUGlndWV0NnM7
-vAAAAABJRU5ErkJggg==
----1463757054-952513540-958744548=:8452
-Content-Type: APPLICATION/octet-stream; name="blueball.png"
-Content-Transfer-Encoding: BASE64
-Content-ID: <Pine.LNX.4.21.0005190955481.8452@penguin.example.com>
-Content-Description: A PNG graphic file
-Content-Disposition: attachment; filename="blueball.png"
-
-iVBORw0KGgoAAAANSUhEUgAAABsAAAAbCAMAAAC6CgRnAAADAFBMVEX///8A
-AAgAABAAABgAAAAACCkAEEIAEEoACDEAEFIIIXMIKXsIKYQIIWsAGFoACDkI
-IWMQOZwYQqUYQq0YQrUQOaUQMZQAGFIQMYwpUrU5Y8Y5Y84pWs4YSs4YQs4Y
-Qr1Ca8Z7nNacvd6Mtd5jlOcxa94hUt4YStYYQsYQMaUAACHO5+/n7++cxu9S
-hO8pWucQOa1Ke86tzt6lzu9ajO8QMZxahNat1ufO7++Mve9Ke+8YOaUYSsaM
-vee15++Uve8AAClajOdzpe9rnO8IKYwxY+8pWu8IIXsAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADB
-Mg1VAAAAAXRSTlMAQObYZgAAABZ0RVh0U29mdHdhcmUAZ2lmMnBuZyAyLjAu
-MT1evmgAAAGISURBVHicddJtV5swGAbgEk6AJhBSk4bMCUynBSLaqovbrG/b
-fPn/vyh70lbsscebL5xznTsh5BmNhgQoRChwo50EOIohUYLDj4zHhKYQkrEo
-Qdvock4ne0IKMVUpKZLQDeqSTIsv+18PyqqWUw2IBsRM7307PPp+fDJrWtnp
-LDJvewYxnewfnvanZ+fzpmwXijC8KbqEa3Fx2ff91Y95U9XCUpaDeQwiMpHX
-P/v+1++bWVPWQoGFawtjury9vru/f/C1Vi7ezT0WWpQHf/7+u/G71aLThK/M
-jRxmT6KdzZ9fGk9yatMsTgZLl3XVgFRAC6spj/13enssqJVtWVa3NdBSacL8
-+VZmYqKmdd1CSYoOiMOSGwtzlqqlFFIuOqv0a1ZEZrUkWICLLFW266y1KvWE
-1zV/iDAH1EopnVLCiygZCIomH3NCKX0lnI+B1iuuzCGTxwXjnDO4d7NpbX42
-YJJHkBwmAm2TxwAZg40J3+Xtbv1rgOAZwG0NxW62p+lT+Yi747sD/wEUVMzY
-mWkOvwAAACV0RVh0Q29tbWVudABjbGlwMmdpZiB2LjAuNiBieSBZdmVzIFBp
-Z3VldDZzO7wAAAAASUVORK5CYII=
----1463757054-952513540-958744548=:8452--
diff --git a/mail/src/leap/mail/tests/rfc822.plain.message b/mail/src/leap/mail/tests/rfc822.plain.message deleted file mode 100644 index fc627c3..0000000 --- a/mail/src/leap/mail/tests/rfc822.plain.message +++ /dev/null @@ -1,66 +0,0 @@ -From pyar-bounces@python.org.ar Wed Jan 8 14:46:02 2014 -Return-Path: <pyar-bounces@python.org.ar> -X-Spam-Checker-Version: SpamAssassin 3.3.2 (2011-06-06) on spamd2.riseup.net -X-Spam-Level: ** -X-Spam-Pyzor: Reported 0 times. -X-Spam-Status: No, score=2.1 required=8.0 tests=AM_TRUNCATED,CK_419SIZE, - CK_NAIVER_NO_DNS,CK_NAIVE_NO_DNS,ENV_FROM_DIFF0,HAS_REPLY_TO,LINK_NR_TOP, - NO_REAL_NAME,RDNS_NONE,RISEUP_SPEAR_C shortcircuit=no autolearn=disabled - version=3.3.2 -Delivered-To: kali@leap.se -Received: from mx1.riseup.net (mx1-pn.riseup.net [10.0.1.33]) - (using TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits)) - (Client CN "*.riseup.net", Issuer "Gandi Standard SSL CA" (not verified)) - by vireo.riseup.net (Postfix) with ESMTPS id 6C39A8F - for <kali@leap.se>; Wed, 8 Jan 2014 18:46:02 +0000 (UTC) -Received: from pyar.usla.org.ar (unknown [190.228.30.157]) - by mx1.riseup.net (Postfix) with ESMTP id F244C533F4 - for <kali@leap.se>; Wed, 8 Jan 2014 10:46:01 -0800 (PST) -Received: from [127.0.0.1] (localhost [127.0.0.1]) - by pyar.usla.org.ar (Postfix) with ESMTP id CC51D26A4F - for <kali@leap.se>; Wed, 8 Jan 2014 15:46:00 -0300 (ART) -MIME-Version: 1.0 -Content-Type: text/plain; charset="iso-8859-1" -Content-Transfer-Encoding: quoted-printable -From: pyar-request@python.org.ar -To: kali@leap.se -Subject: confirm 0e47e4342e4d42508e8c283175b05b3377148ac2 -Reply-To: pyar-request@python.org.ar -Auto-Submitted: auto-replied -Message-ID: <mailman.245.1389206759.1579.pyar@python.org.ar> -Date: Wed, 08 Jan 2014 15:45:59 -0300 -Precedence: bulk -X-BeenThere: pyar@python.org.ar -X-Mailman-Version: 2.1.15 -List-Id: Python Argentina <pyar.python.org.ar> -X-List-Administrivia: yes -Errors-To: pyar-bounces@python.org.ar -Sender: "pyar" <pyar-bounces@python.org.ar> -X-Virus-Scanned: clamav-milter 0.97.8 at mx1 -X-Virus-Status: Clean - -Mailing list subscription confirmation notice for mailing list pyar - -We have received a request de kaliyuga@riseup.net for subscription of -your email address, "kaliyuga@riseup.net", to the pyar@python.org.ar -mailing list. To confirm that you want to be added to this mailing -list, simply reply to this message, keeping the Subject: header -intact. Or visit this web page: - - http://listas.python.org.ar/confirm/pyar/0e47e4342e4d42508e8c283175b05b= -3377148ac2 - - -Or include the following line -- and only the following line -- in a -message to pyar-request@python.org.ar: - - confirm 0e47e4342e4d42508e8c283175b05b3377148ac2 - -Note that simply sending a `reply' to this message should work from -most mail readers, since that usually leaves the Subject: line in the -right form (additional "Re:" text in the Subject: is okay). - -If you do not wish to be subscribed to this list, please simply -disregard this message. If you think you are being maliciously -subscribed to the list, or have any other questions, send them to -pyar-owner@python.org.ar. diff --git a/mail/src/leap/mail/tests/test_mail.py b/mail/src/leap/mail/tests/test_mail.py deleted file mode 100644 index f9cded2..0000000 --- a/mail/src/leap/mail/tests/test_mail.py +++ /dev/null @@ -1,399 +0,0 @@ -# -*- coding: utf-8 -*- -# test_mail.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 <http://www.gnu.org/licenses/>. -""" -Tests for the mail module. -""" -import os -import time -import uuid - -from functools import partial -from email.parser import Parser -from email.Utils import formatdate - -from leap.mail.adaptors.soledad import SoledadMailAdaptor -from leap.mail.mail import MessageCollection, Account, _unpack_headers -from leap.mail.mailbox_indexer import MailboxIndexer -from leap.mail.testing.common import SoledadTestMixin - -HERE = os.path.split(os.path.abspath(__file__))[0] - - -def _get_raw_msg(multi=False): - if multi: - sample = "rfc822.multi.message" - else: - sample = "rfc822.message" - with open(os.path.join(HERE, sample)) as f: - raw = f.read() - return raw - - -def _get_parsed_msg(multi=False): - mail_parser = Parser() - raw = _get_raw_msg(multi=multi) - return mail_parser.parsestr(raw) - - -def _get_msg_time(): - timestamp = time.mktime((2010, 12, 12, 1, 1, 1, 1, 1, 1)) - return formatdate(timestamp) - - -class CollectionMixin(object): - - def get_collection(self, mbox_collection=True, mbox_name=None, - mbox_uuid=None): - """ - Get a collection for tests. - """ - adaptor = SoledadMailAdaptor() - store = self._soledad - adaptor.store = store - - if mbox_collection: - mbox_indexer = MailboxIndexer(store) - mbox_name = mbox_name or "TestMbox" - mbox_uuid = mbox_uuid or str(uuid.uuid4()) - else: - mbox_indexer = mbox_name = None - - def get_collection_from_mbox_wrapper(wrapper): - wrapper.uuid = mbox_uuid - return MessageCollection( - adaptor, store, - mbox_indexer=mbox_indexer, mbox_wrapper=wrapper) - - d = adaptor.initialize_store(store) - if mbox_collection: - d.addCallback(lambda _: mbox_indexer.create_table(mbox_uuid)) - d.addCallback(lambda _: adaptor.get_or_create_mbox(store, mbox_name)) - d.addCallback(get_collection_from_mbox_wrapper) - return d - - -# TODO profile add_msg. Why are these tests so SLOW??! -class MessageTestCase(SoledadTestMixin, CollectionMixin): - """ - Tests for the Message class. - """ - msg_flags = ('\Recent', '\Unseen', '\TestFlag') - msg_tags = ('important', 'todo', 'wonderful') - internal_date = "19-Mar-2015 19:22:21 -0500" - - maxDiff = None - - def _do_insert_msg(self, multi=False): - """ - Inserts and return a regular message, for tests. - """ - def insert_message(collection): - self._mbox_uuid = collection.mbox_uuid - return collection.add_msg( - raw, flags=self.msg_flags, tags=self.msg_tags, - date=self.internal_date) - - raw = _get_raw_msg(multi=multi) - - d = self.get_collection() - d.addCallback(insert_message) - return d - - def get_inserted_msg(self, multi=False): - d = self._do_insert_msg(multi=multi) - d.addCallback(lambda _: self.get_collection(mbox_uuid=self._mbox_uuid)) - d.addCallback(lambda col: col.get_message_by_uid(1)) - return d - - def test_get_flags(self): - d = self.get_inserted_msg() - d.addCallback(self._test_get_flags_cb) - return d - - def _test_get_flags_cb(self, msg): - self.assertTrue(msg is not None) - self.assertEquals(tuple(msg.get_flags()), self.msg_flags) - - def test_get_internal_date(self): - d = self.get_inserted_msg() - d.addCallback(self._test_get_internal_date_cb) - - def _test_get_internal_date_cb(self, msg): - self.assertTrue(msg is not None) - self.assertDictEqual(msg.get_internal_date(), - self.internal_date) - - def test_get_headers(self): - d = self.get_inserted_msg() - d.addCallback(self._test_get_headers_cb) - return d - - def _test_get_headers_cb(self, msg): - self.assertTrue(msg is not None) - expected = [ - (str(key.lower()), str(value)) - for (key, value) in _get_parsed_msg().items()] - self.assertItemsEqual(_unpack_headers(msg.get_headers()), expected) - - def test_get_body_file(self): - d = self.get_inserted_msg(multi=True) - d.addCallback(self._test_get_body_file_cb) - return d - - def _test_get_body_file_cb(self, msg): - self.assertTrue(msg is not None) - orig = _get_parsed_msg(multi=True) - expected = orig.get_payload()[0].get_payload() - d = msg.get_body_file(self._soledad) - - def assert_body(fd): - self.assertTrue(fd is not None) - self.assertEqual(fd.read(), expected) - d.addCallback(assert_body) - return d - - def test_get_size(self): - d = self.get_inserted_msg() - d.addCallback(self._test_get_size_cb) - return d - - def _test_get_size_cb(self, msg): - self.assertTrue(msg is not None) - expected = len(_get_parsed_msg().as_string()) - self.assertEqual(msg.get_size(), expected) - - def test_is_multipart_no(self): - d = self.get_inserted_msg() - d.addCallback(self._test_is_multipart_no_cb) - return d - - def _test_is_multipart_no_cb(self, msg): - self.assertTrue(msg is not None) - expected = _get_parsed_msg().is_multipart() - self.assertEqual(msg.is_multipart(), expected) - - def test_is_multipart_yes(self): - d = self.get_inserted_msg(multi=True) - d.addCallback(self._test_is_multipart_yes_cb) - return d - - def _test_is_multipart_yes_cb(self, msg): - self.assertTrue(msg is not None) - expected = _get_parsed_msg(multi=True).is_multipart() - self.assertEqual(msg.is_multipart(), expected) - - def test_get_subpart(self): - d = self.get_inserted_msg(multi=True) - d.addCallback(self._test_get_subpart_cb) - return d - - def _test_get_subpart_cb(self, msg): - self.assertTrue(msg is not None) - - def test_get_tags(self): - d = self.get_inserted_msg() - d.addCallback(self._test_get_tags_cb) - return d - - def _test_get_tags_cb(self, msg): - self.assertTrue(msg is not None) - self.assertEquals(msg.get_tags(), self.msg_tags) - - -class MessageCollectionTestCase(SoledadTestMixin, CollectionMixin): - """ - Tests for the MessageCollection class. - """ - _mbox_uuid = None - - def assert_collection_count(self, _, expected): - def _assert_count(count): - self.assertEqual(count, expected) - - d = self.get_collection() - d.addCallback(lambda col: col.count()) - d.addCallback(_assert_count) - return d - - def add_msg_to_collection(self): - raw = _get_raw_msg() - - def add_msg_to_collection(collection): - # We keep the uuid in case we need to instantiate the same - # collection afterwards. - self._mbox_uuid = collection.mbox_uuid - d = collection.add_msg(raw, date=_get_msg_time()) - return d - - d = self.get_collection() - d.addCallback(add_msg_to_collection) - return d - - def test_is_mailbox_collection(self): - d = self.get_collection() - d.addCallback(self._test_is_mailbox_collection_cb) - return d - - def _test_is_mailbox_collection_cb(self, collection): - self.assertTrue(collection.is_mailbox_collection()) - - def test_get_uid_next(self): - d = self.add_msg_to_collection() - d.addCallback(lambda _: self.get_collection()) - d.addCallback(lambda col: col.get_uid_next()) - d.addCallback(self._test_get_uid_next_cb) - - def _test_get_uid_next_cb(self, next_uid): - self.assertEqual(next_uid, 2) - - def test_add_and_count_msg(self): - d = self.add_msg_to_collection() - d.addCallback(self._test_add_and_count_msg_cb) - return d - - def _test_add_and_count_msg_cb(self, _): - return partial(self.assert_collection_count, expected=1) - - def test_copy_msg(self): - # TODO ---- update when implementing messagecopier - # interface - pass - test_copy_msg.skip = "Not yet implemented" - - def test_delete_msg(self): - d = self.add_msg_to_collection() - - def del_msg(collection): - def _delete_it(msg): - self.assertTrue(msg is not None) - return collection.delete_msg(msg) - - d = collection.get_message_by_uid(1) - d.addCallback(_delete_it) - return d - - # We need to instantiate an mbox collection with the same uuid that - # the one in which we inserted the doc. - d.addCallback(lambda _: self.get_collection(mbox_uuid=self._mbox_uuid)) - d.addCallback(del_msg) - d.addCallback(self._test_delete_msg_cb) - return d - - def _test_delete_msg_cb(self, _): - return partial(self.assert_collection_count, expected=0) - - def test_update_flags(self): - d = self.add_msg_to_collection() - d.addCallback(self._test_update_flags_cb) - return d - - def _test_update_flags_cb(self, msg): - pass - - def test_update_tags(self): - d = self.add_msg_to_collection() - d.addCallback(self._test_update_tags_cb) - return d - - def _test_update_tags_cb(self, msg): - pass - - -class AccountTestCase(SoledadTestMixin): - """ - Tests for the Account class. - """ - def get_account(self, user_id): - store = self._soledad - return Account(store, user_id) - - def test_add_mailbox(self): - acc = self.get_account('some_user_id') - d = acc.callWhenReady(lambda _: acc.add_mailbox("TestMailbox")) - d.addCallback(lambda _: acc.list_all_mailbox_names()) - d.addCallback(self._test_add_mailbox_cb) - return d - - def _test_add_mailbox_cb(self, mboxes): - expected = ['INBOX', 'TestMailbox'] - self.assertItemsEqual(mboxes, expected) - - def test_delete_mailbox(self): - acc = self.get_account('some_user_id') - d = acc.callWhenReady(lambda _: acc.delete_mailbox("Inbox")) - d.addCallback(lambda _: acc.list_all_mailbox_names()) - d.addCallback(self._test_delete_mailbox_cb) - return d - - def _test_delete_mailbox_cb(self, mboxes): - expected = [] - self.assertItemsEqual(mboxes, expected) - - def test_rename_mailbox(self): - acc = self.get_account('some_user_id') - d = acc.callWhenReady(lambda _: acc.add_mailbox("OriginalMailbox")) - d.addCallback(lambda _: acc.rename_mailbox( - "OriginalMailbox", "RenamedMailbox")) - d.addCallback(lambda _: acc.list_all_mailbox_names()) - d.addCallback(self._test_rename_mailbox_cb) - return d - - def _test_rename_mailbox_cb(self, mboxes): - expected = ['INBOX', 'RenamedMailbox'] - self.assertItemsEqual(mboxes, expected) - - def test_get_all_mailboxes(self): - acc = self.get_account('some_user_id') - d = acc.callWhenReady(lambda _: acc.add_mailbox("OneMailbox")) - d.addCallback(lambda _: acc.add_mailbox("TwoMailbox")) - d.addCallback(lambda _: acc.add_mailbox("ThreeMailbox")) - d.addCallback(lambda _: acc.add_mailbox("anotherthing")) - d.addCallback(lambda _: acc.add_mailbox("anotherthing2")) - d.addCallback(lambda _: acc.get_all_mailboxes()) - d.addCallback(self._test_get_all_mailboxes_cb) - return d - - def _test_get_all_mailboxes_cb(self, mailboxes): - expected = ["INBOX", "OneMailbox", "TwoMailbox", "ThreeMailbox", - "anotherthing", "anotherthing2"] - names = [m.mbox for m in mailboxes] - self.assertItemsEqual(names, expected) - - def test_get_collection_by_mailbox(self): - acc = self.get_account('some_user_id') - d = acc.callWhenReady(lambda _: acc.get_collection_by_mailbox("INBOX")) - d.addCallback(self._test_get_collection_by_mailbox_cb) - return d - - def _test_get_collection_by_mailbox_cb(self, collection): - self.assertTrue(collection.is_mailbox_collection()) - - def assert_uid_next_empty_collection(uid): - self.assertEqual(uid, 1) - d = collection.get_uid_next() - d.addCallback(assert_uid_next_empty_collection) - return d - - def test_get_collection_by_docs(self): - pass - - test_get_collection_by_docs.skip = "Not yet implemented" - - def test_get_collection_by_tag(self): - pass - - test_get_collection_by_tag.skip = "Not yet implemented" diff --git a/mail/src/leap/mail/tests/test_mailbox_indexer.py b/mail/src/leap/mail/tests/test_mailbox_indexer.py deleted file mode 100644 index 5c1891d..0000000 --- a/mail/src/leap/mail/tests/test_mailbox_indexer.py +++ /dev/null @@ -1,250 +0,0 @@ -# -*- coding: utf-8 -*- -# test_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 <http://www.gnu.org/licenses/>. -""" -Tests for the mailbox_indexer module. -""" -import uuid -from functools import partial - -from leap.mail import mailbox_indexer as mi -from leap.mail.testing.common import SoledadTestMixin - -hash_test0 = '590c9f8430c7435807df8ba9a476e3f1295d46ef210f6efae2043a4c085a569e' -hash_test1 = '1b4f0e9851971998e732078544c96b36c3d01cedf7caa332359d6f1d83567014' -hash_test2 = '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' -hash_test3 = 'fd61a03af4f77d870fc21e05e7e80678095c92d808cfb3b5c279ee04c74aca13' -hash_test4 = 'a4e624d686e03ed2767c0abd85c14426b0b1157d2ce81d27bb4fe4f6f01d688a' - - -def fmt_hash(mailbox_uuid, hash): - return "M-" + mailbox_uuid.replace('-', '_') + "-" + hash - -mbox_id = str(uuid.uuid4()) - - -class MailboxIndexerTestCase(SoledadTestMixin): - """ - Tests for the MailboxUID class. - """ - def get_mbox_uid(self): - m_uid = mi.MailboxIndexer(self._soledad) - return m_uid - - def list_mail_tables_cb(self, ignored): - def filter_mailuid_tables(tables): - filtered = [ - table[0] for table in tables if - table[0].startswith(mi.MailboxIndexer.table_preffix)] - return filtered - - sql = "SELECT name FROM sqlite_master WHERE type='table';" - d = self._soledad.raw_sqlcipher_query(sql) - d.addCallback(filter_mailuid_tables) - return d - - def select_uid_rows(self, mailbox): - sql = "SELECT * FROM %s%s;" % ( - mi.MailboxIndexer.table_preffix, mailbox.replace('-', '_')) - d = self._soledad.raw_sqlcipher_query(sql) - return d - - def test_create_table(self): - def assert_table_created(tables): - self.assertEqual( - tables, ["leapmail_uid_" + mbox_id.replace('-', '_')]) - - m_uid = self.get_mbox_uid() - d = m_uid.create_table(mbox_id) - d.addCallback(self.list_mail_tables_cb) - d.addCallback(assert_table_created) - return d - - def test_create_and_delete_table(self): - def assert_table_deleted(tables): - self.assertEqual(tables, []) - - m_uid = self.get_mbox_uid() - d = m_uid.create_table(mbox_id) - d.addCallback(lambda _: m_uid.delete_table(mbox_id)) - d.addCallback(self.list_mail_tables_cb) - d.addCallback(assert_table_deleted) - return d - - def test_insert_doc(self): - m_uid = self.get_mbox_uid() - - h1 = fmt_hash(mbox_id, hash_test0) - h2 = fmt_hash(mbox_id, hash_test1) - h3 = fmt_hash(mbox_id, hash_test2) - h4 = fmt_hash(mbox_id, hash_test3) - h5 = fmt_hash(mbox_id, hash_test4) - - def assert_uid_rows(rows): - expected = [(1, h1), (2, h2), (3, h3), (4, h4), (5, h5)] - self.assertEquals(rows, expected) - - d = m_uid.create_table(mbox_id) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h1)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h2)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h3)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h4)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h5)) - d.addCallback(lambda _: self.select_uid_rows(mbox_id)) - d.addCallback(assert_uid_rows) - return d - - def test_insert_doc_return(self): - m_uid = self.get_mbox_uid() - - def assert_rowid(rowid, expected=None): - self.assertEqual(rowid, expected) - - h1 = fmt_hash(mbox_id, hash_test0) - h2 = fmt_hash(mbox_id, hash_test1) - h3 = fmt_hash(mbox_id, hash_test2) - - d = m_uid.create_table(mbox_id) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h1)) - d.addCallback(partial(assert_rowid, expected=1)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h2)) - d.addCallback(partial(assert_rowid, expected=2)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h3)) - d.addCallback(partial(assert_rowid, expected=3)) - return d - - def test_delete_doc(self): - m_uid = self.get_mbox_uid() - - h1 = fmt_hash(mbox_id, hash_test0) - h2 = fmt_hash(mbox_id, hash_test1) - h3 = fmt_hash(mbox_id, hash_test2) - h4 = fmt_hash(mbox_id, hash_test3) - h5 = fmt_hash(mbox_id, hash_test4) - - def assert_uid_rows(rows): - expected = [(4, h4), (5, h5)] - self.assertEquals(rows, expected) - - d = m_uid.create_table(mbox_id) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h1)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h2)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h3)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h4)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h5)) - - d.addCallbacks(lambda _: m_uid.delete_doc_by_uid(mbox_id, 1)) - d.addCallbacks(lambda _: m_uid.delete_doc_by_uid(mbox_id, 2)) - d.addCallbacks(lambda _: m_uid.delete_doc_by_hash(mbox_id, h3)) - - d.addCallback(lambda _: self.select_uid_rows(mbox_id)) - d.addCallback(assert_uid_rows) - return d - - def test_get_doc_id_from_uid(self): - m_uid = self.get_mbox_uid() - - h1 = fmt_hash(mbox_id, hash_test0) - - def assert_doc_hash(res): - self.assertEqual(res, h1) - - d = m_uid.create_table(mbox_id) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h1)) - d.addCallback(lambda _: m_uid.get_doc_id_from_uid(mbox_id, 1)) - d.addCallback(assert_doc_hash) - return d - - def test_count(self): - m_uid = self.get_mbox_uid() - - h1 = fmt_hash(mbox_id, hash_test0) - h2 = fmt_hash(mbox_id, hash_test1) - h3 = fmt_hash(mbox_id, hash_test2) - h4 = fmt_hash(mbox_id, hash_test3) - h5 = fmt_hash(mbox_id, hash_test4) - - d = m_uid.create_table(mbox_id) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h1)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h2)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h3)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h4)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h5)) - - def assert_count_after_inserts(count): - self.assertEquals(count, 5) - - d.addCallback(lambda _: m_uid.count(mbox_id)) - d.addCallback(assert_count_after_inserts) - - d.addCallbacks(lambda _: m_uid.delete_doc_by_uid(mbox_id, 1)) - d.addCallbacks(lambda _: m_uid.delete_doc_by_uid(mbox_id, 2)) - - def assert_count_after_deletions(count): - self.assertEquals(count, 3) - - d.addCallback(lambda _: m_uid.count(mbox_id)) - d.addCallback(assert_count_after_deletions) - return d - - def test_get_next_uid(self): - m_uid = self.get_mbox_uid() - - h1 = fmt_hash(mbox_id, hash_test0) - h2 = fmt_hash(mbox_id, hash_test1) - h3 = fmt_hash(mbox_id, hash_test2) - h4 = fmt_hash(mbox_id, hash_test3) - h5 = fmt_hash(mbox_id, hash_test4) - - d = m_uid.create_table(mbox_id) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h1)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h2)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h3)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h4)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h5)) - - def assert_next_uid(result, expected=1): - self.assertEquals(result, expected) - - d.addCallback(lambda _: m_uid.get_next_uid(mbox_id)) - d.addCallback(partial(assert_next_uid, expected=6)) - return d - - def test_all_uid_iter(self): - - m_uid = self.get_mbox_uid() - - h1 = fmt_hash(mbox_id, hash_test0) - h2 = fmt_hash(mbox_id, hash_test1) - h3 = fmt_hash(mbox_id, hash_test2) - h4 = fmt_hash(mbox_id, hash_test3) - h5 = fmt_hash(mbox_id, hash_test4) - - d = m_uid.create_table(mbox_id) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h1)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h2)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h3)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h4)) - d.addCallback(lambda _: m_uid.insert_doc(mbox_id, h5)) - d.addCallback(lambda _: m_uid.delete_doc_by_uid(mbox_id, 1)) - d.addCallback(lambda _: m_uid.delete_doc_by_uid(mbox_id, 4)) - - def assert_all_uid(result, expected=[2, 3, 5]): - self.assertEquals(result, expected) - - d.addCallback(lambda _: m_uid.all_uid_iter(mbox_id)) - d.addCallback(partial(assert_all_uid)) - return d diff --git a/mail/src/leap/mail/tests/test_walk.py b/mail/src/leap/mail/tests/test_walk.py deleted file mode 100644 index 826ec10..0000000 --- a/mail/src/leap/mail/tests/test_walk.py +++ /dev/null @@ -1,81 +0,0 @@ -""" -Tests for leap.mail.walk module -""" -import os.path -from email.parser import Parser - -from leap.mail import walk - -CORPUS = { - 'simple': 'rfc822.message', - 'multimin': 'rfc822.multi-minimal.message', - 'multisigned': 'rfc822.multi-signed.message', - 'bounced': 'rfc822.bounce.message', -} - -_here = os.path.dirname(__file__) -_parser = Parser() - - -# tests - - -def test_simple_mail(): - msg = _parse('simple') - tree = walk.get_tree(msg) - assert len(tree['part_map']) == 0 - assert tree['ctype'] == 'text/plain' - assert tree['multi'] is False - - -def test_multipart_minimal(): - msg = _parse('multimin') - tree = walk.get_tree(msg) - - assert tree['multi'] is True - assert len(tree['part_map']) == 1 - first = tree['part_map'][1] - assert first['multi'] is False - assert first['ctype'] == 'text/plain' - - -def test_multi_signed(): - msg = _parse('multisigned') - tree = walk.get_tree(msg) - assert tree['multi'] is True - assert len(tree['part_map']) == 2 - - _first = tree['part_map'][1] - _second = tree['part_map'][2] - assert len(_first['part_map']) == 3 - assert(_second['multi'] is False) - - -def test_bounce_mime(): - msg = _parse('bounced') - tree = walk.get_tree(msg) - - ctypes = [tree['part_map'][index]['ctype'] - for index in sorted(tree['part_map'].keys())] - third = tree['part_map'][3] - three_one_ctype = third['part_map'][1]['ctype'] - assert three_one_ctype == 'multipart/signed' - - assert ctypes == [ - 'text/plain', - 'message/delivery-status', - 'message/rfc822'] - - -# utils - -def _parse(name): - _str = _get_string_for_message(name) - return _parser.parsestr(_str) - - -def _get_string_for_message(name): - filename = os.path.join(_here, CORPUS[name]) - with open(filename) as f: - msgstr = f.read() - return msgstr diff --git a/mail/src/leap/mail/utils.py b/mail/src/leap/mail/utils.py deleted file mode 100644 index 64fca98..0000000 --- a/mail/src/leap/mail/utils.py +++ /dev/null @@ -1,375 +0,0 @@ -# -*- coding: utf-8 -*- -# utils.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 <http://www.gnu.org/licenses/>. -""" -Mail utilities. -""" -from email.utils import parseaddr -import json -import re -import traceback -import Queue - -from leap.soledad.common.document import SoledadDocument -from leap.common.check import leap_assert_type -from twisted.mail import smtp - - -CHARSET_PATTERN = r"""charset=([\w-]+)""" -CHARSET_RE = re.compile(CHARSET_PATTERN, re.IGNORECASE) - - -def first(things): - """ - Return the head of a collection. - """ - try: - return things[0] - except (IndexError, TypeError): - return None - - -def empty(thing): - """ - Return True if a thing is None or its length is zero. - If thing is a number (int, float, long), return False. - """ - if thing is None: - return True - if isinstance(thing, (int, float, long)): - return False - if isinstance(thing, SoledadDocument): - thing = thing.content - try: - return len(thing) == 0 - except (ReferenceError, TypeError): - return True - - -def maybe_call(thing): - """ - Return the same thing, or the result of its invocation if it is a - callable. - """ - return thing() if callable(thing) else thing - - -def find_charset(thing, default=None): - """ - Looks into the object 'thing' for a charset specification. - It searchs into the object's `repr`. - - :param thing: the object to look into. - :type thing: object - :param default: the dafault charset to return if no charset is found. - :type default: str - - :return: the charset or 'default' - :rtype: str or None - """ - charset = first(CHARSET_RE.findall(repr(thing))) - if charset is None: - charset = default - return charset - - -def lowerdict(_dict): - """ - Return a dict with the keys in lowercase. - - :param _dict: the dict to convert - :rtype: dict - """ - # TODO should properly implement a CaseInsensitive dict. - # Look into requests code. - return dict((key.lower(), value) - for key, value in _dict.items()) - - -PART_MAP = "part_map" -PHASH = "phash" - - -def _str_dict(d, k): - """ - Convert the dictionary key to string if it was a string. - - :param d: the dict - :type d: dict - :param k: the key - :type k: object - """ - if isinstance(k, int): - val = d[k] - d[str(k)] = val - del(d[k]) - - -def stringify_parts_map(d): - """ - Modify a dictionary making all the nested dicts under "part_map" keys - having strings as keys. - - :param d: the dictionary to modify - :type d: dictionary - :rtype: dictionary - """ - for k in d: - if k == PART_MAP: - pmap = d[k] - for kk in pmap.keys(): - _str_dict(d[k], kk) - for kk in pmap.keys(): - stringify_parts_map(d[k][str(kk)]) - return d - - -def phash_iter(d): - """ - A recursive generator that extracts all the payload-hashes - from an arbitrary nested parts-map dictionary. - - :param d: the dictionary to walk - :type d: dictionary - :return: a list of all the phashes found - :rtype: list - """ - if PHASH in d: - yield d[PHASH] - if PART_MAP in d: - for key in d[PART_MAP]: - for phash in phash_iter(d[PART_MAP][key]): - yield phash - - -def accumulator(fun, lim): - """ - A simple accumulator that uses a closure and a mutable - object to collect items. - When the count of items is greater than `lim`, the - collection is flushed after invoking a map of the function `fun` - over it. - - The returned accumulator can also be flushed at any moment - by passing a boolean as a second parameter. - - :param fun: the function to call over the collection - when its size is greater than `lim` - :type fun: callable - :param lim: the turning point for the collection - :type lim: int - :rtype: function - - >>> from pprint import pprint - >>> acc = accumulator(pprint, 2) - >>> acc(1) - >>> acc(2) - [1, 2] - >>> acc(3) - >>> acc(4) - [3, 4] - >>> acc = accumulator(pprint, 5) - >>> acc(1) - >>> acc(2) - >>> acc(3) - >>> acc(None, flush=True) - [1,2,3] - """ - KEY = "items" - _o = {KEY: []} - - def _accumulator(item, flush=False): - collection = _o[KEY] - collection.append(item) - if len(collection) >= lim or flush: - map(fun, filter(None, collection)) - _o[KEY] = [] - - return _accumulator - - -def accumulator_queue(fun, lim): - """ - A version of the accumulator that uses a queue. - - When the count of items is greater than `lim`, the - queue is flushed after invoking the function `fun` - over its items. - - The returned accumulator can also be flushed at any moment - by passing a boolean as a second parameter. - - :param fun: the function to call over the collection - when its size is greater than `lim` - :type fun: callable - :param lim: the turning point for the collection - :type lim: int - :rtype: function - """ - _q = Queue.Queue() - - def _accumulator(item, flush=False): - _q.put(item) - if _q.qsize() >= lim or flush: - collection = [_q.get() for i in range(_q.qsize())] - map(fun, filter(None, collection)) - - return _accumulator - - -def validate_address(address): - """ - Validate C{address} as defined in RFC 2822. - - :param address: The address to be validated. - :type address: str - - @return: A valid address. - @rtype: str - - @raise smtp.SMTPBadRcpt: Raised if C{address} is invalid. - """ - leap_assert_type(address, str) - # in the following, the address is parsed as described in RFC 2822 and - # ('', '') is returned if the parse fails. - _, address = parseaddr(address) - if address == '': - raise smtp.SMTPBadRcpt(address) - return address - -# -# String manipulation -# - - -class CustomJsonScanner(object): - """ - This class is a context manager definition used to monkey patch the default - json string parsing behavior. - The emails can have more than one encoding, so the `str` objects have more - than one encoding and json does not support direct work with `str` - (only `unicode`). - """ - - def _parse_string_str(self, s, idx, *args, **kwargs): - """ - Parses the string "s" starting at the point idx and returns an `str` - object. Which basically means it works exactly the same as the regular - JSON string parsing, except that it doesn't try to decode utf8. - We need this because mail raw strings might have bytes in multiple - encodings. - - :param s: the string we want to parse - :type s: str - :param idx: the starting point for parsing - :type idx: int - - :returns: the parsed string and the index where the - string ends. - :rtype: tuple (str, int) - """ - # NOTE: we just want to use this monkey patched version if we are - # calling the loads from our custom method. Otherwise, we use the - # json's default parser. - monkey_patched = False - for i in traceback.extract_stack(): - # look for json_loads method in the call stack - if i[2] == json_loads.__name__: - monkey_patched = True - break - - if not monkey_patched: - return self._orig_scanstring(s, idx, *args, **kwargs) - - # TODO profile to see if a compiled regex can get us some - # benefit here. - found = False - end = s.find("\"", idx) - while not found: - try: - if s[end - 1] != "\\": - found = True - else: - end = s.find("\"", end + 1) - except Exception: - found = True - return s[idx:end].decode("string-escape"), end + 1 - - def __enter__(self): - """ - Replace the json methods with the needed ones. - Also make a backup to restore them later. - """ - # backup original values - self._orig_make_scanner = json.scanner.make_scanner - self._orig_scanstring = json.decoder.scanstring - - # We need the make_scanner function to be the python one so we can - # monkey_patch the json string parsing - json.scanner.make_scanner = json.scanner.py_make_scanner - - # And now we monkey patch the money method - json.decoder.scanstring = self._parse_string_str - - def __exit__(self, exc_type, exc_value, traceback): - """ - Restores the backuped methods. - """ - # restore original values - json.scanner.make_scanner = self._orig_make_scanner - json.decoder.scanstring = self._orig_scanstring - - -def json_loads(data): - """ - It works as json.loads but supporting multiple encodings in the same - string and accepting an `str` parameter that won't be converted to unicode. - - :param data: the string to load the objects from - :type data: str - - :returns: the corresponding python object result of parsing 'data', this - behaves similarly as json.loads, with the exception of that - returns always `str` instead of `unicode`. - """ - obj = None - with CustomJsonScanner(): - # We need to use the cls parameter in order to trigger the code - # that will let us control the string parsing method. - obj = json.loads(data, cls=json.JSONDecoder) - - return obj - - -class CaseInsensitiveDict(dict): - """ - A dictionary subclass that will allow case-insenstive key lookups. - """ - def __init__(self, d=None): - if d is None: - d = [] - if isinstance(d, dict): - for key, value in d.items(): - self[key] = value - else: - for key, value in d: - self[key] = value - - def __setitem__(self, key, value): - super(CaseInsensitiveDict, self).__setitem__(key.lower(), value) - - def __getitem__(self, key): - return super(CaseInsensitiveDict, self).__getitem__(key.lower()) diff --git a/mail/src/leap/mail/walk.py b/mail/src/leap/mail/walk.py deleted file mode 100644 index d143d61..0000000 --- a/mail/src/leap/mail/walk.py +++ /dev/null @@ -1,107 +0,0 @@ -# -*- coding: utf-8 -*- -# walk.py -# Copyright (C) 2013-2015 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 <http://www.gnu.org/licenses/>. -""" -Walk a message tree and generate documents that can be inserted in the backend -store. -""" -from email.parser import Parser - -from cryptography.hazmat.backends.multibackend import MultiBackend -from cryptography.hazmat.backends.openssl.backend import ( - Backend as OpenSSLBackend) -from cryptography.hazmat.primitives import hashes - -from leap.mail.utils import first - -crypto_backend = MultiBackend([OpenSSLBackend()]) - -_parser = Parser() - - -def get_tree(msg): - p = {} - p['ctype'] = msg.get_content_type() - p['headers'] = msg.items() - - payload = msg.get_payload() - is_multi = msg.is_multipart() - if is_multi: - p['part_map'] = dict( - [(idx, get_tree(part)) for idx, part in enumerate(payload, 1)]) - p['parts'] = len(payload) - p['phash'] = None - else: - p['parts'] = 0 - p['size'] = len(payload) - p['phash'] = get_hash(payload) - p['part_map'] = {} - p['multi'] = is_multi - return p - - -def get_tree_from_string(messagestr): - return get_tree(_parser.parsestr(messagestr)) - - -def get_body_phash(msg): - """ - Find the body payload-hash for this message. - """ - for part in msg.walk(): - # XXX what other ctypes should be considered body? - if part.get_content_type() in ("text/plain", "text/html"): - # XXX avoid hashing again - return get_hash(part.get_payload()) - - -def get_raw_docs(msg): - """ - We get also some of the headers to be able to - index the content. Here we remove any mutable part, as the the filename - in the content disposition. - """ - return ( - {'type': 'cnt', - 'raw': part.get_payload(), - 'phash': get_hash(part.get_payload()), - 'content-type': part.get_content_type(), - 'content-disposition': first(part.get( - 'content-disposition', '').split(';')), - 'content-transfer-encoding': part.get( - 'content-transfer-encoding', '') - } for part in msg.walk() if not isinstance(part.get_payload(), list)) - - -def get_hash(s): - digest = hashes.Hash(hashes.SHA256(), crypto_backend) - digest.update(s) - return digest.finalize().encode("hex").upper() - - -""" -Groucho Marx: Now pay particular attention to this first clause, because it's - most important. There's the party of the first part shall be - known in this contract as the party of the first part. How do you - like that, that's pretty neat eh? - -Chico Marx: No, that's no good. -Groucho Marx: What's the matter with it? - -Chico Marx: I don't know, let's hear it again. -Groucho Marx: So the party of the first part shall be known in this contract as - the party of the first part. -""" |