mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-23 13:09:23 -06:00
API documentation changes for groupware-apidocs
* add example generator infrastructure, with some examples for pkg/jmap and pkg/groupware, with more needing to be done * alter the apidoc Makefile to stop using go-swagger but, instead, use the openapi.yml file that must be dropped into that directory using groupware-apidocs (will improve the integration there later) * add Makefile target to generate examples * bump redocly from 2.4.0 to 2.14.5 * introduce Request.PathParam() and .PathParamDoc() to improve API documentation, as well as future-proofing * improve X-Request-ID and Trace-Id header handling in the middleware by logging it safely when an error occurs in the middleware
This commit is contained in:
1
pkg/jmap/.gitignore
vendored
Normal file
1
pkg/jmap/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/apidoc-examples.json
|
||||
@@ -14,7 +14,7 @@ func (j *Client) GetQuotas(accountIds []string, session *Session, ctx context.Co
|
||||
|
||||
invocations := make([]Invocation, len(uniqueAccountIds))
|
||||
for i, accountId := range uniqueAccountIds {
|
||||
invocations[i] = invocation(CommandQuotaGet, MailboxQueryCommand{AccountId: accountId}, mcid(accountId, "0"))
|
||||
invocations[i] = invocation(CommandQuotaGet, QuotaGetCommand{AccountId: accountId}, mcid(accountId, "0"))
|
||||
}
|
||||
cmd, err := j.request(session, logger, invocations...)
|
||||
if err != nil {
|
||||
|
||||
@@ -134,6 +134,7 @@ const (
|
||||
CalendarEventType = ObjectType("CalendarEvent")
|
||||
CalendarEventNotificationType = ObjectType("CalendarEventNotification")
|
||||
ParticipantIdentityType = ObjectType("ParticipantIdentity")
|
||||
FileNodeType = ObjectType("FileNode")
|
||||
|
||||
JmapKeywordPrefix = "$"
|
||||
JmapKeywordSeen = "$seen"
|
||||
@@ -2116,7 +2117,11 @@ const (
|
||||
EmailPropertyHtmlBody = "htmlBody"
|
||||
EmailPropertyAttachments = "attachments"
|
||||
EmailPropertyHasAttachment = "hasAttachment"
|
||||
EmailPropertyHasKeyword = "hasKeyword"
|
||||
EmailPropertyPreview = "preview"
|
||||
|
||||
EmailSortPropertyAllInThreadHaveKeyword = "allInThreadHaveKeyword"
|
||||
EmailSortPropertySomeInThreadHaveKeyword = "someInThreadHaveKeyword"
|
||||
)
|
||||
|
||||
var EmailProperties = []string{
|
||||
@@ -3317,12 +3322,13 @@ type IdentitySetResponse struct {
|
||||
NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"`
|
||||
}
|
||||
|
||||
// An Identity object stores information about an email address or domain the user may send from.
|
||||
type Identity struct {
|
||||
// The id of the Identity.
|
||||
Id string `json:"id,omitempty"`
|
||||
Id string `json:"id,omitempty" doc:"!request"`
|
||||
|
||||
// The “From” name the client SHOULD use when creating a new Email from this Identity.
|
||||
Name string `json:"name,omitempty"`
|
||||
Name string `json:"name,omitempty" doc:"req"`
|
||||
|
||||
// The “From” email address the client MUST use when creating a new Email from this Identity.
|
||||
//
|
||||
@@ -4879,7 +4885,7 @@ type Shareable struct {
|
||||
// It then shows as well the current usage in regard to that limit.
|
||||
type Quota struct {
|
||||
// The unique identifier for this object.
|
||||
Id string `json:"id"`
|
||||
Id string `json:"id" doc:"!request"`
|
||||
|
||||
// The resource type of the quota.
|
||||
ResourceType ResourceType `json:"resourceType"`
|
||||
@@ -4897,9 +4903,9 @@ type Quota struct {
|
||||
// The Scope data type is used to represent the entities the quota applies to.
|
||||
//
|
||||
// It is defined as a "String" with values from the following set:
|
||||
// !- `account`: The quota information applies to just the client's account.
|
||||
// !- `domain`: The quota information applies to all accounts sharing this domain.
|
||||
// !- `global`: The quota information applies to all accounts belonging to the server.
|
||||
// - `account`: The quota information applies to just the client's account.
|
||||
// - `domain`: The quota information applies to all accounts sharing this domain.
|
||||
// - `global`: The quota information applies to all accounts belonging to the server.
|
||||
Scope Scope `json:"scope"`
|
||||
|
||||
// The name of the quota.
|
||||
|
||||
694
pkg/jmap/jmap_model_examples.go
Normal file
694
pkg/jmap/jmap_model_examples.go
Normal file
@@ -0,0 +1,694 @@
|
||||
//go:build groupware_examples
|
||||
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Exampler struct {
|
||||
AccountId string
|
||||
SharedAccountId string
|
||||
IdentityId string
|
||||
IdentityName string
|
||||
EmailAddress string
|
||||
BccName string
|
||||
BccAddress string
|
||||
OtherIdentityId string
|
||||
OtherIdentityName string
|
||||
OtherIdentityEmailAddress string
|
||||
SharedIdentityId string
|
||||
SharedIdentityName string
|
||||
SharedIdentityEmailAddress string
|
||||
ThreadId string
|
||||
EmailId string
|
||||
EmailIds []string
|
||||
QuotaId string
|
||||
SharedQuotaId string
|
||||
Username string
|
||||
SharedAccountName string
|
||||
TextSignature string
|
||||
MailboxInboxId string
|
||||
MailboxDraftsId string
|
||||
MailboxSentId string
|
||||
MailboxJunkId string
|
||||
MailboxDeletedId string
|
||||
MailboxProjectId string
|
||||
SenderEmailAddress string
|
||||
SenderName string
|
||||
}
|
||||
|
||||
var ExamplerInstance = Exampler{
|
||||
AccountId: "b",
|
||||
Username: "cdrummer",
|
||||
IdentityId: "aemua9ai",
|
||||
IdentityName: "Camina Drummer",
|
||||
EmailAddress: "cdrummer@opa.example.com",
|
||||
BccName: "OPA Secretary",
|
||||
BccAddress: "secretary@opa.example.com",
|
||||
OtherIdentityId: "reogh7ia",
|
||||
OtherIdentityName: "Transport Union President",
|
||||
OtherIdentityEmailAddress: "pres@tu.example.com",
|
||||
SharedAccountId: "s",
|
||||
SharedAccountName: "OPA Leadership",
|
||||
SharedIdentityId: "eeyie4qu",
|
||||
SharedIdentityName: "OPA",
|
||||
SharedIdentityEmailAddress: "bosmangs@opa.example.com",
|
||||
ThreadId: "soh0aixi",
|
||||
EmailId: "oot8eev2",
|
||||
EmailIds: []string{"oot8eev2", "phah6ang", "fo7raidi", "kahsha6p"},
|
||||
QuotaId: "iezuu7ah",
|
||||
SharedQuotaId: "voos4oht",
|
||||
TextSignature: strings.Join([]string{"Camina Drummer", "President of the Transport Union"}, "\n"),
|
||||
MailboxInboxId: "a",
|
||||
MailboxDraftsId: "d",
|
||||
MailboxSentId: "e",
|
||||
MailboxJunkId: "c",
|
||||
MailboxDeletedId: "b",
|
||||
MailboxProjectId: "i",
|
||||
SenderEmailAddress: "klaes@opa.example.com",
|
||||
SenderName: "Klaes Ashford",
|
||||
}
|
||||
|
||||
func (e Exampler) SessionMailAccountCapabilities() SessionMailAccountCapabilities {
|
||||
return SessionMailAccountCapabilities{
|
||||
MaxMailboxesPerEmail: 0,
|
||||
MaxMailboxDepth: 10,
|
||||
MaxSizeMailboxName: 255,
|
||||
MaxSizeAttachmentsPerEmail: 50000000,
|
||||
EmailQuerySortOptions: []string{
|
||||
EmailPropertyReceivedAt,
|
||||
EmailPropertySize,
|
||||
EmailPropertyFrom,
|
||||
EmailPropertyTo,
|
||||
EmailPropertySubject,
|
||||
EmailPropertySentAt,
|
||||
EmailPropertyHasAttachment,
|
||||
EmailSortPropertyAllInThreadHaveKeyword,
|
||||
EmailSortPropertySomeInThreadHaveKeyword,
|
||||
},
|
||||
MayCreateTopLevelMailbox: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionSubmissionAccountCapabilities() SessionSubmissionAccountCapabilities {
|
||||
return SessionSubmissionAccountCapabilities{
|
||||
MaxDelayedSend: 2592000,
|
||||
SubmissionExtensions: map[string][]string{
|
||||
"DELIVERYBY": {},
|
||||
"DSN": {},
|
||||
"FUTURERELEASE": {},
|
||||
"MT-PRIORITY": {"MIXER"},
|
||||
"REQUIRETLS": {},
|
||||
"SIZE": {},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionVacationResponseAccountCapabilities() SessionVacationResponseAccountCapabilities {
|
||||
return SessionVacationResponseAccountCapabilities{}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionSieveAccountCapabilities() SessionSieveAccountCapabilities {
|
||||
return SessionSieveAccountCapabilities{
|
||||
MaxSizeScriptName: 512,
|
||||
MaxSizeScript: 1048576,
|
||||
MaxNumberScripts: 100,
|
||||
MaxNumberRedirects: 1,
|
||||
SieveExtensions: []string{
|
||||
"body",
|
||||
"comparator-elbonia",
|
||||
"comparator-i;ascii-casemap",
|
||||
"comparator-i;ascii-numeric",
|
||||
"comparator-i;octet",
|
||||
"convert",
|
||||
"copy",
|
||||
"date",
|
||||
"duplicate",
|
||||
"editheader",
|
||||
"enclose",
|
||||
"encoded-character",
|
||||
"enotify",
|
||||
"envelope",
|
||||
"envelope-deliverby",
|
||||
"envelope-dsn",
|
||||
"environment",
|
||||
"ereject",
|
||||
"extlists",
|
||||
"extracttext",
|
||||
"fcc",
|
||||
"fileinto",
|
||||
"foreverypart",
|
||||
"ihave",
|
||||
"imap4flags",
|
||||
"imapsieve",
|
||||
"include",
|
||||
"index",
|
||||
"mailbox",
|
||||
"mailboxid",
|
||||
"mboxmetadata",
|
||||
"mime",
|
||||
"redirect-deliverby",
|
||||
"redirect-dsn",
|
||||
"regex",
|
||||
"reject",
|
||||
"relational",
|
||||
"replace",
|
||||
"servermetadata",
|
||||
"spamtest",
|
||||
"spamtestplus",
|
||||
"special-use",
|
||||
"subaddress",
|
||||
"vacation",
|
||||
"vacation-seconds",
|
||||
"variables",
|
||||
"virustest",
|
||||
},
|
||||
NotificationMethods: []string{"mailto"},
|
||||
ExternalLists: nil,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionBlobAccountCapabilities() SessionBlobAccountCapabilities {
|
||||
return SessionBlobAccountCapabilities{
|
||||
MaxSizeBlobSet: 7499488,
|
||||
MaxDataSources: 16,
|
||||
SupportedTypeNames: []string{"Email", "Thread", "SieveScript"},
|
||||
SupportedDigestAlgorithms: []HttpDigestAlgorithm{
|
||||
HttpDigestAlgorithmSha,
|
||||
HttpDigestAlgorithmSha256,
|
||||
HttpDigestAlgorithmSha512,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionQuotaAccountCapabilities() SessionQuotaAccountCapabilities {
|
||||
return SessionQuotaAccountCapabilities{}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionContactsAccountCapabilities() SessionContactsAccountCapabilities {
|
||||
return SessionContactsAccountCapabilities{
|
||||
MaxAddressBooksPerCard: 128,
|
||||
MayCreateAddressBook: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionCalendarsAccountCapabilities() SessionCalendarsAccountCapabilities {
|
||||
var maxCals uint = 128
|
||||
var maxParticipants uint = 20
|
||||
minDate := UTCDate("0001-01-01T00:00:00Z")
|
||||
maxDate := UTCDate("65534-12-31T23:59:59Z")
|
||||
maxExp := Duration("P52W1D")
|
||||
create := true
|
||||
return SessionCalendarsAccountCapabilities{
|
||||
MaxCalendarsPerEvent: &maxCals,
|
||||
MinDateTime: &minDate,
|
||||
MaxDateTime: &maxDate,
|
||||
MaxExpandedQueryDuration: maxExp,
|
||||
MaxParticipantsPerEvent: &maxParticipants,
|
||||
MayCreateCalendar: &create,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionCalendarsParseAccountCapabilities() SessionCalendarsParseAccountCapabilities {
|
||||
return SessionCalendarsParseAccountCapabilities{}
|
||||
}
|
||||
|
||||
func (e Exampler) sessionPrincipalsAccountCapabilities(accountId string) SessionPrincipalsAccountCapabilities {
|
||||
return SessionPrincipalsAccountCapabilities{
|
||||
CurrentUserPrincipalId: accountId,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionPrincipalsAccountCapabilities() SessionPrincipalsAccountCapabilities {
|
||||
return e.sessionPrincipalsAccountCapabilities(e.AccountId)
|
||||
}
|
||||
|
||||
func (e Exampler) SessionPrincipalAvailabilityAccountCapabilities() SessionPrincipalAvailabilityAccountCapabilities {
|
||||
return SessionPrincipalAvailabilityAccountCapabilities{
|
||||
MaxAvailabilityDuration: Duration("P52W1D"),
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionTasksAccountCapabilities() SessionTasksAccountCapabilities {
|
||||
return SessionTasksAccountCapabilities{
|
||||
MinDateTime: LocalDate("0001-01-01T00:00:00Z"),
|
||||
MaxDateTime: LocalDate("65534-12-31T23:59:59Z"),
|
||||
MayCreateTaskList: true,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionTasksAlertsAccountCapabilities() SessionTasksAlertsAccountCapabilities {
|
||||
return SessionTasksAlertsAccountCapabilities{}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionTasksAssigneesAccountCapabilities() SessionTasksAssigneesAccountCapabilities {
|
||||
return SessionTasksAssigneesAccountCapabilities{}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionTasksRecurrencesAccountCapabilities() SessionTasksRecurrencesAccountCapabilities {
|
||||
return SessionTasksRecurrencesAccountCapabilities{
|
||||
MaxExpandedQueryDuration: Duration("P260W1D"),
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionTasksMultilingualAccountCapabilities() SessionTasksMultilingualAccountCapabilities {
|
||||
return SessionTasksMultilingualAccountCapabilities{}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionTasksCustomTimezonesAccountCapabilities() SessionTasksCustomTimezonesAccountCapabilities {
|
||||
return SessionTasksCustomTimezonesAccountCapabilities{}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionPrincipalsOwnerAccountCapabilities() SessionPrincipalsOwnerAccountCapabilities {
|
||||
return SessionPrincipalsOwnerAccountCapabilities{
|
||||
AccountIdForPrincipal: e.AccountId,
|
||||
PrincipalId: e.AccountId,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionMDNAccountCapabilities() SessionMDNAccountCapabilities {
|
||||
return SessionMDNAccountCapabilities{}
|
||||
}
|
||||
|
||||
func (e Exampler) SessionAccountCapabilities() SessionAccountCapabilities {
|
||||
return e.sessionAccountCapabilities(e.AccountId)
|
||||
}
|
||||
|
||||
func (e Exampler) sessionAccountCapabilities(accountId string) SessionAccountCapabilities {
|
||||
mail := e.SessionMailAccountCapabilities()
|
||||
submission := e.SessionSubmissionAccountCapabilities()
|
||||
vacationResponse := e.SessionVacationResponseAccountCapabilities()
|
||||
sieve := e.SessionSieveAccountCapabilities()
|
||||
blob := e.SessionBlobAccountCapabilities()
|
||||
quota := e.SessionQuotaAccountCapabilities()
|
||||
contacts := e.SessionContactsAccountCapabilities()
|
||||
calendars := e.SessionCalendarsAccountCapabilities()
|
||||
calendarsParse := e.SessionCalendarsParseAccountCapabilities()
|
||||
principals := e.sessionPrincipalsAccountCapabilities(accountId)
|
||||
principalsAvailability := e.SessionPrincipalAvailabilityAccountCapabilities()
|
||||
tasks := e.SessionTasksAccountCapabilities()
|
||||
tasksAlerts := e.SessionTasksAlertsAccountCapabilities()
|
||||
tasksAssignees := e.SessionTasksAssigneesAccountCapabilities()
|
||||
tasksRecurrences := e.SessionTasksRecurrencesAccountCapabilities()
|
||||
tasksMultilingual := e.SessionTasksMultilingualAccountCapabilities()
|
||||
tasksCustomTimezones := e.SessionTasksCustomTimezonesAccountCapabilities()
|
||||
principalsOwner := e.SessionPrincipalsOwnerAccountCapabilities()
|
||||
mdn := e.SessionMDNAccountCapabilities()
|
||||
return SessionAccountCapabilities{
|
||||
Mail: &mail,
|
||||
Submission: &submission,
|
||||
VacationResponse: &vacationResponse,
|
||||
Sieve: &sieve,
|
||||
Blob: &blob,
|
||||
Quota: "a,
|
||||
Contacts: &contacts,
|
||||
Calendars: &calendars,
|
||||
CalendarsParse: &calendarsParse,
|
||||
Principals: &principals,
|
||||
PrincipalsAvailability: &principalsAvailability,
|
||||
Tasks: &tasks,
|
||||
TasksAlerts: &tasksAlerts,
|
||||
TasksAssignees: &tasksAssignees,
|
||||
TasksRecurrences: &tasksRecurrences,
|
||||
TasksMultilingual: &tasksMultilingual,
|
||||
TasksCustomTimezones: &tasksCustomTimezones,
|
||||
PrincipalsOwner: &principalsOwner,
|
||||
MDN: &mdn,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) Account() (Account, string) {
|
||||
return Account{
|
||||
Name: e.Username,
|
||||
IsPersonal: true,
|
||||
IsReadOnly: false,
|
||||
AccountCapabilities: e.SessionAccountCapabilities(),
|
||||
}, "A personal account"
|
||||
}
|
||||
|
||||
func (e Exampler) SharedAccount() (Account, string, string) {
|
||||
return Account{
|
||||
Name: e.SharedAccountId,
|
||||
IsPersonal: false,
|
||||
IsReadOnly: true,
|
||||
AccountCapabilities: e.sessionAccountCapabilities(e.SharedAccountId),
|
||||
}, "A read-only shared account", "shared"
|
||||
}
|
||||
|
||||
func (e Exampler) Accounts() []Account {
|
||||
a, _ := e.Account()
|
||||
s, _, _ := e.SharedAccount()
|
||||
return []Account{a, s}
|
||||
}
|
||||
|
||||
func (e Exampler) Quota() Quota {
|
||||
return Quota{
|
||||
Id: e.QuotaId,
|
||||
ResourceType: "octets",
|
||||
Scope: "account",
|
||||
Used: 11696865,
|
||||
HardLimit: 20000000000,
|
||||
Name: e.Username,
|
||||
Types: []ObjectType{
|
||||
EmailType,
|
||||
SieveScriptType,
|
||||
FileNodeType,
|
||||
CalendarEventType,
|
||||
ContactCardType,
|
||||
},
|
||||
Description: e.IdentityName,
|
||||
SoftLimit: 19000000000,
|
||||
WarnLimit: 10000000000,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) Quotas() []Quota {
|
||||
return []Quota{
|
||||
e.Quota(),
|
||||
{
|
||||
Id: e.SharedQuotaId,
|
||||
ResourceType: "octets",
|
||||
Scope: "account",
|
||||
Used: 29102918,
|
||||
HardLimit: 50000000000,
|
||||
Name: e.SharedAccountId,
|
||||
Types: []ObjectType{
|
||||
EmailType,
|
||||
SieveScriptType,
|
||||
FileNodeType,
|
||||
CalendarEventType,
|
||||
ContactCardType,
|
||||
},
|
||||
Description: e.SharedAccountName,
|
||||
SoftLimit: 90000000000,
|
||||
WarnLimit: 100000000000,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) Identity() Identity {
|
||||
return Identity{
|
||||
Id: e.IdentityId,
|
||||
Name: e.IdentityName,
|
||||
Email: e.EmailAddress,
|
||||
Bcc: &[]EmailAddress{
|
||||
{Name: e.BccName, Email: e.BccAddress},
|
||||
},
|
||||
MayDelete: true,
|
||||
TextSignature: &e.TextSignature,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) OtherIdentity() (Identity, string, string) {
|
||||
return Identity{
|
||||
Id: e.OtherIdentityId,
|
||||
Name: e.OtherIdentityName,
|
||||
Email: e.OtherIdentityEmailAddress,
|
||||
MayDelete: false,
|
||||
}, "Another Identity", "other"
|
||||
}
|
||||
|
||||
func (e Exampler) Identities() []Identity {
|
||||
a := e.Identity()
|
||||
b, _, _ := e.OtherIdentity()
|
||||
return []Identity{a, b}
|
||||
}
|
||||
|
||||
func (e Exampler) Identity_req() Identity {
|
||||
return Identity{
|
||||
Name: e.IdentityName,
|
||||
Email: e.EmailAddress,
|
||||
Bcc: &[]EmailAddress{
|
||||
{Name: e.BccName, Email: e.BccAddress},
|
||||
},
|
||||
TextSignature: &e.TextSignature,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) Thread() Thread {
|
||||
return Thread{
|
||||
Id: e.ThreadId,
|
||||
EmailIds: e.EmailIds,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) MailboxInbox() (Mailbox, string, string) {
|
||||
return Mailbox{
|
||||
Id: e.MailboxInboxId,
|
||||
Name: "Inbox",
|
||||
Role: JmapMailboxRoleInbox,
|
||||
SortOrder: intPtr(0),
|
||||
TotalEmails: 1291,
|
||||
UnreadEmails: 82,
|
||||
TotalThreads: 891,
|
||||
UnreadThreads: 55,
|
||||
MyRights: &MailboxRights{
|
||||
MayReadItems: true,
|
||||
MayAddItems: true,
|
||||
MayRemoveItems: true,
|
||||
MaySetSeen: true,
|
||||
MaySetKeywords: true,
|
||||
MayCreateChild: true,
|
||||
MayRename: true,
|
||||
MayDelete: true,
|
||||
MaySubmit: true,
|
||||
},
|
||||
IsSubscribed: boolPtr(true),
|
||||
}, "An Inbox Mailbox", "inbox"
|
||||
}
|
||||
|
||||
func (e Exampler) MailboxInboxProjects() (Mailbox, string, string) {
|
||||
return Mailbox{
|
||||
Id: e.MailboxProjectId,
|
||||
ParentId: e.MailboxInboxId,
|
||||
Name: "Projects",
|
||||
SortOrder: intPtr(0),
|
||||
TotalEmails: 112,
|
||||
UnreadEmails: 3,
|
||||
TotalThreads: 85,
|
||||
UnreadThreads: 2,
|
||||
MyRights: &MailboxRights{
|
||||
MayReadItems: true,
|
||||
MayAddItems: true,
|
||||
MayRemoveItems: true,
|
||||
MaySetSeen: true,
|
||||
MaySetKeywords: true,
|
||||
MayCreateChild: true,
|
||||
MayRename: true,
|
||||
MayDelete: true,
|
||||
MaySubmit: true,
|
||||
},
|
||||
IsSubscribed: boolPtr(true),
|
||||
}, "A Projects Mailbox under the Inbox", "projects"
|
||||
}
|
||||
|
||||
func (e Exampler) MailboxDrafts() (Mailbox, string, string) {
|
||||
return Mailbox{
|
||||
Id: e.MailboxDraftsId,
|
||||
Name: "Drafts",
|
||||
Role: JmapMailboxRoleDrafts,
|
||||
SortOrder: intPtr(0),
|
||||
TotalEmails: 12,
|
||||
UnreadEmails: 1,
|
||||
TotalThreads: 12,
|
||||
UnreadThreads: 1,
|
||||
MyRights: &MailboxRights{
|
||||
MayReadItems: true,
|
||||
MayAddItems: true,
|
||||
MayRemoveItems: true,
|
||||
MaySetSeen: true,
|
||||
MaySetKeywords: true,
|
||||
MayCreateChild: true,
|
||||
MayRename: true,
|
||||
MayDelete: true,
|
||||
MaySubmit: true,
|
||||
},
|
||||
IsSubscribed: boolPtr(true),
|
||||
}, "A Drafts Mailbox", "drafts"
|
||||
}
|
||||
|
||||
func (e Exampler) MailboxSent() (Mailbox, string, string) {
|
||||
return Mailbox{
|
||||
Id: e.MailboxSentId,
|
||||
Name: "Sent Items",
|
||||
Role: JmapMailboxRoleSent,
|
||||
SortOrder: intPtr(0),
|
||||
TotalEmails: 1621,
|
||||
UnreadEmails: 0,
|
||||
TotalThreads: 1621,
|
||||
UnreadThreads: 0,
|
||||
MyRights: &MailboxRights{
|
||||
MayReadItems: true,
|
||||
MayAddItems: true,
|
||||
MayRemoveItems: true,
|
||||
MaySetSeen: true,
|
||||
MaySetKeywords: true,
|
||||
MayCreateChild: true,
|
||||
MayRename: true,
|
||||
MayDelete: true,
|
||||
MaySubmit: true,
|
||||
},
|
||||
IsSubscribed: boolPtr(true),
|
||||
}, "A Sent Mailbox", "sent"
|
||||
}
|
||||
|
||||
func (e Exampler) MailboxJunk() (Mailbox, string, string) {
|
||||
return Mailbox{
|
||||
Id: e.MailboxJunkId,
|
||||
Name: "Junk Mail",
|
||||
Role: JmapMailboxRoleJunk,
|
||||
SortOrder: intPtr(0),
|
||||
TotalEmails: 251,
|
||||
UnreadEmails: 0,
|
||||
TotalThreads: 251,
|
||||
UnreadThreads: 0,
|
||||
MyRights: &MailboxRights{
|
||||
MayReadItems: true,
|
||||
MayAddItems: true,
|
||||
MayRemoveItems: true,
|
||||
MaySetSeen: true,
|
||||
MaySetKeywords: true,
|
||||
MayCreateChild: true,
|
||||
MayRename: true,
|
||||
MayDelete: true,
|
||||
MaySubmit: true,
|
||||
},
|
||||
IsSubscribed: boolPtr(true),
|
||||
}, "A Junk Mailbox", "junk"
|
||||
}
|
||||
|
||||
func (e Exampler) MailboxDeleted() (Mailbox, string, string) {
|
||||
return Mailbox{
|
||||
Id: e.MailboxDeletedId,
|
||||
Name: "Deleted Items",
|
||||
Role: JmapMailboxRoleTrash,
|
||||
SortOrder: intPtr(0),
|
||||
TotalEmails: 99,
|
||||
UnreadEmails: 0,
|
||||
TotalThreads: 91,
|
||||
UnreadThreads: 0,
|
||||
MyRights: &MailboxRights{
|
||||
MayReadItems: true,
|
||||
MayAddItems: true,
|
||||
MayRemoveItems: true,
|
||||
MaySetSeen: true,
|
||||
MaySetKeywords: true,
|
||||
MayCreateChild: true,
|
||||
MayRename: true,
|
||||
MayDelete: true,
|
||||
MaySubmit: true,
|
||||
},
|
||||
IsSubscribed: boolPtr(true),
|
||||
}, "A Trash Mailbox", "deleted"
|
||||
}
|
||||
|
||||
func (e Exampler) Mailboxes() []Mailbox {
|
||||
a, _, _ := e.MailboxInbox()
|
||||
b, _, _ := e.MailboxDrafts()
|
||||
c, _, _ := e.MailboxSent()
|
||||
d, _, _ := e.MailboxJunk()
|
||||
f, _, _ := e.MailboxDeleted()
|
||||
g, _, _ := e.MailboxInboxProjects()
|
||||
return []Mailbox{a, b, c, d, f, g}
|
||||
}
|
||||
|
||||
func SerializeExamples(e any) {
|
||||
type example struct {
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key,omitempty"`
|
||||
Title string `json:"title,omitempty"`
|
||||
Scope string `json:"scope,omitempty"`
|
||||
Origin string `json:"origin,omitempty"`
|
||||
Example any `json:"example"`
|
||||
}
|
||||
|
||||
filename := os.Getenv("EXAMPLE_OUTPUT_FILE")
|
||||
if filename == "" {
|
||||
filename = "apidoc-examples.json"
|
||||
}
|
||||
|
||||
funcs := map[string]func() (example, error){}
|
||||
reflected := reflect.ValueOf(e)
|
||||
r := reflect.TypeOf(e)
|
||||
conflicts := map[string]example{}
|
||||
for i := 0; i < r.NumMethod(); i++ {
|
||||
name := r.Method(i).Name
|
||||
m := reflected.MethodByName(name)
|
||||
funcs[name] = func() (example, error) {
|
||||
results := m.Call(nil)
|
||||
title := ""
|
||||
key := "default"
|
||||
typ := ""
|
||||
uniqueId := ""
|
||||
var result reflect.Value
|
||||
switch len(results) {
|
||||
case 1:
|
||||
result = results[0]
|
||||
case 2:
|
||||
result = results[0]
|
||||
title = results[1].String()
|
||||
case 3:
|
||||
result = results[0]
|
||||
title = results[1].String()
|
||||
key = results[2].String()
|
||||
case 4:
|
||||
result = results[0]
|
||||
title = results[1].String()
|
||||
key = results[2].String()
|
||||
typ = results[3].String()
|
||||
default:
|
||||
return example{}, fmt.Errorf("method result does not have 1 or 2 or 3 or 4 results but %d", len(results))
|
||||
}
|
||||
t := result.Type()
|
||||
scope := "" // same as "any"
|
||||
if strings.HasSuffix(name, "_req") {
|
||||
scope = "request"
|
||||
}
|
||||
origin := fmt.Sprintf("%s:%s", r.String(), name)
|
||||
|
||||
if typ == "" {
|
||||
typ = t.String()
|
||||
}
|
||||
|
||||
if uniqueId == "" {
|
||||
uniqueId = typ + "." + key
|
||||
}
|
||||
conflictKey := uniqueId + "/" + scope
|
||||
if conflict, ok := conflicts[conflictKey]; ok {
|
||||
panic(fmt.Errorf("conflicting examples with the same unique identifier '%s', consider adding a key to either of '%s' or '%s'", conflictKey, conflict.Origin, origin))
|
||||
}
|
||||
|
||||
ex := example{
|
||||
Type: typ,
|
||||
Key: uniqueId,
|
||||
Title: title,
|
||||
Scope: scope,
|
||||
Origin: origin,
|
||||
Example: result.Interface(),
|
||||
}
|
||||
conflicts[conflictKey] = ex
|
||||
return ex, nil
|
||||
}
|
||||
}
|
||||
|
||||
examples := []example{}
|
||||
for name, f := range funcs {
|
||||
if ex, err := f(); err != nil {
|
||||
panic(fmt.Errorf("the example producing method '%s' produced an error: %w", name, err))
|
||||
} else {
|
||||
examples = append(examples, ex)
|
||||
}
|
||||
}
|
||||
if b, err := json.MarshalIndent(examples, "", " "); err != nil {
|
||||
panic(fmt.Errorf("failed to serialize to JSON: %w", err))
|
||||
} else {
|
||||
if err := os.WriteFile(filename, b, 0644); err != nil {
|
||||
panic(fmt.Errorf("failed to write the serialized JSON output to the file '%s': %w", filename, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
8
pkg/jmap/jmap_model_examples_test.go
Normal file
8
pkg/jmap/jmap_model_examples_test.go
Normal file
@@ -0,0 +1,8 @@
|
||||
//go:build groupware_examples
|
||||
|
||||
package jmap
|
||||
|
||||
func Example() {
|
||||
SerializeExamples(ExamplerInstance)
|
||||
//Output:
|
||||
}
|
||||
@@ -334,3 +334,11 @@ func mapPairs[K comparable, L, R any](left map[K]L, right map[K]R) map[K]pair[L,
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func intPtr(i int) *int {
|
||||
return &i
|
||||
}
|
||||
|
||||
func boolPtr(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
1
pkg/jscalendar/.gitignore
vendored
Normal file
1
pkg/jscalendar/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/apidoc-examples.json
|
||||
1
pkg/jscontact/.gitignore
vendored
Normal file
1
pkg/jscontact/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/apidoc-examples.json
|
||||
1
services/groupware/.gitignore
vendored
1
services/groupware/.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
/swagger.yml
|
||||
/openapi.yml
|
||||
/api.html
|
||||
/api.html.template
|
||||
/node_modules
|
||||
|
||||
@@ -21,8 +21,10 @@ node_modules:
|
||||
pnpm install
|
||||
|
||||
.PHONY: swagger.yml
|
||||
swagger.yml: apidoc.yml tsnode
|
||||
swagger generate spec --include='groupware' --include='jmap' --include='jscalendar' --include='jscontact' --scan-models --input=$< | NODE_OPTIONS='--no-warnings --loader ts-node/esm' pnpm exec ts-node apidoc-process.ts > $@
|
||||
swagger.yml: apidoc.yml tsnode openapi.yml
|
||||
#swagger generate spec --include='groupware' --include='jmap' --include='jscalendar' --include='jscontact' --scan-models --input=$< | NODE_OPTIONS='--no-warnings --loader ts-node/esm' pnpm exec ts-node apidoc-process.ts > $@
|
||||
#NODE_OPTIONS='--no-warnings --loader ts-node/esm' pnpm exec ts-node apidoc-process.ts > $@ < openapi.yml
|
||||
cp openapi.yml $@
|
||||
|
||||
APIDOC_PORT=9999
|
||||
|
||||
@@ -37,3 +39,10 @@ api.html: swagger.yml favicon.png tsnode
|
||||
|
||||
.PHONY: apidoc-static
|
||||
apidoc-static: api.html
|
||||
|
||||
.PHONY: examples
|
||||
examples:
|
||||
cd ../../pkg/jmap/ && go test -tags=groupware_examples . -v -count=1 -run Example
|
||||
cd ../../pkg/jscontact/ && go test -tags=groupware_examples . -v -count=1 -run Example
|
||||
cd ../../pkg/jscalendar/ && go test -tags=groupware_examples . -v -count=1 -run Example
|
||||
cd ./pkg/groupware/ && go test -tags=groupware_examples . -v -count=1 -run Example
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
openapi: 3.0.4
|
||||
servers:
|
||||
- url: https://localhost:9200/
|
||||
description: Local Development Server
|
||||
tags:
|
||||
- name: untagged
|
||||
x-displayName: Uncategorized
|
||||
description: APIs that are not categorized yet
|
||||
- name: bootstrap
|
||||
x-displayName: Bootstrapping
|
||||
description: Initialization APIs
|
||||
@@ -37,7 +41,14 @@ tags:
|
||||
description: APIs about tasks
|
||||
- name: quota
|
||||
x-displayName: Quota
|
||||
description: APIs about quotas
|
||||
description: |-
|
||||
Quotas are objects that display the limits set to an account usage.
|
||||
|
||||
They also indicate the current usage in regard to those limits.
|
||||
|
||||
JMAP Quotas are defined in [RFC9524](https://www.rfc-editor.org/rfc/rfc9425.html).
|
||||
|
||||
This sectiong roups the APIs that pertain to retrieving and modifying quotas.
|
||||
- name: vacation
|
||||
x-displayName: Vacation Responses
|
||||
description: APIs about vacation responses
|
||||
@@ -69,6 +80,9 @@ x-tagGroups:
|
||||
- name: Quotas
|
||||
tags:
|
||||
- quota
|
||||
- name: Uncategorized
|
||||
tags:
|
||||
- untagged
|
||||
components:
|
||||
securitySchemes:
|
||||
api:
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@redocly/cli": "^2.4.0",
|
||||
"@redocly/cli": "^2.14.5",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"cheerio": "^1.1.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"js-yaml": "^4.1.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.2"
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"packageManager": "pnpm@10.18.1+sha512.77a884a165cbba2d8d1c19e3b4880eee6d2fcabd0d879121e282196b80042351d5eb3ca0935fa599da1dc51265cc68816ad2bddd2a2de5ea9fdf92adbec7cd34",
|
||||
"packageManager": "pnpm@10.28.0+sha512.05df71d1421f21399e053fde567cea34d446fa02c76571441bfc1c7956e98e363088982d940465fd34480d4d90a0668bc12362f8aa88000a64e83d0b0e47be48",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^24.3.1"
|
||||
"@types/node": "^24.10.7"
|
||||
}
|
||||
}
|
||||
|
||||
1
services/groupware/pkg/groupware/.gitignore
vendored
Normal file
1
services/groupware/pkg/groupware/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/apidoc-examples.json
|
||||
@@ -33,7 +33,8 @@ func (g *Groupware) GetAccount(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
return etagResponse(single(accountId), account, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "")
|
||||
var body jmap.Account = account
|
||||
return etagResponse(single(accountId), body, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -66,7 +67,8 @@ func (g *Groupware) GetAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
// sort on accountId to have a stable order that remains the same with every query
|
||||
slices.SortFunc(list, func(a, b AccountWithId) int { return strings.Compare(a.AccountId, b.AccountId) })
|
||||
return etagResponse(structs.Map(list, func(a AccountWithId) string { return a.AccountId }), list, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "")
|
||||
var RBODY []AccountWithId = list
|
||||
return etagResponse(structs.Map(list, func(a AccountWithId) string { return a.AccountId }), RBODY, req.session.State, AccountResponseObjectType, jmap.State(req.session.State), "")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -94,7 +96,8 @@ func (g *Groupware) GetAccountsWithTheirIdentities(w http.ResponseWriter, r *htt
|
||||
}
|
||||
// sort on accountId to have a stable order that remains the same with every query
|
||||
slices.SortFunc(list, func(a, b AccountWithIdAndIdentities) int { return strings.Compare(a.AccountId, b.AccountId) })
|
||||
return etagResponse(structs.Map(list, func(a AccountWithIdAndIdentities) string { return a.AccountId }), list, sessionState, AccountResponseObjectType, state, lang)
|
||||
var RBODY []AccountWithIdAndIdentities = list
|
||||
return etagResponse(structs.Map(list, func(a AccountWithIdAndIdentities) string { return a.AccountId }), RBODY, sessionState, AccountResponseObjectType, state, lang)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -108,34 +111,3 @@ type AccountWithIdAndIdentities struct {
|
||||
jmap.Account
|
||||
Identities []jmap.Identity `json:"identities,omitempty"`
|
||||
}
|
||||
|
||||
type AccountBootstrapResponse struct {
|
||||
// The API version.
|
||||
Version string `json:"version"`
|
||||
|
||||
// A list of capabilities of this API version.
|
||||
Capabilities []string `json:"capabilities"`
|
||||
|
||||
// API limits.
|
||||
Limits IndexLimits `json:"limits"`
|
||||
|
||||
// Accounts that are available to the user.
|
||||
//
|
||||
// The key of the mapis the identifier.
|
||||
Accounts map[string]IndexAccount `json:"accounts"`
|
||||
|
||||
// Primary accounts for usage types.
|
||||
PrimaryAccounts IndexPrimaryAccounts `json:"primaryAccounts"`
|
||||
|
||||
// Mailboxes.
|
||||
Mailboxes map[string][]jmap.Mailbox `json:"mailboxes"`
|
||||
}
|
||||
|
||||
// When the request suceeds.
|
||||
// swagger:response GetAccountBootstrapResponse200
|
||||
type SwaggerAccountBootstrapResponse struct {
|
||||
// in: body
|
||||
Body struct {
|
||||
*AccountBootstrapResponse
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package groupware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
)
|
||||
|
||||
@@ -22,9 +20,9 @@ func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
l := req.logger.With().Str(logAccountId, accountId)
|
||||
|
||||
blobId := chi.URLParam(req.r, UriParamBlobId)
|
||||
if blobId == "" {
|
||||
return req.parameterErrorResponse(single(accountId), UriParamBlobId, fmt.Sprintf("Invalid value for path parameter '%v': empty", UriParamBlobId))
|
||||
blobId, err := req.PathParam(UriParamBlobId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Str(UriParamBlobId, blobId)
|
||||
|
||||
@@ -34,8 +32,7 @@ func (g *Groupware) GetBlobMeta(w http.ResponseWriter, r *http.Request) {
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(single(accountId), jerr)
|
||||
}
|
||||
blob := res
|
||||
if blob == nil {
|
||||
if res == nil {
|
||||
return notFoundResponse(single(accountId), sessionState)
|
||||
}
|
||||
return etagResponse(single(accountId), res, sessionState, BlobResponseObjectType, state, lang)
|
||||
@@ -72,10 +69,15 @@ func (g *Groupware) UploadBlob(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
func (g *Groupware) DownloadBlob(w http.ResponseWriter, r *http.Request) {
|
||||
g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
|
||||
blobId := chi.URLParam(req.r, UriParamBlobId)
|
||||
name := chi.URLParam(req.r, UriParamBlobName)
|
||||
q := req.r.URL.Query()
|
||||
typ := q.Get(QueryParamBlobType)
|
||||
blobId, err := req.PathParam(UriParamBlobId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := req.PathParam(UriParamBlobName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
typ, _ := req.getStringParam(QueryParamBlobType, "")
|
||||
|
||||
accountId, gwerr := req.GetAccountIdForBlob()
|
||||
if gwerr != nil {
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
)
|
||||
@@ -68,7 +67,10 @@ func (g *Groupware) GetCalendarById(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
l := req.logger.With()
|
||||
|
||||
calendarId := chi.URLParam(r, UriParamCalendarId)
|
||||
calendarId, err := req.PathParam(UriParamCalendarId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Str(UriParamCalendarId, log.SafeString(calendarId))
|
||||
|
||||
logger := log.From(l)
|
||||
@@ -110,7 +112,10 @@ func (g *Groupware) GetEventsInCalendar(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
l := req.logger.With()
|
||||
|
||||
calendarId := chi.URLParam(r, UriParamCalendarId)
|
||||
calendarId, err := req.PathParam(UriParamCalendarId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Str(UriParamCalendarId, log.SafeString(calendarId))
|
||||
|
||||
offset, ok, err := req.parseUIntParam(QueryParamOffset, 0)
|
||||
@@ -157,9 +162,6 @@ func (g *Groupware) CreateCalendarEvent(w http.ResponseWriter, r *http.Request)
|
||||
|
||||
l := req.logger.With()
|
||||
|
||||
calendarId := chi.URLParam(r, UriParamCalendarId)
|
||||
l = l.Str(UriParamCalendarId, log.SafeString(calendarId))
|
||||
|
||||
var create jmap.CalendarEvent
|
||||
err := req.body(&create)
|
||||
if err != nil {
|
||||
@@ -175,6 +177,7 @@ func (g *Groupware) CreateCalendarEvent(w http.ResponseWriter, r *http.Request)
|
||||
})
|
||||
}
|
||||
|
||||
// @api:tag XYZ
|
||||
func (g *Groupware) DeleteCalendarEvent(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
ok, accountId, resp := req.needCalendarWithAccount()
|
||||
@@ -183,9 +186,11 @@ func (g *Groupware) DeleteCalendarEvent(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
l := req.logger.With().Str(accountId, log.SafeString(accountId))
|
||||
|
||||
calendarId := chi.URLParam(r, UriParamCalendarId)
|
||||
eventId := chi.URLParam(r, UriParamEventId)
|
||||
l.Str(UriParamCalendarId, log.SafeString(calendarId)).Str(UriParamEventId, log.SafeString(eventId))
|
||||
eventId, err := req.PathParam(UriParamEventId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l.Str(UriParamEventId, log.SafeString(eventId))
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
@@ -220,7 +225,10 @@ func (g *Groupware) ParseIcalBlob(w http.ResponseWriter, r *http.Request) {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
|
||||
blobId := chi.URLParam(r, UriParamBlobId)
|
||||
blobId, err := req.PathParam(UriParamBlobId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
|
||||
blobIds := strings.Split(blobId, ",")
|
||||
l := req.logger.With().Array(UriParamBlobId, log.SafeStringArray(blobIds))
|
||||
|
||||
@@ -3,7 +3,6 @@ package groupware
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
@@ -68,7 +67,10 @@ func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
l := req.logger.With()
|
||||
|
||||
addressBookId := chi.URLParam(r, UriParamAddressBookId)
|
||||
addressBookId, err := req.PathParam(UriParamAddressBookId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
|
||||
|
||||
logger := log.From(l)
|
||||
@@ -110,7 +112,10 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ
|
||||
|
||||
l := req.logger.With()
|
||||
|
||||
addressBookId := chi.URLParam(r, UriParamAddressBookId)
|
||||
addressBookId, err := req.PathParam(UriParamAddressBookId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
|
||||
|
||||
offset, ok, err := req.parseUIntParam(QueryParamOffset, 0)
|
||||
@@ -157,7 +162,10 @@ func (g *Groupware) GetContactById(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
l := req.logger.With()
|
||||
|
||||
contactId := chi.URLParam(r, UriParamContactId)
|
||||
contactId, err := req.PathParam(UriParamContactId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Str(UriParamContactId, log.SafeString(contactId))
|
||||
|
||||
logger := log.From(l)
|
||||
@@ -183,11 +191,14 @@ func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
l := req.logger.With()
|
||||
|
||||
addressBookId := chi.URLParam(r, UriParamAddressBookId)
|
||||
addressBookId, err := req.PathParam(UriParamAddressBookId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
|
||||
|
||||
var create jscontact.ContactCard
|
||||
err := req.body(&create)
|
||||
err = req.bodydoc(&create, "The contact to create, which may not have its id attribute set")
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
@@ -209,7 +220,10 @@ func (g *Groupware) DeleteContact(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
l := req.logger.With().Str(accountId, log.SafeString(accountId))
|
||||
|
||||
contactId := chi.URLParam(r, UriParamContactId)
|
||||
contactId, err := req.PathParam(UriParamContactId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l.Str(UriParamContactId, log.SafeString(contactId))
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
@@ -39,6 +38,54 @@ type SwaggerGetAllEmailsInMailboxSince200 struct {
|
||||
}
|
||||
}
|
||||
|
||||
// swagger:route GET /groupware/accounts/{account}/mailboxes/{mailbox}/emails/since/{since} email get_all_emails_in_mailbox_since
|
||||
// Get all the emails in a mailbox since a given state.
|
||||
//
|
||||
// Retrieve the list of all the emails that are in a given mailbox since a given state.
|
||||
//
|
||||
// The mailbox must be specified by its id, as part of the request URL path.
|
||||
//
|
||||
// A limit and an offset may be specified using the query parameters 'limit' and 'offset',
|
||||
// respectively.
|
||||
//
|
||||
// responses:
|
||||
//
|
||||
// 200: GetAllEmailsInMailboxSince200
|
||||
// 400: ErrorResponse400
|
||||
// 404: ErrorResponse404
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) GetAllEmailsInMailboxSince(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
maxChanges := uint(0)
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
|
||||
mailboxId, err := req.PathParam(UriParamMailboxId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
|
||||
since, err := req.PathParamDoc(UriParamSince, "State identifier that indicates the coordinate from whence on to list mailbox changes")
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
|
||||
logger := log.From(req.logger.With().Str(HeaderParamSince, log.SafeString(since)).Str(logAccountId, log.SafeString(accountId)))
|
||||
|
||||
changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, since, true, g.config.maxBodyValueBytes, maxChanges)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(single(accountId), jerr)
|
||||
}
|
||||
|
||||
return etagResponse(single(accountId), changes, sessionState, EmailResponseObjectType, state, lang)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// swagger:route GET /groupware/accounts/{account}/mailboxes/{mailbox}/emails email get_all_emails_in_mailbox
|
||||
// Get all the emails in a mailbox.
|
||||
//
|
||||
@@ -49,108 +96,79 @@ type SwaggerGetAllEmailsInMailboxSince200 struct {
|
||||
// A limit and an offset may be specified using the query parameters 'limit' and 'offset',
|
||||
// respectively.
|
||||
//
|
||||
// When the query parameter 'since' or the 'if-none-match' header is specified, then the
|
||||
// request behaves differently, performing a changes query to determine what has changed in
|
||||
// that mailbox since a given state identifier.
|
||||
//
|
||||
// responses:
|
||||
//
|
||||
// 200: GetAllEmailsInMailbox200
|
||||
// 200: GetAllEmailsInMailboxSince200
|
||||
// 400: ErrorResponse400
|
||||
// 404: ErrorResponse404
|
||||
// 500: ErrorResponse500
|
||||
// 200: GetAllEmailsInMailbox200
|
||||
// 400: ErrorResponse400
|
||||
// 404: ErrorResponse404
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) GetAllEmailsInMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
mailboxId := chi.URLParam(r, UriParamMailboxId)
|
||||
since := r.Header.Get(HeaderSince)
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
l := req.logger.With()
|
||||
|
||||
if since != "" {
|
||||
// ... then it's a completely different operation
|
||||
maxChanges := uint(0)
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Str(logAccountId, accountId)
|
||||
|
||||
if mailboxId == "" {
|
||||
return req.parameterErrorResponse(single(accountId), UriParamMailboxId, fmt.Sprintf("Missing required mailbox ID path parameter '%v'", UriParamMailboxId))
|
||||
}
|
||||
mailboxId, err := req.PathParam(UriParamMailboxId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
|
||||
logger := log.From(req.logger.With().Str(HeaderSince, log.SafeString(since)).Str(logAccountId, log.SafeString(accountId)))
|
||||
offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Int(QueryParamOffset, offset)
|
||||
}
|
||||
|
||||
changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, since, true, g.config.maxBodyValueBytes, maxChanges)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(single(accountId), jerr)
|
||||
}
|
||||
limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Uint(QueryParamLimit, limit)
|
||||
}
|
||||
|
||||
return etagResponse(single(accountId), changes, sessionState, EmailResponseObjectType, state, lang)
|
||||
})
|
||||
} else {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
l := req.logger.With()
|
||||
logger := log.From(l)
|
||||
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Str(logAccountId, accountId)
|
||||
collapseThreads := false
|
||||
fetchBodies := false
|
||||
withThreads := true
|
||||
|
||||
if mailboxId == "" {
|
||||
return req.parameterErrorResponse(single(accountId), UriParamMailboxId, fmt.Sprintf("Missing required mailbox ID path parameter '%v'", UriParamMailboxId))
|
||||
}
|
||||
emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, req.session, req.ctx, logger, req.language(), mailboxId, offset, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(single(accountId), jerr)
|
||||
}
|
||||
|
||||
offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Int(QueryParamOffset, offset)
|
||||
}
|
||||
sanitized, err := req.sanitizeEmails(emails.Emails)
|
||||
if err != nil {
|
||||
return errorResponseWithSessionState(single(accountId), err, sessionState)
|
||||
}
|
||||
|
||||
limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Uint(QueryParamLimit, limit)
|
||||
}
|
||||
safe := jmap.Emails{
|
||||
Emails: sanitized,
|
||||
Total: emails.Total,
|
||||
Limit: emails.Limit,
|
||||
Offset: emails.Offset,
|
||||
}
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
collapseThreads := false
|
||||
fetchBodies := false
|
||||
withThreads := true
|
||||
|
||||
emails, sessionState, state, lang, jerr := g.jmap.GetAllEmailsInMailbox(accountId, req.session, req.ctx, logger, req.language(), mailboxId, offset, limit, collapseThreads, fetchBodies, g.config.maxBodyValueBytes, withThreads)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(single(accountId), jerr)
|
||||
}
|
||||
|
||||
sanitized, err := req.sanitizeEmails(emails.Emails)
|
||||
if err != nil {
|
||||
return errorResponseWithSessionState(single(accountId), err, sessionState)
|
||||
}
|
||||
|
||||
safe := jmap.Emails{
|
||||
Emails: sanitized,
|
||||
Total: emails.Total,
|
||||
Limit: emails.Limit,
|
||||
Offset: emails.Offset,
|
||||
}
|
||||
|
||||
return etagResponse(single(accountId), safe, sessionState, EmailResponseObjectType, state, lang)
|
||||
})
|
||||
}
|
||||
return etagResponse(single(accountId), safe, sessionState, EmailResponseObjectType, state, lang)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, UriParamEmailId)
|
||||
ids := strings.Split(id, ",")
|
||||
|
||||
accept := r.Header.Get("Accept")
|
||||
if accept == "message/rfc822" {
|
||||
g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
|
||||
id, err := req.PathParam(UriParamEmailId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ids := strings.Split(id, ",")
|
||||
if len(ids) != 1 {
|
||||
return req.parameterError(UriParamEmailId, fmt.Sprintf("when the Accept header is set to '%s', the API only supports serving a single email id", accept))
|
||||
}
|
||||
@@ -194,6 +212,11 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
l := req.logger.With().Str(logAccountId, log.SafeString(accountId))
|
||||
|
||||
id, err := req.PathParam(UriParamEmailId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
ids := strings.Split(id, ",")
|
||||
if len(ids) < 1 {
|
||||
return req.parameterErrorResponse(single(accountId), UriParamEmailId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamEmailId, log.SafeString(id), "empty list of mail ids"))
|
||||
}
|
||||
@@ -244,8 +267,6 @@ func (g *Groupware) GetEmailsById(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, UriParamEmailId)
|
||||
|
||||
contextAppender := func(l zerolog.Context) zerolog.Context { return l }
|
||||
q := r.URL.Query()
|
||||
var attachmentSelector func(jmap.EmailBodyPart) bool = nil
|
||||
@@ -274,6 +295,12 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request)
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l := req.logger.With().Str(logAccountId, log.SafeString(accountId))
|
||||
|
||||
id, err := req.PathParam(UriParamEmailId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
|
||||
logger := log.From(l)
|
||||
emails, _, sessionState, state, lang, jerr := g.jmap.GetEmails(accountId, req.session, req.ctx, logger, req.language(), []string{id}, false, 0, false, false)
|
||||
if jerr != nil {
|
||||
@@ -286,20 +313,29 @@ func (g *Groupware) GetEmailAttachments(w http.ResponseWriter, r *http.Request)
|
||||
if err != nil {
|
||||
return errorResponseWithSessionState(single(accountId), err, sessionState)
|
||||
}
|
||||
return etagResponse(single(accountId), email.Attachments, sessionState, EmailResponseObjectType, state, lang)
|
||||
var body []jmap.EmailBodyPart = email.Attachments
|
||||
return etagResponse(single(accountId), body, sessionState, EmailResponseObjectType, state, lang)
|
||||
})
|
||||
} else {
|
||||
g.stream(w, r, func(req Request, w http.ResponseWriter) *Error {
|
||||
mailAccountId, gwerr := req.GetAccountIdForMail()
|
||||
if gwerr != nil {
|
||||
return gwerr
|
||||
mailAccountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
blobAccountId, gwerr := req.GetAccountIdForBlob()
|
||||
if gwerr != nil {
|
||||
return gwerr
|
||||
blobAccountId, err := req.GetAccountIdForBlob()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l := req.logger.With().Str(logAccountId, log.SafeString(mailAccountId)).Str(logBlobAccountId, log.SafeString(blobAccountId))
|
||||
id, err := req.PathParam(UriParamEmailId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
l := req.logger.With().
|
||||
Str(logAccountId, log.SafeString(mailAccountId)).
|
||||
Str(logBlobAccountId, log.SafeString(blobAccountId)).
|
||||
Str(UriParamEmailId, log.SafeString(id))
|
||||
l = contextAppender(l)
|
||||
logger := log.From(l)
|
||||
|
||||
@@ -434,25 +470,30 @@ type EmailSearchResults struct {
|
||||
QueryState jmap.State `json:"queryState,omitempty"`
|
||||
}
|
||||
|
||||
func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, bool, int, uint, *log.Logger, *Error) {
|
||||
q := req.r.URL.Query()
|
||||
mailboxId := q.Get(QueryParamMailboxId)
|
||||
notInMailboxIds := q[QueryParamNotInMailboxId]
|
||||
text := q.Get(QueryParamSearchText)
|
||||
from := q.Get(QueryParamSearchFrom)
|
||||
to := q.Get(QueryParamSearchTo)
|
||||
cc := q.Get(QueryParamSearchCc)
|
||||
bcc := q.Get(QueryParamSearchBcc)
|
||||
subject := q.Get(QueryParamSearchSubject)
|
||||
body := q.Get(QueryParamSearchBody)
|
||||
keywords := q[QueryParamSearchKeyword]
|
||||
messageId := q.Get(QueryParamSearchMessageId)
|
||||
func (g *Groupware) buildEmailFilter(req Request) (bool, jmap.EmailFilterElement, bool, int, uint, *log.Logger, *Error) {
|
||||
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
|
||||
to, _ := req.getStringParam(QueryParamSearchTo, "") // text that must be included in the To header of the Email
|
||||
cc, _ := req.getStringParam(QueryParamSearchCc, "") // text that must be included in the Cc header of the Email
|
||||
bcc, _ := req.getStringParam(QueryParamSearchBcc, "") // text that must be included in the Bcc header of the Email
|
||||
subject, _ := req.getStringParam(QueryParamSearchSubject, "") // text that must be included in the Subject of the Email
|
||||
body, _ := req.getStringParam(QueryParamSearchBody, "") // text that must be included in any text/* part of the body of the Email
|
||||
messageId, _ := req.getStringParam(QueryParamSearchMessageId, "") // value of the Message-ID header of the Email
|
||||
notInMailboxIds, _, err := req.parseOptStringListParam(QueryParamNotInMailboxId) // a comma-separated list of identifiers of Mailboxes the Email must *not* be in
|
||||
if err != nil {
|
||||
return false, nil, false, 0, 0, nil, err
|
||||
}
|
||||
keywords, _, err := req.parseOptStringListParam(QueryParamSearchKeyword) // the Email must have all those keywords
|
||||
if err != nil {
|
||||
return false, nil, false, 0, 0, nil, err
|
||||
}
|
||||
|
||||
snippets := false
|
||||
|
||||
l := req.logger.With()
|
||||
|
||||
offset, ok, err := req.parseIntParam(QueryParamOffset, 0)
|
||||
offset, ok, err := req.parseIntParam(QueryParamOffset, 0) // pagination element offset
|
||||
if err != nil {
|
||||
return false, nil, snippets, 0, 0, nil, err
|
||||
}
|
||||
@@ -460,7 +501,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
|
||||
l = l.Int(QueryParamOffset, offset)
|
||||
}
|
||||
|
||||
limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit)
|
||||
limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaults.emailLimit) // maximum number of results (size of a page)
|
||||
if err != nil {
|
||||
return false, nil, snippets, 0, 0, nil, err
|
||||
}
|
||||
@@ -468,7 +509,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
|
||||
l = l.Uint(QueryParamLimit, limit)
|
||||
}
|
||||
|
||||
before, ok, err := req.parseDateParam(QueryParamSearchBefore)
|
||||
before, ok, err := req.parseDateParam(QueryParamSearchBefore) // the Email must have been received before this date-time
|
||||
if err != nil {
|
||||
return false, nil, snippets, 0, 0, nil, err
|
||||
}
|
||||
@@ -476,7 +517,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
|
||||
l = l.Time(QueryParamSearchBefore, before)
|
||||
}
|
||||
|
||||
after, ok, err := req.parseDateParam(QueryParamSearchAfter)
|
||||
after, ok, err := req.parseDateParam(QueryParamSearchAfter) // the Email must have been received after this date-time
|
||||
if err != nil {
|
||||
return false, nil, snippets, 0, 0, nil, err
|
||||
}
|
||||
@@ -515,7 +556,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
|
||||
l = l.Str(QueryParamSearchMessageId, log.SafeString(messageId))
|
||||
}
|
||||
|
||||
minSize, ok, err := req.parseIntParam(QueryParamSearchMinSize, 0)
|
||||
minSize, ok, err := req.parseIntParam(QueryParamSearchMinSize, 0) // the minimum size of the Email
|
||||
if err != nil {
|
||||
return false, nil, snippets, 0, 0, nil, err
|
||||
}
|
||||
@@ -523,7 +564,7 @@ func (g *Groupware) buildFilter(req Request) (bool, jmap.EmailFilterElement, boo
|
||||
l = l.Int(QueryParamSearchMinSize, minSize)
|
||||
}
|
||||
|
||||
maxSize, ok, err := req.parseIntParam(QueryParamSearchMaxSize, 0)
|
||||
maxSize, ok, err := req.parseIntParam(QueryParamSearchMaxSize, 0) // the maximum size of the Email
|
||||
if err != nil {
|
||||
return false, nil, snippets, 0, 0, nil, err
|
||||
}
|
||||
@@ -586,7 +627,7 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
since := q.Get(QueryParamSince)
|
||||
if since == "" {
|
||||
since = r.Header.Get(HeaderSince)
|
||||
since = r.Header.Get(HeaderParamSince)
|
||||
}
|
||||
if since != "" {
|
||||
// get email changes since a given state
|
||||
@@ -599,7 +640,7 @@ func (g *Groupware) GetEmails(w http.ResponseWriter, r *http.Request) {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
|
||||
ok, filter, makesSnippets, offset, limit, logger, err := g.buildFilter(req)
|
||||
ok, filter, makesSnippets, offset, limit, logger, err := g.buildEmailFilter(req)
|
||||
if !ok {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
@@ -658,7 +699,7 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
allAccountIds := req.AllAccountIds()
|
||||
|
||||
ok, filter, makesSnippets, offset, limit, logger, err := g.buildFilter(req)
|
||||
ok, filter, makesSnippets, offset, limit, logger, err := g.buildEmailFilter(req)
|
||||
if !ok {
|
||||
return errorResponse(allAccountIds, err)
|
||||
}
|
||||
@@ -698,12 +739,14 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
// TODO offset and limit over the aggregated results by account
|
||||
|
||||
return etagResponse(allAccountIds, EmailSearchSnippetsResults{
|
||||
body := EmailSearchSnippetsResults{
|
||||
Results: flattened,
|
||||
Total: totalOverAllAccounts,
|
||||
Limit: limit,
|
||||
QueryState: state,
|
||||
}, sessionState, EmailResponseObjectType, state, lang)
|
||||
}
|
||||
|
||||
return etagResponse(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang)
|
||||
} else {
|
||||
withThreads := true
|
||||
|
||||
@@ -735,12 +778,14 @@ func (g *Groupware) GetEmailsForAllAccounts(w http.ResponseWriter, r *http.Reque
|
||||
|
||||
// TODO offset and limit over the aggregated results by account
|
||||
|
||||
return etagResponse(allAccountIds, EmailSearchResults{
|
||||
body := EmailSearchResults{
|
||||
Results: flattened,
|
||||
Total: totalAcrossAllAccounts,
|
||||
Limit: limit,
|
||||
QueryState: state,
|
||||
}, sessionState, EmailResponseObjectType, state, lang)
|
||||
}
|
||||
|
||||
return etagResponse(allAccountIds, body, sessionState, EmailResponseObjectType, state, lang)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -845,12 +890,15 @@ func (g *Groupware) ReplaceEmail(w http.ResponseWriter, r *http.Request) {
|
||||
return errorResponse(single(accountId), gwerr)
|
||||
}
|
||||
|
||||
replaceId := chi.URLParam(r, UriParamEmailId)
|
||||
replaceId, err := req.PathParam(UriParamEmailId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
|
||||
logger = log.From(logger.With().Str(logAccountId, log.SafeString(accountId)))
|
||||
|
||||
var body jmap.EmailCreate
|
||||
err := req.body(&body)
|
||||
err = req.body(&body)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
@@ -883,10 +931,7 @@ type SwaggerUpdateEmailBody struct {
|
||||
|
||||
func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
emailId := chi.URLParam(r, UriParamEmailId)
|
||||
|
||||
l := req.logger.With()
|
||||
l.Str(UriParamEmailId, log.SafeString(emailId))
|
||||
|
||||
accountId, gwerr := req.GetAccountIdForMail()
|
||||
if gwerr != nil {
|
||||
@@ -894,10 +939,16 @@ func (g *Groupware) UpdateEmail(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
l.Str(logAccountId, accountId)
|
||||
|
||||
emailId, err := req.PathParam(UriParamEmailId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l.Str(UriParamEmailId, log.SafeString(emailId))
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
var body map[string]any
|
||||
err := req.body(&body)
|
||||
err = req.body(&body)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
@@ -936,10 +987,7 @@ func (e emailKeywordUpdates) IsEmpty() bool {
|
||||
|
||||
func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
emailId := chi.URLParam(r, UriParamEmailId)
|
||||
|
||||
l := req.logger.With()
|
||||
l.Str(UriParamEmailId, log.SafeString(emailId))
|
||||
|
||||
accountId, gwerr := req.GetAccountIdForMail()
|
||||
if gwerr != nil {
|
||||
@@ -947,10 +995,16 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
l.Str(logAccountId, accountId)
|
||||
|
||||
emailId, err := req.PathParam(UriParamEmailId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l.Str(UriParamEmailId, log.SafeString(emailId))
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
var body emailKeywordUpdates
|
||||
err := req.body(&body)
|
||||
err = req.body(&body)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
@@ -1000,10 +1054,7 @@ func (g *Groupware) UpdateEmailKeywords(w http.ResponseWriter, r *http.Request)
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
emailId := chi.URLParam(r, UriParamEmailId)
|
||||
|
||||
l := req.logger.With()
|
||||
l.Str(UriParamEmailId, log.SafeString(emailId))
|
||||
|
||||
accountId, gwerr := req.GetAccountIdForMail()
|
||||
if gwerr != nil {
|
||||
@@ -1011,10 +1062,16 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
l.Str(logAccountId, accountId)
|
||||
|
||||
emailId, err := req.PathParam(UriParamEmailId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l.Str(UriParamEmailId, log.SafeString(emailId))
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
var body []string
|
||||
err := req.body(&body)
|
||||
err = req.body(&body)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
@@ -1065,21 +1122,24 @@ func (g *Groupware) AddEmailKeywords(w http.ResponseWriter, r *http.Request) {
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
emailId := chi.URLParam(r, UriParamEmailId)
|
||||
|
||||
l := req.logger.With()
|
||||
l.Str(UriParamEmailId, log.SafeString(emailId))
|
||||
|
||||
accountId, gwerr := req.GetAccountIdForMail()
|
||||
if gwerr != nil {
|
||||
return errorResponse(single(accountId), gwerr)
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l.Str(logAccountId, accountId)
|
||||
|
||||
emailId, err := req.PathParam(UriParamEmailId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l.Str(UriParamEmailId, log.SafeString(emailId))
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
var body []string
|
||||
err := req.body(&body)
|
||||
err = req.body(&body)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
@@ -1130,16 +1190,19 @@ func (g *Groupware) RemoveEmailKeywords(w http.ResponseWriter, r *http.Request)
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) DeleteEmail(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
emailId := chi.URLParam(r, UriParamEmailId)
|
||||
|
||||
l := req.logger.With()
|
||||
l.Str(UriParamEmailId, emailId)
|
||||
|
||||
accountId, gwerr := req.GetAccountIdForMail()
|
||||
if gwerr != nil {
|
||||
return errorResponse(single(accountId), gwerr)
|
||||
}
|
||||
l.Str(logAccountId, accountId)
|
||||
l.Str(logAccountId, log.SafeString(accountId))
|
||||
|
||||
emailId, err := req.PathParam(UriParamEmailId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l.Str(UriParamEmailId, log.SafeString(emailId))
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
@@ -1188,6 +1251,7 @@ type SwaggerDeleteEmailsBody struct {
|
||||
// 404: ErrorResponse404
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) DeleteEmails(w http.ResponseWriter, r *http.Request) {
|
||||
/// @api body
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
l := req.logger.With()
|
||||
|
||||
@@ -1237,7 +1301,10 @@ func (g *Groupware) SendEmail(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
l.Str(logAccountId, accountId)
|
||||
|
||||
emailId := chi.URLParam(r, UriParamEmailId)
|
||||
emailId, err := req.PathParam(UriParamEmailId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l.Str(UriParamEmailId, log.SafeString(emailId))
|
||||
|
||||
identityId, err := req.getMandatoryStringParam(QueryParamIdentityId)
|
||||
@@ -1333,10 +1400,8 @@ func relatedEmailsFilter(email jmap.Email, beacon time.Time, days uint) jmap.Ema
|
||||
}
|
||||
|
||||
func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
|
||||
id := chi.URLParam(r, UriParamEmailId)
|
||||
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
l := req.logger.With().Str(logEmailId, log.SafeString(id))
|
||||
l := req.logger.With()
|
||||
|
||||
accountId, gwerr := req.GetAccountIdForMail()
|
||||
if gwerr != nil {
|
||||
@@ -1344,6 +1409,12 @@ func (g *Groupware) RelatedToEmail(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
l = l.Str(logAccountId, log.SafeString(accountId))
|
||||
|
||||
id, err := req.PathParam(UriParamEmailId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Str(logEmailId, log.SafeString(id))
|
||||
|
||||
limit, ok, err := req.parseUIntParam(QueryParamLimit, 10) // TODO configurable default limit
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
@@ -1715,7 +1786,7 @@ func (g *Groupware) GetLatestEmailsSummaryForAllAccounts(w http.ResponseWriter,
|
||||
return errorResponse(allAccountIds, err)
|
||||
}
|
||||
if offset > 0 {
|
||||
return notImplementesResponse()
|
||||
return notImplementedResponse()
|
||||
}
|
||||
if ok {
|
||||
l = l.Uint(QueryParamOffset, limit)
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
@@ -48,7 +47,10 @@ func (g *Groupware) GetIdentityById(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
id := chi.URLParam(r, UriParamIdentityId)
|
||||
id, err := req.PathParam(UriParamIdentityId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
logger := log.From(req.logger.With().Str(logAccountId, accountId).Str(logIdentityId, id))
|
||||
res, sessionState, state, lang, jerr := g.jmap.GetIdentities(accountId, req.session, req.ctx, logger, req.language(), []string{id})
|
||||
if jerr != nil {
|
||||
@@ -57,7 +59,8 @@ func (g *Groupware) GetIdentityById(w http.ResponseWriter, r *http.Request) {
|
||||
if len(res) < 1 {
|
||||
return notFoundResponse(single(accountId), sessionState)
|
||||
}
|
||||
return etagResponse(single(accountId), res[0], sessionState, IdentityResponseObjectType, state, lang)
|
||||
var body jmap.Identity = res[0]
|
||||
return etagResponse(single(accountId), body, sessionState, IdentityResponseObjectType, state, lang)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -105,6 +108,7 @@ func (g *Groupware) ModifyIdentity(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// Delete an identity.
|
||||
func (g *Groupware) DeleteIdentity(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
@@ -113,10 +117,13 @@ func (g *Groupware) DeleteIdentity(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
logger := log.From(req.logger.With().Str(logAccountId, accountId))
|
||||
|
||||
id := chi.URLParam(r, UriParamIdentityId)
|
||||
id, err := req.PathParam(UriParamIdentityId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
ids := strings.Split(id, ",")
|
||||
if len(ids) < 1 {
|
||||
return req.parameterErrorResponse(single(accountId), UriParamEmailId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamIdentityId, log.SafeString(id), "empty list of identity ids"))
|
||||
return req.parameterErrorResponse(single(accountId), UriParamIdentityId, fmt.Sprintf("Invalid value for path parameter '%v': '%s': %s", UriParamIdentityId, log.SafeString(id), "empty list of identity ids"))
|
||||
}
|
||||
|
||||
deletion, sessionState, state, _, jerr := g.jmap.DeleteIdentity(accountId, req.session, req.ctx, logger, req.language(), ids)
|
||||
|
||||
@@ -153,6 +153,7 @@ type SwaggerIndexResponse struct {
|
||||
|
||||
// swagger:route GET /groupware bootstrap index
|
||||
// Get initial bootstrapping information for a user.
|
||||
// @api:tag bootstrap
|
||||
//
|
||||
// responses:
|
||||
//
|
||||
@@ -166,13 +167,14 @@ func (g *Groupware) Index(w http.ResponseWriter, r *http.Request) {
|
||||
return req.errorResponseFromJmap(accountIds, err)
|
||||
}
|
||||
|
||||
return etagResponse(accountIds, IndexResponse{
|
||||
var RBODY IndexResponse = IndexResponse{
|
||||
Version: Version,
|
||||
Capabilities: Capabilities,
|
||||
Limits: buildIndexLimits(req.session),
|
||||
Accounts: buildIndexAccounts(req.session, boot),
|
||||
PrimaryAccounts: buildIndexPrimaryAccounts(req.session),
|
||||
}, sessionState, IndexResponseObjectType, state, lang)
|
||||
}
|
||||
return etagResponse(accountIds, RBODY, sessionState, IndexResponseObjectType, state, lang)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
@@ -35,13 +34,17 @@ type SwaggerGetMailboxById200 struct {
|
||||
// 404: ErrorResponse404
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) GetMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
mailboxId := chi.URLParam(r, UriParamMailboxId)
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
|
||||
mailboxId, err := req.PathParam(UriParamMailboxId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
|
||||
mailboxes, sessionState, state, lang, jerr := g.jmap.GetMailbox(accountId, req.session, req.ctx, req.logger, req.language(), []string{mailboxId})
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(single(accountId), jerr)
|
||||
@@ -92,22 +95,21 @@ type SwaggerMailboxesResponse200 struct {
|
||||
// 400: ErrorResponse400
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) GetMailboxes(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
var filter jmap.MailboxFilterCondition
|
||||
|
||||
hasCriteria := false
|
||||
name := q.Get(QueryParamMailboxSearchName)
|
||||
if name != "" {
|
||||
filter.Name = name
|
||||
hasCriteria = true
|
||||
}
|
||||
role := q.Get(QueryParamMailboxSearchRole)
|
||||
if role != "" {
|
||||
filter.Role = role
|
||||
hasCriteria = true
|
||||
}
|
||||
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
var filter jmap.MailboxFilterCondition
|
||||
|
||||
hasCriteria := false
|
||||
name, ok := req.getStringParam(QueryParamMailboxSearchName, "") // the mailbox name to filter on
|
||||
if ok && name != "" {
|
||||
filter.Name = name
|
||||
hasCriteria = true
|
||||
}
|
||||
role, ok := req.getStringParam(QueryParamMailboxSearchRole, "") // the mailbox role to filter on
|
||||
if role != "" {
|
||||
filter.Role = role
|
||||
hasCriteria = true
|
||||
}
|
||||
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
@@ -157,8 +159,7 @@ type SwaggerMailboxesForAllAccountsResponse200 struct {
|
||||
}
|
||||
|
||||
// swagger:route GET /groupware/accounts/all/mailboxes mailboxesforallaccounts mailbox
|
||||
// Get the list of all the mailboxes of all accounts of a user, potentially filtering on the
|
||||
// role of the mailboxes.
|
||||
// Get the list of all the mailboxes of all accounts of a user, potentially filtering on the role of the mailboxes.
|
||||
//
|
||||
// responses:
|
||||
//
|
||||
@@ -166,28 +167,24 @@ type SwaggerMailboxesForAllAccountsResponse200 struct {
|
||||
// 400: ErrorResponse400
|
||||
// 500: ErrorResponse500
|
||||
func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
var filter jmap.MailboxFilterCondition
|
||||
|
||||
hasCriteria := false
|
||||
role := q.Get(QueryParamMailboxSearchRole)
|
||||
if role != "" {
|
||||
filter.Role = role
|
||||
hasCriteria = true
|
||||
}
|
||||
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
accountIds := req.AllAccountIds()
|
||||
if len(accountIds) < 1 {
|
||||
return noContentResponse(nil, "")
|
||||
return noContentResponse(nil, "") // when the user has no accounts
|
||||
}
|
||||
logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)))
|
||||
|
||||
subscribed, set, err := req.parseBoolParam(QueryParamMailboxSearchSubscribed, false)
|
||||
if err != nil {
|
||||
return errorResponse(accountIds, err)
|
||||
var filter jmap.MailboxFilterCondition
|
||||
hasCriteria := false
|
||||
|
||||
if role, set := req.getStringParam(QueryParamMailboxSearchRole, ""); set {
|
||||
filter.Role = role
|
||||
hasCriteria = true
|
||||
}
|
||||
if set {
|
||||
|
||||
if subscribed, set, err := req.parseBoolParam(QueryParamMailboxSearchSubscribed, false); err != nil {
|
||||
return errorResponse(accountIds, err)
|
||||
} else if set {
|
||||
filter.IsSubscribed = &subscribed
|
||||
hasCriteria = true
|
||||
}
|
||||
@@ -208,22 +205,28 @@ func (g *Groupware) GetMailboxesForAllAccounts(w http.ResponseWriter, r *http.Re
|
||||
})
|
||||
}
|
||||
|
||||
// Retrieve Mailboxes by their role for all accounts.
|
||||
func (g *Groupware) GetMailboxByRoleForAllAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
role := chi.URLParam(r, UriParamRole)
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
accountIds := req.AllAccountIds()
|
||||
if len(accountIds) < 1 {
|
||||
return noContentResponse(nil, "")
|
||||
return noContentResponse(nil, "") // when the user has no accounts
|
||||
}
|
||||
|
||||
role, err := req.PathParamDoc(UriParamRole, "Role of the mailboxes to retrieve across all accounts")
|
||||
if err != nil {
|
||||
return errorResponse(accountIds, err)
|
||||
}
|
||||
|
||||
logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)).Str("role", role))
|
||||
|
||||
filter := jmap.MailboxFilterCondition{
|
||||
Role: role,
|
||||
}
|
||||
|
||||
mailboxesByAccountId, sessionState, state, lang, err := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter)
|
||||
if err != nil {
|
||||
return req.errorResponseFromJmap(accountIds, err)
|
||||
mailboxesByAccountId, sessionState, state, lang, jerr := g.jmap.SearchMailboxes(accountIds, req.session, req.ctx, logger, req.language(), filter)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(accountIds, jerr)
|
||||
}
|
||||
return etagResponse(accountIds, sortMailboxesMap(mailboxesByAccountId), sessionState, MailboxResponseObjectType, state, lang)
|
||||
})
|
||||
@@ -245,11 +248,8 @@ type SwaggerMailboxChangesResponse200 struct {
|
||||
// 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)
|
||||
l := req.logger.With()
|
||||
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
@@ -257,6 +257,11 @@ func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
l = l.Str(logAccountId, accountId)
|
||||
|
||||
mailboxId, err := req.PathParam(UriParamMailboxId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
|
||||
maxChanges, ok, err := req.parseUIntParam(QueryParamMaxChanges, 0)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
@@ -265,6 +270,12 @@ func (g *Groupware) GetMailboxChanges(w http.ResponseWriter, r *http.Request) {
|
||||
l = l.Uint(QueryParamMaxChanges, maxChanges)
|
||||
}
|
||||
|
||||
sinceState, err := req.HeaderParamDoc(HeaderParamSince, "Specifies the state identifier from which on to list mailbox changes")
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Str(HeaderParamSince, log.SafeString(sinceState))
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
changes, sessionState, state, lang, jerr := g.jmap.GetMailboxChanges(accountId, req.session, req.ctx, logger, req.language(), mailboxId, sinceState, true, g.config.maxBodyValueBytes, maxChanges)
|
||||
@@ -329,6 +340,8 @@ func (g *Groupware) GetMailboxChangesForAllAccounts(w http.ResponseWriter, r *ht
|
||||
})
|
||||
}
|
||||
|
||||
// Retrieve the roles of all the Mailboxes of all Accounts.
|
||||
// @api:example mailboxrolesbyaccount
|
||||
func (g *Groupware) GetMailboxRoles(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
l := req.logger.With()
|
||||
@@ -346,10 +359,8 @@ func (g *Groupware) GetMailboxRoles(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (g *Groupware) UpdateMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
mailboxId := chi.URLParam(r, UriParamMailboxId)
|
||||
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
l := req.logger.With().Str(UriParamMailboxId, log.SafeString(mailboxId))
|
||||
l := req.logger.With()
|
||||
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
if err != nil {
|
||||
@@ -357,6 +368,12 @@ func (g *Groupware) UpdateMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
l = l.Str(logAccountId, accountId)
|
||||
|
||||
mailboxId, err := req.PathParamDoc(UriParamMailboxId, "the identifier of the mailbox to update")
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Str(UriParamMailboxId, log.SafeString(mailboxId))
|
||||
|
||||
var body jmap.MailboxChange
|
||||
err = req.body(&body)
|
||||
if err != nil {
|
||||
@@ -398,10 +415,12 @@ func (g *Groupware) CreateMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
})
|
||||
}
|
||||
|
||||
// Delete Mailboxes by their unique identifiers.
|
||||
//
|
||||
// Returns the identifiers of the Mailboxes that have successfully been deleted.
|
||||
//
|
||||
// @api:example deletedmailboxes
|
||||
func (g *Groupware) DeleteMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
mailboxId := chi.URLParam(r, UriParamMailboxId)
|
||||
mailboxIds := strings.Split(mailboxId, ",")
|
||||
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
l := req.logger.With()
|
||||
accountId, err := req.GetAccountIdForMail()
|
||||
@@ -410,11 +429,16 @@ func (g *Groupware) DeleteMailbox(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
l = l.Str(logAccountId, accountId)
|
||||
|
||||
mailboxIds, err := req.PathListParamDoc(UriParamMailboxId, "the identifier of the mailbox to delete")
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
l = l.Array(UriParamMailboxId, log.SafeStringArray(mailboxIds))
|
||||
|
||||
if len(mailboxIds) < 1 {
|
||||
return noContentResponse(single(accountId), req.session.State)
|
||||
return noContentResponse(single(accountId), req.session.State) // no mailbox identifiers were mentioned in the request
|
||||
}
|
||||
|
||||
l = l.Array(UriParamMailboxId, log.SafeStringArray(mailboxIds))
|
||||
logger := log.From(l)
|
||||
|
||||
deleted, sessionState, state, lang, jerr := g.jmap.DeleteMailboxes(accountId, req.session, req.ctx, logger, req.language(), "", mailboxIds)
|
||||
@@ -441,8 +465,8 @@ func scoreMailbox(m jmap.Mailbox) int {
|
||||
return 1000
|
||||
}
|
||||
|
||||
func sortMailboxesMap[K comparable](mailboxesByAccountId map[K][]jmap.Mailbox) map[K][]jmap.Mailbox {
|
||||
sortedByAccountId := make(map[K][]jmap.Mailbox, len(mailboxesByAccountId))
|
||||
func sortMailboxesMap(mailboxesByAccountId map[string][]jmap.Mailbox) map[string][]jmap.Mailbox {
|
||||
sortedByAccountId := make(map[string][]jmap.Mailbox, len(mailboxesByAccountId))
|
||||
for accountId, unsorted := range mailboxesByAccountId {
|
||||
mailboxes := make([]jmap.Mailbox, len(unsorted))
|
||||
copy(mailboxes, unsorted)
|
||||
|
||||
@@ -17,6 +17,10 @@ type SwaggerGetQuotaResponse200 struct {
|
||||
// swagger:route GET /groupware/accounts/{account}/quota quota get_quota
|
||||
// Get quota limits.
|
||||
//
|
||||
// Retrieves the list of Quota configurations for a given account.
|
||||
//
|
||||
// Note that there may be multiple Quota objects for different resource types.
|
||||
//
|
||||
// responses:
|
||||
//
|
||||
// 200: GetQuotaResponse200
|
||||
@@ -35,7 +39,8 @@ func (g *Groupware) GetQuota(w http.ResponseWriter, r *http.Request) {
|
||||
return req.errorResponseFromJmap(single(accountId), jerr)
|
||||
}
|
||||
for _, v := range res {
|
||||
return etagResponse(single(accountId), v.List, sessionState, QuotaResponseObjectType, state, lang)
|
||||
body := v.List
|
||||
return etagResponse(single(accountId), body, sessionState, QuotaResponseObjectType, state, lang)
|
||||
}
|
||||
return notFoundResponse(single(accountId), sessionState)
|
||||
})
|
||||
@@ -56,6 +61,9 @@ type SwaggerGetQuotaForAllAccountsResponse200 struct {
|
||||
// swagger:route GET /groupware/accounts/all/quota quota get_quota_for_all_accounts
|
||||
// Get quota limits for all accounts.
|
||||
//
|
||||
// Retrieves the Quota configuration for all the accounts the user currently has access to,
|
||||
// as a dictionary that has the account identifier as its key and an array of Quotas as its value.
|
||||
//
|
||||
// responses:
|
||||
//
|
||||
// 200: GetQuotaForAllAccountsResponse200
|
||||
@@ -65,7 +73,7 @@ func (g *Groupware) GetQuotaForAllAccounts(w http.ResponseWriter, r *http.Reques
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
accountIds := req.AllAccountIds()
|
||||
if len(accountIds) < 1 {
|
||||
return noContentResponse(accountIds, "")
|
||||
return noContentResponse(accountIds, "") // user has no accounts
|
||||
}
|
||||
logger := log.From(req.logger.With().Array(logAccountId, log.SafeStringArray(accountIds)))
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@ package groupware
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
)
|
||||
|
||||
@@ -31,7 +30,8 @@ func (g *Groupware) GetTaskLists(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var _ string = accountId
|
||||
|
||||
return etagResponse(single(accountId), AllTaskLists, req.session.State, TaskListResponseObjectType, TaskListsState, "")
|
||||
var body []jmap.TaskList = AllTaskLists
|
||||
return etagResponse(single(accountId), body, req.session.State, TaskListResponseObjectType, TaskListsState, "")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,7 +61,10 @@ func (g *Groupware) GetTaskListById(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var _ string = accountId
|
||||
|
||||
tasklistId := chi.URLParam(r, UriParamTaskListId)
|
||||
tasklistId, err := req.PathParam(UriParamTaskListId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
// TODO replace with proper implementation
|
||||
for _, tasklist := range AllTaskLists {
|
||||
if tasklist.Id == tasklistId {
|
||||
@@ -96,7 +99,10 @@ func (g *Groupware) GetTasksInTaskList(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
var _ string = accountId
|
||||
|
||||
tasklistId := chi.URLParam(r, UriParamTaskListId)
|
||||
tasklistId, err := req.PathParam(UriParamTaskListId)
|
||||
if err != nil {
|
||||
return errorResponse(single(accountId), err)
|
||||
}
|
||||
// TODO replace with proper implementation
|
||||
tasks, ok := TaskMapByTaskListId[tasklistId]
|
||||
if !ok {
|
||||
|
||||
@@ -194,7 +194,7 @@ const (
|
||||
ErrorCodeMissingContactsSessionCapability = "MSCCON"
|
||||
ErrorCodeMissingContactsAccountCapability = "MACCON"
|
||||
ErrorCodeMissingTasksSessionCapability = "MSCTSK"
|
||||
ErrorCodeMissingTaskAccountCapability = "MACTSK"
|
||||
ErrorCodeMissingTasksAccountCapability = "MACTSK"
|
||||
ErrorCodeFailedToDeleteEmail = "DELEML"
|
||||
ErrorCodeFailedToDeleteSomeIdentities = "DELSID"
|
||||
ErrorCodeFailedToSanitizeEmail = "FSANEM"
|
||||
@@ -318,12 +318,6 @@ var (
|
||||
Title: "Invalid Request",
|
||||
Detail: "The request is invalid.",
|
||||
}
|
||||
ErrorIndeterminateAccount = GroupwareError{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: ErrorCodeNonExistingAccount,
|
||||
Title: "Invalid Account Parameter",
|
||||
Detail: "The account the request is for does not exist.",
|
||||
}
|
||||
ErrorNonExistingAccount = GroupwareError{
|
||||
Status: http.StatusBadRequest,
|
||||
Code: ErrorCodeIndeterminateAccount,
|
||||
@@ -392,39 +386,39 @@ var (
|
||||
}
|
||||
ErrorMissingCalendarsSessionCapability = GroupwareError{
|
||||
Status: http.StatusExpectationFailed,
|
||||
Code: ErrorCodeMissingCalendarsSessionCapability,
|
||||
Title: "Session is missing the task capability '" + jmap.JmapCalendars + "'",
|
||||
Detail: "The JMAP Session of the user does not have the required capability '" + jmap.JmapTasks + "'.",
|
||||
Code: ErrorCodeMissingCalendarsAccountCapability,
|
||||
Title: "Session is missing the calendars session capability",
|
||||
Detail: "The JMAP Session of the user does not have the required capability for calendars.",
|
||||
}
|
||||
ErrorMissingCalendarsAccountCapability = GroupwareError{
|
||||
Status: http.StatusExpectationFailed,
|
||||
Code: ErrorCodeMissingCalendarsSessionCapability,
|
||||
Title: "Account is missing the task capability '" + jmap.JmapCalendars + "'",
|
||||
Detail: "The JMAP Account of the user does not have the required capability '" + jmap.JmapTasks + "'.",
|
||||
Title: "Account is missing the calendars capability",
|
||||
Detail: "The JMAP Account of the user does not have the required capability for calendars.",
|
||||
}
|
||||
ErrorMissingContactsSessionCapability = GroupwareError{
|
||||
Status: http.StatusExpectationFailed,
|
||||
Code: ErrorCodeMissingContactsSessionCapability,
|
||||
Title: "Session is missing the task capability '" + jmap.JmapContacts + "'",
|
||||
Detail: "The JMAP Session of the user does not have the required capability '" + jmap.JmapContacts + "'.",
|
||||
Code: ErrorCodeMissingContactsAccountCapability,
|
||||
Title: "Session is missing the contacts capability",
|
||||
Detail: "The JMAP Session of the user does not have the required capability for accounts.",
|
||||
}
|
||||
ErrorMissingContactsAccountCapability = GroupwareError{
|
||||
Status: http.StatusExpectationFailed,
|
||||
Code: ErrorCodeMissingContactsSessionCapability,
|
||||
Title: "Account is missing the task capability '" + jmap.JmapContacts + "'",
|
||||
Detail: "The JMAP Account of the user does not have the required capability '" + jmap.JmapContacts + "'.",
|
||||
Code: ErrorCodeMissingContactsAccountCapability,
|
||||
Title: "Account is missing the contacts capability",
|
||||
Detail: "The JMAP Account of the user does not have the required capability for accounts.",
|
||||
}
|
||||
ErrorMissingTasksSessionCapability = GroupwareError{
|
||||
Status: http.StatusExpectationFailed,
|
||||
Code: ErrorCodeMissingTasksSessionCapability,
|
||||
Title: "Session is missing the task capability '" + jmap.JmapTasks + "'",
|
||||
Detail: "The JMAP Session of the user does not have the required capability '" + jmap.JmapTasks + "'.",
|
||||
Title: "Session is missing the tasks capability",
|
||||
Detail: "The JMAP Session of the user does not have the required capability for tasks.",
|
||||
}
|
||||
ErrorMissingTasksAccountCapability = GroupwareError{
|
||||
Status: http.StatusExpectationFailed,
|
||||
Code: ErrorCodeMissingTasksSessionCapability,
|
||||
Title: "Account is missing the task capability '" + jmap.JmapTasks + "'",
|
||||
Detail: "The JMAP Account of the user does not have the required capability '" + jmap.JmapTasks + "'.",
|
||||
Code: ErrorCodeMissingTasksAccountCapability,
|
||||
Title: "Account is missing the tasks capability",
|
||||
Detail: "The JMAP Account of the user does not have the required capability for tasks",
|
||||
}
|
||||
ErrorFailedToDeleteEmail = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
|
||||
219
services/groupware/pkg/groupware/groupware_examples_test.go
Normal file
219
services/groupware/pkg/groupware/groupware_examples_test.go
Normal file
@@ -0,0 +1,219 @@
|
||||
//go:build groupware_examples
|
||||
|
||||
package groupware
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jmap"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
var (
|
||||
exampleQuotaState = "veiv8iez"
|
||||
)
|
||||
|
||||
type Exampler struct{}
|
||||
|
||||
var j = jmap.ExamplerInstance
|
||||
|
||||
func Example() {
|
||||
jmap.SerializeExamples(Exampler{})
|
||||
//Output:
|
||||
}
|
||||
|
||||
func (e Exampler) AccountQuota() AccountQuota {
|
||||
return AccountQuota{
|
||||
Quotas: []jmap.Quota{j.Quota()},
|
||||
State: jmap.State(exampleQuotaState),
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) AccountQuotaMap() map[string]AccountQuota {
|
||||
return map[string]AccountQuota{
|
||||
j.AccountId: e.AccountQuota(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) AccountWithId() AccountWithId {
|
||||
a, _ := j.Account()
|
||||
return AccountWithId{
|
||||
AccountId: j.AccountId,
|
||||
Account: a,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) AccountWithIdAndIdentities() AccountWithIdAndIdentities {
|
||||
a, _ := j.Account()
|
||||
return AccountWithIdAndIdentities{
|
||||
AccountId: j.AccountId,
|
||||
Account: a,
|
||||
Identities: j.Identities(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) IndexAccountMailCapabilities() IndexAccountMailCapabilities {
|
||||
m := j.SessionMailAccountCapabilities()
|
||||
s := j.SessionSubmissionAccountCapabilities()
|
||||
return IndexAccountMailCapabilities{
|
||||
MaxMailboxDepth: m.MaxMailboxDepth,
|
||||
MaxSizeMailboxName: m.MaxSizeMailboxName,
|
||||
MaxMailboxesPerEmail: m.MaxMailboxesPerEmail,
|
||||
MaxSizeAttachmentsPerEmail: m.MaxSizeAttachmentsPerEmail,
|
||||
MayCreateTopLevelMailbox: m.MayCreateTopLevelMailbox,
|
||||
MaxDelayedSend: s.MaxDelayedSend,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) IndexAccountSieveCapabilities() IndexAccountSieveCapabilities {
|
||||
s := j.SessionSieveAccountCapabilities()
|
||||
return IndexAccountSieveCapabilities{
|
||||
MaxSizeScriptName: s.MaxSizeScriptName,
|
||||
MaxSizeScript: s.MaxSizeScript,
|
||||
MaxNumberScripts: s.MaxNumberScripts,
|
||||
MaxNumberRedirects: s.MaxNumberRedirects,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) IndexAccountCapabilities() IndexAccountCapabilities {
|
||||
return IndexAccountCapabilities{
|
||||
Mail: e.IndexAccountMailCapabilities(),
|
||||
Sieve: e.IndexAccountSieveCapabilities(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) IndexAccount() IndexAccount {
|
||||
a, _ := j.Account()
|
||||
return IndexAccount{
|
||||
AccountId: j.AccountId,
|
||||
Name: a.Name,
|
||||
IsPersonal: a.IsPersonal,
|
||||
IsReadOnly: a.IsReadOnly,
|
||||
Capabilities: e.IndexAccountCapabilities(),
|
||||
Identities: j.Identities(),
|
||||
Quotas: j.Quotas(),
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) IndexAccounts() []IndexAccount {
|
||||
return []IndexAccount{
|
||||
e.IndexAccount(),
|
||||
{
|
||||
AccountId: j.SharedAccountId,
|
||||
Name: j.SharedAccountName,
|
||||
IsPersonal: false,
|
||||
IsReadOnly: true,
|
||||
Capabilities: e.IndexAccountCapabilities(),
|
||||
Identities: []jmap.Identity{
|
||||
{
|
||||
Id: j.SharedIdentityId,
|
||||
Name: j.SharedIdentityName,
|
||||
Email: j.SharedIdentityEmailAddress,
|
||||
},
|
||||
},
|
||||
Quotas: j.Quotas(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) IndexPrimaryAccounts() IndexPrimaryAccounts {
|
||||
return IndexPrimaryAccounts{
|
||||
Mail: j.AccountId,
|
||||
Submission: j.AccountId,
|
||||
Blob: j.AccountId,
|
||||
VacationResponse: j.AccountId,
|
||||
Sieve: j.AccountId,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) IndexResponse() IndexResponse {
|
||||
return IndexResponse{
|
||||
Version: "4.0.0",
|
||||
Capabilities: []string{"mail:1"},
|
||||
Limits: IndexLimits{
|
||||
MaxSizeUpload: 50000000,
|
||||
MaxConcurrentUpload: 4,
|
||||
MaxSizeRequest: 10000000,
|
||||
MaxConcurrentRequests: 4,
|
||||
},
|
||||
PrimaryAccounts: e.IndexPrimaryAccounts(),
|
||||
Accounts: []IndexAccount{e.IndexAccount()},
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) ErrorResponse() ErrorResponse {
|
||||
err := apiError("6d9c65d1-0368-4833-b09f-885aa0171b95", ErrorNoMailboxWithDraftRole)
|
||||
return ErrorResponse{
|
||||
Errors: []Error{
|
||||
*err,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) MailboxesByAccountId() (map[string][]jmap.Mailbox, string) {
|
||||
j := jmap.ExamplerInstance
|
||||
return map[string][]jmap.Mailbox{
|
||||
j.AccountId: j.Mailboxes(),
|
||||
}, "All mailboxes for all accounts, without a role filter"
|
||||
}
|
||||
|
||||
func (e Exampler) MailboxesByAccountIdFilteredOnInboxRole() (map[string][]jmap.Mailbox, string, string) {
|
||||
j := jmap.ExamplerInstance
|
||||
return map[string][]jmap.Mailbox{
|
||||
j.AccountId: structs.Filter(j.Mailboxes(), func(m jmap.Mailbox) bool { return m.Role == jmap.JmapMailboxRoleInbox }),
|
||||
}, "All mailboxes for all accounts, filtered on the 'inbox' role", "inboxrole"
|
||||
}
|
||||
|
||||
func (e Exampler) EmailSearchResults() EmailSearchResults {
|
||||
sent, _ := time.Parse(time.RFC3339, "2026-01-12T21:46:01Z")
|
||||
received, _ := time.Parse(time.RFC3339, "2026-01-12T21:47:21Z")
|
||||
j := jmap.ExamplerInstance
|
||||
return EmailSearchResults{
|
||||
Results: []jmap.Email{
|
||||
{
|
||||
Id: "ov7ienge",
|
||||
BlobId: "ccyxndo0fxob1jnm3z2lroex131oj7eo2ezo1djhlfgtsu7jgucfeaiasiba",
|
||||
ThreadId: "is",
|
||||
MailboxIds: map[string]bool{j.MailboxInboxId: true},
|
||||
Keywords: map[string]bool{jmap.JmapKeywordAnswered: true},
|
||||
Size: 1084,
|
||||
ReceivedAt: received,
|
||||
MessageId: []string{"1768845021.1753110@example.com"},
|
||||
Sender: []jmap.EmailAddress{
|
||||
{Name: j.SenderName, Email: j.SenderEmailAddress},
|
||||
},
|
||||
From: []jmap.EmailAddress{
|
||||
{Name: j.SenderName, Email: j.SenderEmailAddress},
|
||||
},
|
||||
To: []jmap.EmailAddress{
|
||||
{Name: j.IdentityName, Email: j.EmailAddress},
|
||||
},
|
||||
Subject: "Remember the Cant",
|
||||
SentAt: sent,
|
||||
TextBody: []jmap.EmailBodyPart{
|
||||
{PartId: "1", BlobId: "ckyxndo0fxob1jnm3z2lroex131oj7eo2ezo1djhlfgtsu7jgucfeaiasibnebdw", Size: 115, Type: "text/plain", Charset: "utf-8"},
|
||||
},
|
||||
HtmlBody: []jmap.EmailBodyPart{
|
||||
{PartId: "2", BlobId: "ckyxndo0fxob1jnm3z2lroex131oj7eo2ezo1djhlfgtsu7jgucfeaiasibnsbvjae", Size: 163, Type: "text/html", Charset: "utf-8"},
|
||||
},
|
||||
Preview: "The Canterbury was destroyed while investigating a false distress call from the Scopuli.",
|
||||
},
|
||||
},
|
||||
Total: 132,
|
||||
Limit: 1,
|
||||
QueryState: "seehug3p",
|
||||
}
|
||||
}
|
||||
|
||||
func (e Exampler) MailboxRolesByAccounts() (map[string][]string, string, string, string) {
|
||||
j := jmap.ExamplerInstance
|
||||
return map[string][]string{
|
||||
j.AccountId: jmap.JmapMailboxRoles,
|
||||
j.SharedAccountId: jmap.JmapMailboxRoles,
|
||||
}, "Roles of the Mailboxes of each Account", "", "mailboxrolesbyaccount"
|
||||
}
|
||||
|
||||
func (e Exampler) DeletedMailboxes() ([]string, string, string, string) {
|
||||
j := jmap.ExamplerInstance
|
||||
return []string{j.MailboxProjectId, j.MailboxJunkId}, "Identifiers of the Mailboxes that have successfully been deleted", "", "deletedmailboxes"
|
||||
}
|
||||
@@ -68,14 +68,64 @@ var (
|
||||
errNoPrimaryAccountForBlob = errors.New("no primary account for blob")
|
||||
errNoPrimaryAccountForVacationResponse = errors.New("no primary account for vacation response")
|
||||
errNoPrimaryAccountForSubmission = errors.New("no primary account for submission")
|
||||
errNoPrimaryAccountForTask = errors.New("no primary account for task")
|
||||
errNoPrimaryAccountForCalendar = errors.New("no primary account for calendar")
|
||||
errNoPrimaryAccountForContact = errors.New("no primary account for contact")
|
||||
errNoPrimaryAccountForQuota = errors.New("no primary account for quota")
|
||||
// errNoPrimaryAccountForTask = errors.New("no primary account for task")
|
||||
// errNoPrimaryAccountForCalendar = errors.New("no primary account for calendar")
|
||||
// errNoPrimaryAccountForContact = errors.New("no primary account for contact")
|
||||
// errNoPrimaryAccountForSieve = errors.New("no primary account for sieve")
|
||||
// errNoPrimaryAccountForWebsocket = errors.New("no primary account for websocket")
|
||||
)
|
||||
|
||||
func (r Request) HeaderParam(name string) (string, *Error) {
|
||||
value := r.r.Header.Get(name)
|
||||
if value == "" {
|
||||
msg := fmt.Sprintf("Missing mandatory request header '%s'", name)
|
||||
return "", r.observedParameterError(ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Header: name}),
|
||||
)
|
||||
} else {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r Request) HeaderParamDoc(name string, _ string) (string, *Error) {
|
||||
return r.HeaderParam(name)
|
||||
}
|
||||
|
||||
func (r Request) OptHeaderParam(name string) string {
|
||||
return r.r.Header.Get(name)
|
||||
}
|
||||
|
||||
func (r Request) OptHeaderParamDoc(name string, _ string) string {
|
||||
return r.OptHeaderParam(name)
|
||||
}
|
||||
|
||||
func (r Request) PathParam(name string) (string, *Error) {
|
||||
value := chi.URLParam(r.r, name)
|
||||
if value == "" {
|
||||
msg := fmt.Sprintf("Missing mandatory path parameter '%s'", name)
|
||||
return "", r.observedParameterError(ErrorInvalidRequestParameter,
|
||||
withDetail(msg),
|
||||
withSource(&ErrorSource{Parameter: name}),
|
||||
)
|
||||
} else {
|
||||
return value, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r Request) PathParamDoc(name string, _ string) (string, *Error) {
|
||||
return r.PathParam(name)
|
||||
}
|
||||
|
||||
func (r Request) PathListParamDoc(name string, _ string) ([]string, *Error) {
|
||||
value, err := r.PathParam(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return strings.Split(value, ","), nil
|
||||
}
|
||||
|
||||
func (r Request) AllAccountIds() []string {
|
||||
// TODO potentially filter on "subscribed" accounts?
|
||||
return structs.Uniq(structs.Keys(r.session.Accounts))
|
||||
@@ -313,6 +363,26 @@ func (r Request) parseMapParam(param string) (map[string]string, bool, *Error) {
|
||||
return result, true, nil
|
||||
}
|
||||
|
||||
func (r Request) parseOptStringListParam(param string) ([]string, bool, *Error) {
|
||||
result := []string{}
|
||||
q := r.r.URL.Query()
|
||||
if !q.Has(param) {
|
||||
return nil, false, nil
|
||||
}
|
||||
for _, value := range q[param] {
|
||||
for _, v := range strings.Split(value, ",") {
|
||||
if strings.TrimSpace(v) != "" {
|
||||
result = append(result, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result, true, nil
|
||||
}
|
||||
|
||||
func (r Request) bodydoc(target any, _ string) *Error {
|
||||
return r.body(target)
|
||||
}
|
||||
|
||||
func (r Request) body(target any) *Error {
|
||||
body := r.r.Body
|
||||
defer func(b io.ReadCloser) {
|
||||
|
||||
@@ -163,7 +163,7 @@ func etagNotFoundResponse(accountIds []string, sessionState jmap.SessionState, o
|
||||
}
|
||||
}
|
||||
|
||||
func notImplementesResponse() Response {
|
||||
func notImplementedResponse() Response {
|
||||
return Response{
|
||||
body: nil,
|
||||
status: http.StatusNotImplemented,
|
||||
|
||||
@@ -11,19 +11,20 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
UriParamAccountId = "accountid"
|
||||
UriParamMailboxId = "mailboxid"
|
||||
UriParamEmailId = "emailid"
|
||||
UriParamIdentityId = "identityid"
|
||||
UriParamBlobId = "blobid"
|
||||
UriParamAccountId = "accountid" // Identifier of the account
|
||||
UriParamMailboxId = "mailboxid" // Identifier of the mailbox
|
||||
UriParamEmailId = "emailid" // Identifier of the email
|
||||
UriParamIdentityId = "identityid" // Identifier of the identity
|
||||
UriParamBlobId = "blobid" // Identifier of theblob
|
||||
UriParamStreamId = "stream" // Identifier of the stream
|
||||
UriParamAddressBookId = "addressbookid" // Identifier of the address book
|
||||
UriParamCalendarId = "calendarid" // Identifier of the calendar
|
||||
UriParamTaskListId = "tasklistid" // Identifier of the tasklist
|
||||
UriParamContactId = "contactid" // Identifier of the contact
|
||||
UriParamEventId = "eventid" // Idenfitier of the event
|
||||
UriParamBlobName = "blobname"
|
||||
UriParamStreamId = "stream"
|
||||
UriParamSince = "since"
|
||||
UriParamRole = "role"
|
||||
UriParamAddressBookId = "addressbookid"
|
||||
UriParamCalendarId = "calendarid"
|
||||
UriParamTaskListId = "tasklistid"
|
||||
UriParamContactId = "contactid"
|
||||
UriParamEventId = "eventid"
|
||||
QueryParamMailboxSearchName = "name"
|
||||
QueryParamMailboxSearchRole = "role"
|
||||
QueryParamMailboxSearchSubscribed = "subscribed"
|
||||
@@ -57,7 +58,7 @@ const (
|
||||
QueryParamSeen = "seen"
|
||||
QueryParamUndesirable = "undesirable"
|
||||
QueryParamMarkAsSeen = "markAsSeen"
|
||||
HeaderSince = "if-none-match"
|
||||
HeaderParamSince = "if-none-match"
|
||||
)
|
||||
|
||||
func (g *Groupware) Route(r chi.Router) {
|
||||
@@ -100,6 +101,7 @@ func (g *Groupware) Route(r chi.Router) {
|
||||
r.Route("/{mailboxid}", func(r chi.Router) {
|
||||
r.Get("/", g.GetMailbox)
|
||||
r.Get("/emails", g.GetAllEmailsInMailbox)
|
||||
r.Get("/emails/since/{since}", g.GetAllEmailsInMailboxSince)
|
||||
r.Get("/changes", g.GetMailboxChanges)
|
||||
r.Patch("/", g.UpdateMailbox)
|
||||
r.Delete("/", g.DeleteMailbox)
|
||||
|
||||
@@ -29,7 +29,7 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler {
|
||||
opt := authOptions(opts...)
|
||||
tokenManager, err := jwt.New(map[string]any{
|
||||
"secret": opt.JWTSecret,
|
||||
"expires": int64(24 * 60 * 60),
|
||||
"expires": int64(24 * 60 * 60), // token expiration in seconds
|
||||
})
|
||||
if err != nil {
|
||||
opt.Logger.Fatal().Err(err).Msgf("Could not initialize token-manager")
|
||||
@@ -39,19 +39,37 @@ func Auth(opts ...account.Option) func(http.Handler) http.Handler {
|
||||
ctx := r.Context()
|
||||
t := r.Header.Get(revactx.TokenHeader)
|
||||
if t == "" {
|
||||
opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Msgf("missing access token in header %v", revactx.TokenHeader)
|
||||
requestID := r.Header.Get("X-Request-ID")
|
||||
traceID := GetTraceID(ctx)
|
||||
l := opt.Logger.Error().Str(log.RequestIDString, log.SafeString(requestID))
|
||||
if traceID != "" {
|
||||
l = l.Str(LogTraceID, log.SafeString(traceID))
|
||||
}
|
||||
l.Msgf("missing access token in header %v", revactx.TokenHeader)
|
||||
w.WriteHeader(http.StatusUnauthorized) // missing access token
|
||||
return
|
||||
}
|
||||
|
||||
u, tokenScope, err := tokenManager.DismantleToken(r.Context(), t)
|
||||
if err != nil {
|
||||
opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Err(err).Msgf("invalid access token in header %v", revactx.TokenHeader)
|
||||
requestID := r.Header.Get("X-Request-ID")
|
||||
traceID := GetTraceID(ctx)
|
||||
l := opt.Logger.Error().Str(log.RequestIDString, log.SafeString(requestID))
|
||||
if traceID != "" {
|
||||
l = l.Str(LogTraceID, log.SafeString(traceID))
|
||||
}
|
||||
l.Err(err).Msgf("invalid access token in header %v", revactx.TokenHeader)
|
||||
w.WriteHeader(http.StatusUnauthorized) // invalid token
|
||||
return
|
||||
}
|
||||
if ok, err := scope.VerifyScope(ctx, tokenScope, r); err != nil || !ok {
|
||||
opt.Logger.Error().Str(log.RequestIDString, r.Header.Get("X-Request-ID")).Err(err).Msg("verifying scope failed")
|
||||
requestID := r.Header.Get("X-Request-ID")
|
||||
traceID := GetTraceID(ctx)
|
||||
l := opt.Logger.Error().Str(log.RequestIDString, log.SafeString(requestID))
|
||||
if traceID != "" {
|
||||
l = l.Str(LogTraceID, log.SafeString(traceID))
|
||||
}
|
||||
l.Err(err).Msg("verifying scope failed")
|
||||
w.WriteHeader(http.StatusUnauthorized) // invalid scope
|
||||
return
|
||||
}
|
||||
|
||||
@@ -8,6 +8,10 @@ import (
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
)
|
||||
|
||||
var (
|
||||
LogTraceID = "traceId"
|
||||
)
|
||||
|
||||
func GroupwareLogger(logger log.Logger) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -37,12 +41,10 @@ func GroupwareLogger(logger log.Logger) func(http.Handler) http.Handler {
|
||||
ctx := r.Context()
|
||||
|
||||
requestID := middleware.GetReqID(ctx)
|
||||
level = level.Str(log.RequestIDString, log.SafeString(requestID))
|
||||
traceID := GetTraceID(ctx)
|
||||
|
||||
level.Str(log.RequestIDString, requestID)
|
||||
|
||||
if traceID != "" {
|
||||
level.Str("traceId", traceID)
|
||||
level = level.Str(LogTraceID, log.SafeString(traceID))
|
||||
}
|
||||
|
||||
level.
|
||||
|
||||
638
services/groupware/pnpm-lock.yaml
generated
638
services/groupware/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user