The webmail is now a PWA: web app manifest, icons (any + maskable, a status-bar badge), theme-coloured title bar following the chosen theme, and an app-icon badge with the unread Inbox count. The service worker is always installed; offline access is on only when it is registered as /sw.js?offline=1, so turning it on or off is an update that applies at once. Notifications (Settings → Accounts → Notifications, per device) use Web Push. Encryption (RFC 8291) and VAPID signing (RFC 8292) are implemented in app/push/webpush.py — pywebpush needs a newer cryptography than we pin — and checked against the RFC's worked example. The server makes its key pair on first use (private half encrypted with MASTER_KEY). New mail found by the live sync in an Inbox enqueues a job 5 s later that tells the user's devices about messages still unread there, under 2 hours old and not from the user; more than three become one "N new messages". A subscription belongs to the session it was made under: signed out or revoked, nothing more is sent there. Endpoints are limited to the known push services (no SSRF). Nothing shows while the app is in front (except on Apple, which requires it). Tested with real pushes through Google's service in Chromium and Opera. The mobile drawer gets "Install app": Chrome's own install dialog where the browser offers one, else short steps for iPhone/iPad, Opera and Firefox, with "Don't show this again". docs/nas-install.md §5.3 covers installing, notifications, Public URL and a later Android app (TWA). Migration 0022: push_subscriptions, app_settings.vapid_*. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
66 lines
2.1 KiB
Python
66 lines
2.1 KiB
Python
"""Worker job callables and queue function-sets (refactor §1, §6).
|
|
|
|
The process is launched via ``workers.run`` (``WORKER_ROLE`` decides which arq
|
|
workers run). This module only defines the job callables and the two queue
|
|
function-sets; the runner wires them onto the ``interactive`` / ``bulk`` queues.
|
|
The sync role runs the coordinator directly (see ``workers.run``), not via arq.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import uuid
|
|
from typing import Any
|
|
|
|
from arq import func
|
|
|
|
from app.outbox.draft_sync import push_draft_to_imap_job, remove_imap_draft_job
|
|
from app.outbox.jobs import file_sent_copy_job, send_job
|
|
from app.push.service import push_new_mail_job
|
|
from app.sync.actions import (
|
|
ACTION_MAX_TRIES,
|
|
delete_thread_job,
|
|
empty_folder_job,
|
|
move_job,
|
|
move_thread_to_folder_job,
|
|
push_flags_job,
|
|
restore_thread_job,
|
|
)
|
|
from app.sync.engine import bootstrap_and_backfill, refresh_account_job
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
async def sync_bootstrap_account(ctx: dict[str, Any], account_id: str) -> int:
|
|
"""Bootstrap + backfill an account (enqueued on account creation)."""
|
|
logger.info("sync.bootstrap_account start account=%s", account_id)
|
|
count = await bootstrap_and_backfill(uuid.UUID(account_id))
|
|
logger.info("sync.bootstrap_account done account=%s messages=%d", account_id, count)
|
|
return count
|
|
|
|
|
|
# Fast, interactive user actions (§6): short timeout, high concurrency. The ones
|
|
# pushing a change to the mail server retry (arq ``Retry``) for ~24 hours while
|
|
# the server is unreachable — more tries than arq's default of 5 allows.
|
|
_RETRYING_ACTIONS: list[Any] = [
|
|
push_flags_job,
|
|
move_job,
|
|
move_thread_to_folder_job,
|
|
restore_thread_job,
|
|
delete_thread_job,
|
|
empty_folder_job,
|
|
file_sent_copy_job,
|
|
]
|
|
|
|
INTERACTIVE_FUNCTIONS: list[Any] = [
|
|
*(func(job, max_tries=ACTION_MAX_TRIES) for job in _RETRYING_ACTIONS),
|
|
send_job,
|
|
push_draft_to_imap_job,
|
|
remove_imap_draft_job,
|
|
refresh_account_job,
|
|
push_new_mail_job,
|
|
]
|
|
|
|
# Bulk work (§6): long timeout, low concurrency, isolated from interactive jobs.
|
|
BULK_FUNCTIONS = [sync_bootstrap_account]
|