email_mirror: Add the sender at the start of stream message.

Fixes part 3 of #10612. When sending an email to the email mirror to a
stream address, if "+show-sender" is added in the address, the stream
message will now include "From: <sender>" at the top.
This commit is contained in:
Mateusz Mandera 2019-02-08 14:13:33 +01:00 committed by Tim Abbott
parent 60c7467464
commit dbff533e09
3 changed files with 85 additions and 17 deletions

View File

@ -4465,20 +4465,28 @@ def get_email_gateway_message_string_from_address(address: str) -> Optional[str]
return msg_string return msg_string
def decode_email_address(email: str) -> Optional[Tuple[str, str]]: def decode_email_address(email: str) -> Optional[Tuple[str, str, bool]]:
# Perform the reverse of encode_email_address. Returns a tuple of (streamname, email_token) # Perform the reverse of encode_email_address. Returns a tuple of
# (streamname, email_token, show_sender)
msg_string = get_email_gateway_message_string_from_address(email) msg_string = get_email_gateway_message_string_from_address(email)
if msg_string is None: if msg_string is None:
return None return None
elif '.' in msg_string:
if msg_string.endswith(('+show-sender', '.show-sender')):
show_sender = True
msg_string = msg_string[:-12] # strip "+show-sender"
else:
show_sender = False
if '.' in msg_string:
# Workaround for Google Groups and other programs that don't accept emails # Workaround for Google Groups and other programs that don't accept emails
# that have + signs in them (see Trac #2102) # that have + signs in them (see Trac #2102)
encoded_stream_name, token = msg_string.split('.') encoded_stream_name, token = msg_string.split('.')
else: else:
encoded_stream_name, token = msg_string.split('+') encoded_stream_name, token = msg_string.split('+')
stream_name = re.sub(r"%\d{4}", lambda x: chr(int(x.group(0)[1:])), encoded_stream_name) stream_name = re.sub(r"%\d{4}", lambda x: chr(int(x.group(0)[1:])), encoded_stream_name)
return stream_name, token return stream_name, token, show_sender
SubHelperT = Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]]] SubHelperT = Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]]]

View File

