diff --git a/internal/web/controller/setting.go b/internal/web/controller/setting.go index 17bd2ac..a2cebf5 100644 --- a/internal/web/controller/setting.go +++ b/internal/web/controller/setting.go @@ -11,7 +11,6 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/web/entity" "github.com/mhsanaei/3x-ui/v3/internal/web/middleware" "github.com/mhsanaei/3x-ui/v3/internal/web/service" - "github.com/mhsanaei/3x-ui/v3/internal/web/service/email" "github.com/mhsanaei/3x-ui/v3/internal/web/service/panel" "github.com/mhsanaei/3x-ui/v3/internal/web/session" @@ -32,8 +31,7 @@ type updateUserForm struct { // see #5724). type updateSettingForm struct { entity.AllSetting - ClearTgBotToken bool `json:"clearTgBotToken" form:"clearTgBotToken"` - ClearSmtpPassword bool `json:"clearSmtpPassword" form:"clearSmtpPassword"` + ClearTgBotToken bool `json:"clearTgBotToken" form:"clearTgBotToken"` } type validateRegexForm struct { @@ -72,7 +70,6 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) { g.POST("/apiTokens/create", a.createApiToken) g.POST("/apiTokens/delete/:id", a.deleteApiToken) g.POST("/apiTokens/setEnabled/:id", a.setApiTokenEnabled) - g.POST("/testSmtp", a.testSmtp) g.POST("/testTgBot", a.testTgBot) } @@ -127,8 +124,7 @@ func (a *SettingController) updateSetting(c *gin.Context) { oldTgChatId, _ := a.settingService.GetTgBotChatId() oldTgAPIServer, _ := a.settingService.GetTgBotAPIServer() err := a.settingService.UpdateAllSetting(allSetting, service.SecretClears{ - TgBotToken: form.ClearTgBotToken, - SmtpPassword: form.ClearSmtpPassword, + TgBotToken: form.ClearTgBotToken, }) if err == nil && form.PanelOutbound != oldPanelOutbound { // The egress bridge lives in the generated config; reconcile the @@ -261,30 +257,6 @@ func (a *SettingController) setApiTokenEnabled(c *gin.Context) { jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), a.apiTokenService.SetEnabledExpectedScope(id, form.ExpectedScope, form.Enabled)) } -func (a *SettingController) testSmtp(c *gin.Context) { - if emailService == nil { - jsonMsg(c, I18nWeb(c, "pages.settings.smtpNotInitialized"), errors.New("email service not available")) - return - } - logger.Info("SMTP test: starting...") - result := emailService.TestConnection() - if !result.Success { - logger.Warning("SMTP test failed at", result.Stage+":", result.Message) - c.JSON(200, gin.H{ - "success": false, - "stage": result.Stage, - "msg": result.Message, - }) - return - } - logger.Info("SMTP test: success") - c.JSON(200, gin.H{ - "success": true, - "stage": result.Stage, - "msg": result.Message, - }) -} - func (a *SettingController) testTgBot(c *gin.Context) { enabled, err := a.settingService.GetTgbotEnabled() if err != nil || !enabled { @@ -314,9 +286,3 @@ func SetTestTgFunc(fn func() error) { testTgFunc = fn } var reloadTgbotFunc func() func SetReloadTgbotFunc(fn func()) { reloadTgbotFunc = fn } - -// emailService is set from web layer. -var emailService *email.EmailService - -// SetEmailService registers the email service for test endpoints. -func SetEmailService(s *email.EmailService) { emailService = s } diff --git a/internal/web/entity/entity.go b/internal/web/entity/entity.go index 489b2a3..8baaf56 100644 --- a/internal/web/entity/entity.go +++ b/internal/web/entity/entity.go @@ -4,7 +4,6 @@ import ( "crypto/tls" "math" "net" - "net/mail" "net/netip" "strings" "time" @@ -49,19 +48,6 @@ type AllSetting struct { TgLang string `json:"tgLang" form:"tgLang"` TgEnabledEvents string `json:"tgEnabledEvents" form:"tgEnabledEvents"` - SmtpEnable bool `json:"smtpEnable" form:"smtpEnable"` - SmtpHost string `json:"smtpHost" form:"smtpHost"` - SmtpPort int `json:"smtpPort" form:"smtpPort" validate:"gte=1,lte=65535"` - SmtpUsername string `json:"smtpUsername" form:"smtpUsername"` - SmtpPassword string `json:"smtpPassword" form:"smtpPassword"` - SmtpFrom string `json:"smtpFrom" form:"smtpFrom"` - SmtpFromName string `json:"smtpFromName" form:"smtpFromName"` - SmtpTo string `json:"smtpTo" form:"smtpTo"` - SmtpEncryptionType string `json:"smtpEncryptionType" form:"smtpEncryptionType"` - SmtpEnabledEvents string `json:"smtpEnabledEvents" form:"smtpEnabledEvents"` - SmtpCpu int `json:"smtpCpu" form:"smtpCpu" validate:"gte=0,lte=100"` - SmtpMemory int `json:"smtpMemory" form:"smtpMemory" validate:"gte=0,lte=100"` - OutboundDownThreshold int `json:"outboundDownThreshold" form:"outboundDownThreshold" validate:"gte=1,lte=100"` TimeLocation string `json:"timeLocation" form:"timeLocation"` @@ -111,11 +97,10 @@ type AllSetting struct { type AllSettingView struct { AllSetting - HasTgBotToken bool `json:"hasTgBotToken"` - HasApiToken bool `json:"hasApiToken"` - HasWarpSecret bool `json:"hasWarpSecret"` - HasNordSecret bool `json:"hasNordSecret"` - HasSmtpPassword bool `json:"hasSmtpPassword"` + HasTgBotToken bool `json:"hasTgBotToken"` + HasApiToken bool `json:"hasApiToken"` + HasWarpSecret bool `json:"hasWarpSecret"` + HasNordSecret bool `json:"hasNordSecret"` } func pathHasForbiddenChar(s string) bool { @@ -260,12 +245,6 @@ func (s *AllSetting) CheckValid() error { return common.NewError("time location not exist:", s.TimeLocation) } - if s.SmtpFrom != "" { - if _, err := mail.ParseAddress(s.SmtpFrom); err != nil { - return common.NewError("SMTP from address is not valid:", s.SmtpFrom) - } - } - return nil } diff --git a/internal/web/service/email/email.go b/internal/web/service/email/email.go deleted file mode 100644 index 5bcbdab..0000000 --- a/internal/web/service/email/email.go +++ /dev/null @@ -1,408 +0,0 @@ -package email - -import ( - "context" - "crypto/rand" - "crypto/tls" - "encoding/hex" - "fmt" - "mime" - "net" - "net/mail" - "net/smtp" - "strings" - "time" - - "github.com/mhsanaei/3x-ui/v3/internal/web/service" -) - -// EmailService sends email notifications via SMTP. -type EmailService struct { - settingService service.SettingService -} - -// SMTPTestResult holds the result of an SMTP connection test. -type SMTPTestResult struct { - Success bool `json:"success"` - Stage string `json:"stage"` // "connect" | "auth" | "send" - Message string `json:"message"` // classified error message -} - -// NewEmailService creates a new EmailService. -func NewEmailService(settingService service.SettingService) *EmailService { - return &EmailService{settingService: settingService} -} - -// smtpConnectTimeout bounds the TCP dial. smtpDeadline bounds every SMTP -// protocol step after the connection is up, so a server that accepts the socket -// but then stalls cannot block the sender goroutine (and leak its socket) long -// after the caller's own timeout has already fired. smtpDeadline is a var only -// so tests can shorten it. -const smtpConnectTimeout = 10 * time.Second - -var smtpDeadline = 30 * time.Second - -// Send sends an HTML email to all configured recipients. -func (s *EmailService) Send(subject, body string) error { - host, err := s.settingService.GetSmtpHost() - if err != nil || host == "" { - return fmt.Errorf("smtp host not configured") - } - port, err := s.settingService.GetSmtpPort() - if err != nil || port <= 0 { - port = 587 - } - username, _ := s.settingService.GetSmtpUsername() - password, _ := s.settingService.GetSmtpPassword() - fromAddr, _ := s.settingService.GetSmtpFrom() - fromName, _ := s.settingService.GetSmtpFromName() - toStr, _ := s.settingService.GetSmtpTo() - encryptionType, _ := s.settingService.GetSmtpEncryptionType() - - from := fromAddr - if from == "" { - from = username - } - if from == "" { - return fmt.Errorf("smtp from not configured") - } - from, fromName = resolveFrom(from, fromName) - - recipients := parseRecipients(toStr) - if len(recipients) == 0 { - return fmt.Errorf("no recipients configured") - } - - addr := net.JoinHostPort(host, fmt.Sprintf("%d", port)) - msg := buildMessage(from, fromName, recipients, subject, body) - - // Authenticate only when credentials are set. Go's PlainAuth refuses to run - // over the unencrypted "none" transport, so an open relay must use nil auth. - var auth smtp.Auth - if username != "" && password != "" { - auth = smtp.PlainAuth("", username, password, host) - } - - // Wrap in a channel with timeout to prevent indefinite blocking - type result struct{ err error } - ch := make(chan result, 1) - go func() { - switch encryptionType { - case "tls": - ch <- result{s.sendWithTLS(addr, auth, from, recipients, msg, host)} - case "starttls", "none": - ch <- result{s.sendPlain(addr, auth, from, recipients, msg, host)} - default: - ch <- result{fmt.Errorf("unknown SMTP encryption type: %s", encryptionType)} - } - }() - - select { - case r := <-ch: - return r.err - case <-time.After(30 * time.Second): - return fmt.Errorf("smtp connection timed out after 30s") - } -} - -// TestConnection tests SMTP connection stage by stage and sends a test email. -func (s *EmailService) TestConnection() SMTPTestResult { - host, err := s.settingService.GetSmtpHost() - if err != nil || host == "" { - return SMTPTestResult{false, "connect", "smtpHostNotConfigured"} - } - port, err := s.settingService.GetSmtpPort() - if err != nil || port <= 0 { - port = 587 - } - username, _ := s.settingService.GetSmtpUsername() - password, _ := s.settingService.GetSmtpPassword() - fromAddr, _ := s.settingService.GetSmtpFrom() - fromName, _ := s.settingService.GetSmtpFromName() - toStr, _ := s.settingService.GetSmtpTo() - encryptionType, _ := s.settingService.GetSmtpEncryptionType() - - from := fromAddr - if from == "" { - from = username - } - if from == "" { - return SMTPTestResult{false, "send", "smtpFromNotConfigured"} - } - from, fromName = resolveFrom(from, fromName) - - recipients := parseRecipients(toStr) - if len(recipients) == 0 { - return SMTPTestResult{false, "send", "smtpNoRecipients"} - } - - addr := net.JoinHostPort(host, fmt.Sprintf("%d", port)) - - // Stage 1: Connect - var conn net.Conn - dialer := &net.Dialer{Timeout: 5 * time.Second} - - switch encryptionType { - case "tls": - conn, err = (&tls.Dialer{NetDialer: dialer, Config: &tls.Config{ - ServerName: host, - InsecureSkipVerify: false, - }}).DialContext(context.Background(), "tcp", addr) - default: - conn, err = dialer.Dial("tcp", addr) - } - - if err != nil { - return SMTPTestResult{false, "connect", classifySMTPError(err)} - } - defer conn.Close() - _ = conn.SetDeadline(time.Now().Add(smtpDeadline)) - - // Stage 2: Handshake + Auth - client, err := smtp.NewClient(conn, host) - if err != nil { - return SMTPTestResult{false, "auth", classifySMTPError(err)} - } - defer client.Close() - - if err = client.Hello("localhost"); err != nil { - return SMTPTestResult{false, "auth", classifySMTPError(err)} - } - - // STARTTLS upgrade for non-TLS connections - if encryptionType == "starttls" { - if ok, _ := client.Extension("STARTTLS"); ok { - if err = client.StartTLS(&tls.Config{ServerName: host}); err != nil { - return SMTPTestResult{false, "auth", classifySMTPError(err)} - } - } - } - - if username != "" && password != "" { - auth := smtp.PlainAuth("", username, password, host) - if err = client.Auth(auth); err != nil { - return SMTPTestResult{false, "auth", classifySMTPError(err)} - } - } - - // Stage 3: Send test email - if err = client.Mail(from); err != nil { - return SMTPTestResult{false, "send", classifySMTPError(err)} - } - for _, r := range recipients { - if err = client.Rcpt(r); err != nil { - return SMTPTestResult{false, "send", classifySMTPError(err)} - } - } - - msg := buildMessage(from, fromName, recipients, "[3x-ui] Test email", - `
-If you received this, SMTP is configured correctly.
-`) - - w, err := client.Data() - if err != nil { - return SMTPTestResult{false, "send", classifySMTPError(err)} - } - if _, err = w.Write(msg); err != nil { - return SMTPTestResult{false, "send", classifySMTPError(err)} - } - if err = w.Close(); err != nil { - return SMTPTestResult{false, "send", classifySMTPError(err)} - } - - return SMTPTestResult{true, "send", "smtpTestSuccess"} -} - -func (s *EmailService) sendWithTLS(addr string, auth smtp.Auth, from string, to []string, msg []byte, host string) error { - // Dial with explicit timeout - dialer := &net.Dialer{Timeout: smtpConnectTimeout} - conn, err := (&tls.Dialer{NetDialer: dialer, Config: &tls.Config{ - ServerName: host, - InsecureSkipVerify: false, - }}).DialContext(context.Background(), "tcp", addr) - if err != nil { - return err - } - defer conn.Close() - _ = conn.SetDeadline(time.Now().Add(smtpDeadline)) - - client, err := smtp.NewClient(conn, host) - if err != nil { - return err - } - defer client.Close() - - if err = client.Hello("localhost"); err != nil { - return err - } - if auth != nil { - if err = client.Auth(auth); err != nil { - return err - } - } - if err = client.Mail(from); err != nil { - return err - } - for _, r := range to { - if err = client.Rcpt(r); err != nil { - return err - } - } - w, err := client.Data() - if err != nil { - return err - } - if _, err = w.Write(msg); err != nil { - return err - } - return w.Close() -} - -// sendPlain delivers over a plain TCP connection, opportunistically upgrading -// via STARTTLS when the server advertises it (the behavior net/smtp.SendMail -// gives the "starttls" and "none" transports). Unlike SendMail it dials with a -// timeout and arms a connection deadline, so a server that never speaks or -// stalls mid-protocol cannot block the sender goroutine past smtpDeadline. -func (s *EmailService) sendPlain(addr string, auth smtp.Auth, from string, to []string, msg []byte, host string) error { - conn, err := (&net.Dialer{Timeout: smtpConnectTimeout}).Dial("tcp", addr) - if err != nil { - return err - } - defer conn.Close() - _ = conn.SetDeadline(time.Now().Add(smtpDeadline)) - - client, err := smtp.NewClient(conn, host) - if err != nil { - return err - } - defer client.Close() - - if err = client.Hello("localhost"); err != nil { - return err - } - if ok, _ := client.Extension("STARTTLS"); ok { - if err = client.StartTLS(&tls.Config{ServerName: host}); err != nil { - return err - } - } - if auth != nil { - if err = client.Auth(auth); err != nil { - return err - } - } - if err = client.Mail(from); err != nil { - return err - } - for _, r := range to { - if err = client.Rcpt(r); err != nil { - return err - } - } - w, err := client.Data() - if err != nil { - return err - } - if _, err = w.Write(msg); err != nil { - return err - } - return w.Close() -} - -// SendTest sends a test email and returns any error with detail. -func (s *EmailService) SendTest() error { - return s.Send( - "[3x-ui] Test email", - ` -If you received this, SMTP is configured correctly.
-`, - ) -} - -// classifySMTPError maps raw SMTP errors to human-readable messages. -func classifySMTPError(err error) string { - msg := err.Error() - msgLower := strings.ToLower(msg) - - switch { - case strings.Contains(msg, "535") || strings.Contains(msgLower, "authentication"): - return "pages.settings.smtpErrorAuth" - case strings.Contains(msg, "534") || strings.Contains(msgLower, "starttls"): - return "pages.settings.smtpErrorStarttls" - case strings.Contains(msg, "465") || strings.Contains(msgLower, "tls"): - return "pages.settings.smtpErrorTls" - case strings.Contains(msgLower, "connection refused") || strings.Contains(msgLower, "dial"): - return "pages.settings.smtpErrorRefused" - case strings.Contains(msgLower, "timeout"): - return "pages.settings.smtpErrorTimeout" - case strings.Contains(msg, "550") || strings.Contains(msgLower, "relay"): - return "pages.settings.smtpErrorRelay" - case strings.Contains(msgLower, "eof"): - return "pages.settings.smtpErrorEof" - default: - return fmt.Sprintf("pages.settings.smtpErrorUnknown: %s", msg) - } -} - -func parseRecipients(toStr string) []string { - if toStr == "" { - return nil - } - var out []string - for s := range strings.SplitSeq(toStr, ",") { - s = strings.TrimSpace(s) - if s != "" { - out = append(out, s) - } - } - return out -} - -// buildMessage assembles an RFC 5322 message. It emits the two mandatory -// header fields (Date, From) plus Message-ID, so strict receivers such as Gmail -// accept it and spam filters do not penalize a missing date or message id. The -// From header is a proper name-addr ("Name"%s
-`, host, title, content, i18n("tgbot.messages.time", "Time=="+ts)) - } - - kv := func(key, val string) string { - return fmt.Sprintf("%s: %s
", key, val) - } - - switch e.Type { - case eventbus.EventOutboundDown: - subject = host + " " + i18n("tgbot.messages.eventOutboundDown", "Tag=="+e.Source) - content := kv(i18n("email.labelStatus"), ``+i18n("email.statusDown")+``) - content += kv(i18n("email.labelOutbound"), e.Source) - if data, ok := e.Data.(*eventbus.OutboundHealthData); ok { - if data.Error != "" { - content += kv(i18n("email.labelError"), data.Error) - } - if data.Delay > 0 { - content += kv(i18n("email.labelDelay"), fmt.Sprintf("%dms", data.Delay)) - } - } - body = wrap(i18n("tgbot.messages.eventOutboundDown", "Tag=="+e.Source), content) - - case eventbus.EventOutboundUp: - subject = host + " " + i18n("tgbot.messages.eventOutboundUp", "Tag=="+e.Source) - content := kv(i18n("email.labelStatus"), ``+i18n("email.statusUp")+``) - content += kv(i18n("email.labelOutbound"), e.Source) - if data, ok := e.Data.(*eventbus.OutboundHealthData); ok && data.Delay > 0 { - content += kv(i18n("email.labelDelay"), fmt.Sprintf("%dms", data.Delay)) - } - body = wrap(i18n("tgbot.messages.eventOutboundUp", "Tag=="+e.Source), content) - - case eventbus.EventXrayCrash: - subject = host + " " + i18n("tgbot.messages.eventXrayCrash") - content := kv(i18n("email.labelStatus"), ``+i18n("email.statusCrashed")+``) - if e.Data != nil { - content += kv(i18n("email.labelError"), fmt.Sprint(e.Data)) - } - body = wrap(i18n("tgbot.messages.eventXrayCrash"), content) - - case eventbus.EventNodeDown: - subject = host + " " + i18n("tgbot.messages.eventNodeDown", "Name=="+e.Source) - content := kv(i18n("email.labelStatus"), ``+i18n("email.statusDown")+``) - content += kv(i18n("email.labelNode"), e.Source) - if data, ok := e.Data.(*eventbus.NodeHealthData); ok && data.XrayError != "" { - content += kv(i18n("email.labelError"), data.XrayError) - } - body = wrap(i18n("tgbot.messages.eventNodeDown", "Name=="+e.Source), content) - - case eventbus.EventNodeUp: - subject = host + " " + i18n("tgbot.messages.eventNodeUp", "Name=="+e.Source) - content := kv(i18n("email.labelStatus"), ``+i18n("email.statusUp")+``) - content += kv(i18n("email.labelNode"), e.Source) - if data, ok := e.Data.(*eventbus.NodeHealthData); ok && data.LatencyMs > 0 { - content += kv(i18n("email.labelDelay"), fmt.Sprintf("%dms", data.LatencyMs)) - } - body = wrap(i18n("tgbot.messages.eventNodeUp", "Name=="+e.Source), content) - - case eventbus.EventCPUHigh: - if data, ok := e.Data.(*eventbus.SystemMetricData); ok { - smtpCpu, err := s.settingService.GetSmtpCpu() - if err != nil || smtpCpu <= 0 || data.Percent <= float64(smtpCpu) { - return - } - subject = host + " " + i18n("tgbot.messages.cpuThreshold", - "Percent=="+strconv.FormatFloat(data.Percent, 'f', 2, 64), - "Threshold=="+fmt.Sprintf("%d", smtpCpu)) - content := kv(i18n("email.labelStatus"), ``+i18n("email.statusHigh")+``) - body = wrap(subject, content) - } - - case eventbus.EventMemoryHigh: - if data, ok := e.Data.(*eventbus.SystemMetricData); ok { - smtpMemory, err := s.settingService.GetSmtpMemory() - if err != nil || smtpMemory <= 0 || data.Percent <= float64(smtpMemory) { - return - } - subject = host + " " + i18n("tgbot.messages.memoryThreshold", - "Percent=="+strconv.FormatFloat(data.Percent, 'f', 2, 64), - "Threshold=="+fmt.Sprintf("%d", smtpMemory)) - content := kv(i18n("email.labelStatus"), ``+i18n("email.statusHigh")+``) - body = wrap(subject, content) - } - - case eventbus.EventLoginAttempt: - if data, ok := e.Data.(*eventbus.LoginEventData); ok { - if data.Status == "success" { - subject = host + " " + i18n("tgbot.messages.loginSuccess") - content := kv(i18n("email.labelStatus"), ``+i18n("email.statusSuccess")+``) - content += kv(i18n("email.labelUsername"), data.Username) - content += kv(i18n("email.labelIP"), data.IP) - body = wrap(i18n("tgbot.messages.loginSuccess"), content) - } else { - subject = host + " " + i18n("tgbot.messages.loginFailed") - content := kv(i18n("email.labelStatus"), ``+i18n("email.statusFailed")+``) - if data.Reason != "" { - content += kv(i18n("email.labelReason"), data.Reason) - } - content += kv(i18n("email.labelUsername"), data.Username) - content += kv(i18n("email.labelIP"), data.IP) - body = wrap(i18n("tgbot.messages.loginFailed"), content) - } - } else { - subject = host + " " + i18n("tgbot.messages.loginFailed") - content := kv(i18n("email.labelStatus"), ``+i18n("email.statusFailed")+``) - content += kv(i18n("email.labelSource"), e.Source) - body = wrap(i18n("tgbot.messages.loginFailed"), content) - } - } - - return -} - -func hostname() (string, error) { - return os.Hostname() -} diff --git a/internal/web/service/setting.go b/internal/web/service/setting.go index 47730cb..2ca9f8f 100644 --- a/internal/web/service/setting.go +++ b/internal/web/service/setting.go @@ -128,24 +128,10 @@ var defaultValueMap = map[string]string{ "devChannelEnable": "false", // Event bus — per-subscriber event filtering (empty = all disabled) - "tgEnabledEvents": "login.attempt,cpu.high", - "smtpEnabledEvents": "login.attempt,cpu.high", - "smtpCpu": "80", - "smtpMemory": "80", + "tgEnabledEvents": "login.attempt,cpu.high", // Consecutive failed observatory probes before an outbound.down event fires "outboundDownThreshold": "3", - - // Email (SMTP) notifications - "smtpEnable": "false", - "smtpHost": "", - "smtpPort": "587", - "smtpUsername": "", - "smtpPassword": "", - "smtpFrom": "", - "smtpFromName": "", - "smtpTo": "", - "smtpEncryptionType": "starttls", // no, starttls, tls } // SettingService provides business logic for application settings management. @@ -245,13 +231,11 @@ func (s *SettingService) GetAllSettingView() (*entity.AllSettingView, error) { view.HasTgBotToken = secretConfigured(allSetting.TgBotToken) view.HasWarpSecret = secretConfigured(mustString(s.GetWarp())) view.HasNordSecret = secretConfigured(mustString(s.GetNord())) - view.HasSmtpPassword = secretConfigured(allSetting.SmtpPassword) var apiTokenCount int64 if err := database.GetDB().Model(model.ApiToken{}).Where("enabled = ?", true).Count(&apiTokenCount).Error; err == nil { view.HasApiToken = apiTokenCount > 0 } view.TgBotToken = "" - view.SmtpPassword = "" return view, nil } @@ -911,104 +895,6 @@ func (s *SettingService) SetTgEnabledEvents(events string) error { return s.setString("tgEnabledEvents", events) } -func (s *SettingService) GetSmtpEnabledEvents() (string, error) { - return s.getString("smtpEnabledEvents") -} - -func (s *SettingService) SetSmtpEnabledEvents(events string) error { - return s.setString("smtpEnabledEvents", events) -} - -// Email (SMTP) settings - -func (s *SettingService) GetSmtpEnable() (bool, error) { - return s.getBool("smtpEnable") -} - -func (s *SettingService) SetSmtpEnable(value bool) error { - return s.setBool("smtpEnable", value) -} - -func (s *SettingService) GetSmtpHost() (string, error) { - return s.getString("smtpHost") -} - -func (s *SettingService) SetSmtpHost(value string) error { - return s.setString("smtpHost", value) -} - -func (s *SettingService) GetSmtpPort() (int, error) { - return s.getInt("smtpPort") -} - -func (s *SettingService) SetSmtpPort(value int) error { - return s.setInt("smtpPort", value) -} - -func (s *SettingService) GetSmtpUsername() (string, error) { - return s.getString("smtpUsername") -} - -func (s *SettingService) SetSmtpUsername(value string) error { - return s.setString("smtpUsername", value) -} - -func (s *SettingService) GetSmtpFrom() (string, error) { - return s.getString("smtpFrom") -} - -func (s *SettingService) SetSmtpFrom(value string) error { - return s.setString("smtpFrom", value) -} - -func (s *SettingService) GetSmtpFromName() (string, error) { - return s.getString("smtpFromName") -} - -func (s *SettingService) SetSmtpFromName(value string) error { - return s.setString("smtpFromName", value) -} - -func (s *SettingService) GetSmtpPassword() (string, error) { - return s.getString("smtpPassword") -} - -func (s *SettingService) SetSmtpPassword(value string) error { - return s.setString("smtpPassword", value) -} - -func (s *SettingService) GetSmtpTo() (string, error) { - return s.getString("smtpTo") -} - -func (s *SettingService) SetSmtpTo(value string) error { - return s.setString("smtpTo", value) -} - -func (s *SettingService) GetSmtpEncryptionType() (string, error) { - return s.getString("smtpEncryptionType") -} - -func (s *SettingService) SetSmtpEncryptionType(value string) error { - return s.setString("smtpEncryptionType", value) -} - -func (s *SettingService) GetSmtpCpu() (int, error) { - return s.getInt("smtpCpu") -} - -func (s *SettingService) SetSmtpCpu(value int) error { - return s.setInt("smtpCpu", value) -} - -func (s *SettingService) GetSmtpMemory() (int, error) { - return s.getInt("smtpMemory") -} - -func (s *SettingService) SetSmtpMemory(value int) error { - return s.setInt("smtpMemory", value) -} - // GetOutboundDownThreshold returns how many consecutive failed observatory // probes an outbound must accumulate before an outbound.down notification is // emitted. 1 preserves the legacy "notify on the first failed probe" behaviour. @@ -1024,8 +910,7 @@ func (s *SettingService) SetOutboundDownThreshold(value int) error { // flag, a blank submitted secret means "unchanged" (the field is always served // blank to the browser) and the stored value is preserved. type SecretClears struct { - TgBotToken bool - SmtpPassword bool + TgBotToken bool } func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting, clears SecretClears) error { @@ -1128,13 +1013,6 @@ func (s *SettingService) preserveRedactedSecrets(allSetting *entity.AllSetting, } allSetting.TgBotToken = value } - if !clears.SmtpPassword && strings.TrimSpace(allSetting.SmtpPassword) == "" { - value, err := s.GetSmtpPassword() - if err != nil { - return err - } - allSetting.SmtpPassword = value - } return nil } @@ -1310,8 +1188,7 @@ func (s *SettingService) GetDefaultSettings(host string) (any, error) { } var factoryDefaultSecretKeys = map[string]bool{ - "tgBotToken": true, - "smtpPassword": true, + "tgBotToken": true, } /* diff --git a/internal/web/web.go b/internal/web/web.go index 5dee61d..ab8120a 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -30,7 +30,6 @@ import ( "github.com/mhsanaei/3x-ui/v3/internal/web/network" "github.com/mhsanaei/3x-ui/v3/internal/web/runtime" "github.com/mhsanaei/3x-ui/v3/internal/web/service" - "github.com/mhsanaei/3x-ui/v3/internal/web/service/email" "github.com/mhsanaei/3x-ui/v3/internal/web/service/panel" "github.com/mhsanaei/3x-ui/v3/internal/web/service/tgbot" "github.com/mhsanaei/3x-ui/v3/internal/web/websocket" @@ -432,13 +431,6 @@ func (s *Server) cpuAlarmWanted() bool { return true } } - if on, _ := s.settingService.GetSmtpEnable(); on { - events, _ := s.settingService.GetSmtpEnabledEvents() - cpu, _ := s.settingService.GetSmtpCpu() - if wants(events, cpu) { - return true - } - } return false } @@ -462,13 +454,6 @@ func (s *Server) memoryAlarmWanted() bool { return true } } - if on, _ := s.settingService.GetSmtpEnable(); on { - events, _ := s.settingService.GetSmtpEnabledEvents() - mem, _ := s.settingService.GetSmtpMemory() - if wants(events, mem) { - return true - } - } return false } @@ -616,14 +601,6 @@ func (s *Server) start(restartXray bool, startTgBot bool) (err error) { } } - // Register email subscriber (always — it checks smtpEnable at runtime) - emailService := email.NewEmailService(s.settingService) - emailSub := email.NewSubscriber(s.settingService, emailService) - s.bus.Subscribe("email-notifier", emailSub.HandleEvent) - - // Wire email service to controller for test endpoint - controller.SetEmailService(emailService) - // Wire Telegram test function to controller controller.SetTestTgFunc(func() error { if !s.tgbotService.IsRunning() {