diff --git a/h/emails/flag_notification.py b/h/emails/flag_notification.py index c672d972deb..e1652def4ce 100644 --- a/h/emails/flag_notification.py +++ b/h/emails/flag_notification.py @@ -1,6 +1,7 @@ from pyramid.renderers import render from h.i18n import TranslationString as _ +from h.services.email import EmailTag def generate(request, email, incontext_link): @@ -27,4 +28,4 @@ def generate(request, email, incontext_link): "h:templates/emails/flag_notification.html.jinja2", context, request=request ) - return [email], subject, text, html + return [email], subject, text, EmailTag.FLAG_NOTIFICATION, html diff --git a/h/emails/reply_notification.py b/h/emails/reply_notification.py index 078ce971962..71388527d30 100644 --- a/h/emails/reply_notification.py +++ b/h/emails/reply_notification.py @@ -5,6 +5,7 @@ from h.models import Subscriptions from h.notification.reply import Notification from h.services import SubscriptionService +from h.services.email import EmailTag def generate(request: Request, notification: Notification): @@ -49,7 +50,13 @@ def generate(request: Request, notification: Notification): "h:templates/emails/reply_notification.html.jinja2", context, request=request ) - return [notification.parent_user.email], subject, text, html + return ( + [notification.parent_user.email], + subject, + text, + EmailTag.REPLY_NOTIFICATION, + html + ) def _get_user_url(user, request): diff --git a/h/emails/reset_password.py b/h/emails/reset_password.py index fd81816e191..d0fe8265fd7 100644 --- a/h/emails/reset_password.py +++ b/h/emails/reset_password.py @@ -1,6 +1,7 @@ from pyramid.renderers import render from h.i18n import TranslationString as _ +from h.services.email import EmailTag def generate(request, user): @@ -31,4 +32,4 @@ def generate(request, user): "h:templates/emails/reset_password.html.jinja2", context, request=request ) - return [user.email], subject, text, html + return [user.email], subject, text, EmailTag.RESET_PASSWORD, html diff --git a/h/emails/signup.py b/h/emails/signup.py index 1df59e50c2b..60928fb14d0 100644 --- a/h/emails/signup.py +++ b/h/emails/signup.py @@ -1,6 +1,7 @@ from pyramid.renderers import render from h.i18n import TranslationString as _ +from h.services.email import EmailTag def generate(request, user_id, email, activation_code): @@ -27,4 +28,4 @@ def generate(request, user_id, email, activation_code): text = render("h:templates/emails/signup.txt.jinja2", context, request=request) html = render("h:templates/emails/signup.html.jinja2", context, request=request) - return [email], subject, text, html + return [email], subject, text, EmailTag.ACTIVATION, html diff --git a/h/emails/test.py b/h/emails/test.py index fbe430af030..a362f2626bf 100644 --- a/h/emails/test.py +++ b/h/emails/test.py @@ -4,6 +4,7 @@ from pyramid.renderers import render from h import __version__ +from h.services.email import EmailTag def generate(request, recipient): @@ -28,4 +29,4 @@ def generate(request, recipient): text = render("h:templates/emails/test.txt.jinja2", context, request=request) html = render("h:templates/emails/test.html.jinja2", context, request=request) - return [recipient], "Test mail", text, html + return [recipient], "Test mail", text, EmailTag.TEST, html diff --git a/h/services/email.py b/h/services/email.py index 34d7ee09f5f..11c29b18fe4 100644 --- a/h/services/email.py +++ b/h/services/email.py @@ -1,6 +1,7 @@ # noqa: A005 import smtplib +from enum import StrEnum import pyramid_mailer import pyramid_mailer.message @@ -12,6 +13,14 @@ logger = get_task_logger(__name__) +class EmailTag(StrEnum): + ACTIVATION = "activation" + FLAG_NOTIFICATION = "flag_notification" + REPLY_NOTIFICATION = "reply_notification" + RESET_PASSWORD = "reset_password" # noqa: S105 + TEST = "test" + + class EmailService: """A service for sending emails.""" @@ -20,10 +29,20 @@ def __init__(self, request: Request, mailer: IMailer) -> None: self._mailer = mailer def send( - self, recipients: list[str], subject: str, body: str, html: str | None = None - ): + self, + recipients: list[str], + subject: str, + body: str, + tag: EmailTag, + html: str | None = None, + ) -> None: + extra_headers = {"X-MC-Tags": tag} email = pyramid_mailer.message.Message( - subject=subject, recipients=recipients, body=body, html=html + subject=subject, + recipients=recipients, + body=body, + html=html, + extra_headers=extra_headers, ) if self._request.debug: # pragma: no cover logger.info("emailing in debug mode: check the `mail/` directory") diff --git a/h/tasks/mailer.py b/h/tasks/mailer.py index bf4ee51af2e..08e3c03d2e5 100644 --- a/h/tasks/mailer.py +++ b/h/tasks/mailer.py @@ -6,14 +6,21 @@ import smtplib -from h.services.email import EmailService +from h.services.email import EmailService, EmailTag from h.tasks.celery import celery __all__ = ("send",) @celery.task(bind=True, max_retries=3, acks_late=True) -def send(self, recipients, subject, body, html=None): +def send( # noqa: PLR0913 + self, + tag: EmailTag, + recipients: list[str], + subject: str, + body: str, + html: str | None = None, +) -> None: """ Send an email. @@ -28,7 +35,9 @@ def send(self, recipients, subject, body, html=None): """ service = celery.request.find_service(EmailService) try: - service.send(recipients=recipients, subject=subject, body=body, html=html) + service.send( + tag=tag, recipients=recipients, subject=subject, body=body, html=html + ) except smtplib.socket.error as exc: # Exponential backoff in case the SMTP service is having problems. countdown = self.default_retry_delay * 2**self.request.retries diff --git a/tests/unit/h/emails/flag_notification_test.py b/tests/unit/h/emails/flag_notification_test.py index 86a3e7e2038..ce467fbe90c 100644 --- a/tests/unit/h/emails/flag_notification_test.py +++ b/tests/unit/h/emails/flag_notification_test.py @@ -1,6 +1,7 @@ import pytest from h.emails.flag_notification import generate +from h.services.email import EmailTag class TestGenerate: @@ -23,7 +24,7 @@ def test_appropriate_return_values( html_renderer.string_response = "HTML output" text_renderer.string_response = "Text output" - recipients, subject, text, html = generate( + recipients, subject, text, tag, html = generate( pyramid_request, email="foo@example.com", incontext_link="http://hyp.is/a/ann1", @@ -32,6 +33,7 @@ def test_appropriate_return_values( assert recipients == ["foo@example.com"] assert subject == "An annotation has been flagged" assert html == "HTML output" + assert tag == EmailTag.FLAG_NOTIFICATION assert text == "Text output" @pytest.fixture diff --git a/tests/unit/h/emails/reply_notification_test.py b/tests/unit/h/emails/reply_notification_test.py index af634e6dea2..9ca3591db1f 100644 --- a/tests/unit/h/emails/reply_notification_test.py +++ b/tests/unit/h/emails/reply_notification_test.py @@ -99,7 +99,7 @@ def test_supports_non_ascii_display_names( parent_user.display_name = "Parent 👩" reply_user.display_name = "Child 👧" - (_, subject, _, _) = generate(pyramid_request, notification) + (_, subject, _, _, _) = generate(pyramid_request, notification) assert subject == "Child 👧 has replied to your annotation" @@ -130,7 +130,7 @@ def test_returns_text_and_body_results_from_renderers( html_renderer.string_response = "HTML output" text_renderer.string_response = "Text output" - _, _, text, html = generate(pyramid_request, notification) + _, _, text, _, html = generate(pyramid_request, notification) assert html == "HTML output" assert text == "Text output" @@ -138,7 +138,7 @@ def test_returns_text_and_body_results_from_renderers( def test_returns_subject_with_reply_display_name( self, notification, pyramid_request ): - _, subject, _, _ = generate(pyramid_request, notification) + _, subject, _, _, _ = generate(pyramid_request, notification) assert subject == "Ron Burgundy has replied to your annotation" @@ -146,12 +146,12 @@ def test_returns_subject_with_reply_username( self, notification, pyramid_request, reply_user ): reply_user.display_name = None - _, subject, _, _ = generate(pyramid_request, notification) + _, subject, _, _, _ = generate(pyramid_request, notification) assert subject == "ron has replied to your annotation" def test_returns_parent_email_as_recipients(self, notification, pyramid_request): - recipients, _, _, _ = generate(pyramid_request, notification) + recipients, _, _, _, _ = generate(pyramid_request, notification) assert recipients == ["pat@ric.ia"] diff --git a/tests/unit/h/emails/reset_password_test.py b/tests/unit/h/emails/reset_password_test.py index d70d8b5ff05..fc2adcfa1b7 100644 --- a/tests/unit/h/emails/reset_password_test.py +++ b/tests/unit/h/emails/reset_password_test.py @@ -3,6 +3,7 @@ import pytest from h.emails.reset_password import generate +from h.services.email import EmailTag @pytest.mark.usefixtures("routes") @@ -38,11 +39,12 @@ def test_appropriate_return_values( html_renderer.string_response = "HTML output" text_renderer.string_response = "Text output" - recipients, subject, text, html = generate(pyramid_request, user) + recipients, subject, text, tag, html = generate(pyramid_request, user) assert recipients == [user.email] assert subject == "Reset your password" assert html == "HTML output" + assert tag == EmailTag.RESET_PASSWORD assert text == "Text output" def test_jinja_templates_render( diff --git a/tests/unit/h/emails/signup_test.py b/tests/unit/h/emails/signup_test.py index 2e690c18494..cbafc12227e 100644 --- a/tests/unit/h/emails/signup_test.py +++ b/tests/unit/h/emails/signup_test.py @@ -1,6 +1,7 @@ import pytest from h.emails.signup import generate +from h.services.email import EmailTag @pytest.mark.usefixtures("routes") @@ -27,7 +28,7 @@ def test_appropriate_return_values( html_renderer.string_response = "HTML output" text_renderer.string_response = "Text output" - recipients, subject, text, html = generate( + recipients, subject, text, tag, html = generate( pyramid_request, user_id=1234, email="foo@example.com", @@ -37,6 +38,7 @@ def test_appropriate_return_values( assert recipients == ["foo@example.com"] assert subject == "Please activate your account" assert html == "HTML output" + assert tag == EmailTag.ACTIVATION assert text == "Text output" def test_jinja_templates_render(self, pyramid_config, pyramid_request): diff --git a/tests/unit/h/emails/test_test.py b/tests/unit/h/emails/test_test.py index 1ae9495ab01..18c968c4827 100644 --- a/tests/unit/h/emails/test_test.py +++ b/tests/unit/h/emails/test_test.py @@ -3,6 +3,7 @@ from h import __version__ from h.emails.test import generate +from h.services.email import EmailTag class TestGenerate: @@ -26,13 +27,14 @@ def test_appropriate_return_values( html_renderer.string_response = "HTML output" text_renderer.string_response = "Text output" - recipients, subject, text, html = generate( + recipients, subject, text, tag, html = generate( pyramid_request, "meerkat@example.com" ) assert recipients == ["meerkat@example.com"] assert subject == "Test mail" assert html == "HTML output" + assert tag == EmailTag.TEST assert text == "Text output" def test_jinja_templates_render(self, pyramid_config, pyramid_request): diff --git a/tests/unit/h/services/email_test.py b/tests/unit/h/services/email_test.py index 83c2c1c08d1..92905168306 100644 --- a/tests/unit/h/services/email_test.py +++ b/tests/unit/h/services/email_test.py @@ -3,7 +3,7 @@ import pytest -from h.services.email import EmailService, factory +from h.services.email import EmailService, EmailTag, factory class TestEmailService: @@ -12,6 +12,7 @@ def test_send_creates_email_message(self, email_service, pyramid_mailer): recipients=["foo@example.com"], subject="My email subject", body="Some text body", + tag=EmailTag.TEST, ) pyramid_mailer.message.Message.assert_called_once_with( @@ -19,6 +20,7 @@ def test_send_creates_email_message(self, email_service, pyramid_mailer): subject="My email subject", body="Some text body", html=None, + extra_headers={"X-MC-Tags": EmailTag.TEST}, ) def test_send_creates_email_message_with_html_body( @@ -28,6 +30,7 @@ def test_send_creates_email_message_with_html_body( recipients=["foo@example.com"], subject="My email subject", body="Some text body", + tag=EmailTag.TEST, html="

