Files
dbbackup/internal/notify/smtp.go
Alexander Renz d10f334508
Some checks failed
CI/CD / Test (push) Has been cancelled
CI/CD / Integration Tests (push) Has been cancelled
CI/CD / Native Engine Tests (push) Has been cancelled
CI/CD / Lint (push) Has been cancelled
CI/CD / Build Binary (push) Has been cancelled
CI/CD / Test Release Build (push) Has been cancelled
CI/CD / Release Binaries (push) Has been cancelled
v5.7.7: DR Drill MariaDB fixes, SMTP notifications, verify paths
### Fixed (5.7.3 - 5.7.7)
- MariaDB binlog position bug (4 vs 5 columns)
- Notify test command ENV variable reading
- SMTP 250 Ok response treated as error
- Verify command absolute path handling
- DR Drill for modern MariaDB containers:
  - Use mariadb-admin/mariadb client
  - TCP instead of socket connections
  - DROP DATABASE before restore

### Improved
- Better --password flag error message
- PostgreSQL peer auth fallback logging
- Binlog warnings at DEBUG level
2026-02-03 13:42:02 +01:00

187 lines
4.3 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)
}
_, err = w.Write([]byte(message))
if err != nil {
return fmt.Errorf("write failed: %w", err)
}
// Close the data writer to finalize the message
if err = w.Close(); err != nil {
return fmt.Errorf("data close failed: %w", err)
}
// Quit gracefully - ignore the response as long as it's a 2xx code
// Some servers return "250 2.0.0 Ok: queued as..." which isn't an error
_ = client.Quit()
return nil
}
// 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
}
}