#!/usr/bin/perl --
# $Id$
#
# sims - sims is more than sig2dot
#
# Copyright (C) 2005 Sebastian Harl
# Written by Sebastian Harl <sh@tokkee.de>
#
# sims 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.
# 
# sims 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 sims; if not, write to the Free Software Foundation, Inc., 
# 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

=head1 NAME

B<sims> - creates graphs and statistics of a GnuPG keyring

=head1 SYNOPSIS

sims [I<OPTIONS>] < I<input> > I<output>

=head1 DESCRIPTION

B<sims> (sims is more than sig2dot) aims to be a replacement for sig2dot. It 
was written from scratch by Sebastian Harl and provides a whole bunch of new
features.

B<sims> parses the output of "gpg --list-sigs" and produces graphs in 
different formats as output.

=cut

use strict;
use warnings;

use Getopt::Long;
use IPC::Run qw( run );


# globale variables
my $keys = {};

my $input = "";
my $key_id = "";

my $max_sigs = 0;

my $trust_level_colors = [ 
    '"0.111111111111111,1,1"',  # sig 0
    '"0.186111111111111,1,1"',  # sig 1
    '"0.258333333333333,1,1"',  # sig 2
    '"0.333333333333333,1,1"',  # sig 3
    '"0,1,1"'                   # revoked sig
];
my $state_colors = { 
    normal => '"#00FF00"', 
    expired => '"#FFFF00"', 
    revoked => '"#FF0000"'
};

my $current_date = "";

my $graphviz_bin = "neato";

# options
my $black_and_white = '',
my $trustlevels = '',
my $state = '',
my $date = '',
   
my $show_email = '';
my $show_all_emails = '';
my $show_keyid = '';
my $show_name = '1';

my $display_keys = '';
my $display_all = '';
my $display_selfsigs = '';
my $display_revoked = '';
my $display_revoked_sigs = '';
my $display_single = '';
my $display_unknown = '';

my $output_file = '-';
my $html_file = '';
my $output_format = 'simsdot';

my $source = '';
my $destination = '';
my $trustpath = 0;


=head1 OPTIONS

The following options are recognized:

=over 4

=item B<-b|--black-and-white=>I<0|1>

don't use colors (default: 0)

=item B<-t|--trustlevels=>I<0|1>

emphasize trustlevels using different colors for edges (default: 0)

=item B<-T|--state=>I<0|1>

emphasize state of a key (normal, expired, revoked) (default: 0)

=item B<-d|--date=>I<date>

ignores signatures, keys and revokations after date (must be in format
YYYY-MM-DD)

=back

=head2 Node Display Options

=over 4

=item B<-e|--show-email=>I<0|1>
    
print email-addresses of first UID (default: 0)

=item B<-E|--show-all-emails=>I<0|1>
    
print all email-addresses (default: 0)

=item B<-i|--show-keyid=>I<0|1>

print key-id (default: 0)

=item B<-n|--show-name=>I<0|1>

print name (if no name is found the key-id is used instead) (default: 1)

=back

=head2 Graph Display Options

=over 4

=item B<-k|--keys=>I<key>[,I<key>,...]

display only the specified keys

=item B<-a|--display-all=>I<0|1>
    
display all keys in keyring (overwrites skip-single and skip-unknown) 
(default: 0)
    
=item B<-S|--display-selfsigs=>I<0|1>
    
display self signatures (default: 0)
    
=item B<-r|--display-revoked=>I<0|1>

display revoked keys (default: 0)

=item B<-R|--display-revoked-sigs=>I<0|1>

display revoked signatures (default: 0)

=item B<-s|--display-single=>I<0|1>
    
display keys without incoming or outgoing signatures (default: 0)
    
=item B<-u|--display-unknown=>I<0|1>

display keys with unknown UID (default: 0)

=back

=head2 Output Options

=over 4

=item B<-o|--output-file=>I<filename>

output to I<filename> (default: - (stdout))

=item B<-H|--html=>I<filename>

output a HTML formatted report to I<filename>

=item B<-f|--output-format=>I<format>

see section "OUTPUT FORMATS" for supported formats (default: simsdot)

=back

=head2 Trustpath Options

=over 4

=item B<-C|--source=>I<key id>

source key to find trust pathes for

=item B<-D|--destination=>I<key id>

destination key to find trust pathes for

=back

=cut

Getopt::Long::Configure("bundling");

if (!GetOptions(
        'b|black-and-white=i' => \$black_and_white,
        't|trustlevels=i' => \$trustlevels,
        'T|state=i' => \$state,
        'd|date=s' => \$date,
        'e|show-email=i' => \$show_email,
        'E|show-all-emails=i' => \$show_all_emails,
        'i|show-keyid=i' => \$show_keyid,
        'n|show-name=i' => \$show_name,
        'k|keys=s' => \$display_keys,
        'a|display-all=i' => \$display_all,
        'S|display-selfsigs=i' => \$display_selfsigs,
        'r|display-revoked=i' => \$display_revoked,
        'R|display-revoked-sigs=i' => \$display_revoked_sigs,
        's|display-single=i' => \$display_single,
        'u|display-unknown=i' => \$display_unknown,
        'o|output-file=s' => \$output_file,
        'f|output-format=s' => \$output_format,
        'C|source=s' => \$source,
        'D|destination=s' => \$destination,
        'H|html=s' => \$html_file,
)) {
    exit 1;
}


if ($output_format !~ /^(canon|(sims|x)?dot|cmap|dia|fig|gd2?|gif|hpgl|is?map|cmapx|jpe?g|mif|mp|pcl|pic|plain(-ext)?|png|ps2?|svgz?|vrml|vtx|wbmp)$/) {
    print STDERR "Unknown output-format \"$output_format\"!\n";
    exit 1;
}

$source = uc $source;
$destination = uc $destination;

if ($source ne "") {
    if ($source =~ /^[0-9A-F]{8}$/) {
        $trustpath += 1;
    } else {
        print STDERR "Source key is not valid!\n";
        exit 1;
    }
}

if ($destination ne "") {
    if ($destination =~ /^[0-9A-F]{8}$/) {
        $trustpath += 2;
    } else {
        print STDERR "Destination key is not valid!\n";
        exit 1;
    }
}


$current_date = `date '+\%F'`;


=begin comment

We'll save the following information for each key (if available):

$keys = {
          '<key-id>' => {
                          'name' => "<name>",
                          'email' => [ 
                                       "<email addresse>",
                                       ...
                                     ],
                          'length' => "<length>",
                          'type' => "<type>",
                          'created' => "<creation date>",
                          'expires' => "<expiration date>",
                          'revoked' => "<revokation date>",
                          'signatures' => {
                                            '<signing key-id>' => "<trust>",
                                            ...
                                          },
                          'revoked_sigs' => {
                                              '<signing key-id>' => -1,
                                              ...
                                            },
                          'signed_keys' => { 
                                             '<signed key-id>' => "<trust>",
                                             ...
                                           },
                          'subs' => {
                                      '<key-id>' => {
                                                      'length' => "<length>",
                                                      'type' => "<type>",
                                                      'created' => "<date>",
                                                      'expires' => "<date>",
                                                      'revoked' => "<date>"
                                                    }
                                    },
                          'display' => [0|1],
                          'distance_from_source' => "<distance>",
                          'predecessor' => "<parent in tree to source>",
                          'distance_to_destination' => "<distance>",
                          'successor' => "child in tree to destination>"
                        }
        },
          ...
}

=end comment

=cut


if (! -p STDIN) {
	close(STDIN);
	open(STDIN, "gpg --list-sigs |");
}

