mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-22 08:48:38 +00:00
First successful live run surfaced three issues: 1. Email looked rubbish (a giant raw presigned URL). Now sends a proper HTML email with a 'Download documents' button plus a plain-text fallback, and a summary (N documents across M properties, expiry). Email delivery is now best-effort: a transport failure no longer loses an already-built package (the link is still on sub_task.outputs), and the SMTP connect has a 30s timeout so an unreachable SES endpoint fails fast instead of hanging to the 900s Lambda timeout. 2. Every folder was 'address unavailable (...)': the resolver read property.address, but these are HubSpot deals with no property row. It now uses the deal's dealname from hubspot_deal_data. 3. No logs / no idea why a run took ~9 minutes: the worker's INFO logs were dropped (Lambda root logger defaults to WARNING). The handler now raises the level, and the orchestrator logs per-phase timing and volume (gather+plan, packaged N files / X MB, upload, email, total) so the slow phase is visible. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
139 lines
4.2 KiB
Python
139 lines
4.2 KiB
Python
"""The SES-over-SMTP email sender (ADR-0059) — delivers a plain-text message to
|
|
the requesting user via an injected SMTP transport."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from email.message import EmailMessage
|
|
from types import TracebackType
|
|
from typing import Any, Optional, Type
|
|
|
|
from infrastructure.email.ses_smtp_email_sender import SesSmtpEmailSender
|
|
|
|
|
|
class _FakeSmtp:
|
|
"""Records what the sender hands the transport, standing in for a live SES
|
|
SMTP connection."""
|
|
|
|
def __init__(self, host: str, port: int) -> None:
|
|
self.host = host
|
|
self.port = port
|
|
self.messages: list[EmailMessage] = []
|
|
self.started_tls = False
|
|
self.login_args: Optional[tuple[str, str]] = None
|
|
|
|
def __enter__(self) -> "_FakeSmtp":
|
|
return self
|
|
|
|
def __exit__(
|
|
self,
|
|
exc_type: Optional[Type[BaseException]],
|
|
exc: Optional[BaseException],
|
|
tb: Optional[TracebackType],
|
|
) -> Optional[bool]:
|
|
return None
|
|
|
|
def starttls(self) -> Any:
|
|
self.started_tls = True
|
|
|
|
def login(self, user: str, password: str) -> Any:
|
|
self.login_args = (user, password)
|
|
|
|
def send_message(self, message: EmailMessage) -> Any:
|
|
self.messages.append(message)
|
|
|
|
|
|
def test_send_delivers_a_plaintext_message_with_the_right_envelope() -> None:
|
|
# Arrange
|
|
created: list[_FakeSmtp] = []
|
|
|
|
def factory(host: str, port: int) -> _FakeSmtp:
|
|
smtp = _FakeSmtp(host, port)
|
|
created.append(smtp)
|
|
return smtp
|
|
|
|
sender = SesSmtpEmailSender(
|
|
host="email-smtp.eu-west-2.amazonaws.com",
|
|
port=587,
|
|
username="AKIA_SMTP_USER",
|
|
password="smtp-password",
|
|
from_address="noreply@domna.homes",
|
|
smtp_factory=factory,
|
|
)
|
|
|
|
# Act
|
|
sender.send(
|
|
to="user@example.com",
|
|
subject="Your document download is ready",
|
|
body="Download it here: https://signed-url",
|
|
)
|
|
|
|
# Assert — one message, correctly addressed, body carried through.
|
|
assert len(created) == 1
|
|
assert len(created[0].messages) == 1
|
|
message = created[0].messages[0]
|
|
assert message["To"] == "user@example.com"
|
|
assert message["From"] == "noreply@domna.homes"
|
|
assert message["Subject"] == "Your document download is ready"
|
|
assert "https://signed-url" in message.get_content()
|
|
|
|
|
|
def test_send_adds_an_html_alternative_when_given() -> None:
|
|
# Arrange
|
|
created: list[_FakeSmtp] = []
|
|
|
|
def factory(host: str, port: int) -> _FakeSmtp:
|
|
smtp = _FakeSmtp(host, port)
|
|
created.append(smtp)
|
|
return smtp
|
|
|
|
sender = SesSmtpEmailSender(
|
|
host="email-smtp.eu-west-2.amazonaws.com",
|
|
port=587,
|
|
username="AKIA_SMTP_USER",
|
|
password="smtp-password",
|
|
from_address="noreply@domna.homes",
|
|
smtp_factory=factory,
|
|
)
|
|
|
|
# Act
|
|
sender.send(
|
|
to="user@example.com",
|
|
subject="s",
|
|
body="plain text fallback",
|
|
html_body="<p>Download documents</p>",
|
|
)
|
|
|
|
# Assert — a multipart/alternative carrying both the plain text and the HTML.
|
|
message = created[0].messages[0]
|
|
assert message.get_content_type() == "multipart/alternative"
|
|
plain_part = message.get_body(preferencelist=("plain",))
|
|
html_part = message.get_body(preferencelist=("html",))
|
|
assert plain_part is not None and "plain text fallback" in plain_part.get_content()
|
|
assert html_part is not None and "Download documents" in html_part.get_content()
|
|
|
|
|
|
def test_send_starts_tls_and_authenticates_before_sending() -> None:
|
|
# Arrange — SES SMTP rejects unauthenticated, cleartext senders, so the
|
|
# transport must STARTTLS and log in with the SMTP credentials.
|
|
created: list[_FakeSmtp] = []
|
|
|
|
def factory(host: str, port: int) -> _FakeSmtp:
|
|
smtp = _FakeSmtp(host, port)
|
|
created.append(smtp)
|
|
return smtp
|
|
|
|
sender = SesSmtpEmailSender(
|
|
host="email-smtp.eu-west-2.amazonaws.com",
|
|
port=587,
|
|
username="AKIA_SMTP_USER",
|
|
password="smtp-password",
|
|
from_address="noreply@domna.homes",
|
|
smtp_factory=factory,
|
|
)
|
|
|
|
# Act
|
|
sender.send(to="user@example.com", subject="s", body="b")
|
|
|
|
# Assert
|
|
assert created[0].started_tls is True
|
|
assert created[0].login_args == ("AKIA_SMTP_USER", "smtp-password")
|