remove resend and use custom email client

This commit is contained in:
juancwu 2026-01-02 18:22:47 -05:00
commit 9fe6a6beb1
6 changed files with 180 additions and 74 deletions

View file

@ -14,7 +14,11 @@ JWT_EXPIRY=168h
MAILER_SMTP_HOST= MAILER_SMTP_HOST=
MAILER_SMTP_PORT= MAILER_SMTP_PORT=
MAILER_IMAP_HOST=
MAILER_IMAP_PORT=
MAILER_USERNAME= MAILER_USERNAME=
MAILER_PASSWORD= MAILER_PASSWORD=
MAILER_EMAIL_FROM= MAILER_EMAIL_FROM=
MAILER_ENVELOPE_FROM= MAILER_ENVELOPE_FROM=
MAILER_SUPPORT_EMAIL=
MAILER_SUPPORT_ENVELOPE_FROM=

8
go.mod
View file

@ -6,12 +6,13 @@ require (
github.com/Oudwins/tailwind-merge-go v0.2.1 github.com/Oudwins/tailwind-merge-go v0.2.1
github.com/a-h/templ v0.3.960 github.com/a-h/templ v0.3.960
github.com/alexedwards/argon2id v1.0.0 github.com/alexedwards/argon2id v1.0.0
github.com/emersion/go-imap v1.2.1
github.com/golang-jwt/jwt/v5 v5.2.2 github.com/golang-jwt/jwt/v5 v5.2.2
github.com/jackc/pgx/v5 v5.7.6 github.com/jackc/pgx/v5 v5.7.6
github.com/jmoiron/sqlx v1.4.0 github.com/jmoiron/sqlx v1.4.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/pressly/goose/v3 v3.26.0 github.com/pressly/goose/v3 v3.26.0
github.com/resend/resend-go/v2 v2.28.0 github.com/wneessen/go-mail v0.7.2
modernc.org/sqlite v1.40.1 modernc.org/sqlite v1.40.1
) )
@ -28,6 +29,7 @@ require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/elastic/go-sysinfo v1.15.4 // indirect github.com/elastic/go-sysinfo v1.15.4 // indirect
github.com/elastic/go-windows v1.0.2 // indirect github.com/elastic/go-windows v1.0.2 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
github.com/fatih/color v1.18.0 // indirect github.com/fatih/color v1.18.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-faster/city v1.0.1 // indirect github.com/go-faster/city v1.0.1 // indirect
@ -70,9 +72,9 @@ require (
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b // indirect
golang.org/x/mod v0.27.0 // indirect golang.org/x/mod v0.27.0 // indirect
golang.org/x/net v0.43.0 // indirect golang.org/x/net v0.43.0 // indirect
golang.org/x/sync v0.16.0 // indirect golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect golang.org/x/sys v0.36.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.29.0 // indirect
golang.org/x/tools v0.36.0 // indirect golang.org/x/tools v0.36.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect
google.golang.org/grpc v1.62.1 // indirect google.golang.org/grpc v1.62.1 // indirect

18
go.sum
View file

@ -59,6 +59,12 @@ github.com/elastic/go-sysinfo v1.15.4/go.mod h1:ZBVXmqS368dOn/jvijV/zHLfakWTYHBZ
github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU= github.com/elastic/go-windows v1.0.0/go.mod h1:TsU0Nrp7/y3+VwE82FoZF8gC/XFg/Elz6CcloAxnPgU=
github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI= github.com/elastic/go-windows v1.0.2 h1:yoLLsAsV5cfg9FLhZ9EXZ2n2sQFKeDYrHenkcivY4vI=
github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8= github.com/elastic/go-windows v1.0.2/go.mod h1:bGcDpBzXgYSqM0Gx3DM4+UxFj300SZLixie9u9ixLM8=
github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@ -191,8 +197,6 @@ github.com/rekby/fixenv v0.6.1 h1:jUFiSPpajT4WY2cYuc++7Y1zWrnCxnovGCIX72PZniM=
github.com/rekby/fixenv v0.6.1/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c= github.com/rekby/fixenv v0.6.1/go.mod h1:/b5LRc06BYJtslRtHKxsPWFT/ySpHV+rWvzTg+XWk4c=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
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/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
@ -218,6 +222,8 @@ github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d h1:
github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s= github.com/tursodatabase/libsql-client-go v0.0.0-20240902231107-85af5b9d094d/go.mod h1:l8xTsYB90uaVdMHXMCxKKLSgw5wLYBwBKKefNIUnm9s=
github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw= github.com/vertica/vertica-sql-go v1.3.3 h1:fL+FKEAEy5ONmsvya2WH5T8bhkvY27y/Ik3ReR2T+Qw=
github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4= github.com/vertica/vertica-sql-go v1.3.3/go.mod h1:jnn2GFuv+O2Jcjktb7zyc4Utlbu9YVqpHH/lx63+1M4=
github.com/wneessen/go-mail v0.7.2 h1:xxPnhZ6IZLSgxShebmZ6DPKh1b6OJcoHfzy7UjOkzS8=
github.com/wneessen/go-mail v0.7.2/go.mod h1:+TkW6QP3EVkgTEqHtVmnAE/1MRhmzb8Y9/W3pweuS+k=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g= github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8= github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
@ -291,8 +297,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -322,8 +328,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=

View file

@ -29,13 +29,13 @@ func New(cfg *config.Config) (*App, error) {
return nil, fmt.Errorf("failed to run migrations: %w", err) return nil, fmt.Errorf("failed to run migrations: %w", err)
} }
emailClient := service.NewResendClient(cfg.ResendKey) emailClient := service.NewEmailClient(cfg.MailerSMTPHost, cfg.MailerSMTPPort, cfg.MailerIMAPHost, cfg.MailerIMAPPort, cfg.MailerUsername, cfg.MailerPassword)
userRepository := repository.NewUserRepository(database) userRepository := repository.NewUserRepository(database)
userService := service.NewUserService(userRepository) userService := service.NewUserService(userRepository)
authService := service.NewAuthService(userRepository) authService := service.NewAuthService(userRepository)
emailService := service.NewEmailService(emailClient, cfg.EmailFrom, cfg.AppURL, cfg.AppName, cfg.AppEnv == "development") emailService := service.NewEmailService(emailClient, cfg.MailerEmailFrom, cfg.MailerEnvelopeFrom, cfg.MailerSupportFrom, cfg.MailerSupportEnvelopeFrom, cfg.AppURL, cfg.AppName, cfg.AppEnv == "development")
return &App{ return &App{
Cfg: cfg, Cfg: cfg,

View file

@ -3,6 +3,7 @@ package config
import ( import (
"log/slog" "log/slog"
"os" "os"
"strconv"
"time" "time"
"github.com/joho/godotenv" "github.com/joho/godotenv"
@ -23,11 +24,15 @@ type Config struct {
JWTExpiry time.Duration JWTExpiry time.Duration
MailerSMTPHost string MailerSMTPHost string
MailerSMTPPort string MailerSMTPPort int
MailerIMAPHost string
MailerIMAPPort int
MailerUsername string MailerUsername string
MailerPassword string MailerPassword string
MailerEmailFrom string MailerEmailFrom string
MailerEnvelopeFrom string MailerEnvelopeFrom string
MailerSupportFrom string
MailerSupportEnvelopeFrom string
} }
func Load() *Config { func Load() *Config {
@ -51,11 +56,15 @@ func Load() *Config {
JWTExpiry: envDuration("JWT_EXPIRY", 168*time.Hour), // 7 days default JWTExpiry: envDuration("JWT_EXPIRY", 168*time.Hour), // 7 days default
MailerSMTPHost: envString("MAILER_SMTP_HOST", ""), MailerSMTPHost: envString("MAILER_SMTP_HOST", ""),
MailerSMTPPort: envString("MAILER_SMTP_PORT", ""), MailerSMTPPort: envInt("MAILER_SMTP_PORT", 587),
MailerIMAPHost: envString("MAILER_IMAP_HOST", ""),
MailerIMAPPort: envInt("MAILER_IMAP_PORT", 993),
MailerUsername: envString("MAILER_USERNAME", ""), MailerUsername: envString("MAILER_USERNAME", ""),
MailerPassword: envString("MAILER_PASSWORD", ""), MailerPassword: envString("MAILER_PASSWORD", ""),
MailerEmailFrom: envString("MAILER_EMAIL_FROM", ""), MailerEmailFrom: envString("MAILER_EMAIL_FROM", ""),
MailerEnvelopeFrom: envString("MAILER_ENVELOPE_FROM", ""), MailerEnvelopeFrom: envString("MAILER_ENVELOPE_FROM", ""),
MailerSupportFrom: envString("MAILER_SUPPORT_EMAIL_FROM", ""),
MailerSupportEnvelopeFrom: envString("MAILER_SUPPORT_ENVELOPE_FROM", ""),
} }
return cfg return cfg
@ -89,6 +98,19 @@ func envString(key, def string) string {
return value return value
} }
func envInt(key string, def int) int {
value, exists := os.LookupEnv(key)
if !exists {
return def
}
i, err := strconv.ParseInt(value, 10, 32)
if err != nil {
slog.Warn("config invalid integer, using default", "key", key, "value", value, "default", def)
return def
}
return int(i)
}
func envDuration(key string, def time.Duration) time.Duration { func envDuration(key string, def time.Duration) time.Duration {
value, ok := os.LookupEnv(key) value, ok := os.LookupEnv(key)
if !ok || value == "" { if !ok || value == "" {

View file

@ -1,15 +1,20 @@
package service package service
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"log/slog" "log/slog"
"time"
"github.com/resend/resend-go/v2" "github.com/emersion/go-imap"
"github.com/emersion/go-imap/client"
"github.com/wneessen/go-mail"
) )
type EmailParams struct { type EmailParams struct {
From string From string
EnvelopeFrom string
To []string To []string
Bcc []string Bcc []string
Cc []string Cc []string
@ -19,54 +24,125 @@ type EmailParams struct {
Html string Html string
} }
type EmailClient interface { type EmailClient struct {
SendWithContext(ctx context.Context, params *EmailParams) (string, error) smtpHost string
smtpPort int
imapHost string
imapPort int
username string
password string
} }
type ResendClient struct { func NewEmailClient(smtpHost string, smtpPort int, imapHost string, imapPort int, username, password string) *EmailClient {
client *resend.Client return &EmailClient{
smtpHost: smtpHost,
smtpPort: smtpPort,
imapHost: imapHost,
imapPort: imapPort,
username: username,
password: password,
}
} }
func NewResendClient(apiKey string) *ResendClient { func (nc *EmailClient) SendWithContext(ctx context.Context, params *EmailParams) (string, error) {
var client *resend.Client m := mail.NewMsg()
if apiKey != "" { m.From(params.From)
client = resend.NewClient(apiKey) m.EnvelopeFrom(params.EnvelopeFrom)
} else { m.To(params.To...)
slog.Warn("cannot initialize Resend client with empty api key") m.Subject(params.Subject)
return nil m.SetBodyString(mail.TypeTextPlain, params.Text)
} m.SetBodyString(mail.TypeTextHTML, params.Html)
return &ResendClient{client: client} m.ReplyTo(params.ReplyTo)
m.SetDate()
m.SetMessageID()
msgID := m.GetMessageID()
var msgBuffer bytes.Buffer
if _, err := m.WriteTo(&msgBuffer); err != nil {
return "", fmt.Errorf("failed to buffer message: %w", err)
} }
func (c *ResendClient) SendWithContext(ctx context.Context, params *EmailParams) (string, error) { smtpClient, err := nc.connectToSMTP()
res, err := c.client.Emails.SendWithContext(ctx, &resend.SendEmailRequest{
From: params.From,
To: params.To,
Bcc: params.Bcc,
Cc: params.Cc,
ReplyTo: params.ReplyTo,
Subject: params.Subject,
Text: params.Text,
Html: params.Html,
})
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("failed to connect to SMTP server: %w", err)
} }
return res.Id, nil
err = smtpClient.DialAndSendWithContext(ctx, m)
if err != nil {
return "", fmt.Errorf("failed to send email: %w", err)
}
imapClient, err := nc.connectToIMAP()
if err != nil {
slog.Error("failed to establish connection with IMAP server", "error", err)
return msgID, nil
}
defer imapClient.Logout()
flags := []string{imap.SeenFlag}
folderName := "Sent"
literal := bytes.NewReader(msgBuffer.Bytes())
err = imapClient.Append(folderName, flags, time.Now(), literal)
if err != nil {
slog.Error("IMAP append failed", "error", err)
}
return msgID, nil
}
func (nc *EmailClient) connectToSMTP() (*mail.Client, error) {
smtpClient, err := mail.NewClient(
nc.smtpHost,
mail.WithPort(nc.smtpPort),
mail.WithSMTPAuth(mail.SMTPAuthPlain),
mail.WithUsername(nc.username),
mail.WithPassword(nc.password),
mail.WithTLSPolicy(mail.TLSMandatory),
)
return smtpClient, err
}
func (nc *EmailClient) connectToIMAP() (*client.Client, error) {
var c *client.Client
var err error
addr := fmt.Sprintf("%s:%d", nc.imapHost, nc.imapPort)
c, err = client.DialTLS(addr, nil)
if err != nil {
return nil, err
}
err = c.Login(nc.username, nc.password)
if err != nil {
return nil, err
}
return c, nil
} }
type EmailService struct { type EmailService struct {
client EmailClient client *EmailClient
fromEmail string fromEmail string
fromEnvelope string
supportEmail string
supportEnvelope string
isDev bool isDev bool
appURL string appURL string
appName string appName string
} }
func NewEmailService(client EmailClient, fromEmail, appURL, appName string, isDev bool) *EmailService { func NewEmailService(client *EmailClient, fromEmail, fromEnvelope, supportEmail, supportEnvelope, appURL, appName string, isDev bool) *EmailService {
return &EmailService{ return &EmailService{
client: client, client: client,
fromEmail: fromEmail, fromEmail: fromEmail,
fromEnvelope: fromEnvelope,
supportEmail: supportEmail,
supportEnvelope: supportEnvelope,
isDev: isDev, isDev: isDev,
appURL: appURL, appURL: appURL,
appName: appName, appName: appName,
@ -82,10 +158,6 @@ func (s *EmailService) SendMagicLinkEmail(email, token, name string) error {
return nil return nil
} }
if s.client == nil {
return fmt.Errorf("email service not configured (missing RESEND_API_KEY)")
}
params := &EmailParams{ params := &EmailParams{
From: s.fromEmail, From: s.fromEmail,
To: []string{email}, To: []string{email},