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.
This commit is contained in:
Yuri Khachaturyan
2026-07-14 13:55:46 +03:00
committed by GitHub
parent ae0da4c51f
commit 1cfd7b49b0
25 changed files with 292 additions and 14 deletions

View File

@@ -142,6 +142,12 @@
"smtpEncryptionType": { "smtpEncryptionType": {
"type": "string" "type": "string"
}, },
"smtpFrom": {
"type": "string"
},
"smtpFromName": {
"type": "string"
},
"smtpHost": { "smtpHost": {
"type": "string" "type": "string"
}, },
@@ -375,6 +381,8 @@
"smtpEnable", "smtpEnable",
"smtpEnabledEvents", "smtpEnabledEvents",
"smtpEncryptionType", "smtpEncryptionType",
"smtpFrom",
"smtpFromName",
"smtpHost", "smtpHost",
"smtpMemory", "smtpMemory",
"smtpPassword", "smtpPassword",
@@ -575,6 +583,12 @@
"smtpEncryptionType": { "smtpEncryptionType": {
"type": "string" "type": "string"
}, },
"smtpFrom": {
"type": "string"
},
"smtpFromName": {
"type": "string"
},
"smtpHost": { "smtpHost": {
"type": "string" "type": "string"
}, },
@@ -815,6 +829,8 @@
"smtpEnable", "smtpEnable",
"smtpEnabledEvents", "smtpEnabledEvents",
"smtpEncryptionType", "smtpEncryptionType",
"smtpFrom",
"smtpFromName",
"smtpHost", "smtpHost",
"smtpMemory", "smtpMemory",
"smtpPassword", "smtpPassword",

View File

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

View File

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

View File

