From 64af4c6f33373ddfa30a284b25412c759a309a43 Mon Sep 17 00:00:00 2001
From: Pascal Bleser
Date: Wed, 29 Oct 2025 19:05:00 +0100
Subject: [PATCH] groupware: implement/fix email submission
---
pkg/jmap/jmap_api_email.go | 97 +++++++++------
pkg/jmap/jmap_model.go | 55 +++++++++
pkg/structs/structs.go | 7 ++
.../pkg/groupware/groupware_api_emails.go | 111 +++++++++---------
.../pkg/groupware/groupware_error.go | 7 ++
.../pkg/groupware/groupware_request.go | 28 +++++
.../pkg/groupware/groupware_route.go | 74 ++++++++----
7 files changed, 260 insertions(+), 119 deletions(-)
diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go
index c490af6e2c..987415a1ad 100644
--- a/pkg/jmap/jmap_api_email.go
+++ b/pkg/jmap/jmap_api_email.go
@@ -730,9 +730,11 @@ func (j *Client) UpdateEmails(accountId string, updates map[string]EmailUpdate,
if err != nil {
return nil, "", err
}
- if len(setResponse.NotUpdated) != len(updates) {
- // error occured
- // TODO(pbleser-oc) handle submission errors
+ if len(setResponse.NotUpdated) > 0 {
+ // TODO we don't have composite errors
+ for _, notUpdated := range setResponse.NotUpdated {
+ return nil, "", setErrorError(notUpdated, EmailType)
+ }
}
return setResponse.Updated, setResponse.NewState, nil
})
@@ -783,29 +785,49 @@ type SubmittedEmail struct {
MdnBlobIds []string `json:"mdnBlobIds,omitempty"`
}
-func (j *Client) SubmitEmail(accountId string, identityId string, emailId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, data []byte) (SubmittedEmail, SessionState, State, Language, Error) {
+type MoveMail struct {
+ FromMailboxId string
+ ToMailboxId string
+}
+
+func (j *Client) SubmitEmail(accountId string, identityId string, emailId string, move *MoveMail, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (EmailSubmission, SessionState, State, Language, Error) {
+ logger = j.logger("SubmitEmail", session, logger)
+
+ update := map[string]any{
+ EmailPropertyKeywords + "/" + JmapKeywordDraft: nil, // unmark as draft
+ EmailPropertyKeywords + "/" + JmapKeywordSeen: true, // mark as seen (read)
+ }
+ if move != nil && move.FromMailboxId != "" && move.ToMailboxId != "" && move.FromMailboxId != move.ToMailboxId {
+ update[EmailPropertyMailboxIds+"/"+move.FromMailboxId] = nil
+ update[EmailPropertyMailboxIds+"/"+move.ToMailboxId] = true
+ }
+
+ id := "s0"
+
set := EmailSubmissionSetCommand{
AccountId: accountId,
Create: map[string]EmailSubmissionCreate{
- "s0": {
+ id: {
IdentityId: identityId,
EmailId: emailId,
+ // leaving Envelope empty
},
},
OnSuccessUpdateEmail: map[string]PatchObject{
- "#s0": {
- EmailPropertyKeywords + "/" + JmapKeywordDraft: nil,
- },
+ "#" + id: update,
},
}
- get := EmailSubmissionGetRefCommand{
+ get := EmailSubmissionGetCommand{
AccountId: accountId,
- IdRef: &ResultReference{
- ResultOf: "0",
- Name: CommandEmailSubmissionSet,
- Path: "/created/s0/" + EmailPropertyId,
- },
+ Ids: []string{"#" + id},
+ /*
+ IdRef: &ResultReference{
+ ResultOf: "0",
+ Name: CommandEmailSubmissionSet,
+ Path: ["#"]"/created/" + "#" + id + "/" + EmailPropertyId,
+ },
+ */
}
cmd, err := j.request(session, logger,
@@ -813,14 +835,14 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
invocation(CommandEmailSubmissionGet, get, "1"),
)
if err != nil {
- return SubmittedEmail{}, "", "", "", err
+ return EmailSubmission{}, "", "", "", err
}
- return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (SubmittedEmail, State, Error) {
+ return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (EmailSubmission, State, Error) {
var submissionResponse EmailSubmissionSetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailSubmissionSet, "0", &submissionResponse)
if err != nil {
- return SubmittedEmail{}, "", err
+ return EmailSubmission{}, "", err
}
if len(submissionResponse.NotCreated) > 0 {
@@ -836,31 +858,28 @@ func (j *Client) SubmitEmail(accountId string, identityId string, emailId string
var setResponse EmailSetResponse
err = retrieveResponseMatchParameters(logger, body, CommandEmailSet, "0", &setResponse)
if err != nil {
- return SubmittedEmail{}, "", err
+ return EmailSubmission{}, "", err
}
- var getResponse EmailSubmissionGetResponse
- err = retrieveResponseMatchParameters(logger, body, CommandEmailSubmissionGet, "1", &getResponse)
- if err != nil {
- return SubmittedEmail{}, "", err
+ if emailId := structs.FirstKey(setResponse.Updated); emailId != nil && len(setResponse.Updated) == 1 {
+ var getResponse EmailSubmissionGetResponse
+ err = retrieveResponseMatchParameters(logger, body, CommandEmailSubmissionGet, "1", &getResponse)
+ if err != nil {
+ return EmailSubmission{}, "", err
+ }
+
+ if len(getResponse.List) != 1 {
+ // for some reason (error?)...
+ // TODO(pbleser-oc) handle absence of emailsubmission
+ }
+
+ submission := getResponse.List[0]
+
+ return submission, setResponse.NewState, nil
+ } else {
+ err = simpleError(fmt.Errorf("failed to submit email: updated is empty"), 0) // TODO proper error handling
+ return EmailSubmission{}, "", err
}
-
- if len(getResponse.List) != 1 {
- // for some reason (error?)...
- // TODO(pbleser-oc) handle absence of emailsubmission
- }
-
- submission := getResponse.List[0]
-
- return SubmittedEmail{
- Id: submission.Id,
- SendAt: submission.SendAt,
- ThreadId: submission.ThreadId,
- UndoStatus: submission.UndoStatus,
- Envelope: submission.Envelope,
- DsnBlobIds: submission.DsnBlobIds,
- MdnBlobIds: submission.MdnBlobIds,
- }, setResponse.NewState, nil
})
}
diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go
index 8a6ae7599b..ee34ad858e 100644
--- a/pkg/jmap/jmap_model.go
+++ b/pkg/jmap/jmap_model.go
@@ -2540,6 +2540,24 @@ type EmailSubmission struct {
MdnBlobIds []string `json:"mdnBlobIds,omitempty"`
}
+type EmailSubmissionGetCommand struct {
+ // The id of the account to use.
+ AccountId string `json:"accountId"`
+
+ // The ids of the EmailSubmission objects to return.
+ //
+ // If null, then all records of the data type are returned, if this is supported for that data
+ // type and the number of records does not exceed the maxObjectsInGet limit.
+ Ids []string `json:"ids,omitempty"`
+
+ // If supplied, only the properties listed in the array are returned for each EmailSubmission object.
+ //
+ // If null, all properties of the object are returned. The id property of the object is always returned,
+ // even if not explicitly requested. If an invalid property is requested, the call MUST be rejected
+ // with an invalidArguments error.
+ Properties []string `json:"properties,omitempty"`
+}
+
type EmailSubmissionGetRefCommand struct {
// The id of the account to use.
AccountId string `json:"accountId"`
@@ -2952,6 +2970,18 @@ type EmailCreate struct {
// ["From:" field]: https://www.rfc-editor.org/rfc/rfc5322.html#section-3.6.2
From []EmailAddress `json:"from,omitempty"`
+ // The value is identical to the value of header:To:asAddresses.
+ To []EmailAddress `json:"to,omitempty"`
+
+ // The value is identical to the value of header:Cc:asAddresses.
+ Cc []EmailAddress `json:"cc,omitempty"`
+
+ // The value is identical to the value of header:Bcc:asAddresses.
+ Bcc []EmailAddress `json:"bcc,omitempty"`
+
+ // The value is identical to the value of header:Reply-To:asAddresses.
+ ReplyTo []EmailAddress `json:"replyTo,omitempty"`
+
// The "Subject:" field contains a short string identifying the topic of the message.
Subject string `json:"subject,omitempty"`
@@ -2982,6 +3012,31 @@ type EmailCreate struct {
// This is a map of partId to an EmailBodyValue object for none, some, or all text/* parts.
BodyValues map[string]EmailBodyValue `json:"bodyValues,omitempty"`
+
+ // A list of text/plain, text/html, image/*, audio/*, and/or video/* parts to display (sequentially) as the
+ // message body, with a preference for text/plain when alternative versions are available.
+ TextBody []EmailBodyPart `json:"textBody,omitempty"`
+
+ // A list of text/plain, text/html, image/*, audio/*, and/or video/* parts to display (sequentially) as the
+ // message body, with a preference for text/html when alternative versions are available.
+ HtmlBody []EmailBodyPart `json:"htmlBody,omitempty"`
+
+ // A list, traversing depth-first, of all parts in bodyStructure.
+ //
+ // They must satisfy either of the following conditions:
+ //
+ // - not of type multipart/* and not included in textBody or htmlBody
+ // - of type image/*, audio/*, or video/* and not in both textBody and htmlBody
+ //
+ // None of these parts include subParts, including message/* types.
+ //
+ // Attached messages may be fetched using the Email/parse method and the blobId.
+ //
+ // Note that a text/html body part HTML may reference image parts in attachments by using cid:
+ // links to reference the Content-Id, as defined in [RFC2392], or by referencing the Content-Location.
+ //
+ // [RFC2392]: https://www.rfc-editor.org/rfc/rfc2392.html
+ Attachments []EmailBodyPart `json:"attachments,omitempty"`
}
type EmailUpdate map[string]any
diff --git a/pkg/structs/structs.go b/pkg/structs/structs.go
index 6d95c5e07e..e9e888e467 100644
--- a/pkg/structs/structs.go
+++ b/pkg/structs/structs.go
@@ -137,3 +137,10 @@ func Missing[E comparable](expected, actual []E) []E {
}
return missing
}
+
+func FirstKey[K comparable, V any](m map[K]V) *K {
+ for k := range m {
+ return &k
+ }
+ return nil
+}
diff --git a/services/groupware/pkg/groupware/groupware_api_emails.go b/services/groupware/pkg/groupware/groupware_api_emails.go
index bd2f91fad1..ee08bb03cd 100644
--- a/services/groupware/pkg/groupware/groupware_api_emails.go
+++ b/services/groupware/pkg/groupware/groupware_api_emails.go
@@ -887,24 +887,13 @@ func (g *Groupware) CreateEmail(w http.ResponseWriter, r *http.Request) {
}
logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
- var body jmap.Email
+ var body jmap.EmailCreate
err := req.body(&body)
if err != nil {
return errorResponse(err)
}
- create := jmap.EmailCreate{
- MailboxIds: body.MailboxIds,
- Keywords: body.Keywords,
- From: body.From,
- Subject: body.Subject,
- ReceivedAt: body.ReceivedAt,
- SentAt: body.SentAt,
- BodyStructure: body.BodyStructure,
- BodyValues: body.BodyValues,
- }
-
- created, sessionState, state, lang, jerr := g.jmap.CreateEmail(accountId, create, "", req.session, req.ctx, logger, req.language())
+ created, sessionState, state, lang, jerr := g.jmap.CreateEmail(accountId, body, "", req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -926,24 +915,13 @@ func (g *Groupware) ReplaceEmail(w http.ResponseWriter, r *http.Request) {
logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
- var body jmap.Email
+ var body jmap.EmailCreate
err := req.body(&body)
if err != nil {
return errorResponse(err)
}
- create := jmap.EmailCreate{
- MailboxIds: body.MailboxIds,
- Keywords: body.Keywords,
- From: body.From,
- Subject: body.Subject,
- ReceivedAt: body.ReceivedAt,
- SentAt: body.SentAt,
- BodyStructure: body.BodyStructure,
- BodyValues: body.BodyValues,
- }
-
- created, sessionState, state, lang, jerr := g.jmap.CreateEmail(accountId, create, replaceId, req.session, req.ctx, logger, req.language())
+ created, sessionState, state, lang, jerr := g.jmap.CreateEmail(accountId, body, replaceId, req.session, req.ctx, logger, req.language())
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -1305,6 +1283,58 @@ func (g *Groupware) DeleteEmails(w http.ResponseWriter, r *http.Request) {
})
}
+func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) {
+ g.respond(w, r, func(req Request) Response {
+ emailId := chi.URLParam(r, UriParamEmailId)
+
+ l := req.logger.With()
+ l.Str(UriParamEmailId, log.SafeString(emailId))
+
+ identityId, err := req.getMandatoryStringParam(QueryParamIdentityId)
+ if err != nil {
+ return errorResponse(err)
+ }
+ l.Str(QueryParamIdentityId, log.SafeString(identityId))
+
+ var move *jmap.MoveMail = nil
+ {
+ moveFromMailboxId, _ := req.getStringParam(QueryParamMoveFromMailboxId, "")
+ moveToMailboxId, _ := req.getStringParam(QueryParamMoveToMailboxId, "")
+ if moveFromMailboxId != "" && moveToMailboxId != "" {
+ move = &jmap.MoveMail{FromMailboxId: moveFromMailboxId, ToMailboxId: moveToMailboxId}
+ l.Str(QueryParamMoveFromMailboxId, log.SafeString(moveFromMailboxId)).Str(QueryParamMoveToMailboxId, log.SafeString(moveFromMailboxId))
+ } else if moveFromMailboxId == "" && moveToMailboxId == "" {
+ // nothing to change
+ } else {
+ missing := moveFromMailboxId
+ if moveFromMailboxId == "" {
+ missing = moveFromMailboxId
+ }
+ // only one is set
+ msg := fmt.Sprintf("Missing required value for query parameter '%v'", missing)
+ return errorResponse(req.observedParameterError(ErrorMissingMandatoryRequestParameter,
+ withDetail(msg),
+ withSource(&ErrorSource{Parameter: missing})))
+ }
+ }
+
+ accountId, gwerr := req.GetAccountIdForMail()
+ if gwerr != nil {
+ return errorResponse(gwerr)
+ }
+ l.Str(logAccountId, accountId)
+
+ logger := log.From(l)
+
+ resp, sessionState, state, lang, jerr := g.jmap.SubmitEmail(accountId, identityId, emailId, move, req.session, req.ctx, logger, req.language())
+ if jerr != nil {
+ return req.errorResponseFromJmap(jerr)
+ }
+
+ return etagResponse(resp, sessionState, state, lang)
+ })
+}
+
type AboutEmailsEvent struct {
Id string `json:"id"`
Source string `json:"source"`
@@ -1833,35 +1863,6 @@ func filterFromNotKeywords(keywords []string) jmap.EmailFilterElement {
}
}
-func squashQueryState[V any](all map[string]V, mapper func(V) jmap.State) jmap.State {
- n := len(all)
- if n == 0 {
- return jmap.State("")
- }
- if n == 1 {
- for _, v := range all {
- return mapper(v)
- }
- }
-
- parts := make([]string, n)
- sortedKeys := make([]string, n)
- i := 0
- for k := range all {
- sortedKeys[i] = k
- i++
- }
- slices.Sort(sortedKeys)
- for i, k := range sortedKeys {
- if v, ok := all[k]; ok {
- parts[i] = k + ":" + string(mapper(v))
- } else {
- parts[i] = k + ":"
- }
- }
- return jmap.State(strings.Join(parts, ","))
-}
-
var sanitizationPolicy *bluemonday.Policy = bluemonday.UGCPolicy()
var sanitizableMediaTypes = []string{
diff --git a/services/groupware/pkg/groupware/groupware_error.go b/services/groupware/pkg/groupware/groupware_error.go
index bb586d3928..b9ed2429b7 100644
--- a/services/groupware/pkg/groupware/groupware_error.go
+++ b/services/groupware/pkg/groupware/groupware_error.go
@@ -174,6 +174,7 @@ const (
ErrorCodeInvalidRequestPayload = "INVRQP"
ErrorCodeInvalidResponsePayload = "INVRSP"
ErrorCodeInvalidRequestParameter = "INVPAR"
+ ErrorCodeMissingMandatoryRequestParameter = "MISMPA"
ErrorCodeInvalidRequestBody = "INVBDY"
ErrorCodeNonExistingAccount = "INVACC"
ErrorCodeIndeterminateAccount = "INDACC"
@@ -297,6 +298,12 @@ var (
Title: "Invalid Request Parameter",
Detail: "At least one of the parameters in the request is invalid.",
}
+ ErrorMissingMandatoryRequestParameter = GroupwareError{
+ Status: http.StatusBadRequest,
+ Code: ErrorCodeMissingMandatoryRequestParameter,
+ Title: "Missing Mandatory Request Parameter",
+ Detail: "A mandatory request parameter is missing.",
+ }
ErrorInvalidRequestBody = GroupwareError{
Status: http.StatusBadRequest,
Code: ErrorCodeInvalidRequestBody,
diff --git a/services/groupware/pkg/groupware/groupware_request.go b/services/groupware/pkg/groupware/groupware_request.go
index 2a9b4e3a0b..5ac5a74958 100644
--- a/services/groupware/pkg/groupware/groupware_request.go
+++ b/services/groupware/pkg/groupware/groupware_request.go
@@ -168,6 +168,34 @@ func (r Request) parameterErrorResponse(param string, detail string) Response {
return errorResponse(r.parameterError(param, detail))
}
+func (r Request) getStringParam(param string, defaultValue string) (string, bool) {
+ q := r.r.URL.Query()
+ if !q.Has(param) {
+ return defaultValue, false
+ }
+ str := q.Get(param)
+ if str == "" {
+ return defaultValue, false
+ }
+ return str, true
+}
+
+func (r Request) getMandatoryStringParam(param string) (string, *Error) {
+ str := ""
+ q := r.r.URL.Query()
+ if q.Has(param) {
+ str = q.Get(param)
+ }
+ if str == "" {
+ msg := fmt.Sprintf("Missing required value for query parameter '%v'", param)
+ return "", r.observedParameterError(ErrorMissingMandatoryRequestParameter,
+ withDetail(msg),
+ withSource(&ErrorSource{Parameter: param}),
+ )
+ }
+ return str, nil
+}
+
func (r Request) parseIntParam(param string, defaultValue int) (int, bool, *Error) {
q := r.r.URL.Query()
if !q.Has(param) {
diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go
index c15b1b69d4..f5df7f3b39 100644
--- a/services/groupware/pkg/groupware/groupware_route.go
+++ b/services/groupware/pkg/groupware/groupware_route.go
@@ -30,6 +30,9 @@ const (
QueryParamSince = "since"
QueryParamMaxChanges = "maxchanges"
QueryParamMailboxId = "mailbox"
+ QueryParamIdentityId = "identity"
+ QueryParamMoveFromMailboxId = "move-from"
+ QueryParamMoveToMailboxId = "move-to"
QueryParamNotInMailboxId = "notmailbox"
QueryParamSearchText = "text"
QueryParamSearchFrom = "from"
@@ -74,43 +77,58 @@ func (g *Groupware) Route(r chi.Router) {
r.Get("/", g.GetEmailsForAllAccounts)
r.Get("/latest/summary", g.GetLatestEmailsSummaryForAllAccounts) // ?limit=10&seen=true&undesirable=true
})
- r.Get("/quota", g.GetQuotaForAllAccounts)
+ r.Route("/quota", func(r chi.Router) {
+ r.Get("/", g.GetQuotaForAllAccounts)
+ })
})
r.Route("/{accountid}", func(r chi.Router) {
r.Get("/", g.GetAccount)
r.Route("/identities", func(r chi.Router) {
r.Get("/", g.GetIdentities)
- r.Get("/{identityid}", g.GetIdentityById)
r.Post("/", g.AddIdentity)
- r.Patch("/{identityid}", g.ModifyIdentity)
- r.Delete("/{identityid}", g.DeleteIdentity)
+ r.Route("/{identityid}", func(r chi.Router) {
+ r.Get("/", g.GetIdentityById)
+ r.Patch("/", g.ModifyIdentity)
+ r.Delete("/", g.DeleteIdentity)
+ })
})
r.Get("/vacation", g.GetVacation)
r.Put("/vacation", g.SetVacation)
r.Get("/quota", g.GetQuota)
r.Route("/mailboxes", func(r chi.Router) {
r.Get("/", g.GetMailboxes) // ?name=&role=&subcribed=
- r.Get("/{mailboxid}", g.GetMailbox)
- r.Get("/{mailboxid}/emails", g.GetAllEmailsInMailbox)
- r.Get("/{mailboxid}/changes", g.GetMailboxChanges)
r.Post("/", g.CreateMailbox)
- r.Patch("/{mailboxid}", g.UpdateMailbox)
- r.Delete("/{mailboxid}", g.DeleteMailbox)
+ r.Route("/{mailboxid}", func(r chi.Router) {
+ r.Get("/", g.GetMailbox)
+ r.Get("/emails", g.GetAllEmailsInMailbox)
+ r.Get("/changes", g.GetMailboxChanges)
+ r.Patch("/", g.UpdateMailbox)
+ r.Delete("/", g.DeleteMailbox)
+ })
})
r.Route("/emails", func(r chi.Router) {
r.Get("/", g.GetEmails) // ?fetchemails=true&fetchbodies=true&text=&subject=&body=&keyword=&keyword=&...
r.Post("/", g.CreateEmail)
r.Delete("/", g.DeleteEmails)
- r.Get("/{emailid}", g.GetEmailsById) // Accept:message/rfc822
- r.Put("/{emailid}", g.ReplaceEmail)
- r.Patch("/{emailid}", g.UpdateEmail)
- r.Patch("/{emailid}/keywords", g.UpdateEmailKeywords)
- r.Post("/{emailid}/keywords", g.AddEmailKeywords)
- r.Delete("/{emailid}/keywords", g.RemoveEmailKeywords)
- r.Delete("/{emailid}", g.DeleteEmail)
- Report(r, "/{emailid}", g.RelatedToEmail)
- r.Get("/{emailid}/related", g.RelatedToEmail)
- r.Get("/{emailid}/attachments", g.GetEmailAttachments) // ?partId=&name=?&blobId=?
+ r.Route("/{emailid}", func(r chi.Router) {
+ r.Get("/", g.GetEmailsById) // Accept:message/rfc822
+ r.Put("/", g.ReplaceEmail)
+ r.Post("/", g.SendEmail)
+ r.Patch("/", g.UpdateEmail)
+ r.Delete("/", g.DeleteEmail)
+ Report(r, "/", g.RelatedToEmail)
+ r.Route("/related", func(r chi.Router) {
+ r.Get("/", g.RelatedToEmail)
+ })
+ r.Route("/keywords", func(r chi.Router) {
+ r.Patch("/", g.UpdateEmailKeywords)
+ r.Post("/", g.AddEmailKeywords)
+ r.Delete("/", g.RemoveEmailKeywords)
+ })
+ r.Route("/attachments", func(r chi.Router) {
+ r.Get("/", g.GetEmailAttachments) // ?partId=&name=?&blobId=?
+ })
+ })
})
r.Route("/blobs", func(r chi.Router) {
r.Get("/{blobid}", g.GetBlobMeta)
@@ -121,8 +139,10 @@ func (g *Groupware) Route(r chi.Router) {
})
r.Route("/addressbooks", func(r chi.Router) {
r.Get("/", g.GetAddressbooks)
- r.Get("/{addressbookid}", g.GetAddressbook)
- r.Get("/{addressbookid}/contacts", g.GetContactsInAddressbook)
+ r.Route("/{addressbookid}", func(r chi.Router) {
+ r.Get("/", g.GetAddressbook)
+ r.Get("/contacts", g.GetContactsInAddressbook)
+ })
r.Route("/contacts", func(r chi.Router) {
r.Post("/", g.CreateContact)
r.Delete("/{contactid}", g.DeleteContact)
@@ -130,13 +150,17 @@ func (g *Groupware) Route(r chi.Router) {
})
r.Route("/calendars", func(r chi.Router) {
r.Get("/", g.GetCalendars)
- r.Get("/{calendarid}", g.GetCalendarById)
- r.Get("/{calendarid}/events", g.GetEventsInCalendar)
+ r.Route("/{calendarid}", func(r chi.Router) {
+ r.Get("/", g.GetCalendarById)
+ r.Get("/events", g.GetEventsInCalendar)
+ })
})
r.Route("/tasklists", func(r chi.Router) {
r.Get("/", g.GetTaskLists)
- r.Get("/{tasklistid}", g.GetTaskListById)
- r.Get("/{tasklistid}/tasks", g.GetTasksInTaskList)
+ r.Route("/{tasklistid}", func(r chi.Router) {
+ r.Get("/", g.GetTaskListById)
+ r.Get("/tasks", g.GetTasksInTaskList)
+ })
})
})
})