subscriber.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182
  1. package email
  2. import (
  3. "fmt"
  4. "os"
  5. "strconv"
  6. "strings"
  7. "time"
  8. "github.com/mhsanaei/3x-ui/v3/internal/eventbus"
  9. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  10. "github.com/mhsanaei/3x-ui/v3/internal/web/locale"
  11. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  12. )
  13. // Subscriber handles event bus messages and sends email notifications.
  14. type Subscriber struct {
  15. settingService service.SettingService
  16. emailService *EmailService
  17. limiter *eventbus.RateLimiter
  18. }
  19. // NewSubscriber creates a new email event subscriber.
  20. func NewSubscriber(settingService service.SettingService, emailService *EmailService) *Subscriber {
  21. return &Subscriber{
  22. settingService: settingService,
  23. emailService: emailService,
  24. limiter: eventbus.NewRateLimiter(1 * time.Minute),
  25. }
  26. }
  27. // HandleEvent is the eventbus subscriber callback.
  28. func (s *Subscriber) HandleEvent(e eventbus.Event) {
  29. if !s.isEventEnabled(e.Type) {
  30. return
  31. }
  32. if e.Type != eventbus.EventLoginAttempt {
  33. if !s.limiter.Allow(e.Type, e.Source) {
  34. return
  35. }
  36. }
  37. subject, body := s.formatMessage(e)
  38. if subject == "" {
  39. return
  40. }
  41. if err := s.emailService.Send(subject, body); err != nil {
  42. logger.Warning("email subscriber: send failed:", err)
  43. }
  44. }
  45. func (s *Subscriber) isEventEnabled(t eventbus.EventType) bool {
  46. events, err := s.settingService.GetSmtpEnabledEvents()
  47. if err != nil || events == "" {
  48. return false
  49. }
  50. for _, e := range strings.Split(events, ",") {
  51. if strings.TrimSpace(e) == string(t) {
  52. return true
  53. }
  54. }
  55. return false
  56. }
  57. func i18n(key string, params ...string) string {
  58. return locale.I18n(locale.Bot, key, params...)
  59. }
  60. func (s *Subscriber) formatMessage(e eventbus.Event) (subject, body string) {
  61. h, _ := hostname()
  62. host := h
  63. ts := e.Timestamp.Format("2006-01-02 15:04:05")
  64. wrap := func(title, content string) string {
  65. // Strip newlines from title to prevent broken HTML
  66. title = strings.ReplaceAll(title, "\r\n", "")
  67. title = strings.ReplaceAll(title, "\n", "")
  68. return fmt.Sprintf(`<html><body style="font-family:monospace;font-size:14px;color:#333">
  69. <h2 style="color:#555;border-bottom:1px solid #ddd;padding-bottom:8px">📡 %s %s</h2>
  70. %s
  71. <p style="color:#999;font-size:12px;margin-top:20px">%s</p>
  72. </body></html>`, host, title, content, i18n("tgbot.messages.time", "Time=="+ts))
  73. }
  74. kv := func(key, val string) string {
  75. return fmt.Sprintf("<p><b>%s:</b> %s</p>", key, val)
  76. }
  77. switch e.Type {
  78. case eventbus.EventOutboundDown:
  79. subject = host + " " + i18n("tgbot.messages.eventOutboundDown", "Tag=="+e.Source)
  80. content := kv(i18n("email.labelStatus"), `<span style="color:red">`+i18n("email.statusDown")+`</span>`)
  81. content += kv(i18n("email.labelOutbound"), e.Source)
  82. if data, ok := e.Data.(*eventbus.OutboundHealthData); ok {
  83. if data.Error != "" {
  84. content += kv(i18n("email.labelError"), data.Error)
  85. }
  86. if data.Delay > 0 {
  87. content += kv(i18n("email.labelDelay"), fmt.Sprintf("%dms", data.Delay))
  88. }
  89. }
  90. body = wrap(i18n("tgbot.messages.eventOutboundDown", "Tag=="+e.Source), content)
  91. case eventbus.EventOutboundUp:
  92. subject = host + " " + i18n("tgbot.messages.eventOutboundUp", "Tag=="+e.Source)
  93. content := kv(i18n("email.labelStatus"), `<span style="color:green">`+i18n("email.statusUp")+`</span>`)
  94. content += kv(i18n("email.labelOutbound"), e.Source)
  95. if data, ok := e.Data.(*eventbus.OutboundHealthData); ok && data.Delay > 0 {
  96. content += kv(i18n("email.labelDelay"), fmt.Sprintf("%dms", data.Delay))
  97. }
  98. body = wrap(i18n("tgbot.messages.eventOutboundUp", "Tag=="+e.Source), content)
  99. case eventbus.EventXrayCrash:
  100. subject = host + " " + i18n("tgbot.messages.eventXrayCrash")
  101. content := kv(i18n("email.labelStatus"), `<span style="color:red">`+i18n("email.statusCrashed")+`</span>`)
  102. if e.Data != nil {
  103. content += kv(i18n("email.labelError"), fmt.Sprint(e.Data))
  104. }
  105. body = wrap(i18n("tgbot.messages.eventXrayCrash"), content)
  106. case eventbus.EventNodeDown:
  107. subject = host + " " + i18n("tgbot.messages.eventNodeDown", "Name=="+e.Source)
  108. content := kv(i18n("email.labelStatus"), `<span style="color:red">`+i18n("email.statusDown")+`</span>`)
  109. content += kv(i18n("email.labelNode"), e.Source)
  110. if data, ok := e.Data.(*eventbus.NodeHealthData); ok && data.XrayError != "" {
  111. content += kv(i18n("email.labelError"), data.XrayError)
  112. }
  113. body = wrap(i18n("tgbot.messages.eventNodeDown", "Name=="+e.Source), content)
  114. case eventbus.EventNodeUp:
  115. subject = host + " " + i18n("tgbot.messages.eventNodeUp", "Name=="+e.Source)
  116. content := kv(i18n("email.labelStatus"), `<span style="color:green">`+i18n("email.statusUp")+`</span>`)
  117. content += kv(i18n("email.labelNode"), e.Source)
  118. if data, ok := e.Data.(*eventbus.NodeHealthData); ok && data.LatencyMs > 0 {
  119. content += kv(i18n("email.labelDelay"), fmt.Sprintf("%dms", data.LatencyMs))
  120. }
  121. body = wrap(i18n("tgbot.messages.eventNodeUp", "Name=="+e.Source), content)
  122. case eventbus.EventCPUHigh:
  123. if data, ok := e.Data.(*eventbus.SystemMetricData); ok {
  124. smtpCpu, err := s.settingService.GetSmtpCpu()
  125. if err != nil || smtpCpu <= 0 || data.Percent <= float64(smtpCpu) {
  126. return
  127. }
  128. subject = host + " " + i18n("tgbot.messages.cpuThreshold",
  129. "Percent=="+strconv.FormatFloat(data.Percent, 'f', 2, 64),
  130. "Threshold=="+fmt.Sprintf("%d", smtpCpu))
  131. content := kv(i18n("email.labelStatus"), `<span style="color:orange">`+i18n("email.statusHigh")+`</span>`)
  132. body = wrap(subject, content)
  133. }
  134. case eventbus.EventLoginAttempt:
  135. if data, ok := e.Data.(*eventbus.LoginEventData); ok {
  136. if data.Status == "success" {
  137. subject = host + " " + i18n("tgbot.messages.loginSuccess")
  138. content := kv(i18n("email.labelStatus"), `<span style="color:green">`+i18n("email.statusSuccess")+`</span>`)
  139. content += kv(i18n("email.labelUsername"), data.Username)
  140. content += kv(i18n("email.labelIP"), data.IP)
  141. body = wrap(i18n("tgbot.messages.loginSuccess"), content)
  142. } else {
  143. subject = host + " " + i18n("tgbot.messages.loginFailed")
  144. content := kv(i18n("email.labelStatus"), `<span style="color:red">`+i18n("email.statusFailed")+`</span>`)
  145. if data.Reason != "" {
  146. content += kv(i18n("email.labelReason"), data.Reason)
  147. }
  148. content += kv(i18n("email.labelUsername"), data.Username)
  149. content += kv(i18n("email.labelIP"), data.IP)
  150. body = wrap(i18n("tgbot.messages.loginFailed"), content)
  151. }
  152. } else {
  153. subject = host + " " + i18n("tgbot.messages.loginFailed")
  154. content := kv(i18n("email.labelStatus"), `<span style="color:red">`+i18n("email.statusFailed")+`</span>`)
  155. content += kv(i18n("email.labelSource"), e.Source)
  156. body = wrap(i18n("tgbot.messages.loginFailed"), content)
  157. }
  158. }
  159. return
  160. }
  161. func hostname() (string, error) {
  162. return os.Hostname()
  163. }