Files
dbbackup/internal/notify/smtp.go
Alexander Renz d0d83b61ef feat: add dry-run mode, GFS retention policies, and notifications
- Add --dry-run/-n flag for backup commands with comprehensive preflight checks
  - Database connectivity validation
  - Required tools availability check
  - Storage target and permissions verification
  - Backup size estimation
  - Encryption and cloud storage configuration validation

- Implement GFS (Grandfather-Father-Son) retention policies
  - Daily/Weekly/Monthly/Yearly tier classification
  - Configurable retention counts per tier
  - Custom weekly day and monthly day settings
  - ISO week handling for proper week boundaries

- Add notification system with SMTP and webhook support
  - SMTP email notifications with TLS/STARTTLS
  - Webhook HTTP notifications with HMAC-SHA256 signing
  - Slack-compatible webhook payload format
  - Event types: backup/restore started/completed/failed, cleanup, verify, PITR
  - Configurable severity levels and retry logic

- Update README.md with documentation for all new features
2025-12-13 19:00:54 +01:00

180 lines
4.1 KiB
Go

// Package notify - SMTP email notifications
package notify
import (
"context"
"crypto/tls"
"fmt"
"net"
"net/smtp"
"strings"
"time"
)
// SMTPNotifier sends notifications via email
type SMTPNotifier struct {
config Config
}
// NewSMTPNotifier creates a new SMTP notifier
func NewSMTPNotifier(config Config) *SMTPNotifier {
return &SMTPNotifier{
config: config,
}
}
// Name returns the notifier name
func (s *SMTPNotifier) Name() string {
return "smtp"
}
// IsEnabled returns whether SMTP notifications are enabled
func (s *SMTPNotifier) IsEnabled() bool {
return s.config.SMTPEnabled && s.config.SMTPHost != "" && len(s.config.SMTPTo) > 0
}
// Send sends an email notification
func (s *SMTPNotifier) Send(ctx context.Context, event *Event) error {
if !s.IsEnabled() {
return nil
}
// Build email
subject := FormatEventSubject(event)
body := FormatEventBody(event)
// Build headers
headers := make(map[string]string)
headers["From"] = s.config.SMTPFrom
headers["To"] = strings.Join(s.config.SMTPTo, ", ")
headers["Subject"] = subject
headers["MIME-Version"] = "1.0"
headers["Content-Type"] = "text/plain; charset=UTF-8"
headers["Date"] = time.Now().Format(time.RFC1123Z)
headers["X-Priority"] = s.getPriority(event.Severity)
// Build message
var msg strings.Builder
for k, v := range headers {
msg.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
}
msg.WriteString("\r\n")
msg.WriteString(body)
// Send with retries
var lastErr error
for attempt := 0; attempt <= s.config.Retries; attempt++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if attempt > 0 {
time.Sleep(s.config.RetryDelay)
}
err := s.sendMail(ctx, msg.String())
if err == nil {
return nil
}
lastErr = err
}
return fmt.Errorf("smtp: failed after %d attempts: %w", s.config.Retries+1, lastErr)
}
// sendMail sends the email message
func (s *SMTPNotifier) sendMail(ctx context.Context, message string) error {
addr := fmt.Sprintf("%s:%d", s.config.SMTPHost, s.config.SMTPPort)
// Create connection with timeout
dialer := &net.Dialer{
Timeout: 30 * time.Second,
}
var conn net.Conn
var err error
if s.config.SMTPTLS {
// Direct TLS connection (port 465)
tlsConfig := &tls.Config{
ServerName: s.config.SMTPHost,
}
conn, err = tls.DialWithDialer(dialer, "tcp", addr, tlsConfig)
} else {
conn, err = dialer.DialContext(ctx, "tcp", addr)
}
if err != nil {
return fmt.Errorf("dial failed: %w", err)
}
defer conn.Close()
// Create SMTP client
client, err := smtp.NewClient(conn, s.config.SMTPHost)
if err != nil {
return fmt.Errorf("smtp client creation failed: %w", err)
}
defer client.Close()
// STARTTLS if needed (and not already using TLS)
if s.config.SMTPStartTLS && !s.config.SMTPTLS {
if ok, _ := client.Extension("STARTTLS"); ok {
tlsConfig := &tls.Config{
ServerName: s.config.SMTPHost,
}
if err = client.StartTLS(tlsConfig); err != nil {
return fmt.Errorf("starttls failed: %w", err)
}
}
}
// Authenticate if credentials provided
if s.config.SMTPUser != "" && s.config.SMTPPassword != "" {
auth := smtp.PlainAuth("", s.config.SMTPUser, s.config.SMTPPassword, s.config.SMTPHost)
if err = client.Auth(auth); err != nil {
return fmt.Errorf("auth failed: %w", err)
}
}
// Set sender
if err = client.Mail(s.config.SMTPFrom); err != nil {
return fmt.Errorf("mail from failed: %w", err)
}
// Set recipients
for _, to := range s.config.SMTPTo {
if err = client.Rcpt(to); err != nil {
return fmt.Errorf("rcpt to failed: %w", err)
}
}
// Send message body
w, err := client.Data()
if err != nil {
return fmt.Errorf("data command failed: %w", err)
}
defer w.Close()
_, err = w.Write([]byte(message))
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
return client.Quit()
}
// getPriority returns X-Priority header value based on severity
func (s *SMTPNotifier) getPriority(severity Severity) string {
switch severity {
case SeverityCritical:
return "1" // Highest
case SeverityError:
return "2" // High
case SeverityWarning:
return "3" // Normal
default:
return "3" // Normal
}
}