while ($input = <STDIN>) {
    chomp $input;

    if ($input =~ /^pub\s+(\d{4})(\w)\/([0-9A-F]{8}) ([0-9\-]{10})(.*)?$/) {
        my $key_length = $1;
        my $key_type = $2;
        $key_id = $3;
        my $key_created = $4;

        if ($date eq "" || $date ge $key_created) {
            $keys->{$key_id}->{'length'} = $key_length;
            $keys->{$key_id}->{'type'} = $key_type;
            $keys->{$key_id}->{'created'} = $key_created;

            if (defined $5 && $5 ne "") {
                # key is either expired or revoked
                $5 =~ /\[(\w+): ([0-9\-]{10})\]/;

                if ($1 eq "expires" || $1 eq "revoked") {
                    if ($date eq "" || $date ge $2) {
                        $keys->{$key_id}->{$1} = $2;
                    }
                } else {
                    print STDERR "Found unrecognized data at end of pub-line\n";
                }
            }

            while (($input = <STDIN>) !~ /^uid/) {
                print STDERR "Still looking for first UID for key $key_id\n";
            }
        } else {
            $key_id = "";
        }
    } elsif ($key_id eq "") {
        print STDERR "Looking for next pub-line.\n";
        next;
    }
    
    if ($input =~ /^uid\s+([^\<]*?)( \<(.*?)\>)?$/) {
        if (!defined $keys->{$key_id}->{'name'} 
                || $keys->{$key_id}->{'name'} eq "") {
            $keys->{$key_id}->{'name'} = $1;
        }
        
        if (defined $3 && $3 ne "") {
            push @{$keys->{$key_id}->{'email'}}, $3;
        }
    }

    if ($input =~ /^sig (.).{8}([0-9A-F]{8}) ([0-9\-]{10})/) {
        my $trust_level = $1;
        my $signing_key = $2;
        my $signing_date = $3;
        
        if ($date eq "" || $date ge $signing_date) {
            if ($trust_level eq ' ') {
                $trust_level = 0;
            }

            if ($trust_level !~ /\d/) {
                print STDERR "Unknown trustlevel found!\n";
                $trust_level = 0;
            }
            
            $keys->{$key_id}->{'signatures'}->{$signing_key} = $trust_level;
            $keys->{$signing_key}->{'signed_keys'}->{$key_id} = $trust_level;
        }
    }

    if ($input =~ /^rev.{10}([0-9A-F]{8}) ([0-9\-]{10})/) {
        my $revoking_key = $1;
        my $revoking_date = $2;
        
        if ($date eq "" || $date ge $revoking_date) {
            $keys->{$key_id}->{'revoked_sigs'}->{$revoking_key} = 4;
            $keys->{$revoking_key}->{'signed_keys'}->{$key_id} = 4;
        } else {
            $keys->{$key_id}->{'signatures'}->{$revoking_key} = 0;
            $keys->{$revoking_key}->{'signed_keys'}->{$key_id} = 0;
        }
    }

    if ($input =~ /^sub\s+(\d{4})(\w)\/([0-9A-F]{8}) ([0-9\-]{10})(.*)?$/) {
        my $subkey_length = $1;
        my $subkey_type = $2;
        my $subkey_id = $3;
        my $subkey_created = $4;

        if ($date eq "" || $date ge $subkey_created) {
            $keys->{$key_id}->{'subs'}->{$subkey_id}->{'length'} = 
                $subkey_length;
            $keys->{$key_id}->{'subs'}->{$subkey_id}->{'type'} = $subkey_type;
            $keys->{$key_id}->{'subs'}->{$subkey_id}->{'created'} = 
                $subkey_created;

            if (defined $5 && $5 ne "") {
                # sub key is either expired or revoked
                $5 =~ /\[(\w+): ([0-9\-]{10})\]/;
                
                if ($1 eq "expires" || $1 eq "revoked") {
                    if ($date eq "" || $date ge $2) {
                        $keys->{$key_id}->{'subs'}->{$subkey_id}->{$1} = $2;
                    }
                } else {
                    print STDERR "Found unrecognized data at end of sub-line\n";
                }
            }
        }
    }
}

