#!/usr/bin/env ruby

$:.unshift File.dirname(__FILE__) + '/../lib'
require 'schleuder'
require 'etc'
require 'open3'

class ListCreator
  def self.usage
    puts "Usage:
  Required options: 
     listname@hostname.tld
  Not required options (user will be promted unless -nointeractive is set or not run in a terminal),
     -realname \"Foo List\"
     -adminaddress listadmin@foobar.com
     -initmember member1@foobar.com -initmemberkey /path/to/initmember_publickey
  Optional options (flags on the same line have to be used together):
     -mailuser mail (The user, which will invoke schleuder from your MTA, if non is supplied, the current user is taken)
     -privatekeyfile /path/to/privatekey -publickeyfile /path/to/publickey -passphrase key_passphrase
     -nointeractive

#{File.basename($0)} listname@hostname.tld (-realname \"Foo List\") (-adminaddress listadmin@foobar.com) (-initmember member1@foobar.com -initmemberkey /path/to/initmember_publickey) [-privatekeyfile /path/to/privatekey -publickeyfile /path/to/publickey -passphrase key_passphrase] [-nointeractive]"
    exit 1
  end
  
  
  def self.process(arg)
    # set safe umask
    File.umask(0077)

    listname = ARGV.shift.to_s
    usage unless listname.split('@').size == 2
    args = Hash.new
    interactive = STDIN.tty?
    while nextarg = ARGV.shift
      if nextarg == '-realname'
        args[:list_realname] = ARGV.shift
      elsif nextarg == '-c'
        Schleuder.config(ARGV.shift)
      elsif nextarg == '-adminaddress'
        args[:list_adminaddress] = ARGV.shift
      elsif nextarg == '-initmember'
        args[:list_initmember] = ARGV.shift
      elsif nextarg == '-initmemberkey'
        args[:list_initmemberkey] = ARGV.shift
      elsif nextarg == '-privatekeyfile'
        args[:list_privatekeyfile] = ARGV.shift
      elsif nextarg == '-publickeyfile'
        args[:list_publickeyfile] = ARGV.shift
      elsif nextarg == '-passphrase'
        args[:list_passphrase] = ARGV.shift
      elsif nextarg == '-mailuser'
        args[:mailuser] = ARGV.shift
      elsif nextarg == '-nointeractive'
        interactive = false
      else
        usage
      end
    end
    Schleuder.log.debug "Calling Processor.newlist(#{listname})"
    begin
      ListCreator::create(listname,interactive,args)
    rescue NewListError => e
      puts "Error while creating new list: " + e.message
      exit 1
    end
  end

  # Creates a new list
  # listname: name of the list
  # interactive: Wether we can ask for missing informations. This requires ruby-highline! (Default: true)
  # args: additional parameters as hash
  def self.create(listname,interactive=true,args=nil)
    
    # verfiy all arguments quite in a huge block
    Schleuder.log.debug "Verifying arguments..."
    args = Hash.new if args.nil?
    begin
      require 'highline/import' if interactive
    rescue LoadError => ex
      puts "Unable to load 'highline'.\n\n"
      puts "Please install the highline gem before trying to use"
      puts "#{$0} in interactive mode."
      exit 1
    end

    # verify basic information
    Schleuder.log.debug "Verifying basic information..."
    listname = ListCreator::verify_strvar(listname,interactive,"The listname")
    listdir = File.join([Schleuder.config.lists_dir, listname.split('@').reverse].flatten)
    raise NewListError, "List or parts of a list named: #{listname} already exists!" if File.directory?(listdir)
    list_email = ListCreator::verify_emailvar(listname,interactive,"The lists's email address")
    list_realname = ListCreator::verify_strvar(args[:list_realname],interactive,"'Realname' (for GPG-key and email-headers)")
    list_adminaddress = ListCreator::verify_emailvar(args[:list_adminaddress],interactive,"Admin email address")

    raise NewListError,"Lists' email address and the admin address can't be the same" if list_email == list_adminaddress

    # verify keyfiles
    Schleuder.log.debug "Verifying keyfiles..."
    list_privatekeyfile = args[:list_privatekeyfile] ||  'none'
    list_publickeyfile = args[:list_publickeyfile] || 'none'
    list_passphrase = args[:list_passphrase] || 'none'
    unless args[:mailuser].nil?
      mailuser = Etc.getpwnam(args[:mailuser]).uid
    else
      mailuser = Process::Sys.getuid
    end
    unless (list_privatekeyfile == 'none') and 
        (list_publickeyfile == 'none') and 
        (list_passphrase == 'none') then
      list_privatekeyfile = ListCreator::verify_filevar(
          args[:list_privatekeyfile] || '',
          interactive,
          "the lists' private key file"
           )
      list_publickeyfile = ListCreator::verify_filevar(
          args[:list_publickeyfile] || '',
          interactive,
          "the lists' public key file"
          )
      list_passphrase = ListCreator::verify_strvar(
          args[:list_passphrase] || '', 
          interactive,
          "the lists' key passphrase",
          false
          )
    end

    # Verify init member
    Schleuder.log.debug "Verifying init member..."
    list_initmember = ListCreator::verify_emailvar(
        args[:list_initmember] || '',
        interactive,
        "Email address of the lists' initial member"
        )
    list_initmemberkey = ListCreator::verify_filevar(
        args[:list_initmemberkey] || '',
        interactive,
        "the public key of the lists' initial member"
        )
    Schleuder.log.debug "Arguments verified..."

    Schleuder.log.debug "Initialize list..."
    list = ListCreator::init_list(listname,listdir)

    Schleuder.log.debug "Set list options..."
    list.config.myaddr = list_email.to_s
    list.config.myname = list_realname.to_s

    if list_passphrase == 'none' then
      list.config.gpg_password = Schleuder::Utils::random_password.to_s
    else
      list.config.gpg_password = list_passphrase.to_s
    end
    
    if  (list_privatekeyfile == 'none' and list_publickeyfile == 'none') then
      Schleuder.log.debug "Generate list's keypair..."
      puts "Creating list key, this can take some time..." if interactive
      ListCreator::generate_fresh_keypair(listdir,list.config,interactive)
    else
      Schleuder.log.debug "Import list's keypair..."
      ListCreator::import_keypair(list,list_privatekeyfile,list_publickeyfile) 
    end
    if  (list_initmember != 'none' and list_initmemberkey != 'none') then
      Schleuder.log.debug "Add initmember to list..."
      ListCreator::add_init_member(list,list_initmember,list_initmemberkey)
    end
    
    # set the lists key_fingerprint
    list.config.key_fingerprint = list.key_fingerprint

    # add the admin here, as we should have already imported the key at this point
    new_admin = Schleuder::Member.new(:email => list_adminaddress.to_s)
    key, msg = new_admin.key
    if key
      new_admin.key_fingerprint = key.subkeys.first.fingerprint
      list.config.admins = new_admin
    else
      raise NewListError,"Could not find a suitable key for the list admin. Reason: #{msg}"
    end

    # store the config
    Schleuder.log.debug "Store list config..."
    list.config = list.config
    Schleuder.log.debug "Changing ownership..."
    ListCreator::filepermissions(listdir,mailuser)
    Schleuder.log.debug "List successfully created..."
    ListCreator::print_list_infos(list) if interactive
  end

  private

  def self.init_list(listname,listdir)
    require 'fileutils'
    FileUtils.mkdir_p(listdir)
    list = Schleuder::List.new(listname,true)
    ENV['GNUPGHOME'] = listdir
    list
  end

  def self.add_init_member(list,list_initmember,list_initmemberkey)
    if key = Schleuder::Crypt.new(list.config.gpg_password).add_key_from_file(list_initmemberkey).imports.first
      list.members = Array.new(1,Schleuder::Member.new({ :email => list_initmember, :key_fingerprint => key.fingerprint }))
    else
      raise NewListError,"Importing the init member key failed for some reason. Please verify the passed keyfile!"
    end
  end

  def self.verify_strvar(var,interactive,question, echo=true)
    if (var.nil? or var.empty?) and interactive then
      str = question+": "
      if echo
        var = ask(str)
      else
        var = ask(str) { |question| question.echo = '*' }
      end
    end
    raise NewListError,"Missing mandatory variable: "+question if (var.nil? or var.empty?)
    var
  end

  def self.verify_emailvar(var,interactive,question)
    var = ListCreator::verify_strvar(var,interactive,question)
    begin
      Schleuder::Utils::verify_addr(question,var) 
    rescue Exception => e
      raise NewListError,"Mandatory emailaddress (#{question}) is not valid: " + e.message
    end
    var
  end

  def self.verify_filevar(var,interactive,question)
    if (not var.nil? and not File.exist?(var)) and interactive then
      var = ask("Filepath for "+question+": ")
    end
    raise NewListError,"Missing mandatory file: "+question if (not var.nil? and not File.exist?(var))
    var
  end

  def self.progfunc(hook, what, type, current, total)
    $stderr.write("#{what}: #{current}/#{total}\r")
    $stderr.flush
  end


  def self.generate_fresh_keypair(listdir,listconfig,interactive)
    _name = listconfig.myname
    _email = listconfig.myaddr
    _pass = listconfig.gpg_password
    _type = Schleuder.config.gpg_key_type
    _length = Schleuder.config.gpg_key_length
    _sub_type = Schleuder.config.gpg_subkey_type
    _sub_length = Schleuder.config.gpg_subkey_length
    if GPGME.respond_to? 'check_version'
      GPGME::check_version('0.0.0')
    end
    GPGME::Ctx.new.genkey(
      ListCreator::create_gnupg_params_template(_name,_email,_pass,_type,_length,_sub_type,_sub_length),
      nil,nil
    ) 

    # Add listname-request@hostname as UID.
    gpg_adduid = "gpg --no-tty --command-fd 0 --status-fd 1 --yes --edit-key #{_email} adduid"
    Open3.popen3(gpg_adduid) do |stdin, stdout|
      owner_done = false
      request_done = false
      while line = stdout.readline rescue nil;
        case line.chomp
        when '[GNUPG:] GET_LINE keygen.name' then
          reply = _name
        when '[GNUPG:] GET_LINE keygen.email' then
          if ! request_done
            email = _email.sub(/@/, '-request@')
            request_done = true
          else
            email = _email.sub(/@/, '-owner@')
          end
          reply = email
        when '[GNUPG:] GET_LINE keygen.comment' then
          reply = 'schleuder list'
        when '[GNUPG:] GET_HIDDEN passphrase.enter' then
          reply = _pass
        when '[GNUPG:] GET_LINE keyedit.prompt' then
          if ! owner_done
            reply = "adduid"
            owner_done = true
          else
            reply = "save"
          end
        else
          reply = nil
        end
        #$stderr.puts line
        if reply
          #$stderr.puts reply
          stdin.puts reply
        end
      end
    end

    # Make list@host the primary UID to avoid confusion.
    # For some f***** up reason these two time calling gpg do not work in one run.
    gpg_adduid = "gpg --no-tty --command-fd 0 --status-fd 1 --yes --edit-key #{_email}"
    Open3.popen3(gpg_adduid) do |stdin, stdout|
      uid_done = false
      primary_done = false
      while line = stdout.readline rescue nil;
        case line.chomp
        when '[GNUPG:] GET_LINE keyedit.prompt' then
          if ! uid_done
            reply = "uid 2"
            uid_done = true
          elsif ! primary_done
            reply = "primary"
            primary_done = true
          else
            reply = "save"
          end
        when '[GNUPG:] GET_HIDDEN passphrase.enter' then
          reply = _pass
        else
          reply = nil
        end
        #$stderr.puts line
        if reply
          #$stderr.puts reply
          stdin.puts reply
        end
      end
    end
    $stderr.puts
  end

  def self.import_keypair(list,list_privatekeyfile,list_publickeyfile)
    crypt = Schleuder::Crypt.new(list.config.gpg_password)
    Schleuder.log.debug "Importing private key from #{list_privatekeyfile}"
    crypt.add_key_from_file(list_privatekeyfile)
    Schleuder.log.debug "Importing public key from #{list_publickeyfile}"
    crypt.add_key_from_file(list_publickeyfile)
  end
    
  def self.create_gnupg_params_template(name,email,pass,type,length,sub_type,sub_length)
    "<GnupgKeyParms format=\"internal\">
