#!/usr/bin/python

# vim:shiftwidth=2:tabstop=2:expandtab:textwidth=80:softtabstop=2:ai:
#
# Copyright (c) 2010-present Phil Dibowitz <phil@ipom.com>

'''Determine why signatures from/to a key are not cross-signatures.'''

__author__ = 'Phil Dibowitz <phil@ipom.com>'

import os.path
import subprocess
import sys
import re
from optparse import OptionParser

from libpius import util
from libpius.constants import *
from libpius.exceptions import *
from libpius import signer as psigner
from libpius import mailer as pmailer
from libpius.state import SignState

class PiusReporter(object):
  # states a key can be in
  kSIGNED_AND_UPLOADED = 'SIGNED_AND_NOT_UPLOADED'
  kSIGNED_BUT_NOT_UPLOADED = 'SIGNED_BUT_NOT_UPLOADED'
  kNOT_SIGNED = 'NOT_SIGNED'
  kWILL_NOT_SIGN = 'WILL_NOT_SIGN'

  kREPORT_DATA = {
      'need_upload': {
          'do_email': True,
          'title': 'The following people need to upload your signature:',
          'subject': 'Reminder: Please upload signature of your PGP key',
          'template': '''Dear %(name)s,

You were at the %(party)s PGP Keysigning Party and I have signed your key
(%(keyid)s) and with mine (%(signer)s) and sent it encrypted to you.
However, as far as I can tell you haven't uploaded 1 or more of my signatures.

This is a gentle reminder to please import the signatures and then upload your
key so that we can ensure a strong web of trust. Thanks!

For more information see this site:
  http://www.phildev.net/pgp/

Generated by PIUS Report BETA (http://www.phildev.net/pius/).
''',
      },
      'need_signature': {
          'do_email': True,
          'title': 'The following people need to sign your key:',
          'subject': 'Reminder: Please sign PGP keys',
          'template': '''Dear %(name)s,

You were at the %(party)s PGP Keysigning Party but I have not received a
signature from your key (%(keyid)s) on my key (%(signer)s).

This is a gentle reminder to please sign keys from the party and send them out.
Thanks!

For more information see this site:
  http://www.phildev.net/pgp/

Generated by PIUS Report BETA (http://www.phildev.net/pius/).
''',
      },
      '_need_both': {
          'subject': ('Reminder: Please sign PGP keys, and upload received'
                      ' signatures of your PGP key'),
          'template': '''Dear %(name)s,

You were at the %(party)s PGP Keysigning Party and I have signed your key
%(keyid)s and with mine (%(signer)s) and sent it encrypted to you.
However, I have neither received a signature from you on my key nor have you
(as far as I can tell) uploaded my signature of your key to a keyserver.

This is a gentle reminder to please sign keys from the party and send them out,
and import and upload signatures others have sent to you.

For more information see this site:
  http://www.phildev.net/pgp/

Generated by PIUS Report BETA (http://www.phildev.net/pius/).
''',
      },
      'must_sign': {
          'do_email': True,
          'title': 'You have not signed the following keys:'
      },
  }

  def __init__(self, gpg, signer_id, keyring, party, pius_state):
    print 'Loading keyring...'
    self.signer = signer_id
    self.keyring = keyring
    self.pius_state = pius_state
    self.party = party
    self.signed_us = []
    self.sigs = {}
    self.gpg = gpg

    # we can go ahead and load the key list off the keyring,
    # but we cannot load sigs until the user tells us to incase
    # they want to refresh them first.
    signer = psigner.PiusSigner(
        None, None, None, self.keyring, self.gpg, None, None, None, None, None,
        None, None, None, None
    )
    self.keys = signer.get_all_keyids()
    signer = None

    self.need_upload = []
    self.must_sign = []
    self.need_signature = []

  def load_keyring(self):
    self.sigs = self.get_local_sigs(self.keys)

  def generate(self):
    for key in self.keys:
      util.debug('Processing %s' % key)
      ret = self.we_signed_key(key)
      if ret == PiusReporter.kSIGNED_BUT_NOT_UPLOADED:
        util.debug(' -- they need to upload')
        self.need_upload.append(key)
      elif ret == PiusReporter.kNOT_SIGNED:
        util.debug(' -- we need to sign')
        self.must_sign.append(key)
      if not self.key_signed_us(key):
        util.debug(' -- they need to sign')
        self.need_signature.append(key)

  def report(self, mail, mailer):
    reports = {}
    for rtype in self.kREPORT_DATA.keys():
      if rtype.startswith('_'):
        continue
      reports[rtype] = self.get_uid_info(getattr(self, rtype))
      if len(reports[rtype]) == 0:
        continue
      print self.kREPORT_DATA[rtype]['title']
      self.print_uids(reports[rtype])
      print

    if mail:
      ans = raw_input(
          'Do you want to email the people who you are waiting on to remind '
          'them? (y/N) '
      )
      if not ans.lower() in ['y', 'yes']:
        print 'OK, skipping sending emails.'
        return

      # a list of people who we email about both problems.
      both = []
      for rtype in reports.keys():
        report = reports[rtype]
        report_info = self.kREPORT_DATA[rtype]
        if len(report) == 0 or not report_info['do_email']:
          continue

        for keyid in report.keys():
          if keyid in both:
            continue
          if (keyid in reports['need_signature'] and
              keyid in reports['need_upload']):
            both.append(keyid)
            template = self.kREPORT_DATA['_need_both']['template']
            subject = self.kREPORT_DATA['_need_both']['subject']
          else:
            template = report_info['template']
            subject = report_info['subject']
          uids = report[keyid]
          body = template % {
              'keyid': keyid,
              'name': uids[0]['name'],
              'email': uids[0]['email'],
              'signer': self.signer,
              'party': self.party,
              'from': mail,
          }
          util.debug("reminding %s (%s)" % (uids[0]['email'], subject))
          sys.stdout.write('Mailing %s... '% uids[0]['email'])
          mailer.send_mail(uids[0]['email'], subject, body)
          print 'done'

  def get_uid_info(self, uids):
    data = {}
    uid_re = re.compile(r'(.*) <(.*)>$')
    for uid in uids:
      data[uid] = []
      gpg = os.popen('%s --fixed-list-mode --with-colons --fingerprint %s'
                     % (self.gpg, uid))
      for line in gpg:
        if line.startswith('uid'):
          match = uid_re.search(line.split(':')[9])
          if match:
            datum = {'name': match.group(1), 'email': match.group(2)}
            data[uid].append(datum)
      gpg.close()
    return data

  def print_uids(self, keys):
    '''Print UIDs for each key in keys.'''
    for keyid in keys.keys():
      uids = keys[keyid]
      print ' ', keyid
      for uid in uids:
        print '    - %s <%s>' % (uid['name'], uid['email'])
    return

  def get_local_sigs(self, keys):
    cmd = [
        self.gpg,
        '--fixed-list-mode', '--with-colons',
        '--keyid-format', 'long',
        '--list-sigs',
    ] + keys
    sigs = {}
    util.logcmd(cmd)
    gpg = subprocess.Popen(cmd, close_fds=True, stdout=subprocess.PIPE)
    current_key = None
    for line in gpg.stdout:
      line = line.strip()
      if line.startswith('pub:'):
        current_key = line.split(':')[4]
        continue
      if not line.startswith('sig:'):
        continue
      keyid = line.split(':')[4]
      if not current_key in sigs:
        sigs[current_key] = []
      sigs[current_key].append(keyid)
    return sigs

  def ask_user(self, key):
    cmd = [
        self.gpg,
        '--no-default-keyring',
        '--keyring', self.keyring,
        '--fingerprint', key,
    ]
    util.logcmd(cmd)
    gpg = subprocess.Popen(
        cmd,
        stdout=subprocess.PIPE,
        close_fds=True
    )
    output = gpg.stdout.read()
    output = output.strip()
    retval = gpg.wait()
    if retval != 0:
      print 'KeyID not found, bailing out!'
      sys.exit(1)
    print 'There is no record of you signing this key...'
    print output
    print ' (1) I have signed it'
    print ' (2) I don\'t want to / will not sign it'
    print ' (3) Oops, report that as something I should sign'
    ans = None
    while not ans in ['1', '2', '3']:
      ans = raw_input('Please choose 1, 2, or 3 > ')
    if ans == '1':
      return SignState.kSIGNED
    if ans == '2':
      return SignState.kWILL_NOT_SIGN
    if ans == '3':
      return SignState.kNOT_SIGNED

  def we_signed_key(self, key):
    # If the local primary keychain includes our signature on their key
    # we signed it and they uploaded it...
    if self.signer in self.sigs[key]:
      return PiusReporter.kSIGNED_AND_UPLOADED

    # If that's not the case, but we recorded signing it, they never uploaded it
    if self.pius_state.signed(key):
      return PiusReporter.kSIGNED_BUT_NOT_UPLOADED

    if self.pius_state.will_not_sign(key):
      return PiusReporter.kWILL_NOT_SIGN

    return PiusReporter.kNOT_SIGNED

  def key_signed_us(self, key):
    if len(self.signed_us) == 0:
      self.signed_us = self.get_local_sigs([self.signer])[self.signer]

    return key in self.signed_us

  def check_missing_sign_data(self):
    unsigned_keys = []
    for key in self.keys:
      if key not in self.pius_state and self.signer not in self.sigs[key]:
        unsigned_keys.append(key)
    if len(unsigned_keys) > 0:
      print('Not all keys on this keyring were signed by you. Would you'
            ' like me to:')
      print ' (1) Assume any keys not signed on this keyring are by choice.'
      print ' (2) Ask about any keys I don\'t know about.'
      print ' (3) Report them as keys you need to sign.'
      ans = None
      while not ans in ['1', '2', '3']:
        ans = raw_input('Please choose 1, 2, or 3 > ')

      if ans == '1':
        for key in unsigned_keys:
          self.pius_state.update(key, SignState.kWILL_NOT_SIGN)
        return

      if ans == '2':
        for key in unsigned_keys:
          ret = self.ask_user(key)
          self.pius_state.update(key, ret)

      if ans == '3':
        return