close(STDIN);

if ($source ne "" && !defined($keys->{$source})) {
    print STDERR "Did not find source key id $source!\n";
    exit 1;
}

if ($destination ne "" && !defined($keys->{$destination})) {
    print STDERR "Did not find destination key id $destination!\n";
    exit 1;
}

if (!$display_revoked && $display_keys eq "") {
    remove_revoked_keys();
}

if (!$display_selfsigs) {
    remove_selfsigs();
}

if ($trustpath == 1) {
    shortest_path($source, "signed_keys");
} elsif ($trustpath == 2) {
    shortest_path($destination, "signatures");
} elsif ($trustpath == 3) {
    shortest_path($source, "signed_keys");
    shortest_path($destination, "signatures");

    foreach my $key (keys %{$keys}) {
        if ((!(defined($keys->{$key}->{'successor'})
                    && $keys->{$key}->{'successor'} ne "NULL"
                    && defined($keys->{$key}->{'predecessor'})
                    && $keys->{$key}->{'predecessor'} ne "NULL")
                && $key ne $source && $key ne $destination)
                || $keys->{$key}->{'predecessor'} eq
                        $keys->{$key}->{'successor'}) {
            $keys->{$key}->{'display'} = 0;
        }
    }
}

if ($display_keys ne "") {
    foreach my $key (split ',', $display_keys) {
        $key = uc $key;
        if (defined($keys->{$key})) {
            $keys->{$key}->{'display'} = 1;
        } else {
            print STDERR "Requested key \"$key\" not found... Skipping.\n";
        }
    }

    foreach my $key (keys %{$keys}) {
        if (!defined($keys->{$key}->{'display'})) {
            $keys->{$key}->{'display'} = 0;
            print STDERR "Skipping key \"$key\" as requested.\n";
        }
    }
}

# get max_sigs
foreach my $key (keys %{$keys}) {
    my $tmp_sigs = 0;
    
    foreach (keys %{$keys->{$key}->{'signatures'}}) {
        if (defined $keys->{$_}->{'name'}) {
            ++$tmp_sigs;
        }
    }

    if ($tmp_sigs > $max_sigs) {
        $max_sigs = $tmp_sigs;
    }
}

=head1 OUTPUT FORMATS

Basically B<sims> supports all output formats that graphviz does.

  * canon
  * dot
  * xdot
  * cmap
  * dia
  * fig
  * gd
  * gd2
  * gif
  * hpgl
  * imap
  * cmapx
  * ismap
  * jpg
  * jpeg
  * mif
  * mp
  * pcl
  * pic
  * plain
  * plain-ext
  * png
  * ps
  * ps2
  * svg
  * svgz
  * vrml
  * vtx
  * wbmp

See http://www.graphviz.org/cvs/doc/info/output.html for more details.

Additionally B<sims> supports "simsdot", which is a .dot file produced by
B<sims>.

=cut

{
    my $outfile = \*STDOUT;
    
    unless ($output_file eq "-") {
        open(OUT, ">", "$output_file") 
            or die "Could not open $output_file: $!\n";
        $outfile = \*OUT;
    }

    if ($output_format eq "simsdot") {
        run \&write_dotfile, \undef, $outfile;
    } else {
        run \&write_dotfile, '|', 
            [ "$graphviz_bin", "-T$output_format" ], $outfile;
    }

    unless ($output_file eq "-") {
        close(OUT);
    }

    if ($html_file) {
        write_html();
    }
}

exit 0;


