- 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
180 lines
4.1 KiB
Go
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
|
|
}
|
|
}
|