ultisuite-backend/internal/api/mail/handlers_search.go
R3D347HR4Y cd0a80f5e8 huhu
2026-05-25 13:52:27 +02:00

124 lines
3.2 KiB
Go

package mail
import (
"net/http"
"net/url"
"strings"
"time"
"github.com/ultisuite/ulti-backend/internal/api/apiresponse"
"github.com/ultisuite/ulti-backend/internal/api/apivalidate"
"github.com/ultisuite/ulti-backend/internal/api/middleware"
"github.com/ultisuite/ulti-backend/internal/api/query"
)
func (h *Handler) SearchMessages(w http.ResponseWriter, r *http.Request) {
claims := middleware.ClaimsFromContext(r.Context())
params, err := query.ParseList(stripNonDateListRangeKeys(r.URL.Query()))
if err != nil {
apivalidate.WriteQueryError(w, r, err)
return
}
filter, verr := parseMessageSearchFilter(r)
if verr != nil {
apivalidate.WriteValidationError(w, r, verr)
return
}
result, err := h.svc.SearchMessages(r.Context(), claims.Sub, filter, params)
if err != nil {
h.logger.Error("search messages", "error", err)
apivalidate.WriteInternal(w, r)
return
}
apiresponse.WriteJSON(w, http.StatusOK, result)
}
func parseMessageSearchFilter(r *http.Request) (MessageSearchFilter, *apivalidate.ValidationError) {
q := r.URL.Query()
filter := MessageSearchFilter{
Query: q.Get("q"),
Sender: parseSearchSender(q),
Label: q.Get("label"),
AccountID: q.Get("account_id"),
}
if raw := q.Get("date_from"); raw != "" {
t, err := time.Parse(time.RFC3339, raw)
if err != nil {
return filter, apivalidate.NewValidationError(apivalidate.FieldDetail{
Field: "date_from", Message: "invalid RFC3339 datetime",
})
}
filter.DateFrom = &t
}
if raw := q.Get("date_to"); raw != "" {
t, err := time.Parse(time.RFC3339, raw)
if err != nil {
return filter, apivalidate.NewValidationError(apivalidate.FieldDetail{
Field: "date_to", Message: "invalid RFC3339 datetime",
})
}
filter.DateTo = &t
}
if raw := q.Get("has_attachment"); raw != "" {
switch raw {
case "true", "1":
v := true
filter.HasAttachments = &v
case "false", "0":
v := false
filter.HasAttachments = &v
default:
return filter, apivalidate.NewValidationError(apivalidate.FieldDetail{
Field: "has_attachment", Message: "must be true or false",
})
}
}
if filter.Query == "" && filter.Sender == "" && filter.Label == "" &&
filter.AccountID == "" && filter.DateFrom == nil && filter.DateTo == nil &&
filter.HasAttachments == nil {
return filter, apivalidate.NewValidationError(apivalidate.FieldDetail{
Field: "q", Message: "at least one search filter required",
})
}
return filter, nil
}
// stripNonDateListRangeKeys removes from/to when they are sender/recipient filters,
// not YYYY-MM-DD list date bounds (shared param names on /mail/search).
func stripNonDateListRangeKeys(values url.Values) url.Values {
out := values
clone := make(url.Values, len(values))
for k, vv := range values {
clone[k] = append([]string(nil), vv...)
}
out = clone
for _, key := range []string{"from", "to"} {
raw := strings.TrimSpace(out.Get(key))
if raw == "" {
continue
}
if _, err := query.ParseDate(raw); err != nil {
out.Del(key)
}
}
return out
}
func parseSearchSender(q url.Values) string {
if s := strings.TrimSpace(q.Get("sender")); s != "" {
return s
}
from := strings.TrimSpace(q.Get("from"))
if from == "" {
return ""
}
if _, err := query.ParseDate(from); err != nil {
return from
}
return ""
}