mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-02-24 02:56:52 -05:00
groupware: add ContactCard operations
This commit is contained in:
193
pkg/jmap/jmap_api_contact.go
Normal file
193
pkg/jmap/jmap_api_contact.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package jmap
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
"github.com/opencloud-eu/opencloud/pkg/log"
|
||||
"github.com/opencloud-eu/opencloud/pkg/structs"
|
||||
)
|
||||
|
||||
type AddressBooksResponse struct {
|
||||
AddressBooks []AddressBook `json:"addressbooks"`
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
func (j *Client) GetAddressbooks(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, ids []string) (AddressBooksResponse, SessionState, Language, Error) {
|
||||
logger = j.logger("GetAddressbooks", session, logger)
|
||||
|
||||
cmd, err := j.request(session, logger,
|
||||
invocation(CommandAddressBookGet, AddressBookGetCommand{AccountId: accountId, Ids: ids}, "0"),
|
||||
)
|
||||
if err != nil {
|
||||
return AddressBooksResponse{}, "", "", err
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (AddressBooksResponse, Error) {
|
||||
var response AddressBookGetResponse
|
||||
err = retrieveResponseMatchParameters(logger, body, CommandAddressBookGet, "0", &response)
|
||||
if err != nil {
|
||||
return AddressBooksResponse{}, err
|
||||
}
|
||||
return AddressBooksResponse{
|
||||
AddressBooks: response.List,
|
||||
NotFound: response.NotFound,
|
||||
State: response.State,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
func (j *Client) QueryContactCards(accountIds []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string,
|
||||
filter ContactCardFilterElement, sortBy []ContactCardComparator,
|
||||
position uint, limit uint) (map[string][]jscontact.ContactCard, SessionState, Language, Error) {
|
||||
logger = j.logger("QueryContactCards", session, logger)
|
||||
|
||||
uniqueAccountIds := structs.Uniq(accountIds)
|
||||
|
||||
if sortBy == nil {
|
||||
sortBy = []ContactCardComparator{{Property: jscontact.ContactCardPropertyUpdated, IsAscending: false}}
|
||||
}
|
||||
|
||||
invocations := make([]Invocation, len(uniqueAccountIds)*2)
|
||||
for i, accountId := range uniqueAccountIds {
|
||||
query := ContactCardQueryCommand{
|
||||
AccountId: accountId,
|
||||
Filter: filter,
|
||||
Sort: sortBy,
|
||||
}
|
||||
if limit > 0 {
|
||||
query.Limit = limit
|
||||
}
|
||||
if position > 0 {
|
||||
query.Position = position
|
||||
}
|
||||
invocations[i*2+0] = invocation(CommandContactCardQuery, query, mcid(accountId, "0"))
|
||||
invocations[i*2+1] = invocation(CommandContactCardGet, ContactCardGetRefCommand{
|
||||
AccountId: accountId,
|
||||
IdsRef: &ResultReference{
|
||||
Name: CommandContactCardQuery,
|
||||
Path: "/ids/*",
|
||||
ResultOf: mcid(accountId, "0"),
|
||||
},
|
||||
}, mcid(accountId, "1"))
|
||||
}
|
||||
cmd, err := j.request(session, logger, invocations...)
|
||||
if err != nil {
|
||||
return nil, "", "", err
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (map[string][]jscontact.ContactCard, Error) {
|
||||
resp := map[string][]jscontact.ContactCard{}
|
||||
for _, accountId := range uniqueAccountIds {
|
||||
var response ContactCardGetResponse
|
||||
err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, mcid(accountId, "1"), &response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(response.NotFound) > 0 {
|
||||
// TODO what to do when there are not-found emails here? potentially nothing, they could have been deleted between query and get?
|
||||
}
|
||||
resp[accountId] = response.List
|
||||
}
|
||||
return resp, nil
|
||||
})
|
||||
}
|
||||
|
||||
type CreatedContactCard struct {
|
||||
ContactCard *jscontact.ContactCard `json:"contactCard"`
|
||||
State State `json:"state"`
|
||||
}
|
||||
|
||||
func (j *Client) CreateContactCard(accountId string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string, create jscontact.ContactCard) (CreatedContactCard, SessionState, Language, Error) {
|
||||
logger = j.logger("CreateContactCard", session, logger)
|
||||
|
||||
cmd, err := j.request(session, logger,
|
||||
invocation(CommandContactCardSet, ContactCardSetCommand{
|
||||
AccountId: accountId,
|
||||
Create: map[string]jscontact.ContactCard{
|
||||
"c": create,
|
||||
},
|
||||
}, "0"),
|
||||
invocation(CommandContactCardGet, ContactCardGetRefCommand{
|
||||
AccountId: accountId,
|
||||
IdsRef: &ResultReference{
|
||||
ResultOf: "0",
|
||||
Name: CommandContactCardSet,
|
||||
Path: "/created/c/id",
|
||||
},
|
||||
}, "1"),
|
||||
)
|
||||
if err != nil {
|
||||
return CreatedContactCard{}, "", "", err
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (CreatedContactCard, Error) {
|
||||
var setResponse ContactCardSetResponse
|
||||
err = retrieveResponseMatchParameters(logger, body, CommandContactCardSet, "0", &setResponse)
|
||||
if err != nil {
|
||||
return CreatedContactCard{}, err
|
||||
}
|
||||
|
||||
setErr, notok := setResponse.NotCreated["c"]
|
||||
if notok {
|
||||
logger.Error().Msgf("%T.NotCreated returned an error %v", setResponse, setErr)
|
||||
return CreatedContactCard{}, setErrorError(setErr, EmailType)
|
||||
}
|
||||
|
||||
if created, ok := setResponse.Created["c"]; !ok || created != nil {
|
||||
berr := fmt.Errorf("failed to find %s in %s response", string(ContactCardType), string(CommandContactCardSet))
|
||||
logger.Error().Err(berr)
|
||||
return CreatedContactCard{}, simpleError(berr, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
|
||||
var getResponse ContactCardGetResponse
|
||||
err = retrieveResponseMatchParameters(logger, body, CommandContactCardGet, "1", &getResponse)
|
||||
if err != nil {
|
||||
return CreatedContactCard{}, err
|
||||
}
|
||||
|
||||
if len(getResponse.List) < 1 {
|
||||
berr := fmt.Errorf("failed to find %s in %s response", string(ContactCardType), string(CommandContactCardSet))
|
||||
logger.Error().Err(berr)
|
||||
return CreatedContactCard{}, simpleError(berr, JmapErrorInvalidJmapResponsePayload)
|
||||
}
|
||||
|
||||
return CreatedContactCard{
|
||||
ContactCard: &getResponse.List[0],
|
||||
State: setResponse.NewState,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
|
||||
type DeletedContactCards struct {
|
||||
State State `json:"state"`
|
||||
NotDestroyed map[string]SetError `json:"notDestroyed"`
|
||||
}
|
||||
|
||||
func (j *Client) DeleteContactCard(accountId string, destroy []string, session *Session, ctx context.Context, logger *log.Logger, acceptLanguage string) (DeletedContactCards, SessionState, Language, Error) {
|
||||
logger = j.logger("DeleteContactCard", session, logger)
|
||||
|
||||
cmd, err := j.request(session, logger,
|
||||
invocation(CommandContactCardSet, ContactCardSetCommand{
|
||||
AccountId: accountId,
|
||||
Destroy: destroy,
|
||||
}, "0"),
|
||||
)
|
||||
if err != nil {
|
||||
return DeletedContactCards{}, "", "", err
|
||||
}
|
||||
|
||||
return command(j.api, logger, ctx, session, j.onSessionOutdated, cmd, acceptLanguage, func(body *Response) (DeletedContactCards, Error) {
|
||||
var setResponse ContactCardSetResponse
|
||||
err = retrieveResponseMatchParameters(logger, body, CommandContactCardSet, "0", &setResponse)
|
||||
if err != nil {
|
||||
return DeletedContactCards{}, err
|
||||
}
|
||||
return DeletedContactCards{
|
||||
State: setResponse.NewState,
|
||||
NotDestroyed: setResponse.NotDestroyed,
|
||||
}, nil
|
||||
})
|
||||
}
|
||||
@@ -11,18 +11,6 @@ import (
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
const (
|
||||
emailSortByReceivedAt = "receivedAt"
|
||||
emailSortBySize = "size"
|
||||
emailSortByFrom = "from"
|
||||
emailSortByTo = "to"
|
||||
emailSortBySubject = "subject"
|
||||
emailSortBySentAt = "sentAt"
|
||||
emailSortByHasKeyword = "hasKeyword"
|
||||
emailSortByAllInThreadHaveKeyword = "allInThreadHaveKeyword"
|
||||
emailSortBySomeInThreadHaveKeyword = "someInThreadHaveKeyword"
|
||||
)
|
||||
|
||||
type Emails struct {
|
||||
Emails []Email `json:"emails,omitempty"`
|
||||
Total uint `json:"total,omitzero"`
|
||||
@@ -128,7 +116,7 @@ func (j *Client) GetAllEmailsInMailbox(accountId string, session *Session, ctx c
|
||||
query := EmailQueryCommand{
|
||||
AccountId: accountId,
|
||||
Filter: &EmailFilterCondition{InMailbox: mailboxId},
|
||||
Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}},
|
||||
Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}},
|
||||
CollapseThreads: collapseThreads,
|
||||
CalculateTotal: true,
|
||||
}
|
||||
@@ -276,7 +264,7 @@ func (j *Client) QueryEmailSnippets(accountIds []string, filter EmailFilterEleme
|
||||
query := EmailQueryCommand{
|
||||
AccountId: accountId,
|
||||
Filter: filter,
|
||||
Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}},
|
||||
Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}},
|
||||
CollapseThreads: true,
|
||||
CalculateTotal: true,
|
||||
}
|
||||
@@ -392,7 +380,7 @@ func (j *Client) QueryEmails(accountIds []string, filter EmailFilterElement, ses
|
||||
query := EmailQueryCommand{
|
||||
AccountId: accountId,
|
||||
Filter: filter,
|
||||
Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}},
|
||||
Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}},
|
||||
CollapseThreads: true,
|
||||
CalculateTotal: true,
|
||||
}
|
||||
@@ -474,7 +462,7 @@ func (j *Client) QueryEmailsWithSnippets(accountIds []string, filter EmailFilter
|
||||
query := EmailQueryCommand{
|
||||
AccountId: accountId,
|
||||
Filter: filter,
|
||||
Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}},
|
||||
Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}},
|
||||
CollapseThreads: false,
|
||||
CalculateTotal: true,
|
||||
}
|
||||
@@ -915,7 +903,6 @@ func (j *Client) EmailsInThread(accountId string, threadId string, session *Sess
|
||||
}
|
||||
return emailsResponse.List, nil
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
type EmailsSummary struct {
|
||||
@@ -957,7 +944,7 @@ func (j *Client) QueryEmailSummaries(accountIds []string, session *Session, ctx
|
||||
invocations[i*factor+0] = invocation(CommandEmailQuery, EmailQueryCommand{
|
||||
AccountId: accountId,
|
||||
Filter: filter,
|
||||
Sort: []EmailComparator{{Property: emailSortByReceivedAt, IsAscending: false}},
|
||||
Sort: []EmailComparator{{Property: EmailPropertyReceivedAt, IsAscending: false}},
|
||||
Limit: limit,
|
||||
//CalculateTotal: false,
|
||||
}, mcid(accountId, "0"))
|
||||
|
||||
@@ -100,7 +100,7 @@ func (j *Client) request(session *Session, logger *log.Logger, methodCalls ...In
|
||||
return Request{}, err
|
||||
}
|
||||
return Request{
|
||||
Using: []string{JmapCore, JmapMail},
|
||||
Using: []string{JmapCore, JmapMail, JmapContacts},
|
||||
MethodCalls: methodCalls,
|
||||
CreatedIds: nil,
|
||||
}, nil
|
||||
|
||||
@@ -206,7 +206,7 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio
|
||||
if logger.Trace().Enabled() {
|
||||
requestBytes, err := httputil.DumpRequestOut(req, true)
|
||||
if err == nil {
|
||||
logger.Trace().Str(logEndpoint, endpoint).Msg(string(requestBytes))
|
||||
logger.Trace().Str(logEndpoint, endpoint).Str("proto", "jmap").Str("type", "request").Msg(string(requestBytes))
|
||||
}
|
||||
}
|
||||
h.auth(session.Username, logger, req)
|
||||
@@ -217,6 +217,14 @@ func (h *HttpJmapClient) Command(ctx context.Context, logger *log.Logger, sessio
|
||||
logger.Error().Err(err).Msgf("failed to perform POST %v", jmapUrl)
|
||||
return nil, "", SimpleError{code: JmapErrorSendingRequest, err: err}
|
||||
}
|
||||
|
||||
if logger.Trace().Enabled() {
|
||||
requestBytes, err := httputil.DumpResponse(res, true)
|
||||
if err == nil {
|
||||
logger.Trace().Str(logEndpoint, endpoint).Str("proto", "jmap").Str("type", "response").Msg(string(requestBytes))
|
||||
}
|
||||
}
|
||||
|
||||
language := Language(res.Header.Get("Content-Language"))
|
||||
if res.StatusCode < 200 || res.StatusCode > 299 {
|
||||
h.listener.OnFailedRequestWithStatus(endpoint, res.StatusCode)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscalendar"
|
||||
"github.com/opencloud-eu/opencloud/pkg/jscontact"
|
||||
)
|
||||
|
||||
// https://www.iana.org/assignments/jmap/jmap.xml#jmap-data-types
|
||||
@@ -4722,6 +4723,483 @@ type QuotaGetResponse struct {
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
}
|
||||
|
||||
type AddressBookGetCommand struct {
|
||||
AccountId string `json:"accountId"`
|
||||
Ids []string `json:"ids,omitempty"`
|
||||
}
|
||||
|
||||
type AddressBookGetResponse struct {
|
||||
AccountId string `json:"accountId"`
|
||||
State State `json:"state,omitempty"`
|
||||
List []AddressBook `json:"list,omitempty"`
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
}
|
||||
|
||||
type ContacCardGetResponse struct {
|
||||
AccountId string `json:"accountId"`
|
||||
State State `json:"state,omitempty"`
|
||||
List []AddressBook `json:"list,omitempty"`
|
||||
NotFound []string `json:"notFound,omitempty"`
|
||||
}
|
||||
|
||||
type ContactCardComparator struct {
|
||||
// The name of the property on the objects to compare.
|
||||
Property string `json:"property,omitempty"`
|
||||
|
||||
// If true, sort in ascending order.
|
||||
//
|
||||
// Optional; default value: true.
|
||||
//
|
||||
// If false, reverse the comparator’s results to sort in descending order.
|
||||
IsAscending bool `json:"isAscending,omitempty"`
|
||||
|
||||
// The identifier, as registered in the collation registry defined in [RFC4790],
|
||||
// for the algorithm to use when comparing the order of strings.
|
||||
//
|
||||
// Optional; default is server dependent.
|
||||
//
|
||||
// The algorithms the server supports are advertised in the capabilities object returned
|
||||
// with the Session object.
|
||||
//
|
||||
// [RFC4790]: https://www.rfc-editor.org/rfc/rfc4790.html
|
||||
Collation string `json:"collation,omitempty"`
|
||||
|
||||
// ContactCard-specific: The “created” date on the ContactCard.
|
||||
Created time.Time `json:"created,omitzero"`
|
||||
|
||||
// ContactCard-specific: The "updated” date on the ContactCard.
|
||||
Updated time.Time `json:"updated,omitzero"`
|
||||
}
|
||||
|
||||
type ContactCardFilterElement interface {
|
||||
_isAContactCardFilterElement() // marker method
|
||||
IsNotEmpty() bool
|
||||
}
|
||||
|
||||
type ContactCardFilterCondition struct {
|
||||
// An AddressBook id.
|
||||
//
|
||||
// A card must be in this address book to match the condition.
|
||||
InAddressBook string `json:"inAddressBook,omitempty"`
|
||||
|
||||
// A card must have this string exactly as its uid to match.
|
||||
Uid string `json:"uid,omitempty"`
|
||||
|
||||
// A card must have a “members” property that contains this string as one of the uids in the set to match.
|
||||
HasMember string `json:"hasMember,omitempty"`
|
||||
|
||||
// A card must have a type property that equals this string exactly to match.
|
||||
Kind string `json:"kind,omitempty"`
|
||||
|
||||
// The “created” date-time of the ContactCard must be before this date-time to match the condition.
|
||||
CreatedBefore UTCDate `json:"createdBefore,omitzero"`
|
||||
|
||||
// The “created” date-time of the ContactCard must be the same or after this date-time to match the condition.
|
||||
CreatedAfter UTCDate `json:"createdAfter,omitzero"`
|
||||
|
||||
// The “updated” date-time of the ContactCard must be before this date-time to match the condition.
|
||||
UpdatedBefore UTCDate `json:"updatedBefore,omitzero"`
|
||||
|
||||
// The “updated” date-time of the ContactCard must be the same or after this date-time to match the condition.
|
||||
UpdatedAfter UTCDate `json:"updatedAfter,omitzero"`
|
||||
|
||||
// A card matches this condition if the text matches with text in the card.
|
||||
Text string `json:"text,omitempty"`
|
||||
|
||||
// A card matches this condition if the value of any NameComponent in the “name” property, or the
|
||||
// “full” property in the “name” property of the card matches the value.
|
||||
Name string `json:"name,omitempty"`
|
||||
|
||||
// A card matches this condition if the value of a NameComponent with kind “given” inside the “name” property of
|
||||
// the card matches the value.
|
||||
NameGiven string `json:"name/given,omitempty"`
|
||||
|
||||
// A card matches this condition if the value of a NameComponent with kind “surname” inside the “name” property
|
||||
// of the card matches the value.
|
||||
NameSurname string `json:"name/surname,omitempty"`
|
||||
|
||||
// A card matches this condition if the value of a NameComponent with kind “surname2” inside the “name” property
|
||||
// of the card matches the value.
|
||||
NameSurname2 string `json:"name/surname2,omitempty"`
|
||||
|
||||
// A card matches this condition if the “name” of any NickName in the “nickNames” property of the card matches the value.
|
||||
NickName string `json:"nickName,omitempty"`
|
||||
|
||||
// A card matches this condition if the “name” of any Organization in the “organizations” property of the card
|
||||
// matches the value.
|
||||
Organization string `json:"organization,omitempty"`
|
||||
|
||||
// A card matches this condition if the “address” or “label” of any EmailAddress in the “emails” property of the
|
||||
// card matches the value.
|
||||
Email string `json:"email,omitempty"`
|
||||
|
||||
// A card matches this condition if the “number” or “label” of any Phone in the “phones” property of the card
|
||||
// matches the value.
|
||||
Phone string `json:"phone,omitempty"`
|
||||
|
||||
// A card matches this condition if the “service”, “uri”, “user”, or “label” of any OnlineService in the
|
||||
// “onlineServices” property of the card matches the value.
|
||||
OnlineService string `json:"onlineService,omitempty"`
|
||||
|
||||
// A card matches this condition if the value of any StreetComponent in the “street” property, or the “locality”,
|
||||
// “region”, “country”, or “postcode” property in any Address in the “addresses” property of the card matches the value.
|
||||
Address string `json:"address,omitempty"`
|
||||
|
||||
// A card matches this condition if the “note” of any Note in the “notes” property of the card matches the value.
|
||||
Note string `json:"note,omitempty"`
|
||||
}
|
||||
|
||||
func (f ContactCardFilterCondition) _isAContactCardFilterElement() {
|
||||
}
|
||||
|
||||
func (f ContactCardFilterCondition) IsNotEmpty() bool {
|
||||
if len(f.InAddressBook) != 0 {
|
||||
return true
|
||||
}
|
||||
if f.Uid != "" {
|
||||
return true
|
||||
}
|
||||
if f.HasMember != "" {
|
||||
return true
|
||||
}
|
||||
if f.Kind != "" {
|
||||
return true
|
||||
}
|
||||
if !f.CreatedBefore.IsZero() {
|
||||
return true
|
||||
}
|
||||
if !f.CreatedAfter.IsZero() {
|
||||
return true
|
||||
}
|
||||
if !f.UpdatedBefore.IsZero() {
|
||||
return true
|
||||
}
|
||||
if !f.UpdatedAfter.IsZero() {
|
||||
return true
|
||||
}
|
||||
if f.Text != "" {
|
||||
return true
|
||||
}
|
||||
if f.Name != "" {
|
||||
return true
|
||||
}
|
||||
if f.NameGiven != "" {
|
||||
return true
|
||||
}
|
||||
if f.NameSurname != "" {
|
||||
return true
|
||||
}
|
||||
if f.NameSurname2 != "" {
|
||||
return true
|
||||
}
|
||||
if f.NickName != "" {
|
||||
return true
|
||||
}
|
||||
if f.Organization != "" {
|
||||
return true
|
||||
}
|
||||
if f.Email != "" {
|
||||
return true
|
||||
}
|
||||
if f.Phone != "" {
|
||||
return true
|
||||
}
|
||||
if f.OnlineService != "" {
|
||||
return true
|
||||
}
|
||||
if f.Address != "" {
|
||||
return true
|
||||
}
|
||||
if f.Note != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
var _ ContactCardFilterElement = &ContactCardFilterCondition{}
|
||||
|
||||
type ContactCardFilterOperator struct {
|
||||
Operator FilterOperatorTerm `json:"operator"`
|
||||
Conditions []ContactCardFilterElement `json:"conditions,omitempty"`
|
||||
}
|
||||
|
||||
func (o ContactCardFilterOperator) _isAContactCardFilterElement() {
|
||||
}
|
||||
|
||||
func (o ContactCardFilterOperator) IsNotEmpty() bool {
|
||||
return len(o.Conditions) > 0
|
||||
}
|
||||
|
||||
var _ ContactCardFilterElement = &ContactCardFilterOperator{}
|
||||
|
||||
type ContactCardQueryCommand struct {
|
||||
AccountId string `json:"accountId"`
|
||||
|
||||
Filter ContactCardFilterElement `json:"filter,omitempty"`
|
||||
|
||||
Sort []ContactCardComparator `json:"sort,omitempty"`
|
||||
|
||||
// The zero-based index of the first id in the full list of results to return.
|
||||
//
|
||||
// If a negative value is given, it is an offset from the end of the list.
|
||||
// Specifically, the negative value MUST be added to the total number of results given
|
||||
// the filter, and if still negative, it’s clamped to 0. This is now the zero-based
|
||||
// index of the first id to return.
|
||||
//
|
||||
// If the index is greater than or equal to the total number of objects in the results
|
||||
// list, then the ids array in the response will be empty, but this is not an error.
|
||||
Position uint `json:"position,omitempty"`
|
||||
|
||||
// An Email id.
|
||||
//
|
||||
// If supplied, the position argument is ignored.
|
||||
// The index of this id in the results will be used in combination with the anchorOffset
|
||||
// argument to determine the index of the first result to return.
|
||||
Anchor string `json:"anchor,omitempty"`
|
||||
|
||||
// The index of the first result to return relative to the index of the anchor,
|
||||
// if an anchor is given.
|
||||
//
|
||||
// Default: 0.
|
||||
//
|
||||
// This MAY be negative.
|
||||
//
|
||||
// For example, -1 means the Email immediately preceding the anchor is the first result in
|
||||
// the list returned.
|
||||
AnchorOffset int `json:"anchorOffset,omitzero"`
|
||||
|
||||
// The maximum number of results to return.
|
||||
//
|
||||
// If null, no limit presumed.
|
||||
// The server MAY choose to enforce a maximum limit argument.
|
||||
// In this case, if a greater value is given (or if it is null), the limit is clamped
|
||||
// to the maximum; the new limit is returned with the response so the client is aware.
|
||||
//
|
||||
// If a negative value is given, the call MUST be rejected with an invalidArguments error.
|
||||
Limit uint `json:"limit,omitempty"`
|
||||
|
||||
// Does the client wish to know the total number of results in the query?
|
||||
//
|
||||
// This may be slow and expensive for servers to calculate, particularly with complex filters,
|
||||
// so clients should take care to only request the total when needed.
|
||||
CalculateTotal bool `json:"calculateTotal,omitempty"`
|
||||
}
|
||||
|
||||
type ContactCardQueryResponse struct {
|
||||
// The id of the account used for the call.
|
||||
AccountId string `json:"accountId"`
|
||||
|
||||
// A string encoding the current state of the query on the server.
|
||||
//
|
||||
// This string MUST change if the results of the query (i.e., the matching ids and their sort order) have changed.
|
||||
// The queryState string MAY change if something has changed on the server, which means the results may have changed
|
||||
// but the server doesn’t know for sure.
|
||||
//
|
||||
// The queryState string only represents the ordered list of ids that match the particular query (including its sort/filter).
|
||||
// There is no requirement for it to change if a property on an object matching the query changes but the query results are unaffected
|
||||
// (indeed, it is more efficient if the queryState string does not change in this case).
|
||||
//
|
||||
// The queryState string only has meaning when compared to future responses to a query with the same type/sort/filter or when used with
|
||||
// /queryChanges to fetch changes.
|
||||
//
|
||||
// Should a client receive back a response with a different queryState string to a previous call, it MUST either throw away the currently
|
||||
// cached query and fetch it again (note, this does not require fetching the records again, just the list of ids) or call
|
||||
// Email/queryChanges to get the difference.
|
||||
QueryState State `json:"queryState"`
|
||||
|
||||
// This is true if the server supports calling ContactCard/queryChanges with these filter/sort parameters.
|
||||
//
|
||||
// Note, this does not guarantee that the ContactCard/queryChanges call will succeed, as it may only be possible for a limited time
|
||||
// afterwards due to server internal implementation details.
|
||||
CanCalculateChanges bool `json:"canCalculateChanges"`
|
||||
|
||||
// The zero-based index of the first result in the ids array within the complete list of query results.
|
||||
Position uint `json:"position"`
|
||||
|
||||
// The list of ids for each ContactCard in the query results, starting at the index given by the position argument of this
|
||||
// response and continuing until it hits the end of the results or reaches the limit number of ids.
|
||||
//
|
||||
// If position is >= total, this MUST be the empty list.
|
||||
Ids []string `json:"ids"`
|
||||
|
||||
// The total number of ContactCards in the results (given the filter).
|
||||
//
|
||||
// Only if requested.
|
||||
//
|
||||
// This argument MUST be omitted if the calculateTotal request argument is not true.
|
||||
Total uint `json:"total,omitempty,omitzero"`
|
||||
|
||||
// The limit enforced by the server on the maximum number of results to return (if set by the server).
|
||||
//
|
||||
// This is only returned if the server set a limit or used a different limit than that given in the request.
|
||||
Limit uint `json:"limit,omitempty,omitzero"`
|
||||
}
|
||||
|
||||
type ContactCardGetCommand struct {
|
||||
// The ids of the ContactCard objects to return.
|
||||
//
|
||||
// If null, then all records of the data type are returned, if this is supported for that
|
||||
// data type and the number of records does not exceed the maxObjectsInGet limit.
|
||||
Ids []string `json:"ids,omitempty"`
|
||||
|
||||
// The id of the account to use.
|
||||
AccountId string `json:"accountId"`
|
||||
|
||||
// If supplied, only the properties listed in the array are returned for each ContactCard object.
|
||||
//
|
||||
// The id property of the object is always returned, even if not explicitly requested.
|
||||
//
|
||||
// If an invalid property is requested, the call MUST be rejected with an invalidArguments error.
|
||||
Properties []string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type ContactCardGetRefCommand struct {
|
||||
// The ids of the ContactCard objects to return.
|
||||
//
|
||||
// If null, then all records of the data type are returned, if this is supported for that
|
||||
// data type and the number of records does not exceed the maxObjectsInGet limit.
|
||||
IdsRef *ResultReference `json:"#ids,omitempty"`
|
||||
|
||||
// The id of the account to use.
|
||||
AccountId string `json:"accountId"`
|
||||
|
||||
// If supplied, only the properties listed in the array are returned for each ContactCard object.
|
||||
//
|
||||
// The id property of the object is always returned, even if not explicitly requested.
|
||||
//
|
||||
// If an invalid property is requested, the call MUST be rejected with an invalidArguments error.
|
||||
Properties []string `json:"properties,omitempty"`
|
||||
}
|
||||
|
||||
type ContactCardGetResponse struct {
|
||||
// The id of the account used for the call.
|
||||
AccountId string `json:"accountId"`
|
||||
|
||||
// A (preferably short) string representing the state on the server for all the data of this type
|
||||
// in the account (not just the objects returned in this call).
|
||||
//
|
||||
// If the data changes, this string MUST change.
|
||||
// If the Email data is unchanged, servers SHOULD return the same state string on subsequent requests for this data type.
|
||||
State State `json:"state"`
|
||||
|
||||
// An array of the ContactCard objects requested.
|
||||
//
|
||||
// This is the empty array if no objects were found or if the ids argument passed in was also an empty array.
|
||||
//
|
||||
// The results MAY be in a different order to the ids in the request arguments.
|
||||
//
|
||||
// If an identical id is included more than once in the request, the server MUST only include it once in either
|
||||
// the list or the notFound argument of the response.
|
||||
List []jscontact.ContactCard `json:"list"`
|
||||
|
||||
// This array contains the ids passed to the method for records that do not exist.
|
||||
//
|
||||
// The array is empty if all requested ids were found or if the ids argument passed in was either null or an empty array.
|
||||
NotFound []any `json:"notFound"`
|
||||
}
|
||||
|
||||
type ContactCardUpdate map[string]any
|
||||
|
||||
type ContactCardSetCommand struct {
|
||||
// The id of the account to use.
|
||||
AccountId string `json:"accountId"`
|
||||
|
||||
// This is a state string as returned by the `ContactCard/get` method.
|
||||
//
|
||||
// If supplied, the string must match the current state; otherwise, the method will be aborted and a
|
||||
// `stateMismatch` error returned.
|
||||
//
|
||||
// If null, any changes will be applied to the current state.
|
||||
IfInState string `json:"ifInState,omitempty"`
|
||||
|
||||
// A map of a creation id (a temporary id set by the client) to ContactCard objects,
|
||||
// or null if no objects are to be created.
|
||||
//
|
||||
// The ContactCard object type definition may define default values for properties.
|
||||
//
|
||||
// Any such property may be omitted by the client.
|
||||
//
|
||||
// The client MUST omit any properties that may only be set by the server.
|
||||
Create map[string]jscontact.ContactCard `json:"create,omitempty"`
|
||||
|
||||
// A map of an id to a `Patch` object to apply to the current Email object with that id,
|
||||
// or null if no objects are to be updated.
|
||||
//
|
||||
// A `PatchObject` is of type `String[*]` and represents an unordered set of patches.
|
||||
//
|
||||
// The keys are a path in JSON Pointer Format [@!RFC6901], with an implicit leading `/` (i.e., prefix each key
|
||||
// with `/` before applying the JSON Pointer evaluation algorithm).
|
||||
//
|
||||
// All paths MUST also conform to the following restrictions; if there is any violation, the update
|
||||
// MUST be rejected with an `invalidPatch` error:
|
||||
// !- The pointer MUST NOT reference inside an array (i.e., you MUST NOT insert/delete from an array; the array MUST be replaced in its entirety instead).
|
||||
// !- All parts prior to the last (i.e., the value after the final slash) MUST already exist on the object being patched.
|
||||
// !- There MUST NOT be two patches in the `PatchObject` where the pointer of one is the prefix of the pointer of the other, e.g., `"alerts/1/offset"` and `"alerts"`.
|
||||
//
|
||||
// The value associated with each pointer determines how to apply that patch:
|
||||
// !- If null, set to the default value if specified for this property; otherwise, remove the property from the patched object. If the key is not present in the parent, this a no-op.
|
||||
// !- Anything else: The value to set for this property (this may be a replacement or addition to the object being patched).
|
||||
//
|
||||
// Any server-set properties MAY be included in the patch if their value is identical to the current server value
|
||||
// (before applying the patches to the object). Otherwise, the update MUST be rejected with an `invalidProperties` `SetError`.
|
||||
//
|
||||
// This patch definition is designed such that an entire Email object is also a valid `PatchObject`.
|
||||
//
|
||||
// The client may choose to optimise network usage by just sending the diff or may send the whole object; the server
|
||||
// processes it the same either way.
|
||||
Update map[string]ContactCardUpdate `json:"update,omitempty"`
|
||||
|
||||
// A list of ids for ContactCard objects to permanently delete, or null if no objects are to be destroyed.
|
||||
Destroy []string `json:"destroy,omitempty"`
|
||||
}
|
||||
|
||||
type ContactCardSetResponse struct {
|
||||
// The id of the account used for the call.
|
||||
AccountId string `json:"accountId"`
|
||||
|
||||
// The state string that would have been returned by ContactCard/get before making the
|
||||
// requested changes, or null if the server doesn’t know what the previous state
|
||||
// string was.
|
||||
OldState State `json:"oldState,omitempty"`
|
||||
|
||||
// The state string that will now be returned by Email/get.
|
||||
NewState State `json:"newState"`
|
||||
|
||||
// A map of the creation id to an object containing any properties of the created Email object
|
||||
// that were not sent by the client.
|
||||
//
|
||||
// This includes all server-set properties (such as the id in most object types) and any properties
|
||||
// that were omitted by the client and thus set to a default by the server.
|
||||
//
|
||||
// This argument is null if no ContactCard objects were successfully created.
|
||||
Created map[string]*jscontact.ContactCard `json:"created,omitempty"`
|
||||
|
||||
// The keys in this map are the ids of all Emails that were successfully updated.
|
||||
//
|
||||
// The value for each id is an ContactCard object containing any property that changed in a way not
|
||||
// explicitly requested by the PatchObject sent to the server, or null if none.
|
||||
//
|
||||
// This lets the client know of any changes to server-set or computed properties.
|
||||
//
|
||||
// This argument is null if no ContactCard objects were successfully updated.
|
||||
Updated map[string]*jscontact.ContactCard `json:"updated,omitempty"`
|
||||
|
||||
// A list of ContactCard ids for records that were successfully destroyed, or null if none.
|
||||
Destroyed []string `json:"destroyed,omitempty"`
|
||||
|
||||
// A map of the creation id to a SetError object for each record that failed to be created,
|
||||
// or null if all successful.
|
||||
NotCreated map[string]SetError `json:"notCreated,omitempty"`
|
||||
|
||||
// A map of the ContactCard id to a SetError object for each record that failed to be updated,
|
||||
// or null if all successful.
|
||||
NotUpdated map[string]SetError `json:"notUpdated,omitempty"`
|
||||
|
||||
// A map of the ContactCard id to a SetError object for each record that failed to be destroyed,
|
||||
// or null if all successful.
|
||||
NotDestroyed map[string]SetError `json:"notDestroyed,omitempty"`
|
||||
}
|
||||
|
||||
type ErrorResponse struct {
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description,omitempty"`
|
||||
@@ -4748,6 +5226,10 @@ const (
|
||||
CommandVacationResponseSet Command = "VacationResponse/set"
|
||||
CommandSearchSnippetGet Command = "SearchSnippet/get"
|
||||
CommandQuotaGet Command = "Quota/get"
|
||||
CommandAddressBookGet Command = "AddressBook/get"
|
||||
CommandContactCardQuery Command = "ContactCard/query"
|
||||
CommandContactCardGet Command = "ContactCard/get"
|
||||
CommandContactCardSet Command = "ContactCard/set"
|
||||
)
|
||||
|
||||
var CommandResponseTypeMap = map[Command]func() any{
|
||||
@@ -4770,4 +5252,8 @@ var CommandResponseTypeMap = map[Command]func() any{
|
||||
CommandVacationResponseSet: func() any { return VacationResponseSetResponse{} },
|
||||
CommandSearchSnippetGet: func() any { return SearchSnippetGetResponse{} },
|
||||
CommandQuotaGet: func() any { return QuotaGetResponse{} },
|
||||
CommandAddressBookGet: func() any { return AddressBookGetResponse{} },
|
||||
CommandContactCardQuery: func() any { return ContactCardQueryResponse{} },
|
||||
CommandContactCardGet: func() any { return ContactCardGetResponse{} },
|
||||
CommandContactCardSet: func() any { return ContactCardSetResponse{} },
|
||||
}
|
||||
|
||||
@@ -2129,7 +2129,7 @@ type ContactCard struct {
|
||||
// This is a JMAP extension and not part of [RFC9553].
|
||||
//
|
||||
// [RFC9553]: https://www.rfc-editor.org/rfc/rfc9553.html
|
||||
Id string `json:"id"`
|
||||
Id string `json:"id,omitempty"`
|
||||
|
||||
// The set of AddressBook ids this Card belongs to.
|
||||
//
|
||||
@@ -2142,7 +2142,7 @@ type ContactCard struct {
|
||||
// This is a JMAP extension and not part of [RFC9553].
|
||||
//
|
||||
// [RFC9553]: https://www.rfc-editor.org/rfc/rfc9553.html
|
||||
AddressBookIds map[string]bool `json:"addressBookIds"`
|
||||
AddressBookIds map[string]bool `json:"addressBookIds,omitempty"`
|
||||
|
||||
// The JSContact type of the Card object: the value MUST be "Card".
|
||||
Type TypeOfContactCard `json:"@type,omitempty"`
|
||||
@@ -2376,3 +2376,75 @@ type ContactCard struct {
|
||||
// The personal information of the entity represented by the Card.
|
||||
PersonalInfo map[string]PersonalInfo `json:"personalInfo,omitempty"`
|
||||
}
|
||||
|
||||
const (
|
||||
ContactCardPropertyId = "id"
|
||||
ContactCardPropertyAddressBookIds = "addressBookIds"
|
||||
ContactCardPropertyType = "@type"
|
||||
ContactCardPropertyVersion = "version"
|
||||
ContactCardPropertyCreated = "created"
|
||||
ContactCardPropertyKind = "kind"
|
||||
ContactCardPropertyLanguage = "language"
|
||||
ContactCardPropertyMembers = "members"
|
||||
ContactCardPropertyProdId = "prodId"
|
||||
ContactCardPropertyRelatedTo = "relatedTo"
|
||||
ContactCardPropertyUid = "uid"
|
||||
ContactCardPropertyUpdated = "updated"
|
||||
ContactCardPropertyName = "name"
|
||||
ContactCardPropertyNicknames = "nicknames"
|
||||
ContactCardPropertyOrganizations = "organizations"
|
||||
ContactCardPropertySpeakToAs = "speakToAs"
|
||||
ContactCardPropertyTitles = "titles"
|
||||
ContactCardPropertyEmails = "emails"
|
||||
ContactCardPropertyOnlineServices = "onlineServices"
|
||||
ContactCardPropertyPhones = "phones"
|
||||
ContactCardPropertyPreferredLanguages = "preferredLanguages"
|
||||
ContactCardPropertyCalendars = "calendars"
|
||||
ContactCardPropertySchedulingAddresses = "schedulingAddresses"
|
||||
ContactCardPropertyAddresses = "addresses"
|
||||
ContactCardPropertyCryptoKeys = "cryptoKeys"
|
||||
ContactCardPropertyDirectories = "directories"
|
||||
ContactCardPropertyLinks = "links"
|
||||
ContactCardPropertyMedia = "media"
|
||||
ContactCardPropertyLocalizations = "localizations"
|
||||
ContactCardPropertyAnniversaries = "anniversaries"
|
||||
ContactCardPropertyKeywords = "keywords"
|
||||
ContactCardPropertyNotes = "notes"
|
||||
ContactCardPropertyPersonalInfo = "personalInfo"
|
||||
)
|
||||
|
||||
var ContactCardProperties = []string{
|
||||
ContactCardPropertyId,
|
||||
ContactCardPropertyAddressBookIds,
|
||||
ContactCardPropertyType,
|
||||
ContactCardPropertyVersion,
|
||||
ContactCardPropertyCreated,
|
||||
ContactCardPropertyKind,
|
||||
ContactCardPropertyLanguage,
|
||||
ContactCardPropertyMembers,
|
||||
ContactCardPropertyProdId,
|
||||
ContactCardPropertyRelatedTo,
|
||||
ContactCardPropertyUid,
|
||||
ContactCardPropertyUpdated,
|
||||
ContactCardPropertyName,
|
||||
ContactCardPropertyNicknames,
|
||||
ContactCardPropertyOrganizations,
|
||||
ContactCardPropertySpeakToAs,
|
||||
ContactCardPropertyTitles,
|
||||
ContactCardPropertyEmails,
|
||||
ContactCardPropertyOnlineServices,
|
||||
ContactCardPropertyPhones,
|
||||
ContactCardPropertyPreferredLanguages,
|
||||
ContactCardPropertyCalendars,
|
||||
ContactCardPropertySchedulingAddresses,
|
||||
ContactCardPropertyAddresses,
|
||||
ContactCardPropertyCryptoKeys,
|
||||
ContactCardPropertyDirectories,
|
||||
ContactCardPropertyLinks,
|
||||
ContactCardPropertyMedia,
|
||||
ContactCardPropertyLocalizations,
|
||||
ContactCardPropertyAnniversaries,
|
||||
ContactCardPropertyKeywords,
|
||||
ContactCardPropertyNotes,
|
||||
ContactCardPropertyPersonalInfo,
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ type Mail struct {
|
||||
Timeout time.Duration `yaml:"timeout" env:"GROUPWARE_JMAP_TIMEOUT"`
|
||||
DefaultEmailLimit uint `yaml:"default_email_limit" env:"GROUPWARE_DEFAULT_EMAIL_LIMIT"`
|
||||
MaxBodyValueBytes uint `yaml:"max_body_value_bytes" env:"GROUPWARE_MAX_BODY_VALUE_BYTES"`
|
||||
DefaultContactLimit uint `yaml:"default_contact_limit" env:"GROUPWARE_DEFAULT_CONTACT_LIMIT"`
|
||||
ResponseHeaderTimeout time.Duration `yaml:"response_header_timeout" env:"GROUPWARE_RESPONSE_HEADER_TIMEOUT"`
|
||||
PushHandshakeTimeout time.Duration `yaml:"push_handshake_timeout" env:"GROUPWARE_PUSH_HANDSHAKE_TIMEOUT"`
|
||||
SessionCache MailSessionCache `yaml:"session_cache"`
|
||||
|
||||
@@ -33,6 +33,7 @@ func DefaultConfig() *config.Config {
|
||||
Timeout: 30 * time.Second,
|
||||
DefaultEmailLimit: uint(0),
|
||||
MaxBodyValueBytes: uint(0),
|
||||
DefaultContactLimit: uint(0),
|
||||
ResponseHeaderTimeout: 10 * time.Second,
|
||||
PushHandshakeTimeout: 10 * time.Second,
|
||||
SessionCache: config.MailSessionCache{
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"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"
|
||||
)
|
||||
|
||||
// When the request succeeds.
|
||||
@@ -30,10 +31,13 @@ func (g *Groupware) GetAddressbooks(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
return resp
|
||||
}
|
||||
var _ string = accountId
|
||||
|
||||
// TODO replace with proper implementation
|
||||
return response(AllAddressBooks, req.session.State, "")
|
||||
addressbooks, sessionState, lang, jerr := g.jmap.GetAddressbooks(accountId, req.session, req.ctx, req.logger, req.language(), nil)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
return response(addressbooks, sessionState, lang)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,16 +65,23 @@ func (g *Groupware) GetAddressbook(w http.ResponseWriter, r *http.Request) {
|
||||
if !ok {
|
||||
return resp
|
||||
}
|
||||
var _ string = accountId
|
||||
|
||||
l := req.logger.With()
|
||||
|
||||
addressBookId := chi.URLParam(r, UriParamAddressBookId)
|
||||
// TODO replace with proper implementation
|
||||
for _, ab := range AllAddressBooks {
|
||||
if ab.Id == addressBookId {
|
||||
return response(ab, req.session.State, "")
|
||||
}
|
||||
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
|
||||
|
||||
logger := log.From(l)
|
||||
addressbooks, sessionState, lang, jerr := g.jmap.GetAddressbooks(accountId, req.session, req.ctx, logger, req.language(), []string{addressBookId})
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
if len(addressbooks.NotFound) > 0 {
|
||||
return notFoundResponse(sessionState)
|
||||
} else {
|
||||
return response(addressbooks, sessionState, lang)
|
||||
}
|
||||
return notFoundResponse(req.session.State)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -96,14 +107,107 @@ func (g *Groupware) GetContactsInAddressbook(w http.ResponseWriter, r *http.Requ
|
||||
if !ok {
|
||||
return resp
|
||||
}
|
||||
var _ string = accountId
|
||||
|
||||
l := req.logger.With()
|
||||
|
||||
addressBookId := chi.URLParam(r, UriParamAddressBookId)
|
||||
// TODO replace with proper implementation
|
||||
contactCards, ok := ContactsMapByAddressBookId[addressBookId]
|
||||
if !ok {
|
||||
return notFoundResponse(req.session.State)
|
||||
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
|
||||
|
||||
offset, ok, err := req.parseUIntParam(QueryParamOffset, 0)
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Uint(QueryParamOffset, offset)
|
||||
}
|
||||
|
||||
limit, ok, err := req.parseUIntParam(QueryParamLimit, g.defaultContactLimit)
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
if ok {
|
||||
l = l.Uint(QueryParamLimit, limit)
|
||||
}
|
||||
|
||||
filter := jmap.ContactCardFilterCondition{
|
||||
InAddressBook: addressBookId,
|
||||
}
|
||||
sortBy := []jmap.ContactCardComparator{{Property: jscontact.ContactCardPropertyUpdated, IsAscending: false}}
|
||||
|
||||
logger := log.From(l)
|
||||
contactsByAccountId, sessionState, lang, jerr := g.jmap.QueryContactCards([]string{accountId}, req.session, req.ctx, logger, req.language(), filter, sortBy, offset, limit)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
if contacts, ok := contactsByAccountId[accountId]; ok {
|
||||
return response(contacts, req.session.State, lang)
|
||||
} else {
|
||||
return notFoundResponse(sessionState)
|
||||
}
|
||||
return response(contactCards, req.session.State, "")
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Groupware) CreateContact(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
ok, accountId, resp := req.needContactWithAccount()
|
||||
if !ok {
|
||||
return resp
|
||||
}
|
||||
|
||||
l := req.logger.With()
|
||||
|
||||
addressBookId := chi.URLParam(r, UriParamAddressBookId)
|
||||
l = l.Str(UriParamAddressBookId, log.SafeString(addressBookId))
|
||||
|
||||
var create jscontact.ContactCard
|
||||
err := req.body(&create)
|
||||
if err != nil {
|
||||
return errorResponse(err)
|
||||
}
|
||||
|
||||
logger := log.From(l)
|
||||
created, sessionState, lang, jerr := g.jmap.CreateContactCard(accountId, req.session, req.ctx, logger, req.language(), create)
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
return etagResponse(created.ContactCard, sessionState, created.State, lang)
|
||||
})
|
||||
}
|
||||
|
||||
func (g *Groupware) DeleteContact(w http.ResponseWriter, r *http.Request) {
|
||||
g.respond(w, r, func(req Request) Response {
|
||||
ok, accountId, resp := req.needContactWithAccount()
|
||||
if !ok {
|
||||
return resp
|
||||
}
|
||||
l := req.logger.With().Str(accountId, log.SafeString(accountId))
|
||||
|
||||
contactId := chi.URLParam(r, UriParamContactId)
|
||||
l.Str(UriParamContactId, log.SafeString(contactId))
|
||||
|
||||
logger := log.From(l)
|
||||
|
||||
deleted, sessionState, _, jerr := g.jmap.DeleteContactCard(accountId, []string{contactId}, req.session, req.ctx, logger, req.language())
|
||||
if jerr != nil {
|
||||
return req.errorResponseFromJmap(jerr)
|
||||
}
|
||||
|
||||
for _, e := range deleted.NotDestroyed {
|
||||
desc := e.Description
|
||||
if desc != "" {
|
||||
return errorResponseWithSessionState(apiError(
|
||||
req.errorId(),
|
||||
ErrorFailedToDeleteContact,
|
||||
withDetail(e.Description),
|
||||
), sessionState)
|
||||
} else {
|
||||
return errorResponseWithSessionState(apiError(
|
||||
req.errorId(),
|
||||
ErrorFailedToDeleteContact,
|
||||
), sessionState)
|
||||
}
|
||||
}
|
||||
return noContentResponseWithEtag(sessionState, deleted.State)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -197,6 +197,7 @@ const (
|
||||
ErrorCodeFailedToDeleteEmail = "DELEML"
|
||||
ErrorCodeFailedToDeleteSomeIdentities = "DELSID"
|
||||
ErrorCodeFailedToSanitizeEmail = "FSANEM"
|
||||
ErrorCodeFailedToDeleteContact = "DELCNT"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -434,6 +435,12 @@ var (
|
||||
Title: "Failed to sanitize an email",
|
||||
Detail: "Email content sanitization failed.",
|
||||
}
|
||||
ErrorFailedToDeleteContact = GroupwareError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Code: ErrorCodeFailedToDeleteContact,
|
||||
Title: "Failed to delete contacts",
|
||||
Detail: "One or more contacts could not be deleted.",
|
||||
}
|
||||
)
|
||||
|
||||
type ErrorOpt interface {
|
||||
|
||||
@@ -86,11 +86,12 @@ type Groupware struct {
|
||||
// unfortunately, the sse implementation does not provide such a function.
|
||||
// Key: the stream ID, which is the username
|
||||
// Value: the timestamp of the creation of the stream
|
||||
streams cmap.ConcurrentMap
|
||||
logger *log.Logger
|
||||
defaultEmailLimit uint
|
||||
maxBodyValueBytes uint
|
||||
sanitize bool
|
||||
streams cmap.ConcurrentMap
|
||||
logger *log.Logger
|
||||
defaultEmailLimit uint
|
||||
defaultContactLimit uint
|
||||
maxBodyValueBytes uint
|
||||
sanitize bool
|
||||
// Caches successful and failed Sessions by the username.
|
||||
sessionCache sessionCache
|
||||
jmap *jmap.Client
|
||||
@@ -176,6 +177,7 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
|
||||
defaultEmailLimit := max(config.Mail.DefaultEmailLimit, 0)
|
||||
maxBodyValueBytes := max(config.Mail.MaxBodyValueBytes, 0)
|
||||
defaultContactLimit := max(config.Mail.DefaultContactLimit, 0)
|
||||
responseHeaderTimeout := max(config.Mail.ResponseHeaderTimeout, 0)
|
||||
sessionCacheMaxCapacity := uint64(max(config.Mail.SessionCache.MaxCapacity, 0))
|
||||
sessionCacheTtl := max(config.Mail.SessionCache.Ttl, 0)
|
||||
@@ -332,20 +334,21 @@ func NewGroupware(config *config.Config, logger *log.Logger, mux *chi.Mux, prome
|
||||
}
|
||||
|
||||
g := &Groupware{
|
||||
mux: mux,
|
||||
metrics: m,
|
||||
sseServer: sseServer,
|
||||
streams: cmap.New(),
|
||||
logger: logger,
|
||||
sessionCache: sessionCache,
|
||||
userProvider: userProvider,
|
||||
jmap: &jmapClient,
|
||||
defaultEmailLimit: defaultEmailLimit,
|
||||
maxBodyValueBytes: maxBodyValueBytes,
|
||||
sanitize: sanitize,
|
||||
eventChannel: eventChannel,
|
||||
jobsChannel: jobsChannel,
|
||||
jobCounter: atomic.Uint64{},
|
||||
mux: mux,
|
||||
metrics: m,
|
||||
sseServer: sseServer,
|
||||
streams: cmap.New(),
|
||||
logger: logger,
|
||||
sessionCache: sessionCache,
|
||||
userProvider: userProvider,
|
||||
jmap: &jmapClient,
|
||||
defaultEmailLimit: defaultEmailLimit,
|
||||
defaultContactLimit: defaultContactLimit,
|
||||
maxBodyValueBytes: maxBodyValueBytes,
|
||||
sanitize: sanitize,
|
||||
eventChannel: eventChannel,
|
||||
jobsChannel: jobsChannel,
|
||||
jobCounter: atomic.Uint64{},
|
||||
}
|
||||
|
||||
for w := 1; w <= workerPoolSize; w++ {
|
||||
|
||||
@@ -395,10 +395,8 @@ func (r Request) needCalendarWithAccount() (bool, string, Response) {
|
||||
}
|
||||
|
||||
func (r Request) needContact() (bool, Response) {
|
||||
if !IgnoreSessionCapabilityChecks {
|
||||
if r.session.Capabilities.Contacts == nil {
|
||||
return false, errorResponseWithSessionState(r.apiError(&ErrorMissingContactsSessionCapability), r.session.State)
|
||||
}
|
||||
if r.session.Capabilities.Contacts == nil {
|
||||
return false, errorResponseWithSessionState(r.apiError(&ErrorMissingContactsSessionCapability), r.session.State)
|
||||
}
|
||||
return true, Response{}
|
||||
}
|
||||
@@ -411,10 +409,8 @@ func (r Request) needContactForAccount(accountId string) (bool, Response) {
|
||||
if !ok {
|
||||
return false, errorResponseWithSessionState(r.apiError(&ErrorAccountNotFound), r.session.State)
|
||||
}
|
||||
if !IgnoreSessionCapabilityChecks {
|
||||
if account.AccountCapabilities.Contacts == nil {
|
||||
return false, errorResponseWithSessionState(r.apiError(&ErrorMissingContactsAccountCapability), r.session.State)
|
||||
}
|
||||
if account.AccountCapabilities.Contacts == nil {
|
||||
return false, errorResponseWithSessionState(r.apiError(&ErrorMissingContactsAccountCapability), r.session.State)
|
||||
}
|
||||
return true, Response{}
|
||||
}
|
||||
@@ -424,10 +420,8 @@ func (r Request) needContactWithAccount() (bool, string, Response) {
|
||||
if err != nil {
|
||||
return false, "", errorResponse(err)
|
||||
}
|
||||
if !IgnoreSessionCapabilityChecks {
|
||||
if ok, resp := r.needContactForAccount(accountId); !ok {
|
||||
return false, accountId, resp
|
||||
}
|
||||
if ok, resp := r.needContactForAccount(accountId); !ok {
|
||||
return false, accountId, resp
|
||||
}
|
||||
return true, accountId, Response{}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ const (
|
||||
UriParamAddressBookId = "addressbookid"
|
||||
UriParamCalendarId = "calendarid"
|
||||
UriParamTaskListId = "tasklistid"
|
||||
UriParamContactId = "contactid"
|
||||
QueryParamMailboxSearchName = "name"
|
||||
QueryParamMailboxSearchRole = "role"
|
||||
QueryParamMailboxSearchSubscribed = "subscribed"
|
||||
@@ -115,6 +116,10 @@ func (g *Groupware) Route(r chi.Router) {
|
||||
r.Get("/", g.GetAddressbooks)
|
||||
r.Get("/{addressbookid}", g.GetAddressbook)
|
||||
r.Get("/{addressbookid}/contacts", g.GetContactsInAddressbook)
|
||||
r.Route("/contacts", func(r chi.Router) {
|
||||
r.Post("/", g.CreateContact)
|
||||
r.Delete("/{contactid}", g.DeleteContact)
|
||||
})
|
||||
})
|
||||
r.Route("/calendars", func(r chi.Router) {
|
||||
r.Get("/", g.GetCalendars)
|
||||
|
||||
Reference in New Issue
Block a user