Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

1package email 2 3import ( 4 "net" 5 "net/mail" 6 "strings" 7 8 "github.com/resend/resend-go/v3" 9) 10 11type Email struct { 12 From string 13 Subject string 14 Text string 15 Html string 16 APIKey string 17} 18 19func SendEmail(email Email, recipients ...string) error { 20 client := resend.NewClient(email.APIKey) 21 _, err := client.Emails.Send(&resend.SendEmailRequest{ 22 From: email.From, 23 To: recipients, 24 Subject: email.Subject, 25 Text: email.Text, 26 Html: email.Html, 27 }) 28 return err 29} 30 31// AddNewsletterContact creates a global contact in Resend and adds them to the newsletter segment. 32func AddNewsletterContact(apiKey, segmentID, emailAddr string) error { 33 client := resend.NewClient(apiKey) 34 _, err := client.Contacts.Create(&resend.CreateContactRequest{ 35 Email: emailAddr, 36 }) 37 if err != nil { 38 return fmt.Errorf("error creating contact: %w", err) 39 } 40 _, err = client.Contacts.Segments.Add(&resend.AddContactSegmentRequest{ 41 SegmentId: segmentID, 42 Email: emailAddr, 43 }) 44 if err != nil { 45 return fmt.Errorf("error adding contact to newsletter segment: %w", err) 46 } 47 return nil 48} 49 50func IsValidEmail(email string) bool { 51 // Reject whitespace (ParseAddress normalizes it away) 52 if strings.ContainsAny(email, " \t\n\r") { 53 return false 54 } 55 56 // Use stdlib RFC 5322 parser 57 addr, err := mail.ParseAddress(email) 58 if err != nil { 59 return false 60 } 61 62 // Split email into local and domain parts 63 parts := strings.Split(addr.Address, "@") 64 domain := parts[1] 65 66 canonical := coalesceToCanonicalName(domain) 67 mx, err := net.LookupMX(canonical) 68 69 // Don't check err here; mx will only contain valid mx records, and we should 70 // only fallback to an implicit mx if there are no mx records defined (whether 71 // they are valid or not). 72 if len(mx) != 0 { 73 return true 74 } 75 76 if err != nil { 77 // If the domain resolves to an address, assume it's an implicit mx. 78 address, _ := net.LookupIP(canonical) 79 if len(address) != 0 { 80 return true 81 } 82 } 83 84 return false 85} 86 87func coalesceToCanonicalName(domain string) string { 88 canonical, err := net.LookupCNAME(domain) 89 if err != nil { 90 // net.LookupCNAME() returns an error if there is no cname record *and* no 91 // a/aaaa records, but there may still be mx records. 92 return domain 93 } 94 return canonical 95}