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"} 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(", "))) }