@bobfrankston/mailx 1.0.465 → 1.0.500

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/.globalize.json5 +25 -0
  2. package/README.md +17 -420
  3. package/bin/mailx.js +87 -84
  4. package/bin/mailx.js.map +1 -1
  5. package/bin/mailx.ts +87 -84
  6. package/client/android.html +5 -5
  7. package/client/app.js +42 -38
  8. package/client/components/folder-tree.js +7 -5
  9. package/client/components/message-list.js +485 -448
  10. package/client/components/message-viewer.js +36 -41
  11. package/client/index.html +8 -8
  12. package/client/lib/message-state.js +46 -65
  13. package/index.js +59 -0
  14. package/package.json +12 -114
  15. package/packages/mailx-send/mailsend/{package-lock.json → node_modules/.package-lock.json} +0 -16
  16. package/packages/mailx-send/mailsend/node_modules/@types/node/LICENSE +21 -0
  17. package/packages/mailx-send/mailsend/node_modules/@types/node/README.md +15 -0
  18. package/packages/mailx-send/mailsend/node_modules/@types/node/assert/strict.d.ts +111 -0
  19. package/packages/mailx-send/mailsend/node_modules/@types/node/assert.d.ts +1078 -0
  20. package/packages/mailx-send/mailsend/node_modules/@types/node/async_hooks.d.ts +603 -0
  21. package/packages/mailx-send/mailsend/node_modules/@types/node/buffer.buffer.d.ts +472 -0
  22. package/packages/mailx-send/mailsend/node_modules/@types/node/buffer.d.ts +1934 -0
  23. package/packages/mailx-send/mailsend/node_modules/@types/node/child_process.d.ts +1476 -0
  24. package/packages/mailx-send/mailsend/node_modules/@types/node/cluster.d.ts +578 -0
  25. package/packages/mailx-send/mailsend/node_modules/@types/node/compatibility/disposable.d.ts +14 -0
  26. package/packages/mailx-send/mailsend/node_modules/@types/node/compatibility/index.d.ts +9 -0
  27. package/packages/mailx-send/mailsend/node_modules/@types/node/compatibility/indexable.d.ts +20 -0
  28. package/packages/mailx-send/mailsend/node_modules/@types/node/compatibility/iterators.d.ts +20 -0
  29. package/packages/mailx-send/mailsend/node_modules/@types/node/console.d.ts +452 -0
  30. package/packages/mailx-send/mailsend/node_modules/@types/node/constants.d.ts +21 -0
  31. package/packages/mailx-send/mailsend/node_modules/@types/node/crypto.d.ts +4545 -0
  32. package/packages/mailx-send/mailsend/node_modules/@types/node/dgram.d.ts +600 -0
  33. package/packages/mailx-send/mailsend/node_modules/@types/node/diagnostics_channel.d.ts +578 -0
  34. package/packages/mailx-send/mailsend/node_modules/@types/node/dns/promises.d.ts +503 -0
  35. package/packages/mailx-send/mailsend/node_modules/@types/node/dns.d.ts +923 -0
  36. package/packages/mailx-send/mailsend/node_modules/@types/node/domain.d.ts +170 -0
  37. package/packages/mailx-send/mailsend/node_modules/@types/node/events.d.ts +976 -0
  38. package/packages/mailx-send/mailsend/node_modules/@types/node/fs/promises.d.ts +1295 -0
  39. package/packages/mailx-send/mailsend/node_modules/@types/node/fs.d.ts +4461 -0
  40. package/packages/mailx-send/mailsend/node_modules/@types/node/globals.d.ts +172 -0
  41. package/packages/mailx-send/mailsend/node_modules/@types/node/globals.typedarray.d.ts +38 -0
  42. package/packages/mailx-send/mailsend/node_modules/@types/node/http.d.ts +2089 -0
  43. package/packages/mailx-send/mailsend/node_modules/@types/node/http2.d.ts +2644 -0
  44. package/packages/mailx-send/mailsend/node_modules/@types/node/https.d.ts +579 -0
  45. package/packages/mailx-send/mailsend/node_modules/@types/node/index.d.ts +97 -0
  46. package/packages/mailx-send/mailsend/node_modules/@types/node/inspector.d.ts +253 -0
  47. package/packages/mailx-send/mailsend/node_modules/@types/node/inspector.generated.d.ts +4052 -0
  48. package/packages/mailx-send/mailsend/node_modules/@types/node/module.d.ts +891 -0
  49. package/packages/mailx-send/mailsend/node_modules/@types/node/net.d.ts +1057 -0
  50. package/packages/mailx-send/mailsend/node_modules/@types/node/os.d.ts +506 -0
  51. package/packages/mailx-send/mailsend/node_modules/@types/node/package.json +145 -0
  52. package/packages/mailx-send/mailsend/node_modules/@types/node/path.d.ts +200 -0
  53. package/packages/mailx-send/mailsend/node_modules/@types/node/perf_hooks.d.ts +968 -0
  54. package/packages/mailx-send/mailsend/node_modules/@types/node/process.d.ts +2084 -0
  55. package/packages/mailx-send/mailsend/node_modules/@types/node/punycode.d.ts +117 -0
  56. package/packages/mailx-send/mailsend/node_modules/@types/node/querystring.d.ts +152 -0
  57. package/packages/mailx-send/mailsend/node_modules/@types/node/readline/promises.d.ts +161 -0
  58. package/packages/mailx-send/mailsend/node_modules/@types/node/readline.d.ts +594 -0
  59. package/packages/mailx-send/mailsend/node_modules/@types/node/repl.d.ts +428 -0
  60. package/packages/mailx-send/mailsend/node_modules/@types/node/sea.d.ts +153 -0
  61. package/packages/mailx-send/mailsend/node_modules/@types/node/sqlite.d.ts +721 -0
  62. package/packages/mailx-send/mailsend/node_modules/@types/node/stream/consumers.d.ts +38 -0
  63. package/packages/mailx-send/mailsend/node_modules/@types/node/stream/promises.d.ts +90 -0
  64. package/packages/mailx-send/mailsend/node_modules/@types/node/stream/web.d.ts +622 -0
  65. package/packages/mailx-send/mailsend/node_modules/@types/node/stream.d.ts +1664 -0
  66. package/packages/mailx-send/mailsend/node_modules/@types/node/string_decoder.d.ts +67 -0
  67. package/packages/mailx-send/mailsend/node_modules/@types/node/test.d.ts +2163 -0
  68. package/packages/mailx-send/mailsend/node_modules/@types/node/timers/promises.d.ts +108 -0
  69. package/packages/mailx-send/mailsend/node_modules/@types/node/timers.d.ts +287 -0
  70. package/packages/mailx-send/mailsend/node_modules/@types/node/tls.d.ts +1319 -0
  71. package/packages/mailx-send/mailsend/node_modules/@types/node/trace_events.d.ts +197 -0
  72. package/packages/mailx-send/mailsend/node_modules/@types/node/ts5.6/buffer.buffer.d.ts +468 -0
  73. package/packages/mailx-send/mailsend/node_modules/@types/node/ts5.6/globals.typedarray.d.ts +34 -0
  74. package/packages/mailx-send/mailsend/node_modules/@types/node/ts5.6/index.d.ts +97 -0
  75. package/packages/mailx-send/mailsend/node_modules/@types/node/tty.d.ts +208 -0
  76. package/packages/mailx-send/mailsend/node_modules/@types/node/url.d.ts +984 -0
  77. package/packages/mailx-send/mailsend/node_modules/@types/node/util.d.ts +2606 -0
  78. package/packages/mailx-send/mailsend/node_modules/@types/node/v8.d.ts +920 -0
  79. package/packages/mailx-send/mailsend/node_modules/@types/node/vm.d.ts +1000 -0
  80. package/packages/mailx-send/mailsend/node_modules/@types/node/wasi.d.ts +181 -0
  81. package/packages/mailx-send/mailsend/node_modules/@types/node/web-globals/abortcontroller.d.ts +34 -0
  82. package/packages/mailx-send/mailsend/node_modules/@types/node/web-globals/domexception.d.ts +68 -0
  83. package/packages/mailx-send/mailsend/node_modules/@types/node/web-globals/events.d.ts +97 -0
  84. package/packages/mailx-send/mailsend/node_modules/@types/node/web-globals/fetch.d.ts +55 -0
  85. package/packages/mailx-send/mailsend/node_modules/@types/node/web-globals/navigator.d.ts +22 -0
  86. package/packages/mailx-send/mailsend/node_modules/@types/node/web-globals/storage.d.ts +24 -0
  87. package/packages/mailx-send/mailsend/node_modules/@types/node/worker_threads.d.ts +784 -0
  88. package/packages/mailx-send/mailsend/node_modules/@types/node/zlib.d.ts +747 -0
  89. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/LICENSE +21 -0
  90. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/README.md +15 -0
  91. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/index.d.ts +82 -0
  92. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/addressparser/index.d.ts +31 -0
  93. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/base64/index.d.ts +22 -0
  94. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/dkim/index.d.ts +45 -0
  95. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/dkim/message-parser.d.ts +75 -0
  96. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/dkim/relaxed-body.d.ts +75 -0
  97. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/dkim/sign.d.ts +21 -0
  98. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/fetch/cookies.d.ts +54 -0
  99. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/fetch/index.d.ts +38 -0
  100. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/json-transport/index.d.ts +53 -0
  101. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mail-composer/index.d.ts +25 -0
  102. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mailer/index.d.ts +283 -0
  103. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mailer/mail-message.d.ts +32 -0
  104. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mime-funcs/index.d.ts +87 -0
  105. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mime-funcs/mime-types.d.ts +2 -0
  106. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mime-node/index.d.ts +224 -0
  107. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/mime-node/last-newline.d.ts +9 -0
  108. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/qp/index.d.ts +23 -0
  109. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/sendmail-transport/index.d.ts +53 -0
  110. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/sendmail-transport/le-unix.d.ts +7 -0
  111. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/sendmail-transport/le-windows.d.ts +7 -0
  112. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/ses-transport/index.d.ts +146 -0
  113. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/shared/index.d.ts +58 -0
  114. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/smtp-connection/data-stream.d.ts +11 -0
  115. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/smtp-connection/http-proxy-client.d.ts +16 -0
  116. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/smtp-connection/index.d.ts +270 -0
  117. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/smtp-pool/index.d.ts +93 -0
  118. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/smtp-pool/pool-resource.d.ts +66 -0
  119. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/smtp-transport/index.d.ts +115 -0
  120. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/stream-transport/index.d.ts +59 -0
  121. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/well-known/index.d.ts +6 -0
  122. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/lib/xoauth2/index.d.ts +114 -0
  123. package/packages/mailx-send/mailsend/node_modules/@types/nodemailer/package.json +38 -0
  124. package/packages/mailx-send/mailsend/node_modules/nodemailer/.ncurc.js +9 -0
  125. package/packages/mailx-send/mailsend/node_modules/nodemailer/.prettierignore +8 -0
  126. package/packages/mailx-send/mailsend/node_modules/nodemailer/.prettierrc +12 -0
  127. package/packages/mailx-send/mailsend/node_modules/nodemailer/.prettierrc.js +10 -0
  128. package/packages/mailx-send/mailsend/node_modules/nodemailer/.release-please-config.json +9 -0
  129. package/packages/mailx-send/mailsend/node_modules/nodemailer/LICENSE +16 -0
  130. package/packages/mailx-send/mailsend/node_modules/nodemailer/README.md +86 -0
  131. package/packages/mailx-send/mailsend/node_modules/nodemailer/SECURITY.txt +22 -0
  132. package/packages/mailx-send/mailsend/node_modules/nodemailer/eslint.config.js +88 -0
  133. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/addressparser/index.js +383 -0
  134. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/base64/index.js +139 -0
  135. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/dkim/index.js +253 -0
  136. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/dkim/message-parser.js +155 -0
  137. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/dkim/relaxed-body.js +154 -0
  138. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/dkim/sign.js +117 -0
  139. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/fetch/cookies.js +281 -0
  140. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/fetch/index.js +280 -0
  141. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/json-transport/index.js +82 -0
  142. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mail-composer/index.js +629 -0
  143. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mailer/index.js +441 -0
  144. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mailer/mail-message.js +316 -0
  145. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mime-funcs/index.js +625 -0
  146. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mime-funcs/mime-types.js +2113 -0
  147. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mime-node/index.js +1316 -0
  148. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mime-node/last-newline.js +33 -0
  149. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mime-node/le-unix.js +43 -0
  150. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/mime-node/le-windows.js +52 -0
  151. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/nodemailer.js +157 -0
  152. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/punycode/index.js +460 -0
  153. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/qp/index.js +227 -0
  154. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/sendmail-transport/index.js +210 -0
  155. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/ses-transport/index.js +234 -0
  156. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/shared/index.js +754 -0
  157. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/smtp-connection/data-stream.js +108 -0
  158. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/smtp-connection/http-proxy-client.js +143 -0
  159. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/smtp-connection/index.js +1870 -0
  160. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/smtp-pool/index.js +652 -0
  161. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/smtp-pool/pool-resource.js +259 -0
  162. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/smtp-transport/index.js +421 -0
  163. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/stream-transport/index.js +135 -0
  164. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/well-known/index.js +47 -0
  165. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/well-known/services.json +611 -0
  166. package/packages/mailx-send/mailsend/node_modules/nodemailer/lib/xoauth2/index.js +427 -0
  167. package/packages/mailx-send/mailsend/node_modules/nodemailer/package.json +47 -0
  168. package/packages/mailx-send/mailsend/node_modules/undici-types/LICENSE +21 -0
  169. package/packages/mailx-send/mailsend/node_modules/undici-types/README.md +6 -0
  170. package/packages/mailx-send/mailsend/node_modules/undici-types/agent.d.ts +31 -0
  171. package/packages/mailx-send/mailsend/node_modules/undici-types/api.d.ts +43 -0
  172. package/packages/mailx-send/mailsend/node_modules/undici-types/balanced-pool.d.ts +29 -0
  173. package/packages/mailx-send/mailsend/node_modules/undici-types/cache.d.ts +36 -0
  174. package/packages/mailx-send/mailsend/node_modules/undici-types/client.d.ts +108 -0
  175. package/packages/mailx-send/mailsend/node_modules/undici-types/connector.d.ts +34 -0
  176. package/packages/mailx-send/mailsend/node_modules/undici-types/content-type.d.ts +21 -0
  177. package/packages/mailx-send/mailsend/node_modules/undici-types/cookies.d.ts +28 -0
  178. package/packages/mailx-send/mailsend/node_modules/undici-types/diagnostics-channel.d.ts +66 -0
  179. package/packages/mailx-send/mailsend/node_modules/undici-types/dispatcher.d.ts +256 -0
  180. package/packages/mailx-send/mailsend/node_modules/undici-types/env-http-proxy-agent.d.ts +21 -0
  181. package/packages/mailx-send/mailsend/node_modules/undici-types/errors.d.ts +149 -0
  182. package/packages/mailx-send/mailsend/node_modules/undici-types/eventsource.d.ts +61 -0
  183. package/packages/mailx-send/mailsend/node_modules/undici-types/fetch.d.ts +209 -0
  184. package/packages/mailx-send/mailsend/node_modules/undici-types/file.d.ts +39 -0
  185. package/packages/mailx-send/mailsend/node_modules/undici-types/filereader.d.ts +54 -0
  186. package/packages/mailx-send/mailsend/node_modules/undici-types/formdata.d.ts +108 -0
  187. package/packages/mailx-send/mailsend/node_modules/undici-types/global-dispatcher.d.ts +9 -0
  188. package/packages/mailx-send/mailsend/node_modules/undici-types/global-origin.d.ts +7 -0
  189. package/packages/mailx-send/mailsend/node_modules/undici-types/handlers.d.ts +15 -0
  190. package/packages/mailx-send/mailsend/node_modules/undici-types/header.d.ts +4 -0
  191. package/packages/mailx-send/mailsend/node_modules/undici-types/index.d.ts +71 -0
  192. package/packages/mailx-send/mailsend/node_modules/undici-types/interceptors.d.ts +17 -0
  193. package/packages/mailx-send/mailsend/node_modules/undici-types/mock-agent.d.ts +50 -0
  194. package/packages/mailx-send/mailsend/node_modules/undici-types/mock-client.d.ts +25 -0
  195. package/packages/mailx-send/mailsend/node_modules/undici-types/mock-errors.d.ts +12 -0
  196. package/packages/mailx-send/mailsend/node_modules/undici-types/mock-interceptor.d.ts +93 -0
  197. package/packages/mailx-send/mailsend/node_modules/undici-types/mock-pool.d.ts +25 -0
  198. package/packages/mailx-send/mailsend/node_modules/undici-types/package.json +55 -0
  199. package/packages/mailx-send/mailsend/node_modules/undici-types/patch.d.ts +33 -0
  200. package/packages/mailx-send/mailsend/node_modules/undici-types/pool-stats.d.ts +19 -0
  201. package/packages/mailx-send/mailsend/node_modules/undici-types/pool.d.ts +39 -0
  202. package/packages/mailx-send/mailsend/node_modules/undici-types/proxy-agent.d.ts +28 -0
  203. package/packages/mailx-send/mailsend/node_modules/undici-types/readable.d.ts +65 -0
  204. package/packages/mailx-send/mailsend/node_modules/undici-types/retry-agent.d.ts +8 -0
  205. package/packages/mailx-send/mailsend/node_modules/undici-types/retry-handler.d.ts +116 -0
  206. package/packages/mailx-send/mailsend/node_modules/undici-types/util.d.ts +18 -0
  207. package/packages/mailx-send/mailsend/node_modules/undici-types/webidl.d.ts +228 -0
  208. package/packages/mailx-send/mailsend/node_modules/undici-types/websocket.d.ts +150 -0
  209. package/packages/mailx-server/index.js +1 -1
  210. package/packages/mailx-settings/cloud.js +12 -7
  211. package/packages/mailx-settings/index.js +22 -1
  212. package/client/.gitattributes +0 -10
  213. package/client/app.js.map +0 -1
  214. package/client/app.ts +0 -3190
  215. package/client/components/address-book.js.map +0 -1
  216. package/client/components/address-book.ts +0 -204
  217. package/client/components/alarms.js.map +0 -1
  218. package/client/components/alarms.ts +0 -276
  219. package/client/components/calendar-sidebar.js.map +0 -1
  220. package/client/components/calendar-sidebar.ts +0 -474
  221. package/client/components/calendar.js.map +0 -1
  222. package/client/components/calendar.ts +0 -211
  223. package/client/components/context-menu.js.map +0 -1
  224. package/client/components/context-menu.ts +0 -95
  225. package/client/components/folder-picker.js.map +0 -1
  226. package/client/components/folder-picker.ts +0 -127
  227. package/client/components/folder-tree.js.map +0 -1
  228. package/client/components/folder-tree.ts +0 -1069
  229. package/client/components/message-list.js.map +0 -1
  230. package/client/components/message-list.ts +0 -1129
  231. package/client/components/message-viewer.js.map +0 -1
  232. package/client/components/message-viewer.ts +0 -1257
  233. package/client/components/outbox-view.js.map +0 -1
  234. package/client/components/outbox-view.ts +0 -102
  235. package/client/components/tasks.js.map +0 -1
  236. package/client/components/tasks.ts +0 -234
  237. package/client/compose/compose.js.map +0 -1
  238. package/client/compose/compose.ts +0 -1231
  239. package/client/compose/editor.js.map +0 -1
  240. package/client/compose/editor.ts +0 -599
  241. package/client/compose/ghost-text.js.map +0 -1
  242. package/client/compose/ghost-text.ts +0 -140
  243. package/client/lib/android-bootstrap.js.map +0 -1
  244. package/client/lib/android-bootstrap.ts +0 -9
  245. package/client/lib/api-client.js.map +0 -1
  246. package/client/lib/api-client.ts +0 -439
  247. package/client/lib/local-service.js.map +0 -1
  248. package/client/lib/local-service.ts +0 -646
  249. package/client/lib/local-store.js.map +0 -1
  250. package/client/lib/local-store.ts +0 -283
  251. package/client/lib/message-state.js.map +0 -1
  252. package/client/lib/message-state.ts +0 -160
  253. package/client/tsconfig.json +0 -19
  254. package/packages/mailx-api/.gitattributes +0 -10
  255. package/packages/mailx-api/index.d.ts.map +0 -1
  256. package/packages/mailx-api/index.js.map +0 -1
  257. package/packages/mailx-api/index.ts +0 -283
  258. package/packages/mailx-api/tsconfig.json +0 -9
  259. package/packages/mailx-compose/.gitattributes +0 -10
  260. package/packages/mailx-compose/index.d.ts.map +0 -1
  261. package/packages/mailx-compose/index.js.map +0 -1
  262. package/packages/mailx-compose/index.ts +0 -85
  263. package/packages/mailx-compose/tsconfig.json +0 -9
  264. package/packages/mailx-host/.gitattributes +0 -10
  265. package/packages/mailx-host/index.d.ts +0 -21
  266. package/packages/mailx-host/index.d.ts.map +0 -1
  267. package/packages/mailx-host/index.js +0 -29
  268. package/packages/mailx-host/index.js.map +0 -1
  269. package/packages/mailx-host/index.ts +0 -38
  270. package/packages/mailx-host/package.json +0 -23
  271. package/packages/mailx-host/tsconfig.json +0 -9
  272. package/packages/mailx-host/types-shim.d.ts +0 -14
  273. package/packages/mailx-imap/.gitattributes +0 -10
  274. package/packages/mailx-imap/index.d.ts +0 -442
  275. package/packages/mailx-imap/index.d.ts.map +0 -1
  276. package/packages/mailx-imap/index.js +0 -3684
  277. package/packages/mailx-imap/index.js.map +0 -1
  278. package/packages/mailx-imap/index.ts +0 -3652
  279. package/packages/mailx-imap/package-lock.json +0 -131
  280. package/packages/mailx-imap/package.json +0 -28
  281. package/packages/mailx-imap/providers/gmail-api.d.ts +0 -8
  282. package/packages/mailx-imap/providers/gmail-api.d.ts.map +0 -1
  283. package/packages/mailx-imap/providers/gmail-api.js +0 -8
  284. package/packages/mailx-imap/providers/gmail-api.js.map +0 -1
  285. package/packages/mailx-imap/providers/gmail-api.ts +0 -8
  286. package/packages/mailx-imap/providers/outlook-api.ts +0 -7
  287. package/packages/mailx-imap/providers/types.d.ts +0 -9
  288. package/packages/mailx-imap/providers/types.d.ts.map +0 -1
  289. package/packages/mailx-imap/providers/types.js +0 -9
  290. package/packages/mailx-imap/providers/types.js.map +0 -1
  291. package/packages/mailx-imap/providers/types.ts +0 -9
  292. package/packages/mailx-imap/tsconfig.json +0 -9
  293. package/packages/mailx-imap/tsconfig.tsbuildinfo +0 -1
  294. package/packages/mailx-send/.gitattributes +0 -10
  295. package/packages/mailx-send/cli-queue.d.ts.map +0 -1
  296. package/packages/mailx-send/cli-queue.js.map +0 -1
  297. package/packages/mailx-send/cli-queue.ts +0 -62
  298. package/packages/mailx-send/cli-send.d.ts.map +0 -1
  299. package/packages/mailx-send/cli-send.js.map +0 -1
  300. package/packages/mailx-send/cli-send.ts +0 -83
  301. package/packages/mailx-send/cli.d.ts.map +0 -1
  302. package/packages/mailx-send/cli.js.map +0 -1
  303. package/packages/mailx-send/cli.ts +0 -126
  304. package/packages/mailx-send/index.d.ts.map +0 -1
  305. package/packages/mailx-send/index.js.map +0 -1
  306. package/packages/mailx-send/index.ts +0 -333
  307. package/packages/mailx-send/mailsend/cli.d.ts.map +0 -1
  308. package/packages/mailx-send/mailsend/cli.js.map +0 -1
  309. package/packages/mailx-send/mailsend/cli.ts +0 -81
  310. package/packages/mailx-send/mailsend/index.d.ts.map +0 -1
  311. package/packages/mailx-send/mailsend/index.js.map +0 -1
  312. package/packages/mailx-send/mailsend/index.ts +0 -333
  313. package/packages/mailx-send/mailsend/tsconfig.json +0 -21
  314. package/packages/mailx-send/package-lock.json +0 -65
  315. package/packages/mailx-send/tsconfig.json +0 -21
  316. package/packages/mailx-server/.gitattributes +0 -10
  317. package/packages/mailx-server/index.d.ts.map +0 -1
  318. package/packages/mailx-server/index.js.map +0 -1
  319. package/packages/mailx-server/index.ts +0 -429
  320. package/packages/mailx-server/tsconfig.json +0 -9
  321. package/packages/mailx-settings/.gitattributes +0 -10
  322. package/packages/mailx-settings/cloud.d.ts.map +0 -1
  323. package/packages/mailx-settings/cloud.js.map +0 -1
  324. package/packages/mailx-settings/cloud.ts +0 -388
  325. package/packages/mailx-settings/index.d.ts.map +0 -1
  326. package/packages/mailx-settings/index.js.map +0 -1
  327. package/packages/mailx-settings/index.ts +0 -892
  328. package/packages/mailx-settings/tsconfig.json +0 -9
  329. package/packages/mailx-store/.gitattributes +0 -10
  330. package/packages/mailx-store/db.d.ts.map +0 -1
  331. package/packages/mailx-store/db.js.map +0 -1
  332. package/packages/mailx-store/db.ts +0 -2007
  333. package/packages/mailx-store/file-store.d.ts.map +0 -1
  334. package/packages/mailx-store/file-store.js.map +0 -1
  335. package/packages/mailx-store/file-store.ts +0 -82
  336. package/packages/mailx-store/index.d.ts.map +0 -1
  337. package/packages/mailx-store/index.js.map +0 -1
  338. package/packages/mailx-store/index.ts +0 -7
  339. package/packages/mailx-store/tsconfig.json +0 -9
  340. package/packages/mailx-types/.gitattributes +0 -10
  341. package/packages/mailx-types/index.d.ts.map +0 -1
  342. package/packages/mailx-types/index.js.map +0 -1
  343. package/packages/mailx-types/index.ts +0 -498
  344. package/packages/mailx-types/tsconfig.json +0 -9
