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 { Amount float64 ExplorerUrl string Tx string CustomerID string OrderID string Block string Timestamp time.Time 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, "{{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) } 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, "{{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) } 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.Tx, -1) template = strings.Replace(template, "{{block_hash}}", data.Block, -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 }