diff --git a/backend/conftest.py b/backend/conftest.py index 9958c992b3..a5eaf9192e 100644 --- a/backend/conftest.py +++ b/backend/conftest.py @@ -75,22 +75,6 @@ def change_azure_account_to_test_name(settings): settings.AZURE_STORAGE_ACCOUNT_NAME = "pytest-fakestorageaccount" -class TestEmailBackend: - ALL_EMAIL_BACKEND_CALLS = [] - - def __init__(self, *args, **kwargs) -> None: - pass - - def send_email(self, **kwargs): - TestEmailBackend.ALL_EMAIL_BACKEND_CALLS.append(kwargs) - - -@pytest.fixture -def sent_emails(): - TestEmailBackend.ALL_EMAIL_BACKEND_CALLS = [] - yield TestEmailBackend.ALL_EMAIL_BACKEND_CALLS - - @pytest.fixture def image_file(): def wrapper(filename: str = "test.jpg"): diff --git a/backend/notifications/aws.py b/backend/notifications/aws.py deleted file mode 100644 index 716fc369a3..0000000000 --- a/backend/notifications/aws.py +++ /dev/null @@ -1,52 +0,0 @@ -import typing - -import boto3 -from django.conf import settings - -from newsletters.exporter import Endpoint -from users.models import User - - -def _get_client(): - return boto3.client("pinpoint", region_name="eu-central-1") - - -def chunks(arr, n): - for i in range(0, len(arr), n): - yield arr[i : i + n] # noqa - - -def send_endpoints_to_pinpoint(endpoints: typing.Iterable[Endpoint]): - # batch only supports 100 at the time - endpoint_chunks = chunks(list(endpoints), 100) - - for endpoints_chunk in endpoint_chunks: - data = {"Item": [endpoint.to_item() for endpoint in endpoints_chunk]} - - client = _get_client() - client.update_endpoints_batch( - ApplicationId=settings.PINPOINT_APPLICATION_ID, EndpointBatchRequest=data - ) - - -def send_notification( - template_name: str, - users: typing.List[User], - substitutions: typing.Dict[str, typing.List[str]], -): - client = _get_client() - client.send_users_messages( - ApplicationId=settings.PINPOINT_APPLICATION_ID, - SendUsersMessageRequest={ - "MessageConfiguration": { - "EmailMessage": { - "FromAddress": "noreply@pycon.it", - "Substitutions": substitutions, - } - }, - "TemplateConfiguration": {"EmailTemplate": {"Name": template_name}}, - "Users": {str(user.id): {} for user in users}, - }, - ) - - # TODO: validate that it has been sent correctly diff --git a/backend/notifications/backends/__init__.py b/backend/notifications/backends/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/notifications/backends/base.py b/backend/notifications/backends/base.py deleted file mode 100644 index 8961ce6a66..0000000000 --- a/backend/notifications/backends/base.py +++ /dev/null @@ -1,20 +0,0 @@ -from typing import Dict, List, Optional - -from notifications.templates import EmailTemplate - - -class EmailBackend: - def __init__(self, environment: Optional[str] = None) -> None: - self.environment = environment - - def send_email( - self, - *, - template: EmailTemplate, - subject: str, - from_: str, - to: str, - variables: Optional[Dict[str, str]] = None, - reply_to: List[str] = None, - ) -> str: - raise NotImplementedError() diff --git a/backend/notifications/backends/local.py b/backend/notifications/backends/local.py deleted file mode 100644 index 3a1319a8aa..0000000000 --- a/backend/notifications/backends/local.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Dict, List, Optional -from uuid import uuid4 - -from notifications.templates import EmailTemplate - -from notifications.backends.base import EmailBackend - - -class LocalEmailBackend(EmailBackend): - def __init__(self, environment: Optional[str] = None) -> None: - super().__init__(environment=environment) - - def send_email( - self, - *, - template: EmailTemplate, - subject: str, - from_: str, - to: str, - variables: Optional[Dict[str, str]] = None, - reply_to: List[str] = None, - ) -> str: - reply_to = reply_to or [] - - print("=== Email sending ===") - print(f"Template: {template}") - print(f"From: {from_}") - print(f"To: {to}") - print(f"Subject: {subject}") - print(f"Variables: {str(variables)}") - print(f"Reply to: {str(reply_to)}") - print("=== End Email sending ===") - - return f"messageid-{uuid4()}" diff --git a/backend/notifications/backends/ses.py b/backend/notifications/backends/ses.py deleted file mode 100644 index c4a7e961c0..0000000000 --- a/backend/notifications/backends/ses.py +++ /dev/null @@ -1,47 +0,0 @@ -import json -from typing import Any, Dict, List, Optional -import html -import boto3 -from notifications.templates import EmailTemplate -from notifications.emails import SafeString -from notifications.backends.base import EmailBackend - - -class SESEmailBackend(EmailBackend): - def __init__(self, environment: str) -> None: - super().__init__(environment) - self.ses = boto3.client("ses") - - def send_email( - self, - *, - template: EmailTemplate, - subject: str, - from_: str, - to: str, - variables: Optional[Dict[str, str]] = None, - reply_to: List[str] = None, - ) -> str: - reply_to = reply_to or [] - - variables = self.encode_vars({"subject": subject, **(variables or {})}) - response = self.ses.send_templated_email( - Source=from_, - Destination={"ToAddresses": [to]}, - Template=f"pythonit-{self.environment}-{template}", - TemplateData=json.dumps(variables), - ReplyToAddresses=reply_to, - ConfigurationSetName="primary", - ) - return response["MessageId"] - - def encode_vars(self, variables: dict[str, Any]) -> dict[str, Any]: - vars = dict() - - for key, value in variables.items(): - if isinstance(value, str) and not isinstance(value, SafeString): - value = html.escape(value) - - vars[key] = value - - return vars diff --git a/backend/notifications/backends/tests/__init__.py b/backend/notifications/backends/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/notifications/backends/tests/test_base.py b/backend/notifications/backends/tests/test_base.py deleted file mode 100644 index 34521e1996..0000000000 --- a/backend/notifications/backends/tests/test_base.py +++ /dev/null @@ -1,15 +0,0 @@ -import pytest -from notifications.backends.base import EmailBackend - - -def test_base_backend(): - base = EmailBackend() - with pytest.raises(NotImplementedError): - base.send_email( - template="template", - subject="subject", - from_="from", - to="to", - variables={"key": "value"}, - reply_to=["reply"], - ) diff --git a/backend/notifications/backends/tests/test_local.py b/backend/notifications/backends/tests/test_local.py deleted file mode 100644 index efba1c99b9..0000000000 --- a/backend/notifications/backends/tests/test_local.py +++ /dev/null @@ -1,26 +0,0 @@ -from unittest.mock import patch - -from notifications.backends.local import LocalEmailBackend -from notifications.templates import EmailTemplate - - -def test_local_email_just_logs(): - with patch("notifications.backends.local.print") as mock_logger: - LocalEmailBackend().send_email( - template=EmailTemplate.RESET_PASSWORD, - from_="from@from.it", - to="hello@world.it", - subject="Hello World!", - variables={"a": "b", "c": "d"}, - reply_to=["test@placeholder.com"], - ) - - assert mock_logger.call_count == 8 - mock_logger.assert_any_call("=== Email sending ===") - mock_logger.assert_any_call(f"Template: {str(EmailTemplate.RESET_PASSWORD)}") - mock_logger.assert_any_call("From: from@from.it") - mock_logger.assert_any_call("To: hello@world.it") - mock_logger.assert_any_call("Subject: Hello World!") - mock_logger.assert_any_call("Variables: {'a': 'b', 'c': 'd'}") - mock_logger.assert_any_call("Reply to: ['test@placeholder.com']") - mock_logger.assert_any_call("=== End Email sending ===") diff --git a/backend/notifications/backends/tests/test_ses.py b/backend/notifications/backends/tests/test_ses.py deleted file mode 100644 index a9a4a77796..0000000000 --- a/backend/notifications/backends/tests/test_ses.py +++ /dev/null @@ -1,142 +0,0 @@ -from unittest.mock import patch -from notifications.emails import mark_safe -from notifications.backends.ses import SESEmailBackend -from notifications.templates import EmailTemplate - - -def test_send_email_via_ses(): - with patch("notifications.backends.ses.boto3") as mock_boto: - mock_boto.client.return_value.send_templated_email.return_value = { - "MessageId": "msg-id-123" - } - message_id = SESEmailBackend("production").send_email( - template=EmailTemplate.RESET_PASSWORD, - subject="Subject", - from_="test@email.it", - to="destination@email.it", - variables={"a": "b", "c": "d"}, - ) - - assert message_id == "msg-id-123" - - mock_boto.client.return_value.send_templated_email.assert_called_once_with( - Source="test@email.it", - Destination={"ToAddresses": ["destination@email.it"]}, - Template="pythonit-production-reset-password", - TemplateData='{"subject": "Subject", "a": "b", "c": "d"}', - ReplyToAddresses=[], - ConfigurationSetName="primary", - ) - - -def test_send_email_without_variables(): - with patch("notifications.backends.ses.boto3") as mock_boto: - SESEmailBackend("production").send_email( - template=EmailTemplate.RESET_PASSWORD, - subject="Subject", - from_="test@email.it", - to="destination@email.it", - ) - - mock_boto.client.return_value.send_templated_email.assert_called_once_with( - Source="test@email.it", - Destination={"ToAddresses": ["destination@email.it"]}, - Template="pythonit-production-reset-password", - TemplateData='{"subject": "Subject"}', - ReplyToAddresses=[], - ConfigurationSetName="primary", - ) - - -def test_send_email_with_reply_to(): - with patch("notifications.backends.ses.boto3") as mock_boto: - SESEmailBackend("production").send_email( - template=EmailTemplate.RESET_PASSWORD, - subject="Subject", - from_="test@email.it", - to="destination@email.it", - reply_to=[ - "test1@placeholder.com", - "test2@placeholder.com", - ], - ) - - mock_boto.client.return_value.send_templated_email.assert_called_once_with( - Source="test@email.it", - Destination={"ToAddresses": ["destination@email.it"]}, - Template="pythonit-production-reset-password", - TemplateData='{"subject": "Subject"}', - ReplyToAddresses=[ - "test1@placeholder.com", - "test2@placeholder.com", - ], - ConfigurationSetName="primary", - ) - - -def test_variables_are_html_encoded(): - with patch("notifications.backends.ses.boto3") as mock_boto: - SESEmailBackend("production").send_email( - template=EmailTemplate.RESET_PASSWORD, - subject="Subject", - from_="test@email.it", - to="destination@email.it", - variables={ - "a": 'link', - }, - ) - - mock_boto.client.return_value.send_templated_email.assert_called_once_with( - Source="test@email.it", - Destination={"ToAddresses": ["destination@email.it"]}, - Template="pythonit-production-reset-password", - TemplateData='{"subject": "Subject", "a": "<a href="https://google.it">link</a>"}', - ReplyToAddresses=[], - ConfigurationSetName="primary", - ) - - -def test_safe_string_variables_are_not_encoded(): - with patch("notifications.backends.ses.boto3") as mock_boto: - SESEmailBackend("production").send_email( - template=EmailTemplate.RESET_PASSWORD, - subject="Subject", - from_="test@email.it", - to="destination@email.it", - variables={ - "safe": mark_safe('link'), - "not_safe": 'link', - }, - ) - - mock_boto.client.return_value.send_templated_email.assert_called_once_with( - Source="test@email.it", - Destination={"ToAddresses": ["destination@email.it"]}, - Template="pythonit-production-reset-password", - TemplateData='{"subject": "Subject", "safe": "link", "not_safe": "<a href="https://google.it">link</a>"}', - ReplyToAddresses=[], - ConfigurationSetName="primary", - ) - - -def test_non_string_variables_are_not_encoded(): - with patch("notifications.backends.ses.boto3") as mock_boto: - SESEmailBackend("production").send_email( - template=EmailTemplate.RESET_PASSWORD, - subject="Subject", - from_="test@email.it", - to="destination@email.it", - variables={ - "value": 1, - "boolean": False, - }, - ) - - mock_boto.client.return_value.send_templated_email.assert_called_once_with( - Source="test@email.it", - Destination={"ToAddresses": ["destination@email.it"]}, - Template="pythonit-production-reset-password", - TemplateData='{"subject": "Subject", "value": 1, "boolean": false}', - ReplyToAddresses=[], - ConfigurationSetName="primary", - ) diff --git a/backend/notifications/emails.py b/backend/notifications/emails.py deleted file mode 100644 index 220f4c0d95..0000000000 --- a/backend/notifications/emails.py +++ /dev/null @@ -1,65 +0,0 @@ -from typing import List, Optional - -from django.conf import settings -from notifications.templates import EmailTemplate - - -from dataclasses import dataclass -import importlib - -from .backends.base import EmailBackend - -EMAIL_BACKEND_CACHE: dict[str, EmailBackend] = {} - - -def get_email_backend(backend_path: str, **options: dict[str, str]) -> EmailBackend: - global EMAIL_BACKEND_CACHE - - instance = EMAIL_BACKEND_CACHE.get(backend_path, None) - - if not instance: - module_path, class_name = backend_path.rsplit(".", 1) - module = importlib.import_module(module_path) - class_impl = getattr(module, class_name) - instance = class_impl(**options) - EMAIL_BACKEND_CACHE[backend_path] = instance - - return instance - - -@dataclass -class SafeString(str): - original_str: str - - def __str__(self) -> str: - return self.original_str - - -def mark_safe(string: str) -> SafeString: - if string is None: - raise ValueError("string cannot be None") - - return SafeString(string) - - -def send_email( - *, - template: EmailTemplate, - to: str, - subject: str, - from_: Optional[str] = None, - variables: Optional[dict[str, str]] = None, - reply_to: List[str] = None, -): - from_ = from_ or settings.DEFAULT_FROM_EMAIL - backend = get_email_backend( - settings.PYTHONIT_EMAIL_BACKEND, environment=settings.ENVIRONMENT - ) - backend.send_email( - template=template, - from_=from_, - to=to, - subject=subject, - variables=variables, - reply_to=reply_to, - ) diff --git a/backend/notifications/templates.py b/backend/notifications/templates.py deleted file mode 100644 index f482e79779..0000000000 --- a/backend/notifications/templates.py +++ /dev/null @@ -1,9 +0,0 @@ -from enum import Enum - - -class EmailTemplate(str, Enum): - # Users - RESET_PASSWORD = "reset-password" - - def __str__(self) -> str: - return str.__str__(self) diff --git a/backend/notifications/tests/test_emails.py b/backend/notifications/tests/test_emails.py deleted file mode 100644 index 4f27468508..0000000000 --- a/backend/notifications/tests/test_emails.py +++ /dev/null @@ -1,66 +0,0 @@ -from unittest.mock import patch - -from notifications.emails import SafeString, get_email_backend, mark_safe -import pytest - - -class FakeBackend: - pass - - -class FakeBackendWithEnv: - def __init__(self, a: int, b: int) -> None: - self.a = a - self.b = b - - -def test_get_email_backend(): - loaded_backend = get_email_backend("notifications.tests.test_emails.FakeBackend") - assert "notifications.tests.test_emails.FakeBackend" in str(type(loaded_backend)) - - -def test_loading_same_backend_uses_cache(): - loaded_backend = get_email_backend("notifications.tests.test_emails.FakeBackend") - assert "notifications.tests.test_emails.FakeBackend" in str(type(loaded_backend)) - - with patch("notifications.emails.importlib.import_module") as mock: - loaded_backend = get_email_backend( - "notifications.tests.test_emails.FakeBackend" - ) - - assert "notifications.tests.test_emails.FakeBackend" in str(type(loaded_backend)) - assert not mock.called - - mock.reset_mock() - - with patch("notifications.emails.importlib.import_module") as mock: - get_email_backend("notifications.tests.test_emails.FakeBackend2") - - assert mock.called - - -def test_get_email_backend_with_envs(): - loaded_backend = get_email_backend( - "notifications.tests.test_emails.FakeBackendWithEnv", a=1, b=2 - ) - assert loaded_backend.a == 1 - assert loaded_backend.b == 2 - - -def test_mark_safe(): - safe_string = mark_safe("abc") - - assert isinstance(safe_string, SafeString) - assert str(safe_string) == "abc" - - -def test_mark_safe_empty_string(): - safe_string = mark_safe("") - - assert isinstance(safe_string, SafeString) - assert str(safe_string) == "" - - -def test_mark_safe_with_none_fails(): - with pytest.raises(ValueError): - mark_safe(None) diff --git a/backend/pycon/settings/base.py b/backend/pycon/settings/base.py index 9a5b505b40..4e299a6a6b 100644 --- a/backend/pycon/settings/base.py +++ b/backend/pycon/settings/base.py @@ -286,19 +286,12 @@ QUERY_INSPECT_LOG_TRACEBACKS = True QUERY_INSPECT_TRACEBACK_ROOTS = [root(".")] -PINPOINT_APPLICATION_ID = env("PINPOINT_APPLICATION_ID", default="") - MAILCHIMP_SECRET_KEY = env("MAILCHIMP_SECRET_KEY", default="") MAILCHIMP_DC = env("MAILCHIMP_DC", default="us3") MAILCHIMP_LIST_ID = env("MAILCHIMP_LIST_ID", default="") DEFAULT_AUTO_FIELD = "django.db.models.AutoField" -PYTHONIT_EMAIL_BACKEND = env( - "PYTHONIT_EMAIL_BACKEND", - default="notifications.backends.local.LocalEmailBackend", -) - SPEAKERS_EMAIL_ADDRESS = env("SPEAKERS_EMAIL_ADDRESS", default="") VOLUNTEERS_PUSH_NOTIFICATIONS_IOS_ARN = env( diff --git a/backend/pycon/settings/test.py b/backend/pycon/settings/test.py index bad943d26e..3d1fb00529 100644 --- a/backend/pycon/settings/test.py +++ b/backend/pycon/settings/test.py @@ -34,7 +34,6 @@ "BACKEND": "django.core.cache.backends.dummy.DummyCache", } } -PYTHONIT_EMAIL_BACKEND = "conftest.TestEmailBackend" CELERY_TASK_ALWAYS_EAGER = True CELERY_TASK_EAGER_PROPAGATES = True diff --git a/infrastructure/applications/pycon_backend/main.tf b/infrastructure/applications/pycon_backend/main.tf index 60fbb125bc..663b7b5130 100644 --- a/infrastructure/applications/pycon_backend/main.tf +++ b/infrastructure/applications/pycon_backend/main.tf @@ -94,12 +94,10 @@ module "lambda" { AWS_REGION_NAME = aws_s3_bucket.backend_media.region SPEAKERS_EMAIL_ADDRESS = module.secrets.value.speakers_email_address EMAIL_BACKEND = "django_ses.SESBackend" - PYTHONIT_EMAIL_BACKEND = "notifications.backends.ses.SESEmailBackend" FRONTEND_URL = "https://pycon.it" PRETIX_API = "https://tickets.pycon.it/api/v1/" AWS_S3_CUSTOM_DOMAIN = local.cdn_url PRETIX_API_TOKEN = module.common_secrets.value.pretix_api_token - PINPOINT_APPLICATION_ID = module.secrets.value.pinpoint_application_id MAILCHIMP_SECRET_KEY = module.common_secrets.value.mailchimp_secret_key MAILCHIMP_DC = module.common_secrets.value.mailchimp_dc MAILCHIMP_LIST_ID = module.common_secrets.value.mailchimp_list_id diff --git a/infrastructure/applications/pycon_backend/worker.tf b/infrastructure/applications/pycon_backend/worker.tf index 718c060f01..8746f573ac 100644 --- a/infrastructure/applications/pycon_backend/worker.tf +++ b/infrastructure/applications/pycon_backend/worker.tf @@ -60,10 +60,6 @@ locals { name = "EMAIL_BACKEND", value = "django_ses.SESBackend" }, - { - name = "PYTHONIT_EMAIL_BACKEND", - value = "notifications.backends.ses.SESEmailBackend" - }, { name = "FRONTEND_URL", value = "https://pycon.it" @@ -80,10 +76,6 @@ locals { name = "PRETIX_API_TOKEN", value = module.common_secrets.value.pretix_api_token }, - { - name = "PINPOINT_APPLICATION_ID", - value = module.secrets.value.pinpoint_application_id - }, { name = "MAILCHIMP_SECRET_KEY", value = module.common_secrets.value.mailchimp_secret_key