feat(mail): sender display name (SMTP_FROM_NAME, default MindSift)
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:
sandrodachandClaude Opus 5.5 committed 2026-10-29 23:47:38 +01:00
1 parent 1632dd22c4
commit 56c1fa02e6
12 files changed
+78 -6

No files matched your search

+4 -1
View File
@@ -62,8 +62,11 @@ SMTP_PORT=587
# Anmeldung am Mailserver; leer, wenn der Server keine verlangt. # Anmeldung am Mailserver; leer, wenn der Server keine verlangt.
SMTP_USER= SMTP_USER=
SMTP_PASS= SMTP_PASS=
# Absenderadresse, z. B. MindSift <noreply@example.com>. Leer = SMTP_USER bzw. noreply@mindsift.local. # Absenderadresse, z. B. noreply@example.com. Leer = SMTP_USER bzw. noreply@mindsift.local.
SMTP_FROM= SMTP_FROM=
# Name, den Mailprogramme als Absender zeigen (wenn SMTP_FROM nur eine Adresse ist).
# Ein Name direkt in SMTP_FROM ("Mein Dienst <noreply@example.com>") hat Vorrang.
SMTP_FROM_NAME=MindSift
# true = nur verschlüsselt senden. # true = nur verschlüsselt senden.
SMTP_TLS=true SMTP_TLS=true
# true = selbstsignierte Zertifikate akzeptieren (nur für interne Mailserver). # true = selbstsignierte Zertifikate akzeptieren (nur für interne Mailserver).
+3 -1
View File
@@ -9,7 +9,7 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
### Added ### Added
- Eigene Spracherkennung (optional): Mit `STT_URL` schickt MindSift Aufnahmen an einen selbst betriebenen Whisper-Server (OpenAI-kompatibel, z. B. speaches) statt an Google oder Microsoft. Einstellung nur per Umgebungsvariablen (`STT_*`) inklusive Grenzwerten; `docker-compose.yml` enthält den Dienst `whisper` als auskommentierten Block zum Einschalten, das Modell lädt MindSift beim ersten Start selbst. Administration → Server & Isolation zeigt den aktiven Modus und bietet eine Testaufnahme. In der Demo bleibt es bei der Browser-Erkennung. - Eigene Spracherkennung (optional): Mit `STT_URL` schickt MindSift Aufnahmen an einen selbst betriebenen Whisper-Server (OpenAI-kompatibel, z. B. speaches) statt an Google oder Microsoft. Einstellung nur per Umgebungsvariablen (`STT_*`) inklusive Grenzwerten; `docker-compose.yml` enthält den Dienst `whisper` als auskommentierten Block zum Einschalten, das Modell lädt MindSift beim ersten Start selbst. Administration → Server & Isolation zeigt den aktiven Modus und bietet eine Testaufnahme. In der Demo bleibt es bei der Browser-Erkennung.
- API: `POST /voice/transcribe`, `GET /admin/voice`; `/auth/status` meldet `voice.mode`. - API: `POST /voice/transcribe`, `GET /admin/voice`; `/auth/status` meldet `voice.mode`.
- Bilder in Notizen: `/bild` in einem leeren Punkt öffnet direkt im Punkt eine Auswahl – Datei, Kamera (Handy), Hineinziehen, Einfügen mit Strg+V oder Bild-Link (der Server übernimmt eine Kopie). Fotos werden vor dem Hochladen auf höchstens 2048 px verkleinert und als WebP gespeichert, Standortdaten entfallen dabei. Klick vergrößert, darunter eine optionale Beschriftung. Esc oder ✕ macht den Punkt wieder zum leeren Punkt. - Bilder in Notizen: `/image` (auch `/bild`) in einem leeren Punkt öffnet direkt im Punkt eine Auswahl – Datei, Kamera (Handy), Hineinziehen, Einfügen mit Strg+V oder Bild-Link (der Server übernimmt eine Kopie). Fotos werden vor dem Hochladen auf höchstens 2048 px verkleinert und als WebP gespeichert, Standortdaten entfallen dabei. Klick vergrößert, darunter eine optionale Beschriftung. Esc oder ✕ macht den Punkt wieder zum leeren Punkt.
- Sprachnotizen in Notizen: `/audio` in einem leeren Punkt zeigt eine Aufnahme-Blase – grüner Knopf startet, er wird rot und links laufen Live-Wellen mit der Zeit, ein Tipp speichert die Aufnahme als Audio-Player im Punkt. Alternativ eine Audiodatei hochladen. Mit eigener Spracherkennung macht „Transkribieren“ aus der Aufnahme die (durchsuchbare) Beschriftung. - Sprachnotizen in Notizen: `/audio` in einem leeren Punkt zeigt eine Aufnahme-Blase – grüner Knopf startet, er wird rot und links laufen Live-Wellen mit der Zeit, ein Tipp speichert die Aufnahme als Audio-Player im Punkt. Alternativ eine Audiodatei hochladen. Mit eigener Spracherkennung macht „Transkribieren“ aus der Aufnahme die (durchsuchbare) Beschriftung.
- Diktat in Notizen: `/diktat` in einem leeren Punkt hört sofort zu; der Punkt wird zu einer Fläche mit Live-Wellen und „Stopp“, ein Tipp setzt den erkannten Text in den Punkt. Nutzt die eingestellte Spracherkennung (eigener Server oder Browser) und wird ohne Spracherkennung nicht angeboten. - Diktat in Notizen: `/diktat` in einem leeren Punkt hört sofort zu; der Punkt wird zu einer Fläche mit Live-Wellen und „Stopp“, ein Tipp setzt den erkannten Text in den Punkt. Nutzt die eingestellte Spracherkennung (eigener Server oder Browser) und wird ohne Spracherkennung nicht angeboten.
- Backup und Export mit Medien: Das Backup-ZIP (auch die automatischen Server-Backups pro Konto) enthält die Bilder und Sprachnotizen der eigenen Notizen, das Markdown-Archiv legt sie unter `attachments/` ab und bettet sie Obsidian-kompatibel ein. Beim Zurückspielen kommen sie wieder als Bild- und Audioblöcke. Der Obsidian-Import übernimmt jetzt auch eingebettete Bilder und Audiodateien (`![[bild.png]]`, `![](bild.png)`). - Backup und Export mit Medien: Das Backup-ZIP (auch die automatischen Server-Backups pro Konto) enthält die Bilder und Sprachnotizen der eigenen Notizen, das Markdown-Archiv legt sie unter `attachments/` ab und bettet sie Obsidian-kompatibel ein. Beim Zurückspielen kommen sie wieder als Bild- und Audioblöcke. Der Obsidian-Import übernimmt jetzt auch eingebettete Bilder und Audiodateien (`![[bild.png]]`, `![](bild.png)`).
@@ -17,6 +17,8 @@ Das Format folgt [Keep a Changelog](https://keepachangelog.com/de/1.1.0/), die V
- Code-Blöcke in Notizen: Sprachauswahl (rund 50 Sprachen, bei Bedarf nachgeladen), Farben beim Tippen im Stil von VS Code Dark+/Light+ für alle Themes, „Kopieren“, Tab/Shift+Tab rückt ein und aus. `/code yaml` oder ```` ```yaml ```` legt die Sprache direkt fest. - Code-Blöcke in Notizen: Sprachauswahl (rund 50 Sprachen, bei Bedarf nachgeladen), Farben beim Tippen im Stil von VS Code Dark+/Light+ für alle Themes, „Kopieren“, Tab/Shift+Tab rückt ein und aus. `/code yaml` oder ```` ```yaml ```` legt die Sprache direkt fest.
### Changed ### Changed
- E-Mails zeigen einen Absendernamen: `SMTP_FROM_NAME` bzw. das neue Feld „Absender-Name“ in Administration → E-Mail (Standard „MindSift“), wenn die Absenderadresse keinen eigenen Namen enthält.
- Slash-Befehle einheitlich englisch: `/image` statt `/bild` (`/bild` findet ihn weiterhin), kürzere Beschreibungen, breiteres Menü ohne Umbrüche.
- `docker-compose.yml` enthält nur noch die Zuordnungen; alle Erklärungen zu den Variablen stehen in `.env.example`. Neu durchgereicht werden `TZ`, `TRUSTED_PROXIES`, `LOGIN_PROTECTION`, `DEMO_RESET_MINUTES` und `LEGAL_*_CONTENT`, die vorher in `.env` gesetzt wirkungslos blieben. - `docker-compose.yml` enthält nur noch die Zuordnungen; alle Erklärungen zu den Variablen stehen in `.env.example`. Neu durchgereicht werden `TZ`, `TRUSTED_PROXIES`, `LOGIN_PROTECTION`, `DEMO_RESET_MINUTES` und `LEGAL_*_CONTENT`, die vorher in `.env` gesetzt wirkungslos blieben.
- Workspace-Seitenleiste wie ein Datei-Explorer: angeheftete Notizen oben, darunter Ordner und Notizen ohne Zwischenüberschriften. - Workspace-Seitenleiste wie ein Datei-Explorer: angeheftete Notizen oben, darunter Ordner und Notizen ohne Zwischenüberschriften.
- Spracheingabe in Quick Dump, im mobilen Erfassen-Sheet und beim Halten des +-Knopfs folgt der Server-Einstellung: mit eigenem Whisper-Server funktioniert sie in allen Browsern (auch Firefox), mit Live-Wellen während der Aufnahme und „Wird erkannt…“ danach. Ohne Unterstützung im Browser oder mit `STT_DISABLED=true` ist das Mikrofon ausgeblendet statt einer Fehlermeldung. - Spracheingabe in Quick Dump, im mobilen Erfassen-Sheet und beim Halten des +-Knopfs folgt der Server-Einstellung: mit eigenem Whisper-Server funktioniert sie in allen Browsern (auch Firefox), mit Live-Wellen während der Aufnahme und „Wird erkannt…“ danach. Ohne Unterstützung im Browser oder mit `STT_DISABLED=true` ist das Mikrofon ausgeblendet statt einer Fehlermeldung.
+1
View File
@@ -68,6 +68,7 @@ Das Frontend liegt per `go:embed` im Binary ([cmd/server/main.go](../cmd/server/
| `MINDA_DEMO` (auch `MINDSIFT_DEMO`, `DEMO_MODE`) | `false` | Demo-Konten anlegen. Die Demo verschickt nichts (keine Alarme, Test- oder Hinweismails, Webhooks), Server-Einstellungen sind schreibgeschützt, Passwort, E-Mail und Konten der gemeinsam genutzten Demo-Konten lassen sich nicht ändern (`notInDemo` in `internal/api/demo.go`) | | `MINDA_DEMO` (auch `MINDSIFT_DEMO`, `DEMO_MODE`) | `false` | Demo-Konten anlegen. Die Demo verschickt nichts (keine Alarme, Test- oder Hinweismails, Webhooks), Server-Einstellungen sind schreibgeschützt, Passwort, E-Mail und Konten der gemeinsam genutzten Demo-Konten lassen sich nicht ändern (`notInDemo` in `internal/api/demo.go`) |
| `DEMO_RESET_MINUTES` | `20` | Demo-Modus: periodischer Reset auf die Beispieldaten (`0` = aus). Feste IDs der Demo-Konten (`usr_demo_admin`, `usr_demo_user`) halten Sitzungen gültig; laufende Anfragen sperren den Reset (`internal/api/demo.go`, Streams ausgenommen); danach `DEMO_RESET` an alle Verbindungen, `/auth/status` liefert `demoNextReset`/`demoResetInSeconds`. | | `DEMO_RESET_MINUTES` | `20` | Demo-Modus: periodischer Reset auf die Beispieldaten (`0` = aus). Feste IDs der Demo-Konten (`usr_demo_admin`, `usr_demo_user`) halten Sitzungen gültig; laufende Anfragen sperren den Reset (`internal/api/demo.go`, Streams ausgenommen); danach `DEMO_RESET` an alle Verbindungen, `/auth/status` liefert `demoNextReset`/`demoResetInSeconds`. |
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`, `SMTP_TLS` | leer / `587` / `true` | Mailversand, auch im Admin-UI einstellbar | | `SMTP_HOST`, `SMTP_PORT`, `SMTP_USER`, `SMTP_PASS`, `SMTP_FROM`, `SMTP_TLS` | leer / `587` / `true` | Mailversand, auch im Admin-UI einstellbar |
| `SMTP_FROM_NAME` | `MindSift` | Anzeigename des Absenders, wenn `SMTP_FROM` nur eine Adresse ist (`From: "MindSift" <adresse>`, der SMTP-Umschlag bleibt die reine Adresse); ein Name in `SMTP_FROM` hat Vorrang |
| `SMTP_SKIP_VERIFY` | `false` | Zertifikat des Mailservers nicht prüfen (nur interne Server mit selbstsigniertem Zertifikat) | | `SMTP_SKIP_VERIFY` | `false` | Zertifikat des Mailservers nicht prüfen (nur interne Server mit selbstsigniertem Zertifikat) |
| `STT_URL` | leer | Eigene Spracherkennung: Basis-URL eines OpenAI-kompatiblen Whisper-Servers (z. B. `http://whisper:8000`, speaches). Gesetzt = Server-Modus, sonst Spracherkennung des Browsers; in der Demo immer Browser | | `STT_URL` | leer | Eigene Spracherkennung: Basis-URL eines OpenAI-kompatiblen Whisper-Servers (z. B. `http://whisper:8000`, speaches). Gesetzt = Server-Modus, sonst Spracherkennung des Browsers; in der Demo immer Browser |
| `STT_MODEL`, `STT_LANGUAGE`, `STT_API_KEY` | `deepdml/faster-whisper-large-v3-turbo-ct2` / `de` / leer | Modell, Sprache (`auto` = erkennen), Schlüssel falls der Whisper-Server einen verlangt | | `STT_MODEL`, `STT_LANGUAGE`, `STT_API_KEY` | `deepdml/faster-whisper-large-v3-turbo-ct2` / `de` / leer | Modell, Sprache (`auto` = erkennen), Schlüssel falls der Whisper-Server einen verlangt |
+1 -1
View File
@@ -56,7 +56,7 @@ func main() {
} }
// Seed SMTP defaults from environment variables into DB if not yet configured in DB // Seed SMTP defaults from environment variables into DB if not yet configured in DB
store.SeedSMTPDefaultsFromEnv(cfg.SMTPHost, cfg.SMTPPort, cfg.SMTPUser, cfg.SMTPPass, cfg.SMTPFrom, cfg.SMTPTLS, cfg.SMTPSkipVerify) store.SeedSMTPDefaultsFromEnv(cfg.SMTPHost, cfg.SMTPPort, cfg.SMTPUser, cfg.SMTPPass, cfg.SMTPFrom, cfg.SMTPFromName, cfg.SMTPTLS, cfg.SMTPSkipVerify)
wsHub := ws.NewHub() wsHub := ws.NewHub()
+1
View File
@@ -29,6 +29,7 @@ services:
- SMTP_USER=${SMTP_USER:-} - SMTP_USER=${SMTP_USER:-}
- SMTP_PASS=${SMTP_PASS:-} - SMTP_PASS=${SMTP_PASS:-}
- SMTP_FROM=${SMTP_FROM:-} - SMTP_FROM=${SMTP_FROM:-}
- SMTP_FROM_NAME=${SMTP_FROM_NAME:-MindSift}
- SMTP_TLS=${SMTP_TLS:-true} - SMTP_TLS=${SMTP_TLS:-true}
- SMTP_SKIP_VERIFY=${SMTP_SKIP_VERIFY:-false} - SMTP_SKIP_VERIFY=${SMTP_SKIP_VERIFY:-false}
# Impressum / Datenschutz # Impressum / Datenschutz
@@ -103,6 +103,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
smtpUser: '', smtpUser: '',
smtpPass: '', smtpPass: '',
smtpFrom: '', smtpFrom: '',
smtpFromName: '',
smtpTls: true, smtpTls: true,
smtpSkipVerify: false, smtpSkipVerify: false,
smtpConfiguredViaEnv: false, smtpConfiguredViaEnv: false,
@@ -133,6 +134,7 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
smtpUser: sys.smtpUser || '', smtpUser: sys.smtpUser || '',
smtpPass: sys.smtpPass || '', smtpPass: sys.smtpPass || '',
smtpFrom: sys.smtpFrom || '', smtpFrom: sys.smtpFrom || '',
smtpFromName: sys.smtpFromName || '',
smtpTls: sys.smtpTls ?? true, smtpTls: sys.smtpTls ?? true,
smtpSkipVerify: Boolean(sys.smtpSkipVerify), smtpSkipVerify: Boolean(sys.smtpSkipVerify),
allowPrivateWebhooks: Boolean(sys.allowPrivateWebhooks), allowPrivateWebhooks: Boolean(sys.allowPrivateWebhooks),
@@ -1258,11 +1260,30 @@ export const AdminSettingsView: React.FC<AdminSettingsViewProps> = ({
onChange={(e) => onChange={(e) =>
setSettings((prev) => ({ ...prev, smtpFrom: e.target.value })) setSettings((prev) => ({ ...prev, smtpFrom: e.target.value }))
} }
placeholder="MindSift <alerts@deinedomain.de>" placeholder="alerts@deinedomain.de"
className="w-full h-9 px-3 input-field" className="w-full h-9 px-3 input-field"
/> />
</div> </div>
<div>
<label className="block text-muted font-medium mb-1">
Absender-Name (`SMTP_FROM_NAME`)
</label>
<input
type="text"
value={settings.smtpFromName || ''}
onChange={(e) =>
setSettings((prev) => ({ ...prev, smtpFromName: e.target.value }))
}
placeholder="MindSift"
className="w-full h-9 px-3 input-field"
/>
<p className="text-[11px] text-faint mt-1">
Erscheint beim Empfänger als Absender. Leer = „MindSift“; ein Name direkt in der Adresse
(„Name &lt;adresse&gt;“) hat Vorrang.
</p>
</div>
<div className="sm:col-span-2 space-y-3 pt-1"> <div className="sm:col-span-2 space-y-3 pt-1">
<div className="flex items-center justify-between gap-3"> <div className="flex items-center justify-between gap-3">
<div> <div>
+1
View File
@@ -203,6 +203,7 @@ export interface SystemSettings {
smtpPass?: string; // write only: the server never sends it back, empty keeps the stored one smtpPass?: string; // write only: the server never sends it back, empty keeps the stored one
smtpPassSet?: boolean; smtpPassSet?: boolean;
smtpFrom?: string; smtpFrom?: string;
smtpFromName?: string; // display name when smtpFrom is a bare address (empty = MindSift)
smtpTls?: boolean; smtpTls?: boolean;
smtpSkipVerify?: boolean; smtpSkipVerify?: boolean;
allowPrivateWebhooks?: boolean; // alarm webhooks may reach the LAN (and plain http) allowPrivateWebhooks?: boolean; // alarm webhooks may reach the LAN (and plain http)
+20 -1
View File
@@ -20,6 +20,7 @@ import (
// smtpSettings is the effective mail server configuration (admin settings, else environment). // smtpSettings is the effective mail server configuration (admin settings, else environment).
type smtpSettings struct { type smtpSettings struct {
Host, Port, User, Pass, From string Host, Port, User, Pass, From string
FromName string // display name when From is a bare address
TLS bool // 465: implicit TLS, otherwise STARTTLS (required when set) TLS bool // 465: implicit TLS, otherwise STARTTLS (required when set)
SkipVerify bool // accept self-signed / mismatching certificates SkipVerify bool // accept self-signed / mismatching certificates
@@ -46,12 +47,14 @@ func (s *Server) smtpSettings() (smtpSettings, error) {
cfg = smtpSettings{ cfg = smtpSettings{
Host: strings.TrimSpace(sys.SMTPHost), Port: strings.TrimSpace(sys.SMTPPort), Host: strings.TrimSpace(sys.SMTPHost), Port: strings.TrimSpace(sys.SMTPPort),
User: strings.TrimSpace(sys.SMTPUser), Pass: sys.SMTPPass, From: strings.TrimSpace(sys.SMTPFrom), User: strings.TrimSpace(sys.SMTPUser), Pass: sys.SMTPPass, From: strings.TrimSpace(sys.SMTPFrom),
FromName: strings.TrimSpace(sys.SMTPFromName),
TLS: sys.SMTPTLS, SkipVerify: sys.SMTPSkipVerify, TLS: sys.SMTPTLS, SkipVerify: sys.SMTPSkipVerify,
} }
} else if strings.TrimSpace(s.cfg.SMTPHost) != "" { } else if strings.TrimSpace(s.cfg.SMTPHost) != "" {
cfg = smtpSettings{ cfg = smtpSettings{
Host: strings.TrimSpace(s.cfg.SMTPHost), Port: strings.TrimSpace(s.cfg.SMTPPort), 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), User: strings.TrimSpace(s.cfg.SMTPUser), Pass: s.cfg.SMTPPass, From: strings.TrimSpace(s.cfg.SMTPFrom),
FromName: strings.TrimSpace(s.cfg.SMTPFromName),
TLS: s.cfg.SMTPTLS, SkipVerify: s.cfg.SMTPSkipVerify, TLS: s.cfg.SMTPTLS, SkipVerify: s.cfg.SMTPSkipVerify,
} }
} }
@@ -61,6 +64,9 @@ func (s *Server) smtpSettings() (smtpSettings, error) {
if cfg.Port == "" { if cfg.Port == "" {
cfg.Port = "587" cfg.Port = "587"
} }
if cfg.FromName == "" {
cfg.FromName = "MindSift"
}
if cfg.From == "" { if cfg.From == "" {
if strings.Contains(cfg.User, "@") { if strings.Contains(cfg.User, "@") {
cfg.From = 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 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 // 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. // (umlauts, emoji) and a quoted-printable UTF-8 body with CRLF line endings.
func buildMessage(from, to, subject, body string, now time.Time) []byte { 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 // 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. // properly, honour SkipVerify and report the exact step a server refuses.
func sendMailOnce(cfg smtpSettings, to, subject, body string) error { 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 { if err != nil {
return &smtpStepError{"Absender", err} return &smtpStepError{"Absender", err}
} }
+14
View File
@@ -270,3 +270,17 @@ func TestAuthLoginOnlyAndBothFailing(t *testing.T) {
t.Fatalf("expected both mechanisms and the server hint in the error, got %v", err) 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)
}
}
}
+3
View File
@@ -37,6 +37,8 @@ type Config struct {
SMTPUser string SMTPUser string
SMTPPass string SMTPPass string
SMTPFrom string SMTPFrom string
// SMTPFromName is the sender's display name when SMTP_FROM is a bare address
SMTPFromName string
SMTPTLS bool SMTPTLS bool
// SMTPSkipVerify accepts self-signed or mismatching certificates (internal mail servers) // SMTPSkipVerify accepts self-signed or mismatching certificates (internal mail servers)
SMTPSkipVerify bool SMTPSkipVerify bool
@@ -220,6 +222,7 @@ func Load() *Config {
SMTPUser: smtpUser, SMTPUser: smtpUser,
SMTPPass: smtpPass, SMTPPass: smtpPass,
SMTPFrom: smtpFrom, SMTPFrom: smtpFrom,
SMTPFromName: getEnv("SMTP_FROM_NAME", "MindSift"),
SMTPTLS: smtpTLS, SMTPTLS: smtpTLS,
SMTPSkipVerify: smtpSkipVerify, SMTPSkipVerify: smtpSkipVerify,
Speech: SpeechConfig{ Speech: SpeechConfig{
+2
View File
@@ -290,6 +290,8 @@ type SystemSettings struct {
SMTPPass string `json:"smtpPass"` SMTPPass string `json:"smtpPass"`
SMTPPassSet bool `json:"smtpPassSet,omitempty"` SMTPPassSet bool `json:"smtpPassSet,omitempty"`
SMTPFrom string `json:"smtpFrom"` SMTPFrom string `json:"smtpFrom"`
// SMTPFromName is shown as sender when SMTPFrom is a bare address (empty = "MindSift")
SMTPFromName string `json:"smtpFromName"`
SMTPTLS bool `json:"smtpTls"` SMTPTLS bool `json:"smtpTls"`
SMTPSkipVerify bool `json:"smtpSkipVerify"` // accept self-signed certificates SMTPSkipVerify bool `json:"smtpSkipVerify"` // accept self-signed certificates
// AllowPrivateWebhooks lets alarm webhooks reach LAN addresses (and plain http); localhost and cloud // AllowPrivateWebhooks lets alarm webhooks reach LAN addresses (and plain http); localhost and cloud
+6 -1
View File
@@ -92,7 +92,7 @@ func (s *Store) ExportUserData(userID string) (*domain.FullExport, error) {
// --- SYSTEM SETTINGS --- // --- SYSTEM SETTINGS ---
func (s *Store) SeedSMTPDefaultsFromEnv(host, port, user, pass, from string, tls, skipVerify bool) { func (s *Store) SeedSMTPDefaultsFromEnv(host, port, user, pass, from, fromName string, tls, skipVerify bool) {
cur, err := s.GetSystemSettings() cur, err := s.GetSystemSettings()
if err != nil || cur == nil { if err != nil || cur == nil {
return return
@@ -106,6 +106,7 @@ func (s *Store) SeedSMTPDefaultsFromEnv(host, port, user, pass, from string, tls
cur.SMTPUser = strings.TrimSpace(user) cur.SMTPUser = strings.TrimSpace(user)
cur.SMTPPass = pass cur.SMTPPass = pass
cur.SMTPFrom = strings.TrimSpace(from) cur.SMTPFrom = strings.TrimSpace(from)
cur.SMTPFromName = strings.TrimSpace(fromName)
cur.SMTPTLS = tls cur.SMTPTLS = tls
cur.SMTPSkipVerify = skipVerify cur.SMTPSkipVerify = skipVerify
_ = s.UpdateSystemSettings(cur) _ = s.UpdateSystemSettings(cur)
@@ -151,6 +152,8 @@ func (s *Store) GetSystemSettings() (*domain.SystemSettings, error) {
settings.SMTPPass = v settings.SMTPPass = v
case "smtp_from": case "smtp_from":
settings.SMTPFrom = v settings.SMTPFrom = v
case "smtp_from_name":
settings.SMTPFromName = v
case "smtp_tls": case "smtp_tls":
settings.SMTPTLS = (v != "false" && v != "0") settings.SMTPTLS = (v != "false" && v != "0")
case "smtp_skip_verify": case "smtp_skip_verify":
@@ -212,6 +215,7 @@ func (s *Store) UpdateSystemSettings(settings *domain.SystemSettings) error {
('smtp_user', ?), ('smtp_user', ?),
('smtp_pass', ?), ('smtp_pass', ?),
('smtp_from', ?), ('smtp_from', ?),
('smtp_from_name', ?),
('smtp_tls', ?), ('smtp_tls', ?),
('smtp_skip_verify', ?), ('smtp_skip_verify', ?),
('allow_private_webhooks', ?), ('allow_private_webhooks', ?),
@@ -227,6 +231,7 @@ func (s *Store) UpdateSystemSettings(settings *domain.SystemSettings) error {
strings.TrimSpace(settings.SMTPUser), strings.TrimSpace(settings.SMTPUser),
settings.SMTPPass, settings.SMTPPass,
strings.TrimSpace(settings.SMTPFrom), strings.TrimSpace(settings.SMTPFrom),
strings.TrimSpace(settings.SMTPFromName),
tlsStr, tlsStr,
skipVerifyStr, skipVerifyStr,
privateHooksStr, privateHooksStr,