refactor: use external email package

This commit is contained in:
Urko. 2023-12-26 10:58:26 +01:00
parent da1228082a
commit bc29b63114
12 changed files with 78 additions and 268 deletions

View File

@ -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

View File

@ -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))

5
go.mod
View File

@ -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

6
go.sum
View File

@ -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=

View File

@ -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,

View File

@ -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:"-"`
}

View File

@ -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,
})
}

View File

@ -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)
}

View File

@ -1,4 +1,6 @@
<!DOCTYPE html>
package templates
var ClientConfirm = `<!DOCTYPE html>
<html>
<head>
<style>
@ -48,4 +50,4 @@
</div>
</div>
</body>
</html>
</html>`

View File

@ -0,0 +1,3 @@
package templates
var Error = ``

View File

@ -1 +0,0 @@
TODO:

View File

@ -1,4 +1,6 @@
<!DOCTYPE html>
package templates
var ProviderConfirm = `<!DOCTYPE html>
<html>
<head>
<style>
@ -50,4 +52,4 @@
</div>
</div>
</body>
</html>
</html>`