~rockorager/go-jmap-devel

go-jmap: treewide: strip RFC comments v1 PROPOSED

Robin Jarry: 1
 treewide: strip RFC comments

 45 files changed, 117 insertions(+), 1697 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.sr.ht/~rockorager/go-jmap-devel/patches/46374/mbox | git am -3
Learn more about email & git

[PATCH go-jmap] treewide: strip RFC comments Export this patch

RFC text is subject to a specific license which does not allow
modification of content. Remove all comments that are quoted from RFCs
and replace them with links to the RFCs themselves.

Signed-off-by: Robin Jarry <robin@jarry.cc>
---
 core/blob/copy.go                         |  12 +-
 core/push/subscription/get.go             |   4 +-
 core/push/subscription/set.go             |   2 +
 core/push/subscription/subscription.go    |  26 +---
 mail/email/changes.go                     |  17 +--
 mail/email/copy.go                        |  39 +++---
 mail/email/email.go                       | 142 +-------------------
 mail/email/filter.go                      |  63 +--------
 mail/email/get.go                         |  67 +---------
 mail/email/import.go                      |  33 +----
 mail/email/parse.go                       |  58 +-------
 mail/email/query.go                       | 105 +--------------
 mail/email/querychanges.go                |  66 +---------
 mail/email/set.go                         | 154 +---------------------
 mail/email/sort.go                        |  41 +-----
 mail/emailsubmission/changes.go           |  28 +---
 mail/emailsubmission/emailsubmission.go   |  34 +----
 mail/emailsubmission/filter.go            |  17 +--
 mail/emailsubmission/get.go               |  36 +----
 mail/emailsubmission/query.go             | 101 +-------------
 mail/emailsubmission/querychanges.go      |  58 +-------
 mail/emailsubmission/set.go               |  95 +------------
 mail/emailsubmission/sort.go              |  35 -----
 mail/identity/changes.go                  |  28 +---
 mail/identity/get.go                      |  15 +--
 mail/identity/identity.go                 |  24 +---
 mail/identity/set.go                      |  83 +-----------
 mail/mailbox/changes.go                   |  19 +--
 mail/mailbox/filter.go                    |  19 +--
 mail/mailbox/get.go                       |  13 +-
 mail/mailbox/mailbox.go                   |  31 +----
 mail/mailbox/query.go                     |  26 +---
 mail/mailbox/querychanges.go              |  18 +--
 mail/mailbox/set.go                       |  23 +---
 mail/mdn/mdn.go                           |  35 +----
 mail/mdn/parse.go                         |   9 +-
 mail/mdn/send.go                          |  11 +-
 mail/searchsnippet/get.go                 |  15 +--
 mail/searchsnippet/searchsnippet.go       |  26 +---
 mail/thread/changes.go                    |  27 +---
 mail/thread/get.go                        |  35 +----
 mail/thread/thread.go                     |  13 +-
 mail/vacationresponse/get.go              |  13 +-
 mail/vacationresponse/set.go              |  83 +-----------
 mail/vacationresponse/vacationresponse.go |  15 +--
 45 files changed, 117 insertions(+), 1697 deletions(-)

diff --git a/core/blob/copy.go b/core/blob/copy.go
index 06216d7841c5..5a380ff205ff 100644
--- a/core/blob/copy.go
+++ b/core/blob/copy.go
@@ -5,15 +5,13 @@ import (
	"git.sr.ht/~rockorager/go-jmap/core"
)

// Copy copies data between accounts
// Copy a binary blob from one account to another
// https://www.rfc-editor.org/rfc/rfc8620.html#section-6.3
type Copy struct {
	// The ID of the account to copy blobs from
	FromAccount jmap.ID `json:"fromAccountId,omitempty"`

	// The ID of the account to copy blobs to
	Account jmap.ID `json:"accountId,omitempty"`

	// A list of IDs of blobs to copy
	IDs []jmap.ID `json:"blobIds,omitempty"`
}

@@ -22,18 +20,12 @@ func (m *Copy) Name() string { return "Blob/copy" }
func (m *Copy) Requires() []jmap.URI { return []jmap.URI{core.URI} }

type CopyResponse struct {
	// The ID of the account blobs were copied from
	FromAccount jmap.ID `json:"fromAccountId,omitempty"`

	// The ID of the account blobs were copied to
	Account jmap.ID `json:"accountId,omitempty"`

	// A map of the blobId in the fromAccount to the ID of the blob in the
	// account it was copied to. Map is null if no blobs were copied
	Copied map[jmap.ID]jmap.ID `json:"blobIds,omitempty"`

	// A map of blobId to a SetError object for each blob that failed to be
	// copied, or null if none.
	NotCopied map[jmap.ID]*jmap.SetError `json:"notCopied,omitempty"`
}

diff --git a/core/push/subscription/get.go b/core/push/subscription/get.go
index 0f8696d8c7bc..84c0185bf59f 100644
--- a/core/push/subscription/get.go
+++ b/core/push/subscription/get.go
@@ -5,7 +5,8 @@ import (
	"git.sr.ht/~rockorager/go-jmap/core"
)

