import os
import random
import sys
from datetime import date
from urllib import urlencode

import mailpile.auth
from mailpile.defaults import CONFIG_RULES
from mailpile.i18n import ListTranslations, ActivateTranslation, gettext
from mailpile.i18n import gettext as _
from mailpile.i18n import ngettext as _n
from mailpile.plugins import PluginManager
from mailpile.plugins import PLUGINS
from mailpile.plugins.contacts import AddProfile
from mailpile.plugins.contacts import ListProfiles
from mailpile.plugins.migrate import Migrate
from mailpile.plugins.tags import AddTag
from mailpile.commands import Command
from mailpile.config import SecurePassphraseStorage
from mailpile.crypto.gpgi import GnuPG, SignatureInfo, EncryptionInfo
from mailpile.crypto.gpgi import GnuPGKeyGenerator, GnuPGKeyEditor
from mailpile.httpd import BLOCK_HTTPD_LOCK, Idle_HTTPD
from mailpile.smtp_client import SendMail, SendMailError
from mailpile.urlmap import UrlMap
from mailpile.ui import Session
from mailpile.util import *


_ = lambda s: s
_plugins = PluginManager(builtin=__file__)


##[ Commands ]################################################################

class SetupMagic(Command):
    """Perform initial setup"""
    SYNOPSIS = (None, None, None, None)
    ORDER = ('Internals', 0)
    LOG_PROGRESS = True

    TAGS = {
        'New': {
            'type': 'unread',
            'label': False,
            'display': 'invisible',
            'icon': 'icon-new',
            'label_color': '03-gray-dark',
            'name': _('New'),
        },
        'Inbox': {
            'type': 'inbox',
            'display': 'priority',
            'display_order': 2,
            'icon': 'icon-inbox',
            'label_color': '06-blue',
            'name': _('Inbox'),
        },
        'Blank': {
            'type': 'blank',
            'flag_editable': True,
            'display': 'invisible',
            'name': _('Blank'),
        },
        'Drafts': {
            'type': 'drafts',
            'flag_editable': True,
            'display': 'priority',
            'display_order': 1,
            'icon': 'icon-compose',
            'label_color': '03-gray-dark',
            'name': _('Drafts'),
        },
        'Outbox': {
            'type': 'outbox',
            'display': 'priority',
            'display_order': 3,
            'icon': 'icon-outbox',
            'label_color': '06-blue',
            'name': _('Outbox'),
        },
        'Sent': {
            'type': 'sent',
            'display': 'priority',
            'display_order': 4,
            'icon': 'icon-sent',
            'label_color': '03-gray-dark',
            'name': _('Sent'),
        },
        'Spam': {
            'type': 'spam',
            'flag_hides': True,
            'display': 'priority',
            'display_order': 5,
            'icon': 'icon-spam',
            'label_color': '10-orange',
            'name': _('Spam'),
        },
        'MaybeSpam': {
            'display': 'invisible',
            'icon': 'icon-spam',
            'label_color': '10-orange',
            'name': _('MaybeSpam'),
        },
        'Ham': {
            'type': 'ham',
            'display': 'invisible',
            'name': _('Ham'),
        },
        'Trash': {
            'type': 'trash',
            'flag_hides': True,
            'display': 'priority',
            'display_order': 6,
            'icon': 'icon-trash',
            'label_color': '13-brown',
            'name': _('Trash'),
        },
        # These are magical tags that perform searches and show
        # messages in contextual views.
        'All Mail': {
            'type': 'tag',
            'icon': 'icon-logo',
            'label_color': '06-blue',
            'search_terms': 'all:mail',
            'name': _('All Mail'),
            'display_order': 1000,
        },
        'Photos': {
            'type': 'tag',
            'icon': 'icon-photos',
            'label_color': '08-green',
            'search_terms': 'att:jpg',
            'name': _('Photos'),
            'template': 'photos',
            'display_order': 1001,
        },
        'Files': {
            'type': 'tag',
            'icon': 'icon-document',
            'label_color': '06-blue',
            'search_terms': 'has:attachment',
            'name': _('Files'),
            'template': 'files',
            'display_order': 1002,
        },
        'Links': {
            'type': 'tag',
            'icon': 'icon-links',
            'label_color': '12-red',
            'search_terms': 'http',
            'name': _('Links'),
            'display_order': 1003,
        },
        # These are internal tags, used for tracking user actions on
        # messages, as input for machine learning algorithms. These get
        # automatically added, and may be automatically removed as well
        # to keep the working sets reasonably small.
        'mp_rpl': {'type': 'replied', 'label': False, 'display': 'invisible'},
        'mp_fwd': {'type': 'fwded', 'label': False, 'display': 'invisible'},
        'mp_tag': {'type': 'tagged', 'label': False, 'display': 'invisible'},
        'mp_read': {'type': 'read', 'label': False, 'display': 'invisible'},
        'mp_ham': {'type': 'ham', 'label': False, 'display': 'invisible'},
    }

    def basic_app_config(self, session,
                         save_and_update_workers=True,
                         want_daemons=True):
        # Create local mailboxes
        session.config.open_local_mailbox(session)

        # Create standard tags and filters
        created = []
        for t in self.TAGS:
            if not session.config.get_tag_id(t):
                AddTag(session, arg=[t]).run(save=False)
                created.append(t)
            session.config.get_tag(t).update(self.TAGS[t])
        for stype, statuses in (('sig', SignatureInfo.STATUSES),
                                ('enc', EncryptionInfo.STATUSES)):
            for status in statuses:
                tagname = 'mp_%s-%s' % (stype, status)
                if not session.config.get_tag_id(tagname):
                    AddTag(session, arg=[tagname]).run(save=False)
                    created.append(tagname)
                session.config.get_tag(tagname).update({
                    'type': 'attribute',
                    'display': 'invisible',
                    'label': False,
                })

        if 'New' in created:
            session.ui.notify(_('Created default tags'))

        # Import all the basic plugins
        reload_config = False
        for plugin in PLUGINS:
            if plugin not in session.config.sys.plugins:
                session.config.sys.plugins.append(plugin)
                reload_config = True
        for plugin in session.config.plugins.WANTED:
            if plugin in session.config.plugins.available():
                session.config.sys.plugins.append(plugin)
        if reload_config:
            with session.config._lock:
                session.config.save()
                session.config.load(session)

        try:
            # If spambayes is not installed, this will fail
            import mailpile.plugins.autotag_sb
            if 'autotag_sb' not in session.config.sys.plugins:
                session.config.sys.plugins.append('autotag_sb')
                session.ui.notify(_('Enabling spambayes autotagger'))
        except ImportError:
            session.ui.warning(_('Please install spambayes '
                                 'for super awesome spam filtering'))

        vcard_importers = session.config.prefs.vcard.importers
        if not vcard_importers.gravatar:
            vcard_importers.gravatar.append({'active': True})
            session.ui.notify(_('Enabling gravatar image importer'))

        gpg_home = os.path.expanduser('~/.gnupg')
        if os.path.exists(gpg_home) and not vcard_importers.gpg:
            vcard_importers.gpg.append({'active': True,
                                        'gpg_home': gpg_home})
            session.ui.notify(_('Importing contacts from GPG keyring'))

        if ('autotag_sb' in session.config.sys.plugins and
                len(session.config.prefs.autotag) == 0):
            session.config.prefs.autotag.append({
                'match_tag': 'spam',
                'unsure_tag': 'maybespam',
                'tagger': 'spambayes',
                'trainer': 'spambayes'
            })
            session.config.prefs.autotag[0].exclude_tags[0] = 'ham'

        if save_and_update_workers:
            session.config.save()
            session.config.prepare_workers(session, daemons=want_daemons)

    def setup_command(self, session, do_gpg_stuff=False):
        do_gpg_stuff = do_gpg_stuff or ('do_gpg_stuff' in self.args)

        # Stop the workers...
        want_daemons = session.config.cron_worker is not None
        session.config.stop_workers()

        # Perform any required migrations
        Migrate(session).run(before_setup=True, after_setup=False)

        # Basic app config, tags, plugins, etc.
        self.basic_app_config(session,
                              save_and_update_workers=False,
                              want_daemons=want_daemons)

        # Assumption: If you already have secret keys, you want to
        #             use the associated addresses for your e-mail.
        #             If you don't already have secret keys, you should have
        #             one made for you, if GnuPG is available.
        #             If GnuPG is not available, you should be warned.
        if do_gpg_stuff:
            gnupg = GnuPG(None)
            accepted_keys = []
            if gnupg.is_available():
                keys = gnupg.list_secret_keys()
                for key, details in keys.iteritems():
                    # Ignore revoked/expired keys.
                    if ("revocation-date" in details and
                        details["revocation-date"] <=
                            date.today().strftime("%Y-%m-%d")):
                        continue

                    accepted_keys.append(key)
                    for uid in details["uids"]:
                        if "email" not in uid or uid["email"] == "":
                            continue

                        if uid["email"] in [x["email"]
                                            for x in session.config.profiles]:
                            # Don't set up the same e-mail address twice.
                            continue

                        # FIXME: Add route discovery mechanism.
                        profile = {
                            "email": uid["email"],
                            "name": uid["name"],
                        }
                        session.config.profiles.append(profile)
                    if (session.config.prefs.gpg_recipient in (None, '', '!CREATE')
                           and details["capabilities_map"]["encrypt"]):
                        session.config.prefs.gpg_recipient = key
                        session.ui.notify(_('Encrypting config to %s') % key)
                    if session.config.prefs.crypto_policy == 'none':
                        session.config.prefs.crypto_policy = 'openpgp-sign'

                if len(accepted_keys) == 0:
                    # FIXME: Start background process generating a key once a user
                    #        has supplied a name and e-mail address.
                    pass

            else:
                session.ui.warning(_('Oh no, PGP/GPG support is unavailable!'))

        # If we have a GPG key, but no master key, create it
        self.make_master_key()

        # Perform any required migrations
        Migrate(session).run(before_setup=False, after_setup=True)

        session.config.save()
        session.config.prepare_workers(session, daemons=want_daemons)

        return self._success(_('Performed initial Mailpile setup'))

    def make_master_key(self):
        session = self.session
        if (session.config.prefs.gpg_recipient not in (None, '', '!CREATE')
                and not session.config.master_key
                and not session.config.prefs.obfuscate_index):
            #
            # This secret is arguably the most critical bit of data in the
            # app, it is used as an encryption key and to seed hashes in
            # a few places.  As such, the user may need to type this in
            # manually as part of data recovery, so we keep it reasonably
            # sized and devoid of confusing chars.
            #
            # The strategy below should give about 281 bits of randomness:
            #
            #   import math
            #   math.log((25 + 25 + 8) ** (12 * 4), 2) == 281.183...
            #
            secret = ''
            chars = 12 * 4
            while len(secret) < chars:
                secret = sha512b64(os.urandom(1024),
                                   '%s' % session.config,
                                   '%s' % time.time())
                secret = CleanText(secret,
                                   banned=CleanText.NONALNUM + 'O01l'
                                   ).clean[:chars]
            session.config.master_key = secret
            if self._idx() and self._idx().INDEX:
                session.ui.warning(_('Unable to obfuscate search index '
                                     'without losing data. Not indexing '
                                     'encrypted mail.'))
            else:
                session.config.prefs.obfuscate_index = True
                session.config.prefs.index_encrypted = True
                session.ui.notify(_('Obfuscating search index and enabling '
                                    'indexing of encrypted e-mail. Yay!'))
            return True
        else:
            return False

    def command(self, *args, **kwargs):
        session = self.session
        if session.config.sys.lockdown:
            return self._error(_('In lockdown, doing nothing.'))
        return self.setup_command(session, *args, **kwargs)


