import os
import random
import time
from email import encoders

import mailpile.config.defaults
import mailpile.security as security
from mailpile.crypto.gpgi import GnuPG
from mailpile.crypto.gpgi import GnuPGBaseKeyGenerator, GnuPGKeyGenerator
from mailpile.plugins import EmailTransform, PluginManager
from mailpile.commands import Command, Action
from mailpile.eventlog import Event
from mailpile.i18n import gettext as _
from mailpile.i18n import ngettext as _n
from mailpile.mailutils.addresses import AddressHeaderParser
from mailpile.mailutils.emails import Email, ExtractEmails, ExtractEmailAndName
from mailpile.security import SecurePassphraseStorage
from mailpile.vcard import VCardLine, VCardStore, MailpileVCard, AddressInfo
from mailpile.util import *


_plugins = PluginManager(builtin=__file__)

GLOBAL_VCARD_LOCK = VCardRLock()


##[ VCards ]########################################

class VCardCommand(Command):
    VCARD = "vcard"
    IS_USER_ACTIVITY = True
    WITH_CONTEXT = (GLOBAL_VCARD_LOCK,)

    class CommandResult(Command.CommandResult):
        IGNORE = ('line_id', 'pid', 'x-rank')

        def as_text(self):
            try:
                return self._as_text()
            except (KeyError, ValueError, IndexError, TypeError):
                return ''

        def _as_text(self):
            if isinstance(self.result, dict):
                co = self.command_obj
                if co.VCARD in self.result:
                    return self._vcards_as_text([self.result[co.VCARD]])
                if co.VCARD + 's' in self.result:
                    return self._vcards_as_text(self.result[co.VCARD + 's'])
            return Command.CommandResult.as_text(self)

        def _vcards_as_text(self, result):
            lines = []
            b64re = re.compile('base64,.*$')
            for card in result:
                if isinstance(card, list):
                    for line in card:
                        key = line.name
                        data = re.sub(b64re, _('(BASE64 ENCODED DATA)'),
                                      unicode(line[key]))
                        attrs = ', '.join([('%s=%s' % (k, v))
                                           for k, v in line.attrs
                                           if k not in ('pid',)])
                        if attrs:
                            attrs = ' (%s)' % attrs
                        lines.append('%3.3s %-5.5s %s: %s%s'
                                     % (line.line_id,
                                        line.get('pid', ''),
                                        key, data, attrs))
                    lines.append('')
                else:
                    emails = [k['email'] for k in card['email']]
                    photos = [k['photo'] for k in card.get('photo', [])]
                    lines.append('%s %-32.32s %s'
                                 % (photos and ':)' or '  ',
                                    card['fn'] + (' (%s)' % card['note']
                                                  if card.get('note') else ''),
                                    ', '.join(emails)))
                    for key in [k['key'].split(',')[-1]
                                for k in card.get('key', [])]:
                        lines.append('   %-32.32s key:%s' % ('', key))
            return '\n'.join(lines)

    def _form_defaults(self):
        return {'form': self.HTTP_POST_VARS}

    def _make_new_vcard(self, handle, name, note, kind):
        l = [VCardLine(name='fn', value=name),
             VCardLine(name='kind', value=kind)]
        if note:
            l.append(VCardLine(name='note', value=note))
        if kind in VCardStore.KINDS_PEOPLE:
            return MailpileVCard(VCardLine(name='email',
                                           value=handle, type='pref'), *l,
                                 config=self.session.config)
        else:
            return MailpileVCard(VCardLine(name='nickname', value=handle), *l,
                                 config=self.session.config)

    def _valid_vcard_handle(self, vc_handle):
        return (vc_handle and '@' in vc_handle[1:])

    def _pre_delete_vcard(self, vcard):
        pass

    def _vcard_list(self, vcards, mode='mpCard', info=None, simplify=False):
        info = info or {}
        if mode == 'lines':
            data = [x.as_lines() for x in vcards if x]
        else:
            data = [x.as_mpCard() for x in vcards if x]

        # Generate some helpful indexes for finding stuff
        by_email = {}
        by_rid = {}
        for count, vc in enumerate(vcards):
            by_rid[vc.random_uid] = count
            by_email[vc.email] = count
        for count, vc in enumerate(vcards):
            for vcl in vc.get_all('EMAIL'):
                if vcl.value not in by_email:
                    by_email[vcl.value] = count

        # Simplify lists when there is only one element?
        if simplify and len(data) == 1:
            data = data[0]
            whatsit = self.VCARD
        else:
            whatsit = self.VCARD + 's'

        info.update({
            whatsit: data,
            "emails": by_email,
            "rids": by_rid,
            "count": len(vcards)
        })
        return info


class VCard(VCardCommand):
    """Display a single vcard"""
    SYNOPSIS = (None, 'vcards/view', None, '<nickname>')
    ORDER = ('Internals', 6)
    KIND = ''

    def command(self, save=True):
        self._idx()  # Make sure VCards are all loaded
        session, config = self.session, self.session.config
        vcards = []
        for email in self.args:
            vcard = config.vcards.get_vcard(email)
            if vcard:
                vcards.append(vcard)
            else:
                session.ui.warning('No such %s: %s' % (self.VCARD, email))
        return self._success(_('Found %d results') % len(vcards),
                             result=self._vcard_list(vcards, simplify=True))


