From 2f23a66733dbddc26de88b7b08af5f2e550aee34 Mon Sep 17 00:00:00 2001 From: Benson Yan Date: Wed, 24 Jun 2026 12:45:39 +0800 Subject: [PATCH] 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 --- common/constants.go | 2 + common/email.go | 112 ++-- common/email_ntlm_auth.go | 83 +++ common/email_test.go | 531 ++++++++++++++++++ common/init.go | 2 + go.mod | 2 + go.sum | 2 + model/option.go | 8 +- .../src/components/settings/SystemSetting.jsx | 64 ++- web/classic/src/i18n/locales/en.json | 5 + web/classic/src/i18n/locales/fr.json | 5 + web/classic/src/i18n/locales/ja.json | 5 + web/classic/src/i18n/locales/ru.json | 5 + web/classic/src/i18n/locales/vi.json | 5 + web/classic/src/i18n/locales/zh-CN.json | 5 + web/classic/src/i18n/locales/zh-TW.json | 5 + web/classic/src/i18n/locales/zh.json | 5 + .../integrations/email-settings-section.tsx | 105 +++- .../system-settings/operations/index.tsx | 2 + .../operations/section-registry.tsx | 2 + .../src/features/system-settings/types.ts | 2 + web/default/src/i18n/locales/en.json | 9 + web/default/src/i18n/locales/fr.json | 9 + web/default/src/i18n/locales/ja.json | 9 + web/default/src/i18n/locales/ru.json | 9 + web/default/src/i18n/locales/vi.json | 9 + web/default/src/i18n/locales/zh.json | 9 + 27 files changed, 959 insertions(+), 52 deletions(-) create mode 100644 common/email_ntlm_auth.go create mode 100644 common/email_test.go diff --git a/common/constants.go b/common/constants.go index b0386178..469237f0 100644 --- a/common/constants.go +++ b/common/constants.go @@ -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 = "" diff --git a/common/email.go b/common/email.go index c43ddec9..6ee26c0c 100644 --- a/common/email.go +++ b/common/email.go @@ -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. } - return smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer) +} + +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 + } + + 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 { - return err - } - } else { - err = smtp.SendMail(addr, auth, SMTPFrom, to, mail) } + if err = client.Mail(SMTPFrom); err != nil { + return err + } + for _, receiver := range to { + 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 { + return err + } + err = client.Quit() if err != nil { SysError(fmt.Sprintf("failed to send email to %s: %v", receiver, err)) } diff --git a/common/email_ntlm_auth.go b/common/email_ntlm_auth.go new file mode 100644 index 00000000..59e2d40c --- /dev/null +++ b/common/email_ntlm_auth.go @@ -0,0 +1,83 @@ +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 +} diff --git a/common/email_test.go b/common/email_test.go new file mode 100644 index 00000000..01c01cdd --- /dev/null +++ b/common/email_test.go @@ -0,0 +1,531 @@ +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 ."); 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", "

123456

") + require.NoError(t, err) + + select { + case message := <-server.messages: + require.Contains(t, message, "Subject: =?UTF-8?B?") + require.Contains(t, message, "

123456

") + 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", "

123456

") + 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", "

123456

") + 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, "

123456

") + 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", "

123456

") + 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, "

123456

") + 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", "

123456

") + 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, "

123456

") + 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", "

123456

") + 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", "

123456

") + 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", "

123456

