Browse Source

fix(settings): show the SMTP failure reason instead of a raw i18n key

classifySMTPError returned keys already carrying "pages.settings.", while the
four keys TestConnection returns directly do not, and the alert renders every
Message under that one prefix. Any classified failure therefore looked up
pages.settings.pages.settings.smtpErrorAuth, which does not exist, so the panel
printed the key instead of "Authentication failed — check username and
password". The unknown case was worse: it appended the raw error to the key, and
its own text interpolated {{ .Error }}, Go template syntax the frontend's
i18next never fills.

Return the keys unprefixed like the rest, and point the unknown case at the
panel log, which already carries the underlying error.
Sanaei 8 hours ago
parent
commit
efc603f59c

+ 11 - 9
internal/web/service/email/email.go

@@ -321,28 +321,30 @@ func (s *EmailService) SendTest() error {
 	)
 }
 
-// classifySMTPError maps raw SMTP errors to human-readable messages.
+// classifySMTPError maps a raw SMTP error to an i18n key. The key is returned
+// unprefixed: the caller renders it under "pages.settings.", as does every
+// other Message this file produces.
 func classifySMTPError(err error) string {
 	msg := err.Error()
 	msgLower := strings.ToLower(msg)
 
 	switch {
 	case strings.Contains(msg, "535") || strings.Contains(msgLower, "authentication"):
-		return "pages.settings.smtpErrorAuth"
+		return "smtpErrorAuth"
 	case strings.Contains(msg, "534") || strings.Contains(msgLower, "starttls"):
-		return "pages.settings.smtpErrorStarttls"
+		return "smtpErrorStarttls"
 	case strings.Contains(msg, "465") || strings.Contains(msgLower, "tls"):
-		return "pages.settings.smtpErrorTls"
+		return "smtpErrorTls"
 	case strings.Contains(msgLower, "connection refused") || strings.Contains(msgLower, "dial"):
-		return "pages.settings.smtpErrorRefused"
+		return "smtpErrorRefused"
 	case strings.Contains(msgLower, "timeout"):
-		return "pages.settings.smtpErrorTimeout"
+		return "smtpErrorTimeout"
 	case strings.Contains(msg, "550") || strings.Contains(msgLower, "relay"):
-		return "pages.settings.smtpErrorRelay"
+		return "smtpErrorRelay"
 	case strings.Contains(msgLower, "eof"):
-		return "pages.settings.smtpErrorEof"
+		return "smtpErrorEof"
 	default:
-		return fmt.Sprintf("pages.settings.smtpErrorUnknown: %s", msg)
+		return "smtpErrorUnknown"
 	}
 }
 

+ 31 - 0
internal/web/service/email/email_test.go

@@ -2,6 +2,7 @@ package email
 
 import (
 	"bufio"
+	"errors"
 	"fmt"
 	"io"
 	"mime"
@@ -222,3 +223,33 @@ func TestBuildMessageStripsHeaderInjection(t *testing.T) {
 		t.Errorf("injected X-Evil header leaked: %q", got)
 	}
 }
+
+// Every Message this file emits is rendered by the frontend under a single
+// "pages.settings." prefix, so a key that carries its own resolves to nothing
+// and the alert shows the raw key instead of the classified reason.
+func TestClassifySMTPErrorReturnsUnprefixedKeys(t *testing.T) {
+	cases := []struct {
+		name string
+		err  error
+		want string
+	}{
+		{"auth", errors.New("535 5.7.8 Authentication credentials invalid"), "smtpErrorAuth"},
+		{"starttls", errors.New("534 must issue a STARTTLS command first"), "smtpErrorStarttls"},
+		{"refused", errors.New("dial tcp 127.0.0.1:25: connection refused"), "smtpErrorRefused"},
+		{"timeout", errors.New("i/o timeout"), "smtpErrorTimeout"},
+		{"relay", errors.New("550 relay not permitted"), "smtpErrorRelay"},
+		{"eof", errors.New("unexpected EOF"), "smtpErrorEof"},
+		{"unknown", errors.New("something nobody classified"), "smtpErrorUnknown"},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			got := classifySMTPError(tc.err)
+			if got != tc.want {
+				t.Fatalf("classifySMTPError = %q, want %q", got, tc.want)
+			}
+			if strings.HasPrefix(got, "pages.settings.") {
+				t.Fatalf("key %q carries the prefix the frontend adds, so it resolves to nothing", got)
+			}
+		})
+	}
+}

+ 1 - 1
internal/web/translation/ar-EG.json

@@ -1394,7 +1394,7 @@
       "smtpErrorTimeout": "انتهت مهلة الاتصال — تعذّر الوصول إلى الخادم",
       "smtpErrorRelay": "الخادم يرفض الإرسال من هذا العنوان",
       "smtpErrorEof": "تم إغلاق الاتصال من قبل الخادم",
-      "smtpErrorUnknown": "خطأ SMTP: {{ .Error }}",
+      "smtpErrorUnknown": "خطأ SMTP — راجع سجل اللوحة للتفاصيل",
       "eventMemoryHigh": "ارتفاع استخدام الذاكرة (%)",
       "remarkTemplate": "قالب الملاحظة",
       "remarkTemplateDesc": "عند تعيينه، يحل هذا محل نموذج الملاحظة لكل رابط اشتراك — اكتب صيغتك الخاصة باستخدام رموز المتغيرات (استخدم الزر لإدراجها). اتركه فارغاً لاستخدام النموذج أعلاه.",

+ 1 - 1
internal/web/translation/en-US.json

@@ -1516,7 +1516,7 @@
       "smtpErrorTimeout": "Connection timeout — host unreachable",
       "smtpErrorRelay": "Server rejects sending from this address",
       "smtpErrorEof": "Connection closed by server",
-      "smtpErrorUnknown": "SMTP error: {{ .Error }}",
+      "smtpErrorUnknown": "SMTP error — see the panel log for details",
       "eventMemoryHigh": "Memory high (%)",
       "validation": {
         "pathLeadingSlash": "Path must start with /"

+ 1 - 1
internal/web/translation/es-ES.json

@@ -1394,7 +1394,7 @@
       "smtpErrorTimeout": "Tiempo de conexión agotado: servidor inaccesible",
       "smtpErrorRelay": "El servidor rechaza el envío desde esta dirección",
       "smtpErrorEof": "Conexión cerrada por el servidor",
-      "smtpErrorUnknown": "Error de SMTP: {{ .Error }}",
+      "smtpErrorUnknown": "Error de SMTP — consulta el registro del panel",
       "eventMemoryHigh": "Uso de memoria alto (%)",
       "remarkTemplate": "Plantilla de notas",
       "remarkTemplateDesc": "Cuando se define, esto reemplaza el modelo de notas para cada enlace de suscripción — escribe tu propio formato con los tokens de variable (usa el botón para insertarlos). Déjalo vacío para usar el modelo anterior.",

+ 1 - 1
internal/web/translation/fa-IR.json

@@ -1398,7 +1398,7 @@
       "smtpErrorTimeout": "مهلت اتصال به پایان رسید — میزبان در دسترس نیست",
       "smtpErrorRelay": "سرور ارسال از این آدرس را رد می‌کند",
       "smtpErrorEof": "اتصال توسط سرور بسته شد",
-      "smtpErrorUnknown": "خطای SMTP: {{ .Error }}",
+      "smtpErrorUnknown": "خطای SMTP — جزئیات در لاگ پنل",
       "eventMemoryHigh": "مصرف حافظه بالا (%)",
       "validation": {
         "pathLeadingSlash": "مسیر باید با / شروع شود"

+ 1 - 1
internal/web/translation/id-ID.json

@@ -1394,7 +1394,7 @@
       "smtpErrorTimeout": "Koneksi waktu habis — host tidak dapat dijangkau",
       "smtpErrorRelay": "Server menolak pengiriman dari alamat ini",
       "smtpErrorEof": "Koneksi ditutup oleh server",
-      "smtpErrorUnknown": "Kesalahan SMTP: {{ .Error }}",
+      "smtpErrorUnknown": "Kesalahan SMTP — lihat log panel",
       "eventMemoryHigh": "Penggunaan memori tinggi (%)",
       "remarkTemplate": "Templat Catatan",
       "remarkTemplateDesc": "Jika diatur, ini menggantikan model catatan untuk setiap tautan langganan — tulis format Anda sendiri dengan token variabel (gunakan tombol untuk menyisipkannya). Biarkan kosong untuk memakai model di atas.",

+ 1 - 1
internal/web/translation/ja-JP.json

@@ -1394,7 +1394,7 @@
       "smtpErrorTimeout": "接続がタイムアウトしました — ホストに到達できません",
       "smtpErrorRelay": "サーバーはこのアドレスからの送信を拒否しています",
       "smtpErrorEof": "サーバーによって接続が閉じられました",
-      "smtpErrorUnknown": "SMTPエラー: {{ .Error }}",
+      "smtpErrorUnknown": "SMTPエラー — 詳細はパネルのログを参照",
       "eventMemoryHigh": "メモリ使用率が高い (%)",
       "remarkTemplate": "備考テンプレート",
       "remarkTemplateDesc": "設定すると、すべてのサブスクリプションリンクの備考モデルを置き換えます — 変数トークンを使って独自の形式を記述してください(ボタンで挿入できます)。空欄にすると上記のモデルが使用されます。",

+ 1 - 1
internal/web/translation/pt-BR.json

@@ -1394,7 +1394,7 @@
       "smtpErrorTimeout": "Tempo de conexão esgotado — host inacessível",
       "smtpErrorRelay": "O servidor rejeita o envio a partir deste endereço",
       "smtpErrorEof": "Conexão encerrada pelo servidor",
-      "smtpErrorUnknown": "Erro de SMTP: {{ .Error }}",
+      "smtpErrorUnknown": "Erro de SMTP — consulte o log do painel",
       "eventMemoryHigh": "Uso de memória alto (%)",
       "remarkTemplate": "Modelo de Observação",
       "remarkTemplateDesc": "Quando definido, isto substitui o modelo de observação de cada link de assinatura — escreva seu próprio formato com os tokens de variáveis (use o botão para inseri-los). Deixe vazio para usar o modelo acima.",

+ 1 - 1
internal/web/translation/ru-RU.json

@@ -1394,7 +1394,7 @@
       "smtpErrorTimeout": "Таймаут соединения — хост недоступен",
       "smtpErrorRelay": "Сервер отклоняет отправку с этого адреса",
       "smtpErrorEof": "Соединение закрыто сервером",
-      "smtpErrorUnknown": "Ошибка SMTP: {{ .Error }}",
+      "smtpErrorUnknown": "Ошибка SMTP — подробности в журнале панели",
       "eventMemoryHigh": "Превышение порога памяти (%)",
       "remarkTemplate": "Шаблон примечания",
       "remarkTemplateDesc": "Если задан, заменяет модель примечания для каждой ссылки подписки — задайте собственный формат с помощью токенов переменных (используйте кнопку для их вставки). Оставьте пустым, чтобы использовать модель выше.",

+ 1 - 1
internal/web/translation/tr-TR.json

@@ -1394,7 +1394,7 @@
       "smtpErrorTimeout": "Bağlantı zaman aşımına uğradı — sunucuya ulaşılamıyor",
       "smtpErrorRelay": "Sunucu bu adresten gönderimi reddediyor",
       "smtpErrorEof": "Bağlantı sunucu tarafından kapatıldı",
-      "smtpErrorUnknown": "SMTP hatası: {{ .Error }}",
+      "smtpErrorUnknown": "SMTP hatası — ayrıntılar için panel günlüğüne bakın",
       "eventMemoryHigh": "Bellek kullanımı yüksek (%)",
       "remarkTemplate": "Açıklama Şablonu",
       "remarkTemplateDesc": "Ayarlandığında, her abonelik bağlantısının açıklama modelinin yerini alır — değişken belirteçleriyle kendi formatınızı yazın (eklemek için düğmeyi kullanın). Yukarıdaki modeli kullanmak için boş bırakın.",

+ 1 - 1
internal/web/translation/uk-UA.json

@@ -1394,7 +1394,7 @@
       "smtpErrorTimeout": "Час очікування з'єднання вичерпано — хост недоступний",
       "smtpErrorRelay": "Сервер відхиляє надсилання з цієї адреси",
       "smtpErrorEof": "З'єднання закрито сервером",
-      "smtpErrorUnknown": "Помилка SMTP: {{ .Error }}",
+      "smtpErrorUnknown": "Помилка SMTP — подробиці в журналі панелі",
       "eventMemoryHigh": "Високе використання пам'яті (%)",
       "remarkTemplate": "Шаблон примітки",
       "remarkTemplateDesc": "Якщо задано, це замінює модель примітки для кожного посилання підписки — напишіть власний формат із токенами змінних (використовуйте кнопку для їх вставлення). Залиште порожнім, щоб використовувати модель вище.",

+ 1 - 1
internal/web/translation/vi-VN.json

@@ -1394,7 +1394,7 @@
       "smtpErrorTimeout": "Hết thời gian kết nối — không thể truy cập máy chủ",
       "smtpErrorRelay": "Máy chủ từ chối gửi từ địa chỉ này",
       "smtpErrorEof": "Kết nối đã bị máy chủ đóng",
-      "smtpErrorUnknown": "Lỗi SMTP: {{ .Error }}",
+      "smtpErrorUnknown": "Lỗi SMTP — xem nhật ký bảng điều khiển",
       "eventMemoryHigh": "Sử dụng bộ nhớ cao (%)",
       "remarkTemplate": "Mẫu ghi chú",
       "remarkTemplateDesc": "Khi được đặt, mục này thay thế mô hình ghi chú cho mọi liên kết đăng ký — hãy viết định dạng riêng của bạn bằng các token biến (dùng nút để chèn chúng). Để trống để dùng mô hình ở trên.",

+ 1 - 1
internal/web/translation/zh-CN.json

@@ -1394,7 +1394,7 @@
       "smtpErrorTimeout": "连接超时——主机无法访问",
       "smtpErrorRelay": "服务器拒绝从此地址发送",
       "smtpErrorEof": "连接被服务器关闭",
-      "smtpErrorUnknown": "SMTP 错误:{{ .Error }}",
+      "smtpErrorUnknown": "SMTP 错误 — 详情见面板日志",
       "eventMemoryHigh": "内存使用率高 (%)",
       "remarkTemplate": "备注模板",
       "remarkTemplateDesc": "设置后,将替换每个订阅链接的备注模型 — 使用变量标记编写您自己的格式(用按钮插入它们)。留空则使用上方的模型。",

+ 1 - 1
internal/web/translation/zh-TW.json

@@ -1394,7 +1394,7 @@
       "smtpErrorTimeout": "連線逾時 — 無法連線至主機",
       "smtpErrorRelay": "伺服器拒絕從此地址傳送",
       "smtpErrorEof": "連線已被伺服器關閉",
-      "smtpErrorUnknown": "SMTP 錯誤:{{ .Error }}",
+      "smtpErrorUnknown": "SMTP 錯誤 — 詳情見面板日誌",
       "eventMemoryHigh": "記憶體使用率高 (%)",
       "remarkTemplate": "備註範本",
       "remarkTemplateDesc": "設定後,這將取代每個訂閱連結的備註模型——使用變數標記撰寫您自己的格式(使用按鈕來插入)。留空則使用上方的模型。",