mirror of
https://github.com/zulip/zulip.git
synced 2025-11-04 14:03:30 +00:00
mypy should be able to figure this out given the xor just above, but it's not surprising that it doesn't.
158 lines
7.2 KiB
Python
158 lines
7.2 KiB
Python
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.template.exceptions import TemplateDoesNotExist
|
|
from zerver.models import UserProfile, ScheduledEmail, get_user_profile_by_id, \
|
|
EMAIL_TYPES, Realm
|
|
|
|
import datetime
|
|
from email.utils import parseaddr, formataddr
|
|
import logging
|
|
import ujson
|
|
|
|
import os
|
|
from typing import Any, Dict, Iterable, List, Mapping, Optional
|
|
|
|
from zerver.lib.logging_util import log_to_file
|
|
from confirmation.models import generate_key
|
|
|
|
## Logging setup ##
|
|
|
|
logger = logging.getLogger('zulip.send_email')
|
|
log_to_file(logger, settings.EMAIL_LOG_PATH)
|
|
|
|
class FromAddress:
|
|
SUPPORT = parseaddr(settings.ZULIP_ADMINISTRATOR)[1]
|
|
NOREPLY = parseaddr(settings.NOREPLY_EMAIL_ADDRESS)[1]
|
|
|
|
# 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
|
|
|
|
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:
|
|
# 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:
|
|
to_users = [get_user_profile_by_id(to_user_id) for to_user_id in to_user_ids]
|
|
# Change to formataddr((to_user.full_name, to_user.email)) once
|
|
# https://github.com/zulip/zulip/issues/4676 is resolved
|
|
to_emails = [to_user.delivery_email for to_user in to_users]
|
|
|
|
if context is None:
|
|
context = {}
|
|
|
|
context.update({
|
|
'support_email': FromAddress.SUPPORT,
|
|
'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)
|
|
|
|
if from_name is None:
|
|
from_name = "Zulip"
|
|
if from_address is None:
|
|
from_address = FromAddress.NOREPLY
|
|
from_email = formataddr((from_name, from_address))
|
|
reply_to = None
|
|
if reply_to_email is not None:
|
|
reply_to = [reply_to_email]
|
|
# 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]
|
|
|
|
mail = EmailMultiAlternatives(subject, message, from_email, to_emails, reply_to=reply_to)
|
|
if html_message is not None:
|
|
mail.attach_alternative(html_message, 'text/html')
|
|
return mail
|
|
|
|
class EmailNotDeliveredException(Exception):
|
|
pass
|
|
|
|
# When changing the arguments to this function, you may need to write a
|
|
# migration to change or remove any emails in ScheduledEmail.
|
|
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)
|
|
template = template_prefix.split("/")[-1]
|
|
logger.info("Sending %s email to %s" % (template, mail.to))
|
|
|
|
if mail.send() == 0:
|
|
logger.error("Error sending %s email to %s" % (template, mail.to))
|
|
raise EmailNotDeliveredException
|
|
|
|
def send_email_from_dict(email_dict: Mapping[str, Any]) -> None:
|
|
send_email(**dict(email_dict))
|
|
|
|
def send_future_email(template_prefix: str, realm: Realm, to_user_ids: Optional[List[int]]=None,
|
|
to_emails: Optional[List[str]]=None, from_name: Optional[str]=None,
|
|
from_address: Optional[str]=None, context: Dict[str, Any]={},
|
|
delay: datetime.timedelta=datetime.timedelta(0)) -> None:
|
|
# WARNING: Be careful when using this with multiple recipients;
|
|
# because the current ScheduledEmail model (used primarily for
|
|
# cancelling planned emails) does not support multiple recipients,
|
|
# this is only valid to use for such emails where we don't expect
|
|
# the cancellation feature to be relevant.
|
|
#
|
|
# For that reason, we currently assert that the list of
|
|
# to_user_ids/to_emails is 1 below, but in theory that could be
|
|
# changed as long as the callers are in a situation where the
|
|
# above problem is not relevant.
|
|
template_name = template_prefix.split('/')[-1]
|
|
email_fields = {'template_prefix': template_prefix, 'to_user_ids': to_user_ids, 'to_emails': to_emails,
|
|
'from_name': from_name, 'from_address': from_address, 'context': context}
|
|
|
|
if settings.DEVELOPMENT and not settings.TEST_SUITE:
|
|
send_email(template_prefix, to_user_ids=to_user_ids, to_emails=to_emails, from_name=from_name,
|
|
from_address=from_address, context=context)
|
|
# For logging the email
|
|
|
|
assert (to_user_ids is None) ^ (to_emails is None)
|
|
if to_user_ids is not None:
|
|
# The realm is redundant if we have a to_user_id; this assert just
|
|
# expresses that fact
|
|
assert(len(to_user_ids) == 1)
|
|
assert(UserProfile.objects.filter(id__in=to_user_ids, realm=realm).exists())
|
|
to_field = {'user_id': to_user_ids[0]} # type: Dict[str, Any]
|
|
else:
|
|
assert to_emails is not None
|
|
assert(len(to_emails) == 1)
|
|
to_field = {'address': parseaddr(to_emails[0])[1]}
|
|
|
|
ScheduledEmail.objects.create(
|
|
type=EMAIL_TYPES[template_name],
|
|
scheduled_timestamp=timezone_now() + delay,
|
|
realm=realm,
|
|
data=ujson.dumps(email_fields),
|
|
**to_field)
|
|
|
|
def send_email_to_admins(template_prefix: str, realm: Realm, from_name: Optional[str]=None,
|
|
from_address: Optional[str]=None, context: Dict[str, Any]={}) -> None:
|
|
admins = realm.get_admin_users()
|
|
admin_user_ids = [admin.id for admin in admins]
|
|
send_email(template_prefix, to_user_ids=admin_user_ids, from_name=from_name,
|
|
from_address=from_address, context=context)
|