def enqueue_welcome_emails(user: UserProfile, realm_creation: bool=False) -> None: from zerver.context_processors import common_context if settings.WELCOME_EMAIL_SENDER is not None: # line break to avoid triggering lint rule from_name = settings.WELCOME_EMAIL_SENDER['name'] from_address = settings.WELCOME_EMAIL_SENDER['email'] else: from_name = None from_address = FromAddress.SUPPORT unsubscribe_link = one_click_unsubscribe_link(user, "welcome") context = common_context(user) context.update({ 'unsubscribe_link': unsubscribe_link, 'keyboard_shortcuts_link': user.realm.uri + '/help/keyboard-shortcuts', }) if user.is_realm_admin: context['user_role_group'] = _('admins') context['getting_started_link'] = (user.realm.uri + '/help/getting-your-organization-started-with-zulip') else: context['user_role_group'] = _('members') context['getting_started_link'] = user.realm.uri + '/help/getting-started-with-zulip' send_future_email( "zerver/emails/followup_day1", user.realm, to_user_id=user.id, from_name=from_name, from_address=from_address, context=context) send_future_email( "zerver/emails/followup_day2", user.realm, to_user_id=user.id, from_name=from_name, from_address=from_address, context=context, delay=followup_day2_email_delay(user))
def test_multiple_stream_senders( self, mock_send_future_email: mock.MagicMock, mock_enough_traffic: mock.MagicMock) -> None: othello = self.example_user('othello') self.subscribe(othello, 'Verona') one_day_ago = timezone_now() - datetime.timedelta(days=1) Message.objects.all().update(pub_date=one_day_ago) one_hour_ago = timezone_now() - datetime.timedelta(seconds=3600) cutoff = time.mktime(one_hour_ago.timetuple()) senders = ['hamlet', 'cordelia', 'iago', 'prospero', 'ZOE'] self.simulate_stream_conversation('Verona', senders) flush_per_request_caches() # When this test is run in isolation, one additional query is run which # is equivalent to # ContentType.objects.get(app_label='zerver', model='userprofile') # This code is run when we call `confirmation.models.create_confirmation_link`. # To trigger this, we call the one_click_unsubscribe_link function below. one_click_unsubscribe_link(othello, 'digest') with queries_captured() as queries: handle_digest_email(othello.id, cutoff) self.assert_length(queries, 6) self.assertEqual(mock_send_future_email.call_count, 1) kwargs = mock_send_future_email.call_args[1] self.assertEqual(kwargs['to_user_ids'], [othello.id]) hot_convo = kwargs['context']['hot_conversations'][0] expected_participants = { self.example_user(sender).full_name for sender in senders } self.assertEqual(set(hot_convo['participants']), expected_participants) self.assertEqual(hot_convo['count'], 5 - 2) # 5 messages, but 2 shown teaser_messages = hot_convo['first_few_messages'][0]['senders'] self.assertIn('some content', teaser_messages[0]['content'][0]['plain']) self.assertIn(teaser_messages[0]['sender'], expected_participants)
def test_guest_user_multiple_stream_sender( self, mock_send_future_email: mock.MagicMock, mock_enough_traffic: mock.MagicMock) -> None: othello = self.example_user("othello") hamlet = self.example_user("hamlet") cordelia = self.example_user("cordelia") polonius = self.example_user("polonius") create_stream_if_needed(cordelia.realm, "web_public_stream", is_web_public=True) self.subscribe(othello, "web_public_stream") self.subscribe(hamlet, "web_public_stream") self.subscribe(cordelia, "web_public_stream") self.subscribe(polonius, "web_public_stream") one_day_ago = timezone_now() - datetime.timedelta(days=1) Message.objects.all().update(date_sent=one_day_ago) one_hour_ago = timezone_now() - datetime.timedelta(seconds=3600) cutoff = time.mktime(one_hour_ago.timetuple()) senders = ["hamlet", "cordelia", "othello", "desdemona"] self.simulate_stream_conversation("web_public_stream", senders) # Remove RealmAuditoLog rows, so we don't exclude polonius. RealmAuditLog.objects.all().delete() flush_per_request_caches() # When this test is run in isolation, one additional query is run which # is equivalent to # ContentType.objects.get(app_label='zerver', model='userprofile') # This code is run when we call `confirmation.models.create_confirmation_link`. # To trigger this, we call the one_click_unsubscribe_link function below. one_click_unsubscribe_link(polonius, "digest") with queries_captured() as queries: bulk_handle_digest_email([polonius.id], cutoff) self.assert_length(queries, 9) self.assertEqual(mock_send_future_email.call_count, 1) kwargs = mock_send_future_email.call_args[1] self.assertEqual(kwargs["to_user_ids"], [polonius.id]) new_stream_names = kwargs["context"]["new_streams"]["plain"] self.assertTrue("web_public_stream" in new_stream_names)
def enqueue_welcome_emails(user: UserProfile, realm_creation: bool = False) -> None: from zerver.context_processors import common_context if settings.WELCOME_EMAIL_SENDER is not None: # line break to avoid triggering lint rule from_name = settings.WELCOME_EMAIL_SENDER['name'] from_address = settings.WELCOME_EMAIL_SENDER['email'] else: from_name = None from_address = FromAddress.SUPPORT other_account_count = UserProfile.objects.filter( email__iexact=user.email).exclude(id=user.id).count() unsubscribe_link = one_click_unsubscribe_link(user, "welcome") context = common_context(user) context.update({ 'unsubscribe_link': unsubscribe_link, 'keyboard_shortcuts_link': user.realm.uri + '/help/keyboard-shortcuts', 'realm_name': user.realm.name, 'realm_creation': realm_creation, 'email': user.email, 'is_realm_admin': user.is_realm_admin, }) if user.is_realm_admin: context['getting_started_link'] = ( user.realm.uri + '/help/getting-your-organization-started-with-zulip') else: context['getting_started_link'] = "https://zulipchat.com" from zproject.backends import email_belongs_to_ldap, require_email_format_usernames if email_belongs_to_ldap(user.realm, user.email): context["ldap"] = True if settings.LDAP_APPEND_DOMAIN: for backend in get_backends(): if isinstance(backend, LDAPBackend): context["ldap_username"] = backend.django_to_ldap_username( user.email) elif not settings.LDAP_EMAIL_ATTR: context["ldap_username"] = user.email send_future_email("zerver/emails/followup_day1", user.realm, to_user_ids=[user.id], from_name=from_name, from_address=from_address, context=context) if other_account_count == 0: send_future_email("zerver/emails/followup_day2", user.realm, to_user_ids=[user.id], from_name=from_name, from_address=from_address, context=context, delay=followup_day2_email_delay(user))
def bulk_get_digest_context(users: List[UserProfile], cutoff: float) -> Dict[int, Dict[str, Any]]: # Convert from epoch seconds to a datetime object. cutoff_date = datetime.datetime.fromtimestamp(int(cutoff), tz=datetime.timezone.utc) result: Dict[int, Dict[str, Any]] = {} user_ids = [user.id for user in users] def get_stream_map(user_ids: List[int]) -> Dict[int, List[int]]: # maps user_id -> [stream_id, stream_id, ...] rows = Subscription.objects.filter( user_profile_id__in=user_ids, recipient__type=Recipient.STREAM, active=True, is_muted=False, ).values('user_profile_id', 'recipient__type_id') dct: Dict[int, List[int]] = defaultdict(list) for row in rows: dct[row['user_profile_id']].append(row['recipient__type_id']) return dct stream_map = get_stream_map(user_ids) for user in users: context = common_context(user) # Start building email template data. unsubscribe_link = one_click_unsubscribe_link(user, "digest") context.update(unsubscribe_link=unsubscribe_link) home_view_streams = stream_map[user.id] if not user.long_term_idle: stream_ids = home_view_streams else: stream_ids = exclude_subscription_modified_streams( user, home_view_streams, cutoff_date) topic_activity = get_recent_topic_activity(stream_ids, cutoff_date) hot_topics = get_hot_topics(topic_activity) # Gather hot conversations. context["hot_conversations"] = gather_hot_topics( user, hot_topics, topic_activity) # Gather new streams. new_streams_count, new_streams = gather_new_streams(user, cutoff_date) context["new_streams"] = new_streams context["new_streams_count"] = new_streams_count result[user.id] = context return result
def test_multiple_stream_senders( self, mock_send_future_email: mock.MagicMock, mock_enough_traffic: mock.MagicMock ) -> None: othello = self.example_user("othello") self.subscribe(othello, "Verona") one_day_ago = timezone_now() - datetime.timedelta(days=1) Message.objects.all().update(date_sent=one_day_ago) one_hour_ago = timezone_now() - datetime.timedelta(seconds=3600) cutoff = time.mktime(one_hour_ago.timetuple()) senders = ["hamlet", "cordelia", "iago", "prospero", "ZOE"] self.simulate_stream_conversation("Verona", senders) # Remove RealmAuditoLog rows, so we don't exclude polonius. RealmAuditLog.objects.all().delete() flush_per_request_caches() # When this test is run in isolation, one additional query is run which # is equivalent to # ContentType.objects.get(app_label='zerver', model='userprofile') # This code is run when we call `confirmation.models.create_confirmation_link`. # To trigger this, we call the one_click_unsubscribe_link function below. one_click_unsubscribe_link(othello, "digest") with queries_captured() as queries: bulk_handle_digest_email([othello.id], cutoff) self.assert_length(queries, 9) self.assertEqual(mock_send_future_email.call_count, 1) kwargs = mock_send_future_email.call_args[1] self.assertEqual(kwargs["to_user_ids"], [othello.id]) hot_convo = kwargs["context"]["hot_conversations"][0] expected_participants = {self.example_user(sender).full_name for sender in senders} self.assertEqual(set(hot_convo["participants"]), expected_participants) self.assertEqual(hot_convo["count"], 5 - 2) # 5 messages, but 2 shown teaser_messages = hot_convo["first_few_messages"][0]["senders"] self.assertIn("some content", teaser_messages[0]["content"][0]["plain"]) self.assertIn(teaser_messages[0]["sender"], expected_participants)
def test_guest_user_multiple_stream_sender( self, mock_send_future_email: mock.MagicMock, mock_enough_traffic: mock.MagicMock) -> None: othello = self.example_user('othello') hamlet = self.example_user('hamlet') cordelia = self.example_user('cordelia') polonius = self.example_user('polonius') create_stream_if_needed(cordelia.realm, 'web_public_stream', is_web_public=True) self.subscribe(othello, 'web_public_stream') self.subscribe(hamlet, 'web_public_stream') self.subscribe(cordelia, 'web_public_stream') self.subscribe(polonius, 'web_public_stream') one_day_ago = timezone_now() - datetime.timedelta(days=1) Message.objects.all().update(date_sent=one_day_ago) one_hour_ago = timezone_now() - datetime.timedelta(seconds=3600) cutoff = time.mktime(one_hour_ago.timetuple()) senders = ['hamlet', 'cordelia', 'othello', 'desdemona'] self.simulate_stream_conversation('web_public_stream', senders) flush_per_request_caches() # When this test is run in isolation, one additional query is run which # is equivalent to # ContentType.objects.get(app_label='zerver', model='userprofile') # This code is run when we call `confirmation.models.create_confirmation_link`. # To trigger this, we call the one_click_unsubscribe_link function below. one_click_unsubscribe_link(polonius, 'digest') with queries_captured() as queries: handle_digest_email(polonius.id, cutoff) self.assert_length(queries, 7) self.assertEqual(mock_send_future_email.call_count, 1) kwargs = mock_send_future_email.call_args[1] self.assertEqual(kwargs['to_user_ids'], [polonius.id]) new_stream_names = kwargs['context']['new_streams']['plain'] self.assertTrue('web_public_stream' in new_stream_names)
def handle_digest_email(user_profile_id: int, cutoff: float, render_to_web: bool = False) -> Union[None, Dict[str, Any]]: user_profile = get_user_profile_by_id(user_profile_id) # Convert from epoch seconds to a datetime object. cutoff_date = datetime.datetime.fromtimestamp(int(cutoff), tz=datetime.timezone.utc) context = common_context(user_profile) # Start building email template data. context.update( unsubscribe_link=one_click_unsubscribe_link(user_profile, "digest"), ) home_view_streams = Subscription.objects.filter( user_profile=user_profile, recipient__type=Recipient.STREAM, active=True, is_muted=False).values_list('recipient__type_id', flat=True) if not user_profile.long_term_idle: stream_ids = home_view_streams else: stream_ids = exclude_subscription_modified_streams(user_profile, home_view_streams, cutoff_date) # Fetch list of all messages sent after cutoff_date where the user is subscribed messages = Message.objects.filter( recipient__type=Recipient.STREAM, recipient__type_id__in=stream_ids, date_sent__gt=cutoff_date).select_related('recipient', 'sender', 'sending_client') # Gather hot conversations. context["hot_conversations"] = gather_hot_conversations( user_profile, messages) # Gather new streams. new_streams_count, new_streams = gather_new_streams( user_profile, cutoff_date) context["new_streams"] = new_streams context["new_streams_count"] = new_streams_count # TODO: Set has_preheader if we want to include a preheader. if render_to_web: return context # We don't want to send emails containing almost no information. if enough_traffic(context["hot_conversations"], new_streams_count): logger.info("Sending digest email for user %s", user_profile.id) # Send now, as a ScheduledEmail send_future_email('zerver/emails/digest', user_profile.realm, to_user_ids=[user_profile.id], from_name="Zulip Digest", from_address=FromAddress.no_reply_placeholder, context=context) return None
def bulk_get_digest_context(users: List[UserProfile], cutoff: float) -> Dict[int, Dict[str, Any]]: # Convert from epoch seconds to a datetime object. cutoff_date = datetime.datetime.fromtimestamp(int(cutoff), tz=datetime.timezone.utc) result: Dict[int, Dict[str, Any]] = {} user_ids = [user.id for user in users] def get_stream_map(user_ids: List[int]) -> Dict[int, Set[int]]: rows = Subscription.objects.filter( user_profile_id__in=user_ids, recipient__type=Recipient.STREAM, active=True, is_muted=False, ).values('user_profile_id', 'recipient__type_id') # maps user_id -> {stream_id, stream_id, ...} dct: Dict[int, Set[int]] = defaultdict(set) for row in rows: dct[row['user_profile_id']].add(row['recipient__type_id']) return dct stream_map = get_stream_map(user_ids) for user in users: context = common_context(user) # Start building email template data. unsubscribe_link = one_click_unsubscribe_link(user, "digest") context.update(unsubscribe_link=unsubscribe_link) stream_ids = stream_map[user.id] if user.long_term_idle: stream_ids -= streams_recently_modified_for_user(user, cutoff_date) recent_topics = get_recent_topics(sorted(list(stream_ids)), cutoff_date) hot_topics = get_hot_topics(recent_topics) # Get context data for hot conversations. context["hot_conversations"] = [ hot_topic.teaser_data(user) for hot_topic in hot_topics ] # Gather new streams. new_streams_count, new_streams = gather_new_streams(user, cutoff_date) context["new_streams"] = new_streams context["new_streams_count"] = new_streams_count result[user.id] = context return result
def enqueue_welcome_emails(user: UserProfile, realm_creation: bool=False) -> None: from zerver.context_processors import common_context if settings.WELCOME_EMAIL_SENDER is not None: # line break to avoid triggering lint rule from_name = settings.WELCOME_EMAIL_SENDER['name'] from_address = settings.WELCOME_EMAIL_SENDER['email'] else: from_name = None from_address = FromAddress.SUPPORT other_account_count = UserProfile.objects.filter( delivery_email__iexact=user.delivery_email).exclude(id=user.id).count() unsubscribe_link = one_click_unsubscribe_link(user, "welcome") context = common_context(user) context.update({ 'unsubscribe_link': unsubscribe_link, 'keyboard_shortcuts_link': user.realm.uri + '/help/keyboard-shortcuts', 'realm_name': user.realm.name, 'realm_creation': realm_creation, 'email': user.delivery_email, 'is_realm_admin': user.role == UserProfile.ROLE_REALM_ADMINISTRATOR, }) if user.is_realm_admin: context['getting_started_link'] = (user.realm.uri + '/help/getting-your-organization-started-with-zulip') else: context['getting_started_link'] = "https://zulipchat.com" # Imported here to avoid import cycles. from zproject.backends import email_belongs_to_ldap, ZulipLDAPAuthBackend if email_belongs_to_ldap(user.realm, user.delivery_email): context["ldap"] = True for backend in get_backends(): # If the user is doing authentication via LDAP, Note that # we exclude ZulipLDAPUserPopulator here, since that # isn't used for authentication. if isinstance(backend, ZulipLDAPAuthBackend): context["ldap_username"] = backend.django_to_ldap_username(user.delivery_email) break send_future_email( "zerver/emails/followup_day1", user.realm, to_user_ids=[user.id], from_name=from_name, from_address=from_address, context=context) if other_account_count == 0: send_future_email( "zerver/emails/followup_day2", user.realm, to_user_ids=[user.id], from_name=from_name, from_address=from_address, context=context, delay=followup_day2_email_delay(user))
def email_on_new_login(sender: Any, user: UserProfile, request: Any, **kwargs: Any) -> None: if not user.enable_login_emails: return # We import here to minimize the dependencies of this module, # since it runs as part of `manage.py` initialization from zerver.context_processors import common_context if not settings.SEND_LOGIN_EMAILS: return if request: # If the user's account was just created, avoid sending an email. if (timezone_now() - user.date_joined).total_seconds() <= JUST_CREATED_THRESHOLD: return user_agent = request.META.get('HTTP_USER_AGENT', "").lower() context = common_context(user) context['user_email'] = user.delivery_email user_tz = user.timezone if user_tz == '': user_tz = timezone_get_current_timezone_name() local_time = timezone_now().astimezone(get_timezone(user_tz)) if user.twenty_four_hour_time: hhmm_string = local_time.strftime('%H:%M') else: hhmm_string = local_time.strftime('%I:%M%p') context['login_time'] = local_time.strftime( '%A, %B %d, %Y at {} %Z'.format(hhmm_string)) context['device_ip'] = request.META.get('REMOTE_ADDR') or _( "Unknown IP address") context['device_os'] = get_device_os(user_agent) or _( "an unknown operating system") context['device_browser'] = get_device_browser(user_agent) or _( "An unknown browser") context['unsubscribe_link'] = one_click_unsubscribe_link(user, 'login') email_dict = { 'template_prefix': 'zerver/emails/notify_new_login', 'to_user_ids': [user.id], 'from_name': FromAddress.security_email_from_name(user_profile=user), 'from_address': FromAddress.NOREPLY, 'context': context } queue_json_publish("email_senders", email_dict)
def email_on_new_login(sender: Any, user: UserProfile, request: Any, **kwargs: Any) -> None: if not user.enable_login_emails: return # We import here to minimize the dependencies of this module, # since it runs as part of `manage.py` initialization from zerver.context_processors import common_context if not settings.SEND_LOGIN_EMAILS: return if request: # If the user's account was just created, avoid sending an email. if (timezone_now() - user.date_joined).total_seconds() <= JUST_CREATED_THRESHOLD: return user_agent = request.META.get("HTTP_USER_AGENT", "").lower() context = common_context(user) context["user_email"] = user.delivery_email user_tz = user.timezone if user_tz == "": user_tz = timezone_get_current_timezone_name() local_time = timezone_now().astimezone(pytz.timezone(user_tz)) if user.twenty_four_hour_time: hhmm_string = local_time.strftime("%H:%M") else: hhmm_string = local_time.strftime("%I:%M%p") context["login_time"] = local_time.strftime( f"%A, %B %d, %Y at {hhmm_string} %Z") context["device_ip"] = request.META.get("REMOTE_ADDR") or _( "Unknown IP address") context["device_os"] = get_device_os(user_agent) or _( "an unknown operating system") context["device_browser"] = get_device_browser(user_agent) or _( "An unknown browser") context["unsubscribe_link"] = one_click_unsubscribe_link(user, "login") email_dict = { "template_prefix": "zerver/emails/notify_new_login", "to_user_ids": [user.id], "from_name": FromAddress.security_email_from_name(user_profile=user), "from_address": FromAddress.NOREPLY, "context": context, } queue_json_publish("email_senders", email_dict)
def enqueue_welcome_emails(user: UserProfile, realm_creation: bool=False) -> None: from zerver.context_processors import common_context if settings.WELCOME_EMAIL_SENDER is not None: # line break to avoid triggering lint rule from_name = settings.WELCOME_EMAIL_SENDER['name'] from_address = settings.WELCOME_EMAIL_SENDER['email'] else: from_name = None from_address = FromAddress.SUPPORT other_account_count = UserProfile.objects.filter( delivery_email__iexact=user.delivery_email).exclude(id=user.id).count() unsubscribe_link = one_click_unsubscribe_link(user, "welcome") context = common_context(user) context.update({ 'unsubscribe_link': unsubscribe_link, 'keyboard_shortcuts_link': user.realm.uri + '/help/keyboard-shortcuts', 'realm_name': user.realm.name, 'realm_creation': realm_creation, 'email': user.email, 'is_realm_admin': user.is_realm_admin, }) if user.is_realm_admin: context['getting_started_link'] = (user.realm.uri + '/help/getting-your-organization-started-with-zulip') else: context['getting_started_link'] = "https://zulipchat.com" from zproject.backends import email_belongs_to_ldap if email_belongs_to_ldap(user.realm, user.email): context["ldap"] = True if settings.LDAP_APPEND_DOMAIN: for backend in get_backends(): if isinstance(backend, LDAPBackend): context["ldap_username"] = backend.django_to_ldap_username(user.email) elif not settings.LDAP_EMAIL_ATTR: context["ldap_username"] = user.email send_future_email( "zerver/emails/followup_day1", user.realm, to_user_ids=[user.id], from_name=from_name, from_address=from_address, context=context) if other_account_count == 0: send_future_email( "zerver/emails/followup_day2", user.realm, to_user_ids=[user.id], from_name=from_name, from_address=from_address, context=context, delay=followup_day2_email_delay(user))
def enqueue_welcome_emails(user: UserProfile, realm_creation: bool = False) -> None: from zerver.context_processors import common_context if settings.WELCOME_EMAIL_SENDER is not None: # line break to avoid triggering lint rule from_name = settings.WELCOME_EMAIL_SENDER['name'] from_address = settings.WELCOME_EMAIL_SENDER['email'] else: from_name = None from_address = FromAddress.SUPPORT unsubscribe_link = one_click_unsubscribe_link(user, "welcome") context = common_context(user) context.update({ 'unsubscribe_link': unsubscribe_link, 'keyboard_shortcuts_link': user.realm.uri + '/help/keyboard-shortcuts', }) if user.is_realm_admin: context['user_role_group'] = _('admins') context['getting_started_link'] = ( user.realm.uri + '/help/getting-your-organization-started-with-zulip') else: context['user_role_group'] = _('members') context[ 'getting_started_link'] = user.realm.uri + '/help/getting-started-with-zulip' send_future_email("zerver/emails/followup_day1", user.realm, to_user_id=user.id, from_name=from_name, from_address=from_address, context=context) send_future_email("zerver/emails/followup_day2", user.realm, to_user_id=user.id, from_name=from_name, from_address=from_address, context=context, delay=followup_day2_email_delay(user))
def email_on_new_login(sender: Any, user: UserProfile, request: Any, **kwargs: Any) -> None: if not user.enable_login_emails: return # We import here to minimize the dependencies of this module, # since it runs as part of `manage.py` initialization from zerver.context_processors import common_context if not settings.SEND_LOGIN_EMAILS: return if request: # If the user's account was just created, avoid sending an email. if (timezone_now() - user.date_joined).total_seconds() <= JUST_CREATED_THRESHOLD: return user_agent = request.META.get('HTTP_USER_AGENT', "").lower() context = common_context(user) context['user_email'] = user.email user_tz = user.timezone if user_tz == '': user_tz = timezone_get_current_timezone_name() local_time = timezone_now().astimezone(get_timezone(user_tz)) if user.twenty_four_hour_time: hhmm_string = local_time.strftime('%H:%M') else: hhmm_string = local_time.strftime('%I:%M%p') context['login_time'] = local_time.strftime('%A, %B %d, %Y at {} %Z'.format(hhmm_string)) context['device_ip'] = request.META.get('REMOTE_ADDR') or _("Unknown IP address") context['device_os'] = get_device_os(user_agent) or _("an unknown operating system") context['device_browser'] = get_device_browser(user_agent) or _("An unknown browser") context['unsubscribe_link'] = one_click_unsubscribe_link(user, 'login') email_dict = { 'template_prefix': 'zerver/emails/notify_new_login', 'to_user_ids': [user.id], 'from_name': 'Zulip Account Security', 'from_address': FromAddress.NOREPLY, 'context': context} queue_json_publish("email_senders", email_dict)
def test_soft_deactivated_user_multiple_stream_senders( self, mock_send_future_email: mock.MagicMock, mock_enough_traffic: mock.MagicMock) -> None: one_day_ago = timezone_now() - datetime.timedelta(days=1) Message.objects.all().update(date_sent=one_day_ago) othello = self.example_user('othello') for stream in ['Verona', 'Scotland', 'Denmark']: self.subscribe(othello, stream) RealmAuditLog.objects.all().delete() othello.long_term_idle = True othello.save(update_fields=['long_term_idle']) # Send messages to a stream and unsubscribe - subscribe from that stream senders = ['hamlet', 'cordelia', 'iago', 'prospero', 'ZOE'] self.simulate_stream_conversation('Verona', senders) self.unsubscribe(othello, 'Verona') self.subscribe(othello, 'Verona') # Send messages to other streams self.simulate_stream_conversation('Scotland', senders) self.simulate_stream_conversation('Denmark', senders) one_hour_ago = timezone_now() - datetime.timedelta(seconds=3600) cutoff = time.mktime(one_hour_ago.timetuple()) flush_per_request_caches() # When this test is run in isolation, one additional query is run which # is equivalent to # ContentType.objects.get(app_label='zerver', model='userprofile') # This code is run when we call `confirmation.models.create_confirmation_link`. # To trigger this, we call the one_click_unsubscribe_link function below. one_click_unsubscribe_link(othello, 'digest') with queries_captured() as queries: handle_digest_email(othello.id, cutoff) # This can definitely be optimized; for both the huddle and # stream cases, the get_narrow_url API ends up double-fetching # some data because of how the functions are organized. self.assert_length(queries, 9) self.assertEqual(mock_send_future_email.call_count, 1) kwargs = mock_send_future_email.call_args[1] self.assertEqual(kwargs['to_user_ids'], [othello.id]) hot_conversations = kwargs['context']['hot_conversations'] self.assertEqual(2, len(hot_conversations), [othello.id]) hot_convo = hot_conversations[0] expected_participants = { self.example_user(sender).full_name for sender in senders } self.assertEqual(set(hot_convo['participants']), expected_participants) self.assertEqual(hot_convo['count'], 5 - 2) # 5 messages, but 2 shown teaser_messages = hot_convo['first_few_messages'][0]['senders'] self.assertIn('some content', teaser_messages[0]['content'][0]['plain']) self.assertIn(teaser_messages[0]['sender'], expected_participants)
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: 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 send_custom_email(users: List[UserProfile], options: Dict[str, Any]) -> None: """ Can be used directly with from a management shell with send_custom_email(user_profile_list, dict( markdown_template_path="/path/to/markdown/file.md", subject="Email Subject", from_name="Sender Name") ) """ with open(options["markdown_template_path"]) as f: text = f.read() parsed_email_template = Parser(policy=default).parsestr(text) email_template_hash = hashlib.sha256(text.encode("utf-8")).hexdigest()[0:32] email_filename = f"custom/custom_email_{email_template_hash}.source.html" email_id = f"zerver/emails/custom/custom_email_{email_template_hash}" markdown_email_base_template_path = "templates/zerver/emails/custom_email_base.pre.html" html_source_template_path = f"templates/{email_id}.source.html" plain_text_template_path = f"templates/{email_id}.txt" subject_path = f"templates/{email_id}.subject.txt" os.makedirs(os.path.dirname(html_source_template_path), exist_ok=True) # First, we render the Markdown input file just like our # user-facing docs with render_markdown_path. with open(plain_text_template_path, "w") as f: f.write(parsed_email_template.get_payload()) from zerver.templatetags.app_filters import render_markdown_path rendered_input = render_markdown_path(plain_text_template_path.replace("templates/", "")) # And then extend it with our standard email headers. with open(html_source_template_path, "w") as f: with open(markdown_email_base_template_path) as base_template: # Note that we're doing a hacky non-Jinja2 substitution here; # we do this because the normal render_markdown_path ordering # doesn't commute properly with inline_email_css. f.write(base_template.read().replace("{{ rendered_input }}", rendered_input)) with open(subject_path, "w") as f: f.write(get_header(options.get("subject"), parsed_email_template.get("subject"), "subject")) inline_template(email_filename) # Finally, we send the actual emails. for user_profile in users: if options.get("admins_only") and not user_profile.is_realm_admin: continue context = { "realm_uri": user_profile.realm.uri, "realm_name": user_profile.realm.name, "unsubscribe_link": one_click_unsubscribe_link(user_profile, "marketing"), } send_email( email_id, to_user_ids=[user_profile.id], from_address=FromAddress.SUPPORT, reply_to_email=options.get("reply_to"), from_name=get_header( options.get("from_name"), parsed_email_template.get("from"), "from_name" ), context=context, dry_run=options["dry_run"], ) if options["dry_run"]: break
def test_soft_deactivated_user_multiple_stream_senders(self) -> None: one_day_ago = timezone_now() - datetime.timedelta(days=1) Message.objects.all().update(date_sent=one_day_ago) digest_users = [ self.example_user('othello'), self.example_user('aaron'), self.example_user('desdemona'), self.example_user('polonius'), ] digest_users.sort(key=lambda user: user.id) for digest_user in digest_users: for stream in ['Verona', 'Scotland', 'Denmark']: self.subscribe(digest_user, stream) RealmAuditLog.objects.all().delete() # Send messages to a stream and unsubscribe - subscribe from that stream senders = ['hamlet', 'cordelia', 'iago', 'prospero', 'ZOE'] self.simulate_stream_conversation('Verona', senders) for digest_user in digest_users: self.unsubscribe(digest_user, 'Verona') self.subscribe(digest_user, 'Verona') # Send messages to other streams self.simulate_stream_conversation('Scotland', senders) self.simulate_stream_conversation('Denmark', senders) one_hour_ago = timezone_now() - datetime.timedelta(seconds=3600) cutoff = time.mktime(one_hour_ago.timetuple()) flush_per_request_caches() # When this test is run in isolation, one additional query is run which # is equivalent to # ContentType.objects.get(app_label='zerver', model='userprofile') # This code is run when we call `confirmation.models.create_confirmation_link`. # To trigger this, we call the one_click_unsubscribe_link function below. one_click_unsubscribe_link(digest_users[0], 'digest') with mock.patch('zerver.lib.digest.send_future_email' ) as mock_send_future_email: digest_user_ids = [user.id for user in digest_users] with queries_captured() as queries: with cache_tries_captured() as cache_tries: bulk_handle_digest_email(digest_user_ids, cutoff) self.assert_length(queries, 12) self.assert_length(cache_tries, 0) self.assertEqual(mock_send_future_email.call_count, len(digest_users)) for i, digest_user in enumerate(digest_users): kwargs = mock_send_future_email.call_args_list[i][1] self.assertEqual(kwargs['to_user_ids'], [digest_user.id]) hot_conversations = kwargs['context']['hot_conversations'] self.assertEqual(2, len(hot_conversations), [digest_user.id]) hot_convo = hot_conversations[0] expected_participants = { self.example_user(sender).full_name for sender in senders } self.assertEqual(set(hot_convo['participants']), expected_participants) self.assertEqual(hot_convo['count'], 5 - 2) # 5 messages, but 2 shown teaser_messages = hot_convo['first_few_messages'][0]['senders'] self.assertIn('some content', teaser_messages[0]['content'][0]['plain']) self.assertIn(teaser_messages[0]['sender'], expected_participants) last_message_id = get_last_message_id() for digest_user in digest_users: log_rows = RealmAuditLog.objects.filter( modified_user_id=digest_user.id, event_type=RealmAuditLog.USER_DIGEST_EMAIL_CREATED, ) (log, ) = log_rows self.assertEqual(log.event_last_message_id, last_message_id)
def handle_digest_email( user_profile_id: int, cutoff: float, render_to_web: bool = False) -> Union[None, Dict[str, Any]]: user_profile = get_user_profile_by_id(user_profile_id) # We are disabling digest emails for soft deactivated users for the time. # TODO: Find an elegant way to generate digest emails for these users. if user_profile.long_term_idle: return None # Convert from epoch seconds to a datetime object. cutoff_date = datetime.datetime.fromtimestamp(int(cutoff), tz=pytz.utc) all_messages = UserMessage.objects.filter( user_profile=user_profile, message__pub_date__gt=cutoff_date).select_related('message').order_by( "message__pub_date") context = common_context(user_profile) # Start building email template data. context.update({ 'unsubscribe_link': one_click_unsubscribe_link(user_profile, "digest") }) # Gather recent missed PMs, re-using the missed PM email logic. # You can't have an unread message that you sent, but when testing # this causes confusion so filter your messages out. pms = all_messages.filter(~Q(message__recipient__type=Recipient.STREAM) & ~Q(message__sender=user_profile)) # Show up to 4 missed PMs. pms_limit = 4 context['unread_pms'] = build_message_list( user_profile, [pm.message for pm in pms[:pms_limit]]) context['remaining_unread_pms_count'] = max(0, len(pms) - pms_limit) home_view_recipients = Subscription.objects.filter( user_profile=user_profile, active=True, in_home_view=True).values_list('recipient_id', flat=True) stream_messages = all_messages.filter( message__recipient__type=Recipient.STREAM, message__recipient__in=home_view_recipients) messages = [um.message for um in stream_messages] # Gather hot conversations. context["hot_conversations"] = gather_hot_conversations( user_profile, messages) # Gather new streams. new_streams_count, new_streams = gather_new_streams( user_profile, cutoff_date) context["new_streams"] = new_streams context["new_streams_count"] = new_streams_count # Gather users who signed up recently. new_users_count, new_users = gather_new_users(user_profile, cutoff_date) context["new_users"] = new_users if render_to_web: return context # We don't want to send emails containing almost no information. if enough_traffic(context["unread_pms"], context["hot_conversations"], new_streams_count, new_users_count): logger.info("Sending digest email for %s" % (user_profile.email, )) # Send now, as a ScheduledEmail send_future_email('zerver/emails/digest', user_profile.realm, to_user_ids=[user_profile.id], from_name="Zulip Digest", from_address=FromAddress.NOREPLY, context=context) return None
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': message_content_allowed_in_missedmessage_emails(user_profile) }) 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 message_content_allowed_in_missedmessage_emails(user_profile): 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 handle_digest_email(user_profile_id: int, cutoff: float, render_to_web: bool = False) -> Union[None, Dict[str, Any]]: user_profile = get_user_profile_by_id(user_profile_id) # We are disabling digest emails for soft deactivated users for the time. # TODO: Find an elegant way to generate digest emails for these users. if user_profile.long_term_idle: return None # Convert from epoch seconds to a datetime object. cutoff_date = datetime.datetime.fromtimestamp(int(cutoff), tz=pytz.utc) all_messages = UserMessage.objects.filter( user_profile=user_profile, message__pub_date__gt=cutoff_date ).select_related('message').order_by("message__pub_date") context = common_context(user_profile) # Start building email template data. context.update({ 'realm_name': user_profile.realm.name, 'name': user_profile.full_name, 'unsubscribe_link': one_click_unsubscribe_link(user_profile, "digest") }) # Gather recent missed PMs, re-using the missed PM email logic. # You can't have an unread message that you sent, but when testing # this causes confusion so filter your messages out. pms = all_messages.filter( ~Q(message__recipient__type=Recipient.STREAM) & ~Q(message__sender=user_profile)) # Show up to 4 missed PMs. pms_limit = 4 context['unread_pms'] = build_message_list( user_profile, [pm.message for pm in pms[:pms_limit]]) context['remaining_unread_pms_count'] = min(0, len(pms) - pms_limit) home_view_recipients = [sub.recipient for sub in Subscription.objects.filter( user_profile=user_profile, active=True, in_home_view=True)] stream_messages = all_messages.filter( message__recipient__type=Recipient.STREAM, message__recipient__in=home_view_recipients) # Gather hot conversations. context["hot_conversations"] = gather_hot_conversations( user_profile, stream_messages) # Gather new streams. new_streams_count, new_streams = gather_new_streams( user_profile, cutoff_date) context["new_streams"] = new_streams context["new_streams_count"] = new_streams_count # Gather users who signed up recently. new_users_count, new_users = gather_new_users( user_profile, cutoff_date) context["new_users"] = new_users if render_to_web: return context # We don't want to send emails containing almost no information. if enough_traffic(context["unread_pms"], context["hot_conversations"], new_streams_count, new_users_count): logger.info("Sending digest email for %s" % (user_profile.email,)) # Send now, as a ScheduledEmail send_future_email('zerver/emails/digest', user_profile.realm, to_user_ids=[user_profile.id], from_name="Zulip Digest", from_address=FromAddress.NOREPLY, context=context) return None
def enqueue_welcome_emails(user: UserProfile, realm_creation: bool = False) -> None: from zerver.context_processors import common_context if settings.WELCOME_EMAIL_SENDER is not None: # line break to avoid triggering lint rule from_name = settings.WELCOME_EMAIL_SENDER["name"] from_address = settings.WELCOME_EMAIL_SENDER["email"] else: from_name = None from_address = FromAddress.support_placeholder other_account_count = (UserProfile.objects.filter( delivery_email__iexact=user.delivery_email).exclude( id=user.id).count()) unsubscribe_link = one_click_unsubscribe_link(user, "welcome") context = common_context(user) context.update( unsubscribe_link=unsubscribe_link, keyboard_shortcuts_link=user.realm.uri + "/help/keyboard-shortcuts", realm_name=user.realm.name, realm_creation=realm_creation, email=user.delivery_email, is_realm_admin=user.is_realm_admin, is_demo_org=user.realm.demo_organization_scheduled_deletion_date is not None, ) if user.is_realm_admin: context["getting_started_link"] = ( user.realm.uri + "/help/getting-your-organization-started-with-zulip") else: context["getting_started_link"] = "https://zulip.com" # Imported here to avoid import cycles. from zproject.backends import ZulipLDAPAuthBackend, email_belongs_to_ldap if email_belongs_to_ldap(user.realm, user.delivery_email): context["ldap"] = True for backend in get_backends(): # If the user is doing authentication via LDAP, Note that # we exclude ZulipLDAPUserPopulator here, since that # isn't used for authentication. if isinstance(backend, ZulipLDAPAuthBackend): context["ldap_username"] = backend.django_to_ldap_username( user.delivery_email) break send_future_email( "zerver/emails/followup_day1", user.realm, to_user_ids=[user.id], from_name=from_name, from_address=from_address, context=context, ) if other_account_count == 0: send_future_email( "zerver/emails/followup_day2", user.realm, to_user_ids=[user.id], from_name=from_name, from_address=from_address, context=context, delay=followup_day2_email_delay(user), )
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 bulk_get_digest_context(users: List[UserProfile], cutoff: float) -> Dict[int, Dict[str, Any]]: # We expect a non-empty list of users all from the same realm. assert users realm = users[0].realm for user in users: assert user.realm_id == realm.id # Convert from epoch seconds to a datetime object. cutoff_date = datetime.datetime.fromtimestamp(int(cutoff), tz=datetime.timezone.utc) result: Dict[int, Dict[str, Any]] = {} user_ids = [user.id for user in users] user_stream_map = get_user_stream_map(user_ids) recently_modified_streams = get_modified_streams(user_ids, cutoff_date) all_stream_ids = set() for user in users: stream_ids = user_stream_map[user.id] stream_ids -= recently_modified_streams.get(user.id, set()) all_stream_ids |= stream_ids # Get all the recent topics for all the users. This does the heavy # lifting of making an expensive query to the Message table. Then # for each user, we filter to just the streams they care about. recent_topics = get_recent_topics(sorted(list(all_stream_ids)), cutoff_date) stream_map = get_slim_stream_map(all_stream_ids) recent_streams = get_recent_streams(realm, cutoff_date) for user in users: stream_ids = user_stream_map[user.id] hot_topics = get_hot_topics(recent_topics, stream_ids) context = common_context(user) # Start building email template data. unsubscribe_link = one_click_unsubscribe_link(user, "digest") context.update(unsubscribe_link=unsubscribe_link) # Get context data for hot conversations. context["hot_conversations"] = [ hot_topic.teaser_data(user, stream_map) for hot_topic in hot_topics ] # Gather new streams. new_streams_count, new_streams = gather_new_streams( realm=realm, recent_streams=recent_streams, can_access_public=user.can_access_public_streams(), ) context["new_streams"] = new_streams context["new_streams_count"] = new_streams_count result[user.id] = context return result