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/v2"
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
36func IsValidEmail(email string) bool {
37 // Reject whitespace (ParseAddress normalizes it away)
38 if strings.ContainsAny(email, " \t\n\r") {
39 return false
40 }
41
42 // Use stdlib RFC 5322 parser
43 addr, err := mail.ParseAddress(email)
44 if err != nil {
45 return false
46 }
47
48 // Split email into local and domain parts
49 parts := strings.Split(addr.Address, "@")
50 domain := parts[1]
51
52 canonical := coalesceToCanonicalName(domain)
53 mx, err := net.LookupMX(canonical)
54
55 // Don't check err here; mx will only contain valid mx records, and we should
56 // only fallback to an implicit mx if there are no mx records defined (whether
57 // they are valid or not).
58 if len(mx) != 0 {
59 return true
60 }
61
62 if err != nil {
63 // If the domain resolves to an address, assume it's an implicit mx.
64 address, _ := net.LookupIP(canonical)
65 if len(address) != 0 {
66 return true
67 }
68 }
69
70 return false
71}
72
73func coalesceToCanonicalName(domain string) string {
74 canonical, err := net.LookupCNAME(domain)
75 if err != nil {
76 // net.LookupCNAME() returns an error if there is no cname record *and* no
77 // a/aaaa records, but there may still be mx records.
78 return domain
79 }
80 return canonical
81}