// This is a standard “/get” method as described in [@!RFC8620], Section 5.1.
// Get push subscription details
// https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2.1
type Get struct {
	IDs        []jmap.ID `json:"ids,omitempty"`
	Properties []string  `json:"properties,omitempty"`
@@ -15,7 +16,6 @@ func (m *Get) Name() string { return "PushSubscription/get" }

func (m *Get) Requires() []jmap.URI { return []jmap.URI{core.URI} }

// This is a standard “/get” method as described in [@!RFC8620], Section 5.1.
type GetResponse struct {
	List     []*PushSubscription `json:"list,omitempty"`
	NotFound []jmap.ID           `json:"notFound,omitempty"`
diff --git a/core/push/subscription/set.go b/core/push/subscription/set.go
index 4da0be8815fe..02ad7763e497 100644
--- a/core/push/subscription/set.go
+++ b/core/push/subscription/set.go
@@ -5,6 +5,8 @@ import (
	"git.sr.ht/~rockorager/go-jmap/core"
)

// Modify push subscription details
// https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2.2
type Set struct {
	Create  map[jmap.ID]*PushSubscription `json:"create,omitempty"`
	Update  map[jmap.ID]*jmap.Patch       `json:"update,omitempty"`
diff --git a/core/push/subscription/subscription.go b/core/push/subscription/subscription.go
index bf7f16381870..5c8de6dedc99 100644
--- a/core/push/subscription/subscription.go
+++ b/core/push/subscription/subscription.go
@@ -11,43 +11,21 @@ func init() {
	jmap.RegisterMethod("PushSubscription/set", newSetResponse)
}

// A PushSubscription object
// Server side push notification
// https://www.rfc-editor.org/rfc/rfc8620.html#section-7.2
type PushSubscription struct {
	// The ID of the push subscription
	//
	// immutable;server-set
	ID jmap.ID `json:"id,omitempty"`

	// An ID that uniquely identifies the client + device the subscription
	// is running on
	//
	// immutable
	DeviceClientID string `json:"deviceClientId,omitempty"`

	// An absolute URL where the JMAP server will POST the data for the push
	// message. This must start with "https://"
	//
	// immutable
	URL string `json:"url,omitempty"`

	// Client-generated encryption keys. If specified, the server will
	// encrypt the push data
	Keys *Key `json:"keys,omitempty"`

	// This must be null or omitted when the subscription is created. The
	// JMAP server will generate a code and send it in a push message. The
	// client must then update this field with that code
	VerificationCode string `json:"verificationCode,omitempty"`

	// The time this subscription expires, if specified. If not specified,
	// the subscription does not expire, however the server may specify a
	// time
	//
	// Must be in UTC
	Expires *time.Time `json:"expires,omitempty"`

	// A list of type changes the client is subscribing to, using the same
	// keys as a TypeState object
	Types []string `json:"types,omitempty"`
}

diff --git a/mail/email/changes.go b/mail/email/changes.go
index 1cefbd63064a..894b21c0b430 100644
--- a/mail/email/changes.go
+++ b/mail/email/changes.go
@@ -5,15 +5,13 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard "/changes" method as described in [RFC8620], Section 5.2.
// Get changes to emails on the whole account since a given state
// https://www.rfc-editor.org/rfc/rfc8621.html#section-4.3
type Changes struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The current state of the client
	SinceState string `json:"sinceState,omitempty"`

	// The maximum number of ids to return in the response
	MaxChanges uint64 `json:"maxChanges,omitempty"`
}

@@ -21,22 +19,19 @@ func (m *Changes) Name() string { return "Email/changes" }

func (m *Changes) Requires() []jmap.URI { return []jmap.URI{mail.URI} }

// This is a standard "/changes" method as described in [RFC8620], Section 5.2.
type ChangesResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// This is the sinceState argument echoed back
	OldState string `json:"oldState,omitempty"`

	// The state the client will be in after applying the Changes
	NewState string `json:"newState,omitempty"`

	// If true, not all changes were returned in this response
	HasMoreChanges bool `json:"hasMoreChanges,omitempty"`

	Created   []jmap.ID `json:"created,omitempty"`
	Updated   []jmap.ID `json:"updated,omitempty"`
	Created []jmap.ID `json:"created,omitempty"`

	Updated []jmap.ID `json:"updated,omitempty"`

	Destroyed []jmap.ID `json:"destroyed,omitempty"`
}

diff --git a/mail/email/copy.go b/mail/email/copy.go
index 9421a48cc7c3..6ca7396f5a51 100644
--- a/mail/email/copy.go
+++ b/mail/email/copy.go
@@ -5,19 +5,22 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard "/copy" method as described in [RFC8620], Section 5.4,
// Copy messages from one account to another
// https://www.rfc-editor.org/rfc/rfc8621.html#section-4.7
type Copy struct {
	// The id of the account to copy records from.
	FromAccount   jmap.ID `json:"fromAccountId,omitempty"`
	IfFromInState string  `json:"ifFromInState,omitempty"`
	FromAccount jmap.ID `json:"fromAccountId,omitempty"`

	// The id of the account to copy records to. This MUST be different to
	// the fromAccountId.
	Account                  jmap.ID            `json:"accountId,omitempty"`
	IfInState                string             `json:"ifInState,omitempty"`
	Create                   map[jmap.ID]*Email `json:"create,omitempty"`
	OnSuccessDestroyOriginal bool               `json:"onSuccessDestroyOriginal,omitempty"`
	DestroyFromIfInState     string             `json:"destroyFromIfInState,omitempty"`
	IfFromInState string `json:"ifFromInState,omitempty"`

	Account jmap.ID `json:"accountId,omitempty"`

	IfInState string `json:"ifInState,omitempty"`

	Create map[jmap.ID]*Email `json:"create,omitempty"`

	OnSuccessDestroyOriginal bool `json:"onSuccessDestroyOriginal,omitempty"`

	DestroyFromIfInState string `json:"destroyFromIfInState,omitempty"`
}

func (m *Copy) Name() string { return "Email/copy" }
@@ -25,14 +28,16 @@ func (m *Copy) Name() string { return "Email/copy" }
func (m *Copy) Requires() []jmap.URI { return []jmap.URI{mail.URI} }

type CopyResponse struct {
	// The id of the account records were copied from.
	FromAccount jmap.ID `json:"fromAccountId,omitempty"`

	// The id of the account records were copied to.
	Account    jmap.ID                    `json:"accountId,omitempty"`
	OldState   string                     `json:"oldState,omitempty"`
	NewState   string                     `json:"newState,omitempty"`
	Created    map[jmap.ID]*Email         `json:"created,omitempty"`
	Account jmap.ID `json:"accountId,omitempty"`

	OldState string `json:"oldState,omitempty"`

	NewState string `json:"newState,omitempty"`

	Created map[jmap.ID]*Email `json:"created,omitempty"`

	NotCreated map[jmap.ID]*jmap.SetError `json:"notCreated,omitempty"`
}

diff --git a/mail/email/email.go b/mail/email/email.go
index b8735655456b..ca731e562531 100644
--- a/mail/email/email.go
+++ b/mail/email/email.go
@@ -19,246 +19,112 @@ func init() {
	jmap.RegisterMethod("Email/parse", newParseResponse)
}

// Representation of an RFC5322 message
// https://www.rfc-editor.org/rfc/rfc8621.html#section-4
type Email struct {
	// The ID of the Email. Note: this is _not_ the Message-ID
	//
	// immutable;server-set
	ID jmap.ID `json:"id,omitempty"`

	// The ID of the raw RFC5322 message
	//
	// immutable;server-set
	BlobID jmap.ID `json:"blobId,omitempty"`

	// The id of the Thread to which this Email belongs.
	//
	// immutable;server-set
	ThreadID jmap.ID `json:"threadId,omitempty"`

	// The set of Mailbox ids this Email belongs to. An Email in the mail
	// store MUST belong to one or more Mailboxes at all times (until it
	// is destroyed).
	MailboxIDs map[jmap.ID]bool `json:"mailboxIds,omitempty"`

	// A set of keywords that apply to the Email. Each key must have an
	// associated value of "true"
	Keywords map[string]bool `json:"keywords,omitempty"`

	// The size, in bytes, of the message
	//
	// immutable;server-set
	Size uint64 `json:"size,omitempty"`

	// The date the Email was received. Equivalent to INTERNAL_DATE in IMAP
	//
	// immutable
	ReceivedAt *time.Time `json:"receivedAt,omitempty"`

	// This is a list of all header fields, in the same order they appear in
	// the message.
	//
	// immutable
	Headers []*Header `json:"headers,omitempty"`

	// The Message-ID of the email. For conforming messages, this will be
	// len() == 1
	//
	// immutable
	MessageID []string `json:"messageId,omitempty"`

	// immutable
	InReplyTo []string `json:"inReplyTo,omitempty"`

	// immutable
	References []string `json:"references,omitempty"`

	// immutable
	Sender []*mail.Address `json:"sender,omitempty"`

	// immutable
	From []*mail.Address `json:"from,omitempty"`

	// immutable
	To []*mail.Address `json:"to,omitempty"`

	// immutable
	CC []*mail.Address `json:"cc,omitempty"`

	// immutable
	BCC []*mail.Address `json:"bcc,omitempty"`

	// immutable
	ReplyTo []*mail.Address `json:"replyTo,omitempty"`

	// immutable
	Subject string `json:"subject,omitempty"`

	// SentAt is the Date header value
	//
	// immutable
	SentAt *time.Time `json:"sentAt,omitempty"`

	// This is the full MIME structure of the message body, without
	// recursing into message/rfc822 or message/global parts.
	//
	// immutable
	BodyStructure *BodyPart `json:"bodyStructure,omitempty"`

	// This is a map of partId to an EmailBodyValue object for none, some,
	// or all text/* parts. Which parts are included and whether the value
	// is truncated is determined by various arguments to Email/get and
	// Email/parse.
	//
	// immutable
	BodyValues map[string]*BodyValue `json:"bodyValues,omitempty"`

	// A list of text/plain, text/html, image/*, audio/*, and/or video/*
	// parts to display (sequentially) as the message body, with a
	// preference for text/plain when alternative versions are available.
	//
	// immutable
	TextBody []*BodyPart `json:"textBody,omitempty"`

	// A list of text/plain, text/html, image/*, audio/*, and/or video/*
	// parts to display (sequentially) as the message body, with a
	// preference for text/html when alternative versions are available.
	//
	// immutable
	HTMLBody []*BodyPart `json:"htmlBody,omitempty"`

	// A list, traversing depth-first, of all parts in bodyStructure that
	// satisfy either of the following conditions:
	//
	//     not of type multipart/* and not included in textBody or htmlBody
	//
	//     of type image/*, audio/*, or video/* and not in both textBody
	//     and htmlBody
	//
	// immutable
	Attachments []*BodyPart `json:"attachments,omitempty"`

	// immutable;server-set
	HasAttachment bool `json:"hasAttachment,omitempty"`

	// A plaintext fragment of the message body.	// This MUST NOT be more
	// than 256 characters in length.
	//
	// immutable;server-set
	Preview string `json:"preview,omitempty"`

	// If empty, there is no S/MIME signature. Otherwise will be one of the
	// following strings
	// - "unknown" - Can be returned for OpenPGP signed messages
	// - "signed" - S/MIME signed but not yet verified
	// - "signed/verified" - Signed and verified per RFC8551 and RFC8550
	// - "signed/failed"
	// - "encrypted+signed/verified"
	// - "encrypted+signed/failed"
	//
	// server-set
	SMIMEStatus string `json:"smimeStatus,omitempty"`

	// If empty, there is no S/MIME signature. Otherwise will be one of the
	// following strings, and represents the status at time of delivery
	// - "unknown" - Can be returned for OpenPGP signed messages
	// - "signed" - S/MIME signed but not yet verified
	// - "signed/verified" - Signed and verified per RFC8551 and RFC8550
	// - "signed/failed"
	// - "encrypted+signed/verified"
	// - "encrypted+signed/failed"
	//
	// server-set
	SMIMEStatusAtDelivery string `json:"smimeStatusAtDelivery,omitempty"`

	// If empty, no errors or no signature. Otherwise, this will contain any
	// errors during verification of SMIME properties
	//
	// server-set
	SMIMEErrors []string `json:"smimeErrors,omitempty"`

	// If empty, no signature or not verified. Otherwise, this is the time
	// the signature was most recently verified
	//
	// server-set
	SMIMEVerifiedAt *time.Time `json:"smimeVerifiedAt,omitempty"`
}

type AddressGroup struct {
	// The display-name of the group
	Name      string          `json:"name,omitempty"`
	Name string `json:"name,omitempty"`

	Addresses []*mail.Address `json:"addresses,omitempty"`
}

type Header struct {
	// The header field name, with the same capitalization that it has in
	// the message.
	Name string `json:"name,omitempty"`

	// The header field value in Raw form.
	Value string `json:"value,omitempty"`
}

type BodyPart struct {
	// Identifies this part uniquely within the Email. This is scoped to
	// the emailId and has no meaning outside of the JMAP Email object
	//
	// Multipart messages do not have a PartID
	PartID string `json:"partId,omitempty"`

	// The Blob ID representing this Part
	BlobID jmap.ID `json:"blobId,omitempty"`

	// The number of bytes the user would download
	Size uint64 `json:"size,omitempty"`

	// This is a list of all header fields in the part, in the order they
	// appear in the message. The values are in Raw form.
	Headers []*Header `json:"headers,omitempty"`

	// The filename associated with this Part, if given
	Name string `json:"name,omitempty"`

	// The value of the Content-Type header field of the part, if present;
	// otherwise, the implicit type as per the MIME standard (text/plain or
	// message/rfc822 if inside a multipart/digest)
	Type string `json:"type,omitempty"`

	// The value of the charset parameter of the Content-Type header
	// field, if present, or null if the header field is present but not
	// of type text/*. If there is no Content-Type header field, or it
	// exists and is of type text/* but has no charset parameter, this is
	// the implicit charset as per the MIME standard: us-ascii.
	Charset string `json:"charset,omitempty"`

	// The value of the Content-Disposition header field of the part, if
	// present; otherwise, it’s null. CFWS is removed and any parameters
	// are stripped.
	Disposition string `json:"disposition,omitempty"`

	// The value of the Content-Id header field of the part, if present;
	// otherwise it’s null.
	CID string `json:"cid,omitempty"`

	// The list of language tags, as defined in RFC3282, in the
	// Content-Language header field of the part, if present.
	Language []string `json:"language,omitempty"`

	// The URI, as defined in RFC2557, in the Content-Location header field
	// of the part, if present.
	Location string `json:"location,omitempty"`

	// If the type is multipart/*, this contains the body parts of each
	// child.
	SubParts []*BodyPart `json:"subParts,omitempty"`
}

type BodyValue struct {
	// The value of the BodyValue
	Value string `json:"value,omitempty"`

	// True if there was an encoding problem
	IsEncodingProblem bool `json:"isEncodingProblem,omitempty"`

	// This is true if the value has been truncated
	IsTruncated bool `json:"isTruncated"`
}
diff --git a/mail/email/filter.go b/mail/email/filter.go
index 288985d30215..5c6dc146a9b8 100644
--- a/mail/email/filter.go
+++ b/mail/email/filter.go
@@ -12,113 +12,60 @@ type Filter interface {
}

type FilterOperator struct {
	Operator   jmap.Operator `json:"operator,omitempty"`
	Conditions []Filter      `json:"conditions,omitempty"`
	Operator jmap.Operator `json:"operator,omitempty"`

	Conditions []Filter `json:"conditions,omitempty"`
}

func (fo *FilterOperator) implementsFilter() {}

// EmailFilterCondition is an interface that represents FilterCondition
// objects. A filter condition object can be either a named struct, ie
// EmailFilterConditionName, or an EmailFilter itself. EmailFilters can
// be used to create complex filtering
// Email query condition that can be compounded with FilterOperator
// https://www.rfc-editor.org/rfc/rfc8621.html#section-4.4.1
type FilterCondition struct {
	// A Mailbox id.  An Email must be in this Mailbox to match the condition.
	InMailbox jmap.ID `json:"inMailbox,omitempty"`

	// A list of Mailbox ids.  An Email must be in at least one Mailbox not in this
	// list to match the condition.  This is to allow messages solely in trash/spam
	// to be easily excluded from a search.
	InMailboxOtherThan []jmap.ID `json:"inMailboxOtherThan,omitempty"`

	// The "receivedAt" date-time of the Email must be before this date- time to
	// match the condition.
	Before *time.Time `json:"before,omitempty"`

	// The "receivedAt" date-time of the Email must be the same or after this
	// date-time to match the condition.
	After *time.Time `json:"after,omitempty"`

	// The "size" property of the Email must be equal to or greater than this
	// number to match the condition.
	MinSize uint64 `json:"minSize,omitempty"`

	// The "size" property of the Email must be less than this number to match the
	// condition.
	MaxSize uint64 `json:"maxSize,omitempty"`

	// All Emails (including this one) in the same Thread as this Email must have
	// the given keyword to match the condition.
	AllInThreadHaveKeyword string `json:"allInThreadHaveKeyword,omitempty"`

	// At least one Email (possibly this one) in the same Thread as this Email must
	// have the given keyword to match the condition.
	SomeInThreadHaveKeyword string `json:"someInThreadHaveKeyword,omitempty"`

	// All Emails (including this one) in the same Thread as this Email must *not*
	// have the given keyword to match the condition.
	NoneInThreadHaveKeyword string `json:"noneInThreadHaveKeyword,omitempty"`

	// This Email must have the given keyword to match the condition.
	HasKeyword string `json:"hasKeyword,omitempty"`

	// This Email must not have the given keyword to match the condition.
	NotKeyword string `json:"notKeyword,omitempty"`

	// The "hasAttachment" property of the Email must be identical to the value
	// given to match the condition.
	HasAttachment bool `json:"hasAttachment,omitempty"`

	// Looks for the text in Emails.  The server MUST look up text in the From, To,
	// Cc, Bcc, and Subject header fields of the message and SHOULD look inside any
	// "text/*" or other body parts that may be converted to text by the server.
	// The server MAY extend the search to any additional textual property.
	Text string `json:"text,omitempty"`

	// Looks for the text in the From header field of the message.
	From string `json:"from,omitempty"`

	// Looks for the text in the To header field of the message.
	To string `json:"to,omitempty"`

	// Looks for the text in the Cc header field of the message.
	Cc string `json:"cc,omitempty"`

	// Looks for the text in the Bcc header field of the message.
	Bcc string `json:"bcc,omitempty"`

	// Looks for the text in the Subject header field of the message.
	Subject string `json:"subject,omitempty"`

	// Looks for the text in one of the body parts of the message.  The server MAY
	// exclude MIME body parts with content media types other than "text/*" and
	// "message/*" from consideration in search matching.  Care should be taken to
	// match based on the text content actually presented to an end user by viewers
	// for that media type or otherwise identified as appropriate for search
	// indexing. Matching document metadata uninteresting to an end user (e.g.,
	// markup tag and attribute names) is undesirable.
	Body string `json:"body,omitempty"`

	// The array MUST contain either one or two elements.  The first element is the
	// name of the header field to match against.  The second (optional) element is
	// the text to look for in the header field value.  If not supplied, the
	// message matches simply if it has a header field of the given name.
	Header []string `json:"header,omitempty"`

	// When true, only messages where smimeStatus is not null match
	//
	// Requires server to support urn:ietf:jmap:smimeverify
	HasSMIME bool `json:"hasSmime,omitempty"`

	// When true, only messages with successfully verified SMIME match
	//
	// Requires server to support urn:ietf:jmap:smimeverify
	HasVerifiedSMIME bool `json:"hasVerifiedSmime,omitempty"`

	// When true, only messages with successfully verified SMIME at the time
	// of delivery match
	//
	// Requires server to support urn:ietf:jmap:smimeverify
	HasVerifiedSMIMEAtDelivery bool `json:"hasVerifiedSmimeAtDelivery,omitempty"`
}

diff --git a/mail/email/get.go b/mail/email/get.go
index 41418e22e1ae..9aaad53057d5 100644
--- a/mail/email/get.go
+++ b/mail/email/get.go
@@ -5,68 +5,27 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard get request
//
// If the standard "properties" argument is omitted or null, the following
// default MUST be used instead of "all" properties:
//
// [ "id", "blobId", "threadId", "mailboxIds", "keywords", "size",
// "receivedAt", "messageId", "inReplyTo", "references", "sender", "from",
// "to", "cc", "bcc", "replyTo", "subject", "sentAt", "hasAttachment",
// "preview", "bodyValues", "textBody", "htmlBody", "attachments" ]
// Get email details
// https://www.rfc-editor.org/rfc/rfc8621.html#section-4.2
type Get struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The ids of the Foo objects to return. If null, then all records of
	// the data type are returned, if this is supported for that data type
	// and the number of records does not exceed the maxObjectsInGet limit.
	IDs []jmap.ID `json:"ids,omitempty"`

	// If supplied, only the properties listed in the array are returned
	// for each Foo object. If null, all properties of the object are
	// returned. The id property of the object is always returned, even if
	// not explicitly requested. If an invalid property is requested, the
	// call MUST be rejected with an invalidArguments error.
	Properties []string `json:"properties,omitempty"`

	// A list of properties to fetch for each EmailBodyPart returned.  If
	// omitted, this defaults to:
	//
	//    [ "partId", "blobId", "size", "name", "type", "charset",
	//      "disposition", "cid", "language", "location" ]
	BodyProperties []string `json:"bodyProperties,omitempty"`

	// If true, the "bodyValues" property includes any "text/*" part in
	// the "textBody" property.
	FetchTextBodyValues bool `json:"fetchTextBodyValues,omitempty"`

	// If true, the "bodyValues" property includes any "text/*" part in
	// the "htmlBody" property.
	FetchHTMLBodyValues bool `json:"fetchHTMLBodyValues,omitempty"`

	// If true, the "bodyValues" property includes any "text/*" part in
	// the "bodyStructure" property.
	FetchAllBodyValues bool `json:"fetchAllBodyValues,omitempty"`

	// If greater than zero, the "value" property of any EmailBodyValue
	// object returned in "bodyValues" MUST be truncated if necessary so
	// it does not exceed this number of octets in size.  If 0 (the
	// default), no truncation occurs.
	//
	// The server MUST ensure the truncation results in valid UTF-8 and
	// does not occur mid-codepoint.  If the part is of type "text/html",
	// the server SHOULD NOT truncate inside an HTML tag, e.g., in the
	// middle of "<a href="https://example.com">".  There is no
	// requirement for the truncated form to be a balanced tree or valid
	// HTML (indeed, the original source may well be neither of these
	// things).
	MaxBodyValueBytes uint64 `json:"maxBodyValueBytes,omitempty"`

	// Use IDs from a previous call
	ReferenceIDs *jmap.ResultReference `json:"#ids,omitempty"`

	// Use IDs from a previous call
	ReferenceProperties *jmap.ResultReference `json:"#properties,omitempty"`
}

@@ -74,35 +33,13 @@ func (m *Get) Name() string { return "Email/get" }

func (m *Get) Requires() []jmap.URI { return []jmap.URI{mail.URI} }

// This is a standard “/get” method as described in [@!RFC8620], Section 5.1.
type GetResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// A (preferably short) string representing the state on the server for
	// all the data of this type in the account (not just the objects
	// returned in this call). If the data changes, this string MUST
	// change. If the Foo data is unchanged, servers SHOULD return the same
	// state string on subsequent requests for this data type.
	//
	// When a client receives a response with a different state string to a
	// previous call, it MUST either throw away all currently cached
	// objects for the type or call Foo/changes to get the exact changes.
	State string `json:"state,omitempty"`

	// An array of the Foo objects requested. This is the empty array
	// if no objects were found or if the ids argument passed in was also
	// an empty array. The results MAY be in a different order to the ids
	// in the request arguments. If an identical id is included more than
	// once in the request, the server MUST only include it once in either
	// the list or the notFound argument of the response.
	//
	// Each specification must define it's own List property
	List []*Email `json:"list,omitempty"`

	// This array contains the ids passed to the method for records that do
	// not exist. The array is empty if all requested ids were found or if
	// the ids argument passed in was either null or an empty array.
	NotFound []jmap.ID `json:"notFound,omitempty"`
}

diff --git a/mail/email/import.go b/mail/email/import.go
index eba8ebfd811f..8186a19cc46c 100644
--- a/mail/email/import.go
+++ b/mail/email/import.go
@@ -7,22 +7,13 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// The "Email/import" method adds messages [RFC5322] to the set of Emails in an
// account.  The server MUST support messages with Email Address
// Internationalization (EAI) headers [RFC6532].  The messages must first be
// uploaded as blobs using the standard upload mechanism.
// Import email from binary blobs
// https://www.rfc-editor.org/rfc/rfc8621.html#section-4.8
type Import struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// This is a state string as returned by the Foo/get method
	// (representing the state of all objects of this type in the account).
	// If supplied, the string must match the current state; otherwise, the
	// method will be aborted and a stateMismatch error returned. If null,
	// any changes will be applied to the current state.
	IfInState string `json:"ifInState,omitempty"`

	// A map of creation id (client specified) to EmailImport objects.
	Emails map[string]*EmailImport `json:"emails,omitempty"`
}

@@ -31,44 +22,24 @@ func (m *Import) Name() string { return "Email/import" }
func (m *Import) Requires() []jmap.URI { return []jmap.URI{mail.URI} }

type EmailImport struct {
	// The id of the blob containing the raw message [RFC5322].
	BlobID jmap.ID `json:"blobId,omitempty"`

	// The ids of the Mailboxes to assign this Email to.  At least one
	// Mailbox MUST be given.
	MailboxIDs map[jmap.ID]bool `json:"mailboxIds,omitempty"`

	// The keywords to apply to the Email.
	Keywords map[string]bool `json:"keywords,omitempty"`

	// The "receivedAt" date to set on the Email. The value must be in UTC
	ReceivedAt *time.Time `json:"receivedAt,omitempty"`
}

type ImportResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// The state string that would have been returned by Foo/get before
	// making the requested changes, or null if the server doesn’t know
	// what the previous state string was.
	OldState string `json:"oldState,omitempty"`

	// The state string that will now be returned by Foo/get.
	NewState string `json:"newState,omitempty"`

	// A map of the creation id to an object containing any properties of
	// the created Foo object that were not sent by the client. This
	// includes all server-set properties (such as the id in most object
	// types) and any properties that were omitted by the client and thus
	// set to a default by the server.
	//
	// This argument is null if no Foo objects were successfully created.
	Created map[jmap.ID]*Email `json:"created,omitempty"`

	// A map of the creation id to a SetError object for each Email that
	// failed to be created, or null if all successful.  The possible
	// errors are defined above.
	NotCreated map[jmap.ID]*jmap.SetError `json:"notCreated,omitempty"`
}

