Content filtering preserves/forwards the original message.
[mailman.git] / src / mailman / handlers / tests / test_mimedel.py
blob1e0db94b49938a76efd078e7028883f5d98fe5cf
1 # Copyright (C) 2012-2023 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <https://www.gnu.org/licenses/>.
18 """Test the mime_delete handler."""
20 import os
21 import sys
22 import email
23 import shutil
24 import tempfile
25 import unittest
27 from contextlib import contextmanager, ExitStack
28 from importlib_resources import open_binary as resource_open, read_text
29 from io import StringIO
30 from mailman.app.lifecycle import create_list
31 from mailman.config import config
32 from mailman.handlers import mime_delete
33 from mailman.interfaces.action import FilterAction
34 from mailman.interfaces.member import MemberRole
35 from mailman.interfaces.pipeline import DiscardMessage, RejectMessage
36 from mailman.interfaces.usermanager import IUserManager
37 from mailman.testing.helpers import (
38 configuration,
39 get_queue_messages,
40 LogFileMark,
41 specialized_message_from_string as mfs,
43 from mailman.testing.layers import ConfigLayer
44 from unittest.mock import patch
45 from zope.component import getUtility
48 @contextmanager
49 def dummy_script(arg=''):
50 exe = sys.executable
51 non_ascii = ''
52 report = 'no'
53 if arg == 'non-ascii':
54 non_ascii = '‘...’'
55 extra = ''
56 if arg == 'scripterr':
57 extra = 'error'
58 if arg == 'report':
59 report = 'yes'
60 with ExitStack() as resources:
61 tempdir = tempfile.mkdtemp()
62 resources.callback(shutil.rmtree, tempdir)
63 filter_path = os.path.join(tempdir, 'filter.py')
64 if arg in ('noperm', 'nonexist'):
65 exe = filter_path
66 with open(filter_path, 'w', encoding='utf-8') as fp:
67 print("""\
68 import sys
69 if len(sys.argv) > 2:
70 sys.exit(1)
71 print('Converted text/html to text/plain{}')
72 print('Filename:', sys.argv[1])
73 print(open(sys.argv[1]).readlines()[0])
74 """.format(non_ascii), file=fp)
75 config.push('dummy script', """\
76 [mailman]
77 html_to_plain_text_command = {exe} {script} {extra} $filename
78 filter_report = {report}
79 """.format(exe=exe, script=filter_path, extra=extra, report=report))
81 resources.callback(config.pop, 'dummy script')
82 if arg == 'nonexist':
83 os.rename(filter_path, filter_path + 'xxx')
84 elif arg == 'noperm':
85 os.chmod(filter_path, 0o644)
86 yield
89 class TestDispose(unittest.TestCase):
90 """Test the mime_delete handler."""
92 layer = ConfigLayer
93 maxxDiff = None
95 def setUp(self):
96 self._mlist = create_list('test@example.com')
97 self._msg = mfs("""\
98 From: anne@example.com
99 To: test@example.com
100 Subject: A disposable message
101 Message-ID: <ant>
103 """)
104 config.push('dispose', """
105 [mailman]
106 site_owner: noreply@example.com
107 """)
108 self.addCleanup(config.pop, 'dispose')
110 def test_dispose_discard(self):
111 self._mlist.filter_action = FilterAction.discard
112 with self.assertRaises(DiscardMessage) as cm:
113 mime_delete.dispose(self._mlist, self._msg, {}, 'discarding')
114 self.assertEqual(cm.exception.message, 'discarding')
115 # There should be no messages in the 'bad' queue.
116 get_queue_messages('bad', expected_count=0)
118 def test_dispose_discard_no_spurious_log(self):
119 self._mlist.filter_action = FilterAction.discard
120 mark = LogFileMark('mailman.error')
121 with self.assertRaises(DiscardMessage):
122 mime_delete.dispose(self._mlist, self._msg, {}, 'discarding')
123 self.assertEqual(mark.readline(), '')
125 def test_dispose_bounce(self):
126 self._mlist.filter_action = FilterAction.reject
127 with self.assertRaises(RejectMessage) as cm:
128 mime_delete.dispose(self._mlist, self._msg, {}, 'rejecting')
129 self.assertEqual(cm.exception.message, 'rejecting')
130 # There should be no messages in the 'bad' queue.
131 get_queue_messages('bad', expected_count=0)
133 def test_dispose_forward(self):
134 # The disposed message gets forwarded to the list administrators. So
135 # first add an owner and a moderator.
136 user_manager = getUtility(IUserManager)
137 anne = user_manager.create_address('anne@example.com')
138 bart = user_manager.create_address('bart@example.com')
139 self._mlist.subscribe(anne, MemberRole.owner)
140 self._mlist.subscribe(bart, MemberRole.moderator)
141 # Now set the filter action and dispose the message.
142 self._mlist.filter_action = FilterAction.forward
143 with self.assertRaises(DiscardMessage) as cm:
144 mime_delete.dispose(self._mlist, self._msg, {}, 'forwarding')
145 self.assertEqual(cm.exception.message, 'forwarding')
146 # There should now be a multipart message in the virgin queue destined
147 # for the mailing list owners.
148 items = get_queue_messages('virgin', expected_count=1)
149 message = items[0].msg
150 self.assertEqual(message.get_content_type(), 'multipart/mixed')
151 # Anne and Bart should be recipients of the message, but it will look
152 # like the message is going to the list owners.
153 self.assertEqual(message['to'], 'test-owner@example.com')
154 self.assertEqual(message.recipients,
155 set(['anne@example.com', 'bart@example.com']))
156 # The list owner should be the sender.
157 self.assertEqual(message['from'], 'noreply@example.com')
158 self.assertEqual(message['subject'],
159 'Content filter message notification')
160 # The body of the first part provides the moderators some details.
161 part0 = message.get_payload(0)
162 self.assertEqual(part0.get_content_type(), 'text/plain')
163 self.assertMultiLineEqual(part0.get_payload(), """\
164 The attached message matched the Test mailing list's content
165 filtering rules and was prevented from being forwarded on to the list
166 membership. You are receiving the only remaining copy of the discarded
167 message.
169 """)
170 # The second part is the container for the original message.
171 part1 = message.get_payload(1)
172 self.assertEqual(part1.get_content_type(), 'message/rfc822')
173 # And the first part of *that* message will be the original message.
174 original = part1.get_payload(0)
175 self.assertEqual(original['subject'], 'A disposable message')
176 self.assertEqual(original['message-id'], '<ant>')
178 @configuration('mailman', filtered_messages_are_preservable='no')
179 def test_dispose_non_preservable(self):
180 # Two actions can happen here, depending on a site-wide setting. If
181 # the site owner has indicated that filtered messages cannot be
182 # preserved, then this is the same as discarding them.
183 self._mlist.filter_action = FilterAction.preserve
184 with self.assertRaises(DiscardMessage) as cm:
185 mime_delete.dispose(self._mlist, self._msg, {}, 'not preserved')
186 self.assertEqual(cm.exception.message, 'not preserved')
187 # There should be no messages in the 'bad' queue.
188 get_queue_messages('bad', expected_count=0)
190 @configuration('mailman', filtered_messages_are_preservable='yes')
191 def test_dispose_preservable(self):
192 # Two actions can happen here, depending on a site-wide setting. If
193 # the site owner has indicated that filtered messages can be
194 # preserved, then this is similar to discarding the message except
195 # that a copy is preserved in the 'bad' queue.
196 self._mlist.filter_action = FilterAction.preserve
197 with self.assertRaises(DiscardMessage) as cm:
198 mime_delete.dispose(self._mlist, self._msg, {}, 'preserved')
199 self.assertEqual(cm.exception.message, 'preserved')
200 # There should be a message in the 'bad' queue.
201 items = get_queue_messages('bad', expected_count=1)
202 message = items[0].msg
203 self.assertEqual(message['subject'], 'A disposable message')
204 self.assertEqual(message['message-id'], '<ant>')
206 @configuration('mailman', filtered_messages_are_preservable='yes')
207 def test_preserved_message_has_content(self):
208 msg = mfs("""\
209 From: anne@example.com
210 To: test@example.com
211 Subject: Testing preserved message has content
212 Message-ID: <ant>
213 MIME-Version: 1.0
214 Content-Type: multipart/mixed; boundary="AAAA"
216 --AAAA
217 Content-Type: text/bogus; charset="utf-8"
218 Content-Transfer-Encoding: quoted-printable
220 Let=E2=80=99s also consider
222 --AAAA
223 Content-Type: text/other; charset="utf-8"
224 Content-Transfer-Encoding: quoted-printable
226 Let=E2=80=99s also consider
228 --AAAA--
229 """)
230 self._mlist.filter_content = True
231 self._mlist.filter_action = FilterAction.preserve
232 self._mlist.pass_types = ['multipart', 'text/plain']
233 with self.assertRaises(DiscardMessage) as cm:
234 mime_delete.process(self._mlist, msg, {})
235 self.assertEqual(cm.exception.message,
236 'After content filtering, the message was empty')
237 # There should be a message in the 'bad' queue.
238 items = get_queue_messages('bad', expected_count=1)
239 message = items[0].msg
240 self.assertEqual(message['subject'],
241 'Testing preserved message has content')
242 self.assertEqual(message['message-id'], '<ant>')
243 # The message should be multipart with two subparts.
244 self.assertEqual(len(message.get_payload()), 2)
246 def test_bad_action(self):
247 # This should never happen, but what if it does?
248 # FilterAction.accept, FilterAction.hold, and FilterAction.defer are
249 # not valid. They are treated as discard actions, but the problem is
250 # also logged.
251 for action in (FilterAction.accept,
252 FilterAction.hold,
253 FilterAction.defer):
254 self._mlist.filter_action = action
255 mark = LogFileMark('mailman.error')
256 with self.assertRaises(DiscardMessage) as cm:
257 mime_delete.dispose(self._mlist, self._msg, {}, 'bad action')
258 self.assertEqual(cm.exception.message, 'bad action')
259 line = mark.readline()[:-1]
260 self.assertTrue(line.endswith(
261 'test@example.com invalid FilterAction: {}. '
262 'Treating as discard'.format(action.name)))
265 class TestHTMLFilter(unittest.TestCase):
266 """Test the conversion of HTML to plaintext."""
268 layer = ConfigLayer
270 def setUp(self):
271 self._mlist = create_list('test@example.com')
272 self._mlist.convert_html_to_plaintext = True
273 self._mlist.filter_content = True
275 def test_convert_html_to_plaintext(self):
276 # Converting to plain text calls a command line script.
277 msg = mfs("""\
278 From: aperson@example.com
279 Content-Type: text/html
280 MIME-Version: 1.0
282 <html><head></head>
283 <body></body></html>
284 """)
285 process = config.handlers['mime-delete'].process
286 with dummy_script():
287 process(self._mlist, msg, {})
288 self.assertEqual(msg.get_content_type(), 'text/plain')
289 self.assertTrue(
290 msg['x-content-filtered-by'].startswith('Mailman/MimeDel'))
291 payload_lines = msg.get_payload().splitlines()
292 self.assertEqual(payload_lines[0], 'Converted text/html to text/plain')
294 def test_convert_html_to_plaintext_base64(self):
295 # Converting to plain text calls a command line script with decoded
296 # message body.
297 msg = mfs("""\
298 From: aperson@example.com
299 Content-Type: text/html
300 Content-Transfer-Encoding: base64
301 MIME-Version: 1.0
303 PGh0bWw+PGhlYWQ+PC9oZWFkPgo8Ym9keT48L2JvZHk+PC9odG1sPgo=
304 """)
305 process = config.handlers['mime-delete'].process
306 with dummy_script():
307 process(self._mlist, msg, {})
308 self.assertEqual(msg.get_content_type(), 'text/plain')
309 self.assertTrue(
310 msg['x-content-filtered-by'].startswith('Mailman/MimeDel'))
311 payload_lines = msg.get_payload().splitlines()
312 self.assertEqual(payload_lines[0], 'Converted text/html to text/plain')
313 self.assertEqual(payload_lines[2], '<html><head></head>')
315 def test_convert_html_to_plaintext_encodes_new_payload(self):
316 # Test that the converted payload with non-ascii is encoded.
317 msg = mfs("""\
318 From: aperson@example.com
319 Content-Type: text/html; charset=utf-8
320 Content-Transfer-Encoding: base64
321 MIME-Version: 1.0
323 Q29udmVydGVkIHRleHQvaHRtbCB0byB0ZXh0L3BsYWlu4oCYLi4u4oCZCg==
324 """)
325 process = config.handlers['mime-delete'].process
326 with dummy_script('non-ascii'):
327 process(self._mlist, msg, {})
328 self.assertEqual(msg['content-type'], 'text/plain; charset="utf-8"')
329 self.assertEqual(msg['content-transfer-encoding'], 'base64')
330 self.assertTrue(
331 msg['x-content-filtered-by'].startswith('Mailman/MimeDel'))
332 payload_lines = (
333 msg.get_payload(decode=True).decode('utf-8').splitlines())
334 self.assertEqual(payload_lines[0],
335 'Converted text/html to text/plain‘...’')
336 self.assertTrue(payload_lines[1].startswith('Filename'))
338 def test_convert_html_to_plaintext_error_return(self):
339 # Calling a script which returns an error status is properly logged.
340 msg = mfs("""\
341 From: aperson@example.com
342 Content-Type: text/html
343 MIME-Version: 1.0
345 <html><head></head>
346 <body></body></html>
347 """)
348 process = config.handlers['mime-delete'].process
349 mark = LogFileMark('mailman.error')
350 with dummy_script('scripterr'):
351 process(self._mlist, msg, {})
352 line = mark.readline()[:-1]
353 self.assertTrue(line.endswith('HTML -> text/plain command error'))
354 self.assertEqual(msg.get_content_type(), 'text/html')
355 self.assertIsNone(msg['x-content-filtered-by'])
356 payload_lines = msg.get_payload().splitlines()
357 self.assertEqual(payload_lines[0], '<html><head></head>')
359 def test_missing_html_to_plain_text_command(self):
360 # Calling a missing html_to_plain_text_command is properly logged.
361 msg = mfs("""\
362 From: aperson@example.com
363 Content-Type: text/html
364 MIME-Version: 1.0
366 <html><head></head>
367 <body></body></html>
368 """)
369 process = config.handlers['mime-delete'].process
370 mark = LogFileMark('mailman.error')
371 with dummy_script('nonexist'):
372 process(self._mlist, msg, {})
373 line = mark.readline()[:-1]
374 self.assertTrue(line.endswith('HTML -> text/plain command error'))
375 self.assertEqual(msg.get_content_type(), 'text/html')
376 self.assertIsNone(msg['x-content-filtered-by'])
377 payload_lines = msg.get_payload().splitlines()
378 self.assertEqual(payload_lines[0], '<html><head></head>')
380 def test_no_permission_html_to_plain_text_command(self):
381 # Calling an html_to_plain_text_command without permission is
382 # properly logged.
383 msg = mfs("""\
384 From: aperson@example.com
385 Content-Type: text/html
386 MIME-Version: 1.0
388 <html><head></head>
389 <body></body></html>
390 """)
391 process = config.handlers['mime-delete'].process
392 mark = LogFileMark('mailman.error')
393 with dummy_script('noperm'):
394 process(self._mlist, msg, {})
395 line = mark.readline()[:-1]
396 self.assertTrue(line.endswith('HTML -> text/plain command error'))
397 self.assertEqual(msg.get_content_type(), 'text/html')
398 self.assertIsNone(msg['x-content-filtered-by'])
399 payload_lines = msg.get_payload().splitlines()
400 self.assertEqual(payload_lines[0], '<html><head></head>')
402 def test_html_part_with_non_ascii(self):
403 # Ensure we can convert HTML to plain text in an HTML sub-part which
404 # contains non-ascii.
405 with resource_open(
406 'mailman.handlers.tests.data',
407 'html_to_plain.eml') as fp:
408 msg = email.message_from_binary_file(fp)
409 process = config.handlers['mime-delete'].process
410 with dummy_script():
411 process(self._mlist, msg, {})
412 part = msg.get_payload(1)
413 cset = part.get_content_charset('us-ascii')
414 text = part.get_payload(decode=True).decode(cset).splitlines()
415 self.assertEqual(text[0], 'Converted text/html to text/plain')
416 self.assertEqual(text[2], 'Um frühere Nachrichten')
419 class TestMiscellaneous(unittest.TestCase):
420 """Test various miscellaneous filtering actions."""
422 layer = ConfigLayer
423 maxDiff = None
425 def setUp(self):
426 self._mlist = create_list('test@example.com')
427 self._mlist.collapse_alternatives = True
428 self._mlist.filter_content = True
429 self._mlist.filter_extensions = ['xlsx']
431 def test_collapse_alternatives(self):
432 with resource_open(
433 'mailman.handlers.tests.data',
434 'collapse_alternatives.eml') as fp:
435 msg = email.message_from_binary_file(fp)
436 process = config.handlers['mime-delete'].process
437 process(self._mlist, msg, {})
438 structure = StringIO()
439 email.iterators._structure(msg, fp=structure)
440 self.assertEqual(structure.getvalue(), """\
441 multipart/signed
442 multipart/mixed
443 text/plain
444 text/plain
445 application/pgp-signature
446 """)
448 def test_collapse_alternatives_non_ascii(self):
449 # Ensure we can flatten as bytes a message whose non-ascii payload
450 # has been reset.
451 with resource_open(
452 'mailman.handlers.tests.data',
453 'c_a_non_ascii.eml') as fp:
454 msg = email.message_from_binary_file(fp)
455 process = config.handlers['mime-delete'].process
456 process(self._mlist, msg, {})
457 self.assertFalse(msg.is_multipart())
458 self.assertEqual(msg.get_payload(decode=True),
459 b'Body with non-ascii can\xe2\x80\x99t see '
460 b'won\xe2\x80\x99t know\n')
461 # Ensure we can flatten it.
462 dummy = msg.as_bytes() # noqa: F841
464 def test_collapse_alternatives_non_ascii_encoded(self):
465 msg = mfs("""\
466 From: anne@example.com
467 To: test@example.com
468 Subject: Testing mpa with transfer encoded subparts
469 Message-ID: <ant>
470 MIME-Version: 1.0
471 Content-Type: multipart/alternative; boundary="AAAA"
473 --AAAA
474 Content-Type: text/plain; charset="utf-8"
475 Content-Transfer-Encoding: quoted-printable
477 Let=E2=80=99s also consider
479 --AAAA
480 Content-Type: text/html; charset="utf-8"
481 Content-Transfer-Encoding: quoted-printable
483 Let=E2=80=99s also consider
485 --AAAA--
486 """)
487 process = config.handlers['mime-delete'].process
488 process(self._mlist, msg, {})
489 self.assertFalse(msg.is_multipart())
490 self.assertEqual(msg.get_payload(decode=True),
491 b'Let\xe2\x80\x99s also consider\n')
492 # Ensure we can flatten it.
493 dummy = msg.as_bytes() # noqa: F841
495 def test_reset_payload_multipart(self):
496 msg = mfs("""\
497 From: anne@example.com
498 To: test@example.com
499 Subject: Testing mpa with multipart subparts
500 Message-ID: <ant>
501 MIME-Version: 1.0
502 Content-Type: multipart/alternative; boundary="AAAA"
504 --AAAA
505 Content-Type: multipart/mixed; boundary="BBBB"
507 --BBBB
508 Content-Type: text/plain
510 Part 1
512 --BBBB
513 Content-Type: text/plain
515 Part 2
517 --BBBB--
519 --AAAA
520 Content-Type: multipart/mixed; boundary="CCCC"
522 --CCCC
523 Content-Type: text/html
525 Part 3
527 --CCCC
528 Content-Type: text/html
530 Part 4
532 --CCCC--
534 --AAAA--
535 """)
536 process = config.handlers['mime-delete'].process
537 process(self._mlist, msg, {})
538 self.assertTrue(msg.is_multipart())
539 self.assertEqual(msg.get_content_type(), 'multipart/mixed')
540 self.assertEqual(len(msg.get_payload()), 2)
541 self.assertEqual(msg.get_payload(0).get_payload(), 'Part 1\n')
542 self.assertEqual(msg.get_payload(1).get_payload(), 'Part 2\n')
544 def test_msg_rfc822(self):
545 with resource_open(
546 'mailman.handlers.tests.data', 'msg_rfc822.eml') as fp:
547 msg = email.message_from_binary_file(fp)
548 process = config.handlers['mime-delete'].process
549 # Mock this so that the X-Content-Filtered-By header isn't sensitive to
550 # Mailman version bumps.
551 with patch('mailman.handlers.mime_delete.VERSION', '123'):
552 expected_msg = read_text(
553 'mailman.handlers.tests.data', 'msg_rfc822_out.eml')
554 process(self._mlist, msg, {})
555 self.assertEqual(msg.as_string(), expected_msg)
557 def test_mixed_case_ext_and_recast(self):
558 msg = mfs("""\
559 From: anne@example.com
560 To: test@example.com
561 Subject: Testing mixed extension
562 Message-ID: <ant>
563 MIME-Version: 1.0
564 Content-Type: multipart/mixed; boundary="AAAA"
566 --AAAA
567 Content-Type: text/plain; charset="utf-8"
569 Plain text
571 --AAAA
572 Content-Type: application/octet-stream; name="test.xlsX"
573 Content-Disposition: attachment; filename="test.xlsX"
575 spreadsheet
577 --AAAA--
578 """)
579 process = config.handlers['mime-delete'].process
580 process(self._mlist, msg, {})
581 self.assertEqual(msg['content-type'], 'text/plain; charset="utf-8"')
582 self.assertEqual(msg.get_payload(decode=True), b"""\
583 Plain text
584 """)
586 def test_report(self):
587 # Hit all the pass and filter conditions for reporting
588 self._mlist.pass_extensions = ['txt']
589 self._mlist.filter_types = ['image']
590 self._mlist.pass_types = ['text', 'application', 'multipart']
591 msg = mfs("""\
592 From: anne@example.com
593 To: test@example.com
594 Subject: Testing mixed extension
595 Message-ID: <ant>
596 MIME-Version: 1.0
597 Content-Type: multipart/mixed; boundary="AAAA"
599 --AAAA
600 Content-Type: multipart/alternative; boundary="BBBB"
602 --BBBB
603 Content-Type: text/plain; charset="utf-8"
605 Plain text
607 --BBBB
608 Content-Type: text/html; charset="utf-8"
610 HTML text
612 --BBBB--
613 --AAAA
614 Content-Type: application/octet-stream; name="test.xlsX"
615 Content-Disposition: attachment; filename="test.xlsX"
617 spreadsheet
619 --AAAA
620 Content-Type: application/octet-stream; name="test.exe"
621 Content-Disposition: attachment; filename="test.exe"
623 executable
625 --AAAA
626 Content-Type: image/jpeg; name="My_image"
627 Content-Disposition: inline; filename="My_image"
629 image
631 --AAAA
632 Content-Type: video/mp4; name="My_video"
633 Content-Disposition: inline; filename="My_video"
635 video
637 --AAAA--
638 """)
639 process = config.handlers['mime-delete'].process
640 with dummy_script('report'):
641 process(self._mlist, msg, {})
642 self.assertEqual(msg['content-type'], 'text/plain; charset="utf-8"')
643 self.assertEqual(msg.get_payload(decode=True), b"""\
644 Plain text
646 ___________________________________________
647 Mailman's content filtering has removed the
648 following MIME parts from this message.
650 Content-Type: application/octet-stream
651 Name: test.xlsX
653 Content-Type: application/octet-stream
654 Name: test.exe
656 Content-Type: image/jpeg
657 Name: My_image
659 Content-Type: video/mp4
660 Name: My_video
662 Replaced multipart/alternative part with first alternative.
663 """)
665 def test_report_mixed(self):
666 # Hit all the pass and filter conditions for reporting
667 self._mlist.pass_extensions = ['txt']
668 self._mlist.pass_types = ['text', 'application', 'multipart', 'image']
669 msg = mfs("""\
670 From: anne@example.com
671 To: test@example.com
672 Subject: Testing mixed extension
673 Message-ID: <ant>
674 MIME-Version: 1.0
675 Content-Type: multipart/mixed; boundary="AAAA"
677 --AAAA
678 Content-Type: multipart/alternative; boundary="BBBB"
680 --BBBB
681 Content-Type: text/plain; charset="utf-8"
683 Plain text
685 --BBBB
686 Content-Type: text/html; charset="utf-8"
688 HTML text
690 --BBBB--
691 --AAAA
692 Content-Type: application/octet-stream; name="test.xlsX"
693 Content-Disposition: attachment; filename="test.xlsX"
695 spreadsheet
697 --AAAA
698 Content-Type: application/octet-stream; name="test.exe"
699 Content-Disposition: attachment; filename="test.exe"
701 executable
703 --AAAA
704 Content-Type: image/jpeg; name="My_image"
705 Content-Disposition: inline; filename="My_image"
707 image
709 --AAAA
710 Content-Type: video/mp4; name="My_video"
711 Content-Disposition: inline; filename="My_video"
713 video
715 --AAAA--
716 """)
717 process = config.handlers['mime-delete'].process
718 with dummy_script('report'):
719 process(self._mlist, msg, {})
720 self.assertIn('multipart/mixed', msg['content-type'])
721 self.assertEqual(len(msg.get_payload()), 3)
722 self.assertIn('text/plain', msg.get_payload(0).get_content_type())
723 self.assertIn('image/jpeg', msg.get_payload(1).get_content_type())
724 self.assertEqual(msg.get_payload(2).get_payload(decode=True), b"""\
726 ___________________________________________
727 Mailman's content filtering has removed the
728 following MIME parts from this message.
730 Content-Type: application/octet-stream
731 Name: test.xlsX
733 Content-Type: application/octet-stream
734 Name: test.exe
736 Content-Type: video/mp4
737 Name: My_video
739 Replaced multipart/alternative part with first alternative.
740 """)
742 def test_report_other_multi(self):
743 # Hit all the pass and filter conditions for reporting
744 self._mlist.pass_extensions = ['txt']
745 self._mlist.pass_types = ['text', 'application', 'multipart']
746 msg = mfs("""\
747 From: anne@example.com
748 To: test@example.com
749 Subject: Testing mixed extension
750 Message-ID: <ant>
751 MIME-Version: 1.0
752 Content-Type: multipart/signed; boundary="AAAA"
754 --AAAA
755 Content-Type: multipart/alternative; boundary="BBBB"
757 --BBBB
758 Content-Type: text/plain; charset="utf-8"
760 Plain text
762 --BBBB
763 Content-Type: application/pkcs7-signature
765 HTML text
767 --BBBB--
768 --AAAA
769 Content-Type: application/pkcs7-signature
771 Signature
772 --AAAA
773 Content-Type: application/octet-stream; name="test.xlsX"
774 Content-Disposition: attachment; filename="test.xlsX"
776 spreadsheet
778 --AAAA--
779 """)
780 process = config.handlers['mime-delete'].process
781 with dummy_script('report'):
782 process(self._mlist, msg, {})
783 self.assertIn('multipart/mixed', msg['content-type'])
784 self.assertEqual(len(msg.get_payload()), 2)
785 self.assertIn('multipart/signed',
786 msg.get_payload(0).get_content_type())
787 self.assertIn('text/plain', msg.get_payload(1).get_content_type())
788 self.assertEqual(msg.get_payload(1).get_payload(decode=True), b"""\
790 ___________________________________________
791 Mailman's content filtering has removed the
792 following MIME parts from this message.
794 Content-Type: application/octet-stream
795 Name: test.xlsX
797 Replaced multipart/alternative part with first alternative.
798 """)