#!/usr/bin/perl -w
#
#  anonvelope
#      - main program for anonvelope
#
#  Copyright (C) 2000 Frank J. Tobin <ftobin@uiuc.edu>
#
#  This file is part of anonvelope
#
#  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/copyleft/gpl.html
#
#  $Id: anonvelope,v 1.36 2001/07/28 02:56:28 ftobin Exp $
#

use strict;
use English;
use File::Spec;
use IO::File;
use IO::Seekable;
use File::Copy;
use Class::Struct;
use sigtrap 'handler' => 'die_signal_handler', 'normal-signals';

use LWP::UserAgent;
use HTTP::Request;
use AppConfig qw( :argcount :expand );
use GnuPG::Interface 0.10;

use Anonvelope::Terminal;

use vars qw( $VERSION );

$VERSION = '0.1.0';

my $program = 'anonvelope';


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

my $config = AppConfig->new();
define_config( $config );
$config->getopt( \@ARGV );

set_homedir_dependents();
my $options_file = $config->get( 'options' );
$config->file( $options_file ) if -e $options_file;

my $debug = $config->get( 'debug' );
my @recipients = $config->get( 'anon-to' )
  ? @{ $config->get( 'anon-to' ) } : ();
my $use_latency = $config->get( 'use-latency' );
my $stats_url = $config->get( 'stats-url' );
my $real_subject;

if (  $config->get( 'stats' ) )
{
    show_remailer_stats();
    exit 0;
}

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

my $gnupg = GnuPG::Interface->new();
$gnupg->options->hash_init
  ( comment               => '',
    meta_pgp_5_compatible => $config->get( 'pgp-5-compatible' ),
    armor                 => 1,
    extra_args            => [ '--no-version' ],
  );

my $encryption_program = 'GnuPG';
my $encryption_program_call = $config->get( 'encryption-program-call' ) || $gnupg->gnupg_call();
$gnupg->gnupg_call( $encryption_program_call );

my $term = Anonvelope::Terminal->new();
$term->setup();
my $output_handle = $term->output_handle();

select $output_handle;
$term->output_handle->format_name( 'MENU' );

$OUTPUT_AUTOFLUSH = 1;

die_gracefully( "cannot have multiple recipients for anonymous remailing\n" )
  if @ARGV > 1;

usage() unless @ARGV;

my $initial_address = shift @ARGV;

struct( Remailer => [ address => "\$", keyid => "\$" ] );

my @remailer_path;
my $initial_remailer;
my %remailers = get_remailers( $config->get( 'remailers' ) );

{
    my $initial_is_known = 0;
    foreach my $r ( keys %remailers )
    {
	next unless $remailers{$r}->address() eq $initial_address;
	@remailer_path = ( $r );
	$initial_remailer = $r;
	last;
    }
    unless ( $initial_remailer )
    {
	die_gracefully( "error: no remailer configuration exists for the recipient $initial_address.\n" );
    }
}


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

my $menu_message = '';

while ( 1 )
{
    $term->clear_screen();
    write;
    
    $menu_message = '';
    
    my $k = $term->readkey();
    
    if ( $k eq 't' )
    {
	set_recipients();
    }
    elsif( $k eq 'r' )
    {
	set_remailer_path();
    }
    elsif ( $k eq 's' )
    {
	set_real_subject();
    }
    elsif ( $k eq 'l' )
    {
	$use_latency = not $use_latency;
	$menu_message = $use_latency
	  ? 'Selected to use latency.' : 'Selected to NOT use latency.';
    }
    elsif ( $k eq 'i' )
    {
	show_remailer_stats();
	$term->wait_for_key();
    }
    elsif ( $k eq 'h' )
    {
	display_menu_help();
	$term->wait_for_key();
    }
    elsif ( $k eq 'w' )
    {
	display_warranty();
	$term->wait_for_key();
    }
    elsif ( $k eq 'c' )
    {
	die_gracefully( "$program: cancelling\n" );
    }
    elsif ( $k eq "\n" )
    {
	if ( commit() )
	{
	    $term->wait_for_key();
	    exit 0;
	}
    }
    else
    {
	$menu_message = "unknown command '$k'";
    }
}


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


