jmapcore

package
v2.4.5 Latest Latest
Warning

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

Go to latest
Published: Oct 31, 2026 License: AGPL-3.0 Imports: 17 Imported by: 0

Documentation

Overview

Package jmapcore is the JMAP protocol layer: the wire types of RFC 8620 (core) and RFC 8621 (mail), and the encoding rules around them.

It imports nothing from yarilo. The package is meant to become a standalone library, so a server's config, storage and logging reach it only as plain arguments.

Index

Constants

View Source
const (
	// ErrCannotCalculateChanges is the honest refusal: the client refetches.
	// Every way of not knowing ends here rather than in an empty answer.
	ErrCannotCalculateChanges = "cannotCalculateChanges"
	ErrTooManyChanges         = "tooManyChanges"
)

Errors a Foo/changes can raise (RFC 8620 §5.2).

View Source
const (
	ErrUnknownMethod               = "unknownMethod"
	ErrInvalidArguments            = "invalidArguments"
	ErrInvalidResultReference      = "invalidResultReference"
	ErrServerFail                  = "serverFail"
	ErrAccountNotFound             = "accountNotFound"
	ErrAccountNotSupportedByMethod = "accountNotSupportedByMethod"
	ErrRequestTooLarge             = "requestTooLarge"
	// ErrServerUnavailable is the temporary one (RFC 8620 §3.6.2): the client
	// may retry the same call unchanged. Distinct from serverFail, which a
	// client is entitled to treat as final -- an index still catching up is
	// not the same news as a broken lookup.
	ErrServerUnavailable = "serverUnavailable"
)

Method error types this package raises itself. A server adds its own.

View Source
const (
	RoleAll       = "all"
	RoleArchive   = "archive"
	RoleDrafts    = "drafts"
	RoleFlagged   = "flagged"
	RoleImportant = "important"
	RoleInbox     = "inbox"
	RoleJunk      = "junk"
	RoleSent      = "sent"
	RoleTrash     = "trash"
)

Mailbox roles registered for JMAP (RFC 8621 §2, IANA "JMAP Mailbox Roles").

View Source
const (
	ErrUnsupportedFilter = "unsupportedFilter"
	ErrUnsupportedSort   = "unsupportedSort"
)

Query error types raised by a Foo/query (RFC 8620 §5.5).

View Source
const (
	ProblemUnknownCapability = "urn:ietf:params:jmap:error:unknownCapability"
	ProblemNotJSON           = "urn:ietf:params:jmap:error:notJSON"
	ProblemNotRequest        = "urn:ietf:params:jmap:error:notRequest"
	ProblemLimit             = "urn:ietf:params:jmap:error:limit"
)

JMAP names its own problem types under this prefix (RFC 8620 §3.6.1).

View Source
const (
	QuotaResourceOctets = "octets"
	QuotaResourceCount  = "count"
)

Resource types of RFC 9425 §1.3. "octets" counts storage, "count" objects.

View Source
const (
	CapCore = "urn:ietf:params:jmap:core"
	CapMail = "urn:ietf:params:jmap:mail"
)

Capability URNs advertised in the session resource.

View Source
const (
	SetErrForbidden    = "forbidden"
	SetErrOverQuota    = "overQuota"
	SetErrTooLarge     = "tooLarge"
	SetErrNotFound     = "notFound"
	SetErrInvalidPatch = "invalidPatch"
	SetErrWillDestroy  = "willDestroy"
	SetErrInvalidProps = "invalidProperties"
	SetErrSingleton    = "singleton"
	// SetErrAlreadyExists, SetErrMailboxHasChild and SetErrMailboxHasEmail are
	// RFC 8621 §2.5's for Mailbox/set.
	SetErrAlreadyExists   = "alreadyExists"
	SetErrMailboxHasChild = "mailboxHasChild"
	SetErrMailboxHasEmail = "mailboxHasEmail"
	// SetErrNotImplemented is ours. §5.3 lets a method define further types,
	// and the alternative was worse: reporting an unbuilt operation as
	// "forbidden" tells a client it lacks permission and invites it to retry
	// as someone else, while a method-level error would fail the whole call
	// including the objects that did succeed.
	SetErrNotImplemented = "notImplemented"
)

SetError types from RFC 8620 §5.3, plus one of ours.

View Source
const (
	SetErrInvalidSieve  = "invalidSieve"
	SetErrSieveIsActive = "sieveIsActive"
)

SetError types of RFC 9661 §2.4.

View Source
const (
	KindEmail   = 'e'
	KindMailbox = 'm'
	// KindThread states carry no per-folder entries at all: a conversation is
	// not owned by a folder, and its history is the sidecar log, so the whole
	// state is a position in it.
	KindThread = 't'
)

Description kinds. Carried in the payload so an Email state handed to a Mailbox method is refused rather than diffed as though it meant anything.

View Source
const (
	SetErrInvalidEmail      = "invalidEmail"
	SetErrTooManyRecipients = "tooManyRecipients"
	SetErrNoRecipients      = "noRecipients"
	SetErrInvalidRecipients = "invalidRecipients"
	SetErrForbiddenMailFrom = "forbiddenMailFrom"
	SetErrForbiddenFrom     = "forbiddenFrom"
	SetErrForbiddenToSend   = "forbiddenToSend"
	SetErrCannotUnsend      = "cannotUnsend"
)

SetError types of RFC 8621 §7.5.

View Source
const CapQuota = "urn:ietf:params:jmap:quota"

CapQuota is the quota capability of RFC 9425. Its object is empty: the limits live in the Quota objects, not in the capability.

View Source
const CapSieve = "urn:ietf:params:jmap:sieve"

CapSieve is the capability of RFC 9661.

View Source
const CapSubmission = "urn:ietf:params:jmap:submission"

CapSubmission is the Identity and EmailSubmission capability (RFC 8621 §1.3.2).

View Source
const CapVacation = "urn:ietf:params:jmap:vacationresponse"

CapVacation is the VacationResponse capability (RFC 8621 §1.3.3).