") + require.Error(t, err) + require.Contains(t, fmt.Sprint(err), "certificate") +} diff --git a/common/init.go b/common/init.go index f67c38ee..3a178c65 100644 --- a/common/init.go +++ b/common/init.go @@ -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")) diff --git a/go.mod b/go.mod index 81f43db7..411781aa 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 0e3e8c6b..4a40fe5c 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/model/option.go b/model/option.go index ed1af72e..8e8587f2 100644 --- a/model/option.go +++ b/model/option.go @@ -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": diff --git a/web/classic/src/components/settings/SystemSetting.jsx b/web/classic/src/components/settings/SystemSetting.jsx index 63b20c70..de0962c1 100644 --- a/web/classic/src/components/settings/SystemSetting.jsx +++ b/web/classic/src/components/settings/SystemSetting.jsx @@ -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 = () => { /> - - handleCheckboxChange('SMTPSSLEnabled', e) + {t('SMTP 加密方式')} + - {t('启用SMTP SSL')} - + {t('无加密')} + {t('SSL/TLS')} + {t('STARTTLS')} + + + {t('请选择一种 SMTP 传输加密方式')} + 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({ )} /> + + {t('SMTP encryption')} + + { + const mode = value as SmtpSecurityMode + form.setValue('SMTPSSLEnabled', mode === 'ssl_tls', { + shouldDirty: true, + }) + form.setValue('SMTPStartTLSEnabled', mode === 'starttls', { + shouldDirty: true, + }) + }} + className='gap-3' + > +
+ + +
+
+ + +
+
+ + +
+
+
+ + {t('Choose one SMTP transport security mode')} + +
+ ( - {t('Enable SSL/TLS')} + + {t('Skip SMTP TLS certificate verification')} + - {t('Use secure connection when sending emails')} + {t( + 'Allow self-signed or hostname-mismatched SMTP certificates' + )} diff --git a/web/default/src/features/system-settings/operations/index.tsx b/web/default/src/features/system-settings/operations/index.tsx index 859e3ccc..acc9f678 100644 --- a/web/default/src/features/system-settings/operations/index.tsx +++ b/web/default/src/features/system-settings/operations/index.tsx @@ -36,6 +36,8 @@ const defaultOperationsSettings: OperationsSettings = { SMTPFrom: '', SMTPToken: '', SMTPSSLEnabled: false, + SMTPStartTLSEnabled: false, + SMTPInsecureSkipVerify: false, SMTPForceAuthLogin: false, WorkerUrl: '', WorkerValidKey: '', diff --git a/web/default/src/features/system-settings/operations/section-registry.tsx b/web/default/src/features/system-settings/operations/section-registry.tsx index a8adeeb1..f9473a40 100644 --- a/web/default/src/features/system-settings/operations/section-registry.tsx +++ b/web/default/src/features/system-settings/operations/section-registry.tsx @@ -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, }} /> diff --git a/web/default/src/features/system-settings/types.ts b/web/default/src/features/system-settings/types.ts index abcb97d0..6d3bdd3e 100644 --- a/web/default/src/features/system-settings/types.ts +++ b/web/default/src/features/system-settings/types.ts @@ -334,6 +334,8 @@ export type OperationsSettings = { SMTPFrom: string SMTPToken: string SMTPSSLEnabled: boolean + SMTPStartTLSEnabled: boolean + SMTPInsecureSkipVerify: boolean SMTPForceAuthLogin: boolean WorkerUrl: string WorkerValidKey: string diff --git a/web/default/src/i18n/locales/en.json b/web/default/src/i18n/locales/en.json index c1c0e9ca..533c8443 100644 --- a/web/default/src/i18n/locales/en.json +++ b/web/default/src/i18n/locales/en.json @@ -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", diff --git a/web/default/src/i18n/locales/fr.json b/web/default/src/i18n/locales/fr.json index 5c096ca7..ee862042 100644 --- a/web/default/src/i18n/locales/fr.json +++ b/web/default/src/i18n/locales/fr.json @@ -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", diff --git a/web/default/src/i18n/locales/ja.json b/web/default/src/i18n/locales/ja.json index 7e694fe0..148fcac7 100644 --- a/web/default/src/i18n/locales/ja.json +++ b/web/default/src/i18n/locales/ja.json @@ -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": "ファイルをアップロード", diff --git a/web/default/src/i18n/locales/ru.json b/web/default/src/i18n/locales/ru.json index ee6839b9..10f00973 100644 --- a/web/default/src/i18n/locales/ru.json +++ b/web/default/src/i18n/locales/ru.json @@ -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": "Загрузить файл", diff --git a/web/default/src/i18n/locales/vi.json b/web/default/src/i18n/locales/vi.json index c1791f29..9a9d43b1 100644 --- a/web/default/src/i18n/locales/vi.json +++ b/web/default/src/i18n/locales/vi.json @@ -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", diff --git a/web/default/src/i18n/locales/zh.json b/web/default/src/i18n/locales/zh.json index 30b92653..558834a0 100644 --- a/web/default/src/i18n/locales/zh.json +++ b/web/default/src/i18n/locales/zh.json @@ -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": "上传文件",