]> Sergey Matveev's repositories - public-inbox.git/blob - t/mda.t
t/mda: switch to run_script for testing
[public-inbox.git] / t / mda.t
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>
3 use strict;
4 use warnings;
5 use Test::More;
6 use Email::MIME;
7 use File::Temp qw/tempdir/;
8 use Cwd qw(getcwd);
9 use PublicInbox::MID qw(mid2path);
10 use PublicInbox::Git;
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/";
24 my $mime;
25 my $git = PublicInbox::Git->new($maindir);
26
27 {
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)");
35
36         my %cfg = (
37                 "$cfgpfx.address" => $addr,
38                 "$cfgpfx.inboxdir" => $maindir,
39         );
40         while (my ($k,$v) = each %cfg) {
41                 is(0, system(qw(git config --file), $pi_config, $k, $v),
42                         "setup $k");
43         }
44 }
45
46 local $ENV{GIT_COMMITTER_NAME} = eval {
47         use PublicInbox::MDA;
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> };
53         close $fh;
54         my $msg = Email::MIME->new($str);
55
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');
60
61         is('El&#233;anor',
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');
67         $author;
68 };
69 die $@ if $@;
70
71 {
72         my $good_rev;
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>
79 Cc: $addr
80 Message-Id: <blah\@example.com>
81 Subject: hihi
82 Date: Thu, 01 Jan 1970 00:00:00 +0000
83
84 EOF
85         my $in = $simple->as_string;
86
87         # ensure successful message delivery
88         {
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");
93                 chomp $rev;
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");
99                 $good_rev = $rev;
100         }
101
102         # ensure failures work, fail with bad spamc
103         {
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");
112         }
113
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>
118 Subject: hihi
119 Date: Thu, 01 Jan 1970 00:00:00 +0000
120
121         my $fail = fail_bad_header($good_rev, "duplicate Message-ID", <<"");
122 From: Me <me\@example.com>
123 To: You <you\@example.com>
124 Cc: $addr
125 Message-ID: <blah\@example.com>
126 Subject: hihi
127 Date: Thu, 01 Jan 1970 00:00:00 +0000
128
129         like($fail->[2], qr/CONFLICT/, "duplicate Message-ID message");
130
131         fail_bad_header($good_rev, "missing From:", <<"");
132 To: $addr
133 Message-ID: <missing-from\@example.com>
134 Subject: hihi
135 Date: Thu, 01 Jan 1970 00:00:00 +0000
136
137         fail_bad_header($good_rev, "short subject:", <<"");
138 To: $addr
139 From: cat\@example.com
140 Message-ID: <short-subject\@example.com>
141 Subject: a
142 Date: Thu, 01 Jan 1970 00:00:00 +0000
143
144         fail_bad_header($good_rev, "no date", <<"");
145 To: $addr
146 From: u\@example.com
147 Message-ID: <no-date\@example.com>
148 Subject: hihi
149
150         fail_bad_header($good_rev, "bad date", <<"");
151 To: $addr
152 From: u\@example.com
153 Message-ID: <bad-date\@example.com>
154 Subject: hihi
155 Date: deadbeef
156
157 }
158
159 # spam training
160 {
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>
169 Cc: $addr
170 Message-ID: <$mid>
171 Subject: this message will be trained as spam
172 Date: Thu, 01 Jan 1970 00:00:00 +0000
173
174 EOF
175         my $in = $simple->as_string;
176
177         {
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");
183
184                 # now train it
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");
193         }
194 }
195
196 # train ham message
197 {
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>
206 Cc: $addr
207 Message-ID: <$mid>
208 Subject: this message will be trained as spam
209 Date: Thu, 01 Jan 1970 00:00:00 +0000
210
211 EOF
212         my $in = $simple->as_string;
213
214         # now train it
215         # these should be overridden
216         local $ENV{GIT_AUTHOR_EMAIL} = 'trainer@example.com';
217         local $ENV{GIT_COMMITTER_EMAIL} = 'trainer@example.com';
218
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 ");
226
227         # ensure trained email is filtered, too
228         my $html_body = "<html><body>hi</body></html>";
229         my $parts = [
230                 Email::MIME->create(
231                         attributes => {
232                                 content_type => 'text/html; charset=UTF-8',
233                                 encoding => 'base64',
234                         },
235                         body => $html_body,
236                 ),
237                 Email::MIME->create(
238                         attributes => {
239                                 content_type => 'text/plain',
240                                 encoding => 'quoted-printable',
241                         },
242                         body => 'hi = "bye"',
243                 )
244         ];
245         $mid = 'multipart-html-sucks@11';
246         $mime = Email::MIME->create(
247                 header_str => [
248                   From => 'a@example.com',
249                   Subject => 'blah',
250                   Cc => $addr,
251                   'Message-ID' => "<$mid>",
252                   'Content-Type' => 'multipart/alternative',
253                 ],
254                 parts => $parts,
255         );
256
257         {
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');
265         }
266 }
267
268 # List-ID based delivery
269 {
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>
280 Cc: $addr
281 Message-ID: <$mid>
282 List-Id: <$list_id>
283 Subject: this message will be trained as spam
284 Date: Thu, 01 Jan 1970 00:00:00 +0000
285
286 EOF
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');
295
296         # try a message w/o precheck
297         $simple = Email::Simple->new(<<EOF);
298 To: You <you\@example.com>
299 List-Id: <$list_id>
300
301 this message would not be accepted without --no-precheck
302 EOF
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');
311
312         # try a message with multiple List-ID headers
313         $in = <<EOF;
314 List-ID: <foo.bar>
315 List-ID: <$list_id>
316 Message-ID: <2lids\@example>
317 Subject: two List-IDs
318 From: user <user\@example.com>
319 To: $addr
320 Date: Fri, 02 Oct 1993 00:00:00 +0000
321
322 EOF
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');
330 }
331
332 done_testing();
333
334 sub fail_bad_header {
335         my ($good_rev, $msg, $in) = @_;
336         my @f = glob("$faildir/*/*");
337         unlink @f if @f;
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));
344         chomp $rev;
345         is($rev, $good_rev, "bad revision not commited ($msg)");
346         @f = glob("$faildir/*/*");
347         is(scalar @f, 1, "faildir written to");
348         [ $in, $out, $err ];
349 }