def _download_config(self):
        """
        Download the Soledad config for the given provider
        """
        leap_assert(self._provider_config, "We need a provider configuration!")
        logger.debug("Downloading Soledad config for %s" %
                     (self._provider_config.get_domain(), ))

        self._soledad_config = SoledadConfig()
        download_service_config(self._provider_config, self._soledad_config,
                                self._session, self._download_if_needed)
    def _pick_server(self, uuid):
        """
        Choose a soledad server to sync against.

        :param uuid: the uuid for the user.
        :type uuid: unicode
        :returns: the server url
        :rtype: unicode
        """
        if not self._soledad_config:
            self._soledad_config = SoledadConfig()

        # TODO: Select server based on timezone (issue #3308)
        server_dict = self._soledad_config.get_hosts()

        if not server_dict.keys():
            # XXX raise more specific exception, and catch it properly!
            raise Exception("No soledad server found")

        selected_server = server_dict[first(server_dict.keys())]
        server_url = "https://%s:%s/user-%s" % (
            selected_server["hostname"],
            selected_server["port"],
            uuid)
        logger.debug("Using soledad server url: %s" % (server_url,))
        return server_url
    def _download_config(self):
        """
        Download the Soledad config for the given provider
        """
        leap_assert(self._provider_config,
                    "We need a provider configuration!")
        logger.debug("Downloading Soledad config for %s" %
                     (self._provider_config.get_domain(),))

        self._soledad_config = SoledadConfig()
        download_service_config(
            self._provider_config,
            self._soledad_config,
            self._session,
            self._download_if_needed)
    def _pick_server(self, uuid):
        """
        Choose a soledad server to sync against.

        :param uuid: the uuid for the user.
        :type uuid: unicode
        :returns: the server url
        :rtype: unicode
        """
        if not self._soledad_config:
            self._soledad_config = SoledadConfig()

        # TODO: Select server based on timezone (issue #3308)
        server_dict = self._soledad_config.get_hosts()

        if not server_dict.keys():
            # XXX raise more specific exception, and catch it properly!
            raise Exception("No soledad server found")

        selected_server = server_dict[first(server_dict.keys())]
        server_url = "https://%s:%s/user-%s" % (selected_server["hostname"],
                                                selected_server["port"], uuid)
        logger.debug("Using soledad server url: %s" % (server_url, ))
        return server_url
