diff options
-rw-r--r-- | changes/feature_4792_repair-mailboxes | 3 | ||||
-rw-r--r-- | src/leap/bitmask/app.py | 8 | ||||
-rw-r--r-- | src/leap/bitmask/services/mail/repair.py | 234 | ||||
-rw-r--r-- | src/leap/bitmask/services/soledad/soledadbootstrapper.py | 54 | ||||
-rw-r--r-- | src/leap/bitmask/util/leap_argparse.py | 6 |
5 files changed, 279 insertions, 26 deletions
diff --git a/changes/feature_4792_repair-mailboxes b/changes/feature_4792_repair-mailboxes new file mode 100644 index 00000000..cb570e8b --- /dev/null +++ b/changes/feature_4792_repair-mailboxes @@ -0,0 +1,3 @@ +- Add --repair-mailboxes command line option. It will be needed to migrate + existing account after a data schema changes, like it will be happening for + 0.5.0. Closes: #4792 diff --git a/src/leap/bitmask/app.py b/src/leap/bitmask/app.py index 3bb9c8c3..3bf4575e 100644 --- a/src/leap/bitmask/app.py +++ b/src/leap/bitmask/app.py @@ -54,7 +54,9 @@ from leap.bitmask.util import log_silencer from leap.bitmask.util.leap_log_handler import LeapLogHandler from leap.bitmask.util.streamtologger import StreamToLogger from leap.bitmask.platform_init import IS_WIN +from leap.bitmask.services.mail.repair import repair_account from leap.common.events import server as event_server +from leap.mail import __version__ as MAIL_VERSION import codecs codecs.register(lambda name: codecs.lookup('utf-8') @@ -170,6 +172,11 @@ def main(): if opts.version: print "Bitmask version: %s" % (VERSION,) + print "leap.mail version: %s" % (MAIL_VERSION,) + sys.exit(0) + + if opts.acct_to_repair: + repair_account(opts.acct_to_repair) sys.exit(0) standalone = opts.standalone @@ -217,6 +224,7 @@ def main(): logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') logger.info('Bitmask version %s', VERSION) + logger.info('leap.mail version %s', MAIL_VERSION) logger.info('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~') logger.info('Starting app') diff --git a/src/leap/bitmask/services/mail/repair.py b/src/leap/bitmask/services/mail/repair.py new file mode 100644 index 00000000..767df1ef --- /dev/null +++ b/src/leap/bitmask/services/mail/repair.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +# repair.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/>. +""" +Utils for repairing mailbox indexes. +""" +import logging +import getpass +import os + +from collections import defaultdict + +from leap.bitmask.config.providerconfig import ProviderConfig +from leap.bitmask.crypto.srpauth import SRPAuth +from leap.bitmask.util import get_path_prefix +from leap.bitmask.services.soledad.soledadbootstrapper import get_db_paths + +from leap.mail.imap.server import SoledadBackedAccount +from leap.soledad.client import Soledad + +logger = logging.getLogger(__name__) +logger.setLevel(logging.DEBUG) + + +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 + """ + # XXX TODO unify with an authoritative source of mocks + # for soledad (or partial initializations). + # This is copied from the imap tests. + + server_url = "http://provider" + cert_file = "" + + class Mock(object): + def __init__(self, return_value=None): + self._return = return_value + + def __call__(self, *args, **kwargs): + return self._return + + class MockSharedDB(object): + + get_doc = Mock() + put_doc = Mock() + lock = Mock(return_value=('atoken', 300)) + unlock = Mock(return_value=True) + + def __call__(self): + return self + + Soledad._shared_db = MockSharedDB() + soledad = Soledad( + uuid, + passwd, + secrets, + localdb, + server_url, + cert_file) + + return soledad + + +class MBOXPlumber(object): + """ + An class that can fix things inside a soledadbacked account. + The idea is to gather in this helper different fixes for mailboxes + that can be invoked when data migration in the client is needed. + """ + + def __init__(self, userid, passwd): + """ + Initializes the plumber with all that's needed to authenticate + against the provider. + + :param userid: user identifier, foo@bar + :type userid: basestring + :param passwd: the soledad passphrase + :type passwd: basestring + """ + self.userid = userid + self.passwd = passwd + user, provider = userid.split('@') + self.user = user + self.sol = None + provider_config_path = os.path.join( + get_path_prefix(), + "leap", "providers", + provider, "provider.json") + provider_config = ProviderConfig() + loaded = provider_config.load(provider_config_path) + if not loaded: + print "could not load provider config!" + return self.exit() + + self.srp = SRPAuth(provider_config) + self.srp.authentication_finished.connect(self.repair_account) + + def start_auth(self): + """ + returns the user identifier for a given provider. + + :param provider: the provider to which we authenticate against. + """ + print "Authenticating with provider..." + self.d = self.srp.authenticate(self.user, self.passwd) + + def repair_account(self, *args): + """ + Gets the user id for this account. + """ + print "Got authenticated." + self.uid = self.srp.get_uid() + if not self.uid: + print "Got BAD UID from provider!" + return self.exit() + print "UID: %s" % (self.uid) + + secrets, localdb = get_db_paths(self.uid) + + self.sol = initialize_soledad( + self.uid, self.userid, self.passwd, + secrets, localdb, "/tmp", "/tmp") + + self.acct = SoledadBackedAccount(self.userid, self.sol) + for mbox_name in self.acct.mailboxes: + self.repair_mbox(mbox_name) + print "done." + self.exit() + + def repair_mbox(self, mbox_name): + """ + Repairs indexes for a given mbox + + :param mbox_name: mailbox to repair + :type mbox_name: basestring + """ + print + print "REPAIRING INDEXES FOR MAILBOX %s" % (mbox_name,) + print "----------------------------------------------" + mbox = self.acct.getMailbox(mbox_name) + len_mbox = mbox.getMessageCount() + print "There are %s messages" % (len_mbox,) + + last_ok = True if mbox.last_uid == len_mbox else False + uids_iter = (doc.content['uid'] for doc in mbox.messages.get_all()) + dupes = self._has_dupes(uids_iter) + if last_ok and not dupes: + print "Mbox does not need repair." + return + + msgs = mbox.messages.get_all() + for zindex, doc in enumerate(msgs): + mindex = zindex + 1 + old_uid = doc.content['uid'] + doc.content['uid'] = mindex + self.sol.put_doc(doc) + print "%s -> %s (%s)" % (mindex, doc.content['uid'], old_uid) + + old_last_uid = mbox.last_uid + mbox.last_uid = len_mbox + print "LAST UID: %s (%s)" % (mbox.last_uid, old_last_uid) + + def _has_dupes(self, sequence): + """ + Returns True if the given sequence of ints has duplicates. + + :param sequence: a sequence of ints + :type sequence: sequence + :rtype: bool + """ + d = defaultdict(lambda: 0) + for uid in sequence: + d[uid] += 1 + if d[uid] != 1: + return True + return False + + def exit(self): + from twisted.internet import reactor + self.d.cancel() + if self.sol: + self.sol.close() + try: + reactor.stop() + except Exception: + pass + return + + +def repair_account(userid): + """ + Starts repair process for a given account. + :param userid: the user id (email-like) + """ + from twisted.internet import reactor + passwd = unicode(getpass.getpass("Passphrase: ")) + + # go mario! + plumber = MBOXPlumber(userid, passwd) + reactor.callLater(1, plumber.start_auth) + reactor.run() + + +if __name__ == "__main__": + import sys + + logging.basicConfig() + + if len(sys.argv) != 2: + print "Usage: repair <username>" + sys.exit(1) + repair_account(sys.argv[1]) diff --git a/src/leap/bitmask/services/soledad/soledadbootstrapper.py b/src/leap/bitmask/services/soledad/soledadbootstrapper.py index d078ae96..a92c24a0 100644 --- a/src/leap/bitmask/services/soledad/soledadbootstrapper.py +++ b/src/leap/bitmask/services/soledad/soledadbootstrapper.py @@ -59,6 +59,33 @@ class SoledadInitError(Exception): message = "Error while initializing Soledad" +def get_db_paths(uuid): + """ + Returns the secrets and local db paths needed for soledad + initialization + + :param uuid: uuid for user + :type uuid: str + + :return: a tuple with secrets, local_db paths + :rtype: tuple + """ + prefix = os.path.join(get_path_prefix(), "leap", "soledad") + secrets = "%s/%s.secret" % (prefix, uuid) + local_db = "%s/%s.db" % (prefix, uuid) + + # We remove an empty file if found to avoid complains + # about the db not being properly initialized + if is_file(local_db) and is_empty_file(local_db): + try: + os.remove(local_db) + except OSError: + logger.warning( + "Could not remove empty file %s" + % local_db) + return secrets, local_db + + class SoledadBootstrapper(AbstractBootstrapper): """ Soledad init procedure @@ -127,31 +154,6 @@ class SoledadBootstrapper(AbstractBootstrapper): """ self._soledad_retries += 1 - def _get_db_paths(self, uuid): - """ - Returns the secrets and local db paths needed for soledad - initialization - - :param uuid: uuid for user - :type uuid: str - - :return: a tuple with secrets, local_db paths - :rtype: tuple - """ - prefix = os.path.join(get_path_prefix(), "leap", "soledad") - secrets = "%s/%s.secret" % (prefix, uuid) - local_db = "%s/%s.db" % (prefix, uuid) - - # We remove an empty file if found to avoid complains - # about the db not being properly initialized - if is_file(local_db) and is_empty_file(local_db): - try: - os.remove(local_db) - except OSError: - logger.warning("Could not remove empty file %s" - % local_db) - return secrets, local_db - # initialization def load_and_sync_soledad(self): @@ -163,7 +165,7 @@ class SoledadBootstrapper(AbstractBootstrapper): uuid = self.srpauth.get_uid() token = self.srpauth.get_token() - secrets_path, local_db_path = self._get_db_paths(uuid) + secrets_path, local_db_path = get_db_paths(uuid) # TODO: Select server based on timezone (issue #3308) server_dict = self._soledad_config.get_hosts() diff --git a/src/leap/bitmask/util/leap_argparse.py b/src/leap/bitmask/util/leap_argparse.py index e8a9fda9..00192247 100644 --- a/src/leap/bitmask/util/leap_argparse.py +++ b/src/leap/bitmask/util/leap_argparse.py @@ -51,6 +51,12 @@ Launches Bitmask""", epilog=epilog) 'searching') parser.add_argument('-V', '--version', action="store_true", help='Displays Bitmask version and exits') + parser.add_argument('-r', '--repair-mailboxes', metavar="user@provider", + nargs='?', + action="store", dest="acct_to_repair", + help='Repair mailboxes for a given account. ' + 'Use when upgrading versions after a schema ' + 'change.') # Not in use, we might want to reintroduce them. #parser.add_argument('-i', '--no-provider-checks', |