def test_confirm_with_no_command_in_utf8_body(self): # Clear out the virgin queue so that the test below only sees the # reply to the confirmation message. get_queue_messages('virgin') subject = 'Re: confirm {0}'.format(self._token) to = 'test-confirm+{0}@example.com'.format(self._token) msg = mfs("""\ From: Anne Person <*****@*****.**> MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: quoted-printable Franziskanerstra=C3=9Fe """) msg['Subject'] = subject msg['To'] = to self._commandq.enqueue(msg, dict(listid='test.example.com')) self._runner.run() # Anne is now a confirmed member so her user record and email address # should exist in the database. manager = getUtility(IUserManager) user = manager.get_user('*****@*****.**') address = list(user.addresses)[0] self.assertEqual(address.email, '*****@*****.**') self.assertEqual(address.verified_on, datetime(2005, 8, 1, 7, 49, 23)) address = manager.get_address('*****@*****.**') self.assertEqual(address.email, '*****@*****.**') messages = get_queue_messages('virgin') self.assertEqual(len(messages), 1) self.assertEqual(messages[0].msgdata['recipients'], set(['*****@*****.**']))
def setUp(self): self._mlist = create_list('*****@*****.**') self._member = add_member(self._mlist, '*****@*****.**', 'Anne Person', 'xxx', DeliveryMode.regular, 'en') self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: You bounced Message-ID: <first> """) # Set up the translation context. self._var_dir = tempfile.mkdtemp() xx_template_path = os.path.join( self._var_dir, 'templates', 'site', 'xx', 'probe.txt') os.makedirs(os.path.dirname(xx_template_path)) config.push('xx template dir', """\ [paths.testing] var_dir: {0} """.format(self._var_dir)) language_manager = getUtility(ILanguageManager) language_manager.add('xx', 'utf-8', 'Freedonia') self._member.preferences.preferred_language = 'xx' with open(xx_template_path, 'w') as fp: print("""\ blah blah blah $listname $address $optionsurl $owneraddr """, file=fp) # Let assertMultiLineEqual work without bounds. self.maxDiff = None
def test_send_one_digest_to_missing_fqdn_listname(self): msg = mfs("""\ To: [email protected] From: [email protected] Subject: message 1 """) self._handler.process(self._mlist, msg, {}) del msg['subject'] msg['subject'] = 'message 2' self._handler.process(self._mlist, msg, {}) # There are no digests already being sent, but the ant mailing list # does have a digest mbox collecting messages. get_queue_messages('digest', expected_count=0) mailbox_path = os.path.join(self._mlist.data_path, 'digest.mmdf') self.assertGreater(os.path.getsize(mailbox_path), 0) args = FakeArgs() args.send = True args.lists.append('*****@*****.**') stderr = StringIO() with patch('mailman.commands.cli_digests.sys.stderr', stderr): self._command.process(args) self._runner.run() # The warning was printed to stderr. self.assertEqual(stderr.getvalue(), 'No such list found: [email protected]\n') # And no digest was prepared. self.assertGreater(os.path.getsize(mailbox_path), 0) get_queue_messages('virgin', expected_count=0)
def setUp(self): self._mlist = create_list('*****@*****.**') self._mlist.personalize = Personalization.individual # Make Anne a member of this mailing list. self._anne = add_member(self._mlist, '*****@*****.**', 'Anne Person', 'xyz', DeliveryMode.regular, 'en') # Clear out any results from the previous test. del _deliveries[:] self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: test """) # Set up a personalized footer for decoration. self._template_dir = tempfile.mkdtemp() path = os.path.join(self._template_dir, 'site', 'en', 'member-footer.txt') os.makedirs(os.path.dirname(path)) with open(path, 'w') as fp: print("""\ address : $user_address delivered: $user_delivered_to language : $user_language name : $user_name options : $user_optionsurl """, file=fp) config.push('templates', """ [paths.testing] template_dir: {0} """.format(self._template_dir)) self._mlist.footer_uri = 'mailman:///member-footer.txt' # Let assertMultiLineEqual work without bounds. self.maxDiff = None
def test_return_value(self): msg = mfs("""\ Message-ID: aardvark> """) hash32 = add_message_hash(msg) self.assertEqual(hash32, '5KH3RA7ZM4VM6XOZXA7AST2XN2X4S3WY')
def setUp(self): # Create a fake mailing list and message object. self._msg = mfs("""\ To: [email protected] From: [email protected] Subject: Testing the test list Message-ID: <ant> Message-ID-Hash: MS6QLWERIJLGCRF44J7USBFDELMNT2BW Tests are better than no tests but the water deserves to be swum. """) with transaction(): self._mlist = create_list('*****@*****.**') tempdir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, tempdir) # Here's the command to execute our fake MHonArc process. shutil.copy( resource_filename('mailman.archiving.tests', 'fake_mhonarc.py'), tempdir) self._output_file = os.path.join(tempdir, 'output.txt') command = '{} {} {}'.format( sys.executable, os.path.join(tempdir, 'fake_mhonarc.py'), self._output_file) # Write an external configuration file which points the command at our # fake MHonArc process. self._cfg = os.path.join(tempdir, 'mhonarc.cfg') with open(self._cfg, 'w', encoding='utf-8') as fp: print("""\ [general] base_url: http://$hostname/archives/$fqdn_listname command: {command} """.format(command=command), file=fp)
def test_welcome_message_after_confirmation(self): # Confirmations with a welcome message set. self._mlist.send_welcome_message = True self._mlist.welcome_message_uri = 'mailman:///welcome.txt' # 'confirm' in the Subject and in the To header should not try to # confirm the token twice. # # Clear out the virgin queue so that the test below only sees the # reply to the confirmation message. get_queue_messages('virgin') subject = 'Re: confirm {0}'.format(self._token) to = 'test-confirm+{0}@example.com'.format(self._token) msg = mfs("""\ From: Anne Person <*****@*****.**> """) msg['Subject'] = subject msg['To'] = to self._commandq.enqueue(msg, dict(listid='test.example.com', subaddress='confirm')) self._runner.run() # Now there's a email command notification and a welcome message. All # we care about for this test is the welcome message. messages = get_queue_messages('virgin', sort_on='subject') self.assertEqual(len(messages), 2) message = messages[1].msg self.assertEqual(str(message['subject']), 'Welcome to the "Test" mailing list')
def test_send_digest_to_one_missing_and_one_existing_list(self): msg = mfs("""\ To: [email protected] From: [email protected] Subject: message 1 """) self._handler.process(self._mlist, msg, {}) del msg['subject'] msg['subject'] = 'message 2' self._handler.process(self._mlist, msg, {}) # There are no digests already being sent, but the ant mailing list # does have a digest mbox collecting messages. get_queue_messages('digest', expected_count=0) mailbox_path = os.path.join(self._mlist.data_path, 'digest.mmdf') self.assertGreater(os.path.getsize(mailbox_path), 0) args = FakeArgs() args.send = True args.lists.extend(('ant.example.com', 'bee.example.com')) stderr = StringIO() with patch('mailman.commands.cli_digests.sys.stderr', stderr): self._command.process(args) self._runner.run() # The warning was printed to stderr. self.assertEqual(stderr.getvalue(), 'No such list found: bee.example.com\n') # But ant's digest was still prepared. self.assertFalse(os.path.exists(mailbox_path)) items = get_queue_messages('virgin', expected_count=1) digest_contents = str(items[0].msg) self.assertIn('Subject: message 1', digest_contents) self.assertIn('Subject: message 2', digest_contents)
def test_hold_chain_crosspost(self): mlist2 = create_list('*****@*****.**') msg = mfs("""\ From: [email protected] To: [email protected], [email protected] Subject: A message Message-ID: <ant> MIME-Version: 1.0 A message body. """) process_chain(self._mlist, msg, {}, start_chain='hold') process_chain(mlist2, msg, {}, start_chain='hold') # There are four items in the virgin queue. Two of them are for the # list owners who need to moderate the held message, and the other is # for anne telling her that her message was held for approval. items = get_queue_messages('virgin', expected_count=4) anne_froms = set() owner_tos = set() for item in items: if item.msg['to'] == '*****@*****.**': anne_froms.add(item.msg['from']) else: owner_tos.add(item.msg['to']) self.assertEqual(anne_froms, set(['*****@*****.**', '*****@*****.**'])) self.assertEqual(owner_tos, set(['*****@*****.**', '*****@*****.**'])) # And the message appears in the store. messages = list(getUtility(IMessageStore).messages) self.assertEqual(len(messages), 1) self.assertEqual(messages[0]['message-id'], '<ant>')
def test_hold_chain(self): msg = mfs("""\ From: [email protected] To: [email protected] Subject: A message Message-ID: <ant> MIME-Version: 1.0 A message body. """) msgdata = dict(moderation_reasons=[ 'TEST-REASON-1', 'TEST-REASON-2', ]) logfile = LogFileMark('mailman.vette') process_chain(self._mlist, msg, msgdata, start_chain='hold') messages = get_queue_messages('virgin', expected_count=2) payloads = {} for item in messages: if item.msg['to'] == '*****@*****.**': part = item.msg.get_payload(0) payloads['owner'] = part.get_payload().splitlines() elif item.msg['To'] == '*****@*****.**': payloads['sender'] = item.msg.get_payload().splitlines() else: self.fail('Unexpected message: %s' % item.msg) self.assertIn(' TEST-REASON-1', payloads['owner']) self.assertIn(' TEST-REASON-2', payloads['owner']) self.assertIn(' TEST-REASON-1', payloads['sender']) self.assertIn(' TEST-REASON-2', payloads['sender']) logged = logfile.read() self.assertIn('TEST-REASON-1', logged) self.assertIn('TEST-REASON-2', logged)
def test_priority_site_over_list(self): # Test that the site-wide checks take precedence over the list-specific # checks. msg = mfs("""\ From: [email protected] To: [email protected] Subject: A message Message-ID: <ant> Foo: foo MIME-Version: 1.0 A message body. """) msgdata = {} header_matches = IHeaderMatchList(self._mlist) header_matches.append('Foo', 'foo', 'accept') # This event subscriber records the event that occurs when the message # is processed by the owner chain. events = [] with event_subscribers(events.append): process(self._mlist, msg, msgdata, start_chain='header-match') self.assertEqual(len(events), 1) event = events[0] # Site-wide wants to hold the message, the list wants to accept it. self.assertIsInstance(event, HoldEvent) self.assertEqual(event.chain, config.chains['hold'])
def test_banned_address_linked_to_user(self): # Anne is subscribed to a mailing list as a user with her preferred # address. She also has a secondary address which is banned and which # she uses to post to the mailing list. Both the MemberModeration and # NonmemberModeration rules miss because the posting address is # banned. user_manager = getUtility(IUserManager) anne = user_manager.create_user('*****@*****.**') set_preferred(anne) self._mlist.subscribe(anne, MemberRole.member) anne.link(user_manager.create_address('*****@*****.**')) IBanManager(self._mlist).ban('*****@*****.**') msg = mfs("""\ From: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) rule = moderation.MemberModeration() result = rule.check(self._mlist, msg, {}) self.assertFalse(result) rule = moderation.NonmemberModeration() result = rule.check(self._mlist, msg, {}) self.assertFalse(result)
def test_member_and_nonmember(self): user_manager = getUtility(IUserManager) anne = user_manager.create_address('*****@*****.**') user_manager.create_address('*****@*****.**') self._mlist.subscribe(anne, MemberRole.member) rule = moderation.NonmemberModeration() msg = mfs("""\ From: [email protected] Sender: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) # Both Anne and Bill are in the message's senders list. self.assertIn('*****@*****.**', msg.senders) self.assertIn('*****@*****.**', msg.senders) # The NonmemberModeration rule should *not* hit, because even though # Bill is in the list of senders he is not a member of the mailing # list. Anne is also in the list of senders and she *is* a member, so # she takes precedence. result = rule.check(self._mlist, msg, {}) self.assertFalse(result, 'NonmemberModeration rule should not hit') # After the rule runs, Bill becomes a non-member. bill_member = self._mlist.nonmembers.get_member('*****@*****.**') self.assertIsNotNone(bill_member) # Bill is not a member. bill_member = self._mlist.members.get_member('*****@*****.**') self.assertIsNone(bill_member)
def setUp(self): self._mlist = create_list('*****@*****.**') self._member = subscribe(self._mlist, 'Anne', email='*****@*****.**') self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: You bounced Message-ID: <first> """) # Set up the translation context. self._var_dir = tempfile.mkdtemp() self.addCleanup(shutil.rmtree, self._var_dir) xx_template_path = os.path.join( self._var_dir, 'templates', 'site', 'xx', 'probe.txt') os.makedirs(os.path.dirname(xx_template_path)) config.push('xx template dir', """\ [paths.testing] var_dir: {} """.format(self._var_dir)) self.addCleanup(config.pop, 'xx template dir') language_manager = getUtility(ILanguageManager) language_manager.add('xx', 'utf-8', 'Freedonia') self._member.preferences.preferred_language = 'xx' with open(xx_template_path, 'w') as fp: print("""\ blah blah blah $listname $address $optionsurl $owneraddr """, file=fp)
def test_bump_before_send(self): self._mlist.digest_volume_frequency = DigestFrequency.monthly self._mlist.volume = 7 self._mlist.next_digest_number = 4 self._mlist.digest_last_sent_at = right_now() + timedelta( days=-32) msg = mfs("""\ To: [email protected] From: [email protected] Subject: message 1 """) self._handler.process(self._mlist, msg, {}) args = FakeArgs() args.bump = True args.send = True args.lists.append('ant.example.com') self._command.process(args) self._runner.run() # The volume is 8 and the digest number is 2 because a digest was sent # after the volume/number was bumped. self.assertEqual(self._mlist.volume, 8) self.assertEqual(self._mlist.next_digest_number, 2) self.assertEqual(self._mlist.digest_last_sent_at, right_now()) items = get_queue_messages('virgin', expected_count=1) self.assertEqual(items[0].msg['subject'], 'Ant Digest, Vol 8, Issue 1')
def test_send_one_digest_by_fqdn_listname(self): msg = mfs("""\ To: [email protected] From: [email protected] Subject: message 1 """) self._handler.process(self._mlist, msg, {}) del msg['subject'] msg['subject'] = 'message 2' self._handler.process(self._mlist, msg, {}) # There are no digests already being sent, but the ant mailing list # does have a digest mbox collecting messages. get_queue_messages('digest', expected_count=0) mailbox_path = os.path.join(self._mlist.data_path, 'digest.mmdf') self.assertGreater(os.path.getsize(mailbox_path), 0) args = FakeArgs() args.send = True args.lists.append('*****@*****.**') self._command.process(args) self._runner.run() # Now, there's no digest mbox and there's a plaintext digest in the # outgoing queue. self.assertFalse(os.path.exists(mailbox_path)) items = get_queue_messages('virgin', expected_count=1) digest_contents = str(items[0].msg) self.assertIn('Subject: message 1', digest_contents) self.assertIn('Subject: message 2', digest_contents)
def test_request_order(self): # Requests must be sorted in creation order. # # This test only "works" for PostgreSQL, in the sense that if you # remove the fix in ../requests.py, it will still pass in SQLite. # Apparently SQLite auto-sorts results by ID but PostgreSQL autosorts # by insertion time. It's still worth keeping the test to prevent # regressions. # # We modify the auto-incremented ids by listening to SQLAlchemy's # flush event, and hacking all the _Request object id's to the next # value in a descending counter. request_ids = [] counter = count(200, -1) def id_hacker(session, flush_context, instances): # noqa for instance in session.new: if isinstance(instance, _Request): instance.id = next(counter) with before_flush(id_hacker): for index in range(10): msg = mfs(self._msg.as_string()) msg.replace_header('Message-ID', '<alpha{}>'.format(index)) request_ids.append(hold_message(self._mlist, msg)) config.db.store.flush() # Make sure that our ID are not already sorted. self.assertNotEqual(request_ids, sorted(request_ids)) # Get requests and check their order. requests = self._requests_db.of_type(RequestType.held_message) self.assertEqual([r.id for r in requests], sorted(request_ids))
def test_moderation_reason(self): # When a message is moderated, a reason is added to the metadata. user_manager = getUtility(IUserManager) anne = user_manager.create_address('*****@*****.**') msg = mfs("""\ From: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) # Anne is in the message's senders list. self.assertIn('*****@*****.**', msg.senders) # Now run the rule. rule = moderation.NonmemberModeration() msgdata = {} result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result, 'NonmemberModeration rule should hit') # The reason for moderation should be in the msgdata. reasons = msgdata['moderation_reasons'] self.assertEqual(reasons, ['The message is not from a list member']) # Now make Anne a moderated member... anne_member = self._mlist.subscribe(anne, MemberRole.member) anne_member.moderation_action = Action.hold # ...and run the rule again. rule = moderation.MemberModeration() msgdata = {} result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result, 'MemberModeration rule should hit') # The reason for moderation should be in the msgdata. reasons = msgdata['moderation_reasons'] self.assertEqual( reasons, ['The message comes from a moderated member'])
def setUp(self): self._mlist = create_list('*****@*****.**') self._now = now() # Enable just the dummy archiver. config.push('dummy', """ [archiver.dummy] class: mailman.runners.tests.test_archiver.DummyArchiver enable: no [archiver.prototype] enable: no [archiver.mhonarc] enable: no [archiver.mail_archive] enable: no """) self._archiveq = config.switchboards['archive'] self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: My first post Message-ID: <first> X-Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB First post! """) self._runner = make_testable_runner(ArchiveRunner) IListArchiverSet(self._mlist).get('dummy').is_enabled = True
def test_crash_event(self): runner = make_testable_runner(CrashingRunner, 'in') # When an exception occurs in Runner._process_one_file(), a zope.event # gets triggered containing the exception object. msg = mfs("""\ From: [email protected] To: [email protected] Message-ID: <ant> """) config.switchboards['in'].enqueue(msg, listid='test.example.com') with event_subscribers(self._got_event): runner.run() # We should now have exactly one event, which will contain the # exception, plus additional metadata containing the mailing list, # message, and metadata. self.assertEqual(len(self._events), 1) event = self._events[0] self.assertTrue(isinstance(event, RunnerCrashEvent)) self.assertEqual(event.mailing_list, self._mlist) self.assertEqual(event.message['message-id'], '<ant>') self.assertEqual(event.metadata['listid'], 'test.example.com') self.assertTrue(isinstance(event.error, RuntimeError)) self.assertEqual(str(event.error), 'borked') self.assertTrue(isinstance(event.runner, CrashingRunner)) # The message should also have ended up in the shunt queue. shunted = get_queue_messages('shunt') self.assertEqual(len(shunted), 1) self.assertEqual(shunted[0].msg['message-id'], '<ant>')
def setUp(self): # Create a fake mailing list and message object self._msg = mfs("""\ To: [email protected] From: [email protected] Subject: Testing the test list Message-ID: <ant> X-Message-ID-Hash: MS6QLWERIJLGCRF44J7USBFDELMNT2BW Tests are better than no tests but the water deserves to be swum. """) with transaction(): self._mlist = create_list('*****@*****.**') # Set up a temporary directory for the prototype archiver so that it's # easier to clean up. self._tempdir = tempfile.mkdtemp() config.push('prototype', """ [paths.testing] archive_dir: {0} """.format(self._tempdir)) # Capture the structure of a maildir. self._expected_dir_structure = set( (os.path.join(config.ARCHIVE_DIR, path) for path in ( 'prototype', os.path.join('prototype', self._mlist.fqdn_listname), os.path.join('prototype', self._mlist.fqdn_listname, 'cur'), os.path.join('prototype', self._mlist.fqdn_listname, 'new'), os.path.join('prototype', self._mlist.fqdn_listname, 'tmp'), ))) self._expected_dir_structure.add(config.ARCHIVE_DIR)
def test_member_fallback_to_list_defaults(self): # https://gitlab.com/mailman/mailman/issues/189 self._mlist.default_member_action = Action.accept user_manager = getUtility(IUserManager) anne = user_manager.create_address('*****@*****.**') member = self._mlist.subscribe(anne, MemberRole.member) # Anne's moderation rule falls back to the list default. self.assertIsNone(member.moderation_action) rule = moderation.MemberModeration() msg = mfs("""\ From: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) # First, the message gets accepted. msgdata = {} result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result) self.assertEqual(msgdata.get('moderation_action'), 'accept') # Then the list's default member action is changed. self._mlist.default_member_action = Action.hold msg.replace_header('Message-ID', '<bee>') # This time, the message is held. result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result) self.assertEqual(msgdata.get('moderation_action'), 'hold')
def test_nonmember_fallback_to_list_defaults(self): # https://gitlab.com/mailman/mailman/issues/189 self._mlist.default_nonmember_action = Action.hold user_manager = getUtility(IUserManager) user_manager.create_address('*****@*****.**') rule = moderation.NonmemberModeration() msg = mfs("""\ From: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) # First, the message should get held. msgdata = {} result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result) self.assertEqual(msgdata['moderation_action'], 'hold') # As a side-effect, Anne has been added as a nonmember with a # moderation action that falls back to the list's default. anne = self._mlist.nonmembers.get_member('*****@*****.**') self.assertIsNone(anne.moderation_action) # Then the list's default nonmember action is changed. self._mlist.default_nonmember_action = Action.discard msg.replace_header('Message-ID', '<bee>') # This time, the message should be discarded. result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result) self.assertEqual(msgdata.get('moderation_action'), 'discard')
def test_simple_message(self): msg = mfs("""\ From: [email protected] To: [email protected] message triggering a digest """) mbox_path = os.path.join(self._mlist.data_path, 'digest.mmdf') self._process(self._mlist, msg, {}) self._digestq.enqueue( msg, listname=self._mlist.fqdn_listname, digest_path=mbox_path, volume=1, digest_number=1) self._runner.run() # There are two messages in the virgin queue: the digest as plain-text # and as multipart. messages = get_queue_messages('virgin') self.assertEqual(len(messages), 2) self.assertEqual( sorted(item.msg.get_content_type() for item in messages), ['multipart/mixed', 'text/plain']) for item in messages: self.assertEqual(item.msg['subject'], 'Test Digest, Vol 1, Issue 1')
def test_confirm_with_utf8_body(self): # Clear out the virgin queue so that the test below only sees the # reply to the confirmation message. get_queue_messages('virgin') subject = 'Re: confirm {0}'.format(self._token) to = 'test-confirm+{0}@example.com'.format(self._token) msg = mfs("""\ From: Anne Person <*****@*****.**> MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: quoted-printable * test-confirm+90bf6ef335d92cfbbe540a5c9ebfecb107a48e48@example.com <= test-confirm+90bf6ef335d92cfbbe540a5c9ebfecb107a48e48@example.com>: > Email Address Registration Confirmation >=20 > Hello, this is the GNU Mailman server at example.com. >=20 > We have received a registration request for the email address >=20 > [email protected] >=20 > Before you can start using GNU Mailman at this site, you must first con= firm > that this is your email address. You can do this by replying to this m= essage, > keeping the Subject header intact. Or you can visit this web page >=20 > http://example.com/confirm/90bf6ef335d92cfbbe540a5c9ebfecb107a48e48 >=20 > If you do not wish to register this email address simply disregard this > message. If you think you are being maliciously subscribed to the list= , or > have any other questions, you may contact >=20 > [email protected] --=20 Franziskanerstra=C3=9Fe """) msg['Subject'] = subject msg['To'] = to self._commandq.enqueue(msg, dict(listid='test.example.com')) self._runner.run() # Anne is now a confirmed member so her user record and email address # should exist in the database. manager = getUtility(IUserManager) user = manager.get_user('*****@*****.**') address = list(user.addresses)[0] self.assertEqual(address.email, '*****@*****.**') self.assertEqual(address.verified_on, datetime(2005, 8, 1, 7, 49, 23)) address = manager.get_address('*****@*****.**') self.assertEqual(address.email, '*****@*****.**') messages = get_queue_messages('virgin') self.assertEqual(len(messages), 1) self.assertEqual(messages[0].msgdata['recipients'], set(['*****@*****.**']))
def setUp(self): self._mlist = create_list('*****@*****.**') self._msg = mfs("""\ From: [email protected] To: [email protected] Message-ID: <ant> """)
def test_no_verp_with_non_match(self): # No VERP address is found, and a header had a non-matching pattern. msg = mfs("""\ From: [email protected] To: [email protected] """) self.assertEqual(self._verper.get_verp(self._mlist, msg), set())
def setUp(self): self._mlist = create_list('*****@*****.**') self._handler = config.handlers['file-recipients'].process self._msg = mfs("""\ From: [email protected] A message. """)
def setUp(self): self._mlist = create_list('*****@*****.**') self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: Ignore """)
def test_no_verp(self): # The empty set is returned when there is no VERP headers. msg = mfs("""\ From: [email protected] To: [email protected] """) self.assertEqual(self._verper.get_verp(self._mlist, msg), set())
def test_get_message_by_hash(self): # Messages have an X-Message-ID-Hash header, the value of which can be # used to look the message up in the message store. message = mfs("""\ Subject: An important message Message-ID: <ant> This message is very important. """) add_message_hash(message) self._store.add(message) self.assertEqual(message['x-message-id-hash'], 'V3YEHAFKE2WVJNK63Z7RFP4JMHISI2RG') found = self._store.get_message_by_hash( 'V3YEHAFKE2WVJNK63Z7RFP4JMHISI2RG') self.assertEqual(found['message-id'], '<ant>') self.assertEqual(found['x-message-id-hash'], 'V3YEHAFKE2WVJNK63Z7RFP4JMHISI2RG')
def test_no_bak_but_pck(self): # if there is no .bak file but a .pck with the same filebase, # .finish() should handle the .pck. msg = mfs("""\ From: [email protected] To: [email protected] Message-ID: <ant> """) switchboard = config.switchboards['shunt'] # Enqueue the message. filebase = switchboard.enqueue(msg) # Now call .finish() without first dequeueing. switchboard.finish(filebase, preserve=True) # And ensure the file got preserved. bad_dir = config.switchboards['bad'].queue_directory psvfile = os.path.join(bad_dir, filebase + '.psv') self.assertTrue(os.path.isfile(psvfile))
def setUp(self): self._mlist = create_list('*****@*****.**') self._manager = getUtility(IUserManager) anne = self._manager.create_address('*****@*****.**') bart = self._manager.create_address('*****@*****.**') cris = self._manager.create_address('*****@*****.**') dave = self._manager.create_address('*****@*****.**') # Make Cris and Dave owners of the mailing list. self._anne = self._mlist.subscribe(anne, MemberRole.member) self._bart = self._mlist.subscribe(bart, MemberRole.member) self._cris = self._mlist.subscribe(cris, MemberRole.owner) self._dave = self._mlist.subscribe(dave, MemberRole.owner) self._process = config.handlers['owner-recipients'].process self._msg = mfs("""\ From: Elle Person <*****@*****.**> To: [email protected] """)
def test_max_recipients_returns_reason(self): # Ensure max_recipients rule returns a reason. msg = mfs("""\ From: [email protected] To: [email protected] Cc: [email protected], [email protected] Subject: A Subject Message-ID: <ant> A message body. """) rule = max_recipients.MaximumRecipients() self._mlist.max_num_recipients = 2 msgdata = {} result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result) self.assertEqual(msgdata['moderation_reasons'], [('Message has more than {} recipients', 2)])
def test_message_id_hash_gets_replaced_backward_compatibility(self): msg = mfs("""\ Message-ID: <ant> Message-ID-Hash: abc X-Message-ID-Hash: abc """) self._store.add(msg) stored_msg = self._store.get_message_by_id('<ant>') message_id_hashes = stored_msg.get_all('message-id-hash') self.assertEqual(len(message_id_hashes), 1) self.assertEqual(message_id_hashes[0], 'MS6QLWERIJLGCRF44J7USBFDELMNT2BW') # For backward compatibility with the old spec. x_message_id_hashes = stored_msg.get_all('x-message-id-hash') self.assertEqual(len(x_message_id_hashes), 1) self.assertEqual(x_message_id_hashes[0], 'MS6QLWERIJLGCRF44J7USBFDELMNT2BW')
def test_subject_encoding_error(self): # GL#383: messages with badly encoded Subject headers crash the REST # server. self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: =?GB2312?B?saa9o7fmtNPEpbVaQ2h1o6zDt7uoz+PX1L/guq7AtKGj?= Message-ID: <alpha> Something else. """) with transaction(): held_id = hold_message(self._mlist, self._msg) json, response = call_api( 'http://*****:*****@example.com/held') self.assertEqual(response.status_code, 200) self.assertEqual(json['total_size'], 1) self.assertEqual(json['entries'][0]['request_id'], held_id)
def test_confirm_leave_moderate(self): msg = mfs("""\ From: Anne Person <*****@*****.**> To: test-confirm+{token}@example.com Subject: Re: confirm {token} """.format(token=self._token)) self._mlist.unsubscription_policy = ( SubscriptionPolicy.confirm_then_moderate) # Clear any previously queued confirmation messages. get_queue_messages('virgin') Confirm().process(self._mlist, msg, {}, (self._token, ), Results()) # Anne is still a member of the mailing list. member = self._mlist.members.get_member('*****@*****.**') self.assertIsNotNone(member) # There should be a notice to the list owners item = get_queue_messages('virgin', expected_count=1)[0] self.assertEqual(item.msg['to'], '*****@*****.**')
def test_hold_chain(self): msg = mfs("""\ From: [email protected] To: [email protected] Subject: A message Message-ID: <ant> MIME-Version: 1.0 A message body. """) msgdata = dict(moderation_reasons=[ 'TEST-REASON-1', 'TEST-REASON-2', ('TEST-{}-REASON-{}', 'FORMAT', 3), ]) logfile = LogFileMark('mailman.vette') process_chain(self._mlist, msg, msgdata, start_chain='hold') messages = get_queue_messages('virgin', expected_count=2) payloads = {} for item in messages: if item.msg['to'] == '*****@*****.**': part = item.msg.get_payload(0) payloads['owner'] = part.get_payload().splitlines() elif item.msg['To'] == '*****@*****.**': payloads['sender'] = item.msg.get_payload().splitlines() else: self.fail('Unexpected message: %s' % item.msg) self.assertIn(' TEST-REASON-1', payloads['owner']) self.assertIn(' TEST-REASON-2', payloads['owner']) self.assertIn(' TEST-FORMAT-REASON-3', payloads['owner']) self.assertIn(' TEST-REASON-1', payloads['sender']) self.assertIn(' TEST-REASON-2', payloads['sender']) self.assertIn(' TEST-FORMAT-REASON-3', payloads['sender']) logged = logfile.read() self.assertIn('TEST-REASON-1', logged) self.assertIn('TEST-REASON-2', logged) self.assertIn('TEST-FORMAT-REASON-3', logged) # Check the reason passed to hold_message(). requests = IListRequests(self._mlist) self.assertEqual(requests.count_of(RequestType.held_message), 1) request = requests.of_type(RequestType.held_message)[0] key, data = requests.get_request(request.id) self.assertEqual(data.get('_mod_reason'), 'TEST-REASON-1; TEST-REASON-2; TEST-FORMAT-REASON-3')
def test_reply_to_list(self): # Test a post from a member with the list posting address in Reply-To:. rule = moderation.NonmemberModeration() user_manager = getUtility(IUserManager) anne = user_manager.create_address('*****@*****.**') user_manager.create_address('*****@*****.**') self._mlist.subscribe(anne, MemberRole.member) msg = mfs("""\ From: [email protected] To: [email protected] Reply-To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) result = rule.check(self._mlist, msg, {}) self.assertFalse(result)
def test_confirm_with_random_ascii_prefix(self): subject = '\x99AW: confirm {}'.format(self._token) msg = mfs("""\ From: [email protected] To: [email protected] """) msg['Subject'] = subject self._commandq.enqueue(msg, dict(listid='test.example.com')) self._runner.run() # Anne is now a confirmed member so her user record and email address # should exist in the database. manager = getUtility(IUserManager) user = manager.get_user('*****@*****.**') address = list(user.addresses)[0] self.assertEqual(address.email, '*****@*****.**') self.assertEqual(address.verified_on, datetime(2005, 8, 1, 7, 49, 23)) address = manager.get_address('*****@*****.**') self.assertEqual(address.email, '*****@*****.**')
def test_dmarc_dns_exception(self): mlist = create_list('*****@*****.**') # Use action reject. The rule only hits on reject and discard. mlist.dmarc_mitigate_action = DMARCMitigateAction.reject msg = mfs("""\ From: [email protected] To: [email protected] """) mark = LogFileMark('mailman.error') rule = dmarc.DMARCMitigation() with get_dns_resolver(): self.assertTrue(rule.check(mlist, msg, {})) line = mark.readline() self.assertEqual( line[-144:], 'DNSException: Unable to query DMARC policy for ' '[email protected] (_dmarc.example.info). ' 'Abstract base class shared by all dnspython exceptions.\n')
def test_confirm_subcommand_with_more_commands(self): # confirm command stops the processing of rest of the commands. get_queue_messages('virgin') subject = 'bad-command' to = 'test-confirm+{}@example.com'.format(self._token) msg = mfs("""\ From: Anne Person <*****@*****.**> MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: quoted-printable """) msg['Subject'] = subject msg['To'] = to self._commandq.enqueue( msg, dict(listid='test.example.com', subaddress='confirm')) self._runner.run() # This should not send out any email. get_queue_messages('virgin', expected_count=0)
def test_multiple_cnames(self): mlist = create_list('*****@*****.**') # Use action reject. The rule only hits on reject and discard. mlist.dmarc_mitigate_action = DMARCMitigateAction.reject msg = mfs("""\ From: [email protected] To: [email protected] """) mark = LogFileMark('mailman.vette') rule = dmarc.DMARCMitigation() with get_dns_resolver(cmult=True): self.assertTrue(rule.check(mlist, msg, {})) line = mark.readline() self.assertEqual( line[-128:], 'ant: DMARC lookup for [email protected] (_dmarc.example.biz) ' 'found p=quarantine in _dmarc.example.com. = v=DMARC1; ' 'p=quarantine;\n')
def test_these_nonmembers(self): # Test the legacy *_these_nonmembers attributes. user_manager = getUtility(IUserManager) actions = { '*****@*****.**': 'accept', '*****@*****.**': 'hold', '*****@*****.**': 'reject', '*****@*****.**': 'discard', '^anne-.*@example.com': 'accept', '^bill-.*@example.com': 'hold', '^chris-.*@example.com': 'reject', '^dana-.*@example.com': 'discard', } rule = moderation.NonmemberModeration() user_manager = getUtility(IUserManager) for address, action_name in actions.items(): setattr(self._mlist, '{}_these_nonmembers'.format(action_name), [address]) if address.startswith('^'): # It's a pattern, craft a proper address. address = address[1:].replace('.*', 'something') user_manager.create_address(address) msg = mfs("""\ From: {} To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """.format(address)) msgdata = {} result = rule.check(self._mlist, msg, msgdata) if action_name == 'accept': self.assertFalse(result, 'NonmemberModeration rule should miss') else: self.assertTrue(result, 'NonmemberModeration rule should hit') self.assertIn('member_moderation_action', msgdata) self.assertEqual( msgdata['member_moderation_action'], action_name, 'Wrong action for {}: {}'.format(address, action_name))
def test_double_confirmation(self): # A join request comes in using both the -join address and the word # 'subscribe' in the first line of the body. This should produce just # one subscription request and one confirmation response. msg = mfs("""\ From: [email protected] To: [email protected] subscribe """) # Adding the subaddress to the metadata dictionary mimics what happens # when the above email message is first processed by the lmtp runner. # For convenience, we skip that step in this test. self._commandq.enqueue( msg, dict(listid='test.example.com', subaddress='join')) self._runner.run() # There will be two messages in the queue. The first one is a reply # to Anne notifying her of the status of her command email. The # second one is the confirmation message of her join request. items = get_queue_messages('virgin', sort_on='subject', expected_count=2) self.assertTrue(str(items[1].msg['subject']).startswith('confirm')) self.assertEqual(items[0].msg['subject'], 'The results of your email commands') # Search the contents of the results message. There should be just # one 'Confirmation email' line. confirmation_lines = [] in_results = False for line in body_line_iterator(items[0].msg): line = line.strip() if in_results: if line.startswith('- Done'): break if len(line) > 0: confirmation_lines.append(line) if line.strip() == '- Results:': in_results = True # There should be exactly one confirmation line. self.assertEqual(len(confirmation_lines), 1) # And the confirmation line should name Anne's email address. self.assertIn('*****@*****.**', confirmation_lines[0])
def setUp(self): self._mlist = create_list('*****@*****.**') # Set personalized delivery. self._mlist.personalize = Personalization.individual # Make Anne a member of this mailing list. self._anne = subscribe(self._mlist, 'Anne', email='*****@*****.**') self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: test """) self._deliverer = find_name(config.mta.outgoing) # Set the maximum transactions per connection. config.push( 'maxtrans', """ [mta] max_sessions_per_connection: 3 """) self.addCleanup(config.pop, 'maxtrans')
def setUp(self): self._mlist = create_list('*****@*****.**') # The default testing hash algorithm is "roundup_plaintext" which # yields hashed passwords of the form: {plaintext}abc # # Migration is automatically supported when a more modern password # hash is chosen after the original password is set. As long as the # old password still validates, the migration happens automatically. self._mlist.moderator_password = config.password_context.encrypt( 'super secret') self._rule = approved.Approved() self._msg = mfs("""\ From: [email protected] To: [email protected] Subject: A Message with non-ascii body Message-ID: <ant> MIME-Version: 1.0 A message body. """)
def test_banned_sender_among_multiple_senders(self): # Two addresses are created, one of which is banned. The rule matches # because all senders are checked. user_manager = getUtility(IUserManager) user_manager.create_address('*****@*****.**') user_manager.create_address('*****@*****.**') IBanManager(self._mlist).ban('*****@*****.**') msg = mfs("""\ From: [email protected] Sender: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) rule = banned_address.BannedAddress() result = rule.check(self._mlist, msg, {}) self.assertTrue(result)
def test_missing_html_to_plain_text_command(self): # Calling a missing html_to_plain_text_command is properly logged. msg = mfs("""\ From: [email protected] Content-Type: text/html MIME-Version: 1.0 <html><head></head> <body></body></html> """) process = config.handlers['mime-delete'].process mark = LogFileMark('mailman.error') with dummy_script('nonexist'): process(self._mlist, msg, {}) line = mark.readline()[:-1] self.assertTrue(line.endswith('HTML -> text/plain command error')) self.assertEqual(msg.get_content_type(), 'text/html') self.assertIsNone(msg['x-content-filtered-by']) payload_lines = msg.get_payload().splitlines() self.assertEqual(payload_lines[0], '<html><head></head>')
def test_max_size_returns_reason(self): # Ensure max_size rule returns a reason. msg = mfs("""\ From: [email protected] To: [email protected] Subject: A Subject Message-ID: <ant> A message body. """) rule = max_size.MaximumSize() self._mlist.max_message_size = 1 # Fake the size. msg.original_size = 2048 msgdata = {} result = rule.check(self._mlist, msg, msgdata) self.assertTrue(result) self.assertEqual( msgdata['moderation_reasons'], [('The message is larger than the {} KB maximum size', 1)])
def test_leave(self): with transaction(): self._mlist.unsubscription_policy = SubscriptionPolicy.confirm anne = getUtility(IUserManager).create_user('*****@*****.**') set_preferred(anne) self._mlist.subscribe(anne.preferred_address) msg = mfs("""\ From: [email protected] To: [email protected] leave """) self._commandq.enqueue( msg, dict(listid='test.example.com', subaddress='leave')) self._runner.run() # One message with confirmation of her unsubscription event should be # sent. items = get_queue_messages('virgin', expected_count=1) confirmation = items[0].msg self.assertTrue(str(confirmation['subject']).startswith('confirm'))
def test_double_leave(self): # In this case, the user can be unsubscribed immediately because the # policy does not require confirmation, however because the email is # sent to the -leave address and it contains the 'leave' command, we # should only process one command per email. with transaction(): self._mlist.unsubscription_policy = SubscriptionPolicy.open anne = getUtility(IUserManager).create_user('*****@*****.**') set_preferred(anne) self._mlist.subscribe(anne.preferred_address) msg = mfs("""\ From: [email protected] To: [email protected] leave """) self._commandq.enqueue( msg, dict(listid='test.example.com', subaddress='leave')) self._runner.run() get_queue_messages('virgin', sort_on='subject', expected_count=0)
def test_convert_html_to_plaintext_base64(self): # Converting to plain text calls a command line script with decoded # message body. msg = mfs("""\ From: [email protected] Content-Type: text/html Content-Transfer-Encoding: base64 MIME-Version: 1.0 PGh0bWw+PGhlYWQ+PC9oZWFkPgo8Ym9keT48L2JvZHk+PC9odG1sPgo= """) process = config.handlers['mime-delete'].process with dummy_script(): process(self._mlist, msg, {}) self.assertEqual(msg.get_content_type(), 'text/plain') self.assertTrue( msg['x-content-filtered-by'].startswith('Mailman/MimeDel')) payload_lines = msg.get_payload().splitlines() self.assertEqual(payload_lines[0], 'Converted text/html to text/plain') self.assertEqual(payload_lines[2], '<html><head></head>')
def test_double_confirmation(self): # 'confirm' in the Subject and in the To header should not try to # confirm the token twice. # # Clear out the virgin queue so that the test below only sees the # reply to the confirmation message. get_queue_messages('virgin') subject = 'Re: confirm {0}'.format(self._token) to = 'test-confirm+{0}@example.com'.format(self._token) msg = mfs("""\ From: Anne Person <*****@*****.**> """) msg['Subject'] = subject msg['To'] = to self._commandq.enqueue( msg, dict(listid='test.example.com', subaddress='confirm')) self._runner.run() # Anne is now a confirmed member so her user record and email address # should exist in the database. manager = getUtility(IUserManager) user = manager.get_user('*****@*****.**') self.assertEqual(list(user.addresses)[0].email, '*****@*****.**') # Make sure that the confirmation was not attempted twice. messages = get_queue_messages('virgin') self.assertEqual(len(messages), 1) # Search the contents of the results message. There should be just # one 'Confirmation email' line. confirmation_lines = [] in_results = False for line in body_line_iterator(messages[0].msg): line = line.strip() if in_results: if line.startswith('- Done'): break if len(line) > 0: confirmation_lines.append(line) if line.strip() == '- Results:': in_results = True self.assertEqual(len(confirmation_lines), 1) self.assertFalse('did not match' in confirmation_lines[0])
def test_no_text_plain_part(self): # When the message body only contains HTML, the rule should not throw # AttributeError: 'NoneType' object has no attribute 'get_payload' # LP: #1158721 msg = mfs("""\ From: [email protected] To: [email protected] Subject: HTML only email Message-ID: <ant> MIME-Version: 1.0 Content-Type: text/html; charset="Windows-1251" Content-Transfer-Encoding: 7bit <HTML> <BODY> <P>This message contains only HTML, no plain/text part</P> </BODY> </HTML> """) result = self._rule.check(self._mlist, msg, {}) self.assertFalse(result)
def test_banned_sender_among_multiple_senders(self): # Two addresses are created, one of which is banned. Even though the # The Nonmember moderation rule misses if any of the banned addresses # appear in the 'senders' headers of the message. user_manager = getUtility(IUserManager) user_manager.create_address('*****@*****.**') user_manager.create_address('*****@*****.**') IBanManager(self._mlist).ban('*****@*****.**') rule = moderation.NonmemberModeration() msg = mfs("""\ From: [email protected] Sender: [email protected] To: [email protected] Subject: A test message Message-ID: <ant> MIME-Version: 1.0 A message body. """) result = rule.check(self._mlist, msg, {}) self.assertFalse(result)
def setUp(self): config.push('no_archivers', """ [archiver.prototype] enable: no [archiver.mail_archive] enable: no [archiver.mhonarc] enable: no [archiver.pipermail] enable: no """) self.addCleanup(config.pop, 'no_archivers') self._mlist = create_list('*****@*****.**') self._mlist.archive_policy = ArchivePolicy.public self._msg = mfs("""\ From: [email protected] Message-ID: <first> Message-ID-Hash: 4CMWUN6BHVCMHMDAOSJZ2Q72G5M32MWB Dummy text """)
def test_log_exception_in_finish(self): # If something bad happens in .finish(), the traceback should get # logged. LP: #1165589. msg = mfs("""\ From: [email protected] To: [email protected] Message-ID: <ant> """) switchboard = config.switchboards['shunt'] # Enqueue the message. filebase = switchboard.enqueue(msg) error_log = LogFileMark('mailman.error') msg, data = switchboard.dequeue(filebase) # Now, cause .finish() to throw an exception. with patch('mailman.core.switchboard.os.rename', side_effect=OSError('Oops!')): switchboard.finish(filebase, preserve=True) traceback = error_log.read().splitlines() self.assertEqual(traceback[1], 'Traceback (most recent call last):') self.assertEqual(traceback[-1], 'OSError: Oops!')
def test_confirm_in_subject_with_more_commands(self): get_queue_messages('virgin') subject = 'confirm {}'.format(self._token) to = '*****@*****.**' msg = mfs("""\ From: Anne Person <*****@*****.**> MIME-Version: 1.0 Content-Type: text/plain; charset=utf-8 Content-Disposition: inline Content-Transfer-Encoding: quoted-printable bad-command """) msg['Subject'] = subject msg['To'] = to self._commandq.enqueue( msg, dict(listid='test.example.com', subaddress='confirm')) self._runner.run() # This should send out one email that confirms that token was accepted. items = get_queue_messages('virgin', expected_count=1) self.assertNotIn('No such command: bad-command', str(items[0].msg))
def test_message_id_hash_gets_replaced(self): # Any existing Message-ID-Hash header (or for backward compatibility # X-Message-ID-Hash) gets replaced with its new value. msg = mfs("""\ Subject: Testing Message-ID: <ant> Message-ID-Hash: abc X-Message-ID-Hash: abc """) self._store.add(msg) stored_msg = self._store.get_message_by_id('<ant>') message_id_hashes = stored_msg.get_all('message-id-hash') self.assertEqual(len(message_id_hashes), 1) self.assertEqual(message_id_hashes[0], 'MS6QLWERIJLGCRF44J7USBFDELMNT2BW') # For backward compatibility with the old spec. x_message_id_hashes = stored_msg.get_all('x-message-id-hash') self.assertEqual(len(x_message_id_hashes), 1) self.assertEqual(x_message_id_hashes[0], 'MS6QLWERIJLGCRF44J7USBFDELMNT2BW')