caff: brace-style fixes
[pgp-tools.git] / caff / caff
index 052ef8b84314748974bab8fdc3006936f0657672..f32d5b5da615e71526f8c3cc2e32045f969490db 100755 (executable)
--- a/caff/caff
+++ b/caff/caff
@@ -3,8 +3,8 @@
 # caff  --  CA - Fire and Forget
 # $Id$
 #
-# Copyright (c) 2004, 2005 Peter Palfrader <peter@palfrader.org>
-# Copyright (c) 2005 Christoph Berg <cb@df7cb.de>
+# Copyright (c) 2004, 2005, 2006 Peter Palfrader <peter@palfrader.org>
+# Copyright (c) 2005, 2006 Christoph Berg <cb@df7cb.de>
 #
 # All rights reserved.
 #
@@ -40,7 +40,7 @@ caff -- CA - Fire and Forget
 
 =over
 
-=item B<caff> [-eEmMRS] [-u I<yourkeyid>] I<keyid> [I<keyid> ..]
+=item B<caff> [-eERS] [-m I<yes|ask-yes|ask-no|no>] [-u I<yourkeyid>] I<keyid> [I<keyid> ..]
 
 =back
 
@@ -50,7 +50,8 @@ CA Fire and Forget is a script that helps you in keysigning.  It takes a list
 of keyids on the command line, fetches them from a keyserver and calls GnuPG so
 that you can sign it.  It then mails each key to all its email addresses - only
 including the one UID that we send to in each mail, pruned from all but self
-sigs and sigs done by you.
+sigs and sigs done by you.  The mailed key is encrypted with itself as a means
+to verify that key belongs to the recipient.
 
 =head1 OPTIONS
 
@@ -65,13 +66,10 @@ Export old signatures. Default is to ask the user for each old signature.
 Do not export old signatures. Default is to ask the user for each old
 signature.
 
-=item B<-m>, B<--mail>
+=item B<-m>, B<--mail> I<yes|ask-yes|ask-no|no>
 
-Send mail after signing. Default is to ask the user for each uid.
-
-=item B<-M>, B<--no-mail>
-
-Do not send mail after signing. Default is to ask the user for each uid.
+Whether to send mail after signing. Default is to ask, for each uid,
+with a default value of yes.
 
 =item B<-R>, B<--no-download>
 
@@ -84,6 +82,9 @@ Do not sign the keys.
 =item B<-u> I<yourkeyid>, B<--local-user> I<yourkeyid>
 
 Select the key that is used for signing, in case you have more than one key.
+To sign with multiple keys at once, separate multiple keyids by comma. This
+option requires the key(s) to be defined through the keyid variable in the
+configuration file.
 
 =item B<--key-file> I<file>
 
@@ -103,7 +104,7 @@ Import keys from file. Can be supplied more than once.
 
 =item $HOME/.caff/gnupghome/gpg.conf  -  gpg configuration
 
-useful options include use-agent, default-cert-level, etc.
+useful options include use-agent, keyserver-options, default-cert-level, etc.
 
 =back
 
@@ -114,9 +115,9 @@ The file is generated when it does not exist.
 
 Example:
 
-       $CONFIG{owner} = q{Peter Palfrader};
-       $CONFIG{email} = q{peter@palfrader.org};
-       $CONFIG{keyid} = [ qw{DE7AAF6E94C09C7F 62AF4031C82E0039} ];
+       $CONFIG{'owner'} = q{Peter Palfrader};
+       $CONFIG{'email'} = q{peter@palfrader.org};
+       $CONFIG{'keyid'} = [ qw{DE7AAF6E94C09C7F 62AF4031C82E0039} ];
 
 =head2 Required basic settings
 
@@ -136,14 +137,22 @@ A list of your keys.  This is used to determine which signatures to keep
 in the pruning step.  If you select a key using B<-u> it has to be in
 this list.  B<REQUIRED>.
 
+=back
+
 =head2 General settings
 
+=over
+
 =item B<caffhome> [string]
 
 Base directory for the files caff stores.  Default: B<$HOME/.caff/>.
 
+=back
+
 =head2 GnuPG settings
 
+=over
+
 =item B<gpg> [string]
 
 Path to the GnuPG binary.  Default: B<gpg>.
@@ -163,19 +172,24 @@ is set to.
 
 Path to your secret keyring.  Default: B<$HOME/.gnupg/secring.gpg>.
 
-=item B<also-encrypt-to> [keyid]
+=item B<also-encrypt-to> [keyid, or list of keyids]
 
