"""Snooze: a conversation leaves the Inbox until a set time, then comes back.""" from __future__ import annotations import datetime as dt from sqlalchemy import select from app.models import Message, Thread from app.snooze import wake_due_job from tests.api.conftest import HDR, MailCtx, raw_mail def _in(days: float = 1) -> str: return (dt.datetime.now(dt.UTC) + dt.timedelta(days=days)).isoformat() async def _inbox_ids(ctx: MailCtx) -> list[str]: resp = await ctx.client.get("/api/threads", params={"view": "folder:inbox"}) return [t["id"] for t in resp.json()["threads"]] async def test_snoozed_thread_leaves_the_inbox(mail_ctx: MailCtx) -> None: old = await mail_ctx.deliver( raw_mail("Old"), 1, at=dt.datetime.now(dt.UTC) - dt.timedelta(days=2) ) new = await mail_ctx.deliver(raw_mail("New"), 2) c = mail_ctx.client resp = await c.post(f"/api/threads/{new.thread_id}/snooze", json={"until": _in()}, headers=HDR) assert resp.status_code == 200, resp.text assert await _inbox_ids(mail_ctx) == [str(old.thread_id)] snoozed = (await c.get("/api/threads", params={"view": "snoozed"})).json()["threads"] assert [t["id"] for t in snoozed] == [str(new.thread_id)] assert snoozed[0]["snoozed_until"] is not None counts = (await c.get("/api/mailboxes/counts")).json() assert counts["snoozed"] == 1 and counts["inbox_unread"] == 1 # Unsnooze: straight back, at the top. await c.post(f"/api/threads/{new.thread_id}/unsnooze", headers=HDR) assert await _inbox_ids(mail_ctx) == [str(new.thread_id), str(old.thread_id)] async def test_snooze_needs_a_future_time(mail_ctx: MailCtx) -> None: msg = await mail_ctx.deliver(raw_mail("x"), 1) for until in (_in(-1), _in(400), "2030-01-01T09:00:00"): resp = await mail_ctx.client.post( f"/api/threads/{msg.thread_id}/snooze", json={"until": until}, headers=HDR ) assert resp.status_code == 400, until async def test_due_thread_wakes_on_top_and_unread(mail_ctx: MailCtx) -> None: week_ago = dt.datetime.now(dt.UTC) - dt.timedelta(days=7) snoozed = await mail_ctx.deliver(raw_mail("Snoozed"), 1, seen=True, at=week_ago) newer = await mail_ctx.deliver(raw_mail("Newer"), 2, seen=True) thread = await mail_ctx.session.get(Thread, snoozed.thread_id) assert thread is not None thread.snoozed_until = dt.datetime.now(dt.UTC) - dt.timedelta(minutes=1) thread.snoozed_at = week_ago await mail_ctx.session.commit() assert await wake_due_job({}) >= 1 assert await _inbox_ids(mail_ctx) == [str(snoozed.thread_id), str(newer.thread_id)] snoozed_id = snoozed.id mail_ctx.session.expire_all() flags = await mail_ctx.session.scalar(select(Message.flags).where(Message.id == snoozed_id)) assert flags is not None and "\\Seen" not in flags async def test_a_reply_wakes_a_snoozed_thread(mail_ctx: MailCtx) -> None: first = await mail_ctx.deliver(raw_mail("Question", mid=""), 1) await mail_ctx.client.post( f"/api/threads/{first.thread_id}/snooze", json={"until": _in(3)}, headers=HDR ) m = raw_mail("Re: Question", headers={"In-Reply-To": "", "References": ""}) await mail_ctx.deliver(m, 2, at=dt.datetime.now(dt.UTC) + dt.timedelta(seconds=5)) await wake_due_job({}) assert await _inbox_ids(mail_ctx) == [str(first.thread_id)]