mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-10 18:22:20 +02:00
* docs(specs): restore bootstrap after dev rewrite
* docs(specs): remove runtime inventory snapshot
* docs(specs): reconcile current dev truth
* docs(specs): document scheduled task actions as an owner-attribution source
Owner Attribution covered cookie, bearer-token and internal-loopback
requests. Scheduled task actions are a fourth source and behave
differently: _execute_action passes owner=task.owner off the stored
ScheduledTask row, so no request and no resolved principal are in
flight, and route-level require_user() never runs.
Webhook triggers are the sharp case. They are unauthenticated by
design with the token as the only credential and execute under the
stored task.owner.
Paths cite routes/task/task_routes.py, the canonical location after
the task subpackage move (#6081); routes/task_routes.py on current dev
is the backward-compat shim.
* docs(specs): add chained tasks to the trigger list, refresh dev stamp
Review feedback from RaresKeY on the previous commit.
"Every trigger path" was too broad: success-chained tasks are another
path into _execute_action. Added them with their own citation, and
noted that chaining additionally requires the target task to share
task.owner and rejects cycles, which is stricter than the trigger-side
checks. Softened the lead-in to "these trigger paths".
Line 56 still pointed at routes/task_routes.py for webhook credential
validation. That path is the backward-compat shim on current dev after
the task subpackage move (#6081); repointed to the canonical
routes/task/task_routes.py.
Stamp moved to dev@2a6b09b. Inspection backing that bump was scoped:
every file path cited in this spec was mechanically checked to resolve
on 2a6b09b, and every file:line in the Owner Attribution additions was
read against it. Behavioral claims elsewhere in the file were not
re-audited.
* docs(specs): correct SECURE_COOKIES description to match current behavior
Third of the stale details RaresKeY enumerated. The cookie section
described SECURE_COOKIES as purely opt-in, which stopped being true.
_secure_cookie() (routes/auth_routes.py:89) treats an explicit true or
false as authoritative and derives the Secure attribute from the
request otherwise, including when the variable is unset and when
docker-compose injects it present-but-empty. Either the connection
scheme or the first X-Forwarded-Proto hop being https is enough.
* docs(specs): refresh current dev truth
---------
Co-authored-by: StressTestor <212606152+StressTestor@users.noreply.github.com>
210 lines
20 KiB
Markdown
210 lines
20 KiB
Markdown
# Email And Contacts
|
|
|
|
Last updated: dev@2e2bb52 | 2026-08-16
|
|
|
|
## Scope
|
|
|
|
This spec covers mail and contacts in:
|
|
|
|
- app wiring in `app.py`;
|
|
- `core.database.EmailAccount`;
|
|
- `routes/email_routes.py`, `routes/email_helpers.py`, and `routes/email_pollers.py`;
|
|
- email threading in `src/email_thread_parser.py`;
|
|
- email MCP tools in `mcp_servers/email_server.py`;
|
|
- canonical contact/CardDAV routes in `routes/contacts/contacts_routes.py`,
|
|
with `routes/contacts_routes.py` as a compatibility shim;
|
|
- Codex email bridge in `routes/codex_routes.py`;
|
|
- document signed-reply flows in canonical `routes/document/document_routes.py` and document `source_email_*` fields;
|
|
- reminder/task email senders in `routes/note_routes.py` and `src/task_scheduler.py`;
|
|
- email/contact agent surfaces in `src/tool_implementations.py`, `src/tool_schemas.py`, `src/tool_index.py`, and `src/agent_loop.py`;
|
|
- CLI wrappers `scripts/odysseus-mail` and `scripts/odysseus-contacts`;
|
|
- frontend modules `static/js/emailInbox.js`, `static/js/emailLibrary.js`, `static/js/emailLibrary/*`, `static/js/emailShared.js`, `static/js/chatStream.js`, `static/js/document.js`, and `static/js/settings.js`;
|
|
- tests under `tests/test_email_*`, `tests/test_contacts_*`, `tests/test_mail_cli_*`, `tests/test_mcp_email_*`, `tests/test_schedule_email_*`, email/contact JS tests, and email security regressions.
|
|
|
|
## Current Call Sites Include
|
|
|
|
- browser email inbox/library, compose, schedule, account, and attachment actions;
|
|
- document-editor compose, recipient autocomplete, compose uploads, and signed-reply handoff;
|
|
- Codex email read/draft/send routes using API-token scopes;
|
|
- note reminder and task-output email delivery;
|
|
- built-in email summary/reply/calendar/urgency actions;
|
|
- scheduled email pollers and CLI one-shot pollers;
|
|
- MCP email tools;
|
|
- contact manager settings, compose contact autocomplete, agent contact tools, and contacts CLI.
|
|
|
|
## Email Accounts And Transport
|
|
|
|
`EmailAccount` rows own IMAP/SMTP configuration. Password fields are string columns containing encrypted ciphertext written with `src.secret_storage`; startup migrations handle legacy plaintext rows. Google OAuth account rows also carry `oauth_provider`, encrypted access/refresh tokens, token expiry, and an optional outbound `display_name`. Do not return decrypted credentials or OAuth tokens, or write them to logs.
|
|
|
|
Exactly one default account per owner is enforced as a serialized database transition. Startup normalizes legacy duplicate defaults and installs a unique per-owner default constraint/index; first create, delete/promotion, set-default, demo teardown, and owner rename lock the relevant owner rows and commit atomically. Multi-owner rename acquires locks in canonical order so stale concurrent writers fail closed.
|
|
|
|
`routes.email_helpers` owns:
|
|
|
|
- account owner assertions and config fallback order;
|
|
- IMAP/SMTP connection helpers and related transport utilities;
|
|
- Google OAuth2 state signing/verification, token refresh, and XOAUTH2 framing;
|
|
- SMTP security modes (`ssl`, `starttls`, `none`);
|
|
- envelope recipients and Odysseus headers;
|
|
- attachment extraction helpers;
|
|
- email pre-retrieval context for AI reply drafting;
|
|
- scheduled email, summary, reply, tag, calendar extraction, urgency, and signature-boundary side databases.
|
|
|
|
Email config can fall back to legacy `data/settings.json` or environment variables when no scoped account is configured. Account discovery now owner-scopes the default/first-enabled fallback and can still match legacy account rows by IMAP username or from-address. That fallback remains compatibility-sensitive in multi-user contexts.
|
|
|
|
Email owner semantics are route-local and compatibility-sensitive:
|
|
|
|
- `routes.email_helpers._require_auth()` returns `""` in `AUTH_ENABLED=false` mode, rejects configured auth with no user, and only tolerates first-run anonymous loopback fallback.
|
|
- Empty owner is treated as single-user compatibility: account-ownership assertions no-op, default/first-enabled account fallback can be global, and email cache clauses include `owner = '' OR owner IS NULL`.
|
|
- Non-empty owners scope account/config/cache queries. Legacy ownerless account
|
|
rows are visible to an authenticated owner only when the row's IMAP username
|
|
or from-address matches that owner, so old unowned rows do not become global
|
|
cross-user accounts in configured multi-user deployments.
|
|
|
|
`routes.email_routes` owns the HTTP mail surface:
|
|
|
|
- account CRUD, test, default, and masked config reads;
|
|
- Google OAuth authorize/callback for Workspace and .edu Gmail-style accounts;
|
|
- list, search, read, folders, and contacts;
|
|
- folder role resolution and UID fetch/search helpers used by the route surface;
|
|
- owner-scoped route caches and IMAP pool behavior;
|
|
- attachments, bulk attachment ZIP downloads, and attachment-to-document flows;
|
|
- compose upload, draft/send, `wait_for_delivery`, Sent append, and source `\Answered` marking;
|
|
- schedule/list/delete scheduled emails;
|
|
- pending agent-draft approval/cancel flows;
|
|
- mark read/unread/answered, spam flags, move, archive, and delete. IMAP move/delete/archive operations use UID commands for message identity and fail safe when the requested UID no longer exists; they never reinterpret a missing UID as a sequence number, which could mutate or expunge an unrelated message.
|
|
|
|
Google OAuth behavior is account-owned:
|
|
|
|
- `/api/email/oauth/google/authorize` requires an authenticated owner, checks account ownership, HMAC-signs state with account id, owner, and nonce, and redirects to Google with mail/userinfo scopes;
|
|
- `/api/email/oauth/google/callback` verifies signed state before token exchange, re-checks the target account owner before writing tokens, stores access/refresh tokens encrypted, stores token expiry as a timestamp, and redirects with generic success/error codes rather than raw provider errors;
|
|
- token refresh uses `GOOGLE_OAUTH_CLIENT_ID` and `GOOGLE_OAUTH_CLIENT_SECRET`, stores refreshed access tokens encrypted, and logs only generic/account-id context on failures;
|
|
- SMTP and IMAP use XOAUTH2 when `oauth_provider == "google"`; OAuth accounts are send-capable without an SMTP password when host and user are configured;
|
|
- outbound mail formats the `From` header with `display_name` when present.
|
|
- authorize/callback redirect URIs derive their scheme and host from the mounted request unless `GOOGLE_OAUTH_REDIRECT_URI` explicitly pins a value; the browser preserves the selected SMTP security mode during connect and reopens Settings after the callback.
|
|
|
|
MCP full-message read/reply/attachment fetches use IMAP `BODY.PEEK[]` rather than bare `RFC822`, so iCloud-style servers return the full body without marking messages seen. Poller UID handling must tolerate both bytes and string UIDs. Built-in signature-learning and daily-brief actions also use UID SEARCH/FETCH rather than sequence-number commands.
|
|
|
|
IMAP helpers quote mailbox names, raise the Python IMAP line cap for large messages, close sockets after connect/login failures, and preserve Gmail FETCH attributes that follow header literals so unread flag state is not lost. Browser list routes offload blocking IMAP work from async handlers; browser search runs in FastAPI's threadpool, rejects CRLF query input, tokenizes quoted phrases/terms, searches FROM/TO/CC/SUBJECT/TEXT, can search Gmail All Mail when an INBOX query should include archived or labelled messages, and supports `scope=folder` when callers intentionally want the selected folder only. The local index fallback can return indexed results when IMAP returns empty or fails.
|
|
|
|
## Runtime And Pollers
|
|
|
|
Scheduled email rows live in `data/scheduled_emails.db` and are owner-scoped. Scheduled send times are normalized before storage.
|
|
|
|
`routes.email_pollers` owns the scheduled-send poller and single-shot/task/CLI automation passes. Before SMTP work, each poller atomically claims a due row with a conditional `pending` to `sending` update; concurrent in-process/CLI pollers that lose the claim skip the row instead of sending a duplicate. Only the scheduled-send poller starts in-process by default when `ODYSSEUS_INPROCESS_POLLERS` allows it; Docker forwards that gate. Background email automation can also consult the foreground activity gate so auto actions do not compete with active browser/model work. Native cron/systemd can drive one-shot pollers through `scripts/odysseus-mail`.
|
|
|
|
Manual and scheduled summaries use the shared LLM adapter and owner-scoped cache instead of constructing provider calls locally. Scheduled summaries use background fallback policy and yield to foreground work; provider exception text is shaped before it can reach the browser.
|
|
|
|
Urgency delivery publishes through a serialized atomic checkpoint transaction. Generation and membership fences prevent stale scans from overwriting newer state; authoritative scans retire deleted/disabled accounts, partial failures preserve the prior checkpoint, concurrent account-scoped actions merge disjoint facts, and cancellation rolls back without publishing.
|
|
|
|
Transport degraded behavior:
|
|
|
|
- IMAP timeouts are clamped by configuration;
|
|
- providers can use implicit SSL, STARTTLS, or plain connections;
|
|
- poisoned IMAP sockets are reconnected around known provider failures;
|
|
- SMTP-capable account fallback is used where supported;
|
|
- route helpers, MCP, and CLI do not all share identical SMTP/IMAP parsing and security behavior today.
|
|
|
|
## Caching And Staleness
|
|
|
|
Email list/read behavior uses short route caches, longer read caches, capped warm prefetch, and owner/account-aware pool/cache keys. The frontend email library has its own session SWR cache, cache-buster refreshes, scheduled/search cache exclusions, and stale-row behavior when refresh fails.
|
|
|
|
Opening an unread message is one authoritative backend IMAP operation. The read route fetches/parses the message and applies `\Seen` over the same connection; cached bodies still await one UID STORE, read-only mailboxes serve content without claiming a mark, and STORE failure returns the body with explicit failure state rather than caching a false read. Inbox/library clients deduplicate opens, carry immutable mailbox context, and ignore late responses after account, folder, or message changes.
|
|
|
|
Library prewarm runs only while genuinely idle, as one bounded single-flight request for the default or last-used enabled account and initial page. Visible foreground work, panel lifecycle, account changes, or explicit reads cancel or join it so delayed duplicate IMAP work cannot escape the idle gate.
|
|
|
|
List/read route caches are owner/account-aware. Helper-side summary, AI-reply, tag, calendar-extraction, urgency-alert, and learned sender-signature tables carry owner columns and owner clauses. Thread-boundary rows are still keyed by message shape rather than a full owner/account/mailbox key, so they remain cross-owner audit points when identical messages appear in multiple mailboxes.
|
|
|
|
## Attachments And Signed Replies
|
|
|
|
Compose uploads live under `ODYSSEUS_MAIL_ATTACHMENTS_DIR`; missing staged files are skipped with warnings. Attachment-to-document supports PDF, DOCX, TXT, and MD. DOCX depends on `python-docx`; PDF form/open-in-doc flows can depend on optional PyMuPDF.
|
|
|
|
Email attachment-as-document flows stamp `Document.source_email_*` provenance. `GET /api/email/attachments-download/{uid}` builds an owner-scoped ZIP of visible non-signature attachments using safe names. `compose-from-odysseus` and `compose-from-odysseus-zip` can stage owner-visible documents and gallery images as compose uploads, preserving legacy session fallback only where the source object remains visible to the owner. `prepare-signed-reply` verifies document ownership, reconstructs reply headers, flattens/stages signed PDFs as compose uploads, and leaves final send/draft review to the compose flow.
|
|
|
|
Email bodies and attachments are untrusted model context.
|
|
|
|
## Threading And Rendering
|
|
|
|
`src.email_thread_parser` owns splitting plaintext/HTML email threads into quoted conversation parts. Frontend email library modules own reply-recipient logic, signature folding, local state, and rendering behavior. Bulk selections are cleared when folder/account loads, search text, search pills, or result scope changes so actions cannot carry stale UIDs into a different visible context. `static/js/emailShared.js` owns shared email UI helpers used across inbox/library surfaces.
|
|
|
|
Remote inbound email HTML is sanitized by frontend email-library utilities before `innerHTML` insertion. Server-side email routes sanitize composed/generated outbound HTML with an allowlist before draft/send, dropping scripts/styles and unsafe attributes. Both sides are part of the rendering invariant.
|
|
|
|
When the email reader is active, browser chat sends selected-message metadata. `src.tool_implementations` stores that request-local active email reference, `src.agent_loop` injects it as protected untrusted context, and `static/js/chatStream.js` handles `ui_control open_email_reply` so default reply/draft behavior opens the selected message's compose flow instead of a generic new document.
|
|
|
|
## MCP Email
|
|
|
|
`mcp_servers/email_server.py` exposes email tools for MCP/agent use. It has its own account discovery, IMAP/SMTP, attachment, cache, and send paths, but account visibility now mirrors the HTTP owner policy. The active owner comes from a hidden `_odysseus_owner` argument when the caller provides one, or from `ODYSSEUS_MCP_EMAIL_OWNER` / `ODYSSEUS_EMAIL_OWNER`. If any enabled account is owner-scoped and no current/configured owner exists, email MCP returns an owner-scope error instead of listing global accounts.
|
|
|
|
MCP email account filtering includes owner-owned rows and legacy ownerless rows
|
|
whose mailbox/from-address matches the owner. Confirmation-first `send_email`
|
|
resolves the selected account before stashing an `agent_draft`, so drafts cannot
|
|
be staged against another owner's account. MCP-created draft documents use the
|
|
resolved hidden/configured owner when available, with `ODYSSEUS_DOCUMENT_OWNER`
|
|
and single-admin fallback only as document-visibility compatibility.
|
|
|
|
MCP email send behavior is confirmation-first by default: `send_email` and reply send paths stash a `scheduled_emails` row with `status='agent_draft'` when `agent_email_confirm` is true, and browser routes expose pending drafts for approval or cancellation. Separate MCP draft tools create Odysseus compose documents for user review without sending.
|
|
|
|
MCP email remains a separate local/admin trust boundary. Public and non-admin users must not see or execute email MCP tools. It still needs route-helper parity audits for attachment path containment, sanitization, transport behavior, and pending-draft result text, but global all-account behavior is no longer the current owner model.
|
|
|
|
## Contacts
|
|
|
|
`routes.contacts.contacts_routes` owns global/admin contacts and CardDAV behavior. The top-level `routes.contacts_routes` module is a compatibility shim. The canonical package supports local contacts, CardDAV config, list/search/add/update/delete, VCF/CSV import/export, and clear.
|
|
|
|
Contact runtime behavior:
|
|
|
|
- contacts routes are admin-gated;
|
|
- local `data/contacts.json` is used when CardDAV is unconfigured;
|
|
- import paths tolerate malformed or non-string contact bodies by skipping invalid rows instead of crashing the import;
|
|
- configured CardDAV uses REPORT with GET fallback and a short in-memory cache;
|
|
- configured-but-offline CardDAV can return cached reads but writes fail instead of falling back to local JSON;
|
|
- CardDAV config reads mask the password, settings-stored passwords are encrypted with `src.secret_storage`, omitted password updates preserve the existing secret, and an explicit empty password clears it;
|
|
- the native contacts CLI is CardDAV-oriented and does not fully match web JSON fallback behavior;
|
|
- agent contact tools reuse helper functions in-process because the HTTP routes require browser/admin auth.
|
|
|
|
Contacts are global admin-only data today. There is no per-user contact sharing model unless a future spec defines one.
|
|
|
|
## Security Policy
|
|
|
|
Email HTTP access is owner-scoped, including account selection, scheduled email rows, and attachment routes. Null-owner/single-user compatibility paths are security-sensitive and must not allow cross-user mailbox access.
|
|
|
|
Codex email routes are the scoped bearer-token email API. They enforce `email:read`, `email:draft`, and `email:send` scopes and use token-owner attribution before borrowing email route handlers.
|
|
|
|
Known security policy details:
|
|
|
|
- decrypted email credentials stay process-local;
|
|
- account/config reads mask passwords and expose only OAuth status fields, not access or refresh token values;
|
|
- SMTP/IMAP security mode behavior is part of the credential contract;
|
|
- Google OAuth state and callback owner checks are part of the account-boundary contract;
|
|
- scheduled emails must remain owner-scoped;
|
|
- email pre-retrieval contacts context is allowed only for admin/single-user situations;
|
|
- MCP attachment downloads need route-level path-containment parity; current MCP paths are separate from the HTTP compose/attachment helper path.
|
|
|
|
CardDAV credentials and URLs are security-sensitive. CardDAV URL setup and derived href writes/deletes pass through outbound URL validation; absolute hrefs from a CardDAV server are constrained back to the configured origin before credentials are reused. CardDAV passwords in settings are encrypted and masked on read; environment-sourced legacy password values are used as supplied.
|
|
|
|
## Degraded Behavior
|
|
|
|
- IMAP/SMTP providers can be slow or inconsistent; folder resolution, pooled connections, and reconnect behavior should fail with clear errors.
|
|
- Google OAuth requires external Google endpoints plus configured `GOOGLE_OAUTH_CLIENT_ID`/`GOOGLE_OAUTH_CLIENT_SECRET`; missing client credentials or refresh failures degrade to reconnect-required or generic OAuth error paths.
|
|
- Scheduled email delivery depends on `scheduled_emails.db`, poller runtime, and configured SMTP.
|
|
- Attachment handling must tolerate missing staged files, unsupported formats, and inaccessible remote messages.
|
|
- CardDAV local fallback applies only when CardDAV is unconfigured; configured CardDAV outages are not treated as local-write mode.
|
|
- Multi-account list/search behavior can be sequential and cache-sensitive.
|
|
|
|
## Testing Coverage
|
|
|
|
Existing coverage includes header/envelope/IMAP/SMTP behavior, serialized default accounts, Google OAuth state/callback/token-refresh/XOAUTH2/redirect/settings behavior, shared-adapter summaries, authoritative read/mark-seen and frontend dedup, idle prewarm, UID-only mutations, scheduled-email claims and urgency checkpoint transactions, MCP full-message/owner behavior, owner scope/caches/signatures, thread/sanitizer behavior, CardDAV password encryption, mail CLI behavior, contacts basics, and selected frontend/security regressions.
|
|
|
|
Route-level and duplicate-path coverage is still thin for email list/read/search/mutations, account CRUD/security outside the OAuth path, send/draft security, attachments, scheduled-poller failures, contacts admin/CardDAV routes, MCP account/scope behavior, CardDAV degraded mode, and executable frontend behavior.
|
|
|
|
## Current Gaps
|
|
|
|
- Owner-keyed cache policy still needs an explicit decision for thread boundaries, plus continued migration/query audits for every email side table.
|
|
- CardDAV still needs redirect/proxy policy and broader route-level tests for URL validation, private-address blocking configuration, and same-origin href enforcement.
|
|
- MCP email needs continued route-helper parity for attachment path containment,
|
|
sanitization, transport behavior, and pending-draft result text.
|
|
- Empty-owner route compatibility and ownerless email cache rows need
|
|
end-to-end owner-boundary tests.
|
|
- CLI send/contact paths need parity decisions for SMTP security, recipient parsing, local fallback, and normalized contact shapes.
|
|
- Email HTTP route coverage is concentrated in scheduling/account-test helpers rather than full list/read/search/mutation/send/draft/account/attachment flows.
|
|
- Contacts coverage lacks admin-gate, config masking, import/export, CardDAV fallback, and CardDAV write-failure tests.
|
|
- Multi-account performance and cache staleness remain known audit areas.
|