sub remove_selfsigs
{
    foreach my $key (sort keys %{$keys}) {
        # remove selfsigs from signatures
        if (defined $keys->{$key}->{'signatures'}->{$key}) {
            delete $keys->{$key}->{'signatures'}->{$key};
        }

        if (!%{$keys->{$key}->{'signatures'}}) {
            delete $keys->{$key}->{'signatures'};
        }

        # remove selfsigs from signed_keys
        if (defined $keys->{$key}->{'signed_keys'}->{$key}) {
            delete $keys->{$key}->{'signed_keys'}->{$key};
        }

        if (!%{$keys->{$key}->{'signed_keys'}}) {
            delete $keys->{$key}->{'signed_keys'};
        }
    }
    return 0;
}

sub remove_revoked_keys
{
    foreach my $key (sort keys %{$keys}) {
        if (defined $keys->{$key}->{'revoked'}) {
            undef %{$keys->{$key}};
        }
    }
    return 0;
}

sub shortest_path 
{
    my $root = shift;
    my $direction = shift;
    
    my %cache = ();
    my $dis_text = "";
    my $pre_suc = "";

    if ($direction eq "signed_keys") {
        $dis_text = "distance_from_source";
        $pre_suc = "predecessor";
    } elsif ($direction eq "signatures") {
        $dis_text = "distance_to_destination";
        $pre_suc = "successor";
    } else {
        print STDERR "shortest_path() called with wrong arguments!\n";
        exit 1;
    }
    
    # initialize
    foreach my $key (keys %{$keys}) {
        if ($key eq $root) {
            $keys->{$key}->{"$dis_text"} = 0;
            $keys->{$key}->{"$pre_suc"} = "NULL";
        } else {
            # we'll use -1 as "infinity"
            $keys->{$key}->{"$dis_text"} = -1;
            $keys->{$key}->{"$pre_suc"} = "NULL";
        }
    }
    $cache{$root} = 1;
    $keys->{$root}->{'display'} = 1;

    while (%cache) {
        my $tmp = (keys(%cache))[0];
        delete $cache{$tmp};

        foreach my $key (keys %{$keys->{$tmp}->{"$direction"}}) {
            if ($keys->{$key}->{"$dis_text"} == -1) {
                $keys->{$key}->{"$dis_text"} = 1
                    + $keys->{$tmp}->{"$dis_text"};
                $keys->{$key}->{"$pre_suc"} = $tmp;
                $keys->{$key}->{'display'} = 1;

                $cache{$key} = 1;
            }
        }
    }

    foreach my $key (keys %{$keys}) {
        if (!defined($keys->{$key}->{'display'})) {
            $keys->{$key}->{'display'} = 0;
        }

        if (!defined($keys->{$key}->{'name'}) 
                && !($display_single || $display_all)) {
            $keys->{$key}->{'display'} = 0;
        }
    }
    return 0;
}

sub display_node
{
    my $key = shift;

    if (defined $keys->{$key}->{'display'}) {
        return $keys->{$key}->{'display'};
    }

    if ($display_all) {
        $keys->{$key}->{'display'} = 1;
        return 1;
    }

    if (!$display_unknown && !defined $keys->{$key}->{'name'}) {
        $keys->{$key}->{'display'} = 0;
        print STDERR "Unknown key... ";
        return 0;
    }

    if (!$display_single) {
        if (!defined $keys->{$key}->{'signatures'}
                && !defined $keys->{$key}->{'signed_keys'}) {
            $keys->{$key}->{'display'} = 0;
            print STDERR "No incoming/outgoing signatures... ";
            return 0;
        } else {
            foreach (keys %{$keys->{$key}->{'signatures'}}) {
                if (defined $keys->{$_}->{'name'}) {
                    $keys->{$key}->{'display'} = 1;
                    return 1;
                }
            }

            foreach (keys %{$keys->{$key}->{'signed_keys'}}) {
                if (defined $keys->{$_}->{'name'}) {
                    $keys->{$key}->{'display'} = 1;
                    return 1;
                }
            }

            $keys->{$key}->{'display'} = 0;
            print STDERR "No incoming/outgoing signatures to known keys... ";
            return 0;
        }
    }
    $keys->{$key}->{'display'} = 1;
    return 1;
}

