From 1cfd7b49b0915aafb21028d5d13c779413b6dc97 Mon Sep 17 00:00:00 2001 From: Yuri Khachaturyan Date: Tue, 14 Jul 2026 13:55:46 +0300 Subject: [PATCH] fix(email): build an RFC 5322 message with a proper From address and name (#5941) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notification/test email carried only From/To/Subject/MIME headers, and the From header was the raw SMTP username. Two problems: - When the SMTP login is not a bare email address (common with relays and submission services), the From header has no valid address and strict receivers reject the message — e.g. Gmail returns "550-5.7.1 ... Messages missing a valid address in From: header". - There was no Date (mandatory per RFC 5322 section 3.6) and no Message-ID, which also raises spam score. Add smtpFrom (sender address) and smtpFromName (display name) settings and assemble the message with net/mail: a name-addr From ("Name" ), a Date, a Message-ID, and an RFC 2047 encoded Subject, in a deterministic header order. From falls back to the username when smtpFrom is empty, so existing setups keep working. Wire the settings through the model, the SMTP send and test paths, the Email settings UI, and all 13 locale files; regenerate the Zod/OpenAPI artifacts. Validate smtpFrom in AllSetting.CheckValid (reject anything net/mail cannot parse), which surfaces a bad address at configuration time and prevents CRLF header injection; strip CR/LF in buildMessage as defense in depth. Add buildMessage and CheckValid tests. --- frontend/public/openapi.json | 16 +++++ frontend/src/generated/examples.ts | 4 ++ frontend/src/generated/schemas.ts | 16 +++++ frontend/src/generated/types.ts | 4 ++ frontend/src/generated/zod.ts | 4 ++ frontend/src/models/setting.ts | 2 + frontend/src/pages/settings/EmailTab.tsx | 10 +++ internal/web/entity/check_valid_test.go | 29 +++++++++ internal/web/entity/entity.go | 9 +++ internal/web/service/email/email.go | 61 ++++++++++++++---- internal/web/service/email/email_test.go | 81 ++++++++++++++++++++++++ internal/web/service/setting.go | 18 ++++++ internal/web/translation/ar-EG.json | 4 ++ internal/web/translation/en-US.json | 4 ++ internal/web/translation/es-ES.json | 4 ++ internal/web/translation/fa-IR.json | 4 ++ internal/web/translation/id-ID.json | 4 ++ internal/web/translation/ja-JP.json | 4 ++ internal/web/translation/pt-BR.json | 4 ++ internal/web/translation/ru-RU.json | 4 ++ internal/web/translation/tr-TR.json | 4 ++ internal/web/translation/uk-UA.json | 4 ++ internal/web/translation/vi-VN.json | 4 ++ internal/web/translation/zh-CN.json | 4 ++ internal/web/translation/zh-TW.json | 4 ++ 25 files changed, 292 insertions(+), 14 deletions(-) create mode 100644 internal/web/entity/check_valid_test.go create mode 100644 internal/web/service/email/email_test.go 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": "收件人",