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, \
|
2017-12-05 03:19:48 +01:00
|
|
|
EMAIL_TYPES, Realm
|
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-12-13 01:45:57 +01:00
|
|
|
import logging
|
2017-05-04 03:11:47 +02:00
|
|
|
import ujson
|
|
|
|
|
2017-09-26 01:41:15 +02:00
|
|
|
import os
|
2018-05-11 01:40:23 +02:00
|
|
|
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
2017-05-02 01:15:58 +02:00
|
|
|
|
2017-12-13 01:45:57 +01:00
|
|
|
from zerver.lib.logging_util import log_to_file
|
2018-06-08 11:06:18 +02:00
|
|
|
from confirmation.models import generate_key
|
2017-08-28 08:41:13 +02:00
|
|
|
|
|
|
|
## Logging setup ##
|
|
|
|
|
2017-12-13 01:45:57 +01:00
|
|
|
logger = logging.getLogger('zulip.send_email')
|
|
|
|
log_to_file(logger, settings.EMAIL_LOG_PATH)
|
2017-08-28 08:41:13 +02:00
|
|
|
|
2017-11-05 11:37:41 +01:00
|
|
|
class FromAddress:
|
2017-06-26 19:43:32 +02:00
|
|
|
SUPPORT = parseaddr(settings.ZULIP_ADMINISTRATOR)[1]
|
|
|
|
NOREPLY = parseaddr(settings.NOREPLY_EMAIL_ADDRESS)[1]
|
|
|
|
|
2018-06-08 11:06:18 +02:00
|
|
|
# Generates an unpredictable noreply address.
|
|
|
|
@staticmethod
|
|
|
|
def tokenized_no_reply_address() -> str:
|
|
|
|
if settings.ADD_TOKENS_TO_NOREPLY_ADDRESS:
|
|
|
|
return parseaddr(settings.TOKENIZED_NOREPLY_EMAIL_ADDRESS)[1].format(token=generate_key())
|
|
|
|
return FromAddress.NOREPLY
|
|
|
|
|
2017-12-24 16:03:33 +01:00
|
|
|
def build_email(template_prefix: str, to_user_id: Optional[int]=None,
|
2018-05-11 01:40:23 +02:00
|
|
|
to_email: Optional[str]=None, from_name: Optional[str]=None,
|
|
|
|
from_address: Optional[str]=None, reply_to_email: Optional[str]=None,
|
2017-12-24 16:03:33 +01:00
|
|
|
context: Optional[Dict[str, Any]]=None) -> 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
|
models: Create delivery_email field in userprofile.
This commit creates a new field called delivery_email. For now, it is
exactly the same as email upon user profile creation and should stay
that way even when email is changed, and is used only for sending
outgoing email from Zulip.
The purpose of this field is to support an upcoming option where the
existing `email` field in Zulip becomes effectively the user's
"display email" address, as part of making it possible for users
actual email addresses (that can receive email, stored in the
delivery_email field) to not be available to other non-administrator
users in the organization.
Because the `email` field is used in numerous places in display code,
in the API, and in database queries, the shortest path to implementing
this "private email" feature is to keep "email" as-is in those parts
of the codebase, and just set the existing "email" ("display email")
model field to be something generated like
"username@zulip.example.com" for display purposes.
Eventually, we'll want to do further refactoring, either in the form
of having both `display_email` and `delivery_email` as fields, or
renaming "email" to "username".
2018-07-05 20:08:30 +02:00
|
|
|
to_email = to_user.delivery_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-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-10-19 04:09:53 +02:00
|
|
|
'physical_address': settings.PHYSICAL_ADDRESS,
|
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.
|
2018-05-11 01:40:23 +02:00
|
|
|
def send_email(template_prefix: str, to_user_id: Optional[int]=None, to_email: Optional[str]=None,
|
|
|
|
from_name: Optional[str]=None, from_address: Optional[str]=None,
|
|
|
|
reply_to_email: Optional[str]=None, context: 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-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-11-05 11:15:10 +01:00
|
|
|
def send_email_from_dict(email_dict: Mapping[str, Any]) -> None:
|
2017-05-05 01:31:07 +02:00
|
|
|
send_email(**dict(email_dict))
|
|
|
|
|
2017-12-24 16:03:33 +01:00
|
|
|
def send_future_email(template_prefix: str, realm: Realm, to_user_id: Optional[int]=None,
|
2018-05-11 01:40:23 +02:00
|
|
|
to_email: Optional[str]=None, from_name: Optional[str]=None,
|
|
|
|
from_address: Optional[str]=None, context: Dict[str, Any]={},
|
2017-12-24 16:03:33 +01:00
|
|
|
delay: datetime.timedelta=datetime.timedelta(0)) -> 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-10-03 02:04:32 +02:00
|
|
|
if settings.DEVELOPMENT and not settings.TEST_SUITE:
|
|
|
|
send_email(template_prefix, to_user_id=to_user_id, to_email=to_email, from_name=from_name,
|
|
|
|
from_address=from_address, context=context)
|
|
|
|
# For logging the email
|
2017-09-24 00:39:19 +02:00
|
|
|
|
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:
|
2017-12-05 03:19:48 +01:00
|
|
|
# The realm is redundant if we have a to_user_id; this assert just
|
|
|
|
# expresses that fact
|
|
|
|
assert(UserProfile.objects.filter(id=to_user_id, realm=realm).exists())
|
2017-07-12 02:26:10 +02:00
|
|
|
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-12-05 03:19:48 +01:00
|
|
|
realm=realm,
|
2017-07-12 02:26:10 +02:00
|
|
|
data=ujson.dumps(email_fields),
|
|
|
|
**to_field)
|