/[pgp-tools]/trunk/caff/caff
ViewVC logotype

Contents of /trunk/caff/caff

Parent Directory Parent Directory | Revision Log Revision Log


Revision 126 - (hide annotations) (download)
Mon Jul 11 11:49:26 2005 UTC (7 years, 10 months ago) by kink-guest
File size: 33839 byte(s)
new option to pause before continuing to the signing step,
thanks Christof Douma (Closes: #316129).
1 weasel 5 #!/usr/bin/perl -w
2    
3 weasel 9 # caff -- CA - Fire and Forget
4 weasel 6 # $Id$
5 weasel 5 #
6 weasel 36 # Copyright (c) 2004, 2005 Peter Palfrader <peter@palfrader.org>
7 myon-guest 102 # Copyright (c) 2005 Christoph Berg <cb@df7cb.de>
8 weasel 5 #
9     # All rights reserved.
10     #
11     # Redistribution and use in source and binary forms, with or without
12     # modification, are permitted provided that the following conditions
13     # are met:
14     # 1. Redistributions of source code must retain the above copyright
15     # notice, this list of conditions and the following disclaimer.
16     # 2. Redistributions in binary form must reproduce the above copyright
17     # notice, this list of conditions and the following disclaimer in the
18     # documentation and/or other materials provided with the distribution.
19     # 3. The name of the author may not be used to endorse or promote products
20     # derived from this software without specific prior written permission.
21     #
22     # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
23     # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
24     # OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
25     # IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
26     # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
27     # NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28     # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29     # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30     # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
31     # THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32    
33 weasel 9 =pod
34    
35     =head1 NAME
36    
37     caff -- CA - Fire and Forget
38    
39     =head1 SYNOPSIS
40    
41     =over
42    
43 myon-guest 106 =item B<caff> [-eEmMRS] [-u I<yourkeyid>] I<keyid> [I<keyid> ..]
44 weasel 9
45     =back
46    
47     =head1 DESCRIPTION
48    
49     CA Fire and Forget is a script that helps you in keysigning. It takes a list
50     of keyids on the command line, fetches them from a keyserver and calls GnuPG so
51     that you can sign it. It then mails each key to all its email addresses - only
52     including the one UID that we send to in each mail, pruned from all but self
53     sigs and sigs done by you.
54    
55     =head1 OPTIONS
56    
57     =over
58    
59 weasel 109 =item B<-e>, B<--export-old>
60 myon-guest 79
61 weasel 109 Export old signatures. Default is to ask the user for each old signature.
62    
63     =item B<-E>, B<--no-export-old>
64    
65     Do not export old signatures. Default is to ask the user for each old
66 myon-guest 106 signature.
67 myon-guest 79
68 weasel 109 =item B<-m>, B<--mail>
69 myon-guest 79
70 weasel 109 Send mail after signing. Default is to ask the user for each uid.
71 weasel 95
72 weasel 109 =item B<-M>, B<--no-mail>
73    
74     Do not send mail after signing. Default is to ask the user for each uid.
75    
76 weasel 95 =item B<-R>, B<--no-download>
77    
78 myon-guest 79 Do not retrieve the key to be signed from a keyserver.
79    
80 myon-guest 106 =item B<-S>, B<--no-sign>
81    
82     Do not sign the keys.
83    
84 weasel 95 =item B<-u> I<yourkeyid>, B<--local-user> I<yourkeyid>
85 weasel 9
86     Select the key that is used for signing, in case you have more than one key.
87    
88     =back
89    
90     =head1 FILES
91    
92     =over
93    
94     =item $HOME/.caffrc - configuration file
95    
96     =back
97    
98     =head1 CONFIGURATION FILE OPTIONS
99    
100     The configuration file is a perl script that sets values in the hash B<%CONFIG>.
101    
102     Example:
103    
104 myon-guest 41 $CONFIG{owner} = q{Peter Palfrader};
105     $CONFIG{email} = q{peter@palfrader.org};
106     $CONFIG{keyid} = [ qw{DE7AAF6E94C09C7F 62AF4031C82E0039} ];
107 weasel 9
108 myon-guest 102 =head2 Required basic settings
109 weasel 9
110     =over
111    
112     =item B<owner> [string]
113    
114     Your name. B<REQUIRED>.
115    
116     =item B<email> [string]
117    
118     Your email address, used in From: lines. B<REQUIRED>.
119    
120     =item B<keyid> [list of keyids]
121    
122     A list of your keys. This is used to determine which signatures to keep
123     in the pruning step. If you select a key using B<-u> it has to be in
124     this list. B<REQUIRED>.
125    
126 myon-guest 102 =head2 General settings
127 weasel 9
128 myon-guest 102 =item B<caffhome> [string]
129 weasel 9
130 myon-guest 102 Base directory for the files caff stores. Default: B<$HOME/.caff/>.
131 weasel 9
132 myon-guest 102 =head2 GnuPG settings
133 weasel 9
134     =item B<gpg> [string]
135    
136     Path to the GnuPG binary. Default: B<gpg>.
137    
138     =item B<gpg-sign> [string]
139    
140     Path to the GnuPG binary which is used to sign keys. Default: what
141     B<gpg> is set to.
142    
143 weasel 18 =item B<gpg-delsig> [string]
144 weasel 9
145 weasel 69 Path to the GnuPG binary which is used to split off signatures. This was
146     needed while the upstream GnuPG was not fixed. Default: what B<gpg>
147     is set to.
148 weasel 9
149     =item B<secret-keyring> [string]
150    
151     Path to your secret keyring. Default: B<$HOME/.gnupg/secring.gpg>.
152    
153 weasel 18 =item B<also-encrypt-to> [keyid]
154    
155     An additional keyid to encrypt messages to. Default: none.
156    
157 myon-guest 106 =item B<gpg-sign-args> [string]
158    
159 myon-guest 119 Additional arguments to pass to gpg. Default: none.
160 myon-guest 106
161 myon-guest 102 =head2 Keyserver settings
162    
163     =item B<keyserver> [string]
164    
165     Keyserver to download keys from. Default: B<subkeys.pgp.net>.
166    
167 weasel 18 =item B<no-download> [boolean]
168    
169     If true, then skip the step of fetching keys from the keyserver.
170     Default: B<0>.
171    
172 myon-guest 102 =head2 Signing settings
173    
174 weasel 18 =item B<no-sign> [boolean]
175    
176     If true, then skip the signing step. Default: B<0>.
177    
178 kink-guest 126 =item B<ask-sign> [boolean]
179    
180     If true, then pause before continuing to the signing step.
181     This is useful for offline signing. Default: B<0>.
182    
183 myon-guest 102 =item B<export-sig-age> [seconds]
184    
185     Don't export UIDs by default, on which your latest signature is older
186     than this age. Default: B<24*60*60> (i.e. one day).
187    
188     =head2 Mail settings
189    
190     =item B<mail> [boolean]
191    
192     Do not prompt for sending mail, just do it. Default: B<0>.
193    
194     =item B<no-mail> [boolean]
195    
196     Do not prompt for sending mail. The messages are still written to
197     $CONFIG{caffhome}/keys/. Default: B<0>.
198    
199 weasel 93 =item B<mail-template> [string]
200    
201 weasel 104 Email template which is used as the body text for the email sent out
202 weasel 93 instead of the default text if specified. The following perl variables
203     can be used in the template:
204    
205     =over
206    
207     =item B<{owner}> [string]
208    
209     Your name as specified in the L<B<owner>|/item_owner__5bstring_5d> setting.
210    
211     =item B<{key}> [string]
212    
213     The keyid of the key you signed.
214    
215     =item B<{@uids}> [array]
216    
217     The UIDs for which signatures are included in the mail.
218    
219 weasel 9 =back
220    
221 myon-guest 101 =item B<bcc> [string]
222    
223     Address to send blind carbon copies to when sending mail.
224     Default: none.
225    
226 weasel 93 =back
227    
228 myon-guest 102 =head1 AUTHORS
229 weasel 9
230 myon-guest 102 =over
231 weasel 9
232 myon-guest 102 =item Peter Palfrader <peter@palfrader.org>
233    
234     =item Christoph Berg <cb@df7cb.de>
235    
236     =back
237    
238 myon-guest 41 =head1 WEBSITE
239    
240     http://pgp-tools.alioth.debian.org/
241    
242 weasel 9 =cut
243    
244 weasel 5 use strict;
245     use IO::Handle;
246     use English;
247     use File::Path;
248     use File::Temp qw{tempdir};
249 weasel 93 use Text::Template;
250 weasel 5 use MIME::Entity;
251     use Fcntl;
252     use IO::Select;
253 weasel 95 use Getopt::Long;
254 weasel 5 use GnuPG::Interface;
255    
256     my %CONFIG;
257 weasel 6 my $REVISION = '$Rev$';
258 weasel 7 my ($REVISION_NUMER) = $REVISION =~ /(\d+)/;
259     my $VERSION = "0.0.0.$REVISION_NUMER";
260 weasel 5
261     sub load_config() {
262     my $config = $ENV{'HOME'} . '/.caffrc';
263 myon-guest 41 -f $config or die "No file $config present. See caff(1).\n";
264 weasel 5 unless (scalar eval `cat $config`) {
265     die "Couldn't parse $config: $EVAL_ERROR\n" if $EVAL_ERROR;
266     };
267    
268 weasel 9 $CONFIG{'caffhome'}=$ENV{'HOME'}.'/.caff' unless defined $CONFIG{'caffhome'};
269 weasel 5 die ("owner is not defined.\n") unless defined $CONFIG{'owner'};
270     die ("email is not defined.\n") unless defined $CONFIG{'email'};
271     die ("keyid is not defined.\n") unless defined $CONFIG{'keyid'};
272     die ("keyid is not an array ref\n") unless (ref $CONFIG{'keyid'} eq 'ARRAY');
273     for my $keyid (@{$CONFIG{'keyid'}}) {
274 kink-guest 124 $keyid =~ /^[A-Fa-f0-9]{16}$/ or die ("key $keyid is not a long (16 digit) keyid.\n");
275 weasel 5 };
276     @{$CONFIG{'keyid'}} = map { uc } @{$CONFIG{'keyid'}};
277     $CONFIG{'export-sig-age'}= 24*60*60 unless defined $CONFIG{'export-sig-age'};
278     $CONFIG{'keyserver'} = 'subkeys.pgp.net' unless defined $CONFIG{'keyserver'};
279     $CONFIG{'gpg'} = 'gpg' unless defined $CONFIG{'gpg'};
280     $CONFIG{'gpg-sign'} = $CONFIG{'gpg'} unless defined $CONFIG{'gpg-sign'};
281     $CONFIG{'gpg-delsig'} = $CONFIG{'gpg'} unless defined $CONFIG{'gpg-delsig'};
282     $CONFIG{'secret-keyring'} = $ENV{'HOME'}.'/.gnupg/secring.gpg' unless defined $CONFIG{'secret-keyring'};
283 weasel 18 $CONFIG{'no-download'} = 0 unless defined $CONFIG{'no-download'};
284     $CONFIG{'no-sign'} = 0 unless defined $CONFIG{'no-sign'};
285 weasel 93 $CONFIG{'mail-template'} = <<'EOM' unless defined $CONFIG{'mail-template'};
286     Hi,
287    
288     please find attached the user id{(scalar @uids >= 2 ? 's' : '')}.
289     {foreach $uid (@uids) {
290     $OUT .= "\t".$uid."\n";
291     };} of your key {$key} signed by me.
292    
293 kink-guest 123 Note that I did not upload your key to any keyservers.
294     If you have multiple user ids, I sent the signature for each user id
295     separately to that user id's associated email address. You can import
296     the signatures by running each through `gpg --import`.
297    
298     If you want this new signature to be available to others, please upload
299     it yourself. With GnuPG this can be done using
300 weasel 93 gpg --keyserver subkeys.pgp.net --send-key {$key}
301    
302     If you have any questions, don't hesitate to ask.
303    
304     Regards,
305     {$owner}
306     EOM
307 weasel 5 };
308    
309     sub notice($) {
310     my ($line) = @_;
311     print "[NOTICE] $line\n";
312     };
313     sub info($) {
314     my ($line) = @_;
315     print "[INFO] $line\n";
316     };
317     sub debug($) {
318     my ($line) = @_;
319 enrico 25 #print "[DEBUG] $line\n";
320 weasel 5 };
321     sub trace($) {
322     my ($line) = @_;
323     #print "[trace] $line\n";
324     };
325     sub trace2($) {
326     my ($line) = @_;
327     #print "[trace2] $line\n";
328     };
329    
330     sub make_gpg_fds() {
331     my %fds = (
332     stdin => IO::Handle->new(),
333     stdout => IO::Handle->new(),
334     stderr => IO::Handle->new(),
335     status => IO::Handle->new() );
336     my $handles = GnuPG::Handles->new( %fds );
337     return ($fds{'stdin'}, $fds{'stdout'}, $fds{'stderr'}, $fds{'status'}, $handles);
338     };
339    
340     sub readwrite_gpg($$$$$%) {
341     my ($in, $inputfd, $stdoutfd, $stderrfd, $statusfd, %options) = @_;
342    
343     trace("Entering readwrite_gpg.");
344    
345 weasel 88 my ($first_line, undef) = split /\n/, $in;
346 weasel 5 debug("readwrite_gpg sends ".(defined $first_line ? $first_line : "<nothing>"));
347    
348     local $INPUT_RECORD_SEPARATOR = undef;
349     my $sout = IO::Select->new();
350     my $sin = IO::Select->new();
351     my $offset = 0;
352    
353     trace("input is $inputfd; output is $stdoutfd; err is $stderrfd; status is ".(defined $statusfd ? $statusfd : 'undef').".");
354    
355     $inputfd->blocking(0);
356     $stdoutfd->blocking(0);
357     $statusfd->blocking(0) if defined $statusfd;
358     $stderrfd->blocking(0);
359     $sout->add($stdoutfd);
360     $sout->add($stderrfd);
361     $sout->add($statusfd) if defined $statusfd;
362     $sin->add($inputfd);
363    
364     my ($stdout, $stderr, $status) = ("", "", "");
365     my $exitwhenstatusmatches = $options{'exitwhenstatusmatches'};
366 weasel 26 trace("doing stuff until we find $exitwhenstatusmatches") if defined $exitwhenstatusmatches;
367 weasel 5
368 weasel 24 my $readwrote_stuff_this_time = 0;
369     my $do_not_wait_on_select = 0;
370 weasel 5 my ($readyr, $readyw, $written);
371     while ($sout->count() > 0 || (defined($sin) && ($sin->count() > 0))) {
372     if (defined $exitwhenstatusmatches) {
373     if ($status =~ /$exitwhenstatusmatches/m) {
374     trace("readwrite_gpg found match on $exitwhenstatusmatches");
375 weasel 24 if ($readwrote_stuff_this_time) {
376     trace("read/write some more\n");
377     $do_not_wait_on_select = 1;
378     } else {
379     trace("that's it in our while loop.\n");
380     last;
381     }
382 weasel 5 };
383     };
384    
385 weasel 24 $readwrote_stuff_this_time = 0;
386 weasel 5 trace("select waiting for ".($sout->count())." fds.");
387 weasel 24 ($readyr, $readyw, undef) = IO::Select::select($sout, $sin, undef, $do_not_wait_on_select ? 0 : 1);
388 weasel 5 trace("ready: write: ".(defined $readyw ? scalar @$readyw : 0 )."; read: ".(defined $readyr ? scalar @$readyr : 0));
389     for my $wfd (@$readyw) {
390 weasel 24 $readwrote_stuff_this_time = 1;
391 weasel 5 if (length($in) != $offset) {
392     trace("writing to $wfd.");
393     $written = $wfd->syswrite($in, length($in) - $offset, $offset);
394     $offset += $written;
395     };
396     if ($offset == length($in)) {
397     trace("writing to $wfd done.");
398     unless ($options{'nocloseinput'}) {
399     close $wfd;
400     trace("$wfd closed.");
401     };
402     $sin->remove($wfd);
403     $sin = undef;
404     }
405     }
406    
407     next unless (defined(@$readyr)); # Wait some more.
408    
409     for my $rfd (@$readyr) {
410 weasel 24 $readwrote_stuff_this_time = 1;
411 weasel 5 if ($rfd->eof) {
412     trace("reading from $rfd done.");
413     $sout->remove($rfd);
414     close($rfd);
415     next;
416     }
417     trace("reading from $rfd.");
418     if ($rfd == $stdoutfd) {
419     $stdout .= <$rfd>;
420     trace2("stdout is now $stdout\n================");
421     next;
422     }
423     if (defined $statusfd && $rfd == $statusfd) {
424     $status .= <$rfd>;
425     trace2("status is now $status\n================");
426     next;
427     }
428     if ($rfd == $stderrfd) {
429     $stderr .= <$rfd>;
430     trace2("stderr is now $stderr\n================");
431     next;
432     }
433     }
434     }
435     trace("readwrite_gpg done.");
436     return ($stdout, $stderr, $status);
437     };
438    
439 myon-guest 106 sub ask($$;$$) {
440     my ($question, $default, $forceyes, $forceno) = @_;
441     return $default if $forceyes and $forceno;
442     return 1 if $forceyes;
443     return 0 if $forceno;
444 weasel 5 my $answer;
445     while (1) {
446     print $question,' ',($default ? '[Y/n]' : '[y/N]'), ' ';
447     $answer = <STDIN>;
448     chomp $answer;
449     last if ((defined $answer) && (length $answer <= 1));
450     print "grrrrrr.\n";
451     sleep 1;
452     };
453     my $result = $default;
454     $result = 1 if $answer =~ /y/i;
455     $result = 0 if $answer =~ /n/i;
456     return $result;
457     };
458    
459    
460    
461    
462    
463     my $KEYEDIT_PROMPT = '^\[GNUPG:\] GET_LINE keyedit.prompt';
464     my $KEYEDIT_DELUID_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.remove.uid.okay';
465     my $KEYEDIT_DELSIG_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.delsig';
466     my $KEYEDIT_KEYEDIT_OR_DELSIG_PROMPT = '^\[GNUPG:\] (GET_BOOL keyedit.delsig|GET_LINE keyedit.prompt)';
467     my $KEYEDIT_DELSUBKEY_PROMPT = '^\[GNUPG:\] GET_BOOL keyedit.remove.subkey';
468    
469     load_config;
470 myon-guest 102 my $USER_AGENT = "caff $VERSION - (c) 2004, 2005 Peter Palfrader et al.";
471 weasel 5
472     my $KEYSBASE = $CONFIG{'caffhome'}.'/keys';
473     my $GNUPGHOME = $CONFIG{'caffhome'}.'/gnupghome';
474    
475     -d $KEYSBASE || mkpath($KEYSBASE , 0, 0700) or die ("Cannot create $KEYSBASE: $!\n");
476     -d $GNUPGHOME || mkpath($GNUPGHOME, 0, 0700) or die ("Cannot create $GNUPGHOME: $!\n");
477    
478     my $NOW = time;
479     my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdst) = localtime($NOW);
480     my $DATE_STRING = sprintf("%04d-%02d-%02d", $year+1900, $mon+1, $mday);
481    
482    
483 weasel 95 sub version($) {
484     my ($fd) = @_;
485 myon-guest 102 print $fd "caff $VERSION - (c) 2004, 2005 Peter Palfrader et al.\n";
486 weasel 5 };
487    
488 weasel 95 sub usage($$) {
489     my ($fd, $exitcode) = @_;
490     version($fd);
491 myon-guest 106 print $fd "Usage: $PROGRAM_NAME [-eEmMRS] [-u <yourkeyid>] <keyid> [<keyid> ...]\n";
492 weasel 95 print $fd "Consult the manual page for more information.\n";
493     exit $exitcode;
494     };
495    
496 weasel 97 ######
497     # export key $keyid from $gnupghome
498     ######
499 weasel 5 sub export_key($$) {
500     my ($gnupghome, $keyid) = @_;
501    
502     my $gpg = GnuPG::Interface->new();
503     $gpg->call( $CONFIG{'gpg'} );
504     $gpg->options->hash_init(
505     'homedir' => $gnupghome,
506     'armor' => 1 );
507     $gpg->options->meta_interactive( 0 );
508     my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
509     my $pid = $gpg->export_keys(handles => $handles, command_args => [ $keyid ]);
510     my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
511     waitpid $pid, 0;
512    
513     return $stdout;
514     };
515    
516 weasel 97 ######
517     # import a key from the scalar $asciikey into a gpg homedirectory in $tempdir
518     ######
519     sub import_key($$) {
520 weasel 100 my ($gnupghome, $asciikey) = @_;
521 weasel 97
522     my $gpg = GnuPG::Interface->new();
523     $gpg->call( $CONFIG{'gpg'} );
524     $gpg->options->hash_init( 'homedir' => $gnupghome );
525     $gpg->options->meta_interactive( 0 );
526     my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
527     my $pid = $gpg->import_keys(handles => $handles);
528     my ($stdout, $stderr, $status) = readwrite_gpg($asciikey, $inputfd, $stdoutfd, $stderrfd, $statusfd);
529     waitpid $pid, 0;
530    
531     if ($status !~ /^\[GNUPG:\] IMPORT_OK/m) {
532     return undef;
533     };
534     return 1;
535     };
536    
537    
538     ######
539     # Send an email to $address. If $can_encrypt is true then the mail
540     # will be PGP/MIME encrypted to $longkeyid.
541     #
542     # $longkeyid, $uid, and @attached will be used in the email and the template.
543     ######
544 weasel 5 #send_mail($address, $can_encrypt, $longkeyid, $uid, @attached);
545     sub send_mail($$$@) {
546     my ($address, $can_encrypt, $key_id, @keys) = @_;
547    
548 weasel 93 my $template = Text::Template->new(TYPE => 'STRING', SOURCE => $CONFIG{'mail-template'})
549     or die "Error creating template: $Text::Template::ERROR";
550 weasel 5
551 weasel 93 my @uids;
552 weasel 5 for my $key (@keys) {
553 weasel 93 push @uids, $key->{'text'};
554 weasel 5 };
555 weasel 93 my $message = $template->fill_in(HASH => { key => $key_id,
556     uids => \@uids,
557     owner => $CONFIG{'owner'}})
558     or die "Error filling template in: $Text::Template::ERROR";
559 weasel 5
560     my $message_entity = MIME::Entity->build(
561     Type => "text/plain",
562     Charset => "utf-8",
563     Disposition => 'inline',
564     Data => $message);
565    
566     my @key_entities;
567     for my $key (@keys) {
568     $message_entity->attach(
569     Type => "application/pgp-keys",
570     Disposition => 'attachment',
571     Encoding => "7bit",
572     Description => "PGP Key 0x$key_id, uid ".($key->{'text'}).' ('.($key->{'serial'}).')',
573     Data => $key->{'key'},
574     Filename => "0x$key_id.".$key->{'serial'}.".asc");
575     };
576    
577     if ($can_encrypt) {
578     my $message = $message_entity->stringify();
579    
580     my $gpg = GnuPG::Interface->new();
581     $gpg->call( $CONFIG{'gpg'} );
582     $gpg->options->hash_init( 'homedir' => $GNUPGHOME,
583     'extra_args' => '--always-trust',
584     'armor' => 1 );
585     $gpg->options->meta_interactive( 0 );
586     my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
587     $gpg->options->push_recipients( $key_id );
588     $gpg->options->push_recipients( $CONFIG{'also-encrypt-to'} ) if defined $CONFIG{'also-encrypt-to'};
589     my $pid = $gpg->encrypt(handles => $handles);
590     my ($stdout, $stderr, $status) = readwrite_gpg($message, $inputfd, $stdoutfd, $stderrfd, $statusfd);
591     waitpid $pid, 0;
592     if ($stdout eq '') {
593     warn ("No data from gpg for list-key $key_id\n");
594     next;
595     };
596     $message = $stdout;
597    
598     $message_entity = MIME::Entity->build(
599     Type => 'multipart/encrypted; protocol="application/pgp-encrypted"');
600    
601     $message_entity->attach(
602     Type => "application/pgp-encrypted",
603     Disposition => 'attachment',
604     Encoding => "7bit",
605     Data => "Version: 1\n");
606    
607     $message_entity->attach(
608     Type => "application/octet-stream",
609     Filename => 'msg.asc',
610     Disposition => 'inline',
611     Encoding => "7bit",
612     Data => $message);
613     };
614    
615     $message_entity->head->add("Subject", "Your signed PGP key 0x$key_id");
616     $message_entity->head->add("To", $address);
617 weasel 28 $message_entity->head->add("From", '"'.$CONFIG{'owner'}.'" <'.$CONFIG{'email'}.'>');
618 myon-guest 101 $message_entity->head->add("Bcc", $CONFIG{'bcc'}) if defined $CONFIG{'bcc'};
619 weasel 5 $message_entity->head->add("User-Agent", $USER_AGENT);
620     $message_entity->send();
621     $message_entity->stringify();
622     };
623    
624 weasel 97 ######
625     # clean up a UID so that it can be used on the FS.
626     ######
627 weasel 15 sub sanitize_uid($) {
628     my ($uid) = @_;
629    
630 weasel 16 my $good_uid = $uid;
631     $good_uid =~ tr#/:\\#_#;
632 weasel 15 trace2("[sanitize_uid] changed UID from $uid to $good_uid.\n") if $good_uid ne $uid;
633     return $good_uid;
634     };
635    
636 weasel 100 sub delete_signatures($$$$$$) {
637     my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $longkeyid, $keyids) =@_;
638 weasel 97
639 weasel 100 my $signed_by_me = 0;
640    
641 weasel 97 my ($stdout, $stderr, $status) =
642     readwrite_gpg("delsig\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_DELSIG_PROMPT, nocloseinput => 1);
643    
644     while($status =~ /$KEYEDIT_DELSIG_PROMPT/m) {
645     # sig:?::17:EA2199412477CAF8:1058095214:::::13x:
646     my @sigline = grep { /^sig/ } (split /\n/, $stdout);
647     $stdout =~ s/\n/\\n/g;
648     notice("[sigremoval] why are there ".(scalar @sigline)." siglines in that part of the dialog!? got: $stdout") if scalar @sigline >= 2; # XXX
649     my $line = pop @sigline;
650     my $answer = "no";
651     if (defined $line) { # only if we found a sig here - we never remove revocation packets for instance
652     debug("[sigremoval] doing line $line.");
653     my (undef, undef, undef, undef, $signer, $created, undef, undef, undef) = split /:/, $line;
654     if ($signer eq $longkeyid) {
655     debug("[sigremoval] selfsig ($signer).");
656     $answer = "no";
657     } elsif (grep { $signer eq $_ } @{$keyids}) {
658     debug("[sigremoval] signed by us ($signer).");
659     $answer = "no";
660     $signed_by_me = $signed_by_me > $created ? $signed_by_me : $created;
661     } else {
662     debug("[sigremoval] not interested in that sig ($signer).");
663     $answer = "yes";
664     };
665     } else {
666     debug("[sigremoval] no sig line here, only got: ".$stdout);
667     };
668     ($stdout, $stderr, $status) =
669     readwrite_gpg($answer."\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_KEYEDIT_OR_DELSIG_PROMPT, nocloseinput => 1);
670     };
671 weasel 100
672     return $signed_by_me;
673 weasel 97 };
674    
675    
676    
677 weasel 5 my $USER;
678     my @KEYIDS;
679 weasel 95 my $params;
680 weasel 5
681 weasel 95 Getopt::Long::config('bundling');
682     if (!GetOptions (
683     '-h' => \$params->{'help'},
684     '--help' => \$params->{'help'},
685     '--version' => \$params->{'version'},
686     '-V' => \$params->{'version'},
687     '-u=s' => \$params->{'local-user'},
688     '--local-user=s' => \$params->{'local-user'},
689 weasel 109 '-e' => \$params->{'export-old'},
690     '--export-old' => \$params->{'export-old'},
691     '-E' => \$params->{'no-export-old'},
692     '--no-export-old' => \$params->{'no-export-old'},
693 weasel 95 '-m' => \$params->{'mail'},
694     '--mail' => \$params->{'mail'},
695     '-M' => \$params->{'no-mail'},
696     '--no-mail' => \$params->{'no-mail'},
697     '-R' => \$params->{'no-download'},
698     '--no-download' => \$params->{'no-download'},
699 myon-guest 106 '-S' => \$params->{'no-sign'},
700     '--no-sign' => \$params->{'no-sign'},
701 weasel 95 )) {
702     usage(\*STDERR, 1);
703     };
704     if ($params->{'help'}) {
705     usage(\*STDOUT, 0);
706     };
707     if ($params->{'version'}) {
708     version(\*STDOUT);
709     exit(0);
710     };
711     usage(\*STDERR, 1) unless scalar @ARGV >= 1;
712 myon-guest 79
713 weasel 95
714    
715     if ($params->{'local-user'}) {
716     $USER = $params->{'local-user'};
717 myon-guest 58 $USER =~ s/^0x//i;
718 weasel 110 unless ($USER =~ /^([A-Z0-9]{8}|[A-Z0-9]{16}|[A-Z0-9]{40})$/i) {
719 weasel 5 print STDERR "-u $USER is not a keyid.\n";
720 weasel 95 usage(\*STDERR, 1);
721 weasel 5 };
722     $USER = uc($USER);
723     };
724 weasel 95
725 weasel 5 for my $keyid (@ARGV) {
726 myon-guest 58 $keyid =~ s/^0x//i;
727 weasel 110 unless ($keyid =~ /^([A-Z0-9]{8}|[A-Z0-9]{16}||[A-Z0-9]{40})$/i) {
728 weasel 5 print STDERR "$keyid is not a keyid.\n";
729 weasel 95 usage(\*STDERR, 1);
730 weasel 5 };
731     push @KEYIDS, uc($keyid);
732     };
733    
734 weasel 95 $CONFIG{'no-download'} = $params->{'no-download'} if defined $params->{'no-download'};
735     $CONFIG{'no-mail'} = $params->{'no-mail'} if defined $params->{'no-mail'};
736     $CONFIG{'mail'} = $params->{'mail'} if defined $params->{'mail'};
737 myon-guest 106 $CONFIG{'no-sign'} = $params->{'no-sign'} if defined $params->{'no-sign'};
738 weasel 5
739 weasel 18
740 myon-guest 41 #################
741     # import own keys
742     #################
743     my $gpg = GnuPG::Interface->new();
744     $gpg->call( $CONFIG{'gpg'} );
745     $gpg->options->hash_init(
746     'homedir' => $GNUPGHOME,
747     'extra_args' => '--keyserver='.$CONFIG{'keyserver'} );
748     $gpg->options->meta_interactive( 0 );
749     my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
750     $gpg->options->hash_init( 'extra_args' => [ '--with-colons', '--fixed-list-mode' ] );
751     my $pid = $gpg->list_public_keys(handles => $handles, command_args => $CONFIG{'keyid'});
752     my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
753     waitpid $pid, 0;
754     if ($stdout eq '') {
755     warn ("No data from gpg for list-key\n");
756     next;
757     };
758     foreach my $keyid (@{$CONFIG{'keyid'}}) {
759     unless ($stdout =~ /^pub:(?:[^:]*:){3,3}$keyid:/m) {
760     info("Importing $keyid");
761     system "gpg --export $keyid | gpg --import --homedir $GNUPGHOME";
762     }
763     }
764    
765 weasel 5 #############################
766     # receive keys from keyserver
767     #############################
768     my @keyids_ok;
769 weasel 95 if ($CONFIG{'no-download'}) {
770 weasel 18 @keyids_ok = @KEYIDS;
771     } else {
772 myon-guest 106 info ("fetching keys, this will take a while...");
773    
774 weasel 18 my $gpg = GnuPG::Interface->new();
775     $gpg->call( $CONFIG{'gpg'} );
776     $gpg->options->hash_init(
777     'homedir' => $GNUPGHOME,
778     'extra_args' => '--keyserver='.$CONFIG{'keyserver'} );
779     $gpg->options->meta_interactive( 0 );
780     my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
781 myon-guest 105 my $pid = $gpg->recv_keys(handles => $handles, command_args => [ @KEYIDS ]);
782     my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
783     waitpid $pid, 0;
784 weasel 18
785 weasel 5 # [GNUPG:] IMPORT_OK 0 5B00C96D5D54AEE1206BAF84DE7AAF6E94C09C7F
786     # [GNUPG:] NODATA 1
787     # [GNUPG:] NODATA 1
788     # [GNUPG:] IMPORT_OK 0 25FC1614B8F87B52FF2F99B962AF4031C82E0039
789 myon-guest 106 my %local_keyids = map { $_ => 1 } @KEYIDS;
790 myon-guest 105 for my $line (split /\n/, $status) {
791 weasel 110 if ($line =~ /^\[GNUPG:\] IMPORT_OK \d+ ([0-9A-F]{40})/) {
792     my $imported_key = $1;
793     my $whole_fpr = $imported_key;
794     my $long_keyid = substr($imported_key, -16);
795     my $short_keyid = substr($imported_key, -8);
796     my $speced_key;
797     for my $spec (($whole_fpr, $long_keyid, $short_keyid)) {
798     $speced_key = $spec if $local_keyids{$spec};
799     };
800     unless ($speced_key) {
801     notice ("Imported unexpected key; got: $imported_key\n");
802 myon-guest 105 next;
803 weasel 30 };
804 weasel 110 debug ("Imported $imported_key for $speced_key");
805     delete $local_keyids{$speced_key};
806 myon-guest 105 unshift @keyids_ok, $imported_key;
807 myon-guest 107 } elsif ($line =~ /^\[GNUPG:\] (NODATA|IMPORT_RES|IMPORTED) /) {
808 myon-guest 106 } else {
809     notice ("got unknown reply from gpg: $line");
810 myon-guest 105 }
811 weasel 30 };
812 myon-guest 106 if (scalar %local_keyids) {
813     notice ("Import failed for: ". (join ' ', keys %local_keyids).".");
814     exit 1 unless ask ("Some keys could not be imported - continue anyway?", 0);
815     }
816 weasel 18 };
817 weasel 5
818 myon-guest 106 unless (@keyids_ok) {
819     notice ("No keys to sign found");
820     exit 0;
821     }
822    
823 weasel 5 ###########
824     # sign keys
825     ###########
826 kink-guest 126 if ($CONFIG{'ask-sign'} && ! $CONFIG{'no-sign'}) {
827     $CONFIG{'no-sign'} = ! ask("Continue with signing?", 1);
828     }
829    
830 weasel 18 unless ($CONFIG{'no-sign'}) {
831 weasel 35 info("Sign the following keys according to your policy, then exit gpg with 'save' after signing each key");
832 weasel 18 for my $keyid (@keyids_ok) {
833     my @command;
834     push @command, $CONFIG{'gpg-sign'};
835     push @command, '--local-user', $USER if (defined $USER);
836     push @command, "--homedir=$GNUPGHOME";
837     push @command, '--secret-keyring', $CONFIG{'secret-keyring'};
838 weasel 34 push @command, '--edit', $keyid;
839     push @command, 'sign';
840 myon-guest 119 push @command, split ' ', $CONFIG{'gpg-sign-args'} || "";
841 weasel 18 print join(' ', @command),"\n";
842     system (@command);
843     };
844 weasel 5 };
845    
846     ##################
847     # export and prune
848     ##################
849     KEYS:
850     for my $keyid (@keyids_ok) {
851     # get key listing
852     #################
853 weasel 18 my $gpg = GnuPG::Interface->new();
854 weasel 5 $gpg->call( $CONFIG{'gpg'} );
855     $gpg->options->hash_init( 'homedir' => $GNUPGHOME );
856     $gpg->options->meta_interactive( 0 );
857 weasel 18 my ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
858 weasel 5 $gpg->options->hash_init( 'extra_args' => [ '--with-colons', '--fixed-list-mode' ] );
859 weasel 18 my $pid = $gpg->list_public_keys(handles => $handles, command_args => [ $keyid ]);
860     my ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd);
861 weasel 5 waitpid $pid, 0;
862     if ($stdout eq '') {
863     warn ("No data from gpg for list-key $keyid\n");
864     next;
865     };
866     my @publine = grep { /^pub/ } (split /\n/, $stdout);
867 weasel 90 if (scalar @publine == 0) {
868     warn ("No public keys found with list-key $keyid (note that caff uses its own keyring in $GNUPGHOME).\n");
869     next;
870     };
871 weasel 88 my (undef, undef, undef, undef, $longkeyid, undef, undef, undef, undef, undef, undef, $flags) = split /:/, pop @publine;
872 weasel 89 if (scalar @publine > 0) {
873     warn ("More than one key matched $keyid. Try to specify the long keyid or fingerprint\n");
874     next;
875     };
876 weasel 5 unless (defined $longkeyid) {
877 weasel 89 warn ("Didn't find public keyid in --list-key of key $keyid.\n");
878 weasel 5 next;
879     };
880 weasel 89 unless (defined $flags) {
881     warn ("Didn't find flags in --list-key of key $keyid.\n");
882     next;
883     };
884     my $can_encrypt = $flags =~ /E/;
885 weasel 5
886     # export the key
887     ################
888     my $asciikey = export_key($GNUPGHOME, $keyid);
889     if ($asciikey eq '') {
890     warn ("No data from gpg for export $keyid\n");
891     next;
892     };
893    
894     my @UIDS;
895     my $uid_number = 0;
896     while (1) {
897     my $this_uid_text = '';
898     $uid_number++;
899 enrico 25 debug("Doing key $keyid, uid $uid_number");
900 weasel 97 my $tempdir = tempdir( "caff-$keyid-XXXXX", DIR => '/tmp/', CLEANUP => 1);
901 weasel 5
902     # import into temporary gpghome
903     ###############################
904 weasel 97 my $result = import_key($tempdir, $asciikey);
905     unless ($result) {
906 weasel 5 warn ("Could not import $keyid into temporary gnupg.\n");
907     next;
908     };
909    
910     # prune it
911     ##########
912     $gpg = GnuPG::Interface->new();
913     $gpg->call( $CONFIG{'gpg-delsig'} );
914     $gpg->options->hash_init(
915     'homedir' => $tempdir,
916     'extra_args' => [ '--with-colons', '--fixed-list-mode', '--command-fd=0', '--no-tty' ] );
917     ($inputfd, $stdoutfd, $stderrfd, $statusfd, $handles) = make_gpg_fds();
918     $pid = $gpg->wrap_call(
919     commands => [ '--edit' ],
920     command_args => [ $keyid ],
921     handles => $handles );
922    
923     debug("Starting edit session");
924     ($stdout, $stderr, $status) = readwrite_gpg('', $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
925    
926     # delete other uids
927     ###################
928     my $number_of_subkeys = 0;
929     my $i = 1;
930     my $have_one = 0;
931     my $is_uat = 0;
932     my $delete_some = 0;
933     debug("Parsing stdout output.");
934     for my $line (split /\n/, $stdout) {
935     debug("Checking line $line");
936 weasel 88 my ($type, undef, undef, undef, undef, undef, undef, undef, undef, $uidtext) = split /:/, $line;
937 weasel 5 if ($type eq 'sub') {
938     $number_of_subkeys++;
939     };
940     next unless ($type eq 'uid' || $type eq 'uat');
941     debug("line is interesting.");
942     if ($uid_number != $i) {
943     debug("mark for deletion.");
944     readwrite_gpg("$i\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
945 weasel 100 $delete_some++;
946 weasel 5 } else {
947     debug("keep it.");
948     $have_one = 1;
949 weasel 98 $this_uid_text = ($type eq 'uid') ? $uidtext : '[attribute]';
950 weasel 5 $is_uat = $type eq 'uat';
951     };
952     $i++;
953     };
954     debug("Parsing stdout output done.");
955     unless ($have_one) {
956 enrico 25 debug("Uid ".($uid_number-1)." was the last, there is no $uid_number.");
957 weasel 5 info("key $keyid done.");
958     last;
959     };
960 weasel 100
961     my $prune_some_sigs_on_uid;
962     my $prune_all_sigs_on_uid;
963 weasel 99 if ($is_uat) {
964 weasel 100 debug("handling attribute userid of key $keyid.");
965     if ($uid_number == 1) {
966     debug(" attribute userid is #1, unmarking #2 for deletion.");
967     readwrite_gpg("2\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
968     $delete_some--;
969     $prune_some_sigs_on_uid = 1;
970     $prune_all_sigs_on_uid = 2;
971     } else {
972     debug("attribute userid is not #1, unmarking #1 for deletion.");
973     readwrite_gpg("1\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
974     $delete_some--;
975     $prune_some_sigs_on_uid = 2;
976     $prune_all_sigs_on_uid = 1;
977     };
978     } else {
979     $prune_some_sigs_on_uid = 1;
980 weasel 99 };
981 weasel 100
982 weasel 5 if ($delete_some) {
983 weasel 100 debug("need to delete $delete_some uids.");
984 weasel 5 readwrite_gpg("deluid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_DELUID_PROMPT, nocloseinput => 1);
985     readwrite_gpg("yes\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
986     };
987    
988     # delete subkeys
989     ################
990     if ($number_of_subkeys > 0) {
991     for (my $i=1; $i<=$number_of_subkeys; $i++) {
992     readwrite_gpg("key $i\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
993     };
994     readwrite_gpg("delkey\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_DELSUBKEY_PROMPT, nocloseinput => 1);
995     readwrite_gpg("yes\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1);
996     };
997    
998     # delete signatures
999     ###################
1000 weasel 100 readwrite_gpg("$prune_some_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1); # mark uid for delsig
1001     my $signed_by_me = delete_signatures($inputfd, $stdoutfd, $stderrfd, $statusfd, $longkeyid, $CONFIG{'keyid'});
1002     readwrite_gpg("$prune_some_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1); # unmark uid from delsig
1003     if (defined $prune_all_sigs_on_uid) {
1004     readwrite_gpg("$prune_all_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1); # mark uid for delsig
1005     delete_signatures($inputfd, $stdoutfd, $stderrfd, $statusfd, $longkeyid, []);
1006     readwrite_gpg("$prune_all_sigs_on_uid\n", $inputfd, $stdoutfd, $stderrfd, $statusfd, exitwhenstatusmatches => $KEYEDIT_PROMPT, nocloseinput => 1); # unmark uid from delsig
1007     };
1008 weasel 5
1009 weasel 97
1010 weasel 5 readwrite_gpg("save\n", $inputfd, $stdoutfd, $stderrfd, $statusfd);
1011     waitpid $pid, 0;
1012    
1013 weasel 89 my $asciikey = export_key($tempdir, $keyid);
1014 weasel 5 if ($asciikey eq '') {
1015 weasel 89 warn ("No data from gpg for export $keyid\n");
1016 weasel 5 next;
1017     };
1018    
1019     if ($signed_by_me) {
1020     if ($NOW - $signed_by_me > $CONFIG{'export-sig-age'} ) {
1021 weasel 109 my $write = ask("Signature on $this_uid_text is old. Export?", 0, $params->{'export-old'}, $params->{'no-export-old'});
1022 weasel 5 next unless $write;
1023     };
1024     my $keydir = "$KEYSBASE/$DATE_STRING";
1025     -d $keydir || mkpath($keydir , 0, 0700) or die ("Cannot create $keydir $!\n");
1026    
1027 weasel 15 my $keyfile = "$keydir/$longkeyid.key.$uid_number.".sanitize_uid($this_uid_text).".asc";
1028 weasel 91 open (KEY, ">$keyfile") or die ("Cannot open $keyfile: $!\n");
1029 weasel 5 print KEY $asciikey;
1030     close KEY;
1031    
1032 weasel 98 push @UIDS, { text => $this_uid_text, key => $asciikey, serial => $uid_number, "is_uat" => $is_uat };
1033 weasel 5
1034     info("$longkeyid $uid_number $this_uid_text done.");
1035     } else {
1036     info("$longkeyid $uid_number $this_uid_text is not signed by me, not writing.");
1037     };
1038     };
1039    
1040     if (scalar @UIDS == 0) {
1041     info("found no signed uids for $keyid");
1042     } else {
1043 weasel 95 next if $CONFIG{'no-mail'}; # do not send mail
1044 myon-guest 79
1045     my @attached;
1046 weasel 5 for my $uid (@UIDS) {
1047 weasel 15 trace("UID: $uid->{'text'}\n");
1048 weasel 98 if ($uid->{'is_uat'}) {
1049     my $attach = ask("UID $uid->{'text'} is an attribute UID, attach it to every email sent?", 1);
1050     push @attached, $uid if $attach;
1051     } elsif ($uid->{'text'} !~ /@/) {
1052 weasel 5 my $attach = ask("UID $uid->{'text'} is no email address, attach it to every email sent?", 1);
1053 weasel 16 push @attached, $uid if $attach;
1054 weasel 5 };
1055     };
1056    
1057     notice("Key has no encryption capabilities, mail will be sent unencrypted") unless $can_encrypt;
1058     for my $uid (@UIDS) {
1059 weasel 98 if (!$uid->{'is_uat'} && ($uid->{'text'} =~ /@/)) {
1060 weasel 5 my $address = $uid->{'text'};
1061     $address =~ s/.*<(.*)>.*/$1/;
1062 myon-guest 106 if (ask("Send mail to '$address' for $uid->{'text'}?", 1, $CONFIG{'mail'})) {
1063 weasel 5 my $mail = send_mail($address, $can_encrypt, $longkeyid, $uid, @attached);
1064    
1065     my $keydir = "$KEYSBASE/$DATE_STRING";
1066 weasel 15 my $mailfile = "$keydir/$longkeyid.mail.".$uid->{'serial'}.".".sanitize_uid($uid->{'text'});
1067 weasel 91 open (KEY, ">$mailfile") or die ("Cannot open $mailfile: $!\n");
1068 weasel 5 print KEY $mail;
1069     close KEY;
1070     };
1071     };
1072     };
1073     };
1074    
1075     };

Properties

Name Value
svn:executable *
svn:keywords Id Rev

  ViewVC Help
Powered by ViewVC 1.1.5