View Source
const CapWebSocket = "urn:ietf:params:jmap:websocket"

CapWebSocket is JMAP over WebSocket (RFC 8887).

View Source
const CapWebSocketTicket = "com:linagora:params:jmap:ws:ticket"

CapWebSocketTicket is one web client's own extension, the only vendor name in this protocol, by decision: a browser cannot send credentials on a WebSocket.

View Source
const ErrStateMismatch = "stateMismatch"

ErrStateMismatch is the method-level error for an ifInState that does not match (RFC 8620 §5.3).

View Source
const ProblemBlank = "about:blank"

ProblemBlank is the type of a request-level error with no JMAP-specific meaning beyond its status code.

View Source
const QuotaScopeAccount = "account"

QuotaScopeAccount is the only scope yarilo has: limits are per account, not per domain or installation.

View Source
const QuotaTypeMail = "Mail"

QuotaTypeMail names the data type a quota applies to (RFC 9425 §1.3).

View Source
const SessionPath = "/.well-known/jmap"

SessionPath is the well-known endpoint of RFC 8620 §2.2. A client is given only this URL and discovers everything else from what it returns.

View Source
const UploadPath = "/jmap/upload/"

UploadPath prefixes the upload URL; the login edge caps it by maxSizeUpload.

View Source
const VacationID = "singleton"

VacationID is the id of the one VacationResponse an account has (§8).

View Source
const WebSocketPath = "/jmap/ws/"

WebSocketPath is the WebSocket endpoint.

View Source
const WebSocketTicketPath = "/jmap/ws/ticket"

WebSocketTicketPath issues a WebSocket ticket on POST; jmap-login serves it.

Variables

View Source
var (
	// ErrStateVersion says the string was written by a different version of
	// this encoding. Callers answer cannotCalculateChanges.
	ErrStateVersion = errors.New("jmapcore: state string from another format version")
	// ErrStateFormat says the string is not one of ours at all -- a client
	// inventing a value, or a state from a different object type.
	ErrStateFormat = errors.New("jmapcore: malformed state string")
)
View Source
var CollationAlgorithms = []string{"i;ascii-numeric", "i;ascii-casemap", "i;octet"}

CollationAlgorithms are the comparators the server implements. i;ascii-numeric is the only one RFC 8620 requires.

Functions

func AccountID

func AccountID(username string) string

AccountID is the user's account id: RFC 8620 §1.2 allows only the URL-safe base64 alphabet, so the username is encoded, behind a letter.

func AddCreated

func AddCreated(ctx context.Context, creationID, id string)

AddCreated records what a creation id created; a reused creation id maps to the most recently created item (RFC 8620 §5.3).

func EffectiveBodyBytes

func EffectiveBodyBytes(clientMax uint32, ceiling uint32) uint32

EffectiveBodyBytes resolves the client's cap against the server's ceiling. The smaller wins, and a client that named no cap gets the ceiling rather than everything - the ceiling is the operator's bound on work, not a default a client can opt out of.

func EffectiveLimit

func EffectiveLimit(clientLimit *uint, ceiling uint) uint

EffectiveLimit resolves the client's limit against the server's ceiling, the same way EffectiveBodyBytes does for body values: the smaller wins, and a client naming none gets the ceiling rather than everything. RFC 8620 §5.5 lets a server impose its own limit provided the response reports it.

func HeaderFormAllowed

func HeaderFormAllowed(name string, form HeaderForm) bool

HeaderFormAllowed reports whether a field may be given in a form.

func IsHeaderProperty

func IsHeaderProperty(p string) bool

IsHeaderProperty reports whether a property names a header field.

func KnownProperties

func KnownProperties(value any) []string

KnownProperties lists the property names an object carries, sorted. For tests and for an error message that has to suggest something.

func ParseRequest

func ParseRequest(body []byte, declared []string, lim Limits) (*Request, *RequestError)

ParseRequest decodes and validates the envelope. declared is the set of capability URNs this server advertises; it is passed in rather than read from anywhere, so this package stays free of a server's configuration.

func Project

func Project(value any, props []string, extra map[string]any) any

Project returns the object's requested properties as a map ready to be marshalled (RFC 8620 §5.1).

Without it a response carries every field of the object regardless of the request, which is a deviation from the specification and, worse, a silent misstatement: a field the server never computed marshals as its zero value and is indistinguishable from one computed as empty. A client asking for a subject would be told, as a fact, that the message has no attachments.

props nil means the client named none, which asks for all of them; the value is returned unchanged so it marshals as itself.

A map of values rather than a json.Marshaler returning bytes: encoding/json validates and compacts whatever a Marshaler hands back, so a projection built that way re-parses everything it just encoded - on a message whose body the client did ask for, that is a second pass over the body. Selecting the fields and letting the encoder run once avoids both that and encoding the fields nobody asked for.

func ResolveID

func ResolveID(ctx context.Context, id string) string

ResolveID turns "#<creation id>" into the id created earlier in the request; anything else, an unknown creation id included, comes back as given.

func ResolveIDs

func ResolveIDs(ctx context.Context, ids []string) []string

ResolveIDs applies ResolveID to each id, in place.

func ResolveKeys

func ResolveKeys[V any](ctx context.Context, m map[string]V) map[string]V

ResolveKeys applies ResolveID to the keys of an id-keyed map.

func SessionState

func SessionState(lim Limits) string

SessionState is the session resource's "state" string (RFC 8620 §2). A client caches the session until it changes, so it moves with the advertised values and stays put otherwise.

func TruncateBody

func TruncateBody(s string, limit uint32) (string, bool)

TruncateBody cuts a decoded body value to at most limit bytes and reports whether anything was removed. RFC 8621 §4.2.2 forbids splitting a multi-octet character, so the cut moves back to a rune boundary; a limit of 0 means no limit.

func TruncateHTML

func TruncateHTML(s string, limit uint32) (string, bool)

