emails: Refactor send_email functions to take both a sender name and address.

This will allow for customized senders for emails, e.g. 'Zulip Digest' for
digest emails and 'Zulip Missed Messages' for missed message emails.

Also:
* Converts the sender name to always be "Zulip", if the from_email used to
  be settings.NOREPLY_EMAIL_ADDRESS or settings.ZULIP_ADMINISTRATOR.

* Changes the default value of settings.NOREPLY_EMAIL_ADDRESS in the
  prod_setting_template to no longer have a display name. The only use of
  that display name was in the email pathway.
This commit is contained in:
James Rowan
2017-06-26 13:43:32 -04:00
committed by Tim Abbott
parent a8af0b6d91
commit 368bd66d8b
9 changed files with 72 additions and 36 deletions

View File

@@ -640,7 +640,7 @@ def do_start_email_change_process(user_profile, new_email):
activation_url = EmailChangeConfirmation.objects.get_link_for_object(obj, host=user_profile.realm.host)
context = {'realm': user_profile.realm, 'old_email': old_email, 'new_email': new_email,
'activate_url': activation_url}
send_email('zerver/emails/confirm_new_email', new_email, from_email=settings.ZULIP_ADMINISTRATOR,
send_email('zerver/emails/confirm_new_email', new_email, from_address=settings.ZULIP_ADMINISTRATOR.split()[-1],
context=context)
def compute_irc_user_fullname(email):
@@ -3054,7 +3054,7 @@ def do_send_confirmation_email(invitee, referrer, body):
"""
activation_url = Confirmation.objects.get_link_for_object(invitee, host=referrer.realm.host)
context = {'referrer': referrer, 'custom_body': body, 'activate_url': activation_url}
send_email('zerver/emails/invitation', invitee.email, from_email=settings.ZULIP_ADMINISTRATOR,
send_email('zerver/emails/invitation', invitee.email, from_address=settings.ZULIP_ADMINISTRATOR.split()[-1],
context=context)
def is_inactive(email):

View File

@@ -309,8 +309,7 @@ def do_send_missedmessage_events_reply_in_zulip(user_profile, missed_messages, m
'realm_str': user_profile.realm.name,
})
from_email = None
from_name, from_address = None, None
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.
@@ -318,16 +317,16 @@ def do_send_missedmessage_events_reply_in_zulip(user_profile, missed_messages, m
# record for the domain, or there will be spam/deliverability
# problems.
sender = senders[0]
from_email = '"%s" <%s>' % (sender.full_name, sender.email)
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_email': display_email(user_profile),
'from_email': from_email,
'from_name': from_name,
'from_address': from_address,
'reply_to_email': address,
'context': context}
queue_json_publish("missedmessage_email_senders", email_dict, send_email_from_dict)
@@ -395,10 +394,11 @@ def enqueue_welcome_emails(email, name):
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
from_name = settings.WELCOME_EMAIL_SENDER['name']
from_address = settings.WELCOME_EMAIL_SENDER['email']
else:
from_email = settings.ZULIP_ADMINISTRATOR
from_name = None
from_address = settings.ZULIP_ADMINISTRATOR.split()[-1]
user_profile = get_user_profile_by_email(email)
unsubscribe_link = one_click_unsubscribe_link(user_profile, "welcome")
@@ -407,11 +407,11 @@ def enqueue_welcome_emails(email, name):
'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))
"zerver/emails/followup_day1", '%s <%s>' % (name, email), from_name=from_name,
from_address=from_address, 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))
"zerver/emails/followup_day2", '%s <%s>' % (name, email), from_name=from_name,
from_address=from_address, context=context, delay=datetime.timedelta(days=1))
def convert_html_to_markdown(html):
# type: (Text) -> Text

View File

@@ -5,7 +5,7 @@ from django.utils.timezone import now as timezone_now
from zerver.models import UserProfile, ScheduledJob
import datetime
from email.utils import parseaddr
from email.utils import parseaddr, formataddr
import ujson
from typing import Any, Dict, Iterable, List, Mapping, Optional, Text
@@ -17,8 +17,9 @@ def display_email(user):
return user.email
# Intended only for test code
def build_email(template_prefix, to_email, from_email=None, reply_to_email=None, context={}):
# type: (str, Text, Optional[Text], Optional[Text], Dict[str, Any]) -> EmailMultiAlternatives
def build_email(template_prefix, to_email, from_name=None, from_address=None,
reply_to_email=None, context={}):
# type: (str, Text, Optional[Text], Optional[Text], Optional[Text], Dict[str, Any]) -> EmailMultiAlternatives
context.update({
'support_email': settings.ZULIP_ADMINISTRATOR,
'verbose_support_offers': settings.VERBOSE_SUPPORT_OFFERS,
@@ -28,8 +29,12 @@ def build_email(template_prefix, to_email, from_email=None, reply_to_email=None,
message = loader.render_to_string(template_prefix + '.txt',
context=context, using='Jinja2_plaintext')
html_message = loader.render_to_string(template_prefix + '.html', context)
if from_email is None:
from_email = settings.NOREPLY_EMAIL_ADDRESS
if from_name is None:
from_name = "Zulip"
if from_address is None:
from_address = parseaddr(settings.NOREPLY_EMAIL_ADDRESS)[1]
from_email = formataddr((from_name, from_address))
reply_to = None
if reply_to_email is not None:
reply_to = [reply_to_email]
@@ -39,15 +44,16 @@ def build_email(template_prefix, to_email, from_email=None, reply_to_email=None,
mail.attach_alternative(html_message, 'text/html')
return mail
def send_email(template_prefix, to_email, from_email=None, reply_to_email=None, context={}):
# type: (str, Text, Optional[Text], Optional[Text], Dict[str, Any]) -> bool
mail = build_email(template_prefix, to_email, from_email=from_email,
reply_to_email=reply_to_email, context=context)
def send_email(template_prefix, to_email, from_name=None, from_address=None, reply_to_email=None, context={}):
# type: (str, Text, Optional[Text], Optional[Text], Optional[Text], Dict[str, Any]) -> bool
mail = build_email(template_prefix, to_email, from_name=from_name,
from_address=from_address, reply_to_email=reply_to_email, context=context)
return mail.send() > 0
def send_email_to_user(template_prefix, user, from_email=None, context={}):
# type: (str, UserProfile, Optional[Text], Dict[str, Text]) -> bool
return send_email(template_prefix, display_email(user), from_email=from_email, context=context)
def send_email_to_user(template_prefix, user, from_name=None, from_address=None, context={}):
# type: (str, UserProfile, Optional[Text], Optional[Text], Dict[str, Text]) -> bool
return send_email(template_prefix, display_email(user), from_name=from_name,
from_address=from_address, context=context)
# Returns None instead of bool so that the type signature matches the third
# argument of zerver.lib.queue.queue_json_publish
@@ -55,11 +61,11 @@ def send_email_from_dict(email_dict):
# type: (Mapping[str, Any]) -> None
send_email(**dict(email_dict))
def send_future_email(template_prefix, to_email, from_email=None, context={},
def send_future_email(template_prefix, to_email, from_name=None, from_address=None, context={},
delay=datetime.timedelta(0)):
# type: (str, Text, Optional[Text], Dict[str, Any], datetime.timedelta) -> None
email_fields = {'template_prefix': template_prefix, 'to_email': to_email, 'from_email': from_email,
'context': context}
# type: (str, Text, Optional[Text], Optional[Text], Dict[str, Any], datetime.timedelta) -> None
email_fields = {'template_prefix': template_prefix, 'to_email': to_email, 'from_name': from_name,
'from_address': from_address, 'context': context}
ScheduledJob.objects.create(type=ScheduledJob.EMAIL, filter_string=parseaddr(to_email)[1],
data=ujson.dumps(email_fields),
scheduled_timestamp=timezone_now() + delay)

View File

@@ -0,0 +1,27 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-05-10 05:59
from __future__ import unicode_literals
from django.db import migrations
from django.db.backends.postgresql_psycopg2.schema import DatabaseSchemaEditor
from django.db.migrations.state import StateApps
def delete_old_scheduled_jobs(apps, schema_editor):
# type: (StateApps, DatabaseSchemaEditor) -> None
"""Delete any old scheduled jobs, to handle changes in the format of
send_email. Ideally, we'd translate the jobs, but it's not really
worth the development effort to save a few invitation reminders
and day2 followup emails.
"""
ScheduledJob = apps.get_model('zerver', 'ScheduledJob')
ScheduledJob.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('zerver', '0086_realm_alter_default_org_type'),
]
operations = [
migrations.RunPython(delete_old_scheduled_jobs),
]

View File

@@ -743,7 +743,7 @@ so we didn't send them an invitation. We did send invitations to everyone else!"
with self.settings(EMAIL_BACKEND='django.core.mail.backends.console.EmailBackend'):
send_future_email(
"zerver/emails/invitation_reminder", data["email"],
from_email=settings.ZULIP_ADMINISTRATOR, context=context)
from_address=settings.ZULIP_ADMINISTRATOR.split()[-1], context=context)
email_jobs_to_deliver = ScheduledJob.objects.filter(
type=ScheduledJob.EMAIL,
scheduled_timestamp__lte=timezone_now())

View File

@@ -313,7 +313,7 @@ def send_registration_completion_email(email, request, realm_creation=False):
"""
prereg_user = create_preregistration_user(email, request, realm_creation)
activation_url = Confirmation.objects.get_link_for_object(prereg_user, host=request.get_host())
send_email('zerver/emails/confirm_registration', email, from_email=settings.ZULIP_ADMINISTRATOR,
send_email('zerver/emails/confirm_registration', email, from_address=settings.ZULIP_ADMINISTRATOR.split()[-1],
context={'activate_url': activation_url})
if settings.DEVELOPMENT and realm_creation:
request.session['confirmation_key'] = {'confirmation_key': activation_url.split('/')[-1]}

View File

@@ -52,7 +52,7 @@ def confirm_email_change(request, confirmation_key):
'new_email': new_email,
}
send_email('zerver/emails/notify_change_in_email', old_email,
from_email=settings.ZULIP_ADMINISTRATOR, context=context)
from_address=settings.ZULIP_ADMINISTRATOR.split()[-1], context=context)
ctx = {
'confirmed': confirmed,

View File

@@ -180,7 +180,7 @@ class ConfirmationEmailWorker(QueueProcessingWorker):
send_future_email(
"zerver/emails/invitation_reminder",
data["email"],
from_email=settings.ZULIP_ADMINISTRATOR,
from_address=settings.ZULIP_ADMINISTRATOR.split()[-1],
context=context,
delay=datetime.timedelta(days=2))

View File

@@ -40,6 +40,8 @@ ALLOWED_HOSTS = [EXTERNAL_HOST.split(":")[0]]
# appear on 404 pages, is used as the sender's address for many automated
# emails, and is advertised as a support address. An email address like
# support@example.com is totally reasonable, as is admin@example.com.
# Do not put a display name; e.g. 'support@example.com', not
# 'Zulip Support <support@example.com>'.
ZULIP_ADMINISTRATOR = 'zulip-admin@example.com'
# Configure the outgoing SMTP server below. You will need working
@@ -76,8 +78,9 @@ EMAIL_PORT = 587
EMAIL_USE_TLS = True
# The noreply address to be used as the sender for certain generated emails.
# Messages sent to this address could contain sensitive user data and should
# not be delivered anywhere. (e.g. "Zulip <noreply@example.com>")
NOREPLY_EMAIL_ADDRESS = "Zulip <noreply@" + EXTERNAL_HOST.split(":")[0] + ">"
# not be delivered anywhere. Do not put a display name;
# e.g. 'noreply@example.com', not 'Zulip <noreply@example.com>'.
NOREPLY_EMAIL_ADDRESS = "noreply@" + EXTERNAL_HOST.split(":")[0]
## OPTIONAL SETTINGS