def refresh_keys(gpg, keys):
  cmd = [
      gpg,
      '--refresh-keys',
  ] + keys
  util.logcmd(cmd)
  gpg = subprocess.Popen(
      cmd,
      stdout=subprocess.PIPE,
      close_fds=True
  )
  gpg.wait()


def main():
  '''Main.'''

  parser = OptionParser(option_class=util.MyOption)
  parser.set_defaults(gpg_path=DEFAULT_GPG_PATH)
  parser.add_option('-b', '--gpg-path', dest='gpg_path', metavar='PATH',
                    nargs=1,# type="not_another_opt",
                    help='Path to gpg binary. [default: %default]')
  parser.add_option('-d', '--debug', action='store_true', dest='debug',
                    help='Debug')
  parser.add_option('-s', '--signer', dest='keyid',
                    help='The key you sign with.')
  parser.add_option('-r', '--keyring', dest='keyring', help='Party Keyring')
  parser.add_option('-m', '--mail', dest='mail', metavar='EMAIL', nargs=1,
                    type='email',
                    help='Email the encrypted, signed keys to the'
                         ' respective email addresses. EMAIL is the address'
                         ' to send from. See also -H and -P.')
  parser.add_option('-t', '--tmp-dir', dest='tmp_dir', nargs=1,
                    type='not_another_opt',
                    help='Directory to put temporary stuff in. [default:'
                         ' %default]')
  parser.add_option('-p', '--party', dest='party', metavar='NAME', nargs=1,
                    help='The name of the party. This will be printed in the'
                         ' emails sent out. Only useful with -m.')
  parser.add_option('--no-refresh', dest='refresh', action='store_false',
                    default=True)

  pmailer.PiusMailer.add_options(parser)

  all_opts = []
  all_opts = util.parse_dotfile(parser)
  all_opts.extend(sys.argv[1:])

  options, _ = parser.parse_args(all_opts)

  if options.debug:
    util.DEBUG_ON = True

  if not options.keyid:
    print 'Missing signer option'
    sys.exit(1)

  if len(options.keyid) == 8:
    print 'You must using long key IDs, for security'
    sys.exit(1)

  if not options.keyring:
    print 'Missing keyring option'
    sys.exit(1)

  if not options.party:
    print 'Missing party options'
    sys.exit(1)

  print 'Loading PIUS state...'
  signed_keys = SignState()

  report = PiusReporter(
      options.gpg_path,
      options.keyid,
      options.keyring,
      options.party,
      signed_keys
  )

  if options.refresh:
    print 'Refreshing keys...'
    refresh_keys(options.gpg_path, list(set(report.keys + [options.keyid])))

  report.load_keyring()

  # OK, for any keys we don't have some info on, let's ask
  report.check_missing_sign_data()
  # Crunch the numbers...
  report.generate()
  if options.mail:
    mailer = pmailer.PiusMailer(
        options.mail,
        options.mail_host,
        options.mail_port,
        options.mail_user,
        options.mail_tls,
        options.mail_no_pgp_mime,
        options.mail_override,
        options.mail_text,
        options.tmp_dir
    )

  if options.mail_user:
    while True:
      mailer.get_pass()
      try:
        if not mailer.verify_pass():
          print ('Sorry, cannot authenticate to %s as %s with that passwword,'
                 ' try again.' % (options.mail_host, options.mail_user))
        else:
          break
      except MailSendError, msg:
        print ('There was a problem talking to the mail server (%s): %s'
               % (options.mail_host, msg))
        sys.exit(1)

  # and emit the report
  report.report(options.mail, mailer)

  if signed_keys.modified:
    ans = raw_input(
        'Would you like to save the info about keys you\'ve signed? (y/N) '
    )
    if ans.lower() in ('y', 'yes'):
      signed_keys.save()
    else:
      print 'Discarding modified state...'

if __name__ == '__main__':
  main()

