mailbox_test.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646
  1. package email
  2. import (
  3. "bytes"
  4. "errors"
  5. "strings"
  6. "testing"
  7. "time"
  8. "github.com/emersion/go-imap/v2"
  9. gomessage "github.com/emersion/go-message"
  10. )
  11. const htmlMessageWithAttachment = `From: Synology <noreply@synology.test>
  12. To: Alice <alice@example.test>
  13. Subject: =?utf-8?B?UmU6IEFyb3pPUyDmlbTlkIg=?=
  14. Date: Tue, 03 Mar 2026 09:00:00 +0000
  15. Message-ID: <proposal@synology.test>
  16. List-Unsubscribe: <mailto:leave@synology.test>, <https://synology.test/unsub>
  17. Authentication-Results: mx.example.test; spf=pass smtp.mailfrom=synology.test; dkim=pass header.d=synology.test; dmarc=pass
  18. MIME-Version: 1.0
  19. Content-Type: multipart/mixed; boundary="outer"
  20. --outer
  21. Content-Type: multipart/alternative; boundary="alt"
  22. --alt
  23. Content-Type: text/plain; charset=utf-8
  24. Hi Toby, thanks for the proposal.
  25. --alt
  26. Content-Type: multipart/related; boundary="rel"
  27. --rel
  28. Content-Type: text/html; charset=utf-8
  29. <html><body><p onclick="steal()">Hi Toby, thanks for the proposal.</p><img src="cid:logo@synology.test"><img src="https://tracker.test/pixel.gif"><script>alert(1)</script></body></html>
  30. --rel
  31. Content-Type: image/png
  32. Content-ID: <logo@synology.test>
  33. Content-Transfer-Encoding: base64
  34. iVBORw0KGgo=
  35. --rel--
  36. --alt--
  37. --outer
  38. Content-Type: application/pdf; name="proposal.pdf"
  39. Content-Disposition: attachment; filename="proposal.pdf"
  40. Content-Transfer-Encoding: base64
  41. JVBERi0xLjQKJcfsj6IK
  42. --outer--
  43. `
  44. func TestAddAccountVerifiesCredentials(t *testing.T) {
  45. manager := newTestManager(t)
  46. imapServer := startIMAPServer(t, nil)
  47. smtpServer := startSMTPServer(t)
  48. _, result, err := manager.AddAccount(testContext(t), testAdmin, testAccountInput(imapServer, smtpServer, "wrong password"))
  49. if err == nil {
  50. t.Fatalf("AddAccount accepted a wrong password")
  51. }
  52. if !result.AuthFailed || result.Stage != "imap" {
  53. t.Errorf("expected an IMAP auth failure, got %+v", result)
  54. }
  55. info, result, err := manager.AddAccount(testContext(t), testAdmin, testAccountInput(imapServer, smtpServer, testMailPassword))
  56. if err != nil {
  57. t.Fatalf("AddAccount: %v", err)
  58. }
  59. if !result.IMAPOK || !result.SMTPOK {
  60. t.Errorf("expected both servers verified, got %+v", result)
  61. }
  62. if !info.HasSecret || info.Email != testMailUser {
  63. t.Errorf("unexpected account info %+v", info)
  64. }
  65. accounts, err := manager.ListAccounts(testAdmin)
  66. if err != nil || len(accounts) != 1 {
  67. t.Fatalf("ListAccounts = %v, %v", accounts, err)
  68. }
  69. //The same mailbox cannot be added twice
  70. if _, _, err := manager.AddAccount(testContext(t), testAdmin, testAccountInput(imapServer, smtpServer, testMailPassword)); err == nil {
  71. t.Errorf("duplicate account was accepted")
  72. }
  73. //Secrets are sealed at rest
  74. stored, err := manager.store.getAccount(testAdmin.Username, info.ID)
  75. if err != nil {
  76. t.Fatalf("getAccount: %v", err)
  77. }
  78. if strings.Contains(stored.Secret, testMailPassword) || stored.Secret == "" {
  79. t.Errorf("password is not sealed: %q", stored.Secret)
  80. }
  81. }
  82. func TestNonAdminCannotReachLocalServers(t *testing.T) {
  83. manager := newTestManager(t)
  84. imapServer := startIMAPServer(t, nil)
  85. smtpServer := startSMTPServer(t)
  86. user := Principal{Username: "bob"}
  87. input := testAccountInput(imapServer, smtpServer, testMailPassword)
  88. if _, _, err := manager.AddAccount(testContext(t), user, input); !errors.Is(err, ErrInsecureBlocked) {
  89. t.Fatalf("expected ErrInsecureBlocked, got %v", err)
  90. }
  91. if err := manager.SetAdminConfig(testAdmin, AdminConfigInput{AllowInsecure: true}); err != nil {
  92. t.Fatalf("SetAdminConfig: %v", err)
  93. }
  94. if _, _, err := manager.AddAccount(testContext(t), user, input); err == nil || !strings.Contains(err.Error(), ErrBlockedAddress.Error()) {
  95. t.Fatalf("expected the loopback address to be blocked, got %v", err)
  96. }
  97. if err := manager.SetAdminConfig(testAdmin, AdminConfigInput{AllowInsecure: true, AllowPrivateHosts: true}); err != nil {
  98. t.Fatalf("SetAdminConfig: %v", err)
  99. }
  100. if _, _, err := manager.AddAccount(testContext(t), user, input); err != nil {
  101. t.Fatalf("AddAccount after the admin allowed LAN servers: %v", err)
  102. }
  103. }
  104. func TestFoldersDetectRoles(t *testing.T) {
  105. manager := newTestManager(t)
  106. account, _, _ := addTestAccount(t, manager)
  107. folders, err := manager.Folders(testContext(t), testAdmin, account.ID, true)
  108. if err != nil {
  109. t.Fatalf("Folders: %v", err)
  110. }
  111. roles := map[string]string{}
  112. for _, folder := range folders {
  113. roles[folder.Name] = folder.Role
  114. }
  115. expected := map[string]string{"INBOX": RoleInbox, "Sent": RoleSent, "Drafts": RoleDrafts, "Trash": RoleTrash, "Junk": RoleJunk, "Projects": ""}
  116. for name, role := range expected {
  117. if got, ok := roles[name]; !ok || got != role {
  118. t.Errorf("folder %s: role %q, want %q", name, got, role)
  119. }
  120. }
  121. if folders[0].Name != "INBOX" {
  122. t.Errorf("INBOX should be listed first, got %s", folders[0].Name)
  123. }
  124. }
  125. func TestFoldersWithoutListStatus(t *testing.T) {
  126. manager := newTestManager(t)
  127. imapServer := startIMAPServer(t, imap.CapSet{imap.CapIMAP4rev1: {}})
  128. smtpServer := startSMTPServer(t)
  129. imapServer.appendRaw(t, "INBOX", sampleMessage("Counted", "Bob <bob@example.test>", "hello"))
  130. account, _, err := manager.AddAccount(testContext(t), testAdmin, testAccountInput(imapServer, smtpServer, testMailPassword))
  131. if err != nil {
  132. t.Fatalf("AddAccount: %v", err)
  133. }
  134. folders, err := manager.Folders(testContext(t), testAdmin, account.ID, true)
  135. if err != nil {
  136. t.Fatalf("Folders: %v", err)
  137. }
  138. for _, folder := range folders {
  139. if folder.Name == "INBOX" && (folder.Total != 1 || folder.Unread != 1) {
  140. t.Errorf("INBOX counts = %d/%d, want 1/1", folder.Unread, folder.Total)
  141. }
  142. }
  143. }
  144. func TestListMessagesPagingAndFilters(t *testing.T) {
  145. manager := newTestManager(t)
  146. account, imapServer, _ := addTestAccount(t, manager)
  147. for i := 1; i <= 7; i++ {
  148. var flags []imap.Flag
  149. if i%2 == 0 {
  150. flags = append(flags, imap.FlagSeen)
  151. }
  152. imapServer.appendRaw(t, "INBOX", sampleMessage("Message "+itoa(i), "Bob <bob@example.test>", "Body of message "+itoa(i)), flags...)
  153. }
  154. page, err := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX", PageSize: 3, Previews: true})
  155. if err != nil {
  156. t.Fatalf("ListMessages: %v", err)
  157. }
  158. if page.Total != 7 || len(page.Messages) != 3 {
  159. t.Fatalf("page 0: total %d len %d", page.Total, len(page.Messages))
  160. }
  161. if page.Messages[0].Subject != "Message 7" {
  162. t.Errorf("newest first expected, got %q", page.Messages[0].Subject)
  163. }
  164. if page.Messages[0].Preview != "Body of message 7" {
  165. t.Errorf("preview = %q", page.Messages[0].Preview)
  166. }
  167. last, err := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX", PageSize: 3, Page: 2})
  168. if err != nil {
  169. t.Fatalf("ListMessages page 2: %v", err)
  170. }
  171. if len(last.Messages) != 1 || last.Messages[0].Subject != "Message 1" {
  172. t.Errorf("last page = %+v", last.Messages)
  173. }
  174. unread, err := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX", Filter: "unread"})
  175. if err != nil {
  176. t.Fatalf("ListMessages unread: %v", err)
  177. }
  178. if unread.Total != 4 {
  179. t.Errorf("unread total = %d, want 4", unread.Total)
  180. }
  181. search, err := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX", Search: "message 5", SearchIn: "subject"})
  182. if err != nil {
  183. t.Fatalf("ListMessages search: %v", err)
  184. }
  185. if search.Total != 1 || search.Messages[0].Subject != "Message 5" {
  186. t.Errorf("search result = %+v", search.Messages)
  187. }
  188. bySubject, err := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX", Sort: "subject"})
  189. if err != nil {
  190. t.Fatalf("ListMessages sort: %v", err)
  191. }
  192. if bySubject.Messages[0].Subject != "Message 1" {
  193. t.Errorf("subject sort starts with %q", bySubject.Messages[0].Subject)
  194. }
  195. }
  196. func TestGetMessageSanitisesAndMarksSeen(t *testing.T) {
  197. manager := newTestManager(t)
  198. account, imapServer, _ := addTestAccount(t, manager)
  199. imapServer.appendRaw(t, "INBOX", htmlMessageWithAttachment)
  200. list, err := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX"})
  201. if err != nil || len(list.Messages) != 1 {
  202. t.Fatalf("ListMessages: %v %+v", err, list)
  203. }
  204. summary := list.Messages[0]
  205. if !summary.HasAttachments {
  206. t.Errorf("list should report the PDF attachment")
  207. }
  208. if summary.Subject != "Re: ArozOS 整合" {
  209. t.Errorf("encoded subject decoded to %q", summary.Subject)
  210. }
  211. message, err := manager.GetMessage(testContext(t), testAdmin, account.ID, "INBOX", summary.UID, GetOptions{MarkSeen: true})
  212. if err != nil {
  213. t.Fatalf("GetMessage: %v", err)
  214. }
  215. for _, forbidden := range []string{"<script", "onclick", "https://tracker.test/pixel.gif\""} {
  216. if strings.Contains(message.HTML, forbidden) && !strings.Contains(message.HTML, "data-remote-src=\"https://tracker.test/pixel.gif\"") {
  217. t.Errorf("sanitised HTML still contains %q: %s", forbidden, message.HTML)
  218. }
  219. }
  220. if !strings.Contains(message.HTML, "data:image/png;base64,iVBORw0KGgo=") {
  221. t.Errorf("inline image was not resolved: %s", message.HTML)
  222. }
  223. if !message.HasRemoteContent || message.RemoteAllowed {
  224. t.Errorf("remote content flags wrong: has=%v allowed=%v", message.HasRemoteContent, message.RemoteAllowed)
  225. }
  226. if len(message.Attachments) != 1 || message.Attachments[0].Filename != "proposal.pdf" {
  227. t.Fatalf("attachments = %+v", message.Attachments)
  228. }
  229. if message.ListUnsubscribe != "https://synology.test/unsub" {
  230. t.Errorf("unsubscribe = %q", message.ListUnsubscribe)
  231. }
  232. if message.Auth == nil || message.Auth.SPF != "pass" || message.Auth.DKIM != "pass" || message.Auth.DMARC != "pass" {
  233. t.Errorf("auth results = %+v", message.Auth)
  234. }
  235. if !message.Seen {
  236. t.Errorf("message should be marked seen")
  237. }
  238. //The flag must reach the server, not just the response
  239. relisted, err := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX"})
  240. if err != nil || len(relisted.Messages) != 1 || !relisted.Messages[0].Seen {
  241. t.Errorf("seen flag was not stored on the server: %+v %v", relisted, err)
  242. }
  243. statuses, _ := manager.CheckInboxes(testContext(t), testAdmin)
  244. if len(statuses) != 1 || statuses[0].Unread != 0 {
  245. t.Errorf("STATUS still counts the message as unread: %+v", statuses)
  246. }
  247. part, err := manager.GetPart(testContext(t), testAdmin, account.ID, "INBOX", summary.UID, message.Attachments[0].ID)
  248. if err != nil {
  249. t.Fatalf("GetPart: %v", err)
  250. }
  251. if !bytes.HasPrefix(part.Data, []byte("%PDF-1.4")) || part.Filename != "proposal.pdf" {
  252. t.Errorf("attachment content = %q (%s)", part.Data, part.Filename)
  253. }
  254. remote, err := manager.GetMessage(testContext(t), testAdmin, account.ID, "INBOX", summary.UID, GetOptions{AllowRemote: true})
  255. if err != nil {
  256. t.Fatalf("GetMessage allowRemote: %v", err)
  257. }
  258. if !strings.Contains(remote.HTML, `src="https://tracker.test/pixel.gif"`) {
  259. t.Errorf("remote image should load when allowed: %s", remote.HTML)
  260. }
  261. raw, err := manager.GetRaw(testContext(t), testAdmin, account.ID, "INBOX", summary.UID)
  262. if err != nil {
  263. t.Fatalf("GetRaw: %v", err)
  264. }
  265. if !strings.HasSuffix(raw.Filename, ".eml") || !bytes.Contains(raw.Data, []byte("proposal.pdf")) {
  266. t.Errorf("raw message = %s / %d bytes", raw.Filename, len(raw.Data))
  267. }
  268. }
  269. func TestFlagsMoveAndDelete(t *testing.T) {
  270. manager := newTestManager(t)
  271. account, imapServer, _ := addTestAccount(t, manager)
  272. imapServer.appendRaw(t, "INBOX", sampleMessage("Keep", "Bob <bob@example.test>", "keep"))
  273. imapServer.appendRaw(t, "INBOX", sampleMessage("Remove", "Bob <bob@example.test>", "remove"))
  274. list, _ := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX"})
  275. keep, remove := list.Messages[1], list.Messages[0]
  276. if _, err := manager.SetFlag(testContext(t), testAdmin, account.ID, "INBOX", []uint32{keep.UID}, "flagged", true); err != nil {
  277. t.Fatalf("SetFlag: %v", err)
  278. }
  279. flagged, _ := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX", Filter: "flagged"})
  280. if flagged.Total != 1 || flagged.Messages[0].Subject != "Keep" {
  281. t.Errorf("flagged filter = %+v", flagged.Messages)
  282. }
  283. result, err := manager.Delete(testContext(t), testAdmin, account.ID, "INBOX", []uint32{remove.UID}, false)
  284. if err != nil {
  285. t.Fatalf("Delete: %v", err)
  286. }
  287. if result.Permanent || result.Destination != "Trash" {
  288. t.Errorf("delete should move to Trash, got %+v", result)
  289. }
  290. trash, _ := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "Trash"})
  291. if trash.Total != 1 {
  292. t.Fatalf("Trash has %d messages", trash.Total)
  293. }
  294. result, err = manager.Delete(testContext(t), testAdmin, account.ID, "Trash", []uint32{trash.Messages[0].UID}, false)
  295. if err != nil {
  296. t.Fatalf("Delete from Trash: %v", err)
  297. }
  298. if !result.Permanent {
  299. t.Errorf("delete inside Trash should be permanent")
  300. }
  301. trash, _ = manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "Trash"})
  302. if trash.Total != 0 {
  303. t.Errorf("Trash still has %d messages", trash.Total)
  304. }
  305. moved, err := manager.MoveToRole(testContext(t), testAdmin, account.ID, "INBOX", []uint32{keep.UID}, RoleArchive)
  306. if err != nil {
  307. t.Fatalf("archive: %v", err)
  308. }
  309. if moved.Destination != "Archive" {
  310. t.Errorf("archive should create the Archive folder, got %q", moved.Destination)
  311. }
  312. inbox, _ := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX"})
  313. if inbox.Total != 0 {
  314. t.Errorf("INBOX should be empty, has %d", inbox.Total)
  315. }
  316. }
  317. func TestMoveFallsBackWithoutMoveCapability(t *testing.T) {
  318. manager := newTestManager(t)
  319. imapServer := startIMAPServer(t, imap.CapSet{imap.CapIMAP4rev1: {}, imap.CapUIDPlus: {}})
  320. smtpServer := startSMTPServer(t)
  321. imapServer.appendRaw(t, "INBOX", sampleMessage("Moving", "Bob <bob@example.test>", "x"))
  322. account, _, err := manager.AddAccount(testContext(t), testAdmin, testAccountInput(imapServer, smtpServer, testMailPassword))
  323. if err != nil {
  324. t.Fatalf("AddAccount: %v", err)
  325. }
  326. list, _ := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX"})
  327. if _, err := manager.Move(testContext(t), testAdmin, account.ID, "INBOX", []uint32{list.Messages[0].UID}, "Projects"); err != nil {
  328. t.Fatalf("Move: %v", err)
  329. }
  330. projects, _ := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "Projects"})
  331. inbox, _ := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX"})
  332. if projects.Total != 1 || inbox.Total != 0 {
  333. t.Errorf("after move: Projects %d, INBOX %d", projects.Total, inbox.Total)
  334. }
  335. }
  336. func TestSendDeliversAndFilesSentCopy(t *testing.T) {
  337. manager := newTestManager(t)
  338. account, _, smtpServer := addTestAccount(t, manager)
  339. result, err := manager.Send(testContext(t), testAdmin, &ComposeRequest{
  340. AccountID: account.ID,
  341. To: []string{"Bob <bob@example.test>"},
  342. Bcc: []string{"secret@example.test"},
  343. Subject: "Quarterly report",
  344. HTML: "<p>Hello <b>Bob</b></p>",
  345. Attachments: []ComposeAttachment{
  346. {Name: "report.csv", Data: []byte("a,b\n1,2\n")},
  347. },
  348. })
  349. if err != nil {
  350. t.Fatalf("Send: %v", err)
  351. }
  352. if result.Queued || result.Warning != "" {
  353. t.Errorf("unexpected send result %+v", result)
  354. }
  355. received := smtpServer.received()
  356. if len(received) != 1 {
  357. t.Fatalf("SMTP received %d messages", len(received))
  358. }
  359. if strings.Join(received[0].to, ",") != "bob@example.test,secret@example.test" {
  360. t.Errorf("recipients = %v", received[0].to)
  361. }
  362. if bytes.Contains(received[0].data, []byte("secret@example.test")) {
  363. t.Errorf("Bcc leaked into the delivered message")
  364. }
  365. sent, err := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "Sent"})
  366. if err != nil || sent.Total != 1 {
  367. t.Fatalf("Sent folder: %v %+v", err, sent)
  368. }
  369. copyMessage, err := manager.GetMessage(testContext(t), testAdmin, account.ID, "Sent", sent.Messages[0].UID, GetOptions{})
  370. if err != nil {
  371. t.Fatalf("GetMessage sent copy: %v", err)
  372. }
  373. if len(copyMessage.Bcc) != 1 || copyMessage.Bcc[0].Email != "secret@example.test" {
  374. t.Errorf("Sent copy should keep Bcc, got %+v", copyMessage.Bcc)
  375. }
  376. if len(copyMessage.Attachments) != 1 || copyMessage.Attachments[0].Filename != "report.csv" {
  377. t.Errorf("Sent copy attachments = %+v", copyMessage.Attachments)
  378. }
  379. contacts, _ := manager.SearchContacts(testAdmin, "bo", 5)
  380. if len(contacts) != 1 || contacts[0].Email != "bob@example.test" || contacts[0].Name != "Bob" {
  381. t.Errorf("recipient was not collected: %+v", contacts)
  382. }
  383. }
  384. func TestDraftsAreReplaced(t *testing.T) {
  385. manager := newTestManager(t)
  386. account, _, _ := addTestAccount(t, manager)
  387. request := &ComposeRequest{AccountID: account.ID, To: []string{"bob@example.test"}, Subject: "Draft v1", HTML: "<p>one</p>"}
  388. first, err := manager.SaveDraft(testContext(t), testAdmin, request)
  389. if err != nil {
  390. t.Fatalf("SaveDraft: %v", err)
  391. }
  392. if first.Folder != "Drafts" || first.UID == 0 {
  393. t.Fatalf("draft saved to %+v", first)
  394. }
  395. request.Subject = "Draft v2"
  396. request.DraftFolder = first.Folder
  397. request.DraftUID = first.UID
  398. second, err := manager.SaveDraft(testContext(t), testAdmin, request)
  399. if err != nil {
  400. t.Fatalf("SaveDraft v2: %v", err)
  401. }
  402. drafts, _ := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "Drafts"})
  403. if drafts.Total != 1 || drafts.Messages[0].Subject != "Draft v2" || drafts.Messages[0].UID != second.UID {
  404. t.Errorf("drafts after replace = %+v", drafts.Messages)
  405. }
  406. if !drafts.Messages[0].Draft {
  407. t.Errorf("draft flag missing")
  408. }
  409. }
  410. func TestUndoableSendGoesThroughOutbox(t *testing.T) {
  411. manager := newTestManager(t)
  412. account, _, smtpServer := addTestAccount(t, manager)
  413. result, err := manager.Send(testContext(t), testAdmin, &ComposeRequest{
  414. AccountID: account.ID, To: []string{"bob@example.test"}, Subject: "Undo me", Text: "hi", UndoSeconds: 30,
  415. })
  416. if err != nil {
  417. t.Fatalf("Send: %v", err)
  418. }
  419. if !result.Queued || result.OutboxID == "" {
  420. t.Fatalf("expected a queued result, got %+v", result)
  421. }
  422. if _, err := manager.OutboxCancel(testContext(t), testAdmin, result.OutboxID, false); err != nil {
  423. t.Fatalf("OutboxCancel: %v", err)
  424. }
  425. if items, _ := manager.Outbox(testAdmin); len(items) != 0 {
  426. t.Errorf("outbox still has %d items", len(items))
  427. }
  428. result, err = manager.Send(testContext(t), testAdmin, &ComposeRequest{
  429. AccountID: account.ID, To: []string{"bob@example.test"}, Subject: "Deliver me", Text: "hi", UndoSeconds: 30,
  430. })
  431. if err != nil {
  432. t.Fatalf("Send: %v", err)
  433. }
  434. if err := manager.OutboxSendNow(testAdmin, result.OutboxID); err != nil {
  435. t.Fatalf("OutboxSendNow: %v", err)
  436. }
  437. manager.outbox.processDue(testContext(t))
  438. if received := smtpServer.received(); len(received) != 1 || !bytes.Contains(received[0].data, []byte("Deliver me")) {
  439. t.Fatalf("outbox did not deliver, SMTP got %d messages", len(received))
  440. }
  441. if items, _ := manager.Outbox(testAdmin); len(items) != 0 {
  442. t.Errorf("delivered item still in the outbox")
  443. }
  444. }
  445. func TestUnifiedInboxMergesAccounts(t *testing.T) {
  446. manager := newTestManager(t)
  447. first, firstServer, _ := addTestAccount(t, manager)
  448. //The same address on the same host counts as a duplicate, so the second
  449. //test server is addressed by name
  450. secondServer := startIMAPServer(t, nil)
  451. secondSMTP := startSMTPServer(t)
  452. input := testAccountInput(secondServer, secondSMTP, testMailPassword)
  453. input.DisplayName = "Alice Work"
  454. input.IMAP.Host = "localhost"
  455. input.SMTP.Host = "localhost"
  456. secondInfo, _, err := manager.AddAccount(testContext(t), testAdmin, input)
  457. if err != nil {
  458. t.Fatalf("AddAccount second: %v", err)
  459. }
  460. now := time.Now()
  461. firstServer.appendRawAt(t, "INBOX", sampleMessage("From first", "Bob <bob@example.test>", "a"), now.Add(-time.Hour))
  462. secondServer.appendRawAt(t, "INBOX", sampleMessage("From second", "Carol <carol@example.test>", "b"), now)
  463. merged, err := manager.UnifiedList(testContext(t), testAdmin, ViewInbox, nil, ListQuery{})
  464. if err != nil {
  465. t.Fatalf("UnifiedList: %v", err)
  466. }
  467. if merged.Total != 2 || len(merged.Messages) != 2 {
  468. t.Fatalf("merged = %+v", merged)
  469. }
  470. if merged.Messages[0].AccountID != secondInfo.ID || merged.Messages[1].AccountID != first.ID {
  471. t.Errorf("messages are not ordered by arrival across accounts")
  472. }
  473. statuses, err := manager.CheckInboxes(testContext(t), testAdmin)
  474. if err != nil || len(statuses) != 2 {
  475. t.Fatalf("CheckInboxes: %v %+v", err, statuses)
  476. }
  477. for _, status := range statuses {
  478. if status.Unread != 1 || status.UIDNext == 0 {
  479. t.Errorf("status = %+v", status)
  480. }
  481. }
  482. }
  483. func TestImportAndParseEML(t *testing.T) {
  484. manager := newTestManager(t)
  485. account, _, _ := addTestAccount(t, manager)
  486. raw := []byte(strings.ReplaceAll(htmlMessageWithAttachment, "\n", "\r\n"))
  487. message, err := manager.ParseEML(testAdmin, raw, false)
  488. if err != nil {
  489. t.Fatalf("ParseEML: %v", err)
  490. }
  491. if message.Subject != "Re: ArozOS 整合" || len(message.Attachments) != 1 {
  492. t.Errorf("parsed eml = %q, %+v", message.Subject, message.Attachments)
  493. }
  494. part, err := EMLPart(raw, message.Attachments[0].ID)
  495. if err != nil || !bytes.HasPrefix(part.Data, []byte("%PDF")) {
  496. t.Errorf("EMLPart = %v, %q", err, part)
  497. }
  498. uid, err := manager.ImportMessage(testContext(t), testAdmin, account.ID, "Projects", raw)
  499. if err != nil || uid == 0 {
  500. t.Fatalf("ImportMessage: %v uid %d", err, uid)
  501. }
  502. imported, err := manager.GetMessage(testContext(t), testAdmin, account.ID, "Projects", uid, GetOptions{})
  503. if err != nil {
  504. t.Fatalf("GetMessage imported: %v", err)
  505. }
  506. if imported.MessageID != "proposal@synology.test" {
  507. t.Errorf("imported message id = %q", imported.MessageID)
  508. }
  509. }
  510. func TestComposeStructure(t *testing.T) {
  511. manager := newTestManager(t)
  512. account, _, _ := addTestAccount(t, manager)
  513. stored, _ := manager.store.getAccount(testAdmin.Username, account.ID)
  514. built, err := manager.buildMessage(testContext(t), testAdmin, stored, &ComposeRequest{
  515. AccountID: account.ID,
  516. To: []string{"\"Wang, Xiao\" <xiao@example.test>; bob@example.test"},
  517. Subject: "會議 notes",
  518. HTML: `<p>See chart</p><img src="data:image/png;base64,iVBORw0KGgo=">`,
  519. InReplyTo: "<orig@example.test>",
  520. References: []string{"<root@example.test>"},
  521. Priority: "high",
  522. Attachments: []ComposeAttachment{
  523. {Name: "報告.txt", Data: []byte("hello")},
  524. },
  525. }, false)
  526. if err != nil {
  527. t.Fatalf("buildMessage: %v", err)
  528. }
  529. if len(built.recipients) != 2 {
  530. t.Errorf("recipients = %v", built.recipients)
  531. }
  532. entity, err := gomessage.Read(bytes.NewReader(built.raw))
  533. if err != nil {
  534. t.Fatalf("generated message does not parse: %v", err)
  535. }
  536. tree := treeFromEntity(entity)
  537. parts := selectDisplayParts(tree)
  538. if parts.html == nil || parts.text == nil {
  539. t.Fatalf("expected html and text parts")
  540. }
  541. if !strings.Contains(string(parts.html.body), "cid:img1.") {
  542. t.Errorf("pasted image was not converted to a cid reference: %s", parts.html.body)
  543. }
  544. if len(parts.inline) != 1 {
  545. t.Errorf("expected one inline image, got %d", len(parts.inline))
  546. }
  547. attachments := attachmentInfo(parts.attachments)
  548. if len(attachments) != 1 || attachments[0].Filename != "報告.txt" {
  549. t.Errorf("attachments = %+v", attachments)
  550. }
  551. header := parseHeaderBytes(built.raw)
  552. if subject, _ := header.Subject(); subject != "會議 notes" {
  553. t.Errorf("subject = %q", subject)
  554. }
  555. if references := header.Get("References"); !strings.Contains(references, "<root@example.test>") || !strings.Contains(references, "<orig@example.test>") {
  556. t.Errorf("references = %q", references)
  557. }
  558. if header.Get("X-Priority") == "" || header.Get("Importance") != "High" {
  559. t.Errorf("priority headers missing")
  560. }
  561. }
  562. func TestRemoveAccountClearsLocalState(t *testing.T) {
  563. manager := newTestManager(t)
  564. account, imapServer, _ := addTestAccount(t, manager)
  565. imapServer.appendRaw(t, "INBOX", sampleMessage("Labelled", "Bob <bob@example.test>", "x"))
  566. list, _ := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{Folder: "INBOX"})
  567. if err := manager.SetMessageLabels(testAdmin, list.Messages[0], []string{"work"}); err != nil {
  568. t.Fatalf("SetMessageLabels: %v", err)
  569. }
  570. if err := manager.RemoveAccount(testAdmin, account.ID); err != nil {
  571. t.Fatalf("RemoveAccount: %v", err)
  572. }
  573. if labelled, _ := manager.LabelMessages(testAdmin, "work"); len(labelled) != 0 {
  574. t.Errorf("labels of a removed account survived: %+v", labelled)
  575. }
  576. if _, err := manager.ListMessages(testContext(t), testAdmin, account.ID, ListQuery{}); !errors.Is(err, ErrAccountNotFound) {
  577. t.Errorf("expected ErrAccountNotFound, got %v", err)
  578. }
  579. }