Commit 2f23a667 by Benson Yan Committed by GitHub

fix: support SMTP STARTTLS mode and NTLM auth (#5426)

* fix: support SMTP STARTTLS mode and NTLM auth

Add explicit SMTP STARTTLS configuration for 587-style connections and keep SSL/TLS as the implicit TLS mode.

Prefer PLAIN when advertised, keep LOGIN compatibility, and add NTLM as a fallback for Exchange SMTP servers that require it after STARTTLS.

* fix: respect explicit SMTP encryption mode

* fix: preserve SMTP TLS compatibility
parent 9fc9c8f1
......@@ -120,6 +120,8 @@ var InsecureTLSConfig = &tls.Config{InsecureSkipVerify: true}
var SMTPServer = ""
var SMTPPort = 587
var SMTPSSLEnabled = false
var SMTPStartTLSEnabled = false
var SMTPInsecureSkipVerify = false
var SMTPForceAuthLogin = false
var SMTPAccount = ""
var SMTPFrom = ""
......
......@@ -27,10 +27,52 @@ func shouldUseSMTPLoginAuth() bool {
}
func getSMTPAuth() smtp.Auth {
if shouldUseSMTPLoginAuth() {
return LoginAuth(SMTPAccount, SMTPToken)
return AutoSMTPAuth(SMTPAccount, SMTPToken)
}
func shouldAuthenticateSMTP() bool {
return SMTPAccount != "" && SMTPToken != ""
}
func smtpTLSConfig() *tls.Config {
return &tls.Config{
ServerName: SMTPServer,
InsecureSkipVerify: SMTPInsecureSkipVerify, // #nosec G402 -- admin-controlled SMTP compatibility option.
}
}
func newSMTPClient(addr string) (*smtp.Client, error) {
if SMTPSSLEnabled || (SMTPPort == 465 && !SMTPStartTLSEnabled) {
conn, err := tls.Dial("tcp", addr, smtpTLSConfig())
if err != nil {
return nil, err
}
client, err := smtp.NewClient(conn, SMTPServer)
if err != nil {
_ = conn.Close()
return nil, err
}
return client, nil
}
return smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
client, err := smtp.Dial(addr)
if err != nil {
return nil, err
}
if SMTPStartTLSEnabled {
startTLSSupported, _ := client.Extension("STARTTLS")
if !startTLSSupported {
_ = client.Close()
return nil, fmt.Errorf("SMTP server does not support STARTTLS")
}
if err := client.StartTLS(smtpTLSConfig()); err != nil {
_ = client.Close()
return nil, err
}
}
return client, nil
}
func SendEmail(subject string, receiver string, content string) error {
......@@ -56,47 +98,37 @@ func SendEmail(subject string, receiver string, content string) error {
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
to := strings.Split(receiver, ";")
var err error
if SMTPPort == 465 || SMTPSSLEnabled {
tlsConfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: SMTPServer,
}
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", SMTPServer, SMTPPort), tlsConfig)
if err != nil {
return err
}
client, err := smtp.NewClient(conn, SMTPServer)
if err != nil {
return err
}
defer client.Close()
client, err := newSMTPClient(addr)
if err != nil {
return err
}
defer client.Close()
if shouldAuthenticateSMTP() {
if err = client.Auth(auth); err != nil {
return err
}
if err = client.Mail(SMTPFrom); err != nil {
return err
}
receiverEmails := strings.Split(receiver, ";")
for _, receiver := range receiverEmails {
if err = client.Rcpt(receiver); err != nil {
return err
}
}
w, err := client.Data()
if err != nil {
return err
}
_, err = w.Write(mail)
if err != nil {
return err
}
err = w.Close()
if err != nil {
}
if err = client.Mail(SMTPFrom); err != nil {
return err
}
for _, receiver := range to {
if err = client.Rcpt(receiver); err != nil {
return err
}
} else {
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
}
w, err := client.Data()
if err != nil {
return err
}
_, err = w.Write(mail)
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
err = client.Quit()
if err != nil {
SysError(fmt.Sprintf("failed to send email to %s: %v", receiver, err))
}
......
package common
import (
"errors"
"net/smtp"
"strings"
ntlmssp "github.com/Azure/go-ntlmssp"
)
type smtpAutoAuth struct {
username string
password string
mech string
}
func AutoSMTPAuth(username, password string) smtp.Auth {
return &smtpAutoAuth{username: username, password: password}
}
func (a *smtpAutoAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
useLoginAuth := SMTPForceAuthLogin
if !useLoginAuth && shouldUseSMTPLoginAuth() {
useLoginAuth = !(server != nil && len(server.Auth) == 1 && smtpServerSupportsAuth(server, "NTLM"))
}
if useLoginAuth {
a.mech = "LOGIN"
return "LOGIN", []byte{}, nil
}
switch {
case smtpServerSupportsAuth(server, "PLAIN"):
a.mech = "PLAIN"
return "PLAIN", []byte("\x00" + a.username + "\x00" + a.password), nil
case smtpServerSupportsAuth(server, "LOGIN"):
a.mech = "LOGIN"
return "LOGIN", []byte{}, nil
case smtpServerSupportsAuth(server, "NTLM"):
a.mech = "NTLM"
negotiateMessage, err := ntlmssp.NewNegotiateMessage("", "")
if err != nil {
return "", nil, err
}
return "NTLM", negotiateMessage, nil
default:
a.mech = "PLAIN"
return "PLAIN", []byte("\x00" + a.username + "\x00" + a.password), nil
}
}
func (a *smtpAutoAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if !more {
return nil, nil
}
switch a.mech {
case "LOGIN":
switch string(fromServer) {
case "Username:":
return []byte(a.username), nil
case "Password:":
return []byte(a.password), nil
default:
return nil, errors.New("unknown SMTP AUTH LOGIN challenge")
}
case "NTLM":
return ntlmssp.NewAuthenticateMessage(fromServer, a.username, a.password, nil)
default:
return nil, errors.New("unexpected SMTP auth challenge")
}
}
func smtpServerSupportsAuth(server *smtp.ServerInfo, mechanism string) bool {
if server == nil {
return false
}
for _, auth := range server.Auth {
if strings.EqualFold(auth, mechanism) {
return true
}
}
return false
}
package common
import (
"bufio"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"net"
"strconv"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
)
type fakeSMTPServer struct {
listener net.Listener
host string
port int
cert tls.Certificate
advertiseSTARTTLS bool
authMechanisms []string
messages chan string
authCommands chan string
startTLSCommands chan string
}
func newFakeSMTPServer(t *testing.T) *fakeSMTPServer {
return newFakeSMTPServerWithSTARTTLSAdvertisement(t, true)
}
func newFakeSMTPServerWithSTARTTLSAdvertisement(t *testing.T, advertiseSTARTTLS bool) *fakeSMTPServer {
t.Helper()
cert, err := newTestTLSCertificate()
require.NoError(t, err)
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
host, portText, err := net.SplitHostPort(listener.Addr().String())
require.NoError(t, err)
port, err := strconv.Atoi(portText)
require.NoError(t, err)
server := &fakeSMTPServer{
listener: listener,
host: host,
port: port,
cert: cert,
advertiseSTARTTLS: advertiseSTARTTLS,
authMechanisms: []string{"PLAIN", "LOGIN"},
messages: make(chan string, 1),
authCommands: make(chan string, 1),
startTLSCommands: make(chan string, 1),
}
go server.serve()
return server
}
func newFakeImplicitTLSSMTPServer(t *testing.T) *fakeSMTPServer {
t.Helper()
cert, err := newTestTLSCertificate()
require.NoError(t, err)
listener, err := net.Listen("tcp", "127.0.0.1:0")
require.NoError(t, err)
host, portText, err := net.SplitHostPort(listener.Addr().String())
require.NoError(t, err)
port, err := strconv.Atoi(portText)
require.NoError(t, err)
server := &fakeSMTPServer{
listener: tls.NewListener(listener, &tls.Config{Certificates: []tls.Certificate{cert}}),
host: host,
port: port,
cert: cert,
advertiseSTARTTLS: false,
authMechanisms: []string{"PLAIN", "LOGIN"},
messages: make(chan string, 1),
authCommands: make(chan string, 1),
startTLSCommands: make(chan string, 1),
}
go server.serve()
return server
}
func (s *fakeSMTPServer) close() {
_ = s.listener.Close()
}
func (s *fakeSMTPServer) serve() {
conn, err := s.listener.Accept()
if err != nil {
return
}
defer conn.Close()
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
rw := bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
if err := writeSMTPLine(rw, "220 fake.smtp.local ESMTP"); err != nil {
return
}
encrypted := false
for {
line, err := rw.ReadString('\n')
if err != nil {
return
}
command := strings.TrimRight(line, "\r\n")
upperCommand := strings.ToUpper(command)
switch {
case strings.HasPrefix(upperCommand, "EHLO"):
if err := writeSMTPLine(rw, "250-fake.smtp.local"); err != nil {
return
}
if !encrypted && s.advertiseSTARTTLS {
if err := writeSMTPLine(rw, "250-STARTTLS"); err != nil {
return
}
}
if len(s.authMechanisms) > 0 {
if err := writeSMTPLine(rw, "250 AUTH "+strings.Join(s.authMechanisms, " ")); err != nil {
return
}
} else if err := writeSMTPLine(rw, "250 8BITMIME"); err != nil {
return
}
case upperCommand == "STARTTLS":
if encrypted || !s.advertiseSTARTTLS {
if err := writeSMTPLine(rw, "502 5.5.1 STARTTLS not supported"); err != nil {
return
}
continue
}
select {
case s.startTLSCommands <- command:
default:
}
if err := writeSMTPLine(rw, "220 2.0.0 Ready to start TLS"); err != nil {
return
}
tlsConn := tls.Server(conn, &tls.Config{Certificates: []tls.Certificate{s.cert}})
if err := tlsConn.Handshake(); err != nil {
return
}
conn = tlsConn
rw = bufio.NewReadWriter(bufio.NewReader(conn), bufio.NewWriter(conn))
encrypted = true
case strings.HasPrefix(upperCommand, "AUTH"):
select {
case s.authCommands <- command:
default:
}
if err := writeSMTPLine(rw, "235 2.7.0 Authentication successful"); err != nil {
return
}
case strings.HasPrefix(upperCommand, "MAIL FROM:"):
if err := writeSMTPLine(rw, "250 2.1.0 Sender OK"); err != nil {
return
}
case strings.HasPrefix(upperCommand, "RCPT TO:"):
if err := writeSMTPLine(rw, "250 2.1.5 Recipient OK"); err != nil {
return
}
case upperCommand == "DATA":
if err := writeSMTPLine(rw, "354 End data with <CR><LF>.<CR><LF>"); err != nil {
return
}
var data strings.Builder
for {
dataLine, err := rw.ReadString('\n')
if err != nil {
return
}
if strings.TrimRight(dataLine, "\r\n") == "." {
break
}
data.WriteString(dataLine)
}
s.messages <- data.String()
if err := writeSMTPLine(rw, "250 2.0.0 Queued"); err != nil {
return
}
case upperCommand == "QUIT":
_ = writeSMTPLine(rw, "221 2.0.0 Bye")
return
default:
if err := writeSMTPLine(rw, "502 5.5.1 Command not implemented"); err != nil {
return
}
}
}
}
func writeSMTPLine(rw *bufio.ReadWriter, line string) error {
_, err := rw.WriteString(line + "\r\n")
if err != nil {
return err
}
return rw.Flush()
}
func newTestTLSCertificate() (tls.Certificate, error) {
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
return tls.Certificate{}, err
}
template := x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "aixinexchange01.aixin-chip.com",
},
NotBefore: time.Now().Add(-time.Hour),
NotAfter: time.Now().Add(time.Hour),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
DNSNames: []string{"aixinexchange01", "aixinexchange01.aixin-chip.com"},
}
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
if err != nil {
return tls.Certificate{}, err
}
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes})
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})
return tls.X509KeyPair(certPEM, keyPEM)
}
func withSMTPSettings(t *testing.T) {
t.Helper()
originalSMTPServer := SMTPServer
originalSMTPPort := SMTPPort
originalSMTPSSLEnabled := SMTPSSLEnabled
originalSMTPStartTLSEnabled := SMTPStartTLSEnabled
originalSMTPInsecureSkipVerify := SMTPInsecureSkipVerify
originalSMTPForceAuthLogin := SMTPForceAuthLogin
originalSMTPAccount := SMTPAccount
originalSMTPFrom := SMTPFrom
originalSMTPToken := SMTPToken
originalSystemName := SystemName
t.Cleanup(func() {
SMTPServer = originalSMTPServer
SMTPPort = originalSMTPPort
SMTPSSLEnabled = originalSMTPSSLEnabled
SMTPStartTLSEnabled = originalSMTPStartTLSEnabled
SMTPInsecureSkipVerify = originalSMTPInsecureSkipVerify
SMTPForceAuthLogin = originalSMTPForceAuthLogin
SMTPAccount = originalSMTPAccount
SMTPFrom = originalSMTPFrom
SMTPToken = originalSMTPToken
SystemName = originalSystemName
})
}
func TestSendEmailUsesExplicitStartTLSWithInsecureCertificate(t *testing.T) {
server := newFakeSMTPServer(t)
defer server.close()
withSMTPSettings(t)
SMTPServer = server.host
SMTPPort = server.port
SMTPSSLEnabled = false
SMTPStartTLSEnabled = true
SMTPInsecureSkipVerify = true
SMTPForceAuthLogin = false
SMTPAccount = "sender@example.com"
SMTPFrom = "sender@example.com"
SMTPToken = "secret"
SystemName = "New API"
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
require.NoError(t, err)
select {
case message := <-server.messages:
require.Contains(t, message, "Subject: =?UTF-8?B?")
require.Contains(t, message, "<p>123456</p>")
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for SMTP DATA")
}
}
func TestSendEmailExplicitStartTLSRequiresServerSupport(t *testing.T) {
server := newFakeSMTPServerWithSTARTTLSAdvertisement(t, false)
defer server.close()
withSMTPSettings(t)
SMTPServer = server.host
SMTPPort = server.port
SMTPSSLEnabled = false
SMTPStartTLSEnabled = true
SMTPInsecureSkipVerify = true
SMTPForceAuthLogin = false
SMTPAccount = "sender@example.com"
SMTPFrom = "sender@example.com"
SMTPToken = "secret"
SystemName = "New API"
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
require.Error(t, err)
require.Contains(t, err.Error(), "STARTTLS")
}
func TestSendEmailDoesNotAutoUpgradeWhenStartTLSDisabled(t *testing.T) {
server := newFakeSMTPServerWithSTARTTLSAdvertisement(t, true)
defer server.close()
withSMTPSettings(t)
SMTPServer = server.host
SMTPPort = server.port
SMTPSSLEnabled = false
SMTPStartTLSEnabled = false
SMTPInsecureSkipVerify = false
SMTPForceAuthLogin = false
SMTPAccount = "sender@example.com"
SMTPFrom = "sender@example.com"
SMTPToken = "secret"
SystemName = "New API"
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
require.NoError(t, err)
select {
case command := <-server.startTLSCommands:
t.Fatalf("unexpected SMTP STARTTLS command: %s", command)
default:
}
select {
case message := <-server.messages:
require.Contains(t, message, "<p>123456</p>")
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for SMTP DATA")
}
}
func TestNewSMTPClientHonorsExplicitStartTLSWhenPortIs465(t *testing.T) {
server := newFakeSMTPServer(t)
defer server.close()
withSMTPSettings(t)
SMTPServer = server.host
SMTPPort = 465
SMTPSSLEnabled = false
SMTPStartTLSEnabled = true
SMTPInsecureSkipVerify = true
client, err := newSMTPClient(fmt.Sprintf("%s:%d", server.host, server.port))
require.NoError(t, err)
defer client.Close()
select {
case command := <-server.startTLSCommands:
require.Equal(t, "STARTTLS", command)
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for SMTP STARTTLS")
}
}
func TestNewSMTPClientKeepsImplicitTLSForLegacyPort465(t *testing.T) {
server := newFakeImplicitTLSSMTPServer(t)
defer server.close()
withSMTPSettings(t)
SMTPServer = server.host
SMTPPort = 465
SMTPSSLEnabled = false
SMTPStartTLSEnabled = false
SMTPInsecureSkipVerify = true
client, err := newSMTPClient(fmt.Sprintf("%s:%d", server.host, server.port))
require.NoError(t, err)
defer client.Close()
}
func TestSendEmailSkipsAuthWhenCredentialsAreEmpty(t *testing.T) {
server := newFakeSMTPServerWithSTARTTLSAdvertisement(t, false)
defer server.close()
withSMTPSettings(t)
SMTPServer = server.host
SMTPPort = server.port
SMTPSSLEnabled = false
SMTPStartTLSEnabled = false
SMTPInsecureSkipVerify = false
SMTPForceAuthLogin = false
SMTPAccount = ""
SMTPFrom = "sender@example.com"
SMTPToken = ""
SystemName = "New API"
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
require.NoError(t, err)
select {
case command := <-server.authCommands:
t.Fatalf("unexpected SMTP auth command: %s", command)
default:
}
select {
case message := <-server.messages:
require.Contains(t, message, "<p>123456</p>")
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for SMTP DATA")
}
}
func TestSendEmailSkipsAuthWhenCredentialsAreIncomplete(t *testing.T) {
server := newFakeSMTPServerWithSTARTTLSAdvertisement(t, false)
defer server.close()
withSMTPSettings(t)
SMTPServer = server.host
SMTPPort = server.port
SMTPSSLEnabled = false
SMTPStartTLSEnabled = false
SMTPInsecureSkipVerify = false
SMTPForceAuthLogin = false
SMTPAccount = "sender@example.com"
SMTPFrom = "sender@example.com"
SMTPToken = ""
SystemName = "New API"
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
require.NoError(t, err)
select {
case command := <-server.authCommands:
t.Fatalf("unexpected SMTP auth command: %s", command)
default:
}
select {
case message := <-server.messages:
require.Contains(t, message, "<p>123456</p>")
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for SMTP DATA")
}
}
func TestSendEmailUsesNTLMWhenServerOnlySupportsNTLM(t *testing.T) {
server := newFakeSMTPServer(t)
server.authMechanisms = []string{"NTLM"}
defer server.close()
withSMTPSettings(t)
SMTPServer = server.host
SMTPPort = server.port
SMTPSSLEnabled = false
SMTPStartTLSEnabled = true
SMTPInsecureSkipVerify = true
SMTPForceAuthLogin = false
SMTPAccount = "no-reply"
SMTPFrom = "no-reply@example.com"
SMTPToken = "secret"
SystemName = "New API"
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
require.NoError(t, err)
select {
case command := <-server.authCommands:
require.True(t, strings.HasPrefix(command, "AUTH NTLM "), "unexpected auth command: %s", command)
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for SMTP AUTH")
}
}
func TestSendEmailUsesNTLMForMicrosoftAccountWhenServerOnlySupportsNTLM(t *testing.T) {
server := newFakeSMTPServer(t)
server.authMechanisms = []string{"NTLM"}
defer server.close()
withSMTPSettings(t)
SMTPServer = server.host
SMTPPort = server.port
SMTPSSLEnabled = false
SMTPStartTLSEnabled = true
SMTPInsecureSkipVerify = true
SMTPForceAuthLogin = false
SMTPAccount = "no-reply@contoso.onmicrosoft.com"
SMTPFrom = "no-reply@contoso.onmicrosoft.com"
SMTPToken = "secret"
SystemName = "New API"
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
require.NoError(t, err)
select {
case command := <-server.authCommands:
require.True(t, strings.HasPrefix(command, "AUTH NTLM "), "unexpected auth command: %s", command)
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for SMTP AUTH")
}
}
func TestSendEmailExplicitStartTLSRejectsUntrustedCertificateByDefault(t *testing.T) {
server := newFakeSMTPServer(t)
defer server.close()
withSMTPSettings(t)
SMTPServer = server.host
SMTPPort = server.port
SMTPSSLEnabled = false
SMTPStartTLSEnabled = true
SMTPInsecureSkipVerify = false
SMTPForceAuthLogin = false
SMTPAccount = "sender@example.com"
SMTPFrom = "sender@example.com"
SMTPToken = "secret"
SystemName = "New API"
err := SendEmail("Verification", "receiver@example.com", "<p>123456</p>")
require.Error(t, err)
require.Contains(t, fmt.Sprint(err), "certificate")
}
......@@ -95,6 +95,8 @@ func InitEnv() {
}
}
}
SMTPStartTLSEnabled = GetEnvOrDefaultBool("SMTP_STARTTLS_ENABLE", GetEnvOrDefaultBool("SMTP_STARTTLS_ENABLED", false))
SMTPInsecureSkipVerify = GetEnvOrDefaultBool("SMTP_INSECURE_SKIP_VERIFY", GetEnvOrDefaultBool("SMTP_TLS_INSECURE_SKIP_VERIFY", false))
// Parse requestInterval and set RequestInterval
requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
......
......@@ -78,6 +78,8 @@ require (
go.opentelemetry.io/otel/trace v1.19.0 // indirect
)
require github.com/Azure/go-ntlmssp v0.1.1 // indirect
require (
github.com/DmitriyVTitov/size v1.5.0 // indirect
github.com/anknown/darts v0.0.0-20151216065714-83ff685239e6 // indirect
......
......@@ -608,6 +608,8 @@ github.com/Azure/azure-sdk-for-go v56.3.0+incompatible/go.mod h1:9XXNKU+eRnpl9mo
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20210608223527-2377c96fe795/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ntlmssp v0.1.1 h1:l+FM/EEMb0U9QZE7mKNEDw5Mu3mFiaa2GKOoTSsNDPw=
github.com/Azure/go-ntlmssp v0.1.1/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
github.com/Azure/go-autorest/autorest v0.11.1/go.mod h1:JFgpikqFJ/MleTTxwepExTKnFUKKszPS8UavbQYUMuw=
......
......@@ -63,6 +63,8 @@ func InitOptionMap() {
common.OptionMap["SMTPAccount"] = ""
common.OptionMap["SMTPToken"] = ""
common.OptionMap["SMTPSSLEnabled"] = strconv.FormatBool(common.SMTPSSLEnabled)
common.OptionMap["SMTPStartTLSEnabled"] = strconv.FormatBool(common.SMTPStartTLSEnabled)
common.OptionMap["SMTPInsecureSkipVerify"] = strconv.FormatBool(common.SMTPInsecureSkipVerify)
common.OptionMap["SMTPForceAuthLogin"] = strconv.FormatBool(common.SMTPForceAuthLogin)
common.OptionMap["Notice"] = ""
common.OptionMap["About"] = ""
......@@ -275,7 +277,7 @@ func updateOptionMap(key string, value string) (err error) {
common.ImageDownloadPermission = intValue
}
}
if strings.HasSuffix(key, "Enabled") || key == "DefaultCollapseSidebar" || key == "DefaultUseAutoGroup" || key == "SMTPForceAuthLogin" {
if strings.HasSuffix(key, "Enabled") || key == "DefaultCollapseSidebar" || key == "DefaultUseAutoGroup" || key == "SMTPForceAuthLogin" || key == "SMTPInsecureSkipVerify" {
boolValue := value == "true"
switch key {
case "PasswordRegisterEnabled":
......@@ -350,6 +352,10 @@ func updateOptionMap(key string, value string) (err error) {
setting.StopOnSensitiveEnabled = boolValue
case "SMTPSSLEnabled":
common.SMTPSSLEnabled = boolValue
case "SMTPStartTLSEnabled":
common.SMTPStartTLSEnabled = boolValue
case "SMTPInsecureSkipVerify":
common.SMTPInsecureSkipVerify = boolValue
case "SMTPForceAuthLogin":
common.SMTPForceAuthLogin = boolValue
case "WorkerAllowHttpImageRequestEnabled":
......
......@@ -91,6 +91,7 @@ const SystemSetting = () => {
EmailDomainRestrictionEnabled: '',
EmailAliasRestrictionEnabled: '',
SMTPSSLEnabled: '',
SMTPStartTLSEnabled: '',
SMTPForceAuthLogin: '',
EmailDomainWhitelist: [],
TelegramOAuthEnabled: '',
......@@ -183,6 +184,7 @@ const SystemSetting = () => {
case 'EmailDomainRestrictionEnabled':
case 'EmailAliasRestrictionEnabled':
case 'SMTPSSLEnabled':
case 'SMTPStartTLSEnabled':
case 'SMTPForceAuthLogin':
case 'LinuxDOOAuthEnabled':
case 'discord.enabled':
......@@ -321,6 +323,13 @@ const SystemSetting = () => {
const submitSMTP = async () => {
const options = [];
const smtpSecurityMode = inputs.SMTPSSLEnabled
? 'ssl_tls'
: inputs.SMTPStartTLSEnabled
? 'starttls'
: 'none';
const nextSMTPSSLEnabled = smtpSecurityMode === 'ssl_tls';
const nextSMTPStartTLSEnabled = smtpSecurityMode === 'starttls';
if (originInputs['SMTPServer'] !== inputs.SMTPServer) {
options.push({ key: 'SMTPServer', value: inputs.SMTPServer });
......@@ -343,6 +352,15 @@ const SystemSetting = () => {
) {
options.push({ key: 'SMTPToken', value: inputs.SMTPToken });
}
if (originInputs['SMTPSSLEnabled'] !== nextSMTPSSLEnabled) {
options.push({ key: 'SMTPSSLEnabled', value: nextSMTPSSLEnabled });
}
if (originInputs['SMTPStartTLSEnabled'] !== nextSMTPStartTLSEnabled) {
options.push({
key: 'SMTPStartTLSEnabled',
value: nextSMTPStartTLSEnabled,
});
}
if (options.length > 0) {
await updateOptions(options);
......@@ -691,6 +709,23 @@ const SystemSetting = () => {
}
};
const handleSMTPSecurityModeChange = async (event) => {
const mode = event && event.target ? event.target.value : event;
const nextSMTPSSLEnabled = mode === 'ssl_tls';
const nextSMTPStartTLSEnabled = mode === 'starttls';
formApiRef.current?.setValue('SMTPSSLEnabled', nextSMTPSSLEnabled);
formApiRef.current?.setValue(
'SMTPStartTLSEnabled',
nextSMTPStartTLSEnabled,
);
await updateOptions([
{ key: 'SMTPSSLEnabled', value: nextSMTPSSLEnabled },
{ key: 'SMTPStartTLSEnabled', value: nextSMTPStartTLSEnabled },
]);
};
const handlePasswordLoginConfirm = async () => {
await updateOptions([{ key: 'PasswordLoginEnabled', value: false }]);
setShowPasswordLoginConfirmModal(false);
......@@ -1328,15 +1363,30 @@ const SystemSetting = () => {
/>
</Col>
<Col xs={24} sm={24} md={8} lg={8} xl={8}>
<Form.Checkbox
field='SMTPSSLEnabled'
noLabel
onChange={(e) =>
handleCheckboxChange('SMTPSSLEnabled', e)
<Text strong>{t('SMTP 加密方式')}</Text>
<Radio.Group
type='button'
value={
inputs.SMTPSSLEnabled
? 'ssl_tls'
: inputs.SMTPStartTLSEnabled
? 'starttls'
: 'none'
}
onChange={handleSMTPSecurityModeChange}
style={{ marginTop: 8, marginBottom: 8 }}
>
{t('启用SMTP SSL')}
</Form.Checkbox>
<Radio value='none'>{t('无加密')}</Radio>
<Radio value='ssl_tls'>{t('SSL/TLS')}</Radio>
<Radio value='starttls'>{t('STARTTLS')}</Radio>
</Radio.Group>
<Text
type='secondary'
size='small'
style={{ display: 'block', marginBottom: 8 }}
>
{t('请选择一种 SMTP 传输加密方式')}
</Text>
<Form.Checkbox
field='SMTPForceAuthLogin'
noLabel
......
......@@ -234,6 +234,9 @@
"SMTP 端口": "SMTP Port",
"SMTP 访问凭证": "SMTP Access Credential",
"SMTP 账户": "SMTP Account",
"SMTP 加密方式": "SMTP encryption",
"SSL/TLS": "SSL/TLS",
"STARTTLS": "STARTTLS",
"Sonnet 模型": "Sonnet Model",
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "The speed field controls Claude inference speed mode. Disabled by default to avoid unintentionally switching to fast mode",
"SSE 事件": "SSE Events",
......@@ -1808,6 +1811,7 @@
"新获取的模型": "New models",
"新额度:": "New quota: ",
"无": "None",
"无加密": "No encryption",
"无GPU": "No GPU",
"无冲突项": "No conflict items",
"无效的部署信息": "Invalid deployment information",
......@@ -3232,6 +3236,7 @@
"请选择该渠道所支持的模型,留空则不更改": "Please select the models supported by the channel, leaving blank will not change",
"请选择过期时间": "Please select expiration time",
"请选择通知方式": "Please select notification method",
"请选择一种 SMTP 传输加密方式": "Please select one SMTP transport security mode",
"调整额度": "Adjust Quota",
"调整额度成功": "Quota adjusted successfully",
"调用次数": "Call Count",
......
......@@ -232,6 +232,9 @@
"SMTP 端口": "Port SMTP",
"SMTP 访问凭证": "Informations d'identification d'accès SMTP",
"SMTP 账户": "Compte SMTP",
"SMTP 加密方式": "Chiffrement SMTP",
"SSL/TLS": "SSL/TLS",
"STARTTLS": "STARTTLS",
"Sonnet 模型": "Modèle Sonnet",
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "Le champ speed contrôle le mode de vitesse d'inférence de Claude. Désactivé par défaut pour éviter un passage involontaire au mode fast",
"SSE 事件": "Événement SSE",
......@@ -1801,6 +1804,7 @@
"新获取的模型": "Nouveaux modèles",
"新额度:": "Nouveau quota : ",
"无": "Aucun",
"无加密": "Aucun chiffrement",
"无GPU": "No GPU",
"无冲突项": "Aucun élément en conflit",
"无效的部署信息": "Invalid deployment information",
......@@ -3216,6 +3220,7 @@
"请选择该渠道所支持的模型,留空则不更改": "Veuillez sélectionner les modèles pris en charge par le canal, laisser vide ne changera rien",
"请选择过期时间": "Veuillez sélectionner une date d'expiration",
"请选择通知方式": "Veuillez sélectionner la méthode de notification",
"请选择一种 SMTP 传输加密方式": "Veuillez sélectionner un mode de sécurité de transport SMTP",
"调整额度": "Ajuster le quota",
"调整额度成功": "Quota ajusté avec succès",
"调用次数": "Nombre d'appels",
......
......@@ -226,6 +226,9 @@
"SMTP 端口": "SMTP ポート",
"SMTP 访问凭证": "SMTP 認証情報",
"SMTP 账户": "SMTP アカウント",
"SMTP 加密方式": "SMTP 暗号化方式",
"SSL/TLS": "SSL/TLS",
"STARTTLS": "STARTTLS",
"Sonnet 模型": "Sonnetモデル",
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "speed フィールドは Claude の推論速度モードを制御します。意図せず fast モードへ切り替わるのを避けるため、デフォルトで無効です",
"SSE 事件": "SSEイベント",
......@@ -1772,6 +1775,7 @@
"新获取的模型": "新たに取得したモデル",
"新额度:": "新しい残高:",
"无": "なし",
"无加密": "暗号化なし",
"无GPU": "No GPU",
"无冲突项": "競合項目なし",
"无效的部署信息": "Invalid deployment information",
......@@ -3185,6 +3189,7 @@
"请选择该渠道所支持的模型,留空则不更改": "このチャネルに対応しているモデルを選択してください。空欄の場合は変更されません",
"请选择过期时间": "有効期限を選択してください",
"请选择通知方式": "通知方法を選択してください",
"请选择一种 SMTP 传输加密方式": "SMTP の転送暗号化方式を 1 つ選択してください",
"调整额度": "残高調整",
"调整额度成功": "残高の調整に成功しました",
"调用次数": "呼び出し回数",
......
......@@ -236,6 +236,9 @@
"SMTP 端口": "Порт SMTP",
"SMTP 访问凭证": "Учетные данные доступа SMTP",
"SMTP 账户": "Учетная запись SMTP",
"SMTP 加密方式": "Шифрование SMTP",
"SSL/TLS": "SSL/TLS",
"STARTTLS": "STARTTLS",
"Sonnet 模型": "Модель Sonnet",
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "Поле speed управляет режимом скорости инференса Claude. По умолчанию отключено, чтобы избежать непреднамеренного переключения в режим fast",
"SSE 事件": "Событие SSE",
......@@ -1819,6 +1822,7 @@
"新获取的模型": "Новые полученные модели",
"新额度:": "Новая квота: ",
"无": "Нет",
"无加密": "Без шифрования",
"无GPU": "No GPU",
"无冲突项": "Нет конфликтующих элементов",
"无效的部署信息": "Invalid deployment information",
......@@ -3236,6 +3240,7 @@
"请选择该渠道所支持的模型,留空则不更改": "Пожалуйста, выберите модели, поддерживаемые этим каналом, оставьте пустым для без изменений",
"请选择过期时间": "Пожалуйста, выберите время истечения",
"请选择通知方式": "Пожалуйста, выберите способ уведомления",
"请选择一种 SMTP 传输加密方式": "Выберите один из режимов защиты SMTP-транспорта",
"调整额度": "Скорректировать квоту",
"调整额度成功": "Квота успешно скорректирована",
"调用次数": "Количество вызовов",
......
......@@ -227,6 +227,9 @@
"SMTP 端口": "Cổng SMTP",
"SMTP 访问凭证": "Thông tin xác thực truy cập SMTP",
"SMTP 账户": "Tài khoản SMTP",
"SMTP 加密方式": "Mã hóa SMTP",
"SSL/TLS": "SSL/TLS",
"STARTTLS": "STARTTLS",
"Sonnet 模型": "Model Sonnet",
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "Trường speed kiểm soát chế độ tốc độ suy luận Claude. Mặc định tắt để tránh vô tình chuyển sang chế độ fast",
"SSE 事件": "Sự kiện SSE",
......@@ -1773,6 +1776,7 @@
"新获取的模型": "Mô hình mới",
"新额度:": "Hạn ngạch mới: ",
"无": "Không",
"无加密": "Không mã hóa",
"无GPU": "No GPU",
"无冲突项": "Không có mục xung đột",
"无效的部署信息": "Invalid deployment information",
......@@ -3632,6 +3636,7 @@
"请选择语言": "Vui lòng chọn ngôn ngữ",
"请选择过期时间": "Vui lòng chọn thời gian hết hạn",
"请选择通知方式": "Vui lòng chọn phương thức thông báo",
"请选择一种 SMTP 传输加密方式": "Vui lòng chọn một chế độ bảo mật truyền tải SMTP",
"请阅读并同意": "Vui lòng đọc và đồng ý",
"读取": "Đọc",
"读取失败": "Đọc thất bại",
......
......@@ -219,6 +219,9 @@
"SMTP 端口": "SMTP 端口",
"SMTP 访问凭证": "SMTP 访问凭证",
"SMTP 账户": "SMTP 账户",
"SMTP 加密方式": "SMTP 加密方式",
"SSL/TLS": "SSL/TLS",
"STARTTLS": "STARTTLS",
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式",
"SSE 事件": "SSE 事件",
"SSE数据流": "SSE数据流",
......@@ -1766,6 +1769,7 @@
"新获取的模型": "新获取的模型",
"新额度:": "新额度:",
"无": "无",
"无加密": "无加密",
"无GPU": "无GPU",
"无冲突项": "无冲突项",
"无效的部署信息": "无效的部署信息",
......@@ -3187,6 +3191,7 @@
"请选择该渠道所支持的模型,留空则不更改": "请选择该渠道所支持的模型,留空则不更改",
"请选择过期时间": "请选择过期时间",
"请选择通知方式": "请选择通知方式",
"请选择一种 SMTP 传输加密方式": "请选择一种 SMTP 传输加密方式",
"调整额度": "调整额度",
"调整额度成功": "调整额度成功",
"调用次数": "调用次数",
......
......@@ -225,6 +225,9 @@
"SMTP 端口": "SMTP 端口",
"SMTP 访问凭证": "SMTP 訪問憑證",
"SMTP 账户": "SMTP 帳號",
"SMTP 加密方式": "SMTP 加密方式",
"SSL/TLS": "SSL/TLS",
"STARTTLS": "STARTTLS",
"speed 字段用于控制 Claude 推理速度模式。默认关闭以避免意外切换到 fast 模式": "speed 字段用於控制 Claude 推理速度模式。預設關閉以避免意外切換到 fast 模式",
"SSE 事件": "SSE 事件",
"SSE数据流": "SSE數據流",
......@@ -1776,6 +1779,7 @@
"新获取的模型": "新獲取的模型",
"新额度:": "新額度:",
"无": "無",
"无加密": "無加密",
"无GPU": "無GPU",
"无冲突项": "無衝突項",
"无效的部署信息": "無效的部署資訊",
......@@ -3197,6 +3201,7 @@
"请选择该渠道所支持的模型,留空则不更改": "請選擇該管道所支援的模型,留空則不更改",
"请选择过期时间": "請選擇過期時間",
"请选择通知方式": "請選擇通知方式",
"请选择一种 SMTP 传输加密方式": "請選擇一種 SMTP 傳輸加密方式",
"调整额度": "調整額度",
"调整额度成功": "調整額度成功",
"调用次数": "調用次數",
......
......@@ -144,6 +144,9 @@
"SMTP 端口": "SMTP 端口",
"SMTP 访问凭证": "SMTP 访问凭证",
"SMTP 账户": "SMTP 账户",
"SMTP 加密方式": "SMTP 加密方式",
"SSL/TLS": "SSL/TLS",
"STARTTLS": "STARTTLS",
"SSE 事件": "SSE 事件",
"SSE数据流": "SSE数据流",
"SSRF防护开关详细说明": "总开关控制是否启用SSRF防护功能。关闭后将跳过所有SSRF检查,允许访问任意URL。⚠️ 仅在完全信任环境中关闭此功能。",
......@@ -1217,6 +1220,7 @@
"新获取的模型": "新获取的模型",
"新额度:": "新额度:",
"无": "无",
"无加密": "无加密",
"无GPU": "无GPU",
"无冲突项": "无冲突项",
"无效的部署信息": "无效的部署信息",
......@@ -2242,6 +2246,7 @@
"请选择该渠道所支持的模型,留空则不更改": "请选择该渠道所支持的模型,留空则不更改",
"请选择过期时间": "请选择过期时间",
"请选择通知方式": "请选择通知方式",
"请选择一种 SMTP 传输加密方式": "请选择一种 SMTP 传输加密方式",
"调用次数": "调用次数",
"调用次数分布": "调用次数分布",
"调用次数排行": "调用次数排行",
......
......@@ -30,6 +30,8 @@ import {
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { Switch } from '@/components/ui/switch'
import {
SettingsForm,
......@@ -57,6 +59,8 @@ const createEmailSchema = (t: (key: string) => string) =>
}, t('Enter a valid email or leave blank')),
SMTPToken: z.string(),
SMTPSSLEnabled: z.boolean(),
SMTPStartTLSEnabled: z.boolean(),
SMTPInsecureSkipVerify: z.boolean(),
SMTPForceAuthLogin: z.boolean(),
})
......@@ -66,6 +70,17 @@ type EmailSettingsSectionProps = {
defaultValues: EmailFormValues
}
type SmtpSecurityMode = 'none' | 'ssl_tls' | 'starttls'
function getSmtpSecurityMode(values: {
SMTPSSLEnabled: boolean
SMTPStartTLSEnabled: boolean
}): SmtpSecurityMode {
if (values.SMTPSSLEnabled) return 'ssl_tls'
if (values.SMTPStartTLSEnabled) return 'starttls'
return 'none'
}
export function EmailSettingsSection({
defaultValues,
}: EmailSettingsSectionProps) {
......@@ -81,13 +96,16 @@ export function EmailSettingsSection({
useResetForm(form, defaultValues)
const onSubmit = async (values: EmailFormValues) => {
const securityMode = getSmtpSecurityMode(values)
const sanitized = {
SMTPServer: values.SMTPServer.trim(),
SMTPPort: values.SMTPPort.trim(),
SMTPAccount: values.SMTPAccount.trim(),
SMTPFrom: values.SMTPFrom.trim(),
SMTPToken: values.SMTPToken.trim(),
SMTPSSLEnabled: values.SMTPSSLEnabled,
SMTPSSLEnabled: securityMode === 'ssl_tls',
SMTPStartTLSEnabled: securityMode === 'starttls',
SMTPInsecureSkipVerify: values.SMTPInsecureSkipVerify,
SMTPForceAuthLogin: values.SMTPForceAuthLogin,
}
......@@ -98,6 +116,8 @@ export function EmailSettingsSection({
SMTPFrom: defaultValues.SMTPFrom.trim(),
SMTPToken: defaultValues.SMTPToken.trim(),
SMTPSSLEnabled: defaultValues.SMTPSSLEnabled,
SMTPStartTLSEnabled: defaultValues.SMTPStartTLSEnabled,
SMTPInsecureSkipVerify: defaultValues.SMTPInsecureSkipVerify,
SMTPForceAuthLogin: defaultValues.SMTPForceAuthLogin,
}
......@@ -130,6 +150,20 @@ export function EmailSettingsSection({
})
}
if (sanitized.SMTPStartTLSEnabled !== initial.SMTPStartTLSEnabled) {
updates.push({
key: 'SMTPStartTLSEnabled',
value: sanitized.SMTPStartTLSEnabled,
})
}
if (sanitized.SMTPInsecureSkipVerify !== initial.SMTPInsecureSkipVerify) {
updates.push({
key: 'SMTPInsecureSkipVerify',
value: sanitized.SMTPInsecureSkipVerify,
})
}
if (sanitized.SMTPForceAuthLogin !== initial.SMTPForceAuthLogin) {
updates.push({
key: 'SMTPForceAuthLogin',
......@@ -197,15 +231,78 @@ export function EmailSettingsSection({
)}
/>
<FormItem>
<FormLabel>{t('SMTP encryption')}</FormLabel>
<FormControl>
<RadioGroup
value={getSmtpSecurityMode({
SMTPSSLEnabled: form.watch('SMTPSSLEnabled'),
SMTPStartTLSEnabled: form.watch('SMTPStartTLSEnabled'),
})}
onValueChange={(value) => {
const mode = value as SmtpSecurityMode
form.setValue('SMTPSSLEnabled', mode === 'ssl_tls', {
shouldDirty: true,
})
form.setValue('SMTPStartTLSEnabled', mode === 'starttls', {
shouldDirty: true,
})
}}
className='gap-3'
>
<div className='flex items-center gap-2'>
<RadioGroupItem value='none' id='smtp-security-none' />
<Label
htmlFor='smtp-security-none'
className='cursor-pointer font-normal'
>
{t('No encryption')}
</Label>
</div>
<div className='flex items-center gap-2'>
<RadioGroupItem
value='ssl_tls'
id='smtp-security-ssl-tls'
/>
<Label
htmlFor='smtp-security-ssl-tls'
className='cursor-pointer font-normal'
>
{t('SSL/TLS')}
</Label>
</div>
<div className='flex items-center gap-2'>
<RadioGroupItem
value='starttls'
id='smtp-security-starttls'
/>
<Label
htmlFor='smtp-security-starttls'
className='cursor-pointer font-normal'
>
{t('STARTTLS')}
</Label>
</div>
</RadioGroup>
</FormControl>
<FormDescription>
{t('Choose one SMTP transport security mode')}
</FormDescription>
</FormItem>
<FormField
control={form.control}
name='SMTPSSLEnabled'
name='SMTPInsecureSkipVerify'
render={({ field }) => (
<SettingsSwitchItem>
<SettingsSwitchContent>
<FormLabel>{t('Enable SSL/TLS')}</FormLabel>
<FormLabel>
{t('Skip SMTP TLS certificate verification')}
</FormLabel>
<FormDescription>
{t('Use secure connection when sending emails')}
{t(
'Allow self-signed or hostname-mismatched SMTP certificates'
)}
</FormDescription>
</SettingsSwitchContent>
<FormControl>
......
......@@ -36,6 +36,8 @@ const defaultOperationsSettings: OperationsSettings = {
SMTPFrom: '',
SMTPToken: '',
SMTPSSLEnabled: false,
SMTPStartTLSEnabled: false,
SMTPInsecureSkipVerify: false,
SMTPForceAuthLogin: false,
WorkerUrl: '',
WorkerValidKey: '',
......
......@@ -71,6 +71,8 @@ const OPERATIONS_SECTIONS = [
SMTPFrom: settings.SMTPFrom,
SMTPToken: settings.SMTPToken,
SMTPSSLEnabled: settings.SMTPSSLEnabled,
SMTPStartTLSEnabled: settings.SMTPStartTLSEnabled,
SMTPInsecureSkipVerify: settings.SMTPInsecureSkipVerify,
SMTPForceAuthLogin: settings.SMTPForceAuthLogin,
}}
/>
......
......@@ -334,6 +334,8 @@ export type OperationsSettings = {
SMTPFrom: string
SMTPToken: string
SMTPSSLEnabled: boolean
SMTPStartTLSEnabled: boolean
SMTPInsecureSkipVerify: boolean
SMTPForceAuthLogin: boolean
WorkerUrl: string
WorkerValidKey: string
......
......@@ -299,6 +299,7 @@
"Allow requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)": "Allow requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)",
"Allow Retry": "Allow Retry",
"Allow safety_identifier passthrough": "Allow safety_identifier passthrough",
"Allow self-signed or hostname-mismatched SMTP certificates": "Allow self-signed or hostname-mismatched SMTP certificates",
"Allow service_tier passthrough": "Allow service_tier passthrough",
"Allow speed passthrough": "Allow speed passthrough",
"Allow upstream callbacks": "Allow upstream callbacks",
......@@ -757,6 +758,7 @@
"Choose how the platform will operate": "Choose how the platform will operate",
"Choose how to filter domains": "Choose how to filter domains",
"Choose how to filter IP addresses": "Choose how to filter IP addresses",
"Choose one SMTP transport security mode": "Choose one SMTP transport security mode",
"Choose the bundle type and define the items inside it.": "Choose the bundle type and define the items inside it.",
"Choose the default charts, range, and time granularity for model analytics.": "Choose the default charts, range, and time granularity for model analytics.",
"Choose where to fetch upstream metadata.": "Choose where to fetch upstream metadata.",
......@@ -1493,6 +1495,7 @@
"Enable selected models": "Enable selected models",
"Enable SSL/TLS": "Enable SSL/TLS",
"Enable SSRF Protection": "Enable SSRF Protection",
"Enable STARTTLS": "Enable STARTTLS",
"Enable streaming mode for the test request.": "Enable streaming mode for the test request.",
"Enable Telegram OAuth": "Enable Telegram OAuth",
"Enable test mode for Creem payments": "Enable test mode for Creem payments",
......@@ -2806,6 +2809,7 @@
"Non-stream": "Non-stream",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.",
"None": "None",
"No encryption": "No encryption",
"noreply@example.com": "noreply@example.com",
"Normalized:": "Normalized:",
"Not available": "Not available",
......@@ -3917,13 +3921,16 @@
"Size:": "Size:",
"sk_xxx or rk_xxx": "sk_xxx or rk_xxx",
"Skip retry on failure": "Skip retry on failure",
"Skip SMTP TLS certificate verification": "Skip SMTP TLS certificate verification",
"Skip to Main": "Skip to Main",
"Slug": "Slug",
"Slug can only contain letters, numbers, hyphens, and underscores": "Slug can only contain letters, numbers, hyphens, and underscores",
"Slug is required": "Slug is required",
"Slug must be less than 100 characters": "Slug must be less than 100 characters",
"Smallest USD amount users can recharge (Epay)": "Smallest USD amount users can recharge (Epay)",
"SSL/TLS": "SSL/TLS",
"SMTP Email": "SMTP Email",
"SMTP encryption": "SMTP encryption",
"SMTP Host": "SMTP Host",
"smtp.example.com": "smtp.example.com",
"socks5://user:pass@host:port": "socks5://user:pass@host:port",
......@@ -3953,6 +3960,7 @@
"SSRF Protection": "SSRF Protection",
"Standard": "Standard",
"Standard price": "Standard price",
"STARTTLS": "STARTTLS",
"Start": "Start",
"Start a conversation to see messages here": "Start a conversation to see messages here",
"Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.": "Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.",
......@@ -4478,6 +4486,7 @@
"Updated user {{username}} (ID: {{id}})": "Updated user {{username}} (ID: {{id}})",
"Updating all channel balances. This may take a while. Please refresh to see results.": "Updating all channel balances. This may take a while. Please refresh to see results.",
"Upgrade Group": "Upgrade Group",
"Upgrade plaintext SMTP connection with STARTTLS before authentication": "Upgrade plaintext SMTP connection with STARTTLS before authentication",
"Upload": "Upload",
"Upload a single service account JSON file": "Upload a single service account JSON file",
"Upload file": "Upload file",
......
......@@ -299,6 +299,7 @@
"Allow requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)": "Autoriser les requêtes vers les plages d'adresses IP privées (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)",
"Allow Retry": "Autoriser la relance",
"Allow safety_identifier passthrough": "Autoriser la transmission de safety_identifier",
"Allow self-signed or hostname-mismatched SMTP certificates": "Autoriser les certificats SMTP auto-signés ou dont le nom d'hôte ne correspond pas",
"Allow service_tier passthrough": "Autoriser la transmission de service_tier",
"Allow speed passthrough": "Autoriser la transmission de speed",
"Allow upstream callbacks": "Autoriser les callbacks en amont",
......@@ -757,6 +758,7 @@
"Choose how the platform will operate": "Choisissez le mode de fonctionnement de la plateforme",
"Choose how to filter domains": "Choisissez comment filtrer les domaines",
"Choose how to filter IP addresses": "Choisissez comment filtrer les adresses IP",
"Choose one SMTP transport security mode": "Choisissez un mode de sécurité de transport SMTP",
"Choose the bundle type and define the items inside it.": "Choisissez le type de bundle et définissez les éléments qu'il contient.",
"Choose the default charts, range, and time granularity for model analytics.": "Choisissez les graphiques, la plage et la granularité temporelle par défaut pour l'analyse des modèles.",
"Choose where to fetch upstream metadata.": "Choisissez où récupérer les métadonnées amont.",
......@@ -1493,6 +1495,7 @@
"Enable selected models": "Activer les modèles sélectionnés",
"Enable SSL/TLS": "Activer SSL/TLS",
"Enable SSRF Protection": "Activer la protection SSRF",
"Enable STARTTLS": "Activer STARTTLS",
"Enable streaming mode for the test request.": "Activer le mode streaming pour la requête de test.",
"Enable Telegram OAuth": "Activer Telegram OAuth",
"Enable test mode for Creem payments": "Activer le mode test pour les paiements Creem",
......@@ -2806,6 +2809,7 @@
"Non-stream": "Non-streaming",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Les récompenses d’invitation non nulles nécessitent une confirmation de conformité dans les paramètres de la passerelle de paiement.",
"None": "Aucun",
"No encryption": "Aucun chiffrement",
"noreply@example.com": "noreply@example.com",
"Normalized:": "Normalisé :",
"Not available": "Non disponible",
......@@ -3917,13 +3921,16 @@
"Size:": "Taille :",
"sk_xxx or rk_xxx": "sk_xxx ou rk_xxx",
"Skip retry on failure": "Ne pas réessayer en cas d'échec",
"Skip SMTP TLS certificate verification": "Ignorer la vérification du certificat TLS SMTP",
"Skip to Main": "Aller au contenu principal",
"Slug": "Slug",
"Slug can only contain letters, numbers, hyphens, and underscores": "Le slug ne peut contenir que des lettres, des chiffres, des tirets et des underscores",
"Slug is required": "Le slug est requis",
"Slug must be less than 100 characters": "Le slug doit contenir moins de 100 caractères",
"Smallest USD amount users can recharge (Epay)": "Montant minimum en USD que les utilisateurs peuvent recharger (Epay)",
"SSL/TLS": "SSL/TLS",
"SMTP Email": "E-mail SMTP",
"SMTP encryption": "Chiffrement SMTP",
"SMTP Host": "Hôte SMTP",
"smtp.example.com": "smtp.example.com",
"socks5://user:pass@host:port": "socks5://user:pass@host:port",
......@@ -3953,6 +3960,7 @@
"SSRF Protection": "Protection SSRF",
"Standard": "Standard",
"Standard price": "Prix standard",
"STARTTLS": "STARTTLS",
"Start": "Début",
"Start a conversation to see messages here": "Démarrez une conversation pour voir les messages ici",
"Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.": "Commencez à encaisser des paiements dans le monde entier sans créer de société. Conçu pour les développeurs indépendants, les entrepreneurs individuels OPC et les startups. Waffo Pancake agit comme Merchant of Record et prend en charge la conformité liée à l’encaissement mondial : taxes à la consommation, facturation, gestion des abonnements, remboursements et rétrofacturations. Les développeurs solo peuvent lancer rapidement leur produit et rester concentrés sur celui-ci plutôt que sur la conformité. Intégration en quelques minutes, d’une seule invite à une intégration complète.",
......@@ -4478,6 +4486,7 @@
"Updated user {{username}} (ID: {{id}})": "Utilisateur {{username}} mis à jour (ID : {{id}})",
"Updating all channel balances. This may take a while. Please refresh to see results.": "Mise à jour de tous les soldes des canaux. Cela peut prendre un certain temps. Veuillez actualiser pour voir les résultats.",
"Upgrade Group": "Groupe de mise à niveau",
"Upgrade plaintext SMTP connection with STARTTLS before authentication": "Mettre à niveau la connexion SMTP en clair avec STARTTLS avant l'authentification",
"Upload": "Téléverser",
"Upload a single service account JSON file": "Télécharger un seul fichier JSON de compte de service",
"Upload file": "Téléverser un fichier",
......
......@@ -299,6 +299,7 @@
"Allow requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)": "プライベートIP範囲へのリクエストを許可 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)",
"Allow Retry": "リトライ許可",
"Allow safety_identifier passthrough": "SAFETY_IDENTIFIERパススルーを許可する",
"Allow self-signed or hostname-mismatched SMTP certificates": "自己署名またはホスト名が一致しない SMTP 証明書を許可する",
"Allow service_tier passthrough": "Service_tierパススルーを許可する",
"Allow speed passthrough": "speed パススルーを許可",
"Allow upstream callbacks": "アップストリームコールバックを許可",
......@@ -757,6 +758,7 @@
"Choose how the platform will operate": "プラットフォームの運用方法を選択",
"Choose how to filter domains": "ドメインをフィルタリングする方法を選択してください",
"Choose how to filter IP addresses": "IPアドレスをフィルタリングする方法を選択してください",
"Choose one SMTP transport security mode": "SMTP の転送暗号化方式を 1 つ選択してください",
"Choose the bundle type and define the items inside it.": "バンドルタイプを選択し、その中のアイテムを定義してください。",
"Choose the default charts, range, and time granularity for model analytics.": "モデル分析のデフォルトチャート、範囲、時間粒度を選択します。",
"Choose where to fetch upstream metadata.": "アップストリームのメタデータをどこからフェッチするかを選択してください。",
......@@ -1493,6 +1495,7 @@
"Enable selected models": "選択したモデルを有効にする",
"Enable SSL/TLS": "SSL/TLSを有効にする",
"Enable SSRF Protection": "SSRF保護を有効にする",
"Enable STARTTLS": "STARTTLSを有効にする",
"Enable streaming mode for the test request.": "テストリクエストのストリーミングモードを有効にします。",
"Enable Telegram OAuth": "Telegram OAuthを有効にする",
"Enable test mode for Creem payments": "Creem 決済のテストモードを有効にする",
......@@ -2806,6 +2809,7 @@
"Non-stream": "非ストリーミング",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "0 以外の招待報酬には、支払いゲートウェイ設定でのコンプライアンス確認が必要です。",
"None": "なし",
"No encryption": "暗号化なし",
"noreply@example.com": "noreply@example.com",
"Normalized:": "正規化:",
"Not available": "利用できません",
......@@ -3917,13 +3921,16 @@
"Size:": "サイズ:",
"sk_xxx or rk_xxx": "sk_xxx または rk_xxx",
"Skip retry on failure": "失敗時にリトライしない",
"Skip SMTP TLS certificate verification": "SMTP TLS証明書の検証をスキップ",
"Skip to Main": "メインコンテンツへスキップ",
"Slug": "スラッグ",
"Slug can only contain letters, numbers, hyphens, and underscores": "スラッグには英数字、ハイフン、アンダースコアのみ使用できます",
"Slug is required": "スラッグは必須です",
"Slug must be less than 100 characters": "スラッグは100文字以内にしてください",
"Smallest USD amount users can recharge (Epay)": "ユーザーがチャージできる最小USD金額 (Epay)",
"SSL/TLS": "SSL/TLS",
"SMTP Email": "SMTPメール",
"SMTP encryption": "SMTP 暗号化方式",
"SMTP Host": "SMTPホスト",
"smtp.example.com": "smtp.example.com",
"socks5://user:pass@host:port": "socks5://user:pass@host:port",
......@@ -3953,6 +3960,7 @@
"SSRF Protection": "SSRF保護",
"Standard": "標準",
"Standard price": "標準価格",
"STARTTLS": "STARTTLS",
"Start": "開始",
"Start a conversation to see messages here": "会話を開始すると、ここにメッセージが表示されます",
"Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.": "法人を設立せずに世界中で決済を受け付けられます。個人開発者、OPC 個人事業主、スタートアップ向けに設計されています。Waffo Pancake は Merchant of Record として、消費税、請求書、サブスクリプション管理、返金、チャージバックなど、グローバル決済のコンプライアンス負担を引き受けます。個人開発者はコンプライアンスではなくプロダクトに集中しながら素早くローンチできます。数分でオンボーディングし、1 つのプロンプトから完全な統合まで進められます。",
......@@ -4478,6 +4486,7 @@
"Updated user {{username}} (ID: {{id}})": "ユーザー {{username}} を更新しました(ID: {{id}})",
"Updating all channel balances. This may take a while. Please refresh to see results.": "すべてのチャネル残高を更新中です。これには少し時間がかかる場合があります。結果を確認するには更新してください。",
"Upgrade Group": "グループをアップグレード",
"Upgrade plaintext SMTP connection with STARTTLS before authentication": "認証前に STARTTLS で平文の SMTP 接続を暗号化する",
"Upload": "アップロード",
"Upload a single service account JSON file": "単一のサービスアカウントJSONファイルをアップロードする",
"Upload file": "ファイルをアップロード",
......
......@@ -299,6 +299,7 @@
"Allow requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)": "Разрешить запросы к частным диапазонам IP-адресов (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)",
"Allow Retry": "Разрешить повтор",
"Allow safety_identifier passthrough": "Разрешить сквозную передачу Safety_Identifier",
"Allow self-signed or hostname-mismatched SMTP certificates": "Разрешить самоподписанные SMTP-сертификаты или сертификаты с несоответствующим именем узла",
"Allow service_tier passthrough": "Разрешить сквозную передачу service_tier",
"Allow speed passthrough": "Разрешить передачу speed",
"Allow upstream callbacks": "Разрешить обратные вызовы upstream",
......@@ -757,6 +758,7 @@
"Choose how the platform will operate": "Выберите режим работы платформы",
"Choose how to filter domains": "Выберите, как фильтровать домены",
"Choose how to filter IP addresses": "Выберите, как фильтровать IP-адреса",
"Choose one SMTP transport security mode": "Выберите один из режимов защиты SMTP-транспорта",
"Choose the bundle type and define the items inside it.": "Выберите тип пакета и определите элементы внутри него.",
"Choose the default charts, range, and time granularity for model analytics.": "Выберите графики, диапазон и временную детализацию по умолчанию для аналитики моделей.",
"Choose where to fetch upstream metadata.": "Выберите, откуда получать метаданные вышестоящего источника.",
......@@ -1493,6 +1495,7 @@
"Enable selected models": "Включить выбранные модели",
"Enable SSL/TLS": "Включить SSL/TLS",
"Enable SSRF Protection": "Включить защиту от SSRF",
"Enable STARTTLS": "Включить STARTTLS",
"Enable streaming mode for the test request.": "Включить потоковый режим для тестового запроса.",
"Enable Telegram OAuth": "Включить Telegram OAuth",
"Enable test mode for Creem payments": "Включить тестовый режим для платежей Creem",
......@@ -2806,6 +2809,7 @@
"Non-stream": "Не потоковый",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Ненулевые награды за приглашения требуют подтверждения соответствия в настройках платежного шлюза.",
"None": "Нет",
"No encryption": "Без шифрования",
"noreply@example.com": "noreply@example.com",
"Normalized:": "Нормализовано:",
"Not available": "Недоступно",
......@@ -3917,13 +3921,16 @@
"Size:": "Размер:",
"sk_xxx or rk_xxx": "sk_xxx или rk_xxx",
"Skip retry on failure": "Не повторять при ошибке",
"Skip SMTP TLS certificate verification": "Пропустить проверку TLS-сертификата SMTP",
"Skip to Main": "Перейти к основному содержимому",
"Slug": "Идентификатор",
"Slug can only contain letters, numbers, hyphens, and underscores": "Slug может содержать только буквы, цифры, дефисы и подчёркивания",
"Slug is required": "Slug обязателен",
"Slug must be less than 100 characters": "Slug должен содержать менее 100 символов",
"Smallest USD amount users can recharge (Epay)": "Минимальная сумма в USD, которую пользователи могут пополнить (Epay)",
"SSL/TLS": "SSL/TLS",
"SMTP Email": "Электронная почта SMTP",
"SMTP encryption": "Шифрование SMTP",
"SMTP Host": "Хост SMTP",
"smtp.example.com": "smtp.example.com",
"socks5://user:pass@host:port": "socks5://user:pass@host:port",
......@@ -3953,6 +3960,7 @@
"SSRF Protection": "Защита от SSRF",
"Standard": "Стандартный",
"Standard price": "Стандартная цена",
"STARTTLS": "STARTTLS",
"Start": "Начало",
"Start a conversation to see messages here": "Начните разговор, чтобы увидеть сообщения здесь",
"Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.": "Начните принимать платежи по всему миру без регистрации компании. Подходит для независимых разработчиков, индивидуальных предпринимателей OPC и стартапов. Waffo Pancake выступает как Merchant of Record и берет на себя комплаенс глобального приема платежей: потребительские налоги, выставление счетов, управление подписками, возвраты и чарджбеки. Одиночные разработчики могут быстро запуститься и сосредоточиться на продукте, а не на комплаенсе. Подключение за минуты — от одного запроса до полной интеграции.",
......@@ -4478,6 +4486,7 @@
"Updated user {{username}} (ID: {{id}})": "Обновлён пользователь {{username}} (ID: {{id}})",
"Updating all channel balances. This may take a while. Please refresh to see results.": "Обновление балансов всех каналов. Это может занять некоторое время. Пожалуйста, обновите страницу, чтобы увидеть результаты.",
"Upgrade Group": "Повысить группу",
"Upgrade plaintext SMTP connection with STARTTLS before authentication": "Перед аутентификацией повысить открытое SMTP-соединение до STARTTLS",
"Upload": "Загрузка",
"Upload a single service account JSON file": "Загрузите JSON-файл одного сервисного аккаунта",
"Upload file": "Загрузить файл",
......
......@@ -299,6 +299,7 @@
"Allow requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)": "Cho phép các yêu cầu đến các dải IP riêng (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)",
"Allow Retry": "Cho phép thử lại",
"Allow safety_identifier passthrough": "Cho phép chuyển tiếp safety_identifier",
"Allow self-signed or hostname-mismatched SMTP certificates": "Cho phép chứng chỉ SMTP tự ký hoặc không khớp tên máy chủ",
"Allow service_tier passthrough": "Cho phép chuyển tiếp service_tier",
"Allow speed passthrough": "Cho phép truyền speed",
"Allow upstream callbacks": "Cho phép callback upstream",
......@@ -757,6 +758,7 @@
"Choose how the platform will operate": "Chọn cách nền tảng sẽ hoạt động",
"Choose how to filter domains": "Chọn cách lọc tên miền",
"Choose how to filter IP addresses": "Chọn cách lọc địa chỉ IP",
"Choose one SMTP transport security mode": "Chọn một chế độ bảo mật truyền tải SMTP",
"Choose the bundle type and define the items inside it.": "Chọn loại gói và định nghĩa các mục bên trong nó.",
"Choose the default charts, range, and time granularity for model analytics.": "Chọn biểu đồ, khoảng thời gian và độ chi tiết thời gian mặc định cho phân tích mô hình.",
"Choose where to fetch upstream metadata.": "Chọn nơi để tìm nạp siêu dữ liệu thượng nguồn.",
......@@ -1493,6 +1495,7 @@
"Enable selected models": "Kích hoạt các mô hình đã chọn",
"Enable SSL/TLS": "Bật SSL/TLS",
"Enable SSRF Protection": "Kích hoạt Bảo vệ SSRF",
"Enable STARTTLS": "Bật STARTTLS",
"Enable streaming mode for the test request.": "Bật chế độ streaming cho yêu cầu thử nghiệm.",
"Enable Telegram OAuth": "Bật Telegram OAuth",
"Enable test mode for Creem payments": "Bật chế độ thử nghiệm cho thanh toán Creem",
......@@ -2806,6 +2809,7 @@
"Non-stream": "Không phát trực tuyến",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "Phần thưởng mời khác 0 yêu cầu xác nhận tuân thủ trong cài đặt Cổng thanh toán.",
"None": "Không có",
"No encryption": "Không mã hóa",
"noreply@example.com": "noreply@example.com",
"Normalized:": "Chuẩn hóa:",
"Not available": "Không khả dụng",
......@@ -3917,13 +3921,16 @@
"Size:": "Kích thước:",
"sk_xxx or rk_xxx": "sk_xxx hoặc rk_xxx",
"Skip retry on failure": "Không thử lại khi thất bại",
"Skip SMTP TLS certificate verification": "Bỏ qua xác minh chứng chỉ TLS SMTP",
"Skip to Main": "Bỏ qua đến nội dung chính",
"Slug": "Slug",
"Slug can only contain letters, numbers, hyphens, and underscores": "Slug chỉ có thể chứa chữ cái, số, dấu gạch ngang và dấu gạch dưới",
"Slug is required": "Slug là bắt buộc",
"Slug must be less than 100 characters": "Slug phải ít hơn 100 ký tự",
"Smallest USD amount users can recharge (Epay)": "Số tiền USD tối thiểu người dùng có thể nạp (Epay)",
"SSL/TLS": "SSL/TLS",
"SMTP Email": "Email SMTP",
"SMTP encryption": "Mã hóa SMTP",
"SMTP Host": "Máy chủ SMTP",
"smtp.example.com": "smtp.example.com",
"socks5://user:pass@host:port": "socks5://user:pass@host:port",
......@@ -3953,6 +3960,7 @@
"SSRF Protection": "Bảo vệ SSRF",
"Standard": "Tiêu chuẩn",
"Standard price": "Giá tiêu chuẩn",
"STARTTLS": "STARTTLS",
"Start": "Bắt đầu",
"Start a conversation to see messages here": "Bắt đầu một cuộc trò chuyện để xem tin nhắn tại đây",
"Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.": "Bắt đầu thu thanh toán toàn cầu mà không cần đăng ký công ty. Dành cho lập trình viên độc lập, chủ sở hữu OPC và startup. Waffo Pancake đóng vai trò Merchant of Record, chịu trách nhiệm tuân thủ cho việc thu thanh toán toàn cầu — thuế tiêu dùng, hóa đơn, quản lý đăng ký, hoàn tiền và tranh chấp thanh toán. Lập trình viên cá nhân có thể ra mắt nhanh và tập trung vào sản phẩm thay vì tuân thủ. Onboard trong vài phút — từ một prompt đến tích hợp hoàn chỉnh.",
......@@ -4478,6 +4486,7 @@
"Updated user {{username}} (ID: {{id}})": "Đã cập nhật người dùng {{username}} (ID: {{id}})",
"Updating all channel balances. This may take a while. Please refresh to see results.": "Đang cập nhật tất cả số dư kênh. Quá trình này có thể mất một chút thời gian. Vui lòng làm mới để xem kết quả.",
"Upgrade Group": "Nhóm nâng cấp",
"Upgrade plaintext SMTP connection with STARTTLS before authentication": "Nâng cấp kết nối SMTP dạng rõ bằng STARTTLS trước khi xác thực",
"Upload": "Tải lên",
"Upload a single service account JSON file": "Tải lên một tệp JSON tài khoản dịch vụ",
"Upload file": "Tải tệp lên",
......
......@@ -299,6 +299,7 @@
"Allow requests to private IP ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)": "允许请求私有 IP 范围 (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)",
"Allow Retry": "允许重试",
"Allow safety_identifier passthrough": "允许透传 safety_identifier",
"Allow self-signed or hostname-mismatched SMTP certificates": "允许自签名或主机名不匹配的 SMTP 证书",
"Allow service_tier passthrough": "允许透传 service_tier",
"Allow speed passthrough": "允许 speed 透传",
"Allow upstream callbacks": "允许上游回调",
......@@ -757,6 +758,7 @@
"Choose how the platform will operate": "选择平台的运行模式",
"Choose how to filter domains": "选择如何过滤域名",
"Choose how to filter IP addresses": "选择如何过滤 IP 地址",
"Choose one SMTP transport security mode": "选择一种 SMTP 传输加密方式",
"Choose the bundle type and define the items inside it.": "选择捆绑包类型并定义其中的项目。",
"Choose the default charts, range, and time granularity for model analytics.": "选择模型调用分析的默认图表、范围和时间粒度。",
"Choose where to fetch upstream metadata.": "选择从何处获取上游元数据。",
......@@ -1493,6 +1495,7 @@
"Enable selected models": "启用选定的模型",
"Enable SSL/TLS": "启用 SSL/TLS",
"Enable SSRF Protection": "启用 SSRF 保护",
"Enable STARTTLS": "启用 STARTTLS",
"Enable streaming mode for the test request.": "为测试请求启用流式模式。",
"Enable Telegram OAuth": "启用 Telegram OAuth",
"Enable test mode for Creem payments": "启用 Creem 支付测试模式",
......@@ -2806,6 +2809,7 @@
"Non-stream": "非流式",
"Non-zero invitation rewards require compliance confirmation in Payment Gateway settings.": "非零邀请奖励需要先在支付网关设置中确认合规条款。",
"None": "无",
"No encryption": "无加密",
"noreply@example.com": "noreply@example.com",
"Normalized:": "已归一化:",
"Not available": "不可用",
......@@ -3917,13 +3921,16 @@
"Size:": "大小:",
"sk_xxx or rk_xxx": "sk_xxx 或 rk_xxx",
"Skip retry on failure": "失败后不重试",
"Skip SMTP TLS certificate verification": "跳过 SMTP TLS 证书验证",
"Skip to Main": "跳到主内容",
"Slug": "标识符",
"Slug can only contain letters, numbers, hyphens, and underscores": "Slug 只能包含字母、数字、连字符和下划线",
"Slug is required": "Slug 不能为空",
"Slug must be less than 100 characters": "Slug 不能超过 100 个字符",
"Smallest USD amount users can recharge (Epay)": "用户可以充值的最小美元金额 (Epay)",
"SSL/TLS": "SSL/TLS",
"SMTP Email": "SMTP 邮箱",
"SMTP encryption": "SMTP 加密方式",
"SMTP Host": "SMTP 主机",
"smtp.example.com": "smtp.example.com",
"socks5://user:pass@host:port": "socks5://user:pass@host:port",
......@@ -3953,6 +3960,7 @@
"SSRF Protection": "SSRF 保护",
"Standard": "标准",
"Standard price": "标准价格",
"STARTTLS": "STARTTLS",
"Start": "开始",
"Start a conversation to see messages here": "开始对话以在此处查看消息",
"Start collecting payments globally without registering a company. Built for indie developers, OPC sole proprietorships, and startups. Waffo Pancake acts as your Merchant of Record, taking on the compliance burden of global payment collection — consumption tax, invoicing, subscription management, refunds, and chargebacks. Solo developers can launch fast and stay focused on product instead of compliance. Onboard in minutes — one prompt to a full integration.": "无需注册公司即可开始全球收款。面向独立开发者、OPC 个体经营者和初创团队构建。Waffo Pancake 作为你的登记商户(Merchant of Record),承担全球收款相关的合规负担,包括消费税、开票、订阅管理、退款和拒付。个人开发者可以快速上线,专注产品而不是合规事务。几分钟即可完成入驻,从一个提示词到完整集成。",
......@@ -4478,6 +4486,7 @@
"Updated user {{username}} (ID: {{id}})": "更新用户 {{username}}(ID: {{id}})",
"Updating all channel balances. This may take a while. Please refresh to see results.": "正在更新所有渠道余额。这可能需要一段时间。请刷新以查看结果。",
"Upgrade Group": "升级分组",
"Upgrade plaintext SMTP connection with STARTTLS before authentication": "在身份验证前使用 STARTTLS 升级明文 SMTP 连接",
"Upload": "上传",
"Upload a single service account JSON file": "上传单个服务账号 JSON 文件",
"Upload file": "上传文件",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment