"""Snooze: a conversation leaves the Inbox until a chosen time, then comes back. Kept in this database only (the mail server knows nothing of it): a snoozed thread is hidden from the Inbox view and listed under Snoozed. The wake job, run every minute, brings back threads whose time has come — or that got a new message meanwhile, as Gmail does — at the top of the Inbox, the newest message marked unread. """ from __future__ import annotations import datetime as dt import logging import uuid from typing import Any from sqlalchemy import or_, select from app.db import session_scope from app.events import get_event_bus from app.models import Mailbox, Message, Thread from app.queue import enqueue_job from app.sync.actions import apply_flags_to_messages logger = logging.getLogger(__name__) MAX_SNOOZE = dt.timedelta(days=366) _SEEN = frozenset({"\\Seen"}) def snooze(thread: Thread, until: dt.datetime, now: dt.datetime) -> None: thread.snoozed_until = until thread.snoozed_at = now def wake(thread: Thread, now: dt.datetime) -> None: thread.snoozed_until = None thread.snoozed_at = None thread.unsnoozed_at = now async def wake_due_job(ctx: dict[str, Any]) -> int: """Cron (every minute): wake snoozed threads that are due. Returns how many.""" now = dt.datetime.now(dt.UTC) woken: list[tuple[uuid.UUID, uuid.UUID]] = [] unread: list[tuple[uuid.UUID, Message]] = [] async with session_scope() as session: due = list( await session.scalars( select(Thread) .where( Thread.snoozed_until.isnot(None), or_(Thread.snoozed_until <= now, Thread.last_message_at > Thread.snoozed_at), ) .with_for_update(skip_locked=True) ) ) for thread in due: on_time = thread.snoozed_until is not None and thread.snoozed_until <= now wake(thread, now) woken.append((thread.user_id, thread.id)) if not on_time: continue # a new message woke it: that message is unread already # Back as new: the newest Inbox message becomes unread again. newest = await session.scalar( select(Message) .join(Mailbox, Mailbox.id == Message.mailbox_id) .where( Message.thread_id == thread.id, Message.user_id == thread.user_id, Mailbox.special_use == "inbox", ) .order_by(Message.internal_date.desc()) .limit(1) ) if newest is not None and "\\Seen" in newest.flags: await apply_flags_to_messages(session, [newest], frozenset(), _SEEN) unread.append((thread.user_id, newest)) await session.commit() bus = get_event_bus() for user_id, message in unread: await enqueue_job("push_flags_job", str(message.id), [], ["\\Seen"]) await bus.publish( "flags.changed", {"user_id": str(user_id), "message_id": str(message.id), "flags": list(message.flags)}, ) for user_id, thread_id in woken: await bus.publish("thread.updated", {"user_id": str(user_id), "thread_id": str(thread_id)}) if woken: logger.info("woke %d snoozed thread(s)", len(woken)) return len(woken)