~rockorager/go-jmap-devel

go-jmap: mailbox: reword comments v1 PROPOSED

Robin Jarry: 1
 mailbox: reword comments

 8 files changed, 73 insertions(+), 528 deletions(-)
Tim Culverhouse, Nov 03, 2023 at 01:34:
Next
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/46356/mbox | git am -3
Learn more about email & git

[PATCH go-jmap] mailbox: reword comments Export this patch

For copyright reasons, RFC text cannot be quoted and modified. Reword
all comments that come from RFCs.

Signed-off-by: Robin Jarry <robin@jarry.cc>
---
 mail/mailbox/changes.go      |  46 +++-----
 mail/mailbox/filter.go       |  15 ++-
 mail/mailbox/get.go          |  38 ++-----
 mail/mailbox/mailbox.go      | 198 ++---------------------------------
 mail/mailbox/query.go        | 119 +++------------------
 mail/mailbox/querychanges.go |  58 ++--------
 mail/mailbox/set.go          |  90 +++-------------
 mail/mailbox/sort.go         |  37 +------
 8 files changed, 73 insertions(+), 528 deletions(-)

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

// This is a standard “/changes” method as described in [@!RFC8620], Section
// 5.2.
//
// When the state of the set of Foo records in an account changes on the server
// (whether due to creation, updates, or deletion), the state property of the
// Foo/get response will change. The Foo/changes method allows a client to
// efficiently update the state of its Foo cache to match the new state on the
// server.
// This is a standard /changes method as described in RFC8620, Section 5.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.
	// The current state of the client.
	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.
	// The maximum number of ids to return in the response.
	MaxChanges uint64 `json:"maxChanges,omitempty"`
}

@@ -35,42 +20,35 @@ 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
// 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 {
	// 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.
	// 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 Foo/changes again with the newState
	// returned to get further updates. If false, newState is the current
	// 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"`

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

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

	// An array of ids for records that have been destroyed since the old
	// state.
	// 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. If the
	// server is unable to tell if only counts have changed, it MUST just
	// be null.
	// 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 4c42336ac341..777a68521c0c 100644
--- a/mail/mailbox/filter.go
+++ b/mail/mailbox/filter.go
@@ -2,27 +2,24 @@ package mailbox

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

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

// Determines the set of Mailboxes returned in the results. If null, all
// objects in the account of this type are included in the results.
// FilterOperator can be used to create complex filtering (e.g.: return
// mailboxes which are subscribed and NOT named Inbox)
type FilterOperator struct {
	// This MUST be one of the following strings: “AND” / “OR” / “NOT”
	// jmap.OperatorOR, jmap.OperatorAND or jmap.OperatorNOT
	Operator jmap.Operator `json:"operator,omitempty"`

	// The conditions to evaluate against each record.
	// List of nested FilterOperator or FilterCondition.
	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
// See RFC8621, section 4.4.1.
type FilterCondition struct {
	// The Mailbox parentId property must match the given value exactly.
	ParentID jmap.ID `json:"parentId,omitempty"`
diff --git a/mail/mailbox/get.go b/mail/mailbox/get.go
index fba103b4c8e8..8f8c4a0f620a 100644
--- a/mail/mailbox/get.go
+++ b/mail/mailbox/get.go
@@ -5,24 +5,16 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard “/get” method as described in [@!RFC8620], Section 5.1.
//
// Objects of type Mailbox are fetched via a call to Mailbox/get The ids
// argument may be null to fetch all at once.
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.
	// 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. 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.
	// for each Mailbox object.
	Properties []string `json:"properties,omitempty"`

	// Use IDs from a previous call
@@ -38,35 +30,17 @@ func (m *Get) Name() string {

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.
	// A string representing the state on the server for all the data of
	// this type in the account.
	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 of the Mailbox objects requested.
	List []*Mailbox `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.
	// 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 64253a8399f3..a142e3adafd3 100644
--- a/mail/mailbox/mailbox.go
+++ b/mail/mailbox/mailbox.go
@@ -12,293 +12,115 @@ func init() {

// 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. A Mailbox may perform a certain role in the system;
// see below for more details.
// label in other systems.
//
// For compatibility with IMAP, an Email MUST belong to one or more Mailboxes.
// The Email id does not change if the Email changes Mailboxes.
// See RFC8621, section 2.
type Mailbox struct {
	// The id of the Mailbox.
	//
	// immutable;server-set
	ID jmap.ID `json:"id,omitempty"`

	// User-visible name for the Mailbox, e.g., “Inbox”. This MUST be a
	// Net-Unicode string [@!RFC5198] of at least 1 character in length,
	// subject to the maximum size given in the capability object. There
	// MUST NOT be two sibling Mailboxes with both the same parent and the
	// same name. Servers MAY reject names that violate server policy
	// (e.g., names containing a slash (/) or control characters).
	// 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. Mailboxes form acyclic graphs (forests)
	// directed by the child-to-parent relationship. There MUST NOT be a
	// loop.
	// 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).
	//
	// This value is shared with IMAP (exposed in IMAP via the SPECIAL-USE
	// extension [@!RFC6154]). However, unlike in IMAP, a Mailbox MUST only
	// have a single role, and there MUST NOT be two Mailboxes in the same
	// account with the same role. Servers providing IMAP access to the
	// same data are encouraged to enforce these extra restrictions in IMAP
	// as well. Otherwise, modifying the IMAP attributes to ensure
	// compliance when exposing the data over JMAP is implementation
	// dependent.
	//
	// The value MUST be one of the Mailbox attribute names listed in the
	// IANA IMAP Mailbox Name Attributes registry, as established in
	// [@!RFC8457], converted to lowercase. New roles may be established
	// here in the future.
	//
	// An account is not required to have Mailboxes with any particular
	// roles.
	Role Role `json:"role,omitempty"`

	// Defines the sort order of Mailboxes when presented in the client’s
	// UI, so it is consistent between devices. The number MUST be an
	// integer in the range 0 <= sortOrder < 2^31.
	// 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. Mailboxes with equal order SHOULD be
	// sorted in alphabetical order by name. The sorting should take into
	// account locale-specific character order convention.
	// listing in the client’s UI.
	SortOrder uint64 `json:"sortOrder,omitempty"`

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

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

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

	// An indication of the number of “unread” Threads in the Mailbox.
	//
	// For compatibility with existing implementations, the way “unread
	// Threads” is determined is not mandated in this document. The
	// simplest solution to implement is simply the number of Threads where
	// at least one Email in the Thread is both in this Mailbox and has
	// neither the $seen nor $draft keywords.
	//
	// However, a quality implementation will return the number of unread
	// items the user would see if they opened that Mailbox. A Thread is
	// shown as unread if it contains any unread Emails that will be
	// displayed when the Thread is opened. Therefore, unreadThreads should
	// be the number of Threads where at least one Email in the Thread has
	// neither the $seen nor the $draft keyword AND at least one Email in
	// the Thread is in this Mailbox. Note that the unread Email does not
	// need to be the one in this Mailbox. In addition, the trash Mailbox
	// (that is, a Mailbox whose role is trash) requires special treatment:
	//
	//     Emails that are only in the trash (and no other Mailbox) are
	//     ignored when calculating the unreadThreads count of other
	//     Mailboxes. Emails that are not in the trash are ignored when
	//     calculating the unreadThreads count for the trash Mailbox.
	//
	// The result of this is that Emails in the trash are treated as though
	// they are in a separate Thread for the purposes of unread counts. It
	// is expected that clients will hide Emails in the trash when viewing
	// a Thread in another Mailbox, and vice versa. This allows you to
	// delete a single Email to the trash out of a Thread.
	//
	// For example, suppose you have an account where the entire contents
	// is a single Thread with 2 Emails: an unread Email in the trash and a
	// read Email in the inbox. The unreadThreads count would be 1 for the
	// trash and 0 for the inbox.
	// The number of Emails in this Mailbox.
	//
	// server-set
	UnreadThreads uint64 `json:"unreadThreads,omitempty"`

	// The set of rights (Access Control Lists (ACLs)) the user has in
	// relation to this Mailbox. These are backwards compatible with IMAP
	// ACLs, as defined in [@!RFC4314].
	// The number of Emails in this Mailbox.
	//
	// server-set
	// relation to this Mailbox.
	Rights *Rights `json:"myRights,omitempty"`

	// Has the user indicated they wish to see this Mailbox in their
	// client? This SHOULD default to false for Mailboxes in shared
	// accounts the user has access to and true for any new Mailboxes
	// created by the user themself. This MUST be stored separately per
	// user where multiple users have access to a shared Mailbox.
	//
	// A user may have permission to access a large number of shared
	// accounts, or a shared account with a very large set of Mailboxes,
	// but only be interested in the contents of a few of these. Clients
	// may choose to only display Mailboxes where the isSubscribed property
	// is set to true, and offer a separate UI to allow the user to see and
	// subscribe/unsubscribe from the full set of Mailboxes. However,
	// clients MAY choose to ignore this property, either entirely for ease
	// of implementation or just for an account where isPersonal is true
	// (indicating it is the user’s own rather than a shared account).
	//
	// This property corresponds to IMAP [@?RFC3501] Mailbox subscriptions.
	// true if the user indicated they wish to see this Mailbox in their
	// client.
	IsSubscribed bool `json:"isSubscribed,omitempty"`
}

// The set of rights (Access Control Lists (ACLs)) the user has in relation to
// this Mailbox. These are backwards compatible with IMAP ACLs, as defined in
// [@!RFC4314].
// Access Control Lists (ACLs)
type Rights struct {
	// If true, the user may use this Mailbox as part of a filter in an
	// Email/query call, and the Mailbox may be included in the mailboxIds
	// property of Email objects. Email objects may be fetched if they are
	// in at least one Mailbox with this permission. If a sub-Mailbox is
	// shared but not the parent Mailbox, this may be false. Corresponds to
	// IMAP ACLs lr (if mapping from IMAP, both are required for this to be
	// true).
	MayReadItems bool `json:"mayReadItems,omitempty"`

	// The user may add mail to this Mailbox (by either creating a new
	// Email or moving an existing one). Corresponds to IMAP ACL i.
	MayAddItems bool `json:"mayAddItems,omitempty"`

	// The user may remove mail from this Mailbox (by either changing the
	// Mailboxes of an Email or destroying the Email). Corresponds to IMAP
	// ACLs te (if mapping from IMAP, both are required for this to be
	// true).
	MayRemoveItems bool `json:"mayRemoveItems,omitempty"`

	// The user may add or remove the $seen keyword to/from an Email. If an
	// Email belongs to multiple Mailboxes, the user may only modify $seen
	// if they have this permission for all of the Mailboxes. Corresponds
	// to IMAP ACL s.
	MaySetSeen bool `json:"maySetSeen,omitempty"`

	// The user may add or remove any keyword other than $seen to/from an
	// Email. If an Email belongs to multiple Mailboxes, the user may only
	// modify keywords if they have this permission for all of the
	// Mailboxes. Corresponds to IMAP ACL w.
	MaySetKeywords bool `json:"maySetKeywords,omitempty"`

	// The user may create a Mailbox with this Mailbox as its parent.
	// Corresponds to IMAP ACL k.
	MayCreateChild bool `json:"mayCreateChild,omitempty"`

	// The user may rename the Mailbox or make it a child of another
	// Mailbox. Corresponds to IMAP ACL x (although this covers both rename
	// and delete permissions).
	MayRename bool `json:"mayRename,omitempty"`

	// The user may delete the Mailbox itself. Corresponds to IMAP ACL x
	// (although this covers both rename and delete permissions).
	MayDelete bool `json:"mayDelete,omitempty"`

	// Messages may be submitted directly to this Mailbox. Corresponds to
	// IMAP ACL p.
	MaySubmit bool `json:"maySubmit,omitempty"`
}

// Identifies Mailboxes that have a particular common purpose (e.g., the
// “inbox”), regardless of the name property (which may be localised).
//
// This value is shared with IMAP (exposed in IMAP via the SPECIAL-USE
// extension [@!RFC6154]). However, unlike in IMAP, a Mailbox MUST only have a
// single role, and there MUST NOT be two Mailboxes in the same account with
// the same role. Servers providing IMAP access to the same data are encouraged
// to enforce these extra restrictions in IMAP as well. Otherwise, modifying
// the IMAP attributes to ensure compliance when exposing the data over JMAP is
// implementation dependent.
//
// The value MUST be one of the Mailbox attribute names listed in the IANA IMAP
// Mailbox Name Attributes registry, as established in [@!RFC8457], converted
// to lowercase. New roles may be established here in the future.
//
// An account is not required to have Mailboxes with any particular roles.
//
// Reference:
// https://www.iana.org/assignments/imap-mailbox-name-attributes/imap-mailbox-name-attributes.xhtml
type Role string

const (
	// All messages
	RoleAll Role = "all"

	// Archived Messages
	RoleArchive Role = "archive"

	// Messages that are working drafts
	RoleDrafts Role = "drafts"

	// Messages with the \Flagged flag
	RoleFlagged Role = "flagged"

	// Has accessible child mailboxes
	//
	// Not used by JMAP
	RoleHasChildren Role = "haschildren"

	// Has no accessible child mailboxes
	//
	// Not used by JMAP
	RoleHasNoChildren Role = "hasnochildren"

	// Messages deemed important to user
	RoleImportant Role = "important"

	// New mail is delivered here by default
	//
	// JMAP only
	RoleInbox Role = "inbox"

	// Messages identified as Spam/Junk
	RoleJunk Role = "junk"

	// Server has marked the mailbox as "interesting"
	//
	// Not used by JMAP
	RoleMarked Role = "marked"

	// No hierarchy under this name
	//
	// Not used by JMAP
	RoleNoInferiors Role = "noinferiors"

	// The mailbox name doesn't actually exist
	//
	// Not used by JMAP
	RoleNonExistent Role = "nonexistent"

	// The mailbox is not selectable
	//
	// Not used by JMAP
	RoleNoSelect Role = "noselect"

	// The mailbox exists on a remote server
	//
	// Not used by JMAP
	RoleRemote Role = "remote"

	// Sent mail
	RoleSent Role = "sent"

	// The mailbox is subscribed to
	RoleSubscribed Role = "subscribed"

	// Messages the user has discarded
	RoleTrash Role = "trash"

	// No new messages since last select
	//
	// Not used by JMAP
	RoleUnmarked Role = "unmarked"
)
diff --git a/mail/mailbox/query.go b/mail/mailbox/query.go
index 2489d8946f15..5f1de013808b 100644
--- a/mail/mailbox/query.go
+++ b/mail/mailbox/query.go
@@ -5,98 +5,37 @@ import (
	"git.sr.ht/~rockorager/go-jmap/mail"
)

// This is a standard “/query” method as described in [@!RFC8620], Section 5.5,
// This is a standard /query method as described in RFC8620, Section 5.5,
// but with the following additional request argument: sortAsTree, filterAsTree
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
	// Determines the set of emails returned in the results.
	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
	// 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.
	//
	// 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.
	// 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. 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).
	// anchor, if an anchor is given.
	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.
	// 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? 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.
	// query?
	CalculateTotal bool `json:"calculateTotal,omitempty"`

	// If true, when sorting the query results and comparing Mailboxes A
	// and B:
	//
	// - If A is an ancestor of B, it always comes first regardless of the
	// sort comparators. Similarly, if A is descendant of B, then B always
	// comes first.
	//
	// - Otherwise, if A and B do not share a parentId, find the nearest
	// ancestors of each that do have the same parentId and compare the
	// sort properties on those Mailboxes instead.
	//
	// The result of this is that the Mailboxes are sorted as a tree
	// according to the parentId properties, with each set of children with
	// a common parent sorted according to the standard sort comparators.
	// 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
@@ -109,56 +48,26 @@ func (m *Query) Name() string { return "Mailbox/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.
	// 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 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.
	// 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 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.
	// The list of ids for each Mailbox in the query results
	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.
	// 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. This is only returned if the server set a limit or used a
	// different limit than that given in the request.
	// return.
	Limit uint64 `json:"limit,omitempty"`
}

diff --git a/mail/mailbox/querychanges.go b/mail/mailbox/querychanges.go
index 56ef60fb1973..ae3f57b3844f 100644
--- a/mail/mailbox/querychanges.go
+++ b/mail/mailbox/querychanges.go
@@ -6,42 +6,29 @@ import (
)

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
	// The filter argument that was used with Mailbox/query.
	Filter Filter `json:"filter,omitempty"`

	// The sort argument that was used with Foo/query.
	//
	// Each implementation must supply it's own Sort property
	// 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 Foo/query
	// 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. See error
	// descriptions below for more details.
	// 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. 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.
	// query results.
	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.
	// query?
	CalculateTotal bool `json:"calculateTotal,omitempty"`
}

@@ -50,46 +37,19 @@ func (m *QueryChanges) Name() string { return "Mailbox/queryChanges" }
func (m *QueryChanges) Requires() []jmap.URI { return []jmap.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.
	// 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"`

	// 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.
	// Deleted Mailbox IDs
	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 Mailbox IDs
	Added []*jmap.AddedItem `json:"added,omitempty"`
}

diff --git a/mail/mailbox/set.go b/mail/mailbox/set.go
index fe7d053c712a..5b380fbd5870 100644
--- a/mail/mailbox/set.go
+++ b/mail/mailbox/set.go
@@ -5,71 +5,22 @@ 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
// This is a standard /set method as described in RFC8620, Section 5.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.
	// 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 Foo
	// 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.
	//
	// 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]*Mailbox `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.
	// 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 Foo objects to permanently delete, or null if no
	// objects are to be destroyed.
	// 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
@@ -85,39 +36,22 @@ func (m *Set) Name() string { return "Mailbox/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.
	// 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 Foo/get.
	// The state string that will now be returned by Mailbox/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 mailboxes
	Created map[jmap.ID]*Mailbox `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 mailboxes
	Updated map[jmap.ID]*Mailbox `json:"updated,omitempty"`

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

	// A map of ID to a SetError for each record that failed to be created
diff --git a/mail/mailbox/sort.go b/mail/mailbox/sort.go
index e0388cb99009..fb07b6d8e391 100644
--- a/mail/mailbox/sort.go
+++ b/mail/mailbox/sort.go
@@ -3,43 +3,14 @@ package mailbox
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:
	// - sortOrder
	// - name
	// The name of the property on the Mailbox objects to compare.
	Property string `json:"property,omitempty"`

	// If true, sort in ascending order. If false, reverse the comparator’s
	// results to sort in descending order.
	// If true, sort in ascending 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.
	// RFC4790, for the algorithm to use when comparing the order of
	// strings.
	Collation jmap.CollationAlgo `json:"collation,omitempty"`
}
-- 
2.41.0