diff --git a/zerver/lib/actions.py b/zerver/lib/actions.py index ec4a1ac206..0ccafc79b8 100644 --- a/zerver/lib/actions.py +++ b/zerver/lib/actions.py @@ -4465,20 +4465,28 @@ def get_email_gateway_message_string_from_address(address: str) -> Optional[str] return msg_string -def decode_email_address(email: str) -> Optional[Tuple[str, str]]: - # Perform the reverse of encode_email_address. Returns a tuple of (streamname, email_token) +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, show_sender) msg_string = get_email_gateway_message_string_from_address(email) - if msg_string is 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 # that have + signs in them (see Trac #2102) encoded_stream_name, token = msg_string.split('.') else: 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) - return stream_name, token + return stream_name, token, show_sender SubHelperT = Tuple[List[Dict[str, Any]], List[Dict[str, Any]], List[Dict[str, Any]]] diff --git a/zerver/lib/email_mirror.py b/zerver/lib/email_mirror.py index c04f81d352..0fd5e639fc 100644 --- a/zerver/lib/email_mirror.py +++ b/zerver/lib/email_mirror.py @@ -1,4 +1,4 @@ -from typing import Any, Dict, Optional +from typing import Any, Dict, Optional, Tuple import logging import re @@ -133,7 +133,8 @@ def mark_missed_message_address_as_used(address: str) -> None: redis_client.delete(key) 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) # Remove null characters, since Zulip will reject body = body.replace("\x00", "") @@ -142,6 +143,11 @@ def construct_zulip_body(message: message.Message, realm: Realm) -> str: body = body.strip() if not body: body = '(No email body)' + + if show_sender: + sender = message.get("From") + body = "From: %s\n%s" % (sender, body) + return body 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) -def extract_and_validate(email: str) -> Stream: +def extract_and_validate(email: str) -> Tuple[Stream, bool]: temp = decode_email_address(email) if temp is None: raise ZulipEmailForwardError("Malformed email recipient " + email) - stream_name, token = temp + stream_name, token, show_sender = temp if not valid_stream(stream_name, token): 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: # 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, debug_info: Dict[str, Any]) -> None: - stream = extract_and_validate(to) - body = construct_zulip_body(message, stream.realm) + stream, show_sender = extract_and_validate(to) + body = construct_zulip_body(message, stream.realm, show_sender) debug_info["stream"] = stream send_zulip(settings.EMAIL_GATEWAY_BOT, stream, subject, body) logger.info("Successfully processed email to %s (%s)" % ( diff --git a/zerver/tests/test_email_mirror.py b/zerver/tests/test_email_mirror.py index fcf29ef635..c381bcdb9c 100644 --- a/zerver/tests/test_email_mirror.py +++ b/zerver/tests/test_email_mirror.py @@ -54,24 +54,54 @@ class TestEncodeDecode(ZulipTestCase): self.assertTrue(email_address.endswith('@testserver')) tup = decode_email_address(email_address) 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) - email_address = email_address.replace('+', '.') - tup = decode_email_address(email_address) + parts = email_address.split('@') + parts[0] += "+show-sender" + email_address_show = '@'.join(parts) + tup = decode_email_address(email_address_show) 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(token, stream.email_token) 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_show), None) with self.settings(EMAIL_GATEWAY_EXTRA_PATTERN_HACK='@zulip.org'): tup = decode_email_address(email_address) 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(token, stream.email_token) @@ -206,6 +236,30 @@ 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_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): def test_receive_stream_email_messages_empty_body(self) -> None: