Files

93 lines
3.3 KiB
Python
Raw Permalink Normal View History

"""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)