subscriber.go 7.2 KB

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