sub set_recipients
{
    my $line = clean_line( $term->readline( 'Please enter a comma-delimited list of recipients: ' ) );
    @recipients = split /\s*,\s*/, $line;
    $menu_message = 'Set recipients to: ' . join( ', ', @recipients );
}



sub set_remailer_path
{
    @remailer_path = ( $initial_remailer );
    
    my %inputs = ( end    => '?done',
		   stats  => '?stats',
		   config => '?config',
		   sofar  => '?so far',
		   help   => '?help',
		 );
    
    show_remailer_input_help( %inputs );
    
    print "Initial remailer is $initial_remailer.\n";
    
    while ( 1 )
    {
	my $line = clean_line( $term->readline('remailer: ' ) );
	last if $line eq $inputs{end};

	next unless $line =~ /\w/;
	
	if ( $line eq $inputs{stats} )
	{
	    show_remailer_stats();
	}
	elsif ( $line eq $inputs{config} )
	{
	    show_remailer_config();
	}
	elsif ( $line eq $inputs{sofar} )
	{
	    foreach ( @remailer_path ) { print $_, "\n"; }
	}
	elsif ( $line eq $inputs{help} )
	{
	    show_remailer_input_help( %inputs );
	}
	elsif ( not $remailers{$line} )
	{
	    print "'$line' is not a known remailer\n";
	}
	else
	{
	    push @remailer_path, $line;
	}
    }
    
    $menu_message = 'Set remailer path to: ' . join ( ', ', @remailer_path );
}



sub set_real_subject
{
    $real_subject = $term->readline( 'Please enter the real message subject: ' );
    $menu_message = 'Set subject to: ' . $real_subject;
}



sub show_remailer_stats
{
    print "Retrieving $stats_url for remailer stats...\n\n";
    my $ua = LWP::UserAgent->new();
    
    my $request = HTTP::Request->new( 'GET', $stats_url );
    my $response = $ua->request($request);
    
    if ( $response->is_success() )
    {
	my $start_writing = 0;
	foreach ( split /\n/, $response->content() )
	{
	    $start_writing = 1 if /^Last update/;
	    print $_, "\n" if $start_writing;
	}
    }
    else
    {
	print
	  'Got error code ', $response->code(), ': ', 
	  $response->message(), "\n";
    }
    print "\n";
}



sub show_remailer_config
{
    foreach my $r ( sort keys %remailers )
    {
	my $address = $remailers{$r}->address();
	my $keyid   = $remailers{$r}->keyid();
	print <<EOF;
$r
     address        : $address
     OpenPGP key id : $keyid
EOF
    }
}


sub show_remailer_input_help
{
    my ( %inputs ) = @_;
    print <<EOF;
Enter nicknames of remailers to use after $initial_remailer, one per line.
Entering any of the following will cause the described behaviour:
    $inputs{end}    end the list
    $inputs{sofar}  show what remailer have been chosen so far
    $inputs{stats}  retrieve remailer stats
    $inputs{config} show known remailer-related information
    $inputs{help}   show this help

EOF
}


sub display_menu_help
{
    print <<EOF;
t        set the email address for the final recipient of the message.
r        set the remailers to use.
s        set the subject the final recipient should see.
i        show current remailer stats.
h        show this help.
w        show the warranty.
c        cancel sending this message.
enter    prepare the message using the current settings.
         this is the final action to take.

EOF
}



sub display_warranty
{
    print <<EOF;
$program $VERSION, a anonymous-remailer message preparer
Copyright (C) 2000 Frank J. Tobin <ftobin\@uiuc.edu>

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/copyleft/gpl.html

EOF
}