TruncateHTML cuts an HTML body value like TruncateBody, then drops a trailing partial tag. Cutting mid-tag would leave markup a client renders as text or, worse, as an unterminated element swallowing the rest of the document.

func UnknownProperties

func UnknownProperties(value any, props []string) []string

UnknownProperties returns the requested names that value does not carry, in the order the client named them.

A header field property is valid when it parses (§4.1.3); anything else must be a JSON property of the object.

func WriteJSON

func WriteJSON(w http.ResponseWriter, status int, v any)

WriteJSON emits a response body. A failure mid-write cannot be signalled to the client, the status line is already gone, so it is only logged.

func WriteProblem

func WriteProblem(w http.ResponseWriter, status int, detail string)

WriteProblem emits the RFC 7807 body JMAP uses for request-level errors (RFC 8620 §3.6.1), with no JMAP-specific type.

func WriteProblemType

func WriteProblemType(w http.ResponseWriter, status int, problemType, detail string)

WriteProblemType emits an RFC 7807 body naming a specific problem type.

Types

type Account

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

Account describes one account the authenticated user can reach (RFC 8620 §2).

type AddedItem

type AddedItem struct {
	ID    string `json:"id"`
	Index uint   `json:"index"`
}

AddedItem is one id and its position in a query result (RFC 8620 §5.6).

type ChangesRequest

type ChangesRequest struct {
	AccountID  string `json:"accountId"`
	SinceState string `json:"sinceState"`
	// MaxChanges is a ceiling, not a page size: a server that cannot answer
	// within it returns tooManyChanges rather than a truncated list, which
	// would tell the client it had seen everything.
	MaxChanges *uint `json:"maxChanges"`
}

ChangesRequest is the shape every Foo/changes takes (RFC 8620 §5.2).

type ChangesResponse

type ChangesResponse struct {
	AccountID string `json:"accountId"`
	OldState  string `json:"oldState"`
	NewState  string `json:"newState"`
	// HasMoreChanges is always false here: this server answers a window whole
	// or refuses it, so there is never a remainder to fetch.
	HasMoreChanges bool     `json:"hasMoreChanges"`
	Created        []string `json:"created"`
	Updated        []string `json:"updated"`
	Destroyed      []string `json:"destroyed"`
}

ChangesResponse is the answer. The three lists are always emitted, empty rather than absent -- a client reading destroyed must not have to tell "nothing was deleted" from "the server did not say".

type Comparator

type Comparator struct {
	Property    string `json:"property"`
	IsAscending *bool  `json:"isAscending"`
}

Comparator is one sort key (RFC 8620 §5.5).

func (Comparator) Ascending

func (c Comparator) Ascending() bool

Ascending reports the direction, defaulting to true as the RFC requires.

type CoreCapability

type CoreCapability struct {
	MaxSizeUpload         int64    `json:"maxSizeUpload"`
	MaxConcurrentUpload   int      `json:"maxConcurrentUpload"`
	MaxSizeRequest        int64    `json:"maxSizeRequest"`
	MaxConcurrentRequests int      `json:"maxConcurrentRequests"`
	MaxCallsInRequest     int      `json:"maxCallsInRequest"`
	MaxObjectsInGet       int      `json:"maxObjectsInGet"`
	MaxObjectsInSet       int      `json:"maxObjectsInSet"`
	CollationAlgorithms   []string `json:"collationAlgorithms"`
}

CoreCapability is the urn:ietf:params:jmap:core object (RFC 8620 §2).

func CoreCapabilityFor

func CoreCapabilityFor(lim Limits) CoreCapability

CoreCapabilityFor renders the core capability from the advertised limits.

type Description

type Description struct {
	Kind    byte
	Entries []StateEntry
	// Extra carries account-wide positions that no folder owns.
	//
	// The threading sidecar is one: a merge renames the threadId of messages
	// across folders while touching no folder's markers, so an Email state
	// built from markers alone cannot see it -- and a client syncing Email
	// objects would keep the old conversation for ever (RFC 8620 §5.2 requires
	// the updated records; threadId is an Email property).
	//
	// A position rather than a digest, for the same reason the rest of this is
	// a description: Foo/changes diffs two states, and a hash cannot be
	// diffed.
	Extra []uint64
}

Description is the whole account, as a sorted set of entries. A set, not a maximum: a deleted folder removes its entry, which changes the state without lowering anything, so the string can never travel backwards.

func ParseDescription

func ParseDescription(s string, kind byte) (Description, error)

ParseDescription is the inverse, and the gate: a string from another version is reported as such rather than guessed at.

func (Description) String

func (d Description) String() string

String renders the state a client sees.

type Email

type Email struct {
	ID         string          `json:"id"`
	BlobID     string          `json:"blobId"`
	ThreadID   string          `json:"threadId"`
	MailboxIDs map[string]bool `json:"mailboxIds"`
	Keywords   map[string]bool `json:"keywords"`
	Size       uint32          `json:"size"`
	ReceivedAt string          `json:"receivedAt"`

	// Headers is every header field of the message, in the order it carries
	// them, with the raw value (§4.1.2.1). It is what a client falls back to
	// when it needs a field this type does not model and does not know the
	// name in advance.
	Headers []EmailHeader `json:"headers"`

	// Header-derived fields (§4.1.2).
	MessageID  []string       `json:"messageId"`
	InReplyTo  []string       `json:"inReplyTo"`
	References []string       `json:"references"`
	Sender     []EmailAddress `json:"sender"`
	From       []EmailAddress `json:"from"`
	To         []EmailAddress `json:"to"`
	CC         []EmailAddress `json:"cc"`
	BCC        []EmailAddress `json:"bcc"`
	ReplyTo    []EmailAddress `json:"replyTo"`
	Subject    *string        `json:"subject"`
	SentAt     *string        `json:"sentAt"`

	// Body (§4.1.4).
	BodyStructure *EmailBodyPart            `json:"bodyStructure,omitempty"`
	BodyValues    map[string]EmailBodyValue `json:"bodyValues"`
	TextBody      []EmailBodyPart           `json:"textBody"`
	HTMLBody      []EmailBodyPart           `json:"htmlBody"`
	Attachments   []EmailBodyPart           `json:"attachments"`
	HasAttachment bool                      `json:"hasAttachment"`
	Preview       string                    `json:"preview"`
}

Email is the Email object of RFC 8621 §4.1. Only the properties this server derives are present; a client asking for others gets them omitted rather than invented.

type EmailAddress

type EmailAddress struct {
	Name  *string `json:"name"`
	Email string  `json:"email"`
}

EmailAddress is one address in a header field (RFC 8621 §4.1.2.3).

type EmailAddressGroup

type EmailAddressGroup struct {
	Name      *string        `json:"name"`
	Addresses []EmailAddress `json:"addresses"`
}

EmailAddressGroup is an RFC 5322 address group as JMAP models it (RFC 8621 §4.1.2.3). Addresses that belong to no group are carried under a null name.

type EmailBodyPart

type EmailBodyPart struct {
	PartID      *string         `json:"partId"`
	BlobID      *string         `json:"blobId"`
	Size        uint32          `json:"size"`
	Name        *string         `json:"name"`
	Type        string          `json:"type"`
	Charset     *string         `json:"charset"`
	Disposition *string         `json:"disposition"`
	CID         *string         `json:"cid"`
	Language    []string        `json:"language"`
	Location    *string         `json:"location"`
	SubParts    []EmailBodyPart `json:"subParts,omitempty"`
}

EmailBodyPart describes one part of the message (RFC 8621 §4.1.4).

type EmailBodyValue

type EmailBodyValue struct {
	Value string `json:"value"`
	// IsEncodingProblem reports that the part could not be decoded cleanly.
	IsEncodingProblem bool `json:"isEncodingProblem"`
	// IsTruncated must be true whenever the server returned less than the whole
	// part. A server may truncate; it may not do so silently.
	IsTruncated bool `json:"isTruncated"`
}

EmailBodyValue carries a decoded body part (RFC 8621 §4.1.4).

type EmailFilter

type EmailFilter struct {
	Operator string `json:"operator"`

	InMailbox          *string  `json:"inMailbox"`
	InMailboxOtherThan []string `json:"inMailboxOtherThan"`
	Before             *string  `json:"before"`
	After              *string  `json:"after"`
	MinSize            *uint32  `json:"minSize"`
	MaxSize            *uint32  `json:"maxSize"`
	HasKeyword         *string  `json:"hasKeyword"`
	NotKeyword         *string  `json:"notKeyword"`
	HasAttachment      *bool    `json:"hasAttachment"`

	// Text conditions need the full-text index. They are named here so a
	// request carrying one can be refused explicitly instead of being ignored,
	// which would return a confidently wrong result set.
	Text    *string  `json:"text"`
	From    *string  `json:"from"`
	To      *string  `json:"to"`
	Cc      *string  `json:"cc"`
	Bcc     *string  `json:"bcc"`
	Subject *string  `json:"subject"`
	Body    *string  `json:"body"`
	Header  []string `json:"header"`
}

EmailFilter is a filter condition (RFC 8621 §4.4.1). An operator node (AND/OR/NOT) sets Operator, which is how that case is detected and refused rather than silently matching everything.

func (*EmailFilter) TextConditions

func (f *EmailFilter) TextConditions() []string

TextConditions reports which full-text conditions the filter carries, so a caller can name them in its refusal rather than saying "unsupported".

func (*EmailFilter) TextValues

func (f *EmailFilter) TextValues() []string

TextValues are the strings the text conditions search for. A snippet highlights these, so it needs the values rather than the condition names TextConditions reports.

type EmailGetRequest

type EmailGetRequest struct {
	GetRequest
	BodyProperties      *[]string `json:"bodyProperties"`
	FetchTextBodyValues bool      `json:"fetchTextBodyValues"`
	FetchHTMLBodyValues bool      `json:"fetchHTMLBodyValues"`
	FetchAllBodyValues  bool      `json:"fetchAllBodyValues"`
	// MaxBodyValueBytes is the client's own cap (§4.2.2). Zero means the client
	// stated none, which is not the same as asking for everything: the server's
	// ceiling still applies.
	MaxBodyValueBytes uint32 `json:"maxBodyValueBytes"`
}

EmailGetRequest is Email/get (RFC 8621 §4.2).

func (EmailGetRequest) EnvelopeSuffices

func (r EmailGetRequest) EnvelopeSuffices() bool

EnvelopeSuffices reports whether every property this request names can be answered from a cached envelope, so the message need not be opened.

A request naming no properties gets the full default set and does not qualify.

func (EmailGetRequest) HeaderProperties

func (r EmailGetRequest) HeaderProperties() []HeaderProperty

HeaderProperties returns the parsed header field properties of the request, in the order the client named them.

func (EmailGetRequest) NeedsHeaders

func (r EmailGetRequest) NeedsHeaders() bool

NeedsHeaders reports whether any requested property comes from the header block.

func (EmailGetRequest) NeedsMessage

func (r EmailGetRequest) NeedsMessage() bool

NeedsMessage reports whether answering this request requires reading the message at all. A client that named only index-backed properties - id, mailboxIds, keywords, size, receivedAt - is answered without opening it. A client that named no properties gets the full default set, which does require the message.

func (EmailGetRequest) NeedsStructure

func (r EmailGetRequest) NeedsStructure() bool

NeedsStructure reports whether answering the request requires walking the MIME tree and decoding its parts.

A request naming only header properties does not: it is the shape a client uses to render a message list, and the walk it used to pay for is the whole body of every message in that list.

func (EmailGetRequest) WantsBodyValues

func (r EmailGetRequest) WantsBodyValues() bool

WantsBodyValues reports whether any body value was asked for at all.

type EmailHeader

type EmailHeader struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

EmailHeader is one header field as JMAP carries it (RFC 8621 §4.1.2.1). The value is raw: everything after the colon, folding included, minus the CRLF.

type EmailQueryRequest

