import hashlib import shutil import subprocess from argparse import ArgumentParser from typing import Any, Dict, List from zerver.lib.management import CommandError, ZulipBaseCommand from zerver.lib.send_email import FromAddress, send_email from zerver.models import UserProfile from zerver.templatetags.app_filters import render_markdown_path def send_custom_email(users: List[UserProfile], options: Dict[str, Any]) -> None: """ Can be used directly with from a management shell with send_custom_email(user_profile_list, dict( markdown_template_path="/path/to/markdown/file.md", subject="Email Subject", from_name="Sender Name") ) """ with open(options["markdown_template_path"], "r") as f: email_template_hash = hashlib.sha256(f.read().encode('utf-8')).hexdigest()[0:32] email_id = "zerver/emails/custom_email_%s" % (email_template_hash,) markdown_email_base_template_path = "templates/zerver/emails/custom_email_base.pre.html" html_source_template_path = "templates/%s.source.html" % (email_id,) plain_text_template_path = "templates/%s.txt" % (email_id,) subject_path = "templates/%s.subject.txt" % (email_id,) # First, we render the markdown input file just like our # user-facing docs with render_markdown_path. shutil.copyfile(options['markdown_template_path'], plain_text_template_path) rendered_input = render_markdown_path(plain_text_template_path.replace("templates/", "")) # And then extend it with our standard email headers. with open(html_source_template_path, "w") as f: with open(markdown_email_base_template_path, "r") as base_template: # Note that we're doing a hacky non-Jinja2 substitution here; # we do this because the normal render_markdown_path ordering # doesn't commute properly with inline-email-css. f.write(base_template.read().replace('{{ rendered_input }}', rendered_input)) with open(subject_path, "w") as f: f.write(options["subject"]) # Then, we compile the email template using inline-email-css to # add our standard styling to the paragraph tags (etc.). # # TODO: Ideally, we'd just refactor inline-email-css to # compile this one template, not all of them. subprocess.check_call(["./scripts/setup/inline-email-css"]) # Finally, we send the actual emails. for user_profile in users: context = { 'realm_uri': user_profile.realm.uri, 'realm_name': user_profile.realm.name, } send_email(email_id, to_user_ids=[user_profile.id], from_address=FromAddress.SUPPORT, reply_to_email=options.get("reply_to"), from_name=options["from_name"], context=context) class Command(ZulipBaseCommand): help = """Send email to specified email address.""" def add_arguments(self, parser: ArgumentParser) -> None: parser.add_argument('--entire-server', action="store_true", default=False, help="Send to every user on the server. ") parser.add_argument('--markdown-template-path', '--path', dest='markdown_template_path', required=True, type=str, help='Path to a markdown-format body for the email') parser.add_argument('--subject', required=True, type=str, help='Subject line for the email') parser.add_argument('--from-name', required=True, type=str, help='From line for the email') parser.add_argument('--reply-to', type=str, help='Optional reply-to line for the email') self.add_user_list_args(parser, help="Email addresses of user(s) to send emails to.", all_users_help="Send to every user on the realm.") self.add_realm_args(parser) def handle(self, *args: Any, **options: str) -> None: if options["entire_server"]: users = UserProfile.objects.filter(is_active=True, is_bot=False, is_mirror_dummy=False) else: realm = self.get_realm(options) try: users = self.get_users(options, realm, is_bot=False) except CommandError as error: if str(error) == "You have to pass either -u/--users or -a/--all-users.": raise CommandError("You have to pass -u/--users or -a/--all-users or --entire-server.") raise error send_custom_email(users, options)