#!/usr/pkg/bin/perl -w
#
#  pgpenvelope_decrypt
#    - call to pgpenvelope for deciphering
#
#  Copyright (C) 2000 Frank J. Tobin <ftobin@uiuc.edu>
#
#  This file is part of pgpenvelope
#
#  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; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, visit the following URL:
#  http://www.gnu.org
#
#  $Id: pgpenvelope_decrypt,v 1.27 2001/05/12 18:56:36 ftobin Exp $
#

use 5.005;

use strict;
use English;
use Symbol;
use IO::Seekable;
use FindBin;
use sigtrap 'handler' => 'die_signal_handler', 'normal-signals';

use GnuPG::Interface 0.30;
use String::Approx qw( aindex );

use PGPEnvelope::Common;
use PGPEnvelope::Config;
use PGPEnvelope::Terminal;

my $config = new PGPEnvelope::Config;
$config->getopt( \@ARGV );

my $homedir = $config->get( 'homedir' ) or die "no homedir known";
PGPEnvelope::Common->check_and_create_dir( $homedir );

my $is_filter = $config->get( 'filter' ) ? 1 : 0;

$config->parse_file
  ( File::Spec->catfile( $homedir, PGPEnvelope::Common->prefs_filename() ) );

##########################################################

my $terminal;
if ( $is_filter )
{
    select STDERR;
}
else
{
    $terminal = setup_terminal();
    select $terminal->output_handle();
}

$OUTPUT_AUTOFLUSH = 1;
STDOUT->autoflush( 1 );

##########################################################

my $program         = $config->get( 'decryption-program' );
my $program_args    = $config->get( 'decryption-program-call' );
my $confirm_decrypt = $config->get( 'confirm-decryption' );

my ( $call, @user_args )= split( /\s+/, $program_args );

unless ( lc $program eq 'gpg' or lc $program eq 'gnupg' )
{
    PGPEnvelope::Common->die_gracefully( "$FindBin::Script: don't know how to interact with decryption_program $program\n" );
}


if ( $confirm_decrypt and not $is_filter )
{
    unless ( confirm_decryption() )
    {
	my $prefix = $config->get( 'message-prefix-nullifier' );
	while ( <STDIN> )
	{
	    $_ = "$prefix$_" if /^-----BEGIN PGP /;
	    print STDOUT $_;
	}
	exit 0;
    }
}

##########################################################

my $gnupg = GnuPG::Interface->new();
$gnupg->options->meta_interactive( not $is_filter );
$gnupg->options->push_extra_args( @user_args ) if @user_args;
$gnupg->call( $call );

my $do_forgery_check  = $config->get( 'do-forgery-check' );
my $forgery_warning   = $config->get( 'forgery-warning' );
my $forgery_threshold = $config->get( 'forgery-threshold' );
my $quiet             = $config->get( 'quiet' );

my @border_names = qw( top-border info-top-border info-bottom-border );
my @borders = map { $config->get( $_ ) } @border_names;

LINE: while ( <STDIN> )
{
    
    if ( $do_forgery_check and detect_forgery( $_ ) )
    {
	$_ = $forgery_warning . $_;
    }
    
    unless ( /^-----BEGIN PGP / )
    {
	print STDOUT $_;
	next LINE;
    }
    
    my ( $isa_keyblock, $is_signed ) = ( 0, 0 );
    
    if ( /^-----BEGIN PGP SIGNED MESSAGE-----\s*$/ )
    {
	$is_signed = 1;
    }
    elsif ( /^-----BEGIN PGP PUBLIC KEY BLOCK-----\s*$/ )
    {
	$isa_keyblock = 1;
    }
    
    # we don't want to deal with encrypted messages while being a filter
    if ( $is_filter and not ( $isa_keyblock or $is_signed ) )
    {
	print STDOUT $_;
	next LINE;
    }
    
    my $temp_in = PGPEnvelope::Common->open_tempfile();
    $temp_in->print( $_ );
    
    
    {
	my $start_message = $isa_keyblock
	  ? "$FindBin::Script: adding key to keyring with $program ($call)...\n"
	  : "$FindBin::Script: performing decryption/verification with $program ($call)...\n";
	
	print $start_message unless $quiet;
    }
    
    
    my @header_info = ();
    my $in_header = 1;
    
 PGP_LINE: while ( <STDIN> )
    {
	$temp_in->print( $_ );
	last PGP_LINE if /^-----END PGP /;
	
	# these lines grab pgp-header info
	$in_header = 0 if not /\w/;
	push @header_info, $_ if $in_header;
	
	# this re-turns on header-capturing for
	# the signature-block
	$in_header = 1 if /^-----BEGIN PGP /;
    }
    
    push @header_info, "\n$FindBin::Script: importing a keyblock\n"
      if $isa_keyblock;
    
    $temp_in->seek( 0, SEEK_SET ) or die;
    
    my $temp_out = PGPEnvelope::Common->open_tempfile();
    my $logger = gensym;
    
    my $handles = GnuPG::Handles->new( stdin  => $temp_in,
				       stdout => $temp_out,
				       logger => $logger,
				     );
    
    $handles->options( 'stdin'  )->{direct} = 1;
    $handles->options( 'stdout' )->{direct} = 1;
    
    my $call = $isa_keyblock ? 'import_keys' : 'decrypt';
    
    $gnupg->$call( handles => $handles );
    wait;
    
    my $out_size = -s $temp_out;

    if ( $out_size == 0 and not $isa_keyblock )
    {
	# this section happens if there was an error
	
	my $message =  "$FindBin::Script: $program produced no output (possibly a failed decryption, or invalid data)\nskipping a block\n";
	
	print $message unless $quiet;;
	
	if ( not $is_filter )
	{ 
	    print STDOUT
	      $config->get( 'top-border' ), "\n\n",
	      <$logger>, "\n",
	      $message, , "\n",
	      $config->get( 'info-bottom-border' ), "\n\n";
	}
	
	$temp_in->seek( 0, SEEK_SET ) or die;
	print STDOUT $temp_in->getlines();
    }
    else
    {
	# this section happens if there was no error
	
	if ( not $isa_keyblock )
	{
	    print STDOUT $config->get( 'top-border' ), "\n\n";
	    
	    # the whitespace-eliminating is for various
	    # oddities with multiple newlines in messages
	    $temp_out->seek( 0, SEEK_SET ) or die;
	    while ( <$temp_out> )
	    {
		chomp;
		s/\s+$//;
		print STDOUT $_, "\n";
	    }
	    
	    PGPEnvelope::Common->clear_fh( $temp_out );
	    $temp_out->close();
	}
	
	print STDOUT
	  "\n",
	  $config->get( 'info-top-border' ), "\n",
	  @header_info, "\n",
	  <$logger>;
	
	if ( $is_filter and $isa_keyblock )
	{
	    explain_no_gnupg_output();
	}
	
	print STDOUT
	  "\n",
	  "$FindBin::Script: message processed at ", scalar localtime, "\n\n",
	  $config->get( 'info-bottom-border' ), "\n";
    }
}

exit 0;


######################################################################


sub setup_terminal
{
    $terminal = PGPEnvelope::Terminal->new();
    $terminal->setup();
    return $terminal;
}



sub die_signal_handler
{
    my ( $signal_name ) = @_;
    print "$FindBin::Script: Signal $signal_name caught.  Cleaning up.\n";
    print STDOUT "-----UNPROCESSED MESSAGE BELOW-----\n";
    print STDOUT <STDIN>;
    $terminal->cleanup() if $terminal;
    print "Exiting.\n";
    $is_filter ? exit 1 : exit 0;
}


sub explain_no_gnupg_output
{
    print STDOUT <<EOF;
$FindBin::Script: Note to the user:
If GnuPG produced no output here, it is unfortunately because
GnuPG's --no-tty option  (necessary while pgpenvelope_decrypt
is a filter) as a side-effect invokes the same behaviour as 
the option --quiet does.
EOF
}



sub confirm_decryption
{
    my @positive_responses = ( 'y', "\n" );
    my @negative_responses = ( 'n' );
    my @all_responses = ( @positive_responses, @negative_responses );
    my $key;
      
    while ( 1 )
    {
	print
	  "$FindBin::Script: confirmation requested\n",
	  "Do do you wish to run pgpenvelope over this message? [Y/n] ";
	$key = $terminal->readkey();
	print "\n\n";
	
	last if grep( /^$key$/i, @all_responses );
	
	print "Invalid choice: '$key'.\n\n";
    }
    
    return scalar ( grep( /^$key$/i, @positive_responses ) );
}



sub detect_forgery
{
    my ( $line ) = @_;
    
    # Perform an approximate match against the border
    # messages to see if the mail is trying to impersonate
    # our headers.
    # Many thanks to Billy Donahue <billy@dadadada.net> for
    # doing a lot of the work here and in other files related
    # to this.
    # Note: the line must at least match our border at
    # the first character; no cause for alarm if this isn't true.
    
    # We'll strip whitespace of the end of the line
    # so this doesn't fool us by causing us to over-trip
    # the threshold
    $line =~ s/\s+$//;
    
    foreach my $border ( @borders )
    {
	my ( $index ) = aindex( $border,
				[ $forgery_threshold ],
				$line,
			      );
	return 1 if $index == 0;
    }
    
    return 0;
}
