Skip to content

Commit e42e38e

Browse files
authored
Merge pull request #100 from AvaCodeSolutions/feat/39/mail-sender-protocol
feat: #30 add email sender protocol
2 parents e998232 + 2838642 commit e42e38e

8 files changed

Lines changed: 80 additions & 0 deletions

File tree

django_email_learning/ports/__init__.py

Whitespace-only changes.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
from typing import Protocol
2+
from django.core.mail import EmailMultiAlternatives
3+
4+
5+
class EmailSenderProtocol(Protocol):
6+
def send_email(self, email: EmailMultiAlternatives) -> None:
7+
...

django_email_learning/services/__init__.py

Whitespace-only changes.

django_email_learning/services/deafults/__init__.py

Whitespace-only changes.
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import logging
2+
from django_email_learning.ports.email_sender_protocol import EmailSenderProtocol
3+
from django.core.mail import EmailMultiAlternatives
4+
5+
logger = logging.getLogger(__name__)
6+
7+
8+
class DjangoEmailSender(EmailSenderProtocol):
9+
def _mask_email(self, email_address: str) -> str:
10+
"""Mask email address for logging privacy."""
11+
try:
12+
username, domain = email_address.split("@")
13+
masked_username = username[0] + "***"
14+
return f"{masked_username}@{domain}"
15+
except ValueError:
16+
return "***@***"
17+
18+
def _mask_recipients(self, recipients: list[str]) -> str:
19+
"""Mask all recipient email addresses for logging."""
20+
if not recipients:
21+
return "no recipients"
22+
masked = [self._mask_email(recipient) for recipient in recipients]
23+
return ", ".join(masked)
24+
25+
def send_email(self, email: EmailMultiAlternatives) -> None:
26+
masked_recipients = self._mask_recipients(email.to)
27+
try:
28+
logger.info(f"Sending email to {masked_recipients}")
29+
email.send()
30+
logger.info(f"Email sent successfully to {masked_recipients}")
31+
except Exception as e:
32+
logger.error(f"Failed to send email to {masked_recipients}: {str(e)}")
33+
raise

tests/services/__init__.py

Whitespace-only changes.

tests/services/defaults/__init__.py

Whitespace-only changes.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from django_email_learning.services.deafults.email_sender import DjangoEmailSender
2+
from django.core.mail import EmailMultiAlternatives
3+
from unittest.mock import Mock
4+
import pytest
5+
6+
7+
@pytest.fixture
8+
def email_sender() -> DjangoEmailSender:
9+
return DjangoEmailSender()
10+
11+
12+
@pytest.fixture
13+
def email_multi_alternatives() -> EmailMultiAlternatives:
14+
email = Mock(spec=EmailMultiAlternatives)
15+
email.to = ["recipient@example.com"]
16+
return email
17+
18+
19+
def test_email_sender_logs_success_with_masked_recipients(
20+
email_sender: DjangoEmailSender,
21+
email_multi_alternatives: EmailMultiAlternatives,
22+
caplog,
23+
):
24+
with caplog.at_level("INFO"):
25+
email_sender.send_email(email_multi_alternatives)
26+
assert "Sending email to r***@example.com" in caplog.text
27+
28+
29+
def test_email_sender_logs_failure_with_masked_recipients(
30+
email_sender: DjangoEmailSender,
31+
email_multi_alternatives: EmailMultiAlternatives,
32+
caplog,
33+
):
34+
email_multi_alternatives.send.side_effect = Exception("SMTP error")
35+
36+
with caplog.at_level("ERROR"):
37+
with pytest.raises(Exception):
38+
email_sender.send_email(email_multi_alternatives)
39+
40+
assert "Failed to send email to r***@example.com" in caplog.text

0 commit comments

Comments
 (0)