From 5df0ec0e25f9bef080e3bdc5f6819c54e2bb014f Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Wed, 1 Oct 2025 12:13:08 +0200
Subject: [PATCH] groupware: more mock data, added missing JMAP types
---
pkg/jmap/jmap_model.go | 564 +++++++++++++++++-
pkg/jscalendar/jscalendar_model.go | 34 +-
pkg/jscalendar/jscalendar_model_test.go | 1 -
services/groupware/api-examples.yaml | 1 -
.../pkg/groupware/groupware_api_account.go | 4 +-
.../pkg/groupware/groupware_mock_calendars.go | 123 +++-
.../pkg/groupware/groupware_mock_contacts.go | 14 +-
.../pkg/groupware/groupware_request.go | 6 +-
8 files changed, 668 insertions(+), 79 deletions(-)
diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go
index 52448a547..529442eed 100644
--- a/pkg/jmap/jmap_model.go
+++ b/pkg/jmap/jmap_model.go
@@ -7,11 +7,64 @@ import (
"github.com/opencloud-eu/opencloud/pkg/jscalendar"
)
+// https://www.iana.org/assignments/jmap/jmap.xml#jmap-data-types
+type ObjectType string
+
// TODO
-type UTCDate struct {
+type UTCDateTime struct {
time.Time
}
+// TODO
+type LocalDate struct {
+ time.Time
+}
+
+// Should the calendar’s 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 user’s 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 (
JmapCore = "urn:ietf:params:jmap:core"
JmapMail = "urn:ietf:params:jmap:mail"
@@ -24,6 +77,30 @@ const (
JmapBlob = "urn:ietf:params:jmap:blob"
JmapQuota = "urn:ietf:params:jmap:quota"
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 = "$"
JmapKeywordSeen = "$seen"
@@ -47,9 +124,71 @@ const (
JmapMailboxRoleSent = "sent"
//JmapMailboxRoleSubscribed = "subscribed"
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 (
+ 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{
JmapMailboxRoleInbox,
JmapMailboxRoleSent,
@@ -57,17 +196,43 @@ var (
JmapMailboxRoleJunk,
JmapMailboxRoleTrash,
}
-)
-// Should the calendar’s 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 user’s own account, and “none” for calendars shared with the user.
-type IncludeInAvailability string
+ CalendarEventNotificationOptionTypes = []CalendarEventNotificationTypeOption{
+ CalendarEventNotificationTypeOptionCreated,
+ CalendarEventNotificationTypeOptionUpdated,
+ CalendarEventNotificationTypeOptionDestroyed,
+ }
+
+ PrincipalTypeOptions = []PrincipalTypeOption{
+ PrincipalTypeOptionIndividual,
+ PrincipalTypeOptionGroup,
+ PrincipalTypeOptionResource,
+ PrincipalTypeOptionLocation,
+ PrincipalTypeOptionOther,
+ }
+
+ HttpDigestAlgorithms = []HttpDigestAlgorithm{
+ HttpDigestAlgorithmAdler32,
+ HttpDigestAlgorithmCrc32c,
+ HttpDigestAlgorithmMd5,
+ HttpDigestAlgorithmSha,
+ HttpDigestAlgorithmSha256,
+ HttpDigestAlgorithmSha512,
+ HttpDigestAlgorithmUnixSum,
+ HttpDigestAlgorithmUnixcksum,
+ }
+
+ ResourceTypes = []ResourceType{
+ ResourceTypeCount,
+ ResourceTypeOctets,
+ }
+
+ Scopes = []Scope{
+ ScopeAccount,
+ ScopeDomain,
+ ScopeGlobal,
+ }
+)
const (
IncludeInAvailabilityAll = IncludeInAvailability("all")
@@ -263,7 +428,7 @@ type SessionBlobAccountCapabilities struct {
// 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
- SupportedDigestAlgorithms []string `json:"supportedDigestAlgorithms"`
+ SupportedDigestAlgorithms []HttpDigestAlgorithm `json:"supportedDigestAlgorithms"`
}
type SessionQuotaAccountCapabilities struct {
@@ -280,6 +445,20 @@ type SessionContactsAccountCapabilities struct {
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 {
Mail SessionMailAccountCapabilities `json:"urn:ietf:params:jmap:mail"`
Submission SessionSubmissionAccountCapabilities `json:"urn:ietf:params:jmap:submission"`
@@ -288,9 +467,11 @@ type SessionAccountCapabilities struct {
Blob SessionBlobAccountCapabilities `json:"urn:ietf:params:jmap:blob"`
Quota SessionQuotaAccountCapabilities `json:"urn:ietf:params:jmap:quota"`
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.
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.
@@ -357,6 +538,9 @@ type SessionWebsocketCapabilities struct {
type SessionContactsCapabilities struct {
}
+type SessionPrincipalCapabilities struct {
+}
+
type SessionCapabilities struct {
Core SessionCoreCapabilities `json:"urn:ietf:params:jmap:core"`
Mail SessionMailCapabilities `json:"urn:ietf:params:jmap:mail"`
@@ -366,7 +550,8 @@ type SessionCapabilities struct {
Blob SessionBlobCapabilities `json:"urn:ietf:params:jmap:blob"`
Quota SessionQuotaCapabilities `json:"urn:ietf:params:jmap:quota"`
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 {
@@ -387,7 +572,7 @@ type State string
type SessionResponse struct {
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 user’s main or default
// 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
}
-type ObjectType string
-
-const (
- VacationResponseType ObjectType = "VacationResponse"
- EmailType ObjectType = "Email"
- EmailDeliveryType ObjectType = "EmailDelivery"
- MailboxType ObjectType = "Mailbox"
-)
-
type Command string
type Invocation struct {
@@ -3160,23 +3336,347 @@ type CalendarEvent struct {
// `recurrenceId` within a particular account.
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 event’s `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
-
- // TODO https://jmap.io/spec-calendars.html#calendar-events
+ // The server calculates the end time in UTC from the start/timeZone/duration properties of the event.
+ //
+ // 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
}
+// A ParticipantIdentity stores information about a URI that represents the user within that account in an event’s 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 string `json:"type"`
Description string `json:"description,omitempty"`
diff --git a/pkg/jscalendar/jscalendar_model.go b/pkg/jscalendar/jscalendar_model.go
index 2546f490f..b5c8cda6b 100644
--- a/pkg/jscalendar/jscalendar_model.go
+++ b/pkg/jscalendar/jscalendar_model.go
@@ -37,7 +37,6 @@ type SignedDuration string // TODO
type Relationship string
type Display string
type Rel string
-type Method string
type LocationTypeOption string
type LocationRelation string
type VirtualLocationFeature string
@@ -225,17 +224,6 @@ const (
RelWorkingCopy = Rel("working-copy")
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")
LocationTypeOptionAirport = LocationTypeOption("airport")
LocationTypeOptionArena = LocationTypeOption("arena")
@@ -537,17 +525,6 @@ var (
RelWorkingCopyOf,
}
- Methods = []Method{
- MethodPublish,
- MethodRequest,
- MethodReply,
- MethodAdd,
- MethodCancel,
- MethodRefresh,
- MethodCounter,
- MethodDeclineCounter,
- }
-
LocationTypeOptions = []LocationTypeOption{
LocationTypeOptionAircraft,
LocationTypeOptionAirport,
@@ -1781,10 +1758,11 @@ type Object struct {
// to know which version of the object a scheduling message relates to.
Sequence uint `json:"sequence,omitzero"`
- // This is the iTIP [RFC5546] method, in lowercase.
- //
- // This MUST only be present if the JSCalendar object represents an iTIP scheduling message.
- Method Method `json:"method,omitempty"`
+ /*
+ // CalendarEvent objects MUST NOT have a “method” property as this is only used when representing iTIP
+ // [@!RFC5546] scheduling messages, not events in a data store.
+ Method Method `json:"method,omitempty"`
+ */
// 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
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"'
diff --git a/pkg/jscalendar/jscalendar_model_test.go b/pkg/jscalendar/jscalendar_model_test.go
index c7b1c0fa5..5061927a5 100644
--- a/pkg/jscalendar/jscalendar_model_test.go
+++ b/pkg/jscalendar/jscalendar_model_test.go
@@ -727,7 +727,6 @@ func TestEvent(t *testing.T) {
},
},
Sequence: 3,
- Method: MethodRefresh,
ShowWithoutTime: true,
Locations: map[string]Location{
"loc1": {
diff --git a/services/groupware/api-examples.yaml b/services/groupware/api-examples.yaml
index e8d97e073..ddc474c6c 100644
--- a/services/groupware/api-examples.yaml
+++ b/services/groupware/api-examples.yaml
@@ -9,7 +9,6 @@ examples:
emailReceivedAt: '2025-09-23T10:58:03Z'
emailSentAt: '2025-09-23T12:58:03+02:00'
blobId: 'cfz7vkmhcfwl1gfln02hga2fb3xwsqirirousda0rs1soeosla2p1aiaahcqjwaf'
- attachmentName: 'Alloy_Yellow_Scale.pdf'
attachmentType: 'application/pdf'
attachmentSize: 192128
attachmentDisposition: 'attachment'
diff --git a/services/groupware/pkg/groupware/groupware_api_account.go b/services/groupware/pkg/groupware/groupware_api_account.go
index 94c6cb4d5..10500fb34 100644
--- a/services/groupware/pkg/groupware/groupware_api_account.go
+++ b/services/groupware/pkg/groupware/groupware_api_account.go
@@ -13,7 +13,7 @@ import (
type SwaggerGetAccountResponse struct {
// in: body
Body struct {
- *jmap.SessionAccount
+ *jmap.Account
}
}
@@ -40,7 +40,7 @@ func (g *Groupware) GetAccount(w http.ResponseWriter, r *http.Request) {
// swagger:response GetAccountsResponse200
type SwaggerGetAccountsResponse struct {
// in: body
- Body map[string]jmap.SessionAccount
+ Body map[string]jmap.Account
}
// swagger:route GET /groupware/accounts account accounts
diff --git a/services/groupware/pkg/groupware/groupware_mock_calendars.go b/services/groupware/pkg/groupware/groupware_mock_calendars.go
index 363fda8bc..fb0043fe1 100644
--- a/services/groupware/pkg/groupware/groupware_mock_calendars.go
+++ b/services/groupware/pkg/groupware/groupware_mock_calendars.go
@@ -20,7 +20,7 @@ var C1 = jmap.Calendar{
Type: jscalendar.AlertType,
Trigger: jscalendar.AbsoluteTrigger{
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{
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{
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",
Status: jscalendar.StatusConfirmed,
Object: jscalendar.Object{
CommonObject: jscalendar.CommonObject{
Uid: "9a7ab91a-edca-4988-886f-25e00743430d",
ProdId: "Mock 0.0",
- Created: MustParse("2025-09-29T16:17:18Z"),
- Updated: MustParse("2025-09-29T16:17:18Z"),
+ Created: mustParseTime("2025-09-29T16:17:18Z"),
+ Updated: mustParseTime("2025-09-29T16:17:18Z"),
Title: "Meeting of the Minds",
Description: "Internal meeting about the grand strategy for the future",
DescriptionContentType: "text/plain",
@@ -104,7 +112,6 @@ var E1 = jmap.CalendarEvent{
},
RelatedTo: map[string]jscalendar.Relation{},
Sequence: 0,
- Method: jscalendar.MethodAdd,
ShowWithoutTime: false,
Locations: map[string]jscalendar.Location{
"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,
},
},
}
diff --git a/services/groupware/pkg/groupware/groupware_mock_contacts.go b/services/groupware/pkg/groupware/groupware_mock_contacts.go
index 009f0a437..dfdc436b6 100644
--- a/services/groupware/pkg/groupware/groupware_mock_contacts.go
+++ b/services/groupware/pkg/groupware/groupware_mock_contacts.go
@@ -7,7 +7,7 @@ import (
"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)
if err != nil {
panic(err)
@@ -53,8 +53,8 @@ var CaminaDrummerContact = jscontact.ContactCard{
A2.Id: true,
},
Version: jscontact.JSContactVersion_1_0,
- Created: MustParse("2025-09-30T11:00:12Z").UTC(),
- Updated: MustParse("2025-09-30T11:00:12Z").UTC(),
+ Created: mustParseTime("2025-09-30T11:00:12Z").UTC(),
+ Updated: mustParseTime("2025-09-30T11:00:12Z").UTC(),
Kind: jscontact.ContactCardKindIndividual,
Language: "en-GB",
ProdId: "Mock 0.0",
@@ -330,7 +330,7 @@ var CaminaDrummerContact = jscontact.ContactCard{
Notes: map[string]jscontact.Note{
"n1": {
Type: jscontact.NoteType,
- Created: MustParse("2025-09-30T11:00:12Z").UTC(),
+ Created: mustParseTime("2025-09-30T11:00:12Z").UTC(),
Author: &jscontact.Author{
Type: jscontact.AuthorType,
Name: "expanse.fandom.com",
@@ -348,8 +348,8 @@ var AndersonDawesContact = jscontact.ContactCard{
A1.Id: true,
},
Version: jscontact.JSContactVersion_1_0,
- Created: MustParse("2025-09-30T11:00:12Z").UTC(),
- Updated: MustParse("2025-09-30T11:00:12Z").UTC(),
+ Created: mustParseTime("2025-09-30T11:00:12Z").UTC(),
+ Updated: mustParseTime("2025-09-30T11:00:12Z").UTC(),
Kind: jscontact.ContactCardKindIndividual,
Language: "en-GB",
ProdId: "Mock 0.0",
@@ -544,7 +544,7 @@ var AndersonDawesContact = jscontact.ContactCard{
Kind: jscontact.AnniversaryKindBirth,
Date: jscontact.Timestamp{
Type: jscontact.TimestampType,
- Utc: MustParse("1961-08-24T00:00:00Z"),
+ Utc: mustParseTime("1961-08-24T00:00:00Z"),
},
},
},
diff --git a/services/groupware/pkg/groupware/groupware_request.go b/services/groupware/pkg/groupware/groupware_request.go
index 625538cbd..ca83c987d 100644
--- a/services/groupware/pkg/groupware/groupware_request.go
+++ b/services/groupware/pkg/groupware/groupware_request.go
@@ -105,17 +105,17 @@ func (r Request) GetAccountIdForSubmission() (string, *Error) {
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()
if err != nil {
- return jmap.SessionAccount{}, err
+ return jmap.Account{}, err
}
account, ok := r.session.Accounts[accountId]
if !ok {
r.logger.Debug().Msgf("failed to find account '%v'", accountId)
// 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))),
withSource(&ErrorSource{Parameter: UriParamAccountId}),
)