refactor: use external email package

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

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