|
|
|
|
@@ -2,10 +2,12 @@ package services
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/tls"
|
|
|
|
|
"fmt"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net"
|
|
|
|
|
"net/smtp"
|
|
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
|
|
|
|
"time"
|
|
|
|
|
@@ -458,26 +460,85 @@ type UpdatePreferencesInput struct {
|
|
|
|
|
DailyDigest bool `json:"daily_digest"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SendEmail sends an email using the `m mail send` CLI command.
|
|
|
|
|
// In Docker, this will fail gracefully (m CLI not available).
|
|
|
|
|
// TODO: Replace with direct SMTP for production.
|
|
|
|
|
// SendEmail sends an email via direct SMTP over TLS.
|
|
|
|
|
// Requires SMTP_HOST, SMTP_USER, SMTP_PASS env vars. Falls back to no-op if unconfigured.
|
|
|
|
|
func SendEmail(to, subject, body string) error {
|
|
|
|
|
host := os.Getenv("SMTP_HOST")
|
|
|
|
|
port := os.Getenv("SMTP_PORT")
|
|
|
|
|
user := os.Getenv("SMTP_USER")
|
|
|
|
|
pass := os.Getenv("SMTP_PASS")
|
|
|
|
|
from := os.Getenv("MAIL_FROM")
|
|
|
|
|
|
|
|
|
|
if port == "" {
|
|
|
|
|
port = "465"
|
|
|
|
|
}
|
|
|
|
|
if from == "" {
|
|
|
|
|
from = "mgmt@msbls.de"
|
|
|
|
|
}
|
|
|
|
|
cmd := exec.Command("m", "mail", "send",
|
|
|
|
|
"--from", from,
|
|
|
|
|
"--to", to,
|
|
|
|
|
"--subject", subject,
|
|
|
|
|
"--body", body,
|
|
|
|
|
"--yes")
|
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Warn("email send failed (m CLI may not be available in Docker)", "to", to, "error", err, "output", string(output))
|
|
|
|
|
return fmt.Errorf("m mail send failed: %w", err)
|
|
|
|
|
|
|
|
|
|
if host == "" || user == "" || pass == "" {
|
|
|
|
|
slog.Warn("SMTP not configured, skipping email", "to", to, "subject", subject)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
slog.Info("email sent", "from", from, "to", to, "subject", subject)
|
|
|
|
|
|
|
|
|
|
// Build RFC 2822 message
|
|
|
|
|
msg := fmt.Sprintf("From: \"KanzlAI-mGMT\" <%s>\r\n"+
|
|
|
|
|
"To: %s\r\n"+
|
|
|
|
|
"Subject: [KanzlAI] %s\r\n"+
|
|
|
|
|
"MIME-Version: 1.0\r\n"+
|
|
|
|
|
"Content-Type: text/plain; charset=utf-8\r\n"+
|
|
|
|
|
"Date: %s\r\n"+
|
|
|
|
|
"\r\n%s",
|
|
|
|
|
from, to, subject,
|
|
|
|
|
time.Now().Format(time.RFC1123Z),
|
|
|
|
|
body)
|
|
|
|
|
|
|
|
|
|
addr := net.JoinHostPort(host, port)
|
|
|
|
|
|
|
|
|
|
// Connect with implicit TLS (port 465)
|
|
|
|
|
tlsConfig := &tls.Config{ServerName: host}
|
|
|
|
|
conn, err := tls.Dial("tcp", addr, tlsConfig)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("smtp tls dial: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
client, err := smtp.NewClient(conn, host)
|
|
|
|
|
if err != nil {
|
|
|
|
|
conn.Close()
|
|
|
|
|
return fmt.Errorf("smtp new client: %w", err)
|
|
|
|
|
}
|
|
|
|
|
defer client.Close()
|
|
|
|
|
|
|
|
|
|
// Authenticate
|
|
|
|
|
auth := smtp.PlainAuth("", user, pass, host)
|
|
|
|
|
if err := client.Auth(auth); err != nil {
|
|
|
|
|
return fmt.Errorf("smtp auth: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Send
|
|
|
|
|
if err := client.Mail(from); err != nil {
|
|
|
|
|
return fmt.Errorf("smtp mail from: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if err := client.Rcpt(to); err != nil {
|
|
|
|
|
return fmt.Errorf("smtp rcpt to: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w, err := client.Data()
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("smtp data: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := w.Write([]byte(msg)); err != nil {
|
|
|
|
|
return fmt.Errorf("smtp write: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if err := w.Close(); err != nil {
|
|
|
|
|
return fmt.Errorf("smtp close data: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := client.Quit(); err != nil {
|
|
|
|
|
slog.Warn("smtp quit error (non-fatal)", "error", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
slog.Info("email sent via SMTP", "from", from, "to", to, "subject", subject)
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|