class TestableWebbable(SetupMagic):
    HTTP_AUTH_REQUIRED = 'Maybe'
    HTTP_CALLABLE = ('GET', )
    HTTP_QUERY_VARS = {
        '_path': 'Redirect path'
    }
    HTTP_POST_VARS = {
        'testing': 'Yes or No, if testing',
        'advance': 'Yes or No, advance setup flow',
    }
    TRUTHY = {
        '0': False, 'no': False, 'fuckno': False, 'false': False,
        '1': True, 'yes': True, 'hellyeah': True, 'true': True,
    }

    def _advance(self):
        path = self.data.get('_path', [None])[0]
        data = dict([(k, v) for k, v in self.data.iteritems()
                     if k not in self.HTTP_POST_VARS
                     and k not in ('_method',)])

        nxt = Setup.Next(self.session.config, None, needed_auth=False)
        if nxt:
            url = '/%s/' % nxt.SYNOPSIS[2]
        elif path and path != '/%s/' % Setup.SYNOPSIS[2]:
            # Use the same redirection logic as the Authenticator
            mailpile.auth.Authenticate.RedirectBack(path, data)
        else:
            url = '/'

        qs = urlencode([(k, v) for k, vl in data.iteritems() for v in vl])
        raise UrlRedirectException(''.join([url, '?%s' % qs if qs else '']))

    def _success(self, message, result=True, advance=False):
        if (advance or
                self.TRUTHY.get(self.data.get('advance', ['no'])[0].lower())):
            self._advance()
        return SetupMagic._success(self, message, result=result)

    def _testing(self):
        self._testing_yes(lambda: True)
        return (self.testing is not None)

    def _testing_yes(self, method, *args, **kwargs):
        testination = self.data.get('testing')
        if testination:
            self.testing = random.randint(0, 1)
            if testination[0].lower() in self.TRUTHY:
                self.testing = self.TRUTHY[testination[0].lower()]
            return self.testing
        self.testing = None
        return method(*args, **kwargs)

    def _testing_data(self, method, tdata, *args, **kwargs):
        result = self._testing_yes(method, *args, **kwargs) or []
        return (result
                if (self.testing is None) else
                (self.testing and tdata or []))

    def setup_command(self, session):
        raise Exception('FIXME')


