def test_missed_message(self) -> None: email = self.example_email('othello') self.login(email) result = self.client_post( "/json/messages", { "type": "private", "content": "test_receive_missed_message_email_messages", "client": "test suite", "to": ujson.dumps([ self.example_email('cordelia'), self.example_email('iago') ]) }) self.assert_json_success(result) user_profile = self.example_user('cordelia') usermessage = most_recent_usermessage(user_profile) with self.settings(EMAIL_GATEWAY_PATTERN=''): mm_address = create_missed_message_address(user_profile, usermessage.message) self.assertEqual(mm_address, FromAddress.NOREPLY)
def test_receive_missed_huddle_message_email_messages(self) -> None: # build dummy messages for missed messages email reply # have Othello send Iago and Cordelia a PM. Cordelia will reply via email # Iago and Othello will receive the message. email = self.example_email('othello') self.login(email) result = self.client_post( "/json/messages", { "type": "private", "content": "test_receive_missed_message_email_messages", "client": "test suite", "to": ujson.dumps([ self.example_email('cordelia'), self.example_email('iago') ]) }) self.assert_json_success(result) user_profile = self.example_user('cordelia') usermessage = most_recent_usermessage(user_profile) # we don't want to send actual emails but we do need to create and store the # token for looking up who did reply. mm_address = create_missed_message_address(user_profile, usermessage.message) incoming_valid_message = MIMEText( 'TestMissedHuddleMessageEmailMessages Body' ) # type: Any # https://github.com/python/typeshed/issues/275 incoming_valid_message[ 'Subject'] = 'TestMissedHuddleMessageEmailMessages Subject' incoming_valid_message['From'] = self.example_email('cordelia') incoming_valid_message['To'] = mm_address incoming_valid_message['Reply-to'] = self.example_email('cordelia') process_message(incoming_valid_message) # Confirm Iago received the message. user_profile = self.example_user('iago') message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedHuddleMessageEmailMessages Body") self.assertEqual(message.sender, self.example_user('cordelia')) self.assertEqual(message.recipient.type, Recipient.HUDDLE) # Confirm Othello received the message. user_profile = self.example_user('othello') message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedHuddleMessageEmailMessages Body") self.assertEqual(message.sender, self.example_user('cordelia')) self.assertEqual(message.recipient.type, Recipient.HUDDLE)
def test_receive_missed_message_email_token_missing_data(self) -> None: email = self.example_email('hamlet') self.login(email) result = self.client_post("/json/messages", {"type": "private", "content": "test_receive_missed_message_email_token_missing_data", "client": "test suite", "to": self.example_email('othello')}) self.assert_json_success(result) user_profile = self.example_user('othello') usermessage = most_recent_usermessage(user_profile) mm_address = create_missed_message_address(user_profile, usermessage.message) incoming_valid_message = MIMEText('TestMissedMessageEmailMessages Body') incoming_valid_message['Subject'] = 'TestMissedMessageEmailMessages Subject' incoming_valid_message['From'] = self.example_email('othello') incoming_valid_message['To'] = mm_address incoming_valid_message['Reply-to'] = self.example_email('othello') # We need to force redis_client.hmget to return some None values: with mock.patch('zerver.lib.email_mirror.redis_client.hmget', return_value=[None, None, None]): exception_message = '' try: process_missed_message(mm_address, incoming_valid_message, False) except ZulipEmailForwardError as e: exception_message = str(e) self.assertEqual(exception_message, 'Missing missed message address data')
def do_send_missedmessage_events_reply_in_zulip(user_profile, missed_messages, message_count): # type: (UserProfile, List[Message], int) -> None """ Send a reminder email to a user if she's missed some PMs by being offline. The email will have its reply to address set to a limited used email address that will send a zulip message to the correct recipient. This allows the user to respond to missed PMs, huddles, and @-mentions directly from the email. `user_profile` is the user to send the reminder to `missed_messages` is a list of Message objects to remind about they should all have the same recipient and subject """ # Disabled missedmessage emails internally if not user_profile.enable_offline_email_notifications: return recipients = set((msg.recipient_id, msg.subject) for msg in missed_messages) if len(recipients) != 1: raise ValueError( 'All missed_messages must have the same recipient and subject %r' % recipients ) template_payload = { 'name': user_profile.full_name, 'messages': build_message_list(user_profile, missed_messages), 'message_count': message_count, 'url': 'https://%s' % (settings.EXTERNAL_HOST,), 'reply_warning': False, 'external_host': settings.EXTERNAL_HOST, 'mention': missed_messages[0].recipient.type == Recipient.STREAM, 'reply_to_zulip': True, } headers = {} from zerver.lib.email_mirror import create_missed_message_address address = create_missed_message_address(user_profile, missed_messages[0]) headers['Reply-To'] = address senders = set(m.sender.full_name for m in missed_messages) sender_str = ", ".join(senders) plural_messages = 's' if len(missed_messages) > 1 else '' subject = "Missed Zulip%s from %s" % (plural_messages, sender_str) from_email = "%s (via Zulip) <%s>" % (sender_str, settings.NOREPLY_EMAIL_ADDRESS) text_content = loader.render_to_string('zerver/missed_message_email.txt', template_payload) html_content = loader.render_to_string('zerver/missed_message_email_html.txt', template_payload) msg = EmailMultiAlternatives(subject, text_content, from_email, [user_profile.email], headers = headers) msg.attach_alternative(html_content, "text/html") msg.send() user_profile.last_reminder = datetime.datetime.now() user_profile.save(update_fields=['last_reminder'])
def test_receive_missed_huddle_message_email_messages(self): # build dummy messages for missed messages email reply # have Othello send Iago and Cordelia a PM. Cordelia will reply via email # Iago and Othello will receive the message. self.login("*****@*****.**") result = self.client.post( "/json/messages", { "type": "private", "content": "test_receive_missed_message_email_messages", "client": "test suite", "to": ujson.dumps(["*****@*****.**", "*****@*****.**"]) }) self.assert_json_success(result) user_profile = get_user_profile_by_email("*****@*****.**") usermessage = most_recent_usermessage(user_profile) # we don't want to send actual emails but we do need to create and store the # token for looking up who did reply. mm_address = create_missed_message_address(user_profile, usermessage.message) incoming_valid_message = MIMEText( 'TestMissedHuddleMessageEmailMessages Body') incoming_valid_message[ 'Subject'] = 'TestMissedHuddleMessageEmailMessages Subject' incoming_valid_message['From'] = "*****@*****.**" incoming_valid_message['To'] = mm_address incoming_valid_message['Reply-to'] = "*****@*****.**" process_message(incoming_valid_message) # Confirm Iago received the message. user_profile = get_user_profile_by_email("*****@*****.**") message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedHuddleMessageEmailMessages Body") self.assertEqual(message.sender, get_user_profile_by_email("*****@*****.**")) self.assertEqual(message.recipient.type, Recipient.HUDDLE) # Confirm Othello received the message. user_profile = get_user_profile_by_email("*****@*****.**") message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedHuddleMessageEmailMessages Body") self.assertEqual(message.sender, get_user_profile_by_email("*****@*****.**")) self.assertEqual(message.recipient.type, Recipient.HUDDLE)
def test_missed_message(self) -> None: email = self.example_email('othello') self.login(email) result = self.client_post("/json/messages", {"type": "private", "content": "test_receive_missed_message_email_messages", "client": "test suite", "to": ujson.dumps([self.example_email('cordelia'), self.example_email('iago')])}) self.assert_json_success(result) user_profile = self.example_user('cordelia') usermessage = most_recent_usermessage(user_profile) with self.settings(EMAIL_GATEWAY_PATTERN=''): mm_address = create_missed_message_address(user_profile, usermessage.message) self.assertEqual(mm_address, FromAddress.NOREPLY)
def send_private_message(self) -> str: email = self.example_email('othello') self.login(email) result = self.client_post( "/json/messages", { "type": "private", "content": "test_receive_missed_message_email_messages", "client": "test suite", "to": ujson.dumps([self.example_email('cordelia'), self.example_email('iago')]) }) self.assert_json_success(result) user_profile = self.example_user('cordelia') user_message = most_recent_usermessage(user_profile) return create_missed_message_address(user_profile, user_message.message)
def send_private_message(self) -> Text: email = self.example_email('othello') self.login(email) result = self.client_post( "/json/messages", { "type": "private", "content": "test_receive_missed_message_email_messages", "client": "test suite", "to": ujson.dumps([self.example_email('cordelia'), self.example_email('iago')]) }) self.assert_json_success(result) user_profile = self.example_user('cordelia') user_message = most_recent_usermessage(user_profile) return create_missed_message_address(user_profile, user_message.message)
def test_receive_missed_personal_message_email_messages(self): # type: () -> None # build dummy messages for missed messages email reply # have Hamlet send Othello a PM. Othello will reply via email # Hamlet will receive the message. email = self.example_email('hamlet') self.login(email) result = self.client_post( "/json/messages", { "type": "private", "content": "test_receive_missed_message_email_messages", "client": "test suite", "to": self.example_email('othello') }) self.assert_json_success(result) user_profile = self.example_user('othello') usermessage = most_recent_usermessage(user_profile) # we don't want to send actual emails but we do need to create and store the # token for looking up who did reply. mm_address = create_missed_message_address(user_profile, usermessage.message) incoming_valid_message = MIMEText( 'TestMissedMessageEmailMessages Body' ) # type: Any # https://github.com/python/typeshed/issues/275 incoming_valid_message[ 'Subject'] = 'TestMissedMessageEmailMessages Subject' incoming_valid_message['From'] = self.example_email('othello') incoming_valid_message['To'] = mm_address incoming_valid_message['Reply-to'] = self.example_email('othello') process_message(incoming_valid_message) # self.login(self.example_email("hamlet")) # confirm that Hamlet got the message user_profile = self.example_user('hamlet') message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedMessageEmailMessages Body") self.assertEqual(message.sender, self.example_user('othello')) self.assertEqual(message.recipient.id, user_profile.id) self.assertEqual(message.recipient.type, Recipient.PERSONAL)
def test_receive_missed_huddle_message_email_messages(self): # type: () -> None # build dummy messages for missed messages email reply # have Othello send Iago and Cordelia a PM. Cordelia will reply via email # Iago and Othello will receive the message. email = self.example_email('othello') self.login(email) result = self.client_post("/json/messages", {"type": "private", "content": "test_receive_missed_message_email_messages", "client": "test suite", "to": ujson.dumps([self.example_email('cordelia'), self.example_email('iago')])}) self.assert_json_success(result) user_profile = self.example_user('cordelia') usermessage = most_recent_usermessage(user_profile) # we don't want to send actual emails but we do need to create and store the # token for looking up who did reply. mm_address = create_missed_message_address(user_profile, usermessage.message) incoming_valid_message = MIMEText('TestMissedHuddleMessageEmailMessages Body') # type: Any # https://github.com/python/typeshed/issues/275 incoming_valid_message['Subject'] = 'TestMissedHuddleMessageEmailMessages Subject' incoming_valid_message['From'] = self.example_email('cordelia') incoming_valid_message['To'] = mm_address incoming_valid_message['Reply-to'] = self.example_email('cordelia') process_message(incoming_valid_message) # Confirm Iago received the message. user_profile = self.example_user('iago') message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedHuddleMessageEmailMessages Body") self.assertEqual(message.sender, self.example_user('cordelia')) self.assertEqual(message.recipient.type, Recipient.HUDDLE) # Confirm Othello received the message. user_profile = self.example_user('othello') message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedHuddleMessageEmailMessages Body") self.assertEqual(message.sender, self.example_user('cordelia')) self.assertEqual(message.recipient.type, Recipient.HUDDLE)
def test_receive_missed_personal_message_email_messages(self): # build dummy messages for missed messages email reply # have Hamlet send Othello a PM. Othello will reply via email # Hamlet will receive the message. self.login("*****@*****.**") result = self.client.post( "/json/messages", { "type": "private", "content": "test_receive_missed_message_email_messages", "client": "test suite", "to": "*****@*****.**" }) self.assert_json_success(result) user_profile = get_user_profile_by_email("*****@*****.**") usermessage = most_recent_usermessage(user_profile) # we don't want to send actual emails but we do need to create and store the # token for looking up who did reply. mm_address = create_missed_message_address(user_profile, usermessage.message) incoming_valid_message = MIMEText( 'TestMissedMessageEmailMessages Body') incoming_valid_message[ 'Subject'] = 'TestMissedMessageEmailMessages Subject' incoming_valid_message['From'] = "*****@*****.**" incoming_valid_message['To'] = mm_address incoming_valid_message['Reply-to'] = "*****@*****.**" process_message(incoming_valid_message) # self.login("*****@*****.**") # confirm that Hamlet got the message user_profile = get_user_profile_by_email("*****@*****.**") message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedMessageEmailMessages Body") self.assertEqual(message.sender, get_user_profile_by_email("*****@*****.**")) self.assertEqual(message.recipient.id, user_profile.id) self.assertEqual(message.recipient.type, Recipient.PERSONAL)
def test_receive_missed_huddle_message_email_messages(self): # build dummy messages for missed messages email reply # have Othello send Iago and Cordelia a PM. Cordelia will reply via email # Iago and Othello will receive the message. self.login("*****@*****.**") result = self.client.post("/json/send_message", {"type": "private", "content": "test_receive_missed_message_email_messages", "client": "test suite", "to": ujson.dumps(["*****@*****.**", "*****@*****.**"])}) self.assert_json_success(result) user_profile = get_user_profile_by_email("*****@*****.**") usermessage = most_recent_usermessage(user_profile) # we don't want to send actual emails but we do need to create and store the # token for looking up who did reply. mm_address = create_missed_message_address(user_profile, usermessage.message) incoming_valid_message = MIMEText('TestMissedHuddleMessageEmailMessages Body') incoming_valid_message['Subject'] = 'TestMissedHuddleMessageEmailMessages Subject' incoming_valid_message['From'] = "*****@*****.**" incoming_valid_message['To'] = mm_address incoming_valid_message['Reply-to'] = "*****@*****.**" process_message(incoming_valid_message) # Confirm Iago received the message. user_profile = get_user_profile_by_email("*****@*****.**") message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedHuddleMessageEmailMessages Body") self.assertEqual(message.sender, get_user_profile_by_email("*****@*****.**")) self.assertEqual(message.recipient.type, Recipient.HUDDLE) # Confirm Othello received the message. user_profile = get_user_profile_by_email("*****@*****.**") message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedHuddleMessageEmailMessages Body") self.assertEqual(message.sender, get_user_profile_by_email("*****@*****.**")) self.assertEqual(message.recipient.type, Recipient.HUDDLE)
def test_receive_missed_stream_message_email_messages(self) -> None: # build dummy messages for missed messages email reply # have Hamlet send a message to stream Denmark, that Othello # will receive a missed message email about. # Othello will reply via email. # Hamlet will see the message in the stream. self.subscribe(self.example_user("hamlet"), "Denmark") self.subscribe(self.example_user("othello"), "Denmark") email = self.example_email('hamlet') self.login(email) result = self.client_post("/json/messages", {"type": "stream", "topic": "test topic", "content": "test_receive_missed_stream_message_email_messages", "client": "test suite", "to": "Denmark"}) self.assert_json_success(result) user_profile = self.example_user('othello') usermessage = most_recent_usermessage(user_profile) mm_address = create_missed_message_address(user_profile, usermessage.message) incoming_valid_message = MIMEText('TestMissedMessageEmailMessages Body') incoming_valid_message['Subject'] = 'TestMissedMessageEmailMessages Subject' incoming_valid_message['From'] = self.example_email('othello') incoming_valid_message['To'] = mm_address incoming_valid_message['Reply-to'] = self.example_email('othello') process_message(incoming_valid_message) # confirm that Hamlet got the message user_profile = self.example_user('hamlet') message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedMessageEmailMessages Body") self.assertEqual(message.sender, self.example_user('othello')) self.assertEqual(message.recipient.type, Recipient.STREAM) self.assertEqual(message.recipient.id, usermessage.message.recipient.id)
def test_receive_missed_personal_message_email_messages(self): # type: () -> None # build dummy messages for missed messages email reply # have Hamlet send Othello a PM. Othello will reply via email # Hamlet will receive the message. email = self.example_email('hamlet') self.login(email) result = self.client_post("/json/messages", {"type": "private", "content": "test_receive_missed_message_email_messages", "client": "test suite", "to": self.example_email('othello')}) self.assert_json_success(result) user_profile = self.example_user('othello') usermessage = most_recent_usermessage(user_profile) # we don't want to send actual emails but we do need to create and store the # token for looking up who did reply. mm_address = create_missed_message_address(user_profile, usermessage.message) incoming_valid_message = MIMEText('TestMissedMessageEmailMessages Body') # type: Any # https://github.com/python/typeshed/issues/275 incoming_valid_message['Subject'] = 'TestMissedMessageEmailMessages Subject' incoming_valid_message['From'] = self.example_email('othello') incoming_valid_message['To'] = mm_address incoming_valid_message['Reply-to'] = self.example_email('othello') process_message(incoming_valid_message) # self.login(self.example_email("hamlet")) # confirm that Hamlet got the message user_profile = self.example_user('hamlet') message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedMessageEmailMessages Body") self.assertEqual(message.sender, self.example_user('othello')) self.assertEqual(message.recipient.id, user_profile.id) self.assertEqual(message.recipient.type, Recipient.PERSONAL)
def test_receive_missed_personal_message_email_messages(self): # build dummy messages for missed messages email reply # have Hamlet send Othello a PM. Othello will reply via email # Hamlet will receive the message. self.login("*****@*****.**") result = self.client.post("/json/send_message", {"type": "private", "content": "test_receive_missed_message_email_messages", "client": "test suite", "to": "*****@*****.**"}) self.assert_json_success(result) user_profile = get_user_profile_by_email("*****@*****.**") usermessage = most_recent_usermessage(user_profile) # we don't want to send actual emails but we do need to create and store the # token for looking up who did reply. mm_address = create_missed_message_address(user_profile, usermessage.message) incoming_valid_message = MIMEText('TestMissedMessageEmailMessages Body') incoming_valid_message['Subject'] = 'TestMissedMessageEmailMessages Subject' incoming_valid_message['From'] = "*****@*****.**" incoming_valid_message['To'] = mm_address incoming_valid_message['Reply-to'] = "*****@*****.**" process_message(incoming_valid_message) # self.login("*****@*****.**") # confirm that Hamlet got the message user_profile = get_user_profile_by_email("*****@*****.**") message = most_recent_message(user_profile) self.assertEqual(message.content, "TestMissedMessageEmailMessages Body") self.assertEqual(message.sender, get_user_profile_by_email("*****@*****.**")) self.assertEqual(message.recipient.id, user_profile.id) self.assertEqual(message.recipient.type, Recipient.PERSONAL)
def do_send_missedmessage_events_reply_in_zulip(user_profile: UserProfile, missed_messages: List[Dict[str, Any]], message_count: int) -> None: """ Send a reminder email to a user if she's missed some PMs by being offline. The email will have its reply to address set to a limited used email address that will send a zulip message to the correct recipient. This allows the user to respond to missed PMs, huddles, and @-mentions directly from the email. `user_profile` is the user to send the reminder to `missed_messages` is a list of dictionaries to Message objects and other data for a group of messages that share a recipient (and topic) """ from zerver.context_processors import common_context # Disabled missedmessage emails internally if not user_profile.enable_offline_email_notifications: return recipients = set((msg['message'].recipient_id, msg['message'].topic_name()) for msg in missed_messages) if len(recipients) != 1: raise ValueError( 'All missed_messages must have the same recipient and topic %r' % recipients ) unsubscribe_link = one_click_unsubscribe_link(user_profile, "missed_messages") context = common_context(user_profile) context.update({ 'name': user_profile.full_name, 'message_count': message_count, 'unsubscribe_link': unsubscribe_link, 'realm_name_in_notifications': user_profile.realm_name_in_notifications, 'show_message_content': user_profile.message_content_in_email_notifications, }) triggers = list(message['trigger'] for message in missed_messages) unique_triggers = set(triggers) context.update({ 'mention': 'mentioned' in unique_triggers, 'mention_count': triggers.count('mentioned'), }) # If this setting (email mirroring integration) is enabled, only then # can users reply to email to send message to Zulip. Thus, one must # ensure to display warning in the template. if settings.EMAIL_GATEWAY_PATTERN: context.update({ 'reply_warning': False, 'reply_to_zulip': True, }) else: context.update({ 'reply_warning': True, 'reply_to_zulip': False, }) from zerver.lib.email_mirror import create_missed_message_address reply_to_address = create_missed_message_address(user_profile, missed_messages[0]['message']) if reply_to_address == FromAddress.NOREPLY: reply_to_name = None else: reply_to_name = "Zulip" senders = list(set(m['message'].sender for m in missed_messages)) if (missed_messages[0]['message'].recipient.type == Recipient.HUDDLE): display_recipient = get_display_recipient(missed_messages[0]['message'].recipient) # Make sure that this is a list of strings, not a string. assert not isinstance(display_recipient, str) other_recipients = [r['full_name'] for r in display_recipient if r['id'] != user_profile.id] context.update({'group_pm': True}) if len(other_recipients) == 2: huddle_display_name = "%s" % (" and ".join(other_recipients)) context.update({'huddle_display_name': huddle_display_name}) elif len(other_recipients) == 3: huddle_display_name = "%s, %s, and %s" % ( other_recipients[0], other_recipients[1], other_recipients[2]) context.update({'huddle_display_name': huddle_display_name}) else: huddle_display_name = "%s, and %s others" % ( ', '.join(other_recipients[:2]), len(other_recipients) - 2) context.update({'huddle_display_name': huddle_display_name}) elif (missed_messages[0]['message'].recipient.type == Recipient.PERSONAL): context.update({'private_message': True}) elif context['mention']: # Keep only the senders who actually mentioned the user senders = list(set(m['message'].sender for m in missed_messages if m['trigger'] == 'mentioned')) # TODO: When we add wildcard mentions that send emails, we # should make sure the right logic applies here. elif ('stream_email_notify' in unique_triggers): context.update({'stream_email_notify': True}) else: raise AssertionError("Invalid messages!") # If message content is disabled, then flush all information we pass to email. if not user_profile.message_content_in_email_notifications: context.update({ 'reply_to_zulip': False, 'messages': [], 'sender_str': "", 'realm_str': user_profile.realm.name, 'huddle_display_name': "", }) else: context.update({ 'messages': build_message_list(user_profile, list(m['message'] for m in missed_messages)), 'sender_str': ", ".join(sender.full_name for sender in senders), 'realm_str': user_profile.realm.name, }) from_name = "Zulip missed messages" # type: str from_address = FromAddress.NOREPLY if len(senders) == 1 and settings.SEND_MISSED_MESSAGE_EMAILS_AS_USER: # If this setting is enabled, you can reply to the Zulip # missed message emails directly back to the original sender. # However, one must ensure the Zulip server is in the SPF # record for the domain, or there will be spam/deliverability # problems. sender = senders[0] from_name, from_address = (sender.full_name, sender.email) context.update({ 'reply_warning': False, 'reply_to_zulip': False, }) email_dict = { 'template_prefix': 'zerver/emails/missed_message', 'to_user_ids': [user_profile.id], 'from_name': from_name, 'from_address': from_address, 'reply_to_email': formataddr((reply_to_name, reply_to_address)), 'context': context} queue_json_publish("email_senders", email_dict) user_profile.last_reminder = timezone_now() user_profile.save(update_fields=['last_reminder'])
def do_send_missedmessage_events_reply_in_zulip(user_profile, missed_messages, message_count): # type: (UserProfile, List[Message], int) -> None """ Send a reminder email to a user if she's missed some PMs by being offline. The email will have its reply to address set to a limited used email address that will send a zulip message to the correct recipient. This allows the user to respond to missed PMs, huddles, and @-mentions directly from the email. `user_profile` is the user to send the reminder to `missed_messages` is a list of Message objects to remind about they should all have the same recipient and subject """ from zerver.context_processors import common_context # Disabled missedmessage emails internally if not user_profile.enable_offline_email_notifications: return recipients = set( (msg.recipient_id, msg.subject) for msg in missed_messages) if len(recipients) != 1: raise ValueError( 'All missed_messages must have the same recipient and subject %r' % recipients) unsubscribe_link = one_click_unsubscribe_link(user_profile, "missed_messages") context = common_context(user_profile) context.update({ 'name': user_profile.full_name, 'messages': build_message_list(user_profile, missed_messages), 'message_count': message_count, 'mention': missed_messages[0].is_stream_message(), 'unsubscribe_link': unsubscribe_link, }) # If this setting (email mirroring integration) is enabled, only then # can users reply to email to send message to Zulip. Thus, one must # ensure to display warning in the template. if settings.EMAIL_GATEWAY_PATTERN: context.update({ 'reply_warning': False, 'reply_to_zulip': True, }) else: context.update({ 'reply_warning': True, 'reply_to_zulip': False, }) from zerver.lib.email_mirror import create_missed_message_address reply_to_address = create_missed_message_address(user_profile, missed_messages[0]) if reply_to_address == FromAddress.NOREPLY: reply_to_name = None else: reply_to_name = "Zulip" senders = list(set(m.sender for m in missed_messages)) if (missed_messages[0].recipient.type == Recipient.HUDDLE): display_recipient = get_display_recipient(missed_messages[0].recipient) # Make sure that this is a list of strings, not a string. assert not isinstance(display_recipient, Text) other_recipients = [ r['full_name'] for r in display_recipient if r['id'] != user_profile.id ] context.update({'group_pm': True}) if len(other_recipients) == 2: huddle_display_name = u"%s" % (" and ".join(other_recipients)) context.update({'huddle_display_name': huddle_display_name}) elif len(other_recipients) == 3: huddle_display_name = u"%s, %s, and %s" % ( other_recipients[0], other_recipients[1], other_recipients[2]) context.update({'huddle_display_name': huddle_display_name}) else: huddle_display_name = u"%s, and %s others" % (', '.join( other_recipients[:2]), len(other_recipients) - 2) context.update({'huddle_display_name': huddle_display_name}) elif (missed_messages[0].recipient.type == Recipient.PERSONAL): context.update({'private_message': True}) else: # Keep only the senders who actually mentioned the user # # TODO: When we add wildcard mentions that send emails, add # them to the filter here. senders = list( set(m.sender for m in missed_messages if UserMessage.objects.filter( message=m, user_profile=user_profile, flags=UserMessage.flags.mentioned).exists())) context.update({'at_mention': True}) context.update({ 'sender_str': ", ".join(sender.full_name for sender in senders), 'realm_str': user_profile.realm.name, }) from_name = "Zulip missed messages" # type: Text from_address = FromAddress.NOREPLY if len(senders) == 1 and settings.SEND_MISSED_MESSAGE_EMAILS_AS_USER: # If this setting is enabled, you can reply to the Zulip # missed message emails directly back to the original sender. # However, one must ensure the Zulip server is in the SPF # record for the domain, or there will be spam/deliverability # problems. sender = senders[0] from_name, from_address = (sender.full_name, sender.email) context.update({ 'reply_warning': False, 'reply_to_zulip': False, }) email_dict = { 'template_prefix': 'zerver/emails/missed_message', 'to_user_id': user_profile.id, 'from_name': from_name, 'from_address': from_address, 'reply_to_email': formataddr((reply_to_name, reply_to_address)), 'context': context } queue_json_publish("missedmessage_email_senders", email_dict, send_email_from_dict, call_consume_in_tests=True) user_profile.last_reminder = timezone_now() user_profile.save(update_fields=['last_reminder'])
def do_send_missedmessage_events_reply_in_zulip(user_profile: UserProfile, missed_messages: List[Dict[ str, Any]], message_count: int) -> None: """ Send a reminder email to a user if she's missed some PMs by being offline. The email will have its reply to address set to a limited used email address that will send a zulip message to the correct recipient. This allows the user to respond to missed PMs, huddles, and @-mentions directly from the email. `user_profile` is the user to send the reminder to `missed_messages` is a list of dictionaries to Message objects and other data for a group of messages that share a recipient (and topic) """ from zerver.context_processors import common_context # Disabled missedmessage emails internally if not user_profile.enable_offline_email_notifications: return recipients = {(msg['message'].recipient_id, msg['message'].topic_name()) for msg in missed_messages} if len(recipients) != 1: raise ValueError( f'All missed_messages must have the same recipient and topic {recipients!r}', ) # This link is no longer a part of the email, but keeping the code in case # we find a clean way to add it back in the future unsubscribe_link = one_click_unsubscribe_link(user_profile, "missed_messages") context = common_context(user_profile) context.update({ 'name': user_profile.full_name, 'message_count': message_count, 'unsubscribe_link': unsubscribe_link, 'realm_name_in_notifications': user_profile.realm_name_in_notifications, }) triggers = list(message['trigger'] for message in missed_messages) unique_triggers = set(triggers) context.update({ 'mention': 'mentioned' in unique_triggers or 'wildcard_mentioned' in unique_triggers, 'stream_email_notify': 'stream_email_notify' in unique_triggers, 'mention_count': triggers.count('mentioned') + triggers.count("wildcard_mentioned"), }) # If this setting (email mirroring integration) is enabled, only then # can users reply to email to send message to Zulip. Thus, one must # ensure to display warning in the template. if settings.EMAIL_GATEWAY_PATTERN: context.update({ 'reply_to_zulip': True, }) else: context.update({ 'reply_to_zulip': False, }) from zerver.lib.email_mirror import create_missed_message_address reply_to_address = create_missed_message_address( user_profile, missed_messages[0]['message']) if reply_to_address == FromAddress.NOREPLY: reply_to_name = "" else: reply_to_name = "Zulip" narrow_url = get_narrow_url(user_profile, missed_messages[0]['message']) context.update({ 'narrow_url': narrow_url, }) senders = list({m['message'].sender for m in missed_messages}) if (missed_messages[0]['message'].recipient.type == Recipient.HUDDLE): display_recipient = get_display_recipient( missed_messages[0]['message'].recipient) # Make sure that this is a list of strings, not a string. assert not isinstance(display_recipient, str) other_recipients = [ r['full_name'] for r in display_recipient if r['id'] != user_profile.id ] context.update({'group_pm': True}) if len(other_recipients) == 2: huddle_display_name = " and ".join(other_recipients) context.update({'huddle_display_name': huddle_display_name}) elif len(other_recipients) == 3: huddle_display_name = f"{other_recipients[0]}, {other_recipients[1]}, and {other_recipients[2]}" context.update({'huddle_display_name': huddle_display_name}) else: huddle_display_name = "{}, and {} others".format( ', '.join(other_recipients[:2]), len(other_recipients) - 2) context.update({'huddle_display_name': huddle_display_name}) elif (missed_messages[0]['message'].recipient.type == Recipient.PERSONAL): context.update({'private_message': True}) elif (context['mention'] or context['stream_email_notify']): # Keep only the senders who actually mentioned the user if context['mention']: senders = list({ m['message'].sender for m in missed_messages if m['trigger'] == 'mentioned' or m['trigger'] == 'wildcard_mentioned' }) message = missed_messages[0]['message'] stream = Stream.objects.only('id', 'name').get(id=message.recipient.type_id) stream_header = f"{stream.name} > {message.topic_name()}" context.update({ 'stream_header': stream_header, }) else: raise AssertionError("Invalid messages!") # If message content is disabled, then flush all information we pass to email. if not message_content_allowed_in_missedmessage_emails(user_profile): realm = user_profile.realm context.update({ 'reply_to_zulip': False, 'messages': [], 'sender_str': "", 'realm_str': realm.name, 'huddle_display_name': "", 'show_message_content': False, 'message_content_disabled_by_user': not user_profile.message_content_in_email_notifications, 'message_content_disabled_by_realm': not realm.message_content_allowed_in_email_notifications, }) else: context.update({ 'messages': build_message_list(user_profile, list(m['message'] for m in missed_messages)), 'sender_str': ", ".join(sender.full_name for sender in senders), 'realm_str': user_profile.realm.name, 'show_message_content': True, }) with override_language(user_profile.default_language): from_name: str = _("Zulip missed messages") from_address = FromAddress.NOREPLY if len(senders) == 1 and settings.SEND_MISSED_MESSAGE_EMAILS_AS_USER: # If this setting is enabled, you can reply to the Zulip # missed message emails directly back to the original sender. # However, one must ensure the Zulip server is in the SPF # record for the domain, or there will be spam/deliverability # problems. # # Also, this setting is not really compatible with # EMAIL_ADDRESS_VISIBILITY_ADMINS. sender = senders[0] from_name, from_address = (sender.full_name, sender.email) context.update({ 'reply_to_zulip': False, }) email_dict = { 'template_prefix': 'zerver/emails/missed_message', 'to_user_ids': [user_profile.id], 'from_name': from_name, 'from_address': from_address, 'reply_to_email': str(Address(display_name=reply_to_name, addr_spec=reply_to_address)), 'context': context } queue_json_publish("email_senders", email_dict) user_profile.last_reminder = timezone_now() user_profile.save(update_fields=['last_reminder'])
def test_redact_email_address(self) -> None: user_profile = self.example_user('hamlet') self.login(user_profile.email) self.subscribe(user_profile, "errors") stream = get_stream("Denmark", user_profile.realm) # Test for a stream address: stream_to_address = encode_email_address(stream) stream_address_parts = stream_to_address.split('@') scrubbed_stream_address = 'X'*len(stream_address_parts[0]) + '@' + stream_address_parts[1] error_message = "test message {}" error_message = error_message.format(stream_to_address) expected_message = "test message {} <Address to stream id: {}>" expected_message = expected_message.format(scrubbed_stream_address, stream.id) redacted_message = redact_email_address(error_message) self.assertEqual(redacted_message, expected_message) # Test for an invalid email address: invalid_address = "invalid@testserver" error_message = "test message {}" error_message = error_message.format(invalid_address) expected_message = "test message {} <Invalid address>" expected_message = expected_message.format('XXXXXXX@testserver') redacted_message = redact_email_address(error_message) self.assertEqual(redacted_message, expected_message) # Test for a missed message address: result = self.client_post( "/json/messages", { "type": "private", "content": "test_redact_email_message", "client": "test suite", "to": ujson.dumps([self.example_email('cordelia'), self.example_email('iago')]) }) self.assert_json_success(result) cordelia_profile = self.example_user('cordelia') user_message = most_recent_usermessage(cordelia_profile) mm_address = create_missed_message_address(user_profile, user_message.message) error_message = "test message {}" error_message = error_message.format(mm_address) expected_message = "test message {} <Missed message address>" expected_message = expected_message.format('X'*34 + '@testserver') redacted_message = redact_email_address(error_message) self.assertEqual(redacted_message, expected_message) # Test if redacting correctly scrubs multiple occurrences of the address: error_message = "test message first occurrence: {} second occurrence: {}" error_message = error_message.format(stream_to_address, stream_to_address) expected_message = "test message first occurrence: {} <Address to stream id: {}>" expected_message += " second occurrence: {} <Address to stream id: {}>" expected_message = expected_message.format(scrubbed_stream_address, stream.id, scrubbed_stream_address, stream.id) redacted_message = redact_email_address(error_message) self.assertEqual(redacted_message, expected_message) # Test with EMAIL_GATEWAY_EXTRA_PATTERN_HACK: with self.settings(EMAIL_GATEWAY_EXTRA_PATTERN_HACK='@zulip.org'): stream_to_address = stream_to_address.replace('@testserver', '@zulip.org') scrubbed_stream_address = scrubbed_stream_address.replace('@testserver', '@zulip.org') error_message = "test message {}" error_message = error_message.format(stream_to_address) expected_message = "test message {} <Address to stream id: {}>" expected_message = expected_message.format(scrubbed_stream_address, stream.id) redacted_message = redact_email_address(error_message) self.assertEqual(redacted_message, expected_message)
def do_send_missedmessage_events_reply_in_zulip(user_profile: UserProfile, missed_messages: List[Message], message_count: int) -> None: """ Send a reminder email to a user if she's missed some PMs by being offline. The email will have its reply to address set to a limited used email address that will send a zulip message to the correct recipient. This allows the user to respond to missed PMs, huddles, and @-mentions directly from the email. `user_profile` is the user to send the reminder to `missed_messages` is a list of Message objects to remind about they should all have the same recipient and subject """ from zerver.context_processors import common_context # Disabled missedmessage emails internally if not user_profile.enable_offline_email_notifications: return recipients = set((msg.recipient_id, msg.subject) for msg in missed_messages) if len(recipients) != 1: raise ValueError( 'All missed_messages must have the same recipient and subject %r' % recipients ) unsubscribe_link = one_click_unsubscribe_link(user_profile, "missed_messages") context = common_context(user_profile) context.update({ 'name': user_profile.full_name, 'message_count': message_count, 'mention': missed_messages[0].is_stream_message(), 'unsubscribe_link': unsubscribe_link, 'realm_name_in_notifications': user_profile.realm_name_in_notifications, 'show_message_content': user_profile.message_content_in_email_notifications, }) # If this setting (email mirroring integration) is enabled, only then # can users reply to email to send message to Zulip. Thus, one must # ensure to display warning in the template. if settings.EMAIL_GATEWAY_PATTERN: context.update({ 'reply_warning': False, 'reply_to_zulip': True, }) else: context.update({ 'reply_warning': True, 'reply_to_zulip': False, }) from zerver.lib.email_mirror import create_missed_message_address reply_to_address = create_missed_message_address(user_profile, missed_messages[0]) if reply_to_address == FromAddress.NOREPLY: reply_to_name = None else: reply_to_name = "Zulip" senders = list(set(m.sender for m in missed_messages)) if (missed_messages[0].recipient.type == Recipient.HUDDLE): display_recipient = get_display_recipient(missed_messages[0].recipient) # Make sure that this is a list of strings, not a string. assert not isinstance(display_recipient, str) other_recipients = [r['full_name'] for r in display_recipient if r['id'] != user_profile.id] context.update({'group_pm': True}) if len(other_recipients) == 2: huddle_display_name = "%s" % (" and ".join(other_recipients)) context.update({'huddle_display_name': huddle_display_name}) elif len(other_recipients) == 3: huddle_display_name = "%s, %s, and %s" % ( other_recipients[0], other_recipients[1], other_recipients[2]) context.update({'huddle_display_name': huddle_display_name}) else: huddle_display_name = "%s, and %s others" % ( ', '.join(other_recipients[:2]), len(other_recipients) - 2) context.update({'huddle_display_name': huddle_display_name}) elif (missed_messages[0].recipient.type == Recipient.PERSONAL): context.update({'private_message': True}) else: # Keep only the senders who actually mentioned the user # # TODO: When we add wildcard mentions that send emails, add # them to the filter here. senders = list(set(m.sender for m in missed_messages if UserMessage.objects.filter(message=m, user_profile=user_profile, flags=UserMessage.flags.mentioned).exists())) context.update({'at_mention': True}) # If message content is disabled, then flush all information we pass to email. if not user_profile.message_content_in_email_notifications: context.update({ 'reply_to_zulip': False, 'messages': [], 'sender_str': "", 'realm_str': user_profile.realm.name, 'huddle_display_name': "", }) else: context.update({ 'messages': build_message_list(user_profile, missed_messages), 'sender_str': ", ".join(sender.full_name for sender in senders), 'realm_str': user_profile.realm.name, }) from_name = "Zulip missed messages" # type: str from_address = FromAddress.NOREPLY if len(senders) == 1 and settings.SEND_MISSED_MESSAGE_EMAILS_AS_USER: # If this setting is enabled, you can reply to the Zulip # missed message emails directly back to the original sender. # However, one must ensure the Zulip server is in the SPF # record for the domain, or there will be spam/deliverability # problems. sender = senders[0] from_name, from_address = (sender.full_name, sender.email) context.update({ 'reply_warning': False, 'reply_to_zulip': False, }) email_dict = { 'template_prefix': 'zerver/emails/missed_message', 'to_user_id': user_profile.id, 'from_name': from_name, 'from_address': from_address, 'reply_to_email': formataddr((reply_to_name, reply_to_address)), 'context': context} queue_json_publish("email_senders", email_dict) user_profile.last_reminder = timezone_now() user_profile.save(update_fields=['last_reminder'])
def do_send_missedmessage_events_reply_in_zulip(user_profile, missed_messages, message_count): # type: (UserProfile, List[Message], int) -> None """ Send a reminder email to a user if she's missed some PMs by being offline. The email will have its reply to address set to a limited used email address that will send a zulip message to the correct recipient. This allows the user to respond to missed PMs, huddles, and @-mentions directly from the email. `user_profile` is the user to send the reminder to `missed_messages` is a list of Message objects to remind about they should all have the same recipient and subject """ from zerver.context_processors import common_context # Disabled missedmessage emails internally if not user_profile.enable_offline_email_notifications: return recipients = set( (msg.recipient_id, msg.subject) for msg in missed_messages) if len(recipients) != 1: raise ValueError( 'All missed_messages must have the same recipient and subject %r' % recipients) unsubscribe_link = one_click_unsubscribe_link(user_profile, "missed_messages") template_payload = common_context(user_profile) template_payload.update({ 'name': user_profile.full_name, 'messages': build_message_list(user_profile, missed_messages), 'message_count': message_count, 'mention': missed_messages[0].recipient.type == Recipient.STREAM, 'unsubscribe_link': unsubscribe_link, }) # If this setting (email mirroring integration) is enabled, only then # can users reply to email to send message to Zulip. Thus, one must # ensure to display warning in the template. if settings.EMAIL_GATEWAY_PATTERN: template_payload.update({ 'reply_warning': False, 'reply_to_zulip': True, }) else: template_payload.update({ 'reply_warning': True, 'reply_to_zulip': False, }) headers = {} from zerver.lib.email_mirror import create_missed_message_address address = create_missed_message_address(user_profile, missed_messages[0]) headers['Reply-To'] = address senders = set(m.sender.full_name for m in missed_messages) sender_str = ", ".join(senders) plural_messages = 's' if len(missed_messages) > 1 else '' subject = "Missed Zulip%s from %s" % (plural_messages, sender_str) from_email = 'Zulip <%s>' % (settings.NOREPLY_EMAIL_ADDRESS, ) if len(senders) == 1 and settings.SEND_MISSED_MESSAGE_EMAILS_AS_USER: # If this setting is enabled, you can reply to the Zulip # missed message emails directly back to the original sender. # However, one must ensure the Zulip server is in the SPF # record for the domain, or there will be spam/deliverability # problems. headers['Sender'] = from_email sender = missed_messages[0].sender from_email = '"%s" <%s>' % (sender_str, sender.email) template_payload.update({ 'reply_warning': False, 'reply_to_zulip': False, }) text_content = loader.render_to_string('zerver/missed_message_email.txt', template_payload) html_content = loader.render_to_string('zerver/missed_message_email.html', template_payload) email_content = { 'subject': subject, 'text_content': text_content, 'html_content': html_content, 'from_email': from_email, 'to': [user_profile.email], 'headers': headers } queue_json_publish("missedmessage_email_senders", email_content, send_missedmessage_email) user_profile.last_reminder = timezone.now() user_profile.save(update_fields=['last_reminder'])
def do_send_missedmessage_events_reply_in_zulip(user_profile, missed_messages, message_count): # type: (UserProfile, List[Message], int) -> None """ Send a reminder email to a user if she's missed some PMs by being offline. The email will have its reply to address set to a limited used email address that will send a zulip message to the correct recipient. This allows the user to respond to missed PMs, huddles, and @-mentions directly from the email. `user_profile` is the user to send the reminder to `missed_messages` is a list of Message objects to remind about they should all have the same recipient and subject """ # Disabled missedmessage emails internally if not user_profile.enable_offline_email_notifications: return recipients = set((msg.recipient_id, msg.subject) for msg in missed_messages) if len(recipients) != 1: raise ValueError( 'All missed_messages must have the same recipient and subject %r' % recipients ) unsubscribe_link = one_click_unsubscribe_link(user_profile, "missed_messages") template_payload = { 'name': user_profile.full_name, 'messages': build_message_list(user_profile, missed_messages), 'message_count': message_count, 'reply_warning': False, 'external_host': settings.EXTERNAL_HOST, 'external_uri_scheme': settings.EXTERNAL_URI_SCHEME, 'server_uri': settings.SERVER_URI, 'realm_uri': user_profile.realm.uri, 'mention': missed_messages[0].recipient.type == Recipient.STREAM, 'reply_to_zulip': True, 'unsubscribe_link': unsubscribe_link, } headers = {} from zerver.lib.email_mirror import create_missed_message_address address = create_missed_message_address(user_profile, missed_messages[0]) headers['Reply-To'] = address senders = set(m.sender.full_name for m in missed_messages) sender_str = ", ".join(senders) plural_messages = 's' if len(missed_messages) > 1 else '' subject = "Missed Zulip%s from %s" % (plural_messages, sender_str) from_email = 'Zulip <%s>' % (settings.NOREPLY_EMAIL_ADDRESS,) if len(senders) == 1 and settings.SEND_MISSED_MESSAGE_EMAILS_AS_USER: # If this setting is enabled, you can reply to the Zulip # missed message emails directly back to the original sender. # However, one must ensure the Zulip server is in the SPF # record for the domain, or there will be spam/deliverability # problems. headers['Sender'] = from_email sender = missed_messages[0].sender from_email = '"%s" <%s>' % (sender_str, sender.email) text_content = loader.render_to_string('zerver/missed_message_email.txt', template_payload) html_content = loader.render_to_string('zerver/missed_message_email_html.txt', template_payload) msg = EmailMultiAlternatives(subject, text_content, from_email, [user_profile.email], headers = headers) msg.attach_alternative(html_content, "text/html") msg.send() user_profile.last_reminder = datetime.datetime.now() user_profile.save(update_fields=['last_reminder'])
def do_send_missedmessage_events_reply_in_zulip(user_profile: UserProfile, missed_messages: List[Dict[ str, Any]], message_count: int) -> None: """ Send a reminder email to a user if she's missed some PMs by being offline. The email will have its reply to address set to a limited used email address that will send a Zulip message to the correct recipient. This allows the user to respond to missed PMs, huddles, and @-mentions directly from the email. `user_profile` is the user to send the reminder to `missed_messages` is a list of dictionaries to Message objects and other data for a group of messages that share a recipient (and topic) """ from zerver.context_processors import common_context # Disabled missedmessage emails internally if not user_profile.enable_offline_email_notifications: return recipients = {(msg["message"].recipient_id, msg["message"].topic_name()) for msg in missed_messages} if len(recipients) != 1: raise ValueError( f"All missed_messages must have the same recipient and topic {recipients!r}", ) # This link is no longer a part of the email, but keeping the code in case # we find a clean way to add it back in the future unsubscribe_link = one_click_unsubscribe_link(user_profile, "missed_messages") context = common_context(user_profile) context.update( name=user_profile.full_name, message_count=message_count, unsubscribe_link=unsubscribe_link, realm_name_in_notifications=user_profile.realm_name_in_notifications, ) triggers = [message["trigger"] for message in missed_messages] unique_triggers = set(triggers) context.update( mention="mentioned" in unique_triggers or "wildcard_mentioned" in unique_triggers, stream_email_notify="stream_email_notify" in unique_triggers, mention_count=triggers.count("mentioned") + triggers.count("wildcard_mentioned"), ) # If this setting (email mirroring integration) is enabled, only then # can users reply to email to send message to Zulip. Thus, one must # ensure to display warning in the template. if settings.EMAIL_GATEWAY_PATTERN: context.update(reply_to_zulip=True, ) else: context.update(reply_to_zulip=False, ) from zerver.lib.email_mirror import create_missed_message_address reply_to_address = create_missed_message_address( user_profile, missed_messages[0]["message"]) if reply_to_address == FromAddress.NOREPLY: reply_to_name = "" else: reply_to_name = "Zulip" narrow_url = get_narrow_url(user_profile, missed_messages[0]["message"]) context.update(narrow_url=narrow_url, ) senders = list({m["message"].sender for m in missed_messages}) if missed_messages[0]["message"].recipient.type == Recipient.HUDDLE: display_recipient = get_display_recipient( missed_messages[0]["message"].recipient) # Make sure that this is a list of strings, not a string. assert not isinstance(display_recipient, str) other_recipients = [ r["full_name"] for r in display_recipient if r["id"] != user_profile.id ] context.update(group_pm=True) if len(other_recipients) == 2: huddle_display_name = " and ".join(other_recipients) context.update(huddle_display_name=huddle_display_name) elif len(other_recipients) == 3: huddle_display_name = ( f"{other_recipients[0]}, {other_recipients[1]}, and {other_recipients[2]}" ) context.update(huddle_display_name=huddle_display_name) else: huddle_display_name = "{}, and {} others".format( ", ".join(other_recipients[:2]), len(other_recipients) - 2) context.update(huddle_display_name=huddle_display_name) elif missed_messages[0]["message"].recipient.type == Recipient.PERSONAL: context.update(private_message=True) elif context["mention"] or context["stream_email_notify"]: # Keep only the senders who actually mentioned the user if context["mention"]: senders = list({ m["message"].sender for m in missed_messages if m["trigger"] == "mentioned" or m["trigger"] == "wildcard_mentioned" }) message = missed_messages[0]["message"] stream = Stream.objects.only("id", "name").get(id=message.recipient.type_id) stream_header = f"{stream.name} > {message.topic_name()}" context.update(stream_header=stream_header, ) else: raise AssertionError("Invalid messages!") # If message content is disabled, then flush all information we pass to email. if not message_content_allowed_in_missedmessage_emails(user_profile): realm = user_profile.realm context.update( reply_to_zulip=False, messages=[], sender_str="", realm_str=realm.name, huddle_display_name="", show_message_content=False, message_content_disabled_by_user=not user_profile. message_content_in_email_notifications, message_content_disabled_by_realm=not realm. message_content_allowed_in_email_notifications, ) else: context.update( messages=build_message_list( user=user_profile, messages=[m["message"] for m in missed_messages], stream_map={}, ), sender_str=", ".join(sender.full_name for sender in senders), realm_str=user_profile.realm.name, show_message_content=True, ) with override_language(user_profile.default_language): from_name: str = _("Zulip missed messages") from_address = FromAddress.NOREPLY if len(senders) == 1 and settings.SEND_MISSED_MESSAGE_EMAILS_AS_USER: # If this setting is enabled, you can reply to the Zulip # missed message emails directly back to the original sender. # However, one must ensure the Zulip server is in the SPF # record for the domain, or there will be spam/deliverability # problems. # # Also, this setting is not really compatible with # EMAIL_ADDRESS_VISIBILITY_ADMINS. sender = senders[0] from_name, from_address = (sender.full_name, sender.email) context.update(reply_to_zulip=False, ) email_dict = { "template_prefix": "zerver/emails/missed_message", "to_user_ids": [user_profile.id], "from_name": from_name, "from_address": from_address, "reply_to_email": str(Address(display_name=reply_to_name, addr_spec=reply_to_address)), "context": context, } queue_json_publish("email_senders", email_dict) user_profile.last_reminder = timezone_now() user_profile.save(update_fields=["last_reminder"])
def do_send_missedmessage_events_reply_in_zulip(user_profile, missed_messages, message_count): # type: (UserProfile, List[Message], int) -> None """ Send a reminder email to a user if she's missed some PMs by being offline. The email will have its reply to address set to a limited used email address that will send a zulip message to the correct recipient. This allows the user to respond to missed PMs, huddles, and @-mentions directly from the email. `user_profile` is the user to send the reminder to `missed_messages` is a list of Message objects to remind about they should all have the same recipient and subject """ from zerver.context_processors import common_context # Disabled missedmessage emails internally if not user_profile.enable_offline_email_notifications: return recipients = set((msg.recipient_id, msg.subject) for msg in missed_messages) if len(recipients) != 1: raise ValueError( 'All missed_messages must have the same recipient and subject %r' % recipients ) unsubscribe_link = one_click_unsubscribe_link(user_profile, "missed_messages") template_payload = common_context(user_profile) template_payload.update({ 'name': user_profile.full_name, 'messages': build_message_list(user_profile, missed_messages), 'message_count': message_count, 'mention': missed_messages[0].recipient.type == Recipient.STREAM, 'unsubscribe_link': unsubscribe_link, }) # If this setting (email mirroring integration) is enabled, only then # can users reply to email to send message to Zulip. Thus, one must # ensure to display warning in the template. if settings.EMAIL_GATEWAY_PATTERN: template_payload.update({ 'reply_warning': False, 'reply_to_zulip': True, }) else: template_payload.update({ 'reply_warning': True, 'reply_to_zulip': False, }) headers = {} from zerver.lib.email_mirror import create_missed_message_address address = create_missed_message_address(user_profile, missed_messages[0]) headers['Reply-To'] = address senders = set(m.sender.full_name for m in missed_messages) sender_str = ", ".join(senders) plural_messages = 's' if len(missed_messages) > 1 else '' subject = "Missed Zulip%s from %s" % (plural_messages, sender_str) from_email = 'Zulip <%s>' % (settings.NOREPLY_EMAIL_ADDRESS,) if len(senders) == 1 and settings.SEND_MISSED_MESSAGE_EMAILS_AS_USER: # If this setting is enabled, you can reply to the Zulip # missed message emails directly back to the original sender. # However, one must ensure the Zulip server is in the SPF # record for the domain, or there will be spam/deliverability # problems. headers['Sender'] = from_email sender = missed_messages[0].sender from_email = '"%s" <%s>' % (sender_str, sender.email) template_payload.update({ 'reply_warning': False, 'reply_to_zulip': False, }) text_content = loader.render_to_string('zerver/missed_message_email.txt', template_payload) html_content = loader.render_to_string('zerver/missed_message_email.html', template_payload) email_content = { 'subject': subject, 'text_content': text_content, 'html_content': html_content, 'from_email': from_email, 'to': [user_profile.email], 'headers': headers } queue_json_publish("missedmessage_email_senders", email_content, send_missedmessage_email) user_profile.last_reminder = timezone.now() user_profile.save(update_fields=['last_reminder'])