Domains, mailboxes, folders, storage and the outgoing queue.
The mail part of core owns the data the SMTP, IMAP and webmail parts read and write. Other modules reach it through the core handlers, never its tables directly.
Models
| Model | What it stores |
|---|---|
mail_domains | Domain name, status, catch-all address and the DKIM key pair. |
mail_mailboxes | Address, scrypt password hash, quota in MB and bytes used. |
mail_aliases | An address and the list of addresses it forwards to. |
mail_folders | Per-mailbox folders with IMAP uid_next, uid_validity and modseq. |
mail_messages | One row per message: folder, UID, flags, size, summary fields and a storage key. |
mail_blobs | Message content when the database driver is used. |
mail_queue | Outgoing mail per destination domain with retry state. |
New mailboxes get INBOX, Sent, Drafts, Junk, Trash and Archive, each with its special-use flag.
Storage
The raw message is kept apart from its row. storage.put(buffer) returns a key with a driver prefix, so changing the driver later does not break older messages.
| Driver | Key prefix | Where |
|---|---|---|
disk | disk: | data/mail/messages, or the folder set in the config. |
s3 | s3: | Any S3 compatible bucket. |
database | db: | The mail_blobs table. |
Handlers
| Handler | Main methods |
|---|---|
domains | create, update (pass rotate for a new DKIM key), remove, records(id) with a live DNS check. |
mailboxes | create, update, remove, authenticate(address, password, ip), resolve(address), senders(mailbox). |
store | Folder methods (list, folder, special, create, rename, drop) and message methods (deliver, read, rows, flag, copy, move, remove, expunge). |
outbound | send(sender, recipients, raw) delivers locally and queues the rest. process, retry, cancel. |
tls | get() returns the certificate and key from the config, or a self-signed pair for the hostname. |
authenticate blocks an address and IP pair after 10 failures in 15 minutes. resolve follows aliases, catch-all addresses and name+tag@ addresses, and returns the local mailbox IDs and any outside addresses.
store.deliver(mailboxId, raw, options) checks the quota, allocates the next UID in a locked transaction and emits mail.changed with { mailbox, folder }. IMAP IDLE listens for that event.
const store = this.oasis.modules.handlers.get("core", "store");
await store.deliver(mailbox.id, raw, { special: "sent", flags: ["\\Seen"] });Hostname
domains.hostname() is the name the servers use in their greeting, in Received headers, on the self-signed certificate and in the MX record. It is the hostname setting when one is set, otherwise mail. plus the first domain added, otherwise the machine name. Adding or removing a domain restarts the servers when the name changes.
records(id) lists the DNS records a domain needs. When Oasis runs in remote mode it also lists the A record that points the hostname at the server's public address.
Delivery
Outgoing mail is DKIM signed with the sender domain's key and grouped by destination domain. A clock runs outbound.process() every minute.
Mail goes through the relay host when one is set. Otherwise Oasis looks up the MX records and connects on port 25 with opportunistic TLS. A 5xx answer fails at once. Anything else retries after 5, 15, 30, 60, 120, 240 and 480 minutes until the retry window ends. Local senders get a bounce message when delivery fails.
Config
These keys are in the mail group of the core config.
| Section | Keys |
|---|---|
| Server | hostname (empty uses mail.<first domain>), max_size (MB), quota (MB) |
| Storage | driver, disk_path, s3_endpoint, s3_region, s3_bucket, s3_key, s3_secret, s3_path_style |
| TLS | tls_cert, tls_key (file paths) |
| Delivery | relay_host, relay_port, relay_user, relay_password, retry_hours |