1 # Copyright (C) 2013, Eric Wong <normalperson@yhbt.net> and all contributors
2 # License: AGPLv3 or later (https://www.gnu.org/licenses/agpl-3.0.txt)
8 use PublicInbox::Filter;
10 sub count_body_parts {
11 my ($bodies, $part) = @_;
12 my $body = $part->body_raw;
15 $bodies->{$body} ||= 0;
19 # plain-text email is passed through unchanged
21 my $s = Email::Simple->create(
23 From => 'a@example.com',
24 To => 'b@example.com',
25 'Content-Type' => 'text/plain',
26 Subject => 'this is a subject',
28 body => "hello world\n",
30 my $f = Email::Filter->new(data => $s->as_string);
31 is(1, PublicInbox::Filter->run($f->simple), "run was a success");
32 is($s->as_string, $f->simple->as_string, "plain email unchanged");
35 # convert single-part HTML to plain-text
37 my $s = Email::Simple->create(
39 From => 'a@example.com',
40 To => 'b@example.com',
41 'Content-Type' => 'text/html',
42 Subject => 'HTML only badness',
44 body => "<html><body>bad body</body></html>\n",
46 my $f = Email::Filter->new(data => $s->as_string);
47 is(1, PublicInbox::Filter->run($f->simple), "run was a success");
48 unlike($f->simple->as_string, qr/<html>/, "HTML removed");
49 is("text/plain", $f->simple->header("Content-Type"),
50 "content-type changed");
51 like($f->simple->body, qr/\A\s*bad body\s*\z/, "body");
52 like($f->simple->header("X-Content-Filtered-By"),
53 qr/PublicInbox::Filter/, "XCFB header added");
56 # multipart/alternative: HTML and plain-text, keep the plain-text
58 my $html_body = "<html><body>hi</body></html>";
62 content_type => 'text/html; charset=UTF-8',
69 content_type => 'text/plain',
74 my $email = Email::MIME->create(
76 From => 'a@example.com',
78 'Content-Type' => 'multipart/alternative'
82 my $f = Email::Filter->new(data => $email->as_string);
83 is(1, PublicInbox::Filter->run($f->simple), "run was a success");
84 my $parsed = Email::MIME->new($f->simple->as_string);
85 is("text/plain", $parsed->header("Content-Type"));
86 is(scalar $parsed->parts, 1, "HTML part removed");
88 $parsed->walk_parts(sub {
90 return if $part->subparts; # walk_parts already recurses
91 count_body_parts(\%bodies, $part);
93 is(scalar keys %bodies, 1, "one bodies");
94 is($bodies{"hi"}, 1, "plain text part unchanged");
97 # multi-part plain-text-only
101 attributes => { content_type => 'text/plain', },
105 attributes => { content_type => 'text/plain', },
109 my $email = Email::MIME->create(
110 header_str => [ From => 'a@example.com', Subject => 'blah' ],
113 my $f = Email::Filter->new(data => $email->as_string);
114 is(1, PublicInbox::Filter->run($f->simple), "run was a success");
115 my $parsed = Email::MIME->new($f->simple->as_string);
116 is(scalar $parsed->parts, 2, "still 2 parts");
118 $parsed->walk_parts(sub {
120 return if $part->subparts; # walk_parts already recurses
121 count_body_parts(\%bodies, $part);
123 is(scalar keys %bodies, 2, "two bodies");
124 is($bodies{"bye"}, 1, "bye part exists");
125 is($bodies{"hi"}, 1, "hi part exists");
126 is($parsed->header("X-Content-Filtered-By"), undef,
127 "XCFB header unset");
130 # multi-part HTML, several HTML parts
135 content_type => 'text/html',
136 encoding => 'base64',
138 body => '<html><body>b64 body</body></html>',
142 content_type => 'text/html',
143 encoding => 'quoted-printable',
145 body => '<html><body>qp body</body></html>',
148 my $email = Email::MIME->create(
149 header_str => [ From => 'a@example.com', Subject => 'blah' ],
152 my $f = Email::Filter->new(data => $email->as_string);
153 is(1, PublicInbox::Filter->run($f->simple), "run was a success");
154 my $parsed = Email::MIME->new($f->simple->as_string);
155 is(scalar $parsed->parts, 2, "still 2 parts");
157 $parsed->walk_parts(sub {
159 return if $part->subparts; # walk_parts already recurses
160 count_body_parts(\%bodies, $part);
162 is(scalar keys %bodies, 2, "two body parts");
163 is($bodies{"b64 body"}, 1, "base64 part converted");
164 is($bodies{"qp body"}, 1, "qp part converted");
165 like($parsed->header("X-Content-Filtered-By"), qr/PublicInbox::Filter/,
166 "XCFB header added");
169 # plain-text with image attachments, kill images
173 attributes => { content_type => 'text/plain' },
178 content_type => 'image/jpeg',
179 filename => 'scary.jpg',
180 encoding => 'base64',
185 my $email = Email::MIME->create(
186 header_str => [ From => 'a@example.com', Subject => 'blah' ],
189 my $f = Email::Filter->new(data => $email->as_string);
190 is(1, PublicInbox::Filter->run($f->simple), "run was a success");
191 my $parsed = Email::MIME->new($f->simple->as_string);
192 is(scalar $parsed->parts, 1, "image part removed");
194 $parsed->walk_parts(sub {
196 return if $part->subparts; # walk_parts already recurses
197 count_body_parts(\%bodies, $part);
199 is(scalar keys %bodies, 1, "one body");
200 is($bodies{'see image'}, 1, 'original body exists');
201 like($parsed->header("X-Content-Filtered-By"), qr/PublicInbox::Filter/,
202 "XCFB header added");
210 content_type => 'image/jpeg',
211 filename => 'scary.jpg',
212 encoding => 'base64',
218 content_type => 'text/plain',
219 filename => 'scary.exe',
220 encoding => 'base64',
225 my $email = Email::MIME->create(
226 header_str => [ From => 'a@example.com', Subject => 'blah' ],
229 my $f = Email::Filter->new(data => $email->as_string);
230 is(0, PublicInbox::Filter->run($f->simple),
231 "run signaled to stop delivery");
232 my $parsed = Email::MIME->new($f->simple->as_string);
233 is(scalar $parsed->parts, 1, "bad parts removed");
235 $parsed->walk_parts(sub {
237 return if $part->subparts; # walk_parts already recurses
238 count_body_parts(\%bodies, $part);
240 is(scalar keys %bodies, 1, "one body");
241 is($bodies{"all attachments scrubbed by PublicInbox::Filter"}, 1,
242 "attachment scrubber left its mark");
243 like($parsed->header("X-Content-Filtered-By"), qr/PublicInbox::Filter/,
244 "XCFB header added");
248 my $s = Email::Simple->create(
250 From => 'a@example.com',
251 To => 'b@example.com',
252 'Content-Type' => 'test/pain',
253 Subject => 'this is a subject',
255 body => "hello world\n",
257 my $f = Email::Filter->new(data => $s->as_string);
258 is(0, PublicInbox::Filter->run($f->simple), "run was a failure");
259 like($f->simple->as_string, qr/scrubbed/, "scrubbed message");
263 my $s = Email::Simple->create(
265 From => 'a@example.com',
266 To => 'b@example.com',
267 'Content-Type' => 'text/plain',
268 'Mail-Followup-To' => 'c@example.com',
269 Subject => 'mfttest',
274 is('c@example.com', $s->header("Mail-Followup-To"),
275 "mft set correctly");
276 my $f = Email::Filter->new(data => $s->as_string);
277 is(1, PublicInbox::Filter->run($f->simple), "run succeeded for mft");
278 is(undef, $f->simple->header("Mail-Followup-To"), "mft stripped");