package tools import ( "encoding/base64" "fmt" "log" "net/smtp" "os" "strconv" "strings" ) // SMTPConfig holds email server configuration. type SMTPConfig struct { Host string Port int User string Password string From string } // Attachment represents an email file attachment. type Attachment struct { Filename string ContentType string Data []byte } // LoadSMTPConfig loads SMTP settings from environment variables. // It first checks ODOO_SMTP_* prefixed vars, then falls back to SMTP_*. func LoadSMTPConfig() *SMTPConfig { cfg := &SMTPConfig{ Port: 587, } // Prefer ODOO_SMTP_* env vars, fall back to SMTP_* cfg.Host = envOr("ODOO_SMTP_HOST", os.Getenv("SMTP_HOST")) cfg.User = envOr("ODOO_SMTP_USER", os.Getenv("SMTP_USER")) cfg.Password = envOr("ODOO_SMTP_PASSWORD", os.Getenv("SMTP_PASSWORD")) cfg.From = envOr("ODOO_SMTP_FROM", os.Getenv("SMTP_FROM")) if v := envOr("ODOO_SMTP_PORT", os.Getenv("SMTP_PORT")); v != "" { if p, err := strconv.Atoi(v); err == nil { cfg.Port = p } } return cfg } // envOr returns the value of the named environment variable if set, otherwise fallback. func envOr(key, fallback string) string { if v := os.Getenv(key); v != "" { return v } return fallback } // SendEmail sends a simple HTML email without attachments. // Returns nil (with a log message) if SMTP is not configured. func SendEmail(cfg *SMTPConfig, to, subject, body string) error { if cfg.Host == "" { log.Printf("email: SMTP not configured, would send to=%s subject=%s", to, subject) return nil } msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/html; charset=utf-8\r\n\r\n%s", cfg.From, to, subject, body) auth := smtp.PlainAuth("", cfg.User, cfg.Password, cfg.Host) addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) return smtp.SendMail(addr, auth, cfg.From, []string{to}, []byte(msg)) } // SendEmailWithAttachments sends an HTML email with optional file attachments. // If SMTP is not configured, it logs the intent and returns nil. func SendEmailWithAttachments(cfg *SMTPConfig, to []string, subject, bodyHTML string, attachments []Attachment) error { if cfg.Host == "" { log.Printf("email: SMTP not configured, would send to=%s subject=%s attachments=%d", strings.Join(to, ","), subject, len(attachments)) return nil } addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) boundary := "==odoo-go-boundary-42==" var msg strings.Builder msg.WriteString(fmt.Sprintf("From: %s\r\n", cfg.From)) msg.WriteString(fmt.Sprintf("To: %s\r\n", strings.Join(to, ", "))) msg.WriteString(fmt.Sprintf("Subject: %s\r\n", subject)) msg.WriteString("MIME-Version: 1.0\r\n") if len(attachments) > 0 { msg.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n\r\n", boundary)) // HTML body part msg.WriteString(fmt.Sprintf("--%s\r\n", boundary)) msg.WriteString("Content-Type: text/html; charset=utf-8\r\n") msg.WriteString("Content-Transfer-Encoding: 7bit\r\n\r\n") msg.WriteString(bodyHTML) msg.WriteString("\r\n") // Attachment parts for _, att := range attachments { msg.WriteString(fmt.Sprintf("\r\n--%s\r\n", boundary)) ct := att.ContentType if ct == "" { ct = "application/octet-stream" } msg.WriteString(fmt.Sprintf("Content-Type: %s; name=\"%s\"\r\n", ct, att.Filename)) msg.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=\"%s\"\r\n", att.Filename)) msg.WriteString("Content-Transfer-Encoding: base64\r\n\r\n") msg.WriteString(base64.StdEncoding.EncodeToString(att.Data)) } msg.WriteString(fmt.Sprintf("\r\n--%s--\r\n", boundary)) } else { msg.WriteString("Content-Type: text/html; charset=utf-8\r\n\r\n") msg.WriteString(bodyHTML) } // Auth (nil if no user/password for relay servers) var auth smtp.Auth if cfg.User != "" { auth = smtp.PlainAuth("", cfg.User, cfg.Password, cfg.Host) } return smtp.SendMail(addr, auth, cfg.From, to, []byte(msg.String())) }