178 lines
4.5 KiB
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(", ")))
|
|
}
|