class AddVCard(VCardCommand):
    """Add one or more vcards"""
    SYNOPSIS = (None, 'vcards/add', None, '[all] <msgs> OR <email> = <name>')
    ORDER = ('Internals', 6)
    KIND = ''
    HTTP_CALLABLE = ('POST', 'PUT', 'GET')
    HTTP_POST_VARS = {
        'email': 'E-mail address',
        'name': 'Contact name',
        'note': 'Note about contact',
        'mid': 'Message ID'
    }
    COMMAND_SECURITY = security.CC_CHANGE_CONTACTS

    IGNORED_EMAILS_AND_DOMAINS = (
        'reply.airbnb.com',
        'notifications@github.com'
    )

    def _add_from_messages(self, args, add_recipients):
        pairs, idx = [], self._idx()
        for email in [Email(idx, i) for i in self._choose_messages(args)]:
            msg_info = email.get_msg_info()
            pairs.append(ExtractEmailAndName(msg_info[idx.MSG_FROM]))
            if add_recipients:
                people = (idx.expand_to_list(msg_info) +
                          idx.expand_to_list(msg_info, field=idx.MSG_CC))
                for e in people:
                    pair = ExtractEmailAndName(e)
                    domain = pair[0].split('@')[-1]
                    if (pair[0] not in self.IGNORED_EMAILS_AND_DOMAINS and
                            domain not in self.IGNORED_EMAILS_AND_DOMAINS and
                            'noreply' not in pair[0]):
                        pairs.append(pair)
        return [(p1, p2, '') for p1, p2 in pairs]

    def _sanity_check(self, kind, vcard):
        pass

    def _before_vcard_create(self, kind, triplets):
        return {}

    def _after_vcard_create(self, kind, vcard, state):
        pass

    def command(self, recipients=False, quietly=False, internal=False):
        idx = self._idx()  # Make sure VCards are all loaded
        session, config = self.session, self.session.config
        args = list(self.args)

        if self.data.get('_method', 'not-http').upper() == 'GET':
            return self._success(_('Add contacts here!'),
                                 self._form_defaults())

        if (len(args) > 2
                and args[1] == '='
                and self._valid_vcard_handle(args[0])):
            handle = args[0]
            name = []
            note = []
            inname = True
            for v in args[2:]:
                if v.startswith('('):
                    inname = False
                    v = v[1:]
                if v.endswith(')'):
                    v = v[:-1]
                if inname:
                    name.append(v)
                else:
                    note.append(v)

            triplets = [(args[0], ' '.join(name), ' '.join(note))]

        elif self.data:
            if self.data.get('email'):
                emails = self.data["email"]
                names = self.data["name"][:]
                names.extend(['' for i in range(len(names), len(emails))])
                notes = self.data.get("note", [])[:]
                notes.extend(['' for i in range(len(notes), len(emails))])
                triplets = zip(emails, names, notes)
            elif self.data.get('mid'):
                mids = self.data.get('mid')
                triplets = self._add_from_messages(
                    ['=%s' % mid.replace('=', '') for mid in mids])
        else:
            if args and args[0] == 'all':
                recipients = args.pop(0) and True
            triplets = self._add_from_messages(args, recipients)

        if triplets:
            vcards = []
            kind = self.KIND if not internal else 'internal'

            self._sanity_check(kind, triplets)
            state = self._before_vcard_create(kind, triplets)

            for handle, name, note in triplets:
                vcard = config.vcards.get(handle.lower())
                if vcard:
                    if not quietly:
                        session.ui.warning('Already exists: %s' % handle)
                    if kind != 'profile' and vcard.kind != 'internal':
                        continue

                if vcard and vcard.kind == 'internal':
                    config.vcards.deindex_vcard(vcard)
                    vcard.email = handle.lower()
                    vcard.name = name
                    vcard.note = note
                    vcard.kind = kind
                else:
                    vcard = self._make_new_vcard(handle.lower(), name, note,
                                                 kind)
                self._after_vcard_create(kind, vcard, state)
                config.vcards.add_vcards(vcard)
                vcards.append(vcard)
            if state.get('save_config', False):
                self._background_save(config=True)
        else:
            return self._error('Nothing to do!')
        return self._success(_('Added %d contacts') % len(vcards),
            result=self._vcard_list(vcards, simplify=True))


class RemoveVCard(VCardCommand):
    """Delete vcards"""
    SYNOPSIS = (None, 'vcards/remove', None, '<email|x-mailpile-rid>')
    ORDER = ('Internals', 6)
    KIND = ''
    HTTP_CALLABLE = ('POST', 'DELETE')
    HTTP_POST_VARS = {
        'email': 'delete by e-mail',
        'rid': 'delete by x-mailpile-rid'
    }
    COMMAND_SECURITY = security.CC_CHANGE_CONTACTS

    def command(self):
        idx = self._idx()  # Make sure VCards are all loaded
        session, config = self.session, self.session.config
        removed = []
        for handle in (list(self.args) +
                       self.data.get('email', []) +
                       self.data.get('rid', [])):
            vcard = config.vcards.get_vcard(handle)
            if vcard:
                self._pre_delete_vcard(vcard)
                config.vcards.del_vcards(vcard)
                removed.append(handle)
            else:
                session.ui.error(_('No such contact: %s') % handle)
        if removed:
            return self._success(_('Removed contacts: %s')
                                 % ', '.join(removed))
        else:
            return self._error(_('No contacts found'))


class VCardAddLines(VCardCommand):
    """Add a lines to a VCard"""
    SYNOPSIS = (None,
                'vcards/addlines', 'vcards/addlines',
                '<email> <[[<NR>]=]line> ...')
    ORDER = ('Internals', 6)
    KIND = ''
    HTTP_CALLABLE = ('POST', 'UPDATE')
    HTTP_POST_VARS = {
        'email': 'update by e-mail',
        'rid': 'update by x-mailpile-rid',
        'name': 'Line name',
        'value': 'Line value',
        'replace': 'int=replace line by number',
        'replace_all': 'Boolean: replace all lines, or not',
        'client': 'Source of this change'
    }
    COMMAND_SECURITY = security.CC_CHANGE_CONTACTS
    DEFAULT_REPLACE_ALL = False

    def _get_vcard(self, handle):
        return self.session.config.vcards.get_vcard(handle)

    def command(self):
        idx = self._idx()  # Make sure VCards are all loaded
        session, config = self.session, self.session.config

        if self.args:
            handle, lines = self.args[0], self.args[1:]
        else:
            handle = self.data.get('rid', self.data.get('email', [None]))[0]
            if not handle:
                raise ValueError('Must set rid or email to choose VCard')
            name, value, replace, replace_all = (self.data.get(n, [None])[0]
                for n in ('name', 'value', 'replace', 'replace_all'))
            if not name or not value or ':' in name or '=' in name:
                raise ValueError('Must send a line name and line data')
            value = '%s:%s' % (name, value)
            if replace:
                value = '%d=%s' % (replace, value)
            elif truthy(replace_all, default=self.DEFAULT_REPLACE_ALL):
                value = '=' + value
            lines = [value]

        vcard = self._get_vcard(handle)
        if not vcard:
            return self._error('%s not found: %s' % (self.VCARD, handle))
        config.vcards.deindex_vcard(vcard)
        client = self.data.get('client', [vcard.USER_CLIENT])[0]
        try:
            for l in lines:
                lname = l.split(':', 1)[0].lower()
                if lname[0] == '=':
                    l = l[1:].strip()
                    lname = lname[1:]
                    removing = [ex._line_id for ex in vcard.get_all(lname)]
                elif lname in MailpileVCard.MPCARD_SINGLETONS:
                    removing = [ex._line_id for ex in vcard.get_all(lname)]
                else:
                    removing = []

                if '=' in l[:5]:
                    ln, l = l.split('=', 1)
                    vcard.set_line(int(ln.strip()), VCardLine(l.strip()),
                                   client=client)

                else:
                    if removing:
                        vcard.remove(*removing)
                    vcard.add(VCardLine(l), client=client)

            vcard.save()
            return self._success(_("Added %d lines") % len(lines),
                result=self._vcard_list([vcard], simplify=True, info={
                    'updated': handle,
                    'added': len(lines)
                }))
        except KeyboardInterrupt:
            raise
        except:
            config.vcards.index_vcard(vcard)
            self._ignore_exception()
            return self._error(_('Error adding lines to %s') % handle)
        finally:
            config.vcards.index_vcard(vcard)


