Replace AWS SES with Resend for email delivery

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Andrew Gundersen 2026-03-01 22:08:26 -05:00
commit 4841986e0b
4 changed files with 33 additions and 43 deletions

View file

@ -49,8 +49,9 @@ func main() {
BaseDomain: getEnv("BASE_DOMAIN", "crimata.com"),
})
notifyClient := notify.NewClient(awsCfg, notify.Config{
FromEmail: getEnv("SES_FROM_EMAIL", "noreply@crimata.com"),
notifyClient := notify.NewClient(notify.Config{
ApiKey: mustEnv("RESEND_API_KEY"),
FromEmail: getEnv("MAILER_FROM", "hello@crimata.com"),
BaseDomain: getEnv("BASE_DOMAIN", "crimata.com"),
})

2
go.mod
View file

@ -8,10 +8,10 @@ require (
github.com/aws/aws-sdk-go-v2/service/ec2 v1.151.0
github.com/aws/aws-sdk-go-v2/service/route53 v1.40.0
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.0
github.com/aws/aws-sdk-go-v2/service/ses v1.22.0
github.com/aws/aws-sdk-go-v2/service/ssm v1.49.0
github.com/go-chi/chi/v5 v5.1.0
github.com/lib/pq v1.10.9
github.com/resend/resend-go/v2 v2.28.0
golang.org/x/crypto v0.48.0
)

11
go.sum
View file

@ -30,8 +30,6 @@ github.com/aws/aws-sdk-go-v2/service/route53 v1.40.0 h1:MRriK+ntpKpUc8RwcYJbc5W/
github.com/aws/aws-sdk-go-v2/service/route53 v1.40.0/go.mod h1:n6oZO1BbhPw2X46ObAjn8ol00kujRT+Y+Q9AnbrRUe0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.0 h1:r3o2YsgW9zRcIP3Q0WCmttFVhTuugeKIvT5z9xDspc0=
github.com/aws/aws-sdk-go-v2/service/s3 v1.53.0/go.mod h1:w2E4f8PUfNtyjfL6Iu+mWI96FGttE03z3UdNcUEC4tA=
github.com/aws/aws-sdk-go-v2/service/ses v1.22.0 h1:1HJMg3WpV9L003TOhH8eEQgkPnegOwCE3/j0Iqlmt0U=
github.com/aws/aws-sdk-go-v2/service/ses v1.22.0/go.mod h1:9MbVy83ucnbP+4D9lky3IeUBxjECR2bHq63yzkl+WKU=
github.com/aws/aws-sdk-go-v2/service/ssm v1.49.0 h1:EtNvvxv0m6aP4cbTyo43vBRXeTpyt8juyNPmgKSTyYs=
github.com/aws/aws-sdk-go-v2/service/ssm v1.49.0/go.mod h1:wzPAvA+afHPFlAMkCf80sg7bm7GbCuFX1INetlm9DAk=
github.com/aws/aws-sdk-go-v2/service/sso v1.19.0 h1:u6OkVDxtBPnxPkZ9/63ynEe+8kHbtS5IfaC4PzVxzWM=
@ -42,8 +40,9 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.27.0 h1:cjTRjh700H36MQ8M0LnDn33W3Jmw
github.com/aws/aws-sdk-go-v2/service/sts v1.27.0/go.mod h1:nXfOBMWPokIbOY+Gi7a1psWMSvskUCemZzI+SMB7Akc=
github.com/aws/smithy-go v1.20.1 h1:4SZlSlMr36UEqC7XOyRVb27XMeZubNcBNN+9IgEPIQw=
github.com/aws/smithy-go v1.20.1/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
@ -56,7 +55,11 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/resend/resend-go/v2 v2.28.0 h1:ttM1/VZR4fApBv3xI1TneSKi1pbfFsVrq7fXFlHKtj4=
github.com/resend/resend-go/v2 v2.28.0/go.mod h1:3YCb8c8+pLiqhtRFXTyFwlLvfjQtluxOr9HEh2BwCkQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
@ -66,3 +69,5 @@ golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -4,63 +4,47 @@ import (
"context"
"fmt"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/ses"
sestypes "github.com/aws/aws-sdk-go-v2/service/ses/types"
resend "github.com/resend/resend-go/v2"
)
type Config struct {
ApiKey string
FromEmail string
BaseDomain string
}
type Client struct {
ses *ses.Client
resend *resend.Client
cfg Config
}
func NewClient(awsCfg aws.Config, cfg Config) *Client {
func NewClient(cfg Config) *Client {
return &Client{
ses: ses.NewFromConfig(awsCfg),
resend: resend.NewClient(cfg.ApiKey),
cfg: cfg,
}
}
func (c *Client) SendWelcome(ctx context.Context, email, slug, password string) error {
func (c *Client) SendWelcome(_ context.Context, email, slug, password string) error {
url := fmt.Sprintf("https://%s.%s", slug, c.cfg.BaseDomain)
subject := "Your Crimata hub is ready"
text := fmt.Sprintf("Your hub is live at %s\n\nPassword: %s\n\nChange your password in Settings after logging in.\n\n— Crimata", url, password)
html := fmt.Sprintf(`<p>Your hub is live at <a href="%s">%s</a></p><p><strong>Password:</strong> <code>%s</code></p><p style="color:#555">Change your password in Settings after logging in.</p><p>— Crimata</p>`, url, url, password)
_, err := c.ses.SendEmail(ctx, &ses.SendEmailInput{
Source: aws.String(c.cfg.FromEmail),
Destination: &sestypes.Destination{ToAddresses: []string{email}},
Message: &sestypes.Message{
Subject: &sestypes.Content{Data: aws.String(subject)},
Body: &sestypes.Body{
Text: &sestypes.Content{Data: aws.String(text)},
Html: &sestypes.Content{Data: aws.String(html)},
},
},
_, err := c.resend.Emails.Send(&resend.SendEmailRequest{
From: c.cfg.FromEmail,
To: []string{email},
Subject: "Your Crimata hub is ready",
Text: fmt.Sprintf("Your hub is live at %s\n\nUsername: %s\nPassword: %s\n\nChange your password in Settings after logging in.\n\n— Crimata", url, slug, password),
Html: fmt.Sprintf(`<p>Your hub is live at <a href="%s">%s</a></p><p><strong>Username:</strong> <code>%s</code><br><strong>Password:</strong> <code>%s</code></p><p style="color:#555">Change your password in Settings after logging in.</p><p>— Crimata</p>`, url, url, slug, password),
})
return err
}
func (c *Client) SendDataExport(ctx context.Context, email, downloadURL string) error {
subject := "Your Crimata data export is ready"
text := fmt.Sprintf("Your data export is ready for download:\n\n%s\n\nThis link expires in 24 hours.\n\n— Crimata", downloadURL)
html := fmt.Sprintf(`<p>Your data export is ready: <a href="%s">Download</a></p><p style="color:#555">This link expires in 24 hours.</p><p>— Crimata</p>`, downloadURL)
_, err := c.ses.SendEmail(ctx, &ses.SendEmailInput{
Source: aws.String(c.cfg.FromEmail),
Destination: &sestypes.Destination{ToAddresses: []string{email}},
Message: &sestypes.Message{
Subject: &sestypes.Content{Data: aws.String(subject)},
Body: &sestypes.Body{
Text: &sestypes.Content{Data: aws.String(text)},
Html: &sestypes.Content{Data: aws.String(html)},
},
},
func (c *Client) SendDataExport(_ context.Context, email, downloadURL string) error {
_, err := c.resend.Emails.Send(&resend.SendEmailRequest{
From: c.cfg.FromEmail,
To: []string{email},
Subject: "Your Crimata data export is ready",
Text: fmt.Sprintf("Your data export is ready for download:\n\n%s\n\nThis link expires in 24 hours.\n\n— Crimata", downloadURL),
Html: fmt.Sprintf(`<p>Your data export is ready: <a href="%s">Download</a></p><p style="color:#555">This link expires in 24 hours.</p><p>— Crimata</p>`, downloadURL),
})
return err
}