mirror of
https://github.com/zulip/zulip.git
synced 2025-10-24 08:33:43 +00:00
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:
@@ -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):
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user