email_test.go 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. package email
  2. import (
  3. "bufio"
  4. "fmt"
  5. "io"
  6. "mime"
  7. "net"
  8. "net/mail"
  9. "path/filepath"
  10. "strconv"
  11. "strings"
  12. "sync"
  13. "testing"
  14. "github.com/mhsanaei/3x-ui/v3/internal/database"
  15. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  16. )
  17. func TestBuildMessageIsRFC5322(t *testing.T) {
  18. raw := buildMessage("[email protected]", "3x-ui", []string{"[email protected]", "[email protected]"}, "Тест", "<b>hi</b>")
  19. msg, err := mail.ReadMessage(strings.NewReader(string(raw)))
  20. if err != nil {
  21. t.Fatalf("message does not parse as RFC 5322: %v", err)
  22. }
  23. from, err := mail.ParseAddress(msg.Header.Get("From"))
  24. if err != nil {
  25. t.Fatalf("From header does not parse: %v", err)
  26. }
  27. if from.Name != "3x-ui" || from.Address != "[email protected]" {
  28. t.Errorf("From = %q <%q>, want name %q addr %q", from.Name, from.Address, "3x-ui", "[email protected]")
  29. }
  30. if _, err := msg.Header.Date(); err != nil {
  31. t.Errorf("Date header missing or unparseable: %v", err)
  32. }
  33. id := msg.Header.Get("Message-ID")
  34. if !strings.HasPrefix(id, "<") || !strings.HasSuffix(id, "@example.com>") {
  35. t.Errorf("Message-ID = %q, want <[email protected]>", id)
  36. }
  37. subject, err := (&mime.WordDecoder{}).DecodeHeader(msg.Header.Get("Subject"))
  38. if err != nil {
  39. t.Fatalf("Subject does not decode: %v", err)
  40. }
  41. if subject != "Тест" {
  42. t.Errorf("Subject = %q, want %q", subject, "Тест")
  43. }
  44. body, _ := io.ReadAll(msg.Body)
  45. if string(body) != "<b>hi</b>" {
  46. t.Errorf("body = %q, want %q", body, "<b>hi</b>")
  47. }
  48. }
  49. func TestBuildMessageFromWithoutName(t *testing.T) {
  50. raw := buildMessage("[email protected]", "", []string{"[email protected]"}, "s", "b")
  51. msg, err := mail.ReadMessage(strings.NewReader(string(raw)))
  52. if err != nil {
  53. t.Fatalf("parse: %v", err)
  54. }
  55. from, err := mail.ParseAddress(msg.Header.Get("From"))
  56. if err != nil {
  57. t.Fatalf("From header does not parse: %v", err)
  58. }
  59. if from.Name != "" || from.Address != "[email protected]" {
  60. t.Errorf("From = %q <%q>, want bare addr", from.Name, from.Address)
  61. }
  62. }
  63. func startFakeSMTPServer(t *testing.T) (string, func() []string) {
  64. t.Helper()
  65. ln, err := net.Listen("tcp", "127.0.0.1:0")
  66. if err != nil {
  67. t.Fatal(err)
  68. }
  69. t.Cleanup(func() { _ = ln.Close() })
  70. var mu sync.Mutex
  71. var lines []string
  72. record := func(line string) {
  73. mu.Lock()
  74. defer mu.Unlock()
  75. lines = append(lines, line)
  76. }
  77. go func() {
  78. conn, err := ln.Accept()
  79. if err != nil {
  80. return
  81. }
  82. defer conn.Close()
  83. reader := bufio.NewReader(conn)
  84. fmt.Fprint(conn, "220 fake ready\r\n")
  85. inData := false
  86. for {
  87. line, err := reader.ReadString('\n')
  88. if err != nil {
  89. return
  90. }
  91. line = strings.TrimRight(line, "\r\n")
  92. record(line)
  93. if inData {
  94. if line == "." {
  95. inData = false
  96. fmt.Fprint(conn, "250 ok\r\n")
  97. }
  98. continue
  99. }
  100. switch {
  101. case strings.HasPrefix(line, "DATA"):
  102. inData = true
  103. fmt.Fprint(conn, "354 send\r\n")
  104. case strings.HasPrefix(line, "QUIT"):
  105. fmt.Fprint(conn, "221 bye\r\n")
  106. return
  107. default:
  108. fmt.Fprint(conn, "250 ok\r\n")
  109. }
  110. }
  111. }()
  112. return ln.Addr().String(), func() []string {
  113. mu.Lock()
  114. defer mu.Unlock()
  115. return append([]string(nil), lines...)
  116. }
  117. }
  118. func TestSendUsesBareAddressFromNameAddrSmtpFrom(t *testing.T) {
  119. if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  120. t.Fatal(err)
  121. }
  122. t.Cleanup(func() { _ = database.CloseDB() })
  123. addr, recordedLines := startFakeSMTPServer(t)
  124. host, portStr, err := net.SplitHostPort(addr)
  125. if err != nil {
  126. t.Fatal(err)
  127. }
  128. port, err := strconv.Atoi(portStr)
  129. if err != nil {
  130. t.Fatal(err)
  131. }
  132. settingService := service.SettingService{}
  133. mustSet := func(name string, err error) {
  134. t.Helper()
  135. if err != nil {
  136. t.Fatalf("set %s: %v", name, err)
  137. }
  138. }
  139. mustSet("host", settingService.SetSmtpHost(host))
  140. mustSet("port", settingService.SetSmtpPort(port))
  141. mustSet("from", settingService.SetSmtpFrom("3x-ui Panel <[email protected]>"))
  142. mustSet("to", settingService.SetSmtpTo("[email protected]"))
  143. mustSet("encryption", settingService.SetSmtpEncryptionType("none"))
  144. if err := NewEmailService(settingService).Send("subject", "<b>hi</b>"); err != nil {
  145. t.Fatalf("send: %v", err)
  146. }
  147. var mailFrom, fromHeader string
  148. for _, line := range recordedLines() {
  149. if strings.HasPrefix(line, "MAIL FROM:") {
  150. mailFrom = line
  151. }
  152. if strings.HasPrefix(line, "From: ") {
  153. fromHeader = line
  154. }
  155. }
  156. if want := "MAIL FROM:<[email protected]>"; mailFrom != want {
  157. t.Errorf("envelope sender = %q, want %q", mailFrom, want)
  158. }
  159. if want := `From: "3x-ui Panel" <[email protected]>`; fromHeader != want {
  160. t.Errorf("from header = %q, want %q", fromHeader, want)
  161. }
  162. }
  163. func TestConnectionReportsMissingFrom(t *testing.T) {
  164. if err := database.InitDB(filepath.Join(t.TempDir(), "x-ui.db")); err != nil {
  165. t.Fatal(err)
  166. }
  167. t.Cleanup(func() { _ = database.CloseDB() })
  168. settingService := service.SettingService{}
  169. mustSet := func(name string, err error) {
  170. t.Helper()
  171. if err != nil {
  172. t.Fatalf("set %s: %v", name, err)
  173. }
  174. }
  175. mustSet("host", settingService.SetSmtpHost("127.0.0.1"))
  176. mustSet("port", settingService.SetSmtpPort(1))
  177. mustSet("to", settingService.SetSmtpTo("[email protected]"))
  178. mustSet("encryption", settingService.SetSmtpEncryptionType("none"))
  179. got := NewEmailService(settingService).TestConnection()
  180. want := SMTPTestResult{Success: false, Stage: "send", Message: "smtpFromNotConfigured"}
  181. if got != want {
  182. t.Errorf("TestConnection() = %+v, want %+v", got, want)
  183. }
  184. }
  185. func TestBuildMessageStripsHeaderInjection(t *testing.T) {
  186. raw := buildMessage(
  187. "[email protected]\r\nBcc: [email protected]",
  188. "Name\r\nX-Evil: 1",
  189. []string{"[email protected]"}, "s", "b",
  190. )
  191. msg, err := mail.ReadMessage(strings.NewReader(string(raw)))
  192. if err != nil {
  193. t.Fatalf("parse: %v", err)
  194. }
  195. if got := msg.Header.Get("Bcc"); got != "" {
  196. t.Errorf("injected Bcc header leaked: %q", got)
  197. }
  198. if got := msg.Header.Get("X-Evil"); got != "" {
  199. t.Errorf("injected X-Evil header leaked: %q", got)
  200. }
  201. }