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