feat(mail): sender display name (SMTP_FROM_NAME, default MindSift)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m54s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m54s
A bare SMTP_FROM address gets the display name in the From header; a name already in SMTP_FROM wins, the envelope keeps the bare address. Also a field in the admin mail settings. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1632dd22c4
commit
56c1fa02e6
12 files changed
+86
-14
No files matched your search
+24
-5
@@ -20,8 +20,9 @@ import (
|
||||
// smtpSettings is the effective mail server configuration (admin settings, else environment).
|
||||
type smtpSettings struct {
|
||||
Host, Port, User, Pass, From string
|
||||
TLS bool // 465: implicit TLS, otherwise STARTTLS (required when set)
|
||||
SkipVerify bool // accept self-signed / mismatching certificates
|
||||
FromName string // display name when From is a bare address
|
||||
TLS bool // 465: implicit TLS, otherwise STARTTLS (required when set)
|
||||
SkipVerify bool // accept self-signed / mismatching certificates
|
||||
|
||||
authMech string // "" = automatic; "LOGIN" forces AUTH LOGIN (fallback after a refused PLAIN)
|
||||
}
|
||||
@@ -46,13 +47,15 @@ func (s *Server) smtpSettings() (smtpSettings, error) {
|
||||
cfg = smtpSettings{
|
||||
Host: strings.TrimSpace(sys.SMTPHost), Port: strings.TrimSpace(sys.SMTPPort),
|
||||
User: strings.TrimSpace(sys.SMTPUser), Pass: sys.SMTPPass, From: strings.TrimSpace(sys.SMTPFrom),
|
||||
TLS: sys.SMTPTLS, SkipVerify: sys.SMTPSkipVerify,
|
||||
FromName: strings.TrimSpace(sys.SMTPFromName),
|
||||
TLS: sys.SMTPTLS, SkipVerify: sys.SMTPSkipVerify,
|
||||
}
|
||||
} else if strings.TrimSpace(s.cfg.SMTPHost) != "" {
|
||||
cfg = smtpSettings{
|
||||
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),
|
||||
TLS: s.cfg.SMTPTLS, SkipVerify: s.cfg.SMTPSkipVerify,
|
||||
FromName: strings.TrimSpace(s.cfg.SMTPFromName),
|
||||
TLS: s.cfg.SMTPTLS, SkipVerify: s.cfg.SMTPSkipVerify,
|
||||
}
|
||||
}
|
||||
if cfg.Host == "" {
|
||||
@@ -61,6 +64,9 @@ func (s *Server) smtpSettings() (smtpSettings, error) {
|
||||
if cfg.Port == "" {
|
||||
cfg.Port = "587"
|
||||
}
|
||||
if cfg.FromName == "" {
|
||||
cfg.FromName = "MindSift"
|
||||
}
|
||||
if cfg.From == "" {
|
||||
if strings.Contains(cfg.User, "@") {
|
||||
cfg.From = cfg.User
|
||||
@@ -96,6 +102,19 @@ func splitAddress(raw string) (envelope, header string, err error) {
|
||||
return addr.Address, addr.String(), nil
|
||||
}
|
||||
|
||||
// senderAddress is splitAddress for the sender: a bare address gets the display name, so mail clients
|
||||
// show "MindSift" instead of only the address. A name already in SMTP_FROM wins.
|
||||
func senderAddress(raw, name string) (envelope, header string, err error) {
|
||||
addr, err := mail.ParseAddress(strings.TrimSpace(raw))
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("ungültige E-Mail-Adresse %q", raw)
|
||||
}
|
||||
if addr.Name == "" {
|
||||
addr.Name = strings.TrimSpace(name)
|
||||
}
|
||||
return addr.Address, addr.String(), nil
|
||||
}
|
||||
|
||||
// buildMessage creates an RFC 5322 message: Date and Message-ID headers, RFC 2047 encoded subject
|
||||
// (umlauts, emoji) and a quoted-printable UTF-8 body with CRLF line endings.
|
||||
func buildMessage(from, to, subject, body string, now time.Time) []byte {
|
||||
@@ -202,7 +221,7 @@ func sendMail(cfg smtpSettings, to, subject, body string) error {
|
||||
// sendMailOnce runs the SMTP dialogue step by step instead of smtp.SendMail, so it can introduce itself
|
||||
// properly, honour SkipVerify and report the exact step a server refuses.
|
||||
func sendMailOnce(cfg smtpSettings, to, subject, body string) error {
|
||||
fromAddr, fromHeader, err := splitAddress(cfg.From)
|
||||
fromAddr, fromHeader, err := senderAddress(cfg.From, cfg.FromName)
|
||||
if err != nil {
|
||||
return &smtpStepError{"Absender", err}
|
||||
}
|
||||
|
||||
@@ -270,3 +270,17 @@ func TestAuthLoginOnlyAndBothFailing(t *testing.T) {
|
||||
t.Fatalf("expected both mechanisms and the server hint in the error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSenderAddressGetsDisplayName(t *testing.T) {
|
||||
cases := []struct{ raw, name, envelope, header string }{
|
||||
{"alerts@example.com", "MindSift", "alerts@example.com", `"MindSift" <alerts@example.com>`},
|
||||
{`"Mein Dienst" <alerts@example.com>`, "MindSift", "alerts@example.com", `"Mein Dienst" <alerts@example.com>`},
|
||||
{"alerts@example.com", "Büro Müller", "alerts@example.com", "=?utf-8?q?B=C3=BCro_M=C3=BCller?= <alerts@example.com>"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
env, hdr, err := senderAddress(c.raw, c.name)
|
||||
if err != nil || env != c.envelope || hdr != c.header {
|
||||
t.Fatalf("senderAddress(%q, %q) = %q, %q, %v", c.raw, c.name, env, hdr, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user