type EmailQueryRequest struct {
	QueryRequest
	Filter *EmailFilter `json:"filter"`
	Sort   []Comparator `json:"sort"`
}

EmailQueryRequest is Email/query (RFC 8621 §4.4).

type EmailSubmission

type EmailSubmission struct {
	ID             string    `json:"id"`
	IdentityID     string    `json:"identityId"`
	EmailID        string    `json:"emailId"`
	ThreadID       string    `json:"threadId"`
	Envelope       *Envelope `json:"envelope"`
	SendAt         time.Time `json:"sendAt"`
	UndoStatus     string    `json:"undoStatus"`
	DeliveryStatus any       `json:"deliveryStatus"`
	DsnBlobIDs     []string  `json:"dsnBlobIds"`
	MdnBlobIDs     []string  `json:"mdnBlobIds"`
}

EmailSubmission is the object of RFC 8621 §7.

type Envelope

type Envelope struct {
	MailFrom SubmissionAddress   `json:"mailFrom"`
	RcptTo   []SubmissionAddress `json:"rcptTo"`
}

Envelope is an EmailSubmission's SMTP envelope.

type FollowedBy

type FollowedBy struct {
	Result any
	Then   []Implicit
}

FollowedBy is a method result that carries the responses of the calls it made implicitly, sent right after its own (RFC 8621 §7.5).

type GetRequest

type GetRequest struct {
	AccountID string `json:"accountId"`
	// IDs nil means "every object of this type" (§5.1). The distinction from an
	// empty array, which means "none", is why this is a pointer.
	IDs        *[]string `json:"ids"`
	Properties *[]string `json:"properties"`
}

GetRequest is the shape every Foo/get takes (RFC 8620 §5.1).

type GetResponse

type GetResponse[T any] struct {
	AccountID string `json:"accountId"`
	// State is the object type's state string. A client passes it back to
	// Foo/changes; it is not comparable across types.
	State    string   `json:"state"`
	List     []T      `json:"list"`
	NotFound []string `json:"notFound"`
}

GetResponse is the shape every Foo/get returns (RFC 8620 §5.1).

type HeaderForm

type HeaderForm string

HeaderForm is one of the forms of §4.1.3.

const (
	FormRaw              HeaderForm = "asRaw"
	FormText             HeaderForm = "asText"
	FormAddresses        HeaderForm = "asAddresses"
	FormGroupedAddresses HeaderForm = "asGroupedAddresses"
	FormMessageIDs       HeaderForm = "asMessageIds"
	FormDate             HeaderForm = "asDate"
	FormURLs             HeaderForm = "asURLs"
)

The header forms, by their property suffix.

type HeaderProperty

type HeaderProperty struct {
	// Property is the name as the client wrote it, which is the key the answer
	// must come back under.
	Property string
	// Name is the header field name, matched case-insensitively.
	Name string
	// Form is the requested form; Raw when the client named none.
	Form HeaderForm
	// All requests every occurrence rather than the last one.
	All bool
}

HeaderProperty is a parsed "header:{name}[:as{Form}][:all]" property (RFC 8621 §4.1.3).

These matter beyond conformance: they are the only way a client reaches a header the data type does not model. List-Unsubscribe, X-Spam-*, DKIM results - all present in the message and, without this, unreachable over JMAP.

func ParseHeaderProperty

func ParseHeaderProperty(p string) (HeaderProperty, bool)

ParseHeaderProperty splits a header property into its parts.

ok=false means the property is malformed - an empty field name, an unknown form, or suffixes in the wrong order. It is deliberately not "assume Raw and carry on": a client asking for asURLs and silently receiving a raw string gets a type it did not ask for, and would have no way to notice.

type Identity

type Identity struct {
	ID            string         `json:"id"`
	Name          string         `json:"name"`
	Email         string         `json:"email"`
	ReplyTo       []EmailAddress `json:"replyTo"`
	Bcc           []EmailAddress `json:"bcc"`
	TextSignature string         `json:"textSignature"`
	HTMLSignature string         `json:"htmlSignature"`
	MayDelete     bool           `json:"mayDelete"`
}

Identity is the object of RFC 8621 §6.

type Implicit

type Implicit struct {
	Name   string
	Result any
}

Implicit is one implicit call's response.

type Invocation

type Invocation struct {
	Name string
	Args json.RawMessage
	// CallID ties a response to the call that produced it and is what a
	// back-reference names.
	CallID string
}

Invocation is one method call or response (RFC 8620 §3.2). On the wire it is a three-element array, not an object, which is why it carries its own marshalling.

func (Invocation) MarshalJSON

func (i Invocation) MarshalJSON() ([]byte, error)

MarshalJSON renders the wire triple.

func (*Invocation) UnmarshalJSON

func (i *Invocation) UnmarshalJSON(b []byte) error

UnmarshalJSON reads the wire triple. Anything that is not exactly three elements is a malformed request, not a method that failed: the server cannot tell which call it was meant to answer.

type Limits

type Limits struct {
	// BaseURL is the public origin clients reach the server on. It prefixes
	// every URL in the session resource, so a proxied deployment sets it to the
	// externally visible name, not the pod address.
	BaseURL               string
	MaxSizeUpload         int64
	MaxSizeRequest        int64
	MaxConcurrentRequests int
	MaxCallsInRequest     int
	MaxObjectsInGet       int
	MaxObjectsInSet       int
	// Sieve advertises RFC 9661; nil leaves it out.
	Sieve *SieveSession
	// Submission advertises Identity and EmailSubmission; nil leaves them out.
	Submission *SubmissionCapability
	// WebSocket advertises RFC 8887; nil leaves it out.
	WebSocket *WebSocketCapability
	// WebSocketTicket advertises the ticket extension with WebSocket, at the
	// session and the account level, where the client looks for both.
	WebSocketTicket bool
	// Vacation advertises VacationResponse; nil leaves it out.
	Vacation *VacationCapability
}

Limits are the server's advertised bounds. Clients batch against them, so a caller must publish the same numbers it enforces.

