testutil_test.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. package email
  2. import (
  3. "bytes"
  4. "context"
  5. "errors"
  6. "io"
  7. "net"
  8. "strconv"
  9. "strings"
  10. "sync"
  11. "testing"
  12. "time"
  13. "github.com/emersion/go-imap/v2"
  14. "github.com/emersion/go-imap/v2/imapserver"
  15. "github.com/emersion/go-imap/v2/imapserver/imapmemserver"
  16. "github.com/emersion/go-sasl"
  17. "github.com/emersion/go-smtp"
  18. )
  19. const (
  20. testMailUser = "alice@example.test"
  21. testMailPassword = "correct horse"
  22. )
  23. var testAdmin = Principal{Username: "admin", Admin: true}
  24. // newTestManager returns a manager rooted in a temp dir with the background
  25. // goroutine disabled.
  26. func newTestManager(t *testing.T) *Manager {
  27. t.Helper()
  28. manager, err := NewManager(Options{DataDir: t.TempDir(), DisableBackground: true})
  29. if err != nil {
  30. t.Fatalf("NewManager: %v", err)
  31. }
  32. t.Cleanup(manager.Close)
  33. return manager
  34. }
  35. // testIMAP is an in-memory IMAP server listening on localhost.
  36. type testIMAP struct {
  37. host string
  38. port int
  39. user *imapmemserver.User
  40. }
  41. func startIMAPServer(t *testing.T, caps imap.CapSet) *testIMAP {
  42. t.Helper()
  43. memServer := imapmemserver.New()
  44. user := imapmemserver.NewUser(testMailUser, testMailPassword)
  45. for _, name := range []string{"INBOX", "Sent", "Drafts", "Trash", "Junk", "Projects"} {
  46. if err := user.Create(name, nil); err != nil {
  47. t.Fatalf("create %s: %v", name, err)
  48. }
  49. }
  50. memServer.AddUser(user)
  51. if caps == nil {
  52. caps = imap.CapSet{imap.CapIMAP4rev1: {}, imap.CapIMAP4rev2: {}}
  53. }
  54. server := imapserver.New(&imapserver.Options{
  55. NewSession: func(conn *imapserver.Conn) (imapserver.Session, *imapserver.GreetingData, error) {
  56. return memServer.NewSession(), nil, nil
  57. },
  58. InsecureAuth: true,
  59. Caps: caps,
  60. })
  61. listener, err := net.Listen("tcp", "127.0.0.1:0")
  62. if err != nil {
  63. t.Fatalf("listen: %v", err)
  64. }
  65. go server.Serve(listener)
  66. t.Cleanup(func() { server.Close() })
  67. address := listener.Addr().(*net.TCPAddr)
  68. return &testIMAP{host: "127.0.0.1", port: address.Port, user: user}
  69. }
  70. // appendRaw stores a message straight into a mailbox of the test server.
  71. func (s *testIMAP) appendRaw(t *testing.T, mailbox string, raw string, flags ...imap.Flag) {
  72. t.Helper()
  73. s.appendRawAt(t, mailbox, raw, time.Time{}, flags...)
  74. }
  75. // appendRawAt sets the arrival time (INTERNALDATE has one second precision).
  76. func (s *testIMAP) appendRawAt(t *testing.T, mailbox string, raw string, arrival time.Time, flags ...imap.Flag) {
  77. t.Helper()
  78. data := []byte(strings.ReplaceAll(raw, "\n", "\r\n"))
  79. options := &imap.AppendOptions{Flags: flags, Time: arrival}
  80. if _, err := s.user.Append(mailbox, &literalReader{Reader: bytes.NewReader(data), size: int64(len(data))}, options); err != nil {
  81. t.Fatalf("append: %v", err)
  82. }
  83. }
  84. type literalReader struct {
  85. io.Reader
  86. size int64
  87. }
  88. func (r *literalReader) Size() int64 { return r.size }
  89. // testSMTP is a minimal SMTP submission server that records messages.
  90. type testSMTP struct {
  91. host string
  92. port int
  93. mutex sync.Mutex
  94. messages []receivedMail
  95. }
  96. type receivedMail struct {
  97. from string
  98. to []string
  99. data []byte
  100. }
  101. func (s *testSMTP) received() []receivedMail {
  102. s.mutex.Lock()
  103. defer s.mutex.Unlock()
  104. return append([]receivedMail{}, s.messages...)
  105. }
  106. type smtpSession struct {
  107. server *testSMTP
  108. authed bool
  109. from string
  110. to []string
  111. }
  112. func (s *smtpSession) AuthMechanisms() []string { return []string{sasl.Plain} }
  113. func (s *smtpSession) Auth(mech string) (sasl.Server, error) {
  114. return sasl.NewPlainServer(func(identity, username, password string) error {
  115. if username != testMailUser || password != testMailPassword {
  116. return errors.New("invalid credentials")
  117. }
  118. s.authed = true
  119. return nil
  120. }), nil
  121. }
  122. func (s *smtpSession) Mail(from string, opts *smtp.MailOptions) error {
  123. if !s.authed {
  124. return &smtp.SMTPError{Code: 530, Message: "authentication required"}
  125. }
  126. s.from = from
  127. return nil
  128. }
  129. func (s *smtpSession) Rcpt(to string, opts *smtp.RcptOptions) error {
  130. s.to = append(s.to, to)
  131. return nil
  132. }
  133. func (s *smtpSession) Data(r io.Reader) error {
  134. data, err := io.ReadAll(r)
  135. if err != nil {
  136. return err
  137. }
  138. s.server.mutex.Lock()
  139. s.server.messages = append(s.server.messages, receivedMail{from: s.from, to: s.to, data: data})
  140. s.server.mutex.Unlock()
  141. return nil
  142. }
  143. func (s *smtpSession) Reset() { s.from = ""; s.to = nil }
  144. func (s *smtpSession) Logout() error { return nil }
  145. func startSMTPServer(t *testing.T) *testSMTP {
  146. t.Helper()
  147. fake := &testSMTP{}
  148. server := smtp.NewServer(smtp.BackendFunc(func(c *smtp.Conn) (smtp.Session, error) {
  149. return &smtpSession{server: fake}, nil
  150. }))
  151. server.Domain = "localhost"
  152. server.AllowInsecureAuth = true
  153. listener, err := net.Listen("tcp", "127.0.0.1:0")
  154. if err != nil {
  155. t.Fatalf("listen: %v", err)
  156. }
  157. go server.Serve(listener)
  158. t.Cleanup(func() { server.Close() })
  159. fake.host = "127.0.0.1"
  160. fake.port = listener.Addr().(*net.TCPAddr).Port
  161. return fake
  162. }
  163. // testAccountInput describes an account on the test servers.
  164. func testAccountInput(imapServer *testIMAP, smtpServer *testSMTP, password string) AccountInput {
  165. return AccountInput{
  166. Email: testMailUser,
  167. DisplayName: "Alice",
  168. Provider: "custom",
  169. IMAP: ServerConfig{Host: imapServer.host, Port: imapServer.port, Security: SecurityNone, Username: testMailUser},
  170. SMTP: ServerConfig{Host: smtpServer.host, Port: smtpServer.port, Security: SecurityNone, Username: testMailUser},
  171. Auth: AuthPassword,
  172. Password: password,
  173. SaveSent: "always",
  174. }
  175. }
  176. // addTestAccount wires a manager to fresh test servers.
  177. func addTestAccount(t *testing.T, manager *Manager) (*AccountInfo, *testIMAP, *testSMTP) {
  178. t.Helper()
  179. imapServer := startIMAPServer(t, nil)
  180. smtpServer := startSMTPServer(t)
  181. info, result, err := manager.AddAccount(testContext(t), testAdmin, testAccountInput(imapServer, smtpServer, testMailPassword))
  182. if err != nil {
  183. t.Fatalf("AddAccount: %v (%+v)", err, result)
  184. }
  185. return info, imapServer, smtpServer
  186. }
  187. func sampleMessage(subject string, from string, body string) string {
  188. return "From: " + from + "\n" +
  189. "To: Alice <" + testMailUser + ">\n" +
  190. "Subject: " + subject + "\n" +
  191. "Date: Mon, 02 Mar 2026 10:00:00 +0000\n" +
  192. "Message-ID: <" + strings.ReplaceAll(strings.ToLower(subject), " ", "-") + "@example.test>\n" +
  193. "Content-Type: text/plain; charset=utf-8\n\n" +
  194. body + "\n"
  195. }
  196. func itoa(n int) string { return strconv.Itoa(n) }
  197. // testContext bounds a test's network calls.
  198. func testContext(t *testing.T) context.Context {
  199. t.Helper()
  200. ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
  201. t.Cleanup(cancel)
  202. return ctx
  203. }