class SoledadBootstrapper(AbstractBootstrapper):
    """
    Soledad init procedure.
    """
    SOLEDAD_KEY = "soledad"
    KEYMANAGER_KEY = "keymanager"

    PUBKEY_KEY = "user[public_key]"

    MAX_INIT_RETRIES = 10

    def __init__(self, signaler=None):
        AbstractBootstrapper.__init__(self, signaler)

        if signaler is not None:
            self._cancel_signal = signaler.soledad_cancelled_bootstrap

        self._provider_config = None
        self._soledad_config = None
        self._download_if_needed = False

        self._user = ""
        self._password = u""
        self._address = ""
        self._uuid = ""

        self._srpauth = None

        self._soledad = None
        self._keymanager = None

    @property
    def srpauth(self):
        if flags.OFFLINE is True:
            return None
        if self._srpauth is None:
            leap_assert(self._provider_config is not None,
                        "We need a provider config")
            self._srpauth = SRPAuth(self._provider_config)
        return self._srpauth

    @property
    def soledad(self):
        return self._soledad

    @property
    def keymanager(self):
        return self._keymanager

    # initialization

    def load_offline_soledad(self, username, password, uuid):
        """
        Instantiate Soledad for offline use.

        :param username: full user id (user@provider)
        :type username: str or unicode
        :param password: the soledad passphrase
        :type password: unicode
        :param uuid: the user uuid
        :type uuid: str or unicode
        """
        self._address = username
        self._password = password
        self._uuid = uuid

        def error(failure):
            # TODO: we should handle more specific exceptions in here
            logger.exception(failure.value)
            self._signaler.signal(self._signaler.soledad_offline_failed)

        d = self.load_and_sync_soledad(uuid, offline=True)
        d.addCallback(
            lambda _: self._signaler.signal(
                self._signaler.soledad_offline_finished))
        d.addErrback(error)
        return d

    def _get_soledad_local_params(self, uuid, offline=False):
        """
        Return the locals parameters needed for the soledad initialization.

        :param uuid: the uuid of the user, used in offline mode.
        :type uuid: unicode, or None.
        :return: secrets_path, local_db_path, token
        :rtype: tuple
        """
        # in the future, when we want to be able to switch to
        # online mode, this should be a proxy object too.
        # Same for server_url below.

        if offline is False:
            token = self.srpauth.get_token()
        else:
            token = ""

        secrets_path, local_db_path = get_db_paths(uuid)

        logger.debug('secrets_path:%s' % (secrets_path,))
        logger.debug('local_db:%s' % (local_db_path,))
        return (secrets_path, local_db_path, token)

    def _get_soledad_server_params(self, uuid, offline):
        """
        Return the remote parameters needed for the soledad initialization.

        :param uuid: the uuid of the user, used in offline mode.
        :type uuid: unicode, or None.
        :return: server_url, cert_file
        :rtype: tuple
        """
        if offline is True:
            server_url = "http://localhost:9999/"
            cert_file = ""
        else:
            if uuid is None:
                uuid = self.srpauth.get_uuid()
            server_url = self._pick_server(uuid)
            cert_file = self._provider_config.get_ca_cert_path()

        return server_url, cert_file

    def _do_soledad_init(self, uuid, secrets_path, local_db_path,
                         server_url, cert_file, token, syncable):
        """
        Initialize soledad, retry if necessary and raise an exception if we
        can't succeed.

        :param uuid: user identifier
        :type uuid: str
        :param secrets_path: path to secrets file
        :type secrets_path: str
        :param local_db_path: path to local db file
        :type local_db_path: str
        :param server_url: soledad server uri
        :type server_url: str
        :param cert_file: path to the certificate of the ca used
                          to validate the SSL certificate used by the remote
                          soledad server.
        :type cert_file: str
        :param auth token: auth token
        :type auth_token: str
        """
        init_tries = 1
        while init_tries <= self.MAX_INIT_RETRIES:
            try:
                logger.debug("Trying to init soledad....")
                self._try_soledad_init(
                    uuid, secrets_path, local_db_path,
                    server_url, cert_file, token, syncable)
                logger.debug("Soledad has been initialized.")
                return
            except Exception as exc:
                init_tries += 1
                msg = "Init failed, retrying... (retry {0} of {1})".format(
                    init_tries, self.MAX_INIT_RETRIES)
                logger.warning(msg)
                continue

        self._signaler.signal(self._signaler.soledad_bootstrap_failed)
        logger.exception(exc)
        raise SoledadInitError()

    def load_and_sync_soledad(self, uuid=u"", offline=False):
        """
        Once everthing is in the right place, we instantiate and sync
        Soledad

        :param uuid: the uuid of the user, used in offline mode.
        :type uuid: unicode.
        :param offline: whether to instantiate soledad for offline use.
        :type offline: bool

        :return: A Deferred which fires when soledad is sync, or which fails
                 with SoledadInitError or SoledadSyncError
        :rtype: defer.Deferred
        """
        local_param = self._get_soledad_local_params(uuid, offline)
        remote_param = self._get_soledad_server_params(uuid, offline)

        secrets_path, local_db_path, token = local_param
        server_url, cert_file = remote_param

        if offline:
            return self._load_soledad_nosync(
                uuid, secrets_path, local_db_path, cert_file, token)

        else:
            return self._load_soledad_online(uuid, secrets_path, local_db_path,
                                             server_url, cert_file, token)

    def _load_soledad_online(self, uuid, secrets_path, local_db_path,
                             server_url, cert_file, token):
        syncable = True
        try:
            self._do_soledad_init(uuid, secrets_path, local_db_path,
                                  server_url, cert_file, token, syncable)
        except SoledadInitError as e:
            # re-raise the exceptions from try_init,
            # we're currently handling the retries from the
            # soledad-launcher in the gui.
            return defer.fail(e)

        leap_assert(not sameProxiedObjects(self._soledad, None),
                    "Null soledad, error while initializing")

        address = make_address(
            self._user, self._provider_config.get_domain())
        syncer = Syncer(self._soledad, self._signaler)

        d = self._init_keymanager(address, token)
        d.addCallback(lambda _: syncer.sync())
        d.addErrback(self._soledad_sync_errback)
        return d

    def _load_soledad_nosync(self, uuid, secrets_path, local_db_path,
                             cert_file, token):
        syncable = False
        self._do_soledad_init(uuid, secrets_path, local_db_path,
                              "", cert_file, token, syncable)
        d = self._init_keymanager(self._address, token)
        return d

    def _soledad_sync_errback(self, failure):
        failure.trap(InvalidAuthTokenError)
        # in the case of an invalid token we have already turned off mail and
        # warned the user

    def _pick_server(self, uuid):
        """
        Choose a soledad server to sync against.

        :param uuid: the uuid for the user.
        :type uuid: unicode
        :returns: the server url
        :rtype: unicode
        """
        if not self._soledad_config:
            self._soledad_config = SoledadConfig()

        # TODO: Select server based on timezone (issue #3308)
        server_dict = self._soledad_config.get_hosts()

        if not server_dict.keys():
            # XXX raise more specific exception, and catch it properly!
            raise Exception("No soledad server found")

        selected_server = server_dict[first(server_dict.keys())]
        server_url = "https://%s:%s/user-%s" % (
            selected_server["hostname"],
            selected_server["port"],
            uuid)
        logger.debug("Using soledad server url: %s" % (server_url,))
        return server_url

    def _try_soledad_init(self, uuid, secrets_path, local_db_path,
                          server_url, cert_file, auth_token, syncable):
        """
        Try to initialize soledad.

        :param uuid: user identifier
        :type uuid: str
        :param secrets_path: path to secrets file
        :type secrets_path: str
        :param local_db_path: path to local db file
        :type local_db_path: str
        :param server_url: soledad server uri
        :type server_url: str
        :param cert_file: path to the certificate of the ca used
                          to validate the SSL certificate used by the remote
                          soledad server.
        :type cert_file: str
        :param auth token: auth token
        :type auth_token: str
        """
        # TODO: If selected server fails, retry with another host
        # (issue #3309)
        encoding = sys.getfilesystemencoding()

        try:
            self._soledad = Soledad(
                uuid,
                self._password,
                secrets_path=secrets_path.encode(encoding),
                local_db_path=local_db_path.encode(encoding),
                server_url=server_url,
                cert_file=cert_file.encode(encoding),
                auth_token=auth_token,
                defer_encryption=True,
                syncable=syncable)

        # XXX All these errors should be handled by soledad itself,
        # and return a subclass of SoledadInitializationFailed

        # recoverable, will guarantee retries
        except (socket.timeout, socket.error, BootstrapSequenceError):
            logger.warning("Error while initializing Soledad")
            raise

        # unrecoverable
        except (u1db_errors.Unauthorized, u1db_errors.HTTPError):
            logger.error("Error while initializing Soledad (u1db error).")
            raise
        except Exception as exc:
            logger.exception("Unhandled error while initializating "
                             "Soledad: %r" % (exc,))
            raise

    def _download_config(self):
        """
        Download the Soledad config for the given provider
        """
        leap_assert(self._provider_config,
                    "We need a provider configuration!")
        logger.debug("Downloading Soledad config for %s" %
                     (self._provider_config.get_domain(),))

        self._soledad_config = SoledadConfig()
        download_service_config(
            self._provider_config,
            self._soledad_config,
            self._session,
            self._download_if_needed)

    def _get_gpg_bin_path(self):
        """
        Return the path to gpg binary.

        :returns: the gpg binary path
        :rtype: str
        """
        gpgbin = None
        if flags.STANDALONE:
            gpgbin = os.path.join(
                get_path_prefix(), "..", "apps", "mail", "gpg")
            if IS_WIN:
                gpgbin += ".exe"
        else:
            try:
                gpgbin_options = which("gpg")
                # gnupg checks that the path to the binary is not a
                # symlink, so we need to filter those and come up with
                # just one option.
                for opt in gpgbin_options:
                    if not os.path.islink(opt):
                        gpgbin = opt
                        break
            except IndexError as e:
                logger.debug("Couldn't find the gpg binary!")
                logger.exception(e)
        if IS_MAC:
            gpgbin = os.path.abspath(
                os.path.join(here(), "apps", "mail", "gpg"))

        # During the transition towards gpg2, we can look for /usr/bin/gpg1
        # binary, in case it was renamed using dpkg-divert or manually.
        # We could just pick gpg2, but we need to solve #7564 first.
        if gpgbin is None:
            try:
                gpgbin_options = which("gpg1")
                for opt in gpgbin_options:
                    if not os.path.islink(opt):
                        gpgbin = opt
                        break
            except IndexError as e:
                logger.debug("Couldn't find the gpg1 binary!")
                logger.exception(e)
        leap_check(gpgbin is not None, "Could not find gpg1 binary")
        return gpgbin

    def _init_keymanager(self, address, token):
        """
        Initialize the keymanager.

        :param address: the address to initialize the keymanager with.
        :type address: str
        :param token: the auth token for accessing webapp.
        :type token: str
        :rtype: Deferred
        """
        logger.debug('initializing keymanager...')

        if flags.OFFLINE:
            nickserver_uri = "https://localhost"
            kwargs = {
                "ca_cert_path": "",
                "api_uri": "",
                "api_version": "",
                "uid": self._uuid,
                "gpgbinary": self._get_gpg_bin_path()
            }
        else:
            nickserver_uri = "https://nicknym.%s:6425" % (
                self._provider_config.get_domain(),)
            kwargs = {
                "token": token,
                "ca_cert_path": self._provider_config.get_ca_cert_path(),
                "api_uri": self._provider_config.get_api_uri(),
                "api_version": self._provider_config.get_api_version(),
                "uid": self.srpauth.get_uuid(),
                "gpgbinary": self._get_gpg_bin_path()
            }
        self._keymanager = KeyManager(address, nickserver_uri, self._soledad,
                                      **kwargs)

        if flags.OFFLINE is False:
            # make sure key is in server
            logger.debug('Trying to send key to server...')

            def send_errback(failure):
                if failure.check(KeyNotFound):
                    logger.debug(
                        'No key found for %s, it might be because soledad not '
                        'synced yet or it will generate it soon.' % address)
                else:
                    logger.error("Error sending key to server.")
                    logger.exception(failure.value)
                    # but we do not raise

            d = self._keymanager.send_key(openpgp.OpenPGPKey)
            d.addErrback(send_errback)
            return d
        else:
            return defer.succeed(None)

    def _gen_key(self):
        """
        Generates the key pair if needed, uploads it to the webapp and
        nickserver
        :rtype: Deferred
        """
        leap_assert(self._provider_config is not None,
                    "We need a provider configuration!")
        leap_assert(self._soledad is not None,
                    "We need a non-null soledad to generate keys")

        address = make_address(
            self._user, self._provider_config.get_domain())
        logger.debug("Retrieving key for %s" % (address,))

        def if_not_found_generate(failure):
            failure.trap(KeyNotFound)
            logger.debug("Key not found. Generating key for %s"
                         % (address,))
            d = self._keymanager.gen_key(openpgp.OpenPGPKey)
            d.addCallbacks(send_key, log_key_error("generating"))
            return d

        def send_key(_):
            d = self._keymanager.send_key(openpgp.OpenPGPKey)
            d.addCallbacks(
                lambda _: logger.debug("Key generated successfully."),
                log_key_error("sending"))

        def log_key_error(step):
            def log_err(failure):
                logger.error("Error while %s key!", (step,))
                logger.exception(failure.value)
                return failure
            return log_err

        d = self._keymanager.get_key(
            address, openpgp.OpenPGPKey, private=True, fetch_remote=False)
        d.addErrback(if_not_found_generate)
        return d

    def run_soledad_setup_checks(self, provider_config, user, password,
                                 download_if_needed=False):
        """
        Starts the checks needed for a new soledad setup

        :param provider_config: Provider configuration
        :type provider_config: ProviderConfig
        :param user: User's login
        :type user: unicode
        :param password: User's password
        :type password: unicode
        :param download_if_needed: If True, it will only download
                                   files if the have changed since the
                                   time it was previously downloaded.
        :type download_if_needed: bool

        :return: Deferred
        """
        leap_assert_type(provider_config, ProviderConfig)

        # XXX we should provider a method for setting provider_config
        self._provider_config = provider_config
        self._download_if_needed = download_if_needed
        self._user = user
        self._password = password

        if flags.OFFLINE:
            signal_finished = self._signaler.soledad_offline_finished
            self._signaler.signal(signal_finished)
            return defer.succeed(True)

        signal_finished = self._signaler.soledad_bootstrap_finished
        signal_failed = self._signaler.soledad_bootstrap_failed

        try:
            # XXX FIXME make this async too! (use txrequests)
            # Also, why the f**k would we want to download it *every time*?
            # We should be fine by using last-time config, or at least
            # trying it.
            self._download_config()
            uuid = self.srpauth.get_uuid()
        except Exception as e:
            # TODO: we should handle more specific exceptions in here
            self._soledad = None
            self._keymanager = None
            logger.exception("Error while bootstrapping Soledad: %r" % (e,))
            self._signaler.signal(signal_failed)
            return defer.succeed(None)

        # soledad config is ok, let's proceed to load and sync soledad
        d = self.load_and_sync_soledad(uuid)
        d.addCallback(lambda _: self._gen_key())
        d.addCallback(lambda _: self._signaler.signal(signal_finished))
        return d