sub calculate_fillcolor
{
    my $key = shift;

    my $sigs = 0;
    my $signed = 0;

    # minimum saturation
    my $min_value = 0.2;

    my ($hue, $saturation, $value) = (0.0, 0.0, 0.0);

    foreach (keys %{$keys->{$key}->{'signatures'}}) {
        if (defined $keys->{$_}->{'name'}) {
            ++$sigs;
        }
    }

    foreach (keys %{$keys->{$key}->{'signed_keys'}}) {
        if (defined $keys->{$_}->{'name'}) {
            ++$signed;
        }
    }

    if ($sigs == 0 && $signed == 0) {
        $hue = 240 / 360;
    } elsif ($signed >= $sigs) {
        $hue = $sigs / $signed * 60;
        $hue /= 360;
    } elsif ($sigs > $signed) {
        $hue = (1 - $signed / $sigs) * 60;
        $hue += 60;
        $hue /= 360;
    }

    $saturation = $sigs / $max_sigs;
    $saturation = $saturation * (1 - $min_value) + $min_value;
    $value = 1;
    return "$hue,$saturation,$value";
}

sub print_dot_header
{
    # print a directed graph
    print "digraph \"keyring\" {\n";
    # nodes should not overlap, scale image instead
    print "overlap=scale\n";
    # use splines
    print "splines=true\n";
    return 0;
}

sub print_dot_footer
{
    print "}\n";
    return 0;
}

sub print_node
{
    my $key = shift;
    my $color = "";
    my $label = "";

    if (!$black_and_white) {
        $color = "fillcolor=\"";
        $color .= calculate_fillcolor($key);
        $color .= "\",";
        if ($state) {
            my $key_state = "";

            if (defined $keys->{$key}->{'revoked'}) {
                $key_state = 'revoked';
            } elsif (defined $keys->{$key}->{'expires'}
                    && $current_date ge $keys->{$key}->{'expires'}) {
                $key_state = 'expired';
            } else {
                $key_state = 'normal';
            }
            
            $color .= "color=$state_colors->{$key_state},";
        }
    }
    
    if (defined $keys->{$key}->{'name'} && $show_name) {
        $label = $keys->{$key}->{'name'};
    }

    if ((!defined $keys->{$key}->{'name'} && $show_name) 
            || $show_keyid) {
        $label .= '\n' if ($label ne "");
        $label .= "0x$key";
    }

    if ($show_all_emails) {
        foreach (@{$keys->{$key}->{'email'}}) {
        $label .= '\n' if ($label ne "");
            $label .= "$_";
        }
    } elsif ($show_email) {
        $label .= '\n' if ($label ne "");
        $label .= "$keys->{$key}->{'email'}->[0]";
    }
    
    print "node [style=filled]\n";
    print "\"$key\" [${color}label=\"$label\"]\n";
    return 0;
}

sub print_edge
{
    my $from = shift;
    my $to = shift;
    my $type = shift;
    
    if (!$black_and_white && $trustlevels) {
        my $trustlvl = 0;
        
        if ($type eq 'sig') {
            $trustlvl = $keys->{$to}->{'signatures'}->{$from};
        } elsif ($type eq 'rev') {
            $trustlvl = $keys->{$to}->{'revoked_sigs'}->{$from};
        } else {
            print STDERR "Unknown type of signature to print edge for!\n";
            return 1;
        }
        print "edge [color=$trust_level_colors->[$trustlvl]]\n";
    }
    
    print "\"$from\" -> \"$to\"\n";
    return 0;
}

# chooses with subroutine to call depending on command line options
sub write_dotfile
{
    if ($trustpath > 0) {
        write_dotfile_trustpath();
    } else {
        write_dotfile_default();
    }
}