diff --git a/mail/email/parse.go b/mail/email/parse.go
index d98f1fda195a..66da9ba6747a 100644
--- a/mail/email/parse.go
+++ b/mail/email/parse.go
@@ -5,71 +5,23 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This method allows you to parse blobs as messages [RFC5322] to get
// Email objects.  The server MUST support messages with EAI headers
// [RFC6532].  This can be used to parse and display attached messages
// without having to import them as top-level Email objects in the mail
// store in their own right.
//
// The following metadata properties on the Email objects will be null
// if requested:
//
// o  id
//
// o  mailboxIds
//
// o  keywords
//
// o  receivedAt
//
// The "threadId" property of the Email MAY be present if the server can
// calculate which Thread the Email would be assigned to were it to be
// imported.  Otherwise, this too is null if fetched.
// Parse binary blobs as RFC5322 messages
// https://www.rfc-editor.org/rfc/rfc8621.html#section-4.9
type Parse struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The ids of the blobs to parse.
	BlobIDs []jmap.ID `json:"blobIds,omitempty"`

	// If supplied, only the properties listed in the array are returned
	// for each Foo object. If null, all properties of the object are
	// returned. The id property of the object is always returned, even if
	// not explicitly requested. If an invalid property is requested, the
	// call MUST be rejected with an invalidArguments error.
	Properties []string `json:"properties,omitempty"`

	// A list of properties to fetch for each EmailBodyPart returned.  If
	// omitted, this defaults to:
	//
	//    [ "partId", "blobId", "size", "name", "type", "charset",
	//      "disposition", "cid", "language", "location" ]
	BodyProperties []string `json:"bodyProperties,omitempty"`

	// If true, the "bodyValues" property includes any "text/*" part in
	// the "textBody" property.
	FetchTextBodyValues bool `json:"fetchTextBodyValues,omitempty"`

	// If true, the "bodyValues" property includes any "text/*" part in
	// the "htmlBody" property.
	FetchHTMLBodyValues bool `json:"fetchHTMLBodyValues,omitempty"`

	// If true, the "bodyValues" property includes any "text/*" part in
	// the "bodyStructure" property.
	FetchAllBodyValues bool `json:"fetchAllBodyValues,omitempty"`

	// If greater than zero, the "value" property of any EmailBodyValue
	// object returned in "bodyValues" MUST be truncated if necessary so
	// it does not exceed this number of octets in size.  If 0 (the
	// default), no truncation occurs.
	//
	// The server MUST ensure the truncation results in valid UTF-8 and
	// does not occur mid-codepoint.  If the part is of type "text/html",
	// the server SHOULD NOT truncate inside an HTML tag, e.g., in the
	// middle of "<a href="https://example.com">".  There is no
	// requirement for the truncated form to be a balanced tree or valid
	// HTML (indeed, the original source may well be neither of these
	// things).
	MaxBodyValueBytes uint64 `json:"maxBodyValueBytes,omitempty"`
}

@@ -78,18 +30,12 @@ func (m *Parse) Name() string { return "Email/parse" }
func (m *Parse) Requires() []jmap.URI { return []jmap.URI{mail.URI} }

type ParseResponse struct {
	// The id of the account used for the call
	Account jmap.ID `json:"accountId,omitempty"`

	// A map of blob id to parsed Email representation for each
	// successfully parsed blob, or null if none.
	Parsed map[jmap.ID]*Email `json:"parsed,omitempty"`

	// A list of ids given that corresponded to blobs that could not be
	// parsed as Emails, or null if none.
	NotParsable []jmap.ID `json:"notParsable,omitempty"`

	// A list of blob ids given that could not be found, or null if none.
	NotFound []jmap.ID `json:"notFound,omitempty"`
}

diff --git a/mail/email/query.go b/mail/email/query.go
index aeaa98e6884e..f6255d987e3f 100644
--- a/mail/email/query.go
+++ b/mail/email/query.go
@@ -5,88 +5,25 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard "/query" method as described in [RFC8620], Section 5.5
// but with the following additional request arguments:
// Get list of email IDs based on filter and sort criteria
// https://www.rfc-editor.org/rfc/rfc8621.html#section-4.4
type Query struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// Determines the set of Foos returned in the results. If null, all
	// objects in the account of this type are included in the results.
	//
	// Each implementation must implement it's own Filter
	Filter Filter `json:"filter,omitempty"`

	// Lists the names of properties to compare between two Foo records,
	// and how to compare them, to determine which comes first in the sort.
	// If two Foo records have an identical value for the first comparator,
	// the next comparator will be considered, and so on. If all
	// comparators are the same (this includes the case where an empty
	// array or null is given as the sort argument), the sort order is
	// server dependent, but it MUST be stable between calls to Foo/query.
	//
	// Each implementation must define it's own Sort property. The
	// SortComparator object can be used as a basis
	Sort []*SortComparator `json:"sort,omitempty"`

	// The zero-based index of the first id in the full list of results to
	// return.
	//
	// If a negative value is given, it is an offset from the end of the
	// list. Specifically, the negative value MUST be added to the total
	// number of results given the filter, and if still negative, it’s
	// clamped to 0. This is now the zero-based index of the first id to
	// return.
	//
	// If the index is greater than or equal to the total number of objects
	// in the results list, then the ids array in the response will be
	// empty, but this is not an error.
	Position int64 `json:"position,omitempty"`

	// A Foo id. If supplied, the position argument is ignored. The index
	// of this id in the results will be used in combination with the
	// anchorOffset argument to determine the index of the first result to
	// return (see below for more details).
	//
	// If an anchor argument is given, the anchor is looked for in the
	// results after filtering and sorting. If found, the anchorOffset is
	// then added to its index. If the resulting index is now negative, it
	// is clamped to 0. This index is now used exactly as though it were
	// supplied as the position argument. If the anchor is not found, the
	// call is rejected with an anchorNotFound error.
	//
	// If an anchor is specified, any position argument supplied by the
	// client MUST be ignored. If no anchor is supplied, any anchorOffset
	// argument MUST be ignored.
	//
	// A client can use anchor instead of position to find the index of an
	// id within a large set of results.
	Anchor jmap.ID `json:"anchor,omitempty"`

	// The index of the first result to return relative to the index of the
	// anchor, if an anchor is given. This MAY be negative. For example, -1
	// means the Foo immediately preceding the anchor is the first result
	// in the list returned (see below for more details).
	AnchorOffset int64 `json:"anchorOffset,omitempty"`

	// The maximum number of results to return. If null, no limit presumed.
	// The server MAY choose to enforce a maximum limit argument. In this
	// case, if a greater value is given (or if it is null), the limit is
	// clamped to the maximum; the new limit is returned with the response
	// so the client is aware. If a negative value is given, the call MUST
	// be rejected with an invalidArguments error.
	Limit uint64 `json:"limit,omitempty"`

	// Does the client wish to know the total number of results in the
	// query? This may be slow and expensive for servers to calculate,
	// particularly with complex filters, so clients should take care to
	// only request the total when needed.
	CalculateTotal bool `json:"calculateTotal,omitempty"`

	// If true, Emails in the same Thread as a previous Email in the list
	// (given the filter and sort order) will be removed from the list.
	// This means only one Email at most will be included in the list for
	// any given Thread.
	CollapseThreads bool `json:"collapseThreads,omitempty"`
}

@@ -95,56 +32,18 @@ func (m *Query) Name() string { return "Email/query" }
func (m *Query) Requires() []jmap.URI { return []jmap.URI{mail.URI} }

type QueryResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// A string encoding the current state of the query on the server. This
	// string MUST change if the results of the query (i.e., the matching
	// ids and their sort order) have changed. The queryState string MAY
	// change if something has changed on the server, which means the
	// results may have changed but the server doesn’t know for sure.
	//
	// The queryState string only represents the ordered list of ids that
	// match the particular query (including its sort/filter). There is no
	// requirement for it to change if a property on an object matching the
	// query changes but the query results are unaffected (indeed, it is
	// more efficient if the queryState string does not change in this
	// case). The queryState string only has meaning when compared to
	// future responses to a query with the same type/sort/filter or when
	// used with /queryChanges to fetch changes.
	//
	// Should a client receive back a response with a different queryState
	// string to a previous call, it MUST either throw away the currently
	// cached query and fetch it again (note, this does not require
	// fetching the records again, just the list of ids) or call
	// Foo/queryChanges to get the difference.
	QueryState string `json:"queryState,omitempty"`

	// This is true if the server supports calling Foo/queryChanges with
	// these filter/sort parameters. Note, this does not guarantee that the
	// Foo/queryChanges call will succeed, as it may only be possible for a
	// limited time afterwards due to server internal implementation
	// details.
	CanCalculateChanges bool `json:"canCalculateChanges,omitempty"`

	// The zero-based index of the first result in the ids array within the
	// complete list of query results.
	Position uint64 `json:"position,omitempty"`

	// The list of ids for each Foo in the query results, starting at the
	// index given by the position argument of this response and continuing
	// until it hits the end of the results or reaches the limit number of
	// ids. If position is >= total, this MUST be the empty list.
	IDs []jmap.ID `json:"ids,omitempty"`

	// The total number of Foos in the results (given the filter). This
	// argument MUST be omitted if the calculateTotal request argument is
	// not true.
	Total uint64 `json:"total,omitempty"`

	// The limit enforced by the server on the maximum number of results to
	// return. This is only returned if the server set a limit or used a
	// different limit than that given in the request.
	Limit uint64 `json:"limit,omitempty"`
}

diff --git a/mail/email/querychanges.go b/mail/email/querychanges.go
index 219fbe60290a..aa11c723320c 100644
--- a/mail/email/querychanges.go
+++ b/mail/email/querychanges.go
@@ -5,51 +5,23 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard "/queryChanges" method as described in [RFC8620], Section
// 5.6 with the following additional request argument: collapseThreads
// Get changes to an email query since a given state
// https://www.rfc-editor.org/rfc/rfc8621.html#section-4.5
type QueryChanges struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The filter argument that was used with Foo/query.
	//
	// Each implementation must supply it's own Filter property
	Filter Filter `json:"filter,omitempty"`

	// The sort argument that was used with Foo/query.
	//
	// Each implementation must supply it's own Sort property
	Sort []*SortComparator `json:"sort,omitempty"`

	// The current state of the query in the client. This is the string
	// that was returned as the queryState argument in the Foo/query
	// response with the same sort/filter. The server will return the
	// changes made to the query since this state.
	SinceQueryState string `json:"sinceQueryState,omitempty"`

	// The maximum number of changes to return in the response. See error
	// descriptions below for more details.
	MaxChanges uint64 `json:"maxChanges,omitempty"`

	// The last (highest-index) id the client currently has cached from the
	// query results. When there are a large number of results, in a common
	// case, the client may have only downloaded and cached a small subset
	// from the beginning of the results. If the sort and filter are both
	// only on immutable properties, this allows the server to omit changes
	// after this point in the results, which can significantly increase
	// efficiency. If they are not immutable, this argument is ignored.
	UpToID jmap.ID `json:"upToId,omitempty"`

	// Does the client wish to know the total number of results now in the
	// query? This may be slow and expensive for servers to calculate,
	// particularly with complex filters, so clients should take care to
	// only request the total when needed.
	CalculateTotal bool `json:"calculateTotal,omitempty"`

	// If true, Emails in the same Thread as a previous Email in the list
	// (given the filter and sort order) will be removed from the list.
	// This means only one Email at most will be included in the list for
	// any given Thread.
	CollapseThreads bool `json:"collapseThreads,omitempty"`
}

@@ -57,49 +29,15 @@ func (m *QueryChanges) Name() string { return "Email/queryChanges" }

func (m *QueryChanges) Requires() []jmap.URI { return []jmap.URI{mail.URI} }

// This is a standard "/queryChanges" method as described in [RFC8620], Section
// 5.6
type QueryChangesResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// This is the sinceQueryState argument echoed back; that is, the state
	// from which the server is returning changes.
	OldQueryState string `json:"oldQueryState,omitempty"`

	// This is the state the query will be in after applying the set of
	// changes to the old state.
	NewQueryState string `json:"newQueryState,omitempty"`

	// The id for every Foo that was in the query results in the old state
	// and that is not in the results in the new state.
	//
	// If the server cannot calculate this exactly, the server MAY return
	// the ids of extra Foos in addition that may have been in the old
	// results but are not in the new results.
	//
	// If the sort and filter are both only on immutable properties and an
	// upToId is supplied and exists in the results, any ids that were
	// removed but have a higher index than upToId SHOULD be omitted.
	//
	// If the filter or sort includes a mutable property, the server MUST
	// include all Foos in the current results for which this property may
	// have changed. The position of these may have moved in the results,
	// so must be reinserted by the client to ensure its query cache is
	// correct.
	Removed []jmap.ID `json:"removed,omitempty"`

	// The id and index in the query results (in the new state) for every
	// Foo that has been added to the results since the old state AND every
	// Foo in the current results that was included in the removed array
	// (due to a filter or sort based upon a mutable property).
	//
	// If the sort and filter are both only on immutable properties and an
	// upToId is supplied and exists in the results, any ids that were
	// added but have a higher index than upToId SHOULD be omitted.
	//
	// The array MUST be sorted in order of index, with the lowest index
	// first.
	Added []jmap.AddedItem `json:"added,omitempty"`
}

diff --git a/mail/email/set.go b/mail/email/set.go
index c046527c962b..83e4296da7b7 100644
--- a/mail/email/set.go
+++ b/mail/email/set.go
@@ -5,141 +5,17 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard "/set" method as described in [RFC8620], Section 5.3. The
// "Email/set" method encompasses:
//
// o  Creating a draft
//
// o  Changing the keywords of an Email (e.g., unread/flagged status)
//
// o  Adding/removing an Email to/from Mailboxes (moving a message)
//
// o  Deleting Emails
//
// The format of the "keywords"/"mailboxIds" properties means that when
// updating an Email, you can either replace the entire set of keywords/
// Mailboxes (by setting the full value of the property) or add/remove
// individual ones using the JMAP patch syntax (see [RFC8620],
// Section 5.3 for the specification and Section 5.7 for an example).
//
// Due to the format of the Email object, when creating an Email, there
// are a number of ways to specify the same information.  To ensure that
// the message [RFC5322] to create is unambiguous, the following
// constraints apply to Email objects submitted for creation:
//
//   - The "headers" property MUST NOT be given on either the top-level
//     Email or an EmailBodyPart -- the client must set each header field
//     as an individual property.
//
// o  There MUST NOT be two properties that represent the same header
//
//	field (e.g., "header:from" and "from") within the Email or
//	particular EmailBodyPart.
//
// o  Header fields MUST NOT be specified in parsed forms that are
//
//	forbidden for that particular field.
//
// o  Header fields beginning with "Content-" MUST NOT be specified on
//
//	the Email object, only on EmailBodyPart objects.
//
// o  If a "bodyStructure" property is given, there MUST NOT be
//
//	"textBody", "htmlBody", or "attachments" properties.
//
// o  If given, the "bodyStructure" EmailBodyPart MUST NOT contain a
//
//	property representing a header field that is already defined on
//	the top-level Email object.
//
// o  If given, textBody MUST contain exactly one body part and it MUST
//
//	be of type "text/plain".
//
// o  If given, htmlBody MUST contain exactly one body part and it MUST
//
//	be of type "text/html".
//
// -  Within an EmailBodyPart:
//
//   - The client may specify a partId OR a blobId, but not both.  If
//     a partId is given, this partId MUST be present in the
//     "bodyValues" property.
//
//   - The "charset" property MUST be omitted if a partId is given
//     (the part's content is included in bodyValues, and the server
//     may choose any appropriate encoding).
//
//   - The "size" property MUST be omitted if a partId is given.  If a
//     blobId is given, it may be included but is ignored by the
//     server (the size is actually calculated from the blob content
//     itself).
//
//   - A Content-Transfer-Encoding header field MUST NOT be given.
// Create, delete or update emails
// https://www.rfc-editor.org/rfc/rfc8621.html#section-4.6
type Set struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// This is a state string as returned by the Foo/get method
	// (representing the state of all objects of this type in the account).
	// If supplied, the string must match the current state; otherwise, the
	// method will be aborted and a stateMismatch error returned. If null,
	// any changes will be applied to the current state.
	IfInState string `json:"ifInState,omitempty"`

	// A map of a creation id (a temporary id set by the client) to Foo
	// objects, or null if no objects are to be created.
	//
	// The Foo object type definition may define default values for
	// properties. Any such property may be omitted by the client.
	//
	// The client MUST omit any properties that may only be set by the
	// server (for example, the id property on most object types).
	Create map[jmap.ID]*Email `json:"create,omitempty"`

	// A map of an id to a Patch object to apply to the current Foo object
	// with that id, or null if no objects are to be updated.
	//
	// A PatchObject is of type String[*] and represents an unordered set
	// of patches. The keys are a path in JSON Pointer Format [@!RFC6901],
	// with an implicit leading “/” (i.e., prefix each key with “/” before
	// applying the JSON Pointer evaluation algorithm).
	//
	// All paths MUST also conform to the following restrictions; if there
	// is any violation, the update MUST be rejected with an invalidPatch
	// error:
	//
	//     The pointer MUST NOT reference inside an array (i.e., you MUST
	//     NOT insert/delete from an array; the array MUST be replaced in
	//     its entirety instead). All parts prior to the last (i.e., the
	//     value after the final slash) MUST already exist on the object
	//     being patched. There MUST NOT be two patches in the PatchObject
	//     where the pointer of one is the prefix of the pointer of the
	//     other, e.g., “alerts/1/offset” and “alerts”.
	//
	// The value associated with each pointer determines how to apply that
	// patch:
	//
	//     If null, set to the default value if specified for this
	//     property; otherwise, remove the property from the patched
	//     object. If the key is not present in the parent, this a no-op.
	//     Anything else: The value to set for this property (this may be a
	//     replacement or addition to the object being patched).
	//
	// Any server-set properties MAY be included in the patch if their
	// value is identical to the current server value (before applying the
	// patches to the object). Otherwise, the update MUST be rejected with
	// an invalidProperties SetError.
	//
	// This patch definition is designed such that an entire Foo object is
	// also a valid PatchObject. The client may choose to optimise network
	// usage by just sending the diff or may send the whole object; the
	// server processes it the same either way.
	Update map[jmap.ID]jmap.Patch `json:"update,omitempty"`

	// A list of ids for Foo objects to permanently delete, or null if no
	// objects are to be destroyed.
	Destroy []jmap.ID `json:"destroy,omitempty"`
}

@@ -148,48 +24,22 @@ func (m *Set) Name() string { return "Email/set" }
func (m *Set) Requires() []jmap.URI { return []jmap.URI{mail.URI} }

type SetResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// The state string that would have been returned by Foo/get before
	// making the requested changes, or null if the server doesn’t know
	// what the previous state string was.
	OldState string `json:"oldState,omitempty"`

	// The state string that will now be returned by Foo/get.
	NewState string `json:"newState,omitempty"`

	// A map of the creation id to an object containing any properties of
	// the created Foo object that were not sent by the client. This
	// includes all server-set properties (such as the id in most object
	// types) and any properties that were omitted by the client and thus
	// set to a default by the server.
	//
	// This argument is null if no Foo objects were successfully created.
	Created map[jmap.ID]*Email `json:"created,omitempty"`

	// The keys in this map are the ids of all Foos that were successfully
	// updated.
	//
	// The value for each id is a Foo object containing any property that
	// changed in a way not explicitly requested by the PatchObject sent to
	// the server, or null if none. This lets the client know of any
	// changes to server-set or computed properties.
	//
	// This argument is null if no Foo objects were successfully updated.
	Updated map[jmap.ID]*Email `json:"updated,omitempty"`

	// An array of ids for records that have been destroyed since the old
	// state.
	Destroyed []jmap.ID `json:"destroyed,omitempty"`

	// A map of ID to a SetError for each record that failed to be created
	NotCreated map[jmap.ID]*jmap.SetError `json:"notCreated,omitempty"`

	// A map of ID to a SetError for each record that failed to be updated
	NotUpdated map[jmap.ID]*jmap.SetError `json:"notUpdated,omitempty"`

	// A map of ID to a SetError for each record that failed to be destroyed
	NotDestroyed map[jmap.ID]*jmap.SetError `json:"notDestroyed,omitempty"`
}

diff --git a/mail/email/sort.go b/mail/email/sort.go
index 4c9bceebdc9b..f28e396aab96 100644
--- a/mail/email/sort.go
+++ b/mail/email/sort.go
@@ -2,51 +2,14 @@ package email

import "git.sr.ht/~rockorager/go-jmap"

// Email sort criteria
// https://www.rfc-editor.org/rfc/rfc8621.html#section-4.4.2
type SortComparator struct {
	// The name of the property on the Foo objects to compare. Servers MUST
	// support sorting by the following properties:
	// - receivedAt
	//
	// Additional supported properties are reported in the mail capability
	// object
	Property string `json:"property,omitempty"`

	// When specifying a "hasKeyword", "allInThreadHaveKeyword", or
	// "someInThreadHaveKeyword" sort, the Comparator object MUST also have
	// a "keyword" property.
	Keyword string `json:"keyword,omitempty"`

	// If true, sort in ascending order. If false, reverse the comparator’s
	// results to sort in descending order.
	IsAscending bool `json:"isAscending,omitempty"`

	// The identifier, as registered in the collation registry defined in
	// [@!RFC4790], for the algorithm to use when comparing the order of
	// strings. The algorithms the server supports are advertised in the
	// capabilities object returned with the Session object (see Section
	// 2).
	//
	// If omitted, the default algorithm is server-dependent, but:
	//
	//     It MUST be unicode-aware. It MAY be selected based on an
	//     Accept-Language header in the request (as defined in
	//     [@!RFC7231], Section 5.3.5), or out-of-band information about
	//     the user’s language/locale. It SHOULD be case insensitive where
	//     such a concept makes sense for a language/locale. Where the
	//     user’s language is unknown, it is RECOMMENDED to follow the
	//     advice in Section 5.2.3 of [@!RFC8264].
	//
	// The “i;unicode-casemap” collation [@!RFC5051] and the Unicode
	// Collation Algorithm (http://www.unicode.org/reports/tr10/) are two
	// examples that fulfil these criterion and provide reasonable
	// behaviour for a large number of languages.
	//
	// When the property being compared is not a string, the collation
	// property is ignored, and the following comparison rules apply based
	// on the type. In ascending order:
	//
	//     Boolean: false comes before true. Number: A lower number comes
	//     before a higher number. Date/UTCDate: The earlier date comes
	//     first.
	Collation jmap.CollationAlgo `json:"collation,omitempty"`
}
diff --git a/mail/emailsubmission/changes.go b/mail/emailsubmission/changes.go
index 4d9e72a308fb..f8aec24ccaf5 100644
--- a/mail/emailsubmission/changes.go
+++ b/mail/emailsubmission/changes.go
@@ -5,22 +5,13 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard “/changes” method as described in [@!RFC8620], Section 5.2.
// Get email submission changes for the whole account
// https://www.rfc-editor.org/rfc/rfc8621.html#section-7.2
type Changes struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The current state of the client. This is the string that was
	// returned as the state argument in the Foo/get response. The server
	// will return the changes that have occurred since this state.
	SinceState string `json:"sinceState,omitempty"`

	// The maximum number of ids to return in the response. The server MAY
	// choose to return fewer than this value but MUST NOT return more. If
	// not given by the client, the server may choose how many to return.
	// If supplied by the client, the value MUST be a positive integer
	// greater than 0. If a value outside of this range is given, the
	// server MUST reject the call with an invalidArguments error.
	MaxChanges uint64 `json:"maxChanges,omitempty"`
}

@@ -28,34 +19,19 @@ func (m *Changes) Name() string { return "EmailSubmission/changes" }

func (m *Changes) Requires() []jmap.URI { return []jmap.URI{URI, mail.URI} }

// This is a standard “/changes” method as described in [@!RFC8620], Section 5.2.
type ChangesResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// This is the sinceState argument echoed back; it’s the state from
	// which the server is returning changes.
	OldState string `json:"oldState,omitempty"`

	// This is the state the client will be in after applying the set of
	// changes to the old state.
	NewState string `json:"newState,omitempty"`

	// If true, the client may call Foo/changes again with the newState
	// returned to get further updates. If false, newState is the current
	// server state.
	HasMoreChanges bool `json:"hasMoreChanges,omitempty"`

	// An array of ids for records that have been created since the old
	// state.
	Created []jmap.ID `json:"created,omitempty"`

	// An array of ids for records that have been updated since the old
	// state.
	Updated []jmap.ID `json:"updated,omitempty"`

	// An array of ids for records that have been destroyed since the old
	// state.
	Destroyed []jmap.ID `json:"destroyed,omitempty"`
}

diff --git a/mail/emailsubmission/emailsubmission.go b/mail/emailsubmission/emailsubmission.go
index 949cc3b48ee4..e679c7957334 100644
--- a/mail/emailsubmission/emailsubmission.go
+++ b/mail/emailsubmission/emailsubmission.go
@@ -35,57 +35,27 @@ func (m *Capability) URI() jmap.URI { return URI }

func (m *Capability) New() jmap.Capability { return &Capability{} }

// Submission of an Email for delivery to one or more recipients.
// https://www.rfc-editor.org/rfc/rfc8621.html#section-7
type EmailSubmission struct {
	// The ID of the [EmailSubmission]
	//
	// immutable;server-set
	ID jmap.ID `json:"id,omitempty"`

	// The ID of the Identity to associate with this submission
	//
	// immutable
	IdentityID jmap.ID `json:"identityId,omitempty"`

	// The ID of the Email to send
	//
	// immutable
	EmailID jmap.ID `json:"emailId,omitempty"`

	// The Thread ID of the Email to send
	//
	// immutable;server-set
	ThreadID jmap.ID `json:"threadId,omitempty"`

	// The Envelope used for SMTP
	//
	// immutable
	Envelope *Envelope `json:"envelope,omitempty"`

	// The date the submission was/will be released for delivery
	//
	// immutable;server-set
	SendAt *time.Time `json:"sendAt,omitempty"`

	// A status indicating if the send can be undone. One of:
	// - "pending": it may be possible to cancel
	// - "final": the message has been sent
	// - "canceled": the submission was canceled
	//
	// If this is "pending", a client can attempt to cancel by issuing a set
	// method with this set to canceled
	UndoStatus string `json:"undoStatus,omitempty"`

	// The delivery status for each recipient
	DeliveryStatus map[string]*DeliveryStatus `json:"deliveryStatus,omitempty"`

	// A list of blob IDs for DSNs received for this submission
	//
	// server-set
	DSNBlobIDs []jmap.ID `json:"dsnBlobIds,omitempty"`

	// A list of blob IDs for MDNs received for this submission
	//
	// server-set
	MDNBlobIDs []jmap.ID `json:"mdnBlobIds,omitempty"`
}

diff --git a/mail/emailsubmission/filter.go b/mail/emailsubmission/filter.go
index 9c35866fa829..d2b0fb75f00d 100644
--- a/mail/emailsubmission/filter.go
+++ b/mail/emailsubmission/filter.go
@@ -11,40 +11,27 @@ type Filter interface {
	implementsFilter()
}

// Determines the set of EmailSubmissions returned in the results. If null, all
// objects in the account of this type are included in the results.
type FilterOperator struct {
	// This MUST be one of the following strings: “AND” / “OR” / “NOT”
	Operator jmap.Operator `json:"operator,omitempty"`

	// The conditions to evaluate against each record.
	Conditions []Filter `json:"conditions,omitempty"`
}

func (fo *FilterOperator) implementsFilter() {}

// FilterCondition is an interface that represents FilterCondition
// objects. A filter condition object can be either a named struct, ie
// MailboxFilterConditionName, or a MailboxFilter itself. MailboxFilters can
// be used to create complex filtering ie return mailboxes which are subscribed
// and NOT named Inbox
// Email submission filter criteria
// https://www.rfc-editor.org/rfc/rfc8621.html#section-7.3
type FilterCondition struct {
	// identityIds field must be in this list to match
	IdentityIDs []jmap.ID `json:"identityIds,omitempty"`

	// emailId field must be in this list to match
	EmailIDs []jmap.ID `json:"emailIds,omitempty"`

	// threadId field must be in this list to match
	ThreadIDs []jmap.ID `json:"threadIds,omitempty"`

	// The undoStatus property must exactly match this to match
	UndoStatus string `json:"undoStatus,omitempty"`

	// UTC. The sendAt property must be before this time to match
	Before *time.Time `json:"before,omitempty"`

	// UTC. The sendAt property must be after this time to match
	After *time.Time `json:"after,omitempty"`
}

diff --git a/mail/emailsubmission/get.go b/mail/emailsubmission/get.go
index 9f20e4e06551..d39be6eec7d6 100644
--- a/mail/emailsubmission/get.go
+++ b/mail/emailsubmission/get.go
@@ -5,27 +5,17 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard “/get” method as described in [@!RFC8620], Section 5.1.
// Get email submission details
// https://www.rfc-editor.org/rfc/rfc8621.html#section-7.1
type Get struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The ids of the Foo objects to return. If null, then all records of
	// the data type are returned, if this is supported for that data type
	// and the number of records does not exceed the maxObjectsInGet limit.
	IDs []jmap.ID `json:"ids,omitempty"`

	// If supplied, only the properties listed in the array are returned
	// for each Foo object. If null, all properties of the object are
	// returned. The id property of the object is always returned, even if
	// not explicitly requested. If an invalid property is requested, the
	// call MUST be rejected with an invalidArguments error.
	Properties []string `json:"properties,omitempty"`

	// Use IDs from a previous call
	ReferenceIDs *jmap.ResultReference `json:"#ids,omitempty"`

	// Use Properties from a previous call
	ReferenceProperties *jmap.ResultReference `json:"#properties,omitempty"`
}

@@ -33,35 +23,13 @@ func (m *Get) Name() string { return "EmailSubmission/get" }

func (m *Get) Requires() []jmap.URI { return []jmap.URI{URI, mail.URI} }

// This is a standard “/get” method as described in [@!RFC8620], Section 5.1.
type GetResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// A (preferably short) string representing the state on the server for
	// all the data of this type in the account (not just the objects
	// returned in this call). If the data changes, this string MUST
	// change. If the Foo data is unchanged, servers SHOULD return the same
	// state string on subsequent requests for this data type.
	//
	// When a client receives a response with a different state string to a
	// previous call, it MUST either throw away all currently cached
	// objects for the type or call Foo/changes to get the exact changes.
	State string `json:"state,omitempty"`

	// An array of the Foo objects requested. This is the empty array
	// if no objects were found or if the ids argument passed in was also
	// an empty array. The results MAY be in a different order to the ids
	// in the request arguments. If an identical id is included more than
	// once in the request, the server MUST only include it once in either
	// the list or the notFound argument of the response.
	//
	// Each specification must define it's own List property
	List []*EmailSubmission `json:"list,omitempty"`

	// This array contains the ids passed to the method for records that do
	// not exist. The array is empty if all requested ids were found or if
	// the ids argument passed in was either null or an empty array.
	NotFound []jmap.ID `json:"notFound,omitempty"`
}

diff --git a/mail/emailsubmission/query.go b/mail/emailsubmission/query.go
index ad69383524c9..120bd0b91a45 100644
--- a/mail/emailsubmission/query.go
+++ b/mail/emailsubmission/query.go
@@ -5,82 +5,23 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard “/query” method as described in [@!RFC8620], Section 5.5,
// but with the following additional request argument: sortAsTree, filterAsTree
// List email submission IDs based on filter and sort criteria
// https://www.rfc-editor.org/rfc/rfc8621.html#section-7.3
type Query struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// Determines the set of Foos returned in the results. If null, all
	// objects in the account of this type are included in the results.
	//
	// Each implementation must implement it's own Filter
	Filter Filter `json:"filter,omitempty"`

	// Lists the names of properties to compare between two Foo records,
	// and how to compare them, to determine which comes first in the sort.
	// If two Foo records have an identical value for the first comparator,
	// the next comparator will be considered, and so on. If all
	// comparators are the same (this includes the case where an empty
	// array or null is given as the sort argument), the sort order is
	// server dependent, but it MUST be stable between calls to Foo/query.
	//
	// Each implementation must define it's own Sort property. The
	// SortComparator object can be used as a basis
	Sort []*SortComparator `json:"sort,omitempty"`

	// The zero-based index of the first id in the full list of results to
	// return.
	//
	// If a negative value is given, it is an offset from the end of the
	// list. Specifically, the negative value MUST be added to the total
	// number of results given the filter, and if still negative, it’s
	// clamped to 0. This is now the zero-based index of the first id to
	// return.
	//
	// If the index is greater than or equal to the total number of objects
	// in the results list, then the ids array in the response will be
	// empty, but this is not an error.
	Position int64 `json:"position,omitempty"`

	// A Foo id. If supplied, the position argument is ignored. The index
	// of this id in the results will be used in combination with the
	// anchorOffset argument to determine the index of the first result to
	// return (see below for more details).
	//
	// If an anchor argument is given, the anchor is looked for in the
	// results after filtering and sorting. If found, the anchorOffset is
	// then added to its index. If the resulting index is now negative, it
	// is clamped to 0. This index is now used exactly as though it were
	// supplied as the position argument. If the anchor is not found, the
	// call is rejected with an anchorNotFound error.
	//
	// If an anchor is specified, any position argument supplied by the
	// client MUST be ignored. If no anchor is supplied, any anchorOffset
	// argument MUST be ignored.
	//
	// A client can use anchor instead of position to find the index of an
	// id within a large set of results.
	Anchor jmap.ID `json:"anchor,omitempty"`

	// The index of the first result to return relative to the index of the
	// anchor, if an anchor is given. This MAY be negative. For example, -1
	// means the Foo immediately preceding the anchor is the first result
	// in the list returned (see below for more details).
	AnchorOffset int64 `json:"anchorOffset,omitempty"`

	// The maximum number of results to return. If null, no limit presumed.
	// The server MAY choose to enforce a maximum limit argument. In this
	// case, if a greater value is given (or if it is null), the limit is
	// clamped to the maximum; the new limit is returned with the response
	// so the client is aware. If a negative value is given, the call MUST
	// be rejected with an invalidArguments error.
	Limit uint64 `json:"limit,omitempty"`

	// Does the client wish to know the total number of results in the
	// query? This may be slow and expensive for servers to calculate,
	// particularly with complex filters, so clients should take care to
	// only request the total when needed.
	CalculateTotal bool `json:"calculateTotal,omitempty"`
}

@@ -89,56 +30,18 @@ func (m *Query) Name() string { return "EmailSubmission/query" }
func (m *Query) Requires() []jmap.URI { return []jmap.URI{URI, mail.URI} }

type QueryResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// A string encoding the current state of the query on the server. This
	// string MUST change if the results of the query (i.e., the matching
	// ids and their sort order) have changed. The queryState string MAY
	// change if something has changed on the server, which means the
	// results may have changed but the server doesn’t know for sure.
	//
	// The queryState string only represents the ordered list of ids that
	// match the particular query (including its sort/filter). There is no
	// requirement for it to change if a property on an object matching the
	// query changes but the query results are unaffected (indeed, it is
	// more efficient if the queryState string does not change in this
	// case). The queryState string only has meaning when compared to
	// future responses to a query with the same type/sort/filter or when
	// used with /queryChanges to fetch changes.
	//
	// Should a client receive back a response with a different queryState
	// string to a previous call, it MUST either throw away the currently
	// cached query and fetch it again (note, this does not require
	// fetching the records again, just the list of ids) or call
	// Foo/queryChanges to get the difference.
	QueryState string `json:"queryState,omitempty"`

	// This is true if the server supports calling Foo/queryChanges with
	// these filter/sort parameters. Note, this does not guarantee that the
	// Foo/queryChanges call will succeed, as it may only be possible for a
	// limited time afterwards due to server internal implementation
	// details.
	CanCalculateChanges bool `json:"canCalculateChanges,omitempty"`

	// The zero-based index of the first result in the ids array within the
	// complete list of query results.
	Position uint64 `json:"position,omitempty"`

	// The list of ids for each Foo in the query results, starting at the
	// index given by the position argument of this response and continuing
	// until it hits the end of the results or reaches the limit number of
	// ids. If position is >= total, this MUST be the empty list.
	IDs []jmap.ID `json:"ids,omitempty"`

	// The total number of Foos in the results (given the filter). This
	// argument MUST be omitted if the calculateTotal request argument is
	// not true.
	Total int64 `json:"total,omitempty"`

	// The limit enforced by the server on the maximum number of results to
	// return. This is only returned if the server set a limit or used a
	// different limit than that given in the request.
	Limit uint64 `json:"limit,omitempty"`
}

diff --git a/mail/emailsubmission/querychanges.go b/mail/emailsubmission/querychanges.go
index 1d37662f4492..8dfb6a510e39 100644
--- a/mail/emailsubmission/querychanges.go
+++ b/mail/emailsubmission/querychanges.go
@@ -5,43 +5,21 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// Get changes over an email submission query
// https://www.rfc-editor.org/rfc/rfc8621.html#section-7.4
type QueryChanges struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The filter argument that was used with Foo/query.
	//
	// Each implementation must supply it's own Filter property
	Filter Filter `json:"filter,omitempty"`

	// The sort argument that was used with Foo/query.
	//
	// Each implementation must supply it's own Sort property
	Sort []*SortComparator `json:"sort,omitempty"`

	// The current state of the query in the client. This is the string
	// that was returned as the queryState argument in the Foo/query
	// response with the same sort/filter. The server will return the
	// changes made to the query since this state.
	SinceQueryState string `json:"sinceQueryState,omitempty"`

	// The maximum number of changes to return in the response. See error
	// descriptions below for more details.
	MaxChanges uint64 `json:"maxChanges,omitempty"`

	// The last (highest-index) id the client currently has cached from the
	// query results. When there are a large number of results, in a common
	// case, the client may have only downloaded and cached a small subset
	// from the beginning of the results. If the sort and filter are both
	// only on immutable properties, this allows the server to omit changes
	// after this point in the results, which can significantly increase
	// efficiency. If they are not immutable, this argument is ignored.
	UpToID jmap.ID `json:"upToId,omitempty"`

	// Does the client wish to know the total number of results now in the
	// query? This may be slow and expensive for servers to calculate,
	// particularly with complex filters, so clients should take care to
	// only request the total when needed.
	CalculateTotal bool `json:"calculateTotal,omitempty"`
}

@@ -50,46 +28,14 @@ func (m *QueryChanges) Name() string { return "EmailSubmission/queryChanges" }
func (m *QueryChanges) Requires() []jmap.URI { return []jmap.URI{URI, mail.URI} }

type QueryChangesResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// This is the sinceQueryState argument echoed back; that is, the state
	// from which the server is returning changes.
	OldQueryState string `json:"oldQueryState,omitempty"`

	// This is the state the query will be in after applying the set of
	// changes to the old state.
	NewQueryState string `json:"newQueryState,omitempty"`

	// The id for every Foo that was in the query results in the old state
	// and that is not in the results in the new state.
	//
	// If the server cannot calculate this exactly, the server MAY return
	// the ids of extra Foos in addition that may have been in the old
	// results but are not in the new results.
	//
	// If the sort and filter are both only on immutable properties and an
	// upToId is supplied and exists in the results, any ids that were
	// removed but have a higher index than upToId SHOULD be omitted.
	//
	// If the filter or sort includes a mutable property, the server MUST
	// include all Foos in the current results for which this property may
	// have changed. The position of these may have moved in the results,
	// so must be reinserted by the client to ensure its query cache is
	// correct.
	Removed []jmap.ID `json:"removed,omitempty"`

	// The id and index in the query results (in the new state) for every
	// Foo that has been added to the results since the old state AND every
	// Foo in the current results that was included in the removed array
	// (due to a filter or sort based upon a mutable property).
	//
	// If the sort and filter are both only on immutable properties and an
	// upToId is supplied and exists in the results, any ids that were
	// added but have a higher index than upToId SHOULD be omitted.
	//
	// The array MUST be sorted in order of index, with the lowest index
	// first.
	Added []*jmap.AddedItem `json:"added,omitempty"`
}

diff --git a/mail/emailsubmission/set.go b/mail/emailsubmission/set.go
index ac47319e265c..fb0404527238 100644
--- a/mail/emailsubmission/set.go
+++ b/mail/emailsubmission/set.go
@@ -5,86 +5,21 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard “/set” method as described in [@!RFC8620], Section 5.3,
// but with the following additional request argument: onDestroyRemoveEmails
// Create, delete or modify an email submission
// https://www.rfc-editor.org/rfc/rfc8621.html#section-7.5
type Set struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// This is a state string as returned by the Foo/get method
	// (representing the state of all objects of this type in the account).
	// If supplied, the string must match the current state; otherwise, the
	// method will be aborted and a stateMismatch error returned. If null,
	// any changes will be applied to the current state.
	IfInState string `json:"ifInState,omitempty"`

	// A map of a creation id (a temporary id set by the client) to Foo
	// objects, or null if no objects are to be created.
	//
	// The Foo object type definition may define default values for
	// properties. Any such property may be omitted by the client.
	//
	// The client MUST omit any properties that may only be set by the
	// server (for example, the id property on most object types).
	Create map[jmap.ID]*EmailSubmission `json:"create,omitempty"`

	// A map of an id to a Patch object to apply to the current Foo object
	// with that id, or null if no objects are to be updated.
	//
	// A PatchObject is of type String[*] and represents an unordered set
	// of patches. The keys are a path in JSON Pointer Format [@!RFC6901],
	// with an implicit leading “/” (i.e., prefix each key with “/” before
	// applying the JSON Pointer evaluation algorithm).
	//
	// All paths MUST also conform to the following restrictions; if there
	// is any violation, the update MUST be rejected with an invalidPatch
	// error:
	//
	//     The pointer MUST NOT reference inside an array (i.e., you MUST
	//     NOT insert/delete from an array; the array MUST be replaced in
	//     its entirety instead). All parts prior to the last (i.e., the
	//     value after the final slash) MUST already exist on the object
	//     being patched. There MUST NOT be two patches in the PatchObject
	//     where the pointer of one is the prefix of the pointer of the
	//     other, e.g., “alerts/1/offset” and “alerts”.
	//
	// The value associated with each pointer determines how to apply that
	// patch:
	//
	//     If null, set to the default value if specified for this
	//     property; otherwise, remove the property from the patched
	//     object. If the key is not present in the parent, this a no-op.
	//     Anything else: The value to set for this property (this may be a
	//     replacement or addition to the object being patched).
	//
	// Any server-set properties MAY be included in the patch if their
	// value is identical to the current server value (before applying the
	// patches to the object). Otherwise, the update MUST be rejected with
	// an invalidProperties SetError.
	//
	// This patch definition is designed such that an entire Foo object is
	// also a valid PatchObject. The client may choose to optimise network
	// usage by just sending the diff or may send the whole object; the
	// server processes it the same either way.
	Update map[jmap.ID]jmap.Patch `json:"update,omitempty"`

	// A list of ids for Foo objects to permanently delete, or null if no
	// objects are to be destroyed.
	Destroy []jmap.ID `json:"destroy,omitempty"`

	// A map of EmailSubmission id to an object containing properties to
	// update on the Email object referenced by the EmailSubmission if the
	// create/update/destroy succeeds. (For references to EmailSubmissions
	// created in the same “/set” invocation, this is equivalent to a
	// creation-reference, so the id will be the creation id prefixed with
	// a #.)
	OnSuccessUpdateEmail map[jmap.ID]jmap.Patch `json:"onSuccessUpdateEmail,omitempty"`

	// A list of EmailSubmission ids for which the Email with the
	// corresponding emailId should be destroyed if the
	// create/update/destroy succeeds. (For references to EmailSubmission
	// creations, this is equivalent to a creation-reference so the id will
	// be the creation id prefixed with a #.)
	OnSuccessDestroyEmail []jmap.ID `json:"onSuccessDestroyEmail,omitempty"`
}

@@ -93,48 +28,22 @@ func (m *Set) Name() string { return "EmailSubmission/set" }
func (m *Set) Requires() []jmap.URI { return []jmap.URI{URI, mail.URI} }

type SetResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// The state string that would have been returned by Foo/get before
	// making the requested changes, or null if the server doesn’t know
	// what the previous state string was.
	OldState string `json:"oldState,omitempty"`

	// The state string that will now be returned by Foo/get.
	NewState string `json:"newState,omitempty"`

	// A map of the creation id to an object containing any properties of
	// the created Foo object that were not sent by the client. This
	// includes all server-set properties (such as the id in most object
	// types) and any properties that were omitted by the client and thus
	// set to a default by the server.
	//
	// This argument is null if no Foo objects were successfully created.
	Created map[jmap.ID]*EmailSubmission `json:"created,omitempty"`

	// The keys in this map are the ids of all Foos that were successfully
	// updated.
	//
	// The value for each id is a Foo object containing any property that
	// changed in a way not explicitly requested by the PatchObject sent to
	// the server, or null if none. This lets the client know of any
	// changes to server-set or computed properties.
	//
	// This argument is null if no Foo objects were successfully updated.
	Updated map[jmap.ID]*EmailSubmission `json:"updated,omitempty"`

	// An array of ids for records that have been destroyed since the old
	// state.
	Destroyed []jmap.ID `json:"destroyed,omitempty"`

	// A map of ID to a SetError for each record that failed to be created
	NotCreated map[jmap.ID]*jmap.SetError `json:"notCreated,omitempty"`

	// A map of ID to a SetError for each record that failed to be updated
	NotUpdated map[jmap.ID]*jmap.SetError `json:"notUpdated,omitempty"`

	// A map of ID to a SetError for each record that failed to be destroyed
	NotDestroyed map[jmap.ID]*jmap.SetError `json:"notDestroyed,omitempty"`
}

diff --git a/mail/emailsubmission/sort.go b/mail/emailsubmission/sort.go
index 7dbb0262f7a3..04d617484ccc 100644
--- a/mail/emailsubmission/sort.go
+++ b/mail/emailsubmission/sort.go
@@ -3,44 +3,9 @@ package emailsubmission
import "git.sr.ht/~rockorager/go-jmap"

