diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go index 529442eed7..29b9f6aeda 100644 --- a/pkg/jmap/jmap_model.go +++ b/pkg/jmap/jmap_model.go @@ -10,16 +10,65 @@ import ( // https://www.iana.org/assignments/jmap/jmap.xml#jmap-data-types type ObjectType string -// TODO -type UTCDateTime struct { +// Where `UTCDate` is given as a type, it means a `Date` where the "time-offset" +// component MUST be `"Z"` (i.e., it must be in UTC time). +// +// For example, `"2014-10-30T06:12:00Z"`. +type UTCDate struct { time.Time } -// TODO +func (t UTCDate) MarshalJSON() ([]byte, error) { + // TODO imperfect, we're going to need something smarter here as the timezone is not actually + // fixed to be UTC but, instead, depends on the timezone that is defined in another property + // of the object where this LocalDate shows up in; alternatively, we might have to use a string + // here and leave the conversion to a usable timestamp up to the client or caller instead + return []byte("\"" + t.UTC().Format(time.RFC3339) + "\""), nil +} + +func (t *UTCDate) UnmarshalJSON(b []byte) error { + var tt time.Time + err := tt.UnmarshalJSON(b) + if err != nil { + return err + } + t.Time = tt.UTC() + return nil +} + +// Where `LocalDate` is given as a type, it means a string in the same format as `Date` +// (see [RFC8620, Section 1.4]), but with the time-offset omitted from the end. +// +// For example, `2014-10-30T14:12:00`. +// +// The interpretation in absolute time depends upon the time zone for the event, which +// may not be a fixed offset (for example when daylight saving time occurs). +// +// [RFC8620, Section 1.4]: https://www.rfc-editor.org/rfc/rfc8620.html#section-1.4 type LocalDate struct { time.Time } +const RFC3339Local = "2006-01-02T15:04:05" + +func (t LocalDate) MarshalJSON() ([]byte, error) { + // TODO imperfect, we're going to need something smarter here as the timezone is not actually + // fixed to be UTC but, instead, depends on the timezone that is defined in another property + // of the object where this LocalDate shows up in; alternatively, we might have to use a string + // here and leave the conversion to a usable timestamp up to the client or caller instead + return []byte("\"" + t.UTC().Format(RFC3339Local) + "\""), nil +} + +func (t *LocalDate) UnmarshalJSON(b []byte) error { + var tt time.Time + err := tt.UnmarshalJSON(b) + if err != nil { + return err + } + t.Time = tt.UTC() + return nil +} + // Should the calendar’s events be used as part of availability calculation? // // This MUST be one of: @@ -65,6 +114,10 @@ type ResourceType string // The Scope data type is used to represent the entities the quota applies to. type Scope string +type ActionMode string +type SendingMode string +type DispositionTypeOption string + const ( JmapCore = "urn:ietf:params:jmap:core" JmapMail = "urn:ietf:params:jmap:mail" @@ -162,6 +215,21 @@ const ( ScopeDomain = Scope("domain") // The quota information applies to all accounts belonging to the server. ScopeGlobal = Scope("global") + + ActionModeManualAction = ActionMode("manual-action") + ActionModeAutomaticAction = ActionMode("automatic-action") + + SendingModeMdnSentManually = SendingMode("mdn-sent-manually") + SendingModeMdnSentAutomatically = SendingMode("mdn-sent-automatically") + + DispositionTypeOptionDeleted = DispositionTypeOption("deleted") + DispositionTypeOptionDispatched = DispositionTypeOption("dispatched") + DispositionTypeOptionDisplayed = DispositionTypeOption("displayed") + DispositionTypeOptionProcessed = DispositionTypeOption("processed") + + IncludeInAvailabilityAll = IncludeInAvailability("all") + IncludeInAvailabilityAttending = IncludeInAvailability("attending") + IncludeInAvailabilityNone = IncludeInAvailability("none") ) var ( @@ -232,15 +300,24 @@ var ( ScopeDomain, ScopeGlobal, } -) -const ( - IncludeInAvailabilityAll = IncludeInAvailability("all") - IncludeInAvailabilityAttending = IncludeInAvailability("attending") - IncludeInAvailabilityNone = IncludeInAvailability("none") -) + ActionModes = []ActionMode{ + ActionModeManualAction, + ActionModeAutomaticAction, + } + + SendingModes = []SendingMode{ + SendingModeMdnSentManually, + SendingModeMdnSentAutomatically, + } + + DispositionTypeOptions = []DispositionTypeOption{ + DispositionTypeOptionDeleted, + DispositionTypeOptionDispatched, + DispositionTypeOptionDisplayed, + DispositionTypeOptionProcessed, + } -var ( IncludeInAvailabilities = []IncludeInAvailability{ IncludeInAvailabilityAll, IncludeInAvailabilityAttending, @@ -459,6 +536,9 @@ type SessionPrincipalsOwnerAccountCapabilities struct { PrincipalId string `json:"principalId,omitempty"` } +type SessionMDNAccountCapabilities struct { +} + type SessionAccountCapabilities struct { Mail SessionMailAccountCapabilities `json:"urn:ietf:params:jmap:mail"` Submission SessionSubmissionAccountCapabilities `json:"urn:ietf:params:jmap:submission"` @@ -469,6 +549,7 @@ type SessionAccountCapabilities struct { 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"` + MDN *SessionMDNAccountCapabilities `json:"urn:ietf:params:jmap:mdn,omitempty"` } type Account struct { @@ -541,6 +622,9 @@ type SessionContactsCapabilities struct { type SessionPrincipalCapabilities struct { } +type SessionMDNCapabilities struct { +} + type SessionCapabilities struct { Core SessionCoreCapabilities `json:"urn:ietf:params:jmap:core"` Mail SessionMailCapabilities `json:"urn:ietf:params:jmap:mail"` @@ -552,6 +636,7 @@ type SessionCapabilities struct { Websocket SessionWebsocketCapabilities `json:"urn:ietf:params:jmap:websocket"` Contacts *SessionContactsCapabilities `json:"urn:ietf:params:jmap:contacts"` Principals *SessionPrincipalCapabilities `json:"urn:ietf:params:jmap:principals"` + MDN *SessionMDNCapabilities `json:"urn:ietf:params:jmap:mdn,omitempty"` } type SessionPrimaryAccounts struct { @@ -757,6 +842,9 @@ const ( // // A description String property MAY be present on the SetError object to display to the user why they are not permitted. SetErrorForbiddenToSend = "forbiddenToSend" + + // The message has the `$mdnsent` keyword already set. + SetErrorMdnAlreadySent = "mdnAlreadySent" ) type SetError struct { @@ -3394,7 +3482,7 @@ type CalendarEvent struct { // `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"` + UtcStart UTCDate `json:"utcStart,omitzero"` // The server calculates the end time in UTC from the start/timeZone/duration properties of the event. // @@ -3403,7 +3491,7 @@ type CalendarEvent struct { // 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"` + UtcEnd UTCDate `json:"utcEnd,omitzero"` jscalendar.Event } @@ -3489,7 +3577,7 @@ type CalendarEventNotification struct { Id string `json:"id"` // The time this notification was created. - Created UTCDateTime `json:"created,omitzero"` + Created UTCDate `json:"created,omitzero"` // Who made the change. ChangedBy *Person `json:"person,omitempty"` @@ -3526,6 +3614,463 @@ type CalendarEventNotification struct { EventPatch PatchObject `json:"eventPatch,omitempty"` } +// Denotes the task list has a special purpose. +// +// This MUST be one of the following: +// !- `inbox`: This is the principal’s default task list; +// !- `trash`: This task list holds messages the user has discarded; +type TaskListRole string + +const ( + // This is the principal’s default task list. + TaskListRoleInbox = TaskListRole("inbox") + // This task list holds messages the user has discarded. + TaskListRoleTrash = TaskListRole("trash") +) + +var ( + DefaultWorkflowStatuses = []string{ + "completed", + "failed", + "in-process", + "needs-action", + "cancelled", + "pending", + } +) + +type TaskRights struct { + // The user may fetch the tasks in this task list. + MayReadItems bool `json:"mayReadItems"` + + // The user may create, modify or destroy all tasks in this task list, + // or move tasks to or from this task list. + // + // If this is `true`, the `mayWriteOwn`, `mayUpdatePrivate` and `mayRSVP` properties + // MUST all also be `true`. + MayWriteAll bool `json:"mayWriteAll"` + + // The user may create, modify or destroy a task on this task list if either they are + // the owner of the task (see below) or the task has no owner. + // + // This means the user may also transfer ownership by updating a task so they are no longer + // an owner. + MayWriteOwn bool `json:"mayWriteOwn"` + + // The user may modify the following properties on all tasks in the task list, even + // if they would not otherwise have permission to modify that task. + // + // These properties MUST all be stored per-user, and changes do not affect any other user of the task list. + // + // The user may also modify the above on a per-occurrence basis for recurring tasks + // (updating the `recurrenceOverrides` property of the task to do so). + MayUpdatePrivate bool `json:"mayUpdatePrivate"` + + // The user may modify the following properties of any `Participant` object that corresponds + // to one of the user’s `ParticipantIdentity` objects in the account, even if they would not + // otherwise have permission to modify that task + // !- `participationStatus` + // !- `participationComment` + // !- `expectReply` + // + // If the task has its `mayInviteSelf` property set to true, then the user may also add a new + // `Participant` to the task with a `sendTo` property that is the same as the `sendTo` property + // of one of the user’s `ParticipantIdentity` objects in the account. + // The `roles` property of the participant MUST only contain `attendee`. + // + // If the task has its `mayInviteOthers` property set to `true` and there is an existing + // `Participant` in the task corresponding to one of the user’s `ParticipantIdentity` objects + // in the account, then the user may also add new participants. + // The `roles` property of any new participant MUST only contain `attendee`. + // + // The user may also do all of the above on a per-occurrence basis for recurring tasks + // (updating the `recurrenceOverrides` property of the task to do so). + MayRSVP bool `json:"mayRSVP"` + + // The user may modify sharing for this task list. + MayAdmin bool `json:"mayAdmin"` + + // The user may delete the task list itself (server-set). + // + // This property MUST be false if the account to which this task list belongs has the `isReadOnly` + // property set to true. + MayDelete bool `json:"mayDelete"` +} + +type TaskList struct { + // The id of the task list (immutable; server-set). + Id string `json:"id,omitempty"` + + // Denotes the task list has a special purpose. + // + // This MUST be one of the following: + // !- `inbox`: This is the principal’s default task list; + // !- `trash`: This task list holds messages the user has discarded; + Role TaskListRole `json:"role,omitempty"` + + // The user-visible name of the task list. + // + // This may be any UTF-8 string of at least 1 character in length and maximum 255 octets in size. + Name string `json:"name,omitempty"` + + // An optional longer-form description of the task list, to provide context in shared environments + // where users need more than just the name. + Description string `json:"description,omitempty"` + + // A color to be used when displaying tasks associated with the task list. + // + // If not null, the value MUST be a case-insensitive color name taken from the set of names defined + // in Section 4.3 of CSS Color Module Level 3 COLORS, or an RGB value in hexadecimal notation, + // as defined in Section 4.2.1 of CSS Color Module Level 3. + // + // The color SHOULD have sufficient contrast to be used as text on a white background. + Color string `json:"color,omitempty"` + + // A map of keywords to the colors used when displaying the keywords associated to a task. + // + // The same considerations, as for `color` above, apply. + KeywordColors map[string]string `json:"keywordColors,omitempty"` + + // A map of categories to the colors used when displaying the categories associated to a task. + // + // The same considerations, as for `color` above, apply. + CategoryColors map[string]string `json:"categoryColors,omitempty"` + + // Defines the sort order of task lists 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. + // + // A task list with a lower order should be displayed before a list with a higher order in any list + // of task lists in the client’s UI. + // + // Task lists with equal order SHOULD be sorted in alphabetical order by name. + // + // The sorting should take into account locale-specific character order convention. + SortOrder uint `json:"sortOrder,omitzero"` + + // Has the user indicated they wish to see this task list in their client? + // + // This SHOULD default to false for task lists in shared accounts the user has access to, + // and true for any new task list created by the user themselves. + // + // If false, the task list should only be displayed when the user explicitly + // requests it or to offer it for the user to subscribe to. + IsSubscribed bool `json:"isSubscribed,omitzero"` + + // The time zone to use for tasks without a time zone when the server needs to resolve them + // into absolute time, e.g., for alerts or availability calculation. + // + // The value MUST be a time zone id from the IANA Time Zone Database TZDB. + // + // If null, the timeZone of the account’s associated Principal will be used. + // + // Clients SHOULD use this as the default for new tasks in this task list, if set. + TimeZone string `json:"timeZone,omitempty"` + + // Defines the allowed values for `workflowStatus`. + // + // The default values are based on the values defined within [@!RFC8984], Section 5.2.5 and `pending`. + // + // `pending` indicates the task has been created and accepted, but it currently is on-hold. + // + // As naming and workflows differ between systems, mapping the status correctly to the present values + // of the `Task` can be challenging. In the most simple case, a task system may support merely two states - `done` + // and `not-done`. + // + // On the other hand, statuses and their semantic meaning can differ between systems or task lists (e.g. projects). + // + // In case of uncertainty, here are some recommendations for mapping commonly observed values that can help + // during implementation: + // !- `completed`: `done` (most simple case), `closed`, `verified`, … + // !- `in-process`: `in-progress`, `active`, `assigned`, … + // !- `needs-action`: `not-done` (most simple case), `not-started`, `new`, … + // !- `pending`: `waiting`, `deferred`, `on-hold`, `paused`, … + WorkflowStatuses []string `json:"workflowStatuses,omitempty"` + + // A map of `Principal` id to rights for principals this task list is shared with. + // + // The principal to which this task list belongs MUST NOT be in this set. + // + // This is null if the task list is not shared with anyone. + // + // May be modified only if the user has the `mayAdmin` right. + // + // The account id for the principals may be found in the `urn:ietf:params:jmap:principals:owner` capability + // of the `Account` to which the task list belongs. + ShareWith map[string]TaskRights `json:"shareWith,omitempty"` + + // The set of access rights the user has in relation to this `TaskList`. + // + // The user may fetch the task if they have the `mayReadItems` right on any task list the task is in. + // + // The user may remove a task from a task list (by modifying the task’s `taskListId` property) if the user has the + // appropriate permission for that task list. + // + // The user may make other changes to the task if they have the right to do so in all task list to which the task belongs. + MyRights *TaskRights `json:"myRights,omitempty"` + + // A map of alert ids to `Alert` objects (see [@!RFC8984], Section 4.5.2) to apply for tasks + // where `showWithoutTime` is `false` and `useDefaultAlerts` is `true`. + // + // Ids MUST be unique across all default alerts in the account, including those in other task + // lists; a UUID is recommended. + // + // If omitted on creation, the default is server dependent. + // + // For example, servers may choose to always default to null, or may copy the alerts from the default task list. + DefaultAlertsWithTime map[string]jscalendar.Alert `json:"defaultAlertsWithTime,omitempty"` + + // A map of alert ids to `Alert` objects (see [@!RFC8984], Section 4.5.2) to apply for tasks + // where `showWithoutTime` is `true` and `useDefaultAlerts` is `true`. + // + // Ids MUST be unique across all default alerts in the account, including those in other task + // lists; a UUID is recommended. + // + // If omitted on creation, the default is server dependent. For example, servers may choose to always + // default to `null`, or may copy the alerts from the default task list. + DefaultAlertsWithoutTime map[string]jscalendar.Alert `json:"defaultAlertsWithoutTime,omitempty"` +} + +type TypeOfChecklist string +type TypeOfCheckItem string +type TypeOfTaskPerson string +type TypeOfComment string + +type TaskNotificationTypeOption string + +const ChecklistType = TypeOfChecklist("Checklist") +const CheckItemType = TypeOfCheckItem("CheckItem") +const TaskPersonType = TypeOfTaskPerson("Person") +const CommentType = TypeOfComment("Comment") +const TaskNotificationTypeOptionCreated = TaskNotificationTypeOption("created") +const TaskNotificationTypeOptionUpdated = TaskNotificationTypeOption("updated") +const TaskNotificationTypeOptionDestroyed = TaskNotificationTypeOption("destroyed") + +// The Person object has the following properties of which either principalId or uri MUST be defined. +type TaskPerson struct { + // Specifies the type of this object, this MUST be `Person`. + Type TypeOfTaskPerson `json:"@type,omitempty"` + + // The name of the person. + Name string `json:"name,omitempty"` + + // A URI value that identifies the person. + // + // This SHOULD be the `scheduleId` of the participant that this item was assigned to. + Uri string `json:"uri,omitempty"` + + // The id of the Principal corresponding to the person, if any. + PrincipalId string `json:"principalId,omitempty"` +} + +type Comment struct { + // Specifies the type of this object, this MUST be `Comment`. + Type TypeOfComment `json:"@type,omitempty"` + + // The free text value of this comment. + Message string `json:"message"` + + // The date and time when this note was created. + Created UTCDate `json:"created,omitzero"` + + // The date and time when this note was updated. + Updated UTCDate `json:"updated,omitzero"` + + // The author of this comment. + Author *TaskPerson `json:"author,omitempty"` +} + +type CheckItem struct { + // Specifies the type of this object, this MUST be `CheckItem`. + Type TypeOfCheckItem `json:"@type,omitempty"` + + // Title of the item. + Title string `json:"title,omitempty"` + + // Defines the sort order of `CheckItem` when presented in the client’s UI. + // + // The number MUST be an integer in the range 0 <= sortOrder < 2^31. + // + // An item with a lower order should be displayed before an item with a higher order. + // + // Items with equal order SHOULD be sorted in alphabetical order by name. + // + // The sorting should take into account locale-specific character order convention. + SortOrder uint `json:"sortOrder,omitzero"` + + // The date and time when this item was updated. + Updated UTCDate `json:"updated,omitzero"` + + IsComplete bool `json:"isComplete,omitzero"` + + // The person that this item is assigned to. + // + // The `Person` object has the following properties of which either `principalId` or `uri` + // MUST be defined. + Assignee *TaskPerson `json:"assignee,omitempty"` + + // Free-text comments associated with this task. + Comments map[string]Comment `json:"comments,omitempty"` +} + +type Checklist struct { + // Specifies the type of this object, this MUST be `Checklist`. + Type TypeOfChecklist `json:"@type,omitempty"` + + // Title of the list. + Title string `json:"title,omitempty"` + + // The items of the check list. + CheckItems []CheckItem `json:"checkItems,omitempty"` +} + +// A `Task` object contains information about a task. +// +// It is a JSTask object, as defined in [@!RFC8984]. However, as use-cases of task systems vary, this +// Section defines relevant parts of the JSTask object to implement the core task capability as well +// as several extensions to it. +// +// Only the core capability MUST be implemented by any task system. +// +// Implementers can choose the extensions that fit their own use case. +// +// For example, the recurrence extension allows having a `Task` object represent a series of recurring `Task`s. +type Task struct { + + // The id of the Task. + // + // This property is immutable. + // + // The id uniquely identifies a JSTask with a particular `uid` and `recurrenceId` within a particular account. + Id string `json:"id"` + + // The `TaskList` id this task belongs to. + // + // A task MUST belong to exactly one `TaskList` at all times (until it is destroyed). + TaskListId string `json:"taskListId"` + + // If `true`, this task is to be considered a draft. + // + // The server will not send any 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"` + + UtcStart UTCDate `json:"utcStart,omitzero"` + + UtcDue UTCDate `json:"utcDue,omitzero"` + + SortOrder uint `json:"sortOrder,omitzero"` + + WorkflowStatus string `json:"workflowStatus,omitempty"` + + jscalendar.Task + + // This specifies the estimated amount of work the task takes to complete. + // + // In Agile software development or Scrum, it is known as complexity or story points. + // + // The number has no actual unit, but a larger number means more work. + EstimatedWork uint `json:"estimatedWork,omitzero"` + + // This specifies the impact or severity of the task, but does not say anything + // about the actual prioritization. + // + // Some examples are: `minor`, `trivial`, `major` or `block`. + // + // Usually, the priority of a task is based upon its impact and urgency. + Impact string `json:"impact,omitempty"` + + // A map of Checklist IDs to Checklist objects, containing checklist items. + Checklists map[string]Checklist `json:"checklists,omitempty"` + + // This is only defined if the id property is a synthetic id, generated by the server + // to represent a particular instance of a recurring Task (immutable; server-set). + // + // This property gives the id of the “real” Task this was generated from. + BaseTaskId string `json:"baseTaskId,omitempty"` + + // Is this the authoritative source for this task (i.e., does it control scheduling + // for this task; the task has not been added as a result of an invitation from another + // task management system)? + // + // This is `true` if, and only if: + // !- the task’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 task. + IsOrigin bool `json:"isOrigin,omitzero"` + + // If true, any user that has access to the task may add themselves to it as a participant + // with the `attendee` role. + // + // This property MUST NOT be altered in the `recurrenceOverrides`; it may only be set on the master object. + // + // This indicates the task will accept “party crasher” RSVPs via iTIP, subject to any other domain-specific + // restrictions, and users may add themselves to the task via JMAP as long as they have the `mayRSVP` + // permission for the task list. + // + // default: false + MayInviteSelf bool `json:"mayInviteSelf,omitzero"` + + // If true, any current participant with the `attendee` role may add new participants with + // the `attendee` role to the task. + // + // This property MUST NOT be altered in the `recurrenceOverrides`; it may only be set on the master object. + // + // default: false + MayInviteOthers bool `json:"mayInviteOthers,omitzero"` + + // If true, only the owners of the task may see the full set of participants. + // + // Other sharees of the task may only see the owners and themselves. + // + // This property MUST NOT be altered in the `recurrenceOverrides`; it may only be set on the master object. + HideAttendees bool `json:"hideAttendees,omitzero"` +} + +// The `TaskNotification` data type records changes made by external entities to tasks in task lists +// the user is subscribed to. +// +// Notifications are stored in the same `Account` as the `Task` that was changed. +type TaskNotification struct { + // The id of the `TaskNotification`. + Id string `json:"id"` + + // The time this notification was created. + Created UTCDate `json:"created,omitzero"` + + // Who made the change. + ChangedBy *TaskPerson `json:"changedBy,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"` + + // This MUST be one of + // !- `created` + // !- `updated` + // !- `destroyed` + Type TaskNotificationTypeOption `json:"type"` + + // The id of the Task that this notification is about. + TaskId string `json:"taskId"` + + // Is this task 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). + Task *jscalendar.Task `json:"task,omitempty"` + + // A patch encoding the change between the data in the task property, and the data after the update updated only). + TaskPatch PatchObject `json:"taskPatch,omitempty"` +} + // A Principal represents an individual, group, location (e.g. a room), resource (e.g. a projector) or other entity // in a collaborative environment. // @@ -3677,6 +4222,96 @@ type Quota struct { Description string `json:"description,omitempty"` } +// See [RFC8098] for the exact meaning of these different fields. +// +// These fields are defined as case insensitive in [RFC8098] but are case sensitive in this RFC +// and MUST be converted to lowercase by "MDN/parse". +type Disposition struct { + ActionMode ActionMode `json:"actionMode,omitempty"` + SendingMode SendingMode `json:"sendingMode,omitempty"` + Type DispositionTypeOption `json:"type,omitempty"` +} + +// Message Disposition Notifications (MDNs) are defined in [RFC8098] and are used as "read receipts", +// "acknowledgements", or "receipt notifications". +// +// A client can come across MDNs in different ways: +// 1. When receiving an email message, an MDN can be sent to the sender. This specification defines an `MDN/send` method to cover this case. +// 2. When sending an email message, an MDN can be requested. This must be done with the help of a header field, as already specified by [RFC8098]; +// the header field can already be handled by guidance in [RFC8621]. +// 3. When receiving an MDN, the MDN could be related to an existing sent message. This is already covered by [RFC8621] in the +// `EmailSubmission` object. A client might want to display detailed information about a received MDN. +// This specification defines an `MDN/parse` method to cover this case. +type MDN struct { + // The `Email` id of the received message to which this MDN is related. + // + // This property MUST NOT be null for `MDN/send` but MAY be null in the response from the `MDN/parse` method. + ForEmailId string `json:"forEmailId,omitempty"` + + // The subject used as `Subject` header field for this MDN. + Subject string `json:"subject,omitempty"` + + // The human-readable part of the MDN, as plain text. + TextBody string `json:"textBody,omitempty"` + + // If true, the content of the original message will appear in the third component of the `multipart/report` generated + // for the MDN. + // + // See [RFC8098] for details and security considerations. + IncludeOriginalMessage bool `json:"includeOriginalMessage,omitzero"` + + // The name of the Mail User Agent (MUA) creating this MDN. + // + // It is used to build the MDN report part of the MDN. + // + // Note that a null value may have better privacy properties. + ReportingUA string `json:"reportingUA,omitempty"` + + // The object containing the diverse MDN disposition options. + Disposition Disposition `json:"disposition"` + + // The name of the gateway or Message Transfer Agent (MTA) that translated a foreign (non-Internet) + // message disposition notification into this MDN (server-set). + MdnGateway string `json:"mdnGateway,omitempty"` + + // The original recipient address as specified by the sender of the message for which the MDN is being issued (server-set). + OriginalRecipient string `json:"originalRecipient,omitempty"` + + // The recipient for which the MDN is being issued. + // + // If set, it overrides the value that would be calculated by the server from the `Identity` defined + // in the `MDN/send` method, unless explicitly set by the client. + FinalRecipient string `json:"finalRecipient,omitempty"` + + // The `Message-ID` header field [RFC5322] (not the JMAP id) of the message for which the MDN is being issued. + OriginalMessageId string `json:"originalMessageId,omitempty"` + + // Additional information in the form of text messages when the `error` disposition modifier appears. + Error []string `json:"error,omitempty"` + + // The object where keys are extension-field names, and values are extension-field values (see [RFC8098], Section 3.3). + ExtensionFields map[string]string `json:"extensionFields,omitempty"` +} + +type SendMDN struct { + // The id of the account to use. + AccountId string `json:"accountId"` + + // The id of the `Identity` to associate with these MDNs. + // + // The server will use this identity to define the sender of the MDNs and to set the `finalRecipient` field. + IdentityId string `json:"identityId"` + + // A map of the creation id (client specified) to MDN objects. + Send map[string]MDN `json:"send,omitempty"` + + // A map of the id to an object containing properties to update on the `Email` object referenced by the `MDN/send` + // if the sending succeeds. + // + // This will always be a backward reference to the creation id. + OnSuccessUpdateEmail map[string]PatchObject `json:"onSuccessUpdateEmail,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 b5c8cda6bb..034fa7c9ac 100644 --- a/pkg/jscalendar/jscalendar_model.go +++ b/pkg/jscalendar/jscalendar_model.go @@ -84,6 +84,20 @@ const ( RelationshipChild = Relationship("child") RelationshipParent = Relationship("parent") + // Only for Task, JMAP extension: this task depends on the referenced task in some manner. + // + // For example, a task may be blocked waiting on the other, referenced, task. + RelationshipDependsOn = Relationship("depends-on") + + // Only for Task, JMAP extension: the referenced task was cloned from this task. + RelationshipClone = Relationship("clone") + + // Only for Task, JMAP extension: the referenced task is a duplicate of this task. + RelationshipDuplicate = Relationship("duplicate") + + // Only for Task, JMAP extension: the referenced task was the cause for this task. + RelationshipCause = Relationship("cause") + DisplayBadge = Display("badge") DisplayGraphic = Display("graphic") DisplayFullsize = Display("fullsize") @@ -343,6 +357,9 @@ const ( RoleChair = Role("chair") RoleContact = Role("contact") + // JMAP Task extension: the participant is expected to work on the task. + RoleAssignee = Role("assignee") + ParticipationStatusNeedsAction = ParticipationStatus("needs-action") ParticipationStatusAccepted = ParticipationStatus("accepted") ParticipationStatusDeclined = ParticipationStatus("declined") @@ -380,6 +397,10 @@ var ( RelationshipNext, RelationshipChild, RelationshipParent, + RelationshipDependsOn, + RelationshipClone, + RelationshipDuplicate, + RelationshipCause, } Displays = []Display{ @@ -667,6 +688,7 @@ var ( RoleInformational, RoleChair, RoleContact, + RoleAssignee, } ParticipationStatuses = []ParticipationStatus{ diff --git a/services/groupware/pkg/groupware/groupware_api_calendars.go b/services/groupware/pkg/groupware/groupware_api_calendars.go index 046130cbc8..2cab9bf5e1 100644 --- a/services/groupware/pkg/groupware/groupware_api_calendars.go +++ b/services/groupware/pkg/groupware/groupware_api_calendars.go @@ -49,7 +49,7 @@ type SwaggerGetCalendarById200 struct { // 500: ErrorResponse500 func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { - calendarId := chi.URLParam(r, UriParamAddressBookId) + calendarId := chi.URLParam(r, UriParamCalendarId) // TODO replace with proper implementation for _, calendar := range AllCalendars { if calendar.Id == calendarId { @@ -78,7 +78,7 @@ type SwaggerGetEventsInCalendar200 struct { // 500: ErrorResponse500 func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { - calendarId := chi.URLParam(r, UriParamAddressBookId) + calendarId := chi.URLParam(r, UriParamCalendarId) // TODO replace with proper implementation events, ok := EventsMapByCalendarId[calendarId] if !ok { diff --git a/services/groupware/pkg/groupware/groupware_mock_calendars.go b/services/groupware/pkg/groupware/groupware_mock_calendars.go index fb0043fe1e..64505464f7 100644 --- a/services/groupware/pkg/groupware/groupware_mock_calendars.go +++ b/services/groupware/pkg/groupware/groupware_mock_calendars.go @@ -69,8 +69,8 @@ var E1 = jmap.CalendarEvent{ 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")}, + UtcStart: jmap.UTCDate{Time: mustParseTime("2025-10-01T00:00:00Z")}, + UtcEnd: jmap.UTCDate{Time: mustParseTime("2025-10-07T00:00:00Z")}, Event: jscalendar.Event{ Type: jscalendar.EventType, Start: jscalendar.LocalDateTime{Time: mustParseTime("2025-09-30T12:00:00Z")},