type MailCapability

type MailCapability struct {
	MaxMailboxesPerEmail       *int     `json:"maxMailboxesPerEmail"`
	MaxMailboxDepth            *int     `json:"maxMailboxDepth"`
	MaxSizeMailboxName         int      `json:"maxSizeMailboxName"`
	MaxSizeAttachmentsPerEmail int64    `json:"maxSizeAttachmentsPerEmail"`
	EmailQuerySortOptions      []string `json:"emailQuerySortOptions"`
	MayCreateTopLevelMailbox   bool     `json:"mayCreateTopLevelMailbox"`
}

MailCapability is the per-account urn:ietf:params:jmap:mail object (RFC 8621 §1.6). Nulls mean "no limit" and must stay null rather than 0.

func MailCapabilityFor

func MailCapabilityFor() MailCapability

MailCapabilityFor renders the per-account mail capability.

type Mailbox

type Mailbox struct {
	ID string `json:"id"`
	// Name is the leaf name, not the path: hierarchy is expressed by ParentID,
	// so a client never has to know the server's delimiter.
	Name     string  `json:"name"`
	ParentID *string `json:"parentId"`
	// Role is the IANA-registered purpose ("inbox", "sent", …), null when the
	// mailbox has none.
	Role      *string `json:"role"`
	SortOrder uint32  `json:"sortOrder"`

	TotalEmails  uint32 `json:"totalEmails"`
	UnreadEmails uint32 `json:"unreadEmails"`
	TotalThreads uint32 `json:"totalThreads"`
	// UnreadThreads counts threads with at least one unread message.
	UnreadThreads uint32 `json:"unreadThreads"`

	MyRights     MailboxRights `json:"myRights"`
	IsSubscribed bool          `json:"isSubscribed"`
}

Mailbox is the Mailbox object of RFC 8621 §2. Field order follows the RFC so a hand-read response matches the spec side by side.

type MailboxFilter

type MailboxFilter struct {
	// Operator is set only when the client sent an operator node, which is how
	// that case is detected.
	Operator string `json:"operator"`

	ParentID     *string `json:"parentId"`
	Name         *string `json:"name"`
	Role         *string `json:"role"`
	HasAnyRole   *bool   `json:"hasAnyRole"`
	IsSubscribed *bool   `json:"isSubscribed"`
}

MailboxFilter is a filter condition. RFC 8620 §5.5 also allows an operator node (AND/OR/NOT) here; a request carrying one is refused with unsupportedFilter rather than silently matching everything.

type MailboxQueryRequest

type MailboxQueryRequest struct {
	QueryRequest
	Filter *MailboxFilter `json:"filter"`
	Sort   []Comparator   `json:"sort"`
}

MailboxQueryRequest is Mailbox/query (RFC 8621 §2.3).

type MailboxRights

type MailboxRights struct {
	MayReadItems   bool `json:"mayReadItems"`
	MayAddItems    bool `json:"mayAddItems"`
	MayRemoveItems bool `json:"mayRemoveItems"`
	MaySetSeen     bool `json:"maySetSeen"`
	MaySetKeywords bool `json:"maySetKeywords"`
	MayCreateChild bool `json:"mayCreateChild"`
	MayRename      bool `json:"mayRename"`
	MayDelete      bool `json:"mayDelete"`
	MaySubmit      bool `json:"maySubmit"`
}

MailboxRights is the per-mailbox permission set of RFC 8621 §2. Every member is required: a client that cannot see a right assumes it is absent.

type Method

type Method func(ctx context.Context, args json.RawMessage) (any, *MethodError)

Method runs one call. args have had their back-references resolved already. A returned MethodError becomes an "error" response for this call and leaves the rest of the batch running.

type MethodEntry

type MethodEntry struct {
	Capability string
	Fn         Method
}

MethodEntry binds a method to the capability a client must have declared in "using" to reach it.

type MethodError

type MethodError struct {
	Type        string `json:"type"`
	Description string `json:"description,omitempty"`
	// Arguments names the request arguments the error is about, which
	// invalidArguments carries so a client can point at what it sent rather
	// than re-reading the whole call (RFC 8620 §3.6.2).
	Arguments []string `json:"arguments,omitempty"`
}

MethodError is a method-level failure (RFC 8620 §3.6.2). It travels as an "error" invocation in methodResponses, keeping the batch's shape: one response per call, in order.

func CoreEcho

func CoreEcho(_ context.Context, args json.RawMessage) (any, *MethodError)

CoreEcho is RFC 8620 §4: the server returns the arguments it was given. It exists so a client can prove the batch machinery - dispatch, ordering, back-references - works before any data method is involved.

func InvalidProperties

func InvalidProperties(unknown []string) *MethodError

InvalidProperties builds the error a request with unknown property names must be answered with (RFC 8620 §5.1).

The whole call is refused rather than the names ignored. Ignoring them is what this replaces, and it fails in the worst way available: the client receives a successful response with the property absent, and cannot tell a typo from a property the server has not implemented - which is exactly the pair it needs to tell apart.

func (*MethodError) Error

func (e *MethodError) Error() string

type QueryChangesResponse

type QueryChangesResponse struct {
	AccountID     string      `json:"accountId"`
	OldQueryState string      `json:"oldQueryState"`
	NewQueryState string      `json:"newQueryState"`
	Removed       []string    `json:"removed"`
	Added         []AddedItem `json:"added"`
}

QueryChangesResponse is the Foo/queryChanges response of RFC 8620 §5.6, for a query whose result set a server can say the difference in.

type QueryRequest

type QueryRequest struct {
	AccountID      string `json:"accountId"`
	Position       int    `json:"position"`
	Limit          *uint  `json:"limit"`
	CalculateTotal bool   `json:"calculateTotal"`
}

QueryRequest is the shape every Foo/query takes (RFC 8620 §5.5).

type QueryResponse

