ultisuite-backend/internal/contacts/discovery/signatures_loader.go
R3D347HR4Y 556d5f416d Enhance API and configuration for contact discovery and public sharing
- Introduced new endpoints for contact discovery, including scanning, listing, and managing discovered contacts.
- Implemented retry logic for handling missing DAV credentials during contact operations.
- Added public share functionality for drive API, allowing users to manage public shares, including upload, delete, and rename operations.
- Updated Nextcloud configuration to support public share links and improved error handling for public share permissions.
- Enhanced logging and validation across contact and drive APIs for better error tracking and user feedback.
- Added tests for new contact matching and ranking functionalities to ensure accuracy and reliability.
2026-06-06 20:27:02 +02:00

145 lines
3.7 KiB
Go

package discovery
import (
"context"
"sort"
"time"
)
func (s *Service) fillSignaturesFromStoredMessages(ctx context.Context, email string, agg *addressAgg) {
if len(agg.fromMessageRefs) == 0 {
return
}
ids := make([]string, 0, len(agg.fromMessageRefs))
for _, ref := range agg.fromMessageRefs {
ids = append(ids, ref.id)
}
rows, err := s.db.Query(ctx, `
SELECT id::text,
LEFT(COALESCE(body_text, ''), $2),
LEFT(COALESCE(body_html, ''), $2),
date
FROM messages
WHERE id = ANY($1::uuid[])
ORDER BY date DESC
LIMIT $3
`, ids, maxBodyChars, maxSignatureMsgsPerEmail)
if err != nil {
return
}
defer rows.Close()
agg.Signatures = agg.Signatures[:0]
for rows.Next() {
var id, bodyText, bodyHTML string
var msgDate time.Time
if err := rows.Scan(&id, &bodyText, &bodyHTML, &msgDate); err != nil {
continue
}
sigText, conf := extractSignature(bodyText, bodyHTML, email, agg.DisplayName)
if sigText == "" {
continue
}
agg.Signatures = append(agg.Signatures, signatureCandidate{
MessageID: id,
SignatureText: sigText,
MessageDate: msgDate,
Confidence: conf,
})
}
sort.Slice(agg.Signatures, func(i, j int) bool {
return agg.Signatures[i].MessageDate.After(agg.Signatures[j].MessageDate)
})
if len(agg.Signatures) > 5 {
agg.Signatures = agg.Signatures[:5]
}
}
func (s *Service) fillSignaturesForEmail(ctx context.Context, externalUserID, email string, agg *addressAgg) {
if len(agg.fromMessageRefs) > 0 {
s.fillSignaturesFromStoredMessages(ctx, email, agg)
return
}
rows, err := s.db.Query(ctx, `
SELECT m.id::text,
LEFT(COALESCE(m.body_text, ''), $3),
LEFT(COALESCE(m.body_html, ''), $3),
m.date
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)
AND EXISTS (
SELECT 1 FROM jsonb_array_elements(m.from_addr) a
WHERE lower(coalesce(a->>'address', '')) = lower($2)
)
ORDER BY m.date DESC
LIMIT $4
`, externalUserID, email, maxBodyChars, maxSignatureMsgsPerEmail)
if err != nil {
return
}
defer rows.Close()
for rows.Next() {
var id, bodyText, bodyHTML string
var msgDate time.Time
if err := rows.Scan(&id, &bodyText, &bodyHTML, &msgDate); err != nil {
continue
}
sigText, conf := extractSignature(bodyText, bodyHTML, email, agg.DisplayName)
if sigText == "" {
continue
}
agg.Signatures = append(agg.Signatures, signatureCandidate{
MessageID: id,
SignatureText: sigText,
MessageDate: msgDate,
Confidence: conf,
})
}
sort.Slice(agg.Signatures, func(i, j int) bool {
return agg.Signatures[i].MessageDate.After(agg.Signatures[j].MessageDate)
})
if len(agg.Signatures) > 5 {
agg.Signatures = agg.Signatures[:5]
}
}
type enrichCandidate struct {
email string
agg *addressAgg
}
func selectPreEnrichCandidates(aggs map[string]*addressAgg, limit int) []enrichCandidate {
var candidates []enrichCandidate
for email, agg := range aggs {
isML, isDisp, isSpamHeavy, _ := classifyAddress(agg)
if isML || isDisp || isSpamHeavy {
continue
}
if agg.MessageCount < minMessagesForLLM {
continue
}
if _, ok := agg.Roles["from"]; !ok {
continue
}
candidates = append(candidates, enrichCandidate{email: email, agg: agg})
}
sort.Slice(candidates, func(i, j int) bool {
return candidates[i].agg.MessageCount > candidates[j].agg.MessageCount
})
if len(candidates) > limit {
candidates = candidates[:limit]
}
return candidates
}
func enrichCandidateSet(candidates []enrichCandidate) map[string]bool {
out := make(map[string]bool, len(candidates))
for _, c := range candidates {
out[c.email] = true
}
return out
}