type SortComparator struct {
	// The name of the property on the Foo objects to compare. Servers MUST
	// support sorting by the following properties:
	// - emailId
	// - threadId
	// - sentAt
	Property string `json:"property,omitempty"`

	// If true, sort in ascending order. If false, reverse the comparator’s
	// results to sort in descending order.
	IsAscending bool `json:"isAscending,omitempty"`

	// The identifier, as registered in the collation registry defined in
	// [@!RFC4790], for the algorithm to use when comparing the order of
	// strings. The algorithms the server supports are advertised in the
	// capabilities object returned with the Session object (see Section
	// 2).
	//
	// If omitted, the default algorithm is server-dependent, but:
	//
	//     It MUST be unicode-aware. It MAY be selected based on an
	//     Accept-Language header in the request (as defined in
	//     [@!RFC7231], Section 5.3.5), or out-of-band information about
	//     the user’s language/locale. It SHOULD be case insensitive where
	//     such a concept makes sense for a language/locale. Where the
	//     user’s language is unknown, it is RECOMMENDED to follow the
	//     advice in Section 5.2.3 of [@!RFC8264].
	//
	// The “i;unicode-casemap” collation [@!RFC5051] and the Unicode
	// Collation Algorithm (http://www.unicode.org/reports/tr10/) are two
	// examples that fulfil these criterion and provide reasonable
	// behaviour for a large number of languages.
	//
	// When the property being compared is not a string, the collation
	// property is ignored, and the following comparison rules apply based
	// on the type. In ascending order:
	//
	//     Boolean: false comes before true. Number: A lower number comes
	//     before a higher number. Date/UTCDate: The earlier date comes
	//     first.
	Collation jmap.CollationAlgo `json:"collation,omitempty"`
}
diff --git a/mail/identity/changes.go b/mail/identity/changes.go
index 20da80bf8967..18351e32912b 100644
--- a/mail/identity/changes.go
+++ b/mail/identity/changes.go
@@ -5,22 +5,13 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail/emailsubmission"
)

// An Identity/changes method call
// Get identity changes
// https://www.rfc-editor.org/rfc/rfc8621.html#section-6.2
type Changes struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The current state of the client. This is the string that was
	// returned as the state argument in the Foo/get response. The server
	// will return the changes that have occurred since this state.
	SinceState string `json:"sinceState,omitempty"`

	// The maximum number of ids to return in the response. The server MAY
	// choose to return fewer than this value but MUST NOT return more. If
	// not given by the client, the server may choose how many to return.
	// If supplied by the client, the value MUST be a positive integer
	// greater than 0. If a value outside of this range is given, the
	// server MUST reject the call with an invalidArguments error.
	MaxChanges uint64 `json:"maxChanges,omitempty"`
}

@@ -28,34 +19,19 @@ func (m *Changes) Name() string { return "Identity/changes" }

func (m *Changes) Requires() []jmap.URI { return []jmap.URI{emailsubmission.URI} }

// An Identity/changes response
type ChangesResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// This is the sinceState argument echoed back; it’s the state from
	// which the server is returning changes.
	OldState string `json:"oldState,omitempty"`

	// This is the state the client will be in after applying the set of
	// changes to the old state.
	NewState string `json:"newState,omitempty"`

	// If true, the client may call Foo/changes again with the newState
	// returned to get further updates. If false, newState is the current
	// server state.
	HasMoreChanges bool `json:"hasMoreChanges,omitempty"`

	// An array of ids for records that have been created since the old
	// state.
	Created []jmap.ID `json:"created,omitempty"`

	// An array of ids for records that have been updated since the old
	// state.
	Updated []jmap.ID `json:"updated,omitempty"`

	// An array of ids for records that have been destroyed since the old
	// state.
	Destroyed []jmap.ID `json:"destroyed,omitempty"`
}

diff --git a/mail/identity/get.go b/mail/identity/get.go
index 2e9c5bbffc4c..89c6c362a0af 100644
--- a/mail/identity/get.go
+++ b/mail/identity/get.go
@@ -5,22 +5,17 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail/emailsubmission"
)

// An Identity/get request
// Get details identity details
// https://www.rfc-editor.org/rfc/rfc8621.html#section-6.1
type Get struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The IDs of Identity objects to return. Leave blank to return all,
	// subject to the MaxObjectsInGet limit of the server
	IDs []jmap.ID `json:"ids,omitempty"`

	// Only the supplied properties will be returned
	Properties []string `json:"properties,omitempty"`

	// Use IDs from a previous call
	ReferenceIDs *jmap.ResultReference `json:"#ids,omitempty"`

	// Use Properties from a previous call
	ReferenceProperties *jmap.ResultReference `json:"#properties,omitempty"`
}

@@ -28,19 +23,13 @@ func (m *Get) Name() string { return "Identity/get" }

func (m *Get) Requires() []jmap.URI { return []jmap.URI{emailsubmission.URI} }

// This is a standard “/get” method as described in [@!RFC8620], Section 5.1.
type GetResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// State for all Identity objects on the server for this account
	State string `json:"state,omitempty"`

	// The Identity objects requested
	List []*Identity `json:"list,omitempty"`

	// Slice of objects not found. Only present if specific IDs were
	// requested
	NotFound []jmap.ID `json:"notFound,omitempty"`
}

diff --git a/mail/identity/identity.go b/mail/identity/identity.go
index edd243513d79..fb29151ede35 100644
--- a/mail/identity/identity.go
+++ b/mail/identity/identity.go
@@ -11,42 +11,22 @@ func init() {
	jmap.RegisterMethod("Identity/set", newSetResponse)
}

// Information about an email address or domain the user may send from
// https://www.rfc-editor.org/rfc/rfc8621.html#section-6
type Identity struct {
	// The ID of the Identity
	//
	// immutable;server-set
	ID jmap.ID `json:"id,omitempty"`

	// The "From" name the client SHOULD use when creating a new Email from
	// this identity
	Name string `json:"name,omitempty"`

	// The "From" email address the client MUST use when creating a new
	// Email from this Identity. If the mailbox part of the address (the
	// section before the "@") is the single character "*", then the client
	// may use any valid address ending in that domain
	//
	// immutable
	Email string `json:"email,omitempty"`

	// The Reply-To value the client SHOULD set when creating a new Email
	// from this identity
	ReplyTo []*mail.Address `json:"replyTo,omitempty"`

	// The Bcc value the client SHOULD set when creating a new Email from
	// this Identity
	Bcc []*mail.Address `json:"bcc,omitempty"`

	// A signature the client SHOULD insert into new plaintext messages that
	// will be sent from this identity
	TextSignature string `json:"textSignature,omitempty"`

	// A signature the client SHOULD insert into new html messages that
	// will be sent from this identity
	HTMLSignature string `json:"htmlSignature,omitempty"`

	// If the user is allowed to delete this identity
	//
	// server-set
	MayDelete bool `json:"mayDelete,omitempty"`
}
diff --git a/mail/identity/set.go b/mail/identity/set.go
index 9c83cad3fbe7..815f8072f1d1 100644
--- a/mail/identity/set.go
+++ b/mail/identity/set.go
@@ -5,70 +5,17 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail/emailsubmission"
)

// An Identity/set method call
// Modify identities
// https://www.rfc-editor.org/rfc/rfc8621.html#section-6.3
type Set struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// This is a state string as returned by the Foo/get method
	// (representing the state of all objects of this type in the account).
	// If supplied, the string must match the current state; otherwise, the
	// method will be aborted and a stateMismatch error returned. If null,
	// any changes will be applied to the current state.
	IfInState string `json:"ifInState,omitempty"`

	// A map of a creation id (a temporary id set by the client) to Foo
	// objects, or null if no objects are to be created.
	//
	// The Foo object type definition may define default values for
	// properties. Any such property may be omitted by the client.
	//
	// The client MUST omit any properties that may only be set by the
	// server (for example, the id property on most object types).
	Create map[jmap.ID]*Identity `json:"create,omitempty"`

	// A map of an id to a Patch object to apply to the current Foo object
	// with that id, or null if no objects are to be updated.
	//
	// A PatchObject is of type String[*] and represents an unordered set
	// of patches. The keys are a path in JSON Pointer Format [@!RFC6901],
	// with an implicit leading “/” (i.e., prefix each key with “/” before
	// applying the JSON Pointer evaluation algorithm).
	//
	// All paths MUST also conform to the following restrictions; if there
	// is any violation, the update MUST be rejected with an invalidPatch
	// error:
	//
	//     The pointer MUST NOT reference inside an array (i.e., you MUST
	//     NOT insert/delete from an array; the array MUST be replaced in
	//     its entirety instead). All parts prior to the last (i.e., the
	//     value after the final slash) MUST already exist on the object
	//     being patched. There MUST NOT be two patches in the PatchObject
	//     where the pointer of one is the prefix of the pointer of the
	//     other, e.g., “alerts/1/offset” and “alerts”.
	//
	// The value associated with each pointer determines how to apply that
	// patch:
	//
	//     If null, set to the default value if specified for this
	//     property; otherwise, remove the property from the patched
	//     object. If the key is not present in the parent, this a no-op.
	//     Anything else: The value to set for this property (this may be a
	//     replacement or addition to the object being patched).
	//
	// Any server-set properties MAY be included in the patch if their
	// value is identical to the current server value (before applying the
	// patches to the object). Otherwise, the update MUST be rejected with
	// an invalidProperties SetError.
	//
	// This patch definition is designed such that an entire Foo object is
	// also a valid PatchObject. The client may choose to optimise network
	// usage by just sending the diff or may send the whole object; the
	// server processes it the same either way.
	Update map[jmap.ID]jmap.Patch `json:"update,omitempty"`

	// A list of ids for Foo objects to permanently delete, or null if no
	// objects are to be destroyed.
	Destroy []jmap.ID `json:"destroy,omitempty"`
}

@@ -77,48 +24,22 @@ func (m *Set) Name() string { return "Identity/set" }
func (m *Set) Requires() []jmap.URI { return []jmap.URI{emailsubmission.URI} }

type SetResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// The state string that would have been returned by Foo/get before
	// making the requested changes, or null if the server doesn’t know
	// what the previous state string was.
	OldState string `json:"oldState,omitempty"`

	// The state string that will now be returned by Foo/get.
	NewState string `json:"newState,omitempty"`

	// A map of the creation id to an object containing any properties of
	// the created Foo object that were not sent by the client. This
	// includes all server-set properties (such as the id in most object
	// types) and any properties that were omitted by the client and thus
	// set to a default by the server.
	//
	// This argument is null if no Foo objects were successfully created.
	Created map[jmap.ID]*Identity `json:"created,omitempty"`

	// The keys in this map are the ids of all Foos that were successfully
	// updated.
	//
	// The value for each id is a Foo object containing any property that
	// changed in a way not explicitly requested by the PatchObject sent to
	// the server, or null if none. This lets the client know of any
	// changes to server-set or computed properties.
	//
	// This argument is null if no Foo objects were successfully updated.
	Updated map[jmap.ID]*Identity `json:"updated,omitempty"`

	// An array of ids for records that have been destroyed since the old
	// state.
	Destroyed []jmap.ID `json:"destroyed,omitempty"`

	// A map of ID to a SetError for each record that failed to be created
	NotCreated map[jmap.ID]*jmap.SetError `json:"notCreated,omitempty"`

	// A map of ID to a SetError for each record that failed to be updated
	NotUpdated map[jmap.ID]*jmap.SetError `json:"notUpdated,omitempty"`

	// A map of ID to a SetError for each record that failed to be destroyed
	NotDestroyed map[jmap.ID]*jmap.SetError `json:"notDestroyed,omitempty"`
}

diff --git a/mail/mailbox/changes.go b/mail/mailbox/changes.go
index 58a490a54d05..df00f5b89ac4 100644
--- a/mail/mailbox/changes.go
+++ b/mail/mailbox/changes.go
@@ -5,14 +5,13 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard /changes method as described in RFC8620, Section 5.2.
// Get mailbox changes for the whole account
// https://www.rfc-editor.org/rfc/rfc8621.html#section-2.2
type Changes struct {
	Account jmap.ID `json:"accountId,omitempty"`

	// The current state of the client.
	SinceState string `json:"sinceState,omitempty"`

	// The maximum number of ids to return in the response.
	MaxChanges uint64 `json:"maxChanges,omitempty"`
}

@@ -20,35 +19,21 @@ func (m *Changes) Name() string { return "Mailbox/changes" }

func (m *Changes) Requires() []jmap.URI { return []jmap.URI{mail.URI} }

// This is a standard /changes method as described in RFC8620, Section 5.2
// but with one extra argument to the response: updatedProperties
type ChangesResponse struct {
	Account jmap.ID `json:"accountId,omitempty"`

	// This is the SinceState argument echoed back
	OldState string `json:"oldState,omitempty"`

	// This is the state the client will be in after applying the set of
	// changes to the old state.
	NewState string `json:"newState,omitempty"`

	// If true, the client may call /changes again with the NewState
	// returned to get further updates. If false, NewState is the current
	// server state.
	HasMoreChanges bool `json:"hasMoreChanges,omitempty"`

	// New mailbox IDs.
	Created []jmap.ID `json:"created,omitempty"`

	// Updated mailbox IDs.
	Updated []jmap.ID `json:"updated,omitempty"`

	// Deleted mailbox IDs.
	Destroyed []jmap.ID `json:"destroyed,omitempty"`

	// If only the “totalEmails”, “unreadEmails”, “totalThreads”, and/or
	// “unreadThreads” Mailbox properties have changed since the old state,
	// this will be the list of properties that may have changed.
	UpdatedProperties []string `json:"updatedProperties,omitempty"`
}

diff --git a/mail/mailbox/filter.go b/mail/mailbox/filter.go
index 777a68521c0c..34f3e5d60ebb 100644
--- a/mail/mailbox/filter.go
+++ b/mail/mailbox/filter.go
@@ -2,36 +2,29 @@ package mailbox

import "git.sr.ht/~rockorager/go-jmap"

// Filter argument for a /query operation (see RFC8620, section 5.5)
type Filter interface {
	implementsFilter()
}

// FilterOperator can be used to create complex filtering (e.g.: return
// mailboxes which are subscribed and NOT named Inbox)
type FilterOperator struct {
	// jmap.OperatorOR, jmap.OperatorAND or jmap.OperatorNOT
	Operator jmap.Operator `json:"operator,omitempty"`

	// List of nested FilterOperator or FilterCondition.
	Conditions []Filter `json:"conditions,omitempty"`
}

func (fo *FilterOperator) implementsFilter() {}

// See RFC8621, section 4.4.1.
// Filter criteria for mailbox queries
// https://www.rfc-editor.org/rfc/rfc8621.html#section-2.3
type FilterCondition struct {
	// The Mailbox parentId property must match the given value exactly.
	ParentID jmap.ID `json:"parentId,omitempty"`
	// The Mailbox name property contains the given string.

	Name string `json:"name,omitempty"`
	// The Mailbox role property must match the given value exactly.

	Role Role `json:"role,omitempty"`
	// If true, a Mailbox matches if it has any non-null value for its role
	// property.

	HasAnyRole bool `json:"hasAnyRole,omitempty"`
	// The isSubscribed property of the Mailbox must be identical to the
	// value given to match the condition.

	IsSubscribed bool `json:"isSubscribed,omitempty"`
}

diff --git a/mail/mailbox/get.go b/mail/mailbox/get.go
index 8f8c4a0f620a..7e52817f464a 100644
--- a/mail/mailbox/get.go
+++ b/mail/mailbox/get.go
@@ -5,22 +5,17 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// Objects of type Mailbox are fetched via a call to Mailbox/get The ids
// argument may be null to fetch all at once.
// Get mailbox details
// https://www.rfc-editor.org/rfc/rfc8621.html#section-2.1
type Get struct {
	Account jmap.ID `json:"accountId,omitempty"`

	// The ids of the Mailbox objects to return.
	IDs []jmap.ID `json:"ids,omitempty"`

	// If supplied, only the properties listed in the array are returned
	// for each Mailbox object.
	Properties []string `json:"properties,omitempty"`

	// Use IDs from a previous call
	ReferenceIDs *jmap.ResultReference `json:"#ids,omitempty"`

	// Use Properties from a previous call
	ReferenceProperties *jmap.ResultReference `json:"#properties,omitempty"`
}

@@ -33,14 +28,10 @@ func (m *Get) Requires() []jmap.URI { return []jmap.URI{mail.URI} }
type GetResponse struct {
	Account jmap.ID `json:"accountId,omitempty"`

	// A string representing the state on the server for all the data of
	// this type in the account.
	State string `json:"state,omitempty"`

	// List of the Mailbox objects requested.
	List []*Mailbox `json:"list,omitempty"`

	// List of Mailbox IDs that do not exist.
	NotFound []string `json:"notFound,omitempty"`
}

diff --git a/mail/mailbox/mailbox.go b/mail/mailbox/mailbox.go
index a142e3adafd3..417b644b7645 100644
--- a/mail/mailbox/mailbox.go
+++ b/mail/mailbox/mailbox.go
@@ -10,55 +10,30 @@ func init() {
	jmap.RegisterMethod("Mailbox/set", newSetResponse)
}

// A Mailbox represents a named set of Emails. This is the primary mechanism
// for organising Emails within an account. It is analogous to a folder or a
// label in other systems.
//
// See RFC8621, section 2.
// Named set of Email objects. Can be viewed as a folder or a label.
// An email must be part of at least one Mailbox.
// https://www.rfc-editor.org/rfc/rfc8621.html#section-2
type Mailbox struct {
	// The id of the Mailbox.
	ID jmap.ID `json:"id,omitempty"`

	// User-visible name for the Mailbox, e.g., “Inbox”.
	Name string `json:"name,omitempty"`

	// The Mailbox id for the parent of this Mailbox, or null if this
	// Mailbox is at the top level.
	ParentID jmap.ID `json:"parentId,omitempty"`

	// Identifies Mailboxes that have a particular common purpose (e.g.,
	// the “inbox”), regardless of the name property (which may be
	// localised).
	Role Role `json:"role,omitempty"`

	// Defines the sort order of Mailboxes when presented in the client’s
	// UI, so it is consistent between devices.
	//
	// A Mailbox with a lower order should be displayed before a Mailbox
	// with a higher order (that has the same parent) in any Mailbox
	// listing in the client’s UI.
	SortOrder uint64 `json:"sortOrder,omitempty"`

	// The number of Emails in this Mailbox.
	TotalEmails uint64 `json:"totalEmails,omitempty"`

	// The number of Emails in this Mailbox that have neither the $seen
	// keyword nor the $draft keyword.
	UnreadEmails uint64 `json:"unreadEmails,omitempty"`

	// The number of Threads where at least one Email in the Thread is in
	// this Mailbox.
	TotalThreads uint64 `json:"totalThreads,omitempty"`

	// An indication of the number of “unread” Threads in the Mailbox.
	UnreadThreads uint64 `json:"unreadThreads,omitempty"`

	// The set of rights (Access Control Lists (ACLs)) the user has in
	// relation to this Mailbox.
	Rights *Rights `json:"myRights,omitempty"`

	// true if the user indicated they wish to see this Mailbox in their
	// client.
	IsSubscribed bool `json:"isSubscribed,omitempty"`
}

diff --git a/mail/mailbox/query.go b/mail/mailbox/query.go
index 5f1de013808b..ce529329b3f9 100644
--- a/mail/mailbox/query.go
+++ b/mail/mailbox/query.go
@@ -5,41 +5,27 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard /query method as described in RFC8620, Section 5.5,
// but with the following additional request argument: sortAsTree, filterAsTree
// Get a list of mailbox IDs based on filter and sort criteria
// https://www.rfc-editor.org/rfc/rfc8621.html#section-2.3
type Query struct {
	Account jmap.ID `json:"accountId,omitempty"`

	// Determines the set of emails returned in the results.
	Filter Filter `json:"filter,omitempty"`

	// Lists the names of properties to compare between two Email records,
	Sort []*SortComparator `json:"sort,omitempty"`

	// The zero-based index of the first id in the full list of results to
	// return.
	Position int64 `json:"position,omitempty"`

	// An Email id to use along with AnchorOffset.
	Anchor jmap.ID `json:"anchor,omitempty"`

	// The index of the first result to return relative to the index of the
	// anchor, if an anchor is given.
	AnchorOffset int64 `json:"anchorOffset,omitempty"`

	// The maximum number of results to return.
	Limit uint64 `json:"limit,omitempty"`

	// Does the client wish to know the total number of results in the
	// query?
	CalculateTotal bool `json:"calculateTotal,omitempty"`

	// Sort mailboxes according to their tree structure first, and then
	// according to the Sort argument.
	SortAsTree bool `json:"sortAsTree,omitempty"`

	// If true, a Mailbox is only included in the query if all its
	// ancestors are also included in the query according to the filter.
	FilterAsTree bool `json:"filterAsTree,omitempty"`
}

@@ -50,24 +36,16 @@ func (m *Query) Requires() []jmap.URI { return []jmap.URI{mail.URI} }
type QueryResponse struct {
	Account jmap.ID `json:"accountId,omitempty"`

	// A string encoding the current state of the query on the server.
	QueryState string `json:"queryState,omitempty"`

	// This is true if the server supports calling Mailbox/queryChanges
	CanCalculateChanges bool `json:"canCalculateChanges,omitempty"`

	// The zero-based index of the first result in the ids array within the
	// complete list of query results.
	Position uint64 `json:"position,omitempty"`

	// The list of ids for each Mailbox in the query results
	IDs []jmap.ID `json:"ids,omitempty"`

	// The total number of Mailboxes in the results (given the filter).
	Total int64 `json:"total,omitempty"`

	// The limit enforced by the server on the maximum number of results to
	// return.
	Limit uint64 `json:"limit,omitempty"`
}

diff --git a/mail/mailbox/querychanges.go b/mail/mailbox/querychanges.go
index ae3f57b3844f..50b89e6b86bd 100644
--- a/mail/mailbox/querychanges.go
+++ b/mail/mailbox/querychanges.go
@@ -5,30 +5,21 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// Get changes on a mailbox query
// https://www.rfc-editor.org/rfc/rfc8621.html#section-2.4
type QueryChanges struct {
	Account jmap.ID `json:"accountId,omitempty"`

	// The filter argument that was used with Mailbox/query.
	Filter Filter `json:"filter,omitempty"`

	// The sort argument that was used with Mailbox/query.
	Sort []*SortComparator `json:"sort,omitempty"`

	// The current state of the query in the client. This is the string
	// that was returned as the queryState argument in the Mailbox/query
	// response with the same sort/filter. The server will return the
	// changes made to the query since this state.
	SinceQueryState string `json:"sinceQueryState,omitempty"`

	// The maximum number of changes to return in the response.
	MaxChanges uint64 `json:"maxChanges,omitempty"`

	// The last (highest-index) id the client currently has cached from the
	// query results.
	UpToID jmap.ID `json:"upToId,omitempty"`

	// Does the client wish to know the total number of results now in the
	// query?
	CalculateTotal bool `json:"calculateTotal,omitempty"`
}

@@ -39,17 +30,12 @@ func (m *QueryChanges) Requires() []jmap.URI { return []jmap.URI{mail.URI} }
type QueryChangesResponse struct {
	Account jmap.ID `json:"accountId,omitempty"`

	// This is the SinceQueryState argument echoed back
	OldQueryState string `json:"oldQueryState,omitempty"`

	// This is the state the query will be in after applying the set of
	// changes to the old state.
	NewQueryState string `json:"newQueryState,omitempty"`

	// Deleted Mailbox IDs
	Removed []jmap.ID `json:"removed,omitempty"`

	// Added Mailbox IDs
	Added []*jmap.AddedItem `json:"added,omitempty"`
}

diff --git a/mail/mailbox/set.go b/mail/mailbox/set.go
index 5b380fbd5870..9347d617aaf3 100644
--- a/mail/mailbox/set.go
+++ b/mail/mailbox/set.go
@@ -5,29 +5,19 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard /set method as described in RFC8620, Section 5.3,
// Create, delete & modify mailboxes
// https://www.rfc-editor.org/rfc/rfc8621.html#section-2.5
type Set struct {
	Account jmap.ID `json:"accountId,omitempty"`

	// This is a state string as returned by the Mailbox/get method
	IfInState string `json:"ifInState,omitempty"`

	// A map of a creation id (a temporary id set by the client) to Mailbox
	// objects, or null if no objects are to be created.
	Create map[jmap.ID]*Mailbox `json:"create,omitempty"`

	// A map of an id to a Patch object to apply to the current Mailbox
	// object with that id, or null if no objects are to be updated.
	Update map[jmap.ID]jmap.Patch `json:"update,omitempty"`

	// A list of ids for Mailbox objects to permanently delete
	Destroy []jmap.ID `json:"destroy,omitempty"`

	// If false, any attempt to destroy a Mailbox that still has Emails in
	// it will be rejected with a mailboxHasEmail SetError. If true, any
	// Emails that were in the Mailbox will be removed from it, and if in
	// no other Mailboxes, they will be destroyed when the Mailbox is
	// destroyed.
	OnDestroyRemoveEmails bool `json:"onDestroyRemoveEmails,omitempty"`
}

@@ -38,29 +28,20 @@ func (m *Set) Requires() []jmap.URI { return []jmap.URI{mail.URI} }
type SetResponse struct {
	Account jmap.ID `json:"accountId,omitempty"`

	// The state string that would have been returned by Mailbox/get before
	// making the requested changes
	OldState string `json:"oldState,omitempty"`

	// The state string that will now be returned by Mailbox/get.
	NewState string `json:"newState,omitempty"`

	// Created mailboxes
	Created map[jmap.ID]*Mailbox `json:"created,omitempty"`

	// Updated mailboxes
	Updated map[jmap.ID]*Mailbox `json:"updated,omitempty"`

	// Deleted mailbox ids
	Destroyed []jmap.ID `json:"destroyed,omitempty"`

	// A map of ID to a SetError for each record that failed to be created
	NotCreated map[jmap.ID]*jmap.SetError `json:"notCreated,omitempty"`

	// A map of ID to a SetError for each record that failed to be updated
	NotUpdated map[jmap.ID]*jmap.SetError `json:"notUpdated,omitempty"`

	// A map of ID to a SetError for each record that failed to be destroyed
	NotDestroyed map[jmap.ID]*jmap.SetError `json:"notDestroyed,omitempty"`
}

diff --git a/mail/mdn/mdn.go b/mail/mdn/mdn.go
index 33d3af6d4e61..1335b19a4e91 100644
--- a/mail/mdn/mdn.go
+++ b/mail/mdn/mdn.go
@@ -1,10 +1,3 @@
// Package mdn is an implementation of RFC 9007: Handling Message Disposition
// Notification with the JSON Meta Application Protocol (JMAP). In plain terms,
// it handles read receipts of emails.
//
// Documentation strings for most of the protocol objects are taken from (or
// based on) contents of RFC 9007 and is subject to the IETF Trust Provisions.
// See https://trustee.ietf.org/license-info for details.
package mdn

import "git.sr.ht/~rockorager/go-jmap"
@@ -25,56 +18,30 @@ func (m *Capability) URI() jmap.URI { return URI }
func (m *Capability) New() jmap.Capability { return &Capability{} }

// A Message Delivery Notification (MDN) object
// https://www.rfc-editor.org/rfc/rfc9007.html#section-2
type MDN struct {
	// The Email ID of the received message to which this MDN is related
	ForEmailID jmap.ID `json:"forEmailId,omitempty"`

	// The Subject of the MDN
	Subject string `json:"subject,omitempty"`

	// The human-readable part of the MDN, as plain text
	TextBody string `json:"textBody,omitempty"`

	// If true, the content of the original message will appear in the third
	// component of the multipart/report generated for the MDN
	IncludeOriginalmessage bool `json:"includeOriginalMessage,omitempty"`

	// The name of the Mail User Agent (MUA) creating this MDN
	ReportingUA string `json:"reportinUA,omitempty"`

	// The object containing the diverse MDN disposition options
	Disposition *Disposition `json:"disposition,omitempty"`

	// The name of the gateway or MTA that translated a foreign
	// (non-internet) MDN into this MDN
	//
	// server-set
	MDNGateway string `json:"mdnGateway,omitempty"`

	// The original recipient address specified by the sender of the message
	// which the MDN is for
	//
	// server-set
	OriginalRecipient string `json:"originalRecipient,omitempty"`

	// The recipient for which the MDN is issued
	//
	// server-set
	FinalRecipient string `json:"finalRecipient,omitempty"`

	// The "Message-ID" header field of the message this MDN is for
	//
	// server-set
	OriginalMessageID string `json:"originalMessageId,omitempty"`

	// Additional information in the form of text messages when the "error"
	// disposition modifier appears
	//
	// server-set
	Error []string `json:"error,omitempty"`

	// The object where keys are extension-field names and values are
	// extension-field values
	ExtensionFields map[string]string `json:"extensionFields,omitempty"`
}

diff --git a/mail/mdn/parse.go b/mail/mdn/parse.go
index 4e978676204d..ac5b35fc6a4c 100644
--- a/mail/mdn/parse.go
+++ b/mail/mdn/parse.go
@@ -4,12 +4,11 @@ import (
	"git.sr.ht/~rockorager/go-jmap"
)

// Sends an RFC5322 message from an MDN object
// Parse blobs as messages in the style of RFC5322 to get MDN objects
// https://www.rfc-editor.org/rfc/rfc9007.html#section-2.2
type Parse struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The IDs of blobs to parse as MDNs
	BlobIDs []jmap.ID `json:"blobIds,omitempty"`
}

@@ -18,16 +17,12 @@ func (m *Parse) Name() string { return "MDN/parse" }
func (m *Parse) Requires() []jmap.URI { return []jmap.URI{URI} }

type ParseResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// A map of the blob ID to the MDN resulting from the parse
	Parsed map[jmap.ID]*MDN `json:"parsed,omitempty"`

	// A list blob IDs that could not be parsed as MDNs
	NotParsable []jmap.ID `json:"notParsable,omitempty"`

	// A list of blob IDs that couldn't be found
	NotFound []jmap.ID `json:"notFound,omitempty"`
}

diff --git a/mail/mdn/send.go b/mail/mdn/send.go
index da8e4c45c0ce..a3d14be38981 100644
--- a/mail/mdn/send.go
+++ b/mail/mdn/send.go
@@ -6,19 +6,14 @@ import (
)

// Sends an RFC5322 message from an MDN object
// https://www.rfc-editor.org/rfc/rfc9007.html#section-2.1
type Send struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The ID of the Identity to associate with these MDNs
	IdentityID jmap.ID `json:"identityId,omitempty"`

	// A map of client-specified creation ID to MDN object
	Send map[jmap.ID]*MDN `json:"send,omitempty"`

	// A map of the ID to a patch of update the Email object referenced by
	// MDN/send, if the sending succeeds. The ID will always be a backward
	// reference to the creation ids
	OnSuccessUpdateEmail map[jmap.ID]*jmap.Patch `json:"onSuccessUpdateEmail,omitempty"`
}

@@ -27,14 +22,10 @@ func (m *Send) Name() string { return "MDN/send" }
func (m *Send) Requires() []jmap.URI { return []jmap.URI{mail.URI, URI} }

type SendResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// A map of the creation ID to an MDN containing any properties that
	// were not set by the client
	Sent map[jmap.ID]*MDN `json:"sent,omitempty"`

	// A map of creation ID to a SetError for each MDN not sent
	NotSent map[jmap.ID]*jmap.SetError `json:"notSent,omitempty"`
}

diff --git a/mail/searchsnippet/get.go b/mail/searchsnippet/get.go
index 32cdaf173eea..6d6ac8d3492e 100644
--- a/mail/searchsnippet/get.go
+++ b/mail/searchsnippet/get.go
@@ -5,20 +5,15 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// Get search snippet details
// https://www.rfc-editor.org/rfc/rfc8621.html#section-5.1
type Get struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// Determines the set of Foos returned in the results. If null, all
	// objects in the account of this type are included in the results.
	//
	// Each implementation must implement it's own Filter
	Filter interface{} `json:"filter,omitempty"`

	// The ids of the Emails to fetch snippets for.
	EmailIDs []jmap.ID `json:"emailIds,omitempty"`

	// Use IDs from a previous call
	ReferenceIDs *jmap.ResultReference `json:"#emailIds,omitempty"`
}

