package main import ( "errors" "net/mail" "strings" "time" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Contact struct { gorm.Model Firstname string Lastname string Association string Job string DisplayImageID uint DisplayImage DisplayImage Emails []EmailAddress PhoneNumbers []PhoneNumber Urls []URL StreetAddresses []StreetAddress Dates []Date Other []OtherField Notes string } func (contact Contact) Validate() error { if len(strings.TrimSpace(contact.Firstname)) == 0 && len(strings.TrimSpace(contact.Lastname)) == 0 && len(contact.Emails) == 0 { return errors.New("Not a valid contact") } return nil } func (app *App) GetUserContact() Contact { return app.UserContact } func (app *App) NewContact() Contact { return Contact{} } func (app *App) SaveContact(contact Contact) error { if err := contact.Validate(); err != nil { return err } if contact.ID == 1 { app.Log("Updating user contact."). Stdout(). StatusLine() app.UserContact = contact } return app.DB. Session(&gorm.Session{FullSaveAssociations: true}). Save(&contact).Error } func (app *App) GetAllContactsSorted() ([]Contact, error) { var contacts []Contact app.Log("Getting contacts..."). Stdout(). StatusLine() err := app.DB. Preload(clause.Associations). Order("lastname ASC, firstname ASC, association ASC"). Find(&contacts).Error app.Log("found %d contacts.", len(contacts)). Stdout(). StatusLine() return contacts, err } type EmailAddress struct { gorm.Model ContactID uint Key string Value string } func (app *App) GetContactFromEmail(email EmailAddress) Contact { var contact Contact app.DB.Preload(clause.Associations).First(&contact, email.ContactID) return contact } func (app *App) GetContactsFromEmails(emails []EmailAddress) ([]Contact, error) { if len(emails) == 0 { return nil, nil } var emailIDs []uint var emailValues []string seen := make(map[string]struct{}) for _, e := range emails { if _, ok := seen[e.Value]; !ok { emailIDs = append(emailIDs, e.ID) emailValues = append(emailValues, e.Value) seen[e.Value] = struct{}{} } } var contacts []Contact err := app.DB. Preload(clause.Associations). Where("EXISTS (SELECT 1 FROM email_addresses WHERE contact_id = contacts.id AND (id IN ? OR value IN ?))", emailIDs, emailValues). Find(&contacts).Error if err != nil { return nil, err } return contacts, nil } func (app *App) GetOrCreateEmailAddress(rawEmail string) (EmailAddress, error) { addr, err := mail.ParseAddress(rawEmail) if err != nil { addr = &mail.Address{Address: rawEmail} } var existing EmailAddress result := app.DB.Where("value = ?", addr.Address).First(&existing) if result.Error == nil { return existing, nil } if !errors.Is(result.Error, gorm.ErrRecordNotFound) { return existing, result.Error } newContact := Contact{ Emails: []EmailAddress{{ Key: "Personal", Value: addr.Address, }}, } if err := app.DB.Session(&gorm.Session{FullSaveAssociations: true}).Create(&newContact).Error; err != nil { return existing, err } return newContact.Emails[0], nil } func (app *App) GetRecipientDisplay(recipients []EmailAddress) []string { if len(recipients) == 0 { return nil } var emailIDs []uint var emailValues []string seen := make(map[string]struct{}) for _, r := range recipients { if _, ok := seen[r.Value]; !ok { emailIDs = append(emailIDs, r.ID) emailValues = append(emailValues, r.Value) seen[r.Value] = struct{}{} } } var contacts []Contact app.DB. Preload("Emails"). Where("EXISTS (SELECT 1 FROM email_addresses WHERE contact_id = contacts.id AND (id IN ? OR value IN ?))", emailIDs, emailValues). Find(&contacts) emailToContact := make(map[string]*Contact) for i := range contacts { c := &contacts[i] for _, e := range c.Emails { emailToContact[e.Value] = c } } contactCount := make(map[uint]int) for _, r := range recipients { if c := emailToContact[r.Value]; c != nil { contactCount[c.ID]++ } } result := make([]string, len(recipients)) for i, r := range recipients { c := emailToContact[r.Value] if c == nil { result[i] = r.Value continue } name := strings.TrimSpace(c.Firstname + " " + c.Lastname) if name == "" { name = c.Association } if name != "" && contactCount[c.ID] > 1 { result[i] = name + " <" + r.Value + ">" } else if name != "" { result[i] = name } else { result[i] = "<" + r.Value + ">" } } return result } type PhoneNumber struct { gorm.Model ContactID uint Key string Value string } type URL struct { gorm.Model ContactID uint Key string Value string } type StreetAddress struct { gorm.Model ContactID uint Key string Street1 string Street2 string City string State string ZipCode string Country string } type Date struct { gorm.Model ContactID uint Key string Value time.Time } type OtherField struct { gorm.Model ContactID uint Key string Value string }