216 lines
6.2 KiB
Go
216 lines
6.2 KiB
Go
|
package mail
|
||
|
|
||
|
import (
|
||
|
"crypto/tls"
|
||
|
"crypto/x509"
|
||
|
"fmt"
|
||
|
"net/smtp"
|
||
|
"os"
|
||
|
"strings"
|
||
|
"time"
|
||
|
|
||
|
"golang.org/x/exp/slices"
|
||
|
)
|
||
|
|
||
|
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"
|
||
|
)
|
||
|
|
||
|
type MailService struct {
|
||
|
auth smtp.Auth
|
||
|
host string
|
||
|
port string
|
||
|
from string
|
||
|
templatesDir string
|
||
|
tlsconfig *tls.Config
|
||
|
}
|
||
|
|
||
|
type SendOK struct {
|
||
|
Price float64
|
||
|
ExplorerUrl string
|
||
|
TxID string
|
||
|
BlockHash string
|
||
|
DocHash string
|
||
|
To 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(template, "{{tx_id}}", data.TxID, -1)
|
||
|
template = strings.Replace(template, "{{block_hash}}", data.BlockHash, -1)
|
||
|
template = strings.Replace(template, "{{doc_hash}}", data.DocHash, -1)
|
||
|
template = strings.Replace(template, "{{support_email}}", m.from, -1)
|
||
|
msg := []byte(m.messageWithHeaders(okSubject, data.To, template))
|
||
|
return m.send(data.To, msg)
|
||
|
}
|
||
|
|
||
|
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(template, "{{tx_id}}", data.TxID, -1)
|
||
|
template = strings.Replace(template, "{{block_hash}}", data.BlockHash, -1)
|
||
|
template = strings.Replace(template, "{{doc_hash}}", data.DocHash, -1)
|
||
|
template = strings.Replace(template, "{{support_email}}", m.from, -1)
|
||
|
msg := []byte(m.messageWithHeaders(okSubject, data.To, template))
|
||
|
return m.send(data.To, msg)
|
||
|
}
|
||
|
|
||
|
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(template, "{{tx_id}}", data.TxID, -1)
|
||
|
template = strings.Replace(template, "{{block_hash}}", data.BlockHash, -1)
|
||
|
template = strings.Replace(template, "{{doc_hash}}", data.DocHash, -1)
|
||
|
template = strings.Replace(template, "{{support_email}}", m.from, -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
|
||
|
}
|