class SetupGetEmailSettings(TestableWebbable):
    """Guess server details for an e-mail address"""
    SYNOPSIS = (None, 'setup/email_servers', 'setup/email_servers', None)
    HTTP_CALLABLE = ('GET', )
    HTTP_QUERY_VARS = dict_merge(TestableWebbable.HTTP_QUERY_VARS, {
        'email': 'E-mail address'
    })
    TEST_DATA = {
        'imap_host': 'imap.wigglebonk.com',
        'imap_port': 993,
        'imap_tls': True,
        'pop3_host': 'pop3.wigglebonk.com',
        'pop3_port': 110,
        'pop3_tls': False,
        'smtp_host': 'smtp.wigglebonk.com',
        'smtp_port': 465,
        'smtp_tls': False
    }

    def _get_domain_settings(self, domain):
        raise Exception('FIXME')

    def setup_command(self, session):
        results = {}
        for email in list(self.args) + self.data.get('email'):
            settings = self._testing_data(self._get_domain_settings,
                                          self.TEST_DATA, email)
            if settings:
                results[email] = settings
        if results:
            self._success(_('Found settings for %d addresses'), results)
        else:
            self._error(_('No settings found'))


class SetupWelcome(TestableWebbable):
    SYNOPSIS = (None, None, 'setup/welcome', None)
    HTTP_CALLABLE = ('GET', 'POST')
    HTTP_POST_VARS = dict_merge(TestableWebbable.HTTP_POST_VARS, {
        'language': 'Language selection'
    })

    def bg_setup_stage_1(self):
        # Wait a bit, so the user has something to look at befor we
        # block the web server and do real work.
        time.sleep(2)

        # Intial configuration of app goes here...
        if not self.session.config.tags:
            with BLOCK_HTTPD_LOCK, Idle_HTTPD(allowed=0):
                self.basic_app_config(self.session)

        # Next, if we have any secret GPG keys, extract all the e-mail
        # addresses and create a profile for each one.
        with BLOCK_HTTPD_LOCK, Idle_HTTPD(allowed=0):
            SetupProfiles(self.session).auto_create_profiles()

    def setup_command(self, session):
        config = session.config
        if self.data.get('_method') == 'POST' or self._testing():
            language = self.data.get('language', [''])[0]
            if language:
                try:
                    i18n = lambda: ActivateTranslation(session, config,
                                                       language)
                    if not self._testing_yes(i18n):
                        raise ValueError('Failed to configure i18n')
                    config.prefs.language = language
                    if not self._testing():
                        self._background_save(config=True)
                except ValueError:
                    return self._error(_('Invalid language: %s') % language)

            config.slow_worker.add_unique_task(
                session, 'Setup, Stage 1', lambda: self.bg_setup_stage_1())

        results = {
            'languages': ListTranslations(config),
            'language': config.prefs.language
        }
        return self._success(_('Welcome to Mailpile!'), results)


