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.
This commit is contained in:
Tom Daff 2019-01-03 14:53:27 +00:00 committed by Tim Abbott
parent e3c8e8a839
commit f2e06128c6
2 changed files with 38 additions and 10 deletions

View File

@ -4,6 +4,7 @@ import logging
import re import re
from email.header import decode_header, Header from email.header import decode_header, Header
from email.utils import getaddresses
import email.message as message import email.message as message
from django.conf import settings 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 # 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 # it is more accurate, so try to find the most-accurate
# recipient list in descending priority order # recipient list in descending priority order
recipient_headers = ["X-Gm-Original-To", "Delivered-To", "To"] recipient_headers = ["X-Gm-Original-To", "Delivered-To",
recipients = [] # type: List[Union[str, Header]] "Resent-To", "Resent-CC", "To", "CC"]
for recipient_header in recipient_headers:
r = message.get_all(recipient_header, None)
if r:
recipients = r
break
pattern_parts = [re.escape(part) for part in settings.EMAIL_GATEWAY_PATTERN.split('%s')] pattern_parts = [re.escape(part) for part in settings.EMAIL_GATEWAY_PATTERN.split('%s')]
match_email_re = re.compile(".*?".join(pattern_parts)) 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): header_addresses = [str(addr)
return recipient_email 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") raise ZulipEmailForwardError("Missing recipient in mirror email")

View File

@ -185,6 +185,33 @@ class TestStreamEmailMessagesSuccess(ZulipTestCase):
self.assertEqual(get_display_recipient(message.recipient), stream.name) self.assertEqual(get_display_recipient(message.recipient), stream.name)
self.assertEqual(message.topic_name(), incoming_valid_message['Subject']) 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 <another@example.org>",
"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): class TestStreamEmailMessagesEmptyBody(ZulipTestCase):
def test_receive_stream_email_messages_empty_body(self) -> None: def test_receive_stream_email_messages_empty_body(self) -> None: