diff --git a/frontend/public/openapi.json b/frontend/public/openapi.json index f3c880afb..6e18c70a9 100644 --- a/frontend/public/openapi.json +++ b/frontend/public/openapi.json @@ -142,6 +142,12 @@ "smtpEncryptionType": { "type": "string" }, + "smtpFrom": { + "type": "string" + }, + "smtpFromName": { + "type": "string" + }, "smtpHost": { "type": "string" }, @@ -375,6 +381,8 @@ "smtpEnable", "smtpEnabledEvents", "smtpEncryptionType", + "smtpFrom", + "smtpFromName", "smtpHost", "smtpMemory", "smtpPassword", @@ -575,6 +583,12 @@ "smtpEncryptionType": { "type": "string" }, + "smtpFrom": { + "type": "string" + }, + "smtpFromName": { + "type": "string" + }, "smtpHost": { "type": "string" }, @@ -815,6 +829,8 @@ "smtpEnable", "smtpEnabledEvents", "smtpEncryptionType", + "smtpFrom", + "smtpFromName", "smtpHost", "smtpMemory", "smtpPassword", diff --git a/frontend/src/generated/examples.ts b/frontend/src/generated/examples.ts index 57b2524c2..4390fd5c2 100644 --- a/frontend/src/generated/examples.ts +++ b/frontend/src/generated/examples.ts @@ -35,6 +35,8 @@ export const EXAMPLES: Record = { "smtpEnable": false, "smtpEnabledEvents": "", "smtpEncryptionType": "", + "smtpFrom": "", + "smtpFromName": "", "smtpHost": "", "smtpMemory": 0, "smtpPassword": "", @@ -138,6 +140,8 @@ export const EXAMPLES: Record = { "smtpEnable": false, "smtpEnabledEvents": "", "smtpEncryptionType": "", + "smtpFrom": "", + "smtpFromName": "", "smtpHost": "", "smtpMemory": 0, "smtpPassword": "", diff --git a/frontend/src/generated/schemas.ts b/frontend/src/generated/schemas.ts index 6528d7392..df62bd33a 100644 --- a/frontend/src/generated/schemas.ts +++ b/frontend/src/generated/schemas.ts @@ -116,6 +116,12 @@ export const SCHEMAS: Record = { "smtpEncryptionType": { "type": "string" }, + "smtpFrom": { + "type": "string" + }, + "smtpFromName": { + "type": "string" + }, "smtpHost": { "type": "string" }, @@ -349,6 +355,8 @@ export const SCHEMAS: Record = { "smtpEnable", "smtpEnabledEvents", "smtpEncryptionType", + "smtpFrom", + "smtpFromName", "smtpHost", "smtpMemory", "smtpPassword", @@ -549,6 +557,12 @@ export const SCHEMAS: Record = { "smtpEncryptionType": { "type": "string" }, + "smtpFrom": { + "type": "string" + }, + "smtpFromName": { + "type": "string" + }, "smtpHost": { "type": "string" }, @@ -789,6 +803,8 @@ export const SCHEMAS: Record = { "smtpEnable", "smtpEnabledEvents", "smtpEncryptionType", + "smtpFrom", + "smtpFromName", "smtpHost", "smtpMemory", "smtpPassword", diff --git a/frontend/src/generated/types.ts b/frontend/src/generated/types.ts index f17635a56..81771963f 100644 --- a/frontend/src/generated/types.ts +++ b/frontend/src/generated/types.ts @@ -41,6 +41,8 @@ export interface AllSetting { smtpEnable: boolean; smtpEnabledEvents: string; smtpEncryptionType: string; + smtpFrom: string; + smtpFromName: string; smtpHost: string; smtpMemory: number; smtpPassword: string; @@ -145,6 +147,8 @@ export interface AllSettingView { smtpEnable: boolean; smtpEnabledEvents: string; smtpEncryptionType: string; + smtpFrom: string; + smtpFromName: string; smtpHost: string; smtpMemory: number; smtpPassword: string; diff --git a/frontend/src/generated/zod.ts b/frontend/src/generated/zod.ts index 7f0720c21..14e4f3861 100644 --- a/frontend/src/generated/zod.ts +++ b/frontend/src/generated/zod.ts @@ -53,6 +53,8 @@ export const AllSettingSchema = z.object({ smtpEnable: z.boolean(), smtpEnabledEvents: z.string(), smtpEncryptionType: z.string(), + smtpFrom: z.string(), + smtpFromName: z.string(), smtpHost: z.string(), smtpMemory: z.number().int().min(0).max(100), smtpPassword: z.string(), @@ -158,6 +160,8 @@ export const AllSettingViewSchema = z.object({ smtpEnable: z.boolean(), smtpEnabledEvents: z.string(), smtpEncryptionType: z.string(), + smtpFrom: z.string(), + smtpFromName: z.string(), smtpHost: z.string(), smtpMemory: z.number().int().min(0).max(100), smtpPassword: z.string(), diff --git a/frontend/src/models/setting.ts b/frontend/src/models/setting.ts index c2eca193b..79fab8804 100644 --- a/frontend/src/models/setting.ts +++ b/frontend/src/models/setting.ts @@ -91,6 +91,8 @@ export class AllSetting { smtpPort = 587; smtpUsername = ''; smtpPassword = ''; + smtpFrom = ''; + smtpFromName = ''; smtpTo = ''; smtpEncryptionType = 'starttls'; smtpEnabledEvents = ''; diff --git a/frontend/src/pages/settings/EmailTab.tsx b/frontend/src/pages/settings/EmailTab.tsx index 975dd37ad..250a89009 100644 --- a/frontend/src/pages/settings/EmailTab.tsx +++ b/frontend/src/pages/settings/EmailTab.tsx @@ -82,6 +82,16 @@ export default function EmailTab({ allSetting, updateSetting }: EmailTabProps) { onClearArmedChange={(armed) => updateSetting({ clearSmtpPassword: armed })} /> + + updateSetting({ smtpFrom: e.target.value })} /> + + + + updateSetting({ smtpFromName: e.target.value })} /> + + updateSetting({ smtpTo: e.target.value })} /> diff --git a/internal/web/entity/check_valid_test.go b/internal/web/entity/check_valid_test.go new file mode 100644 index 000000000..78744f65f --- /dev/null +++ b/internal/web/entity/check_valid_test.go @@ -0,0 +1,29 @@ +package entity + +import "testing" + +func TestCheckValidSmtpFrom(t *testing.T) { + base := func() *AllSetting { + return &AllSetting{WebPort: 2053, SubPort: 2096} + } + + for _, v := range []string{"", "panel@example.com"} { + s := base() + s.SmtpFrom = v + if err := s.CheckValid(); err != nil { + t.Errorf("CheckValid with smtpFrom=%q: unexpected error %v", v, err) + } + } + + for _, v := range []string{ + "not-an-address", + "panel@example.com\r\nBcc: evil@example.com", + "a@b\nSubject: injected", + } { + s := base() + s.SmtpFrom = v + if err := s.CheckValid(); err == nil { + t.Errorf("CheckValid with smtpFrom=%q: want error, got nil", v) + } + } +} diff --git a/internal/web/entity/entity.go b/internal/web/entity/entity.go index 910fa467c..5fc087711 100644 --- a/internal/web/entity/entity.go +++ b/internal/web/entity/entity.go @@ -4,6 +4,7 @@ import ( "crypto/tls" "math" "net" + "net/mail" "strings" "time" @@ -50,6 +51,8 @@ type AllSetting struct { SmtpPort int `json:"smtpPort" form:"smtpPort" validate:"gte=1,lte=65535"` SmtpUsername string `json:"smtpUsername" form:"smtpUsername"` SmtpPassword string `json:"smtpPassword" form:"smtpPassword"` + SmtpFrom string `json:"smtpFrom" form:"smtpFrom"` + SmtpFromName string `json:"smtpFromName" form:"smtpFromName"` SmtpTo string `json:"smtpTo" form:"smtpTo"` SmtpEncryptionType string `json:"smtpEncryptionType" form:"smtpEncryptionType"` SmtpEnabledEvents string `json:"smtpEnabledEvents" form:"smtpEnabledEvents"` @@ -241,6 +244,12 @@ func (s *AllSetting) CheckValid() error { return common.NewError("time location not exist:", s.TimeLocation) } + if s.SmtpFrom != "" { + if _, err := mail.ParseAddress(s.SmtpFrom); err != nil { + return common.NewError("SMTP from address is not valid:", s.SmtpFrom) + } + } + return nil } diff --git a/internal/web/service/email/email.go b/internal/web/service/email/email.go index 2dfc78d5c..1aca8b0d6 100644 --- a/internal/web/service/email/email.go +++ b/internal/web/service/email/email.go @@ -2,9 +2,13 @@ package email import ( "context" + "crypto/rand" "crypto/tls" + "encoding/hex" "fmt" + "mime" "net" + "net/mail" "net/smtp" "strings" "time" @@ -41,10 +45,15 @@ func (s *EmailService) Send(subject, body string) error { } username, _ := s.settingService.GetSmtpUsername() password, _ := s.settingService.GetSmtpPassword() + fromAddr, _ := s.settingService.GetSmtpFrom() + fromName, _ := s.settingService.GetSmtpFromName() toStr, _ := s.settingService.GetSmtpTo() encryptionType, _ := s.settingService.GetSmtpEncryptionType() - from := username + from := fromAddr + if from == "" { + from = username + } if from == "" { return fmt.Errorf("smtp from not configured") } @@ -55,7 +64,7 @@ func (s *EmailService) Send(subject, body string) error { } addr := net.JoinHostPort(host, fmt.Sprintf("%d", port)) - msg := buildMessage(from, recipients, subject, body) + msg := buildMessage(from, fromName, recipients, subject, body) // Authenticate only when credentials are set. Go's PlainAuth refuses to run // over the unencrypted "none" transport, so an open relay must use nil auth. @@ -98,10 +107,15 @@ func (s *EmailService) TestConnection() SMTPTestResult { } username, _ := s.settingService.GetSmtpUsername() password, _ := s.settingService.GetSmtpPassword() + fromAddr, _ := s.settingService.GetSmtpFrom() + fromName, _ := s.settingService.GetSmtpFromName() toStr, _ := s.settingService.GetSmtpTo() encryptionType, _ := s.settingService.GetSmtpEncryptionType() - from := username + from := fromAddr + if from == "" { + from = username + } recipients := parseRecipients(toStr) if len(recipients) == 0 { @@ -166,7 +180,7 @@ func (s *EmailService) TestConnection() SMTPTestResult { } } - msg := buildMessage(from, recipients, "[3x-ui] Test email", + msg := buildMessage(from, fromName, recipients, "[3x-ui] Test email", `

Test email from 3x-ui

If you received this, SMTP is configured correctly.

@@ -280,18 +294,37 @@ func parseRecipients(toStr string) []string { return out } -func buildMessage(from string, to []string, subject, body string) []byte { - headers := map[string]string{ - "From": from, - "To": strings.Join(to, ","), - "Subject": subject, - "MIME-Version": "1.0", - "Content-Type": "text/html; charset=utf-8", +// buildMessage assembles an RFC 5322 message. It emits the two mandatory +// header fields (Date, From) plus Message-ID, so strict receivers such as Gmail +// accept it and spam filters do not penalize a missing date or message id. The +// From header is a proper name-addr ("Name" ) via net/mail, and a +// non-ASCII subject is RFC 2047 encoded. +// headerSanitizer drops CR/LF so a crafted address or name cannot inject extra +// header lines. Configured addresses are already validated at save time +// (entity.AllSetting.CheckValid), this is defense in depth for buildMessage. +var headerSanitizer = strings.NewReplacer("\r", "", "\n", "") + +func buildMessage(fromAddr, fromName string, to []string, subject, body string) []byte { + fromAddr = headerSanitizer.Replace(fromAddr) + fromName = headerSanitizer.Replace(fromName) + from := (&mail.Address{Name: fromName, Address: fromAddr}).String() + + domain := "localhost" + if at := strings.LastIndex(fromAddr, "@"); at >= 0 && at+1 < len(fromAddr) { + domain = fromAddr[at+1:] } + var token [16]byte + _, _ = rand.Read(token[:]) + messageID := fmt.Sprintf("<%s@%s>", hex.EncodeToString(token[:]), domain) + var msg strings.Builder - for k, v := range headers { - fmt.Fprintf(&msg, "%s: %s\r\n", k, v) - } + fmt.Fprintf(&msg, "Date: %s\r\n", time.Now().Format(time.RFC1123Z)) + fmt.Fprintf(&msg, "From: %s\r\n", from) + fmt.Fprintf(&msg, "To: %s\r\n", strings.Join(to, ", ")) + fmt.Fprintf(&msg, "Message-ID: %s\r\n", messageID) + fmt.Fprintf(&msg, "Subject: %s\r\n", mime.QEncoding.Encode("utf-8", subject)) + msg.WriteString("MIME-Version: 1.0\r\n") + msg.WriteString("Content-Type: text/html; charset=utf-8\r\n") msg.WriteString("\r\n") msg.WriteString(body) return []byte(msg.String()) diff --git a/internal/web/service/email/email_test.go b/internal/web/service/email/email_test.go new file mode 100644 index 000000000..225f3cef6 --- /dev/null +++ b/internal/web/service/email/email_test.go @@ -0,0 +1,81 @@ +package email + +import ( + "io" + "mime" + "net/mail" + "strings" + "testing" +) + +func TestBuildMessageIsRFC5322(t *testing.T) { + raw := buildMessage("panel@example.com", "3x-ui", []string{"a@example.com", "b@example.com"}, "Тест", "hi") + + msg, err := mail.ReadMessage(strings.NewReader(string(raw))) + if err != nil { + t.Fatalf("message does not parse as RFC 5322: %v", err) + } + + from, err := mail.ParseAddress(msg.Header.Get("From")) + if err != nil { + t.Fatalf("From header does not parse: %v", err) + } + if from.Name != "3x-ui" || from.Address != "panel@example.com" { + t.Errorf("From = %q <%q>, want name %q addr %q", from.Name, from.Address, "3x-ui", "panel@example.com") + } + + if _, err := msg.Header.Date(); err != nil { + t.Errorf("Date header missing or unparseable: %v", err) + } + + id := msg.Header.Get("Message-ID") + if !strings.HasPrefix(id, "<") || !strings.HasSuffix(id, "@example.com>") { + t.Errorf("Message-ID = %q, want ", id) + } + + subject, err := (&mime.WordDecoder{}).DecodeHeader(msg.Header.Get("Subject")) + if err != nil { + t.Fatalf("Subject does not decode: %v", err) + } + if subject != "Тест" { + t.Errorf("Subject = %q, want %q", subject, "Тест") + } + + body, _ := io.ReadAll(msg.Body) + if string(body) != "hi" { + t.Errorf("body = %q, want %q", body, "hi") + } +} + +func TestBuildMessageFromWithoutName(t *testing.T) { + raw := buildMessage("panel@example.com", "", []string{"a@example.com"}, "s", "b") + msg, err := mail.ReadMessage(strings.NewReader(string(raw))) + if err != nil { + t.Fatalf("parse: %v", err) + } + from, err := mail.ParseAddress(msg.Header.Get("From")) + if err != nil { + t.Fatalf("From header does not parse: %v", err) + } + if from.Name != "" || from.Address != "panel@example.com" { + t.Errorf("From = %q <%q>, want bare addr", from.Name, from.Address) + } +} + +func TestBuildMessageStripsHeaderInjection(t *testing.T) { + raw := buildMessage( + "panel@example.com\r\nBcc: evil@example.com", + "Name\r\nX-Evil: 1", + []string{"a@example.com"}, "s", "b", + ) + msg, err := mail.ReadMessage(strings.NewReader(string(raw))) + if err != nil { + t.Fatalf("parse: %v", err) + } + if got := msg.Header.Get("Bcc"); got != "" { + t.Errorf("injected Bcc header leaked: %q", got) + } + if got := msg.Header.Get("X-Evil"); got != "" { + t.Errorf("injected X-Evil header leaked: %q", got) + } +} diff --git a/internal/web/service/setting.go b/internal/web/service/setting.go index 00ab86545..b675e5837 100644 --- a/internal/web/service/setting.go +++ b/internal/web/service/setting.go @@ -148,6 +148,8 @@ var defaultValueMap = map[string]string{ "smtpPort": "587", "smtpUsername": "", "smtpPassword": "", + "smtpFrom": "", + "smtpFromName": "", "smtpTo": "", "smtpEncryptionType": "starttls", // no, starttls, tls } @@ -1045,6 +1047,22 @@ func (s *SettingService) SetSmtpUsername(value string) error { return s.setString("smtpUsername", value) } +func (s *SettingService) GetSmtpFrom() (string, error) { + return s.getString("smtpFrom") +} + +func (s *SettingService) SetSmtpFrom(value string) error { + return s.setString("smtpFrom", value) +} + +func (s *SettingService) GetSmtpFromName() (string, error) { + return s.getString("smtpFromName") +} + +func (s *SettingService) SetSmtpFromName(value string) error { + return s.setString("smtpFromName", value) +} + func (s *SettingService) GetSmtpPassword() (string, error) { return s.getString("smtpPassword") } diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index bcf7bf081..1241451ef 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -1358,6 +1358,10 @@ "smtpPortDesc": "منفذ خادم SMTP (الافتراضي: 587)", "smtpUsername": "اسم مستخدم SMTP", "smtpUsernameDesc": "اسم المستخدم للمصادقة على SMTP", + "smtpFrom": "عنوان المُرسِل (From)", + "smtpFromDesc": "العنوان المستخدم في ترويسة From للبريد. اتركه فارغًا لاستخدام اسم المستخدم.", + "smtpFromName": "اسم المُرسِل (From)", + "smtpFromNameDesc": "اسم عرض اختياري يظهر قبل العنوان في ترويسة From.", "smtpPassword": "كلمة مرور SMTP", "smtpPasswordDesc": "كلمة المرور للمصادقة على SMTP", "smtpTo": "المستلمون", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 17bafb7e0..cb909ad6d 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -1477,6 +1477,10 @@ "smtpPortDesc": "SMTP server port (default: 587)", "smtpUsername": "SMTP Username", "smtpUsernameDesc": "SMTP authentication username", + "smtpFrom": "SMTP From Address", + "smtpFromDesc": "Sender address used in the email From header. Leave empty to use the username.", + "smtpFromName": "SMTP Sender Name", + "smtpFromNameDesc": "Optional display name shown before the sender address in the From header.", "smtpPassword": "SMTP Password", "smtpPasswordDesc": "SMTP authentication password", "smtpTo": "Recipients", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index b60e486e8..0264623e3 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -1358,6 +1358,10 @@ "smtpPortDesc": "Puerto del servidor SMTP (predeterminado: 587)", "smtpUsername": "Usuario SMTP", "smtpUsernameDesc": "Usuario de autenticación SMTP", + "smtpFrom": "Dirección de remitente (From)", + "smtpFromDesc": "Dirección usada en el encabezado From del correo. Déjelo vacío para usar el nombre de usuario.", + "smtpFromName": "Nombre del remitente (From)", + "smtpFromNameDesc": "Nombre para mostrar opcional antes de la dirección en el encabezado From.", "smtpPassword": "Contraseña SMTP", "smtpPasswordDesc": "Contraseña de autenticación SMTP", "smtpTo": "Destinatarios", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index 946ec59f0..8c1140d71 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -1360,6 +1360,10 @@ "smtpPortDesc": "پورت سرور SMTP (پیش‌فرض: ۵۸۷)", "smtpUsername": "نام‌کاربری SMTP", "smtpUsernameDesc": "نام‌کاربری احراز هویت SMTP", + "smtpFrom": "آدرس فرستنده (From)", + "smtpFromDesc": "آدرس استفاده‌شده در سرآیند From ایمیل. برای استفاده از نام کاربری، خالی بگذارید.", + "smtpFromName": "نام فرستنده (From)", + "smtpFromNameDesc": "نام نمایشی اختیاری پیش از آدرس در سرآیند From.", "smtpPassword": "رمز عبور SMTP", "smtpPasswordDesc": "رمز عبور احراز هویت SMTP", "smtpTo": "گیرندگان", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index 41b2df0a1..8be8e76b0 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -1358,6 +1358,10 @@ "smtpPortDesc": "Port server SMTP (bawaan: 587)", "smtpUsername": "Nama Pengguna SMTP", "smtpUsernameDesc": "Nama pengguna autentikasi SMTP", + "smtpFrom": "Alamat Pengirim (From)", + "smtpFromDesc": "Alamat yang dipakai pada header From email. Kosongkan untuk memakai nama pengguna.", + "smtpFromName": "Nama Pengirim (From)", + "smtpFromNameDesc": "Nama tampilan opsional sebelum alamat pada header From.", "smtpPassword": "Kata Sandi SMTP", "smtpPasswordDesc": "Kata sandi autentikasi SMTP", "smtpTo": "Penerima", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index c3d356598..1ef885153 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -1358,6 +1358,10 @@ "smtpPortDesc": "SMTPサーバーのポート(既定値: 587)", "smtpUsername": "SMTPユーザー名", "smtpUsernameDesc": "SMTP認証用のユーザー名", + "smtpFrom": "送信元アドレス (From)", + "smtpFromDesc": "メールの From ヘッダーに使用するアドレス。空欄の場合はユーザー名を使用します。", + "smtpFromName": "送信者名 (From)", + "smtpFromNameDesc": "From ヘッダーでアドレスの前に表示される任意の表示名。", "smtpPassword": "SMTPパスワード", "smtpPasswordDesc": "SMTP認証用のパスワード", "smtpTo": "受信者", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index c8dcc92c4..c4971a1dd 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -1358,6 +1358,10 @@ "smtpPortDesc": "Porta do servidor SMTP (padrão: 587)", "smtpUsername": "Usuário SMTP", "smtpUsernameDesc": "Nome de usuário para autenticação SMTP", + "smtpFrom": "Endereço do remetente (From)", + "smtpFromDesc": "Endereço usado no cabeçalho From do e-mail. Deixe vazio para usar o nome de usuário.", + "smtpFromName": "Nome do remetente (From)", + "smtpFromNameDesc": "Nome de exibição opcional antes do endereço no cabeçalho From.", "smtpPassword": "Senha SMTP", "smtpPasswordDesc": "Senha para autenticação SMTP", "smtpTo": "Destinatários", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index 84ad78eaf..e3a690bbd 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -1358,6 +1358,10 @@ "smtpPortDesc": "Порт SMTP сервера (по умолчанию: 587)", "smtpUsername": "SMTP логин", "smtpUsernameDesc": "Логин для аутентификации SMTP", + "smtpFrom": "Адрес отправителя (From)", + "smtpFromDesc": "Адрес в заголовке From письма. Оставьте пустым, чтобы использовать имя пользователя.", + "smtpFromName": "Имя отправителя (From)", + "smtpFromNameDesc": "Необязательное отображаемое имя перед адресом в заголовке From.", "smtpPassword": "SMTP пароль", "smtpPasswordDesc": "Пароль для аутентификации SMTP", "smtpTo": "Получатели", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index 5086fc318..d88e307e4 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -1358,6 +1358,10 @@ "smtpPortDesc": "SMTP sunucu bağlantı noktası (varsayılan: 587)", "smtpUsername": "SMTP Kullanıcı Adı", "smtpUsernameDesc": "SMTP kimlik doğrulama kullanıcı adı", + "smtpFrom": "Gönderen Adresi (From)", + "smtpFromDesc": "E-postanın From başlığında kullanılan adres. Boş bırakılırsa kullanıcı adı kullanılır.", + "smtpFromName": "Gönderen Adı (From)", + "smtpFromNameDesc": "From başlığında adresten önce görünen isteğe bağlı ad.", "smtpPassword": "SMTP Parolası", "smtpPasswordDesc": "SMTP kimlik doğrulama parolası", "smtpTo": "Alıcılar", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index ae0ef80b5..97a470cba 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -1358,6 +1358,10 @@ "smtpPortDesc": "Порт сервера SMTP (типово: 587)", "smtpUsername": "Ім'я користувача SMTP", "smtpUsernameDesc": "Ім'я користувача для автентифікації SMTP", + "smtpFrom": "Адреса відправника (From)", + "smtpFromDesc": "Адреса в заголовку From листа. Залиште порожнім, щоб використати ім'я користувача.", + "smtpFromName": "Ім'я відправника (From)", + "smtpFromNameDesc": "Необов'язкове відображуване ім'я перед адресою в заголовку From.", "smtpPassword": "Пароль SMTP", "smtpPasswordDesc": "Пароль для автентифікації SMTP", "smtpTo": "Отримувачі", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 1fec4597d..1bf74be51 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -1358,6 +1358,10 @@ "smtpPortDesc": "Cổng máy chủ SMTP (mặc định: 587)", "smtpUsername": "Tên đăng nhập SMTP", "smtpUsernameDesc": "Tên đăng nhập xác thực SMTP", + "smtpFrom": "Địa chỉ người gửi (From)", + "smtpFromDesc": "Địa chỉ dùng trong tiêu đề From của email. Để trống để dùng tên người dùng.", + "smtpFromName": "Tên người gửi (From)", + "smtpFromNameDesc": "Tên hiển thị tùy chọn trước địa chỉ trong tiêu đề From.", "smtpPassword": "Mật khẩu SMTP", "smtpPasswordDesc": "Mật khẩu xác thực SMTP", "smtpTo": "Người nhận", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index b45b59252..b0eab2cc8 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -1358,6 +1358,10 @@ "smtpPortDesc": "SMTP 服务器端口(默认:587)", "smtpUsername": "SMTP 用户名", "smtpUsernameDesc": "SMTP 认证用户名", + "smtpFrom": "发件人地址 (From)", + "smtpFromDesc": "邮件 From 头使用的地址。留空则使用用户名。", + "smtpFromName": "发件人名称 (From)", + "smtpFromNameDesc": "From 头中显示在地址前的可选显示名称。", "smtpPassword": "SMTP 密码", "smtpPasswordDesc": "SMTP 认证密码", "smtpTo": "收件人", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index c28f76b98..50e15f9c0 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -1358,6 +1358,10 @@ "smtpPortDesc": "SMTP 伺服器連接埠(預設:587)", "smtpUsername": "SMTP 使用者名稱", "smtpUsernameDesc": "SMTP 驗證使用者名稱", + "smtpFrom": "寄件者位址 (From)", + "smtpFromDesc": "郵件 From 標頭使用的位址。留空則使用使用者名稱。", + "smtpFromName": "寄件者名稱 (From)", + "smtpFromNameDesc": "From 標頭中顯示在位址前的可選顯示名稱。", "smtpPassword": "SMTP 密碼", "smtpPasswordDesc": "SMTP 驗證密碼", "smtpTo": "收件人",