def enqueue_welcome_emails(user: UserProfile) -> 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, 'organization_setup_advice_link': user.realm.uri + '/help/getting-your-organization-started-with-zulip', 'getting_started_with_zulip_link': user.realm.uri + '/help/getting-started-with-zulip', 'keyboard_shortcuts_link': user.realm.uri + '/help/keyboard-shortcuts', 'is_realm_admin': user.is_realm_admin, }) 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: # 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 getattr(user, "just_registered", False): 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)) context['login_time'] = local_time.strftime('%A, %B %d, %Y at %I:%M%p ') + user_tz context['device_ip'] = request.META.get('REMOTE_ADDR') or _("Unknown IP address") context['device_os'] = get_device_os(user_agent) context['device_browser'] = get_device_browser(user_agent) email_dict = { 'template_prefix': 'zerver/emails/notify_new_login', 'to_user_id': user.id, 'from_name': 'Zulip Account Security', 'from_address': FromAddress.NOREPLY, 'context': context} queue_json_publish("email_senders", email_dict)
def enqueue_welcome_emails(email, name): # type: (Text, Text) -> None from zerver.context_processors import common_context if settings.WELCOME_EMAIL_SENDER is not None: sender = settings.WELCOME_EMAIL_SENDER # type: Dict[str, Text] else: sender = {'email': settings.ZULIP_ADMINISTRATOR, 'name': 'Zulip'} user_profile = get_user_profile_by_email(email) unsubscribe_link = one_click_unsubscribe_link(user_profile, "welcome") template_payload = common_context(user_profile) template_payload.update({ 'verbose_support_offers': settings.VERBOSE_SUPPORT_OFFERS, 'unsubscribe_link': unsubscribe_link }) # Send day 1 email send_local_email_template_with_delay([{'email': email, 'name': name}], "zerver/emails/followup/day1", template_payload, datetime.timedelta(hours=1), tags=["followup-emails"], sender=sender) # Send day 2 email send_local_email_template_with_delay([{'email': email, 'name': name}], "zerver/emails/followup/day2", template_payload, datetime.timedelta(days=1), tags=["followup-emails"], sender=sender)
def consume(self, data: Mapping[str, Any]) -> None: if "email" in data: # When upgrading from a version up through 1.7.1, there may be # existing items in the queue with `email` instead of `prereg_id`. invitee = PreregistrationUser.objects.filter( email__iexact=data["email"].strip()).latest("invited_at") else: invitee = PreregistrationUser.objects.filter(id=data["prereg_id"]).first() if invitee is None: # The invitation could have been revoked return referrer = get_user_profile_by_id(data["referrer_id"]) logger.info("Sending invitation for realm %s to %s" % (referrer.realm.string_id, invitee.email)) do_send_confirmation_email(invitee, referrer) # queue invitation reminder for two days from now. link = create_confirmation_link(invitee, referrer.realm.host, Confirmation.INVITATION) context = common_context(referrer) context.update({ 'activate_url': link, 'referrer_name': referrer.full_name, 'referrer_email': referrer.email, 'referrer_realm_name': referrer.realm.name, }) send_future_email( "zerver/emails/invitation_reminder", referrer.realm, to_emails=[invitee.email], from_address=FromAddress.tokenized_no_reply_address(), language=referrer.realm.default_language, context=context, delay=datetime.timedelta(days=2))
def email_on_new_login(sender, user, request, **kwargs): # type: (Any, UserProfile, Any, **Any) -> None # 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 getattr(user, "just_registered", False): return login_time = timezone_now().strftime('%A, %B %d, %Y at %I:%M%p ') + \ timezone_get_current_timezone_name() user_agent = request.META.get('HTTP_USER_AGENT', "").lower() device_browser = get_device_browser(user_agent) device_os = get_device_os(user_agent) device_ip = request.META.get('REMOTE_ADDR') or "Uknown IP address" device_info = {"device_browser": device_browser, "device_os": device_os, "device_ip": device_ip, "login_time": login_time } context = common_context(user) context['device_info'] = device_info context['user'] = user send_email('zerver/emails/notify_new_login', to_user_id=user.id, from_name='Zulip Account Security', from_address=FromAddress.NOREPLY, context=context)
def process_unsubscribe(request: HttpRequest, confirmation_key: str, subscription_type: str, unsubscribe_function: Callable[[UserProfile], None]) -> HttpResponse: try: user_profile = get_object_from_key(confirmation_key, Confirmation.UNSUBSCRIBE) except ConfirmationKeyException as exception: return render(request, 'zerver/unsubscribe_link_error.html') unsubscribe_function(user_profile) context = common_context(user_profile) context.update({"subscription_type": subscription_type}) return render(request, 'zerver/unsubscribe_success.html', context=context)
def process_unsubscribe(request, token, subscription_type, unsubscribe_function): # type: (HttpRequest, str, str, Callable[[UserProfile], None]) -> HttpResponse try: confirmation = Confirmation.objects.get(confirmation_key=token) except Confirmation.DoesNotExist: return render(request, 'zerver/unsubscribe_link_error.html') user_profile = confirmation.content_object unsubscribe_function(user_profile) context = common_context(user_profile) context.update({"subscription_type": subscription_type}) return render(request, 'zerver/unsubscribe_success.html', context=context)
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 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 find_account(request: HttpRequest) -> HttpResponse: from zerver.context_processors import common_context url = reverse('zerver.views.registration.find_account') emails = [] # type: List[str] if request.method == 'POST': form = FindMyTeamForm(request.POST) if form.is_valid(): emails = form.cleaned_data['emails'] for user in UserProfile.objects.filter( email__in=emails, is_active=True, is_bot=False, realm__deactivated=False): context = common_context(user) context.update({ 'email': user.email, }) send_email('zerver/emails/find_team', to_user_ids=[user.id], context=context) # Note: Show all the emails in the result otherwise this # feature can be used to ascertain which email addresses # are associated with Zulip. data = urllib.parse.urlencode({'emails': ','.join(emails)}) return redirect(url + "?" + data) else: form = FindMyTeamForm() result = request.GET.get('emails') # The below validation is perhaps unnecessary, in that we # shouldn't get able to get here with an invalid email unless # the user hand-edits the URLs. if result: for email in result.split(','): try: validators.validate_email(email) emails.append(email) except ValidationError: pass return render(request, 'zerver/find_account.html', context={'form': form, 'current_url': lambda: url, 'emails': emails},)
def consume(self, data): # type: (Mapping[str, Any]) -> None invitee = get_prereg_user_by_email(data["email"]) referrer = get_user_profile_by_id(data["referrer_id"]) body = data["email_body"] do_send_confirmation_email(invitee, referrer, body) # queue invitation reminder for two days from now. link = Confirmation.objects.get_link_for_object(invitee, host=referrer.realm.host) context = common_context(referrer) context.update({ 'activate_url': link, 'referrer_name': referrer.full_name, 'referrer_email': referrer.email, 'referrer_realm_name': referrer.realm.name, }) send_future_email( "zerver/emails/invitation_reminder", data["email"], from_email=settings.ZULIP_ADMINISTRATOR, context=context, delay=datetime.timedelta(days=2))
def enqueue_welcome_emails(email, name): # type: (Text, Text) -> None from zerver.context_processors import common_context if settings.WELCOME_EMAIL_SENDER is not None: # line break to avoid triggering lint rule from_email = '%(name)s <%(email)s>' % \ settings.WELCOME_EMAIL_SENDER else: from_email = settings.ZULIP_ADMINISTRATOR user_profile = get_user_profile_by_email(email) unsubscribe_link = one_click_unsubscribe_link(user_profile, "welcome") context = common_context(user_profile) context.update({ 'unsubscribe_link': unsubscribe_link }) send_future_email( "zerver/emails/followup_day1", '%s <%s>' % (name, email), from_email=from_email, context=context, delay=datetime.timedelta(hours=1)) send_future_email( "zerver/emails/followup_day2", '%s <%s>' % (name, email), from_email=from_email, context=context, delay=datetime.timedelta(days=1))
def consume(self, data): # type: (Mapping[str, Any]) -> None invitee = get_prereg_user_by_email(data["email"]) referrer = get_user_profile_by_email(data["referrer_email"]) do_send_confirmation_email(invitee, referrer) # queue invitation reminder for two days from now. link = Confirmation.objects.get_link_for_object(invitee, host=referrer.realm.host) context = common_context(referrer) context.update({ 'activate_url': link, 'referrer': referrer, 'verbose_support_offers': settings.VERBOSE_SUPPORT_OFFERS, 'support_email': settings.ZULIP_ADMINISTRATOR }) send_local_email_template_with_delay( [{'email': data["email"], 'name': ""}], "zerver/emails/invitation/invitation_reminder_email", context, datetime.timedelta(days=2), tags=["invitation-reminders"], sender={'email': settings.ZULIP_ADMINISTRATOR, 'name': 'Zulip'})
def consume(self, data): # type: (Mapping[str, Any]) -> None invitee = get_prereg_user_by_email(data["email"]) referrer = get_user_profile_by_id(data["referrer_id"]) body = data["email_body"] do_send_confirmation_email(invitee, referrer, body) # queue invitation reminder for two days from now. link = create_confirmation_link(invitee, referrer.realm.host, Confirmation.INVITATION) context = common_context(referrer) context.update({ 'activate_url': link, 'referrer_name': referrer.full_name, 'referrer_email': referrer.email, 'referrer_realm_name': referrer.realm.name, }) send_future_email( "zerver/emails/invitation_reminder", to_email=data["email"], from_address=FromAddress.NOREPLY, context=context, delay=datetime.timedelta(days=2))
def email_on_new_login(sender, user, request, **kwargs): # type: (Any, UserProfile, Any, **Any) -> None # 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: # Login emails are for returning users, not new registrations. # Determine if login request was from new registration. path = request.META.get('PATH_INFO', None) if path: if path == "/accounts/register/": return login_time = timezone_now().strftime('%A, %B %d, %Y at %I:%M%p ') + \ timezone_get_current_timezone_name() user_agent = request.META.get('HTTP_USER_AGENT', "").lower() device_browser = get_device_browser(user_agent) device_os = get_device_os(user_agent) device_ip = request.META.get('REMOTE_ADDR') or "Uknown IP address" device_info = {"device_browser": device_browser, "device_os": device_os, "device_ip": device_ip, "login_time": login_time } context = common_context(user) context['device_info'] = device_info context['user'] = user send_email_to_user('zerver/emails/notify_new_login', user, from_name='Zulip Account Security', from_address=FromAddress.NOREPLY, context=context)
def enqueue_welcome_emails(user_id): # type: (int) -> 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 user_profile = get_user_profile_by_id(user_id) unsubscribe_link = one_click_unsubscribe_link(user_profile, "welcome") context = common_context(user_profile) context.update({ 'unsubscribe_link': unsubscribe_link }) send_future_email( "zerver/emails/followup_day1", to_user_id=user_id, from_name=from_name, from_address=from_address, context=context, delay=datetime.timedelta(hours=1)) send_future_email( "zerver/emails/followup_day2", to_user_id=user_id, from_name=from_name, from_address=from_address, context=context, delay=datetime.timedelta(days=1))
def handle_digest_email(user_profile_id: int, cutoff: float) -> None: 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).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 # 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_id=user_profile.id, from_name="Zulip Digest", from_address=FromAddress.NOREPLY, context=context)
def handle_digest_email(user_profile_id, cutoff): # type: (int, float) -> None user_profile = UserProfile.objects.get(id=user_profile_id) # Convert from epoch seconds to a datetime object. cutoff_date = datetime.datetime.utcfromtimestamp(int(cutoff)) all_messages = UserMessage.objects.filter( user_profile=user_profile, message__pub_date__gt=cutoff_date).order_by("message__pub_date") template_payload = common_context(user_profile) # Start building email template data. template_payload.update({ '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 template_payload['unread_pms'] = build_message_list( user_profile, [pm.message for pm in pms[:pms_limit]]) template_payload['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. template_payload["hot_conversations"] = gather_hot_conversations( user_profile, stream_messages) # Gather new streams. new_streams_count, new_streams = gather_new_streams( user_profile, cutoff_date) template_payload["new_streams"] = new_streams template_payload["new_streams_count"] = new_streams_count # Gather users who signed up recently. new_users_count, new_users = gather_new_users( user_profile, cutoff_date) template_payload["new_users"] = new_users text_content = loader.render_to_string( 'zerver/emails/digest/digest_email.txt', template_payload) html_content = loader.render_to_string( 'zerver/emails/digest/digest_email_html.txt', template_payload) # We don't want to send emails containing almost no information. if enough_traffic(template_payload["unread_pms"], template_payload["hot_conversations"], new_streams_count, new_users_count): logger.info("Sending digest email for %s" % (user_profile.email,)) send_digest_email(user_profile, html_content, text_content)
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 """ 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, 'reply_warning': False, '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 = 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) # Convert from epoch seconds to a datetime object. cutoff_date = datetime.datetime.fromtimestamp(int(cutoff), tz=pytz.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 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 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, ) mentioned_user_group_name = get_mentioned_user_group_name( missed_messages, user_profile) 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"), mentioned_user_group_name=mentioned_user_group_name, ) # 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 notifications") 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 # message notification 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 handle_digest_email(user_profile_id, cutoff): # type: (int, float) -> None user_profile = UserProfile.objects.get(id=user_profile_id) # Convert from epoch seconds to a datetime object. cutoff_date = datetime.datetime.utcfromtimestamp(int(cutoff)) all_messages = UserMessage.objects.filter( user_profile=user_profile, message__pub_date__gt=cutoff_date).order_by("message__pub_date") template_payload = common_context(user_profile) # Start building email template data. template_payload.update({ '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 template_payload['unread_pms'] = build_message_list( user_profile, [pm.message for pm in pms[:pms_limit]]) template_payload['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. template_payload["hot_conversations"] = gather_hot_conversations( user_profile, stream_messages) # Gather new streams. new_streams_count, new_streams = gather_new_streams( user_profile, cutoff_date) template_payload["new_streams"] = new_streams template_payload["new_streams_count"] = new_streams_count # Gather users who signed up recently. new_users_count, new_users = gather_new_users(user_profile, cutoff_date) template_payload["new_users"] = new_users text_content = loader.render_to_string( 'zerver/emails/digest/digest_email.txt', template_payload) html_content = loader.render_to_string( 'zerver/emails/digest/digest_email_html.txt', template_payload) # We don't want to send emails containing almost no information. if enough_traffic(template_payload["unread_pms"], template_payload["hot_conversations"], new_streams_count, new_users_count): logger.info("Sending digest email for %s" % (user_profile.email, )) send_digest_email(user_profile, html_content, text_content)
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
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( 'All missed_messages must have the same recipient and topic %r' % (recipients, ), ) # 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 = None 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': 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 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.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://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 = 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_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") 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].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: 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) 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]]: # 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) all_stream_ids = set() for user in users: stream_ids = stream_map[user.id] if user.long_term_idle: stream_ids -= streams_recently_modified_for_user(user, cutoff_date) 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) for user in users: stream_ids = 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) 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 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 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 = [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