- 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.
123 lines
2.9 KiB
Go
123 lines
2.9 KiB
Go
package threading
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
var messageIDToken = regexp.MustCompile(`<[^>]+>`)
|
|
|
|
// NormalizeMessageID returns a canonical angle-bracket Message-ID when possible.
|
|
func NormalizeMessageID(raw string) string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return ""
|
|
}
|
|
if strings.HasPrefix(raw, "<") && strings.HasSuffix(raw, ">") {
|
|
return raw
|
|
}
|
|
return "<" + strings.Trim(raw, "<>") + ">"
|
|
}
|
|
|
|
// ParseMessageIDs extracts Message-IDs from a References or In-Reply-To header value.
|
|
func ParseMessageIDs(header string) []string {
|
|
header = strings.TrimSpace(header)
|
|
if header == "" {
|
|
return nil
|
|
}
|
|
matches := messageIDToken.FindAllString(header, -1)
|
|
if len(matches) == 0 {
|
|
if id := NormalizeMessageID(header); id != "" {
|
|
return []string{id}
|
|
}
|
|
return nil
|
|
}
|
|
seen := make(map[string]struct{}, len(matches))
|
|
out := make([]string, 0, len(matches))
|
|
for _, m := range matches {
|
|
id := NormalizeMessageID(m)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
out = append(out, id)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// BuildReferences returns the References chain for a reply (ancestors + parent).
|
|
func BuildReferences(parentMessageID string, parentReferences []string) []string {
|
|
parentMessageID = NormalizeMessageID(parentMessageID)
|
|
if parentMessageID == "" {
|
|
return nil
|
|
}
|
|
seen := make(map[string]struct{}, len(parentReferences)+1)
|
|
out := make([]string, 0, len(parentReferences)+1)
|
|
for _, ref := range parentReferences {
|
|
id := NormalizeMessageID(ref)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
out = append(out, id)
|
|
}
|
|
if _, ok := seen[parentMessageID]; !ok {
|
|
out = append(out, parentMessageID)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func candidateMessageIDs(inReplyTo string, references []string) []string {
|
|
seen := make(map[string]struct{})
|
|
var out []string
|
|
add := func(id string) {
|
|
id = NormalizeMessageID(id)
|
|
if id == "" {
|
|
return
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
return
|
|
}
|
|
seen[id] = struct{}{}
|
|
out = append(out, id)
|
|
}
|
|
add(inReplyTo)
|
|
for _, ref := range references {
|
|
add(ref)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// AssignThreadID picks an existing thread for the account or allocates a new one.
|
|
func AssignThreadID(ctx context.Context, db *pgxpool.Pool, accountID, inReplyTo string, references []string) (string, error) {
|
|
ids := candidateMessageIDs(inReplyTo, references)
|
|
if len(ids) > 0 {
|
|
var threadID *uuid.UUID
|
|
err := db.QueryRow(ctx, `
|
|
SELECT thread_id FROM messages
|
|
WHERE account_id = $1 AND message_id = ANY($2) AND thread_id IS NOT NULL
|
|
ORDER BY date ASC
|
|
LIMIT 1
|
|
`, accountID, ids).Scan(&threadID)
|
|
if err == nil && threadID != nil {
|
|
return threadID.String(), nil
|
|
}
|
|
if err != nil && !errors.Is(err, pgx.ErrNoRows) {
|
|
return "", err
|
|
}
|
|
}
|
|
return uuid.New().String(), nil
|
|
}
|