type QueryResponse struct {
	AccountID string `json:"accountId"`
	// QueryState identifies the result set. Whether it can be used for an
	// incremental update is stated by CanCalculateChanges, not assumed.
	QueryState          string   `json:"queryState"`
	CanCalculateChanges bool     `json:"canCalculateChanges"`
	Position            int      `json:"position"`
	IDs                 []string `json:"ids"`
	Total               *uint    `json:"total,omitempty"`
	Limit               *uint    `json:"limit,omitempty"`
}

QueryResponse is the shape every Foo/query returns (RFC 8620 §5.5).

type Quota

type Quota struct {
	ID           string   `json:"id"`
	ResourceType string   `json:"resourceType"`
	Used         int64    `json:"used"`
	HardLimit    int64    `json:"hardLimit"`
	Scope        string   `json:"scope"`
	Name         string   `json:"name"`
	Types        []string `json:"types"`
	// Nullable per the RFC: a missing soft limit is null, never 0, which a
	// client would read as "no headroom at all".
	WarnLimit   *int64  `json:"warnLimit"`
	SoftLimit   *int64  `json:"softLimit"`
	Description *string `json:"description"`
}

Quota is the Quota object of RFC 9425 §1.3. Field order follows the RFC so a hand-read response matches the spec side by side.

type QuotaChangesResponse

type QuotaChangesResponse struct {
	AccountID         string    `json:"accountId"`
	OldState          string    `json:"oldState"`
	NewState          string    `json:"newState"`
	HasMoreChanges    bool      `json:"hasMoreChanges"`
	Created           []string  `json:"created"`
	Updated           []string  `json:"updated"`
	Destroyed         []string  `json:"destroyed"`
	UpdatedProperties *[]string `json:"updatedProperties"`
}

QuotaChangesResponse is Quota/changes (RFC 9425 §4.3): the shared changes arguments plus updatedProperties, which is null when the server names none.

type Registry

type Registry map[string]MethodEntry

Registry maps method names to their implementations.

func CoreRegistry

func CoreRegistry() Registry

CoreRegistry is the method set of the core capability.

type Request

type Request struct {
	// Using lists the capabilities this request relies on. Core is not
	// implicit: a method whose capability is absent is answered with
	// unknownMethod, which is what lets a server add capabilities without
	// changing how older clients behave.
	Using       []string          `json:"using"`
	MethodCalls []Invocation      `json:"methodCalls"`
	CreatedIDs  map[string]string `json:"createdIds,omitempty"`
}

Request is the API request envelope (RFC 8620 §3.3).

type RequestError

type RequestError struct {
	Type   string
	Status int
	Detail string
	// Limit names the exceeded limit. §3.6.1 requires it on a limit problem,
	// since "too big" is useless without knowing which bound was hit.
	Limit string
}

RequestError is a request-level failure (RFC 8620 §3.6.1): the batch is not run at all and the client gets a problem document rather than an envelope.

func (*RequestError) Error

func (e *RequestError) Error() string

func (*RequestError) Write

func (e *RequestError) Write(w http.ResponseWriter)

Write emits the problem document.

type Response

type Response struct {
	MethodResponses []Invocation `json:"methodResponses"`
	// CreatedIDs is a pointer so a client that sent an empty map gets one back.
	CreatedIDs *map[string]string `json:"createdIds,omitempty"`
	// SessionState is the same value the session resource carries, so a client
	// can tell its cached session went stale without refetching it.
	SessionState string `json:"sessionState"`
}

Response is the API response envelope (RFC 8620 §3.4).

func Execute

func Execute(ctx context.Context, req *Request, reg Registry, lim Limits) *Response

Execute runs the batch in order and returns the envelope. Order is what makes back-references well-defined: a call can only see results already produced.

type ResultReference

type ResultReference struct {
	// ResultOf is the callId of the earlier invocation.
	ResultOf string `json:"resultOf"`
	// Name is the method name that invocation must have responded with. A
	// client states it so a call answered by "error" cannot be mistaken for a
	// result.
	Name string `json:"name"`
	// Path is a JSON Pointer (RFC 6901) into that result, extended by "*".
	Path string `json:"path"`
}

ResultReference is a back-reference (RFC 8620 §3.7): an argument whose value is taken from an earlier call's result, so a client can chain calls in one request instead of waiting for a round trip.

type SearchSnippet

type SearchSnippet struct {
	EmailID string  `json:"emailId"`
	Subject *string `json:"subject"`
	Preview *string `json:"preview"`
}

SearchSnippet is one message's highlighted fragments (RFC 8621 §5.1). Either field may be null: a query whose only hit is the subject has nothing to show in the body, and inventing a fragment would be worse than showing none.

type SearchSnippetGetRequest

type SearchSnippetGetRequest struct {
	AccountID string       `json:"accountId"`
	Filter    *EmailFilter `json:"filter"`
	EmailIDs  []string     `json:"emailIds"`
}

SearchSnippetGetRequest is the method's arguments. The filter is the one the snippets highlight against -- normally the same one Email/query was given.

type SearchSnippetGetResponse

type SearchSnippetGetResponse struct {
	AccountID string          `json:"accountId"`
	List      []SearchSnippet `json:"list"`
	NotFound  []string        `json:"notFound"`
}

SearchSnippetGetResponse mirrors the /get shape: what was produced and what could not be (§5.1).

type Session

type Session struct {
	Capabilities    map[string]any     `json:"capabilities"`
	Accounts        map[string]Account `json:"accounts"`
	PrimaryAccounts map[string]string  `json:"primaryAccounts"`
	Username        string             `json:"username"`
	APIURL          string             `json:"apiUrl"`
	DownloadURL     string             `json:"downloadUrl"`
	UploadURL       string             `json:"uploadUrl"`
	EventSourceURL  string             `json:"eventSourceUrl"`
	State           string             `json:"state"`
}

Session is the session resource of RFC 8620 §2. Field order follows the RFC so a hand-read response matches the spec side by side.

func BuildSession

func BuildSession(lim Limits, username string) *Session

BuildSession renders the session resource for one authenticated user: one account per user, its id AccountID(username).

