- Updated the Contacts API to support contact synchronization with incremental updates using sync tokens. - Added functionality for merging duplicate contacts on the server side. - Introduced new endpoints for enriching contact interactions, including mail, meetings, and files. - Implemented ETag support for contact updates to ensure data integrity. - Enhanced validation for sync tokens and interaction queries. - Updated project checklist to reflect the completion of Contacts API enhancements.
100 lines
2.6 KiB
Go
100 lines
2.6 KiB
Go
package contacts
|
|
|
|
import (
|
|
"net/mail"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
|
|
"github.com/ultisuite/ulti-backend/internal/nextcloud"
|
|
)
|
|
|
|
const (
|
|
maxRequestBody = 64 << 10
|
|
maxSyncTokenLen = 8192
|
|
)
|
|
|
|
func validateSyncToken(raw string) (string, *apivalidate.ValidationError) {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return "", nil
|
|
}
|
|
if len(raw) > maxSyncTokenLen {
|
|
return "", apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "sync_token", Message: "too long",
|
|
})
|
|
}
|
|
if strings.ContainsAny(raw, "\r\n\x00") || strings.ContainsAny(raw, "<>&") {
|
|
return "", apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "sync_token", Message: "invalid",
|
|
})
|
|
}
|
|
return raw, nil
|
|
}
|
|
|
|
func validateCreateContact(contact *nextcloud.Contact) *apivalidate.ValidationError {
|
|
if strings.TrimSpace(contact.FullName) == "" {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "full_name", Message: "required",
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateIfMatch(ifMatch string) *apivalidate.ValidationError {
|
|
if strings.TrimSpace(ifMatch) == "" {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "If-Match", Message: "required",
|
|
})
|
|
}
|
|
if strings.ContainsAny(ifMatch, "\r\n") {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "If-Match", Message: "invalid",
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateDeletePath(path string) *apivalidate.ValidationError {
|
|
if strings.TrimSpace(path) == "" {
|
|
return apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "path", Message: "required",
|
|
})
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateInteractionQuery(emailRaw, limitRaw string) (string, int, *apivalidate.ValidationError) {
|
|
email := strings.TrimSpace(emailRaw)
|
|
if email == "" {
|
|
return "", 0, apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "email", Message: "required",
|
|
})
|
|
}
|
|
if len(email) > 320 || strings.ContainsAny(email, "\r\n") {
|
|
return "", 0, apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "email", Message: "invalid",
|
|
})
|
|
}
|
|
parsed, err := mail.ParseAddress(email)
|
|
if err != nil || parsed.Address == "" {
|
|
return "", 0, apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "email", Message: "invalid",
|
|
})
|
|
}
|
|
|
|
limit := 20
|
|
raw := strings.TrimSpace(limitRaw)
|
|
if raw != "" {
|
|
parsedLimit, err := strconv.Atoi(raw)
|
|
if err != nil || parsedLimit < 1 || parsedLimit > 100 {
|
|
return "", 0, apivalidate.NewValidationError(apivalidate.FieldDetail{
|
|
Field: "limit", Message: "must be between 1 and 100",
|
|
})
|
|
}
|
|
limit = parsedLimit
|
|
}
|
|
|
|
return parsed.Address, limit, nil
|
|
}
|