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],
from_name="Zulip Account Security",
from_address=FromAddress.tokenized_no_reply_address(),
language=request.LANGUAGE_CODE,
context=context)
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],
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:
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}
from_name = "%s (via Zulip)" % (referrer.full_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:
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.template import loader
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 zerver.models import UserProfile, ScheduledEmail, get_user_profile_by_id, \
EMAIL_TYPES, Realm
@@ -12,7 +13,7 @@ import logging
import ujson
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 confirmation.models import generate_key
@@ -36,7 +37,8 @@ class FromAddress:
def build_email(template_prefix: str, to_user_ids: Optional[List[int]]=None,
to_emails: Optional[List[str]]=None, from_name: 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.
assert (to_user_ids is None) ^ (to_emails is 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',
'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:
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)
def render_templates() -> Tuple[str, str, str]:
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:
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:
from_name = "Zulip"
@@ -94,9 +109,10 @@ class EmailNotDeliveredException(Exception):
def send_email(template_prefix: str, to_user_ids: Optional[List[int]]=None,
to_emails: Optional[List[str]]=None, from_name: Optional[str]=None,
from_address: Optional[str]=None, reply_to_email: 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,
from_address=from_address, reply_to_email=reply_to_email, context=context)
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, from_address=from_address,
reply_to_email=reply_to_email, language=language, context=context)
template = template_prefix.split("/")[-1]
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.conf import settings
from django.http import HttpResponse
from django.core import mail
from http.cookies import SimpleCookie
from zerver.lib.test_classes import (
ZulipTestCase,
)
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):
"""

View File

@@ -359,10 +359,10 @@ def prepare_activation_url(email: str, request: HttpRequest,
request.session['confirmation_key'] = {'confirmation_key': activation_url.split('/')[-1]}
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],
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:
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)
try:
send_confirm_registration_email(email, activation_url)
send_confirm_registration_email(email, activation_url, request.LANGUAGE_CODE)
except smtplib.SMTPException as e:
logging.error('Error in create_realm: %s' % (str(e),))
return HttpResponseRedirect("/config-error/smtp")
@@ -439,7 +439,7 @@ def accounts_home(request: HttpRequest, multiuse_object: Optional[MultiuseInvite
email = form.cleaned_data['email']
activation_url = prepare_activation_url(email, request, streams=streams_to_subscribe)
try:
send_confirm_registration_email(email, activation_url)
send_confirm_registration_email(email, activation_url, request.LANGUAGE_CODE)
except smtplib.SMTPException as e:
logging.error('Error in accounts_home: %s' % (str(e),))
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}
send_email('zerver/emails/notify_change_in_email', to_emails=[old_email],
from_name="Zulip Account Security", from_address=FromAddress.SUPPORT,
context=context)
language=user_profile.default_language, context=context)
ctx = {
'new_email': new_email,