i18n: Use the recipient's language when sending outgoing emails.

It appears that our i18n logic was only using the recipient's language
for logged-in emails, so even properly tagged for translation and
translated emails for functions like "Find my team" and "password
reset" were being always sent in English.

With great work by Vishnu Ks on the tests and the to_emails code path.
This commit is contained in:
Tim Abbott
2018-12-13 23:41:42 -08:00
parent b10c23c233
commit b2fc017671
6 changed files with 77 additions and 24 deletions

View File

@@ -264,6 +264,7 @@ class ZulipPasswordResetForm(PasswordResetForm):
send_email('zerver/emails/password_reset', to_emails=[email], send_email('zerver/emails/password_reset', to_emails=[email],
from_name="Zulip Account Security", from_name="Zulip Account Security",
from_address=FromAddress.tokenized_no_reply_address(), from_address=FromAddress.tokenized_no_reply_address(),
language=request.LANGUAGE_CODE,
context=context) context=context)
class CreateUserForm(forms.Form): class CreateUserForm(forms.Form):

View File

@@ -815,7 +815,7 @@ def do_start_email_change_process(user_profile: UserProfile, new_email: str) ->
}) })
send_email('zerver/emails/confirm_new_email', to_emails=[new_email], send_email('zerver/emails/confirm_new_email', to_emails=[new_email],
from_name='Zulip Account Security', from_address=FromAddress.tokenized_no_reply_address(), from_name='Zulip Account Security', from_address=FromAddress.tokenized_no_reply_address(),
context=context) language=user_profile.default_language, context=context)
def compute_irc_user_fullname(email: str) -> str: def compute_irc_user_fullname(email: str) -> str:
return email.split("@")[0] + " (IRC)" return email.split("@")[0] + " (IRC)"
@@ -4461,7 +4461,8 @@ def do_send_confirmation_email(invitee: PreregistrationUser,
'activate_url': activation_url, 'referrer_realm_name': referrer.realm.name} 'activate_url': activation_url, 'referrer_realm_name': referrer.realm.name}
from_name = "%s (via Zulip)" % (referrer.full_name,) from_name = "%s (via Zulip)" % (referrer.full_name,)
send_email('zerver/emails/invitation', to_emails=[invitee.email], from_name=from_name, send_email('zerver/emails/invitation', to_emails=[invitee.email], from_name=from_name,
from_address=FromAddress.tokenized_no_reply_address(), context=context) from_address=FromAddress.tokenized_no_reply_address(),
language=referrer.realm.default_language, context=context)
def email_not_system_bot(email: str) -> None: def email_not_system_bot(email: str) -> None:
if is_cross_realm_bot_email(email): if is_cross_realm_bot_email(email):

View File

@@ -2,6 +2,7 @@ from django.conf import settings
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.template import loader from django.template import loader
from django.utils.timezone import now as timezone_now from django.utils.timezone import now as timezone_now
from django.utils.translation import override as override_language
from django.template.exceptions import TemplateDoesNotExist from django.template.exceptions import TemplateDoesNotExist
from zerver.models import UserProfile, ScheduledEmail, get_user_profile_by_id, \ from zerver.models import UserProfile, ScheduledEmail, get_user_profile_by_id, \
EMAIL_TYPES, Realm EMAIL_TYPES, Realm
@@ -12,7 +13,7 @@ import logging
import ujson import ujson
import os import os
from typing import Any, Dict, Iterable, List, Mapping, Optional from typing import Any, Dict, Iterable, List, Mapping, Optional, Tuple
from zerver.lib.logging_util import log_to_file from zerver.lib.logging_util import log_to_file
from confirmation.models import generate_key from confirmation.models import generate_key
@@ -36,7 +37,8 @@ class FromAddress:
def build_email(template_prefix: str, to_user_ids: Optional[List[int]]=None, def build_email(template_prefix: str, to_user_ids: Optional[List[int]]=None,
to_emails: Optional[List[str]]=None, from_name: Optional[str]=None, to_emails: Optional[List[str]]=None, from_name: Optional[str]=None,
from_address: Optional[str]=None, reply_to_email: Optional[str]=None, from_address: Optional[str]=None, reply_to_email: Optional[str]=None,
context: Optional[Dict[str, Any]]=None) -> EmailMultiAlternatives: language: Optional[str]=None, context: Optional[Dict[str, Any]]=None
) -> EmailMultiAlternatives:
# Callers should pass exactly one of to_user_id and to_email. # Callers should pass exactly one of to_user_id and to_email.
assert (to_user_ids is None) ^ (to_emails is None) assert (to_user_ids is None) ^ (to_emails is None)
if to_user_ids is not None: if to_user_ids is not None:
@@ -53,19 +55,32 @@ def build_email(template_prefix: str, to_user_ids: Optional[List[int]]=None,
'email_images_base_uri': settings.ROOT_DOMAIN_URI + '/static/images/emails', 'email_images_base_uri': settings.ROOT_DOMAIN_URI + '/static/images/emails',
'physical_address': settings.PHYSICAL_ADDRESS, 'physical_address': settings.PHYSICAL_ADDRESS,
}) })
subject = loader.render_to_string(template_prefix + '.subject',
context=context,
using='Jinja2_plaintext').strip().replace('\n', '')
message = loader.render_to_string(template_prefix + '.txt',
context=context, using='Jinja2_plaintext')
try: def render_templates() -> Tuple[str, str, str]:
html_message = loader.render_to_string(template_prefix + '.html', context) subject = loader.render_to_string(template_prefix + '.subject',
except TemplateDoesNotExist: context=context,
emails_dir = os.path.dirname(template_prefix) using='Jinja2_plaintext').strip().replace('\n', '')
template = os.path.basename(template_prefix) message = loader.render_to_string(template_prefix + '.txt',
compiled_template_prefix = os.path.join(emails_dir, "compiled", template) context=context, using='Jinja2_plaintext')
html_message = loader.render_to_string(compiled_template_prefix + '.html', context)
try:
html_message = loader.render_to_string(template_prefix + '.html', context)
except TemplateDoesNotExist:
emails_dir = os.path.dirname(template_prefix)
template = os.path.basename(template_prefix)
compiled_template_prefix = os.path.join(emails_dir, "compiled", template)
html_message = loader.render_to_string(compiled_template_prefix + '.html', context)
return (html_message, message, subject)
if not language and to_user_ids is not None:
language = to_users[0].default_language
if language:
with override_language(language):
# Make sure that we render the email using the target's native language
(html_message, message, subject) = render_templates()
else:
(html_message, message, subject) = render_templates()
logger.warning("Missing language for email template '{}'".format(template_prefix))
if from_name is None: if from_name is None:
from_name = "Zulip" from_name = "Zulip"
@@ -94,9 +109,10 @@ class EmailNotDeliveredException(Exception):
def send_email(template_prefix: str, to_user_ids: Optional[List[int]]=None, def send_email(template_prefix: str, to_user_ids: Optional[List[int]]=None,
to_emails: Optional[List[str]]=None, from_name: Optional[str]=None, to_emails: Optional[List[str]]=None, from_name: Optional[str]=None,
from_address: Optional[str]=None, reply_to_email: Optional[str]=None, from_address: Optional[str]=None, reply_to_email: Optional[str]=None,
context: Dict[str, Any]={}) -> None: language: Optional[str]=None, context: Dict[str, Any]={}) -> None:
mail = build_email(template_prefix, to_user_ids=to_user_ids, to_emails=to_emails, from_name=from_name, mail = build_email(template_prefix, to_user_ids=to_user_ids, to_emails=to_emails,
from_address=from_address, reply_to_email=reply_to_email, context=context) from_name=from_name, from_address=from_address,
reply_to_email=reply_to_email, language=language, context=context)
template = template_prefix.split("/")[-1] template = template_prefix.split("/")[-1]
logger.info("Sending %s email to %s" % (template, mail.to)) logger.info("Sending %s email to %s" % (template, mail.to))

View File

