openvibers/openvibe.mail STATUS.json
STATUS.json
| 1 | { |
| 2 | "repository": "OpenVibers/OpenVibe.Mail", |
| 3 | "stage": "alpha", |
| 4 | "code": true, |
| 5 | "wave": "M1", |
| 6 | "plan": "OpenVibe Seven Domains Plan (2026-11-07) §8, M1: Stalwart receiving, masks, burners and agent inboxes; HO6 (channels): an agent answers the mail sent to its own address", |
| 7 | "runtime": "node 22, express, PostgreSQL 18 (openvibe-sdk/db), Stalwart Community 1.0.3 (AGPL-3.0-only), local filesystem blobs (S3 optional)", |
| 8 | "port": 5250, |
| 9 | "domain": "openvibe.email; receive-only MX. The API is public through nginx (deploy/nginx/openvibe.email.conf); no UI yet.", |
| 10 | "contracts": "openvibe-contracts v0.419.0", |
| 11 | "sdk": "openvibe-sdk v0.161.0", |
| 12 | "shared": "openvibe-shared v4.234.0", |
| 13 | "contractsProposed": [], |
| 14 | "stalwart": { |
| 15 | "version": "1.0.3", |
| 16 | "asset": "stalwart-x86_64-unknown-linux-gnu.tar.gz", |
| 17 | "sha256": "8ae481ff410cd09dd53d7f49f528dcad35d90b4b2ffb0c49c9ae23aaf43c4831", |
| 18 | "dataStore": "PostgreSQL (database ov_mail)", |
| 19 | "blobStore": "local filesystem /var/lib/openvibe-stalwart/blobs by default (STALWART_BLOB_STORE=fs); S3-compatible optional (Backblaze B2 in production, MinIO in CI)", |
| 20 | "listeners": "SMTP 25 inbound; JMAP/management 8080 and IMAP 143 on loopback; no submission, POP3, Sieve or HTTPS listener", |
| 21 | "spamFilter": "built-in, on", |
| 22 | "directory": "SQL over mail_directory_accounts: every burner is its own account (name = burner address); masks and agent inboxes are aliases of the owner's mailbox" |
| 23 | }, |
| 24 | "capabilities": [], |
| 25 | "capabilitiesRegistered": false, |
| 26 | "eventsProduced": [ |
| 27 | "mail.mask.created", |
| 28 | "mail.received", |
| 29 | "mail.agent_inbox.received" |
| 30 | ], |
| 31 | "eventsPayload": "mail.mask.created and mail.received are metadata only: neither carries a subject, body or code. mail.agent_inbox.received (HO6) carries the subject and a plain-text excerpt of at most 4000 characters for an inbox whose answering switch the person turned on; never the HTML, never an attachment, and nothing at all for spam or a blocked sender", |
| 32 | "limits": { |
| 33 | "masksPerPerson": 20, |
| 34 | "liveBurnersPerPerson": 5, |
| 35 | "accountlessBurnersPerIpPerHour": 10, |
| 36 | "accountlessBurnerTtl": "10m", |
| 37 | "liveAnonymousBurnersGlobal": 2000 |
| 38 | }, |
| 39 | "privacy": [ |
| 40 | "a burner's mail is isolated by its own Stalwart account, never by sender-written To/Cc/Delivered-To headers", |
| 41 | "the shared person mailbox matches only the first Delivered-To header Stalwart stamps", |
| 42 | "message bodies are never logged, stored by this service, served as HTML or put in an event; the messages API returns metadata only", |
| 43 | "a mask never forwards outside OpenVibe", |
| 44 | "the code extractor is the only reader of a body and drops it immediately", |
| 45 | "an expired burner's mail and account are deleted by a due-based purge job; a failure keeps the row for the next run", |
| 46 | "an agent's reply is recorded with its outcome (mail_sent_mail) and carries no attachment; a message body reaches an event only as the excerpt of an inbox whose answering is on", |
| 47 | "an IMAP app password is sealed with AES-256-GCM under MAIL_SECRETS_KEY (owner and connection id as AAD) before the INSERT; no API answer or log line carries it", |
| 48 | "an IMAP mailbox is read only: EXAMINE and BODY.PEEK, no STORE, MOVE, EXPUNGE or DELETE, and the TLS socket resolves through openvibe-shared/egress so a private address is refused" |
| 49 | ], |
| 50 | "tests": { |
| 51 | "unit": "node test/run.js (PGlite, in-process Network and mail server)", |
| 52 | "postgres": "MAIL_TEST_STORE=pg node test/run.js", |
| 53 | "acceptance": "node test/stalwart/run.js (pinned Stalwart + PostgreSQL, fs blobs by default and one S3 CI run; SMTP delivery, code, forged To/Cc/Delivered-To isolation, 300-message isolation, expiry account deletion and bounce, disabled-mask bounce, 404s, no body in logs, blob on disk)", |
| 54 | "frontdoor": "node test/frontdoor.test.js (CORS allow-list, same-site cookie write rule, /me/mail)", |
| 55 | "imap": "node test/imap.test.js (TLS required, private target refused, the seal is not returned or logged, UID resume, UIDVALIDITY reset, disconnect with and without the pulled summaries)" |
| 56 | }, |
| 57 | "deployed": false, |
| 58 | "deployment": "M1 is deliberately not deployed by this job. Opus installs it on the host; docs/INSTALL.md has the steps and docs/DNS.md the records.", |
| 59 | "security": [ |
| 60 | "the API is behind OpenVibe.Network auth; typed tokens are refused", |
| 61 | "someone else's address is 404, never 403", |
| 62 | "Stalwart runs in its own systemd slice: MemoryMax=1G, CPUQuota=100%, NoNewPrivileges, ProtectSystem=strict", |
| 63 | "the mail server has no password-authenticated listener and no relay: domains are allowRelaying=false and the submission/POP3/Sieve/HTTPS listeners are removed", |
| 64 | "a cookie-authenticated write must come from openvibe.email (Sec-Fetch-Site/Origin)", |
| 65 | "CORS is an exact allow-list from the Contracts manifests; no wildcard and no credentialed cross-origin request" |
| 66 | ], |
| 67 | "publicApi": { |
| 68 | "base": "https://openvibe.email", |
| 69 | "paths": [ |
| 70 | "/api/v1/", |
| 71 | "/api/health", |
| 72 | "/api/ready", |
| 73 | "/auth/" |
| 74 | ], |
| 75 | "placeholder": "/ answers a placeholder page until the UI lands", |
| 76 | "nginx": "deploy/nginx/openvibe.email.conf", |
| 77 | "cors": "Bearer from any first-party origin in the Contracts manifests; cookie writes only same-origin on openvibe.email", |
| 78 | "trustProxy": 2 |
| 79 | }, |
| 80 | "agentMail": { |
| 81 | "switch": "off by default, per agent; the poll walks only inboxes whose switch is on", |
| 82 | "poll": "due-based, under its own lease in mail_settings; a message is announced once (the row and the event commit together)", |
| 83 | "skips": "a message the mail server marked as spam, a sender on the person's block list, and the inbox's own address", |
| 84 | "read": "GET /internal/mail/agents/:agent_id/messages/:message_id (mail.agent.read), loopback only", |
| 85 | "reply": "POST /internal/mail/agents/:agent_id/reply (mail.agent.reply), from the inbox address, to the sender only, plain text", |
| 86 | "delivery": "a reply to the owner's own openvibe.email addresses (mailbox, masks, burners) is handed to the mail server over loopback SMTP. Sending outside OpenVibe is not ours yet (plan M2, waiting on the owner): MAIL_OUTBOUND=on is the only way on and it is OFF by default, so with it off such a reply opens no connection and answers 409 mail.outbound_closed, which the agent's system turns into the person's send approval (the reply waits, nothing is sent, nothing is lost)", |
| 87 | "outbound": "GET /internal/mail/outbound says whether Mail may send outside OpenVibe, so an approval card can say plainly that the reply will wait" |
| 88 | }, |
| 89 | "updated": "2026-11-13" |
| 90 | } |
| 91 |