@@ -1,1069 +0,0 @@
1
- /**
2
- * Folder tree component -- renders account folders with hierarchy,
3
- * expand/collapse, and optional unified inbox.
4
- */
5
-
6
- import { getAccounts, getFolders, moveMessage, moveMessages, markFolderRead, createFolder, renameFolder, deleteFolder, emptyFolder, setupAccount, getDeviceAccounts, getVersion, syncAccount } from "../lib/api-client.js";
7
- import { showContextMenu, type MenuItem } from "./context-menu.js";
8
-
9
- type FolderSelectHandler = (accountId: string, folderId: number, folderName: string, specialUse: string) => void;
10
- // Unified inbox uses folderId = -1 as a sentinel
11
- type UnifiedHandler = () => void;
12
-
13
- let onFolderSelect: FolderSelectHandler;
14
- let onUnifiedInbox: UnifiedHandler | null = null;
15
- let selectedElement: HTMLElement;
16
- let selectedAccountId: string | null = null;
17
- let selectedFolderId: number | null = null;
18
- let isFirstLoad = true; // only auto-select on first load
19
- let hasAutoSelected = false; // track whether we've ever managed to auto-select
20
-
21
- // Debounce timer for refreshFolderTree
22
- let refreshDebounceTimer: ReturnType<typeof setTimeout> | null = null;
23
-
24
- // Persist expand/collapse state in localStorage
25
- const expandState: Record<string, boolean> = JSON.parse(localStorage.getItem("mailx-folders-expanded") || "{}");
26
-
27
- function saveExpandState(): void {
28
- localStorage.setItem("mailx-folders-expanded", JSON.stringify(expandState));
29
- }
30
-
31
- // Last-sync tracking: populated by folderSynced events. Memory-only; fresh
32
- // restarts start with an empty map and fill in as syncs happen.
33
- const folderLastSync = new Map<string, number>(); // key = `${accountId}:${folderId}`
34
-
35
- function syncKey(accountId: string, folderId: number): string {
36
- return `${accountId}:${folderId}`;
37
- }
38
-
39
- export function setFolderSynced(accountId: string, folderId: number, syncedAt: number): void {
40
- folderLastSync.set(syncKey(accountId, folderId), syncedAt);
41
- // Update the row in place if rendered — avoids a full re-render.
42
- const el = document.querySelector<HTMLElement>(`.ft-folder[data-account-id="${CSS.escape(accountId)}"][data-folder-id="${folderId}"]`);
43
- if (el) applyFreshness(el, syncedAt);
44
- }
45
-
46
- export function getFolderSynced(accountId: string, folderId: number): number | undefined {
47
- return folderLastSync.get(syncKey(accountId, folderId));
48
- }
49
-
50
- function formatAge(ms: number): string {
51
- const secs = Math.round(ms / 1000);
52
- if (secs < 60) return `${secs}s ago`;
53
- const mins = Math.round(secs / 60);
54
- if (mins < 60) return `${mins}m ago`;
55
- const hours = Math.round(mins / 60);
56
- if (hours < 24) return `${hours}h ago`;
57
- return `${Math.round(hours / 24)}d ago`;
58
- }
59
-
60
- function freshnessClass(ageMs: number): string {
61
- if (ageMs < 5 * 60_000) return "fresh"; // green
62
- if (ageMs < 30 * 60_000) return "stale-soft"; // yellow
63
- return "stale"; // red
64
- }
65
-
66
- function applyFreshness(el: HTMLElement, syncedAt: number): void {
67
- const age = Date.now() - syncedAt;
68
- el.classList.remove("fresh", "stale-soft", "stale");
69
- el.classList.add(freshnessClass(age));
70
- el.title = `Last synced: ${formatAge(age)} (${new Date(syncedAt).toLocaleTimeString()})`;
71
- }
72
-
73
- interface FolderNode {
74
- id: number;
75
- accountId: string;
76
- path: string;
77
- name: string;
78
- specialUse: string;
79
- delimiter: string;
80
- unreadCount: number;
81
- totalCount: number;
82
- children: FolderNode[];
83
- }
84
-
85
- /** Build a tree from flat folder list using delimiter */
86
- function buildTree(folders: any[], delimiter: string, accountId: string): FolderNode[] {
87
- const root: FolderNode[] = [];
88
- const byPath: Record<string, FolderNode> = {};
89
-
90
- // Sort by path so parents come before children
91
- const sorted = [...folders].sort((a, b) => a.path.localeCompare(b.path));
92
-
93
- for (const f of sorted) {
94
- const node: FolderNode = {
95
- id: f.id,
96
- accountId: f.accountId,
97
- path: f.path,
98
- name: f.name,
99
- specialUse: f.specialUse,
100
- delimiter: f.delimiter || delimiter,
101
- unreadCount: f.unreadCount || 0,
102
- totalCount: f.totalCount || 0,
103
- children: [],
104
- };
105
- byPath[f.path] = node;
106
-
107
- // Find parent by stripping the last segment
108
- const lastDelim = f.path.lastIndexOf(delimiter);
109
- if (lastDelim > 0) {
110
- const parentPath = f.path.substring(0, lastDelim);
111
- let parent = byPath[parentPath];
112
- if (!parent) {
113
- // Create virtual parent for non-selectable folders (e.g., "Added2")
114
- const parentName = parentPath.split(delimiter).pop() || parentPath;
115
- parent = {
116
- id: -1, // virtual, not selectable
117
- accountId,
118
- path: parentPath,
119
- name: parentName,
120
- specialUse: "",
121
- delimiter,
122
- unreadCount: 0,
123
- totalCount: 0,
124
- children: [],
125
- };
126
- byPath[parentPath] = parent;
127
- // Insert the virtual parent into the tree
128
- const grandParentDelim = parentPath.lastIndexOf(delimiter);
129
- if (grandParentDelim > 0) {
130
- const grandParent = byPath[parentPath.substring(0, grandParentDelim)];
131
- if (grandParent) {
132
- grandParent.children.push(parent);
133
- } else {
134
- root.push(parent);
135
- }
136
- } else {
137
- root.push(parent);
138
- }
139
- }
140
- parent.children.push(node);
141
- } else {
142
- root.push(node);
143
- }
144
- }
145
-
146
- // Aggregate counts from children to parents (so collapsed parents show totals)
147
- function aggregateCounts(nodes: FolderNode[]): { unread: number; total: number } {
148
- let unread = 0, total = 0;
149
- for (const n of nodes) {
150
- const child = aggregateCounts(n.children);
151
- n.unreadCount += child.unread;
152
- n.totalCount += child.total;
153
- unread += n.unreadCount;
154
- total += n.totalCount;
155
- }
156
- return { unread, total };
157
- }
158
- aggregateCounts(root);
159
-
160
- return root;
161
- }
162
-
163
- /** Sort: INBOX first, then special folders, then alphabetical */
164
- function sortFolders(nodes: FolderNode[]): void {
165
- const specialOrder: Record<string, number> = { inbox: 0, sent: 1, outbox: 2, drafts: 3, trash: 4, junk: 5, archive: 6 };
166
- const nameOrder: Record<string, number> = { inbox: 0, sent: 1, "sent items": 1, outbox: 2, drafts: 3, trash: 4, "deleted items": 4, junk: 5, "junk email": 5, "junk e-mail": 5, spam: 5, archive: 6 };
167
- nodes.sort((a, b) => {
168
- const aOrder = specialOrder[a.specialUse] ?? nameOrder[a.name.toLowerCase()] ?? 99;
169
- const bOrder = specialOrder[b.specialUse] ?? nameOrder[b.name.toLowerCase()] ?? 99;
170
- if (aOrder !== bOrder) return aOrder - bOrder;
171
- return a.name.localeCompare(b.name);
172
- });
173
- for (const n of nodes) {
174
- if (n.children.length > 0) sortFolders(n.children);
175
- }
176
- }
177
-
178
- /** Render a folder node and its children recursively */
179
- function renderNode(node: FolderNode, container: HTMLElement, depth: number): void {
180
- const hasChildren = node.children.length > 0;
181
- const expandKey = `${node.accountId}:${node.path}`;
182
- const isExpanded = expandState[expandKey] === true; // default collapsed
183
-
184
- const folderEl = document.createElement("div");
185
- folderEl.className = "ft-folder";
186
- folderEl.dataset.accountId = node.accountId;
187
- folderEl.dataset.folderId = String(node.id);
188
- folderEl.dataset.folderPath = node.path;
189
- folderEl.dataset.specialUse = node.specialUse || "";
190
- folderEl.style.paddingLeft = `${depth * 16 + 8}px`;
191
-
192
- // Expand/collapse toggle
193
- const toggle = document.createElement("span");
194
- toggle.className = "ft-toggle";
195
- if (hasChildren) {
196
- toggle.textContent = isExpanded ? "▾" : "▸";
197
- toggle.addEventListener("click", (e) => {
198
- e.stopPropagation();
199
- expandState[expandKey] = !isExpanded;
200
- saveExpandState();
201
- // Re-render the tree
202
- const treeContainer = document.getElementById("folder-tree");
203
- if (treeContainer) loadFolderTree(treeContainer);
204
- });
205
- } else {
206
- toggle.textContent = " ";
207
- }
208
- folderEl.appendChild(toggle);
209
-
210
- const freshnessDot = document.createElement("span");
211
- freshnessDot.className = "ft-freshness";
212
- freshnessDot.setAttribute("aria-hidden", "true");
213
- folderEl.appendChild(freshnessDot);
214
-
215
- const nameSpan = document.createElement("span");
216
- nameSpan.className = "ft-folder-name";
217
- nameSpan.textContent = node.name;
218
- folderEl.appendChild(nameSpan);
219
-
220
- const syncedAt = getFolderSynced(node.accountId, node.id);
221
- if (syncedAt) applyFreshness(folderEl, syncedAt);
222
-
223
- const isOutbox = node.specialUse === "outbox" || node.path.toLowerCase() === "outbox";
224
- if (isOutbox && node.totalCount > 0) {
225
- // Outbox: show total (pending) count with warning style
226
- const badge = document.createElement("span");
227
- badge.className = "ft-badge ft-badge-outbox";
228
- badge.textContent = String(node.totalCount);
229
- badge.title = `${node.totalCount} pending`;
230
- folderEl.appendChild(badge);
231
- } else if (node.unreadCount > 0) {
232
- const badge = document.createElement("span");
233
- badge.className = "ft-badge";
234
- badge.textContent = String(node.unreadCount);
235
- badge.title = `${node.unreadCount} unread`;
236
- folderEl.appendChild(badge);
237
- }
238
- // Total count (shown when View > Folder counts is checked)
239
- if (node.totalCount > 0) {
240
- const total = document.createElement("span");
241
- total.className = "ft-total-count";
242
- total.textContent = String(node.totalCount);
243
- total.title = `${node.totalCount} total messages`;
244
- folderEl.appendChild(total);
245
- }
246
-
247
- folderEl.addEventListener("click", () => {
248
- if (node.id === -1) {
249
- // Virtual parent — toggle expand instead of selecting
250
- expandState[expandKey] = !isExpanded;
251
- saveExpandState();
252
- const treeContainer = document.getElementById("folder-tree");
253
- if (treeContainer) loadFolderTree(treeContainer);
254
- return;
255
- }
256
- if (selectedElement) selectedElement.classList.remove("selected");
257
- folderEl.classList.add("selected");
258
- selectedElement = folderEl;
259
- selectedAccountId = node.accountId;
260
- selectedFolderId = node.id;
261
- onFolderSelect(node.accountId, node.id, node.name, node.specialUse || node.path.toLowerCase());
262
- });
263
-
264
- // ── Right-click context menu ──
265
- folderEl.addEventListener("contextmenu", (e) => {
266
- e.preventDefault();
267
- e.stopPropagation();
268
- const isTrash = node.specialUse === "trash" || node.path.toLowerCase().includes("trash");
269
- const isJunk = node.specialUse === "junk" || node.path.toLowerCase().includes("spam") || node.path.toLowerCase().includes("junk");
270
-
271
- const items: MenuItem[] = [
272
- { label: "Mark all read", action: async () => {
273
- try {
274
- await markFolderRead(node.accountId, node.id);
275
- const treeContainer = document.getElementById("folder-tree");
276
- if (treeContainer) loadFolderTree(treeContainer);
277
- } catch { /* ignore */ }
278
- }},
279
- { label: "", action: () => {}, separator: true },
280
- { label: "New subfolder...", action: async () => {
281
- const name = prompt("New folder name:");
282
- if (!name) return;
283
- try {
284
- await createFolder(node.accountId, node.path, name);
285
- const treeContainer = document.getElementById("folder-tree");
286
- if (treeContainer) loadFolderTree(treeContainer);
287
- } catch (err: any) { alert(`Failed: ${err.message}`); }
288
- }},
289
- { label: "Rename...", action: async () => {
290
- const newName = prompt("Rename folder:", node.name);
291
- if (!newName || newName === node.name) return;
292
- try {
293
- await renameFolder(node.accountId, node.id, newName);
294
- const treeContainer = document.getElementById("folder-tree");
295
- if (treeContainer) loadFolderTree(treeContainer);
296
- } catch (err: any) { alert(`Failed: ${err.message}`); }
297
- }, disabled: !!node.specialUse },
298
- { label: "Delete folder", action: async () => {
299
- if (!confirm(`Delete folder "${node.name}"? Messages will be moved to Trash.`)) return;
300
- try {
301
- await deleteFolder(node.accountId, node.id);
302
- const treeContainer = document.getElementById("folder-tree");
303
- if (treeContainer) loadFolderTree(treeContainer);
304
- } catch (err: any) { alert(`Failed: ${err.message}`); }
305
- }, disabled: !!node.specialUse },
306
- { label: "", action: () => {}, separator: true },
307
- // Q57: copy IMAP path so user can paste into accounts.jsonc as
308
- // a spam/sent/drafts/trash hint without retyping case-sensitively.
309
- { label: "Copy folder path", action: async () => {
310
- try {
311
- await navigator.clipboard.writeText(node.path);
312
- const status = document.getElementById("status-sync");
313
- if (status) status.textContent = `Copied: ${node.path}`;
314
- } catch {
315
- prompt("Folder path:", node.path);
316
- }
317
- }},
318
- ];
319
-
320
- if (isTrash || isJunk) {
321
- items.push({ label: "", action: () => {}, separator: true });
322
- items.push({ label: `Empty ${node.name}`, action: async () => {
323
- if (!confirm(`Permanently delete all messages in "${node.name}"?`)) return;
324
- try {
325
- await emptyFolder(node.accountId, node.id);
326
- const { setMessages } = await import("../lib/message-state.js");
327
- setMessages([]); // Folder emptied — clear list and viewer
328
- const treeContainer = document.getElementById("folder-tree");
329
- if (treeContainer) loadFolderTree(treeContainer);
330
- } catch (err: any) { alert(`Failed: ${err.message}`); }
331
- }});
332
- }
333
-
334
- showContextMenu(e.clientX, e.clientY, items);
335
- });
336
-
337
- // ── Drop target for message drag-and-drop ──
338
- if (node.id !== -1) {
339
- let dragExpandTimer: ReturnType<typeof setTimeout> | null = null;
340
- folderEl.addEventListener("dragover", (e) => {
341
- e.preventDefault();
342
- e.dataTransfer!.dropEffect = e.ctrlKey ? "copy" : "move";
343
- folderEl.classList.add("drop-target");
344
- });
345
- folderEl.addEventListener("dragenter", () => {
346
- if (hasChildren && !isExpanded && !dragExpandTimer) {
347
- dragExpandTimer = setTimeout(() => {
348
- dragExpandTimer = null;
349
- expandState[expandKey] = true;
350
- saveExpandState();
351
- const treeContainer = document.getElementById("folder-tree");
352
- if (treeContainer) loadFolderTree(treeContainer);
353
- }, 500);
354
- }
355
- });
356
- folderEl.addEventListener("dragleave", () => {
357
- folderEl.classList.remove("drop-target");
358
- if (dragExpandTimer) { clearTimeout(dragExpandTimer); dragExpandTimer = null; }
359
- });
360
- folderEl.addEventListener("drop", async (e) => {
361
- e.preventDefault();
362
- folderEl.classList.remove("drop-target");
363
- if (dragExpandTimer) { clearTimeout(dragExpandTimer); dragExpandTimer = null; }
364
-
365
- // Multi-message or single-message drop
366
- const multiData = e.dataTransfer!.getData("application/x-mailx-messages");
367
- const singleData = e.dataTransfer!.getData("application/x-mailx-message");
368
- const messages: { accountId: string; uid: number; folderId: number }[] =
369
- multiData ? JSON.parse(multiData) : singleData ? [JSON.parse(singleData)] : [];
370
-
371
- // Filter: not already in target folder
372
- const toMove = messages.filter(m => m.folderId !== node.id || m.accountId !== node.accountId);
373
- if (toMove.length === 0) return;
374
-
375
- const statusEl = document.getElementById("status-sync");
376
- const crossAccount = toMove.some(m => m.accountId !== node.accountId);
377
- try {
378
- if (crossAccount) {
379
- // Cross-account: must do one at a time
380
- for (const msg of toMove) {
381
- const targetAccountId = msg.accountId !== node.accountId ? node.accountId : undefined;
382
- await moveMessage(msg.accountId, msg.uid, node.id, targetAccountId);
383
- }
384
- } else {
385
- // Same account: bulk move
386
- const accountId = toMove[0].accountId;
387
- const uids = toMove.map(m => m.uid);
388
- await moveMessages(accountId, uids, node.id);
389
- }
390
- const moved = toMove.length;
391
- if (statusEl) statusEl.textContent = `Moved ${moved} message${moved > 1 ? "s" : ""} to ${node.name} — Ctrl+Z to undo`;
392
- // Remove from shared state — list and viewer update automatically
393
- const { removeMessages } = await import("../lib/message-state.js");
394
- removeMessages(toMove);
395
- const treeContainer = document.getElementById("folder-tree");
396
- if (treeContainer) loadFolderTree(treeContainer);
397
- // Notify app.ts so Ctrl+Z can undo this move. Each entry carries
398
- // its ORIGINAL folderId/accountId so we know where to move back to.
399
- document.dispatchEvent(new CustomEvent("mailx-moved", {
400
- detail: {
401
- messages: toMove.map(m => ({ accountId: m.accountId, uid: m.uid, sourceFolderId: m.folderId })),
402
- targetAccountId: node.accountId,
403
- targetFolderId: node.id,
404
- },
405
- }));
406
- } catch (err: any) {
407
- console.error(`Move failed: ${err.message}`);
408
- if (statusEl) statusEl.textContent = `Move failed: ${err.message}`;
409
- }
410
- });
411
- }
412
-
413
- container.appendChild(folderEl);
414
-
415
- // Render children if expanded
416
- if (hasChildren && isExpanded) {
417
- for (const child of node.children) {
418
- renderNode(child, container, depth + 1);
419
- }
420
- }
421
- }
422
-
423
- export function initFolderTree(container: HTMLElement, handler: FolderSelectHandler, unifiedHandler?: UnifiedHandler): void {
424
- onFolderSelect = handler;
425
- onUnifiedInbox = unifiedHandler || null;
426
- loadFolderTree(container);
427
- }
428
-
429
- // Item 12: outbox total drives a synthesized "Send-pending" row at the top
430
- // of the folder tree. The server pushes outboxStatus on every mutation; when
431
- // the total flips between zero and non-zero we re-render so the row appears
432
- // / disappears without waiting for the next full refresh.
433
- let lastOutboxTotal = 0;
434
- export function setOutboxTotal(total: number): void {
435
- const prev = lastOutboxTotal;
436
- lastOutboxTotal = total | 0;
437
- // Zero → zero: nothing to render, nothing to clear.
438
- if (prev === 0 && lastOutboxTotal === 0) return;
439
- const existing = document.getElementById("ft-send-pending") as HTMLElement;
440
- // Non-zero in both → just update the badge text; avoid a full re-render.
441
- if (prev > 0 && lastOutboxTotal > 0 && existing) {
442
- const badge = existing.querySelector<HTMLElement>(".ft-badge");
443
- if (badge) badge.textContent = String(lastOutboxTotal);
444
- existing.title = `${lastOutboxTotal} message${lastOutboxTotal === 1 ? "" : "s"} queued for send`;
445
- return;
446
- }
447
- // Presence flipped (0→N or N→0) — re-render to insert / remove the row.
448
- const container = document.getElementById("folder-tree");
449
- if (container) loadFolderTree(container);
450
- }
451
-
452
- async function loadFolderTree(container: HTMLElement): Promise<void> {
453
- // Show loading state while preserving existing tree (if any) on refresh
454
- const hadContent = container.children.length > 0 && !container.querySelector(".folder-loading");
455
- if (!hadContent) {
456
- container.innerHTML = `<div class="folder-loading">Loading accounts...</div>`;
457
- }
458
-
459
- try {
460
- let accounts = await getAccounts();
461
-
462
- // Accounts may still be registering (OAuth in progress) — retry a few times
463
- if (accounts.length === 0) {
464
- for (let retry = 0; retry < 5 && accounts.length === 0; retry++) {
465
- await new Promise(r => setTimeout(r, 2000));
466
- accounts = await getAccounts();
467
- }
468
- }
469
-
470
- if (accounts.length === 0) {
471
- container.innerHTML = `<div class="folder-loading">No accounts</div>`;
472
- // Hide the message list and show setup in the viewer pane (full width)
473
- const mlSection = document.querySelector(".message-list") as HTMLElement;
474
- if (mlSection) mlSection.style.display = "none";
475
- const splitter = document.getElementById("splitter-h");
476
- if (splitter) splitter.style.display = "none";
477
- const mvHeader = document.getElementById("mv-header");
478
- if (mvHeader) mvHeader.style.display = "none";
479
- const mainBody = document.getElementById("mv-body");
480
- if (mainBody) {
481
- const isAndroid = (window as any).mailxapi?.platform === "android";
482
- const formDisplay = isAndroid ? "display:none;" : "";
483
- const introText = isAndroid ? "" : "Add your email account to get started.";
484
- const checkingHtml = isAndroid ? '<div style="padding:0.5rem;color:var(--color-text-muted)">Checking for accounts...</div>' : "";
485
- mainBody.innerHTML = `<div style="padding:2rem;line-height:1.8;max-width:500px">
486
- <h2 style="margin-bottom:1rem">Welcome to mailx</h2>
487
- <div id="setup-device-accounts">${checkingHtml}</div>
488
- <div id="setup-cloud-status"></div>
489
- <p id="setup-form-intro">${introText}</p>
490
- <form id="setup-form" style="margin-top:1rem;${formDisplay}">
491
- <label style="display:block;margin-bottom:0.5rem">
492
- Email address
493
- <input id="setup-email" type="email" placeholder="you@gmail.com" required style="display:block;width:100%;padding:0.5rem;margin-top:0.25rem;background:var(--color-bg-surface);color:var(--color-text);border:1px solid var(--color-border);border-radius:4px">
494
- </label>
495
- <div id="setup-provider-preview" style="display:none;margin-bottom:0.5rem;padding:0.4rem 0.6rem;background:var(--color-bg-surface);border:1px solid var(--color-border);border-radius:4px;font-size:0.9rem">
496
- <span id="setup-provider-icon" style="display:inline-block;width:1.2em;text-align:center;margin-right:0.4em"></span><span id="setup-provider-label"></span>
497
- </div>
498
- <label id="setup-name-row" style="display:none;margin-bottom:0.5rem">
499
- Your name <span style="color:var(--color-text-muted);font-size:0.85rem">(optional — auto-detected from Google)</span>
500
- <input id="setup-name" type="text" placeholder="Your Name (leave blank to use Google profile)" style="display:block;width:100%;padding:0.5rem;margin-top:0.25rem;background:var(--color-bg-surface);color:var(--color-text);border:1px solid var(--color-border);border-radius:4px">
501
- </label>
502
- <label id="setup-password-row" style="display:none;margin-bottom:0.5rem">
503
- Password
504
- <input id="setup-password" type="password" placeholder="password" style="display:block;width:100%;padding:0.5rem;margin-top:0.25rem;background:var(--color-bg-surface);color:var(--color-text);border:1px solid var(--color-border);border-radius:4px">
505
- <div id="setup-app-password-help" style="display:none;margin-top:0.25rem;font-size:0.8rem;color:var(--color-text-muted)"></div>
506
- </label>
507
- <button id="setup-submit" type="submit" style="display:none;margin-top:1rem;padding:0.5rem 2rem;background:var(--color-accent);color:#fff;border:none;border-radius:4px;cursor:pointer;font-size:1rem">Add Account</button>
508
- <div id="setup-status" style="margin-top:1rem;color:var(--color-text-muted)"></div>
509
- </form>
510
- <details style="margin-top:2rem;color:var(--color-text-muted)">
511
- <summary>Manual setup (advanced)</summary>
512
- <p style="margin-top:0.5rem">Create <code>~/.mailx/config.jsonc</code> with a cloud provider:</p>
513
- <code style="display:block;padding:0.75rem;background:var(--color-bg-surface);border:1px solid var(--color-border);border-radius:4px;margin:0.5rem 0;white-space:pre;font-size:0.85rem">{ "sharedDir": { "provider": "gdrive", "path": "home/.mailx" } }</code>
514
- <p style="margin-top:0.5rem;font-size:0.85rem">Settings sync via Google Drive API (auto-configured for Gmail accounts).</p>
515
- </details>
516
- </div>`;
517
- // Wire up the setup form
518
- const form = document.getElementById("setup-form") as HTMLFormElement;
519
- const emailInput = document.getElementById("setup-email") as HTMLInputElement;
520
- const statusEl = document.getElementById("setup-status")!;
521
- // Hide password for OAuth providers, show app-password help for others
522
- const APP_PASSWORD_HELP: Record<string, string> = {
523
- "yahoo.com": "Use an app password: Yahoo Settings → Account Security → Generate app password",
524
- "aol.com": "Use an app password: AOL Settings → Account Security → Generate app password",
525
- "icloud.com": "Use an app-specific password: appleid.apple.com → Sign-In and Security → App-Specific Passwords",
526
- };
527
- // Q67: describe the detected provider so the user knows which
528
- // auto-config path we're about to take BEFORE they hit Next.
529
- // Gmail / Google Workspace domains auto-detect via MX in the
530
- // service; here we can only name the known ones up front and
531
- // say "will auto-detect" for everything else.
532
- const PROVIDER_PREVIEW: Record<string, { icon: string; label: string }> = {
533
- "gmail.com": { icon: "✉", label: "Gmail — OAuth (no password needed)" },
534
- "googlemail.com": { icon: "✉", label: "Gmail — OAuth (no password needed)" },
535
- "outlook.com": { icon: "✉", label: "Outlook.com — OAuth (no password needed)" },
536
- "hotmail.com": { icon: "✉", label: "Outlook.com — OAuth (no password needed)" },
537
- "live.com": { icon: "✉", label: "Outlook.com — OAuth (no password needed)" },
538
- "yahoo.com": { icon: "✉", label: "Yahoo Mail — IMAP (needs app password)" },
539
- "aol.com": { icon: "✉", label: "AOL Mail — IMAP (needs app password)" },
540
- "icloud.com": { icon: "✉", label: "iCloud Mail — IMAP (needs app-specific password)" },
541
- "me.com": { icon: "✉", label: "iCloud Mail — IMAP (needs app-specific password)" },
542
- "mac.com": { icon: "✉", label: "iCloud Mail — IMAP (needs app-specific password)" },
543
- };
544
- let oauthAutoFired = false;
545
- emailInput?.addEventListener("input", () => {
546
- const email = emailInput.value.trim();
547
- const domain = email.split("@")[1]?.toLowerCase() || "";
548
- const hasAt = email.includes("@") && domain.length > 0;
549
- const isOAuth = ["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com"].includes(domain);
550
- const isGmailLike = ["gmail.com", "googlemail.com"].includes(domain);
551
- // Provider preview row
552
- const preview = document.getElementById("setup-provider-preview");
553
- const icon = document.getElementById("setup-provider-icon");
554
- const label = document.getElementById("setup-provider-label");
555
- if (preview && icon && label) {
556
- if (hasAt) {
557
- const hit = PROVIDER_PREVIEW[domain];
558
- icon.textContent = hit ? hit.icon : "❓";
559
- label.textContent = hit ? hit.label : `${domain} — will auto-detect via MX records`;
560
- preview.style.display = "block";
561
- } else {
562
- preview.style.display = "none";
563
- }
564
- }
565
- // OAuth providers: auto-fire setup immediately once domain
566
- // is recognized — don't show name/password (name is auto-
567
- // detected from Google profile, no password needed). This
568
- // eliminates the "form flash" where fields briefly appear
569
- // before the page reloads.
570
- if (hasAt && isOAuth && !oauthAutoFired && !setupTriggered) {
571
- oauthAutoFired = true;
572
- statusEl.textContent = `Connecting to ${isGmailLike ? "Gmail" : "Outlook"}...`;
573
- trySetup();
574
- return;
575
- }
576
- // Non-OAuth: progressive reveal of name + password + submit.
577
- const nameRow = document.getElementById("setup-name-row");
578
- const pwRow = document.getElementById("setup-password-row");
579
- const submitBtn = document.getElementById("setup-submit");
580
- if (nameRow) nameRow.style.display = hasAt && !isOAuth ? "block" : "none";
581
- if (pwRow) pwRow.style.display = hasAt && !isOAuth ? "block" : "none";
582
- if (submitBtn) submitBtn.style.display = hasAt && !isOAuth ? "block" : "none";
583
- const helpEl = document.getElementById("setup-app-password-help");
584
- if (helpEl) {
585
- const help = APP_PASSWORD_HELP[domain];
586
- helpEl.style.display = help ? "block" : "none";
587
- helpEl.textContent = help || "";
588
- }
589
- });
590
- // When a valid email is entered, try setup immediately — if cloud has
591
- // existing accounts, loads them without needing name/password
592
- let setupTriggered = false;
593
- async function trySetup(): Promise<void> {
594
- const email = emailInput.value.trim();
595
- if (!email || !email.includes("@") || setupTriggered) return;
596
- const name = (document.getElementById("setup-name") as HTMLInputElement).value.trim();
597
- const password = (document.getElementById("setup-password") as HTMLInputElement).value;
598
- setupTriggered = true;
599
- statusEl.textContent = "Checking for existing accounts...";
600
- try {
601
- const data = await setupAccount(name, email, password);
602
- if (data.ok) {
603
- statusEl.textContent = data.message || "Accounts loaded! Syncing...";
604
- setTimeout(() => location.reload(), 5000);
605
- } else {
606
- setupTriggered = false;
607
- statusEl.textContent = `Error: ${data.error || "Setup failed"}`;
608
- }
609
- } catch (err: any) {
610
- setupTriggered = false;
611
- statusEl.textContent = `Error: ${err.message}`;
612
- }
613
- }
614
- form?.addEventListener("submit", async (e) => {
615
- e.preventDefault();
616
- await trySetup();
617
- });
618
- // Auto-trigger for OAuth providers when email looks complete
619
- emailInput?.addEventListener("change", async () => {
620
- const domain = emailInput.value.split("@")[1]?.toLowerCase() || "";
621
- const isOAuth = ["gmail.com", "googlemail.com", "outlook.com", "hotmail.com", "live.com"].includes(domain);
622
- if (isOAuth) await trySetup();
623
- });
624
- // Show cloud storage status in setup form
625
- getVersion().then((d: any) => {
626
- const cloudEl = document.getElementById("setup-cloud-status");
627
- if (!cloudEl) return;
628
- const s = d.storage || {};
629
- if (s.cloudError) {
630
- cloudEl.innerHTML = `<div style="padding:0.75rem;margin-bottom:1rem;background:#5c1a1a;color:#fca;border:1px solid #a33;border-radius:4px">
631
- <strong>Cloud storage unavailable:</strong> ${s.cloudError}<br>
632
- <span style="font-size:0.85rem">Settings on ${s.provider || "cloud"} cannot be read. Add an account below to initialize cloud storage.</span>
633
- </div>`;
634
- } else if (s.mode === "api") {
635
- cloudEl.innerHTML = `<div style="padding:0.5rem;margin-bottom:1rem;background:var(--color-bg-surface);border:1px solid var(--color-border);border-radius:4px;color:var(--color-text-muted)">
636
- Using ${s.provider} API (no local mount)
637
- </div>`;
638
- }
639
- }).catch(() => {});
640
- // On Android, check for device Google accounts
641
- getDeviceAccounts().then(async (deviceAccounts) => {
642
- const pickerEl = document.getElementById("setup-device-accounts");
643
- if (!pickerEl) return;
644
- if (deviceAccounts.length === 0) {
645
- // No device accounts — show the form
646
- pickerEl.innerHTML = "";
647
- const f = document.getElementById("setup-form") as HTMLElement;
648
- const i = document.getElementById("setup-form-intro") as HTMLElement;
649
- if (f) f.style.display = "block";
650
- if (i) i.textContent = "Add your email account to get started.";
651
- return;
652
- }
653
- const formEl = document.getElementById("setup-form") as HTMLElement;
654
- const introEl = document.getElementById("setup-form-intro") as HTMLElement;
655
-
656
- // Auto-setup helper
657
- async function autoSetup(email: string, name: string): Promise<void> {
658
- if (introEl) introEl.textContent = "";
659
- if (formEl) formEl.style.display = "none";
660
- pickerEl.innerHTML = `<div style="padding:0.5rem;color:var(--color-text-muted)">Setting up ${email}...</div>`;
661
- const result = await setupAccount(name, email, "");
662
- if (result?.ok) {
663
- pickerEl.innerHTML = `<div style="padding:0.5rem;color:var(--color-accent)">${result.message || "Account added!"}</div>`;
664
- setTimeout(() => location.reload(), 2000);
665
- } else {
666
- pickerEl.innerHTML = `<div style="padding:0.5rem;color:#f55">${result?.error || "Setup failed"}</div>`;
667
- if (formEl) formEl.style.display = "block";
668
- }
669
- }
670
-
671
- // One account — auto-select it
672
- if (deviceAccounts.length === 1) {
673
- await autoSetup(deviceAccounts[0].email, deviceAccounts[0].name);
674
- return;
675
- }
676
-
677
- // Multiple accounts — show picker
678
- if (introEl) introEl.textContent = "Select an account:";
679
- if (formEl) formEl.style.display = "none";
680
- pickerEl.innerHTML = deviceAccounts.map((a: { email: string; name: string }) =>
681
- `<button class="device-account-btn" data-email="${a.email}" data-name="${a.name}" style="display:block;width:100%;padding:0.75rem 1rem;margin-bottom:0.5rem;background:var(--color-accent);color:#fff;border:none;border-radius:6px;cursor:pointer;font-size:1rem;text-align:left">Use ${a.email}</button>`
682
- ).join("") + `<button id="setup-show-form" style="margin-top:0.5rem;padding:0.5rem 1rem;background:none;color:var(--color-text-muted);border:none;cursor:pointer;font-size:0.9rem">Use a different account...</button>`;
683
- pickerEl.querySelectorAll(".device-account-btn").forEach((btn: Element) => {
684
- btn.addEventListener("click", async () => {
685
- await autoSetup((btn as HTMLElement).dataset.email || "", (btn as HTMLElement).dataset.name || "");
686
- });
687
- });
688
- document.getElementById("setup-show-form")?.addEventListener("click", () => {
689
- pickerEl.style.display = "none";
690
- if (formEl) formEl.style.display = "block";
691
- if (introEl) introEl.textContent = "Add your email account to get started.";
692
- });
693
- }).catch(() => {
694
- // Bridge failed — show the form
695
- const p = document.getElementById("setup-device-accounts");
696
- if (p) p.innerHTML = "";
697
- const f = document.getElementById("setup-form") as HTMLElement;
698
- const i = document.getElementById("setup-form-intro") as HTMLElement;
699
- if (f) f.style.display = "block";
700
- if (i) i.textContent = "Add your email account to get started.";
701
- });
702
- }
703
- // Dismiss startup overlay
704
- const overlay = document.getElementById("startup-overlay");
705
- if (overlay) { overlay.classList.add("hidden"); setTimeout(() => overlay.remove(), 400); }
706
- return;
707
- }
708
-
709
- // Fetch ALL account folder data in parallel BEFORE touching the DOM
710
- const accountFolderData: { account: any; folders: any[] }[] = await Promise.all(
711
- accounts.map(async (account: any) => {
712
- const accountKey = `account:${account.id}`;
713
- const accountExpanded = expandState[accountKey] !== false;
714
- const folders = accountExpanded ? await getFolders(account.id) : [];
715
- return { account, folders };
716
- })
717
- );
718
-
719
- // Save scroll position before rebuild
720
- const savedScroll = container.scrollTop;
721
-
722
- // Build entire new tree into a DocumentFragment (off-screen, no reflows)
723
- const fragment = document.createDocumentFragment();
724
-
725
- // Unified Inbox — always shown so startup auto-selects it consistently
726
- // (with one account it's effectively that account's INBOX, but the UI
727
- // stays uniform so the auto-select path doesn't fork on account count)
728
- if (accounts.length >= 1) {
729
- const unifiedEl = document.createElement("div");
730
- unifiedEl.className = "ft-folder ft-unified";
731
- unifiedEl.title = accounts.length > 1
732
- ? "Merged inbox view of all accounts — click to see messages from every account's INBOX sorted by date"
733
- : "Inbox view across all your accounts";
734
- unifiedEl.innerHTML = `<span class="ft-toggle"> </span><span class="ft-folder-name">All Inboxes</span>`;
735
- unifiedEl.addEventListener("click", () => {
736
- if (selectedElement) selectedElement.classList.remove("selected");
737
- unifiedEl.classList.add("selected");
738
- selectedElement = unifiedEl;
739
- selectedAccountId = null;
740
- selectedFolderId = -1;
741
- if (onUnifiedInbox) onUnifiedInbox();
742
- });
743
- fragment.appendChild(unifiedEl);
744
- }
745
-
746
- // Item 12: Send-pending virtual row — synthesized from the outbox
747
- // queue, only shown when something is actually queued. Clicking
748
- // opens the outbox-view modal (pink rows, cancellable). Lives at
749
- // the top of the tree so a stuck send is impossible to miss.
750
- if (lastOutboxTotal > 0) {
751
- const pendingEl = document.createElement("div");
752
- pendingEl.className = "ft-folder ft-unified ft-send-pending";
753
- pendingEl.id = "ft-send-pending";
754
- pendingEl.title = `${lastOutboxTotal} message${lastOutboxTotal === 1 ? "" : "s"} queued for send`;
755
- pendingEl.innerHTML = `<span class="ft-toggle"> </span><span class="ft-folder-name">Send-pending</span><span class="ft-badge ft-badge-outbox">${lastOutboxTotal}</span>`;
756
- pendingEl.addEventListener("click", async () => {
757
- try {
758
- const { openOutboxView } = await import("./outbox-view.js");
759
- openOutboxView();
760
- } catch { /* outbox-view load failed — silent is OK, status pill still works */ }
761
- });
762
- fragment.appendChild(pendingEl);
763
- }
764
-
765
- for (const { account, folders } of accountFolderData) {
766
- const accountEl = document.createElement("div");
767
- accountEl.className = "ft-account";
768
-
769
- const accountKey = `account:${account.id}`;
770
- const accountExpanded = expandState[accountKey] !== false; // accounts default expanded
771
-
772
- const header = document.createElement("div");
773
- header.className = "ft-account-header";
774
- header.textContent = `${accountExpanded ? "▾" : "▸"} ${(account as any).label || account.name}`;
775
- header.addEventListener("click", () => {
776
- expandState[accountKey] = !accountExpanded;
777
- saveExpandState();
778
- const treeContainer = document.getElementById("folder-tree");
779
- if (treeContainer) loadFolderTree(treeContainer);
780
- });
781
-
782
- // Right-click: full menu instead of plain toggle (Q54).
783
- header.addEventListener("contextmenu", (e) => {
784
- e.preventDefault();
785
- e.stopPropagation();
786
- const items: MenuItem[] = [
787
- { label: "Mark all read (account)", action: async () => {
788
- const folderRows = folders.slice();
789
- for (const f of folderRows) {
790
- try { await markFolderRead(account.id, f.id); } catch { /* keep going */ }
791
- }
792
- const tc = document.getElementById("folder-tree");
793
- if (tc) loadFolderTree(tc);
794
- }},
795
- { label: "", action: () => {}, separator: true },
796
- { label: "Expand all folders", action: () => {
797
- const keys = Object.keys(expandState).filter(k => k.startsWith(`${account.id}:`));
798
- for (const k of keys) expandState[k] = true;
799
- expandState[accountKey] = true;
800
- saveExpandState();
801
- const tc = document.getElementById("folder-tree");
802
- if (tc) loadFolderTree(tc);
803
- }},
804
- { label: "Collapse all folders", action: () => {
805
- const keys = Object.keys(expandState).filter(k => k.startsWith(`${account.id}:`));
806
- for (const k of keys) expandState[k] = false;
807
- expandState[accountKey] = false;
808
- saveExpandState();
809
- const tc = document.getElementById("folder-tree");
810
- if (tc) loadFolderTree(tc);
811
- }},
812
- { label: "", action: () => {}, separator: true },
813
- { label: "Sync this account now", action: async () => {
814
- try { await syncAccount(account.id); } catch (err: any) { alert(`Sync failed: ${err?.message || err}`); }
815
- }},
816
- ];
817
- showContextMenu(e.clientX, e.clientY, items);
818
- });
819
-
820
- accountEl.appendChild(header);
821
-
822
- if (accountExpanded && folders.length > 0) {
823
- const delimiter = folders[0]?.delimiter || ".";
824
- const tree = buildTree(folders, delimiter, account.id);
825
- sortFolders(tree);
826
-
827
- // Case-duplicate detection: fold folder paths to lowercase and
828
- // flag any whose form matches another. Common with servers that
829
- // let users create `Archive` and `archive` as distinct folders,
830
- // or `Sent Items` alongside a `Sent items` rename gone sideways.
831
- // A ⚠ glyph on the affected rows lets the user notice before
832
- // losing mail to the wrong one.
833
- const lowerCounts = new Map<string, number>();
834
- for (const f of folders) {
835
- const key = (f.path || "").toLowerCase();
836
- lowerCounts.set(key, (lowerCounts.get(key) || 0) + 1);
837
- }
838
- const duplicatePaths = new Set<string>();
839
- for (const [k, c] of lowerCounts) if (c > 1) duplicatePaths.add(k);
840
-
841
- for (const node of tree) {
842
- renderNode(node, accountEl, 1);
843
- }
844
-
845
- if (duplicatePaths.size > 0) {
846
- accountEl.querySelectorAll<HTMLElement>(".ft-folder").forEach(el => {
847
- const p = (el.dataset.folderPath || "").toLowerCase();
848
- if (duplicatePaths.has(p)) {
849
- el.classList.add("ft-folder-duplicate");
850
- el.title = (el.title ? el.title + " — " : "") +
851
- "Case-duplicate folder name on the server (another folder with the same name in different case exists)";
852
- }
853
- });
854
- }
855
- }
856
-
857
- fragment.appendChild(accountEl);
858
- }
859
-
860
- // Atomic swap — single reflow, no intermediate empty state
861
- container.replaceChildren(fragment);
862
-
863
- // Restore scroll position
864
- container.scrollTop = savedScroll;
865
-
866
- // Re-select previous folder, or auto-select on first load
867
- const allFolderEls = container.querySelectorAll('.ft-folder');
868
- let target: HTMLElement | null = null;
869
-
870
- if (selectedFolderId === -1) {
871
- // Unified inbox was selected — just re-highlight it, don't click
872
- const unified = container.querySelector('.ft-unified') as HTMLElement;
873
- if (unified) {
874
- unified.classList.add("selected");
875
- selectedElement = unified;
876
- target = unified;
877
- }
878
- } else if (selectedAccountId && selectedFolderId !== null && selectedFolderId >= 0) {
879
- for (const f of allFolderEls) {
880
- const el = f as HTMLElement;
881
- if (el.dataset.accountId === selectedAccountId && el.dataset.folderId === String(selectedFolderId)) {
882
- el.classList.add("selected");
883
- selectedElement = el;
884
- target = el;
885
- break;
886
- }
887
- }
888
- }
889
-
890
- // Auto-select on first load OR until we successfully auto-selected at least once
891
- // (handles Android where folders don't exist on first load — they arrive after sync)
892
- if (!target && (isFirstLoad || !hasAutoSelected)) {
893
- // Auto-select only on first load — not on refresh (prevents jumping)
894
- const unified = container.querySelector('.ft-unified') as HTMLElement;
895
- if (unified) {
896
- target = unified;
897
- } else {
898
- let bestInbox: HTMLElement | null = null;
899
- let bestCount = -1;
900
- for (const f of allFolderEls) {
901
- const name = f.querySelector('.ft-folder-name')?.textContent ?? "";
902
- if (name.toLowerCase() === "inbox") {
903
- const badge = f.querySelector('.ft-badge');
904
- const count = badge ? parseInt(badge.textContent || "0") : 0;
905
- if (count > bestCount) {
906
- bestCount = count;
907
- bestInbox = f as HTMLElement;
908
- }
909
- }
910
- }
911
- target = bestInbox;
912
- }
913
- if (!target && allFolderEls.length > 0) target = allFolderEls[0] as HTMLElement;
914
- if (target) {
915
- target.click();
916
- hasAutoSelected = true;
917
- }
918
- }
919
- isFirstLoad = false;
920
- // Dismiss startup overlay once tree is loaded
921
- const overlay = document.getElementById("startup-overlay");
922
- if (overlay) overlay.classList.add("hidden");
923
- // Remove from DOM after transition
924
- setTimeout(() => overlay?.remove(), 400);
925
-
926
- } catch (e: any) {
927
- // Don't destroy existing folder tree on error — just log it
928
- console.error(`Folder tree error: ${e.message}`);
929
- // Only show error if tree is completely empty (first load failure)
930
- if (container.children.length === 0 || container.querySelector(".folder-loading")) {
931
- const errEl = document.createElement("div");
932
- errEl.className = "folder-loading";
933
- errEl.textContent = `Error loading folders: ${e.message}`;
934
- container.replaceChildren(errEl);
935
- }
936
- // Dismiss overlay on error too
937
- const overlay = document.getElementById("startup-overlay");
938
- if (overlay) {
939
- const status = document.getElementById("startup-status");
940
- if (status) status.textContent = `Error: ${e.message}`;
941
- setTimeout(() => { overlay.classList.add("hidden"); setTimeout(() => overlay.remove(), 400); }, 2000);
942
- }
943
- }
944
- }
945
-
946
- /** Refresh folder tree (e.g., after sync) — debounced to prevent rapid rebuilds */
947
- export function refreshFolderTree(): void {
948
- if (refreshDebounceTimer) clearTimeout(refreshDebounceTimer);
949
- refreshDebounceTimer = setTimeout(() => {
950
- refreshDebounceTimer = null;
951
- const container = document.getElementById("folder-tree");
952
- if (container) loadFolderTree(container);
953
- }, 300);
954
- }
955
-
956
- /**
957
- * Incremental count update — patches badge counts in-place without rebuilding the DOM.
958
- * Used for folderCountsChanged events to avoid jitter. Falls back to full rebuild
959
- * if the folder structure has changed.
960
- */
961
- export async function updateFolderCounts(): Promise<void> {
962
- const container = document.getElementById("folder-tree");
963
- if (!container) return;
964
-
965
- // If tree hasn't loaded yet, do a full load
966
- if (container.children.length === 0 || container.querySelector(".folder-loading")) {
967
- refreshFolderTree();
968
- return;
969
- }
970
-
971
- try {
972
- const accounts = await getAccounts();
973
-
974
- // Fetch all folder data in parallel
975
- const allFolderData = await Promise.all(
976
- accounts.map(async (account: any) => {
977
- const folders = await getFolders(account.id);
978
- return { accountId: account.id, folders };
979
- })
980
- );
981
-
982
- // Build a lookup: accountId+folderId → { unreadCount, totalCount }
983
- // Also rebuild trees to get aggregated counts
984
- const countMap = new Map<string, { unread: number; total: number }>();
985
- for (const { accountId, folders } of allFolderData) {
986
- const delimiter = folders[0]?.delimiter || ".";
987
- const tree = buildTree(folders, delimiter, accountId);
988
- // Walk the tree and collect counts (buildTree already aggregates)
989
- function collectCounts(nodes: FolderNode[]): void {
990
- for (const n of nodes) {
991
- countMap.set(`${n.accountId}:${n.id}`, { unread: n.unreadCount, total: n.totalCount });
992
- collectCounts(n.children);
993
- }
994
- }
995
- collectCounts(tree);
996
- }
997
-
998
- // Patch existing DOM elements in-place
999
- const folderEls = container.querySelectorAll(".ft-folder[data-account-id][data-folder-id]");
1000
- let structureChanged = false;
1001
-
1002
- for (const el of folderEls) {
1003
- const htmlEl = el as HTMLElement;
1004
- const key = `${htmlEl.dataset.accountId}:${htmlEl.dataset.folderId}`;
1005
- const counts = countMap.get(key);
1006
- if (!counts) continue; // folder not found — structure may have changed
1007
-
1008
- // Update unread badge
1009
- const isOutbox = htmlEl.dataset.specialUse === "outbox" || htmlEl.dataset.folderPath?.toLowerCase() === "outbox";
1010
- let badge = htmlEl.querySelector(".ft-badge") as HTMLElement;
1011
- const outboxBadge = htmlEl.querySelector(".ft-badge-outbox") as HTMLElement;
1012
-
1013
- if (isOutbox) {
1014
- if (counts.total > 0) {
1015
- if (outboxBadge) {
1016
- outboxBadge.textContent = String(counts.total);
1017
- } else {
1018
- const b = document.createElement("span");
1019
- b.className = "ft-badge ft-badge-outbox";
1020
- b.textContent = String(counts.total);
1021
- htmlEl.querySelector(".ft-folder-name")?.after(b);
1022
- }
1023
- } else if (outboxBadge) {
1024
- outboxBadge.remove();
1025
- }
1026
- } else {
1027
- if (counts.unread > 0) {
1028
- if (badge) {
1029
- badge.textContent = String(counts.unread);
1030
- } else {
1031
- const b = document.createElement("span");
1032
- b.className = "ft-badge";
1033
- b.textContent = String(counts.unread);
1034
- htmlEl.querySelector(".ft-folder-name")?.after(b);
1035
- }
1036
- } else if (badge && !badge.classList.contains("ft-badge-outbox")) {
1037
- badge.remove();
1038
- }
1039
- }
1040
-
1041
- // Update total count
1042
- let totalEl = htmlEl.querySelector(".ft-total-count") as HTMLElement;
1043
- if (counts.total > 0) {
1044
- if (totalEl) {
1045
- totalEl.textContent = String(counts.total);
1046
- } else {
1047
- const t = document.createElement("span");
1048
- t.className = "ft-total-count";
1049
- t.textContent = String(counts.total);
1050
- htmlEl.appendChild(t);
1051
- }
1052
- } else if (totalEl) {
1053
- totalEl.remove();
1054
- }
1055
- }
1056
-
1057
- // Check if folder count changed (new folders added or removed)
1058
- const existingCount = folderEls.length;
1059
- let serverCount = 0;
1060
- for (const { folders } of allFolderData) serverCount += folders.length;
1061
- if (Math.abs(existingCount - serverCount) > 2) {
1062
- // Structure changed significantly — do a full rebuild
1063
- refreshFolderTree();
1064
- }
1065
- } catch {
1066
- // If count update fails, fall back to full rebuild
1067
- refreshFolderTree();
1068
- }
1069
- }