class VCardSet(VCardAddLines):
    """Add a lines to a VCard, ensuring VCard exists"""
    SYNOPSIS = (None, 'vcards/set', 'vcards/set', '<email> <[[<NR>]=]line> ...')
    HTTP_POST_VARS = dict_merge(VCardAddLines.HTTP_POST_VARS, {
        'fn': 'Name on card'
    })
    DEFAULT_REPLACE_ALL = True

    def _get_vcard(self, handle):
        vcard = self.session.config.vcards.get_vcard(handle)
        if not vcard:
            vcard = self._make_new_vcard(handle,
                                         self.data.get('fn', [handle])[0],
                                         None,
                                         self.KIND or 'individual')
            self.session.config.vcards.add_vcards(vcard)
        return vcard


class VCardRemoveLines(VCardCommand):
    """Remove lines from a VCard"""
    SYNOPSIS = (None, 'vcards/rmlines', None, '<email> <line IDs>')
    ORDER = ('Internals', 6)
    KIND = ''
    HTTP_CALLABLE = ('POST', 'UPDATE')
    COMMAND_SECURITY = security.CC_CHANGE_CONTACTS

    def command(self):
        idx = self._idx()  # Make sure VCards are all loaded
        session, config = self.session, self.session.config

        handle, line_ids = self.args[0], self.args[1:]
        vcard = config.vcards.get_vcard(handle)
        if not vcard:
            return self._error('%s not found: %s' % (self.VCARD, handle))
        config.vcards.deindex_vcard(vcard)
        removed = 0
        try:
            removed = vcard.remove(*[int(li) for li in line_ids])
            vcard.save()
            return self._success(_("Removed %d lines") % removed,
                result=self._vcard_list([vcard], simplify=True, info={
                    'updated': handle,
                    'removed': removed
                }))
        except KeyboardInterrupt:
            raise
        except:
            config.vcards.index_vcard(vcard)
            self._ignore_exception()
            return self._error(_('Error removing lines from %s') % handle)
        finally:
            config.vcards.index_vcard(vcard)


class ListVCards(VCardCommand):
    """Find vcards"""
    SYNOPSIS = (None, 'vcards', None, '[--lines] [<terms>]')
    ORDER = ('Internals', 6)
    KIND = ''
    HTTP_QUERY_VARS = {
        'q': 'search terms',
        'format': 'lines or mpCard (default)',
        'count': 'how many to display (default=40)',
        'offset': 'skip how many in the display (default=0)',
    }
    HTTP_CALLABLE = ('GET')

    def _augment_list_info(self, info):
        return info

    def command(self):
        session, config = self.session, self.session.config
        kinds = self.KIND and [self.KIND] or None
        args = list(self.args)

        if 'format' in self.data:
            fmt = self.data['format'][0]
        elif args and args[0] == '--lines':
            args.pop(0)
            fmt = 'lines'
        else:
            fmt = 'mpCard'

        if 'q' in self.data:
            terms = self.data['q']
        else:
            terms = args

        if 'count' in self.data:
            count = int(self.data['count'][0])
        else:
            count = 120

        if 'offset' in self.data:
            offset = int(self.data['offset'][0])
        else:
            offset = 0

        # If we're loading, stall a bit but then report current state
        loading, loaded = config.vcards.loading, config.vcards.loaded
        if loading:
            time.sleep(2)
            loading, loaded = config.vcards.loading, config.vcards.loaded

        vcards = config.vcards.find_vcards(terms, kinds=kinds)
        total = len(vcards)
        vcards = vcards[offset:offset + count]
        info = self._augment_list_info({
                   'terms': args,
                   'offset': offset,
                   'count': min(count, total),
                   'total': total,
                   'start': offset,
                   'end': offset + min(count, total - offset),
                   'loading': loading,
                   'loaded': loaded})
        return self._success(
            _("Listed %d/%d results") % (min(total, count), total),
            result=self._vcard_list(vcards, mode=fmt, info=info))


def ContactVCard(parent):
    """A factory for generating contact commands"""
    synopsis = [(t and t.replace('vcard', 'contact') or t)
                for t in parent.SYNOPSIS]
    synopsis[2] = synopsis[1]

    class ContactVCardCommand(parent):
        SYNOPSIS = tuple(synopsis)
        KIND = 'individual'
        ORDER = ('Tagging', 3)
        VCARD = "contact"

    return ContactVCardCommand


class Contact(ContactVCard(VCard)):
    """View contacts"""
    SYNOPSIS = (None, 'contacts/view', 'contacts/view', '[<email>]')

    def command(self, save=True):
        contact = VCard.command(self, save)
        # Tee-hee, monkeypatching results.
        contact["sent_messages"] = 0
        contact["received_messages"] = 0
        contact["last_contact_from"] = 10000000000000
        contact["last_contact_to"] = 10000000000000

        for email in contact["contact"]["email"]:
            s = Action(self.session, "search",
                       ["in:Sent", "to:%s" % (email["email"])]).as_dict()
            contact["sent_messages"] += s["result"]["stats"]["total"]
            for mid in s["result"]["thread_ids"]:
                msg = s["result"]["data"]["metadata"][mid]
                if msg["timestamp"] < contact["last_contact_to"]:
                    contact["last_contact_to"] = msg["timestamp"]
                    contact["last_contact_to_msg_url"] = msg["urls"]["thread"]

            s = Action(self.session, "search",
                       ["from:%s" % (email["email"])]).as_dict()
            contact["received_messages"] += s["result"]["stats"]["total"]
            for mid in s["result"]["thread_ids"]:
                msg = s["result"]["data"]["metadata"][mid]
                if msg["timestamp"] < contact["last_contact_from"]:
                    contact["last_contact_from"] = msg["timestamp"]
                    contact["last_contact_from_msg_url"
                            ] = msg["urls"]["thread"]

        if contact["last_contact_to"] == 10000000000000:
            contact["last_contact_to"] = False
            contact["last_contact_to_msg_url"] = ""

        if contact["last_contact_from"] == 10000000000000:
            contact["last_contact_from"] = False
            contact["last_contact_from_msg_url"] = ""

        return contact


class AddContact(ContactVCard(AddVCard)):
    """Add contacts"""


class RemoveContact(ContactVCard(RemoveVCard)):
    """Remove a contact"""


class ListContacts(ContactVCard(ListVCards)):
    SYNOPSIS = (None, 'contacts', 'contacts', '[--lines] [<terms>]')
    """Find contacts"""


class ContactSet(ContactVCard(VCardSet)):
    """Set contact lines, ensuring contact exists"""


