From f2e06128c63be68ef3f963eba642c9fc85ee2f1a Mon Sep 17 00:00:00 2001 From: Tom Daff Date: Thu, 3 Jan 2019 14:53:27 +0000 Subject: [PATCH] email_mirror: Add email address parsing. When trying to find the email gateway address, use the `email.util.getaddresses` function to deal with cases where multiple recipients are included in the email header or the stream address appears as an angle-addr with a name given (e.g. if someone added it to their address book). Added some other headers where the required address may appear: "Resent" headers are sometimes used for forwarding, and streams may also be found in CC. There is no way to find the address if the email was recieved as a BCC. --- zerver/lib/email_mirror.py | 21 +++++++++++---------- zerver/tests/test_email_mirror.py | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/zerver/lib/email_mirror.py b/zerver/lib/email_mirror.py index 0959cd5dcd..8469537640 100644 --- a/zerver/lib/email_mirror.py +++ b/zerver/lib/email_mirror.py @@ -4,6 +4,7 @@ import logging import re from email.header import decode_header, Header +from email.utils import getaddresses import email.message as message from django.conf import settings @@ -287,19 +288,19 @@ def find_emailgateway_recipient(message: message.Message) -> str: # We can't use Delivered-To; if there is a X-Gm-Original-To # it is more accurate, so try to find the most-accurate # recipient list in descending priority order - recipient_headers = ["X-Gm-Original-To", "Delivered-To", "To"] - recipients = [] # type: List[Union[str, Header]] - for recipient_header in recipient_headers: - r = message.get_all(recipient_header, None) - if r: - recipients = r - break + recipient_headers = ["X-Gm-Original-To", "Delivered-To", + "Resent-To", "Resent-CC", "To", "CC"] pattern_parts = [re.escape(part) for part in settings.EMAIL_GATEWAY_PATTERN.split('%s')] match_email_re = re.compile(".*?".join(pattern_parts)) - for recipient_email in [str(recipient) for recipient in recipients]: - if match_email_re.match(recipient_email): - return recipient_email + + header_addresses = [str(addr) + for recipient_header in recipient_headers + for addr in message.get_all(recipient_header, [])] + + for addr_tuple in getaddresses(header_addresses): + if match_email_re.match(addr_tuple[1]): + return addr_tuple[1] raise ZulipEmailForwardError("Missing recipient in mirror email") diff --git a/zerver/tests/test_email_mirror.py b/zerver/tests/test_email_mirror.py index 84ed3fe5ee..9092c9d41e 100644 --- a/zerver/tests/test_email_mirror.py +++ b/zerver/tests/test_email_mirror.py @@ -185,6 +185,33 @@ class TestStreamEmailMessagesSuccess(ZulipTestCase): self.assertEqual(get_display_recipient(message.recipient), stream.name) self.assertEqual(message.topic_name(), incoming_valid_message['Subject']) + def test_receive_stream_email_multiple_recipient_success(self) -> None: + user_profile = self.example_user('hamlet') + self.login(user_profile.email) + self.subscribe(user_profile, "Denmark") + stream = get_stream("Denmark", user_profile.realm) + + # stream address is angle-addr within multiple addresses + stream_to_addresses = ["A.N. Other ", + "Denmark <{}>".format(encode_email_address(stream))] + + incoming_valid_message = MIMEText('TestStreamEmailMessages Body') # type: Any # https://github.com/python/typeshed/issues/275 + + incoming_valid_message['Subject'] = 'TestStreamEmailMessages Subject' + incoming_valid_message['From'] = self.example_email('hamlet') + incoming_valid_message['To'] = ", ".join(stream_to_addresses) + incoming_valid_message['Reply-to'] = self.example_email('othello') + + process_message(incoming_valid_message) + + # Hamlet is subscribed to this stream so should see the email message from Othello. + message = most_recent_message(user_profile) + + self.assertEqual(message.content, "TestStreamEmailMessages Body") + self.assertEqual(get_display_recipient(message.recipient), stream.name) + self.assertEqual(message.topic_name(), incoming_valid_message['Subject']) + + class TestStreamEmailMessagesEmptyBody(ZulipTestCase): def test_receive_stream_email_messages_empty_body(self) -> None: