Forráskód Böngészése

fix(email): build an RFC 5322 message with a proper From address and name (#5941)

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" <addr>), 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.
Yuri Khachaturyan 10 órája
szülő
commit
1cfd7b49b0

+ 16 - 0
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",

+ 4 - 0
frontend/src/generated/examples.ts

@@ -35,6 +35,8 @@ export const EXAMPLES: Record<string, unknown> = {
     "smtpEnable": false,
     "smtpEnabledEvents": "",
     "smtpEncryptionType": "",
+    "smtpFrom": "",
+    "smtpFromName": "",
     "smtpHost": "",
     "smtpMemory": 0,
     "smtpPassword": "",
@@ -138,6 +140,8 @@ export const EXAMPLES: Record<string, unknown> = {
     "smtpEnable": false,
     "smtpEnabledEvents": "",
     "smtpEncryptionType": "",
+    "smtpFrom": "",
+    "smtpFromName": "",
     "smtpHost": "",
     "smtpMemory": 0,
     "smtpPassword": "",

+ 16 - 0
frontend/src/generated/schemas.ts

@@ -116,6 +116,12 @@ export const SCHEMAS: Record<string, unknown> = {
       "smtpEncryptionType": {
         "type": "string"
       },
+      "smtpFrom": {
+        "type": "string"
+      },
+      "smtpFromName": {
+        "type": "string"
+      },
       "smtpHost": {
         "type": "string"
       },
@@ -349,6 +355,8 @@ export const SCHEMAS: Record<string, unknown> = {
       "smtpEnable",
       "smtpEnabledEvents",
       "smtpEncryptionType",
+      "smtpFrom",
+      "smtpFromName",
       "smtpHost",
       "smtpMemory",
       "smtpPassword",
@@ -549,6 +557,12 @@ export const SCHEMAS: Record<string, unknown> = {
       "smtpEncryptionType": {
         "type": "string"
       },
+      "smtpFrom": {
+        "type": "string"
+      },
+      "smtpFromName": {
+        "type": "string"
+      },
       "smtpHost": {
         "type": "string"
       },
@@ -789,6 +803,8 @@ export const SCHEMAS: Record<string, unknown> = {
       "smtpEnable",
       "smtpEnabledEvents",
       "smtpEncryptionType",
+      "smtpFrom",
+      "smtpFromName",
       "smtpHost",
       "smtpMemory",
       "smtpPassword",

+ 4 - 0
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;

+ 4 - 0
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(),

+ 2 - 0
frontend/src/models/setting.ts

@@ -91,6 +91,8 @@ export class AllSetting {
   smtpPort = 587;
   smtpUsername = '';
   smtpPassword = '';
+  smtpFrom = '';
+  smtpFromName = '';
   smtpTo = '';
   smtpEncryptionType = 'starttls';
   smtpEnabledEvents = '';

+ 10 - 0
frontend/src/pages/settings/EmailTab.tsx

@@ -82,6 +82,16 @@ export default function EmailTab({ allSetting, updateSetting }: EmailTabProps) {
                 onClearArmedChange={(armed) => updateSetting({ clearSmtpPassword: armed })} />
             </SettingListItem>
 
+            <SettingListItem paddings="small" title={t('pages.settings.smtpFrom')} description={t('pages.settings.smtpFromDesc')}>
+              <Input value={allSetting.smtpFrom} placeholder="[email protected]"
+                onChange={(e) => updateSetting({ smtpFrom: e.target.value })} />
+            </SettingListItem>
+
+            <SettingListItem paddings="small" title={t('pages.settings.smtpFromName')} description={t('pages.settings.smtpFromNameDesc')}>
+              <Input value={allSetting.smtpFromName} placeholder="3x-ui"
+                onChange={(e) => updateSetting({ smtpFromName: e.target.value })} />
+            </SettingListItem>
+
             <SettingListItem paddings="small" title={t('pages.settings.smtpTo')} description={t('pages.settings.smtpToDesc')}>
               <Input value={allSetting.smtpTo} placeholder="[email protected], [email protected]"
                 onChange={(e) => updateSetting({ smtpTo: e.target.value })} />

+ 29 - 0
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{"", "[email protected]"} {
+		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",
+		"[email protected]\r\nBcc: [email protected]",
+		"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)
+		}
+	}
+}

+ 9 - 0
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
 }
 

+ 48 - 15
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",
 		`<html><body style="font-family:monospace;font-size:14px">
 <h2>Test email from 3x-ui</h2>
 <p>If you received this, SMTP is configured correctly.</p>
@@ -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" <addr>) 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())

+ 81 - 0
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("[email protected]", "3x-ui", []string{"[email protected]", "[email protected]"}, "Тест", "<b>hi</b>")
+
+	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 != "[email protected]" {
+		t.Errorf("From = %q <%q>, want name %q addr %q", from.Name, from.Address, "3x-ui", "[email protected]")
+	}
+
+	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 <[email protected]>", 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) != "<b>hi</b>" {
+		t.Errorf("body = %q, want %q", body, "<b>hi</b>")
+	}
+}
+
+func TestBuildMessageFromWithoutName(t *testing.T) {
+	raw := buildMessage("[email protected]", "", []string{"[email protected]"}, "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 != "[email protected]" {
+		t.Errorf("From = %q <%q>, want bare addr", from.Name, from.Address)
+	}
+}
+
+func TestBuildMessageStripsHeaderInjection(t *testing.T) {
+	raw := buildMessage(
+		"[email protected]\r\nBcc: [email protected]",
+		"Name\r\nX-Evil: 1",
+		[]string{"[email protected]"}, "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)
+	}
+}

+ 18 - 0
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")
 }

+ 4 - 0
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": "المستلمون",

+ 4 - 0
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",

+ 4 - 0
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",

+ 4 - 0
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": "گیرندگان",

+ 4 - 0
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",

+ 4 - 0
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": "受信者",

+ 4 - 0
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",

+ 4 - 0
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": "Получатели",

+ 4 - 0
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",

+ 4 - 0
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": "Отримувачі",

+ 4 - 0
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",

+ 4 - 0
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": "收件人",

+ 4 - 0
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": "收件人",