class ContactImport(Command):
    """Import contacts"""
    SYNOPSIS = (None, 'contacts/import', 'contacts/import', '[<parameters>]')
    ORDER = ('Internals', 6)
    HTTP_CALLABLE = ('GET', )
    COMMAND_SECURITY = security.CC_CHANGE_CONTACTS

    def command(self, format, terms=None, **kwargs):
        idx = self._idx()  # Make sure VCards are all loaded
        session, config = self.session, self.session.config

        if not format in PluginManager.CONTACT_IMPORTERS.keys():
            session.ui.error("No such import format")
            return False

        importer = PluginManager.CONTACT_IMPORTERS[format]

        if not all([x in kwargs.keys() for x in importer.required_parameters]):
            session.ui.error(
                _("Required parameter missing. Required parameters "
                  "are: %s") % ", ".join(importer.required_parameters))
            return False

        allparams = importer.required_parameters + importer.optional_parameters

        if not all([x in allparams for x in kwargs.keys()]):
            session.ui.error(
                _("Unknown parameter passed to importer. "
                  "Provided %s; but known parameters are: %s"
                  ) % (", ".join(kwargs), ", ".join(allparams)))
            return False

        imp = importer(kwargs)
        if terms:
            contacts = imp.filter_contacts(terms)
        else:
            contacts = imp.get_contacts()

        for importedcontact in contacts:
            # Check if contact exists. If yes, then update. Else create.
            pass


class ContactImporters(Command):
    """Return a list of contact importers"""
    SYNOPSIS = (None, 'contacts/importers', 'contacts/importers', '')
    ORDER = ('Internals', 6)
    HTTP_CALLABLE = ('GET', )

    def command(self):
        res = []
        for iname, importer in CONTACT_IMPORTERS.iteritems():
            r = {}
            r["short_name"] = iname
            r["format_name"] = importer.format_name
            r["format_description"] = importer.format_description
            r["optional_parameters"] = importer.optional_parameters
            r["required_parameters"] = importer.required_parameters
            res.append(r)

        return res


class AddressSearch(VCardCommand):
    """Find addresses (in contacts or mail index)"""
    SYNOPSIS = (None, 'search/address', 'search/address', '[<terms>]')
    ORDER = ('Searching', 6)
    HTTP_QUERY_VARS = {
        'q': 'search terms',
        'count': 'number of results',
        'offset': 'offset results',
        'ms': 'deadline in ms'
    }

    def _boost_rank(self, boost, term, *matches):
        boost = 0.0
        for match in matches:
            match = match.lower()
            if term in match:
                if match.startswith(term):
                    boost += boost * boost * (float(len(term)) / len(match))
                else:
                    boost += boost * (float(len(term)) / len(match))
        return int(boost)

    def _vcard_addresses(self, cfg, terms, ignored_count, deadline):
        addresses = {}
        for vcard in cfg.vcards.find_vcards(terms,
                                            kinds=VCardStore.KINDS_PEOPLE):
            fn = vcard.get('fn')
            for email_vcl in vcard.get_all('email'):
                info = addresses.get(email_vcl.value) or {}
                info.update(AddressInfo(email_vcl.value, fn.value,
                                        vcard=vcard))
                info['rank'] = min(15, info.get('rank', 15))
                addresses[email_vcl.value] = info
                for term in terms:
                    info['rank'] += self._boost_rank(5, term, fn.value,
                                                     email_vcl.value)
            if len(addresses) and time.time() > deadline:
                break

        return addresses.values()

    def _index_addresses(self, cfg, terms, vcard_addresses, count, deadline):
        existing = dict([(k['address'].lower(), k) for k in vcard_addresses])
        index = self._idx()

        # Figure out which tags are invisible so we can skip messages marked
        # with those tags.
        invisible = set([t._key for t in cfg.get_tags(flag_hides=True)])
        matches = {}
        addresses = []

        # 1st, search the social graph for matches, give low priority.
        for frm in index.EMAILS:
            frm_lower = frm.lower()
            match = True
            for term in terms:
                if term not in frm_lower:
                    match = False
                    break
            if match:
                matches[frm] = matches.get(frm, 0) + 3
                if len(matches) > (count * 10):
                    break
            elif len(matches) and time.time() > deadline:
                break

        # 2nd, go through at most the last 5000 messages in the index and
        # search for matching senders or recipients, give medium priority.
        # Note: This is more CPU intensive, so we do this last.
        if len(matches) < (count * 5):
            for msg_idx in xrange(max(0, len(index.INDEX)-5000),
                                  len(index.INDEX)):
                msg_info = index.get_msg_at_idx_pos(msg_idx)
                tags = set(msg_info[index.MSG_TAGS].split(','))
                match = not (tags & invisible)
                if match:
                    frm = msg_info[index.MSG_FROM]
                    search = (frm + ' ' + msg_info[index.MSG_SUBJECT]).lower()
                    for term in terms:
                        if term not in search:
                            match = False
                            break
                    if match:
                        matches[frm] = matches.get(frm, 0) + 1
                        if len(matches) > (count * 5):
                            break
                    if len(matches) and time.time() > deadline:
                        break

        # Assign info & scores!
        for frm in matches:
            email, fn = ExtractEmailAndName(frm)
            boost = min(10, matches[frm])
            for term in terms:
                boost += self._boost_rank(4, term, fn, email)

            if not email or '@' not in email:
                # FIXME: This may not be the right thing for alternate
                #        message transports.
                pass
            elif email.lower() in existing:
                existing[email.lower()]['rank'] += boost
            else:
                info = AddressInfo(email, fn)
                info['rank'] = info.get('rank', 0) + boost
                existing[email.lower()] = info
                addresses.append(info)

        return addresses

    def command(self):
        session, config = self.session, self.session.config

        count = int(self.data.get('count', 10))
        offset = int(self.data.get('offset', 0))
        deadline = time.time() + float(self.data.get('ms', 150)) / 1000.0
        terms = []
        for q in self.data.get('q', []):
            terms.extend(q.lower().split())
        for a in self.args:
            terms.extend(a.lower().split())

        self.session.ui.mark('Searching VCards')
        vcard_addrs = self._vcard_addresses(config, terms, count, deadline)

        self.session.ui.mark('Searching Metadata')
        index_addrs = self._index_addresses(config, terms, vcard_addrs,
                                            count, deadline)

        self.session.ui.mark('Sorting')
        addresses = vcard_addrs + index_addrs
        addresses.sort(key=lambda k: -k['rank'])
        total = len(addresses)
        return self._success(_('Searched for addresses'), result={
            'addresses': addresses[offset:min(offset+count, total)],
            'displayed': min(count, total),
            'total': total,
            'offset': offset,
            'count': count,
            'start': offset,
            'end': offset+count,
        })


