diff --git a/papyrus/services/auth/verification.py b/papyrus/services/auth/verification.py index b774143..233fee4 100644 --- a/papyrus/services/auth/verification.py +++ b/papyrus/services/auth/verification.py @@ -192,10 +192,24 @@ async def deliver_pending_verifications(session: AsyncSession) -> int: job.next_attempt_at = min(now + timedelta(minutes=5), action.expires_at) await session.commit() url = _build_app_url("/verify-email", {"token": job.token}) + subject = "Verify your Papyrus email address" body = f"Verify your Papyrus email address.\n\n{url}\n\nThis link expires 24 hours after it was requested." try: - await asyncio.to_thread(email.send_email, action.email, "Verify your Papyrus email address", body) + html = email.render_email_html( + subject=subject, + preheader="Verify your email address to sign in to Papyrus.", + category="ACCOUNT SECURITY", + heading="Verify your email address", + greeting="Hello,", + summary="Verify your email address to sign in to Papyrus.", + guidance="This link expires 24 hours after it was requested and can only be used once.", + security_note="If you didn't request this email, you can ignore it.", + action_label="Verify email", + action_url=url, + ) + + await asyncio.to_thread(email.send_email, action.email, subject, body, html_body=html) except Exception: retry_at = min(_now() + timedelta(seconds=min(60 * 2 ** min(job.attempts - 1, 6), 3600)), action.expires_at) diff --git a/tests/api/routes/test_auth.py b/tests/api/routes/test_auth.py index c536848..8c700de 100644 --- a/tests/api/routes/test_auth.py +++ b/tests/api/routes/test_auth.py @@ -708,8 +708,17 @@ async def test_resend_verification_sends_email_when_configured( recipient, subject, body, html = configured_email_delivery[0] assert recipient == "test@example.com" assert subject == "Verify your Papyrus email address" - assert "/verify-email?token=" in body - assert html is None + url = next(line for line in body.splitlines() if "/verify-email?token=" in line) + assert html is not None + assert "" in html + assert "This is an automatic account notification from Papyrus." in html + assert f'href="{url}"' in html + assert "Verify email" in html + assert "24 hours after it was requested" in body + assert "24 hours after it was requested" in html + token = parse_qs(urlparse(url).query)["token"][0] + verified = await client.post("/v1/auth/verify-email", json={"token": token}) + assert verified.status_code == 200 async def test_forgot_password_returns_configuration_message( diff --git a/tests/services/test_verification.py b/tests/services/test_verification.py index 2d7f700..ce65b97 100644 --- a/tests/services/test_verification.py +++ b/tests/services/test_verification.py @@ -170,16 +170,19 @@ def fail(*args, **kwargs): assert "SMTP secret" not in caplog.text job.next_attempt_at = datetime.now(UTC) - timedelta(seconds=1) await session.commit() - monkeypatch.setattr(email, "send_email", lambda *args: sent.append(args)) + monkeypatch.setattr(email, "send_email", lambda *args, **kwargs: sent.append((*args, kwargs["html_body"]))) assert await verification.deliver_pending_verifications(session) == 1 - assert f"https://app.example.test/reader/verify-email?token={original_token}" in sent[0][2] + url = f"https://app.example.test/reader/verify-email?token={original_token}" + assert url in sent[0][2] + assert f'href="{url}"' in sent[0][3] + assert "Verify email" in sent[0][3] assert await session.scalar(select(VerificationDelivery)) is None token = await session.scalar(select(EmailActionToken)) assert token.expires_at == original_expiry async def test_expired_delivery_removed_without_smtp(test_session_maker, monkeypatch): - monkeypatch.setattr(email, "send_email", lambda *args: pytest.fail("expired mail sent")) + monkeypatch.setattr(email, "send_email", lambda *args, **kwargs: pytest.fail("expired mail sent")) async with test_session_maker() as session: await register(session) @@ -401,7 +404,7 @@ async def test_linked_google_preserves_owner_and_primary_email( async def test_delivery_lease_prevents_two_workers_sending_the_same_job(test_session_maker, monkeypatch): sent = [] - monkeypatch.setattr(email, "send_email", lambda *args: sent.append(args)) + monkeypatch.setattr(email, "send_email", lambda *args, **kwargs: sent.append((*args, kwargs["html_body"]))) async with test_session_maker() as session: await register(session)