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.1';
  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 global
  33.  - domain-entry will also match its subdomains
  34.  - specify "none" in types-tag to omit signing
  35.  
  36. <dkimsign>
  37.   <global algorithm="rsa-sha256" domain="/var/qmail/control/me" keyfile="/var/qmail/control/dkim/global.key" method="simple" selector="beta">
  38.   <types>dkim</types>
  39.   </global>
  40.   <example.com selector="beta2">
  41.   <types>dkim</types>
  42.   <types>domainkey</types>
  43.   </example.com>
  44. </dkimsign>
  45. =cut
  46.  
  47. my $configfile = undef;
  48. $configfile = '/var/qmail/control/dkim/signconf.xml';
  49. my $debugfile = undef;
  50. #$debugfile = '/tmp/dkim.debug';
  51. my $qremote = '/var/qmail/bin/qmail-remote.orig';
  52. my $binary = 0;
  53. our $config;
  54. $config->{'global'} = {
  55. types => 'dkim',
  56. keyfile => '/var/qmail/control/dkim/global.key',
  57. algorithm => 'rsa-sha256',
  58. method => 'simple',
  59. selector => 'beta',
  60. # either string or file (first line of file will be used)
  61. domain => '/var/qmail/control/me'
  62. };
  63.  
  64. #-------------------------------------------------------------------------------
  65.  
  66. # read config file. safely
  67. if (defined($configfile) && -r $configfile)
  68. {
  69. eval 'use XML::Simple';
  70. if (!$@)
  71. {
  72. my $xmlconf;
  73. eval { $xmlconf = XMLin($configfile, ForceArray => 0); };
  74. qexit_deferral('Unable to read config file: ', $@)
  75. if ($@);
  76. ConfigMerge::merge($config, $xmlconf);
  77. }
  78. }
  79.  
  80. # open debug file
  81. my $debugfh = undef;
  82. if (defined($debugfile))
  83. {
  84. open($debugfh, '>', $debugfile)
  85. or qexit_deferral('Unable to open ', $debugfile, ' to writing: ', $!);
  86. }
  87.  
  88. # generate signatures
  89. my $dkim;
  90. my $mailbuf = '';
  91. eval
  92. {
  93. my $conf = $config->{'global'};
  94. $dkim = Mail::DKIM::Signer->new(
  95. Policy => 'MySignerPolicy',
  96. Debug_Canonicalization => $debugfh
  97. );
  98.  
  99. if ($binary)
  100. {
  101. binmode STDIN;
  102. }
  103.  
  104. while (<STDIN>)
  105. {
  106. $mailbuf .= $_;
  107. unless ($binary)
  108. {
  109. chomp $_;
  110. s/\015?$/\015\012/s;
  111. }
  112. $dkim->PRINT($_);
  113. }
  114. $dkim->CLOSE();
  115. };
  116. qexit_deferral('Error while signing: ', $@)
  117. if ($@);
  118.  
  119. # close debug file
  120. close($debugfh)
  121. if (defined($debugfh));
  122.  
  123. # execute qmail-remote
  124. unshift(@ARGV, $qremote);
  125. open(QR, '|-') || exec { $ARGV[0] } @ARGV
  126. or qexit_deferral('Unable to run qmail-remote: ', $!);
  127. foreach my $dkim_signature ($dkim->signatures)
  128. {
  129. my $sig = $dkim_signature->as_string;
  130. $sig =~ s/\015\012\t/\012\t/g;
  131. print QR $sig."\012";
  132. }
  133. print QR $mailbuf;
  134. close(QR);
  135.  
  136. #-------------------------------------------------------------------------------
  137.  
  138. sub qexit
  139. {
  140. print @_, "\0";
  141. exit(0);
  142. }
  143.  
  144. sub qexit_deferral
  145. {
  146. return qexit('Z', @_);
  147. }
  148.  
  149. sub qexit_failure
  150. {
  151. return qexit('D', @_);
  152. }
  153.  
  154. sub qexit_success
  155. {
  156. return qexit('K', @_);
  157. }
  158.  
  159. #-------------------------------------------------------------------------------
  160.  
  161. package ConfigMerge;
  162. our $VERSION = '0.1';
  163.  
  164. # merge config hashes. arrays and scalars will be copied.
  165. sub merge
  166. {
  167. my ($left, $right) = @_;
  168. foreach my $rkey (keys(%$right))
  169. {
  170. my $rtype = ref($right->{$rkey}) eq 'HASH' ? 'HASH'
  171. : ref($right->{$rkey}) eq 'ARRAY' ? 'ARRAY'
  172. : 'SCALAR';
  173. if (!defined($left->{$rkey}) || $rtype ne 'HASH')
  174. {
  175. $left->{$rkey} = $right->{$rkey};
  176. }
  177. else
  178. {
  179. merge($left->{$rkey}, $right->{$rkey});
  180. }
  181. }
  182. return;
  183. }
  184.  
  185. #-------------------------------------------------------------------------------
  186.  
  187. package MySignerPolicy;
  188. use Mail::DKIM::SignerPolicy;
  189. use base 'Mail::DKIM::SignerPolicy';
  190. use Mail::DKIM::Signature;
  191. use Mail::DKIM::DkSignature;
  192. use Carp;
  193. our $VERSION = '0.1';
  194.  
  195. sub apply
  196. {
  197. my ($self, $signer) = @_;
  198. my $domain = undef;
  199. $domain = lc($signer->message_sender->host)
  200. if (defined($signer->message_sender));
  201.  
  202. # merge configs
  203. while($domain)
  204. {
  205. if (defined($config->{$domain}))
  206. {
  207. ConfigMerge::merge($config->{'global'}, $config->{$domain});
  208. last;
  209. }
  210. (undef, $domain) = split(/\./, $domain, 2);
  211. }
  212.  
  213. my $conf = $config->{'global'};
  214. return 0
  215. if (!defined($conf->{'types'}) || ($conf->{'types'}) eq 'none');
  216.  
  217. # set key file
  218. $signer->key_file($conf->{'keyfile'});
  219.  
  220. # parse (signature) domain
  221. if (substr($conf->{'domain'}, 0, 1) eq '/')
  222. {
  223. open(FH, '<', $conf->{'domain'})
  224. or croak('Unable to open domain-file: '.$!);
  225. my $newdom = (split(/ /, <FH>))[0];
  226. close(FH);
  227. croak("Unable to read domain-file. Maybe empty file.")
  228. if (!$newdom);
  229. chomp($newdom);
  230. $conf->{'domain'} = $newdom;
  231. }
  232.  
  233. # generate signatures
  234. my $types = (ref($conf->{'types'}) eq 'ARRAY') ? $conf->{'types'} : [ $conf->{'types'} ];
  235. foreach my $type (@$types)
  236. {
  237. if ($type eq 'dkim')
  238. {
  239. $signer->add_signature(
  240. new Mail::DKIM::Signature(
  241. Algorithm => $conf->{'algorithm'} || $signer->algorithm,
  242. Method => $conf->{'method'} || $signer->method,
  243. Headers => $conf->{'headers'} || $signer->headers,
  244. Domain => $conf->{'domain'} || $signer->domain,
  245. Selector => $conf->{'selector'} || $signer->selector,
  246. Query => $conf->{'query'},
  247. Identity => $conf->{'identity'},
  248. Expiration => $conf->{'expiration'}
  249. )
  250. );
  251. }
  252. elsif ($type eq 'domainkey')
  253. {
  254. $signer->add_signature(
  255. new Mail::DKIM::DkSignature(
  256. Algorithm => 'rsa-sha1', # only rsa-sha1 supported
  257. Method => $conf->{'method'} || $signer->method,
  258. Headers => $conf->{'headers'} || $signer->headers,
  259. Domain => $conf->{'domain'} || $signer->domain,
  260. Selector => $conf->{'selector'} || $signer->selector,
  261. Query => $conf->{'query'}
  262. )
  263. );
  264. }
  265. }
  266.  
  267. return 1;
  268. }
  269.