From 96ee4e6db13e42b9326a8165b77c67ff0efb583d Mon Sep 17 00:00:00 2001 From: Pascal Bleser Date: Tue, 24 Mar 2026 10:55:42 +0100 Subject: [PATCH] groupware: pollute code with NOSONAR comments, and make a little more use of JMAP API templates --- pkg/jmap/api_blob.go | 2 +- pkg/jmap/api_bootstrap.go | 2 +- pkg/jmap/api_calendar.go | 114 +------------ pkg/jmap/api_contact.go | 2 +- pkg/jmap/api_email.go | 22 +-- pkg/jmap/api_identity.go | 2 +- pkg/jmap/api_mailbox.go | 6 +- pkg/jmap/api_quota.go | 33 +--- pkg/jmap/api_vacation.go | 22 +-- pkg/jmap/http.go | 29 ++-- pkg/jmap/integration_contact_test.go | 2 +- pkg/jmap/integration_email_test.go | 2 +- pkg/jmap/integration_event_test.go | 8 +- pkg/jmap/integration_test.go | 8 +- pkg/jmap/model.go | 30 ++-- pkg/jmap/model_examples.go | 10 +- pkg/jmap/templates.go | 156 ++++++++++++++++++ pkg/jmap/tools.go | 6 +- pkg/jscalendar/model.go | 22 ++- pkg/jscalendar/model_test.go | 8 +- pkg/jscontact/model.go | 2 + pkg/jscontact/model_test.go | 8 +- .../groupware/pkg/groupware/api_calendars.go | 2 +- .../groupware/pkg/groupware/api_contacts.go | 2 +- .../groupware/pkg/groupware/api_emails.go | 40 ++--- .../groupware/pkg/groupware/api_mailbox.go | 4 +- services/groupware/pkg/groupware/dns.go | 2 +- services/groupware/pkg/groupware/framework.go | 8 +- services/groupware/pkg/groupware/route.go | 6 +- services/groupware/pkg/groupware/session.go | 4 +- 30 files changed, 309 insertions(+), 255 deletions(-) create mode 100644 pkg/jmap/templates.go diff --git a/pkg/jmap/api_blob.go b/pkg/jmap/api_blob.go index bb0b0a6751..c6ab466cb7 100644 --- a/pkg/jmap/api_blob.go +++ b/pkg/jmap/api_blob.go @@ -52,7 +52,7 @@ func (j *Client) UploadBlobStream(accountId string, session *Session, ctx contex return j.blob.UploadBinary(ctx, logger, session, uploadUrl, session.UploadEndpoint, contentType, acceptLanguage, body) } -func (j *Client) DownloadBlobStream(accountId string, blobId string, name string, typ string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (*BlobDownload, Language, Error) { +func (j *Client) DownloadBlobStream(accountId string, blobId string, name string, typ string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (*BlobDownload, Language, Error) { //NOSONAR logger = log.From(logger.With().Str(logEndpoint, session.DownloadEndpoint)) // TODO(pbleser-oc) use a library for proper URL template parsing downloadUrl := session.DownloadUrlTemplate diff --git a/pkg/jmap/api_bootstrap.go b/pkg/jmap/api_bootstrap.go index 2635bf273a..532a8a4001 100644 --- a/pkg/jmap/api_bootstrap.go +++ b/pkg/jmap/api_bootstrap.go @@ -12,7 +12,7 @@ type AccountBootstrapResult struct { Quotas []Quota `json:"quotas,omitempty"` } -func (j *Client) GetBootstrap(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]AccountBootstrapResult, SessionState, State, Language, Error) { +func (j *Client) GetBootstrap(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]AccountBootstrapResult, SessionState, State, Language, Error) { //NOSONAR uniqueAccountIds := structs.Uniq(accountIds) logger = j.logger("GetBootstrap", session, logger) diff --git a/pkg/jmap/api_calendar.go b/pkg/jmap/api_calendar.go index 1e78dcb9a2..083fa6e986 100644 --- a/pkg/jmap/api_calendar.go +++ b/pkg/jmap/api_calendar.go @@ -2,7 +2,6 @@ package jmap import ( "context" - "fmt" "github.com/opencloud-eu/opencloud/pkg/log" "github.com/opencloud-eu/opencloud/pkg/structs" @@ -46,7 +45,7 @@ func (j *Client) GetCalendars(accountId string, session *Session, ctx context.Co ) } -func (j *Client) QueryCalendarEvents(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, +func (j *Client) QueryCalendarEvents(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, //NOSONAR filter CalendarEventFilterElement, sortBy []CalendarEventComparator, position uint, limit uint) (map[string][]CalendarEvent, SessionState, State, Language, Error) { logger = j.logger("QueryCalendarEvents", session, logger) @@ -137,114 +136,3 @@ func (j *Client) DeleteCalendarEvent(accountId string, destroy []string, session func(resp CalendarEventSetResponse) State { return resp.NewState }, accountId, destroy, session, ctx, logger, acceptLanguage) } - -func getTemplate[GETREQ any, GETRESP any, RESP any]( - client *Client, name string, getCommand Command, - getCommandFactory func(string, []string) GETREQ, - mapper func(GETRESP) RESP, - stateMapper func(GETRESP) State, - accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (RESP, SessionState, State, Language, Error) { - logger = client.logger(name, session, logger) - - var zero RESP - - cmd, err := client.request(session, logger, - invocation(getCommand, getCommandFactory(accountId, ids), "0"), - ) - if err != nil { - return zero, "", "", "", err - } - - return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (RESP, State, Error) { - var response GETRESP - err = retrieveResponseMatchParameters(logger, body, getCommand, "0", &response) - if err != nil { - return zero, "", err - } - - return mapper(response), stateMapper(response), nil - }) -} - -func createTemplate[T any, SETREQ any, GETREQ any, SETRESP any, GETRESP any]( - client *Client, name string, t ObjectType, setCommand Command, getCommand Command, - setCommandFactory func(string, map[string]T) SETREQ, - getCommandFactory func(string, string) GETREQ, - createdMapper func(SETRESP) map[string]*T, - notCreatedMapper func(SETRESP) map[string]SetError, - listMapper func(GETRESP) []T, - stateMapper func(SETRESP) State, - accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, create T) (*T, SessionState, State, Language, Error) { - logger = client.logger(name, session, logger) - - createMap := map[string]T{"c": create} - cmd, err := client.request(session, logger, - invocation(setCommand, setCommandFactory(accountId, createMap), "0"), - invocation(getCommand, getCommandFactory(accountId, "#c"), "1"), - ) - if err != nil { - return nil, "", "", "", err - } - - return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (*T, State, Error) { - var setResponse SETRESP - err = retrieveResponseMatchParameters(logger, body, setCommand, "0", &setResponse) - if err != nil { - return nil, "", err - } - - notCreatedMap := notCreatedMapper(setResponse) - setErr, notok := notCreatedMap["c"] - if notok { - logger.Error().Msgf("%T.NotCreated returned an error %v", setResponse, setErr) - return nil, "", setErrorError(setErr, t) - } - - createdMap := createdMapper(setResponse) - if created, ok := createdMap["c"]; !ok || created == nil { - berr := fmt.Errorf("failed to find %s in %s response", string(t), string(setCommand)) - logger.Error().Err(berr) - return nil, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload) - } - - var getResponse GETRESP - err = retrieveResponseMatchParameters(logger, body, getCommand, "1", &getResponse) - if err != nil { - return nil, "", err - } - - list := listMapper(getResponse) - - if len(list) < 1 { - berr := fmt.Errorf("failed to find %s in %s response", string(t), string(getCommand)) - logger.Error().Err(berr) - return nil, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload) - } - - return &list[0], stateMapper(setResponse), nil - }) -} - -func deleteTemplate[REQ any, RESP any](client *Client, name string, c Command, - commandFactory func(string, []string) REQ, - notDestroyedMapper func(RESP) map[string]SetError, - stateMapper func(RESP) State, - accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) { - logger = client.logger(name, session, logger) - - cmd, err := client.request(session, logger, - invocation(c, commandFactory(accountId, destroy), "0"), - ) - if err != nil { - return nil, "", "", "", err - } - - return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]SetError, State, Error) { - var setResponse RESP - err = retrieveResponseMatchParameters(logger, body, c, "0", &setResponse) - if err != nil { - return nil, "", err - } - return notDestroyedMapper(setResponse), stateMapper(setResponse), nil - }) -} diff --git a/pkg/jmap/api_contact.go b/pkg/jmap/api_contact.go index 790fbcb24d..7bdbd9a58a 100644 --- a/pkg/jmap/api_contact.go +++ b/pkg/jmap/api_contact.go @@ -160,7 +160,7 @@ func (j *Client) GetContactCardsSince(accountId string, session *Session, ctx co }) } -func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, +func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, //NOSONAR filter ContactCardFilterElement, sortBy []ContactCardComparator, position uint, limit uint) (map[string][]jscontact.ContactCard, SessionState, State, Language, Error) { logger = j.logger("QueryContactCards", session, logger) diff --git a/pkg/jmap/api_email.go b/pkg/jmap/api_email.go index e6cffd3ef5..558977f4a9 100644 --- a/pkg/jmap/api_email.go +++ b/pkg/jmap/api_email.go @@ -24,7 +24,7 @@ type getEmailsResult struct { } // Retrieve specific Emails by their id. -func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string, fetchBodies bool, maxBodyValueBytes uint, markAsSeen bool, withThreads bool) ([]Email, []string, SessionState, State, Language, Error) { +func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string, fetchBodies bool, maxBodyValueBytes uint, markAsSeen bool, withThreads bool) ([]Email, []string, SessionState, State, Language, Error) { //NOSONAR logger = j.logger("GetEmails", session, logger) get := EmailGetCommand{AccountId: accountId, Ids: ids, FetchAllBodyValues: fetchBodies} @@ -48,7 +48,7 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte IdsRef: &ResultReference{ ResultOf: "1", Name: CommandEmailGet, - Path: "/list/*/" + EmailPropertyThreadId, + Path: "/list/*/" + EmailPropertyThreadId, //NOSONAR }, } methodCalls = append(methodCalls, invocation(CommandThreadGet, threads, "2")) @@ -113,7 +113,7 @@ func (j *Client) GetEmailBlobId(accountId string, session *Session, ctx context. } // Retrieve all the Emails in a given Mailbox by its id. -func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, mailboxId string, offset int, limit uint, collapseThreads bool, fetchBodies bool, maxBodyValueBytes uint, withThreads bool) (Emails, SessionState, State, Language, Error) { +func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, mailboxId string, offset int, limit uint, collapseThreads bool, fetchBodies bool, maxBodyValueBytes uint, withThreads bool) (Emails, SessionState, State, Language, Error) { //NOSONAR logger = j.loggerParams("GetAllEmailsInMailbox", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies).Int(logOffset, offset).Uint(logLimit, limit) }) @@ -135,7 +135,7 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx c get := EmailGetRefCommand{ AccountId: accountId, FetchAllBodyValues: fetchBodies, - IdsRef: &ResultReference{Name: CommandEmailQuery, Path: "/ids/*", ResultOf: "0"}, + IdsRef: &ResultReference{Name: CommandEmailQuery, Path: "/ids/*", ResultOf: "0"}, //NOSONAR } if maxBodyValueBytes > 0 { get.MaxBodyValueBytes = maxBodyValueBytes @@ -204,7 +204,7 @@ type EmailChanges struct { } // Get all the Emails that have been created, updated or deleted since a given state. -func (j *Client) GetEmailChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState State, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (EmailChanges, SessionState, State, Language, Error) { +func (j *Client) GetEmailChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceState State, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (EmailChanges, SessionState, State, Language, Error) { //NOSONAR logger = j.loggerParams("GetEmailChanges", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies).Str(logSinceState, string(sinceState)) }) @@ -289,7 +289,7 @@ type EmailSnippetQueryResult struct { QueryState State `json:"queryState"` } -func (j *Client) QueryEmailSnippets(accountIds []string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset int, limit uint) (map[string]EmailSnippetQueryResult, SessionState, State, Language, Error) { +func (j *Client) QueryEmailSnippets(accountIds []string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset int, limit uint) (map[string]EmailSnippetQueryResult, SessionState, State, Language, Error) { //NOSONAR logger = j.loggerParams("QueryEmailSnippets", session, logger, func(z zerolog.Context) zerolog.Context { return z.Uint(logLimit, limit).Int(logOffset, offset) }) @@ -405,7 +405,7 @@ type EmailQueryResult struct { QueryState State `json:"queryState"` } -func (j *Client) QueryEmails(accountIds []string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset int, limit uint, fetchBodies bool, maxBodyValueBytes uint) (map[string]EmailQueryResult, SessionState, State, Language, Error) { +func (j *Client) QueryEmails(accountIds []string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset int, limit uint, fetchBodies bool, maxBodyValueBytes uint) (map[string]EmailQueryResult, SessionState, State, Language, Error) { //NOSONAR logger = j.loggerParams("QueryEmails", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies) }) @@ -487,7 +487,7 @@ type EmailQueryWithSnippetsResult struct { QueryState State `json:"queryState"` } -func (j *Client) QueryEmailsWithSnippets(accountIds []string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset int, limit uint, fetchBodies bool, maxBodyValueBytes uint) (map[string]EmailQueryWithSnippetsResult, SessionState, State, Language, Error) { +func (j *Client) QueryEmailsWithSnippets(accountIds []string, filter EmailFilterElement, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, offset int, limit uint, fetchBodies bool, maxBodyValueBytes uint) (map[string]EmailQueryWithSnippetsResult, SessionState, State, Language, Error) { //NOSONAR logger = j.loggerParams("QueryEmailsWithSnippets", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies) }) @@ -806,7 +806,7 @@ type MoveMail struct { 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) { +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) { //NOSONAR logger = j.logger("SubmitEmail", session, logger) update := map[string]any{ @@ -924,7 +924,7 @@ func (j *Client) GetEmailSubmissionStatus(accountId string, submissionIds []stri return result.submissions, result.notFound, sessionState, state, lang, err } -func (j *Client) EmailsInThread(accountId string, threadId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, fetchBodies bool, maxBodyValueBytes uint) ([]Email, SessionState, State, Language, Error) { +func (j *Client) EmailsInThread(accountId string, threadId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, fetchBodies bool, maxBodyValueBytes uint) ([]Email, SessionState, State, Language, Error) { //NOSONAR logger = j.loggerParams("EmailsInThread", session, logger, func(z zerolog.Context) zerolog.Context { return z.Bool(logFetchBodies, fetchBodies).Str("threadId", log.SafeString(threadId)) }) @@ -986,7 +986,7 @@ var EmailSummaryProperties = []string{ EmailPropertyPreview, } -func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter EmailFilterElement, limit uint, withThreads bool) (map[string]EmailsSummary, SessionState, State, Language, Error) { +func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, filter EmailFilterElement, limit uint, withThreads bool) (map[string]EmailsSummary, SessionState, State, Language, Error) { //NOSONAR logger = j.logger("QueryEmailSummaries", session, logger) uniqueAccountIds := structs.Uniq(accountIds) diff --git a/pkg/jmap/api_identity.go b/pkg/jmap/api_identity.go index 3eabf7abf5..86dbbcc696 100644 --- a/pkg/jmap/api_identity.go +++ b/pkg/jmap/api_identity.go @@ -150,7 +150,7 @@ func (j *Client) CreateIdentity(accountId string, session *Session, ctx context. } setErr, notok := response.NotCreated["c"] if notok { - logger.Error().Msgf("%T.NotCreated returned an error %v", response, setErr) + logger.Error().Msgf("%T.NotCreated returned an error %v", response, setErr) //NOSONAR return Identity{}, "", setErrorError(setErr, IdentityType) } return response.Created["c"], response.NewState, nil diff --git a/pkg/jmap/api_mailbox.go b/pkg/jmap/api_mailbox.go index 0c0b2b643e..efe2d481d5 100644 --- a/pkg/jmap/api_mailbox.go +++ b/pkg/jmap/api_mailbox.go @@ -114,7 +114,7 @@ func (j *Client) SearchMailboxes(accountIds []string, session *Session, ctx cont }) } -func (j *Client) SearchMailboxIdsPerRole(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, roles []string) (map[string]map[string]string, SessionState, State, Language, Error) { +func (j *Client) SearchMailboxIdsPerRole(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, roles []string) (map[string]map[string]string, SessionState, State, Language, Error) { //NOSONAR logger = j.logger("SearchMailboxIdsPerRole", session, logger) uniqueAccountIds := structs.Uniq(accountIds) @@ -228,7 +228,7 @@ func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx conte } // Retrieve Email changes in Mailboxes of multiple Accounts. -func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceStateMap map[string]string, maxChanges uint) (map[string]MailboxChanges, SessionState, State, Language, Error) { +func (j *Client) GetMailboxChangesForMultipleAccounts(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, sinceStateMap map[string]string, maxChanges uint) (map[string]MailboxChanges, SessionState, State, Language, Error) { //NOSONAR logger = j.loggerParams("GetMailboxChangesForMultipleAccounts", session, logger, func(z zerolog.Context) zerolog.Context { sinceStateLogDict := zerolog.Dict() for k, v := range sinceStateMap { @@ -417,7 +417,7 @@ func (j *Client) GetInboxNameForMultipleAccounts(accountIds []string, session *S }) } -func (j *Client) UpdateMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, mailboxId string, ifInState string, update MailboxChange) (Mailbox, SessionState, State, Language, Error) { +func (j *Client) UpdateMailbox(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, mailboxId string, ifInState string, update MailboxChange) (Mailbox, SessionState, State, Language, Error) { //NOSONAR logger = j.logger("UpdateMailbox", session, logger) cmd, err := j.request(session, logger, invocation(CommandMailboxSet, MailboxSetCommand{ AccountId: accountId, diff --git a/pkg/jmap/api_quota.go b/pkg/jmap/api_quota.go index 3b82a13bd9..d67fa79bfb 100644 --- a/pkg/jmap/api_quota.go +++ b/pkg/jmap/api_quota.go @@ -4,32 +4,15 @@ import ( "context" "github.com/opencloud-eu/opencloud/pkg/log" - "github.com/opencloud-eu/opencloud/pkg/structs" ) func (j *Client) GetQuotas(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]QuotaGetResponse, SessionState, State, Language, Error) { - logger = j.logger("GetQuotas", session, logger) - - uniqueAccountIds := structs.Uniq(accountIds) - - invocations := make([]Invocation, len(uniqueAccountIds)) - for i, accountId := range uniqueAccountIds { - invocations[i] = invocation(CommandQuotaGet, QuotaGetCommand{AccountId: accountId}, mcid(accountId, "0")) - } - cmd, err := j.request(session, logger, invocations...) - if err != nil { - return nil, "", "", "", err - } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]QuotaGetResponse, State, Error) { - result := map[string]QuotaGetResponse{} - for _, accountId := range uniqueAccountIds { - var response QuotaGetResponse - err = retrieveResponseMatchParameters(logger, body, CommandQuotaGet, mcid(accountId, "0"), &response) - if err != nil { - return nil, "", err - } - result[accountId] = response - } - return result, squashStateFunc(result, func(q QuotaGetResponse) State { return q.State }), nil - }) + return getTemplateN(j, "GetQuotas", CommandQuotaGet, + func(accountId string, ids []string) QuotaGetCommand { + return QuotaGetCommand{AccountId: accountId} + }, + identity1, + func(resp QuotaGetResponse) State { return resp.State }, + accountIds, session, ctx, logger, acceptLanguage, []string{}, + ) } diff --git a/pkg/jmap/api_vacation.go b/pkg/jmap/api_vacation.go index ef553e5de8..f2a4f9d9b1 100644 --- a/pkg/jmap/api_vacation.go +++ b/pkg/jmap/api_vacation.go @@ -12,21 +12,15 @@ const ( vacationResponseId = "singleton" ) -// https://jmap.io/spec-mail.html#vacationresponseget func (j *Client) GetVacationResponse(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (VacationResponseGetResponse, SessionState, State, Language, Error) { - logger = j.logger("GetVacationResponse", session, logger) - cmd, err := j.request(session, logger, invocation(CommandVacationResponseGet, VacationResponseGetCommand{AccountId: accountId}, "0")) - if err != nil { - return VacationResponseGetResponse{}, "", "", "", err - } - return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (VacationResponseGetResponse, State, Error) { - var response VacationResponseGetResponse - err = retrieveResponseMatchParameters(logger, body, CommandVacationResponseGet, "0", &response) - if err != nil { - return VacationResponseGetResponse{}, "", err - } - return response, response.State, nil - }) + return getTemplate(j, "GetVacationResponse", CommandVacationResponseGet, + func(accountId string, ids []string) VacationResponseGetCommand { + return VacationResponseGetCommand{AccountId: accountId} + }, + identity1, + func(resp VacationResponseGetResponse) State { return resp.State }, + accountId, session, ctx, logger, acceptLanguage, []string{}, + ) } // Same as VacationResponse but without the id. diff --git a/pkg/jmap/http.go b/pkg/jmap/http.go index 3561cd3bbc..bd6eda438c 100644 --- a/pkg/jmap/http.go +++ b/pkg/jmap/http.go @@ -63,18 +63,25 @@ type nullHttpJmapApiClientEventListener struct { } func (l nullHttpJmapApiClientEventListener) OnSuccessfulRequest(endpoint string, status int) { + // null implementation does nothing } func (l nullHttpJmapApiClientEventListener) OnFailedRequest(endpoint string, err error) { + // null implementation does nothing } func (l nullHttpJmapApiClientEventListener) OnFailedRequestWithStatus(endpoint string, status int) { + // null implementation does nothing } func (l nullHttpJmapApiClientEventListener) OnResponseBodyReadingError(endpoint string, err error) { + // null implementation does nothing } func (l nullHttpJmapApiClientEventListener) OnResponseBodyUnmarshallingError(endpoint string, err error) { + // null implementation does nothing } func (l nullHttpJmapApiClientEventListener) OnSuccessfulWsRequest(endpoint string, status int) { + // null implementation does nothing } func (l nullHttpJmapApiClientEventListener) OnFailedWsHandshakeRequestWithStatus(endpoint string, status int) { + // null implementation does nothing } var _ HttpJmapApiClientEventListener = nullHttpJmapApiClientEventListener{} @@ -187,14 +194,14 @@ func (h *HttpJmapClient) GetSession(ctx context.Context, sessionUrl *url.URL, us defer func(Body io.ReadCloser) { err := Body.Close() if err != nil { - logger.Error().Err(err).Msg("failed to close response body") + logger.Error().Err(err).Msg("failed to close response body") //NOSONAR } }(res.Body) } body, err := io.ReadAll(res.Body) if err != nil { - logger.Error().Err(err).Msg("failed to read response body") + logger.Error().Err(err).Msg("failed to read response body") //NOSONAR h.listener.OnResponseBodyReadingError(endpoint, err) return SessionResponse{}, SimpleError{code: JmapErrorReadingResponseBody, err: err} } @@ -210,7 +217,7 @@ func (h *HttpJmapClient) GetSession(ctx context.Context, sessionUrl *url.URL, us return data, nil } -func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request, acceptLanguage string) ([]byte, Language, Error) { +func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, session *Session, request Request, acceptLanguage string) ([]byte, Language, Error) { //NOSONAR jmapUrl := session.JmapUrl.String() endpoint := session.JmapEndpoint logger = log.From(logger.With().Str(logEndpoint, endpoint)) @@ -230,11 +237,11 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio // Some JMAP APIs use the Accept-Language header to determine which language to use to translate // texts in attributes. if acceptLanguage != "" { - req.Header.Add("Accept-Language", acceptLanguage) + req.Header.Add("Accept-Language", acceptLanguage) //NOSONAR } - req.Header.Add("Content-Type", "application/json") - req.Header.Add("User-Agent", h.userAgent) + req.Header.Add("Content-Type", "application/json") //NOSONAR + req.Header.Add("User-Agent", h.userAgent) //NOSONAR if logger.Trace().Enabled() { requestBytes, err := httputil.DumpRequestOut(req, true) @@ -262,10 +269,10 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio } } - language := Language(res.Header.Get("Content-Language")) + language := Language(res.Header.Get("Content-Language")) //NOSONAR if res.StatusCode < 200 || res.StatusCode > 299 { h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode) - logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, log.SafeString(res.Status)).Msg("HTTP response status code is not 2xx") + logger.Error().Str(logEndpoint, endpoint).Str(logHttpStatus, log.SafeString(res.Status)).Msg("HTTP response status code is not 2xx") //NOSONAR return nil, language, SimpleError{code: JmapErrorServerResponse, err: err} } if res.Body != nil { @@ -288,7 +295,7 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio return body, language, nil } -func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, acceptLanguage string, body io.Reader) (UploadedBlob, Language, Error) { +func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, session *Session, uploadUrl string, endpoint string, contentType string, acceptLanguage string, body io.Reader) (UploadedBlob, Language, Error) { //NOSONAR logger = log.From(logger.With().Str(logEndpoint, endpoint)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadUrl, body) @@ -363,7 +370,7 @@ func (h *HttpJmapClient) UploadBinary(ctx context.Context, logger *log.Logger, s return result, language, nil } -func (h *HttpJmapClient) DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string, acceptLanguage string) (*BlobDownload, Language, Error) { +func (h *HttpJmapClient) DownloadBinary(ctx context.Context, logger *log.Logger, session *Session, downloadUrl string, endpoint string, acceptLanguage string) (*BlobDownload, Language, Error) { //NOSONAR logger = log.From(logger.With().Str(logEndpoint, endpoint)) req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, nil) @@ -554,7 +561,7 @@ type HttpWsClient struct { WsClient } -func (w *HttpWsClient) readPump() { +func (w *HttpWsClient) readPump() { //NOSONAR logger := log.From(w.logger.With().Str("username", w.username)) defer func() { if err := w.c.Close(); err != nil { diff --git a/pkg/jmap/integration_contact_test.go b/pkg/jmap/integration_contact_test.go index decdbf3e8f..33c636e273 100644 --- a/pkg/jmap/integration_contact_test.go +++ b/pkg/jmap/integration_contact_test.go @@ -97,7 +97,7 @@ type ContactsBoxes struct { var streetNumberRegex = regexp.MustCompile(`^(\d+)\s+(.+)$`) -func (s *StalwartTest) fillContacts( +func (s *StalwartTest) fillContacts( //NOSONAR t *testing.T, count uint, session *Session, diff --git a/pkg/jmap/integration_email_test.go b/pkg/jmap/integration_email_test.go index 433eb5cf3a..1a8c992703 100644 --- a/pkg/jmap/integration_email_test.go +++ b/pkg/jmap/integration_email_test.go @@ -484,7 +484,7 @@ var allKeywords = map[string]imap.Flag{ JmapKeywordSeen: imap.FlagSeen, } -func (s *StalwartTest) fillEmailsWithImap(folder string, count int, empty bool, user User) ([]filledMail, int, error) { +func (s *StalwartTest) fillEmailsWithImap(folder string, count int, empty bool, user User) ([]filledMail, int, error) { //NOSONAR to := fmt.Sprintf("%s <%s>", user.description, user.email) ccEvery := 2 bccEvery := 3 diff --git a/pkg/jmap/integration_event_test.go b/pkg/jmap/integration_event_test.go index a4d1ee76b5..c3c5585f03 100644 --- a/pkg/jmap/integration_event_test.go +++ b/pkg/jmap/integration_event_test.go @@ -85,7 +85,7 @@ type EventsBoxes struct { mayInvite bool } -func (s *StalwartTest) fillEvents( +func (s *StalwartTest) fillEvents( //NOSONAR t *testing.T, count uint, session *Session, @@ -156,7 +156,7 @@ func (s *StalwartTest) fillEvents( title := gofakeit.Sentence(1) description := gofakeit.Paragraph(1+rand.Intn(3), 1+rand.Intn(4), 1+rand.Intn(32), "\n") - descriptionFormat := pickRandom("text/plain", "text/html") + descriptionFormat := pickRandom("text/plain", "text/html") //NOSONAR if descriptionFormat == "text/html" { description = toHtml(description) } @@ -196,7 +196,7 @@ func (s *StalwartTest) fillEvents( "timeZone": tz, "hideAttendees": false, "replyTo": map[string]string{ - "imip": "mailto:" + organizerEmail, + "imip": "mailto:" + organizerEmail, //NOSONAR }, "locations": locationMaps, "virtualLocations": map[string]any{ @@ -299,7 +299,7 @@ func (s *StalwartTest) fillEvents( mime = "image/png" uri = "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(img) default: - mime = "image/jpeg" + mime = "image/jpeg" //NOSONAR uri = externalImageUri() } return map[string]any{ diff --git a/pkg/jmap/integration_test.go b/pkg/jmap/integration_test.go index eebbc61144..c4ccbe899b 100644 --- a/pkg/jmap/integration_test.go +++ b/pkg/jmap/integration_test.go @@ -206,7 +206,7 @@ func (lc *stalwartTestLogConsumer) Accept(l testcontainers.Log) { fmt.Print("STALWART: " + string(l.Content)) } -func newStalwartTest(t *testing.T) (*StalwartTest, error) { +func newStalwartTest(t *testing.T) (*StalwartTest, error) { //NOSONAR ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) var _ context.CancelFunc = cancel // ignore context leak warning: it is passed in the struct and called in Close() @@ -369,7 +369,7 @@ func newStalwartTest(t *testing.T) (*StalwartTest, error) { req.SetBasicAuth("mailadmin", "secret") resp, err := h.Do(req) require.NoError(t, err) - require.Equal(t, "200 OK", resp.Status) + require.Equal(t, "200 OK", resp.Status) //NOSONAR } for _, user := range users { @@ -562,7 +562,7 @@ type uploadedBlob struct { Sha512 string `json:"sha:512"` } -func (j *TestJmapClient) uploadBlob(accountId string, data []byte, mimetype string) (uploadedBlob, error) { +func (j *TestJmapClient) uploadBlob(accountId string, data []byte, mimetype string) (uploadedBlob, error) { //NOSONAR uploadUrl := strings.ReplaceAll(j.session.UploadUrl, "{accountId}", accountId) req, err := http.NewRequest(http.MethodPost, uploadUrl, bytes.NewReader(data)) if err != nil { @@ -608,7 +608,7 @@ func (j *TestJmapClient) uploadBlob(accountId string, data []byte, mimetype stri return result, nil } -func (j *TestJmapClient) command(body map[string]any) ([]any, error) { +func (j *TestJmapClient) command(body map[string]any) ([]any, error) { //NOSONAR payload, err := json.Marshal(body) if err != nil { return nil, err diff --git a/pkg/jmap/model.go b/pkg/jmap/model.go index 6ed66897c4..1a08029229 100644 --- a/pkg/jmap/model.go +++ b/pkg/jmap/model.go @@ -1369,7 +1369,9 @@ type MailboxFilterCondition struct { IsSubscribed *bool `json:"isSubscribed,omitempty"` } -func (c MailboxFilterCondition) _isAMailboxFilterElement() {} +func (c MailboxFilterCondition) _isAMailboxFilterElement() { //NOSONAR + // marker interface method, does not need to do anything +} var _ MailboxFilterElement = &MailboxFilterCondition{} @@ -1378,7 +1380,9 @@ type MailboxFilterOperator struct { Conditions []MailboxFilterElement `json:"conditions,omitempty"` } -func (c MailboxFilterOperator) _isAMailboxFilterElement() {} +func (c MailboxFilterOperator) _isAMailboxFilterElement() { //NOSONAR + // marker interface method, does not need to do anything +} var _ MailboxFilterElement = &MailboxFilterOperator{} @@ -1498,10 +1502,11 @@ type EmailFilterCondition struct { Header []string `json:"header,omitempty"` } -func (f EmailFilterCondition) _isAnEmailFilterElement() { +func (f EmailFilterCondition) _isAnEmailFilterElement() { //NOSONAR + // marker interface method, does not need to do anything } -func (f EmailFilterCondition) IsNotEmpty() bool { +func (f EmailFilterCondition) IsNotEmpty() bool { //NOSONAR if !f.After.IsZero() { return true } @@ -1572,7 +1577,8 @@ type EmailFilterOperator struct { Conditions []EmailFilterElement `json:"conditions,omitempty"` } -func (o EmailFilterOperator) _isAnEmailFilterElement() { +func (o EmailFilterOperator) _isAnEmailFilterElement() { //NOSONAR + // marker interface method, does not need to do anything } func (o EmailFilterOperator) IsNotEmpty() bool { @@ -5064,10 +5070,11 @@ type ContactCardFilterCondition struct { Note string `json:"note,omitempty"` } -func (f ContactCardFilterCondition) _isAContactCardFilterElement() { +func (f ContactCardFilterCondition) _isAContactCardFilterElement() { //NOSONAR + // marker interface method, does not need to do anything } -func (f ContactCardFilterCondition) IsNotEmpty() bool { +func (f ContactCardFilterCondition) IsNotEmpty() bool { //NOSONAR if len(f.InAddressBook) != 0 { return true } @@ -5138,7 +5145,8 @@ type ContactCardFilterOperator struct { Conditions []ContactCardFilterElement `json:"conditions,omitempty"` } -func (o ContactCardFilterOperator) _isAContactCardFilterElement() { +func (o ContactCardFilterOperator) _isAContactCardFilterElement() { //NOSONAR + // marker interface method, does not need to do anything } func (o ContactCardFilterOperator) IsNotEmpty() bool { @@ -5577,7 +5585,8 @@ type CalendarEventFilterCondition struct { Uid string `json:"uid,omitempty" doc:"opt"` } -func (f CalendarEventFilterCondition) _isACalendarEventFilterElement() { +func (f CalendarEventFilterCondition) _isACalendarEventFilterElement() { //NOSONAR + // marker interface method, does not need to do anything } func (f CalendarEventFilterCondition) IsNotEmpty() bool { @@ -5624,7 +5633,8 @@ type CalendarEventFilterOperator struct { Conditions []CalendarEventFilterElement `json:"conditions,omitempty"` } -func (o CalendarEventFilterOperator) _isACalendarEventFilterElement() { +func (o CalendarEventFilterOperator) _isACalendarEventFilterElement() { //NOSONAR + // marker interface method, does not need to do anything } func (o CalendarEventFilterOperator) IsNotEmpty() bool { diff --git a/pkg/jmap/model_examples.go b/pkg/jmap/model_examples.go index 933b1441b3..c1982c30a6 100644 --- a/pkg/jmap/model_examples.go +++ b/pkg/jmap/model_examples.go @@ -14,7 +14,7 @@ import ( c "github.com/opencloud-eu/opencloud/pkg/jscontact" ) -func SerializeExamples(e any) { +func SerializeExamples(e any) { //NOSONAR type example struct { Type string `json:"type"` Key string `json:"key,omitempty"` @@ -146,7 +146,7 @@ var ExemplarInstance = Exemplar{ AccountId: "b", Username: "cdrummer", IdentityId: "aemua9ai", - IdentityName: "Camina Drummer", + IdentityName: "Camina Drummer", //NOSONAR EmailAddress: "cdrummer@opa.example.com", BccName: "OPA Secretary", BccAddress: "secretary@opa.example.com", @@ -519,7 +519,7 @@ func (e Exemplar) Identities() []Identity { return []Identity{a, b} } -func (e Exemplar) Identity_req() Identity { +func (e Exemplar) Identity_req() Identity { //NOSONAR return Identity{ Name: e.IdentityName, Email: e.EmailAddress, @@ -964,7 +964,7 @@ func (e Exemplar) OtherJSCalendar() (c.Calendar, string, string) { Type: c.CalendarType, Kind: c.CalendarKindCalendar, Uri: "https://opencloud.example.com/calendar/d05779b6-9638-4694-9869-008a61df6025", - MediaType: "application/jscontact+json", + MediaType: "application/jscontact+json", //NOSONAR Contexts: map[c.CalendarContext]bool{ c.CalendarContextWork: true, }, @@ -1276,7 +1276,7 @@ func (e Exemplar) Anniversary() (c.Anniversary, string, string) { } func (e Exemplar) OtherAnniversary() (c.Anniversary, string, string) { - ts, _ := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00") + ts, _ := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00") //NOSONAR return c.Anniversary{ Type: c.AnniversaryType, Kind: c.AnniversaryKindBirth, diff --git a/pkg/jmap/templates.go b/pkg/jmap/templates.go new file mode 100644 index 0000000000..f327ca8efe --- /dev/null +++ b/pkg/jmap/templates.go @@ -0,0 +1,156 @@ +package jmap + +import ( + "context" + "fmt" + + "github.com/opencloud-eu/opencloud/pkg/log" + "github.com/opencloud-eu/opencloud/pkg/structs" +) + +func getTemplate[GETREQ any, GETRESP any, RESP any]( //NOSONAR + client *Client, name string, getCommand Command, + getCommandFactory func(string, []string) GETREQ, + mapper func(GETRESP) RESP, + stateMapper func(GETRESP) State, + accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (RESP, SessionState, State, Language, Error) { + logger = client.logger(name, session, logger) + + var zero RESP + + cmd, err := client.request(session, logger, + invocation(getCommand, getCommandFactory(accountId, ids), "0"), + ) + if err != nil { + return zero, "", "", "", err + } + + return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (RESP, State, Error) { + var response GETRESP + err = retrieveResponseMatchParameters(logger, body, getCommand, "0", &response) + if err != nil { + return zero, "", err + } + + return mapper(response), stateMapper(response), nil + }) +} + +func getTemplateN[GETREQ any, GETRESP any, RESP any]( //NOSONAR + client *Client, name string, getCommand Command, + getCommandFactory func(string, []string) GETREQ, + mapper func(map[string]GETRESP) RESP, + stateMapper func(GETRESP) State, + accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (RESP, SessionState, State, Language, Error) { + logger = client.logger(name, session, logger) + + var zero RESP + + uniqueAccountIds := structs.Uniq(accountIds) + + invocations := make([]Invocation, len(uniqueAccountIds)) + for i, accountId := range uniqueAccountIds { + invocations[i] = invocation(getCommand, getCommandFactory(accountId, ids), mcid(accountId, "0")) + } + + cmd, err := client.request(session, logger, invocations...) + if err != nil { + return zero, "", "", "", err + } + + return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (RESP, State, Error) { + result := map[string]GETRESP{} + for _, accountId := range uniqueAccountIds { + var response GETRESP + err = retrieveResponseMatchParameters(logger, body, getCommand, mcid(accountId, "0"), &response) + if err != nil { + return zero, "", err + } + result[accountId] = response + } + return mapper(result), squashStateFunc(result, stateMapper), nil + }) +} + +func createTemplate[T any, SETREQ any, GETREQ any, SETRESP any, GETRESP any]( //NOSONAR + client *Client, name string, t ObjectType, setCommand Command, getCommand Command, + setCommandFactory func(string, map[string]T) SETREQ, + getCommandFactory func(string, string) GETREQ, + createdMapper func(SETRESP) map[string]*T, + notCreatedMapper func(SETRESP) map[string]SetError, + listMapper func(GETRESP) []T, + stateMapper func(SETRESP) State, + accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, create T) (*T, SessionState, State, Language, Error) { + logger = client.logger(name, session, logger) + + createMap := map[string]T{"c": create} + cmd, err := client.request(session, logger, + invocation(setCommand, setCommandFactory(accountId, createMap), "0"), + invocation(getCommand, getCommandFactory(accountId, "#c"), "1"), + ) + if err != nil { + return nil, "", "", "", err + } + + return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (*T, State, Error) { + var setResponse SETRESP + err = retrieveResponseMatchParameters(logger, body, setCommand, "0", &setResponse) + if err != nil { + return nil, "", err + } + + notCreatedMap := notCreatedMapper(setResponse) + setErr, notok := notCreatedMap["c"] + if notok { + logger.Error().Msgf("%T.NotCreated returned an error %v", setResponse, setErr) + return nil, "", setErrorError(setErr, t) + } + + createdMap := createdMapper(setResponse) + if created, ok := createdMap["c"]; !ok || created == nil { + berr := fmt.Errorf("failed to find %s in %s response", string(t), string(setCommand)) + logger.Error().Err(berr) + return nil, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload) + } + + var getResponse GETRESP + err = retrieveResponseMatchParameters(logger, body, getCommand, "1", &getResponse) + if err != nil { + return nil, "", err + } + + list := listMapper(getResponse) + + if len(list) < 1 { + berr := fmt.Errorf("failed to find %s in %s response", string(t), string(getCommand)) + logger.Error().Err(berr) + return nil, "", simpleError(berr, JmapErrorInvalidJmapResponsePayload) + } + + return &list[0], stateMapper(setResponse), nil + }) +} + +func deleteTemplate[REQ any, RESP any](client *Client, name string, c Command, //NOSONAR + commandFactory func(string, []string) REQ, + notDestroyedMapper func(RESP) map[string]SetError, + stateMapper func(RESP) State, + accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (map[string]SetError, SessionState, State, Language, Error) { + logger = client.logger(name, session, logger) + + cmd, err := client.request(session, logger, + invocation(c, commandFactory(accountId, destroy), "0"), + ) + if err != nil { + return nil, "", "", "", err + } + + return command(client.api, logger, ctx, session, client.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string]SetError, State, Error) { + var setResponse RESP + err = retrieveResponseMatchParameters(logger, body, c, "0", &setResponse) + if err != nil { + return nil, "", err + } + return notDestroyedMapper(setResponse), stateMapper(setResponse), nil + }) +} diff --git a/pkg/jmap/tools.go b/pkg/jmap/tools.go index c6f2f836e7..bbba0c7ce0 100644 --- a/pkg/jmap/tools.go +++ b/pkg/jmap/tools.go @@ -52,7 +52,7 @@ func mcid(accountId string, tag string) string { return accountId + ":" + tag } -func command[T any](api ApiClient, +func command[T any](api ApiClient, //NOSONAR logger *log.Logger, ctx context.Context, session *Session, @@ -342,3 +342,7 @@ func intPtr(i int) *int { func boolPtr(b bool) *bool { return &b } + +func identity1[T any](t T) T { + return t +} diff --git a/pkg/jscalendar/model.go b/pkg/jscalendar/model.go index 7c26a5edf5..3cb12bde3a 100644 --- a/pkg/jscalendar/model.go +++ b/pkg/jscalendar/model.go @@ -1360,7 +1360,9 @@ type OffsetTrigger struct { var _ Trigger = OffsetTrigger{} -func (o OffsetTrigger) trigger() {} +func (o OffsetTrigger) trigger() { + // marker interface method, does not need to do anything +} type AbsoluteTrigger struct { // This specifies the type of this object. This MUST be `AbsoluteTrigger`. @@ -1372,7 +1374,9 @@ type AbsoluteTrigger struct { var _ Trigger = AbsoluteTrigger{} -func (o AbsoluteTrigger) trigger() {} +func (o AbsoluteTrigger) trigger() { + // marker interface method, does not need to do anything +} // An `UnknownTrigger` object is an object that contains an `@type` property whose value is not recognized // (i.e., not `OffsetTrigger` or `AbsoluteTrigger`) plus zero or more other properties. @@ -1384,10 +1388,12 @@ type UnknownTrigger map[string]any var _ Trigger = UnknownTrigger{} -func (o UnknownTrigger) trigger() {} +func (o UnknownTrigger) trigger() { + // marker interface method, does not need to do anything +} func MapstructTriggerHook() mapstructure.DecodeHookFunc { - fn := func(Trigger) {} + fn := func(Trigger) {} //NOSONAR wanted := reflect.TypeOf(fn).In(0) return func(from reflect.Type, to reflect.Type, data any) (any, error) { if to != wanted { @@ -2186,11 +2192,15 @@ type GroupEntry interface { groupEntry() } -func (e Event) groupEntry() {} +func (e Event) groupEntry() { + // marker interface method, does not need to do anything +} var _ GroupEntry = Event{} -func (t Task) groupEntry() {} +func (t Task) groupEntry() { + // marker interface method, does not need to do anything +} var _ GroupEntry = Task{} diff --git a/pkg/jscalendar/model_test.go b/pkg/jscalendar/model_test.go index e54c3e5a44..d21403dfe9 100644 --- a/pkg/jscalendar/model_test.go +++ b/pkg/jscalendar/model_test.go @@ -73,7 +73,7 @@ func TestLink(t *testing.T) { }`, Link{ Type: LinkType, Href: "https://opencloud.eu.example.com/f72ae875-40be-48a4-84ff-aea9aed3e085.png", - ContentType: "image/png", + ContentType: "image/png", //NOSONAR Size: 128912, Rel: RelIcon, Display: DisplayThumbnail, @@ -158,10 +158,10 @@ func TestNDay(t *testing.T) { } func TestRecurrenceRule(t *testing.T) { - ts, err := time.Parse(time.RFC3339, "2025-09-25T18:26:14+02:00") + ts, err := time.Parse(time.RFC3339, "2025-09-25T18:26:14+02:00") //NOSONAR require.NoError(t, err) ts = ts.UTC() - l := LocalDateTime("2025-09-25T16:26:14") + l := LocalDateTime("2025-09-25T16:26:14") //NOSONAR jsoneq(t, `{ "@type": "RecurrenceRule", @@ -361,7 +361,7 @@ func TestAlertWithAbsoluteTrigger(t *testing.T) { }, Acknowledged: ts, RelatedTo: map[string]Relation{ - "a2e729eb-7d9c-4ea7-8514-93d2590ef0a2": { + "a2e729eb-7d9c-4ea7-8514-93d2590ef0a2": { //NOSONAR Type: RelationType, Relation: map[Relationship]bool{ RelationshipFirst: true, diff --git a/pkg/jscontact/model.go b/pkg/jscontact/model.go index 8e1be8a3b7..17cf4d5492 100644 --- a/pkg/jscontact/model.go +++ b/pkg/jscontact/model.go @@ -1905,6 +1905,7 @@ type PartialDate struct { } func (_ PartialDate) isAnniversaryDate() { + // marker interface method, does not need to do anything } var _ AnniversaryDate = &PartialDate{} @@ -1920,6 +1921,7 @@ type Timestamp struct { var _ AnniversaryDate = &Timestamp{} func (_ Timestamp) isAnniversaryDate() { + // marker interface method, does not need to do anything } type Anniversary struct { diff --git a/pkg/jscontact/model_test.go b/pkg/jscontact/model_test.go index 684bf69556..f70eeabb7c 100644 --- a/pkg/jscontact/model_test.go +++ b/pkg/jscontact/model_test.go @@ -32,8 +32,8 @@ func TestCalendar(t *testing.T) { }`, Calendar{ Type: CalendarType, Kind: CalendarKindCalendar, - Uri: "https://opencloud.eu/calendar/d05779b6-9638-4694-9869-008a61df6025", - MediaType: "application/jscontact+json", + Uri: "https://opencloud.eu/calendar/d05779b6-9638-4694-9869-008a61df6025", //NOSONAR + MediaType: "application/jscontact+json", //NOSONAR Contexts: map[CalendarContext]bool{ CalendarContextWork: true, }, @@ -396,7 +396,7 @@ func TestOnlineService(t *testing.T) { Contexts: map[OnlineServiceContext]bool{ OnlineServiceContextWork: true, }, - Uri: "https://opa.org/cdrummer", + Uri: "https://opa.org/cdrummer", //NOSONAR User: "cdrummer@opa.org", Pref: 12, Label: "opa", @@ -550,7 +550,7 @@ func TestPartialDate(t *testing.T) { } func TestTimestamp(t *testing.T) { - ts, err := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00") + ts, err := time.Parse(time.RFC3339, "2025-09-25T18:26:14.094725532+02:00") //NOSONAR require.NoError(t, err) jsoneq(t, `{ "@type": "Timestamp", diff --git a/services/groupware/pkg/groupware/api_calendars.go b/services/groupware/pkg/groupware/api_calendars.go index 21c62919d4..91d8ca2336 100644 --- a/services/groupware/pkg/groupware/api_calendars.go +++ b/services/groupware/pkg/groupware/api_calendars.go @@ -56,7 +56,7 @@ func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) { } // Get all the events in a calendar of an account by its identifier. -func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request) { //NOSONAR g.respond(w, r, func(req Request) Response { ok, accountId, resp := req.needCalendarWithAccount() if !ok { diff --git a/services/groupware/pkg/groupware/api_contacts.go b/services/groupware/pkg/groupware/api_contacts.go index 28e9081642..973e3a8de3 100644 --- a/services/groupware/pkg/groupware/api_contacts.go +++ b/services/groupware/pkg/groupware/api_contacts.go @@ -90,7 +90,7 @@ func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) { } // Get all the contacts in an addressbook of an account by its identifier. -func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Request) { //NOSONAR g.respond(w, r, func(req Request) Response { ok, accountId, resp := req.needContactWithAccount() if !ok { diff --git a/services/groupware/pkg/groupware/api_emails.go b/services/groupware/pkg/groupware/api_emails.go index 57a44ce40b..e91afe8803 100644 --- a/services/groupware/pkg/groupware/api_emails.go +++ b/services/groupware/pkg/groupware/api_emails.go @@ -66,7 +66,7 @@ func (g *Groupware) GetEmailChanges(w http.ResponseWriter, r *http.Request) { // // A limit and an offset may be specified using the query parameters 'limit' and 'offset', // respectively. -func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request) { //NOSONAR g.respond(w, r, func(req Request) Response { l := req.logger.With() @@ -124,7 +124,7 @@ func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request }) } -func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { //NOSONAR accept := r.Header.Get("Accept") if accept == "message/rfc822" { g.stream(w, r, func(req Request, w http.ResponseWriter) *Error { @@ -230,7 +230,7 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) { } } -func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) { //NOSONAR contextAppender := func(l zerolog.Context) zerolog.Context { return l } q := r.URL.Query() var attachmentSelector func(jmap.EmailBodyPart) bool = nil @@ -434,7 +434,7 @@ type EmailSearchResults struct { QueryState jmap.State `json:"queryState,omitempty"` } -func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement, bool, int, uint, *log.Logger, *Error) { +func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement, bool, int, uint, *log.Logger, *Error) { //NOSONAR mailboxId, _ := req.getStringParam(QueryParamMailboxId, "") // the identifier of the Mailbox to which to restrict the search text, _ := req.getStringParam(QueryParamSearchText, "") // text that must be included in the Email, specifically in From, To, Cc, Bcc, Subject and any text/* body part from, _ := req.getStringParam(QueryParamSearchFrom, "") // text that must be included in the From header of the Email @@ -587,7 +587,7 @@ func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement return true, filter, snippets, offset, limit, logger, nil } -func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { //NOSONAR q := r.URL.Query() since := q.Get(QueryParamSince) if since == "" { @@ -659,7 +659,7 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) { } } -func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Request) { //NOSONAR g.respond(w, r, func(req Request) Response { allAccountIds := req.AllAccountIds() @@ -782,7 +782,7 @@ var sentEmailAutoMailboxRolePrecedence = []string{ var draftAndSentMailboxRoles = structs.Uniq(structs.Concat(draftEmailAutoMailboxRolePrecedence, sentEmailAutoMailboxRolePrecedence)) -func findSentMailboxId(j *jmap.Client, accountId string, req Request, logger *log.Logger) (string, string, Response) { +func findSentMailboxId(j *jmap.Client, accountId string, req Request, logger *log.Logger) (string, string, Response) { //NOSONAR mailboxIdsPerAccountIds, sessionState, _, lang, jerr := j.SearchMailboxIdsPerRole(single(accountId), req.session, req.ctx, logger, req.language(), draftAndSentMailboxRoles) if jerr != nil { return "", "", req.jmapError(accountId, jerr, sessionState, lang) @@ -919,13 +919,13 @@ func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) { } if result == nil { - return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", - "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) + return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Missing Email Update Response", //NOSONAR + "An internal API behaved unexpectedly: missing Email update response from JMAP endpoint"))) //NOSONAR } updatedEmail, ok := result[emailId] if !ok { - return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", - "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) + return req.error(accountId, apiError(req.errorId(), ErrorApiInconsistency, withTitle("API Inconsistency: Wrong Email Update Response ID", //NOSONAR + "An internal API behaved unexpectedly: wrong Email update ID response from JMAP endpoint"))) //NOSONAR } return req.respond(accountId, updatedEmail, sessionState, EmailResponseObjectType, state) @@ -941,7 +941,7 @@ func (e emailKeywordUpdates) IsEmpty() bool { return len(e.Add) == 0 && len(e.Remove) == 0 } -func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) { //NOSONAR g.respond(w, r, func(req Request) Response { l := req.logger.With() @@ -971,10 +971,10 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) patch := jmap.EmailUpdate{} for _, keyword := range body.Add { - patch["keywords/"+keyword] = true + patch["keywords/"+keyword] = true //NOSONAR } for _, keyword := range body.Remove { - patch["keywords/"+keyword] = nil + patch["keywords/"+keyword] = nil //NOSONAR } patches := map[string]jmap.EmailUpdate{ emailId: patch, @@ -1000,7 +1000,7 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) } // Add keywords to an email by its unique identifier. -func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { //NOSONAR g.respond(w, r, func(req Request) Response { l := req.logger.With() @@ -1060,7 +1060,7 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) { } // Remove keywords of an email by its unique identifier. -func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request) { //NOSONAR g.respond(w, r, func(req Request) Response { l := req.logger.With() @@ -1207,7 +1207,7 @@ func (g *Groupware) DeleteEmails(w http.ResponseWriter, r *http.Request) { }) } -func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) { //NOSONAR g.respond(w, r, func(req Request) Response { l := req.logger.With() @@ -1323,7 +1323,7 @@ func relatedEmailsFilter(email jmap.Email, beacon time.Time, days uint) jmap.Ema return filter } -func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) { //NOSONAR g.respond(w, r, func(req Request) Response { l := req.logger.With() @@ -1618,7 +1618,7 @@ type EmailSummaries struct { // // * `seen`: when `true`, emails that have already been seen (read) will be included as well (default is to only include emails that have not been read yet) // * `undesirable`: when `true`, emails that are flagged as spam or phishing will also be summarized (default is to ignore those) -func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter, r *http.Request) { //NOSONAR g.respond(w, r, func(req Request) Response { l := req.logger.With() @@ -1741,7 +1741,7 @@ var sanitizableMediaTypes = []string{ "text/xhtml", } -func (r *Request) sanitizeEmail(source jmap.Email) (jmap.Email, *Error) { +func (r *Request) sanitizeEmail(source jmap.Email) (jmap.Email, *Error) { //NOSONAR if !r.g.config.sanitize { return source, nil } diff --git a/services/groupware/pkg/groupware/api_mailbox.go b/services/groupware/pkg/groupware/api_mailbox.go index c571d9e855..8baa9abbcb 100644 --- a/services/groupware/pkg/groupware/api_mailbox.go +++ b/services/groupware/pkg/groupware/api_mailbox.go @@ -51,7 +51,7 @@ func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) { // It is analogous to a folder or a label in other systems. // // When none of the query parameters are specified, all the mailboxes are returned. -func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { //NOSONAR g.respond(w, r, func(req Request) Response { var filter jmap.MailboxFilterCondition @@ -109,7 +109,7 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) { } // Get the list of all the mailboxes of all accounts of a user, potentially filtering on the role of the mailboxes. -func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Request) { +func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Request) { //NOSONAR g.respond(w, r, func(req Request) Response { accountIds := req.AllAccountIds() if len(accountIds) < 1 { diff --git a/services/groupware/pkg/groupware/dns.go b/services/groupware/pkg/groupware/dns.go index 2ca67df8e4..c05c7b7a66 100644 --- a/services/groupware/pkg/groupware/dns.go +++ b/services/groupware/pkg/groupware/dns.go @@ -74,7 +74,7 @@ func (d DnsSessionUrlResolver) isRedListed(domain string) bool { return !slices.Contains(d.domainRedList, domain) } -func (d DnsSessionUrlResolver) Resolve(ctx context.Context, username string) (*url.URL, *GroupwareError) { +func (d DnsSessionUrlResolver) Resolve(ctx context.Context, username string) (*url.URL, *GroupwareError) { //NOSONAR // heuristic to detect whether the username is an email address parts := strings.Split(username, "@") domain := d.defaultDomain diff --git a/services/groupware/pkg/groupware/framework.go b/services/groupware/pkg/groupware/framework.go index 9b384955a4..1869759098 100644 --- a/services/groupware/pkg/groupware/framework.go +++ b/services/groupware/pkg/groupware/framework.go @@ -172,7 +172,7 @@ func (r groupwareHttpJmapApiClientMetricsRecorder) OnFailedWsHandshakeRequestWit // TODO metrics for WSS } -func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prometheusRegistry prometheus.Registerer) (*Groupware, error) { +func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prometheusRegistry prometheus.Registerer) (*Groupware, error) { //NOSONAR baseUrl, err := url.Parse(config.Mail.BaseUrl) if err != nil { logger.Error().Err(err).Msgf("failed to parse configured Mail.Baseurl '%v'", config.Mail.BaseUrl) @@ -318,7 +318,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome { eventBufferSizeMetric, err := prometheus.NewConstMetric(m.EventBufferSizeDesc, prometheus.GaugeValue, float64(eventChannelSize)) if err != nil { - logger.Warn().Err(err).Msgf("failed to create metric %v", m.EventBufferSizeDesc.String()) + logger.Warn().Err(err).Msgf("failed to create metric %v", m.EventBufferSizeDesc.String()) //NOSONAR } else { if err := prometheusRegistry.Register(metrics.ConstMetricCollector{Metric: eventBufferSizeMetric}); err != nil { logger.Error().Err(err).Msg("failed to register event buffer size metric collector") @@ -567,7 +567,7 @@ func (g *Groupware) serveError(w http.ResponseWriter, r *http.Request, error *Er return } g.log(error) - w.Header().Add("Content-Type", ContentTypeJsonApi) + w.Header().Add("Content-Type", ContentTypeJsonApi) //NOSONAR if !retryAfter.IsZero() { // https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Retry-After // either as an absolute timestamp: @@ -578,7 +578,7 @@ func (g *Groupware) serveError(w http.ResponseWriter, r *http.Request, error *Er render.Status(r, error.NumStatus) w.WriteHeader(error.NumStatus) if err := render.Render(w, r, errorResponses(*error)); err != nil { - g.logger.Error().Err(err).Msgf("failed to render error response") + g.logger.Error().Err(err).Msg("failed to render error response") //NOSONAR } } diff --git a/services/groupware/pkg/groupware/route.go b/services/groupware/pkg/groupware/route.go index f1e2c25117..91a41e71be 100644 --- a/services/groupware/pkg/groupware/route.go +++ b/services/groupware/pkg/groupware/route.go @@ -67,13 +67,13 @@ func (g *Groupware) Route(r chi.Router) { r.Get("/", g.GetAccountsWithTheirIdentities) r.Route("/all", func(r chi.Router) { r.Get("/", g.GetAccounts) - r.Route("/mailboxes", func(r chi.Router) { + r.Route("/mailboxes", func(r chi.Router) { //NOSONAR r.Get("/", g.GetMailboxesForAllAccounts) // ?role= r.Get("/changes", g.GetMailboxChangesForAllAccounts) r.Get("/roles", g.GetMailboxRoles) // ?role= r.Get("/roles/{role}", g.GetMailboxByRoleForAllAccounts) // ?role= }) - r.Route("/emails", func(r chi.Router) { + r.Route("/emails", func(r chi.Router) { //NOSONAR r.Get("/", g.GetEmailsForAllAccounts) r.Get("/latest/summary", g.GetLatestEmailsSummaryForAllAccounts) // ?limit=10&seen=true&undesirable=true }) @@ -141,7 +141,7 @@ func (g *Groupware) Route(r chi.Router) { r.Get("/", g.GetAddressbooks) r.Route("/{addressbookid}", func(r chi.Router) { r.Get("/", g.GetAddressbook) - r.Get("/contacts", g.GetContactsInAddressbook) + r.Get("/contacts", g.GetContactsInAddressbook) //NOSONAR }) }) r.Route("/contacts", func(r chi.Router) { diff --git a/services/groupware/pkg/groupware/session.go b/services/groupware/pkg/groupware/session.go index 2ef0aaa86b..3feac3a763 100644 --- a/services/groupware/pkg/groupware/session.go +++ b/services/groupware/pkg/groupware/session.go @@ -172,7 +172,7 @@ type sessionCacheBuilder struct { sessionFailureCacheTtl time.Duration } -func newSessionCacheBuilder( +func newSessionCacheBuilder( //NOSONAR sessionUrl *url.URL, logger *log.Logger, sessionSupplier func(ctx context.Context, sessionUrl *url.URL, username string, logger *log.Logger) (jmap.Session, jmap.Error), @@ -229,7 +229,7 @@ func (b *sessionCacheBuilder) withDnsAutoDiscovery( return b } -func (b *sessionCacheBuilder) build() (sessionCache, error) { +func (b *sessionCacheBuilder) build() (sessionCache, error) { //NOSONAR var cache *ttlcache.Cache[sessionCacheKey, cachedSession] sessionUrlResolver, err := b.sessionUrlResolverFactory()