]> Repos - crimata-infra/commitdiff
Replace AWS SES with Resend for email delivery
authorAndrew Gundersen <adgundersen@gmail.com>
Mon, 2 Mar 2026 03:08:26 +0000 (22:08 -0500)
committerAndrew Gundersen <adgundersen@gmail.com>
Mon, 2 Mar 2026 03:08:26 +0000 (22:08 -0500)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
cmd/server/main.go
go.mod
go.sum
internal/notify/notify.go

index ffbc4beb5ef1c20d3c6487d32f7eaada3d1367c3..3af8b027b2a657808cfa46da5e5737347c9a56b6 100644 (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"),
        })
 
diff --git a/go.mod b/go.mod
index f2ba217209890795a5553780f1307ec12f901d44..8a34ad4129209cdfe980f6d932344eba23bc9b78 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -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
 )
 
diff --git a/go.sum b/go.sum
index 42f08ff13cd7b43351197e84cd1a10c03d70a25d..217b05749a4f334a4e81ba6b38104a4441d3c165 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -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=
index 48bf1f16c00551ca5d7edee836732bacea46b02f..ebe67929785ee6b224c307d1b7ce821e726a046e 100644 (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
-       cfg Config
+       resend *resend.Client
+       cfg    Config
 }
 
-func NewClient(awsCfg aws.Config, cfg Config) *Client {
+func NewClient(cfg Config) *Client {
        return &Client{
-               ses: ses.NewFromConfig(awsCfg),
-               cfg: cfg,
+               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
 }