diff options
Diffstat (limited to 'src/mailman/utilities')
| -rw-r--r-- | src/mailman/utilities/email.py | 33 | ||||
| -rw-r--r-- | src/mailman/utilities/interact.py | 86 | ||||
| -rw-r--r-- | src/mailman/utilities/tests/test_email.py | 75 |
3 files changed, 191 insertions, 3 deletions
diff --git a/src/mailman/utilities/email.py b/src/mailman/utilities/email.py index 4b1023ce1..38c9980a2 100644 --- a/src/mailman/utilities/email.py +++ b/src/mailman/utilities/email.py @@ -21,10 +21,17 @@ from __future__ import absolute_import, unicode_literals __metaclass__ = type __all__ = [ + 'add_message_hash', 'split_email', ] +from base64 import b32encode +from hashlib import sha1 + + + + def split_email(address): """Split an email address into a user name and domain. @@ -39,3 +46,29 @@ def split_email(address): # There was no at-sign in the email address. return local_part, None return local_part, domain.split('.') + + +def add_message_hash(msg): + """Add a X-Message-ID-Hash header derived from Message-ID. + + This function works by side-effect; the original message is mutated. Any + existing X-Message-ID-Headers are deleted if a Message-ID header is + found. If no Message-ID header is found, the original message is not + modified. + + :param msg: An email message + :type msg: `email.message.Message` or derived + """ + message_id = msg.get('message-id') + if message_id is None: + return + # The angle brackets are not part of the Message-ID. See RFC 2822 + # and http://wiki.list.org/display/DEV/Stable+URLs + if message_id.startswith('<') and message_id.endswith('>'): + message_id = message_id[1:-1] + else: + message_id = message_id.strip() + digest = sha1(message_id).digest() + message_id_hash = b32encode(digest) + del msg['x-message-id-hash'] + msg['X-Message-ID-Hash'] = message_id_hash diff --git a/src/mailman/utilities/interact.py b/src/mailman/utilities/interact.py new file mode 100644 index 000000000..dd6bac9f0 --- /dev/null +++ b/src/mailman/utilities/interact.py @@ -0,0 +1,86 @@ +# Copyright (C) 2006-2012 by the Free Software Foundation, Inc. +# +# This file is part of GNU Mailman. +# +# GNU Mailman is free software: you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# GNU Mailman is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +# more details. +# +# You should have received a copy of the GNU General Public License along with +# GNU Mailman. If not, see <http://www.gnu.org/licenses/>. + +"""Provide an interactive prompt, mimicking the Python interpreter.""" + +from __future__ import unicode_literals + +__metaclass__ = type +__all__ = [ + 'interact', + ] + + +import os +import sys +import code + +DEFAULT_BANNER = '' + + + +def interact(upframe=True, banner=DEFAULT_BANNER, overrides=None): + """Start an interactive interpreter prompt. + + :param upframe: Whether or not to populate the interpreter's globals with + the locals from the frame that called this function. + :type upfframe: bool + :param banner: The banner to print before the interpreter starts. + :type banner: string + :param overrides: Additional interpreter globals to add. + :type overrides: dict + """ + # The interactive prompt's namespace. + namespace = dict() + # Populate the console's with the locals of the frame that called this + # function (i.e. one up from here). + if upframe: + frame = sys._getframe(1) + namespace.update(frame.f_globals) + namespace.update(frame.f_locals) + if overrides is not None: + namespace.update(overrides) + interp = code.InteractiveConsole(namespace) + # Try to import the readline module, but don't worry if it's unavailable. + try: + import readline + except ImportError: + pass + # Mimic the real interactive interpreter's loading of any $PYTHONSTARTUP + # file. Note that if the startup file is not prepared to be exec'd more + # than once, this could cause a problem. + startup = os.environ.get('PYTHONSTARTUP') + if startup: + try: + execfile(startup, namespace) + except: + pass + # We don't want the funky console object in parentheses in the banner. + if banner == DEFAULT_BANNER: + banner = '''\ +Python %s on %s +Type "help", "copyright", "credits" or "license" for more information.''' % ( + sys.version, sys.platform) + elif not banner: + banner = None + interp.interact(banner) + # When an exception occurs in the InteractiveConsole, the various + # sys.exc_* attributes get set so that error handling works the same way + # there as it does in the built-in interpreter. Be anal about clearing + # any exception information before we're done. + sys.exc_clear() + sys.last_type = sys.last_value = sys.last_traceback = None diff --git a/src/mailman/utilities/tests/test_email.py b/src/mailman/utilities/tests/test_email.py index 833d631b5..478322aaf 100644 --- a/src/mailman/utilities/tests/test_email.py +++ b/src/mailman/utilities/tests/test_email.py @@ -15,22 +15,27 @@ # You should have received a copy of the GNU General Public License along with # GNU Mailman. If not, see <http://www.gnu.org/licenses/>. -"""Testing app.bounces functions.""" +"""Testing functions in the email utilities.""" -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, print_function, unicode_literals __metaclass__ = type __all__ = [ + 'TestEmail', ] import unittest -from mailman.utilities.email import split_email +from mailman.testing.helpers import ( + specialized_message_from_string as mfs) +from mailman.utilities.email import add_message_hash, split_email class TestEmail(unittest.TestCase): + """Testing functions in the email utilities.""" + def test_normal_split(self): self.assertEqual(split_email('anne@example.com'), ('anne', ['example', 'com'])) @@ -39,3 +44,67 @@ class TestEmail(unittest.TestCase): def test_no_at_split(self): self.assertEqual(split_email('anne'), ('anne', None)) + + def test_adding_the_message_hash(self): + # When the message has a Message-ID header, this will add the + # X-Mailman-Hash-ID header. + msg = mfs("""\ +Message-ID: <aardvark> + +""") + add_message_hash(msg) + self.assertEqual(msg['x-message-id-hash'], + '75E2XSUXAFQGWANWEROVQ7JGYMNWHJBT') + + def test_remove_hash_headers_first(self): + # Any existing X-Mailman-Hash-ID header is removed first. + msg = mfs("""\ +Message-ID: <aardvark> +X-Message-ID-Hash: abc + +""") + add_message_hash(msg) + headers = msg.get_all('x-message-id-hash') + self.assertEqual(len(headers), 1) + self.assertEqual(headers[0], '75E2XSUXAFQGWANWEROVQ7JGYMNWHJBT') + + def test_hash_header_left_alone_if_no_message_id(self): + # If the original message has no Message-ID header, then any existing + # X-Message-ID-Hash headers are left intact. + msg = mfs("""\ +X-Message-ID-Hash: abc + +""") + add_message_hash(msg) + headers = msg.get_all('x-message-id-hash') + self.assertEqual(len(headers), 1) + self.assertEqual(headers[0], 'abc') + + def test_angle_brackets_dont_contribute_to_hash(self): + # According to RFC 5322, the [matching] angle brackets do not + # contribute to the hash. + msg = mfs("""\ +Message-ID: aardvark + +""") + add_message_hash(msg) + self.assertEqual(msg['x-message-id-hash'], + '75E2XSUXAFQGWANWEROVQ7JGYMNWHJBT') + + def test_mismatched_angle_brackets_do_contribute_to_hash(self): + # According to RFC 5322, the [matching] angle brackets do not + # contribute to the hash. + msg = mfs("""\ +Message-ID: <aardvark + +""") + add_message_hash(msg) + self.assertEqual(msg['x-message-id-hash'], + 'AOJ545GHRYD2Y3RUFG2EWMPHUABTG4SM') + msg = mfs("""\ +Message-ID: aardvark> + +""") + add_message_hash(msg) + self.assertEqual(msg['x-message-id-hash'], + '5KH3RA7ZM4VM6XOZXA7AST2XN2X4S3WY') |
