fix(smtp): proper SMTP dialogue, encoded mails, optional skip-verify
- Explicit SMTP flow (dial -> EHLO -> STARTTLS -> AUTH -> MAIL/RCPT/DATA) with errors naming the refusing step (also logged as smtp_step) - EHLO with the sender's domain instead of "localhost" - Envelope gets the bare address; "Name <addr>" stays in the From header (was rejected as invalid MAIL FROM) - Date and Message-ID headers, RFC 2047 subject, quoted-printable body - SMTP_SKIP_VERIFY / admin switch for self-signed certificates; SMTP checkboxes are switches now - Tests against a fake SMTP server (STARTTLS, 451, display names) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
bf0d68e7e1
commit
ce321ce579
15 files changed
+477
-96
No files matched your search
+11
-79
@@ -2,7 +2,6 @@ package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -10,7 +9,6 @@ import (
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -2923,85 +2921,19 @@ func (s *Server) handleAdminTestSMTP(w http.ResponseWriter, r *http.Request) {
|
||||
// --- MULTI-CHANNEL ALARM DISPATCHER (EMAIL, DISCORD, CUSTOM WEBHOOK) ---
|
||||
|
||||
func (s *Server) sendEmailNotification(toEmail, subject, bodyText string) error {
|
||||
sys, _ := s.store.GetSystemSettings()
|
||||
host := ""
|
||||
port := "587"
|
||||
user := ""
|
||||
pass := ""
|
||||
from := ""
|
||||
useTLS := true
|
||||
|
||||
if sys != nil && strings.TrimSpace(sys.SMTPHost) != "" {
|
||||
host = strings.TrimSpace(sys.SMTPHost)
|
||||
port = strings.TrimSpace(sys.SMTPPort)
|
||||
user = strings.TrimSpace(sys.SMTPUser)
|
||||
pass = sys.SMTPPass
|
||||
from = strings.TrimSpace(sys.SMTPFrom)
|
||||
useTLS = sys.SMTPTLS
|
||||
} else if strings.TrimSpace(s.cfg.SMTPHost) != "" {
|
||||
host = strings.TrimSpace(s.cfg.SMTPHost)
|
||||
port = strings.TrimSpace(s.cfg.SMTPPort)
|
||||
user = strings.TrimSpace(s.cfg.SMTPUser)
|
||||
pass = s.cfg.SMTPPass
|
||||
from = strings.TrimSpace(s.cfg.SMTPFrom)
|
||||
useTLS = s.cfg.SMTPTLS
|
||||
}
|
||||
|
||||
if host == "" {
|
||||
return fmt.Errorf("kein SMTP-Server konfiguriert (bitte in den Administratoreinstellungen oder per Docker-Env hinterlegen)")
|
||||
}
|
||||
if port == "" {
|
||||
port = "587"
|
||||
}
|
||||
if from == "" {
|
||||
if user != "" && strings.Contains(user, "@") {
|
||||
from = user
|
||||
} else {
|
||||
from = "noreply@mindsift.local"
|
||||
}
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%s", host, port)
|
||||
msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=\"utf-8\"\r\n\r\n%s\r\n", from, toEmail, subject, bodyText)
|
||||
|
||||
var smtpAuth smtp.Auth
|
||||
if user != "" || pass != "" {
|
||||
smtpAuth = smtp.PlainAuth("", user, pass, host)
|
||||
}
|
||||
|
||||
if port == "465" && useTLS {
|
||||
tlsCfg := &tls.Config{ServerName: host}
|
||||
conn, err := tls.Dial("tcp", addr, tlsCfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer client.Quit()
|
||||
if smtpAuth != nil {
|
||||
if err := client.Auth(smtpAuth); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := client.Mail(from); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := client.Rcpt(toEmail); err != nil {
|
||||
return err
|
||||
}
|
||||
wc, err := client.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = wc.Write([]byte(msg))
|
||||
_ = wc.Close()
|
||||
cfg, err := s.smtpSettings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return smtp.SendMail(addr, smtpAuth, from, []string{toEmail}, []byte(msg))
|
||||
if err := sendMail(cfg, toEmail, subject, bodyText); err != nil {
|
||||
step := ""
|
||||
if se, ok := err.(*smtpStepError); ok {
|
||||
step = se.Step
|
||||
}
|
||||
slog.Warn("E-Mail-Versand fehlgeschlagen", "smtp_step", step, "host", cfg.Host, "port", cfg.Port, "to", toEmail, "error", err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) sendDiscordNotification(webhookURL, title, description, dueDate, priority string) error {
|
||||
|
||||
Reference in new issue
Block a user