@@ -41,6 +41,8 @@ export interface AllSetting {
smtpEnable: boolean; smtpEnable: boolean;
smtpEnabledEvents: string; smtpEnabledEvents: string;
smtpEncryptionType: string; smtpEncryptionType: string;
smtpFrom: string;
smtpFromName: string;
smtpHost: string; smtpHost: string;
smtpMemory: number; smtpMemory: number;
smtpPassword: string; smtpPassword: string;
@@ -145,6 +147,8 @@ export interface AllSettingView {
smtpEnable: boolean; smtpEnable: boolean;
smtpEnabledEvents: string; smtpEnabledEvents: string;
smtpEncryptionType: string; smtpEncryptionType: string;
smtpFrom: string;
smtpFromName: string;
smtpHost: string; smtpHost: string;
smtpMemory: number; smtpMemory: number;
smtpPassword: string; smtpPassword: string;

View File

@@ -53,6 +53,8 @@ export const AllSettingSchema = z.object({
smtpEnable: z.boolean(), smtpEnable: z.boolean(),
smtpEnabledEvents: z.string(), smtpEnabledEvents: z.string(),
smtpEncryptionType: z.string(), smtpEncryptionType: z.string(),
smtpFrom: z.string(),
smtpFromName: z.string(),
smtpHost: z.string(), smtpHost: z.string(),
smtpMemory: z.number().int().min(0).max(100), smtpMemory: z.number().int().min(0).max(100),
smtpPassword: z.string(), smtpPassword: z.string(),
@@ -158,6 +160,8 @@ export const AllSettingViewSchema = z.object({
smtpEnable: z.boolean(), smtpEnable: z.boolean(),
smtpEnabledEvents: z.string(), smtpEnabledEvents: z.string(),
smtpEncryptionType: z.string(), smtpEncryptionType: z.string(),
smtpFrom: z.string(),
smtpFromName: z.string(),
smtpHost: z.string(), smtpHost: z.string(),
smtpMemory: z.number().int().min(0).max(100), smtpMemory: z.number().int().min(0).max(100),
smtpPassword: z.string(), smtpPassword: z.string(),

View File

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

View File

@@ -82,6 +82,16 @@ export default function EmailTab({ allSetting, updateSetting }: EmailTabProps) {
onClearArmedChange={(armed) => updateSetting({ clearSmtpPassword: armed })} /> onClearArmedChange={(armed) => updateSetting({ clearSmtpPassword: armed })} />
</SettingListItem> </SettingListItem>
<SettingListItem paddings="small" title={t('pages.settings.smtpFrom')} description={t('pages.settings.smtpFromDesc')}>
<Input value={allSetting.smtpFrom} placeholder="user@gmail.com"
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')}> <SettingListItem paddings="small" title={t('pages.settings.smtpTo')} description={t('pages.settings.smtpToDesc')}>
<Input value={allSetting.smtpTo} placeholder="admin@example.com, ops@example.com" <Input value={allSetting.smtpTo} placeholder="admin@example.com, ops@example.com"
onChange={(e) => updateSetting({ smtpTo: e.target.value })} /> onChange={(e) => updateSetting({ smtpTo: e.target.value })} />

View File

@@ -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)
}
}
}

View File

@@ -4,6 +4,7 @@ import (
"crypto/tls" "crypto/tls"
"math" "math"
"net" "net"
"net/mail"
"strings" "strings"
"time" "time"
@@ -50,6 +51,8 @@ type AllSetting struct {
SmtpPort int `json:"smtpPort" form:"smtpPort" validate:"gte=1,lte=65535"` SmtpPort int `json:"smtpPort" form:"smtpPort" validate:"gte=1,lte=65535"`
SmtpUsername string `json:"smtpUsername" form:"smtpUsername"` SmtpUsername string `json:"smtpUsername" form:"smtpUsername"`
SmtpPassword string `json:"smtpPassword" form:"smtpPassword"` SmtpPassword string `json:"smtpPassword" form:"smtpPassword"`
SmtpFrom string `json:"smtpFrom" form:"smtpFrom"`
SmtpFromName string `json:"smtpFromName" form:"smtpFromName"`
SmtpTo string `json:"smtpTo" form:"smtpTo"` SmtpTo string `json:"smtpTo" form:"smtpTo"`
SmtpEncryptionType string `json:"smtpEncryptionType" form:"smtpEncryptionType"` SmtpEncryptionType string `json:"smtpEncryptionType" form:"smtpEncryptionType"`
SmtpEnabledEvents string `json:"smtpEnabledEvents" form:"smtpEnabledEvents"` SmtpEnabledEvents string `json:"smtpEnabledEvents" form:"smtpEnabledEvents"`
@@ -241,6 +244,12 @@ func (s *AllSetting) CheckValid() error {
return common.NewError("time location not exist:", s.TimeLocation) 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 return nil
} }

View File

@@ -2,9 +2,13 @@ package email
import ( import (
"context" "context"
"crypto/rand"
"crypto/tls" "crypto/tls"
"encoding/hex"
"fmt" "fmt"
"mime"
"net" "net"
"net/mail"
"net/smtp" "net/smtp"
"strings" "strings"
"time" "time"
@@ -41,10 +45,15 @@ func (s *EmailService) Send(subject, body string) error {
} }
username, _ := s.settingService.GetSmtpUsername() username, _ := s.settingService.GetSmtpUsername()
password, _ := s.settingService.GetSmtpPassword() password, _ := s.settingService.GetSmtpPassword()
fromAddr, _ := s.settingService.GetSmtpFrom()
fromName, _ := s.settingService.GetSmtpFromName()
toStr, _ := s.settingService.GetSmtpTo() toStr, _ := s.settingService.GetSmtpTo()
encryptionType, _ := s.settingService.GetSmtpEncryptionType() encryptionType, _ := s.settingService.GetSmtpEncryptionType()
from := username from := fromAddr
if from == "" {
from = username
}
if from == "" { if from == "" {
return fmt.Errorf("smtp from not configured") 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)) 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 // 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. // 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() username, _ := s.settingService.GetSmtpUsername()
password, _ := s.settingService.GetSmtpPassword() password, _ := s.settingService.GetSmtpPassword()
fromAddr, _ := s.settingService.GetSmtpFrom()
fromName, _ := s.settingService.GetSmtpFromName()
toStr, _ := s.settingService.GetSmtpTo() toStr, _ := s.settingService.GetSmtpTo()
encryptionType, _ := s.settingService.GetSmtpEncryptionType() encryptionType, _ := s.settingService.GetSmtpEncryptionType()
from := username from := fromAddr
if from == "" {
from = username
}
recipients := parseRecipients(toStr) recipients := parseRecipients(toStr)
if len(recipients) == 0 { 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"> `<html><body style="font-family:monospace;font-size:14px">
<h2>Test email from 3x-ui</h2> <h2>Test email from 3x-ui</h2>
<p>If you received this, SMTP is configured correctly.</p> <p>If you received this, SMTP is configured correctly.</p>
@@ -280,18 +294,37 @@ func parseRecipients(toStr string) []string {
return out return out
} }
func buildMessage(from string, to []string, subject, body string) []byte { // buildMessage assembles an RFC 5322 message. It emits the two mandatory
headers := map[string]string{ // header fields (Date, From) plus Message-ID, so strict receivers such as Gmail
"From": from, // accept it and spam filters do not penalize a missing date or message id. The
"To": strings.Join(to, ","), // From header is a proper name-addr ("Name" <addr>) via net/mail, and a
"Subject": subject, // non-ASCII subject is RFC 2047 encoded.
"MIME-Version": "1.0", // headerSanitizer drops CR/LF so a crafted address or name cannot inject extra
"Content-Type": "text/html; charset=utf-8", // 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 var msg strings.Builder
for k, v := range headers { fmt.Fprintf(&msg, "Date: %s\r\n", time.Now().Format(time.RFC1123Z))
fmt.Fprintf(&msg, "%s: %s\r\n", k, v) 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("\r\n")
msg.WriteString(body) msg.WriteString(body)
return []byte(msg.String()) return []byte(msg.String())

View File

@@ -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"}, "Тест", "<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 != "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 <token@example.com>", 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("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)
}
}

View File

@@ -148,6 +148,8 @@ var defaultValueMap = map[string]string{
"smtpPort": "587", "smtpPort": "587",
"smtpUsername": "", "smtpUsername": "",
"smtpPassword": "", "smtpPassword": "",
"smtpFrom": "",
"smtpFromName": "",
"smtpTo": "", "smtpTo": "",
"smtpEncryptionType": "starttls", // no, starttls, tls "smtpEncryptionType": "starttls", // no, starttls, tls
} }
@@ -1045,6 +1047,22 @@ func (s *SettingService) SetSmtpUsername(value string) error {
return s.setString("smtpUsername", value) 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) { func (s *SettingService) GetSmtpPassword() (string, error) {
return s.getString("smtpPassword") return s.getString("smtpPassword")
} }

View File

@@ -1358,6 +1358,10 @@
"smtpPortDesc": "منفذ خادم SMTP (الافتراضي: 587)", "smtpPortDesc": "منفذ خادم SMTP (الافتراضي: 587)",
"smtpUsername": "اسم مستخدم SMTP", "smtpUsername": "اسم مستخدم SMTP",
"smtpUsernameDesc": "اسم المستخدم للمصادقة على SMTP", "smtpUsernameDesc": "اسم المستخدم للمصادقة على SMTP",
"smtpFrom": "عنوان المُرسِل (From)",
"smtpFromDesc": "العنوان المستخدم في ترويسة From للبريد. اتركه فارغًا لاستخدام اسم المستخدم.",
"smtpFromName": "اسم المُرسِل (From)",
"smtpFromNameDesc": "اسم عرض اختياري يظهر قبل العنوان في ترويسة From.",
"smtpPassword": "كلمة مرور SMTP", "smtpPassword": "كلمة مرور SMTP",
"smtpPasswordDesc": "كلمة المرور للمصادقة على SMTP", "smtpPasswordDesc": "كلمة المرور للمصادقة على SMTP",
"smtpTo": "المستلمون", "smtpTo": "المستلمون",

View File

@@ -1477,6 +1477,10 @@
"smtpPortDesc": "SMTP server port (default: 587)", "smtpPortDesc": "SMTP server port (default: 587)",
"smtpUsername": "SMTP Username", "smtpUsername": "SMTP Username",
"smtpUsernameDesc": "SMTP authentication 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", "smtpPassword": "SMTP Password",
"smtpPasswordDesc": "SMTP authentication password", "smtpPasswordDesc": "SMTP authentication password",
"smtpTo": "Recipients", "smtpTo": "Recipients",

View File

@@ -1358,6 +1358,10 @@
"smtpPortDesc": "Puerto del servidor SMTP (predeterminado: 587)", "smtpPortDesc": "Puerto del servidor SMTP (predeterminado: 587)",
"smtpUsername": "Usuario SMTP", "smtpUsername": "Usuario SMTP",
"smtpUsernameDesc": "Usuario de autenticación 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", "smtpPassword": "Contraseña SMTP",
"smtpPasswordDesc": "Contraseña de autenticación SMTP", "smtpPasswordDesc": "Contraseña de autenticación SMTP",
"smtpTo": "Destinatarios", "smtpTo": "Destinatarios",

View File

@@ -1360,6 +1360,10 @@
"smtpPortDesc": "پورت سرور SMTP (پیش‌فرض: ۵۸۷)", "smtpPortDesc": "پورت سرور SMTP (پیش‌فرض: ۵۸۷)",
"smtpUsername": "نام‌کاربری SMTP", "smtpUsername": "نام‌کاربری SMTP",
"smtpUsernameDesc": "نام‌کاربری احراز هویت SMTP", "smtpUsernameDesc": "نام‌کاربری احراز هویت SMTP",
"smtpFrom": "آدرس فرستنده (From)",
"smtpFromDesc": "آدرس استفاده‌شده در سرآیند From ایمیل. برای استفاده از نام کاربری، خالی بگذارید.",
"smtpFromName": "نام فرستنده (From)",
"smtpFromNameDesc": "نام نمایشی اختیاری پیش از آدرس در سرآیند From.",
"smtpPassword": "رمز عبور SMTP", "smtpPassword": "رمز عبور SMTP",
"smtpPasswordDesc": "رمز عبور احراز هویت SMTP", "smtpPasswordDesc": "رمز عبور احراز هویت SMTP",
"smtpTo": "گیرندگان", "smtpTo": "گیرندگان",

View File

@@ -1358,6 +1358,10 @@
"smtpPortDesc": "Port server SMTP (bawaan: 587)", "smtpPortDesc": "Port server SMTP (bawaan: 587)",
"smtpUsername": "Nama Pengguna SMTP", "smtpUsername": "Nama Pengguna SMTP",
"smtpUsernameDesc": "Nama pengguna autentikasi 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", "smtpPassword": "Kata Sandi SMTP",
"smtpPasswordDesc": "Kata sandi autentikasi SMTP", "smtpPasswordDesc": "Kata sandi autentikasi SMTP",
"smtpTo": "Penerima", "smtpTo": "Penerima",

View File

@@ -1358,6 +1358,10 @@
"smtpPortDesc": "SMTPサーバーのポート既定値: 587", "smtpPortDesc": "SMTPサーバーのポート既定値: 587",
"smtpUsername": "SMTPユーザー名", "smtpUsername": "SMTPユーザー名",
"smtpUsernameDesc": "SMTP認証用のユーザー名", "smtpUsernameDesc": "SMTP認証用のユーザー名",
"smtpFrom": "送信元アドレス (From)",
"smtpFromDesc": "メールの From ヘッダーに使用するアドレス。空欄の場合はユーザー名を使用します。",
"smtpFromName": "送信者名 (From)",
"smtpFromNameDesc": "From ヘッダーでアドレスの前に表示される任意の表示名。",
"smtpPassword": "SMTPパスワード", "smtpPassword": "SMTPパスワード",
"smtpPasswordDesc": "SMTP認証用のパスワード", "smtpPasswordDesc": "SMTP認証用のパスワード",
"smtpTo": "受信者", "smtpTo": "受信者",

View File

@@ -1358,6 +1358,10 @@
"smtpPortDesc": "Porta do servidor SMTP (padrão: 587)", "smtpPortDesc": "Porta do servidor SMTP (padrão: 587)",
"smtpUsername": "Usuário SMTP", "smtpUsername": "Usuário SMTP",
"smtpUsernameDesc": "Nome de usuário para autenticação 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", "smtpPassword": "Senha SMTP",
"smtpPasswordDesc": "Senha para autenticação SMTP", "smtpPasswordDesc": "Senha para autenticação SMTP",
"smtpTo": "Destinatários", "smtpTo": "Destinatários",

View File

@@ -1358,6 +1358,10 @@
"smtpPortDesc": "Порт SMTP сервера (по умолчанию: 587)", "smtpPortDesc": "Порт SMTP сервера (по умолчанию: 587)",
"smtpUsername": "SMTP логин", "smtpUsername": "SMTP логин",
"smtpUsernameDesc": "Логин для аутентификации SMTP", "smtpUsernameDesc": "Логин для аутентификации SMTP",
"smtpFrom": "Адрес отправителя (From)",
"smtpFromDesc": "Адрес в заголовке From письма. Оставьте пустым, чтобы использовать имя пользователя.",
"smtpFromName": "Имя отправителя (From)",
"smtpFromNameDesc": "Необязательное отображаемое имя перед адресом в заголовке From.",
"smtpPassword": "SMTP пароль", "smtpPassword": "SMTP пароль",
"smtpPasswordDesc": "Пароль для аутентификации SMTP", "smtpPasswordDesc": "Пароль для аутентификации SMTP",
"smtpTo": "Получатели", "smtpTo": "Получатели",

View File

@@ -1358,6 +1358,10 @@
"smtpPortDesc": "SMTP sunucu bağlantı noktası (varsayılan: 587)", "smtpPortDesc": "SMTP sunucu bağlantı noktası (varsayılan: 587)",
"smtpUsername": "SMTP Kullanıcı Adı", "smtpUsername": "SMTP Kullanıcı Adı",
"smtpUsernameDesc": "SMTP kimlik doğrulama 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ı", "smtpPassword": "SMTP Parolası",
"smtpPasswordDesc": "SMTP kimlik doğrulama parolası", "smtpPasswordDesc": "SMTP kimlik doğrulama parolası",
"smtpTo": "Alıcılar", "smtpTo": "Alıcılar",

View File

@@ -1358,6 +1358,10 @@
"smtpPortDesc": "Порт сервера SMTP (типово: 587)", "smtpPortDesc": "Порт сервера SMTP (типово: 587)",
"smtpUsername": "Ім'я користувача SMTP", "smtpUsername": "Ім'я користувача SMTP",
"smtpUsernameDesc": "Ім'я користувача для автентифікації SMTP", "smtpUsernameDesc": "Ім'я користувача для автентифікації SMTP",
"smtpFrom": "Адреса відправника (From)",
"smtpFromDesc": "Адреса в заголовку From листа. Залиште порожнім, щоб використати ім'я користувача.",
"smtpFromName": "Ім'я відправника (From)",
"smtpFromNameDesc": "Необов'язкове відображуване ім'я перед адресою в заголовку From.",
"smtpPassword": "Пароль SMTP", "smtpPassword": "Пароль SMTP",
"smtpPasswordDesc": "Пароль для автентифікації SMTP", "smtpPasswordDesc": "Пароль для автентифікації SMTP",
"smtpTo": "Отримувачі", "smtpTo": "Отримувачі",

View File

@@ -1358,6 +1358,10 @@
"smtpPortDesc": "Cổng máy chủ SMTP (mặc định: 587)", "smtpPortDesc": "Cổng máy chủ SMTP (mặc định: 587)",
"smtpUsername": "Tên đăng nhập SMTP", "smtpUsername": "Tên đăng nhập SMTP",
"smtpUsernameDesc": "Tên đăng nhập xác thực 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", "smtpPassword": "Mật khẩu SMTP",
"smtpPasswordDesc": "Mật khẩu xác thực SMTP", "smtpPasswordDesc": "Mật khẩu xác thực SMTP",
"smtpTo": "Người nhận", "smtpTo": "Người nhận",

View File

@@ -1358,6 +1358,10 @@
"smtpPortDesc": "SMTP 服务器端口默认587", "smtpPortDesc": "SMTP 服务器端口默认587",
"smtpUsername": "SMTP 用户名", "smtpUsername": "SMTP 用户名",
"smtpUsernameDesc": "SMTP 认证用户名", "smtpUsernameDesc": "SMTP 认证用户名",
"smtpFrom": "发件人地址 (From)",
"smtpFromDesc": "邮件 From 头使用的地址。留空则使用用户名。",
"smtpFromName": "发件人名称 (From)",
"smtpFromNameDesc": "From 头中显示在地址前的可选显示名称。",
"smtpPassword": "SMTP 密码", "smtpPassword": "SMTP 密码",
"smtpPasswordDesc": "SMTP 认证密码", "smtpPasswordDesc": "SMTP 认证密码",
"smtpTo": "收件人", "smtpTo": "收件人",

View File

@@ -1358,6 +1358,10 @@
"smtpPortDesc": "SMTP 伺服器連接埠預設587", "smtpPortDesc": "SMTP 伺服器連接埠預設587",
"smtpUsername": "SMTP 使用者名稱", "smtpUsername": "SMTP 使用者名稱",
"smtpUsernameDesc": "SMTP 驗證使用者名稱", "smtpUsernameDesc": "SMTP 驗證使用者名稱",
"smtpFrom": "寄件者位址 (From)",
"smtpFromDesc": "郵件 From 標頭使用的位址。留空則使用使用者名稱。",
"smtpFromName": "寄件者名稱 (From)",
"smtpFromNameDesc": "From 標頭中顯示在位址前的可選顯示名稱。",
"smtpPassword": "SMTP 密碼", "smtpPassword": "SMTP 密碼",
"smtpPasswordDesc": "SMTP 驗證密碼", "smtpPasswordDesc": "SMTP 驗證密碼",
"smtpTo": "收件人", "smtpTo": "收件人",