sub commit
{
    print "Committing...\n";
    unless ( @recipients )
    {
	print "You haven't determined message recipients yet!\n";
	$term->wait_for_key();
	$menu_message = '****commit cancelled due to insufficient recipients';
	return 0;
    }
    
    my $tmp_in = IO::File->new_tmpfile()
      or die "error creating tmpfile: $ERRNO";
    my $tmp_out = IO::File->new_tmpfile()
      or die "error creating tmpfile: $ERRNO";
    
    copy( \*STDIN, $tmp_out );
    
    my $min_latency    = $config->get( 'latency-min' );
    my $max_latency    = $config->get( 'latency-max' );
    my $latency_range  = $max_latency - $min_latency;
    
    my $use_garbage    = $config->get( 'use-garbage' );
    my $max_garbage    = $config->get( 'garbage-max' );
    my $min_garbage    = $config->get( 'garbage-min' );
    my $garbage_range  = $max_garbage - $min_garbage;
    
    my $cutmarks = "=====$program cutmarks=====";

    my $pretend_encrypt = $config->get( 'pretend-encrypt' );
    
    # note we need to work from the end of the remailer path
    # backwards
    my $layer = 0;
    my $max_layers = scalar @remailer_path;
    my $prev_wrapped_hop = undef;
    
    # $hop is what we are currently preparing the message for
    # That is, we are encrypting to that remailer
    while ( my $hop = pop @remailer_path )
    {
	$layer++;
	print "preparing messge for $hop (layer $layer of $max_layers)...\n";
	my $r = $remailers{$hop};
	
	my $subject;
	my $anon_to;
	
	if ( $layer == 1 )
	{
	    $subject = $real_subject;
	    $anon_to = join( ', ', @recipients );
	}
	else
	{
	    $anon_to = $remailers{$prev_wrapped_hop}->address()
		or die "cannot determine address for remailer $prev_wrapped_hop";
	}
	
	my $key_id = $remailers{$hop}->keyid()
	  or die "cannot determine which key to use for remailer $hop";
	my $latency = int( rand $latency_range ) + $min_latency;
	
	$tmp_in->seek(  0, SEEK_SET );
	$tmp_out->seek( 0, SEEK_SET );
	$tmp_in->truncate( 0 );
	
	$tmp_in->print( "::\n",
			"Anon-To: $anon_to\n",
		      );
	
	$tmp_in->print( "Test-To: ", join( ', ', @recipients ), "\n" )
	  if $debug;
	$tmp_in->print( "Latent-Time: +$latency\n" ) if $use_latency;
	$tmp_in->print( "Cutmarks: $cutmarks\n" )    if $use_garbage;
	
	# this ends remailer directives.
	$tmp_in->print( "\n" );
	
	# this is for any message headers to use
	$tmp_in->print("##\n",
		       "Subject: $subject\n",
		       "\n" )
	    if defined $subject;
	
	$tmp_in->flush();
	copy( $tmp_out, $tmp_in );
	
	if ( $use_garbage )
	{
	    my $garbage_amount = int( rand $garbage_range ) + $min_garbage;
	    
	    $tmp_in->print( $cutmarks, "\n" );
	    
	    my $rand_string = '';
	    foreach (1..(.25*$garbage_amount))  # roughly how much needed
	    {
		$rand_string .= pack('b*', rand());
	    }
	    
	    my $garbage = encode_base64($rand_string);
	    
	    # chop off what we don't want
	    $garbage = substr($garbage, 0, $garbage_amount);
	    
	    $tmp_in->print($garbage, "\n");
	}
	
	$tmp_in->seek(  0, SEEK_SET );
	$tmp_out->seek( 0, SEEK_SET );
	$tmp_out->truncate( 0 );
	
	if ( not $pretend_encrypt )
	{
	    my $handles = GnuPG::Handles->new( stdin  => $tmp_in,
					       stdout => $tmp_out,
					     );
	    $handles->options( 'stdin',  { direct => 1 } );
	    $handles->options( 'stdout', { direct => 1 } );
	
	    $gnupg->options->clear_recipients();
	    $gnupg->options->recipients( $key_id );
	
	    my $pid = $gnupg->encrypt( handles => $handles );
	    
	    waitpid $pid, 0;
	    
	    die_gracefully( "error calling $encryption_program ($encryption_program_call)\n" )
	      unless $CHILD_ERROR == 0;
	}
	else
	{
	    my $recip = "$hop <" . $remailers{$hop}->address() . "> ($key_id)";
	    $tmp_out->print( "-----BEGIN PGP MESSAGE-----\n",
			     "Recipient: $recip\n\n",
			     $tmp_in->getlines(),
			     "-----END PGP MESSAGE-----\n",
			   );
	}
	
	# this sequence let's us write the proper headers for
	# the encrypted message
	$tmp_in->seek(  0, SEEK_SET );
	$tmp_out->seek( 0, SEEK_SET );
	
	$tmp_in->print( "::\n",
			"Encrypted: PGP\n\n",
		      );
	
	$tmp_in->flush();
	copy( $tmp_out, $tmp_in );
	
	# we need to copy back tmp_in to tmp_out, as tmp_out
	# should contain our next input.
	$tmp_in->seek(  0, SEEK_SET );
	$tmp_out->seek( 0, SEEK_SET );
	
	$tmp_out->flush();
	copy( $tmp_in, $tmp_out );
	
	$prev_wrapped_hop = $hop;
    }
    
    $tmp_out->seek( 0, SEEK_SET );
    copy( $tmp_out, \*STDOUT );
    
    print
      "\n",
      "Message successfully prepared for remailing.\n";
    
    return 1;
}