@@ -8,13 +8,48 @@ from django.test import TestCase
from django.utils import translation from django.utils import translation
from django.conf import settings from django.conf import settings
from django.http import HttpResponse from django.http import HttpResponse
from django.core import mail
from http.cookies import SimpleCookie from http.cookies import SimpleCookie
from zerver.lib.test_classes import ( from zerver.lib.test_classes import (
ZulipTestCase, ZulipTestCase,
) )
from zerver.management.commands import makemessages from zerver.management.commands import makemessages
from zerver.lib.notifications import enqueue_welcome_emails
from django.utils.timezone import now as timezone_now
class EmailTranslationTestCase(ZulipTestCase):
def test_email_translation(self) -> None:
def check_translation(phrase: str, request_type: str, *args: Any, **kwargs: Any) -> None:
if request_type == "post":
self.client_post(*args, **kwargs)
elif request_type == "patch":
self.client_patch(*args, **kwargs)
email_message = mail.outbox[0]
self.assertIn(phrase, email_message.body)
for i in range(len(mail.outbox)):
mail.outbox.pop()
hamlet = self.example_user("hamlet")
hamlet.default_language = "de"
hamlet.save()
realm = hamlet.realm
realm.default_language = "de"
realm.save()
self.login(hamlet.email)
check_translation("Viele Grüße", "patch", "/json/settings", {"email": "hamlets-new@zulip.com"})
check_translation("Felicidades", "post", "/accounts/home/", {"email": "new-email@zulip.com"}, HTTP_ACCEPT_LANGUAGE="pt")
check_translation("Danke, dass Du", "post", '/accounts/find/', {'emails': hamlet.email})
check_translation("Viele Grüße", "post", "/json/invites", {"invitee_emails": "new-email@zulip.com", "stream": ["Denmark"]})
with self.settings(DEVELOPMENT_LOG_EMAILS=True):
enqueue_welcome_emails(hamlet)
check_translation("Viele Grüße", "")
class TranslationTestCase(ZulipTestCase): class TranslationTestCase(ZulipTestCase):
""" """

View File

@@ -359,10 +359,10 @@ def prepare_activation_url(email: str, request: HttpRequest,
request.session['confirmation_key'] = {'confirmation_key': activation_url.split('/')[-1]} request.session['confirmation_key'] = {'confirmation_key': activation_url.split('/')[-1]}
return activation_url return activation_url
def send_confirm_registration_email(email: str, activation_url: str) -> None: def send_confirm_registration_email(email: str, activation_url: str, language: str) -> None:
send_email('zerver/emails/confirm_registration', to_emails=[email], send_email('zerver/emails/confirm_registration', to_emails=[email],
from_address=FromAddress.tokenized_no_reply_address(), from_address=FromAddress.tokenized_no_reply_address(),
context={'activate_url': activation_url}) language=language, context={'activate_url': activation_url})
def redirect_to_email_login_url(email: str) -> HttpResponseRedirect: def redirect_to_email_login_url(email: str) -> HttpResponseRedirect:
login_url = reverse('django.contrib.auth.views.login') login_url = reverse('django.contrib.auth.views.login')
@@ -398,7 +398,7 @@ def create_realm(request: HttpRequest, creation_key: Optional[str]=None) -> Http
return HttpResponseRedirect(activation_url) return HttpResponseRedirect(activation_url)
try: try:
send_confirm_registration_email(email, activation_url) send_confirm_registration_email(email, activation_url, request.LANGUAGE_CODE)
except smtplib.SMTPException as e: except smtplib.SMTPException as e:
logging.error('Error in create_realm: %s' % (str(e),)) logging.error('Error in create_realm: %s' % (str(e),))
return HttpResponseRedirect("/config-error/smtp") return HttpResponseRedirect("/config-error/smtp")
@@ -439,7 +439,7 @@ def accounts_home(request: HttpRequest, multiuse_object: Optional[MultiuseInvite
email = form.cleaned_data['email'] email = form.cleaned_data['email']
activation_url = prepare_activation_url(email, request, streams=streams_to_subscribe) activation_url = prepare_activation_url(email, request, streams=streams_to_subscribe)
try: try:
send_confirm_registration_email(email, activation_url) send_confirm_registration_email(email, activation_url, request.LANGUAGE_CODE)
except smtplib.SMTPException as e: except smtplib.SMTPException as e:
logging.error('Error in accounts_home: %s' % (str(e),)) logging.error('Error in accounts_home: %s' % (str(e),))
return HttpResponseRedirect("/config-error/smtp") return HttpResponseRedirect("/config-error/smtp")

View File

@@ -46,7 +46,7 @@ def confirm_email_change(request: HttpRequest, confirmation_key: str) -> HttpRes
context = {'realm_name': user_profile.realm.name, 'new_email': new_email} context = {'realm_name': user_profile.realm.name, 'new_email': new_email}
send_email('zerver/emails/notify_change_in_email', to_emails=[old_email], send_email('zerver/emails/notify_change_in_email', to_emails=[old_email],
from_name="Zulip Account Security", from_address=FromAddress.SUPPORT, from_name="Zulip Account Security", from_address=FromAddress.SUPPORT,
context=context) language=user_profile.default_language, context=context)
ctx = { ctx = {
'new_email': new_email, 'new_email': new_email,