Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion papyrus/services/auth/verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
13 changes: 11 additions & 2 deletions tests/api/routes/test_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<!doctype html>" in html
assert "This is an automatic account notification from Papyrus." in html
assert f'href="{url}"' in html
assert "Verify email</a>" 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(
Expand Down
11 changes: 7 additions & 4 deletions tests/services/test_verification.py
Original file line number Diff line number Diff line change
Expand Up @@ -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</a>" 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)
Expand Down Expand Up @@ -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)
Expand Down
Loading