package discovery import ( "context" "sort" "strings" "github.com/google/uuid" ) var consumerMailDomains = map[string]struct{}{ "gmail.com": {}, "googlemail.com": {}, "outlook.com": {}, "hotmail.com": {}, "live.com": {}, "msn.com": {}, "yahoo.com": {}, "yahoo.fr": {}, "icloud.com": {}, "me.com": {}, "mac.com": {}, "proton.me": {}, "protonmail.com": {}, "aol.com": {}, "gmx.com": {}, "gmx.fr": {}, "mail.com": {}, "zoho.com": {}, "yandex.com": {}, "yandex.ru": {}, "fastmail.com": {}, } func isConsumerMailDomain(domain string) bool { domain = strings.ToLower(strings.TrimSpace(domain)) if domain == "" { return true } if _, ok := consumerMailDomains[domain]; ok { return true } return false } func personGroupKey(p Profile) string { if p.EnrichedData != nil { fn := strings.TrimSpace(p.EnrichedData.FirstName) ln := strings.TrimSpace(p.EnrichedData.LastName) if fn != "" || ln != "" { return strings.ToLower(strings.TrimSpace(fn + " " + ln)) } } dn := strings.TrimSpace(p.DisplayName) if dn != "" && !strings.Contains(dn, "@") { return strings.ToLower(dn) } return "" } func groupProfiles(profiles []Profile) []ProfileGroup { if len(profiles) == 0 { return nil } buckets := map[string][]Profile{} var solo []Profile for _, p := range profiles { key := personGroupKey(p) if key == "" { solo = append(solo, p) continue } buckets[key] = append(buckets[key], p) } var groups []ProfileGroup for key, list := range buckets { if len(list) == 1 { solo = append(solo, list[0]) continue } groups = append(groups, mergeProfileGroup(key, list)) } for _, p := range solo { groups = append(groups, mergeProfileGroup(p.ID, []Profile{p})) } sort.Slice(groups, func(i, j int) bool { return compareProfileGroupsByInteraction(groups[i], groups[j]) }) return groups } func mergeProfileGroup(key string, profiles []Profile) ProfileGroup { sort.Slice(profiles, func(i, j int) bool { return compareProfilesByInteraction(profiles[i], profiles[j]) }) primary := profiles[0] ids := make([]string, 0, len(profiles)) emailSeen := map[string]struct{}{} var allEmails []EmailEntry accountMap := map[string]*AccountDetection{} totalMessages := 0 for _, p := range profiles { ids = append(ids, p.ID) totalMessages += p.MessageCount for _, e := range p.AllEmails { low := strings.ToLower(strings.TrimSpace(e.Email)) if low == "" { continue } if _, ok := emailSeen[low]; ok { continue } emailSeen[low] = struct{}{} allEmails = append(allEmails, e) } if _, ok := emailSeen[strings.ToLower(primary.PrimaryEmail)]; !ok { emailSeen[strings.ToLower(primary.PrimaryEmail)] = struct{}{} } for _, a := range p.DetectedInAccounts { hit, ok := accountMap[a.AccountID] if !ok { cp := a accountMap[a.AccountID] = &cp continue } hit.MessageCount += a.MessageCount } } var accounts []AccountDetection for _, a := range accountMap { accounts = append(accounts, *a) } sort.Slice(accounts, func(i, j int) bool { return accounts[i].MessageCount > accounts[j].MessageCount }) merged := primary merged.AllEmails = allEmails merged.DetectedInAccounts = accounts merged.MessageCount = totalMessages merged.AllEmails = allEmails merged.DetectedInAccounts = accounts merged.MessageCount = totalMessages return ProfileGroup{ GroupKey: key, ProfileIDs: ids, Profile: merged, Profiles: profiles, DisplayName: profileDisplayName(merged), PrimaryEmail: merged.PrimaryEmail, MessageCount: totalMessages, } } func profileDisplayName(p Profile) string { if p.EnrichedData != nil { fn := strings.TrimSpace(p.EnrichedData.FirstName) ln := strings.TrimSpace(p.EnrichedData.LastName) if fn != "" || ln != "" { return strings.TrimSpace(fn + " " + ln) } } if strings.TrimSpace(p.DisplayName) != "" { return strings.TrimSpace(p.DisplayName) } return p.PrimaryEmail } func (s *Service) assignPersonGroups(ctx context.Context, externalUserID string) error { profiles, err := s.ListProfilesByStatus(ctx, externalUserID, ProfileSuggested) if err != nil { return err } groups := groupProfiles(profiles) for _, g := range groups { if len(g.ProfileIDs) < 2 { continue } groupID := uuid.NewString() for _, id := range g.ProfileIDs { _, _ = s.db.Exec(ctx, ` UPDATE contact_discovered_profiles SET person_group_id = $3::uuid, updated_at = NOW() WHERE id = $2::uuid AND user_id = (SELECT id FROM users WHERE external_id = $1) `, externalUserID, id, groupID) } } return nil }