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