refactor: use external email package
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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:"-"`
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
package templates
|
||||
|
||||
var ClientConfirm = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
@@ -48,4 +50,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>`
|
||||
3
internal/services/mail/templates/error.go
Normal file
3
internal/services/mail/templates/error.go
Normal file
@@ -0,0 +1,3 @@
|
||||
package templates
|
||||
|
||||
var Error = ``
|
||||
@@ -1 +0,0 @@
|
||||
TODO:
|
||||
@@ -1,4 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
package templates
|
||||
|
||||
var ProviderConfirm = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<style>
|
||||
@@ -50,4 +52,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>`
|
||||
Reference in New Issue
Block a user