Key-Type: #{type}
Key-Length: #{length}
Subkey-Type: #{sub_type}
Subkey-Length: #{sub_length}
Name-Real: #{name}
Name-Comment: schleuder list
Name-Email: #{email}
Expire-Date: 0
Passphrase: #{pass}
</GnupgKeyParms>"
  end

  def self.filepermissions(listdir, mailuser)
    File.chown(mailuser,nil,listdir)
    File.chmod(0700,listdir)
    Dir.new(listdir).each{ |f|
      unless f =~ /^\./
        File.chown(mailuser,nil,listdir+"/"+f)
        File.chmod(0600)
      end
    }
  end

  def self.print_list_infos(list)
    puts "A new schleuder list called '#{list.config.myname}' has been created.".fmt
    puts
    puts "To get a working list you have to tell your MTA to handle this list. For various examples have a look at <http://schleuder.nadir.org/documentation/creating_lists.html>".fmt
    puts
    puts "Lists' key fingerprint:".fmt
    crypt =  Schleuder::Crypt.new(list.config.gpg_password)
    key = crypt.get_key(list.config.myaddr).first
    puts Schleuder::Utils::get_pretty_fingerprint(key)
  end
end

begin
  ListCreator.process(ARGV)
rescue NewListError => e
  puts "Error while creating new list: " + e.message
  exit 1
end
