summaryrefslogtreecommitdiff
path: root/src/leap
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap')
-rw-r--r--src/leap/mx/__init__.py4
-rw-r--r--src/leap/mx/couchdbhelper.py14
-rw-r--r--src/leap/mx/exceptions.py23
-rw-r--r--src/leap/mx/mail_receiver.py57
-rw-r--r--src/leap/mx/runner.py83
-rw-r--r--src/leap/mx/util/config.py221
-rw-r--r--src/leap/mx/util/log.py143
-rw-r--r--src/leap/mx/util/net.py126
-rw-r--r--src/leap/mx/util/storage.py42
9 files changed, 50 insertions, 663 deletions
diff --git a/src/leap/mx/__init__.py b/src/leap/mx/__init__.py
index 8ef1bc9..0590c90 100644
--- a/src/leap/mx/__init__.py
+++ b/src/leap/mx/__init__.py
@@ -6,6 +6,4 @@ Module initialization file for leap.mx .
"""
from leap.mx.util import version
-__all__ = ['alias_resolver', 'couchdb', 'exceptions', 'runner', 'util']
-__author__ = version.getAuthors()
-__version__ = version.getVersion()
+__all__ = ['alias_resolver', 'couchdbhelper']
diff --git a/src/leap/mx/couchdbhelper.py b/src/leap/mx/couchdbhelper.py
index 4e76be3..2bbca77 100644
--- a/src/leap/mx/couchdbhelper.py
+++ b/src/leap/mx/couchdbhelper.py
@@ -20,6 +20,10 @@ Classes for working with CouchDB or BigCouch instances which store email alias
maps, user UUIDs, and GPG keyIDs.
"""
+import logging
+
+from functools import partial
+
try:
from paisley import client
except ImportError:
@@ -32,9 +36,7 @@ except ImportError:
print "This software requires Twisted. Please see the README file"
print "for instructions on getting required dependencies."
-from functools import partial
-
-from leap.mx.util import log
+logger = logging.getLogger(__name__)
class ConnectedCouchDB(client.CouchDB):
@@ -82,9 +84,9 @@ class ConnectedCouchDB(client.CouchDB):
@param data: response from the listDB command
@type data: array
"""
- log.msg("Available databases:")
+ logger.msg("Available databases:")
for database in data:
- log.msg(" * %s" % (database,))
+ logger.msg(" * %s" % (database,))
def createDB(self, dbName):
"""
@@ -117,7 +119,7 @@ class ConnectedCouchDB(client.CouchDB):
reduce=False,
include_docs=True)
- d.addCallbacks(partial(self._get_uuid, alias), log.err)
+ d.addCallbacks(partial(self._get_uuid, alias), logger.error)
return d
diff --git a/src/leap/mx/exceptions.py b/src/leap/mx/exceptions.py
deleted file mode 100644
index 63b946c..0000000
--- a/src/leap/mx/exceptions.py
+++ /dev/null
@@ -1,23 +0,0 @@
-#! -*- encoding: utf-8 -*-
-"""
-Custom exceptions for leap_mx.
-
-@authors: Isis Lovecruft, <isis@leap.se> 0x2cdb8b35
-@version: 0.0.1
-@license: see included LICENSE file
-"""
-
-
-class MissingConfig(Exception):
- """Raised when the config file cannot be found."""
- def __init__(self, message=None, config_file=None):
- if message:
- return
- else:
- self.message = "Cannot locate config file"
- if config_file:
- self.message += " %s" % config_file
- self.message += "."
-
-class UnsupportedOS(Exception):
- """Raised when we're not *nix or *BSD."""
diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py
index 09200ac..ae32f25 100644
--- a/src/leap/mx/mail_receiver.py
+++ b/src/leap/mx/mail_receiver.py
@@ -17,10 +17,13 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
+import uuid as pyuuid
import logging
import argparse
import ConfigParser
+import json
+
from email import message_from_string
from functools import partial
@@ -28,41 +31,58 @@ from twisted.internet import inotify, reactor
from twisted.python import filepath
from leap.mx import couchdbhelper
+
+from leap.soledad import LeapDocument
+from leap.soledad.backends.leap_backend import EncryptionSchemes
from leap.soledad.backends.couch import CouchDatabase
+from leap.common.keymanager import openpgp
logger = logging.getLogger(__name__)
-def _get_pubkey(uuid):
- # TODO: implent!
+def _get_pubkey(uuid, cdb):
logger.debug("Fetching pubkey for %s" % (uuid,))
- return uuid, ""
+ return uuid, cdb.getPubKey(uuid)
-def _encrypt_message(uuid_pubkey, message):
- # TODO: implement!
+def _encrypt_message(uuid_pubkey, address_message):
uuid, pubkey = uuid_pubkey
+ address, message = address_message
logger.debug("Encrypting message to %s's pubkey" % (uuid,))
logger.debug("Pubkey: %s" % (pubkey,))
if pubkey is None or len(pubkey) == 0:
- # TODO: This is only for testing!! REMOVE!
- return uuid, message
+ logger.exception("No public key found")
+ raise Exception("No public key found")
+
+ doc = LeapDocument(encryption_scheme=EncryptionSchemes.PUBKEY,
+ doc_id=str(pyuuid.uuid4()))
+
+ def _ascii_to_openpgp_cb(gpg):
+ key = gpg.list_keys().pop()
+ return openpgp._build_key_from_gpg(address, key, pubkey)
- encrypted = ""
+ openpgp_key = openpgp._safe_call(_ascii_to_openpgp_cb, pubkey)
- return uuid, encrypted
+ data = {'incoming': True, 'content': message}
+ doc.content = {
+ "_encrypted_json": openpgp.encrypt_asym(json.dumps(data), openpgp_key)
+ }
-def _export_message(uuid_message, couch_url):
- uuid, message = uuid_message
+ return uuid, doc
+
+
+def _export_message(uuid_doc, couch_url):
+ uuid, doc = uuid_doc
logger.debug("Exporting message for %s" % (uuid,))
if uuid is None:
uuid = 0
- db_url = couch_url + '/user-%s' % uuid
- db = CouchDatabase.open_database(db_url, create=True)
- doc = db.create_doc({'content': str(message)})
+ db = CouchDatabase(couch_url, "user-%s" % (uuid,))
+ db.put_doc(doc)
+
+ logger.debug("Done exporting")
return True
@@ -73,6 +93,7 @@ def _conditional_remove(do_remove, filepath):
try:
logger.debug("Removing %s" % (filepath.path,))
filepath.remove()
+ logger.debug("Done removing")
except Exception as e:
# TODO: better handle exceptions
logger.exception("%s" % (e,))
@@ -85,10 +106,14 @@ def _process_incoming_email(users_db, mail_couchdb_url_prefix, self, filepath, m
mail_data = f.read()
mail = message_from_string(mail_data)
owner = mail["Delivered-To"]
+ owner = owner.split("@")[0]
+ owner = owner.split("+")[0]
+ logger.debug("Mail owner: %s" % (owner,))
+
logger.debug("%s received a new mail" % (owner,))
d = users_db.queryByLoginOrAlias(owner)
- d.addCallback(_get_pubkey)
- d.addCallback(_encrypt_message, (mail_data))
+ d.addCallback(_get_pubkey, (users_db))
+ d.addCallback(_encrypt_message, (owner, mail_data))
d.addCallback(_export_message, (mail_couchdb_url_prefix))
d.addCallback(_conditional_remove, (filepath))
diff --git a/src/leap/mx/runner.py b/src/leap/mx/runner.py
deleted file mode 100644
index daf956e..0000000
--- a/src/leap/mx/runner.py
+++ /dev/null
@@ -1,83 +0,0 @@
-#-*- coding: utf-8 -*-
-"""
-runner
-------
-A module containing application and daemon process utilities.
-
-@author Isis Agora Lovecruft <isis@leap.se>, 0x2cdb8b35
-@version 0.0.1
-
-"""
-
-from os import path as ospath
-
-import re
-
-
-class CheckRequirements(ImportError):
- """
- Raised when we're missing something from requirements.pip.
- """
- def __init__(self, package_name, pipfile, message=None):
- """
- Display an error message with instructions for obtaining missing
- dependencies.
-
- @param message: A string describing the error.
- @param missing: A string indicating which dependency is missing.
- @param pipfile: The path and filename of the pip requirements file,
- relative to the top-level repository directory.
- """
- if message:
- self.message = message
- return self
-
- self.package_name = package_name
- self.pipfile = pipfile
- self.dependencies = self.__read_pip_requirements__()
- self.missing = []
-
- for package, version in self.dependencies:
- pkg = package.lower() if package == "Twisted" else package
- try:
- __import__(pkg)
- except ImportError:
- self.missing.append(package)
-
- if len(self.missing) > 0:
- self.message = self.package_name + " requires "
- elif len(self.missing) <= 0:
- return None
-
- if len(self.missing) >= 1:
- for missed in self.missing[:-1]:
- self.message += missed + ", "
- self.message += "and "
-
- if len(self.missing) == 1:
- self.message += self.missing[0] + "."
- self.message += "\nPlease see %s for ".format(self.pipfile)
- self.message += "instruction on installing dependencies."
- raise self(self.message)
-
- def __read_pip_requirements__(self, file=None):
- """
- Check the pip requirements file to determine our dependencies.
-
- @param file: The full path of the pip requirements.txt file.
- @returns: A list of tuple(package_name, package_version).
- """
- if not file:
- file = self.pipfile
-
- requirement = re.compile('[^0-9=><]+')
- dependencies = []
-
- with open(file) as pipfile:
- for line in pipfile.readlines():
- shortened = line.strip()
- matched = requirement.match(shortened)
- package_name = matched.group()
- package_version = shortened.split(package_name, 1)[1]
- dependencies.append((package_name, package_version))
- return dependencies
diff --git a/src/leap/mx/util/config.py b/src/leap/mx/util/config.py
deleted file mode 100644
index f655ca9..0000000
--- a/src/leap/mx/util/config.py
+++ /dev/null
@@ -1,221 +0,0 @@
-#! -*- encoding: utf-8 -*-
-"""
-Config file utilities.
-
-This module has an :attr:`config_filename`, which can be used to set the
-filename outside of function calls:
-
- >>> from leap.mx.util import config
- >>> config.config_filename = "blahblah.yaml"
-
-If not set anywhere, it will default to using the top level repository
-directory, i.e. "/.../leap_mx/leap_mx.conf", and will create that file with
-the default settings if it does not exist.
-
-The config file can be loaded/created with :func:`config.loadConfig`:
-
- >>> config.loadConfig()
-
-Once the config file is loaded, this module presents a highly object-oriented
-interface, so that sections taken from the config file become attribute of
-this module, and the name of their respective settings become attributes of
-the section names. Like this:
-
- >>> print config.basic.postfix_port
- 465
-
-@authors: Isis Lovecruft, <isis@leap.se> 0x2cdb8b35
-@version: 0.0.1
-@license: see included LICENSE file
-"""
-
-from os import path as ospath
-
-import sys
-import yaml
-
-from leap.mx.util import version, storage
-from leap.mx.exceptions import MissingConfig, UnsupportedOS
-
-
-filename = None
-config_version = None
-basic = storage.Storage()
-couch = storage.Storage()
-advanced = storage.Storage()
-
-PLATFORMS = {'LINUX': sys.platform.startswith("linux"),
- 'OPENBSD': sys.platform.startswith("openbsd"),
- 'FREEBSD': sys.platform.startswith("freebsd"),
- 'NETBSD': sys.platform.startswith("netbsd"),
- 'DARWIN': sys.platform.startswith("darwin"),
- 'SOLARIS': sys.platform.startswith("sunos"),
- 'WINDOWS': sys.platform.startswith("win32")}
-
-def getClientPlatform(platform_name=None):
- """
- Determine the client's operating system platform. Optionally, if
- :param:`platform_name` is given, check that this is indeed the platform
- we're operating on.
-
- @param platform_name: A string, upper-, lower-, or mixed case, of one
- of the keys in the :attr:`leap.util.version.PLATFORMS`
- dictionary. E.g. 'Linux' or 'OPENBSD', etc.
- @returns: A string specifying the platform name, and the boolean test
- used to determine it.
- """
- for name, test in PLATFORMS.items():
- if not platform_name or platform_name.upper() == name:
- if test:
- return name, test
-
-def _create_config_file(conffile):
- """
- Create the config file if it doesn't exist.
-
- @param conffile: The full path to the config file to write to.
- """
- with open(conffile, 'w+') as conf:
- conf.write("""
-#
-# mx.conf
-# =======
-# Configurable options for the leap_mx encrypting mail exchange.
-#
-# This file follows YAML markup format: http://yaml.org/spec/1.2/spec.html
-# Keep in mind that indentation matters.
-#
-
-basic:
- # Whether or not to log to file:
- enable_logfile: True
- # The name of the logfile:
- logfile: mx.log
- # Where is the spoolfile of messages to encrypt?:
- spoolfile: /var/mail/encrypt_me
-couch:
- # The couch username for authentication to a CouchDB instance:
- user: admin
- # The couch username's password:
- passwd: passwd
- # The CouchDB hostname or IP address to connect to:
- host: couchdb.example.com
- # The CouchDB port to connect to:
- port: 7001
-advanced:
- # Which port on localhost should postfix send check_recipient queries to?:
- check_recipient_access_port: 1347
- # Which port on localhost should postfix ask for UUIDs?:
- virtual_alias_map_port: 1348
- # Enable debugging output in the logger:
- debug: True
- # Print enough things really fast to make you look super 1337:
- noisy: False
-config_version: 0.0.2
-
-""")
- conf.flush()
- assert ospath.isfile(conffile), "Config file %s not created!" % conffile
-
-def _get_config_location(config_filename=None,
- use_dot_config_directory=False):
- """
- Get the full path and filename of the config file.
- """
- platform = getClientPlatform()[0]
-
- ## If not given, default to the application's name + '.conf'
- if not config_filename:
- if not filename:
- config_filename = "mx.conf"
- else:
- config_filename = filename
-
- ## Oh hell, it could be said only to beguile:
- ## That windoze users are capable of editing a .conf file.
- ## Also, what maddened wingnut would be so fool
- ## To run a mail exchange on a windoze nodule?
- ## I'm ignoring these loons for now. And pardon if I seem jaded,
- ## But srsly, this and that solaris sh*t should be deprecated.
- if not platform.endswith('LINUX') and not platform.endswith('BSD'):
- raise UnsupportedOS("Sorry, your operating system isn't supported.")
-
- where = None
- if use_dot_config_directory:
- ## xxx only install/import this in *nix
- from xdg import BaseDirectory
-
- dot_config_dirs = BaseDirectory.xdg_config_dirs
- for dir in dot_config_dirs:
- our_dir = ospath.join(dir, package_name)
- if ospath.isdir(our_dir):
- if config_filename in os.listdir(our_dir):
- where = ospath.abspath(our_dir)
- ## Use repo dir instead:
- if not where:
- where = version.getRepoDir()
-
- conffile = ospath.join(where, config_filename)
- try:
- with open(conffile) as cf: pass
- except IOError:
- _create_config_file(conffile)
- finally:
- return conffile
-
-def loadConfig(file=None):
- """
- Some of this is taken from OONI config code for now, and so this should be
- refacotored, along with the leap_client config code, so that we have
- similarly structured config files. It is perhaps desirable to also use
- soledad as a backend for remote setup and maintainance, and thus this code
- will need to hook into u1db (and potentially "pysqlcipher").
-
- Excuse the yaml for now, I just wanted something that works.
-
- @param file: (optional) If provided, use this filename.
- """
- if not file:
- file = _get_config_location()
-
- if ospath.isfile(file):
- with open(file, 'a+') as conf:
- config_contents = '\n'.join(conf.readlines())
- cfg = yaml.safe_load(config_contents)
-
- ## These become objects with their keys loaded as attributes:
- ##
- ## from leap.util import config
- ## config.basic.foo = bar
- ##
- try:
- for k, v in cfg['basic'].items():
- basic[k] = v
- except (AttributeError, KeyError): pass
-
- try:
- for k, v in cfg['advanced'].items():
- advanced[k] = v
- except (AttributeError, KeyError): pass
-
- try:
- for k, v in cfg['couch'].items():
- couch[k] = v
- except (AttributeError, KeyError): pass
-
- if 'config_version' in cfg:
- config_version = cfg['config_version']
- else:
- config_version = 'unknown'
-
- return basic, couch, advanced, config_version
- else:
- raise MissingConfig("Could not load config file.")
-
-
-## This is the name of the config file to use:
-## If not set, it defaults to 'leap_mx/leap_mx.conf'
-if not filename:
- filename = _get_config_location()
-else:
- filename = _get_config_location(config_filename=filename)
diff --git a/src/leap/mx/util/log.py b/src/leap/mx/util/log.py
deleted file mode 100644
index f31684d..0000000
--- a/src/leap/mx/util/log.py
+++ /dev/null
@@ -1,143 +0,0 @@
-# -*- encoding: utf-8 -*-
-'''
-log.py
-------
-Logging for leap_mx.
-
-@authors: Isis Agora Lovecruft, <isis@leap.se> 0x2cdb8b35
-@licence: see included LICENSE file
-@copyright: 2013 Isis Agora Lovecruft
-'''
-
-from datetime import datetime
-from functools import wraps
-
-import logging
-import os
-import sys
-import time
-import traceback
-
-from twisted.python import log as txlog
-from twisted.python import util as txutil
-from twisted.python import logfile as txlogfile
-from twisted.python.failure import Failure
-
-from leap.mx.util import version, config
-
-
-class InvalidTimestampFormat(Exception):
- pass
-
-class UnprefixedLogfile(txlog.FileLogObserver):
- """Logfile with plain messages, without timestamp prefixes."""
- def emit(self, eventDict):
- text = txlog.textFromEventDict(eventDict)
- if text is None:
- return
-
- txutil.untilConcludes(self.write, "%s\n" % text)
- txutil.untilConcludes(self.flush)
-
-
-def utcDateNow():
- """The current date for UTC time."""
- return datetime.utcnow()
-
-def utcTimeNow():
- """Seconds since epoch in UTC time, as type float."""
- return time.mktime(time.gmtime())
-
-def dateToTime(date):
- """Convert datetime to seconds since epoch."""
- return time.mktime(date.timetuple())
-
-def prettyDateNow():
- """Pretty string for the local time."""
- return datetime.now().ctime()
-
-def utcPrettyDateNow():
- """Pretty string for UTC."""
- return datetime.utcnow().ctime()
-
-def timeToPrettyDate(time_val):
- """Convert seconds since epoch to date."""
- return time.ctime(time_val)
-
-def start(logfilename=None, logfiledir=None):
- """
- Start logging to stdout, and optionally to a logfile as well.
-
- @param logfile: The full path of the filename to store logs in.
- """
- txlog.startLoggingWithObserver(UnprefixedLogfile(sys.stdout).emit)
-
- if logfilename and logfiledir:
- if not os.path.isdir(logfiledir):
- os.makedirs(logfiledir)
- daily_logfile = txlogfile.DailyLogFile(logfilename, logfiledir)
- txlog.addObserver(txlog.FileLogObserver(daily_logfile).emit)
-
- txlog.msg("Starting %s, version %s, on %s UTC" % (version.getPackageName(),
- version.getVersion(),
- utcPrettyDateNow()))
- txlog.msg("Authors: %s" % version.getAuthors())
-
-def msg(msg, *arg, **kwarg):
- """Log a message at the INFO level."""
- print "[*] %s" % msg
-
-def debug(msg, *arg, **kwarg):
- """Log a message at the DEBUG level."""
- if config.advanced.debug:
- print "[d] %s" % msg
-
-def warn(msg, *arg, **kwarg):
- """Log a message at the WARN level."""
- if config.basic.show_warnings:
- txlog.logging.captureWarnings('true')
- print "[#] %s" % msg
-
-def err(msg, *arg, **kwarg):
- """Log a message at the ERROR level."""
- print "[!] %s" % msg
-
-def fail(*failure):
- """Log a message at the CRITICAL level."""
- logging.critical(failure)
- ## xxx should we take steps to exit here?
-
-def exception(error):
- """
- Catch an exception and print only the error message, then continue normal
- program execution.
-
- @param error: Can be error messages printed to stdout and to the
- logfile, or can be a twisted.python.failure.Failure instance.
- """
- if isinstance(error, Failure):
- error.printTraceback()
- else:
- exc_type, exc_value, exc_traceback = sys.exc_info()
- traceback.print_exception(exc_type, exc_value, exc_traceback)
-
-def catch(func):
- """
- Quick wrapper to add around test methods for debugging purposes,
- catches the given Exception. Use like so:
-
- >>> @log.catch
- def foo(bar):
- if bar == 'baz':
- raise Exception("catch me no matter what I am")
- >>> foo("baz")
- [!] catch me no matter what I am
-
- """
- @wraps(func)
- def _catch(*args, **kwargs):
- try:
- func(*args, **kwargs)
- except Exception, exc:
- exception(exc)
- return _catch
diff --git a/src/leap/mx/util/net.py b/src/leap/mx/util/net.py
deleted file mode 100644
index 64dbc90..0000000
--- a/src/leap/mx/util/net.py
+++ /dev/null
@@ -1,126 +0,0 @@
-#!/usr/bin/env python
-# -*- encoding: utf-8 -*-
-'''
-net.py
--------
-Utilities for networking.
-
-@authors: Isis Agora Lovecruft, <isis@leap.se> 0x2cdb8b35
-@license: see included LICENSE file
-@copyright: 2013 Isis Agora Lovecruft
-'''
-
-import ipaddr
-import sys
-import socket
-
-from random import randint
-
-from leap.mx.util import log
-
-
-PLATFORMS = {'LINUX': sys.platform.startswith("linux"),
- 'OPENBSD': sys.platform.startswith("openbsd"),
- 'FREEBSD': sys.platform.startswith("freebsd"),
- 'NETBSD': sys.platform.startswith("netbsd"),
- 'DARWIN': sys.platform.startswith("darwin"),
- 'SOLARIS': sys.platform.startswith("sunos"),
- 'WINDOWS': sys.platform.startswith("win32")}
-
-
-class UnsupportedPlatform(Exception):
- """Support for this platform is not currently available."""
-
-class IfaceError(Exception):
- """Could not find default network interface."""
-
-class PermissionsError(SystemExit):
- """This test requires admin or root privileges to run. Exiting..."""
-
-
-def checkIPaddress(addr):
- """
- Check that a given string is a valid IPv4 or IPv6 address.
-
- @param addr: Any string defining an IP address, i.e. '1.2.3.4' or '::1'.
- @returns: True if :param:`addr` defines a valid IPAddress, else False.
- """
- import ipaddr
-
- try:
- check = ipaddr.IPAddress(addr)
- except ValueError, ve:
- log.warn(ve.message)
- return False
- else:
- return True
-
-def getClientPlatform(platform_name=None):
- for name, test in PLATFORMS.items():
- if not platform_name or platform_name.upper() == name:
- if test:
- return name, test
-
-def getPosixIfaces():
- from twisted.internet.test import _posixifaces
- log.msg("Attempting to discover network interfaces...")
- ifaces = _posixifaces._interfaces()
- return ifaces
-
-def getWindowsIfaces():
- from twisted.internet.test import _win32ifaces
- log.msg("Attempting to discover network interfaces...")
- ifaces = _win32ifaces._interfaces()
- return ifaces
-
-def getIfaces(platform_name=None):
- client, test = getClientPlatform(platform_name)
- if client:
- if client == ('LINUX' or 'DARWIN') or client[-3:] == 'BSD':
- return getPosixIfaces()
- elif client == 'WINDOWS':
- return getWindowsIfaces()
- ## XXX fixme figure out how to get iface for Solaris
- else:
- raise UnsupportedPlatform
- else:
- raise UnsupportedPlatform
-
-def getRandomUnusedPort(addr=None):
- free = False
- while not free:
- port = randint(1024, 65535)
- s = socket.socket()
- try:
- s.bind((addr, port))
- free = True
- except:
- pass
- s.close()
- return port
-
-def getNonLoopbackIfaces(platform_name=None):
- try:
- ifaces = getIfaces(platform_name)
- except UnsupportedPlatform, up:
- log.err(up)
-
- if not ifaces:
- log.msg("Unable to discover network interfaces...")
- return None
- else:
- found = [{i[0]: i[2]} for i in ifaces if i[0] != 'lo']
- log.debug("Found non-loopback interfaces: %s" % found)
- for iface in ifaces:
- try:
- interface = checkInterfaces(found)
- except IfaceError, ie:
- log.err(ie)
- return None
- else:
- return interfaces
-
-
-def getLocalAddress():
- default_iface = getDefaultIface()
- return default_iface.ipaddr
diff --git a/src/leap/mx/util/storage.py b/src/leap/mx/util/storage.py
deleted file mode 100644
index c4c797a..0000000
--- a/src/leap/mx/util/storage.py
+++ /dev/null
@@ -1,42 +0,0 @@
-
-class Storage(dict):
- """
- A Storage object is like a dictionary except `obj.foo` can be used
- in addition to `obj['foo']`.
-
- >>> o = Storage(a=1)
- >>> o.a
- 1
- >>> o['a']
- 1
- >>> o.a = 2
- >>> o['a']
- 2
- >>> del o.a
- >>> o.a
- None
- """
- def __getattr__(self, key):
- try:
- return self[key]
- except KeyError, k:
- return None
-
- def __setattr__(self, key, value):
- self[key] = value
-
- def __delattr__(self, key):
- try:
- del self[key]
- except KeyError, k:
- raise AttributeError, k
-
- def __repr__(self):
- return '<Storage ' + dict.__repr__(self) + '>'
-
- def __getstate__(self):
- return dict(self)
-
- def __setstate__(self, value):
- for (k, v) in value.items():
- self[k] = v