diff --git a/pkg/jmap/jmap_api_email.go b/pkg/jmap/jmap_api_email.go index 07faa9f04f..bd99d37a7b 100644 --- a/pkg/jmap/jmap_api_email.go +++ b/pkg/jmap/jmap_api_email.go @@ -23,6 +23,9 @@ const ( type Emails struct { Emails []Email `json:"emails,omitempty"` + Total int `json:"total,omitzero"` + Limit int `json:"limit,omitzero"` + Offset int `json:"offset,omitzero"` State string `json:"state,omitempty"` } @@ -60,7 +63,7 @@ func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Co Filter: &EmailFilterCondition{InMailbox: mailboxId}, Sort: []Sort{{Property: emailSortByReceivedAt, IsAscending: false}}, CollapseThreads: true, - CalculateTotal: false, + CalculateTotal: true, } if offset >= 0 { query.Position = offset @@ -87,12 +90,24 @@ func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Co } return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (Emails, Error) { - var response EmailGetResponse - err = retrieveResponseMatchParameters(body, EmailGet, "1", &response) + var queryResponse EmailQueryResponse + err = retrieveResponseMatchParameters(body, EmailQuery, "0", &queryResponse) if err != nil { return Emails{}, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err} } - return Emails{Emails: response.List, State: body.SessionState}, nil + var getResponse EmailGetResponse + err = retrieveResponseMatchParameters(body, EmailGet, "1", &getResponse) + if err != nil { + return Emails{}, SimpleError{code: JmapErrorInvalidJmapResponsePayload, err: err} + } + + return Emails{ + Emails: getResponse.List, + State: body.SessionState, + Total: queryResponse.Total, + Limit: queryResponse.Limit, + Offset: queryResponse.Position, + }, nil }) } diff --git a/pkg/jmap/jmap_api_vacation.go b/pkg/jmap/jmap_api_vacation.go index a9e3936058..c26523470b 100644 --- a/pkg/jmap/jmap_api_vacation.go +++ b/pkg/jmap/jmap_api_vacation.go @@ -2,10 +2,15 @@ package jmap import ( "context" + "time" "github.com/opencloud-eu/opencloud/pkg/log" ) +const ( + vacationResponseId = "singleton" +) + // https://jmap.io/spec-mail.html#vacationresponseget func (j *Client) GetVacationResponse(accountId string, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseGetResponse, Error) { aid := session.MailAccountId(accountId) @@ -20,3 +25,115 @@ func (j *Client) GetVacationResponse(accountId string, session *Session, ctx con return response, simpleError(err, JmapErrorInvalidJmapResponsePayload) }) } + +type VacationResponseStatusChange struct { + VacationResponse VacationResponse `json:"vacationResponse"` + ResponseState string `json:"state"` + SessionState string `json:"sessionState"` +} + +func (j *Client) SetVacationResponseStatus(accountId string, enabled bool, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseStatusChange, Error) { + aid := session.MailAccountId(accountId) + logger = j.logger(aid, "EnableVacationResponse", session, logger) + + cmd, err := request(invocation(VacationResponseSet, VacationResponseSetRequest{ + AccountId: aid, + Update: map[string]PatchObject{ + "u": { + "/isEnabled": enabled, + }, + }, + }, "0")) + + if err != nil { + return VacationResponseStatusChange{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err} + } + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (VacationResponseStatusChange, Error) { + var response VacationResponseSetResponse + err = retrieveResponseMatchParameters(body, VacationResponseSet, "0", &response) + if err != nil { + return VacationResponseStatusChange{}, simpleError(err, JmapErrorInvalidJmapResponsePayload) + } + updated, ok := response.Updated["u"] + if !ok { + // TODO implement error when not updated + } + + return VacationResponseStatusChange{ + VacationResponse: updated, + ResponseState: response.NewState, + SessionState: response.State, + }, nil + }) +} + +type VacationResponseBody struct { + // Should a vacation response be sent if a message arrives between the "fromDate" and "toDate"? + IsEnabled bool `json:"isEnabled"` + // If "isEnabled" is true, messages that arrive on or after this date-time (but before the "toDate" if defined) should receive the + // user's vacation response. If null, the vacation response is effective immediately. + FromDate time.Time `json:"fromDate,omitzero"` + // If "isEnabled" is true, messages that arrive before this date-time but on or after the "fromDate" if defined) should receive the + // user's vacation response. If null, the vacation response is effective indefinitely. + ToDate time.Time `json:"toDate,omitzero"` + // The subject that will be used by the message sent in response to messages when the vacation response is enabled. + // If null, an appropriate subject SHOULD be set by the server. + Subject string `json:"subject,omitempty"` + // The plaintext body to send in response to messages when the vacation response is enabled. + // If this is null, the server SHOULD generate a plaintext body part from the "htmlBody" when sending vacation responses + // but MAY choose to send the response as HTML only. If both "textBody" and "htmlBody" are null, an appropriate default + // body SHOULD be generated for responses by the server. + TextBody string `json:"textBody,omitempty"` + // The HTML body to send in response to messages when the vacation response is enabled. + // If this is null, the server MAY choose to generate an HTML body part from the "textBody" when sending vacation responses + // or MAY choose to send the response as plaintext only. + HtmlBody string `json:"htmlBody,omitempty"` +} + +type VacationResponseChange struct { + VacationResponse VacationResponse `json:"vacationResponse"` + ResponseState string `json:"state"` + SessionState string `json:"sessionState"` +} + +func (j *Client) SetVacationResponse(accountId string, vacation VacationResponseBody, session *Session, ctx context.Context, logger *log.Logger) (VacationResponseChange, Error) { + aid := session.MailAccountId(accountId) + logger = j.logger(aid, "SetVacationResponse", session, logger) + + set := VacationResponseSetRequest{ + AccountId: aid, + Create: map[string]VacationResponse{ + vacationResponseId: { + IsEnabled: vacation.IsEnabled, + FromDate: vacation.FromDate, + ToDate: vacation.ToDate, + Subject: vacation.Subject, + TextBody: vacation.TextBody, + HtmlBody: vacation.HtmlBody, + }, + }, + } + + cmd, err := request(invocation(VacationResponseSet, set, "0")) + if err != nil { + return VacationResponseChange{}, SimpleError{code: JmapErrorInvalidJmapRequestPayload, err: err} + } + return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (VacationResponseChange, Error) { + var response VacationResponseSetResponse + err = retrieveResponseMatchParameters(body, VacationResponseSet, "0", &response) + if err != nil { + return VacationResponseChange{}, simpleError(err, JmapErrorInvalidJmapResponsePayload) + } + + created, ok := response.Created[vacationResponseId] + if !ok { + // TODO handle case where created is missing + } + + return VacationResponseChange{ + VacationResponse: created, + ResponseState: response.NewState, + SessionState: response.State, + }, nil + }) +} diff --git a/pkg/jmap/jmap_model.go b/pkg/jmap/jmap_model.go index 9cb7c51d41..2c35380d4e 100644 --- a/pkg/jmap/jmap_model.go +++ b/pkg/jmap/jmap_model.go @@ -1616,6 +1616,27 @@ type VacationResponseGetResponse struct { NotFound []any `json:"notFound,omitempty"` } +type VacationResponseSetRequest struct { + AccountId string `json:"accountId"` + IfInState string `json:"ifInState,omitempty"` + Create map[string]VacationResponse `json:"create,omitempty"` + Update map[string]PatchObject `json:"update,omitempty"` + Destroy []string `json:"destroy,omitempty"` +} + +type VacationResponseSetResponse struct { + AccountId string `json:"accountId"` + OldState string `json:"oldState,omitempty"` + NewState string `json:"newState,omitempty"` + Created map[string]VacationResponse `json:"created,omitempty"` + Updated map[string]VacationResponse `json:"updated,omitempty"` + Destroyed []string `json:"destroyed,omitempty"` + NotCreated map[string]SetError `json:"notCreated,omitempty"` + NotUpdated map[string]SetError `json:"notUpdated,omitempty"` + NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"` + State string `json:"state,omitempty"` +} + // One of these attributes must be set, but not both. type DataSourceObject struct { DataAsText string `json:"data:asText,omitempty"` @@ -1741,6 +1762,7 @@ const ( MailboxChanges Command = "Mailbox/changes" IdentityGet Command = "Identity/get" VacationResponseGet Command = "VacationResponse/get" + VacationResponseSet Command = "VacationResponse/set" SearchSnippetGet Command = "SearchSnippet/get" ) @@ -1758,5 +1780,6 @@ var CommandResponseTypeMap = map[Command]func() any{ ThreadGet: func() any { return ThreadGetResponse{} }, IdentityGet: func() any { return IdentityGetResponse{} }, VacationResponseGet: func() any { return VacationResponseGetResponse{} }, + VacationResponseSet: func() any { return VacationResponseSetResponse{} }, SearchSnippetGet: func() any { return SearchSnippetGetResponse{} }, } diff --git a/services/groupware/pkg/groupware/groupware_api.go b/services/groupware/pkg/groupware/groupware_api.go index e3c3e0eea8..c01dfb0e25 100644 --- a/services/groupware/pkg/groupware/groupware_api.go +++ b/services/groupware/pkg/groupware/groupware_api.go @@ -1,7 +1,7 @@ package groupware const ( - Version = "1.0.0" + Version = "0.0.1" ) const ( diff --git a/services/groupware/pkg/groupware/groupware_api_identity.go b/services/groupware/pkg/groupware/groupware_api_identity.go index 9a7f812184..a4e7b03459 100644 --- a/services/groupware/pkg/groupware/groupware_api_identity.go +++ b/services/groupware/pkg/groupware/groupware_api_identity.go @@ -4,7 +4,7 @@ import ( "net/http" ) -func (g Groupware) GetIdentity(w http.ResponseWriter, r *http.Request) { +func (g Groupware) GetIdentities(w http.ResponseWriter, r *http.Request) { g.respond(w, r, func(req Request) Response { res, err := g.jmap.GetIdentity(req.GetAccountId(), req.session, req.ctx, req.logger) if err != nil { diff --git a/services/groupware/pkg/groupware/groupware_api_messages.go b/services/groupware/pkg/groupware/groupware_api_messages.go index 9a79521edb..8e910f3980 100644 --- a/services/groupware/pkg/groupware/groupware_api_messages.go +++ b/services/groupware/pkg/groupware/groupware_api_messages.go @@ -12,7 +12,7 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" ) -func (g Groupware) GetAllMessages(w http.ResponseWriter, r *http.Request) { +func (g Groupware) GetAllMessagesInMailbox(w http.ResponseWriter, r *http.Request) { mailboxId := chi.URLParam(r, UriParamMailboxId) since := r.Header.Get(HeaderSince) @@ -280,6 +280,14 @@ func (g Groupware) searchMessages(w http.ResponseWriter, r *http.Request) { return errResp } + var empty jmap.EmailFilterElement + + if filter == empty { + errorId := req.errorId() + msg := "Invalid search request has no criteria" + return errorResponse(apiError(errorId, ErrorInvalidUserRequest, withDetail(msg))) + } + fetchEmails, ok, err := req.parseBoolParam(QueryParamSearchFetchEmails, false) if err != nil { return errorResponse(err) diff --git a/services/groupware/pkg/groupware/groupware_api_vacation.go b/services/groupware/pkg/groupware/groupware_api_vacation.go index b6b3dc8c16..e845fbfbc3 100644 --- a/services/groupware/pkg/groupware/groupware_api_vacation.go +++ b/services/groupware/pkg/groupware/groupware_api_vacation.go @@ -37,3 +37,19 @@ func (g Groupware) GetVacation(w http.ResponseWriter, r *http.Request) { return response(res, res.State) }) } + +func (g Groupware) SetVacation(w http.ResponseWriter, r *http.Request) { + g.respond(w, r, func(req Request) Response { + var body jmap.VacationResponseBody + err := req.body(&body) + if err != nil { + return errorResponse(err) + } + + res, jerr := g.jmap.SetVacationResponse(req.GetAccountId(), body, req.session, req.ctx, req.logger) + if jerr != nil { + return req.errorResponseFromJmap(jerr) + } + return response(res, res.SessionState) + }) +} diff --git a/services/groupware/pkg/groupware/groupware_error.go b/services/groupware/pkg/groupware/groupware_error.go index 15bc54ddf8..eeb01ba16e 100644 --- a/services/groupware/pkg/groupware/groupware_error.go +++ b/services/groupware/pkg/groupware/groupware_error.go @@ -119,7 +119,7 @@ func groupwareErrorFromJmap(j jmap.Error) *GroupwareError { case jmap.JmapErrorAuthenticationFailed: return &ErrorForbidden case jmap.JmapErrorInvalidHttpRequest: - return &ErrorInvalidRequest + return &ErrorInvalidBackendRequest case jmap.JmapErrorServerResponse: return &ErrorServerResponse case jmap.JmapErrorReadingResponseBody: @@ -148,7 +148,7 @@ const ( ErrorCodeInvalidAuthentication = "AUTINV" ErrorCodeMissingAuthentication = "AUTMIS" ErrorCodeForbiddenGeneric = "AUTFOR" - ErrorCodeInvalidRequest = "INVREQ" + ErrorCodeInvalidBackendRequest = "INVREQ" ErrorCodeServerResponse = "SRVRSP" ErrorCodeStreamingResponse = "SRVRST" ErrorCodeServerReadingResponse = "SRVRRE" @@ -162,6 +162,7 @@ const ( ErrorCodeInvalidRequestParameter = "INVPAR" ErrorCodeNonExistingAccount = "INVACC" ErrorCodeApiInconsistency = "APIINC" + ErrorCodeInvalidUserRequest = "INVURQ" ) var ( @@ -189,9 +190,9 @@ var ( Title: "Invalid Authentication", Detail: "Authentication credentials were provided but are either invalid or not authorized to perform the request operation.", } - ErrorInvalidRequest = GroupwareError{ + ErrorInvalidBackendRequest = GroupwareError{ Status: http.StatusInternalServerError, - Code: ErrorCodeInvalidRequest, + Code: ErrorCodeInvalidBackendRequest, Title: "Invalid Request", Detail: "The request that was meant to be sent to the mail server is invalid, which might be caused by configuration issues.", } @@ -261,6 +262,12 @@ var ( Title: "Invalid Request Parameter", Detail: "At least one of the parameters in the request is invalid.", } + ErrorInvalidUserRequest = GroupwareError{ + Status: http.StatusBadRequest, + Code: ErrorCodeInvalidUserRequest, + Title: "Invalid Request", + Detail: "The request is invalid.", + } ErrorNonExistingAccount = GroupwareError{ Status: http.StatusBadRequest, Code: ErrorCodeNonExistingAccount, diff --git a/services/groupware/pkg/groupware/groupware_route.go b/services/groupware/pkg/groupware/groupware_route.go index 687b6eccee..3c860eac04 100644 --- a/services/groupware/pkg/groupware/groupware_route.go +++ b/services/groupware/pkg/groupware/groupware_route.go @@ -39,19 +39,19 @@ func (g Groupware) Route(r chi.Router) { r.Get("/accounts", g.GetAccounts) r.Route("/accounts/{accountid}", func(r chi.Router) { r.Get("/", g.GetAccount) - r.Get("/identity", g.GetIdentity) + r.Get("/identities", g.GetIdentities) r.Get("/vacation", g.GetVacation) + r.Post("/vacation", g.SetVacation) r.Route("/mailboxes", func(r chi.Router) { r.Get("/", g.GetMailboxes) // ?name=&role=&subcribed= r.Get("/{mailbox}", g.GetMailbox) - r.Get("/{mailbox}/messages", g.GetAllMessages) + r.Get("/{mailbox}/messages", g.GetAllMessagesInMailbox) }) r.Route("/messages", func(r chi.Router) { r.Get("/", g.GetMessages) // ?fetchemails=true&fetchbodies=true&text=&subject=&body=&keyword=&keyword=&... r.Post("/", g.CreateMessage) r.Get("/{messageid}", g.GetMessagesById) - r.Patch("/{messageid}", g.UpdateMessage) // or PUT? - r.Put("/{messageid}", g.UpdateMessage) // or PATCH? + r.Put("/{messageid}", g.UpdateMessage) // or PATCH? r.Delete("/{messageId}", g.DeleteMessage) }) r.Route("/blobs", func(r chi.Router) {