@@ -27,16 +22,10 @@ func (m *Get) Name() string { return "Mailbox/get" }
func (m *Get) Requires() []jmap.URI { return []jmap.URI{mail.URI} }

type GetResponse struct {
	// The id of the account used for the call
	Account jmap.ID `json:"accountId,omitempty"`

	// An array of SearchSnippet objects for the requested Email ids.
	// This may not be in the same order as the ids that were in the
	// request.
	List []*SearchSnippet `json:"list,omitempty"`

	// An array of Email ids requested that could not be found, or null
	// if all ids were found.
	NotFound []jmap.ID `json:"notFound,omitempty"`
}

diff --git a/mail/searchsnippet/searchsnippet.go b/mail/searchsnippet/searchsnippet.go
index 7053cefc99d7..317da6e06447 100644
--- a/mail/searchsnippet/searchsnippet.go
+++ b/mail/searchsnippet/searchsnippet.go
@@ -6,34 +6,12 @@ func init() {
	jmap.RegisterMethod("SearchSnippet/get", newGetResponse)
}

// When doing a search on a "String" property, the client may wish to
// show the relevant section of the body that matches the search as a
// preview and to highlight any matching terms in both this and the
// subject of the Email.  Search snippets represent this data.
// Search preview snippet
// https://www.rfc-editor.org/rfc/rfc8621.html#section-5
type SearchSnippet struct {
	// The Email id the snippet applies to.
	Email jmap.ID `json:"emailId,omitempty"`

	// If text from the filter matches the subject, this is the subject
	// of the Email with the following transformations:
	//
	// 1.  Any instance of the following three characters MUST be
	//     replaced by an appropriate HTML entity: & (ampersand), <
	//     (less-than sign), and > (greater-than sign) [HTML].  Other
	//     characters MAY also be replaced with an HTML entity form.
	//
	// 2.  The matching words/phrases from the filter are wrapped in HTML
	//     "<mark></mark>" tags.
	//
	// If the subject does not match text from the filter, this property
	// is null.
	Subject string `json:"subject,omitempty"`

	// If text from the filter matches the plaintext or HTML body, this is
	// the relevant section of the body (converted to plaintext if
	// originally HTML), with the same transformations as the "subject"
	// property.  It MUST NOT be bigger than 255 octets in size.  If the
	// body does not contain a match for the text from the filter, this
	// property is null.
	Preview string `json:"preview,omitempty"`
}
diff --git a/mail/thread/changes.go b/mail/thread/changes.go
index b383d01b058d..ff38836b5418 100644
--- a/mail/thread/changes.go
+++ b/mail/thread/changes.go
@@ -5,22 +5,12 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard “/changes” method as described in [@!RFC8620], Section 5.2.
// See RFC8621, Section 3.2.
type Changes struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The current state of the client. This is the string that was
	// returned as the state argument in the Foo/get response. The server
	// will return the changes that have occurred since this state.
	SinceState string `json:"sinceState,omitempty"`

	// The maximum number of ids to return in the response. The server MAY
	// choose to return fewer than this value but MUST NOT return more. If
	// not given by the client, the server may choose how many to return.
	// If supplied by the client, the value MUST be a positive integer
	// greater than 0. If a value outside of this range is given, the
	// server MUST reject the call with an invalidArguments error.
	MaxChanges uint64 `json:"maxChanges,omitempty"`
}

@@ -28,34 +18,19 @@ func (m *Changes) Name() string { return "Thread/changes" }

func (m *Changes) Requires() []jmap.URI { return []jmap.URI{mail.URI} }

// This is a standard “/changes” method as described in [@!RFC8620], Section 5.2.
type ChangesResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// This is the sinceState argument echoed back; it’s the state from
	// which the server is returning changes.
	OldState string `json:"oldState,omitempty"`

	// This is the state the client will be in after applying the set of
	// changes to the old state.
	NewState string `json:"newState,omitempty"`

	// If true, the client may call Foo/changes again with the newState
	// returned to get further updates. If false, newState is the current
	// server state.
	HasMoreChanges bool `json:"hasMoreChanges,omitempty"`

	// An array of ids for records that have been created since the old
	// state.
	Created []jmap.ID `json:"created,omitempty"`

	// An array of ids for records that have been updated since the old
	// state.
	Updated []jmap.ID `json:"updated,omitempty"`

	// An array of ids for records that have been destroyed since the old
	// state.
	Destroyed []jmap.ID `json:"destroyed,omitempty"`
}

diff --git a/mail/thread/get.go b/mail/thread/get.go
index c0c534c92e63..51c9d11a404e 100644
--- a/mail/thread/get.go
+++ b/mail/thread/get.go
@@ -5,27 +5,16 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard “/get” method as described in [@!RFC8620], Section 5.1.
// See RFC8621, Section 3.1.
type Get struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The ids of the Foo objects to return. If null, then all records of
	// the data type are returned, if this is supported for that data type
	// and the number of records does not exceed the maxObjectsInGet limit.
	IDs []jmap.ID `json:"ids,omitempty"`

	// If supplied, only the properties listed in the array are returned
	// for each Foo object. If null, all properties of the object are
	// returned. The id property of the object is always returned, even if
	// not explicitly requested. If an invalid property is requested, the
	// call MUST be rejected with an invalidArguments error.
	Properties []string `json:"properties,omitempty"`

	// Use IDs from a previous call
	ReferenceIDs *jmap.ResultReference `json:"#ids,omitempty"`

	// Use Properties from a previous call
	ReferenceProperties *jmap.ResultReference `json:"#properties,omitempty"`
}

@@ -33,35 +22,13 @@ func (m *Get) Name() string { return "Thread/get" }

func (m *Get) Requires() []jmap.URI { return []jmap.URI{mail.URI} }

// This is a standard “/get” method as described in [@!RFC8620], Section 5.1.
type GetResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// A (preferably short) string representing the state on the server for
	// all the data of this type in the account (not just the objects
	// returned in this call). If the data changes, this string MUST
	// change. If the Foo data is unchanged, servers SHOULD return the same
	// state string on subsequent requests for this data type.
	//
	// When a client receives a response with a different state string to a
	// previous call, it MUST either throw away all currently cached
	// objects for the type or call Foo/changes to get the exact changes.
	State string `json:"state,omitempty"`

	// An array of the Foo objects requested. This is the empty array
	// if no objects were found or if the ids argument passed in was also
	// an empty array. The results MAY be in a different order to the ids
	// in the request arguments. If an identical id is included more than
	// once in the request, the server MUST only include it once in either
	// the list or the notFound argument of the response.
	//
	// Each specification must define it's own List property
	List []*Thread `json:"list,omitempty"`

	// This array contains the ids passed to the method for records that do
	// not exist. The array is empty if all requested ids were found or if
	// the ids argument passed in was either null or an empty array.
	NotFound []jmap.ID `json:"notFound,omitempty"`
}

diff --git a/mail/thread/thread.go b/mail/thread/thread.go
index 324eda46fb25..42614399b8ac 100644
--- a/mail/thread/thread.go
+++ b/mail/thread/thread.go
@@ -7,20 +7,9 @@ func init() {
	jmap.RegisterMethod("Thread/changes", newChangesResponse)
}

// Replies are grouped together with the original message to form a Thread. In
// JMAP, a Thread is simply a flat list of Emails, ordered by date. Every Email
// MUST belong to a Thread, even if it is the only Email in the Thread.
// See RFC8621, Section 3.
type Thread struct {
	// The ID of the thread
	//
	// immutable;server-set
	ID jmap.ID `json:"id,omitempty"`

	// The ids of the Emails in the Thread, sorted by the receivedAt date
	// of the Email, oldest first. If two Emails have an identical date,
	// the sort is server dependent but MUST be stable (sorting by id is
	// recommended).
	//
	// server-set
	EmailIDs []jmap.ID `json:"emailIds,omitempty"`
}
diff --git a/mail/vacationresponse/get.go b/mail/vacationresponse/get.go
index fe19ffd02be7..fe090ba10404 100644
--- a/mail/vacationresponse/get.go
+++ b/mail/vacationresponse/get.go
@@ -5,16 +5,13 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// An Identity/get request
// Get vacation response details
// https://www.rfc-editor.org/rfc/rfc8621.html#section-8.1
type Get struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// The IDs of Identity objects to return. Leave blank to return all,
	// subject to the MaxObjectsInGet limit of the server
	IDs []jmap.ID `json:"ids,omitempty"`

	// Only the supplied properties will be returned
	Properties []string `json:"properties,omitempty"`
}

@@ -22,19 +19,13 @@ func (m *Get) Name() string { return "VacationResponse/get" }

func (m *Get) Requires() []jmap.URI { return []jmap.URI{mail.URI, URI} }

// This is a standard “/get” method as described in [@!RFC8620], Section 5.1.
type GetResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// State for all Identity objects on the server for this account
	State string `json:"state,omitempty"`

	// The Identity objects requested
	List []*VacationResponse `json:"list,omitempty"`

	// Slice of objects not found. Only present if specific IDs were
	// requested
	NotFound []jmap.ID `json:"notFound,omitempty"`
}

diff --git a/mail/vacationresponse/set.go b/mail/vacationresponse/set.go
index ecf540f38d51..37c054293990 100644
--- a/mail/vacationresponse/set.go
+++ b/mail/vacationresponse/set.go
@@ -5,70 +5,17 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// An Identity/set method call
// Create, update & modify vacation responses
// https://www.rfc-editor.org/rfc/rfc8621.html#section-8.2
type Set struct {
	// The id of the account to use.
	Account jmap.ID `json:"accountId,omitempty"`

	// This is a state string as returned by the Foo/get method
	// (representing the state of all objects of this type in the account).
	// If supplied, the string must match the current state; otherwise, the
	// method will be aborted and a stateMismatch error returned. If null,
	// any changes will be applied to the current state.
	IfInState string `json:"ifInState,omitempty"`

	// A map of a creation id (a temporary id set by the client) to Foo
	// objects, or null if no objects are to be created.
	//
	// The Foo object type definition may define default values for
	// properties. Any such property may be omitted by the client.
	//
	// The client MUST omit any properties that may only be set by the
	// server (for example, the id property on most object types).
	Create map[jmap.ID]*VacationResponse `json:"create,omitempty"`

	// A map of an id to a Patch object to apply to the current Foo object
	// with that id, or null if no objects are to be updated.
	//
	// A PatchObject is of type String[*] and represents an unordered set
	// of patches. The keys are a path in JSON Pointer Format [@!RFC6901],
	// with an implicit leading “/” (i.e., prefix each key with “/” before
	// applying the JSON Pointer evaluation algorithm).
	//
	// All paths MUST also conform to the following restrictions; if there
	// is any violation, the update MUST be rejected with an invalidPatch
	// error:
	//
	//     The pointer MUST NOT reference inside an array (i.e., you MUST
	//     NOT insert/delete from an array; the array MUST be replaced in
	//     its entirety instead). All parts prior to the last (i.e., the
	//     value after the final slash) MUST already exist on the object
	//     being patched. There MUST NOT be two patches in the PatchObject
	//     where the pointer of one is the prefix of the pointer of the
	//     other, e.g., “alerts/1/offset” and “alerts”.
	//
	// The value associated with each pointer determines how to apply that
	// patch:
	//
	//     If null, set to the default value if specified for this
	//     property; otherwise, remove the property from the patched
	//     object. If the key is not present in the parent, this a no-op.
	//     Anything else: The value to set for this property (this may be a
	//     replacement or addition to the object being patched).
	//
	// Any server-set properties MAY be included in the patch if their
	// value is identical to the current server value (before applying the
	// patches to the object). Otherwise, the update MUST be rejected with
	// an invalidProperties SetError.
	//
	// This patch definition is designed such that an entire Foo object is
	// also a valid PatchObject. The client may choose to optimise network
	// usage by just sending the diff or may send the whole object; the
	// server processes it the same either way.
	Update map[jmap.ID]jmap.Patch `json:"update,omitempty"`

	// A list of ids for Foo objects to permanently delete, or null if no
	// objects are to be destroyed.
	Destroy []jmap.ID `json:"destroy,omitempty"`
}

@@ -77,48 +24,22 @@ func (m *Set) Name() string { return "VacationResponse/set" }
func (m *Set) Requires() []jmap.URI { return []jmap.URI{mail.URI, URI} }

type SetResponse struct {
	// The id of the account used for the call.
	Account jmap.ID `json:"accountId,omitempty"`

	// The state string that would have been returned by Foo/get before
	// making the requested changes, or null if the server doesn’t know
	// what the previous state string was.
	OldState string `json:"oldState,omitempty"`

	// The state string that will now be returned by Foo/get.
	NewState string `json:"newState,omitempty"`

	// A map of the creation id to an object containing any properties of
	// the created Foo object that were not sent by the client. This
	// includes all server-set properties (such as the id in most object
	// types) and any properties that were omitted by the client and thus
	// set to a default by the server.
	//
	// This argument is null if no Foo objects were successfully created.
	Created map[jmap.ID]*VacationResponse `json:"created,omitempty"`

	// The keys in this map are the ids of all Foos that were successfully
	// updated.
	//
	// The value for each id is a Foo object containing any property that
	// changed in a way not explicitly requested by the PatchObject sent to
	// the server, or null if none. This lets the client know of any
	// changes to server-set or computed properties.
	//
	// This argument is null if no Foo objects were successfully updated.
	Updated map[jmap.ID]*VacationResponse `json:"updated,omitempty"`

	// An array of ids for records that have been destroyed since the old
	// state.
	Destroyed []jmap.ID `json:"destroyed,omitempty"`

	// A map of ID to a SetError for each record that failed to be created
	NotCreated map[jmap.ID]*jmap.SetError `json:"notCreated,omitempty"`

	// A map of ID to a SetError for each record that failed to be updated
	NotUpdated map[jmap.ID]*jmap.SetError `json:"notUpdated,omitempty"`

	// A map of ID to a SetError for each record that failed to be destroyed
	NotDestroyed map[jmap.ID]*jmap.SetError `json:"notDestroyed,omitempty"`
}

diff --git a/mail/vacationresponse/vacationresponse.go b/mail/vacationresponse/vacationresponse.go
index 2967218ce8fe..73798b1a48e8 100644
--- a/mail/vacationresponse/vacationresponse.go
+++ b/mail/vacationresponse/vacationresponse.go
@@ -22,32 +22,21 @@ func (m *Capability) URI() jmap.URI { return URI }

func (m *Capability) New() jmap.Capability { return &Capability{} }

// Automatic reply when a message is delivered to the mail store
// https://www.rfc-editor.org/rfc/rfc8621.html#section-8
type VacationResponse struct {
	// The ID of the object. There is only ever one VacationResponse object,
	// and it's ID is constant: "singleton"
	//
	// immutable;server-set;constant
	ID string `json:"id,omitempty"`

	// If the response is enabled
	IsEnabled bool `json:"isEnabled,omitempty"`

	// If IsEnabled is true, the response is active for messages received
	// after this time. Must be UTC
	FromDate *time.Time `json:"fromDate,omitempty"`

	// If IsEnabled is true, the response is active for messages received
	// before this time. Must be UTC
	ToDate *time.Time `json:"toDate,omitempty"`

	// The subject for the response. If null, the server MAY set a suitable
	// subject
	Subject *string `json:"subject,omitempty"`

	// The plaintext body to send in the response
	TextBody *string `json:"textBody,omitempty"`

	// The HTML body to send in the response
	HTMLBody *string `json:"htmlBody,omitempty"`
}

-- 
2.41.0