The Go framework for building JMAP servers.
Build production JMAP servers with RFC 8620 (Core), RFC 8621 (Mail), and more with
pluggable storage, authentication, search, and delivery. Stdlib-only core.
Build complete JMAP servers without implementing the protocol yourself.
naust-jmap has no opinion about anything the RFCs are silent on. You supply object types, storage, authentication, search, and delivery through small interfaces; the runtime supplies protocol correctness: the session resource, request dispatch, standard method semantics, change tracking, state strings, and push.
- Datatypes are data, not code. Describe a type once and the runtime derives
its
/get,/changes,/set,/copy,/queryand/queryChangesmethods. - Protocol-complete. RFC 8620 end to end: session resource, batched requests, back-references, the full error catalog, blobs, and push.
- Bring your own infrastructure. Storage, authentication, blob persistence, leases, notifications and search are small interfaces with working defaults.
- No dependencies in
core. Standard library only, forever. Anything needing third-party code is a separate module, so you import only what you use. - The runtime owns correctness. Plugins own meaning. Backends own persistence.
- One node or a fleet. SQLite for a single process, Postgres with cross- instance leases and push for several.
- Mail included.
datatypes/mailimplements RFC 8621 - reading, composing, sending, and delivery over LMTP or HTTP ingest, and a tracked RFC compliance matrix.
flowchart TB
C["JMAP clients"]
subgraph TR["transport"]
H["HTTP<br/>/api, /eventsource"]
WS["websocket<br/>RFC 8887, optional"]
end
RT["Naust-JMAP (core)<br/>session, dispatch,<br/>the standard methods,<br/>state, blobs, push"]
subgraph EXT["what you plug in"]
DT["datatypes<br/>mail (RFC 8621), your own"]
PV["providers<br/>auth, backend, blob, lease, notify"]
end
C --> TR
TR --> RT
RT --> EXT
style WS stroke-dasharray: 5 5
core is the only required import. Everything below it is an interface with a
working in-process default, and everything beside it is a module you take only
if you want it - unimported code is absent from the binary and from the
dependency graph.
The runtime is the only required module:
go get github.com/naust-mail/naust-jmap/coreAdd what you actually serve. Each is its own module, and an unimported module never enters your binary or your dependency graph:
go get github.com/naust-mail/naust-jmap/datatypes/mail # RFC 8621 mail
go get github.com/naust-mail/naust-jmap/drivers/sqlite # single-node storage
go get github.com/naust-mail/naust-jmap/drivers/postgres # fleet storage
go get github.com/naust-mail/naust-jmap/capabilities/websocket # RFC 8887 transport
coreandcapabilities/websocketbuild on Go 1.24.datatypes/mail, both drivers and the examples require Go 1.25.
git clone https://github.com/naust-mail/naust-jmap
cd naust-jmap
go run ./examples/quickstartThen speak JMAP to it (user demo@example.com, password demo):
curl -su demo@example.com:demo http://localhost:8080/.well-known/jmapexamples/quickstart has the full request: it creates a
record, queries for it, and fetches it via a back-reference in one round trip.
Each module documents its own public API; the package documentation is the API reference; the site covers tasks. Nothing is duplicated across the three.
| You want | Look in |
|---|---|
| What a module provides and its public API | That module's README - core, datatypes/mail, drivers/sqlite, drivers/postgres, capabilities/websocket, capabilities/mdn |
| The API reference | pkg.go.dev |
| Working code | examples/ - quickstart, a full mail server, a two-instance cluster proof |
| How to accomplish a task | naust.email/naust-jmap - quickstart, auth, mail, websocket, fleet, reference |
| Method-by-method RFC status | reference matrices |
| Reporting a vulnerability | SECURITY.md, HARDENING.md |
Error handling is elided for brevity; the linked files check every call.
Describe it, register it, and the six standard methods exist. No method code:
// Define a type with two properties, one indexed and one with a default value
todo := &descriptor.Type{
Name: "Todo",
Capability: "urn:example:todo",
Properties: map[string]descriptor.Property{
"title": {Kind: descriptor.KindString, Indexed: true},
"done": {Kind: descriptor.KindBool, Indexed: true, Default: json.RawMessage(`false`)},
},
}
// Register it with the runtime, which derives the standard methods
proc := runtime.NewProcessor()
core := runtime.DefaultCoreCapabilities()
runtime.RegisterStandardType(proc, db, todo, core)
// Serve it over HTTP
srv, _ := runtime.NewServer(users, proc, "http://localhost:8080", core)
srv.Capability("urn:example:todo").Advertise(struct{}{}, struct{}{})
http.ListenAndServe("localhost:8080", srv)Full file: examples/quickstart.
The RFC 8621 types register the same way, into the same processor. Delivery
and submission are their own packages, datatypes/mail/deliver and
datatypes/mail/submit, imported only when you need them:
// Register the five RFC 8621 (mail) types. Each takes a config struct;
// RegisterEmail takes its search implementation explicitly - here the
// built-in substring search.
mail.RegisterMailbox(proc, mail.MailboxConfig{DB: db, Core: core})
mail.RegisterThread(proc, mail.ThreadConfig{DB: db, Core: core})
mail.RegisterEmail(proc, mail.EmailConfig{
DB: db, Store: blobs, Core: core,
AccountCapability: mail.DefaultAccountCapability(),
Searcher: search.New(blobs, search.DefaultConfig()),
})
mail.RegisterIdentity(proc, mail.IdentityConfig{DB: db, Core: core, Policy: policy})
// submit.Register returns the submission queue a sending worker reads.
// A nil Limits means submit.DefaultLimits().
queue, _ := submit.Register(proc, submit.Config{DB: db, Store: blobs, Core: core, Policy: policy})Full file: examples/mailserver, which also wires LMTP
and HTTP ingest so mail can actually arrive.
Storage is two lines, and nothing above them changes:
// Pick one backend:
store, _ := sqlite.Open("./naust-mail.db") // single process
// store, _ := postgres.Open(ctx, "postgres://...") // several processes
db := objectdb.New(store, lease.NewInProcess(store))
blobs, _ := chunkstore.New(store) // one of three blob storesSee choosing a blob store, and
examples/cluster for what changes with more than one node.
Neither driver fits? A backend is four methods over an ordered key-value store,
and drivers/ is the guide to writing one.
Requests and push on one connection instead of /api plus /eventsource:
ws := websocket.NewHandler(srv, users, websocket.Config{})
srv.Capability(websocket.CapabilityURI).
Advertise(websocket.SessionCapability(srv.BaseURL(), "/ws", ws.SupportsPush()), struct{}{})
mux.Handle("/ws", ws)Full module: capabilities/websocket.
One module per component. Drivers implement providers. Datatypes consume the runtime.
| Directory | What lives there | You... |
|---|---|---|
core/ |
The runtime library, one Go module, stdlib-only forever | import always |
core/providers/ |
The interfaces the runtime needs (storage, blobs, leases, notifications, auth), each with a built-in in-process implementation | pick or implement |
drivers/ |
Provider implementations that need third-party dependencies (sqlite, postgres), each its own module - and the guide to writing your own | import at most one or two |
datatypes/ |
JMAP datatypes served on top of the runtime (mail arrives here first), each its own module | import what you serve |
capabilities/ |
Optional protocol capabilities beside the core endpoints (the RFC 8887 WebSocket transport first), each its own module | import what you want |
examples/ |
Runnable servers: the quickstart above, a full mail server, and a two-instance cluster proof | read |
The core protocol (RFC 8620) is implemented end to end:
- The core protocol: session resource, request envelope, back-references, capability gating, request limits, strict I-JSON.
- The RFC derived standard methods over any registered datatype:
/get,/changes,/set,/copy,/query,/queryChanges. - Binary data (
Server.EnableBlobs): streaming upload/download endpoints andBlob/copy, with reference tracking and unreferenced-blob sweeping. Three blob stores ship, all behindblob.Store(see choosing one). - Push (
Server.EnablePush): the event-source endpoint, plus verifiedPushSubscriptionwebhooks with RFC 8291 encryption when given a subscription store and sender. - JMAP over WebSocket (RFC 8887): the optional, stdlib-only
capabilities/websocketmodule, sharingmaxConcurrentRequestswith the HTTP endpoint and adding StateChange push over the socket.
The core/ README carries the full RFC 8620 support
matrix and the runtime's recorded design decisions; datatypes/mail
carries the mail ones.
The first datatype module, datatypes/mail, implements RFC
8621: Mailbox, Thread, Email, Identity and EmailSubmission as descriptor types
over the derived RFC 8620 machinery, plus message delivery through LMTP and
HTTP ingest adapters and a sending worker. Its README carries the public API,
the RFC 8621 support matrix, and the calls the module makes where the spec is
silent.
A complete, persistent mail server - the sqlite driver, all five types, both
delivery adapters, sending, and push - is in
examples/mailserver:
go run ./examples/mailservernaust-jmap is pre-release (pre-1.0): modules version independently and
breaking changes may still land in minor bumps. The mail module (see Mail
above) reads, composes and sends, over either the sqlite or postgres driver
(the latter including a multi-node cluster hint layer); read receipts are
covered by capabilities/mdn (MDN send/parse, RFC
9007). Coming next, in order:
- S/MIME verification and quotas as further RFC 8621-family modules
Released under the Apache-2.0 license. See also NOTICE.