def ProfileVCard(parent):
    """A factory for generating profile commands"""
    synopsis = [(t and t.replace('vcard', 'profile') or t)
                for t in parent.SYNOPSIS]
    synopsis[2] = synopsis[1]

    class ProfileVCardCommand(parent):
        SYNOPSIS = tuple(synopsis)
        KIND = 'profile'
        ORDER = ('Tagging', 3)
        VCARD = "profile"

        DEFAULT_KEYTYPE = 'RSA3072'

        def _default_signature(self):
            return _('Sent using Mailpile, Free Software from www.mailpile.is')

        def _augment_list_info(self, info):
            info['default_sig'] = self._default_signature()
            return info

        def _yn(self, val, default='no'):
            return truthy(self.data.get(val, [default])[0])

        def _sendmail_command(self):
            # FIXME - figure out where sendmail is for reals
            return mailpile.config.defaults.DEFAULT_SENDMAIL

        def _sanity_check(self, kind, triplets):
            route_id = self.data.get('route_id', [None])[0]
            if (route_id or [k for k in self.data.keys() if k[:5] in
                             ('route', 'smtp-', 'sourc', 'secur', 'local')]):
                if len(triplets) > 1 or kind != 'profile':
                    raise ValueError('Can only configure detailed settings '
                                     'for one profile at a time')

            # FIXME: Check more important invariants and raise

        def _configure_sending_route(self, vcard, route_id):
            # Sending route
            route = self.session.config.routes.get(route_id)
            protocol = self.data.get('route-protocol', ['none'])[0]
            if protocol == 'none':
                if route:
                    del self.session.config.routes[route_id]
                vcard.route = ''
                return
            elif protocol == 'local':
                route.password = route.username = route.host = ''
                route.name = _("Local mail")
                route.command = self.data.get('route-command', [None]
                                              )[0] or self._sendmail_command()
            elif protocol in ('smtp', 'smtptls', 'smtpssl'):
                route.command = ''
                route.name = vcard.email
                for var in ('route-username', 'route-auth_type',
                            'route-host', 'route-port'):
                    rvar = var.split('-', 1)[1]
                    route[rvar] = self.data.get(var, [''])[0]
                if 'route-password' in self.data:
                    route['password'] = self.data['route-password'][0]
            else:
                raise ValueError(_('Unhandled outgoing mail protocol: %s'
                                   ) % protocol)
            route.protocol = protocol
            vcard.route = route_id

        def _get_mail_spool(self):
            path = os.getenv('MAIL') or None
            user = os.getenv('USER')
            if user and not path:
                if os.path.exists('/var/spool/mail'):
                    path = os.path.normpath('/var/spool/mail/%s' % user)
                if os.path.exists('/var/mail'):
                    path = os.path.normpath('/var/mail/%s' % user)
            return path

        def _configure_mail_sources(self, vcard):
            config = self.session.config
            sources = [r[7:].rsplit('-', 1)[0] for r in self.data.keys()
                       if r.startswith('source-') and r.endswith('-protocol')]
            for src_id in sources:
                prefix = 'source-%s-' % src_id
                protocol = self.data.get(prefix + 'protocol', ['none'])[0]
                def configure_source(source):
                    source.host = ''
                    source.username = ''
                    source.enabled = self._yn(prefix + 'enabled')
                    source.discovery.create_tag = True
                    source.discovery.process_new = True
                    if src_id not in vcard.sources():
                        vcard.add_source(source._key)
                    return source
                def make_new_source():
                    # This little dance makes sure source is actually a
                    # config section, not just an anonymous dict.
                    if src_id not in config.sources:
                        config.sources[src_id] = {}
                    source = config.sources[src_id]
                    source.profile = vcard.random_uid
                    source.discovery.apply_tags = [vcard.tag]
                    return configure_source(source)

                if protocol == 'none':
                    pass

                elif protocol == 'local':
                    source = configure_source(vcard.get_source_by_proto(
                        'local', create=src_id))

                elif protocol == 'spool':
                    path = self._get_mail_spool()
                    if not path:
                        raise ValueError(_('Mail spool not found'))

                    if path in config.sys.mailbox.values():
                        raise ValueError(_('Already configured: %s') % path)
                    else:
                        mailbox_idx = config.sys.mailbox.append(path)

                    source = configure_source(vcard.get_source_by_proto(
                        'local', create=src_id))
                    src_id = source._key

                    # We need to communicate with the source below,
                    # so we save config to trigger instanciation.
                    self._background_save(config=True, wait=True)

                    inbox = [t._key for t in config.get_tags(type='inbox')]
                    local_copy = self._yn(prefix + 'copy-local')
                    if self._yn(prefix + 'delete-source'):
                        policy = 'move'
                    else:
                        policy = 'read'

                    src_obj = config.mail_sources[src_id]
                    src_obj.take_over_mailbox(mailbox_idx,
                                              policy=policy,
                                              create_local=local_copy,
                                              apply_tags=inbox,
                                              save=False)

                elif protocol in ('imap', 'imap_ssl', 'imap_tls',
                                  'pop3', 'pop3_ssl'):
                    source = make_new_source()

                    # Discovery policy
                    disco = source.discovery
                    if self._yn(prefix + 'index-all-mail'):
                        if self._yn(prefix + 'leave-on-server'):
                            disco.policy = 'sync'
                        else:
                            disco.policy = 'move'
                        disco.local_copy = True
                        disco.paths = ['']
                    else:
                        disco.policy = 'ignore'
                        disco.local_copy = False
                        disco.paths = []
                    disco.guess_tags = True

                    # Connection settings
                    for rvar in ('protocol', 'auth_type', 'username',
                                 'host', 'port'):
                        source[rvar] = self.data.get(prefix + rvar, [''])[0]
                    if (prefix + 'password') in self.data:
                        source['password'] = self.data[prefix + 'password'][0]
                    if (self._yn(prefix + 'force-starttls')
                            and source.protocol == 'imap'):
                        source.protocol = 'imap_tls'
                    username = source.username
                    if '@' not in username:
                        username += '@%s' % source.host
                    source.name = username

                    # We need to communicate with the source below,
                    # so we save config to trigger instanciation.
                    self._background_save(config=True, wait=True)
                    src_obj = config.mail_sources[src_id]

                else:
                    raise ValueError(_('Unhandled incoming mail protocol: %s'
                                       ) % protocol)

        def _new_key_created(self, event, vcard_rid, passphrase):
            config = self.session.config
            fingerprint = self._key_generator.generated_key
            if fingerprint:
                vcard = vcard_rid and config.vcards.get_vcard(vcard_rid)
                if vcard:
                    vcard.pgp_key = fingerprint
                    vcard.save()
                    event.message = _('The PGP key for %s is ready for use.'
                                      ) % vcard.email
                else:
                    event.message = _('PGP key generation is complete')

                # Record the passphrase!
                config.secrets[fingerprint] = {
                    'password': passphrase,
                    'policy': 'protect'}

                # FIXME: Toggle something that indicates we need a backup ASAP.
                self._background_save(config=True)
            else:
                event.message = _('PGP key generation failed!')
                event.data['keygen_failed'] = True

            event.flags = event.COMPLETE
            event.data['keygen_finished'] = int(time.time())
            config.event_log.log_event(event)

        def _create_new_key(self, vcard, keytype):
            passphrase = okay_random(20, self.session.config.master_key
                                     ).lower()
            passphrase = '-'.join([passphrase[i:i+4] for i in
                                   range(0, len(passphrase), 4)])
            random_uid = vcard.random_uid
            bits = int(keytype.replace('RSA', ''))
            key_args = {
                # FIXME: EC keys!
                'bits': bits,
                'name': vcard.fn,
                'email': vcard.email,
                'passphrase': passphrase,
                'comment': ''
            }
            event = Event(source=self,
                          flags=Event.INCOMPLETE,
                          data={'keygen_started': int(time.time()),
                                'profile_id': random_uid},
                          private_data=key_args)
            self._key_generator = GnuPGKeyGenerator(
               # FIXME: Passphrase handling is a problem here
               GnuPG(self.session.config, event=event),
               event=event,
               variables=dict_merge(GnuPGBaseKeyGenerator.VARIABLES, key_args),
               on_complete=(random_uid,
                            lambda: self._new_key_created(event, random_uid,
                                                          passphrase)))
            self._key_generator.start()
            self.session.config.event_log.log_event(event)

        def _configure_security(self, vcard):
            openpgp_key = self.data.get('security-pgp-key', [''])[0]
            if openpgp_key:
                if openpgp_key.startswith('!CREATE'):
                    key_type = openpgp_key[8:] or self.DEFAULT_KEYTYPE
                    self._create_new_key(vcard, key_type)
                else:
                    vcard.pgp_key = openpgp_key
                    # FIXME: Schedule a background sync job which edits
                    #        the key to add this Account as a UID, if it

            else:
                vcard.remove_all('key')

            # Set the following even if we don't have a key, so they don't
            # get lost if the user edits settings while a key is being
            # generated - or if they just deselect a key temporarily.

            # Encryption policy rules
            outg_auto = self._yn('security-best-effort-crypto')
            outg_sig  = self._yn('security-always-sign')
            outg_enc  = self._yn('security-always-encrypt')
            if outg_enc and outg_sig:
                vcard.crypto_policy = 'openpgp-sign-encrypt'
            elif outg_sig:
                vcard.crypto_policy = 'openpgp-sign'
            elif outg_enc:
                vcard.crypto_policy = 'openpgp-encrypt'
            elif outg_auto:
                vcard.crypto_policy = 'best-effort'
            else:
                vcard.crypto_policy = 'none'

            # Crypto formatting rules
            pgp_autocrypt    = self._yn('security-use-autocrypt')
            pgp_publish      = self._yn('security-publish-to-keyserver')
            pgp_keys         = self._yn('security-attach-keys')
            pgp_inline       = self._yn('security-prefer-inline')
            pgp_pgpmime      = self._yn('security-prefer-pgpmime')
            pgp_obscure_meta = self._yn('security-obscure-metadata')
            pgp_hdr_enc      = self._yn('security-openpgp-header-encrypt')
            pgp_hdr_sig      = self._yn('security-openpgp-header-sign')
            pgp_hdr_none     = self._yn('security-openpgp-header-none')
            pgp_hdr_both     = pgp_hdr_enc and pgp_hdr_sig
            if pgp_hdr_both:
                pgp_hdr_enc = pgp_hdr_sig = False
            if pgp_pgpmime and pgp_inline:
                pgp_pgpmime = pgp_inline = False
            vcard.crypto_format = ''.join([
                'openpgp_header:SE' if (pgp_hdr_both)     else '',
                'openpgp_header:S'  if (pgp_hdr_sig)      else '',
                'openpgp_header:E'  if (pgp_hdr_enc)      else '',
                'openpgp_header:N'  if (pgp_hdr_none)     else '',
                '+autocrypt'        if (pgp_autocrypt)    else '',
                '+send_keys'        if (pgp_keys)         else '',
                '+prefer_inline'    if (pgp_inline)       else '',
                '+pgpmime'          if (pgp_pgpmime)      else '',
                '+obscure_meta'     if (pgp_obscure_meta) else '',
                '+publish'          if (pgp_publish)      else ''
            ])

    return ProfileVCardCommand