class SoledadBootstrapper(AbstractBootstrapper):
    """
    Soledad init procedure.
    """
    SOLEDAD_KEY = "soledad"
    KEYMANAGER_KEY = "keymanager"

    PUBKEY_KEY = "user[public_key]"

    MAX_INIT_RETRIES = 10

    def __init__(self, signaler=None):
        AbstractBootstrapper.__init__(self, signaler)

        if signaler is not None:
            self._cancel_signal = signaler.soledad_cancelled_bootstrap

        self._provider_config = None
        self._soledad_config = None
        self._download_if_needed = False

        self._user = ""
        self._password = u""
        self._address = ""
        self._uuid = ""

        self._srpauth = None

        self._soledad = None
        self._keymanager = None

    @property
    def srpauth(self):
        if flags.OFFLINE is True:
            return None
        if self._srpauth is None:
            leap_assert(self._provider_config is not None,
                        "We need a provider config")
            self._srpauth = SRPAuth(self._provider_config)
        return self._srpauth

    @property
    def soledad(self):
        return self._soledad

    @property
    def keymanager(self):
        return self._keymanager

    # initialization

    def load_offline_soledad(self, username, password, uuid):
        """
        Instantiate Soledad for offline use.

        :param username: full user id (user@provider)
        :type username: str or unicode
        :param password: the soledad passphrase
        :type password: unicode
        :param uuid: the user uuid
        :type uuid: str or unicode
        """
        self._address = username
        self._password = password
        self._uuid = uuid

        def error(failure):
            # TODO: we should handle more specific exceptions in here
            logger.exception(failure.value)
            self._signaler.signal(self._signaler.soledad_offline_failed)

        d = self.load_and_sync_soledad(uuid, offline=True)
        d.addCallback(lambda _: self._signaler.signal(
            self._signaler.soledad_offline_finished))
        d.addErrback(error)
        return d

    def _get_soledad_local_params(self, uuid, offline=False):
        """
        Return the locals parameters needed for the soledad initialization.

        :param uuid: the uuid of the user, used in offline mode.
        :type uuid: unicode, or None.
        :return: secrets_path, local_db_path, token
        :rtype: tuple
        """
        # in the future, when we want to be able to switch to
        # online mode, this should be a proxy object too.
        # Same for server_url below.

        if offline is False:
            token = self.srpauth.get_token()
        else:
            token = ""

        secrets_path, local_db_path = get_db_paths(uuid)

        logger.debug('secrets_path:%s' % (secrets_path, ))
        logger.debug('local_db:%s' % (local_db_path, ))
        return (secrets_path, local_db_path, token)

    def _get_soledad_server_params(self, uuid, offline):
        """
        Return the remote parameters needed for the soledad initialization.

        :param uuid: the uuid of the user, used in offline mode.
        :type uuid: unicode, or None.
        :return: server_url, cert_file
        :rtype: tuple
        """
        if offline is True:
            server_url = "http://localhost:9999/"
            cert_file = ""
        else:
            if uuid is None:
                uuid = self.srpauth.get_uuid()
            server_url = self._pick_server(uuid)
            cert_file = self._provider_config.get_ca_cert_path()

        return server_url, cert_file

    def _do_soledad_init(self, uuid, secrets_path, local_db_path, server_url,
                         cert_file, token, syncable):
        """
        Initialize soledad, retry if necessary and raise an exception if we
        can't succeed.

        :param uuid: user identifier
        :type uuid: str
        :param secrets_path: path to secrets file
        :type secrets_path: str
        :param local_db_path: path to local db file
        :type local_db_path: str
        :param server_url: soledad server uri
        :type server_url: str
        :param cert_file: path to the certificate of the ca used
                          to validate the SSL certificate used by the remote
                          soledad server.
        :type cert_file: str
        :param auth token: auth token
        :type auth_token: str
        """
        init_tries = 1
        while init_tries <= self.MAX_INIT_RETRIES:
            try:
                logger.debug("Trying to init soledad....")
                self._try_soledad_init(uuid, secrets_path, local_db_path,
                                       server_url, cert_file, token, syncable)
                logger.debug("Soledad has been initialized.")
                return
            except Exception as exc:
                init_tries += 1
                msg = "Init failed, retrying... (retry {0} of {1})".format(
                    init_tries, self.MAX_INIT_RETRIES)
                logger.warning(msg)
                continue

        self._signaler.signal(self._signaler.soledad_bootstrap_failed)
        logger.exception(exc)
        raise SoledadInitError()

    def load_and_sync_soledad(self, uuid=u"", offline=False):
        """
        Once everthing is in the right place, we instantiate and sync
        Soledad

        :param uuid: the uuid of the user, used in offline mode.
        :type uuid: unicode.
        :param offline: whether to instantiate soledad for offline use.
        :type offline: bool

        :return: A Deferred which fires when soledad is sync, or which fails
                 with SoledadInitError or SoledadSyncError
        :rtype: defer.Deferred
        """
        local_param = self._get_soledad_local_params(uuid, offline)
        remote_param = self._get_soledad_server_params(uuid, offline)

        secrets_path, local_db_path, token = local_param
        server_url, cert_file = remote_param

        if offline:
            return self._load_soledad_nosync(uuid, secrets_path, local_db_path,
                                             cert_file, token)

        else:
            return self._load_soledad_online(uuid, secrets_path, local_db_path,
                                             server_url, cert_file, token)

    def _load_soledad_online(self, uuid, secrets_path, local_db_path,
                             server_url, cert_file, token):
        syncable = True
        try:
            self._do_soledad_init(uuid, secrets_path, local_db_path,
                                  server_url, cert_file, token, syncable)
        except SoledadInitError as e:
            # re-raise the exceptions from try_init,
            # we're currently handling the retries from the
            # soledad-launcher in the gui.
            return defer.fail(e)

        leap_assert(not sameProxiedObjects(self._soledad, None),
                    "Null soledad, error while initializing")

        address = make_address(self._user, self._provider_config.get_domain())
        syncer = Syncer(self._soledad, self._signaler)

        d = self._init_keymanager(address, token)
        d.addCallback(lambda _: syncer.sync())
        d.addErrback(self._soledad_sync_errback)
        return d

    def _load_soledad_nosync(self, uuid, secrets_path, local_db_path,
                             cert_file, token):
        syncable = False
        self._do_soledad_init(uuid, secrets_path, local_db_path, "", cert_file,
                              token, syncable)
        d = self._init_keymanager(self._address, token)
        return d

    def _soledad_sync_errback(self, failure):
        failure.trap(InvalidAuthTokenError)
        # in the case of an invalid token we have already turned off mail and
        # warned the user

    def _pick_server(self, uuid):
        """
        Choose a soledad server to sync against.

        :param uuid: the uuid for the user.
        :type uuid: unicode
        :returns: the server url
        :rtype: unicode
        """
        if not self._soledad_config:
            self._soledad_config = SoledadConfig()

        # TODO: Select server based on timezone (issue #3308)
        server_dict = self._soledad_config.get_hosts()

        if not server_dict.keys():
            # XXX raise more specific exception, and catch it properly!
            raise Exception("No soledad server found")

        selected_server = server_dict[first(server_dict.keys())]
        server_url = "https://%s:%s/user-%s" % (selected_server["hostname"],
                                                selected_server["port"], uuid)
        logger.debug("Using soledad server url: %s" % (server_url, ))
        return server_url

    def _try_soledad_init(self, uuid, secrets_path, local_db_path, server_url,
                          cert_file, auth_token, syncable):
        """
        Try to initialize soledad.

        :param uuid: user identifier
        :type uuid: str
        :param secrets_path: path to secrets file
        :type secrets_path: str
        :param local_db_path: path to local db file
        :type local_db_path: str
        :param server_url: soledad server uri
        :type server_url: str
        :param cert_file: path to the certificate of the ca used
                          to validate the SSL certificate used by the remote
                          soledad server.
        :type cert_file: str
        :param auth token: auth token
        :type auth_token: str
        """
        # TODO: If selected server fails, retry with another host
        # (issue #3309)
        encoding = sys.getfilesystemencoding()

        try:
            self._soledad = Soledad(
                uuid,
                self._password,
                secrets_path=secrets_path.encode(encoding),
                local_db_path=local_db_path.encode(encoding),
                server_url=server_url,
                cert_file=cert_file.encode(encoding),
                auth_token=auth_token,
                defer_encryption=True,
                syncable=syncable)

        # XXX All these errors should be handled by soledad itself,
        # and return a subclass of SoledadInitializationFailed

        # recoverable, will guarantee retries
        except (socket.timeout, socket.error, BootstrapSequenceError):
            logger.warning("Error while initializing Soledad")
            raise

        # unrecoverable
        except (u1db_errors.Unauthorized, u1db_errors.HTTPError):
            logger.error("Error while initializing Soledad (u1db error).")
            raise
        except Exception as exc:
            logger.exception("Unhandled error while initializating "
                             "Soledad: %r" % (exc, ))
            raise

    def _download_config(self):
        """
        Download the Soledad config for the given provider
        """
        leap_assert(self._provider_config, "We need a provider configuration!")
        logger.debug("Downloading Soledad config for %s" %
                     (self._provider_config.get_domain(), ))

        self._soledad_config = SoledadConfig()
        download_service_config(self._provider_config, self._soledad_config,
                                self._session, self._download_if_needed)

    def _get_gpg_bin_path(self):
        """
        Return the path to gpg binary.

        :returns: the gpg binary path
        :rtype: str
        """
        gpgbin = None
        if flags.STANDALONE:
            gpgbin = os.path.join(get_path_prefix(), "..", "apps", "mail",
                                  "gpg")
            if IS_WIN:
                gpgbin += ".exe"
        else:
            try:
                gpgbin_options = which("gpg")
                # gnupg checks that the path to the binary is not a
                # symlink, so we need to filter those and come up with
                # just one option.
                for opt in gpgbin_options:
                    if not os.path.islink(opt):
                        gpgbin = opt
                        break
            except IndexError as e:
                logger.debug("Couldn't find the gpg binary!")
                logger.exception(e)
        if IS_MAC:
            gpgbin = os.path.abspath(
                os.path.join(here(), "apps", "mail", "gpg"))

        # During the transition towards gpg2, we can look for /usr/bin/gpg1
        # binary, in case it was renamed using dpkg-divert or manually.
        # We could just pick gpg2, but we need to solve #7564 first.
        if gpgbin is None:
            try:
                gpgbin_options = which("gpg1")
                for opt in gpgbin_options:
                    if not os.path.islink(opt):
                        gpgbin = opt
                        break
            except IndexError as e:
                logger.debug("Couldn't find the gpg1 binary!")
                logger.exception(e)
        leap_check(gpgbin is not None, "Could not find gpg1 binary")
        return gpgbin

    def _init_keymanager(self, address, token):
        """
        Initialize the keymanager.

        :param address: the address to initialize the keymanager with.
        :type address: str
        :param token: the auth token for accessing webapp.
        :type token: str
        :rtype: Deferred
        """
        logger.debug('initializing keymanager...')

        if flags.OFFLINE:
            nickserver_uri = "https://localhost"
            kwargs = {
                "ca_cert_path": "",
                "api_uri": "",
                "api_version": "",
                "uid": self._uuid,
                "gpgbinary": self._get_gpg_bin_path()
            }
        else:
            nickserver_uri = "https://nicknym.%s:6425" % (
                self._provider_config.get_domain(), )
            kwargs = {
                "token": token,
                "ca_cert_path": self._provider_config.get_ca_cert_path(),
                "api_uri": self._provider_config.get_api_uri(),
                "api_version": self._provider_config.get_api_version(),
                "uid": self.srpauth.get_uuid(),
                "gpgbinary": self._get_gpg_bin_path()
            }
        self._keymanager = KeyManager(address, nickserver_uri, self._soledad,
                                      **kwargs)

        if flags.OFFLINE is False:
            # make sure key is in server
            logger.debug('Trying to send key to server...')

            def send_errback(failure):
                if failure.check(KeyNotFound):
                    logger.debug(
                        'No key found for %s, it might be because soledad not '
                        'synced yet or it will generate it soon.' % address)
                else:
                    logger.error("Error sending key to server.")
                    logger.exception(failure.value)
                    # but we do not raise

            d = self._keymanager.send_key(openpgp.OpenPGPKey)
            d.addErrback(send_errback)
            return d
        else:
            return defer.succeed(None)

    def _gen_key(self):
        """
        Generates the key pair if needed, uploads it to the webapp and
        nickserver
        :rtype: Deferred
        """
        leap_assert(self._provider_config is not None,
                    "We need a provider configuration!")
        leap_assert(self._soledad is not None,
                    "We need a non-null soledad to generate keys")

        address = make_address(self._user, self._provider_config.get_domain())
        logger.debug("Retrieving key for %s" % (address, ))

        def if_not_found_generate(failure):
            failure.trap(KeyNotFound)
            logger.debug("Key not found. Generating key for %s" % (address, ))
            d = self._keymanager.gen_key(openpgp.OpenPGPKey)
            d.addCallbacks(send_key, log_key_error("generating"))
            return d

        def send_key(_):
            d = self._keymanager.send_key(openpgp.OpenPGPKey)
            d.addCallbacks(
                lambda _: logger.debug("Key generated successfully."),
                log_key_error("sending"))

        def log_key_error(step):
            def log_err(failure):
                logger.error("Error while %s key!", (step, ))
                logger.exception(failure.value)
                return failure

            return log_err

        d = self._keymanager.get_key(address,
                                     openpgp.OpenPGPKey,
                                     private=True,
                                     fetch_remote=False)
        d.addErrback(if_not_found_generate)
        return d

    def run_soledad_setup_checks(self,
                                 provider_config,
                                 user,
                                 password,
                                 download_if_needed=False):
        """
        Starts the checks needed for a new soledad setup

        :param provider_config: Provider configuration
        :type provider_config: ProviderConfig
        :param user: User's login
        :type user: unicode
        :param password: User's password
        :type password: unicode
        :param download_if_needed: If True, it will only download
                                   files if the have changed since the
                                   time it was previously downloaded.
        :type download_if_needed: bool

        :return: Deferred
        """
        leap_assert_type(provider_config, ProviderConfig)

        # XXX we should provider a method for setting provider_config
        self._provider_config = provider_config
        self._download_if_needed = download_if_needed
        self._user = user
        self._password = password

        if flags.OFFLINE:
            signal_finished = self._signaler.soledad_offline_finished
            self._signaler.signal(signal_finished)
            return defer.succeed(True)

        signal_finished = self._signaler.soledad_bootstrap_finished
        signal_failed = self._signaler.soledad_bootstrap_failed

        try:
            # XXX FIXME make this async too! (use txrequests)
            # Also, why the f**k would we want to download it *every time*?
            # We should be fine by using last-time config, or at least
            # trying it.
            self._download_config()
            uuid = self.srpauth.get_uuid()
        except Exception as e:
            # TODO: we should handle more specific exceptions in here
            self._soledad = None
            self._keymanager = None
            logger.exception("Error while bootstrapping Soledad: %r" % (e, ))
            self._signaler.signal(signal_failed)
            return defer.succeed(None)

        # soledad config is ok, let's proceed to load and sync soledad
        d = self.load_and_sync_soledad(uuid)
        d.addCallback(lambda _: self._gen_key())
        d.addCallback(lambda _: self._signaler.signal(signal_finished))
        return d