@ -1,4 +1,4 @@
from typing import Any, Dict, Optional from typing import Any, Dict, Optional, Tuple
import logging import logging
import re import re
@ -133,7 +133,8 @@ def mark_missed_message_address_as_used(address: str) -> None:
redis_client.delete(key) redis_client.delete(key)
raise ZulipEmailForwardError('Missed message address has already been used') raise ZulipEmailForwardError('Missed message address has already been used')
def construct_zulip_body(message: message.Message, realm: Realm) -> str: def construct_zulip_body(message: message.Message, realm: Realm,
show_sender: bool=False) -> str:
body = extract_body(message) body = extract_body(message)
# Remove null characters, since Zulip will reject # Remove null characters, since Zulip will reject
body = body.replace("\x00", "") body = body.replace("\x00", "")
@ -142,6 +143,11 @@ def construct_zulip_body(message: message.Message, realm: Realm) -> str:
body = body.strip() body = body.strip()
if not body: if not body:
body = '(No email body)' body = '(No email body)'
if show_sender:
sender = message.get("From")
body = "From: %s\n%s" % (sender, body)
return body return body
def send_to_missed_message_address(address: str, message: message.Message) -> None: def send_to_missed_message_address(address: str, message: message.Message) -> None:
@ -282,16 +288,16 @@ def extract_and_upload_attachments(message: message.Message, realm: Realm) -> st
return "\n".join(attachment_links) return "\n".join(attachment_links)
def extract_and_validate(email: str) -> Stream: def extract_and_validate(email: str) -> Tuple[Stream, bool]:
temp = decode_email_address(email) temp = decode_email_address(email)
if temp is None: if temp is None:
raise ZulipEmailForwardError("Malformed email recipient " + email) raise ZulipEmailForwardError("Malformed email recipient " + email)
stream_name, token = temp stream_name, token, show_sender = temp
if not valid_stream(stream_name, token): if not valid_stream(stream_name, token):
raise ZulipEmailForwardError("Bad stream token from email recipient " + email) raise ZulipEmailForwardError("Bad stream token from email recipient " + email)
return Stream.objects.get(email_token=token) return Stream.objects.get(email_token=token), show_sender
def find_emailgateway_recipient(message: message.Message) -> str: 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
@ -322,8 +328,8 @@ def strip_from_subject(subject: str) -> str:
def process_stream_message(to: str, subject: str, message: message.Message, def process_stream_message(to: str, subject: str, message: message.Message,
debug_info: Dict[str, Any]) -> None: debug_info: Dict[str, Any]) -> None:
stream = extract_and_validate(to) stream, show_sender = extract_and_validate(to)
body = construct_zulip_body(message, stream.realm) body = construct_zulip_body(message, stream.realm, show_sender)
debug_info["stream"] = stream debug_info["stream"] = stream
send_zulip(settings.EMAIL_GATEWAY_BOT, stream, subject, body) send_zulip(settings.EMAIL_GATEWAY_BOT, stream, subject, body)
logger.info("Successfully processed email to %s (%s)" % ( logger.info("Successfully processed email to %s (%s)" % (

View File

@ -54,24 +54,54 @@ class TestEncodeDecode(ZulipTestCase):
self.assertTrue(email_address.endswith('@testserver')) self.assertTrue(email_address.endswith('@testserver'))
tup = decode_email_address(email_address) tup = decode_email_address(email_address)
assert tup is not None assert tup is not None
(decoded_stream_name, token) = tup (decoded_stream_name, token, show_sender) = tup
self.assertFalse(show_sender)
self.assertEqual(decoded_stream_name, stream_name) self.assertEqual(decoded_stream_name, stream_name)
self.assertEqual(token, stream.email_token) self.assertEqual(token, stream.email_token)
email_address = email_address.replace('+', '.') parts = email_address.split('@')
tup = decode_email_address(email_address) parts[0] += "+show-sender"
email_address_show = '@'.join(parts)
tup = decode_email_address(email_address_show)
assert tup is not None assert tup is not None
(decoded_stream_name, token) = tup (decoded_stream_name, token, show_sender) = tup
self.assertTrue(show_sender)
self.assertEqual(decoded_stream_name, stream_name)
self.assertEqual(token, stream.email_token)
email_address_dots = email_address.replace('+', '.')
tup = decode_email_address(email_address_dots)
assert tup is not None
(decoded_stream_name, token, show_sender) = tup
self.assertFalse(show_sender)
self.assertEqual(decoded_stream_name, stream_name)
self.assertEqual(token, stream.email_token)
email_address_dots_show = email_address_show.replace('+', '.')
tup = decode_email_address(email_address_dots_show)
assert tup is not None
(decoded_stream_name, token, show_sender) = tup
self.assertTrue(show_sender)
self.assertEqual(decoded_stream_name, stream_name) self.assertEqual(decoded_stream_name, stream_name)
self.assertEqual(token, stream.email_token) self.assertEqual(token, stream.email_token)
email_address = email_address.replace('@testserver', '@zulip.org') email_address = email_address.replace('@testserver', '@zulip.org')
email_address_show = email_address_show.replace('@testserver', '@zulip.org')
self.assertEqual(decode_email_address(email_address), None) self.assertEqual(decode_email_address(email_address), None)
self.assertEqual(decode_email_address(email_address_show), None)
with self.settings(EMAIL_GATEWAY_EXTRA_PATTERN_HACK='@zulip.org'): with self.settings(EMAIL_GATEWAY_EXTRA_PATTERN_HACK='@zulip.org'):
tup = decode_email_address(email_address) tup = decode_email_address(email_address)
assert tup is not None assert tup is not None
(decoded_stream_name, token) = tup (decoded_stream_name, token, show_sender) = tup
self.assertFalse(show_sender)
self.assertEqual(decoded_stream_name, stream_name)
self.assertEqual(token, stream.email_token)
tup = decode_email_address(email_address_show)
assert tup is not None
(decoded_stream_name, token, show_sender) = tup
self.assertTrue(show_sender)
self.assertEqual(decoded_stream_name, stream_name) self.assertEqual(decoded_stream_name, stream_name)
self.assertEqual(token, stream.email_token) self.assertEqual(token, stream.email_token)
@ -206,6 +236,30 @@ 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_show_sender_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_to_address = encode_email_address(stream)
parts = stream_to_address.split('@')
parts[0] += "+show-sender"
stream_to_address = '@'.join(parts)
incoming_valid_message = MIMEText('TestStreamEmailMessages Body')
incoming_valid_message['Subject'] = 'TestStreamEmailMessages Subject'
incoming_valid_message['From'] = self.example_email('hamlet')
incoming_valid_message['To'] = stream_to_address
incoming_valid_message['Reply-to'] = self.example_email('othello')
process_message(incoming_valid_message)
message = most_recent_message(user_profile)
self.assertEqual(message.content, "From: %s\n%s" % (self.example_email('hamlet'),
"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: