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{
emailSrv := email.NewSecure(email.MailServiceConfig{
Auth: smtp.PlainAuth("", config.MailUser, config.MailPassword, config.MailHost),
Host: config.MailHost,
Port: config.MailPort,
From: config.MailFrom,
TemplatesDir: config.MailTemplatesDir,
},
)
})
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,33 +1,27 @@
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"
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 {
@ -39,180 +33,49 @@ type SendOK struct {
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>`