class SoledadBootstrapper(AbstractBootstrapper):
    """
    Soledad init procedure
    """

    SOLEDAD_KEY = "soledad"
    KEYMANAGER_KEY = "keymanager"

    PUBKEY_KEY = "user[public_key]"

    MAX_INIT_RETRIES = 10

    # All dicts returned are of the form
    # {"passed": bool, "error": str}
    download_config = QtCore.Signal(dict)
    gen_key = QtCore.Signal(dict)
    soledad_timeout = QtCore.Signal()
    soledad_failed = QtCore.Signal()

    def __init__(self):
        AbstractBootstrapper.__init__(self)

        self._provider_config = None
        self._soledad_config = None
        self._keymanager = None
        self._download_if_needed = False
        self._user = ""
        self._password = ""
        self._srpauth = None
        self._soledad = None

        self._soledad_retries = 0

    @property
    def keymanager(self):
        return self._keymanager

    @property
    def soledad(self):
        return self._soledad

    @property
    def srpauth(self):
        leap_assert(self._provider_config is not None,
                    "We need a provider config")
        return SRPAuth(self._provider_config)

    # retries

    def cancel_bootstrap(self):
        self._soledad_retries = self.MAX_INIT_RETRIES

    def should_retry_initialization(self):
        """
        Returns True if we should retry the initialization.
        """
        logger.debug("current retries: %s, max retries: %s" % (
            self._soledad_retries,
            self.MAX_INIT_RETRIES))
        return self._soledad_retries < self.MAX_INIT_RETRIES

    def increment_retries_count(self):
        """
        Increments the count of initialization retries.
        """
        self._soledad_retries += 1

    # initialization

    def load_and_sync_soledad(self):
        """
        Once everthing is in the right place, we instantiate and sync
        Soledad

        :param srp_auth: SRPAuth object used
        :type srp_auth: SRPAuth
        """
        srp_auth = self.srpauth
        uuid = srp_auth.get_uid()

        prefix = os.path.join(self._soledad_config.get_path_prefix(),
                              "leap", "soledad")
        secrets_path = "%s/%s.secret" % (prefix, uuid)
        local_db_path = "%s/%s.db" % (prefix, uuid)

        # TODO: Select server based on timezone (issue #3308)
        server_dict = self._soledad_config.get_hosts()

        if server_dict.keys():
            selected_server = server_dict[server_dict.keys()[0]]
            server_url = "https://%s:%s/user-%s" % (
                selected_server["hostname"],
                selected_server["port"],
                uuid)

            logger.debug("Using soledad server url: %s" % (server_url,))

            cert_file = self._provider_config.get_ca_cert_path()

            # TODO: If selected server fails, retry with another host
            # (issue #3309)
            try:
                self._soledad = Soledad(
                    uuid,
                    self._password.encode("utf-8"),
                    secrets_path=secrets_path,
                    local_db_path=local_db_path,
                    server_url=server_url,
                    cert_file=cert_file,
                    auth_token=srp_auth.get_token())
                self._soledad.sync()

            # XXX All these errors should be handled by soledad itself,
            # and return a subclass of SoledadInitializationFailed
            except socket.timeout:
                logger.debug("SOLEDAD TIMED OUT...")
                self.soledad_timeout.emit()
            except socket.error as exc:
                logger.error("Socket error while initializing soledad")
                self.soledad_failed.emit()
            except u1db_errors.Unauthorized:
                logger.error("Error while initializing soledad "
                             "(unauthorized).")
                self.soledad_failed.emit()
            except Exception as exc:
                logger.error("Unhandled error while initializating "
                             "soledad: %r" % (exc,))
                raise
        else:
            raise Exception("No soledad server found")

    def _download_config(self):
        """
        Downloads the Soledad config for the given provider
        """

        leap_assert(self._provider_config,
                    "We need a provider configuration!")

        logger.debug("Downloading Soledad config for %s" %
                     (self._provider_config.get_domain(),))

        self._soledad_config = SoledadConfig()

        headers = {}
        mtime = get_mtime(
            os.path.join(
                self._soledad_config.get_path_prefix(),
                "leap", "providers",
                self._provider_config.get_domain(),
                "soledad-service.json"))

        if self._download_if_needed and mtime:
            headers['if-modified-since'] = mtime

        api_version = self._provider_config.get_api_version()

        # there is some confusion with this uri,
        config_uri = "%s/%s/config/soledad-service.json" % (
            self._provider_config.get_api_uri(),
            api_version)
        logger.debug('Downloading soledad config from: %s' % config_uri)

        # TODO factor out this srpauth protected get (make decorator)
        srp_auth = self.srpauth
        session_id = srp_auth.get_session_id()
        cookies = None
        if session_id:
            cookies = {"_session_id": session_id}

        res = self._session.get(config_uri,
                                verify=self._provider_config
                                .get_ca_cert_path(),
                                headers=headers,
                                cookies=cookies)
        res.raise_for_status()

        self._soledad_config.set_api_version(api_version)

        # Not modified
        if res.status_code == 304:
            logger.debug("Soledad definition has not been modified")
            self._soledad_config.load(
                os.path.join(
                    "leap", "providers",
                    self._provider_config.get_domain(),
                    "soledad-service.json"))
        else:
            soledad_definition, mtime = get_content(res)

            self._soledad_config.load(data=soledad_definition, mtime=mtime)
            self._soledad_config.save(["leap",
                                       "providers",
                                       self._provider_config.get_domain(),
                                       "soledad-service.json"])

        self.load_and_sync_soledad()

    def _gen_key(self, _):
        """
        Generates the key pair if needed, uploads it to the webapp and
        nickserver
        """
        leap_assert(self._provider_config,
                    "We need a provider configuration!")

        address = "%s@%s" % (self._user, self._provider_config.get_domain())

        logger.debug("Retrieving key for %s" % (address,))

        srp_auth = self.srpauth

        # TODO: use which implementation with known paths
        # TODO: Fix for Windows
        gpgbin = "/usr/bin/gpg"

        if self._standalone:
            gpgbin = os.path.join(self._provider_config.get_path_prefix(),
                                  "..", "apps", "mail", "gpg")

        self._keymanager = KeyManager(
            address,
            "https://nicknym.%s:6425" % (self._provider_config.get_domain(),),
            self._soledad,
            #token=srp_auth.get_token(),  # TODO: enable token usage
            session_id=srp_auth.get_session_id(),
            ca_cert_path=self._provider_config.get_ca_cert_path(),
            api_uri=self._provider_config.get_api_uri(),
            api_version=self._provider_config.get_api_version(),
            uid=srp_auth.get_uid(),
            gpgbinary=gpgbin)
        try:
            self._keymanager.get_key(address, openpgp.OpenPGPKey,
                                     private=True, fetch_remote=False)
        except KeyNotFound:
            logger.debug("Key not found. Generating key for %s" % (address,))
            self._keymanager.gen_key(openpgp.OpenPGPKey)
            self._keymanager.send_key(openpgp.OpenPGPKey)
            logger.debug("Key generated successfully.")

    def run_soledad_setup_checks(self,
                                 provider_config,
                                 user,
                                 password,
                                 download_if_needed=False,
                                 standalone=False):
        """
        Starts the checks needed for a new soledad setup

        :param provider_config: Provider configuration
        :type provider_config: ProviderConfig
        :param user: User's login
        :type user: str
        :param password: User's password
        :type password: str
        :param download_if_needed: If True, it will only download
                                   files if the have changed since the
                                   time it was previously downloaded.
        :type download_if_needed: bool
        :param standalone: If True, it'll look for paths inside the
                           bundle (like for gpg)
        :type standalone: bool
        """
        leap_assert_type(provider_config, ProviderConfig)

        # XXX we should provider a method for setting provider_config
        self._provider_config = provider_config
        self._download_if_needed = download_if_needed
        self._user = user
        self._password = password
        self._standalone = standalone

        cb_chain = [
            (self._download_config, self.download_config),
            (self._gen_key, self.gen_key)
        ]

        self.addCallbackChain(cb_chain)
    def _download_config(self):
        """
        Downloads the Soledad config for the given provider
        """

        leap_assert(self._provider_config,
                    "We need a provider configuration!")

        logger.debug("Downloading Soledad config for %s" %
                     (self._provider_config.get_domain(),))

        self._soledad_config = SoledadConfig()

        headers = {}
        mtime = get_mtime(
            os.path.join(
                self._soledad_config.get_path_prefix(),
                "leap", "providers",
                self._provider_config.get_domain(),
                "soledad-service.json"))

        if self._download_if_needed and mtime:
            headers['if-modified-since'] = mtime

        api_version = self._provider_config.get_api_version()

        # there is some confusion with this uri,
        config_uri = "%s/%s/config/soledad-service.json" % (
            self._provider_config.get_api_uri(),
            api_version)
        logger.debug('Downloading soledad config from: %s' % config_uri)

        # TODO factor out this srpauth protected get (make decorator)
        srp_auth = self.srpauth
        session_id = srp_auth.get_session_id()
        cookies = None
        if session_id:
            cookies = {"_session_id": session_id}

        res = self._session.get(config_uri,
                                verify=self._provider_config
                                .get_ca_cert_path(),
                                headers=headers,
                                cookies=cookies)
        res.raise_for_status()

        self._soledad_config.set_api_version(api_version)

        # Not modified
        if res.status_code == 304:
            logger.debug("Soledad definition has not been modified")
            self._soledad_config.load(
                os.path.join(
                    "leap", "providers",
                    self._provider_config.get_domain(),
                    "soledad-service.json"))
        else:
            soledad_definition, mtime = get_content(res)

            self._soledad_config.load(data=soledad_definition, mtime=mtime)
            self._soledad_config.save(["leap",
                                       "providers",
                                       self._provider_config.get_domain(),
                                       "soledad-service.json"])

        self.load_and_sync_soledad()