ultisuite-backend/internal/api/mail/search_advanced.go
R3D347HR4Y 95196f7777 Add mail attachment and draft management features
- Introduced new functionality for managing email attachments and drafts in the mail API.
- Added handlers for listing, uploading, and downloading message attachments in `internal/api/mail/handlers_attachments.go`.
- Implemented draft management endpoints for creating, updating, and deleting drafts in `internal/api/mail/handlers_drafts.go`.
- Created new service methods for handling draft and attachment operations in `internal/api/mail/drafts.go` and `internal/api/mail/storage.go`.
- Added validation and error handling for draft and attachment operations.
- Included unit tests for draft and folder functionalities in `internal/api/mail/drafts_test.go` and `internal/api/mail/folders_test.go`.
- Updated API routes to support new draft and attachment features, enhancing overall mail management capabilities.
2026-05-22 17:14:36 +02:00

130 lines
3.5 KiB
Go

package mail
import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"github.com/ultisuite/ulti-backend/internal/api/query"
)
type MessageSearchFilter struct {
Query string
Sender string
DateFrom *time.Time
DateTo *time.Time
HasAttachments *bool
Label string
AccountID string
}
type MessageSearchResult struct {
Messages []map[string]any `json:"messages"`
Pagination query.PaginationMeta `json:"pagination,omitempty"`
}
func (s *Service) SearchMessages(ctx context.Context, externalID string, filter MessageSearchFilter, params query.ListParams) (MessageSearchResult, error) {
base := `
FROM messages m
JOIN mail_accounts ma ON m.account_id = ma.id
WHERE ma.user_id = (SELECT id FROM users WHERE external_id = $1)
`
args := []any{externalID}
argIdx := 2
if filter.AccountID != "" {
base += fmt.Sprintf(" AND m.account_id = $%d", argIdx)
args = append(args, filter.AccountID)
argIdx++
}
if filter.Sender != "" {
base += fmt.Sprintf(" AND m.from_addr::text ILIKE '%%' || $%d || '%%'", argIdx)
args = append(args, filter.Sender)
argIdx++
}
if filter.DateFrom != nil {
base += fmt.Sprintf(" AND m.date >= $%d", argIdx)
args = append(args, *filter.DateFrom)
argIdx++
}
if filter.DateTo != nil {
base += fmt.Sprintf(" AND m.date <= $%d", argIdx)
args = append(args, *filter.DateTo)
argIdx++
}
if filter.HasAttachments != nil {
base += fmt.Sprintf(" AND m.has_attachments = $%d", argIdx)
args = append(args, *filter.HasAttachments)
argIdx++
}
if filter.Label != "" {
base += fmt.Sprintf(" AND $%d = ANY(m.labels)", argIdx)
args = append(args, filter.Label)
argIdx++
}
if q := strings.TrimSpace(filter.Query); q != "" {
tsQuery := toMailTSQuery(q)
base += fmt.Sprintf(" AND m.search_vector @@ to_tsquery('simple', $%d)", argIdx)
args = append(args, tsQuery)
argIdx++
}
var total int64
if err := s.db.QueryRow(ctx, "SELECT COUNT(*) "+base, args...).Scan(&total); err != nil {
return MessageSearchResult{}, err
}
listQuery := `
SELECT m.id, m.message_id, m.thread_id, m.subject, m.from_addr, m.to_addrs,
m.date, m.snippet, m.flags, m.labels, m.has_attachments
` + base + fmt.Sprintf(" ORDER BY m.date DESC LIMIT $%d OFFSET $%d", argIdx, argIdx+1)
args = append(args, params.Limit(), params.Offset())
rows, err := s.db.Query(ctx, listQuery, args...)
if err != nil {
return MessageSearchResult{}, err
}
defer rows.Close()
messages := make([]map[string]any, 0)
for rows.Next() {
var id, messageID, subject, snippet string
var threadID *string
var fromAddr, toAddrs []byte
var date any
var flags, labels []string
var hasAttachments bool
if err := rows.Scan(&id, &messageID, &threadID, &subject, &fromAddr, &toAddrs, &date, &snippet, &flags, &labels, &hasAttachments); err != nil {
return MessageSearchResult{}, err
}
entry := map[string]any{
"id": id, "message_id": messageID, "subject": subject,
"from": json.RawMessage(fromAddr), "to": json.RawMessage(toAddrs),
"date": date, "snippet": snippet, "flags": flags, "labels": labels,
"has_attachments": hasAttachments,
}
if threadID != nil {
entry["thread_id"] = *threadID
}
messages = append(messages, entry)
}
if err := rows.Err(); err != nil {
return MessageSearchResult{}, err
}
return MessageSearchResult{
Messages: messages,
Pagination: params.Meta(&total),
}, nil
}
func toMailTSQuery(input string) string {
words := strings.Fields(input)
for i, w := range words {
words[i] = w + ":*"
}
return strings.Join(words, " & ")
}