mirror of
https://github.com/zulip/zulip.git
synced 2025-11-03 05:23:35 +00:00
send_custom_email: Split out the sending to remote servers.
This commit is contained in:
committed by
Tim Abbott
parent
62e6b10ecd
commit
791d66fe28
@@ -8,7 +8,7 @@ from email.headerregistry import Address
|
||||
from email.parser import Parser
|
||||
from email.policy import default
|
||||
from email.utils import formataddr, parseaddr
|
||||
from typing import Any, Callable, Dict, List, Mapping, Optional, Sequence, Tuple, Union
|
||||
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Union
|
||||
|
||||
import backoff
|
||||
import css_inline
|
||||
@@ -32,6 +32,9 @@ from zerver.lib.logging_util import log_to_file
|
||||
from zerver.models import EMAIL_TYPES, Realm, ScheduledEmail, UserProfile, get_user_profile_by_id
|
||||
from zproject.email_backends import EmailLogBackEnd, get_forward_address
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
from zilencer.models import RemoteZulipServer
|
||||
|
||||
MAX_CONNECTION_TRIES = 3
|
||||
|
||||
## Logging setup ##
|
||||
@@ -507,26 +510,15 @@ def get_header(option: Optional[str], header: Optional[str], name: str) -> str:
|
||||
return str(option or header)
|
||||
|
||||
|
||||
def send_custom_email(
|
||||
users: QuerySet[UserProfile],
|
||||
*,
|
||||
target_emails: Sequence[str] = [],
|
||||
def custom_email_sender(
|
||||
markdown_template_path: str,
|
||||
dry_run: bool,
|
||||
options: Dict[str, str],
|
||||
add_context: Optional[Callable[[Dict[str, object], UserProfile], None]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Helper for `manage.py send_custom_email`.
|
||||
|
||||
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"]) as f:
|
||||
subject: Optional[str] = None,
|
||||
from_name: Optional[str] = None,
|
||||
reply_to: Optional[str] = None,
|
||||
**kwargs: Any,
|
||||
) -> Callable[..., None]:
|
||||
with open(markdown_template_path) as f:
|
||||
text = f.read()
|
||||
parsed_email_template = Parser(policy=default).parsestr(text)
|
||||
email_template_hash = hashlib.sha256(text.encode()).hexdigest()[0:32]
|
||||
@@ -559,9 +551,45 @@ def send_custom_email(
|
||||
f.write(base_template.read().replace("{{ rendered_input }}", rendered_input))
|
||||
|
||||
with open(subject_path, "w") as f:
|
||||
f.write(get_header(options.get("subject"), parsed_email_template.get("subject"), "subject"))
|
||||
f.write(get_header(subject, parsed_email_template.get("subject"), "subject"))
|
||||
|
||||
# Finally, we send the actual emails.
|
||||
def send_one_email(
|
||||
context: Dict[str, Any], to_user_id: Optional[int] = None, to_email: Optional[str] = None
|
||||
) -> None:
|
||||
assert to_user_id is not None or to_email is not None
|
||||
with suppress(EmailNotDeliveredError):
|
||||
send_email(
|
||||
email_id,
|
||||
to_user_ids=[to_user_id] if to_user_id is not None else None,
|
||||
to_emails=[to_email] if to_email is not None else None,
|
||||
from_address=FromAddress.SUPPORT,
|
||||
reply_to_email=reply_to,
|
||||
from_name=get_header(from_name, parsed_email_template.get("from"), "from_name"),
|
||||
context=context,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
|
||||
return send_one_email
|
||||
|
||||
|
||||
def send_custom_email(
|
||||
users: QuerySet[UserProfile],
|
||||
*,
|
||||
dry_run: bool,
|
||||
options: Dict[str, str],
|
||||
add_context: Optional[Callable[[Dict[str, object], UserProfile], None]] = None,
|
||||
) -> None:
|
||||
"""
|
||||
Helper for `manage.py send_custom_email`.
|
||||
|
||||
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")
|
||||
)
|
||||
"""
|
||||
email_sender = custom_email_sender(**options, dry_run=dry_run)
|
||||
for user_profile in users.select_related("realm").order_by("id"):
|
||||
context: Dict[str, object] = {
|
||||
"realm": user_profile.realm,
|
||||
@@ -571,35 +599,30 @@ def send_custom_email(
|
||||
}
|
||||
if add_context is not None:
|
||||
add_context(context, user_profile)
|
||||
with suppress(EmailNotDeliveredError):
|
||||
send_email(
|
||||
email_id,
|
||||
to_user_ids=[user_profile.id],
|
||||
from_address=FromAddress.SUPPORT,
|
||||
reply_to_email=options.get("reply_to"),
|
||||
from_name=get_header(
|
||||
options.get("from_name"), parsed_email_template.get("from"), "from_name"
|
||||
),
|
||||
context=context,
|
||||
dry_run=dry_run,
|
||||
)
|
||||
email_sender(
|
||||
to_user_id=user_profile.id,
|
||||
context=context,
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
break
|
||||
|
||||
# Now send emails to any recipients without a user account.
|
||||
# This code path is intended for rare RemoteZulipServer emails.
|
||||
for email_address in target_emails:
|
||||
send_email(
|
||||
email_id,
|
||||
to_emails=[email_address],
|
||||
from_address=FromAddress.SUPPORT,
|
||||
reply_to_email=options.get("reply_to"),
|
||||
from_name=get_header(
|
||||
options.get("from_name"), parsed_email_template.get("from"), "from_name"
|
||||
),
|
||||
context={"remote_server_email": True},
|
||||
dry_run=dry_run,
|
||||
|
||||
def send_custom_server_email(
|
||||
remote_servers: QuerySet["RemoteZulipServer"],
|
||||
*,
|
||||
dry_run: bool,
|
||||
options: Dict[str, str],
|
||||
add_context: Optional[Callable[[Dict[str, object], "RemoteZulipServer"], None]] = None,
|
||||
) -> None:
|
||||
email_sender = custom_email_sender(**options, dry_run=dry_run)
|
||||
|
||||
for server in remote_servers:
|
||||
email_sender(
|
||||
to_email=server.contact_email,
|
||||
context={
|
||||
"remote_server_email": True,
|
||||
},
|
||||
)
|
||||
|
||||
if dry_run:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from argparse import ArgumentParser
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
from typing import Any, Callable, Dict, Optional
|
||||
|
||||
import orjson
|
||||
from django.conf import settings
|
||||
@@ -8,9 +8,12 @@ from typing_extensions import override
|
||||
|
||||
from confirmation.models import one_click_unsubscribe_link
|
||||
from zerver.lib.management import ZulipBaseCommand
|
||||
from zerver.lib.send_email import send_custom_email
|
||||
from zerver.lib.send_email import send_custom_email, send_custom_server_email
|
||||
from zerver.models import Realm, UserProfile
|
||||
|
||||
if settings.ZILENCER_ENABLED:
|
||||
from zilencer.models import RemoteZulipServer
|
||||
|
||||
|
||||
class Command(ZulipBaseCommand):
|
||||
help = """
|
||||
@@ -89,10 +92,18 @@ class Command(ZulipBaseCommand):
|
||||
def handle(
|
||||
self, *args: Any, dry_run: bool = False, admins_only: bool = False, **options: str
|
||||
) -> None:
|
||||
target_emails: List[str] = []
|
||||
users: QuerySet[UserProfile] = UserProfile.objects.none()
|
||||
add_context: Optional[Callable[[Dict[str, object], UserProfile], None]] = None
|
||||
|
||||
if options["remote_servers"]:
|
||||
servers = RemoteZulipServer.objects.filter(deactivated=False)
|
||||
send_custom_server_email(servers, dry_run=dry_run, options=options)
|
||||
if dry_run:
|
||||
print("Would send the above email to:")
|
||||
for server in servers:
|
||||
print(f" {server.contact_email} ({server.hostname})")
|
||||
return
|
||||
|
||||
if options["entire_server"]:
|
||||
users = UserProfile.objects.filter(
|
||||
is_active=True, is_bot=False, is_mirror_dummy=False, realm__deactivated=False
|
||||
@@ -113,16 +124,7 @@ class Command(ZulipBaseCommand):
|
||||
context["unsubscribe_link"] = one_click_unsubscribe_link(user, "marketing")
|
||||
|
||||
add_context = add_marketing_unsubscribe
|
||||
elif options["remote_servers"]:
|
||||
from zilencer.models import RemoteZulipServer
|
||||
|
||||
target_emails = list(
|
||||
set(
|
||||
RemoteZulipServer.objects.filter(deactivated=False).values_list(
|
||||
"contact_email", flat=True
|
||||
)
|
||||
)
|
||||
)
|
||||
elif options["all_sponsored_org_admins"]:
|
||||
# Sends at most one copy to each email address, even if it
|
||||
# is an administrator in several organizations.
|
||||
@@ -162,7 +164,6 @@ class Command(ZulipBaseCommand):
|
||||
)
|
||||
send_custom_email(
|
||||
users,
|
||||
target_emails=target_emails,
|
||||
dry_run=dry_run,
|
||||
options=options,
|
||||
add_context=add_context,
|
||||
@@ -172,5 +173,3 @@ class Command(ZulipBaseCommand):
|
||||
print("Would send the above email to:")
|
||||
for user in users:
|
||||
print(f" {user.delivery_email} ({user.realm.string_id})")
|
||||
for email in target_emails:
|
||||
print(f" {email}")
|
||||
|
||||
@@ -16,9 +16,14 @@ from zerver.lib.email_notifications import (
|
||||
get_onboarding_email_schedule,
|
||||
send_account_registered_email,
|
||||
)
|
||||
from zerver.lib.send_email import deliver_scheduled_emails, send_custom_email
|
||||
from zerver.lib.send_email import (
|
||||
deliver_scheduled_emails,
|
||||
send_custom_email,
|
||||
send_custom_server_email,
|
||||
)
|
||||
from zerver.lib.test_classes import ZulipTestCase
|
||||
from zerver.models import Realm, ScheduledEmail, UserProfile, get_realm
|
||||
from zilencer.models import RemoteZulipServer
|
||||
|
||||
|
||||
class TestCustomEmails(ZulipTestCase):
|
||||
@@ -60,11 +65,9 @@ class TestCustomEmails(ZulipTestCase):
|
||||
email_subject = "subject_test"
|
||||
reply_to = "reply_to_test"
|
||||
from_name = "from_name_test"
|
||||
contact_email = "zulip-admin@example.com"
|
||||
markdown_template_path = "templates/corporate/policies/index.md"
|
||||
send_custom_email(
|
||||
UserProfile.objects.none(),
|
||||
target_emails=[contact_email],
|
||||
send_custom_server_email(
|
||||
remote_servers=RemoteZulipServer.objects.all(),
|
||||
dry_run=False,
|
||||
options={
|
||||
"markdown_template_path": markdown_template_path,
|
||||
@@ -76,7 +79,7 @@ class TestCustomEmails(ZulipTestCase):
|
||||
self.assert_length(mail.outbox, 1)
|
||||
msg = mail.outbox[0]
|
||||
self.assertEqual(msg.subject, email_subject)
|
||||
self.assertEqual(msg.to, [contact_email])
|
||||
self.assertEqual(msg.to, ["remotezulipserver@zulip.com"])
|
||||
self.assert_length(msg.reply_to, 1)
|
||||
self.assertEqual(msg.reply_to[0], reply_to)
|
||||
self.assertNotIn("{% block content %}", msg.body)
|
||||
|
||||
Reference in New Issue
Block a user