-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathqmail-remote
executable file
·334 lines (285 loc) · 9.48 KB
/
qmail-remote
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
#!/usr/bin/perl -T
#
# Copyright (C) 2007 Manuel Mausz ([email protected])
# Copyright (C) 2015-2021 Christian Jaeger (ch at christianjaeger ch)
#
# 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; use warnings FATAL => 'uninitialized';
# find modules
use Cwd 'abs_path';
our ($mydir, $myname);
BEGIN {
my $location= (-l $0) ? abs_path ($0) : $0;
$location=~ /(.*?)([^\/]+?)_?\z/s or die "?";
($mydir, $myname)=($1,$2);
}
use lib "$mydir/chj-perllib";
use lib "$mydir/functional-perl/lib";
use lib "$mydir/lib";
my ($host, $sender, @recip)= @ARGV;
# use safe PATH setting: XX make configurable?
$ENV{PATH}=
join(":",
qw(/etc/better-qmail-remote/sbin
/usr/local/sbin
/usr/local/bin
/usr/sbin
/usr/bin
/sbin
/bin));
our $VERSION = '0.2';
my $debug=0;
use Mail::DKIM 0.29;
use Mail::DKIM::Signer;
use MySignerPolicy;
use ConfigMerge qw(config_merge);
use QmailExit ":all";
use HashCash qw(have_hashcash mint_hashcash);
use Spamscore ":all";
use DeliverMaildir 'deliver_wholemail_maildir';
use FP::Untainted qw(untainted);
use Spambounce_config qw(maildir_spambounce_path);
use Chj::xopen qw(xopen_read);
use Hash::Util 'lock_hash';
sub xfirst_line {
my ($path)= @_;
my $f= xopen_read($path);
my $line= $f->xreadline;
$f->xclose;
$line=~ s/\s+\z//s;
$line
}
sub maybe_first_line {
my ($path)= @_;
(-e $path) ? xfirst_line $path : undef
}
# enable support for "pretty" signatures, if available
eval 'require Mail::DKIM::TextWrap';
my $configfile= '/var/qmail/control/dkim/signconf.xml';
my $maybe_debugfh= ($ENV{BETTER_QMAIL_REMOTE__DEBUG} || $debug) ? do {
require Chj::xtmpfile;
require Chj::singlequote; "Chj::singlequote"->import(qw(singlequote_sh_many));
my $t= Chj::xtmpfile::xtmpfile ("/tmp/qmail-dkim_");
$t->autoclean(0);
$t->xprint (singlequote_sh_many($0, @ARGV),"\n");
for (sort keys %ENV) {
$t->xprint("$_=$ENV{$_}\n");
}
# use warn not qlog here, as it's in a special context anyway? XX
# not fleshed out
warn "writing debug info to ".$t->path;
$t
} : undef;
sub Debug {
if ($maybe_debugfh) {
my ($package, $filename, $line, $subroutine);
my $i=0;
TRY: {
if (my @r= caller($i)) {
($package, $filename, $line, $subroutine)= @r[0..3];
$i++;
my $subname= lc $subroutine;
$subname=~ s/.*:://;
if ($subname eq "debug") {
redo TRY;
}
}
}
$maybe_debugfh->xprintln("$subroutine at $filename line $line: ",
join(" ",@_));
}
}
my $qremote=
$ENV{BETTER_QMAIL_REMOTE__ORIG} ?
untainted($ENV{BETTER_QMAIL_REMOTE__ORIG}) # XX really trust it?
: '/var/qmail/bin/qmail-remote.orig';
my $binary= 0;
our $config;
sub set_config_domain {
my ($domain)=@_;
my $keydir= $ENV{BETTER_QMAIL_REMOTE__KEYDIR} || '/var/qmail/control/dkim';
# Loop through all available .key files, add as corresponding
# selector, implementing
# https://github.com/pflanze/better-qmail-remote/issues/1 :
for my $keyfile (sort glob "$keydir/*.key") {
my ($selector)= $keyfile=~ m{([^/]+)\.key$}s
or die "BUG: can't extract selector from file '$keyfile'";
# $selector would e.g. be 'global'.
my $keyfile_config_base= $keyfile;
$keyfile_config_base=~ s/\.key$//;
my $method= maybe_first_line("$keyfile_config_base.method.txt")
|| 'relaxed/relaxed';
my $algorithm= maybe_first_line("$keyfile_config_base.algorithm.txt")
|| 'rsa-sha256';
my $cfg = +{
types => { dkim => {} },
keyfile => $keyfile,
algorithm => $algorithm,
method => $method,
selector => $selector,
domain => $domain
};
lock_hash %$cfg;
$config->{$selector} = $cfg;
}
}
set_config_domain ( $sender=~ m{[^@]+\@([^/]+)}s ? $1
: xfirst_line('/var/qmail/control/me') );
# XXX: this allows *any* from address domain and will sign it!
lock_hash %$config;
# ----------------------------------------------------------------------
# Create DKIM signature
# read config file. safely
if (defined $configfile and -r $configfile) {
eval 'use XML::Simple; 1' and do {
my $xmlconf;
eval {
$xmlconf = XMLin($configfile,
ForceArray => ['types'],
KeyAttr => ['id']);
1
} || do {
qexit_deferral('Unable to read config file: ', $@)
};
config_merge($config, $xmlconf);
};
}
# generate signatures
my $dkim;
my $mailbuf = '';
eval {
$dkim =
Mail::DKIM::Signer->new(
Policy => MySignerPolicy->new($config,
$maybe_debugfh ? \&Debug : undef),
Debug_Canonicalization => $maybe_debugfh
);
if ($binary) {
binmode STDIN;
}
while (<STDIN>) {
$mailbuf .= $_;
unless ($binary)
{
chomp $_;
s/\015?$/\015\012/s;
}
$dkim->PRINT($_);
}
$dkim->CLOSE();
1
} || do {
qexit_deferral('Error while signing: ', $@)
};
$maybe_debugfh->xflush if $maybe_debugfh;
# ----------------------------------------------------------------------
# Backscatter avoidance
# Check whether we really want to deliver this message: if it
# has a high spam score, don't. Although, if those are locally
# generated messages, *iff* they ever get an SA score, then the
# refusal should be in the smtpd [or imapd] part, not here. What we
# really just want is, stop delivery of *bounces* of high-spamscore
# emails.
# Can't do this earlier since we had to read $mailbuf first.
sub stopit {
my ($spamscore, $kind)= @_;
# deliver to 'emergency' local account instead.
my $maildir= maildir_spambounce_path;
qlog "msg in case '$kind' treated as likely spam (score $spamscore), diverting to local maildir at '$maildir'";
deliver_wholemail_maildir $mailbuf, $maildir, undef, 0644;
qexit_success;
}
eval {
if (my ($to_whom,$return_and_orig)= perhaps_wholemail_doublebounce $mailbuf) {
qlog "not sure how comes that we're trying to send a doublebounce";
# should be impossible, qmail doesn't send double bounces
} elsif (my ($return,$orig)= perhaps_wholemail_bounce $mailbuf) {
if (my ($spamscore)= wholemail_spamscore $orig) {
# likely spam to a non-existing address.
if ($spamscore >= 0.5) {
stopit $spamscore, "bounce";
}
} else {
# The original part is missing the spamscore; this happens
# if a mail is bigger than the size cut-off at which it is
# being fed through SA (qpsmtpd has a hard-coded limit in
# its `spamassassin` module, you need to patch it to
# increase it!); or if spamd was not running. To avoid
# any chance of sending out spam as backscatter, do not
# deliver it.
stopit $spamscore, "spamcheck failure";
}
} else {
# not a bounce. still check spamscore, even though normal
# outgoing emails should never have one in the current setup
# (those don't run through spamassassin), but it happens for
# mailing lists (incoming mail gets score before
# delivery). There should be proper moderation instead (the
# disadvantage of this is that such mails are still making it
# to the mailing list archive, and there are as many
# duplicates of the mail landing in the Maildir_spambounce as
# there are subscribers), but better be safe.
if (my ($spamscore)= wholemail_spamscore $mailbuf) {
if ($spamscore > 1) {
stopit $spamscore, "non-bounce";
}
} else {
# Now only reached if spamcheck fails; it's a locally
# generated mail, should it be delivered? No, be
# consistent and refuse to send any unchecked mails. (OK,
# *could* fall back to try to get the *old* spam headers
# and check *those* for spamscore, in wholemail_spamscore,
# and trust those. But, prefer simplicity?)
#stopit "n.A.", "spamcheck failed";
# Or, better, give a temporary failure:
qexit_deferral("spamcheck failure");
}
}
1
} || do {
qexit_deferral('Error while checking: ', $@)
# qlog "ignoring exception during spam check: $@";
};
# ----------------------------------------------------------------------
# Deliver the message, and add hashcash and the DKIM signature
# execute qmail-remote
open(QR, '|-') || qexec ($qremote, map { untainted ($_) } @ARGV)
or qexit_deferral('Unable to run qmail-remote: ', $!);
if (have_hashcash) { # XX add configuration option (instead)?
my $bits= 23; # XX configuration option, too.
for my $recip (@recip) {
eval {
my $c= mint_hashcash $bits, $recip;
print QR $c
or qexit_deferral ('Printing to qmail-remote: ', $!);
1
} or qlog "$@";
}
} else {
Debug ("don't have hashcash");
}
for my $dkim_signature ($dkim->signatures) {
my $sig = $dkim_signature->as_string;
$sig =~ s/\015\012\t/\012\t/g;
print QR $sig."\012"
or qexit_deferral ('Printing to qmail-remote: ', $!);
}
print QR $mailbuf or qexit_deferral ('Printing to qmail-remote: ', $!);
close(QR) or qexit_deferral ('Sending to qmail-remote: ', $!);
# why is qexit_success never called? Because the piped-to qmail-remote
# issues it.
$maybe_debugfh->xclose
if defined $maybe_debugfh;