diff --git a/django_email_learning/ports/__init__.py b/django_email_learning/ports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_email_learning/ports/email_sender_protocol.py b/django_email_learning/ports/email_sender_protocol.py new file mode 100644 index 0000000..0b677d6 --- /dev/null +++ b/django_email_learning/ports/email_sender_protocol.py @@ -0,0 +1,7 @@ +from typing import Protocol +from django.core.mail import EmailMultiAlternatives + + +class EmailSenderProtocol(Protocol): + def send_email(self, email: EmailMultiAlternatives) -> None: + ... diff --git a/django_email_learning/services/__init__.py b/django_email_learning/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_email_learning/services/deafults/__init__.py b/django_email_learning/services/deafults/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/django_email_learning/services/deafults/email_sender.py b/django_email_learning/services/deafults/email_sender.py new file mode 100644 index 0000000..5ff215c --- /dev/null +++ b/django_email_learning/services/deafults/email_sender.py @@ -0,0 +1,33 @@ +import logging +from django_email_learning.ports.email_sender_protocol import EmailSenderProtocol +from django.core.mail import EmailMultiAlternatives + +logger = logging.getLogger(__name__) + + +class DjangoEmailSender(EmailSenderProtocol): + def _mask_email(self, email_address: str) -> str: + """Mask email address for logging privacy.""" + try: + username, domain = email_address.split("@") + masked_username = username[0] + "***" + return f"{masked_username}@{domain}" + except ValueError: + return "***@***" + + def _mask_recipients(self, recipients: list[str]) -> str: + """Mask all recipient email addresses for logging.""" + if not recipients: + return "no recipients" + masked = [self._mask_email(recipient) for recipient in recipients] + return ", ".join(masked) + + def send_email(self, email: EmailMultiAlternatives) -> None: + masked_recipients = self._mask_recipients(email.to) + try: + logger.info(f"Sending email to {masked_recipients}") + email.send() + logger.info(f"Email sent successfully to {masked_recipients}") + except Exception as e: + logger.error(f"Failed to send email to {masked_recipients}: {str(e)}") + raise diff --git a/tests/services/__init__.py b/tests/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/services/defaults/__init__.py b/tests/services/defaults/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/services/defaults/test_email_sender.py b/tests/services/defaults/test_email_sender.py new file mode 100644 index 0000000..47ab536 --- /dev/null +++ b/tests/services/defaults/test_email_sender.py @@ -0,0 +1,40 @@ +from django_email_learning.services.deafults.email_sender import DjangoEmailSender +from django.core.mail import EmailMultiAlternatives +from unittest.mock import Mock +import pytest + + +@pytest.fixture +def email_sender() -> DjangoEmailSender: + return DjangoEmailSender() + + +@pytest.fixture +def email_multi_alternatives() -> EmailMultiAlternatives: + email = Mock(spec=EmailMultiAlternatives) + email.to = ["recipient@example.com"] + return email + + +def test_email_sender_logs_success_with_masked_recipients( + email_sender: DjangoEmailSender, + email_multi_alternatives: EmailMultiAlternatives, + caplog, +): + with caplog.at_level("INFO"): + email_sender.send_email(email_multi_alternatives) + assert "Sending email to r***@example.com" in caplog.text + + +def test_email_sender_logs_failure_with_masked_recipients( + email_sender: DjangoEmailSender, + email_multi_alternatives: EmailMultiAlternatives, + caplog, +): + email_multi_alternatives.send.side_effect = Exception("SMTP error") + + with caplog.at_level("ERROR"): + with pytest.raises(Exception): + email_sender.send_email(email_multi_alternatives) + + assert "Failed to send email to r***@example.com" in caplog.text