june-docs

Disposable Mail Engine (Stalwart RFC 8620 JMAP)

June features an integrated ephemeral email platform powered by Stalwart Mail Server using modern JMAP (JSON Meta Application Protocol, RFC 8620 & RFC 8621). This architecture replaces antiquated IMAP/POP3 polling with efficient, stateless JSON-over-HTTP requests and real-time event streaming.


Architectural Workflow

sequenceDiagram
    autonumber
    actor Sender as External Mail Sender
    participant MX as MX (temp.4ofl.dev)
    participant Stalwart as Stalwart JMAP Server
    participant June as June Application Core
    participant DB as SQLite DB
    participant SSE as Server-Sent Events Hub
    actor User as Discord / Web User

    User->>June: Create Inbox (/tempmail or Dashboard)
    June->>DB: Store Address Lease & Expiry (Fixed TTL: 60m)
    June-->>User: Return Allocated Address (e.g. 7f6899f...@temp.4ofl.dev)
    
    Sender->>MX: Deliver Email via SMTP
    MX->>Stalwart: Ingest & Parse MIME Message
    
    par Real-Time Push Notification
        Stalwart->>June: Webhook / JMAP Push Notification
        June->>SSE: Broadcast New Message Event
        SSE-->>User: Live Notification on Dashboard
    and On-Demand Query
        User->>June: Fetch Inbox / Read Message
        June->>Stalwart: JMAP Email/get Request (using resolved accountId)
        Stalwart-->>June: Return Parsed JSON (Headers, Text, HTML)
        June-->>User: Render Sanitized Email Viewer
    end

Why JMAP (RFC 8620)?

Legacy mail protocols like IMAP require persistent stateful socket connections, expensive bandwidth-heavy synchronization, and complex MIME parsing on the client.

JMAP provides distinct architectural advantages:

  1. JSON Native: Message payloads, headers, threading, and mailbox metadata are returned as structured JSON objects.
  2. Stateless HTTPS: Requests execute over standard TLS-encrypted HTTPS endpoints (/jmap/session), eliminating socket state issues across container restarts.
  3. Pipelining & Batching: Multiple calls (e.g. fetch mailbox list + list newest messages) execute in a single round-trip HTTP request.
  4. Memory Efficient: Payloads are streamed with strict size boundaries (MAX_JMAP_RESPONSE_BYTES = 2MB), preventing memory exhaustion.

Ingestion & Lifecycle Management

1. Ephemeral Address Allocation & SQLite Leases

When an inbox is created via /tempmail or the web dashboard:

2. Message Querying (Email/query and Email/get)

To fetch messages for an address, June’s JMAP client (src/utils/tempmail.js) first queries the JMAP session endpoint (/jmap/session) to extract the resolved account ID from session.primaryAccounts['urn:ietf:params:jmap:mail'] (e.g. "june"), and then constructs an authenticated JMAP invocation:

{
  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
  "methodCalls": [
    [
      "Email/query",
      {
        "accountId": "<resolved_account_id>",
        "filter": { "to": "7f6899f3a1@temp.4ofl.dev" },
        "sort": [{ "property": "receivedAt", "isAscending": false }],
        "limit": 10
      },
      "call-0"
    ],
    [
      "Email/get",
      {
        "accountId": "<resolved_account_id>",
        "#ids": {
          "resultOf": "call-0",
          "name": "Email/query",
          "path": "/ids"
        },
        "properties": ["id", "blobId", "threadId", "from", "to", "subject", "receivedAt", "preview", "hasAttachment"]
      },
      "call-1"
    ]
  ]
}

The server evaluates the query and returns the matching email headers in a single atomic response.

3. Expiry vs Storage Boundary

It is important to understand the boundary between June’s SQLite lease and Stalwart’s message store:

4. HTML Sanitization & Safety

When rendering email bodies on Discord or the web dashboard: