jmap

package module
v0.0.0-...-61c97f7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 9, 2026 License: MIT Imports: 11 Imported by: 0

README

go-jmap

CI Go Reference codecov

Work in progress — not yet usable. The API is unstable, incomplete, and will change without notice. Do not use this in production.

A Go client library for the JMAP protocol (RFC 8620).

Status

This library is in early development.

Example

package main

import (
	"context"
	"fmt"
	"log"
	"os"

	"github.com/rhyselsmore/go-jmap"
	"github.com/rhyselsmore/go-jmap/core"
	"github.com/rhyselsmore/go-jmap/mail"
)

func main() {
	// Create a client with bearer token authentication.
	client, err := jmap.NewClient(
		jmap.WithBearerTokenAuthentication(os.Getenv("FASTMAIL_API_TOKEN")),
		jmap.WithStaticResolver("https://api.fastmail.com"),
	)
	if err != nil {
		log.Fatal(err)
	}

	// Fetch the JMAP session. This is cached for subsequent calls.
	session, err := client.GetSession(context.Background())
	if err != nil {
		log.Fatal(err)
	}

	// Look up the primary mail account and inspect capabilities.
	accountId := session.PrimaryAccounts[mail.Capability]
	caps, _ := mail.GetAccountCapabilities(session.Accounts[accountId])
	fmt.Printf("Max mailbox depth: %v\n", caps.MaxMailboxDepth)

	// Build a request with two dependent calls: query all mailboxes,
	// then fetch them using a result reference — resolved server-side
	// in a single round trip.
	req := jmap.NewRequest(core.Capability, mail.Capability)

	q1 := &mail.MailboxQuery{
		AccountID: accountId,
	}
	req.Add(q1)

	q2 := &mail.MailboxGet{
		AccountID: accountId,
		IDRef:     jmap.Ref(q1, "/ids/*"),
	}
	req.Add(q2)

	if _, err = client.Do(context.Background(), req); err != nil {
		log.Fatal(err)
	}

	// Responses are available directly on the invocation objects.
	for _, mb := range q2.Response().List {
		fmt.Printf("Mailbox: %s (%s)\n", mb.Name, mb.ID)
	}
}

Packages

Package Description
github.com/rhyselsmore/go-jmap Core client, session, request/response envelope
github.com/rhyselsmore/go-jmap/contacts JMAP Contact capability (RFC 9610)
github.com/rhyselsmore/go-jmap/core JMAP Core capability (RFC 8620)
github.com/rhyselsmore/go-jmap/mail JMAP Mail methods — Mailbox, Email (RFC 8621)

Requirements

  • Go 1.26+

License

MIT

Documentation

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func GetAccountCapabilities

func GetAccountCapabilities[T any](acc Account, c Capability) (T, error)

GetAccountCapabilities decodes the account-level capability object for the given capability identifier from an Account. Account-level capabilities describe per-account constraints and permissions, which may differ from the server-level capabilities. Returns an error if the capability is not present or cannot be decoded.

func GetCapabilities

func GetCapabilities[T any](s Session, c Capability) (T, error)

GetCapabilities decodes the server-level capability object for the given capability identifier from the Session. The type parameter T should match the expected shape of the capability object as defined by the relevant RFC. Returns an error if the capability is not present or cannot be decoded.

Example
package main

import (
	"encoding/json"
	"fmt"

	jmap "github.com/rhyselsmore/go-jmap"
)

func main() {
	sess := jmap.Session{
		Capabilities: map[jmap.Capability]json.RawMessage{
			"urn:ietf:params:jmap:core": json.RawMessage(`{"maxSizeUpload":50000000}`),
		},
	}

	type CoreCapability struct {
		MaxSizeUpload int `json:"maxSizeUpload"`
	}

	cap, err := jmap.GetCapabilities[CoreCapability](sess, "urn:ietf:params:jmap:core")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(cap.MaxSizeUpload)
}
Output:
50000000

Types

type Account

type Account struct {
	Name                string                         `json:"name"`
	IsPersonal          bool                           `json:"isPersonal"`
	IsReadOnly          bool                           `json:"isReadOnly"`
	AccountCapabilities map[Capability]json.RawMessage `json:"accountCapabilities"`
}

Account represents a single JMAP account.

type Authenticator

type Authenticator interface {
	// Authenticate mutates req to add authentication credentials.
	Authenticate(req *http.Request) error
}

Authenticator signs outgoing HTTP requests, e.g. by adding an Authorization header. Implement this interface to support custom authentication schemes.

type BearerTokenAuthenticator

type BearerTokenAuthenticator struct {
	// contains filtered or unexported fields
}

BearerTokenAuthenticator implements Authenticator using a static Bearer token. Use NewBearerTokenAuthenticator or WithBearerTokenAuthentication to construct one.

func NewBearerTokenAuthenticator

func NewBearerTokenAuthenticator(token string) (*BearerTokenAuthenticator, error)

NewBearerTokenAuthenticator returns a BearerTokenAuthenticator for the given token. Returns an error if the token is empty or whitespace-only.

Example
package main

import (
	"fmt"

	jmap "github.com/rhyselsmore/go-jmap"
)

func main() {
	authn, err := jmap.NewBearerTokenAuthenticator("my-token")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(authn != nil)
}
Output:
true

func (*BearerTokenAuthenticator) Authenticate

func (b *BearerTokenAuthenticator) Authenticate(req *http.Request) error

type Capability

type Capability string

Capability is a JMAP capability identifier, typically a URN such as "urn:ietf:params:jmap:core" or "urn:ietf:params:jmap:mail". Capabilities are used as keys in the Session and Account objects to advertise server support and constraints, and are passed in the "using" field of requests.

type Client

type Client struct {
	// contains filtered or unexported fields
}

Client is a JMAP client. Use NewClient to construct one.

func NewClient

func NewClient(opts ...ClientOpt) (*Client, error)

NewClient constructs a Client by applying the given options and then validating that all required fields (resolver, authenticator) are set.

func (*Client) Do

func (cl *Client) Do(ctx context.Context, req *Request) (*Response, error)

Do executes a JMAP request against the server's API URL, decodes the response, and correlates each method response back to its originating invocation via [Response.correlate]. Returns an error for non-2xx HTTP status codes, JSON decode failures, or correlation errors.

func (*Client) GetSession

func (cl *Client) GetSession(ctx context.Context) (Session, error)

GetSession returns the cached JMAP session, fetching it from the server if the cache is empty or expired.

type ClientOpt

type ClientOpt func(*Client) error

ClientOpt is a functional option applied to a Client during construction.

func WithAuthenticator

func WithAuthenticator(authn Authenticator) ClientOpt

WithAuthenticator sets the Authenticator used to sign every outgoing request. This option is required; NewClient will return an error if it is not provided.

func WithBearerTokenAuthentication

func WithBearerTokenAuthentication(token string) ClientOpt

WithBearerTokenAuthentication is a convenience ClientOpt that creates a BearerTokenAuthenticator from the given token and sets it on the client.

func WithHTTPClient

func WithHTTPClient(cl *http.Client) ClientOpt

WithHTTPClient configures the Client to use the provided *http.Client instead of the default. The client is shallow-cloned to avoid mutating the caller's value.

func WithStaticResolver

func WithStaticResolver(raw string) ClientOpt

WithStaticResolver is a ClientOpt that configures the client to use a StaticResolver with the given HTTPS host URL.

type DefaultSessionCache

type DefaultSessionCache struct {
	// contains filtered or unexported fields
}

DefaultSessionCache is an in-memory SessionCache that refreshes the Session after a configurable TTL. It is safe for concurrent use.

func NewDefaultSessionCache

func NewDefaultSessionCache(ttl time.Duration) *DefaultSessionCache

NewDefaultSessionCache returns a DefaultSessionCache that caches the Session for the given TTL duration.

func (*DefaultSessionCache) Get

Get returns the cached Session if it is still within the TTL, otherwise calls fn to fetch a fresh one and stores it. Uses a double-checked lock to avoid redundant fetches under concurrent access.

func (*DefaultSessionCache) Invalidate

func (c *DefaultSessionCache) Invalidate(_ context.Context) error

Invalidate clears the cached Session so the next call to Get fetches a fresh one from the server.

type Invocation

type Invocation interface {
	// Name returns the JMAP method name, e.g. "Mailbox/query".
	Name() string

	// ID returns the client-specified call id (the 3rd element in the array).
	// If it returns "", the request will generate one automatically.
	ID() string

	// DecodeResponse decodes the raw JSON response into the invocation's
	// response type. The provided json.RawMessage is safe to mutate.
	DecodeResponse(b json.RawMessage) error
}

Invocation represents a single JMAP method invocation in a request.

type InvocationError

type InvocationError struct {
	CallID string // the call ID of the failed invocation
	Type   string `json:"type"`
	Detail string `json:"description,omitempty"`
}

InvocationError represents a server-side error returned for a single JMAP method invocation.

func (*InvocationError) Error

func (e *InvocationError) Error() string

type MethodResponse

type MethodResponse struct {
	Name string          // e.g. "Mailbox/get"
	Args json.RawMessage // raw JSON object of the method's result
	ID   string          // clientCallId, e.g. "c1"
}

MethodResponse represents a single JMAP method response triple: [ "Method/name", {args}, "clientCallId" ]

type Request

type Request struct {
	Using       []Capability
	MethodCalls []Invocation
	// contains filtered or unexported fields
}

Request is the top-level JMAP request envelope sent to the server.

func NewRequest

func NewRequest(using ...Capability) *Request

NewRequest creates a new request with the given capabilities.

Example
package main

import (
	"encoding/json"
	"fmt"

	jmap "github.com/rhyselsmore/go-jmap"
)

func main() {
	req := jmap.NewRequest("urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail")
	data, err := json.Marshal(req)
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(string(data))
}
Output:
{"using":["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],"methodCalls":[]}

func (*Request) Add

func (r *Request) Add(inv Invocation)

Add appends a method call to the request and assigns a unique call ID.

func (*Request) MarshalJSON

func (r *Request) MarshalJSON() ([]byte, error)

MarshalJSON converts the request into the JMAP wire format:

{
  "using": [...],
  "methodCalls": [
    ["Mailbox/query", {...}, "c1"],
    ...
  ]
}

type Resolver

type Resolver interface {
	Resolve(ctx context.Context) (*url.URL, error)
}

Resolver resolves the JMAP session URL for a given server. Implementations may perform DNS lookups, SRV record queries, or simply return a statically configured URL.

type Response

type Response struct {
	MethodResponses []MethodResponse `json:"methodResponses"`
	SessionState    string           `json:"sessionState"`
}

Response is the top-level JMAP response envelope.

func (*Response) UnmarshalJSON

func (r *Response) UnmarshalJSON(b []byte) error

UnmarshalJSON implements custom decoding for the JMAP response envelope.

type ResultReference

type ResultReference struct {
	// ResultOf is the call ID of the previous method invocation whose
	// result is being referenced.
	ResultOf string `json:"resultOf"`

	// Name is the method name of the previous invocation, e.g. "Mailbox/query".
	// The server uses this to verify the reference points to the expected method.
	Name string `json:"name"`

	// Path is a JSON Pointer (RFC 6901) into the result object, identifying
	// the value to extract. For example, "/ids/*" references the ids array
	// from a /query response.
	Path string `json:"path"`
}

ResultReference is a back-reference to the result of a previous method call within the same JMAP request, as defined in RFC 8620 Section 3.7. It allows the output of one invocation to be used as input to another, enabling dependent calls to be batched in a single round trip.

func Ref

func Ref(inv Invocation, path string) *ResultReference

Ref creates a ResultReference from an existing Invocation, deriving the call ID and method name automatically. The path is a JSON Pointer into the referenced invocation's response.

Example
package main

import (
	"encoding/json"
	"fmt"

	jmap "github.com/rhyselsmore/go-jmap"
)

func main() {
	// Ref creates a back-reference to use the output of one method call
	// as input to another within the same request.
	inv := &exampleInvocation{name: "Mailbox/query", id: "c1"}
	ref := jmap.Ref(inv, "/ids/*")
	fmt.Println(ref.ResultOf, ref.Name, ref.Path)
}

// exampleInvocation implements jmap.Invocation for examples.
type exampleInvocation struct {
	name string
	id   string
}

func (e *exampleInvocation) Name() string                           { return e.name }
func (e *exampleInvocation) ID() string                             { return e.id }
func (e *exampleInvocation) DecodeResponse(_ json.RawMessage) error { return nil }
Output:
c1 Mailbox/query /ids/*

type Session

type Session struct {

	// Capabilities advertises the capabilities the server supports,
	// keyed by capability URI (e.g., "urn:ietf:params:jmap:core").
	Capabilities map[Capability]json.RawMessage `json:"capabilities"`

	// Accounts lists accounts the user has access to, keyed by account ID.
	Accounts map[string]Account `json:"accounts"`

	// PrimaryAccounts maps each capability to the default account ID
	// for that capability.
	PrimaryAccounts map[Capability]string `json:"primaryAccounts"`

	// Username is the user’s primary identifier for this session.
	Username string `json:"username"`

	// APIUrl is the endpoint used for all JMAP method calls (POST).
	APIURL string `json:"apiUrl"`

	// DownloadURL is a template for downloading blobs.
	// Replace {accountId} and {blobId}.
	DownloadURL string `json:"downloadUrl"`

	// UploadURL is a template for uploading blobs.
	// Replace {accountId} and {blobId}.
	UploadURL string `json:"uploadUrl"`

	// EventSourceURL is the long-poll URL for push changes.
	EventSourceURL string `json:"eventSourceUrl"`

	// State is a string used to detect when the session object changes.
	State string `json:"state"`

	// Extensions can contain any unrecognized or server-specific fields.
	Extensions map[string]any `json:"-"`
	// contains filtered or unexported fields
}

Session represents the JMAP Session object defined in RFC 8620 §2.

type SessionCache

type SessionCache interface {
	// Get returns a cached Session, calling fn to fetch a fresh one if needed.
	Get(ctx context.Context, fn SessionGetter) (Session, error)
	// Invalidate discards the cached Session, forcing the next Get to refresh.
	Invalidate(ctx context.Context) error
}

SessionCache manages caching of the JMAP Session object. Implement this interface to provide custom caching strategies (e.g. Redis, per-tenant).

type SessionGetter

type SessionGetter func(ctx context.Context) (Session, error)

SessionGetter is a function that fetches a fresh Session from the server. It is passed to [SessionCache.Get] and called only when the cache is empty or expired.

type StaticResolver

type StaticResolver struct {
	// contains filtered or unexported fields
}

StaticResolver is a Resolver that returns a fixed JMAP session URL. It performs no network lookups; the URL is fully determined at construction time.

func NewStaticResolver

func NewStaticResolver(raw string) (*StaticResolver, error)

NewStaticResolver creates a Resolver that always returns the JMAP well-known URL for the given host. The input must be an HTTPS URL containing only a scheme and host (with optional port). Paths, query parameters, and fragments are not permitted.

Example:

r, err := NewStaticResolver("https://api.fastmail.com")
Example
package main

import (
	"fmt"

	jmap "github.com/rhyselsmore/go-jmap"
)

func main() {
	r, err := jmap.NewStaticResolver("https://api.fastmail.com")
	if err != nil {
		fmt.Println("error:", err)
		return
	}
	fmt.Println(r != nil)
}
Output:
true

func (*StaticResolver) Resolve

func (sr *StaticResolver) Resolve(_ context.Context) (*url.URL, error)

Resolve returns the pre-configured JMAP session URL. The context is accepted for interface compatibility but is not used.

Directories

Path Synopsis
protocol

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL