From 18d4522d31a833178e454351aa9f5a2e240fbe73 Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Mon, 13 Apr 2026 16:53:52 +0200
Subject: [PATCH] groupware: add endpoints to patch addressbooks, contacts,
calendars, events
---
.../pkg/groupware/api_addressbooks.go | 29 ++++++++++++++++
.../groupware/pkg/groupware/api_calendars.go | 33 +++++++++++++++++--
.../groupware/pkg/groupware/api_contacts.go | 29 ++++++++++++++++
.../groupware/pkg/groupware/api_events.go | 29 ++++++++++++++++
services/groupware/pkg/groupware/route.go | 14 +++++---
5 files changed, 128 insertions(+), 6 deletions(-)
diff --git a/services/groupware/pkg/groupware/api_addressbooks.go b/services/groupware/pkg/groupware/api_addressbooks.go
index 6f169b81aa..e54e4c190b 100644
--- a/services/groupware/pkg/groupware/api_addressbooks.go
+++ b/services/groupware/pkg/groupware/api_addressbooks.go
@@ -158,3 +158,32 @@ func (g *Groupware) DeleteAddressBook(w http.ResponseWriter, r *http.Request) {
return req.noContent(accountId, sessionState, AddressBookResponseObjectType, state)
})
}
+
+func (g *Groupware) ModifyAddressBook(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := req.needContactWithAccount()
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+ id, err := req.PathParamDoc(UriParamAddressBookId, "The unique identifier of the AddressBook to modify")
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ l.Str(UriParamAddressBookId, log.SafeString(id))
+
+ var change jmap.AddressBookChange
+ err = req.body(&change)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ updated, sessionState, state, lang, jerr := g.jmap.UpdateAddressBook(accountId, id, change, ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+ return req.respond(accountId, updated, sessionState, AddressBookResponseObjectType, state)
+ })
+}
diff --git a/services/groupware/pkg/groupware/api_calendars.go b/services/groupware/pkg/groupware/api_calendars.go
index d2a2af3dbd..02f9dc874e 100644
--- a/services/groupware/pkg/groupware/api_calendars.go
+++ b/services/groupware/pkg/groupware/api_calendars.go
@@ -95,7 +95,7 @@ func (g *Groupware) GetCalendarChanges(w http.ResponseWriter, r *http.Request) {
func (g *Groupware) CreateCalendar(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needContactWithAccount()
+ ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
@@ -120,7 +120,7 @@ func (g *Groupware) CreateCalendar(w http.ResponseWriter, r *http.Request) {
func (g *Groupware) DeleteCalendar(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
- ok, accountId, resp := req.needContactWithAccount()
+ ok, accountId, resp := req.needCalendarWithAccount()
if !ok {
return resp
}
@@ -157,3 +157,32 @@ func (g *Groupware) DeleteCalendar(w http.ResponseWriter, r *http.Request) {
return req.noContent(accountId, sessionState, CalendarResponseObjectType, state)
})
}
+
+func (g *Groupware) ModifyCalendar(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := req.needCalendarWithAccount()
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+ id, err := req.PathParamDoc(UriParamCalendarId, "The unique identifier of the Calendar to modify")
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ l.Str(UriParamCalendarId, log.SafeString(id))
+
+ var change jmap.CalendarChange
+ err = req.body(&change)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ updated, sessionState, state, lang, jerr := g.jmap.UpdateCalendar(accountId, id, change, ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+ return req.respond(accountId, updated, sessionState, CalendarResponseObjectType, state)
+ })
+}
diff --git a/services/groupware/pkg/groupware/api_contacts.go b/services/groupware/pkg/groupware/api_contacts.go
index b7aeedc1ca..ebceefde6a 100644
--- a/services/groupware/pkg/groupware/api_contacts.go
+++ b/services/groupware/pkg/groupware/api_contacts.go
@@ -258,6 +258,35 @@ func (g *Groupware) DeleteContact(w http.ResponseWriter, r *http.Request) {
})
}
+func (g *Groupware) ModifyContact(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := req.needContactWithAccount()
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+ id, err := req.PathParamDoc(UriParamContactId, "The unique identifier of the Contact to modify")
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ l.Str(UriParamContactId, log.SafeString(id))
+
+ var change jmap.ContactCardChange
+ err = req.body(&change)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ updated, sessionState, state, lang, jerr := g.jmap.UpdateContactCard(accountId, id, change, ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+ return req.respond(accountId, updated, sessionState, ContactResponseObjectType, state)
+ })
+}
+
func mapContactCardSort(s SortCrit) jmap.ContactCardComparator {
attr := s.Attribute
if mapped, ok := ContactSortingPropertyMapping[s.Attribute]; ok {
diff --git a/services/groupware/pkg/groupware/api_events.go b/services/groupware/pkg/groupware/api_events.go
index 431febd46b..4c2195687c 100644
--- a/services/groupware/pkg/groupware/api_events.go
+++ b/services/groupware/pkg/groupware/api_events.go
@@ -159,6 +159,35 @@ func (g *Groupware) DeleteCalendarEvent(w http.ResponseWriter, r *http.Request)
})
}
+func (g *Groupware) ModifyCalendarEvent(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ ok, accountId, resp := req.needCalendarWithAccount()
+ if !ok {
+ return resp
+ }
+ l := req.logger.With().Str(accountId, log.SafeString(accountId))
+ id, err := req.PathParamDoc(UriParamEventId, "The unique identifier of the Calendar Event to modify")
+ if err != nil {
+ return req.error(accountId, err)
+ }
+ l.Str(UriParamEventId, log.SafeString(id))
+
+ var change jmap.CalendarEventChange
+ err = req.body(&change)
+ if err != nil {
+ return req.error(accountId, err)
+ }
+
+ logger := log.From(l)
+ ctx := req.ctx.WithLogger(logger)
+ updated, sessionState, state, lang, jerr := g.jmap.UpdateCalendarEvent(accountId, id, change, ctx)
+ if jerr != nil {
+ return req.jmapError(accountId, jerr, sessionState, lang)
+ }
+ return req.respond(accountId, updated, sessionState, EventResponseObjectType, state)
+ })
+}
+
// Parse a blob that contains an iCal file and return it as JSCalendar.
//
// @api:tags calendar,blob
diff --git a/services/groupware/pkg/groupware/route.go b/services/groupware/pkg/groupware/route.go
index 96667614e5..3edbad2681 100644
--- a/services/groupware/pkg/groupware/route.go
+++ b/services/groupware/pkg/groupware/route.go
@@ -151,8 +151,9 @@ func (g *Groupware) Route(r chi.Router) {
r.Post("/", g.CreateAddressBook)
r.Route("/{addressbookid}", func(r chi.Router) {
r.Get("/", g.GetAddressbook)
- r.Get("/contacts", g.GetContactsInAddressbook) //NOSONAR
+ r.Patch("/", g.ModifyAddressBook)
r.Delete("/", g.DeleteAddressBook)
+ r.Get("/contacts", g.GetContactsInAddressbook) //NOSONAR
})
})
r.Route("/contacts", func(r chi.Router) {
@@ -160,6 +161,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Post("/", g.CreateContact)
r.Route("/{contactid}", func(r chi.Router) {
r.Get("/", g.GetContactById)
+ r.Patch("/", g.ModifyContact)
r.Delete("/", g.DeleteContact)
})
})
@@ -168,12 +170,14 @@ func (g *Groupware) Route(r chi.Router) {
r.Post("/", g.CreateCalendar)
r.Route("/{calendarid}", func(r chi.Router) {
r.Get("/", g.GetCalendarById)
- r.Get("/events", g.GetEventsInCalendar) //NOSONAR
+ r.Patch("/", g.ModifyCalendar)
r.Delete("/", g.DeleteCalendar)
+ r.Get("/events", g.GetEventsInCalendar) //NOSONAR
})
})
r.Route("/events", func(r chi.Router) {
r.Post("/", g.CreateCalendarEvent)
+ r.Patch("/", g.ModifyCalendarEvent)
r.Delete("/{eventid}", g.DeleteCalendarEvent)
})
r.Route("/tasklists", func(r chi.Router) {
@@ -194,8 +198,10 @@ func (g *Groupware) Route(r chi.Router) {
// r.Get("/quotas", g.GetQuotaChanges)
r.Get("/identities", g.GetIdentityChanges)
})
- r.Get("/objects", g.GetObjects)
- r.Post("/objects", g.GetObjects)
+ r.Route("/objects", func(r chi.Router) {
+ r.Get("/", g.GetObjects)
+ r.Post("/", g.GetObjects) // this is actually a read-only operation
+ })
})
})