An HTML body

", ) @@ -36,6 +39,7 @@ def test_send_creates_email_message_with_html_body( subject="My email subject", body="Some text body", html="

An HTML body

", + extra_headers={"X-MC-Tags": EmailTag.TEST}, ) def test_send_dispatches_email_using_request_mailer( @@ -48,6 +52,7 @@ def test_send_dispatches_email_using_request_mailer( recipients=["foo@example.com"], subject="My email subject", body="Some text body", + tag=EmailTag.TEST, ) request_mailer.send_immediately.assert_called_once_with(message) @@ -61,6 +66,7 @@ def test_raises_smtplib_exception(self, email_service, pyramid_mailer): recipients=["foo@example.com"], subject="My email subject", body="Some text body", + tag=EmailTag.TEST, ) @pytest.fixture diff --git a/tests/unit/h/tasks/mailer_test.py b/tests/unit/h/tasks/mailer_test.py index e3e8330796d..fae4abddbed 100644 --- a/tests/unit/h/tasks/mailer_test.py +++ b/tests/unit/h/tasks/mailer_test.py @@ -3,6 +3,7 @@ import pytest +from h.services.email import EmailTag from h.tasks import mailer @@ -14,6 +15,7 @@ def test_send_retries_if_mailing_fails(email_service): recipients=["foo@example.com"], subject="My email subject", body="Some text body", + tag=EmailTag.TEST, ) assert mailer.send.retry.called