#!/usr/bin/python
"""A utility to sign all UIDs on a list of PGP keys and PGP/Mime encrypt-email
them to the respective emails."""

# vim:shiftwidth=2:tabstop=2:expandtab:textwidth=80:softtabstop=2:ai:

#
# Copyright (c) 2008 - present Phil Dibowitz (phil@ipom.com)
#
#   This program is free software; you can redistribute it and/or
#   modify it under the terms of the GNU General Public License as
#   published by the Free Software Foundation, version 2.
#
# Note that we only import pexpect if -i is specified. In order to do this
# in a relatively clean way (if you just import it it will only be local), we
# use a trick that pylint will complain about. I'm quite alright with that.
#
# TODO:
#   - Offer ability to "pick up where we left off"
#

from libpius import signer as psigner
from libpius import mailer as pmailer
from libpius.state import SignState
from libpius.constants import *
from libpius.exceptions import *
from libpius import util
from optparse import OptionParser
import os
import sys

def print_default_email(no_mime):
  '''Print the default email that is sent out.'''
  interpolation_dict = {'keyid': '<keyid>', 'signer': '<signer>',
                        'email': '<email>'}
  print 'DEFAULT EMAIL TEXT:\n'
  if not no_mime:
    print DEFAULT_MIME_EMAIL_TEXT % interpolation_dict
  else:
    print DEFAULT_NON_MIME_EMAIL_TEXT % interpolation_dict



def check_options(parser, options, args):
  '''Given the parsed options, sanity check them.'''

  if options.debug == True:
    print 'Setting debug'
    util.DEBUG_ON = True

  if not os.path.exists(options.gpg_path):
    parser.error('GnuPG binary not found at %s.' % options.gpg_path)

  if not options.signer:
    parser.error('You must specify a keyid to sign with.')

  if options.keyring:
    options.keyring = os.path.expanduser(options.keyring)
    if not os.path.exists(options.keyring):
      parser.error('Keyring %s doesn\'t exist' % options.keyring)

  if not options.all_keys:
    if not args:
      parser.error('Keyid (or -A) required')
  elif not options.keyring:
    parser.error('The -A options requires the -r option')

  if options.mail and options.mail_no_pgp_mime and not options.encrypt_outfiles:
    print 'NOTE: -O and -m are present, turning on -e'
    options.encrypt_outfiles = True

  if options.mail_user and not options.mail_tls:
    print 'NOTE: -u is present, turning off -S.'
    options.mail_tls = True

  if options.mail_text and not options.mail:
    parser.error('ERROR: -M requires -m')

  for mydir in (options.tmp_dir, options.out_dir):
    if os.path.exists(mydir) and not os.path.isdir(mydir):
      parser.error('%s exists but isn\'t a directory. It must not exist or be\n'
                   'a directory.' % mydir)
    if not os.path.exists(mydir):
      os.mkdir(mydir, 0700)

def warn_if_short_keyids(ids):
  for keyid in ids:
    if len(keyid) == 8:
      print('WARNING: You passed in short keyids. Short keyids are forgable'
            ' and should be avoided.')
      ans = raw_input('Type "I understand" to continue: ')
      if ans == 'I understand':
        return
      else:
        print 'ERROR: Danger not acknowledged, exiting.'
        sys.exit(1)

def warn_for_gpg1():
  print(
    "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
    "\n"
    "!! WARNING: You are using GnuPG 1.x. Support for the 1.x series will be !!"
    "\n"
    "!!          dropped in the next release, please migrated to GnuPG 2.x.  !!"
    "\n"
    "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
    "\n"
  )