sub write_dotfile_trustpath
{
    print_dot_header();
    
    foreach my $key (keys %{$keys}) {
        if (display_node($key)) {
            print_node($key);
        } else {
            print STDERR "skipping \"$key\".\n";
        }
    }

    foreach my $key (keys %{$keys}) {
        if (display_node($key)) {
            if (defined $keys->{$key}->{'successor'} 
                    && $keys->{$key}->{'successor'} ne "NULL") {
                print_edge($key, $keys->{$key}->{'successor'}, 'sig');
            }

            if (defined $keys->{$key}->{'predecessor'}
                    && $keys->{$key}->{'predecessor'} ne "NULL") {
                print_edge($keys->{$key}->{'predecessor'}, $key, 'sig');
            }
        }
    }

    print_dot_footer();
    return 0;
}

sub write_dotfile_default 
{
    print_dot_header();
    
    foreach my $key (sort keys %{$keys}) {
        if (display_node($key)) {
            print_node($key);
        } else {
            print STDERR "skipping \"$key\".\n";
        }
    }
    
    foreach my $key (sort keys %{$keys}) {
        if (display_node($key) && %{$keys->{$key}->{'signatures'}}) {
            foreach my $sig (sort keys %{$keys->{$key}->{'signatures'}}) {
                if (display_node($sig)) {
                    print_edge($sig, $key, 'sig');
                }
            }

            if ($display_revoked_sigs) {
                foreach my $sig (sort keys %{$keys->{$key}->{'revoked_sigs'}}) {
                    if (display_node($sig)) {
                        print_edge($sig, $key, 'rev');
                    }
                }
            }
        }
    }
    print_dot_footer();
    return 0;
}

sub write_html
{
    if (open(HTML, ">", $html_file)) {
        print HTML "<?xml version='1.0' encoding='iso-8859-1'?>\n" 
            . "<!DOCTYPE html PUBLIC "
            . "\"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n"
            . "\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitinoal.dtd\">\n"
            . "<html xmlns=\"http://www.w3.org/1999/xhtml\">\n"
            . "<head><title>Keyring statistic</title></head>\n"
            . "<body>\n";

        print HTML "<table>\n"
            . "<tr>\n"
            . "\t<td>KeyID</td>\n"
            . "\t<td>Name</td>\n"
            . "\t<td>eMail</td>\n"
            . "\t<td colspan=\"2\">Signatures</td>\n"
            . "</tr>\n";

        foreach my $key (sort keys %{$keys}) {
            if (display_node($key)) {
                my $no_sigs = 0;

                foreach (keys %{$keys->{$key}->{'signatures'}}) {
                    if (defined $keys->{$_}->{'name'}) {
                        ++$no_sigs;
                    }
                }
                
                print HTML "<tr>\n"
                    . "\t<td>$key</td>\n"
                    . "\t<td>$keys->{$key}->{'name'}</td>\n"
                    . "\t<td>$keys->{$key}->{'email'}->[0]</td>\n"
                    . "\t<td>$no_sigs</td>\n"
                    . "\t<td>";

                for (my $i = 0; $i < $no_sigs; ++$i) {
                    print HTML "*";
                }

                print HTML "</td>\n"
                    . "</tr>\n";
            }
        }

        print HTML "</tables>\n";

        print HTML "</body>\n"
            . "</html>\n";
        close(HTML);
    } else {
        print STDERR "Could not open $html_file for writing: $!\n";
        exit 1;
    }
}

=head1 EXAMPLES

gpg --list-sigs | sims > keyring.dot

gpg --list-sigs | sims -o keyring.dot

gpg --list-sigs | sims -f ps | convert - keyring.png

In my opinion, the last example produces nicer output than C<gpg --list-sigs |
sims -f png E<gt> keyring.png>.

=head1 SEE ALSO

neato(1)

=head1 AUTHOR

B<sims> is written by Sebastian Harl <sh@tokkee.de>. You can get the most up 
to date version from http://www.tokkee.org/.

=cut

