Steffen Ullrich

NAME Mail::DKIM::Iterator

Iterativ validation of DKIM records or DKIM signing of mails.

SYNOPSIS

    # ---- Verify all DKIM signature headers found within a mail --------------

    my $mailfile = $ARGV[0];

    use Mail::DKIM::Iterator;
    use Net::DNS;

    my %dnscache;
    my $res = Net::DNS::Resolver->new;

    # Create a new Mail::DKIM::Iterator object.
    # Feed parts from the mail and results from DNS lookups into the object
    # until we have the final result.

    open( my $fh,'<',$mailfile) or die $!;
    my $dkim = Mail::DKIM::Iterator->new(dns => \%dnscache);
    my $rv;
    my @todo = \'';
    while (@todo) {
        my $todo = shift(@todo);
        if (ref($todo)) {
            # need more data from mail
            if (read($fh,$buf,8192)) {
                ($rv,@todo) = $dkim->next($buf);
            } else {
                ($rv,@todo) = $dkim->next('');
            }
        } else {
            # need a DNS lookup
            if (my $q = $res->query($todo,'TXT')) {
                # successful lookup
                ($rv,@todo) = $dkim->next({
                    $todo => [
                        map { $_->type eq 'TXT' ? ($_->txtdata) : () }
                        $q->answer
                    ]
                });
            } else {
                # failed lookup
                ($rv,@todo) = $dkim->next({ $todo => undef });
            }
        }
    }

    # This final result consists of a VerifyRecord for each DKIM signature
    # in the header, which provides access to the status. Status is one of
    # of DKIM_SUCCESS, DKIM_PERMFAIL, DKIM_TEMPFAIL, DKIM_SOFTFAIL or
    # DKIM_INVALID_HDR. In case of error $record->error contains a string
    # representation of the error.

    for(@$rv) {
        my $status = $_->status;
        my $name = $_->domain;
        if (!defined $status) {
            print STDERR "$mailfile: $name UNKNOWN\n";
        } elsif ($status == DKIM_SUCCESS) {
            # fully validated
            print STDERR "$mailfile: $name OK\n";
        } elsif ($status == DKIM_PERMFAIL) {
            # hard error
            print STDERR "$mailfile: $name FAIL ".$_->error."\n";
        } else {
            # soft-fail, temp-fail, invalid-header
            print STDERR "$mailfile: $name $status ".$_->error."\n";
        }
    }


    # ---- Create signature for a mail ----------------------------------------

    my $mailfile = $ARGV[0];

    use Mail::DKIM::Iterator;

    my $dkim = Mail::DKIM::Iterator->new(sign => {
        c => 'relaxed/relaxed',
        a => 'rsa-sha1',
        d => 'example.com',
        s => 'foobar',
        ':key' => ... PEM string for private key or Crypt::OpenSSL::RSA object
    });

    open(my $fh,'<',$mailfile) or die $!;
    my $rv;
    my @todo = \'';
    while (@todo) {
        my $todo = shift @todo;
        die "DNS lookups should not be needed here" if !ref($todo);
        # need more data from mail
        if (read($fh,$buf,8192)) {
            ($rv,@todo) = $dkim->next($buf);
        } else {
            ($rv,@todo) = $dkim->next('');
        }
    }
    for(@$rv) {
        my $status = $_->status;
        my $name = $_->domain;
        if (!defined $status) {
            print STDERR "$mailfile: $name UNKNOWN\n";
        } elsif (status != DKIM_SUCCESS) {
            print STDERR "$mailfile: $name $status - ".$_->error."\n";
        } else {
            # show signature
            print $_->signature;
        }
    }

DESCRIPTION

With this module one can validate DKIM Signatures in mails and also create DKIM signatures for mails.

The main difference to Mail::DKIM is that the validation can be done iterative, that is the mail can be streamed into the object and if DNS lookups are necessary their results can be added to the DKIM object asynchronously. There are no blocking operation or waiting for input, everything is directly driven by the user/application feeding the DKIM object with data.

