folders.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. package email
  2. /*
  3. folders.go
  4. Mailbox listing, role detection and folder management.
  5. Roles come from SPECIAL-USE attributes when the server provides them
  6. (Gmail, Outlook, iCloud, Dovecot) and from well-known names otherwise.
  7. Folder lists are cached briefly per account because every delete, archive
  8. or junk action needs to know where the Trash / Archive / Junk folder is.
  9. */
  10. import (
  11. "context"
  12. "errors"
  13. "sort"
  14. "strings"
  15. "sync"
  16. "time"
  17. "github.com/emersion/go-imap/v2"
  18. "github.com/emersion/go-imap/v2/imapclient"
  19. )
  20. const folderCacheTTL = 10 * time.Minute
  21. type folderCacheEntry struct {
  22. folders []Folder
  23. loaded time.Time
  24. }
  25. // folderCache remembers each account's folder list for a few minutes.
  26. type folderCache struct {
  27. mutex sync.Mutex
  28. entries map[string]folderCacheEntry
  29. }
  30. // specialUseRoles maps lower-cased SPECIAL-USE attributes (attributes are
  31. // case-insensitive on the wire) to roles.
  32. var specialUseRoles = map[string]string{
  33. strings.ToLower(string(imap.MailboxAttrSent)): RoleSent,
  34. strings.ToLower(string(imap.MailboxAttrDrafts)): RoleDrafts,
  35. strings.ToLower(string(imap.MailboxAttrTrash)): RoleTrash,
  36. strings.ToLower(string(imap.MailboxAttrJunk)): RoleJunk,
  37. strings.ToLower(string(imap.MailboxAttrArchive)): RoleArchive,
  38. strings.ToLower(string(imap.MailboxAttrAll)): RoleAll,
  39. strings.ToLower(string(imap.MailboxAttrFlagged)): RoleFlagged,
  40. strings.ToLower(string(imap.MailboxAttrImportant)): RoleImportant,
  41. }
  42. // Folder names used by servers without SPECIAL-USE, keyed by lower case.
  43. var wellKnownFolderNames = map[string]string{
  44. "sent": RoleSent, "sent items": RoleSent, "sent messages": RoleSent, "sent mail": RoleSent,
  45. "sent-mail": RoleSent, "outbox sent": RoleSent, "已发送": RoleSent, "已傳送": RoleSent,
  46. "已發送": RoleSent, "送信済み": RoleSent, "gesendet": RoleSent, "envoyés": RoleSent,
  47. "drafts": RoleDrafts, "draft": RoleDrafts, "草稿": RoleDrafts, "草稿箱": RoleDrafts,
  48. "下書き": RoleDrafts, "entwürfe": RoleDrafts, "brouillons": RoleDrafts,
  49. "trash": RoleTrash, "deleted items": RoleTrash, "deleted messages": RoleTrash,
  50. "deleted": RoleTrash, "bin": RoleTrash, "已删除": RoleTrash, "已刪除": RoleTrash,
  51. "ゴミ箱": RoleTrash, "papierkorb": RoleTrash, "corbeille": RoleTrash,
  52. "junk": RoleJunk, "junk email": RoleJunk, "junk e-mail": RoleJunk, "spam": RoleJunk,
  53. "bulk mail": RoleJunk, "bulk": RoleJunk, "垃圾邮件": RoleJunk, "垃圾郵件": RoleJunk,
  54. "迷惑メール": RoleJunk,
  55. "archive": RoleArchive, "archives": RoleArchive, "归档": RoleArchive, "封存": RoleArchive,
  56. "all mail": RoleAll,
  57. }
  58. var roleOrder = map[string]int{
  59. RoleInbox: 0, RoleFlagged: 1, RoleDrafts: 2, RoleSent: 3, RoleArchive: 4,
  60. RoleAll: 5, RoleImportant: 6, RoleJunk: 7, RoleTrash: 8,
  61. }
  62. // Folders lists the mailboxes of an account with message counts.
  63. func (m *Manager) Folders(ctx context.Context, p Principal, accountID string, refresh bool) ([]Folder, error) {
  64. account, err := m.account(p, accountID)
  65. if err != nil {
  66. return nil, err
  67. }
  68. key := poolKey(p.Username, account.ID)
  69. if !refresh {
  70. if cached, ok := m.cachedFolders(key); ok {
  71. return cached, nil
  72. }
  73. }
  74. var folders []Folder
  75. err = m.withIMAP(ctx, p, account, func(c *imapclient.Client) error {
  76. var err error
  77. folders, err = listFolders(c, true)
  78. return err
  79. })
  80. if err != nil {
  81. return nil, err
  82. }
  83. m.clearAuthError(p, account)
  84. m.storeFolders(key, folders)
  85. return folders, nil
  86. }
  87. // foldersQuick returns the cached folder list or lists without counts, for
  88. // operations that only need to find a role folder.
  89. func (m *Manager) foldersQuick(c *imapclient.Client, key string) ([]Folder, error) {
  90. if cached, ok := m.cachedFolders(key); ok {
  91. return cached, nil
  92. }
  93. folders, err := listFolders(c, false)
  94. if err != nil {
  95. return nil, err
  96. }
  97. m.storeFolders(key, folders)
  98. return folders, nil
  99. }
  100. func (m *Manager) cachedFolders(key string) ([]Folder, bool) {
  101. m.folders.mutex.Lock()
  102. defer m.folders.mutex.Unlock()
  103. entry, ok := m.folders.entries[key]
  104. if !ok || time.Since(entry.loaded) > folderCacheTTL {
  105. return nil, false
  106. }
  107. return entry.folders, true
  108. }
  109. func (m *Manager) storeFolders(key string, folders []Folder) {
  110. m.folders.mutex.Lock()
  111. defer m.folders.mutex.Unlock()
  112. if m.folders.entries == nil {
  113. m.folders.entries = map[string]folderCacheEntry{}
  114. }
  115. m.folders.entries[key] = folderCacheEntry{folders: folders, loaded: time.Now()}
  116. }
  117. func (m *Manager) dropFolderCache(key string) {
  118. m.folders.mutex.Lock()
  119. defer m.folders.mutex.Unlock()
  120. delete(m.folders.entries, key)
  121. }
  122. // listFolders runs LIST (and STATUS where needed) on a connection.
  123. func listFolders(c *imapclient.Client, withCounts bool) ([]Folder, error) {
  124. caps := c.Caps()
  125. options := &imap.ListOptions{}
  126. if caps.Has(imap.CapSpecialUse) && (caps.Has(imap.CapListExtended) || caps.Has(imap.CapIMAP4rev2)) {
  127. options.ReturnSpecialUse = true
  128. }
  129. listStatus := withCounts && (caps.Has(imap.CapListStatus) || caps.Has(imap.CapIMAP4rev2))
  130. if listStatus {
  131. options.ReturnStatus = &imap.StatusOptions{NumMessages: true, NumUnseen: true}
  132. }
  133. mailboxes, err := c.List("", "*", options).Collect()
  134. if err != nil && options.ReturnStatus != nil {
  135. //Some servers advertise LIST-STATUS but choke on it; retry plainly
  136. options.ReturnStatus = nil
  137. listStatus = false
  138. mailboxes, err = c.List("", "*", options).Collect()
  139. }
  140. if err != nil {
  141. return nil, err
  142. }
  143. folders := make([]Folder, 0, len(mailboxes))
  144. for _, mailbox := range mailboxes {
  145. folder := Folder{
  146. Name: mailbox.Mailbox,
  147. Selectable: true,
  148. Total: -1,
  149. Unread: -1,
  150. }
  151. if mailbox.Delim != 0 {
  152. folder.Delimiter = string(mailbox.Delim)
  153. }
  154. for _, attr := range mailbox.Attrs {
  155. lower := strings.ToLower(string(attr))
  156. if lower == strings.ToLower(string(imap.MailboxAttrNoSelect)) || lower == strings.ToLower(string(imap.MailboxAttrNonExistent)) {
  157. folder.Selectable = false
  158. }
  159. if role, ok := specialUseRoles[lower]; ok && folder.Role == "" {
  160. folder.Role = role
  161. }
  162. }
  163. if strings.EqualFold(folder.Name, "INBOX") {
  164. folder.Role = RoleInbox
  165. }
  166. if mailbox.Status != nil {
  167. if mailbox.Status.NumMessages != nil {
  168. folder.Total = int(*mailbox.Status.NumMessages)
  169. }
  170. if mailbox.Status.NumUnseen != nil {
  171. folder.Unread = int(*mailbox.Status.NumUnseen)
  172. }
  173. }
  174. segments := []string{folder.Name}
  175. if folder.Delimiter != "" {
  176. segments = strings.Split(folder.Name, folder.Delimiter)
  177. }
  178. folder.Display = segments[len(segments)-1]
  179. folder.Depth = len(segments) - 1
  180. if len(segments) > 1 {
  181. folder.Parent = strings.Join(segments[:len(segments)-1], folder.Delimiter)
  182. }
  183. if folder.Role == RoleInbox {
  184. folder.Display = "Inbox"
  185. }
  186. folders = append(folders, folder)
  187. }
  188. assignWellKnownRoles(folders)
  189. if withCounts && !listStatus {
  190. fillFolderCounts(c, folders)
  191. }
  192. sortFolders(folders)
  193. return folders, nil
  194. }
  195. // assignWellKnownRoles names folders for servers without SPECIAL-USE. A role
  196. // is only assigned when no folder carries it yet, preferring shallow folders.
  197. func assignWellKnownRoles(folders []Folder) {
  198. taken := map[string]bool{}
  199. for _, folder := range folders {
  200. if folder.Role != "" {
  201. taken[folder.Role] = true
  202. }
  203. }
  204. //Top-level folders first, then "INBOX.Sent" style servers that nest
  205. //every folder under the inbox
  206. for depth := 0; depth <= 1; depth++ {
  207. for i := range folders {
  208. folder := &folders[i]
  209. if folder.Role != "" || folder.Depth != depth || !folder.Selectable {
  210. continue
  211. }
  212. if depth == 1 && !strings.EqualFold(folder.Parent, "INBOX") {
  213. continue
  214. }
  215. role, ok := wellKnownFolderNames[strings.ToLower(folder.Display)]
  216. if !ok || taken[role] {
  217. continue
  218. }
  219. folder.Role = role
  220. taken[role] = true
  221. }
  222. }
  223. }
  224. // fillFolderCounts pipelines STATUS for every selectable folder.
  225. func fillFolderCounts(c *imapclient.Client, folders []Folder) {
  226. type pending struct {
  227. index int
  228. command *imapclient.StatusCommand
  229. }
  230. commands := []pending{}
  231. for i := range folders {
  232. if !folders[i].Selectable || len(commands) >= 200 {
  233. continue
  234. }
  235. commands = append(commands, pending{index: i, command: c.Status(folders[i].Name, &imap.StatusOptions{NumMessages: true, NumUnseen: true})})
  236. }
  237. for _, item := range commands {
  238. data, err := item.command.Wait()
  239. if err != nil || data == nil {
  240. continue
  241. }
  242. if data.NumMessages != nil {
  243. folders[item.index].Total = int(*data.NumMessages)
  244. }
  245. if data.NumUnseen != nil {
  246. folders[item.index].Unread = int(*data.NumUnseen)
  247. }
  248. }
  249. }
  250. // sortFolders puts special folders first and keeps children under parents.
  251. func sortFolders(folders []Folder) {
  252. rank := func(folder Folder) int {
  253. if order, ok := roleOrder[folder.Role]; ok {
  254. return order
  255. }
  256. return 100
  257. }
  258. sort.SliceStable(folders, func(i, j int) bool {
  259. ri, rj := rank(folders[i]), rank(folders[j])
  260. if ri != rj && (ri < 100 || rj < 100) {
  261. return ri < rj
  262. }
  263. return strings.ToLower(folders[i].Name) < strings.ToLower(folders[j].Name)
  264. })
  265. }
  266. // folderByRole finds the folder playing a role.
  267. func folderByRole(folders []Folder, role string) (Folder, bool) {
  268. for _, folder := range folders {
  269. if folder.Role == role && folder.Selectable {
  270. return folder, true
  271. }
  272. }
  273. return Folder{}, false
  274. }
  275. // roleOf returns the role of a folder by name.
  276. func roleOf(folders []Folder, name string) string {
  277. for _, folder := range folders {
  278. if folder.Name == name {
  279. return folder.Role
  280. }
  281. }
  282. if strings.EqualFold(name, "INBOX") {
  283. return RoleInbox
  284. }
  285. return ""
  286. }
  287. // ensureRoleFolder finds the folder of a role, creating a top-level one with
  288. // a conventional name when the server has none.
  289. func (m *Manager) ensureRoleFolder(c *imapclient.Client, key string, role string) (string, error) {
  290. folders, err := m.foldersQuick(c, key)
  291. if err != nil {
  292. return "", err
  293. }
  294. if folder, ok := folderByRole(folders, role); ok {
  295. return folder.Name, nil
  296. }
  297. names := map[string]string{RoleTrash: "Trash", RoleArchive: "Archive", RoleJunk: "Junk", RoleSent: "Sent", RoleDrafts: "Drafts"}
  298. name, ok := names[role]
  299. if !ok {
  300. return "", errors.New("no folder for " + role)
  301. }
  302. //The INBOX.Sent convention: create under INBOX when everything lives there
  303. for _, folder := range folders {
  304. if folder.Parent != "" && strings.EqualFold(folder.Parent, "INBOX") && folder.Delimiter != "" {
  305. name = "INBOX" + folder.Delimiter + name
  306. break
  307. }
  308. }
  309. createOptions := &imap.CreateOptions{}
  310. attrs := map[string]imap.MailboxAttr{RoleTrash: imap.MailboxAttrTrash, RoleArchive: imap.MailboxAttrArchive,
  311. RoleJunk: imap.MailboxAttrJunk, RoleSent: imap.MailboxAttrSent, RoleDrafts: imap.MailboxAttrDrafts}
  312. if c.Caps().Has(imap.CapCreateSpecialUse) {
  313. createOptions.SpecialUse = []imap.MailboxAttr{attrs[role]}
  314. }
  315. if err := c.Create(name, createOptions).Wait(); err != nil {
  316. var statusErr *imap.Error
  317. if !(errors.As(err, &statusErr) && statusErr.Code == imap.ResponseCodeAlreadyExists) {
  318. return "", err
  319. }
  320. }
  321. m.dropFolderCache(key)
  322. return name, nil
  323. }
  324. // CreateFolder makes a new mailbox, optionally inside parent.
  325. func (m *Manager) CreateFolder(ctx context.Context, p Principal, accountID string, parent string, name string) (string, error) {
  326. name = strings.TrimSpace(name)
  327. if name == "" {
  328. return "", errors.New("folder name is empty")
  329. }
  330. account, err := m.account(p, accountID)
  331. if err != nil {
  332. return "", err
  333. }
  334. key := poolKey(p.Username, account.ID)
  335. full := name
  336. err = m.withIMAP(ctx, p, account, func(c *imapclient.Client) error {
  337. if parent != "" {
  338. folders, err := m.foldersQuick(c, key)
  339. if err != nil {
  340. return err
  341. }
  342. delimiter := "/"
  343. for _, folder := range folders {
  344. if folder.Name == parent && folder.Delimiter != "" {
  345. delimiter = folder.Delimiter
  346. }
  347. }
  348. if strings.Contains(name, delimiter) {
  349. return errors.New("folder name cannot contain \"" + delimiter + "\"")
  350. }
  351. full = parent + delimiter + name
  352. }
  353. err := c.Create(full, nil).Wait()
  354. if err == nil {
  355. c.Subscribe(full).Wait()
  356. }
  357. return err
  358. })
  359. m.dropFolderCache(key)
  360. return full, err
  361. }
  362. // RenameFolder renames a mailbox (keeping it under the same parent).
  363. func (m *Manager) RenameFolder(ctx context.Context, p Principal, accountID string, folder string, newName string) (string, error) {
  364. newName = strings.TrimSpace(newName)
  365. if newName == "" {
  366. return "", errors.New("folder name is empty")
  367. }
  368. account, err := m.account(p, accountID)
  369. if err != nil {
  370. return "", err
  371. }
  372. key := poolKey(p.Username, account.ID)
  373. full := newName
  374. err = m.withIMAP(ctx, p, account, func(c *imapclient.Client) error {
  375. folders, err := m.foldersQuick(c, key)
  376. if err != nil {
  377. return err
  378. }
  379. var current *Folder
  380. for i := range folders {
  381. if folders[i].Name == folder {
  382. current = &folders[i]
  383. }
  384. }
  385. if current == nil {
  386. return ErrFolderNotFound
  387. }
  388. if current.Role != "" {
  389. return errors.New("special folders cannot be renamed")
  390. }
  391. if current.Parent != "" {
  392. full = current.Parent + current.Delimiter + newName
  393. }
  394. return c.Rename(folder, full, nil).Wait()
  395. })
  396. m.dropFolderCache(key)
  397. return full, err
  398. }
  399. // DeleteFolder removes a mailbox and its messages.
  400. func (m *Manager) DeleteFolder(ctx context.Context, p Principal, accountID string, folder string) error {
  401. account, err := m.account(p, accountID)
  402. if err != nil {
  403. return err
  404. }
  405. key := poolKey(p.Username, account.ID)
  406. err = m.withIMAP(ctx, p, account, func(c *imapclient.Client) error {
  407. folders, err := m.foldersQuick(c, key)
  408. if err != nil {
  409. return err
  410. }
  411. if role := roleOf(folders, folder); role != "" {
  412. return errors.New("special folders cannot be deleted")
  413. }
  414. if selected := c.Mailbox(); selected != nil && selected.Name == folder {
  415. c.Unselect().Wait()
  416. }
  417. c.Unsubscribe(folder).Wait()
  418. return c.Delete(folder).Wait()
  419. })
  420. m.dropFolderCache(key)
  421. return err
  422. }