class Profile(ProfileVCard(VCard)):
    """View profile"""


class AddProfile(ProfileVCard(AddVCard)):
    """Add profiles (Accounts)"""
    HTTP_POST_VARS = dict_merge(AddVCard.HTTP_POST_VARS, {
        'route_id': 'Route ID for sending mail',

        'signature': '.signature',
        'route-*': 'Route settings',
        'source-*': 'Source settings',
        'security-*': 'Security settings'
    })

    def _form_defaults(self):
        new_src_id = randomish_uid();
        return dict_merge(AddVCard._form_defaults(self), {
            'new_src_id': new_src_id,
            'signature': self._default_signature(),
            'route-protocol': 'none',
            'route-auth_type': 'password',
            'source-NEW-protocol': 'none',
            'source-NEW-auth_type': 'password',
            'source-NEW-leave-on-server': True,
            'source-NEW-index-all-mail': True,
            'source-NEW-force-starttls': False,
            'source-NEW-copy-local': True,
            'source-NEW-delete-source': False,
            'security-best-effort-crypto': True,
            'security-use-autocrypt': False,
            'security-always-sign': False,
            'security-always-encrypt': False,
            'security-always-encrypt': False,
            'security-attach-keys': True,  # FIXME: Autocrypt changes this
            'security-prefer-inline': False,
            'security-prefer-pgpmime': False,
            'security-obscure-metadata': False,
            'security-openpgp-header-encrypt': False,
            'security-openpgp-header-sign': True,
            'security-openpgp-header-none': False,
            'security-publish-to-keyserver': False
        });

    def _before_vcard_create(self, kind, triplets, vcard=None):
        route_id = self.data.get('route_id',
                                 [vcard and vcard.route or None])[0]
        if route_id:
            if route_id not in self.session.config.routes:
                raise ValueError('Not a valid route ID: %s' % route_id)
        elif self.data.get('route-protocol', ['none'])[0] != 'none':
            route_id = self.session.config.routes.append({})

        return {
            'save_config': True,
            'route_id': route_id
        }

    def _update_vcard_from_post(self, vcard, state=None):
        if not state:
            # When editing, this doesn't run first, so we invoke it now.
            state = self._before_vcard_create(vcard.kind, [], vcard=vcard)

        vcard.signature = self.data.get('signature', [''])[0]
        vcard.email = self.data.get('email', [None])[0] or vcard.email
        vcard.fn = self.data.get('name', [None])[0] or vcard.fn

        if not vcard.tag:
            with self.session.config._lock:
                tags = self.session.config.tags
                vcard.tag = tags.append({
                    'name': vcard.email,
                    'slug': '%8.8x' % time.time(),
                    'type': 'profile',
                    'icon': 'icon-user',
                    'flag_msg_only': True,
                    'label': False,
                    'display': 'invisible'
                })
                from mailpile.plugins.tags import Slugify
                tags[vcard.tag].slug = Slugify(
                    'account-%s' % vcard.email, tags=self.session.config.tags)

        route_id = state.get('route_id')
        if route_id:
            self._configure_sending_route(vcard, route_id)

        self._configure_mail_sources(vcard)
        self._configure_security(vcard)

    def _after_vcard_create(self, kind, vcard, state):
        self._update_vcard_from_post(vcard, state=state)


