Files
goodie/pkg/tools/email.go
Marc 2c7c1e6c88 PDF invoice reports + SMTP email sending
PDF Reports:
- Professional invoice HTML renderer (company header, partner address,
  styled line items, totals, Odoo-purple branding, A4 @page CSS)
- wkhtmltopdf installed in Docker runtime stage for real PDF generation
- Print button on invoice form (opens PDF in new tab)
- Exported HtmlToPDF/RenderInvoiceHTML for cross-package use

SMTP Email:
- SendEmail + SendEmailWithAttachments with MIME multipart support
- Base64 file attachments
- Config via ODOO_SMTP_HOST/PORT/USER/PASSWORD/FROM env vars
- LoadSMTPConfig helper, nil auth for relay servers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 13:58:19 +02:00

130 lines
3.9 KiB
Go

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