-An additional keyid to encrypt messages to. Default: none.
+Additional keyids to encrypt messages to. Default: none.
 
 =item B<gpg-sign-args> [string]
 
-Additional arguments to pass to gpg.  Default: none.
+Additional commands to pass to gpg after the "sign" command.
+Default: none.
+
+=back
 
 =head2 Keyserver settings
 
+=over
+
 =item B<keyserver> [string]
 
-Keyserver to download keys from.  Default: B<subkeys.pgp.net>.
+Keyserver to download keys from.  Default: B<pool.sks-keyservers.net>.
 
 =item B<no-download> [boolean]
 
@@ -186,8 +200,12 @@ Default: B<0>.
 
 A list of files containing keys to be imported.
 
+=back
+
 =head2 Signing settings
 
+=over
+
 =item B<no-sign> [boolean]
 
 If true, then skip the signing step. Default: B<0>.
@@ -202,16 +220,24 @@ This is useful for offline signing. Default: B<0>.
 Don't export UIDs by default, on which your latest signature is older
 than this age.  Default: B<24*60*60> (i.e. one day).
 
+=item B<local-user> [keyid, or list of keyids]
+
+Select the key that is used for signing, in case you have more than one key.
+With multiple keyids, sign with each key in turn.
+
+=back
+
 =head2 Mail settings
 
-=item B<mail> [boolean]
+=over
 
-Do not prompt for sending mail, just do it. Default: B<0>.
+=item B<mail> [boolean]
 
-=item B<no-mail> [boolean]
+Whether to send mails. This is a quad-option, with which you can set the
+behaviour: yes always sends, no never sends; ask-yes and ask-no asks, for
+each uid, with according defaults for the question. Default: B<ask-yes>.
 
-Do not prompt for sending mail. The messages are still written to
-$CONFIG{caffhome}/keys/. Default: B<0>.
+In any case, the messages are also written to $CONFIG{'caffhome'}/keys/
 
 =item B<mail-template> [string]
 
@@ -244,6 +270,23 @@ Add a Reply-To: header to messages sent. Default: none.
 Address to send blind carbon copies to when sending mail.
 Default: none.
 
+=item B<mailer-send> [array]
+
+Parameters to pass to Mail::Mailer.
+This could for example be
+
+       $CONFIG{'mailer-send'} =  [ 'smtp', Server => 'mail.server', Auth => ['user', 'pass'] ];
+
+to use the perl SMTP client or
+
+       $CONFIG{'mailer-send'} =  [ 'sendmail', '-o8' ];
+
+to pass arguments to the sendmail program.
+For more information run C<< perldoc Mail::Mailer >>.
+Setting this option is strongly discouraged.  Fix your local MTA
+instead.
+Default: none.
+
 =back
 
 =head1 AUTHORS
@@ -273,6 +316,7 @@ use File::Path;
 use File::Temp qw{tempdir};
 use Text::Template;
 use MIME::Entity;
+use Encode;
 use Fcntl;
 use IO::Select;
 use Getopt::Long;
@@ -283,8 +327,26 @@ my $REVISION = '$Rev$';
 my ($REVISION_NUMER) = $REVISION =~ /(\d+)/;
 my $VERSION = "0.0.0.$REVISION_NUMER";
 
+# Global variables
+my @KEYIDS;
+my $params;
 
+##
+# Display an error message on STDERR and then exit.
+#
+# @param $exitcode exit code status to use to end the program
+# @param $line     error message to display on STDERR
+#
+sub myerror($$) {
+       my ($exitcode, $line) = @_;
+       print "[ERROR] $line\n";        
+       exit $exitcode;
+};
 
