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
- Variables
- func AccountID(username string) string
- func AddCreated(ctx context.Context, creationID, id string)
- func EffectiveBodyBytes(clientMax uint32, ceiling uint32) uint32
- func EffectiveLimit(clientLimit *uint, ceiling uint) uint
- func HeaderFormAllowed(name string, form HeaderForm) bool
- func IsHeaderProperty(p string) bool
- func KnownProperties(value any) []string
- func ParseRequest(body []byte, declared []string, lim Limits) (*Request, *RequestError)
- func Project(value any, props []string, extra map[string]any) any
- func ResolveID(ctx context.Context, id string) string
- func ResolveIDs(ctx context.Context, ids []string) []string
- func ResolveKeys[V any](ctx context.Context, m map[string]V) map[string]V
- func SessionState(lim Limits) string
- func TruncateBody(s string, limit uint32) (string, bool)
- func TruncateHTML(s string, limit uint32) (string, bool)
- func UnknownProperties(value any, props []string) []string
- func WriteJSON(w http.ResponseWriter, status int, v any)
- func WriteProblem(w http.ResponseWriter, status int, detail string)
- func WriteProblemType(w http.ResponseWriter, status int, problemType, detail string)
- type Account
- type AddedItem
- type ChangesRequest
- type ChangesResponse
- type Comparator
- type CoreCapability
- type Description
- type Email
- type EmailAddress
- type EmailAddressGroup
- type EmailBodyPart
- type EmailBodyValue
- type EmailFilter
- type EmailGetRequest
- type EmailHeader
- type EmailQueryRequest
- type EmailSubmission
- type Envelope
- type FollowedBy
- type GetRequest
- type GetResponse
- type HeaderForm
- type HeaderProperty
- type Identity
- type Implicit
- type Invocation
- type Limits
- type MailCapability
- type Mailbox
- type MailboxFilter
- type MailboxQueryRequest
- type MailboxRights
- type Method
- type MethodEntry
- type MethodError
- type QueryChangesResponse
- type QueryRequest
- type QueryResponse
- type Quota
- type QuotaChangesResponse
- type Registry
- type Request
- type RequestError
- type Response
- type ResultReference
- type SearchSnippet
- type SearchSnippetGetRequest
- type SearchSnippetGetResponse
- type Session
- type SetError
- type SetRequest
- type SetResponse
- type SieveCapability
- type SieveScript
- type SieveSession
- type StateEntry
- type SubmissionAddress
- type SubmissionCapability
- type Thread
- type VacationCapability
- type VacationResponse
- type WebSocketCapability
- type WebSocketTicketCapability
Constants ¶
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).
const ( ErrUnknownMethod = "unknownMethod" ErrInvalidArguments = "invalidArguments" ErrInvalidResultReference = "invalidResultReference" ErrServerFail = "serverFail" ErrAccountNotFound = "accountNotFound" ErrAccountNotSupportedByMethod = "accountNotSupportedByMethod" ErrRequestTooLarge = "requestTooLarge" // 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.
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").
const ( ErrUnsupportedFilter = "unsupportedFilter" ErrUnsupportedSort = "unsupportedSort" )
Query error types raised by a Foo/query (RFC 8620 §5.5).
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).
const ( QuotaResourceOctets = "octets" QuotaResourceCount = "count" )
Resource types of RFC 9425 §1.3. "octets" counts storage, "count" objects.
const ( CapCore = "urn:ietf:params:jmap:core" CapMail = "urn:ietf:params:jmap:mail" )
Capability URNs advertised in the session resource.
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.
const ( SetErrInvalidSieve = "invalidSieve" SetErrSieveIsActive = "sieveIsActive" )
SetError types of RFC 9661 §2.4.
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.
const ( SetErrInvalidEmail = "invalidEmail" SetErrTooManyRecipients = "tooManyRecipients" SetErrNoRecipients = "noRecipients" SetErrInvalidRecipients = "invalidRecipients" SetErrForbiddenMailFrom = "forbiddenMailFrom" SetErrForbiddenFrom = "forbiddenFrom" SetErrForbiddenToSend = "forbiddenToSend" SetErrCannotUnsend = "cannotUnsend" )
SetError types of RFC 8621 §7.5.
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.
const CapSieve = "urn:ietf:params:jmap:sieve"
CapSieve is the capability of RFC 9661.
const CapSubmission = "urn:ietf:params:jmap:submission"
CapSubmission is the Identity and EmailSubmission capability (RFC 8621 §1.3.2).
const CapVacation = "urn:ietf:params:jmap:vacationresponse"
CapVacation is the VacationResponse capability (RFC 8621 §1.3.3).
const CapWebSocket = "urn:ietf:params:jmap:websocket"
CapWebSocket is JMAP over WebSocket (RFC 8887).
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.
const ErrStateMismatch = "stateMismatch"
ErrStateMismatch is the method-level error for an ifInState that does not match (RFC 8620 §5.3).
const ProblemBlank = "about:blank"
ProblemBlank is the type of a request-level error with no JMAP-specific meaning beyond its status code.
const QuotaScopeAccount = "account"
QuotaScopeAccount is the only scope yarilo has: limits are per account, not per domain or installation.
const QuotaTypeMail = "Mail"
QuotaTypeMail names the data type a quota applies to (RFC 9425 §1.3).
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.
const UploadPath = "/jmap/upload/"
UploadPath prefixes the upload URL; the login edge caps it by maxSizeUpload.
const VacationID = "singleton"
VacationID is the id of the one VacationResponse an account has (§8).
const WebSocketPath = "/jmap/ws/"
WebSocketPath is the WebSocket endpoint.
const WebSocketTicketPath = "/jmap/ws/ticket"
WebSocketTicketPath issues a WebSocket ticket on POST; jmap-login serves it.
Variables ¶
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") )
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 ¶
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 ¶
AddCreated records what a creation id created; a reused creation id maps to the most recently created item (RFC 8620 §5.3).
func EffectiveBodyBytes ¶
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 ¶
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 ¶
IsHeaderProperty reports whether a property names a header field.
func KnownProperties ¶
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 ¶
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 ¶
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 ¶
ResolveIDs applies ResolveID to each id, in place.
func ResolveKeys ¶
ResolveKeys applies ResolveID to the keys of an id-keyed map.
func SessionState ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
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 ¶
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 ¶
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 ¶
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 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 ¶
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).
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 ¶
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 ¶
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 ¶
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.