class SetupCrypto(TestableWebbable):
    SYNOPSIS = (None, None, 'setup/crypto', None)

    HTTP_CALLABLE = ('GET', 'POST')
    HTTP_POST_VARS = dict_merge(TestableWebbable.HTTP_POST_VARS, {
        'choose_key': 'Select an existing key to use',
        'passphrase': 'Specify a passphrase',
        'passphrase_confirm': 'Confirm the passphrase',
        'index_encrypted': 'y/n: index encrypted mail?',
#       'obfuscate_index': 'y/n: obfuscate keywords?',  # Omitted do to DANGER
        'encrypt_mail': 'y/n: encrypt locally stored mail?',
        'encrypt_index': 'y/n: encrypt search index?',
        'encrypt_vcards': 'y/n: encrypt vcards?',
        'encrypt_events': 'y/n: encrypt event log?',
        'encrypt_misc': 'y/n: encrypt plugin and misc data?'
    })
    TEST_DATA = {}

    def list_secret_keys(self):
        today = date.today().strftime("%Y-%m-%d")
        keylist = {}
        for key, details in self._gnupg().list_secret_keys().iteritems():
            # Ignore revoked keys
            if ("revocation-date" in details and
                    details["revocation-date"] <= today):
                # FIXME: Does this check expiry as well?
                continue

            # Ignore keys that cannot both encrypt and sign
            caps = details["capabilities_map"]
            if not caps["encrypt"] or not caps["sign"]:
                continue

            keylist[key] = details
        return keylist

    def gpg_key_ready(self, gpg_keygen):
        if not gpg_keygen.failed:
            self.session.config.prefs.gpg_recipient = gpg_keygen.generated_key
            self.make_master_key()
            self._background_save(config=True)
            self.save_profiles_to_key()

    def save_profiles_to_key(self, key_id=None, add_all=False, now=False,
                                   profiles=None):
        if key_id is None:
            if (Setup.KEY_CREATING_THREAD and
                    not Setup.KEY_CREATING_THREAD.failed):
                key_id = Setup.KEY_CREATING_THREAD.generated_key
                add_all = True

        if not add_all:
            self.session.ui.warning('FIXME: Not updating GPG key!')
            return

        if key_id is not None:
            uids = []
            data = ListProfiles(self.session).run().result
            for profile in data['profiles']:
                uids.append({
                    'name': profile["fn"],
                    'email': profile["email"][0]["email"],
                    'comment': profile.get('note', '')
                })
            if not uids:
                return

            editor = GnuPGKeyEditor(key_id, set_uids=uids,
                                    sps=self.session.config.gnupg_passphrase,
                                    deletes=max(10, 2*len(uids)))

            def start_editor(*unused_args):
                with Setup.KEY_WORKER_LOCK:
                    Setup.KEY_EDITING_THREAD = editor
                    editor.start()

            with Setup.KEY_WORKER_LOCK:
                 if now:
                     start_editor()
                 elif Setup.KEY_EDITING_THREAD is not None:
                     Setup.KEY_EDITING_THREAD.on_complete('edit keys',
                                                          start_editor)
                 elif Setup.KEY_CREATING_THREAD is not None:
                     Setup.KEY_CREATING_THREAD.on_complete('edit keys',
                                                           start_editor)
                 else:
                     start_editor()

    def setup_command(self, session):
        changed = authed = False
        results = {
            'secret_keys': self.list_secret_keys(),
        }
        error_info = None

        if self.data.get('_method') == 'POST' or self._testing():

            # 1st, are we choosing or creating a new key?
            choose_key = self.data.get('choose_key', [''])[0]
            if choose_key and not error_info:
                if (choose_key not in results['secret_keys'] and
                        choose_key != '!CREATE'):
                    error_info = (_('Invalid key'), {
                        'invalid_key': True,
                        'chosen_key': choose_key
                    })

            # 2nd, check authentication...
            #
            # FIXME: Creating a new key will allow a malicious actor to
            #        bypass authentication and change settings.
            #
            try:
                passphrase = self.data.get('passphrase', [''])[0]
                passphrase2 = self.data.get('passphrase_confirm', [''])[0]
                chosen_key = ((not error_info) and choose_key
                              ) or session.config.prefs.gpg_recipient

                if not error_info:
                    assert(passphrase == passphrase2)
                    if chosen_key == '!CREATE':
                        assert(passphrase != '')
                        sps = SecurePassphraseStorage(passphrase)
                    elif chosen_key:
                        sps = mailpile.auth.VerifyAndStorePassphrase(
                            session.config,
                            passphrase=passphrase,
                            key=chosen_key)
                    else:
                        sps = mailpile.auth.VerifyAndStorePassphrase(
                            session.config, passphrase=passphrase)
                    if not chosen_key:
                        choose_key = '!CREATE'
                    results['updated_passphrase'] = True
                    session.config.gnupg_passphrase.data = sps.data
                    mailpile.auth.SetLoggedIn(self)
            except AssertionError:
                error_info = (_('Invalid passphrase'), {
                    'invalid_passphrase': True,
                    'chosen_key': session.config.prefs.gpg_recipient
                })

            # 3rd, if necessary master key and/or GPG key
            with BLOCK_HTTPD_LOCK, Idle_HTTPD():
                if choose_key and not error_info:
                    session.config.prefs.gpg_recipient = choose_key
                    # FIXME: This should probably only happen if the GPG
                    #        key was successfully created.
                    self.make_master_key()
                    changed = True

                with Setup.KEY_WORKER_LOCK:
                    if ((not error_info) and
                            (session.config.prefs.gpg_recipient
                             == '!CREATE') and
                            (Setup.KEY_CREATING_THREAD is None or
                             Setup.KEY_CREATING_THREAD.failed)):
                        gk = GnuPGKeyGenerator(
                            sps=session.config.gnupg_passphrase,
                            on_complete=('notify',
                                         lambda: self.gpg_key_ready(gk)))
                        Setup.KEY_CREATING_THREAD = gk
                        Setup.KEY_CREATING_THREAD.start()

            # Finally we update misc. settings
            for key in self.HTTP_POST_VARS.keys():
                # FIXME: This should probably only happen if the GPG
                #        key was successfully created.

                # Continue iff all is well...
                if error_info:
                    break
                if key in (['choose_key', 'passphrase', 'passphrase_confirm'] +
                           TestableWebbable.HTTP_POST_VARS.keys()):
                    continue
                try:
                    val = self.data.get(key, [''])[0]
                    if val:
                        session.config.prefs[key] = self.TRUTHY[val.lower()]
                        changed = True
                except (ValueError, KeyError):
                    error_info = (_('Invalid preference'), {
                        'invalid_setting': True,
                        'variable': key
                    })

        results.update({
            'creating_key': (Setup.KEY_CREATING_THREAD is not None and
                             Setup.KEY_CREATING_THREAD.running),
            'creating_failed': (Setup.KEY_CREATING_THREAD is not None and
                                Setup.KEY_CREATING_THREAD.failed),
            'chosen_key': session.config.prefs.gpg_recipient,
            'prefs': {
                'index_encrypted': session.config.prefs.index_encrypted,
                'obfuscate_index': session.config.prefs.obfuscate_index,
                'encrypt_mail': session.config.prefs.encrypt_mail,
                'encrypt_index': session.config.prefs.encrypt_index,
                'encrypt_vcards': session.config.prefs.encrypt_vcards,
                'encrypt_events': session.config.prefs.encrypt_events,
                'encrypt_misc': session.config.prefs.encrypt_misc
            }
        })

        if changed:
            self._background_save(config=True)

        if error_info:
            return self._error(error_info[0],
                               info=error_info[1], result=results)
        elif changed:
            return self._success(_('Updated crypto preferences'), results)
        else:
            return self._success(_('Configure crypto preferences'), results)


class SetupProfiles(SetupCrypto):
    SYNOPSIS = (None, None, 'setup/profiles', None)

    HTTP_AUTH_REQUIRED = True
    HTTP_CALLABLE = ('GET', 'POST')
    HTTP_QUERY_VARS = dict_merge(TestableWebbable.HTTP_QUERY_VARS, {
    })
    HTTP_POST_VARS = dict_merge(TestableWebbable.HTTP_POST_VARS, {
        'email': 'Create a profile for this e-mail address',
        'name': 'Name associated with this e-mail',
        'note': 'Profile note',
        'pass': 'Password for remote accounts',
        'route_id': 'Route ID for sending mail',
    })
    TEST_DATA = {}

    # This is where we cache the passwords we are given, for use later.
    # This is deliberately made a singleton on the class.
    PASSWORD_CACHE = {}

    def _auto_configurable(self, email):
        # FIXME: Actually look things up, this is super lame
        return email.endswith('@gmail.com')

    def get_profiles(self, secret_keys=None):
        data = ListProfiles(self.session).run().result
        profiles = {}
        for rid, ofs in data["rids"].iteritems():
            profile = data["profiles"][ofs]
            email = profile["email"][0]["email"]
            name = profile["fn"]
            note = profile.get('note', '')
            profiles[rid] = {
                "name": name,
                "note": note,
                "pgp_keys": [],  # FIXME
                "email": email,
                "route_id": profile.get('x-mailpile-profile-route', ''),
                "photo": profile.get('photo', [{}])[0].get('photo', ''),
                "auto_configurable": self._auto_configurable(email)
            }
        for key, info in (secret_keys or {}).iteritems():
            for uid in info['uids']:
                email = uid.get('email')
                if email in profiles:
                    profiles[email]["pgp_keys"].append(key)
        return profiles

    def discover_new_email_addresses(self, profiles):
        addresses = {}
        existing = set([p['email'] for p in profiles.values()])
        for key, info in self._gnupg().list_secret_keys().iteritems():
            for uid in info['uids']:
                email = uid.get('email')
                note = uid.get('comment')
                if email:
                    if email in existing:
                        continue
                    if email not in addresses:
                        addresses[email] = {'pgp_keys': [],
                                            'name': '', 'note': ''}
                    ai = addresses[email]
                    name = uid.get('name')
                    ai['name'] = name if name else ai['name']
                    ai['note'] = note if note else ai['note']
                    ai['pgp_keys'].append(key)

        # FIXME: Scan Thunderbird and MacMail for e-mails, other apps...

        return addresses

    def auto_create_profiles(self):
        new_emails = self.discover_new_email_addresses(self.get_profiles())
        for email, info in new_emails.iteritems():
            AddProfile(self.session, data={
                '_method': 'POST',
                'email': [email],
                'note': [info["note"]],
                'name': [info['name']]
            }).run()

    def _result(self):
        profiles = self.get_profiles()
        return {
            'new_emails': self.discover_new_email_addresses(profiles),
            'profiles': profiles,
            'routes': self.session.config.routes,
            'default_email': self.session.config.prefs.default_email
        }

    def setup_command(self, session):
        changed = False
        if self.data.get('_method') == 'POST' or self._testing():
            name, email, note, pwd = (self.data.get(k, [None])[0] for k in
                                      ('name', 'email', 'note', 'pass'))
            if email:
                rv = AddProfile(session, data=self.data).run()
                if rv.status == 'success':
                    #
                    # FIXME: We need to fire off a background process to
                    #        try and auto-discover routes and sources.
                    #
                    if not session.config.prefs.default_email:
                        session.config.prefs.default_email = email
                        changed = True
                    self.save_profiles_to_key()
                else:
                    return self._error(_('Failed to add profile'),
                                       info=rv.error_info,
                                       result=self._result())
            if email and pwd:
                sps = SecurePassphraseStorage(pwd)
                SetupProfiles.PASSWORD_CACHE[email] = sps

            result = self._result()
            if not result['default_email']:
                profiles = result['profiles'].values()
                profiles.sort(key=lambda p: (len(p['pgp_keys']),
                                             len(p['name'])))
                e = result['default_email'] = profiles[-1]['email']
                session.config.prefs.default_email = e
                changed = True
        else:
            result = self._result()

        if changed:
            self._background_save(config=True)

        return self._success(_('Your profiles'), result)


class SetupConfigureKey(SetupProfiles):
    SYNOPSIS = (None, None, 'setup/configure_key', None)

    HTTP_AUTH_REQUIRED = True
    HTTP_CALLABLE = ('GET', 'POST')
    HTTP_QUERY_VARS = dict_merge(TestableWebbable.HTTP_QUERY_VARS, {
    })
    HTTP_POST_VARS = dict_merge(TestableWebbable.HTTP_POST_VARS, {
    })
    TEST_DATA = {}

    def _result(self):
        keylist = self.list_secret_keys()
        profiles = self.get_profiles(secret_keys=keylist)
        return {
            'secret_keys': keylist,
            'profiles': profiles,
        }

    def setup_command(self, session):

        # FIXME!

        return self._success(_('Configuring a key'), self._result())


class SetupTestRoute(SetupProfiles):
    SYNOPSIS = (None, None, 'setup/test_route', None)

    HTTP_AUTH_REQUIRED = True
    HTTP_CALLABLE = ('POST', )
    HTTP_POST_VARS = dict_merge(TestableWebbable.HTTP_POST_VARS,
                                dict((k, v[0]) for k, v in
                                     CONFIG_RULES['routes'][1].iteritems()),
                                {'route_id': 'ID of existing route'})
    TEST_DATA = {}

    def setup_command(self, session):

        if self.args:
            route_id = self.args[0]
        elif 'route_id' in self.data:
            route_id = self.data['route_id'][0]
        else:
            route_id = None

        if route_id:
            route = self.session.config.routes[route_id]
            assert(route)
        else:
            route = {}
            for k in CONFIG_RULES['routes'][1]:
                if k not in self.data:
                    pass
                elif CONFIG_RULES['routes'][1][k][1] in (int, 'int'):
                    route[k] = int(self.data[k][0])
                else:
                    route[k] = self.data[k][0]

        fromaddr = route.get('username', '')
        if '@' not in fromaddr:
            fromaddr = self.session.config.get_profile()['email']
        assert(fromaddr)

        error_info = {'error': _('Unknown error')}
        try:
            assert(SendMail(self.session, None,
                            [(fromaddr,
                              [fromaddr, 'test@mailpile.is'],
                              None,
                              [self.event])],
                            test_only=True, test_route=route))
            return self._success(_('Route is working'),
                                 result=route)
        except OSError:
            error_info = {'error': _('Invalid command'),
                          'invalid_command': True}
        except SendMailError, e:
            error_info = {'error': e.message,
                          'sendmail_error': True}
            error_info.update(e.error_info)
        except:
            import traceback
            traceback.print_exc()

        return self._error(_('Route is not working'),
                           result=route, info=error_info)


