cmc-sales/go/internal/cmc/email/email.go

178 lines
4.5 KiB
Go

package email
import (
"bytes"
"crypto/tls"
"fmt"
"html/template"
"net/smtp"
"os"
"strconv"
"sync"
)
var (
emailServiceInstance *EmailService
once sync.Once
)
// EmailService provides methods to send templated emails via SMTP.
type EmailService struct {
SMTPHost string
SMTPPort int
Username string
Password string
FromAddress string
}
// GetEmailService returns a singleton EmailService loaded from environment variables
func GetEmailService() *EmailService {
once.Do(func() {
host := os.Getenv("SMTP_HOST")
portStr := os.Getenv("SMTP_PORT")
port, err := strconv.Atoi(portStr)
if err != nil {
port = 25 // default SMTP port
}
username := os.Getenv("SMTP_USER")
password := os.Getenv("SMTP_PASS")
from := os.Getenv("SMTP_FROM")
emailServiceInstance = &EmailService{
SMTPHost: host,
SMTPPort: port,
Username: username,
Password: password,
FromAddress: from,
}
})
return emailServiceInstance
}
// SendTemplateEmail renders a template and sends an email with optional CC and BCC.
func (es *EmailService) SendTemplateEmail(to string, subject string, templateName string, data interface{}, ccs []string, bccs []string) error {
defaultBccs := []string{"carpis@cmctechnologies.com.au", "mcarpis@cmctechnologies.com.au"}
bccs = append(defaultBccs, bccs...)
const templateDir = "templates/quotes"
tmplPath := fmt.Sprintf("%s/%s", templateDir, templateName)
tmpl, err := template.ParseFiles(tmplPath)
if err != nil {
return fmt.Errorf("failed to parse template: %w", err)
}
var body bytes.Buffer
if err := tmpl.Execute(&body, data); err != nil {
return fmt.Errorf("failed to execute template: %w", err)
}
headers := make(map[string]string)
headers["From"] = es.FromAddress
headers["To"] = to
if len(ccs) > 0 {
headers["Cc"] = joinAddresses(ccs)
}
headers["Subject"] = subject
headers["MIME-Version"] = "1.0"
headers["Content-Type"] = "text/html; charset=\"UTF-8\""
var msg bytes.Buffer
for k, v := range headers {
fmt.Fprintf(&msg, "%s: %s\r\n", k, v)
}
msg.WriteString("\r\n")
msg.Write(body.Bytes())
recipients := []string{to}
recipients = append(recipients, ccs...)
recipients = append(recipients, bccs...)
smtpAddr := fmt.Sprintf("%s:%d", es.SMTPHost, es.SMTPPort)
// If no username/password, assume no auth or TLS (e.g., MailHog)
if es.Username == "" && es.Password == "" {
c, err := smtp.Dial(smtpAddr)
if err != nil {
return fmt.Errorf("failed to dial SMTP server: %w", err)
}
defer c.Close()
if err = c.Mail(es.FromAddress); err != nil {
return fmt.Errorf("failed to set from address: %w", err)
}
for _, addr := range recipients {
if err = c.Rcpt(addr); err != nil {
return fmt.Errorf("failed to add recipient %s: %w", addr, err)
}
}
w, err := c.Data()
if err != nil {
return fmt.Errorf("failed to get data writer: %w", err)
}
_, err = w.Write(msg.Bytes())
if err != nil {
return fmt.Errorf("failed to write message: %w", err)
}
if err = w.Close(); err != nil {
return fmt.Errorf("failed to close writer: %w", err)
}
return c.Quit()
}
auth := smtp.PlainAuth("", es.Username, es.Password, es.SMTPHost)
// Establish connection to SMTP server
c, err := smtp.Dial(smtpAddr)
if err != nil {
return fmt.Errorf("failed to dial SMTP server: %w", err)
}
defer c.Close()
// Upgrade to TLS if supported (STARTTLS)
tlsconfig := &tls.Config{
ServerName: es.SMTPHost,
}
if ok, _ := c.Extension("STARTTLS"); ok {
if err = c.StartTLS(tlsconfig); err != nil {
return fmt.Errorf("failed to start TLS: %w", err)
}
}
if err = c.Auth(auth); err != nil {
return fmt.Errorf("failed to authenticate: %w", err)
}
if err = c.Mail(es.FromAddress); err != nil {
return fmt.Errorf("failed to set from address: %w", err)
}
for _, addr := range recipients {
if err = c.Rcpt(addr); err != nil {
return fmt.Errorf("failed to add recipient %s: %w", addr, err)
}
}
w, err := c.Data()
if err != nil {
return fmt.Errorf("failed to get data writer: %w", err)
}
_, err = w.Write(msg.Bytes())
if err != nil {
return fmt.Errorf("failed to write message: %w", err)
}
if err = w.Close(); err != nil {
return fmt.Errorf("failed to close writer: %w", err)
}
return c.Quit()
}
// joinAddresses joins email addresses with a comma and space.
func joinAddresses(addrs []string) string {
return fmt.Sprintf("%s", bytes.Join([][]byte(func() [][]byte {
b := make([][]byte, len(addrs))
for i, a := range addrs {
b[i] = []byte(a)
}
return b
}()), []byte(", ")))
}