concepts
Emails
Email parsing, storage, attachments, and the reading experience.
Email Storage
Emails are stored in PostgreSQL with fields for from_address, to_address, subject, body_text, body_html, raw_headers, size_bytes, spam_score, is_read, and has_attachments. spam_score is computed at ingest on a 0–10 scale from header completeness (Message-ID, Subject, Date, From), a Received-SPF failure, and a live SPF lookup against the connecting IP; it is recorded for reference and does not filter anything.
HTML Rendering
Inbound HTML is sanitized server-side at ingest with bluemonday's UGC policy (a permissive allowlist that strips scripts and dangerous attributes). The frontend then renders the sanitized HTML inside an <iframe sandbox="allow-popups"> — no scripts, no forms, no same-origin access; only opening a link in a new tab is permitted. A Content-Security-Policy meta tag injected into the frame defaults to default-src 'none' and blocks remote images (tracking pixels) until the reader clicks "Load images".
Attachments
Attachments are stored in S3-compatible object storage (MinIO by default, with a local-filesystem fallback) and served via presigned URLs. On the local-filesystem backend the download URL is served by the API under /api/v1/files instead:
- MIME parser extracts attachments from inbound email
- Settings resolver checks if attachments are allowed (org → domain assignment cascade)
- Files uploaded to object storage at
attachments/{email_id}/{random_uuid}/{sanitized_filename} - Download generates a time-limited presigned URL (configurable via
defaults.presigned_url_ttl)
Attachments with executable extensions are dropped at ingest — a fixed list of 16, checked before the size limit. See the SMTP pipeline for the full list.
API Operations
| Method | Endpoint | Description |
|---|---|---|
GET | /api/v1/inboxes/{id}/emails | List emails (paginated; pass q to search) |
POST | /api/v1/inboxes/{id}/emails/mark-all-read | Mark every email in the inbox as read |
GET | /api/v1/emails/{id} | Get email detail |
PATCH | /api/v1/emails/{id} | Mark read/unread |
DELETE | /api/v1/emails/{id} | Delete email + attachments |
GET | /api/v1/emails/{id}/attachments/{aid} | Get presigned download URL |
Full-Text Search
Pass q to the list endpoint to search within an inbox:
GET /api/v1/inboxes/{inboxId}/emails?q=invoice&page=1&per_page=20Search is backed by a PostgreSQL tsvector column on emails with a GIN index,
kept current by a trigger over subject, body_text and from_address. The
query runs through plainto_tsquery with the english configuration, so terms
are stemmed and combined with AND — operators, quoted phrases and prefix
matching are not supported. Queries longer than 200 characters are rejected with
400. Results paginate and sort newest-first like an unfiltered list.