2017-05-02 01:15:58 +02:00
|
|
|
from django.conf import settings
|
2017-05-05 01:03:22 +02:00
|
|
|
from django.core.mail import EmailMultiAlternatives
|
2017-05-14 15:02:49 +02:00
|
|
|
from django.template import loader
|
2017-05-04 03:11:47 +02:00
|
|
|
from django.utils.timezone import now as timezone_now
|
2017-09-26 01:41:15 +02:00
|
|
|
from django.template.exceptions import TemplateDoesNotExist
|
2017-07-02 21:10:41 +02:00
|
|
|
from zerver.models import UserProfile, ScheduledEmail, get_user_profile_by_id, \
|
|
|
|
EMAIL_TYPES
|
2017-05-02 01:15:58 +02:00
|
|
|
|
2017-05-04 03:11:47 +02:00
|
|
|
import datetime
|
2017-06-26 19:43:32 +02:00
|
|
|
from email.utils import parseaddr, formataddr
|
2017-05-04 03:11:47 +02:00
|
|
|
import ujson
|
|
|
|
|
2017-09-26 01:41:15 +02:00
|
|
|
import os
|
2017-05-05 01:31:07 +02:00
|
|
|
from typing import Any, Dict, Iterable, List, Mapping, Optional, Text
|
2017-05-02 01:15:58 +02:00
|
|
|
|
2017-08-28 08:41:13 +02:00
|
|
|
from zerver.lib.logging_util import create_logger
|
|
|
|
|
|
|
|
## Logging setup ##
|
|
|
|
|
|
|
|
logger = create_logger('zulip.send_email', settings.EMAIL_LOG_PATH, 'INFO')
|
|
|
|
|
2017-06-26 19:43:32 +02:00
|
|
|
class FromAddress(object):
|
|
|
|
SUPPORT = parseaddr(settings.ZULIP_ADMINISTRATOR)[1]
|
|
|
|
NOREPLY = parseaddr(settings.NOREPLY_EMAIL_ADDRESS)[1]
|
|
|
|
|
2017-09-24 00:39:19 +02:00
|
|
|
def log_email(email, template_prefix):
|
|
|
|
# type: (EmailMultiAlternatives, str) -> None
|
|
|
|
"""Used in development to record sent emails in a nice HTML log"""
|
|
|
|
html_message = 'Missing HTML message'
|
|
|
|
if len(email.alternatives) > 0:
|
|
|
|
html_message = email.alternatives[0][0]
|
|
|
|
|
|
|
|
context = {
|
|
|
|
'template': template_prefix + ".html",
|
|
|
|
'subject': email.subject,
|
|
|
|
'from_email': email.from_email,
|
|
|
|
'recipients': email.to,
|
|
|
|
'body': email.body,
|
|
|
|
'html_message': html_message
|
|
|
|
}
|
|
|
|
|
|
|
|
new_email = loader.render_to_string('zerver/email.html', context)
|
|
|
|
|
|
|
|
# Read in the pre-existing log, so that we can add the new entry
|
|
|
|
# at the top.
|
|
|
|
try:
|
|
|
|
with open(settings.EMAIL_CONTENT_LOG_PATH, "r") as f:
|
|
|
|
previous_emails = f.read()
|
|
|
|
except FileNotFoundError:
|
|
|
|
previous_emails = ""
|
|
|
|
|
|
|
|
with open(settings.EMAIL_CONTENT_LOG_PATH, "w+") as f:
|
|
|
|
f.write(new_email + previous_emails)
|
|
|
|
|
2017-07-11 05:01:32 +02:00
|
|
|
def build_email(template_prefix, to_user_id=None, to_email=None, from_name=None,
|
2017-08-25 18:43:53 +02:00
|
|
|
from_address=None, reply_to_email=None, context=None):
|
|
|
|
# type: (str, Optional[int], Optional[Text], Optional[Text], Optional[Text], Optional[Text], Optional[Dict[str, Any]]) -> EmailMultiAlternatives
|
2017-07-17 05:42:08 +02:00
|
|
|
# Callers should pass exactly one of to_user_id and to_email.
|
2017-07-11 05:01:32 +02:00
|
|
|
assert (to_user_id is None) ^ (to_email is None)
|
|
|
|
if to_user_id is not None:
|
|
|
|
to_user = get_user_profile_by_id(to_user_id)
|
2017-07-11 06:13:23 +02:00
|
|
|
# Change to formataddr((to_user.full_name, to_user.email)) once
|
|
|
|
# https://github.com/zulip/zulip/issues/4676 is resolved
|
|
|
|
to_email = to_user.email
|
2017-07-11 05:01:32 +02:00
|
|
|
|
2017-08-25 18:43:53 +02:00
|
|
|
if context is None:
|
|
|
|
context = {}
|
|
|
|
|
2017-06-10 10:16:01 +02:00
|
|
|
context.update({
|
2017-07-14 03:33:35 +02:00
|
|
|
'realm_name_in_notifications': False,
|
2017-07-02 05:27:01 +02:00
|
|
|
'support_email': FromAddress.SUPPORT,
|
2017-08-16 12:10:55 +02:00
|
|
|
'email_images_base_uri': settings.ROOT_DOMAIN_URI + '/static/images/emails',
|
2017-06-10 10:16:01 +02:00
|
|
|
})
|
2017-06-06 03:46:35 +02:00
|
|
|
subject = loader.render_to_string(template_prefix + '.subject',
|
2017-07-14 03:33:35 +02:00
|
|
|
context=context,
|
|
|
|
using='Jinja2_plaintext').strip().replace('\n', '')
|
2017-06-06 03:46:35 +02:00
|
|
|
message = loader.render_to_string(template_prefix + '.txt',
|
|
|
|
context=context, using='Jinja2_plaintext')
|
2017-09-26 01:41:15 +02:00
|
|
|
|
|
|
|
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)
|
2017-06-26 19:43:32 +02:00
|
|
|
|
|
|
|
if from_name is None:
|
|
|
|
from_name = "Zulip"
|
|
|
|
if from_address is None:
|
2017-06-26 19:43:32 +02:00
|
|
|
from_address = FromAddress.NOREPLY
|
2017-06-26 19:43:32 +02:00
|
|
|
from_email = formataddr((from_name, from_address))
|
2017-05-05 01:03:22 +02:00
|
|
|
reply_to = None
|
|
|
|
if reply_to_email is not None:
|
|
|
|
reply_to = [reply_to_email]
|
2017-07-05 08:28:43 +02:00
|
|
|
# Remove the from_name in the reply-to for noreply emails, so that users
|
|
|
|
# see "noreply@..." rather than "Zulip" or whatever the from_name is
|
|
|
|
# when they reply in their email client.
|
|
|
|
elif from_address == FromAddress.NOREPLY:
|
|
|
|
reply_to = [FromAddress.NOREPLY]
|
2017-05-05 01:03:22 +02:00
|
|
|
|
|
|
|
mail = EmailMultiAlternatives(subject, message, from_email, [to_email], reply_to=reply_to)
|
|
|
|
if html_message is not None:
|
|
|
|
mail.attach_alternative(html_message, 'text/html')
|
2017-06-10 06:19:32 +02:00
|
|
|
return mail
|
|
|
|
|
2017-07-12 01:05:59 +02:00
|
|
|
class EmailNotDeliveredException(Exception):
|
|
|
|
pass
|
|
|
|
|
2017-07-02 21:10:41 +02:00
|
|
|
# When changing the arguments to this function, you may need to write a
|
|
|
|
# migration to change or remove any emails in ScheduledEmail.
|
2017-07-11 05:01:32 +02:00
|
|
|
def send_email(template_prefix, to_user_id=None, to_email=None, from_name=None,
|
|
|
|
from_address=None, reply_to_email=None, context={}):
|
2017-07-12 01:05:59 +02:00
|
|
|
# type: (str, Optional[int], Optional[Text], Optional[Text], Optional[Text], Optional[Text], Dict[str, Any]) -> None
|
2017-07-11 05:01:32 +02:00
|
|
|
mail = build_email(template_prefix, to_user_id=to_user_id, to_email=to_email, from_name=from_name,
|
2017-06-26 19:43:32 +02:00
|
|
|
from_address=from_address, reply_to_email=reply_to_email, context=context)
|
2017-08-28 08:41:13 +02:00
|
|
|
template = template_prefix.split("/")[-1]
|
|
|
|
logger.info("Sending %s email to %s" % (template, mail.to))
|
|
|
|
|
2017-09-24 00:39:19 +02:00
|
|
|
if settings.DEVELOPMENT:
|
|
|
|
log_email(mail, template_prefix)
|
|
|
|
|
2017-07-12 01:05:59 +02:00
|
|
|
if mail.send() == 0:
|
2017-08-28 08:41:13 +02:00
|
|
|
logger.error("Error sending %s email to %s" % (template, mail.to))
|
2017-07-12 01:05:59 +02:00
|
|
|
raise EmailNotDeliveredException
|
2017-05-02 01:15:58 +02:00
|
|
|
|
2017-05-05 01:31:07 +02:00
|
|
|
def send_email_from_dict(email_dict):
|
|
|
|
# type: (Mapping[str, Any]) -> None
|
|
|
|
send_email(**dict(email_dict))
|
|
|
|
|
2017-07-11 05:34:32 +02:00
|
|
|
def send_future_email(template_prefix, to_user_id=None, to_email=None, from_name=None,
|
|
|
|
from_address=None, context={}, delay=datetime.timedelta(0)):
|
|
|
|
# type: (str, Optional[int], Optional[Text], Optional[Text], Optional[Text], Dict[str, Any], datetime.timedelta) -> None
|
2017-07-15 03:06:04 +02:00
|
|
|
template_name = template_prefix.split('/')[-1]
|
|
|
|
email_fields = {'template_prefix': template_prefix, 'to_user_id': to_user_id, 'to_email': to_email,
|
|
|
|
'from_name': from_name, 'from_address': from_address, 'context': context}
|
|
|
|
|
2017-09-24 00:39:19 +02:00
|
|
|
if settings.DEVELOPMENT:
|
|
|
|
mail = build_email(template_prefix, to_user_id=to_user_id, to_email=to_email, from_name=from_name,
|
|
|
|
from_address=from_address, context=context)
|
|
|
|
# Technically, this will be called. But we currently don't
|
|
|
|
# run the `manage.py deliver_email` backend job in the
|
|
|
|
# development environment.
|
|
|
|
log_email(mail, template_prefix)
|
|
|
|
|
2017-07-11 05:34:32 +02:00
|
|
|
assert (to_user_id is None) ^ (to_email is None)
|
2017-07-12 02:26:10 +02:00
|
|
|
if to_user_id is not None:
|
|
|
|
to_field = {'user_id': to_user_id} # type: Dict[str, Any]
|
|
|
|
else:
|
|
|
|
to_field = {'address': parseaddr(to_email)[1]}
|
2017-07-15 03:06:04 +02:00
|
|
|
|
2017-07-02 21:10:41 +02:00
|
|
|
ScheduledEmail.objects.create(
|
|
|
|
type=EMAIL_TYPES[template_name],
|
|
|
|
scheduled_timestamp=timezone_now() + delay,
|
2017-07-12 02:26:10 +02:00
|
|
|
data=ujson.dumps(email_fields),
|
|
|
|
**to_field)
|