+sub mywarn($) {
+       my ($line) = @_;
+       print "[WARN] $line\n";
+};
 sub notice($) {
        my ($line) = @_;
        print "[NOTICE] $line\n";
@@ -312,9 +374,12 @@ sub generate_config() {
        my $gecos = defined $ENV{'LOGNAME'} ? (getpwnam($ENV{LOGNAME}))[6] : undef;
        my $email;
        my @keys;
-       my $hostname = `hostname -f`;
+       # BSD does not have hostname -f, so we try without -f first
+       my $hostname = `hostname`;
+       $hostname = `hostname -f` unless $hostname =~ /\./;
        chomp $hostname;
        my ($Cgecos,$Cemail,$Ckeys) = ('','','');
+
        if (defined $gecos) {
                $gecos =~ s/,.*//;
 
@@ -351,26 +416,39 @@ sub generate_config() {
                ($Cgecos,$Cemail,$Ckeys) = ('#','#','#');
        };
 
-       return <<EOT;
-# .caffrc -- vim:syntax=perl:
+       my $template = <<EOT;
+# .caffrc -- vim:ft=perl:
 # This file is in perl(1) format - see caff(1) for details.
 
-$Cgecos\$CONFIG{'owner'}       = '$gecos';
-$Cemail\$CONFIG{'email'}       = '$email';
+$Cgecos\$CONFIG{'owner'} = '$gecos';
+$Cemail\$CONFIG{'email'} = '$email';
+#\$CONFIG{'reply-to'} = 'foo\@bla.org';
 
-# you can get your long keyid from
+# You can get your long keyid from
 #   gpg --with-colons --list-key <yourkeyid|name|emailaddress..>
 #
-# if you have a v4 key, it will simply be the last 16 digits of
+# If you have a v4 key, it will simply be the last 16 digits of
 # your fingerprint.
 #
 # Example:
-#   \$CONFIG{'keyid'}       = [ qw{FEDCBA9876543210} ];
+#   \$CONFIG{'keyid'} = [ qw{FEDCBA9876543210} ];
 #  or, if you have more than one key:
-#   \$CONFIG{'keyid'}       = [ qw{0123456789ABCDEF 89ABCDEF76543210} ];
+#   \$CONFIG{'keyid'} = [ qw{0123456789ABCDEF 89ABCDEF76543210} ];
+$Ckeys\$CONFIG{'keyid'} = [ qw{@keys} ];
+
+# Select this/these keys to sign with
+#\$CONFIG{'local-user'} = [ qw{@keys} ];
+
+# Additionally encrypt messages for these keyids
+#\$CONFIG{'also-encrypt-to'} = [ qw{@keys} ];
 
-$Ckeys\$CONFIG{'keyid'}       = [ qw{@keys} ];
+# Mail template to use for the encrypted part
+#\$CONFIG{'mail-template'} = << 'EOM';
 EOT
+
+       $template .= "#$_" foreach <DATA>;
+       $template .= "#EOM\n";
+       return $template;
 };
 
 sub check_executable($$) {
@@ -414,39 +492,26 @@ sub load_config() {
        };
        @{$CONFIG{'keyid'}} = map { uc } @{$CONFIG{'keyid'}};
        $CONFIG{'export-sig-age'}= 24*60*60 unless defined $CONFIG{'export-sig-age'};
-       $CONFIG{'keyserver'} = 'subkeys.pgp.net' unless defined $CONFIG{'keyserver'};
+       $CONFIG{'keyserver'} = 'pool.sks-keyservers.net' unless defined $CONFIG{'keyserver'};
        $CONFIG{'gpg'} = 'gpg' unless defined $CONFIG{'gpg'};
        $CONFIG{'gpg-sign'} = $CONFIG{'gpg'} unless defined $CONFIG{'gpg-sign'};
        $CONFIG{'gpg-delsig'} = $CONFIG{'gpg'} unless defined $CONFIG{'gpg-delsig'};
        check_executable("gpg", $CONFIG{'gpg'});
        check_executable("gpg-sign", $CONFIG{'gpg-sign'});
        check_executable("gpg-delsig", $CONFIG{'gpg-delsig'});
-       $CONFIG{'secret-keyring'} = $ENV{'HOME'}.'/.gnupg/secring.gpg' unless defined $CONFIG{'secret-keyring'};
+       $CONFIG{'secret-keyring'} = ($ENV{'GNUPGHOME'} || "$ENV{'HOME'}/.gnupg") . '/secring.gpg'
+               unless defined $CONFIG{'secret-keyring'};
        $CONFIG{'no-download'} = 0 unless defined $CONFIG{'no-download'};
        $CONFIG{'no-sign'} = 0 unless defined $CONFIG{'no-sign'};
        $CONFIG{'key-files'} = () unless defined $CONFIG{'key-files'};
-       $CONFIG{'mail-template'} = <<'EOM' unless defined $CONFIG{'mail-template'};
-Hi,
-
-please find attached the user id{(scalar @uids >= 2 ? 's' : '')}.
-{foreach $uid (@uids) {
-    $OUT .= "\t".$uid."\n";
-};} of your key {$key} signed by me.
-
-Note that I did not upload your key to any keyservers.
-If you have multiple user ids, I sent the signature for each user id
-separately to that user id's associated email address. You can import
-the signatures by running each through `gpg --import`.
-
-If you want this new signature to be available to others, please upload
-it yourself. With GnuPG this can be done using
-       gpg --keyserver subkeys.pgp.net --send-key {$key}
-
-If you have any questions, don't hesitate to ask.
-
-Regards,
-{$owner}
-EOM
+       $CONFIG{'mailer-send'} = [] unless defined $CONFIG{'mailer-send'};
+       die ("$PROGRAM_NAME: mailer-send is not an array ref in $config.\n") unless (ref $CONFIG{'mailer-send'} eq 'ARRAY');
+       unless (defined $CONFIG{'mail-template'}) {
+               $CONFIG{'mail-template'} .= $_ foreach <DATA>;
+       }
+       if ((defined $CONFIG{'also-encrypt-to'}) && ! (ref($CONFIG{'also-encrypt-to'}) eq 'ARRAY')) {
+               $CONFIG{'also-encrypt-to'} = [ $CONFIG{'also-encrypt-to'} ];
+       };
 };
 
 sub make_gpg_fds() {
@@ -608,7 +673,7 @@ my $KEYEDIT_KEYEDIT_OR_DELSIG_PROMPT = '^\[GNUPG:\] (GET_BOOL keyedit.delsig|GET
 my $KEYEDIT_DELSUBKEY_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.remove.subkey';
 
 load_config;
-my $USER_AGENT = "caff $VERSION - (c) 2004, 2005 Peter Palfrader et al.";
+my $USER_AGENT = "caff $VERSION - http://pgp-tools.alioth.debian.org/";
 
 my $KEYSBASE =  $CONFIG{'caffhome'}.'/keys';
 my $GNUPGHOME = $CONFIG{'caffhome'}.'/gnupghome';
@@ -623,13 +688,13 @@ my $DATE_STRING = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday);
 
 sub version($) {
        my ($fd) = @_;
-       print $fd "caff $VERSION - (c) 2004, 2005 Peter Palfrader et al.\n";
+       print $fd "caff $VERSION - (c) 2004, 2005, 2006 Peter Palfrader et al.\n";
 };
 
 sub usage($$) {
        my ($fd, $exitcode) = @_;
        version($fd);
-       print $fd "Usage: $PROGRAM_NAME [-eEmMRS] [-u <yourkeyid>] <keyid> [<keyid> ...]\n";
+       print $fd "Usage: $PROGRAM_NAME [-eERS] [-m <yes|ask-yes|ask-no|no>] [-u <yourkeyid>] <keyid> [<keyid> ...]\n";
        print $fd "Consult the manual page for more information.\n";
        exit $exitcode;
 };
@@ -735,21 +800,37 @@ sub send_mail($$$@) {
                $gpg->options->meta_interactive( 0 );
                my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
                $gpg->options->push_recipients( $key_id );
-               $gpg->options->push_recipients( $CONFIG{'also-encrypt-to'} ) if defined $CONFIG{'also-encrypt-to'};
+               if (defined $CONFIG{'also-encrypt-to'}) {
+                       $gpg->options->push_recipients($_) foreach @{$CONFIG{'also-encrypt-to'}};
+               }
                my $pid = $gpg->encrypt(handles => $handles);
                my ($stdout, $stderr, $status) = readwrite_gpg($message, $inputfd, $stdoutfd, $stderrfd, $statusfd);
                waitpid $pid, 0;
                if ($stdout eq '') {
-                       warn ("No data from gpg for list-key $key_id\n");
-                       next;
+                       if (($status =~ /^\[GNUPG:\] INV_RECP ([0-9]+) ([0-9A-F]+)/m) and
+                           (defined $CONFIG{'also-encrypt-to'})) {
+                               my $reason = $1;
+                               my $keyid = $2;
+                               if (grep { $_ eq $keyid } @{$CONFIG{'also-encrypt-to'}}) {
+                                       warn("Could not encrypt to $keyid, specified in CONFIG{'also-encrypt-to'}.\n".
+                                            "Try to update the key using gpg --homedir=$GNUPGHOME --import <exported key>\n".
+                                            "or try the following if you are slightly more daring:\n".
+                                            " gpg --export $keyid | gpg --homedir=$GNUPGHOME --import\n");
+                                       return;
+                               };
+                       };
+                       warn ("No data from gpg for encrypting mail.  STDERR was:\n$stderr\nstatus output was:\n$status\n");
+                       return;
                };
                $message = $stdout;
 
                $message_entity = MIME::Entity->build(
-                       Type        => 'multipart/encrypted; protocol="application/pgp-encrypted"');
+                       Type        => 'multipart/encrypted; protocol="application/pgp-encrypted"',
+                       Encoding    => '7bit');
 
                $message_entity->attach(
                        Type        => "application/pgp-encrypted",
+                       Filename    => "signedkey.msg",
                        Disposition => 'attachment',
                        Encoding    => "7bit",
                        Data        => "Version: 1\n");
@@ -764,11 +845,13 @@ sub send_mail($$$@) {
 
        $message_entity->head->add("Subject", "Your signed PGP key 0x$key_id");
        $message_entity->head->add("To", $address);
-       $message_entity->head->add("From", '"'.$CONFIG{'owner'}.'" <'.$CONFIG{'email'}.'>');
+       $message_entity->head->add("From", '"'.Encode::encode('MIME-Q', $CONFIG{'owner'}).'" <'.$CONFIG{'email'}.'>');
+       $message_entity->head->add("Sender", '"'.Encode::encode('MIME-Q', $CONFIG{'owner'}).'" <'.$CONFIG{'email'}.'>');
        $message_entity->head->add("Reply-To", $CONFIG{'reply-to'}) if defined $CONFIG{'reply-to'};
        $message_entity->head->add("Bcc", $CONFIG{'bcc'}) if defined $CONFIG{'bcc'};
        $message_entity->head->add("User-Agent", $USER_AGENT);
-       $message_entity->send();
+       mywarn("You have set arguments to pass to Mail::Mailer.  Better fix your MTA.  (Also, Mail::Mailer's error reporting is non existant, so it won't tell you when it doesn't work.)") if (scalar @{$CONFIG{'mailer-send'}} > 0);
+       $message_entity->send(@{$CONFIG{'mailer-send'}});
        $message_entity->stringify();
 };
 
@@ -823,12 +906,161 @@ sub delete_signatures($$$$$$) {
        return $signed_by_me;
 };
 
+##
+# Check the local user keys.
+#
+# This function checks if the keyids defined through the --local-user
+# command line option or set in ~/.caffrc are valid and known to be one of the
+# keyids listed in ~/.caffrc.
+#
+# @return an array containing the local user keys\n
+#         (undef) if no valid key has been found
+#
+sub get_local_user_keys() {
+       my @local_user = ();
+       my @key_list;
+       
+       # No user-defined key id has been specified by the user, no need for
+       # further checks
+       if (!$CONFIG{'local-user'}) {
+               return (undef);
+       }       
+       
+       # Parse the list of keys
+       if (ref($CONFIG{'local-user'})) {
+               @key_list = @{$CONFIG{'local-user'}};
+       }
+       else {
+               @key_list = split /\s*,\s*/, $CONFIG{'local-user'};
+       }
+
+       # Check every key defined by the user...
+       for my $user_key (@key_list) {
+               
+               $user_key =~ s/^0x//i;
+               $user_key = uc($user_key);
+               
+               unless ($user_key =~ m/^([A-F0-9]{8}|[A-F0-9]{16}|[A-F0-9]{40})$/) {
+                       mywarn "Local-user $user_key is not a valid keyid.";
+                       next;
+               }
+               
+               unless (grep (/$user_key$/, @{$CONFIG{'keyid'}})) {
+                       mywarn "Local-user $user_key is not defined as one of your keyid in ~/.caffrc (it will not be used).";
+                       next;
+               }
+               
+               push (@local_user, $user_key);
+       }
 
+       # If no local-user key are valid, there is no need to go further
+       unless (defined $local_user[0]) {
+               myerror (1, "None of the local-user keys seem to be known as a keyid listed in ~/.caffrc.");
+       }
 
-my $USER;
-my @KEYIDS;
-my $params;
+       return @local_user;
+}
+
+##
+# Import a key from the user gnupghome into a specified gnupghome.
+#
+# @param asciikey      ascii format of the gpg key to import
+# @param dst_gnupghome gnupghome directory where to import the key
+#
+# @return 0 if successful\n
+#         1 if the key could not be exported.\n
+#         2 if the key could not be imported.
+#
+sub import_key_from_user_gnupghome() {
+    my $err;
+    my ($asciikey, $dst_gpghome) = @_;
+
+    trace("Exporting key $asciikey from your normal GnuPGHOME.");
+    my $key = export_key(undef, $asciikey);
+    if (defined $key && $key ne '') {
+        trace("Importing key $asciikey into $GNUPGHOME.");
+        if (import_key($GNUPGHOME, $key)) {
+            $err = 0;
+        } else {
+            warn("Could not import $asciikey into caff's gnupghome.");
+            $err = 2;
+        }
+    } else {
+        $err = 1;
+    }
+
+    return $err;
+}
+
+##
+# Import a key file into a specified gnupghome.
+#
+# @param keyfile       file containing the keys to import
+# @param dst_gnupghome gnupghome directory where to import the key
+#
+# @return 0 if successful\n
+#         1 if an error occured.
+#
+sub import_key_files() {
+    my $err;
+    my ($keyfile, $dst_gpghome) = @_;
 
+    my $gpg = GnuPG::Interface->new();
+    $gpg->call( $CONFIG{'gpg'} );
+    $gpg->options->hash_init(
+        'homedir' => $dst_gpghome,
+        'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always } ] );
+    $gpg->options->meta_interactive( 0 );
+    my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
+    my $pid = $gpg->import_keys(handles => $handles, command_args => $keyfile);
+    my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
+    info("Importing keys from file $keyfile");
+    waitpid $pid, 0;
+
+    if ($status !~ /^\[GNUPG:\] IMPORT_OK/m) {
+        warn $stderr;
+        $err = 1;
+    } else {
+        $err = 0;
+    }
+
+    return $err;
+}
+
+##
+# Import keys to be signed into caff gnupghome directory.
+#
+# This function imports the keys the user wants to sign into the caff gnupghome
+# directory. We looks for the keys in the the user gnupghome directory first,
+# and in the key files specified by the user if not all of the keys have been
+# found.
+#
+sub import_keys_to_sign() {
+    # Check if we can find the gpg key from our normal gnupghome, and then
+    # try to import it into our working gnupghome directory
+    my $imported_keys = 0;
+    foreach my $keyid (@KEYIDS) {
+        if (!&import_key_from_user_gnupghome($keyid, $GNUPGHOME)) {
+            info("Key $keyid imported from your normal GnuPGHOME.");
+            $imported_keys++;
+        }
+    }
+
+    # If all of the keys have been successfully imported, there is no need to
+    # go further
+    return 1 if ($imported_keys == scalar (@KEYIDS));
+
+    # Import user specified key files
+    foreach my $keyfile (@{$CONFIG{'key-files'}}) {
+        &import_key_files($keyfile, $GNUPGHOME);
+    }
+
+    return 0;
+}
+
+###################
+# argument handling
+###################
 Getopt::Long::config('bundling');
 if (!GetOptions (
        '-h'              =>  \$params->{'help'},
@@ -841,8 +1073,8 @@ if (!GetOptions (
        '--export-old'    =>  \$params->{'export-old'},
        '-E'              =>  \$params->{'no-export-old'},
        '--no-export-old' =>  \$params->{'no-export-old'},
-       '-m'              =>  \$params->{'mail'},
-       '--mail'          =>  \$params->{'mail'},
+       '-m:s'              =>  \$params->{'mail'},
+       '--mail:s'          =>  \$params->{'mail'},
        '-M'              =>  \$params->{'no-mail'},
        '--no-mail'       =>  \$params->{'no-mail'},
        '-R'              =>  \$params->{'no-download'},
@@ -862,89 +1094,50 @@ if ($params->{'version'}) {
 };
 usage(\*STDERR, 1) unless scalar @ARGV >= 1;
 
+$CONFIG{'local-user'}  = $params->{'local-user'}  if defined $params->{'local-user'};
+$CONFIG{'no-download'} = $params->{'no-download'} if defined $params->{'no-download'};
+$CONFIG{'no-sign'}     = $params->{'no-sign'}     if defined $params->{'no-sign'};
 
+$CONFIG{'no-mail'}     = $params->{'no-mail'}     if defined $params->{'no-mail'};
+$CONFIG{'mail'}        = $params->{'mail'}        if defined $params->{'mail'};
 
-if ($params->{'local-user'}) {
-       $USER = $params->{'local-user'};
-       $USER =~ s/^0x//i;
-       unless ($USER =~ /^([A-F0-9]{8}|[A-F0-9]{16}|[A-F0-9]{40})$/i) {
-               print STDERR "-u $USER is not a keyid.\n";
-               usage(\*STDERR, 1);
-       };
-       $USER = uc($USER);
-};
+# If old 'no-mail' parameter, or if the 'mail' parameter is set to 'no'
+if ( defined $CONFIG{'no-mail'} || 
+     ( defined $CONFIG{'mail'} && $CONFIG{'mail'}  eq 'no' ) ) {
+       $CONFIG{'mail'} = 'no';
+
+} elsif ( !defined $CONFIG{'mail'} ) {
+       $CONFIG{'mail'} = 'ask-yes';
+}
+
+push @{$CONFIG{'key-files'}}, @{$params->{'key-files'}} if defined $params->{'key-files'};
 
-for my $keyid (@ARGV) {
+for my $keyid (map { split /\n/ } @ARGV) { # caff "`cat txt`" is a single argument
        $keyid =~ s/^0x//i;
-       unless ($keyid =~ /^([A-F0-9]{8}|[A-F0-9]{16}||[A-F0-9]{40})$/i) {
-               if ($keyid =~ /^[A-F0-9]{32}$/) {
-                       info("Ignoring v3 fingerprint $keyid.  v3 keys are obsolete.");
-                       next;
-               };
+       $keyid =~ s/ //g; # gpg --fingerprint includes spaces
+       if ($keyid =~ /^[A-F0-9]{32}$/i) {
+               info("Ignoring v3 fingerprint $keyid.  v3 keys are obsolete.");
+               next;
+       };
+       if ($keyid !~ /^([A-F0-9]{8}|[A-F0-9]{16}|[A-F0-9]{40})$/i) {
                print STDERR "$keyid is not a keyid.\n";
                usage(\*STDERR, 1);
        };
        push @KEYIDS, uc($keyid);
 };
 
-$CONFIG{'no-download'} = $params->{'no-download'} if defined $params->{'no-download'};
-$CONFIG{'no-mail'}     = $params->{'no-mail'}     if defined $params->{'no-mail'};
-$CONFIG{'mail'}        = $params->{'mail'}        if defined $params->{'mail'};
-$CONFIG{'no-sign'}     = $params->{'no-sign'}     if defined $params->{'no-sign'};
-push @{$CONFIG{'key-files'}}, @{$params->{'key-files'}} if defined $params->{'key-files'};
-
-
 #################
 # import own keys
 #################
 for my $keyid (@{$CONFIG{'keyid'}}) {
-       my $gpg = GnuPG::Interface->new();
-       $gpg->call( $CONFIG{'gpg'} );
-       $gpg->options->hash_init(
-               'homedir' => $GNUPGHOME,
-               'extra_args' => [ qw{ --no-auto-check-trustdb --trust-model=always --with-colons --fixed-list-mode --fast-list-mode } ] );
-       $gpg->options->meta_interactive( 0 );
-       my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
-       my $pid = $gpg->list_public_keys(handles => $handles, command_args => $keyid);
-       my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
-       waitpid $pid, 0;
-
-       if ($stdout eq '') {
-               warn ("No data from gpg for list-key\n"); # There should be at least 'tru:' everywhere.
-       };
-       unless ($stdout =~ /^pub:(?:[^:]*:){3,3}$keyid:/m) {
-               info("Key $keyid not found in caff's home.  Getting it from your normal GnuPGHome.");
-               my $key = export_key(undef, $keyid);
-               if (!defined $key || $key eq '') {
-                       warn ("Did not get key $keyid from your normal GnuPGHome\n");
-                       next;
-               };
-               my $result = import_key($GNUPGHOME, $key);
-               unless ($result) {
-                       warn ("Could not import $keyid into caff's gnupghome.\n");
-                       next;
-               };
-       }
-}
-
-########################
-# import keys from files
-########################
-foreach my $keyfile (@{$CONFIG{'key-files'}}) {
-    my $gpg = GnuPG::Interface->new();
-    $gpg->call( $CONFIG{'gpg'} );
-    $gpg->options->hash_init('homedir' => $GNUPGHOME);
-    $gpg->options->meta_interactive( 0 );
-    my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
-    my $pid = $gpg->import_keys(handles => $handles, command_args => $keyfile);
-    my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
-    info ("Importing keys from $keyfile");
-    waitpid $pid, 0;
-    if ($status !~ /^\[GNUPG:\] IMPORT_OK/m) {
-       warn $stderr;
+    info("Importing key $keyid from your normal GnuPGHome.");
+    if (&import_key_from_user_gnupghome($keyid, $GNUPGHOME)) {
+        mywarn("Key $keyid not found.");
     }
 }
 
+&import_keys_to_sign();
+
 #############################
 # receive keys from keyserver
 #############################
@@ -1000,6 +1193,12 @@ if ($CONFIG{'no-download'}) {
        if (scalar %local_keyids) {
                notice ("Import failed for: ". (join ' ', keys %local_keyids)."." . ($had_v3_keys ? " (Or maybe it's one of those ugly v3 keys?)" :  ""));
                exit 1 unless ask ("Some keys could not be imported - continue anyway?", 0);
+               if (scalar keys %local_keyids == 1) {
+                       mywarn("Assuming ". (join ' ', keys %local_keyids)." is a fine keyid.");
+               } else {
+                       mywarn("Assuming ". (join ' ', keys %local_keyids)." are fine keyids.");
+               };
+               push @keyids_ok, keys %local_keyids;
        }
 };
 
@@ -1016,20 +1215,24 @@ if ($CONFIG{'ask-sign'} && ! $CONFIG{'no-sign'}) {
 }
        
 unless ($CONFIG{'no-sign'}) {
+       my @local_user = &get_local_user_keys();
+
        info("Sign the following keys according to your policy, then exit gpg with 'save' after signing each key");
        for my $keyid (@keyids_ok) {
-               my @command;
-               push @command, $CONFIG{'gpg-sign'};
-               push @command, '--local-user', $USER if (defined $USER);
-               push @command, "--homedir=$GNUPGHOME";
-               push @command, '--secret-keyring', $CONFIG{'secret-keyring'};
-               push @command, '--no-auto-check-trustdb';
-               push @command, '--trust-model=always';
-               push @command, '--edit', $keyid;
-               push @command, 'sign';
-               push @command, split ' ', $CONFIG{'gpg-sign-args'} || "";
-               print join(' ', @command),"\n";
-               system (@command);
+               foreach my $local_user (@local_user) {
+                       my @command;
+                       push @command, $CONFIG{'gpg-sign'};
+                       push @command, '--local-user', $local_user if (defined $local_user);
+                       push @command, "--homedir=$GNUPGHOME";
+                       push @command, '--secret-keyring', $CONFIG{'secret-keyring'};
+                       push @command, '--no-auto-check-trustdb';
+                       push @command, '--trust-model=always';
+                       push @command, '--edit', $keyid;
+                       push @command, 'sign';
+                       push @command, split ' ', $CONFIG{'gpg-sign-args'} || "";
+                       print join(' ', @command),"\n";
+                       system (@command);
+               };
        };
 };
 
@@ -1231,7 +1434,7 @@ for my $keyid (@keyids_ok) {
        if (scalar @UIDS == 0) {
                info("found no signed uids for $keyid");
        } else {
-               next if $CONFIG{'no-mail'}; # do not send mail
+               next if ($CONFIG{'mail'} eq 'no'); # do not send mail
 
                my @attached;
                for my $uid (@UIDS) {
@@ -1250,17 +1453,46 @@ for my $keyid (@keyids_ok) {
                        if (!$uid->{'is_uat'} && ($uid->{'text'} =~ /@/)) {
                                my $address = $uid->{'text'};
                                $address =~ s/.*<(.*)>.*/$1/;
-                               if (ask("Mail signature for $uid->{'text'} to '$address'?", 1, $CONFIG{'mail'})) {
+                               if (ask("Mail signature for $uid->{'text'} to '$address'?", $CONFIG{'mail'} ne 'ask-no', $CONFIG{'mail'} eq 'yes')) {
                                        my $mail = send_mail($address, $can_encrypt, $longkeyid, $uid, @attached);
-
-                                       my $keydir = "$KEYSBASE/$DATE_STRING";
-                                       my $mailfile = "$keydir/$longkeyid.mail.".$uid->{'serial'}.".".sanitize_uid($uid->{'text'});
-                                       open (KEY, ">$mailfile") or die ("Cannot open $mailfile: $!\n");
-                                       print KEY $mail;
-                                       close KEY;
+                                       if (defined $mail) {
+                                               my $keydir = "$KEYSBASE/$DATE_STRING";
+                                               my $mailfile = "$keydir/$longkeyid.mail.".$uid->{'serial'}.".".sanitize_uid($uid->{'text'});
+                                               open (KEY, ">$mailfile") or die ("Cannot open $mailfile: $!\n");
+                                               print KEY $mail;
+                                               close KEY;
+                                       } else {
+                                               warn "Generating mail failed.\n";
+                                       };
                                };
                        };
                };
        };
 
 };
+
+###########################
+# the default mail template
+###########################
+
+__DATA__
+Hi,
+
+please find attached the user id{(scalar @uids >= 2 ? 's' : '')}
+{foreach $uid (@uids) {
+    $OUT .= "\t".$uid."\n";
+};}of your key {$key} signed by me.
+
+If you have multiple user ids, I sent the signature for each user id
+separately to that user id's associated email address. You can import
+the signatures by running each through `gpg --import`.
+
+Note that I did not upload your key to any keyservers. If you want this
+new signature to be available to others, please upload it yourself.
+With GnuPG this can be done using
+       gpg --keyserver pool.sks-keyservers.net --send-key {$key}
+
+If you have any questions, don't hesitate to ask.
+
+Regards,
+{$owner}