class EditProfile(AddProfile):
    """Edit a profile"""
    SYNOPSIS = (None, None, 'profiles/edit', None)
    HTTP_QUERY_VARS = dict_merge(AddProfile.HTTP_QUERY_VARS, {
        'rid': 'update by x-mailpile-rid',
    })

    def _vcard_to_post_vars(self, vcard):
        cp = vcard.crypto_policy or ''
        cf = vcard.crypto_format or ''
        vc_sig = vcard.signature
        default_sig = self._default_signature()
        pvars = {
            'rid': vcard.random_uid,
            'name': vcard.fn,
            'email': vcard.email,
            'signature': default_sig if (vc_sig is None) else vc_sig,
            'password': '',
            'route-protocol': 'none',
            'route-auth_type': 'password',
            'source-NEW-protocol': 'none',
            'source-NEW-auth_type': 'password',
            'security-pgp-key': vcard.pgp_key or '',
            'security-best-effort-crypto': ('best-effort' in cp),
            'security-use-autocrypt': ('autocrypt' in cf),
            'security-always-sign': ('sign' in cp),
            'security-always-encrypt': ('encrypt' in cp),
            'security-attach-keys': ('send_keys' in cf),
            'security-prefer-inline': ('prefer_inline' in cf),
            'security-prefer-pgpmime': ('pgpmime' in cf),
            'security-obscure-metadata': ('obscure_meta' in cf),
            'security-openpgp-header-encrypt': ('openpgp_header:E' in cf or
                                                'openpgp_header:SE' in cf),
            'security-openpgp-header-sign': ('openpgp_header:S' in cf or
                                             'openpgp_header:ES' in cf),
            'security-openpgp-header-none': ('openpgp_header:N' in cf),
            'security-publish-to-keyserver': ('publish' in cf)
        }
        route = self.session.config.routes.get(vcard.route or 'ha ha ha')
        if route:
            pvars.update({
                'route-protocol': route.protocol,
                'route-host': route.host,
                'route-port': route.port,
                'route-username': route.username,
                'route-password': route.password,
                'route-auth_type': route.auth_type or 'password',
                'route-command': route.command
            })
        pvars['sources'] = vcard.sources()
        for sid in pvars['sources']:
            prefix = 'source-%s-' % sid
            source = self.session.config.sources.get(sid)
            disco = source.discovery
            info = {}
            for rvar in ('protocol', 'auth_type', 'host', 'port',
                         'username', 'password'):
                info[prefix + rvar] = source[rvar]
            dp = disco.policy
            if not info[prefix + 'auth_type']:
                info[prefix + 'auth_type'] = 'password'
            info[prefix + 'leave-on-server'] = (dp not in 'move')
            info[prefix + 'index-all-mail'] = (dp in ('move', 'sync', 'read')
                                               and disco.local_copy)
            info[prefix + 'enabled'] = source.enabled
            if source.protocol == 'imap_tls':
                info[prefix + 'protocol'] = 'imap'
                info[prefix + 'force-starttls'] = True
            else:
                info[prefix + 'force-starttls'] = False

            pvars.update(info)
        return pvars

    def command(self):
        idx = self._idx()  # Make sure VCards are all loaded
        session, config = self.session, self.session.config

        # OK, fetch the VCard.
        safe_assert('rid' in self.data and len(self.data['rid']) == 1)
        vcard = config.vcards.get_vcard(self.data['rid'][0])
        safe_assert(vcard)

        if self.data.get('_method') == 'POST':
            self._update_vcard_from_post(vcard)
            self._background_save(config=True)
            vcard.save()
            return self._success(_('Account Updated!'),
                                 self._vcard_to_post_vars(vcard))
        else:
            return self._success(_('Edit Account'), dict_merge(
                 self._form_defaults(), self._vcard_to_post_vars(vcard)))


class RemoveProfile(ProfileVCard(RemoveVCard)):
    """Remove a profile"""
    HTTP_CALLABLE = ('GET', 'POST')
    HTTP_QUERY_VARS = dict_merge(RemoveVCard.HTTP_QUERY_VARS, {
        'rid': 'x-mailpile-rid of profile to remove'})
    HTTP_POST_VARS = {
        'rid': 'x-mailpile-rid of profile to remove',
        'trash-email': 'If yes, move linked e-mail to Trash',
        'delete-keys': 'If yes, delete linked PGP keys',
        'delete-tags': 'If yes, remove linked Tags'}

    def _trash_email(self, vcard):
        trashes = self.session.config.get_tags(type='trash', default=[])
        if vcard.tag and trashes:
            idx = self.session.config.index
            idx.add_tag(self.session, trashes[0]._key,
                        msg_idxs=idx.TAGS.get(vcard.tag, set()),
                        allow_message_id_clearing=True,
                        conversation=False)

    def _delete_keys(self, vcard):
        if vcard.pgp_key:
            found = 0
            for vc in self.session.config.vcards.find_vcards([], kinds=['profile']):
                if vc.pgp_key == vcard.pgp_key:
                    found += 1
            if found == 1:
                self._gnupg().delete_key(vcard.pgp_key)

    def _cleanup_tags(self, vcard, delete_tags=False):
        if vcard.tag:
            if delete_tags:
                from mailpile.plugins.tags import DeleteTag
                DeleteTag(self.session, arg=[vcard.tag]).run()
            else:
                self.session.config.tags[vcard.tag].type = 'attribute'
                self.session.config.tags[vcard.tag].display = 'invisible'
                self.session.config.tags[vcard.tag].label = True

    def _unique_usernames(self, vcard):
        config, usernames = self.session.config, set()

        if (vcard.route not in ('', None)) and config.routes[vcard.route].username:
            usernames.add(config.routes[vcard.route].username)
        for source_id in vcard.sources():
            if config.sources[source_id].username:
                usernames.add(config.sources[source_id].username)

        for msid, source in config.sources.iteritems():
            if (source.username in usernames) and (source.profile != vcard.random_uid):
                usernames.remove(source.username)
        for mrid, route in config.routes.iteritems():
            if (route.username in usernames) and (mrid != vcard.route):
                usernames.remove(source.username)

        return usernames

    def _delete_credentials(self, vcard):
        # Check every stored credential; if it is in use by any route or source that
        # doesn't belong to this VCard, leave intact. Otherwise, delete.
        usernames = self._unique_usernames(vcard)
        oauth_tks = self.session.config.oauth.tokens
        for username in (set(oauth_tks.keys()) & usernames):
            del oauth_tks[username]
        secrets = self.session.config.secrets
        for username in (set(secrets.keys()) & usernames):
            del secrets[username]

    def _delete_routes(self, vcard):
        if vcard.route not in (None, ''):
            found = 0
            for vc in self.session.config.vcards.find_vcards([], kinds=['profile']):
                if vc.route == vcard.route:
                    found += 1
            if found == 1:
                self.session.config.routes[vcard.route] = {}

    def _delete_sources(self, vcard, delete_tags=False):
        config, sources = self.session.config, self.session.config.sources
        for source_id in vcard.sources():
            src = sources[source_id]
            tids = set()

            # Tell the worker to shut down, if any
            if source_id in config.mail_sources:
                config.mail_sources[source_id].quit()
                config.mail_sources[source_id].event.flags = Event.COMPLETE

            # Keep the reference to our local mailboxes in sys.mailbox
            for mbx_id, mbx_info in src.mailbox.iteritems():
                if mbx_info.primary_tag:
                    tids.add(mbx_info.primary_tag)
                if mbx_info.local and (mbx_info.path[:1] == '@'):
                    config.sys.mailbox[mbx_info.path[1:]] = mbx_info.local

            # Reconfigure all the tags
            from mailpile.plugins.tags import DeleteTag
            if src.discovery.parent_tag:
                tids.add(src.discovery.parent_tag)
            for tid in tids:
                if ((tid in config.tags)
                        and config.tags[tid].type in ('mailbox', 'profile')):
                    config.tags[tid].type = 'tag'
                    if delete_tags:
                        DeleteTag(self.session, arg=[tid]).run()

            # Nuke it!
            sources[source_id] = {
                'name': 'Deleted source',
                'enabled': False}

    def _trash_email_is_safe(self, vcard):
        if vcard:
            for src_id in vcard.sources():
                if self.session.config.sources[src_id].protocol == 'local':
                    return False
            return True
        return False

    def command(self, *args, **kwargs):
        session, config = self.session, self.session.config

        if 'rid' in self.data:
            vcard = config.vcards.get_vcard(self.data['rid'][0])
        else:
            vcard = None

        if vcard and self.data.get('_method', 'not-http').upper() != 'GET':
            if self.data.get('trash-email', [''])[0].lower() == 'yes':
                self._trash_email(vcard)

            if self.data.get('delete-keys', [''])[0].lower() == 'yes':
                self._delete_keys(vcard)

            delete_tags = (self.data.get('delete-tags', [''])[0].lower() == 'yes')
            self._delete_credentials(vcard)
            self._delete_routes(vcard)
            self._delete_sources(vcard, delete_tags=delete_tags)
            self._cleanup_tags(vcard, delete_tags=delete_tags)
            self._background_save(config=True, index=True)

            return RemoveVCard.command(self, *args, **kwargs)

        return self._success(_("Remove account"), result=dict_merge(
            self._form_defaults(), {
                'rid': vcard.random_uid if vcard else None,
                'trash_email_is_safe': self._trash_email_is_safe(vcard),
                'profile': (self._vcard_list([vcard])['profiles'][0]
                            if vcard else None)}))


class ListProfiles(ProfileVCard(ListVCards)):
    """Find profiles"""
    SYNOPSIS = (None, 'profiles', 'profiles', '[--lines] [<terms>]')


class ProfileSet(ProfileVCard(VCardSet)):
    """Set contact lines, ensuring contact exists"""


class ChooseFromAddress(Command):
    """Display a single vcard"""
    SYNOPSIS = (None, 'profiles/choose_from', 'profiles/choose_from',
                '<MIDs or addresses>')
    ORDER = ('Internals', 6)
    HTTP_CALLABLE = ('GET',)
    HTTP_QUERY_VARS = {
        'mid': 'Message ID',
        'email': 'E-mail address',
        'no_from': 'Ignore From: lines'
    }

    def command(self):
        idx, vcards = self._idx(), self.session.config.vcards

        emails = [e for e in self.args if '@' in e]
        emails.extend(self.data.get('email', []))

        messages = self._choose_messages(
            [m for m in self.args if '@' not in m] +
            ['=%s' % mid for mid in self.data.get('mid', [])]
        )
        for msg_idx_pos in messages:
            try:
                msg_info = idx.get_msg_at_idx_pos(msg_idx_pos)
                msg_emails = (idx.expand_to_list(msg_info, field=idx.MSG_TO) +
                              idx.expand_to_list(msg_info, field=idx.MSG_CC))
                emails.extend(msg_emails)
                if 'no_from' not in self.data:
                    emails.append(msg_info[idx.MSG_FROM])
            except ValueError:
                pass

        addrs = [ai for ee in emails
                 for ai in AddressHeaderParser(unicode_data=ee)]
        return self._success(_('Choosing from address'), result={
            'emails': addrs,
            'from': vcards.choose_from_address(self.session.config, addrs)
        })


class ContentTxf(EmailTransform):
    def TransformOutgoing(self, sender, rcpts, msg, **kwargs):
        txf_matched, txf_continue = False, True

        profile = self._get_sender_profile(sender, kwargs)
        sig = profile.get('signature')
        if sig:
            part = self._get_first_part(msg, 'text/plain')
            if part is not None:
                msg_text = (part.get_payload(decode=True) or '\n\n'
                            ).replace('\r', '').decode('utf-8')
                if '\n-- \n' not in msg_text:
                    msg_text = msg_text.strip() + '\n\n-- \n' + sig
                    try:
                        msg_text.encode('us-ascii')
                        need_utf8 = False
                    except (UnicodeEncodeError, UnicodeDecodeError):
                        msg_text = msg_text.encode('utf-8')
                        need_utf8 = True

                    part.set_payload(msg_text)
                    if need_utf8:
                        part.set_charset('utf-8')
                        while 'content-transfer-encoding' in part:
                            del part['content-transfer-encoding']
                        encoders.encode_base64(part)

                    txf_matched = True

        return sender, rcpts, msg, txf_matched, txf_continue


_plugins.register_commands(VCard, AddVCard, RemoveVCard, ListVCards,
                           VCardAddLines, VCardSet, VCardRemoveLines)
_plugins.register_commands(Contact, AddContact, RemoveContact, ListContacts,
                           ContactSet, AddressSearch)
_plugins.register_commands(Profile, AddProfile, EditProfile,
                           RemoveProfile, ListProfiles, ProfileSet,
                           ChooseFromAddress)
_plugins.register_commands(ContactImport, ContactImporters)

_plugins.register_outgoing_email_content_transform('100_sender_vc', ContentTxf)