type SetError

type SetError struct {
	Type        string   `json:"type"`
	Description string   `json:"description,omitempty"`
	Properties  []string `json:"properties,omitempty"`
	// NotFound lists every missing blob of a blobNotFound (RFC 8621 §4.6).
	NotFound []string `json:"notFound,omitempty"`
	// ExistingID names the object an alreadyExists collided with (RFC 9661 §2.4).
	ExistingID string `json:"existingId,omitempty"`
	// MaxSize, MaxRecipients and InvalidRecipients go with tooLarge,
	// tooManyRecipients and invalidRecipients (RFC 8621 §7.5).
	MaxSize           *int64   `json:"maxSize,omitempty"`
	MaxRecipients     *int     `json:"maxRecipients,omitempty"`
	InvalidRecipients []string `json:"invalidRecipients,omitempty"`
}

SetError is the per-object failure of a Foo/set (RFC 8620 §5.3). It is not a MethodError: one object failing leaves the rest of the call standing, which is the whole point of the type.

type SetRequest

type SetRequest struct {
	AccountID string `json:"accountId"`
	// IfInState is the state the client believes the account is in. A pointer
	// because absent means "apply regardless", which is not the same as an
	// empty string a client could send deliberately.
	IfInState *string                    `json:"ifInState"`
	Create    map[string]json.RawMessage `json:"create"`
	Update    map[string]json.RawMessage `json:"update"`
	Destroy   []string                   `json:"destroy"`
}

SetRequest is the shape every Foo/set takes (RFC 8620 §5.3).

create and update carry per-object payloads whose shape depends on the type, so they stay raw here and are decoded by the method that understands them. The id order within them is not meaningful; the RFC's ordering rules are about create/update/destroy as phases, which is where this package leaves it.

type SetResponse

type SetResponse struct {
	AccountID string `json:"accountId"`
	// OldState is what the state was before this call, NewState after it. A
	// client uses the pair to know whether anything else changed in between.
	OldState     string               `json:"oldState"`
	NewState     string               `json:"newState"`
	Created      map[string]any       `json:"created"`
	Updated      map[string]any       `json:"updated"`
	Destroyed    []string             `json:"destroyed"`
	NotCreated   map[string]*SetError `json:"notCreated"`
	NotUpdated   map[string]*SetError `json:"notUpdated"`
	NotDestroyed map[string]*SetError `json:"notDestroyed"`
}

SetResponse is the shape every Foo/set returns (RFC 8620 §5.3).

The four maps are always emitted, empty rather than absent: a client reading notUpdated to find out what failed must not have to distinguish "nothing failed" from "the server did not say".

func NewSetResponse

func NewSetResponse(accountID, state string) *SetResponse

NewSetResponse builds a response with every collection non-nil, so the JSON carries empty objects rather than nulls.

type SieveCapability

type SieveCapability struct {
	MaxSizeScriptName   int      `json:"maxSizeScriptName"`
	MaxSizeScript       *int     `json:"maxSizeScript"`
	MaxNumberScripts    *int     `json:"maxNumberScripts"`
	MaxNumberRedirects  *int     `json:"maxNumberRedirects"`
	SieveExtensions     []string `json:"sieveExtensions"`
	NotificationMethods []string `json:"notificationMethods"`
	ExternalLists       []string `json:"externalLists"`
}

SieveCapability is the per-account urn:ietf:params:jmap:sieve object (RFC 9661 §1.2.1). A nil limit or list is sent as null.

type SieveScript

type SieveScript struct {
	ID       string `json:"id"`
	Name     string `json:"name"`
	BlobID   string `json:"blobId"`
	IsActive bool   `json:"isActive"`
}

SieveScript is the object of RFC 9661 §2.1.

type SieveSession

type SieveSession struct {
	Implementation string
	Account        SieveCapability
}

SieveSession is what a server advertises for RFC 9661: Implementation at the session level, Account in each account's capabilities.

type StateEntry

type StateEntry struct {
	Key    [8]byte
	Fields []uint64
}

StateEntry is one folder's contribution. Key is the first 8 bytes of the folder GUID: rename-stable, and short enough that a mailbox with hundreds of folders still produces a state a client can hold.

type SubmissionAddress

type SubmissionAddress struct {
	Email      string             `json:"email"`
	Parameters map[string]*string `json:"parameters"`
}

SubmissionAddress is an envelope address with its SMTP parameters.

type SubmissionCapability

type SubmissionCapability struct {
	MaxDelayedSend       int                 `json:"maxDelayedSend"`
	SubmissionExtensions map[string][]string `json:"submissionExtensions"`
}

SubmissionCapability is its per-account object.

type Thread

type Thread struct {
	ID string `json:"id"`
	// EmailIDs is ordered by receivedAt (§3). With one message per thread the
	// order is trivially satisfied.
	EmailIDs []string `json:"emailIds"`
}

Thread is the Thread object of RFC 8621 §3.

type VacationCapability

type VacationCapability struct{}

VacationCapability is its per-account object, empty in RFC 8621.

type VacationResponse

type VacationResponse struct {
	ID        string  `json:"id"`
	IsEnabled bool    `json:"isEnabled"`
	FromDate  *string `json:"fromDate"`
	ToDate    *string `json:"toDate"`
	Subject   *string `json:"subject"`
	TextBody  *string `json:"textBody"`
	HTMLBody  *string `json:"htmlBody"`
}

VacationResponse is the object of RFC 8621 §8; the dates are UTCDate strings.

type WebSocketCapability

type WebSocketCapability struct {
	URL          string `json:"url"`
	SupportsPush bool   `json:"supportsPush"`
}

WebSocketCapability is the urn:ietf:params:jmap:websocket object (RFC 8887 §4.1). BuildSession fills URL from the base URL.

type WebSocketTicketCapability

type WebSocketTicketCapability struct {
	GenerationEndpoint string `json:"generationEndpoint"`
}

WebSocketTicketCapability names where a ticket is issued.

Jump to

Keyboard shortcuts

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