Download | Plain Text | Line Numbers


#!/usr/bin/perl
#
# Copyright (C) 2007 Manuel Mausz (manuel@mausz.at)
#
# This program 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.
#
# This program 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 this program; if not, write to the Free Software Foundation,
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 
use strict;
use warnings;
our $VERSION = '0.1';
 
use Mail::DKIM 0.29;
use Mail::DKIM::Signer;
 
# enable support for "pretty" signatures, if available
eval 'require Mail::DKIM::TextWrap';
 
=head
config file structure
 - missing settings will be merged from global
 - domain-entry will also match its subdomains
 - specify "none" in types-tag to omit signing
 
<dkimsign>
  <global algorithm="rsa-sha256" domain="/var/qmail/control/me" keyfile="/var/qmail/control/dkim/global.key" method="simple" selector="beta">
    <types>dkim</types>
  </global>
  <example.com selector="beta2">
    <types>dkim</types>
    <types>domainkey</types>
  </example.com>
</dkimsign>
=cut
 
my $configfile = undef;
$configfile    = '/var/qmail/control/dkim/signconf.xml';
my $debugfile  = undef;
#$debugfile    = '/tmp/dkim.debug';
my $qremote    = '/var/qmail/bin/qmail-remote.orig';
my $binary     = 0;
our $config;
$config->{'global'} = {
  types     => 'dkim',
  keyfile   => '/var/qmail/control/dkim/global.key',
  algorithm => 'rsa-sha256',
  method    => 'simple',
  selector  => 'beta',
  # either string or file (first line of file will be used)
  domain    => '/var/qmail/control/me'
};
 
#-------------------------------------------------------------------------------
 
# read config file. safely
if (defined($configfile) && -r $configfile)
{
  eval 'use XML::Simple';
  if (!$@)
  {
    my $xmlconf;
    eval { $xmlconf = XMLin($configfile, ForceArray => 0); };
    qexit_deferral('Unable to read config file: ', $@)
      if ($@);
    ConfigMerge::merge($config, $xmlconf);
  }
}
 
# open debug file
my $debugfh = undef;
if (defined($debugfile))
{
  open($debugfh, '>', $debugfile)
    or qexit_deferral('Unable to open ', $debugfile, ' to writing: ', $!);
}
 
# generate signatures
my $dkim;
my $mailbuf = '';
eval
{
  my $conf = $config->{'global'};
  $dkim =  Mail::DKIM::Signer->new(
    Policy => 'MySignerPolicy',
    Debug_Canonicalization => $debugfh
  );
 
  if ($binary)
  {
    binmode STDIN;
  }
 
  while (<STDIN>)
  {
    $mailbuf .= $_;
    unless ($binary)
    {
      chomp $_;
      s/\015?$/\015\012/s;
    }
    $dkim->PRINT($_);
  }
  $dkim->CLOSE();
};
qexit_deferral('Error while signing: ', $@)
  if ($@);
 
# close debug file
close($debugfh)
  if (defined($debugfh));
 
# execute qmail-remote
unshift(@ARGV, $qremote);
open(QR, '|-') || exec { $ARGV[0] } @ARGV
  or qexit_deferral('Unable to run qmail-remote: ', $!);
foreach my $dkim_signature ($dkim->signatures)
{
  my $sig = $dkim_signature->as_string;
  $sig =~ s/\015\012\t/\012\t/g;
  print QR $sig."\012";
}
print QR $mailbuf;
close(QR);
 
#-------------------------------------------------------------------------------
 
sub qexit
{
  print @_, "\0";
  exit(0);
}
 
sub qexit_deferral
{
  return qexit('Z', @_);
}
 
sub qexit_failure
{
  return qexit('D', @_);
}
 
sub qexit_success
{
  return qexit('K', @_);
}
 
#-------------------------------------------------------------------------------
 
package ConfigMerge;
our $VERSION = '0.1';
 
# merge config hashes. arrays and scalars will be copied.
sub merge
{
  my ($left, $right) = @_;
  foreach my $rkey (keys(%$right))
  {
    my $rtype = ref($right->{$rkey}) eq 'HASH' ? 'HASH'
              : ref($right->{$rkey}) eq 'ARRAY' ? 'ARRAY'
              : 'SCALAR';
    if (!defined($left->{$rkey}) || $rtype ne 'HASH')
    {
      $left->{$rkey} = $right->{$rkey};
    }
    else
    {
      merge($left->{$rkey}, $right->{$rkey});
    }
  }
  return;
}
 
#-------------------------------------------------------------------------------
 
package MySignerPolicy;
use Mail::DKIM::SignerPolicy;
use base 'Mail::DKIM::SignerPolicy';
use Mail::DKIM::Signature;
use Mail::DKIM::DkSignature;
use Carp;
our $VERSION = '0.1';
 
sub apply
{
  my ($self, $signer) = @_;
  my $domain = undef;
  $domain = lc($signer->message_sender->host)
    if (defined($signer->message_sender));
 
  # merge configs
  while($domain)
  {
    if (defined($config->{$domain}))
    {
      ConfigMerge::merge($config->{'global'}, $config->{$domain});
      last;
    }
    (undef, $domain) = split(/\./, $domain, 2);
  }
 
  my $conf = $config->{'global'};
  return 0
    if (!defined($conf->{'types'}) || ($conf->{'types'}) eq 'none');
 
  # set key file
  $signer->key_file($conf->{'keyfile'});
 
  # parse (signature) domain
  if (substr($conf->{'domain'}, 0, 1) eq '/')
  {
    open(FH, '<', $conf->{'domain'})
      or croak('Unable to open domain-file: '.$!);
    my $newdom = (split(/ /, <FH>))[0];
    close(FH);
    croak("Unable to read domain-file. Maybe empty file.")
      if (!$newdom);
    chomp($newdom);
    $conf->{'domain'} = $newdom;
  }
 
  # generate signatures
  my $types = (ref($conf->{'types'}) eq 'ARRAY') ? $conf->{'types'} : [ $conf->{'types'} ];
  foreach my $type (@$types)
  {
    if ($type eq 'dkim')
    {
      $signer->add_signature(
        new Mail::DKIM::Signature(
          Algorithm  => $conf->{'algorithm'} || $signer->algorithm,
          Method     => $conf->{'method'}    || $signer->method,
          Headers    => $conf->{'headers'}   || $signer->headers,
          Domain     => $conf->{'domain'}    || $signer->domain,
          Selector   => $conf->{'selector'}  || $signer->selector,
          Query      => $conf->{'query'},
          Identity   => $conf->{'identity'},
          Expiration => $conf->{'expiration'}
        )
      );
    }
    elsif ($type eq 'domainkey')
    {
      $signer->add_signature(
        new Mail::DKIM::DkSignature(
          Algorithm  => 'rsa-sha1', # only rsa-sha1 supported
          Method     => $conf->{'method'}   || $signer->method,
          Headers    => $conf->{'headers'}  || $signer->headers,
          Domain     => $conf->{'domain'}   || $signer->domain,
          Selector   => $conf->{'selector'} || $signer->selector,
          Query      => $conf->{'query'}
        )
      );
    }
  }
 
  return 1;
}