This module implements only DKIM according to RFC 6376. It does not support the historic DomainKeys standard (RFC 4870).

The following methods are relevant. For details of their use see the examples in the SYNOPSIS.

new(%args) -> $dkim

This will create a new object. The following arguments are supported

dns => \%hash

A hash with the DNS name as key and the DKIM record for this name as value. This can be used as a common DNS cache shared over multiple instances of the class. If none is given only a local hash will be created inside the object.

sign => \@dkim_sig

List of DKIM signatures which should be used for signing the mail (usually only a single one). These can be given as string or hash (see parse_signature below). These DKIM signatures are only used to collect the relevant information from the header and body of the mail, the actual signing is done in the SignRecord object (see below).

sign_and_verify => 0|1

Usually it either signs the mail (if sign is given) or validates signatures inside the mail. When this option is true it will validate existing signatures additionally to creating new signatures if sign is used.

filter => $sub

A filter function which gets applied to all signatures. Signatures not matching the filter will be removed. The function is called as $sub->(\%sig,$header) where %sig is the signature hash and $header the header of the mail (which can be considered the same over all calls of $sub). Typically this is used to exclude any signatures which don't match the domain of the From header, i.e. check against $sig{d}.

$dkim->next([ $mailchunk | \%dns ]*) -> ($rv,@todo)

This is used to add new information to the DKIM object. These information can be a new chunk from the mail (string), the signal for end of mail input (empty string '') or a mapping between the name and the record for a DKIM key.

If there are still things todo to get the final result @todo will get the necessary instructions, either as a string containing a DNS name which should be used to lookup a DKIM key record, or a reference to a scalar \'' to signal that more data from the mail are needed. $rv might already contain preliminary results.

Once the final result could be computed @todo will be empty and $rv will contain the results as a list. Each of the objects in the list is either a VerifyRecord (in case of DKIM verification) or a SignRecord (in case of DKIM signing).

Both VerifyRecord and SignRecord have the following methods:

status - undef if no DKIM result is yet known for the record (preliminary result). Otherwise any of DKIM_SUCCESS, DKIM_INVALID_HDR, DKIM_TEMPFAIL, DKIM_SOFTFAIL, DKIM_PERMFAIL.
error - an error description in case the status shows an error, i.e. with all status values except undef and DKIM_SUCCESS.
sig - the DKIM signature as hash
domain - the domain value from the DKIM signature
dnsname - the dnsname value, i.e. based on domain and selector

A SignRecord has additionally the following methods:

signature - the DKIM-Signature value, only if DKIM_SUCCESS
filter($sub)

Sets a filter function and applies it immediately if the mail header is already known. See filter argument of new for more details.

Apart from these methods the following utility functions are provided

parse_signature($dkim_sig,\$error) -> \%dkim_sig|undef

This parses the value from the DKIM-Signature field of mail and returns it as a hash. On any problems while interpreting the value undef will be returned and $error will be filled with a string representation of the problem.

parse_dkimkey($dkim_key,\$error) -> \%dkim_key|undef

This parses a DKIM key which is usually found as a TXT record in DNS and returns it as a hash. On any problems while interpreting the value undef will be returned and $error will be filled with a string representation of the problem.

parse_taglist($string,\$error) -> \%hash

This parses a tag list like found in DKIM record, DKIM signatures or DMARC records and returns it as a hash.

sign($dkim_sig,$priv_key,$hdr,\$error) -> $signed_dkim_sig

This takes a DKIM signature $dkim_sig (as string or hash), an RSA private key $priv_key (as PEM string or Crypt::OpenSSL::RSA object) and the header of the mail and computes the signature. The result $signed_dkim_sig will be a signature string which can be put on top of the mail.

On errors $error will be set and undef will returned.

SEE ALSO

Mail::DKIM

Mail::SPF::Iterator

AUTHOR

Steffen Ullrich <sullr[at]cpan[dot]org>

COPYRIGHT

Steffen Ullrich, 2015..2016

This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself.