Example #1
0
    def test_no_action_defaults_to_site_wide_action(self):
        # If the list-specific header check matches, but there is no defined
        # action, the site-wide antispam action is used.
        msg = mfs("""\
From: [email protected]
To: [email protected]
Subject: A message
Message-ID: <ant>
Foo: foo
MIME-Version: 1.0

A message body.
""")
        header_matches = IHeaderMatchList(self._mlist)
        header_matches.append('Foo', 'foo')
        # This event subscriber records the event that occurs when the message
        # is processed by the owner chain, which holds its for approval.
        events = []

        def record_holds(event):  # noqa: E301
            if not isinstance(event, HoldEvent):
                return
            events.append(event)

        with event_subscribers(record_holds):
            # Set the site-wide antispam action to hold the message.
            with configuration('antispam',
                               header_checks="""
                Spam: [*]{3,}
                """,
                               jump_chain='hold'):  # noqa: E125
                process(self._mlist, msg, {}, start_chain='header-match')
            self.assertEqual(len(events), 1)
            event = events[0]
            self.assertIsInstance(event, HoldEvent)
            self.assertEqual(event.chain, config.chains['hold'])
            self.assertEqual(event.mlist, self._mlist)
            self.assertEqual(event.msg, msg)
        events = []

        def record_discards(event):  # noqa: E301
            if not isinstance(event, DiscardEvent):
                return
            events.append(event)

        with event_subscribers(record_discards):
            # Set the site-wide default to discard the message.
            msg.replace_header('Message-Id', '<bee>')
            with configuration('antispam',
                               header_checks="""
                Spam: [*]{3,}
                """,
                               jump_chain='discard'):  # noqa: E125
                process(self._mlist, msg, {}, start_chain='header-match')
            self.assertEqual(len(events), 1)
            event = events[0]
            self.assertIsInstance(event, DiscardEvent)
            self.assertEqual(event.chain, config.chains['discard'])
            self.assertEqual(event.mlist, self._mlist)
            self.assertEqual(event.msg, msg)
Example #2
0
    def test_no_action_defaults_to_site_wide_action(self):
        # If the list-specific header check matches, but there is no defined
        # action, the site-wide antispam action is used.
        msg = mfs("""\
From: [email protected]
To: [email protected]
Subject: A message
Message-ID: <ant>
Foo: foo
MIME-Version: 1.0

A message body.
""")
        header_matches = IHeaderMatchList(self._mlist)
        header_matches.append('Foo', 'foo')
        # This event subscriber records the event that occurs when the message
        # is processed by the owner chain, which holds its for approval.
        events = []
        def record_holds(event):                    # noqa
            if not isinstance(event, HoldEvent):
                return
            events.append(event)
        with event_subscribers(record_holds):
            # Set the site-wide antispam action to hold the message.
            with configuration('antispam', header_checks="""
                Spam: [*]{3,}
                """, jump_chain='hold'):            # noqa
                process(self._mlist, msg, {}, start_chain='header-match')
            self.assertEqual(len(events), 1)
            event = events[0]
            self.assertIsInstance(event, HoldEvent)
            self.assertEqual(event.chain, config.chains['hold'])
            self.assertEqual(event.mlist, self._mlist)
            self.assertEqual(event.msg, msg)
        events = []
        def record_discards(event):                 # noqa
            if not isinstance(event, DiscardEvent):
                return
            events.append(event)
        with event_subscribers(record_discards):
            # Set the site-wide default to discard the message.
            msg.replace_header('Message-Id', '<bee>')
            with configuration('antispam', header_checks="""
                Spam: [*]{3,}
                """, jump_chain='discard'):         # noqa
                process(self._mlist, msg, {}, start_chain='header-match')
            self.assertEqual(len(events), 1)
            event = events[0]
            self.assertIsInstance(event, DiscardEvent)
            self.assertEqual(event.chain, config.chains['discard'])
            self.assertEqual(event.mlist, self._mlist)
            self.assertEqual(event.msg, msg)
Example #3
0
 def test_push_and_pop_trigger_events(self):
     # Pushing a new configuration onto the stack triggers a
     # post-processing event.
     events = []
     def on_event(event):                                     # noqa: E306
         if isinstance(event, ConfigurationUpdatedEvent):
             # Record both the event and the top overlay.
             events.append(event.config.overlays[0].name)
     # Do two pushes, and then pop one of them.
     with event_subscribers(on_event):
         with configuration('test', _configname='first'):
             with configuration('test', _configname='second'):
                 pass
             self.assertEqual(events, ['first', 'second', 'first'])
Example #4
0
 def test_push_and_pop_trigger_events(self):
     # Pushing a new configuration onto the stack triggers a
     # post-processing event.
     events = []
     def on_event(event):
         if isinstance(event, ConfigurationUpdatedEvent):
             # Record both the event and the top overlay.
             events.append(event.config.overlays[0].name)
     # Do two pushes, and then pop one of them.
     with event_subscribers(on_event):
         with configuration('test', _configname='first'):
             with configuration('test', _configname='second'):
                 pass
             self.assertEqual(events, ['first', 'second', 'first'])
Example #5
0
    def test_successful_login_updates_password(self):
        # Passlib supports updating the hash when the hash algorithm changes.
        # When a user logs in successfully, the password will be updated if
        # necessary.
        #
        # Start by hashing Anne's password with a different hashing algorithm
        # than the one that the REST runner uses by default during testing.
        config_file = os.path.join(config.VAR_DIR, 'passlib-tmp.config')
        with open(config_file, 'w') as fp:
            print("""\
[passlib]
schemes = hex_md5
""", file=fp)
        with configuration('passwords', configuration=config_file):
            with transaction():
                self.anne.password = config.password_context.encrypt('abc123')
                # Just ensure Anne's password is hashed correctly.
                self.assertEqual(self.anne.password,
                                 'e99a18c428cb38d5f260853678922e03')
        # Now, Anne logs in with a successful password.  This should change it
        # back to the plaintext hash.
        call_api('http://localhost:9001/3.0/users/1/login', {
            'cleartext_password': '******',
        })
        self.assertEqual(self.anne.password, '{plaintext}abc123')
Example #6
0
 def test_verp_never(self):
     # Never VERP when the interval is zero.
     msgdata = {}
     self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
     with configuration('mta', verp_delivery_interval=0):
         self._runner.run()
     self.assertFalse(captured_msgdata['verp'])
Example #7
0
 def test_verp_never(self):
     # Never VERP when the interval is zero.
     msgdata = {}
     self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
     with configuration('mta', verp_delivery_interval=0):
         self._runner.run()
     self.assertFalse(captured_msgdata['verp'])
Example #8
0
 def test_verp_always(self):
     # Always VERP when the interval is one.
     msgdata = {}
     self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
     with configuration('mta', verp_delivery_interval=1):
         self._runner.run()
     self.assertTrue(captured_msgdata['verp'])
Example #9
0
 def test_verp_always(self):
     # Always VERP when the interval is one.
     msgdata = {}
     self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
     with configuration('mta', verp_delivery_interval=1):
         self._runner.run()
     self.assertTrue(captured_msgdata['verp'])
Example #10
0
    def test_successful_login_updates_password(self):
        # Passlib supports updating the hash when the hash algorithm changes.
        # When a user logs in successfully, the password will be updated if
        # necessary.
        #
        # Start by hashing Anne's password with a different hashing algorithm
        # than the one that the REST runner uses by default during testing.
        config_file = os.path.join(config.VAR_DIR, 'passlib-tmp.config')
        with open(config_file, 'w') as fp:
            print("""\
[passlib]
schemes = hex_md5
""", file=fp)
        with configuration('passwords', configuration=config_file):
            with transaction():
                self.anne.password = config.password_context.encrypt('abc123')
                # Just ensure Anne's password is hashed correctly.
                self.assertEqual(self.anne.password,
                                 'e99a18c428cb38d5f260853678922e03')
        # Now, Anne logs in with a successful password.  This should change it
        # back to the plaintext hash.
        call_api('http://localhost:9001/3.0/users/1/login', {
                 'cleartext_password': '******',
                 })
        self.assertEqual(self.anne.password, '{plaintext}abc123')
Example #11
0
def use_test_organizational_data(resources):
    # Point the organizational URL to our test data.
    filename = str(resources.enter_context(
        path('mailman.rules.tests.data', 'org_domain.txt')))
    url = 'file:///{}'.format(filename)
    return resources.enter_context(
        configuration('dmarc', org_domain_data_url=url))
Example #12
0
 def test_no_verp_on_interval_miss(self):
     # VERP every so often, when the post_id matches.
     self._mlist.post_id = 4
     msgdata = {}
     self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
     with configuration('mta', verp_delivery_interval=5):
         self._runner.run()
     self.assertFalse(captured_msgdata['verp'])
Example #13
0
 def test_no_verp_on_interval_miss(self):
     # VERP every so often, when the post_id matches.
     self._mlist.post_id = 4
     msgdata = {}
     self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
     with configuration('mta', verp_delivery_interval=5):
         self._runner.run()
     self.assertFalse(captured_msgdata['verp'])
Example #14
0
 def test_personalized_full_deliveries_verp(self):
     # When deliveries are personalized, and the configuration setting
     # indicates, messages will be VERP'd.
     msgdata = {}
     self._mlist.personalize = Personalization.full
     self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
     with configuration('mta', verp_personalized_deliveries='yes'):
         self._runner.run()
     self.assertTrue(captured_msgdata['verp'])
Example #15
0
 def test_personalized_full_deliveries_verp(self):
     # When deliveries are personalized, and the configuration setting
     # indicates, messages will be VERP'd.
     msgdata = {}
     self._mlist.personalize = Personalization.full
     self._outq.enqueue(self._msg, msgdata, listid='test.example.com')
     with configuration('mta', verp_personalized_deliveries='yes'):
         self._runner.run()
     self.assertTrue(captured_msgdata['verp'])
Example #16
0
 def test_mhonarc(self):
     # The archiver properly sends stdin to the subprocess.
     with configuration('archiver.mhonarc',
                        configuration=self._cfg,
                        enable='yes'):
         MHonArc().archive_message(self._mlist, self._msg)
     with open(self._output_file, 'r', encoding='utf-8') as fp:
         results = fp.read().splitlines()
     self.assertEqual(results[0], '<ant>')
     self.assertEqual(results[1], 'MS6QLWERIJLGCRF44J7USBFDELMNT2BW')
Example #17
0
 def test_mhonarc(self):
     # The archiver properly sends stdin to the subprocess.
     with configuration('archiver.mhonarc',
                        configuration=self._cfg,
                        enable='yes'):
         MHonArc().archive_message(self._mlist, self._msg)
     with open(self._output_file, 'r', encoding='utf-8') as fp:
         results = fp.read().splitlines()
     self.assertEqual(results[0], '<ant>')
     self.assertEqual(results[1], 'MS6QLWERIJLGCRF44J7USBFDELMNT2BW')
Example #18
0
 def test_find_pluggable_components_by_plugin_name(self):
     path = resource_filename('mailman.plugins.testing', '')
     with ExitStack() as resources:
         resources.enter_context(hack_syspath(0, path))
         resources.enter_context(
             configuration(
                 'plugin.example', **{
                     'class': 'example.hooks.ExamplePlugin',
                     'enabled': 'yes',
                 }))
         components = list(find_pluggable_components('rules', IRule))
     self.assertIn('example-rule', {rule.name for rule in components})
Example #19
0
def make_temporary(database):
    """Adapts by monkey patching an existing SQLite IDatabase."""
    tempdir = tempfile.mkdtemp()
    url = 'sqlite:///' + os.path.join(tempdir, 'mailman.db')
    with configuration('database', url=url):
        database.initialize()
    database._cleanup = types.MethodType(
        partial(_cleanup, tempdir=tempdir),
        database)
    # bool column values in SQLite must be integers.
    database.FALSE = 0
    database.TRUE = 1
    return database
Example #20
0
 def test_find_pluggable_components_by_component_package(self):
     with ExitStack() as resources:
         testing_path = resources.enter_context(
             path('mailman.plugins.testing', ''))
         resources.enter_context(hack_syspath(0, str(testing_path)))
         resources.enter_context(
             configuration(
                 'plugin.example', **{
                     'class': 'example.hooks.ExamplePlugin',
                     'enabled': 'yes',
                     'component_package': 'alternate',
                 }))
         components = list(find_pluggable_components('rules', IRule))
     self.assertNotIn('example-rule', {rule.name for rule in components})
     self.assertIn('alternate-rule', {rule.name for rule in components})
Example #21
0
 def test_error_with_numeric_port(self):
     # Test the code path where a socket.error is raised in the delivery
     # function, and the MTA port is set to zero.  The only real effect of
     # that is a log message.  Start by opening the error log and reading
     # the current file position.
     mark = LogFileMark('mailman.error')
     self._outq.enqueue(self._msg, {}, listid='test.example.com')
     with configuration('mta', smtp_port=2112):
         self._runner.run()
     line = mark.readline()
     # The log line will contain a variable timestamp, the PID, and a
     # trailing newline.  Ignore these.
     self.assertEqual(
         line[-53:-1],
         'Cannot connect to SMTP server localhost on port 2112')
Example #22
0
    def test_passlib_from_file_path(self):
        # Set up this test to use a passlib configuration file specified with
        # a file system path.  We prove we're using the new configuration
        # because a non-prefixed, i.e. non-roundup, plaintext hash algorithm
        # will be used.  When a file system path is used, the file can end in
        # any suffix.
        config_file = os.path.join(config.VAR_DIR, 'passlib.config')
        with open(config_file, 'w') as fp:
            print("""\
[passlib]
schemes = plaintext
""", file=fp)
        with configuration('passwords', configuration=config_file):
            self.assertEqual(config.password_context.encrypt('my password'),
                             'my password')
Example #23
0
 def test_error_with_numeric_port(self):
     # Test the code path where a socket.error is raised in the delivery
     # function, and the MTA port is set to zero.  The only real effect of
     # that is a log message.  Start by opening the error log and reading
     # the current file position.
     mark = LogFileMark('mailman.error')
     self._outq.enqueue(self._msg, {}, listid='test.example.com')
     with configuration('mta', smtp_port=2112):
         self._runner.run()
     line = mark.readline()
     # The log line will contain a variable timestamp, the PID, and a
     # trailing newline.  Ignore these.
     self.assertEqual(
         line[-53:-1],
         'Cannot connect to SMTP server localhost on port 2112')
Example #24
0
 def test_push_and_pop_trigger_events(self):
     # Pushing a new configuration onto the stack triggers a
     # post-processing event.
     events = []
     def on_event(event):
         if isinstance(event, ConfigurationUpdatedEvent):
             # Record both the event and the top overlay.
             events.append(event.config.overlays[0].name)
     with event_subscribers(on_event):
         with configuration('test', _configname='my test'):
             pass
     # There should be two pushed configuration names on the list now, one
     # for the push leaving 'my test' on the top of the stack, and one for
     # the pop, leaving the ConfigLayer's 'test config' on top.
     self.assertEqual(events, ['my test', 'test config'])
Example #25
0
    def test_passlib_from_file_path(self):
        # Set up this test to use a passlib configuration file specified with
        # a file system path.  We prove we're using the new configuration
        # because a non-prefixed, i.e. non-roundup, plaintext hash algorithm
        # will be used.  When a file system path is used, the file can end in
        # any suffix.
        config_file = os.path.join(config.VAR_DIR, 'passlib.config')
        with open(config_file, 'w') as fp:
            print("""\
[passlib]
schemes = plaintext
""", file=fp)
        with configuration('passwords', configuration=config_file):
            self.assertEqual(config.password_context.encrypt('my password'),
                             'my password')
Example #26
0
 def test_master_is_elsewhere_and_findable(self):
     with ExitStack() as resources:
         bin_dir = resources.enter_context(TemporaryDirectory())
         old_master = os.path.join(config.BIN_DIR, 'master')
         new_master = os.path.join(bin_dir, 'master')
         shutil.move(old_master, new_master)
         resources.enter_context(
             configuration('paths.testing', bin_dir=bin_dir))
         resources.callback(shutil.move, new_master, old_master)
         # Starting mailman should find master in the new bin_dir.
         self.command.process(self.args)
         # There should a pid file and the process it describes should be
         # killable.  We might have to wait until the process has started.
         master_pid = find_master()
         self.assertIsNotNone(master_pid, 'master did not start')
         kill_with_extreme_prejudice(master_pid)
 def test_list_id_and_language_code_allowed_in_template_uri(self):
     # Issue #196 - allow the list_id in the template uri expansion.
     list_dir = os.path.join(config.TEMPLATE_DIR, 'lists',
                             'ant.example.com', 'it')
     os.makedirs(list_dir)
     footer_path = os.path.join(list_dir, 'myfooter.txt')
     with open(footer_path, 'w', encoding='utf-8') as fp:
         print('${testarchiver_url}', file=fp)
     getUtility(ITemplateManager).set(
         'list:member:regular:footer', self._mlist.list_id,
         'mailman:///${list_id}/${language}/myfooter.txt')
     self._mlist.preferred_language = 'it'
     with configuration('language.it', charset='iso-8859-1'):
         # Default charset='utf-8' base64 encodes the message body.
         decorate.process(self._mlist, self._msg, {})
     self.assertIn('http://example.com/link_to_message',
                   self._msg.as_string())
 def test_master_is_elsewhere_and_findable(self):
     with ExitStack() as resources:
         bin_dir = resources.enter_context(TemporaryDirectory())
         old_master = os.path.join(config.BIN_DIR, 'master')
         new_master = os.path.join(bin_dir, 'master')
         shutil.move(old_master, new_master)
         resources.callback(shutil.move, new_master, old_master)
         with configuration('paths.testing', bin_dir=bin_dir):
             results = self._command.invoke(start)
         # Argument #2 to the execl() call should be the path to the master
         # program, and the path should exist.
         self.assertEqual(len(self._execl.call_args_list), 1,
                          results.output)
         posargs, kws = self._execl.call_args_list[0]
         master_path = posargs[2]
         self.assertEqual(os.path.basename(master_path), 'master')
         self.assertTrue(os.path.exists(master_path), master_path)
Example #29
0
 def test_master_is_elsewhere_and_findable(self):
     with ExitStack() as resources:
         bin_dir = resources.enter_context(TemporaryDirectory())
         old_master = os.path.join(config.BIN_DIR, 'master')
         new_master = os.path.join(bin_dir, 'master')
         shutil.move(old_master, new_master)
         resources.enter_context(
             configuration('paths.testing', bin_dir=bin_dir))
         resources.callback(shutil.move, new_master, old_master)
         # Starting mailman should find master in the new bin_dir.
         self.command.process(self.args)
         # There should a pid file and the process it describes should be
         # killable.  We might have to wait until the process has started.
         master_pid = find_master()
         self.assertIsNotNone(master_pid, 'master did not start')
         os.kill(master_pid, signal.SIGTERM)
         os.waitpid(master_pid, 0)
Example #30
0
 def test_error_with_port_0(self):
     # Test the code path where a socket.error is raised in the delivery
     # function, and the MTA port is set to zero.  The only real effect of
     # that is a log message.  Start by opening the error log and reading
     # the current file position.
     error_log = logging.getLogger('mailman.error')
     filename = error_log.handlers[0].filename
     filepos = os.stat(filename).st_size
     self._outq.enqueue(self._msg, {}, listid='test.example.com')
     with configuration('mta', smtp_port=0):
         self._runner.run()
     with open(filename) as fp:
         fp.seek(filepos)
         line = fp.readline()
     # The log line will contain a variable timestamp, the PID, and a
     # trailing newline.  Ignore these.
     self.assertEqual(
         line[-53:-1],
         'Cannot connect to SMTP server localhost on port smtp')
Example #31
0
 def test_error_with_port_0(self):
     # Test the code path where a socket.error is raised in the delivery
     # function, and the MTA port is set to zero.  The only real effect of
     # that is a log message.  Start by opening the error log and reading
     # the current file position.
     error_log = logging.getLogger('mailman.error')
     filename = error_log.handlers[0].filename
     filepos = os.stat(filename).st_size
     self._outq.enqueue(self._msg, {}, listid='test.example.com')
     with configuration('mta', smtp_port=0):
         self._runner.run()
     with open(filename) as fp:
         fp.seek(filepos)
         line = fp.readline()
     # The log line will contain a variable timestamp, the PID, and a
     # trailing newline.  Ignore these.
     self.assertEqual(
         line[-53:-1],
         'Cannot connect to SMTP server localhost on port smtp')
Example #32
0
def make_temporary(database):
    """Adapts by monkey patching an existing PostgreSQL IDatabase."""
    from mailman.config import config

    parts = urlsplit(config.database.url)
    assert parts.scheme == "postgres"
    new_parts = list(parts)
    new_parts[2] = "/mmtest"
    url = urlunsplit(new_parts)
    # Use the existing database connection to create a new testing
    # database.
    config.db.store.execute("ABORT;")
    config.db.store.execute("CREATE DATABASE mmtest;")
    with configuration("database", url=url):
        database.initialize()
    database._cleanup = types.MethodType(partial(_cleanup, store=database.store, tempdb_name="mmtest"), database)
    # bool column values in PostgreSQL.
    database.FALSE = "False"
    database.TRUE = "True"
    return database
    def test_valid_password_migrates(self):
        # Now that the moderator password is set, change the default password
        # hashing algorithm.  When the old password is validated, it will be
        # automatically migrated to the new hash.
        self.assertEqual(self._mlist.moderator_password,
                         '{plaintext}super secret')
        config_file = os.path.join(config.VAR_DIR, 'passlib.config')
        # XXX passlib seems to choose the default hashing scheme even if it is
        # deprecated.  The default scheme is either specified explicitly, or
        # is the first in this list.  This seems like a bug.
        with open(config_file, 'w') as fp:
            print("""\
[passlib]
schemes = roundup_plaintext, plaintext
default = plaintext
deprecated = roundup_plaintext
""", file=fp)
        with configuration('passwords', configuration=config_file):
            self._msg['Approved'] = 'super secret'
            result = self._rule.check(self._mlist, self._msg, {})
            self.assertTrue(result)
        self.assertEqual(self._mlist.moderator_password, 'super secret')
Example #34
0
    def test_valid_password_migrates(self):
        # Now that the moderator password is set, change the default password
        # hashing algorithm.  When the old password is validated, it will be
        # automatically migrated to the new hash.
        self.assertEqual(self._mlist.moderator_password,
                         '{plaintext}super secret')
        config_file = os.path.join(config.VAR_DIR, 'passlib.config')
        # XXX passlib seems to choose the default hashing scheme even if it is
        # deprecated.  The default scheme is either specified explicitly, or
        # is the first in this list.  This seems like a bug.
        with open(config_file, 'w') as fp:
            print("""\
[passlib]
schemes = roundup_plaintext, plaintext
default = plaintext
deprecated = roundup_plaintext
""", file=fp)
        with configuration('passwords', configuration=config_file):
            self._msg['Approved'] = 'super secret'
            result = self._rule.check(self._mlist, self._msg, {})
            self.assertTrue(result)
        self.assertEqual(self._mlist.moderator_password, 'super secret')
Example #35
0
def use_test_organizational_data():
    # Point the organizational URL to our test data.
    path = resource_filename('mailman.rules.tests.data', 'org_domain.txt')
    url = 'file:///{}'.format(path)
    return configuration('dmarc', org_domain_data_url=url)