- 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.
145 lines
3.7 KiB
Go
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
|
|
}
|