groupware: more mock data, added missing JMAP types

This commit is contained in:
Pascal Bleser
2025-10-01 12:13:08 +02:00
parent 0ef3af4e19
commit ae1f5af2d4
8 changed files with 668 additions and 79 deletions
+532 -32
View File
@@ -7,11 +7,64 @@ import (
"github.com/opencloud-eu/opencloud/pkg/jscalendar" "github.com/opencloud-eu/opencloud/pkg/jscalendar"
) )
// https://www.iana.org/assignments/jmap/jmap.xml#jmap-data-types
type ObjectType string
// TODO // TODO
type UTCDate struct { type UTCDateTime struct {
time.Time time.Time
} }
// TODO
type LocalDate struct {
time.Time
}
// Should the calendars events be used as part of availability calculation?
//
// This MUST be one of:
// !- `all“: all events are considered.
// !- `attending“: events the user is a confirmed or tentative participant of are considered.
// !- `none“: all events are ignored (but may be considered if also in another calendar).
//
// This should default to “all” for the calendars in the users own account, and “none” for calendars shared with the user.
type IncludeInAvailability string
type TypeOfCalendarAlert string
// `CalendarEventNotification` type.
//
// This MUST be one of
// !- `created`
// !- `updated`
// !- `destroyed`
type CalendarEventNotificationTypeOption string
// `Principal` type.
//
// This MUST be one of the following values:
// !- `individual`: This represents a single person.
// !- `group`: This represents a group of people.
// !- `resource`: This represents some resource, e.g. a projector.
// !- `location`: This represents a location.
// !- `other`: This represents some other undefined principal.
type PrincipalTypeOption string
// Algorithms in this list MUST be present in the ["HTTP Digest Algorithm Values" registry]
// defined by [RFC3230]; however, in JMAP, they must be lowercased, e.g., "md5" rather than
// "MD5".
//
// Clients SHOULD prefer algorithms listed earlier in this list.
//
// ["HTTP Digest Algorithm Values" registry]: https://www.iana.org/assignments/http-dig-alg/http-dig-alg.xhtml
type HttpDigestAlgorithm string
// The ResourceType data type is used to act as a unit of measure for the quota usage.
type ResourceType string
// The Scope data type is used to represent the entities the quota applies to.
type Scope string
const ( const (
JmapCore = "urn:ietf:params:jmap:core" JmapCore = "urn:ietf:params:jmap:core"
JmapMail = "urn:ietf:params:jmap:mail" JmapMail = "urn:ietf:params:jmap:mail"
@@ -24,6 +77,30 @@ const (
JmapBlob = "urn:ietf:params:jmap:blob" JmapBlob = "urn:ietf:params:jmap:blob"
JmapQuota = "urn:ietf:params:jmap:quota" JmapQuota = "urn:ietf:params:jmap:quota"
JmapWebsocket = "urn:ietf:params:jmap:websocket" JmapWebsocket = "urn:ietf:params:jmap:websocket"
JmapPrincipals = "urn:ietf:params:jmap:principals"
JmapPrincipalsOwner = "urn:ietf:params:jmap:principals:owner"
CoreType = ObjectType("Core")
PushSubscriptionType = ObjectType("PushSubscription")
MailboxType = ObjectType("Mailbox")
ThreadType = ObjectType("Thread")
EmailType = ObjectType("Email")
EmailDeliveryType = ObjectType("EmailDelivery")
SearchSnippetType = ObjectType("SearchSnippet")
IdentityType = ObjectType("Identity")
EmailSubmissionType = ObjectType("EmailSubmission")
VacationResponseType = ObjectType("VacationResponse")
MDNType = ObjectType("MDN")
QuotaType = ObjectType("Quota")
SieveScriptType = ObjectType("SieveScript")
PrincipalType = ObjectType("PrincipalType")
ShareNotificationType = ObjectType("ShareNotification")
AddressBookType = ObjectType("AddressBook")
ContactCardType = ObjectType("ContactCard")
CalendarType = ObjectType("Calendar")
CalendarEventType = ObjectType("CalendarEvent")
CalendarEventNotificationType = ObjectType("CalendarEventNotification")
ParticipantIdentityType = ObjectType("ParticipantIdentity")
JmapKeywordPrefix = "$" JmapKeywordPrefix = "$"
JmapKeywordSeen = "$seen" JmapKeywordSeen = "$seen"
@@ -47,9 +124,71 @@ const (
JmapMailboxRoleSent = "sent" JmapMailboxRoleSent = "sent"
//JmapMailboxRoleSubscribed = "subscribed" //JmapMailboxRoleSubscribed = "subscribed"
JmapMailboxRoleTrash = "trash" JmapMailboxRoleTrash = "trash"
CalendarAlertType = TypeOfCalendarAlert("CalendarAlert")
CalendarEventNotificationTypeOptionCreated = CalendarEventNotificationTypeOption("created")
CalendarEventNotificationTypeOptionUpdated = CalendarEventNotificationTypeOption("updated")
CalendarEventNotificationTypeOptionDestroyed = CalendarEventNotificationTypeOption("destroyed")
PrincipalTypeOptionIndividual = PrincipalTypeOption("individual")
PrincipalTypeOptionGroup = PrincipalTypeOption("group")
PrincipalTypeOptionResource = PrincipalTypeOption("resource")
PrincipalTypeOptionLocation = PrincipalTypeOption("location")
PrincipalTypeOptionOther = PrincipalTypeOption("other")
HttpDigestAlgorithmAdler32 = HttpDigestAlgorithm("adler32")
HttpDigestAlgorithmCrc32c = HttpDigestAlgorithm("crc32c")
HttpDigestAlgorithmMd5 = HttpDigestAlgorithm("md5")
HttpDigestAlgorithmSha = HttpDigestAlgorithm("sha")
HttpDigestAlgorithmSha256 = HttpDigestAlgorithm("sha-256")
HttpDigestAlgorithmSha512 = HttpDigestAlgorithm("sha-512")
HttpDigestAlgorithmUnixSum = HttpDigestAlgorithm("unixsum")
HttpDigestAlgorithmUnixcksum = HttpDigestAlgorithm("unixcksum")
// The quota is measured in a number of data type objects.
//
// For example, a quota can have a limit of 50 `Mail` objects.
ResourceTypeCount = ResourceType("count")
// The quota is measured in size (in octets).
//
// For example, a quota can have a limit of 25000 octets.
ResourceTypeOctets = ResourceType("octets")
// The quota information applies to just the client's account.
ScopeAccount = Scope("account")
// The quota information applies to all accounts sharing this domain.
ScopeDomain = Scope("domain")
// The quota information applies to all accounts belonging to the server.
ScopeGlobal = Scope("global")
) )
var ( var (
ObjectTypes = []ObjectType{
CoreType,
PushSubscriptionType,
MailboxType,
ThreadType,
EmailType,
EmailDeliveryType,
SearchSnippetType,
IdentityType,
EmailSubmissionType,
VacationResponseType,
MDNType,
QuotaType,
SieveScriptType,
PrincipalType,
ShareNotificationType,
AddressBookType,
ContactCardType,
CalendarType,
CalendarEventType,
CalendarEventNotificationType,
ParticipantIdentityType,
}
JmapMailboxRoles = []string{ JmapMailboxRoles = []string{
JmapMailboxRoleInbox, JmapMailboxRoleInbox,
JmapMailboxRoleSent, JmapMailboxRoleSent,
@@ -57,17 +196,43 @@ var (
JmapMailboxRoleJunk, JmapMailboxRoleJunk,
JmapMailboxRoleTrash, JmapMailboxRoleTrash,
} }
)
// Should the calendars events be used as part of availability calculation? CalendarEventNotificationOptionTypes = []CalendarEventNotificationTypeOption{
// CalendarEventNotificationTypeOptionCreated,
// This MUST be one of: CalendarEventNotificationTypeOptionUpdated,
// !- `all“: all events are considered. CalendarEventNotificationTypeOptionDestroyed,
// !- `attending“: events the user is a confirmed or tentative participant of are considered. }
// !- `none“: all events are ignored (but may be considered if also in another calendar).
// PrincipalTypeOptions = []PrincipalTypeOption{
// This should default to “all” for the calendars in the users own account, and “none” for calendars shared with the user. PrincipalTypeOptionIndividual,
type IncludeInAvailability string PrincipalTypeOptionGroup,
PrincipalTypeOptionResource,
PrincipalTypeOptionLocation,
PrincipalTypeOptionOther,
}
HttpDigestAlgorithms = []HttpDigestAlgorithm{
HttpDigestAlgorithmAdler32,
HttpDigestAlgorithmCrc32c,
HttpDigestAlgorithmMd5,
HttpDigestAlgorithmSha,
HttpDigestAlgorithmSha256,
HttpDigestAlgorithmSha512,
HttpDigestAlgorithmUnixSum,
HttpDigestAlgorithmUnixcksum,
}
ResourceTypes = []ResourceType{
ResourceTypeCount,
ResourceTypeOctets,
}
Scopes = []Scope{
ScopeAccount,
ScopeDomain,
ScopeGlobal,
}
)
const ( const (
IncludeInAvailabilityAll = IncludeInAvailability("all") IncludeInAvailabilityAll = IncludeInAvailability("all")
@@ -263,7 +428,7 @@ type SessionBlobAccountCapabilities struct {
// Clients SHOULD prefer algorithms listed earlier in this list. // Clients SHOULD prefer algorithms listed earlier in this list.
// //
// ["HTTP Digest Algorithm Values" registry]: https://www.iana.org/assignments/http-dig-alg/http-dig-alg.xhtml // ["HTTP Digest Algorithm Values" registry]: https://www.iana.org/assignments/http-dig-alg/http-dig-alg.xhtml
SupportedDigestAlgorithms []string `json:"supportedDigestAlgorithms"` SupportedDigestAlgorithms []HttpDigestAlgorithm `json:"supportedDigestAlgorithms"`
} }
type SessionQuotaAccountCapabilities struct { type SessionQuotaAccountCapabilities struct {
@@ -280,6 +445,20 @@ type SessionContactsAccountCapabilities struct {
MayCreateAddressBook bool `json:"mayCreateAddressBook"` MayCreateAddressBook bool `json:"mayCreateAddressBook"`
} }
type SessionPrincipalsAccountCapabilities struct {
// The id of the principal in this account that corresponds to the user fetching this object, if any.
CurrentUserPrincipalId string `json:"currentUserPrincipalId,omitempty"`
}
type SessionPrincipalsOwnerAccountCapabilities struct {
// The id of an account with the `urn:ietf:params:jmap:principals` capability that contains the
// corresponding `Principal` object.
AccountIdForPrincipal string `json:"accountIdForPrincipal,omitempty"`
// The id of the `Principal` that owns this account.
PrincipalId string `json:"principalId,omitempty"`
}
type SessionAccountCapabilities struct { type SessionAccountCapabilities struct {
Mail SessionMailAccountCapabilities `json:"urn:ietf:params:jmap:mail"` Mail SessionMailAccountCapabilities `json:"urn:ietf:params:jmap:mail"`
Submission SessionSubmissionAccountCapabilities `json:"urn:ietf:params:jmap:submission"` Submission SessionSubmissionAccountCapabilities `json:"urn:ietf:params:jmap:submission"`
@@ -288,9 +467,11 @@ type SessionAccountCapabilities struct {
Blob SessionBlobAccountCapabilities `json:"urn:ietf:params:jmap:blob"` Blob SessionBlobAccountCapabilities `json:"urn:ietf:params:jmap:blob"`
Quota SessionQuotaAccountCapabilities `json:"urn:ietf:params:jmap:quota"` Quota SessionQuotaAccountCapabilities `json:"urn:ietf:params:jmap:quota"`
Contacts SessionContactsAccountCapabilities `json:"urn:ietf:params:jmap:contacts"` Contacts SessionContactsAccountCapabilities `json:"urn:ietf:params:jmap:contacts"`
Principals *SessionPrincipalsAccountCapabilities `json:"urn:ietf:params:jmap:principals,omitempty"`
PrincipalsOwner *SessionPrincipalsOwnerAccountCapabilities `json:"urn:ietf:params:jmap:principals:owner,omitempty"`
} }
type SessionAccount struct { type Account struct {
// A user-friendly string to show when presenting content from this account, e.g., the email address representing the owner of the account. // A user-friendly string to show when presenting content from this account, e.g., the email address representing the owner of the account.
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
// This is true if the account belongs to the authenticated user rather than a group account or a personal account of another user that has been shared with them. // This is true if the account belongs to the authenticated user rather than a group account or a personal account of another user that has been shared with them.
@@ -357,6 +538,9 @@ type SessionWebsocketCapabilities struct {
type SessionContactsCapabilities struct { type SessionContactsCapabilities struct {
} }
type SessionPrincipalCapabilities struct {
}
type SessionCapabilities struct { type SessionCapabilities struct {
Core SessionCoreCapabilities `json:"urn:ietf:params:jmap:core"` Core SessionCoreCapabilities `json:"urn:ietf:params:jmap:core"`
Mail SessionMailCapabilities `json:"urn:ietf:params:jmap:mail"` Mail SessionMailCapabilities `json:"urn:ietf:params:jmap:mail"`
@@ -366,7 +550,8 @@ type SessionCapabilities struct {
Blob SessionBlobCapabilities `json:"urn:ietf:params:jmap:blob"` Blob SessionBlobCapabilities `json:"urn:ietf:params:jmap:blob"`
Quota SessionQuotaCapabilities `json:"urn:ietf:params:jmap:quota"` Quota SessionQuotaCapabilities `json:"urn:ietf:params:jmap:quota"`
Websocket SessionWebsocketCapabilities `json:"urn:ietf:params:jmap:websocket"` Websocket SessionWebsocketCapabilities `json:"urn:ietf:params:jmap:websocket"`
Contacts SessionContactsCapabilities `json:"urn:ietf:params:jmap:contacts"` Contacts *SessionContactsCapabilities `json:"urn:ietf:params:jmap:contacts"`
Principals *SessionPrincipalCapabilities `json:"urn:ietf:params:jmap:principals"`
} }
type SessionPrimaryAccounts struct { type SessionPrimaryAccounts struct {
@@ -387,7 +572,7 @@ type State string
type SessionResponse struct { type SessionResponse struct {
Capabilities SessionCapabilities `json:"capabilities"` Capabilities SessionCapabilities `json:"capabilities"`
Accounts map[string]SessionAccount `json:"accounts,omitempty"` Accounts map[string]Account `json:"accounts,omitempty"`
// A map of capability URIs (as found in accountCapabilities) to the account id that is considered to be the users main or default // A map of capability URIs (as found in accountCapabilities) to the account id that is considered to be the users main or default
// account for data pertaining to that capability. // account for data pertaining to that capability.
@@ -2010,15 +2195,6 @@ type EmailSubmissionSetResponse struct {
// TODO(pbleser-oc) add updated and destroyed when they are needed // TODO(pbleser-oc) add updated and destroyed when they are needed
} }
type ObjectType string
const (
VacationResponseType ObjectType = "VacationResponse"
EmailType ObjectType = "Email"
EmailDeliveryType ObjectType = "EmailDelivery"
MailboxType ObjectType = "Mailbox"
)
type Command string type Command string
type Invocation struct { type Invocation struct {
@@ -3160,23 +3336,347 @@ type CalendarEvent struct {
// `recurrenceId` within a particular account. // `recurrenceId` within a particular account.
Id string `json:"id"` Id string `json:"id"`
baseEventId string // This is only defined if the `id` property is a synthetic id, generated by the
// server to represent a particular instance of a recurring event (immutable; server-set).
//
// This property gives the id of the "real" `CalendarEvent` this was generated from.
BaseEventId string `json:"baseEventId,omitempty"`
calendarIds map[string]bool // The set of Calendar ids this event belongs to.
//
// An event MUST belong to one or more Calendars at all times (until it is destroyed).
//
// The set is represented as an object, with each key being a Calendar id.
//
// The value for each key in the object MUST be `true`.
CalendarIds map[string]bool `json:"calendarIds,omitempty"`
isDraft bool // If true, this event is to be considered a draft.
//
// The server will not send any scheduling messages to participants or send push notifications
// for alerts.
//
// This may only be set to `true` upon creation.
//
// Once set to `false`, the value cannot be updated to `true`.
//
// This property MUST NOT appear in `recurrenceOverrides`.
IsDraft bool `json:"isDraft,omitzero"`
isOrigin bool // Is this the authoritative source for this event (i.e., does it control scheduling for
// this event; the event has not been added as a result of an invitation from another calendar system)?
//
// This is true if, and only if:
// !- the events `replyTo` property is null; or
// !- the account will receive messages sent to at least one of the methods specified in the `replyTo` property of the event.
IsOrigin bool `json:"isOrigin,omitzero"`
utcStart UTCDate // For simple clients that do not implement time zone support.
//
// Clients should only use this if also asking the server to expand recurrences, as you cannot accurately
// expand a recurrence without the original time zone.
//
// This property is calculated at fetch time by the server.
//
// Time zones are political and they can and do change at any time.
//
// Fetching exactly the same property again may return a different results if the time zone data has been updated on the server.
//
// Time zone data changes are not considered `updates` to the event.
//
// If set, the server will convert the UTC date to the event's current time zone and store the local time.
//
// This property is not included in `CalendarEvent/get` responses by default and must be requested explicitly.
//
// Floating events (events without a time zone) will be interpreted as per the time zone given as a `CalendarEvent/get` argument.
//
// Note that it is not possible to accurately calculate the expansion of recurrence rules or recurrence overrides with the
// `utcStart` property rather than the local start time. Even simple recurrences such as "repeat weekly" may cross a
// daylight-savings boundary and end up at a different UTC time. Clients that wish to use "utcStart" are RECOMMENDED to
// request the server expand recurrences.
UtcStart UTCDateTime `json:"utcStart,omitzero"`
utcEnd UTCDate // The server calculates the end time in UTC from the start/timeZone/duration properties of the event.
//
// TODO https://jmap.io/spec-calendars.html#calendar-events // This property is not included by default and must be requested explicitly.
//
// Like `utcStart`, it is calculated at fetch time if requested and may change due to time zone data changes.
//
// Floating events will be interpreted as per the time zone given as a `CalendarEvent/get` argument.
UtcEnd UTCDateTime `json:"utcEnd,omitzero"`
jscalendar.Event jscalendar.Event
} }
// A ParticipantIdentity stores information about a URI that represents the user within that account in an events participants.
type ParticipantIdentity struct {
// The id of the ParticipantIdentity (immutable; server-set).
Id string `json:"id"`
// The display name of the participant to use when adding this participant to an event, e.g. "Joe Bloggs".
//
// default:
Name string `json:"name,omitempty"`
// The URI that represents this participant for scheduling.
//
// This URI MAY also be the URI for one of the sendTo methods.
ScheduleId string `json:"scheduleId"`
// Represents methods by which the participant may receive invitations and updates to an event.
//
// The keys in the property value are the available methods and MUST only contain ASCII alphanumeric
// characters (`A-Za-z0-9`).
//
// The value is a URI for the method specified in the key.
SendTo map[string]string `json:"sendTo,omitempty"`
// This SHOULD be true for exactly one participant identity in any account, and MUST NOT be true for more
// than one participant identity within an account (server-set).
//
// The default identity should be used by clients whenever they need to choose an identity for the user
// within this account, and they do not have any other information on which to make a choice.
//
// For example, if creating a scheduled event in this account, the default identity may be automatically
// added as an owner. (But the client may ignore this if, for example, it has its own feature to allow
// users to choose which identity to use based on the invitees.)
IsDefault bool `json:"isDefault,omitzero"`
}
type CalendarAlert struct {
// This MUST be the string `CalendarAlert`.
Type TypeOfCalendarAlert `json:"@type,omitempty"`
// The account id for the calendar in which the alert triggered.
AccountId string `json:"accountId"`
// The CalendarEvent id for the alert that triggered.
//
// Note, for a recurring event this is the id of the base event, never a synthetic id for a particular instance.
CalendarEventId string `json:"calendarEventId"`
// The uid property of the CalendarEvent for the alert that triggered.
Uid string `json:"uid"`
// The `recurrenceId` for the instance of the event for which this alert is being
// triggered, or null if the event is not recurring.
RecurrenceId LocalDate `json:"recurrenceId,omitzero"`
// The id for the alert that triggered.
AlertId string `json:"alertId"`
}
type Person struct {
// The name of the person who made the change.
Name string `json:"name"`
// The email of the person who made the change, or null if no email is available.
Email string `json:"email,omitempty"`
// The id of the `Principal` corresponding to the person who made the change, if any.
//
// This will be null if the change was due to receving an iTIP message.
PrincipalId string `json:"principalId,omitempty"`
// The `scheduleId` URI of the person who made the change, if any.
//
// This will normally be set if the change was made due to receving an iTIP message.
ScheduleId string `json:"scheduleId,omitempty"`
}
type CalendarEventNotification struct {
// The id of the `CalendarEventNotification`.
Id string `json:"id"`
// The time this notification was created.
Created UTCDateTime `json:"created,omitzero"`
// Who made the change.
ChangedBy *Person `json:"person,omitempty"`
// Comment sent along with the change by the user that made it.
//
// (e.g. `COMMENT` property in an iTIP message), if any.
Comment string `json:"comment,omitempty"`
// `CalendarEventNotification` type.
//
// This MUST be one of
// !- `created`
// !- `updated`
// !- `destroyed`
Type CalendarEventNotificationTypeOption `json:"type"`
// The id of the CalendarEvent that this notification is about.
//
// If the change only affects a single instance of a recurring event, the server MAY set the
// `event` and `event`atch properties for just that instance; the `calendarEventId` MUST
// still be for the base event.
CalendarEventId string `json:"calendarEventId"`
// Is this event a draft? (created/updated only)
IsDraft bool `json:"isDraft,omitzero"`
// The data before the change (if updated or destroyed),
// or the data after creation (if created).
Event *jscalendar.Event `json:"event,omitempty"`
// A patch encoding the change between the data in the event property,
// and the data after the update (updated only).
EventPatch PatchObject `json:"eventPatch,omitempty"`
}
// A Principal represents an individual, group, location (e.g. a room), resource (e.g. a projector) or other entity
// in a collaborative environment.
//
// Sharing in JMAP is generally configured by assigning rights to certain data within an account to other principals,
// for example a user may assign permission to read their calendar to a principal representing another user, or their team.
//
// In a shared environment such as a workplace, a user may have access to a large number of principals.
//
// In most systems the user will have access to a single `Account` containing `Principal` objects, but they may
// have access to multiple if, for example, aggregating data from different places.
type Principal struct {
// The id of the principal.
Id string `json:"id"`
// `Principal` type.
//
// This MUST be one of the following values:
// !- `individual`: This represents a single person.
// !- `group`: This represents a group of people.
// !- `resource`: This represents some resource, e.g. a projector.
// !- `location`: This represents a location.
// !- `other`: This represents some other undefined principal.
Type PrincipalTypeOption `json:"type"`
// The name of the principal, e.g. `"Jane Doe"`, or `"Room 4B"`.
Name string `json:"name"`
// A longer description of the principal, for example details about the
// facilities of a resource, or null if no description available.
Description string `json:"description,omitempty"`
// An email address for the principal, or null if no email is available.
Email string `json:"email,omitempty"`
// The time zone for this principal, if known.
//
// If not null, the value MUST be a time zone id from the IANA Time Zone Database TZDB.
TimeZone string `json:"timeZone,omitempty"`
// A map of JMAP capability URIs to domain specific information about the principal in relation
// to that capability, as defined in the document that registered the capability.
Capabilities map[string]any `json:"capabilities,omitempty"`
// A map of account id to `Account` object for each JMAP Account containing data for
// this principal that the user has access to, or null if none.
Accounts map[string]Account `json:"accounts,omitempty"`
}
// TODO https://jmap.io/spec-sharing.html#object-properties
type ShareNotification struct {
}
type Shareable struct {
// Has the user indicated they wish to see this data?
//
// The initial value for this when data is shared by another user is implementation dependent,
// although data types may give advice on appropriate defaults.
IsSubscribed bool `json:"isSubscribed,omitzero"`
// The set of permissions the user currently has.
//
// Appropriate permissions are domain specific and must be defined per data type.
MyRights map[string]bool `json:"myRights,omitempty"`
// A map of principal id to rights to give that principal, or null if not shared with anyone.
//
// The account id for the principal id can be found in the capabilities of the `Account` this object is in.
//
// Users with appropriate permission may set this property to modify who the data is shared with.
//
// The principal that owns the account this data is in MUST NOT be in the set of sharees; their rights are implicit.
ShareWith map[string]map[string]bool `json:"shareWith,omitempty"`
}
// The Quota is an object that displays the limit set to an account usage.
//
// It then shows as well the current usage in regard to that limit.
type Quota struct {
// The unique identifier for this object.
Id string `json:"id"`
// The resource type of the quota.
ResourceType ResourceType `json:"resourceType"`
// The current usage of the defined quota, using the `resourceType` defined as unit of measure.
//
// Computation of this value is handled by the server.
Used uint `json:"used"`
// The hard limit set by this quota, using the `resourceType` defined as unit of measure.
//
// Objects in scope may not be created or updated if this limit is reached.
HardLimit uint `json:"hardLimit"`
// The Scope data type is used to represent the entities the quota applies to.
//
// It is defined as a "String" with values from the following set:
// !- `account`: The quota information applies to just the client's account.
// !- `domain`: The quota information applies to all accounts sharing this domain.
// !- `global`: The quota information applies to all accounts belonging to the server.
Scope Scope `json:"scope"`
// The name of the quota.
//
// Useful for managing quotas and using queries for searching.
Name string `json:"name"`
// A list of all the type names as defined in the "JMAP Types Names" registry
// (e.g., `Email`, `Calendar`, etc.) to which this quota applies.
//
// This allows the quotas to be assigned to distinct or shared data types.
//
// The server MUST filter out any types for which the client did not request the associated capability
// in the `using` section of the request.
//
// Further, the server MUST NOT return Quota objects for which there are no types recognized by the client.
Types []ObjectType `json:"types,omitempty"`
// The warn limit set by this quota, using the `resourceType` defined as unit of measure.
//
// It can be used to send a warning to an entity about to reach the hard limit soon, but with no
// action taken yet.
//
// If set, it SHOULD be lower than the `softLimit` (if present and different from null) and the `hardLimit`.
WarnLimit uint `json:"warnLimit,omitzero"`
// The soft limit set by this quota, using the `resourceType` defined as unit of measure.
//
// It can be used to still allow some operations but refuse some others.
//
// What is allowed or not is up to the server.
//
// For example, it could be used for blocking outgoing events of an entity (sending emails, creating
// calendar events, etc.) while still receiving incoming events (receiving emails, receiving calendars
// events, etc.).
//
// If set, it SHOULD be higher than the `warnLimit` (if present and different from null) but lower
// than the `hardLimit`.
SoftLimit uint `json:"softLimit,omitzero"`
// Arbitrary, free, human-readable description of this quota.
//
// It might be used to explain where the different limits come from and explain the entities and data
// types this quota applies to.
//
// The description MUST be encoded in UTF-8 [RFC3629] as described in [RFC8620], Section 1.5, and
// selected based on an `Accept-Language` header in the request (as defined in [RFC9110], Section 12.5.4)
// or out-of-band information about the user's language or locale.
Description string `json:"description,omitempty"`
}
type ErrorResponse struct { type ErrorResponse struct {
Type string `json:"type"` Type string `json:"type"`
Description string `json:"description,omitempty"` Description string `json:"description,omitempty"`
+7 -27
View File
@@ -37,7 +37,6 @@ type SignedDuration string // TODO
type Relationship string type Relationship string
type Display string type Display string
type Rel string type Rel string
type Method string
type LocationTypeOption string type LocationTypeOption string
type LocationRelation string type LocationRelation string
type VirtualLocationFeature string type VirtualLocationFeature string
@@ -225,17 +224,6 @@ const (
RelWorkingCopy = Rel("working-copy") RelWorkingCopy = Rel("working-copy")
RelWorkingCopyOf = Rel("working-copy-of") RelWorkingCopyOf = Rel("working-copy-of")
MethodPublish = Method("publish")
MethodRequest = Method("request")
MethodReply = Method("reply")
MethodAdd = Method("add")
MethodCancel = Method("cancel")
MethodRefresh = Method("refresh")
MethodCounter = Method("counter")
MethodDeclineCounter = Method("declinecounter")
// mlr --csv --headerless-csv-output cut -f Token ./location-type-registry-1.csv |sort|perl -ne 'chomp; print "LocationTypeOption".ucfirst($_)." = LocationTypeOption(\"".$_."\")\n"'
LocationTypeOptionAircraft = LocationTypeOption("aircraft") LocationTypeOptionAircraft = LocationTypeOption("aircraft")
LocationTypeOptionAirport = LocationTypeOption("airport") LocationTypeOptionAirport = LocationTypeOption("airport")
LocationTypeOptionArena = LocationTypeOption("arena") LocationTypeOptionArena = LocationTypeOption("arena")
@@ -537,17 +525,6 @@ var (
RelWorkingCopyOf, RelWorkingCopyOf,
} }
Methods = []Method{
MethodPublish,
MethodRequest,
MethodReply,
MethodAdd,
MethodCancel,
MethodRefresh,
MethodCounter,
MethodDeclineCounter,
}
LocationTypeOptions = []LocationTypeOption{ LocationTypeOptions = []LocationTypeOption{
LocationTypeOptionAircraft, LocationTypeOptionAircraft,
LocationTypeOptionAirport, LocationTypeOptionAirport,
@@ -1781,10 +1758,11 @@ type Object struct {
// to know which version of the object a scheduling message relates to. // to know which version of the object a scheduling message relates to.
Sequence uint `json:"sequence,omitzero"` Sequence uint `json:"sequence,omitzero"`
// This is the iTIP [RFC5546] method, in lowercase. /*
// // CalendarEvent objects MUST NOT have a “method” property as this is only used when representing iTIP
// This MUST only be present if the JSCalendar object represents an iTIP scheduling message. // [@!RFC5546] scheduling messages, not events in a data store.
Method Method `json:"method,omitempty"` Method Method `json:"method,omitempty"`
*/
// This indicates that the time is not important to display to the user when rendering this calendar object. // This indicates that the time is not important to display to the user when rendering this calendar object.
// //
@@ -2232,3 +2210,5 @@ func (g *Group) UnmarshalJSON(b []byte) error {
type tmp Group type tmp Group
return json.Unmarshal(b, (*tmp)(g)) return json.Unmarshal(b, (*tmp)(g))
} }
// mlr --csv --headerless-csv-output cut -f Token ./location-type-registry-1.csv |sort|perl -ne 'chomp; print "LocationTypeOption".ucfirst($_)." = LocationTypeOption(\"".$_."\")\n"'
-1
View File
@@ -727,7 +727,6 @@ func TestEvent(t *testing.T) {
}, },
}, },
Sequence: 3, Sequence: 3,
Method: MethodRefresh,
ShowWithoutTime: true, ShowWithoutTime: true,
Locations: map[string]Location{ Locations: map[string]Location{
"loc1": { "loc1": {
-1
View File
@@ -9,7 +9,6 @@ examples:
emailReceivedAt: '2025-09-23T10:58:03Z' emailReceivedAt: '2025-09-23T10:58:03Z'
emailSentAt: '2025-09-23T12:58:03+02:00' emailSentAt: '2025-09-23T12:58:03+02:00'
blobId: 'cfz7vkmhcfwl1gfln02hga2fb3xwsqirirousda0rs1soeosla2p1aiaahcqjwaf' blobId: 'cfz7vkmhcfwl1gfln02hga2fb3xwsqirirousda0rs1soeosla2p1aiaahcqjwaf'
attachmentName: 'Alloy_Yellow_Scale.pdf'
attachmentType: 'application/pdf' attachmentType: 'application/pdf'
attachmentSize: 192128 attachmentSize: 192128
attachmentDisposition: 'attachment' attachmentDisposition: 'attachment'
@@ -13,7 +13,7 @@ import (
type SwaggerGetAccountResponse struct { type SwaggerGetAccountResponse struct {
// in: body // in: body
Body struct { Body struct {
*jmap.SessionAccount *jmap.Account
} }
} }
@@ -40,7 +40,7 @@ func (g *Groupware) GetAccount(w http.ResponseWriter, r *http.Request) {
// swagger:response GetAccountsResponse200 // swagger:response GetAccountsResponse200
type SwaggerGetAccountsResponse struct { type SwaggerGetAccountsResponse struct {
// in: body // in: body
Body map[string]jmap.SessionAccount Body map[string]jmap.Account
} }
// swagger:route GET /groupware/accounts account accounts // swagger:route GET /groupware/accounts account accounts
@@ -20,7 +20,7 @@ var C1 = jmap.Calendar{
Type: jscalendar.AlertType, Type: jscalendar.AlertType,
Trigger: jscalendar.AbsoluteTrigger{ Trigger: jscalendar.AbsoluteTrigger{
Type: jscalendar.AbsoluteTriggerType, Type: jscalendar.AbsoluteTriggerType,
When: MustParse("2025-09-30T20:34:12Z"), When: mustParseTime("2025-09-30T20:34:12Z"),
}, },
}, },
}, },
@@ -63,17 +63,25 @@ var AllCalendars = []jmap.Calendar{C1}
var E1 = jmap.CalendarEvent{ var E1 = jmap.CalendarEvent{
Id: "ovei9oqu", Id: "ovei9oqu",
CalendarIds: map[string]bool{
C1.Id: true,
},
BaseEventId: "ahtah9qu",
IsDraft: true,
IsOrigin: true,
UtcStart: jmap.UTCDateTime{Time: mustParseTime("2025-10-01T00:00:00Z")},
UtcEnd: jmap.UTCDateTime{Time: mustParseTime("2025-10-07T00:00:00Z")},
Event: jscalendar.Event{ Event: jscalendar.Event{
Type: jscalendar.EventType, Type: jscalendar.EventType,
Start: jscalendar.LocalDateTime{Time: MustParse("2025-09-30T12:00:00Z")}, Start: jscalendar.LocalDateTime{Time: mustParseTime("2025-09-30T12:00:00Z")},
Duration: "PT30M", Duration: "PT30M",
Status: jscalendar.StatusConfirmed, Status: jscalendar.StatusConfirmed,
Object: jscalendar.Object{ Object: jscalendar.Object{
CommonObject: jscalendar.CommonObject{ CommonObject: jscalendar.CommonObject{
Uid: "9a7ab91a-edca-4988-886f-25e00743430d", Uid: "9a7ab91a-edca-4988-886f-25e00743430d",
ProdId: "Mock 0.0", ProdId: "Mock 0.0",
Created: MustParse("2025-09-29T16:17:18Z"), Created: mustParseTime("2025-09-29T16:17:18Z"),
Updated: MustParse("2025-09-29T16:17:18Z"), Updated: mustParseTime("2025-09-29T16:17:18Z"),
Title: "Meeting of the Minds", Title: "Meeting of the Minds",
Description: "Internal meeting about the grand strategy for the future", Description: "Internal meeting about the grand strategy for the future",
DescriptionContentType: "text/plain", DescriptionContentType: "text/plain",
@@ -104,7 +112,6 @@ var E1 = jmap.CalendarEvent{
}, },
RelatedTo: map[string]jscalendar.Relation{}, RelatedTo: map[string]jscalendar.Relation{},
Sequence: 0, Sequence: 0,
Method: jscalendar.MethodAdd,
ShowWithoutTime: false, ShowWithoutTime: false,
Locations: map[string]jscalendar.Location{ Locations: map[string]jscalendar.Location{
"ux1uokie": { "ux1uokie": {
@@ -139,7 +146,111 @@ var E1 = jmap.CalendarEvent{
}, },
}, },
}, },
// TODO more properties, a lot more properties RecurrenceRules: []jscalendar.RecurrenceRule{
{
Type: jscalendar.RecurrenceRuleType,
Frequency: jscalendar.FrequencyWeekly,
Interval: 1,
Rscale: jscalendar.RscaleIso8601,
Skip: jscalendar.SkipOmit,
FirstDayOfWeek: jscalendar.DayOfWeekMonday,
Count: 4,
},
},
FreeBusyStatus: jscalendar.FreeBusyStatusBusy,
Privacy: jscalendar.PrivacyPublic,
ReplyTo: map[jscalendar.ReplyMethod]string{
jscalendar.ReplyMethodImip: "mailto:organizer@example.com",
},
SentBy: "organizer@example.com",
Participants: map[string]jscalendar.Participant{
"eegh7uph": {
Type: jscalendar.ParticipantType,
Name: "Anderson Dawes",
Email: "adawes@opa.org",
Description: "Called the meeting",
SendTo: map[jscalendar.SendToMethod]string{
jscalendar.SendToMethodImip: "mailto:adawes@opa.org",
},
Kind: jscalendar.ParticipantKindIndividual,
Roles: map[jscalendar.Role]bool{
jscalendar.RoleAttendee: true,
jscalendar.RoleChair: true,
jscalendar.RoleOwner: true,
},
LocationId: "ux1uokie",
Language: "en-GB",
ParticipationStatus: jscalendar.ParticipationStatusAccepted,
ParticipationComment: "I'll be there for sure",
ExpectReply: true,
ScheduleAgent: jscalendar.ScheduleAgentServer,
ScheduleSequence: 1,
ScheduleStatus: []string{"1.0"},
ScheduleUpdated: mustParseTime("2025-10-01T11:59:12Z"),
SentBy: "adawes@opa.org",
InvitedBy: "eegh7uph",
Links: map[string]jscalendar.Link{
"ieni5eiw": {
Type: jscalendar.LinkType,
Href: "https://static.wikia.nocookie.net/expanse/images/1/1e/OPA_leader.png/revision/latest?cb=20250121103410",
ContentType: "image/png",
Rel: jscalendar.RelIcon,
Size: 192812,
Display: jscalendar.DisplayBadge,
Title: "Anderson Dawes' photo",
},
},
ScheduleId: "mailto:adawes@opa.org",
},
"xeikie9p": {
Type: jscalendar.ParticipantType,
Name: "Klaes Ashford",
Email: "ashford@opa.org",
Description: "As the first officer on the Behemoth",
SendTo: map[jscalendar.SendToMethod]string{
jscalendar.SendToMethodImip: "mailto:ashford@opa.org",
jscalendar.SendToMethodOther: "https://behemoth.example.com/ping/@ashford",
},
Kind: jscalendar.ParticipantKindIndividual,
Roles: map[jscalendar.Role]bool{
jscalendar.RoleAttendee: true,
},
LocationId: "em4eal0o",
Language: "en-GB",
ParticipationStatus: jscalendar.ParticipationStatusNeedsAction,
ExpectReply: true,
ScheduleAgent: jscalendar.ScheduleAgentServer,
ScheduleSequence: 0,
SentBy: "adawes@opa.org",
InvitedBy: "eegh7uph",
Links: map[string]jscalendar.Link{
"oifooj6g": {
Type: jscalendar.LinkType,
Href: "https://static.wikia.nocookie.net/expanse/images/0/02/Klaes_Ashford_-_Expanse_season_4_promotional_2.png/revision/latest?cb=20191206012007",
ContentType: "image/png",
Rel: jscalendar.RelIcon,
Size: 201291,
Display: jscalendar.DisplayBadge,
Title: "Ashford on Medina Station",
},
},
ScheduleId: "mailto:ashford@opa.org",
},
},
Alerts: map[string]jscalendar.Alert{
"ahqu4xi0": {
Type: jscalendar.AlertType,
Trigger: jscalendar.OffsetTrigger{
Type: jscalendar.OffsetTriggerType,
Offset: "PT-5M",
RelativeTo: jscalendar.RelativeToStart,
},
},
},
TimeZone: "UTC",
MayInviteSelf: true,
MayInviteOthers: true,
HideAttendees: false,
}, },
}, },
} }
@@ -7,7 +7,7 @@ import (
"github.com/opencloud-eu/opencloud/pkg/jscontact" "github.com/opencloud-eu/opencloud/pkg/jscontact"
) )
func MustParse(text string) time.Time { func mustParseTime(text string) time.Time {
t, err := time.Parse(time.RFC3339, text) t, err := time.Parse(time.RFC3339, text)
if err != nil { if err != nil {
panic(err) panic(err)
@@ -53,8 +53,8 @@ var CaminaDrummerContact = jscontact.ContactCard{
A2.Id: true, A2.Id: true,
}, },
Version: jscontact.JSContactVersion_1_0, Version: jscontact.JSContactVersion_1_0,
Created: MustParse("2025-09-30T11:00:12Z").UTC(), Created: mustParseTime("2025-09-30T11:00:12Z").UTC(),
Updated: MustParse("2025-09-30T11:00:12Z").UTC(), Updated: mustParseTime("2025-09-30T11:00:12Z").UTC(),
Kind: jscontact.ContactCardKindIndividual, Kind: jscontact.ContactCardKindIndividual,
Language: "en-GB", Language: "en-GB",
ProdId: "Mock 0.0", ProdId: "Mock 0.0",
@@ -330,7 +330,7 @@ var CaminaDrummerContact = jscontact.ContactCard{
Notes: map[string]jscontact.Note{ Notes: map[string]jscontact.Note{
"n1": { "n1": {
Type: jscontact.NoteType, Type: jscontact.NoteType,
Created: MustParse("2025-09-30T11:00:12Z").UTC(), Created: mustParseTime("2025-09-30T11:00:12Z").UTC(),
Author: &jscontact.Author{ Author: &jscontact.Author{
Type: jscontact.AuthorType, Type: jscontact.AuthorType,
Name: "expanse.fandom.com", Name: "expanse.fandom.com",
@@ -348,8 +348,8 @@ var AndersonDawesContact = jscontact.ContactCard{
A1.Id: true, A1.Id: true,
}, },
Version: jscontact.JSContactVersion_1_0, Version: jscontact.JSContactVersion_1_0,
Created: MustParse("2025-09-30T11:00:12Z").UTC(), Created: mustParseTime("2025-09-30T11:00:12Z").UTC(),
Updated: MustParse("2025-09-30T11:00:12Z").UTC(), Updated: mustParseTime("2025-09-30T11:00:12Z").UTC(),
Kind: jscontact.ContactCardKindIndividual, Kind: jscontact.ContactCardKindIndividual,
Language: "en-GB", Language: "en-GB",
ProdId: "Mock 0.0", ProdId: "Mock 0.0",
@@ -544,7 +544,7 @@ var AndersonDawesContact = jscontact.ContactCard{
Kind: jscontact.AnniversaryKindBirth, Kind: jscontact.AnniversaryKindBirth,
Date: jscontact.Timestamp{ Date: jscontact.Timestamp{
Type: jscontact.TimestampType, Type: jscontact.TimestampType,
Utc: MustParse("1961-08-24T00:00:00Z"), Utc: mustParseTime("1961-08-24T00:00:00Z"),
}, },
}, },
}, },
@@ -105,17 +105,17 @@ func (r Request) GetAccountIdForSubmission() (string, *Error) {
return r.getAccountId(r.session.PrimaryAccounts.Blob, errNoPrimaryAccountForSubmission) return r.getAccountId(r.session.PrimaryAccounts.Blob, errNoPrimaryAccountForSubmission)
} }
func (r Request) GetAccountForMail() (jmap.SessionAccount, *Error) { func (r Request) GetAccountForMail() (jmap.Account, *Error) {
accountId, err := r.GetAccountIdForMail() accountId, err := r.GetAccountIdForMail()
if err != nil { if err != nil {
return jmap.SessionAccount{}, err return jmap.Account{}, err
} }
account, ok := r.session.Accounts[accountId] account, ok := r.session.Accounts[accountId]
if !ok { if !ok {
r.logger.Debug().Msgf("failed to find account '%v'", accountId) r.logger.Debug().Msgf("failed to find account '%v'", accountId)
// TODO metric for inexistent accounts // TODO metric for inexistent accounts
return jmap.SessionAccount{}, apiError(r.errorId(), ErrorNonExistingAccount, return jmap.Account{}, apiError(r.errorId(), ErrorNonExistingAccount,
withDetail(fmt.Sprintf("The account '%v' does not exist", log.SafeString(accountId))), withDetail(fmt.Sprintf("The account '%v' does not exist", log.SafeString(accountId))),
withSource(&ErrorSource{Parameter: UriParamAccountId}), withSource(&ErrorSource{Parameter: UriParamAccountId}),
) )