class Setup(TestableWebbable):
    """Enter setup flow"""
    SYNOPSIS = (None, 'setup', 'setup', '[do_gpg_stuff]')

    ORDER = ('Internals', 0)
    LOG_PROGRESS = True
    HTTP_CALLABLE = ('GET',)
    HTTP_AUTH_REQUIRED = True

    # These are a global, may be modified...
    KEY_WORKER_LOCK = CryptoRLock()
    KEY_CREATING_THREAD = None
    KEY_EDITING_THREAD = None

    @classmethod
    def _check_profiles(self, config):
        data = ListProfiles(Session(config)).run().result
        okay = routes = bad = 0
        for rid, ofs in data["rids"].iteritems():
            profile = data["profiles"][ofs]
            if profile.get('email', None):
                okay += 1
                route_id = profile.get('x-mailpile-profile-route', '')
                if route_id:
                    if route_id in config.routes:
                        routes += 1
                    else:
                        bad += 1
            else:
                bad += 1
        return (routes > 0) and (okay > 0) and (bad == 0)

    @classmethod
    def _CHECKPOINTS(self, config):
        return [
            # Stage 0: Welcome: Choose app language
            ('language', lambda: config.prefs.language, SetupWelcome),

            # Stage 1: Crypto: Configure our master key stuff
            ('crypto', lambda: config.prefs.gpg_recipient, SetupCrypto),

            # Stage 2: Identity (via. single page install flow)
            ('profiles', lambda: self._check_profiles(config), Setup),

            # Stage 3: Routes (via. single page install flow)
            ('routes', lambda: config.routes, Setup),

            # Stage 4: Sources (via. single page install flow)
            ('sources', lambda: config.sources, Setup),

            # Stage 5: Is All Complete
            ('complete', lambda: config.web.setup_complete, Setup),

            # FIXME: Check for this too?
            #(lambda: config.prefs.crypto_policy != 'none', SetupConfigureKey),
        ]

    @classmethod
    def Next(cls, config, default, needed_auth=True):
        if not config.loaded_config:
            return default

        for name, guard, step in cls._CHECKPOINTS(config):
            auth_required = (step.HTTP_AUTH_REQUIRED is True
                             or (config.prefs.gpg_recipient and
                                 step.HTTP_AUTH_REQUIRED == 'Maybe'))
            if not guard():
                if (not needed_auth) or (not auth_required):
                    return step

        return default

    def setup_command(self, session):
        if '_method' in self.data:
            return self._success(_('Entering setup flow'), result=dict(
                ((c[0], c[1]() and True or False)
                 for c in self._CHECKPOINTS(session.config)
            )))
        else:
            return SetupMagic.setup_command(self, session)


_ = gettext
_plugins.register_commands(SetupMagic,
                           SetupGetEmailSettings,
                           SetupWelcome,
                           SetupCrypto,
                           SetupProfiles,
                           SetupConfigureKey,
                           SetupTestRoute,
                           Setup)