def main():
  """Main."""
  usage = ('%prog [options] -s <signer_keyid> <keyid> [<keyid> ...]\n'
           '       %prog [options] -A -r <keyring_path> -s <signer_keyid>')
  parser = OptionParser(usage=usage, version='%%prog %s' % VERSION,
                        option_class=util.MyOption)
  parser.set_defaults(mode=MODE_AGENT,
                      gpg_path=DEFAULT_GPG_PATH,
                      out_dir=DEFAULT_OUT_DIR,
                      tmp_dir=DEFAULT_TMP_DIR,
                      keyring=DEFAULT_KEYRING,
                      sort_keyring=True)
  parser.add_option('-a', '--use-agent', action='store_const', const=MODE_AGENT,
                    dest='mode',
                    help='Use pgp-agent instead of letting gpg prompt the'
                         ' user for every UID. [default: true]')
  parser.add_option('-A', '--all-keys', action='store_true', dest='all_keys',
                    help='Sign all keys on the keyring. Requires -r.')
  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('-e', '--encrypt-outfiles', action='store_true',
                    dest='encrypt_outfiles',
                    help='Encrypt output files with respective keys.')
  parser.add_option('-d', '--debug', action='store_true', dest='debug',
                    help='Enable debugging output.')
  parser.add_option('-H', '--mail-host', dest='mail_host', metavar='HOSTNAME',
                    nargs=1, type='not_another_opt',
                    help='Hostname of SMTP server. [default: %default]')
  parser.add_option('-i', '--interactive', action='store_const',
                    const=MODE_INTERACTIVE, dest='mode',
                    help='Use the pexpect module for signing and drop to the'
                         ' gpg shell for entering the passphrase. [default:'
                         ' false]')
  parser.add_option('-I', '--import', action='store_true',
                    dest='import_keyring',
                    help='Also import the unsigned keys from the keyring'
                         ' into the default keyring. Ignored if -r is not'
                         ' specified, or if it\'s the same as the default'
                         ' 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('-M', '--mail-text', dest='mail_text', metavar='FILE',
                    nargs=1, type='not_another_opt',
                    help='Use the text in FILE as the body of email when'
                         ' sending out emails instead of the default text.'
                         ' To see the default text use'
                         ' --print-default-email. Requires -m.')
  parser.add_option('-N', '--no-sort-keyring', dest='sort_keyring',
                    action='store_false',
                    help='Do not sort the keyring by name.')
  parser.add_option('-n', '--override-email', dest='mail_override',
                    metavar='EMAIL', nargs=1, type='email',
                    help='Rather than send to the user, send to this address.'
                         ' Mostly useful for debugging.')
  parser.add_option('-o', '--out-dir', dest='out_dir', metavar='OUTDIR',
                    nargs=1, type='not_another_opt',
                    help='Directory to put signed keys in. [default: %default]')
  parser.add_option('-O', '--no-pgp-mime', action='store_true',
                    dest='mail_no_pgp_mime',
                    help='Do not use PGP/Mime when sending email.')
  parser.add_option('-p', '--cache-passphrase', action='store_const',
                    const=MODE_CACHE_PASSPHRASE, dest='mode',
                    help='Cache private key passphrase in memory and provide'
                         ' it to gpg instead of letting gpg prompt the user'
                         ' for every UID. [default: false]')
  parser.add_option('-P', '--mail-port', dest='mail_port', metavar='PORT',
                    nargs=1, type='int',
                    help='Port of SMTP server. [default: %default]')
  parser.add_option('-r', '--keyring', dest='keyring', metavar='KEYRING',
                    nargs=1, type='not_another_opt',
                    help='The keyring to use. Be sure to specify full or'
                         ' relative path. Just a filename will cause GPG to'
                         ' assume relative to ~/.gnupg. [default: %default]')
  parser.add_option('-s', '--signer', dest='signer', nargs=1,
                    type='keyid',
                    help='The keyid to sign with (required).')
  parser.add_option('-S', '--no-mail-tls', action='store_false',
                    dest='mail_tls',
                    help='Do not use STARTTLS when talking to the SMTP server.')
  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('-T', '--print-default-email', dest='print_default_email',
                    action='store_true', help='Print the default email.')
  parser.add_option('-u', '--mail-user', dest='mail_user', metavar='USER',
                    type='not_another_opt', nargs=1,
                    help='Authenticate to the SMTP server, and use username'
                         ' USER. You will be prompted for the password.')
  parser.add_option('-U', '--policy-url', dest='policy_url',
                    help='Policy URL to include in each signature')
  parser.add_option('-v', '--verbose', dest='verbose', action='store_true',
                    help='Be more verbose.')

  # Check for extra options in the ~/.pius file
  all_opts = []
  all_opts = util.parse_dotfile(parser)
  # Note that by putting this at the end we allow the command line to override
  # options specified in the config file, BUT if any options conflict, the first
  # wins, so the config file wins. Meh.
  all_opts.extend(sys.argv[1:])
  (options, args) = parser.parse_args(all_opts)

  print 'Welcome to PIUS, the PGP Individual UID Signer.\n'

  # The easy thing first...
  if options.print_default_email:
    print_default_email(options.mail_no_pgp_mime)
    sys.exit(0)

  # Check input to make sure users want sane things
  check_options(parser, options, args)

  # Check to see if the user wants to send email if they didn't specify
  if not options.mail:
    ans = raw_input('Would you like to automatically send the signed UIDs to'
                    ' their owners using\nPGP/Mime encryption as you sign each'
                    ' one? ')
    if ans in ('y', 'Y', 'yes', 'YES', 'Yes'):
      ans = raw_input('What email address should we send from? ')
      util.check_email(parser, '-m', ans)
      options.mail = ans
      print

  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
    )
  else:
    mailer = None

  signer = psigner.PiusSigner(
      options.signer,
      options.mode,
      options.keyring,
      options.gpg_path,
      options.tmp_dir,
      options.out_dir,
      options.encrypt_outfiles,
      options.mail,
      mailer,
      options.verbose,
      options.sort_keyring,
      options.policy_url,
      options.mail_host
  )

  if signer.gpg2:
    if options.mode != MODE_AGENT:
      print "ERROR: gpg2 requires using an agent\n"
      sys.exit(1)
  else:
    warn_for_gpg1()

  if options.all_keys:
    key_list = signer.get_all_keyids()
    if len(key_list) == 0:
      print "ERROR: Failed to find keys on this keyring\n"
      sys.exit(1)
    if args:
      key_list.extend(args)
  else:
    warn_if_short_keyids(args)
    key_list = args

  if options.mode == MODE_CACHE_PASSPHRASE:
    print 'NOTE: See the README about security implications.'
    while True:
      signer.get_passphrase()
      if not signer.verify_passphrase():
        print 'Sorry, cannot unlock the key with that passphrase, try again.'
      else:
        break

  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)

  # The actual signing
  signed_keys = {}
  for key in key_list:
    retval = signer.check_fingerprint(key)
    if retval == False:
      continue
    print 'Signing all UIDs on key %s' % key
    if signer.sign_all_uids(key, retval):
      signed_keys[key] = True
    print ''

  # If the user asked, import the keys
  if options.import_keyring:
    if (not options.keyring) or (options.keyring == DEFAULT_KEYRING):
      print ('WARNING: Ignoring -i: Either -r wasn\'t specified, or it was'
             ' the same as the default keyring.')
    else:
      signer.import_unsigned_keys()

  signer.cleanup()
  SignState.store_signed_keys(dict((x, SignState.kSIGNED) for x in signed_keys))

if __name__ == '__main__':
  main()
