docs(groupware): OpenAPI improvements

* refactor some pkg/jmap and groupware methods to make more sense from
   an API point-of-view

 * add path parameter documentation, but automate it by injecting their
   definition into the OpenAPI YAML tree that is extracted from the
   source code using go-swagger as it is too cumbersome, repetitive and
   error-prine to document them in the source code; wrote a TypeScript
   file apidoc-process.ts to do so

 * add generating an offline HTML file for the OpenAPI documentation
   using redocly, and injecting a favicon into the resulting HTML; wrote
   a TypeScript file apidoc-postprocess-html.ts to do so
This commit is contained in:
Pascal Bleser
2025-09-05 14:36:54 +02:00
parent b171609376
commit 3968eedcc5
24 changed files with 2839 additions and 127 deletions

View File

@@ -31,5 +31,5 @@ const (
logDownloadUrl = "download-url"
logBlobId = "blob-id"
logUploadUrl = "download-url"
logSince = "since"
logSinceState = "since-state"
)

View File

@@ -30,6 +30,7 @@ type Emails struct {
State State `json:"state,omitempty"`
}
// Retrieve specific Emails by their id.
func (j *Client) GetEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, ids []string, fetchBodies bool, maxBodyValueBytes uint) (Emails, SessionState, Error) {
logger = j.logger(accountId, "GetEmails", session, logger)
@@ -54,8 +55,9 @@ func (j *Client) GetEmails(accountId string, session *Session, ctx context.Conte
})
}
func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Context, logger *log.Logger, mailboxId string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (Emails, SessionState, Error) {
logger = j.loggerParams(accountId, "GetAllEmails", session, logger, func(z zerolog.Context) zerolog.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, mailboxId string, offset uint, limit uint, fetchBodies bool, maxBodyValueBytes uint) (Emails, SessionState, Error) {
logger = j.loggerParams(accountId, "GetAllEmailsInMailbox", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies).Uint(logOffset, offset).Uint(logLimit, limit)
})
@@ -115,96 +117,15 @@ func (j *Client) GetAllEmails(accountId string, session *Session, ctx context.Co
})
}
type EmailsSince struct {
Destroyed []string `json:"destroyed,omitzero"`
HasMoreChanges bool `json:"hasMoreChanges,omitzero"`
NewState State `json:"newState"`
Created []Email `json:"created,omitempty"`
Updated []Email `json:"updated,omitempty"`
State State `json:"state,omitempty"`
}
func (j *Client) GetEmailsInMailboxSince(accountId string, session *Session, ctx context.Context, logger *log.Logger, mailboxId string, since string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (EmailsSince, SessionState, Error) {
logger = j.loggerParams(accountId, "GetEmailsInMailboxSince", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies).Str(logSince, since)
})
changes := MailboxChangesCommand{
AccountId: accountId,
SinceState: since,
}
if maxChanges > 0 {
changes.MaxChanges = maxChanges
}
getCreated := EmailGetRefCommand{
AccountId: accountId,
FetchAllBodyValues: fetchBodies,
IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: "0"},
}
if maxBodyValueBytes > 0 {
getCreated.MaxBodyValueBytes = maxBodyValueBytes
}
getUpdated := EmailGetRefCommand{
AccountId: accountId,
FetchAllBodyValues: fetchBodies,
IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: "0"},
}
if maxBodyValueBytes > 0 {
getUpdated.MaxBodyValueBytes = maxBodyValueBytes
}
cmd, err := request(
invocation(CommandMailboxChanges, changes, "0"),
invocation(CommandEmailGet, getCreated, "1"),
invocation(CommandEmailGet, getUpdated, "2"),
)
if err != nil {
logger.Error().Err(err)
return EmailsSince{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (EmailsSince, Error) {
var mailboxResponse MailboxChangesResponse
err = retrieveResponseMatchParameters(body, CommandMailboxChanges, "0", &mailboxResponse)
if err != nil {
logger.Error().Err(err)
return EmailsSince{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
var createdResponse EmailGetResponse
err = retrieveResponseMatchParameters(body, CommandEmailGet, "1", &createdResponse)
if err != nil {
logger.Error().Err(err)
return EmailsSince{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
var updatedResponse EmailGetResponse
err = retrieveResponseMatchParameters(body, CommandEmailGet, "2", &updatedResponse)
if err != nil {
logger.Error().Err(err)
return EmailsSince{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
return EmailsSince{
Destroyed: mailboxResponse.Destroyed,
HasMoreChanges: mailboxResponse.HasMoreChanges,
NewState: mailboxResponse.NewState,
Created: createdResponse.List,
Updated: createdResponse.List,
State: createdResponse.State,
}, nil
})
}
func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.Context, logger *log.Logger, since string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (EmailsSince, SessionState, Error) {
// Get all the Emails that have been created, updated or deleted since a given state.
func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.Context, logger *log.Logger, sinceState string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (MailboxChanges, SessionState, Error) {
logger = j.loggerParams(accountId, "GetEmailsSince", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies).Str(logSince, since)
return z.Bool(logFetchBodies, fetchBodies).Str(logSinceState, sinceState)
})
changes := EmailChangesCommand{
AccountId: accountId,
SinceState: since,
SinceState: sinceState,
}
if maxChanges > 0 {
changes.MaxChanges = maxChanges
@@ -233,32 +154,32 @@ func (j *Client) GetEmailsSince(accountId string, session *Session, ctx context.
invocation(CommandEmailGet, getUpdated, "2"),
)
if err != nil {
return EmailsSince{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
return MailboxChanges{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (EmailsSince, Error) {
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (MailboxChanges, Error) {
var changesResponse EmailChangesResponse
err = retrieveResponseMatchParameters(body, CommandEmailChanges, "0", &changesResponse)
if err != nil {
logger.Error().Err(err)
return EmailsSince{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
return MailboxChanges{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
var createdResponse EmailGetResponse
err = retrieveResponseMatchParameters(body, CommandEmailGet, "1", &createdResponse)
if err != nil {
logger.Error().Err(err)
return EmailsSince{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
return MailboxChanges{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
var updatedResponse EmailGetResponse
err = retrieveResponseMatchParameters(body, CommandEmailGet, "2", &updatedResponse)
if err != nil {
logger.Error().Err(err)
return EmailsSince{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
return MailboxChanges{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
return EmailsSince{
return MailboxChanges{
Destroyed: changesResponse.Destroyed,
HasMoreChanges: changesResponse.HasMoreChanges,
NewState: changesResponse.NewState,

View File

@@ -4,6 +4,7 @@ import (
"context"
"github.com/opencloud-eu/opencloud/pkg/log"
"github.com/rs/zerolog"
)
type MailboxesResponse struct {
@@ -85,3 +86,86 @@ func (j *Client) SearchMailboxes(accountId string, session *Session, ctx context
return Mailboxes{Mailboxes: response.List, State: response.State}, nil
})
}
type MailboxChanges struct {
Destroyed []string `json:"destroyed,omitzero"`
HasMoreChanges bool `json:"hasMoreChanges,omitzero"`
NewState State `json:"newState"`
Created []Email `json:"created,omitempty"`
Updated []Email `json:"updated,omitempty"`
State State `json:"state,omitempty"`
}
// Retrieve Email changes in a given Mailbox since a given state.
func (j *Client) GetMailboxChanges(accountId string, session *Session, ctx context.Context, logger *log.Logger, mailboxId string, sinceState string, fetchBodies bool, maxBodyValueBytes uint, maxChanges uint) (MailboxChanges, SessionState, Error) {
logger = j.loggerParams(accountId, "GetMailboxChanges", session, logger, func(z zerolog.Context) zerolog.Context {
return z.Bool(logFetchBodies, fetchBodies).Str(logSinceState, sinceState)
})
changes := MailboxChangesCommand{
AccountId: accountId,
SinceState: sinceState,
}
if maxChanges > 0 {
changes.MaxChanges = maxChanges
}
getCreated := EmailGetRefCommand{
AccountId: accountId,
FetchAllBodyValues: fetchBodies,
IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/created", ResultOf: "0"},
}
if maxBodyValueBytes > 0 {
getCreated.MaxBodyValueBytes = maxBodyValueBytes
}
getUpdated := EmailGetRefCommand{
AccountId: accountId,
FetchAllBodyValues: fetchBodies,
IdRef: &ResultReference{Name: CommandMailboxChanges, Path: "/updated", ResultOf: "0"},
}
if maxBodyValueBytes > 0 {
getUpdated.MaxBodyValueBytes = maxBodyValueBytes
}
cmd, err := request(
invocation(CommandMailboxChanges, changes, "0"),
invocation(CommandEmailGet, getCreated, "1"),
invocation(CommandEmailGet, getUpdated, "2"),
)
if err != nil {
logger.Error().Err(err)
return MailboxChanges{}, "", simpleError(err, JmapErrorInvalidJmapRequestPayload)
}
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, func(body *Response) (MailboxChanges, Error) {
var mailboxResponse MailboxChangesResponse
err = retrieveResponseMatchParameters(body, CommandMailboxChanges, "0", &mailboxResponse)
if err != nil {
logger.Error().Err(err)
return MailboxChanges{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
var createdResponse EmailGetResponse
err = retrieveResponseMatchParameters(body, CommandEmailGet, "1", &createdResponse)
if err != nil {
logger.Error().Err(err)
return MailboxChanges{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
var updatedResponse EmailGetResponse
err = retrieveResponseMatchParameters(body, CommandEmailGet, "2", &updatedResponse)
if err != nil {
logger.Error().Err(err)
return MailboxChanges{}, simpleError(err, JmapErrorInvalidJmapResponsePayload)
}
return MailboxChanges{
Destroyed: mailboxResponse.Destroyed,
HasMoreChanges: mailboxResponse.HasMoreChanges,
NewState: mailboxResponse.NewState,
Created: createdResponse.List,
Updated: createdResponse.List,
State: createdResponse.State,
}, nil
})
}

View File

@@ -449,7 +449,7 @@ func TestWithStalwart(t *testing.T) {
}
{
resp, sessionState, err := j.GetAllEmails(accountId, session, ctx, logger, inboxId, 0, 0, false, 0)
resp, sessionState, err := j.GetAllEmailsInMailbox(accountId, session, ctx, logger, inboxId, 0, 0, false, 0)
require.NoError(err)
require.Equal(session.State, sessionState)

View File

@@ -156,7 +156,7 @@ func TestRequests(t *testing.T) {
require.Len(folders.Mailboxes, 5)
require.NotEmpty(sessionState)
emails, sessionState, err := client.GetAllEmails("a", &session, ctx, &logger, "Inbox", 0, 0, true, 0)
emails, sessionState, err := client.GetAllEmailsInMailbox("a", &session, ctx, &logger, "Inbox", 0, 0, true, 0)
require.NoError(err)
require.Len(emails.Emails, 3)
require.NotEmpty(sessionState)

View File

@@ -1 +1,4 @@
/swagger.yml
/api.html
/api.html.template
/node_modules

View File

@@ -13,12 +13,27 @@ include ../../.make/docs.mk
.PHONY: apidoc
apidoc: swagger.yml
.PHONY: tsnode
tsnode: node_modules
.PHONY: node_modules
node_modules:
pnpm install
.PHONY: swagger.yml
swagger.yml: apidoc.yml
swagger generate spec --output=$@ --include='groupware' --include='jmap' --scan-models --input=$<
swagger.yml: apidoc.yml tsnode
swagger generate spec --include='groupware' --include='jmap' --scan-models --input=$< | NODE_OPTIONS='--no-warnings' pnpm exec ts-node apidoc-process.ts > $@
APIDOC_PORT=9999
.PHONY: serve-apidoc
serve-apidoc: swagger.yml
serve-apidoc: swagger.yml tsnode
swagger serve --no-open --port=$(APIDOC_PORT) --host=127.0.0.1 --flavor=redoc $<
api.html: swagger.yml favicon.png tsnode
pnpm exec redocly build-docs --output=$@.template --title="OpenCloud Groupware API" --theme.openapi.hideHostname=false --theme.openapi.hideTryItPanel=false --theme.openapi.pathInMiddlePanel=true $<
NODE_OPTIONS='--no-warnings' pnpm exec ts-node ./apidoc-postprocess-html.ts favicon.png < $@.template > $@
rm $@.template
.PHONY: apidoc-static
apidoc-static: api.html

View File

@@ -0,0 +1,30 @@
# Groupware
The OpenCloud Groupware service provides a REST API for performing all the backend operations needed by the OpenCloud Groupware frontends.
## OpenAPI Documentation
To generate the OpenAPI ("Swagger") documentation of the REST API, [`pnpm`](https://pnpm.io/) is a pre-requisite.
Run the following command in this directory to generate the `swagger.yml` OpenAPI definition file:
```bash
make apidoc
```
To generate a static HTML file using [Redocly](https://redocly.com/), which will generate a file `api.html`:
```bash
make apidoc-static
```
### Path Parameters
Path parameters are documented in the file [`api-params.yaml`](file:api-params.yaml) and injected into the OpenAPI specification using the script [`apidoc-process.ts`](file:apidoc-process.ts) (which is done automatically when using the `Makefile` as described above.)
### Favicon
A [favicon](https://developer.mozilla.org/en-US/docs/Glossary/Favicon) is inserted into the static (Redocly) HTML file as part of the build process in the `Makefile`, using [`favicon.png`](file:favicon.png) as the source, computing its base64 to insert it as an image using a [data URL](https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Schemes/data) in order to embed it.
That is performed by the script [`apidoc-postprocess-html.ts`](file:apidoc-postprocess-html.ts) (which is done automatically when using then `Makefile` as described above.)

View File

@@ -0,0 +1,5 @@
params:
account:
description: The identifier of the Account to use for this operation
mailbox:
description: The identifier of the Mailbox to perform this operation on

View File

@@ -0,0 +1,25 @@
import * as fs from 'fs'
import * as cheerio from 'cheerio'
const faviconFile = process.argv[2]
const favicon = fs.readFileSync(faviconFile).toString('base64')
let html = ''
process.stdin.on('data', (chunk) => {
html += chunk.toString()
})
process.stdin.on('end', () => {
try {
const $ = cheerio.load(html)
$('head').append(`<link rel="icon" href="data:image/png;base64,${favicon}">`)
process.stdout.write($.html())
process.stdout.write("\n")
} catch (error) {
if (error instanceof Error) {
console.error(`Error occured while post-processing HTML: ${error.message}`)
} else {
console.error("Unknown error occurred")
}
}
});

View File

@@ -0,0 +1,58 @@
import * as fs from 'fs'
import * as yaml from 'js-yaml'
interface Param {
description: string
type: string
}
interface Config {
params: {[param:string]:Param}
}
let inputData = ''
process.stdin.on('data', (chunk) => {
inputData += chunk.toString()
})
process.stdin.on('end', () => {
try {
const config = yaml.load(fs.readFileSync('api-params.yaml', 'utf8')) as Config
const params = config.params || {}
const data = yaml.load(inputData) as any
for (const path in data.paths) {
for (const param in params) {
if (path.includes(`{${param}}`)) {
const paramsData = params[param] as Param
const pathData = data.paths[path] as any
for (const verb in pathData) {
const verbData = pathData[verb] as any
if (!Object.getOwnPropertyNames(verbData).includes('parameters')) {
verbData.parameters = []
}
verbData['parameters'].push({
name: param,
required: true,
type: paramsData.type !== 'undefined' ? paramsData.type : 'string',
in: 'path',
description: paramsData.description,
})
}
}
}
}
process.stdout.write(yaml.dump(data))
process.stdout.write("\n")
} catch (error) {
if (error instanceof Error) {
console.error(`Error occured while post-processing OpenAPI: ${error.message}`)
} else {
console.error("Unknown error occurred")
}
}
});

View File

@@ -1,9 +1,43 @@
servers:
- url: https://localhost:9200/
description: Local Development Server
tags:
- name: init
- name: bootstrap
x-displayName: Bootstrapping
description: Initialization APIs
- name: mailboxes
- name: account
x-displayName: Accounts
description: APIs for accounts
- name: identity
x-displayName: Identities
description: APIs for identities
- name: mailbox
x-displayName: Mailboxes
description: APIs that pertain to mailboxes
- name: messages
- name: message
x-displayName: Messages
description: APIs about emails
- name: vacation
description: APIs about vacation responses
x-displayName: Vacation Responses
description: APIs about vacation responses
x-tagGroups:
- name: Bootstrapping
tags:
- bootstrap
- name: Accounts
tags:
- account
- name: Emails
tags:
- identity
- mailbox
- message
- vacation
components:
securitySchemes:
api:
description: Authentication for API Calls
type: openIdConnect
openIdConnectUrl: https://keycloak.opencloud.test/realms/openCloud/.well-known/openid-configuration
security:
- api

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,15 @@
{
"dependencies": {
"@redocly/cli": "^2.0.8",
"@types/js-yaml": "^4.0.9",
"cheerio": "^1.1.2",
"js-yaml": "^4.1.0",
"ts-node": "^10.9.2",
"typescript": "^5.9.2"
},
"packageManager": "pnpm@10.15.1+sha512.34e538c329b5553014ca8e8f4535997f96180a1d0f614339357449935350d924e22f8614682191264ec33d1462ac21561aff97f6bb18065351c162c7e8f6de67",
"type": "module",
"devDependencies": {
"@types/node": "^24.3.1"
}
}

View File

@@ -8,6 +8,24 @@ import (
"github.com/opencloud-eu/opencloud/pkg/structs"
)
// When the request succeeds.
// swagger:response GetAccountResponse200
type SwaggerGetAccountResponse struct {
// in: body
Body struct {
*jmap.SessionAccount
}
}
// swagger:route GET /groupware/accounts/{account} account account
// Get attributes of a given account.
//
// responses:
//
// 200: GetAccountResponse200
// 400: ErrorResponse400
// 404: ErrorResponse404
// 500: ErrorResponse500
func (g *Groupware) GetAccount(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
account, err := req.GetAccountForMail()
@@ -18,6 +36,22 @@ func (g *Groupware) GetAccount(w http.ResponseWriter, r *http.Request) {
})
}
// When the request succeeds.
// swagger:response GetAccountsResponse200
type SwaggerGetAccountsResponse struct {
// in: body
Body map[string]jmap.SessionAccount
}
// swagger:route GET /groupware/accounts account accounts
// Get the list of all of the user's accounts.
//
// responses:
//
// 200: GetAccountsResponse200
// 400: ErrorResponse400
// 404: ErrorResponse404
// 500: ErrorResponse500
func (g *Groupware) GetAccounts(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
return response(req.session.Accounts, req.session.State)
@@ -47,7 +81,7 @@ type AccountBootstrapResponse struct {
}
// When the request suceeds.
// swagger:response IndexResponse
// swagger:response GetAccountBootstrapResponse200
type SwaggerAccountBootstrapResponse struct {
// in: body
Body struct {
@@ -55,6 +89,15 @@ type SwaggerAccountBootstrapResponse struct {
}
}
// swagger:route GET /groupware/accounts/{account}/bootstrap account accountbootstrap
// Get account bootstrapping.
//
// responses:
//
// 200: GetAccountBootstrapResponse200
// 400: ErrorResponse400
// 404: ErrorResponse404
// 500: ErrorResponse500
func (g *Groupware) GetAccountBootstrap(w http.ResponseWriter, r *http.Request) {
g.respond(w, r, func(req Request) Response {
mailAccountId, err := req.GetAccountIdForMail()

View File

@@ -16,7 +16,7 @@ type SwaggerGetIdentitiesResponse struct {
}
}
// swagger:route GET /accounts/{accountid}/identities identities identities
// swagger:route GET /groupware/accounts/{account}/identities identity identities
// Get the list of identities that are associated with an account.
//
// responses:

View File

@@ -142,7 +142,7 @@ type SwaggerIndexResponse struct {
}
}
// swagger:route GET / init index
// swagger:route GET /groupware bootstrap index
// Get initial bootstrapping information for a user.
//
// responses:

View File

@@ -18,7 +18,7 @@ type SwaggerGetMailboxById200 struct {
}
}
// swagger:route GET /accounts/{account}/mailboxes/{id} mailboxes mailboxes_by_id
// swagger:route GET /groupware/accounts/{account}/mailboxes/{mailbox} mailbox mailboxes_by_id
// Get a specific mailbox by its identifier.
//
// A Mailbox represents a named set of Emails.
@@ -73,7 +73,7 @@ type SwaggerMailboxesResponse200 struct {
Body []jmap.Mailbox
}
// swagger:route GET /accounts/{account}/mailboxes mailboxes mailboxes
// swagger:route GET /groupware/accounts/{account}/mailboxes mailbox mailboxes
// Get the list of all the mailboxes of an account.
//
// A Mailbox represents a named set of Emails.
@@ -134,3 +134,50 @@ func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) {
}
})
}
// When the request succeeds.
// swagger:response MailboxChangesResponse200
type SwaggerMailboxChangesResponse200 struct {
// in: body
Body *jmap.MailboxChanges
}
// swagger:route GET /groupware/accounts/{account}/mailboxes/{mailbox}/changes mailbox mailboxchanges
// Get the changes that occured in a given mailbox since a certain state.
//
// responses:
//
// 200: MailboxChangesResponse200
// 400: ErrorResponse400
// 500: ErrorResponse500
func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
mailboxId := chi.URLParam(r, UriParamMailboxId)
sinceState := r.Header.Get(HeaderSince)
g.respond(w, r, func(req Request) Response {
l := req.logger.With().Str(HeaderSince, sinceState)
maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
if err != nil {
return errorResponse(err)
}
if ok {
l = l.Uint(QueryParamMaxChanges, maxChanges)
}
accountId, err := req.GetAccountIdForMail()
if err != nil {
return errorResponse(err)
}
l = l.Str(logAccountId, accountId)
logger := log.From(l)
changes, sessionState, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, mailboxId, sinceState, true, g.maxBodyValueBytes, maxChanges)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
return etagResponse(changes, sessionState, changes.State)
})
}

View File

@@ -28,11 +28,11 @@ type SwaggerGetAllMessagesInMailbox200 struct {
type SwaggerGetAllMessagesInMailboxSince200 struct {
// in: body
Body struct {
*jmap.EmailsSince
*jmap.MailboxChanges
}
}
// swagger:route GET /accounts/{account}/mailboxes/{id}/messages messages get_all_messages_in_mailbox
// swagger:route GET /groupware/accounts/{account}/mailboxes/{mailbox}/messages message get_all_messages_in_mailbox
// Get all the emails in a mailbox.
//
// Retrieve the list of all the emails that are in a given mailbox.
@@ -72,7 +72,7 @@ func (g *Groupware) GetAllMessagesInMailbox(w http.ResponseWriter, r *http.Reque
logger := log.From(req.logger.With().Str(HeaderSince, since).Str(logAccountId, accountId))
emails, sessionState, jerr := g.jmap.GetEmailsInMailboxSince(accountId, req.session, req.ctx, logger, mailboxId, since, true, g.maxBodyValueBytes, maxChanges)
emails, sessionState, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, mailboxId, since, true, g.maxBodyValueBytes, maxChanges)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}
@@ -109,7 +109,7 @@ func (g *Groupware) GetAllMessagesInMailbox(w http.ResponseWriter, r *http.Reque
logger := log.From(l)
emails, sessionState, jerr := g.jmap.GetAllEmails(accountId, req.session, req.ctx, logger, mailboxId, offset, limit, true, g.maxBodyValueBytes)
emails, sessionState, jerr := g.jmap.GetAllEmailsInMailbox(accountId, req.session, req.ctx, logger, mailboxId, offset, limit, true, g.maxBodyValueBytes)
if jerr != nil {
return req.errorResponseFromJmap(jerr)
}

View File

@@ -16,7 +16,7 @@ type SwaggerGetVacationResponse200 struct {
}
}
// swagger:route GET /accounts/{account}/vacation vacation getvacation
// swagger:route GET /groupware/accounts/{account}/vacation vacation getvacation
// Get vacation notice information.
//
// A vacation response sends an automatic reply when a message is delivered to the mail store, informing the original
@@ -54,7 +54,7 @@ type SwaggerSetVacationResponse200 struct {
}
}
// swagger:route PUT /accounts/{account}/vacation vacation setvacation
// swagger:route PUT /groupware/accounts/{account}/vacation vacation setvacation
// Set the vacation notice information.
//
// A vacation response sends an automatic reply when a message is delivered to the mail store, informing the original

View File

@@ -16,12 +16,6 @@
// Security:
// - bearer
//
// SecurityDefinitions:
// bearer:
// type: http
// scheme: bearer
// bearerFormat: JWT
//
// swagger:meta
package groupware
@@ -47,10 +41,3 @@ type SwaggerErrorResponse500 struct {
*ErrorResponse
}
}
// swagger:parameters vacation mailboxes
type SwaggerAccountParams struct {
// The identifier of the account.
// in: path
Account string `json:"account"`
}

View File

@@ -56,6 +56,7 @@ func (g *Groupware) Route(r chi.Router) {
r.Get("/", g.GetMailboxes) // ?name=&role=&subcribed=
r.Get("/{mailbox}", g.GetMailbox)
r.Get("/{mailbox}/messages", g.GetAllMessagesInMailbox)
r.Get("/{mailbox}/changes", g.GetMailboxChanges)
})
r.Route("/messages", func(r chi.Router) {
r.Get("/", g.GetMessages) // ?fetchemails=true&fetchbodies=true&text=&subject=&body=&keyword=&keyword=&...

2434
services/groupware/pnpm-lock.yaml generated Normal file
View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
{
"ts-node": {
"transpileOnly": true,
"compilerOptions": {
"module": "ESNext",
"esModuleInterop": true
},
"esm": true
}
}