1 # Copyright (C) 2014-2019 all contributors <meta@public-inbox.org>
2 # License: AGPL-3.0+ <https://www.gnu.org/licenses/agpl-3.0.txt>
7 use File::Temp qw/tempdir/;
9 use PublicInbox::MID qw(mid2path);
11 require './t/common.perl';
12 my $tmpdir = tempdir('pi-mda-XXXXXX', TMPDIR => 1, CLEANUP => 1);
13 my $home = "$tmpdir/pi-home";
14 my $pi_home = "$home/.public-inbox";
15 my $pi_config = "$pi_home/config";
16 my $maindir = "$tmpdir/main.git";
17 my $main_bin = getcwd()."/t/main-bin";
18 my $main_path = "$main_bin:$ENV{PATH}"; # for spamc ham mock
19 my $fail_bin = getcwd()."/t/fail-bin";
20 my $fail_path = "$fail_bin:$ENV{PATH}"; # for spamc spam mock
21 my $addr = 'test-public@example.com';
22 my $cfgpfx = "publicinbox.test";
23 my $faildir = "$home/faildir/";
25 my $git = PublicInbox::Git->new($maindir);
28 ok(-x "$main_bin/spamc",
29 "spamc ham mock found (run in top of source tree");
30 ok(-x "$fail_bin/spamc",
31 "spamc mock found (run in top of source tree");
32 is(1, mkdir($home, 0755), "setup ~/ for testing");
33 is(1, mkdir($pi_home, 0755), "setup ~/.public-inbox");
34 is(0, system(qw(git init -q --bare), $maindir), "git init (main)");
37 "$cfgpfx.address" => $addr,
38 "$cfgpfx.inboxdir" => $maindir,
40 while (my ($k,$v) = each %cfg) {
41 is(0, system(qw(git config --file), $pi_config, $k, $v),
46 local $ENV{GIT_COMMITTER_NAME} = eval {
48 use PublicInbox::Address;
49 use Encode qw/encode/;
50 my $mbox = 't/utf8.mbox';
51 open(my $fh, '<', $mbox) or die "failed to open mbox: $mbox\n";
52 my $str = eval { local $/; <$fh> };
54 my $msg = Email::MIME->new($str);
56 my $from = $msg->header('From');
57 my ($author) = PublicInbox::Address::names($from);
58 my ($email) = PublicInbox::Address::emails($from);
59 my $date = $msg->header('Date');
62 encode('us-ascii', my $tmp = $author, Encode::HTMLCREF),
63 'HTML conversion is correct');
64 is($email, 'e@example.com', 'email parsed correctly');
65 is($date, 'Thu, 01 Jan 1970 00:00:00 +0000',
66 'message date parsed correctly');
73 local $ENV{PI_EMERGENCY} = $faildir;
74 local $ENV{HOME} = $home;
75 local $ENV{ORIGINAL_RECIPIENT} = $addr;
76 my $simple = Email::Simple->new(<<EOF);
77 From: Me <me\@example.com>
78 To: You <you\@example.com>
80 Message-Id: <blah\@example.com>
82 Date: Thu, 01 Jan 1970 00:00:00 +0000
85 my $in = $simple->as_string;
87 # ensure successful message delivery
89 local $ENV{PATH} = $main_path;
90 ok(run_script(['-mda'], undef, { 0 => \$in }));
91 my $rev = $git->qx(qw(rev-list HEAD));
92 like($rev, qr/\A[a-f0-9]{40}/, "good revision committed");
94 my $cmt = $git->cat_file($rev);
95 like($$cmt, qr/^author Me <me\@example\.com> 0 \+0000\n/m,
96 "author info set correctly");
97 like($$cmt, qr/^committer test <test-public\@example\.com>/m,
98 "committer info set correctly");
102 # ensure failures work, fail with bad spamc
104 my @prev = <$faildir/new/*>;
105 is(scalar @prev, 0 , "nothing in PI_EMERGENCY before");
106 local $ENV{PATH} = $fail_path;
107 ok(run_script(['-mda'], undef, { 0 => \$in }));
108 my @revs = $git->qx(qw(rev-list HEAD));
109 is(scalar @revs, 1, "bad revision not committed");
110 my @new = <$faildir/new/*>;
111 is(scalar @new, 1, "PI_EMERGENCY is written to");
114 fail_bad_header($good_rev, "bad recipient", <<"");
115 From: Me <me\@example.com>
116 To: You <you\@example.com>
117 Message-Id: <bad-recipient\@example.com>
119 Date: Thu, 01 Jan 1970 00:00:00 +0000
121 my $fail = fail_bad_header($good_rev, "duplicate Message-ID", <<"");
122 From: Me <me\@example.com>
123 To: You <you\@example.com>
125 Message-ID: <blah\@example.com>
127 Date: Thu, 01 Jan 1970 00:00:00 +0000
129 like($fail->[2], qr/CONFLICT/, "duplicate Message-ID message");
131 fail_bad_header($good_rev, "missing From:", <<"");
133 Message-ID: <missing-from\@example.com>
135 Date: Thu, 01 Jan 1970 00:00:00 +0000
137 fail_bad_header($good_rev, "short subject:", <<"");
139 From: cat\@example.com
140 Message-ID: <short-subject\@example.com>
142 Date: Thu, 01 Jan 1970 00:00:00 +0000
144 fail_bad_header($good_rev, "no date", <<"");
147 Message-ID: <no-date\@example.com>
150 fail_bad_header($good_rev, "bad date", <<"");
153 Message-ID: <bad-date\@example.com>
161 local $ENV{PI_EMERGENCY} = $faildir;
162 local $ENV{HOME} = $home;
163 local $ENV{ORIGINAL_RECIPIENT} = $addr;
164 local $ENV{PATH} = $main_path;
165 my $mid = 'spam-train@example.com';
166 my $simple = Email::Simple->new(<<EOF);
167 From: Spammer <spammer\@example.com>
168 To: You <you\@example.com>
171 Subject: this message will be trained as spam
172 Date: Thu, 01 Jan 1970 00:00:00 +0000
175 my $in = $simple->as_string;
178 # deliver the spam message, first
179 ok(run_script(['-mda'], undef, { 0 => \$in }));
180 my $path = mid2path($mid);
181 my $msg = $git->cat_file("HEAD:$path");
182 like($$msg, qr/\Q$mid\E/, "message delivered");
185 local $ENV{GIT_AUTHOR_EMAIL} = 'trainer@example.com';
186 local $ENV{GIT_COMMITTER_EMAIL} = 'trainer@example.com';
187 local $ENV{GIT_COMMITTER_NAME};
188 delete $ENV{GIT_COMMITTER_NAME};
189 ok(run_script(['-learn', 'spam'], undef, { 0 => $msg }),
190 "no failure from learning spam");
191 ok(run_script(['-learn', 'spam'], undef, { 0 => $msg }),
192 "no failure from learning spam idempotently");
198 local $ENV{PI_EMERGENCY} = $faildir;
199 local $ENV{HOME} = $home;
200 local $ENV{ORIGINAL_RECIPIENT} = $addr;
201 local $ENV{PATH} = $main_path;
202 my $mid = 'ham-train@example.com';
203 my $simple = Email::Simple->new(<<EOF);
204 From: False-positive <hammer\@example.com>
205 To: You <you\@example.com>
208 Subject: this message will be trained as spam
209 Date: Thu, 01 Jan 1970 00:00:00 +0000
212 my $in = $simple->as_string;
215 # these should be overridden
216 local $ENV{GIT_AUTHOR_EMAIL} = 'trainer@example.com';
217 local $ENV{GIT_COMMITTER_EMAIL} = 'trainer@example.com';
219 ok(run_script(['-learn', 'ham'], undef, { 0 => \$in }),
220 "learned ham without failure");
221 my $path = mid2path($mid);
222 my $msg = $git->cat_file("HEAD:$path");
223 like($$msg, qr/\Q$mid\E/, "ham message delivered");
224 ok(run_script(['-learn', 'ham'], undef, { 0 => \$in }),
225 "learned ham idempotently ");
227 # ensure trained email is filtered, too
228 my $html_body = "<html><body>hi</body></html>";
232 content_type => 'text/html; charset=UTF-8',
233 encoding => 'base64',
239 content_type => 'text/plain',
240 encoding => 'quoted-printable',
242 body => 'hi = "bye"',
245 $mid = 'multipart-html-sucks@11';
246 $mime = Email::MIME->create(
248 From => 'a@example.com',
251 'Message-ID' => "<$mid>",
252 'Content-Type' => 'multipart/alternative',
258 $in = $mime->as_string;
259 ok(run_script(['-learn', 'ham'], undef, { 0 => \$in }),
260 "learned ham without failure");
261 my $path = mid2path($mid);
262 $msg = $git->cat_file("HEAD:$path");
263 like($$msg, qr/<\Q$mid\E>/, "ham message delivered");
264 unlike($$msg, qr/<html>/i, '<html> filtered');
268 # List-ID based delivery
270 local $ENV{PI_EMERGENCY} = $faildir;
271 local $ENV{HOME} = $home;
272 local $ENV{ORIGINAL_RECIPIENT} = undef;
273 delete $ENV{ORIGINAL_RECIPIENT};
274 local $ENV{PATH} = $main_path;
275 my $list_id = 'foo.example.com';
276 my $mid = 'list-id-delivery@example.com';
277 my $simple = Email::Simple->new(<<EOF);
278 From: user <user\@example.com>
279 To: You <you\@example.com>
283 Subject: this message will be trained as spam
284 Date: Thu, 01 Jan 1970 00:00:00 +0000
287 system(qw(git config --file), $pi_config, "$cfgpfx.listid", $list_id);
288 $? == 0 or die "failed to set listid $?";
289 my $in = $simple->as_string;
290 ok(run_script(['-mda'], undef, { 0 => \$in }),
291 'mda OK with List-Id match');
292 my $path = mid2path($mid);
293 my $msg = $git->cat_file("HEAD:$path");
294 like($$msg, qr/\Q$list_id\E/, 'delivered message w/ List-ID matches');
296 # try a message w/o precheck
297 $simple = Email::Simple->new(<<EOF);
298 To: You <you\@example.com>
301 this message would not be accepted without --no-precheck
303 $in = $simple->as_string;
304 my ($out, $err) = ('', '');
305 my $rdr = { 0 => \$in, 1 => \$out, 2 => \$err };
306 ok(run_script(['-mda', '--no-precheck'], undef, $rdr),
307 'mda OK with List-Id match and --no-precheck');
308 my $cur = $git->qx(qw(diff HEAD~1..HEAD));
309 like($cur, qr/this message would not be accepted without --no-precheck/,
310 '--no-precheck delivered message anyways');
312 # try a message with multiple List-ID headers
316 Message-ID: <2lids\@example>
317 Subject: two List-IDs
318 From: user <user\@example.com>
320 Date: Fri, 02 Oct 1993 00:00:00 +0000
323 ($out, $err) = ('', '');
324 ok(run_script(['-mda'], undef, $rdr),
325 'mda OK with multiple List-Id matches');
326 $cur = $git->qx(qw(diff HEAD~1..HEAD));
327 like($cur, qr/Message-ID: <2lids\@example>/,
328 'multi List-ID match delivered');
329 like($err, qr/multiple List-ID/, 'warned about multiple List-ID');
334 sub fail_bad_header {
335 my ($good_rev, $msg, $in) = @_;
336 my @f = glob("$faildir/*/*");
338 my ($out, $err) = ("", "");
339 my $opt = { 0 => \$in, 1 => \$out, 2 => \$err };
340 local $ENV{PATH} = $main_path;
341 ok(run_script(['-mda'], undef, $opt),
342 "no error on undeliverable ($msg)");
343 my $rev = $git->qx(qw(rev-list HEAD));
345 is($rev, $good_rev, "bad revision not commited ($msg)");
346 @f = glob("$faildir/*/*");
347 is(scalar @f, 1, "faildir written to");