Download | Plain Text | No Line Numbers


  1. #!/usr/bin/perl
  2. #
  3. # Copyright (C) 2007 Manuel Mausz (manuel@mausz.at)
  4. #
  5. # This program is free software; you can redistribute it and/or
  6. # modify it under the terms of the GNU General Public License
  7. # as published by the Free Software Foundation; either
  8. # version 2 of the License, or (at your option) any later
  9. # version.
  10. #
  11. # This program is distributed in the hope that it will be useful,
  12. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. # GNU General Public License for more details.
  15. #
  16. # You should have received a copy of the GNU General Public License
  17. # along with this program; if not, write to the Free Software Foundation,
  18. # Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
  19.  
  20. use strict;
  21. use warnings;
  22. our $VERSION = '0.2';
  23.  
  24. use Mail::DKIM 0.29;
  25. use Mail::DKIM::Signer;
  26.  
  27. # enable support for "pretty" signatures, if available
  28. eval 'require Mail::DKIM::TextWrap';
  29.  
  30. =head
  31. config file structure
  32.  - missing settings will be merged from the global-node
  33.  - domain-entry will also match its subdomains
  34.  - create empty domain-node to omit signing (or specify "none" as id)
  35.  
  36. <dkimsign>
  37.   <!-- per default sign all mails using dkim -->
  38.   <global algorithm="rsa-sha256" domain="/var/qmail/control/me" keyfile="/var/qmail/control/dkim/global.key" method="simple" selector="beta">
  39.   <types id="dkim" />
  40.   </global>
  41.  
  42.   <!-- use dkim + domainkey for example.com -->
  43.   <example.com selector="beta2">
  44.   <types id="dkim" />
  45.   <types id="domainkey" method="nofws" />
  46.   </example.com>
  47.  
  48.   <!-- no signing for example2.com -->
  49.   <example2.com />
  50. </dkimsign>
  51. =cut
  52.  
  53. my $configfile = undef;
  54. $configfile = '/var/qmail/control/dkim/signconf.xml';
  55. my $debugfile = undef;
  56. #$debugfile = '/tmp/dkim.debug';
  57. my $qremote = '/var/qmail/bin/qmail-remote.orig';
  58. my $binary = 0;
  59. our $config;
  60. $config->{'global'} = {
  61. types => { dkim => {} },
  62. keyfile => '/var/qmail/control/dkim/global.key',
  63. algorithm => 'rsa-sha256',
  64. method => 'simple',
  65. selector => 'beta',
  66. # either string or file (first line of file will be used)
  67. #domain => '/var/qmail/control/me'
  68. };
  69.  
  70. #-------------------------------------------------------------------------------
  71.  
  72. # read config file. safely
  73. if (defined($configfile) && -r $configfile)
  74. {
  75. eval 'use XML::Simple';
  76. if (!$@)
  77. {
  78. my $xmlconf;
  79. eval { $xmlconf = XMLin($configfile, ForceArray => ['types'], KeyAttr => ['id']); };
  80. qexit_deferral('Unable to read config file: ', $@)
  81. if ($@);
  82. ConfigMerge::merge($config, $xmlconf);
  83. }
  84. }
  85.  
  86. # open debug file
  87. my $debugfh = undef;
  88. if (defined($debugfile))
  89. {
  90. open($debugfh, '>', $debugfile)
  91. or qexit_deferral('Unable to open ', $debugfile, ' to writing: ', $!);
  92. }
  93.  
  94. # generate signatures
  95. my $dkim;
  96. my $mailbuf = '';
  97. eval
  98. {
  99. my $conf = $config->{'global'};
  100. $dkim = Mail::DKIM::Signer->new(
  101. Policy => 'MySignerPolicy',
  102. Debug_Canonicalization => $debugfh
  103. );
  104.  
  105. if ($binary)
  106. {
  107. binmode STDIN;
  108. }
  109.  
  110. while (<STDIN>)
  111. {
  112. $mailbuf .= $_;
  113. unless ($binary)
  114. {
  115. chomp $_;
  116. s/\015?$/\015\012/s;
  117. }
  118. $dkim->PRINT($_);
  119. }
  120. $dkim->CLOSE();
  121. };
  122. qexit_deferral('Error while signing: ', $@)
  123. if ($@);
  124.  
  125. # close debug file
  126. close($debugfh)
  127. if (defined($debugfh));
  128.  
  129. # execute qmail-remote
  130. unshift(@ARGV, $qremote);
  131. open(QR, '|-') || exec { $ARGV[0] } @ARGV
  132. or qexit_deferral('Unable to run qmail-remote: ', $!);
  133. foreach my $dkim_signature ($dkim->signatures)
  134. {
  135. my $sig = $dkim_signature->as_string;
  136. $sig =~ s/\015\012\t/\012\t/g;
  137. print QR $sig."\012";
  138. }
  139. print QR $mailbuf;
  140. close(QR);
  141.  
  142. #-------------------------------------------------------------------------------
  143.  
  144. sub qexit
  145. {
  146. print @_, "\0";
  147. exit(0);
  148. }
  149.  
  150. sub qexit_deferral
  151. {
  152. return qexit('Z', @_);
  153. }
  154.  
  155. sub qexit_failure
  156. {
  157. return qexit('D', @_);
  158. }
  159.  
  160. sub qexit_success
  161. {
  162. return qexit('K', @_);
  163. }
  164.  
  165. #-------------------------------------------------------------------------------
  166.  
  167. package ConfigMerge;
  168.  
  169. # merge config hashes. arrays and scalars will be copied.
  170. sub merge
  171. {
  172. my ($left, $right) = @_;
  173. foreach my $rkey (keys(%$right))
  174. {
  175. my $rtype = ref($right->{$rkey}) eq 'HASH' ? 'HASH'
  176. : ref($right->{$rkey}) eq 'ARRAY' ? 'ARRAY'
  177. : defined($right->{$rkey}) ? 'SCALAR'
  178. : '';
  179. my $ltype = ref($left->{$rkey}) eq 'HASH' ? 'HASH'
  180. : ref($left->{$rkey}) eq 'ARRAY' ? 'ARRAY'
  181. : defined($left->{$rkey}) ? 'SCALAR'
  182. : '';
  183. if ($rtype ne 'HASH' || $ltype ne 'HASH')
  184. {
  185. $left->{$rkey} = $right->{$rkey};
  186. }
  187. else
  188. {
  189. merge($left->{$rkey}, $right->{$rkey});
  190. }
  191. }
  192. return;
  193. }
  194.  
  195. #-------------------------------------------------------------------------------
  196.  
  197. package MySignerPolicy;
  198. use Mail::DKIM::SignerPolicy;
  199. use base 'Mail::DKIM::SignerPolicy';
  200. use Mail::DKIM::Signature;
  201. use Mail::DKIM::DkSignature;
  202. use Carp;
  203. use strict;
  204. use warnings;
  205.  
  206. sub apply
  207. {
  208. my ($self, $signer) = @_;
  209. my $domain = undef;
  210. $domain = lc($signer->message_sender->host)
  211. if (defined($signer) && defined($signer->message_sender)
  212. && defined($signer->message_sender->host));
  213.  
  214. # merge configs
  215. while($domain)
  216. {
  217. if (defined($config->{$domain}))
  218. {
  219. $config->{'global'}->{'types'} = undef;
  220. ConfigMerge::merge($config->{'global'}, $config->{$domain});
  221. last;
  222. }
  223. (undef, $domain) = split(/\./, $domain, 2);
  224. }
  225.  
  226. my $conf = $config->{'global'};
  227. return 0
  228. if (!defined($conf->{'types'}) || defined($conf->{'types'}->{'none'}));
  229.  
  230. # set key file
  231. $signer->key_file($conf->{'keyfile'});
  232.  
  233. # parse (signature) domain
  234. if (substr($conf->{'domain'}, 0, 1) eq '/')
  235. {
  236. open(FH, '<', $conf->{'domain'})
  237. or croak('Unable to open domain-file: '.$!);
  238. my $newdom = (split(/ /, <FH>))[0];
  239. close(FH);
  240. croak("Unable to read domain-file. Maybe empty file.")
  241. if (!$newdom);
  242. chomp($newdom);
  243. $conf->{'domain'} = $newdom;
  244. }
  245.  
  246. # generate signatures
  247. my $sigdone = 0;
  248. foreach my $type (keys(%{$conf->{'types'}}))
  249. {
  250. my $sigconf = $conf->{'types'}->{$type};
  251.  
  252. if ($type eq 'dkim')
  253. {
  254. $signer->add_signature(
  255. new Mail::DKIM::Signature(
  256. Algorithm => $sigconf->{'algorithm'} || $conf->{'algorithm'} || $signer->algorithm,
  257. Method => $sigconf->{'method'} || $conf->{'method'} || $signer->method,
  258. Headers => $sigconf->{'headers'} || $conf->{'headers'} || $signer->headers,
  259. Domain => $sigconf->{'domain'} || $conf->{'domain'} || $signer->domain,
  260. Selector => $sigconf->{'selector'} || $conf->{'selector'} || $signer->selector,
  261. Query => $sigconf->{'query'} || $conf->{'query'},
  262. Identity => $sigconf->{'identity'} || $conf->{'identity'},
  263. Expiration => $sigconf->{'expiration'} || $conf->{'expiration'}
  264. )
  265. );
  266. $sigdone = 1;
  267. }
  268. elsif ($type eq 'domainkey')
  269. {
  270. $signer->add_signature(
  271. new Mail::DKIM::DkSignature(
  272. Algorithm => 'rsa-sha1', # only rsa-sha1 supported
  273. Method => $sigconf->{'method'} || $conf->{'method'} || $signer->method,
  274. Headers => $sigconf->{'selector'} || $conf->{'headers'} || $signer->headers,
  275. Domain => $sigconf->{'domain'} || $conf->{'domain'} || $signer->domain,
  276. Selector => $sigconf->{'selector'} || $conf->{'selector'} || $signer->selector,
  277. Query => $sigconf->{'query'} || $conf->{'query'}
  278. )
  279. );
  280. $sigdone = 1;
  281. }
  282. }
  283.  
  284. return $sigdone;
  285. }
  286.