From bc29b63114952c1d67efa3cd488408b74ff19c60 Mon Sep 17 00:00:00 2001 From: "Urko." Date: Tue, 26 Dec 2023 10:58:26 +0100 Subject: [PATCH] refactor: use external email package --- Makefile | 3 +- cmd/http/server/main.go | 19 +- go.mod | 5 +- go.sum | 6 +- internal/api/server.go | 9 +- internal/domain/order.go | 4 +- internal/services/mail/mail.go | 223 ++++-------------- internal/services/mail/mail_test.go | 61 ----- ...{client_confirm.html => client_confirm.go} | 6 +- internal/services/mail/templates/error.go | 3 + internal/services/mail/templates/error.html | 1 - ...vider_confirm.html => provider_confirm.go} | 6 +- 12 files changed, 78 insertions(+), 268 deletions(-) delete mode 100644 internal/services/mail/mail_test.go rename internal/services/mail/templates/{client_confirm.html => client_confirm.go} (95%) create mode 100644 internal/services/mail/templates/error.go delete mode 100644 internal/services/mail/templates/error.html rename internal/services/mail/templates/{provider_confirm.html => provider_confirm.go} (96%) diff --git a/Makefile b/Makefile index 9543d0d..7404807 100644 --- a/Makefile +++ b/Makefile @@ -55,4 +55,5 @@ clean: rm -rf $(BIN_DIR) .PHONY: rebuild -rebuild: clean build_linux_amd64 build_linux_arm64 build_windows_amd64 build_windows_386 build_mac_amd64 build_mac_arm64 +#rebuild: clean build_linux_amd64 build_linux_arm64 build_windows_amd64 build_windows_386 build_mac_amd64 build_mac_arm64 +rebuild: clean build_linux_amd64 build_windows_amd64 build_windows_386 build_mac_amd64 diff --git a/cmd/http/server/main.go b/cmd/http/server/main.go index 1ddd9dc..7f5deb8 100644 --- a/cmd/http/server/main.go +++ b/cmd/http/server/main.go @@ -15,9 +15,9 @@ import ( "gitea.urkob.com/urko/btc-pay-checker/internal/platform/mongodb/order" "gitea.urkob.com/urko/btc-pay-checker/internal/services" "gitea.urkob.com/urko/btc-pay-checker/internal/services/btc" - "gitea.urkob.com/urko/btc-pay-checker/internal/services/mail" "gitea.urkob.com/urko/btc-pay-checker/internal/services/price" "gitea.urkob.com/urko/btc-pay-checker/kit/cfg" + "gitea.urkob.com/urko/emailsender/pkg/email" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) @@ -61,17 +61,14 @@ func main() { priceSrv := price.NewPriceConversor(config.ConversorApi, config.ConversorApi) btcSrv := btc.NewBitcoinService(config.RpcHost, config.RpcAuth, config.RpcZmq, config.WalletAddress).WithTestnet() - mailSrv := mail.NewMailService( - mail.MailServiceConfig{ - Auth: smtp.PlainAuth("", config.MailUser, config.MailPassword, config.MailHost), - Host: config.MailHost, - Port: config.MailPort, - From: config.MailFrom, - TemplatesDir: config.MailTemplatesDir, - }, - ) + emailSrv := email.NewSecure(email.MailServiceConfig{ + Auth: smtp.PlainAuth("", config.MailUser, config.MailPassword, config.MailHost), + Host: config.MailHost, + Port: config.MailPort, + From: config.MailFrom, + }) - restServer := api.NewRestServer(config, orderSrv, btcSrv, priceSrv, mailSrv) + restServer := api.NewRestServer(config, orderSrv, btcSrv, priceSrv, emailSrv) go func() { if err = restServer.Start(ctx, config.ApiPort, config.Views); err != nil { panic(fmt.Errorf("restServer.Start: %w", err)) diff --git a/go.mod b/go.mod index f84f782..19129a7 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,9 @@ module gitea.urkob.com/urko/btc-pay-checker -go 1.20 +go 1.21.1 require ( + gitea.urkob.com/urko/emailsender v0.0.0-20231226090954-aca310503955 gitea.urkob.com/urko/go-root-dir v0.0.0-20230311113851-2f6d4355888a github.com/docker/go-units v0.5.0 github.com/gofiber/fiber/v2 v2.48.0 @@ -12,7 +13,6 @@ require ( github.com/pebbe/zmq4 v1.2.10 github.com/stretchr/testify v1.8.4 go.mongodb.org/mongo-driver v1.12.0 - golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 ) require ( @@ -22,6 +22,7 @@ require ( github.com/gofiber/template v1.8.2 // indirect github.com/gofiber/utils v1.1.0 // indirect github.com/golang/snappy v0.0.1 // indirect + github.com/google/go-cmp v0.5.8 // indirect github.com/google/uuid v1.3.0 // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/mattn/go-colorable v0.1.13 // indirect diff --git a/go.sum b/go.sum index 5bee8f6..4855cfd 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +gitea.urkob.com/urko/emailsender v0.0.0-20231226090954-aca310503955 h1:wQE6MlojyWmOlEp3j88BMGIM0A/ZWqByTFZzXbsbtuQ= +gitea.urkob.com/urko/emailsender v0.0.0-20231226090954-aca310503955/go.mod h1:V0m9luBiPICIA72Yr7GJKIOS0GZ+UK0aajtl3Eugqqw= gitea.urkob.com/urko/go-root-dir v0.0.0-20230311113851-2f6d4355888a h1:s73cd3bRR6v0LGiBei841iIolbBJN2tbkUwN54X9vVg= gitea.urkob.com/urko/go-root-dir v0.0.0-20230311113851-2f6d4355888a/go.mod h1:mU9nRHl70tBhJFbgKotpoXMV+s0wx+1uJ988p4oEpSo= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= @@ -20,6 +22,7 @@ github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= +github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= @@ -72,8 +75,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= -golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -117,5 +118,6 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/api/server.go b/internal/api/server.go index d4b58b8..ec95612 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -18,6 +18,7 @@ import ( "gitea.urkob.com/urko/btc-pay-checker/internal/services/mail" "gitea.urkob.com/urko/btc-pay-checker/internal/services/price" "gitea.urkob.com/urko/btc-pay-checker/kit/cfg" + "gitea.urkob.com/urko/emailsender/pkg/email" ) const ( @@ -30,7 +31,7 @@ type RestServer struct { config *cfg.Config btcService *btc.BitcoinService orderSrv *services.Order - mailSrv *mail.MailService + emailSrv *mail.MailService priceSrv *price.PriceConversor } @@ -39,14 +40,14 @@ func NewRestServer( orderSrv *services.Order, btcService *btc.BitcoinService, priceSrv *price.PriceConversor, - mailSrv *mail.MailService, + emailSrv *email.EmailService, ) *RestServer { return &RestServer{ config: config, orderSrv: orderSrv, btcService: btcService, priceSrv: priceSrv, - mailSrv: mailSrv, + emailSrv: mail.NewMailService(emailSrv), } } @@ -118,7 +119,7 @@ func (s *RestServer) onNotification(ctx context.Context, notifChan chan domain.N } // Send email to client and provider - if err := s.mailSrv.SendProviderConfirm(mail.SendOK{ + if err := s.emailSrv.SendProviderConfirm(mail.SendOK{ Tx: order.Tx, Block: order.Block, Amount: order.Amount, diff --git a/internal/domain/order.go b/internal/domain/order.go index b4ed798..2da1957 100644 --- a/internal/domain/order.go +++ b/internal/domain/order.go @@ -15,6 +15,6 @@ type Order struct { Tx string `bson:"tx" json:"tx"` Block string `bson:"block" json:"block"` PaidAt time.Time `bson:"paid_at" json:"paid_at"` - CreatedAt time.Time `bson:"created_at" json:"created_at"` - ExpiresAt time.Time `bson:"expires_at" json:"expires_at"` + CreatedAt time.Time `bson:"created_at" json:"-"` + ExpiresAt time.Time `bson:"expires_at" json:"-"` } diff --git a/internal/services/mail/mail.go b/internal/services/mail/mail.go index 112adda..b076f09 100644 --- a/internal/services/mail/mail.go +++ b/internal/services/mail/mail.go @@ -1,218 +1,81 @@ package mail import ( - "crypto/tls" - "crypto/x509" - "fmt" - "net/smtp" - "os" "strings" "time" - "golang.org/x/exp/slices" + "gitea.urkob.com/urko/btc-pay-checker/internal/services/mail/templates" + "gitea.urkob.com/urko/emailsender/pkg/email" ) const ( - mime = "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" - okSubject = "Proof-of-Evidence record successful" - failSubject = "Proof-of-Evidence record failed" - templateError = "errror.html" - templateClientConfirm = "client_confirm.html" - templateProviderConfirm = "provider_confirm.html" + mime = "MIME-version: 1.0;\nContent-Type: text/html; charset=\"UTF-8\";\n\n" + okSubject = "BTC Pay Checker successful" + failSubject = "BTC Pay Checker failed" ) type MailService struct { - auth smtp.Auth - host string - port string - from string - templatesDir string - tlsconfig *tls.Config + emailSrv *email.EmailService +} + +func NewMailService(emailSrv *email.EmailService) *MailService { + return &MailService{ + emailSrv: emailSrv, + } } type SendOK struct { - Amount float64 - ExplorerUrl string - Tx string - CustomerID string - OrderID string - Block string - Timestamp time.Time - To string + Amount float64 + ExplorerUrl string + Tx string + CustomerID string + OrderID string + Block string + Timestamp time.Time + To string + SupportEmail string } -type MailServiceConfig struct { - Auth smtp.Auth - Host string - Port string - From string // Sender email address - TemplatesDir string // Should end with slash '/' -} - -var validCommonNames = []string{"ISRG Root X1", "R3", "DST Root CA X3"} - -func NewMailService(config MailServiceConfig) *MailService { - return &MailService{ - auth: config.Auth, - host: config.Host, - port: config.Port, - from: config.From, - templatesDir: config.TemplatesDir, - tlsconfig: &tls.Config{ - InsecureSkipVerify: true, - ServerName: config.Host, - VerifyConnection: func(cs tls.ConnectionState) error { - - // // Check the server's common name - // for _, cert := range cs.PeerCertificates { - // log.Println("cert.DNSNames", cert.DNSNames) - // if err := cert.VerifyHostname(config.Host); err != nil { - // return fmt.Errorf("invalid common name: %w", err) - // } - // } - - // Check the certificate chain - opts := x509.VerifyOptions{ - Intermediates: x509.NewCertPool(), - } - for _, cert := range cs.PeerCertificates[1:] { - opts.Intermediates.AddCert(cert) - } - _, err := cs.PeerCertificates[0].Verify(opts) - if err != nil { - return fmt.Errorf("invalid certificate chain: %w", err) - } - - // Iterate over the certificates again to perform custom checks - for _, cert := range cs.PeerCertificates { - // Add your own custom checks here... - if time.Now().After(cert.NotAfter) { - return fmt.Errorf("certificate has expired") - } - if time.Now().Add(30 * 24 * time.Hour).After(cert.NotAfter) { - return fmt.Errorf("certificate will expire within 30 days") - } - - if !slices.Contains(validCommonNames, cert.Issuer.CommonName) { - return fmt.Errorf("certificate is not issued by a trusted CA") - } - // log.Println("cert.ExtKeyUsage", cert.ExtKeyUsage) - // if cert.KeyUsage&x509.KeyUsageDigitalSignature == 0 || len(cert.ExtKeyUsage) == 0 || !slices.Contains(cert.ExtKeyUsage, x509.ExtKeyUsageServerAuth) { - // log.Printf("%+v", cert) - // return fmt.Errorf("certificate cannot be used for server authentication") - // } - if cert.PublicKeyAlgorithm != x509.RSA { - return fmt.Errorf("unsupported public key algorithm") - } - } - - return nil - }, - }, - } -} func (m *MailService) SendProviderConfirm(data SendOK) error { - bts, err := os.ReadFile(m.templatesDir + templateProviderConfirm) - if err != nil { - return fmt.Errorf("os.ReadFile: %s", err) - } - template := strings.Replace(string(bts), "{{explorer_url}}", data.ExplorerUrl, -1) + template := strings.Replace(templates.ProviderConfirm, "{{explorer_url}}", data.ExplorerUrl, -1) template = strings.Replace(template, "{{customer_id}}", data.CustomerID, -1) template = strings.Replace(template, "{{order_id}}", data.OrderID, -1) template = strings.Replace(template, "{{tx}}", data.Tx, -1) template = strings.Replace(template, "{{block}}", data.Block, -1) template = strings.Replace(template, "{{timestamp}}", data.Timestamp.Format(time.RFC3339), -1) - msg := []byte(m.messageWithHeaders(okSubject, data.To, template)) - return m.send(data.To, msg) + + return m.emailSrv.SendEmail(email.EmailMessage{ + To: data.To, + Subject: okSubject, + Body: template, + }) } func (m *MailService) SendClientConfirm(data SendOK) error { - bts, err := os.ReadFile(m.templatesDir + templateClientConfirm) - if err != nil { - return fmt.Errorf("os.ReadFile: %s", err) - } - template := strings.Replace(string(bts), "{{explorer_url}}", data.ExplorerUrl, -1) + template := strings.Replace(templates.ClientConfirm, "{{explorer_url}}", data.ExplorerUrl, -1) template = strings.Replace(template, "{{customer_id}}", data.CustomerID, -1) template = strings.Replace(template, "{{order_id}}", data.OrderID, -1) template = strings.Replace(template, "{{tx}}", data.Tx, -1) template = strings.Replace(template, "{{block}}", data.Block, -1) template = strings.Replace(template, "{{timestamp}}", data.Timestamp.Format(time.RFC3339), -1) - msg := []byte(m.messageWithHeaders(okSubject, data.To, template)) - return m.send(data.To, msg) + + return m.emailSrv.SendEmail(email.EmailMessage{ + To: data.To, + Subject: okSubject, + Body: template, + }) } func (m *MailService) SendFail(data SendOK) error { - //templateError - bts, err := os.ReadFile(m.templatesDir + templateError) - if err != nil { - return fmt.Errorf("os.ReadFile: %s", err) - } - template := strings.Replace(string(bts), "{{explorer_url}}", data.ExplorerUrl, -1) + template := strings.Replace(templates.Error, "{{explorer_url}}", data.ExplorerUrl, -1) template = strings.Replace(template, "{{tx_id}}", data.Tx, -1) template = strings.Replace(template, "{{block_hash}}", data.Block, -1) - template = strings.Replace(template, "{{support_email}}", m.from, -1) + template = strings.Replace(template, "{{support_email}}", data.SupportEmail, -1) // TODO: Alert client too - msg := []byte(m.messageWithHeaders(okSubject, data.To, template)) - return m.send(data.To, msg) -} - -func (m *MailService) send(to string, msg []byte) error { - c, err := smtp.Dial(m.host + ":" + m.port) - if err != nil { - return fmt.Errorf("DIAL: %s", err) - } - - if err = c.StartTLS(m.tlsconfig); err != nil { - return fmt.Errorf("c.StartTLS: %s", err) - } - - // Auth - if err = c.Auth(m.auth); err != nil { - return fmt.Errorf("c.Auth: %s", err) - } - - // To && From - if err = c.Mail(m.from); err != nil { - return fmt.Errorf("c.Mail: %s", err) - } - - if err = c.Rcpt(to); err != nil { - return fmt.Errorf("c.Rcpt: %s", err) - } - - // Data - w, err := c.Data() - if err != nil { - return fmt.Errorf("c.Data: %s", err) - } - - _, err = w.Write(msg) - if err != nil { - return fmt.Errorf("w.Write: %s", err) - } - - if err = w.Close(); err != nil { - return fmt.Errorf("w.Close: %s", err) - } - - if err = c.Quit(); err != nil { - return fmt.Errorf("w.Quit: %s", err) - } - return nil -} - -func (m *MailService) messageWithHeaders(subject, to, body string) string { - headers := make(map[string]string) - headers["From"] = m.from - headers["To"] = to - headers["Subject"] = subject - headers["MIME-Version"] = "1.0" - - message := "" - for k, v := range headers { - message += fmt.Sprintf("%s: %s\r\n", k, v) - } - message += "Content-Type: text/html; charset=utf-8\r\n" + body - - return message + + return m.emailSrv.SendEmail(email.EmailMessage{ + To: data.To, + Subject: failSubject, + Body: template, + }) } diff --git a/internal/services/mail/mail_test.go b/internal/services/mail/mail_test.go deleted file mode 100644 index 0ca8f69..0000000 --- a/internal/services/mail/mail_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package mail - -import ( - "net/smtp" - "testing" - "time" - - "gitea.urkob.com/urko/btc-pay-checker/kit" - "gitea.urkob.com/urko/btc-pay-checker/kit/cfg" - "github.com/stretchr/testify/require" -) - -var ( - mailSrv *MailService - config *cfg.Config -) - -func init() { - config = cfg.NewConfig(kit.RootDir() + "/.test.env") - mailSrv = NewMailService( - MailServiceConfig{ - Auth: smtp.PlainAuth("", config.MailUser, config.MailPassword, config.MailHost), - Host: config.MailHost, - Port: config.MailPort, - From: config.MailFrom, - TemplatesDir: config.MailTemplatesDir, - }, - ) -} - -func Test_mailService_SendOK(t *testing.T) { - dto := SendOK{ - Amount: 12.0, - ExplorerUrl: "test", - Tx: "test-hash", - CustomerID: "client", - OrderID: "order", - Block: "block", - Timestamp: time.Now(), - To: config.MailTo, - } - - err := mailSrv.SendClientConfirm(dto) - require.NoError(t, err) -} - -func Test_mailService_SendConfirm(t *testing.T) { - dto := SendOK{ - Amount: 12.0, - ExplorerUrl: "test", - Tx: "test-hash", - CustomerID: "client", - OrderID: "order", - Block: "block", - Timestamp: time.Now(), - To: config.MailTo, - } - - err := mailSrv.SendProviderConfirm(dto) - require.NoError(t, err) -} diff --git a/internal/services/mail/templates/client_confirm.html b/internal/services/mail/templates/client_confirm.go similarity index 95% rename from internal/services/mail/templates/client_confirm.html rename to internal/services/mail/templates/client_confirm.go index eea6eb5..552da41 100644 --- a/internal/services/mail/templates/client_confirm.html +++ b/internal/services/mail/templates/client_confirm.go @@ -1,4 +1,6 @@ - +package templates + +var ClientConfirm = `