SRS / DKIM per qmail-remote wrapper Script

Ohne SPF/SRS und DKIM ist es inzwischen fast unmöglich Mail an Google oder GMX zu senden. Support für SRS soll im nächsten sqmail Release nativ kommen aber nachdem ich für DKIM sowieso einen Wrapper brauchte habe ich gleich beides integiert. Das nachfolgende Script wird an die Stelle des Original qmail-remote binary kopiert.

Ich DKIM nutze ich nur EINEN Schlüssel für alle Domains und SRS verwendet anstatt der Originaldomain ebenfalls eine systemweite Domain weil es einfacher war das dem vorgeschalteten Spam-Filter beizubringen.

#!/usr/bin/perl 
#
# Based on dkimsign.pl written by Jason Long, jlong@messiah.edu.

use strict;
use warnings;

use Net::DNS;
use Data::Dumper;
use Mail::SRS;
use Mail::DKIM::Signer;
use Mail::DKIM::TextWrap;
use Pod::Usage;
use File::Temp;
use Log::Log4perl qw(:easy);
my $expiration;
my $identity;

my @args = @ARGV;

Log::Log4perl->easy_init( { level => $INFO,
                            file    => ">>/tmp/qmail-remote.log" } );
 
# check if sender is from a local domain
my $sender = $args[1] || '';
#my ($nn, $domain) = $sender =~ m{ \@([\w-]+\.)*([\w-]+\.[\w-]+) \z}xms;
my $local;
my $domain = '';
($local, $domain) = split /\@/, $sender;

eval{
DEBUG("Sender is $sender - Domain is $domain");
if (!is_local_domain($domain)) {
    # Not a local domain -> add srs but not for postmaster
    if ($sender =~ /^postmaster\@/) {
        DEBUG("Remote postmaster address $sender");
        $domain = "";
    } elsif(need_spf($domain)) {
        $domain = "serverpilot.net";
        my $srs = new Mail::SRS( Secret  => "ReplaceMe" );
        # sender is second argument in @args
        $args[1] = $srs->forward($sender, 'mail@srs.serverpilot.net');
        INFO("SRS $sender -> " . $args[1]);
    }
}

my $cmd;
my $fh_msg = new File::Temp();
my $fh_head; # must be defined here to avoid early deletion of tempfile
# disables DKIM for forwarded postmaster mail 
if (!$domain) {
    DEBUG("No DKIM");
    while ()
    {
        print $fh_msg $_;
    }
    $cmd = "/var/qmail/bin/qmail-remote.orig @ARGV < $fh_msg";
} else {
    my $dkim = new Mail::DKIM::Signer(
        Policy => \&signer_policy,
        Algorithm => 'rsa-sha1',
        Method => 'relaxed',
        Selector => 'serverpilot',
        KeyFile => "/var/qmail/dkim/rsa.private",
    );

    while ()
    {
        print $fh_msg $_;
        {
            chomp $_;
            s/\015?$/\015\012/s;
        }
    $dkim->PRINT($_);
    } 

    $dkim->CLOSE;

    $fh_head = new File::Temp();
    my $dkim_sig = $dkim->signature->as_string;
    $dkim_sig =~ s{\r}{}gxms;
    print $fh_head $dkim_sig . "\n";
    close $fh_head;
    INFO("DKIM added for $sender");
    DEBUG("DKIM Sig: " . $dkim_sig );

    $cmd = "cat $fh_head $fh_msg | /var/qmail/bin/qmail-remote.orig @args";
}

die "Error handling message" unless($cmd);
system($cmd) && die "DKIM Error ($cmd)";
};

if ($@) {
    die "Z$@";
}

sub signer_policy
{
    my $dkim = shift;

    use Mail::DKIM::DkSignature;

    $dkim->domain($domain);

    my $sig = Mail::DKIM::Signature->new(
            Algorithm => $dkim->algorithm,
            Method => $dkim->method,
            Headers => $dkim->headers,
            Domain => $dkim->domain,
            Selector => $dkim->selector,
            defined($expiration) ? (Expiration => time() + $expiration) : (),
            defined($identity) ? (Identity => $identity) : (),
        );
    $dkim->add_signature($sig);
    return;
}

sub is_local_domain {

    my $domain  = shift || '';
return 1;
    return unless ($domain);

    $domain =~ s/\./\\./;
    my $pattern = '\A\+'.$domain.'-';

    open(my $users, "    my $found = 0;
    while (<$users>) {
        if ($_ =~ qr/$pattern/) {
            $found = 1;
            last;
        }
    }
    close $users;
    return $found;
}


sub need_spf {

    my $domain  = shift || '';

    # fail safe
    return 1 unless ($domain);

    my $res = Net::DNS::Resolver->new(
        tcp_timeout => 2,
        udp_timeout => 2,
    );
    my $reply = $res->search( $domain, "TXT");
    if (!$reply) {
        WARN('DNS Query failed for domain ' . $domain);
 DEBUG('DNS Error: ' . $res->errorstring);
        return 1;
    }
       
    my $need_srs = 0;
    foreach my $rr ($reply->answer) {
        if ($rr->can('txtdata')) {
            if ($rr->txtdata =~ /\Av=spf/) {
 DEBUG('SPF Record found ' . $rr->txtdata);

                # if rackport is in spf record stop checks - its ok
 return 0 if ($rr->txtdata =~ /spf\.rackport\.net/);
 
 # non rackport spf record found, so srs is needed
                $need_srs = 1;
            }
        }
    }
    return $need_srs;

}

__END__

Um die SRS Rückläufer wieder an den Originalempfänger zuzustellen, wird für die SRS Domain ein .qmail-default Eintrag angelegt der alle eingehenden Mails an ein Maildrop Filter weiterleitet (| maildrop ~vpopmail/etc/mailfilter.srs-reverse ).
Das Filterscript stellt die Mails per qmail-inject zu bzw. schiebt diese in eine Sammelmailbox falls das SRS Decoding schief geht:

NEWRCPT=/usr/local/sbin/srsr.pl $EXT
if ($NEWRCPT) {
to "| /var/qmail/bin/qmail-inject $NEWRCPT"
}
to "/var/vpopmail/domains/5/srs.serverpilot.net/bounces"

Das SRS Script selber verwendet wieder Mail::SRS:

#!/usr/bin/perl

use strict;
use Mail::SRS;

my $rcpt = shift || '';
eval {
    my $srs = new Mail::SRS( Secret  => "SRSSecret" );
    print $srs->reverse($rcpt.'@srs.serverpilot.net');
} if ($rcpt =~ /\ASRS/);
# Do not handle non SRS Mail