# from MIME::Base64
sub encode_base64($;$)
{
    my $res = "";
    my $eol = $_[1];
    $eol = "\n" unless defined $eol;
    pos($_[0]) = 0;                          # ensure start at the beginning

    $res = join '', map( pack('u',$_)=~ /^.(\S*)/, ($_[0]=~/(.{1,45})/gs));

    $res =~ tr|\` -_|AA-Za-z0-9+/|;               # help emacs
    # fix padding at the end
    my $padding = (3 - length($_[0]) % 3) % 3;
    $res =~ s/.{$padding}$/'=' x $padding/e if $padding;
    # break encoded string into lines of no more than 76 characters each
    if (length $eol) {
        $res =~ s/(.{1,76})/$1$eol/g;
    }
    return $res;
}

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

sub get_default_home
{
    my $home = ( $ENV{ ( uc $program ) . '_HOME' }
                 || File::Spec->catdir( $ENV{HOME}, ".$program" )
               );
    
    die 'cannot determine a home directory to use' unless $home;
    return $home;
}



sub set_homedir_dependents
{
    my $homedir = get_default_home();
    
    my %file_dependents = ( options   => 'options',
			    remailers => 'remailers',
			  );
    
    foreach my $key ( keys %file_dependents )
    {
        my $location = File::Spec->catfile( $homedir,
					    $file_dependents{$key},
					  );
        $config->set( $key, $location )
          unless defined $config->get( $key );
    }
}



sub define_config
{
    my ( $config ) = @_;
    
    $config->define
	( homedir          => { ARGCOUNT => ARGCOUNT_ONE,
				DEFAULT  => get_default_home(),
			      },
	 options          => { ARGCOUNT => ARGCOUNT_ONE },
	 
	 'pgp-5-compatible' => { ARGCOUNT => ARGCOUNT_NONE,
				 DEFAULT  => 0,
			       },
	 
	 remailers        => { ARGCOUNT => ARGCOUNT_ONE },
	 
	 'encryption-program-call' => { ARGCOUNT => ARGCOUNT_ONE },
	 
	 'stats-url'   => { ARGCOUNT => ARGCOUNT_ONE,
			    DEFAULT  => 'http://anon.efga.org/Remailers/rlist',
			  },
	 stats       => { ARGCOUNT => ARGCOUNT_NONE },
	 
	 'use-latency' => { ARGCOUNT => ARGCOUNT_NONE },

	 'latency-min' => { ARGCOUNT => ARGCOUNT_ONE,
			    DEFAULT  => 10,
			  },
	 'latency-max' => { ARGCOUNT => ARGCOUNT_ONE,
			   DEFAULT  => 60,
			  },
	 
	 'use-garbage' => { ARGCOUNT => ARGCOUNT_NONE,
			    DEFAULT  => 1,
			  },
	 'garbage-min' => { ARGCOUNT => ARGCOUNT_ONE,
			    DEFAULT  => 256,
			  },
	 'garbage-max' => { ARGCOUNT => ARGCOUNT_ONE,
			    DEFAULT  => 4096,
			  },
	 
	 'anon-to'     => { ARGCOUNT => ARGCOUNT_LIST },
	 
	 debug         => { ARGCOUNT => ARGCOUNT_NONE },
       
	 'pretend-encrypt' => { ARGCOUNT => ARGCOUNT_NONE },
	 
	 );
}


sub get_remailers
{
    my ( $file ) = @_;
    my $f = IO::File->new( $file )
      or die_gracefully
	( "error opening remailers $file: $ERRNO\n",
	  "Please setup your remailer configuration in this file\n",
	  "as described in the $program documentation.\n",
	);
    
    my %remailers;
    
    while ( <$f> )
    {
	s/\#.*$//;  # strip comments
	next unless /\S/;
	my ( $nick, $address, $key ) = split;
	unless ( $address and $key )
	{
	    print "warning: incomplete remailer description: $file at line $INPUT_LINE_NUMBER";
	    next;
	}
	
	$remailers{$nick} ||= Remailer->new();
	$remailers{$nick}->address( $address );
	$remailers{$nick}->keyid( $key );
    }
    
    return %remailers;
}


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


format MENU =
//////////////////////// @||||||||||||||||||||| \\\\\\\\\\\\\\\\\\\\\\\
                          "$program $VERSION"

Recipients    : @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
               @recipients ? join ( ', ', @recipients ) : '[unset]'
Real Subject  : @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
               $real_subject || '[unset]'
Remailer Path : @<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
               join( ', ', @remailer_path )

    (t) set recipients          (i) get remailer stats
    (r) set remailer path       (h) help
    (s) set real subject        (w) warranty
[@] (l) use latency             (c) cancel
$use_latency ? 'X' : ''
                            <enter> commit

@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$menu_message
Please select an option from above:

.


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


sub usage
{
    print "usage: $PROGRAM_NAME [options] initial-recipient\n";
    exit 1;
}


sub clean_line
{
    my ( $line ) = @_;
    $line =~ s/^\s+//;
    $line =~ s/\s+$//;
    return $line;
}


sub die_signal_handler
{
    my ( $signal_name ) = @_;
    print "$program: Signal $signal_name caught. Cleaning up.\n";
    $term->cleanup() if $term;
    die_gracefully();
}



sub die_gracefully
{
    print @_;
    exit 1;
}



sub state
{
    print @_ if $debug
}


__END__

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


=head1 NAME

anonvelope - format messages for anonymous remailers

=head1 SYNOPSIS

B<anonvelope> I<initial-address>

B<anonvelope> --stats

=head1 DESCRIPTION

anonvelope is designed to be a mail body filter that
prepares a message for sending to anonymous Type-I (cpunk-style)
remailers.  anonvelope may be used by different terminal-based
MUA's as a filtering mechanism, but it is specifically designed
to be workable with Pine.

Encrypted messages are created using GnuPG, which is available
at http://www.gnupg.org/.

=head1 USAGE

=head2 Stats

If anonvelope is called with just plain B<--stats> then
it simply fetch remailer stats.  This is useful in determining
what initial remailer to send a message to, as explained
in L<"Pine">.

=head2 Pine

To setup anonvelope with Pine, one needs to make changes in the Pine
configuration menu.  To access this, from the main menu, select
Setup, then Config.  The setting that needs to be changed
is sending-filters.

    sending-filters = (path_to)/anonvelope _RECIPIENTS_

Substitute (path_to) with the absolute path to the B<anonvelope>
executable.  For example, this could end up being:

    /usr/local/bin/anonvelope _RECIPIENTS_

When composing a message to be mailed anonymously,
put as your recipient the address of the initial remailer
along the route.  Unfortunately, anonvelope cannot set
this for you.  Do B<not> put the address of your intended
final recipient of the anonymous message.

It is also recommended that one not set any subject when
composing a message to be mailed anymously.  One can
set this subject from within the anonvelope menu.
By settin it inside anonvelope, you can be assured that only
the last remailer in the chain and the recipient are aware
of a message with that subject.

=head1 Remailers configuration file

One must setup F<~/.anonvelope/remailers> to hold remailer
configurations.  The layout of this file is having each line
describe a remailer.  Information after hash characters (#)
are considered comments.

  nickname      email      keyID

For example:

  shinn   remailer@mixmaster.shinn.net  0x7F0BE4FF
  xganon  remailer@anon.xg.nu           0xB0F08BEF

Note that these examples may be out of date.

=head1 OPTIONS

The following options may be set in the F<options> file,
or as arguments to B<anonvelope>.

For options placed placed in the options file, do not
write the 2 dashes, but simply supply the name of the
option, and any required arguments.  Lines with a hash as
the first non-whitespace character are ignored.

Options which do not require arguments may be negated by
preceding them with "no"; for example, to negate the
option foobar, one could place "nofoobar" in the options
file.  These options can also set to a true or false value
by setting them to 1 (true) or 0 (false) in the options
file.  However, note that this does not work for them as
command-line arguments; the "no" prefix just be used then.

=over 4

=item B<--use-latency>

If true, a random latency will be used at each remailer hop,
subject to the B<latency-min> and B<latency-max> options.
This setting may be set on a per-message basis via the menu.
Default is false.

=item B<--latency-min> I<minutes>

Minimum latency to use if latency is selected.
Default is 10 (minutes).

=item B<--latency-max> I<minutes>

Maximum latency to use if latency is selected.
Default is 60 (minutes).

=item B<--use-garbage>

Generate random garbage at the bottom of mail messages
so that there can be little correlation in size between
the size of the remailer's incoming, encrypted text,
and the resulting message the remailer passes on.
This random garbage is stripped off at the remailer.
The randomness of the garbage is weak.
Sizes of garbage to use on each hop are
are randomly determined between
B<garbage-min> and B<garbage-max> options.
Default is true.

=item B<--garbage-min> I<bytes>

Minimum random garbage to use if using garbage is selected.
Default is 256 (bytes).

=item B<--garbage-max> I<bytes>

Maximum random garbage to use if using garbage is selected.
Default is 4096 (bytes).

=item B<--anon-to> I<email>

The address which the final remailer in a chain should send to.
This option may be set on a per-message via the menu.
This option may be repeated.
There is no default.

=item B<--pgp-5-compatible>

If set, use options to have GnuPG generate messages compatible
with PGP 5.  Might be needed to be successful using some
remailers.  Default is false.

=item B<--stats-url> I<url>

Use B<url> when fetching remailer statistics.
Default is "http://anon.efga.org/Remailers/rlist".

=item B<--encryption-program-call> I<program>

This is the call to the encryption program to use.
Currently, only GnuPG is support, and hence
the default is "gpg".

=item B<--homedir> I<directory>

Location of home directory.
It does not make sense to set this inside the options file.
Default is F<~/.anonvelope/>.

=item B<--options> I<file>

Location of the options file.
It does not make sense to to set this inside the options file.
Default is F<~/.anonvelope/options>.

=item B<--remailers> I<file>

Location of the remailers file.
Default is F<~/.anonvelope/remailers>.

=item B<--pretend-encryp>

Do not really encrypt messages, but pretend that
we did.  Meant for debugging.

=back

=head1 FILES

=over 4

=item F<~/.anonvelope/options>

If exists, may contain options.

=item F<~/.anonvelope/remailers>

Must exist.  Contains configuration information about
remailers.

=head1 SECURITY NOTES

anonvelope uses temporary files to achieve
being able repeatedly proess the same message, without
running into buffering problems that occur when just putting
the message into memory and opening up pipes to GnuPG.
The temporary files are created from the call
IO::File->new_tmpfile() (see L<IO::File/new_tmpfile> for details).

=head1 HOMEPAGE

http://anonvelope.sourceforge.net/

=head1 AUTHOR

Frank J. Tobin <ftobin@uiuc.edu>

=over 4

=item OpenPGP fingerprint:

4F86 3BBB A816 6F0A 340F  6003 56FF D10A 260C 4FA3

=back

=head1 COPYRIGHT

Copyright (C) 2000 Frank J. Tobin <ftobin@uiuc.edu>

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/copyleft/gpl.html

=cut
